diff --git a/src/NUnitEngine/nunit.engine.api/NUnitEngineException.cs b/src/NUnitEngine/nunit.engine.api/Exceptions/NUnitEngineException.cs similarity index 93% rename from src/NUnitEngine/nunit.engine.api/NUnitEngineException.cs rename to src/NUnitEngine/nunit.engine.api/Exceptions/NUnitEngineException.cs index 820ab3542..aa1812835 100644 --- a/src/NUnitEngine/nunit.engine.api/NUnitEngineException.cs +++ b/src/NUnitEngine/nunit.engine.api/Exceptions/NUnitEngineException.cs @@ -41,14 +41,16 @@ public class NUnitEngineException : Exception /// /// Construct with a message /// - public NUnitEngineException(string message) : base(message) { } + public NUnitEngineException(string message) : base(message) + { + } /// /// Construct with a message and inner exception /// - /// - /// - public NUnitEngineException(string message, Exception innerException) : base(message, innerException) { } + public NUnitEngineException(string message, Exception innerException) : base(message, innerException) + { + } #if !NETSTANDARD1_3 /// diff --git a/src/NUnitEngine/nunit.engine.api/NUnitEngineNotFoundException.cs b/src/NUnitEngine/nunit.engine.api/Exceptions/NUnitEngineNotFoundException.cs similarity index 100% rename from src/NUnitEngine/nunit.engine.api/NUnitEngineNotFoundException.cs rename to src/NUnitEngine/nunit.engine.api/Exceptions/NUnitEngineNotFoundException.cs diff --git a/src/NUnitEngine/nunit.engine.api/Exceptions/NUnitEngineUnloadException.cs b/src/NUnitEngine/nunit.engine.api/Exceptions/NUnitEngineUnloadException.cs new file mode 100644 index 000000000..f0bae47d1 --- /dev/null +++ b/src/NUnitEngine/nunit.engine.api/Exceptions/NUnitEngineUnloadException.cs @@ -0,0 +1,81 @@ +// *********************************************************************** +// Copyright (c) 2017 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 ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// *********************************************************************** + +using System; +using System.Collections.Generic; +#if !NETSTANDARD1_3 +using System.Runtime.Serialization; +#endif + +namespace NUnit.Engine +{ + + /// + /// NUnitEngineUnloadException is thrown when a test run has completed successfully + /// but one or more errors were encountered when attempting to unload + /// and shut down the test run cleanly. + /// +#if !NETSTANDARD1_3 + [Serializable] +#endif + public class NUnitEngineUnloadException : NUnitEngineException //Inherits from NUnitEngineException for backwards compatibility of calling runners + { + private const string AggregatedExceptionsMsg = + "Multiple exceptions encountered. Retrieve AggregatedExceptions property for more information"; + + + /// + /// Construct with a message + /// + public NUnitEngineUnloadException(string message) : base(message) + { + } + + /// + /// Construct with a message and inner exception + /// + public NUnitEngineUnloadException(string message, Exception innerException) : base(message, innerException) + { + } + + /// + /// Construct with a message and a collection of exceptions. + /// + public NUnitEngineUnloadException(ICollection aggregatedExceptions) : base(AggregatedExceptionsMsg) + { + AggregatedExceptions = aggregatedExceptions; + } + +#if !NETSTANDARD1_3 + /// + /// Serialization constructor. + /// + public NUnitEngineUnloadException(SerializationInfo info, StreamingContext context) : base(info, context) { } +#endif + + /// + /// Gets the collection of exceptions . + /// + public ICollection AggregatedExceptions { get; } + } +} \ No newline at end of file diff --git a/src/NUnitEngine/nunit.engine.api/nunit.engine.api.csproj b/src/NUnitEngine/nunit.engine.api/nunit.engine.api.csproj index c6f4f009d..2da76bbe7 100644 --- a/src/NUnitEngine/nunit.engine.api/nunit.engine.api.csproj +++ b/src/NUnitEngine/nunit.engine.api/nunit.engine.api.csproj @@ -56,6 +56,7 @@ Properties\EngineApiVersion.cs + @@ -73,7 +74,7 @@ - + @@ -82,7 +83,7 @@ - + @@ -106,4 +107,4 @@ --> - + \ No newline at end of file diff --git a/src/NUnitEngine/nunit.engine.netstandard/nunit.engine.netstandard.csproj b/src/NUnitEngine/nunit.engine.netstandard/nunit.engine.netstandard.csproj index 6ad145c63..4849da455 100644 --- a/src/NUnitEngine/nunit.engine.netstandard/nunit.engine.netstandard.csproj +++ b/src/NUnitEngine/nunit.engine.netstandard/nunit.engine.netstandard.csproj @@ -29,6 +29,8 @@ + + Extensibility\IFrameworkDriver.cs @@ -139,9 +141,6 @@ ITestRunner.cs - - NUnitEngineException.cs - TestFilter.cs diff --git a/src/NUnitEngine/nunit.engine.tests/Services/DomainManagerTests.cs b/src/NUnitEngine/nunit.engine.tests/Services/DomainManagerTests.cs index 0a2bbf1e5..338f42e5e 100644 --- a/src/NUnitEngine/nunit.engine.tests/Services/DomainManagerTests.cs +++ b/src/NUnitEngine/nunit.engine.tests/Services/DomainManagerTests.cs @@ -100,12 +100,12 @@ public void CanUnloadDomain() } [Test] - public void UnloadingTwiceThrowsNUnitEngineException() + public void UnloadingTwiceThrowsNUnitEngineUnloadException() { var domain = _domainManager.CreateDomain(_package); _domainManager.Unload(domain); - Assert.That(() => _domainManager.Unload(domain), Throws.TypeOf()); + Assert.That(() => _domainManager.Unload(domain), Throws.TypeOf()); CheckDomainIsUnloaded(domain); } diff --git a/src/NUnitEngine/nunit.engine/Internal/ExceptionHelper.cs b/src/NUnitEngine/nunit.engine/Internal/ExceptionHelper.cs index d4b2a15f6..f521f5fe0 100644 --- a/src/NUnitEngine/nunit.engine/Internal/ExceptionHelper.cs +++ b/src/NUnitEngine/nunit.engine/Internal/ExceptionHelper.cs @@ -25,6 +25,7 @@ using System.Collections.Generic; using System.Reflection; using System.Text; +using NUnit.Engine; namespace NUnit.Common { @@ -100,9 +101,18 @@ private static List FlattenExceptionHierarchy(Exception exception) { var result = new List(); - if (exception is ReflectionTypeLoadException) + var unloadException = exception as NUnitEngineUnloadException; + if (unloadException?.AggregatedExceptions != null) + { + result.AddRange(unloadException.AggregatedExceptions); + + foreach (var aggregatedException in unloadException.AggregatedExceptions) + result.AddRange(FlattenExceptionHierarchy(aggregatedException)); + } + + var reflectionException = exception as ReflectionTypeLoadException; + if (reflectionException != null) { - var reflectionException = exception as ReflectionTypeLoadException; result.AddRange(reflectionException.LoaderExceptions); foreach (var innerException in reflectionException.LoaderExceptions) diff --git a/src/NUnitEngine/nunit.engine/Runners/AggregatingTestRunner.cs b/src/NUnitEngine/nunit.engine/Runners/AggregatingTestRunner.cs index 61c87d472..9d1dc6706 100644 --- a/src/NUnitEngine/nunit.engine/Runners/AggregatingTestRunner.cs +++ b/src/NUnitEngine/nunit.engine/Runners/AggregatingTestRunner.cs @@ -21,9 +21,14 @@ // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // *********************************************************************** +using System; using System.Collections.Generic; using NUnit.Engine.Internal; +#if !NETSTANDARD1_3 +using NUnit.Common; +#endif + namespace NUnit.Engine.Runners { /// @@ -39,6 +44,11 @@ public class AggregatingTestRunner : AbstractTestRunner // of writing this comment) be either TestDomainRunners or ProcessRunners. private List _runners; + // Exceptions from unloading individual runners are caught and rethrown + // on AggregatingTestRunner disposal, to allow TestResults to be + // written and execution of other runners to continue. + private readonly List _unloadExceptions = new List(); + // Public for testing purposes public virtual int LevelOfParallelism { @@ -73,7 +83,7 @@ public AggregatingTestRunner(IServiceLocator services, TestPackage package) : ba } #endif - #region AbstractTestRunner Overrides +#region AbstractTestRunner Overrides /// /// Explore a TestPackage and return information about @@ -115,7 +125,16 @@ protected override TestEngineResult LoadPackage() public override void UnloadPackage() { foreach (ITestEngineRunner runner in Runners) - runner.Unload(); + { + try + { + runner.Unload(); + } + catch (Exception e) + { + _unloadExceptions.Add(e); + } + } } /// @@ -169,8 +188,9 @@ private void RunTestsSequentially(ITestEventListener listener, TestFilter filter { foreach (ITestEngineRunner runner in Runners) { - results.Add(runner.Run(listener, filter)); - if (disposeRunners) runner.Dispose(); + var task = new TestExecutionTask(runner, listener, filter, disposeRunners); + task.Execute(); + LogResultsFromTask(task, results, _unloadExceptions); } } @@ -191,7 +211,7 @@ private void RunTestsInParallel(ITestEventListener listener, TestFilter filter, workerPool.WaitAll(); foreach (var task in tasks) - results.Add(task.Result()); + LogResultsFromTask(task, results, _unloadExceptions); } #endif @@ -210,9 +230,21 @@ protected override void Dispose(bool disposing) base.Dispose(disposing); foreach (var runner in Runners) - runner.Dispose(); + { + try + { + runner.Dispose(); + } + catch (Exception e) + { + _unloadExceptions.Add(e); + } + } Runners.Clear(); + + if (_unloadExceptions.Count > 0) + throw new NUnitEngineUnloadException(_unloadExceptions); } #endregion @@ -221,5 +253,19 @@ protected virtual ITestEngineRunner CreateRunner(TestPackage package) { return TestRunnerFactory.MakeTestRunner(package); } + + private static void LogResultsFromTask(TestExecutionTask task, List results, List unloadExceptions) + { + var result = task.Result; + if (result != null) + { + results.Add(result); + } + + if (task.UnloadException != null) + { + unloadExceptions.Add(task.UnloadException); + } + } } } diff --git a/src/NUnitEngine/nunit.engine/Runners/TestExecutionTask.cs b/src/NUnitEngine/nunit.engine/Runners/TestExecutionTask.cs index 64f639016..ede8a385b 100644 --- a/src/NUnitEngine/nunit.engine/Runners/TestExecutionTask.cs +++ b/src/NUnitEngine/nunit.engine/Runners/TestExecutionTask.cs @@ -21,6 +21,8 @@ // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // *********************************************************************** +using System; + namespace NUnit.Engine.Runners { public class TestExecutionTask : ITestExecutionTask @@ -30,6 +32,8 @@ public class TestExecutionTask : ITestExecutionTask private readonly TestFilter _filter; private volatile TestEngineResult _result; private readonly bool _disposeRunner; + private bool _hasExecuted = false; + private Exception _unloadException; public TestExecutionTask(ITestEngineRunner runner, ITestEventListener listener, TestFilter filter, bool disposeRunner) { @@ -41,14 +45,44 @@ public TestExecutionTask(ITestEngineRunner runner, ITestEventListener listener, public void Execute() { - _result = _runner.Run(_listener, _filter); - if (_disposeRunner) - _runner.Dispose(); + _hasExecuted = true; + try + { + _result = _runner.Run(_listener, _filter); + } + finally + { + try + { + if (_disposeRunner) + _runner.Dispose(); + } + catch (Exception e) + { + _unloadException = e; + } + } + } + + public TestEngineResult Result + { + get + { + Guard.OperationValid(_hasExecuted, "Can not access result until task has been executed"); + return _result; + } } - public TestEngineResult Result() + /// + /// Stored exception thrown during test assembly unload. + /// + public Exception UnloadException { - return _result; + get + { + Guard.OperationValid(_hasExecuted, "Can not access thrown exceptions until task has been executed"); + return _unloadException; + } } } } diff --git a/src/NUnitEngine/nunit.engine/Services/DomainManager.cs b/src/NUnitEngine/nunit.engine/Services/DomainManager.cs index b0af4c0cc..a4f0585d9 100644 --- a/src/NUnitEngine/nunit.engine/Services/DomainManager.cs +++ b/src/NUnitEngine/nunit.engine/Services/DomainManager.cs @@ -167,11 +167,11 @@ public void Unload() log.Error(msg); Kill(_unloadThread); - throw new NUnitEngineException(msg); + throw new NUnitEngineUnloadException(msg); } if (_unloadException != null) - throw new NUnitEngineException("Exception encountered unloading AppDomain", _unloadException); + throw new NUnitEngineUnloadException("Exception encountered unloading AppDomain", _unloadException); } private void UnloadOnThread()