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