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

AggregatingTestRunner to handle app domain unload failure in same fashion as single assembly runner #321

Merged
merged 5 commits into from
Dec 12, 2017

Conversation

ChrisMaddock
Copy link
Member

@ChrisMaddock ChrisMaddock commented Dec 2, 2017

Fixes #191. This still needs tests, but sounds like it might be worth discussion before I go much further.

To clarify current behaviour:

  1. 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)

  2. When running multiple assemblies via AggregatingTestRunner, if EnginePackageSettings.DisposeRunners = false, the same behaviour is maintained.

  3. 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!)

@CharliePoole
Copy link
Collaborator

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.

@ChrisMaddock
Copy link
Member Author

Thanks Charlie. No rush, I won't be back to this for a few days now. 🙂


Runners.Clear();
throw new NUnitEngineException(_thrownExceptions);
Copy link
Member Author

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.

Copy link
Collaborator

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?

Copy link
Member Author

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:

  1. Bring the AggregatingTestRunner inline with the other ITestEngineRunners.
  2. 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:

  1. 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.
  2. 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.
  3. 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.

Copy link
Member Author

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

Copy link
Collaborator

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.

Copy link
Member Author

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? 🙂

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like it!

Copy link
Collaborator

@CharliePoole CharliePoole left a 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;
}
Copy link
Collaborator

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?

Copy link
Member Author

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.
Copy link
Collaborator

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);
Copy link
Collaborator

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?

@CharliePoole
Copy link
Collaborator

Still "do not merge"?

@ChrisMaddock ChrisMaddock changed the title WIP - AggregatingTestRunner to handle app domain unload failure in same fashion as single assembly runner AggregatingTestRunner to handle app domain unload failure in same fashion as single assembly runner Dec 12, 2017
@ChrisMaddock
Copy link
Member Author

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.

@ChrisMaddock
Copy link
Member Author

Thanks Charlie. 😄

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Exception encountered unloading AppDomain
2 participants