01 · Punto de partida
Depurar simulaciones no es depurar un CRUD
En un programa común, un bug suele mostrarse siempre igual. En una simulación, el bug se esconde detrás del tiempo y del azar: aparece en el paso 47, solo con cierta semilla, solo cuando la cola se vacía un martes simulado. Adivinar y tocar código al azar solo agrega bugs nuevos.
- ¿Cómo reproducir el fallo a voluntad?
- ¿Cómo saber el primer instante donde miente?
- ¿Cómo reducir 500 líneas a 10 culpables?
- ¿Cómo saber que el arreglo no rompió otra cosa?
02 · Definición
Qué hace especial al bug de simulación
Temporal
El error nace en t* pero se ve 20 pasos después. Sin traza por paso, mirás el cadáver y no la escena del crimen.
Estocástico
Con azar sin semilla fija, cada corrida es distinta: el bug es un fantasma. Con semilla fija, es un insecto disecado.
Acumulativo
Cada estado depende del anterior: un error de 0,1 en t=2 es un error de 5 en t=50. Hay que buscar el primer desvío, no el más grande.
| Programa común | Simulación | Antídoto |
|---|---|---|
| Falla siempre igual | Falla solo con cierta semilla | Fijar e imprimir la semilla |
| Stack trace al crash | Números «raros» sin crash | Invariantes que gritan en t* |
| Un input, un output | Historia de 100 pasos | Bisección temporal + traza mínima |
03 · Método
El protocolo en 5 pasos
- 1Reproducir con semilla y caso mínimo.
Fijá semilla, recortá pasos y datos hasta el ejemplo más chico que aún falla. Si no reproduce a voluntad, no depurés: estabilizá primero.
- 2Leer la traza, no el gráfico.
Imprimí (t, antes → después, evento). El gráfico muestra el síntoma; la traza muestra el primer paso imposible.
- 3Bisecar el tiempo.
Si falla en 12 pasos pero no en 6, el t* está en 7…12. Partí el intervalo hasta dar con el primer desvío exacto.
- 4Hipótesis de una sola causa.
Formulá «creo que en t* la salida se aplica dos veces» y diseñá un experimento que la refute. Una hipótesis por vez.
- 5Fijar y blindar con regresión.
Arreglá, agregá el caso mínimo a la suite del Tema 3 y corré todo. Un bug cazado sin test nuevo volverá.
04 · Herramientas
Trazas, prints estructurados y visores
Lo que sí ayuda
Traza mínima y comparable
Primeros N pasos con formato fijo, esperada vs. real lado a lado y marca del primer desvío. 10 líneas valen más que 3 gráficos.
Lo que estorba
Prints caóticos y gráficos solos
Miles de líneas sin tiempo, o solo la curva final. El ojo perdona un desfase de un paso; la comparación automática, no.
- Heisenbug
- Bug que desaparece al observarlo (prints que cambian el timing, azar distinto). Se fija con semilla y traza a archivo, no con prints invasivos.
- Delta debugging
- Recortar input, pasos y código hasta el mínimo que falla. Si con 3 pasos ya falla, no mires 100.
- Punto de observación
- Invariante impreso en cada paso (0 ≤ nivel ≤ tope). El primer «ROTO» es el t*.
05 · Representación en Python
Cazar con traza comparada y bisección
El patrón profesional: una función que genera traza, una que compara contra la esperada y una bisección que encuentra t* automáticamente.
Python en tu navegador. Hay un bug inyectado a propósito. Ejecutá, leé el t* y corregilo.
def sim_bug(nivel, entrada, salida, tope, pasos):
h = []
for t in range(pasos):
nivel = nivel + entrada - salida # BUG: falta max(0, min(tope, ...))
h.append(nivel)
return h
def esperada(nivel, entrada, salida, tope, pasos):
h = []
for t in range(pasos):
nivel = max(0, min(tope, nivel + entrada - salida))
h.append(nivel)
return h
esp = esperada(2, 1, 3, 14, 6)
mal = sim_bug(2, 1, 3, 14, 6)
for t, (e, m) in enumerate(zip(esp, mal), start=1):
marca = " <-- DIVERGE" if e != m else ""
print(f"t={t}: esperada={e} programa={m}{marca}")
La primera línea con DIVERGE es el t*: ahí nace el bug, lo demás es arrastre.
Bisección automática del primer desvío
# Reusa esp y mal del bloque anterior
def primer_desvio(esp, mal):
lo, hi, ans = 0, len(esp) - 1, None
while lo <= hi:
mid = (lo + hi) // 2
if esp[:mid+1] == mal[:mid+1]:
lo = mid + 1
else:
ans = mid + 1 # en base 1
hi = mid - 1
return ans
print("t* =", primer_desvio(esp, mal))
print("Arreglo: envolver con max(0, min(tope, ...)) y agregar test")
06 · Exploración
Laboratorio: la caza del t*
Elegí el bug inyectado (0 = ninguno, 1 = desfase, 2 = signo invertido, 3 = piso olvidado) y la entrada. El gráfico compara la esperada (lima) con el programa (naranja), marca el primer desvío con un círculo rojo y te da la pista. Tu trabajo: predecir el t* antes de mirar el veredicto.
Reproducir · aislar · fijar
semilla fija 42 · 12 pasos · tope 14
Con bug de desfase, el primer desvío ya está en t=1: todo lo demás es arrastre.
Salida fija en 2, inicio en 10 y tope en 14. La bisección se hace sola: achicá «ver hasta el paso» para partir el intervalo como harías depurando (¿falla en 6? ¿en 3?).
Preguntas para explorar
- Con bug 1 (desfase) y entrada 3: ¿cuál es el t*? Achicá el zoom a 6, luego a 3. ¿En cuántas particiones lo aislás?
- Cambiá a bug 3 (piso olvidado) con entrada 1. ¿El t* aparece al inicio o cuando cruza cero? ¿Por qué la traza inicial «parece» correcta?
- Con bug 2 (signo invertido), ¿la divergencia crece o se mantiene? ¿Qué invariante lo delataría en un solo paso?
Ver respuestas sugeridas
- t* = 1: el desfase corre todo desde el primer valor. Con bisección lo aislás en ~4 particiones (12→6→3→1). Por eso se busca el primero, no el mayor.
- Aparece cuando cruza cero (t ≈ 5 con entrada 1 y salida 2 desde 10). Antes coincide: el bug dormido solo despierta en el borde. Moraleja: los tests deben incluir bordes.
- Crece linealmente (resta cuando debía sumar o viceversa). Un test de un solo paso con balance conocido lo caza al instante: no hacen falta 12 pasos.
07 · Comprensión
Confusiones frecuentes
«Cambio cosas hasta que el gráfico se ve bien»
Eso es calibrar un bug, no depurar. El gráfico puede quedar lindo por compensación (Tema 1, caso C). Solo la traza comparada da el alta.
«Con más prints se caza más rápido»
Más prints caóticos enlentecen. Un formato fijo (t, antes → después) y pocos pasos mínimos cazan más rápido que mil líneas.
«Si falla solo a veces, es el azar, no un bug»
El azar con semilla fija es determinista. «A veces» casi siempre es estado compartido entre réplicas o semilla no registrada.
«Arreglado, borro la traza»
El arreglo sin test nuevo es una promesa. Cada t* debe quedar como test mínimo en la suite del Tema 3.
08 · Práctica guiada
Ejercicios con Python
Ejercicio 1: mínimo reproducible
Reducí este fallo de 12 pasos al mínimo que aún diverge.
def esperada(n, e, s, tope, pasos):
h = []
for _ in range(pasos):
n = max(0, min(tope, n + e - s))
h.append(n)
return h
def rota(n, e, s, tope, pasos):
h = []
for _ in range(pasos):
n = n + e - s # sin piso/techo
h.append(n)
return h
for pasos in [12, 6, 3, 2, 1]:
e = esperada(2, 1, 3, 14, pasos)
r = rota(2, 1, 3, 14, pasos)
print(pasos, e == r, r)
Ver solución razonada
Con 1 paso ambas dan [0] (2+1−3 = 0, el piso no actúa). Con 2 pasos divergen: [0, 0] vs [0, -2]. El mínimo reproducible son 2 pasos: todo lo demás es ruido.
Ejercicio 2: leer la traza como un detective
¿En qué paso nace el bug y qué hipótesis sugiere?
traza = [(1, 10, 11), (2, 11, 12), (3, 12, 12), (4, 12, 12)]
# (t, esperada, programa)
for t, e, m in traza:
print(t, e, m, "<-- t*" if e != m else "")
Ver solución
t* = 3: hasta t=2 coinciden. Hipótesis: el techo (tope 12) no se aplica desde t=3, o la entrada dejó de sumarse. Una sola causa, un experimento para refutarla.
Ejercicio 3: blindar con regresión
Convertí el bug del piso en un test con nombre que quede para siempre.
Ver una posible respuesta
def sim(nivel, e, s, tope, pasos):
h = []
for _ in range(pasos):
nivel = max(0, min(tope, nivel + e - s))
h.append(nivel)
return h
def test_piso_no_negativo():
assert sim(2, 1, 3, 14, 3) == [0, 0, 0]
test_piso_no_negativo()
print("Regresión OK: el bug del piso no volverá en silencio")
El test nombra al bug y fija el mínimo reproducible. Si alguien quita el max(0,…), la suite grita en milisegundos.
09 · Síntesis
Ideas para recordar
- Depurar simulaciones = reproducir con semilla + traza comparada + bisección hasta t*.
- Se busca el primer desvío, no el más grande: lo demás es arrastre acumulativo.
- Menos es más: caso mínimo, pocos pasos, formato fijo de traza.
- Cada bug cazado se convierte en test de regresión con nombre.
- Solo con la suite en verde tiene sentido validar contra la realidad (Tema 5).
En el próximo tema cruzaremos el puente hacia el mundo real: la validación frente al sistema real.