Private/New-AzLocalJUnitXml.ps1

Function New-AzLocalJUnitXml {
    <#
    .SYNOPSIS
 
    Generates a JUnit XML report from an array of test result objects.
 
    .DESCRIPTION
 
    Builds a JUnit-compatible XML document from deployment test results. Each cluster
    is represented as a test case with pass, fail, or skip status. The output is compatible
    with dorny/test-reporter (GitHub Actions) and PublishTestResults@2 (Azure DevOps).
 
    .PARAMETER TestResults
    Array of PSCustomObjects with properties: TestName, ClassName, Status (Passed/Failed/Skipped),
    Message, Duration (seconds).
 
    .PARAMETER SuiteName
    Name of the test suite in the JUnit XML. Default: 'AzLocalDeploymentAutomation'.
 
    .PARAMETER OutputPath
    Optional. File path to write the XML. If omitted, returns the XML string.
 
    #>


    [OutputType([string])]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, Position = 0)]
        [PSCustomObject[]]$TestResults,

        [Parameter(Mandatory = $false)]
        [string]$SuiteName = 'AzLocalDeploymentAutomation',

        [Parameter(Mandatory = $false)]
        [string]$OutputPath = ""
    )

    $timestamp = Get-Date -Format "yyyy-MM-ddTHH:mm:ss"
    $totalTests = $TestResults.Count
    $failures = @($TestResults | Where-Object { $_.Status -eq 'Failed' }).Count
    $skipped = @($TestResults | Where-Object { $_.Status -eq 'Skipped' }).Count
    $totalTime = ($TestResults | Measure-Object -Property Duration -Sum).Sum
    if (-not $totalTime) { $totalTime = 0 }

    # Build XML using XmlDocument for proper escaping and well-formed output
    $xml = [System.Xml.XmlDocument]::new()
    $xml.AppendChild($xml.CreateXmlDeclaration("1.0", "UTF-8", $null)) | Out-Null

    $testSuites = $xml.CreateElement("testsuites")
    $xml.AppendChild($testSuites) | Out-Null

    $testSuite = $xml.CreateElement("testsuite")
    $testSuite.SetAttribute("name", $SuiteName)
    $testSuite.SetAttribute("tests", $totalTests.ToString())
    $testSuite.SetAttribute("failures", $failures.ToString())
    $testSuite.SetAttribute("errors", "0")
    $testSuite.SetAttribute("skipped", $skipped.ToString())
    $testSuite.SetAttribute("time", $totalTime.ToString())
    $testSuite.SetAttribute("timestamp", $timestamp)
    $testSuites.AppendChild($testSuite) | Out-Null

    foreach ($result in $TestResults) {
        $duration = if ($result.Duration) { $result.Duration } else { 0 }
        $message = if ($result.Message) { $result.Message } else { "" }

        $testCase = $xml.CreateElement("testcase")
        $testCase.SetAttribute("name", $result.TestName)
        $testCase.SetAttribute("classname", $result.ClassName)
        $testCase.SetAttribute("time", $duration.ToString())

        if ($result.Status -eq 'Failed') {
            $failure = $xml.CreateElement("failure")
            $failure.SetAttribute("message", "$($result.TestName) failed")
            $failure.SetAttribute("type", "DeploymentFailure")
            $failure.AppendChild($xml.CreateCDataSection($message)) | Out-Null
            $testCase.AppendChild($failure) | Out-Null
        } elseif ($result.Status -eq 'Skipped') {
            $skippedEl = $xml.CreateElement("skipped")
            $skippedEl.SetAttribute("message", $message)
            $testCase.AppendChild($skippedEl) | Out-Null
        } else {
            if ($message) {
                $systemOut = $xml.CreateElement("system-out")
                $systemOut.AppendChild($xml.CreateCDataSection($message)) | Out-Null
                $testCase.AppendChild($systemOut) | Out-Null
            }
        }

        $testSuite.AppendChild($testCase) | Out-Null
    }

    # Pretty-print the XML
    $stringWriter = [System.IO.StringWriter]::new()
    $xmlWriter = [System.Xml.XmlTextWriter]::new($stringWriter)
    $xmlWriter.Formatting = [System.Xml.Formatting]::Indented
    $xmlWriter.Indentation = 2
    $xml.WriteTo($xmlWriter)
    $xmlWriter.Flush()
    $xmlContent = $stringWriter.ToString()
    $xmlWriter.Close()
    $stringWriter.Close()

    if (-not [string]::IsNullOrWhiteSpace($OutputPath)) {
        $outputDir = Split-Path $OutputPath -Parent
        if ($outputDir -and -not (Test-Path $outputDir)) {
            New-Item -Path $outputDir -ItemType Directory -Force | Out-Null
        }
        $xmlContent | Out-File -FilePath $OutputPath -Encoding utf8 -Force
        Write-AzLocalLog "JUnit XML report written to '$OutputPath'." -Level Success
    }

    return $xmlContent
}