Ir al contenido

Las capas de prueba

Las pruebas están en tres capas, y conviene distinguirlas porque solo la primera es gratis. Dos de ellas levantan nueve contenedores, y quien decide si espera merece saber qué compra con esa espera.

CapaOrdenDockerTiempoQué demuestra
L1, el contratomake testnounos 5 slos bytes exactos que cada destino pone en el cable
L2, los almacenesmake test-e2e-docker53 s en calienteque un almacén real acepta esos bytes y conserva la fecha del hecho
L3, los dashboardsel mismo objetivoincluido arribaque las consultas propias de los cinco dashboards responden contra lo que escribieron los destinos

L1 corre en cada push. L2 y L3 son una sola suite tras la etiqueta de compilación dockere2e, así que go test ./... no levanta ningún contenedor; en CI corren semanalmente, a demanda y como puerta de publicación.

test/e2e compila el binario real, lo ejecuta contra un GitHub falso cuyas fixtures están en test/e2e/testdata y cuya tabla de rutas es test/e2e/fakegh, y apunta cada destino a un servidor de captura httptest. Después comprueba los bytes: el line protocol, el sobre _bulk, las sentencias SQL, la ruta de Graphite, la carga OTLP.

El falso además cobra sus respuestas como lo hace api.github.com: un ETag por fixture REST y ninguno en las respuestas GraphQL, un 304 sin coste para la petición que lo presenta, uno por cada otra respuesta de la API, nada por el almacenamiento de objetos al que redirige un log de job, y el bloque de presupuesto en cada respuesta GraphQL. Eso es lo que permite que TestTheSecondSweepIsPricedByTheCache ejecute dos pasadas en un mismo proceso y sostenga que cada URL respondida 200 la primera vez volvió 304 la segunda, que la segunda pasada cobró menos de la mitad de las peticiones core de la primera, y que own_cost en la fila gh_rate_limit es el número de consultas que hizo el proceso y no cero.

Esa es la prueba correcta para un formato, y es lo bastante rápida para correr mientras se toca un destino. Lo que no puede detectar es nada sobre lo que el receptor tenga una opinión. Un servidor de captura responde 204 a todo. No tiene tipos de columna, ni mapping, ni planificador de consultas, ni esquema.

test/e2e/docker levanta los almacenes reales en contenedores, ejecuta una pasada desde el mismo GitHub falso hacia todos ellos y luego relee cada almacén y comprueba el valor, las etiquetas y sobre todo la marca de tiempo.

La regla de fechado es el producto de esta herramienta: una estrella se fecha cuando se dio, una ejecución de workflow cuando terminó, un día de tráfico en la fecha de ese día. Ninguna prueba de captura puede demostrar que el almacén guardó la fecha del hecho y no la de la pasada, porque guardarla es trabajo del almacén.

Estos son los defectos por los que existe esta capa, todos reales:

InfluxDB fija el tipo de una columna la primera vez que la ve. InfluxDB 3 decide si una columna es etiqueta o campo en cuanto la ve y rechaza toda escritura posterior que no coincida: 400 invalid column type for column 'owner', expected iox::column_type::tag. Un servidor de captura responde 204 y no se entera. Esto ya costó borrar una base de datos, y reproducirlo fue lo primero que se hizo con el stack en contenedores.

El mapping dinámico de Elasticsearch decide si los dashboards pueden agregar. Un panel saca una url con un top_metrics, que normalmente necesita un campo keyword y no uno de texto. Dos auditorías distintas lo anotaron como no verificable por falta de un Elasticsearch real. Esta capa lo responde: indexando por _bulk y releyendo el mapping que el clúster se construyó solo.

PostgreSQL tiene que aceptar el DDL. El destino SQL emite sentencias en lugar de hablar el protocolo, así que hasta ahora nadie las había parseado nunca. La suite las pasa por psql, inserta y planifica las consultas de los paneles contra el esquema que creó el destino, y no contra uno transcrito de InfluxDB.

Las rutas de Graphite deben tener la profundidad que indexan los dashboards. Los dashboards direccionan los nodos de la ruta por posición, y el acuerdo entre esas posiciones y lo que escribe el destino lo mantenía una tabla a mano que no comprobaba nadie. Aquí el destino escribe a carbon y se le pide la ruta de vuelta a la API de render.

Con los almacenes ya cargados, los cinco dashboards generados se ejecutan por /api/ds/query de Grafana, que es el camino que toma cmd/check_dashboards contra un Grafana vivo. Cada datasource se provisiona al arrancar con un uid fijo y el arnés emite un token de cuenta de servicio, así que la consulta de un panel pasa por Grafana igual que para una persona que mira el dashboard.

