diff --git a/src/NUnitEngine/nunit.engine.core/Internal/TestAssemblyLoadContext.cs b/src/NUnitEngine/nunit.engine.core/Internal/TestAssemblyLoadContext.cs index 69d432fb4..cf9ed03f9 100644 --- a/src/NUnitEngine/nunit.engine.core/Internal/TestAssemblyLoadContext.cs +++ b/src/NUnitEngine/nunit.engine.core/Internal/TestAssemblyLoadContext.cs @@ -12,6 +12,8 @@ namespace NUnit.Engine.Internal { internal sealed class TestAssemblyLoadContext : AssemblyLoadContext { + private static readonly Logger log = InternalTrace.GetLogger(typeof(TestAssemblyLoadContext)); + private readonly string _testAssemblyPath; private readonly string _basePath; private readonly TestAssemblyResolver _resolver; @@ -27,19 +29,16 @@ public TestAssemblyLoadContext(string testAssemblyPath) protected override Assembly Load(AssemblyName name) { - var assemblies = AppDomain.CurrentDomain.GetAssemblies(); - var loadedAssembly = assemblies.FirstOrDefault(x => x.GetName().Name == name.Name); - if (loadedAssembly != null) - { - return loadedAssembly; - } + log.Debug("Loading {0} assembly", name); - loadedAssembly = base.Load(name); + var loadedAssembly = base.Load(name); if (loadedAssembly != null) { + log.Info("Assembly {0} ({1}) is loaded using default base.Load()", name, GetAssemblyLocationInfo(loadedAssembly)); return loadedAssembly; } + var runtimeResolverPath = _runtimeResolver.ResolveAssemblyToPath(name); if (string.IsNullOrEmpty(runtimeResolverPath) == false && File.Exists(runtimeResolverPath)) @@ -49,12 +48,15 @@ protected override Assembly Load(AssemblyName name) if (loadedAssembly != null) { + log.Info("Assembly {0} ({1}) is loaded using the deps.json info", name, GetAssemblyLocationInfo(loadedAssembly)); return loadedAssembly; } loadedAssembly = _resolver.Resolve(this, name); if (loadedAssembly != null) { + log.Info("Assembly {0} ({1}) is loaded using the TestAssembliesResolver", name, GetAssemblyLocationInfo(loadedAssembly)); + return loadedAssembly; } @@ -68,8 +70,29 @@ protected override Assembly Load(AssemblyName name) loadedAssembly = LoadFromAssemblyPath(assemblyPath); } + if (loadedAssembly != null) + { + log.Info("Assembly {0} ({1}) is loaded using base path", name, GetAssemblyLocationInfo(loadedAssembly)); + return loadedAssembly; + } + return loadedAssembly; } + + private static string GetAssemblyLocationInfo(Assembly assembly) + { + if (assembly.IsDynamic) + { + return $"Dynamic {assembly.FullName}"; + } + + if (string.IsNullOrEmpty(assembly.Location)) + { + return $"No location for {assembly.FullName}"; + } + + return $"{assembly.FullName} from {assembly.Location}"; + } } } diff --git a/src/NUnitEngine/nunit.engine.core/Internal/TestAssemblyResolver.cs b/src/NUnitEngine/nunit.engine.core/Internal/TestAssemblyResolver.cs index c5c28f4d7..c3cfc570a 100644 --- a/src/NUnitEngine/nunit.engine.core/Internal/TestAssemblyResolver.cs +++ b/src/NUnitEngine/nunit.engine.core/Internal/TestAssemblyResolver.cs @@ -17,6 +17,8 @@ namespace NUnit.Engine.Internal { internal sealed class TestAssemblyResolver : IDisposable { + private static readonly Logger log = InternalTrace.GetLogger(typeof(TestAssemblyResolver)); + private readonly ICompilationAssemblyResolver _assemblyResolver; private readonly DependencyContext _dependencyContext; private readonly AssemblyLoadContext _loadContext; @@ -62,6 +64,14 @@ public Assembly Resolve(AssemblyLoadContext context, AssemblyName name) private Assembly OnResolving(AssemblyLoadContext context, AssemblyName name) { + context = context ?? _loadContext; + + if (TryLoadFromTrustedPlatformAssemblies(context, name, out var loadedAssembly)) + { + log.Info("'{0}' assembly is loaded from trusted path '{1}'", name, loadedAssembly.Location); + return loadedAssembly; + } + foreach (var library in _dependencyContext.RuntimeLibraries) { var wrapper = new CompilationLibrary( @@ -79,24 +89,81 @@ private Assembly OnResolving(AssemblyLoadContext context, AssemblyName name) foreach (var assemblyPath in assemblies) { if (name.Name == Path.GetFileNameWithoutExtension(assemblyPath)) - return _loadContext.LoadFromAssemblyPath(assemblyPath); + { + loadedAssembly = context.LoadFromAssemblyPath(assemblyPath); + log.Info("'{0}' ({1}) assembly is loaded from runtime libraries {2} dependencies", + name, + loadedAssembly.Location, + library.Name); + + return loadedAssembly; + } } } + if (name.Version == null) + { + return null; + } + foreach (string frameworkDirectory in AdditionalFrameworkDirectories) { var versionDir = FindBestVersionDir(frameworkDirectory, name.Version); + if (versionDir != null) { string candidate = Path.Combine(frameworkDirectory, versionDir, name.Name + ".dll"); if (File.Exists(candidate)) - return _loadContext.LoadFromAssemblyPath(candidate); + { + loadedAssembly = context.LoadFromAssemblyPath(candidate); + log.Info("'{0}' ({1}) assembly is loaded from AdditionalFrameworkDirectory {2} dependencies with best candidate version {3}", + name, + loadedAssembly.Location, + frameworkDirectory, + versionDir); + + return loadedAssembly; + } + else + { + log.Debug("Best version dir for {0} is {1}, but there is no {2} file", frameworkDirectory, versionDir, candidate); + } } } + log.Info("Cannot resolve assembly '{0}'", name); return null; } + private static bool TryLoadFromTrustedPlatformAssemblies(AssemblyLoadContext context, AssemblyName assemblyName, out Assembly loadedAssembly) + { + // https://learn.microsoft.com/en-us/dotnet/core/dependency-loading/default-probing + loadedAssembly = null; + var trustedAssemblies = System.AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES") as string; + if (string.IsNullOrEmpty(trustedAssemblies)) + { + return false; + } + + var separator = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? ";" : ":"; + foreach (var assemblyPath in trustedAssemblies.Split(separator)) + { + var fileName = Path.GetFileNameWithoutExtension(assemblyPath); + if (string.Equals(fileName, assemblyName.Name, StringComparison.InvariantCultureIgnoreCase) == false) + { + continue; + } + + if (File.Exists(assemblyPath)) + { + loadedAssembly = context.LoadFromAssemblyPath(assemblyPath); + return true; + } + } + + return false; + } + private static string GetDotNetInstallDirectory() { if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) @@ -119,9 +186,11 @@ private static string FindBestVersionDir(string libraryDir, Version targetVersio { Version version; if (TryGetVersionFromString(Path.GetFileName(subdir), out version)) + { if (version >= targetVersion) if (bestVersion.Major == 0 || bestVersion > version) bestVersion = version; + } } return bestVersion.Major > 0