Generated banner for Coverage Analysis
Generated logo for Coverage Analysis

Coverage Analysis

.NET (Microsoft)OfficialPublished by .NET (Microsoft)

  • Skill
  • Review and audit
  • Software development
  • MIT
  • Low risk
  • Has scripts

From the author

Activation requires either supplied .NET coverage reports/percentages/line, branch, or condition metrics, or an explicit request to collect .NET coverage for analysis. USE FOR: interpreting that evidence, including Cobertura data, partial conditions, plateaus, target arithmetic, project-wide coverage-backed CRAP, and coverage-backed refactoring safety. Analyze supplied reports directly without rerunning tests or installing tools. DO NOT USE FOR: requests with neither coverage evidence nor explicit coverage-collection intent, including hypothetical change-survival questions (use test-gap-analysis); CRAP or refactoring safety for one named target (use crap-score); or requests owned by test-tagging, find-untested-sources, test-anti-patterns, run-tests, or code-testing.
— the description by .NET (Microsoft)

AutoGineer summary

Written by AutoGineer from the skill’s own files, not by the author

Coverage Analysis provides diagnostic insights into .NET test coverage evidence. It interprets line, branch, and condition metrics from existing Cobertura reports or collects them on demand. The skill focuses on reconciling coverage totals, identifying specific code blocks that hinder progress, and performing risk-based analysis using CRAP (Change Risk Anti-Patterns) scores when explicitly requested. It is designed to provide actionable feedback without requiring manual tool installation or redundant test execution.

What it does

  • Interprets .NET line, branch, and condition coverage metrics.
  • Reconciles coverage totals and identifies specific blocking members.
  • Computes project-wide CRAP scores to identify high-risk hotspots.
  • Assesses refactoring safety based on complexity and coverage data.
  • Calculates projected coverage improvements for specific code targets.
  • Generates structured reports only upon explicit user request.

When to use it

  • You have existing Cobertura coverage reports and need to understand the gaps.
  • You need to determine if a specific refactoring task is safe based on current coverage.
  • You want to identify high-risk code areas (hotspots) that require better test coverage.
  • You need to calculate how much coverage will increase by testing specific methods.
  • You are stuck on a coverage plateau and need to know which files or methods are blocking progress.

Advantages

  • Directly analyzes existing reports without rerunning tests.
  • Provides objective risk assessment using the CRAP metric.
  • Avoids unnecessary tool installation or environment modification.
  • Offers clear, prioritized recommendations for improving test coverage.
  • Maintains a lightweight footprint by avoiding full reports unless requested.

Usage examples

“Analyze my Cobertura report at ./coverage/results.xml and tell me why my branch coverage is stuck at 60%.”
The agent parses the XML, identifies the specific methods or branches causing the plateau, and provides a summary of the uncovered logic with recommendations.
“Which methods in my project are the most dangerous to refactor based on coverage and complexity?”
The agent calculates CRAP scores for the project, identifies the top 3 high-risk hotspots, and suggests where to add tests to improve safety.
“If I add tests for the PaymentProcessor class, what will my total line coverage be?”
The agent calculates the projected coverage increase by assuming full coverage of the specified class and provides the new estimated total.

Install

  1. Download the skill as a ZIP:

    https://mp-staging.autogineer.com/agent-skills/dotnet/coverage-analysis/download
  2. Unzip it into the cross-client skills folder of your project:

    mkdir -p .agents/skills && unzip coverage-analysis.zip -d .agents/skills

Documentation by .NET (Microsoft)

Coverage Analysis

Purpose

Explain what .NET coverage evidence proves, reconcile target arithmetic, and identify the code blocking progress. Add complexity/CRAP ranking only when the user explicitly asks for risk hotspots, CRAP, priorities by risk, or refactoring safety.

When to Use

Use this skill for interpreting supplied .NET line/branch/condition evidence, coverage gaps and plateaus, target arithmetic, or explicit project-wide coverage-backed risk analysis.

