Search code examples
c#powershellcode-coveragexunitcobertura

How to check that the code/test-coverage of cobertura.xml files is above a specified threshold?


I have a .Net 5 solution with multiple projects and multiple test projects. I want to make sure either everything or a specified percentage value (e.g. 80%) got covered by tests. I'm using xUnit for my tests and created the following Powershell script based on the docs

https://learn.microsoft.com/en-us/dotnet/core/testing/unit-testing-code-coverage?tabs=windows#generate-reports

dotnet test --collect:"XPlat Code Coverage";

reportgenerator -reports:'**/coverage.cobertura.xml' -targetdir:'CoverageReports' -reporttypes:'Cobertura';

[XML]$report = Get-Content CoverageReports/Cobertura.xml

if($report.coverage.'line-rate' -ge 20)
{
    Write-Host "greater or equal than 20"
}
else
{
    Write-Host "less than 20"
}

Read-Host -Prompt 'done'

which does the following

  • it runs the tests and creates one cobertura file per test project
  • it merges every cobertura file into one and puts it into the CoverageReports directory
  • it parses the .xml file

now I have access to the coverage info, e.g. the line-rate. Instead of the dummy if statement, how can I achieve the following sample?

if($report.coverage.percentage -lt 80)
{
    Write-Host "Coverage is less than 80 percent"
}

and as a bonus I could write down a list of things that are not covered yet.


This is the content of a generated Cobertura.xml file from a dummy project

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE coverage SYSTEM "http://cobertura.sourceforge.net/xml/coverage-04.dtd">
<coverage line-rate="1" branch-rate="1" lines-covered="4" lines-valid="4" branches-covered="0" branches-valid="0" complexity="4" version="0" timestamp="1627911309">
  <sources>
    <source>C:\</source>
  </sources>
  <packages>
    <package name="ClassLibrary1" line-rate="1" branch-rate="1" complexity="3">
      <classes>
        <class name="ClassLibrary1.BoolReturner" filename="C:\...\ClassLibrary1\BoolReturner.cs" line-rate="1" branch-rate="1" complexity="3">
          <methods>
            <method name="Get" signature="(...)" line-rate="1" branch-rate="1" complexity="1">
              <lines>
                <line number="5" hits="3" branch="false" />
              </lines>
            </method>
            <method name="GetFalse" signature="()" line-rate="1" branch-rate="1" complexity="1">
              <lines>
                <line number="6" hits="1" branch="false" />
              </lines>
            </method>
            <method name="X" signature="()" line-rate="1" branch-rate="1" complexity="1">
              <lines>
                <line number="7" hits="1" branch="false" />
              </lines>
            </method>
          </methods>
          <lines>
            <line number="5" hits="3" branch="false" />
            <line number="6" hits="1" branch="false" />
            <line number="7" hits="1" branch="false" />
          </lines>
        </class>
      </classes>
    </package>
    <package name="ClassLibrary2" line-rate="1" branch-rate="1" complexity="1">
      <classes>
        <class name="ClassLibrary2.StringCombiner" filename="C:\...\StringCombiner.cs" line-rate="1" branch-rate="1" complexity="1">
          <methods>
            <method name="Combine" signature="(...)" line-rate="1" branch-rate="1" complexity="1">
              <lines>
                <line number="7" hits="2" branch="false" />
              </lines>
            </method>
          </methods>
          <lines>
            <line number="7" hits="2" branch="false" />
          </lines>
        </class>
      </classes>
    </package>
  </packages>
</coverage>

Solution

  • Cobertura report provides line coverage information as 'line-rate' attribute. It contains value in range from 0 to 1 (1 means 100%). Attribute 'line-rate' is defined at different levels: for entire report - coverage root element; for particular assembly - package element; for particular class - class element and etc.

    If the goal is to check code coverage percentage for the entire report (the entire code base), this can be achieved with the flowing code snippet

    [XML]$report = Get-Content CoverageReports/Cobertura.xml
    
    if ($report.coverage.'line-rate' -lt 0.8) {
      Write-Host "Coverage is less than 80 percent"
    }
    

    If the code coverage percentage should be checked more selectively, for example at the class level, then code snippet could be as follows

    [XML]$report = Get-Content CoverageReports/Cobertura.xml
    
    # Select all rows with line-rate < 0.8 (80%).
    $classes = $report.SelectNodes('//class[@line-rate < 0.8]');
    
    # Check number of selected rows.
    if ($classes.Count -gt 0) {
      Write-Host "Coverage is less than 80 percent"
    
      #Write list of files with low coverage.
      $classes | Sort-Object -Property 'line-rate' | Format-Table -Property 'line-rate', filename
    }
    

    In this example the list of classes with the low coverage is selected using XPath expression //class[@line-rate < 0.8]. It selects all class elements with line-rate attribute value lesser than 0.8 (80%).


    Also, instead of XPath, similar logic can be written directly in PS code when more complex data analysis is required, for example

    [XML]$report = Get-Content CoverageReports/Cobertura.xml
    
    $classes = $report.SelectNodes('//class');
    $lowCoverage = $false;
    
    foreach($class in $classes){
    
      # Convert line-rate value to 0..100% range.
      $percentage = [int](100.0 * [double]$class.'line-rate');
    
      if ($percentage -lt 80) {
        Write-Host $class.filename " [$percentage%]"
        $lowCoverage = $true;
      }
    }
    
    if ($lowCoverage) {
      Write-Host "Coverage is less than 80 percent"
    }
    

    Advanced XML processing techniques can be found in answers to ForEach in XML file with PowerShell question.