Skip to main content

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, PromptId, Response, 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,
44            cmd_rx,
45        }
46    }
47
48    /// Pre-stage an operator response to a prompt the action under test will
49    /// issue. A reporter allocates prompt ids from zero in order, so the first
50    /// prompt an action issues has id `0`. The response sits in the command
51    /// channel until the reporter receives it, so call this before running the
52    /// action.
53    pub fn respond(&self, prompt_id: u64, response: Response) {
54        // The receiver lives in this harness, so the send cannot fail; ignore
55        // the result rather than unwrap (this module is compiled as library
56        // code, where `unwrap`/`expect` are linted).
57        let _ = self.cmd_tx.send(UiCommand::PromptResponse {
58            prompt_id: PromptId::new(prompt_id),
59            response,
60        });
61    }
62
63    /// Build a reporter scoped to the given step. The reporter borrows
64    /// the harness for its lifetime.
65    ///
66    /// The entropy source is seeded with a fixed test seed, mirroring the
67    /// runner, so actions that draw values (serials, nonces) work out of the
68    /// box. Tests that need a specific seed can call
69    /// [`Reporter::seed_entropy`] again.
70    pub fn reporter(&mut self, step: StepId) -> Reporter<'_> {
71        let mut reporter = Reporter::new(
72            &self.event_tx,
73            &self.cmd_rx,
74            &mut self.sink,
75            Arc::new(SystemClock),
76        );
77        reporter.set_current_step(Some(step));
78        reporter.seed_entropy(b"rite-test-harness-seed");
79        reporter
80    }
81
82    /// Facts recorded by the harness's transcript sink, in order.
83    #[must_use]
84    pub fn facts(&self) -> &[StepFact] {
85        self.sink.facts()
86    }
87}
88
89impl Default for ReporterHarness {
90    fn default() -> Self {
91        Self::new()
92    }
93}
94
95/// A fixed instant for tests that need a deterministic event time. Arbitrary
96/// but stable, so recorded `at` values and snapshots stay reproducible.
97#[must_use]
98pub fn fixed_test_time() -> DateTime<Utc> {
99    // `from_timestamp_nanos` is infallible, unlike the seconds-based
100    // constructor, so this needs no `expect` in non-test library code.
101    DateTime::from_timestamp_nanos(1_700_000_000_000_000_000)
102}
103
104/// Wrap a fact into an [`ExecEvent::Fact`] stamped with [`fixed_test_time`],
105/// for frontend tests that feed synthetic events into a driver.
106#[must_use]
107pub fn fact_event(fact: StepFact) -> ExecEvent {
108    ExecEvent::Fact {
109        at: fixed_test_time(),
110        fact,
111    }
112}
113
114/// A [`Clock`](crate::Clock) frozen at a caller-chosen instant, for asserting
115/// that event times come from the injected clock rather than the wall clock.
116pub struct FixedClock(pub DateTime<Utc>);
117
118impl Clock for FixedClock {
119    fn now(&self) -> DateTime<Utc> {
120        self.0
121    }
122}