When Not to Use

  • Named method, class, or file CRAP/refactoring-safety analysis — use the crap-score skill instead

  • Static source-to-test pairing or listing files with no tests — use find-untested-sources

  • Behavioral or pseudo-mutation gaps in existing tests — use test-gap-analysis

  • Test trait/category distributions or coverage shape by test type — use test-tagging

  • Writing or generating tests — this skill identifies where tests are needed, not write them

  • General test execution unrelated to coverage or CRAP analysis

  • Only collecting .NET coverage or printing a raw percentage with no diagnosis — use run-tests; use native tooling for non-.NET coverage collection or analysis. Interpreting .NET line/branch gaps remains in scope here

Inputs

InputRequiredDefaultDescription
Project/solution pathNoCurrent directoryPath to the .NET solution or project
Line coverage thresholdNo80%Minimum acceptable line coverage
Branch coverage thresholdNo70%Minimum acceptable branch coverage
Existing Cobertura pathNoDiscover only if not suppliedPreferred input; never rerun tests when usable
CRAP thresholdNo30Used only for explicit risk/CRAP requests
Hotspot countNo3Explicit risk requests only; cap at 10 unless the user asks for more

Discover optional inputs from the workspace. Do not ask for a project path when the current directory or a supplied report is sufficient.

Choose the smallest matching path

User intentRequired workDo not do
Explain a supplied excerpt, condition, or summaryAnswer directly from the supplied evidenceTools, CRAP, discovery, report files
Interpret a supplied Cobertura path or diagnose a plateauRead that report, reconcile totals, name all material gaps, answer directlyRerun tests, install tools, compute CRAP, or generate files unless explicitly requested
Rank risk hotspots, compute project-wide CRAP, or assess refactoring safetyUse the supplied/existing report, read references/guidelines.md, and compute CRAP before rankingCoverage-only ranking or a full report template unless requested
Analyze coverage when no report existsInvoke run-tests to collect coverage with the repository-compatible runner, then analyze the generated reportChoose or execute a test command independently; CRAP unless risk was requested
Produce a full markdown/HTML/CSV reportFirst deliver the direct answer; then read references/output-format.md or references/report-generation.mdReport generation before the answer

Words such as analyze coverage, what is blocking coverage, or why is coverage stuck do not by themselves request CRAP. Explicit signals include risk hotspot, CRAP, complexity-weighted priority, safe to refactor, or an equivalent request to combine complexity with coverage.

Existing-data fast path

When the user supplies a coverage excerpt, summary, or valid Cobertura path:

  • Treat it as authoritative input and start there.

  • Do not discover the solution or test projects unless source mapping is necessary.

  • Do not run dotnet test, install ReportGenerator, add a coverage package, or read references/setup-discovery.md or references/report-generation.md.

  • Do not write coverage-analysis.md or create a report directory unless the user requested a saved/full report.

  • For interpretation and plateau questions, parse only the evidence needed to answer. For explicit project-wide risk requests, use the bundled scripts as described in references/guidelines.md.

A failed read/view operation is not proof that a named path does not exist. Classify the failure, then make one targeted existence probe and use a normalized path or alternate reader only for confirmed tool availability, transport, or path-normalization failures and only after verifying the canonical path remains inside the workspace. Stop on content-exclusion, permission/policy, workspace-boundary, or unknown failures. Report a missing path only when the independent probe also fails; do not broaden the search or substitute another artifact.

Collection path

Use this path only when no usable coverage evidence exists and the user asked for analysis that requires it.

  1. Read references/setup-discovery.md.

  2. Prefer existing Cobertura discovered under the requested root.

  3. If none exists, invoke run-tests for repository overlay, platform, runner, and command selection, and have it collect Cobertura once per entry point.

  4. Analyze the resulting Cobertura. Compute CRAP only if risk analysis was explicitly requested.

When the workspace contains the project/test manifests needed by this path, perform discovery directly and delegate collection to run-tests. Do not ask the user to attach files or pre-run coverage that the current workspace lets you inspect and generate.

Do not modify production code. Any incidental test-project change needed for collection is owned by run-tests; include that change and its revert command in the final analysis.

The automatic collection path is for SDK-style projects. For classic non-SDK or packages.config projects, use only a repository-owned coverage command. If none exists, stop and request Cobertura XML. Never migrate the project, inject an SDK-style provider, create a wrapper project, or report substitute coverage from another assembly.

