1use std::collections::BTreeMap;
25use std::sync::{Arc, Mutex};
26
27use beck_core::backend::{Backend, Callable, ExecError, Interceptor};
28use beck_core::core::{Core, CoreKind, VarId};
29use beck_core::testing::{Clause, Count, Expectation, TestDef};
30use beck_core::{digest, Effect, Placed, Tier, Ty, Value};
31
32use crate::log::{Envelope, Instant};
33use crate::program::Runtime;
34
35pub const DEFAULT_RUNS: u64 = 100;
37
38pub const DEFAULT_ACTOR: &str = "test";
40
41#[derive(Clone, Debug, Default)]
42pub struct Options {
43 pub filter: Option<String>,
45 pub runs: u64,
47 pub base_dir: std::path::PathBuf,
50 pub update_snapshots: bool,
56}
57
58impl Options {
59 pub fn runs(&self) -> u64 {
60 if self.runs == 0 {
61 DEFAULT_RUNS
62 } else {
63 self.runs
64 }
65 }
66}
67
68#[derive(Clone, Debug)]
69pub struct Report {
70 pub cases: Vec<Case>,
71}
72
73impl Report {
74 pub fn passed(&self) -> usize {
75 self.cases.iter().filter(|c| c.outcome.is_pass()).count()
76 }
77 pub fn failed(&self) -> usize {
78 self.cases
79 .iter()
80 .filter(|c| matches!(c.outcome, Outcome::Failed { .. }))
81 .count()
82 }
83 pub fn skipped(&self) -> usize {
84 self.cases
85 .iter()
86 .filter(|c| matches!(c.outcome, Outcome::Skipped(_)))
87 .count()
88 }
89 pub fn ok(&self) -> bool {
90 self.failed() == 0
91 }
92}
93
94#[derive(Clone, Debug)]
95pub struct Case {
96 pub name: Arc<str>,
97 pub outcome: Outcome,
98 pub stubbed: Vec<Stubbed>,
101 pub runs: u64,
103}
104
105#[derive(Clone, Debug)]
106pub enum Outcome {
107 Passed,
108 Failed {
109 why: String,
110 },
111 Skipped(String),
113}
114
115impl Outcome {
116 pub fn is_pass(&self) -> bool {
117 matches!(self, Outcome::Passed)
118 }
119}
120
121#[derive(Clone, Debug)]
122pub struct Stubbed {
123 pub def: Arc<str>,
124 pub atom: String,
125 pub returned: Option<Value>,
130 pub raised: Option<String>,
133 pub calls: usize,
134 pub explicit: bool,
136 pub from_the_call: bool,
138}
139
140enum Answer {
152 Value(Value),
153 Fails(ExecError),
159 FromTheCall(Callable),
160}
161
162struct Entry {
165 atom: Effect,
166 answer: Answer,
167 explicit: bool,
168 refused: Option<String>,
173}
174
175struct Call {
177 def: Arc<str>,
178 args: Vec<Value>,
179 returned: Option<Value>,
181 raised: Option<String>,
182}
183
184#[derive(Default)]
185struct Recorder {
186 entries: BTreeMap<Arc<str>, Entry>,
187 calls: Mutex<Vec<Call>>,
188 problems: Mutex<Vec<String>>,
191}
192
193impl Interceptor for Recorder {
194 fn intercept(&self, name: &str, args: &[Value]) -> Option<Result<Value, ExecError>> {
195 let e = self.entries.get(name)?;
196 if let Some(why) = &e.refused {
197 let atom = e.atom.name();
198 self.problems.lock().expect("stub log").push(format!(
199 "the generator cannot invent a return value for `{name}` ({atom}): {why}\n \
200 write the stub out: `stub {atom}: <value>`"
201 ));
202 }
203 let answered: Result<Value, ExecError> = match &e.answer {
210 Answer::Value(v) => Ok(v.clone()),
211 Answer::Fails(err) => Err(err.clone()),
212 Answer::FromTheCall(f) => match f(args.to_vec()) {
213 Ok(v) => Ok(v),
214 Err(err) if err.raised.is_some() => Err(err),
215 Err(err) => {
216 self.problems
217 .lock()
218 .expect("stub log")
219 .push(format!("the stub for `{}` failed: {err}", e.atom.name()));
220 Ok(Value::Unit)
221 }
222 },
223 };
224 self.calls.lock().expect("stub log").push(Call {
225 def: Arc::from(name),
226 args: args.to_vec(),
227 returned: answered.as_ref().ok().cloned(),
228 raised: answered.as_ref().err().map(|e| e.message.clone()),
229 });
230 Some(answered)
231 }
232}
233
234impl Recorder {
235 fn count(&self, atom: &Effect) -> usize {
236 self.calls
237 .lock()
238 .expect("stub log")
239 .iter()
240 .filter(|c| self.entries.get(&c.def).map(|e| &e.atom) == Some(atom))
241 .count()
242 }
243
244 fn called_with(&self, atom: &Effect, wanted: &Value) -> bool {
245 let want = digest(wanted);
246 self.calls
247 .lock()
248 .expect("stub log")
249 .iter()
250 .filter(|c| self.entries.get(&c.def).map(|e| &e.atom) == Some(atom))
251 .any(|c| c.args.iter().any(|a| digest(a) == want))
252 }
253
254 fn report(&self) -> Vec<Stubbed> {
255 let calls = self.calls.lock().expect("stub log");
256 let mut out: Vec<Stubbed> = self
257 .entries
258 .iter()
259 .map(|(def, e)| {
260 let mine: Vec<&Call> = calls.iter().filter(|c| c.def == *def).collect();
261 Stubbed {
262 def: def.clone(),
263 atom: e.atom.name(),
264 returned: match &e.answer {
265 Answer::Value(v) => Some(v.clone()),
266 Answer::Fails(_) => None,
267 Answer::FromTheCall(_) => mine.last().and_then(|c| c.returned.clone()),
270 },
271 raised: match &e.answer {
272 Answer::Fails(err) => Some(err.message.clone()),
273 Answer::Value(_) => None,
274 Answer::FromTheCall(_) => mine.last().and_then(|c| c.raised.clone()),
275 },
276 calls: mine.len(),
277 explicit: e.explicit,
278 from_the_call: matches!(e.answer, Answer::FromTheCall(_)),
279 }
280 })
281 .collect();
282 out.sort_by(|a, b| a.def.cmp(&b.def));
283 out
284 }
285}
286
287pub fn run(placed: &Placed, backend: Arc<dyn Backend>, opts: &Options) -> Report {
298 match backend.stack_bytes() {
299 0 => run_here(placed, backend, opts),
300 bytes => std::thread::scope(|scope| {
301 std::thread::Builder::new()
302 .stack_size(bytes)
303 .name("beck-test".into())
304 .spawn_scoped(scope, || run_here(placed, backend, opts))
305 .expect("a thread for the tests")
306 .join()
307 .unwrap_or_else(|panic| std::panic::resume_unwind(panic))
308 }),
309 }
310}
311
312fn run_here(placed: &Placed, backend: Arc<dyn Backend>, opts: &Options) -> Report {
313 let mut cases = Vec::new();
314 for t in &placed.program.tests {
315 if let Some(f) = &opts.filter {
316 if !t.name.contains(f.as_str()) {
317 continue;
318 }
319 }
320 cases.push(run_one(placed, backend.clone(), t, opts));
321 }
322 Report { cases }
323}
324
325fn needs_an_application(t: &TestDef) -> Option<&'static str> {
337 for clause in &t.clauses {
338 match clause {
339 Clause::Given { .. } => return Some("`given` folds a log, and a library has none"),
340 Clause::When { .. } => {
341 return Some("`when` proposes a command through `validate`, and a library has none")
342 }
343 Clause::Expect { what, .. } => match what {
344 Expectation::PageContains { .. } | Expectation::PageMatchesSnapshot { .. } => {
345 return Some("`page` is the view of an application, and a library has none")
346 }
347 Expectation::FoldEquals { .. } => {
348 return Some("`fold_of` folds a log, and a library has none")
349 }
350 _ => {}
351 },
352 Clause::Stub { .. } => {}
353 }
354 }
355 None
356}
357
358fn run_one(placed: &Placed, backend: Arc<dyn Backend>, t: &TestDef, opts: &Options) -> Case {
359 if !placed.is_application() {
360 if let Some(why) = needs_an_application(t) {
361 return Case {
362 name: t.name.clone(),
363 outcome: Outcome::Failed {
364 why: format!(
365 "{why}. This module has no merge point, so it is a library: add \
366 `proposals: Stream[Proposal] = merge_clients()` and a `durable` fold to \
367 make it an application, or write this test over the module's own \
368 definitions"
369 ),
370 },
371 stubbed: Vec::new(),
372 runs: 0,
373 };
374 }
375 }
376 let stubs = match build_stubs(placed, &backend, t) {
377 Ok(s) => s,
378 Err(why) => {
379 return Case {
380 name: t.name.clone(),
381 outcome: Outcome::Failed { why },
382 stubbed: Vec::new(),
383 runs: 0,
384 }
385 }
386 };
387
388 if t.is_static_only() {
391 let outcome = match static_only(placed, t, opts) {
392 Ok(()) => Outcome::Passed,
393 Err(why) => Outcome::Failed { why },
394 };
395 return Case {
396 name: t.name.clone(),
397 outcome,
398 stubbed: Vec::new(),
399 runs: 0,
400 };
401 }
402
403 let recorder = Arc::new(stubs);
404 let needs_stubs = !recorder.entries.is_empty();
405 let exec: Arc<dyn Backend> = if needs_stubs {
406 match backend.intercepting(recorder.clone()) {
407 Some(b) => b,
408 None => {
409 return Case {
410 name: t.name.clone(),
411 outcome: Outcome::Skipped(format!(
412 "the `{}` backend cannot install stubs, and this test's subject performs \
413 effects that must not run for real",
414 backend.name()
415 )),
416 stubbed: Vec::new(),
417 runs: 0,
418 }
419 }
420 }
421 } else {
422 backend.clone()
423 };
424
425 let runtime = match Runtime::new(placed.clone(), exec) {
426 Ok(r) => r,
427 Err(e) => {
428 return Case {
429 name: t.name.clone(),
430 outcome: Outcome::Failed {
431 why: format!("preparing the program: {e}"),
432 },
433 stubbed: Vec::new(),
434 runs: 0,
435 }
436 }
437 };
438
439 let runs = if t.is_property() { opts.runs() } else { 1 };
440 let mut ran = 0;
441 for run in 0..runs {
442 ran += 1;
443 let inputs = match generate(placed, t, run) {
444 Ok(v) => v,
445 Err(why) => {
446 return Case {
447 name: t.name.clone(),
448 outcome: Outcome::Failed { why },
449 stubbed: recorder.report(),
450 runs: ran,
451 }
452 }
453 };
454 if let Err(why) = execute(placed, &runtime, &recorder, t, &inputs, opts) {
455 let (inputs, why) = shrink_failure(placed, &runtime, &recorder, t, inputs, why, opts);
458 let shown = describe_inputs(t, &inputs);
459 return Case {
460 name: t.name.clone(),
461 outcome: Outcome::Failed {
462 why: if shown.is_empty() {
463 why
464 } else {
465 format!("{why}\n with {shown}")
466 },
467 },
468 stubbed: recorder.report(),
469 runs: ran,
470 };
471 }
472 }
473
474 let problems = recorder.problems.lock().expect("stub log").clone();
475 if !problems.is_empty() {
476 return Case {
477 name: t.name.clone(),
478 outcome: Outcome::Failed {
479 why: problems.join("\n"),
480 },
481 stubbed: recorder.report(),
482 runs: ran,
483 };
484 }
485
486 Case {
487 name: t.name.clone(),
488 outcome: Outcome::Passed,
489 stubbed: recorder.report(),
490 runs: ran,
491 }
492}
493
494fn build_stubs(
496 placed: &Placed,
497 backend: &Arc<dyn Backend>,
498 t: &TestDef,
499) -> Result<Recorder, String> {
500 let program = &placed.program;
501 let mut explicit: BTreeMap<Effect, Answer> = BTreeMap::new();
504 for c in &t.clauses {
505 if let Clause::Stub {
506 atom,
507 params,
508 value,
509 ..
510 } = c
511 {
512 let answer = if params.is_empty() {
513 match backend.constant(value) {
514 Ok(v) => Answer::Value(v),
515 Err(e) if e.raised.is_some() => Answer::Fails(e),
518 Err(e) => {
519 return Err(format!("evaluating the stub for `{}`: {e}", atom.name()))
520 }
521 }
522 } else {
523 let lam = Core {
524 kind: CoreKind::Lam {
525 params: params.clone().into(),
526 body: std::sync::Arc::new(value.clone()),
527 },
528 ty: Ty::fun(Vec::new(), value.ty.clone()),
529 tier: Tier::Any,
530 span: value.span,
531 last_use: false,
532 order: beck_core::fields::UNORDERED,
533 locals: 0,
534 };
535 Answer::FromTheCall(
536 backend
537 .function(&lam)
538 .map_err(|e| format!("preparing the stub for `{}`: {e}", atom.name()))?,
539 )
540 };
541 explicit.insert(atom.clone(), answer);
542 }
543 }
544
545 let mut entries = BTreeMap::new();
546 for (name, def) in &program.defs {
547 let atom = def
555 .effects
556 .iter()
557 .filter(|e| beck_core::testing::performs_itself(def, e))
558 .find(|e| explicit.contains_key(e))
559 .or_else(|| {
560 def.effects
561 .iter()
562 .filter(|e| beck_core::testing::performs_itself(def, e))
563 .find(|e| beck_core::testing::is_auto_stubbable(e))
564 });
565 let Some(atom) = atom.cloned() else { continue };
566 let (answer, refused, is_explicit) = match explicit.get(&atom) {
567 Some(Answer::Value(v)) => (Answer::Value(v.clone()), None, true),
568 Some(Answer::Fails(e)) => (Answer::Fails(e.clone()), None, true),
569 Some(Answer::FromTheCall(f)) => (Answer::FromTheCall(f.clone()), None, true),
570 None => match beck_core::gen::canonical(&def.ret, &program.types) {
571 Ok(v) => (Answer::Value(v), None, false),
572 Err(e) => (Answer::Value(Value::Unit), Some(e.to_string()), false),
573 },
574 };
575 entries.insert(
576 name.clone(),
577 Entry {
578 atom,
579 answer,
580 explicit: is_explicit,
581 refused,
582 },
583 );
584 }
585
586 Ok(Recorder {
589 entries,
590 ..Default::default()
591 })
592}
593
594fn generate(placed: &Placed, t: &TestDef, run: u64) -> Result<Vec<Value>, String> {
596 let mut rng = beck_core::gen::Rng::seeded(&t.name, run);
597 let mut out = Vec::with_capacity(t.params.len());
598 for (_, name, ty) in &t.params {
599 let v = beck_core::gen::arbitrary(ty, &placed.program.types, &mut rng)
600 .map_err(|e| format!("generating `{name}`: {e}"))?;
601 out.push(v);
602 }
603 Ok(out)
604}
605
606fn describe_inputs(t: &TestDef, inputs: &[Value]) -> String {
607 t.params
608 .iter()
609 .zip(inputs)
610 .map(|((_, n, _), v)| format!("{n} = {}", v.display()))
611 .collect::<Vec<_>>()
612 .join(", ")
613}
614
615fn shrink_failure(
620 placed: &Placed,
621 runtime: &Runtime,
622 recorder: &Arc<Recorder>,
623 t: &TestDef,
624 inputs: Vec<Value>,
625 why: String,
626 opts: &Options,
627) -> (Vec<Value>, String) {
628 let mut best = inputs;
629 let mut why = why;
630 let mut improved = true;
631 while improved {
632 improved = false;
633 for i in 0..best.len() {
634 for candidate in beck_core::gen::shrink(&best[i]) {
635 let mut next = best.clone();
636 next[i] = candidate;
637 if let Err(w) = execute(placed, runtime, recorder, t, &next, opts) {
638 best = next;
639 why = w;
640 improved = true;
641 break;
642 }
643 }
644 }
645 }
646 (best, why)
647}
648
649fn snapshot(
666 opts: &Options,
667 test: &str,
668 name: Option<&str>,
669 actor: &str,
670 rendered: &str,
671) -> Result<(), String> {
672 let dir = opts.base_dir.join("snapshots");
673 let path = dir.join(format!("{}.html", snapshot_key(test, name, actor)));
674
675 if opts.update_snapshots {
676 std::fs::create_dir_all(&dir).map_err(|e| format!("creating {}: {e}", dir.display()))?;
677 std::fs::write(&path, rendered).map_err(|e| format!("writing {}: {e}", path.display()))?;
678 return Ok(());
679 }
680
681 let Ok(want) = std::fs::read_to_string(&path) else {
682 return Err(format!(
683 "no snapshot recorded at {}\n \
684 run `beck test --update` to write it, then review the file like any other diff",
685 path.display()
686 ));
687 };
688 if want == rendered {
689 return Ok(());
690 }
691 Err(format!(
692 "the page `{actor}` sees does not match {}\n{}",
693 path.display(),
694 first_difference(&want, rendered)
695 ))
696}
697
698fn window(line: &str, at: usize) -> String {
700 const EITHER_SIDE: usize = 60;
701 let floor = |i: usize| {
702 (0..=i)
703 .rev()
704 .find(|&i| line.is_char_boundary(i))
705 .unwrap_or(0)
706 };
707 let ceil = |i: usize| {
708 (i..=line.len())
709 .find(|&i| line.is_char_boundary(i))
710 .unwrap_or(line.len())
711 };
712 let start = floor(at.saturating_sub(EITHER_SIDE));
713 let end = ceil((at + EITHER_SIDE).min(line.len()));
714 format!(
715 "{}{}{}",
716 if start > 0 { "…" } else { "" },
717 &line[start..end],
718 if end < line.len() { "…" } else { "" }
719 )
720}
721
722fn snapshot_key(test: &str, name: Option<&str>, actor: &str) -> String {
728 let slug = |s: &str| -> String {
729 let mut out = String::new();
730 let mut dash = false;
731 for c in s.chars() {
732 if c.is_ascii_alphanumeric() || c == '_' {
733 out.push(c);
734 dash = false;
735 } else if !dash && !out.is_empty() {
736 out.push('-');
737 dash = true;
738 }
739 }
740 out.trim_end_matches('-').to_string()
741 };
742 format!("{}@{}", slug(name.unwrap_or(test)), slug(actor))
743}
744
745fn first_difference(want: &str, got: &str) -> String {
752 for (i, (a, b)) in want.lines().zip(got.lines()).enumerate() {
753 if a != b {
754 let at = a
755 .char_indices()
756 .zip(b.char_indices())
757 .find(|((_, x), (_, y))| x != y)
758 .map(|((i, _), _)| i)
759 .unwrap_or_else(|| a.len().min(b.len()));
760 return format!(
761 " line {}, column {}:\n snapshot: {}\n rendered: {}",
762 i + 1,
763 a[..at].chars().count() + 1,
764 window(a, at),
765 window(b, at)
766 );
767 }
768 }
769 let (w, g) = (want.lines().count(), got.lines().count());
770 if w == g {
771 return " the lines are identical and the files are not — a trailing newline differs"
774 .to_string();
775 }
776 format!(" the snapshot has {w} lines and the page rendered {g}")
777}
778
779fn execute(
781 placed: &Placed,
782 runtime: &Runtime,
783 recorder: &Arc<Recorder>,
784 t: &TestDef,
785 inputs: &[Value],
786 opts: &Options,
787) -> Result<(), String> {
788 let program = &placed.program;
789 let mut state = runtime
790 .initial_state()
791 .map_err(|e| format!("evaluating the initial state: {e}"))?;
792 let mut events: Vec<Value> = Vec::new();
793 let mut result = Value::ok(Value::list(Vec::new()));
794 let mut seq: u64 = 0;
795 let mut actor: Arc<str> = Arc::from(DEFAULT_ACTOR);
796
797 for clause in &t.clauses {
798 match clause {
799 Clause::Given {
800 events: code,
801 actor: who,
802 ..
803 } => {
804 let who = who.clone().unwrap_or_else(|| actor.clone());
805 let log = eval(runtime, t, code, &state, &events, &result, inputs)?;
806 let log = log
807 .as_list()
808 .map(|xs| xs.to_vec())
809 .ok_or_else(|| "`given` did not produce a list of events".to_string())?;
810 for e in log {
811 seq += 1;
812 state = fold(runtime, &state, seq, &who, e)?;
813 }
814 }
815 Clause::When {
816 actor: who,
817 route,
818 commands,
819 ..
820 } => {
821 if let Some(w) = who {
822 actor = w.clone();
823 }
824 let from = at(&actor, route);
825 for cmd in commands {
826 let c = eval(runtime, t, cmd, &state, &events, &result, inputs)?;
827 let proposal = runtime.proposal(&from, c);
828 let out = runtime
829 .decide(&state, &proposal)
830 .map_err(|e| format!("`validate` failed: {e}"))?;
831 result = out.clone();
832 if out.variant() == Some("Ok") {
834 let produced = out
835 .field("value")
836 .and_then(|v| v.as_list())
837 .map(|xs| xs.to_vec())
838 .unwrap_or_default();
839 for e in produced {
840 events.push(e.clone());
841 seq += 1;
842 state = fold(runtime, &state, seq, &actor, e)?;
843 }
844 }
845 }
846 }
847 Clause::Stub { .. } => {}
848 Clause::Expect { what, .. } => match what {
849 Expectation::Holds(code) => {
850 let v = eval(runtime, t, code, &state, &events, &result, inputs)?;
851 if v.as_bool() != Some(true) {
852 if let CoreKind::Prim {
856 op: beck_core::Prim::Eq,
857 args,
858 } = &code.kind
859 {
860 if args.len() == 2 {
861 let l =
862 eval(runtime, t, &args[0], &state, &events, &result, inputs)?;
863 let r =
864 eval(runtime, t, &args[1], &state, &events, &result, inputs)?;
865 return Err(format!(
866 "these are not equal\n is: {}\n wanted: {}",
867 elide(&l.display()),
868 elide(&r.display())
869 ));
870 }
871 }
872 return Err(format!("expected true, got {}", v.display()));
873 }
874 }
875 Expectation::PageContains {
876 needle,
877 actor: who,
878 route,
879 } => {
880 let n = eval(runtime, t, needle, &state, &events, &result, inputs)?;
881 let n = n.as_str().unwrap_or_default().to_string();
882 let who = who.clone().unwrap_or_else(|| actor.clone());
883 let seen = at(&who, route);
884 let page = runtime
885 .view(&state, &seen)
886 .map_err(|e| format!("rendering the page for `{who}`: {e}"))?;
887 let rendered = page.render();
888 if !rendered.contains(&n) {
889 return Err(format!(
890 "the page `{who}` sees does not contain {n:?}\n page: {}",
891 elide(&rendered)
892 ));
893 }
894 }
895 Expectation::PageMatchesSnapshot {
896 name,
897 actor: who,
898 route,
899 } => {
900 let who = who.clone().unwrap_or_else(|| actor.clone());
901 let seen = at(&who, route);
902 let page = runtime
903 .view(&state, &seen)
904 .map_err(|e| format!("rendering the page for `{who}`: {e}"))?;
905 snapshot(opts, &t.name, name.as_deref(), &who, &page.render())?;
906 }
907 Expectation::FoldEquals {
908 events: code,
909 actor: who,
910 } => {
911 let log = eval(runtime, t, code, &state, &events, &result, inputs)?;
912 let log = log
913 .as_list()
914 .cloned()
915 .ok_or_else(|| "`fold_of` did not get a list of events".to_string())?;
916 let who = who.clone().unwrap_or_else(|| Arc::from(DEFAULT_ACTOR));
917 let mut other = runtime
918 .initial_state()
919 .map_err(|e| format!("evaluating the initial state: {e}"))?;
920 for (i, e) in log.into_iter().enumerate() {
921 other = fold(runtime, &other, i as u64 + 1, &who, e)?;
922 }
923 if digest(&state) != digest(&other) {
924 return Err(format!(
925 "the state is not the fold of that log\n is: {}\n wanted: {}",
926 elide(&state.display()),
927 elide(&other.display())
928 ));
929 }
930 }
931 Expectation::Performed { atom, how } => match how {
932 Count::Never => {
933 let n = recorder.count(atom);
934 if n != 0 {
935 return Err(format!(
936 "`{}` was performed {n} time(s), and the test says it is not",
937 atom.name()
938 ));
939 }
940 }
941 Count::Times(k) => {
942 let n = recorder.count(atom) as i64;
943 if n != *k {
944 return Err(format!(
945 "`{}` was performed {n} time(s), not {k}",
946 atom.name()
947 ));
948 }
949 }
950 Count::With(code) => {
951 let want = eval(runtime, t, code, &state, &events, &result, inputs)?;
952 if !recorder.called_with(atom, &want) {
953 return Err(format!(
954 "`{}` was never performed with {}",
955 atom.name(),
956 want.display()
957 ));
958 }
959 }
960 },
961 Expectation::Place { .. }
962 | Expectation::Flow { .. }
963 | Expectation::WireCompatible { .. } => {
964 check_static(placed, what, opts)?;
965 }
966 },
967 }
968 }
969 let _ = program;
970 Ok(())
971}
972
973fn static_only(placed: &Placed, t: &TestDef, opts: &Options) -> Result<(), String> {
974 for c in &t.clauses {
975 if let Clause::Expect { what, .. } = c {
976 check_static(placed, what, opts)?;
977 }
978 }
979 Ok(())
980}
981
982fn check_static(placed: &Placed, what: &Expectation, opts: &Options) -> Result<(), String> {
984 match what {
985 Expectation::Place {
986 what: name, tier, ..
987 } => {
988 let actual = placed
989 .program
990 .defs
991 .get(name.as_ref())
992 .map(|d| d.tier)
993 .or_else(|| {
994 placed
995 .program
996 .signals
997 .iter()
998 .find(|s| s.name == *name)
999 .map(|s| s.tier)
1000 })
1001 .ok_or_else(|| format!("`{name}` is not a definition or a signal"))?;
1002 if actual != *tier {
1003 return Err(format!(
1004 "`{name}` is placed on `{}`, not `{}`",
1005 actual.name(),
1006 tier.name()
1007 ));
1008 }
1009 Ok(())
1010 }
1011 Expectation::Flow { ty, tier } => {
1012 let reached: Vec<String> = beck_core::secure::flow(&placed.program, ty)
1016 .into_iter()
1017 .filter(|r| r.tier == *tier || r.tier == Tier::Any)
1018 .map(|r| format!("{} ({})", r.what, r.tier.name()))
1019 .collect();
1020 if !reached.is_empty() {
1021 return Err(format!(
1022 "`{ty}` reaches {} on `{}`",
1023 reached.join(", "),
1024 tier.name()
1025 ));
1026 }
1027 Ok(())
1028 }
1029 Expectation::WireCompatible { path } => {
1030 let file = opts.base_dir.join(path.as_ref());
1031 let src = std::fs::read_to_string(&file)
1032 .map_err(|e| format!("reading `{}`: {e}", file.display()))?;
1033 let mut diags = beck_diag::Diagnostics::new();
1034 let mut map = beck_diag::SourceMap::new();
1035 let previous =
1036 beck_core::Interface::parse(&placed.program.name, &src, &mut map, &mut diags);
1037 if diags.has_errors() {
1038 return Err(format!("`{}` is not a readable interface", file.display()));
1039 }
1040 let current = beck_core::Interface::of(&placed.program);
1041 let changes = beck_core::compare(&previous, ¤t);
1042 if beck_core::is_breaking(&changes) {
1043 let why: Vec<String> = changes
1044 .iter()
1045 .filter(|c| c.severity == beck_core::compat::Severity::Breaking)
1046 .map(|c| format!("{}: {}", c.what, c.because))
1047 .collect();
1048 return Err(format!(
1049 "not wire-compatible with `{path}`\n {}",
1050 why.join("\n ")
1051 ));
1052 }
1053 Ok(())
1054 }
1055 _ => Ok(()),
1056 }
1057}
1058
1059fn fold(
1060 runtime: &Runtime,
1061 state: &Value,
1062 seq: u64,
1063 actor: &str,
1064 event: Value,
1065) -> Result<Value, String> {
1066 let env = Envelope {
1069 seq,
1070 at: Instant(seq as i64),
1071 actor: actor.to_string(),
1072 body: event.clone(),
1073 };
1074 runtime
1075 .fold(state, &env, event)
1076 .map_err(|e| format!("folding at seq {seq}: {e}"))
1077}
1078
1079fn eval(
1089 runtime: &Runtime,
1090 t: &TestDef,
1091 code: &Core,
1092 state: &Value,
1093 events: &[Value],
1094 result: &Value,
1095 inputs: &[Value],
1096) -> Result<Value, String> {
1097 let mut params: Vec<VarId> = vec![t.bindings.state, t.bindings.events, t.bindings.result];
1098 params.extend(t.params.iter().map(|(id, _, _)| *id));
1099 let lam = Core {
1100 kind: CoreKind::Lam {
1101 params: params.into(),
1102 body: std::sync::Arc::new(code.clone()),
1103 },
1104 ty: Ty::fun(Vec::new(), code.ty.clone()),
1105 tier: Tier::Any,
1106 span: code.span,
1107 last_use: false,
1108 order: beck_core::fields::UNORDERED,
1109 locals: beck_core::frames::locals_of(code),
1110 };
1111 let f = runtime
1112 .prepare(&lam)
1113 .map_err(|e| format!("preparing an expectation: {e}"))?;
1114 let mut args = vec![state.clone(), Value::list(events.to_vec()), result.clone()];
1115 args.extend(inputs.iter().cloned());
1116 f(args).map_err(|e| e.to_string())
1117}
1118
1119fn elide(s: &str) -> String {
1120 if s.chars().count() <= 200 {
1121 s.to_string()
1122 } else {
1123 let head: String = s.chars().take(200).collect();
1124 format!("{head}…")
1125 }
1126}
1127
1128pub fn render(report: &Report, verbose: bool) -> String {
1130 let mut out = String::new();
1131 for c in &report.cases {
1132 let mark = match &c.outcome {
1133 Outcome::Passed => "ok".to_string(),
1134 Outcome::Failed { .. } => "FAILED".to_string(),
1135 Outcome::Skipped(_) => "skipped".to_string(),
1136 };
1137 let runs = if c.runs > 1 {
1138 format!(" ({} inputs)", c.runs)
1139 } else {
1140 String::new()
1141 };
1142 out.push_str(&format!("test {:?} … {mark}{runs}\n", c.name));
1143 match &c.outcome {
1144 Outcome::Failed { why } => {
1145 for line in why.lines() {
1146 out.push_str(&format!(" {line}\n"));
1147 }
1148 }
1149 Outcome::Skipped(why) => out.push_str(&format!(" {why}\n")),
1150 Outcome::Passed => {}
1151 }
1152 let shown: Vec<&Stubbed> = c
1154 .stubbed
1155 .iter()
1156 .filter(|s| verbose || s.calls > 0)
1157 .collect();
1158 if !shown.is_empty() && (verbose || matches!(c.outcome, Outcome::Failed { .. })) {
1159 out.push_str(" stubbed:\n");
1160 for s in shown {
1161 let how = match (s.explicit, s.from_the_call) {
1162 (true, true) => "named, from the call",
1163 (true, false) => "named",
1164 (false, _) => "automatically",
1165 };
1166 let answered = match (&s.returned, &s.raised) {
1167 (Some(v), _) => v.display(),
1168 (None, Some(why)) => why.clone(),
1169 (None, None) => "—".into(),
1170 };
1171 out.push_str(&format!(
1172 " {:<24} by `{}` → {answered} called {}× ({how})\n",
1173 s.atom, s.def, s.calls
1174 ));
1175 }
1176 }
1177 }
1178 out.push_str(&format!(
1179 "\n{} passed, {} failed, {} skipped\n",
1180 report.passed(),
1181 report.failed(),
1182 report.skipped()
1183 ));
1184 out
1185}
1186
1187fn at(actor: &Arc<str>, route: &Option<Arc<str>>) -> crate::program::At<Arc<str>> {
1193 crate::program::At {
1194 who: actor.clone(),
1195 path: route
1196 .clone()
1197 .unwrap_or_else(|| Arc::from(beck_core::edge::ROOT)),
1198 }
1199}