Skip to main content

harn_vm/testbench/
mod.rs

1//! Testbench: hermetic-execution composition primitive.
2//!
3//! Wires the four pluggable axes Harn already had — virtual time, mocked
4//! LLM, filesystem overlay, recorded subprocess — behind a single
5//! [`Testbench`] handle. Production wires real impls; tests/demos pick a
6//! config and get an audit trail of everything that crossed the host
7//! boundary.
8//!
9//! # Axes
10//!
11//! - **Clock** ([`crate::clock_mock`]). Pinned wall-clock + monotonic time
12//!   honored by stdlib `now_ms`/`sleep`/`monotonic_ms`, the trigger
13//!   dispatcher, and the cron scheduler. Tests advance with
14//!   [`crate::clock_mock::advance`] or the script-side `advance_time(...)`.
15//!
16//! - **LLM** ([`crate::llm`]). The CLI replay/record path
17//!   (`install_cli_llm_mocks` / `enable_cli_llm_mock_recording`) is the
18//!   workhorse; [`crate::llm::FakeLlmProvider`] adds streaming/error
19//!   fidelity for tests that care about per-token order.
20//!
21//! - **Filesystem** ([`overlay_fs`]). Copy-on-write overlay rooted at a
22//!   real worktree: reads pass through, writes land in an in-memory
23//!   layer, and [`overlay_fs::OverlayFs::diff`] surfaces a unified-style
24//!   diff that can be applied back or discarded.
25//!
26//! - **Subprocess** ([`process_tape`]). Records `(program, args, cwd) →
27//!   (stdout, stderr, exit, virtual Δt)` tuples in record mode and
28//!   replays them deterministically in replay mode. Env-var matching
29//!   is documented as future work — the JSON tape carries an `env`
30//!   field reserved for it.
31//!
32//! # Network
33//!
34//! Network egress is deny-by-default in testbench mode — outbound HTTP
35//! and connector requests fail fast unless an explicit allowlist names
36//! the destination. The deny pass routes through [`crate::egress`], the
37//! same policy engine production uses.
38
39pub mod annotations;
40pub mod fidelity;
41pub mod mcp_mock;
42pub mod overlay_fs;
43pub mod process_tape;
44pub mod tape;
45#[cfg(feature = "testbench-wasi")]
46pub mod wasi_process;
47
48use std::path::PathBuf;
49use std::sync::Arc;
50
51use crate::clock_mock::leak_audit::{self, ClockLeak};
52use crate::clock_mock::{install_override, ClockOverrideGuard, MockClock};
53use crate::egress::reset_egress_policy_for_host;
54
55use overlay_fs::{install_overlay, OverlayFs, OverlayFsGuard};
56use process_tape::{install_process_tape, ProcessTape, ProcessTapeGuard, ProcessTapeMode};
57use tape::{install_recorder, TapeHeader, TapeRecorder, TapeRecorderGuard};
58
59/// Declarative configuration for [`Testbench::activate`]. Every axis is
60/// optional so callers can compose only the surfaces they need.
61#[derive(Debug, Default, Clone)]
62pub struct Testbench {
63    pub clock: ClockConfig,
64    pub llm: LlmConfig,
65    pub filesystem: FilesystemConfig,
66    pub subprocess: SubprocessConfig,
67    pub network: NetworkConfig,
68    pub tape: TapeConfig,
69}
70
71/// Configures the unified mock clock. Defaults to the runtime's real
72/// clock so the testbench stays opt-in.
73#[derive(Debug, Default, Clone)]
74pub enum ClockConfig {
75    /// Leave the clock alone. Real wall-clock + monotonic time.
76    #[default]
77    Real,
78    /// Pin time to the given UNIX-epoch milliseconds. Honored by stdlib
79    /// `now_ms`/`sleep`, the trigger dispatcher, and cron.
80    Paused { starting_at_ms: i64 },
81}
82
83/// LLM provider configuration. Mirrors `harn run --llm-mock` /
84/// `--llm-mock-record` so the testbench is a strict superset of that
85/// flag pair. The testbench *does not* install LLM mocks itself — it
86/// stays declarative so [`crate::llm::install_cli_llm_mocks`] (or its
87/// `harn-cli` wrapper) remains the single mutator of LLM state.
88#[derive(Debug, Default, Clone)]
89pub enum LlmConfig {
90    /// No LLM substitution. Calls go through the configured provider.
91    #[default]
92    Real,
93    /// Replay scripted responses from a JSONL fixture.
94    Replay { fixture: PathBuf },
95    /// Capture executed responses into a JSONL fixture.
96    Record { fixture: PathBuf },
97}
98
99/// Filesystem overlay configuration.
100#[derive(Debug, Default, Clone)]
101pub enum FilesystemConfig {
102    /// No overlay. Reads and writes hit the real filesystem.
103    #[default]
104    Real,
105    /// Read-through, copy-on-write overlay rooted at `worktree`. Writes
106    /// stay in memory until the run ends, at which point the configured
107    /// emitter (CLI flag, in-process API) can read the diff.
108    Overlay { worktree: PathBuf },
109}
110
111/// Subprocess record/replay configuration.
112#[derive(Debug, Default, Clone)]
113pub enum SubprocessConfig {
114    /// No interception. Subprocesses spawn against the host OS.
115    #[default]
116    Real,
117    /// Record `(program, args, cwd)` tuples and their outputs into
118    /// `tape` so a follow-up run can replay them.
119    Record { tape: PathBuf },
120    /// Look every spawn up in `tape` and emit the recorded result. Errors
121    /// loudly when a tuple is not in the tape.
122    Replay { tape: PathBuf },
123    /// Resolve subprocess invocations against a directory of WASI
124    /// (`wasm32-wasi`) modules. Each `program` resolves to
125    /// `<dir>/<program>.wasm`; the module runs under wasmtime with the
126    /// testbench's mock clock virtualized into `clock_time_get` and
127    /// `poll_oneoff`. Calls whose program has no matching `.wasm` fall
128    /// through to the native spawn path. Requires the `testbench-wasi`
129    /// Cargo feature.
130    WasiToolchain { dir: PathBuf },
131}
132
133/// Network policy. Defaults to the production egress policy (no
134/// override). Testbench callers usually pick `DenyByDefault`.
135#[derive(Debug, Default, Clone)]
136pub enum NetworkConfig {
137    /// Use whatever egress policy the host has already installed.
138    #[default]
139    Real,
140    /// Deny outbound requests unless `allow` matches. Installs a typed
141    /// [`crate::egress`] policy that replaces any prior configuration —
142    /// including ambient `HARN_EGRESS_*` environment — for the session's
143    /// lifetime.
144    DenyByDefault {
145        /// Allow rules in the `HARN_EGRESS_ALLOW` syntax (e.g.
146        /// `"github.com"`, `"*.openai.com"`). Empty means deny everything.
147        allow: Vec<String>,
148    },
149}
150
151/// Unified-tape configuration. Recording is opt-in: `Off` (the default)
152/// installs nothing and pays nothing in production; `Emit { path }`
153/// installs a [`tape::TapeRecorder`] consulted by every host-capability
154/// axis, then persists the result to `path` (plus `path.cas/` for large
155/// payloads) when [`TestbenchSession::finalize`] runs.
156#[derive(Debug, Default, Clone)]
157pub enum TapeConfig {
158    #[default]
159    Off,
160    Emit {
161        path: PathBuf,
162        /// Argv forwarded to the script after `--`. Captured in the tape
163        /// header so two tapes that differ only in argv are
164        /// distinguishable.
165        argv: Vec<String>,
166        /// Path to the `.harn` script. Informational only; used to
167        /// populate the tape header so consumers can attribute records.
168        script_path: Option<String>,
169    },
170}
171
172impl Testbench {
173    /// Convenience: construct a builder.
174    pub fn builder() -> TestbenchBuilder {
175        TestbenchBuilder::default()
176    }
177
178    /// Activate every configured axis and return an RAII handle. Drop
179    /// the handle to restore the prior state.
180    pub fn activate(self) -> Result<TestbenchSession, TestbenchError> {
181        TestbenchSession::install(self)
182    }
183}
184
185/// Fluent constructor for [`Testbench`].
186#[derive(Debug, Default, Clone)]
187pub struct TestbenchBuilder {
188    bench: Testbench,
189}
190
191impl TestbenchBuilder {
192    pub fn paused_clock_at_ms(mut self, starting_at_ms: i64) -> Self {
193        self.bench.clock = ClockConfig::Paused { starting_at_ms };
194        self
195    }
196
197    pub fn replay_llm(mut self, fixture: impl Into<PathBuf>) -> Self {
198        self.bench.llm = LlmConfig::Replay {
199            fixture: fixture.into(),
200        };
201        self
202    }
203
204    pub fn record_llm(mut self, fixture: impl Into<PathBuf>) -> Self {
205        self.bench.llm = LlmConfig::Record {
206            fixture: fixture.into(),
207        };
208        self
209    }
210
211    pub fn fs_overlay(mut self, worktree: impl Into<PathBuf>) -> Self {
212        self.bench.filesystem = FilesystemConfig::Overlay {
213            worktree: worktree.into(),
214        };
215        self
216    }
217
218    pub fn record_subprocesses(mut self, tape: impl Into<PathBuf>) -> Self {
219        self.bench.subprocess = SubprocessConfig::Record { tape: tape.into() };
220        self
221    }
222
223    pub fn replay_subprocesses(mut self, tape: impl Into<PathBuf>) -> Self {
224        self.bench.subprocess = SubprocessConfig::Replay { tape: tape.into() };
225        self
226    }
227
228    /// Use a directory of WASI modules as the subprocess source. See
229    /// [`SubprocessConfig::WasiToolchain`].
230    pub fn wasi_toolchain(mut self, dir: impl Into<PathBuf>) -> Self {
231        self.bench.subprocess = SubprocessConfig::WasiToolchain { dir: dir.into() };
232        self
233    }
234
235    pub fn deny_network(mut self) -> Self {
236        self.bench.network = NetworkConfig::DenyByDefault { allow: Vec::new() };
237        self
238    }
239
240    pub fn allow_network(mut self, allow: impl IntoIterator<Item = String>) -> Self {
241        self.bench.network = NetworkConfig::DenyByDefault {
242            allow: allow.into_iter().collect(),
243        };
244        self
245    }
246
247    pub fn emit_tape(mut self, path: impl Into<PathBuf>) -> Self {
248        self.bench.tape = TapeConfig::Emit {
249            path: path.into(),
250            argv: Vec::new(),
251            script_path: None,
252        };
253        self
254    }
255
256    pub fn emit_tape_for(
257        mut self,
258        path: impl Into<PathBuf>,
259        script_path: Option<String>,
260        argv: Vec<String>,
261    ) -> Self {
262        self.bench.tape = TapeConfig::Emit {
263            path: path.into(),
264            argv,
265            script_path,
266        };
267        self
268    }
269
270    pub fn build(self) -> Testbench {
271        self.bench
272    }
273}
274
275/// RAII handle returned by [`Testbench::activate`]. Holds every guard
276/// for the active axes; dropping it tears them all down in order.
277#[must_use = "the testbench tears down on drop; bind the handle to a `_session` local"]
278pub struct TestbenchSession {
279    _clock_leak_scope: Option<leak_audit::ClockLeakScopeGuard>,
280    _clock: Option<ClockOverrideGuard>,
281    _process: Option<ProcessTapeGuard>,
282    _overlay: Option<OverlayFsGuard>,
283    _recorder: Option<TapeRecorderGuard>,
284    process_tape: Option<Arc<ProcessTape>>,
285    overlay: Option<Arc<OverlayFs>>,
286    recorder: Option<Arc<TapeRecorder>>,
287    tape_path: Option<PathBuf>,
288    tape_started_at_unix_ms: Option<i64>,
289    tape_script_path: Option<String>,
290    tape_argv: Vec<String>,
291    subprocess_mode: ProcessTapeMode,
292    subprocess_tape_path: Option<PathBuf>,
293    #[cfg(feature = "testbench-wasi")]
294    _wasi_toolchain: Option<wasi_process::WasiToolchainGuard>,
295    /// Whether this session installed a deny-by-default egress policy that
296    /// must be torn down on drop.
297    egress_policy_installed: bool,
298}
299
300impl TestbenchSession {
301    fn install(bench: Testbench) -> Result<Self, TestbenchError> {
302        let (clock_leak_scope, clock_guard, started_at_unix_ms) = match bench.clock {
303            ClockConfig::Real => (None, None, None),
304            ClockConfig::Paused { starting_at_ms } => (
305                Some(leak_audit::install_scope()),
306                Some(install_override(MockClock::at_wall_ms(starting_at_ms))),
307                Some(starting_at_ms),
308            ),
309        };
310
311        // LLM state is *not* installed here — the caller owns the
312        // CliLlmMockMode channel. Reading bench.llm just keeps the
313        // declarative config visible to test inspection.
314        #[allow(clippy::no_effect_underscore_binding)]
315        let _llm_config = bench.llm;
316
317        #[cfg(feature = "testbench-wasi")]
318        let mut wasi_guard: Option<wasi_process::WasiToolchainGuard> = None;
319
320        let (process_tape, process_guard, subprocess_mode, subprocess_tape_path) =
321            match bench.subprocess {
322                SubprocessConfig::Real => (None, None, ProcessTapeMode::Replay, None),
323                SubprocessConfig::Record { tape } => {
324                    let active = Arc::new(ProcessTape::recording());
325                    let guard = install_process_tape(Arc::clone(&active));
326                    (
327                        Some(Arc::clone(&active)),
328                        Some(guard),
329                        ProcessTapeMode::Record,
330                        Some(tape),
331                    )
332                }
333                SubprocessConfig::Replay { tape } => {
334                    let loaded = ProcessTape::load(&tape).map_err(TestbenchError::Subprocess)?;
335                    let active = Arc::new(loaded);
336                    let guard = install_process_tape(Arc::clone(&active));
337                    (
338                        Some(Arc::clone(&active)),
339                        Some(guard),
340                        ProcessTapeMode::Replay,
341                        Some(tape),
342                    )
343                }
344                #[cfg(feature = "testbench-wasi")]
345                SubprocessConfig::WasiToolchain { dir } => {
346                    if !dir.exists() {
347                        return Err(TestbenchError::Subprocess(format!(
348                            "wasi toolchain directory does not exist: {}",
349                            dir.display()
350                        )));
351                    }
352                    wasi_guard = Some(wasi_process::install_wasi_toolchain(dir));
353                    (None, None, ProcessTapeMode::Replay, None)
354                }
355                #[cfg(not(feature = "testbench-wasi"))]
356                SubprocessConfig::WasiToolchain { .. } => {
357                    return Err(TestbenchError::Subprocess(
358                        "WasiToolchain requires the `testbench-wasi` Cargo feature".to_string(),
359                    ));
360                }
361            };
362
363        let (overlay, overlay_guard) = match bench.filesystem {
364            FilesystemConfig::Real => (None, None),
365            FilesystemConfig::Overlay { worktree } => {
366                let overlay = Arc::new(OverlayFs::rooted_at(worktree));
367                let guard = install_overlay(Arc::clone(&overlay));
368                (Some(overlay), Some(guard))
369            }
370        };
371
372        let egress_policy_installed = match bench.network {
373            NetworkConfig::Real => false,
374            NetworkConfig::DenyByDefault { allow } => {
375                crate::egress::install_deny_by_default_policy(&allow)
376                    .map_err(|error| TestbenchError::Network(error.to_string()))?;
377                true
378            }
379        };
380
381        let (recorder, recorder_guard, tape_path, tape_argv, tape_script_path) = match bench.tape {
382            TapeConfig::Off => (None, None, None, Vec::new(), None),
383            TapeConfig::Emit {
384                path,
385                argv,
386                script_path,
387            } => {
388                let recorder = Arc::new(TapeRecorder::new());
389                let guard = install_recorder(Arc::clone(&recorder));
390                (
391                    Some(Arc::clone(&recorder)),
392                    Some(guard),
393                    Some(path),
394                    argv,
395                    script_path,
396                )
397            }
398        };
399
400        Ok(Self {
401            _clock_leak_scope: clock_leak_scope,
402            _clock: clock_guard,
403            _process: process_guard,
404            _overlay: overlay_guard,
405            _recorder: recorder_guard,
406            process_tape,
407            overlay,
408            recorder,
409            tape_path,
410            tape_started_at_unix_ms: started_at_unix_ms,
411            tape_script_path,
412            tape_argv,
413            subprocess_mode,
414            subprocess_tape_path,
415            #[cfg(feature = "testbench-wasi")]
416            _wasi_toolchain: wasi_guard,
417            egress_policy_installed,
418        })
419    }
420
421    /// Whether subprocess interception is recording new entries.
422    pub fn subprocess_mode(&self) -> ProcessTapeMode {
423        self.subprocess_mode
424    }
425
426    /// Path that recorded subprocess tape entries should land in, or
427    /// where replay loaded them from.
428    pub fn subprocess_tape_path(&self) -> Option<&std::path::Path> {
429        self.subprocess_tape_path.as_deref()
430    }
431
432    /// Reference to the active filesystem overlay (if any).
433    pub fn overlay(&self) -> Option<&Arc<OverlayFs>> {
434        self.overlay.as_ref()
435    }
436
437    /// Reference to the active process tape (if any).
438    pub fn process_tape(&self) -> Option<&Arc<ProcessTape>> {
439        self.process_tape.as_ref()
440    }
441
442    /// Reference to the active tape recorder (if any).
443    pub fn tape_recorder(&self) -> Option<&Arc<TapeRecorder>> {
444        self.recorder.as_ref()
445    }
446
447    /// Persist the recorded subprocess tape (if recording) and return
448    /// the filesystem diff (if an overlay is active). Tearing down the
449    /// session via [`Drop`] will not persist; call this explicitly to
450    /// flush.
451    pub fn finalize(self) -> Result<TestbenchFinalize, TestbenchError> {
452        let diff = self
453            .overlay
454            .as_ref()
455            .map(|overlay| overlay.diff())
456            .unwrap_or_default();
457        let recorded = if matches!(self.subprocess_mode, ProcessTapeMode::Record) {
458            if let (Some(tape), Some(path)) = (
459                self.process_tape.as_ref(),
460                self.subprocess_tape_path.as_ref(),
461            ) {
462                tape.persist(path).map_err(TestbenchError::Subprocess)?;
463            }
464            self.process_tape
465                .as_ref()
466                .map(|tape| tape.recorded())
467                .unwrap_or_default()
468        } else {
469            Vec::new()
470        };
471        let mut emitted_tape = None;
472        if let (Some(recorder), Some(path)) = (self.recorder.as_ref(), self.tape_path.as_ref()) {
473            let header = TapeHeader::current(
474                self.tape_started_at_unix_ms,
475                self.tape_script_path.clone(),
476                self.tape_argv.clone(),
477            );
478            let tape = recorder.snapshot(header);
479            tape.persist(path).map_err(TestbenchError::Tape)?;
480            emitted_tape = Some(EmittedTape {
481                path: path.clone(),
482                records: tape.records.len(),
483            });
484        }
485        // Drain the leak audit last so anything emitted while we
486        // serialized other artifacts (e.g. tape persistence reading the
487        // wall clock for timestamps it shouldn't be reading) is still
488        // captured in this session's report.
489        let clock_leaks = leak_audit::drain();
490        // The Drop impl undoes mocks regardless of finalize success.
491        Ok(TestbenchFinalize {
492            fs_diff: diff,
493            recorded_subprocesses: recorded,
494            tape: emitted_tape,
495            clock_leaks,
496        })
497    }
498}
499
500impl Drop for TestbenchSession {
501    fn drop(&mut self) {
502        if self.egress_policy_installed {
503            reset_egress_policy_for_host();
504        }
505        // The remaining `_clock`/`_overlay`/`_process` guards drop in
506        // field-declared order, restoring the prior thread-local state.
507    }
508}
509
510/// Outcome of a finalized testbench session — the artifacts the operator
511/// inspects after a hermetic run.
512#[derive(Debug, Default, Clone)]
513pub struct TestbenchFinalize {
514    pub fs_diff: Vec<overlay_fs::DiffEntry>,
515    pub recorded_subprocesses: Vec<process_tape::TapeEntry>,
516    pub tape: Option<EmittedTape>,
517    /// Capabilities that observed real wall-clock or monotonic time
518    /// during the session. Empty under a hermetic run; non-empty entries
519    /// are fidelity hazards the operator should investigate or migrate
520    /// off of direct host-clock reads.
521    pub clock_leaks: Vec<ClockLeak>,
522}
523
524/// Summary metadata for a unified tape that was emitted at finalize-time.
525#[derive(Debug, Clone)]
526pub struct EmittedTape {
527    pub path: PathBuf,
528    pub records: usize,
529}
530
531/// Errors surfaced when activating or finalizing a testbench session.
532#[derive(Debug)]
533pub enum TestbenchError {
534    Subprocess(String),
535    Tape(String),
536    Network(String),
537}
538
539impl std::fmt::Display for TestbenchError {
540    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
541        match self {
542            Self::Subprocess(msg) => write!(f, "testbench subprocess: {msg}"),
543            Self::Tape(msg) => write!(f, "testbench tape: {msg}"),
544            Self::Network(msg) => write!(f, "testbench network: {msg}"),
545        }
546    }
547}
548
549impl std::error::Error for TestbenchError {}
550
551#[cfg(test)]
552mod tests {
553    use super::*;
554    use std::sync::Mutex;
555
556    static ENV_TEST_LOCK: Mutex<()> = Mutex::new(());
557
558    /// Some tests in this module mutate process-global env vars. Keep
559    /// those serialized without coupling them to the clock-leak audit,
560    /// which is session-scoped.
561    fn serial<F: FnOnce()>(body: F) {
562        let _guard = ENV_TEST_LOCK.lock().unwrap_or_else(|p| p.into_inner());
563        body();
564    }
565
566    #[test]
567    fn paused_clock_pins_now_ms_for_session_lifetime() {
568        serial(|| {
569            let bench = Testbench::builder()
570                .paused_clock_at_ms(1_700_000_000_000)
571                .build();
572            let session = bench.activate().expect("activate");
573            assert_eq!(crate::clock_mock::now_ms(), 1_700_000_000_000);
574            crate::clock_mock::advance(std::time::Duration::from_mins(1));
575            assert_eq!(crate::clock_mock::now_ms(), 1_700_000_060_000);
576            drop(session);
577            // After drop the override is gone; no assertion on real time.
578            assert!(!crate::clock_mock::is_mocked());
579        });
580    }
581
582    #[test]
583    fn deny_by_default_blocks_egress_until_drop() {
584        serial(|| {
585            let _env = crate::egress::test_env_guard();
586            let bench = Testbench::builder().deny_network().build();
587            let session = bench.activate().expect("activate");
588            assert!(
589                crate::egress::check_url("testbench", "https://example.com/x")
590                    .expect("policy check")
591                    .is_some()
592            );
593            drop(session);
594            assert!(
595                crate::egress::check_url("testbench", "https://example.com/x")
596                    .expect("policy check")
597                    .is_none()
598            );
599        });
600    }
601
602    #[test]
603    fn finalize_surfaces_clock_leaks_for_contrived_capability() {
604        serial(|| {
605            let bench = Testbench::builder()
606                .paused_clock_at_ms(1_700_000_000_000)
607                .build();
608            let session = bench.activate().expect("activate");
609
610            // Contrived "leaky" capability: routes through the audit shim
611            // while a paused mock is installed. Production callers (e.g.
612            // `stdlib/date_iso`) follow the exact same pattern.
613            let _ = leak_audit::wall_now("test/contrived_leak");
614            let _ = leak_audit::instant_now("test/contrived_instant");
615            let _ = leak_audit::wall_now("test/contrived_leak");
616
617            let finalize = session.finalize().expect("finalize");
618            let by_id: std::collections::BTreeMap<&str, &ClockLeak> = finalize
619                .clock_leaks
620                .iter()
621                .map(|leak| (leak.capability_id.as_str(), leak))
622                .collect();
623            let wall = by_id
624                .get("test/contrived_leak")
625                .expect("wall leak surfaced");
626            assert_eq!(wall.count, 2);
627            let inst = by_id
628                .get("test/contrived_instant")
629                .expect("instant leak surfaced");
630            assert_eq!(inst.count, 1);
631
632            // Drain semantics: a fresh session sees no carry-over.
633            let next_session = Testbench::builder()
634                .paused_clock_at_ms(1_700_000_000_000)
635                .build()
636                .activate()
637                .expect("activate next");
638            let next = next_session.finalize().expect("finalize next");
639            assert!(next.clock_leaks.is_empty());
640        });
641    }
642
643    #[test]
644    fn audit_quiet_when_no_mock_is_active() {
645        serial(|| {
646            leak_audit::reset();
647            // No `Testbench` activated → no mock clock → no leak entries
648            // even when the helpers are called.
649            let _ = leak_audit::wall_now("test/no_mock");
650            let _ = leak_audit::instant_now("test/no_mock");
651            assert!(leak_audit::snapshot().is_empty());
652        });
653    }
654}