Skip to content

Commit d4aa573

Browse files
committed
made dispose check work for generic types
1 parent bc114a3 commit d4aa573

File tree

5 files changed

+86
-3
lines changed

5 files changed

+86
-3
lines changed

Documentation/Changelog.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
66

77
## Unreleased
88

9+
### Fixed
10+
-Incorrect coverage for methods returning IAsyncEnumerable in generic classes [#1383](https://github.com/coverlet-coverage/coverlet/issues/1383)
11+
912
### Breaking changes
1013
- 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`.
1114
- 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.

src/coverlet.core/Symbols/CecilSymbolHelper.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -877,8 +877,10 @@ static bool DisposeCheck(List<Instruction> instructions, Instruction instruction
877877

878878
if (currentIndex >= 2 &&
879879
instructions[currentIndex - 1].OpCode == OpCodes.Ldfld &&
880-
instructions[currentIndex - 1].Operand is FieldDefinition field &&
881-
IsCompilerGenerated(field) && field.FullName.EndsWith("__disposeMode") &&
880+
(
881+
(instructions[currentIndex - 1].Operand is FieldDefinition field && IsCompilerGenerated(field) && field.FullName.EndsWith("__disposeMode")) ||
882+
(instructions[currentIndex - 1].Operand is FieldReference fieldRef && IsCompilerGenerated(fieldRef.Resolve()) && fieldRef.FullName.EndsWith("__disposeMode"))
883+
) &&
882884
(instructions[currentIndex - 2].OpCode == OpCodes.Ldarg ||
883885
instructions[currentIndex - 2].OpCode == OpCodes.Ldarg_0))
884886
{
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// Copyright (c) Toni Solarin-Sodara
2+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
3+
4+
using System.Collections.Generic;
5+
using System.IO;
6+
using System.Threading.Tasks;
7+
using Coverlet.Core.Samples.Tests;
8+
using Xunit;
9+
10+
namespace Coverlet.Core.Tests
11+
{
12+
public partial class CoverageTests
13+
{
14+
[Fact]
15+
public void GenericAsyncIterator()
16+
{
17+
string path = Path.GetTempFileName();
18+
try
19+
{
20+
FunctionExecutor.Run(async (string[] pathSerialize) =>
21+
{
22+
CoveragePrepareResult coveragePrepareResult = await TestInstrumentationHelper.Run<GenericAsyncIterator<int>>(instance =>
23+
{
24+
List<int> res = ((Task<List<int>>)instance.Issue1383()).GetAwaiter().GetResult();
25+
26+
return Task.CompletedTask;
27+
}, persistPrepareResultToFile: pathSerialize[0]);
28+
return 0;
29+
}, new string[] { path });
30+
31+
TestInstrumentationHelper.GetCoverageResult(path)
32+
.Document("Instrumentation.GenericAsyncIterator.cs")
33+
.AssertLinesCovered(BuildConfiguration.Debug, (13, 1), (14, 1), (20, 1), (21, 1), (22, 1))
34+
.ExpectedTotalNumberOfBranches(BuildConfiguration.Debug, 0);
35+
}
36+
finally
37+
{
38+
File.Delete(path);
39+
}
40+
}
41+
}
42+
}

test/coverlet.core.tests/Coverage/InstrumenterHelper.cs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ public static async Task<CoveragePrepareResult> Run<T>(Func<dynamic, Task> callM
101101
IncludeFilters = (includeFilter is null ? defaultFilters(fileName) : includeFilter(fileName)).Concat(
102102
new string[]
103103
{
104-
$"[{Path.GetFileNameWithoutExtension(fileName)}*]{typeof(T).FullName}*"
104+
$"[{Path.GetFileNameWithoutExtension(fileName)}*]{GetTypeFullName<T>()}*"
105105
}).ToArray(),
106106
IncludeDirectories = Array.Empty<string>(),
107107
ExcludeFilters = (excludeFilter is null ? defaultFilters(fileName) : excludeFilter(fileName)).Concat(new string[]
@@ -180,6 +180,17 @@ private static void SetTestContainer(string testModule = null, bool disableResto
180180
return serviceCollection.BuildServiceProvider();
181181
});
182182
}
183+
184+
private static string GetTypeFullName<T>()
185+
{
186+
string name = typeof(T).FullName;
187+
if (typeof(T).IsGenericType && name != null)
188+
{
189+
int index = name.IndexOf('`');
190+
return index == -1 ? name : name[..index];
191+
}
192+
return name;
193+
}
183194
}
184195

185196
class CustomProcessExitHandler : IProcessExitHandler
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// Remember to use full name because adding new using directives change line numbers
2+
3+
using System.Collections.Generic;
4+
using System.Linq;
5+
using System.Threading.Tasks;
6+
7+
namespace Coverlet.Core.Samples.Tests
8+
{
9+
public class GenericAsyncIterator<T>
10+
{
11+
public async Task<List<int>> Issue1383()
12+
{
13+
var sequence = await CreateSequenceAsync().ToListAsync();
14+
return sequence;
15+
}
16+
17+
18+
public async IAsyncEnumerable<int> CreateSequenceAsync()
19+
{
20+
await Task.CompletedTask;
21+
yield return 5;
22+
yield return 2;
23+
}
24+
}
25+
}

0 commit comments

Comments
 (0)