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    next_response_id: u64,
32}
33
34impl ReporterHarness {
35    /// Build a harness with empty channels and an empty sink.
36    #[must_use]
37    pub fn new() -> Self {
38        let (event_tx, event_rx) = unbounded();
39        let (cmd_tx, cmd_rx) = unbounded();
40        Self {
41            sink: InMemorySink::new(),
42            event_tx,
43            _event_rx: event_rx,
44            cmd_tx,
45            cmd_rx,
46            next_response_id: 0,
47        }
48    }
49
50    /// Pre-queue a response to the next prompt the action under test will issue.
51    ///
52    /// Prompt ids start at 0 and increase by one per prompt, so queued
53    /// responses are matched in issue order: the first call answers the first
54    /// prompt, the second answers the second, and so on. The reporter reads
55    /// commands from an unbounded channel, so queuing before `execute` is
56    /// enough and no second thread is needed.
57    ///
58    /// Assumes a single reporter built from this harness (the id sequence is
59    /// not reset by [`Self::reporter`]); that matches every current test.
60    pub fn enqueue_response(&mut self, response: Response) {
61        let prompt_id = PromptId::new(self.next_response_id);
62        self.next_response_id = self.next_response_id.wrapping_add(1);
63        // The receiver is held by the harness for its lifetime, so this send
64        // cannot disconnect.
65        let _ = self.cmd_tx.send(UiCommand::PromptResponse {
66            prompt_id,
67            response,
68        });
69    }
70
71    /// Build a reporter scoped to the given step. The reporter borrows
72    /// the harness for its lifetime.
73    ///
74    /// The entropy source is seeded with a fixed test seed, mirroring the
75    /// runner, so actions that draw values (serials, nonces) work out of the
76    /// box. Tests that need a specific seed can call
77    /// [`Reporter::seed_entropy`] again.
78    pub fn reporter(&mut self, step: StepId) -> Reporter<'_> {
79        let mut reporter = Reporter::new(
80            &self.event_tx,
81            &self.cmd_rx,
82            &mut self.sink,
83            Arc::new(SystemClock),
84        );
85        reporter.set_current_step(Some(step));
86        reporter.seed_entropy(b"rite-test-harness-seed");
87        reporter
88    }
89
90    /// Facts recorded by the harness's transcript sink, in order.
91    #[must_use]
92    pub fn facts(&self) -> &[StepFact] {
93        self.sink.facts()
94    }
95}
96
97impl Default for ReporterHarness {
98    fn default() -> Self {
99        Self::new()
100    }
101}
102
103/// A fixed instant for tests that need a deterministic event time. Arbitrary
104/// but stable, so recorded `at` values and snapshots stay reproducible.
105#[must_use]
106pub fn fixed_test_time() -> DateTime<Utc> {
107    // `from_timestamp_nanos` is infallible, unlike the seconds-based
108    // constructor, so this needs no `expect` in non-test library code.
109    DateTime::from_timestamp_nanos(1_700_000_000_000_000_000)
110}
111
112/// Wrap a fact into an [`ExecEvent::Fact`] stamped with [`fixed_test_time`],
113/// for frontend tests that feed synthetic events into a driver.
114#[must_use]
115pub fn fact_event(fact: StepFact) -> ExecEvent {
116    ExecEvent::Fact {
117        at: fixed_test_time(),
118        fact,
119    }
120}
121
122/// A [`Clock`](crate::Clock) frozen at a caller-chosen instant, for asserting
123/// that event times come from the injected clock rather than the wall clock.
124pub struct FixedClock(pub DateTime<Utc>);
125
126impl Clock for FixedClock {
127    fn now(&self) -> DateTime<Utc> {
128        self.0
129    }
130}