diff --git a/Documentation/Changelog.md b/Documentation/Changelog.md index 397dda5..ec12292 100644 --- a/Documentation/Changelog.md +++ b/Documentation/Changelog.md @@ -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) diff --git a/src/coverlet.core/Symbols/CecilSymbolHelper.cs b/src/coverlet.core/Symbols/CecilSymbolHelper.cs index ca0b0e9..b6727de 100644 --- a/src/coverlet.core/Symbols/CecilSymbolHelper.cs +++ b/src/coverlet.core/Symbols/CecilSymbolHelper.cs @@ -855,7 +855,6 @@ namespace Coverlet.Core.Symbols return false; } - static bool DisposeCheck(List instructions, Instruction instruction, int currentIndex) { // Within the compiler-generated async iterator, there are at least a @@ -891,6 +890,40 @@ namespace Coverlet.Core.Symbols } } + private bool SkipGeneratedBranchesForEnumeratorCancellationAttribute(List 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/'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 && @@ -973,6 +1006,11 @@ namespace Coverlet.Core.Symbols } } + if (SkipGeneratedBranchesForEnumeratorCancellationAttribute(instructions, instruction)) + { + continue; + } + if (SkipBranchGeneratedExceptionFilter(instruction, methodDefinition)) { continue; diff --git a/test/coverlet.core.tests/Coverage/CoverageTests.AsyncAwait.cs b/test/coverlet.core.tests/Coverage/CoverageTests.AsyncAwait.cs index 8727b7c..40d733b 100644 --- a/test/coverlet.core.tests/Coverage/CoverageTests.AsyncAwait.cs +++ b/test/coverlet.core.tests/Coverage/CoverageTests.AsyncAwait.cs @@ -1,5 +1,6 @@ using System.IO; using System.Linq; +using System.Threading; using System.Threading.Tasks; using Coverlet.Core.Samples.Tests; @@ -182,5 +183,35 @@ namespace Coverlet.Core.Tests File.Delete(path); } } + + [Fact] + public void AsyncAwait_Issue_1275() + { + string path = Path.GetTempFileName(); + try + { + FunctionExecutor.Run(async (string[] pathSerialize) => + { + CoveragePrepareResult coveragePrepareResult = await TestInstrumentationHelper.Run(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); + } + } } } \ No newline at end of file diff --git a/test/coverlet.core.tests/Samples/Instrumentation.AsyncAwait.cs b/test/coverlet.core.tests/Samples/Instrumentation.AsyncAwait.cs index 0b4992c..31361c9 100644 --- a/test/coverlet.core.tests/Samples/Instrumentation.AsyncAwait.cs +++ b/test/coverlet.core.tests/Samples/Instrumentation.AsyncAwait.cs @@ -151,4 +151,28 @@ namespace Coverlet.Core.Samples.Tests } } } + + public class Issue_1275 + { + public async Task Execute(System.Threading.CancellationToken token) + { + int sum = 0; + + await foreach (int result in AsyncEnumerable(token)) + { + sum += result; + } + + return sum; + } + + async System.Collections.Generic.IAsyncEnumerable AsyncEnumerable([System.Runtime.CompilerServices.EnumeratorCancellation] System.Threading.CancellationToken cancellationToken) + { + for (int i = 0; i < 1; i++) + { + await Task.Delay(1, cancellationToken); + yield return i; + } + } + } }