Add tests

Signed-off-by: Slendi <slendi@socopon.com>
This commit is contained in:
2025-08-24 20:17:43 +03:00
parent dfb8981810
commit 34ce572098
3 changed files with 272 additions and 67 deletions

View File

@@ -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()

View File

@@ -1,4 +1,6 @@
/* Copyright 2025 Slendi <slendi@socopon.com>
/* smath - Single-file linear algebra math library for C++23.
*
* Copyright 2025 Slendi <slendi@socopon.com>
*
* 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 <std::size_t N, typename T>
requires std::is_arithmetic_v<T>
struct VecV;
struct Vec;
namespace detail {
@@ -41,27 +43,26 @@ template <std::size_t N> struct FixedString {
}
constexpr char operator[](std::size_t i) const { return data[i]; }
};
template <class X> struct is_vecv : std::false_type {};
template <std::size_t M, class U>
struct is_vecv<VecV<M, U>> : std::true_type {};
template <class X> struct is_Vec : std::false_type {};
template <std::size_t M, class U> struct is_Vec<Vec<M, U>> : std::true_type {};
template <class X>
inline constexpr bool is_vecv_v = is_vecv<std::remove_cvref_t<X>>::value;
inline constexpr bool is_Vec_v = is_Vec<std::remove_cvref_t<X>>::value;
template <class X>
inline constexpr bool is_scalar_v =
std::is_arithmetic_v<std::remove_cvref_t<X>>;
template <class X> struct vecv_size;
template <class X> struct Vec_size;
template <std::size_t M, class U>
struct vecv_size<VecV<M, U>> : std::integral_constant<std::size_t, M> {};
struct Vec_size<Vec<M, U>> : std::integral_constant<std::size_t, M> {};
} // namespace detail
template <std::size_t N, typename T = float>
requires std::is_arithmetic_v<T>
struct VecV : std::array<T, N> {
struct Vec : std::array<T, N> {
private:
template <class X> static consteval std::size_t extent() {
if constexpr (detail::is_vecv_v<X>)
return detail::vecv_size<std::remove_cvref_t<X>>::value;
if constexpr (detail::is_Vec_v<X>)
return detail::Vec_size<std::remove_cvref_t<X>>::value;
else if constexpr (detail::is_scalar_v<X>)
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 <typename... Args>
requires((detail::is_scalar_v<Args> || detail::is_vecv_v<Args>) && ...) &&
requires((detail::is_scalar_v<Args> || detail::is_Vec_v<Args>) && ...) &&
(total_extent<Args...>() == N) &&
(!(sizeof...(Args) == 1 && (detail::is_vecv_v<Args> && ...)))
constexpr VecV(Args &&...args) noexcept {
(!(sizeof...(Args) == 1 && (detail::is_Vec_v<Args> && ...)))
constexpr Vec(Args &&...args) noexcept {
std::size_t i = 0;
(fill_one(i, std::forward<Args>(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<N, T> const {
[[nodiscard]] constexpr auto normalized() noexcept -> Vec<N, T> const {
return (*this) / this->magnitude();
}
[[nodiscard]] constexpr auto normalize() noexcept -> VecV<N, T> const {
[[nodiscard]] constexpr auto normalize() noexcept -> Vec<N, T> const {
return this->normalized();
}
[[nodiscard]] constexpr auto unit() noexcept -> VecV<N, T> const {
[[nodiscard]] constexpr auto unit() noexcept -> Vec<N, T> const {
return this->normalized();
}
[[nodiscard]] constexpr auto dot(VecV<N, T> const &other) noexcept -> T {
[[nodiscard]] constexpr auto dot(Vec<N, T> 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 <class U = T>
[[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<std::is_floating_point_v<U>, 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 <typename U = T>
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 <class U>
requires(std::is_arithmetic_v<U> && N >= 1)
constexpr explicit(!std::is_convertible_v<U, T>)
VecV(VecV<N, U> const &other) noexcept {
Vec(Vec<N, U> const &other) noexcept {
for (std::size_t i = 0; i < N; ++i)
this->operator[](i) = static_cast<T>(other[i]);
}
@@ -259,8 +261,8 @@ public:
template <class U>
requires(std::is_arithmetic_v<U> && N >= 1)
constexpr explicit(!std::is_convertible_v<T, U>)
operator VecV<N, U>() const noexcept {
VecV<N, U> r{};
operator Vec<N, U>() const noexcept {
Vec<N, U> r{};
for (std::size_t i = 0; i < N; ++i)
r[i] = static_cast<U>((*this)[i]);
return r;
@@ -268,7 +270,7 @@ public:
template <class U>
requires(std::is_arithmetic_v<U> && !std::is_same_v<U, T>)
constexpr VecV &operator=(VecV<N, U> const &rhs) noexcept {
constexpr Vec &operator=(Vec<N, U> const &rhs) noexcept {
for (std::size_t i = 0; i < N; ++i)
(*this)[i] = static_cast<T>(rhs[i]);
return *this;
@@ -285,42 +287,41 @@ private:
(*this)[i++] = static_cast<T>(v);
}
template <std::size_t M, class U>
constexpr void fill_one(std::size_t &i, const VecV<M, U> &v) noexcept {
constexpr void fill_one(std::size_t &i, const Vec<M, U> &v) noexcept {
for (std::size_t k = 0; k < M; ++k)
(*this)[i++] = static_cast<T>(v[k]);
}
#endif // SMATH_IMPLICIT_CONVERSIONS
template <std::size_t M>
constexpr void fill_one(std::size_t &i, const VecV<M, T> &v) noexcept {
constexpr void fill_one(std::size_t &i, const Vec<M, T> &v) noexcept {
for (std::size_t k = 0; k < M; ++k)
(*this)[i++] = static_cast<T>(v[k]);
}
};
template <size_t I, size_t N, class T>
constexpr T &get(VecV<N, T> &v) noexcept {
template <size_t I, size_t N, class T> constexpr T &get(Vec<N, T> &v) noexcept {
static_assert(I < N);
return v[I];
}
template <size_t I, size_t N, class T>
constexpr const T &get(const VecV<N, T> &v) noexcept {
constexpr const T &get(const Vec<N, T> &v) noexcept {
static_assert(I < N);
return v[I];
}
template <size_t I, size_t N, class T>
constexpr T &&get(VecV<N, T> &&v) noexcept {
constexpr T &&get(Vec<N, T> &&v) noexcept {
static_assert(I < N);
return std::move(v[I]);
}
template <size_t I, size_t N, class T>
constexpr const T &&get(const VecV<N, T> &&v) noexcept {
constexpr const T &&get(const Vec<N, T> &&v) noexcept {
static_assert(I < N);
return std::move(v[I]);
}
template <std::size_t N, typename T = float>
requires std::is_arithmetic_v<T>
using Vec = std::conditional_t<N == 1, T, VecV<N, T>>;
using VecOrScalar = std::conditional_t<N == 1, T, Vec<N, T>>;
namespace detail {
@@ -358,12 +359,12 @@ constexpr auto is_valid(char c) -> bool {
}
template <detail::FixedString S, std::size_t N, typename T, std::size_t... I>
constexpr auto swizzle_impl(VecV<N, T> const &v, std::index_sequence<I...>)
-> Vec<S.size, T> {
constexpr auto swizzle_impl(Vec<N, T> const &v, std::index_sequence<I...>)
-> VecOrScalar<S.size, T> {
static_assert(((is_valid(S[I])) && ...), "Invalid swizzle component");
static_assert(((char_to_idx(S[I]) < N) && ...),
"Pattern index out of bounds");
Vec<S.size, T> out{};
VecOrScalar<S.size, T> out{};
std::size_t i = 0;
((out[i++] = v[char_to_idx(S[I])]), ...);
return out;
@@ -387,29 +388,29 @@ concept ValidSwizzle =
template <detail::FixedString S, std::size_t N, typename T>
requires detail::ValidSwizzle<S, N>
constexpr auto swizzle(VecV<N, T> const &v) -> Vec<S.size, T> {
constexpr auto swizzle(Vec<N, T> const &v) -> VecOrScalar<S.size, T> {
return detail::swizzle_impl<S>(v, std::make_index_sequence<S.size>{});
}
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 <std::size_t N, typename T>
requires std::formattable<T, char>
struct std::formatter<smath::VecV<N, T>> : std::formatter<T> {
struct std::formatter<smath::Vec<N, T>> : std::formatter<T> {
constexpr auto parse(std::format_parse_context &ctx) {
return std::formatter<T>::parse(ctx);
}
template <typename Ctx>
auto format(smath::VecV<N, T> const &v, Ctx &ctx) const {
auto format(smath::Vec<N, T> 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<smath::VecV<N, T>> : std::formatter<T> {
namespace std {
template <size_t N, class T>
struct tuple_size<smath::VecV<N, T>> : std::integral_constant<size_t, N> {};
struct tuple_size<smath::Vec<N, T>> : std::integral_constant<size_t, N> {};
template <size_t I, size_t N, class T>
struct tuple_element<I, smath::VecV<N, T>> {
struct tuple_element<I, smath::Vec<N, T>> {
static_assert(I < N);
using type = T;
};

179
tests/vec.cpp Normal file
View File

@@ -0,0 +1,179 @@
#include <format>
#include <string>
#include <type_traits>
#include <gtest/gtest.h>
#include <smath.hpp>
using smath::Vec;
using smath::Vec2;
using smath::Vec3;
using smath::Vec4;
template <class T>
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<Vec3> == 3);
static_assert(std::is_same_v<std::tuple_element_t<1, Vec3>, 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<smath::Vec<3, int>>(vf);
EXPECT_EQ(vi2[0], 1);
EXPECT_EQ(vi2[1], 2);
EXPECT_EQ(vi2[2], 3);
}