Finding-I from doc/reviewer/external-audit-2026-05-30.md.
Root cause: CGAL+Eigen at -O3 drives cc1plus peak RAM to ~700 MB per
Unity compilation unit (batch size 4) on ARM64. With the 1600 MB
container limit the 2nd or 3rd TU reliably triggered OOM-kill, so
test-cgal was gated off via `if: false` since 2026-05-26.
Fix: new CMake option CONFORMALLAB_LOW_MEMORY_BUILD=ON applies four
orthogonal memory-saving measures to conformallab_cgal_tests:
1. -O0 (no debug info): optimizer passes entirely skipped → cc1plus
peak drops from ~700 MB to ~150-200 MB per TU on ARM64.
Omitting -g avoids the additional object-file / linker RAM cost.
2. CONFORMALLAB_USE_PCH=OFF: saves the one-time ~200 MB PCH
compilation cost; each TU re-parses CGAL headers (fast at -O0).
3. UNITY_BUILD_BATCH_SIZE=1: one source file per cc1plus invocation,
removing the "4-file template-explosion" per-unit multiplier.
4. -Wl,--no-keep-memory (GNU ld): linker releases symbol tables after
each input file → ~15-25 % less linker RSS.
Verified locally with cmake -DCONFORMALLAB_LOW_MEMORY_BUILD=ON:
277/277 CGAL tests pass, 31 s runtime (vs 2 s at -O3 — expected;
tests run 15× slower without optimizer but all correct).
CI workflow changes (cpp-tests.yml):
- test-cgal re-enabled: `if: github.event_name == 'pull_request'`
- Configure step adds -DCONFORMALLAB_LOW_MEMORY_BUILD=ON
- Container memory: 1600m → 2000m (--memory-swap=3000m for 1 GB swap
headroom), using ~half of the Pi's 3-4 GB while leaving OS margin.
CLAUDE.md updated: new flag added to compile-time options table; CI
status row corrected from DISABLED to active.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
191 lines
8.8 KiB
YAML
191 lines
8.8 KiB
YAML
name: C++ Tests
|
||
|
||
on:
|
||
push:
|
||
branches:
|
||
- main
|
||
- dev
|
||
- "claude/**"
|
||
- "feature/**"
|
||
pull_request:
|
||
|
||
# ─────────────────────────────────────────────────────────────────────────────
|
||
# Job 1 — test-fast
|
||
# Pure-math tests (Clausen, ImLi₂, Hyper-ideal geometry).
|
||
# No CGAL, no Boost. Eigen + GTest only. Runs on ALL branches.
|
||
# ─────────────────────────────────────────────────────────────────────────────
|
||
jobs:
|
||
test-fast:
|
||
runs-on: eulernest
|
||
container:
|
||
image: git.eulernest.eu/conformallab/ci-cpp:latest
|
||
|
||
steps:
|
||
- uses: actions/checkout@v4
|
||
|
||
- name: Configure (tests-only)
|
||
run: cmake -S code -B build -DCMAKE_BUILD_TYPE=Release
|
||
|
||
- name: Build
|
||
run: nice -n 19 cmake --build build --target conformallab_tests -j$(nproc)
|
||
|
||
- name: Run tests
|
||
run: >
|
||
ctest --test-dir build
|
||
--output-on-failure
|
||
--output-junit test-results.xml
|
||
|
||
- name: Summary
|
||
if: always()
|
||
run: |
|
||
if [ -f test-results.xml ]; then
|
||
total=$(grep -o 'tests="[0-9]*"' test-results.xml | grep -o '[0-9]*' | head -1)
|
||
failed=$(grep -o 'failures="[0-9]*"' test-results.xml | grep -o '[0-9]*' | head -1)
|
||
skipped=$(grep -o 'skipped="[0-9]*"' test-results.xml | grep -o '[0-9]*' | head -1)
|
||
passed=$(( ${total:-0} - ${failed:-0} - ${skipped:-0} ))
|
||
echo "FAST ▸ TOTAL ${total:-0} | PASSED $passed | FAILED ${failed:-0} | SKIPPED ${skipped:-0}"
|
||
fi
|
||
|
||
# ─────────────────────────────────────────────────────────────────────────────
|
||
# Job 2 — test-cgal
|
||
# Full CGAL test suite (Phase 3–7, 158 tests).
|
||
# Runs ONLY on pull requests (not on direct pushes to dev/main).
|
||
# Starts only after test-fast succeeds.
|
||
#
|
||
# Uses -DWITH_CGAL_TESTS=ON (not -DWITH_CGAL=ON) to avoid building
|
||
# Viewer/GLFW — the CI container has no wayland-scanner.
|
||
#
|
||
# Boost (libboost-dev) is already present in the container since the image rebuild.
|
||
# ─────────────────────────────────────────────────────────────────────────────
|
||
# ─── RE-ENABLED 2026-05-31 with CONFORMALLAB_LOW_MEMORY_BUILD ────────────
|
||
# Root cause of previous OOMs: CGAL+Eigen at -O3 drives cc1plus peak to
|
||
# ~700 MB per Unity compilation unit on ARM64. With a 1600 MB container
|
||
# limit and 4 files per unity batch, the 2nd or 3rd TU reliably OOMs.
|
||
#
|
||
# Fix: CONFORMALLAB_LOW_MEMORY_BUILD=ON applies three measures:
|
||
# 1. -O0 (no debug info): drops cc1plus backend RAM from ~700 MB to
|
||
# ~150-200 MB per TU (optimizer passes are entirely skipped).
|
||
# 2. CONFORMALLAB_USE_PCH=OFF: saves the ~200 MB PCH compilation cost.
|
||
# 3. UNITY_BUILD_BATCH_SIZE=1: one source file per cc1plus invocation,
|
||
# removing the "4-file template-explosion" multiplier.
|
||
# 4. --no-keep-memory linker flag: reduces GNU ld RSS by ~15-25 %.
|
||
#
|
||
# Memory budget (ARM64, GCC, estimated peak per cc1plus after fix):
|
||
# ~150-200 MB/TU × 1 TU at a time (-j1) → fits in 2000 MB container.
|
||
# Container memory raised 1600 → 2000 MB to give a comfortable margin;
|
||
# swap re-enabled (memory-swap=3000m) so OOM-kills fail fast only if
|
||
# the container truly runs out of physical+swap, not just RAM.
|
||
#
|
||
# Trade-off: tests run 2-4× slower (CGAL traversals unoptimized at -O0);
|
||
# wall-clock build time increases (no PCH, no unity batching) but the
|
||
# correctness guarantee is identical — all 277 CGAL tests pass.
|
||
#
|
||
# Trigger: pull requests only (same as before).
|
||
test-cgal:
|
||
needs: test-fast
|
||
if: github.event_name == 'pull_request'
|
||
runs-on: eulernest
|
||
container:
|
||
image: git.eulernest.eu/conformallab/ci-cpp:latest
|
||
# 2000 MB hard limit for the container; swap headroom up to 3000 MB
|
||
# (memory-swap = total of RAM + swap, so 3000-2000 = 1000 MB swap).
|
||
# This uses ~half of the Pi's 3-4 GB while leaving room for the OS
|
||
# and the runner daemon. With LOW_MEMORY_BUILD the peak per TU is
|
||
# ~150-200 MB, well within the 2000 MB ceiling.
|
||
options: "--memory=2000m --memory-swap=3000m"
|
||
|
||
steps:
|
||
- uses: actions/checkout@v4
|
||
|
||
- name: Configure (LOW_MEMORY_BUILD — -O0, no PCH, unity batch 1)
|
||
run: |
|
||
cmake -S code -B build \
|
||
-DWITH_CGAL_TESTS=ON \
|
||
-DCMAKE_BUILD_TYPE=Release \
|
||
-DCONFORMALLAB_LOW_MEMORY_BUILD=ON
|
||
|
||
- name: Build CGAL-Tests
|
||
run: nice -n 19 cmake --build build --target conformallab_cgal_tests -j1
|
||
|
||
- name: Run CGAL-Tests
|
||
run: >
|
||
ctest --test-dir build
|
||
-R "^cgal\."
|
||
--output-on-failure
|
||
--output-junit cgal-results.xml
|
||
|
||
- name: Summary
|
||
if: always()
|
||
run: |
|
||
if [ -f cgal-results.xml ]; then
|
||
total=$(grep -o 'tests="[0-9]*"' cgal-results.xml | grep -o '[0-9]*' | head -1)
|
||
failed=$(grep -o 'failures="[0-9]*"' cgal-results.xml | grep -o '[0-9]*' | head -1)
|
||
skipped=$(grep -o 'skipped="[0-9]*"' cgal-results.xml | grep -o '[0-9]*' | head -1)
|
||
passed=$(( ${total:-0} - ${failed:-0} - ${skipped:-0} ))
|
||
echo "CGAL ▸ TOTAL ${total:-0} | PASSED $passed | FAILED ${failed:-0} | SKIPPED ${skipped:-0}"
|
||
fi
|
||
|
||
# ── Structural gate: doc/api/tests.md totals match ctest reality ───
|
||
# Single source of truth for test counts (see doc/release-policy.md).
|
||
# Reuses the already-built ./build dir via BUILD_DIR env var, so this
|
||
# adds ~5 s on top of the existing CGAL job.
|
||
- name: Verify test-count consistency (doc/api/tests.md)
|
||
run: BUILD_DIR=build bash scripts/check-test-counts.sh
|
||
|
||
# ── Structural gate: end-to-end smoke (try_it.sh) ──────────────────
|
||
# The user-facing quick-start script: configure + build + run the
|
||
# full ctest + run the Euclidean example on a bundled mesh. If
|
||
# this regresses, README quick-start instructions are broken.
|
||
# try_it.sh creates its own build-try/ — accept the ~3 min cost as
|
||
# the price of guaranteeing the documented workflow stays working.
|
||
- name: End-to-end smoke test (scripts/try_it.sh)
|
||
run: bash scripts/try_it.sh
|
||
|
||
# ─────────────────────────────────────────────────────────────────────────────
|
||
# Job 3 — quality-gates (style + convention block)
|
||
#
|
||
# Cheap, deterministic checks that should never break unless a contributor
|
||
# introduces a regression. Each gate is a script under scripts/quality/
|
||
# and exits 0 only when its tree is clean. These ran for weeks locally
|
||
# at zero findings before being promoted here.
|
||
#
|
||
# Tools installed at job-start (the ci-cpp image already has python3 +
|
||
# bash; we add codespell + shellcheck on top). Total wall-time: ~30 s
|
||
# on the eulernest runner.
|
||
#
|
||
# Strictly required for merges into main/dev — a regression fails the PR.
|
||
# ─────────────────────────────────────────────────────────────────────────────
|
||
quality-gates:
|
||
needs: test-fast
|
||
runs-on: eulernest
|
||
container:
|
||
image: git.eulernest.eu/conformallab/ci-cpp:latest
|
||
|
||
steps:
|
||
- uses: actions/checkout@v4
|
||
|
||
- name: Install codespell + shellcheck (job-local)
|
||
run: |
|
||
apt-get update -qq
|
||
apt-get install -y --no-install-recommends \
|
||
codespell shellcheck
|
||
|
||
- name: License headers (every C++ source carries MIT SPDX)
|
||
run: bash scripts/quality/license-headers.sh
|
||
|
||
- name: CGAL conventions (6 rules over CGAL public headers)
|
||
run: python3 scripts/quality/cgal-conventions.py
|
||
|
||
- name: codespell (docs + source comments + script messages)
|
||
run: bash scripts/quality/codespell.sh
|
||
|
||
- name: shellcheck (scripts/**/*.sh, severity=warning, strict)
|
||
run: bash scripts/quality/shellcheck.sh --strict
|
||
|
||
- name: Summary
|
||
if: always()
|
||
run: |
|
||
echo "QUALITY ▸ all four gates passed."
|
||
echo " see scripts/quality/README.md for the full catalogue"
|
||
echo " (sanitizers, clang-tidy, coverage, etc. are local-only)"
|