#!/usr/bin/env bash
# scripts/quality/sanitizers.sh
#
# Build the fast test suite with AddressSanitizer + UndefinedBehaviorSanitizer
# and run it.  Catches:
#   * use-after-free, double-free, heap-buffer-overflow (ASan)
#   * signed integer overflow, NaN propagation, alignment violations (UBSan)
#   * Eigen / CGAL template-induced UB that escapes the regular build
#
# Local-only (not in CI): the sanitizer build is ~3× slower and brittle
# against system libraries.  Run it before every release tag, after
# touching any Newton/Hessian code, or when investigating intermittent
# test failures.
#
# Usage:
#   bash scripts/quality/sanitizers.sh           # default: ASan + UBSan
#   ASAN_OPTIONS=...  UBSAN_OPTIONS=...  bash scripts/quality/sanitizers.sh
#
# Exit codes:
#   0  every test passes under sanitizer instrumentation
#   1  a sanitizer report was triggered (test failure or runtime error)
#   2  prerequisite missing (no clang/gcc with sanitizer support)

set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
cd "$ROOT"

BUILD_DIR="build-sanitizers"
SAN_FLAGS="-fsanitize=address,undefined -fno-omit-frame-pointer -O1 -g"

# Default ASan/UBSan runtime options — print stack on first error,
# abort on first issue (so CI logs make the cause obvious).
export ASAN_OPTIONS="${ASAN_OPTIONS:-detect_leaks=1:abort_on_error=1:print_stacktrace=1}"
export UBSAN_OPTIONS="${UBSAN_OPTIONS:-print_stacktrace=1:halt_on_error=1}"

echo "========================================"
echo "  Sanitizer build (ASan + UBSan)"
echo "  flags : $SAN_FLAGS"
echo "  ASAN  : $ASAN_OPTIONS"
echo "  UBSAN : $UBSAN_OPTIONS"
echo "========================================"

# ── Pick a compiler with sanitizer support ──────────────────────────────────
# Prefer clang (better diagnostics); fall back to gcc.
CXX_BIN=""
for cand in clang++-17 clang++-16 clang++-15 clang++ g++; do
    if command -v "$cand" >/dev/null 2>&1; then
        CXX_BIN="$cand"
        break
    fi
done
if [ -z "$CXX_BIN" ]; then
    echo "FAIL: no clang++ / g++ found in PATH" >&2
    exit 2
fi
echo "Using CXX = $CXX_BIN ($("$CXX_BIN" --version | head -1))"
echo

# ── Configure ────────────────────────────────────────────────────────────────
# CMAKE_GTEST_DISCOVER_TESTS_DISCOVERY_MODE=PRE_TEST: without this,
# gtest_discover_tests runs the (sanitizer-instrumented) test binary at
# *build* time to enumerate test cases.  ASan aborts that subprocess
# the moment it sees any allocation in static-init, which fails the
# build before we can even get to ctest.  PRE_TEST defers discovery to
# `ctest` invocation, which is exactly what we want.
cmake -S code -B "$BUILD_DIR" \
    -DCMAKE_CXX_COMPILER="$CXX_BIN" \
    -DCMAKE_CXX_FLAGS="$SAN_FLAGS" \
    -DCMAKE_EXE_LINKER_FLAGS="$SAN_FLAGS" \
    -DCMAKE_BUILD_TYPE=Debug \
    -DCMAKE_GTEST_DISCOVER_TESTS_DISCOVERY_MODE=PRE_TEST \
    -Wno-dev

# ── Build the fast (non-CGAL) tests only ────────────────────────────────────
# CGAL tests would 4–5× the build time under sanitizers and have a
# higher false-positive surface (CGAL's expression-template trickery).
# Use the fast suite as the sanitizer canary; full coverage of the CGAL
# layer is covered by coverage.sh + the regular Release build.
nice -n 19 cmake --build "$BUILD_DIR" --target conformallab_tests \
    -j"$(nproc 2>/dev/null || sysctl -n hw.logicalcpu 2>/dev/null || echo 2)"

# ── Run ──────────────────────────────────────────────────────────────────────
cd "$BUILD_DIR"
if ctest -E "^cgal\." --output-on-failure --output-junit san-results.xml; then
    cd "$ROOT"
    echo
    echo "OK: all sanitizer-instrumented tests passed."
    exit 0
else
    cd "$ROOT"
    echo
    echo "FAIL: sanitizer-instrumented tests reported issues."
    echo "      See: $BUILD_DIR/Testing/Temporary/LastTest.log"
    exit 1
fi
