Skip to content

Cobertura post-processing double-counts <line> nodes, inflating lines-valid and every coverage rate #441

Description

@drmoisan

Discovered during research for issue #432 (epic #136, child F1).

Summary

scripts/vscode/Invoke-MSTestWithCoverage.ps1 and scripts/vscode/Invoke-MSTestWithCoverage.Helpers.ps1 compute coverage summaries using the XPath descendant axis .//lines/line. In the Cobertura documents actually produced by the pipeline, each <class> carries its <line> nodes twice: once nested under each <method> and once again as a class-level rollup. The descendant axis matches both sets, so every line is counted twice.

Evidence

  • Get-CoberturaCoverageSummary (scripts/vscode/Invoke-MSTestWithCoverage.Helpers.ps1:98) selects .//lines/line.
  • Merge-CoberturaClassesByFilename (scripts/vscode/Invoke-MSTestWithCoverage.Helpers.ps1:167) recomputes the merged line-rate through the same path, additionally double-weighting the primary class.
  • In the committed sample docs/features/active/2026-08-06-quickfiler-high-confidence-queue-init-stall-424/evidence/qa-gates/coverage-final.cobertura.xml, the header attribute lines-valid="110849" equals exactly the raw count of <line number= elements in the file, confirming the double count rather than the distinct-line count.

Full analysis: docs/features/active/2026-08-07-quickfiler-coverage-ledger-432/research/2026-08-07T22-15-quickfiler-coverage-ledger-research.md sections 4.2 and 4.5.

Impact

Reported repository-wide and per-assembly line-coverage figures are computed over an inflated denominator. The reported rate is not necessarily wrong in the same direction for every assembly, because the duplication is not uniform across classes. Every existing coverage gate and every committed coverage evidence baseline consumes this figure.

Why this is not fixed in #432

Issue #432 (the QuickFiler per-file coverage ledger and harness) deliberately does not change this behavior. Correcting the denominator would perturb every existing gate threshold and invalidate every committed coverage baseline in the repository at once, which is a separate, repo-wide change requiring its own baseline re-capture. The #432 harness avoids the defect locally by recomputing per-file rates directly from deduplicated <line> nodes and never reading the <class> line-rate attribute.

Suggested remediation

  1. Replace the .//lines/line descendant axis with a selection that reads each line exactly once, deduplicating by line number with max(hits).
  2. Re-capture the repository-wide coverage baseline against the corrected denominator.
  3. Reconcile existing gate thresholds and committed evidence artifacts against the corrected figures in the same change.

Activity

  1. drmoisan commented on Aug 8, 2026

    @drmoisan
    OwnerAuthor

    Confirmed, plus a second independent defect in the same two functions (from issue #454 preparation research)

    Preparation research for issue #454 (epic #136, child F11) independently re-verified this issue and
    found a second, distinct defect that a fix for the descendant axis alone would not correct. It is
    filed separately as #478.

    This issue's citations all hold

    • Get-CoberturaCoverageSummary selects .//lines/line at
      scripts/vscode/Invoke-MSTestWithCoverage.Helpers.ps1:98 — confirmed.
    • Merge-CoberturaClassesByFilename at :167 — confirmed.
    • In docs/features/active/2026-08-06-quickfiler-high-confidence-queue-init-stall-424/evidence/qa-gates/coverage-final.cobertura.xml,
      lines-valid="110849" equals the raw count of <line number= elements exactly, confirming the
      double count rather than a distinct-line count.

    The second defect (#478)

    Merge-CoberturaClassesByFilename unions the class-level <lines> of all <class> elements sharing
    a filename correctly (the union at :217-268 takes max hits per line number), but it never merges
    the corresponding <methods> subtrees of the non-primary group members. It then recomputes
    line-rate over .//lines/line, which matches the correct union plus only the primary class's
    method-level lines.

    The emitted per-file line-rate is therefore a blend of two different denominators. Worked example
    from the committed report, QfcHomeController.Iteration.cs:

    • True per-file figure from the class-level union alone: 45/56 = 80.36%
    • Emitted attribute: 0.8625, which is exactly 69/80 — the blended numerator and denominator

    Independent confirmation of the correct recipe: #424's own delta evidence
    (docs/features/active/2026-08-06-quickfiler-high-confidence-queue-init-stall-424/evidence/qa-gates/coverage-delta.2026-08-07T00-48.md:10,41)
    arrived at the class-level-only computation.

    Why the two should be fixed together

    Both live in the same two functions and both feed the same output attribute. Correcting the
    descendant axis without also merging the <methods> subtrees would leave the attribute wrong in a
    different way rather than fixing it. Whoever schedules this should take #441 and #478 as one change,
    including the baseline re-capture step this issue already lists.

    A related trap for the re-capture step

    #424's committed baseline artifact is raw (no <sources> element, line-rate=0.7019) while its
    final artifact is post-processed (0.856453). Epic #136 imported the raw 70.19% figure as its
    authoritative repository-wide baseline. Any re-capture must compare like with like, or it will
    appear to show a large coverage jump that is purely an artifact of comparing a raw report against a
    post-processed one.

    Full analysis: docs/features/active/2026-08-07-quickfiler-collection-controller-coverage-454/research/coverage-harness-contract.md
    sections A, B, and D.

  2. 6 remaining items

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions