From 34ce5720986b3ea7bdc86090707c216d052a711d Mon Sep 17 00:00:00 2001 From: Slendi Date: Sun, 24 Aug 2025 20:17:43 +0300 Subject: [PATCH] Add tests Signed-off-by: Slendi --- CMakeLists.txt | 27 ++++++- include/smath.hpp | 133 +++++++++++++++++----------------- tests/vec.cpp | 179 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 272 insertions(+), 67 deletions(-) create mode 100644 tests/vec.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 2c83bb9..7383bfa 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,11 +4,13 @@ project(SmathExamples CXX) set(CMAKE_CXX_STANDARD 23) set(CMAKE_CXX_STANDARD_REQUIRED ON) +option(BUILD_EXAMPLES "Build example programs" ON) +option(BUILD_TESTS "Build unit tests" ON) + add_library(smath INTERFACE) target_include_directories(smath INTERFACE ${CMAKE_SOURCE_DIR}/include) add_library(smath::smath ALIAS smath) -option(BUILD_EXAMPLES "Build example programs" ON) if(BUILD_EXAMPLES) file(GLOB EXAMPLE_SOURCES "${CMAKE_SOURCE_DIR}/examples/*.cpp") foreach(EXAMPLE_FILE ${EXAMPLE_SOURCES}) @@ -17,3 +19,26 @@ if(BUILD_EXAMPLES) target_link_libraries(${EXAMPLE_NAME} PRIVATE smath::smath) endforeach() endif() + +if(BUILD_TESTS) + enable_testing() + + include(FetchContent) + FetchContent_Declare( + googletest + URL https://github.com/google/googletest/archive/refs/tags/v1.15.2.zip + ) + set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) + FetchContent_MakeAvailable(googletest) + + file(GLOB TEST_SOURCES "${CMAKE_SOURCE_DIR}/tests/*.cpp") + + add_executable(smath_tests ${TEST_SOURCES}) + target_link_libraries(smath_tests PRIVATE + smath::smath + GTest::gtest_main + ) + + include(GoogleTest) + gtest_discover_tests(smath_tests) +endif() diff --git a/include/smath.hpp b/include/smath.hpp index e4bba35..ae69b0a 100644 --- a/include/smath.hpp +++ b/include/smath.hpp @@ -1,4 +1,6 @@ -/* Copyright 2025 Slendi +/* smath - Single-file linear algebra math library for C++23. + * + * Copyright 2025 Slendi * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,7 +30,7 @@ namespace smath { template requires std::is_arithmetic_v -struct VecV; +struct Vec; namespace detail { @@ -41,27 +43,26 @@ template struct FixedString { } constexpr char operator[](std::size_t i) const { return data[i]; } }; -template struct is_vecv : std::false_type {}; -template -struct is_vecv> : std::true_type {}; +template struct is_Vec : std::false_type {}; +template struct is_Vec> : std::true_type {}; template -inline constexpr bool is_vecv_v = is_vecv>::value; +inline constexpr bool is_Vec_v = is_Vec>::value; template inline constexpr bool is_scalar_v = std::is_arithmetic_v>; -template struct vecv_size; +template struct Vec_size; template -struct vecv_size> : std::integral_constant {}; +struct Vec_size> : std::integral_constant {}; } // namespace detail template requires std::is_arithmetic_v -struct VecV : std::array { +struct Vec : std::array { private: template static consteval std::size_t extent() { - if constexpr (detail::is_vecv_v) - return detail::vecv_size>::value; + if constexpr (detail::is_Vec_v) + return detail::Vec_size>::value; else if constexpr (detail::is_scalar_v) return 1; else @@ -73,21 +74,21 @@ private: public: // Constructors - constexpr VecV() noexcept { + constexpr Vec() noexcept { for (auto &v : *this) v = T(0); } - explicit constexpr VecV(T const &s) noexcept { + explicit constexpr Vec(T const &s) noexcept { for (auto &v : *this) v = s; } template - requires((detail::is_scalar_v || detail::is_vecv_v) && ...) && + requires((detail::is_scalar_v || detail::is_Vec_v) && ...) && (total_extent() == N) && - (!(sizeof...(Args) == 1 && (detail::is_vecv_v && ...))) - constexpr VecV(Args &&...args) noexcept { + (!(sizeof...(Args) == 1 && (detail::is_Vec_v && ...))) + constexpr Vec(Args &&...args) noexcept { std::size_t i = 0; (fill_one(i, std::forward(args)), ...); } @@ -123,17 +124,17 @@ public: #undef VEC_ACC // RHS operations - friend constexpr auto operator+(T s, VecV const &v) noexcept -> VecV { + friend constexpr auto operator+(T s, Vec const &v) noexcept -> Vec { return v + s; } - friend constexpr auto operator-(T s, VecV const &v) noexcept -> VecV { - return VecV(s) - v; + friend constexpr auto operator-(T s, Vec const &v) noexcept -> Vec { + return Vec(s) - v; } - friend constexpr auto operator*(T s, VecV const &v) noexcept -> VecV { + friend constexpr auto operator*(T s, Vec const &v) noexcept -> Vec { return v * s; } - friend constexpr auto operator/(T s, VecV const &v) noexcept -> VecV { - VecV r{}; + friend constexpr auto operator/(T s, Vec const &v) noexcept -> Vec { + Vec r{}; for (std::size_t i = 0; i < N; ++i) r[i] = s / v[i]; return r; @@ -141,15 +142,15 @@ public: // Members #define VEC_OP(op) \ - constexpr auto operator op(VecV const &rhs) const noexcept -> VecV { \ - VecV result{}; \ + constexpr auto operator op(Vec const &rhs) const noexcept -> Vec { \ + Vec result{}; \ for (std::size_t i = 0; i < N; ++i) { \ result[i] = (*this)[i] op rhs[i]; \ } \ return result; \ } \ - constexpr auto operator op(T const &rhs) const noexcept -> VecV { \ - VecV result{}; \ + constexpr auto operator op(T const &rhs) const noexcept -> Vec { \ + Vec result{}; \ for (std::size_t i = 0; i < N; ++i) { \ result[i] = (*this)[i] op rhs; \ } \ @@ -161,12 +162,12 @@ public: VEC_OP(/) #undef VEC_OP #define VEC_OP_ASSIGN(sym) \ - constexpr VecV &operator sym##=(VecV const &rhs) noexcept { \ + constexpr Vec &operator sym##=(Vec const &rhs) noexcept { \ for (std::size_t i = 0; i < N; ++i) \ (*this)[i] sym## = rhs[i]; \ return *this; \ } \ - constexpr VecV &operator sym##=(T const &s) noexcept { \ + constexpr Vec &operator sym##=(T const &s) noexcept { \ for (std::size_t i = 0; i < N; ++i) \ (*this)[i] sym## = s; \ return *this; \ @@ -185,25 +186,26 @@ public: } constexpr auto length() const noexcept -> T { return this->magnitude(); } - constexpr VecV normalized_safe(T eps = eps_default) const noexcept { + constexpr Vec normalized_safe(T eps = eps_default) const noexcept { auto m = magnitude(); - return (m > eps) ? (*this) / m : VecV{}; + return (m > eps) ? (*this) / m : Vec{}; } - constexpr VecV normalize_safe(T eps = eps_default) const noexcept { + constexpr Vec normalize_safe(T eps = eps_default) const noexcept { return normalized_safe(eps); } - [[nodiscard]] constexpr auto normalized() noexcept -> VecV const { + [[nodiscard]] constexpr auto normalized() noexcept -> Vec const { return (*this) / this->magnitude(); } - [[nodiscard]] constexpr auto normalize() noexcept -> VecV const { + [[nodiscard]] constexpr auto normalize() noexcept -> Vec const { return this->normalized(); } - [[nodiscard]] constexpr auto unit() noexcept -> VecV const { + [[nodiscard]] constexpr auto unit() noexcept -> Vec const { return this->normalized(); } - [[nodiscard]] constexpr auto dot(VecV const &other) noexcept -> T { + [[nodiscard]] constexpr auto dot(Vec const &other) const noexcept + -> T const { T res = 0; for (std::size_t i = 0; i < N; ++i) { res += (*this)[i] * other[i]; @@ -214,7 +216,7 @@ public: static constexpr T eps_default = T(1e-6); template [[nodiscard]] constexpr auto - approx_equal(VecV const &rhs, U eps = eps_default) const noexcept { + approx_equal(Vec const &rhs, U eps = eps_default) const noexcept { using F = std::conditional_t, U, double>; for (size_t i = 0; i < N; ++i) if (std::abs(F((*this)[i] - rhs[i])) > F(eps)) @@ -232,26 +234,26 @@ public: template requires(N == 3) - constexpr VecV cross(const VecV &r) const noexcept { + constexpr Vec cross(const Vec &r) const noexcept { return {(*this)[1] * r[2] - (*this)[2] * r[1], (*this)[2] * r[0] - (*this)[0] * r[2], (*this)[0] * r[1] - (*this)[1] * r[0]}; } - constexpr T distance(VecV const &r) const noexcept { + constexpr T distance(Vec const &r) const noexcept { return (*this - r).magnitude(); } - constexpr VecV project_onto(VecV const &n) const noexcept { + constexpr Vec project_onto(Vec const &n) const noexcept { auto d = this->dot(n); auto nn = n.dot(n); - return (nn ? (d / nn) * n : VecV()); + return (nn ? (d / nn) * n : Vec()); } template requires(std::is_arithmetic_v && N >= 1) constexpr explicit(!std::is_convertible_v) - VecV(VecV const &other) noexcept { + Vec(Vec const &other) noexcept { for (std::size_t i = 0; i < N; ++i) this->operator[](i) = static_cast(other[i]); } @@ -259,8 +261,8 @@ public: template requires(std::is_arithmetic_v && N >= 1) constexpr explicit(!std::is_convertible_v) - operator VecV() const noexcept { - VecV r{}; + operator Vec() const noexcept { + Vec r{}; for (std::size_t i = 0; i < N; ++i) r[i] = static_cast((*this)[i]); return r; @@ -268,7 +270,7 @@ public: template requires(std::is_arithmetic_v && !std::is_same_v) - constexpr VecV &operator=(VecV const &rhs) noexcept { + constexpr Vec &operator=(Vec const &rhs) noexcept { for (std::size_t i = 0; i < N; ++i) (*this)[i] = static_cast(rhs[i]); return *this; @@ -285,42 +287,41 @@ private: (*this)[i++] = static_cast(v); } template - constexpr void fill_one(std::size_t &i, const VecV &v) noexcept { + constexpr void fill_one(std::size_t &i, const Vec &v) noexcept { for (std::size_t k = 0; k < M; ++k) (*this)[i++] = static_cast(v[k]); } #endif // SMATH_IMPLICIT_CONVERSIONS template - constexpr void fill_one(std::size_t &i, const VecV &v) noexcept { + constexpr void fill_one(std::size_t &i, const Vec &v) noexcept { for (std::size_t k = 0; k < M; ++k) (*this)[i++] = static_cast(v[k]); } }; -template -constexpr T &get(VecV &v) noexcept { +template constexpr T &get(Vec &v) noexcept { static_assert(I < N); return v[I]; } template -constexpr const T &get(const VecV &v) noexcept { +constexpr const T &get(const Vec &v) noexcept { static_assert(I < N); return v[I]; } template -constexpr T &&get(VecV &&v) noexcept { +constexpr T &&get(Vec &&v) noexcept { static_assert(I < N); return std::move(v[I]); } template -constexpr const T &&get(const VecV &&v) noexcept { +constexpr const T &&get(const Vec &&v) noexcept { static_assert(I < N); return std::move(v[I]); } template requires std::is_arithmetic_v -using Vec = std::conditional_t>; +using VecOrScalar = std::conditional_t>; namespace detail { @@ -358,12 +359,12 @@ constexpr auto is_valid(char c) -> bool { } template -constexpr auto swizzle_impl(VecV const &v, std::index_sequence) - -> Vec { +constexpr auto swizzle_impl(Vec const &v, std::index_sequence) + -> VecOrScalar { static_assert(((is_valid(S[I])) && ...), "Invalid swizzle component"); static_assert(((char_to_idx(S[I]) < N) && ...), "Pattern index out of bounds"); - Vec out{}; + VecOrScalar out{}; std::size_t i = 0; ((out[i++] = v[char_to_idx(S[I])]), ...); return out; @@ -387,29 +388,29 @@ concept ValidSwizzle = template requires detail::ValidSwizzle -constexpr auto swizzle(VecV const &v) -> Vec { +constexpr auto swizzle(Vec const &v) -> VecOrScalar { return detail::swizzle_impl(v, std::make_index_sequence{}); } -using Vec2 = VecV<2>; -using Vec3 = VecV<3>; -using Vec4 = VecV<4>; +using Vec2 = Vec<2>; +using Vec3 = Vec<3>; +using Vec4 = Vec<4>; -using Vec2d = VecV<2, double>; -using Vec3d = VecV<3, double>; -using Vec4d = VecV<4, double>; +using Vec2d = Vec<2, double>; +using Vec3d = Vec<3, double>; +using Vec4d = Vec<4, double>; } // namespace smath template requires std::formattable -struct std::formatter> : std::formatter { +struct std::formatter> : std::formatter { constexpr auto parse(std::format_parse_context &ctx) { return std::formatter::parse(ctx); } template - auto format(smath::VecV const &v, Ctx &ctx) const { + auto format(smath::Vec const &v, Ctx &ctx) const { auto out = ctx.out(); *out++ = '{'; for (std::size_t i = 0; i < N; ++i) { @@ -426,10 +427,10 @@ struct std::formatter> : std::formatter { namespace std { template -struct tuple_size> : std::integral_constant {}; +struct tuple_size> : std::integral_constant {}; template -struct tuple_element> { +struct tuple_element> { static_assert(I < N); using type = T; }; diff --git a/tests/vec.cpp b/tests/vec.cpp new file mode 100644 index 0000000..b4d6502 --- /dev/null +++ b/tests/vec.cpp @@ -0,0 +1,179 @@ +#include +#include +#include + +#include + +#include + +using smath::Vec; +using smath::Vec2; +using smath::Vec3; +using smath::Vec4; + +template +static void ExpectVecNear(const Vec<3, T> &a, const Vec<3, T> &b, + T eps = T(1e-6)) { + for (int i = 0; i < 3; ++i) + EXPECT_NEAR(double(a[i]), double(b[i]), double(eps)); +} + +// Constructors and accessors +TEST(Vec, DefaultZero) { + Vec3 v; + EXPECT_EQ(v[0], 0.0f); + EXPECT_EQ(v[1], 0.0f); + EXPECT_EQ(v[2], 0.0f); +} + +TEST(Vec, ScalarFillCtor) { + Vec4 v{2.0f}; + EXPECT_EQ(v.x(), 2.0f); + EXPECT_EQ(v.y(), 2.0f); + EXPECT_EQ(v.z(), 2.0f); + EXPECT_EQ(v.w(), 2.0f); +} + +TEST(Vec, VariadicCtorScalarsAndSubvectors) { + Vec2 a{1.0f, 2.0f}; + Vec2 b{3.0f, 4.0f}; + Vec4 v{a, b}; + EXPECT_EQ(v.r(), 1.0f); + EXPECT_EQ(v.g(), 2.0f); + EXPECT_EQ(v.b(), 3.0f); + EXPECT_EQ(v.a(), 4.0f); +} + +TEST(Vec, NamedAccessorsAliases) { + Vec3 v{1.0f, 2.0f, 3.0f}; + EXPECT_EQ(v.x(), v.r()); + EXPECT_EQ(v.y(), v.g()); + EXPECT_EQ(v.z(), v.b()); +} + +// Arithmetic +TEST(Vec, ElementwiseAndScalarOps) { + Vec3 a{1.0f, 2.0f, 3.0f}; + Vec3 b{4.0f, 5.0f, 6.0f}; + + auto s1 = a + b; + EXPECT_EQ(s1[0], 5.0f); + EXPECT_EQ(s1[1], 7.0f); + EXPECT_EQ(s1[2], 9.0f); + + auto s2 = a * 2.0f; + EXPECT_EQ(s2[0], 2.0f); + EXPECT_EQ(s2[1], 4.0f); + EXPECT_EQ(s2[2], 6.0f); + + auto s3 = 2.0f * a; // RHS overloads + EXPECT_EQ(s3[0], 2.0f); + EXPECT_EQ(s3[1], 4.0f); + EXPECT_EQ(s3[2], 6.0f); + + Vec3 c{1.0f, 2.0f, 3.0f}; + c += Vec3{1.0f, 1.0f, 1.0f}; + EXPECT_EQ(c[0], 2.0f); + EXPECT_EQ(c[1], 3.0f); + EXPECT_EQ(c[2], 4.0f); + + c *= 2.0f; + EXPECT_EQ(c[0], 4.0f); + EXPECT_EQ(c[1], 6.0f); + EXPECT_EQ(c[2], 8.0f); +} + +// Length, dot, cross, normalize +TEST(Vec, MagnitudeAndDot) { + Vec3 v{3.0f, 4.0f, 12.0f}; + EXPECT_FLOAT_EQ(v.magnitude(), 13.0f); + EXPECT_FLOAT_EQ(v.length(), 13.0f); + + Vec3 u{1.0f, 0.0f, 2.0f}; + EXPECT_FLOAT_EQ(v.dot(u), 27.0f); +} + +TEST(Vec, Cross3D) { + Vec3 x{1.0f, 0.0f, 0.0f}; + Vec3 y{0.0f, 1.0f, 0.0f}; + auto z = x.cross(y); + EXPECT_EQ(z[0], 0.0f); + EXPECT_EQ(z[1], 0.0f); + EXPECT_EQ(z[2], 1.0f); +} + +TEST(Vec, NormalizeAndSafeNormalize) { + Vec3 v{10.0f, 0.0f, 0.0f}; + auto n = v.normalized(); + auto ns = v.normalized_safe(); + ExpectVecNear(n, Vec3{1.0f, 0.0f, 0.0f}); + + Vec3 zero{}; + auto zs = zero.normalized_safe(); + EXPECT_EQ(zs[0], 0.0f); + EXPECT_EQ(zs[1], 0.0f); + EXPECT_EQ(zs[2], 0.0f); +} + +TEST(Vec, DistanceAndProjection) { + Vec3 a{1.0f, 2.0f, 3.0f}; + Vec3 b{4.0f, 6.0f, 3.0f}; + EXPECT_FLOAT_EQ(a.distance(b), 5.0f); + + Vec3 n{2.0f, 0.0f, 0.0f}; // onto x-axis scaled + auto p = a.project_onto(n); // (a·n)/(n·n) * n = (2)/4 * n = 0.5 * n + ExpectVecNear(p, Vec3{1.0f, 0.0f, 0.0f}); +} + +// Approx equal +TEST(Vec, ApproxEqual) { + Vec3 a{1.0f, 2.0f, 3.0f}; + Vec3 b{1.0f + 1e-7f, 2.0f - 1e-7f, 3.0f}; + EXPECT_TRUE(a.approx_equal(b, 1e-6f)); + EXPECT_FALSE(a.approx_equal(b, 1e-9f)); +} + +// std::get & tuple interop +TEST(Vec, StdGetAndTuple) { + Vec3 v{7.0f, 8.0f, 9.0f}; + static_assert(std::tuple_size_v == 3); + static_assert(std::is_same_v, float>); + EXPECT_EQ(std::get<0>(v), 7.0f); + EXPECT_EQ(std::get<1>(v), 8.0f); + EXPECT_EQ(std::get<2>(v), 9.0f); +} + +// Swizzle +TEST(Vec, SwizzleBasic) { + const Vec3 v{1.0f, 2.0f, 3.0f}; + + auto yz = smath::swizzle<"yz">(v); + EXPECT_EQ(yz[0], 2.0f); + EXPECT_EQ(yz[1], 3.0f); + + auto rxx = smath::swizzle<"xxy">(v); + EXPECT_EQ(rxx[0], 1.0f); + EXPECT_EQ(rxx[1], 1.0f); + EXPECT_EQ(rxx[2], 2.0f); +} + +// std::formatter +TEST(Vec, Formatter) { + smath::Vec<3, int> vi{1, 2, 3}; + std::string s = std::format("{}", vi); + EXPECT_EQ(s, "{1, 2, 3}"); +} + +// Conversions +TEST(Vec, ExplicitConversionBetweenScalarTypes) { + smath::Vec<3, int> vi{1, 2, 3}; + smath::Vec<3, float> vf{vi}; + EXPECT_EQ(vf[0], 1.0f); + EXPECT_EQ(vf[1], 2.0f); + EXPECT_EQ(vf[2], 3.0f); + + auto vi2 = static_cast>(vf); + EXPECT_EQ(vi2[0], 1); + EXPECT_EQ(vi2[1], 2); + EXPECT_EQ(vi2[2], 3); +}