Skip to content

Eliminates phantom branches from async try-finally with await statements#1904

Merged
Bertk merged 11 commits into
coverlet-coverage:masterfrom
Bertk:fix-issue-1767
May 16, 2026
Merged

Eliminates phantom branches from async try-finally with await statements#1904
Bertk merged 11 commits into
coverlet-coverage:masterfrom
Bertk:fix-issue-1767

Conversation

@Bertk
Copy link
Copy Markdown
Collaborator

@Bertk Bertk commented Apr 21, 2026

  • Improves branch coverage accuracy for async methods
  • Maintains existing coverage for real user code branches
  • Consistent with existing patterns in the codebase (similar to await foreach/using handlers)

Issues #1313, #1337 relates to incorrect or incomplete coverage reporting for async methods containing try-finally blocks with await statements in the finally block. When the C# compiler generates IL for such constructs, it creates complex exception handling patterns within the async state machine's MoveNext() method that include:

  1. Exception state tracking fields (<>s__X fields of type int or object)
  2. Compiler-generated branches to check exception state and manage finally block execution
  3. Await continuation logic within the finally block handlers
    These compiler-generated branches were being reported as user code branches, leading to:
    • Phantom branches in coverage reports
    • Incorrect branch coverage metrics
    • Confusing coverage results where simple linear code shows uncovered branches

Issue #1828 is resolved and the attributes are copied during instrumentation.

Copilot AI review requested due to automatic review settings April 21, 2026 09:59
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates coverlet’s branch-point detection to ignore compiler-generated conditional branches produced by async state machines for try/finally blocks that contain await in the finally, and adds coverage samples/tests intended to prevent regressions.

Changes:

  • Add async try/finally branch-skipping logic to CecilSymbolHelper.GetBranchPoints() for async state machine MoveNext().
  • Add new async sample methods (Issue_1767) covering multiple try/finally + await patterns.
  • Add new coverage tests asserting “no phantom branches” (and preserving real user branches) for the new samples.

Reviewed changes

Copilot reviewed 2 out of 3 changed files in this pull request and generated 5 comments.

File Description
src/coverlet.core/Symbols/CecilSymbolHelper.cs Adds SkipGeneratedBranchesForAsyncTryFinally and wires it into async state-machine branch filtering.
test/coverlet.core.coverage.tests/Samples/Instrumentation.AsyncAwait.cs Appends new async try/finally sample methods used for instrumentation/coverage validation.
test/coverlet.core.coverage.tests/Coverage/CoverageTests.AsyncAwait.cs Adds new tests aimed at detecting/removing phantom branches for the new async try/finally samples.

Comment thread test/coverlet.core.coverage.tests/Coverage/CoverageTests.AsyncAwait.cs Outdated
Comment thread src/coverlet.core/Symbols/CecilSymbolHelper.cs
Comment thread test/coverlet.core.coverage.tests/Coverage/CoverageTests.AsyncAwait.cs Outdated
Comment thread test/coverlet.core.coverage.tests/Coverage/CoverageTests.AsyncAwait.cs Outdated
@Bertk Bertk marked this pull request as draft April 21, 2026 10:29
sstjean added a commit to sstjean/epcubegraph that referenced this pull request Apr 29, 2026
Coverlet 10.0.0 emits async state machine classes (d__N) with 0 hits
for await using / try-finally patterns, causing false coverage drops.
Known upstream issue: coverlet-coverage/coverlet#1337, #1767.
Active fix in progress: coverlet-coverage/coverlet#1904.

8.0.1 correctly reports 100% coverage for the same code.
Upgrade to 10.x once PR #1904 ships.
@Bertk Bertk requested a review from Copilot May 1, 2026 07:04
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 5 changed files in this pull request and generated 6 comments.

Comment thread test/coverlet.core.coverage.tests/Samples/Instrumentation.AsyncAwait.cs Outdated
Comment thread test/coverlet.core.coverage.tests/Coverage/CoverageTests.AsyncAwait.cs Outdated
Comment thread test/coverlet.core.coverage.tests/Coverage/CoverageTests.AsyncAwait.cs Outdated
Comment thread test/coverlet.core.tests/Reporters/CoberturaReporterTests.cs
Comment thread src/coverlet.core/Reporters/CoberturaReporter.cs
Comment thread src/coverlet.core/Reporters/CoberturaReporter.cs
@Bertk Bertk changed the title Eliminates phantom branches from async try-finally with await statements Eliminates phantom branches from async try-finally with await statements and cobertura path-separator normalization May 1, 2026
@Bertk Bertk force-pushed the fix-issue-1767 branch from 8c77b11 to 172bb13 Compare May 2, 2026 07:36
@Bertk Bertk changed the title Eliminates phantom branches from async try-finally with await statements and cobertura path-separator normalization Eliminates phantom branches from async try-finally with await statements May 2, 2026
@Bertk Bertk marked this pull request as ready for review May 4, 2026 09:41
@Bertk Bertk requested a review from Copilot May 4, 2026 11:01
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 12 out of 15 changed files in this pull request and generated 6 comments.

