1use crate::{
2 ast::{
3 BinOp, DataTypeKey, IfBranch, OnTestFailure, Span, TraceLevel, Tracing, TypedArg,
4 TypedDataType, TypedTest,
5 },
6 expr::{CallArg, TypedExpr, UntypedExpr},
7 format::Formatter,
8 gen_uplc::CodeGenerator,
9 plutus_version::PlutusVersion,
10 tipo::{Type, convert_opaque_type},
11};
12use cryptoxide::{blake2b::Blake2b, digest::Digest};
13use indexmap::IndexMap;
14use itertools::Itertools;
15use owo_colors::{OwoColorize, Stream, Stream::Stderr};
16use pallas_primitives::alonzo::{Constr, PlutusData};
17use patricia_tree::PatriciaMap;
18#[cfg(not(target_family = "wasm"))]
19use std::time::Duration;
20use std::{
21 borrow::Borrow,
22 collections::BTreeMap,
23 convert::TryFrom,
24 fmt::{Debug, Display},
25 ops::Deref,
26 path::PathBuf,
27 rc::Rc,
28};
29use uplc::{
30 ast::{Constant, Data, Name, NamedDeBruijn, Program, Term},
31 machine::{cost_model::ExBudget, eval_result::EvalResult},
32};
33use vec1::{Vec1, vec1};
34
35#[derive(Debug, Clone, Copy)]
36pub enum RunnableKind {
37 Test,
38 Bench,
39}
40
41#[derive(Debug, Clone)]
59pub enum Test {
60 UnitTest(UnitTest),
61 PropertyTest(PropertyTest),
62 Benchmark(Benchmark),
63}
64
65unsafe impl Send for Test {}
66
67impl Test {
68 pub fn unit_test(
69 generator: &mut CodeGenerator<'_>,
70 test: TypedTest,
71 module_name: String,
72 input_path: PathBuf,
73 ) -> Test {
74 let program = generator.generate_raw(&test.body, &[], &module_name);
75
76 let assertion = match test.body.try_into() {
77 Err(..) => None,
78 Ok(Assertion { bin_op, head, tail }) => {
79 let as_constant = |generator: &mut CodeGenerator<'_>, side| {
80 Program::<NamedDeBruijn>::try_from(generator.generate_raw(
81 &side,
82 &[],
83 &module_name,
84 ))
85 .expect("failed to convert assertion operand to NamedDeBruijn")
86 .eval(ExBudget::max())
87 .unwrap_constant()
88 .map(|cst| (cst, side.tipo()))
89 };
90
91 Some(Assertion {
93 bin_op,
94 head: as_constant(generator, head.expect("cannot be Err at this point")),
95 tail: tail
96 .expect("cannot be Err at this point")
97 .try_mapped(|e| as_constant(generator, e)),
98 })
99 }
100 };
101
102 Test::UnitTest(UnitTest {
103 input_path,
104 module: module_name,
105 name: test.name,
106 program,
107 assertion,
108 on_test_failure: test.on_test_failure,
109 })
110 }
111
112 pub fn property_test(
113 input_path: PathBuf,
114 module: String,
115 name: String,
116 on_test_failure: OnTestFailure,
117 program: Program<Name>,
118 fuzzer: Fuzzer<Name>,
119 ) -> Test {
120 Test::PropertyTest(PropertyTest {
121 input_path,
122 module,
123 name,
124 program,
125 on_test_failure,
126 fuzzer,
127 })
128 }
129
130 pub fn from_function_definition(
131 generator: &mut CodeGenerator<'_>,
132 test: TypedTest,
133 module_name: String,
134 input_path: PathBuf,
135 kind: RunnableKind,
136 ) -> Test {
137 if test.arguments.is_empty() {
138 if matches!(kind, RunnableKind::Bench) {
139 unreachable!("benchmark must have at least one argument");
140 } else {
141 Self::unit_test(generator, test, module_name, input_path)
142 }
143 } else {
144 let parameter = test.arguments.first().unwrap().to_owned();
145
146 let via = parameter.via.clone();
147
148 let type_info = parameter.arg.tipo.clone();
149
150 let stripped_type_info = convert_opaque_type(&type_info, generator.data_types(), true);
151
152 let program = generator.clone().generate_raw(
153 &test.body,
154 &[TypedArg {
155 tipo: stripped_type_info.clone(),
156 ..parameter.clone().into()
157 }],
158 &module_name,
159 );
160
161 let generator_program = generator.clone().generate_raw(&via, &[], &module_name);
165
166 match kind {
167 RunnableKind::Bench => Test::Benchmark(Benchmark {
168 input_path,
169 module: module_name,
170 name: test.name,
171 program,
172 on_test_failure: test.on_test_failure,
173 sampler: Sampler {
174 program: generator_program,
175 type_info,
176 stripped_type_info,
177 },
178 }),
179 RunnableKind::Test => Self::property_test(
180 input_path,
181 module_name,
182 test.name,
183 test.on_test_failure,
184 program,
185 Fuzzer {
186 program: generator_program,
187 stripped_type_info,
188 type_info,
189 },
190 ),
191 }
192 }
193 }
194
195 pub fn run(
196 self,
197 seed: u32,
198 max_success: usize,
199 plutus_version: &PlutusVersion,
200 tracing: Tracing,
201 ) -> TestResult<(Constant, Rc<Type>), PlutusData> {
202 match self {
203 Test::UnitTest(unit_test) => {
204 TestResult::UnitTestResult(unit_test.run(plutus_version, tracing))
205 }
206 Test::PropertyTest(property_test) => {
207 TestResult::PropertyTestResult(property_test.run(seed, max_success, plutus_version))
208 }
209 Test::Benchmark(benchmark) => {
210 TestResult::BenchmarkResult(benchmark.run(seed, max_success, plutus_version))
211 }
212 }
213 }
214}
215
216#[derive(Debug, Clone)]
219pub struct UnitTest {
220 pub input_path: PathBuf,
221 pub module: String,
222 pub name: String,
223 pub on_test_failure: OnTestFailure,
224 pub program: Program<Name>,
225 pub assertion: Option<Assertion<(Constant, Rc<Type>)>>,
226}
227
228unsafe impl Send for UnitTest {}
229
230impl UnitTest {
231 pub fn run(
232 self,
233 plutus_version: &PlutusVersion,
234 tracing: Tracing,
235 ) -> UnitTestResult<(Constant, Rc<Type>)> {
236 let eval_result = Program::<NamedDeBruijn>::try_from(self.program.clone())
237 .unwrap()
238 .eval_version(ExBudget::max(), &plutus_version.into());
239
240 let is_evaluation_failure = eval_result.failed(true, &plutus_version.into());
241
242 let success = match self.on_test_failure {
243 OnTestFailure::SucceedEventually | OnTestFailure::SucceedImmediately => {
244 is_evaluation_failure
245 }
246 OnTestFailure::FailImmediately => !is_evaluation_failure,
247 };
248
249 let mut logs = Vec::new();
250 if let Err(err) = eval_result.result()
251 && tracing.trace_level(false) == TraceLevel::Verbose
252 {
253 logs.push(format!("{err}"))
254 }
255 logs.extend(eval_result.logs());
256
257 UnitTestResult {
258 success,
259 test: self.to_owned(),
260 spent_budget: eval_result.cost(),
261 logs,
262 assertion: self.assertion,
263 }
264 }
265}
266
267#[derive(Debug, Clone)]
270pub struct PropertyTest {
271 pub input_path: PathBuf,
272 pub module: String,
273 pub name: String,
274 pub on_test_failure: OnTestFailure,
275 pub program: Program<Name>,
276 pub fuzzer: Fuzzer<Name>,
277}
278
279unsafe impl Send for PropertyTest {}
280
281#[derive(Debug, Clone)]
282pub struct Fuzzer<T> {
283 pub program: Program<T>,
284
285 pub type_info: Rc<Type>,
286
287 pub stripped_type_info: Rc<Type>,
291}
292
293#[derive(Debug, Clone, thiserror::Error, miette::Diagnostic)]
294#[error("Fuzzer exited unexpectedly: {uplc_error}.")]
295pub struct FuzzerError {
296 logs: Vec<String>,
297 uplc_error: uplc::machine::Error,
298}
299
300#[derive(Debug, Clone)]
301pub enum Event {
302 Simplifying {
303 choices: usize,
304 },
305 Simplified {
306 #[cfg(not(target_family = "wasm"))]
307 duration: Duration,
308 #[cfg(target_family = "wasm")]
309 duration: (),
310 steps: usize,
311 },
312}
313
314impl Display for Event {
315 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::result::Result<(), std::fmt::Error> {
316 match self {
317 Event::Simplifying { choices } => f.write_str(&format!(
318 "{} {}",
319 " Simplifying"
320 .if_supports_color(Stderr, |s| s.bold())
321 .if_supports_color(Stderr, |s| s.purple()),
322 format!("counterexample from {choices} choices")
323 .if_supports_color(Stderr, |s| s.bold()),
324 )),
325 #[cfg(target_family = "wasm")]
326 Event::Simplified { steps, .. } => f.write_str(&format!(
327 "{} {}",
328 " Simplified"
329 .if_supports_color(Stderr, |s| s.bold())
330 .if_supports_color(Stderr, |s| s.purple()),
331 format!("counterexample after {steps} steps",)
332 .if_supports_color(Stderr, |s| s.bold()),
333 )),
334 #[cfg(not(target_family = "wasm"))]
335 Event::Simplified { duration, steps } => f.write_str(&format!(
336 "{} {}",
337 " Simplified"
338 .if_supports_color(Stderr, |s| s.bold())
339 .if_supports_color(Stderr, |s| s.purple()),
340 format!(
341 "counterexample in {} after {steps} steps",
342 if duration.as_secs() == 0 {
343 format!("{}ms", duration.as_millis())
344 } else {
345 format!("{}s", duration.as_secs())
346 }
347 )
348 .if_supports_color(Stderr, |s| s.bold()),
349 )),
350 }
351 }
352}
353
354impl PropertyTest {
355 pub const DEFAULT_MAX_SUCCESS: usize = 100;
356
357 pub fn run(
360 self,
361 seed: u32,
362 n: usize,
363 plutus_version: &PlutusVersion,
364 ) -> PropertyTestResult<PlutusData> {
365 let mut labels = BTreeMap::new();
366 let mut remaining = n;
367
368 let (logs, counterexample, iterations) = match self.run_n_times(
369 &mut remaining,
370 Prng::from_seed(seed),
371 &mut labels,
372 plutus_version,
373 ) {
374 Ok(None) => (Vec::new(), Ok(None), n),
375 Ok(Some(counterexample)) => (
376 self.eval(&counterexample.value, plutus_version).logs(),
377 Ok(Some(counterexample.value)),
378 n - remaining,
379 ),
380 Err(FuzzerError { logs, uplc_error }) => (logs, Err(uplc_error), n - remaining + 1),
381 };
382
383 PropertyTestResult {
384 test: self,
385 counterexample,
386 iterations,
387 labels,
388 logs,
389 }
390 }
391
392 pub fn run_n_times<'a>(
393 &'a self,
394 remaining: &mut usize,
395 initial_prng: Prng,
396 labels: &mut BTreeMap<String, usize>,
397 plutus_version: &'a PlutusVersion,
398 ) -> Result<Option<Counterexample<'a>>, FuzzerError> {
399 let mut prng = initial_prng;
400 let mut counterexample = None;
401
402 while *remaining > 0 && counterexample.is_none() {
403 (prng, counterexample) = self.run_once(prng, labels, plutus_version)?;
404 *remaining -= 1;
405 }
406
407 Ok(counterexample)
408 }
409
410 fn run_once<'a>(
411 &'a self,
412 prng: Prng,
413 labels: &mut BTreeMap<String, usize>,
414 plutus_version: &'a PlutusVersion,
415 ) -> Result<(Prng, Option<Counterexample<'a>>), FuzzerError> {
416 use OnTestFailure::*;
417
418 let (next_prng, value) = prng
419 .sample(&self.fuzzer.program)?
420 .expect("A seeded PRNG returned 'None' which indicates a fuzzer is ill-formed and implemented wrongly; please contact library's authors.");
421
422 let result = self.eval(&value, plutus_version);
423
424 for label in result.labels() {
425 labels
429 .entry(label)
430 .and_modify(|count| *count += 1)
431 .or_insert(1);
432 }
433
434 let is_failure = result.failed(true, &plutus_version.into());
435
436 let is_success = !is_failure;
437
438 let keep_counterexample = match self.on_test_failure {
439 FailImmediately | SucceedImmediately => is_failure,
440 SucceedEventually => is_success,
441 };
442
443 if keep_counterexample {
444 let mut counterexample = Counterexample {
445 value,
446 choices: next_prng.choices(),
447 cache: Cache::new(|choices| {
448 match Prng::from_choices(choices).sample(&self.fuzzer.program) {
449 Err(..) => Status::Invalid,
450 Ok(None) => Status::Invalid,
451 Ok(Some((_, value))) => {
452 let is_failure = self
453 .eval(&value, plutus_version)
454 .failed(true, &plutus_version.into());
455
456 match self.on_test_failure {
457 FailImmediately | SucceedImmediately => {
458 if is_failure {
459 Status::Keep(value)
460 } else {
461 Status::Ignore
462 }
463 }
464
465 SucceedEventually => {
466 if is_failure {
467 Status::Ignore
468 } else {
469 Status::Keep(value)
470 }
471 }
472 }
473 }
474 }
475 }),
476 };
477
478 if !counterexample.choices.is_empty() {
479 counterexample.simplify();
480 }
481
482 Ok((next_prng, Some(counterexample)))
483 } else {
484 Ok((next_prng, None))
485 }
486 }
487
488 pub fn eval(&self, value: &PlutusData, plutus_version: &PlutusVersion) -> EvalResult {
489 let program = self.program.apply_data(value.clone());
490
491 Program::<NamedDeBruijn>::try_from(program)
492 .unwrap()
493 .eval_version(ExBudget::max(), &plutus_version.into())
494 }
495}
496
497#[derive(Debug, Clone)]
500pub struct Sampler<T> {
501 pub program: Program<T>,
502
503 pub type_info: Rc<Type>,
504
505 pub stripped_type_info: Rc<Type>,
509}
510
511#[derive(Debug, Clone, thiserror::Error, miette::Diagnostic)]
512pub enum BenchmarkError {
513 #[error("Sampler exited unexpectedly: {uplc_error}.")]
514 SamplerError {
515 logs: Vec<String>,
516 uplc_error: uplc::machine::Error,
517 },
518 #[error("Bench exited unexpectedly: {uplc_error}.")]
519 BenchError {
520 logs: Vec<String>,
521 uplc_error: uplc::machine::Error,
522 },
523}
524
525impl BenchmarkError {
526 pub fn logs(&self) -> &[String] {
527 match self {
528 BenchmarkError::SamplerError { logs, .. } | BenchmarkError::BenchError { logs, .. } => {
529 logs.as_slice()
530 }
531 }
532 }
533}
534
535#[derive(Debug, Clone)]
536pub struct Benchmark {
537 pub input_path: PathBuf,
538 pub module: String,
539 pub name: String,
540 pub on_test_failure: OnTestFailure,
541 pub program: Program<Name>,
542 pub sampler: Sampler<Name>,
543}
544
545unsafe impl Send for Benchmark {}
546
547impl Benchmark {
548 pub const DEFAULT_MAX_SIZE: usize = 30;
549
550 pub fn run(
551 self,
552 seed: u32,
553 max_size: usize,
554 plutus_version: &PlutusVersion,
555 ) -> BenchmarkResult {
556 let mut measures = Vec::with_capacity(max_size);
557 let mut prng = Prng::from_seed(seed);
558 let mut error = None;
559 let mut size = 0;
560
561 while error.is_none() && max_size >= size {
562 let fuzzer = self
563 .sampler
564 .program
565 .apply_term(&Term::Constant(Constant::Integer(size.into()).into()));
566
567 match prng.sample(&fuzzer) {
568 Ok(None) => {
569 panic!(
570 "A seeded PRNG returned 'None' which indicates a sampler is ill-formed and implemented wrongly; please contact library's authors."
571 );
572 }
573
574 Ok(Some((new_prng, value))) => {
575 prng = new_prng;
576 let result = self.eval(&value, plutus_version);
577 match result.result() {
578 Ok(_) => measures.push((size, result.cost())),
579 Err(uplc_error) => {
580 error = Some(BenchmarkError::BenchError {
581 logs: result.logs(),
582 uplc_error,
583 });
584 }
585 }
586 }
587
588 Err(FuzzerError { logs, uplc_error }) => {
589 error = Some(BenchmarkError::SamplerError { logs, uplc_error });
590 }
591 }
592
593 size += 1;
594 }
595
596 BenchmarkResult {
597 bench: self,
598 measures,
599 error,
600 }
601 }
602
603 pub fn eval(&self, value: &PlutusData, plutus_version: &PlutusVersion) -> EvalResult {
604 let program = self.program.apply_data(value.clone());
605
606 Program::<NamedDeBruijn>::try_from(program)
607 .unwrap()
608 .eval_version(ExBudget::max(), &plutus_version.into())
609 }
610}
611
612#[derive(Debug)]
631pub enum Prng {
632 Seeded { choices: Vec<u8>, uplc: PlutusData },
633 Replayed { choices: Vec<u8>, uplc: PlutusData },
634}
635
636impl Prng {
637 const SEEDED: u64 = 0;
639 const REPLAYED: u64 = 1;
641
642 const SOME: u64 = 0;
644 const NONE: u64 = 1;
646
647 pub fn uplc(&self) -> PlutusData {
648 match self {
649 Prng::Seeded { uplc, .. } => uplc.clone(),
650 Prng::Replayed { uplc, .. } => uplc.clone(),
651 }
652 }
653
654 pub fn choices(&self) -> Vec<u8> {
655 match self {
656 Prng::Seeded { choices, .. } => {
657 let mut choices = choices.to_vec();
658 choices.reverse();
659 choices
660 }
661 Prng::Replayed { choices, .. } => choices.to_vec(),
662 }
663 }
664
665 pub fn from_seed(seed: u32) -> Prng {
667 let mut digest = [0u8; 32];
668 let mut context = Blake2b::new(32);
669 context.input(&seed.to_be_bytes()[..]);
670 context.result(&mut digest);
671
672 Prng::Seeded {
673 choices: vec![],
674 uplc: Data::constr(
675 Prng::SEEDED,
676 vec![
677 Data::bytestring(digest.to_vec()), Data::bytestring(vec![]), ],
680 ),
681 }
682 }
683
684 pub fn from_choices(choices: &[u8]) -> Prng {
686 Prng::Replayed {
687 uplc: Data::constr(
688 Prng::REPLAYED,
689 vec![
690 Data::integer(choices.len().into()),
691 Data::bytestring(choices.iter().rev().cloned().collect::<Vec<_>>()),
692 ],
693 ),
694 choices: choices.to_vec(),
695 }
696 }
697
698 pub fn sample(
700 &self,
701 fuzzer: &Program<Name>,
702 ) -> Result<Option<(Prng, PlutusData)>, FuzzerError> {
703 let program = Program::<NamedDeBruijn>::try_from(fuzzer.apply_data(self.uplc())).unwrap();
704 let result = program.eval(ExBudget::max());
705 result
706 .result()
707 .map_err(|uplc_error| FuzzerError {
708 logs: result.logs(),
709 uplc_error,
710 })
711 .map(Prng::from_result)
712 }
713
714 pub fn from_result(result: Term<NamedDeBruijn>) -> Option<(Self, PlutusData)> {
725 fn as_prng(cst: &PlutusData) -> Prng {
727 if let PlutusData::Constr(Constr { tag, fields, .. }) = cst {
728 if *tag == 121 + Prng::SEEDED
729 && let [
730 PlutusData::BoundedBytes(bytes),
731 PlutusData::BoundedBytes(choices),
732 ] = &fields[..]
733 {
734 return Prng::Seeded {
735 choices: choices.to_vec(),
736 uplc: Data::constr(
737 Prng::SEEDED,
738 vec![
739 PlutusData::BoundedBytes(bytes.to_owned()),
740 PlutusData::BoundedBytes(vec![].into()),
743 ],
744 ),
745 };
746 }
747
748 if *tag == 121 + Prng::REPLAYED
749 && let [PlutusData::BigInt(..), PlutusData::BoundedBytes(choices)] = &fields[..]
750 {
751 return Prng::Replayed {
752 choices: choices.to_vec(),
753 uplc: cst.clone(),
754 };
755 }
756 }
757
758 unreachable!("malformed Prng: {cst:#?}")
759 }
760
761 if let Term::Constant(rc) = &result
762 && let Constant::Data(PlutusData::Constr(Constr { tag, fields, .. })) = &rc.borrow()
763 {
764 if *tag == 121 + Prng::SOME
765 && let [PlutusData::Array(elems)] = &fields[..]
766 && let [new_seed, value] = &elems[..]
767 {
768 return Some((as_prng(new_seed), value.clone()));
769 }
770
771 if *tag == 121 + Prng::NONE {
776 return None;
777 }
778 }
779
780 unreachable!("Fuzzer yielded a malformed result? {result:#?}")
781 }
782}
783
784pub struct Counterexample<'a> {
791 pub value: PlutusData,
792 pub choices: Vec<u8>,
793 pub cache: Cache<'a, PlutusData>,
794}
795
796impl Counterexample<'_> {
797 fn consider(&mut self, choices: &[u8]) -> bool {
798 if choices == self.choices {
799 return true;
800 }
801
802 match self.cache.get(choices) {
803 Status::Invalid | Status::Ignore => false,
804 Status::Keep(value) => {
805 if choices.len() <= self.choices.len() || choices < &self.choices[..] {
808 self.value = value;
809 self.choices = choices.to_vec();
810 true
811 } else {
812 false
813 }
814 }
815 }
816 }
817
818 pub fn simplify(&mut self) {
839 let mut prev;
840
841 let mut steps = 0;
842
843 #[cfg(not(target_family = "wasm"))]
844 let now = std::time::Instant::now();
845
846 eprintln!(
847 "{}",
848 Event::Simplifying {
849 choices: self.choices.len(),
850 }
851 );
852
853 loop {
854 prev = self.choices.clone();
855
856 let mut k = 8;
861 while k > 0 {
862 let (mut i, mut underflow) = if self.choices.len() < k {
863 (0, true)
864 } else {
865 (self.choices.len() - k, false)
866 };
867
868 while !underflow {
869 if i >= self.choices.len() {
870 (i, underflow) = i.overflowing_sub(1);
871 steps += 1;
872 continue;
873 }
874
875 let j = i + k;
876
877 let mut choices = [
878 &self.choices[..i],
879 if j < self.choices.len() {
880 &self.choices[j..]
881 } else {
882 &[]
883 },
884 ]
885 .concat();
886
887 if !self.consider(&choices) {
888 if i > 0 && choices[i - 1] > 0 {
894 choices[i - 1] -= 1;
895 if self.consider(&choices) {
896 i += 1;
897 };
898 }
899
900 (i, underflow) = i.overflowing_sub(1);
901 }
902
903 steps += 1;
904 }
905
906 k /= 2
907 }
908
909 if !self.choices.is_empty() {
910 let mut k = 8;
914 while k > 1 {
915 let mut i = self.choices.len();
916 while i >= k {
917 steps += 1;
918 let ivs = (i - k..i).map(|j| (j, 0)).collect::<Vec<_>>();
919 i -= if self.replace(ivs) { k } else { 1 }
920 }
921 k /= 2
922 }
923
924 let (mut i, mut underflow) = (self.choices.len() - 1, false);
928 while !underflow {
929 steps += 1;
930 self.binary_search_replace(0, self.choices[i], |v| vec![(i, v)]);
931 (i, underflow) = i.overflowing_sub(1);
932 }
933
934 let mut k = 8;
936 while k > 1 {
937 let mut i = self.choices.len() - 1;
938 while i >= k {
939 steps += 1;
940 let (from, to) = (i - k, i);
941 self.replace(
942 (from..to)
943 .zip(self.choices[from..to].iter().cloned().sorted())
944 .collect(),
945 );
946 i -= 1;
947 }
948 k /= 2
949 }
950
951 for k in [2, 1] {
956 let mut j = self.choices.len() - 1;
957 while j >= k {
958 let i = j - k;
959
960 if self.choices[i] > self.choices[j] {
962 self.replace(vec![(i, self.choices[j]), (j, self.choices[i])]);
963 }
964
965 let iv = self.choices[i];
966 let jv = self.choices[j];
967
968 if iv > 0 && jv <= u8::MAX - iv {
970 self.binary_search_replace(0, iv, |v| vec![(i, v), (j, jv + (iv - v))]);
971 }
972
973 steps += 1;
974
975 j -= 1
976 }
977 }
978 }
979
980 if prev.as_slice() == self.choices.as_slice() {
983 break;
984 }
985 }
986
987 eprintln!(
988 "{}",
989 Event::Simplified {
990 #[cfg(not(target_family = "wasm"))]
991 duration: now.elapsed(),
992 #[cfg(target_family = "wasm")]
993 duration: (),
994 steps,
995 }
996 );
997 }
998
999 fn binary_search_replace<F>(&mut self, lo: u8, hi: u8, f: F) -> u8
1002 where
1003 F: Fn(u8) -> Vec<(usize, u8)>,
1004 {
1005 if self.replace(f(lo)) {
1006 return lo;
1007 }
1008
1009 let mut lo = lo;
1010 let mut hi = hi;
1011
1012 while lo + 1 < hi {
1013 let mid = lo + (hi - lo) / 2;
1014 if self.replace(f(mid)) {
1015 hi = mid;
1016 } else {
1017 lo = mid;
1018 }
1019 }
1020
1021 hi
1022 }
1023
1024 fn replace(&mut self, ivs: Vec<(usize, u8)>) -> bool {
1027 let mut choices = self.choices.clone();
1028
1029 for (i, v) in ivs {
1030 if i >= choices.len() {
1031 return false;
1032 }
1033 choices[i] = v;
1034 }
1035
1036 self.consider(&choices)
1037 }
1038}
1039
1040pub struct Cache<'a, T> {
1052 db: PatriciaMap<Status<T>>,
1053 #[allow(clippy::type_complexity)]
1054 run: Box<dyn Fn(&[u8]) -> Status<T> + 'a>,
1055}
1056
1057#[derive(Debug, Clone, Copy, PartialEq)]
1058pub enum Status<T> {
1059 Keep(T),
1060 Ignore,
1061 Invalid,
1062}
1063
1064impl<'a, T> Cache<'a, T>
1065where
1066 T: PartialEq + Clone,
1067{
1068 pub fn new<F>(run: F) -> Cache<'a, T>
1069 where
1070 F: Fn(&[u8]) -> Status<T> + 'a,
1071 {
1072 Cache {
1073 db: PatriciaMap::new(),
1074 run: Box::new(run),
1075 }
1076 }
1077
1078 pub fn size(&self) -> usize {
1079 self.db.len()
1080 }
1081
1082 pub fn get(&mut self, choices: &[u8]) -> Status<T> {
1083 if let Some((prefix, status)) = self.db.get_longest_common_prefix(choices) {
1084 let status = status.clone();
1085 if status != Status::Invalid || prefix == choices {
1086 return status;
1087 }
1088 }
1089
1090 let status = self.run.deref()(choices);
1091
1092 if status != Status::Invalid {
1098 let keys = self
1099 .db
1100 .iter_prefix(choices)
1101 .map(|(k, _)| k)
1102 .collect::<Vec<_>>();
1103 for k in keys {
1104 self.db.remove(k);
1105 }
1106 }
1107
1108 self.db.insert(choices, status.clone());
1109
1110 status
1111 }
1112}
1113
1114#[derive(Debug, Clone)]
1121pub enum TestResult<U, T> {
1122 UnitTestResult(UnitTestResult<U>),
1123 PropertyTestResult(PropertyTestResult<T>),
1124 BenchmarkResult(BenchmarkResult),
1125}
1126
1127unsafe impl<U, T> Send for TestResult<U, T> {}
1128
1129impl TestResult<(Constant, Rc<Type>), PlutusData> {
1130 pub fn reify(
1131 self,
1132 data_types: &IndexMap<&DataTypeKey, &TypedDataType>,
1133 ) -> TestResult<UntypedExpr, UntypedExpr> {
1134 match self {
1135 TestResult::UnitTestResult(test) => TestResult::UnitTestResult(test.reify(data_types)),
1136 TestResult::PropertyTestResult(test) => {
1137 TestResult::PropertyTestResult(test.reify(data_types))
1138 }
1139 TestResult::BenchmarkResult(result) => TestResult::BenchmarkResult(result),
1140 }
1141 }
1142}
1143
1144impl<U, T> TestResult<U, T> {
1145 pub fn is_success(&self) -> bool {
1146 match self {
1147 TestResult::UnitTestResult(UnitTestResult { success, .. }) => *success,
1148 TestResult::PropertyTestResult(PropertyTestResult {
1149 counterexample: Err(..),
1150 ..
1151 }) => false,
1152 TestResult::PropertyTestResult(PropertyTestResult {
1153 counterexample: Ok(counterexample),
1154 test,
1155 ..
1156 }) => match test.on_test_failure {
1157 OnTestFailure::FailImmediately | OnTestFailure::SucceedEventually => {
1158 counterexample.is_none()
1159 }
1160 OnTestFailure::SucceedImmediately => counterexample.is_some(),
1161 },
1162 TestResult::BenchmarkResult(BenchmarkResult { error, .. }) => error.is_none(),
1163 }
1164 }
1165
1166 pub fn module(&self) -> &str {
1167 match self {
1168 TestResult::UnitTestResult(UnitTestResult { test, .. }) => test.module.as_str(),
1169 TestResult::PropertyTestResult(PropertyTestResult { test, .. }) => test.module.as_str(),
1170 TestResult::BenchmarkResult(BenchmarkResult { bench, .. }) => bench.module.as_str(),
1171 }
1172 }
1173
1174 pub fn title(&self) -> &str {
1175 match self {
1176 TestResult::UnitTestResult(UnitTestResult { test, .. }) => test.name.as_str(),
1177 TestResult::PropertyTestResult(PropertyTestResult { test, .. }) => test.name.as_str(),
1178 TestResult::BenchmarkResult(BenchmarkResult { bench, .. }) => bench.name.as_str(),
1179 }
1180 }
1181
1182 pub fn logs(&self) -> &[String] {
1183 match self {
1184 TestResult::UnitTestResult(UnitTestResult { logs, .. })
1185 | TestResult::PropertyTestResult(PropertyTestResult { logs, .. }) => logs,
1186 TestResult::BenchmarkResult(BenchmarkResult { error, .. }) => {
1187 error.as_ref().map(|e| e.logs()).unwrap_or_default()
1188 }
1189 }
1190 }
1191}
1192
1193#[derive(Debug, Clone)]
1194pub struct UnitTestResult<T> {
1195 pub success: bool,
1196 pub spent_budget: ExBudget,
1197 pub logs: Vec<String>,
1198 pub test: UnitTest,
1199 pub assertion: Option<Assertion<T>>,
1200}
1201
1202unsafe impl<T> Send for UnitTestResult<T> {}
1203
1204impl UnitTestResult<(Constant, Rc<Type>)> {
1205 pub fn reify(
1206 self,
1207 data_types: &IndexMap<&DataTypeKey, &TypedDataType>,
1208 ) -> UnitTestResult<UntypedExpr> {
1209 UnitTestResult {
1210 success: self.success,
1211 spent_budget: self.spent_budget,
1212 logs: self.logs,
1213 test: self.test,
1214 assertion: self.assertion.and_then(|assertion| {
1215 if self.success {
1218 return None;
1219 }
1220
1221 Some(Assertion {
1222 bin_op: assertion.bin_op,
1223 head: assertion.head.map(|(cst, tipo)| {
1224 UntypedExpr::reify_constant(data_types, cst, tipo)
1225 .expect("failed to reify assertion operand?")
1226 }),
1227 tail: assertion.tail.map(|xs| {
1228 xs.mapped(|(cst, tipo)| {
1229 UntypedExpr::reify_constant(data_types, cst, tipo)
1230 .expect("failed to reify assertion operand?")
1231 })
1232 }),
1233 })
1234 }),
1235 }
1236 }
1237}
1238
1239#[derive(Debug, Clone)]
1240pub struct PropertyTestResult<T> {
1241 pub test: PropertyTest,
1242 pub counterexample: Result<Option<T>, uplc::machine::Error>,
1243 pub iterations: usize,
1244 pub labels: BTreeMap<String, usize>,
1245 pub logs: Vec<String>,
1246}
1247
1248unsafe impl<T> Send for PropertyTestResult<T> {}
1249
1250impl PropertyTestResult<PlutusData> {
1251 pub fn reify(
1252 self,
1253 data_types: &IndexMap<&DataTypeKey, &TypedDataType>,
1254 ) -> PropertyTestResult<UntypedExpr> {
1255 PropertyTestResult {
1256 counterexample: self.counterexample.map(|ok| {
1257 ok.map(|counterexample| {
1258 UntypedExpr::reify_data(
1259 data_types,
1260 counterexample,
1261 self.test.fuzzer.type_info.clone(),
1262 )
1263 .expect("failed to reify counterexample?")
1264 })
1265 }),
1266 iterations: self.iterations,
1267 test: self.test,
1268 labels: self.labels,
1269 logs: self.logs,
1270 }
1271 }
1272}
1273
1274#[derive(Debug, Clone)]
1275pub struct Assertion<T> {
1276 pub bin_op: BinOp,
1277 pub head: Result<T, ()>,
1278 pub tail: Result<Vec1<T>, ()>,
1279}
1280
1281impl TryFrom<TypedExpr> for Assertion<TypedExpr> {
1282 type Error = ();
1283
1284 fn try_from(body: TypedExpr) -> Result<Self, Self::Error> {
1285 match body {
1286 TypedExpr::BinOp {
1287 name,
1288 tipo,
1289 left,
1290 right,
1291 ..
1292 } if tipo == Type::bool() => {
1293 match (*right).clone().try_into() {
1295 Ok(Assertion {
1296 bin_op,
1297 head: Ok(head),
1298 tail: Ok(tail),
1299 ..
1300 }) if bin_op == name => {
1301 let mut both = vec1![head];
1302 both.extend(tail);
1303 Ok(Assertion {
1304 bin_op: name,
1305 head: Ok(*left),
1306 tail: Ok(both),
1307 })
1308 }
1309 _ => Ok(Assertion {
1310 bin_op: name,
1311 head: Ok(*left),
1312 tail: Ok(vec1![*right]),
1313 }),
1314 }
1315 }
1316
1317 TypedExpr::If {
1319 branches,
1320 final_else,
1321 ..
1322 } => {
1323 if let [
1324 IfBranch {
1325 condition, body, ..
1326 },
1327 ] = &branches[..]
1328 {
1329 let then_is_true = match body {
1330 TypedExpr::Var {
1331 name, constructor, ..
1332 } => name == "True" && constructor.tipo == Type::bool(),
1333 _ => false,
1334 };
1335
1336 let else_is_wrapped_false = match *final_else {
1337 TypedExpr::Trace { then, .. } => match *then {
1338 TypedExpr::Var {
1339 name, constructor, ..
1340 } => name == "False" && constructor.tipo == Type::bool(),
1341 _ => false,
1342 },
1343 _ => false,
1344 };
1345
1346 if then_is_true && else_is_wrapped_false {
1347 return condition.to_owned().try_into();
1348 }
1349 }
1350
1351 Err(())
1352 }
1353
1354 TypedExpr::Trace { then, .. } => (*then).try_into(),
1355
1356 TypedExpr::Sequence { expressions, .. } | TypedExpr::Pipeline { expressions, .. } => {
1357 if let Ok(Assertion {
1358 bin_op,
1359 head: Ok(head),
1360 tail: Ok(tail),
1361 }) = expressions.last().unwrap().to_owned().try_into()
1362 {
1363 let replace = |expr| {
1364 let mut expressions = expressions.clone();
1365 expressions.pop();
1366 expressions.push(expr);
1367 TypedExpr::Sequence {
1368 expressions,
1369 location: Span::empty(),
1370 }
1371 };
1372
1373 Ok(Assertion {
1374 bin_op,
1375 head: Ok(replace(head)),
1376 tail: Ok(tail.mapped(replace)),
1377 })
1378 } else {
1379 Err(())
1380 }
1381 }
1382
1383 TypedExpr::Call {
1384 args,
1385 location,
1386 fun,
1387 ..
1388 } => {
1389 if let Some((last_arg, first_args)) = args.split_last()
1391 && let TypedExpr::Fn {
1392 body: last_arg_body,
1393 location: last_arg_location,
1394 tipo: last_arg_tipo,
1395 is_capture: last_arg_is_capture,
1396 return_annotation: last_arg_return_annotation,
1397 args: last_arg_args,
1398 } = &last_arg.value
1399 {
1400 let Assertion { bin_op, head, tail } = Self::try_from(*last_arg_body.clone())?;
1401
1402 let new_callback_tipo = |body: &TypedExpr| -> Rc<Type> {
1403 match last_arg_tipo.as_ref() {
1404 Type::Fn {
1405 args,
1406 ret: _ret,
1407 alias,
1408 } => Rc::new(Type::Fn {
1409 args: args.clone(),
1410 ret: body.tipo(), alias: alias.clone(),
1412 }),
1413 Type::App { .. }
1414 | Type::Var { .. }
1415 | Type::Pair { .. }
1416 | Type::Tuple { .. } => {
1417 unreachable!(
1418 "guard above on 'last_arg.value' guarantees that type is necessarily a function (Fn)"
1419 )
1420 }
1421 }
1422 };
1423
1424 let new_fun = |body: &TypedExpr, callback_tipo: Rc<Type>| -> Box<TypedExpr> {
1425 let fun_tipo = match fun.as_ref().tipo().as_ref() {
1426 Type::Fn {
1427 args,
1428 alias,
1429 ret: _,
1430 } => {
1431 let mut args = args
1432 .split_last()
1433 .expect("function has at least one arg")
1434 .1
1435 .to_vec();
1436 args.push(callback_tipo); Rc::new(Type::Fn {
1438 args,
1439 ret: body.tipo(), alias: alias.clone(),
1441 })
1442 }
1443 Type::App { .. }
1444 | Type::Var { .. }
1445 | Type::Pair { .. }
1446 | Type::Tuple { .. } => {
1447 unreachable!(
1448 "guard above on 'last_arg.value' guarantees that type is necessarily a function (Fn)"
1449 )
1450 }
1451 };
1452
1453 let mut fun = fun.clone();
1454 fun.replace_type(fun_tipo);
1455
1456 fun
1457 };
1458
1459 let new_args =
1460 |body: TypedExpr, callback_tipo: Rc<Type>| -> Vec<CallArg<TypedExpr>> {
1461 let mut args = first_args.to_vec();
1462 args.push(CallArg {
1463 label: last_arg.label.clone(),
1464 location: last_arg.location,
1465 value: TypedExpr::Fn {
1466 location: *last_arg_location,
1467 tipo: callback_tipo.clone(),
1468 is_capture: *last_arg_is_capture,
1469 return_annotation: last_arg_return_annotation.clone(),
1470 args: last_arg_args.clone(),
1471 body: Box::new(body),
1472 },
1473 });
1474 args
1475 };
1476
1477 return Ok(Assertion {
1478 bin_op,
1479 head: head.map(|body| {
1480 let callback_tipo = new_callback_tipo(&body);
1481 TypedExpr::Call {
1482 location,
1483 tipo: body.tipo(),
1484 fun: new_fun(&body, callback_tipo.clone()),
1485 args: new_args(body, callback_tipo.clone()),
1486 }
1487 }),
1488 tail: tail.map(|tail| {
1489 tail.mapped(|body| {
1490 let callback_tipo = new_callback_tipo(&body);
1491 TypedExpr::Call {
1492 location,
1493 tipo: body.tipo(),
1494 fun: new_fun(&body, callback_tipo.clone()),
1495 args: new_args(body, callback_tipo.clone()),
1496 }
1497 })
1498 }),
1499 });
1500 }
1501 Err(())
1502 }
1503
1504 _ => Err(()),
1505 }
1506 }
1507}
1508
1509pub struct AssertionStyleOptions<'a> {
1510 red: Box<dyn Fn(String) -> String + 'a>,
1511 bold: Box<dyn Fn(String) -> String + 'a>,
1512}
1513
1514impl<'a> AssertionStyleOptions<'a> {
1515 pub fn new(stream: Option<&'a Stream>) -> Self {
1516 match stream {
1517 Some(stream) => Self {
1518 red: Box::new(|s| {
1519 s.if_supports_color(stream.to_owned(), |s| s.red())
1520 .to_string()
1521 }),
1522 bold: Box::new(|s| {
1523 s.if_supports_color(stream.to_owned(), |s| s.bold())
1524 .to_string()
1525 }),
1526 },
1527 None => Self {
1528 red: Box::new(|s| s),
1529 bold: Box::new(|s| s),
1530 },
1531 }
1532 }
1533}
1534
1535impl Assertion<UntypedExpr> {
1536 #[allow(clippy::just_underscores_and_digits)]
1537 pub fn to_string(&self, expect_failure: bool, style: &AssertionStyleOptions) -> String {
1538 let red = |s: &str| style.red.as_ref()(s.to_string());
1539 let x = |s: &str| style.red.as_ref()(style.bold.as_ref()(format!("× {s}")));
1540
1541 if self.head.is_err() {
1543 return x("program failed");
1544 }
1545
1546 if self.tail.is_err() {
1548 return x("program failed");
1549 }
1550
1551 fn fmt_side(side: &UntypedExpr, red: &dyn Fn(&str) -> String) -> String {
1552 let __ = red("│");
1553
1554 Formatter::new()
1555 .expr(side, false)
1556 .to_pretty_string(60)
1557 .lines()
1558 .map(|line| format!("{__} {line}"))
1559 .collect::<Vec<String>>()
1560 .join("\n")
1561 }
1562
1563 let left = fmt_side(self.head.as_ref().unwrap(), &red);
1564
1565 let tail = self.tail.as_ref().unwrap();
1566
1567 let right = fmt_side(tail.first(), &red);
1568
1569 format!(
1570 "{}{}{}",
1571 x("expected"),
1572 if expect_failure && self.bin_op == BinOp::Or {
1573 x(" neither\n")
1574 } else {
1575 "\n".to_string()
1576 },
1577 if expect_failure {
1578 match self.bin_op {
1579 BinOp::And => [
1580 left,
1581 x("and"),
1582 [
1583 tail.mapped_ref(|s| fmt_side(s, &red))
1584 .join(format!("\n{}\n", x("and")).as_str()),
1585 if tail.len() > 1 {
1586 x("to not all be true")
1587 } else {
1588 x("to not both be true")
1589 },
1590 ]
1591 .join("\n"),
1592 ],
1593 BinOp::Or => [
1594 left,
1595 x("nor"),
1596 [
1597 tail.mapped_ref(|s| fmt_side(s, &red))
1598 .join(format!("\n{}\n", x("nor")).as_str()),
1599 x("to be true"),
1600 ]
1601 .join("\n"),
1602 ],
1603 BinOp::Eq => [left, x("to not equal"), right],
1604 BinOp::NotEq => [left, x("to not be different"), right],
1605 BinOp::LtInt => [left, x("to not be lower than"), right],
1606 BinOp::LtEqInt => [left, x("to not be lower than or equal to"), right],
1607 BinOp::GtInt => [left, x("to not be greater than"), right],
1608 BinOp::GtEqInt => [left, x("to not be greater than or equal to"), right],
1609 _ => unreachable!("unexpected non-boolean binary operator in assertion?"),
1610 }
1611 .join("\n")
1612 } else {
1613 match self.bin_op {
1614 BinOp::And => [
1615 left,
1616 x("and"),
1617 [
1618 tail.mapped_ref(|s| fmt_side(s, &red))
1619 .join(format!("\n{}\n", x("and")).as_str()),
1620 if tail.len() > 1 {
1621 x("to all be true")
1622 } else {
1623 x("to both be true")
1624 },
1625 ]
1626 .join("\n"),
1627 ],
1628 BinOp::Or => [
1629 left,
1630 x("or"),
1631 [
1632 tail.mapped_ref(|s| fmt_side(s, &red))
1633 .join(format!("\n{}\n", x("or")).as_str()),
1634 x("to be true"),
1635 ]
1636 .join("\n"),
1637 ],
1638 BinOp::Eq => [left, x("to equal"), right],
1639 BinOp::NotEq => [left, x("to not equal"), right],
1640 BinOp::LtInt => [left, x("to be lower than"), right],
1641 BinOp::LtEqInt => [left, x("to be lower than or equal to"), right],
1642 BinOp::GtInt => [left, x("to be greater than"), right],
1643 BinOp::GtEqInt => [left, x("to be greater than or equal to"), right],
1644 _ => unreachable!("unexpected non-boolean binary operator in assertion?"),
1645 }
1646 .join("\n")
1647 }
1648 )
1649 }
1650}
1651
1652#[derive(Debug, Clone)]
1653pub struct BenchmarkResult {
1654 pub bench: Benchmark,
1655 pub measures: Vec<(usize, ExBudget)>,
1656 pub error: Option<BenchmarkError>,
1657}
1658
1659unsafe impl Send for BenchmarkResult {}
1660unsafe impl Sync for BenchmarkResult {}
1661
1662#[cfg(test)]
1663mod test {
1664 use super::*;
1665
1666 #[test]
1667 fn test_cache() {
1668 let called = std::cell::RefCell::new(0);
1669
1670 let mut cache = Cache::new(|choices| {
1671 called.replace_with(|n| *n + 1);
1672
1673 match choices {
1674 [0, 0, 0] => Status::Keep(true),
1675 _ => {
1676 if choices.len() <= 2 {
1677 Status::Invalid
1678 } else {
1679 Status::Ignore
1680 }
1681 }
1682 }
1683 });
1684
1685 assert_eq!(cache.get(&[1, 1]), Status::Invalid); assert_eq!(cache.get(&[1, 1, 2, 3]), Status::Ignore); assert_eq!(cache.get(&[1, 1, 2]), Status::Ignore); assert_eq!(cache.get(&[1, 1, 2, 2]), Status::Ignore); assert_eq!(cache.get(&[1, 1, 2, 1]), Status::Ignore); assert_eq!(cache.get(&[0, 1, 2]), Status::Ignore); assert_eq!(cache.get(&[0, 0, 0]), Status::Keep(true)); assert_eq!(cache.get(&[0, 0, 0]), Status::Keep(true)); assert_eq!(called.borrow().deref().to_owned(), 5, "execution calls");
1695 assert_eq!(cache.size(), 4, "cache size");
1696 }
1697}