mirror of
https://github.com/orange-cpp/omath.git
synced 2026-02-13 07:03:25 +00:00
Temp (#123)
* Coverage * added fixes * removed spacing * removed junk * removed print * removed coverage * removed useless stuff * fix --------- Co-authored-by: Saikari <lin@sz.cn.eu.org>
This commit is contained in:
@@ -1,8 +1,125 @@
|
||||
//
|
||||
// Created by Vlad on 18.08.2024.
|
||||
//
|
||||
// Extra unit tests for the project's A* implementation
|
||||
#include <gtest/gtest.h>
|
||||
#include <omath/pathfinding/a_star.hpp>
|
||||
#include <omath/pathfinding/navigation_mesh.hpp>
|
||||
#include <array>
|
||||
#include <utility>
|
||||
|
||||
using namespace omath;
|
||||
using namespace omath::pathfinding;
|
||||
|
||||
TEST(AStarExtra, TrivialNeighbor)
|
||||
{
|
||||
NavigationMesh nav;
|
||||
Vector3<float> v1{0.f,0.f,0.f};
|
||||
Vector3<float> v2{1.f,0.f,0.f};
|
||||
nav.m_vertex_map[v1] = {v2};
|
||||
nav.m_vertex_map[v2] = {v1};
|
||||
|
||||
auto path = Astar::find_path(v1, v2, nav);
|
||||
ASSERT_EQ(path.size(), 1u);
|
||||
EXPECT_EQ(path.front(), v2);
|
||||
}
|
||||
|
||||
TEST(AStarExtra, StartEqualsGoal)
|
||||
{
|
||||
NavigationMesh nav;
|
||||
Vector3<float> v{1.f,1.f,0.f};
|
||||
nav.m_vertex_map[v] = {};
|
||||
|
||||
auto path = Astar::find_path(v, v, nav);
|
||||
ASSERT_EQ(path.size(), 1u);
|
||||
EXPECT_EQ(path.front(), v);
|
||||
}
|
||||
|
||||
TEST(AStarExtra, BlockedNoPathBetweenTwoVertices)
|
||||
{
|
||||
NavigationMesh nav;
|
||||
Vector3<float> left{0.f,0.f,0.f};
|
||||
Vector3<float> right{2.f,0.f,0.f};
|
||||
// both vertices present but no connections
|
||||
nav.m_vertex_map[left] = {};
|
||||
nav.m_vertex_map[right] = {};
|
||||
|
||||
auto path = Astar::find_path(left, right, nav);
|
||||
// disconnected vertices -> empty result
|
||||
EXPECT_TRUE(path.empty());
|
||||
}
|
||||
|
||||
TEST(AStarExtra, LongerPathAvoidsBlock)
|
||||
{
|
||||
NavigationMesh nav;
|
||||
// build 3x3 grid of vertices, block center (1,1)
|
||||
auto idx = [&](int x, int y){ return Vector3<float>{static_cast<float>(x), static_cast<float>(y), 0.f}; };
|
||||
for (int y = 0; y < 3; ++y)
|
||||
{
|
||||
for (int x = 0; x < 3; ++x)
|
||||
{
|
||||
Vector3<float> v = idx(x,y);
|
||||
if (x==1 && y==1) continue; // center is omitted (blocked)
|
||||
std::vector<Vector3<float>> neigh;
|
||||
const std::array<std::pair<int,int>,4> offs{{{1,0},{-1,0},{0,1},{0,-1}}};
|
||||
for (auto [dx,dy]: offs)
|
||||
{
|
||||
int nx = x + dx, ny = y + dy;
|
||||
if (nx < 0 || nx >= 3 || ny < 0 || ny >= 3) continue;
|
||||
if (nx==1 && ny==1) continue; // neighbor is the blocked center
|
||||
neigh.push_back(idx(nx,ny));
|
||||
}
|
||||
nav.m_vertex_map[v] = neigh;
|
||||
}
|
||||
}
|
||||
|
||||
Vector3<float> start = idx(0,1);
|
||||
Vector3<float> goal = idx(2,1);
|
||||
auto path = Astar::find_path(start, goal, nav);
|
||||
ASSERT_FALSE(path.empty());
|
||||
EXPECT_EQ(path.front(), goal); // Astar convention: single-element or endpoint present
|
||||
}
|
||||
|
||||
|
||||
TEST(AstarTests, TrivialDirectNeighborPath)
|
||||
{
|
||||
NavigationMesh nav;
|
||||
// create two vertices directly connected
|
||||
Vector3<float> v1{0.f,0.f,0.f};
|
||||
Vector3<float> v2{1.f,0.f,0.f};
|
||||
nav.m_vertex_map.emplace(v1, std::vector<Vector3<float>>{v2});
|
||||
nav.m_vertex_map.emplace(v2, std::vector<Vector3<float>>{v1});
|
||||
|
||||
auto path = Astar::find_path(v1, v2, nav);
|
||||
// Current A* implementation returns the end vertex as the reconstructed
|
||||
// path (single-element) in the simple neighbor scenario. Assert that the
|
||||
// endpoint is present and reachable.
|
||||
ASSERT_EQ(path.size(), 1u);
|
||||
EXPECT_EQ(path.front(), v2);
|
||||
}
|
||||
|
||||
TEST(AstarTests, NoPathWhenDisconnected)
|
||||
{
|
||||
NavigationMesh nav;
|
||||
Vector3<float> v1{0.f,0.f,0.f};
|
||||
Vector3<float> v2{10.f,0.f,0.f};
|
||||
// nav has only v1
|
||||
nav.m_vertex_map.emplace(v1, std::vector<Vector3<float>>{});
|
||||
|
||||
auto path = Astar::find_path(v1, v2, nav);
|
||||
// When the nav mesh contains only the start vertex, the closest
|
||||
// vertex for both start and end will be the same vertex. In that
|
||||
// case Astar returns a single-element path with the start vertex.
|
||||
ASSERT_EQ(path.size(), 1u);
|
||||
EXPECT_EQ(path.front(), v1);
|
||||
}
|
||||
|
||||
TEST(AstarTests, EmptyNavReturnsNoPath)
|
||||
{
|
||||
NavigationMesh nav;
|
||||
Vector3<float> v1{0.f,0.f,0.f};
|
||||
Vector3<float> v2{1.f,0.f,0.f};
|
||||
|
||||
auto path = Astar::find_path(v1, v2, nav);
|
||||
EXPECT_TRUE(path.empty());
|
||||
}
|
||||
|
||||
TEST(unit_test_a_star, finding_right_path)
|
||||
{
|
||||
|
||||
89
tests/general/unit_test_collision_extra.cpp
Normal file
89
tests/general/unit_test_collision_extra.cpp
Normal file
@@ -0,0 +1,89 @@
|
||||
// Extra collision tests: Simplex, MeshCollider, EPA
|
||||
#include <gtest/gtest.h>
|
||||
#include <omath/collision/simplex.hpp>
|
||||
#include <omath/collision/mesh_collider.hpp>
|
||||
#include <omath/collision/epa_algorithm.hpp>
|
||||
#include <omath/engines/source_engine/collider.hpp>
|
||||
|
||||
using namespace omath;
|
||||
using namespace omath::collision;
|
||||
|
||||
TEST(CollisionExtra, SimplexLineHandle)
|
||||
{
|
||||
Simplex<Vector3<float>> s;
|
||||
s = { Vector3<float>{1.f,0.f,0.f}, Vector3<float>{2.f,0.f,0.f} };
|
||||
Vector3<float> dir{0,0,0};
|
||||
EXPECT_FALSE(s.handle(dir));
|
||||
// direction should not be zero
|
||||
EXPECT_GT(dir.length_sqr(), 0.0f);
|
||||
}
|
||||
|
||||
TEST(CollisionExtra, SimplexTriangleHandle)
|
||||
{
|
||||
Simplex<Vector3<float>> s;
|
||||
s = { Vector3<float>{1.f,0.f,0.f}, Vector3<float>{0.f,1.f,0.f}, Vector3<float>{0.f,0.f,1.f} };
|
||||
Vector3<float> dir{0,0,0};
|
||||
EXPECT_FALSE(s.handle(dir));
|
||||
EXPECT_GT(dir.length_sqr(), 0.0f);
|
||||
}
|
||||
|
||||
TEST(CollisionExtra, SimplexTetrahedronInside)
|
||||
{
|
||||
Simplex<Vector3<float>> s;
|
||||
// tetra that surrounds origin roughly
|
||||
s = { Vector3<float>{1.f,0.f,0.f}, Vector3<float>{0.f,1.f,0.f}, Vector3<float>{0.f,0.f,1.f}, Vector3<float>{-1.f,-1.f,-1.f} };
|
||||
Vector3<float> dir{0,0,0};
|
||||
// if origin inside, handle returns true
|
||||
const bool inside = s.handle(dir);
|
||||
EXPECT_TRUE(inside);
|
||||
}
|
||||
|
||||
TEST(CollisionExtra, MeshColliderOriginAndFurthest)
|
||||
{
|
||||
omath::source_engine::Mesh mesh = {
|
||||
std::vector<omath::primitives::Vertex<>>{
|
||||
{ { 1.f, 1.f, 1.f }, {}, {} },
|
||||
{ {-1.f, -1.f, -1.f }, {}, {} }
|
||||
},
|
||||
{}
|
||||
};
|
||||
mesh.set_origin({0, 2, 0});
|
||||
omath::source_engine::MeshCollider collider(mesh);
|
||||
|
||||
EXPECT_EQ(collider.get_origin(), omath::Vector3<float>(0,2,0));
|
||||
collider.set_origin({1,2,3});
|
||||
EXPECT_EQ(collider.get_origin(), omath::Vector3<float>(1,2,3));
|
||||
|
||||
const auto v = collider.find_abs_furthest_vertex_position({1.f,0.f,0.f});
|
||||
// the original vertex at (1,1,1) translated by origin (1,2,3) becomes (2,3,4)
|
||||
EXPECT_EQ(v, omath::Vector3<float>(2.f,3.f,4.f));
|
||||
}
|
||||
|
||||
TEST(CollisionExtra, EPAConvergesOnSimpleCase)
|
||||
{
|
||||
// Build two simple colliders using simple meshes that overlap
|
||||
omath::source_engine::Mesh meshA = {
|
||||
std::vector<omath::primitives::Vertex<>>{{ {0.f,0.f,0.f}, {}, {} }, { {1.f,0.f,0.f}, {}, {} } },
|
||||
{}
|
||||
};
|
||||
omath::source_engine::Mesh meshB = meshA;
|
||||
meshB.set_origin({0.5f, 0.f, 0.f}); // translate to overlap
|
||||
|
||||
omath::source_engine::MeshCollider A(meshA);
|
||||
omath::source_engine::MeshCollider B(meshB);
|
||||
|
||||
// Create a simplex that approximately contains the origin in Minkowski space
|
||||
Simplex<omath::Vector3<float>> simplex;
|
||||
simplex = { omath::Vector3<float>{0.5f,0.f,0.f}, omath::Vector3<float>{-0.5f,0.f,0.f}, omath::Vector3<float>{0.f,0.5f,0.f}, omath::Vector3<float>{0.f,-0.5f,0.f} };
|
||||
|
||||
auto pool = std::pmr::monotonic_buffer_resource(1024);
|
||||
auto res = Epa<omath::source_engine::MeshCollider>::solve(A, B, simplex, {}, pool);
|
||||
// EPA may or may not converge depending on numerics; ensure it returns optionally
|
||||
// but if it does, fields should be finite
|
||||
if (res.has_value())
|
||||
{
|
||||
auto r = *res;
|
||||
EXPECT_TRUE(std::isfinite(r.depth));
|
||||
EXPECT_GT(r.normal.length_sqr(), 0.0f);
|
||||
}
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
//
|
||||
// Created by Vlad on 01.09.2024.
|
||||
//
|
||||
#include <omath/utility/color.hpp>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
using namespace omath;
|
||||
|
||||
class unit_test_color : public ::testing::Test
|
||||
{
|
||||
protected:
|
||||
Color color1;
|
||||
Color color2;
|
||||
|
||||
void SetUp() override
|
||||
{
|
||||
color1 = Color::red();
|
||||
color2 = Color::green();
|
||||
}
|
||||
};
|
||||
|
||||
// Test constructors
|
||||
TEST_F(unit_test_color, Constructor_Float)
|
||||
{
|
||||
constexpr Color color(0.5f, 0.5f, 0.5f, 1.0f);
|
||||
EXPECT_FLOAT_EQ(color.x, 0.5f);
|
||||
EXPECT_FLOAT_EQ(color.y, 0.5f);
|
||||
EXPECT_FLOAT_EQ(color.z, 0.5f);
|
||||
EXPECT_FLOAT_EQ(color.w, 1.0f);
|
||||
}
|
||||
|
||||
TEST_F(unit_test_color, Constructor_Vector4)
|
||||
{
|
||||
constexpr omath::Vector4 vec(0.2f, 0.4f, 0.6f, 0.8f);
|
||||
constexpr Color color(vec);
|
||||
EXPECT_FLOAT_EQ(color.x, 0.2f);
|
||||
EXPECT_FLOAT_EQ(color.y, 0.4f);
|
||||
EXPECT_FLOAT_EQ(color.z, 0.6f);
|
||||
EXPECT_FLOAT_EQ(color.w, 0.8f);
|
||||
}
|
||||
|
||||
// Test static methods for color creation
|
||||
TEST_F(unit_test_color, FromRGBA)
|
||||
{
|
||||
constexpr Color color = Color::from_rgba(128, 64, 32, 255);
|
||||
EXPECT_FLOAT_EQ(color.x, 128.0f / 255.0f);
|
||||
EXPECT_FLOAT_EQ(color.y, 64.0f / 255.0f);
|
||||
EXPECT_FLOAT_EQ(color.z, 32.0f / 255.0f);
|
||||
EXPECT_FLOAT_EQ(color.w, 1.0f);
|
||||
}
|
||||
|
||||
TEST_F(unit_test_color, FromHSV)
|
||||
{
|
||||
constexpr Color color = Color::from_hsv(0.0f, 1.0f, 1.0f); // Red in HSV
|
||||
EXPECT_FLOAT_EQ(color.x, 1.0f);
|
||||
EXPECT_FLOAT_EQ(color.y, 0.0f);
|
||||
EXPECT_FLOAT_EQ(color.z, 0.0f);
|
||||
EXPECT_FLOAT_EQ(color.w, 1.0f);
|
||||
}
|
||||
|
||||
// Test HSV conversion
|
||||
TEST_F(unit_test_color, ToHSV)
|
||||
{
|
||||
const auto [hue, saturation, value] = color1.to_hsv(); // Red color
|
||||
EXPECT_FLOAT_EQ(hue, 0.0f);
|
||||
EXPECT_FLOAT_EQ(saturation, 1.0f);
|
||||
EXPECT_FLOAT_EQ(value, 1.0f);
|
||||
}
|
||||
|
||||
// Test color blending
|
||||
TEST_F(unit_test_color, Blend)
|
||||
{
|
||||
const Color blended = color1.blend(color2, 0.5f);
|
||||
EXPECT_FLOAT_EQ(blended.x, 0.5f);
|
||||
EXPECT_FLOAT_EQ(blended.y, 0.5f);
|
||||
EXPECT_FLOAT_EQ(blended.z, 0.0f);
|
||||
EXPECT_FLOAT_EQ(blended.w, 1.0f);
|
||||
}
|
||||
|
||||
// Test predefined colors
|
||||
TEST_F(unit_test_color, PredefinedColors)
|
||||
{
|
||||
constexpr Color red = Color::red();
|
||||
constexpr Color green = Color::green();
|
||||
constexpr Color blue = Color::blue();
|
||||
|
||||
EXPECT_FLOAT_EQ(red.x, 1.0f);
|
||||
EXPECT_FLOAT_EQ(red.y, 0.0f);
|
||||
EXPECT_FLOAT_EQ(red.z, 0.0f);
|
||||
EXPECT_FLOAT_EQ(red.w, 1.0f);
|
||||
|
||||
EXPECT_FLOAT_EQ(green.x, 0.0f);
|
||||
EXPECT_FLOAT_EQ(green.y, 1.0f);
|
||||
EXPECT_FLOAT_EQ(green.z, 0.0f);
|
||||
EXPECT_FLOAT_EQ(green.w, 1.0f);
|
||||
|
||||
EXPECT_FLOAT_EQ(blue.x, 0.0f);
|
||||
EXPECT_FLOAT_EQ(blue.y, 0.0f);
|
||||
EXPECT_FLOAT_EQ(blue.z, 1.0f);
|
||||
EXPECT_FLOAT_EQ(blue.w, 1.0f);
|
||||
}
|
||||
|
||||
// Test non-member function: Blend for Vector3
|
||||
TEST_F(unit_test_color, BlendVector3)
|
||||
{
|
||||
constexpr Color v1(1.0f, 0.0f, 0.0f, 1.f); // Red
|
||||
constexpr Color v2(0.0f, 1.0f, 0.0f, 1.f); // Green
|
||||
constexpr Color blended = v1.blend(v2, 0.5f);
|
||||
EXPECT_FLOAT_EQ(blended.x, 0.5f);
|
||||
EXPECT_FLOAT_EQ(blended.y, 0.5f);
|
||||
EXPECT_FLOAT_EQ(blended.z, 0.0f);
|
||||
}
|
||||
293
tests/general/unit_test_color_grouped.cpp
Normal file
293
tests/general/unit_test_color_grouped.cpp
Normal file
@@ -0,0 +1,293 @@
|
||||
// Combined color tests
|
||||
// This file merges multiple color-related unit test files into one grouped TU
|
||||
// to make the tests look more organized.
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include <omath/utility/color.hpp>
|
||||
#include <format>
|
||||
#include <algorithm>
|
||||
|
||||
using namespace omath;
|
||||
|
||||
class UnitTestColorGrouped : public ::testing::Test
|
||||
{
|
||||
protected:
|
||||
Color color1;
|
||||
Color color2;
|
||||
|
||||
void SetUp() override
|
||||
{
|
||||
color1 = Color::red();
|
||||
color2 = Color::green();
|
||||
}
|
||||
};
|
||||
|
||||
// From original unit_test_color.cpp
|
||||
TEST_F(UnitTestColorGrouped, Constructor_Float)
|
||||
{
|
||||
constexpr Color color(0.5f, 0.5f, 0.5f, 1.0f);
|
||||
EXPECT_FLOAT_EQ(color.x, 0.5f);
|
||||
EXPECT_FLOAT_EQ(color.y, 0.5f);
|
||||
EXPECT_FLOAT_EQ(color.z, 0.5f);
|
||||
EXPECT_FLOAT_EQ(color.w, 1.0f);
|
||||
}
|
||||
|
||||
TEST_F(UnitTestColorGrouped, Constructor_Vector4)
|
||||
{
|
||||
constexpr omath::Vector4 vec(0.2f, 0.4f, 0.6f, 0.8f);
|
||||
constexpr Color color(vec);
|
||||
EXPECT_FLOAT_EQ(color.x, 0.2f);
|
||||
EXPECT_FLOAT_EQ(color.y, 0.4f);
|
||||
EXPECT_FLOAT_EQ(color.z, 0.6f);
|
||||
EXPECT_FLOAT_EQ(color.w, 0.8f);
|
||||
}
|
||||
|
||||
TEST_F(UnitTestColorGrouped, FromRGBA)
|
||||
{
|
||||
constexpr Color color = Color::from_rgba(128, 64, 32, 255);
|
||||
EXPECT_FLOAT_EQ(color.x, 128.0f / 255.0f);
|
||||
EXPECT_FLOAT_EQ(color.y, 64.0f / 255.0f);
|
||||
EXPECT_FLOAT_EQ(color.z, 32.0f / 255.0f);
|
||||
EXPECT_FLOAT_EQ(color.w, 1.0f);
|
||||
}
|
||||
|
||||
TEST_F(UnitTestColorGrouped, FromHSV)
|
||||
{
|
||||
constexpr Color color = Color::from_hsv(0.0f, 1.0f, 1.0f); // Red in HSV
|
||||
EXPECT_FLOAT_EQ(color.x, 1.0f);
|
||||
EXPECT_FLOAT_EQ(color.y, 0.0f);
|
||||
EXPECT_FLOAT_EQ(color.z, 0.0f);
|
||||
EXPECT_FLOAT_EQ(color.w, 1.0f);
|
||||
}
|
||||
|
||||
TEST_F(UnitTestColorGrouped, ToHSV)
|
||||
{
|
||||
const auto [hue, saturation, value] = color1.to_hsv(); // Red color
|
||||
EXPECT_FLOAT_EQ(hue, 0.0f);
|
||||
EXPECT_FLOAT_EQ(saturation, 1.0f);
|
||||
EXPECT_FLOAT_EQ(value, 1.0f);
|
||||
}
|
||||
|
||||
TEST_F(UnitTestColorGrouped, Blend)
|
||||
{
|
||||
const Color blended = color1.blend(color2, 0.5f);
|
||||
EXPECT_FLOAT_EQ(blended.x, 0.5f);
|
||||
EXPECT_FLOAT_EQ(blended.y, 0.5f);
|
||||
EXPECT_FLOAT_EQ(blended.z, 0.0f);
|
||||
EXPECT_FLOAT_EQ(blended.w, 1.0f);
|
||||
}
|
||||
|
||||
TEST_F(UnitTestColorGrouped, PredefinedColors)
|
||||
{
|
||||
constexpr Color red = Color::red();
|
||||
constexpr Color green = Color::green();
|
||||
constexpr Color blue = Color::blue();
|
||||
|
||||
EXPECT_FLOAT_EQ(red.x, 1.0f);
|
||||
EXPECT_FLOAT_EQ(red.y, 0.0f);
|
||||
EXPECT_FLOAT_EQ(red.z, 0.0f);
|
||||
EXPECT_FLOAT_EQ(red.w, 1.0f);
|
||||
|
||||
EXPECT_FLOAT_EQ(green.x, 0.0f);
|
||||
EXPECT_FLOAT_EQ(green.y, 1.0f);
|
||||
EXPECT_FLOAT_EQ(green.z, 0.0f);
|
||||
EXPECT_FLOAT_EQ(green.w, 1.0f);
|
||||
|
||||
EXPECT_FLOAT_EQ(blue.x, 0.0f);
|
||||
EXPECT_FLOAT_EQ(blue.y, 0.0f);
|
||||
EXPECT_FLOAT_EQ(blue.z, 1.0f);
|
||||
EXPECT_FLOAT_EQ(blue.w, 1.0f);
|
||||
}
|
||||
|
||||
TEST_F(UnitTestColorGrouped, BlendVector3)
|
||||
{
|
||||
constexpr Color v1(1.0f, 0.0f, 0.0f, 1.f); // Red
|
||||
constexpr Color v2(0.0f, 1.0f, 0.0f, 1.f); // Green
|
||||
constexpr Color blended = v1.blend(v2, 0.5f);
|
||||
EXPECT_FLOAT_EQ(blended.x, 0.5f);
|
||||
EXPECT_FLOAT_EQ(blended.y, 0.5f);
|
||||
EXPECT_FLOAT_EQ(blended.z, 0.0f);
|
||||
}
|
||||
|
||||
// From unit_test_color_extra.cpp
|
||||
TEST(UnitTestColorGrouped_Extra, SetHueSaturationValue)
|
||||
{
|
||||
Color c = Color::red();
|
||||
auto h1 = c.to_hsv();
|
||||
EXPECT_FLOAT_EQ(h1.hue, 0.f);
|
||||
|
||||
c.set_hue(0.5f);
|
||||
auto h2 = c.to_hsv();
|
||||
EXPECT_NEAR(h2.hue, 0.5f, 1e-3f);
|
||||
|
||||
c = Color::from_hsv(0.25f, 0.8f, 0.6f);
|
||||
c.set_saturation(0.3f);
|
||||
auto h3 = c.to_hsv();
|
||||
EXPECT_NEAR(h3.saturation, 0.3f, 1e-3f);
|
||||
|
||||
c.set_value(1.0f);
|
||||
auto h4 = c.to_hsv();
|
||||
EXPECT_NEAR(h4.value, 1.0f, 1e-3f);
|
||||
}
|
||||
|
||||
TEST(UnitTestColorGrouped_Extra, ToStringVariants)
|
||||
{
|
||||
Color c = Color::from_rgba(10, 20, 30, 255);
|
||||
auto s = c.to_string();
|
||||
EXPECT_NE(s.find("r:"), std::string::npos);
|
||||
|
||||
auto ws = c.to_wstring();
|
||||
EXPECT_FALSE(ws.empty());
|
||||
|
||||
auto u8 = c.to_u8string();
|
||||
EXPECT_FALSE(u8.empty());
|
||||
}
|
||||
|
||||
TEST(UnitTestColorGrouped_Extra, BlendEdgeCases)
|
||||
{
|
||||
Color a = Color::red();
|
||||
Color b = Color::blue();
|
||||
auto r0 = a.blend(b, 0.f);
|
||||
EXPECT_FLOAT_EQ(r0.x, a.x);
|
||||
auto r1 = a.blend(b, 1.f);
|
||||
EXPECT_FLOAT_EQ(r1.x, b.x);
|
||||
}
|
||||
|
||||
// From unit_test_color_more.cpp
|
||||
TEST(UnitTestColorGrouped_More, DefaultCtorIsZero)
|
||||
{
|
||||
Color c;
|
||||
EXPECT_FLOAT_EQ(c.x, 0.0f);
|
||||
EXPECT_FLOAT_EQ(c.y, 0.0f);
|
||||
EXPECT_FLOAT_EQ(c.z, 0.0f);
|
||||
EXPECT_FLOAT_EQ(c.w, 0.0f);
|
||||
}
|
||||
|
||||
TEST(UnitTestColorGrouped_More, FloatCtorAndClampForRGB)
|
||||
{
|
||||
Color c(1.2f, -0.5f, 0.5f, 2.0f);
|
||||
EXPECT_FLOAT_EQ(c.x, 1.0f);
|
||||
EXPECT_FLOAT_EQ(c.y, 0.0f);
|
||||
EXPECT_FLOAT_EQ(c.z, 0.5f);
|
||||
EXPECT_FLOAT_EQ(c.w, 2.0f);
|
||||
}
|
||||
|
||||
TEST(UnitTestColorGrouped_More, FromRgbaProducesScaledComponents)
|
||||
{
|
||||
Color c = Color::from_rgba(25u, 128u, 230u, 64u);
|
||||
EXPECT_NEAR(c.x, 25.0f/255.0f, 1e-6f);
|
||||
EXPECT_NEAR(c.y, 128.0f/255.0f, 1e-6f);
|
||||
EXPECT_NEAR(c.z, 230.0f/255.0f, 1e-6f);
|
||||
EXPECT_NEAR(c.w, 64.0f/255.0f, 1e-6f);
|
||||
}
|
||||
|
||||
TEST(UnitTestColorGrouped_More, BlendProducesIntermediate)
|
||||
{
|
||||
Color c0(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
Color c1(1.0f, 1.0f, 1.0f, 0.0f);
|
||||
Color mid = c0.blend(c1, 0.5f);
|
||||
EXPECT_FLOAT_EQ(mid.x, 0.5f);
|
||||
EXPECT_FLOAT_EQ(mid.y, 0.5f);
|
||||
EXPECT_FLOAT_EQ(mid.z, 0.5f);
|
||||
EXPECT_FLOAT_EQ(mid.w, 0.5f);
|
||||
}
|
||||
|
||||
TEST(UnitTestColorGrouped_More, HsvRoundTrip)
|
||||
{
|
||||
Color red = Color::red();
|
||||
auto hsv = red.to_hsv();
|
||||
Color back = Color::from_hsv(hsv);
|
||||
EXPECT_NEAR(back.x, 1.0f, 1e-6f);
|
||||
EXPECT_NEAR(back.y, 0.0f, 1e-6f);
|
||||
EXPECT_NEAR(back.z, 0.0f, 1e-6f);
|
||||
}
|
||||
|
||||
TEST(UnitTestColorGrouped_More, ToStringContainsComponents)
|
||||
{
|
||||
Color c = Color::from_rgba(10, 20, 30, 40);
|
||||
std::string s = c.to_string();
|
||||
EXPECT_NE(s.find("r:"), std::string::npos);
|
||||
EXPECT_NE(s.find("g:"), std::string::npos);
|
||||
EXPECT_NE(s.find("b:"), std::string::npos);
|
||||
EXPECT_NE(s.find("a:"), std::string::npos);
|
||||
}
|
||||
|
||||
// From unit_test_color_more2.cpp
|
||||
TEST(UnitTestColorGrouped_More2, FromRgbaAndToString)
|
||||
{
|
||||
auto c = Color::from_rgba(255, 128, 0, 64);
|
||||
const auto s = c.to_string();
|
||||
EXPECT_NE(s.find("r:255"), std::string::npos);
|
||||
EXPECT_NE(s.find("g:128"), std::string::npos);
|
||||
EXPECT_NE(s.find("b:0"), std::string::npos);
|
||||
EXPECT_NE(s.find("a:64"), std::string::npos);
|
||||
}
|
||||
|
||||
TEST(UnitTestColorGrouped_More2, FromHsvCases)
|
||||
{
|
||||
const float eps = 1e-5f;
|
||||
|
||||
auto check_hue = [&](float h) {
|
||||
SCOPED_TRACE(::testing::Message() << "h=" << h);
|
||||
Color c = Color::from_hsv(h, 1.f, 1.f);
|
||||
EXPECT_TRUE(std::isfinite(c.x));
|
||||
EXPECT_TRUE(std::isfinite(c.y));
|
||||
EXPECT_TRUE(std::isfinite(c.z));
|
||||
EXPECT_GE(c.x, -eps);
|
||||
EXPECT_LE(c.x, 1.f + eps);
|
||||
EXPECT_GE(c.y, -eps);
|
||||
EXPECT_LE(c.y, 1.f + eps);
|
||||
EXPECT_GE(c.z, -eps);
|
||||
EXPECT_LE(c.z, 1.f + eps);
|
||||
|
||||
float mx = std::max({c.x, c.y, c.z});
|
||||
float mn = std::min({c.x, c.y, c.z});
|
||||
EXPECT_GE(mx, 0.999f);
|
||||
EXPECT_LE(mn, 1e-3f + 1e-4f);
|
||||
};
|
||||
|
||||
check_hue(0.f / 6.f);
|
||||
check_hue(1.f / 6.f);
|
||||
check_hue(2.f / 6.f);
|
||||
check_hue(3.f / 6.f);
|
||||
check_hue(4.f / 6.f);
|
||||
check_hue(5.f / 6.f);
|
||||
}
|
||||
|
||||
TEST(UnitTestColorGrouped_More2, ToHsvAndSetters)
|
||||
{
|
||||
Color c{0.2f, 0.4f, 0.6f, 1.f};
|
||||
auto hsv = c.to_hsv();
|
||||
EXPECT_NEAR(hsv.value, 0.6f, 1e-6f);
|
||||
|
||||
c.set_hue(0.0f);
|
||||
EXPECT_TRUE(std::isfinite(c.x));
|
||||
|
||||
c.set_saturation(0.0f);
|
||||
EXPECT_TRUE(std::isfinite(c.y));
|
||||
|
||||
c.set_value(0.5f);
|
||||
EXPECT_TRUE(std::isfinite(c.z));
|
||||
}
|
||||
|
||||
TEST(UnitTestColorGrouped_More2, BlendAndStaticColors)
|
||||
{
|
||||
Color a = Color::red();
|
||||
Color b = Color::blue();
|
||||
auto mid = a.blend(b, 0.5f);
|
||||
EXPECT_GT(mid.x, 0.f);
|
||||
EXPECT_GT(mid.z, 0.f);
|
||||
|
||||
auto all_a = a.blend(b, -1.f);
|
||||
EXPECT_NEAR(all_a.x, a.x, 1e-6f);
|
||||
|
||||
auto all_b = a.blend(b, 2.f);
|
||||
EXPECT_NEAR(all_b.z, b.z, 1e-6f);
|
||||
}
|
||||
|
||||
TEST(UnitTestColorGrouped_More2, FormatterUsesToString)
|
||||
{
|
||||
Color c = Color::from_rgba(10, 20, 30, 40);
|
||||
const auto formatted = std::format("{}", c);
|
||||
EXPECT_NE(formatted.find("r:10"), std::string::npos);
|
||||
}
|
||||
46
tests/general/unit_test_epa_internal.cpp
Normal file
46
tests/general/unit_test_epa_internal.cpp
Normal file
@@ -0,0 +1,46 @@
|
||||
#include "omath/collision/epa_algorithm.hpp"
|
||||
#include "omath/collision/simplex.hpp"
|
||||
#include "omath/linear_algebra/vector3.hpp"
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
using Vector3f = omath::Vector3<float>;
|
||||
|
||||
// Dummy collider type that exposes VectorType and returns small offsets
|
||||
struct DummyCollider
|
||||
{
|
||||
using VectorType = Vector3f;
|
||||
VectorType find_abs_furthest_vertex_position(const VectorType& dir) const noexcept
|
||||
{
|
||||
// map direction to a small point so support_point is finite
|
||||
return Vector3f{dir.x * 0.01f, dir.y * 0.01f, dir.z * 0.01f};
|
||||
}
|
||||
};
|
||||
|
||||
using EpaDummy = omath::collision::Epa<DummyCollider>;
|
||||
using Simplex = omath::collision::Simplex<Vector3f>;
|
||||
|
||||
TEST(EpaInternal, SolveHandlesSmallPolytope)
|
||||
{
|
||||
// Create a simplex that is nearly degenerate but valid for solve
|
||||
Simplex s;
|
||||
s = { Vector3f{0.01f, 0.f, 0.f}, Vector3f{0.f, 0.01f, 0.f}, Vector3f{0.f, 0.f, 0.01f}, Vector3f{-0.01f, -0.01f, -0.01f} };
|
||||
|
||||
DummyCollider a, b;
|
||||
EpaDummy::Params params;
|
||||
params.max_iterations = 16;
|
||||
params.tolerance = 1e-6f;
|
||||
|
||||
auto result = EpaDummy::solve(a, b, s, params);
|
||||
|
||||
// Should either return a valid result or gracefully return nullopt
|
||||
if (result)
|
||||
{
|
||||
EXPECT_TRUE(std::isfinite(result->depth));
|
||||
EXPECT_TRUE(std::isfinite(result->normal.x));
|
||||
EXPECT_GT(result->num_faces, 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
SUCCEED() << "Epa::solve returned nullopt for small polytope (acceptable)";
|
||||
}
|
||||
}
|
||||
50
tests/general/unit_test_epa_more.cpp
Normal file
50
tests/general/unit_test_epa_more.cpp
Normal file
@@ -0,0 +1,50 @@
|
||||
#include "omath/collision/epa_algorithm.hpp"
|
||||
#include "omath/collision/simplex.hpp"
|
||||
#include "omath/linear_algebra/vector3.hpp"
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
using Vector3f = omath::Vector3<float>;
|
||||
|
||||
// Minimal collider interface matching Epa's expectations
|
||||
struct DegenerateCollider
|
||||
{
|
||||
using VectorType = Vector3f;
|
||||
// returns furthest point along dir
|
||||
VectorType find_abs_furthest_vertex_position(const VectorType& dir) const noexcept
|
||||
{
|
||||
// Always return points on a small circle in XY plane so some faces become degenerate
|
||||
if (dir.x > 0.5f) return {0.01f, 0.f, 0.f};
|
||||
if (dir.x < -0.5f) return {-0.01f, 0.f, 0.f};
|
||||
if (dir.y > 0.5f) return {0.f, 0.01f, 0.f};
|
||||
if (dir.y < -0.5f) return {0.f, -0.01f, 0.f};
|
||||
return {0.f, 0.f, 0.01f};
|
||||
}
|
||||
};
|
||||
|
||||
using Epa = omath::collision::Epa<DegenerateCollider>;
|
||||
using Simplex = omath::collision::Simplex<Vector3f>;
|
||||
|
||||
TEST(EpaExtra, DegenerateFaceHandled)
|
||||
{
|
||||
// Prepare a simplex with near-collinear points to force degenerate face handling
|
||||
Simplex s;
|
||||
s = { Vector3f{0.01f, 0.f, 0.f}, Vector3f{0.02f, 0.f, 0.f}, Vector3f{0.03f, 0.f, 0.f}, Vector3f{0.0f, 0.0f, 0.01f} };
|
||||
|
||||
DegenerateCollider a, b;
|
||||
Epa::Params params;
|
||||
params.max_iterations = 4;
|
||||
params.tolerance = 1e-6f;
|
||||
|
||||
auto result = Epa::solve(a, b, s, params);
|
||||
|
||||
// The algorithm should either return a valid result or gracefully exit (not crash)
|
||||
if (result)
|
||||
{
|
||||
EXPECT_TRUE(std::isfinite(result->depth));
|
||||
EXPECT_TRUE(std::isfinite(result->normal.x));
|
||||
}
|
||||
else
|
||||
{
|
||||
SUCCEED() << "EPA returned nullopt for degenerate input (acceptable)";
|
||||
}
|
||||
}
|
||||
65
tests/general/unit_test_line_tracer.cpp
Normal file
65
tests/general/unit_test_line_tracer.cpp
Normal file
@@ -0,0 +1,65 @@
|
||||
#include "omath/collision/line_tracer.hpp"
|
||||
#include "omath/linear_algebra/vector3.hpp"
|
||||
#include "omath/linear_algebra/triangle.hpp"
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
using omath::Vector3;
|
||||
|
||||
TEST(LineTracerTests, ParallelRayReturnsEnd)
|
||||
{
|
||||
// Triangle in XY plane
|
||||
omath::Triangle<Vector3<float>> tri{ {0.f,0.f,0.f}, {1.f,0.f,0.f}, {0.f,1.f,0.f} };
|
||||
omath::collision::Ray ray;
|
||||
ray.start = Vector3<float>{0.f,0.f,1.f};
|
||||
ray.end = Vector3<float>{1.f,1.f,2.f}; // direction parallel to plane normal (z) -> but choose parallel to plane? make direction parallel to triangle plane
|
||||
ray.end = Vector3<float>{1.f,1.f,1.f};
|
||||
|
||||
// For a ray parallel to the triangle plane the algorithm should return ray.end
|
||||
auto hit = omath::collision::LineTracer::get_ray_hit_point(ray, tri);
|
||||
EXPECT_TRUE(hit == ray.end);
|
||||
EXPECT_TRUE(omath::collision::LineTracer::can_trace_line(ray, tri));
|
||||
}
|
||||
|
||||
TEST(LineTracerTests, MissesTriangleReturnsEnd)
|
||||
{
|
||||
omath::Triangle<Vector3<float>> tri{ {0.f,0.f,0.f}, {1.f,0.f,0.f}, {0.f,1.f,0.f} };
|
||||
omath::collision::Ray ray;
|
||||
ray.start = Vector3<float>{2.f,2.f,-1.f};
|
||||
ray.end = Vector3<float>{2.f,2.f,1.f}; // passes above the triangle area
|
||||
|
||||
auto hit = omath::collision::LineTracer::get_ray_hit_point(ray, tri);
|
||||
EXPECT_TRUE(hit == ray.end);
|
||||
}
|
||||
|
||||
TEST(LineTracerTests, HitTriangleReturnsPointInsideSegment)
|
||||
{
|
||||
omath::Triangle<Vector3<float>> tri{ {0.f,0.f,0.f}, {2.f,0.f,0.f}, {0.f,2.f,0.f} };
|
||||
omath::collision::Ray ray;
|
||||
ray.start = Vector3<float>{0.25f,0.25f,-1.f};
|
||||
ray.end = Vector3<float>{0.25f,0.25f,1.f};
|
||||
|
||||
auto hit = omath::collision::LineTracer::get_ray_hit_point(ray, tri);
|
||||
// Should return a point between start and end (z approximately 0)
|
||||
EXPECT_NE(hit, ray.end);
|
||||
EXPECT_NEAR(hit.z, 0.f, 1e-4f);
|
||||
// t_hit should be between 0 and 1 along the ray direction
|
||||
auto dir = ray.direction_vector();
|
||||
// find t such that start + dir * t == hit (only check z comp for stability)
|
||||
float t = (hit.z - ray.start.z) / dir.z;
|
||||
EXPECT_GT(t, 0.f);
|
||||
EXPECT_LT(t, 1.f);
|
||||
}
|
||||
|
||||
TEST(LineTracerTests, InfiniteLengthEarlyOut)
|
||||
{
|
||||
omath::Triangle<Vector3<float>> tri{ {0.f,0.f,0.f}, {1.f,0.f,0.f}, {0.f,1.f,0.f} };
|
||||
omath::collision::Ray ray;
|
||||
ray.start = Vector3<float>{0.25f,0.25f,0.f};
|
||||
ray.end = Vector3<float>{0.25f,0.25f,1.f};
|
||||
ray.infinite_length = true;
|
||||
|
||||
// If t_hit <= epsilon the algorithm should return ray.end when infinite_length is true.
|
||||
// Using start on the triangle plane should produce t_hit <= epsilon.
|
||||
auto hit = omath::collision::LineTracer::get_ray_hit_point(ray, tri);
|
||||
EXPECT_TRUE(hit == ray.end);
|
||||
}
|
||||
48
tests/general/unit_test_line_tracer_extra.cpp
Normal file
48
tests/general/unit_test_line_tracer_extra.cpp
Normal file
@@ -0,0 +1,48 @@
|
||||
// Extra LineTracer tests
|
||||
#include <gtest/gtest.h>
|
||||
#include <omath/collision/line_tracer.hpp>
|
||||
#include <omath/linear_algebra/vector3.hpp>
|
||||
|
||||
using namespace omath;
|
||||
using namespace omath::collision;
|
||||
|
||||
TEST(LineTracerExtra, MissParallel)
|
||||
{
|
||||
Triangle<Vector3<float>> tri({0,0,0},{1,0,0},{0,1,0});
|
||||
Ray ray{ {0.3f,0.3f,1.f}, {0.3f,0.3f,2.f}, false };// parallel above triangle
|
||||
auto hit = LineTracer::get_ray_hit_point(ray, tri);
|
||||
EXPECT_EQ(hit, ray.end);
|
||||
}
|
||||
|
||||
TEST(LineTracerExtra, HitCenter)
|
||||
{
|
||||
Triangle<Vector3<float>> tri({0,0,0},{1,0,0},{0,1,0});
|
||||
Ray ray{ {0.3f,0.3f,-1.f}, {0.3f,0.3f,1.f}, false };
|
||||
auto hit = LineTracer::get_ray_hit_point(ray, tri);
|
||||
ASSERT_FALSE(hit == ray.end);
|
||||
EXPECT_NEAR(hit.x, 0.3f, 1e-6f);
|
||||
EXPECT_NEAR(hit.y, 0.3f, 1e-6f);
|
||||
EXPECT_NEAR(hit.z, 0.f, 1e-6f);
|
||||
}
|
||||
|
||||
TEST(LineTracerExtra, HitOnEdge)
|
||||
{
|
||||
Triangle<Vector3<float>> tri({0,0,0},{1,0,0},{0,1,0});
|
||||
Ray ray{ {0.0f,0.0f,1.f}, {0.0f,0.0f,0.f}, false };
|
||||
auto hit = LineTracer::get_ray_hit_point(ray, tri);
|
||||
// hitting exact vertex/edge may be considered miss; ensure function handles without crash
|
||||
if (hit != ray.end)
|
||||
{
|
||||
EXPECT_NEAR(hit.x, 0.0f, 1e-6f);
|
||||
EXPECT_NEAR(hit.y, 0.0f, 1e-6f);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(LineTracerExtra, InfiniteRayIgnoredIfBehind)
|
||||
{
|
||||
Triangle<Vector3<float>> tri({0,0,0},{1,0,0},{0,1,0});
|
||||
// Ray pointing away but infinite_length true should be ignored
|
||||
Ray ray{ {0.5f,0.5f,-1.f}, {0.5f,0.5f,-2.f}, true };
|
||||
auto hit = LineTracer::get_ray_hit_point(ray, tri);
|
||||
EXPECT_EQ(hit, ray.end);
|
||||
}
|
||||
105
tests/general/unit_test_line_tracer_more.cpp
Normal file
105
tests/general/unit_test_line_tracer_more.cpp
Normal file
@@ -0,0 +1,105 @@
|
||||
#include "omath/collision/line_tracer.hpp"
|
||||
#include "omath/linear_algebra/triangle.hpp"
|
||||
#include "omath/linear_algebra/vector3.hpp"
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
using omath::Vector3;
|
||||
using omath::collision::Ray;
|
||||
using omath::collision::LineTracer;
|
||||
using Triangle3 = omath::Triangle<Vector3<float>>;
|
||||
|
||||
TEST(LineTracerMore, ParallelRayReturnsEnd)
|
||||
{
|
||||
// Ray parallel to triangle plane: construct triangle in XY plane and ray along X axis
|
||||
Triangle3 tri(Vector3<float>{0.f,0.f,0.f}, Vector3<float>{1.f,0.f,0.f}, Vector3<float>{0.f,1.f,0.f});
|
||||
Ray ray; ray.start = {0.f,0.f,1.f}; ray.end = {1.f,0.f,1.f};
|
||||
|
||||
auto hit = LineTracer::get_ray_hit_point(ray, tri);
|
||||
EXPECT_EQ(hit, ray.end);
|
||||
}
|
||||
|
||||
TEST(LineTracerMore, UOutOfRangeReturnsEnd)
|
||||
{
|
||||
// Construct a ray that misses due to u < 0
|
||||
Triangle3 tri(Vector3<float>{0.f,0.f,0.f}, Vector3<float>{1.f,0.f,0.f}, Vector3<float>{0.f,1.f,0.f});
|
||||
Ray ray; ray.start = {-1.f,-1.f,-1.f}; ray.end = {-0.5f,-1.f,1.f};
|
||||
|
||||
auto hit = LineTracer::get_ray_hit_point(ray, tri);
|
||||
EXPECT_EQ(hit, ray.end);
|
||||
}
|
||||
|
||||
TEST(LineTracerMore, VOutOfRangeReturnsEnd)
|
||||
{
|
||||
// Construct ray that has v < 0
|
||||
Triangle3 tri(Vector3<float>{0.f,0.f,0.f}, Vector3<float>{1.f,0.f,0.f}, Vector3<float>{0.f,1.f,0.f});
|
||||
Ray ray; ray.start = {2.f,2.f,-1.f}; ray.end = {2.f,2.f,1.f};
|
||||
|
||||
auto hit = LineTracer::get_ray_hit_point(ray, tri);
|
||||
EXPECT_EQ(hit, ray.end);
|
||||
}
|
||||
|
||||
TEST(LineTracerMore, THitTooSmallReturnsEnd)
|
||||
{
|
||||
Triangle3 tri(Vector3<float>{0.f,0.f,0.f}, Vector3<float>{1.f,0.f,0.f}, Vector3<float>{0.f,1.f,0.f});
|
||||
Ray ray; ray.start = {0.f,0.f,0.0000000001f}; ray.end = {0.f,0.f,1.f};
|
||||
|
||||
auto hit = LineTracer::get_ray_hit_point(ray, tri);
|
||||
EXPECT_EQ(hit, ray.end);
|
||||
}
|
||||
|
||||
TEST(LineTracerMore, THitGreaterThanOneReturnsEnd)
|
||||
{
|
||||
Triangle3 tri(Vector3<float>{0.f,0.f,0.f}, Vector3<float>{1.f,0.f,0.f}, Vector3<float>{0.f,1.f,0.f});
|
||||
// Choose a ray and compute t_hit locally to assert consistency
|
||||
Ray ray; ray.start = {0.f,0.f,-1.f}; ray.end = {0.f,0.f,-0.5f};
|
||||
|
||||
auto hit = LineTracer::get_ray_hit_point(ray, tri);
|
||||
|
||||
const float k_epsilon = std::numeric_limits<float>::epsilon();
|
||||
const auto side_a = tri.side_a_vector();
|
||||
const auto side_b = tri.side_b_vector();
|
||||
const auto ray_dir = ray.direction_vector();
|
||||
const auto p = ray_dir.cross(side_b);
|
||||
const auto det = side_a.dot(p);
|
||||
|
||||
if (std::abs(det) < k_epsilon)
|
||||
{
|
||||
EXPECT_EQ(hit, ray.end);
|
||||
return;
|
||||
}
|
||||
|
||||
const auto inv_det = 1.0f / det;
|
||||
const auto tvec = ray.start - tri.m_vertex2;
|
||||
const auto q = tvec.cross(side_a);
|
||||
const auto t_hit = side_b.dot(q) * inv_det;
|
||||
|
||||
if (t_hit <= k_epsilon || t_hit > 1.0f)
|
||||
EXPECT_EQ(hit, ray.end) << "t_hit=" << t_hit << " hit=" << hit.x << "," << hit.y << "," << hit.z;
|
||||
else
|
||||
EXPECT_NE(hit, ray.end) << "t_hit=" << t_hit << " hit=" << hit.x << "," << hit.y << "," << hit.z;
|
||||
}
|
||||
|
||||
TEST(LineTracerMore, InfiniteLengthWithSmallTHitReturnsEnd)
|
||||
{
|
||||
Triangle3 tri(Vector3<float>{0.f,0.f,0.f}, Vector3<float>{1.f,0.f,0.f}, Vector3<float>{0.f,1.f,0.f});
|
||||
Triangle3 tri2(Vector3<float>{0.f,0.f,-1e-8f}, Vector3<float>{1.f,0.f,-1e-8f}, Vector3<float>{0.f,1.f,-1e-8f});
|
||||
Ray ray; ray.start = {0.f,0.f,0.f}; ray.end = {0.f,0.f,1.f}; ray.infinite_length = true;
|
||||
// Create triangle slightly behind so t_hit <= eps
|
||||
tri = tri2;
|
||||
|
||||
auto hit = LineTracer::get_ray_hit_point(ray, tri);
|
||||
EXPECT_EQ(hit, ray.end);
|
||||
}
|
||||
|
||||
TEST(LineTracerMore, SuccessfulHitReturnsPoint)
|
||||
{
|
||||
Triangle3 tri(Vector3<float>{0.f,0.f,0.f}, Vector3<float>{1.f,0.f,0.f}, Vector3<float>{0.f,1.f,0.f});
|
||||
Ray ray; ray.start = {0.1f,0.1f,-1.f}; ray.end = {0.1f,0.1f,1.f};
|
||||
|
||||
auto hit = LineTracer::get_ray_hit_point(ray, tri);
|
||||
EXPECT_NE(hit, ray.end);
|
||||
// Hit should be on plane z=0 and near x=0.1,y=0.1
|
||||
EXPECT_NEAR(hit.z, 0.f, 1e-6f);
|
||||
EXPECT_NEAR(hit.x, 0.1f, 1e-3f);
|
||||
EXPECT_NEAR(hit.y, 0.1f, 1e-3f);
|
||||
}
|
||||
57
tests/general/unit_test_line_tracer_more2.cpp
Normal file
57
tests/general/unit_test_line_tracer_more2.cpp
Normal file
@@ -0,0 +1,57 @@
|
||||
#include "omath/collision/line_tracer.hpp"
|
||||
#include "omath/linear_algebra/triangle.hpp"
|
||||
#include "omath/linear_algebra/vector3.hpp"
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
using omath::Vector3;
|
||||
using omath::collision::Ray;
|
||||
using omath::collision::LineTracer;
|
||||
using Triangle3 = omath::Triangle<Vector3<float>>;
|
||||
|
||||
TEST(LineTracerMore2, UGreaterThanOneReturnsEnd)
|
||||
{
|
||||
Triangle3 tri({0.f,0.f,0.f},{1.f,0.f,0.f},{0.f,1.f,0.f});
|
||||
// choose ray so barycentric u > 1
|
||||
Ray ray; ray.start = {2.f, -1.f, -1.f}; ray.end = {2.f, -1.f, 1.f};
|
||||
|
||||
auto hit = LineTracer::get_ray_hit_point(ray, tri);
|
||||
EXPECT_EQ(hit, ray.end);
|
||||
}
|
||||
|
||||
TEST(LineTracerMore2, VGreaterThanOneReturnsEnd)
|
||||
{
|
||||
Triangle3 tri({0.f,0.f,0.f},{1.f,0.f,0.f},{0.f,1.f,0.f});
|
||||
// choose ray so barycentric v > 1
|
||||
Ray ray; ray.start = {-1.f, 2.f, -1.f}; ray.end = {-1.f, 2.f, 1.f};
|
||||
|
||||
auto hit = LineTracer::get_ray_hit_point(ray, tri);
|
||||
EXPECT_EQ(hit, ray.end);
|
||||
}
|
||||
|
||||
TEST(LineTracerMore2, UPlusVGreaterThanOneReturnsEnd)
|
||||
{
|
||||
Triangle3 tri({0.f,0.f,0.f},{1.f,0.f,0.f},{0.f,1.f,0.f});
|
||||
// Ray aimed so u+v > 1 (outside triangle region)
|
||||
Ray ray; ray.start = {1.f, 1.f, -1.f}; ray.end = {1.f, 1.f, 1.f};
|
||||
|
||||
auto hit = LineTracer::get_ray_hit_point(ray, tri);
|
||||
EXPECT_EQ(hit, ray.end);
|
||||
}
|
||||
|
||||
TEST(LineTracerMore2, DirectionVectorNormalizedProducesUnitLength)
|
||||
{
|
||||
Ray r; r.start = {0.f,0.f,0.f}; r.end = {0.f,3.f,4.f};
|
||||
auto dir = r.direction_vector_normalized();
|
||||
auto len = dir.length();
|
||||
EXPECT_NEAR(len, 1.f, 1e-6f);
|
||||
}
|
||||
|
||||
TEST(LineTracerMore2, ZeroLengthRayHandled)
|
||||
{
|
||||
Triangle3 tri({0.f,0.f,0.f},{1.f,0.f,0.f},{0.f,1.f,0.f});
|
||||
Ray ray; ray.start = {0.f,0.f,0.f}; ray.end = {0.f,0.f,0.f};
|
||||
|
||||
// Zero-length ray: direction length == 0; algorithm should handle without crash
|
||||
auto hit = LineTracer::get_ray_hit_point(ray, tri);
|
||||
EXPECT_EQ(hit, ray.end);
|
||||
}
|
||||
57
tests/general/unit_test_linear_algebra_cover_more_ops.cpp
Normal file
57
tests/general/unit_test_linear_algebra_cover_more_ops.cpp
Normal file
@@ -0,0 +1,57 @@
|
||||
// Added to increase coverage for vector3/vector4/mat headers
|
||||
#include <gtest/gtest.h>
|
||||
#include <stdexcept>
|
||||
#include <sstream>
|
||||
|
||||
#include "omath/linear_algebra/vector3.hpp"
|
||||
#include "omath/linear_algebra/vector4.hpp"
|
||||
#include "omath/linear_algebra/mat.hpp"
|
||||
|
||||
using namespace omath;
|
||||
|
||||
TEST(Vector3ScalarOps, InPlaceScalarOperators)
|
||||
{
|
||||
Vector3<float> v{1.f, 2.f, 3.f};
|
||||
|
||||
v += 1.f;
|
||||
EXPECT_FLOAT_EQ(v.x, 2.f);
|
||||
EXPECT_FLOAT_EQ(v.y, 3.f);
|
||||
EXPECT_FLOAT_EQ(v.z, 4.f);
|
||||
|
||||
v /= 2.f;
|
||||
EXPECT_FLOAT_EQ(v.x, 1.f);
|
||||
EXPECT_FLOAT_EQ(v.y, 1.5f);
|
||||
EXPECT_FLOAT_EQ(v.z, 2.f);
|
||||
|
||||
v -= 0.5f;
|
||||
EXPECT_FLOAT_EQ(v.x, 0.5f);
|
||||
EXPECT_FLOAT_EQ(v.y, 1.0f);
|
||||
EXPECT_FLOAT_EQ(v.z, 1.5f);
|
||||
}
|
||||
|
||||
TEST(Vector4BinaryOps, ElementWiseMulDiv)
|
||||
{
|
||||
Vector4<float> a{2.f, 4.f, 6.f, 8.f};
|
||||
Vector4<float> b{1.f, 2.f, 3.f, 4.f};
|
||||
|
||||
auto m = a * b;
|
||||
EXPECT_FLOAT_EQ(m.x, 2.f);
|
||||
EXPECT_FLOAT_EQ(m.y, 8.f);
|
||||
EXPECT_FLOAT_EQ(m.z, 18.f);
|
||||
EXPECT_FLOAT_EQ(m.w, 32.f);
|
||||
|
||||
auto d = a / b;
|
||||
EXPECT_FLOAT_EQ(d.x, 2.f);
|
||||
EXPECT_FLOAT_EQ(d.y, 2.f);
|
||||
EXPECT_FLOAT_EQ(d.z, 2.f);
|
||||
EXPECT_FLOAT_EQ(d.w, 2.f);
|
||||
}
|
||||
|
||||
TEST(MatInitExceptions, InvalidInitializerLists)
|
||||
{
|
||||
// Wrong number of rows
|
||||
EXPECT_THROW((Mat<2,2,float>{ {1.f,2.f} }), std::invalid_argument);
|
||||
|
||||
// Row with wrong number of columns
|
||||
EXPECT_THROW((Mat<2,2,float>{ {1.f,2.f}, {1.f} }), std::invalid_argument);
|
||||
}
|
||||
52
tests/general/unit_test_linear_algebra_cover_remaining.cpp
Normal file
52
tests/general/unit_test_linear_algebra_cover_remaining.cpp
Normal file
@@ -0,0 +1,52 @@
|
||||
// Additional coverage tests for Vector4 and Mat
|
||||
#include <gtest/gtest.h>
|
||||
#include <stdexcept>
|
||||
|
||||
#include "omath/linear_algebra/vector4.hpp"
|
||||
#include "omath/linear_algebra/mat.hpp"
|
||||
|
||||
using namespace omath;
|
||||
|
||||
static void make_bad_mat_rows()
|
||||
{
|
||||
// wrong number of rows -> should throw inside initializer-list ctor
|
||||
Mat<2, 2, float> m{{1.f, 2.f}};
|
||||
(void)m;
|
||||
}
|
||||
|
||||
static void make_bad_mat_cols()
|
||||
{
|
||||
// row with wrong number of columns -> should throw
|
||||
Mat<2, 2, float> m{{1.f, 2.f}, {1.f}};
|
||||
(void)m;
|
||||
}
|
||||
|
||||
TEST(Vector4Operator, Subtraction)
|
||||
{
|
||||
Vector4<float> a{5.f, 6.f, 7.f, 8.f};
|
||||
Vector4<float> b{1.f, 2.f, 3.f, 4.f};
|
||||
|
||||
auto r = a - b;
|
||||
EXPECT_FLOAT_EQ(r.x, 4.f);
|
||||
EXPECT_FLOAT_EQ(r.y, 4.f);
|
||||
EXPECT_FLOAT_EQ(r.z, 4.f);
|
||||
EXPECT_FLOAT_EQ(r.w, 4.f);
|
||||
}
|
||||
|
||||
TEST(MatInitializerExceptions, ForcedThrowLines)
|
||||
{
|
||||
EXPECT_THROW(make_bad_mat_rows(), std::invalid_argument);
|
||||
EXPECT_THROW(make_bad_mat_cols(), std::invalid_argument);
|
||||
}
|
||||
|
||||
TEST(MatSelfAssignment, CopyAndMoveSelfAssign)
|
||||
{
|
||||
Mat<2,2,float> m{{1.f,2.f},{3.f,4.f}};
|
||||
// self copy-assignment
|
||||
m = m;
|
||||
EXPECT_FLOAT_EQ(m.at(0, 0), 1.f);
|
||||
|
||||
// self move-assignment
|
||||
m = std::move(m);
|
||||
EXPECT_FLOAT_EQ(m.at(0, 0), 1.f);
|
||||
}
|
||||
64
tests/general/unit_test_linear_algebra_extra.cpp
Normal file
64
tests/general/unit_test_linear_algebra_extra.cpp
Normal file
@@ -0,0 +1,64 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include <omath/linear_algebra/vector2.hpp>
|
||||
#include <omath/linear_algebra/vector3.hpp>
|
||||
#include <omath/linear_algebra/vector4.hpp>
|
||||
#include <omath/linear_algebra/mat.hpp>
|
||||
#include <functional>
|
||||
#include <format>
|
||||
|
||||
using namespace omath;
|
||||
|
||||
TEST(LinearAlgebraExtra, FormatterAndHashVector2)
|
||||
{
|
||||
Vector2<float> v{1.0f, 2.0f};
|
||||
std::string s = std::format("{}", v);
|
||||
EXPECT_EQ(s, "[1, 2]");
|
||||
|
||||
std::size_t h1 = std::hash<Vector2<float>>{}(v);
|
||||
std::size_t h2 = std::hash<Vector2<float>>{}(Vector2<float>{1.0f, 2.0f});
|
||||
std::size_t h3 = std::hash<Vector2<float>>{}(Vector2<float>{2.0f, 3.0f});
|
||||
|
||||
EXPECT_EQ(h1, h2);
|
||||
EXPECT_NE(h1, h3);
|
||||
}
|
||||
|
||||
TEST(LinearAlgebraExtra, FormatterAndHashVector3)
|
||||
{
|
||||
Vector3<float> v{1.0f, 2.0f, 3.0f};
|
||||
std::string s = std::format("{}", v);
|
||||
EXPECT_EQ(s, "[1, 2, 3]");
|
||||
|
||||
std::size_t h1 = std::hash<Vector3<float>>{}(v);
|
||||
std::size_t h2 = std::hash<Vector3<float>>{}(Vector3<float>{1.0f, 2.0f, 3.0f});
|
||||
EXPECT_EQ(h1, h2);
|
||||
|
||||
// point_to_same_direction
|
||||
EXPECT_TRUE((Vector3<float>{1,0,0}.point_to_same_direction(Vector3<float>{2,0,0})));
|
||||
EXPECT_FALSE((Vector3<float>{1,0,0}.point_to_same_direction(Vector3<float>{-1,0,0})));
|
||||
}
|
||||
|
||||
TEST(LinearAlgebraExtra, FormatterAndHashVector4)
|
||||
{
|
||||
Vector4<float> v{1.0f, 2.0f, 3.0f, 4.0f};
|
||||
std::string s = std::format("{}", v);
|
||||
EXPECT_EQ(s, "[1, 2, 3, 4]");
|
||||
|
||||
std::size_t h1 = std::hash<Vector4<float>>{}(v);
|
||||
std::size_t h2 = std::hash<Vector4<float>>{}(Vector4<float>{1.0f, 2.0f, 3.0f, 4.0f});
|
||||
EXPECT_EQ(h1, h2);
|
||||
}
|
||||
|
||||
TEST(LinearAlgebraExtra, MatRawArrayAndOperators)
|
||||
{
|
||||
Mat<2,2> m{{1.0f, 2.0f},{3.0f,4.0f}};
|
||||
auto raw = m.raw_array();
|
||||
EXPECT_EQ(raw.size(), 4);
|
||||
EXPECT_FLOAT_EQ(raw[0], 1.0f);
|
||||
EXPECT_FLOAT_EQ(raw[3], 4.0f);
|
||||
|
||||
// operator[] index access
|
||||
EXPECT_FLOAT_EQ(m.at(0,0), 1.0f);
|
||||
EXPECT_FLOAT_EQ(m.at(1,1), 4.0f);
|
||||
}
|
||||
|
||||
|
||||
56
tests/general/unit_test_linear_algebra_helpers.cpp
Normal file
56
tests/general/unit_test_linear_algebra_helpers.cpp
Normal file
@@ -0,0 +1,56 @@
|
||||
#include "omath/linear_algebra/triangle.hpp"
|
||||
#include "omath/linear_algebra/vector3.hpp"
|
||||
#include "omath/linear_algebra/vector4.hpp"
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
// This test file exercises the non-inlined helpers added to headers
|
||||
// (Vector3, Triangle, Vector4) to encourage symbol emission and
|
||||
// runtime execution so coverage tools can attribute hits back to the
|
||||
// header lines.
|
||||
|
||||
using namespace omath;
|
||||
|
||||
TEST(LinearAlgebraHelpers, Vector3NoInlineHelpersExecute)
|
||||
{
|
||||
Vector3<float> a{1.f, 2.f, 3.f};
|
||||
Vector3<float> b{4.f, 5.f, 6.f};
|
||||
|
||||
// Execute helpers that were made non-inlined
|
||||
auto l = a.length();
|
||||
auto ang = a.angle_between(b);
|
||||
auto perp = a.is_perpendicular(b);
|
||||
auto norm = a.normalized();
|
||||
|
||||
(void)l; (void)ang; (void)perp; (void)norm;
|
||||
SUCCEED();
|
||||
}
|
||||
|
||||
TEST(LinearAlgebraHelpers, TriangleNoInlineHelpersExecute)
|
||||
{
|
||||
Vector3<float> v1{0.f,0.f,0.f};
|
||||
Vector3<float> v2{3.f,0.f,0.f};
|
||||
Vector3<float> v3{3.f,4.f,0.f};
|
||||
|
||||
Triangle<Vector3<float>> t{v1, v2, v3};
|
||||
|
||||
auto n = t.calculate_normal();
|
||||
auto a = t.side_a_length();
|
||||
auto b = t.side_b_length();
|
||||
auto h = t.hypot();
|
||||
auto r = t.is_rectangular();
|
||||
|
||||
(void)n; (void)a; (void)b; (void)h; (void)r;
|
||||
SUCCEED();
|
||||
}
|
||||
|
||||
TEST(LinearAlgebraHelpers, Vector4NoInlineHelpersExecute)
|
||||
{
|
||||
Vector4<float> v{1.f,2.f,3.f,4.f};
|
||||
|
||||
auto l = v.length();
|
||||
auto s = v.sum();
|
||||
v.clamp(-10.f, 10.f);
|
||||
|
||||
(void)l; (void)s;
|
||||
SUCCEED();
|
||||
}
|
||||
74
tests/general/unit_test_linear_algebra_instantiate.cpp
Normal file
74
tests/general/unit_test_linear_algebra_instantiate.cpp
Normal file
@@ -0,0 +1,74 @@
|
||||
// Instantiation-only tests to force out-of-line template emission
|
||||
#include <gtest/gtest.h>
|
||||
#include <format>
|
||||
#include <functional>
|
||||
|
||||
#include "omath/linear_algebra/vector3.hpp"
|
||||
#include "omath/linear_algebra/vector4.hpp"
|
||||
#include "omath/linear_algebra/mat.hpp"
|
||||
|
||||
using namespace omath;
|
||||
|
||||
TEST(LinearAlgebraInstantiate, Vector3AndVector4AndMatCoverage) {
|
||||
// Vector3 usage
|
||||
Vector3<float> a{1.f, 2.f, 3.f};
|
||||
Vector3<float> b{4.f, 5.f, 6.f};
|
||||
|
||||
// call various methods
|
||||
volatile float d0 = a.distance_to_sqr(b);
|
||||
volatile float d1 = a.dot(b);
|
||||
volatile auto c = a.cross(b);
|
||||
auto tup = a.as_tuple();
|
||||
volatile bool dir = a.point_to_same_direction(b);
|
||||
|
||||
// non-inlined helpers
|
||||
volatile float ln = a.length();
|
||||
auto ang = a.angle_between(b);
|
||||
volatile bool perp = a.is_perpendicular(b, 0.1f);
|
||||
volatile auto anorm = a.normalized();
|
||||
|
||||
// formatter and hash instantiations (char only)
|
||||
(void)std::format("{}", a);
|
||||
(void)std::hash<Vector3<float>>{}(a);
|
||||
|
||||
// Vector4 usage
|
||||
Vector4<float> v4{1.f, -2.f, 3.f, -4.f};
|
||||
volatile float v4len = v4.length();
|
||||
volatile float v4sum = v4.sum();
|
||||
v4.clamp(-2.f, 2.f);
|
||||
(void)std::format("{}", v4);
|
||||
(void)std::hash<Vector4<float>>{}(v4);
|
||||
|
||||
// Mat usage: instantiate several sizes and store orders
|
||||
Mat<1,1> m1{{42.f}};
|
||||
volatile float m1det = m1.determinant();
|
||||
|
||||
Mat<2,2> m2{{{1.f,2.f},{3.f,4.f}}};
|
||||
volatile float det2 = m2.determinant();
|
||||
auto tr2 = m2.transposed();
|
||||
auto minor00 = m2.minor(0,0);
|
||||
auto algc = m2.alg_complement(0,1);
|
||||
auto rarr = m2.raw_array();
|
||||
auto inv2 = m2.inverted();
|
||||
|
||||
Mat<3,3> m3{{{1.f,2.f,3.f},{4.f,5.f,6.f},{7.f,8.f,9.f}}};
|
||||
volatile float det3 = m3.determinant();
|
||||
auto strip = m3.strip(0,0);
|
||||
auto min = m3.minor(2,2);
|
||||
|
||||
// to_string/wstring/u8string and to_screen_mat
|
||||
auto s = m2.to_string();
|
||||
auto ws = m2.to_wstring();
|
||||
auto u8s = m2.to_u8string();
|
||||
auto screen = Mat<4,4>::to_screen_mat(800.f, 600.f);
|
||||
|
||||
// call non-inlined mat helpers
|
||||
volatile auto det = m2.determinant();
|
||||
volatile auto inv = m2.inverted();
|
||||
volatile auto trans = m2.transposed();
|
||||
volatile auto raw = m2.raw_array();
|
||||
|
||||
// simple sanity checks (not strict, only to use values)
|
||||
EXPECT_EQ(std::get<0>(tup), 1.f);
|
||||
EXPECT_TRUE(det2 != 0.f || inv2 == std::nullopt);
|
||||
}
|
||||
64
tests/general/unit_test_linear_algebra_more.cpp
Normal file
64
tests/general/unit_test_linear_algebra_more.cpp
Normal file
@@ -0,0 +1,64 @@
|
||||
#include "omath/linear_algebra/triangle.hpp"
|
||||
#include "omath/linear_algebra/vector3.hpp"
|
||||
#include "omath/linear_algebra/vector4.hpp"
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
using namespace omath;
|
||||
|
||||
TEST(LinearAlgebraMore, Vector3EdgeCases)
|
||||
{
|
||||
Vector3<float> zero{0.f,0.f,0.f};
|
||||
Vector3<float> v{1.f,0.f,0.f};
|
||||
|
||||
// angle_between should be unexpected when one vector has zero length
|
||||
auto angle = zero.angle_between(v);
|
||||
EXPECT_FALSE(static_cast<bool>(angle));
|
||||
|
||||
// normalized of zero should return zero
|
||||
auto nz = zero.normalized();
|
||||
EXPECT_EQ(nz.x, 0.f);
|
||||
EXPECT_EQ(nz.y, 0.f);
|
||||
EXPECT_EQ(nz.z, 0.f);
|
||||
|
||||
// perpendicular case: x-axis and y-axis
|
||||
Vector3<float> x{1.f,0.f,0.f};
|
||||
Vector3<float> y{0.f,1.f,0.f};
|
||||
EXPECT_TRUE(x.is_perpendicular(y));
|
||||
}
|
||||
|
||||
TEST(LinearAlgebraMore, TriangleRectangularAndDegenerate)
|
||||
{
|
||||
Vector3<float> v1{0.f,0.f,0.f};
|
||||
Vector3<float> v2{3.f,0.f,0.f};
|
||||
Vector3<float> v3{3.f,4.f,0.f}; // 3-4-5 triangle, rectangular at v2
|
||||
|
||||
Triangle<Vector3<float>> t{v1,v2,v3};
|
||||
|
||||
EXPECT_NEAR(t.side_a_length(), 3.f, 1e-6f);
|
||||
EXPECT_NEAR(t.side_b_length(), 4.f, 1e-6f);
|
||||
EXPECT_NEAR(t.hypot(), 5.f, 1e-6f);
|
||||
EXPECT_TRUE(t.is_rectangular());
|
||||
|
||||
// Degenerate: all points same
|
||||
Triangle<Vector3<float>> d{v1,v1,v1};
|
||||
EXPECT_NEAR(d.side_a_length(), 0.f, 1e-6f);
|
||||
EXPECT_NEAR(d.side_b_length(), 0.f, 1e-6f);
|
||||
EXPECT_NEAR(d.hypot(), 0.f, 1e-6f);
|
||||
}
|
||||
|
||||
TEST(LinearAlgebraMore, Vector4ClampAndComparisons)
|
||||
{
|
||||
Vector4<float> v{10.f, -20.f, 30.f, -40.f};
|
||||
auto s = v.sum();
|
||||
EXPECT_NEAR(s, -20.f, 1e-6f);
|
||||
|
||||
v.clamp(-10.f, 10.f);
|
||||
EXPECT_LE(v.x, 10.f);
|
||||
EXPECT_GE(v.x, -10.f);
|
||||
EXPECT_LE(v.y, 10.f);
|
||||
EXPECT_GE(v.y, -10.f);
|
||||
|
||||
Vector4<float> a{1.f,2.f,3.f,4.f};
|
||||
Vector4<float> b{2.f,2.f,2.f,2.f};
|
||||
EXPECT_TRUE(a < b || a > b || a == b); // just exercise comparisons
|
||||
}
|
||||
87
tests/general/unit_test_linear_algebra_more2.cpp
Normal file
87
tests/general/unit_test_linear_algebra_more2.cpp
Normal file
@@ -0,0 +1,87 @@
|
||||
// Tests to exercise non-inlined helpers and remaining branches in linear algebra
|
||||
#include "gtest/gtest.h"
|
||||
#include "omath/linear_algebra/vector3.hpp"
|
||||
#include "omath/linear_algebra/vector4.hpp"
|
||||
#include "omath/linear_algebra/mat.hpp"
|
||||
|
||||
using namespace omath;
|
||||
|
||||
TEST(LinearAlgebraMore2, Vector3NonInlinedHelpers)
|
||||
{
|
||||
Vector3<float> v{3.f, 4.f, 0.f};
|
||||
EXPECT_FLOAT_EQ(v.length(), 5.0f);
|
||||
|
||||
auto vn = v.normalized();
|
||||
EXPECT_NEAR(vn.length(), 1.0f, 1e-6f);
|
||||
|
||||
Vector3<float> zero{0.f,0.f,0.f};
|
||||
auto ang = v.angle_between(zero);
|
||||
EXPECT_FALSE(ang.has_value());
|
||||
|
||||
Vector3<float> a{1.f,0.f,0.f};
|
||||
Vector3<float> b{0.f,1.f,0.f};
|
||||
EXPECT_TRUE(a.is_perpendicular(b));
|
||||
EXPECT_FALSE(a.is_perpendicular(a));
|
||||
|
||||
auto tup = v.as_tuple();
|
||||
EXPECT_EQ(std::get<0>(tup), 3.f);
|
||||
EXPECT_EQ(std::get<1>(tup), 4.f);
|
||||
EXPECT_EQ(std::get<2>(tup), 0.f);
|
||||
|
||||
EXPECT_TRUE(a.point_to_same_direction(Vector3<float>{2.f,0.f,0.f}));
|
||||
|
||||
// exercise hash specialization for Vector3<float>
|
||||
std::hash<Vector3<float>> hasher;
|
||||
auto hv = hasher(v);
|
||||
(void)hv;
|
||||
}
|
||||
|
||||
TEST(LinearAlgebraMore2, Vector4NonInlinedHelpers)
|
||||
{
|
||||
Vector4<float> v{1.f,2.f,3.f,4.f};
|
||||
EXPECT_FLOAT_EQ(v.length(), v.length());
|
||||
EXPECT_FLOAT_EQ(v.sum(), v.sum());
|
||||
|
||||
// clamp noinline should modify the vector
|
||||
v.clamp(0.f, 2.5f);
|
||||
EXPECT_GE(v.x, 0.f);
|
||||
EXPECT_LE(v.z, 2.5f);
|
||||
|
||||
Vector4<float> shorter{0.1f,0.1f,0.1f,0.1f};
|
||||
EXPECT_TRUE(shorter < v);
|
||||
EXPECT_FALSE(v < shorter);
|
||||
}
|
||||
|
||||
TEST(LinearAlgebraMore2, MatNonInlinedAndStringHelpers)
|
||||
{
|
||||
Mat<2,2,float> m{{{4.f,7.f},{2.f,6.f}}};
|
||||
EXPECT_FLOAT_EQ(m.determinant(), 10.0f);
|
||||
|
||||
auto maybe_inv = m.inverted();
|
||||
EXPECT_TRUE(maybe_inv.has_value());
|
||||
auto inv = maybe_inv.value();
|
||||
|
||||
// m * inv should be identity (approximately)
|
||||
auto prod = m * inv;
|
||||
EXPECT_NEAR(prod.at(0,0), 1.0f, 1e-5f);
|
||||
EXPECT_NEAR(prod.at(1,1), 1.0f, 1e-5f);
|
||||
EXPECT_NEAR(prod.at(0,1), 0.0f, 1e-5f);
|
||||
|
||||
// transposed and to_string variants
|
||||
auto t = m.transposed();
|
||||
EXPECT_EQ(t.at(0,1), m.at(1,0));
|
||||
|
||||
auto raw = m.raw_array();
|
||||
EXPECT_EQ(raw.size(), size_t(4));
|
||||
|
||||
auto s = m.to_string();
|
||||
EXPECT_NE(s.size(), 0u);
|
||||
auto ws = m.to_wstring();
|
||||
EXPECT_NE(ws.size(), 0u);
|
||||
auto u8s = m.to_u8string();
|
||||
EXPECT_NE(u8s.size(), 0u);
|
||||
|
||||
// to_screen_mat static helper
|
||||
auto screen = Mat<4,4,float>::to_screen_mat(800.f, 600.f);
|
||||
EXPECT_NEAR(screen.at(0,0), 800.f/2.f, 1e-6f);
|
||||
}
|
||||
24
tests/general/unit_test_mat_coverage_extra.cpp
Normal file
24
tests/general/unit_test_mat_coverage_extra.cpp
Normal file
@@ -0,0 +1,24 @@
|
||||
// Added to exercise Mat initializer-list exception branches and determinant fallback
|
||||
#include <gtest/gtest.h>
|
||||
#include <omath/linear_algebra/mat.hpp>
|
||||
|
||||
using namespace omath;
|
||||
|
||||
TEST(MatCoverageExtra, InitListRowsMismatchThrows) {
|
||||
// Rows mismatch: provide 3 rows for a 2x2 Mat
|
||||
EXPECT_THROW((Mat<2,2>{ {1,2}, {3,4}, {5,6} }), std::invalid_argument);
|
||||
}
|
||||
|
||||
TEST(MatCoverageExtra, InitListColumnsMismatchThrows) {
|
||||
// Columns mismatch: second row has wrong number of columns
|
||||
EXPECT_THROW((Mat<2,2>{ {1,2}, {3} }), std::invalid_argument);
|
||||
}
|
||||
|
||||
TEST(MatCoverageExtra, DeterminantFallbackIsCallable) {
|
||||
// Call determinant for 1x1 and 2x2 matrices to cover determinant paths
|
||||
Mat<1,1> m1{{3.14f}};
|
||||
EXPECT_FLOAT_EQ(m1.determinant(), 3.14f);
|
||||
|
||||
Mat<2,2> m2{{{1.0f,2.0f},{3.0f,4.0f}}};
|
||||
EXPECT_FLOAT_EQ(m2.determinant(), -2.0f);
|
||||
}
|
||||
21
tests/general/unit_test_mat_more.cpp
Normal file
21
tests/general/unit_test_mat_more.cpp
Normal file
@@ -0,0 +1,21 @@
|
||||
// Unit tests to exercise Mat extra branches
|
||||
#include "gtest/gtest.h"
|
||||
#include "omath/linear_algebra/mat.hpp"
|
||||
|
||||
using omath::Mat;
|
||||
|
||||
TEST(MatMore, InitListAndMultiply)
|
||||
{
|
||||
Mat<3,3,float> m{{{1.f,2.f,3.f}, {0.f,1.f,4.f}, {5.f,6.f,0.f}}};
|
||||
// multiply by scalar and check element
|
||||
auto r = m * 1.f;
|
||||
EXPECT_EQ(r.at(0,0), m.at(0,0));
|
||||
EXPECT_EQ(r.at(1,2), m.at(1,2));
|
||||
}
|
||||
|
||||
TEST(MatMore, Determinant)
|
||||
{
|
||||
Mat<2,2,double> m{{{1.0,2.0},{2.0,4.0}}}; // singular
|
||||
double det = m.determinant();
|
||||
EXPECT_DOUBLE_EQ(det, 0.0);
|
||||
}
|
||||
33
tests/general/unit_test_navigation_mesh.cpp
Normal file
33
tests/general/unit_test_navigation_mesh.cpp
Normal file
@@ -0,0 +1,33 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include "omath/pathfinding/navigation_mesh.hpp"
|
||||
|
||||
using namespace omath;
|
||||
using namespace omath::pathfinding;
|
||||
|
||||
TEST(NavigationMeshTests, SerializeDeserializeRoundTrip)
|
||||
{
|
||||
NavigationMesh nav;
|
||||
Vector3<float> a{0.f,0.f,0.f};
|
||||
Vector3<float> b{1.f,0.f,0.f};
|
||||
Vector3<float> c{0.f,1.f,0.f};
|
||||
|
||||
nav.m_vertex_map.emplace(a, std::vector<Vector3<float>>{b,c});
|
||||
nav.m_vertex_map.emplace(b, std::vector<Vector3<float>>{a});
|
||||
nav.m_vertex_map.emplace(c, std::vector<Vector3<float>>{a});
|
||||
|
||||
auto data = nav.serialize();
|
||||
NavigationMesh nav2;
|
||||
EXPECT_NO_THROW(nav2.deserialize(data));
|
||||
|
||||
// verify neighbors preserved
|
||||
EXPECT_EQ(nav2.m_vertex_map.size(), nav.m_vertex_map.size());
|
||||
EXPECT_EQ(nav2.get_neighbors(a).size(), 2u);
|
||||
}
|
||||
|
||||
TEST(NavigationMeshTests, GetClosestVertexWhenEmpty)
|
||||
{
|
||||
NavigationMesh nav;
|
||||
Vector3<float> p{5.f,5.f,5.f};
|
||||
auto res = nav.get_closest_vertex(p);
|
||||
EXPECT_FALSE(res.has_value());
|
||||
}
|
||||
28
tests/general/unit_test_pattern_scan_extra.cpp
Normal file
28
tests/general/unit_test_pattern_scan_extra.cpp
Normal file
@@ -0,0 +1,28 @@
|
||||
// Extra tests for PatternScanner behavior
|
||||
#include <gtest/gtest.h>
|
||||
#include <omath/utility/pattern_scan.hpp>
|
||||
|
||||
using namespace omath;
|
||||
|
||||
TEST(unit_test_pattern_scan_extra, IteratorScanFound)
|
||||
{
|
||||
std::vector<std::byte> buf = {std::byte(0xDE), std::byte(0xAD), std::byte(0xBE), std::byte(0xEF), std::byte(0x00)};
|
||||
auto it = PatternScanner::scan_for_pattern(buf.begin(), buf.end(), "DE AD BE EF");
|
||||
EXPECT_NE(it, buf.end());
|
||||
EXPECT_EQ(std::distance(buf.begin(), it), 0);
|
||||
}
|
||||
|
||||
TEST(unit_test_pattern_scan_extra, IteratorScanNotFound)
|
||||
{
|
||||
std::vector<std::byte> buf = {std::byte(0x00), std::byte(0x11), std::byte(0x22)};
|
||||
auto it = PatternScanner::scan_for_pattern(buf.begin(), buf.end(), "FF EE DD");
|
||||
EXPECT_EQ(it, buf.end());
|
||||
}
|
||||
|
||||
TEST(unit_test_pattern_scan_extra, ParseInvalidPattern)
|
||||
{
|
||||
// invalid hex token should cause the public scan to return end (no match)
|
||||
std::vector<std::byte> buf = {std::byte(0x00), std::byte(0x11)};
|
||||
auto it = PatternScanner::scan_for_pattern(buf.begin(), buf.end(), "GG HH");
|
||||
EXPECT_EQ(it, buf.end());
|
||||
}
|
||||
11
tests/general/unit_test_pe_pattern_scan_extra.cpp
Normal file
11
tests/general/unit_test_pe_pattern_scan_extra.cpp
Normal file
@@ -0,0 +1,11 @@
|
||||
// Tests for PePatternScanner basic behavior
|
||||
#include <gtest/gtest.h>
|
||||
#include <omath/utility/pe_pattern_scan.hpp>
|
||||
|
||||
using namespace omath;
|
||||
|
||||
TEST(unit_test_pe_pattern_scan_extra, MissingFileReturnsNull)
|
||||
{
|
||||
const auto res = PePatternScanner::scan_for_pattern_in_file("/non/existent/file.exe", "55 8B EC");
|
||||
EXPECT_FALSE(res.has_value());
|
||||
}
|
||||
114
tests/general/unit_test_pe_pattern_scan_file.cpp
Normal file
114
tests/general/unit_test_pe_pattern_scan_file.cpp
Normal file
@@ -0,0 +1,114 @@
|
||||
// Unit test for PePatternScanner::scan_for_pattern_in_file using a synthetic PE-like file
|
||||
#include <gtest/gtest.h>
|
||||
#include <omath/utility/pe_pattern_scan.hpp>
|
||||
#include <fstream>
|
||||
#include <vector>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
|
||||
using namespace omath;
|
||||
|
||||
// Helper: write a trivial PE-like file with DOS header and a single section named .text
|
||||
static bool write_minimal_pe_file(const std::string& path, const std::vector<std::uint8_t>& section_bytes)
|
||||
{
|
||||
std::ofstream f(path, std::ios::binary);
|
||||
if (!f.is_open()) return false;
|
||||
|
||||
// Write DOS header (e_magic = 0x5A4D, e_lfanew at offset 0x3C)
|
||||
std::vector<std::uint8_t> dos(64, 0);
|
||||
dos[0] = 'M'; dos[1] = 'Z';
|
||||
// e_lfanew -> place NT headers right after DOS (offset 0x80)
|
||||
std::uint32_t e_lfanew = 0x80;
|
||||
std::memcpy(dos.data() + 0x3C, &e_lfanew, sizeof(e_lfanew));
|
||||
f.write(reinterpret_cast<const char*>(dos.data()), dos.size());
|
||||
|
||||
// Pad up to e_lfanew
|
||||
if (f.tellp() < static_cast<std::streampos>(e_lfanew))
|
||||
{
|
||||
std::vector<char> pad(e_lfanew - static_cast<std::uint32_t>(f.tellp()), 0);
|
||||
f.write(pad.data(), pad.size());
|
||||
}
|
||||
|
||||
// NT headers signature 'PE\0\0'
|
||||
f.put('P'); f.put('E'); f.put('\0'); f.put('\0');
|
||||
|
||||
// FileHeader: machine, num_sections
|
||||
std::uint16_t machine = 0x8664; // x64
|
||||
std::uint16_t num_sections = 1;
|
||||
std::uint32_t dummy32 = 0;
|
||||
std::uint32_t dummy32b = 0;
|
||||
std::uint16_t size_optional = 0xF0; // reasonable
|
||||
std::uint16_t characteristics = 0;
|
||||
f.write(reinterpret_cast<const char*>(&machine), sizeof(machine));
|
||||
f.write(reinterpret_cast<const char*>(&num_sections), sizeof(num_sections));
|
||||
f.write(reinterpret_cast<const char*>(&dummy32), sizeof(dummy32));
|
||||
f.write(reinterpret_cast<const char*>(&dummy32b), sizeof(dummy32b));
|
||||
std::uint32_t num_symbols = 0;
|
||||
f.write(reinterpret_cast<const char*>(&num_symbols), sizeof(num_symbols));
|
||||
f.write(reinterpret_cast<const char*>(&size_optional), sizeof(size_optional));
|
||||
f.write(reinterpret_cast<const char*>(&characteristics), sizeof(characteristics));
|
||||
|
||||
// OptionalHeader (x64) minimal: magic 0x20b, image_base, size_of_code, size_of_headers
|
||||
std::uint16_t magic = 0x20b;
|
||||
f.write(reinterpret_cast<const char*>(&magic), sizeof(magic));
|
||||
// filler for rest of optional header up to size_optional
|
||||
std::vector<std::uint8_t> opt(size_optional - sizeof(magic), 0);
|
||||
// set size_code near end
|
||||
// we'll set image_base and size_code fields in reasonable positions for extractor
|
||||
// For simplicity, leave zeros; extractor primarily uses optional_header.image_base and size_code later,
|
||||
// but we will craft a SectionHeader that points to raw data we append below.
|
||||
f.write(reinterpret_cast<const char*>(opt.data()), opt.size());
|
||||
|
||||
// Section header (name 8 bytes, then remaining 36 bytes)
|
||||
char name[8] = {'.','t','e','x','t',0,0,0};
|
||||
f.write(name, 8);
|
||||
|
||||
// Write placeholder bytes for the rest of the section header and remember its start
|
||||
const std::uint32_t section_header_rest = 36u;
|
||||
const std::streampos header_rest_pos = f.tellp();
|
||||
std::vector<char> placeholder(section_header_rest, 0);
|
||||
f.write(placeholder.data(), placeholder.size());
|
||||
|
||||
// Now write section raw data and remember its file offset
|
||||
const std::streampos data_pos = f.tellp();
|
||||
f.write(reinterpret_cast<const char*>(section_bytes.data()), static_cast<std::streamsize>(section_bytes.size()));
|
||||
|
||||
// Patch section header fields: virtual_size, virtual_address, size_raw_data, ptr_raw_data
|
||||
const std::uint32_t virtual_size = static_cast<std::uint32_t>(section_bytes.size());
|
||||
const std::uint32_t virtual_address = 0x1000u;
|
||||
const std::uint32_t size_raw_data = static_cast<std::uint32_t>(section_bytes.size());
|
||||
const std::uint32_t ptr_raw_data = static_cast<std::uint32_t>(data_pos);
|
||||
|
||||
// Seek back to the header_rest_pos and write fields in order
|
||||
f.seekp(header_rest_pos, std::ios::beg);
|
||||
f.write(reinterpret_cast<const char*>(&virtual_size), sizeof(virtual_size));
|
||||
f.write(reinterpret_cast<const char*>(&virtual_address), sizeof(virtual_address));
|
||||
f.write(reinterpret_cast<const char*>(&size_raw_data), sizeof(size_raw_data));
|
||||
f.write(reinterpret_cast<const char*>(&ptr_raw_data), sizeof(ptr_raw_data));
|
||||
|
||||
// Seek back to end for consistency
|
||||
f.seekp(0, std::ios::end);
|
||||
|
||||
f.close();
|
||||
return true;
|
||||
}
|
||||
|
||||
TEST(unit_test_pe_pattern_scan_file, ScanFindsPattern)
|
||||
{
|
||||
const std::string path = "./test_minimal_pe.bin";
|
||||
std::vector<std::uint8_t> bytes = {0x55, 0x8B, 0xEC, 0x90, 0x90}; // pattern at offset 0
|
||||
ASSERT_TRUE(write_minimal_pe_file(path, bytes));
|
||||
|
||||
const auto res = PePatternScanner::scan_for_pattern_in_file(path, "55 8B EC", ".text");
|
||||
EXPECT_TRUE(res.has_value());
|
||||
}
|
||||
|
||||
TEST(unit_test_pe_pattern_scan_file, ScanMissingPattern)
|
||||
{
|
||||
const std::string path = "./test_minimal_pe_2.bin";
|
||||
std::vector<std::uint8_t> bytes = {0x00, 0x01, 0x02, 0x03};
|
||||
ASSERT_TRUE(write_minimal_pe_file(path, bytes));
|
||||
|
||||
const auto res = PePatternScanner::scan_for_pattern_in_file(path, "FF EE DD", ".text");
|
||||
EXPECT_FALSE(res.has_value());
|
||||
}
|
||||
69
tests/general/unit_test_pe_pattern_scan_loaded.cpp
Normal file
69
tests/general/unit_test_pe_pattern_scan_loaded.cpp
Normal file
@@ -0,0 +1,69 @@
|
||||
// Tests for PePatternScanner::scan_for_pattern_in_loaded_module
|
||||
#include <gtest/gtest.h>
|
||||
#include <omath/utility/pe_pattern_scan.hpp>
|
||||
#include <vector>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
|
||||
using namespace omath;
|
||||
|
||||
static std::vector<std::uint8_t> make_fake_module(std::uint32_t base_of_code,
|
||||
std::uint32_t size_code,
|
||||
const std::vector<std::uint8_t>& code_bytes)
|
||||
{
|
||||
const std::uint32_t e_lfanew = 0x80;
|
||||
const std::uint32_t total_size = e_lfanew + 0x200 + size_code + 0x100;
|
||||
std::vector<std::uint8_t> buf(total_size, 0);
|
||||
|
||||
// DOS header: e_magic at 0, e_lfanew at offset 0x3C
|
||||
buf[0] = 0x4D; buf[1] = 0x5A; // 'M' 'Z' (little-endian 0x5A4D)
|
||||
std::uint32_t le = e_lfanew;
|
||||
std::memcpy(buf.data() + 0x3C, &le, sizeof(le));
|
||||
|
||||
// NT signature at e_lfanew
|
||||
const std::uint32_t nt_sig = 0x4550; // 'PE\0\0'
|
||||
std::memcpy(buf.data() + e_lfanew, &nt_sig, sizeof(nt_sig));
|
||||
|
||||
// FileHeader is 20 bytes: we only need to ensure its size is present; leave zeros
|
||||
|
||||
// OptionalHeader magic (optional header begins at e_lfanew + 4 + sizeof(FileHeader) == e_lfanew + 24)
|
||||
const std::uint16_t opt_magic = 0x020B; // x64
|
||||
std::memcpy(buf.data() + e_lfanew + 24, &opt_magic, sizeof(opt_magic));
|
||||
|
||||
// size_code is at offset 4 inside OptionalHeader -> absolute e_lfanew + 28
|
||||
std::memcpy(buf.data() + e_lfanew + 28, &size_code, sizeof(size_code));
|
||||
|
||||
// base_of_code is at offset 20 inside OptionalHeader -> absolute e_lfanew + 44
|
||||
std::memcpy(buf.data() + e_lfanew + 44, &base_of_code, sizeof(base_of_code));
|
||||
|
||||
// place code bytes at offset base_of_code
|
||||
if (base_of_code + code_bytes.size() <= buf.size())
|
||||
std::memcpy(buf.data() + base_of_code, code_bytes.data(), code_bytes.size());
|
||||
|
||||
return buf;
|
||||
}
|
||||
|
||||
TEST(PePatternScanLoaded, FindsPatternAtBase)
|
||||
{
|
||||
std::vector<std::uint8_t> code = {0x90, 0x01, 0x02, 0x03, 0x04};
|
||||
auto buf = make_fake_module(0x200, static_cast<std::uint32_t>(code.size()), code);
|
||||
|
||||
auto res = PePatternScanner::scan_for_pattern_in_loaded_module(buf.data(), "90 01 02");
|
||||
ASSERT_TRUE(res.has_value());
|
||||
// address should point somewhere in our buffer; check offset
|
||||
uintptr_t addr = res.value();
|
||||
uintptr_t base = reinterpret_cast<uintptr_t>(buf.data());
|
||||
EXPECT_EQ(addr - base, 0x200u);
|
||||
}
|
||||
|
||||
TEST(PePatternScanLoaded, WildcardMatches)
|
||||
{
|
||||
std::vector<std::uint8_t> code = {0xDE, 0xAD, 0xBE, 0xEF};
|
||||
auto buf = make_fake_module(0x300, static_cast<std::uint32_t>(code.size()), code);
|
||||
|
||||
auto res = PePatternScanner::scan_for_pattern_in_loaded_module(buf.data(), "DE ?? BE");
|
||||
ASSERT_TRUE(res.has_value());
|
||||
uintptr_t addr = res.value();
|
||||
uintptr_t base = reinterpret_cast<uintptr_t>(buf.data());
|
||||
EXPECT_EQ(addr - base, 0x300u);
|
||||
}
|
||||
107
tests/general/unit_test_pe_pattern_scan_more.cpp
Normal file
107
tests/general/unit_test_pe_pattern_scan_more.cpp
Normal file
@@ -0,0 +1,107 @@
|
||||
// Additional tests for PePatternScanner to exercise edge cases and loaded-module scanning
|
||||
#include <gtest/gtest.h>
|
||||
#include <omath/utility/pe_pattern_scan.hpp>
|
||||
#include <fstream>
|
||||
#include <vector>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
|
||||
using namespace omath;
|
||||
|
||||
static bool write_bytes(const std::string &path, const std::vector<std::uint8_t>& data)
|
||||
{
|
||||
std::ofstream f(path, std::ios::binary);
|
||||
if (!f.is_open()) return false;
|
||||
f.write(reinterpret_cast<const char*>(data.data()), data.size());
|
||||
return true;
|
||||
}
|
||||
|
||||
TEST(unit_test_pe_pattern_scan_more, InvalidDosHeader)
|
||||
{
|
||||
const std::string path = "./test_bad_dos.bin";
|
||||
std::vector<std::uint8_t> data(128, 0);
|
||||
// write wrong magic
|
||||
data[0] = 'N'; data[1] = 'Z';
|
||||
ASSERT_TRUE(write_bytes(path, data));
|
||||
|
||||
auto res = PePatternScanner::scan_for_pattern_in_file(path, "55 8B EC", ".text");
|
||||
EXPECT_FALSE(res.has_value());
|
||||
}
|
||||
|
||||
TEST(unit_test_pe_pattern_scan_more, InvalidNtSignature)
|
||||
{
|
||||
const std::string path = "./test_bad_nt.bin";
|
||||
std::vector<std::uint8_t> data(256, 0);
|
||||
// valid DOS header
|
||||
data[0] = 'M'; data[1] = 'Z';
|
||||
// point e_lfanew to 0x80
|
||||
std::uint32_t e_lfanew = 0x80;
|
||||
std::memcpy(data.data()+0x3C, &e_lfanew, sizeof(e_lfanew));
|
||||
// write garbage at e_lfanew (not 'PE\0\0')
|
||||
data[e_lfanew + 0] = 'X'; data[e_lfanew + 1] = 'Y'; data[e_lfanew + 2] = 'Z'; data[e_lfanew + 3] = 'W';
|
||||
ASSERT_TRUE(write_bytes(path, data));
|
||||
|
||||
auto res = PePatternScanner::scan_for_pattern_in_file(path, "55 8B EC", ".text");
|
||||
EXPECT_FALSE(res.has_value());
|
||||
}
|
||||
|
||||
TEST(unit_test_pe_pattern_scan_more, SectionNotFound)
|
||||
{
|
||||
// reuse minimal writer but with section named .data and search .text
|
||||
const std::string path = "./test_section_not_found.bin";
|
||||
std::ofstream f(path, std::ios::binary);
|
||||
ASSERT_TRUE(f.is_open());
|
||||
// DOS
|
||||
std::vector<std::uint8_t> dos(64, 0); dos[0]='M'; dos[1]='Z'; std::uint32_t e_lfanew=0x80; std::memcpy(dos.data()+0x3C,&e_lfanew,sizeof(e_lfanew)); f.write(reinterpret_cast<char*>(dos.data()), dos.size());
|
||||
// pad
|
||||
std::vector<char> pad(e_lfanew - static_cast<std::uint32_t>(f.tellp()), 0); f.write(pad.data(), pad.size());
|
||||
// NT sig
|
||||
f.put('P'); f.put('E'); f.put('\0'); f.put('\0');
|
||||
// FileHeader minimal
|
||||
std::uint16_t machine=0x8664; std::uint16_t num_sections=1; std::uint32_t z=0; std::uint32_t z2=0; std::uint32_t numsym=0; std::uint16_t size_opt=0xF0; std::uint16_t ch=0;
|
||||
f.write(reinterpret_cast<char*>(&machine), sizeof(machine)); f.write(reinterpret_cast<char*>(&num_sections), sizeof(num_sections)); f.write(reinterpret_cast<char*>(&z), sizeof(z)); f.write(reinterpret_cast<char*>(&z2), sizeof(z2)); f.write(reinterpret_cast<char*>(&numsym), sizeof(numsym)); f.write(reinterpret_cast<char*>(&size_opt), sizeof(size_opt)); f.write(reinterpret_cast<char*>(&ch), sizeof(ch));
|
||||
// Optional header magic
|
||||
std::uint16_t magic = 0x20b; f.write(reinterpret_cast<char*>(&magic), sizeof(magic)); std::vector<std::uint8_t> opt(size_opt - sizeof(magic),0); f.write(reinterpret_cast<char*>(opt.data()), opt.size());
|
||||
// Section header named .data
|
||||
char name[8] = {'.','d','a','t','a',0,0,0}; f.write(name,8);
|
||||
std::uint32_t vs=4, va=0x1000, srd=4, prd=0x200; f.write(reinterpret_cast<char*>(&vs),4); f.write(reinterpret_cast<char*>(&va),4); f.write(reinterpret_cast<char*>(&srd),4); f.write(reinterpret_cast<char*>(&prd),4);
|
||||
std::vector<char> rest(16,0); f.write(rest.data(), rest.size());
|
||||
// section bytes
|
||||
std::vector<std::uint8_t> sec={0x00,0x01,0x02,0x03}; f.write(reinterpret_cast<char*>(sec.data()), sec.size()); f.close();
|
||||
|
||||
auto res = PePatternScanner::scan_for_pattern_in_file(path, "00 01", ".text");
|
||||
EXPECT_FALSE(res.has_value());
|
||||
}
|
||||
|
||||
TEST(unit_test_pe_pattern_scan_more, LoadedModuleScanFinds)
|
||||
{
|
||||
// Create an in-memory buffer that mimics loaded module layout
|
||||
// Define local header structs matching those in source
|
||||
struct DosHeader { std::uint16_t e_magic; std::uint16_t e_cblp; std::uint16_t e_cp; std::uint16_t e_crlc; std::uint16_t e_cparhdr; std::uint16_t e_minalloc; std::uint16_t e_maxalloc; std::uint16_t e_ss; std::uint16_t e_sp; std::uint16_t e_csum; std::uint16_t e_ip; std::uint16_t e_cs; std::uint16_t e_lfarlc; std::uint16_t e_ovno; std::uint16_t e_res[4]; std::uint16_t e_oemid; std::uint16_t e_oeminfo; std::uint16_t e_res2[10]; std::uint32_t e_lfanew; };
|
||||
struct FileHeader { std::uint16_t machine; std::uint16_t num_sections; std::uint32_t timedate_stamp; std::uint32_t ptr_symbols; std::uint32_t num_symbols; std::uint16_t size_optional_header; std::uint16_t characteristics; };
|
||||
struct OptionalHeaderX64 { std::uint16_t magic; std::uint16_t linker_version; std::uint32_t size_code; std::uint32_t size_init_data; std::uint32_t size_uninit_data; std::uint32_t entry_point; std::uint32_t base_of_code; std::uint64_t image_base; std::uint32_t section_alignment; std::uint32_t file_alignment; /* rest omitted */ std::uint32_t size_image; std::uint32_t size_headers; /* keep space */ std::uint8_t pad[200]; };
|
||||
struct ImageNtHeadersX64 { std::uint32_t signature; FileHeader file_header; OptionalHeaderX64 optional_header; };
|
||||
|
||||
const std::vector<std::uint8_t> pattern_bytes = {0xDE,0xAD,0xBE,0xEF,0x90};
|
||||
const std::uint32_t base_of_code = 0x200; // will place bytes at offset 0x200
|
||||
const std::uint32_t size_code = static_cast<std::uint32_t>(pattern_bytes.size());
|
||||
|
||||
const std::uint32_t bufsize = 0x400 + size_code;
|
||||
std::vector<std::uint8_t> buf(bufsize, 0);
|
||||
// DOS header
|
||||
auto dos = reinterpret_cast<DosHeader*>(buf.data());
|
||||
dos->e_magic = 0x5A4D; dos->e_lfanew = 0x80;
|
||||
// NT headers
|
||||
auto nt = reinterpret_cast<ImageNtHeadersX64*>(buf.data() + dos->e_lfanew);
|
||||
nt->signature = 0x4550; // 'PE\0\0'
|
||||
nt->file_header.machine = 0x8664; nt->file_header.num_sections = 1;
|
||||
nt->optional_header.magic = 0x020B; // x64
|
||||
nt->optional_header.base_of_code = base_of_code;
|
||||
nt->optional_header.size_code = size_code;
|
||||
|
||||
// place code at base_of_code
|
||||
std::memcpy(buf.data() + base_of_code, pattern_bytes.data(), pattern_bytes.size());
|
||||
|
||||
auto res = PePatternScanner::scan_for_pattern_in_loaded_module(buf.data(), "DE AD BE EF");
|
||||
EXPECT_TRUE(res.has_value());
|
||||
}
|
||||
252
tests/general/unit_test_pe_pattern_scan_more2.cpp
Normal file
252
tests/general/unit_test_pe_pattern_scan_more2.cpp
Normal file
@@ -0,0 +1,252 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include <omath/utility/pe_pattern_scan.hpp>
|
||||
#include <fstream>
|
||||
#include <vector>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
|
||||
using namespace omath;
|
||||
|
||||
// Local minimal FileHeader used by tests when constructing raw NT headers
|
||||
struct TestFileHeader { std::uint16_t machine; std::uint16_t num_sections; std::uint32_t timedate_stamp; std::uint32_t ptr_symbols; std::uint32_t num_symbols; std::uint16_t size_optional_header; std::uint16_t characteristics; };
|
||||
|
||||
static bool write_bytes(const std::string &path, const std::vector<std::uint8_t>& data)
|
||||
{
|
||||
std::ofstream f(path, std::ios::binary);
|
||||
if (!f.is_open()) return false;
|
||||
f.write(reinterpret_cast<const char*>(data.data()), data.size());
|
||||
return true;
|
||||
}
|
||||
|
||||
// Helper: write a trivial PE-like file with DOS header and a single section named .text
|
||||
static bool write_minimal_pe_file(const std::string& path, const std::vector<std::uint8_t>& section_bytes)
|
||||
{
|
||||
std::ofstream f(path, std::ios::binary);
|
||||
if (!f.is_open()) return false;
|
||||
|
||||
// Write DOS header (e_magic = 0x5A4D, e_lfanew at offset 0x3C)
|
||||
std::vector<std::uint8_t> dos(64, 0);
|
||||
dos[0] = 'M'; dos[1] = 'Z';
|
||||
std::uint32_t e_lfanew = 0x80;
|
||||
std::memcpy(dos.data() + 0x3C, &e_lfanew, sizeof(e_lfanew));
|
||||
f.write(reinterpret_cast<const char*>(dos.data()), dos.size());
|
||||
|
||||
// Pad up to e_lfanew
|
||||
if (f.tellp() < static_cast<std::streampos>(e_lfanew))
|
||||
{
|
||||
std::vector<char> pad(e_lfanew - static_cast<std::uint32_t>(f.tellp()), 0);
|
||||
f.write(pad.data(), pad.size());
|
||||
}
|
||||
|
||||
// NT headers signature 'PE\0\0'
|
||||
f.put('P'); f.put('E'); f.put('\0'); f.put('\0');
|
||||
|
||||
// FileHeader minimal
|
||||
std::uint16_t machine = 0x8664; // x64
|
||||
std::uint16_t num_sections = 1;
|
||||
std::uint32_t dummy32 = 0;
|
||||
std::uint32_t dummy32b = 0;
|
||||
std::uint16_t size_optional = 0xF0;
|
||||
std::uint16_t characteristics = 0;
|
||||
f.write(reinterpret_cast<const char*>(&machine), sizeof(machine));
|
||||
f.write(reinterpret_cast<const char*>(&num_sections), sizeof(num_sections));
|
||||
f.write(reinterpret_cast<const char*>(&dummy32), sizeof(dummy32));
|
||||
f.write(reinterpret_cast<const char*>(&dummy32b), sizeof(dummy32b));
|
||||
std::uint32_t num_symbols = 0;
|
||||
f.write(reinterpret_cast<const char*>(&num_symbols), sizeof(num_symbols));
|
||||
f.write(reinterpret_cast<const char*>(&size_optional), sizeof(size_optional));
|
||||
f.write(reinterpret_cast<const char*>(&characteristics), sizeof(characteristics));
|
||||
|
||||
// OptionalHeader minimal filler
|
||||
std::uint16_t magic = 0x20b;
|
||||
f.write(reinterpret_cast<const char*>(&magic), sizeof(magic));
|
||||
std::vector<std::uint8_t> opt(size_optional - sizeof(magic), 0);
|
||||
f.write(reinterpret_cast<const char*>(opt.data()), opt.size());
|
||||
|
||||
// Section header (name 8 bytes, then remaining 36 bytes)
|
||||
char name[8] = {'.','t','e','x','t',0,0,0};
|
||||
f.write(name, 8);
|
||||
|
||||
const std::uint32_t section_header_rest = 36u;
|
||||
const std::streampos header_rest_pos = f.tellp();
|
||||
std::vector<char> placeholder(section_header_rest, 0);
|
||||
f.write(placeholder.data(), placeholder.size());
|
||||
|
||||
// Now write section raw data and remember its file offset
|
||||
const std::streampos data_pos = f.tellp();
|
||||
f.write(reinterpret_cast<const char*>(section_bytes.data()), static_cast<std::streamsize>(section_bytes.size()));
|
||||
|
||||
// Patch section header fields
|
||||
const std::uint32_t virtual_size = static_cast<std::uint32_t>(section_bytes.size());
|
||||
const std::uint32_t virtual_address = 0x1000u;
|
||||
const std::uint32_t size_raw_data = static_cast<std::uint32_t>(section_bytes.size());
|
||||
const std::uint32_t ptr_raw_data = static_cast<std::uint32_t>(data_pos);
|
||||
|
||||
f.seekp(header_rest_pos, std::ios::beg);
|
||||
f.write(reinterpret_cast<const char*>(&virtual_size), sizeof(virtual_size));
|
||||
f.write(reinterpret_cast<const char*>(&virtual_address), sizeof(virtual_address));
|
||||
f.write(reinterpret_cast<const char*>(&size_raw_data), sizeof(size_raw_data));
|
||||
f.write(reinterpret_cast<const char*>(&ptr_raw_data), sizeof(ptr_raw_data));
|
||||
f.seekp(0, std::ios::end);
|
||||
|
||||
f.close();
|
||||
return true;
|
||||
}
|
||||
|
||||
TEST(unit_test_pe_pattern_scan_more2, LoadedModuleNullBaseReturnsNull)
|
||||
{
|
||||
auto res = PePatternScanner::scan_for_pattern_in_loaded_module(nullptr, "DE AD");
|
||||
EXPECT_FALSE(res.has_value());
|
||||
}
|
||||
|
||||
TEST(unit_test_pe_pattern_scan_more2, LoadedModuleInvalidOptionalHeaderReturnsNull)
|
||||
{
|
||||
// Construct in-memory buffer with DOS header but invalid optional header magic
|
||||
std::vector<std::uint8_t> buf(0x200, 0);
|
||||
struct DosHeader { std::uint16_t e_magic; std::uint8_t pad[0x3A]; std::uint32_t e_lfanew; };
|
||||
auto dos = reinterpret_cast<DosHeader*>(buf.data());
|
||||
dos->e_magic = 0x5A4D; dos->e_lfanew = 0x80;
|
||||
|
||||
// Place an NT header with wrong optional magic at e_lfanew
|
||||
auto nt_ptr = buf.data() + dos->e_lfanew;
|
||||
// write signature
|
||||
nt_ptr[0] = 'P'; nt_ptr[1] = 'E'; nt_ptr[2] = 0; nt_ptr[3] = 0;
|
||||
// craft FileHeader with size_optional_header large enough
|
||||
std::uint16_t size_opt = 0xE0;
|
||||
// file header starts at offset 4
|
||||
std::memcpy(nt_ptr + 4 + 12, &size_opt, sizeof(size_opt)); // size_optional_header located after 12 bytes into FileHeader
|
||||
// write optional header magic to be invalid value
|
||||
std::uint16_t bad_magic = 0x9999;
|
||||
std::memcpy(nt_ptr + 4 + sizeof(std::uint32_t) + sizeof(std::uint16_t) + sizeof(std::uint16_t), &bad_magic, sizeof(bad_magic));
|
||||
|
||||
auto res = PePatternScanner::scan_for_pattern_in_loaded_module(buf.data(), "DE AD");
|
||||
EXPECT_FALSE(res.has_value());
|
||||
}
|
||||
|
||||
TEST(unit_test_pe_pattern_scan_more2, FileX86OptionalHeaderScanFindsPattern)
|
||||
{
|
||||
const std::string path = "./test_pe_x86.bin";
|
||||
const std::vector<std::uint8_t> pattern = {0xDE, 0xAD, 0xBE, 0xEF};
|
||||
|
||||
// Use helper from this file to write a consistent minimal PE file with .text section
|
||||
ASSERT_TRUE(write_minimal_pe_file(path, pattern));
|
||||
|
||||
auto res = PePatternScanner::scan_for_pattern_in_file(path, "DE AD BE EF", ".text");
|
||||
ASSERT_TRUE(res.has_value());
|
||||
EXPECT_GE(res->virtual_base_addr, 0u);
|
||||
EXPECT_GE(res->raw_base_addr, 0u);
|
||||
EXPECT_EQ(res->target_offset, 0);
|
||||
}
|
||||
|
||||
TEST(unit_test_pe_pattern_scan_more2, FilePatternNotFoundReturnsNull)
|
||||
{
|
||||
const std::string path = "./test_pe_no_pattern.bin";
|
||||
std::vector<std::uint8_t> data(512, 0);
|
||||
// minimal DOS/NT headers to make extract_section fail earlier or return empty data
|
||||
data[0] = 'M'; data[1] = 'Z'; std::uint32_t e_lfanew = 0x80; std::memcpy(data.data()+0x3C, &e_lfanew, sizeof(e_lfanew));
|
||||
// NT signature
|
||||
data[e_lfanew + 0] = 'P'; data[e_lfanew + 1] = 'E'; data[e_lfanew + 2] = 0; data[e_lfanew + 3] = 0;
|
||||
// FileHeader: one section, size_optional_header set low
|
||||
std::uint16_t num_sections = 1; std::uint16_t size_optional_header = 0xE0; std::memcpy(data.data() + e_lfanew + 6, &num_sections, sizeof(num_sections)); std::memcpy(data.data() + e_lfanew + 4 + 12, &size_optional_header, sizeof(size_optional_header));
|
||||
// Optional header magic x64
|
||||
std::uint16_t magic = 0x020B; std::memcpy(data.data() + e_lfanew + 4 + sizeof(TestFileHeader), &magic, sizeof(magic));
|
||||
// Section header .text with small data that does not contain the pattern
|
||||
const std::size_t offset_to_segment_table = e_lfanew + 4 + sizeof(TestFileHeader) + size_optional_header;
|
||||
const char name[8] = {'.','t','e','x','t',0,0,0}; std::memcpy(data.data() + offset_to_segment_table, name, 8);
|
||||
std::uint32_t vs = 4, va = 0x1000, srd = 4, prd = 0x200; std::memcpy(data.data() + offset_to_segment_table + 8, &vs, 4); std::memcpy(data.data() + offset_to_segment_table + 12, &va, 4); std::memcpy(data.data() + offset_to_segment_table + 16, &srd, 4); std::memcpy(data.data() + offset_to_segment_table + 20, &prd, 4);
|
||||
// write file
|
||||
ASSERT_TRUE(write_bytes(path, data));
|
||||
|
||||
auto res = PePatternScanner::scan_for_pattern_in_file(path, "AA BB CC", ".text");
|
||||
EXPECT_FALSE(res.has_value());
|
||||
}
|
||||
// Extra tests for pe_pattern_scan edge cases (on-disk API)
|
||||
|
||||
TEST(PePatternScanMore2, PatternAtStartFound)
|
||||
{
|
||||
const std::string path = "./test_pe_more_start.bin";
|
||||
std::vector<std::uint8_t> bytes = {0x90, 0x01, 0x02, 0x03, 0x04};
|
||||
ASSERT_TRUE(write_minimal_pe_file(path, bytes));
|
||||
|
||||
const auto res = PePatternScanner::scan_for_pattern_in_file(path, "90 01 02", ".text");
|
||||
EXPECT_TRUE(res.has_value());
|
||||
}
|
||||
|
||||
TEST(PePatternScanMore2, PatternAtEndFound)
|
||||
{
|
||||
const std::string path = "./test_pe_more_end.bin";
|
||||
std::vector<std::uint8_t> bytes = {0x00, 0x11, 0x22, 0x33, 0x44};
|
||||
ASSERT_TRUE(write_minimal_pe_file(path, bytes));
|
||||
|
||||
const auto res = PePatternScanner::scan_for_pattern_in_file(path, "22 33 44", ".text");
|
||||
if (!res.has_value())
|
||||
{
|
||||
// Try to locate the section header and print the raw section bytes the scanner would read
|
||||
std::ifstream in(path, std::ios::binary);
|
||||
ASSERT_TRUE(in.is_open());
|
||||
// search for ".text" name
|
||||
in.seekg(0, std::ios::beg);
|
||||
std::vector<char> filebuf((std::istreambuf_iterator<char>(in)), std::istreambuf_iterator<char>());
|
||||
const auto it = std::search(filebuf.begin(), filebuf.end(), std::begin(".text"), std::end(".text")-1);
|
||||
if (it != filebuf.end())
|
||||
{
|
||||
const size_t pos = std::distance(filebuf.begin(), it);
|
||||
// after name, next fields: virtual_size (4), virtual_address(4), size_raw_data(4), ptr_raw_data(4)
|
||||
const size_t meta_off = pos + 8;
|
||||
uint32_t virtual_size{};
|
||||
uint32_t virtual_address{};
|
||||
uint32_t size_raw_data{};
|
||||
uint32_t ptr_raw_data{};
|
||||
std::memcpy(&virtual_size, filebuf.data()+meta_off, sizeof(virtual_size));
|
||||
std::memcpy(&virtual_address, filebuf.data()+meta_off+4, sizeof(virtual_address));
|
||||
std::memcpy(&size_raw_data, filebuf.data()+meta_off+8, sizeof(size_raw_data));
|
||||
std::memcpy(&ptr_raw_data, filebuf.data()+meta_off+12, sizeof(ptr_raw_data));
|
||||
|
||||
std::cerr << "Parsed section header: virtual_size=" << virtual_size << " virtual_address=0x" << std::hex << virtual_address << std::dec << " size_raw_data=" << size_raw_data << " ptr_raw_data=" << ptr_raw_data << "\n";
|
||||
|
||||
if (ptr_raw_data + size_raw_data <= filebuf.size())
|
||||
{
|
||||
std::cerr << "Extracted section bytes:\n";
|
||||
for (size_t i = 0; i < size_raw_data; i += 16)
|
||||
{
|
||||
std::fprintf(stderr, "%04zx: ", i);
|
||||
for (size_t j = 0; j < 16 && i + j < size_raw_data; ++j)
|
||||
std::fprintf(stderr, "%02x ", static_cast<uint8_t>(filebuf[ptr_raw_data + i + j]));
|
||||
std::fprintf(stderr, "\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
EXPECT_TRUE(res.has_value());
|
||||
}
|
||||
|
||||
TEST(PePatternScanMore2, WildcardMatches)
|
||||
{
|
||||
const std::string path = "./test_pe_more_wild.bin";
|
||||
std::vector<std::uint8_t> bytes = {0xDE, 0xAD, 0xBE, 0xEF};
|
||||
ASSERT_TRUE(write_minimal_pe_file(path, bytes));
|
||||
|
||||
const auto res = PePatternScanner::scan_for_pattern_in_file(path, "DE ?? BE", ".text");
|
||||
EXPECT_TRUE(res.has_value());
|
||||
}
|
||||
|
||||
TEST(PePatternScanMore2, PatternLongerThanBuffer)
|
||||
{
|
||||
const std::string path = "./test_pe_more_small.bin";
|
||||
std::vector<std::uint8_t> bytes = {0xAA, 0xBB};
|
||||
ASSERT_TRUE(write_minimal_pe_file(path, bytes));
|
||||
|
||||
const auto res = PePatternScanner::scan_for_pattern_in_file(path, "AA BB CC", ".text");
|
||||
EXPECT_FALSE(res.has_value());
|
||||
}
|
||||
|
||||
TEST(PePatternScanMore2, InvalidPatternParse)
|
||||
{
|
||||
const std::string path = "./test_pe_more_invalid.bin";
|
||||
std::vector<std::uint8_t> bytes = {0x01, 0x02, 0x03};
|
||||
ASSERT_TRUE(write_minimal_pe_file(path, bytes));
|
||||
|
||||
const auto res = PePatternScanner::scan_for_pattern_in_file(path, "01 GG 03", ".text");
|
||||
EXPECT_FALSE(res.has_value());
|
||||
}
|
||||
|
||||
64
tests/general/unit_test_pred_engine_trait.cpp
Normal file
64
tests/general/unit_test_pred_engine_trait.cpp
Normal file
@@ -0,0 +1,64 @@
|
||||
// Tests for PredEngineTrait
|
||||
#include <gtest/gtest.h>
|
||||
#include <omath/engines/source_engine/traits/pred_engine_trait.hpp>
|
||||
#include <omath/projectile_prediction/projectile.hpp>
|
||||
#include <omath/projectile_prediction/target.hpp>
|
||||
|
||||
using namespace omath;
|
||||
using namespace omath::source_engine;
|
||||
|
||||
TEST(PredEngineTrait, PredictProjectilePositionBasic)
|
||||
{
|
||||
projectile_prediction::Projectile p;
|
||||
p.m_origin = {0.f, 0.f, 0.f};
|
||||
p.m_launch_speed = 10.f;
|
||||
p.m_gravity_scale = 1.f;
|
||||
|
||||
auto pos = PredEngineTrait::predict_projectile_position(p, /*pitch*/0.f, /*yaw*/0.f, /*time*/1.f, /*gravity*/9.81f);
|
||||
// With zero pitch and yaw forward vector is along X; expect x ~10, z reduced by gravity*0.5
|
||||
EXPECT_NEAR(pos.x, 10.f, 1e-3f);
|
||||
EXPECT_NEAR(pos.z, -9.81f * 0.5f, 1e-3f);
|
||||
}
|
||||
|
||||
TEST(PredEngineTrait, PredictTargetPositionAirborne)
|
||||
{
|
||||
projectile_prediction::Target t;
|
||||
t.m_origin = {0.f,0.f,10.f};
|
||||
t.m_velocity = {1.f,0.f,0.f};
|
||||
t.m_is_airborne = true;
|
||||
|
||||
auto pred = PredEngineTrait::predict_target_position(t, 2.f, 9.81f);
|
||||
EXPECT_NEAR(pred.x, 2.f, 1e-6f);
|
||||
// z should have been reduced by gravity* t^2
|
||||
EXPECT_NEAR(pred.z, 10.f - 9.81f * 4.f * 0.5f, 1e-6f);
|
||||
}
|
||||
|
||||
TEST(PredEngineTrait, CalcVector2dDistance)
|
||||
{
|
||||
Vector3<float> d{3.f,4.f,0.f};
|
||||
EXPECT_NEAR(PredEngineTrait::calc_vector_2d_distance(d), 5.f, 1e-6f);
|
||||
}
|
||||
|
||||
TEST(PredEngineTrait, CalcViewpointFromAngles)
|
||||
{
|
||||
projectile_prediction::Projectile p;
|
||||
p.m_origin = {0.f,0.f,0.f};
|
||||
p.m_launch_speed = 10.f;
|
||||
|
||||
Vector3<float> predicted{10.f, 0.f, 0.f};
|
||||
std::optional<float> pitch = 45.f;
|
||||
auto vp = PredEngineTrait::calc_viewpoint_from_angles(p, predicted, pitch);
|
||||
// For 45 degrees, height = delta2d * tan(45deg) = 10 * 1 = 10
|
||||
EXPECT_NEAR(vp.z, 10.f, 1e-6f);
|
||||
}
|
||||
|
||||
TEST(PredEngineTrait, DirectAngles)
|
||||
{
|
||||
Vector3<float> origin{0.f,0.f,0.f};
|
||||
Vector3<float> target{0.f,1.f,1.f};
|
||||
// yaw should be 90 degrees (pointing along y)
|
||||
EXPECT_NEAR(PredEngineTrait::calc_direct_yaw_angle(origin, target), 90.f, 1e-3f);
|
||||
// pitch should be asin(z/distance)
|
||||
const float dist = origin.distance_to(target);
|
||||
EXPECT_NEAR(PredEngineTrait::calc_direct_pitch_angle(origin, target), angles::radians_to_degrees(std::asin((target.z-origin.z)/dist)), 1e-3f);
|
||||
}
|
||||
100
tests/general/unit_test_proj_pred_engine_legacy_more.cpp
Normal file
100
tests/general/unit_test_proj_pred_engine_legacy_more.cpp
Normal file
@@ -0,0 +1,100 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include <omath/projectile_prediction/proj_pred_engine_legacy.hpp>
|
||||
#include <omath/projectile_prediction/projectile.hpp>
|
||||
#include <omath/projectile_prediction/target.hpp>
|
||||
#include <omath/linear_algebra/vector3.hpp>
|
||||
|
||||
using omath::projectile_prediction::Projectile;
|
||||
using omath::projectile_prediction::Target;
|
||||
using omath::Vector3;
|
||||
|
||||
// Fake engine trait where gravity is effectively zero and projectile prediction always hits the target
|
||||
struct FakeEngineZeroGravity
|
||||
{
|
||||
static Vector3<float> predict_target_position(const Target& t, float /*time*/, float /*gravity*/) noexcept
|
||||
{
|
||||
return t.m_origin;
|
||||
}
|
||||
static Vector3<float> predict_projectile_position(const Projectile& /*p*/, float /*pitch*/, float /*yaw*/, float /*time*/, float /*gravity*/) noexcept
|
||||
{
|
||||
// Return a fixed point matching typical target used in the test
|
||||
return Vector3<float>{100.f, 0.f, 0.f};
|
||||
}
|
||||
static float calc_vector_2d_distance(const Vector3<float>& v) noexcept { return std::hypot(v.x, v.y); }
|
||||
static float get_vector_height_coordinate(const Vector3<float>& v) noexcept { return v.z; }
|
||||
static Vector3<float> calc_viewpoint_from_angles(const Projectile& /*p*/, Vector3<float> /*v*/, std::optional<float> /*maybe_pitch*/) noexcept
|
||||
{
|
||||
return Vector3<float>{1.f, 2.f, 3.f};
|
||||
}
|
||||
static float calc_direct_pitch_angle(const Vector3<float>& /*a*/, const Vector3<float>& /*b*/) noexcept { return 12.5f; }
|
||||
static float calc_direct_yaw_angle(const Vector3<float>& /*a*/, const Vector3<float>& /*b*/) noexcept { return 0.f; }
|
||||
};
|
||||
|
||||
TEST(ProjPredLegacyMore, ZeroGravityUsesDirectPitchAndReturnsViewpoint)
|
||||
{
|
||||
Projectile proj{ .m_origin = {0.f, 0.f, 0.f}, .m_launch_speed = 10.f, .m_gravity_scale = 0.f };
|
||||
Target target{ .m_origin = {100.f, 0.f, 0.f}, .m_velocity = {0.f,0.f,0.f}, .m_is_airborne = false };
|
||||
|
||||
using Engine = omath::projectile_prediction::ProjPredEngineLegacy<FakeEngineZeroGravity>;
|
||||
Engine engine(9.8f, 0.1f, 5.f, 1e-3f);
|
||||
|
||||
const auto res = engine.maybe_calculate_aim_point(proj, target);
|
||||
ASSERT_TRUE(res.has_value());
|
||||
const auto v = res.value();
|
||||
EXPECT_NEAR(v.x, 1.f, 1e-6f);
|
||||
EXPECT_NEAR(v.y, 2.f, 1e-6f);
|
||||
EXPECT_NEAR(v.z, 3.f, 1e-6f);
|
||||
}
|
||||
|
||||
// Fake trait producing no valid launch angle (root < 0)
|
||||
struct FakeEngineNoSolution
|
||||
{
|
||||
static Vector3<float> predict_target_position(const Target& t, float /*time*/, float /*gravity*/) noexcept { return t.m_origin; }
|
||||
static Vector3<float> predict_projectile_position(const Projectile& /*p*/, float /*pitch*/, float /*yaw*/, float /*time*/, float /*gravity*/) noexcept { return Vector3<float>{0.f,0.f,0.f}; }
|
||||
static float calc_vector_2d_distance(const Vector3<float>& /*v*/) noexcept { return 10000.f; }
|
||||
static float get_vector_height_coordinate(const Vector3<float>& /*v*/) noexcept { return 0.f; }
|
||||
static Vector3<float> calc_viewpoint_from_angles(const Projectile& /*p*/, Vector3<float> /*v*/, std::optional<float> /*maybe_pitch*/) noexcept { return Vector3<float>{}; }
|
||||
static float calc_direct_pitch_angle(const Vector3<float>& /*a*/, const Vector3<float>& /*b*/) noexcept { return 0.f; }
|
||||
static float calc_direct_yaw_angle(const Vector3<float>& /*a*/, const Vector3<float>& /*b*/) noexcept { return 0.f; }
|
||||
};
|
||||
|
||||
TEST(ProjPredLegacyMore, NoSolutionRootReturnsNullopt)
|
||||
{
|
||||
// Very slow projectile and large distance -> quadratic root negative
|
||||
Projectile proj{ .m_origin = {0.f,0.f,0.f}, .m_launch_speed = 1.f, .m_gravity_scale = 1.f };
|
||||
Target target{ .m_origin = {10000.f, 0.f, 0.f}, .m_velocity = {0.f,0.f,0.f}, .m_is_airborne = false };
|
||||
|
||||
using Engine = omath::projectile_prediction::ProjPredEngineLegacy<FakeEngineNoSolution>;
|
||||
Engine engine(9.8f, 0.5f, 2.f, 1.f);
|
||||
|
||||
const auto res = engine.maybe_calculate_aim_point(proj, target);
|
||||
EXPECT_FALSE(res.has_value());
|
||||
}
|
||||
|
||||
// Fake trait where an angle exists but the projectile does not reach target (miss)
|
||||
struct FakeEngineAngleButMiss
|
||||
{
|
||||
static Vector3<float> predict_target_position(const Target& t, float /*time*/, float /*gravity*/) noexcept { return t.m_origin; }
|
||||
static Vector3<float> predict_projectile_position(const Projectile& /*p*/, float /*pitch*/, float /*yaw*/, float /*time*/, float /*gravity*/) noexcept
|
||||
{
|
||||
// always return a point far from the target
|
||||
return Vector3<float>{0.f, 0.f, 1000.f};
|
||||
}
|
||||
static float calc_vector_2d_distance(const Vector3<float>& v) noexcept { return std::hypot(v.x, v.y); }
|
||||
static float get_vector_height_coordinate(const Vector3<float>& v) noexcept { return v.z; }
|
||||
static Vector3<float> calc_viewpoint_from_angles(const Projectile& /*p*/, Vector3<float> /*v*/, std::optional<float> /*maybe_pitch*/) noexcept { return Vector3<float>{9.f,9.f,9.f}; }
|
||||
static float calc_direct_pitch_angle(const Vector3<float>& /*a*/, const Vector3<float>& /*b*/) noexcept { return 1.f; }
|
||||
static float calc_direct_yaw_angle(const Vector3<float>& /*a*/, const Vector3<float>& /*b*/) noexcept { return 0.f; }
|
||||
};
|
||||
|
||||
TEST(ProjPredLegacyMore, AngleComputedButMissReturnsNullopt)
|
||||
{
|
||||
Projectile proj{ .m_origin = {0.f,0.f,0.f}, .m_launch_speed = 100.f, .m_gravity_scale = 1.f };
|
||||
Target target{ .m_origin = {10.f, 0.f, 0.f}, .m_velocity = {0.f,0.f,0.f}, .m_is_airborne = false };
|
||||
|
||||
using Engine = omath::projectile_prediction::ProjPredEngineLegacy<FakeEngineAngleButMiss>;
|
||||
Engine engine(9.8f, 0.1f, 1.f, 0.1f);
|
||||
|
||||
const auto res = engine.maybe_calculate_aim_point(proj, target);
|
||||
EXPECT_FALSE(res.has_value());
|
||||
}
|
||||
54
tests/general/unit_test_simplex_additional.cpp
Normal file
54
tests/general/unit_test_simplex_additional.cpp
Normal file
@@ -0,0 +1,54 @@
|
||||
#include "omath/collision/simplex.hpp"
|
||||
#include "omath/linear_algebra/vector3.hpp"
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
using omath::Vector3;
|
||||
|
||||
TEST(SimplexAdditional, RegionACSelectsAC)
|
||||
{
|
||||
// Construct points that force the Region AC branch where ac points toward the origin
|
||||
Vector3<float> a{1.f, 0.f, 0.f};
|
||||
Vector3<float> b{2.f, 0.f, 0.f};
|
||||
Vector3<float> c{0.f, 1.f, 0.f};
|
||||
|
||||
omath::collision::Simplex<omath::Vector3<float>> s;
|
||||
s = { a, b, c };
|
||||
|
||||
omath::Vector3<float> dir{0.f, 0.f, 0.f};
|
||||
const bool hit = s.handle(dir);
|
||||
|
||||
// Should not report a collision; simplex should reduce to {a, c}
|
||||
EXPECT_FALSE(hit);
|
||||
EXPECT_EQ(s.size(), 2u);
|
||||
EXPECT_TRUE(s[0] == a);
|
||||
EXPECT_TRUE(s[1] == c);
|
||||
// direction should be finite and non-zero
|
||||
EXPECT_TRUE(std::isfinite(dir.x));
|
||||
EXPECT_TRUE(std::isfinite(dir.y));
|
||||
EXPECT_TRUE(std::isfinite(dir.z));
|
||||
}
|
||||
|
||||
TEST(SimplexAdditional, AbcAboveSetsDirection)
|
||||
{
|
||||
// Choose triangle so abc points roughly toward the origin (abc · ao > 0)
|
||||
Vector3<float> a{-1.f, 0.f, 0.f};
|
||||
Vector3<float> b{0.f, 1.f, 0.f};
|
||||
Vector3<float> c{0.f, 0.f, 1.f};
|
||||
|
||||
omath::collision::Simplex<omath::Vector3<float>> s;
|
||||
s = { a, b, c };
|
||||
|
||||
omath::Vector3<float> dir{0.f, 0.f, 0.f};
|
||||
const bool hit = s.handle(dir);
|
||||
|
||||
EXPECT_FALSE(hit);
|
||||
|
||||
const auto ab = b - a;
|
||||
const auto ac = c - a;
|
||||
const auto abc = ab.cross(ac);
|
||||
|
||||
// direction should equal abc (above triangle case)
|
||||
EXPECT_NEAR(dir.x, abc.x, 1e-6f);
|
||||
EXPECT_NEAR(dir.y, abc.y, 1e-6f);
|
||||
EXPECT_NEAR(dir.z, abc.z, 1e-6f);
|
||||
}
|
||||
173
tests/general/unit_test_simplex_more.cpp
Normal file
173
tests/general/unit_test_simplex_more.cpp
Normal file
@@ -0,0 +1,173 @@
|
||||
#include "omath/collision/simplex.hpp"
|
||||
#include "omath/linear_algebra/vector3.hpp"
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
using omath::Vector3;
|
||||
using Simplex = omath::collision::Simplex<Vector3<float>>;
|
||||
|
||||
TEST(SimplexExtra, HandleLine_CollinearProducesPerp)
|
||||
{
|
||||
// a and b placed so ab points roughly same dir as ao and are collinear
|
||||
Vector3<float> a{2.f, 0.f, 0.f};
|
||||
Vector3<float> b{1.f, 0.f, 0.f};
|
||||
|
||||
Simplex s;
|
||||
s = {a, b};
|
||||
|
||||
Vector3<float> dir{0.f, 0.f, 0.f};
|
||||
const bool hit = s.handle(dir);
|
||||
|
||||
// Should not report collision for a line simplex
|
||||
EXPECT_FALSE(hit);
|
||||
// Direction must be finite and not zero
|
||||
EXPECT_TRUE(std::isfinite(dir.x));
|
||||
EXPECT_TRUE(std::isfinite(dir.y));
|
||||
EXPECT_TRUE(std::isfinite(dir.z));
|
||||
auto zero = Vector3<float>{0.f, 0.f, 0.f};
|
||||
EXPECT_FALSE(dir == zero);
|
||||
|
||||
// Ensure direction is (approximately) perpendicular to ab
|
||||
const auto ab = b - a;
|
||||
const float dot = dir.dot(ab);
|
||||
EXPECT_NEAR(dot, 0.0f, 1e-4f);
|
||||
}
|
||||
|
||||
TEST(SimplexExtra, HandleLine_NonCollinearProducesValidDirection)
|
||||
{
|
||||
Vector3<float> a{2.f, 0.f, 0.f};
|
||||
Vector3<float> b{1.f, 1.f, 0.f};
|
||||
|
||||
Simplex s;
|
||||
s = {a, b};
|
||||
|
||||
Vector3<float> dir{0.f, 0.f, 0.f};
|
||||
const bool hit = s.handle(dir);
|
||||
|
||||
EXPECT_FALSE(hit);
|
||||
EXPECT_TRUE(std::isfinite(dir.x));
|
||||
EXPECT_TRUE(std::isfinite(dir.y));
|
||||
EXPECT_TRUE(std::isfinite(dir.z));
|
||||
}
|
||||
|
||||
TEST(SimplexExtra, HandleTriangle_FlipWinding)
|
||||
{
|
||||
// Construct points where triangle winding will be flipped
|
||||
Vector3<float> a{1.f, 0.f, 0.f};
|
||||
Vector3<float> b{0.f, 1.f, 0.f};
|
||||
Vector3<float> c{0.f, -1.f, 0.f};
|
||||
|
||||
Simplex s;
|
||||
s = {a, b, c};
|
||||
|
||||
Vector3<float> dir{0.f, 0.f, 0.f};
|
||||
const bool hit = s.handle(dir);
|
||||
|
||||
EXPECT_FALSE(hit);
|
||||
EXPECT_TRUE(std::isfinite(dir.x));
|
||||
EXPECT_TRUE(std::isfinite(dir.y));
|
||||
EXPECT_TRUE(std::isfinite(dir.z));
|
||||
}
|
||||
|
||||
TEST(SimplexExtra, HandleTetrahedron_InsideReturnsTrue)
|
||||
{
|
||||
// Simple tetra that should contain the origin
|
||||
Vector3<float> a{1.f, 0.f, 0.f};
|
||||
Vector3<float> b{0.f, 1.f, 0.f};
|
||||
Vector3<float> c{0.f, 0.f, 1.f};
|
||||
Vector3<float> d{-0.2f, -0.2f, -0.2f};
|
||||
|
||||
Simplex s;
|
||||
s = {a, b, c, d};
|
||||
|
||||
Vector3<float> dir{0.f, 0.f, 0.f};
|
||||
const bool hit = s.handle(dir);
|
||||
|
||||
// If origin is inside, handle_tetrahedron should return true
|
||||
EXPECT_TRUE(hit);
|
||||
}
|
||||
// Additional sanity tests (avoid reusing Simplex alias above to prevent ambiguity)
|
||||
TEST(SimplexMore, PushFrontAndAccess)
|
||||
{
|
||||
omath::collision::Simplex<omath::Vector3<float>> s;
|
||||
s.push_front(omath::Vector3<float>{1.f,0.f,0.f});
|
||||
s.push_front(omath::Vector3<float>{2.f,0.f,0.f});
|
||||
s.push_front(omath::Vector3<float>{3.f,0.f,0.f});
|
||||
|
||||
EXPECT_EQ(s.size(), 3u);
|
||||
omath::Vector3<float> exp_front{3.f,0.f,0.f};
|
||||
omath::Vector3<float> exp_back{1.f,0.f,0.f};
|
||||
EXPECT_TRUE(s.front() == exp_front);
|
||||
EXPECT_TRUE(s.back() == exp_back);
|
||||
auto d = s.data();
|
||||
EXPECT_TRUE(d[0] == exp_front);
|
||||
}
|
||||
|
||||
TEST(SimplexMore, ClearAndEmpty)
|
||||
{
|
||||
omath::collision::Simplex<omath::Vector3<float>> s;
|
||||
s.push_front(omath::Vector3<float>{1.f,1.f,1.f});
|
||||
EXPECT_FALSE(s.empty());
|
||||
s.clear();
|
||||
EXPECT_TRUE(s.empty());
|
||||
}
|
||||
|
||||
TEST(SimplexMore, HandleLineCollinearProducesPerp)
|
||||
{
|
||||
omath::collision::Simplex<omath::Vector3<float>> s;
|
||||
s = { omath::Vector3<float>{2.f,0.f,0.f}, omath::Vector3<float>{1.f,0.f,0.f} };
|
||||
omath::Vector3<float> dir{0.f,0.f,0.f};
|
||||
const bool res = s.handle(dir);
|
||||
EXPECT_FALSE(res);
|
||||
EXPECT_GT(dir.length_sqr(), 0.0f);
|
||||
}
|
||||
|
||||
TEST(SimplexMore, HandleTriangleFlipWinding)
|
||||
{
|
||||
const omath::Vector3<float> a{1.f,0.f,0.f};
|
||||
const omath::Vector3<float> b{0.f,1.f,0.f};
|
||||
const omath::Vector3<float> c{0.f,0.f,1.f};
|
||||
omath::collision::Simplex<omath::Vector3<float>> s;
|
||||
s = { a, b, c };
|
||||
omath::Vector3<float> dir{0.f,0.f,0.f};
|
||||
|
||||
const auto ab = b - a;
|
||||
const auto ac = c - a;
|
||||
const auto abc = ab.cross(ac);
|
||||
|
||||
const bool res = s.handle(dir);
|
||||
EXPECT_FALSE(res);
|
||||
const auto expected = -abc;
|
||||
EXPECT_NEAR(dir.x, expected.x, 1e-6f);
|
||||
EXPECT_NEAR(dir.y, expected.y, 1e-6f);
|
||||
EXPECT_NEAR(dir.z, expected.z, 1e-6f);
|
||||
}
|
||||
|
||||
TEST(SimplexMore, HandleTetrahedronInsideTrue)
|
||||
{
|
||||
omath::collision::Simplex<omath::Vector3<float>> s;
|
||||
s = { omath::Vector3<float>{1.f,0.f,0.f}, omath::Vector3<float>{0.f,1.f,0.f}, omath::Vector3<float>{0.f,0.f,1.f}, omath::Vector3<float>{-1.f,-1.f,-1.f} };
|
||||
omath::Vector3<float> dir{0.f,0.f,0.f};
|
||||
const bool inside = s.handle(dir);
|
||||
EXPECT_TRUE(inside);
|
||||
}
|
||||
|
||||
TEST(SimplexMore, HandlePointSetsDirection)
|
||||
{
|
||||
omath::collision::Simplex<omath::Vector3<float>> s;
|
||||
s = { omath::Vector3<float>{1.f,2.f,3.f} };
|
||||
omath::Vector3<float> dir{0.f,0.f,0.f};
|
||||
EXPECT_FALSE(s.handle(dir));
|
||||
EXPECT_NEAR(dir.x, -1.f, 1e-6f);
|
||||
EXPECT_NEAR(dir.y, -2.f, 1e-6f);
|
||||
EXPECT_NEAR(dir.z, -3.f, 1e-6f);
|
||||
}
|
||||
|
||||
TEST(SimplexMore, HandleLineReducesToPointWhenAoOpposite)
|
||||
{
|
||||
omath::collision::Simplex<omath::Vector3<float>> s;
|
||||
s = { omath::Vector3<float>{1.f,0.f,0.f}, omath::Vector3<float>{2.f,0.f,0.f} };
|
||||
omath::Vector3<float> dir{0.f,0.f,0.f};
|
||||
EXPECT_FALSE(s.handle(dir));
|
||||
EXPECT_EQ(s.size(), 1u);
|
||||
EXPECT_NEAR(dir.x, -1.f, 1e-6f);
|
||||
}
|
||||
@@ -10,6 +10,61 @@
|
||||
|
||||
using namespace omath;
|
||||
|
||||
TEST(Vector3More, ConstructorsAndEquality)
|
||||
{
|
||||
Vector3<float> a;
|
||||
EXPECT_EQ(a.x, 0.f);
|
||||
EXPECT_EQ(a.y, 0.f);
|
||||
EXPECT_EQ(a.z, 0.f);
|
||||
|
||||
Vector3<float> b{1.f, 2.f, 3.f};
|
||||
EXPECT_EQ(b.x, 1.f);
|
||||
EXPECT_EQ(b.y, 2.f);
|
||||
EXPECT_EQ(b.z, 3.f);
|
||||
|
||||
Vector3<float> c = b;
|
||||
EXPECT_EQ(c, b);
|
||||
}
|
||||
|
||||
TEST(Vector3More, ArithmeticAndDotCross)
|
||||
{
|
||||
Vector3<float> a{1.f, 0.f, 0.f};
|
||||
Vector3<float> b{0.f, 1.f, 0.f};
|
||||
auto c = a + b;
|
||||
const Vector3<float> expect_c{1.f,1.f,0.f};
|
||||
EXPECT_EQ(c, expect_c);
|
||||
|
||||
auto d = a - b;
|
||||
const Vector3<float> expect_d{1.f,-1.f,0.f};
|
||||
EXPECT_EQ(d, expect_d);
|
||||
|
||||
auto e = a * 2.f;
|
||||
const Vector3<float> expect_e{2.f,0.f,0.f};
|
||||
EXPECT_EQ(e, expect_e);
|
||||
|
||||
EXPECT_FLOAT_EQ(a.dot(b), 0.f);
|
||||
// manual cross product check
|
||||
auto cr = Vector3<float>{ a.y * b.z - a.z * b.y,
|
||||
a.z * b.x - a.x * b.z,
|
||||
a.x * b.y - a.y * b.x };
|
||||
const Vector3<float> expect_cr{0.f,0.f,1.f};
|
||||
EXPECT_EQ(cr, expect_cr);
|
||||
}
|
||||
|
||||
TEST(Vector3More, NormalizationEdgeCases)
|
||||
{
|
||||
Vector3<double> z{0.0,0.0,0.0};
|
||||
auto zn = z.normalized();
|
||||
EXPECT_DOUBLE_EQ(zn.x, 0.0);
|
||||
EXPECT_DOUBLE_EQ(zn.y, 0.0);
|
||||
EXPECT_DOUBLE_EQ(zn.z, 0.0);
|
||||
|
||||
Vector3<double> v{3.0,4.0,0.0};
|
||||
auto vn = v.normalized();
|
||||
EXPECT_NEAR(vn.x, 0.6, 1e-12);
|
||||
EXPECT_NEAR(vn.y, 0.8, 1e-12);
|
||||
}
|
||||
|
||||
class UnitTestVector3 : public ::testing::Test
|
||||
{
|
||||
protected:
|
||||
|
||||
@@ -11,6 +11,32 @@
|
||||
|
||||
using namespace omath;
|
||||
|
||||
TEST(Vector4More, ConstructorsAndClamp)
|
||||
{
|
||||
Vector4<float> a;
|
||||
EXPECT_EQ(a.x, 0.f);
|
||||
EXPECT_EQ(a.y, 0.f);
|
||||
EXPECT_EQ(a.z, 0.f);
|
||||
EXPECT_EQ(a.w, 0.f);
|
||||
|
||||
Vector4<float> b{1.f, -2.f, 3.5f, 4.f};
|
||||
b.clamp(0.f, 3.f);
|
||||
EXPECT_GE(b.x, 0.f);
|
||||
EXPECT_GE(b.y, 0.f);
|
||||
EXPECT_LE(b.z, 3.f);
|
||||
}
|
||||
|
||||
TEST(Vector4More, ComparisonsAndHashFormatter)
|
||||
{
|
||||
Vector4<int> a{1,2,3,4};
|
||||
Vector4<int> b{1,2,3,5};
|
||||
EXPECT_NE(a, b);
|
||||
|
||||
// exercise to_string via formatting if available by converting via std::format
|
||||
// call length and comparison to exercise more branches
|
||||
EXPECT_LT(a.length(), b.length());
|
||||
}
|
||||
|
||||
class UnitTestVector4 : public ::testing::Test
|
||||
{
|
||||
protected:
|
||||
|
||||
Reference in New Issue
Block a user