I am posting this after 2 days of trying various solutions that didn't work for me. This is mostly for my future reference, but I hope others can benefit as well. I used this source as the basis for my work, however this post was able to get Coverlet.MsBuild to work (which would have made things much easier) but for me, Coverlet.MSBuild would not generate anything no matter what I tried to do. So I had to go with Coverlet.Collect which made things a lot more complicated.
Here is how I set up my powershell to generate reports for TeamCity:
#Error Action Preference:
#
# PowerShell halts execution on terminating errors, as mentioned before.
# For non-terminating errors we have the option to tell PowerShell how
# to handle these situations.
# This is where the error action preference comes in. Error Action
# Preference allows us to specify the desired behavior for a non-terminating
# error; it can be scoped at the command level or all the way up to the
# script level.
#
#Available choices for error action preference:
#
# SilentlyContinue – error messages are suppressed and execution continues.
# Stop – forces execution to stop, behaving like a terminating error.
# Continue - the default option. Errors will display and execution will continue.
# Inquire – prompt the user for input to see if we should proceed.
# Ignore – (new in v3) – the error is ignored and not logged to the error stream. Has very restricted usage scenarios.
# Example: Set the preference at the script scope to Stop, place the following near the top of the script file:
$ErrorActionPreference = "Stop"
Set-StrictMode -Version 3.0 # Turns all sorts of silent errors into true errors
# This file is used by TeamCity build steps
$checkoutDir = $args[0]
Write-Output "`$checkoutDir : $checkoutDir"
$donetCliExePath = $args[1]
Write-Output "`$donetCliExePath : $donetCliExePath"
$coverletPath = "$checkoutDir\coverlet"
Write-Output "`$coverletPath : $coverletPath"
$coverageOutputPath = "$checkoutDir\coverage"
Write-Output "`$coverageOutputPath: $coverageOutputPath"
# The TeamCity Build Agent runs using the LocalSystem account
# and all user profile paths are defined in an Environment
# variable $env:USERPROFILE. All global dotnet tools are installed
# in the running users (LocalSystem) profile in the Path below:
$dotNetToolPath = "$env:USERPROFILE\.dotnet\tools"
Write-Output "`$dotNetToolPath : $dotNetToolPath"
Set-Alias -Name DotNet -Value "$donetCliExePath"
try
{
Write-output "Ensuring reportgenerator is available."
& DotNet tool update --global dotnet-reportgenerator-globaltool
# Get a list of all unit testing projects
$testCsProjList = Get-Childitem -Recurse $checkoutDir -Filter *.Tests.Unit.csproj
# Set up a job where each unit test dll is run in parallel
# up to 10 test fixtures at a time.
Write-Output ""
Write-output "Running unit tests and collecting code coverage..."
Write-Output ""
$testCsProjList | foreach-Object -ThrottleLimit 10 -Parallel {
DotNet test `
"$($_.FullName)" `
--no-build `
-c Release `
-l "console;verbosity=normal" `
--test-adapter-path "$env:USERPROFILE\.nuget\packages\xunit.runner.visualstudio\2.4.5\build\netcoreapp3.1" `
--collect:"XPlat Code Coverage" 2>&1
}
Write-Output ""
Write-Output "-----------------------------COMPLETE----------------------------------"
Write-Output ""
# Coverlet.Collector puts the coverage results in a dynamic folder.
# Coverlet.MsBuild has a known issue:
# https://github.com/coverlet-coverage/coverlet/blob/master/Documentation/KnownIssues.md#1-vstest-stops-process-execution-earlydotnet-test
# This problem can only be fixed by switching to Coverlet.Collector.
# Although we did not experience this error, I thought it would
# be prudent to try using the solution that avoids a potential
# coverage-stopping known problem.
# At this time, there is no way to specify the output folder that
# coverlet.collector should use.
# However, we can search the Tests folder for 'cobertura.xml' which
# is where coverlet stores the coverage results.
$found = Get-ChildItem -Path "$checkoutDir\tests" -Filter *.cobertura.xml -Recurse -File | Select-Object -ExpandProperty FullName
if ($null -eq $found) {
throw "No Coverage Results, please view logs for details. Exiting."
}
if(!(Test-Path $coverageOutputPath))
{ # Move-Item will not create the destination path if it does not
# exist and for some silly reason this is not an optional parameter.
# Here we ensure it exists so later we can move coverage reports here.
New-Item -Path $coverageOutputPath -ItemType Directory -Force | Out-Null
}
if(!(Test-Path $coverletPath))
{
New-Item -Path $coverletPath -ItemType Directory -Force | Out-Null
}
Write-output "Moving all coverage results:"
$found | ForEach-Object {
$projectCoveragePath = $_.Trim()
if([string]::IsNullOrWhitespace($_))
{
# Regex matches stuff and puts
# in blank results sometimes, no
# time to figure out why.
continue
}
$foundTestNames = $projectCoveragePath | Select-String 'unit\\(.*)\\TestResults' -AllMatches
if($foundTestNames.Matches.Count -ne 1) {
throw "Could not find coverage results. Likely cause: Unit Testing folder structure."
}
$testProjectName = $foundTestNames.Matches.Groups[1].Value # Assigning to variable for clarity
$destinationPath = "$coverletPath\$testProjectName.cobertura.xml"
Write-Output "Moving $testProjectName coverage results to: $destinationPath"
Move-Item -Path "$projectCoveragePath" -Destination "$destinationPath" -Force
}
Write-output "Generating TeamCity Report:"
& "$dotNetToolPath\reportgenerator.exe" "-targetdir:$coverageOutputPath" "-reports:$coverletPath\*.xml" "-reporttypes:Html"
if($LastExitCode -eq 1) {
throw "Running Unit Tests and Gathering coverage of Nexus.Api.SDK failed, please view logs for details. Exiting."
}
}
catch
{
$Exception = $_;
# Display info about exception
$Exception | Format-List * -Force | Out-String ;
# Display info about invocation context that cause error
$Exception.InvocationInfo | Format-List * -Force | Out-String ;
$Exception = $_.Exception;
# Recursively Display all Inner Exceptions depth-first
for ($i = 0; $Exception; $i++, ($Exception = $Exception.InnerException))
{
Write-Output ("$i" * 80) ;
$Exception | Format-List * -Force | Out-String
}
throw
}
Let me know if there is an easier way to achieve the above!