Skip to content

Commit b5923ca

Browse files
Coverage for "await foreach" loops and compiler-generated async iterators (issue #1104) (#1107)
Coverage for "await foreach" loops and compiler-generated async iterators (issue #1104) (#1107)
1 parent 5de0ad7 commit b5923ca

8 files changed

+616
-7
lines changed

src/coverlet.core/Symbols/CecilSymbolHelper.cs

+288-5
Large diffs are not rendered by default.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
using System.Collections.Generic;
2+
using System.IO;
3+
using System.Linq;
4+
using System.Threading.Tasks;
5+
6+
using Coverlet.Core.Samples.Tests;
7+
using Coverlet.Tests.Xunit.Extensions;
8+
using Xunit;
9+
10+
namespace Coverlet.Core.Tests
11+
{
12+
public partial class CoverageTests
13+
{
14+
[Fact]
15+
public void AsyncForeach()
16+
{
17+
string path = Path.GetTempFileName();
18+
try
19+
{
20+
FunctionExecutor.Run(async (string[] pathSerialize) =>
21+
{
22+
CoveragePrepareResult coveragePrepareResult = await TestInstrumentationHelper.Run<AsyncForeach>(instance =>
23+
{
24+
int res = ((ValueTask<int>)instance.SumWithATwist(AsyncEnumerable.Range(1, 5))).GetAwaiter().GetResult();
25+
res += ((ValueTask<int>)instance.Sum(AsyncEnumerable.Range(1, 3))).GetAwaiter().GetResult();
26+
res += ((ValueTask<int>)instance.SumEmpty()).GetAwaiter().GetResult();
27+
28+
return Task.CompletedTask;
29+
}, persistPrepareResultToFile: pathSerialize[0]);
30+
return 0;
31+
}, new string[] { path });
32+
33+
TestInstrumentationHelper.GetCoverageResult(path)
34+
.Document("Instrumentation.AsyncForeach.cs")
35+
.AssertLinesCovered(BuildConfiguration.Debug,
36+
// SumWithATwist(IAsyncEnumerable<int>)
37+
// Apparently due to entering and exiting the async state machine, line 17
38+
// (the top of an "await foreach" loop) is reached three times *plus* twice
39+
// per loop iteration. So, in this case, with five loop iterations, we end
40+
// up with 3 + 5 * 2 = 13 hits.
41+
(14, 1), (15, 1), (17, 13), (18, 5), (19, 5), (20, 5), (21, 5), (22, 5),
42+
(24, 0), (25, 0), (26, 0), (27, 5), (29, 1), (30, 1),
43+
// Sum(IAsyncEnumerable<int>)
44+
(34, 1), (35, 1), (37, 9), (38, 3), (39, 3), (40, 3), (42, 1), (43, 1),
45+
// SumEmpty()
46+
(47, 1), (48, 1), (50, 3), (51, 0), (52, 0), (53, 0), (55, 1), (56, 1)
47+
)
48+
.AssertBranchesCovered(BuildConfiguration.Debug,
49+
// SumWithATwist(IAsyncEnumerable<int>)
50+
(17, 2, 1), (17, 3, 5), (19, 0, 5), (19, 1, 0),
51+
// Sum(IAsyncEnumerable<int>)
52+
(37, 0, 1), (37, 1, 3),
53+
// SumEmpty()
54+
// If we never entered the loop, that's a branch not taken, which is
55+
// what we want to see.
56+
(50, 0, 1), (50, 1, 0)
57+
)
58+
.ExpectedTotalNumberOfBranches(BuildConfiguration.Debug, 4);
59+
}
60+
finally
61+
{
62+
File.Delete(path);
63+
}
64+
}
65+
}
66+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
using System.Collections.Generic;
2+
using System.IO;
3+
using System.Threading.Tasks;
4+
5+
using Coverlet.Core.Samples.Tests;
6+
using Coverlet.Tests.Xunit.Extensions;
7+
using Xunit;
8+
9+
namespace Coverlet.Core.Tests
10+
{
11+
public partial class CoverageTests
12+
{
13+
[Fact]
14+
public void AsyncIterator()
15+
{
16+
string path = Path.GetTempFileName();
17+
try
18+
{
19+
FunctionExecutor.Run(async (string[] pathSerialize) =>
20+
{
21+
CoveragePrepareResult coveragePrepareResult = await TestInstrumentationHelper.Run<AsyncIterator>(instance =>
22+
{
23+
int res = ((Task<int>)instance.Issue1104_Repro()).GetAwaiter().GetResult();
24+
25+
return Task.CompletedTask;
26+
}, persistPrepareResultToFile: pathSerialize[0]);
27+
return 0;
28+
}, new string[] { path });
29+
30+
TestInstrumentationHelper.GetCoverageResult(path)
31+
.Document("Instrumentation.AsyncIterator.cs")
32+
.AssertLinesCovered(BuildConfiguration.Debug,
33+
// Issue1104_Repro()
34+
(14, 1), (15, 1), (17, 203), (18, 100), (19, 100), (20, 100), (22, 1), (23, 1),
35+
// CreateSequenceAsync()
36+
(26, 1), (27, 202), (28, 100), (29, 100), (30, 100), (31, 100), (32, 1)
37+
)
38+
.AssertBranchesCovered(BuildConfiguration.Debug,
39+
// Issue1104_Repro(),
40+
(17, 0, 1), (17, 1, 100),
41+
// CreateSequenceAsync()
42+
(27, 0, 1), (27, 1, 100)
43+
)
44+
.ExpectedTotalNumberOfBranches(BuildConfiguration.Debug, 2);
45+
}
46+
finally
47+
{
48+
File.Delete(path);
49+
}
50+
}
51+
}
52+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
// Remember to use full name because adding new using directives change line numbers
2+
3+
using System;
4+
using System.Collections.Generic;
5+
using System.Linq;
6+
using System.Threading;
7+
using System.Threading.Tasks;
8+
9+
namespace Coverlet.Core.Samples.Tests
10+
{
11+
public class AsyncForeach
12+
{
13+
async public ValueTask<int> SumWithATwist(IAsyncEnumerable<int> ints)
14+
{
15+
int sum = 0;
16+
17+
await foreach (int i in ints)
18+
{
19+
if (i > 0)
20+
{
21+
sum += i;
22+
}
23+
else
24+
{
25+
sum = 0;
26+
}
27+
}
28+
29+
return sum;
30+
}
31+
32+
33+
async public ValueTask<int> Sum(IAsyncEnumerable<int> ints)
34+
{
35+
int sum = 0;
36+
37+
await foreach (int i in ints)
38+
{
39+
sum += i;
40+
}
41+
42+
return sum;
43+
}
44+
45+
46+
async public ValueTask<int> SumEmpty()
47+
{
48+
int sum = 0;
49+
50+
await foreach (int i in AsyncEnumerable.Empty<int>())
51+
{
52+
sum += i;
53+
}
54+
55+
return sum;
56+
}
57+
}
58+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// Remember to use full name because adding new using directives change line numbers
2+
3+
using System;
4+
using System.Collections.Generic;
5+
using System.Linq;
6+
using System.Threading;
7+
using System.Threading.Tasks;
8+
9+
namespace Coverlet.Core.Samples.Tests
10+
{
11+
public class AsyncIterator
12+
{
13+
async public Task<int> Issue1104_Repro()
14+
{
15+
int sum = 0;
16+
17+
await foreach (int result in CreateSequenceAsync())
18+
{
19+
sum += result;
20+
}
21+
22+
return sum;
23+
}
24+
25+
async private IAsyncEnumerable<int> CreateSequenceAsync()
26+
{
27+
for (int i = 0; i < 100; ++i)
28+
{
29+
await Task.CompletedTask;
30+
yield return i;
31+
}
32+
}
33+
}
34+
}

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

+45
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,51 @@ async public ValueTask AsyncAwait()
197197
}
198198
}
199199

200+
public class AwaitForeachStateMachine
201+
{
202+
async public ValueTask AsyncAwait(IAsyncEnumerable<int> ints)
203+
{
204+
await foreach (int i in ints)
205+
{
206+
await default(ValueTask);
207+
}
208+
}
209+
}
210+
211+
public class AwaitForeachStateMachine_WithBranches
212+
{
213+
async public ValueTask<int> SumWithATwist(IAsyncEnumerable<int> ints)
214+
{
215+
int sum = 0;
216+
217+
await foreach (int i in ints)
218+
{
219+
if (i > 0)
220+
{
221+
sum += i;
222+
}
223+
else
224+
{
225+
sum = 0;
226+
}
227+
}
228+
229+
return sum;
230+
}
231+
}
232+
233+
public class AsyncIteratorStateMachine
234+
{
235+
async public IAsyncEnumerable<int> CreateSequenceAsync()
236+
{
237+
for (int i = 0; i < 100; ++i)
238+
{
239+
await Task.CompletedTask;
240+
yield return i;
241+
}
242+
}
243+
}
244+
200245
[ExcludeFromCoverage]
201246
public class ClassExcludedByCoverletCodeCoverageAttr
202247
{

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

+69
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,75 @@ public void GetBranchPoints_IgnoresBranchesIn_AsyncAwaitValueTaskStateMachine()
310310
Assert.Empty(points);
311311
}
312312

313+
[Fact]
314+
public void GetBranchPoints_IgnoresMostBranchesIn_AwaitForeachStateMachine()
315+
{
316+
// arrange
317+
var nestedName = typeof(AwaitForeachStateMachine).GetNestedTypes(BindingFlags.NonPublic).First().Name;
318+
var type = _module.Types.FirstOrDefault(x => x.FullName == typeof(AwaitForeachStateMachine).FullName);
319+
var nestedType = type.NestedTypes.FirstOrDefault(x => x.FullName.EndsWith(nestedName));
320+
var method = nestedType.Methods.First(x => x.FullName.EndsWith("::MoveNext()"));
321+
322+
// act
323+
var points = _cecilSymbolHelper.GetBranchPoints(method);
324+
325+
// assert
326+
// We do expect there to be a two-way branch (stay in the loop or not?) on
327+
// the line containing "await foreach".
328+
Assert.NotNull(points);
329+
Assert.Equal(2, points.Count());
330+
Assert.Equal(points[0].Offset, points[1].Offset);
331+
Assert.Equal(204, points[0].StartLine);
332+
Assert.Equal(204, points[1].StartLine);
333+
}
334+
335+
[Fact]
336+
public void GetBranchPoints_IgnoresMostBranchesIn_AwaitForeachStateMachine_WithBranchesWithinIt()
337+
{
338+
// arrange
339+
var nestedName = typeof(AwaitForeachStateMachine_WithBranches).GetNestedTypes(BindingFlags.NonPublic).First().Name;
340+
var type = _module.Types.FirstOrDefault(x => x.FullName == typeof(AwaitForeachStateMachine_WithBranches).FullName);
341+
var nestedType = type.NestedTypes.FirstOrDefault(x => x.FullName.EndsWith(nestedName));
342+
var method = nestedType.Methods.First(x => x.FullName.EndsWith("::MoveNext()"));
343+
344+
// act
345+
var points = _cecilSymbolHelper.GetBranchPoints(method);
346+
347+
// assert
348+
// We do expect there to be four branch points (two places where we can branch
349+
// two ways), one being the "stay in the loop or not?" branch on the line
350+
// containing "await foreach" and the other being the "if" statement inside
351+
// the loop.
352+
Assert.NotNull(points);
353+
Assert.Equal(4, points.Count());
354+
Assert.Equal(points[0].Offset, points[1].Offset);
355+
Assert.Equal(points[2].Offset, points[3].Offset);
356+
Assert.Equal(219, points[0].StartLine);
357+
Assert.Equal(219, points[1].StartLine);
358+
Assert.Equal(217, points[2].StartLine);
359+
Assert.Equal(217, points[3].StartLine);
360+
}
361+
362+
[Fact]
363+
public void GetBranchesPoints_IgnoresExtraBranchesIn_AsyncIteratorStateMachine()
364+
{
365+
// arrange
366+
var nestedName = typeof(AsyncIteratorStateMachine).GetNestedTypes(BindingFlags.NonPublic).First().Name;
367+
var type = _module.Types.FirstOrDefault(x => x.FullName == typeof(AsyncIteratorStateMachine).FullName);
368+
var nestedType = type.NestedTypes.FirstOrDefault(x => x.FullName.EndsWith(nestedName));
369+
var method = nestedType.Methods.First(x => x.FullName.EndsWith("::MoveNext()"));
370+
371+
// act
372+
var points = _cecilSymbolHelper.GetBranchPoints(method);
373+
374+
// assert
375+
// We do expect the "for" loop to be a branch with two branch points, but that's it.
376+
Assert.NotNull(points);
377+
Assert.Equal(2, points.Count());
378+
Assert.Equal(237, points[0].StartLine);
379+
Assert.Equal(237, points[1].StartLine);
380+
}
381+
313382
[Fact]
314383
public void GetBranchPoints_ExceptionFilter()
315384
{

test/coverlet.core.tests/coverlet.core.tests.csproj

+4-2
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,11 @@
3030

3131
<ItemGroup>
3232
<!--For test TestInstrument_NetstandardAwareAssemblyResolver_PreserveCompilationContext-->
33-
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="2.2.0" />
33+
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="5.0.0" />
3434
<!--For test issue 809 https://github.com/coverlet-coverage/coverlet/issues/809-->
35-
<PackageReference Include="LinqKit.Microsoft.EntityFrameworkCore" Version="2.0.0" />
35+
<PackageReference Include="LinqKit.Microsoft.EntityFrameworkCore" Version="5.0.23" />
36+
<!--To test issue 1104 https://github.com/coverlet-coverage/coverlet/issues/1104-->
37+
<PackageReference Include="System.Linq.Async" Version="5.0.0" />
3638
</ItemGroup>
3739

3840
<ItemGroup>

0 commit comments

Comments
 (0)