C#:如何测试StackOverflowException

时间:2010-01-06 11:40:02

标签: c# unit-testing nunit stack-overflow

假设您有一种方法可能会陷入无休止的方法调用循环并因StackOverflowException而崩溃。例如this question中提到的我天真的RecursiveSelect方法。

  

从.NET Framework 2.0版开始,try-catch块无法捕获StackOverflowException对象,默认情况下会终止相应的进程。因此,建议用户编写代码以检测并防止堆栈溢出。例如,如果您的应用程序依赖于递归,请使用计数器或状态条件来终止递归循环。

考虑到这些信息(来自this answer),因为无法捕获异常,是否可以为这样的事情编写测试?或者对此进行测试,如果失败,实际上会打破整个测试套件?

注意: 我知道我可以尝试一下,看看会发生什么,但我对它的一般信息更感兴趣。比如,不同的测试框架和测试运行器会以不同的方式处理吗?即使有可能,我应该避免这样的测试吗?

7 个答案:

答案 0 :(得分:7)

你需要解决Halting Problem!这会让你变得富有而着名:)

答案 1 :(得分:3)

如何在断言语句中检查堆栈上的帧数?

const int MaxFrameCount = 100000;
Debug.Assert(new StackTrace().FrameCount < MaxFrameCount);

在相关问题的示例中,这将是(在发布版本中将删除昂贵的断言语句):

public static IEnumerable<T> SelectRecursive<T>(this IEnumerable<T> subjects, Func<T, IEnumerable<T>> selector)
{
    const int MaxFrameCount = 100000;
    Debug.Assert(new StackTrace().FrameCount < MaxFrameCount);

    // Stop if subjects are null or empty
    if(subjects == null || !subjects.Any())
        yield break;

    // For each subject
    foreach(var subject in subjects)
    {
        // Yield it
        yield return subject;

        // Then yield all its decendants
        foreach (var decendant in SelectRecursive(selector(subject), selector))
            yield return decendant;
    }
}

这不是一般的测试,因为你需要预期它发生,而且你只能检查帧数而不是堆栈的实际大小。也无法检查另一个呼叫是否会超出堆栈空间,您所能做的就是粗略估计总共有多少呼叫将适合您的堆栈。

答案 2 :(得分:2)

这是邪恶的,但你可以在一个新的过程中将其旋转。从单元测试启动过程并等待它完成并检查结果。

答案 3 :(得分:2)

这个想法是跟踪递归函数嵌套的深度,以便它不会占用太多的堆栈空间。例如:

string ProcessString(string s, int index) {
   if (index > 1000) return "Too deeply nested";
   s = s.Substring(0, index) + s.Substring(index, 1).ToUpper() + s.Substring(index + 1);
   return ProcessString(s, index + 1);
}

这当然不能完全保护您免受堆栈溢出的影响,因为可以使用太少的堆栈空间来调用该方法,但是它确保该方法不会导致堆栈溢出。

答案 4 :(得分:1)

我们无法对StackOverflow进行测试,因为在没有剩余堆栈用于分配的情况下,应用程序将在这种情况下自动退出。

答案 5 :(得分:1)

如果您正在编写其他人将要使用的库代码,那么堆栈溢出往往比其他错误更糟糕,因为其他代码不能只吞下StackOverflowException;他们的整个过程都在下降。

编写一个期望并捕获StackOverflowException的测试没有简单的方法,但这不是您想要测试的行为!

以下是测试代码的一些提示没有堆栈溢出:

  • 并行化您的测试运行。如果您有一个单独的测试套件用于堆栈溢出情况,那么如果一个测试运行器出现故障,您仍将从其他测试中获得结果。 (即使你没有将测试运行分开,我也会认为堆栈溢出非常糟糕,如果它发生的话,它值得崩溃整个测试运行器。你的代码不应该首先破解!)

  • 线程可能具有不同的堆栈空间量,如果有人调用您的代码,则无法控制可用的堆栈空间量。虽然32位CLR的默认值是1MB堆栈大小而64位是2MB堆栈大小,但请注意web servers will default to a much smaller stack。如果你想验证你的代码不会以较少的可用空间进行堆栈溢出,那么你的测试代码可以使用一个Thread构造函数来获取较小的堆栈大小。

  • 测试你发布的每个不同的构建风格(发布和调试?有或没有调试符号?x86和x64和AnyCPU?)和你支持的平台(64位?32位?64位操作系统运行32 bit?.NET 4.5?3.5?mono?)。生成的本机代码中堆栈帧的实际大小可能不同,因此一台机器上的中断可能不会在另一台机器上中断。

  • 由于您的测试可能会在一台构建计算机上传递但在另一台构建计算机上失败,因此请确保如果它开始失败,则不会阻止整个项目的签入!

  • 一旦衡量了迭代N导致代码堆栈溢出的次数,请不要只测试这个数字!我测试了一个更大的数字(50 N?)的迭代没有堆栈溢出(所以你不会得到传递一个构建服务器的测试但是在另一个构建服务器上失败)。

  • 考虑一下函数最终可以调用自身的每个可能的代码路径。您的产品可能会阻止X() -> X() -> X() -> ...递归堆栈溢出,但是如果X() -> A() -> B() -> C() -> ... -> W() -> X() -> A() -> ...递归情况仍然存在,该怎么办?

PS:我没有关于此策略的更多详细信息,但显然如果您的代码hosts the CLR,那么您可以指定堆栈溢出只会导致AppDomain崩溃?

答案 6 :(得分:0)

首先,我认为该方法应该处理这个并确保它不会过于沉重。

当完成此检查时,我认为应该通过暴露达到的最大深度来测试检查 - 如果有的话 - 并断言它绝不会大于检查所允许的。