Comment thread src/coverlet.core/Instrumentation/Instrumenter.cs Outdated
Comment thread src/coverlet.core/Instrumentation/Instrumenter.cs Outdated
Comment thread test/coverlet.core.coverage.tests/Coverage/CoverageTests.AsyncAwait.cs Outdated
Comment thread test/coverlet.core.coverage.tests/Coverage/CoverageTests.AsyncAwait.cs Outdated
Comment thread test/coverlet.core.coverage.tests/Coverage/CoverageTests.AsyncAwait.cs Outdated
@Bertk Bertk force-pushed the fix-issue-1767 branch from d08310f to 23ed6dd Compare May 4, 2026 12:31
@Bertk Bertk added enhancement General enhancement request as-designed Expected behaviour coverlet-core and removed as-designed Expected behaviour labels May 4, 2026
@Bertk
Copy link
Copy Markdown
Collaborator Author

Bertk commented May 5, 2026

Current coverage metrics for master and pull request.

2026.05.10 master PR branch
Covered lines 5410 5302
Uncovered lines 850 1209
Coverable lines 6260 6511
Total lines 11507 11960
Line coverage 86.4% 81.4%
Covered branches 1878 1956
Total branches 2287 2523
Branch coverage 82.1% 77.5%

@Bertk Bertk marked this pull request as draft May 7, 2026 14:43
@Bertk Bertk marked this pull request as ready for review May 8, 2026 18:08
@Bertk Bertk force-pushed the fix-issue-1767 branch 2 times, most recently from f9584c3 to 26b7ce7 Compare May 8, 2026 18:36
…reporting

- Updated .gitignore to exclude dotnet-install.sh.
- Improved documentation on branch coverage behavior for async methods in UnderstandingBranchCoverage.md.
- Enhanced Instrumenter.cs to implement trampoline instrumentation for taken-branch paths, preventing false-positive coverage for async try-finally patterns.
- Added new methods in CecilSymbolHelper.cs to skip generated branches for async try-finally constructs.
- Introduced tests in CollectCoverageTests.cs and CoverageTests.AsyncAwait.cs to validate coverage behavior for async methods with try-finally blocks, ensuring no phantom branches are reported.
- Updated TestEnvironment.cs to handle interactive stdin scenarios.
- Refined assertions in CoverageTests.SelectionStatements.cs to accurately reflect branch coverage.
Bertk added 10 commits May 13, 2026 10:54
Refactor Instrumenter.cs to use ContainsKey/Add for .NET Standard 2.0 and TryAdd for other frameworks when updating branchRedirectMap, improving cross-framework compatibility. Also, clarify a comment and remove an obsolete issue reference in CoverageTests.AsyncAwait.cs without changing test logic.
…eview comments

Switch trampolines to use (branchOffset, path) as the key, ensuring each branch path (including switch arms sharing targets) gets a unique trampoline and independent counter. Update ReplaceInstructionTarget to match new keying. Adjust async/await test to assert correct branch coverage and prevent phantom branches from multiple awaits in finally blocks.
Detect and skip compiler-generated branches from relational pattern `is >= A and <= B` in compound `&&` expressions to prevent misleading branch coverage. Add targeted samples and unit tests to verify correct handling of both compound and simple relational pattern scenarios. Update copyright headers.
Add regression test and update instrumenter logic to guarantee the injected Tracker class is marked with [CompilerGenerated] and [ExcludeFromCodeCoverage] attributes, addressing issue coverlet-coverage#1828 and improving compatibility with reflection-based and coverage tools.
Update test to match tracker type by exact namespace and name,
using module filename and identifier. This ensures robustness
when assemblies contain multiple tracker types from prior
instrumentation runs.
…ge#1337)

Enhance CecilSymbolHelper to detect both direct and local variable-based exception null checks in async/await state machines, preventing false-positive phantom branches. Add targeted test and sample method for Issue coverlet-coverage#1337. Update instructions and minor formatting.
Remove proximity search for await-related patterns when identifying compiler-generated null-checks in async state machines. Now rely solely on opcode, field name, field type, ldarg.0, and presence of finally handler for detection, streamlining the logic.
Async try/finally blocks compile to Catch handlers, not Finally, so the previous check for ExceptionHandlerType.Finally would always return false. Now, the method always returns true when other conditions are met, ensuring correct identification of compiler-generated null-checks in async methods.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

coverlet-core enhancement General enhancement request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants