111 / Sistemas

nyx test --coverage

nyx test --coverage (necesita llvm-profdata, la misma versión mayor que clang) imprime, después del resumen de la corrida, qué funciones de src/ ninguna prueba llamó — algo que un runner que solo dice «pasó/falló» por archivo no puede contestar. Esta receta demuestra la garantía del LENGUAJE en la que se apoya ese informe: dos funciones homónimas en módulos distintos no se mezclan al medir. El flag en sí es de la herramienta, no algo invocable desde un programa Nyx.

111-nyx-test-coverage.nxFuente →
// Nyx by Example: qué función de src/ ninguna prueba llamó (`nyx test --coverage`).
//
// Un equipo con decenas de archivos de prueba no tenía forma de saber si una
// función quedó sin ejercitar — el runner solo dice "pasó/falló" por archivo,
// nunca "esto de src/ nadie lo tocó" (fricción real de un ERP en Nyx). Este
// archivo demuestra el LENGUAJE (dos funciones `valor` homónimas en módulos
// distintos no se mezclan al medir); la HERRAMIENTA (`nyx test --coverage`)
// es un flag del runner de pruebas, no algo invocable desde un programa, así
// que no se puede correr dentro de esta receta — se documenta con la salida
// REAL de su fixture (`tests/tooling/coverage/`, run_coverage_tests.sh).
//
// Un proyecto real así:
//   src/calc.nx:  pub fn sumar(a, b)        <- la llama un test
//                 pub fn no_llamada(n)      <- ninguna prueba la llama
//                 pub fn valor()            <- ninguna prueba la llama
//                 pub fn correr_en_fibra()  <- llama a en_fibra() DENTRO de un spawn
//   src/aux.nx:   pub fn valor()            <- homónima de calc.valor; SÍ se llama
//   src/otro.nx:  pub fn nadie()            <- ningún archivo de tests/ lo importa
//   tests/calc_test.nx:  importa calc, llama sumar() y correr_en_fibra()
//   tests/calc2_test.nx: importa calc Y aux, llama solo aux.valor()
//
// `nyx test --coverage` (necesita llvm-profdata, misma versión mayor que
// clang) imprime, tras el resumen de la corrida, exactamente esto — captura
// real de `run_coverage_tests.sh`, caso "informe" (docs/TESTS.md, fila
// "Cobertura de nyx test"):
//
//   == Cobertura de funciones (src/) ==
//     src/aux.nx: 1/1 llamadas
//     src/calc.nx: 3/5 llamadas
//       src/calc.nx:9 no_llamada
//       src/calc.nx:27 valor
//     src/otro.nx: 0/1 llamadas (ninguna prueba lo importa)
//       src/otro.nx:2 nadie
//     Total: 4/7 funciones llamadas
//
// Notar lo que NO aparece: `sumar` (la llama un test), `correr_en_fibra` (la
// llama un test) NI `en_fibra` (corre solo dentro del `spawn` de
// correr_en_fibra, pero cuenta como llamada), y `aux.valor` (SÍ se llama,
// aunque comparta nombre con `calc.valor`, que no). `main`, los `__test_N`
// sintéticos y el prelude tampoco cuentan — la tabla es solo funciones de
// src/ atribuibles a código de usuario.
//
// Límites (no son bugs, son el alcance de esta primera fase):
//   - Solo funciones, no líneas ni ramas: un `if` que nunca tomó un lado
//     sigue contando la función entera como llamada.
//   - Corrió/no corrió, sin conteo de veces.
//   - Solo target nativo (`--coverage --target wasm32-wasi` es error).
//   - Una prueba que se cuelga (timeout), revienta o no compila NO deja
//     perfil: sale aparte, bajo "Sin perfil", y las funciones que SOLO esa
//     prueba importaba quedan "sin datos" — nunca como "no llamadas". Sin
//     esto, matar un test con --timeout falsearía el informe.
//   - `--coverage=lcov` además escribe `target/coverage.lcov`
//     (SF/FN/FNDA/FNF/FNH) para un visor externo.
//   - Cuesta tiempo: +13,6% en una suite real de 93 archivos de prueba
//     (105m23s -> 119m41s, medido el 2026-09-17 en 2 núcleos). Ahí cada
//     archivo enlaza por separado y el enlace domina ese sobrecosto, así
//     que una suite de un solo enlace pagará una fracción mayor.
//
// La parte que SÍ se ejecuta en esta receta: las dos `valor` homónimas de
// arriba, para probar que llamar a la de un módulo nunca llama a la del otro
// —la garantía de la que depende todo el informe de cobertura por módulo.

fn calc_valor() -> int {
    return 1
}

fn aux_valor() -> int {
    return 2
}

fn main() {
    let de_calc: int = calc_valor()
    let de_aux: int = aux_valor()
    assert(de_calc == 1 and de_aux == 2, "las homónimas no se mezclan")
    print("calc.valor() = " + int_to_string(de_calc) + ", aux.valor() = " + int_to_string(de_aux))
    exit(0)
}
Salidastdout
calc.valor() = 1, aux.valor() = 2

Cómo funciona

calc_valor y aux_valor juegan el papel de dos funciones valor() homónimas en módulos distintos (src/calc.nx y src/aux.nx en un proyecto real). El assert confirma que llamar a la de un módulo nunca llama a la del otro — la garantía de la que depende todo el informe de cobertura por módulo.

Contra un proyecto real de ejemplo, nyx test --coverage imprime algo así después del resumen: src/calc.nx: 3/5 llamadas con la lista de líneas sin llamar, src/otro.nx: 0/1 llamadas (ninguna prueba lo importa), y un total. Cuenta solo funciones, no líneas ni ramas; una función que corre dentro de un spawn igual cuenta como llamada, y un archivo de prueba que revienta o hace timeout deja sus funciones como «sin datos», nunca como «no llamadas» — matar un test con --timeout no puede falsear el informe.

Medido en una suite real de 93 archivos: +13,6 % de tiempo (105m23s → 119m41s). --coverage=lcov además escribe target/coverage.lcov para un visor externo.