Testing and continuous integration¶
Build configuration · Development guide
Run and extend the tests¶
CTest runs the GoogleTest numerical cases, CLI checks, and optional Python
citation-maintenance checks. Tests reuse a parent-provided GoogleTest target,
prefer an installed GoogleTest 1.12+ package, or fetch v1.17.0. GoogleTest is
only required when BETHE_BUILD_TESTS=ON; library consumers do not inherit it.
Numerical cases are instantiated separately for fp64, native long double,
and optional fp128. Names include the precision, so failures can be isolated.
From the build directory:
ctest -N
ctest -R 'Hubbard.*fp128' --output-on-failure
./tests/bethe_heisenberg_tests --gtest_filter='*/long_double.NativePrecisionSix'
The tests serve different purposes. Small independent exact-diagonalization oracles check sectors, quantum numbers, and multiplicities in fp64. Analytic energies, root equations, and Jacobians separately check the selected arithmetic without narrowing to double. A successful fp64 ED comparison alone does not establish fp128 accuracy.
New models should use the precision type list and comparison helpers in
tests/test_support.hpp.
Uni20's EXPECT_FLOATING_EQ/zero-ULP comparisons test exact scalar round trips,
including native fp80 where available. Solver and analytic checks instead use
native-precision EXPECT_REAL_NEAR (strict absolute error less than the supplied
tolerance), not GoogleTest's double-based EXPECT_NEAR. Keep fatal assertions
before indexing roots or dereferencing optional gaps. Use parameter traces
inside scans and keep expensive ED loops grouped into logical cases.
CTest gives each test one BLAS/OpenMP thread; it can still run independent
cases concurrently with ctest --parallel. The shared discovery helper applies
list-valued environments and labels after discovery, so they remain intact
across supported CMake releases. Parameterized cases retain their GoogleTest
identifiers (including value indices), rather than substituting printed values
that can disappear from CMake's discovered test list.
Expensive end-to-end numerical checks carry the long-running label. From a
build directory, choose routine checks, long checks alone, or the complete suite:
ctest --label-exclude '^long-running$' --output-on-failure
ctest --label-regex '^long-running$' --output-on-failure
ctest --output-on-failure
The long group includes expensive binary128 and MPFR continuum, thermal and
precision-convergence checks. The full Hubbard doped Fredholm reference retains
its 80/160/256-bit accuracy assertions there; an independent 80-bit reference,
mixed-precision Async checks and failure paths remain in routine validation.
Long checks normally have a 30-minute timeout; individual cases can declare a
larger budget. Lieb–Liniger thermodynamic CLI checks run separately for each
precision and for physical corrections, retaining every original assertion.
New costly cases should be declared with LONG_RUNNING_TESTS alongside their
target's bethe_discover_tests call, using GoogleTest names.
Keep test computation focused¶
Use independent physics and precision-growth tests to establish numerical
accuracy. For grid construction, scheduling, ownership and export checks,
choose an interacting fixture that converges economically at the same working
precision and tolerance. Keep every branch, scheduler mode, failure path and
format assertion. For example, the Lieb–Liniger CLI grid tests use c=40;
the separate c=4 accuracy and density-scaling checks keep their original
inputs and tolerances.
When testing multiple output sinks, attach them to one calculation and compare with a separate reference run. This checks simultaneous exports and repeated-run consistency without solving the same problem once per file format.
Profile the CTest timing artifacts before changing a fixture, then benchmark under the same sanitizer settings. Shared-host timings indicate computation cost; they do not predict the whole GitHub job's elapsed time. The November 2026 measurement records an initial comparison, including trials that did not give a useful improvement.
Continuous integration¶
Pull requests and pushes to main run three Linux configurations, defined in
CMakePresets.json:
| Preset | Coverage |
|---|---|
ci-native |
GCC 13 Release; fp64 and native long double; OpenBLAS; optional scalars disabled |
ci-sanitizers |
Clang 19; AddressSanitizer and UndefinedBehaviorSanitizer; native scalars and MPFR; CPU kernels without BLAS/MPLAPACK |
ci-wide |
GCC 13 Release; binary80, binary128 and MPFR; OpenBLAS and source-built, optimized MPLAPACK |
Each builds all frontends and test targets and runs the routine CTest suite,
including CLI/export checks and the installed-program relocation test.
Runtime-precision Lieb–Liniger, nested Newton and complex form-factor tests
run in both ci-wide and the MPFR CPU sanitizer configuration, exercising
MPLAPACK and CPU LU through normal dispatch. These jobs do not claim macOS, Windows or GPU
coverage. Documentation has its own workflow.
The sanitizer preset sets UNI20_SANITIZE as well as the application's compiler
flags, activating Uni20's oneTBB instrumentation boundary. The compiled oneTBB
implementation is excluded; Bethe and Uni20 remain instrumented. Leak detection
stays enabled. The preset enables BETHE_TEST_UNI20_LSAN_SUPPRESSIONS, which
selects tests/sanitizers/lsan.supp from the actual Uni20 dependency checkout
for every CTest case, including CLI subprocesses. This also works with local
source overrides and custom build directories. The upstream file covers
intentionally retained NodeInfo metadata and known MPFR internal cache frames;
ordinary owning scalar leaks remain detectable. Bethe keeps no separate copy
of these rules. See the dependency sanitizer notes.
Suppression statistics are quiet so successful commands retain their normal
stderr behavior; actual sanitizer errors still print and fail the process.
The preset clears DEBUGINFOD_URLS so symbolization uses locally available
debug information without waiting for remote downloads during CLI checks.
Remainder operations also use an MPFR integer pool whose private frame needs
matching debug symbols. The sanitizer CI job prepares these with Uni20's
scripts/prepare-sanitizer-symbols.py before running tests, verifies the loaded
library's build ID and records sanitizer-symbols/symbols.json in its artifacts.
For local sanitizer runs, follow Uni20's
symbol preparation instructions
and set ASAN_SYMBOLIZER_PATH to the resulting wrapper. Downloads happen only
during preparation; neither the solvers nor sanitizer reports fetch symbols.
Weekly CI (Monday 02:00 Asia/Taipei) additionally runs ci-long-running on the
wide-precision build. A manual C++ CI run includes it by default, controlled
by the Include long-running numerical validation input. Run this group when
changing the relevant solvers, quadratures or precision machinery; ordinary
pushes and pull requests do not run it. Results are uploaded separately from
the routine suite. From a configured and built ci-wide tree:
ctest --preset ci-long-running
The workflow fetches Bethe's pinned Uni20, limits compilation and concurrent tests to two jobs, and gives numerical kernels one BLAS/OpenMP thread. Compiler outputs are cached; CMake/build directories are configured afresh. JUnit results, capability reports and configure/build/test logs are uploaded even on failure. MPLAPACK is built in Release mode without fast-math; the first uncached wide-precision build is expected to take longer.
With the named compiler, CMake 3.28+, Ninja, Python and the relevant numerical development packages installed, reproduce a configuration from the source root:
cmake --preset ci-native
cmake --build --preset ci-native
ctest --preset ci-native
Presets default to build/<preset> and do not need ccache locally. For a
shared source checkout, keep the build on local storage instead:
cmake --preset ci-native -B /path/to/local/bethe-ci-native
cmake --build /path/to/local/bethe-ci-native --parallel 2
OPENBLAS_NUM_THREADS=1 OMP_NUM_THREADS=1 \
ctest --test-dir /path/to/local/bethe-ci-native --parallel 2 \
--label-exclude '^long-running$' --output-on-failure