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..75891ae7 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,30 @@ 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) + add_subdirectory(tests) +endif() + # ROSS Models directory OPTION(ROSS_BUILD_MODELS "Build ROSS models?" OFF) IF(ROSS_BUILD_MODELS) 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. 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) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 00000000..3dfa360f --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1,55 @@ +# 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. + +# 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) + target_include_directories(${name} PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/helpers) + target_link_libraries(${name} PRIVATE ROSS::ROSS) + + 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/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/drivers/rand_reverse_test.c b/tests/drivers/rand_reverse_test.c new file mode 100644 index 00000000..d8faeda5 --- /dev/null +++ b/tests/drivers/rand_reverse_test.c @@ -0,0 +1,92 @@ +/* + * 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. + */ + +#include + +#include "test_assert.h" +#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); + 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 + * starts drawing more (or fewer) than once. */ + snapshot_rng(&before, rng); + (void) tw_rand_integer(rng, 0, 1000); + tw_rand_reverse_unif(rng); + 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); + 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); + 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(); + + return 0; +} diff --git a/tests/drivers/splay_order_test.c b/tests/drivers/splay_order_test.c new file mode 100644 index 00000000..926bc211 --- /dev/null +++ b/tests/drivers/splay_order_test.c @@ -0,0 +1,106 @@ +/* + * 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. + */ + +#include + +#include "test_assert.h" + +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; + + tw_stime now = tw_now(lp); + 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++; +} + +static void +splay_final(splay_state *s, tw_lp *lp) +{ + (void) lp; + ROSS_TEST_ASSERT(s->delivery_count == (int) N_EVENTS, + "delivered %d events, expected %d", + s->delivery_count, (int) N_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; +} 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 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