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);