Eso es lo que convierte tres comprobadores manuales en algo que la CI ejecuta, y lo que zanja la duda de Elasticsearch de arriba: un panel que no puede agregar no devuelve ningún data frame.

Todas las capas corren contra el GitHub falso, así que aquí nadie se entera de que GitHub cambie una carga, retire un endpoint o limite de otra manera. Para eso está ghchronicle -once contra un token real.

Tampoco demuestran nada sobre un almacén que la suite no levanta. La respuesta cubre InfluxDB 3 Core, PostgreSQL 18, Elasticsearch 9, Graphite 1.1, Prometheus 3, Loki 3, el colector de OpenTelemetry y Telegraf, en las versiones fijadas. OpenSearch, TimescaleDB y todo lo que quede detrás del salto de Telegraf o de OTLP siguen siendo una inferencia a partir del formato.

Hay cinco cosas más, y ninguna es una capa. Cada una se enciende con una variable de entorno y se salta cuando no está, así que un go test ./... corriente sigue sin red.

El almacén que tú ejecutas. test/live envía por push un puñado de puntos a un Loki o a un colector de OpenTelemetry nombrados en GHC_LIVE_LOKI o GHC_LIVE_OTLP. Responde a la única pregunta que los contenedores no pueden: si tu instancia los acepta.

Ventana de terminal
GHC_LIVE_LOKI=http://localhost:3100 go test ./test/live/

La API real, de extremo a extremo. GHC_E2E_LIVE=1 corre TestLiveAPI contra GitHub en vez de contra el falso, con un GITHUB_TOKEN de verdad, y hace la pasada sobre la cuenta que nombra GHC_E2E_USER.

Ventana de terminal
GHC_E2E_LIVE=1 GHC_E2E_USER=octocat GITHUB_TOKEN=ghp_... go test ./test/e2e/ -run TestLiveAPI

Lo que cuesta una pasada en caché. GHC_LIVE_CONFIG apunta TestLiveSweepCacheFootprint a un fichero de configuración y hace la pasada sobre la cuenta que nombra, informando de las entradas y los bytes que guarda la caché de peticiones condicionales tras cada pasada. Esas son las cifras sobre las que descansan la cota de 256 MB y el coste de una pasada, y así es como se reproducen para tu propia cuenta. GHC_LIVE_DUMP=1 añade la lista por url a la salida estándar.

Ventana de terminal
GHC_LIVE_CONFIG=config.yaml go test ./internal/ghapi/ -run TestLiveSweepCacheFootprint -v

Un repositorio, una familia. cmd/probe corre los colectores contra un solo repositorio e imprime la línea que escribirían, sin escribir nada en ningún sitio. GHC_DUMP=<familia> imprime entero cada punto de esa familia, que es la forma más rápida de ver lo que produce de verdad un colector.

Ventana de terminal
go run ./cmd/probe owner/name
GHC_DUMP=actions go run ./cmd/probe owner/name

Las imágenes de la tarjeta. GHC_CARD_GALLERY nombra un directorio que ya existe y TestCardGallery dibuja en él una tarjeta por diseño, desde el GitHub falso y no desde la cuenta de nadie. De ahí salen las imágenes de la página de diseños, y un diseño que cambia de forma queda a un comando de tener unas imágenes que concuerden con él. La cuenta son los fixtures base con test/e2e/testdata/gallery/ superpuesto: un año de contribuciones, los catorce días completos de tráfico de GitHub, cinco repositorios que ordenar y uno de ellos en seis lenguajes, que la cuenta más pequeña sobre la que afirma el resto de suites no puede dar a una imagen. Un fixture llamado <repo>~<fixture> responde solo por ese repositorio, y cualquier otro toma prestado el de hello-world. Cada diseño sale de una sola pasada con -card-theme both como dos ficheros, card-<diseño>.svg en la paleta clara y card-<diseño>_dark.svg en la oscura, que es lo que leen el ThemeImage del sitio y el <picture> del README. Los dos diseños que hacen bucle salen una segunda vez con -card-motion loop, como card-<diseño>-loop.svg y su gemela _dark. Solo esos dos: en cualquier otro diseño loop dibuja la misma tarjeta que once, así que una imagen en bucle suya sería una segunda copia de la primera con un nombre que promete otra cosa. Cuáles son lo dice Loops en el registro, y la galería lo lee en vez de mantener su propia lista.

Ventana de terminal
mkdir -p /tmp/cards
GHC_CARD_GALLERY=/tmp/cards go test ./test/e2e/ -run TestCardGallery

make check-gallery dibuja la galería en un directorio temporal y falla, nombrando cada diferencia, si el conjunto guardado en el repositorio ya no coincide byte a byte; make gallery la regenera en su sitio. El job «Generated artifacts» de CI ejecuta la comprobación en cada pull request.

