Coverage for "await foreach" loops and compiler-generated async iterators (issue #1104) (#1107)
This commit is contained in:
@@ -61,6 +61,22 @@ namespace Coverlet.Core.Symbols
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool IsMoveNextInsideAsyncIterator(MethodDefinition methodDefinition)
|
||||
{
|
||||
if (methodDefinition.FullName.EndsWith("::MoveNext()") && IsCompilerGenerated(methodDefinition))
|
||||
{
|
||||
foreach (InterfaceImplementation implementedInterface in methodDefinition.DeclaringType.Interfaces)
|
||||
{
|
||||
if (implementedInterface.InterfaceType.FullName.StartsWith("System.Collections.Generic.IAsyncEnumerator`1<"))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool IsMoveNextInsideEnumerator(MethodDefinition methodDefinition)
|
||||
{
|
||||
if (!methodDefinition.FullName.EndsWith("::MoveNext()"))
|
||||
@@ -396,11 +412,267 @@ namespace Coverlet.Core.Symbols
|
||||
return _compilerGeneratedBranchesToExclude[methodDefinition.FullName].Contains(instruction.Offset);
|
||||
}
|
||||
|
||||
private static bool SkipGeneratedBranchesForAwaitForeach(List<Instruction> instructions, Instruction instruction)
|
||||
{
|
||||
// An "await foreach" causes four additional branches to be generated
|
||||
// by the compiler. We want to skip the last three, but we want to
|
||||
// keep the first one.
|
||||
//
|
||||
// (1) At each iteration of the loop, a check that there is another
|
||||
// item in the sequence. This is a branch that we want to keep,
|
||||
// because it's basically "should we stay in the loop or not?",
|
||||
// which is germane to code coverage testing.
|
||||
// (2) A check near the end for whether the IAsyncEnumerator was ever
|
||||
// obtained, so it can be disposed.
|
||||
// (3) A check for whether an exception was thrown in the most recent
|
||||
// loop iteration.
|
||||
// (4) A check for whether the exception thrown in the most recent
|
||||
// loop iteration has (at least) the type System.Exception.
|
||||
//
|
||||
// If we're looking at any of the last three of those four branches,
|
||||
// we should be skipping it.
|
||||
|
||||
int currentIndex = instructions.BinarySearch(instruction, new InstructionByOffsetComparer());
|
||||
|
||||
return SkipGeneratedBranchForAwaitForeach_CheckForAsyncEnumerator(instructions, instruction, currentIndex) ||
|
||||
SkipGeneratedBranchForAwaitForeach_CheckIfExceptionThrown(instructions, instruction, currentIndex) ||
|
||||
SkipGeneratedBranchForAwaitForeach_CheckThrownExceptionType(instructions, instruction, currentIndex);
|
||||
}
|
||||
|
||||
// The pattern for the "should we stay in the loop or not?", which we don't
|
||||
// want to skip (so we have no method to try to find it), looks like this:
|
||||
//
|
||||
// IL_0111: ldloca.s 4
|
||||
// IL_0113: call instance !0 valuetype [System.Private.CoreLib]System.Runtime.CompilerServices.ValueTaskAwaiter`1<bool>::GetResult()
|
||||
// IL_0118: brtrue IL_0047
|
||||
//
|
||||
// In Debug mode, there are additional things that happen in between
|
||||
// the "call" and branch, but it's the same idea either way: branch
|
||||
// if GetResult() returned true.
|
||||
|
||||
private static bool SkipGeneratedBranchForAwaitForeach_CheckForAsyncEnumerator(List<Instruction> instructions, Instruction instruction, int currentIndex)
|
||||
{
|
||||
// We're looking for the following pattern, which checks whether a
|
||||
// compiler-generated field of type IAsyncEnumerator<> is null.
|
||||
//
|
||||
// IL_012b: ldarg.0
|
||||
// IL_012c: ldfld class [System.Private.CoreLib]System.Collections.Generic.IAsyncEnumerator`1<int32> AwaitForeachStateMachine/'<AsyncAwait>d__0'::'<>7__wrap1'
|
||||
// IL_0131: brfalse.s IL_0196
|
||||
|
||||
if (instruction.OpCode != OpCodes.Brfalse &&
|
||||
instruction.OpCode != OpCodes.Brfalse_S)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (currentIndex >= 2 &&
|
||||
(instructions[currentIndex - 2].OpCode == OpCodes.Ldarg ||
|
||||
instructions[currentIndex - 2].OpCode == OpCodes.Ldarg_0) &&
|
||||
instructions[currentIndex - 1].OpCode == OpCodes.Ldfld &&
|
||||
instructions[currentIndex - 1].Operand is FieldDefinition field &&
|
||||
IsCompilerGenerated(field) && field.FieldType.FullName.StartsWith("System.Collections.Generic.IAsyncEnumerator"))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool SkipGeneratedBranchForAwaitForeach_CheckIfExceptionThrown(List<Instruction> instructions, Instruction instruction, int currentIndex)
|
||||
{
|
||||
// Here, we want to find a pattern where we're checking whether a
|
||||
// compiler-generated field of type Object is null. To narrow our
|
||||
// search down and reduce the odds of false positives, we'll also
|
||||
// expect a call to GetResult() to precede the loading of the field's
|
||||
// value. The basic pattern looks like this:
|
||||
//
|
||||
// IL_018f: ldloca.s 2
|
||||
// IL_0191: call instance void [System.Private.CoreLib]System.Runtime.CompilerServices.ValueTaskAwaiter::GetResult()
|
||||
// IL_0196: ldarg.0
|
||||
// IL_0197: ldfld object AwaitForeachStateMachine/'<AsyncAwait>d__0'::'<>7__wrap2'
|
||||
// IL_019c: stloc.s 6
|
||||
// IL_019e: ldloc.s 6
|
||||
// IL_01a0: brfalse.s IL_01b9
|
||||
//
|
||||
// Variants are possible (e.g., a "dup" instruction instead of a
|
||||
// "stloc.s" and "ldloc.s" pair), so we'll just look for the
|
||||
// highlights.
|
||||
|
||||
if (instruction.OpCode != OpCodes.Brfalse &&
|
||||
instruction.OpCode != OpCodes.Brfalse_S)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// We expect the field to be loaded no more than thre instructions before
|
||||
// the branch, so that's how far we're willing to search for it.
|
||||
int minFieldIndex = Math.Max(0, currentIndex - 3);
|
||||
|
||||
for (int i = currentIndex - 1; i >= minFieldIndex; --i)
|
||||
{
|
||||
if (instructions[i].OpCode == OpCodes.Ldfld &&
|
||||
instructions[i].Operand is FieldDefinition field &&
|
||||
IsCompilerGenerated(field) && field.FieldType.FullName == "System.Object")
|
||||
{
|
||||
// We expect the call to GetResult() to be no more than three
|
||||
// instructions before the loading of the field's value.
|
||||
int minCallIndex = Math.Max(0, i - 3);
|
||||
|
||||
for (int j = i - 1; j >= minCallIndex; --j)
|
||||
{
|
||||
if (instructions[j].OpCode == OpCodes.Call &&
|
||||
instructions[j].Operand is MethodReference callRef &&
|
||||
callRef.DeclaringType.FullName.StartsWith("System.Runtime.CompilerServices") &&
|
||||
callRef.DeclaringType.FullName.Contains("TaskAwait") &&
|
||||
callRef.Name == "GetResult")
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool SkipGeneratedBranchForAwaitForeach_CheckThrownExceptionType(List<Instruction> instructions, Instruction instruction, int currentIndex)
|
||||
{
|
||||
// In this case, we're looking for a branch generated by the compiler to
|
||||
// check whether a previously-thrown exception has (at least) the type
|
||||
// System.Exception, the pattern for which looks like this:
|
||||
//
|
||||
// IL_01db: ldloc.s 7
|
||||
// IL_01dd: isinst [System.Private.CoreLib]System.Exception
|
||||
// IL_01e2: stloc.s 9
|
||||
// IL_01e4: ldloc.s 9
|
||||
// IL_01e6: brtrue.s IL_01eb
|
||||
//
|
||||
// Once again, variants are possible here, such as a "dup" instruction in
|
||||
// place of the "stloc.s" and "ldloc.s" pair, and we'll reduce the odds of
|
||||
// a false positive by requiring a "ldloc.s" instruction to precede the
|
||||
// "isinst" instruction.
|
||||
|
||||
if (instruction.OpCode != OpCodes.Brtrue &&
|
||||
instruction.OpCode != OpCodes.Brtrue_S)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
int minTypeCheckIndex = Math.Max(1, currentIndex - 3);
|
||||
|
||||
for (int i = currentIndex - 1; i >= minTypeCheckIndex; --i)
|
||||
{
|
||||
if (instructions[i].OpCode == OpCodes.Isinst &&
|
||||
instructions[i].Operand is TypeReference typeRef &&
|
||||
typeRef.FullName == "System.Exception" &&
|
||||
(instructions[i - 1].OpCode == OpCodes.Ldloc ||
|
||||
instructions[i - 1].OpCode == OpCodes.Ldloc_S))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// https://docs.microsoft.com/en-us/archive/msdn-magazine/2019/november/csharp-iterating-with-async-enumerables-in-csharp-8
|
||||
private static bool SkipGeneratedBranchesForAsyncIterator(List<Instruction> instructions, Instruction instruction)
|
||||
{
|
||||
// There are two branch patterns that we want to eliminate in the
|
||||
// MoveNext() method in compiler-generated async iterators.
|
||||
//
|
||||
// (1) A "switch" instruction near the beginning of MoveNext() that checks
|
||||
// the state machine's current state and jumps to the right place.
|
||||
// (2) A check that the compiler-generated field "<>w__disposeMode" is false,
|
||||
// which is used to know whether the enumerator has been disposed (so it's
|
||||
// necessary not to iterate any further). This is done in more than once
|
||||
// place, but we always want to skip it.
|
||||
|
||||
int currentIndex = instructions.BinarySearch(instruction, new InstructionByOffsetComparer());
|
||||
|
||||
return SkipGeneratedBranchesForAsyncIterator_CheckForStateSwitch(instructions, instruction, currentIndex) ||
|
||||
SkipGeneratedBranchesForAsyncIterator_DisposeCheck(instructions, instruction, currentIndex);
|
||||
}
|
||||
|
||||
private static bool SkipGeneratedBranchesForAsyncIterator_CheckForStateSwitch(List<Instruction> instructions, Instruction instruction, int currentIndex)
|
||||
{
|
||||
// The pattern we're looking for here is this one:
|
||||
//
|
||||
// IL_0000: ldarg.0
|
||||
// IL_0001: ldfld int32 Test.AsyncEnumerableStateMachine/'<CreateSequenceAsync>d__0'::'<>1__state'
|
||||
// IL_0006: stloc.0
|
||||
// .try
|
||||
// {
|
||||
// IL_0007: ldloc.0
|
||||
// IL_0008: ldc.i4.s -4
|
||||
// IL_000a: sub
|
||||
// IL_000b: switch (IL_0026, IL_002b, IL_002f, IL_002f, IL_002d)
|
||||
//
|
||||
// The "switch" instruction is the branch we want to skip. To eliminate
|
||||
// false positives, we'll search back for the "ldfld" of the compiler-
|
||||
// generated "<>1__state" field, making sure it precedes it within five
|
||||
// instructions. To be safe, we'll also require a "ldarg.0" instruction
|
||||
// before the "ldfld".
|
||||
|
||||
if (instruction.OpCode != OpCodes.Switch)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
int minLoadStateFieldIndex = Math.Max(1, currentIndex - 5);
|
||||
|
||||
for (int i = currentIndex - 1; i >= minLoadStateFieldIndex; --i)
|
||||
{
|
||||
if (instructions[i].OpCode == OpCodes.Ldfld &&
|
||||
instructions[i].Operand is FieldDefinition field &&
|
||||
IsCompilerGenerated(field) && field.FullName.EndsWith("__state") &&
|
||||
(instructions[i - 1].OpCode == OpCodes.Ldarg ||
|
||||
instructions[i - 1].OpCode == OpCodes.Ldarg_0))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool SkipGeneratedBranchesForAsyncIterator_DisposeCheck(List<Instruction> instructions, Instruction instruction, int currentIndex)
|
||||
{
|
||||
// Within the compiler-generated async iterator, there are at least a
|
||||
// couple of places where we find this pattern, in which the async
|
||||
// iterator is checking whether it's been disposed, so it'll know to
|
||||
// stop iterating.
|
||||
//
|
||||
// IL_0024: ldarg.0
|
||||
// IL_0025: ldfld bool Test.AsyncEnumerableStateMachine/'<CreateSequenceAsync>d__0'::'<>w__disposeMode'
|
||||
// IL_002a: brfalse.s IL_0031
|
||||
//
|
||||
// We'll eliminate these wherever they appear. It's reasonable to just
|
||||
// look for a "brfalse" or "brfalse.s" instruction, preceded immediately
|
||||
// by "ldfld" of the compiler-generated "<>w__disposeMode" field.
|
||||
|
||||
if (instruction.OpCode != OpCodes.Brfalse &&
|
||||
instruction.OpCode != OpCodes.Brfalse_S)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (currentIndex >= 1 &&
|
||||
instructions[currentIndex - 1].OpCode == OpCodes.Ldfld &&
|
||||
instructions[currentIndex - 1].Operand is FieldDefinition field &&
|
||||
IsCompilerGenerated(field) && field.FullName.EndsWith("__disposeMode"))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// https://github.com/dotnet/roslyn/blob/master/docs/compilers/CSharp/Expression%20Breakpoints.md
|
||||
private bool SkipExpressionBreakpointsBranches(Instruction instruction) => instruction.Previous is not null && instruction.Previous.OpCode == OpCodes.Ldc_I4 &&
|
||||
instruction.Previous.Operand is int operandValue && operandValue == 1 &&
|
||||
instruction.Next is not null && instruction.Next.OpCode == OpCodes.Nop &&
|
||||
instruction.Operand == instruction.Next?.Next;
|
||||
private static bool SkipExpressionBreakpointsBranches(Instruction instruction) => instruction.Previous is not null && instruction.Previous.OpCode == OpCodes.Ldc_I4 &&
|
||||
instruction.Previous.Operand is int operandValue && operandValue == 1 &&
|
||||
instruction.Next is not null && instruction.Next.OpCode == OpCodes.Nop &&
|
||||
instruction.Operand == instruction.Next?.Next;
|
||||
|
||||
public IReadOnlyList<BranchPoint> GetBranchPoints(MethodDefinition methodDefinition)
|
||||
{
|
||||
@@ -415,6 +687,7 @@ namespace Coverlet.Core.Symbols
|
||||
|
||||
bool isAsyncStateMachineMoveNext = IsMoveNextInsideAsyncStateMachine(methodDefinition);
|
||||
bool isMoveNextInsideAsyncStateMachineProlog = isAsyncStateMachineMoveNext && IsMoveNextInsideAsyncStateMachineProlog(methodDefinition);
|
||||
bool isMoveNextInsideAsyncIterator = isAsyncStateMachineMoveNext && IsMoveNextInsideAsyncIterator(methodDefinition);
|
||||
|
||||
// State machine for enumerator uses `brfalse.s`/`beq` or `switch` opcode depending on how many `yield` we have in the method body.
|
||||
// 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
|
||||
@@ -461,11 +734,21 @@ namespace Coverlet.Core.Symbols
|
||||
if (isAsyncStateMachineMoveNext)
|
||||
{
|
||||
if (SkipGeneratedBranchesForExceptionHandlers(methodDefinition, instruction, instructions) ||
|
||||
SkipGeneratedBranchForExceptionRethrown(instructions, instruction))
|
||||
SkipGeneratedBranchForExceptionRethrown(instructions, instruction) ||
|
||||
SkipGeneratedBranchesForAwaitForeach(instructions, instruction))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (isMoveNextInsideAsyncIterator)
|
||||
{
|
||||
if (SkipGeneratedBranchesForAsyncIterator(instructions, instruction))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (SkipBranchGeneratedExceptionFilter(instruction, methodDefinition))
|
||||
{
|
||||
continue;
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Coverlet.Core.Samples.Tests;
|
||||
using Coverlet.Tests.Xunit.Extensions;
|
||||
using Xunit;
|
||||
|
||||
namespace Coverlet.Core.Tests
|
||||
{
|
||||
public partial class CoverageTests
|
||||
{
|
||||
[Fact]
|
||||
public void AsyncForeach()
|
||||
{
|
||||
string path = Path.GetTempFileName();
|
||||
try
|
||||
{
|
||||
FunctionExecutor.Run(async (string[] pathSerialize) =>
|
||||
{
|
||||
CoveragePrepareResult coveragePrepareResult = await TestInstrumentationHelper.Run<AsyncForeach>(instance =>
|
||||
{
|
||||
int res = ((ValueTask<int>)instance.SumWithATwist(AsyncEnumerable.Range(1, 5))).GetAwaiter().GetResult();
|
||||
res += ((ValueTask<int>)instance.Sum(AsyncEnumerable.Range(1, 3))).GetAwaiter().GetResult();
|
||||
res += ((ValueTask<int>)instance.SumEmpty()).GetAwaiter().GetResult();
|
||||
|
||||
return Task.CompletedTask;
|
||||
}, persistPrepareResultToFile: pathSerialize[0]);
|
||||
return 0;
|
||||
}, new string[] { path });
|
||||
|
||||
TestInstrumentationHelper.GetCoverageResult(path)
|
||||
.Document("Instrumentation.AsyncForeach.cs")
|
||||
.AssertLinesCovered(BuildConfiguration.Debug,
|
||||
// SumWithATwist(IAsyncEnumerable<int>)
|
||||
// Apparently due to entering and exiting the async state machine, line 17
|
||||
// (the top of an "await foreach" loop) is reached three times *plus* twice
|
||||
// per loop iteration. So, in this case, with five loop iterations, we end
|
||||
// up with 3 + 5 * 2 = 13 hits.
|
||||
(14, 1), (15, 1), (17, 13), (18, 5), (19, 5), (20, 5), (21, 5), (22, 5),
|
||||
(24, 0), (25, 0), (26, 0), (27, 5), (29, 1), (30, 1),
|
||||
// Sum(IAsyncEnumerable<int>)
|
||||
(34, 1), (35, 1), (37, 9), (38, 3), (39, 3), (40, 3), (42, 1), (43, 1),
|
||||
// SumEmpty()
|
||||
(47, 1), (48, 1), (50, 3), (51, 0), (52, 0), (53, 0), (55, 1), (56, 1)
|
||||
)
|
||||
.AssertBranchesCovered(BuildConfiguration.Debug,
|
||||
// SumWithATwist(IAsyncEnumerable<int>)
|
||||
(17, 2, 1), (17, 3, 5), (19, 0, 5), (19, 1, 0),
|
||||
// Sum(IAsyncEnumerable<int>)
|
||||
(37, 0, 1), (37, 1, 3),
|
||||
// SumEmpty()
|
||||
// If we never entered the loop, that's a branch not taken, which is
|
||||
// what we want to see.
|
||||
(50, 0, 1), (50, 1, 0)
|
||||
)
|
||||
.ExpectedTotalNumberOfBranches(BuildConfiguration.Debug, 4);
|
||||
}
|
||||
finally
|
||||
{
|
||||
File.Delete(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Coverlet.Core.Samples.Tests;
|
||||
using Coverlet.Tests.Xunit.Extensions;
|
||||
using Xunit;
|
||||
|
||||
namespace Coverlet.Core.Tests
|
||||
{
|
||||
public partial class CoverageTests
|
||||
{
|
||||
[Fact]
|
||||
public void AsyncIterator()
|
||||
{
|
||||
string path = Path.GetTempFileName();
|
||||
try
|
||||
{
|
||||
FunctionExecutor.Run(async (string[] pathSerialize) =>
|
||||
{
|
||||
CoveragePrepareResult coveragePrepareResult = await TestInstrumentationHelper.Run<AsyncIterator>(instance =>
|
||||
{
|
||||
int res = ((Task<int>)instance.Issue1104_Repro()).GetAwaiter().GetResult();
|
||||
|
||||
return Task.CompletedTask;
|
||||
}, persistPrepareResultToFile: pathSerialize[0]);
|
||||
return 0;
|
||||
}, new string[] { path });
|
||||
|
||||
TestInstrumentationHelper.GetCoverageResult(path)
|
||||
.Document("Instrumentation.AsyncIterator.cs")
|
||||
.AssertLinesCovered(BuildConfiguration.Debug,
|
||||
// Issue1104_Repro()
|
||||
(14, 1), (15, 1), (17, 203), (18, 100), (19, 100), (20, 100), (22, 1), (23, 1),
|
||||
// CreateSequenceAsync()
|
||||
(26, 1), (27, 202), (28, 100), (29, 100), (30, 100), (31, 100), (32, 1)
|
||||
)
|
||||
.AssertBranchesCovered(BuildConfiguration.Debug,
|
||||
// Issue1104_Repro(),
|
||||
(17, 0, 1), (17, 1, 100),
|
||||
// CreateSequenceAsync()
|
||||
(27, 0, 1), (27, 1, 100)
|
||||
)
|
||||
.ExpectedTotalNumberOfBranches(BuildConfiguration.Debug, 2);
|
||||
}
|
||||
finally
|
||||
{
|
||||
File.Delete(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
// Remember to use full name because adding new using directives change line numbers
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Coverlet.Core.Samples.Tests
|
||||
{
|
||||
public class AsyncForeach
|
||||
{
|
||||
async public ValueTask<int> SumWithATwist(IAsyncEnumerable<int> ints)
|
||||
{
|
||||
int sum = 0;
|
||||
|
||||
await foreach (int i in ints)
|
||||
{
|
||||
if (i > 0)
|
||||
{
|
||||
sum += i;
|
||||
}
|
||||
else
|
||||
{
|
||||
sum = 0;
|
||||
}
|
||||
}
|
||||
|
||||
return sum;
|
||||
}
|
||||
|
||||
|
||||
async public ValueTask<int> Sum(IAsyncEnumerable<int> ints)
|
||||
{
|
||||
int sum = 0;
|
||||
|
||||
await foreach (int i in ints)
|
||||
{
|
||||
sum += i;
|
||||
}
|
||||
|
||||
return sum;
|
||||
}
|
||||
|
||||
|
||||
async public ValueTask<int> SumEmpty()
|
||||
{
|
||||
int sum = 0;
|
||||
|
||||
await foreach (int i in AsyncEnumerable.Empty<int>())
|
||||
{
|
||||
sum += i;
|
||||
}
|
||||
|
||||
return sum;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
// Remember to use full name because adding new using directives change line numbers
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Coverlet.Core.Samples.Tests
|
||||
{
|
||||
public class AsyncIterator
|
||||
{
|
||||
async public Task<int> Issue1104_Repro()
|
||||
{
|
||||
int sum = 0;
|
||||
|
||||
await foreach (int result in CreateSequenceAsync())
|
||||
{
|
||||
sum += result;
|
||||
}
|
||||
|
||||
return sum;
|
||||
}
|
||||
|
||||
async private IAsyncEnumerable<int> CreateSequenceAsync()
|
||||
{
|
||||
for (int i = 0; i < 100; ++i)
|
||||
{
|
||||
await Task.CompletedTask;
|
||||
yield return i;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -197,6 +197,51 @@ namespace Coverlet.Core.Samples.Tests
|
||||
}
|
||||
}
|
||||
|
||||
public class AwaitForeachStateMachine
|
||||
{
|
||||
async public ValueTask AsyncAwait(IAsyncEnumerable<int> ints)
|
||||
{
|
||||
await foreach (int i in ints)
|
||||
{
|
||||
await default(ValueTask);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class AwaitForeachStateMachine_WithBranches
|
||||
{
|
||||
async public ValueTask<int> SumWithATwist(IAsyncEnumerable<int> ints)
|
||||
{
|
||||
int sum = 0;
|
||||
|
||||
await foreach (int i in ints)
|
||||
{
|
||||
if (i > 0)
|
||||
{
|
||||
sum += i;
|
||||
}
|
||||
else
|
||||
{
|
||||
sum = 0;
|
||||
}
|
||||
}
|
||||
|
||||
return sum;
|
||||
}
|
||||
}
|
||||
|
||||
public class AsyncIteratorStateMachine
|
||||
{
|
||||
async public IAsyncEnumerable<int> CreateSequenceAsync()
|
||||
{
|
||||
for (int i = 0; i < 100; ++i)
|
||||
{
|
||||
await Task.CompletedTask;
|
||||
yield return i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[ExcludeFromCoverage]
|
||||
public class ClassExcludedByCoverletCodeCoverageAttr
|
||||
{
|
||||
|
||||
@@ -310,6 +310,75 @@ namespace Coverlet.Core.Symbols.Tests
|
||||
Assert.Empty(points);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetBranchPoints_IgnoresMostBranchesIn_AwaitForeachStateMachine()
|
||||
{
|
||||
// arrange
|
||||
var nestedName = typeof(AwaitForeachStateMachine).GetNestedTypes(BindingFlags.NonPublic).First().Name;
|
||||
var type = _module.Types.FirstOrDefault(x => x.FullName == typeof(AwaitForeachStateMachine).FullName);
|
||||
var nestedType = type.NestedTypes.FirstOrDefault(x => x.FullName.EndsWith(nestedName));
|
||||
var method = nestedType.Methods.First(x => x.FullName.EndsWith("::MoveNext()"));
|
||||
|
||||
// act
|
||||
var points = _cecilSymbolHelper.GetBranchPoints(method);
|
||||
|
||||
// assert
|
||||
// We do expect there to be a two-way branch (stay in the loop or not?) on
|
||||
// the line containing "await foreach".
|
||||
Assert.NotNull(points);
|
||||
Assert.Equal(2, points.Count());
|
||||
Assert.Equal(points[0].Offset, points[1].Offset);
|
||||
Assert.Equal(204, points[0].StartLine);
|
||||
Assert.Equal(204, points[1].StartLine);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetBranchPoints_IgnoresMostBranchesIn_AwaitForeachStateMachine_WithBranchesWithinIt()
|
||||
{
|
||||
// arrange
|
||||
var nestedName = typeof(AwaitForeachStateMachine_WithBranches).GetNestedTypes(BindingFlags.NonPublic).First().Name;
|
||||
var type = _module.Types.FirstOrDefault(x => x.FullName == typeof(AwaitForeachStateMachine_WithBranches).FullName);
|
||||
var nestedType = type.NestedTypes.FirstOrDefault(x => x.FullName.EndsWith(nestedName));
|
||||
var method = nestedType.Methods.First(x => x.FullName.EndsWith("::MoveNext()"));
|
||||
|
||||
// act
|
||||
var points = _cecilSymbolHelper.GetBranchPoints(method);
|
||||
|
||||
// assert
|
||||
// We do expect there to be four branch points (two places where we can branch
|
||||
// two ways), one being the "stay in the loop or not?" branch on the line
|
||||
// containing "await foreach" and the other being the "if" statement inside
|
||||
// the loop.
|
||||
Assert.NotNull(points);
|
||||
Assert.Equal(4, points.Count());
|
||||
Assert.Equal(points[0].Offset, points[1].Offset);
|
||||
Assert.Equal(points[2].Offset, points[3].Offset);
|
||||
Assert.Equal(219, points[0].StartLine);
|
||||
Assert.Equal(219, points[1].StartLine);
|
||||
Assert.Equal(217, points[2].StartLine);
|
||||
Assert.Equal(217, points[3].StartLine);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetBranchesPoints_IgnoresExtraBranchesIn_AsyncIteratorStateMachine()
|
||||
{
|
||||
// arrange
|
||||
var nestedName = typeof(AsyncIteratorStateMachine).GetNestedTypes(BindingFlags.NonPublic).First().Name;
|
||||
var type = _module.Types.FirstOrDefault(x => x.FullName == typeof(AsyncIteratorStateMachine).FullName);
|
||||
var nestedType = type.NestedTypes.FirstOrDefault(x => x.FullName.EndsWith(nestedName));
|
||||
var method = nestedType.Methods.First(x => x.FullName.EndsWith("::MoveNext()"));
|
||||
|
||||
// act
|
||||
var points = _cecilSymbolHelper.GetBranchPoints(method);
|
||||
|
||||
// assert
|
||||
// We do expect the "for" loop to be a branch with two branch points, but that's it.
|
||||
Assert.NotNull(points);
|
||||
Assert.Equal(2, points.Count());
|
||||
Assert.Equal(237, points[0].StartLine);
|
||||
Assert.Equal(237, points[1].StartLine);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetBranchPoints_ExceptionFilter()
|
||||
{
|
||||
|
||||
@@ -30,9 +30,11 @@
|
||||
|
||||
<ItemGroup>
|
||||
<!--For test TestInstrument_NetstandardAwareAssemblyResolver_PreserveCompilationContext-->
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="2.2.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="5.0.0" />
|
||||
<!--For test issue 809 https://github.com/coverlet-coverage/coverlet/issues/809-->
|
||||
<PackageReference Include="LinqKit.Microsoft.EntityFrameworkCore" Version="2.0.0" />
|
||||
<PackageReference Include="LinqKit.Microsoft.EntityFrameworkCore" Version="5.0.23" />
|
||||
<!--To test issue 1104 https://github.com/coverlet-coverage/coverlet/issues/1104-->
|
||||
<PackageReference Include="System.Linq.Async" Version="5.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
Reference in New Issue
Block a user