|
ravel
Deterministic simulation testing for C++. Seed a bug, replay it exact.
|
Format before committing:
.clang-format and .clang-tidy at the repo root pin the style/lint rules; most editors pick them up automatically.
Everything public is in include/ravel/, with one .cpp per header in src/. Read them in this order and the design falls out:
| Header | What it is |
|---|---|
rng.hpp | VirtualRng: the one source of randomness. Every draw is a bounded integer (0 is the simplest outcome) that is recorded, and a recorded list can be replayed. The heart of the design. |
clock.hpp, trace.hpp | Virtual time, and the record of a run (an event list plus its digest). |
task.hpp, scheduler.hpp | Coroutine tasks and the scheduler that picks which ready task runs next (a choice), with a timer queue for virtual time. |
network.hpp, disk.hpp | Channel and Disk: virtual I/O with fault injection. Both are built on Scheduler::call_after and draw every fault from the rng. |
simulation.hpp | Simulation: owns one run's clock, rng, trace, scheduler, channels and disks, and checks invariants at the end. |
runner.hpp, shrink.hpp | run_seeds (many seeds in parallel) and shrink/replay (minimize a failure by editing its choice list). |
determinism.hpp, sweep.hpp | check_determinism, and the ready-made command line for test programs. |
ravel.h, src/c_api.cpp | The small C ABI; see docs/abi-policy.md. |
How a run flows. Simulation::run_until_quiescent calls Scheduler::run_until_quiescent, which loops: if nothing is ready, fire the earliest timers (advancing the virtual clock); otherwise draw a choice, pick a ready task, and resume it until its next co_await. Channel deliveries and disk completions are timers. Each step is recorded in the Trace.
The rules that keep it deterministic (breaking one breaks replay):
VirtualRng. Never use std::rand, std::random_device, <random> engines or distributions.std::map, vectors), never ones ordered by hash or pointer, when the order can affect the run.CHANGELOG.md, and the golden tests (tests/test_rng.cpp, tests/test_simulation.cpp) must be updated on purpose.Adding a fault or a feature. Put its randomness behind VirtualRng with 0 as the simplest outcome (so shrinking steers toward it), record any new kind of event in Trace (and in docs/formats.md), add tests, and extend bench/soak.cpp so the soak run exercises it.
///, and ///< after a member). The API reference is built from them, and CI fails the docs build if a public class or member has none.tests/testing.hpp (TEST(name) { CHECK(...); }); checks stay active in Release builds.CHANGELOG.md under "Unreleased".The tutorial, porting guide and debugging guide quote real code and real program output from docs/snippets. After changing either, refresh the docs and review the diff:
CI runs the same script without --update and fails if the docs are out of date, so the documentation cannot drift from what the code does.
VirtualRng and VirtualClock being seed-deterministic — same seed, same output, on every platform — is the one guarantee the entire library exists to provide. Any change touching include/ravel/rng.hpp, clock.hpp, or src/scheduler.cpp's interleaving logic needs a test proving determinism still holds, not just that the change compiles.
include/ravel/) needs a test and a README update in the same PR: docs and code change together, not in a follow-up.CHANGELOG.md under a new version heading and date, and update the compare links at the bottom.CMakeLists.txt (project(... VERSION x.y.z)), include/ravel/version.hpp, src/version.cpp, packaging/conan/conanfile.py and packaging/vcpkg/ports/ravel/vcpkg.json. (A test checks that version.hpp and version.cpp agree.)soak workflow (Actions tab, or gh workflow run soak.yml) and wait for all three jobs: the soak on Linux, macOS and Windows, the thread-pool stress under ThreadSanitizer, and the sanitized soak. Release only if it is green, and link the run in the release notes. For a bigger local run: ravel_soak 100000.vX.Y.Z and push the tag. The release workflow then checks that the version, the CHANGELOG entry and the soak run for that commit are in order, runs the tests, packs and tests the NuGet package (packaging/nuget), and creates the GitHub release with the package attached. It does not publish to nuget.org: download Ravel.Dst.X.Y.Z.nupkg from the release and run dotnet nuget push yourself (public, and a version can be unlisted but never deleted). It also refuses tags from 1.0 on; remove that check when the maintainer says so.packaging/vcpkg/ports/ravel/portfile.cmake; verify with a local vcpkg install.Never tag 1.0 without the maintainer's explicit go-ahead: from 1.0 the API, the C ABI and the meaning of a seed are promises.
This project follows the Contributor Covenant.