【问题标题】:C#: How to test for StackOverflowExceptionC#:如何测试 StackOverflowException
【发布时间】:2011-01-02 00:38:54
【问题描述】:

假设您有一个方法可能会陷入无限的方法调用循环并因 StackOverflowException 而崩溃。比如我在this question中提到的幼稚的RecursiveSelect方法。

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

考虑到该信息(来自this answer),由于无法捕获异常,是否有可能为这样的事情编写测试?或者如果测试失败,是否会破坏整个测试套件?

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

【问题讨论】:

  • 您似乎在进行某种静态分析,该分析会告诉您方法是否每次都会终止。
  • 也许吧?我在想的是,在提到的方法的情况下,发送一个确实有循环的对象图,并确保它不会抛出 StackOverflowException。但是,由于根据来自微软的消息,该异常无法被捕获。那么你将如何为这种情况编写测试呢?
  • StackOverFlow 异常确实会杀死 NUnit。其他跑步者可能会在某种程度上幸存下来。他们中的一些人在新流程中运行测试,并且可能能够将结果报告给 StackOverflow。由于 NUnit 仅使用 AppDomain 进行分离,我几乎可以肯定它会影响整个测试运行器。
  • 我自己现在使用 TestDriven.Net 作为测试运行器进行了尝试。显然它似乎只是挂起......?

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


【解决方案1】:

您需要解决Halting Problem!那会让你变得富有和出名:)

【讨论】:

  • Awesome... 届时将开始努力!对...... :p 除了愚蠢之外,我想这个问题的答案将是“不能做”的事情。哦,好吧:)
  • @Svish - 你可以做到,只是不是很自然。它需要在另一个进程中运行被测代码并处理检查该进程结果的痛苦。
  • 我向您推荐 Byron Cook 博士的工作,他成功地开发了技术来证明某些类型的代码将终止。 bcs.org/server.php?show=nav.11472
  • 停机问题与这个问题没有任何关系。一个被证明可以退出的方法很容易导致堆栈溢出,而一个方法从不退出这一事实本身并不会导致堆栈溢出。
  • @Guffa,真的。停机问题更像是一个逻辑问题,适用于资源无限的机器,而堆栈溢出发生在资源有限的机器上。它仍然强调了您在尝试预测堆栈溢出时会遇到的困难。
【解决方案2】:

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

【讨论】:

  • 啊,是的。使用批处理文件来隔离不可预测或长时间运行的例程的记忆......
【解决方案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;
    }
}

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

【讨论】:

  • 这应该发生在你的递归函数中。不过,这不是一般测试,因为您需要预期它会发生,而且您只能检查帧数而不是堆栈的实际大小。
  • 没错。有趣的解决方案,但不是我认为我不希望在我的方法中有类似的东西,呵呵。
  • 可能更多的是实用而非美观的方法,但不要忘记在发布版本中删除了代价高昂的 Debug.Assert 语句。
【解决方案4】:

这个想法是跟踪递归函数的嵌套深度,这样它就不会使用太多的堆栈空间。示例:

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

这当然不能完全保护您免受堆栈溢出的影响,因为可以在开始时剩余的堆栈空间太少的情况下调用该方法,但它可以确保该方法不会单独导致堆栈溢出。

【讨论】:

    【解决方案5】:

    我们无法对 StackOverflow 进行测试,因为这种情况下没有更多的堆栈可供分配,应用程序会在这种情况下自动退出。

    【讨论】:

    • 没错。那么如果这样的测试失败,任何测试运行者都会完全崩溃?还是?
    • 拉维亚,你没抓住重点。确实,您无法测试堆栈溢出,但这不是您应该测试的。您可以跟踪您的方法嵌套的深度,以保护自己免受永恒循环的影响。
    【解决方案6】:

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

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

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

    • 并行化您的测试运行。如果您有一个单独的堆栈溢出案例测试套件,那么如果一个测试运行器出现故障,您仍然可以从其他测试中获得结果。 (即使您不分开测试运行,我也会认为堆栈溢出非常糟糕,以至于如果发生这种情况,值得让整个测试运行程序崩溃。您的代码一开始就不应该中断!)

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

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

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

    • 一旦您测量了有多少次迭代N 导致您的代码堆栈溢出,不要只测试这个数字!我会测试更多(50 N?)的迭代不会堆栈溢出(因此您不会得到在一个构建服务器上通过但在另一个构建服务器上失败的测试)。

    • 考虑一下函数最终可以在其中调用自身的所有可能的代码路径。您的产品可能会阻止 X() -&gt; X() -&gt; X() -&gt; ... 递归堆栈溢出,但如果 X() -&gt; A() -&gt; B() -&gt; C() -&gt; ... -&gt; W() -&gt; X() -&gt; A() -&gt; ... 递归情况仍然被破坏怎么办?

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

    【讨论】:

      【解决方案7】:

      首先,我认为该方法应该处理这个问题并确保它不会递归太深。

      当这个检查完成后,我认为应该通过暴露达到的最大深度来测试检查——如果有的话——并断言它永远不会大于检查允许的范围。

      【讨论】:

      • 当然应该处理。但是,本着不插入未经测试的代码的精神,你怎么能确定它实际上在没有测试的情况下正确处理了它?
      • 这就是为什么我认为你应该测试“处理它”。当然,您无法测试处理何时失败,但您可以测试当您向其提供数据时它实际上不会溢出堆栈,否则(没有处理)会。但是当我重新阅读您的问题时,我可以看到,这不是您要问的。对不起:)
      猜你喜欢
      • 2011-06-11
      • 2017-03-17
      • 1970-01-01
      • 1970-01-01
      • 2011-05-07
      • 2015-03-25
      • 1970-01-01
      • 2012-12-20
      • 2021-08-27
      相关资源
      最近更新 更多