hydro_lang/sim/compiled.rs
1//! Interfaces for compiled Hydro simulators and concrete simulation instances.
2//!
3//! # Quiescence and observation soundness
4//!
5//! The scheduler distinguishes two kinds of simulation work:
6//! - **Deterministic work**: running the top-level async dataflows, which simply propagate
7//! whatever data is already in flight. This makes no `nondet!` decisions, so running it can
8//! never change which executions are explored.
9//! - **Nondeterministic work**: running ticks and observations, whose behavior depends on
10//! decisions drawn from the bolero driver (batch boundaries, snapshot versions, message
11//! orderings). Each decision forks the space of possible executions.
12//!
13//! The simulation is **quiescent** when neither kind of work can make progress without new
14//! external input. Test-side observations (the methods on [`SimReceiver`] /
15//! [`SimClusterReceiver`]) interact with the scheduler while waiting, and the key soundness
16//! question is: *when is it okay for an observation to let nondeterministic work run?*
17//!
18//! **Waiting for a message is always sound.** If the message eventually arrives, the work
19//! that ran was necessary to produce it (schedules that run *extra* work are also valid
20//! executions and are explored separately). If the simulation instead quiesces without
21//! producing the message, the assertion fails and the instance ends, so nothing can observe
22//! the overrun. This is why [`SimReceiver::next`], [`SimReceiver::collect_n`], and the
23//! `assert_yields*` prefix checks are safe to use in the middle of a test.
24//!
25//! **Observing the *absence* of a message is dangerous.** Proving that "no more messages can
26//! arrive" requires driving the simulation all the way to quiescence, running *all* pending
27//! nondeterministic work. A later assertion may have needed to observe a state where that
28//! work had not yet run — e.g., `assert_yields_only([1, 2])` followed by reading a counter
29//! must be able to see the counter *before* the ticks that count `1` and `2` have fired.
30//! Forcing quiescence at the first assertion would make some executions unobservable, and
31//! extra messages produced by the forced work could surface at a *later* assertion,
32//! misattributing the failure. Absence-observing APIs therefore proceed in phases:
33//!
34//! 1. **Settle** (see `SettlePauseGuard::poll_settle`): the scheduler runs only deterministic work, pausing
35//! just before nondeterministic work. If the simulation reaches quiescence this way, the
36//! end-of-stream check is *free* — no decision was forced, no execution was cut off — and
37//! the test simply continues.
38//! 2. If nondeterministic work is pending, the check would overrun. What happens next depends
39//! on the API and engine:
40//! - The assertion APIs ([`SimReceiver::assert_no_more`], `assert_yields_only*`,
41//! `collect_n_only`) under [`CompiledSim::exhaustive`] **fork** the search on a bolero
42//! decision: one instance performs the check and then ends (via a discard panic, like
43//! `sim::continue_if!`), while sibling instances skip the check entirely and continue. The
44//! exhaustive driver enumerates the checking instance *first*, so a failing check is
45//! found before any instance runs past it — with a decision trace that leads exactly to
46//! the failing assertion. Since nothing after the check runs in the checking instance,
47//! the overrun it performs is unobservable, and the continuing instances never quiesce,
48//! so every downstream state remains reachable.
49//! - Otherwise (fuzz / RNG / replay engines, or the drain-everything APIs
50//! [`SimReceiver::try_next`], [`SimReceiver::collect`], and `collect_sorted` in every
51//! mode), the pending work runs and the instance is **tainted**
52//! (`QuiescenceState::tainted`). Reads of the now-quiescent state remain sound (they
53//! observe a fully-drained simulation that can no longer advance), so tests may drain
54//! multiple output ports at the end. But once new input is sent, the instance is
55//! **poisoned** (`QuiescenceState::poisoned`): any further receive panics (see
56//! `guard_not_poisoned`), because a failure observed after the forced overrun could
57//! have been caused by it and attributed to the wrong assertion.
58//!
59//! NOTE: This module runs inside bolero's `catch_unwind` scope, which silently
60//! swallows panics. Internal invariant checks should use `abort_assert!`
61//! rather than `panic!`/`assert!`.
62//!
63//! TODO(mingwei): Panics inside the tick DFIR (generated code in the dylib) are
64//! also caught by bolero's `catch_unwind`. Consider a mechanism to detect and
65//! propagate those as well.
66
67/// Like `assert!`, but calls `std::process::abort()` instead of `panic!()`.
68/// Use for internal invariants that must not be silently caught by bolero.
69macro_rules! abort_assert {
70 ($cond:expr, $($arg:tt)*) => {
71 if !$cond {
72 eprintln!("Simulator internal error: {}", format!($($arg)*));
73 std::process::abort();
74 }
75 };
76}
77
78use core::{fmt, panic};
79use std::cell::{Cell, RefCell};
80use std::collections::{HashMap, VecDeque};
81use std::fmt::Debug;
82use std::panic::RefUnwindSafe;
83use std::path::Path;
84use std::pin::{Pin, pin};
85use std::rc::Rc;
86use std::task::{Poll, ready};
87
88use bytes::Bytes;
89use colored::Colorize;
90use dfir_rs::scheduled::context::DfirErased;
91use dfir_rs::util::unsync::mpsc::{Receiver as UnsyncReceiver, Sender as UnsyncSender};
92use futures::StreamExt;
93use libloading::Library;
94use serde::Serialize;
95use serde::de::DeserializeOwned;
96use tempfile::TempPath;
97use tokio::sync::{Mutex, Notify};
98
99use super::runtime::{Hooks, InlineHooks};
100use super::{SimClusterReceiver, SimClusterSender, SimReceiver, SimSender};
101use crate::compile::builder::ExternalPortId;
102use crate::live_collections::stream::{ExactlyOnce, NoOrder, Ordering, Retries, TotalOrder};
103use crate::location::dynamic::LocationId;
104use crate::sim::graph::{SimExternalPort, SimExternalPortRegistry};
105use crate::sim::runtime::SimHook;
106
107struct QuiescenceState {
108 /// Set to true when the scheduler reaches quiescence; reset to false when new input is sent.
109 quiescent: Cell<bool>,
110 /// Notified when the scheduler reaches quiescence (wakes receivers waiting for data).
111 quiescence_notify: Notify,
112 /// Notified when new input is sent, signaling the scheduler to resume.
113 resume_notify: Notify,
114 /// When nonzero, the scheduler must not start nondeterministic work (ticks /
115 /// observations): once only such work remains, it sets `nondet_pending` and pauses until
116 /// resumed. Used by receivers to query whether the simulation can quiesce
117 /// deterministically. This is a count (not a bool) because multiple settling futures can
118 /// be in flight at once (e.g. `select!`/`join!` between two receiver awaits): the
119 /// scheduler must stay paused until *every* one of them has finished settling.
120 pause_nondet: Cell<usize>,
121 /// Set while the scheduler is paused because nondeterministic work is ready to run but
122 /// `pause_nondet` is set.
123 nondet_pending: Cell<bool>,
124 /// Wakers for test-side tasks waiting for the scheduler to settle (either quiesce or set
125 /// `nondet_pending`) while `pause_nondet` is set.
126 settle_wakers: RefCell<Vec<std::task::Waker>>,
127 /// Set when an observation *forced* the simulation to quiesce (running pending
128 /// nondeterministic work) outside of exhaustive mode's forking. Further observations of
129 /// the quiescent state remain sound, but once new input is sent (see `poisoned`), later
130 /// observations could misattribute failures caused by the forced overrun.
131 tainted: Cell<bool>,
132 /// Set when new input is sent after `tainted`; all further receives panic.
133 poisoned: Cell<bool>,
134}
135
136impl QuiescenceState {
137 /// Signal that new input has been sent, waking the scheduler if it was quiescent.
138 fn resume(&self) {
139 if self.tainted.get() {
140 self.poisoned.set(true);
141 }
142 self.quiescent.set(false);
143 self.resume_notify.notify_waiters();
144 }
145
146 /// Whether the scheduler is currently quiescent (no more progress possible without input).
147 fn is_quiescent(&self) -> bool {
148 self.quiescent.get()
149 }
150
151 /// Returns a future that completes when the scheduler next reaches quiescence.
152 fn notified(&self) -> tokio::sync::futures::Notified<'_> {
153 self.quiescence_notify.notified()
154 }
155
156 /// Wakes test-side tasks waiting for the scheduler to settle.
157 fn wake_settled(&self) {
158 for waker in self.settle_wakers.borrow_mut().drain(..) {
159 waker.wake();
160 }
161 }
162
163 /// Enter quiescence and wait for new input before continuing.
164 async fn wait_for_resume(&self) {
165 self.quiescent.set(true);
166 self.quiescence_notify.notify_waiters();
167 self.wake_settled();
168 self.resume_notify.notified().await;
169 self.quiescent.set(false);
170 }
171}
172
173/// Tracks a pending "settle" pause request to the scheduler (see
174/// [`QuiescenceState::pause_nondet`]), releasing it if the requesting future is dropped
175/// mid-settle (e.g. by `select!`) so the scheduler is not left paused forever. Pause
176/// requests are counted, so concurrent settling futures each hold their own request.
177struct SettlePauseGuard {
178 quiescence: Rc<QuiescenceState>,
179 active: bool,
180}
181
182impl SettlePauseGuard {
183 fn new(quiescence: Rc<QuiescenceState>) -> Self {
184 SettlePauseGuard {
185 quiescence,
186 active: false,
187 }
188 }
189
190 fn acquire(&mut self) {
191 abort_assert!(!self.active, "settle pause acquired twice");
192 self.quiescence
193 .pause_nondet
194 .set(self.quiescence.pause_nondet.get() + 1);
195 self.active = true;
196 }
197
198 fn release(&mut self) {
199 abort_assert!(self.active, "settle pause released without being acquired");
200 self.active = false;
201 self.quiescence
202 .pause_nondet
203 .set(self.quiescence.pause_nondet.get() - 1);
204 }
205
206 /// Polls the "settle" handshake with the scheduler: deterministic (non-tick) work is
207 /// allowed to run, but the scheduler pauses instead of starting nondeterministic work
208 /// (ticks / observations). Resolves to `true` if the simulation reached quiescence
209 /// deterministically, or `false` if nondeterministic work is pending (in which case the
210 /// scheduler is resumed).
211 fn poll_settle(&mut self, cx: &mut std::task::Context<'_>) -> Poll<bool> {
212 let quiescence = self.quiescence.clone();
213 if !self.active {
214 if quiescence.is_quiescent() {
215 return Poll::Ready(true);
216 }
217 self.acquire();
218 }
219
220 if quiescence.is_quiescent() {
221 self.release();
222 Poll::Ready(true)
223 } else if quiescence.nondet_pending.get() {
224 self.release();
225 quiescence.resume_notify.notify_waiters();
226 Poll::Ready(false)
227 } else {
228 // This may push a duplicate waker if we are re-polled without an intervening
229 // `wake_settled` (e.g. a `join!` sibling waking the shared task), but duplicates
230 // are harmless (waking is idempotent) and are cleared at the next `wake_settled`,
231 // so deduplicating here isn't worth the scan on every poll.
232 quiescence
233 .settle_wakers
234 .borrow_mut()
235 .push(cx.waker().clone());
236 Poll::Pending
237 }
238 }
239}
240
241impl Drop for SettlePauseGuard {
242 fn drop(&mut self) {
243 if self.active {
244 self.release();
245 // Resume the scheduler in case this was the last pause request (otherwise it
246 // would stay parked forever with nobody left to resume it). If other settlers
247 // still hold requests, this wakeup is spurious but harmless: the scheduler
248 // re-checks `pause_nondet > 0` before starting any nondeterministic work, so it
249 // immediately re-parks without running anything.
250 self.quiescence.resume_notify.notify_waiters();
251 }
252 }
253}
254
255/// Panics if the simulation has been poisoned: an earlier observation forced the simulation
256/// to quiesce (running pending nondeterministic work), and new input has been sent since, so
257/// further observations could misattribute failures caused by the forced overrun.
258fn guard_not_poisoned(quiescence: &QuiescenceState) {
259 if quiescence.poisoned.get() {
260 panic!(
261 "cannot receive more simulator output: an earlier observation (such as `try_next`, `collect`, or a quiescence assertion outside exhaustive mode) forced the simulation to quiesce by running pending nondeterministic work, and new input has been sent since. Failures observed now could be misattributed, so either restructure the test to make quiescence-forcing observations its last step, or insert an explicit `sim::quiesce().await` phase barrier before sending more input."
262 );
263 }
264}
265
266/// Runs the simulation to quiescence, as an explicit *phase barrier* between rounds of a
267/// multi-phase test.
268///
269/// All pending nondeterministic work (ticks / observations) is forced to run until no more
270/// progress is possible without new input. This deliberately narrows the explored executions:
271/// inputs sent after the barrier will never interleave with work from before it, modeling
272/// scenarios where new stimuli (such as timer ticks) arrive long after the system settles.
273/// Pair such tests with a separate barrier-free test if interleaved executions should also be
274/// explored.
275///
276/// Because the barrier is explicit, observations after it are *intended* to see the fully
277/// settled state, so — unlike [`SimReceiver::try_next`] / [`SimReceiver::collect`] forcing
278/// quiescence implicitly — it does not restrict what the test may do afterwards: receives
279/// after the barrier observe only buffered output (plus whatever later input produces), and
280/// failures cannot be misattributed across it.
281pub async fn quiesce() {
282 let quiescence =
283 CURRENT_SIM_CONNECTIONS.with(|connections| connections.borrow().quiescence.clone());
284 guard_not_poisoned(&quiescence);
285
286 let mut notified_fut = pin!(None);
287 std::future::poll_fn(|cx| {
288 if quiescence.is_quiescent() {
289 return Poll::Ready(());
290 }
291 // Registered before the scheduler can run (single-threaded), so the quiescence
292 // notification cannot be missed.
293 if notified_fut.is_none() {
294 notified_fut.set(Some(quiescence.notified()));
295 }
296 let () = ready!(notified_fut.as_mut().as_pin_mut().unwrap().poll(cx));
297 Poll::Ready(())
298 })
299 .await;
300
301 // The barrier subsumes any quiescence forced by earlier observations in this phase:
302 // everything before it has fully settled, and the test has explicitly opted into
303 // observing only post-quiescence states from here on.
304 quiescence.tainted.set(false);
305}
306
307/// Receives the next message from `receiver` while trying not to overrun the simulation:
308/// first the simulation *settles* (deterministic work runs, but the scheduler pauses before
309/// nondeterministic work). If a message arrives, it is returned; if the simulation settles to
310/// quiescence, returns `None` without having run any nondeterministic work. Otherwise the
311/// scheduler is resumed and pending nondeterministic work runs until a message arrives or the
312/// simulation quiesces; quiescing this way *taints* the simulation (see
313/// [`QuiescenceState::tainted`]).
314async fn try_next_bytes(
315 receiver: &Mutex<UnsyncReceiver<Bytes>>,
316 quiescence: &Rc<QuiescenceState>,
317) -> Option<Bytes> {
318 guard_not_poisoned(quiescence);
319
320 let mut receiver_stream = receiver.lock().await;
321 let mut settle_guard = SettlePauseGuard::new(quiescence.clone());
322 // `Some` once the settle phase has concluded that nondeterministic work is pending and
323 // we have started forcing it to run.
324 let mut notified_fut = pin!(None);
325
326 std::future::poll_fn(|cx| {
327 // A message may become available at any point (including from deterministic work
328 // while settling), so always check the stream first.
329 match receiver_stream.poll_next_unpin(cx) {
330 Poll::Ready(Some(bytes)) => return Poll::Ready(Some(bytes)),
331 Poll::Ready(None) => return Poll::Ready(None),
332 Poll::Pending => {}
333 }
334
335 if notified_fut.is_none() {
336 match settle_guard.poll_settle(cx) {
337 // Deterministically quiescent: no more messages, and nothing was overrun.
338 Poll::Ready(true) => return Poll::Ready(None),
339 // Nondeterministic work is pending; start forcing it to run. The `Notified`
340 // is created here and polled (registered) below in this same synchronous
341 // poll — before the scheduler can run — and the simulation is not currently
342 // quiescent, so the quiescence notification cannot be missed.
343 Poll::Ready(false) => notified_fut.set(Some(quiescence.notified())),
344 Poll::Pending => return Poll::Pending,
345 }
346 }
347
348 // Let the scheduler run nondeterministic work until a message arrives or the
349 // simulation quiesces. Note that merely entering this phase does not taint: if a
350 // message arrives (the `Some` exit at the top), waiting was sound for the same
351 // reason as `SimReceiver::next` — the work that ran was needed to produce it. Only
352 // *observing quiescence* after forcing the pending work taints, since that is the
353 // overrun a later observation could misattribute.
354 let () = ready!(notified_fut.as_mut().as_pin_mut().unwrap().poll(cx));
355 quiescence.tainted.set(true);
356 Poll::Ready(None)
357 })
358 .await
359}
360
361struct SimConnections {
362 input_senders: HashMap<SimExternalPort, UnsyncSender<Bytes>>,
363 output_receivers: HashMap<SimExternalPort, Rc<Mutex<UnsyncReceiver<Bytes>>>>,
364 cluster_input_senders: HashMap<SimExternalPort, HashMap<u32, UnsyncSender<Bytes>>>,
365 cluster_output_receivers:
366 HashMap<SimExternalPort, HashMap<u32, Rc<Mutex<UnsyncReceiver<Bytes>>>>>,
367 external_registered: HashMap<ExternalPortId, SimExternalPort>,
368 quiescence: Rc<QuiescenceState>,
369 log: bool,
370 /// Whether this instance is being executed by the exhaustive engine (see
371 /// [`CompiledSim::exhaustive`]), which affects how `assert_yields_only` explores
372 /// quiescence checks.
373 exhaustive: bool,
374}
375
376/// Implementation detail of [`crate::sim::continue_if!`](crate::continue_if); do not call directly.
377///
378/// If `condition` is false, aborts the current simulation instance by panicking with a special
379/// payload ([`bolero::generator::bolero_generator::any::Error`]) that bolero recognizes as an
380/// "invalid input" marker: the instance is discarded (not treated as a test failure, and never
381/// recorded as a reproducer) and exploration moves on to the next instance. If logging is
382/// enabled for the current instance, the failed assumption is logged first.
383#[doc(hidden)]
384#[track_caller]
385pub fn continue_if_impl(condition: bool, message: fmt::Arguments<'_>) {
386 if condition {
387 return;
388 }
389
390 let log = CURRENT_SIM_CONNECTIONS
391 .try_with(|connections| connections.borrow().log)
392 .unwrap_or(true);
393 if log {
394 eprintln!(
395 "{}",
396 render_continue_if_failure(std::panic::Location::caller(), message)
397 );
398 }
399
400 // Panics with `bolero_generator::any::Error`, which bolero's engines treat as an invalid
401 // input rather than a test failure. Both this function and bolero's `assume` are
402 // `#[track_caller]`, so the recorded location is the user's `continue_if!` call site.
403 bolero::generator::bolero_generator::any::assume(false, "simulation assumption failed");
404}
405
406/// Renders the log message for a failed assumption, echoing the source line with a caret
407/// pointing at the `continue_if!` call site, in the same style as the other simulator logs.
408fn render_continue_if_failure(
409 location: &std::panic::Location<'_>,
410 message: fmt::Arguments<'_>,
411) -> String {
412 use std::fmt::Write;
413
414 // `Location::file()` is relative to the directory the crate was compiled from (e.g. the
415 // workspace root), which may not match the current working directory (e.g. the crate
416 // root when running `cargo test`), so walk up from the current directory to find it.
417 let source_line = std::env::current_dir()
418 .ok()
419 .and_then(|cwd| {
420 cwd.ancestors()
421 .find_map(|base| std::fs::read_to_string(base.join(location.file())).ok())
422 })
423 .and_then(|content| {
424 content
425 .lines()
426 .nth((location.line() as usize).saturating_sub(1))
427 .map(|line| line.to_owned())
428 })
429 .unwrap_or_default();
430
431 let caret_indent = " ".repeat((location.column() as usize).saturating_sub(1));
432
433 let mut out = String::new();
434 let _ = writeln!(
435 out,
436 "\n{}",
437 "Condition failed (discarding simulation instance):"
438 .color(colored::Color::Yellow)
439 .bold()
440 );
441 let _ = writeln!(out, "{} {}", "-->".color(colored::Color::Blue), location);
442 let _ = writeln!(out, " {}{}", "|".color(colored::Color::Blue), source_line);
443 let _ = write!(
444 out,
445 " {}{}{}",
446 "|".color(colored::Color::Blue),
447 caret_indent,
448 format!("^ {}", message).color(colored::Color::Yellow)
449 );
450 out
451}
452
453tokio::task_local! {
454 static CURRENT_SIM_CONNECTIONS: RefCell<SimConnections>;
455}
456
457/// A handle to a compiled Hydro simulation, which can be instantiated and run.
458pub struct CompiledSim {
459 pub(super) _path: TempPath,
460 pub(super) lib: Library,
461 pub(super) externals_port_registry: SimExternalPortRegistry,
462 pub(super) unit_test_fuzz_iterations: usize,
463}
464
465#[sealed::sealed]
466/// A trait implemented by closures that can instantiate a compiled simulation.
467///
468/// This is needed to ensure [`RefUnwindSafe`] so instances can be created during fuzzing.
469pub trait Instantiator<'a>: RefUnwindSafe + Fn() -> CompiledSimInstance<'a> {}
470#[sealed::sealed]
471impl<'a, T: RefUnwindSafe + Fn() -> CompiledSimInstance<'a>> Instantiator<'a> for T {}
472
473fn null_handler(_args: fmt::Arguments) {}
474
475fn println_handler(args: fmt::Arguments) {
476 println!("{}", args);
477}
478
479fn eprintln_handler(args: fmt::Arguments) {
480 eprintln!("{}", args);
481}
482
483/// Creates a simulation instance, returning:
484/// - A list of async DFIRs to run (all process / cluster logic outside a tick)
485/// - A list of tick DFIRs to run (where the &'static str is for the tick location id)
486/// - A mapping of hooks for non-deterministic decisions at tick-input boundaries
487/// - A mapping of inline hooks for non-deterministic decisions inside ticks
488type SimLoaded<'a> = libloading::Symbol<
489 'a,
490 unsafe extern "Rust" fn(
491 should_color: bool,
492 external_out: &mut HashMap<usize, UnsyncReceiver<Bytes>>,
493 external_in: &mut HashMap<usize, UnsyncSender<Bytes>>,
494 cluster_external_out: &mut HashMap<usize, HashMap<u32, UnsyncReceiver<Bytes>>>,
495 cluster_external_in: &mut HashMap<usize, HashMap<u32, UnsyncSender<Bytes>>>,
496 println_handler: fn(fmt::Arguments<'_>),
497 eprintln_handler: fn(fmt::Arguments<'_>),
498 ) -> (
499 Vec<(&'static str, Option<u32>, DfirErased)>,
500 Vec<(&'static str, Option<u32>, DfirErased)>,
501 Hooks<&'static str>,
502 InlineHooks<&'static str>,
503 ),
504>;
505
506impl CompiledSim {
507 /// Executes the given closure with a single instance of the compiled simulation.
508 pub fn with_instance<T>(&self, thunk: impl FnOnce(CompiledSimInstance) -> T) -> T {
509 self.with_instantiator(|instantiator| thunk(instantiator()), true)
510 }
511
512 /// Executes the given closure with an [`Instantiator`], which can be called to create
513 /// independent instances of the simulation. This is useful for fuzzing, where we need to
514 /// re-execute the simulation several times with different decisions.
515 ///
516 /// The `always_log` parameter controls whether to log tick executions and stream releases. If
517 /// it is `true`, logging will always be enabled. If it is `false`, logging will only be
518 /// enabled if the `HYDRO_SIM_LOG` environment variable is set to `1`.
519 pub fn with_instantiator<T>(
520 &self,
521 thunk: impl FnOnce(&dyn Instantiator) -> T,
522 always_log: bool,
523 ) -> T {
524 let func: SimLoaded = unsafe { self.lib.get(b"__hydro_runtime").unwrap() };
525 let log = always_log || std::env::var("HYDRO_SIM_LOG").is_ok_and(|v| v == "1");
526 thunk(
527 &(|| CompiledSimInstance {
528 func: func.clone(),
529 externals_port_registry: self.externals_port_registry.clone(),
530 dylib_result: None,
531 log,
532 exhaustive: false,
533 }),
534 )
535 }
536
537 /// Uses a fuzzing strategy to explore possible executions of the simulation. The provided
538 /// closure will be repeatedly executed with instances of the Hydro program where the
539 /// batching boundaries, order of messages, and retries are varied.
540 ///
541 /// During development, you should run the test that invokes this function with the `cargo sim`
542 /// command, which will use `libfuzzer` to intelligently explore the execution space. If a
543 /// failure is found, a minimized test case will be produced in a `sim-failures` directory.
544 /// When running the test with `cargo test` (such as in CI), if a reproducer is found it will
545 /// be executed, and if no reproducer is found a small number of random executions will be
546 /// performed.
547 pub fn fuzz(&self, mut thunk: impl AsyncFnMut() + RefUnwindSafe) {
548 let caller_fn = crate::compile::ir::backtrace::Backtrace::get_backtrace(0)
549 .elements()
550 .into_iter()
551 .find(|e| {
552 !e.fn_name.starts_with("hydro_lang::sim::compiled")
553 && !e.fn_name.starts_with("hydro_lang::sim::flow")
554 && !e.fn_name.starts_with("fuzz<")
555 && !e.fn_name.starts_with("<hydro_lang::sim")
556 })
557 .unwrap();
558
559 let caller_path = Path::new(&caller_fn.filename.unwrap()).to_path_buf();
560 let repro_folder = caller_path.parent().unwrap().join("sim-failures");
561
562 let caller_fuzz_repro_path = repro_folder
563 .join(caller_fn.fn_name.replace("::", "__"))
564 .with_extension("bin");
565
566 if std::env::var("BOLERO_FUZZER").is_ok() {
567 let corpus_dir = std::env::current_dir().unwrap().join(".fuzz-corpus");
568 std::fs::create_dir_all(&corpus_dir).unwrap();
569 let libfuzzer_args = format!(
570 "{} {} -artifact_prefix={}/ -handle_abrt=0",
571 corpus_dir.to_str().unwrap(),
572 corpus_dir.to_str().unwrap(),
573 corpus_dir.to_str().unwrap(),
574 );
575
576 std::fs::create_dir_all(&repro_folder).unwrap();
577
578 if !std::env::var("HYDRO_NO_FAILURE_OUTPUT").is_ok_and(|v| v == "1") {
579 unsafe {
580 std::env::set_var(
581 "BOLERO_FAILURE_OUTPUT",
582 caller_fuzz_repro_path.to_str().unwrap(),
583 );
584 }
585 }
586
587 unsafe {
588 std::env::set_var("BOLERO_LIBFUZZER_ARGS", libfuzzer_args);
589 }
590
591 self.with_instantiator(
592 |instantiator| {
593 bolero::test(bolero::TargetLocation {
594 package_name: "",
595 manifest_dir: "",
596 module_path: "",
597 file: "",
598 line: 0,
599 item_path: "<unknown>::__bolero_item_path__",
600 test_name: None,
601 })
602 .run_with_replay(move |is_replay| {
603 let mut instance = instantiator();
604
605 if instance.log {
606 eprintln!(
607 "{}",
608 "\n==== New Simulation Instance ===="
609 .color(colored::Color::Cyan)
610 .bold()
611 );
612 }
613
614 if is_replay {
615 instance.log = true;
616 }
617
618 tokio::runtime::Builder::new_current_thread()
619 .build()
620 .unwrap()
621 .block_on(async { instance.run(&mut thunk).await })
622 })
623 },
624 false,
625 );
626 } else if let Ok(existing_bytes) = std::fs::read(&caller_fuzz_repro_path) {
627 self.fuzz_repro(existing_bytes, async |compiled| {
628 compiled.launch();
629 thunk().await
630 });
631 } else {
632 eprintln!(
633 "Running a fuzz test without `cargo sim` and no reproducer found at {}, using {} iterations with random inputs.",
634 caller_fuzz_repro_path.display(),
635 self.unit_test_fuzz_iterations,
636 );
637 self.with_instantiator(
638 |instantiator| {
639 bolero::test(bolero::TargetLocation {
640 package_name: "",
641 manifest_dir: "",
642 module_path: "",
643 file: ".",
644 line: 0,
645 item_path: "<unknown>::__bolero_item_path__",
646 test_name: None,
647 })
648 .with_iterations(self.unit_test_fuzz_iterations)
649 .run_with_replay(move |is_replay| {
650 let mut instance = instantiator();
651
652 if instance.log {
653 eprintln!(
654 "{}",
655 "\n==== New Simulation Instance ===="
656 .color(colored::Color::Cyan)
657 .bold()
658 );
659 }
660
661 if is_replay {
662 instance.log = true;
663 }
664
665 tokio::runtime::Builder::new_current_thread()
666 .build()
667 .unwrap()
668 .block_on(async { instance.run(&mut thunk).await })
669 })
670 },
671 false,
672 );
673 }
674 }
675
676 /// Executes the given closure with a single instance of the compiled simulation, using the
677 /// provided bytes as the source of fuzzing decisions. This can be used to manually reproduce a
678 /// failure found during fuzzing.
679 pub fn fuzz_repro<'a>(
680 &'a self,
681 bytes: Vec<u8>,
682 thunk: impl AsyncFnOnce(CompiledSimInstance) + RefUnwindSafe,
683 ) {
684 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
685 self.with_instance(|instance| {
686 bolero::bolero_engine::any::scope::with(
687 Box::new(bolero::bolero_engine::driver::object::Object(
688 bolero::bolero_engine::driver::bytes::Driver::new(
689 bytes,
690 &Default::default(),
691 ),
692 )),
693 || {
694 tokio::runtime::Builder::new_current_thread()
695 .build()
696 .unwrap()
697 .block_on(async { instance.run_without_launching(thunk).await })
698 },
699 )
700 })
701 }));
702
703 if let Err(payload) = result {
704 if payload
705 .downcast_ref::<bolero::generator::bolero_generator::any::Error>()
706 .is_some()
707 {
708 // A `continue_if!` failed (or the driver ran out of entropy) while replaying the
709 // recorded bytes. Instances that fail an assumption are never recorded as
710 // failures, so this means the reproducer is stale or does not correspond to
711 // this program.
712 panic!(
713 "simulation assumption failed while replaying recorded fuzz decisions; the reproducer may be stale or may not correspond to this program"
714 );
715 }
716 std::panic::resume_unwind(payload);
717 }
718 }
719
720 /// Exhaustively searches all possible executions of the simulation. The provided
721 /// closure will be repeatedly executed with instances of the Hydro program where the
722 /// batching boundaries, order of messages, and retries are varied.
723 ///
724 /// Exhaustive searching is feasible when the inputs to the Hydro program are finite and there
725 /// are no dataflow loops that generate infinite messages. Exhaustive searching provides a
726 /// stronger guarantee of correctness than fuzzing, but may take a long time to complete.
727 /// Because no fuzzer is involved, you can run exhaustive tests with `cargo test`.
728 ///
729 /// Returns the number of distinct executions explored.
730 pub fn exhaustive(&self, mut thunk: impl AsyncFnMut() + RefUnwindSafe) -> usize {
731 if std::env::var("BOLERO_FUZZER").is_ok() {
732 eprintln!(
733 "Cannot run exhaustive tests with a fuzzer. Please use `cargo test` instead of `cargo sim`."
734 );
735 std::process::abort();
736 }
737
738 let mut count = 0;
739 let count_mut = &mut count;
740
741 let _span = tracing::debug_span!(target: "hydro_build", "sim_exhaustive").entered();
742
743 self.with_instantiator(
744 |instantiator| {
745 bolero::test(bolero::TargetLocation {
746 package_name: "",
747 manifest_dir: "",
748 module_path: "",
749 file: "",
750 line: 0,
751 item_path: "<unknown>::__bolero_item_path__",
752 test_name: None,
753 })
754 .exhaustive()
755 .run_with_replay(move |is_replay| {
756 *count_mut += 1;
757
758 let mut instance = instantiator();
759 instance.exhaustive = true;
760 if instance.log {
761 eprintln!(
762 "{}",
763 "\n==== New Simulation Instance ===="
764 .color(colored::Color::Cyan)
765 .bold()
766 );
767 }
768
769 if is_replay {
770 instance.log = true;
771 }
772
773 tokio::runtime::Builder::new_current_thread()
774 .build()
775 .unwrap()
776 .block_on(async { instance.run(&mut thunk).await })
777 })
778 },
779 false,
780 );
781
782 count
783 }
784}
785
786// This must be a tuple because it is referenced from generated code in `graph.rs`.
787type DylibResult = (
788 Vec<(&'static str, Option<u32>, DfirErased)>,
789 Vec<(&'static str, Option<u32>, DfirErased)>,
790 Hooks<&'static str>,
791 InlineHooks<&'static str>,
792);
793
794/// A single instance of a compiled Hydro simulation, which provides methods to interactively
795/// execute the simulation, feed inputs, and receive outputs.
796pub struct CompiledSimInstance<'a> {
797 func: SimLoaded<'a>,
798 externals_port_registry: SimExternalPortRegistry,
799 dylib_result: Option<DylibResult>,
800 log: bool,
801 exhaustive: bool,
802}
803
804impl<'a> CompiledSimInstance<'a> {
805 async fn run(self, thunk: impl AsyncFnOnce() + RefUnwindSafe) {
806 self.run_without_launching(async |instance| {
807 instance.launch();
808 thunk().await;
809 })
810 .await;
811 }
812
813 async fn run_without_launching(
814 mut self,
815 thunk: impl AsyncFnOnce(CompiledSimInstance) + RefUnwindSafe,
816 ) {
817 let mut external_out: HashMap<usize, UnsyncReceiver<Bytes>> = HashMap::new();
818 let mut external_in: HashMap<usize, UnsyncSender<Bytes>> = HashMap::new();
819 let mut cluster_external_out: HashMap<usize, HashMap<u32, UnsyncReceiver<Bytes>>> =
820 HashMap::new();
821 let mut cluster_external_in: HashMap<usize, HashMap<u32, UnsyncSender<Bytes>>> =
822 HashMap::new();
823
824 let dylib_result = unsafe {
825 (self.func)(
826 colored::control::SHOULD_COLORIZE.should_colorize(),
827 &mut external_out,
828 &mut external_in,
829 &mut cluster_external_out,
830 &mut cluster_external_in,
831 if self.log {
832 println_handler
833 } else {
834 null_handler
835 },
836 if self.log {
837 eprintln_handler
838 } else {
839 null_handler
840 },
841 )
842 };
843
844 let registered = &self.externals_port_registry.registered;
845
846 let quiescence = Rc::new(QuiescenceState {
847 quiescent: Cell::new(false),
848 quiescence_notify: Notify::new(),
849 resume_notify: Notify::new(),
850 pause_nondet: Cell::new(0),
851 nondet_pending: Cell::new(false),
852 settle_wakers: RefCell::new(vec![]),
853 tainted: Cell::new(false),
854 poisoned: Cell::new(false),
855 });
856
857 let mut input_senders = HashMap::new();
858 let mut output_receivers = HashMap::new();
859 let mut cluster_input_senders = HashMap::new();
860 let mut cluster_output_receivers = HashMap::new();
861
862 #[expect(
863 clippy::disallowed_methods,
864 reason = "inserts into maps also unordered"
865 )]
866 for sim_port in registered.values() {
867 let usize_key = sim_port.into_inner();
868 if let Some(sender) = external_in.remove(&usize_key) {
869 input_senders.insert(*sim_port, sender);
870 }
871 if let Some(receiver) = external_out.remove(&usize_key) {
872 output_receivers.insert(*sim_port, Rc::new(Mutex::new(receiver)));
873 }
874 if let Some(senders) = cluster_external_in.remove(&usize_key) {
875 cluster_input_senders.insert(*sim_port, senders);
876 }
877 if let Some(receivers) = cluster_external_out.remove(&usize_key) {
878 cluster_output_receivers.insert(
879 *sim_port,
880 receivers
881 .into_iter()
882 .map(|(member, r)| (member, Rc::new(Mutex::new(r))))
883 .collect(),
884 );
885 }
886 }
887
888 self.dylib_result = Some(dylib_result);
889
890 let local_set = tokio::task::LocalSet::new();
891 local_set
892 .run_until(CURRENT_SIM_CONNECTIONS.scope(
893 RefCell::new(SimConnections {
894 input_senders,
895 output_receivers,
896 cluster_input_senders,
897 cluster_output_receivers,
898 external_registered: self.externals_port_registry.registered.clone(),
899 quiescence: quiescence.clone(),
900 log: self.log,
901 exhaustive: self.exhaustive,
902 }),
903 async move {
904 thunk(self).await;
905 },
906 ))
907 .await;
908 }
909
910 /// Launches the simulation, which will asynchronously simulate the Hydro program. This should
911 /// be invoked but before receiving any messages.
912 fn launch(self) {
913 tokio::task::spawn_local(self.schedule_with_maybe_logger::<std::io::Empty>(None));
914 }
915
916 /// Returns a future that schedules simulation with the given logger for reporting the
917 /// simulation trace.
918 pub fn schedule_with_logger<W: std::io::Write>(
919 self,
920 log_writer: W,
921 ) -> impl use<W> + Future<Output = ()> {
922 self.schedule_with_maybe_logger(Some(log_writer))
923 }
924
925 fn schedule_with_maybe_logger<W: std::io::Write>(
926 mut self,
927 log_override: Option<W>,
928 ) -> impl use<W> + Future<Output = ()> {
929 let (async_dfirs, tick_dfirs, hooks, inline_hooks) = self.dylib_result.take().unwrap();
930
931 let not_ready_observation = async_dfirs
932 .iter()
933 .map(|(lid, c_id, _)| (serde_json::from_str(lid).unwrap(), *c_id))
934 .collect();
935
936 let quiescence = CURRENT_SIM_CONNECTIONS.with(|connections| {
937 let connections = connections.borrow();
938 connections.quiescence.clone()
939 });
940
941 let mut launched = LaunchedSim {
942 async_dfirs: async_dfirs
943 .into_iter()
944 .map(|(lid, c_id, dfir)| (serde_json::from_str(lid).unwrap(), c_id, dfir))
945 .collect(),
946 possibly_ready_ticks: vec![],
947 not_ready_ticks: tick_dfirs
948 .into_iter()
949 .map(|(lid, c_id, dfir)| (serde_json::from_str(lid).unwrap(), c_id, dfir))
950 .collect(),
951 possibly_ready_observation: vec![],
952 not_ready_observation,
953 hooks: hooks
954 .into_iter()
955 .map(|((lid, cid), hs)| ((serde_json::from_str(lid).unwrap(), cid), hs))
956 .collect(),
957 inline_hooks: inline_hooks
958 .into_iter()
959 .map(|((lid, cid), hs)| ((serde_json::from_str(lid).unwrap(), cid), hs))
960 .collect(),
961 log: if self.log {
962 if let Some(w) = log_override {
963 LogKind::Custom(w)
964 } else {
965 LogKind::Stderr
966 }
967 } else {
968 LogKind::Null
969 },
970 quiescence,
971 };
972
973 async move { launched.scheduler().await }
974 }
975}
976
977impl<T: Serialize + DeserializeOwned, O: Ordering, R: Retries> Clone for SimReceiver<T, O, R> {
978 fn clone(&self) -> Self {
979 *self
980 }
981}
982
983impl<T: Serialize + DeserializeOwned, O: Ordering, R: Retries> Copy for SimReceiver<T, O, R> {}
984
985/// How a [`QuiescenceCheckFuture`] resolves the "did the stream end?" check of
986/// `assert_no_more`. Decided once the simulation has settled (run out of deterministic
987/// work).
988#[derive(Clone, Copy)]
989enum QuiescenceBranch {
990 /// Skip the check and continue the test. Only taken in exhaustive mode, where a
991 /// sibling instance performs the check instead.
992 Continue,
993 /// Perform the check, then end this simulation instance (exhaustive mode), letting
994 /// sibling instances continue past this point without forcing quiescence.
995 CheckThenEnd,
996 /// Perform the check and keep running. Taken when the simulation is already quiescent
997 /// (the check is free) and in non-exhaustive modes.
998 CheckAndKeepRunning,
999}
1000
1001/// Decides how to run the quiescence check when the simulation has pending nondeterministic
1002/// work (ticks / observations) that the check would force to run.
1003fn decide_quiescence_branch() -> QuiescenceBranch {
1004 let (exhaustive, log) = CURRENT_SIM_CONNECTIONS.with(|connections| {
1005 let connections = connections.borrow();
1006 (connections.exhaustive, connections.log)
1007 });
1008
1009 if !exhaustive {
1010 return QuiescenceBranch::CheckAndKeepRunning;
1011 }
1012
1013 // In exhaustive mode, fork the search on a bolero decision. The exhaustive driver
1014 // enumerates `false` first, so the instance that performs the quiescence check is
1015 // explored *before* any instance that continues past this assertion. This ensures that
1016 // if the stream has extra output, the failure is attributed to this assertion (with a
1017 // decision trace leading exactly to the check) rather than leaking the extra messages
1018 // into a later assertion.
1019 let continue_without_check: bool = bolero::any();
1020 if continue_without_check {
1021 if log {
1022 eprintln!(
1023 "\n{}",
1024 "Continuing past quiescence assertion without checking (checked by an earlier instance)"
1025 .color(colored::Color::Cyan)
1026 .bold()
1027 );
1028 }
1029 QuiescenceBranch::Continue
1030 } else {
1031 if log {
1032 eprintln!(
1033 "\n{}",
1034 "Checking that no more messages arrive (this instance will end after the check)"
1035 .color(colored::Color::Cyan)
1036 .bold()
1037 );
1038 }
1039 QuiescenceBranch::CheckThenEnd
1040 }
1041}
1042
1043/// Ends the current simulation instance after a passing quiescence check, by panicking with
1044/// [`bolero::generator::bolero_generator::any::Error`], which bolero's engines treat as an
1045/// invalid input rather than a test failure. The instance has verified everything up to and
1046/// including the quiescence check; sibling instances continue past the check instead.
1047fn end_instance_after_quiescence_check() -> ! {
1048 bolero::generator::bolero_generator::any::assume(
1049 false,
1050 "simulation instance ended after quiescence check",
1051 );
1052 unreachable!()
1053}
1054
1055pin_project_lite::pin_project! {
1056 // The "and then the stream ends" half of `assert_no_more` (and thus of
1057 // `assert_yields_only*` / `collect_n_only`). First lets the simulation *settle* (see
1058 // `poll_settle`): if it settles to quiescence, the check is free and the test simply
1059 // continues. Otherwise, in exhaustive mode the search forks into a checking instance and
1060 // continuing instances (see `SimReceiver::assert_no_more` and
1061 // `decide_quiescence_branch`); in non-exhaustive modes the check runs, forcing the
1062 // pending work (which taints the simulation, via `try_next_bytes`).
1063 //
1064 // See [`FutureTrackingCaller`] for why `poll` is `#[track_caller]`.
1065 struct QuiescenceCheckFuture<F: Future<Output = ()>> {
1066 #[pin]
1067 check: F,
1068 settle: SettlePauseGuard,
1069 branch: Option<QuiescenceBranch>,
1070 }
1071}
1072
1073impl<F: Future<Output = ()>> QuiescenceCheckFuture<F> {
1074 fn new(check: F) -> Self {
1075 QuiescenceCheckFuture {
1076 check,
1077 settle: SettlePauseGuard::new(
1078 CURRENT_SIM_CONNECTIONS.with(|connections| connections.borrow().quiescence.clone()),
1079 ),
1080 branch: None,
1081 }
1082 }
1083}
1084
1085impl<F: Future<Output = ()>> Future for QuiescenceCheckFuture<F> {
1086 type Output = ();
1087
1088 #[track_caller]
1089 fn poll(mut self: Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> Poll<Self::Output> {
1090 let this = self.as_mut().project();
1091
1092 if this.branch.is_none() {
1093 *this.branch = Some(if ready!(this.settle.poll_settle(cx)) {
1094 // Settled to quiescence deterministically, so the check is free.
1095 QuiescenceBranch::CheckAndKeepRunning
1096 } else {
1097 // The check would force nondeterministic work to run.
1098 decide_quiescence_branch()
1099 });
1100 }
1101
1102 match this.branch.unwrap() {
1103 QuiescenceBranch::Continue => Poll::Ready(()),
1104 QuiescenceBranch::CheckAndKeepRunning => this.check.poll(cx),
1105 QuiescenceBranch::CheckThenEnd => {
1106 ready!(this.check.poll(cx));
1107 end_instance_after_quiescence_check()
1108 }
1109 }
1110 }
1111}
1112
1113impl<T: Serialize + DeserializeOwned, O: Ordering, R: Retries> SimReceiver<T, O, R> {
1114 fn connections(&self) -> (Rc<Mutex<UnsyncReceiver<Bytes>>>, Rc<QuiescenceState>) {
1115 CURRENT_SIM_CONNECTIONS.with(|connections| {
1116 let connections = connections.borrow();
1117 let port = connections.external_registered.get(&self.0).unwrap();
1118 (
1119 connections.output_receivers.get(port).unwrap().clone(),
1120 connections.quiescence.clone(),
1121 )
1122 })
1123 }
1124
1125 /// See [`try_next_bytes`].
1126 async fn try_next_impl(&self) -> Option<T> {
1127 let (receiver, quiescence) = self.connections();
1128 try_next_bytes(&receiver, &quiescence)
1129 .await
1130 .map(|bytes| bincode::deserialize(&bytes).unwrap())
1131 }
1132
1133 /// Asserts that the stream has ended and no more messages can possibly arrive.
1134 ///
1135 /// If the check cannot be answered without running pending nondeterministic work (such
1136 /// as ticks with buffered inputs):
1137 /// - Under [`CompiledSim::exhaustive`], the search forks: one instance performs the
1138 /// check and ends there, while sibling instances skip the check and continue.
1139 /// - In other modes, the pending work runs; afterwards, sending more input and then
1140 /// attempting to receive output will panic.
1141 pub fn assert_no_more(self) -> impl Future<Output = ()>
1142 where
1143 T: Debug,
1144 {
1145 QuiescenceCheckFuture::new(FutureTrackingCaller {
1146 future: async move {
1147 if let Some(next) = self.try_next_impl().await {
1148 return Err(format!(
1149 "Stream yielded unexpected message: {:?}, expected termination",
1150 next
1151 ));
1152 }
1153 Ok(())
1154 },
1155 })
1156 }
1157}
1158
1159impl<T: Serialize + DeserializeOwned> SimReceiver<T, TotalOrder, ExactlyOnce> {
1160 /// Receives the next message from the external bincode stream, waiting (and letting the
1161 /// scheduler run any pending simulation work) until one is available. If the simulation
1162 /// becomes quiescent without producing a message, the test fails.
1163 ///
1164 /// This is safe to use in the middle of a test; to observe the *absence* of a message,
1165 /// use [`Self::try_next`] or [`Self::assert_no_more`].
1166 pub fn next(&self) -> impl use<'_, T> + Future<Output = T> {
1167 // Waiting for a message never "overruns" the simulation, even though the scheduler
1168 // may run nondeterministic ticks while we wait: if a message arrives, some pending
1169 // work was necessary to produce it (schedules that run *extra* work are also valid
1170 // executions, explored separately), and if the simulation quiesces instead, the test
1171 // fails right here — so no later observation can be affected by the overrun (the
1172 // taint set by `try_next_impl` is unobservable). See the module docs for the full
1173 // soundness reasoning.
1174 FutureTrackingCaller {
1175 future: async move {
1176 self.try_next_impl().await.ok_or_else(|| {
1177 "Stream ended (simulation quiescent), but another message was expected"
1178 .to_owned()
1179 })
1180 },
1181 }
1182 }
1183
1184 /// Receives the next message from the external bincode stream, or returns `None` if no
1185 /// more messages can possibly arrive.
1186 ///
1187 /// If answering requires forcing pending nondeterministic work to run, then afterwards,
1188 /// sending more input and then attempting to receive output will panic. Prefer
1189 /// [`Self::next`] (or [`Self::assert_no_more`]) when possible.
1190 pub async fn try_next(&self) -> Option<T> {
1191 self.try_next_impl().await
1192 }
1193
1194 /// Receives the next `n` messages from the external bincode stream, waiting (and letting
1195 /// the scheduler run any pending simulation work) until they are available. If the
1196 /// simulation becomes quiescent before `n` messages arrive, the test fails.
1197 ///
1198 /// Like [`Self::next`], this is safe to use in the middle of a test. It does not check
1199 /// that the stream ends afterwards; use [`Self::collect_n_only`] for that.
1200 pub fn collect_n<C: Default + Extend<T>>(
1201 &self,
1202 n: usize,
1203 ) -> impl use<'_, T, C> + Future<Output = C> {
1204 FutureTrackingCaller {
1205 future: async move {
1206 let mut out = C::default();
1207 for i in 0..n {
1208 // Like `next`, waiting for each message is safe mid-test; the taint on a
1209 // forced `None` is unobservable because the test fails below.
1210 if let Some(v) = self.try_next_impl().await {
1211 out.extend([v]);
1212 } else {
1213 return Err(format!(
1214 "Stream ended (simulation quiescent) after {} messages, but {} were expected",
1215 i, n
1216 ));
1217 }
1218 }
1219 Ok(out)
1220 },
1221 }
1222 }
1223
1224 /// Receives the next `n` messages (like [`Self::collect_n`]) and then asserts that the
1225 /// stream ends (like [`Self::assert_no_more`], forking the search in exhaustive mode).
1226 pub async fn collect_n_only<C: Default + Extend<T>>(self, n: usize) -> C
1227 where
1228 T: Debug,
1229 {
1230 let out = self.collect_n(n).await;
1231 self.assert_no_more().await;
1232 out
1233 }
1234
1235 /// Collects all remaining messages from the external bincode stream into a collection,
1236 /// waiting until no more messages can possibly arrive.
1237 ///
1238 /// If this has to force pending nondeterministic work to run, it should be the last
1239 /// observation of the test: afterwards, sending more input and then attempting to
1240 /// receive output will panic. When the number of expected messages is known, prefer
1241 /// [`Self::collect_n`] / [`Self::collect_n_only`].
1242 pub async fn collect<C: Default + Extend<T>>(self) -> C {
1243 let mut out = C::default();
1244 while let Some(v) = self.try_next_impl().await {
1245 out.extend([v]);
1246 }
1247 out
1248 }
1249
1250 /// Asserts that the stream yields exactly the expected sequence of messages, in order.
1251 /// This does not check that the stream ends, use [`Self::assert_yields_only`] for that.
1252 ///
1253 /// Like [`Self::next`], this is safe to use in the middle of a test.
1254 pub fn assert_yields<T2: Debug, I: IntoIterator<Item = T2>>(
1255 &self,
1256 expected: I,
1257 ) -> impl use<'_, T, T2, I> + Future<Output = ()>
1258 where
1259 T: Debug + PartialEq<T2>,
1260 {
1261 FutureTrackingCaller {
1262 future: async {
1263 let mut expected: VecDeque<T2> = expected.into_iter().collect();
1264
1265 while !expected.is_empty() {
1266 // Like `next`, waiting for each expected message is safe mid-test; the
1267 // taint on a forced `None` is unobservable because the test fails below.
1268 if let Some(next) = self.try_next_impl().await {
1269 let next_expected = expected.pop_front().unwrap();
1270 if next != next_expected {
1271 return Err(format!(
1272 "Stream yielded unexpected message: {:?}, expected: {:?}",
1273 next, next_expected
1274 ));
1275 }
1276 } else {
1277 return Err(format!(
1278 "Stream ended early, still expected: {:?}",
1279 expected
1280 ));
1281 }
1282 }
1283
1284 Ok(())
1285 },
1286 }
1287 }
1288
1289 /// Asserts that the stream yields only the expected sequence of messages, in order,
1290 /// and then ends (like [`Self::assert_no_more`], forking the search in exhaustive mode).
1291 pub fn assert_yields_only<T2: Debug, I: IntoIterator<Item = T2>>(
1292 &self,
1293 expected: I,
1294 ) -> impl use<'_, T, T2, I> + Future<Output = ()>
1295 where
1296 T: Debug + PartialEq<T2>,
1297 {
1298 ChainedFuture {
1299 first: self.assert_yields(expected),
1300 second: self.assert_no_more(),
1301 first_done: false,
1302 }
1303 }
1304}
1305
1306pin_project_lite::pin_project! {
1307 // A future that tracks the location of the `.await` call for better panic messages.
1308 //
1309 // `#[track_caller]` is important for us to create assertion methods because it makes
1310 // the panic backtrace show up at that method (instead of inside the call tree within
1311 // that method). This is e.g. what `Option::unwrap` uses. Unfortunately, `#[track_caller]`
1312 // does not work correctly for async methods (or `dyn Future` either), so we have to
1313 // create these concrete future types that (1) have `#[track_caller]` on their `poll()`
1314 // method and (2) have the `panic!` triggered in their `poll()` method (or in a directly
1315 // nested concrete future).
1316 struct FutureTrackingCaller<F> {
1317 #[pin]
1318 future: F,
1319 }
1320}
1321
1322impl<T, F: Future<Output = Result<T, String>>> Future for FutureTrackingCaller<F> {
1323 type Output = T;
1324
1325 #[track_caller]
1326 fn poll(mut self: Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> Poll<Self::Output> {
1327 match ready!(self.as_mut().project().future.poll(cx)) {
1328 Ok(v) => Poll::Ready(v),
1329 Err(e) => panic!("{}", e),
1330 }
1331 }
1332}
1333
1334pin_project_lite::pin_project! {
1335 // A future that first awaits the first future, then the second, propagating caller info.
1336 //
1337 // See [`FutureTrackingCaller`] for context.
1338 struct ChainedFuture<F1: Future<Output = ()>, F2: Future<Output = ()>> {
1339 #[pin]
1340 first: F1,
1341 #[pin]
1342 second: F2,
1343 first_done: bool,
1344 }
1345}
1346
1347impl<F1: Future<Output = ()>, F2: Future<Output = ()>> Future for ChainedFuture<F1, F2> {
1348 type Output = ();
1349
1350 #[track_caller]
1351 fn poll(mut self: Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> Poll<Self::Output> {
1352 if !self.first_done {
1353 ready!(self.as_mut().project().first.poll(cx));
1354 *self.as_mut().project().first_done = true;
1355 }
1356
1357 self.as_mut().project().second.poll(cx)
1358 }
1359}
1360
1361impl<T: Serialize + DeserializeOwned> SimReceiver<T, NoOrder, ExactlyOnce> {
1362 /// Receives the next `n` messages, sorted, waiting (and letting the scheduler run any
1363 /// pending simulation work) until they are available. If the simulation becomes quiescent
1364 /// before `n` messages arrive, the test fails.
1365 ///
1366 /// Like [`SimReceiver::next`], this is safe to use in the middle of a test.
1367 pub fn collect_n_sorted<C: Default + Extend<T> + AsMut<[T]>>(
1368 &self,
1369 n: usize,
1370 ) -> impl use<'_, T, C> + Future<Output = C>
1371 where
1372 T: Ord,
1373 {
1374 FutureTrackingCaller {
1375 future: async move {
1376 let mut out = C::default();
1377 for i in 0..n {
1378 // Like `next`, waiting for each message is safe mid-test; the taint on a
1379 // forced `None` is unobservable because the test fails below.
1380 if let Some(v) = self.try_next_impl().await {
1381 out.extend([v]);
1382 } else {
1383 return Err(format!(
1384 "Stream ended (simulation quiescent) after {} messages, but {} were expected",
1385 i, n
1386 ));
1387 }
1388 }
1389 out.as_mut().sort();
1390 Ok(out)
1391 },
1392 }
1393 }
1394
1395 /// Collects all remaining messages from the external bincode stream into a collection,
1396 /// sorting them. This will wait until no more messages can possibly arrive.
1397 ///
1398 /// If this has to force pending nondeterministic work to run, it should be the last
1399 /// observation of the test; see [`collect`](SimReceiver::collect).
1400 pub async fn collect_sorted<C: Default + Extend<T> + AsMut<[T]>>(self) -> C
1401 where
1402 T: Ord,
1403 {
1404 let mut collected = C::default();
1405 while let Some(v) = self.try_next_impl().await {
1406 collected.extend([v]);
1407 }
1408 collected.as_mut().sort();
1409 collected
1410 }
1411
1412 /// Asserts that the stream yields exactly the expected sequence of messages, in some order.
1413 /// This does not check that the stream ends, use [`Self::assert_yields_only_unordered`] for that.
1414 ///
1415 /// Like [`SimReceiver::next`], this is safe to use in the middle of a test.
1416 pub fn assert_yields_unordered<T2: Debug, I: IntoIterator<Item = T2>>(
1417 &self,
1418 expected: I,
1419 ) -> impl use<'_, T, T2, I> + Future<Output = ()>
1420 where
1421 T: Debug + PartialEq<T2>,
1422 {
1423 FutureTrackingCaller {
1424 future: async {
1425 let mut expected: Vec<T2> = expected.into_iter().collect();
1426
1427 while !expected.is_empty() {
1428 // Like `next`, waiting for each expected message is safe mid-test; the
1429 // taint on a forced `None` is unobservable because the test fails below.
1430 if let Some(next) = self.try_next_impl().await {
1431 let idx = expected.iter().enumerate().find(|(_, e)| &next == *e);
1432 if let Some((i, _)) = idx {
1433 expected.swap_remove(i);
1434 } else {
1435 return Err(format!("Stream yielded unexpected message: {:?}", next));
1436 }
1437 } else {
1438 return Err(format!(
1439 "Stream ended early, still expected: {:?}",
1440 expected
1441 ));
1442 }
1443 }
1444
1445 Ok(())
1446 },
1447 }
1448 }
1449
1450 /// Asserts that the stream yields only the expected sequence of messages, in some order,
1451 /// and then ends (like [`Self::assert_no_more`], forking the search in exhaustive mode).
1452 pub fn assert_yields_only_unordered<T2: Debug, I: IntoIterator<Item = T2>>(
1453 &self,
1454 expected: I,
1455 ) -> impl use<'_, T, T2, I> + Future<Output = ()>
1456 where
1457 T: Debug + PartialEq<T2>,
1458 {
1459 ChainedFuture {
1460 first: self.assert_yields_unordered(expected),
1461 second: self.assert_no_more(),
1462 first_done: false,
1463 }
1464 }
1465}
1466
1467impl<T: Serialize + DeserializeOwned, O: Ordering, R: Retries> SimSender<T, O, R> {
1468 fn with_sink<Out>(&self, thunk: impl FnOnce(&dyn Fn(T)) -> Out) -> Out {
1469 let (sender, quiescence) = CURRENT_SIM_CONNECTIONS.with(|connections| {
1470 let connections = connections.borrow();
1471 (
1472 connections
1473 .input_senders
1474 .get(connections.external_registered.get(&self.0).unwrap())
1475 .unwrap()
1476 .clone(),
1477 connections.quiescence.clone(),
1478 )
1479 });
1480
1481 thunk(&move |t| {
1482 sender
1483 .try_send(bincode::serialize(&t).unwrap().into())
1484 .unwrap();
1485 quiescence.resume();
1486 })
1487 }
1488}
1489
1490impl<T: Serialize + DeserializeOwned, O: Ordering> SimSender<T, O, ExactlyOnce> {
1491 /// Sends several messages to the external bincode sink. The messages will be asynchronously
1492 /// processed as part of the simulation, in non-deterministic order.
1493 pub fn send_many_unordered<I: IntoIterator<Item = T>>(&self, iter: I) {
1494 self.with_sink(|send| {
1495 for t in iter {
1496 send(t);
1497 }
1498 })
1499 }
1500}
1501
1502impl<T: Serialize + DeserializeOwned> SimSender<T, TotalOrder, ExactlyOnce> {
1503 /// Sends a message to the external bincode sink. The message will be asynchronously processed
1504 /// as part of the simulation.
1505 pub fn send(&self, t: T) {
1506 self.with_sink(|send| send(t));
1507 }
1508
1509 /// Sends several messages to the external bincode sink. The messages will be asynchronously
1510 /// processed as part of the simulation.
1511 pub fn send_many<I: IntoIterator<Item = T>>(&self, iter: I) {
1512 self.with_sink(|send| {
1513 for t in iter {
1514 send(t);
1515 }
1516 })
1517 }
1518}
1519
1520impl<T: Serialize + DeserializeOwned, O: Ordering, R: Retries> Clone
1521 for SimClusterReceiver<T, O, R>
1522{
1523 fn clone(&self) -> Self {
1524 *self
1525 }
1526}
1527
1528impl<T: Serialize + DeserializeOwned, O: Ordering, R: Retries> Copy
1529 for SimClusterReceiver<T, O, R>
1530{
1531}
1532
1533impl<T: Serialize + DeserializeOwned, O: Ordering, R: Retries> SimClusterReceiver<T, O, R> {
1534 fn member_connections(
1535 &self,
1536 member_id: u32,
1537 ) -> (Rc<Mutex<UnsyncReceiver<Bytes>>>, Rc<QuiescenceState>) {
1538 CURRENT_SIM_CONNECTIONS.with(|connections| {
1539 let connections = connections.borrow();
1540 let port = connections.external_registered.get(&self.0).unwrap();
1541 let receivers = connections.cluster_output_receivers.get(port).unwrap();
1542 (
1543 receivers[&member_id].clone(),
1544 connections.quiescence.clone(),
1545 )
1546 })
1547 }
1548
1549 /// See [`try_next_bytes`].
1550 async fn try_next_impl(&self, member_id: u32) -> Option<T> {
1551 let (receiver, quiescence) = self.member_connections(member_id);
1552 try_next_bytes(&receiver, &quiescence)
1553 .await
1554 .map(|bytes| bincode::deserialize(&bytes).unwrap())
1555 }
1556}
1557
1558impl<T: Serialize + DeserializeOwned> SimClusterReceiver<T, TotalOrder, ExactlyOnce> {
1559 /// Receives the next value from a specific cluster member, waiting (and letting the
1560 /// scheduler run any pending simulation work) until one is available. If the simulation
1561 /// becomes quiescent without producing a value, the test fails.
1562 ///
1563 /// This is safe to use in the middle of a test; to observe the *absence* of a value,
1564 /// use [`Self::try_next`].
1565 pub fn next(&self, member_id: u32) -> impl use<'_, T> + Future<Output = T> {
1566 // See `SimReceiver::next` for why waiting for a value never "overruns" the
1567 // simulation.
1568 FutureTrackingCaller {
1569 future: async move {
1570 self.try_next_impl(member_id).await.ok_or_else(|| {
1571 "Stream ended (simulation quiescent), but another message was expected"
1572 .to_owned()
1573 })
1574 },
1575 }
1576 }
1577
1578 /// Receives the next value from a specific cluster member, or returns `None` if no more
1579 /// values can possibly arrive.
1580 ///
1581 /// If answering requires forcing pending nondeterministic work to run, then afterwards,
1582 /// sending more input and then attempting to receive output will panic. Prefer
1583 /// [`Self::next`] when possible.
1584 pub async fn try_next(&self, member_id: u32) -> Option<T> {
1585 self.try_next_impl(member_id).await
1586 }
1587
1588 /// Collects all remaining values from a specific cluster member into a collection,
1589 /// waiting until no more values can possibly arrive.
1590 ///
1591 /// If this has to force pending nondeterministic work to run, it should be the last
1592 /// observation of the test; see [`SimReceiver::collect`].
1593 pub async fn collect<C: Default + Extend<T>>(self, member_id: u32) -> C {
1594 let mut out = C::default();
1595 while let Some(v) = self.try_next_impl(member_id).await {
1596 out.extend([v]);
1597 }
1598 out
1599 }
1600}
1601
1602impl<T: Serialize + DeserializeOwned> SimClusterReceiver<T, NoOrder, ExactlyOnce> {
1603 /// Receives the next `n` values from a specific cluster member, sorted, waiting (and
1604 /// letting the scheduler run any pending simulation work) until they are available. If
1605 /// the simulation becomes quiescent before `n` values arrive, the test fails.
1606 ///
1607 /// Like [`SimReceiver::next`], this is safe to use in the middle of a test.
1608 pub fn collect_n_sorted<C: Default + Extend<T> + AsMut<[T]>>(
1609 &self,
1610 member_id: u32,
1611 n: usize,
1612 ) -> impl use<'_, T, C> + Future<Output = C>
1613 where
1614 T: Ord,
1615 {
1616 FutureTrackingCaller {
1617 future: async move {
1618 let mut out = C::default();
1619 for i in 0..n {
1620 // Like `SimReceiver::next`, waiting for each message is safe mid-test;
1621 // the taint on a forced `None` is unobservable because the test fails
1622 // below.
1623 if let Some(v) = self.try_next_impl(member_id).await {
1624 out.extend([v]);
1625 } else {
1626 return Err(format!(
1627 "Stream ended (simulation quiescent) after {} messages, but {} were expected",
1628 i, n
1629 ));
1630 }
1631 }
1632 out.as_mut().sort();
1633 Ok(out)
1634 },
1635 }
1636 }
1637
1638 /// Collects all remaining values from a specific cluster member, sorted, waiting until no
1639 /// more values can possibly arrive.
1640 ///
1641 /// If this has to force pending nondeterministic work to run, it should be the last
1642 /// observation of the test; see [`SimReceiver::collect`].
1643 pub async fn collect_sorted<C: Default + Extend<T> + AsMut<[T]>>(self, member_id: u32) -> C
1644 where
1645 T: Ord,
1646 {
1647 let mut collected = C::default();
1648 while let Some(v) = self.try_next_impl(member_id).await {
1649 collected.extend([v]);
1650 }
1651 collected.as_mut().sort();
1652 collected
1653 }
1654}
1655
1656impl<T: Serialize + DeserializeOwned, O: Ordering, R: Retries> SimClusterSender<T, O, R> {
1657 fn with_sink<Out>(&self, thunk: impl FnOnce(&dyn Fn(u32, T)) -> Out) -> Out {
1658 let (senders, quiescence) = CURRENT_SIM_CONNECTIONS.with(|connections| {
1659 let connections = connections.borrow();
1660 (
1661 connections
1662 .cluster_input_senders
1663 .get(connections.external_registered.get(&self.0).unwrap())
1664 .unwrap()
1665 .clone(),
1666 connections.quiescence.clone(),
1667 )
1668 });
1669
1670 thunk(&move |member_id: u32, t: T| {
1671 let payload = bincode::serialize(&t).unwrap();
1672 senders[&member_id].try_send(Bytes::from(payload)).unwrap();
1673 quiescence.resume();
1674 })
1675 }
1676}
1677
1678impl<T: Serialize + DeserializeOwned, O: Ordering> SimClusterSender<T, O, ExactlyOnce> {
1679 /// Sends multiple values to specific cluster members. The messages will be asynchronously
1680 /// processed as part of the simulation, in non-deterministic order.
1681 pub fn send_many_unordered<I: IntoIterator<Item = (u32, T)>>(&self, iter: I) {
1682 self.with_sink(|send| {
1683 for (member_id, t) in iter {
1684 send(member_id, t);
1685 }
1686 })
1687 }
1688}
1689
1690impl<T: Serialize + DeserializeOwned> SimClusterSender<T, TotalOrder, ExactlyOnce> {
1691 /// Sends a value to a specific cluster member.
1692 pub fn send(&self, member_id: u32, t: T) {
1693 self.with_sink(|send| send(member_id, t));
1694 }
1695
1696 /// Sends multiple values to specific cluster members.
1697 pub fn send_many<I: IntoIterator<Item = (u32, T)>>(&self, iter: I) {
1698 self.with_sink(|send| {
1699 for (member_id, t) in iter {
1700 send(member_id, t);
1701 }
1702 })
1703 }
1704}
1705
1706enum LogKind<W: std::io::Write> {
1707 Null,
1708 Stderr,
1709 Custom(W),
1710}
1711
1712// via https://www.reddit.com/r/rust/comments/t69sld/is_there_a_way_to_allow_either_stdfmtwrite_or/
1713impl<W: std::io::Write> std::fmt::Write for LogKind<W> {
1714 fn write_str(&mut self, s: &str) -> Result<(), std::fmt::Error> {
1715 match self {
1716 LogKind::Null => Ok(()),
1717 LogKind::Stderr => {
1718 eprint!("{}", s);
1719 Ok(())
1720 }
1721 LogKind::Custom(w) => w.write_all(s.as_bytes()).map_err(|_| std::fmt::Error),
1722 }
1723 }
1724}
1725
1726/// A running simulation, which manages the async DFIRs, tick DFIRs, and hook-based
1727/// scheduling decisions for non-deterministic operators like `batch` and `assume_ordering`.
1728///
1729/// The scheduler loops between three kinds of work:
1730/// - **Async DFIRs**: long-running top-level dataflows (one per process/cluster member) that
1731/// produce data consumed by ticks and observations.
1732/// - **Ticks**: tick-scoped DFIRs that execute a single tick. Before running, their associated
1733/// hooks (e.g. from `batch`) are resolved to decide what data to release into the tick.
1734/// - **Observations**: top-level locations that have hooks (e.g. from `assume_ordering` on a
1735/// non-tick stream) needing decisions, but no tick DFIR to execute. The scheduler just
1736/// resolves their hooks.
1737struct LaunchedSim<W: std::io::Write> {
1738 /// Top-level async DFIRs, one per process/cluster member. These run continuously and
1739 /// produce data that feeds into ticks and observations.
1740 async_dfirs: Vec<(LocationId, Option<u32>, DfirErased)>,
1741 /// Tick DFIRs whose parent async DFIR has made progress, so they may be ready to run.
1742 /// The scheduler further filters these by checking whether their hooks have pending decisions.
1743 possibly_ready_ticks: Vec<(LocationId, Option<u32>, DfirErased)>,
1744 /// Tick DFIRs whose parent async DFIR has not yet made progress since they were last checked.
1745 not_ready_ticks: Vec<(LocationId, Option<u32>, DfirErased)>,
1746 /// Top-level locations whose async DFIR has made progress and whose hooks (from top-level
1747 /// `assume_ordering`) may have ordering decisions to resolve. Unlike ticks, these have no
1748 /// DFIR to execute — only hook resolution.
1749 possibly_ready_observation: Vec<(LocationId, Option<u32>)>,
1750 /// Top-level locations whose async DFIR has not yet made progress since they were last checked.
1751 not_ready_observation: Vec<(LocationId, Option<u32>)>,
1752 /// Hooks keyed by (location, cluster_member_id). These are resolved *before* a tick runs
1753 /// (for `batch` hooks) or standalone (for top-level `assume_ordering` hooks via observations).
1754 hooks: Hooks<LocationId>,
1755 /// Inline hooks keyed by (tick location, cluster_member_id). These are resolved *during*
1756 /// tick execution via a `tokio::select!` loop, for operators like `assume_ordering` inside
1757 /// a tick that block on ordering decisions while the tick DFIR is running.
1758 inline_hooks: InlineHooks<LocationId>,
1759 log: LogKind<W>,
1760 /// Represents quiescence state of the simulation.
1761 quiescence: Rc<QuiescenceState>,
1762}
1763
1764impl<W: std::io::Write> LaunchedSim<W> {
1765 async fn scheduler(&mut self) {
1766 loop {
1767 tokio::task::yield_now().await;
1768 let mut any_made_progress = false;
1769 for (loc, c_id, dfir) in &mut self.async_dfirs {
1770 if dfir.run_tick().await {
1771 any_made_progress = true;
1772 let (now_ready, still_not_ready): (Vec<_>, Vec<_>) = self
1773 .not_ready_ticks
1774 .drain(..)
1775 .partition(|(tick_loc, tick_c_id, _)| {
1776 let LocationId::Tick(_, outer) = tick_loc else {
1777 unreachable!()
1778 };
1779 outer.as_ref() == loc && tick_c_id == c_id
1780 });
1781
1782 self.possibly_ready_ticks.extend(now_ready);
1783 self.not_ready_ticks.extend(still_not_ready);
1784
1785 let (now_ready_obs, still_not_ready_obs): (Vec<_>, Vec<_>) = self
1786 .not_ready_observation
1787 .drain(..)
1788 .partition(|(obs_loc, obs_c_id)| obs_loc == loc && obs_c_id == c_id);
1789
1790 self.possibly_ready_observation.extend(now_ready_obs);
1791 self.not_ready_observation.extend(still_not_ready_obs);
1792 }
1793 }
1794
1795 if any_made_progress {
1796 continue;
1797 } else {
1798 use bolero::generator::*;
1799
1800 let (ready_tick, mut not_ready_tick): (Vec<_>, Vec<_>) = self
1801 .possibly_ready_ticks
1802 .drain(..)
1803 .partition(|(name, cid, _)| {
1804 let hooks = self.hooks.get(&(name.clone(), *cid)).unwrap();
1805 // All hooks must be ready (have received input or have a last value)
1806 hooks.iter().all(|hook| hook.is_ready())
1807 // And at least one hook must be able to make progress
1808 && hooks.iter().any(|hook| {
1809 hook.current_decision().unwrap_or(false)
1810 || hook.can_make_nontrivial_decision()
1811 })
1812 });
1813
1814 self.possibly_ready_ticks = ready_tick;
1815 self.not_ready_ticks.append(&mut not_ready_tick);
1816
1817 let (ready_obs, mut not_ready_obs): (Vec<_>, Vec<_>) = self
1818 .possibly_ready_observation
1819 .drain(..)
1820 .partition(|(name, cid)| {
1821 self.hooks
1822 .get(&(name.clone(), *cid))
1823 .into_iter()
1824 .flatten()
1825 .any(|hook| {
1826 hook.current_decision().unwrap_or(false)
1827 || hook.can_make_nontrivial_decision()
1828 })
1829 });
1830
1831 self.possibly_ready_observation = ready_obs;
1832 self.not_ready_observation.append(&mut not_ready_obs);
1833
1834 if self.possibly_ready_ticks.is_empty()
1835 && self.possibly_ready_observation.is_empty()
1836 {
1837 // If any tick is blocked because a hook is not ready, that's a
1838 // simulator bug — it means a singleton never received a value.
1839 for (name, cid, _) in &self.not_ready_ticks {
1840 let hooks = self.hooks.get(&(name.clone(), *cid)).unwrap();
1841 abort_assert!(
1842 hooks.iter().all(|hook| hook.is_ready()),
1843 "tick has a hook that never became ready"
1844 );
1845 }
1846
1847 // Signal quiescence and wait for new input.
1848 self.quiescence.wait_for_resume().await;
1849 } else if self.quiescence.pause_nondet.get() > 0 {
1850 // The test is querying whether the simulation can quiesce without
1851 // nondeterministic work (see `QuiescenceCheckFuture`). Report that
1852 // ticks/observations are pending and pause until the test decides how
1853 // to proceed.
1854 self.quiescence.nondet_pending.set(true);
1855 self.quiescence.wake_settled();
1856 self.quiescence.resume_notify.notified().await;
1857 self.quiescence.nondet_pending.set(false);
1858 } else {
1859 let next_tick_or_obs = (0..(self.possibly_ready_ticks.len()
1860 + self.possibly_ready_observation.len()))
1861 .any();
1862
1863 if next_tick_or_obs < self.possibly_ready_ticks.len() {
1864 let next_tick = next_tick_or_obs;
1865 let mut removed = self.possibly_ready_ticks.remove(next_tick);
1866
1867 match &mut self.log {
1868 LogKind::Null => {}
1869 LogKind::Stderr => {
1870 if let Some(cid) = &removed.1 {
1871 eprintln!(
1872 "\n{}",
1873 format!("Running Tick (Cluster Member {})", cid)
1874 .color(colored::Color::Magenta)
1875 .bold()
1876 )
1877 } else {
1878 eprintln!(
1879 "\n{}",
1880 "Running Tick".color(colored::Color::Magenta).bold()
1881 )
1882 }
1883 }
1884 LogKind::Custom(writer) => {
1885 writeln!(
1886 writer,
1887 "\n{}",
1888 "Running Tick".color(colored::Color::Magenta).bold()
1889 )
1890 .unwrap();
1891 }
1892 }
1893
1894 let mut asterisk_indenter = |_line_no, write: &mut dyn std::fmt::Write| {
1895 write.write_str(&"*".color(colored::Color::Magenta).bold())?;
1896 write.write_str(" ")
1897 };
1898
1899 let mut tick_decision_writer =
1900 (!matches!(self.log, LogKind::Null)).then(|| {
1901 indenter::indented(&mut self.log).with_format(
1902 indenter::Format::Custom {
1903 inserter: &mut asterisk_indenter,
1904 },
1905 )
1906 });
1907
1908 let hooks = self.hooks.get_mut(&(removed.0.clone(), removed.1)).unwrap();
1909 run_hooks(tick_decision_writer.as_mut(), hooks);
1910
1911 let run_tick_future = removed.2.run_tick();
1912 if let Some(inline_hooks) =
1913 self.inline_hooks.get_mut(&(removed.0.clone(), removed.1))
1914 {
1915 let mut run_tick_future_pinned = pin!(run_tick_future);
1916
1917 loop {
1918 tokio::select! {
1919 biased;
1920 r = &mut run_tick_future_pinned => {
1921 abort_assert!(r, "tick DFIR run_tick() returned false");
1922 break;
1923 }
1924 _ = async {} => {
1925 bolero_generator::any::scope::borrow_with(|driver| {
1926 for hook in inline_hooks.iter_mut() {
1927 if hook.pending_decision() {
1928 if !hook.has_decision() {
1929 hook.autonomous_decision(driver);
1930 }
1931
1932 hook.release_decision(
1933 tick_decision_writer
1934 .as_mut()
1935 .map(|w| w as &mut dyn std::fmt::Write),
1936 );
1937 }
1938 }
1939 });
1940 }
1941 }
1942 }
1943 } else {
1944 abort_assert!(
1945 run_tick_future.await,
1946 "tick DFIR run_tick() returned false"
1947 );
1948 }
1949
1950 self.possibly_ready_ticks.push(removed);
1951 } else {
1952 let next_obs = next_tick_or_obs - self.possibly_ready_ticks.len();
1953 let mut default_hooks = vec![];
1954 let hooks = self
1955 .hooks
1956 .get_mut(&self.possibly_ready_observation[next_obs])
1957 .unwrap_or(&mut default_hooks);
1958
1959 let log_writer =
1960 (!matches!(self.log, LogKind::Null)).then_some(&mut self.log);
1961 run_hooks(log_writer, hooks);
1962 }
1963 }
1964 }
1965 }
1966 }
1967}
1968
1969fn run_hooks<W: std::fmt::Write>(
1970 mut tick_decision_writer: Option<&mut W>,
1971 hooks: &mut Vec<Box<dyn SimHook>>,
1972) {
1973 let mut remaining_decision_count = hooks.len();
1974 let mut made_nontrivial_decision = false;
1975
1976 bolero::generator::bolero_generator::any::scope::borrow_with(|driver| {
1977 // first, scan manual decisions
1978 hooks.iter_mut().for_each(|hook| {
1979 if let Some(is_nontrivial) = hook.current_decision() {
1980 made_nontrivial_decision |= is_nontrivial;
1981 remaining_decision_count -= 1;
1982 } else if !hook.can_make_nontrivial_decision() {
1983 // if no nontrivial decision is possible, make a trivial one
1984 // (we need to do this in the first pass to force nontrivial decisions
1985 // on the remaining hooks)
1986 hook.autonomous_decision(driver, false);
1987 remaining_decision_count -= 1;
1988 }
1989 });
1990
1991 hooks.iter_mut().for_each(|hook| {
1992 if hook.current_decision().is_none() {
1993 made_nontrivial_decision |= hook.autonomous_decision(
1994 driver,
1995 !made_nontrivial_decision && remaining_decision_count == 1,
1996 );
1997 remaining_decision_count -= 1;
1998 }
1999
2000 hook.release_decision(
2001 tick_decision_writer
2002 .as_deref_mut()
2003 .map(|w| w as &mut dyn std::fmt::Write),
2004 );
2005 });
2006 });
2007}