NUnit does not fail on exception in Finalizer
Asked Answered
M

3

3

In our framework, there is some key objects which have file handles or WCF client connections. Those objects are IDiposable and we have validation code (with exceptions being thrown) to ensure that they are getting properly disposed when not needed anymore. (Debug-only so that we don't want to crash on release). This is not necessarily on shutdown.

On top of this, we have unit tests which run our code and we thus expect them to fail if we forget such disposals.

Problem is: On .NET 4.5.1, with NUnit (2.6.3.13283) runner (or with ReSharper, or TeamCity) does not trigger test failure when such exception in the Finalizer are thrown.

Weird thing is: Using NCrunch (with is over NUnit also), unit tests DO fail! (Which locally for me, at least I can find such missing disposals)

That's pretty bad, since our build machine (TeamCity) does not see such failures and we think that everything is good! But running our software (in debug) will indeed crash, showing that we forgot a disposal

Here's an example that shows that NUnit does not fail

public class ExceptionInFinalizerObject
{
    ~ExceptionInFinalizerObject()
    {
        //Tried here both "Assert.Fail" and throwing an exception to be sure
        Assert.Fail();
        throw new Exception();
    }
}

[TestFixture]
public class FinalizerTestFixture
{
    [Test]
    public void FinalizerTest()
    {
        CreateFinalizerObject();

        GC.Collect();
        GC.WaitForPendingFinalizers();
    }

    public void CreateFinalizerObject()
    {
        //Create the object in another function to put it out of scope and make it available for garbage collection
        new ExceptionInFinalizerObject();
    }
}

Running this in the NUnit runner: everything is green. Asking ReSharper to debug this test will indeed step into the Finalizer.

Merilee answered 20/8, 2015 at 9:37 Comment(2)
Leave finalizers out of it for a moment. Suppose you have an NUnit test which creates a new thread, that thread runs code which throws. Does the test fail? (Not a rhetorical question; I don't know.) If the test does fail, by what mechanism does the test detect an exception on another thread? If it does not fail, then why would you expect the situation to be different when its the finalizer thread?Eclosion
Good point Eric! You are right, NUnit does not catch exceptions on another thread! So I've found a few hints about it and will get a proper way to fix it! Thank you!Merilee
M
4

So with the help of Eric Lippert, I found out that Exceptions are not caught by NUnit when they are on another thread. So the same happens to the finalizer thread.

I tried finding a solution in the settings of NUnit, but to no avail.

So I came up with subclassing all my TestFixture, so that there is a common [SetUp] and [TearDown] to all my tests:

public class BaseTestFixture
{
    private UnhandledExceptionEventHandler _unhandledExceptionHandler;
    private bool _exceptionWasThrown;

    [SetUp]
    public void UnhandledExceptionRegistering()
    {
        _exceptionWasThrown = false;
        _unhandledExceptionHandler = (s, e) =>
        {
            _exceptionWasThrown = true;
        };

        AppDomain.CurrentDomain.UnhandledException += _unhandledExceptionHandler;
    }

    [TearDown]
    public void VerifyUnhandledExceptionOnFinalizers()
    {
        GC.Collect();
        GC.WaitForPendingFinalizers();

        Assert.IsFalse(_exceptionWasThrown);

        AppDomain.CurrentDomain.UnhandledException -= _unhandledExceptionHandler;
    }
}

Obviously, with this code I can only know that an exception was thrown, but I don't know which one. However, for my usage, it is sufficient. If I change it later, I'll try and update this one (or if someone has a better solution, I'm glad to set as a solution!)

I had two scenarios I needed to cover, so I include them here:

[TestFixture]
public class ThreadExceptionTestFixture : BaseTestFixture
{
    [Test, Ignore("Testing-Testing test: Enable this test to validate that exception in threads are properly caught")]
    public void ThreadExceptionTest()
    {
        var crashingThread = new Thread(CrashInAThread);
        crashingThread.Start();
        crashingThread.Join(500);
    }

    private static void CrashInAThread()
    {
        throw new Exception();
    }

    [Test, Ignore("Testing-Testing test: Enable this test to validate that exceptions in Finalizers are properly caught")]
    public void FinalizerTest()
    {
        CreateFinalizerObject();

        GC.Collect();
        GC.WaitForPendingFinalizers();
    }

    public void CreateFinalizerObject()
    {
        //Create the object in another function to put it out of scope and make it available for garbage collection
        new ExceptionInFinalizerObject();
    }
}

public class ExceptionInFinalizerObject
{
    ~ExceptionInFinalizerObject()
    {
        throw new Exception();
    }
}

As for why NCrunch does it properly, that's a good question...

Merilee answered 23/9, 2015 at 7:53 Comment(1)
Well, instead of keeping a boolean flag (_exceptionWasThrown), you could simply store the Exception itself to report it.Archaism
R
1

Exceptions in finalisers are different, see c# finalizer throwing exception?.

In early .Net they are ignored. In newer version the CLR exits with a fatal error.

Roderic answered 20/8, 2015 at 9:51 Comment(1)
Good info to know, and I added the .Net version I'm using, since it seems to matter (I'm on 4.5.1)Merilee
B
1

To quote Eric Lippert (who knows as much about this as pretty much anyone):

Call the aptly named WaitForPendingFinalizers after calling Collect if you want to guarantee that all finalizers have run. That will pause the current thread until the finalizer thread gets around to emptying the queue. And if you want to ensure that those finalized objects have their memory reclaimed then you're going to have to call Collect a second time. [Emphasis Added]

The inconsistent behavior when running in different environments just highlights how difficult it is to predict GC behavior. For more information about garbage collection see Raymond Chen's articles:

Or Eric's blog entries:

Borman answered 20/8, 2015 at 13:36 Comment(1)
And sadly, after crying a lot, I've already tried queuing 3 times GC.Collect(); GC.WaitForPendingFinalizers();, added even delays between them to see if it helps in any wayMerilee

© 2022 - 2024 — McMap. All rights reserved.