Add functions to merge Pester and Xunit logs (#6854)

This commit is contained in:
Aditya Patwardhan 2018-05-10 17:46:45 -07:00 committed by GitHub
parent db45785341
commit d07a3e7c2f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23

View file

@ -2414,6 +2414,416 @@ function Copy-PSGalleryModules
}
}
function Merge-TestLogs
{
[CmdletBinding()]
param (
[Parameter(Mandatory = $true)]
[ValidateScript({Test-Path $_})]
[string]$XUnitLogPath,
[Parameter(Mandatory = $true)]
[ValidateScript({Test-Path $_})]
[string[]]$NUnitLogPath,
[Parameter()]
[ValidateScript({Test-Path $_})]
[string[]]$AdditionalXUnitLogPath,
[Parameter()]
[string]$OutputLogPath
)
# Convert all the NUnit logs into single object
$convertedNUnit = ConvertFrom-PesterLog -logFile $NUnitLogPath
$xunit = [xml] (Get-Content $XUnitLogPath -ReadCount 0 -Raw)
$strBld = [System.Text.StringBuilder]::new($xunit.assemblies.InnerXml)
foreach($assembly in $convertedNUnit.assembly)
{
$strBld.Append($assembly.ToString()) | Out-Null
}
foreach($path in $AdditionalXUnitLogPath)
{
$addXunit = [xml] (Get-Content $path -ReadCount 0 -Raw)
$strBld.Append($addXunit.assemblies.InnerXml) | Out-Null
}
$xunit.assemblies.InnerXml = $strBld.ToString()
$xunit.Save($OutputLogPath)
}
function ConvertFrom-PesterLog {
[CmdletBinding()]
param (
[Parameter(ValueFromPipeline = $true, Mandatory = $true, Position = 0)]
[string[]]$Logfile,
[Parameter()][switch]$IncludeEmpty,
[Parameter()][switch]$MultipleLog
)
<#
Convert our test logs to
xunit schema - top level assemblies
Pester conversion
foreach $r in "test-results"."test-suite".results."test-suite"
assembly
name = $r.Description
config-file = log file (this is the only way we can determine between admin/nonadmin log)
test-framework = Pester
environment = top-level "test-results.environment.platform
run-date = date (doesn't exist in pester except for beginning)
run-time = time
time =
#>
BEGIN {
# CLASSES
class assemblies {
# attributes
[datetime]$timestamp
# child elements
[System.Collections.Generic.List[testAssembly]]$assembly
assemblies() {
$this.timestamp = [datetime]::now
$this.assembly = [System.Collections.Generic.List[testAssembly]]::new()
}
static [assemblies] op_Addition([assemblies]$ls, [assemblies]$rs) {
$newAssembly = [assemblies]::new()
$newAssembly.assembly.AddRange($ls.assembly)
$newAssembly.assembly.AddRange($rs.assembly)
return $newAssembly
}
[string]ToString() {
$sb = [text.stringbuilder]::new()
$sb.AppendLine('<assemblies timestamp="{0:MM}/{0:dd}/{0:yyyy} {0:HH}:{0:mm}:{0:ss}">' -f $this.timestamp)
foreach ( $a in $this.assembly ) {
$sb.Append("$a")
}
$sb.AppendLine("</assemblies>");
return $sb.ToString()
}
# use Write-Output to emit these into the pipeline
[array]GetTests() {
return $this.Assembly.collection.test
}
}
class testAssembly {
# attributes
[string]$name # path to pester file
[string]${config-file}
[string]${test-framework} # Pester
[string]$environment
[string]${run-date}
[string]${run-time}
[decimal]$time
[int]$total
[int]$passed
[int]$failed
[int]$skipped
[int]$errors
testAssembly ( ) {
$this."config-file" = "no config"
$this."test-framework" = "Pester"
$this.environment = $script:environment
$this."run-date" = $script:rundate
$this."run-time" = $script:runtime
$this.collection = [System.Collections.Generic.List[collection]]::new()
}
# child elements
[error[]]$error
[System.Collections.Generic.List[collection]]$collection
[string]ToString() {
$sb = [System.Text.StringBuilder]::new()
$sb.AppendFormat(' <assembly name="{0}" ', $this.name)
$sb.AppendFormat('environment="{0}" ', [security.securityelement]::escape($this.environment))
$sb.AppendFormat('test-framework="{0}" ', $this."test-framework")
$sb.AppendFormat('run-date="{0}" ', $this."run-date")
$sb.AppendFormat('run-time="{0}" ', $this."run-time")
$sb.AppendFormat('total="{0}" ', $this.total)
$sb.AppendFormat('passed="{0}" ', $this.passed)
$sb.AppendFormat('failed="{0}" ', $this.failed)
$sb.AppendFormat('skipped="{0}" ', $this.skipped)
$sb.AppendFormat('time="{0}" ', $this.time)
$sb.AppendFormat('errors="{0}" ', $this.errors)
$sb.AppendLine(">")
if ( $this.error ) {
$sb.AppendLine(" <errors>")
foreach ( $e in $this.error ) {
$sb.AppendLine($e.ToString())
}
$sb.AppendLine(" </errors>")
} else {
$sb.AppendLine(" <errors />")
}
foreach ( $col in $this.collection ) {
$sb.AppendLine($col.ToString())
}
$sb.AppendLine(" </assembly>")
return $sb.ToString()
}
}
class collection {
# attributes
[string]$name
[decimal]$time
[int]$total
[int]$passed
[int]$failed
[int]$skipped
# child element
[System.Collections.Generic.List[test]]$test
# constructor
collection () {
$this.test = [System.Collections.Generic.List[test]]::new()
}
[string]ToString() {
$sb = [Text.StringBuilder]::new()
if ( $this.test.count -eq 0 ) {
$sb.AppendLine(" <collection />")
} else {
$sb.AppendFormat(' <collection total="{0}" passed="{1}" failed="{2}" skipped="{3}" name="{4}" time="{5}">' + "`n",
$this.total, $this.passed, $this.failed, $this.skipped, [security.securityelement]::escape($this.name), $this.time)
foreach ( $t in $this.test ) {
$sb.AppendLine(" " + $t.ToString());
}
$sb.Append(" </collection>")
}
return $sb.ToString()
}
}
class errors {
[error[]]$error
}
class error {
# attributes
[string]$type
[string]$name
# child elements
[failure]$failure
[string]ToString() {
$sb = [system.text.stringbuilder]::new()
$sb.AppendLine('<error type="{0}" name="{1}" >' -f $this.type, [security.securityelement]::escape($this.Name))
$sb.AppendLine($this.failure -as [string])
$sb.AppendLine("</error>")
return $sb.ToString()
}
}
class cdata {
[string]$text
cdata ( [string]$s ) { $this.text = $s }
[string]ToString() {
return '<![CDATA[' + [security.securityelement]::escape($this.text) + ']]>'
}
}
class failure {
[string]${exception-type}
[cdata]$message
[cdata]${stack-trace}
failure ( [string]$message, [string]$stack ) {
$this."exception-type" = "Pester"
$this.Message = [cdata]::new($message)
$this."stack-trace" = [cdata]::new($stack)
}
[string]ToString() {
$sb = [text.stringbuilder]::new()
$sb.AppendLine(" <failure>")
$sb.AppendLine(" <message>" + ($this.message -as [string]) + "</message>")
$sb.AppendLine(" <stack-trace>" + ($this."stack-trace" -as [string]) + "</stack-trace>")
$sb.Append(" </failure>")
return $sb.ToString()
}
}
enum resultenum {
Pass
Fail
Skip
}
class trait {
# attributes
[string]$name
[string]$value
}
class traits {
[trait[]]$trait
}
class test {
# attributes
[string]$name
[string]$type
[string]$method
[decimal]$time
[resultenum]$result
# child elements
[trait[]]$traits
[failure]$failure
[cdata]$reason # skip reason
[string]ToString() {
$sb = [text.stringbuilder]::new()
$sb.appendformat(' <test name="{0}" type="{1}" method="{2}" time="{3}" result="{4}"',
[security.securityelement]::escape($this.name), [security.securityelement]::escape($this.type),
[security.securityelement]::escape($this.method), $this.time, $this.result)
if ( $this.failure ) {
$sb.AppendLine(">")
$sb.AppendLine($this.failure -as [string])
$sb.append(' </test>')
} else {
$sb.Append("/>")
}
return $sb.ToString()
}
}
function convert-pesterlog ( [xml]$x, $logpath, [switch]$includeEmpty ) {
<#$resultMap = @{
Success = "Pass"
Ignored = "Skip"
Failure = "Fail"
}#>
$resultMap = @{
Success = "Pass"
Ignored = "Skip"
Failure = "Fail"
Inconclusive = "Skip"
}
$configfile = $logpath
$runtime = $x."test-results".time
$environment = $x."test-results".environment.platform + "-" + $x."test-results".environment."os-version"
$rundate = $x."test-results".date
$suites = $x."test-results"."test-suite".results."test-suite"
$assemblies = [assemblies]::new()
foreach ( $suite in $suites ) {
$tCases = $suite.SelectNodes(".//test-case")
# only create an assembly group if we have tests
if ( $tCases.count -eq 0 -and ! $includeEmpty ) { continue }
$tGroup = $tCases | Group-Object result
$total = $tCases.Count
$asm = [testassembly]::new()
$asm.environment = $environment
$asm."run-date" = $rundate
$asm."run-time" = $runtime
$asm.Name = $suite.name
$asm."config-file" = $configfile
$asm.time = $suite.time
$asm.total = $suite.SelectNodes(".//test-case").Count
$asm.Passed = $tGroup|? {$_.Name -eq "Success"} | % {$_.Count}
$asm.Failed = $tGroup|? {$_.Name -eq "Failure"} | % {$_.Count}
$asm.Skipped = $tGroup|? { $_.Name -eq "Ignored" } | % {$_.Count}
$asm.Skipped += $tGroup|? { $_.Name -eq "Inconclusive" } | % {$_.Count}
$c = [collection]::new()
$c.passed = $asm.Passed
$c.failed = $asm.failed
$c.skipped = $asm.skipped
$c.total = $asm.total
$c.time = $asm.time
$c.name = $asm.name
foreach ( $tc in $suite.SelectNodes(".//test-case")) {
if ( $tc.result -match "Success|Ignored|Failure" ) {
$t = [test]::new()
$t.name = $tc.Name
$t.time = $tc.time
$t.method = $tc.description # the pester actually puts the name of the "it" as description
$t.type = $suite.results."test-suite".description | Select-Object -First 1
$t.result = $resultMap[$tc.result]
if ( $tc.failure ) {
$t.failure = [failure]::new($tc.failure.message, $tc.failure."stack-trace")
}
$null = $c.test.Add($t)
}
}
$null = $asm.collection.add($c)
$assemblies.assembly.Add($asm)
}
$assemblies
}
# convert it to our object model
# a simple conversion
function convert-xunitlog {
param ( $x, $logpath )
$asms = [assemblies]::new()
$asms.timestamp = $x.assemblies.timestamp
foreach ( $assembly in $x.assemblies.assembly ) {
$asm = [testAssembly]::new()
$asm.environment = $assembly.environment
$asm."test-framework" = $assembly."test-framework"
$asm."run-date" = $assembly."run-date"
$asm."run-time" = $assembly."run-time"
$asm.total = $assembly.total
$asm.passed = $assembly.passed
$asm.failed = $assembly.failed
$asm.skipped = $assembly.skipped
$asm.time = $assembly.time
$asm.name = $assembly.name
foreach ( $coll in $assembly.collection ) {
$c = [collection]::new()
$c.name = $coll.name
$c.total = $coll.total
$c.passed = $coll.passed
$c.failed = $coll.failed
$c.skipped = $coll.skipped
$c.time = $coll.time
foreach ( $t in $coll.test ) {
$test = [test]::new()
$test.name = $t.name
$test.type = $t.type
$test.method = $t.method
$test.time = $t.time
$test.result = $t.result
$c.test.Add($test)
}
$null = $asm.collection.add($c)
}
$null = $asms.assembly.add($asm)
}
$asms
}
$Logs = @()
}
PROCESS {
#### MAIN ####
foreach ( $log in $Logfile ) {
foreach ( $logpath in (resolve-path $log).path ) {
write-progress "converting file $logpath"
if ( ! $logpath) { throw "Cannot resolve $Logfile" }
$x = [xml](get-content -raw -readcount 0 $logpath)
if ( $x.psobject.properties['test-results'] ) {
$Logs += convert-pesterlog $x $logpath -includeempty:$includeempty
} elseif ( $x.psobject.properties['assemblies'] ) {
$Logs += convert-xunitlog $x $logpath -includeEmpty:$includeEmpty
} else {
write-error "Cannot determine log type"
}
}
}
}
END {
if ( $MultipleLog ) {
$Logs
} else {
$combinedLog = $Logs[0]
for ( $i = 1; $i -lt $logs.count; $i++ ) {
$combinedLog += $Logs[$i]
}
$combinedLog
}
}
}
$script:RESX_TEMPLATE = @'
<?xml version="1.0" encoding="utf-8"?>
<root>