1use std::cell::RefCell;
2use std::rc::Rc;
3
4use runmat_test::context::{TestCommand, TestExecutionContext};
5use runmat_test::descriptor::{ProcedureDescriptor, ProcedureKind, SourceDescriptor, SourceSpan};
6use runmat_test::lifecycle::{ExecutionPhase, FixtureScopeKey};
7use runmat_test::protocol::ProtocolLimits;
8
9#[derive(Clone, Debug)]
10pub struct ActiveTestContext {
11 pub execution: TestExecutionContext,
12 pub phase: ExecutionPhase,
13 pub scope: FixtureScopeKey,
14}
15
16#[derive(Clone, Debug)]
17pub struct RuntimeTeardownInvocation {
18 pub semantic_path: String,
19 pub callback: runmat_value::Value,
20 pub arguments: Vec<runmat_value::Value>,
21}
22
23#[derive(Debug)]
24pub(crate) struct ContextState {
25 active: ActiveTestContext,
26 limits: ProtocolLimits,
27 commands: Vec<TestCommand>,
28 runtime_teardowns: Vec<RuntimeTeardownInvocation>,
29}
30
31#[derive(Clone, Debug)]
32pub struct TestContextHandle {
33 state: Rc<RefCell<ContextState>>,
34}
35
36impl TestContextHandle {
37 pub fn active(&self) -> ActiveTestContext {
38 self.state.borrow().active.clone()
39 }
40
41 pub fn commands(&self) -> Vec<TestCommand> {
42 self.state.borrow().commands.clone()
43 }
44
45 pub fn take_commands(&self) -> Vec<TestCommand> {
46 std::mem::take(&mut self.state.borrow_mut().commands)
47 }
48
49 pub fn runtime_teardowns(&self) -> Vec<RuntimeTeardownInvocation> {
50 self.state.borrow().runtime_teardowns.clone()
51 }
52
53 pub fn take_runtime_teardowns(&self) -> Vec<RuntimeTeardownInvocation> {
54 std::mem::take(&mut self.state.borrow_mut().runtime_teardowns)
55 }
56}
57
58#[derive(Debug)]
59pub struct TestContextGuard {
60 state: Rc<RefCell<ContextState>>,
61 context: Option<Rc<crate::context::RuntimeContextState>>,
62}
63
64impl TestContextGuard {
65 pub fn handle(&self) -> TestContextHandle {
66 TestContextHandle {
67 state: Rc::clone(&self.state),
68 }
69 }
70}
71
72impl Drop for TestContextGuard {
73 fn drop(&mut self) {
74 if let Some(context) = &self.context {
75 let popped = context.test_contexts.borrow_mut().pop();
76 debug_assert!(popped
77 .as_ref()
78 .is_some_and(|state| Rc::ptr_eq(state, &self.state)));
79 } else {
80 TEST_CONTEXT_STACK.with(|stack| {
81 let popped = stack.borrow_mut().pop();
82 debug_assert!(popped
83 .as_ref()
84 .is_some_and(|state| Rc::ptr_eq(state, &self.state)));
85 });
86 }
87 }
88}
89
90thread_local! {
91 static TEST_CONTEXT_STACK: RefCell<Vec<Rc<RefCell<ContextState>>>> =
92 const { RefCell::new(Vec::new()) };
93}
94
95pub fn install_test_context(active: ActiveTestContext, limits: ProtocolLimits) -> TestContextGuard {
96 let context = crate::context::legacy::active().map(|context| Rc::clone(context.state()));
97 install_test_context_for_state(context, active, limits)
98}
99
100pub fn install_test_context_in(
106 context: &crate::context::RuntimeContext,
107 active: ActiveTestContext,
108 limits: ProtocolLimits,
109) -> TestContextGuard {
110 install_test_context_for_state(Some(Rc::clone(context.state())), active, limits)
111}
112
113fn install_test_context_for_state(
114 context: Option<Rc<crate::context::RuntimeContextState>>,
115 active: ActiveTestContext,
116 limits: ProtocolLimits,
117) -> TestContextGuard {
118 let state = Rc::new(RefCell::new(ContextState {
119 active,
120 limits,
121 commands: Vec::new(),
122 runtime_teardowns: Vec::new(),
123 }));
124 if let Some(context) = &context {
125 context.test_contexts.borrow_mut().push(Rc::clone(&state));
126 } else {
127 TEST_CONTEXT_STACK.with(|stack| stack.borrow_mut().push(Rc::clone(&state)));
128 }
129 TestContextGuard { state, context }
130}
131
132fn current_test_context_state() -> Option<Rc<RefCell<ContextState>>> {
133 if let Some(context) = crate::context::legacy::active() {
134 return context.state().test_contexts.borrow().last().cloned();
135 }
136 TEST_CONTEXT_STACK.with(|stack| stack.borrow().last().cloned())
137}
138
139pub fn record_runtime_teardown(
140 callback: runmat_value::Value,
141 arguments: Vec<runmat_value::Value>,
142) -> Result<(), &'static str> {
143 {
144 let state = current_test_context_state()
145 .ok_or("addTeardown requires an active test lifecycle context")?;
146 let mut state = state.borrow_mut();
147 if state.active.phase.is_teardown() {
148 return Err("addTeardown cannot be registered from a teardown phase");
149 }
150 if state.commands.len() >= state.limits.max_commands_per_invocation as usize {
151 return Err("testing command limit exceeded");
152 }
153 let callback_name = match &callback {
154 runmat_value::Value::FunctionHandle(name)
155 | runmat_value::Value::ExternalFunctionHandle(name)
156 | runmat_value::Value::MethodFunctionHandle(name) => name.clone(),
157 runmat_value::Value::BoundFunctionHandle { name, .. } => name.clone(),
158 runmat_value::Value::Closure(closure) => closure.function_name.clone(),
159 _ => return Err("addTeardown callback must be a function handle"),
160 };
161 let semantic_path = format!(
162 "runtime-teardown:{}:{}:{}",
163 state.active.execution.test_id.as_str(),
164 state.active.execution.attempt,
165 state.runtime_teardowns.len()
166 );
167 let source = crate::source_context::current_source_info();
168 let relative_path = source
169 .as_ref()
170 .map(|source| {
171 source
172 .fullpath_name
173 .as_deref()
174 .unwrap_or(source.name.as_ref())
175 .to_string()
176 })
177 .unwrap_or_default();
178 let procedure = ProcedureDescriptor {
179 semantic_path: semantic_path.clone(),
180 display_name: callback_name,
181 kind: ProcedureKind::Teardown,
182 source: SourceDescriptor {
183 owner_identity: "runtime:test-context".into(),
184 semantic_path: semantic_path.clone(),
185 relative_path,
186 span: SourceSpan {
187 start_byte: 0,
188 end_byte: 0,
189 start_line: 1,
190 start_column: 1,
191 end_line: 1,
192 end_column: 1,
193 },
194 },
195 };
196 let scope = state.active.scope.clone();
197 state
198 .commands
199 .push(TestCommand::AddTeardown { scope, procedure });
200 state.runtime_teardowns.push(RuntimeTeardownInvocation {
201 semantic_path,
202 callback,
203 arguments,
204 });
205 Ok(())
206 }
207}
208
209pub fn active_test_context() -> Option<ActiveTestContext> {
210 current_test_context_state().map(|state| state.borrow().active.clone())
211}
212
213pub fn record_test_command(command: TestCommand) -> Result<(), &'static str> {
214 {
215 let state = current_test_context_state()
216 .ok_or("testing command requires an active test lifecycle context")?;
217 let mut state = state.borrow_mut();
218 if state.commands.len() >= state.limits.max_commands_per_invocation as usize {
219 return Err("testing command limit exceeded");
220 }
221 state.commands.push(command);
222 Ok(())
223 }
224}
225
226#[cfg(test)]
227mod tests {
228 use runmat_test::descriptor::FixtureScope;
229 use runmat_test::identity::{RunId, TestId};
230 use runmat_test::lifecycle::QualificationKind;
231 use runmat_test::result::{Diagnostic, DiagnosticSeverity};
232
233 use super::*;
234
235 #[test]
236 fn scoped_context_records_bounded_domain_commands() {
237 let guard = install_test_context(
238 ActiveTestContext {
239 execution: TestExecutionContext {
240 run_id: RunId::derive("revision", "run"),
241 test_id: TestId::derive(&runmat_test::identity::TestIdentityInput {
242 owner_identity: "owner",
243 relative_source_identity: "test.m",
244 semantic_scheme: "function",
245 semantic_item_path: "test",
246 parameter_identity: "",
247 fixture_identity: "",
248 }),
249 attempt: 1,
250 random_seed: 7,
251 },
252 phase: ExecutionPhase::TestBody,
253 scope: FixtureScopeKey {
254 scope: FixtureScope::Test,
255 identity: "test".into(),
256 },
257 },
258 ProtocolLimits::default(),
259 );
260 record_test_command(TestCommand::Qualify {
261 qualification: QualificationKind::VerificationFailed,
262 diagnostic: Diagnostic {
263 identifier: "RunMat:VerificationFailed".into(),
264 message: "nope".into(),
265 severity: DiagnosticSeverity::Error,
266 phase: ExecutionPhase::TestBody,
267 source: None,
268 details: Vec::new(),
269 },
270 })
271 .unwrap();
272 assert_eq!(guard.handle().commands().len(), 1);
273 drop(guard);
274 assert!(active_test_context().is_none());
275 }
276}