|
ravel
Deterministic simulation testing for C++. Seed a bug, replay it exact.
|
Something went wrong: an error message, a crash, a result that surprises you. Find it here. Entries are grouped by where you meet the problem.
Each message below is checked against the source by tools/check_docs.py, so what you read here is what ravel really prints. For what the failure messages of a run mean (an invariant failed, a livelock, and so on) see the debugging guide.
Compile errors about co_await, co_return or <coroutine>. ravel needs C++20 with coroutines: GCC 10 or later, Clang 14 or later, or MSVC 2019 or later, built as C++20. With CMake, target_compile_features(my_test PRIVATE cxx_std_20) does it. GCC 10 also needs -fcoroutines; GCC 11 and later turn coroutines on with -std=c++20.
Linker errors about pthread_create or std::thread on Linux. Link ravel::ravel through CMake (it brings the thread library along). If you link by hand, add -pthread.
An error about designated initializers ({.loss_probability = 0.3}).** These are C++20. Make sure the whole target is built as C++20, not only the ravel library.
**The tutorial's FetchContent builds tests and examples too. It should not: they are off when ravel is a subproject. If you see them building, check that you use FetchContent_MakeAvailable and not a manual add_subdirectory with options set.
shrink replays a failure to minimize it, and this one did not happen the second time. Something outside ravel influences the code under test. Run ravel::check_determinism (or --check-determinism in a run_sweep program): it names the first step where two runs of the same seed part ways. See the checklist.
check_determinism reports one of these, each naming a step and the two events that differ:
two runs of the same seed diverged at step N**: the code is not repeatable. The step names the first event that differs. Look at what feeds that task's timing or decisions: a clock, a static, a container ordered by pointer.one run ended early**: one run had more events than the other, so something changed the number of steps, often state left over from the previous run.the runs made the same moves but ... ended with ...**: the events are identical but an invariant gave a different answer, so the invariant reads something that differs between runs (a static counter, a real clock).replaying run 1's recorded choices ... (something other than ravel's random choices is steering the run)**: the run depends on something ravel does not record, so a saved .choices file would not reproduce it.Your setup function (or the run itself) threw an exception. In a sweep that seed fails, and the rest carry on. The text after the colon is the exception's what(). Setup runs on several threads at once in run_seeds and shrink, so an exception that appears only sometimes usually means the setup touches shared state.
simulation threw an unknown exception is the same, for an exception that is not a std::exception.
A task let an exception escape, which stops the run at that step. If the exception is meant to be part of the test, catch it inside the task.
Tasks kept running without the run ending: usually two tasks yielding to each other, or a retry loop with no backoff. See debugging.md. Raise SimulationOptions::max_steps only if the system is legitimately that busy. If it never goes quiet by design (heartbeats), set time_limit instead.
A channel has at most one receiving task at a time. Give each receiver its own channel (a common pattern is one inbox channel per node that everyone else sends to), or funnel messages to one task that distributes them.
Channel::clear_inbox() was called by a task other than the receiver, at the moment the receiver had a message queued to take. Call clear_inbox from the receiving task itself, for instance when it reboots after a simulated crash.
Code under test started a real thread (or a thread pool) and used the simulation from it. A simulation is cooperative and single-threaded; replace the thread with a ravel task (sim.scheduler().spawn(...)), and any real wait with sleep or receive_within. (call_after is named instead of spawn when a channel or disk was the thing used.)
FaultSpec: loss_probability must be in [0, 1], FaultSpec: latency_min must not exceed latency_max, and DiskFaultSpec: error probabilities must be in [0, 1] (plus its own latency_min message) mean what they say. They are thrown by add_channel and add_disk.
trace_dir is set but ravel could not create the directory or the file: check the path is writable, and that nothing already exists there as a file. The failure itself is unaffected; only the trace is missing.
read_choices (and --replay) accept only what ravel wrote: the header ravel-choices 1, a count, then that many whole numbers. A different version, a missing header, a file cut short, or a value that is not a whole number is refused with one of these two messages. See formats.md.
For programs built on ravel::run_sweep_main. Every one ends with ‘Try 'my_test --help’.` and exit status 2.
**: no such flag. The flag is–seeds<tt>(how many) or –first-seed(where to start).–helplists them all.**–seeds needs a value**: the flag was last on the line, or its value is missing.**–seeds needs a non-negative whole number, got 'many'**: numbers only.**cannot open 'x.choices'**:–replay` could not open the file. Run from the directory the path is relative to, or pass an absolute path.Nothing forces a bug to show up: a run only finds what its faults and invariants can reach.
co_awaits between the steps that should be reorderable? Two operations with none between them cannot be interleaved.The same seed must give the same run everywhere. If it does not, something leaks into the run. --check-determinism finds it. The usual suspects are uninitialized memory, a container ordered by pointer, static state, and a real clock or random source.
Almost always a dangling reference: a coroutine holding a reference to a local variable of the setup function, which has returned by the time the task runs. Keep state in sim.make_state<T>() and capture the reference it returns. See the pitfall. A debug build with AddressSanitizer reports it precisely.
Not the step limit (that fails cleanly), but a real hang: a task did something that really blocks (a real sleep_for, a blocking socket read, waiting on a real mutex or future). ravel is cooperative, so one blocked task stalls everything. Replace it with co_await sim.scheduler().sleep(...) or a channel receive. A debugger's stack of the hung program shows where.
budget_exhausted on the ShrinkResult: if it is true, shrinking stopped at max_attempts (20000 by default) and could go further with more.Each attempt is a whole run, so long or heavy runs make shrinking slow. Shorten the run (time_limit, fewer operations), lower max_shrink_attempts, or give it more threads: ShrinkOptions::threads defaults to every core.
It records every step. Shrink first (the minimal run is usually short), bound the run with max_steps or time_limit, and use ravel_trace.py with --name, --kind, --from and --to to look at a slice.
The choice list records which random draws happen and in what order. If you change the code under test so that different draws happen (a new message, a different number of tasks), the old list no longer describes the same run, and it may now pass. Find the bug again with a sweep and save a fresh reproducer; keep the old one anyway, as it still guards the exact run it captured as long as the structure is unchanged.
tools/check_docs.py found a code or output block that differs from what the programs now do. That is intended: run python3 tools/check_docs.py --build-dir build --update, read the diff, and commit it if the new output is right.