From 919755899d8ffddfb49bae808087cfa28128b778 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20M=C3=BCller?= Date: Sun, 8 Jan 2023 02:38:28 +0100 Subject: [PATCH] made dispose check work for generic types --- Documentation/Changelog.md | 3 ++ .../Symbols/CecilSymbolHelper.cs | 6 ++- .../CoverageTests.GenericAsyncIterator.cs | 42 +++++++++++++++++++ .../Coverage/InstrumenterHelper.cs | 13 +++++- .../Instrumentation.GenericAsyncIterator.cs | 25 +++++++++++ 5 files changed, 86 insertions(+), 3 deletions(-) create mode 100644 test/coverlet.core.tests/Coverage/CoverageTests.GenericAsyncIterator.cs create mode 100644 test/coverlet.core.tests/Samples/Instrumentation.GenericAsyncIterator.cs diff --git a/Documentation/Changelog.md b/Documentation/Changelog.md index 9945361da..64909e976 100644 --- a/Documentation/Changelog.md +++ b/Documentation/Changelog.md @@ -6,6 +6,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Fixed +-Incorrect coverage for methods returning IAsyncEnumerable in generic classes [#1383](https://github.com/coverlet-coverage/coverlet/issues/1383) + ### Breaking changes - New parameter `ExcludeAssembliesWithoutSources` to control automatic assembly exclusion [1164](https://github.com/coverlet-coverage/coverlet/issues/1164). The parameter `InstrumentModulesWithoutLocalSources` has been removed. since it can be handled by setting `ExcludeAssembliesWithoutSources` to `None`. - The default heuristics for determining whether to instrument an assembly has been changed. In previous versions any missing source file was taken as a signal that it was a third-party project that shouldn't be instrumented, with exceptions for some common file name patterns for source generators. Now only assemblies where no source files at all can be found are excluded from instrumentation, and the code for detecting source generator files have been removed. To get back to the behaviour that at least one missing file is sufficient to exclude an assembly, set `ExcludeAssembliesWithoutSources` to `MissingAny`, or use assembly exclusion filters for more fine-grained control. diff --git a/src/coverlet.core/Symbols/CecilSymbolHelper.cs b/src/coverlet.core/Symbols/CecilSymbolHelper.cs index f5f8458b0..ec77c9fe2 100644 --- a/src/coverlet.core/Symbols/CecilSymbolHelper.cs +++ b/src/coverlet.core/Symbols/CecilSymbolHelper.cs @@ -877,8 +877,10 @@ static bool DisposeCheck(List instructions, Instruction instruction if (currentIndex >= 2 && instructions[currentIndex - 1].OpCode == OpCodes.Ldfld && - instructions[currentIndex - 1].Operand is FieldDefinition field && - IsCompilerGenerated(field) && field.FullName.EndsWith("__disposeMode") && + ( + (instructions[currentIndex - 1].Operand is FieldDefinition field && IsCompilerGenerated(field) && field.FullName.EndsWith("__disposeMode")) || + (instructions[currentIndex - 1].Operand is FieldReference fieldRef && IsCompilerGenerated(fieldRef.Resolve()) && fieldRef.FullName.EndsWith("__disposeMode")) + ) && (instructions[currentIndex - 2].OpCode == OpCodes.Ldarg || instructions[currentIndex - 2].OpCode == OpCodes.Ldarg_0)) { diff --git a/test/coverlet.core.tests/Coverage/CoverageTests.GenericAsyncIterator.cs b/test/coverlet.core.tests/Coverage/CoverageTests.GenericAsyncIterator.cs new file mode 100644 index 000000000..8aa18b285 --- /dev/null +++ b/test/coverlet.core.tests/Coverage/CoverageTests.GenericAsyncIterator.cs @@ -0,0 +1,42 @@ +// Copyright (c) Toni Solarin-Sodara +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using Coverlet.Core.Samples.Tests; +using Xunit; + +namespace Coverlet.Core.Tests +{ + public partial class CoverageTests + { + [Fact] + public void GenericAsyncIterator() + { + string path = Path.GetTempFileName(); + try + { + FunctionExecutor.Run(async (string[] pathSerialize) => + { + CoveragePrepareResult coveragePrepareResult = await TestInstrumentationHelper.Run>(instance => + { + List res = ((Task>)instance.Issue1383()).GetAwaiter().GetResult(); + + return Task.CompletedTask; + }, persistPrepareResultToFile: pathSerialize[0]); + return 0; + }, new string[] { path }); + + TestInstrumentationHelper.GetCoverageResult(path) + .Document("Instrumentation.GenericAsyncIterator.cs") + .AssertLinesCovered(BuildConfiguration.Debug, (13, 1), (14, 1), (20, 1), (21, 1), (22, 1)) + .ExpectedTotalNumberOfBranches(BuildConfiguration.Debug, 0); + } + finally + { + File.Delete(path); + } + } + } +} diff --git a/test/coverlet.core.tests/Coverage/InstrumenterHelper.cs b/test/coverlet.core.tests/Coverage/InstrumenterHelper.cs index 8cb5eca7f..fd39d8289 100644 --- a/test/coverlet.core.tests/Coverage/InstrumenterHelper.cs +++ b/test/coverlet.core.tests/Coverage/InstrumenterHelper.cs @@ -101,7 +101,7 @@ public static async Task Run(Func callM IncludeFilters = (includeFilter is null ? defaultFilters(fileName) : includeFilter(fileName)).Concat( new string[] { - $"[{Path.GetFileNameWithoutExtension(fileName)}*]{typeof(T).FullName}*" + $"[{Path.GetFileNameWithoutExtension(fileName)}*]{GetTypeFullName()}*" }).ToArray(), IncludeDirectories = Array.Empty(), ExcludeFilters = (excludeFilter is null ? defaultFilters(fileName) : excludeFilter(fileName)).Concat(new string[] @@ -180,6 +180,17 @@ private static void SetTestContainer(string testModule = null, bool disableResto return serviceCollection.BuildServiceProvider(); }); } + + private static string GetTypeFullName() + { + string name = typeof(T).FullName; + if (typeof(T).IsGenericType && name != null) + { + int index = name.IndexOf('`'); + return index == -1 ? name : name[..index]; + } + return name; + } } class CustomProcessExitHandler : IProcessExitHandler diff --git a/test/coverlet.core.tests/Samples/Instrumentation.GenericAsyncIterator.cs b/test/coverlet.core.tests/Samples/Instrumentation.GenericAsyncIterator.cs new file mode 100644 index 000000000..cc6311506 --- /dev/null +++ b/test/coverlet.core.tests/Samples/Instrumentation.GenericAsyncIterator.cs @@ -0,0 +1,25 @@ +// Remember to use full name because adding new using directives change line numbers + +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Coverlet.Core.Samples.Tests +{ + public class GenericAsyncIterator + { + public async Task> Issue1383() + { + var sequence = await CreateSequenceAsync().ToListAsync(); + return sequence; + } + + + public async IAsyncEnumerable CreateSequenceAsync() + { + await Task.CompletedTask; + yield return 5; + yield return 2; + } + } +}