diff --git a/src/NUnitCommon/nunit.agent.core/Drivers/NUnitFrameworkApi2018.cs b/src/NUnitCommon/nunit.agent.core/Drivers/NUnitFrameworkApi2018.cs index 5d05c8b1c..f366e5e21 100644 --- a/src/NUnitCommon/nunit.agent.core/Drivers/NUnitFrameworkApi2018.cs +++ b/src/NUnitCommon/nunit.agent.core/Drivers/NUnitFrameworkApi2018.cs @@ -4,8 +4,9 @@ using System.Collections.Generic; using System.IO; using System.Reflection; - +using NUnit.Common; #if NETCOREAPP +using System.Runtime.Loader; using NUnit.Engine.Internal; #endif @@ -40,10 +41,12 @@ public class NUnitFrameworkApi2018 : NUnitFrameworkApi private Type? _frameworkControllerType; #if NETCOREAPP - private TestAssemblyLoadContext? _assemblyLoadContext; + private AssemblyLoadContext? _assemblyLoadContext; + private TestAssemblyResolver? _testAssemblyResolver; + private Assembly? _testAssembly; private Assembly? _frameworkAssembly; - internal List? ResolutionStrategies => _assemblyLoadContext?.ResolutionStrategies; + internal List? ResolutionStrategies => _testAssemblyResolver?.ResolutionStrategies; #endif private string? _testAssemblyPath; @@ -67,6 +70,10 @@ public string Load(string testAssemblyPath, IDictionary settings _testAssemblyPath = Path.GetFullPath(testAssemblyPath); var idPrefix = _driverId + "-"; + bool useDefaultAssemblyLoadContext = false; + if (settings.TryGetValue(SettingDefinitions.UseDefaultAssemblyLoadContext, out var val)) + useDefaultAssemblyLoadContext = (bool)val; + #if NETFRAMEWORK try { @@ -95,12 +102,46 @@ public string Load(string testAssemblyPath, IDictionary settings var controllerAssembly = _frameworkControllerType?.Assembly?.GetName(); log.Debug($"Controller assembly is {controllerAssembly}"); #else - _assemblyLoadContext = new TestAssemblyLoadContext(testAssemblyPath); + try + { + _testAssembly = AssemblyHelper.FindLoadedAssemblyByPath(_testAssemblyPath); + + if (_testAssembly is not null) + { + _assemblyLoadContext = AssemblyLoadContext.GetLoadContext(_testAssembly); + log.Debug($" Already loaded in context {_assemblyLoadContext}"); + } + else + { + _assemblyLoadContext = useDefaultAssemblyLoadContext + ? AssemblyLoadContext.Default + : new AssemblyLoadContext(Path.GetFileNameWithoutExtension(testAssemblyPath)); + _testAssembly = _assemblyLoadContext.LoadFromAssemblyPath(testAssemblyPath); + log.Debug($" Loaded into new context {_assemblyLoadContext}"); + } + + _testAssemblyResolver = new TestAssemblyResolver(_assemblyLoadContext.ShouldNotBeNull(), testAssemblyPath); + } + catch (Exception e) + { + var msg = $"Failed to load test assembly {testAssemblyPath}"; + log.Error(msg); + throw new NUnitEngineException(msg, e); + } + log.Debug($"Loaded {testAssemblyPath}"); - var testAssembly = LoadAssembly(testAssemblyPath); - _frameworkAssembly = LoadAssembly(_nunitRef); + try + { + _frameworkAssembly = LoadAssembly(_nunitRef); + } + catch (Exception e) + { + log.Error($"{FAILED_TO_LOAD_NUNIT}\r\n{e}"); + throw new NUnitEngineException(FAILED_TO_LOAD_NUNIT, e); + } + log.Debug("Loaded nunit.framework"); - _frameworkController = CreateInstance(CONTROLLER_TYPE, testAssembly, idPrefix, settings); + _frameworkController = CreateInstance(CONTROLLER_TYPE, _testAssembly, idPrefix, settings); if (_frameworkController is null) { log.Error(INVALID_FRAMEWORK_MESSAGE); diff --git a/src/NUnitCommon/nunit.agent.core/TestAssemblyLoadContext.cs b/src/NUnitCommon/nunit.agent.core/TestAssemblyLoadContext.cs index a2388a8c0..354d44ed9 100644 --- a/src/NUnitCommon/nunit.agent.core/TestAssemblyLoadContext.cs +++ b/src/NUnitCommon/nunit.agent.core/TestAssemblyLoadContext.cs @@ -1,6 +1,6 @@ // Copyright (c) Charlie Poole, Rob Prouse and Contributors. MIT License - see LICENSE.txt -#if NETCOREAPP3_1_OR_GREATER +#if NETCOREAPP3_1_OR_GREATER && false using System.Reflection; using System.Runtime.InteropServices; diff --git a/src/NUnitCommon/nunit.agent.core/TestAssemblyResolver.cs b/src/NUnitCommon/nunit.agent.core/TestAssemblyResolver.cs index 81958c518..e983d41ed 100644 --- a/src/NUnitCommon/nunit.agent.core/TestAssemblyResolver.cs +++ b/src/NUnitCommon/nunit.agent.core/TestAssemblyResolver.cs @@ -25,6 +25,8 @@ internal sealed class TestAssemblyResolver : IDisposable private static readonly Logger log = InternalTrace.GetLogger(typeof(TestAssemblyResolver)); private readonly AssemblyLoadContext _loadContext; + private readonly string _basePath; + private readonly AssemblyDependencyResolver _assemblyDependencyResolver; // Our Strategies for resolving references internal List ResolutionStrategies = new List(); @@ -32,6 +34,11 @@ internal sealed class TestAssemblyResolver : IDisposable public TestAssemblyResolver(AssemblyLoadContext loadContext, string testAssemblyPath) { _loadContext = loadContext; + _basePath = Path.GetDirectoryName(testAssemblyPath).ShouldNotBeNull(); + _assemblyDependencyResolver = new AssemblyDependencyResolver(testAssemblyPath); +#if NET8_0_OR_GREATER + AppContext.SetData("APP_CONTEXT_BASE_DIRECTORY", _basePath); +#endif InitializeResolutionStrategies(loadContext, testAssemblyPath); @@ -76,15 +83,21 @@ public void Dispose() _loadContext.Resolving -= OnResolving; } - public Assembly? Resolve(AssemblyLoadContext context, AssemblyName assemblyName) - { - return OnResolving(context, assemblyName); - } - private Assembly? OnResolving(AssemblyLoadContext loadContext, AssemblyName assemblyName) { Guard.ArgumentNotNull(loadContext); + var runtimeResolverPath = _assemblyDependencyResolver.ResolveAssemblyToPath(assemblyName); + if (!string.IsNullOrEmpty(runtimeResolverPath) && File.Exists(runtimeResolverPath)) + { + var loadedAssembly = _loadContext.LoadFromAssemblyPath(runtimeResolverPath); + if (loadedAssembly is not null) + { + log.Info($"Assembly {assemblyName} ({loadedAssembly}) is loaded using the deps.json info"); + return loadedAssembly; + } + } + foreach (var strategy in ResolutionStrategies) { strategy.Calls++; diff --git a/src/NUnitCommon/nunit.agent.core/nunit.agent.core.csproj b/src/NUnitCommon/nunit.agent.core/nunit.agent.core.csproj index 12e4afd81..9c3c49e43 100644 --- a/src/NUnitCommon/nunit.agent.core/nunit.agent.core.csproj +++ b/src/NUnitCommon/nunit.agent.core/nunit.agent.core.csproj @@ -27,12 +27,10 @@ - - diff --git a/src/NUnitCommon/nunit.common/AssemblyHelper.cs b/src/NUnitCommon/nunit.common/AssemblyHelper.cs index 7689ffcd5..a82ce5aac 100644 --- a/src/NUnitCommon/nunit.common/AssemblyHelper.cs +++ b/src/NUnitCommon/nunit.common/AssemblyHelper.cs @@ -2,6 +2,7 @@ using System; using System.IO; +using System.Linq; using System.Reflection; namespace NUnit { @@ -76,5 +77,17 @@ internal static string GetAssemblyPathFromCodeBase(string codeBase) return codeBase.Substring(start); } #endif + + // For assemblies already loaded by MTP or by some other means + public static Assembly? FindLoadedAssemblyByPath(string assemblyPath) + { + var full = Path.GetFullPath(assemblyPath); + + return AppDomain.CurrentDomain.GetAssemblies() + .FirstOrDefault(a => + !a.IsDynamic && + !string.IsNullOrEmpty(a.Location) && + StringComparer.OrdinalIgnoreCase.Equals(Path.GetFullPath(a.Location), full)); + } } } diff --git a/src/NUnitCommon/nunit.common/DotNet.cs b/src/NUnitCommon/nunit.common/DotNet.cs index 236b0125c..8578c5aa4 100644 --- a/src/NUnitCommon/nunit.common/DotNet.cs +++ b/src/NUnitCommon/nunit.common/DotNet.cs @@ -152,7 +152,8 @@ internal static bool FindBestRuntime(Version targetVersion, IEnumerable= targetVersion) + if (candidate.Version.Major > targetVersion.Major || + candidate.Version.Major == targetVersion.Major && candidate.Version.Minor >= candidate.Version.Minor) if (bestRuntime is null || candidate.Version.Major == bestRuntime.Version.Major) bestRuntime = candidate; } diff --git a/src/NUnitConsole/nunit4-console.tests/nunit4-console.tests.csproj b/src/NUnitConsole/nunit4-console.tests/nunit4-console.tests.csproj index f27524531..9641bd2f5 100644 --- a/src/NUnitConsole/nunit4-console.tests/nunit4-console.tests.csproj +++ b/src/NUnitConsole/nunit4-console.tests/nunit4-console.tests.csproj @@ -16,7 +16,6 @@ - diff --git a/src/NUnitEngine/nunit.engine.api/SettingDefinitions.cs b/src/NUnitEngine/nunit.engine.api/SettingDefinitions.cs index b0b7573b2..01877d071 100644 --- a/src/NUnitEngine/nunit.engine.api/SettingDefinitions.cs +++ b/src/NUnitEngine/nunit.engine.api/SettingDefinitions.cs @@ -256,6 +256,18 @@ static SettingDefinitions() /// public static SettingDefinition ImageTargetFrameworkName { get; } = new(nameof(ImageTargetFrameworkName), string.Empty); + /// + /// Set this to true to force use of the default assembly load context for the + /// test assembly and in resolving all dependencies rather than creating and + /// using a separate instance of AssemblyLoadContext. + /// + /// + /// This is provided for use by the NUnit3 VS Adapter and may not work if used + /// outside of that context. It must be set in the top-level package via the + /// AddSetting method so that the same value is passed to all subpackages. + /// + public const string UseDefaultAssemblyLoadContext = "UseDefaultAssemblyLoadContext"; + #endregion #region Settings Used by the NUnit Framework