-
Notifications
You must be signed in to change notification settings - Fork 149
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
AggregatingTestRunner to handle app domain unload failure in same fashion as single assembly runner #321
Conversation
…ame fashion as main test runners
Based solely on the description, this makes sense to me. I'll be flying home tomorrow, so if the jet lag lets me I can look at code the next day. |
Thanks Charlie. No rush, I won't be back to this for a few days now. 🙂 |
|
||
Runners.Clear(); | ||
throw new NUnitEngineException(_thrownExceptions); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just come across this - D'oh! Of course, that should only throw when there are exceptions to be thrown - will fix this before merge.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So, throwing is the whole point of this change. I want to go back and question why we are throwing in the case of failure to unload. I mentioned this elsewhere (on the issue I think) and I wonder why we can't just report this as an error (or warning) associated with the individual assembly as we do in other cases. The difference here, of course, is that only the engine can detect the error as opposed to the framework. Worth discussing?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For the avoidance of doubt, to be clear of the things I'm aiming to do here:
- Bring the AggregatingTestRunner inline with the other ITestEngineRunners.
- Have the engine throw an exception on domain unload failure, caught by the runner.
The current issue is that the AggregatingTestRunner is throwing the exception mid-run, where as the other ITestEngineRunners would all throw the exception at the end of the test run, as the ITestEngineRunner is disposed, and once the full results have already been returned to the runner.
I think that design currently works. A few reasons:
- I don't think we should silently swallow the exception, this exception indicates an unclean shut down in the user's code. By returning a distinct unload-failure-error-code such as in Return error code -5 when AppDomain fails to unload #320 - we still give the user the choice to ignore this if they wish to.
- Re: associating with xml - by the time the domain is unloaded and the engine cleaned up, the results have already been returned to the runner. Would we want to alter this behaviour in all situations - e.g. with a GUI, unload wouldn't happen after each run, correct? I don't see the advantage of holding on to the xml, incase we wish to add an error that's not strictly related to the test results.
- I feel like app domain unload belongs semantically as an engine exception, rather than a 'test result' in the xml. i.e. I could chose to run in the main domain, in which case I would get no exception.
I don't think that this exception should escape the runner - the current behaviour of handling this within the runner seems correct to me. The main issue in my eyes, is that the exception can currently cause a test run not to be completed, and results which have already ran to be lost. This shouldn't happen.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(The term 'runner' is overloaded in this conversation! I've edited to be clear when I mean ITestEngineRunner - and when I mean runner as in 'console runner'/GUI etc.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see your point about the separation of running from unloading. In that case, I suppose it's up to the runner to decide what to do with the exceptions - error, warn, etc. That would mean that unload exceptions need to be distinguishable from others of course.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How about an NUnitUnloadException which inherits from NUnitEngineExceptions, to not break any existing runners by throwning a new Exception type? 🙂
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like it!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Questions on the details plus the big one about whether we should really throw for failure to unload.
: base(AggregatedExceptionsMsg, new NUnitEngineException(AggregatedExceptionsMsg)) | ||
{ | ||
AggregatedExceptions = aggregatedExceptions; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not clear what you are doing here. Seems like you end up with an NUnitEngineException
with an InnerException
that's also an NUnitEngineException
, both having the same message. The outer also gets a collection of aggregated exceptions. What's the point of the InnerException
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I was maybe thinking of NUnitException
in the framework, where I thought we'd always expect an inner exception? I'll get rid of it.
|
||
#if !NETSTANDARD1_3 | ||
/// <summary> | ||
/// Serialization constructor | ||
/// </summary> | ||
public NUnitEngineException(SerializationInfo info, StreamingContext context) : base(info, context) { } | ||
#endif | ||
|
||
/// <summary> | ||
/// Gets the Exception instance that caused the current exception. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Comment is wrong.
|
||
Runners.Clear(); | ||
throw new NUnitEngineException(_thrownExceptions); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So, throwing is the whole point of this change. I want to go back and question why we are throwing in the case of failure to unload. I mentioned this elsewhere (on the issue I think) and I wonder why we can't just report this as an error (or warning) associated with the individual assembly as we do in other cases. The difference here, of course, is that only the engine can detect the error as opposed to the framework. Worth discussing?
Still "do not merge"? |
Was just waiting for CI to run last night, and didn't get a chance to come back to it. 😄 Ready for a second review. |
Thanks Charlie. 😄 |
Fixes #191. This still needs tests, but sounds like it might be worth discussion before I go much further.
To clarify current behaviour:
When running a single assembly, if the AppDomain fails to unload as
ProcessRunner
is disposed, the exception is caught - and the console runner continues to write the already completed TestResults. A warning is then issued, and the console exits with 0. (To be changed in Return error code -5 when AppDomain fails to unload #320)When running multiple assemblies via AggregatingTestRunner, if
EnginePackageSettings.DisposeRunners = false
, the same behaviour is maintained.If
EnginePackageSettings.DisposeRunners=true
however (default for the console), then the test runners are disposed at the end of each test run, and there is no functionality to handle CannotUnloadAppDomain exceptions, as there is at the other points where TestRunners are disposed.This is less of a problem when running via AggregatingTestRunner in parallel, as the CannotUnloadAppDomain exception is thrown on a separate thread, after the result has already been set. This means (I think, I haven't dug into this) that the exception just crashes the test running thread right at the end of the run - but the error is silently ignored, and the result can still be retrieved.
However, when AggregatingTestRunner is run sequentially (As the user may choose, or as #116 is currently causing all .nunit project users to see) then the exception is thrown on the main thread, with no handing, and the run is terminated immediately with no results (or only the results achieved so far.)
My opinion is that this situation should be handled the same way regardless of the user's choice of execution method. This PR fixes the early termination for running the AggregatingTestRunner sequentially, and catches the exceptions not being handled properly when running AggregatingTestRunner in parallel. This should standardise the handling of CannotUnloadAppDomain, regardless of the number of assemblies, or parallelisation choices.
WIP as I need to add tests, but it sounded like it would be sensible to put this up for feedback before putting much more work into it. 😄
(Edit: looks like I broke netstandard. Will revisit when I come back to this!)