// Unit tests for graph_layouts (issue 0049i). // Cubre los seis layouts estaticos: grid, circular, random, radial, // hierarchical, fixed. Verifica: // - Centrado y geometria basica de los layouts geometricos. // - NF_PINNED se respeta (nodos pinned no se mueven). // - radial coloca el root en (0,0) y los hops en anillos correctos. // - hierarchical asigna niveles por longest-path. // - fixed es no-op. #define CATCH_CONFIG_MAIN #include "catch_amalgamated.hpp" #include "viz/graph_layouts.h" #include "viz/graph_types.h" #include #include namespace { // Helper: construir un GraphData a partir de vectores struct TestGraph { std::vector nodes; std::vector edges; GraphData data{}; void make(int N) { nodes.clear(); edges.clear(); for (int i = 0; i < N; ++i) { nodes.push_back(graph_node(0.0f, 0.0f)); } sync(); } void add_edge(uint32_t s, uint32_t t) { edges.push_back(graph_edge(s, t)); sync(); } void sync() { data.nodes = nodes.data(); data.node_count = (int)nodes.size(); data.node_capacity = (int)nodes.capacity(); data.edges = edges.data(); data.edge_count = (int)edges.size(); data.edge_capacity = (int)edges.capacity(); data.types = nullptr; data.type_count = 0; data.rel_types = nullptr; data.rel_type_count = 0; } }; } // namespace TEST_CASE("layout_grid respects pin and centers", "[viz][layouts][grid]") { TestGraph g; g.make(9); // Pin nodo 0 con posicion deliberadamente fuera de la cuadricula. g.nodes[0].x = 999.0f; g.nodes[0].y = 999.0f; g.nodes[0].flags |= NF_PINNED; g.sync(); graph::layout_grid(g.data, 10.0f); // Pinned no se ha movido REQUIRE(g.nodes[0].x == Catch::Approx(999.0f)); REQUIRE(g.nodes[0].y == Catch::Approx(999.0f)); // Resto: 9 nodos con cols=3, rows=3 -> centro en (0,0) // El nodo 4 esta en (col=1, row=1) -> exactamente (0,0) REQUIRE(g.nodes[4].x == Catch::Approx(0.0f)); REQUIRE(g.nodes[4].y == Catch::Approx(0.0f)); // Velocidades a cero REQUIRE(g.nodes[1].vx == 0.0f); REQUIRE(g.nodes[1].vy == 0.0f); } TEST_CASE("layout_circular places nodes on circle", "[viz][layouts][circular]") { TestGraph g; g.make(8); graph::layout_circular(g.data, 50.0f); for (int i = 0; i < 8; ++i) { float r = std::sqrt(g.nodes[i].x * g.nodes[i].x + g.nodes[i].y * g.nodes[i].y); REQUIRE(r == Catch::Approx(50.0f).margin(1e-3f)); } } TEST_CASE("layout_circular respects NF_PINNED", "[viz][layouts][circular]") { TestGraph g; g.make(4); g.nodes[2].x = 7.0f; g.nodes[2].y = -3.0f; g.nodes[2].flags |= NF_PINNED; g.sync(); graph::layout_circular(g.data, 100.0f); REQUIRE(g.nodes[2].x == Catch::Approx(7.0f)); REQUIRE(g.nodes[2].y == Catch::Approx(-3.0f)); } TEST_CASE("layout_random keeps positions in [-spread, spread]", "[viz][layouts][random]") { TestGraph g; g.make(50); graph::layout_random(g.data, 25.0f); for (int i = 0; i < g.data.node_count; ++i) { REQUIRE(g.nodes[i].x >= -25.0f); REQUIRE(g.nodes[i].x <= 25.0f); REQUIRE(g.nodes[i].y >= -25.0f); REQUIRE(g.nodes[i].y <= 25.0f); REQUIRE(g.nodes[i].vx == 0.0f); REQUIRE(g.nodes[i].vy == 0.0f); } } TEST_CASE("layout_radial root at center, neighbors on first ring", "[viz][layouts][radial]") { // Estrella de 5 puntas: nodo 0 -> nodos 1..5 TestGraph g; g.make(6); for (int i = 1; i <= 5; ++i) g.add_edge(0, i); graph::layout_radial(g.data, /*root=*/0, /*ring=*/100.0f); // root en (0,0) REQUIRE(g.nodes[0].x == Catch::Approx(0.0f).margin(1e-3f)); REQUIRE(g.nodes[0].y == Catch::Approx(0.0f).margin(1e-3f)); // Vecinos a distancia ~100 for (int i = 1; i <= 5; ++i) { float r = std::sqrt(g.nodes[i].x * g.nodes[i].x + g.nodes[i].y * g.nodes[i].y); REQUIRE(r == Catch::Approx(100.0f).margin(1e-3f)); } } TEST_CASE("layout_radial places hop2 on second ring", "[viz][layouts][radial]") { // Cadena: 0 -> 1 -> 2 -> 3 TestGraph g; g.make(4); g.add_edge(0, 1); g.add_edge(1, 2); g.add_edge(2, 3); graph::layout_radial(g.data, 0, 50.0f); auto radius = [&](int i) { return std::sqrt(g.nodes[i].x * g.nodes[i].x + g.nodes[i].y * g.nodes[i].y); }; REQUIRE(radius(0) == Catch::Approx(0.0f).margin(1e-3f)); REQUIRE(radius(1) == Catch::Approx(50.0f).margin(1e-3f)); REQUIRE(radius(2) == Catch::Approx(100.0f).margin(1e-3f)); REQUIRE(radius(3) == Catch::Approx(150.0f).margin(1e-3f)); } TEST_CASE("layout_radial respects pin", "[viz][layouts][radial]") { TestGraph g; g.make(3); g.add_edge(0, 1); g.add_edge(0, 2); g.nodes[1].x = 42.0f; g.nodes[1].y = 7.0f; g.nodes[1].flags |= NF_PINNED; g.sync(); graph::layout_radial(g.data, 0, 50.0f); REQUIRE(g.nodes[1].x == Catch::Approx(42.0f)); REQUIRE(g.nodes[1].y == Catch::Approx(7.0f)); } TEST_CASE("layout_hierarchical levels by longest path (LR)", "[viz][layouts][hierarchical]") { // DAG: 0 -> 1, 0 -> 2, 1 -> 3, 2 -> 3 (diamond) // longest-path levels: 0=0, 1=1, 2=1, 3=2 TestGraph g; g.make(4); g.add_edge(0, 1); g.add_edge(0, 2); g.add_edge(1, 3); g.add_edge(2, 3); graph::layout_hierarchical(g.data, /*direction=*/0, /*layer=*/100.0f, /*node=*/40.0f); // Nivel 0 (nodo 0) en x=0 REQUIRE(g.nodes[0].x == Catch::Approx(0.0f).margin(1e-3f)); // Nivel 1 (nodos 1,2) en x=100 REQUIRE(g.nodes[1].x == Catch::Approx(100.0f).margin(1e-3f)); REQUIRE(g.nodes[2].x == Catch::Approx(100.0f).margin(1e-3f)); // Nivel 2 (nodo 3) en x=200 REQUIRE(g.nodes[3].x == Catch::Approx(200.0f).margin(1e-3f)); } TEST_CASE("layout_hierarchical TB swaps axes", "[viz][layouts][hierarchical]") { TestGraph g; g.make(3); g.add_edge(0, 1); g.add_edge(1, 2); graph::layout_hierarchical(g.data, /*direction=*/2, /*layer=*/50.0f, /*node=*/30.0f); // TB: y crece con el nivel, x es transverse REQUIRE(g.nodes[0].y == Catch::Approx(0.0f).margin(1e-3f)); REQUIRE(g.nodes[1].y == Catch::Approx(50.0f).margin(1e-3f)); REQUIRE(g.nodes[2].y == Catch::Approx(100.0f).margin(1e-3f)); } TEST_CASE("layout_hierarchical respects pin", "[viz][layouts][hierarchical]") { TestGraph g; g.make(3); g.add_edge(0, 1); g.add_edge(1, 2); g.nodes[1].x = -999.0f; g.nodes[1].y = 555.0f; g.nodes[1].flags |= NF_PINNED; g.sync(); graph::layout_hierarchical(g.data, 0, 100.0f, 40.0f); REQUIRE(g.nodes[1].x == Catch::Approx(-999.0f)); REQUIRE(g.nodes[1].y == Catch::Approx(555.0f)); } TEST_CASE("layout_fixed is a no-op", "[viz][layouts][fixed]") { TestGraph g; g.make(5); for (int i = 0; i < 5; ++i) { g.nodes[i].x = (float)(i * 10); g.nodes[i].y = (float)(i * -3); g.nodes[i].vx = 0.5f; g.nodes[i].vy = -0.5f; } g.sync(); graph::layout_fixed(g.data); for (int i = 0; i < 5; ++i) { REQUIRE(g.nodes[i].x == Catch::Approx((float)(i * 10))); REQUIRE(g.nodes[i].y == Catch::Approx((float)(i * -3))); REQUIRE(g.nodes[i].vx == Catch::Approx(0.5f)); REQUIRE(g.nodes[i].vy == Catch::Approx(-0.5f)); } } TEST_CASE("layouts handle empty graph", "[viz][layouts][empty]") { TestGraph g; g.make(0); REQUIRE_NOTHROW(graph::layout_grid(g.data, 10.0f)); REQUIRE_NOTHROW(graph::layout_circular(g.data, 10.0f)); REQUIRE_NOTHROW(graph::layout_random(g.data, 10.0f)); REQUIRE_NOTHROW(graph::layout_radial(g.data, 0, 10.0f)); REQUIRE_NOTHROW(graph::layout_hierarchical(g.data, 0, 10.0f, 10.0f)); REQUIRE_NOTHROW(graph::layout_fixed(g.data)); }