Repository navigation
Compute branch reachability from the control-flow graph (supersedes #2047) - #2056
Open
pcshrosbree wants to merge 2 commits into
Open
pcshrosbree wants to merge 2 commits into
pcshrosbree wants to merge 2 commits into
Conversation
Add the IsReachable property introduced by Bertk in coverlet-coverage#2047 to BranchPoint and BranchInfo. Compute branch reachability by walking each method's control-flow graph once, from the method entry. The walk follows branch targets, stops at return, throw and jmp, and enters the filter and handler of each exception handler whose try entry is reached. Every path of a branch gets the reachability of its branch instruction. Carry the flag through Instrumentation.Branch ([DataMember], default true), both places in Instrumenter that create a Branch, and the four BranchInfo initializers in Coverage. DataContractSerializer does not run field initializers, so Branch sets IsReachable to true in an OnDeserializing callback; otherwise an instrumentation state file without the field reads back with every branch unreachable. Branch coverage totals leave out unreachable branches, as in coverlet-coverage#2047. So do the per-branch details: Cobertura conditions, LCOV BRDA records and OpenCover branch points, so that each report's details agree with its totals. A Cobertura line whose branches are all unreachable is no longer marked as a branch line. The tests compile C# samples with Roslyn in Debug and in Release, so the branch points come from real IL with real offsets. Methods built with Cecil cover a branch and a switch that nothing reaches, and a branch after jmp. An end-to-end test in CoverageTests checks that the flag survives Instrumenter and Coverage. Refs coverlet-coverage#2036. Supersedes coverlet-coverage#2047. Co-authored-by: Bert <bert.k@hotmail.com>
Contributor
There was a problem hiding this comment.
🟡 Changes recommended
The disk-backed end-to-end test should move to the coverage-test project to satisfy mandatory unit-test filesystem isolation.
1 open issue
What changed in this PR
Adds control-flow-based branch reachability to Coverlet’s analysis and reporting, superseding #2047 without claiming to resolve #2036.
Changes:
- Computes reachability and propagates it through instrumentation and coverage results.
- Excludes unreachable branches from report totals and details while preserving older state-file compatibility.
- Adds analysis, serialization, propagation, and reporting regression tests.
| File | Description |
|---|---|
| test/coverlet.core.tests/Symbols/BranchPointReachabilityTests.cs | Tests reachability and state serialization. |
| test/coverlet.core.tests/Reporters/OpenCoverReporterTests.cs | Checks branch details against totals. |
| test/coverlet.core.tests/Reporters/LcovReporterTests.cs | Checks filtered branch records. |
| test/coverlet.core.tests/Reporters/CoberturaReporterTests.cs | Tests unreachable-branch reporting. |
| test/coverlet.core.tests/Coverage/CoverageTests.IsReachable.cs | Tests end-to-end flag propagation. |
| test/coverlet.core.tests/Coverage/CoverageSummaryTests.cs | Tests reachable-only totals. |
| src/coverlet.core/Symbols/CecilSymbolHelper.cs | Computes control-flow reachability. |
| src/coverlet.core/Symbols/BranchPoint.cs | Adds the reachability flag. |
| src/coverlet.core/Reporters/OpenCoverReporter.cs | Filters unreachable branch points. |
| src/coverlet.core/Reporters/LcovReporter.cs | Filters unreachable branch records. |
| src/coverlet.core/Reporters/CoberturaReporter.cs | Filters branch lines and conditions. |
| src/coverlet.core/Instrumentation/InstrumenterResult.cs | Serializes reachability with compatible defaults. |
| src/coverlet.core/Instrumentation/Instrumenter.cs | Copies reachability into instrumentation data. |
| src/coverlet.core/CoverageSummary.cs | Counts reachable branches only. |
| src/coverlet.core/CoverageResult.cs | Adds reachability to result branches. |
| src/coverlet.core/Coverage.cs | Preserves reachability during result construction. |
| Documentation/Changelog.md | Documents the reporting improvement. |
Review effort: Balanced
Comment on lines
+44
to
+45
| string directory = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); | ||
| Directory.CreateDirectory(directory); |
Author
There was a problem hiding this comment.
Moved in 7bebd95. The end-to-end test and the helper that builds its module are now in coverlet.core.coverage.tests. Coverage_BranchWithoutLines_IsReachableCopiedToBranchInfo stays in coverlet.core.tests.
Coverage_DeadBranch_IsReachableFalseSurvivesInstrumenterAndCoverage writes a compiled module and its PDB to a temporary directory. .github/copilot-instructions.md asks unit tests not to use the file system, so move the test and the helper that builds its module to coverlet.core.coverage.tests, as the Copilot review on coverlet-coverage#2056 suggested. Coverage_BranchWithoutLines_IsReachableCopiedToBranchInfo uses mocks and stays in coverlet.core.tests. coverlet.core.coverage.tests does not reference coverlet.core.tests, so the moved test compiles its sample with its own copy of the Roslyn step, and BranchPointReachabilityTests.Compile is private again.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

