Skip to content

Commit 26bd50b

Browse files
authored
Fix resolving assemblies from frameworks not referenced by coverlet itself (#1449)
1 parent 468de3c commit 26bd50b

File tree

20 files changed

+506
-218
lines changed

20 files changed

+506
-218
lines changed

coverlet.sln

Lines changed: 185 additions & 164 deletions
Large diffs are not rendered by default.

src/coverlet.core/Instrumentation/CecilAssemblyResolver.cs

Lines changed: 63 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
using Microsoft.Extensions.DependencyModel;
1111
using Microsoft.Extensions.DependencyModel.Resolution;
1212
using Mono.Cecil;
13+
using Newtonsoft.Json;
14+
using Newtonsoft.Json.Linq;
1315

1416
namespace Coverlet.Core.Instrumentation
1517
{
@@ -70,14 +72,14 @@ public NetstandardAwareAssemblyResolver(string modulePath, ILogger logger)
7072
_modulePath = modulePath;
7173
_logger = logger;
7274

73-
// this is lazy because we cannot create AspNetCoreSharedFrameworkResolver if not on .NET Core runtime,
75+
// this is lazy because we cannot create NetCoreSharedFrameworkResolver if not on .NET Core runtime,
7476
// runtime folders are different
7577
_compositeResolver = new Lazy<CompositeCompilationAssemblyResolver>(() => new CompositeCompilationAssemblyResolver(new ICompilationAssemblyResolver[]
7678
{
7779
new AppBaseCompilationAssemblyResolver(),
78-
new ReferenceAssemblyPathResolver(),
7980
new PackageCompilationAssemblyResolver(),
80-
new AspNetCoreSharedFrameworkResolver(_logger)
81+
new NetCoreSharedFrameworkResolver(modulePath, _logger),
82+
new ReferenceAssemblyPathResolver(),
8183
}), true);
8284
}
8385

@@ -216,23 +218,37 @@ internal AssemblyDefinition TryWithCustomResolverOnDotNetCore(AssemblyNameRefere
216218
}
217219
}
218220

