01 · Punto de partida
Del «corre» al «obedece»
Que el programa ejecute sin excepciones no prueba nada: un índice corrido, una cola atendida en orden inverso o una semilla reusada también «corren». La verificación de la implementación pregunta algo más duro:
- ¿Cada línea ejecuta la regla auditada en el Tema 2?
- ¿Los eventos ocurren en el orden y el instante correctos?
- ¿El azar es reproducible y no contamina entre réplicas?
- ¿Qué evidencia guardás de que todo eso se cumple?
02 · Definición
Qué se verifica y con qué oracle
Cada verificación necesita un oráculo: una fuente independiente de la respuesta correcta.
Casos exactos
Entradas triviales con salida calculable a mano: balance cero, un paso, vaciado total. El oráculo es la aritmética.
Invariantes
Propiedades que siempre valen: 0 ≤ nivel ≤ tope, conservación de clientes, tiempo no retrocede. El oráculo es la lógica.
Determinismo
Misma spec + misma semilla = misma historia, bit a bit. El oráculo es la repetición.
| Chequeo | Oráculo | Falla típica que caza |
|---|---|---|
| Un paso a mano | Cálculo manual | Signo invertido, orden de operaciones |
| Piso y techo | Invariante 0 ≤ s ≤ tope | Falta de max/min, desborde silencioso |
| Misma semilla dos veces | Igualdad de historias | Reloj o estado global contamina el azar |
| Longitud y tiempo | Conteo de pasos | Off-by-one: 11 o 13 pasos en vez de 12 |
03 · Técnicas
La caja de herramientas del verificador
- 1Pruebas unitarias con asserts.
Una función, una regla, un valor esperado exacto. Se ejecutan siempre, en cada cambio. Son la red de seguridad.
- 2Trazas de eventos.
Registrar (t, evento, estado antes → después) en las primeras corridas. Leer 10 líneas de traza revela órdenes imposibles.
- 3Chequeo de invariantes en caliente.
Afirmar en cada paso que 0 ≤ nivel ≤ tope y que el tiempo avanza. Si algo lo viola, el programa se detiene con mensaje, no sigue mintiendo.
- 4Semilla fija y aislamiento del azar.
Un generador por réplica, creado con su semilla. Nada de
randomglobal compartido ni de usar la hora como semilla en tests. - 5Regresión y revisión de código.
Guardar una historia «dorada» y compararla en cada cambio. Otra persona lee el diff preguntando «¿qué regla del papel ejecuta esta línea?».
04 · Defectos típicos
Los sospechosos habituales
Tiempo y orden
Off-by-one y eventos cruzados
Devolver 11 historias en vez de 12, atender la llegada antes que la salida del mismo instante, actualizar el reloj después de usarlo. Se cazan contando pasos y leyendo trazas.
Estado y azar
Piso olvidado y semilla reusada
Permitir niveles negativos, compartir el generador entre réplicas, no reiniciar el estado entre corridas. Se cazan con invariantes y test de determinismo.
- Unidades en el código
- La spec dice litros/paso pero el código suma litros/minuto. Mismo defecto del Tema 2, ahora en la traducción a código.
- Flotantes y comparaciones
- Comparar
nivel == 0con flotantes en vez de tolerancia o de<= 0. El invariante debe usar margen. - Estado global
- Listas o generadores que sobreviven entre llamadas. Cada corrida debe partir de una construcción fresca.
05 · Representación en Python
Tests, traza e invariantes en código
Una implementación verificada no es solo la función que simula: es la función más su suite de chequeos. Este es el molde mínimo.
Python en tu navegador. Ejecutá la suite. Después introducí un bug (quitá el max(0,…)) y observá qué test lo caza primero.
def simular(nivel_inicial, entrada, salida, tope, pasos, traza=None):
nivel = nivel_inicial
historia = []
for t in range(1, pasos + 1):
antes = nivel
nivel = min(tope, max(0, nivel + entrada - salida))
# Invariante en caliente: el programa se acusa a sí mismo
assert 0 <= nivel <= tope, f"invariante roto en t={t}: {nivel}"
assert t == len(historia) + 1, "tiempo fuera de orden"
historia.append(nivel)
if traza is not None:
traza.append((t, antes, entrada, salida, nivel))
return historia
# Suite de verificación (oráculos exactos, sin datos reales)
assert simular(10, 3, 2, 14, 3) == [11, 12, 13]
assert simular(5, 4, 4, 14, 3) == [5, 5, 5]
assert simular(2, 1, 3, 14, 3) == [0, 0, 0]
assert simular(14, 3, 2, 14, 2) == [14, 14]
assert len(simular(10, 3, 2, 14, 12)) == 12
print("Suite OK: 5/5 tests exactos")
La suite tarda milisegundos y se ejecuta en cada cambio. Ese es el punto: verificación barata y continua.
Traza legible + determinismo del azar
import random
def replica_con_azar(semilla, pasos):
rng = random.Random(semilla) # un generador por réplica, aislado
nivel, historia = 10, []
for t in range(pasos):
llegada = rng.choice([2, 3, 4]) # azar controlado
nivel = max(0, min(14, nivel + llegada - 2))
historia.append(nivel)
return historia
traza = []
print(simular(10, 3, 2, 14, 3, traza=traza))
for t, antes, e, s, despues in traza:
print(f"t={t}: {antes} +{e}-{s} -> {despues}")
a = replica_con_azar(42, 6)
b = replica_con_azar(42, 6)
print("determinista:", a == b, a)
06 · Exploración
Laboratorio: suite de verificación en vivo
Este simulador de tanque esconde dos interruptores de bug: desfase de un paso (off-by-one) y olvido del piso (permite negativos). Mové la entrada y activá cada bug para ver qué tests de la suite caen y cómo la curva naranja se separa de la esperada (lima).
¿Obedece el código al papel?
suite: 6 tests · esperada vs. programa
Implementación verificada: 6/6 tests exactos, sin desfase y con piso.
La curva lima es la historia que la especificación promete. La curva naranja es lo que el programa calcula con los bugs activados. Los puntos rojos marcan pasos donde el invariante 0 ≤ nivel ≤ tope se viola.
Preguntas para explorar
- Activá el desfase con entrada 3 y salida 2. ¿Cuántos tests caen? ¿Por qué un error de un paso contamina toda la historia?
- Activá «olvidar el piso» con entrada 1 y salida 4. ¿Dónde aparecen los negativos? ¿Qué test los caza aunque el balance parezca razonable?
- ¿Por qué la suite usa valores exactos y no tolerancias? ¿Qué pasaría si validaras (con MAE) un programa con desfase?
Ver respuestas sugeridas
- Cae el test de longitud/orden y todos los valores se corren un lugar: el error se propaga porque cada nivel depende del anterior. Un off-by-one nunca es «solo un paso».
- Los negativos aparecen desde que el nivel cruza cero; los caza el invariante 0 ≤ nivel, no el promedio. El MAE podría disimularlos si luego se compensa.
- La verificación exige igualdad exacta: el programa debe obedecer bit a bit. Las tolerancias son para la validación contra la realidad (Tema 1), nunca para disimular un bug.
07 · Comprensión
Confusiones frecuentes
«Si no lanza excepciones, está verificado»
Las excepciones cazan crashes, no obediencia. Solo los oráculos (valores exactos, invariantes, determinismo) verifican.
«Probar con datos reales verifica el código»
Probar con datos reales valida (o invalida) el modelo, pero un bug puede compensarse con un sesgo y parecer correcto. La verificación usa datos sintéticos exactos.
«Los tests lentos se corren al final»
La suite de verificación debe tardar segundos y correrse en cada cambio. Lo que se corre al final, no se corre.
«El azar no se puede testear»
Sí: con semilla fija el azar es determinista. Se testea la igualdad de historias y el aislamiento entre réplicas, no el «parecido aleatorio».
08 · Práctica guiada
Ejercicios con Python
Ejercicio 1: cazar el off-by-one
Esta función devuelve un paso de más. Escribí el assert que la delata.
def sim_off(nivel, entrada, salida, pasos):
h = []
for t in range(pasos + 1): # BUG: un paso de más
nivel = max(0, nivel + entrada - salida)
h.append(nivel)
return h
print(len(sim_off(10, 3, 2, 3)))
print("assert len(...) == 3:", len(sim_off(10, 3, 2, 3)) == 3)
Ver solución razonada
Con 3 pasos devuelve 4 valores: len == 4. El assert assert len(sim_off(10,3,2,3)) == 3 falla y delata el range(pasos+1). La corrección es range(pasos) con reloj 1…pasos en la traza.
Ejercicio 2: invariante que grita
Agregá el invariante que detiene esta simulación en cuanto el nivel se vuelve imposible.
def sim_sin_piso(nivel, entrada, salida, pasos):
h = []
for t in range(pasos):
nivel = nivel + entrada - salida # sin max(0, ...)
assert 0 <= nivel, f"invariante roto en t={t+1}: nivel={nivel}"
h.append(nivel)
return h
print(sim_sin_piso(2, 1, 3, 2))
Ver solución
El segundo paso da nivel −2 y el assert detiene con mensaje. Sin el invariante, la función devolvería [0, -2] en silencio y contaminaría promedios y gráficos.
Ejercicio 3: determinismo entre réplicas
Dos réplicas con la misma semilla deben coincidir; con distinta semilla, casi seguro no. Comprobalo.
Ver una posible respuesta
import random
def replica(semilla):
rng = random.Random(semilla)
return [rng.choice([2, 3, 4]) for _ in range(5)]
print(replica(7) == replica(7)) # True: determinista
print(replica(7) == replica(8)) # casi seguro False: independientes
Si la primera comparación diera False, el azar no está aislado (generador global, hora como semilla o estado compartido). La verificación lo rechaza.
09 · Síntesis
Ideas para recordar
- Verificar la implementación es demostrar obediencia exacta a la spec: casos exactos, invariantes y determinismo.
- La suite es barata, rápida y continua: se ejecuta en cada cambio, no al final.
- Trazas legibles e historias doradas convierten errores invisibles en fallas gritonas.
- Un generador por réplica con semilla fija vuelve testeable lo aleatorio.
- Solo con implementación verificada tiene sentido depurar a fondo (Tema 4) y validar contra datos (Temas 5–8).
En el próximo tema veremos la depuración de simulaciones: técnicas sistemáticas para encontrar y aislar el bug cuando la suite se pone roja.