From 998092eec2873fbe6c2649bf41978f5e78288c98 Mon Sep 17 00:00:00 2001 From: Sam Harwell Date: Fri, 5 Dec 2025 13:11:23 -0600 Subject: [PATCH 01/12] chore(deps): add coverlet.console alongside opencover Why - Prepare migration to coverlet while keeping OpenCover available. What - Added coverlet.console 6.0.4 entry to .nuget/packages.config. - Restored packages so coverlet console binaries are present locally. Notes - Kept OpenCover entry to avoid breaking existing scripts mid-migration. Testing - .\.nuget\nuget.exe install .\.nuget\packages.config -OutputDirectory packages --- .nuget/packages.config | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.nuget/packages.config b/.nuget/packages.config index d409bf15b..74bb95867 100644 --- a/.nuget/packages.config +++ b/.nuget/packages.config @@ -1,7 +1,8 @@  + - \ No newline at end of file + From f07946ca8b2f652de9c10ea69802d2988b562b7f Mon Sep 17 00:00:00 2001 From: Sam Harwell Date: Fri, 5 Dec 2025 13:16:09 -0600 Subject: [PATCH 02/12] ci(coverage): use coverlet in test template Why - Move CI coverage off OpenCover to maintained coverlet tooling. - Keep artifact structure unchanged for downstream merge/upload steps. What - Run coverlet.console via dotnet around xunit to gather coverage. - Keep xUnit XML output; store Cobertura in build/OpenCover.Reports. Notes - Kept legacy folder name so downstream merge/upload stays intact. Testing - Not run (CI pipeline change only). --- build/test.yml | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/build/test.yml b/build/test.yml index 224da2fdc..af6c05817 100644 --- a/build/test.yml +++ b/build/test.yml @@ -69,27 +69,25 @@ jobs: targetType: inline script: | $packageConfig = [xml](Get-Content ..\.nuget\packages.config) - $opencover_version = $packageConfig.SelectSingleNode('/packages/package[@id="OpenCover"]').version + $coverlet_version = $packageConfig.SelectSingleNode('/packages/package[@id="coverlet.console"]').version $xunitrunner_version = $packageConfig.SelectSingleNode('/packages/package[@id="xunit.runner.console"]').version $packages_folder = '..\packages' - $opencover_console = "$packages_folder\OpenCover.$opencover_version\tools\OpenCover.Console.exe" + $coverlet_console = "$packages_folder\coverlet.console.$coverlet_version\tools\net6.0\any\coverlet.console.dll" $xunit_runner_console_${{ parameters.FrameworkVersion }} = "$packages_folder\xunit.runner.console.$xunitrunner_version\tools\${{ parameters.FrameworkVersion }}\xunit.console.x86.exe" $report_folder = '.\OpenCover.Reports' - mkdir $report_folder + New-Item -ItemType Directory -Force -Path $report_folder | Out-Null $target_dll_name = If ('${{ parameters.LangVersion }}' -Eq '6') { "StyleCop.Analyzers.Test" } Else { "StyleCop.Analyzers.Test.CSharp${{ parameters.LangVersion }}" } $target_dll_csharp${{ parameters.LangVersion }} = "..\StyleCop.Analyzers\$target_dll_name\bin\${{ parameters.BuildConfiguration }}\${{ parameters.FrameworkVersion }}\$target_dll_name.dll" - &$opencover_console ` - -register:Path32 ` - -threshold:1 -oldStyle ` - -returntargetcode ` - -hideskipped:All ` - -filter:"+[StyleCop*]*" ` - -excludebyattribute:*.ExcludeFromCodeCoverage* ` - -excludebyfile:*\*Designer.cs ` - -output:"$report_folder\OpenCover.StyleCopAnalyzers.CSharp${{ parameters.LangVersion }}.xml" ` - -target:"$xunit_runner_console_${{ parameters.FrameworkVersion }}" ` - -targetargs:"$target_dll_csharp${{ parameters.LangVersion }} -noshadow -xml StyleCopAnalyzers.CSharp${{ parameters.LangVersion }}.xunit.xml" + dotnet $coverlet_console ` + $target_dll_csharp${{ parameters.LangVersion }} ` + --target "$xunit_runner_console_${{ parameters.FrameworkVersion }}" ` + --targetargs "`"$target_dll_csharp${{ parameters.LangVersion }}`" -noshadow -xml StyleCopAnalyzers.CSharp${{ parameters.LangVersion }}.xunit.xml" ` + --format "cobertura" ` + --output "$report_folder\OpenCover.StyleCopAnalyzers.CSharp${{ parameters.LangVersion }}.xml" ` + --include "[StyleCop*]*" ` + --exclude-by-attribute "System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverageAttribute" ` + --exclude-by-file "*\*Designer.cs" - task: PublishTestResults@2 displayName: 📢 Publish test results From ac6869a856ea4671687756ed402feab04276df9c Mon Sep 17 00:00:00 2001 From: Sam Harwell Date: Fri, 5 Dec 2025 13:35:21 -0600 Subject: [PATCH 03/12] test(coverage): add coverlet collector and runsettings Why - Prepare test projects for coverlet collector-based dotnet test runs. - Keep coverage filters aligned with existing OpenCover configuration. What - Added coverlet.collector and Microsoft.NET.Test.Sdk to all test csproj. - Added a shared coverlet.runsettings with include/exclude settings. Notes - OpenCover/coverlet.console tooling remains for now during migration. Testing - Not run (package and coverage configuration updates only). --- .../StyleCop.Analyzers.Test.CSharp10.csproj | 4 +++- .../StyleCop.Analyzers.Test.CSharp11.csproj | 4 +++- .../StyleCop.Analyzers.Test.CSharp12.csproj | 4 +++- .../StyleCop.Analyzers.Test.CSharp13.csproj | 4 +++- .../StyleCop.Analyzers.Test.CSharp7.csproj | 4 +++- .../StyleCop.Analyzers.Test.CSharp8.csproj | 4 +++- .../StyleCop.Analyzers.Test.CSharp9.csproj | 4 +++- .../StyleCop.Analyzers.Test.csproj | 3 ++- build/coverlet.runsettings | 15 +++++++++++++++ 9 files changed, 38 insertions(+), 8 deletions(-) create mode 100644 build/coverlet.runsettings diff --git a/StyleCop.Analyzers/StyleCop.Analyzers.Test.CSharp10/StyleCop.Analyzers.Test.CSharp10.csproj b/StyleCop.Analyzers/StyleCop.Analyzers.Test.CSharp10/StyleCop.Analyzers.Test.CSharp10.csproj index c156608b8..2f84ddc30 100644 --- a/StyleCop.Analyzers/StyleCop.Analyzers.Test.CSharp10/StyleCop.Analyzers.Test.CSharp10.csproj +++ b/StyleCop.Analyzers/StyleCop.Analyzers.Test.CSharp10/StyleCop.Analyzers.Test.CSharp10.csproj @@ -19,6 +19,8 @@ + + @@ -34,4 +36,4 @@ - \ No newline at end of file + diff --git a/StyleCop.Analyzers/StyleCop.Analyzers.Test.CSharp11/StyleCop.Analyzers.Test.CSharp11.csproj b/StyleCop.Analyzers/StyleCop.Analyzers.Test.CSharp11/StyleCop.Analyzers.Test.CSharp11.csproj index 0bf96cd59..4cab5414e 100644 --- a/StyleCop.Analyzers/StyleCop.Analyzers.Test.CSharp11/StyleCop.Analyzers.Test.CSharp11.csproj +++ b/StyleCop.Analyzers/StyleCop.Analyzers.Test.CSharp11/StyleCop.Analyzers.Test.CSharp11.csproj @@ -19,6 +19,8 @@ + + @@ -35,4 +37,4 @@ - \ No newline at end of file + diff --git a/StyleCop.Analyzers/StyleCop.Analyzers.Test.CSharp12/StyleCop.Analyzers.Test.CSharp12.csproj b/StyleCop.Analyzers/StyleCop.Analyzers.Test.CSharp12/StyleCop.Analyzers.Test.CSharp12.csproj index d89fa6f67..23ac82474 100644 --- a/StyleCop.Analyzers/StyleCop.Analyzers.Test.CSharp12/StyleCop.Analyzers.Test.CSharp12.csproj +++ b/StyleCop.Analyzers/StyleCop.Analyzers.Test.CSharp12/StyleCop.Analyzers.Test.CSharp12.csproj @@ -19,6 +19,8 @@ + + @@ -36,4 +38,4 @@ - \ No newline at end of file + diff --git a/StyleCop.Analyzers/StyleCop.Analyzers.Test.CSharp13/StyleCop.Analyzers.Test.CSharp13.csproj b/StyleCop.Analyzers/StyleCop.Analyzers.Test.CSharp13/StyleCop.Analyzers.Test.CSharp13.csproj index 58c62399c..0e5adc450 100644 --- a/StyleCop.Analyzers/StyleCop.Analyzers.Test.CSharp13/StyleCop.Analyzers.Test.CSharp13.csproj +++ b/StyleCop.Analyzers/StyleCop.Analyzers.Test.CSharp13/StyleCop.Analyzers.Test.CSharp13.csproj @@ -19,6 +19,8 @@ + + @@ -37,4 +39,4 @@ - \ No newline at end of file + diff --git a/StyleCop.Analyzers/StyleCop.Analyzers.Test.CSharp7/StyleCop.Analyzers.Test.CSharp7.csproj b/StyleCop.Analyzers/StyleCop.Analyzers.Test.CSharp7/StyleCop.Analyzers.Test.CSharp7.csproj index dbd38c35e..6e4a71ebe 100644 --- a/StyleCop.Analyzers/StyleCop.Analyzers.Test.CSharp7/StyleCop.Analyzers.Test.CSharp7.csproj +++ b/StyleCop.Analyzers/StyleCop.Analyzers.Test.CSharp7/StyleCop.Analyzers.Test.CSharp7.csproj @@ -19,6 +19,8 @@ + + @@ -37,4 +39,4 @@ - \ No newline at end of file + diff --git a/StyleCop.Analyzers/StyleCop.Analyzers.Test.CSharp8/StyleCop.Analyzers.Test.CSharp8.csproj b/StyleCop.Analyzers/StyleCop.Analyzers.Test.CSharp8/StyleCop.Analyzers.Test.CSharp8.csproj index 0ef285c16..23911ad73 100644 --- a/StyleCop.Analyzers/StyleCop.Analyzers.Test.CSharp8/StyleCop.Analyzers.Test.CSharp8.csproj +++ b/StyleCop.Analyzers/StyleCop.Analyzers.Test.CSharp8/StyleCop.Analyzers.Test.CSharp8.csproj @@ -19,6 +19,8 @@ + + @@ -35,4 +37,4 @@ - \ No newline at end of file + diff --git a/StyleCop.Analyzers/StyleCop.Analyzers.Test.CSharp9/StyleCop.Analyzers.Test.CSharp9.csproj b/StyleCop.Analyzers/StyleCop.Analyzers.Test.CSharp9/StyleCop.Analyzers.Test.CSharp9.csproj index 0689fdb35..56ed8f838 100644 --- a/StyleCop.Analyzers/StyleCop.Analyzers.Test.CSharp9/StyleCop.Analyzers.Test.CSharp9.csproj +++ b/StyleCop.Analyzers/StyleCop.Analyzers.Test.CSharp9/StyleCop.Analyzers.Test.CSharp9.csproj @@ -19,6 +19,8 @@ + + @@ -33,4 +35,4 @@ - \ No newline at end of file + diff --git a/StyleCop.Analyzers/StyleCop.Analyzers.Test/StyleCop.Analyzers.Test.csproj b/StyleCop.Analyzers/StyleCop.Analyzers.Test/StyleCop.Analyzers.Test.csproj index 1f48733c0..ec7464e5f 100644 --- a/StyleCop.Analyzers/StyleCop.Analyzers.Test/StyleCop.Analyzers.Test.csproj +++ b/StyleCop.Analyzers/StyleCop.Analyzers.Test/StyleCop.Analyzers.Test.csproj @@ -22,6 +22,7 @@ + @@ -54,4 +55,4 @@ - \ No newline at end of file + diff --git a/build/coverlet.runsettings b/build/coverlet.runsettings new file mode 100644 index 000000000..e05fd4734 --- /dev/null +++ b/build/coverlet.runsettings @@ -0,0 +1,15 @@ + + + + + + + cobertura + [StyleCop*]* + System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverageAttribute + **/*Designer.cs + + + + + From e76fc4e8fb5e0795b61e03c3ea044c93c7c9c51a Mon Sep 17 00:00:00 2001 From: Sam Harwell Date: Fri, 5 Dec 2025 13:41:06 -0600 Subject: [PATCH 04/12] ci(coverage): drive tests with coverlet collector Why - Align CI test runs with coverlet.collector and dotnet test. - Keep coverage artifacts flowing while switching away from OpenCover tooling. What - Run dotnet test per language/TFM with coverlet collector and runsettings. - Emit TRX results to a known directory and publish Cobertura copies from build/coverage. Notes - Coverage files retain the OpenCover.* naming to keep downstream merge steps working until they are updated. Testing - Not run (CI pipeline change only). --- build/test.yml | 49 +++++++++++++++++++++++++++---------------------- 1 file changed, 27 insertions(+), 22 deletions(-) diff --git a/build/test.yml b/build/test.yml index af6c05817..e3752f085 100644 --- a/build/test.yml +++ b/build/test.yml @@ -68,33 +68,38 @@ jobs: workingDirectory: '$(Build.SourcesDirectory)/build' targetType: inline script: | - $packageConfig = [xml](Get-Content ..\.nuget\packages.config) - $coverlet_version = $packageConfig.SelectSingleNode('/packages/package[@id="coverlet.console"]').version - $xunitrunner_version = $packageConfig.SelectSingleNode('/packages/package[@id="xunit.runner.console"]').version + $results_dir = "TestResults" + $coverage_dir = "coverage" + New-Item -ItemType Directory -Force -Path $results_dir | Out-Null + New-Item -ItemType Directory -Force -Path $coverage_dir | Out-Null - $packages_folder = '..\packages' - $coverlet_console = "$packages_folder\coverlet.console.$coverlet_version\tools\net6.0\any\coverlet.console.dll" - $xunit_runner_console_${{ parameters.FrameworkVersion }} = "$packages_folder\xunit.runner.console.$xunitrunner_version\tools\${{ parameters.FrameworkVersion }}\xunit.console.x86.exe" - $report_folder = '.\OpenCover.Reports' - New-Item -ItemType Directory -Force -Path $report_folder | Out-Null - $target_dll_name = If ('${{ parameters.LangVersion }}' -Eq '6') { "StyleCop.Analyzers.Test" } Else { "StyleCop.Analyzers.Test.CSharp${{ parameters.LangVersion }}" } - $target_dll_csharp${{ parameters.LangVersion }} = "..\StyleCop.Analyzers\$target_dll_name\bin\${{ parameters.BuildConfiguration }}\${{ parameters.FrameworkVersion }}\$target_dll_name.dll" - dotnet $coverlet_console ` - $target_dll_csharp${{ parameters.LangVersion }} ` - --target "$xunit_runner_console_${{ parameters.FrameworkVersion }}" ` - --targetargs "`"$target_dll_csharp${{ parameters.LangVersion }}`" -noshadow -xml StyleCopAnalyzers.CSharp${{ parameters.LangVersion }}.xunit.xml" ` - --format "cobertura" ` - --output "$report_folder\OpenCover.StyleCopAnalyzers.CSharp${{ parameters.LangVersion }}.xml" ` - --include "[StyleCop*]*" ` - --exclude-by-attribute "System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverageAttribute" ` - --exclude-by-file "*\*Designer.cs" + $project_name = If ('${{ parameters.LangVersion }}' -Eq '6') { "StyleCop.Analyzers.Test" } Else { "StyleCop.Analyzers.Test.CSharp${{ parameters.LangVersion }}" } + $project_path = "..\StyleCop.Analyzers\$project_name\$project_name.csproj" + $trx_name = "StyleCopAnalyzers.CSharp${{ parameters.LangVersion }}.trx" + + dotnet test $project_path ` + --framework "${{ parameters.FrameworkVersion }}" ` + --configuration "${{ parameters.BuildConfiguration }}" ` + --no-build ` + --settings ".\coverlet.runsettings" ` + --results-directory $results_dir ` + --logger "trx;LogFileName=$trx_name" ` + --collect:"XPlat Code Coverage" + + $coverage_file = Get-ChildItem -Path $results_dir -Recurse -Filter "coverage.cobertura.xml" -ErrorAction SilentlyContinue | Select-Object -First 1 + if (-not $coverage_file) { + Write-Error "Coverage file not found in $results_dir" + exit 1 + } + + Copy-Item $coverage_file.FullName "$coverage_dir\OpenCover.StyleCopAnalyzers.CSharp${{ parameters.LangVersion }}.xml" -Force - task: PublishTestResults@2 displayName: 📢 Publish test results condition: always() inputs: - testResultsFormat: xUnit - testResultsFiles: 'build/*.xml' + testResultsFormat: VSTest + testResultsFiles: 'build/TestResults/**/*.trx' mergeTestResults: true testRunTitle: 'C# ${{ parameters.LangVersion }} ${{ parameters.BuildConfiguration }}' @@ -102,5 +107,5 @@ jobs: - task: PublishPipelineArtifact@1 displayName: Publish code coverage inputs: - targetPath: $(Build.SourcesDirectory)/build/OpenCover.Reports + targetPath: $(Build.SourcesDirectory)/build/coverage artifact: coverageResults-cs${{ parameters.LangVersion }} From eac8662cd55ec656bc975ba97f417188bea1c92e Mon Sep 17 00:00:00 2001 From: Sam Harwell Date: Fri, 5 Dec 2025 13:43:43 -0600 Subject: [PATCH 05/12] ci(coverage): merge collector outputs in wrap-up stage Why - Align the coverage merge with the new collector-produced artifact layout. - Keep the existing ReportGenerator + Codecov flow intact. What - Point ReportGenerator glob at coverageResults-*/coverage/OpenCover.*.xml. Notes - Final Cobertura.xml path stays unchanged for CodecovUploader. Testing - Not run (CI pipeline change only). --- build/build-and-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/build-and-test.yml b/build/build-and-test.yml index 68b911cb0..900c744a6 100644 --- a/build/build-and-test.yml +++ b/build/build-and-test.yml @@ -281,7 +281,7 @@ stages: $packages_folder = '.\packages' $reportgenerator_version = $packageConfig.SelectSingleNode('/packages/package[@id="ReportGenerator"]').version $report_generator = "$packages_folder\ReportGenerator.$reportgenerator_version\tools\net47\ReportGenerator.exe" - &$report_generator -targetdir:$(Pipeline.Workspace)/coverageResults-final -reporttypes:Cobertura "-reports:$(Pipeline.Workspace)/coverageResults-*/OpenCover.*.xml" + &$report_generator -targetdir:$(Pipeline.Workspace)/coverageResults-final -reporttypes:Cobertura "-reports:$(Pipeline.Workspace)/coverageResults-*/coverage/OpenCover.*.xml" - task: PowerShell@2 displayName: Public code coverage to codecov.io From 4cb005c95cd6ba21ddefa14f05a1e4e89cf857eb Mon Sep 17 00:00:00 2001 From: Sam Harwell Date: Fri, 5 Dec 2025 13:45:26 -0600 Subject: [PATCH 06/12] build(coverage): add collector-based local coverage script Why - Provide a local coverage workflow aligned with coverlet.collector instead of OpenCover. - Keep existing entrypoint usable while shifting tooling underneath. What - Added coverage-report.ps1 to run dotnet test with coverlet collector and generate Cobertura + HTML. - Forwarded opencover-report.ps1 to the new script with a deprecation warning. Notes - Script preserves prior flags (Debug/NoBuild/NoReport) for compatibility. - Coverage files keep the OpenCover.* naming to match existing merge expectations. Testing - Not run (local script changes only). --- build/coverage-report.ps1 | 85 ++++++++++++++ build/opencover-report.ps1 | 219 ++----------------------------------- 2 files changed, 93 insertions(+), 211 deletions(-) create mode 100644 build/coverage-report.ps1 diff --git a/build/coverage-report.ps1 b/build/coverage-report.ps1 new file mode 100644 index 000000000..01bb7162a --- /dev/null +++ b/build/coverage-report.ps1 @@ -0,0 +1,85 @@ +param ( + [switch]$Debug, + [switch]$NoBuild, + [switch]$NoReport, + [switch]$AppVeyor, + [switch]$Azure +) + +$ErrorActionPreference = 'Stop' + +$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path +Push-Location $scriptDir + +try { + if (-not $NoBuild) { + if ($Debug) { + .\build.ps1 -Debug -Incremental + } else { + .\build.ps1 -Incremental + } + } + + $configuration = if ($Debug) { 'Debug' } else { 'Release' } + $resultsRoot = Join-Path $scriptDir 'TestResults' + $coverageRoot = Join-Path $scriptDir 'coverage' + + if (Test-Path $resultsRoot) { + Remove-Item -Recurse -Force $resultsRoot + } + + if (Test-Path $coverageRoot) { + Remove-Item -Recurse -Force $coverageRoot + } + + New-Item -ItemType Directory -Force -Path $resultsRoot | Out-Null + New-Item -ItemType Directory -Force -Path $coverageRoot | Out-Null + + $runs = @( + @{ Lang = '6'; Project = 'StyleCop.Analyzers.Test'; Framework = 'net452' } + @{ Lang = '7'; Project = 'StyleCop.Analyzers.Test.CSharp7'; Framework = 'net46' } + @{ Lang = '8'; Project = 'StyleCop.Analyzers.Test.CSharp8'; Framework = 'net472' } + @{ Lang = '9'; Project = 'StyleCop.Analyzers.Test.CSharp9'; Framework = 'net472' } + @{ Lang = '10'; Project = 'StyleCop.Analyzers.Test.CSharp10'; Framework = 'net472' } + @{ Lang = '11'; Project = 'StyleCop.Analyzers.Test.CSharp11'; Framework = 'net472' } + @{ Lang = '12'; Project = 'StyleCop.Analyzers.Test.CSharp12'; Framework = 'net472' } + @{ Lang = '13'; Project = 'StyleCop.Analyzers.Test.CSharp13'; Framework = 'net472' } + ) + + foreach ($run in $runs) { + $projectPath = Join-Path $scriptDir "..\StyleCop.Analyzers\$($run.Project)\$($run.Project).csproj" + $runResultsDir = Join-Path $resultsRoot "CSharp$($run.Lang)" + New-Item -ItemType Directory -Force -Path $runResultsDir | Out-Null + + $trxName = "StyleCopAnalyzers.CSharp$($run.Lang).trx" + + dotnet test $projectPath ` + --framework $run.Framework ` + --configuration $configuration ` + --no-build ` + --settings (Join-Path $scriptDir 'coverlet.runsettings') ` + --results-directory $runResultsDir ` + --logger "trx;LogFileName=$trxName" ` + --collect:"XPlat Code Coverage" + + $coverageFile = Get-ChildItem -Path $runResultsDir -Recurse -Filter 'coverage.cobertura.xml' -ErrorAction SilentlyContinue | Select-Object -First 1 + if (-not $coverageFile) { + throw "Coverage file not found for C# $($run.Lang) in $runResultsDir" + } + + Copy-Item $coverageFile.FullName (Join-Path $coverageRoot "OpenCover.StyleCopAnalyzers.CSharp$($run.Lang).xml") -Force + } + + if (-not $NoReport) { + $packageConfig = [xml](Get-Content ..\.nuget\packages.config) + $packagesFolder = '..\packages' + $reportGeneratorVersion = $packageConfig.SelectSingleNode('/packages/package[@id="ReportGenerator"]').version + $reportGenerator = "$packagesFolder\ReportGenerator.$reportGeneratorVersion\tools\ReportGenerator.exe" + + &$reportGenerator -targetdir:$coverageRoot -reporttypes:Html;Cobertura "-reports:$coverageRoot\OpenCover.*.xml" + Write-Host "Open $coverageRoot\index.htm to see code coverage results." + } +} +finally { + Pop-Location +} diff --git a/build/opencover-report.ps1 b/build/opencover-report.ps1 index 4e0bc3402..19b286657 100644 --- a/build/opencover-report.ps1 +++ b/build/opencover-report.ps1 @@ -1,214 +1,11 @@ param ( - [switch]$Debug, - [switch]$NoBuild, - [switch]$NoReport, - [switch]$AppVeyor, - [switch]$Azure + [switch]$Debug, + [switch]$NoBuild, + [switch]$NoReport, + [switch]$AppVeyor, + [switch]$Azure ) -If (-not $NoBuild) { - # Run a build to ensure everything is up-to-date - If ($Debug) { - .\build.ps1 -Debug -Incremental - } Else { - .\build.ps1 -Incremental - } - - If (-not $?) { - $host.UI.WriteErrorLine('Build failed; coverage analysis aborted.') - Exit $LASTEXITCODE - } -} - -If ($Debug) { - $Configuration = 'Debug' -} Else { - $Configuration = 'Release' -} - -$packageConfig = [xml](Get-Content ..\.nuget\packages.config) -$opencover_version = $packageConfig.SelectSingleNode('/packages/package[@id="OpenCover"]').version -$reportgenerator_version = $packageConfig.SelectSingleNode('/packages/package[@id="ReportGenerator"]').version -$xunitrunner_version = $packageConfig.SelectSingleNode('/packages/package[@id="xunit.runner.console"]').version - -$packages_folder = '..\packages' -$opencover_console = "$packages_folder\OpenCover.$opencover_version\tools\OpenCover.Console.exe" -$xunit_runner_console_net452 = "$packages_folder\xunit.runner.console.$xunitrunner_version\tools\net452\xunit.console.x86.exe" -$xunit_runner_console_net46 = "$packages_folder\xunit.runner.console.$xunitrunner_version\tools\net46\xunit.console.x86.exe" -$xunit_runner_console_net472 = "$packages_folder\xunit.runner.console.$xunitrunner_version\tools\net472\xunit.console.x86.exe" -$report_generator = "$packages_folder\ReportGenerator.$reportgenerator_version\tools\ReportGenerator.exe" -$report_folder = '.\OpenCover.Reports' -$target_dll = "..\StyleCop.Analyzers\StyleCop.Analyzers.Test\bin\$Configuration\net452\StyleCop.Analyzers.Test.dll" -$target_dll_csharp7 = "..\StyleCop.Analyzers\StyleCop.Analyzers.Test.CSharp7\bin\$Configuration\net46\StyleCop.Analyzers.Test.CSharp7.dll" -$target_dll_csharp8 = "..\StyleCop.Analyzers\StyleCop.Analyzers.Test.CSharp8\bin\$Configuration\net472\StyleCop.Analyzers.Test.CSharp8.dll" -$target_dll_csharp9 = "..\StyleCop.Analyzers\StyleCop.Analyzers.Test.CSharp9\bin\$Configuration\net472\StyleCop.Analyzers.Test.CSharp9.dll" -$target_dll_csharp10 = "..\StyleCop.Analyzers\StyleCop.Analyzers.Test.CSharp10\bin\$Configuration\net472\StyleCop.Analyzers.Test.CSharp10.dll" -$target_dll_csharp11 = "..\StyleCop.Analyzers\StyleCop.Analyzers.Test.CSharp11\bin\$Configuration\net472\StyleCop.Analyzers.Test.CSharp11.dll" -$target_dll_csharp12 = "..\StyleCop.Analyzers\StyleCop.Analyzers.Test.CSharp12\bin\$Configuration\net472\StyleCop.Analyzers.Test.CSharp12.dll" -$target_dll_csharp13 = "..\StyleCop.Analyzers\StyleCop.Analyzers.Test.CSharp13\bin\$Configuration\net472\StyleCop.Analyzers.Test.CSharp13.dll" - -If (Test-Path $report_folder) { - Remove-Item -Recurse -Force $report_folder -} - -mkdir $report_folder | Out-Null - -$register_mode = 'user' -If ($AppVeyor) { - $AppVeyorArg = '-appveyor' - $register_mode = 'Path32' -} ElseIf ($Azure) { - $register_mode = 'Path32' -} - -$exitCode = 0 - -&$opencover_console ` - -register:$register_mode ` - -threshold:1 -oldStyle ` - -returntargetcode ` - -hideskipped:All ` - -filter:"+[StyleCop*]*" ` - -excludebyattribute:*.ExcludeFromCodeCoverage* ` - -excludebyfile:*\*Designer.cs ` - -output:"$report_folder\OpenCover.StyleCopAnalyzers.xml" ` - -target:"$xunit_runner_console_net452" ` - -targetargs:"$target_dll -noshadow $AppVeyorArg -xml StyleCopAnalyzers.xunit.xml" - -If (($AppVeyor -or $Azure) -and -not $?) { - $host.UI.WriteErrorLine('Build failed; coverage analysis may be incomplete.') - $exitCode = $LASTEXITCODE -} - -&$opencover_console ` - -register:$register_mode ` - -threshold:1 -oldStyle ` - -returntargetcode ` - -hideskipped:All ` - -filter:"+[StyleCop*]*" ` - -excludebyattribute:*.ExcludeFromCodeCoverage* ` - -excludebyfile:*\*Designer.cs ` - -output:"$report_folder\OpenCover.StyleCopAnalyzers.xml" ` - -mergebyhash -mergeoutput ` - -target:"$xunit_runner_console_net46" ` - -targetargs:"$target_dll_csharp7 -noshadow $AppVeyorArg -xml StyleCopAnalyzers.CSharp7.xunit.xml" - -If (($AppVeyor -or $Azure) -and -not $?) { - $host.UI.WriteErrorLine('Build failed; coverage analysis may be incomplete.') - $exitCode = $LASTEXITCODE -} - -&$opencover_console ` - -register:$register_mode ` - -threshold:1 -oldStyle ` - -returntargetcode ` - -hideskipped:All ` - -filter:"+[StyleCop*]*" ` - -excludebyattribute:*.ExcludeFromCodeCoverage* ` - -excludebyfile:*\*Designer.cs ` - -output:"$report_folder\OpenCover.StyleCopAnalyzers.xml" ` - -mergebyhash -mergeoutput ` - -target:"$xunit_runner_console_net472" ` - -targetargs:"$target_dll_csharp8 -noshadow $AppVeyorArg -xml StyleCopAnalyzers.CSharp8.xunit.xml" - -If (($AppVeyor -or $Azure) -and -not $?) { - $host.UI.WriteErrorLine('Build failed; coverage analysis may be incomplete.') - $exitCode = $LASTEXITCODE -} - -&$opencover_console ` - -register:$register_mode ` - -threshold:1 -oldStyle ` - -returntargetcode ` - -hideskipped:All ` - -filter:"+[StyleCop*]*" ` - -excludebyattribute:*.ExcludeFromCodeCoverage* ` - -excludebyfile:*\*Designer.cs ` - -output:"$report_folder\OpenCover.StyleCopAnalyzers.xml" ` - -mergebyhash -mergeoutput ` - -target:"$xunit_runner_console_net472" ` - -targetargs:"$target_dll_csharp9 -noshadow $AppVeyorArg -xml StyleCopAnalyzers.CSharp9.xunit.xml" - -If (($AppVeyor -or $Azure) -and -not $?) { - $host.UI.WriteErrorLine('Build failed; coverage analysis may be incomplete.') - $exitCode = $LASTEXITCODE -} - -&$opencover_console ` - -register:$register_mode ` - -threshold:1 -oldStyle ` - -returntargetcode ` - -hideskipped:All ` - -filter:"+[StyleCop*]*" ` - -excludebyattribute:*.ExcludeFromCodeCoverage* ` - -excludebyfile:*\*Designer.cs ` - -output:"$report_folder\OpenCover.StyleCopAnalyzers.xml" ` - -mergebyhash -mergeoutput ` - -target:"$xunit_runner_console_net472" ` - -targetargs:"$target_dll_csharp10 -noshadow $AppVeyorArg -xml StyleCopAnalyzers.CSharp10.xunit.xml" - -If (($AppVeyor -or $Azure) -and -not $?) { - $host.UI.WriteErrorLine('Build failed; coverage analysis may be incomplete.') - $exitCode = $LASTEXITCODE -} - -&$opencover_console ` - -register:$register_mode ` - -threshold:1 -oldStyle ` - -returntargetcode ` - -hideskipped:All ` - -filter:"+[StyleCop*]*" ` - -excludebyattribute:*.ExcludeFromCodeCoverage* ` - -excludebyfile:*\*Designer.cs ` - -output:"$report_folder\OpenCover.StyleCopAnalyzers.xml" ` - -mergebyhash -mergeoutput ` - -target:"$xunit_runner_console_net472" ` - -targetargs:"$target_dll_csharp11 -noshadow $AppVeyorArg -xml StyleCopAnalyzers.CSharp11.xunit.xml" - -If (($AppVeyor -or $Azure) -and -not $?) { - $host.UI.WriteErrorLine('Build failed; coverage analysis may be incomplete.') - $exitCode = $LASTEXITCODE -} - -&$opencover_console ` - -register:$register_mode ` - -threshold:1 -oldStyle ` - -returntargetcode ` - -hideskipped:All ` - -filter:"+[StyleCop*]*" ` - -excludebyattribute:*.ExcludeFromCodeCoverage* ` - -excludebyfile:*\*Designer.cs ` - -output:"$report_folder\OpenCover.StyleCopAnalyzers.xml" ` - -mergebyhash -mergeoutput ` - -target:"$xunit_runner_console_net472" ` - -targetargs:"$target_dll_csharp12 -noshadow $AppVeyorArg -xml StyleCopAnalyzers.CSharp12.xunit.xml" - -If (($AppVeyor -or $Azure) -and -not $?) { - $host.UI.WriteErrorLine('Build failed; coverage analysis may be incomplete.') - $exitCode = $LASTEXITCODE -} - -&$opencover_console ` - -register:$register_mode ` - -threshold:1 -oldStyle ` - -returntargetcode ` - -hideskipped:All ` - -filter:"+[StyleCop*]*" ` - -excludebyattribute:*.ExcludeFromCodeCoverage* ` - -excludebyfile:*\*Designer.cs ` - -output:"$report_folder\OpenCover.StyleCopAnalyzers.xml" ` - -mergebyhash -mergeoutput ` - -target:"$xunit_runner_console_net472" ` - -targetargs:"$target_dll_csharp13 -noshadow $AppVeyorArg -xml StyleCopAnalyzers.CSharp13.xunit.xml" - -If (($AppVeyor -or $Azure) -and -not $?) { - $host.UI.WriteErrorLine('Build failed; coverage analysis may be incomplete.') - $exitCode = $LASTEXITCODE -} - -If (-not $NoReport) { - &$report_generator -targetdir:$report_folder -reports:$report_folder\OpenCover.*.xml - $host.UI.WriteLine("Open $report_folder\index.htm to see code coverage results.") -} - -Exit $exitCode +Write-Warning "build/opencover-report.ps1 is deprecated; forwarding to build/coverage-report.ps1." +& "$PSScriptRoot\coverage-report.ps1" @PSBoundParameters +exit $LASTEXITCODE From f133724a70285e62e9654e8782bac7f904b51bd4 Mon Sep 17 00:00:00 2001 From: Sam Harwell Date: Fri, 5 Dec 2025 13:46:57 -0600 Subject: [PATCH 07/12] build(coverage): add diff-focused coverage option Why - Provide contributors a way to inspect coverage for their changed files before pushing. - Keep the collector-based local coverage workflow consistent with CI. What - Added DiffBase/DiffOnly support to coverage-report.ps1 using git diff file filters. - Generate filtered HTML and text summaries via ReportGenerator for changed files. Notes - Defaults to origin/master when DiffOnly is used without an explicit base. - Falls back with warnings if git or diffs are unavailable. Testing - Not run (script changes only). --- build/coverage-report.ps1 | 39 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/build/coverage-report.ps1 b/build/coverage-report.ps1 index 01bb7162a..c9d2b3959 100644 --- a/build/coverage-report.ps1 +++ b/build/coverage-report.ps1 @@ -3,7 +3,9 @@ param ( [switch]$NoBuild, [switch]$NoReport, [switch]$AppVeyor, - [switch]$Azure + [switch]$Azure, + [string]$DiffBase, + [switch]$DiffOnly ) $ErrorActionPreference = 'Stop' @@ -78,6 +80,41 @@ try { &$reportGenerator -targetdir:$coverageRoot -reporttypes:Html;Cobertura "-reports:$coverageRoot\OpenCover.*.xml" Write-Host "Open $coverageRoot\index.htm to see code coverage results." + + $shouldGenerateDiff = $DiffOnly -or -not [string]::IsNullOrWhiteSpace($DiffBase) + if ($shouldGenerateDiff) { + $baseRef = if ([string]::IsNullOrWhiteSpace($DiffBase)) { 'origin/master' } else { $DiffBase } + $repoRoot = Resolve-Path (Join-Path $scriptDir '..') + + try { + $diffFiles = & git -C $repoRoot diff --name-only --diff-filter=AM "$baseRef...HEAD" 2>$null | Where-Object { $_ -like '*.cs' } + if (-not $diffFiles) { + Write-Warning "No changed .cs files found relative to '$baseRef'; skipping diff coverage output." + } + else { + $diffFilters = $diffFiles | ForEach-Object { + $normalized = ($_ -replace '\\', '/') + "+**/$normalized" + } + + $diffTarget = Join-Path $coverageRoot 'diff' + New-Item -ItemType Directory -Force -Path $diffTarget | Out-Null + + &$reportGenerator -targetdir:$diffTarget -reporttypes:Html;TextSummary "-reports:$coverageRoot\OpenCover.*.xml" "-filefilters:$($diffFilters -join ';')" + + $summaryPath = Get-ChildItem $diffTarget -Filter '*Summary.txt' -ErrorAction SilentlyContinue | Select-Object -First 1 + if ($summaryPath) { + Write-Host "Diff coverage summary (relative to $baseRef):" + Get-Content $summaryPath.FullName + } + + Write-Host "Diff coverage HTML: $diffTarget\index.htm" + } + } + catch { + Write-Warning "Failed to compute diff coverage relative to '$baseRef': $($_.Exception.Message)" + } + } } } finally { From 7f0eae0fb4ab97fc5730ecb6bef27125a515fef4 Mon Sep 17 00:00:00 2001 From: Sam Harwell Date: Fri, 5 Dec 2025 13:49:28 -0600 Subject: [PATCH 08/12] docs: add coverage workflow guidance Why - Explain how to run local coverage with the new collector-based scripts. - Help contributors focus on coverage for changed code before pushing. What - Documented full and diff-only coverage commands, outputs, and CI alignment. Notes - Scripts rely on init/build having run; failures bubble as non-zero exit codes. Testing - Not run (documentation change only). --- CONTRIBUTING.md | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8917cb3a6..2390847ad 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -13,6 +13,41 @@ You can also help by filing issues, participating in discussions and doing code * The version of the [.NET Core SDK](https://dotnet.microsoft.com/download/dotnet-core) as specified in the global.json file at the root of this repo. Use the init script at the root of the repo to conveniently acquire and install the right version. +## Code coverage + +You can generate local coverage using the built-in scripts after running `init.ps1` and a build. + +- Full coverage (Release): + + ```powershell + .\build\coverage-report.ps1 + ``` + +- Full coverage (Debug): + + ```powershell + .\build\coverage-report.ps1 -Debug + ``` + +- Diff-focused coverage (changed files vs origin/master): + + ```powershell + .\build\coverage-report.ps1 -DiffOnly + ``` + +- Diff-focused coverage against a specific base: + + ```powershell + .\build\coverage-report.ps1 -DiffBase origin/main -DiffOnly + ``` + +Outputs: +- Cobertura XML: `build\coverage\OpenCover.StyleCopAnalyzers.CSharp*.xml` +- HTML report: `build\coverage\index.htm` +- Diff HTML (when requested): `build\coverage\diff\index.htm` + +CI uses `dotnet test` with `coverlet.collector` and merges Cobertura reports with ReportGenerator before uploading to Codecov. The same filters are applied locally and in CI to keep results consistent. Test failures or missing coverage files will cause the coverage script to fail with a non-zero exit code. + ## Implementing a diagnostic 1. To start working on a diagnostic, add a comment to the issue indicating you are working on implementing it. From a32f7f1a853e2b4f897955dbbefdafafa3fabffe Mon Sep 17 00:00:00 2001 From: Sam Harwell Date: Fri, 5 Dec 2025 14:05:42 -0600 Subject: [PATCH 09/12] chore(deps): drop opencover package Why - OpenCover tooling is no longer used now that coverlet.collector drives coverage. What - Removed OpenCover entry from .nuget/packages.config. Notes - coverlet.console remains during migration; can be dropped once unused. Testing - .\.nuget\nuget.exe install .\.nuget\packages.config -OutputDirectory packages --- .nuget/packages.config | 1 - 1 file changed, 1 deletion(-) diff --git a/.nuget/packages.config b/.nuget/packages.config index 74bb95867..ab8e97bbd 100644 --- a/.nuget/packages.config +++ b/.nuget/packages.config @@ -2,7 +2,6 @@ - From c0fa23ef0ebf35baed97fb14963b04acd92486c6 Mon Sep 17 00:00:00 2001 From: Sam Harwell Date: Fri, 5 Dec 2025 14:08:55 -0600 Subject: [PATCH 10/12] chore(deps): remove coverlet.console dependency Why - Coverage now runs via coverlet.collector, so the console tool is no longer needed. What - Removed coverlet.console from .nuget/packages.config and cleaned its local package folder. Notes - OpenCover tooling was already removed; remaining coverage dependencies are ReportGenerator and CodecovUploader. Testing - Not run (dependency cleanup only). --- .nuget/packages.config | 1 - 1 file changed, 1 deletion(-) diff --git a/.nuget/packages.config b/.nuget/packages.config index ab8e97bbd..e91f8091b 100644 --- a/.nuget/packages.config +++ b/.nuget/packages.config @@ -1,7 +1,6 @@  - From 47b93aa59ea669ed265ae7ab16104dcf97589ed3 Mon Sep 17 00:00:00 2001 From: Sam Harwell Date: Fri, 5 Dec 2025 14:56:00 -0600 Subject: [PATCH 11/12] chore(coverage): include tests and enable single-hit Why - Ensure test assemblies are covered alongside product code. - Improve coverage collection performance for local and CI runs. What - Updated coverlet runsettings to include test assemblies and use single-hit mode. Notes - Existing include/exclude filters remain unchanged. Testing - Not run (configuration-only change). --- build/coverlet.runsettings | 2 ++ 1 file changed, 2 insertions(+) diff --git a/build/coverlet.runsettings b/build/coverlet.runsettings index e05fd4734..40a10ddd6 100644 --- a/build/coverlet.runsettings +++ b/build/coverlet.runsettings @@ -5,6 +5,8 @@ cobertura + true + true [StyleCop*]* System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverageAttribute **/*Designer.cs From 5a47a41612ef9d53fe0261a1ab2f3084d4c3a477 Mon Sep 17 00:00:00 2001 From: Sam Harwell Date: Fri, 5 Dec 2025 14:58:25 -0600 Subject: [PATCH 12/12] chore(gitignore): ignore coverage artifacts Why - Prevent local coverage outputs from cluttering git status. What - Added coverage directories (build/coverage, build/TestResults, etc.) to .gitignore. Notes - Existing ignore rules remain unchanged. Testing - Not run (ignore rule change only). --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 3fe9dd0d8..6c28dd9a9 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,9 @@ packages/ TestResults/ OpenCover.Reports/ OpenCover.Symbols/ +.coverage/ +coverage/ +coverage-*/ .nuget/NuGet.exe build/nuget/ *.log