From a9ed42ab1df0267a0d66b190e1574caf61834d9b Mon Sep 17 00:00:00 2001 From: Caitlin Ross Date: Tue, 2 Jun 2026 21:23:55 -0500 Subject: [PATCH 1/6] gate test discovery on ROSS_BUILD_TESTING Replaces the unconditional ENABLE_TESTING() / include(CTest) block with a project-scoped knob that defaults ON at top level (honoring an explicit BUILD_TESTING=OFF) and OFF when ROSS is consumed via add_subdirectory()/FetchContent. ROSS_TEST_SCHEDULERS and ROSS_TEST_INSTRUMENTATION early-return when the knob is OFF, so a parent superbuild's ambient enable_testing() scope can't pull phold's add_test() calls in by accident. --- CLAUDE.md | 4 +++- CMakeLists.txt | 26 +++++++++++++++++++++++--- models/CMakeLists.txt | 10 ++++++++++ 3 files changed, 36 insertions(+), 4 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 921a87c9..544b982f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -12,7 +12,7 @@ cmake --build build -j cmake --install build ``` -Models are **not** built by default — pass `-DROSS_BUILD_MODELS=ON` to include `phold` and any symlinked-in models. `BUILD_TESTING=ON` enables the ctest suite. +Models are **not** built by default — pass `-DROSS_BUILD_MODELS=ON` to include `phold` and any symlinked-in models. Test discovery is gated by `ROSS_BUILD_TESTING` (defaults ON at top level, inherits from `BUILD_TESTING` if the parent set it; defaults OFF under `add_subdirectory()`/`FetchContent` — parent must opt in). MPI is required and auto-discovered via `find_package(MPI)` — do not set `CC=mpicc` or `-DCMAKE_C_COMPILER=mpicc`. For non-standard installs, hint with `-DMPI_HOME=...` or `module load ` before configuring. The top-level `CMakeLists.txt` does per-arch detection via `CMAKE_SYSTEM_PROCESSOR` and falls back to a `gtod` clock if unrecognized. Set `-DROSS_CLOCK_OVERRIDE=YES` to force the gtod clock. @@ -42,6 +42,8 @@ ctest --test-dir build -V -R # with output Tests live in `models/phold/CMakeLists.txt` and are generated by the `ROSS_TEST_SCHEDULERS` and `ROSS_TEST_INSTRUMENTATION` functions in `models/CMakeLists.txt`. Each new model variant (e.g. `phold_comm_test`, `phold_gvt_hook_test`) gets the full scheduler matrix applied via these functions. When adding a model or test variant, call these helpers rather than hand-writing `ADD_TEST`. +`ROSS_BUILD_TESTING` is the gate for ctest discovery: it controls whether `include(CTest)` runs at all, and the helpers above early-return when it's OFF so models build without registering their tests. Phold binaries still build under `-DROSS_BUILD_TESTING=OFF -DROSS_BUILD_MODELS=ON`; only test registration is suppressed. + ## Architecture ### Simulation hierarchy (top to bottom) diff --git a/CMakeLists.txt b/CMakeLists.txt index 5ea39ba2..64937704 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -54,9 +54,6 @@ include(GNUInstallDirs) # ROSS Configuration Options -ENABLE_TESTING() -INCLUDE(CTest) - # RPATH policy is set only when ROSS is the top-level project. When ROSS is # consumed via add_subdirectory()/FetchContent, the parent project owns # install-RPATH and the global CMAKE_INSTALL_RPATH; mutating those from a @@ -221,6 +218,29 @@ endif() # ROSS Core code ADD_SUBDIRECTORY(core) +# Test suite gating. Project-scoped knob so: +# - top-level builds preserve today's UX (default ON; `-DBUILD_TESTING=OFF` +# still suppresses tests by feeding the default) +# - subdirectory consumption defaults OFF (parent must opt in explicitly) +# Once set, ROSS_BUILD_TESTING is the sole gate on include(CTest) and on +# the add_test() registrations inside ROSS_TEST_SCHEDULERS / +# ROSS_TEST_INSTRUMENTATION (see models/CMakeLists.txt). +if(PROJECT_IS_TOP_LEVEL) + if(DEFINED BUILD_TESTING) + set(_ross_build_testing_default ${BUILD_TESTING}) + else() + set(_ross_build_testing_default ON) + endif() + option(ROSS_BUILD_TESTING "Build ROSS test suite" ${_ross_build_testing_default}) + unset(_ross_build_testing_default) +else() + option(ROSS_BUILD_TESTING "Build ROSS test suite" OFF) +endif() + +if(ROSS_BUILD_TESTING) + include(CTest) +endif() + # ROSS Models directory OPTION(ROSS_BUILD_MODELS "Build ROSS models?" OFF) IF(ROSS_BUILD_MODELS) diff --git a/models/CMakeLists.txt b/models/CMakeLists.txt index 07505af4..f1b17a09 100644 --- a/models/CMakeLists.txt +++ b/models/CMakeLists.txt @@ -1,5 +1,11 @@ ## BASIC SCHEDULERS FUNCTION(ROSS_TEST_SCHEDULERS target_name) + # if a parent superbuild has called + # enable_testing() at its own scope, add_test() here would otherwise + # register against that scope even with ROSS_BUILD_TESTING=OFF. + if(NOT ROSS_BUILD_TESTING) + return() + endif() ADD_TEST(${target_name}_SCHED_Sequential ${target_name} --synch=1) ADD_TEST(${target_name}_SCHED_Conservative mpirun -np 2 ./${target_name} --synch=2) ADD_TEST(${target_name}_SCHED_Optimistic mpirun -np 2 ./${target_name} --synch=3 --extramem=100000) @@ -18,6 +24,10 @@ ENDFUNCTION(ROSS_TEST_SCHEDULERS) ## INSTRUMENTATION TESTS FUNCTION(ROSS_TEST_INSTRUMENTATION target_name) + # See ROSS_TEST_SCHEDULERS above for why this guard exists. + if(NOT ROSS_BUILD_TESTING) + return() + endif() ADD_TEST(${target_name}_INST_OptPE mpirun -np 2 ./${target_name} --synch=3 --engine-stats=4 --event-trace=2 --extramem=100000 --vt-interval=10000 --vt-samp-end=100000) ADD_TEST(${target_name}_INST_OptLP mpirun -np 2 ./${target_name} --synch=3 --engine-stats=4 --event-trace=2 --kp-data=1 --lp-data=1 --extramem=100000 --vt-interval=10000 --vt-samp-end=100000) From 14b3fd18c85bfd70234739de617ca066e17b0624 Mon Sep 17 00:00:00 2001 From: Caitlin Ross Date: Tue, 2 Jun 2026 21:45:48 -0500 Subject: [PATCH 2/6] add lifecycle smoke driver and tests/ scaffolding Stand up the tests/ directory and bring in the first non-phold test. The driver itself is the smallest useful ROSS program: tw_init, register one no-op LP via the shared tests/helpers/test_lp_minimal table, tw_run with zero events, tw_end. Exit code is the pass signal; no assertion helper yet, deferred until a driver needs one. Registered under --synch=1/4/6 so the RollbackCheck path runs on every driver by construction. Multi-rank variants land later. --- CMakeLists.txt | 1 + tests/CMakeLists.txt | 36 +++++++++++++++++ tests/drivers/lifecycle_test.c | 23 +++++++++++ tests/helpers/test_lp_minimal.c | 70 +++++++++++++++++++++++++++++++++ tests/helpers/test_lp_minimal.h | 20 ++++++++++ 5 files changed, 150 insertions(+) create mode 100644 tests/CMakeLists.txt create mode 100644 tests/drivers/lifecycle_test.c create mode 100644 tests/helpers/test_lp_minimal.c create mode 100644 tests/helpers/test_lp_minimal.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 64937704..75891ae7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -239,6 +239,7 @@ endif() if(ROSS_BUILD_TESTING) include(CTest) + add_subdirectory(tests) endif() # ROSS Models directory diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 00000000..0e7edaaa --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1,36 @@ +# Subsystem driver tests. Each driver is a complete ROSS program (calls +# tw_init / tw_run / tw_end itself) that exercises one piece of the +# engine in isolation. See improve-testing.md for design rationale. +# +# This file is reached via add_subdirectory(tests) at top-level, itself +# gated on ROSS_BUILD_TESTING -- so by the time we get here, the gate +# is on. Same pattern as ROSS_TEST_SCHEDULERS. + +function(ROSS_DRIVER_TEST name) + if(NOT ROSS_BUILD_TESTING) + return() + endif() + + add_executable(${name} + drivers/${name}.c + helpers/test_lp_minimal.c) + target_include_directories(${name} PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/helpers) + target_link_libraries(${name} PRIVATE ROSS::ROSS) + + # Single-rank schedulers only for now. + add_test(NAME ${name}_SCHED_Sequential + COMMAND $ --synch=1) + add_test(NAME ${name}_SCHED_OptDebug + COMMAND $ --synch=4 --nkp=1) + add_test(NAME ${name}_SCHED_RollbackCheck + COMMAND $ --synch=6 --nkp=1) + + set_tests_properties( + ${name}_SCHED_Sequential + ${name}_SCHED_OptDebug + ${name}_SCHED_RollbackCheck + PROPERTIES TIMEOUT 30) +endfunction() + +ROSS_DRIVER_TEST(lifecycle_test) diff --git a/tests/drivers/lifecycle_test.c b/tests/drivers/lifecycle_test.c new file mode 100644 index 00000000..24f24bdb --- /dev/null +++ b/tests/drivers/lifecycle_test.c @@ -0,0 +1,23 @@ +/* + * Lifecycle smoke driver: tw_init -> register one minimal LP -> tw_run + * with zero scheduled events -> tw_end. The only assertion is the exit + * code -- if any of init / run / end aborts, ctest fails. Caught here + * means caught the moment it appears, not when phold's teardown + * happens to expose it. + */ + +#include "test_lp_minimal.h" + +int +main(int argc, char **argv) +{ + tw_init(&argc, &argv); + + tw_define_lps(1, sizeof(test_lp_minimal_message)); + tw_lp_settype(0, &test_lp_minimal_type[0]); + + tw_run(); + tw_end(); + + return 0; +} diff --git a/tests/helpers/test_lp_minimal.c b/tests/helpers/test_lp_minimal.c new file mode 100644 index 00000000..56bcf73e --- /dev/null +++ b/tests/helpers/test_lp_minimal.c @@ -0,0 +1,70 @@ +#include "test_lp_minimal.h" + +static void +test_lp_minimal_init(test_lp_minimal_state *s, tw_lp *lp) +{ + (void) s; + (void) lp; +} + +static void +test_lp_minimal_pre_run(test_lp_minimal_state *s, tw_lp *lp) +{ + (void) s; + (void) lp; +} + +static void +test_lp_minimal_event_handler(test_lp_minimal_state *s, tw_bf *bf, + test_lp_minimal_message *m, tw_lp *lp) +{ + (void) s; + (void) bf; + (void) m; + (void) lp; +} + +static void +test_lp_minimal_event_handler_rc(test_lp_minimal_state *s, tw_bf *bf, + test_lp_minimal_message *m, tw_lp *lp) +{ + (void) s; + (void) bf; + (void) m; + (void) lp; +} + +static void +test_lp_minimal_commit(test_lp_minimal_state *s, tw_bf *bf, + test_lp_minimal_message *m, tw_lp *lp) +{ + (void) s; + (void) bf; + (void) m; + (void) lp; +} + +static void +test_lp_minimal_final(test_lp_minimal_state *s, tw_lp *lp) +{ + (void) s; + (void) lp; +} + +static tw_peid +test_lp_minimal_map(tw_lpid gid) +{ + return (tw_peid) gid / g_tw_nlp; +} + +tw_lptype test_lp_minimal_type[] = { + {(init_f) test_lp_minimal_init, + (pre_run_f) test_lp_minimal_pre_run, + (event_f) test_lp_minimal_event_handler, + (revent_f) test_lp_minimal_event_handler_rc, + (commit_f) test_lp_minimal_commit, + (final_f) test_lp_minimal_final, + (map_f) test_lp_minimal_map, + sizeof(test_lp_minimal_state)}, + {0}, +}; diff --git a/tests/helpers/test_lp_minimal.h b/tests/helpers/test_lp_minimal.h new file mode 100644 index 00000000..ecedea17 --- /dev/null +++ b/tests/helpers/test_lp_minimal.h @@ -0,0 +1,20 @@ +#ifndef INC_test_lp_minimal_h +#define INC_test_lp_minimal_h + +#include + +/* Minimal no-op LP for subsystem driver tests. Drivers that need any + * actual behavior register their own tw_lptype; this one is just the + * smallest registration that lets the engine boot. */ + +typedef struct test_lp_minimal_state { + int unused; +} test_lp_minimal_state; + +typedef struct test_lp_minimal_message { + int unused; +} test_lp_minimal_message; + +extern tw_lptype test_lp_minimal_type[]; + +#endif From 2b305c114765d1b517c7cba48911f3dc755774a8 Mon Sep 17 00:00:00 2001 From: Caitlin Ross Date: Tue, 2 Jun 2026 21:57:17 -0500 Subject: [PATCH 3/6] add a rand reverse test --- tests/CMakeLists.txt | 1 + tests/drivers/rand_reverse_test.c | 82 +++++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+) create mode 100644 tests/drivers/rand_reverse_test.c diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 0e7edaaa..cf2386f8 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -34,3 +34,4 @@ function(ROSS_DRIVER_TEST name) endfunction() ROSS_DRIVER_TEST(lifecycle_test) +ROSS_DRIVER_TEST(rand_reverse_test) diff --git a/tests/drivers/rand_reverse_test.c b/tests/drivers/rand_reverse_test.c new file mode 100644 index 00000000..0a619ba7 --- /dev/null +++ b/tests/drivers/rand_reverse_test.c @@ -0,0 +1,82 @@ +/* + * rand-clcg4 reverse-symmetry driver. + * + * Forward draws on a tw_rng_stream are reversed via tw_rand_reverse_unif + * (the only public reverse; tw_rand_integer/exponential/ulong each cost + * one unif draw, so they reverse with one reverse_unif). After N forward + * + N reverse calls the stream's current seed (Cg) and draw counter must + * be byte-identical to the pre-draw snapshot. Asserting this directly + * catches rand-clcg4 arithmetic regressions that phold's specific draw + * pattern might not happen to tickle. + * + * Run before tw_run() so the engine's own RNG use doesn't enter the + * picture -- this isolates the test to rand-clcg4 itself. + */ + +#undef NDEBUG /* assertions must run regardless of build type */ +#include +#include + +#include "test_lp_minimal.h" + +typedef struct rng_snapshot { + int32_t Cg[4]; + unsigned long count; +} rng_snapshot; + +static void +snapshot_rng(rng_snapshot *out, const tw_rng_stream *g) +{ + memcpy(out->Cg, g->Cg, sizeof(out->Cg)); + out->count = g->count; +} + +static int +rng_matches(const rng_snapshot *before, const tw_rng_stream *now) +{ + return memcmp(before->Cg, now->Cg, sizeof(before->Cg)) == 0 + && before->count == now->count; +} + +int +main(int argc, char **argv) +{ + tw_init(&argc, &argv); + + tw_define_lps(1, sizeof(test_lp_minimal_message)); + tw_lp_settype(0, &test_lp_minimal_type[0]); + + tw_rng_stream *rng = &g_tw_lp[0]->rng[0]; + rng_snapshot before; + + /* Bulk unif <-> reverse_unif symmetry. The bedrock invariant. */ + snapshot_rng(&before, rng); + for (int i = 0; i < 1000; i++) + tw_rand_unif(rng); + for (int i = 0; i < 1000; i++) + tw_rand_reverse_unif(rng); + assert(rng_matches(&before, rng) && "tw_rand_unif/reverse_unif asymmetry"); + + /* Each of these is documented to cost exactly one rng_gen_val call; + * the assertion catches a regression where one of them silently + * starts drawing more (or fewer) than once. */ + snapshot_rng(&before, rng); + (void) tw_rand_integer(rng, 0, 1000); + tw_rand_reverse_unif(rng); + assert(rng_matches(&before, rng) && "tw_rand_integer is not one unif draw"); + + snapshot_rng(&before, rng); + (void) tw_rand_exponential(rng, 1.0); + tw_rand_reverse_unif(rng); + assert(rng_matches(&before, rng) && "tw_rand_exponential is not one unif draw"); + + snapshot_rng(&before, rng); + (void) tw_rand_ulong(rng, 0, 1000); + tw_rand_reverse_unif(rng); + assert(rng_matches(&before, rng) && "tw_rand_ulong is not one unif draw"); + + tw_run(); + tw_end(); + + return 0; +} From 31721bb47b06914742396878e0fb225a800566d7 Mon Sep 17 00:00:00 2001 From: Caitlin Ross Date: Wed, 3 Jun 2026 10:56:10 -0500 Subject: [PATCH 4/6] add a splay ordering test --- tests/CMakeLists.txt | 44 +++++++++---- tests/drivers/splay_order_test.c | 105 +++++++++++++++++++++++++++++++ 2 files changed, 136 insertions(+), 13 deletions(-) create mode 100644 tests/drivers/splay_order_test.c diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index cf2386f8..3dfa360f 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -6,11 +6,22 @@ # gated on ROSS_BUILD_TESTING -- so by the time we get here, the gate # is on. Same pattern as ROSS_TEST_SCHEDULERS. +# ROSS_DRIVER_TEST( [SCHEDULERS ...]) +# +# Default scheduler set is all three single-rank schedulers. Override +# with SCHEDULERS when a driver's event-handler side effects don't +# survive replay under --synch=4/6 (e.g. drivers with assertions in +# the handler whose state they mutate). function(ROSS_DRIVER_TEST name) if(NOT ROSS_BUILD_TESTING) return() endif() + cmake_parse_arguments(ARG "" "" "SCHEDULERS" ${ARGN}) + if(NOT ARG_SCHEDULERS) + set(ARG_SCHEDULERS Sequential OptDebug RollbackCheck) + endif() + add_executable(${name} drivers/${name}.c helpers/test_lp_minimal.c) @@ -18,20 +29,27 @@ function(ROSS_DRIVER_TEST name) ${CMAKE_CURRENT_SOURCE_DIR}/helpers) target_link_libraries(${name} PRIVATE ROSS::ROSS) - # Single-rank schedulers only for now. - add_test(NAME ${name}_SCHED_Sequential - COMMAND $ --synch=1) - add_test(NAME ${name}_SCHED_OptDebug - COMMAND $ --synch=4 --nkp=1) - add_test(NAME ${name}_SCHED_RollbackCheck - COMMAND $ --synch=6 --nkp=1) - - set_tests_properties( - ${name}_SCHED_Sequential - ${name}_SCHED_OptDebug - ${name}_SCHED_RollbackCheck - PROPERTIES TIMEOUT 30) + foreach(sched IN LISTS ARG_SCHEDULERS) + if(sched STREQUAL "Sequential") + add_test(NAME ${name}_SCHED_Sequential + COMMAND $ --synch=1) + elseif(sched STREQUAL "OptDebug") + add_test(NAME ${name}_SCHED_OptDebug + COMMAND $ --synch=4 --nkp=1) + elseif(sched STREQUAL "RollbackCheck") + add_test(NAME ${name}_SCHED_RollbackCheck + COMMAND $ --synch=6 --nkp=1) + else() + message(FATAL_ERROR + "ROSS_DRIVER_TEST(${name}): unknown scheduler '${sched}'") + endif() + set_tests_properties(${name}_SCHED_${sched} PROPERTIES TIMEOUT 30) + endforeach() endfunction() ROSS_DRIVER_TEST(lifecycle_test) ROSS_DRIVER_TEST(rand_reverse_test) + +# Splay's event handler asserts and mutates LP state (last_ts, +# delivery_count) without a paired reverse handler. Sequential only. +ROSS_DRIVER_TEST(splay_order_test SCHEDULERS Sequential) diff --git a/tests/drivers/splay_order_test.c b/tests/drivers/splay_order_test.c new file mode 100644 index 00000000..ddcea9ef --- /dev/null +++ b/tests/drivers/splay_order_test.c @@ -0,0 +1,105 @@ +/* + * Splay queue ordering driver. + * + * Schedules a known set of self-events on a single LP with a mix of + * distinct and tied timestamps in non-sorted enqueue order, then + * verifies the splay queue delivers them with monotonically + * non-decreasing recv_ts. The exact order of tied events depends on + * USE_RAND_TIEBREAKER and the LP's RNG state, so we don't pin that -- + * only the bedrock priority-queue invariant (no smaller-ts event + * delivered after a larger-ts one) plus exact delivery count. + * + * Sequential only (--synch=1). The event handler mutates LP state + * without a reverse handler, so replay under --synch=4/6 would + * scramble the test's own bookkeeping. + */ + +#undef NDEBUG /* assertions must run regardless of build type */ +#include + +#include + +typedef struct splay_state { + tw_stime last_ts; + int delivery_count; +} splay_state; + +typedef struct splay_message { + int sched_index; +} splay_message; + +/* 20 events: mix of distinct timestamps (so ordering is checkable) + * and ties (so the comparator's tie path is exercised), enqueued in + * deliberately non-sorted order to force the splay tree to balance + * across both branches on each insert. */ +static const double schedule[] = { + 5.0, 1.0, 9.0, 3.0, 7.0, 2.0, 8.0, 4.0, 6.0, 10.0, + 5.0, 1.0, 3.0, 7.0, 1.0, 5.0, 9.0, 3.0, 1.0, 5.0 +}; +#define N_EVENTS (sizeof(schedule) / sizeof(schedule[0])) + +static void +splay_init(splay_state *s, tw_lp *lp) +{ + s->last_ts = 0.0; + s->delivery_count = 0; + + for (unsigned int i = 0; i < N_EVENTS; i++) { + tw_event *e = tw_event_new(lp->gid, schedule[i], lp); + splay_message *m = (splay_message *) tw_event_data(e); + m->sched_index = (int) i; + tw_event_send(e); + } +} + +static void +splay_event(splay_state *s, tw_bf *bf, splay_message *m, tw_lp *lp) +{ + (void) bf; + (void) m; + + tw_stime now = tw_now(lp); + assert(now >= s->last_ts && "splay delivered event out of timestamp order"); + s->last_ts = now; + s->delivery_count++; +} + +static void +splay_final(splay_state *s, tw_lp *lp) +{ + (void) lp; + assert(s->delivery_count == (int) N_EVENTS + && "splay lost or duplicated events"); +} + +static tw_peid +splay_map(tw_lpid gid) +{ + return (tw_peid) gid / g_tw_nlp; +} + +static tw_lptype splay_lp_type[] = { + {(init_f) splay_init, + (pre_run_f) NULL, + (event_f) splay_event, + (revent_f) NULL, + (commit_f) NULL, + (final_f) splay_final, + (map_f) splay_map, + sizeof(splay_state)}, + {0}, +}; + +int +main(int argc, char **argv) +{ + tw_init(&argc, &argv); + + tw_define_lps(1, sizeof(splay_message)); + tw_lp_settype(0, &splay_lp_type[0]); + + tw_run(); + tw_end(); + + return 0; +} From 15dec7f287f05320b243a65a2fe63e0a9c3cb16c Mon Sep 17 00:00:00 2001 From: Caitlin Ross Date: Mon, 15 Jun 2026 15:38:04 -0500 Subject: [PATCH 5/6] extract ROSS_TEST_ASSERT macro for driver tests Plain assert(cond && "msg") tells you the file:line and the verbatim expression but never the runtime values, so a ctest failure log forces a rerun under gdb to see which delivery went wrong or how many events were lost. The new macro takes a printf-style format so the dynamic context lands in the log on the first run, e.g. [ROSS_TEST] FAIL splay_order_test.c:73 (s->delivery_count == (int) N_EVENTS): delivered 17 events, expected 20 The captured #expr keeps the source-level condition visible alongside the message so they stay in sync. Aborts via MPI_Abort(MPI_COMM_WORLD, 1) rather than abort() so any multi-rank test failure doesn't strand non-faulting ranks until ctest's timeout fires. Safe any time after tw_init -- tw_init brings MPI_Init up first thing. fflush(stderr) before abort because ctest block-buffers the redirected stderr and an aborting process doesn't flush on the way out. --- tests/drivers/rand_reverse_test.c | 22 +++++++++++++------ tests/drivers/splay_order_test.c | 15 +++++++------ tests/helpers/test_assert.h | 36 +++++++++++++++++++++++++++++++ 3 files changed, 60 insertions(+), 13 deletions(-) create mode 100644 tests/helpers/test_assert.h diff --git a/tests/drivers/rand_reverse_test.c b/tests/drivers/rand_reverse_test.c index 0a619ba7..d8faeda5 100644 --- a/tests/drivers/rand_reverse_test.c +++ b/tests/drivers/rand_reverse_test.c @@ -13,10 +13,9 @@ * picture -- this isolates the test to rand-clcg4 itself. */ -#undef NDEBUG /* assertions must run regardless of build type */ -#include #include +#include "test_assert.h" #include "test_lp_minimal.h" typedef struct rng_snapshot { @@ -55,7 +54,12 @@ main(int argc, char **argv) tw_rand_unif(rng); for (int i = 0; i < 1000; i++) tw_rand_reverse_unif(rng); - assert(rng_matches(&before, rng) && "tw_rand_unif/reverse_unif asymmetry"); + ROSS_TEST_ASSERT(rng_matches(&before, rng), + "unif/reverse_unif asymmetry after 1000 round-trips: " + "Cg before=[%d,%d,%d,%d] after=[%d,%d,%d,%d] count_delta=%ld", + before.Cg[0], before.Cg[1], before.Cg[2], before.Cg[3], + rng->Cg[0], rng->Cg[1], rng->Cg[2], rng->Cg[3], + (long)(rng->count - before.count)); /* Each of these is documented to cost exactly one rng_gen_val call; * the assertion catches a regression where one of them silently @@ -63,17 +67,23 @@ main(int argc, char **argv) snapshot_rng(&before, rng); (void) tw_rand_integer(rng, 0, 1000); tw_rand_reverse_unif(rng); - assert(rng_matches(&before, rng) && "tw_rand_integer is not one unif draw"); + ROSS_TEST_ASSERT(rng_matches(&before, rng), + "tw_rand_integer not one unif draw: count_delta=%ld", + (long)(rng->count - before.count)); snapshot_rng(&before, rng); (void) tw_rand_exponential(rng, 1.0); tw_rand_reverse_unif(rng); - assert(rng_matches(&before, rng) && "tw_rand_exponential is not one unif draw"); + ROSS_TEST_ASSERT(rng_matches(&before, rng), + "tw_rand_exponential not one unif draw: count_delta=%ld", + (long)(rng->count - before.count)); snapshot_rng(&before, rng); (void) tw_rand_ulong(rng, 0, 1000); tw_rand_reverse_unif(rng); - assert(rng_matches(&before, rng) && "tw_rand_ulong is not one unif draw"); + ROSS_TEST_ASSERT(rng_matches(&before, rng), + "tw_rand_ulong not one unif draw: count_delta=%ld", + (long)(rng->count - before.count)); tw_run(); tw_end(); diff --git a/tests/drivers/splay_order_test.c b/tests/drivers/splay_order_test.c index ddcea9ef..926bc211 100644 --- a/tests/drivers/splay_order_test.c +++ b/tests/drivers/splay_order_test.c @@ -14,11 +14,10 @@ * scramble the test's own bookkeeping. */ -#undef NDEBUG /* assertions must run regardless of build type */ -#include - #include +#include "test_assert.h" + typedef struct splay_state { tw_stime last_ts; int delivery_count; @@ -56,10 +55,11 @@ static void splay_event(splay_state *s, tw_bf *bf, splay_message *m, tw_lp *lp) { (void) bf; - (void) m; tw_stime now = tw_now(lp); - assert(now >= s->last_ts && "splay delivered event out of timestamp order"); + ROSS_TEST_ASSERT(now >= s->last_ts, + "delivery #%d out of order: now=%g last_ts=%g sched_index=%d", + s->delivery_count, now, s->last_ts, m->sched_index); s->last_ts = now; s->delivery_count++; } @@ -68,8 +68,9 @@ static void splay_final(splay_state *s, tw_lp *lp) { (void) lp; - assert(s->delivery_count == (int) N_EVENTS - && "splay lost or duplicated events"); + ROSS_TEST_ASSERT(s->delivery_count == (int) N_EVENTS, + "delivered %d events, expected %d", + s->delivery_count, (int) N_EVENTS); } static tw_peid diff --git a/tests/helpers/test_assert.h b/tests/helpers/test_assert.h new file mode 100644 index 00000000..0368bf6b --- /dev/null +++ b/tests/helpers/test_assert.h @@ -0,0 +1,36 @@ +#ifndef INC_test_assert_h +#define INC_test_assert_h + +#include +#include +#include + +/* + * ROSS_TEST_ASSERT(expr, fmt, ...) + * + * Single-line failure messages that interpolate runtime values, so a + * ctest failure log tells you *what* went wrong without needing to + * rerun under gdb. The failed expression is captured as a string via + * #expr so the message and the source-level condition stay in sync. + * + * MPI_Abort rather than plain abort() so multi-rank drivers terminate + * all ranks instead of leaving the non-faulting ones to time out. For + * single-rank drivers it has the same effect as abort(). Safe to call + * any time after tw_init (which calls MPI_Init); never call before. + * + * fflush before abort because stderr is block-buffered when ctest + * redirects it, and an aborting process doesn't flush on the way out. + */ +#define ROSS_TEST_ASSERT(expr, ...) do { \ + if (!(expr)) { \ + fprintf(stderr, "[ROSS_TEST] FAIL %s:%d (%s): ", \ + __FILE__, __LINE__, #expr); \ + fprintf(stderr, __VA_ARGS__); \ + fprintf(stderr, "\n"); \ + fflush(stderr); \ + MPI_Abort(MPI_COMM_WORLD, 1); \ + abort(); \ + } \ +} while (0) + +#endif From 9c987d4958da791f60cd4856b84dc5e850415ecd Mon Sep 17 00:00:00 2001 From: Caitlin Ross Date: Mon, 15 Jun 2026 16:32:56 -0500 Subject: [PATCH 6/6] add release note --- Documentation/dev/ross-build-testing-knob.build.md | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 Documentation/dev/ross-build-testing-knob.build.md diff --git a/Documentation/dev/ross-build-testing-knob.build.md b/Documentation/dev/ross-build-testing-knob.build.md new file mode 100644 index 00000000..57ba9f93 --- /dev/null +++ b/Documentation/dev/ross-build-testing-knob.build.md @@ -0,0 +1,10 @@ +Added a project-scoped `ROSS_BUILD_TESTING` option that gates +`include(CTest)` and the test-registration helpers. Defaults to +`BUILD_TESTING`'s value when ROSS is the top-level project, so +`cmake -S . -B build` still runs tests and `-DBUILD_TESTING=OFF` still +suppresses them. Defaults `OFF` when ROSS is consumed via +`add_subdirectory()` or `FetchContent`, so a parent project's test +suite is no longer mixed with ROSS's by default — parents that want +ROSS's tests opt in with `-DROSS_BUILD_TESTING=ON`. Phold binaries +still build under `-DROSS_BUILD_TESTING=OFF -DROSS_BUILD_MODELS=ON`; +only test registration is suppressed.