Skip to content

Fix resolving assemblies from frameworks not referenced by coverlet itself #1449

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jul 23, 2023
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
349 changes: 185 additions & 164 deletions coverlet.sln

Large diffs are not rendered by default.

77 changes: 63 additions & 14 deletions src/coverlet.core/Instrumentation/CecilAssemblyResolver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
using Microsoft.Extensions.DependencyModel;
using Microsoft.Extensions.DependencyModel.Resolution;
using Mono.Cecil;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

namespace Coverlet.Core.Instrumentation
{
Expand Down Expand Up @@ -70,14 +72,14 @@ public NetstandardAwareAssemblyResolver(string modulePath, ILogger logger)
_modulePath = modulePath;
_logger = logger;

// this is lazy because we cannot create AspNetCoreSharedFrameworkResolver if not on .NET Core runtime,
// this is lazy because we cannot create NetCoreSharedFrameworkResolver if not on .NET Core runtime,
// runtime folders are different
_compositeResolver = new Lazy<CompositeCompilationAssemblyResolver>(() => new CompositeCompilationAssemblyResolver(new ICompilationAssemblyResolver[]
{
new AppBaseCompilationAssemblyResolver(),
new ReferenceAssemblyPathResolver(),
new PackageCompilationAssemblyResolver(),
new AspNetCoreSharedFrameworkResolver(_logger)
new NetCoreSharedFrameworkResolver(modulePath, _logger),
new ReferenceAssemblyPathResolver(),
}), true);
}

Expand Down Expand Up @@ -216,23 +218,37 @@ internal AssemblyDefinition TryWithCustomResolverOnDotNetCore(AssemblyNameRefere
}
}

