From 9e99d8f909af1e069710577d8774064cb81cbf0b Mon Sep 17 00:00:00 2001 From: Victor Vazquez Date: Fri, 30 Jun 2023 12:41:26 -0700 Subject: [PATCH 1/9] atr --- .gitattributes | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitattributes b/.gitattributes index aa482a91600..5e572a7873f 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,3 +1,3 @@ *.go text eol=lf -*.snap text eol=lf -*.txt text eol=lf \ No newline at end of file +**/testdata/**/*.(md|snap) text eol=lf +*.txt text eol=lf From d95e1da101c96124505ad4e6849821c4621d4492 Mon Sep 17 00:00:00 2001 From: Victor Vazquez Date: Fri, 30 Jun 2023 12:44:01 -0700 Subject: [PATCH 2/9] like this --- .gitattributes | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.gitattributes b/.gitattributes index 5e572a7873f..1ebc4d661cf 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,3 +1,4 @@ -*.go text eol=lf -**/testdata/**/*.(md|snap) text eol=lf +*.go text eol=lf +**/testdata/**/*.snap text eol=lf +**/testdata/**/*.md text eol=lf *.txt text eol=lf From b4885bfeffa2c165254236af9f53e5f5956c816b Mon Sep 17 00:00:00 2001 From: Victor Vazquez Date: Fri, 15 Aug 2025 06:11:01 +0000 Subject: [PATCH 3/9] update azd to go 1.25 --- .devcontainer/devcontainer.json | 2 +- .devcontainer/light/devcontainer.json | 2 +- .github/workflows/cli-ci.yml | 6 +++--- .github/workflows/copilot-setup-steps.yml | 4 ++-- cli/azd/CONTRIBUTING.md | 2 +- eng/pipelines/templates/steps/setup-go.yml | 2 +- go.mod | 2 +- 7 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index d6ac7f63906..d8add188442 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -3,7 +3,7 @@ "image": "mcr.microsoft.com/devcontainers/base:ubuntu", "features": { "ghcr.io/devcontainers/features/go:1": { - "version": "1.24" + "version": "1.25" }, "ghcr.io/devcontainers/features/docker-in-docker:2.11.0": { "version": "latest", diff --git a/.devcontainer/light/devcontainer.json b/.devcontainer/light/devcontainer.json index a5ca6c919f3..c21d242d835 100644 --- a/.devcontainer/light/devcontainer.json +++ b/.devcontainer/light/devcontainer.json @@ -3,7 +3,7 @@ "image": "mcr.microsoft.com/devcontainers/base:ubuntu", "features": { "ghcr.io/devcontainers/features/go:1": { - "version": "1.24" + "version": "1.25" }, "ghcr.io/devcontainers/features/docker-in-docker:2.11.0": { "version": "latest", diff --git a/.github/workflows/cli-ci.yml b/.github/workflows/cli-ci.yml index 3d4ae5f156e..6bd0fc9d4bf 100644 --- a/.github/workflows/cli-ci.yml +++ b/.github/workflows/cli-ci.yml @@ -26,12 +26,12 @@ jobs: steps: - uses: actions/setup-go@v3 with: - go-version: "^1.24" + go-version: "^1.25" - uses: actions/checkout@v4 - name: golangci-lint - uses: golangci/golangci-lint-action@v7 + uses: golangci/golangci-lint-action@v8 with: - version: v2.0 + version: v2.4 args: -v --timeout 10m0s working-directory: cli/azd diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml index f12a99e1f9f..572b79d9340 100644 --- a/.github/workflows/copilot-setup-steps.yml +++ b/.github/workflows/copilot-setup-steps.yml @@ -38,7 +38,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v5 with: - go-version: "^1.24" + go-version: "^1.25" - name: Set up Node.js uses: actions/setup-node@v4 @@ -46,7 +46,7 @@ jobs: node-version: "20" - name: Install golangci-lint - run: curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/HEAD/install.sh | sh -s -- -b $(go env GOPATH)/bin v2.0.0 + run: curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/HEAD/install.sh | sh -s -- -b $(go env GOPATH)/bin v2.4.0 - name: Install cspell run: npm install -g cspell@8.13.1 diff --git a/cli/azd/CONTRIBUTING.md b/cli/azd/CONTRIBUTING.md index 404836e3e69..6008d756fb7 100644 --- a/cli/azd/CONTRIBUTING.md +++ b/cli/azd/CONTRIBUTING.md @@ -13,7 +13,7 @@ In general, to make contributions a smooth and easy experience, we encourage the Prerequisites: -- [Go](https://go.dev/dl/) 1.24 +- [Go](https://go.dev/dl/) 1.25 Build: diff --git a/eng/pipelines/templates/steps/setup-go.yml b/eng/pipelines/templates/steps/setup-go.yml index 7327ecf69b5..cd4375a9b15 100644 --- a/eng/pipelines/templates/steps/setup-go.yml +++ b/eng/pipelines/templates/steps/setup-go.yml @@ -1,5 +1,5 @@ parameters: - GoVersion: 1.24.0 + GoVersion: 1.25.0 Condition: succeeded() steps: diff --git a/go.mod b/go.mod index 346a62d1230..a1f56f7915c 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/azure/azure-dev -go 1.24 +go 1.25 require ( dario.cat/mergo v1.0.1 From 8903bcfd79a7f6e33f2dbd900dd9d286eeb01adc Mon Sep 17 00:00:00 2001 From: Victor Vazquez Date: Fri, 15 Aug 2025 06:17:25 +0000 Subject: [PATCH 4/9] revert --- .gitattributes | 3 --- 1 file changed, 3 deletions(-) diff --git a/.gitattributes b/.gitattributes index e3fe6123ea7..8fd2351e4ad 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,7 +1,4 @@ *.go text eol=lf -**/testdata/**/*.snap text eol=lf -**/testdata/**/*.md text eol=lf -*.txt text eol=lf *.snap text eol=lf *.txt text eol=lf *.sh text eol=lf From e1508536cac15d91b5b6ab5961c30aa2370d7454 Mon Sep 17 00:00:00 2001 From: Victor Vazquez Date: Sat, 16 Aug 2025 03:29:37 +0000 Subject: [PATCH 5/9] replace gocov and gocov-xml for ps1 script to create Cobertura report for AzureDevOps from the go coberage report --- cli/azd/ci-test.ps1 | 16 +- .../templates/stages/code-coverage-upload.yml | 8 +- eng/scripts/Convert-GoCoverageToCobertura.ps1 | 252 ++++++++++++++++++ 3 files changed, 263 insertions(+), 13 deletions(-) create mode 100644 eng/scripts/Convert-GoCoverageToCobertura.ps1 diff --git a/cli/azd/ci-test.ps1 b/cli/azd/ci-test.ps1 index 5c5e3b6c2a9..75c8f05cd0a 100644 --- a/cli/azd/ci-test.ps1 +++ b/cli/azd/ci-test.ps1 @@ -33,11 +33,11 @@ function New-EmptyDirectory { $unitCoverDir = New-EmptyDirectory -Path $UnitTestCoverageDir Write-Host "Running unit tests..." -# --test.gocoverdir is currently a "under-the-cover" way to pass the coverage directory to a test binary -# See https://github.com/golang/go/issues/51430#issuecomment-1344711300 -# -# This may be improved in go1.21 with an official 'go test' flag. -& $gotestsum -- ./... -short -v -cover -args --test.gocoverdir="$($unitCoverDir.FullName)" +# Using -coverprofile flag introduced in Go 1.21 for modern coverage collection +# This replaces the older --test.gocoverdir approach and provides better integration +# with the standard Go toolchain for coverage reporting. +$unitCoverProfile = Join-Path $unitCoverDir.FullName "coverage.out" +& $gotestsum -- ./... -short -v -coverprofile="$unitCoverProfile" if ($LASTEXITCODE) { exit $LASTEXITCODE } @@ -54,13 +54,15 @@ $oldGOCOVERDIR = $env:GOCOVERDIR $oldGOEXPERIMENT = $env:GOEXPERIMENT # GOCOVERDIR enables any binaries (in this case, azd.exe) built with '-cover', -# to write out coverage output to the specific directory. +# to write out coverage output to the specific coverage directory. +# This works in conjunction with the -coverprofile flag for comprehensive coverage reporting. $env:GOCOVERDIR = $intCoverDir.FullName # Set any experiment flags that are needed for the tests. $env:GOEXPERIMENT="" try { - & $gotestsum -- ./... -v -timeout $IntegrationTestTimeout + $intCoverProfile = Join-Path $intCoverDir.FullName "coverage.out" + & $gotestsum -- ./... -v -timeout $IntegrationTestTimeout -coverprofile="$intCoverProfile" if ($LASTEXITCODE) { exit $LASTEXITCODE } diff --git a/eng/pipelines/templates/stages/code-coverage-upload.yml b/eng/pipelines/templates/stages/code-coverage-upload.yml index e0a800d2348..da80c83c4ae 100644 --- a/eng/pipelines/templates/stages/code-coverage-upload.yml +++ b/eng/pipelines/templates/stages/code-coverage-upload.yml @@ -50,12 +50,8 @@ stages: go tool covdata textfmt -i=cover -o cover.out if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - go install github.com/axw/gocov/gocov@latest - if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - go install github.com/AlekSi/gocov-xml@latest - if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - - ~/go/bin/gocov convert cover.out | ~/go/bin/gocov-xml > coverage.xml + # Convert Go coverage to Cobertura XML format using custom script + & "$(Build.SourcesDirectory)/eng/scripts/Convert-GoCoverageToCobertura.ps1" -CoverageFile cover.out -OutputFile coverage.xml -SourceRoot "$(Build.SourcesDirectory)" if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } displayName: Merge code coverage files diff --git a/eng/scripts/Convert-GoCoverageToCobertura.ps1 b/eng/scripts/Convert-GoCoverageToCobertura.ps1 new file mode 100644 index 00000000000..31053d53325 --- /dev/null +++ b/eng/scripts/Convert-GoCoverageToCobertura.ps1 @@ -0,0 +1,252 @@ +#!/usr/bin/env pwsh + +<# +.SYNOPSIS + Converts Go coverage profile to Cobertura XML format for Azure DevOps. + +.DESCRIPTION + This script takes a Go coverage profile (cover.out) and converts it to Cobertura XML format + that can be consumed by Azure DevOps for code coverage reporting. This replaces the need + for external tools like gocov and gocov-xml. + +.PARAMETER CoverageFile + Path to the Go coverage profile file (typically cover.out) + +.PARAMETER OutputFile + Path where the Cobertura XML file should be written + +.PARAMETER SourceRoot + Root directory of the source code (used for relative path calculation) + +.EXAMPLE + ./Convert-GoCoverageToCobertura.ps1 -CoverageFile cover.out -OutputFile coverage.xml -SourceRoot . +#> + +param( + [Parameter(Mandatory = $true)] + [string]$CoverageFile, + + [Parameter(Mandatory = $true)] + [string]$OutputFile, + + [Parameter(Mandatory = $false)] + [string]$SourceRoot = "." +) + +$ErrorActionPreference = 'Stop' + +function Get-RelativePath { + param( + [string]$Path, + [string]$BasePath + ) + + $resolvedPath = Resolve-Path $Path -Relative + return $resolvedPath.TrimStart('.', '\', '/') +} + +function Parse-GoCoverageFile { + param([string]$FilePath) + + $coverage = @{} + $lines = Get-Content $FilePath + + # Skip the mode line (first line) + for ($i = 1; $i -lt $lines.Count; $i++) { + $line = $lines[$i].Trim() + if ([string]::IsNullOrEmpty($line)) { continue } + + # Parse line format: file.go:startLine.startCol,endLine.endCol numStmt count + if ($line -match '^(.+):(\d+)\.(\d+),(\d+)\.(\d+) (\d+) (\d+)$') { + $file = $matches[1] + $startLine = [int]$matches[2] + $endLine = [int]$matches[4] + $numStmt = [int]$matches[6] + $count = [int]$matches[7] + + if (-not $coverage.ContainsKey($file)) { + $coverage[$file] = @{} + } + + # Mark lines as covered or uncovered + for ($lineNum = $startLine; $lineNum -le $endLine; $lineNum++) { + if (-not $coverage[$file].ContainsKey($lineNum)) { + $coverage[$file][$lineNum] = @{ + hits = 0 + statements = 0 + } + } + $coverage[$file][$lineNum].hits += $count + $coverage[$file][$lineNum].statements += $numStmt + } + } + } + + return $coverage +} + +function Generate-CoberturaXml { + param( + [hashtable]$Coverage, + [string]$SourceRoot + ) + + $xml = [System.Xml.XmlDocument]::new() + $declaration = $xml.CreateXmlDeclaration("1.0", "UTF-8", $null) + $xml.AppendChild($declaration) | Out-Null + + # Create root coverage element + $coverageElement = $xml.CreateElement("coverage") + $coverageElement.SetAttribute("line-rate", "0.0") + $coverageElement.SetAttribute("branch-rate", "0.0") + $coverageElement.SetAttribute("lines-covered", "0") + $coverageElement.SetAttribute("lines-valid", "0") + $coverageElement.SetAttribute("branches-covered", "0") + $coverageElement.SetAttribute("branches-valid", "0") + $coverageElement.SetAttribute("complexity", "0.0") + $coverageElement.SetAttribute("version", "1.0") + $coverageElement.SetAttribute("timestamp", [DateTimeOffset]::Now.ToUnixTimeSeconds()) + $xml.AppendChild($coverageElement) | Out-Null + + # Create sources element + $sourcesElement = $xml.CreateElement("sources") + $sourceElement = $xml.CreateElement("source") + $sourceElement.InnerText = $SourceRoot + $sourcesElement.AppendChild($sourceElement) | Out-Null + $coverageElement.AppendChild($sourcesElement) | Out-Null + + # Create packages element + $packagesElement = $xml.CreateElement("packages") + $coverageElement.AppendChild($packagesElement) | Out-Null + + $totalLinesCovered = 0 + $totalLinesValid = 0 + + # Group files by package (directory) + $packages = @{} + foreach ($file in $Coverage.Keys) { + $packageName = Split-Path $file -Parent + if ([string]::IsNullOrEmpty($packageName)) { + $packageName = "." + } + + if (-not $packages.ContainsKey($packageName)) { + $packages[$packageName] = @{} + } + $packages[$packageName][$file] = $Coverage[$file] + } + + foreach ($packageName in $packages.Keys) { + $packageElement = $xml.CreateElement("package") + $packageElement.SetAttribute("name", $packageName) + + $packageLinesCovered = 0 + $packageLinesValid = 0 + + # Create classes element for this package + $classesElement = $xml.CreateElement("classes") + $packageElement.AppendChild($classesElement) | Out-Null + + foreach ($file in $packages[$packageName].Keys) { + $classElement = $xml.CreateElement("class") + $fileName = Split-Path $file -Leaf + $classElement.SetAttribute("name", $fileName) + $classElement.SetAttribute("filename", $file) + + $fileLinesCovered = 0 + $fileLinesValid = 0 + + # Create methods element (empty for Go files) + $methodsElement = $xml.CreateElement("methods") + $classElement.AppendChild($methodsElement) | Out-Null + + # Create lines element + $linesElement = $xml.CreateElement("lines") + $classElement.AppendChild($linesElement) | Out-Null + + foreach ($lineNum in ($packages[$packageName][$file].Keys | Sort-Object)) { + $lineData = $packages[$packageName][$file][$lineNum] + $lineElement = $xml.CreateElement("line") + $lineElement.SetAttribute("number", $lineNum) + $lineElement.SetAttribute("hits", $lineData.hits) + $lineElement.SetAttribute("branch", "false") + $linesElement.AppendChild($lineElement) | Out-Null + + $fileLinesValid++ + if ($lineData.hits -gt 0) { + $fileLinesCovered++ + } + } + + $packageLinesCovered += $fileLinesCovered + $packageLinesValid += $fileLinesValid + + # Set class coverage attributes + $classLineRate = if ($fileLinesValid -gt 0) { $fileLinesCovered / $fileLinesValid } else { 0.0 } + $classElement.SetAttribute("line-rate", $classLineRate.ToString("F4")) + $classElement.SetAttribute("branch-rate", "0.0") + $classElement.SetAttribute("complexity", "0.0") + + $classesElement.AppendChild($classElement) | Out-Null + } + + $totalLinesCovered += $packageLinesCovered + $totalLinesValid += $packageLinesValid + + # Set package coverage attributes + $packageLineRate = if ($packageLinesValid -gt 0) { $packageLinesCovered / $packageLinesValid } else { 0.0 } + $packageElement.SetAttribute("line-rate", $packageLineRate.ToString("F4")) + $packageElement.SetAttribute("branch-rate", "0.0") + $packageElement.SetAttribute("complexity", "0.0") + + $packagesElement.AppendChild($packageElement) | Out-Null + } + + # Update overall coverage attributes + $overallLineRate = if ($totalLinesValid -gt 0) { $totalLinesCovered / $totalLinesValid } else { 0.0 } + $coverageElement.SetAttribute("line-rate", $overallLineRate.ToString("F4")) + $coverageElement.SetAttribute("lines-covered", $totalLinesCovered) + $coverageElement.SetAttribute("lines-valid", $totalLinesValid) + + return $xml +} + +# Main execution +Write-Host "Converting Go coverage file '$CoverageFile' to Cobertura XML format..." + +if (-not (Test-Path $CoverageFile)) { + throw "Coverage file '$CoverageFile' not found" +} + +# Parse the Go coverage file +$coverage = Parse-GoCoverageFile -FilePath $CoverageFile + +# Generate Cobertura XML +$xml = Generate-CoberturaXml -Coverage $coverage -SourceRoot $SourceRoot + +# Save the XML file +$outputPath = if (Test-Path $OutputFile -IsValid) { + if ([System.IO.Path]::IsPathRooted($OutputFile)) { + $OutputFile + } else { + Join-Path (Get-Location) $OutputFile + } +} else { + $OutputFile +} +$xml.Save($outputPath) + +Write-Host "Successfully converted coverage to Cobertura XML: '$OutputFile'" + +# Display summary +$totalFiles = $coverage.Keys.Count +$totalLines = ($coverage.Values | ForEach-Object { $_.Keys.Count } | Measure-Object -Sum).Sum +$coveredLines = ($coverage.Values | ForEach-Object { + $_.Values | Where-Object { $_.hits -gt 0 } +} | Measure-Object).Count + +$coveragePercentage = if ($totalLines -gt 0) { ($coveredLines / $totalLines) * 100 } else { 0 } + +Write-Host "Coverage Summary:" +Write-Host " Files: $totalFiles" +Write-Host " Lines: $coveredLines/$totalLines ($($coveragePercentage.ToString('F2'))%)" From 76c2ad6fa9d20a45a3f576c46f87ada93ac7ce31 Mon Sep 17 00:00:00 2001 From: Victor Vazquez Date: Sat, 16 Aug 2025 05:13:43 +0000 Subject: [PATCH 6/9] use cov files and merge --- .../templates/stages/code-coverage-upload.yml | 78 +++++--- eng/scripts/Merge-GoCoverageProfiles.ps1 | 168 ++++++++++++++++++ 2 files changed, 219 insertions(+), 27 deletions(-) create mode 100644 eng/scripts/Merge-GoCoverageProfiles.ps1 diff --git a/eng/pipelines/templates/stages/code-coverage-upload.yml b/eng/pipelines/templates/stages/code-coverage-upload.yml index da80c83c4ae..a05a012172e 100644 --- a/eng/pipelines/templates/stages/code-coverage-upload.yml +++ b/eng/pipelines/templates/stages/code-coverage-upload.yml @@ -27,27 +27,51 @@ stages: Artifacts: ${{ parameters.DownloadArtifacts }} - pwsh: | - New-Item -ItemType Directory -Force -Path cover - New-Item -ItemType Directory -Force -Path cover-int - New-Item -ItemType Directory -Force -Path cover-unit - - $unitCoverage = (Get-ChildItem cover-*/unit).FullName -join "," - $integrationCoverage = (Get-ChildItem cover-*/int).FullName -join "," - - # Merge unit test coverage across platforms - go tool covdata merge -i="$unitCoverage" -o cover-unit - if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - - # Merge integration test coverage across platforms - go tool covdata merge -i="$integrationCoverage" -o cover-int - if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - - # Merge unit and integration code coverage - go tool covdata merge -i="cover-unit,cover-int" -o cover - if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - - # Convert to text format - go tool covdata textfmt -i=cover -o cover.out + New-Item -ItemType Directory -Force -Path merged-coverage + + # Find all coverage.out files from different platforms and test types + $unitCoverageFiles = @() + $integrationCoverageFiles = @() + + # Collect unit test coverage files from all platforms + Get-ChildItem "cover-*/unit/coverage.out" -ErrorAction SilentlyContinue | ForEach-Object { + $unitCoverageFiles += $_.FullName + Write-Host "Found unit coverage file: $($_.FullName)" + } + + # Collect integration test coverage files from all platforms + Get-ChildItem "cover-*/int/coverage.out" -ErrorAction SilentlyContinue | ForEach-Object { + $integrationCoverageFiles += $_.FullName + Write-Host "Found integration coverage file: $($_.FullName)" + } + + # Merge unit test coverage files + if ($unitCoverageFiles.Count -gt 0) { + Write-Host "Merging $($unitCoverageFiles.Count) unit test coverage files..." + $unitFilesParam = $unitCoverageFiles -join "," + & "$(Build.SourcesDirectory)/eng/scripts/Merge-GoCoverageProfiles.ps1" -InputFiles $unitFilesParam -OutputFile "merged-coverage/unit-coverage.out" + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + } else { + Write-Warning "No unit test coverage files found" + New-Item -ItemType File -Path "merged-coverage/unit-coverage.out" -Value "mode: set`n" + } + + # Merge integration test coverage files + if ($integrationCoverageFiles.Count -gt 0) { + Write-Host "Merging $($integrationCoverageFiles.Count) integration test coverage files..." + $intFilesParam = $integrationCoverageFiles -join "," + & "$(Build.SourcesDirectory)/eng/scripts/Merge-GoCoverageProfiles.ps1" -InputFiles $intFilesParam -OutputFile "merged-coverage/int-coverage.out" + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + } else { + Write-Warning "No integration test coverage files found" + New-Item -ItemType File -Path "merged-coverage/int-coverage.out" -Value "mode: set`n" + } + + # Merge unit and integration coverage into final coverage file + $allCoverageFiles = @("merged-coverage/unit-coverage.out", "merged-coverage/int-coverage.out") + Write-Host "Merging unit and integration coverage into final coverage file..." + $allFilesParam = $allCoverageFiles -join "," + & "$(Build.SourcesDirectory)/eng/scripts/Merge-GoCoverageProfiles.ps1" -InputFiles $allFilesParam -OutputFile "cover.out" if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Convert Go coverage to Cobertura XML format using custom script @@ -64,11 +88,11 @@ stages: templateContext: outputs: - output: pipelineArtifact - path: cover-unit - artifact: cover-unit - displayName: Upload unit test code coverage + path: merged-coverage + artifact: merged-coverage + displayName: Upload merged coverage profiles - output: pipelineArtifact - path: cover-int - artifact: cover-int - displayName: Upload integration test code coverage + path: coverage.xml + artifact: coverage-xml + displayName: Upload Cobertura XML coverage report diff --git a/eng/scripts/Merge-GoCoverageProfiles.ps1 b/eng/scripts/Merge-GoCoverageProfiles.ps1 new file mode 100644 index 00000000000..feb192e9471 --- /dev/null +++ b/eng/scripts/Merge-GoCoverageProfiles.ps1 @@ -0,0 +1,168 @@ +#!/usr/bin/env pwsh + +<# +.SYNOPSIS + Merges multiple Go coverage profiles into a single coverage profile. + +.DESCRIPTION + This script takes multiple Go coverage profile files (coverage.out format) and merges them + into a single unified coverage profile. It handles deduplication and combines coverage + counts for the same code blocks across different test runs or platforms. + +.PARAMETER InputFiles + Array of paths to the input coverage profile files to merge + +.PARAMETER OutputFile + Path where the merged coverage profile should be written + +.EXAMPLE + ./Merge-GoCoverageProfiles.ps1 -InputFiles @("unit.out", "integration.out") -OutputFile "merged.out" +#> + +param( + [Parameter(Mandatory = $true)] + [string[]]$InputFiles, + + [Parameter(Mandatory = $true)] + [string]$OutputFile +) + +$ErrorActionPreference = 'Stop' + +function Parse-CoverageProfile { + param([string]$FilePath) + + if (-not (Test-Path $FilePath)) { + Write-Warning "Coverage file not found: $FilePath" + return @{} + } + + $lines = Get-Content $FilePath + if ($lines.Count -eq 0) { + Write-Warning "Empty coverage file: $FilePath" + return @{} + } + + $coverage = @{} + + # Skip the mode line (first line) and process coverage data + for ($i = 1; $i -lt $lines.Count; $i++) { + $line = $lines[$i].Trim() + if ([string]::IsNullOrEmpty($line)) { continue } + + # Parse line format: file.go:startLine.startCol,endLine.endCol numStmt count + if ($line -match '^(.+):(\d+)\.(\d+),(\d+)\.(\d+) (\d+) (\d+)$') { + $file = $matches[1] + $startLine = [int]$matches[2] + $startCol = [int]$matches[3] + $endLine = [int]$matches[4] + $endCol = [int]$matches[5] + $numStmt = [int]$matches[6] + $count = [int]$matches[7] + + # Create a unique key for this code block + $blockKey = "${file}:${startLine}.${startCol},${endLine}.${endCol}" + + if ($coverage.ContainsKey($blockKey)) { + # Merge coverage counts for the same block + $coverage[$blockKey].count += $count + } else { + $coverage[$blockKey] = @{ + file = $file + startLine = $startLine + startCol = $startCol + endLine = $endLine + endCol = $endCol + numStmt = $numStmt + count = $count + } + } + } + } + + return $coverage +} + +function Write-CoverageProfile { + param( + [hashtable]$Coverage, + [string]$OutputPath + ) + + $outputLines = @("mode: set") + + # Sort blocks by file and then by line number for consistent output + $sortedBlocks = $Coverage.GetEnumerator() | Sort-Object { + $block = $_.Value + "$($block.file):$($block.startLine.ToString().PadLeft(10, '0')):$($block.startCol.ToString().PadLeft(10, '0'))" + } + + foreach ($entry in $sortedBlocks) { + $block = $entry.Value + $line = "$($block.file):$($block.startLine).$($block.startCol),$($block.endLine).$($block.endCol) $($block.numStmt) $($block.count)" + $outputLines += $line + } + + # Write to output file + $outputLines | Out-File -FilePath $OutputPath -Encoding UTF8 +} + +# Main execution +Write-Host "Merging $($InputFiles.Count) Go coverage profile(s)..." + +# Handle case where InputFiles might be passed as a single comma-separated string +if ($InputFiles.Count -eq 1 -and $InputFiles[0].Contains(',')) { + $InputFiles = $InputFiles[0] -split ',' +} + +$mergedCoverage = @{} +$totalFiles = 0 + +foreach ($inputFile in $InputFiles) { + if (-not (Test-Path $inputFile)) { + Write-Warning "Input file not found: $inputFile" + continue + } + + Write-Host "Processing: $inputFile" + $fileCoverage = Parse-CoverageProfile -FilePath $inputFile + + if ($fileCoverage.Count -eq 0) { + Write-Warning "No coverage data found in: $inputFile" + continue + } + + $totalFiles++ + + # Merge this file's coverage into the overall coverage + foreach ($blockKey in $fileCoverage.Keys) { + if ($mergedCoverage.ContainsKey($blockKey)) { + # Add coverage counts for the same block + $mergedCoverage[$blockKey].count += $fileCoverage[$blockKey].count + } else { + # Copy the block data + $mergedCoverage[$blockKey] = $fileCoverage[$blockKey].Clone() + } + } +} + +if ($totalFiles -eq 0) { + Write-Warning "No valid coverage files found. Creating empty coverage profile." + @("mode: set") | Out-File -FilePath $OutputFile -Encoding UTF8 +} else { + Write-Host "Writing merged coverage profile to: $OutputFile" + Write-CoverageProfile -Coverage $mergedCoverage -OutputPath $OutputFile + + # Display summary + $totalBlocks = $mergedCoverage.Count + $coveredBlocks = ($mergedCoverage.Values | Where-Object { $_.count -gt 0 }).Count + $coveragePercentage = if ($totalBlocks -gt 0) { ($coveredBlocks / $totalBlocks) * 100 } else { 0 } + + Write-Host "Merge Summary:" + Write-Host " Input files processed: $totalFiles" + Write-Host " Total code blocks: $totalBlocks" + Write-Host " Covered blocks: $coveredBlocks" + Write-Host " Coverage: $($coveragePercentage.ToString('F2'))%" +} + +Write-Host "Coverage merge completed successfully!" From 929a901d1c5378e974eb6334db2551e8b3b40bc1 Mon Sep 17 00:00:00 2001 From: Victor Vazquez Date: Sat, 16 Aug 2025 06:00:49 +0000 Subject: [PATCH 7/9] fix merging logic --- .../templates/stages/code-coverage-upload.yml | 31 +++- eng/scripts/Merge-GoCoverageProfiles.ps1 | 169 +++++++++++------- 2 files changed, 123 insertions(+), 77 deletions(-) diff --git a/eng/pipelines/templates/stages/code-coverage-upload.yml b/eng/pipelines/templates/stages/code-coverage-upload.yml index a05a012172e..5e44ba53b32 100644 --- a/eng/pipelines/templates/stages/code-coverage-upload.yml +++ b/eng/pipelines/templates/stages/code-coverage-upload.yml @@ -49,8 +49,11 @@ stages: if ($unitCoverageFiles.Count -gt 0) { Write-Host "Merging $($unitCoverageFiles.Count) unit test coverage files..." $unitFilesParam = $unitCoverageFiles -join "," - & "$(Build.SourcesDirectory)/eng/scripts/Merge-GoCoverageProfiles.ps1" -InputFiles $unitFilesParam -OutputFile "merged-coverage/unit-coverage.out" - if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + pwsh -File "$(Build.SourcesDirectory)/eng/scripts/Merge-GoCoverageProfiles.ps1" -InputFiles $unitFilesParam -OutputFile "merged-coverage/unit-coverage.out" + if ($LASTEXITCODE -ne 0) { + Write-Error "Failed to merge unit test coverage files" + exit $LASTEXITCODE + } } else { Write-Warning "No unit test coverage files found" New-Item -ItemType File -Path "merged-coverage/unit-coverage.out" -Value "mode: set`n" @@ -60,23 +63,33 @@ stages: if ($integrationCoverageFiles.Count -gt 0) { Write-Host "Merging $($integrationCoverageFiles.Count) integration test coverage files..." $intFilesParam = $integrationCoverageFiles -join "," - & "$(Build.SourcesDirectory)/eng/scripts/Merge-GoCoverageProfiles.ps1" -InputFiles $intFilesParam -OutputFile "merged-coverage/int-coverage.out" - if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + pwsh -File "$(Build.SourcesDirectory)/eng/scripts/Merge-GoCoverageProfiles.ps1" -InputFiles $intFilesParam -OutputFile "merged-coverage/int-coverage.out" + if ($LASTEXITCODE -ne 0) { + Write-Error "Failed to merge integration test coverage files" + exit $LASTEXITCODE + } } else { Write-Warning "No integration test coverage files found" New-Item -ItemType File -Path "merged-coverage/int-coverage.out" -Value "mode: set`n" } # Merge unit and integration coverage into final coverage file - $allCoverageFiles = @("merged-coverage/unit-coverage.out", "merged-coverage/int-coverage.out") Write-Host "Merging unit and integration coverage into final coverage file..." + $allCoverageFiles = @("merged-coverage/unit-coverage.out", "merged-coverage/int-coverage.out") $allFilesParam = $allCoverageFiles -join "," - & "$(Build.SourcesDirectory)/eng/scripts/Merge-GoCoverageProfiles.ps1" -InputFiles $allFilesParam -OutputFile "cover.out" - if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + pwsh -File "$(Build.SourcesDirectory)/eng/scripts/Merge-GoCoverageProfiles.ps1" -InputFiles $allFilesParam -OutputFile "cover.out" + if ($LASTEXITCODE -ne 0) { + Write-Error "Failed to merge final coverage files" + exit $LASTEXITCODE + } # Convert Go coverage to Cobertura XML format using custom script - & "$(Build.SourcesDirectory)/eng/scripts/Convert-GoCoverageToCobertura.ps1" -CoverageFile cover.out -OutputFile coverage.xml -SourceRoot "$(Build.SourcesDirectory)" - if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + Write-Host "Converting coverage to Cobertura XML format..." + pwsh -File "$(Build.SourcesDirectory)/eng/scripts/Convert-GoCoverageToCobertura.ps1" -CoverageFile cover.out -OutputFile coverage.xml -SourceRoot "$(Build.SourcesDirectory)" + if ($LASTEXITCODE -ne 0) { + Write-Error "Failed to convert coverage to XML" + exit $LASTEXITCODE + } displayName: Merge code coverage files - task: PublishCodeCoverageResults@1 diff --git a/eng/scripts/Merge-GoCoverageProfiles.ps1 b/eng/scripts/Merge-GoCoverageProfiles.ps1 index feb192e9471..944048a32d9 100644 --- a/eng/scripts/Merge-GoCoverageProfiles.ps1 +++ b/eng/scripts/Merge-GoCoverageProfiles.ps1 @@ -37,50 +37,58 @@ function Parse-CoverageProfile { return @{} } - $lines = Get-Content $FilePath - if ($lines.Count -eq 0) { - Write-Warning "Empty coverage file: $FilePath" - return @{} - } - - $coverage = @{} - - # Skip the mode line (first line) and process coverage data - for ($i = 1; $i -lt $lines.Count; $i++) { - $line = $lines[$i].Trim() - if ([string]::IsNullOrEmpty($line)) { continue } + try { + $lines = Get-Content $FilePath -ErrorAction Stop + if ($lines.Count -eq 0) { + Write-Warning "Empty coverage file: $FilePath" + return @{} + } - # Parse line format: file.go:startLine.startCol,endLine.endCol numStmt count - if ($line -match '^(.+):(\d+)\.(\d+),(\d+)\.(\d+) (\d+) (\d+)$') { - $file = $matches[1] - $startLine = [int]$matches[2] - $startCol = [int]$matches[3] - $endLine = [int]$matches[4] - $endCol = [int]$matches[5] - $numStmt = [int]$matches[6] - $count = [int]$matches[7] - - # Create a unique key for this code block - $blockKey = "${file}:${startLine}.${startCol},${endLine}.${endCol}" + $coverage = @{} + + # Skip the mode line (first line) and process coverage data + for ($i = 1; $i -lt $lines.Count; $i++) { + $line = $lines[$i].Trim() + if ([string]::IsNullOrEmpty($line)) { continue } - if ($coverage.ContainsKey($blockKey)) { - # Merge coverage counts for the same block - $coverage[$blockKey].count += $count - } else { - $coverage[$blockKey] = @{ - file = $file - startLine = $startLine - startCol = $startCol - endLine = $endLine - endCol = $endCol - numStmt = $numStmt - count = $count + # Parse line format: file.go:startLine.startCol,endLine.endCol numStmt count + if ($line -match '^(.+):(\d+)\.(\d+),(\d+)\.(\d+) (\d+) (\d+)$') { + $file = $matches[1] + $startLine = [int]$matches[2] + $startCol = [int]$matches[3] + $endLine = [int]$matches[4] + $endCol = [int]$matches[5] + $numStmt = [int]$matches[6] + $count = [int]$matches[7] + + # Create a unique key for this code block + $blockKey = "${file}:${startLine}.${startCol},${endLine}.${endCol}" + + if ($coverage.ContainsKey($blockKey)) { + # Merge coverage counts for the same block + $coverage[$blockKey].count += $count + } else { + $coverage[$blockKey] = @{ + file = $file + startLine = $startLine + startCol = $startCol + endLine = $endLine + endCol = $endCol + numStmt = $numStmt + count = $count + } } + } else { + Write-Warning "Skipping invalid coverage line in $FilePath : $line" } } + + return $coverage + } + catch { + Write-Error "Error reading coverage file $FilePath : $_" + throw } - - return $coverage } function Write-CoverageProfile { @@ -119,50 +127,75 @@ $mergedCoverage = @{} $totalFiles = 0 foreach ($inputFile in $InputFiles) { + $inputFile = $inputFile.Trim() + if ([string]::IsNullOrEmpty($inputFile)) { continue } + if (-not (Test-Path $inputFile)) { Write-Warning "Input file not found: $inputFile" continue } Write-Host "Processing: $inputFile" - $fileCoverage = Parse-CoverageProfile -FilePath $inputFile - - if ($fileCoverage.Count -eq 0) { - Write-Warning "No coverage data found in: $inputFile" - continue - } - - $totalFiles++ - - # Merge this file's coverage into the overall coverage - foreach ($blockKey in $fileCoverage.Keys) { - if ($mergedCoverage.ContainsKey($blockKey)) { - # Add coverage counts for the same block - $mergedCoverage[$blockKey].count += $fileCoverage[$blockKey].count - } else { - # Copy the block data - $mergedCoverage[$blockKey] = $fileCoverage[$blockKey].Clone() + try { + $fileCoverage = Parse-CoverageProfile -FilePath $inputFile + + if ($fileCoverage.Count -eq 0) { + Write-Warning "No coverage data found in: $inputFile" + continue } + + $totalFiles++ + + # Merge this file's coverage into the overall coverage + foreach ($blockKey in $fileCoverage.Keys) { + if ($mergedCoverage.ContainsKey($blockKey)) { + # Add coverage counts for the same block + $mergedCoverage[$blockKey].count += $fileCoverage[$blockKey].count + } else { + # Copy the block data + $mergedCoverage[$blockKey] = $fileCoverage[$blockKey].Clone() + } + } + + Write-Host "Successfully processed $inputFile with $($fileCoverage.Count) coverage blocks" + } + catch { + Write-Error "Failed to process coverage file $inputFile : $_" + exit 1 } } if ($totalFiles -eq 0) { Write-Warning "No valid coverage files found. Creating empty coverage profile." - @("mode: set") | Out-File -FilePath $OutputFile -Encoding UTF8 + try { + @("mode: set") | Out-File -FilePath $OutputFile -Encoding UTF8 + Write-Host "Created empty coverage profile: $OutputFile" + } + catch { + Write-Error "Failed to create empty coverage profile: $_" + exit 1 + } } else { Write-Host "Writing merged coverage profile to: $OutputFile" - Write-CoverageProfile -Coverage $mergedCoverage -OutputPath $OutputFile - - # Display summary - $totalBlocks = $mergedCoverage.Count - $coveredBlocks = ($mergedCoverage.Values | Where-Object { $_.count -gt 0 }).Count - $coveragePercentage = if ($totalBlocks -gt 0) { ($coveredBlocks / $totalBlocks) * 100 } else { 0 } - - Write-Host "Merge Summary:" - Write-Host " Input files processed: $totalFiles" - Write-Host " Total code blocks: $totalBlocks" - Write-Host " Covered blocks: $coveredBlocks" - Write-Host " Coverage: $($coveragePercentage.ToString('F2'))%" + try { + Write-CoverageProfile -Coverage $mergedCoverage -OutputPath $OutputFile + + # Display summary + $totalBlocks = $mergedCoverage.Count + $coveredBlocks = ($mergedCoverage.Values | Where-Object { $_.count -gt 0 }).Count + $coveragePercentage = if ($totalBlocks -gt 0) { ($coveredBlocks / $totalBlocks) * 100 } else { 0 } + + Write-Host "Merge Summary:" + Write-Host " Input files processed: $totalFiles" + Write-Host " Total code blocks: $totalBlocks" + Write-Host " Covered blocks: $coveredBlocks" + Write-Host " Coverage: $($coveragePercentage.ToString('F2'))%" + } + catch { + Write-Error "Failed to write merged coverage profile: $_" + exit 1 + } } Write-Host "Coverage merge completed successfully!" +exit 0 From 02859554a1c0eefef7a6acde6bdaa5bbeaf56f9c Mon Sep 17 00:00:00 2001 From: Victor Vazquez Date: Mon, 18 Aug 2025 00:41:45 +0000 Subject: [PATCH 8/9] path normalization --- .../templates/stages/code-coverage-upload.yml | 11 +++- eng/scripts/Convert-GoCoverageToCobertura.ps1 | 60 ++++++++++++++++++- eng/scripts/Merge-GoCoverageProfiles.ps1 | 46 ++++++++++++-- 3 files changed, 107 insertions(+), 10 deletions(-) diff --git a/eng/pipelines/templates/stages/code-coverage-upload.yml b/eng/pipelines/templates/stages/code-coverage-upload.yml index 5e44ba53b32..7884fc7a003 100644 --- a/eng/pipelines/templates/stages/code-coverage-upload.yml +++ b/eng/pipelines/templates/stages/code-coverage-upload.yml @@ -49,7 +49,7 @@ stages: if ($unitCoverageFiles.Count -gt 0) { Write-Host "Merging $($unitCoverageFiles.Count) unit test coverage files..." $unitFilesParam = $unitCoverageFiles -join "," - pwsh -File "$(Build.SourcesDirectory)/eng/scripts/Merge-GoCoverageProfiles.ps1" -InputFiles $unitFilesParam -OutputFile "merged-coverage/unit-coverage.out" + pwsh -File "$(Build.SourcesDirectory)/eng/scripts/Merge-GoCoverageProfiles.ps1" -InputFiles $unitFilesParam -OutputFile "merged-coverage/unit-coverage.out" -NormalizePaths if ($LASTEXITCODE -ne 0) { Write-Error "Failed to merge unit test coverage files" exit $LASTEXITCODE @@ -63,7 +63,7 @@ stages: if ($integrationCoverageFiles.Count -gt 0) { Write-Host "Merging $($integrationCoverageFiles.Count) integration test coverage files..." $intFilesParam = $integrationCoverageFiles -join "," - pwsh -File "$(Build.SourcesDirectory)/eng/scripts/Merge-GoCoverageProfiles.ps1" -InputFiles $intFilesParam -OutputFile "merged-coverage/int-coverage.out" + pwsh -File "$(Build.SourcesDirectory)/eng/scripts/Merge-GoCoverageProfiles.ps1" -InputFiles $intFilesParam -OutputFile "merged-coverage/int-coverage.out" -NormalizePaths if ($LASTEXITCODE -ne 0) { Write-Error "Failed to merge integration test coverage files" exit $LASTEXITCODE @@ -77,7 +77,7 @@ stages: Write-Host "Merging unit and integration coverage into final coverage file..." $allCoverageFiles = @("merged-coverage/unit-coverage.out", "merged-coverage/int-coverage.out") $allFilesParam = $allCoverageFiles -join "," - pwsh -File "$(Build.SourcesDirectory)/eng/scripts/Merge-GoCoverageProfiles.ps1" -InputFiles $allFilesParam -OutputFile "cover.out" + pwsh -File "$(Build.SourcesDirectory)/eng/scripts/Merge-GoCoverageProfiles.ps1" -InputFiles $allFilesParam -OutputFile "cover.out" -NormalizePaths if ($LASTEXITCODE -ne 0) { Write-Error "Failed to merge final coverage files" exit $LASTEXITCODE @@ -90,6 +90,11 @@ stages: Write-Error "Failed to convert coverage to XML" exit $LASTEXITCODE } + + # Debug: Show some sample file paths from the coverage XML + Write-Host "Sample file paths in coverage XML:" + $xmlContent = Get-Content coverage.xml | Select-String 'filename=' | Select-Object -First 5 + $xmlContent | ForEach-Object { Write-Host " $_" } displayName: Merge code coverage files - task: PublishCodeCoverageResults@1 diff --git a/eng/scripts/Convert-GoCoverageToCobertura.ps1 b/eng/scripts/Convert-GoCoverageToCobertura.ps1 index 31053d53325..eefb4f01514 100644 --- a/eng/scripts/Convert-GoCoverageToCobertura.ps1 +++ b/eng/scripts/Convert-GoCoverageToCobertura.ps1 @@ -45,8 +45,59 @@ function Get-RelativePath { return $resolvedPath.TrimStart('.', '\', '/') } +function Normalize-GoFilePath { + param( + [string]$GoFilePath, + [string]$SourceRoot + ) + + # Handle Go module paths like "github.com/azure/azure-dev/cli/azd/pkg/test/file.go" + # Convert to relative paths like "cli/azd/pkg/test/file.go" + + # Common Go module path patterns to normalize + $modulePatterns = @( + "github.com/azure/azure-dev/", + "github.com/Azure/azure-dev/" + ) + + $normalizedPath = $GoFilePath + + foreach ($pattern in $modulePatterns) { + if ($normalizedPath.StartsWith($pattern)) { + $normalizedPath = $normalizedPath.Substring($pattern.Length) + break + } + } + + # Ensure we use forward slashes for consistency + $normalizedPath = $normalizedPath.Replace('\', '/') + + # Verify the file exists relative to source root + $fullPath = Join-Path $SourceRoot $normalizedPath + if (Test-Path $fullPath) { + return $normalizedPath + } else { + # If the normalized path doesn't exist, try to find it relative to cli/azd + $cliPath = $normalizedPath + if ($normalizedPath.StartsWith("cli/azd/")) { + $cliPath = $normalizedPath.Substring("cli/azd/".Length) + } + + $cliFullPath = Join-Path $SourceRoot "cli/azd/$cliPath" + if (Test-Path $cliFullPath) { + return "cli/azd/$cliPath" + } + } + + # Return the normalized path even if file doesn't exist (for build artifacts) + return $normalizedPath +} + function Parse-GoCoverageFile { - param([string]$FilePath) + param( + [string]$FilePath, + [string]$SourceRoot + ) $coverage = @{} $lines = Get-Content $FilePath @@ -58,12 +109,15 @@ function Parse-GoCoverageFile { # Parse line format: file.go:startLine.startCol,endLine.endCol numStmt count if ($line -match '^(.+):(\d+)\.(\d+),(\d+)\.(\d+) (\d+) (\d+)$') { - $file = $matches[1] + $originalFile = $matches[1] $startLine = [int]$matches[2] $endLine = [int]$matches[4] $numStmt = [int]$matches[6] $count = [int]$matches[7] + # Normalize the file path to be relative to source root + $file = Normalize-GoFilePath -GoFilePath $originalFile -SourceRoot $SourceRoot + if (-not $coverage.ContainsKey($file)) { $coverage[$file] = @{} } @@ -219,7 +273,7 @@ if (-not (Test-Path $CoverageFile)) { } # Parse the Go coverage file -$coverage = Parse-GoCoverageFile -FilePath $CoverageFile +$coverage = Parse-GoCoverageFile -FilePath $CoverageFile -SourceRoot $SourceRoot # Generate Cobertura XML $xml = Generate-CoberturaXml -Coverage $coverage -SourceRoot $SourceRoot diff --git a/eng/scripts/Merge-GoCoverageProfiles.ps1 b/eng/scripts/Merge-GoCoverageProfiles.ps1 index 944048a32d9..318a46ea89b 100644 --- a/eng/scripts/Merge-GoCoverageProfiles.ps1 +++ b/eng/scripts/Merge-GoCoverageProfiles.ps1 @@ -24,13 +24,44 @@ param( [string[]]$InputFiles, [Parameter(Mandatory = $true)] - [string]$OutputFile + [string]$OutputFile, + + [Parameter(Mandatory = $false)] + [switch]$NormalizePaths ) $ErrorActionPreference = 'Stop' +function Normalize-GoFilePath { + param([string]$GoFilePath) + + # Handle Go module paths like "github.com/azure/azure-dev/cli/azd/pkg/test/file.go" + # Convert to relative paths like "cli/azd/pkg/test/file.go" + + # Common Go module path patterns to normalize + $modulePatterns = @( + "github.com/azure/azure-dev/", + "github.com/Azure/azure-dev/" + ) + + $normalizedPath = $GoFilePath + + foreach ($pattern in $modulePatterns) { + if ($normalizedPath.StartsWith($pattern)) { + $normalizedPath = $normalizedPath.Substring($pattern.Length) + break + } + } + + # Ensure we use forward slashes for consistency + return $normalizedPath.Replace('\', '/') +} + function Parse-CoverageProfile { - param([string]$FilePath) + param( + [string]$FilePath, + [bool]$NormalizePaths = $false + ) if (-not (Test-Path $FilePath)) { Write-Warning "Coverage file not found: $FilePath" @@ -53,7 +84,7 @@ function Parse-CoverageProfile { # Parse line format: file.go:startLine.startCol,endLine.endCol numStmt count if ($line -match '^(.+):(\d+)\.(\d+),(\d+)\.(\d+) (\d+) (\d+)$') { - $file = $matches[1] + $originalFile = $matches[1] $startLine = [int]$matches[2] $startCol = [int]$matches[3] $endLine = [int]$matches[4] @@ -61,6 +92,13 @@ function Parse-CoverageProfile { $numStmt = [int]$matches[6] $count = [int]$matches[7] + # Normalize file path if requested + $file = if ($NormalizePaths) { + Normalize-GoFilePath -GoFilePath $originalFile + } else { + $originalFile + } + # Create a unique key for this code block $blockKey = "${file}:${startLine}.${startCol},${endLine}.${endCol}" @@ -137,7 +175,7 @@ foreach ($inputFile in $InputFiles) { Write-Host "Processing: $inputFile" try { - $fileCoverage = Parse-CoverageProfile -FilePath $inputFile + $fileCoverage = Parse-CoverageProfile -FilePath $inputFile -NormalizePaths $NormalizePaths.IsPresent if ($fileCoverage.Count -eq 0) { Write-Warning "No coverage data found in: $inputFile" From bda4d21391874bd5759fa57573de148008723648 Mon Sep 17 00:00:00 2001 From: Victor Vazquez Date: Tue, 19 Aug 2025 21:30:42 +0000 Subject: [PATCH 9/9] more cov changes --- cli/azd/ci-test.ps1 | 16 +- .../templates/stages/code-coverage-upload.yml | 94 ++----- eng/scripts/Merge-GoCoverageProfiles.ps1 | 239 ------------------ 3 files changed, 35 insertions(+), 314 deletions(-) delete mode 100644 eng/scripts/Merge-GoCoverageProfiles.ps1 diff --git a/cli/azd/ci-test.ps1 b/cli/azd/ci-test.ps1 index 75c8f05cd0a..c2f59857245 100644 --- a/cli/azd/ci-test.ps1 +++ b/cli/azd/ci-test.ps1 @@ -33,11 +33,11 @@ function New-EmptyDirectory { $unitCoverDir = New-EmptyDirectory -Path $UnitTestCoverageDir Write-Host "Running unit tests..." -# Using -coverprofile flag introduced in Go 1.21 for modern coverage collection -# This replaces the older --test.gocoverdir approach and provides better integration -# with the standard Go toolchain for coverage reporting. -$unitCoverProfile = Join-Path $unitCoverDir.FullName "coverage.out" -& $gotestsum -- ./... -short -v -coverprofile="$unitCoverProfile" +# --test.gocoverdir is currently a "under-the-cover" way to pass the coverage directory to a test binary +# See https://github.com/golang/go/issues/51430#issuecomment-1344711300 +# +# As of Go 1.25, it’s still an “under-the-hood” option. +& $gotestsum -- ./... -short -v -cover -args --test.gocoverdir="$($unitCoverDir.FullName)" if ($LASTEXITCODE) { exit $LASTEXITCODE } @@ -54,15 +54,13 @@ $oldGOCOVERDIR = $env:GOCOVERDIR $oldGOEXPERIMENT = $env:GOEXPERIMENT # GOCOVERDIR enables any binaries (in this case, azd.exe) built with '-cover', -# to write out coverage output to the specific coverage directory. -# This works in conjunction with the -coverprofile flag for comprehensive coverage reporting. +# to write out coverage output to the specific directory. $env:GOCOVERDIR = $intCoverDir.FullName # Set any experiment flags that are needed for the tests. $env:GOEXPERIMENT="" try { - $intCoverProfile = Join-Path $intCoverDir.FullName "coverage.out" - & $gotestsum -- ./... -v -timeout $IntegrationTestTimeout -coverprofile="$intCoverProfile" + & $gotestsum -- ./... -v -timeout $IntegrationTestTimeout if ($LASTEXITCODE) { exit $LASTEXITCODE } diff --git a/eng/pipelines/templates/stages/code-coverage-upload.yml b/eng/pipelines/templates/stages/code-coverage-upload.yml index 7884fc7a003..84942e982fd 100644 --- a/eng/pipelines/templates/stages/code-coverage-upload.yml +++ b/eng/pipelines/templates/stages/code-coverage-upload.yml @@ -27,61 +27,28 @@ stages: Artifacts: ${{ parameters.DownloadArtifacts }} - pwsh: | - New-Item -ItemType Directory -Force -Path merged-coverage - - # Find all coverage.out files from different platforms and test types - $unitCoverageFiles = @() - $integrationCoverageFiles = @() - - # Collect unit test coverage files from all platforms - Get-ChildItem "cover-*/unit/coverage.out" -ErrorAction SilentlyContinue | ForEach-Object { - $unitCoverageFiles += $_.FullName - Write-Host "Found unit coverage file: $($_.FullName)" - } - - # Collect integration test coverage files from all platforms - Get-ChildItem "cover-*/int/coverage.out" -ErrorAction SilentlyContinue | ForEach-Object { - $integrationCoverageFiles += $_.FullName - Write-Host "Found integration coverage file: $($_.FullName)" - } - - # Merge unit test coverage files - if ($unitCoverageFiles.Count -gt 0) { - Write-Host "Merging $($unitCoverageFiles.Count) unit test coverage files..." - $unitFilesParam = $unitCoverageFiles -join "," - pwsh -File "$(Build.SourcesDirectory)/eng/scripts/Merge-GoCoverageProfiles.ps1" -InputFiles $unitFilesParam -OutputFile "merged-coverage/unit-coverage.out" -NormalizePaths - if ($LASTEXITCODE -ne 0) { - Write-Error "Failed to merge unit test coverage files" - exit $LASTEXITCODE - } - } else { - Write-Warning "No unit test coverage files found" - New-Item -ItemType File -Path "merged-coverage/unit-coverage.out" -Value "mode: set`n" - } - - # Merge integration test coverage files - if ($integrationCoverageFiles.Count -gt 0) { - Write-Host "Merging $($integrationCoverageFiles.Count) integration test coverage files..." - $intFilesParam = $integrationCoverageFiles -join "," - pwsh -File "$(Build.SourcesDirectory)/eng/scripts/Merge-GoCoverageProfiles.ps1" -InputFiles $intFilesParam -OutputFile "merged-coverage/int-coverage.out" -NormalizePaths - if ($LASTEXITCODE -ne 0) { - Write-Error "Failed to merge integration test coverage files" - exit $LASTEXITCODE - } - } else { - Write-Warning "No integration test coverage files found" - New-Item -ItemType File -Path "merged-coverage/int-coverage.out" -Value "mode: set`n" - } - - # Merge unit and integration coverage into final coverage file - Write-Host "Merging unit and integration coverage into final coverage file..." - $allCoverageFiles = @("merged-coverage/unit-coverage.out", "merged-coverage/int-coverage.out") - $allFilesParam = $allCoverageFiles -join "," - pwsh -File "$(Build.SourcesDirectory)/eng/scripts/Merge-GoCoverageProfiles.ps1" -InputFiles $allFilesParam -OutputFile "cover.out" -NormalizePaths - if ($LASTEXITCODE -ne 0) { - Write-Error "Failed to merge final coverage files" - exit $LASTEXITCODE - } + New-Item -ItemType Directory -Force -Path cover + New-Item -ItemType Directory -Force -Path cover-int + New-Item -ItemType Directory -Force -Path cover-unit + + $unitCoverage = (Get-ChildItem cover-*/unit).FullName -join "," + $integrationCoverage = (Get-ChildItem cover-*/int).FullName -join "," + + # Merge unit test coverage across platforms + go tool covdata merge -i="$unitCoverage" -o cover-unit + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + + # Merge integration test coverage across platforms + go tool covdata merge -i="$integrationCoverage" -o cover-int + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + + # Merge unit and integration code coverage + go tool covdata merge -i="cover-unit,cover-int" -o cover + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + + # Convert to text format + go tool covdata textfmt -i=cover -o cover.out + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # Convert Go coverage to Cobertura XML format using custom script Write-Host "Converting coverage to Cobertura XML format..." @@ -90,11 +57,6 @@ stages: Write-Error "Failed to convert coverage to XML" exit $LASTEXITCODE } - - # Debug: Show some sample file paths from the coverage XML - Write-Host "Sample file paths in coverage XML:" - $xmlContent = Get-Content coverage.xml | Select-String 'filename=' | Select-Object -First 5 - $xmlContent | ForEach-Object { Write-Host " $_" } displayName: Merge code coverage files - task: PublishCodeCoverageResults@1 @@ -106,11 +68,11 @@ stages: templateContext: outputs: - output: pipelineArtifact - path: merged-coverage - artifact: merged-coverage - displayName: Upload merged coverage profiles + path: cover-unit + artifact: cover-unit + displayName: Upload unit test code coverage - output: pipelineArtifact - path: coverage.xml - artifact: coverage-xml - displayName: Upload Cobertura XML coverage report + path: cover-int + artifact: cover-int + displayName: Upload integration test code coverage diff --git a/eng/scripts/Merge-GoCoverageProfiles.ps1 b/eng/scripts/Merge-GoCoverageProfiles.ps1 deleted file mode 100644 index 318a46ea89b..00000000000 --- a/eng/scripts/Merge-GoCoverageProfiles.ps1 +++ /dev/null @@ -1,239 +0,0 @@ -#!/usr/bin/env pwsh - -<# -.SYNOPSIS - Merges multiple Go coverage profiles into a single coverage profile. - -.DESCRIPTION - This script takes multiple Go coverage profile files (coverage.out format) and merges them - into a single unified coverage profile. It handles deduplication and combines coverage - counts for the same code blocks across different test runs or platforms. - -.PARAMETER InputFiles - Array of paths to the input coverage profile files to merge - -.PARAMETER OutputFile - Path where the merged coverage profile should be written - -.EXAMPLE - ./Merge-GoCoverageProfiles.ps1 -InputFiles @("unit.out", "integration.out") -OutputFile "merged.out" -#> - -param( - [Parameter(Mandatory = $true)] - [string[]]$InputFiles, - - [Parameter(Mandatory = $true)] - [string]$OutputFile, - - [Parameter(Mandatory = $false)] - [switch]$NormalizePaths -) - -$ErrorActionPreference = 'Stop' - -function Normalize-GoFilePath { - param([string]$GoFilePath) - - # Handle Go module paths like "github.com/azure/azure-dev/cli/azd/pkg/test/file.go" - # Convert to relative paths like "cli/azd/pkg/test/file.go" - - # Common Go module path patterns to normalize - $modulePatterns = @( - "github.com/azure/azure-dev/", - "github.com/Azure/azure-dev/" - ) - - $normalizedPath = $GoFilePath - - foreach ($pattern in $modulePatterns) { - if ($normalizedPath.StartsWith($pattern)) { - $normalizedPath = $normalizedPath.Substring($pattern.Length) - break - } - } - - # Ensure we use forward slashes for consistency - return $normalizedPath.Replace('\', '/') -} - -function Parse-CoverageProfile { - param( - [string]$FilePath, - [bool]$NormalizePaths = $false - ) - - if (-not (Test-Path $FilePath)) { - Write-Warning "Coverage file not found: $FilePath" - return @{} - } - - try { - $lines = Get-Content $FilePath -ErrorAction Stop - if ($lines.Count -eq 0) { - Write-Warning "Empty coverage file: $FilePath" - return @{} - } - - $coverage = @{} - - # Skip the mode line (first line) and process coverage data - for ($i = 1; $i -lt $lines.Count; $i++) { - $line = $lines[$i].Trim() - if ([string]::IsNullOrEmpty($line)) { continue } - - # Parse line format: file.go:startLine.startCol,endLine.endCol numStmt count - if ($line -match '^(.+):(\d+)\.(\d+),(\d+)\.(\d+) (\d+) (\d+)$') { - $originalFile = $matches[1] - $startLine = [int]$matches[2] - $startCol = [int]$matches[3] - $endLine = [int]$matches[4] - $endCol = [int]$matches[5] - $numStmt = [int]$matches[6] - $count = [int]$matches[7] - - # Normalize file path if requested - $file = if ($NormalizePaths) { - Normalize-GoFilePath -GoFilePath $originalFile - } else { - $originalFile - } - - # Create a unique key for this code block - $blockKey = "${file}:${startLine}.${startCol},${endLine}.${endCol}" - - if ($coverage.ContainsKey($blockKey)) { - # Merge coverage counts for the same block - $coverage[$blockKey].count += $count - } else { - $coverage[$blockKey] = @{ - file = $file - startLine = $startLine - startCol = $startCol - endLine = $endLine - endCol = $endCol - numStmt = $numStmt - count = $count - } - } - } else { - Write-Warning "Skipping invalid coverage line in $FilePath : $line" - } - } - - return $coverage - } - catch { - Write-Error "Error reading coverage file $FilePath : $_" - throw - } -} - -function Write-CoverageProfile { - param( - [hashtable]$Coverage, - [string]$OutputPath - ) - - $outputLines = @("mode: set") - - # Sort blocks by file and then by line number for consistent output - $sortedBlocks = $Coverage.GetEnumerator() | Sort-Object { - $block = $_.Value - "$($block.file):$($block.startLine.ToString().PadLeft(10, '0')):$($block.startCol.ToString().PadLeft(10, '0'))" - } - - foreach ($entry in $sortedBlocks) { - $block = $entry.Value - $line = "$($block.file):$($block.startLine).$($block.startCol),$($block.endLine).$($block.endCol) $($block.numStmt) $($block.count)" - $outputLines += $line - } - - # Write to output file - $outputLines | Out-File -FilePath $OutputPath -Encoding UTF8 -} - -# Main execution -Write-Host "Merging $($InputFiles.Count) Go coverage profile(s)..." - -# Handle case where InputFiles might be passed as a single comma-separated string -if ($InputFiles.Count -eq 1 -and $InputFiles[0].Contains(',')) { - $InputFiles = $InputFiles[0] -split ',' -} - -$mergedCoverage = @{} -$totalFiles = 0 - -foreach ($inputFile in $InputFiles) { - $inputFile = $inputFile.Trim() - if ([string]::IsNullOrEmpty($inputFile)) { continue } - - if (-not (Test-Path $inputFile)) { - Write-Warning "Input file not found: $inputFile" - continue - } - - Write-Host "Processing: $inputFile" - try { - $fileCoverage = Parse-CoverageProfile -FilePath $inputFile -NormalizePaths $NormalizePaths.IsPresent - - if ($fileCoverage.Count -eq 0) { - Write-Warning "No coverage data found in: $inputFile" - continue - } - - $totalFiles++ - - # Merge this file's coverage into the overall coverage - foreach ($blockKey in $fileCoverage.Keys) { - if ($mergedCoverage.ContainsKey($blockKey)) { - # Add coverage counts for the same block - $mergedCoverage[$blockKey].count += $fileCoverage[$blockKey].count - } else { - # Copy the block data - $mergedCoverage[$blockKey] = $fileCoverage[$blockKey].Clone() - } - } - - Write-Host "Successfully processed $inputFile with $($fileCoverage.Count) coverage blocks" - } - catch { - Write-Error "Failed to process coverage file $inputFile : $_" - exit 1 - } -} - -if ($totalFiles -eq 0) { - Write-Warning "No valid coverage files found. Creating empty coverage profile." - try { - @("mode: set") | Out-File -FilePath $OutputFile -Encoding UTF8 - Write-Host "Created empty coverage profile: $OutputFile" - } - catch { - Write-Error "Failed to create empty coverage profile: $_" - exit 1 - } -} else { - Write-Host "Writing merged coverage profile to: $OutputFile" - try { - Write-CoverageProfile -Coverage $mergedCoverage -OutputPath $OutputFile - - # Display summary - $totalBlocks = $mergedCoverage.Count - $coveredBlocks = ($mergedCoverage.Values | Where-Object { $_.count -gt 0 }).Count - $coveragePercentage = if ($totalBlocks -gt 0) { ($coveredBlocks / $totalBlocks) * 100 } else { 0 } - - Write-Host "Merge Summary:" - Write-Host " Input files processed: $totalFiles" - Write-Host " Total code blocks: $totalBlocks" - Write-Host " Covered blocks: $coveredBlocks" - Write-Host " Coverage: $($coveragePercentage.ToString('F2'))%" - } - catch { - Write-Error "Failed to write merged coverage profile: $_" - exit 1 - } -} - -Write-Host "Coverage merge completed successfully!" -exit 0