Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Documentation/Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Unreleased

### Fixed
-Fix wrong branch coverage with EnumeratorCancellation attribute [#1275](https://github.com/coverlet-coverage/coverlet/issues/1275)
-Fix negative coverage exceeding int.MaxValue [#1266](https://github.com/coverlet-coverage/coverlet/issues/1266)
-Fix summary output format for culture de-DE [#1263](https://github.com/coverlet-coverage/coverlet/issues/1263)
-Fix branch coverage issue for finally block with await [#1233](https://github.com/coverlet-coverage/coverlet/issues/1233)
Expand Down
40 changes: 39 additions & 1 deletion src/coverlet.core/Symbols/CecilSymbolHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -855,7 +855,6 @@ instructions[i].Operand is FieldDefinition field &&
return false;
}


static bool DisposeCheck(List<Instruction> instructions, Instruction instruction, int currentIndex)
{
// Within the compiler-generated async iterator, there are at least a
Expand Down Expand Up @@ -891,6 +890,40 @@ static bool DisposeCheck(List<Instruction> instructions, Instruction instruction
}
}

private bool SkipGeneratedBranchesForEnumeratorCancellationAttribute(List<Instruction> instructions, Instruction instruction)
{
// For async-enumerable methods an additional cancellation token despite the default one can be passed.
// The EnumeratorCancellation attribute marks the parameter whose value is received by GetAsyncEnumerator(CancellationToken).
// Therefore the compiler generates the field x__combinedTokens and generates some additional branch points.
//
// IL_0118: ldarg.0
// IL_0119: ldfld class [System.Runtime]System.Threading.CancellationTokenSource Issue1275.AwaitForeachReproduction/'<AsyncEnumerable>d__1'::'<>x__combinedTokens'
// IL_011E: brfalse.s IL_0133
//
// 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 "<>x__combinedTokens" field.

int branchIndex = instructions.BinarySearch(instruction, new InstructionByOffsetComparer());

if (instruction.OpCode != OpCodes.Brfalse &&
instruction.OpCode != OpCodes.Brfalse_S)
{
return false;
}

if (branchIndex >= 2 &&
instructions[branchIndex - 1].OpCode == OpCodes.Ldfld &&
instructions[branchIndex - 1].Operand is FieldDefinition field &&
field.FieldType.FullName.Equals("System.Threading.CancellationTokenSource") &&
field.FullName.EndsWith("x__combinedTokens") &&
(instructions[branchIndex - 2].OpCode == OpCodes.Ldarg ||
instructions[branchIndex - 2].OpCode == OpCodes.Ldarg_0))
{
return true;
}
return false;
}

// https://github.com/dotnet/roslyn/blob/master/docs/compilers/CSharp/Expression%20Breakpoints.md
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 &&
Expand Down Expand Up @@ -973,6 +1006,11 @@ public IReadOnlyList<BranchPoint> GetBranchPoints(MethodDefinition methodDefinit
}
}

if (SkipGeneratedBranchesForEnumeratorCancellationAttribute(instructions, instruction))
{
continue;
}

if (SkipBranchGeneratedExceptionFilter(instruction, methodDefinition))
{
continue;
Expand Down
31 changes: 31 additions & 0 deletions test/coverlet.core.tests/Coverage/CoverageTests.AsyncAwait.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

using Coverlet.Core.Samples.Tests;
Expand Down Expand Up @@ -182,5 +183,35 @@ public void AsyncAwait_Issue_1233()
File.Delete(path);
}
}

[Fact]
public void AsyncAwait_Issue_1275()
{
string path = Path.GetTempFileName();
try
{
FunctionExecutor.Run(async (string[] pathSerialize) =>
{
CoveragePrepareResult coveragePrepareResult = await TestInstrumentationHelper.Run<Issue_1275>(instance =>
{
var cts = new CancellationTokenSource();
((Task)instance.Execute(cts.Token)).ConfigureAwait(false).GetAwaiter().GetResult();
return Task.CompletedTask;
},
persistPrepareResultToFile: pathSerialize[0]);

return 0;
}, new string[] { path });

var document = TestInstrumentationHelper.GetCoverageResult(path).Document("Instrumentation.AsyncAwait.cs");
document.AssertLinesCoveredFromTo(BuildConfiguration.Debug, 170, 176);
document.AssertBranchesCovered(BuildConfiguration.Debug, (171, 0, 1), (171, 1, 1));
Assert.DoesNotContain(document.Branches, x => x.Key.Line == 176);
}
finally
{
File.Delete(path);
}
}
}
}
24 changes: 24 additions & 0 deletions test/coverlet.core.tests/Samples/Instrumentation.AsyncAwait.cs
Original file line number Diff line number Diff line change
Expand Up @@ -151,4 +151,28 @@ async public Task Test()
}
}
}

public class Issue_1275
{
public async Task<int> Execute(System.Threading.CancellationToken token)
{
int sum = 0;

await foreach (int result in AsyncEnumerable(token))
{
sum += result;
}

return sum;
}

async System.Collections.Generic.IAsyncEnumerable<int> AsyncEnumerable([System.Runtime.CompilerServices.EnumeratorCancellation] System.Threading.CancellationToken cancellationToken)
{
for (int i = 0; i < 1; i++)
{
await Task.Delay(1, cancellationToken);
yield return i;
}
}
}
}