From fac89148a8b27b4caca33a817bd34b09991e698f Mon Sep 17 00:00:00 2001 From: Charlie Poole Date: Mon, 16 Sep 2024 14:14:17 -0700 Subject: [PATCH] Reorganize assembly resolution; fix error loading WindowsBase.dll --- .../Internal/TestAssemblyLoadContext.cs | 2 - .../Internal/TestAssemblyResolver.cs | 256 ++++++++++++------ src/TestData/wpf-test/WpfTest.cs | 16 +- 3 files changed, 180 insertions(+), 94 deletions(-) diff --git a/src/NUnitEngine/nunit.engine.core/Internal/TestAssemblyLoadContext.cs b/src/NUnitEngine/nunit.engine.core/Internal/TestAssemblyLoadContext.cs index 93c37ced1..66a218e47 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 1e8d82573..b913d2b6f 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,154 +20,233 @@ 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 = 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; + public abstract bool TryToResolve( + AssemblyLoadContext loadContext, AssemblyName assemblyName, out Assembly loadedAssembly); + } - if (TryLoadFromTrustedPlatformAssemblies(context, name, out var 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)) { - if (name.Name == Path.GetFileNameWithoutExtension(assemblyPath)) + return false; + } + + var separator = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? ";" : ":"; + foreach (var assemblyPath in trustedAssemblies.Split(separator)) + { + 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 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; + + public RuntimeLibrariesStrategy(AssemblyLoadContext loadContext, string testAssemblyPath) + { + _dependencyContext = DependencyContext.Load(loadContext.LoadFromAssemblyPath(testAssemblyPath)); + + _assemblyResolver = new CompositeCompilationAssemblyResolver(new ICompilationAssemblyResolver[] + { + new AppBaseCompilationAssemblyResolver(Path.GetDirectoryName(testAssemblyPath)), + new ReferenceAssemblyPathResolver(), + new PackageCompilationAssemblyResolver() + }); + } - return loadedAssembly; + 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; - if (name.Version == null) + public AdditionalDirectoryStrategy(string frameworkDirectory) { - return null; + _frameworkDirectory = frameworkDirectory; } - foreach (string frameworkDirectory in AdditionalFrameworkDirectories) + public override bool TryToResolve( + AssemblyLoadContext loadContext, AssemblyName assemblyName, out Assembly loadedAssembly) { - var versionDir = FindBestVersionDir(frameworkDirectory, name.Version); + 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; } + } - 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; - } - } + #endregion - return false; - } + #region HelperMethods private static string GetDotNetInstallDirectory() { @@ -232,6 +312,8 @@ private static bool TryGetVersionFromString(string text, out Version newVersion) return false; } } + + #endregion } } #endif diff --git a/src/TestData/wpf-test/WpfTest.cs b/src/TestData/wpf-test/WpfTest.cs index d2b3632b0..64624c647 100644 --- a/src/TestData/wpf-test/WpfTest.cs +++ b/src/TestData/wpf-test/WpfTest.cs @@ -1,6 +1,7 @@ // Copyright (c) Charlie Poole, Rob Prouse and Contributors. MIT License - see LICENSE.txt using System; +using System.Windows; using System.Windows.Controls; using NUnit.Framework; @@ -8,19 +9,24 @@ namespace Test1 { [TestFixture] - public class WPFTest + public class WPFTest : IWeakEventListener { [Test] - public void WithoutFramework() + public void AssertPass() { Assert.Pass(); } - [Test] - public void WithFramework() + [Test, Apartment(System.Threading.ApartmentState.STA)] + public void CreateCheckBox() { - //CheckBox checkbox; + CheckBox checkbox; + checkbox = new CheckBox(); + } + public bool ReceiveWeakEvent(Type managerType, object sender, EventArgs e) + { + throw new NotImplementedException(); } } } \ No newline at end of file