From c26d33980e51cf0dccdbca67daa70bcba24b37a0 Mon Sep 17 00:00:00 2001 From: Charlie Poole Date: Sat, 28 Dec 2024 09:57:51 -0800 Subject: [PATCH] Port issue 1466 to V4 --- package-tests.cake | 18 +- .../Internal/TestAssemblyLoadContext.cs | 2 - .../Internal/TestAssemblyResolver.cs | 268 ++++++++++++------ 3 files changed, 194 insertions(+), 94 deletions(-) diff --git a/package-tests.cake b/package-tests.cake index 778178a55..06df83583 100644 --- a/package-tests.cake +++ b/package-tests.cake @@ -207,15 +207,15 @@ StandardRunnerTests.Add(new PackageTest( // WPF TESTS ////////////////////////////////////////////////////////////////////// -//AddToBothLists(new PackageTest( -// 1, "Net60WPFTest", "Run test using WPF under .NET 6.0", -// "testdata/net6.0-windows/WpfTest.dll --trace=Debug", -// new ExpectedResult("Passed") { Assemblies = new[] { new ExpectedAssemblyResult("WpfTest.dll", "netcore-6.0") } })); +AddToBothLists(new PackageTest( + 1, "Net60WPFTest", "Run test using WPF under .NET 6.0", + "testdata/net6.0-windows/WpfTest.dll --trace=Debug", + new ExpectedResult("Passed") { Assemblies = new[] { new ExpectedAssemblyResult("WpfTest.dll", "netcore-6.0") } })); -//AddToBothLists(new PackageTest( -// 1, "Net80WPFTest", "Run test using WPF under .NET 8.0", -// "testdata/net8.0-windows/WpfTest.dll --trace=Debug", -// new ExpectedResult("Passed") { Assemblies = new[] { new ExpectedAssemblyResult("WpfTest.dll", "netcore-8.0") } })); +AddToBothLists(new PackageTest( + 1, "Net80WPFTest", "Run test using WPF under .NET 8.0", + "testdata/net8.0-windows/WpfTest.dll --trace=Debug", + new ExpectedResult("Passed") { Assemblies = new[] { new ExpectedAssemblyResult("WpfTest.dll", "netcore-8.0") } })); ////////////////////////////////////////////////////////////////////// // RUN TESTS USING EACH OF OUR EXTENSIONS @@ -363,7 +363,7 @@ AddToBothLists(new PackageTest( } })); -StandardRunnerTests.Add(new PackageTest( +AddToBothLists(new PackageTest( 1, "AppContextBaseDirectory_NET80", "Test Setting the BaseDirectory to match test assembly location under .NET 8.0", "testdata/net8.0/AppContextTest.dll", diff --git a/src/NUnitEngine/nunit.engine.core/Internal/TestAssemblyLoadContext.cs b/src/NUnitEngine/nunit.engine.core/Internal/TestAssemblyLoadContext.cs index 0bc5aa164..c9d9661cb 100644 --- a/src/NUnitEngine/nunit.engine.core/Internal/TestAssemblyLoadContext.cs +++ b/src/NUnitEngine/nunit.engine.core/Internal/TestAssemblyLoadContext.cs @@ -15,14 +15,12 @@ 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; private readonly System.Runtime.Loader.AssemblyDependencyResolver _runtimeResolver; public TestAssemblyLoadContext(string testAssemblyPath) { - _testAssemblyPath = testAssemblyPath; _resolver = new TestAssemblyResolver(this, testAssemblyPath); _basePath = Path.GetDirectoryName(testAssemblyPath); _runtimeResolver = new AssemblyDependencyResolver(testAssemblyPath); diff --git a/src/NUnitEngine/nunit.engine.core/Internal/TestAssemblyResolver.cs b/src/NUnitEngine/nunit.engine.core/Internal/TestAssemblyResolver.cs index 7a5751e5e..60e71efeb 100644 --- a/src/NUnitEngine/nunit.engine.core/Internal/TestAssemblyResolver.cs +++ b/src/NUnitEngine/nunit.engine.core/Internal/TestAssemblyResolver.cs @@ -2,6 +2,9 @@ #if NETCOREAPP3_1_OR_GREATER +using Microsoft.Extensions.DependencyModel; +using Microsoft.Extensions.DependencyModel.Resolution; +using Microsoft.Win32; using System; using System.Collections.Generic; using System.IO; @@ -9,9 +12,7 @@ using System.Reflection; using System.Runtime.InteropServices; using System.Runtime.Loader; -using Microsoft.Extensions.DependencyModel; -using Microsoft.Extensions.DependencyModel.Resolution; -using Microsoft.Win32; +using TestCentric.Metadata; namespace NUnit.Engine.Internal { @@ -19,153 +20,252 @@ 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; private static readonly string INSTALL_DIR; private static readonly string WINDOWS_DESKTOP_DIR; private static readonly string ASP_NET_CORE_DIR; - private static readonly List AdditionalFrameworkDirectories; + + // Our Strategies for resolving references + List ResolutionStrategies; static TestAssemblyResolver() { - INSTALL_DIR = DotNet.GetInstallDirectory(); + INSTALL_DIR = GetDotNetInstallDirectory(); WINDOWS_DESKTOP_DIR = Path.Combine(INSTALL_DIR, "shared", "Microsoft.WindowsDesktop.App"); ASP_NET_CORE_DIR = Path.Combine(INSTALL_DIR, "shared", "Microsoft.AspNetCore.App"); - - AdditionalFrameworkDirectories = new List(); - if (Directory.Exists(WINDOWS_DESKTOP_DIR)) - AdditionalFrameworkDirectories.Add(WINDOWS_DESKTOP_DIR); - if (Directory.Exists(ASP_NET_CORE_DIR)) - AdditionalFrameworkDirectories.Add(ASP_NET_CORE_DIR); } - public TestAssemblyResolver(AssemblyLoadContext loadContext, string assemblyPath) + public TestAssemblyResolver(AssemblyLoadContext loadContext, string testAssemblyPath) { _loadContext = loadContext; - _dependencyContext = DependencyContext.Load(loadContext.LoadFromAssemblyPath(assemblyPath)); - _assemblyResolver = new CompositeCompilationAssemblyResolver(new ICompilationAssemblyResolver[] - { - new AppBaseCompilationAssemblyResolver(Path.GetDirectoryName(assemblyPath)), - new ReferenceAssemblyPathResolver(), - new PackageCompilationAssemblyResolver() - }); + InitializeResolutionStrategies(loadContext, testAssemblyPath); _loadContext.Resolving += OnResolving; } + private void InitializeResolutionStrategies(AssemblyLoadContext loadContext, string testAssemblyPath) + { + // First, looking only at direct references by the test assembly, try to determine if + // this assembly is using WindowsDesktop (either SWF or WPF) and/or AspNetCore. + AssemblyDefinition assemblyDef = AssemblyDefinition.ReadAssembly(testAssemblyPath); + bool isWindowsDesktop = false; + bool isAspNetCore = false; + foreach (var reference in assemblyDef.MainModule.GetTypeReferences()) + { + string fn = reference.FullName; + if (fn.StartsWith("System.Windows.") || fn.StartsWith("PresentationFramework")) + isWindowsDesktop = true; + if (fn.StartsWith("Microsoft.AspNetCore.")) + isAspNetCore = true; + } + + // Initialize the list of ResolutionStrategies in the best order depending on + // what we learned. + ResolutionStrategies = new List(); + + if (isWindowsDesktop && Directory.Exists(WINDOWS_DESKTOP_DIR)) + ResolutionStrategies.Add(new AdditionalDirectoryStrategy(WINDOWS_DESKTOP_DIR)); + if (isAspNetCore && Directory.Exists(ASP_NET_CORE_DIR)) + ResolutionStrategies.Add(new AdditionalDirectoryStrategy(ASP_NET_CORE_DIR)); + ResolutionStrategies.Add(new TrustedPlatformAssembliesStrategy()); + ResolutionStrategies.Add(new RuntimeLibrariesStrategy(loadContext, testAssemblyPath)); + if (!isWindowsDesktop && Directory.Exists(WINDOWS_DESKTOP_DIR)) + ResolutionStrategies.Add(new AdditionalDirectoryStrategy(WINDOWS_DESKTOP_DIR)); + if (!isAspNetCore && Directory.Exists(ASP_NET_CORE_DIR)) + ResolutionStrategies.Add(new AdditionalDirectoryStrategy(ASP_NET_CORE_DIR)); + } + public void Dispose() { _loadContext.Resolving -= OnResolving; } - public Assembly Resolve(AssemblyLoadContext context, AssemblyName name) + public Assembly Resolve(AssemblyLoadContext context, AssemblyName assemblyName) + { + return OnResolving(context, assemblyName); + } + + private Assembly OnResolving(AssemblyLoadContext loadContext, AssemblyName assemblyName) { - return OnResolving(context, name); + if (loadContext == null) throw new ArgumentNullException("context"); + + Assembly loadedAssembly; + foreach (var strategy in ResolutionStrategies) + if (strategy.TryToResolve(loadContext, assemblyName, out loadedAssembly)) + return loadedAssembly; + + log.Info("Cannot resolve assembly '{0}'", assemblyName); + return null; } - private Assembly OnResolving(AssemblyLoadContext context, AssemblyName name) + #region Nested ResolutionStrategy Classes + + public abstract class ResolutionStrategy { - context = context ?? _loadContext; - - if (TryLoadFromTrustedPlatformAssemblies(context, name, out var loadedAssembly)) + public abstract bool TryToResolve( + AssemblyLoadContext loadContext, AssemblyName assemblyName, out Assembly loadedAssembly); + } + + public class TrustedPlatformAssembliesStrategy : ResolutionStrategy + { + public override bool TryToResolve( + AssemblyLoadContext loadContext, AssemblyName assemblyName, out Assembly loadedAssembly) { - log.Info("'{0}' assembly is loaded from trusted path '{1}'", name, loadedAssembly.Location); - return loadedAssembly; + return TryLoadFromTrustedPlatformAssemblies(loadContext, assemblyName, out loadedAssembly); } - foreach (var library in _dependencyContext.RuntimeLibraries) + private static bool TryLoadFromTrustedPlatformAssemblies( + AssemblyLoadContext loadContext, AssemblyName assemblyName, out Assembly loadedAssembly) { - var wrapper = new CompilationLibrary( - library.Type, - library.Name, - library.Version, - library.Hash, - library.RuntimeAssemblyGroups.SelectMany(g => g.AssetPaths), - library.Dependencies, - library.Serviceable); - - var assemblies = new List(); - _assemblyResolver.TryResolveAssemblyPaths(wrapper, assemblies); - - foreach (var assemblyPath in assemblies) + // 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)) { - if (name.Name == Path.GetFileNameWithoutExtension(assemblyPath)) + var fileName = Path.GetFileNameWithoutExtension(assemblyPath); + if (FileMatchesAssembly(fileName) && File.Exists(assemblyPath)) { - loadedAssembly = context.LoadFromAssemblyPath(assemblyPath); - log.Info("'{0}' ({1}) assembly is loaded from runtime libraries {2} dependencies", - name, - loadedAssembly.Location, - library.Name); + loadedAssembly = loadContext.LoadFromAssemblyPath(assemblyPath); + log.Info("'{0}' assembly is loaded from trusted path '{1}'", assemblyPath, loadedAssembly.Location); - return loadedAssembly; + return true; } } + + return false; + + bool FileMatchesAssembly(string fileName) => + string.Equals(fileName, assemblyName.Name, StringComparison.InvariantCultureIgnoreCase); } + } + + public class RuntimeLibrariesStrategy : ResolutionStrategy + { + private DependencyContext _dependencyContext; + private readonly ICompilationAssemblyResolver _assemblyResolver; - if (name.Version == null) + public RuntimeLibrariesStrategy(AssemblyLoadContext loadContext, string testAssemblyPath) { - return null; + _dependencyContext = DependencyContext.Load(loadContext.LoadFromAssemblyPath(testAssemblyPath)); + + _assemblyResolver = new CompositeCompilationAssemblyResolver(new ICompilationAssemblyResolver[] + { + new AppBaseCompilationAssemblyResolver(Path.GetDirectoryName(testAssemblyPath)), + new ReferenceAssemblyPathResolver(), + new PackageCompilationAssemblyResolver() + }); + } + + public override bool TryToResolve( + AssemblyLoadContext loadContext, AssemblyName assemblyName, out Assembly loadedAssembly) + { + foreach (var library in _dependencyContext.RuntimeLibraries) + { + var wrapper = new CompilationLibrary( + library.Type, + library.Name, + library.Version, + library.Hash, + library.RuntimeAssemblyGroups.SelectMany(g => g.AssetPaths), + library.Dependencies, + library.Serviceable); + + var assemblies = new List(); + _assemblyResolver.TryResolveAssemblyPaths(wrapper, assemblies); + + foreach (var assemblyPath in assemblies) + { + if (assemblyName.Name == Path.GetFileNameWithoutExtension(assemblyPath)) + { + loadedAssembly = loadContext.LoadFromAssemblyPath(assemblyPath); + log.Info("'{0}' ({1}) assembly is loaded from runtime libraries {2} dependencies", + assemblyName, + loadedAssembly.Location, + library.Name); + + return true; + } + } + } + + loadedAssembly = null; + return false; } + } + + public class AdditionalDirectoryStrategy : ResolutionStrategy + { + private string _frameworkDirectory; - foreach (string frameworkDirectory in AdditionalFrameworkDirectories) + public AdditionalDirectoryStrategy(string frameworkDirectory) { - var versionDir = FindBestVersionDir(frameworkDirectory, name.Version); + _frameworkDirectory = frameworkDirectory; + } + + public override bool TryToResolve( + AssemblyLoadContext loadContext, AssemblyName assemblyName, out Assembly loadedAssembly) + { + loadedAssembly = null; + if (assemblyName.Version == null) + return false; + + var versionDir = FindBestVersionDir(_frameworkDirectory, assemblyName.Version); if (versionDir != null) { - string candidate = Path.Combine(frameworkDirectory, versionDir, name.Name + ".dll"); + string candidate = Path.Combine(_frameworkDirectory, versionDir, assemblyName.Name + ".dll"); if (File.Exists(candidate)) { - loadedAssembly = context.LoadFromAssemblyPath(candidate); + loadedAssembly = loadContext.LoadFromAssemblyPath(candidate); log.Info("'{0}' ({1}) assembly is loaded from AdditionalFrameworkDirectory {2} dependencies with best candidate version {3}", - name, + assemblyName, loadedAssembly.Location, - frameworkDirectory, + _frameworkDirectory, versionDir); - return loadedAssembly; + return true; } else { - log.Debug("Best version dir for {0} is {1}, but there is no {2} file", frameworkDirectory, versionDir, candidate); + log.Debug("Best version dir for {0} is {1}, but there is no {2} file", _frameworkDirectory, versionDir, candidate); + return false; } } - } - 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; } + } + + #endregion - var separator = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? ";" : ":"; - foreach (var assemblyPath in trustedAssemblies.Split(separator)) + #region HelperMethods + + private static string GetDotNetInstallDirectory() + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { - var fileName = Path.GetFileNameWithoutExtension(assemblyPath); - if (string.Equals(fileName, assemblyName.Name, StringComparison.InvariantCultureIgnoreCase) == false) + // Running on Windows so use registry + if (Environment.Is64BitProcess) { - continue; + RegistryKey key = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\dotnet\SetUp\InstalledVersions\x64\sharedHost\"); + return (string)key?.GetValue("Path"); } - - if (File.Exists(assemblyPath)) + else { - loadedAssembly = context.LoadFromAssemblyPath(assemblyPath); - return true; + RegistryKey key = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\WOW6432Node\dotnet\SetUp\InstalledVersions\x86\"); + return (string)key?.GetValue("InstallLocation"); } } - - return false; + else + return "/usr/shared/dotnet/"; } private static string FindBestVersionDir(string libraryDir, Version targetVersion) @@ -212,6 +312,8 @@ private static bool TryGetVersionFromString(string text, out Version newVersion) return false; } } + + #endregion } } #endif