如何获取和解析 CI 管道中所有生成的 coverage.cobertura 文件?

How to fetch and parse all the generated coverage.cobertura files in CI pipelines?

给定一个包含多个 xUnit 测试项目的 .Net 5 解决方案,我可以 运行 dotnet test 从解决方案的根开始,它将 运行 所有测试。

我想根据 https://docs.microsoft.com/en-us/dotnet/core/testing/unit-testing-code-coverage?tabs=windows#integrate-with-net-test I 运行 dotnet test --collect:"XPlat Code Coverage" 生成报告,每个测试项目生成一个 coverage.cobertura 文件。

基于 https://docs.microsoft.com/en-us/dotnet/core/testing/unit-testing-code-coverage?tabs=windows#generate-reports 我知道我可以安装 dotnet-reportgenerator-globaltool 工具并获得基于每个 coverage.cobertura 文件的可视化 HTML 报告。

但我想添加一个 CI 管道,当线路覆盖率低于 x % 时,我想让管道失败。

给定以下示例 Gitlab CI 配置

image: mcr.microsoft.com/dotnet/sdk:5.0

stages:
  - tests

tests:
  stage: tests
  script:
    - dotnet test --collect:"XPlat Code Coverage"

我如何收集所有生成的 coverage.cobertura.xml 文件,读取行覆盖率,如果值低于例如80%?

示例:

tests:
  stage: tests
  script:
    - dotnet test --collect:"XPlat Code Coverage"
    # for each coverage.cobertura file found in the test projects
    # parse the file
    # read the line coverage
    # fail if the value is less than 80 percent

如果像 xUnit 这样的工具已经提供了这样的功能,那我就不用重新发明轮子了!


编辑:

我知道我也可以使用 allow_failure 关键字让这个阶段处于警告状态。这对我来说很好,我只是想知道如何从生成的报告中读取所需的信息并验证它们以决定该阶段是否应该通过、失败或不稳定。

根据Coverlet options supported by VSTest integration,XPlat代码 覆盖率工具尚不支持合并覆盖率报告文件和验证阈值,但他们正在努力。现在有 solution to merge the reports and calculate the threshold provided by Daniel Paz

他们在工具的 VSTest 版本中使用 merge with coverlet.collector:

# set MergeWith value in runsettings.xml
$runsettingsFile = "$pipelineFolder/runsettings.xml"
$coverageJsonFullPath = "$testResultsDirectory/coverage.json"
(Get-Content $runsettingsFile).Replace('#mergewith#', $coverageJsonFullPath) | Set-Content $runsettingsFile

# calculate code coverage
Get-ChildItem -Path "test/unit","test/integration","test/component" -Recurse -Filter *.csproj | 
Foreach-Object {
  $dir = "$testResultsDirectory/$($_.BaseName)"
  dotnet add  $_.FullName package coverlet.collector -v 1.1.0
  dotnet test $_.FullName --collect:"XPlat Code Coverage" --settings $runsettingsFile --no-build --logger trx --results-directory $dir
  Copy-Item -Path "$dir/*/coverage.json" -Destination $coverageJsonFullPath -Force
}

# copy results to $testResultsDirectory
Copy-Item -Path "$dir/*/coverage.cobertura.xml" -Destination $testResultsDirectory
Copy-Item -Path "$dir/*/coverage.opencover.xml" -Destination $testResultsDirectory

运行设置:

<?xml version="1.0" encoding="utf-8" ?>
<RunSettings>
    <DataCollectionRunSettings>
        <DataCollectors>
            <DataCollector friendlyName="XPlat code coverage">
                <Configuration>
                    <Format>opencover,json,cobertura</Format>
                    <MergeWith>#mergewith#</MergeWith>
                    <Exclude>[*]*Migrations*</Exclude> <!-- [Assembly-Filter]Type-Filter -->
                    <ExcludeByAttribute>Obsolete,GeneratedCodeAttribute,CompilerGeneratedAttribute</ExcludeByAttribute>
                    <ExcludeByFile>**/Program.cs,**/test/**/*</ExcludeByFile> <!-- Absolute or relative file paths -->
                    <SingleHit>false</SingleHit>
                </Configuration>
            </DataCollector>
        </DataCollectors>
    </DataCollectionRunSettings>
    <InProcDataCollectionRunSettings>
        <InProcDataCollectors>
            <InProcDataCollector assemblyQualifiedName="Coverlet.Collector.DataCollection.CoverletInProcDataCollector, coverlet.collector, Version=1.1.0.0, Culture=neutral, PublicKeyToken=null"
                                 friendlyName="XPlat Code Coverage"
                                 enabled="True"
                                 codebase="coverlet.collector.dll" />
        </InProcDataCollectors>
    </InProcDataCollectionRunSettings>
</RunSettings>

并检查合并文件的阈值:

# compare code coverage to $coverageThreshold
$coverage =  Select-Xml -Path "$testResultsDirectory/coverage.cobertura.xml" -XPath "/coverage/@branch-rate" | % {[double]::Parse($_.Node.Value) * 100}
Write-Output "Coverage is $coverage"
if ($coverage -lt $coverageThreshold) {
  throw "Code coverage $coverage is less than threshold $coverageThreshold"
}

这听起来像是一个非常新手的方法,但这是对我有用的方法。我正在使用 Azure devops,我有多个类似的项目,导致每个测试项目有多个 coverage.cobertura 文件

首先我使用Report Generator task将所有覆盖率报告合并为一个,并将其存储在工作目录中。下面是 yaml。我同时生成 HtmlFormat 和 Cobertura 报告

- task: Palmmedia.reportgenerator.reportgenerator-build-release-task.reportgenerator@4
  displayName: ReportGenerator
  inputs:
    reports: '$(Build.SourcesDirectory)/**/*cobertura.xml'
    targetdir: '$(System.DefaultWorkingDirectory)/CoverageResults'
    reporttypes: 'HtmlInline_AzurePipelines;Cobertura'

生成的 cobertura 报告如下所示:

在此之后,我尝试使用 powershell task

阅读这份合并的 xml 报告
- powershell: |
   [XML]$coverage = Get-Content $(System.DefaultWorkingDirectory)/CoverageResults/Cobertura.xml
   if($coverage.coverage.'line-rate' -ge .50){Write-Host "The value is greater than 50."}else{throw}
  displayName: 'PowerShell Script'

在这个任务中,我尝试读取覆盖率文件,然后访问线路速率属性。如果合并报告的行速率不大于或等于 50,我将抛出错误。对于我的 powershell 任务,将 ErrorActionPreference 设置为 Stop 后,如果线路覆盖率不符合预期,管道将停止。您还可以包括其他条件,例如分支率以获得更高的准确性。

我想这方面还有很多需要改进的地方,但这只是我可以想出的一个快速解决方法。请随时提出改进建议