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 } } |