Interpretation and arithmetic invariants

  • A line hit proves execution, not both decision outcomes.

  • condition-coverage="50% (1/2)" proves one reported outcome ran, but not which one. Recommend forcing the opposite outcome. Without source, never invent likely predicates or claim whether true or false is missing. State that compound predicates need independently exercised operands and short-circuit combinations when applicable; do not infer the exact combinations without source or fuller XML.

  • Derive overall totals from Cobertura's covered/valid line counts. For target T, required covered lines are ceiling(valid lines × T).

  • Projected coverage is (current covered lines + newly covered distinct lines) / valid lines. State assumptions such as fully covering a method.

  • When asked whether one member can reach a target, show its maximum projected total and at least one concrete sufficient combination of supplied members or line gains. If no supplied combination is sufficient, say so.

  • For a multi-member target, name the exact combination and resulting covered / valid total; saying only "combine it with another member" is incomplete.

  • Reconcile member gaps against project totals. Method line ranges can overlap or omit class-level lines, so do not sum method counts as project truth.

  • Never call one member the sole, entire, or all remaining gap unless its distinct uncovered lines exactly reconcile with the project total and no other below-threshold member remains.

  • Name every supplied or extracted below-threshold member, but keep detail proportional: lead with the blockers, summarize the remainder in one sentence or a compact table.

Response contract

Answer the user's question in the first 2–4 sentences.

  • Excerpt or arithmetic question: one explanation plus the next test or member priority. No dashboard.

  • Existing-report interpretation or plateau: overall line/branch coverage, blocking members, reconciled target impact, and 1–3 recommendations. Use at most one compact table.

  • Explicit risk/CRAP request: top 3 actual hotspots by default, supporting complexity/coverage/CRAP values, remaining flagged count, and 1–3 priorities. Exclude fully covered low-risk methods from the hotspot table. Never exceed 10 rows unless the user requests a larger count. For refactoring safety, rank risky methods by CRAP rather than raw coverage alone, then name comparatively safe well-covered methods separately.

  • Explicit full report request: read references/output-format.md. Save the report only then.

Report only artifacts that exist. Do not announce inaccessible output paths or failed optional file writes when no file was requested.

Optional reports

HTML/CSV/markdown files are not part of normal analysis. Generate them only when the user explicitly requests report files or a CI artifact.

  1. Deliver the direct coverage/risk answer first.

  2. For a full markdown report, read references/output-format.md and save it.

  3. For HTML/CSV, then read references/report-generation.md. Do not install ReportGenerator before the direct answer, and do not retry a failed install.

Validation

  • Confirm a supplied report was used without test execution or tool installation.

  • Reconcile covered, valid, and uncovered line totals before projecting impact.

  • Confirm every stated blocker comes from supplied or extracted evidence.

  • If CRAP was requested, spot-check one score using references/guidelines.md.

  • If files were requested, verify they exist before reporting their paths.

Common Pitfalls

  • Existing report triggers setup work — stop. Analyze the named artifact first.

  • Generic analysis triggers CRAP — stop. CRAP requires explicit risk intent.

  • One method is called the entire gap — reconcile distinct uncovered lines and the full below-threshold set first.

  • A small request gets a dashboard — scale down to a direct answer or one table.

  • No Cobertura from collection — report the collection failure; do not invent substitute coverage.

  • Compiler-generated method names — verify suspicious async, lambda, or local function names against source before presenting them as user-authored members.

  • Test exit code 1 — coverage may still exist; proceed with a warning. Other nonzero build failures stop the collection path.

Files

references/setup-discovery.md

Coverage Analysis — setup and discovery

Read this file only when the user did not supply usable coverage evidence and the request requires workspace discovery or fresh collection. Do not read or run these probes for a supplied excerpt or valid Cobertura path.

Step 1: Locate the solution or project

Given the user's path (default: current directory), find the entry point. If the user or the repository's documented coverage command selects a specific .sln, .slnx, .slnf, or .csproj, pass that exact file as $root; do not replace it with a discovered alternative. Directory discovery stops on multiple solutions or, when no solution exists, multiple projects. Request an explicit entry point rather than selecting the first.

