Skip to content

Instrument test assemblies and raise test-class coverage to 100% #172

Description

@Chris-Wolfgang

Background

A fleet survey on 2026-08-14 found that ETL-SqlBulkCopy is the only repo measuring the coverage of its own test code. Every other repo instruments the src assembly only, so uncovered test code — dead helpers, unreachable branches, fakes nobody calls — has never been visible anywhere in the fleet.

Policy: test code is held to 100% line coverage. Test code that never executes has no purpose; it should be exercised or deleted. This is line coverage — branch coverage legitimately can't hit 100% (compiler-generated async state-machine branches), and the gate reads the line-coverage column only.

[ExcludeFromCodeCoverage] is a last resort, in this order:

  1. Write a test.
  2. If that's not possible, refactor the code to make it testable — without hurting performance.
  3. Only if both fail, apply [ExcludeFromCodeCoverage], and justify it in review.

Status in this repo

Test assemblies are not instrumented. coverlet.runsettings exists but lacks <IncludeTestAssembly>true</IncludeTestAssembly>, so Summary.txt contains src rows only and current test-code coverage is unknown.

Related defect: the Stage 1 coverage gate mis-parses

.github/workflows/pr.yaml's Stage 1 gate anchors its filter on ^[^ ] to skip ReportGenerator's indented per-class rows, but the loop reads with a bare while read -r line. read strips leading IFS whitespace, so the indentation is gone before grep sees it and every class row matches — classes get gated as if they were projects.

This stayed invisible fleet-wide precisely because test assemblies aren't instrumented: with no class rows in Summary.txt, there was nothing to mis-match. It surfaced on ETL-SqlBulkCopy's 0.7.0 release, where the gate failed the release on a test class at 89.4% while both real assemblies were above 97%.

Fix (one line):

-          while read -r line; do
+          while IFS= read -r line; do

Stage 2's PowerShell parser is unaffected (Get-Content preserves indentation). Note .github/workflows/pr.yaml is a protected file, so that change needs a maintainer bypass merge.

Steps

  • 1. Enable instrumentation — add to coverlet.runsettings:
    xml <IncludeTestAssembly>true</IncludeTestAssembly>
    Not a protected file, so this is an ordinary PR. Expect this PR to fail if test-code line coverage is below 90%. The Stage 1 gate already enforces CODECOV_MINIMUM against every assembly row, and enabling instrumentation adds the test assembly as a new row — so the threshold bites immediately, without step 4. That failure IS the measurement. (Confirmed in ETL-SqlBulkCopy's gate output: it checks Wolfgang.Etl.SqlBulkCopy.Tests.Unit as a module; it only passes because that assembly is at 99.5%.) Note release.yaml does NOT pass --settings coverlet.runsettings, so it will not instrument test assemblies even after this change — PR CI becomes stricter than release CI until step 5.
  • 2. Measure — run the suite and read the per-class rows for the test assembly in CoverageReport/Summary.txt. Record the starting number here.
  • 3. Raise every test class to 100% line coverage — following the escalation above. Expect genuinely dead test code; delete it rather than covering it for its own sake.
  • 4. Fix the Stage 1 gate parse (see above) and extend it to enforce 100% on test-assembly classes while src assemblies stay at the existing 90% (CODECOV_MINIMUM).
  • 5. Make release.yaml agree — it runs dotnet test without --settings coverlet.runsettings, so release-time coverage excludes different code than PR-time. Harmless today; once a 100% rule exists it means a release can fail a gate the PR passed.

Notes

  • Do not soften the gate to unblock work. If enforcing 100% walls off PRs until the backlog is cleared, that is the intended behaviour.
  • Steps 1–3 are per-repo. Step 4 is fleet-uniform and should land in repo-template first, then fan out.

Update: fleet sweep, 2026-08-14

A follow-up sweep found a second, larger reason test coverage is invisible: most test projects don't reference coverlet.collector, so dotnet test --collect:"XPlat Code Coverage" silently emits nothing for them — no error, no warning, just a missing coverage file.

Fleet totals: 83 test projects — 51 emit coverage, 31 emit none, 1 excluded by attribute. It is systematic by suite type, not random: Unit and Integration projects almost always have the collector; the specialty suites (AotSmoke, Fuzz, Concurrency, DocExamples, Snapshots, Allocation) were scaffolded without it across every repo that has them.

This repo — 1 of 1 test projects emit coverage

test project emits coverage?
Wolfgang.Extensions.String.Tests.Unit ✅ yes

Revised step 1 — three parts, not one

  1. Add coverlet.collector to every suite that should be measured.
  2. Explicitly decide (and write down) whether AotSmoke / SourceLink-style suites stay uninstrumented.
  3. Then add <IncludeTestAssembly>true</IncludeTestAssembly> — it only has an effect on projects that emit coverage at all.

Steps 2–5 in the original list are unchanged, but note the measurement in step 2 is only meaningful once step 1 is complete.

Scope decision, 2026-08-14

Two calls from Chris, narrowing this issue:

1. AotSmoke needs no coverage. Settled — AotSmoke suites stay uninstrumented deliberately. This is now a recorded decision, not an accident of scaffolding, and no longer an open question on this issue.

2. Phase 1 is unit tests only. Get the unit test projects to 100% first. The other specialty suites — Concurrency, DocExamples, Fuzz, Snapshots, Allocation, SourceLink — and the Integration suite are deferred to a later phase. Adding their collectors is explicitly not part of this issue's immediate work.

Deferred does not mean dismissed: the understated-src-coverage consequence noted above still holds while those suites emit nothing, so don't treat the post-phase-1 src number as the true figure.

Phase 1 scope for this repo

unit test project status
Wolfgang.Extensions.String.Tests.Unit ✅ already emits coverage — only needs IncludeTestAssembly

Revised phase-1 checklist:

  • Add coverlet.collector — not needed, the unit test project(s) already emit coverage.
  • Add <IncludeTestAssembly>true</IncludeTestAssembly> to coverlet.runsettings. Expect the PR to fail if unit-test line coverage is under 90% — the gate enforces CODECOV_MINIMUM against every assembly row. That failure is the measurement.
  • Raise every unit test class to 100% line coverage — write a test, else refactor to make it testable, and only then [ExcludeFromCodeCoverage].
  • Gate change (100% on unit test assemblies, src stays at 90%) — lands in repo-template first, then fans out.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions