Skip to main content

logicaffeine_system/
testworld.rs

1//! The sealed test world.
2//!
3//! While a `## Test` runs, a thread-local [`TestWorld`] intercepts the ambient
4//! IO natives with SIMULATED resources that have real semantics: a virtual
5//! clock frozen at a fixed epoch, an in-memory filesystem seeded by
6//! `Given a file …`, an empty environment, and a per-test seeded RNG. There is
7//! no unrecorded entropy API to misuse — a test is a pure function of
8//! `(program, seed)`. Capability sentences (`This test may use the real
9//! filesystem.`) opt individual resources back to reality under `--live`.
10//!
11//! Zero cost outside tests: each native pays ONE thread-local check on an IO
12//! call (noise next to the syscall it guards); compute paths pay nothing.
13
14use std::cell::RefCell;
15use std::collections::HashMap;
16
17/// The sealed world's fixed epoch: 2020-01-01T00:00:00Z in ms — a stable,
18/// obviously-synthetic date (no test can accidentally depend on today).
19pub const SEALED_EPOCH_MS: u64 = 1_577_836_800_000;
20
21/// Which resources a test may touch FOR REAL (granted by capability sentences).
22#[derive(Debug, Clone, Copy, Default)]
23pub struct Capabilities {
24    pub filesystem: bool,
25    pub network: bool,
26    pub environment: bool,
27    pub clock: bool,
28}
29
30/// One test's sealed world.
31#[derive(Debug)]
32pub struct TestWorld {
33    pub caps: Capabilities,
34    /// Virtual clock (ms since the Unix epoch), starting at [`SEALED_EPOCH_MS`].
35    pub clock_ms: u64,
36    /// The in-memory filesystem (path → content), empty per test.
37    pub files: HashMap<String, String>,
38    /// The sealed environment, empty unless the test declares variables.
39    pub env: HashMap<String, String>,
40    /// SplitMix64 state — the test's recorded entropy stream.
41    pub rng: u64,
42}
43
44impl TestWorld {
45    /// A fresh sealed world under the test's identity-keyed seed.
46    pub fn new(caps: Capabilities, seed: u64) -> Self {
47        TestWorld {
48            caps,
49            clock_ms: SEALED_EPOCH_MS,
50            files: HashMap::new(),
51            env: HashMap::new(),
52            rng: seed,
53        }
54    }
55
56    fn next_u64(&mut self) -> u64 {
57        // SplitMix64 — identical to the runtime's seeded stream discipline.
58        self.rng = self.rng.wrapping_add(0x9E37_79B9_7F4A_7C15);
59        let mut z = self.rng;
60        z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
61        z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
62        z ^ (z >> 31)
63    }
64}
65
66thread_local! {
67    static WORLD: RefCell<Option<TestWorld>> = const { RefCell::new(None) };
68}
69
70/// Install a sealed world for the current thread (the runner, per test).
71pub fn install(world: TestWorld) {
72    WORLD.with(|w| *w.borrow_mut() = Some(world));
73}
74
75/// Remove the sealed world (the runner, after each test).
76pub fn uninstall() {
77    WORLD.with(|w| *w.borrow_mut() = None);
78}
79
80/// Take the installed world, if any, leaving none installed — a nested caller (a candidate page
81/// tier compared against the reference) saves whatever world its caller had before installing its
82/// own, and [`restore`]s it when it is done.
83pub fn take() -> Option<TestWorld> {
84    WORLD.with(|w| w.borrow_mut().take())
85}
86
87/// Put back a world a matching [`take`] returned (or `None`, mirroring its absence).
88pub fn restore(world: Option<TestWorld>) {
89    WORLD.with(|w| *w.borrow_mut() = world);
90}
91
92/// Run `f` against the installed world, if any.
93pub fn with_world<R>(f: impl FnOnce(&mut TestWorld) -> R) -> Option<R> {
94    WORLD.with(|w| w.borrow_mut().as_mut().map(f))
95}
96
97/// Seed a fixture file (`Given a file "…" containing "…".`).
98pub fn seed_file(path: String, content: String) {
99    with_world(|w| {
100        w.files.insert(path, content);
101    });
102}
103
104/// Advance the virtual clock (`Advance time by …`). Returns false when no
105/// world is installed (the caller reports the test-only error).
106pub fn advance_clock(ms: u64) -> bool {
107    with_world(|w| w.clock_ms = w.clock_ms.saturating_add(ms)).is_some()
108}
109
110// ── the native intercepts (None = no sealed world / capability granted → real) ──
111
112pub fn intercept_read(path: &str) -> Option<Result<String, String>> {
113    with_world(|w| {
114        if w.caps.filesystem {
115            return None;
116        }
117        Some(match w.files.get(path) {
118            Some(content) => Ok(content.clone()),
119            None => Err(format!(
120                "'{path}' is not in the test's sealed filesystem — seed it with \
121                 `Given a file \"{path}\" containing \"…\".`, or grant \
122                 `This test may use the real filesystem.`"
123            )),
124        })
125    })
126    .flatten()
127}
128
129pub fn intercept_write(path: &str, content: &str) -> Option<Result<(), String>> {
130    with_world(|w| {
131        if w.caps.filesystem {
132            return None;
133        }
134        w.files.insert(path.to_string(), content.to_string());
135        Some(Ok(()))
136    })
137    .flatten()
138}
139
140pub fn intercept_env_get(key: &str) -> Option<Option<String>> {
141    with_world(|w| {
142        if w.caps.environment {
143            return None;
144        }
145        Some(w.env.get(key).cloned())
146    })
147    .flatten()
148}
149
150pub fn intercept_now() -> Option<u64> {
151    with_world(|w| if w.caps.clock { None } else { Some(w.clock_ms) }).flatten()
152}
153
154/// A sealed `sleep` advances logical time INSTANTLY — the statement that turns
155/// a timeout test into a microsecond assertion. Returns true when handled.
156pub fn intercept_sleep(ms: u64) -> bool {
157    with_world(|w| {
158        if w.caps.clock {
159            return false;
160        }
161        w.clock_ms = w.clock_ms.saturating_add(ms);
162        true
163    })
164    .unwrap_or(false)
165}
166
167/// A uniform draw from `min..=max` off the world's seeded stream: a word at or above the largest
168/// multiple of the span is drawn again, so no value is likelier than another — the rule every real
169/// host's `randomInt` draws by.
170pub fn intercept_random_int(min: i64, max: i64) -> Option<i64> {
171    with_world(|w| {
172        if max <= min {
173            return min;
174        }
175        let span = max.wrapping_sub(min) as u64 as u128 + 1;
176        if span == 1 << 64 {
177            return w.next_u64() as i64;
178        }
179        let span = span as u64;
180        let biased_tail = (u64::MAX % span + 1) % span;
181        loop {
182            let r = w.next_u64();
183            if r <= u64::MAX - biased_tail {
184                return min.wrapping_add((r % span) as i64);
185            }
186        }
187    })
188}
189
190pub fn intercept_random_float() -> Option<f64> {
191    with_world(|w| (w.next_u64() >> 11) as f64 / (1u64 << 53) as f64)
192}