From 7cc24c5e0e7ec72c1a3e92d567c6e68f138d4fc1 Mon Sep 17 00:00:00 2001 From: Saikari Date: Thu, 25 Dec 2025 02:11:22 +0300 Subject: [PATCH 1/2] Implement coverage for Windows/Linux/MacOS (#126) --- .github/workflows/cmake-multi-platform.yml | 104 ++++++- .gitignore | 1 + CMakeLists.txt | 6 +- cmake/Coverage.cmake | 122 ++++++++ include/omath/collision/simplex.hpp | 10 +- include/omath/utility/color.hpp | 41 ++- scripts/coverage-llvm.sh | 169 +++++++++++ scripts/coverage.bat.in | 8 + scripts/coverage.ps1.in | 132 +++++++++ tests/CMakeLists.txt | 5 + tests/engines/unit_test_traits_engines.cpp | 297 ++++++++++++++++++++ tests/general/unit_test_collision_extra.cpp | 23 ++ 12 files changed, 880 insertions(+), 38 deletions(-) create mode 100644 cmake/Coverage.cmake create mode 100755 scripts/coverage-llvm.sh create mode 100644 scripts/coverage.bat.in create mode 100644 scripts/coverage.ps1.in create mode 100644 tests/engines/unit_test_traits_engines.cpp diff --git a/.github/workflows/cmake-multi-platform.yml b/.github/workflows/cmake-multi-platform.yml index 9269fb89..062e8c1e 100644 --- a/.github/workflows/cmake-multi-platform.yml +++ b/.github/workflows/cmake-multi-platform.yml @@ -24,13 +24,15 @@ jobs: triplet: x64-linux runner: ubuntu-latest preset: linux-release-vcpkg + coverage: true install_cmd: | wget -qO- https://apt.llvm.org/llvm-snapshot.gpg.key | sudo tee /etc/apt/trusted.gpg.d/apt.llvm.org.asc sudo add-apt-repository -y "deb http://apt.llvm.org/noble/ llvm-toolchain-noble-21 main" sudo apt-get update sudo apt-get install -y git build-essential cmake ninja-build \ zip unzip curl pkg-config ca-certificates \ - clang-21 lld-21 libc++-21-dev libc++abi-21-dev + clang-21 lld-21 libc++-21-dev libc++abi-21-dev \ + llvm-21 sudo update-alternatives --install /usr/bin/cc cc /usr/bin/clang-21 100 sudo update-alternatives --install /usr/bin/c++ c++ /usr/bin/clang++-21 100 sudo update-alternatives --install /usr/bin/lld lld /usr/bin/lld-21 100 @@ -38,6 +40,7 @@ jobs: triplet: x86-linux runner: ubuntu-latest preset: linux-release-vcpkg-x86 + coverage: false install_cmd: | # Add LLVM 21 repository wget -qO- https://apt.llvm.org/llvm-snapshot.gpg.key | sudo tee /etc/apt/trusted.gpg.d/apt.llvm.org.asc @@ -65,6 +68,7 @@ jobs: triplet: arm64-linux runner: ubuntu-24.04-arm preset: linux-release-vcpkg-arm64 + coverage: false install_cmd: | wget -qO- https://apt.llvm.org/llvm-snapshot.gpg.key | sudo tee /etc/apt/trusted.gpg.d/apt.llvm.org.asc sudo add-apt-repository -y "deb http://apt.llvm.org/noble/ llvm-toolchain-noble-21 main" @@ -91,7 +95,7 @@ jobs: - name: Set up vcpkg shell: bash run: | - git clone --branch 2025.12.12 --single-branch https://github.com/microsoft/vcpkg "$VCPKG_ROOT" + git clone https://github.com/microsoft/vcpkg "$VCPKG_ROOT" cd "$VCPKG_ROOT" ./bootstrap-vcpkg.sh @@ -102,6 +106,7 @@ jobs: -DVCPKG_INSTALL_OPTIONS="--allow-unsupported" \ -DOMATH_BUILD_TESTS=ON \ -DOMATH_BUILD_BENCHMARK=OFF \ + -DOMATH_ENABLE_COVERAGE=${{ matrix.coverage == true && 'ON' || 'OFF' }} \ -DVCPKG_MANIFEST_FEATURES="imgui;avx2;tests" - name: Build @@ -112,6 +117,25 @@ jobs: shell: bash run: ./out/Release/unit_tests + - name: Run Coverage + if: ${{ matrix.coverage == true }} + shell: bash + run: | + sudo apt-get install lcov + chmod +x scripts/coverage-llvm.sh + ./scripts/coverage-llvm.sh \ + "${{ github.workspace }}" \ + "cmake-build/build/${{ matrix.preset }}" \ + "./out/Release/unit_tests" \ + "cmake-build/build/${{ matrix.preset }}/coverage" + + - name: Upload Coverage Report + if: ${{ matrix.coverage == true }} + uses: actions/upload-artifact@v4 + with: + name: coverage-report-linux + path: cmake-build/build/${{ matrix.preset }}/coverage/ + - name: Upload logs on failure if: ${{ failure() }} uses: actions/upload-artifact@v4 @@ -164,7 +188,7 @@ jobs: - name: Configure (cmake --preset) shell: bash - run: cmake --preset ${{ matrix.preset }} -DOMATH_BUILD_TESTS=ON -DOMATH_BUILD_BENCHMARK=OFF -DVCPKG_MANIFEST_FEATURES="imgui;avx2;tests" + run: cmake --preset ${{ matrix.preset }} -DOMATH_BUILD_TESTS=ON -DOMATH_BUILD_BENCHMARK=OFF -DOMATH_ENABLE_COVERAGE=OFF -DVCPKG_MANIFEST_FEATURES="imgui;avx2;tests" - name: Build shell: bash @@ -174,6 +198,36 @@ jobs: shell: bash run: ./out/Release/unit_tests.exe + - name: Install OpenCppCoverage with Chocolatey + if: ${{ matrix.triplet == 'x64-windows' }} + run: choco install opencppcoverage -y + + - name: Build Debug for Coverage + if: ${{ matrix.triplet == 'x64-windows' }} + shell: bash + run: | + cmake --preset ${{ matrix.preset }} \ + -DOMATH_BUILD_TESTS=ON \ + -DOMATH_BUILD_BENCHMARK=OFF \ + -DOMATH_ENABLE_COVERAGE=ON \ + -DCMAKE_BUILD_TYPE=Debug \ + -DVCPKG_MANIFEST_FEATURES="imgui;avx2;tests" + cmake --build cmake-build/build/${{ matrix.preset }} --config Debug --target unit_tests omath + + - name: Run Coverage + if: ${{ matrix.triplet == 'x64-windows' }} + shell: pwsh + run: | + $env:Path = "C:\Program Files\OpenCppCoverage;$env:Path" + cmake --build cmake-build/build/${{ matrix.preset }} --target coverage --config Debug + + - name: Upload Coverage + if: ${{ matrix.triplet == 'x64-windows' }} + uses: actions/upload-artifact@v4 + with: + name: coverage-report-windows + path: cmake-build/build/${{ matrix.preset }}/coverage/ + - name: Upload logs on failure if: ${{ failure() }} uses: actions/upload-artifact@v4 @@ -193,13 +247,15 @@ jobs: matrix: include: - name: macOS (AppleClang) (arm64-osx) - runner: macOS-latest + runner: macos-latest preset: darwin-release-vcpkg triplet: arm64-osx + coverage: true - name: macOS (AppleClang) (x64-osx) runner: macos-15-intel preset: darwin-release-vcpkg-x64 triplet: x64-osx + coverage: false fail-fast: false env: VCPKG_ROOT: ${{ github.workspace }}/vcpkg @@ -217,20 +273,44 @@ jobs: - name: Set up vcpkg shell: bash run: | - git clone --branch 2025.12.12 --single-branch https://github.com/microsoft/vcpkg "$VCPKG_ROOT" + git clone https://github.com/microsoft/vcpkg "$VCPKG_ROOT" - name: Configure (cmake --preset) shell: bash - run: cmake --preset ${{matrix.preset}} -DOMATH_BUILD_TESTS=ON -DOMATH_BUILD_BENCHMARK=OFF -DVCPKG_MANIFEST_FEATURES="imgui;avx2;tests" + run: | + cmake --preset ${{ matrix.preset }} \ + -DOMATH_BUILD_TESTS=ON \ + -DOMATH_BUILD_BENCHMARK=OFF \ + -DOMATH_ENABLE_COVERAGE=${{ matrix.coverage == true && 'ON' || 'OFF' }} \ + -DVCPKG_MANIFEST_FEATURES="imgui;avx2;tests" - name: Build shell: bash - run: cmake --build cmake-build/build/${{matrix.preset}} --target unit_tests omath + run: cmake --build cmake-build/build/${{ matrix.preset }} --target unit_tests omath - name: Run unit_tests shell: bash run: ./out/Release/unit_tests + - name: Run Coverage + if: ${{ matrix.coverage == true }} + shell: bash + run: | + brew install lcov + chmod +x scripts/coverage-llvm.sh + ./scripts/coverage-llvm.sh \ + "${{ github.workspace }}" \ + "cmake-build/build/${{ matrix.preset }}" \ + "./out/Release/unit_tests" \ + "cmake-build/build/${{ matrix.preset }}/coverage" + + - name: Upload Coverage Report + if: ${{ matrix.coverage == true }} + uses: actions/upload-artifact@v4 + with: + name: coverage-report-macos + path: cmake-build/build/${{ matrix.preset }}/coverage/ + - name: Upload logs on failure if: ${{ failure() }} uses: actions/upload-artifact@v4 @@ -268,7 +348,7 @@ jobs: - name: Set up vcpkg shell: bash run: | - git clone --branch 2025.12.12 --single-branch https://github.com/microsoft/vcpkg "$VCPKG_ROOT" + git clone https://github.com/microsoft/vcpkg "$VCPKG_ROOT" cd "$VCPKG_ROOT" ./bootstrap-vcpkg.sh @@ -330,7 +410,7 @@ jobs: # Build vcpkg in /tmp to avoid sshfs timestamp sync issues export VCPKG_ROOT=/tmp/vcpkg rm -rf "$VCPKG_ROOT" - git clone --branch 2025.12.12 --single-branch https://github.com/microsoft/vcpkg "$VCPKG_ROOT" + git clone https://github.com/microsoft/vcpkg "$VCPKG_ROOT" cd "$VCPKG_ROOT" ./bootstrap-vcpkg.sh cd - @@ -399,7 +479,7 @@ jobs: - name: Set up vcpkg shell: bash run: | - git clone --branch 2025.12.12 --single-branch https://github.com/microsoft/vcpkg "$VCPKG_ROOT" + git clone https://github.com/microsoft/vcpkg "$VCPKG_ROOT" cd "$VCPKG_ROOT" ./bootstrap-vcpkg.sh @@ -468,7 +548,7 @@ jobs: - name: Set up vcpkg shell: bash run: | - git clone --branch 2025.12.12 --single-branch https://github.com/microsoft/vcpkg "$VCPKG_ROOT" + git clone https://github.com/microsoft/vcpkg "$VCPKG_ROOT" cd "$VCPKG_ROOT" ./bootstrap-vcpkg.sh @@ -554,7 +634,7 @@ jobs: - name: Set up vcpkg run: | - git clone --branch 2025.12.12 --single-branch https://github.com/microsoft/vcpkg "$VCPKG_ROOT" + git clone https://github.com/microsoft/vcpkg "$VCPKG_ROOT" cd "$VCPKG_ROOT" ./bootstrap-vcpkg.sh diff --git a/.gitignore b/.gitignore index 8dcedc68..24dd39eb 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,5 @@ /extlibs/vcpkg .idea/workspace.xml /build/ +/clang-coverage/ *.gcov \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index f7f31acf..581ea7ec 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -5,6 +5,7 @@ project(omath VERSION ${OMATH_VERSION} LANGUAGES CXX) include(CMakePackageConfigHelpers) include(CheckCXXCompilerFlag) +include(cmake/Coverage.cmake) if (MSVC) check_cxx_compiler_flag("/arch:AVX2" COMPILER_SUPPORTS_AVX2) @@ -23,7 +24,7 @@ option(OMATH_STATIC_MSVC_RUNTIME_LIBRARY "Force Omath to link static runtime" OF option(OMATH_SUPRESS_SAFETY_CHECKS "Supress some safety checks in release build to improve general performance" ON) option(OMATH_USE_UNITY_BUILD "Will enable unity build to speed up compilation" OFF) option(OMATH_ENABLE_LEGACY "Will enable legacy classes that MUST be used ONLY for backward compatibility" ON) - +option(OMATH_ENABLE_COVERAGE "Enable coverage" OFF) if (VCPKG_MANIFEST_FEATURES) foreach (omath_feature IN LISTS VCPKG_MANIFEST_FEATURES) @@ -145,6 +146,9 @@ target_compile_features(${PROJECT_NAME} PUBLIC cxx_std_23) if (OMATH_BUILD_TESTS) add_subdirectory(tests) target_compile_definitions(${PROJECT_NAME} PUBLIC OMATH_BUILD_TESTS) + if(OMATH_ENABLE_COVERAGE) + omath_setup_coverage(${PROJECT_NAME}) + endif() endif () if (OMATH_BUILD_BENCHMARK) diff --git a/cmake/Coverage.cmake b/cmake/Coverage.cmake new file mode 100644 index 00000000..ac445b02 --- /dev/null +++ b/cmake/Coverage.cmake @@ -0,0 +1,122 @@ +# cmake/Coverage.cmake +include_guard(GLOBAL) + +function(omath_setup_coverage TARGET_NAME) + if(ANDROID OR IOS OR EMSCRIPTEN) + return() + endif() + + if(CMAKE_CXX_COMPILER_ID MATCHES "Clang|AppleClang") + # Apply to ALL configs when coverage is enabled (not just Debug) + target_compile_options(${TARGET_NAME} PRIVATE + -fprofile-instr-generate + -fcoverage-mapping + -g + -O0 + ) + target_link_options(${TARGET_NAME} PRIVATE + -fprofile-instr-generate + -fcoverage-mapping + ) + + elseif(CMAKE_CXX_COMPILER_ID STREQUAL "GNU") + target_compile_options(${TARGET_NAME} PRIVATE + --coverage + -g + -O0 + ) + target_link_options(${TARGET_NAME} PRIVATE + --coverage + ) + + elseif(MSVC) + target_compile_options(${TARGET_NAME} PRIVATE + /Zi + /Od + /Ob0 + ) + target_link_options(${TARGET_NAME} PRIVATE + /DEBUG:FULL + /INCREMENTAL:NO + ) + endif() + + # Create coverage target only once + if(TARGET coverage) + return() + endif() + + if(MSVC OR MINGW) + # Windows: OpenCppCoverage + find_program(OPENCPPCOVERAGE_EXECUTABLE + NAMES OpenCppCoverage OpenCppCoverage.exe + PATHS + "$ENV{ProgramFiles}/OpenCppCoverage" + "$ENV{ProgramW6432}/OpenCppCoverage" + "C:/Program Files/OpenCppCoverage" + DOC "Path to OpenCppCoverage executable" + ) + + if(NOT OPENCPPCOVERAGE_EXECUTABLE) + message(WARNING "OpenCppCoverage not found. Install with: choco install opencppcoverage") + set(OPENCPPCOVERAGE_EXECUTABLE "C:/Program Files/OpenCppCoverage/OpenCppCoverage.exe") + else() + message(STATUS "Found OpenCppCoverage: ${OPENCPPCOVERAGE_EXECUTABLE}") + endif() + + file(TO_NATIVE_PATH "${CMAKE_SOURCE_DIR}" COVERAGE_ROOT_PATH) + file(TO_NATIVE_PATH "${CMAKE_BINARY_DIR}/coverage" COVERAGE_OUTPUT_PATH) + file(TO_NATIVE_PATH "${CMAKE_BINARY_DIR}/coverage.xml" COVERAGE_XML_PATH) + file(TO_NATIVE_PATH "${OPENCPPCOVERAGE_EXECUTABLE}" OPENCPPCOVERAGE_NATIVE) + + add_custom_target(coverage + DEPENDS unit_tests + COMMAND "${OPENCPPCOVERAGE_NATIVE}" + --verbose + --sources "${COVERAGE_ROOT_PATH}" + --modules "${COVERAGE_ROOT_PATH}" + --excluded_sources "*\\tests\\*" + --excluded_sources "*\\gtest\\*" + --excluded_sources "*\\googletest\\*" + --excluded_sources "*\\_deps\\*" + --excluded_sources "*\\vcpkg_installed\\*" + --export_type "html:${COVERAGE_OUTPUT_PATH}" + --export_type "cobertura:${COVERAGE_XML_PATH}" + --cover_children + -- "$" + WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" + COMMENT "Running OpenCppCoverage" + ) + + elseif(CMAKE_CXX_COMPILER_ID MATCHES "Clang|AppleClang") + # Linux/macOS: LLVM coverage via script + add_custom_target(coverage + DEPENDS unit_tests + COMMAND bash "${CMAKE_SOURCE_DIR}/scripts/coverage-llvm.sh" + "${CMAKE_SOURCE_DIR}" + "${CMAKE_BINARY_DIR}" + "$" + "${CMAKE_BINARY_DIR}/coverage" + WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" + COMMENT "Running LLVM coverage" + ) + + elseif(CMAKE_CXX_COMPILER_ID STREQUAL "GNU") + # GCC: lcov/gcov + add_custom_target(coverage + DEPENDS unit_tests + COMMAND $ || true + COMMAND lcov --capture --directory "${CMAKE_BINARY_DIR}" + --output-file "${CMAKE_BINARY_DIR}/coverage.info" + --ignore-errors mismatch,gcov + COMMAND lcov --remove "${CMAKE_BINARY_DIR}/coverage.info" + "*/tests/*" "*/gtest/*" "*/googletest/*" "*/_deps/*" "/usr/*" + --output-file "${CMAKE_BINARY_DIR}/coverage.info" + --ignore-errors unused + COMMAND genhtml "${CMAKE_BINARY_DIR}/coverage.info" + --output-directory "${CMAKE_BINARY_DIR}/coverage" + WORKING_DIRECTORY "${CMAKE_BINARY_DIR}" + COMMENT "Running lcov/genhtml" + ) + endif() +endfunction() diff --git a/include/omath/collision/simplex.hpp b/include/omath/collision/simplex.hpp index 7820f53b..d1043962 100644 --- a/include/omath/collision/simplex.hpp +++ b/include/omath/collision/simplex.hpp @@ -139,10 +139,12 @@ namespace omath::collision [[nodiscard]] static constexpr V any_perp(const V& v) { - for (const auto& dir : {V{1, 0, 0}, {0, 1, 0}, {0, 0, 1}}) - if (const auto d = v.cross(dir); !near_zero(d)) - return d; - std::unreachable(); + // If v is not parallel to x-axis, cross with x works + if (const auto d = v.cross(V{1, 0, 0}); !near_zero(d)) + return d; + + // v is parallel to x-axis (v = (a,0,0)), so v × (0,1,0) = (0,0,a) ≠ 0 + return v.cross(V{0, 1, 0}); } [[nodiscard]] diff --git a/include/omath/utility/color.hpp b/include/omath/utility/color.hpp index befed6ee..a1a3c437 100644 --- a/include/omath/utility/color.hpp +++ b/include/omath/utility/color.hpp @@ -46,27 +46,26 @@ namespace omath switch (i % 6) { - case 0: - r = value, g = t, b = p; - break; - case 1: - r = q, g = value, b = p; - break; - case 2: - r = p, g = value, b = t; - break; - case 3: - r = p, g = q, b = value; - break; - case 4: - r = t, g = p, b = value; - break; - case 5: - r = value, g = p, b = q; - break; - - default: - return {0.f, 0.f, 0.f, 0.f}; + case 0: + r = value, g = t, b = p; + break; + case 1: + r = q, g = value, b = p; + break; + case 2: + r = p, g = value, b = t; + break; + case 3: + r = p, g = q, b = value; + break; + case 4: + r = t, g = p, b = value; + break; + case 5: + r = value, g = p, b = q; + break; + default: + std::unreachable(); } return {r, g, b, 1.f}; diff --git a/scripts/coverage-llvm.sh b/scripts/coverage-llvm.sh new file mode 100755 index 00000000..8f45249d --- /dev/null +++ b/scripts/coverage-llvm.sh @@ -0,0 +1,169 @@ +#!/usr/bin/env bash +# scripts/coverage-llvm.sh +# LLVM coverage script that generates LCOV-style reports + +set -e + +SOURCE_DIR="${1:-.}" +BINARY_DIR="${2:-cmake-build/build}" +TEST_BINARY="${3:-}" +OUTPUT_DIR="${4:-${BINARY_DIR}/coverage}" + +echo "[*] Source dir: ${SOURCE_DIR}" +echo "[*] Binary dir: ${BINARY_DIR}" +echo "[*] Output dir: ${OUTPUT_DIR}" + +# Find llvm tools - handle versioned names (Linux) and xcrun (macOS) +find_llvm_tool() { + local tool_name="$1" + + # macOS: use xcrun + if [[ "$(uname)" == "Darwin" ]]; then + if xcrun --find "${tool_name}" &>/dev/null; then + echo "xcrun ${tool_name}" + return 0 + fi + fi + + # Try versioned names (Linux with LLVM 21, 20, 19, etc.) + for version in 21 20 19 18 17 ""; do + local versioned_name="${tool_name}${version:+-$version}" + if command -v "${versioned_name}" &>/dev/null; then + echo "${versioned_name}" + return 0 + fi + done + + echo "" + return 1 +} + +LLVM_PROFDATA=$(find_llvm_tool "llvm-profdata") +LLVM_COV=$(find_llvm_tool "llvm-cov") + +if [[ -z "${LLVM_PROFDATA}" ]] || [[ -z "${LLVM_COV}" ]]; then + echo "Error: llvm-profdata or llvm-cov not found" >&2 + echo "On Linux, install llvm or clang package" >&2 + echo "On macOS, Xcode command line tools should provide these" >&2 + exit 1 +fi + +echo "[*] Using: ${LLVM_PROFDATA}" +echo "[*] Using: ${LLVM_COV}" + +# Find test binary +if [[ -z "${TEST_BINARY}" ]]; then + for path in \ + "${SOURCE_DIR}/out/Debug/unit_tests" \ + "${SOURCE_DIR}/out/Release/unit_tests" \ + "${BINARY_DIR}/unit_tests" \ + "${BINARY_DIR}/tests/unit_tests"; do + if [[ -x "${path}" ]]; then + TEST_BINARY="${path}" + break + fi + done +fi + +if [[ -z "${TEST_BINARY}" ]] || [[ ! -x "${TEST_BINARY}" ]]; then + echo "Error: unit_tests binary not found" >&2 + echo "Searched in: out/Debug, out/Release, ${BINARY_DIR}" >&2 + exit 1 +fi + +echo "[*] Test binary: ${TEST_BINARY}" + +# Clean previous coverage data +rm -rf "${OUTPUT_DIR}" +rm -f "${BINARY_DIR}"/*.profraw "${BINARY_DIR}"/*.profdata +mkdir -p "${OUTPUT_DIR}" + +# Run tests with profiling enabled +PROFILE_FILE="${BINARY_DIR}/default_%p.profraw" +echo "[*] Running tests with LLVM_PROFILE_FILE=${PROFILE_FILE}" + +export LLVM_PROFILE_FILE="${PROFILE_FILE}" +"${TEST_BINARY}" || echo "[!] Some tests failed, continuing with coverage..." + +# Find all generated .profraw files +PROFRAW_FILES=$(find "${BINARY_DIR}" -name "*.profraw" -type f 2>/dev/null) +if [[ -z "${PROFRAW_FILES}" ]]; then + # Also check current directory + PROFRAW_FILES=$(find . -maxdepth 3 -name "*.profraw" -type f 2>/dev/null) +fi + +if [[ -z "${PROFRAW_FILES}" ]]; then + echo "Error: No .profraw files generated" >&2 + echo "Make sure the binary was built with -fprofile-instr-generate -fcoverage-mapping" >&2 + exit 1 +fi + +echo "[*] Found profraw files:" +echo "${PROFRAW_FILES}" + +# Merge profiles +PROFDATA_FILE="${BINARY_DIR}/coverage.profdata" +echo "[*] Merging profiles into ${PROFDATA_FILE}" +${LLVM_PROFDATA} merge -sparse ${PROFRAW_FILES} -o "${PROFDATA_FILE}" + +# Generate text summary +echo "[*] Coverage Summary:" +${LLVM_COV} report "${TEST_BINARY}" \ + -instr-profile="${PROFDATA_FILE}" \ + -ignore-filename-regex="tests/.*" \ + -ignore-filename-regex="googletest/.*" \ + -ignore-filename-regex="gtest/.*" \ + -ignore-filename-regex="_deps/.*" \ + -ignore-filename-regex="vcpkg_installed/.*" + +# Export lcov format (for tools like codecov) +LCOV_FILE="${OUTPUT_DIR}/coverage.lcov" +echo "[*] Exporting LCOV format to ${LCOV_FILE}" +${LLVM_COV} export "${TEST_BINARY}" \ + -instr-profile="${PROFDATA_FILE}" \ + -format=lcov \ + -ignore-filename-regex="tests/.*" \ + -ignore-filename-regex="googletest/.*" \ + -ignore-filename-regex="gtest/.*" \ + -ignore-filename-regex="_deps/.*" \ + -ignore-filename-regex="vcpkg_installed/.*" \ + > "${LCOV_FILE}" || true + +# Generate LCOV-style HTML report using genhtml +if command -v genhtml >/dev/null 2>&1; then + echo "[*] Generating LCOV-style HTML report using genhtml" + genhtml "${LCOV_FILE}" \ + --ignore-errors inconsistent,corrupt \ + --output-directory "${OUTPUT_DIR}" \ + --title "Omath Coverage Report" \ + --show-details \ + --legend \ + --demangle-cpp \ + --num-spaces 4 \ + --sort \ + --function-coverage \ + --branch-coverage + + echo "[*] LCOV-style HTML report generated at: ${OUTPUT_DIR}/index.html" +else + echo "[!] genhtml not found. Installing lcov package..." + echo "[!] On Ubuntu/Debian: sudo apt-get install lcov" + echo "[!] On macOS: brew install lcov" + echo "[!] Falling back to LLVM HTML report..." + + # Fall back to LLVM HTML report + ${LLVM_COV} show "${TEST_BINARY}" \ + -instr-profile="${PROFDATA_FILE}" \ + -format=html \ + -output-dir="${OUTPUT_DIR}" \ + -show-line-counts-or-regions \ + -show-instantiations=false \ + -ignore-filename-regex="tests/.*" \ + -ignore-filename-regex="googletest/.*" \ + -ignore-filename-regex="gtest/.*" \ + -ignore-filename-regex="_deps/.*" \ + -ignore-filename-regex="vcpkg_installed/.*" +fi + +echo "[*] Coverage report generated at: ${OUTPUT_DIR}/index.html" +echo "[*] LCOV file at: ${LCOV_FILE}" diff --git a/scripts/coverage.bat.in b/scripts/coverage.bat.in new file mode 100644 index 00000000..1e619a00 --- /dev/null +++ b/scripts/coverage.bat.in @@ -0,0 +1,8 @@ +@echo off +REM scripts/coverage.bat.in +REM Simple wrapper to run coverage.ps1 + +set SOURCE_DIR=@CMAKE_SOURCE_DIR@ +set BINARY_DIR=@CMAKE_BINARY_DIR@ + +powershell -ExecutionPolicy Bypass -File "%BINARY_DIR%\scripts\coverage.ps1" -SourceDir "%SOURCE_DIR%" -BinaryDir "%BINARY_DIR%" %* diff --git a/scripts/coverage.ps1.in b/scripts/coverage.ps1.in new file mode 100644 index 00000000..6f3abc32 --- /dev/null +++ b/scripts/coverage.ps1.in @@ -0,0 +1,132 @@ +# scripts/coverage.ps1.in +# Windows coverage script using OpenCppCoverage + +param( + [Parameter(Mandatory=$true)] + [string]$SourceDir, + + [Parameter(Mandatory=$true)] + [string]$BinaryDir, + + [string]$TestBinary = "", + [switch]$Cobertura, + [switch]$Html +) + +$ErrorActionPreference = "Stop" + +# CMake-injected variables +$LCOV_IGNORE_ERRORS = '@LCOV_IGNORE_ERRORS@' + +# Resolve paths +$SourceDir = Resolve-Path $SourceDir +$BinaryDir = Resolve-Path $BinaryDir + +Write-Host "[*] Source directory: $SourceDir" -ForegroundColor Cyan +Write-Host "[*] Binary directory: $BinaryDir" -ForegroundColor Cyan + +# Find test binary +if (-not $TestBinary) { + $searchPaths = @( + "$BinaryDir\Debug\unit_tests.exe", + "$BinaryDir\Release\unit_tests.exe", + "$BinaryDir\unit_tests.exe", + "$SourceDir\out\Debug\unit_tests.exe", + "$SourceDir\out\Release\unit_tests.exe" + ) + + foreach ($path in $searchPaths) { + if (Test-Path $path) { + $TestBinary = $path + break + } + } +} + +if (-not $TestBinary -or -not (Test-Path $TestBinary)) { + Write-Error "unit_tests.exe not found. Searched: $($searchPaths -join ', ')" + exit 1 +} + +$TestBinary = Resolve-Path $TestBinary +Write-Host "[*] Test binary: $TestBinary" -ForegroundColor Cyan + +# Check for OpenCppCoverage +$opencppcov = Get-Command "OpenCppCoverage" -ErrorAction SilentlyContinue +if (-not $opencppcov) { + # Try common installation paths + $possiblePaths = @( + "$env:ProgramFiles\OpenCppCoverage\OpenCppCoverage.exe", + "${env:ProgramFiles(x86)}\OpenCppCoverage\OpenCppCoverage.exe", + "$env:LOCALAPPDATA\Programs\OpenCppCoverage\OpenCppCoverage.exe" + ) + + foreach ($path in $possiblePaths) { + if (Test-Path $path) { + $opencppcov = Get-Item $path + break + } + } +} + +if (-not $opencppcov) { + Write-Host @" +OpenCppCoverage not found! + +Install it from: https://github.com/OpenCppCoverage/OpenCppCoverage/releases + +Or via Chocolatey: + choco install opencppcoverage + +Or via winget: + winget install OpenCppCoverage.OpenCppCoverage +"@ -ForegroundColor Red + exit 1 +} + +$OpenCppCoveragePath = if ($opencppcov.Source) { $opencppcov.Source } else { $opencppcov.FullName } +Write-Host "[*] Using OpenCppCoverage: $OpenCppCoveragePath" -ForegroundColor Cyan + +# Create output directory +$CoverageDir = Join-Path $BinaryDir "coverage" +if (-not (Test-Path $CoverageDir)) { + New-Item -ItemType Directory -Path $CoverageDir | Out-Null +} + +# Build OpenCppCoverage arguments +$coverageArgs = @( + "--sources", "$SourceDir\include", + "--sources", "$SourceDir\source", + "--excluded_sources", "*\tests\*", + "--excluded_sources", "*\googletest\*", + "--excluded_sources", "*\gtest\*", + "--excluded_sources", "*\_deps\*", + "--excluded_sources", "*\vcpkg_installed\*", + "--export_type", "html:$CoverageDir", + "--export_type", "cobertura:$CoverageDir\coverage.xml", + "--cover_children", + "--" +) + +Write-Host "[*] Running OpenCppCoverage..." -ForegroundColor Cyan +Write-Host " Command: $OpenCppCoveragePath $($coverageArgs -join ' ') $TestBinary" + +& $OpenCppCoveragePath @coverageArgs $TestBinary + +if ($LASTEXITCODE -ne 0) { + Write-Warning "OpenCppCoverage exited with code $LASTEXITCODE (tests may have failed)" +} + +# Check outputs +$htmlIndex = Join-Path $CoverageDir "index.html" +$coberturaXml = Join-Path $CoverageDir "coverage.xml" + +if (Test-Path $htmlIndex) { + Write-Host "[*] HTML coverage report: $htmlIndex" -ForegroundColor Green +} + +if (Test-Path $coberturaXml) { + Write-Host "[*] Cobertura XML report: $coberturaXml" -ForegroundColor Green +} + +Write-Host "[*] Coverage collection complete!" -ForegroundColor Green diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 59980dfe..6f4be846 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -21,6 +21,11 @@ else() # GTest is being linked as vcpkg package target_link_libraries(${PROJECT_NAME} PRIVATE GTest::gtest GTest::gtest_main omath::omath) endif() +if(OMATH_ENABLE_COVERAGE) + include(${CMAKE_SOURCE_DIR}/cmake/Coverage.cmake) + omath_setup_coverage(${PROJECT_NAME}) +endif() + # Skip test discovery for Android/iOS builds or when cross-compiling - binaries cannot run on host if (NOT (ANDROID OR IOS OR EMSCRIPTEN)) gtest_discover_tests(${PROJECT_NAME}) diff --git a/tests/engines/unit_test_traits_engines.cpp b/tests/engines/unit_test_traits_engines.cpp new file mode 100644 index 00000000..8a82a8be --- /dev/null +++ b/tests/engines/unit_test_traits_engines.cpp @@ -0,0 +1,297 @@ +// Tests for engine trait headers to improve header coverage +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include + +#include +#include +#include + +#include +#include +#include + +#include +#include +#include + +using namespace omath; + +// Small helper to compare matrices roughly (templated to avoid concrete typedef) +template +static void expect_matrix_near(const MatT& a, const MatT& b, float eps = 1e-5f) +{ + for (std::size_t r = 0; r < 4; ++r) + for (std::size_t c = 0; c < 4; ++c) + EXPECT_NEAR(a.at(r, c), b.at(r, c), eps); +} + +// Generic tests for PredEngineTrait behaviour across engines +TEST(TraitTests, Frostbite_Pred_And_Mesh_And_Camera) +{ + namespace E = omath::frostbite_engine; + + 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; + + const auto pos = E::PredEngineTrait::predict_projectile_position(p, 0.f, 0.f, 1.f, 9.81f); + EXPECT_NEAR(pos.x, 0.f, 1e-4f); + EXPECT_NEAR(pos.z, 10.f, 1e-4f); + EXPECT_NEAR(pos.y, -9.81f * 0.5f, 1e-4f); + + projectile_prediction::Target t; + t.m_origin = {0.f, 5.f, 0.f}; + t.m_velocity = {2.f, 0.f, 0.f}; + t.m_is_airborne = true; + const auto pred = E::PredEngineTrait::predict_target_position(t, 2.f, 9.81f); + EXPECT_NEAR(pred.x, 4.f, 1e-6f); + EXPECT_NEAR(pred.y, 5.f - 9.81f * (2.f * 2.f) * 0.5f, 1e-6f); + + // Also test non-airborne path (no gravity applied) + t.m_is_airborne = false; + const auto pred_ground = E::PredEngineTrait::predict_target_position(t, 2.f, 9.81f); + EXPECT_NEAR(pred_ground.x, 4.f, 1e-6f); + EXPECT_NEAR(pred_ground.y, 5.f, 1e-6f); + + EXPECT_NEAR(E::PredEngineTrait::calc_vector_2d_distance({3.f, 0.f, 4.f}), 5.f, 1e-6f); + EXPECT_NEAR(E::PredEngineTrait::get_vector_height_coordinate({1.f, 2.5f, 3.f}), 2.5f, 1e-6f); + + std::optional pitch = 45.f; + auto vp = E::PredEngineTrait::calc_viewpoint_from_angles(p, {10.f, 0.f, 0.f}, pitch); + EXPECT_NEAR(vp.y, 0.f + 10.f * std::tan(angles::degrees_to_radians(45.f)), 1e-6f); + + // Direct angles + Vector3 origin{0.f, 0.f, 0.f}; + Vector3 view_to{0.f, 1.f, 1.f}; + const auto pitch_calc = E::PredEngineTrait::calc_direct_pitch_angle(origin, view_to); + const auto dir = (view_to - origin).normalized(); + EXPECT_NEAR(pitch_calc, angles::radians_to_degrees(std::asin(dir.y)), 1e-3f); + + const auto yaw_calc = E::PredEngineTrait::calc_direct_yaw_angle(origin, view_to); + EXPECT_NEAR(yaw_calc, angles::radians_to_degrees(std::atan2(dir.x, dir.z)), 1e-3f); + + // MeshTrait simply forwards to rotation_matrix; ensure it compiles and returns something + E::ViewAngles va; + const auto m1 = E::MeshTrait::rotation_matrix(va); + const auto m2 = E::rotation_matrix(va); + expect_matrix_near(m1, m2); + + // CameraTrait look at should be callable + const auto angles = E::CameraTrait::calc_look_at_angle({0, 0, 0}, {0, 1, 1}); + (void)angles; + const auto proj = E::CameraTrait::calc_projection_matrix(projection::FieldOfView::from_degrees(60.f), {1280.f, 720.f}, 0.1f, 1000.f); + const auto expected = E::calc_perspective_projection_matrix(60.f, 1280.f / 720.f, 0.1f, 1000.f); + expect_matrix_near(proj, expected); +} + +TEST(TraitTests, IW_Pred_And_Mesh_And_Camera) +{ + namespace E = omath::iw_engine; + + 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; + + const auto pos = E::PredEngineTrait::predict_projectile_position(p, 0.f, 0.f, 1.f, 9.81f); + EXPECT_NEAR(pos.x, 10.f, 1e-4f); + EXPECT_NEAR(pos.z, -9.81f * 0.5f, 1e-4f); + + projectile_prediction::Target t; + t.m_origin = {0.f, 0.f, 5.f}; + t.m_velocity = {0.f, 0.f, 2.f}; + t.m_is_airborne = true; + const auto pred = E::PredEngineTrait::predict_target_position(t, 2.f, 9.81f); + // predicted = origin + velocity * t -> z = 5 + 2*2 = 9; then gravity applied + EXPECT_NEAR(pred.z, 9.f - 9.81f * (2.f * 2.f) * 0.5f, 1e-6f); + + EXPECT_NEAR(E::PredEngineTrait::calc_vector_2d_distance({3.f, 4.f, 0.f}), 5.f, 1e-6f); + EXPECT_NEAR(E::PredEngineTrait::get_vector_height_coordinate({1.f, 2.5f, 3.f}), 3.f, 1e-6f); + + std::optional pitch = 45.f; + auto vp = E::PredEngineTrait::calc_viewpoint_from_angles(p, {10.f, 0.f, 0.f}, pitch); + EXPECT_NEAR(vp.z, 0.f + 10.f * std::tan(angles::degrees_to_radians(45.f)), 1e-6f); + + Vector3 origin{0.f, 0.f, 0.f}; + Vector3 view_to{1.f, 1.f, 1.f}; + const auto pitch_calc = E::PredEngineTrait::calc_direct_pitch_angle(origin, view_to); + const auto dist = origin.distance_to(view_to); + EXPECT_NEAR(pitch_calc, angles::radians_to_degrees(std::asin((view_to.z - origin.z) / dist)), 1e-3f); + + const auto yaw_calc = E::PredEngineTrait::calc_direct_yaw_angle(origin, view_to); + const auto delta = view_to - origin; + EXPECT_NEAR(yaw_calc, angles::radians_to_degrees(std::atan2(delta.y, delta.x)), 1e-3f); + + E::ViewAngles va; + expect_matrix_near(E::MeshTrait::rotation_matrix(va), E::rotation_matrix(va)); + + const auto proj = E::CameraTrait::calc_projection_matrix(projection::FieldOfView::from_degrees(45.f), {1920.f, 1080.f}, 0.1f, 1000.f); + const auto expected = E::calc_perspective_projection_matrix(45.f, 1920.f / 1080.f, 0.1f, 1000.f); + expect_matrix_near(proj, expected); + + // non-airborne + t.m_is_airborne = false; + const auto pred_ground_iw = E::PredEngineTrait::predict_target_position(t, 2.f, 9.81f); + EXPECT_NEAR(pred_ground_iw.z, 9.f, 1e-6f); +} + +TEST(TraitTests, OpenGL_Pred_And_Mesh_And_Camera) +{ + namespace E = omath::opengl_engine; + + 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; + + const auto pos = E::PredEngineTrait::predict_projectile_position(p, 0.f, 0.f, 1.f, 9.81f); + EXPECT_NEAR(pos.z, -10.f, 1e-4f); + EXPECT_NEAR(pos.y, -9.81f * 0.5f, 1e-4f); + + projectile_prediction::Target t; + t.m_origin = {0.f, 5.f, 0.f}; + t.m_velocity = {2.f, 0.f, 0.f}; + t.m_is_airborne = true; + const auto pred = E::PredEngineTrait::predict_target_position(t, 2.f, 9.81f); + EXPECT_NEAR(pred.x, 4.f, 1e-6f); + EXPECT_NEAR(pred.y, 5.f - 9.81f * (2.f * 2.f) * 0.5f, 1e-6f); + + EXPECT_NEAR(E::PredEngineTrait::calc_vector_2d_distance({3.f, 0.f, 4.f}), 5.f, 1e-6f); + EXPECT_NEAR(E::PredEngineTrait::get_vector_height_coordinate({1.f, 2.5f, 3.f}), 2.5f, 1e-6f); + + std::optional pitch = 45.f; + auto vp = E::PredEngineTrait::calc_viewpoint_from_angles(p, {10.f, 0.f, 0.f}, pitch); + EXPECT_NEAR(vp.y, 0.f + 10.f * std::tan(angles::degrees_to_radians(45.f)), 1e-6f); + + Vector3 origin{0.f, 0.f, 0.f}; + Vector3 view_to{0.f, 1.f, 1.f}; + const auto pitch_calc = E::PredEngineTrait::calc_direct_pitch_angle(origin, view_to); + const auto dir = (view_to - origin).normalized(); + EXPECT_NEAR(pitch_calc, angles::radians_to_degrees(std::asin(dir.y)), 1e-3f); + + const auto yaw_calc = E::PredEngineTrait::calc_direct_yaw_angle(origin, view_to); + EXPECT_NEAR(yaw_calc, angles::radians_to_degrees(-std::atan2(dir.x, -dir.z)), 1e-3f); + + E::ViewAngles va; + expect_matrix_near(E::MeshTrait::rotation_matrix(va), E::rotation_matrix(va)); + + const auto proj = E::CameraTrait::calc_projection_matrix(projection::FieldOfView::from_degrees(60.f), {1280.f, 720.f}, 0.1f, 1000.f); + const auto expected = E::calc_perspective_projection_matrix(60.f, 1280.f / 720.f, 0.1f, 1000.f); + expect_matrix_near(proj, expected); + + // non-airborne + t.m_is_airborne = false; + const auto pred_ground_gl = E::PredEngineTrait::predict_target_position(t, 2.f, 9.81f); + EXPECT_NEAR(pred_ground_gl.x, 4.f, 1e-6f); +} + +TEST(TraitTests, Unity_Pred_And_Mesh_And_Camera) +{ + namespace E = omath::unity_engine; + + 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; + + const auto pos = E::PredEngineTrait::predict_projectile_position(p, 0.f, 0.f, 1.f, 9.81f); + EXPECT_NEAR(pos.z, 10.f, 1e-4f); + EXPECT_NEAR(pos.y, -9.81f * 0.5f, 1e-4f); + + projectile_prediction::Target t; + t.m_origin = {0.f, 5.f, 0.f}; + t.m_velocity = {2.f, 0.f, 0.f}; + t.m_is_airborne = true; + const auto pred = E::PredEngineTrait::predict_target_position(t, 2.f, 9.81f); + EXPECT_NEAR(pred.x, 4.f, 1e-6f); + EXPECT_NEAR(pred.y, 5.f - 9.81f * (2.f * 2.f) * 0.5f, 1e-6f); + + EXPECT_NEAR(E::PredEngineTrait::calc_vector_2d_distance({3.f, 0.f, 4.f}), 5.f, 1e-6f); + EXPECT_NEAR(E::PredEngineTrait::get_vector_height_coordinate({1.f, 2.5f, 3.f}), 2.5f, 1e-6f); + + std::optional pitch = 45.f; + auto vp = E::PredEngineTrait::calc_viewpoint_from_angles(p, {10.f, 0.f, 0.f}, pitch); + EXPECT_NEAR(vp.y, 0.f + 10.f * std::tan(angles::degrees_to_radians(45.f)), 1e-6f); + + Vector3 origin{0.f, 0.f, 0.f}; + Vector3 view_to{0.f, 1.f, 1.f}; + const auto pitch_calc = E::PredEngineTrait::calc_direct_pitch_angle(origin, view_to); + const auto dir = (view_to - origin).normalized(); + EXPECT_NEAR(pitch_calc, angles::radians_to_degrees(std::asin(dir.y)), 1e-3f); + + const auto yaw_calc = E::PredEngineTrait::calc_direct_yaw_angle(origin, view_to); + EXPECT_NEAR(yaw_calc, angles::radians_to_degrees(std::atan2(dir.x, dir.z)), 1e-3f); + + E::ViewAngles va; + expect_matrix_near(E::MeshTrait::rotation_matrix(va), E::rotation_matrix(va)); + + const auto proj = E::CameraTrait::calc_projection_matrix(projection::FieldOfView::from_degrees(60.f), {1280.f, 720.f}, 0.1f, 1000.f); + const auto expected = E::calc_perspective_projection_matrix(60.f, 1280.f / 720.f, 0.1f, 1000.f); + expect_matrix_near(proj, expected); + + // non-airborne + t.m_is_airborne = false; + const auto pred_ground_unity = E::PredEngineTrait::predict_target_position(t, 2.f, 9.81f); + EXPECT_NEAR(pred_ground_unity.x, 4.f, 1e-6f); +} + +TEST(TraitTests, Unreal_Pred_And_Mesh_And_Camera) +{ + namespace E = omath::unreal_engine; + + 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; + + const auto pos = E::PredEngineTrait::predict_projectile_position(p, 0.f, 0.f, 1.f, 9.81f); + EXPECT_NEAR(pos.x, 10.f, 1e-4f); + EXPECT_NEAR(pos.y, -9.81f * 0.5f, 1e-4f); + + projectile_prediction::Target t; + t.m_origin = {0.f, 5.f, 0.f}; + t.m_velocity = {2.f, 0.f, 0.f}; + t.m_is_airborne = true; + const auto pred = E::PredEngineTrait::predict_target_position(t, 2.f, 9.81f); + EXPECT_NEAR(pred.x, 4.f, 1e-6f); + EXPECT_NEAR(pred.y, 5.f - 9.81f * (2.f * 2.f) * 0.5f, 1e-6f); + + EXPECT_NEAR(E::PredEngineTrait::calc_vector_2d_distance({3.f, 0.f, 4.f}), 5.f, 1e-6f); + EXPECT_NEAR(E::PredEngineTrait::get_vector_height_coordinate({1.f, 2.5f, 3.f}), 2.5f, 1e-6f); + + std::optional pitch = 45.f; + auto vp = E::PredEngineTrait::calc_viewpoint_from_angles(p, {10.f, 0.f, 0.f}, pitch); + EXPECT_NEAR(vp.z, 0.f + 10.f * std::tan(angles::degrees_to_radians(45.f)), 1e-6f); + + Vector3 origin{0.f, 0.f, 0.f}; + Vector3 view_to{1.f, 1.f, 1.f}; + const auto pitch_calc = E::PredEngineTrait::calc_direct_pitch_angle(origin, view_to); + const auto dir = (view_to - origin).normalized(); + EXPECT_NEAR(pitch_calc, angles::radians_to_degrees(std::asin(dir.z)), 1e-3f); + + const auto yaw_calc = E::PredEngineTrait::calc_direct_yaw_angle(origin, view_to); + EXPECT_NEAR(yaw_calc, angles::radians_to_degrees(std::atan2(dir.y, dir.x)), 1e-3f); + + E::ViewAngles va; + expect_matrix_near(E::MeshTrait::rotation_matrix(va), E::rotation_matrix(va)); + + const auto proj = E::CameraTrait::calc_projection_matrix(projection::FieldOfView::from_degrees(60.f), {1280.f, 720.f}, 0.1f, 1000.f); + const auto expected = E::calc_perspective_projection_matrix(60.f, 1280.f / 720.f, 0.1f, 1000.f); + expect_matrix_near(proj, expected); + + // non-airborne + t.m_is_airborne = false; + const auto pred_ground_unreal = E::PredEngineTrait::predict_target_position(t, 2.f, 9.81f); + EXPECT_NEAR(pred_ground_unreal.x, 4.f, 1e-6f); +} diff --git a/tests/general/unit_test_collision_extra.cpp b/tests/general/unit_test_collision_extra.cpp index 4884c630..bfbaeb67 100644 --- a/tests/general/unit_test_collision_extra.cpp +++ b/tests/general/unit_test_collision_extra.cpp @@ -8,6 +8,29 @@ using namespace omath; using namespace omath::collision; +TEST(SimplexTest, HandleEmptySimplex) +{ + Simplex> simplex; + Vector3 direction{1, 0, 0}; + + EXPECT_EQ(simplex.size(), 0); + EXPECT_FALSE(simplex.handle(direction)); +} + +TEST(SimplexTest, HandleLineCollinearWithXAxis) +{ + using Vec3 = Vector3; + Simplex simplex; + + simplex.push_front(Vec3{1, 0, 0}); + simplex.push_front(Vec3{-1, 0, 0}); + + Vec3 direction{}; + simplex.handle(direction); + + EXPECT_NEAR(direction.x, 0.f, 1e-6f); +} + TEST(CollisionExtra, SimplexLineHandle) { Simplex> s; From a7dd1f916cf5e9f2a5122fa14d2996e642afa613 Mon Sep 17 00:00:00 2001 From: Orange Date: Thu, 25 Dec 2025 02:19:36 +0300 Subject: [PATCH 2/2] reverted --- include/omath/collision/simplex.hpp | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/include/omath/collision/simplex.hpp b/include/omath/collision/simplex.hpp index d1043962..7820f53b 100644 --- a/include/omath/collision/simplex.hpp +++ b/include/omath/collision/simplex.hpp @@ -139,12 +139,10 @@ namespace omath::collision [[nodiscard]] static constexpr V any_perp(const V& v) { - // If v is not parallel to x-axis, cross with x works - if (const auto d = v.cross(V{1, 0, 0}); !near_zero(d)) - return d; - - // v is parallel to x-axis (v = (a,0,0)), so v × (0,1,0) = (0,0,a) ≠ 0 - return v.cross(V{0, 1, 0}); + for (const auto& dir : {V{1, 0, 0}, {0, 1, 0}, {0, 0, 1}}) + if (const auto d = v.cross(dir); !near_zero(d)) + return d; + std::unreachable(); } [[nodiscard]]