TrxParser.psm1

<#
  .Synopsis
  Parse .trx file to PsCustomObject
 
 .Description
  Parse .trx file that generated from MsTest (Visual Studio) into PsCustomObject
 
 .Parameter Path
  File path of the test result file path. It has to be a .trx file from MsTest.
 
 .Parameter NameSpace
  String of namespace of the .trx in the file.
 
 .EXAMPLE
   # Parse the .trx file
   Get-MsTestResult -Path ".\testResult.trx"
 
 .Example
   # Parse the .trx file with different namespace
   Get-MsTestResult -Path ".\testResult.trx" -NameSpace "http://microsoft.com/schemas/VisualStudio/TeamTest/2020"
#>



function ReadTestResult($Path){
    Write-Debug "Reading $Path and parse as XML"
    [xml]$FileContent = Get-Content -Path $Path
    return $FileContent
}

function ProcessTestResultSummary {
    Write-Debug "Begin processing TestResultSummary tag"
    $TestResultSummary = New-Object -TypeName PsObject
    $TestResultSummary | Add-Member -MemberType NoteProperty -Name TrxFile -Value $Path
    $TestResultSummary | Add-Member -MemberType NoteProperty -Name Outcome -Value $FileContent.TestRun.ResultSummary.outcome
    $TestResultSummary | Add-Member -MemberType NoteProperty -Name Total -Value $FileContent.TestRun.ResultSummary.Counters.total
    $TestResultSummary | Add-Member -MemberType NoteProperty -Name Passed -Value $FileContent.TestRun.ResultSummary.Counters.passed
    $TestResultSummary | Add-Member -MemberType NoteProperty -Name Error -Value $FileContent.TestRun.ResultSummary.Counters.error
    $TestResultSummary | Add-Member -MemberType NoteProperty -Name Failed -Value $FileContent.TestRun.ResultSummary.Counters.failed
    $TestResultSummary | Add-Member -MemberType NoteProperty -Name Timeout -Value $FileContent.TestRun.ResultSummary.Counters.timeout
    $TestResultSummary | Add-Member -MemberType NoteProperty -Name Aborted -Value $FileContent.TestRun.ResultSummary.Counters.aborted
    $TestResultSummary | Add-Member -MemberType NoteProperty -Name Inconclusive -Value $FileContent.TestRun.ResultSummary.Counters.inconclusive
    $TestResultSummary | Add-Member -MemberType NoteProperty -Name PassedButRunAborted -Value $FileContent.TestRun.ResultSummary.Counters.passedButRunAborted
    $TestResultSummary | Add-Member -MemberType NoteProperty -Name NotRunnable -Value $FileContent.TestRun.ResultSummary.Counters.notRunnable
    $TestResultSummary | Add-Member -MemberType NoteProperty -Name NotExecuted -Value $FileContent.TestRun.ResultSummary.Counters.notExecuted
    $TestResultSummary | Add-Member -MemberType NoteProperty -Name Disconnected -Value $FileContent.TestRun.ResultSummary.Counters.disconnected
    $TestResultSummary | Add-Member -MemberType NoteProperty -Name Warning -Value $FileContent.TestRun.ResultSummary.Counters.warning
    $TestResultSummary | Add-Member -MemberType NoteProperty -Name Completed -Value $FileContent.TestRun.ResultSummary.Counters.completed
    $TestResultSummary | Add-Member -MemberType NoteProperty -Name InProgress -Value $FileContent.TestRun.ResultSummary.Counters.inProgress
    $TestResultSummary | Add-Member -MemberType NoteProperty -Name Pending -Value $FileContent.TestRun.ResultSummary.Counters.pending

    return $TestResultSummary
}

function ProcessTestSettings {
    Write-Debug "Begin processing TestSettings tag"
    $TestSettings = New-Object -TypeName PsObject
    $TestSettings | Add-Member -MemberType NoteProperty -Name Name -Value $FileContent.TestRun.TestSettings.name
    $TestSettings | Add-Member -MemberType NoteProperty -Name Id $FileContent.TestRun.TestSettings.id
    $TestSettings | Add-Member -MemberType NoteProperty -Name Description $FileContent.TestRun.TestSettings.Description

    return $TestSettings
}
function ProcessTestTimes {
    Write-Debug "Begin processing Times tag"
    $TestTimes = New-Object -TypeName PsObject

    $StartTime = [datetime]::ParseExact($FileContent.TestRun.Times.start, "yyyy-MM-ddTHH:mm:ss.FFFFFFFK", $null)
    $EndTime = [datetime]::ParseExact($FileContent.TestRun.Times.finish, "yyyy-MM-ddTHH:mm:ss.FFFFFFFK", $null)

    $TestTimes | Add-Member -MemberType NoteProperty -Name StartTime -Value $StartTime
    $TestTimes | Add-Member -MemberType NoteProperty -Name EndTime -Value $EndTime
    $TestTimes | Add-Member -MemberType NoteProperty -Name ElapsedTime -Value ($EndTime - $StartTime)

    return $TestTimes
}