219-
internal class AspNetCoreSharedFrameworkResolver : ICompilationAssemblyResolver
221+
internal class NetCoreSharedFrameworkResolver : ICompilationAssemblyResolver
220222
{
221-
private readonly string[] _aspNetSharedFrameworkDirs;
223+
private readonly List<string> _aspNetSharedFrameworkDirs = new();
222224
private readonly ILogger _logger;
223225

224-
public AspNetCoreSharedFrameworkResolver(ILogger logger)
226+
public NetCoreSharedFrameworkResolver(string modulePath, ILogger logger)
225227
{
226228
_logger = logger;
227-
string runtimeRootPath = Path.GetDirectoryName(typeof(object).Assembly.Location);
228-
string runtimeVersion = runtimeRootPath.Substring(runtimeRootPath.LastIndexOf(Path.DirectorySeparatorChar) + 1);
229-
_aspNetSharedFrameworkDirs = new string[]
229+
230+
string runtimeConfigFile = Path.Combine(
231+
Path.GetDirectoryName(modulePath)!,
232+
Path.GetFileNameWithoutExtension(modulePath) + ".runtimeconfig.json");
233+
if (!File.Exists(runtimeConfigFile))
234+
{
235+
return;
236+
}
237+
238+
var reader = new RuntimeConfigurationReader(runtimeConfigFile);
239+
IEnumerable<(string Name, string Version)> referencedFrameworks = reader.GetFrameworks();
240+
string runtimePath = Path.GetDirectoryName(typeof(object).Assembly.Location);
241+
string runtimeRootPath = Path.Combine(runtimePath!, "../..");
242+
foreach ((string frameworkName, string frameworkVersion) in referencedFrameworks)
230243
{
231-
Path.GetFullPath(Path.Combine(runtimeRootPath,"../../Microsoft.AspNetCore.All", runtimeVersion)),
232-
Path.GetFullPath(Path.Combine(runtimeRootPath, "../../Microsoft.AspNetCore.App", runtimeVersion))
233-
};
244+
var majorVersion = string.Join(".", frameworkVersion.Split('.').Take(2)) + ".";
245+
var directory = new DirectoryInfo(Path.Combine(runtimeRootPath, frameworkName));
246+
var latestVersion = directory.GetDirectories().Where(x => x.Name.StartsWith(majorVersion))
247+
.Select(x => Convert.ToUInt32(x.Name.Substring(majorVersion.Length))).Max();
248+
_aspNetSharedFrameworkDirs.Add(Path.Combine(directory.FullName, majorVersion + latestVersion));
249+
}
234250

235-
_logger.LogVerbose("AspNetCoreSharedFrameworkResolver search paths:");
251+
_logger.LogVerbose("NetCoreSharedFrameworkResolver search paths:");
236252
foreach (string searchPath in _aspNetSharedFrameworkDirs)
237253
{
238254
_logger.LogVerbose(searchPath);
@@ -250,7 +266,8 @@ public bool TryResolveAssemblyPaths(CompilationLibrary library, List<string> ass
250266
continue;
251267
}
252268

253-
foreach (string file in Directory.GetFiles(sharedFrameworkPath))
269+
string[] files = Directory.GetFiles(sharedFrameworkPath);
270+
foreach (string file in files)
254271
{
255272
if (Path.GetFileName(file).Equals(dllName, StringComparison.OrdinalIgnoreCase))
256273
{
@@ -264,4 +281,36 @@ public bool TryResolveAssemblyPaths(CompilationLibrary library, List<string> ass
264281
return false;
265282
}
266283
}
284+
285+
internal class RuntimeConfigurationReader
286+
{
287+
private readonly string _runtimeConfigFile;
288+
289+
public RuntimeConfigurationReader(string runtimeConfigFile)
290+
{
291+
_runtimeConfigFile = runtimeConfigFile;
292+
}
293+
294+
public IEnumerable<(string Name, string Version)> GetFrameworks()
295+
{
296+
JObject configuration =
297+
new JsonSerializer().Deserialize<JObject>(
298+
new JsonTextReader(new StringReader(File.ReadAllText(_runtimeConfigFile))));
299+
300+
JToken runtimeOptions = configuration["runtimeOptions"];
301+
JToken framework = runtimeOptions?["framework"];
302+
if (framework != null)
303+
{
304+
return new[] {(framework["name"].Value<string>(), framework["version"].Value<string>())};
305+
}
306+
307+
JToken frameworks = runtimeOptions?["frameworks"];
308+
if (frameworks != null)
309+
{
310+
return frameworks.Select(x => (x["name"].Value<string>(), x["version"].Value<string>()));
311+
}
312+
313+
throw new InvalidOperationException($"Unable to read runtime configuration from {_runtimeConfigFile}.");
314+
}
315+
}
267316
}

src/coverlet.core/Instrumentation/InstrumenterResult.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System;
55
using System.Collections.Generic;
66
using System.Diagnostics;
7+
using System.Diagnostics.CodeAnalysis;
78
using System.Runtime.Serialization;
89

910
namespace Coverlet.Core.Instrumentation
@@ -79,6 +80,7 @@ public Document()
7980

8081
[DebuggerDisplay("isBranch = {isBranch} docIndex = {docIndex} start = {start} end = {end}")]
8182
[DataContract]
83+
[SuppressMessage("Style", "IDE1006", Justification = "suppress casing error for API compatibility")]
8284
internal class HitCandidate
8385
{
8486
public HitCandidate(bool isBranch, int docIndex, int start, int end) => (this.isBranch, this.docIndex, this.start, this.end) = (isBranch, docIndex, start, end);

src/coverlet.core/Properties/AssemblyInfo.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,8 @@
1313
[assembly: InternalsVisibleTo("coverlet.core.tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100757cf9291d78a82e5bb58a827a3c46c2f959318327ad30d1b52e918321ffbd847fb21565b8576d2a3a24562a93e86c77a298b564a0f1b98f63d7a1441a3a8bcc206da3ed09d5dacc76e122a109a9d3ac608e21a054d667a2bae98510a1f0f653c0e6f58f42b4b3934f6012f5ec4a09b3dfd3e14d437ede1424bdb722aead64ad")]
1414
[assembly: InternalsVisibleTo("coverlet.collector.tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100ed0ed6af9693182615b8dcadc83c918b8d36312f86cefc69539d67d4189cd1b89420e7c3871802ffef7f5ca7816c68ad856c77bf7c230cc07824d96aa5d1237eebd30e246b9a14e22695fb26b40c800f74ea96619092cbd3a5d430d6c003fc7a82e8ccd1e315b935105d9232fe9e99e8d7ff54bba6f191959338d4a3169df9b3")]
1515
[assembly: InternalsVisibleTo("coverlet.integration.tests, PublicKey=002400000480000094000000060200000024000052534131000400000100010001d24efbe9cbc2dc49b7a3d2ae34ca37cfb69b4f450acd768a22ce5cd021c8a38ae7dc68b2809a1ac606ad531b578f192a5690b2986990cbda4dd84ec65a3a4c1c36f6d7bb18f08592b93091535eaee2f0c8e48763ed7f190db2008e1f9e0facd5c0df5aaab74febd3430e09a428a72e5e6b88357f92d78e47512d46ebdc3cbb")]
16+
[assembly: InternalsVisibleTo("coverlet.tests.projectsample.aspnet6.tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100757cf9291d78a82e5bb58a827a3c46c2f959318327ad30d1b52e918321ffbd847fb21565b8576d2a3a24562a93e86c77a298b564a0f1b98f63d7a1441a3a8bcc206da3ed09d5dacc76e122a109a9d3ac608e21a054d667a2bae98510a1f0f653c0e6f58f42b4b3934f6012f5ec4a09b3dfd3e14d437ede1424bdb722aead64ad")]
17+
[assembly: InternalsVisibleTo("coverlet.tests.projectsample.wpf6.tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100757cf9291d78a82e5bb58a827a3c46c2f959318327ad30d1b52e918321ffbd847fb21565b8576d2a3a24562a93e86c77a298b564a0f1b98f63d7a1441a3a8bcc206da3ed09d5dacc76e122a109a9d3ac608e21a054d667a2bae98510a1f0f653c0e6f58f42b4b3934f6012f5ec4a09b3dfd3e14d437ede1424bdb722aead64ad")]
18+
1619
// Needed to mock internal type https://github.com/Moq/moq4/wiki/Quickstart#advanced-features
17-
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")]
20+
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")]

test/coverlet.core.tests/Instrumentation/InstrumenterTests.cs

Lines changed: 7 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -619,24 +619,6 @@ public int SampleMethod()
619619
if (expectedExcludes) { loggerMock.Verify(l => l.LogVerbose(It.IsAny<string>())); }
620620
}
621621

622-
[Fact]
623-
public void TestInstrument_AspNetCoreSharedFrameworkResolver()
624-
{
625-
var resolver = new AspNetCoreSharedFrameworkResolver(_mockLogger.Object);
626-
var compilationLibrary = new CompilationLibrary(
627-
"package",
628-
"Microsoft.Extensions.Logging.Abstractions",
629-
"2.2.0",
630-
"sha512-B2WqEox8o+4KUOpL7rZPyh6qYjik8tHi2tN8Z9jZkHzED8ElYgZa/h6K+xliB435SqUcWT290Fr2aa8BtZjn8A==",
631-
Enumerable.Empty<string>(),
632-
Enumerable.Empty<Dependency>(),
633-
true);
634-
635-
var assemblies = new List<string>();
636-
Assert.True(resolver.TryResolveAssemblyPaths(compilationLibrary, assemblies));
637-
Assert.NotEmpty(assemblies);
638-
}
639-
640622
[Fact]
641623
public void TestInstrument_NetstandardAwareAssemblyResolver_PreserveCompilationContext()
642624
{
@@ -740,15 +722,15 @@ public void TestReachabilityHelper()
740722
new[]
741723
{
742724
// Throws
743-
7, 8,
725+
7, 8,
744726
// NoBranches
745-
12, 13, 14, 15, 16,
727+
12, 13, 14, 15, 16,
746728
// If
747-
19, 20, 22, 23, 24, 25, 26, 27, 29, 30,
729+
19, 20, 22, 23, 24, 25, 26, 27, 29, 30,
748730
// Switch
749-
33, 34, 36, 39, 40, 41, 42, 44, 45, 49, 50, 52, 53, 55, 56, 58, 59, 61, 62, 64, 65, 68, 69,
731+
33, 34, 36, 39, 40, 41, 42, 44, 45, 49, 50, 52, 53, 55, 56, 58, 59, 61, 62, 64, 65, 68, 69,
750732
// Subtle
751-
72, 73, 75, 78, 79, 80, 82, 83, 86, 87, 88, 91, 92, 95, 96, 98, 99, 101, 102, 103,
733+
72, 73, 75, 78, 79, 80, 82, 83, 86, 87, 88, 91, 92, 95, 96, 98, 99, 101, 102, 103,
752734
// UnreachableBranch
753735
106, 107, 108, 110, 111, 112, 113, 114,
754736
// ThrowsGeneric
@@ -774,7 +756,7 @@ public void TestReachabilityHelper()
774756
// Switch
775757
41, 42,
776758
// Subtle
777-
79, 80, 88, 96, 98, 99,
759+
79, 80, 88, 96, 98, 99,
778760
// UnreachableBranch
779761
110, 111, 112, 113, 114,
780762
// CallsGenericMethodDoesNotReturn
@@ -822,7 +804,7 @@ public void Instrumenter_MethodsWithoutReferenceToSource_AreSkipped()
822804

823805
var instrumenter = new Instrumenter(Path.Combine(directory.FullName, Path.GetFileName(module)), "_coverlet_tests_projectsample_vbmynamespace", parameters,
824806
loggerMock.Object, instrumentationHelper, new FileSystem(), new SourceRootTranslator(Path.Combine(directory.FullName, Path.GetFileName(module)), loggerMock.Object, new FileSystem(), new AssemblyAdapter()), new CecilSymbolHelper());
825-
807+
826808
instrumentationHelper.BackupOriginalModule(Path.Combine(directory.FullName, Path.GetFileName(module)), "_coverlet_tests_projectsample_vbmynamespace");
827809

828810
InstrumenterResult result = instrumenter.Instrument();

test/coverlet.core.tests/Instrumentation/ModuleTrackerTemplateTests.cs

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -123,26 +123,24 @@ public void MutexBlocksMultipleWriters()
123123
FunctionExecutor.Run(async () =>
124124
{
125125
using var ctx = new TrackerContext();
126-
using (var mutex = new Mutex(
127-
true, Path.GetFileNameWithoutExtension(ModuleTrackerTemplate.HitsFilePath) + "_Mutex", out bool createdNew))
128-
{
129-
Assert.True(createdNew);
126+
using var mutex = new Mutex(
127+
true, Path.GetFileNameWithoutExtension(ModuleTrackerTemplate.HitsFilePath) + "_Mutex", out bool createdNew);
128+
Assert.True(createdNew);
130129

131-
ModuleTrackerTemplate.HitsArray = new[] { 0, 1, 2, 3 };
132-
var unloadTask = Task.Run(() => ModuleTrackerTemplate.UnloadModule(null, null));
130+
ModuleTrackerTemplate.HitsArray = new[] { 0, 1, 2, 3 };
131+
var unloadTask = Task.Run(() => ModuleTrackerTemplate.UnloadModule(null, null));
133132

134-
Assert.False(unloadTask.Wait(5));
133+
Assert.False(unloadTask.Wait(5));
135134

136-
WriteHitsFile(new[] { 0, 3, 2, 1 });
135+
WriteHitsFile(new[] { 0, 3, 2, 1 });
137136

138-
Assert.False(unloadTask.Wait(5));
137+
Assert.False(unloadTask.Wait(5));
139138

140-
mutex.ReleaseMutex();
141-
await unloadTask;
139+
mutex.ReleaseMutex();
140+
await unloadTask;
142141

143-
int[] expectedHitsArray = new[] { 0, 4, 4, 4 };
144-
Assert.Equal(expectedHitsArray, ReadHitsFile());
145-
}
142+
int[] expectedHitsArray = new[] { 0, 4, 4, 4 };
143+
Assert.Equal(expectedHitsArray, ReadHitsFile());
146144

147145
return 0;
148146
});
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
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.Linq;
7+
using Coverlet.Core.Abstractions;
8+
using Coverlet.Core.Instrumentation;
9+
using Coverlet.Tests.Xunit.Extensions;
10+
using Microsoft.Extensions.DependencyModel;
11+
using Moq;
12+
using Xunit;
13+
14+
namespace Coverlet.Integration.Tests
15+
{
16+
public class WpfResolverTests : BaseTest
17+
{
18+
[ConditionalFact]
19+
[SkipOnOS(OS.Linux, "WPF only runs on Windows")]
20+
[SkipOnOS(OS.MacOS, "WPF only runs on Windows")]
21+
public void TestInstrument_NetCoreSharedFrameworkResolver()
22+
{
23+
string wpfProjectPath = "../../../../coverlet.tests.projectsample.wpf6";
24+
Assert.True(DotnetCli($"build \"{wpfProjectPath}\"", out string output, out string error));
25+
string assemblyLocation = Directory.GetFiles($"{wpfProjectPath}/bin", "coverlet.tests.projectsample.wpf6.dll", SearchOption.AllDirectories).First();
26+
27+
var mockLogger = new Mock<ILogger>();
28+
var resolver = new NetCoreSharedFrameworkResolver(assemblyLocation, mockLogger.Object);
29+
var compilationLibrary = new CompilationLibrary(
30+
"package",
31+
"System.Drawing",
32+
"0.0.0.0",
33+
"sha512-not-relevant",
34+
Enumerable.Empty<string>(),
35+
Enumerable.Empty<Dependency>(),
36+
true);
37+
38+
var assemblies = new List<string>();
39+
Assert.True(resolver.TryResolveAssemblyPaths(compilationLibrary, assemblies),
40+
"sample assembly shall be resolved");
41+
Assert.NotEmpty(assemblies);
42+
}
43+
}
44+
}

test/coverlet.integration.tests/coverlet.integration.tests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
<ItemGroup>
1010
<PackageReference Include="Microsoft.NET.Test.Sdk" />
11+
<PackageReference Include="Moq" />
1112
<PackageReference Include="NuGet.Packaging" />
1213
<PackageReference Include="xunit" />
1314
<PackageReference Include="xunit.runner.visualstudio" />
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
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.Reflection;
5+
6+
[assembly: AssemblyKeyFile("coverlet.tests.projectsample.aspnet6.tests.snk")]
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
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.Linq;
6+
using System.Reflection;
7+
using Coverlet.Core.Abstractions;
8+
using Coverlet.Core.Instrumentation;
9+
using Microsoft.Extensions.DependencyModel;
10+
using Moq;
11+
using Xunit;
12+
13+
namespace coverlet.tests.projectsample.aspnet6.tests
14+
{
15+
public class ResolverTests
16+
{
17+
[Fact]
18+
public void TestInstrument_NetCoreSharedFrameworkResolver()
19+
{
20+
Assembly assembly = GetType().Assembly;
21+
var mockLogger = new Mock<ILogger>();
22+
var resolver = new NetCoreSharedFrameworkResolver(assembly.Location, mockLogger.Object);
23+
var compilationLibrary = new CompilationLibrary(
24+
"package",
25+
"Microsoft.Extensions.Logging.Abstractions",
26+
"0.0.0.0",
27+
"sha512-not-relevant",
28+
Enumerable.Empty<string>(),
29+
Enumerable.Empty<Dependency>(),
30+
true);
31+
32+
var assemblies = new List<string>();
33+
Assert.True(resolver.TryResolveAssemblyPaths(compilationLibrary, assemblies),
34+
"sample assembly shall be resolved");
35+
Assert.NotEmpty(assemblies);
36+
}
37+
}
38+
}

0 commit comments

Comments
 (0)