c# unit-testing nunit stack-overflow

C#: Cómo probar para StackOverflowException



unit-testing nunit (7)

¡Necesitarías resolver el problema de detención ! Eso te haría rico y famoso :)

Supongamos que tiene un método que podría atascarse en un interminable bucle de llamada de método y bloquearse con una excepción StackOverflowException. Por ejemplo, mi método ingenuo de selección RecursiveSelect mencionado en esta pregunta .

A partir de .NET Framework versión 2.0, un bloque try-catch no puede capturar un objeto StackOverflowException y el proceso correspondiente finaliza de forma predeterminada. En consecuencia, se recomienda a los usuarios que escriban su código para detectar y evitar un desbordamiento de pila. Por ejemplo, si su aplicación depende de la recursión, use un contador o una condición de estado para terminar el ciclo recursivo.

Teniendo en cuenta esa información (de esta respuesta ), ya que no se puede detectar la excepción, ¿es posible escribir una prueba para algo como esto? ¿O una prueba para esto, si eso fallara, realmente rompería todo el conjunto de pruebas?

Nota: Sé que podría probarlo y ver qué sucede, pero estoy más interesado en la información general al respecto. Al igual que los diferentes marcos de prueba y corredores de prueba, ¿manejarían esto de manera diferente? ¿Debo evitar una prueba como esta aunque sea posible?


¿Qué hay de verificar la cantidad de cuadros en la pila en una declaración de afirmación?

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

En su ejemplo de la pregunta relacionada, esto sería (la declaración de afirmación costosa se eliminaría en la versión de lanzamiento):

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

Sin embargo, no es una prueba general, ya que debe esperar que suceda, además, solo puede verificar el recuento de cuadros y no el tamaño real de la pila. Tampoco es posible verificar si otra llamada excederá el espacio de pila, todo lo que puede hacer es aproximadamente estimar cuántas llamadas cabrán en total en su pila.


En primer lugar, creo que el método debería manejar esto y asegurarse de que no retroceda demasiado.

Cuando se realiza esta comprobación, creo que la comprobación debe probarse, en todo caso, al exponer la profundidad máxima alcanzada y afirmar que nunca es más grande de lo que permite la verificación.


Es malvado, pero puedes hacerlo girar en un nuevo proceso. Inicie el proceso desde la prueba de la unidad y espere a que se complete y verifique el resultado.


La idea es hacer un seguimiento de cuán profundamente está anidada una función recursiva, para que no utilice demasiado espacio de pila. Ejemplo:

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

Por supuesto, esto no puede protegerlo totalmente de los desbordamientos de pila, ya que el método puede invocarse con muy poco espacio de pila para comenzar, pero se asegura de que el método no cause un desbordamiento de pila.


No podemos tener una prueba para porque esta es la situación cuando no queda más pila para la asignación, la aplicación saldría automáticamente en esta situación.


Si está escribiendo un código de biblioteca que alguien más va a usar, los desbordamientos de pila tienden a ser mucho peores que otros errores porque el otro código no puede simplemente tragar la Exception ; todo su proceso está bajando.

No hay una manera fácil de escribir una prueba que espere y atrape una excepción Exception , ¡pero ese no es el comportamiento que desea probar!

Aquí hay algunos consejos para probar que su código no se acumula de desbordamiento:

  • Paraleliza tus pruebas de prueba. Si tiene un conjunto de pruebas separado para los casos de desbordamiento de pila, todavía obtendrá resultados de las otras pruebas si un corredor de pruebas falla. (Incluso si no separa las ejecuciones de prueba, consideraría que los desbordamientos de pila son tan malos que vale la pena bloquear a todo el corredor de la prueba si ocurre. ¡Su código no debería interrumpirse en primer lugar!)

  • Los hilos pueden tener diferentes cantidades de espacio de pila, y si alguien llama a su código, no puede controlar la cantidad de espacio de pila disponible. Si bien el valor predeterminado para CLR de 32 bits es el tamaño de pila de 1 MB y 64 bits para el tamaño de pila de 2 MB, tenga en cuenta que los servidores web se establecerán de manera predeterminada en una pila mucho más pequeña . Su código de prueba podría usar uno de los constructores de Thread que toma un tamaño de pila más pequeño si desea verificar que su código no apile el desbordamiento con menos espacio disponible.

  • Pruebe cada sabor de compilación diferente que envíe (¿Liberar y depurar? Con o sin símbolos de depuración? X86 y x64 y AnyCPU?) Y las plataformas que admitirá (¿64 bit? 32 bit? 64 bit OS ejecutando 32 bit? .NET 4.5? 3.5? Mono?). El tamaño real del marco de pila en el código nativo generado podría ser diferente, por lo que lo que se rompe en una máquina podría no romperse en otra.

  • Ya que sus pruebas pueden pasar en una máquina de compilación pero fallar en otra, ¡asegúrese de que si comienza a fallar, no bloquee los registros en todo su proyecto!

  • Una vez que mida cuántas iteraciones N causa que el código se apile, ¡no solo pruebe ese número! Probaría un número mucho mayor (¿ 50 N ?) De iteraciones no apiladas (así que no obtienes pruebas que pasan en un servidor de compilación pero fallan en otro).

  • Piense en todas las rutas de código posibles donde una función puede eventualmente llamarse a sí misma. Su producto podría evitar el X() -> X() -> X() -> ... desbordamiento de pila recursiva, pero ¿qué pasa si hay un X() -> A() -> B() -> C() -> ... -> W() -> X() -> A() -> ... caso recursivo que todavía está roto?

PD: No tengo más detalles sobre esta estrategia, pero aparentemente, si su código aloja el CLR , ¿puede especificar que el desbordamiento de la pila solo bloquea el dominio de aplicación?