mirror of
https://github.com/MAKS-IT-COM/maksit-certs-ui.git
synced 2026-05-16 04:48:12 +02:00
287 lines
9.2 KiB
PowerShell
287 lines
9.2 KiB
PowerShell
#requires -Version 7.0
|
|
#requires -PSEdition Core
|
|
|
|
<#
|
|
.SYNOPSIS
|
|
PowerShell module for running tests with code coverage.
|
|
|
|
.DESCRIPTION
|
|
Provides the Invoke-TestsWithCoverage function for running .NET tests
|
|
with Coverlet code coverage collection and parsing results.
|
|
|
|
.NOTES
|
|
Author: MaksIT
|
|
Usage: pwsh -Command "Import-Module .\TestRunner.psm1"
|
|
#>
|
|
|
|
function Import-LoggingModuleInternal {
|
|
if (Get-Command Write-Log -ErrorAction SilentlyContinue) {
|
|
return
|
|
}
|
|
|
|
$modulePath = Join-Path $PSScriptRoot "Logging.psm1"
|
|
if (Test-Path $modulePath) {
|
|
Import-Module $modulePath -Force
|
|
}
|
|
}
|
|
|
|
function Write-TestRunnerLogInternal {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$Message,
|
|
|
|
[Parameter(Mandatory = $false)]
|
|
[ValidateSet("INFO", "OK", "WARN", "ERROR", "STEP", "DEBUG")]
|
|
[string]$Level = "INFO"
|
|
)
|
|
|
|
Import-LoggingModuleInternal
|
|
|
|
if (Get-Command Write-Log -ErrorAction SilentlyContinue) {
|
|
Write-Log -Level $Level -Message $Message
|
|
return
|
|
}
|
|
|
|
Write-Host $Message -ForegroundColor Gray
|
|
}
|
|
|
|
function Invoke-TestsWithCoverage {
|
|
<#
|
|
.SYNOPSIS
|
|
Runs unit tests with code coverage and returns coverage metrics.
|
|
|
|
.PARAMETER TestProjectPath
|
|
One or more paths to test project directories (or .csproj files). Each project
|
|
is tested in order; coverage metrics are aggregated across all Cobertura outputs.
|
|
|
|
.PARAMETER Silent
|
|
Suppress console output (for JSON consumption).
|
|
|
|
.PARAMETER ResultsDirectory
|
|
Optional fixed directory where test result files are written.
|
|
|
|
.PARAMETER KeepResults
|
|
Keep the TestResults folder after execution.
|
|
|
|
.OUTPUTS
|
|
PSCustomObject with properties:
|
|
- Success: bool
|
|
- Error: string (if failed)
|
|
- LineRate: double
|
|
- BranchRate: double
|
|
- MethodRate: double
|
|
- TotalMethods: int
|
|
- CoveredMethods: int
|
|
- CoverageFile: string (ReportGenerator reports arg: one file, or semicolon-separated)
|
|
- CoverageFiles: string[] (individual Cobertura paths)
|
|
- ResultsDirectory: string (absolute folder used for dotnet test output; may be removed after run unless -KeepResults)
|
|
|
|
.EXAMPLE
|
|
$result = Invoke-TestsWithCoverage -TestProjectPath ".\Tests"
|
|
if ($result.Success) { Write-TestRunnerLogInternal -Level "INFO" -Message "Line coverage: $($result.LineRate)%" }
|
|
|
|
.EXAMPLE
|
|
$result = Invoke-TestsWithCoverage -TestProjectPath @(".\ProjA.Tests", ".\ProjB.Tests")
|
|
#>
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string[]]$TestProjectPath,
|
|
|
|
[switch]$Silent,
|
|
|
|
[string]$ResultsDirectory,
|
|
|
|
[switch]$KeepResults
|
|
)
|
|
|
|
$ErrorActionPreference = "Stop"
|
|
|
|
# Normalize to a non-empty list of absolute working directories (folder containing the test project).
|
|
$resolvedProjectDirs = [System.Collections.Generic.List[string]]::new()
|
|
foreach ($raw in $TestProjectPath) {
|
|
if ([string]::IsNullOrWhiteSpace($raw)) { continue }
|
|
$full = [System.IO.Path]::GetFullPath($raw.Trim())
|
|
if (-not (Test-Path $full)) {
|
|
return [PSCustomObject]@{
|
|
Success = $false
|
|
Error = "Test project not found at: $raw"
|
|
}
|
|
}
|
|
$item = Get-Item -LiteralPath $full
|
|
$dir = if ($item.PSIsContainer) { $item.FullName } else { $item.Directory.FullName }
|
|
if ($resolvedProjectDirs -notcontains $dir) {
|
|
[void]$resolvedProjectDirs.Add($dir)
|
|
}
|
|
}
|
|
|
|
if ($resolvedProjectDirs.Count -eq 0) {
|
|
return [PSCustomObject]@{
|
|
Success = $false
|
|
Error = "No valid test project paths were provided."
|
|
}
|
|
}
|
|
|
|
if ([string]::IsNullOrWhiteSpace($ResultsDirectory)) {
|
|
$ResultsDir = Join-Path $resolvedProjectDirs[0] "TestResults"
|
|
}
|
|
else {
|
|
$ResultsDir = [System.IO.Path]::GetFullPath($ResultsDirectory)
|
|
}
|
|
|
|
# Clean previous results once (shared output for all test runs).
|
|
if (Test-Path $ResultsDir) {
|
|
Remove-Item -Recurse -Force $ResultsDir
|
|
}
|
|
New-Item -ItemType Directory -Path $ResultsDir -Force | Out-Null
|
|
|
|
if (-not $Silent) {
|
|
Write-TestRunnerLogInternal -Level "STEP" -Message "Running tests with code coverage..."
|
|
foreach ($d in $resolvedProjectDirs) {
|
|
Write-TestRunnerLogInternal -Level "INFO" -Message "Test Project: $d"
|
|
}
|
|
}
|
|
|
|
foreach ($TestProjectDir in $resolvedProjectDirs) {
|
|
Push-Location $TestProjectDir
|
|
try {
|
|
$dotnetArgs = @(
|
|
"test"
|
|
"--collect:XPlat Code Coverage"
|
|
"--results-directory", $ResultsDir
|
|
"--verbosity", $(if ($Silent) { "quiet" } else { "normal" })
|
|
)
|
|
|
|
if ($Silent) {
|
|
$null = & dotnet @dotnetArgs 2>&1
|
|
}
|
|
else {
|
|
& dotnet @dotnetArgs
|
|
}
|
|
|
|
$testExitCode = $LASTEXITCODE
|
|
if ($testExitCode -ne 0) {
|
|
return [PSCustomObject]@{
|
|
Success = $false
|
|
Error = "Tests failed in '$TestProjectDir' with exit code $testExitCode"
|
|
}
|
|
}
|
|
}
|
|
finally {
|
|
Pop-Location
|
|
}
|
|
}
|
|
|
|
$coverageFiles = @(Get-ChildItem -Path $ResultsDir -Filter "coverage.cobertura.xml" -Recurse | Sort-Object FullName)
|
|
|
|
if ($coverageFiles.Count -eq 0) {
|
|
return [PSCustomObject]@{
|
|
Success = $false
|
|
Error = "Coverage file not found under: $ResultsDir"
|
|
}
|
|
}
|
|
|
|
if (-not $Silent) {
|
|
foreach ($cf in $coverageFiles) {
|
|
Write-TestRunnerLogInternal -Level "OK" -Message "Coverage file found: $($cf.FullName)"
|
|
}
|
|
Write-TestRunnerLogInternal -Level "STEP" -Message "Parsing coverage data..."
|
|
}
|
|
|
|
# Aggregate line/branch from Cobertura counters; methods by walking all files.
|
|
$linesCoveredTotal = 0L
|
|
$linesValidTotal = 0L
|
|
$branchesCoveredTotal = 0L
|
|
$branchesValidTotal = 0L
|
|
$totalMethods = 0
|
|
$coveredMethods = 0
|
|
|
|
foreach ($cf in $coverageFiles) {
|
|
[xml]$coverageXml = Get-Content -LiteralPath $cf.FullName -Raw
|
|
$root = $coverageXml.coverage
|
|
$lcAttr = $root.'lines-covered'
|
|
$lvAttr = $root.'lines-valid'
|
|
if ($null -ne $lcAttr -and $null -ne $lvAttr -and [long]$lvAttr -gt 0) {
|
|
$linesCoveredTotal += [long]$lcAttr
|
|
$linesValidTotal += [long]$lvAttr
|
|
}
|
|
|
|
$bcAttr = $root.'branches-covered'
|
|
$bvAttr = $root.'branches-valid'
|
|
if ($null -ne $bcAttr -and $null -ne $bvAttr -and [long]$bvAttr -gt 0) {
|
|
$branchesCoveredTotal += [long]$bcAttr
|
|
$branchesValidTotal += [long]$bvAttr
|
|
}
|
|
|
|
foreach ($package in @($root.packages.package)) {
|
|
foreach ($class in @($package.classes.class)) {
|
|
$methodNodes = $class.methods
|
|
if ($null -eq $methodNodes) { continue }
|
|
foreach ($method in @($methodNodes.method)) {
|
|
if ($null -eq $method) { continue }
|
|
$totalMethods++
|
|
if ([double]$method.'line-rate' -gt 0) {
|
|
$coveredMethods++
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($linesValidTotal -gt 0) {
|
|
$lineRate = [math]::Round(($linesCoveredTotal / $linesValidTotal) * 100, 1)
|
|
}
|
|
else {
|
|
# Fallback: average of per-file line-rate when counters are missing (older Cobertura).
|
|
$acc = 0.0
|
|
$n = 0
|
|
foreach ($cf in $coverageFiles) {
|
|
[xml]$coverageXml = Get-Content -LiteralPath $cf.FullName -Raw
|
|
$acc += [double]$coverageXml.coverage.'line-rate'
|
|
$n++
|
|
}
|
|
$lineRate = [math]::Round(($acc / [math]::Max($n, 1)) * 100, 1)
|
|
}
|
|
|
|
if ($branchesValidTotal -gt 0) {
|
|
$branchRate = [math]::Round(($branchesCoveredTotal / $branchesValidTotal) * 100, 1)
|
|
}
|
|
else {
|
|
$acc = 0.0
|
|
$n = 0
|
|
foreach ($cf in $coverageFiles) {
|
|
[xml]$coverageXml = Get-Content -LiteralPath $cf.FullName -Raw
|
|
$acc += [double]$coverageXml.coverage.'branch-rate'
|
|
$n++
|
|
}
|
|
$branchRate = [math]::Round(($acc / [math]::Max($n, 1)) * 100, 1)
|
|
}
|
|
|
|
$methodRate = if ($totalMethods -gt 0) { [math]::Round(($coveredMethods / $totalMethods) * 100, 1) } else { 0 }
|
|
|
|
$coveragePaths = @($coverageFiles | ForEach-Object { $_.FullName })
|
|
$coverageFileReportArg = $coveragePaths -join ";"
|
|
$resultsDirectoryFull = [System.IO.Path]::GetFullPath($ResultsDir)
|
|
|
|
# Cleanup unless KeepResults is specified
|
|
if (-not $KeepResults) {
|
|
if (Test-Path $ResultsDir) {
|
|
Remove-Item -Recurse -Force $ResultsDir
|
|
}
|
|
}
|
|
|
|
# Return results
|
|
return [PSCustomObject]@{
|
|
Success = $true
|
|
LineRate = $lineRate
|
|
BranchRate = $branchRate
|
|
MethodRate = $methodRate
|
|
TotalMethods = $totalMethods
|
|
CoveredMethods = $coveredMethods
|
|
CoverageFile = $coverageFileReportArg
|
|
CoverageFiles = $coveragePaths
|
|
ResultsDirectory = $resultsDirectoryFull
|
|
}
|
|
}
|
|
|
|
Export-ModuleMember -Function Invoke-TestsWithCoverage
|