Módulo 4: La matriz — múltiples versiones y entornos

6. Leer los N jobs de la matriz

Descripción

Una matriz no produce un resultado, produce N. Donde antes tenías un check —"la suite pasa" o "la suite falla"—, ahora tienes una lista de nueve, o de seis, o de cuatro, cada uno con su nombre y su semáforo. Y esa multiplicación no es solo más para mirar: es más información. Un solo job rojo te dice "algo se rompió". Nueve celdas, ocho verdes y una roja, te dicen "algo se rompió, y fue exactamente en Python 3.11 en Windows, en ningún otro lado". Esta lección te enseña a leer esa cuadrícula de resultados: a convertir N semáforos en un diagnóstico preciso de dónde vive un bug.

Al terminar vas a poder leer la lista de jobs de una corrida de matriz —entender cómo GitHub nombra cada celda, test (windows-latest, 3.11)—, distinguir una cuadrícula toda verde de una con una celda roja o con un patrón de rojos, y leer el log de esa celda roja para saber qué falló: la línea que te dice la versión y el sistema, y el reporte de fallo de pytest que te dice el test y la aserción. Vas a ver, con salida ejecutada de verdad, cómo se lee un log rojo real de Reservo, y cómo esa lectura te da justo el dato —"falló en este entorno"— que necesitas para reproducirlo en local (módulo 3) igualando ese entorno.

Conexión con el módulo: las lecciones 3, 4 y 5 construyeron y esculpieron la matriz; esta te enseña a leer su salida. Es la contraparte de leer el log de un solo job (módulo 2), pero multiplicada: ahora el primer dato no es "¿pasó?" sino "¿cuáles pasaron y cuál no?". La lección 7 usará esta habilidad para justificar el tamaño de la matriz —una celda que nunca se pone roja de forma informativa es una celda que quizá sobra—. Y el puente hacia atrás es explícito: cuando leas "falló en 3.11", el siguiente paso —reproducirlo en tu máquina con 3.11— es la técnica del módulo 3, que aquí solo enganchamos.

El tablero de llegadas del aeropuerto

En un aeropuerto, el tablero de llegadas no dice "hay problemas" a secas. Muestra una fila por vuelo, cada una con su estado: a tiempo, aterrizó, retrasado, cancelado. Si tu vuelo dice "retrasado" y todos los demás dicen "a tiempo", sabes que el problema es de tu vuelo, no del aeropuerto —quizá la aeronave viene tarde de otra ciudad—. Pero si de repente todos los vuelos de una aerolínea dicen "cancelado" y los de las demás siguen a tiempo, el patrón te cuenta otra historia: el problema es de esa aerolínea, no de un vuelo suelto. El tablero no solo reporta; su patrón diagnostica.

La lista de jobs de una matriz es ese tablero de llegadas. Cada celda es una fila con su estado. Una celda roja aislada, rodeada de verdes, apunta a esa combinación específica: "el problema es de Python 3.11 en Windows". Una columna entera de rojos —las tres versiones de Windows— apunta al sistema: "el problema es de Windows, sin importar la versión". Una fila entera de rojos —los tres sistemas en 3.11— apunta a la versión: "el problema es de 3.11, en todos lados". Leer la matriz es leer el patrón, no solo contar rojos.

Un solo job rojo dice "algo falló". Una matriz roja dice "algo falló, y el patrón de qué celdas fallan te dice si es un problema de versión, de sistema, o de una combinación exacta". El diagnóstico empieza en la forma del rojo.

Cómo se nombra cada celda

Para leer la cuadrícula necesitas entender los nombres, porque el nombre es las coordenadas de la celda. GitHub construye el nombre de cada job así: el nombre del job base, más entre paréntesis los valores de las dimensiones de la matriz, separados por comas.

test (ubuntu-latest, 3.11)     <- job "test", os=ubuntu-latest, python-version=3.11
test (windows-latest, 3.12)    <- job "test", os=windows-latest, python-version=3.12
test (3.13)                    <- si solo hay dimension de version, solo aparece esa

El orden de los valores dentro del paréntesis sigue el orden en que declaraste las dimensiones en el YAML. Esto importa porque el nombre es lo que ves en la lista de checks de un pull request, en la pestaña de Actions, y en la protección de rama. Cuando alguien dice "falló test (windows-latest, 3.11)", te está dando las coordenadas exactas: sistema Windows, versión 3.11. Sin leer un solo log todavía, ya sabes dónde mirar.

