diff --git a/Directory.Build.targets b/Directory.Build.targets
index 14eae9a2e..73e2a1a1c 100644
--- a/Directory.Build.targets
+++ b/Directory.Build.targets
@@ -3,6 +3,7 @@
+
diff --git a/src/coverlet.core/Exceptions.cs b/src/coverlet.core/Exceptions.cs
new file mode 100644
index 000000000..81a6809aa
--- /dev/null
+++ b/src/coverlet.core/Exceptions.cs
@@ -0,0 +1,26 @@
+using System;
+
+namespace Coverlet.Core.Exceptions
+{
+ [Serializable]
+ internal class CoverletException : Exception
+ {
+ public CoverletException() { }
+ public CoverletException(string message) : base(message) { }
+ public CoverletException(string message, System.Exception inner) : base(message, inner) { }
+ protected CoverletException(
+ System.Runtime.Serialization.SerializationInfo info,
+ System.Runtime.Serialization.StreamingContext context) : base(info, context) { }
+ }
+
+ [Serializable]
+ internal class CecilAssemblyResolutionException : CoverletException
+ {
+ public CecilAssemblyResolutionException() { }
+ public CecilAssemblyResolutionException(string message) : base(message) { }
+ public CecilAssemblyResolutionException(string message, System.Exception inner) : base(message, inner) { }
+ protected CecilAssemblyResolutionException(
+ System.Runtime.Serialization.SerializationInfo info,
+ System.Runtime.Serialization.StreamingContext context) : base(info, context) { }
+ }
+}
diff --git a/src/coverlet.core/Instrumentation/CecilAssemblyResolver.cs b/src/coverlet.core/Instrumentation/CecilAssemblyResolver.cs
new file mode 100644
index 000000000..60fb289e3
--- /dev/null
+++ b/src/coverlet.core/Instrumentation/CecilAssemblyResolver.cs
@@ -0,0 +1,262 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using Coverlet.Core.Abstracts;
+using Coverlet.Core.Exceptions;
+using Microsoft.Extensions.DependencyModel;
+using Microsoft.Extensions.DependencyModel.Resolution;
+using Mono.Cecil;
+
+namespace Coverlet.Core.Instrumentation
+{
+ ///
+ /// In case of testing different runtime i.e. netfx we could find netstandard.dll in folder.
+ /// netstandard.dll is a forward only lib, there is no IL but only forwards to "runtime" implementation.
+ /// For some classes implementation are in different assembly for different runtime for instance:
+ ///
+ /// For NetFx 4.7
+ /// // Token: 0x2700072C RID: 1836
+ /// .class extern forwarder System.Security.Cryptography.X509Certificates.StoreName
+ /// {
+ /// .assembly extern System
+ /// }
+ ///
+ /// For netcoreapp2.2
+ /// Token: 0x2700072C RID: 1836
+ /// .class extern forwarder System.Security.Cryptography.X509Certificates.StoreName
+ /// {
+ /// .assembly extern System.Security.Cryptography.X509Certificates
+ /// }
+ ///
+ /// There is a concrete possibility that Cecil cannot find implementation and throws StackOverflow exception https://github.com/jbevain/cecil/issues/575
+ /// This custom resolver check if requested lib is a "official" netstandard.dll and load once of "current runtime" with
+ /// correct forwards.
+ /// Check compares 'assembly name' and 'public key token', because versions could differ between runtimes.
+ ///
+ internal class NetstandardAwareAssemblyResolver : DefaultAssemblyResolver
+ {
+ private static readonly System.Reflection.Assembly _netStandardAssembly;
+ private static readonly string _name;
+ private static readonly byte[] _publicKeyToken;
+ private static readonly AssemblyDefinition _assemblyDefinition;
+
+ private readonly string _modulePath;
+ private readonly Lazy _compositeResolver;
+ private readonly ILogger _logger;
+
+ static NetstandardAwareAssemblyResolver()
+ {
+ try
+ {
+ // To be sure to load information of "real" runtime netstandard implementation
+ _netStandardAssembly = System.Reflection.Assembly.LoadFile(Path.Combine(Path.GetDirectoryName(typeof(object).Assembly.Location), "netstandard.dll"));
+ System.Reflection.AssemblyName name = _netStandardAssembly.GetName();
+ _name = name.Name;
+ _publicKeyToken = name.GetPublicKeyToken();
+ _assemblyDefinition = AssemblyDefinition.ReadAssembly(_netStandardAssembly.Location);
+ }
+ catch (FileNotFoundException)
+ {
+ // netstandard not supported
+ }
+ }
+
+ public NetstandardAwareAssemblyResolver(string modulePath, ILogger logger)
+ {
+ _modulePath = modulePath;
+ _logger = logger;
+
+ // this is lazy because we cannot create AspNetCoreSharedFrameworkResolver if not on .NET Core runtime,
+ // runtime folders are different
+ _compositeResolver = new Lazy(() => new CompositeCompilationAssemblyResolver(new ICompilationAssemblyResolver[]
+ {
+ new AppBaseCompilationAssemblyResolver(),
+ new ReferenceAssemblyPathResolver(),
+ new PackageCompilationAssemblyResolver(),
+ new AspNetCoreSharedFrameworkResolver(_logger)
+ }), true);
+ }
+
+ // Check name and public key but not version that could be different
+ private bool CheckIfSearchingNetstandard(AssemblyNameReference name)
+ {
+ if (_netStandardAssembly is null)
+ {
+ return false;
+ }
+
+ if (_name != name.Name)
+ {
+ return false;
+ }
+
+ if (name.PublicKeyToken.Length != _publicKeyToken.Length)
+ {
+ return false;
+ }
+
+ for (int i = 0; i < name.PublicKeyToken.Length; i++)
+ {
+ if (_publicKeyToken[i] != name.PublicKeyToken[i])
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ public override AssemblyDefinition Resolve(AssemblyNameReference name)
+ {
+ if (CheckIfSearchingNetstandard(name))
+ {
+ return _assemblyDefinition;
+ }
+ else
+ {
+ try
+ {
+ return base.Resolve(name);
+ }
+ catch (AssemblyResolutionException)
+ {
+ AssemblyDefinition asm = TryWithCustomResolverOnDotNetCore(name);
+
+ if (asm != null)
+ {
+ return asm;
+ }
+
+ throw;
+ }
+ }
+ }
+
+ private bool IsDotNetCore()
+ {
+ // object for .NET Framework is inside mscorlib.dll
+ return Path.GetFileName(typeof(object).Assembly.Location) == "System.Private.CoreLib.dll";
+ }
+
+ ///
+ ///
+ /// We try to manually load assembly.
+ /// To work test project needs to use
+ ///
+ ///
+ /// true
+ ///
+ ///
+ ///
+ internal AssemblyDefinition TryWithCustomResolverOnDotNetCore(AssemblyNameReference name)
+ {
+ _logger.LogVerbose($"TryWithCustomResolverOnDotNetCore for {name}");
+
+ if (!IsDotNetCore())
+ {
+ _logger.LogVerbose($"Not a dotnet core app");
+ return null;
+ }
+
+ if (string.IsNullOrEmpty(_modulePath))
+ {
+ throw new AssemblyResolutionException(name);
+ }
+
+ using DependencyContextJsonReader contextJsonReader = new DependencyContextJsonReader();
+ Dictionary> libraries = new Dictionary>();
+ foreach (string fileName in Directory.GetFiles(Path.GetDirectoryName(_modulePath), "*.deps.json"))
+ {
+ using FileStream depsFile = File.OpenRead(fileName);
+ _logger.LogVerbose($"Loading {fileName}");
+ DependencyContext dependencyContext = contextJsonReader.Read(depsFile);
+ foreach (CompilationLibrary library in dependencyContext.CompileLibraries)
+ {
+ // we're interested only on nuget package
+ if (library.Type == "project")
+ {
+ continue;
+ }
+
+ try
+ {
+ string path = library.ResolveReferencePaths(_compositeResolver.Value).FirstOrDefault();
+ if (string.IsNullOrEmpty(path))
+ {
+ continue;
+ }
+
+ // We could load more than one deps file, we need to check if lib is already found
+ if (!libraries.ContainsKey(library.Name))
+ {
+ libraries.Add(library.Name, new Lazy(() => AssemblyDefinition.ReadAssembly(path, new ReaderParameters() { AssemblyResolver = this })));
+ }
+ }
+ catch (Exception ex)
+ {
+ // if we don't find a lib go on
+ _logger.LogVerbose($"TryWithCustomResolverOnDotNetCore exception: {ex.ToString()}");
+ }
+ }
+ }
+
+ if (libraries.TryGetValue(name.Name, out Lazy asm))
+ {
+ return asm.Value;
+ }
+
+ throw new CecilAssemblyResolutionException($"AssemblyResolutionException for '{name}'. Try to add true to test projects or pass '/p:CopyLocalLockFileAssemblies=true' option to the 'dotnet test' command-line", new AssemblyResolutionException(name));
+ }
+ }
+
+ internal class AspNetCoreSharedFrameworkResolver : ICompilationAssemblyResolver
+ {
+ private readonly string[] _aspNetSharedFrameworkDirs = null;
+ private readonly ILogger _logger = null;
+
+ public AspNetCoreSharedFrameworkResolver(ILogger logger)
+ {
+ _logger = logger;
+ string runtimeRootPath = Path.GetDirectoryName(typeof(object).Assembly.Location);
+ string runtimeVersion = runtimeRootPath.Substring(runtimeRootPath.LastIndexOf(Path.DirectorySeparatorChar) + 1);
+ _aspNetSharedFrameworkDirs = new string[]
+ {
+ Path.GetFullPath(Path.Combine(runtimeRootPath,"../../Microsoft.AspNetCore.All", runtimeVersion)),
+ Path.GetFullPath(Path.Combine(runtimeRootPath, "../../Microsoft.AspNetCore.App", runtimeVersion))
+ };
+
+ _logger.LogVerbose("AspNetCoreSharedFrameworkResolver search paths:");
+ foreach (string searchPath in _aspNetSharedFrameworkDirs)
+ {
+ _logger.LogVerbose(searchPath);
+ }
+ }
+
+ public bool TryResolveAssemblyPaths(CompilationLibrary library, List assemblies)
+ {
+ string dllName = $"{library.Name}.dll";
+
+ foreach (string sharedFrameworkPath in _aspNetSharedFrameworkDirs)
+ {
+ if (!Directory.Exists(sharedFrameworkPath))
+ {
+ continue;
+ }
+
+ foreach (var file in Directory.GetFiles(sharedFrameworkPath))
+ {
+ if (Path.GetFileName(file).Equals(dllName, StringComparison.OrdinalIgnoreCase))
+ {
+ _logger.LogVerbose($"'{dllName}' found in '{file}'");
+ assemblies.Add(file);
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+ }
+}
diff --git a/src/coverlet.core/Instrumentation/Instrumenter.cs b/src/coverlet.core/Instrumentation/Instrumenter.cs
index 78388b1d1..824d83588 100644
--- a/src/coverlet.core/Instrumentation/Instrumenter.cs
+++ b/src/coverlet.core/Instrumentation/Instrumenter.cs
@@ -5,6 +5,7 @@
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
+
using Coverlet.Core.Abstracts;
using Coverlet.Core.Attributes;
using Coverlet.Core.Symbols;
@@ -141,7 +142,7 @@ public InstrumenterResult Instrument()
private void InstrumentModule()
{
using (var stream = _fileSystem.NewFileStream(_module, FileMode.Open, FileAccess.ReadWrite))
- using (var resolver = new NetstandardAwareAssemblyResolver())
+ using (var resolver = new NetstandardAwareAssemblyResolver(_module, _logger))
{
resolver.AddSearchDirectory(Path.GetDirectoryName(_module));
var parameters = new ReaderParameters { ReadSymbols = true, AssemblyResolver = resolver };
@@ -671,96 +672,6 @@ public MethodReference ImportReference(MethodReference method, IGenericParameter
}
}
- ///
- /// In case of testing different runtime i.e. netfx we could find netstandard.dll in folder.
- /// netstandard.dll is a forward only lib, there is no IL but only forwards to "runtime" implementation.
- /// For some classes implementation are in different assembly for different runtime for instance:
- ///
- /// For NetFx 4.7
- /// // Token: 0x2700072C RID: 1836
- /// .class extern forwarder System.Security.Cryptography.X509Certificates.StoreName
- /// {
- /// .assembly extern System
- /// }
- ///
- /// For netcoreapp2.2
- /// Token: 0x2700072C RID: 1836
- /// .class extern forwarder System.Security.Cryptography.X509Certificates.StoreName
- /// {
- /// .assembly extern System.Security.Cryptography.X509Certificates
- /// }
- ///
- /// There is a concrete possibility that Cecil cannot find implementation and throws StackOverflow exception https://github.com/jbevain/cecil/issues/575
- /// This custom resolver check if requested lib is a "official" netstandard.dll and load once of "current runtime" with
- /// correct forwards.
- /// Check compares 'assembly name' and 'public key token', because versions could differ between runtimes.
- ///
- internal class NetstandardAwareAssemblyResolver : DefaultAssemblyResolver
- {
- private static System.Reflection.Assembly _netStandardAssembly;
- private static string _name;
- private static byte[] _publicKeyToken;
- private static AssemblyDefinition _assemblyDefinition;
-
- static NetstandardAwareAssemblyResolver()
- {
- try
- {
- // To be sure to load information of "real" runtime netstandard implementation
- _netStandardAssembly = System.Reflection.Assembly.LoadFile(Path.Combine(Path.GetDirectoryName(typeof(object).Assembly.Location), "netstandard.dll"));
- System.Reflection.AssemblyName name = _netStandardAssembly.GetName();
- _name = name.Name;
- _publicKeyToken = name.GetPublicKeyToken();
- _assemblyDefinition = AssemblyDefinition.ReadAssembly(_netStandardAssembly.Location);
- }
- catch (FileNotFoundException)
- {
- // netstandard not supported
- }
- }
-
- // Check name and public key but not version that could be different
- private bool CheckIfSearchingNetstandard(AssemblyNameReference name)
- {
- if (_netStandardAssembly is null)
- {
- return false;
- }
-
- if (_name != name.Name)
- {
- return false;
- }
-
- if (name.PublicKeyToken.Length != _publicKeyToken.Length)
- {
- return false;
- }
-
- for (int i = 0; i < name.PublicKeyToken.Length; i++)
- {
- if (_publicKeyToken[i] != name.PublicKeyToken[i])
- {
- return false;
- }
- }
-
- return true;
- }
-
- public override AssemblyDefinition Resolve(AssemblyNameReference name)
- {
- if (CheckIfSearchingNetstandard(name))
- {
- return _assemblyDefinition;
- }
- else
- {
- return base.Resolve(name);
- }
- }
- }
-
// Exclude files helper https://docs.microsoft.com/en-us/dotnet/api/microsoft.extensions.filesystemglobbing.matcher?view=aspnetcore-2.2
internal class ExcludedFilesHelper
{
diff --git a/src/coverlet.core/coverlet.core.csproj b/src/coverlet.core/coverlet.core.csproj
index 0a7649292..4add60886 100644
--- a/src/coverlet.core/coverlet.core.csproj
+++ b/src/coverlet.core/coverlet.core.csproj
@@ -12,6 +12,7 @@
+
diff --git a/test/coverlet.core.tests/Instrumentation/InstrumenterTests.cs b/test/coverlet.core.tests/Instrumentation/InstrumenterTests.cs
index b920bc2c1..83ee7d5bc 100644
--- a/test/coverlet.core.tests/Instrumentation/InstrumenterTests.cs
+++ b/test/coverlet.core.tests/Instrumentation/InstrumenterTests.cs
@@ -14,6 +14,7 @@
using Mono.Cecil;
using Moq;
using Xunit;
+using Microsoft.Extensions.DependencyModel;
namespace Coverlet.Core.Instrumentation.Tests
{
@@ -200,7 +201,7 @@ class InstrumenterTest
[Fact]
public void TestInstrument_NetStandardAwareAssemblyResolver_FromRuntime()
{
- NetstandardAwareAssemblyResolver netstandardResolver = new NetstandardAwareAssemblyResolver();
+ NetstandardAwareAssemblyResolver netstandardResolver = new NetstandardAwareAssemblyResolver(null, _mockLogger.Object);
// We ask for "official" netstandard.dll implementation with know MS public key cc7b13ffcd2ddd51 same in all runtime
AssemblyDefinition resolved = netstandardResolver.Resolve(AssemblyNameReference.Parse("netstandard, Version=0.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51"));
@@ -234,7 +235,7 @@ public void TestInstrument_NetStandardAwareAssemblyResolver_FromFolder()
File.WriteAllBytes("netstandard.dll", dllStream.ToArray());
}
- NetstandardAwareAssemblyResolver netstandardResolver = new NetstandardAwareAssemblyResolver();
+ NetstandardAwareAssemblyResolver netstandardResolver = new NetstandardAwareAssemblyResolver(newAssemlby.Location, _mockLogger.Object);
AssemblyDefinition resolved = netstandardResolver.Resolve(AssemblyNameReference.Parse(newAssemlby.FullName));
// We check if final netstandard.dll resolved is local folder one and not "official" netstandard.dll
@@ -433,5 +434,31 @@ public void TestInstrument_AssemblyMarkedAsExcludeFromCodeCoverage()
Assert.Empty(result.Documents);
loggerMock.Verify(l => l.LogVerbose(It.IsAny()));
}
+
+ [Fact]
+ public void TestInstrument_AspNetCoreSharedFrameworkResolver()
+ {
+ AspNetCoreSharedFrameworkResolver resolver = new AspNetCoreSharedFrameworkResolver(_mockLogger.Object);
+ CompilationLibrary compilationLibrary = new CompilationLibrary(
+ "package",
+ "Microsoft.Extensions.Logging.Abstractions",
+ "2.2.0",
+ "sha512-B2WqEox8o+4KUOpL7rZPyh6qYjik8tHi2tN8Z9jZkHzED8ElYgZa/h6K+xliB435SqUcWT290Fr2aa8BtZjn8A==",
+ Enumerable.Empty(),
+ Enumerable.Empty(),
+ true);
+
+ List assemblies = new List();
+ Assert.True(resolver.TryResolveAssemblyPaths(compilationLibrary, assemblies));
+ Assert.NotEmpty(assemblies);
+ }
+
+ [Fact]
+ public void TestInstrument_NetstandardAwareAssemblyResolver_PreserveCompilationContext()
+ {
+ NetstandardAwareAssemblyResolver netstandardResolver = new NetstandardAwareAssemblyResolver(Assembly.GetExecutingAssembly().Location, _mockLogger.Object);
+ AssemblyDefinition asm = netstandardResolver.TryWithCustomResolverOnDotNetCore(new AssemblyNameReference("Microsoft.Extensions.Logging.Abstractions", new Version("2.2.0")));
+ Assert.NotNull(asm);
+ }
}
}
diff --git a/test/coverlet.core.tests/coverlet.core.tests.csproj b/test/coverlet.core.tests/coverlet.core.tests.csproj
index 5f9a7e6dc..c1d542915 100644
--- a/test/coverlet.core.tests/coverlet.core.tests.csproj
+++ b/test/coverlet.core.tests/coverlet.core.tests.csproj
@@ -6,6 +6,8 @@
false
preview
$(NoWarn);CS8002
+
+ true
@@ -24,6 +26,11 @@
+
+
+
+
+