21

Say you have a method that could potentially get stuck in an endless method-call loop and crash with a StackOverflowException. For example my naive RecursiveSelect method mentioned in this question.

Starting with the .NET Framework version 2.0, a StackOverflowException object cannot be caught by a try-catch block and the corresponding process is terminated by default. Consequently, users are advised to write their code to detect and prevent a stack overflow. For example, if your application depends on recursion, use a counter or a state condition to terminate the recursive loop.

Taking that information (from this answer) into account, since the exception can't be caught, is it even possible to write a test for something like this? Or would a test for this, if that failed, actually break the whole test-suite?

Note: I know I could just try it out and see what happens, but I am more interested in general information about it. Like, would different test frameworks and test runners handle this differently? Should I avoid a test like this even though it might be possible?

Community
  • 1
  • 1
Svish
  • 152,914
  • 173
  • 462
  • 620
  • You seem to be after some kind of static analysis that would tell you if a method will terminate everytime. – Tom Duckering Jan 06 '10 at 11:46
  • Maybe? What I was thinking of was, in the case of the mentioned method, sending in an object graph that did have cycles and make sure it doesn't throw a StackOverflowException. But since that exception according to that message from microsoft that exception can't be caught. So how would you write a test for this case? – Svish Jan 06 '10 at 11:49
  • 1
    the StackOverFlow exception does tend to kill NUnit. Other runners might survive to some degree. Some of them run the tests in a new process and might be able to report results up to the StackOverflow. Since NUnit only separates using an AppDomain I'm almost certain that it takes the whole test runner down. – Mike Two Jan 06 '10 at 11:59
  • Tried it out myself now using TestDriven.Net as the test runner. And apparently it just seems to hang...? – Svish Jan 06 '10 at 13:39

7 Answers7

6

You would need to solve the Halting Problem! That would get you rich and famous :)

Mongus Pong
  • 11,337
  • 9
  • 44
  • 72
  • Awesome... will start working on that then! Right... :p Silliness aside, I guess the answer for this question would be something along the lines of "No can do" then. Oh well :) – Svish Jan 06 '10 at 12:03
  • @ Svish - you can do it, it's just not terribly natural. It requires running the code under test in another process and dealing with the pain that is checking the result of that process. – Mike Two Jan 06 '10 at 12:05
  • I refer you to the work of Dr Byron Cook who has sucessfully developed techniques to prove that certain kinds of code will terminate. http://www.bcs.org/server.php?show=nav.11472 – Tom Duckering Jan 06 '10 at 12:06
  • 9
    The halting problem doesn't really have anything to do with this question. A method that is proven to exit can easily cause a stack overflow, and the fact that a method never exits doesn't cause a stack overflow by itself. – Guffa Jan 06 '10 at 12:13
  • @Guffa: Good point, but it does have something to do with it, doesn't it? Certainly made sense to me anyways... hehe. – Svish Jan 06 '10 at 12:16
  • 1
    @Guffa, true. The halting problem is more a logical problem which applies to machines with infinite resources, whereas stack overflow occurs on machines with limited resources. Still it highlights the difficulties that you would get trying to predict a stack overflow. – Mongus Pong Jan 06 '10 at 12:22
3

How about checking the number of frames on the stack in an assert statement?

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

In your example from the related question this would be (The costly assert statement would be removed in the release build):

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

It's not a general test though, as you need to expect it to happen, plus you can only check the frame count and not the actual size of the stack. It is also not possible to check whether another call will exceed stack space, all that you can do is roughly estimate how many calls in total will fit on your stack.

Dirk Vollmar
  • 172,527
  • 53
  • 255
  • 316
  • This should happen inside your recursive function. It's not a general test though, as you need to expect it to happen, plus you can only check the frame count and not the actual size of the stack. – Dirk Vollmar Jan 06 '10 at 11:51
  • Exactly. Interesting solution, but not I don't think I would like to have something like that in my methods, hehe. – Svish Jan 06 '10 at 11:56
  • Probably more of a pragmatic than a beautiful approach, but don't forget that the costly `Debug.Assert` statement is removed in the release build. – Dirk Vollmar Jan 06 '10 at 12:02
