Depuración básica
| Fuente | Recurso | Notas |
|---|---|---|
| AryanshS | Debugging Correctly | Algunas partes se tomaron de aquí. |
| LCPP | 3.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 = 5000Esos 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 5000Al 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
| Fuente | Recurso | Notas |
|---|---|---|
| GCC | Standard 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
| Fuente | Recurso | Notas |
|---|---|---|
| LCPP | Asserts | Incluye |
| GCC | 5 - Diagnostics | Habla de |
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 134Si 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.
| Fuente | Recurso | Notas |
|---|---|---|
| Errichto | Video - How to test your solution | Usar un script para stress testing. |
| Errichto | Asking 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
doneSe 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 ;
doneAquí 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"
fiDepuradores
| Fuente | Recurso | Notas |
|---|---|---|
| LCPP | 3.6 - Using an integrated debugger | |
| Microsoft | Debug C++ in Visual Studio Code | |
| Jetbrains | Debugging C++ in CLion |
| Fuente | Recurso | Notas |
|---|---|---|
| Microsoft | Debugging Java in Visual Studio Code | |
| Jetbrains | Debugging Java in IntelliJ |
| Fuente | Recurso | Notas |
|---|---|---|
| Microsoft | Debugging Python in Visual Studio Code | |
| Jetbrains | Debugging 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
ien 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.