nUnit Assert.That delegate concurrency issue

2019-07-15 09:19发布

问题:

I am experiencing some temporary dead lock in my code and can't wrap my head around it.

Simple code (I cannot create a simple call chain to reproduce the code in InvokeChangeEvent)

[Test]
public async void Test()
{
    sut.InvokeChangeEvent("./foo.file");
    // Event is handled by an async handler chaining multiple await resulting in a file write

    // await Task.Delay(3000);

    Assert.That(() => Directory.GetFiles("some dir").Count(), Is.EqualTo(3).After(15000, 300));

}

I am aware that y'all (:D) want executable code but I wasn't able to slice it down therefore I hope for some insight by explanation.

What happens: sut.InvokeChangeEvent calls an event handler that later calls an async event handler which then calls some async. The end of the chain results in an Task.Run that boils down to write 3 files.

The Assert above is implemented as a delegate with After that returns a DelayedConstraint and has a very large max time (15 secs) and a small polling interval.

Now when I debug the code the InvokeChangeEvent call is entirely executed to the last Task.Run but when the Task.Run returns, the execution is yielded back to the main thread and the Assert is executed entering the "wait with polling".

However the assert never succeeds. When I debug the issue the return of the Task.Run is always handled after the Assert delegate has run (and failed).

I've figured out, that when I place an await Task.Delay(3000); before the Assert, then the code executes properly.

As mentioned the system under test has plenty await and Task.Runs chained and I was unable to reproduce the issue with some easy runnable code.

I've been googling around for a while and I cannot figure out why the Task.Run (which is executed on a different thread) yield in a (temporary) deadlock even though the DelayedConstraint has an explicit polling interval to allow the main thread to progress.

It looks like the DelayedConstraint locks the main thread by some sort of Thread.Sleep. await Task.Delay does not, I am aware of that. What confuses me is I have checked that I always do an await (and never Task.Result, etc) and therefore would expect that the file has been written before the Assert has executed.

(Note: Thread.Sleep instead of await Task.Delay does not work.)

Usually the DelayedConstraint is used to ensure that file system has properly written all files as I have experienced some delays of the file system dealing with files.

I have some feeling that async void event handler may create a situation which I do not understand.

If I manage to create a simple sample, I will update the thread.

回答1:

By analogy with VS2012 unit testing, try async Task signature rather than async void for your test method. This way, NUnit should be able to keep track of the pending task status and inspect exceptions via Task.Exception.

The async void method is a fire-and-forget concept, by definition. The method returns instantly (precisely, upon the first asynchronous await inside it), and then there is no way to handle its completion or any errors possibly thrown inside it. As is, async void methods are only good for event handlers, provided exceptions are handled within the method with try/catch.