$root = "<user-or-repository-selected-file-or-directory>"
$requested = Get-Item -LiteralPath $root -ErrorAction Stop
$entry = $null
if (-not $requested.PSIsContainer) {
    if ($requested.Extension -notin @('.sln', '.slnx', '.slnf', '.csproj')) {
        throw "Unsupported entry point: $($requested.FullName)"
    }
    $entry = $requested
    $root = $requested.DirectoryName
} else {
    $root = $requested.FullName
    $candidates = @(Get-ChildItem -LiteralPath $root -File -Recurse -Depth 2 -ErrorAction Stop |
        Where-Object { $_.Extension -in @('.sln', '.slnx') -and $_.FullName -notmatch '[/\\](obj|bin)[/\\]' })
    if ($candidates.Count -eq 0) {
        $candidates = @(Get-ChildItem -LiteralPath $root -Filter "*.csproj" -File -Recurse -Depth 2 -ErrorAction Stop |
            Where-Object { $_.FullName -notmatch '[/\\](obj|bin)[/\\]' })
    }
    if ($candidates.Count -gt 1) {
        throw "Ambiguous entry point under $root. Specify one .sln, .slnx, .slnf, or .csproj: $($candidates.FullName -join ', ')"
    }
    if ($candidates.Count -eq 1) { $entry = $candidates[0] }
}
if ($entry) {
    $entryType = if ($entry.Extension -eq '.csproj') { 'Project' } else { 'Solution' }
    Write-Host "ENTRY_TYPE:$entryType"; Write-Host "ENTRY:$($entry.FullName)"
} else {
    Write-Host "ENTRY_TYPE:NotFound"
}

# Test projects: search the requested directory, then its containing Git root only.
$searchRoots = @($root)
$gitOutput = @(git -C $root rev-parse --show-toplevel 2>&1)
if ($LASTEXITCODE -eq 0) {
    $gitRoot = [string]$gitOutput[0]
} else {
    $gitError = $gitOutput | Out-String
    if ($gitError -notmatch 'not a git repository \(or any of the parent directories\): \.git') {
        throw "Git root lookup failed: $gitError"
    }
    $gitRoot = $null
}
if ($gitRoot) { $gitRoot = [System.IO.Path]::GetFullPath($gitRoot) }
if ($gitRoot -and $gitRoot -ne $root) {
    $sep = [System.IO.Path]::DirectorySeparatorChar
    $prefix = $gitRoot.TrimEnd($sep) + $sep
    if (-not $root.StartsWith($prefix, [System.StringComparison]::OrdinalIgnoreCase)) {
        throw "Git root is not a containing directory: $gitRoot"
    }
    $searchRoots += $gitRoot
}

