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 last_return.is_none() || !bad.is_empty() {
245 let last_step = steps.last().unwrap();
246 let err = if last_return.is_none() {
247 StepError::ReturnShape(
248 "a header-bound row step must return a row object with one value per bound cell, got nothing".to_string(),
249 )
250 } else {
251 StepError::CellMismatch(bad)
252 };
253 let failure = attach_location(err, last_step, path);
254 observe(
255 ports,
256 StepObservation {
257 example_index,
258 ordinal: steps.len(),
259 outcome: StepOutcome::Fail,
260 error: Some(failure.clone()),
261 },
262 );
263 thrown = Some(failure);
264 }
265 }
266 }
267 }
268
269 if ex.expected_outcome.as_deref() == Some("fail") {
271 match thrown {
272 None => {
273 return Err(match steps.last() {
274 Some(last) => attach_location(StepError::UnexpectedPass, last, path),
275 None => StepFailure::bare(StepError::UnexpectedPass),
276 });
277 }
278 Some(failure) => {
279 if let Some(expected_msg) = &ex.expected_error_message {
280 if !failure.error.message().contains(expected_msg) {
281 return Err(failure);
282 }
283 }
284 return Ok(());
285 }
286 }
287 }
288
289 match thrown {
290 Some(failure) => Err(failure),
291 None => Ok(()),
292 }
293}
294
295fn create_context(ports: &ExecutePorts, file: &str) -> Rc<dyn Any> {
296 match &ports.create_context {
297 Some(cc) => cc(file),
298 None => Rc::new(()) as Rc<dyn Any>,
299 }
300}
301
302fn observe(ports: &ExecutePorts, observation: StepObservation) {
303 if let Some(observer) = &ports.observer {
304 observer(observation);
305 }
306}
307
308fn table_rows(table: &crate::ast::Table) -> Value {
309 let row =
310 |cells: &[String]| Value::List(cells.iter().map(|c| Value::from(c.as_str())).collect());
311 let mut rows = vec![row(&table.header.cells)];
312 for r in &table.rows {
313 rows.push(row(&r.cells));
314 }
315 Value::List(rows)
316}
317
318fn attach_location(error: StepError, step: &PlannedStep, var_path: &str) -> StepFailure {
319 let anchor = failure_anchor::anchor(&error, step.match_span);
320 let label = truncate_label(&step.text);
321 StepFailure {
322 error,
323 location: Some(FailureLocation {
324 label,
325 path: var_path.to_string(),
326 line: anchor.start_line,
327 }),
328 }
329}
330
331fn truncate_label(text: &str) -> String {
332 if utf16_len(text) > 60 {
333 let truncated: String = text.chars().take(60).collect();
334 format!("{truncated}…")
335 } else {
336 text.to_string()
337 }
338}
339
340fn check_sensor_return(
345 source: &str,
346 step: &PlannedStep,
347 returned: Option<Value>,
348) -> Result<(), StepError> {
349 let extra_count = usize::from(step.data_table.is_some() || step.doc_string.is_some());
350 let slot_count = step.args.len() + extra_count;
351 let returned = match returned {
354 None if slot_count == 0 => return Ok(()),
356 None => {
357 return Err(StepError::ReturnShape(format!(
358 "a sensor with {slot_count} slot(s) must return one value per slot, got nothing"
359 )));
360 }
361 Some(v) => v,
362 };
363 if slot_count == 0 {
364 return Err(StepError::ReturnShape(
365 "this sensor has no parameters, data table or doc string — nothing to compare a return value against \
366 (throw to fail, return nothing to pass)"
367 .to_string(),
368 ));
369 }
370 let slots: Vec<Value> = if slot_count == 1 {
371 vec![returned]
373 } else {
374 match returned {
375 Value::List(list) => {
376 if list.len() != slot_count {
377 return Err(StepError::ReturnShape(format!(
378 "sensor return must have {} element(s), got {}",
379 slot_count,
380 list.len()
381 )));
382 }
383 list
384 }
385 other => {
386 return Err(StepError::ReturnShape(format!(
387 "a sensor with {} slots must return a List of {} values, got {}",
388 slot_count,
389 slot_count,
390 other.type_name()
391 )));
392 }
393 }
394 };
395
396 let arg_count = step.args.len();
397 if arg_count > 0 {
398 let source_texts: Vec<String> = step
399 .param_spans
400 .iter()
401 .map(|s| utf16_slice(source, s.start_offset, s.end_offset).to_string())
402 .collect();
403 let bad: Vec<CellDiff> = compare_params_with_formats(
404 &slots[0..arg_count],
405 &step.args,
406 &step.param_spans,
407 &source_texts,
408 Some(&step.formats),
409 )
410 .into_iter()
411 .filter(|d| !d.ok)
412 .collect();
413 if !bad.is_empty() {
414 return Err(StepError::CellMismatch(bad));
415 }
416 }
417
418 if let Some(table) = &step.data_table {
419 let bad: Vec<CellDiff> = compare_table(Some(&slots[arg_count]), table)?
420 .into_iter()
421 .filter(|d| !d.ok)
422 .collect();
423 if !bad.is_empty() {
424 return Err(StepError::CellMismatch(bad));
425 }
426 } else if let Some(fence) = &step.doc_string {
427 if let Some(diff) =
428 compare_doc_string(Some(&slots[arg_count]), &fence.body, fence.body_span)?
429 {
430 return Err(StepError::CellMismatch(vec![diff]));
431 }
432 }
433 Ok(())
434}
435
436thread_local! {
441 static SUPPRESS_PANIC: Cell<bool> = const { Cell::new(false) };
442}
443
444static HOOK: Once = Once::new();
445
446fn install_hook() {
457 HOOK.call_once(|| {
458 let previous = std::panic::take_hook();
459 std::panic::set_hook(Box::new(move |info| {
460 if SUPPRESS_PANIC.with(Cell::get) {
461 return;
462 }
463 previous(info);
464 }));
465 });
466}
467
468fn invoke_resolve(
471 handler: &Handler,
472 state: Rc<dyn Any>,
473 args: Vec<Value>,
474) -> Result<StepOutput, HandlerError> {
475 install_hook();
476 let caught = SUPPRESS_PANIC.with(|s| {
477 s.set(true);
478 let r = std::panic::catch_unwind(AssertUnwindSafe(|| match handler.call(state, args) {
479 StepReturn::Ready(r) => r,
480 StepReturn::Pending(fut) => block_on(fut),
481 }));
482 s.set(false);
483 r
484 });
485 match caught {
486 Ok(r) => r,
487 Err(payload) => Err(HandlerError::from_panic(payload)),
488 }
489}
490
491fn block_on<T>(mut fut: Pin<Box<dyn Future<Output = T>>>) -> T {
494 struct ThreadWaker(std::thread::Thread);
495 impl Wake for ThreadWaker {
496 fn wake(self: std::sync::Arc<Self>) {
497 self.0.unpark();
498 }
499 fn wake_by_ref(self: &std::sync::Arc<Self>) {
500 self.0.unpark();
501 }
502 }
503 let waker = Waker::from(std::sync::Arc::new(ThreadWaker(std::thread::current())));
504 let mut cx = Context::from_waker(&waker);
505 loop {
506 match fut.as_mut().poll(&mut cx) {
507 Poll::Ready(v) => return v,
508 Poll::Pending => std::thread::park(),
509 }
510 }
511}