Docker con el plugin de compose, y sitio para las imágenes. Luego:

Ventana de terminal
make test-e2e-docker

Arriba, ejecutar, abajo por todos los caminos incluido el de una comprobación fallida, y después una verificación de que docker ps no deja nada del proyecto. Una suite que deja nueve contenedores tras un fallo es una suite que nadie ejecuta dos veces.

Arranque, medido en frío con las imágenes ya descargadas: Elasticsearch 29 s, Loki 21 s, Grafana 13 s, la API de render de Graphite 10 s, InfluxDB 8 s, PostgreSQL 6 s, el resto 6 s. El stack está listo en 30 s; el objetivo de principio a fin, con el desmontaje incluido, son 53 s.

El motivo para fallar una comprobación es ir a mirar el almacén, y un almacén que ya se ha desmontado no se puede mirar. Por eso las dos mitades son objetivos separados, y el arnés reutiliza un stack que encuentra ya levantado y lo deja levantado.

  1. Levanta los almacenes y déjalos en pie. La orden imprime el puerto en el que acabó cada servicio.

    Ventana de terminal
    make e2e-docker-up
  2. Ejecuta la suite, o una sola prueba de ella, tantas veces como haga falta.

    Ventana de terminal
    go test -count=1 -tags dockere2e -timeout 30m -v ./test/e2e/docker/

    GHCHRONICLE_E2E_KEEP=1 además impide que el binario de pruebas desmonte un stack que levantó él mismo, que es lo que quieres cuando falla un único -run.

  3. Pregúntale al almacén qué piensa y luego desmóntalo.

    Ventana de terminal
    make e2e-docker-logs SERVICE=influxdb
    make e2e-docker-down

Con los puertos del paso 1:

Ventana de terminal
# Qué cree InfluxDB que es cada columna. Esta es la respuesta a un 400 al escribir.
curl -s "http://127.0.0.1:<influx>/api/v3/query_sql?db=ghchronicle" \
--data-urlencode "q=SELECT * FROM information_schema.columns WHERE table_name = 'gh_repo'"
# El mapping que Elasticsearch se construyó solo.
curl -s "http://127.0.0.1:<es>/ghchronicle-*/_mapping?pretty"
# Lo que el destino SQL creó de verdad.
psql "postgres://ghchronicle:ghchronicle@127.0.0.1:<pg>/ghchronicle" -c '\d+ gh_repo'
# La ruta de Graphite, nodo a nodo.
curl -s "http://127.0.0.1:<graphite>/metrics/find?query=github.repo.*"

Grafana está en el puerto que publicó, con admin y admin, y todos los datasources vienen provisionados, así que la consulta de un panel se puede pegar en Explore y ejecutar a mano.

Cada una de estas produjo en silencio una respuesta equivocada antes de que se encontrara, y cada una está en el fichero de compose o en su configuración con la medición al lado:

  • Carbon descarta sin avisar un punto más antiguo que su archivo más largo. Una estrella fechada en 2020 desapareció bajo una retención de seis años y la escritura se dio por aceptada. Por eso la retención es 1d:12y.
  • El MAX_CREATES_PER_MINUTE por defecto de carbon es 50, menos rutas de las que crea una sola pasada, así que se perdería la mayor parte de la primera.
  • Loki responde 204 a un push y no lo sirve hasta que se vuelca el chunk. La prueba sondea en vez de preguntar una vez, y chunk_idle_period es 5 s.

Una máquina que añade la regla ejecuta esas comprobaciones por primera vez, que es cuando promNeedsHistory en dashboards_test.go empieza a importar: el exportador vive solo lo que dura el test, así que cada panel de series temporales y cada panel construido sobre increase() se comprueba contra nada, y solo se afirman los paneles instantáneos.

.github/workflows/e2e.yml ejecuta make test-e2e-docker y tiene tres formas de entrar: lanzamiento manual con una ref opcional, una programación semanal sobre main y workflow_call, para que un pipeline de publicación condicione una etiqueta a él con una línea en vez de con una copia del trabajo que se desvía del original.

No es una comprobación obligatoria en una pull request: nueve contenedores y unos 10 GB de imágenes son demasiado para cada push, y para eso está L1. La ejecución semanal es el sentido de la programación. Nada más en el repositorio levanta un contenedor, así que sin ella la suite solo correría cuando alguien se acordara, que es como una suite acaba rota durante semanas sin que nadie lo sepa.

La misma suite corre también bajo el detector de carreras, en .github/workflows/race.yml: cada semana, en cada publicación junto a la puerta de E2E, y a mano. El arnés compila el colector con -race y lo arranca con GORACE=halt_on_error=1, así que una carrera dentro del colector hace fallar la prueba que lo lanzó, con el informe. En local es make test-e2e-docker-race.