From 3a3a8fd9a956d13be5295148cd0f68fd697f67ea Mon Sep 17 00:00:00 2001 From: Egutierrez Date: Sun, 5 Apr 2026 18:19:26 +0200 Subject: [PATCH] docs: convenciones de testing y schema unit_tests/e2e_tests Nuevo docs/testing.md con convenciones de test por lenguaje (Go, Python, Bash con 3 opciones), tablas unit_tests y e2e_tests, consultas FTS5 de ejemplo. Actualiza functions.md y CLAUDE.md con referencia a unit_tests. --- .claude/CLAUDE.md | 5 + docs/functions.md | 2 +- docs/testing.md | 247 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 253 insertions(+), 1 deletion(-) create mode 100644 docs/testing.md diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index 34105fa0..a5500ea8 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -58,9 +58,14 @@ sqlite3 registry.db ".schema" **types** — columnas: `id, name, lang, domain, version, algebraic, definition, description, tags, uses_types, file_path, created_at, updated_at, examples, notes, documentation, code, content_hash, source_repo, source_license, source_file` - Enums: `algebraic`(product|sum) +**unit_tests** — columnas: `id, function_id, name, code, file_path, lang, created_at, updated_at` +- Extraidos automaticamente por `fn index` desde los archivos de test +- FK: `function_id` → `functions.id` + **FTS5 (columnas buscables):** - `functions_fts`: id, name, description, tags, signature, domain, example, notes, documentation, code - `types_fts`: id, name, description, tags, domain, examples, notes, documentation, code +- `unit_tests_fts`: id, name, code, function_id, lang --- diff --git a/docs/functions.md b/docs/functions.md index afdee4f5..fed7ffc3 100644 --- a/docs/functions.md +++ b/docs/functions.md @@ -73,7 +73,7 @@ DataTable(props: { data: T[]; columns: ColumnDef[]; onRowClick?: (row: T) | `kind: pipeline` | `purity` siempre `impure`. `uses_functions` no puede estar vacío. | | `purity: pure` | `returns_optional` siempre `false`. `error_type` vacío. Una pura que devuelve opcional debe modelarse como tipo suma, no como `returns_optional: true`. | | `purity: impure` | `error_type` obligatorio. Toda impura declara explícitamente qué puede salir mal. | -| `tested: true` | `test_file_path` obligatorio. `tests` no puede estar vacío. | +| `tested: true` | `test_file_path` obligatorio. `tests` no puede estar vacío. `fn index` extrae los test cases a la tabla `unit_tests` (ver [testing.md](testing.md)). | | `tested: false` | `tests` vacío. `test_file_path` vacío. | | `uses_functions[]` | Todos los IDs deben existir en la tabla `functions`. Sin referencias huérfanas. | | `uses_types[]` | Todos los IDs deben existir en la tabla `types`. Sin referencias huérfanas. | diff --git a/docs/testing.md b/docs/testing.md new file mode 100644 index 00000000..7de02859 --- /dev/null +++ b/docs/testing.md @@ -0,0 +1,247 @@ +# Testing + +El registry tiene dos niveles de tests: + +- **Unit tests** (`unit_tests` en `registry.db`) — tests individuales extraidos automaticamente de los archivos de test de cada funcion. +- **E2E tests** (`e2e_tests` en `operations.db`) — tests de integracion que verifican como las funciones se componen dentro de una app. + +--- + +## Unit tests + +`fn index` lee cada archivo de test referenciado por `test_file_path` en las funciones testeadas, extrae los test cases individuales con su codigo, y los inserta en la tabla `unit_tests`. + +### Tabla `unit_tests` + +| Campo | Tipo | Descripcion | +|---|---|---| +| `id` | string | `{function_id}_t{n}` (ej: `filter_slice_go_core_t0`) | +| `function_id` | string | FK a `functions.id` | +| `name` | string | Nombre del test case | +| `code` | string | Codigo fuente completo del test | +| `file_path` | string | Ruta relativa al archivo de test | +| `lang` | string | Lenguaje (go, py, bash) | +| `created_at` | datetime | Fecha de indexacion | +| `updated_at` | datetime | Fecha de ultima indexacion | + +FTS5 disponible sobre `id`, `name`, `code`, `function_id`, `lang`. + +### Consultas utiles + +```bash +# Todos los tests de una funcion +sqlite3 registry.db "SELECT id, name FROM unit_tests WHERE function_id = 'filter_slice_go_core';" + +# Buscar tests por contenido (FTS5) +sqlite3 registry.db "SELECT id, function_id, name FROM unit_tests WHERE id IN (SELECT id FROM unit_tests_fts WHERE unit_tests_fts MATCH 'retry') LIMIT 10;" + +# Tests por lenguaje +sqlite3 registry.db "SELECT lang, COUNT(*) FROM unit_tests GROUP BY lang;" + +# Ver codigo de un test +sqlite3 registry.db "SELECT code FROM unit_tests WHERE id = 'cache_decorator_py_core_t0';" +``` + +--- + +## Convenciones de test por lenguaje + +El parser automatico de `fn index` detecta test cases segun el lenguaje. Para que los tests se extraigan correctamente, seguir estas convenciones. + +### Go + +Convencion estandar de Go. El parser detecta funciones `func TestXxx(t *testing.T)`: + +```go +func TestFilterSlice(t *testing.T) { + t.Run("filtra pares", func(t *testing.T) { + got := FilterSlice([]int{1, 2, 3, 4, 5}, func(n int) bool { return n%2 == 0 }) + if len(got) != 2 || got[0] != 2 || got[1] != 4 { + t.Errorf("got %v, want [2 4]", got) + } + }) + + t.Run("slice vacio retorna vacio", func(t *testing.T) { + got := FilterSlice([]int{}, func(n int) bool { return true }) + if len(got) != 0 { + t.Errorf("got %v, want []", got) + } + }) +} +``` + +**Deteccion:** `^func (Test\w+)\s*\(` — cada `func Test...` es un test case. Los subtests (`t.Run`) se incluyen dentro del codigo del test padre. + +**Archivo:** `{domain}/{name}_test.go` (convencion Go estandar). + +### Python + +Convencion estandar de pytest. El parser detecta funciones `def test_xxx(`: + +```python +def test_funcion_llamada_una_vez(store): + calls = [] + + @cache_decorator(store, ttl=60) + def compute(x: int) -> int: + calls.append(x) + return x * 10 + + assert compute(5) == 50 + assert compute(5) == 50 + assert len(calls) == 1 + + +def test_ttl_expirado(store): + # ... +``` + +**Deteccion:** `^def (test_\w+)\s*\(` — cada funcion top-level `def test_...` es un test case. El codigo incluye todo hasta la siguiente `def test_` o fin de archivo. + +**Archivo:** `{domain}/{name}_test.py`. + +### Bash + +Bash no tiene framework estandar de testing. El parser soporta tres convenciones, en orden de prioridad: + +#### Opcion 1: funciones `test_xxx()` (preferida) + +La mas explicita y la que mejor se parsea: + +```bash +#!/usr/bin/env bash +source "$(dirname "$0")/mi_funcion.sh" + +PASS=0; FAIL=0 + +assert_eq() { + local name="$1" got="$2" want="$3" + if [ "$got" = "$want" ]; then echo " PASS: $name"; ((PASS++)) + else echo " FAIL: $name (got='$got', want='$want')"; ((FAIL++)); fi +} + +test_caso_basico() { + local got + got=$(mi_funcion "input") + assert_eq "caso basico" "$got" "expected" +} + +test_caso_vacio() { + local got + got=$(mi_funcion "") + assert_eq "input vacio" "$got" "" +} + +# Ejecutar todos los tests +test_caso_basico +test_caso_vacio + +echo "Resultados: $PASS passed, $FAIL failed" +[ "$FAIL" -eq 0 ] || exit 1 +``` + +**Deteccion:** `^(test_\w+)\s*\(\)\s*\{` — cada funcion `test_xxx() { ... }` es un test case. + +#### Opcion 2: secciones `=== nombre ===` + +Para tests que agrupan multiples asserts bajo secciones nombradas: + +```bash +#!/usr/bin/env bash +source "$(dirname "$0")/mi_funcion.sh" + +echo "=== caso basico ===" + +got=$(mi_funcion "input") +assert_eq "retorna expected" "$got" "expected" + +echo "=== caso edge ===" + +got=$(mi_funcion "") +assert_eq "input vacio" "$got" "" + +echo "=== errores ===" + +assert_fail "input invalido" mi_funcion "--bad" +``` + +**Deteccion:** `^(echo\s+["'])?===\s*(\w[\w\s]*\w)\s*===(["'])?\s*$` — cada linea con `=== nombre ===` (con o sin `echo`) abre una seccion. El nombre debe contener al menos dos caracteres alfanumericos (las lineas de separacion puras como `======` se ignoran). + +#### Opcion 3: comentarios `# Test:` + +Para scripts simples donde cada test se marca con un comentario: + +```bash +#!/usr/bin/env bash +source "$(dirname "$0")/mi_funcion.sh" + +# Test: caso basico +got=$(mi_funcion "input") +[ "$got" = "expected" ] || { echo "FAIL"; exit 1; } + +# Test: input vacio +got=$(mi_funcion "") +[ "$got" = "" ] || { echo "FAIL"; exit 1; } +``` + +**Deteccion:** `^#\s*[Tt]est:\s*(.+)` — cada comentario `# Test: nombre` abre un bloque. + +#### Recomendacion + +Usar **opcion 1** (funciones `test_xxx()`) para tests nuevos. Es la mas explicita, cada test esta aislado en su propia funcion, y se parsea sin ambiguedad. + +La **opcion 2** (secciones `===`) es aceptable cuando ya existe el patron en el archivo (como `pass_test.sh`). + +**Archivo:** `{domain}/{name}_test.sh`. + +--- + +## E2E tests + +Los e2e tests viven en `operations.db` de cada app. No se extraen automaticamente — se crean manualmente o por el bucle reactivo cuando se necesita verificar que un flujo end-to-end funciona. + +### Tabla `e2e_tests` + +| Campo | Tipo | Descripcion | +|---|---|---| +| `id` | string | Identificador unico | +| `name` | string | Nombre descriptivo del test | +| `description` | string | Que verifica este test | +| `relation_id` | string | FK a `relations.id` — que pipeline/relacion prueba | +| `steps` | []string | Funciones involucradas en orden | +| `input_fixture` | JSON | Datos de entrada para el test | +| `expected` | JSON | Resultado esperado | +| `last_status` | string | pass, fail, skip, o vacio | +| `last_run_at` | datetime | Ultima ejecucion | +| `execution_id` | string | Referencia a la ejecucion que lo corrio | +| `duration_ms` | int | Duracion en milisegundos | +| `created_at` | datetime | Fecha de creacion | +| `updated_at` | datetime | Ultima actualizacion | + +FTS5 disponible sobre `id`, `name`, `description`, `steps`. + +### Diferencia con assertions + +| | Assertions | E2E tests | +|---|---|---| +| **Que son** | Reglas declarativas sobre datos | Ejecuciones concretas de flujos | +| **Cuando corren** | En cada ejecucion del bucle reactivo | Bajo demanda o en CI | +| **Sobre que** | Una entity (`precio > 0`) | Un flujo completo (input → pipeline → output) | +| **Resultado** | pass/fail sobre el valor actual | pass/fail comparando output vs expected | + +### Ejemplo de uso + +```bash +# Crear un e2e test para un pipeline +fn ops e2e add --name "metabase_setup_completo" \ + --relation-id "rel_setup_metabase" \ + --steps '["docker_pull_image_go_infra","init_metabase_go_pipelines"]' \ + --input '{"project":"test"}' \ + --expected '{"status":"running"}' + +# Listar e2e tests +fn ops e2e list + +# Ver resultado +fn ops e2e show +```