// test_visual — golden-image diff de las demos de primitives_gallery. // // Asume que existen PNGs en `cpp/tests/golden/.png` generados por // `cpp/scripts/update_goldens.sh`. Si no existen, los SKIPea con INFO. // // El binario primitives_gallery se localiza relativo al directorio de build: // /apps/primitives_gallery/primitives_gallery --capture // // Si el entorno no puede crear contexto GL (WSL minimo, container sin Mesa), // el binario falla; el test reporta SKIP en lugar de FAIL para no bloquear // CI en entornos donde el render headless no es posible. #define CATCH_CONFIG_MAIN #include "catch_amalgamated.hpp" #include "png_diff.h" #include #include #include #include #include #include #include #include namespace fs = std::filesystem; // Defaults inyectados por CMake en el target test_visual: // FN_TEST_GOLDEN_DIR — cpp/tests/golden (absoluto) // FN_TEST_GALLERY_BIN — path absoluto al binario primitives_gallery // FN_TEST_TMP_DIR — directorio temporal donde la corrida actual // escribe sus PNGs (build/tests/visual_actual) #ifndef FN_TEST_GOLDEN_DIR #define FN_TEST_GOLDEN_DIR "cpp/tests/golden" #endif #ifndef FN_TEST_GALLERY_BIN #define FN_TEST_GALLERY_BIN "" #endif #ifndef FN_TEST_TMP_DIR #define FN_TEST_TMP_DIR "/tmp/primitives_gallery_visual" #endif #ifndef FN_TEST_REPO_ROOT #define FN_TEST_REPO_ROOT "." #endif static std::vector list_pngs(const std::string& dir) { std::vector out; if (!fs::exists(dir) || !fs::is_directory(dir)) return out; for (const auto& entry : fs::directory_iterator(dir)) { if (!entry.is_regular_file()) continue; const auto path = entry.path(); if (path.extension() == ".png") { out.push_back(path.stem().string()); } } return out; } static bool file_exists(const std::string& p) { struct stat st{}; return ::stat(p.c_str(), &st) == 0; } TEST_CASE("primitives_gallery visual goldens", "[visual]") { const std::string golden_dir = FN_TEST_GOLDEN_DIR; const std::string gallery_bin = FN_TEST_GALLERY_BIN; const std::string tmp_dir = FN_TEST_TMP_DIR; INFO("golden dir: " << golden_dir); INFO("gallery bin: " << gallery_bin); INFO("tmp dir: " << tmp_dir); auto goldens = list_pngs(golden_dir); if (goldens.empty()) { WARN("No goldens found in '" << golden_dir << "'. " "Run cpp/scripts/update_goldens.sh to generate them. " "Visual diff test SKIPPED."); SUCCEED("no goldens — skipped"); return; } if (gallery_bin.empty() || !file_exists(gallery_bin)) { WARN("primitives_gallery binary not found at '" << gallery_bin << "'. Build target primitives_gallery first. SKIPPED."); SUCCEED("gallery binary missing — skipped"); return; } // Crear tmp_dir y correr captura. std::error_code ec; fs::create_directories(tmp_dir, ec); // Ejecutar binario en modo --capture desde la raiz del repo. Algunas // demos resuelven paths relativos (p.ej. sql_workbench busca registry.db); // correr desde la raiz garantiza determinismo entre maquinas. std::string cmd = std::string("cd '") + FN_TEST_REPO_ROOT + "' && '" + gallery_bin + "' --capture '" + tmp_dir + "' 2>&1"; INFO("capture cmd: " << cmd); const int rc = std::system(cmd.c_str()); if (rc != 0) { WARN("primitives_gallery --capture exited with rc=" << rc << " (likely no GL context — WSL/headless). " << "Run with LIBGL_ALWAYS_SOFTWARE=1 or install Mesa. SKIPPED."); SUCCEED("capture failed — environment lacks GL — skipped"); return; } int matched = 0, missing_actual = 0, diffed = 0; for (const auto& demo_id : goldens) { const std::string g_path = golden_dir + "/" + demo_id + ".png"; const std::string a_path = tmp_dir + "/" + demo_id + ".png"; if (!file_exists(a_path)) { WARN("No actual capture for '" << demo_id << "' at " << a_path); ++missing_actual; continue; } auto r = fn_test::pixel_diff_ratio(g_path, a_path, /*channel_threshold=*/5); INFO("demo: " << demo_id << " diff_ratio=" << r.diff_ratio << " (" << r.pixels_different << "/" << r.pixels_total << ")"); // Tolerancia 1% por defecto. if (r.diff_ratio > 0.01) { ++diffed; FAIL_CHECK("Visual diff for '" << demo_id << "' exceeds 1%: " << (r.diff_ratio * 100.0) << "%. " << "Compare " << a_path << " vs " << g_path << " — if the change is intentional, run " << "cpp/scripts/update_goldens.sh."); } else { ++matched; } } INFO("visual goldens — matched=" << matched << " diffed=" << diffed << " missing_actual=" << missing_actual); REQUIRE(diffed == 0); }