3

It's evil but you can spin it up in a new process. Launch the process from the unit test and wait for it to complete and check the result.

Mike Two
  • 44,935
  • 9
  • 80
  • 96
  • Ah yes. Memories of using batch files to isolate unpredictable or long-running routines... – Mike Dec 12 '14 at 07:02
2

The idea is to keep track of how deeply a recursive funcion is nested, so that it doesn't use too much stack space. Example:

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

This of course can't totally protect you from stack overflows, as the method can be called with too little stack space left to start with, but it makes sure that the method doesn't singelhandedly cause a stack overflow.

Guffa
  • 687,336
  • 108
  • 737
  • 1,005
1

We cannot have a test for StackOverflow because this is the situation when there is no more stack left for allocation, the application would exit automatically in this situation.

Ravi Vanapalli
  • 9,805
  • 3
  • 33
  • 43
  • Exactly. So any test-runner would pretty much crash completely if such a test failed? Or? – Svish Jan 06 '10 at 11:50
  • Ravia, you are missing the point. It's true that you can't test for stack overflow, but that is not what you should test for. You keep track of how deeply your method is nested, to protect yourself from an eternal loop. – Guffa Jan 06 '10 at 12:06
1

If you are writing library code that somebody else is going to use, stack overflows tends to be a lot worse than other bugs because the other code can't just swallow the StackOverflowException; their entire process is going down.

There's no easy way to write a test that expects and catches a StackOverflowException, but that's not the behavior you want to be testing!

Here's some tips for testing your code doesn't stack overflow:

  • Parallelize your test runs. If you have a separate test suite for the stack overflow cases, then you'll still get results from the other tests if one test runner goes down. (Even if you don't separate your test runs, I'd consider stack overflows to be so bad that it's worth crashing the whole test runner if it happens. Your code shouldn't break in the first place!)

  • Threads may have different amounts of stack space, and if somebody is calling your code you can't control how much stack space is available. While the default for 32 bit CLR is 1MB stack size and 64 bit is 2MB stack size, be aware that web servers will default to a much smaller stack. Your test code could use one of the Thread constructors that takes a smaller stack size if you want to verify your code won't stack overflow with less avaliable space.

  • Test every different build flavor that you ship (Release and Debug? with or without debugging symbols? x86 and x64 and AnyCPU?) and platforms you'll support (64 bit? 32 bit? 64 bit OS running 32 bit? .NET 4.5? 3.5? mono?). The actual size of the stack frame in the generated native code could be different, so what breaks on one machine might not break on another.

  • Since your tests might pass on one build machine but fail on another, ensure that if it starts failing it doesn't block checkins for your entire project!

  • Once you measure how few iterations N cause your code to stack overflow, don't just test that number! I'd test a much larger number (50 N?) of iterations doesn't stack overflow (so you don't get tests that pass on one build server but fail on another).

  • Think about every possible code path where a function can eventually later call itself. Your product might prevent the X() -> X() -> X() -> ... recursive stack overflow, but what if there is a X() -> A() -> B() -> C() -> ... -> W() -> X() -> A() -> ... recursive case that is still broken?

PS: I don't have more details about this strategy, but apparently if your code hosts the CLR then you can specify that stack overflow only crashes the AppDomain?

Carl Walsh
  • 6,100
  • 2
  • 46
  • 50
0

First and foremost I think the method should handle this and make sure it does not recurse too deep.

When this check is done, I think the check should be tested - if anything - by exposing the maximum depth reached and assert it is never larger than the check allows.

asgerhallas
  • 16,890
  • 6
  • 50
  • 68
  • Of course it should handle it. But in the spirit of not thrusting untested code, how would you know for sure that it in fact was handling it properly without a test for it? – Svish Jan 06 '10 at 12:05
  • That's why I think you should test the "handling of it". Of course you cannot test when the handling fails, but you can test that it does not actually overflow the stack when you feed it with data, that otherwise (without the handling) would. But when I re-read your question I can see, that's is not what you're asking. Sorry :) – asgerhallas Jan 07 '10 at 10:09