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 @@ + + + + +