Así se ve una corrida toda verde de la matriz 3×3:

tests · push a main
  ✓ test (ubuntu-latest, 3.11)    ✓ test (ubuntu-latest, 3.12)    ✓ test (ubuntu-latest, 3.13)
  ✓ test (macos-latest, 3.11)     ✓ test (macos-latest, 3.12)     ✓ test (macos-latest, 3.13)
  ✓ test (windows-latest, 3.11)   ✓ test (windows-latest, 3.12)   ✓ test (windows-latest, 3.13)

Nueve verdes. Tu código funciona en las nueve combinaciones que prometiste soportar. Eso es lo que la matriz te compra: no "funciona en mi máquina" sino "funciona en estas nueve, verificado".

Leer el patrón de los rojos

Ahora las formas de rojo, cada una con su diagnóstico. Supón que un cambio introdujo un bug. La matriz podría verse de varias maneras, y cada una te cuenta algo distinto.

Una celda roja aislada — el bug depende de esa combinación exacta:

  ✓ test (ubuntu-latest, 3.11)    ✓ test (ubuntu-latest, 3.12)    ✓ test (ubuntu-latest, 3.13)
  ✓ test (macos-latest, 3.11)     ✓ test (macos-latest, 3.12)     ✓ test (macos-latest, 3.13)
  ✗ test (windows-latest, 3.11)   ✓ test (windows-latest, 3.12)   ✓ test (windows-latest, 3.13)

Solo (windows, 3.11) en rojo. Diagnóstico: el bug necesita las dos condiciones a la vez —Windows y 3.11—. Quizá una dependencia sin rueda para ese par exacto, o una feature que falta solo en esa esquina. Es el rojo más específico y, a veces, el más raro.

Una columna de rojos — el bug depende del sistema:

  ✓ test (ubuntu-latest, 3.11)    ✓ test (ubuntu-latest, 3.12)    ✓ test (ubuntu-latest, 3.13)
  ✓ test (macos-latest, 3.11)     ✓ test (macos-latest, 3.12)     ✓ test (macos-latest, 3.13)
  ✗ test (windows-latest, 3.11)   ✗ test (windows-latest, 3.12)   ✗ test (windows-latest, 3.13)

Las tres celdas de Windows en rojo, las de Linux y macOS en verde. Diagnóstico: el bug es de Windows, en todas las versiones —típicamente el separador de ruta \ o el salto de línea \r\n de la lección 4—. La versión no importa; el sistema sí.

Una fila de rojos — el bug depende de la versión:

  ✗ test (ubuntu-latest, 3.11)    ✓ test (ubuntu-latest, 3.12)    ✓ test (ubuntu-latest, 3.13)
  ✗ test (macos-latest, 3.11)     ✓ test (macos-latest, 3.12)     ✓ test (macos-latest, 3.13)
  ✗ test (windows-latest, 3.11)   ✓ test (windows-latest, 3.12)   ✓ test (windows-latest, 3.13)

Las tres celdas de 3.11 en rojo, las de 3.12 y 3.13 en verde. Diagnóstico: el bug es de Python 3.11, en todos los sistemas —típicamente una función de la stdlib que usaste y que no existe antes de 3.12, como el itertools.batched de la lección 2 sin su fallback—. El sistema no importa; la versión sí.

Fíjate en el poder de esto: sin abrir un solo log, la forma del rojo ya te dio la hipótesis. Columna de Windows → mira el manejo de rutas o archivos. Fila de 3.11 → mira qué feature nueva usaste. Celda aislada → mira qué necesita esa combinación exacta. El log confirma la hipótesis; el patrón la genera.

Leer el log de una celda roja

Cuando el patrón te apunta a una celda, abres su log para confirmar. El log de una celda de matriz es idéntico al de un job normal —lo viste en el módulo 2—, con una diferencia crucial: la línea de plataforma te dice qué celda estás mirando. Veamos un log rojo real de Reservo, ejecutado de verdad. Es la celda que caza el bug del separador de ruta de la lección 4 —un test que compara contra un separador de Windows y por eso falla en macOS—:

Ejemplo trabajado

python -m pytest -v tests_red/test_report_path_bug.py

Qué esperar. En Python 3.14.0 sobre macOS, medido ejecutando de verdad:

============================= test session starts ==============================
platform darwin -- Python 3.14.0, pytest-9.1.1, pluggy-1.6.0 -- /private/tmp/reservo-m4/.venv/bin/python
cachedir: .pytest_cache
rootdir: /private/tmp/reservo-m4
collecting ... collected 1 item

