Implement coverage for Windows/Linux/MacOS (#126) (#128)

* Implement coverage for Windows/Linux/MacOS (#126)

* reverted

---------

Co-authored-by: Saikari <lin@sz.cn.eu.org>
This commit is contained in:
2025-12-25 02:28:17 +03:00
committed by GitHub
parent d16984a8b2
commit 29da13d244
11 changed files with 874 additions and 34 deletions

View File

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

1
.gitignore vendored
View File

@@ -4,4 +4,5 @@
/extlibs/vcpkg
.idea/workspace.xml
/build/
/clang-coverage/
*.gcov

View File

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

122
cmake/Coverage.cmake Normal file
View File

@@ -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
-- "$<TARGET_FILE:unit_tests>"
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}"
"$<TARGET_FILE:unit_tests>"
"${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 $<TARGET_FILE:unit_tests> || 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()

View File

@@ -64,9 +64,8 @@ namespace omath
case 5:
r = value, g = p, b = q;
break;
default:
return {0.f, 0.f, 0.f, 0.f};
std::unreachable();
}
return {r, g, b, 1.f};

169
scripts/coverage-llvm.sh Executable file
View File

@@ -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}"

8
scripts/coverage.bat.in Normal file
View File

@@ -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%" %*

132
scripts/coverage.ps1.in Normal file
View File

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

View File

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

View File

@@ -0,0 +1,297 @@
// Tests for engine trait headers to improve header coverage
#include <gtest/gtest.h>
#include <omath/engines/frostbite_engine/traits/pred_engine_trait.hpp>
#include <omath/engines/frostbite_engine/traits/mesh_trait.hpp>
#include <omath/engines/frostbite_engine/traits/camera_trait.hpp>
#include <omath/engines/iw_engine/traits/pred_engine_trait.hpp>
#include <omath/engines/iw_engine/traits/mesh_trait.hpp>
#include <omath/engines/iw_engine/traits/camera_trait.hpp>
#include <omath/engines/opengl_engine/traits/pred_engine_trait.hpp>
#include <omath/engines/opengl_engine/traits/mesh_trait.hpp>
#include <omath/engines/opengl_engine/traits/camera_trait.hpp>
#include <omath/engines/unity_engine/traits/pred_engine_trait.hpp>
#include <omath/engines/unity_engine/traits/mesh_trait.hpp>
#include <omath/engines/unity_engine/traits/camera_trait.hpp>
#include <omath/engines/unreal_engine/traits/pred_engine_trait.hpp>
#include <omath/engines/unreal_engine/traits/mesh_trait.hpp>
#include <omath/engines/unreal_engine/traits/camera_trait.hpp>
#include <omath/projectile_prediction/projectile.hpp>
#include <omath/projectile_prediction/target.hpp>
#include <optional>
using namespace omath;
// Small helper to compare matrices roughly (templated to avoid concrete typedef)
template<typename MatT>
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<float> 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<float> origin{0.f, 0.f, 0.f};
Vector3<float> 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<float> 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<float> origin{0.f, 0.f, 0.f};
Vector3<float> 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<float> 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<float> origin{0.f, 0.f, 0.f};
Vector3<float> 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<float> 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<float> origin{0.f, 0.f, 0.f};
Vector3<float> 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<float> 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<float> origin{0.f, 0.f, 0.f};
Vector3<float> 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);
}

View File

@@ -8,6 +8,29 @@
using namespace omath;
using namespace omath::collision;
TEST(SimplexTest, HandleEmptySimplex)
{
Simplex<Vector3<float>> simplex;
Vector3<float> direction{1, 0, 0};
EXPECT_EQ(simplex.size(), 0);
EXPECT_FALSE(simplex.handle(direction));
}
TEST(SimplexTest, HandleLineCollinearWithXAxis)
{
using Vec3 = Vector3<float>;
Simplex<Vec3> 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<Vector3<float>> s;