Skip to content

Commit e251d23

Browse files
Fix and simplify async coverage (#549)
Fix and simplify async coverage
1 parent 8c7a8c5 commit e251d23

File tree

12 files changed

+249
-142
lines changed

12 files changed

+249
-142
lines changed

src/coverlet.core/Coverage.cs

Lines changed: 0 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -283,54 +283,11 @@ private void CalculateCoverage()
283283
}
284284
}
285285
}
286-
287-
// for MoveNext() compiler autogenerated method we need to patch false positive (IAsyncStateMachine for instance)
288-
// we'll remove all MoveNext() not covered branch
289-
foreach (var document in result.Documents)
290-
{
291-
List<KeyValuePair<BranchKey, Branch>> branchesToRemove = new List<KeyValuePair<BranchKey, Branch>>();
292-
foreach (var branch in document.Value.Branches)
293-
{
294-
//if one branch is covered we search the other one only if it's not covered
295-
if (IsAsyncStateMachineMethod(branch.Value.Method) && branch.Value.Hits > 0)
296-
{
297-
foreach (var moveNextBranch in document.Value.Branches)
298-
{
299-
if (moveNextBranch.Value.Method == branch.Value.Method && moveNextBranch.Value != branch.Value && moveNextBranch.Value.Hits == 0)
300-
{
301-
branchesToRemove.Add(moveNextBranch);
302-
}
303-
}
304-
}
305-
}
306-
foreach (var branchToRemove in branchesToRemove)
307-
{
308-
document.Value.Branches.Remove(branchToRemove.Key);
309-
}
310-
}
311-
312286
_instrumentationHelper.DeleteHitsFile(result.HitsFilePath);
313287
_logger.LogVerbose($"Hit file '{result.HitsFilePath}' deleted");
314288
}
315289
}
316290

317-
private bool IsAsyncStateMachineMethod(string method)
318-
{
319-
if (!method.EndsWith("::MoveNext()"))
320-
{
321-
return false;
322-
}
323-
324-
foreach (var instrumentationResult in _results)
325-
{
326-
if (instrumentationResult.AsyncMachineStateMethod.Contains(method))
327-
{
328-
return true;
329-
}
330-
}
331-
return false;
332-
}
333-
334291
private string GetSourceLinkUrl(Dictionary<string, string> sourceLinkDocuments, string document)
335292
{
336293
if (sourceLinkDocuments.TryGetValue(document, out string url))

src/coverlet.core/CoverageResult.cs

Lines changed: 0 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -108,57 +108,6 @@ internal void Merge(Modules modules)
108108
}
109109
}
110110
}
111-
112-
// for MoveNext() compiler autogenerated method we need to patch false positive (IAsyncStateMachine for instance)
113-
// we'll remove all MoveNext() not covered branch
114-
List<BranchInfo> branchesToRemove = new List<BranchInfo>();
115-
foreach (var module in this.Modules)
116-
{
117-
foreach (var document in module.Value)
118-
{
119-
foreach (var @class in document.Value)
120-
{
121-
foreach (var method in @class.Value)
122-
{
123-
foreach (var branch in method.Value.Branches)
124-
{
125-
//if one branch is covered we search the other one only if it's not covered
126-
if (IsAsyncStateMachineMethod(method.Key) && branch.Hits > 0)
127-
{
128-
foreach (var moveNextBranch in method.Value.Branches)
129-
{
130-
if (moveNextBranch != branch && moveNextBranch.Hits == 0)
131-
{
132-
branchesToRemove.Add(moveNextBranch);
133-
}
134-
}
135-
}
136-
}
137-
foreach (var branchToRemove in branchesToRemove)
138-
{
139-
method.Value.Branches.Remove(branchToRemove);
140-
}
141-
}
142-
}
143-
}
144-
}
145-
}
146-
147-
private bool IsAsyncStateMachineMethod(string method)
148-
{
149-
if (!method.EndsWith("::MoveNext()"))
150-
{
151-
return false;
152-
}
153-
154-
foreach (var instrumentedResult in InstrumentedResults)
155-
{
156-
if (instrumentedResult.AsyncMachineStateMethod.Contains(method))
157-
{
158-
return true;
159-
}
160-
}
161-
return false;
162111
}
163112

164113
public ThresholdTypeFlags GetThresholdTypesBelowThreshold(CoverageSummary summary, double threshold, ThresholdTypeFlags thresholdTypes, ThresholdStatistic thresholdStat)