function ProcessFailedTests {
    # Failed test
    Write-Debug "Begin processing failed test..."
    $FailedTests = @()
    foreach($UnitTestResult in $FileContent.SelectNodes('//ns:UnitTestResult[@outcome="Failed"]', $ns)) {
        $FailedTest = New-Object -TypeName PsObject

        $FailedTest | Add-Member -MemberType NoteProperty -Name ExecutionId -Value $UnitTestResult.executionId
        $FailedTest | Add-Member -MemberType NoteProperty -Name Message -Value $UnitTestResult.Output.ErrorInfo.Message
        $FailedTest | Add-Member -MemberType NoteProperty -Name StackTrace -Value $UnitTestResult.Output.ErrorInfo.StackTrace

        $CompleteStackTrace = $UnitTestResult.Output.ErrorInfo.Message + "`n" + $UnitTestResult.Output.ErrorInfo.StackTrace
        $FailedTest | Add-Member -MemberType NoteProperty -Name CompleteStackTrace -Value $CompleteStackTrace

        $FailedTests += ,@($FailedTest)
    }

    Write-Debug "Match with unit test definition..."
    foreach($FailedTest in $FailedTests){
        $XPath = "//ns:UnitTest/ns:Execution[@id='" + $FailedTest.ExecutionId + "']"
        $TestDefinition = $FileContent.SelectNodes($XPath, $ns)
        $CodeBase = $TestDefinition.NextSibling.codeBase
        $TestName = $TestDefinition.NextSibling.name

        $FailedTest | Add-Member -MemberType NoteProperty -Name CodeBase -Value $CodeBase
        $FailedTest | Add-Member -MemberType NoteProperty -Name TestName -Value $TestName
    }

    return $FailedTests
}

function ProcessNotFailedTest {
    Write-Debug "Begin processing non-failed test..."
    # All test result
    $Results = @()
    foreach($UnitTestResult in $FileContent.SelectNodes('//ns:UnitTestResult[@outcome!="Failed"]', $ns)) {
        $Result = New-Object -TypeName PsObject

        $Result | Add-Member -MemberType NoteProperty -Name ExecutionId -Value $UnitTestResult.executionId
        $Result | Add-Member -MemberType NoteProperty -Name TestName -Value $UnitTestResult.testName
        $Result | Add-Member -MemberType NoteProperty -Name Duration -Value $UnitTestResult.duration
        $Result | Add-Member -MemberType NoteProperty -Name Outcome -Value $UnitTestResult.outcome

        $Results += ,@($Result)
    }

    return $Results
}

function ProcessTestResult($FileContent){
    Write-Debug "Build MsTestResult object..."
    $ns = new-object Xml.XmlNamespaceManager $FileContent.NameTable
    $ns.AddNamespace("ns", $NameSpace)

    return @{
        TestSettings = ProcessTestSettings
        TestTimes = ProcessTestTimes
        TestResultSummary = ProcessTestResultSummary
        FailedTests = ProcessFailedTests
        NonFailedTests = ProcessNotFailedTest
    }
}

function Get-MsTestResult {
    <#
    .Synopsis
    Parse .trx file to PsCustomObject
 
    .Description
    Parse .trx file that generated from MsTest (Visual Studio) into PsCustomObject
 
    .Parameter Path
    File path of the test result file path. It has to be a .trx file from MsTest.
 
    .Parameter NameSpace
    String of namespace of the .trx in the file.
 
    .EXAMPLE
    # Parse the .trx file
    Get-MsTestResult -Path ".\testResult.trx"
 
    .Example
    # Parse the .trx file with different namespace
    Get-MsTestResult -Path ".\testResult.trx" -NameSpace "http://microsoft.com/schemas/VisualStudio/TeamTest/2020"
    #>


    param(
        [string]
        [alias('p')]
        [Parameter(Position=0,mandatory=$true)]
        $Path,

        [string]
        [alias('ns')]
        [Parameter(Position=1,mandatory=$false)]
        $NameSpace = "http://microsoft.com/schemas/VisualStudio/TeamTest/2010"
    )

    if ((Test-Path -Path $Path) -ne "True"){
        throw "Cannot find test result file in $Path"
    }

    Write-Debug "Using $Path as the file to be read."
    $FileContent = ReadTestResult($Path)
    Write-Debug "File has been read. Processing the file content..."
    ProcessTestResult($FileContent)
}

Export-ModuleMember -Function Get-MsTestResult