Modules/businessdev.ALbuild.RuntimePackages/Private/ConvertTo-BcRuntimeIssueJUnit.ps1

function ConvertTo-BcRuntimeIssueJUnit {
    <#
    .SYNOPSIS
        JUnit results with one test case per (app, CAUSE) instead of per (app, platform version).
 
    .DESCRIPTION
        The per-version results answer "is the catalogue complete?". This answers the other question -
        "what is blocking a product, and who has to fix it?" - and it is a different shape.
 
        Run 27383 failed 274 times. Read per version that is 274 findings; read per cause it is four:
        ERiC cannot compile below AL runtime 14, Banking subscribes to events its older Base Applications
        do not have, E-Invoice references namespaces they do not have, and one container refused a
        command. Four is a report a product team can act on. 274 is a wall they have to read first.
 
        The minimum versions in the catalogue are NOT raised to make these go away: what a product
        supports is the product team's decision, not the build's. So the build's job ends at stating the
        blockage precisely - which platform versions, which compiler errors, first diagnostics - and
        handing it over.
 
        Published as its own test run, so the Tests tab shows one red entry per real problem, with history
        across runs: a cause that disappears was fixed, a cause that appears is new.
 
    .PARAMETER Result
        The per-platform-version results from Invoke-BcRuntimeFactory.
 
    .PARAMETER MaxDiagnostic
        Diagnostics to quote per cause.
 
    .OUTPUTS
        System.String - the JUnit XML.
    #>

    [CmdletBinding()]
    [OutputType([string])]
    param(
        [Parameter(Mandatory)] [AllowEmptyCollection()] [object[]] $Result,
        [ValidateRange(1, 50)] [int] $MaxDiagnostic = 6
    )

    $rows = [System.Collections.Generic.List[object]]::new()
    foreach ($v in @($Result)) {
        $pv = "$($v.PlatformVersion)"
        $sortable = try { [version](ConvertTo-BcVersion $pv) } catch { [version]'0.0.0.0' }
        $country = "$(Get-BcRuntimeProperty -InputObject $v -Name 'Country' -Default '')"
        foreach ($p in @(Get-BcRuntimeProperty -InputObject $v -Name 'Products' -Default @())) {
            if ("$($p.Status)" -ne 'Failed') { continue }
            $diag = @(Get-BcRuntimeProperty -InputObject $p -Name 'Diagnostics' -Default @())
            $class = Get-BcRuntimeIssueClass -Message "$(Get-BcRuntimeProperty -InputObject $p -Name 'Error' -Default '')" -Diagnostic $diag
            $rows.Add([PSCustomObject]@{
                    App     = "$($p.Name)"
                    Version = $pv
                    Sort    = $sortable
                    Country = $country
                    Key     = $class.Key
                    Label   = $class.Label
                    Diag    = $diag
                })
        }
    }

    $esc = {
        param([string] $Text)
        [System.Security.SecurityElement]::Escape("$Text")
    }

    $sb = [System.Text.StringBuilder]::new()
    [void]$sb.AppendLine('<?xml version="1.0" encoding="utf-8"?>')
    [void]$sb.AppendLine('<testsuites>')

    foreach ($app in (@($rows | Select-Object -ExpandProperty App -Unique) | Sort-Object)) {
        $mine = @($rows | Where-Object { $_.App -eq $app })
        $causes = @($mine | Group-Object Key)
        [void]$sb.AppendLine((' <testsuite name="{0}" tests="{1}" failures="{1}" skipped="0" time="0">' -f
            (& $esc $app), $causes.Count))
        foreach ($g in ($causes | Sort-Object { $_.Count } -Descending)) {
            $versions = @($g.Group | Sort-Object Sort -Descending)
            $label = "$($g.Group[0].Label)"
            $newest = $versions[0]
            $oldest = $versions[-1]

            $body = [System.Collections.Generic.List[string]]::new()
            $body.Add("$app cannot be built on $($versions.Count) platform version(s): $label.")
            $body.Add('')
            $body.Add("Newest affected: BC $($newest.Version)$(if ($newest.Country) { " ($($newest.Country))" })")
            $body.Add("Oldest affected: BC $($oldest.Version)$(if ($oldest.Country) { " ($($oldest.Country))" })")
            $body.Add('')
            # EVERY affected version, not a sample: the team reading this decides whether to guard the
            # source or move the minimum, and both need the actual list. A capped list turned the
            # 27409 report into a lower bound nobody could act on. Wrapped so it stays readable.
            $body.Add('Affected:')
            $all = @($versions | ForEach-Object { "BC $($_.Version)" })
            for ($i = 0; $i -lt $all.Count; $i += 6) {
                $chunk = @($all[$i..([Math]::Min($i + 5, $all.Count - 1))])
                $body.Add(" $($chunk -join ', ')")
            }
            if (@($g.Group[0].Diag).Count -gt 0) {
                $body.Add('')
                $body.Add('First diagnostics:')
                foreach ($d in @($g.Group[0].Diag | Select-Object -First $MaxDiagnostic)) { $body.Add(" $d") }
            }
            $body.Add('')
            $body.Add('The catalogue minimum for this product is deliberately NOT raised to hide this - what a')
            $body.Add('product supports is the product team''s call. Either the source gains a guard for these')
            $body.Add('versions, or the minimum moves. The full compiler output is in the worker-logs artifact.')

            # classname carries the app so the Tests tab groups by product; name carries the cause -
            # and NOTHING that changes between runs. The count used to sit here, so a blocker that
            # spread from 12 versions to 13 became a different test and lost its history in the Tests
            # tab. The count belongs in the message, which is free to change.
            [void]$sb.AppendLine((' <testcase classname="{0}" name="{1}" time="0">' -f
                (& $esc $app), (& $esc "$app - $label")))
            [void]$sb.AppendLine((' <failure message="{0}">{1}</failure>' -f
                (& $esc $body[0]), (& $esc ($body -join [Environment]::NewLine))))
            [void]$sb.AppendLine(' </testcase>')
        }
        [void]$sb.AppendLine(' </testsuite>')
    }

    [void]$sb.AppendLine('</testsuites>')
    return $sb.ToString()
}