Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use AssemblyLoadContext to correctly load .NET Core assemblies #781

Merged
merged 2 commits into from
Jun 26, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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.
/// </summary>
/// <param name="domain">The domain in which the assembly will be loaded</param>
/// <param name="reference">An AssemblyName referring to the test framework.</param>
/// <returns></returns>
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
}
Expand Down
208 changes: 208 additions & 0 deletions src/NUnitEngine/nunit.engine.core/Drivers/NUnitNetCore31Driver.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// 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.
/// </summary>
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;

/// <summary>
/// An id prefix that will be passed to the test framework and used as part of the
/// test ids created.
/// </summary>
public string ID { get; set; }

/// <summary>
/// Loads the tests in an assembly.
/// </summary>
/// <param name="assemblyPath">The path to the test assembly</param>
/// <param name="settings">The test settings</param>
/// <returns>An XML string representing the loaded test</returns>
public string Load(string assemblyPath, IDictionary<string, object> 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;
}

/// <summary>
/// Counts the number of test cases for the loaded test assembly
/// </summary>
/// <param name="filter">The XML test filter</param>
/// <returns>The number of test cases</returns>
public int CountTestCases(string filter)
{
CheckLoadWasCalled();
object count = ExecuteMethod(COUNT_METHOD, filter);
return count != null ? (int)count : 0;
}

/// <summary>
/// Executes the tests in an assembly.
/// </summary>
/// <param name="listener">An ITestEventHandler that receives progress notices</param>
/// <param name="filter">A filter that controls which tests are executed</param>
/// <returns>An Xml string representing the result</returns>
public string Run(ITestEventListener listener, string filter)
{
CheckLoadWasCalled();
log.Info("Running {0} - see separate log file", _testAssembly.FullName);
Action<string> callback = listener != null ? listener.OnTestEvent : (Action<string>)null;
return ExecuteMethod(RUN_METHOD, new[] { typeof(Action<string>), typeof(string) }, callback, filter) as string;
}

/// <summary>
/// Executes the tests in an assembly asyncronously.
/// </summary>
/// <param name="callback">A callback that receives XML progress notices</param>
/// <param name="filter">A filter that controls which tests are executed</param>
public void RunAsync(Action<string> callback, string filter)
{
CheckLoadWasCalled();
log.Info("Running {0} - see separate log file", _testAssembly.FullName);
ExecuteMethod(RUN_ASYNC_METHOD, new[] { typeof(Action<string>), typeof(string) }, callback, filter);
}

/// <summary>
/// Cancel the ongoing test run. If no test is running, the call is ignored.
/// </summary>
/// <param name="force">If true, cancel any ongoing test threads, otherwise wait for them to complete.</param>
public void StopRun(bool force)
{
ExecuteMethod(STOP_RUN_METHOD, force);
}

/// <summary>
/// Returns information about the tests in an assembly.
/// </summary>
/// <param name="filter">A filter indicating which tests to include</param>
/// <returns>An Xml string representing the tests</returns>
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
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -35,6 +35,12 @@ namespace NUnit.Engine.Drivers
/// <summary>
/// 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)
/// <see cref="NUnitNetCore31Driver" /> is the replacement driver for running .NET Core tests,
/// and should be preferred for use with .NET Core 3.1 and later.
/// </summary>
public class NUnitNetStandardDriver : IFrameworkDriver
{
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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))]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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);
Expand Down