internal class AspNetCoreSharedFrameworkResolver : ICompilationAssemblyResolver
internal class NetCoreSharedFrameworkResolver : ICompilationAssemblyResolver
{
private readonly string[] _aspNetSharedFrameworkDirs;
private readonly List<string> _aspNetSharedFrameworkDirs = new();
private readonly ILogger _logger;

public AspNetCoreSharedFrameworkResolver(ILogger logger)
public NetCoreSharedFrameworkResolver(string modulePath, ILogger logger)
{
_logger = logger;
string runtimeRootPath = Path.GetDirectoryName(typeof(object).Assembly.Location);
string runtimeVersion = runtimeRootPath.Substring(runtimeRootPath.LastIndexOf(Path.DirectorySeparatorChar) + 1);
_aspNetSharedFrameworkDirs = new string[]

string runtimeConfigFile = Path.Combine(
Path.GetDirectoryName(modulePath)!,
Path.GetFileNameWithoutExtension(modulePath) + ".runtimeconfig.json");
if (!File.Exists(runtimeConfigFile))
{
return;
}

var reader = new RuntimeConfigurationReader(runtimeConfigFile);
IEnumerable<(string Name, string Version)> referencedFrameworks = reader.GetFrameworks();
string runtimePath = Path.GetDirectoryName(typeof(object).Assembly.Location);
string runtimeRootPath = Path.Combine(runtimePath!, "../..");
foreach ((string frameworkName, string frameworkVersion) in referencedFrameworks)
{
Path.GetFullPath(Path.Combine(runtimeRootPath,"../../Microsoft.AspNetCore.All", runtimeVersion)),
Path.GetFullPath(Path.Combine(runtimeRootPath, "../../Microsoft.AspNetCore.App", runtimeVersion))
};
var majorVersion = string.Join(".", frameworkVersion.Split('.').Take(2)) + ".";
var directory = new DirectoryInfo(Path.Combine(runtimeRootPath, frameworkName));
var latestVersion = directory.GetDirectories().Where(x => x.Name.StartsWith(majorVersion))
.Select(x => Convert.ToUInt32(x.Name.Substring(majorVersion.Length))).Max();
_aspNetSharedFrameworkDirs.Add(Path.Combine(directory.FullName, majorVersion + latestVersion));
}

_logger.LogVerbose("AspNetCoreSharedFrameworkResolver search paths:");
_logger.LogVerbose("NetCoreSharedFrameworkResolver search paths:");
foreach (string searchPath in _aspNetSharedFrameworkDirs)
{
_logger.LogVerbose(searchPath);
Expand All @@ -250,7 +266,8 @@ public bool TryResolveAssemblyPaths(CompilationLibrary library, List<string> ass
continue;
}

foreach (string file in Directory.GetFiles(sharedFrameworkPath))
string[] files = Directory.GetFiles(sharedFrameworkPath);
foreach (string file in files)
{
if (Path.GetFileName(file).Equals(dllName, StringComparison.OrdinalIgnoreCase))
{
Expand All @@ -264,4 +281,36 @@ public bool TryResolveAssemblyPaths(CompilationLibrary library, List<string> ass
return false;
}
}

internal class RuntimeConfigurationReader
{
private readonly string _runtimeConfigFile;

public RuntimeConfigurationReader(string runtimeConfigFile)
{
_runtimeConfigFile = runtimeConfigFile;
}

public IEnumerable<(string Name, string Version)> GetFrameworks()
{
JObject configuration =
new JsonSerializer().Deserialize<JObject>(
new JsonTextReader(new StringReader(File.ReadAllText(_runtimeConfigFile))));

JToken runtimeOptions = configuration["runtimeOptions"];
JToken framework = runtimeOptions?["framework"];
if (framework != null)
{
return new[] {(framework["name"].Value<string>(), framework["version"].Value<string>())};
}

JToken frameworks = runtimeOptions?["frameworks"];
if (frameworks != null)
{
return frameworks.Select(x => (x["name"].Value<string>(), x["version"].Value<string>()));
}

throw new InvalidOperationException($"Unable to read runtime configuration from {_runtimeConfigFile}.");
}
}
}
2 changes: 2 additions & 0 deletions src/coverlet.core/Instrumentation/InstrumenterResult.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.Serialization;

namespace Coverlet.Core.Instrumentation
Expand Down Expand Up @@ -79,6 +80,7 @@ public Document()

[DebuggerDisplay("isBranch = {isBranch} docIndex = {docIndex} start = {start} end = {end}")]
[DataContract]
[SuppressMessage("Style", "IDE1006", Justification = "suppress casing error for API compatibility")]
internal class HitCandidate
{
public HitCandidate(bool isBranch, int docIndex, int start, int end) => (this.isBranch, this.docIndex, this.start, this.end) = (isBranch, docIndex, start, end);
Expand Down
5 changes: 4 additions & 1 deletion src/coverlet.core/Properties/AssemblyInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,8 @@
[assembly: InternalsVisibleTo("coverlet.core.tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100757cf9291d78a82e5bb58a827a3c46c2f959318327ad30d1b52e918321ffbd847fb21565b8576d2a3a24562a93e86c77a298b564a0f1b98f63d7a1441a3a8bcc206da3ed09d5dacc76e122a109a9d3ac608e21a054d667a2bae98510a1f0f653c0e6f58f42b4b3934f6012f5ec4a09b3dfd3e14d437ede1424bdb722aead64ad")]
[assembly: InternalsVisibleTo("coverlet.collector.tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100ed0ed6af9693182615b8dcadc83c918b8d36312f86cefc69539d67d4189cd1b89420e7c3871802ffef7f5ca7816c68ad856c77bf7c230cc07824d96aa5d1237eebd30e246b9a14e22695fb26b40c800f74ea96619092cbd3a5d430d6c003fc7a82e8ccd1e315b935105d9232fe9e99e8d7ff54bba6f191959338d4a3169df9b3")]
[assembly: InternalsVisibleTo("coverlet.integration.tests, PublicKey=002400000480000094000000060200000024000052534131000400000100010001d24efbe9cbc2dc49b7a3d2ae34ca37cfb69b4f450acd768a22ce5cd021c8a38ae7dc68b2809a1ac606ad531b578f192a5690b2986990cbda4dd84ec65a3a4c1c36f6d7bb18f08592b93091535eaee2f0c8e48763ed7f190db2008e1f9e0facd5c0df5aaab74febd3430e09a428a72e5e6b88357f92d78e47512d46ebdc3cbb")]
[assembly: InternalsVisibleTo("coverlet.tests.projectsample.aspnet6.tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100757cf9291d78a82e5bb58a827a3c46c2f959318327ad30d1b52e918321ffbd847fb21565b8576d2a3a24562a93e86c77a298b564a0f1b98f63d7a1441a3a8bcc206da3ed09d5dacc76e122a109a9d3ac608e21a054d667a2bae98510a1f0f653c0e6f58f42b4b3934f6012f5ec4a09b3dfd3e14d437ede1424bdb722aead64ad")]
[assembly: InternalsVisibleTo("coverlet.tests.projectsample.wpf6.tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100757cf9291d78a82e5bb58a827a3c46c2f959318327ad30d1b52e918321ffbd847fb21565b8576d2a3a24562a93e86c77a298b564a0f1b98f63d7a1441a3a8bcc206da3ed09d5dacc76e122a109a9d3ac608e21a054d667a2bae98510a1f0f653c0e6f58f42b4b3934f6012f5ec4a09b3dfd3e14d437ede1424bdb722aead64ad")]

// Needed to mock internal type https://github.com/Moq/moq4/wiki/Quickstart#advanced-features
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")]
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")]
32 changes: 7 additions & 25 deletions test/coverlet.core.tests/Instrumentation/InstrumenterTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -619,24 +619,6 @@ public int SampleMethod()
if (expectedExcludes) { loggerMock.Verify(l => l.LogVerbose(It.IsAny<string>())); }
}

[Fact]
public void TestInstrument_AspNetCoreSharedFrameworkResolver()
{
var resolver = new AspNetCoreSharedFrameworkResolver(_mockLogger.Object);
var compilationLibrary = new CompilationLibrary(
"package",
"Microsoft.Extensions.Logging.Abstractions",
"2.2.0",
"sha512-B2WqEox8o+4KUOpL7rZPyh6qYjik8tHi2tN8Z9jZkHzED8ElYgZa/h6K+xliB435SqUcWT290Fr2aa8BtZjn8A==",
Enumerable.Empty<string>(),
Enumerable.Empty<Dependency>(),
true);

var assemblies = new List<string>();
Assert.True(resolver.TryResolveAssemblyPaths(compilationLibrary, assemblies));
Assert.NotEmpty(assemblies);
}

[Fact]
public void TestInstrument_NetstandardAwareAssemblyResolver_PreserveCompilationContext()
{
Expand Down Expand Up @@ -740,15 +722,15 @@ public void TestReachabilityHelper()
new[]
{
// Throws
7, 8,
7, 8,
// NoBranches
12, 13, 14, 15, 16,
12, 13, 14, 15, 16,
// If
19, 20, 22, 23, 24, 25, 26, 27, 29, 30,
19, 20, 22, 23, 24, 25, 26, 27, 29, 30,
// Switch
33, 34, 36, 39, 40, 41, 42, 44, 45, 49, 50, 52, 53, 55, 56, 58, 59, 61, 62, 64, 65, 68, 69,
33, 34, 36, 39, 40, 41, 42, 44, 45, 49, 50, 52, 53, 55, 56, 58, 59, 61, 62, 64, 65, 68, 69,
// Subtle
72, 73, 75, 78, 79, 80, 82, 83, 86, 87, 88, 91, 92, 95, 96, 98, 99, 101, 102, 103,
72, 73, 75, 78, 79, 80, 82, 83, 86, 87, 88, 91, 92, 95, 96, 98, 99, 101, 102, 103,
// UnreachableBranch
106, 107, 108, 110, 111, 112, 113, 114,
// ThrowsGeneric
Expand All @@ -774,7 +756,7 @@ public void TestReachabilityHelper()
// Switch
41, 42,
// Subtle
79, 80, 88, 96, 98, 99,
79, 80, 88, 96, 98, 99,
// UnreachableBranch
110, 111, 112, 113, 114,
// CallsGenericMethodDoesNotReturn
Expand Down Expand Up @@ -822,7 +804,7 @@ public void Instrumenter_MethodsWithoutReferenceToSource_AreSkipped()

var instrumenter = new Instrumenter(Path.Combine(directory.FullName, Path.GetFileName(module)), "_coverlet_tests_projectsample_vbmynamespace", parameters,
loggerMock.Object, instrumentationHelper, new FileSystem(), new SourceRootTranslator(Path.Combine(directory.FullName, Path.GetFileName(module)), loggerMock.Object, new FileSystem(), new AssemblyAdapter()), new CecilSymbolHelper());

instrumentationHelper.BackupOriginalModule(Path.Combine(directory.FullName, Path.GetFileName(module)), "_coverlet_tests_projectsample_vbmynamespace");

InstrumenterResult result = instrumenter.Instrument();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,26 +123,24 @@ public void MutexBlocksMultipleWriters()
FunctionExecutor.Run(async () =>
{
using var ctx = new TrackerContext();
using (var mutex = new Mutex(
true, Path.GetFileNameWithoutExtension(ModuleTrackerTemplate.HitsFilePath) + "_Mutex", out bool createdNew))
{
Assert.True(createdNew);
using var mutex = new Mutex(
true, Path.GetFileNameWithoutExtension(ModuleTrackerTemplate.HitsFilePath) + "_Mutex", out bool createdNew);
Assert.True(createdNew);

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

Assert.False(unloadTask.Wait(5));
Assert.False(unloadTask.Wait(5));

WriteHitsFile(new[] { 0, 3, 2, 1 });
WriteHitsFile(new[] { 0, 3, 2, 1 });

Assert.False(unloadTask.Wait(5));
Assert.False(unloadTask.Wait(5));

mutex.ReleaseMutex();
await unloadTask;
mutex.ReleaseMutex();
await unloadTask;

int[] expectedHitsArray = new[] { 0, 4, 4, 4 };
Assert.Equal(expectedHitsArray, ReadHitsFile());
}
int[] expectedHitsArray = new[] { 0, 4, 4, 4 };
Assert.Equal(expectedHitsArray, ReadHitsFile());

return 0;
});
Expand Down
44 changes: 44 additions & 0 deletions test/coverlet.integration.tests/WpfResolverTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// 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.Linq;
using Coverlet.Core.Abstractions;
using Coverlet.Core.Instrumentation;
using Coverlet.Tests.Xunit.Extensions;
using Microsoft.Extensions.DependencyModel;
using Moq;
using Xunit;

namespace Coverlet.Integration.Tests
{
public class WpfResolverTests : BaseTest
{
[ConditionalFact]
[SkipOnOS(OS.Linux, "WPF only runs on Windows")]
[SkipOnOS(OS.MacOS, "WPF only runs on Windows")]
public void TestInstrument_NetCoreSharedFrameworkResolver()
{
string wpfProjectPath = "../../../../coverlet.tests.projectsample.wpf6";
Assert.True(DotnetCli($"build \"{wpfProjectPath}\"", out string output, out string error));
string assemblyLocation = Directory.GetFiles($"{wpfProjectPath}/bin", "coverlet.tests.projectsample.wpf6.dll", SearchOption.AllDirectories).First();

var mockLogger = new Mock<ILogger>();
var resolver = new NetCoreSharedFrameworkResolver(assemblyLocation, mockLogger.Object);
var compilationLibrary = new CompilationLibrary(
"package",
"System.Drawing",
"0.0.0.0",
"sha512-not-relevant",
Enumerable.Empty<string>(),
Enumerable.Empty<Dependency>(),
true);

var assemblies = new List<string>();
Assert.True(resolver.TryResolveAssemblyPaths(compilationLibrary, assemblies),
"sample assembly shall be resolved");
Assert.NotEmpty(assemblies);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="Moq" />
<PackageReference Include="NuGet.Packaging" />
<PackageReference Include="xunit" />
<PackageReference Include="xunit.runner.visualstudio" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// Copyright (c) Toni Solarin-Sodara
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.Reflection;

[assembly: AssemblyKeyFile("coverlet.tests.projectsample.aspnet6.tests.snk")]
38 changes: 38 additions & 0 deletions test/coverlet.tests.projectsample.aspnet6.tests/ResolverTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// 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.Linq;
using System.Reflection;
using Coverlet.Core.Abstractions;
using Coverlet.Core.Instrumentation;
using Microsoft.Extensions.DependencyModel;
using Moq;
using Xunit;

namespace coverlet.tests.projectsample.aspnet6.tests
{
public class ResolverTests
{
[Fact]
public void TestInstrument_NetCoreSharedFrameworkResolver()
{
Assembly assembly = GetType().Assembly;
var mockLogger = new Mock<ILogger>();
var resolver = new NetCoreSharedFrameworkResolver(assembly.Location, mockLogger.Object);
var compilationLibrary = new CompilationLibrary(
"package",
"Microsoft.Extensions.Logging.Abstractions",
"0.0.0.0",
"sha512-not-relevant",
Enumerable.Empty<string>(),
Enumerable.Empty<Dependency>(),
true);

var assemblies = new List<string>();
Assert.True(resolver.TryResolveAssemblyPaths(compilationLibrary, assemblies),
"sample assembly shall be resolved");
Assert.NotEmpty(assemblies);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="Moq" />
<PackageReference Include="xunit" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\coverlet.core\coverlet.core.csproj" />
<ProjectReference Include="..\coverlet.tests.projectsample.aspnet6\coverlet.tests.projectsample.aspnet6.csproj" />
</ItemGroup>


</Project>
Binary file not shown.
23 changes: 23 additions & 0 deletions test/coverlet.tests.projectsample.aspnet6/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// Copyright (c) Toni Solarin-Sodara
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;

namespace coverlet.tests.projectsample.aspnet6
{
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}

public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
}
}
Loading