In a recent post, I shared how to set up a CI/CD pipeline for a .NET Aspire project on GitLab. The pipeline includes unit tests, security scanning, and secret detection, and if any of those fail, the pipeline would fail. Great, but what about code coverage for the unit tests? The pipeline included code coverage commands, but the coverage was not visible in the GitLab interface. Let's fix that.
The Problem
One thing I initially thought was that the regex used to extract the coverage was incorrect. The regex used in the pipeline was:
coverage: '/Total\s*\|\s*(\d+(?:\.\d+)?)%/'
That regex came directly from the GitLab documentation, so I thought it should work correctly. However, coverage still wasn't visible in the GitLab interface.
So with the help of GitHub Copilot, we wrote a few commands to validate:
- That the
coverage.cobertura.xml
was in a consistent location (instead of being in a folder with a GUID name) - That the
coverage.cobertura.xml
file was in a valid format - What exactly the regex was looking for
Everything checked out fine, so why was the coverage not visible?
The Solution
It turns out that the coverage
command with the regex expression is scanning the console output and not the coverage.cobertura.xml
file. Aha! One solution was to install dotnet-tools
to changing where the the test results was persisted; to the console instead of the XML file, but I preferred keeping the .NET environment unchanged.
The solution I ended up implementing was executing a grep
command to extract the coverage from the coverage.cobertura.xml
file and then echoing it to the console. Here's what it looks like:
- COVERAGE=$(grep -o 'line-rate="[0-9.]*"' TestResults/coverage.cobertura.xml | head -1 | grep -o '[0-9.]*' | awk '{printf "%.1f", $1*100}')
- echo "Total | ${COVERAGE}%"
Results
And now when the pipeline runs, the coverage is visible in the GitLab pipeline!
And the badge is updated to show the coverage percentage.
Complete Configuration
Here's the complete test job configuration. Of course, the full .gitlab-ci.yml file is available in the GitLab repository.
test:
stage: test
image: mcr.microsoft.com/dotnet/sdk:9.0
<<: *dotnet_cache
dependencies:
- build
script:
- dotnet test $SOLUTION_FILE --configuration Release --logger "junit;LogFilePath=$CI_PROJECT_DIR/TestResults/test-results.xml" --logger "console;verbosity=detailed" --collect:"XPlat Code Coverage" --results-directory $CI_PROJECT_DIR/TestResults
- find TestResults -name "coverage.cobertura.xml" -exec cp {} TestResults/coverage.cobertura.xml \;
- COVERAGE=$(grep -o 'line-rate="[0-9.]*"' TestResults/coverage.cobertura.xml | head -1 | grep -o '[0-9.]*' | awk '{printf "%.1f", $1*100}')
- echo "Total | ${COVERAGE}%"
artifacts:
when: always
reports:
junit: "TestResults/test-results.xml"
coverage_report:
coverage_format: cobertura
path: "TestResults/coverage.cobertura.xml"
paths:
- TestResults/
expire_in: 1 week
coverage: '/Total\s*\|\s*(\d+(?:\.\d+)?)%/'
Conclusion
I hope this helps others save time when setting up code coverage for their .NET projects on GitLab. The key insight is that GitLab's coverage regex works on console output, not on the files (XML or other formats).
If you have any questions or suggestions, feel free to reach out!
~frank