diff --git a/src/TestCentric.Agent.Core.Tests/TestAgentRunnerTests.cs b/src/TestCentric.Agent.Core.Tests/TestAgentRunnerTests.cs index 56e9807..086532b 100644 --- a/src/TestCentric.Agent.Core.Tests/TestAgentRunnerTests.cs +++ b/src/TestCentric.Agent.Core.Tests/TestAgentRunnerTests.cs @@ -52,7 +52,7 @@ public void Load() [Test] public void CountTestCases() { - int count = _runner.CountTestCases(TestFilter.Empty); + int count = _runner.CountTestCases(TestFilter.Empty); Assert.That(count, Is.EqualTo(MockAssembly.Tests)); CheckPackageLoading(); } diff --git a/src/TestCentric.Agent.Core/Internal/TestAssemblyResolver.cs b/src/TestCentric.Agent.Core/Internal/TestAssemblyResolver.cs deleted file mode 100644 index 5e4085b..0000000 --- a/src/TestCentric.Agent.Core/Internal/TestAssemblyResolver.cs +++ /dev/null @@ -1,144 +0,0 @@ -// *********************************************************************** -// Copyright (c) Charlie Poole and TestCentric contributors. -// Licensed under the MIT License. See LICENSE file in root directory. -// *********************************************************************** - -#if NETCOREAPP3_1_OR_GREATER - -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Reflection; -using System.Runtime.InteropServices; -using System.Runtime.Loader; -using Microsoft.Extensions.DependencyModel; -using Microsoft.Extensions.DependencyModel.Resolution; - -namespace TestCentric.Engine.Internal -{ - internal sealed class TestAssemblyResolver : IDisposable - { - private static readonly Logger log = InternalTrace.GetLogger(nameof(TestAssemblyResolver)); - - private readonly ICompilationAssemblyResolver _assemblyResolver; - private readonly DependencyContext _dependencyContext; - private readonly AssemblyLoadContext _loadContext; - - //private static readonly string NET_CORE_RUNTIME = "Microsoft.NETCore.App"; - private static readonly string WINDOWS_DESKTOP_RUNTIME = "Microsoft.WindowsDesktop.App"; - private static readonly string ASP_NET_CORE_RUNTIME = "Microsoft.AspNetCore.App"; - - private static readonly string[] AdditionalRuntimes = new [] { - ASP_NET_CORE_RUNTIME, WINDOWS_DESKTOP_RUNTIME - }; - - public TestAssemblyResolver(AssemblyLoadContext loadContext, string assemblyPath) - { - _loadContext = loadContext; - _dependencyContext = DependencyContext.Load(loadContext.LoadFromAssemblyPath(assemblyPath)); - - _assemblyResolver = new CompositeCompilationAssemblyResolver(new ICompilationAssemblyResolver[] - { - new AppBaseCompilationAssemblyResolver(Path.GetDirectoryName(assemblyPath)), - new ReferenceAssemblyPathResolver(), - new PackageCompilationAssemblyResolver() - }); - - _loadContext.Resolving += OnResolving; - } - - public void Dispose() - { - _loadContext.Resolving -= OnResolving; - } - - private Assembly OnResolving(AssemblyLoadContext context, AssemblyName name) - { - log.Info($"Resolving {name}"); - - if (TryLoadFromTrustedPlatformAssemblies(context, name, out var loadedAssembly)) - { - log.Info($" TrustedPlatformAssemblies: {loadedAssembly.Location}"); - - return 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 (name.Name == Path.GetFileNameWithoutExtension(assemblyPath)) - { - loadedAssembly = _loadContext.LoadFromAssemblyPath(assemblyPath); - log.Info($" Runtime Library {library.Name}: {loadedAssembly.Location}"); - - return loadedAssembly; - } - } - } - - if (name.Version == null) - return null; - - foreach (string runtime in AdditionalRuntimes) - { - var runtimeDir = DotNetRuntimes.GetBestRuntime(runtime, name.Version).Location; - if (runtimeDir != null) - { - string candidate = Path.Combine(runtimeDir, name.Name + ".dll"); - if (File.Exists(candidate)) - { - log.Info($" Runtime {runtime}: {candidate}"); - return _loadContext.LoadFromAssemblyPath(candidate); - } - } - } - - 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; - } - - //log.Debug($"Trusted Platform Assemblies: {trustedAssemblies}"); - 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; - } - } -} -#endif diff --git a/src/TestCentric.Agent.Core/Resolvers/AdditionalRuntimesResolutionStrategy.cs b/src/TestCentric.Agent.Core/Resolvers/AdditionalRuntimesResolutionStrategy.cs new file mode 100644 index 0000000..414f93e --- /dev/null +++ b/src/TestCentric.Agent.Core/Resolvers/AdditionalRuntimesResolutionStrategy.cs @@ -0,0 +1,58 @@ +// *********************************************************************** +// Copyright (c) Charlie Poole and TestCentric contributors. +// Licensed under the MIT License. See LICENSE file in root directory. +// *********************************************************************** + +#if NETCOREAPP3_1_OR_GREATER + +using System.IO; +using System.Reflection; +using System.Runtime.Loader; + +namespace TestCentric.Engine.Internal +{ + internal abstract class AdditionalRuntimesResolutionStrategy : ResolutionStrategy + { + private static readonly Logger log = InternalTrace.GetLogger(nameof(AdditionalRuntimesResolutionStrategy)); + + protected abstract string RuntimeName { get; } + + public AdditionalRuntimesResolutionStrategy(TestAssemblyResolver resolver) : base(resolver) { } + + public override Assembly TryLoadAssembly(AssemblyLoadContext context, AssemblyName assemblyName) + { + // This strategy requires a version, which may not be present + if (assemblyName.Version == null) + return null; + + var runtimeDir = DotNetRuntimes.GetBestRuntime(RuntimeName, assemblyName.Version).Location; + if (runtimeDir != null) + { + string candidate = Path.Combine(runtimeDir, assemblyName.Name + ".dll"); + if (File.Exists(candidate)) + { + log.Debug($"{RuntimeName} Resolved to {candidate}"); + return LoadContext.LoadFromAssemblyPath(candidate); + } + } + + log.Debug($"{RuntimeName} Failed!"); + return null; + } + } + + internal class AspNetCoreResolutionStrategy : AdditionalRuntimesResolutionStrategy + { + public AspNetCoreResolutionStrategy(TestAssemblyResolver resolver) : base(resolver) { } + + protected override string RuntimeName => "Microsoft.AspNetCore.App"; + } + + internal class WindowsDesktopResolutionStrategy : AdditionalRuntimesResolutionStrategy + { + public WindowsDesktopResolutionStrategy(TestAssemblyResolver resolver) : base(resolver) { } + + protected override string RuntimeName => "Microsoft.WindowsDesktop.App"; + } +} +#endif diff --git a/src/TestCentric.Agent.Core/Internal/ProvidedPathsAssemblyResolver.cs b/src/TestCentric.Agent.Core/Resolvers/ProvidedPathsAssemblyResolver.cs similarity index 100% rename from src/TestCentric.Agent.Core/Internal/ProvidedPathsAssemblyResolver.cs rename to src/TestCentric.Agent.Core/Resolvers/ProvidedPathsAssemblyResolver.cs diff --git a/src/TestCentric.Agent.Core/Resolvers/ResolutionStrategy.cs b/src/TestCentric.Agent.Core/Resolvers/ResolutionStrategy.cs new file mode 100644 index 0000000..6ef71d8 --- /dev/null +++ b/src/TestCentric.Agent.Core/Resolvers/ResolutionStrategy.cs @@ -0,0 +1,48 @@ +// *********************************************************************** +// Copyright (c) Charlie Poole and TestCentric contributors. +// Licensed under the MIT License. See LICENSE file in root directory. +// *********************************************************************** + +#if NETCOREAPP3_1_OR_GREATER + +using System; +using System.Reflection; +using System.Runtime.Loader; +using TestCentric.Engine.Internal; + +namespace TestCentric.Engine.Internal +{ + public abstract class ResolutionStrategy + { + private int _totalCalls; + + internal TestAssemblyLoadContext LoadContext { get; } + internal string TestAssemblyPath { get; } + + + internal ResolutionStrategy(TestAssemblyResolver resolver) + { + LoadContext = resolver.LoadContext; + TestAssemblyPath = resolver.TestAssemblyPath; + } + + public bool TryLoadAssembly(AssemblyLoadContext context, AssemblyName assemblyName, out Assembly loadedAssembly) + { + _totalCalls++; + + loadedAssembly = TryLoadAssembly(context, assemblyName); + return loadedAssembly != null; + } + + public abstract Assembly TryLoadAssembly(AssemblyLoadContext context, AssemblyName assemblyName); + + public void WriteReport() + { + Console.WriteLine(); + Console.WriteLine(GetType().Name); + Console.WriteLine(); + Console.WriteLine($"Total Calls: {_totalCalls}"); + } + } +} +#endif diff --git a/src/TestCentric.Agent.Core/Resolvers/RuntimeLibraryResolutionStrategy.cs b/src/TestCentric.Agent.Core/Resolvers/RuntimeLibraryResolutionStrategy.cs new file mode 100644 index 0000000..cf54117 --- /dev/null +++ b/src/TestCentric.Agent.Core/Resolvers/RuntimeLibraryResolutionStrategy.cs @@ -0,0 +1,69 @@ +// *********************************************************************** +// Copyright (c) Charlie Poole and TestCentric contributors. +// Licensed under the MIT License. See LICENSE file in root directory. +// *********************************************************************** + +#if NETCOREAPP3_1_OR_GREATER + +using Microsoft.Extensions.DependencyModel.Resolution; +using Microsoft.Extensions.DependencyModel; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using System.Runtime.Loader; +using System.Linq; + +namespace TestCentric.Engine.Internal +{ + internal class RuntimeLibraryResolutionStrategy : ResolutionStrategy + { + private static readonly Logger log = InternalTrace.GetLogger(nameof(RuntimeLibraryResolutionStrategy)); + + private List _libraries = new List(); + + public RuntimeLibraryResolutionStrategy(TestAssemblyResolver resolver) : base(resolver) + { + var dependencyContext = DependencyContext.Load(LoadContext.LoadFromAssemblyPath(TestAssemblyPath)); + if (dependencyContext != null) + foreach (var library in dependencyContext.RuntimeLibraries) + _libraries.Add( + new CompilationLibrary( + library.Type, + library.Name, + library.Version, + library.Hash, + library.RuntimeAssemblyGroups.SelectMany(g => g.AssetPaths), + library.Dependencies, + library.Serviceable)); + } + + public override Assembly TryLoadAssembly(AssemblyLoadContext context, AssemblyName assemblyName) + { + foreach (var library in _libraries) + { + var assemblies = new List(); + var assemblyResolver = new CompositeCompilationAssemblyResolver(new ICompilationAssemblyResolver[] + { + new AppBaseCompilationAssemblyResolver(Path.GetDirectoryName(TestAssemblyPath)), + new ReferenceAssemblyPathResolver(), + new PackageCompilationAssemblyResolver() + }); + + assemblyResolver.TryResolveAssemblyPaths(library, assemblies); + + foreach (var assemblyPath in assemblies) + { + if (assemblyName.Name == Path.GetFileNameWithoutExtension(assemblyPath)) + { + log.Debug($"Resolved to {assemblyPath}"); + return LoadContext.LoadFromAssemblyPath(assemblyPath); + } + } + } + + log.Debug("Failed!"); + return null; + } + } +} +#endif diff --git a/src/TestCentric.Agent.Core/Internal/TestAssemblyLoadContext.cs b/src/TestCentric.Agent.Core/Resolvers/TestAssemblyLoadContext.cs similarity index 91% rename from src/TestCentric.Agent.Core/Internal/TestAssemblyLoadContext.cs rename to src/TestCentric.Agent.Core/Resolvers/TestAssemblyLoadContext.cs index 16bd4d8..6561a4f 100644 --- a/src/TestCentric.Agent.Core/Internal/TestAssemblyLoadContext.cs +++ b/src/TestCentric.Agent.Core/Resolvers/TestAssemblyLoadContext.cs @@ -17,15 +17,17 @@ internal sealed class TestAssemblyLoadContext : AssemblyLoadContext { private static readonly Logger log = InternalTrace.GetLogger(nameof(TestAssemblyLoadContext)); - private readonly string _testAssemblyPath; + internal readonly string _testAssemblyPath; private readonly string _basePath; - private readonly TestAssemblyResolver _resolver; + + public TestAssemblyResolver Resolver { get; } public TestAssemblyLoadContext(string testAssemblyPath) { _testAssemblyPath = testAssemblyPath; - _resolver = new TestAssemblyResolver(this, testAssemblyPath); _basePath = Path.GetDirectoryName(testAssemblyPath); + + Resolver = new TestAssemblyResolver(this, testAssemblyPath); } protected override Assembly Load(AssemblyName name) diff --git a/src/TestCentric.Agent.Core/Resolvers/TestAssemblyResolver.cs b/src/TestCentric.Agent.Core/Resolvers/TestAssemblyResolver.cs new file mode 100644 index 0000000..f73c022 --- /dev/null +++ b/src/TestCentric.Agent.Core/Resolvers/TestAssemblyResolver.cs @@ -0,0 +1,84 @@ +// *********************************************************************** +// Copyright (c) Charlie Poole and TestCentric contributors. +// Licensed under the MIT License. See LICENSE file in root directory. +// *********************************************************************** + +#if NETCOREAPP3_1_OR_GREATER + +using System; +using System.Collections.Generic; +using System.Diagnostics.Contracts; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Reflection.Metadata.Ecma335; +using System.Runtime.InteropServices; +using System.Runtime.Loader; +using System.Xml.Linq; +using Microsoft.Extensions.DependencyModel; +using Microsoft.Extensions.DependencyModel.Resolution; + +namespace TestCentric.Engine.Internal +{ + internal sealed class TestAssemblyResolver : IDisposable + { + private static readonly Logger log = InternalTrace.GetLogger(nameof(TestAssemblyResolver)); + + // The AssemblyLoadContext created for the current TestAssembly + public TestAssemblyLoadContext LoadContext { get; } + + // The path to the current TestAssembly + public string TestAssemblyPath { get; } + + // Our ResolverStrategies + public List ResolverStrategies { get; } + + public TestAssemblyResolver(TestAssemblyLoadContext loadContext, string assemblyPath) + { + LoadContext = loadContext; + TestAssemblyPath = assemblyPath; + + ResolverStrategies = new List + { + new TrustedPlatformResolutionStrategy(this), + new RuntimeLibraryResolutionStrategy(this), + new AspNetCoreResolutionStrategy(this), + new WindowsDesktopResolutionStrategy(this) + }; + + LoadContext.Resolving += OnResolving; + } + + public void Dispose() + { + LoadContext.Resolving -= OnResolving; + } + + private Assembly OnResolving(AssemblyLoadContext context, AssemblyName assemblyName) + { + log.Info($"Resolving {assemblyName}"); + + for (int index = 0; index < ResolverStrategies.Count; index++) + { + var strategy = ResolverStrategies[index]; + + if (strategy.TryLoadAssembly(context, assemblyName, out var loadedAssembly)) + { + if (index > 0) + { + // Simplistic approach to favoring the strategy that succeeds + // by moving it to the start of the list. This is safe because + // we are about to return from the method, exiting the loop. + ResolverStrategies.RemoveAt(index); + ResolverStrategies.Insert(0, strategy); + } + + return loadedAssembly; + } + } + + return null; + } + } +} +#endif diff --git a/src/TestCentric.Agent.Core/Resolvers/TrustedPlatformResolutionStrategy.cs b/src/TestCentric.Agent.Core/Resolvers/TrustedPlatformResolutionStrategy.cs new file mode 100644 index 0000000..44a8ea3 --- /dev/null +++ b/src/TestCentric.Agent.Core/Resolvers/TrustedPlatformResolutionStrategy.cs @@ -0,0 +1,52 @@ +// *********************************************************************** +// Copyright (c) Charlie Poole and TestCentric contributors. +// Licensed under the MIT License. See LICENSE file in root directory. +// *********************************************************************** + +#if NETCOREAPP3_1_OR_GREATER + +using System; +using System.IO; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Runtime.Loader; + +namespace TestCentric.Engine.Internal +{ + internal class TrustedPlatformResolutionStrategy : ResolutionStrategy + { + private static readonly Logger log = InternalTrace.GetLogger(nameof(TrustedPlatformResolutionStrategy)); + + private readonly string[] _trustedAssemblies = new string[0]; + + public TrustedPlatformResolutionStrategy(TestAssemblyResolver resolver) : base(resolver) + { + // https://learn.microsoft.com/en-us/dotnet/core/dependency-loading/default-probing + var data = AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES") as string; + + if (data != null) + { + var separator = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? ";" : ":"; + _trustedAssemblies = data.Split(separator); + } + + } + + public override Assembly TryLoadAssembly(AssemblyLoadContext context, AssemblyName assemblyName) + { + foreach (var assemblyPath in _trustedAssemblies) + { + var fileName = Path.GetFileNameWithoutExtension(assemblyPath); + if (string.Equals(fileName, assemblyName.Name, StringComparison.InvariantCultureIgnoreCase) && File.Exists(assemblyPath)) + { + log.Debug($"Resolved to {assemblyPath}"); + return context.LoadFromAssemblyPath(assemblyPath); + } + } + + log.Debug("Failed!"); + return null; + } + } +} +#endif