Modules/businessdev.ALbuild.Apps/Public/Merge-BcCodeCoverage.ps1

function Merge-BcCodeCoverage {
    <#
    .SYNOPSIS
        Merges raw Business Central coverage (.dat) from several runs into one dataset (union, max hits per line).
 
    .DESCRIPTION
        Parallel/sharded test runs each emit their own coverage .dat files. This unions them by (Object, Line),
        keeping the highest hit count seen for each line, and writes a single merged .dat in BC's VariableText
        CSV shape (ObjectType,ObjectID,LineNo,Status,Hits) that Convert-BcCodeCoverage / Get-BcCodeCoverageSummary
        can consume. Status is written as 0 (Covered) because BC only ever exports executed lines.
 
    .PARAMETER CoveragePath
        One or more folders of *.dat files (or individual .dat files) to merge.
 
    .PARAMETER OutputPath
        Destination .dat file (a folder is allowed; 'coverage_merged.dat' is written inside it).
 
    .OUTPUTS
        PSCustomObject: OutputFile, ObjectCount, LineCount.
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Writes a merged data file at an explicit caller-provided path; no destructive ambiguity.')]
    [CmdletBinding()]
    [OutputType([PSCustomObject])]
    param(
        [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string[]] $CoveragePath,
        [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string] $OutputPath
    )

    $datFiles = [System.Collections.Generic.List[string]]::new()
    foreach ($p in $CoveragePath) {
        if (-not (Test-Path -LiteralPath $p)) { throw "Coverage path not found: '$p'." }
        if (Test-Path -LiteralPath $p -PathType Container) {
            foreach ($f in Get-ChildItem -LiteralPath $p -Filter '*.dat' -File -Recurse) { $datFiles.Add($f.FullName) }
        }
        else { $datFiles.Add((Resolve-Path -LiteralPath $p).Path) }
    }
    if ($datFiles.Count -eq 0) { throw 'No coverage .dat files found to merge.' }

    # Union by "Type,Id,Line" keeping the max hit count.
    $merged = @{}
    foreach ($file in $datFiles) {
        foreach ($line in (Get-Content -LiteralPath $file -ErrorAction SilentlyContinue)) {
            $c = $line -split ','
            if ($c.Count -lt 5 -or $c[1] -notmatch '^\d+$' -or $c[2] -notmatch '^\d+$') { continue }
            $type = $c[0].Trim(); $id = [int]$c[1]; $lineNo = [int]$c[2]; $hits = [int]$c[4]
            $key = "$type,$id,$lineNo"
            if (-not $merged.ContainsKey($key) -or $hits -gt $merged[$key]) { $merged[$key] = $hits }
        }
    }

    $outFile = $OutputPath
    if (Test-Path -LiteralPath $OutputPath -PathType Container) { $outFile = Join-Path $OutputPath 'coverage_merged.dat' }
    $dir = Split-Path -Parent $outFile
    if ($dir -and -not (Test-Path -LiteralPath $dir)) { New-Item -ItemType Directory -Force -Path $dir | Out-Null }

    $objects = @{}
    $sb = [System.Text.StringBuilder]::new()
    foreach ($key in ($merged.Keys | Sort-Object)) {
        $parts = $key -split ','
        $objects["$($parts[0]),$($parts[1])"] = $true
        [void]$sb.AppendLine("$($parts[0]),$($parts[1]),$($parts[2]),0,$($merged[$key])")
    }
    Set-Content -LiteralPath $outFile -Value $sb.ToString().TrimEnd() -Encoding Unicode

    Write-ALbuildLog -Level Success ("Merged {0} coverage file(s) -> {1} line(s) across {2} object(s)." -f $datFiles.Count, $merged.Count, $objects.Count)
    return [PSCustomObject]@{ OutputFile = $outFile; ObjectCount = $objects.Count; LineCount = $merged.Count }
}