test-rerun.ps1

<#PSScriptInfo
.VERSION 1.0.0
.GUID 8115a829-e7dc-4cee-bc7e-f495471c29ae
.AUTHOR henrik@laueriksson.com
.COMPANYNAME
.COPYRIGHT
.TAGS .NET Test Rerun Retry
.LICENSEURI
.PROJECTURI https://github.com/hlaueriksson/ConductOfCode
.ICONURI
.EXTERNALMODULEDEPENDENCIES
.REQUIREDSCRIPTS
.EXTERNALSCRIPTDEPENDENCIES
.RELEASENOTES
#>


<#
    .Synopsis
    Runs tests in a given path, and reruns the tests that fails.
 
    .Description
    Runs "dotnet test" and use the trx logger result file to collect failed tests.
    Then reruns the failed tests and reports the final result.
 
    .Parameter Path
    Path to the: project | solution | directory | dll | exe
    https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet-test#arguments
 
    .Parameter Configuration
    Build configuration for environment specific appsettings.json file.
 
    .Parameter Filter
    Filter to run selected tests based on: TestCategory | Priority | Name | FullyQualifiedName
    https://learn.microsoft.com/en-us/dotnet/core/testing/selective-unit-tests
 
    .Parameter Settings
    Path to the .runsettings file.
    https://learn.microsoft.com/en-us/visualstudio/test/configure-unit-tests-by-using-a-dot-runsettings-file
 
    .Parameter Retries
    Number of retries for each failed test.
 
    .Parameter Percentage
    Required percentage of passed tests.
 
    .Example
    .\test.ps1 -filter "TestCategory=RegressionTest"
    # Runs regression tests
 
    .Example
    .\test.ps1 .\FooBar.Tests\FooBar.Tests.csproj -filter "TestCategory=SmokeTest" -configuration "Development"
    # Runs FooBar smoke tests in Development
 
    .Example
    .\test.ps1 -retries 1 -percentage 95
    # Retries failed tests once and reports the run as green if 95% of the tests passed
 
    .Example
    .\test.ps1 -settings .\test.runsettings
    # Runs tests configured with a .runsettings file
 
    .Link
    https://github.com/hlaueriksson/ConductOfCode
#>

param (
    [string]$path = ".",

    [ValidateSet("Debug", "Release", "Development", "Production")]
    [string]$configuration = "Debug",

    [string]$filter,

    [string]$settings,

    [ValidateRange(1, 9)]
    [int]$retries = 2,

    [ValidateRange(0, 100)]
    [int]$percentage = 100
)

function Get-Option {
    param (
        [Parameter(Mandatory)]
        [string]$option,

        [string]$value
    )

    if ($value.Length -gt 0) {
        "$option $value"
    }
    else {
        [string]::Empty
    }
}

function Green {
    process { Write-Host $_ -ForegroundColor Green }
}

function Red {
    process { Write-Host $_ -ForegroundColor Red }
}

function Get-Elapsed {
    param (
        [Parameter(Mandatory)]
        [System.Diagnostics.Stopwatch]$timer
    )

    if ($timer.Elapsed.TotalHours -gt 1) {
        "$($timer.Elapsed.TotalHours.ToString("0.0000")) Hours"
    }
    elseif ($timer.Elapsed.TotalMinutes -gt 1) {
        "$($timer.Elapsed.TotalMinutes.ToString("0.0000")) Minutes"
    }
    else {
        "$($timer.Elapsed.TotalSeconds.ToString("0.0000")) Seconds"
    }
}

$timer = New-Object System.Diagnostics.Stopwatch
$timer.Start()

dotnet build $path --configuration $configuration

if (!$?) {
    # Build FAILED.
    Exit $LastExitCode
}

$currentPath = (Get-Item .).FullName
$testResultsPath = "$($currentPath)$([IO.Path]::DirectorySeparatorChar)testResults.trx"
$options = $("--no-build --logger `"trx;logfilename=$testResultsPath`" --configuration $configuration $(Get-Option "--filter" $filter) $(Get-Option "--settings" $settings)").Split(' ')

dotnet test $path $options

if ($?) {
    # Passed!
    Exit
}

[xml]$testResults = Get-Content -Path $testResultsPath

$failedUnitTestResults = $testResults.TestRun.Results.UnitTestResult | Where-Object outcome -eq 'Failed'
$unitTests = $testResults.TestRun.TestDefinitions.UnitTest
$counters = $testResults.TestRun.ResultSummary.Counters
$passedTests = New-Object Collections.Generic.List[string]
$failedTests = New-Object Collections.Generic.List[string]

Write-Output "`r`nRetry $($counters.failed) failed tests..."

foreach ($failed in $failedUnitTestResults) {
    $unitTest = $unitTests | Where-Object id -eq $failed.testId | Select-Object -First 1
    $fqn = "$($unitTest.TestMethod.className).$($unitTest.TestMethod.name)"

    Write-Output "`r`nRetry $fqn..."

    # Retry
    for ($i = 1; $i -le $retries; $i++) {
        $options = $("--no-build --logger `"console;verbosity=detailed`" --configuration $configuration --filter FullyQualifiedName=$fqn $(Get-Option "--settings" $settings)").Split(' ')
        $escapeparser = '--%'
        $parameters = "-- TestRunParameters.Parameter(name=\`"Retry\`", value=\`"$i\`")"

        dotnet test $path $options $escapeparser $parameters

        if ($?) {
            # Passed!
            $passedTests.Add($fqn)
            break
        }
    }

    if (!$?) {
        # Failed!
        $failedTests.Add($fqn)
    }
}

$timer.Stop()

$successPercentage = (($counters.executed - $failedTests.Count) * 100) / [int]$counters.executed

if ($failedTests.Count -eq 0 -or $successPercentage -ge $percentage) {
    # Passed!
    Write-Output "`r`nTest Rerun Successful." | Green
}
else {
    # Failed!
    Write-Output "`r`nTest Rerun Failed." | Red
}

Write-Output "Total tests: $($counters.executed)"
Write-Output " Reruned: $($counters.failed)"
Write-Output " Passed: $($counters.executed - $failedTests.Count) " | Green
Write-Output " Failed: $($failedTests.Count)" | Red
if ($percentage -lt 100 -and $successPercentage -ge $percentage) {
    Write-Output " Success %: $successPercentage (>= $percentage)" | Green
}
if ($percentage -lt 100 -and $successPercentage -lt $percentage) {
    Write-Output " Success %: $successPercentage (< $percentage)" | Red
}
Write-Output " Total time: $(Get-Elapsed $timer)"

Write-Output "`r`nReruned:"

foreach ($passed in $passedTests) {
    Write-Output "Passed $passed" | Green
}

foreach ($fail in $failedTests) {
    Write-Output "Failed $fail" | Red
}

if ($failedTests.Count -eq 0 -or $successPercentage -ge $percentage) {
    # Passed!
    Exit
}

# Failed!
Exit $failedTests.Count