rite_runtime/test_support.rs
1//! Test-only helpers for action implementors.
2//!
3//! Provides a [`ReporterHarness`] that owns the channels and an
4//! [`InMemorySink`](crate::InMemorySink) needed to construct a
5//! [`Reporter`] in unit and integration tests, so downstream crates can
6//! exercise their actions without dealing with channel plumbing.
7
8use std::sync::Arc;
9
10use chrono::{DateTime, Utc};
11use crossbeam_channel::{Receiver, Sender, unbounded};
12use rite_model::StepId;
13
14use crate::clock::{Clock, SystemClock};
15use crate::protocol::{ExecEvent, UiCommand};
16use crate::reporter::Reporter;
17use crate::transcript_sink::InMemorySink;
18use rite_model::StepFact;
19
20/// Owns the channels and sink needed to build a [`Reporter`] for tests.
21///
22/// The matching event receiver and command sender are kept alive for the
23/// harness's lifetime, so a reporter built from [`Self::reporter`] never
24/// sees a spurious disconnect while emitting facts.
25pub struct ReporterHarness {
26 sink: InMemorySink,
27 event_tx: Sender<ExecEvent>,
28 _event_rx: Receiver<ExecEvent>,
29 _cmd_tx: Sender<UiCommand>,
30 cmd_rx: Receiver<UiCommand>,
31}
32
33impl ReporterHarness {
34 /// Build a harness with empty channels and an empty sink.
35 #[must_use]
36 pub fn new() -> Self {
37 let (event_tx, event_rx) = unbounded();
38 let (cmd_tx, cmd_rx) = unbounded();
39 Self {
40 sink: InMemorySink::new(),
41 event_tx,
42 _event_rx: event_rx,
43 _cmd_tx: cmd_tx,
44 cmd_rx,
45 }
46 }
47
48 /// Build a reporter scoped to the given step. The reporter borrows
49 /// the harness for its lifetime.
50 ///
51 /// The entropy source is seeded with a fixed test seed, mirroring the
52 /// runner, so actions that draw values (serials, nonces) work out of the
53 /// box. Tests that need a specific seed can call
54 /// [`Reporter::seed_entropy`] again.
55 pub fn reporter(&mut self, step: StepId) -> Reporter<'_> {
56 let mut reporter = Reporter::new(
57 &self.event_tx,
58 &self.cmd_rx,
59 &mut self.sink,
60 Arc::new(SystemClock),
61 );
62 reporter.set_current_step(Some(step));
63 reporter.seed_entropy(b"rite-test-harness-seed");
64 reporter
65 }
66
67 /// Facts recorded by the harness's transcript sink, in order.
68 #[must_use]
69 pub fn facts(&self) -> &[StepFact] {
70 self.sink.facts()
71 }
72}
73
74impl Default for ReporterHarness {
75 fn default() -> Self {
76 Self::new()
77 }
78}
79
80/// A fixed instant for tests that need a deterministic event time. Arbitrary
81/// but stable, so recorded `at` values and snapshots stay reproducible.
82#[must_use]
83pub fn fixed_test_time() -> DateTime<Utc> {
84 // `from_timestamp_nanos` is infallible, unlike the seconds-based
85 // constructor, so this needs no `expect` in non-test library code.
86 DateTime::from_timestamp_nanos(1_700_000_000_000_000_000)
87}
88
89/// Wrap a fact into an [`ExecEvent::Fact`] stamped with [`fixed_test_time`],
90/// for frontend tests that feed synthetic events into a driver.
91#[must_use]
92pub fn fact_event(fact: StepFact) -> ExecEvent {
93 ExecEvent::Fact {
94 at: fixed_test_time(),
95 fact,
96 }
97}
98
99/// A [`Clock`](crate::Clock) frozen at a caller-chosen instant, for asserting
100/// that event times come from the injected clock rather than the wall clock.
101pub struct FixedClock(pub DateTime<Utc>);
102
103impl Clock for FixedClock {
104 fn now(&self) -> DateTime<Utc> {
105 self.0
106 }
107}