src/coverlet.core/Instrumentation/Instrumenter.cs

Lines changed: 3 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ internal class Instrumenter
3636
private TypeDefinition _customTrackerTypeDef;
3737
private MethodReference _customTrackerRegisterUnloadEventsMethod;
3838
private MethodReference _customTrackerRecordHitMethod;
39-
private List<string> _asyncMachineStateMethod;
4039
private List<string> _excludedSourceFiles;
4140

4241
public Instrumenter(
@@ -123,8 +122,6 @@ public InstrumenterResult Instrument()
123122

124123
InstrumentModule();
125124

126-
_result.AsyncMachineStateMethod = _asyncMachineStateMethod == null ? Array.Empty<string>() : _asyncMachineStateMethod.ToArray();
127-
128125
if (_excludedSourceFiles != null)
129126
{
130127
foreach (string sourceFile in _excludedSourceFiles)
@@ -398,18 +395,18 @@ private void InstrumentIL(MethodDefinition method)
398395
index += 2;
399396
}
400397

401-
foreach (var _branchTarget in targetedBranchPoints)
398+
foreach (var branchTarget in targetedBranchPoints)
402399
{
403400
/*
404401
* Skip branches with no sequence point reference for now.
405402
* In this case for an anonymous class the compiler will dynamically create an Equals 'utility' method.
406403
* The CecilSymbolHelper will create branch points with a start line of -1 and no document, which
407404
* I am currently not sure how to handle.
408405
*/
409-
if (_branchTarget.StartLine == -1 || _branchTarget.Document == null)
406+
if (branchTarget.StartLine == -1 || branchTarget.Document == null)
410407
continue;
411408

412-
var target = AddInstrumentationCode(method, processor, instruction, _branchTarget);
409+
var target = AddInstrumentationCode(method, processor, instruction, branchTarget);
413410
foreach (var _instruction in processor.Body.Instructions)
414411
ReplaceInstructionTarget(_instruction, instruction, target);
415412

@@ -469,43 +466,13 @@ private Instruction AddInstrumentationCode(MethodDefinition method, ILProcessor
469466
Ordinal = branchPoint.Ordinal
470467
}
471468
);
472-
473-
if (IsAsyncStateMachineBranch(method.DeclaringType, method))
474-
{
475-
if (_asyncMachineStateMethod == null)
476-
{
477-
_asyncMachineStateMethod = new List<string>();
478-
}
479-
480-
if (!_asyncMachineStateMethod.Contains(method.FullName))
481-
{
482-
_asyncMachineStateMethod.Add(method.FullName);
483-
}
484-
}
485469
}
486470

487471
_result.HitCandidates.Add(new HitCandidate(true, document.Index, branchPoint.StartLine, (int)branchPoint.Ordinal));
488472

489473
return AddInstrumentationInstructions(method, processor, instruction, _result.HitCandidates.Count - 1);
490474
}
491475

