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 {
329 label,
330 path: step
331 .doc_path
332 .clone()
333 .unwrap_or_else(|| oath_path.to_string()),
334 line: anchor.start_line,
335 anchor: AnchorRange {
336 from: anchor.start_offset,
337 to: anchor.end_offset,
338 },
339 }),
340 }
341}
342
343fn truncate_label(text: &str) -> String {
344 if utf16_len(text) > 60 {
345 let truncated: String = text.chars().take(60).collect();
346 format!("{truncated}…")
347 } else {
348 text.to_string()
349 }
350}
351
352fn check_sensor_return(
357 source: &str,
358 step: &PlannedStep,
359 returned: Option<Value>,
360) -> Result<(), StepError> {
361 let extra_count = usize::from(step.data_table.is_some() || step.doc_string.is_some());
362 let slot_count = step.args.len() + extra_count;
363 let returned = match returned {
366 None if slot_count == 0 => return Ok(()),
368 None => {
369 return Err(StepError::ReturnShape(format!(
370 "a sensor with {slot_count} slot(s) must return one value per slot, got nothing"
371 )));
372 }
373 Some(v) => v,
374 };
375 if slot_count == 0 {
376 return Err(StepError::ReturnShape(
377 "this sensor has no parameters, data table or doc string — nothing to compare a return value against \
378 (throw to fail, return nothing to pass)"
379 .to_string(),
380 ));
381 }
382 let slots: Vec<Value> = if slot_count == 1 {
383 vec![returned]
385 } else {
386 match returned {
387 Value::List(list) => {
388 if list.len() != slot_count {
389 return Err(StepError::ReturnShape(format!(
390 "sensor return must have {} element(s), got {}",
391 slot_count,
392 list.len()
393 )));
394 }
395 list
396 }
397 other => {
398 return Err(StepError::ReturnShape(format!(
399 "a sensor with {} slots must return a List of {} values, got {}",
400 slot_count,
401 slot_count,
402 other.type_name()
403 )));
404 }
405 }
406 };
407
408 let arg_count = step.args.len();
409 if arg_count > 0 {
410 let source_texts: Vec<String> = step
411 .param_spans
412 .iter()
413 .map(|s| utf16_slice(source, s.start_offset, s.end_offset).to_string())
414 .collect();
415 let bad: Vec<CellDiff> = compare_params_with_formats(
416 &slots[0..arg_count],
417 &step.args,
418 &step.param_spans,
419 &source_texts,
420 Some(&step.formats),
421 )
422 .into_iter()
423 .filter(|d| !d.ok)
424 .collect();
425 if !bad.is_empty() {
426 return Err(StepError::CellMismatch(bad));
427 }
428 }
429
430 if let Some(table) = &step.data_table {
431 let bad: Vec<CellDiff> = compare_table(Some(&slots[arg_count]), table)?
432 .into_iter()
433 .filter(|d| !d.ok)
434 .collect();
435 if !bad.is_empty() {
436 return Err(StepError::CellMismatch(bad));
437 }
438 } else if let Some(fence) = &step.doc_string {
439 if let Some(diff) =
440 compare_doc_string(Some(&slots[arg_count]), &fence.body, fence.body_span)?
441 {
442 return Err(StepError::CellMismatch(vec![diff]));
443 }
444 }
445 Ok(())
446}
447
448thread_local! {
453 static SUPPRESS_PANIC: Cell<bool> = const { Cell::new(false) };
454}
455
456static HOOK: Once = Once::new();
457
458fn install_hook() {
469 HOOK.call_once(|| {
470 let previous = std::panic::take_hook();
471 std::panic::set_hook(Box::new(move |info| {
472 if SUPPRESS_PANIC.with(Cell::get) {
473 return;
474 }
475 previous(info);
476 }));
477 });
478}
479
480fn invoke_resolve(
483 handler: &Handler,
484 state: Rc<dyn Any>,
485 args: Vec<Value>,
486) -> Result<StepOutput, HandlerError> {
487 install_hook();
488 let caught = SUPPRESS_PANIC.with(|s| {
489 s.set(true);
490 let r = std::panic::catch_unwind(AssertUnwindSafe(|| match handler.call(state, args) {
491 StepReturn::Ready(r) => r,
492 StepReturn::Pending(fut) => block_on(fut),
493 }));
494 s.set(false);
495 r
496 });
497 match caught {
498 Ok(r) => r,
499 Err(payload) => Err(HandlerError::from_panic(payload)),
500 }
501}
502
503fn block_on<T>(mut fut: Pin<Box<dyn Future<Output = T>>>) -> T {
506 struct ThreadWaker(std::thread::Thread);
507 impl Wake for ThreadWaker {
508 fn wake(self: std::sync::Arc<Self>) {
509 self.0.unpark();
510 }
511 fn wake_by_ref(self: &std::sync::Arc<Self>) {
512 self.0.unpark();
513 }
514 }
515 let waker = Waker::from(std::sync::Arc::new(ThreadWaker(std::thread::current())));
516 let mut cx = Context::from_waker(&waker);
517 loop {
518 match fut.as_mut().poll(&mut cx) {
519 Poll::Ready(v) => return v,
520 Poll::Pending => std::thread::park(),
521 }
522 }
523}