diff --git a/cpp/tests/CMakeLists.txt b/cpp/tests/CMakeLists.txt index 2040e966..df0bb5ac 100644 --- a/cpp/tests/CMakeLists.txt +++ b/cpp/tests/CMakeLists.txt @@ -59,3 +59,22 @@ add_fn_test(test_dashboard_grid test_dashboard_grid.cpp) add_fn_test(test_sparkline test_sparkline.cpp) add_fn_test(test_table_view test_table_view.cpp) add_fn_test(test_icon_button test_icon_button.cpp) + +# --- Visual golden-image diff (issue 0048) --------------------------------- +# El binario primitives_gallery se compila con --capture; el test compara los +# PNGs generados con los goldens en cpp/tests/golden/. Si no hay goldens o el +# entorno no tiene GL, el test SKIPea. +add_fn_test(test_visual + test_visual.cpp + png_diff.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../vendor/stb/stb_image_impl.cpp) +target_include_directories(test_visual PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/../vendor/stb) +target_compile_definitions(test_visual PRIVATE + "FN_TEST_GOLDEN_DIR=\"${CMAKE_CURRENT_SOURCE_DIR}/golden\"" + "FN_TEST_GALLERY_BIN=\"$\"" + "FN_TEST_TMP_DIR=\"${CMAKE_BINARY_DIR}/tests/visual_actual\"" + # CMAKE_SOURCE_DIR aqui es cpp/, queremos la raiz del repo (un nivel arriba). + "FN_TEST_REPO_ROOT=\"${CMAKE_SOURCE_DIR}/..\"") +# Asegura que primitives_gallery existe antes de correr el test. +add_dependencies(test_visual primitives_gallery) diff --git a/cpp/tests/golden/.gitkeep b/cpp/tests/golden/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/cpp/tests/golden/badge.png b/cpp/tests/golden/badge.png new file mode 100644 index 00000000..ef977c07 Binary files /dev/null and b/cpp/tests/golden/badge.png differ diff --git a/cpp/tests/golden/bar_chart.png b/cpp/tests/golden/bar_chart.png new file mode 100644 index 00000000..a75d321d Binary files /dev/null and b/cpp/tests/golden/bar_chart.png differ diff --git a/cpp/tests/golden/bezier_editor.png b/cpp/tests/golden/bezier_editor.png new file mode 100644 index 00000000..c8311222 Binary files /dev/null and b/cpp/tests/golden/bezier_editor.png differ diff --git a/cpp/tests/golden/button.png b/cpp/tests/golden/button.png new file mode 100644 index 00000000..7e3c7682 Binary files /dev/null and b/cpp/tests/golden/button.png differ diff --git a/cpp/tests/golden/candlestick.png b/cpp/tests/golden/candlestick.png new file mode 100644 index 00000000..5b7c5827 Binary files /dev/null and b/cpp/tests/golden/candlestick.png differ diff --git a/cpp/tests/golden/chord.png b/cpp/tests/golden/chord.png new file mode 100644 index 00000000..1a49d98b Binary files /dev/null and b/cpp/tests/golden/chord.png differ diff --git a/cpp/tests/golden/contour.png b/cpp/tests/golden/contour.png new file mode 100644 index 00000000..86e47461 Binary files /dev/null and b/cpp/tests/golden/contour.png differ diff --git a/cpp/tests/golden/dashboard_panel.png b/cpp/tests/golden/dashboard_panel.png new file mode 100644 index 00000000..778a6edf Binary files /dev/null and b/cpp/tests/golden/dashboard_panel.png differ diff --git a/cpp/tests/golden/empty_state.png b/cpp/tests/golden/empty_state.png new file mode 100644 index 00000000..73b25edc Binary files /dev/null and b/cpp/tests/golden/empty_state.png differ diff --git a/cpp/tests/golden/file_watcher.png b/cpp/tests/golden/file_watcher.png new file mode 100644 index 00000000..b98486c5 Binary files /dev/null and b/cpp/tests/golden/file_watcher.png differ diff --git a/cpp/tests/golden/gauge.png b/cpp/tests/golden/gauge.png new file mode 100644 index 00000000..5fc5b432 Binary files /dev/null and b/cpp/tests/golden/gauge.png differ diff --git a/cpp/tests/golden/gl_texture.png b/cpp/tests/golden/gl_texture.png new file mode 100644 index 00000000..1ce01b80 Binary files /dev/null and b/cpp/tests/golden/gl_texture.png differ diff --git a/cpp/tests/golden/graph_viewport.png b/cpp/tests/golden/graph_viewport.png new file mode 100644 index 00000000..fa8161a2 Binary files /dev/null and b/cpp/tests/golden/graph_viewport.png differ diff --git a/cpp/tests/golden/heatmap.png b/cpp/tests/golden/heatmap.png new file mode 100644 index 00000000..55a663f3 Binary files /dev/null and b/cpp/tests/golden/heatmap.png differ diff --git a/cpp/tests/golden/histogram.png b/cpp/tests/golden/histogram.png new file mode 100644 index 00000000..0454f6cd Binary files /dev/null and b/cpp/tests/golden/histogram.png differ diff --git a/cpp/tests/golden/icon_button.png b/cpp/tests/golden/icon_button.png new file mode 100644 index 00000000..a39cb664 Binary files /dev/null and b/cpp/tests/golden/icon_button.png differ diff --git a/cpp/tests/golden/kpi_card.png b/cpp/tests/golden/kpi_card.png new file mode 100644 index 00000000..6f6540a6 Binary files /dev/null and b/cpp/tests/golden/kpi_card.png differ diff --git a/cpp/tests/golden/line_plot.png b/cpp/tests/golden/line_plot.png new file mode 100644 index 00000000..b7571ec6 Binary files /dev/null and b/cpp/tests/golden/line_plot.png differ diff --git a/cpp/tests/golden/mesh_viewer.png b/cpp/tests/golden/mesh_viewer.png new file mode 100644 index 00000000..7bb7c0d5 Binary files /dev/null and b/cpp/tests/golden/mesh_viewer.png differ diff --git a/cpp/tests/golden/modal_dialog.png b/cpp/tests/golden/modal_dialog.png new file mode 100644 index 00000000..64459765 Binary files /dev/null and b/cpp/tests/golden/modal_dialog.png differ diff --git a/cpp/tests/golden/page_header.png b/cpp/tests/golden/page_header.png new file mode 100644 index 00000000..fa07c0da Binary files /dev/null and b/cpp/tests/golden/page_header.png differ diff --git a/cpp/tests/golden/pie_chart.png b/cpp/tests/golden/pie_chart.png new file mode 100644 index 00000000..c2b606c5 Binary files /dev/null and b/cpp/tests/golden/pie_chart.png differ diff --git a/cpp/tests/golden/process_runner.png b/cpp/tests/golden/process_runner.png new file mode 100644 index 00000000..e3a031c5 Binary files /dev/null and b/cpp/tests/golden/process_runner.png differ diff --git a/cpp/tests/golden/sankey.png b/cpp/tests/golden/sankey.png new file mode 100644 index 00000000..8374aefb Binary files /dev/null and b/cpp/tests/golden/sankey.png differ diff --git a/cpp/tests/golden/scatter_3d.png b/cpp/tests/golden/scatter_3d.png new file mode 100644 index 00000000..2e8ba850 Binary files /dev/null and b/cpp/tests/golden/scatter_3d.png differ diff --git a/cpp/tests/golden/scatter_plot.png b/cpp/tests/golden/scatter_plot.png new file mode 100644 index 00000000..0a6711c0 Binary files /dev/null and b/cpp/tests/golden/scatter_plot.png differ diff --git a/cpp/tests/golden/select.png b/cpp/tests/golden/select.png new file mode 100644 index 00000000..89700530 Binary files /dev/null and b/cpp/tests/golden/select.png differ diff --git a/cpp/tests/golden/shader_canvas.png b/cpp/tests/golden/shader_canvas.png new file mode 100644 index 00000000..8a4bb4f9 Binary files /dev/null and b/cpp/tests/golden/shader_canvas.png differ diff --git a/cpp/tests/golden/sparkline.png b/cpp/tests/golden/sparkline.png new file mode 100644 index 00000000..f0f5628c Binary files /dev/null and b/cpp/tests/golden/sparkline.png differ diff --git a/cpp/tests/golden/sql_workbench.png b/cpp/tests/golden/sql_workbench.png new file mode 100644 index 00000000..5b7c208a Binary files /dev/null and b/cpp/tests/golden/sql_workbench.png differ diff --git a/cpp/tests/golden/surface_plot_3d.png b/cpp/tests/golden/surface_plot_3d.png new file mode 100644 index 00000000..bce73476 Binary files /dev/null and b/cpp/tests/golden/surface_plot_3d.png differ diff --git a/cpp/tests/golden/table_view.png b/cpp/tests/golden/table_view.png new file mode 100644 index 00000000..6a57945c Binary files /dev/null and b/cpp/tests/golden/table_view.png differ diff --git a/cpp/tests/golden/text_editor.png b/cpp/tests/golden/text_editor.png new file mode 100644 index 00000000..067f8600 Binary files /dev/null and b/cpp/tests/golden/text_editor.png differ diff --git a/cpp/tests/golden/text_input.png b/cpp/tests/golden/text_input.png new file mode 100644 index 00000000..46fd57a6 Binary files /dev/null and b/cpp/tests/golden/text_input.png differ diff --git a/cpp/tests/golden/timeline.png b/cpp/tests/golden/timeline.png new file mode 100644 index 00000000..b955bd20 Binary files /dev/null and b/cpp/tests/golden/timeline.png differ diff --git a/cpp/tests/golden/toast.png b/cpp/tests/golden/toast.png new file mode 100644 index 00000000..8b92aa2b Binary files /dev/null and b/cpp/tests/golden/toast.png differ diff --git a/cpp/tests/golden/toolbar.png b/cpp/tests/golden/toolbar.png new file mode 100644 index 00000000..85cf3eef Binary files /dev/null and b/cpp/tests/golden/toolbar.png differ diff --git a/cpp/tests/golden/tree_view.png b/cpp/tests/golden/tree_view.png new file mode 100644 index 00000000..119b90f2 Binary files /dev/null and b/cpp/tests/golden/tree_view.png differ diff --git a/cpp/tests/golden/treemap.png b/cpp/tests/golden/treemap.png new file mode 100644 index 00000000..b995a959 Binary files /dev/null and b/cpp/tests/golden/treemap.png differ diff --git a/cpp/tests/golden/tween.png b/cpp/tests/golden/tween.png new file mode 100644 index 00000000..9ffed7a9 Binary files /dev/null and b/cpp/tests/golden/tween.png differ diff --git a/cpp/tests/golden/voronoi.png b/cpp/tests/golden/voronoi.png new file mode 100644 index 00000000..0252e6d1 Binary files /dev/null and b/cpp/tests/golden/voronoi.png differ diff --git a/cpp/tests/png_diff.cpp b/cpp/tests/png_diff.cpp new file mode 100644 index 00000000..6deb1ed4 --- /dev/null +++ b/cpp/tests/png_diff.cpp @@ -0,0 +1,64 @@ +#include "png_diff.h" + +#include "stb_image.h" + +#include + +namespace fn_test { + +PngDiffResult pixel_diff_ratio(const std::string& path_a, + const std::string& path_b, + int channel_threshold) { + PngDiffResult r; + int wa = 0, ha = 0, ca = 0; + int wb = 0, hb = 0, cb = 0; + unsigned char* a = stbi_load(path_a.c_str(), &wa, &ha, &ca, 4); + unsigned char* b = stbi_load(path_b.c_str(), &wb, &hb, &cb, 4); + r.loaded_a = (a != nullptr); + r.loaded_b = (b != nullptr); + r.width_a = wa; r.height_a = ha; + r.width_b = wb; r.height_b = hb; + + if (!r.loaded_a || !r.loaded_b) { + if (a) stbi_image_free(a); + if (b) stbi_image_free(b); + return r; + } + + if (wa != wb || ha != hb) { + // Dimensiones no coinciden — diff total. + const long area_a = (long)wa * ha; + const long area_b = (long)wb * hb; + r.pixels_total = area_a > area_b ? area_a : area_b; + r.pixels_different = r.pixels_total; + r.diff_ratio = 1.0; + stbi_image_free(a); + stbi_image_free(b); + return r; + } + + const long area = (long)wa * ha; + long diffs = 0; + for (long i = 0; i < area; ++i) { + const unsigned char* pa = a + i * 4; + const unsigned char* pb = b + i * 4; + const int dr = std::abs((int)pa[0] - (int)pb[0]); + const int dg = std::abs((int)pa[1] - (int)pb[1]); + const int db_ = std::abs((int)pa[2] - (int)pb[2]); + const int da = std::abs((int)pa[3] - (int)pb[3]); + if (dr > channel_threshold || dg > channel_threshold || + db_ > channel_threshold || da > channel_threshold) { + ++diffs; + } + } + + r.pixels_total = area; + r.pixels_different = diffs; + r.diff_ratio = (area > 0) ? (double)diffs / (double)area : 0.0; + + stbi_image_free(a); + stbi_image_free(b); + return r; +} + +} // namespace fn_test diff --git a/cpp/tests/png_diff.h b/cpp/tests/png_diff.h new file mode 100644 index 00000000..aedd01f4 --- /dev/null +++ b/cpp/tests/png_diff.h @@ -0,0 +1,29 @@ +#pragma once +// Helper minimo para comparar dos imagenes PNG con tolerancia. +// Usa stb_image (vendored en cpp/vendor/stb). + +#include + +namespace fn_test { + +struct PngDiffResult { + bool loaded_a = false; + bool loaded_b = false; + int width_a = 0, height_a = 0; + int width_b = 0, height_b = 0; + long pixels_total = 0; + long pixels_different = 0; + double diff_ratio = 0.0; // pixels_different / pixels_total +}; + +// Carga ambos PNG, compara pixel a pixel con `channel_threshold` (0..255) por +// canal RGBA. Si la diferencia de cualquier canal supera el umbral, el pixel +// se marca como distinto. `diff_ratio` = pixels distintos / total. +// +// Si las dimensiones no coinciden devuelve loaded_*=true pero diff_ratio=1.0 +// y pixels_total = max(area_a, area_b). +PngDiffResult pixel_diff_ratio(const std::string& path_a, + const std::string& path_b, + int channel_threshold = 5); + +} // namespace fn_test diff --git a/cpp/tests/test_visual.cpp b/cpp/tests/test_visual.cpp new file mode 100644 index 00000000..78be40a6 --- /dev/null +++ b/cpp/tests/test_visual.cpp @@ -0,0 +1,139 @@ +// 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); +}