492-
private bool IsAsyncStateMachineBranch(TypeDefinition typeDef, MethodDefinition method)
493-
{
494-
if (!method.FullName.EndsWith("::MoveNext()"))
495-
{
496-
return false;
497-
}
498-
499-
foreach (InterfaceImplementation implementedInterface in typeDef.Interfaces)
500-
{
501-
if (implementedInterface.InterfaceType.FullName == "System.Runtime.CompilerServices.IAsyncStateMachine")
502-
{
503-
return true;
504-
}
505-
}
506-
return false;
507-
}
508-
509476
private Instruction AddInstrumentationInstructions(MethodDefinition method, ILProcessor processor, Instruction instruction, int hitEntryIndex)
510477
{
511478
if (_customTrackerRecordHitMethod == null)

src/coverlet.core/Instrumentation/InstrumenterResult.cs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,8 +100,6 @@ public InstrumenterResult()
100100
[DataMember]
101101
public string Module;
102102
[DataMember]
103-
public string[] AsyncMachineStateMethod;
104-
[DataMember]
105103
public string HitsFilePath;
106104
[DataMember]
107105
public string ModulePath;

src/coverlet.core/Symbols/BranchPoint.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
using System;
2+
using System.Diagnostics;
23
using System.Text.RegularExpressions;
34

45
namespace Coverlet.Core.Symbols
56
{
67
/// <summary>
78
/// a branch point
89
/// </summary>
10+
[DebuggerDisplay("StartLine = {StartLine}")]
911
public class BranchPoint
1012
{
1113
/// <summary>

src/coverlet.core/Symbols/CecilSymbolHelper.cs

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System;
66
using System.Collections.Generic;
77
using System.Linq;
8+
using System.Runtime.CompilerServices;
89
using System.Text.RegularExpressions;
910

1011
using Coverlet.Core.Extensions;
@@ -18,7 +19,47 @@ namespace Coverlet.Core.Symbols
1819
public static class CecilSymbolHelper
1920
{
2021
private const int StepOverLineCode = 0xFEEFEE;
21-
private static readonly Regex IsMovenext = new Regex(@"\<[^\s>]+\>\w__\w(\w)?::MoveNext\(\)$", RegexOptions.Compiled | RegexOptions.ExplicitCapture);
22+
23+
private static bool IsMoveNextInsideAsyncStateMachine(MethodDefinition methodDefinition)
24+
{
25+
if (!methodDefinition.FullName.EndsWith("::MoveNext()"))
26+
{
27+
return false;
28+
}
29+
30+
if (methodDefinition.DeclaringType.CustomAttributes.Count(ca => ca.AttributeType.FullName == typeof(CompilerGeneratedAttribute).FullName) > 0)
31+
{
32+
foreach (InterfaceImplementation implementedInterface in methodDefinition.DeclaringType.Interfaces)
33+
{
34+
if (implementedInterface.InterfaceType.FullName == "System.Runtime.CompilerServices.IAsyncStateMachine")
35+
{
36+
return true;
37+
}
38+
}
39+
}
40+
41+
return false;
42+
}
43+
44+
private static bool IsMoveNextInsideEnumerator(MethodDefinition methodDefinition)
45+
{
46+
if (!methodDefinition.FullName.EndsWith("::MoveNext()"))
47+
{
48+
return false;
49+
}
50+
if (methodDefinition.DeclaringType.CustomAttributes.Count(ca => ca.AttributeType.FullName == typeof(CompilerGeneratedAttribute).FullName) > 0)
51+
{
52+
foreach (InterfaceImplementation implementedInterface in methodDefinition.DeclaringType.Interfaces)
53+
{
54+
if (implementedInterface.InterfaceType.FullName == "System.Collections.IEnumerator")
55+
{
56+
return true;
57+
}
58+
}
59+
}
60+
61+
return false;
62+
}
2263

2364
public static List<BranchPoint> GetBranchPoints(MethodDefinition methodDefinition)
2465
{
@@ -30,9 +71,10 @@ public static List<BranchPoint> GetBranchPoints(MethodDefinition methodDefinitio
3071
var instructions = methodDefinition.Body.Instructions;
3172

3273
// if method is a generated MoveNext skip first branch (could be a switch or a branch)
33-
var skipFirstBranch = IsMovenext.IsMatch(methodDefinition.FullName);
74+
bool isAsyncStateMachineMoveNext = IsMoveNextInsideAsyncStateMachine(methodDefinition);
75+
bool skipFirstBranch = isAsyncStateMachineMoveNext || IsMoveNextInsideEnumerator(methodDefinition);
3476

35-
foreach (var instruction in instructions.Where(instruction => instruction.OpCode.FlowControl == FlowControl.Cond_Branch))
77+
foreach (Instruction instruction in instructions.Where(instruction => instruction.OpCode.FlowControl == FlowControl.Cond_Branch))
3678
{
3779
try
3880
{
@@ -42,6 +84,15 @@ public static List<BranchPoint> GetBranchPoints(MethodDefinition methodDefinitio
4284
continue;
4385
}
4486

87+
// Skip get_IsCompleted to avoid unuseful branch due to async/await state machine
88+
if (isAsyncStateMachineMoveNext && instruction.Previous.Operand is MethodReference operand &&
89+
operand.Name == "get_IsCompleted" &&
90+
operand.DeclaringType.FullName.StartsWith("System.Runtime.CompilerServices.TaskAwaiter") &&
91+
operand.DeclaringType.Scope.Name == "System.Runtime")
92+
{
93+
continue;
94+
}
95+
4596
if (BranchIsInGeneratedExceptionFilter(instruction, methodDefinition))
4697
continue;
4798

test/coverlet.core.tests/CoverageTests.cs

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,5 +138,59 @@ public void SelectionStatements_Switch()
138138
File.Delete(path);
139139
}
140140
}
141+
142+
[Fact]
143+
public void AsyncAwait()
144+
{
145+
string path = Path.GetTempFileName();
146+
try
147+
{
148+
RemoteExecutor.Invoke(async pathSerialize =>
149+
{
150+
CoveragePrepareResult coveragePrepareResult = await TestInstrumentationHelper.Run<AsyncAwait>(instance =>
151+
{
152+
instance.SyncExecution();
153+
154+
int res = ((Task<int>)instance.AsyncExecution(true)).ConfigureAwait(false).GetAwaiter().GetResult();
155+
res = ((Task<int>)instance.AsyncExecution(1)).ConfigureAwait(false).GetAwaiter().GetResult();
156+
res = ((Task<int>)instance.AsyncExecution(2)).ConfigureAwait(false).GetAwaiter().GetResult();
157+
res = ((Task<int>)instance.AsyncExecution(3)).ConfigureAwait(false).GetAwaiter().GetResult();
158+
res = ((Task<int>)instance.ContinuationCalled()).ConfigureAwait(false).GetAwaiter().GetResult();
159+
160+
return Task.CompletedTask;
161+
}, pathSerialize);
162+
return 0;
163+
}, path).Dispose();
164+
165+
CoverageResult result = TestInstrumentationHelper.GetCoverageResult(path);
166+
result.Document("Instrumentation.AsyncAwait.cs")
167+
.AssertLinesCovered(BuildConfiguration.Debug,
168+
// AsyncExecution(bool)
169+
(10, 1), (11, 1), (12, 1), (14, 1), (16, 1), (17, 0), (18, 0), (19, 0), (21, 1), (22, 1),
170+
// Async
171+
(25, 9), (26, 9), (27, 9), (28, 9),
172+
// SyncExecution
173+
(31, 1), (32, 1), (33, 1),
174+
// Sync
175+
(36, 1), (37, 1), (38, 1),
176+
// AsyncExecution(int)
177+
(41, 3), (42, 3), (43, 3), (46, 1), (47, 1), (48, 1), (51, 1),
178+
(52, 1), (53, 1), (56, 1), (57, 1), (58, 1), (59, 1),
179+
(62, 0), (63, 0), (64, 0), (65, 0), (68, 0), (70, 3), (71, 3),
180+
// ContinuationNotCalled
181+
(74, 0), (75, 0), (76, 0), (77, 0), (78, 0),
182+
// ContinuationCalled -> line 83 should be 1 hit some issue with Continuation state machine
183+
(81, 1), (82, 1), (83, 2), (84, 1), (85, 1)
184+
)
185+
.AssertBranchesCovered(BuildConfiguration.Debug, (16, 0, 0), (16, 1, 1), (43, 0, 3), (43, 1, 1), (43, 2, 1), (43, 3, 1), (43, 4, 0))
186+
// Real branch should be 2, we should try to remove compiler generated branch in method ContinuationNotCalled/ContinuationCalled
187+
// for Continuation state machine
188+
.ExpectedTotalNumberOfBranches(BuildConfiguration.Debug, 4);
189+
}
190+
finally
191+
{
192+
File.Delete(path);
193+
}
194+
}
141195
}
142196
}

test/coverlet.core.tests/Instrumentation/InstrumenterResultTests.cs

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@ public void CoveragePrepareResult_SerializationRoundTrip()
3737
ir.Module = "Module";
3838
ir.ModulePath = "ModulePath";
3939
ir.SourceLink = "SourceLink";
40-
ir.AsyncMachineStateMethod = new string[] { "A", "B" };
4140

4241
ir.HitCandidates.Add(new HitCandidate(true, 1, 2, 3));
4342
ir.HitCandidates.Add(new HitCandidate(false, 4, 5, 6));
@@ -110,11 +109,6 @@ public void CoveragePrepareResult_SerializationRoundTrip()
110109
Assert.Equal(cpr.Results[i].ModulePath, roundTrip.Results[i].ModulePath);
111110
Assert.Equal(cpr.Results[i].SourceLink, roundTrip.Results[i].SourceLink);
112111

113-
for (int k = 0; k < cpr.Results[i].AsyncMachineStateMethod.Length; k++)
114-
{
115-
Assert.Equal(cpr.Results[i].AsyncMachineStateMethod[k], roundTrip.Results[i].AsyncMachineStateMethod[k]);
116-
}
117-
118112
for (int k = 0; k < cpr.Results[i].HitCandidates.Count; k++)
119113
{
120114
Assert.Equal(cpr.Results[i].HitCandidates[k].start, roundTrip.Results[i].HitCandidates[k].start);

0 commit comments

Comments
 (0)