Skip to content

Commit 3264ad3

Browse files
authored
Skip branches in generated MoveNext() for singleton iterators (#813)
Skip branches in generated `MoveNext()` for singleton iterators
1 parent 570cfc9 commit 3264ad3

File tree

6 files changed

+294
-0
lines changed

6 files changed

+294
-0
lines changed

src/coverlet.core/Symbols/CecilSymbolHelper.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -412,7 +412,13 @@ public static List<BranchPoint> GetBranchPoints(MethodDefinition methodDefinitio
412412

413413
bool isAsyncStateMachineMoveNext = IsMoveNextInsideAsyncStateMachine(methodDefinition);
414414
bool isMoveNextInsideAsyncStateMachineProlog = isAsyncStateMachineMoveNext && IsMoveNextInsideAsyncStateMachineProlog(methodDefinition);
415+
416+
// State machine for enumerator uses `brfalse.s`/`beq` or `switch` opcode depending on how many `yield` we have in the method body.
417+
// For more than one `yield` a `switch` is emitted so we should only skip the first branch. In case of a single `yield` we need to
418+
// skip the first two branches to avoid reporting a phantom branch. The first branch (`brfalse.s`) jumps to the `yield`ed value,
419+
// the second one (`beq`) exits the enumeration.
415420
bool skipFirstBranch = IsMoveNextInsideEnumerator(methodDefinition);
421+
bool skipSecondBranch = false;
416422

417423
foreach (Instruction instruction in instructions.Where(instruction => instruction.OpCode.FlowControl == FlowControl.Cond_Branch))
418424
{
@@ -421,6 +427,13 @@ public static List<BranchPoint> GetBranchPoints(MethodDefinition methodDefinitio
421427
if (skipFirstBranch)
422428
{
423429
skipFirstBranch = false;
430+
skipSecondBranch = instruction.OpCode.Code != Code.Switch;
431+
continue;
432+
}
433+
434+
if (skipSecondBranch)
435+
{
436+
skipSecondBranch = false;
424437
continue;
425438
}
426439

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
using System.IO;
2+
using System.Threading.Tasks;
3+
4+
using Coverlet.Core.Samples.Tests;
5+
using Tmds.Utils;
6+
using Xunit;
7+
8+
namespace Coverlet.Core.Tests
9+
{
10+
public partial class CoverageTests : ExternalProcessExecutionTest
11+
{
12+
[Fact]
13+
public void Yield_Single()
14+
{
15+
string path = Path.GetTempFileName();
16+
try
17+
{
18+
FunctionExecutor.Run(async (string[] pathSerialize) =>
19+
{
20+
CoveragePrepareResult coveragePrepareResult = await TestInstrumentationHelper.Run<Yield>(instance =>
21+
{
22+
foreach (var _ in instance.One()) ;
23+
24+
return Task.CompletedTask;
25+
}, persistPrepareResultToFile: pathSerialize[0]);
26+
27+
return 0;
28+
}, new string[] { path });
29+
30+
CoverageResult result = TestInstrumentationHelper.GetCoverageResult(path);
31+
32+
result.Document("Instrumentation.Yield.cs")
33+
.Method("System.Boolean Coverlet.Core.Samples.Tests.Yield/<One>d__0::MoveNext()")
34+
.AssertLinesCovered((9, 1))
35+
.ExpectedTotalNumberOfBranches(0);
36+
}
37+
finally
38+
{
39+
File.Delete(path);
40+
}
41+
}
42+
43+
[Fact]
44+
public void Yield_Two()
45+
{
46+
string path = Path.GetTempFileName();
47+
try
48+
{
49+
FunctionExecutor.Run(async (string[] pathSerialize) =>
50+
{
51+
CoveragePrepareResult coveragePrepareResult = await TestInstrumentationHelper.Run<Yield>(instance =>
52+
{
53+
foreach (var _ in instance.Two()) ;
54+
55+
return Task.CompletedTask;
56+
}, persistPrepareResultToFile: pathSerialize[0]);
57+
return 0;
58+
}, new string[] { path });
59+
60+
CoverageResult result = TestInstrumentationHelper.GetCoverageResult(path);
61+
62+
result.Document("Instrumentation.Yield.cs")
63+
.Method("System.Boolean Coverlet.Core.Samples.Tests.Yield/<Two>d__1::MoveNext()")
64+
.AssertLinesCovered((14, 1), (15, 1))
65+
.ExpectedTotalNumberOfBranches(0);
66+
}
67+
finally
68+
{
69+
File.Delete(path);
70+
}
71+
}
72+
73+
[Fact]
74+
public void Yield_SingleWithSwitch()
75+
{
76+
string path = Path.GetTempFileName();
77+
try
78+
{
79+
FunctionExecutor.Run(async (string[] pathSerialize) =>
80+
{
81+
CoveragePrepareResult coveragePrepareResult = await TestInstrumentationHelper.Run<Yield>(instance =>
82+
{
83+
foreach (var _ in instance.OneWithSwitch(2)) ;
84+
85+
return Task.CompletedTask;
86+
}, persistPrepareResultToFile: pathSerialize[0]);
87+
88+
return 0;
89+
}, new string[] { path });
90+
91+
CoverageResult result = TestInstrumentationHelper.GetCoverageResult(path);
92+
93+
result.Document("Instrumentation.Yield.cs")
94+
.Method("System.Boolean Coverlet.Core.Samples.Tests.Yield/<OneWithSwitch>d__2::MoveNext()")
95+
.AssertLinesCovered(BuildConfiguration.Debug, (21, 1), (30, 1), (31, 1), (37, 1))
96+
.ExpectedTotalNumberOfBranches(1);
97+
}
98+
finally
99+
{
100+
File.Delete(path);
101+
}
102+
}
103+
104+
[Fact]
105+
public void Yield_Three()
106+
{
107+
string path = Path.GetTempFileName();
108+
try
109+
{
110+
FunctionExecutor.Run(async (string[] pathSerialize) =>
111+
{
112+
CoveragePrepareResult coveragePrepareResult = await TestInstrumentationHelper.Run<Yield>(instance =>
113+
{
114+
foreach (var _ in instance.Three()) ;
115+
116+
return Task.CompletedTask;
117+
}, persistPrepareResultToFile: pathSerialize[0]);
118+
return 0;
119+
}, new string[] { path });
120+
121+
CoverageResult result = TestInstrumentationHelper.GetCoverageResult(path);
122+
123+
result.Document("Instrumentation.Yield.cs")
124+
.Method("System.Boolean Coverlet.Core.Samples.Tests.Yield/<Three>d__3::MoveNext()")
125+
.AssertLinesCovered((42, 1), (43, 1), (44, 1))
126+
.ExpectedTotalNumberOfBranches(0);
127+
}
128+
finally
129+
{
130+
File.Delete(path);
131+
}
132+
}
133+
134+
[Fact]
135+
public void Yield_Enumerable()
136+
{
137+
string path = Path.GetTempFileName();
138+
try
139+
{
140+
FunctionExecutor.Run(async (string[] pathSerialize) =>
141+
{
142+
CoveragePrepareResult coveragePrepareResult = await TestInstrumentationHelper.Run<Yield>(instance =>
143+
{
144+
foreach (var _ in instance.Enumerable(new[] { "one", "two", "three", "four" })) ;
145+
146+
return Task.CompletedTask;
147+
}, persistPrepareResultToFile: pathSerialize[0]);
148+
return 0;
149+
}, new string[] { path });
150+
151+
CoverageResult result = TestInstrumentationHelper.GetCoverageResult(path);
152+
153+
result.Document("Instrumentation.Yield.cs")
154+
.Method("System.Boolean Coverlet.Core.Samples.Tests.Yield/<Enumerable>d__4::MoveNext()")
155+
.AssertLinesCovered(BuildConfiguration.Debug, (48, 1), (49, 1), (50, 4), (51, 5), (52, 1), (54, 4), (55, 4), (56, 4), (57, 1))
156+
.ExpectedTotalNumberOfBranches(1);
157+
}
158+
finally
159+
{
160+
File.Delete(path);
161+
}
162+
}
163+
}
164+
}

test/coverlet.core.tests/Coverage/InstrumenterHelper.Assertions.cs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,11 +79,46 @@ public static Document Document(this CoverageResult coverageResult, string docNa
7979
throw new XunitException($"Document not found '{docName}'");
8080
}
8181

82+
public static Document Method(this Document document, string methodName)
83+
{
84+
var methodDoc = new Document { Path = document.Path, Index = document.Index };
85+
86+
if (!document.Lines.Any() && !document.Branches.Any())
87+
{
88+
return methodDoc;
89+
}
90+
91+
if (document.Lines.Values.All(l => l.Method != methodName) && document.Branches.Values.All(l => l.Method != methodName))
92+
{
93+
var methods = document.Lines.Values.Select(l => $"'{l.Method}'")
94+
.Concat(document.Branches.Values.Select(b => $"'{b.Method}'"))
95+
.Distinct();
96+
throw new XunitException($"Method '{methodName}' not found. Methods in document: {string.Join(", ", methods)}");
97+
}
98+
99+
foreach (var line in document.Lines.Where(l => l.Value.Method == methodName))
100+
{
101+
methodDoc.Lines[line.Key] = line.Value;
102+
}
103+
104+
foreach (var branch in document.Branches.Where(b => b.Value.Method == methodName))
105+
{
106+
methodDoc.Branches[branch.Key] = branch.Value;
107+
}
108+
109+
return methodDoc;
110+
}
111+
82112
public static Document AssertBranchesCovered(this Document document, params (int line, int ordinal, int hits)[] lines)
83113
{
84114
return AssertBranchesCovered(document, BuildConfiguration.Debug | BuildConfiguration.Release, lines);
85115
}
86116

117+
public static Document ExpectedTotalNumberOfBranches(this Document document, int totalExpectedBranch)
118+
{
119+
return ExpectedTotalNumberOfBranches(document, BuildConfiguration.Debug | BuildConfiguration.Release, totalExpectedBranch);
120+
}
121+
87122
public static Document ExpectedTotalNumberOfBranches(this Document document, BuildConfiguration configuration, int totalExpectedBranch)
88123
{
89124
if (document is null)
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
// Remember to use full name because adding new using directives change line numbers
2+
3+
namespace Coverlet.Core.Samples.Tests
4+
{
5+
public class Yield
6+
{
7+
public System.Collections.Generic.IEnumerable<int> One()
8+
{
9+
yield return 1;
10+
}
11+
12+
public System.Collections.Generic.IEnumerable<int> Two()
13+
{
14+
yield return 1;
15+
yield return 2;
16+
}
17+
18+
public System.Collections.Generic.IEnumerable<int> OneWithSwitch(int n)
19+
{
20+
int result;
21+
switch (n)
22+
{
23+
case 0:
24+
result = 10;
25+
break;
26+
case 1:
27+
result = 11;
28+
break;
29+
case 2:
30+
result = 12;
31+
break;
32+
default:
33+
result = -1;
34+
break;
35+
}
36+
37+
yield return result;
38+
}
39+
40+
public System.Collections.Generic.IEnumerable<int> Three()
41+
{
42+
yield return 1;
43+
yield return 2;
44+
yield return 3;
45+
}
46+
47+
public System.Collections.Generic.IEnumerable<string> Enumerable(System.Collections.Generic.IList<string> ls)
48+
{
49+
foreach (
50+
var item
51+
in
52+
ls
53+
)
54+
{
55+
yield return item;
56+
}
57+
}
58+
}
59+
}

test/coverlet.core.tests/Samples/Samples.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,14 @@ public IEnumerable<string> Fetch()
172172
}
173173
}
174174

175+
public class SingletonIterator
176+
{
177+
public IEnumerable<string> Fetch()
178+
{
179+
yield return "one";
180+
}
181+
}
182+
175183
public class AsyncAwaitStateMachine
176184
{
177185
async public Task AsyncAwait()

test/coverlet.core.tests/Symbols/CecilSymbolHelperTests.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,7 +259,22 @@ public void GetBranchPoints_IgnoresSwitchIn_GeneratedMoveNext()
259259

260260
// assert
261261
Assert.Empty(points);
262+
}
263+
264+
[Fact]
265+
public void GetBranchPoints_IgnoresBranchesIn_GeneratedMoveNextForSingletonIterator()
266+
{
267+
// arrange
268+
var nestedName = typeof(SingletonIterator).GetNestedTypes(BindingFlags.NonPublic).First().Name;
269+
var type = _module.Types.FirstOrDefault(x => x.FullName == typeof(SingletonIterator).FullName);
270+
var nestedType = type.NestedTypes.FirstOrDefault(x => x.FullName.EndsWith(nestedName));
271+
var method = nestedType.Methods.First(x => x.FullName.EndsWith("::MoveNext()"));
272+
273+
// act
274+
var points = CecilSymbolHelper.GetBranchPoints(method);
262275

276+
// assert
277+
Assert.Empty(points);
263278
}
264279

265280
[Fact]

0 commit comments

Comments
 (0)