function Get-SolutionProjects($solution) {
    $text = Get-Content -LiteralPath $solution.FullName -Raw -ErrorAction Stop
    if ($solution.Extension -eq '.slnx') {
        $xml = [xml]$text
        if ($xml.DocumentElement.Name -ne 'Solution') { throw "Invalid solution: $($solution.FullName)" }
        $paths = @($xml.SelectNodes('//Project[@Path]') | ForEach-Object { $_.Path })
    } elseif ($solution.Extension -eq '.sln') {
        if ($text -notmatch 'Microsoft Visual Studio Solution File, Format Version') {
            throw "Invalid solution: $($solution.FullName)"
        }
        $paths = @([regex]::Matches($text, '(?m)^Project\("[^"]+"\)\s*=\s*"[^"]*",\s*"([^"]+)",') |
            ForEach-Object { $_.Groups[1].Value } |
            Where-Object { [System.IO.Path]::GetExtension($_) -eq '.csproj' })
    } else {
        throw "Unsupported underlying solution: $($solution.FullName)"
    }
    foreach ($path in $paths) {
        if ([System.IO.Path]::GetExtension($path) -eq '.csproj') {
            $relative = $path.Replace('\', [System.IO.Path]::DirectorySeparatorChar)
            [System.IO.Path]::GetFullPath((Join-Path $solution.DirectoryName $relative))
        }
    }
}

# Scope before reading/classifying projects: solution membership, filter membership,
# or the single project that the selected test command actually targets.
if ($entry) {
    if ($entry.Extension -eq '.csproj') {
        $projectPaths = @($entry.FullName)
    } elseif ($entry.Extension -eq '.slnf') {
        $filter = Get-Content -LiteralPath $entry.FullName -Raw -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop
        if (-not $filter.solution.path -or $null -eq $filter.solution.projects) {
            throw "Invalid solution filter: $($entry.FullName)"
        }
        $solutionPath = $filter.solution.path.Replace('\', [System.IO.Path]::DirectorySeparatorChar)
        $solution = Get-Item -LiteralPath (Join-Path $entry.DirectoryName $solutionPath) -ErrorAction Stop
        $members = @(Get-SolutionProjects $solution)
        $projectPaths = @($filter.solution.projects | ForEach-Object {
            $relative = $_.Replace('\', [System.IO.Path]::DirectorySeparatorChar)
            $project = Get-Item -LiteralPath (Join-Path $solution.DirectoryName $relative) -ErrorAction Stop
            if ($members -notcontains $project.FullName) { throw "Filtered project is not in the solution: $($project.FullName)" }
            $project.FullName
        })
    } else {
        $projectPaths = @(Get-SolutionProjects $entry)
    }
    $projects = @($projectPaths | Select-Object -Unique | ForEach-Object {
        Get-Item -LiteralPath $_ -ErrorAction Stop
    })
    $testProjects = @($projects | Where-Object {
        (Select-String -LiteralPath $_.FullName -Pattern 'Microsoft\.NET\.Test\.Sdk|xunit|nunit|MSTest\.TestAdapter|"MSTest"|MSTest\.TestFramework|TUnit' -Quiet -ErrorAction Stop) -or
        $_.Name -match '(?i)(test|spec)'
    })
} else {
    $testProjects = @()
    foreach ($sr in $searchRoots) {
        $projects = @(Get-ChildItem -LiteralPath $sr -Filter "*.csproj" -File -Recurse -Depth 5 -ErrorAction Stop |
            Where-Object { $_.FullName -notmatch '[/\\](obj|bin)[/\\]' })
        $testProjects = @($projects | Where-Object {
            Select-String -LiteralPath $_.FullName -Pattern 'Microsoft\.NET\.Test\.Sdk|xunit|nunit|MSTest\.TestAdapter|"MSTest"|MSTest\.TestFramework|TUnit' -Quiet -ErrorAction Stop
        })
        if ($testProjects.Count -eq 0) {
            $testProjects = @($projects | Where-Object { $_.Name -match '(?i)(test|spec)' })
        }
        if ($testProjects.Count -gt 0) {
            if ($sr -ne $root) { Write-Host "SEARCHED:$sr" }
            break
        }
    }
}
Write-Host "TEST_PROJECTS:$($testProjects.Count)"
$testProjects | ForEach-Object { Write-Host "TEST_PROJECT:$($_.FullName)" }

# Project-system classification controls whether the automatic dotnet/provider path is safe.
$classicTestProjects = @($testProjects | Where-Object {
    $text = Get-Content -LiteralPath $_.FullName -Raw -ErrorAction Stop
    $hasSdk = $text -match '<Project[^>]+\bSdk\s*=' -or $text -match '<Sdk\b'
    $hasPackagesConfig = Test-Path (Join-Path $_.DirectoryName "packages.config")
    $hasClassicSignals = $text -match '\bToolsVersion\s*=' -or
        $text -match 'Microsoft\.(Common\.props|CSharp\.targets)' -or
        $text -match '<Compile\s+Include='
    $hasPackagesConfig -or (-not $hasSdk -and $hasClassicSignals)
})
Write-Host "CLASSIC_TEST_PROJECTS:$($classicTestProjects.Count)"
$classicTestProjects | ForEach-Object { Write-Host "CLASSIC_TEST_PROJECT:$($_.FullName)" }
$sdkTestProjects = @($testProjects | Where-Object {
    $classicTestProjects.FullName -notcontains $_.FullName
})
Write-Host "SDK_TEST_PROJECTS:$($sdkTestProjects.Count)"
$sdkTestProjects | ForEach-Object { Write-Host "SDK_TEST_PROJECT:$($_.FullName)" }

# Resolve the test output root (where coverage-analysis artifacts will be written)
if ($testProjects.Count -eq 0) {
    if ($gitRoot) {
        $testOutputRoot = $gitRoot
    } else {
        $testOutputRoot = $root
    }
} elseif ($testProjects.Count -eq 1) {
    $testOutputRoot = $testProjects[0].DirectoryName
} else {
    # Multiple test projects — find their deepest common parent directory
    $dirs = $testProjects | ForEach-Object { $_.DirectoryName }
    $common = $dirs[0]
    foreach ($d in $dirs[1..($dirs.Count-1)]) {
        $sep = [System.IO.Path]::DirectorySeparatorChar
        while (-not $d.StartsWith("$common$sep", [System.StringComparison]::OrdinalIgnoreCase) -and $d -ne $common) {
            $prevCommon = $common
            $common = Split-Path $common -Parent
            # Terminate if we can no longer move up (at filesystem root or no parent)
            if ([string]::IsNullOrEmpty($common) -or $common -eq $prevCommon) {
                $common = $null
                break
            }
        }
    }
    if ([string]::IsNullOrEmpty($common)) {
        # Fallback when no common parent directory exists (e.g., projects on different drives)
        if ($gitRoot) {
            $testOutputRoot = $gitRoot
        } else {
            $testOutputRoot = $root
        }
    } else {
        $testOutputRoot = $common
    }
}
Write-Host "TEST_OUTPUT_ROOT:$testOutputRoot"
  • If ENTRY_TYPE:NotFound and SDK-style test projects were found → use the test projects directly as dotnet test entry points.

  • If ENTRY_TYPE:NotFound and classic test projects were found → use only the repository's documented coverage command; do not infer dotnet test.

  • If ENTRY_TYPE:NotFound and no test projects found → stop: No .sln, .slnx, or test projects found under <path>. Provide the path to your .NET solution, filter, or project.

  • A missing/unreadable requested path, failed lookup, or ambiguous entry point is a discovery blocker, not measured zero coverage or evidence of an empty suite. Do not continue collection after a discovery error.

  • Discovery is an inventory, not permission to expand collection scope. Pass the exact selected entry point to run-tests and collect only its requested project graph. Do not replace a selected .csproj or .slnf with every test project found under the containing Git root. Apply the classic-project safety rules below within that selected graph.

  • Selected-entry arrays contain only the projects targeted by that entry: solution members, filter members, or one explicit project. Transitive build dependencies are not additional test-run targets. For a generated or nonstandard entry graph that these formats cannot represent, stop and use the repository's evaluated target inventory rather than scanning siblings.

  • If TEST_PROJECTS:0 and EXISTING_COBERTURA_COUNT > 0 (Step 2b) → continue with existing Cobertura XML analysis (no dotnet test run).

  • If TEST_PROJECTS:0 and EXISTING_COBERTURA_COUNT == 0 → stop: No test projects found (expected projects with 'Test' or 'Spec' in the name), and no existing Cobertura XML was provided. Add a test project or provide a Cobertura file path.

  • If CLASSIC_TEST_PROJECTS is nonzero and no existing Cobertura XML is found, search scripts/CI/docs for a repository-owned coverage command. Use it if it emits Cobertura.

  • If classic projects are the only test projects and no repository command exists, stop: Classic non-SDK or packages.config test project detected. The automatic SDK-style coverage-provider path would modify this project incorrectly. Run the repository's supported coverage workflow and provide its Cobertura XML. This is a hard stop: do not create or run a temporary SDK project against the classic source, because its coverage would belong to the substitute assembly, not the requested test project.

  • In a mixed solution, run automatic collection only for SDK_TEST_PROJECTS. Never run the solution entry point if it would include classic projects. Clearly label the result partial until repository-owned Cobertura data for the classic projects is also available.

Step 2: Create the output directory

$coverageDir = Join-Path $testOutputRoot "TestResults" "coverage-analysis"
if (Test-Path $coverageDir) { Remove-Item $coverageDir -Recurse -Force }
New-Item -ItemType Directory -Path $coverageDir -Force | Out-Null
Write-Host "COVERAGE_DIR:$coverageDir"

This step only manages the TestResults/coverage-analysis/ subdirectory (skill-owned outputs). It must never delete user-supplied Cobertura files — those live one level up at TestResults/coverage.cobertura.xml (or wherever the user pointed). If the user provided a path that is TestResults/coverage-analysis/..., copy the file aside before this step recreates the directory.

Step 2b: Discover or accept existing Cobertura XML (required for the existing-data path)

If the user supplied a Cobertura XML path explicitly, use it. Otherwise probe well-known locations and any path the user mentioned:

# 1. Honor a user-supplied path first (highest priority)
$coberturaFiles = @()
if ($userSuppliedCoberturaPath) {
    $coberturaFiles = @(Get-Item -LiteralPath $userSuppliedCoberturaPath -ErrorAction Stop)
}

# 2. Otherwise scan TestResults/ at the repo/test root for any *.cobertura.xml
if ($coberturaFiles.Count -eq 0) {
    $searchPaths = @(
        (Join-Path $testOutputRoot "TestResults"),
        (Join-Path $root "TestResults")
    ) | Where-Object { $_ -and (Test-Path $_) } | Select-Object -Unique
    foreach ($sp in $searchPaths) {
        $found = @(Get-ChildItem -LiteralPath $sp -Filter "*.cobertura.xml" -Recurse -ErrorAction Stop |
            Where-Object { $_.FullName -notmatch '[/\\]coverage-analysis[/\\]raw[/\\]' })
        if ($found.Count -gt 0) { $coberturaFiles = $found; break }
    }
}

Write-Host "EXISTING_COBERTURA_COUNT:$($coberturaFiles.Count)"
$coberturaFiles | ForEach-Object { Write-Host "EXISTING_COBERTURA:$($_.FullName)" }
  • If EXISTING_COBERTURA_COUNT > 0 → skip fresh collection and analyze these paths.

  • If EXISTING_COBERTURA_COUNT == 0 and all test projects are SDK-style → invoke run-tests to select the repository-compatible platform/provider command and collect Cobertura.

  • If EXISTING_COBERTURA_COUNT == 0 and only classic/packages.config projects exist → use a repository-owned coverage command that emits Cobertura; otherwise stop with the message above.

  • If EXISTING_COBERTURA_COUNT == 0 and both classic and SDK-style projects exist → ask run-tests to collect only for SDK_TEST_PROJECTS and mark the result partial until classic-project Cobertura is available.

Step 2c: Recommend ignoring TestResults/

$pattern = "**/TestResults/"
$gitRoot = (git -C $testOutputRoot rev-parse --show-toplevel 2>$null)
if ($gitRoot) { $gitRoot = [System.IO.Path]::GetFullPath($gitRoot) }
if ($gitRoot) {
    $gitignorePath = Join-Path $gitRoot ".gitignore"
    $alreadyIgnored = $false
    if (Test-Path $gitignorePath) {
        $alreadyIgnored = (Select-String -Path $gitignorePath -Pattern '^\s*(\*\*/)?TestResults/?\s*$' -Quiet)
    }
    if ($alreadyIgnored) {
        Write-Host "GITIGNORE_RECOMMENDATION:already-present"
    } else {
        Write-Host "GITIGNORE_RECOMMENDATION:$pattern"
    }
} else {
    Write-Host "GITIGNORE_RECOMMENDATION:$pattern"
}

Requirements and compatibility

Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta

“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.

Security

What our scanner noticed· Low risk

  • Scripts that your agent may run

This skill includes scripts that your agent may run. Read them before you use it.

A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.

Attribution and licence

Published by
.NET (Microsoft)
Publisher
.NET (Microsoft) (Official)
Source
dotnet/skills · plugins/dotnet-test/skills/coverage-analysis · commit 96c3d68
Licence
MIT License (redistribution allowed)
  • LICENSE (added by AutoGineer from LICENSE)
  • ATTRIBUTION.md (written by AutoGineer)
Synced

AutoGineer is not affiliated with or endorsed by .NET (Microsoft).

Download ZIP

The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download coverage-analysis.zip