// Unit tests para `graph_icons` (issue 0049f). // Cubre la parte CPU del builder — bake del atlas, layout en grid 16x16, // regiones 1-indexed, uv_table consistente, y comportamiento ante codepoints // inexistentes en la fuente. La subida a GPU se desactiva con la env // `FN_GRAPH_ICONS_SKIP_GL=1` (set en el setup) para que el test corra sin // contexto GL en CI. #define CATCH_CONFIG_MAIN #include "catch_amalgamated.hpp" #include "viz/graph_icons.h" #include #include namespace { // Tabler 3.41: codepoints que sabemos que existen en la TTF embebida en el // repo (ver cpp/functions/core/icons_tabler.h). const uint16_t k_known[] = { 0xEB4Du, // TI_USER 0xEAE5u, // TI_MAIL 0xEAB9u, // TI_GLOBE 0xEB09u, // TI_PHONE 0xEA4Fu, // TI_BUILDING 0xEA88u, // TI_DATABASE }; constexpr int k_known_count = sizeof(k_known) / sizeof(k_known[0]); bool any_alpha_in_region(const unsigned char* pixels, int W, float u0, float v0, float u1, float v1) { int x0 = (int)(u0 * W); int y0 = (int)(v0 * W); int x1 = (int)(u1 * W); int y1 = (int)(v1 * W); for (int y = y0; y < y1; ++y) { for (int x = x0; x < x1; ++x) { if (pixels[(y * W + x) * 4 + 3] > 0) return true; } } return false; } struct EnvSetup { EnvSetup() { setenv("FN_GRAPH_ICONS_SKIP_GL", "1", /*overwrite=*/1); } }; EnvSetup _env_setup; } // namespace TEST_CASE("graph_icons_build con 6 codepoints conocidos", "[viz][graph_icons]") { IconAtlas* a = graph_icons_build(k_known, k_known_count, 32); REQUIRE(a != nullptr); REQUIRE(graph_icons_count(a) == k_known_count); REQUIRE(graph_icons_width(a) == 512); REQUIRE(graph_icons_height(a) == 512); graph_icons_destroy(a); } TEST_CASE("regiones 1-indexed y orden conservado", "[viz][graph_icons]") { IconAtlas* a = graph_icons_build(k_known, k_known_count, 32); REQUIRE(a != nullptr); // icon_id = 0 -> nullptr (reservado para "sin icono") REQUIRE(graph_icons_region(a, 0) == nullptr); // icon_id = i+1 -> region con codepoint en posicion i del array for (int i = 0; i < k_known_count; ++i) { const IconRegion* r = graph_icons_region(a, (uint16_t)(i + 1)); REQUIRE(r != nullptr); REQUIRE(r->id == (uint16_t)(i + 1)); REQUIRE(r->codepoint == k_known[i]); REQUIRE(r->u0 < r->u1); REQUIRE(r->v0 < r->v1); REQUIRE(r->u0 >= 0.0f); REQUIRE(r->v1 <= 1.0f); } // icon_id fuera de rango -> nullptr REQUIRE(graph_icons_region(a, (uint16_t)(k_known_count + 1)) == nullptr); REQUIRE(graph_icons_region(a, 9999) == nullptr); graph_icons_destroy(a); } TEST_CASE("uv_table coincide con regions", "[viz][graph_icons]") { IconAtlas* a = graph_icons_build(k_known, k_known_count, 32); REQUIRE(a != nullptr); const float* uv = graph_icons_uv_table(a); REQUIRE(uv != nullptr); for (int i = 0; i < k_known_count; ++i) { const IconRegion* r = graph_icons_region(a, (uint16_t)(i + 1)); REQUIRE(uv[i * 4 + 0] == r->u0); REQUIRE(uv[i * 4 + 1] == r->v0); REQUIRE(uv[i * 4 + 2] == r->u1); REQUIRE(uv[i * 4 + 3] == r->v1); } graph_icons_destroy(a); } TEST_CASE("regiones tienen contenido (alpha != 0)", "[viz][graph_icons]") { // El test no se puede correr si el TTF no esta accesible — en ese caso // graph_icons_build devuelve nullptr y skipeamos. IconAtlas* a = graph_icons_build(k_known, k_known_count, 32); if (!a) { WARN("tabler-icons.ttf no encontrado — test de contenido skipped. " "Defina FN_CPP_ROOT o copie el TTF a ./assets/."); return; } const unsigned char* px = graph_icons_pixels(a); REQUIRE(px != nullptr); for (int i = 0; i < k_known_count; ++i) { const IconRegion* r = graph_icons_region(a, (uint16_t)(i + 1)); REQUIRE(any_alpha_in_region(px, graph_icons_width(a), r->u0, r->v0, r->u1, r->v1)); } graph_icons_destroy(a); } TEST_CASE("count fuera de rango devuelve nullptr", "[viz][graph_icons]") { REQUIRE(graph_icons_build(k_known, 0, 32) == nullptr); REQUIRE(graph_icons_build(k_known, -1, 32) == nullptr); REQUIRE(graph_icons_build(k_known, 257, 32) == nullptr); // max 256 REQUIRE(graph_icons_build(nullptr, k_known_count, 32) == nullptr); } TEST_CASE("layout en grid 16x16", "[viz][graph_icons]") { IconAtlas* a = graph_icons_build(k_known, k_known_count, 32); REQUIRE(a != nullptr); // Cada celda es 32px = 32/512 = 0.0625 en UV. Con 1px padding, las UVs // van de 1/512 a 31/512 dentro de la celda. const float cell_uv = 32.0f / 512.0f; for (int i = 0; i < k_known_count; ++i) { const IconRegion* r = graph_icons_region(a, (uint16_t)(i + 1)); int row = i / 16; int col = i % 16; // u0 esta dentro de [col*cell, (col+1)*cell] REQUIRE(r->u0 >= col * cell_uv); REQUIRE(r->u1 <= (col + 1) * cell_uv); REQUIRE(r->v0 >= row * cell_uv); REQUIRE(r->v1 <= (row + 1) * cell_uv); } graph_icons_destroy(a); }