NUnit не дает сбой при исключении в Finalizer

В нашей структуре есть некоторые ключевые объекты, которые имеют дескрипторы файлов или клиентские подключения WCF. Эти объекты IDiposable, и у нас есть код проверки (с генерируемыми исключениями), чтобы гарантировать, что они правильно удаляются, когда они больше не нужны. (Только отладка, поэтому мы не хотим сбой при выпуске). Это не обязательно при выключении.

Вдобавок к этому у нас есть модульные тесты, которые запускают наш код, и поэтому мы ожидаем, что они потерпят неудачу, если мы забудем о таких утилизациях.

Проблема: в .NET 4.5.1 с исполнителем NUnit (2.6.3.13283) (или с ReSharper, или TeamCity) не запускается ошибка теста, когда возникает такое исключение в Finalizer.

Странная вещь: использование NCrunch (с NUnit тоже), модульные тесты < strong> НЕОБХОДИМО потерпеть неудачу! (Что для меня локально, по крайней мере, я могу найти такие недостающие утилизации)

Это очень плохо, так как наша сборочная машина (TeamCity) не видит таких сбоев, и мы думаем, что все хорошо! Но запуск нашего программного обеспечения (в отладке) действительно приведет к сбою, показывая, что мы забыли утилизацию

Вот пример, который показывает, что NUnit не дает сбоев

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();
    }
}

Выполнение этого в средстве выполнения NUnit: все зеленое. Если вы попросите ReSharper отладить этот тест, он действительно войдет в Finalizer.


person FrankyB    schedule 20.08.2015    source источник
comment
На время оставьте финализаторы в стороне. Предположим, у вас есть тест NUnit, который создает новый поток, который запускает код, который выдает. Тест не прошел? (Вопрос не риторический; я не знаю.) Если тест не проходит, каким механизмом тест обнаруживает исключение в другом потоке? Если это не сработает, то почему вы ожидаете, что ситуация изменится, если это поток финализатора?   -  person Eric Lippert    schedule 22.08.2015
comment
Хороший момент, Эрик! Вы правы, NUnit не ловит исключения в другом потоке! Итак, я нашел несколько советов по этому поводу и найду способ исправить это! Спасибо!   -  person FrankyB    schedule 26.08.2015


Ответы (3)


Итак, с помощью Эрика Липперта я выяснил, что Exceptions не перехватываются NUnit, когда они находятся в другом потоке. То же самое происходит с потоком финализатора.

Я пытался найти решение в настройках NUnit, но безуспешно.

Итак, я придумал подклассы всех моих TestFixture, так что для всех моих тестов были общие [SetUp] и [TearDown]:

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;
    }
}

Очевидно, с помощью этого кода я могу знать только то, что было сгенерировано исключение, но я не знаю, какое именно. Однако для моего использования этого достаточно. Если я изменю его позже, я попытаюсь обновить его (или, если у кого-то есть лучшее решение, я рад использовать его в качестве решения!)

У меня было два сценария, которые мне нужно было рассмотреть, поэтому я включил их здесь:

[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();
    }
}

Что касается того, почему NCrunch делает это правильно, это хороший вопрос ...

person FrankyB    schedule 23.09.2015
comment
Что ж, вместо сохранения логического флага (_exceptionWasThrown) вы можете просто сохранить само исключение, чтобы сообщить о нем. - person jeroenh; 23.09.2015

Исключения в финализаторах различны, см. исключение, генерирующее финализатор c #?.

В ранних версиях .Net они игнорируются. В более новой версии CLR завершается с фатальной ошибкой.

person Richard Schneider    schedule 20.08.2015
comment
Полезная информация, которую нужно знать, и я добавил версию .Net, которую использую, поскольку это имеет значение (я на 4.5.1) - person FrankyB; 20.08.2015

Чтобы процитировать Эрика Липперта (который знает об этом не меньше других):

Вызовите метко названный WaitForPendingFinalizers после вызова Collect, если хотите гарантировать, что все финализаторы работают. Это приостановит текущий поток, пока поток финализатора не дойдет до очистки очереди. И если вы хотите, чтобы память этих финализированных объектов была освобождена, вам нужно будет вызвать Collect второй раз. [Выделение добавлено]

Непоследовательное поведение при работе в разных средах лишь подчеркивает, насколько сложно предсказать поведение сборщика мусора. Дополнительные сведения о сборке мусора см. В статьях Раймонда Чена:

Или записи в блоге Эрика:

person theB    schedule 20.08.2015
comment
И, к сожалению, много поплакав, я уже 3 раза пробовал стоять в очереди GC.Collect(); GC.WaitForPendingFinalizers();, добавил даже задержки между ними, чтобы посмотреть, помогает ли это каким-либо образом - person FrankyB; 21.08.2015