From b75b9b8bf8283b7dce755f687bbd18444d0f9bca Mon Sep 17 00:00:00 2001 From: "coderabbitai[bot]" <136622811+coderabbitai[bot]@users.noreply.github.com> Date: Sun, 28 Jun 2026 10:11:27 +0000 Subject: [PATCH] CodeRabbit Generated Unit Tests: Add unit tests for PR changes --- CMakeLists.txt | 22 +- tests/test_adjacency_matrix.cpp | 354 ++++++++++++++++++++++++++ tests/test_grid_and_scene.cpp | 424 ++++++++++++++++++++++++++++++++ tests/test_utils.hpp | 80 ++++++ 4 files changed, 879 insertions(+), 1 deletion(-) create mode 100644 tests/test_adjacency_matrix.cpp create mode 100644 tests/test_grid_and_scene.cpp create mode 100644 tests/test_utils.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index b39723b..65e7268 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -22,7 +22,7 @@ add_library(CrocobyGraph ./src/interface/batch.cpp ./src/interface/color.cpp - ./src/interface/layout.cpp + ./src/interface/decompose.cpp ./src/interface/ecs.cpp ./src/interface/editor_frame.cpp ./src/interface/physics_frame.cpp @@ -52,3 +52,23 @@ target_link_libraries(CrocobyGraph PUBLIC rlImGui entt) add_executable(DemoCrocobyGraph ./bin/main.cpp) target_link_libraries(DemoCrocobyGraph PRIVATE CrocobyGraph) + +# Tests + +enable_testing() + +# test_adjacency_matrix: standalone — only needs adjacency_matrix.cpp (no raylib/imgui linkage). +add_executable(TestAdjacencyMatrix + ./tests/test_adjacency_matrix.cpp + ./src/interface/adjacency_matrix.cpp +) +target_include_directories(TestAdjacencyMatrix PRIVATE ./src) +add_test(NAME AdjacencyMatrix COMMAND TestAdjacencyMatrix) + +# test_grid_and_scene: requires the full CrocobyGraph library. +add_executable(TestGridAndScene + ./tests/test_grid_and_scene.cpp +) +target_include_directories(TestGridAndScene PRIVATE ./src) +target_link_libraries(TestGridAndScene PRIVATE CrocobyGraph) +add_test(NAME GridAndScene COMMAND TestGridAndScene) diff --git a/tests/test_adjacency_matrix.cpp b/tests/test_adjacency_matrix.cpp new file mode 100644 index 0000000..e5b4fdd --- /dev/null +++ b/tests/test_adjacency_matrix.cpp @@ -0,0 +1,354 @@ +// Tests for AdjacencyMatrix (src/interface/adjacency_matrix.hpp/.cpp) +// This file only depends on adjacency_matrix.cpp + stdlib (no raylib/imgui needed). + +#include "test_utils.hpp" +#include "../src/interface/adjacency_matrix.hpp" +#include + +using CrocobyGraph::AdjacencyMatrix; + +// --------------------------------------------------------------------------- +// Construction and basic accessors +// --------------------------------------------------------------------------- + +TEST_CASE(constructor_zero_nodes) { + AdjacencyMatrix m{0}; + CHECK_EQ(m.size(), 0u); +} + +TEST_CASE(constructor_single_node) { + AdjacencyMatrix m{1}; + CHECK_EQ(m.size(), 1u); + CHECK(m.at(0, 0) == false); +} + +TEST_CASE(constructor_initialises_all_false) { + AdjacencyMatrix m{4}; + for (size_t i = 0; i < 4; ++i) + for (size_t j = 0; j < 4; ++j) + CHECK(m.at(i, j) == false); +} + +// --------------------------------------------------------------------------- +// set / at +// --------------------------------------------------------------------------- + +TEST_CASE(set_and_read_single_edge) { + AdjacencyMatrix m{3}; + m.set(0, 1, true); + CHECK(m.at(0, 1) == true); + CHECK(m.at(1, 0) == false); // undirected not implied + CHECK(m.at(0, 0) == false); +} + +TEST_CASE(set_true_then_false) { + AdjacencyMatrix m{2}; + m.set(0, 1, true); + m.set(0, 1, false); + CHECK(m.at(0, 1) == false); +} + +TEST_CASE(set_all_edges_in_2x2) { + AdjacencyMatrix m{2}; + m.set(0, 0, true); + m.set(0, 1, true); + m.set(1, 0, true); + m.set(1, 1, true); + CHECK(m.at(0, 0) == true); + CHECK(m.at(0, 1) == true); + CHECK(m.at(1, 0) == true); + CHECK(m.at(1, 1) == true); +} + +TEST_CASE(set_self_loop) { + AdjacencyMatrix m{3}; + m.set(2, 2, true); + CHECK(m.at(2, 2) == true); + CHECK(m.at(0, 0) == false); + CHECK(m.at(1, 1) == false); +} + +// --------------------------------------------------------------------------- +// operator== +// --------------------------------------------------------------------------- + +TEST_CASE(equality_empty_matrices) { + AdjacencyMatrix a{3}; + AdjacencyMatrix b{3}; + CHECK(a == b); +} + +TEST_CASE(equality_after_same_set) { + AdjacencyMatrix a{3}; + AdjacencyMatrix b{3}; + a.set(0, 2, true); + b.set(0, 2, true); + CHECK(a == b); +} + +TEST_CASE(inequality_different_edges) { + AdjacencyMatrix a{3}; + AdjacencyMatrix b{3}; + a.set(0, 1, true); + b.set(1, 2, true); + CHECK(!(a == b)); +} + +// --------------------------------------------------------------------------- +// Copy constructor & assignment +// --------------------------------------------------------------------------- + +TEST_CASE(copy_constructor) { + AdjacencyMatrix orig{3}; + orig.set(1, 2, true); + AdjacencyMatrix copy{orig}; + CHECK(copy == orig); + CHECK_EQ(copy.size(), 3u); + // Mutation of copy does not affect original + copy.set(0, 0, true); + CHECK(!(copy == orig)); + CHECK(orig.at(0, 0) == false); +} + +TEST_CASE(copy_assignment) { + AdjacencyMatrix a{3}; + a.set(0, 1, true); + AdjacencyMatrix b{2}; + b = a; + CHECK(b == a); + CHECK_EQ(b.size(), 3u); +} + +TEST_CASE(self_assignment) { + AdjacencyMatrix m{2}; + m.set(0, 1, true); + AdjacencyMatrix& ref = m; + ref = ref; // self-assignment + CHECK(m.at(0, 1) == true); + CHECK_EQ(m.size(), 2u); +} + +// --------------------------------------------------------------------------- +// transpose() +// --------------------------------------------------------------------------- + +TEST_CASE(transpose_zero_matrix) { + AdjacencyMatrix m{3}; + AdjacencyMatrix t = m.transpose(); + CHECK(t == m); + CHECK_EQ(t.size(), 3u); +} + +TEST_CASE(transpose_single_directed_edge) { + // m: 0->1 exists, 1->0 does not + AdjacencyMatrix m{3}; + m.set(0, 1, true); + AdjacencyMatrix t = m.transpose(); + CHECK(t.at(1, 0) == true); + CHECK(t.at(0, 1) == false); +} + +TEST_CASE(transpose_symmetric_is_identity) { + AdjacencyMatrix m{3}; + m.set(0, 1, true); + m.set(1, 0, true); + m.set(0, 2, true); + m.set(2, 0, true); + AdjacencyMatrix t = m.transpose(); + CHECK(t == m); +} + +TEST_CASE(transpose_double_transpose_equals_original) { + AdjacencyMatrix m{4}; + m.set(0, 1, true); + m.set(1, 3, true); + m.set(2, 0, true); + AdjacencyMatrix tt = m.transpose().transpose(); + CHECK(tt == m); +} + +TEST_CASE(transpose_full_upper_triangle) { + // Set upper triangle; transpose should produce lower triangle + AdjacencyMatrix m{3}; + m.set(0, 1, true); + m.set(0, 2, true); + m.set(1, 2, true); + AdjacencyMatrix t = m.transpose(); + CHECK(t.at(1, 0) == true); + CHECK(t.at(2, 0) == true); + CHECK(t.at(2, 1) == true); + CHECK(t.at(0, 1) == false); + CHECK(t.at(0, 2) == false); + CHECK(t.at(1, 2) == false); +} + +TEST_CASE(transpose_preserves_self_loops) { + AdjacencyMatrix m{3}; + m.set(1, 1, true); + AdjacencyMatrix t = m.transpose(); + CHECK(t.at(1, 1) == true); + CHECK(t.at(0, 0) == false); + CHECK(t.at(2, 2) == false); +} + +// --------------------------------------------------------------------------- +// operator+ (logical OR of edge sets) +// --------------------------------------------------------------------------- + +TEST_CASE(plus_combines_disjoint_edges) { + AdjacencyMatrix a{3}; + a.set(0, 1, true); + AdjacencyMatrix b{3}; + b.set(1, 2, true); + AdjacencyMatrix c = a + b; + CHECK(c.at(0, 1) == true); + CHECK(c.at(1, 2) == true); + CHECK(c.at(0, 2) == false); +} + +TEST_CASE(plus_overlapping_edges_is_true) { + AdjacencyMatrix a{2}; + a.set(0, 1, true); + AdjacencyMatrix b{2}; + b.set(0, 1, true); + AdjacencyMatrix c = a + b; + CHECK(c.at(0, 1) == true); +} + +TEST_CASE(plus_empty_matrices_stays_empty) { + AdjacencyMatrix a{3}; + AdjacencyMatrix b{3}; + AdjacencyMatrix c = a + b; + for (size_t i = 0; i < 3; ++i) + for (size_t j = 0; j < 3; ++j) + CHECK(c.at(i, j) == false); +} + +TEST_CASE(plus_size_mismatch_throws) { + AdjacencyMatrix a{2}; + AdjacencyMatrix b{3}; + CHECK_THROWS(a + b); +} + +TEST_CASE(plus_does_not_modify_operands) { + AdjacencyMatrix a{2}; + a.set(0, 1, true); + AdjacencyMatrix b{2}; + b.set(1, 0, true); + AdjacencyMatrix orig_a{a}; + AdjacencyMatrix orig_b{b}; + (void)(a + b); + CHECK(a == orig_a); + CHECK(b == orig_b); +} + +// --------------------------------------------------------------------------- +// operator* (boolean matrix multiplication / reachability in two steps) +// --------------------------------------------------------------------------- + +TEST_CASE(multiply_zero_matrices_stays_zero) { + AdjacencyMatrix a{3}; + AdjacencyMatrix b{3}; + AdjacencyMatrix c = a * b; + for (size_t i = 0; i < 3; ++i) + for (size_t j = 0; j < 3; ++j) + CHECK(c.at(i, j) == false); +} + +TEST_CASE(multiply_size_mismatch_throws) { + AdjacencyMatrix a{2}; + AdjacencyMatrix b{3}; + CHECK_THROWS(a * b); +} + +TEST_CASE(multiply_two_step_path) { + // 0->1->2 => A*A should have 0->2 + AdjacencyMatrix m{3}; + m.set(0, 1, true); + m.set(1, 2, true); + AdjacencyMatrix mm = m * m; + CHECK(mm.at(0, 2) == true); + CHECK(mm.at(0, 1) == false); // no direct 2-step 0->1 + CHECK(mm.at(1, 2) == false); // no 2-step 1->2 +} + +TEST_CASE(multiply_identity_matrix_unchanged) { + // Build identity: I[i][i] = true + AdjacencyMatrix ident{3}; + ident.set(0, 0, true); + ident.set(1, 1, true); + ident.set(2, 2, true); + + AdjacencyMatrix m{3}; + m.set(0, 1, true); + m.set(2, 0, true); + + // I * m should equal m + AdjacencyMatrix result = ident * m; + CHECK(result == m); +} + +TEST_CASE(multiply_chain_of_three_nodes) { + // 0->1, 1->2, 2->3 => m*m*m should have 0->3 + AdjacencyMatrix m{4}; + m.set(0, 1, true); + m.set(1, 2, true); + m.set(2, 3, true); + AdjacencyMatrix m3 = (m * m) * m; + CHECK(m3.at(0, 3) == true); +} + +TEST_CASE(multiply_disconnected_graph) { + // Two disconnected edges: 0->1 and 2->3 + AdjacencyMatrix m{4}; + m.set(0, 1, true); + m.set(2, 3, true); + AdjacencyMatrix mm = m * m; + // No 2-step paths possible in this graph + for (size_t i = 0; i < 4; ++i) + for (size_t j = 0; j < 4; ++j) + CHECK(mm.at(i, j) == false); +} + +TEST_CASE(multiply_does_not_modify_operands) { + AdjacencyMatrix a{3}; + a.set(0, 1, true); + AdjacencyMatrix b{3}; + b.set(1, 2, true); + AdjacencyMatrix orig_a{a}; + AdjacencyMatrix orig_b{b}; + (void)(a * b); + CHECK(a == orig_a); + CHECK(b == orig_b); +} + +// --------------------------------------------------------------------------- +// Combined operations / regression +// --------------------------------------------------------------------------- + +TEST_CASE(transpose_of_sum) { + // (A + B)^T == A^T + B^T + AdjacencyMatrix a{3}; + a.set(0, 1, true); + AdjacencyMatrix b{3}; + b.set(2, 0, true); + AdjacencyMatrix lhs = (a + b).transpose(); + AdjacencyMatrix rhs = a.transpose() + b.transpose(); + CHECK(lhs == rhs); +} + +TEST_CASE(boundary_large_1x1_matrix) { + AdjacencyMatrix m{1}; + m.set(0, 0, true); + CHECK(m.at(0, 0) == true); + AdjacencyMatrix t = m.transpose(); + CHECK(t.at(0, 0) == true); + AdjacencyMatrix sum = m + m; + CHECK(sum.at(0, 0) == true); + AdjacencyMatrix prod = m * m; + CHECK(prod.at(0, 0) == true); +} + +int main() { + return TestUtils::report_and_exit(); +} \ No newline at end of file diff --git a/tests/test_grid_and_scene.cpp b/tests/test_grid_and_scene.cpp new file mode 100644 index 0000000..d0d15ca --- /dev/null +++ b/tests/test_grid_and_scene.cpp @@ -0,0 +1,424 @@ +// Tests for: +// - generate_grid() (src/interface/grid.hpp/.cpp) +// - Scene::adjacency_matrix() (src/interface/scene.hpp/.cpp) +// +// Requires linking against the CrocobyGraph library (entt, etc.) but does +// NOT open a window / touch GPU resources. + +#include "test_utils.hpp" +#include "../src/interface/grid.hpp" +#include "../src/interface/scene.hpp" +#include "../src/interface/batch.hpp" +#include "../src/interface/adjacency_matrix.hpp" +#include "../src/interface/entities.hpp" +#include +#include +#include + +using CrocobyGraph::AdjacencyMatrix; +using CrocobyGraph::Batch; +using CrocobyGraph::Scene; +using CrocobyGraph::generate_grid; + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +static float approx_eq(float a, float b, float eps = 0.001f) { + return std::fabs(a - b) < eps; +} + +// --------------------------------------------------------------------------- +// generate_grid: node & edge counts +// --------------------------------------------------------------------------- + +TEST_CASE(grid_1x1_has_one_node_no_edges) { + auto batch = generate_grid(1, 1); + Scene scene; + scene.append(std::move(batch)); + auto ns = scene.nodes(); + CHECK_EQ(ns.size(), 1u); + // No edges in a 1×1 grid + auto [nodes, matrix] = scene.adjacency_matrix(); + CHECK_EQ(nodes.size(), 1u); + CHECK(matrix.at(0, 0) == false); +} + +TEST_CASE(grid_2x1_has_two_nodes_one_edge) { + auto batch = generate_grid(2, 1); + Scene scene; + scene.append(std::move(batch)); + CHECK_EQ(scene.nodes().size(), 2u); + auto [nodes, matrix] = scene.adjacency_matrix(); + CHECK_EQ(nodes.size(), 2u); + // Exactly one undirected edge => both directions true + int edge_count = (matrix.at(0, 1) ? 1 : 0) + (matrix.at(1, 0) ? 1 : 0); + CHECK_EQ(edge_count, 2); +} + +TEST_CASE(grid_1x2_has_two_nodes_one_edge) { + auto batch = generate_grid(1, 2); + Scene scene; + scene.append(std::move(batch)); + CHECK_EQ(scene.nodes().size(), 2u); + auto [nodes, matrix] = scene.adjacency_matrix(); + CHECK_EQ(nodes.size(), 2u); + int edge_count = (matrix.at(0, 1) ? 1 : 0) + (matrix.at(1, 0) ? 1 : 0); + CHECK_EQ(edge_count, 2); +} + +TEST_CASE(grid_2x2_has_four_nodes_four_edges) { + // 2×2 grid: 4 nodes, 4 undirected edges (2 horizontal + 2 vertical) + auto batch = generate_grid(2, 2); + Scene scene; + scene.append(std::move(batch)); + CHECK_EQ(scene.nodes().size(), 4u); + + auto [nodes, matrix] = scene.adjacency_matrix(); + CHECK_EQ(nodes.size(), 4u); + + // Count total directed entries (each undirected edge contributes 2) + int total = 0; + for (size_t i = 0; i < 4; ++i) + for (size_t j = 0; j < 4; ++j) + if (matrix.at(i, j)) ++total; + // 4 undirected edges => 8 directed entries in boolean matrix + CHECK_EQ(total, 8); +} + +TEST_CASE(grid_3x2_node_count) { + // 3 wide, 2 tall => 6 nodes + auto batch = generate_grid(3, 2); + Scene scene; + scene.append(std::move(batch)); + CHECK_EQ(scene.nodes().size(), 6u); +} + +TEST_CASE(grid_2x3_node_count) { + auto batch = generate_grid(2, 3); + Scene scene; + scene.append(std::move(batch)); + CHECK_EQ(scene.nodes().size(), 6u); +} + +TEST_CASE(grid_3x3_node_and_edge_count) { + // 3×3: 9 nodes + // edges: 3*(3-1) horizontal + 3*(3-1) vertical = 6+6 = 12 undirected edges + auto batch = generate_grid(3, 3); + Scene scene; + scene.append(std::move(batch)); + CHECK_EQ(scene.nodes().size(), 9u); + + auto [nodes, matrix] = scene.adjacency_matrix(); + int total = 0; + for (size_t i = 0; i < 9; ++i) + for (size_t j = 0; j < 9; ++j) + if (matrix.at(i, j)) ++total; + // 12 undirected edges => 24 directed entries + CHECK_EQ(total, 24); +} + +// --------------------------------------------------------------------------- +// generate_grid: positions & spacing +// --------------------------------------------------------------------------- + +TEST_CASE(grid_1x1_default_spacing_at_origin) { + auto batch = generate_grid(1, 1); + Scene scene; + auto ids = scene.append(std::move(batch)); + // Single node: offset_x = (1-1)*60*0.5 = 0, offset_y = 0 + auto pos = scene.pos(ids[0]); + CHECK(approx_eq(pos.x, 0.0f)); + CHECK(approx_eq(pos.y, 0.0f)); +} + +TEST_CASE(grid_2x1_spacing) { + const float spacing = 60.0f; + auto batch = generate_grid(2, 1, spacing); + Scene scene; + auto ids = scene.append(std::move(batch)); + // offset_x = (1-2)*60*0.5 = -30 + // node0: x=-30, node1: x=30; y=0 for both + CHECK_EQ(scene.nodes().size(), 2u); + // Verify the two node positions are spacing apart along x + auto p0 = scene.pos(ids[0]); + auto p1 = scene.pos(ids[1]); + CHECK(approx_eq(std::fabs(p0.x - p1.x), spacing)); + CHECK(approx_eq(p0.y, p1.y)); +} + +TEST_CASE(grid_1x2_spacing) { + const float spacing = 60.0f; + auto batch = generate_grid(1, 2, spacing); + Scene scene; + auto ids = scene.append(std::move(batch)); + auto p0 = scene.pos(ids[0]); + auto p1 = scene.pos(ids[1]); + CHECK(approx_eq(p0.x, p1.x)); + CHECK(approx_eq(std::fabs(p0.y - p1.y), spacing)); +} + +TEST_CASE(grid_custom_spacing) { + const float spacing = 100.0f; + auto batch = generate_grid(2, 2, spacing); + Scene scene; + auto ids = scene.append(std::move(batch)); + // Positions: offset_x = (1-2)*100*0.5 = -50, offset_y = -50 + // (0,0)->(-50,-50), (1,0)->(50,-50), (0,1)->(-50,50), (1,1)->(50,50) + // Check that adjacent nodes differ by exactly `spacing` + auto p0 = scene.pos(ids[0]); // node(0,0) + auto p1 = scene.pos(ids[1]); // node(1,0) - horizontal neighbour + CHECK(approx_eq(std::fabs(p0.x - p1.x), spacing)); + CHECK(approx_eq(std::fabs(p0.y - p1.y), 0.0f)); +} + +TEST_CASE(grid_default_spacing_is_60) { + auto batch_default = generate_grid(2, 1); + auto batch_60 = generate_grid(2, 1, 60.0f); + Scene s1, s2; + auto ids1 = s1.append(std::move(batch_default)); + auto ids2 = s2.append(std::move(batch_60)); + auto p1a = s1.pos(ids1[0]); + auto p1b = s1.pos(ids1[1]); + auto p2a = s2.pos(ids2[0]); + auto p2b = s2.pos(ids2[1]); + CHECK(approx_eq(p1a.x, p2a.x)); + CHECK(approx_eq(p1b.x, p2b.x)); +} + +// --------------------------------------------------------------------------- +// generate_grid: undirected edges (both directions reachable) +// --------------------------------------------------------------------------- + +TEST_CASE(grid_edges_are_undirected) { + auto batch = generate_grid(2, 2); + Scene scene; + scene.append(std::move(batch)); + auto [nodes, matrix] = scene.adjacency_matrix(); + // For every i,j: matrix[i][j] == matrix[j][i] + for (size_t i = 0; i < nodes.size(); ++i) + for (size_t j = 0; j < nodes.size(); ++j) + CHECK(matrix.at(i, j) == matrix.at(j, i)); +} + +TEST_CASE(grid_no_self_loops) { + auto batch = generate_grid(3, 3); + Scene scene; + scene.append(std::move(batch)); + auto [nodes, matrix] = scene.adjacency_matrix(); + for (size_t i = 0; i < nodes.size(); ++i) + CHECK(matrix.at(i, i) == false); +} + +// --------------------------------------------------------------------------- +// Scene::adjacency_matrix(): empty scene +// --------------------------------------------------------------------------- + +TEST_CASE(scene_adjacency_empty_scene) { + Scene scene; + auto [nodes, matrix] = scene.adjacency_matrix(); + CHECK_EQ(nodes.size(), 0u); + CHECK_EQ(matrix.size(), 0u); +} + +// --------------------------------------------------------------------------- +// Scene::adjacency_matrix(): undirected edges +// --------------------------------------------------------------------------- + +TEST_CASE(scene_adjacency_two_nodes_undirected) { + Batch b; + auto n0 = b.add_node({}); + auto n1 = b.add_node({}); + b.add_edge({ .node_start = n0, .node_end = n1 }); // undirected (no arrows) + + Scene scene; + scene.append(std::move(b)); + auto [nodes, matrix] = scene.adjacency_matrix(); + CHECK_EQ(nodes.size(), 2u); + CHECK_EQ(matrix.size(), 2u); + + // Undirected: both directions should be set + size_t trues = 0; + for (size_t i = 0; i < 2; ++i) + for (size_t j = 0; j < 2; ++j) + if (matrix.at(i, j)) ++trues; + CHECK_EQ(trues, 2u); +} + +TEST_CASE(scene_adjacency_undirected_is_symmetric) { + Batch b; + auto n0 = b.add_node({}); + auto n1 = b.add_node({}); + auto n2 = b.add_node({}); + b.add_edge({ .node_start = n0, .node_end = n1 }); + b.add_edge({ .node_start = n1, .node_end = n2 }); + + Scene scene; + scene.append(std::move(b)); + auto [nodes, matrix] = scene.adjacency_matrix(); + CHECK_EQ(nodes.size(), 3u); + for (size_t i = 0; i < 3; ++i) + for (size_t j = 0; j < 3; ++j) + CHECK(matrix.at(i, j) == matrix.at(j, i)); +} + +TEST_CASE(scene_adjacency_disconnected_nodes_have_no_edges) { + Batch b; + b.add_node({}); + b.add_node({}); + // No edges added + + Scene scene; + scene.append(std::move(b)); + auto [nodes, matrix] = scene.adjacency_matrix(); + CHECK_EQ(nodes.size(), 2u); + CHECK(matrix.at(0, 0) == false); + CHECK(matrix.at(0, 1) == false); + CHECK(matrix.at(1, 0) == false); + CHECK(matrix.at(1, 1) == false); +} + +// --------------------------------------------------------------------------- +// Scene::adjacency_matrix(): directed edges (arrows) +// --------------------------------------------------------------------------- + +TEST_CASE(scene_adjacency_arrow_on_end_directed) { + // arrow_on_end means edge points from start->end + Batch b; + auto n0 = b.add_node({}); + auto n1 = b.add_node({}); + b.add_edge({ .node_start = n0, .node_end = n1, .arrow_on_end = true }); + + Scene scene; + scene.append(std::move(b)); + auto [nodes, matrix] = scene.adjacency_matrix(); + CHECK_EQ(nodes.size(), 2u); + + // Find indices for n0 and n1 in sorted nodes + // nodes is sorted, so we compare matrix entries: exactly one of (0,1) or (1,0) is true + int a = (matrix.at(0, 1) ? 1 : 0) + (matrix.at(1, 0) ? 1 : 0); + CHECK_EQ(a, 1); // Only one direction +} + +TEST_CASE(scene_adjacency_arrow_on_start_directed) { + // arrow_on_start means the edge points from end->start + Batch b; + auto n0 = b.add_node({}); + auto n1 = b.add_node({}); + b.add_edge({ .node_start = n0, .node_end = n1, .arrow_on_start = true }); + + Scene scene; + scene.append(std::move(b)); + auto [nodes, matrix] = scene.adjacency_matrix(); + CHECK_EQ(nodes.size(), 2u); + + int a = (matrix.at(0, 1) ? 1 : 0) + (matrix.at(1, 0) ? 1 : 0); + CHECK_EQ(a, 1); // Only one direction +} + +TEST_CASE(scene_adjacency_arrow_both_ends_is_undirected_equivalent) { + // arrow_on_start && arrow_on_end => directed is true, but both directions set + Batch b; + auto n0 = b.add_node({}); + auto n1 = b.add_node({}); + b.add_edge({ .node_start = n0, .node_end = n1, .arrow_on_start = true, .arrow_on_end = true }); + + Scene scene; + scene.append(std::move(b)); + auto [nodes, matrix] = scene.adjacency_matrix(); + int a = (matrix.at(0, 1) ? 1 : 0) + (matrix.at(1, 0) ? 1 : 0); + CHECK_EQ(a, 2); // Both directions +} + +TEST_CASE(scene_adjacency_no_self_loops_from_normal_edges) { + Batch b; + auto n0 = b.add_node({}); + auto n1 = b.add_node({}); + b.add_edge({ .node_start = n0, .node_end = n1 }); + + Scene scene; + scene.append(std::move(b)); + auto [nodes, matrix] = scene.adjacency_matrix(); + for (size_t i = 0; i < 2; ++i) + CHECK(matrix.at(i, i) == false); +} + +// --------------------------------------------------------------------------- +// Scene::adjacency_matrix(): multiple edges +// --------------------------------------------------------------------------- + +TEST_CASE(scene_adjacency_three_nodes_triangle) { + Batch b; + auto n0 = b.add_node({}); + auto n1 = b.add_node({}); + auto n2 = b.add_node({}); + b.add_edge({ .node_start = n0, .node_end = n1 }); + b.add_edge({ .node_start = n1, .node_end = n2 }); + b.add_edge({ .node_start = n2, .node_end = n0 }); + + Scene scene; + scene.append(std::move(b)); + auto [nodes, matrix] = scene.adjacency_matrix(); + CHECK_EQ(nodes.size(), 3u); + + // 3 undirected edges => 6 true entries + int total = 0; + for (size_t i = 0; i < 3; ++i) + for (size_t j = 0; j < 3; ++j) + if (matrix.at(i, j)) ++total; + CHECK_EQ(total, 6); +} + +TEST_CASE(scene_adjacency_star_graph) { + // Star: center node connected to 3 leaves + Batch b; + auto center = b.add_node({}); + auto l0 = b.add_node({}); + auto l1 = b.add_node({}); + auto l2 = b.add_node({}); + b.add_edge({ .node_start = center, .node_end = l0 }); + b.add_edge({ .node_start = center, .node_end = l1 }); + b.add_edge({ .node_start = center, .node_end = l2 }); + + Scene scene; + scene.append(std::move(b)); + auto [nodes, matrix] = scene.adjacency_matrix(); + CHECK_EQ(nodes.size(), 4u); + + // Each leaf connects to center => 3 undirected edges => 6 true entries + int total = 0; + for (size_t i = 0; i < 4; ++i) + for (size_t j = 0; j < 4; ++j) + if (matrix.at(i, j)) ++total; + CHECK_EQ(total, 6); + + // Center (whichever index) should have 3 connections + // Find center index by finding the node with 3 true entries in its row + bool found_center = false; + for (size_t i = 0; i < 4; ++i) { + int cnt = 0; + for (size_t j = 0; j < 4; ++j) + if (matrix.at(i, j)) ++cnt; + if (cnt == 3) { found_center = true; break; } + } + CHECK(found_center); +} + +TEST_CASE(scene_adjacency_returns_sorted_nodes_vector) { + Batch b; + b.add_node({}); + b.add_node({}); + b.add_node({}); + + Scene scene; + scene.append(std::move(b)); + auto [nodes, matrix] = scene.adjacency_matrix(); + CHECK_EQ(nodes.size(), 3u); + // nodes should be sorted + for (size_t i = 1; i < nodes.size(); ++i) + CHECK(nodes[i - 1] < nodes[i]); +} + +int main() { + return TestUtils::report_and_exit(); +} \ No newline at end of file diff --git a/tests/test_utils.hpp b/tests/test_utils.hpp new file mode 100644 index 0000000..bc2f10d --- /dev/null +++ b/tests/test_utils.hpp @@ -0,0 +1,80 @@ +#pragma once + +#include +#include +#include +#include + +namespace TestUtils { + +inline int g_failures = 0; +inline int g_checks = 0; +inline const char* g_current_test = nullptr; + +#define TEST_CASE(name) \ + static void test_##name(); \ + static struct Register_##name { \ + Register_##name() { \ + ::TestUtils::g_current_test = #name; \ + std::cout << "[TEST] " << #name << "\n"; \ + test_##name(); \ + } \ + } register_##name##_instance; \ + static void test_##name() + +#define CHECK(expr) \ + do { \ + ++::TestUtils::g_checks; \ + if (!(expr)) { \ + ++::TestUtils::g_failures; \ + std::cerr << " FAIL: " << #expr << " (" << __FILE__ << ":" << __LINE__ << ")\n"; \ + } \ + } while(0) + +#define CHECK_EQ(a, b) \ + do { \ + ++::TestUtils::g_checks; \ + auto _a = (a); \ + auto _b = (b); \ + if (!(_a == _b)) { \ + ++::TestUtils::g_failures; \ + std::cerr << " FAIL: " << #a << " == " << #b \ + << " [got: " << _a << " != " << _b << "]" \ + << " (" << __FILE__ << ":" << __LINE__ << ")\n"; \ + } \ + } while(0) + +#define CHECK_THROWS(expr) \ + do { \ + ++::TestUtils::g_checks; \ + bool _threw = false; \ + try { (expr); } catch (...) { _threw = true; } \ + if (!_threw) { \ + ++::TestUtils::g_failures; \ + std::cerr << " FAIL: expected exception from: " << #expr \ + << " (" << __FILE__ << ":" << __LINE__ << ")\n"; \ + } \ + } while(0) + +#define CHECK_NOTHROW(expr) \ + do { \ + ++::TestUtils::g_checks; \ + try { (expr); } \ + catch (...) { \ + ++::TestUtils::g_failures; \ + std::cerr << " FAIL: unexpected exception from: " << #expr \ + << " (" << __FILE__ << ":" << __LINE__ << ")\n"; \ + } \ + } while(0) + +inline int report_and_exit() { + std::cout << "\n=== Results: " << (g_checks - g_failures) << "/" << g_checks + << " passed"; + if (g_failures > 0) { + std::cout << ", " << g_failures << " FAILED"; + } + std::cout << " ===\n"; + return g_failures > 0 ? 1 : 0; +} + +} // namespace TestUtils \ No newline at end of file