tests_red/test_report_path_bug.py::test_report_path_hardcoded_separator FAILED [100%]

=================================== FAILURES ===================================
_____________________ test_report_path_hardcoded_separator _____________________

    def test_report_path_hardcoded_separator():
        # BUG: compara contra un separador '/' escrito a mano. Pasa en POSIX,
        # se rompe en Windows. Aqui lo forzamos a fallar para ver una celda ROJA.
        path = os.path.join("reports", "2026-08", "daily.txt")
>       assert path == "reports\\2026-08\\daily.txt"  # separador de Windows, en macOS falla
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E       AssertionError: assert 'reports/2026-08/daily.txt' == 'reports\\2026-08\\daily.txt'
E
E         - reports\2026-08\daily.txt
E         ?        ^       ^
E         + reports/2026-08/daily.txt
E         ?        ^       ^

tests_red/test_report_path_bug.py:8: AssertionError
=========================== short test summary info ============================
FAILED tests_red/test_report_path_bug.py::test_report_path_hardcoded_separator - AssertionError: assert 'reports/2026-08/daily.txt' == 'reports\\2026-08\\da...
========================= 1 failed in 0.02s ===============================

Léelo en el orden en que un log de matriz se lee, de arriba abajo:

  1. platform darwin -- Python 3.14.0 — lo primero. Esta línea te confirma en qué celda estás: sistema darwin (macOS), versión 3.14. En el CI, si el nombre del job decía test (macos-latest, 3.14), esta línea debe coincidir. Si no coincidieran, estarías mirando el log equivocado —un error clásico al depurar una matriz—.
  2. test_report_path_hardcoded_separator FAILED — qué test falló, con nombre y todo. En una matriz con muchos tests, esto acota de golpe.
  3. El bloque FAILURES — el reporte detallado: el código del test, la línea exacta marcada con >, y la AssertionError con los dos valores. Aquí se lee clarísimo: el test esperaba reports\2026-08\daily.txt (con \, de Windows) pero os.path.join produjo reports/2026-08/daily.txt (con /, de macOS). El diff con -/+ y los ^ señalando las posiciones que difieren te clava el problema: los separadores.
  4. El short test summary info — el resumen de una línea, perfecto para cuando hay muchos fallos y quieres la lista sin scroll.

Ahora conecta con el patrón. En este ejemplo forzamos el fallo en macOS para verlo, pero en el mundo real este bug produciría una columna de Windows roja (el test bien escrito compara contra el separador real del sistema, y el separador de Windows \ es el que no coincide con el / que el código descuidado asumió). El log confirma lo que el patrón anticipó: es un problema de separador de ruta, que es un problema de sistema operativo.

Del rojo al arreglo: el puente a reproducir en local

Leer la matriz te deja con una frase precisa: "falló en este entorno, en este test, por esta aserción". Esa frase es exactamente el punto de partida del módulo 3. Si la matriz dice "falló en test (ubuntu-latest, 3.11)", tu siguiente paso es reproducirlo en tu máquina igualando ese entorno: instalar Python 3.11 localmente (con pyenv o similar), correr la suite ahí, y ver el mismo rojo en tu terminal, donde puedes usar el depurador y experimentar. La matriz te dio el qué y el dónde; reproducir y arreglar es el módulo 3.

Vale la pena decir por qué la matriz hace esto tan fácil comparado con un solo job. Con un solo job en 3.12, si algo se rompía solo en 3.11, ni te enterabas hasta que un usuario se quejaba, y entonces tenías que adivinar el entorno. Con la matriz, el rojo ya trae las coordenadas del entorno: no adivinas, lees. "Falló en 3.11" no es una hipótesis que armas; es un dato que la celda te entregó. Eso recorta a la mitad el trabajo de diagnóstico —el módulo 3 empieza con el entorno ya identificado—.

Una nota sobre checks requeridos y protección de rama

Para cerrar, el uso más común de estos N resultados: la protección de rama. GitHub te deja marcar ciertos checks como "requeridos" para poder mergear a la rama principal. Con una matriz, cada celda es un check con nombre propio, así que puedes exigir, por ejemplo, que test (ubuntu-latest, 3.12) y test (windows-latest, 3.12) estén en verde antes de permitir el merge. Si cualquiera de esas celdas está roja, el botón de merge se bloquea.

