From eff8d43479e7704df65fae2a80eefa787213f502 Mon Sep 17 00:00:00 2001 From: skal Date: Mon, 9 Feb 2026 20:27:04 +0100 Subject: refactor: Reorganize tests into subsystem subdirectories Restructured test suite for better organization and targeted testing: **Structure:** - src/tests/audio/ - 15 audio system tests - src/tests/gpu/ - 12 GPU/shader tests - src/tests/3d/ - 6 3D rendering tests - src/tests/assets/ - 2 asset system tests - src/tests/util/ - 3 utility tests - src/tests/common/ - 3 shared test helpers - src/tests/scripts/ - 2 bash test scripts (moved conceptually, not physically) **CMake changes:** - Updated add_demo_test macro to accept LABEL parameter - Applied CTest labels to all 36 tests for subsystem filtering - Updated all test file paths in CMakeLists.txt - Fixed common helper paths (webgpu_test_fixture, etc.) - Added custom targets for subsystem testing: - run_audio_tests, run_gpu_tests, run_3d_tests - run_assets_tests, run_util_tests, run_all_tests **Include path updates:** - Fixed relative includes in GPU tests to reference ../common/ **Documentation:** - Updated doc/HOWTO.md with subsystem test commands - Updated doc/CONTRIBUTING.md with new test organization - Updated scripts/check_all.sh to reflect new structure **Verification:** - All 36 tests passing (100%) - ctest -L filters work correctly - make run__tests targets functional - scripts/check_all.sh passes Backward compatible: make test and ctest continue to work unchanged. handoff(Gemini): Test reorganization complete. 36/36 tests passing. --- src/tests/util/test_file_watcher.cc | 63 ++++++++ src/tests/util/test_maths.cc | 299 ++++++++++++++++++++++++++++++++++++ src/tests/util/test_procedural.cc | 137 +++++++++++++++++ 3 files changed, 499 insertions(+) create mode 100644 src/tests/util/test_file_watcher.cc create mode 100644 src/tests/util/test_maths.cc create mode 100644 src/tests/util/test_procedural.cc (limited to 'src/tests/util') diff --git a/src/tests/util/test_file_watcher.cc b/src/tests/util/test_file_watcher.cc new file mode 100644 index 0000000..ac13afd --- /dev/null +++ b/src/tests/util/test_file_watcher.cc @@ -0,0 +1,63 @@ +// test_file_watcher.cc - Unit tests for file change detection + +#include "util/file_watcher.h" +#include +#include +#include + +#if !defined(STRIP_ALL) + +int main() { + // Create a temporary test file + const char* test_file = "/tmp/test_watcher_file.txt"; + { + std::ofstream f(test_file); + f << "initial content\n"; + } + + FileWatcher watcher; + watcher.add_file(test_file); + + // Initial check - no changes yet + bool changed = watcher.check_changes(); + if (changed) { + fprintf(stderr, "FAIL: Expected no changes on first check\n"); + return 1; + } + + // Sleep to ensure mtime changes (some filesystems have 1s granularity) + sleep(1); + + // Modify the file + { + std::ofstream f(test_file, std::ios::app); + f << "modified\n"; + } + + // Check for changes + changed = watcher.check_changes(); + if (!changed) { + fprintf(stderr, "FAIL: Expected changes after file modification\n"); + return 1; + } + + // Reset and check again - should be no changes + watcher.reset(); + changed = watcher.check_changes(); + if (changed) { + fprintf(stderr, "FAIL: Expected no changes after reset\n"); + return 1; + } + + printf("PASS: FileWatcher tests\n"); + return 0; +} + +#else + +int main() { + printf("SKIP: FileWatcher tests (STRIP_ALL build)\n"); + return 0; +} + +#endif diff --git a/src/tests/util/test_maths.cc b/src/tests/util/test_maths.cc new file mode 100644 index 0000000..0fed85c --- /dev/null +++ b/src/tests/util/test_maths.cc @@ -0,0 +1,299 @@ +// This file is part of the 64k demo project. +// It tests the mathematical utility functions. +// Verifies vector operations, matrix transformations, and interpolation. + +#include "util/mini_math.h" +#include +#include +#include +#include + +// Checks if two floats are approximately equal +bool near(float a, float b, float e = 0.001f) { + return std::abs(a - b) < e; +} + +// Generic test runner for any vector type (vec2, vec3, vec4) +template void test_vector_ops(int n) { + T a, b; + // Set values + for (int i = 0; i < n; ++i) { + a[i] = (float)(i + 1); + b[i] = 10.0f; + } + + // Add + T c = a + b; + for (int i = 0; i < n; ++i) + assert(near(c[i], (float)(i + 1) + 10.0f)); + + // Scale + T s = a * 2.0f; + for (int i = 0; i < n; ++i) + assert(near(s[i], (float)(i + 1) * 2.0f)); + + // Dot Product + // vec3(1,2,3) . vec3(1,2,3) = 1+4+9 = 14 + float expected_dot = 0; + for (int i = 0; i < n; ++i) + expected_dot += a[i] * a[i]; + assert(near(T::dot(a, a), expected_dot)); + + // Norm (Length) + assert(near(a.norm(), std::sqrt(expected_dot))); + + // Normalize + T n_vec = a.normalize(); + assert(near(n_vec.norm(), 1.0f)); + + // Normalize zero vector + T zero_vec = T(); // Default construct to zero + T norm_zero = zero_vec.normalize(); + for (int i = 0; i < n; ++i) + assert(near(norm_zero[i], 0.0f)); + + // Lerp + T l = lerp(a, b, 0.3f); + for (int i = 0; i < n; ++i) + assert(near(l[i], .7 * (i + 1) + .3 * 10.0f)); +} + +// Specific test for padding alignment in vec3 +void test_vec3_special() { + std::cout << "Testing vec3 alignment..." << std::endl; + // Verify sizeof is 16 bytes (4 floats) due to padding for WebGPU + assert(sizeof(vec3) == 16); + + vec3 v(1, 0, 0); + vec3 v2(0, 1, 0); + + // Cross Product + vec3 c = vec3::cross(v, v2); + assert(near(c.x, 0) && near(c.y, 0) && near(c.z, 1)); +} + +// Tests quaternion rotation, look_at, and slerp +void test_quat() { + std::cout << "Testing Quat..." << std::endl; + + // Rotation (Rodrigues) + vec3 v(1, 0, 0); + quat q = quat::from_axis({0, 1, 0}, 1.5708f); // 90 deg Y + vec3 r = q.rotate(v); + assert(near(r.x, 0) && near(r.z, -1)); + + // Rotation edge cases: 0 deg, 180 deg, zero vector + quat zero_rot = quat::from_axis({1, 0, 0}, 0.0f); + vec3 rotated_zero = zero_rot.rotate(v); + assert(near(rotated_zero.x, 1.0f)); // Original vector + + quat half_pi_rot = quat::from_axis({0, 1, 0}, 3.14159f); // 180 deg Y + vec3 rotated_half_pi = half_pi_rot.rotate(v); + assert(near(rotated_half_pi.x, -1.0f)); // Rotated 180 deg around Y + + vec3 zero_vec(0, 0, 0); + vec3 rotated_zero_vec = q.rotate(zero_vec); + assert(near(rotated_zero_vec.x, 0.0f) && near(rotated_zero_vec.y, 0.0f) && + near(rotated_zero_vec.z, 0.0f)); + + // Look At + // Looking from origin to +X, with +Y as up. + // The local forward vector (0,0,-1) should be transformed to (1,0,0) + quat l = quat::look_at({0, 0, 0}, {10, 0, 0}, {0, 1, 0}); + vec3 f = l.rotate({0, 0, -1}); + assert(near(f.x, 1.0f) && near(f.y, 0.0f) && near(f.z, 0.0f)); + + // Slerp Midpoint + quat q1(0, 0, 0, 1); + quat q2 = quat::from_axis({0, 1, 0}, 1.5708f); // 90 deg + quat mid = slerp(q1, q2, 0.5f); // 45 deg + assert(near(mid.y, 0.3826f)); // sin(pi/8) + + // Slerp edge cases + quat slerp_mid_edge = slerp(q1, q2, 0.0f); + assert(near(slerp_mid_edge.w, q1.w) && near(slerp_mid_edge.x, q1.x) && + near(slerp_mid_edge.y, q1.y) && near(slerp_mid_edge.z, q1.z)); + slerp_mid_edge = slerp(q1, q2, 1.0f); + assert(near(slerp_mid_edge.w, q2.w) && near(slerp_mid_edge.x, q2.x) && + near(slerp_mid_edge.y, q2.y) && near(slerp_mid_edge.z, q2.z)); + + // FromTo + quat from_to_test = + quat::from_to({1, 0, 0}, {0, 1, 0}); // 90 deg rotation around Z + vec3 rotated = from_to_test.rotate({1, 0, 0}); + assert(near(rotated.y, 1.0f)); +} + +// Tests WebGPU specific matrices +void test_matrices() { + std::cout << "Testing Matrices..." << std::endl; + float n = 0.1f, f = 100.0f; + mat4 p = mat4::perspective(0.785f, 1.0f, n, f); + + // Check WebGPU Z-range [0, 1] + // Z_ndc = (m10 * Z_view + m14) / -Z_view + float z_near = (p.m[10] * -n + p.m[14]) / n; + float z_far = (p.m[10] * -f + p.m[14]) / f; + assert(near(z_near, 0.0f)); + assert(near(z_far, 1.0f)); + + // Test mat4::look_at + vec3 eye(0, 0, 5); + vec3 target(0, 0, 0); + vec3 up(0, 1, 0); + mat4 view = mat4::look_at(eye, target, up); + // Point (0,0,0) in world should be at (0,0,-5) in view space + assert(near(view.m[14], -5.0f)); + + // Test matrix multiplication + mat4 t = mat4::translate({1, 2, 3}); + mat4 s = mat4::scale({2, 2, 2}); + mat4 ts = t * s; // Scale then Translate (if applied to vector on right: M*v) + + // v = (1,1,1,1) -> scale(2,2,2) -> (2,2,2,1) -> translate(1,2,3) -> (3,4,5,1) + vec4 v(1, 1, 1, 1); + vec4 res = ts * v; + assert(near(res.x, 3.0f)); + assert(near(res.y, 4.0f)); + assert(near(res.z, 5.0f)); + + // Test Rotation + // Rotate 90 deg around Z. (1,0,0) -> (0,1,0) + mat4 r = mat4::rotate({0, 0, 1}, 1.570796f); + vec4 v_rot = r * vec4(1, 0, 0, 1); + assert(near(v_rot.x, 0.0f)); + assert(near(v_rot.y, 1.0f)); +} + +// Tests easing curves +void test_ease() { + std::cout << "Testing Easing..." << std::endl; + // Boundary tests + assert(near(ease::out_cubic(0.0f), 0.0f)); + assert(near(ease::out_cubic(1.0f), 1.0f)); + assert(near(ease::in_out_quad(0.0f), 0.0f)); + assert(near(ease::in_out_quad(1.0f), 1.0f)); + assert(near(ease::out_expo(0.0f), 0.0f)); + assert(near(ease::out_expo(1.0f), 1.0f)); + + // Midpoint/Logic tests + assert(ease::out_cubic(0.5f) > + 0.5f); // Out curves should exceed linear value early + assert( + near(ease::in_out_quad(0.5f), 0.5f)); // Symmetric curves hit 0.5 at 0.5 + assert(ease::out_expo(0.5f) > 0.5f); // Exponential out should be above linear +} + +// Tests spring solver +void test_spring() { + std::cout << "Testing Spring..." << std::endl; + float p = 0, v = 0; + // Simulate approx 1 sec with 0.5s smooth time + for (int i = 0; i < 60; ++i) + spring::solve(p, v, 10.0f, 0.5f, 0.016f); + assert(p > 8.5f); // Should be close to 10 after 1 sec + + // Test convergence over longer period + p = 0; + v = 0; + for (int i = 0; i < 200; ++i) + spring::solve(p, v, 10.0f, 0.5f, 0.016f); + assert(near(p, 10.0f, 0.1f)); // Should be very close to target + + // Test vector spring + vec3 vp(0, 0, 0), vv(0, 0, 0), vt(10, 0, 0); + spring::solve(vp, vv, vt, 0.5f, 0.016f * 60.0f); // 1 huge step approx + assert(vp.x > 1.0f); // Should have moved significantly +} + +// Verifies that a matrix is approximately the identity matrix +void check_identity(const mat4& m) { + for (int i = 0; i < 16; ++i) { + float expected = (i % 5 == 0) ? 1.0f : 0.0f; + if (!near(m.m[i], expected, 0.005f)) { + std::cerr << "Matrix not Identity at index " << i << ": got " << m.m[i] + << " expected " << expected << std::endl; + assert(false); + } + } +} + +// Tests matrix inversion and transposition correctness +void test_matrix_inversion() { + std::cout << "Testing Matrix Inversion..." << std::endl; + + // 1. Identity + mat4 id; + check_identity(id.inverse()); + + // 2. Translation + mat4 t = mat4::translate({10.0f, -5.0f, 2.0f}); + mat4 t_inv = t.inverse(); + check_identity(t * t_inv); + check_identity(t_inv * t); + + // 3. Scale (non-uniform) + mat4 s = mat4::scale({2.0f, 0.5f, 4.0f}); + mat4 s_inv = s.inverse(); + check_identity(s * s_inv); + + // 4. Rotation + mat4 r = + mat4::rotate({1.0f, 2.0f, 3.0f}, 0.785f); // 45 deg around complex axis + mat4 r_inv = r.inverse(); + check_identity(r * r_inv); + + // 5. Complex Transform (TRS) + mat4 trs = t * (r * s); + mat4 trs_inv = trs.inverse(); + check_identity(trs * trs_inv); + check_identity(trs_inv * trs); + + // 6. Transposition + std::cout << "Testing Matrix Transposition..." << std::endl; + mat4 trs_t = mat4::transpose(trs); + mat4 trs_tt = mat4::transpose(trs_t); + for (int i = 0; i < 16; ++i) { + assert(near(trs.m[i], trs_tt.m[i])); + } + + // 7. Manual "stress" matrix (some small values, some large) + mat4 stress; + stress.m[0] = 1.0f; + stress.m[5] = 2.0f; + stress.m[10] = 3.0f; + stress.m[15] = 4.0f; + stress.m[12] = 100.0f; + stress.m[1] = 0.5f; + mat4 stress_inv = stress.inverse(); + check_identity(stress * stress_inv); + + // 8. Test Singular Matrix + mat4 singular_scale; + singular_scale.m[5] = 0.0f; // Scale Y by zero, making it singular + mat4 singular_inv = singular_scale.inverse(); + // The inverse of a singular matrix should be the identity matrix as per the + // implementation + check_identity(singular_inv); +} + +int main() { + std::cout << "Testing vec2..." << std::endl; + test_vector_ops(2); + + std::cout << "Testing vec3..." << std::endl; + test_vector_ops(3); + test_vec3_special(); + + std::cout << "Testing vec4..." << std::endl; + test_vector_ops(4); + + test_quat(); + test_matrices(); + test_matrix_inversion(); // New tests + test_ease(); + test_spring(); + + std::cout << "--- ALL TESTS PASSED ---" << std::endl; + return 0; +} \ No newline at end of file diff --git a/src/tests/util/test_procedural.cc b/src/tests/util/test_procedural.cc new file mode 100644 index 0000000..e9f9a02 --- /dev/null +++ b/src/tests/util/test_procedural.cc @@ -0,0 +1,137 @@ +// This file is part of the 64k demo project. +// It tests the procedural generation system. + +#include "procedural/generator.h" +#include +#include +#include +#include + +void test_noise() { + std::cout << "Testing Noise Generator..." << std::endl; + int w = 64, h = 64; + std::vector buffer(w * h * 4); + float params[] = {12345, 1.0f}; // Seed, Intensity + + // Test with explicit params + bool res = procedural::gen_noise(buffer.data(), w, h, params, 2); + assert(res); + assert(buffer[3] == 255); + + // Check that not all pixels are black + bool nonzero = false; + for (size_t i = 0; i < buffer.size(); i += 4) { + if (buffer[i] > 0) { + nonzero = true; + break; + } + } + assert(nonzero); + + // Test with default params + std::fill(buffer.begin(), buffer.end(), 0); + res = procedural::gen_noise(buffer.data(), w, h, nullptr, 0); + assert(res); + assert(buffer[3] == 255); // Alpha should still be set +} + +void test_perlin() { + std::cout << "Testing Perlin Generator..." << std::endl; + int w = 64, h = 64; + std::vector buffer(w * h * 4); + + // Test with explicit params + // Params: Seed, Freq, Amp, Decay, Octaves + float params[] = {12345, 4.0f, 1.0f, 0.5f, 4.0f}; + bool res = procedural::gen_perlin(buffer.data(), w, h, params, 5); + assert(res); + assert(buffer[3] == 255); + + bool nonzero = false; + for (size_t i = 0; i < buffer.size(); i += 4) { + if (buffer[i] > 0) { + nonzero = true; + break; + } + } + assert(nonzero); + + // Test with default params + std::fill(buffer.begin(), buffer.end(), 0); + res = procedural::gen_perlin(buffer.data(), w, h, nullptr, 0); + assert(res); + assert(buffer[3] == 255); + + // Test memory allocation failure simulation (large dimensions) + // This is hard to robustly test without mocking, but we can try an + // excessively large allocation if desired. For now, we trust the logic path. +} + +void test_grid() { + std::cout << "Testing Grid Generator..." << std::endl; + int w = 100, h = 100; + std::vector buffer(w * h * 4); + float params[] = {10, 1}; // Size 10, Thickness 1 + + // Test with explicit params + bool res = procedural::gen_grid(buffer.data(), w, h, params, 2); + assert(res); + + // Pixel (0,0) should be white (on line) + assert(buffer[0] == 255); + // Pixel (5,5) should be black (off line, since size=10) + assert(buffer[(5 * w + 5) * 4] == 0); + // Pixel (10,0) should be white (on vertical line) + assert(buffer[(0 * w + 10) * 4] == 255); + + // Test with default params + res = procedural::gen_grid(buffer.data(), w, h, nullptr, 0); + assert(res); + // Default size is 32, thickness 2 + assert(buffer[0] == 255); + assert(buffer[(0 * w + 32) * 4] == 255); +} + +void test_periodic() { + std::cout << "Testing Periodic Blending..." << std::endl; + int w = 64, h = 64; + std::vector buffer(w * h * 4); + + // Fill with horizontal gradient: left=0, right=255 + for (int y = 0; y < h; ++y) { + for (int x = 0; x < w; ++x) { + int idx = (y * w + x) * 4; + buffer[idx] = (uint8_t)(x * 255 / (w - 1)); + buffer[idx + 1] = 0; + buffer[idx + 2] = 0; + buffer[idx + 3] = 255; + } + } + + // Pre-check: edges are different + assert(buffer[0] == 0); + assert(buffer[(w - 1) * 4] == 255); + + float params[] = {0.1f}; // Blend ratio 10% + bool res = procedural::make_periodic(buffer.data(), w, h, params, 1); + assert(res); + + // Post-check: Left edge (x=0) should now be blended with right edge. + // Logic: blend right edge INTO left edge. At x=0, we copy from right side. + // So buffer[0] should be close to 255 (value from right). + assert(buffer[0] > 200); + + // Check invalid ratio + float invalid_params[] = {-1.0f}; + res = procedural::make_periodic(buffer.data(), w, h, invalid_params, 1); + assert(res); // Should return true but do nothing +} + +int main() { + test_noise(); + test_perlin(); + test_grid(); + test_periodic(); + std::cout << "--- PROCEDURAL TESTS PASSED ---" << std::endl; + return 0; +} \ No newline at end of file -- cgit v1.2.3