Skip to main content

harn_test_runner/
session.rs

1use std::collections::BTreeSet;
2use std::path::PathBuf;
3use std::sync::Mutex;
4
5use serde::Serialize;
6
7use crate::reporting::SuiteModulePreparation;
8
9/// Reusable runtime state for repeated user-test runs.
10///
11/// Logical workers share one bounded cache of immutable prepared bytecode while
12/// every test case still receives a fresh VM and fresh runtime state. Sharing
13/// lets the suite prepare its import graph once before per-test clocks start,
14/// instead of making one arbitrary test on every worker pay the cold compile.
15pub struct TestRunSession {
16    prepared_module_cache: harn_vm::PreparedModuleCache,
17    workers: Mutex<usize>,
18    clock: std::sync::Arc<dyn harn_vm::clock::Clock>,
19    stdio_available: bool,
20    callable_preparations: Mutex<(usize, usize)>,
21}
22
23impl Default for TestRunSession {
24    fn default() -> Self {
25        Self {
26            prepared_module_cache: harn_vm::PreparedModuleCache::default(),
27            workers: Mutex::new(0),
28            clock: harn_vm::clock::RealClock::arc(),
29            stdio_available: true,
30            callable_preparations: Mutex::new((0, 0)),
31        }
32    }
33}
34
35/// Aggregate prepared-module cache counters for a [`TestRunSession`].
36///
37/// This shape is intentionally exhaustive: it is embedded in the versioned
38/// test-worker receipt, so adding a counter must update that protocol and its
39/// schema regression instead of silently extending an internal snapshot.
40#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize)]
41pub struct TestRunSessionStats {
42    pub workers: usize,
43    pub hits: u64,
44    pub misses: u64,
45    pub insertions: u64,
46    pub evictions: u64,
47    pub entries: usize,
48    pub test_files_compiled: usize,
49    pub test_entries_compiled: usize,
50}
51
52impl TestRunSession {
53    /// Build a session for an embedder whose control protocol owns stdio.
54    pub fn without_stdio() -> Self {
55        Self {
56            stdio_available: false,
57            ..Self::default()
58        }
59    }
60
61    pub fn stats(&self) -> TestRunSessionStats {
62        let stats = self.prepared_module_cache.stats();
63        let callable = *self.callable_preparations.lock().unwrap();
64        TestRunSessionStats {
65            workers: *self.workers.lock().unwrap(),
66            hits: stats.hits,
67            misses: stats.misses,
68            insertions: stats.insertions,
69            evictions: stats.evictions,
70            entries: stats.entries,
71            test_files_compiled: callable.0,
72            test_entries_compiled: callable.1,
73        }
74    }
75
76    #[doc(hidden)]
77    pub fn prepared_module_cache(&self, worker_index: usize) -> harn_vm::PreparedModuleCache {
78        let mut workers = self.workers.lock().unwrap();
79        *workers = (*workers).max(worker_index.saturating_add(1));
80        self.prepared_module_cache.clone()
81    }
82
83    #[doc(hidden)]
84    pub fn prepare_import_graphs(
85        &self,
86        roots: impl IntoIterator<Item = (PathBuf, bool)>,
87    ) -> SuiteModulePreparation {
88        let mut user_files = BTreeSet::new();
89        let mut trusted_files = BTreeSet::new();
90        for (path, trusted_host_dispatch) in roots {
91            if trusted_host_dispatch {
92                trusted_files.insert(path);
93            } else {
94                user_files.insert(path);
95            }
96        }
97        let user = self.prepare_import_graph(&user_files.into_iter().collect::<Vec<_>>(), false);
98        let trusted =
99            self.prepare_import_graph(&trusted_files.into_iter().collect::<Vec<_>>(), true);
100        SuiteModulePreparation {
101            duration_ms: user.duration_ms.saturating_add(trusted.duration_ms),
102            modules: user.modules.saturating_add(trusted.modules),
103        }
104    }
105
106    fn prepare_import_graph(
107        &self,
108        roots: &[std::path::PathBuf],
109        trusted_host_dispatch: bool,
110    ) -> SuiteModulePreparation {
111        let started_ms = self.clock.monotonic_ms();
112        let modules = if trusted_host_dispatch {
113            self.prepared_module_cache
114                .prepare_trusted_host_dispatch_import_graph(roots)
115        } else {
116            self.prepared_module_cache.prepare_import_graph(roots)
117        };
118        let duration_ms = self.clock.monotonic_ms().saturating_sub(started_ms).max(0) as u64;
119        SuiteModulePreparation {
120            duration_ms,
121            modules,
122        }
123    }
124
125    #[doc(hidden)]
126    pub fn record_callable_preparation(&self, files: usize, entries: usize) {
127        let mut totals = self.callable_preparations.lock().unwrap();
128        totals.0 = totals.0.saturating_add(files);
129        totals.1 = totals.1.saturating_add(entries);
130    }
131
132    #[doc(hidden)]
133    pub fn stdio_available(&self) -> bool {
134        self.stdio_available
135    }
136}