Esto convierte la matriz de "información útil" en "puerta real": no es que veas el rojo y decidas ignorarlo; es que el rojo impide que el código roto entre a main. No vamos a profundizar en la configuración —es un ajuste del repositorio, no del YAML—, pero quédate con la idea: nombrar bien las celdas importa porque esos nombres son los que eliges como checks requeridos. Una matriz cuyos resultados nadie exige es un tablero de llegadas que nadie mira; conectada a la protección de rama, es un control de acceso.

Errores comunes

Contar rojos en vez de leer el patrón. Qué pasa: alguien ve "tres celdas rojas" y concluye "hay tres bugs", cuando en realidad es un bug que afecta a tres celdas (una columna de Windows, un solo problema de separador). Se pone a arreglar tres cosas cuando era una. Por qué pasa: se cuenta la cantidad de rojos en vez de mirar su forma. Cómo detectarlo: antes de tocar nada, pregúntate "¿estos rojos forman una fila, una columna, o están dispersos?". Cómo corregirlo: lee el patrón primero. Una columna es un problema de sistema; una fila, de versión; disperso, varios problemas. El patrón te dice cuántos bugs hay de verdad y de qué tipo.

Mirar el log de la celda equivocada. Qué pasa: la matriz dice que falló test (windows-latest, 3.11), pero abres el log de test (ubuntu-latest, 3.12) (que está verde), no ves nada raro, y te confundes. Por qué pasa: con nueve logs casi idénticos, es fácil abrir el que no es. Cómo detectarlo: mira la línea platform ... Python X.Y al inicio del log; si no coincide con la celda que crees estar viendo, es el log equivocado. Cómo corregirlo: usa siempre el nombre del job para abrir su log, y verifica la línea de plataforma como confirmación antes de sacar conclusiones.

Ignorar una celda roja porque "las demás pasan". Qué pasa: ocho celdas verdes y una roja en 3.11, y alguien mergea igual pensando "casi todo pasa". El usuario de 3.11 —a quien le prometiste soporte— recibe el bug. Por qué pasa: la mayoría verde da una falsa sensación de "está bien". Cómo detectarlo: si una celda de una versión o sistema que prometes soportar está roja, tu código está roto para esos usuarios, sin importar cuántas otras pasen. Cómo corregirlo: trata cada celda de la matriz como una promesa; una roja es una promesa incumplida. Si de verdad ya no soportas ese entorno, quítalo de la matriz (lección 5/7), no lo ignores en rojo.

Ejercicios

Ejercicio 1 — Diagnostica por el patrón. Para cada cuadrícula de resultados (matriz de 3 sistemas × 3 versiones), di si el bug parece de versión, de sistema, o de una combinación exacta, y qué mirarías primero: (a) rojas solo las tres celdas de la fila 3.11; (b) roja solo (macos-latest, 3.13); (c) rojas las tres celdas de la columna de Windows.

Ver solución
  • (a) Fila 3.11 roja → bug de versión. Las tres celdas de 3.11 fallan en todos los sistemas, y 3.12/3.13 pasan. El sistema no importa, la versión sí. Miraría primero qué feature nueva usé que no existe en 3.11 —una función de la stdlib, una sintaxis— sin su fallback (el patrón de la lección 2).
  • (b) (macos-latest, 3.13) aislada roja → bug de combinación exacta. Solo esa esquina falla. Necesita las dos condiciones: macOS y 3.13. Miraría primero algo específico de ese par —una dependencia sin rueda para macOS+3.13, o un comportamiento que solo cambió en esa combinación—. Es el rojo más raro y a veces el más difícil.
  • (c) Columna de Windows roja → bug de sistema. Las tres celdas de Windows fallan, las de Linux/macOS pasan. La versión no importa, el sistema sí. Miraría primero el manejo de rutas y archivos —separador \ vs /, saltos de línea \r\n— que es la fuente clásica de rojos solo-en-Windows (lección 4).

La regla: fila = versión, columna = sistema, celda aislada = combinación. La forma del rojo genera la hipótesis antes de abrir un log.

Ejercicio 2 — Lee el log y localiza la celda. Te pasan este encabezado de un log de CI y la primera línea del fallo. ¿En qué celda de la matriz estás, y qué tipo de problema sugiere?

platform win32 -- Python 3.11.9, pytest-9.1.1
...
E   ImportError: cannot import name 'batched' from 'itertools'
Ver solución

