Write-JojobaXml.ps1

<#
 
.SYNOPSIS
Converts Jojoba test case objects to a jUnit XML file Jenkins can understand.
 
.DESCRIPTION
Jenkins is best used with a jUnit XML file to provide further information on why tests have passed, failed, or been skipped. This function provides that translation.
 
.PARAMETER Test
One or more Jojoba test case outputs. This would normally be generated by Start-Jojoba and sent here by Publish-Jojoba.
 
.PARAMETER OutputPath
Override the output path. It defaults to ".\Jojoba.xml".
 
.PARAMETER PassThru
Pass on the incoming object to the output stream. This is not recommended because under normal use cases Start-Jojoba outputs the case and this is streamed to the screen by Publish-Jojoba, and separately passed to Write-JojobaXml. If Write-JojobaXml is used to output then it would only be at the very end of all processing.
 
.EXAMPLE
[PSCustomObject] @{
    Suite = "Suite"
    Timestamp = Get-Date
    Time = 0
    ClassName = "Test"
    Name = "Server1"
    Result = "Pass"
    ErrorMessage = New-Object Collections.ArrayList
    Data = New-Object Collections.ArrayList
} | Write-JojobaXml
 
.NOTES
This is for internal use by Jojoba and should not be called externally.
 
#>


function Write-JojobaXml {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        $Test,
        $OutputPath = ".\Jojoba.xml",

        [switch] $PassThru
    )

    begin {
        $xmlDocument = New-Object System.Xml.XmlDocument
        $xmlDocument.LoadXml('<testsuites></testsuites>')

        $templateSuite = New-Object System.Xml.XmlDocument
        $templateSuite.LoadXml('<testsuite name="" tests="0" timestamp="" errors="0" failures="0"></testsuite>')
        $templateCase = New-Object System.Xml.XmlDocument
        $templateCase.LoadXml('<testcase classname="" name="" time="0"></testcase>')
        $templateFailure = New-Object System.Xml.XmlDocument
        $templateFailure.LoadXml('<failure message=""></failure>')
        $templateSkipped = New-Object System.Xml.XmlDocument
        $templateSkipped.LoadXml('<skipped message=""></skipped>')
    }

    process {
        foreach ($suite in ($Test | Group-Object Suite | Sort-Object Case)) {
            # This wouldn't normally be required but is to get around an edge
            # case of users running scripts instead of modules.
            $suiteName = [string] $Suite.Name
            if (!$suiteName) {
                $suiteName = "Jojoba"
            }
            $xmlSuite = $xmlDocument.ImportNode($templateSuite.testsuite, $false)
            $xmlSuite.name = [string] $suiteName
            $xmlSuite.tests = [string] $suite.Group.Count

            $errors = $suite.Group | Where-Object { $_.CriticalFailure -eq $true } | Measure-Object | ForEach-Object Count
            $failures = $suite.Group | Where-Object { $_.Result -eq "Fail" } | Measure-Object | ForEach-Object Count
            $timestamp = $suite.Group | Measure-Object -Minimum Timestamp | ForEach-Object Minimum
            $xmlSuite.timestamp = $timestamp.ToString("o")
            $xmlSuite.failures = [string] $failures
            $xmlSuite.errors = [string] $errors

            foreach ($case in $suite.Group) {
                $xmlCase = $xmlDocument.ImportNode($templateCase.testcase, $false)
                $xmlCase.time = [string] $case.Time

                # Some of these ToString's don't seem necessary, but, it seems
                # in some cases the parameters are a NoteProperty and it really
                # demands they are exactly a string. Sometimes they're $null...
                $xmlCase.classname = [string] $suiteName + "." + $case.ClassName
                $xmlCase.name = [string] $case.Name

                if ($case.Result -eq "Fail") {
                    $xmlFailure = $xmlDocument.ImportNode($templateFailure.failure, $false)
                    if ($case.Message) {
                        $xmlFailure.message = $case.Message -join [Environment]::NewLine
                    }
                    if ($case.Data) {
                        $xmlFailure.InnerText = $case.Data -join [Environment]::NewLine
                    }

                    [void] $xmlCase.AppendChild($xmlFailure)
                } elseif ($case.Result -eq "Skip") {
                    $xmlSkipped = $xmlDocument.ImportNode($templateSkipped.skipped, $false)
                    if ($case.Message) {
                        $xmlSkipped.message = $case.Message -join [Environment]::NewLine
                    }
                    if ($case.Data) {
                        $xmlSkipped.InnerText = $case.Data -join [Environment]::NewLine
                    }

                    [void] $xmlCase.AppendChild($xmlSkipped)
                }
                [void] $xmlSuite.AppendChild($xmlCase)
            }

            [void] $xmlDocument.ChildNodes.AppendChild($xmlSuite)
        }

        if ($PassThru) {
            $Test
        }
    }

    end {
        $xmlDocument.Save($OutputPath)
    }
}