diff --git a/src/NUnitEngine/nunit.engine.core/Drivers/NUnit3DriverFactory.cs b/src/NUnitEngine/nunit.engine.core/Drivers/NUnit3DriverFactory.cs index 4149dfd75..92813c171 100644 --- a/src/NUnitEngine/nunit.engine.core/Drivers/NUnit3DriverFactory.cs +++ b/src/NUnitEngine/nunit.engine.core/Drivers/NUnit3DriverFactory.cs @@ -61,14 +61,16 @@ public IFrameworkDriver GetDriver(AppDomain domain, AssemblyName reference) /// Gets a driver for a given test assembly and a framework /// which the assembly is already known to reference. /// - /// The domain in which the assembly will be loaded /// An AssemblyName referring to the test framework. /// public IFrameworkDriver GetDriver(AssemblyName reference) { Guard.ArgumentValid(IsSupportedTestFramework(reference), "Invalid framework", "reference"); - +#if NETSTANDARD return new NUnitNetStandardDriver(); +#elif NETCOREAPP3_1 + return new NUnitNetCore31Driver(); +#endif } #endif } diff --git a/src/NUnitEngine/nunit.engine.core/Drivers/NUnitNetCore31Driver.cs b/src/NUnitEngine/nunit.engine.core/Drivers/NUnitNetCore31Driver.cs new file mode 100644 index 000000000..db4a11c7d --- /dev/null +++ b/src/NUnitEngine/nunit.engine.core/Drivers/NUnitNetCore31Driver.cs @@ -0,0 +1,208 @@ +// *********************************************************************** +// Copyright (c) 2020 Charlie Poole, Rob Prouse +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN METHOD +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// *********************************************************************** + +#if NETCOREAPP3_1 +using System; +using System.Linq; +using System.Collections.Generic; +using System.IO; +using NUnit.Engine.Internal; +using System.Reflection; +using NUnit.Engine.Extensibility; + +namespace NUnit.Engine.Drivers +{ + /// + /// NUnitNetCore31Driver is used by the test-runner to load and run + /// tests using the NUnit framework assembly. It contains functionality to + /// correctly load assemblies from other directories, using APIs first available in + /// .NET Core 3.1. + /// + public class NUnitNetCore31Driver : IFrameworkDriver + { + const string LOAD_MESSAGE = "Method called without calling Load first"; + const string INVALID_FRAMEWORK_MESSAGE = "Running tests against this version of the framework using this driver is not supported. Please update NUnit.Framework to the latest version."; + const string FAILED_TO_LOAD_TEST_ASSEMBLY = "Failed to load the test assembly {0}"; + const string FAILED_TO_LOAD_NUNIT = "Failed to load the NUnit Framework in the test assembly"; + + static readonly string CONTROLLER_TYPE = "NUnit.Framework.Api.FrameworkController"; + static readonly string LOAD_METHOD = "LoadTests"; + static readonly string EXPLORE_METHOD = "ExploreTests"; + static readonly string COUNT_METHOD = "CountTests"; + static readonly string RUN_METHOD = "RunTests"; + static readonly string RUN_ASYNC_METHOD = "RunTests"; + static readonly string STOP_RUN_METHOD = "StopRun"; + + static ILogger log = InternalTrace.GetLogger(nameof(NUnitNetCore31Driver)); + + Assembly _testAssembly; + Assembly _frameworkAssembly; + object _frameworkController; + Type _frameworkControllerType; + + /// + /// An id prefix that will be passed to the test framework and used as part of the + /// test ids created. + /// + public string ID { get; set; } + + /// + /// Loads the tests in an assembly. + /// + /// The path to the test assembly + /// The test settings + /// An XML string representing the loaded test + public string Load(string assemblyPath, IDictionary settings) + { + var idPrefix = string.IsNullOrEmpty(ID) ? "" : ID + "-"; + + assemblyPath = Path.GetFullPath(assemblyPath); //AssemblyLoadContext requires an absolute path + var assemblyLoadContext = new CustomAssemblyLoadContext(assemblyPath); + + try + { + _testAssembly = assemblyLoadContext.LoadFromAssemblyPath(assemblyPath); + } + catch (Exception e) + { + throw new NUnitEngineException(string.Format(FAILED_TO_LOAD_TEST_ASSEMBLY, assemblyPath), e); + } + + var nunitRef = _testAssembly.GetReferencedAssemblies().FirstOrDefault(reference => reference.Name.Equals("nunit.framework", StringComparison.OrdinalIgnoreCase)); + + if (nunitRef == null) + throw new NUnitEngineException(FAILED_TO_LOAD_NUNIT); + + _frameworkAssembly = assemblyLoadContext.LoadFromAssemblyName(nunitRef); + if (_frameworkAssembly == null) + throw new NUnitEngineException(FAILED_TO_LOAD_NUNIT); + + _frameworkController = CreateObject(CONTROLLER_TYPE, _testAssembly, idPrefix, settings); + if (_frameworkController == null) + throw new NUnitEngineException(INVALID_FRAMEWORK_MESSAGE); + + _frameworkControllerType = _frameworkController.GetType(); + + log.Info("Loading {0} - see separate log file", _testAssembly.FullName); + return ExecuteMethod(LOAD_METHOD) as string; + } + + /// + /// Counts the number of test cases for the loaded test assembly + /// + /// The XML test filter + /// The number of test cases + public int CountTestCases(string filter) + { + CheckLoadWasCalled(); + object count = ExecuteMethod(COUNT_METHOD, filter); + return count != null ? (int)count : 0; + } + + /// + /// Executes the tests in an assembly. + /// + /// An ITestEventHandler that receives progress notices + /// A filter that controls which tests are executed + /// An Xml string representing the result + public string Run(ITestEventListener listener, string filter) + { + CheckLoadWasCalled(); + log.Info("Running {0} - see separate log file", _testAssembly.FullName); + Action callback = listener != null ? listener.OnTestEvent : (Action)null; + return ExecuteMethod(RUN_METHOD, new[] { typeof(Action), typeof(string) }, callback, filter) as string; + } + + /// + /// Executes the tests in an assembly asyncronously. + /// + /// A callback that receives XML progress notices + /// A filter that controls which tests are executed + public void RunAsync(Action callback, string filter) + { + CheckLoadWasCalled(); + log.Info("Running {0} - see separate log file", _testAssembly.FullName); + ExecuteMethod(RUN_ASYNC_METHOD, new[] { typeof(Action), typeof(string) }, callback, filter); + } + + /// + /// Cancel the ongoing test run. If no test is running, the call is ignored. + /// + /// If true, cancel any ongoing test threads, otherwise wait for them to complete. + public void StopRun(bool force) + { + ExecuteMethod(STOP_RUN_METHOD, force); + } + + /// + /// Returns information about the tests in an assembly. + /// + /// A filter indicating which tests to include + /// An Xml string representing the tests + public string Explore(string filter) + { + CheckLoadWasCalled(); + + log.Info("Exploring {0} - see separate log file", _testAssembly.FullName); + return ExecuteMethod(EXPLORE_METHOD, filter) as string; + } + + void CheckLoadWasCalled() + { + if (_frameworkController == null) + throw new InvalidOperationException(LOAD_MESSAGE); + } + + object CreateObject(string typeName, params object[] args) + { + var typeinfo = _frameworkAssembly.DefinedTypes.FirstOrDefault(t => t.FullName == typeName); + if (typeinfo == null) + { + log.Error("Could not find type {0}", typeName); + } + return Activator.CreateInstance(typeinfo.AsType(), args); + } + + object ExecuteMethod(string methodName, params object[] args) + { + var method = _frameworkControllerType.GetMethod(methodName, BindingFlags.Public | BindingFlags.Instance); + return ExecuteMethod(method, args); + } + + object ExecuteMethod(string methodName, Type[] ptypes, params object[] args) + { + var method = _frameworkControllerType.GetMethod(methodName, ptypes); + return ExecuteMethod(method, args); + } + + object ExecuteMethod(MethodInfo method, params object[] args) + { + if (method == null) + { + throw new NUnitEngineException(INVALID_FRAMEWORK_MESSAGE); + } + return method.Invoke(_frameworkController, args); + } + } +} +#endif \ No newline at end of file diff --git a/src/NUnitEngine/nunit.engine.core/Drivers/NUnitNetStandardDriver.cs b/src/NUnitEngine/nunit.engine.core/Drivers/NUnitNetStandardDriver.cs index 12f97af1a..4a364b189 100644 --- a/src/NUnitEngine/nunit.engine.core/Drivers/NUnitNetStandardDriver.cs +++ b/src/NUnitEngine/nunit.engine.core/Drivers/NUnitNetStandardDriver.cs @@ -21,7 +21,7 @@ // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // *********************************************************************** -#if NETSTANDARD || NETCOREAPP3_1 +#if NETSTANDARD using System; using System.Linq; using System.Collections.Generic; @@ -35,6 +35,12 @@ namespace NUnit.Engine.Drivers /// /// NUnitNetStandardDriver is used by the test-runner to load and run /// tests using the NUnit framework assembly. + /// + /// NUnitNetStandardDriver was the original driver for the .NET Standard builds + /// of the engine, however has an issue with loading .NET Core assemblies + /// (https://github.com/nunit/nunit-console/issues/710) + /// is the replacement driver for running .NET Core tests, + /// and should be preferred for use with .NET Core 3.1 and later. /// public class NUnitNetStandardDriver : IFrameworkDriver { diff --git a/src/NUnitEngine/nunit.engine.core/Internal/CustomAssemblyLoadContext.cs b/src/NUnitEngine/nunit.engine.core/Internal/CustomAssemblyLoadContext.cs new file mode 100644 index 000000000..b68315385 --- /dev/null +++ b/src/NUnitEngine/nunit.engine.core/Internal/CustomAssemblyLoadContext.cs @@ -0,0 +1,48 @@ +// *********************************************************************** +// Copyright (c) 2020 Charlie Poole, Rob Prouse +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN METHOD +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// *********************************************************************** + +#if NETCOREAPP3_1 + +using System.Reflection; +using System.Runtime.Loader; + +namespace NUnit.Engine.Internal +{ + internal class CustomAssemblyLoadContext : AssemblyLoadContext + { + private readonly AssemblyDependencyResolver _resolver; + + public CustomAssemblyLoadContext(string mainAssemblyToLoadPath) + { + _resolver = new AssemblyDependencyResolver(mainAssemblyToLoadPath); + } + + protected override Assembly Load(AssemblyName name) + { + var assemblyPath = _resolver.ResolveAssemblyToPath(name); + return assemblyPath != null ? LoadFromAssemblyPath(assemblyPath) : null; + } + } +} + +#endif diff --git a/src/NUnitEngine/nunit.engine.tests/Drivers/NUnitNetStandardDriverTests.cs b/src/NUnitEngine/nunit.engine.tests/Drivers/NUnitNetStandardDriverTests.cs index 569dd6868..41177e841 100644 --- a/src/NUnitEngine/nunit.engine.tests/Drivers/NUnitNetStandardDriverTests.cs +++ b/src/NUnitEngine/nunit.engine.tests/Drivers/NUnitNetStandardDriverTests.cs @@ -21,7 +21,7 @@ // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // *********************************************************************** -#if NETCOREAPP +#if NETCOREAPP1_1 || NETCOREAPP2_1 using System; using System.Collections.Generic; diff --git a/src/NUnitEngine/nunit.engine.tests/Services/DriverServiceTests.cs b/src/NUnitEngine/nunit.engine.tests/Services/DriverServiceTests.cs index fcf5794a9..ba1a074d5 100644 --- a/src/NUnitEngine/nunit.engine.tests/Services/DriverServiceTests.cs +++ b/src/NUnitEngine/nunit.engine.tests/Services/DriverServiceTests.cs @@ -54,7 +54,11 @@ public void ServiceIsStarted() } -#if NETCOREAPP +#if NETCOREAPP3_1 + [TestCase("mock-assembly.dll", false, typeof(NUnitNetCore31Driver))] + [TestCase("mock-assembly.dll", true, typeof(NUnitNetCore31Driver))] + [TestCase("notest-assembly.dll", false, typeof(NUnitNetCore31Driver))] +#elif NETCOREAPP1_1 || NETCOREAPP2_1 [TestCase("mock-assembly.dll", false, typeof(NUnitNetStandardDriver))] [TestCase("mock-assembly.dll", true, typeof(NUnitNetStandardDriver))] [TestCase("notest-assembly.dll", false, typeof(NUnitNetStandardDriver))] diff --git a/src/NUnitEngine/nunit.engine.tests/Services/TestFilteringTests.cs b/src/NUnitEngine/nunit.engine.tests/Services/TestFilteringTests.cs index fd0d1d02c..cfe1f8e36 100644 --- a/src/NUnitEngine/nunit.engine.tests/Services/TestFilteringTests.cs +++ b/src/NUnitEngine/nunit.engine.tests/Services/TestFilteringTests.cs @@ -35,8 +35,10 @@ public class TestFilteringTests { private const string MOCK_ASSEMBLY = "mock-assembly.dll"; -#if NETCOREAPP +#if NETCOREAPP1_1 || NETCOREAPP2_1 private NUnitNetStandardDriver _driver; +#elif NETCOREAPP3_1 + private NUnitNetCore31Driver _driver; #else private NUnit3FrameworkDriver _driver; #endif @@ -45,8 +47,10 @@ public class TestFilteringTests public void LoadAssembly() { var mockAssemblyPath = System.IO.Path.Combine(TestContext.CurrentContext.TestDirectory, MOCK_ASSEMBLY); -#if NETCOREAPP +#if NETCOREAPP1_1 || NETCOREAPP2_1 _driver = new NUnitNetStandardDriver(); +#elif NETCOREAPP3_1 + _driver = new NUnitNetCore31Driver(); #else var assemblyName = typeof(NUnit.Framework.TestAttribute).Assembly.GetName(); _driver = new NUnit3FrameworkDriver(AppDomain.CurrentDomain, assemblyName);