This PR supersedes #2047, as @Bertk asked in #2047 (comment). It keeps Bertk's
IsReachableproperty onBranchPointandBranchInfo, with a default oftrue. It adds the changes from my review of #2047 and regression tests. Refs #2036, but this PR is unlikely to change the partial coverage reported there (see "What to expect in reports").What changes
CecilSymbolHelper.ComputeReachableInstructionswalks each method's control-flow graph once, from the method entry. The walk follows branch targets, stops at return, throw andjmp, and enters the filter and handler of each exception handler whose try entry is reached. Every path of a branch gets the reachability of its branch instruction. This replaces the first Introduce BranchPoint property IsReachable #2047 version's check of the instructions laid out before each path. That check marked the normal path of a guard clause (if (s == null) throw ...;) as unreachable.BranchinInstrumenterResult.csgets[DataMember] public bool IsReachable = true;. Both places inInstrumenterthat create aBranchcopy the flag, and so do the fourBranchInfoinitializers inCoverage.DataContractSerializerdoes not run field initializers. Without help, a state file written before this change has noIsReachableelement, and every branch reads back as unreachable.Branchtherefore sets the field totruein an[OnDeserializing]callback. Incoverlet.msbuild.tasks.tests,Execute_StateUnderTest_WithInstrumentationState_ThresholdTypereads such a file fromTestAssets. It fails without the callback, including at Introduce BranchPoint property IsReachable #2047's current head, c415187.CoverageSummary.CalculateBranchCoverageand the Cobertura<conditions>count reachable branches only, as in Introduce BranchPoint property IsReachable #2047. LCOV branch records and OpenCover branch points also exclude unreachable branches so that report details agree with their totals. A Cobertura line whose branches are all unreachable is no longer markedbranch="True"; before, it showed100% (0/0)with empty conditions. The JSON report keeps every branch and carries the flag. TeamCity output already used the filtered totals.Documentation/Changelog.mdhas an entry under Unreleased.How this relates to the commits on #2047
Bertk's c415187 already adopts the graph walk and the pass-through. When I use c415187's
CecilSymbolHelper.csin this PR, all my tests pass except the newjmptest. Differences from c415187:[OnDeserializing]callback above, thejmpcase, and the LCOV, OpenCover and Cobertura line changes;CoverageResult.Mergestill matches branches as it did before. c415187 addsIsReachableto the match. When the flag is the same on both sides, the extra condition changes nothing. When the flags disagree, c415187 keeps two entries for one branch. That can happen with aMergeWithreport written before this change: such a report has noIsReachablefield, so its branches read back as reachable;BranchPointReachabilityTests.cs. From Introduce BranchPoint property IsReachable #2047 I kept, with small changes, theCoverageSummaryand Cobertura filter tests;BuildPointsForBranchandBuildPointsForSwitchCaseskeep their signatures. Path 1 and the switch cases copy the flag from path 0.Tests
The C# samples in the new
BranchPointReachabilityTestsare compiled at test time with Roslyn, in Debug and in Release. The branch points therefore come from real IL with real offsets and sequence points. Every test that reads branch points asserts the exact list of paths and flags, with noCount >= 2guards. The tests cover:throw,if (x) return, a plainif/elseand aswitch, each in Debug and Release;catchblock, and one inside acatchnested in anothercatch;MemoryStream: a reachable branch, a branch and aswitchthat nothing reaches, and a branch afterjmp;coverlet.core.coverage.tests: it compiles a module with a PDB, appends a branch that nothing reaches, runsCoverage.PrepareModulesandGetCoverageResult, and checksIsReachableon both theBranchand theBranchInfoobjects;BranchInfoinitializers inCoverage;CoveragePrepareResultserialization, with and without theIsReachableelement;The end-to-end test writes a compiled module and its PDB to a temporary directory.
.github/copilot-instructions.mdasks unit tests not to use the file system, so the second commit moves this test fromcoverlet.core.teststocoverlet.core.coverage.tests, as the Copilot review suggested. It does not run the instrumented code, so it runs in the test process.Results on Linux with SDK 10.0.402, Debug:
coverlet.core.testsnet10.0coverlet.core.testsnet8.0coverlet.core.coverage.testsnet10.0 and net8.0, the end-to-end test onlycoverlet.collector.testsnet10.0 and net8.0coverlet.msbuild.tasks.testsnet10.0coverlet.msbuild.tasks.testson net8.0 fails all 14 tests on master and on this PR alike, because MSBuildLocator finds no .NET 8 SDK on my machine.To check that the tests can fail, I broke the code one way at a time and, on net10.0, ran the whole
coverlet.core.testssuite and the end-to-end test. Each of these changes turns at least one new test red:jmp, end-to-end)catchtests, Debug and Release)catch)jmpInstrumentersiteCoveragesites[DataMember]fromBranch.IsReachable[OnDeserializing]callbackCoverageSummarycounts unreachable branches<conditions>include unreachable branchesBRDArecords for unreachable branchesThree changes leave every test green: removing the filter entry from the walk, setting the default of
Branch.IsReachabletofalse, and setting the default ofBranchPoint.IsReachabletofalse. In the current production paths, branches inside filters are skipped and handlers are entered independently. Every production site that constructs aBranchorBranchPointassigns the flag explicitly. TheBranchdeserialization callback also supplies the missing-field default. These mutations caused no failures in the exercised suite.What to expect in reports
In the compiled C# assemblies surveyed here, no branch point was unreachable under the graph walk. With this PR,
GetBranchPointsmarks every branch point in this PR'scoverlet.core.dllas reachable: 2,358 in Debug and 2,348 in Release. With the original look-back check from #2047, the same survey marks 52 branch points unreachable in Debug and 368 in Release. On the small guard-clause sample from my review, the Cobertura report fromcoverlet.consoleis identical to master's apart from its timestamp, and the JSON report gains"IsReachable": trueon each branch. So this PR is unlikely to change the partial coverage reported in #2036. As I wrote in the review, that issue looks like a hit-counting problem, and I have not run the #2036 project.Not tested
LEGACYbuild.coverlet.integration.tests, the MTP test projects, and the rest ofcoverlet.core.coverage.tests.