516db8efc0
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
228 lines
9.0 KiB
Markdown
228 lines
9.0 KiB
Markdown
# /e2e-cpp — Crear/ejecutar tests e2e para apps C++
|
|
|
|
Genera y corre tests e2e con **Dear ImGui Test Engine** sobre las apps C++ del registry. Cada app gana un ejecutable `<app>_tests` que reabre la app dentro de un harness de testing y ejecuta scripts de UI (clicks, escritura, asserts) sobre los componentes ImGui.
|
|
|
|
Suite ya instalada en `cpp/vendor/imgui_test_engine/`. Integracion en framework: `fn::run_app_test()` (ver `cpp/framework/app_base.h`). Opt-in via `-DFN_BUILD_TESTS=ON`. Sin la opcion los builds normales de `/compile` no cambian.
|
|
|
|
## Argumento
|
|
|
|
`$ARGUMENTS` — formato libre. Casos:
|
|
|
|
- `<app_name>` — solo el nombre. Si la app ya tiene tests, los ejecuta. Si no, pide al usuario que describa el flujo a testear.
|
|
- `<app_name> <descripcion del flujo>` — genera un test nuevo para ese flujo y lo ejecuta. Ej: `chart_demo abrir cada tab y verificar que renderiza`.
|
|
- vacio — detectar app desde `pwd` (si estas en `cpp/apps/<X>/` o `projects/*/apps/<X>/`); si no, listar apps disponibles.
|
|
|
|
## Pasos
|
|
|
|
### 1. Resolver app y directorio
|
|
|
|
```bash
|
|
ROOT=$HOME/fn_registry
|
|
ARGS="$ARGUMENTS"
|
|
APP_ARG="${ARGS%% *}" # primera palabra
|
|
FLOW_DESC="${ARGS#* }" # resto (puede coincidir con APP_ARG si solo hay una palabra)
|
|
[ "$FLOW_DESC" = "$APP_ARG" ] && FLOW_DESC=""
|
|
|
|
# Detectar desde CWD si no hay arg
|
|
if [ -z "$APP_ARG" ]; then
|
|
CWD="$(pwd)"
|
|
case "$CWD" in
|
|
"$ROOT"/cpp/apps/*|"$ROOT"/projects/*/apps/*)
|
|
APP_ARG="$(basename "$CWD")" ;;
|
|
esac
|
|
fi
|
|
|
|
if [ -z "$APP_ARG" ]; then
|
|
echo "Apps C++ disponibles:"
|
|
ls "$ROOT"/cpp/apps/ 2>/dev/null
|
|
ls "$ROOT"/projects/*/apps/ 2>/dev/null
|
|
echo "Uso: /e2e-cpp <app> [descripcion del flujo]"
|
|
exit 1
|
|
fi
|
|
|
|
APP_DIR=""
|
|
for cand in "$ROOT/cpp/apps/$APP_ARG" "$ROOT"/projects/*/apps/"$APP_ARG"; do
|
|
[ -d "$cand" ] && [ -f "$cand/CMakeLists.txt" ] && APP_DIR="$cand" && break
|
|
done
|
|
[ -z "$APP_DIR" ] && { echo "App C++ no encontrada: $APP_ARG"; exit 1; }
|
|
echo "App: $APP_ARG"
|
|
echo "Dir: $APP_DIR"
|
|
```
|
|
|
|
### 2. Inspeccionar la app
|
|
|
|
Lee:
|
|
- `$APP_DIR/main.cpp` — identifica:
|
|
- El nombre de la funcion principal de render (suele ser `render()` o `static void render()`).
|
|
- El **window title** que aparece en `ImGui::Begin("...")` — sera el primer arg de `ctx->SetRef("...")` en los tests. Si tiene em-dash u otros UTF-8 no ASCII, anotar la secuencia de bytes (ej: `\xe2\x80\x94` para `—`).
|
|
- Los IDs/labels de los widgets candidatos: tabs (`BeginTabItem`), botones (`Button`), inputs (`InputText`), checkboxes, etc.
|
|
- `$APP_DIR/app.md` — para entender el dominio y proposito.
|
|
- `$APP_DIR/CMakeLists.txt` — para saber que `.cpp` del registry enlaza la app (los tests linkearan los mismos).
|
|
|
|
### 3. Decidir tests a escribir
|
|
|
|
**Si `$FLOW_DESC` esta vacio**: pregunta al usuario que flujo testear. Sugiere 2-3 candidatos basados en los widgets vistos en main.cpp. NO inventes flujos sin confirmacion.
|
|
|
|
**Si `$FLOW_DESC` viene en el comando**: convierte la descripcion en una secuencia de pasos atomicos del Test Context API. Ejemplos canonicos:
|
|
|
|
| Descripcion humano | Llamada Test Engine |
|
|
|---|---|
|
|
| "abrir tab X" | `ctx->ItemClick("##tabs/X")` o el path real del TabBar |
|
|
| "escribir 'hola' en el input search" | `ctx->ItemInput("Search", "hola")` |
|
|
| "click boton Aceptar" | `ctx->ItemClick("Aceptar")` |
|
|
| "verificar que aparece el modal Y" | `IM_CHECK(ctx->WindowInfo("Y").ID != 0)` |
|
|
| "checkbox Z marcado" | `IM_CHECK(ctx->ItemIsChecked("Z"))` |
|
|
| "menu File > Open" | `ctx->MenuClick("File/Open")` |
|
|
|
|
Ver `cpp/vendor/imgui_test_engine/imgui_te_context.h` para el catalogo completo de helpers.
|
|
|
|
### 4. Preparar la app para tests (idempotente)
|
|
|
|
Si es la primera vez que la app gana tests, hay que:
|
|
|
|
**a) Hacer la funcion render() linkable desde otra TU**
|
|
|
|
```cpp
|
|
// Antes: static void render() { ... }
|
|
// Despues: void render() { ... }
|
|
```
|
|
|
|
**b) Excluir `int main()` con guarda `FN_TEST_BUILD`**
|
|
|
|
```cpp
|
|
#ifndef FN_TEST_BUILD
|
|
int main() {
|
|
return fn::run_app({...}, render);
|
|
}
|
|
#endif
|
|
```
|
|
|
|
Verifica con `grep -n "FN_TEST_BUILD\|^static void render" "$APP_DIR/main.cpp"`. Si ya esta, no toques nada.
|
|
|
|
### 5. Generar/extender el archivo de tests
|
|
|
|
`$APP_DIR/tests/<app>_tests.cpp` — un solo archivo por app, varias `IM_REGISTER_TEST` dentro de `register_tests()`.
|
|
|
|
**Plantilla**:
|
|
|
|
```cpp
|
|
// E2E tests para <app> — Dear ImGui Test Engine.
|
|
// Construido solo con -DFN_BUILD_TESTS=ON. Reusa el mismo main.cpp con
|
|
// FN_TEST_BUILD definido para excluir su int main().
|
|
|
|
#include "app_base.h"
|
|
#include "imgui.h"
|
|
#include "imgui_te_engine.h"
|
|
#include "imgui_te_context.h"
|
|
|
|
void render(); // definido en <app>/main.cpp
|
|
|
|
static void register_tests(ImGuiTestEngine* e) {
|
|
ImGuiTest* t = nullptr;
|
|
|
|
t = IM_REGISTER_TEST(e, "<app>", "<test_name>");
|
|
t->TestFunc = [](ImGuiTestContext* ctx) {
|
|
ctx->SetRef("<window_title_exacto>");
|
|
// ... pasos del flujo
|
|
};
|
|
|
|
// mas tests aqui
|
|
}
|
|
|
|
int main() {
|
|
fn::AppConfig cfg{};
|
|
cfg.title = "<app>_tests";
|
|
cfg.width = 1280;
|
|
cfg.height = 800;
|
|
return fn::run_app_test(cfg, render, register_tests);
|
|
}
|
|
```
|
|
|
|
Si el archivo ya existe: **AGREGA** un nuevo `IM_REGISTER_TEST` dentro de la funcion `register_tests` existente. NO sobreescribas tests previos.
|
|
|
|
### 6. Actualizar CMakeLists.txt (idempotente)
|
|
|
|
Si `$APP_DIR/CMakeLists.txt` no tiene aun el bloque de tests, agregar al final:
|
|
|
|
```cmake
|
|
# --- E2E tests (opt-in via -DFN_BUILD_TESTS=ON) ---
|
|
if(FN_BUILD_TESTS)
|
|
add_imgui_app(<app>_tests
|
|
main.cpp
|
|
tests/<app>_tests.cpp
|
|
# mismos .cpp del registry que la app principal
|
|
${CMAKE_SOURCE_DIR}/functions/<dom>/<func>.cpp
|
|
...
|
|
)
|
|
target_compile_definitions(<app>_tests PRIVATE FN_TEST_BUILD)
|
|
endif()
|
|
```
|
|
|
|
Las fuentes deben replicar las del target principal (mismas funciones del registry). Si la app ya tiene un bloque `if(FN_BUILD_TESTS)`, no lo dupliques.
|
|
|
|
### 7. Build
|
|
|
|
```bash
|
|
cd "$ROOT/cpp"
|
|
cmake -S . -B build/linux_tests -DFN_BUILD_TESTS=ON 2>&1 | tail -5
|
|
cmake --build build/linux_tests --target ${APP_ARG}_tests -j4 2>&1 | tail -20
|
|
```
|
|
|
|
Si el build falla:
|
|
- Errores de compilacion en `tests/...cpp` → revisa nombres de widgets/paths con el codigo real de main.cpp.
|
|
- "undefined reference to render" → falta quitar `static` o falta el `#ifndef FN_TEST_BUILD` en main.cpp.
|
|
- "multiple definition of main" → falta el `target_compile_definitions(... FN_TEST_BUILD)` en CMakeLists.
|
|
|
|
### 8. Ejecutar (headless preferente — sin parpadeo)
|
|
|
|
`fn::run_app_test` crea la ventana GLFW **oculta por defecto** (`GLFW_VISIBLE=FALSE`, ver `cpp/framework/app_base.cpp`). El contexto GL real se crea igual, así que el render que ejercita el Test Engine es fiel, pero la ventana nunca se mapea en pantalla: cero parpadeo, no roba foco. Por eso los tests de frontend C++ corren headless por defecto, sin tocar el código de cada app.
|
|
|
|
Dos formas de lanzar, según el entorno:
|
|
|
|
```bash
|
|
cd "$ROOT/cpp/build/linux_tests"
|
|
TEST_BIN="$(find . -name "${APP_ARG}_tests" -type f -executable | head -1)"
|
|
[ -z "$TEST_BIN" ] && { echo "no encuentro el binario de tests"; exit 1; }
|
|
|
|
if [ -n "$DISPLAY" ] && command -v glxinfo >/dev/null 2>&1 \
|
|
&& glxinfo 2>/dev/null | grep -q "OpenGL core profile version"; then
|
|
# Host con GL nativo (PC enmanuel, X11 + GPU): binario directo.
|
|
# La ventana ya nace oculta -> sin parpadeo, y usa la GPU real (rapido).
|
|
timeout 90 "$TEST_BIN" 2>&1
|
|
else
|
|
# CI / WSL sin GLX 4.3 nativo: display virtual en RAM + software Mesa.
|
|
timeout 90 xvfb-run -a -s "-screen 0 1280x800x24" \
|
|
env LIBGL_ALWAYS_SOFTWARE=1 GALLIUM_DRIVER=llvmpipe \
|
|
"$TEST_BIN" 2>&1
|
|
fi
|
|
EXIT=$?
|
|
echo "EXIT: $EXIT"
|
|
```
|
|
|
|
Ambas vías son headless. `xvfb-run` sigue siendo seguro en host con display (corre en su propio display virtual), así que si el sniff de GL falla puedes usar siempre la rama xvfb.
|
|
|
|
**Para depurar un test a ojo** (ver la UI mientras el engine la maneja), desactiva el headless con `FN_HEADLESS=0`:
|
|
|
|
```bash
|
|
FN_HEADLESS=0 timeout 90 "$TEST_BIN" 2>&1
|
|
```
|
|
|
|
### 9. Reportar
|
|
|
|
- Si `EXIT == 0` y la salida contiene `Tests Result: OK` → reporta `N/M tests passed` con la lista de tests ejecutados.
|
|
- Si `EXIT != 0` → muestra el bloque de log del test fallido (test engine imprime el path del widget que no encontro, el archivo y la linea del IM_CHECK que fallo). Sugiere correcciones (widget renombrado, path mal escrito, race entre frames — usar `ctx->Yield()`).
|
|
|
|
### 10. Despues de añadir tests
|
|
|
|
NO ejecutes `fn index` automaticamente — los tests no son funciones del registry, son artefactos de la app. Si el usuario los queria persistir, ya los tiene en `<app_dir>/tests/`.
|
|
|
|
Si la app es un sub-repo (lo normal segun ADR 0002), recordar al usuario que los archivos nuevos viven dentro del repo de la app y necesitan un commit alli (no en `fn_registry`).
|
|
|
|
## Referencias
|
|
|
|
- API de Test Context: `cpp/vendor/imgui_test_engine/imgui_te_context.h`
|
|
- API del engine: `cpp/vendor/imgui_test_engine/imgui_te_engine.h`
|
|
- Implementacion del harness: `cpp/framework/app_base.cpp` (funcion `fn::run_app_test`)
|
|
- Ejemplo canonico: `cpp/apps/chart_demo/tests/chart_demo_tests.cpp`
|
|
- Licencia del test engine: personal/open-source gratis (`cpp/vendor/imgui_test_engine/LICENSE.txt`)
|