diff --git a/CMakeLists.txt b/CMakeLists.txt index b3625a0..80dcfab 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -76,6 +76,9 @@ target_include_directories(kickmsg target_link_libraries(kickmsg PUBLIC ${OS_LIBRARIES}) +# --- Version header (generated from git: cmake/version.cmake) --- +include(cmake/version.cmake) + # --- Install, pkg-config, find_package --- include(cmake/install.cmake) diff --git a/README.md b/README.md index 1bd3c6f..e05fdc9 100644 --- a/README.md +++ b/README.md @@ -187,7 +187,7 @@ for frame in diag.watch("/kickmsg_telemetry", interval=1.0): ```bash # Install dependencies pip install conan -conan install conanfile.py -of=build --build=missing -o unit_tests=True +conan install conan/conanfile.py -of=build --build=missing -o unit_tests=True # Configure and build cmake -S . -B build \ @@ -218,6 +218,10 @@ python examples/python/hello_schema.py python examples/python/cli_playground.py # long-running, drive the `kickmsg` CLI against it ``` +To prove kickmsg works on your own hardware -- the validation ladder from a +quick `ctest` gate to a multi-hour contention soak with a single +`VERDICT: ALL CLEAN` -- see [tests/README.md](tests/README.md). + ### As a subdirectory ```cmake @@ -242,7 +246,7 @@ target_link_libraries(my_app PRIVATE kickmsg) | macOS | `shm_open` / `mmap` | `__ulock_wait` / `__ulock_wake` | | Windows | `CreateFileMapping` / `MapViewOfFile` | `WaitOnAddress` / `WakeByAddressAll` | -Actively validated on Linux x86-64, Linux ARM64 (Raspberry Pi 4B, 12 h continuous stress), and Darwin ARM64 (Apple Silicon) via `scripts/validate.sh`. +Actively validated on Linux x86-64, Linux ARM64 (Raspberry Pi 4B, 12 h continuous stress), and Darwin ARM64 (Apple Silicon, 12 h continuous stress: 2660 passes, 0 failures, 0 reorders) via `scripts/validate.sh` and `tests/endurance.sh`. ## Architecture diff --git a/cmake/install.cmake b/cmake/install.cmake index 14ea8c8..a870ec0 100644 --- a/cmake/install.cmake +++ b/cmake/install.cmake @@ -8,6 +8,11 @@ include(CMakePackageConfigHelpers) install(DIRECTORY include/kickmsg DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} ) +# version.h is generated (cmake/version.cmake), not committed -- install it +# alongside the committed headers. +install(FILES ${KICKMSG_VERSION_H} + DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/kickmsg +) # --- Library target + export --- set_target_properties(kickmsg PROPERTIES EXPORT_NAME kickmsg) @@ -40,7 +45,7 @@ configure_package_config_file( write_basic_package_version_file( ${CMAKE_CURRENT_BINARY_DIR}/kickmsgConfigVersion.cmake - VERSION 1.0.0 + VERSION ${KICKMSG_VERSION} COMPATIBILITY SameMajorVersion ) diff --git a/cmake/kickmsg.pc.in b/cmake/kickmsg.pc.in index 15c45a0..95d8f51 100644 --- a/cmake/kickmsg.pc.in +++ b/cmake/kickmsg.pc.in @@ -5,6 +5,6 @@ includedir=${prefix}/@CMAKE_INSTALL_INCLUDEDIR@ Name: kickmsg Description: Lock-free shared-memory MPMC messaging library -Version: 1.0.0 +Version: @KICKMSG_VERSION@ Libs: -L${libdir} @KICKMSG_PC_LIBS@ @KICKMSG_PC_SYSLIBS@ Cflags: -I${includedir} diff --git a/cmake/version.cmake b/cmake/version.cmake new file mode 100644 index 0000000..5459640 --- /dev/null +++ b/cmake/version.cmake @@ -0,0 +1,112 @@ +# Generate /generated/kickmsg/version.h from cmake/version.h.in, with the +# version + git metadata derived from `git describe`. The header is regenerated +# on every build so the stamp tracks the working tree (commits, dirty state). +# +# Two modes: +# - included from CMakeLists (configure time): generates the header now and +# wires a build-time regeneration target onto the kickmsg library. +# - cmake -D KICKMSG_VERSION_SRC= -D KICKMSG_VERSION_OUT= -P version.cmake +# (build time): re-runs git and rewrites the header. +# +# With no git available (source tarball / conan-from-recipe) it falls back to +# 0.0.0 and "unknown" git fields. + +function(_kickmsg_compute_version src_dir) + set(_describe "unknown") + set(_branch "unknown") + set(_tag "") + set(_hash "unknown") + set(_dirty 0) + set(_ver "0.0.0") + + if(EXISTS "${src_dir}/.git") + execute_process(COMMAND git describe --always --tags --dirty + WORKING_DIRECTORY "${src_dir}" OUTPUT_VARIABLE _describe + OUTPUT_STRIP_TRAILING_WHITESPACE ERROR_QUIET) + execute_process(COMMAND git rev-parse --abbrev-ref HEAD + WORKING_DIRECTORY "${src_dir}" OUTPUT_VARIABLE _branch + OUTPUT_STRIP_TRAILING_WHITESPACE ERROR_QUIET) + execute_process(COMMAND git describe --tags --exact-match + WORKING_DIRECTORY "${src_dir}" OUTPUT_VARIABLE _tag + OUTPUT_STRIP_TRAILING_WHITESPACE ERROR_QUIET) + execute_process(COMMAND git rev-parse --short HEAD + WORKING_DIRECTORY "${src_dir}" OUTPUT_VARIABLE _hash + OUTPUT_STRIP_TRAILING_WHITESPACE ERROR_QUIET) + execute_process(COMMAND git status --porcelain + WORKING_DIRECTORY "${src_dir}" OUTPUT_VARIABLE _status + OUTPUT_STRIP_TRAILING_WHITESPACE ERROR_QUIET) + if(_status) + set(_dirty 1) + endif() + # Nearest tag -> clean MAJOR.MINOR.PATCH for packaging/version string. + execute_process(COMMAND git describe --tags --abbrev=0 + WORKING_DIRECTORY "${src_dir}" OUTPUT_VARIABLE _nearest + OUTPUT_STRIP_TRAILING_WHITESPACE ERROR_QUIET) + string(REGEX REPLACE "^v" "" _nearest "${_nearest}") + if(_nearest MATCHES "^[0-9]+\\.[0-9]+\\.[0-9]+") + set(_ver "${_nearest}") + endif() + endif() + + if(_ver MATCHES "^([0-9]+)\\.([0-9]+)\\.([0-9]+)") + set(_maj "${CMAKE_MATCH_1}") + set(_min "${CMAKE_MATCH_2}") + set(_pat "${CMAKE_MATCH_3}") + else() + set(_maj 0) + set(_min 0) + set(_pat 0) + endif() + + set(KICKMSG_VERSION_STRING "${_ver}" PARENT_SCOPE) + set(KICKMSG_VERSION_MAJOR "${_maj}" PARENT_SCOPE) + set(KICKMSG_VERSION_MINOR "${_min}" PARENT_SCOPE) + set(KICKMSG_VERSION_PATCH "${_pat}" PARENT_SCOPE) + set(KICKMSG_VERSION "${_maj}.${_min}.${_pat}" PARENT_SCOPE) + set(KICKMSG_GIT_DESCRIBE "${_describe}" PARENT_SCOPE) + set(KICKMSG_GIT_BRANCH "${_branch}" PARENT_SCOPE) + set(KICKMSG_GIT_TAG "${_tag}" PARENT_SCOPE) + set(KICKMSG_GIT_COMMIT_HASH "${_hash}" PARENT_SCOPE) + set(KICKMSG_GIT_DIRTY "${_dirty}" PARENT_SCOPE) +endfunction() + +# --- Build-time mode (cmake -P) --- +if(KICKMSG_VERSION_SRC) + _kickmsg_compute_version("${KICKMSG_VERSION_SRC}") + configure_file("${KICKMSG_VERSION_SRC}/cmake/version.h.in" + "${KICKMSG_VERSION_OUT}" @ONLY) + return() +endif() + +# --- Configure-time mode (include() from CMakeLists, after project()) --- +_kickmsg_compute_version("${CMAKE_CURRENT_SOURCE_DIR}") + +set(KICKMSG_VERSION_H "${CMAKE_BINARY_DIR}/generated/kickmsg/version.h") +configure_file("${CMAKE_CURRENT_SOURCE_DIR}/cmake/version.h.in" + "${KICKMSG_VERSION_H}" @ONLY) + +set(_version_deps "${CMAKE_CURRENT_SOURCE_DIR}/cmake/version.h.in") +if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/.git/HEAD") + list(APPEND _version_deps "${CMAKE_CURRENT_SOURCE_DIR}/.git/HEAD") +endif() +if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/.git/index") + list(APPEND _version_deps "${CMAKE_CURRENT_SOURCE_DIR}/.git/index") +endif() + +add_custom_command( + OUTPUT "${KICKMSG_VERSION_H}" + COMMAND ${CMAKE_COMMAND} + -D KICKMSG_VERSION_SRC=${CMAKE_CURRENT_SOURCE_DIR} + -D KICKMSG_VERSION_OUT=${KICKMSG_VERSION_H} + -P "${CMAKE_CURRENT_SOURCE_DIR}/cmake/version.cmake" + DEPENDS ${_version_deps} + COMMENT "Regenerating kickmsg/version.h from git" + VERBATIM +) +add_custom_target(kickmsg_version DEPENDS "${KICKMSG_VERSION_H}") + +# Expose the generated header on the library's public include path and make +# sure it is generated before anything that includes it compiles. +target_include_directories(kickmsg PUBLIC + $) +add_dependencies(kickmsg kickmsg_version) diff --git a/cmake/version.h.in b/cmake/version.h.in new file mode 100644 index 0000000..48f0e69 --- /dev/null +++ b/cmake/version.h.in @@ -0,0 +1,21 @@ +#ifndef KICKMSG_VERSION_H +#define KICKMSG_VERSION_H + +// Generated at build time from cmake/version.h.in by cmake/version.cmake. +// Do not edit. Values are derived from `git describe`; a build without git +// (e.g. a source tarball) falls back to 0.0.0 / "unknown". + +#define KICKMSG_VERSION_MAJOR @KICKMSG_VERSION_MAJOR@ +#define KICKMSG_VERSION_MINOR @KICKMSG_VERSION_MINOR@ +#define KICKMSG_VERSION_PATCH @KICKMSG_VERSION_PATCH@ + +#define KICKMSG_VERSION_STRING "@KICKMSG_VERSION_STRING@" + +// Precise build identity (full `git describe`, e.g. "v0.5.0-3-gabc1234-dirty"). +#define KICKMSG_GIT_DESCRIBE "@KICKMSG_GIT_DESCRIBE@" +#define KICKMSG_GIT_BRANCH "@KICKMSG_GIT_BRANCH@" +#define KICKMSG_GIT_TAG "@KICKMSG_GIT_TAG@" +#define KICKMSG_GIT_COMMIT_HASH "@KICKMSG_GIT_COMMIT_HASH@" +#define KICKMSG_GIT_DIRTY @KICKMSG_GIT_DIRTY@ + +#endif diff --git a/conan/all/conanfile.py b/conan/all/conanfile.py index fe39150..501b235 100644 --- a/conan/all/conanfile.py +++ b/conan/all/conanfile.py @@ -58,6 +58,10 @@ def package(self): dst=os.path.join(self.package_folder, "licenses")) copy(self, "*.h", src=os.path.join(self.source_folder, "include"), dst=os.path.join(self.package_folder, "include")) + # version.h is generated into the build tree (cmake/version.cmake), + # not committed under include/ -- package it from there. + copy(self, "version.h", src=os.path.join(self.build_folder, "generated"), + dst=os.path.join(self.package_folder, "include")) copy(self, "*.a", src=self.build_folder, dst=os.path.join(self.package_folder, "lib"), keep_path=False) copy(self, "*.lib", src=self.build_folder, diff --git a/include/kickmsg/version.h b/include/kickmsg/version.h deleted file mode 100644 index af4c281..0000000 --- a/include/kickmsg/version.h +++ /dev/null @@ -1,10 +0,0 @@ -#ifndef KICKMSG_VERSION_H -#define KICKMSG_VERSION_H - -#define KICKMSG_VERSION_MAJOR 1 -#define KICKMSG_VERSION_MINOR 0 -#define KICKMSG_VERSION_PATCH 0 - -#define KICKMSG_VERSION_STRING "1.0.0" - -#endif diff --git a/scripts/validate.sh b/scripts/validate.sh index 66047cb..c7081de 100755 --- a/scripts/validate.sh +++ b/scripts/validate.sh @@ -39,7 +39,7 @@ info "Platform: $(uname -s) $(uname -m)" step "Installing Conan dependencies → $BUILD_DIR" cd "$PROJECT_DIR" -conan install conanfile.py -of="$BUILD_DIR" --build=missing -o unit_tests=True +conan install conan/conanfile.py -of="$BUILD_DIR" --build=missing -o unit_tests=True step "Configuring CMake (Release, unit + crash tests)" cmake -S "$PROJECT_DIR" -B "$BUILD_PATH" \ diff --git a/src/os/darwin/SharedMemory.cc b/src/os/darwin/SharedMemory.cc index d7bf780..f9dddc7 100644 --- a/src/os/darwin/SharedMemory.cc +++ b/src/os/darwin/SharedMemory.cc @@ -28,7 +28,7 @@ namespace kickmsg if (fd_ < 0) { throw std::system_error(errno, std::system_category(), - "SharedMemory: shm_open(create)"); + "SharedMemory: shm_open(create) '" + name + "'"); } map(size); } diff --git a/src/os/linux/SharedMemory.cc b/src/os/linux/SharedMemory.cc index 6a64296..4832bec 100644 --- a/src/os/linux/SharedMemory.cc +++ b/src/os/linux/SharedMemory.cc @@ -16,7 +16,7 @@ namespace kickmsg if (fd_ < 0) { throw std::system_error(errno, std::system_category(), - "SharedMemory: shm_open(create)"); + "SharedMemory: shm_open(create) '" + name + "'"); } map(size); } diff --git a/src/os/posix/SharedMemory.cc b/src/os/posix/SharedMemory.cc index 08a1bde..d442380 100644 --- a/src/os/posix/SharedMemory.cc +++ b/src/os/posix/SharedMemory.cc @@ -14,9 +14,17 @@ namespace kickmsg { namespace { - [[noreturn]] void throw_system_error(char const* context) + [[noreturn]] void throw_system_error(char const* context, + std::string const& name = "") { - throw std::system_error(errno, std::system_category(), context); + // Append the shm name so the failure points at the offending + // region rather than just naming the syscall. + std::string msg = context; + if (not name.empty()) + { + msg += " '" + name + "'"; + } + throw std::system_error(errno, std::system_category(), msg); } } @@ -85,7 +93,7 @@ namespace kickmsg fd_ = INVALID_SHM_HANDLE; return false; } - throw_system_error("SharedMemory: shm_open(try_create)"); + throw_system_error("SharedMemory: shm_open(try_create)", name); } map(size); return true; @@ -95,7 +103,7 @@ namespace kickmsg { if (not try_open(name)) { - throw_system_error("SharedMemory: shm_open(open)"); + throw_system_error("SharedMemory: shm_open(open)", name); } } @@ -109,7 +117,7 @@ namespace kickmsg fd_ = INVALID_SHM_HANDLE; return false; } - throw_system_error("SharedMemory: shm_open(try_open)"); + throw_system_error("SharedMemory: shm_open(try_open)", name); } struct stat st{}; diff --git a/src/os/windows/SharedMemory.cc b/src/os/windows/SharedMemory.cc index 728d31f..4648cb1 100644 --- a/src/os/windows/SharedMemory.cc +++ b/src/os/windows/SharedMemory.cc @@ -1,13 +1,21 @@ #include "kickmsg/os/SharedMemory.h" +#include #include namespace kickmsg { - static void throw_last_error(char const* context) + static void throw_last_error(char const* context, std::string const& name = "") { + // Append the mapping name so the failure points at the offending + // region rather than just naming the API call. + std::string msg = context; + if (not name.empty()) + { + msg += " '" + name + "'"; + } throw std::system_error( - static_cast(GetLastError()), std::system_category(), context); + static_cast(GetLastError()), std::system_category(), msg); } SharedMemory::SharedMemory(SharedMemory&& other) noexcept @@ -68,7 +76,7 @@ namespace kickmsg fd_ = create_file_mapping(name, size); if (fd_ == INVALID_SHM_HANDLE) { - throw_last_error("SharedMemory: CreateFileMappingA(create)"); + throw_last_error("SharedMemory: CreateFileMappingA(create)", name); } map(size); } @@ -78,7 +86,7 @@ namespace kickmsg fd_ = create_file_mapping(name, size); if (fd_ == INVALID_SHM_HANDLE) { - throw_last_error("SharedMemory: CreateFileMappingA(try_create)"); + throw_last_error("SharedMemory: CreateFileMappingA(try_create)", name); } if (GetLastError() == ERROR_ALREADY_EXISTS) { @@ -94,7 +102,7 @@ namespace kickmsg { if (not try_open(name)) { - throw_last_error("SharedMemory: OpenFileMappingA(open)"); + throw_last_error("SharedMemory: OpenFileMappingA(open)", name); } } @@ -107,7 +115,7 @@ namespace kickmsg { return false; } - throw_last_error("SharedMemory: OpenFileMappingA(try_open)"); + throw_last_error("SharedMemory: OpenFileMappingA(try_open)", name); } address_ = MapViewOfFile(fd_, FILE_MAP_ALL_ACCESS, 0, 0, 0); diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 0000000..49685ec --- /dev/null +++ b/tests/README.md @@ -0,0 +1,127 @@ +# Testing kickmsg + +How to prove kickmsg works on *your* hardware. The headline is the **soak** +(step 4): a long, self-verifying run of the lock-free paths under contention, +ending in a single `VERDICT: ALL CLEAN`. Everything before it is a faster +filter you run first. + +kickmsg is lock-free shared-memory IPC, so the bugs that matter are timing- and +ordering-dependent and surface mainly under real contention on real hardware -- +especially weakly-ordered CPUs (ARM/Apple Silicon) that x86 hides. That is what +these tests exercise. + +## The suites + +| Binary | Covers | +|--------|--------| +| `kickmsg_unit` | Unit + ABI/layout asserts, schema protocol, geometry validation, recovery primitives. Deterministic. | +| `kickmsg_stress_test` | Lock-free contention: MPMC publish/receive, Treiber free-stack, pool exhaustion, churn, fairness, zero-copy. | +| `kickmsg_crash_test` | Crash recovery: fork a participant, SIGKILL mid-operation, verify the channel self-heals (POSIX only). | +| `kickmsg_registry_stress_test` | Registry under concurrent register/deregister + dead-PID sweep (POSIX only). | + +## Prerequisites + +- A C++17 compiler (gcc, clang, or apple-clang), CMake >= 3.16. +- Conan 2.x for the test dependencies (GoogleTest, argparse). Use a venv: + ```bash + python3 -m venv .venv && source .venv/bin/activate && pip install conan + ``` + +## Build + +```bash +source .venv/bin/activate # conan on PATH +scripts/configure.sh build --with=unit_tests +scripts/setup_build.sh build # conan install + CMake (detects your toolchain) +cmake --build build -j +``` + +## The validation ladder + +Run top to bottom; stop at the depth you need. Each rung is strictly more +thorough (and slower) than the last. + +### 1. Quick gate -- seconds, run on every change +```bash +ctest --test-dir build --output-on-failure +``` +Runs unit + crash + registry + a bounded stress pass. Expect: +``` +100% tests passed, 0 tests failed out of 4 +``` + +### 2. Full local pass -- minutes, before a PR / after touching lock-free or platform code +```bash +scripts/validate.sh +``` +Builds fresh, then runs the unit suite, the stress suite x10 (intermittent +ARM ordering bugs do not show in a single pass), and the crash tests. Ends with +`macOS validation complete` / clean. + +### 3. Soak -- hours, the "it works on my hardware" proof +Loops a suite for a wall-clock duration and prints a single verdict. +```bash +# Steady-state lock-free correctness: +tests/endurance.sh build/kickmsg_stress_test 1800 # 30 min +# Crash recovery, hammered: +tests/endurance.sh build/kickmsg_crash_test 1800 +# Heavier contention (see "Contention" below): +tests/endurance.sh build/kickmsg_stress_test 1800 --oversub 400 +``` +A clean run ends with: +``` +=== FINAL RESULTS === +Runs: 110 Scenarios passed: 1650 Scenarios failed: 0 Total reorders: 0 +VERDICT: ALL CLEAN +``` +`fail` and `reorders` must both be 0. A nonzero exit code (and +`VERDICT: FAILURES DETECTED`) means a real problem -- capture the log. + +### 4. Race detection -- highest value per hour +A clean Release soak only fails if a race *manifests* as corruption; +ThreadSanitizer flags the race even when it does not. A few hours of TSAN +endurance is worth more than a long Release soak for finding ordering bugs. +```bash +scripts/configure.sh build_tsan --with=unit_tests --with=tsan +scripts/setup_build.sh build_tsan && cmake --build build_tsan -j +TSAN_OPTIONS="suppressions=$PWD/tests/tsan.supp" \ + tests/endurance.sh build_tsan/kickmsg_stress_test 14400 +``` + +## Contention scales to your machine + +The stress suite sizes its thread counts to the host CPU rather than a fixed +number, so it stays a real contention test on a 192-core box and stays bounded +on a 2-core CI runner. The default targets ~1.5x cores total; tune it: +```bash +build/kickmsg_stress_test --oversub 400 # ~4x cores: heavy +build/kickmsg_stress_test --oversub 50 # light +build/kickmsg_stress_test --help +``` +Each run prints what it resolved to, e.g. `contention: 150% of 24 cores -> 18 threads/side`. + +Caveat: thread sizing uses `std::thread::hardware_concurrency()`, which reports +the host's cores and does **not** see cgroup/container CPU quotas (Docker +`--cpus`, k8s limits). Inside a CPU-throttled container, pass `--oversub` to +bound it explicitly. + +## Detached long soak (survives logout; keeps the machine awake) + +```bash +# macOS: caffeinate prevents idle sleep mid-soak +nohup caffeinate -i tests/endurance.sh build/kickmsg_stress_test 43200 > soak.log 2>&1 & +disown +# Linux: drop `caffeinate -i` +tail -f soak.log +grep -E "VERDICT|FAIL|reorders|Runs:" soak.log # summary without the \r progress noise +``` + +## Known-good results (receipts) + +- Linux x86-64 and Linux ARM64 (Raspberry Pi 4B): 12 h continuous stress, clean. +- Darwin ARM64 (Apple Silicon, 10-core): 12 h continuous stress -- 2660 runs, + 39900 scenarios, 0 failures, 0 reorders. +- TSAN: unit + stress + crash clean. + +For *why* the recovery and lock-free design is correct (and its documented +limits), see [../ARCHITECTURE.md](../ARCHITECTURE.md). diff --git a/tests/endurance.sh b/tests/endurance.sh index 72a36cb..43af316 100755 --- a/tests/endurance.sh +++ b/tests/endurance.sh @@ -1,27 +1,57 @@ #!/bin/bash +# Loop a kickmsg test binary for a wall-clock duration and tally results. +# +# Usage: endurance.sh [duration_secs] [extra args...] +# Extra args after the duration are forwarded to the binary each run, e.g. +# endurance.sh build/kickmsg_stress_test 3600 --oversub 400 # crank contention +# endurance.sh build/kickmsg_crash_test 3600 # soak crash recovery +# +# Binaries that print "=== Summary: N passed, M failed ===" (the stress suite) +# are tallied by those counts; others (e.g. the crash test) are tallied by exit +# code, one pass/fail per run. set -euo pipefail BINARY="$1" DURATION_SECS="${2:-3600}" +shift # drop binary +if [ "$#" -gt 0 ]; then + shift # drop duration, if it was given +fi +EXTRA_ARGS=("$@") # anything left is forwarded to the binary + END_TIME=$(($(date +%s) + DURATION_SECS)) PASS=0 FAIL=0 RUNS=0 REORDERS=0 echo "=== kickmsg endurance test ===" -echo "Binary: $BINARY" +echo "Binary: $BINARY ${EXTRA_ARGS[*]+"${EXTRA_ARGS[*]}"}" echo "Duration: ${DURATION_SECS}s" echo "Start: $(date)" echo "" while [ "$(date +%s)" -lt "$END_TIME" ]; do - OUTPUT=$("$BINARY" 2>&1) + RC=0 + OUTPUT=$("$BINARY" ${EXTRA_ARGS[@]+"${EXTRA_ARGS[@]}"} 2>&1) || RC=$? RUNS=$((RUNS + 1)) if [ "$RUNS" -eq 1 ]; then - echo "$OUTPUT" | grep -iE "harness built" || true + echo "$OUTPUT" | grep -iE "harness built|contention:" || true echo "" fi - SUMMARY=$(echo "$OUTPUT" | grep "Summary:" | tail -1) - RUN_PASS=$(echo "$SUMMARY" | grep -oE '[0-9]+ passed' | grep -oE '[0-9]+') - RUN_FAIL=$(echo "$SUMMARY" | grep -oE '[0-9]+ failed' | grep -oE '[0-9]+') + SUMMARY=$(echo "$OUTPUT" | grep "Summary:" | tail -1 || true) + if [ -n "$SUMMARY" ]; then + RUN_PASS=$(echo "$SUMMARY" | grep -oE '[0-9]+ passed' | grep -oE '[0-9]+') + RUN_FAIL=$(echo "$SUMMARY" | grep -oE '[0-9]+ failed' | grep -oE '[0-9]+') + else + # No summary line (e.g. crash test): tally by exit code. + if [ "$RC" -eq 0 ]; then + RUN_PASS=1 + RUN_FAIL=0 + else + RUN_PASS=0 + RUN_FAIL=1 + fi + fi + RUN_PASS=${RUN_PASS:-0} + RUN_FAIL=${RUN_FAIL:-0} RUN_REORDER=$(echo "$OUTPUT" | { grep -c "REORDER" || true; }) PASS=$((PASS + RUN_PASS)) FAIL=$((FAIL + RUN_FAIL)) @@ -31,7 +61,7 @@ while [ "$(date +%s)" -lt "$END_TIME" ]; do "$ELAPSED" "$DURATION_SECS" "$RUNS" "$PASS" "$FAIL" "$REORDERS" if [ "$RUN_FAIL" -gt 0 ]; then echo "" - echo "$OUTPUT" | grep -E "REORDER|FAIL|WARN" + echo "$OUTPUT" | grep -E "REORDER|FAIL|WARN" || true fi done echo "" diff --git a/tests/stress/main.cc b/tests/stress/main.cc index 5099cd5..ed9679e 100644 --- a/tests/stress/main.cc +++ b/tests/stress/main.cc @@ -47,8 +47,8 @@ int main(int argc, char** argv) // Build stamp + resolved contention so a run is self-describing: // __DATE__/__TIME__ is this harness TU's compile time; the ABI version // confirms the layout; oversub/cores show the contention actually used. - std::printf("kickmsg %s | shm ABI v%u | harness built %s %s\n", - KICKMSG_VERSION_STRING, + std::printf("kickmsg %s (%s) | shm ABI v%u | harness built %s %s\n", + KICKMSG_VERSION_STRING, KICKMSG_GIT_DESCRIBE, static_cast(kickmsg::VERSION), __DATE__, __TIME__); std::printf("contention: %u%% of %u cores -> %u threads/side\n\n",