Blog ·
En verde porque no podía llegar hasta allí
Nuestra prueba de contrato respondía al comando más corriente con «no admitido». Toda prueba que dependía de él estaba en verde por el motivo equivocado.
Por Provibr
Mantenemos un agente simulado. Habla el mismo protocolo que el real, se ejecuta junto a la pasarela y existe para que el lado de plataforma de cada comando pueda probarse sin tocar el hipervisor de un cliente. Es una prueba de contrato con un proceso alrededor.
Hasta esta semana ese simulado respondía al comando más corriente que hay, apagar esa máquina, con COMMAND_STATUS_UNSUPPORTED.
Nadie se había dado cuenta, y la razón por la que nadie se había dado cuenta es el asunto de este artículo. Toda prueba que enviaba un comando de encendido y apagado al simulado estaba en verde. Estaban en verde porque el comando se rechazaba, la plataforma trataba el rechazo correctamente y la afirmación del final se cumplía. Las pruebas no mentían. Describían un camino que nadie pretendía recorrer.
Lo que una rama que falta les hace a las pruebas de alrededor
El motor de flujos tiene un nodo de acción: ejecutar un comando contra un servidor y después seguir el cable ok o el cable error según cómo haya ido. Ese nodo estaba en el producto desde la ronda que lo introdujo, y tenía pruebas.
Esas pruebas solo podían alcanzar el cable error. No por un fallo en ellas, sino porque el único agente del que disponían rechazaba todos los comandos que podían enviar. Una prueba que afirma «se tomó la rama de error» pasa perfectamente cuando el error lo fabricó el arnés de pruebas y no la cosa que se está probando.
Esta es la forma contra la que avisan nuestras propias notas, en una frase que lleva meses en el manual del proyecto: una prueba puede estar en verde porque no puede alcanzar el buen camino. La variante que nos mordió aquí es peor que la habitual, porque no se trata de una rama que nunca se escribió. La rama estaba escrita, funciona, y tuvo al lado una prueba que pasaba todo el tiempo.
El simulado tenía que mantener estado, no solo responder
Añadir los cinco comandos de encendido y apagado no era cuestión de devolver éxito. La plataforma decide qué le está permitido hacer a un comando leyendo el estado actual del servidor: no se puede reanudar una máquina que está en marcha, no se puede arrancar una que ya está levantada. Esa decisión vive en la plataforma, y lee una columna.
Un agente que responde «hecho» sin cambiar nada deja esa columna donde estaba. Así que una prueba puede enviar exactamente un comando y después se queda atascada, porque el segundo comando lo rechaza la propia máquina de estados de la plataforma, correctamente, sobre un estado que nunca se movió.
El simulado mantiene por tanto un estado de encendido por máquina y devuelve la observación de estado por la sesión, igual que hace un agente real. Eso es lo que faltaba de verdad. No una respuesta, una consecuencia.
Con eso puesto, un flujo que recorre una lista en bucle y emite dos comandos por pasada pudo ejecutarse contra el simulado por primera vez. Seis comandos, todos llegando a completado, y un bucle que alcanzó su segunda pasada porque la primera había cambiado algo de verdad.
Dos cosas que el simulado deliberadamente no hace
No rechaza transiciones imposibles con un código de error inventado. La frontera entre lo permitido y lo no permitido vive en la plataforma, y los códigos de motivo del agente son una lista fija compilada en el código fuente del agente real. Inventarse un código que el agente real no tiene sería probar un contrato que no existe. Un simulado más estricto que aquello a lo que sustituye enseña cosas que no son ciertas.
Tampoco simula duración. Un apagado sobre un huésped real espera a que el sistema operativo responda y se repliega a una parada dura pasado un tiempo de espera. En el simulado todo ocurre dentro del tic. Pero el tiempo de espera y la marca de repliegue se arrastran hasta la salida, así que una prueba puede ver que llegaron aunque nada haya esperado por ellos. Pasar un parámetro y respetarlo son afirmaciones distintas, y solo se está haciendo una de las dos.
Un segundo despertador vuelve aleatoria una medición
La otra reparación de la misma ronda va sobre la medición y no sobre el producto.
A las ejecuciones en pausa las despierta un trabajador de la pasarela que consulta una cola cada diez segundos. Nuestro script de extremo a extremo hace el despertar él mismo, dentro del proceso, para poder afirmar exactamente qué ocurrió. Los dos van bien por separado y son catastróficos juntos: si una pasarela con el despertador activado se está ejecutando contra la misma base de datos de desarrollo, se come las filas en cuanto pasa su momento de despertar, y el script mide al que de los dos haya llegado primero.
Eso ocurrió. Una rama del script falló una vez sin que hubiera nada mal en ella, que es la clase de fallo más cara porque la respuesta natural es ponerse a buscar un error.
El arreglo no es un comentario que diga «asegúrese de que no hay ningún trabajador en marcha». Es una función que lo mide. Antes que nada, el script crea una espera desechable con un momento de despertar ya vencido, la vigila durante ocho segundos y comprueba si alguien más la tocó. Si alguien lo hizo, el script se niega a ejecutarse y dice por qué.
Eso es poco código para mucha confianza. Una suposición escrita en un comentario es una suposición; la misma suposición escrita como una comprobación que se ejecuta primero es una condición previa.
Los reintentos necesitan un techo y una espera creciente
Ya que los buenos caminos por fin eran alcanzables, los caminos de fallo también se midieron como es debido.
Una espera que lanza un error al reanudarse, porque se borró una plantilla o un servidor ya no existe, de lo contrario se reintentaría en cada tic para siempre, en una tabla de cola que nunca se vacía. Hay un techo de cinco intentos con una espera que se dobla: sesenta, ciento veinte, doscientos cuarenta, cuatrocientos ochenta segundos. Esos cuatro intervalos se leyeron de una ejecución real y no de la función que los calcula. Al quinto intento la espera se cancela y la ejecución se marca como fallida.
La contraprueba es la parte que importa. Con la guarda de agotamiento desactivada, la misma ejecución se queda en el estado de espera indefinidamente. Una guarda solo queda demostrada cuando la situación que evita se ha producido de verdad.
Debajo hay un segundo modo de fallo. Un trabajador reclama una espera sacándola del estado de espera antes de empezar a recorrer, porque los efectos secundarios de un flujo ocurren durante el recorrido y no después. Si el proceso que la reclamó muere a mitad del recorrido, la fila se queda en el estado de reclamo para siempre, y la consulta que busca esperas vencidas no la ve: pide filas en espera. Un barrido devuelve los reclamos de más de quince minutos, y un solo tic contra una pasarela realmente en marcha liberó uno, despertó uno y reanudó uno, sin ningún fallo.
Algunas cosas pequeñas que solo se encuentran ejecutándolas
Un retardo de tres segundos no produce un nodo inválido. Invalida el grafo entero, porque el mínimo es cinco, y el ejecutor no devuelve nada en absoluto en lugar de una ejecución con un nodo roto. Eso es correcto y también resulta confuso la primera vez, porque se esperaba un error sobre un nodo y se obtuvo silencio sobre un grafo.
Los puertos de la pasarela llevan el nombre de la pasarela. Poner las variables sin ese prefijo la deja escuchando en sus valores por defecto, en silencio, lo que se parece exactamente a un problema de red.
Una parada escribe en el libro como saltados los nodos a los que nunca llegó. Esa es una diferencia deliberada frente a dejarlos fuera: una ejecución que terminó pronto y una ejecución que tomó otro camino son idénticas si los nodos no alcanzados simplemente no están.
Y un nodo de aprobación estaba dejando filas huérfanas en la tabla de notificaciones, dieciséis de ellas en la base de datos de desarrollo. La limpieza de eso va por ejecución y va indexada por la clave de deduplicación, nunca una coincidencia de patrón sobre un prefijo, porque una coincidencia de patrón se llevaría por delante filas pertenecientes a la ronda de otra persona.
Lo que dejaríamos por escrito
- Una prueba que no puede alcanzar el buen camino está en verde por el motivo equivocado. Pregúntese qué haría la prueba si la funcionalidad se borrara.
- Un simulado que lo rechaza todo no es un simulado, es un muro. Toda prueba que hay detrás mide el rechazo.
- Un doble tiene que mantener estado, no solo responder. Si la plataforma decide sobre una columna, algo tiene que mover esa columna.
- No haga un simulado más estricto que la cosa real. Un código de error inventado prueba un contrato que nadie implementa.
- Arrastre un parámetro aunque no pueda respetarlo, para que una prueba pueda demostrar que llegó.
- Dos despertadores sobre una base de datos vuelven aleatoria la medición. Gana uno de los dos y no se sabe cuál.
- Convierta la condición previa del comentario en una comprobación que se ejecuta primero. Entonces es un hecho y no una esperanza.
- Demuestre una guarda apagándola. Un techo de reintentos con el que nadie ha chocado es una afirmación.
- Una fila reclamada es invisible para la consulta que busca trabajo vencido. Algo tiene que devolver los reclamos rancios.
- Los nodos no alcanzados van en el libro como saltados, o una salida temprana es indistinguible de otra ruta.
Para que quede claro qué es esto: siete ramas antes sin demostrar se recorrieron de extremo a extremo, cada una con una contraprueba en rojo; la verificación de la fusión recorrió 16 de 16 pasos sin ninguna línea de fallo, el conjunto de pruebas web dio 4583 en verde, paquetes, pasarela y herramientas juntos 7287 en verde, el conjunto propio del agente simulado 481 en verde, el script de extremo a extremo 89 en verde, la rama del despertador 13 en verde, la rama del correo de aprobación 22 en verde y el bucle con acciones reales 18 en verde. Sin demostrar: no se ejecutó nada contra un Proxmox real, un grafo ilegible en la propia fila de espera, un lote mayor que el tamaño de lote y la entrega real de correo por SMTP.
Del trabajo de plataforma en Provibr. Cómo el recorrido aprendió a detenerse y continuar mañana está en toda llamada a sí misma estaba en posición de cola, y dónde está la línea entre una pausa que una petición puede absorber y una que suspende la ejecución está en una pausa que mantenía abierto el pedido.