Skip to Content

Depuración básica

Recursos
FuenteRecursoNotas
AryanshSDebugging Correctly

Algunas partes se tomaron de aquí.

LCPP3.4 - A Strategy for Debugging

Cómo agregar prints.

Imprimir variables

Prints básicos

La forma más básica de depurar es agregar un print. Sirve y, en la mayoría de los casos, cumple su propósito. Por ejemplo, se puede escribir lo de abajo para ver el valor de x en un punto del código.

#include <iostream> using namespace std; int x = 10; // pretend this variable is important void dbg() { cout << "x = " << x << endl; } int main() { dbg(); // x = 10 x = 5000; dbg(); // x = 5000 }
public class Main { static int x = 10; // pretend this variable is important public static void main(String[] args) { dbg(); // x = 10 x = 5000; dbg(); // x = 5000 } static void dbg() { System.out.print("x = "); System.out.println(x); } }
x = 10 # pretend this variable is important def dbg(): print("x =", x) dbg() # x = 10 x = 5000 dbg() # x = 5000

Esos prints sirven a un nivel básico, y se pueden comentar o definir fuera del código principal cuando haga falta compilar y ejecutar una versión más definitiva.

Flujo de error estándar

Sin embargo, por más útiles que sean los prints, es molesto trabajar con ellos y separarlos de forma eficiente de las partes reales del código. Esto importa, por ejemplo, cuando un juez en línea (OJ) tiene que leer la salida.

El flujo de error estándar es un arreglo rápido. En lugar de imprimir en el flujo de salida estándar, se puede imprimir en un flujo nuevo llamado flujo de error estándar.

#include <iostream> using namespace std; int x = 10; void dbg() { cerr << "x = " << x << endl; } int main() { dbg(); x = 5000; dbg(); }
public class Main { static int x = 10; public static void main(String[] args) { dbg(); x = 5000; dbg(); } static void dbg() { System.err.print("x = "); System.err.println(x); } }
import sys x = 10 def dbg(): print("x =", x, file=sys.stderr) dbg() # outputs 10 x = 5000 dbg() # now outputs 5000

Al ejecutar este programa puede no quedar clara la diferencia. El contenido del flujo de error aparece junto al de la salida estándar. ¡Pero esa es la gracia! Y lo mejor es que, si se envía este programa a un OJ, no va a notar la salida del flujo de error.

Macro de depuración

Recursos
FuenteRecursoNotas
GCCStandard Predefined Macros

Aquí hay una macro práctica que imprime el número de línea (__LINE__) y el nombre de la variable (#v) además del valor de la variable (v).

#include <iostream> using namespace std; #define dbg(v) cout << "Line(" << __LINE__ << ") -> " << #v << " = " << (v) << endl; int x = 10; // pretend this variable is important int main() { dbg(x); // Line(10) -> x = 10 x = 5000; dbg(x); // Line(12) -> x = 5000 }

Depurar estructuras de datos de la STL

Como C++ no tiene funciones de impresión incorporadas para muchas de sus estructuras de datos, conviene tener algo de código preescrito para imprimirlas. Esta  plantilla es bastante fácil de usar. Incluye soporte para básicamente todas las estructuras de datos que se necesitan en programación competitiva. Así se usaría:

#include <iostream> #include <vector> #include "debugging.h" using namespace std; int main() { vector<int> arr{1, 2, 3, 4}; cout << arr << endl; // just feed it into cout like any other variable }

Aserciones y warnings

Recursos
FuenteRecursoNotas
LCPPAsserts

Incluye static_assert y #define NDEBUG.

GCC5 - Diagnostics

Habla de #warning y #error.

Usar assert(cond) para comprobar que se cumple una condición cond. Si no se cumple, el programa termina. Por ejemplo, el siguiente fragmento

#include <cassert> #include <iostream> using namespace std; int main() { assert(false); cout << "OK" << endl; }

produce:

prog: main.cpp:6: int main(): Assertion `false' failed. /tmp/program/run.sh: line 1: 408 Aborted ./prog Command exited with non-zero status 134

Si se quieren desactivar todas las aserciones (por ejemplo, antes de enviar) se puede agregar #define NDEBUG al inicio del programa.

#define NDEBUG #include <cassert> #include <iostream> using namespace std; int main() { assert(false); cout << "OK" << endl; // OK }

Stress testing

Si el código da WA, una opción es correr la solución con bugs contra otra de la que se esté relativamente seguro de que es correcta, sobre casos de prueba generados al azar, hasta encontrar una diferencia. Normalmente alcanzan casos chicos. Véase el video para los detalles.

Recursos
FuenteRecursoNotas
ErrichtoVideo - How to test your solution

Usar un script para stress testing.

ErrichtoAsking for help FAQ

Contiene algunas partes de los videos de arriba.

Opción 1: un solo programa

Escribir un solo programa que incluya las dos soluciones y un generador de entrada. Si se usa C++, conviene envolver cada solución en un namespace separado para evitar errores de compilación, así:

#include <bits/stdc++.h> using namespace std; namespace Solution1 { int solve(int x) { return x < 10; } } // namespace Solution1 namespace Solution2 { int solve(int x) { return x < 11; } } // namespace Solution2 int main() { mt19937 rng(0); while (true) { int x = rng() % 100; int out1 = Solution1::solve(x); int out2 = Solution2::solve(x); if (out1 != out2) { cout << x << " " << out1 << " " << out2 << endl; exit(0); } } }

Por supuesto, esta opción solo se puede usar si ambas soluciones están escritas en el mismo lenguaje.

Opción 2: tres programas

Otra opción es escribir tres programas distintos: uno para el programa correcto, uno para el incorrecto y uno para generar entradas. Luego se puede usar el script de abajo para encontrar una entrada en la que los programas correcto e incorrecto difieran. Si se quiere aprender a escribir estos scripts, véase este módulo.

Scripts

Este es el script que se mencionó en el video:

# A and B are executables you want to compare, gen takes int # as command line arg. Usage: 'sh stress.sh' for ((i = 1; ; ++i)); do # if they are same then will loop forever echo $i ./gen $i > int ./A < int > out1 ./B < int > out2 diff -w out1 out2 || break # diff -w <(./A < int) <(./B < int) || break done

Se puede modificar para otras situaciones. Por ejemplo, si se tienen archivos de entrada y salida (p. ej. 1.in, 1.out, 2.in, 2.out, …, 10.out para problemas viejos de USACO) se puede usar lo siguiente:

# A is the executable you want to test for ((i = 1; i <= 10; ++i)); do echo $i ./A < $i.in > out diff -w out $i.out || break done echo "ALL TESTS PASSED"

Lo siguiente se detiene en el primer archivo de entrada tal que el archivo de salida producido esté vacío.

for((i = 1; ; ++i)); do echo $i ./gen $i > int ./A < int > out if ! [[ -s "out" ]] ; then echo "no output" break fi ; done

Aquí hay un script para probar un programa Java con archivos de entrada y salida. Hay que poner el .java, este script y los archivos de entrada y salida (1.in, 1.out, etc.) en el mismo directorio:

Script de testing en Java
#!/bin/bash RED="\033[0;31m" GREEN="\033[0;32m" NC="\033[0m" wrong=0 fileName=$(ls | grep java) IFS="." read -ra fileArr <<< $fileName className=${fileArr[0]} loop=true testCase=1 while $loop; do # Does this test case exist? if [[ ! -e "$testCase.in" || ! -e "$testCase.out" ]]; then # If not, then stop. break; fi # If so, then test. echo "Testing: $testCase" cp $testCase.in $className.in java $className.java < $className.in > $className.out diffOut=$(diff -w $testCase.out $className.out 2>&1) if [[ $diffOut == "" ]]; then echo -e "$GREEN** PASSED **$NC\n" else echo -e "$RED** FAILED **$NC" echo -e "$diffOut\n" wrong=$((wrong += 1)) fi testCase=$((testCase += 1)) done # Check if any wrong. if [[ wrong -gt 0 ]]; then if [[ $wrong == 1 ]]; then echo -e "$RED*** $wrong/$testCase TEST CASE FAILED ***$NC" else echo -e "$RED*** $wrong/$((testCase - 1)) TEST CASES FAILED ***$NC" fi else echo -e "$GREEN*** ALL TEST CASES PASSED ***$NC" fi

Depuradores

Recursos
FuenteRecursoNotas
MicrosoftDebugging Java in Visual Studio Code
JetbrainsDebugging Java in IntelliJ
Recursos
FuenteRecursoNotas
MicrosoftDebugging Python in Visual Studio Code
JetbrainsDebugging Python in PyCharm

En esencia, un depurador es una herramienta para “trazar el código” por uno. Usar un depurador varía de lenguaje a lenguaje e incluso de IDE a IDE, así que solo cubriremos lo básico.

Ventajas de usar un depurador:

  • No hace falta escribir prints, así que se ahorra tiempo.
  • Se puede avanzar por el código en tiempo real.
  • C++: imprimir la línea donde se disparó un error de ejecución.

Desventajas de usar un depurador:

  • No es muy distinto de imprimir los valores en varios puntos del programa.
  • La mayoría de quienes hacen programación competitiva no usan depuradores.
  • No se puede ver la “salida” global del programa en cada etapa. Por ejemplo, si se quisieran ver todos los valores de i en el programa, no se podría con un depurador.

Breakpoints

Un depurador permite pausar el código en su ejecución y ver los valores en un punto dado. Para eso, se pone un “breakpoint” en cierta línea. Cuando el código llega a ese breakpoint, se pausa y se pueden inspeccionar todas las variables en ese instante.

Hay dos operaciones más útiles y comunes. Una vez en el breakpoint, se puede querer ver qué pasa después de ejecutar la línea actual. Eso es el botón “Step Over”, que permite pasar a la siguiente línea. Si se está en una línea con el siguiente código: dfs(0, -1), al hacer clic en “step over” el depurador no va a mostrar qué pasa dentro de esa función y va a ir a la siguiente línea. Si se hace clic en “step in”, en cambio, se entra a la función y se puede avanzar por ella.