1use crate::cell_diff::{CellDiff, compare_row, compare_table};
8use crate::diagnostics::Diagnostic;
9use crate::doc_string_diff::compare_doc_string;
10use crate::error::{FailureLocation, HandlerError, StepError, StepFailure};
11use crate::failure_anchor;
12use crate::handler::{Handler, StepOutput, StepReturn};
13use crate::offsets::{utf16_len, utf16_slice};
14use crate::param_diff::compare_params_with_formats;
15use crate::plan::{ExecutionPlan, PlannedExample, PlannedStep};
16use crate::step_kind::StepKind;
17use crate::value::Value;
18use std::any::Any;
19use std::cell::Cell;
20use std::collections::HashMap;
21use std::future::Future;
22use std::panic::AssertUnwindSafe;
23use std::pin::Pin;
24use std::rc::Rc;
25use std::sync::Once;
26use std::task::{Context, Poll, Wake, Waker};
27
28#[derive(Clone, Copy, Debug, PartialEq, Eq)]
30pub enum StepOutcome {
31 Pass,
32 Fail,
33 Skipped,
34}
35
36impl StepOutcome {
37 pub fn as_str(self) -> &'static str {
39 match self {
40 StepOutcome::Pass => "pass",
41 StepOutcome::Fail => "fail",
42 StepOutcome::Skipped => "skipped",
43 }
44 }
45}
46
47#[derive(Clone, Debug, PartialEq)]
49pub struct StepObservation {
50 pub example_index: usize,
51 pub ordinal: usize,
52 pub outcome: StepOutcome,
53 pub error: Option<StepFailure>,
54}
55
56pub struct ExecutePorts<'a> {
61 pub reporter: Reporter<'a>,
62 pub create_context: Option<ContextFactory<'a>>,
63 pub observer: Option<Observer<'a>>,
64}
65
66pub type Reporter<'a> = Box<dyn Fn(&Diagnostic) + 'a>;
68pub type ContextFactory<'a> = Box<dyn Fn(&str) -> Rc<dyn Any> + 'a>;
70pub type Observer<'a> = Box<dyn Fn(StepObservation) + 'a>;
72
73impl<'a> ExecutePorts<'a> {
74 pub fn new(reporter: Box<dyn Fn(&Diagnostic) + 'a>) -> ExecutePorts<'a> {
76 ExecutePorts {
77 reporter,
78 create_context: None,
79 observer: None,
80 }
81 }
82}
83
84impl ExecutePorts<'static> {
85 pub fn silent() -> ExecutePorts<'static> {
87 ExecutePorts::new(Box::new(|_| {}))
88 }
89}
90
91pub struct QueuedExample<'a> {
93 pub name: String,
94 run: Box<dyn Fn() -> Result<(), StepFailure> + 'a>,
95}
96
97impl QueuedExample<'_> {
98 pub fn run(&self) -> Result<(), StepFailure> {
100 (self.run)()
101 }
102}
103
104pub fn collect_examples<'a>(
107 plan: &'a ExecutionPlan,
108 ports: &'a ExecutePorts<'a>,
109) -> Vec<QueuedExample<'a>> {
110 for d in &plan.diagnostics {
111 (ports.reporter)(d);
112 }
113 plan.examples
114 .iter()
115 .enumerate()
116 .map(|(i, ex)| QueuedExample {
117 name: ex.name.clone(),
118 run: Box::new(move || run_example(plan, ex, i, ports)),
119 })
120 .collect()
121}
122
123pub fn execute_plan<'a>(
126 plan: &'a ExecutionPlan,
127 ports: &'a ExecutePorts<'a>,
128) -> Result<(), StepFailure> {
129 for q in collect_examples(plan, ports) {
130 q.run()?;
131 }
132 Ok(())
133}
134
135fn run_example(
140 plan: &ExecutionPlan,
141 ex: &PlannedExample,
142 example_index: usize,
143 ports: &ExecutePorts,
144) -> Result<(), StepFailure> {
145 let path = &plan.var_doc.path;
146 let source = &plan.var_doc.source;
147 let steps = &ex.steps;
148
149 let mut state_by_file: HashMap<String, Rc<dyn Any>> = HashMap::new();
150 let mut last_return: Option<Value> = None;
151 let mut thrown: Option<StepFailure> = None;
152
153 for (i, step) in steps.iter().enumerate() {
154 let file = &step.step_def.expression_source_file;
155 let state = match state_by_file.get(file) {
156 Some(s) => s.clone(),
157 None => {
158 let created = create_context(ports, file);
159 state_by_file.insert(file.clone(), created.clone());
160 created
161 }
162 };
163
164 let mut call_args = step.args.clone();
166 if let Some(table) = &step.data_table {
167 call_args.push(table_rows(table));
168 } else if let Some(fence) = &step.doc_string {
169 call_args.push(Value::from(fence.body.as_str()));
170 }
171
172 let step_error: Option<StepError> =
173 match invoke_resolve(&step.step_def.handler, state, call_args) {
174 Err(he) => Some(StepError::Handler(he)),
175 Ok(output) => {
176 last_return = output.compared().cloned();
177 match step.step_def.kind {
178 Some(StepKind::Stimulus) => {
179 let next: Option<Rc<dyn Any>> = match output {
186 StepOutput::State(next) => Some(next),
187 StepOutput::Compared(v) => v.map(|v| Rc::new(v) as Rc<dyn Any>),
188 };
189 if let Some(next) = next {
190 state_by_file.insert(file.clone(), next);
191 }
192 None
193 }
194 Some(StepKind::Sensor) => {
195 if ex.row_checks.is_none() {
197 check_sensor_return(source, step, output.compared().cloned()).err()
198 } else {
199 None
200 }
201 }
202 None => Some(StepError::ReturnShape("unknown step kind: null".to_string())),
203 }
204 }
205 };
206
207 match step_error {
208 None => observe(
209 ports,
210 StepObservation {
211 example_index,
212 ordinal: i + 1,
213 outcome: StepOutcome::Pass,
214 error: None,
215 },
216 ),
217 Some(err) => {
218 let failure = attach_location(err, step, path);
219 observe(
220 ports,
221 StepObservation {
222 example_index,
223 ordinal: i + 1,
224 outcome: StepOutcome::Fail,
225 error: Some(failure.clone()),
226 },
227 );
228 thrown = Some(failure);
229 break;
230 }
231 }
232 }
233
234 if thrown.is_none() {
236 if let Some(checks) = &ex.row_checks {
237 if !checks.is_empty() {
238 let bad: Vec<CellDiff> = compare_row(last_return.as_ref(), checks)
239 .into_iter()
240 .filter(|d| !d.ok)
241 .collect();
242 if !bad.is_empty() {
243 let last_step = steps.last().unwrap();
244 let failure = attach_location(StepError::CellMismatch(bad), last_step, path);
245 observe(
246 ports,
247 StepObservation {
248 example_index,
249 ordinal: steps.len(),
250 outcome: StepOutcome::Fail,
251 error: Some(failure.clone()),
252 },
253 );
254 thrown = Some(failure);
255 }
256 }
257 }
258 }
259
260 if ex.expected_outcome.as_deref() == Some("fail") {
262 match thrown {
263 None => {
264 return Err(match steps.last() {
265 Some(last) => attach_location(StepError::UnexpectedPass, last, path),
266 None => StepFailure::bare(StepError::UnexpectedPass),
267 });
268 }
269 Some(failure) => {
270 if let Some(expected_msg) = &ex.expected_error_message {
271 if !failure.error.message().contains(expected_msg) {
272 return Err(failure);
273 }
274 }
275 return Ok(());
276 }
277 }
278 }
279
280 match thrown {
281 Some(failure) => Err(failure),
282 None => Ok(()),
283 }
284}
285
286fn create_context(ports: &ExecutePorts, file: &str) -> Rc<dyn Any> {
287 match &ports.create_context {
288 Some(cc) => cc(file),
289 None => Rc::new(()) as Rc<dyn Any>,
290 }
291}
292
293fn observe(ports: &ExecutePorts, observation: StepObservation) {
294 if let Some(observer) = &ports.observer {
295 observer(observation);
296 }
297}
298
299fn table_rows(table: &crate::ast::Table) -> Value {
300 let row =
301 |cells: &[String]| Value::List(cells.iter().map(|c| Value::from(c.as_str())).collect());
302 let mut rows = vec![row(&table.header.cells)];
303 for r in &table.rows {
304 rows.push(row(&r.cells));
305 }
306 Value::List(rows)
307}
308
309fn attach_location(error: StepError, step: &PlannedStep, var_path: &str) -> StepFailure {
310 let anchor = failure_anchor::anchor(&error, step.match_span);
311 let label = truncate_label(&step.text);
312 StepFailure {
313 error,
314 location: Some(FailureLocation {
315 label,
316 path: var_path.to_string(),
317 line: anchor.start_line,
318 }),
319 }
320}
321
322fn truncate_label(text: &str) -> String {
323 if utf16_len(text) > 60 {
324 let truncated: String = text.chars().take(60).collect();
325 format!("{truncated}…")
326 } else {
327 text.to_string()
328 }
329}
330
331fn check_sensor_return(
336 source: &str,
337 step: &PlannedStep,
338 returned: Option<Value>,
339) -> Result<(), StepError> {
340 let Some(returned) = returned else {
341 return Ok(());
342 };
343 let extra_count = usize::from(step.data_table.is_some() || step.doc_string.is_some());
344 let slot_count = step.args.len() + extra_count;
345 if slot_count == 0 {
346 return Err(StepError::ReturnShape(
347 "this sensor has no parameters, data table or doc string — nothing to compare a return value against \
348 (throw to fail, return nothing to pass)"
349 .to_string(),
350 ));
351 }
352 let slots: Vec<Value> = if slot_count == 1 {
353 vec![returned]
355 } else {
356 match returned {
357 Value::List(list) => {
358 if list.len() != slot_count {
359 return Err(StepError::ReturnShape(format!(
360 "sensor return must have {} element(s), got {}",
361 slot_count,
362 list.len()
363 )));
364 }
365 list
366 }
367 other => {
368 return Err(StepError::ReturnShape(format!(
369 "a sensor with {} parameters must return a List of {} values, got {}",
370 slot_count,
371 slot_count,
372 other.type_name()
373 )));
374 }
375 }
376 };
377
378 let arg_count = step.args.len();
379 if arg_count > 0 {
380 let source_texts: Vec<String> = step
381 .param_spans
382 .iter()
383 .map(|s| utf16_slice(source, s.start_offset, s.end_offset).to_string())
384 .collect();
385 let bad: Vec<CellDiff> = compare_params_with_formats(
386 &slots[0..arg_count],
387 &step.args,
388 &step.param_spans,
389 &source_texts,
390 Some(&step.formats),
391 )
392 .into_iter()
393 .filter(|d| !d.ok)
394 .collect();
395 if !bad.is_empty() {
396 return Err(StepError::CellMismatch(bad));
397 }
398 }
399
400 if let Some(table) = &step.data_table {
401 let bad: Vec<CellDiff> = compare_table(Some(&slots[arg_count]), table)?
402 .into_iter()
403 .filter(|d| !d.ok)
404 .collect();
405 if !bad.is_empty() {
406 return Err(StepError::CellMismatch(bad));
407 }
408 } else if let Some(fence) = &step.doc_string {
409 if let Some(diff) =
410 compare_doc_string(Some(&slots[arg_count]), &fence.body, fence.body_span)?
411 {
412 return Err(StepError::DocStringMismatch(diff));
413 }
414 }
415 Ok(())
416}
417
418thread_local! {
423 static SUPPRESS_PANIC: Cell<bool> = const { Cell::new(false) };
424}
425
426static HOOK: Once = Once::new();
427
428fn install_hook() {
439 HOOK.call_once(|| {
440 let previous = std::panic::take_hook();
441 std::panic::set_hook(Box::new(move |info| {
442 if SUPPRESS_PANIC.with(Cell::get) {
443 return;
444 }
445 previous(info);
446 }));
447 });
448}
449
450fn invoke_resolve(
453 handler: &Handler,
454 state: Rc<dyn Any>,
455 args: Vec<Value>,
456) -> Result<StepOutput, HandlerError> {
457 install_hook();
458 let caught = SUPPRESS_PANIC.with(|s| {
459 s.set(true);
460 let r = std::panic::catch_unwind(AssertUnwindSafe(|| match handler.call(state, args) {
461 StepReturn::Ready(r) => r,
462 StepReturn::Pending(fut) => block_on(fut),
463 }));
464 s.set(false);
465 r
466 });
467 match caught {
468 Ok(r) => r,
469 Err(payload) => Err(HandlerError::from_panic(payload)),
470 }
471}
472
473fn block_on<T>(mut fut: Pin<Box<dyn Future<Output = T>>>) -> T {
476 struct ThreadWaker(std::thread::Thread);
477 impl Wake for ThreadWaker {
478 fn wake(self: std::sync::Arc<Self>) {
479 self.0.unpark();
480 }
481 fn wake_by_ref(self: &std::sync::Arc<Self>) {
482 self.0.unpark();
483 }
484 }
485 let waker = Waker::from(std::sync::Arc::new(ThreadWaker(std::thread::current())));
486 let mut cx = Context::from_waker(&waker);
487 loop {
488 match fut.as_mut().poll(&mut cx) {
489 Poll::Ready(v) => return v,
490 Poll::Pending => std::thread::park(),
491 }
492 }
493}