La celda es test (windows-latest, 3.11) —o cualquier celda de 3.11; lo que fija la línea platform win32 -- Python 3.11.9 es el sistema (Windows, win32) y la versión (3.11)—. El tipo de problema lo grita el fallo: ImportError: cannot import name 'batched' from 'itertools'. itertools.batched no existe antes de Python 3.12, así que el código lo usó sin fallback y truena en 3.11.

Diagnóstico completo: aunque veas este rojo en la celda de Windows, no es un problema de Windows —es de versión—. Si abrieras las celdas de (ubuntu, 3.11) y (macos, 3.11), también estarían rojas con el mismo ImportError: es una fila de 3.11, no una columna de Windows. La línea de plataforma te dio las coordenadas, y la naturaleza del error (batched no existe en 3.11) te dice que la dimensión culpable es la versión. El arreglo: el fallback de la lección 2, o subir la versión mínima soportada. Reproducir en local (módulo 3) igualando Python 3.11.

Ejercicio 3 — ¿Un bug o varios? Una corrida de matriz de 3×3 muestra rojas estas cuatro celdas: (windows-latest, 3.11), (windows-latest, 3.12), (windows-latest, 3.13) y (ubuntu-latest, 3.11). El resto, verdes. ¿Cuántos bugs distintos sugiere este patrón y de qué tipo es cada uno?

Ver solución

El patrón sugiere dos bugs distintos, porque los rojos no forman una sola figura limpia sino dos superpuestas:

  1. Una columna completa de Windows (3.11, 3.12, 3.13 de windows-latest) → un bug de sistema operativo, presente en todas las versiones de Windows. Probable causa: manejo de rutas (\ vs /) o saltos de línea. La versión no lo afecta.
  2. Una celda extra de Linux en 3.11 (ubuntu-latest, 3.11), que no encaja en la columna de Windows → un segundo problema, de versión, que afecta a 3.11 pero que en Windows queda "tapado" por el bug de sistema. Probable causa: una feature de la stdlib que falta en 3.11.

Cómo verificarlo: la celda (macos-latest, 3.11) sería la prueba —si también estuviera roja, confirmaría que el bug de 3.11 es de versión y afecta a todos los sistemas (una fila de 3.11), y que solo se ve "limpio" en Linux y macOS porque Windows ya está rojo por otra causa—. En el enunciado (macos, 3.11) está verde, lo que complica la lectura: podría ser que el bug de 3.11 dependa además de algo de Linux. La lección real: cuando los rojos no forman una figura limpia, sospecha más de un bug, y usa las celdas verdes/rojas de alrededor para separar las causas. Contar "cuatro rojos = cuatro bugs" sería el error; leer el patrón revela que probablemente son dos causas, una de sistema y una de versión.

Resumen y siguiente paso

En esta lección aprendiste a leer los N resultados de una matriz como un tablero de llegadas: no cuentas rojos, lees su patrón. Una celda aislada roja apunta a una combinación exacta; una columna, a un sistema; una fila, a una versión. La forma del rojo genera la hipótesis antes de abrir un solo log. Viste cómo GitHub nombra cada celda con sus coordenadas —test (windows-latest, 3.11)— y cómo ese nombre es tu mapa, tanto para localizar como para exigir checks requeridos en la protección de rama.

Leíste un log rojo real de Reservo de arriba abajo: la línea platform darwin -- Python 3.14.0 que confirma qué celda estás mirando, el FAILED que dice qué test, el bloque FAILURES con la aserción y el diff que clava la causa (los separadores de ruta), y el short test summary. Y quedó claro el puente: leer la matriz te deja con "falló en este entorno, en este test, por esta aserción" —exactamente el punto de partida para reproducirlo en local igualando ese entorno, que es el módulo 3—.

Antes de avanzar deberías poder: leer una cuadrícula de resultados y diagnosticar por su patrón (fila/columna/celda); nombrar las coordenadas de una celda a partir de su nombre; leer un log rojo y confirmar en qué celda estás por la línea de plataforma; y distinguir un bug que afecta varias celdas de varios bugs distintos.

Lo que sigue, en la lección 7, es la pregunta que ha estado rondando todo el módulo: ¿cuándo esta matriz paga y cuándo es puro ruido y costo? Ya sabes construirla, esculpirla y leerla; ahora vas a decidir, con criterio y un modelo de costo, qué matriz merece de verdad un proyecto —una librería usada por miles versus una app interna de una sola versión— para no encender nueve celdas por reflejo cuando una cuenta la historia completa.

Recursos