aiken_lang/
test_framework.rs

1use crate::{
2    ast::{BinOp, DataTypeKey, IfBranch, OnTestFailure, Span, TypedArg, TypedDataType, TypedTest},
3    expr::{TypedExpr, UntypedExpr},
4    format::Formatter,
5    gen_uplc::CodeGenerator,
6    plutus_version::PlutusVersion,
7    tipo::{Type, convert_opaque_type},
8};
9use cryptoxide::{blake2b::Blake2b, digest::Digest};
10use indexmap::IndexMap;
11use itertools::Itertools;
12use owo_colors::{OwoColorize, Stream, Stream::Stderr};
13use pallas_primitives::alonzo::{Constr, PlutusData};
14use patricia_tree::PatriciaMap;
15#[cfg(not(target_family = "wasm"))]
16use std::time::Duration;
17use std::{
18    borrow::Borrow,
19    collections::BTreeMap,
20    convert::TryFrom,
21    fmt::{Debug, Display},
22    ops::Deref,
23    path::PathBuf,
24    rc::Rc,
25};
26use uplc::{
27    ast::{Constant, Data, Name, NamedDeBruijn, Program, Term},
28    machine::{cost_model::ExBudget, eval_result::EvalResult},
29};
30use vec1::{Vec1, vec1};
31
32#[derive(Debug, Clone, Copy)]
33pub enum RunnableKind {
34    Test,
35    Bench,
36}
37
38/// ----- Test -----------------------------------------------------------------
39///
40/// Aiken supports two kinds of tests: unit and property. A unit test is a simply
41/// UPLC program which returns must be a lambda that returns a boolean.
42///
43/// A property on the other-hand is a template for generating tests, which is also
44/// a lambda but that takes an extra argument. The argument is generated from a
45/// fuzzer which is meant to yield random values in a pseudo-random (albeit seeded)
46/// sequence. On failures, the value that caused a failure is simplified using an
47/// approach similar to what's described in MiniThesis<https://github.com/DRMacIver/minithesis>,
48/// which is a simplified version of Hypothesis, a property-based testing framework
49/// with integrated shrinking.
50///
51/// Our approach could perhaps be called "microthesis", as it implements a subset of
52/// minithesis. More specifically, we do not currently support pre-conditions, nor
53/// targets.
54///
55#[derive(Debug, Clone)]
56pub enum Test {
57    UnitTest(UnitTest),
58    PropertyTest(PropertyTest),
59    Benchmark(Benchmark),
60}
61
62unsafe impl Send for Test {}
63
64impl Test {
65    pub fn unit_test(
66        generator: &mut CodeGenerator<'_>,
67        test: TypedTest,
68        module_name: String,
69        input_path: PathBuf,
70    ) -> Test {
71        let program = generator.generate_raw(&test.body, &[], &module_name);
72
73        let assertion = match test.body.try_into() {
74            Err(..) => None,
75            Ok(Assertion { bin_op, head, tail }) => {
76                let as_constant = |generator: &mut CodeGenerator<'_>, side| {
77                    Program::<NamedDeBruijn>::try_from(generator.generate_raw(
78                        &side,
79                        &[],
80                        &module_name,
81                    ))
82                    .expect("failed to convert assertion operaand to NamedDeBruijn")
83                    .eval(ExBudget::max())
84                    .unwrap_constant()
85                    .map(|cst| (cst, side.tipo()))
86                };
87
88                // Assertion at this point is evaluated so it's not just a normal assertion
89                Some(Assertion {
90                    bin_op,
91                    head: as_constant(generator, head.expect("cannot be Err at this point")),
92                    tail: tail
93                        .expect("cannot be Err at this point")
94                        .try_mapped(|e| as_constant(generator, e)),
95                })
96            }
97        };
98
99        Test::UnitTest(UnitTest {
100            input_path,
101            module: module_name,
102            name: test.name,
103            program,
104            assertion,
105            on_test_failure: test.on_test_failure,
106        })
107    }
108
109    pub fn property_test(
110        input_path: PathBuf,
111        module: String,
112        name: String,
113        on_test_failure: OnTestFailure,
114        program: Program<Name>,
115        fuzzer: Fuzzer<Name>,
116    ) -> Test {
117        Test::PropertyTest(PropertyTest {
118            input_path,
119            module,
120            name,
121            program,
122            on_test_failure,
123            fuzzer,
124        })
125    }
126
127    pub fn from_function_definition(
128        generator: &mut CodeGenerator<'_>,
129        test: TypedTest,
130        module_name: String,
131        input_path: PathBuf,
132        kind: RunnableKind,
133    ) -> Test {
134        if test.arguments.is_empty() {
135            if matches!(kind, RunnableKind::Bench) {
136                unreachable!("benchmark must have at least one argument");
137            } else {
138                Self::unit_test(generator, test, module_name, input_path)
139            }
140        } else {
141            let parameter = test.arguments.first().unwrap().to_owned();
142
143            let via = parameter.via.clone();
144
145            let type_info = parameter.arg.tipo.clone();
146
147            let stripped_type_info = convert_opaque_type(&type_info, generator.data_types(), true);
148
149            let program = generator.clone().generate_raw(
150                &test.body,
151                &[TypedArg {
152                    tipo: stripped_type_info.clone(),
153                    ..parameter.clone().into()
154                }],
155                &module_name,
156            );
157
158            // NOTE: We need not to pass any parameter to the fuzzer/sampler here because the fuzzer
159            // argument is a Data constructor which needs not any conversion. So we can just safely
160            // apply onto it later.
161            let generator_program = generator.clone().generate_raw(&via, &[], &module_name);
162
163            match kind {
164                RunnableKind::Bench => Test::Benchmark(Benchmark {
165                    input_path,
166                    module: module_name,
167                    name: test.name,
168                    program,
169                    on_test_failure: test.on_test_failure,
170                    sampler: Sampler {
171                        program: generator_program,
172                        type_info,
173                        stripped_type_info,
174                    },
175                }),
176                RunnableKind::Test => Self::property_test(
177                    input_path,
178                    module_name,
179                    test.name,
180                    test.on_test_failure,
181                    program,
182                    Fuzzer {
183                        program: generator_program,
184                        stripped_type_info,
185                        type_info,
186                    },
187                ),
188            }
189        }
190    }
191
192    pub fn run(
193        self,
194        seed: u32,
195        max_success: usize,
196        plutus_version: &PlutusVersion,
197    ) -> TestResult<(Constant, Rc<Type>), PlutusData> {
198        match self {
199            Test::UnitTest(unit_test) => TestResult::UnitTestResult(unit_test.run(plutus_version)),
200            Test::PropertyTest(property_test) => {
201                TestResult::PropertyTestResult(property_test.run(seed, max_success, plutus_version))
202            }
203            Test::Benchmark(benchmark) => {
204                TestResult::BenchmarkResult(benchmark.run(seed, max_success, plutus_version))
205            }
206        }
207    }
208}
209
210/// ----- UnitTest -----------------------------------------------------------------
211///
212#[derive(Debug, Clone)]
213pub struct UnitTest {
214    pub input_path: PathBuf,
215    pub module: String,
216    pub name: String,
217    pub on_test_failure: OnTestFailure,
218    pub program: Program<Name>,
219    pub assertion: Option<Assertion<(Constant, Rc<Type>)>>,
220}
221
222unsafe impl Send for UnitTest {}
223
224impl UnitTest {
225    pub fn run(self, plutus_version: &PlutusVersion) -> UnitTestResult<(Constant, Rc<Type>)> {
226        let eval_result = Program::<NamedDeBruijn>::try_from(self.program.clone())
227            .unwrap()
228            .eval_version(ExBudget::max(), &plutus_version.into());
229
230        let success = !eval_result.failed(match self.on_test_failure {
231            OnTestFailure::SucceedEventually | OnTestFailure::SucceedImmediately => true,
232            OnTestFailure::FailImmediately => false,
233        });
234
235        let mut logs = Vec::new();
236        if let Err(err) = eval_result.result() {
237            logs.push(format!("{err}"))
238        }
239        logs.extend(eval_result.logs());
240
241        UnitTestResult {
242            success,
243            test: self.to_owned(),
244            spent_budget: eval_result.cost(),
245            logs,
246            assertion: self.assertion,
247        }
248    }
249}
250
251/// ----- PropertyTest -----------------------------------------------------------------
252
253#[derive(Debug, Clone)]
254pub struct PropertyTest {
255    pub input_path: PathBuf,
256    pub module: String,
257    pub name: String,
258    pub on_test_failure: OnTestFailure,
259    pub program: Program<Name>,
260    pub fuzzer: Fuzzer<Name>,
261}
262
263unsafe impl Send for PropertyTest {}
264
265#[derive(Debug, Clone)]
266pub struct Fuzzer<T> {
267    pub program: Program<T>,
268
269    pub type_info: Rc<Type>,
270
271    /// A version of the Fuzzer's type that has gotten rid of
272    /// all erasable opaque type. This is needed in order to
273    /// generate Plutus data with the appropriate shape.
274    pub stripped_type_info: Rc<Type>,
275}
276
277#[derive(Debug, Clone, thiserror::Error, miette::Diagnostic)]
278#[error("Fuzzer exited unexpectedly: {uplc_error}.")]
279pub struct FuzzerError {
280    logs: Vec<String>,
281    uplc_error: uplc::machine::Error,
282}
283
284#[derive(Debug, Clone)]
285pub enum Event {
286    Simplifying {
287        choices: usize,
288    },
289    Simplified {
290        #[cfg(not(target_family = "wasm"))]
291        duration: Duration,
292        #[cfg(target_family = "wasm")]
293        duration: (),
294        steps: usize,
295    },
296}
297
298impl Display for Event {
299    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::result::Result<(), std::fmt::Error> {
300        match self {
301            Event::Simplifying { choices } => f.write_str(&format!(
302                "{} {}",
303                "  Simplifying"
304                    .if_supports_color(Stderr, |s| s.bold())
305                    .if_supports_color(Stderr, |s| s.purple()),
306                format!("counterexample from {choices} choices")
307                    .if_supports_color(Stderr, |s| s.bold()),
308            )),
309            #[cfg(target_family = "wasm")]
310            Event::Simplified { steps, .. } => f.write_str(&format!(
311                "{} {}",
312                "   Simplified"
313                    .if_supports_color(Stderr, |s| s.bold())
314                    .if_supports_color(Stderr, |s| s.purple()),
315                format!("counterexample after {steps} steps",)
316                    .if_supports_color(Stderr, |s| s.bold()),
317            )),
318            #[cfg(not(target_family = "wasm"))]
319            Event::Simplified { duration, steps } => f.write_str(&format!(
320                "{} {}",
321                "   Simplified"
322                    .if_supports_color(Stderr, |s| s.bold())
323                    .if_supports_color(Stderr, |s| s.purple()),
324                format!(
325                    "counterexample in {} after {steps} steps",
326                    if duration.as_secs() == 0 {
327                        format!("{}ms", duration.as_millis())
328                    } else {
329                        format!("{}s", duration.as_secs())
330                    }
331                )
332                .if_supports_color(Stderr, |s| s.bold()),
333            )),
334        }
335    }
336}
337
338impl PropertyTest {
339    pub const DEFAULT_MAX_SUCCESS: usize = 100;
340
341    /// Run a property test from a given seed. The property is run at most DEFAULT_MAX_SUCCESS times. It
342    /// may stops earlier on failure; in which case a 'counterexample' is returned.
343    pub fn run(
344        self,
345        seed: u32,
346        n: usize,
347        plutus_version: &PlutusVersion,
348    ) -> PropertyTestResult<PlutusData> {
349        let mut labels = BTreeMap::new();
350        let mut remaining = n;
351
352        let (logs, counterexample, iterations) = match self.run_n_times(
353            &mut remaining,
354            Prng::from_seed(seed),
355            &mut labels,
356            plutus_version,
357        ) {
358            Ok(None) => (Vec::new(), Ok(None), n),
359            Ok(Some(counterexample)) => (
360                self.eval(&counterexample.value, plutus_version).logs(),
361                Ok(Some(counterexample.value)),
362                n - remaining,
363            ),
364            Err(FuzzerError { logs, uplc_error }) => (logs, Err(uplc_error), n - remaining + 1),
365        };
366
367        PropertyTestResult {
368            test: self,
369            counterexample,
370            iterations,
371            labels,
372            logs,
373        }
374    }
375
376    pub fn run_n_times<'a>(
377        &'a self,
378        remaining: &mut usize,
379        initial_prng: Prng,
380        labels: &mut BTreeMap<String, usize>,
381        plutus_version: &'a PlutusVersion,
382    ) -> Result<Option<Counterexample<'a>>, FuzzerError> {
383        let mut prng = initial_prng;
384        let mut counterexample = None;
385
386        while *remaining > 0 && counterexample.is_none() {
387            (prng, counterexample) = self.run_once(prng, labels, plutus_version)?;
388            *remaining -= 1;
389        }
390
391        Ok(counterexample)
392    }
393
394    fn run_once<'a>(
395        &'a self,
396        prng: Prng,
397        labels: &mut BTreeMap<String, usize>,
398        plutus_version: &'a PlutusVersion,
399    ) -> Result<(Prng, Option<Counterexample<'a>>), FuzzerError> {
400        use OnTestFailure::*;
401
402        let (next_prng, value) = prng
403            .sample(&self.fuzzer.program)?
404            .expect("A seeded PRNG returned 'None' which indicates a fuzzer is ill-formed and implemented wrongly; please contact library's authors.");
405
406        let result = self.eval(&value, plutus_version);
407
408        for label in result.labels() {
409            // NOTE: There may be other log outputs that interefere with labels. So *by
410            // convention*, we treat as label strings that starts with a NUL byte, which
411            // should be a guard sufficient to prevent inadvertent clashes.
412            labels
413                .entry(label)
414                .and_modify(|count| *count += 1)
415                .or_insert(1);
416        }
417
418        let is_failure = result.failed(false);
419
420        let is_success = !is_failure;
421
422        let keep_counterexample = match self.on_test_failure {
423            FailImmediately | SucceedImmediately => is_failure,
424            SucceedEventually => is_success,
425        };
426
427        if keep_counterexample {
428            let mut counterexample = Counterexample {
429                value,
430                choices: next_prng.choices(),
431                cache: Cache::new(|choices| {
432                    match Prng::from_choices(choices).sample(&self.fuzzer.program) {
433                        Err(..) => Status::Invalid,
434                        Ok(None) => Status::Invalid,
435                        Ok(Some((_, value))) => {
436                            let result = self.eval(&value, plutus_version);
437
438                            let is_failure = result.failed(false);
439
440                            match self.on_test_failure {
441                                FailImmediately | SucceedImmediately => {
442                                    if is_failure {
443                                        Status::Keep(value)
444                                    } else {
445                                        Status::Ignore
446                                    }
447                                }
448
449                                SucceedEventually => {
450                                    if is_failure {
451                                        Status::Ignore
452                                    } else {
453                                        Status::Keep(value)
454                                    }
455                                }
456                            }
457                        }
458                    }
459                }),
460            };
461
462            if !counterexample.choices.is_empty() {
463                counterexample.simplify();
464            }
465
466            Ok((next_prng, Some(counterexample)))
467        } else {
468            Ok((next_prng, None))
469        }
470    }
471
472    pub fn eval(&self, value: &PlutusData, plutus_version: &PlutusVersion) -> EvalResult {
473        let program = self.program.apply_data(value.clone());
474
475        Program::<NamedDeBruijn>::try_from(program)
476            .unwrap()
477            .eval_version(ExBudget::max(), &plutus_version.into())
478    }
479}
480
481/// ----- Benchmark -----------------------------------------------------------------
482
483#[derive(Debug, Clone)]
484pub struct Sampler<T> {
485    pub program: Program<T>,
486
487    pub type_info: Rc<Type>,
488
489    /// A version of the Fuzzer's type that has gotten rid of
490    /// all erasable opaque type. This is needed in order to
491    /// generate Plutus data with the appropriate shape.
492    pub stripped_type_info: Rc<Type>,
493}
494
495#[derive(Debug, Clone, thiserror::Error, miette::Diagnostic)]
496pub enum BenchmarkError {
497    #[error("Sampler exited unexpectedly: {uplc_error}.")]
498    SamplerError {
499        logs: Vec<String>,
500        uplc_error: uplc::machine::Error,
501    },
502    #[error("Bench exited unexpectedly: {uplc_error}.")]
503    BenchError {
504        logs: Vec<String>,
505        uplc_error: uplc::machine::Error,
506    },
507}
508
509impl BenchmarkError {
510    pub fn logs(&self) -> &[String] {
511        match self {
512            BenchmarkError::SamplerError { logs, .. } | BenchmarkError::BenchError { logs, .. } => {
513                logs.as_slice()
514            }
515        }
516    }
517}
518
519#[derive(Debug, Clone)]
520pub struct Benchmark {
521    pub input_path: PathBuf,
522    pub module: String,
523    pub name: String,
524    pub on_test_failure: OnTestFailure,
525    pub program: Program<Name>,
526    pub sampler: Sampler<Name>,
527}
528
529unsafe impl Send for Benchmark {}
530
531impl Benchmark {
532    pub const DEFAULT_MAX_SIZE: usize = 30;
533
534    pub fn run(
535        self,
536        seed: u32,
537        max_size: usize,
538        plutus_version: &PlutusVersion,
539    ) -> BenchmarkResult {
540        let mut measures = Vec::with_capacity(max_size);
541        let mut prng = Prng::from_seed(seed);
542        let mut error = None;
543        let mut size = 0;
544
545        while error.is_none() && max_size >= size {
546            let fuzzer = self
547                .sampler
548                .program
549                .apply_term(&Term::Constant(Constant::Integer(size.into()).into()));
550
551            match prng.sample(&fuzzer) {
552                Ok(None) => {
553                    panic!(
554                        "A seeded PRNG returned 'None' which indicates a sampler is ill-formed and implemented wrongly; please contact library's authors."
555                    );
556                }
557
558                Ok(Some((new_prng, value))) => {
559                    prng = new_prng;
560                    let result = self.eval(&value, plutus_version);
561                    match result.result() {
562                        Ok(_) => measures.push((size, result.cost())),
563                        Err(uplc_error) => {
564                            error = Some(BenchmarkError::BenchError {
565                                logs: result.logs(),
566                                uplc_error,
567                            });
568                        }
569                    }
570                }
571
572                Err(FuzzerError { logs, uplc_error }) => {
573                    error = Some(BenchmarkError::SamplerError { logs, uplc_error });
574                }
575            }
576
577            size += 1;
578        }
579
580        BenchmarkResult {
581            bench: self,
582            measures,
583            error,
584        }
585    }
586
587    pub fn eval(&self, value: &PlutusData, plutus_version: &PlutusVersion) -> EvalResult {
588        let program = self.program.apply_data(value.clone());
589
590        Program::<NamedDeBruijn>::try_from(program)
591            .unwrap()
592            .eval_version(ExBudget::max(), &plutus_version.into())
593    }
594}
595
596/// ----- PRNG -----------------------------------------------------------------
597///
598/// A Pseudo-random generator (PRNG) used to produce random values for fuzzers.
599/// Note that the randomness isn't actually managed by the Rust framework, it
600/// entirely relies on properties of hashing algorithm on-chain (e.g. blake2b).
601///
602/// The PRNG can have two forms:
603///
604/// 1. Seeded: which occurs during the initial run of a property. Each time a
605///    number is drawn from the PRNG, a new seed is created. We retain all the
606///    choices drawn in a _choices_ vector.
607///
608/// 2. Replayed: which is used to replay a Prng sequenced from a list of known
609///    choices. This happens when shrinking an example. Instead of trying to
610///    shrink the value directly, we shrink the PRNG sequence with the hope that
611///    it will generate a smaller value. This implies that generators tend to
612///    generate smaller values when drawing smaller numbers.
613///
614#[derive(Debug)]
615pub enum Prng {
616    Seeded { choices: Vec<u8>, uplc: PlutusData },
617    Replayed { choices: Vec<u8>, uplc: PlutusData },
618}
619
620impl Prng {
621    /// Constructor tag for Prng's 'Seeded'
622    const SEEDED: u64 = 0;
623    /// Constructor tag for Prng's 'Replayed'
624    const REPLAYED: u64 = 1;
625
626    /// Constructor tag for Option's 'Some'
627    const SOME: u64 = 0;
628    /// Constructor tag for Option's 'None'
629    const NONE: u64 = 1;
630
631    pub fn uplc(&self) -> PlutusData {
632        match self {
633            Prng::Seeded { uplc, .. } => uplc.clone(),
634            Prng::Replayed { uplc, .. } => uplc.clone(),
635        }
636    }
637
638    pub fn choices(&self) -> Vec<u8> {
639        match self {
640            Prng::Seeded { choices, .. } => {
641                let mut choices = choices.to_vec();
642                choices.reverse();
643                choices
644            }
645            Prng::Replayed { choices, .. } => choices.to_vec(),
646        }
647    }
648
649    /// Construct a Pseudo-random number generator from a seed.
650    pub fn from_seed(seed: u32) -> Prng {
651        let mut digest = [0u8; 32];
652        let mut context = Blake2b::new(32);
653        context.input(&seed.to_be_bytes()[..]);
654        context.result(&mut digest);
655
656        Prng::Seeded {
657            choices: vec![],
658            uplc: Data::constr(
659                Prng::SEEDED,
660                vec![
661                    Data::bytestring(digest.to_vec()), // Prng's seed
662                    Data::bytestring(vec![]),          // Random choices
663                ],
664            ),
665        }
666    }
667
668    /// Construct a Pseudo-random number generator from a pre-defined list of choices.
669    pub fn from_choices(choices: &[u8]) -> Prng {
670        Prng::Replayed {
671            uplc: Data::constr(
672                Prng::REPLAYED,
673                vec![
674                    Data::integer(choices.len().into()),
675                    Data::bytestring(choices.iter().rev().cloned().collect::<Vec<_>>()),
676                ],
677            ),
678            choices: choices.to_vec(),
679        }
680    }
681
682    /// Generate a pseudo-random value from a fuzzer using the given PRNG.
683    pub fn sample(
684        &self,
685        fuzzer: &Program<Name>,
686    ) -> Result<Option<(Prng, PlutusData)>, FuzzerError> {
687        let program = Program::<NamedDeBruijn>::try_from(fuzzer.apply_data(self.uplc())).unwrap();
688        let result = program.eval(ExBudget::max());
689        result
690            .result()
691            .map_err(|uplc_error| FuzzerError {
692                logs: result.logs(),
693                uplc_error,
694            })
695            .map(Prng::from_result)
696    }
697
698    /// Obtain a Prng back from a fuzzer execution. As a reminder, fuzzers have the following
699    /// signature:
700    ///
701    /// `type Fuzzer<a> = fn(Prng) -> Option<(Prng, a)>`
702    ///
703    /// In nominal scenarios (i.e. when the fuzzer is made from a seed and evolve pseudo-randomly),
704    /// it cannot yield 'None'. When replayed however, we can't easily guarantee that the changes
705    /// made during shrinking aren't breaking underlying invariants (if only, because we run out of
706    /// values to replay). In such case, the replayed sequence is simply invalid and the fuzzer
707    /// aborted altogether with 'None'.
708    pub fn from_result(result: Term<NamedDeBruijn>) -> Option<(Self, PlutusData)> {
709        /// Interpret the given 'PlutusData' as one of two Prng constructors.
710        fn as_prng(cst: &PlutusData) -> Prng {
711            if let PlutusData::Constr(Constr { tag, fields, .. }) = cst {
712                if *tag == 121 + Prng::SEEDED {
713                    if let [
714                        PlutusData::BoundedBytes(bytes),
715                        PlutusData::BoundedBytes(choices),
716                    ] = &fields[..]
717                    {
718                        return Prng::Seeded {
719                            choices: choices.to_vec(),
720                            uplc: Data::constr(
721                                Prng::SEEDED,
722                                vec![
723                                    PlutusData::BoundedBytes(bytes.to_owned()),
724                                    // Clear choices between seeded runs, to not
725                                    // accumulate ALL choices ever made.
726                                    PlutusData::BoundedBytes(vec![].into()),
727                                ],
728                            ),
729                        };
730                    }
731                }
732
733                if *tag == 121 + Prng::REPLAYED {
734                    if let [PlutusData::BigInt(..), PlutusData::BoundedBytes(choices)] = &fields[..]
735                    {
736                        return Prng::Replayed {
737                            choices: choices.to_vec(),
738                            uplc: cst.clone(),
739                        };
740                    }
741                }
742            }
743
744            unreachable!("malformed Prng: {cst:#?}")
745        }
746
747        if let Term::Constant(rc) = &result {
748            if let Constant::Data(PlutusData::Constr(Constr { tag, fields, .. })) = &rc.borrow() {
749                if *tag == 121 + Prng::SOME {
750                    if let [PlutusData::Array(elems)] = &fields[..] {
751                        if let [new_seed, value] = &elems[..] {
752                            return Some((as_prng(new_seed), value.clone()));
753                        }
754                    }
755                }
756
757                // May occurs when replaying a fuzzer from a shrinked sequence of
758                // choices. If we run out of choices, or a choice end up being
759                // invalid as per the expectation, the fuzzer can't go further and
760                // fail.
761                if *tag == 121 + Prng::NONE {
762                    return None;
763                }
764            }
765        }
766
767        unreachable!("Fuzzer yielded a malformed result? {result:#?}")
768    }
769}
770
771/// ----- Counterexample -----------------------------------------------------------------
772///
773/// A counterexample is constructed from a test failure. It holds a value, and a sequence
774/// of random choices that led to this value. It holds a reference to the underlying
775/// property and fuzzer. In many cases, a counterexample can be simplified (a.k.a "shrinked")
776/// into a smaller counterexample.
777pub struct Counterexample<'a> {
778    pub value: PlutusData,
779    pub choices: Vec<u8>,
780    pub cache: Cache<'a, PlutusData>,
781}
782
783impl Counterexample<'_> {
784    fn consider(&mut self, choices: &[u8]) -> bool {
785        if choices == self.choices {
786            return true;
787        }
788
789        match self.cache.get(choices) {
790            Status::Invalid | Status::Ignore => false,
791            Status::Keep(value) => {
792                // If these new choices are shorter or smaller, then we pick them
793                // as new choices and inform that it's been an improvement.
794                if choices.len() <= self.choices.len() || choices < &self.choices[..] {
795                    self.value = value;
796                    self.choices = choices.to_vec();
797                    true
798                } else {
799                    false
800                }
801            }
802        }
803    }
804
805    /// Try to simplify a 'Counterexample' by manipulating the random sequence of generated values
806    /// (a.k.a. choices). While the implementation is quite involved, the strategy is rather simple
807    /// at least conceptually:
808    ///
809    /// Each time a (seeded) fuzzer generates a new value and a new seed, it also stores the
810    /// generated value in a vector, which we call 'choices'. If we re-run the test case with this
811    /// exact choice sequence, we end up with the exact same outcome.
812    ///
813    /// But, we can tweak chunks of this sequence in hope to generate a _smaller sequence_, thus
814    /// generally resulting in a _smaller counterexample_. Each transformations is applied on
815    /// chunks of size 8, 4, 2 and 1; until we no longer make progress (i.e. hit a fix point).
816    ///
817    /// As per MiniThesis, we consider the following transformations:
818    ///
819    /// - Deleting chunks
820    /// - Transforming chunks into sequence of zeroes
821    /// - Replacing chunks of values with smaller values
822    /// - Sorting chunks in ascending order
823    /// - Swapping nearby pairs
824    /// - Redistributing values between nearby pairs
825    pub fn simplify(&mut self) {
826        let mut prev;
827
828        let mut steps = 0;
829
830        #[cfg(not(target_family = "wasm"))]
831        let now = std::time::Instant::now();
832
833        eprintln!(
834            "{}",
835            Event::Simplifying {
836                choices: self.choices.len(),
837            }
838        );
839
840        loop {
841            prev = self.choices.clone();
842
843            // First try deleting each choice we made in chunks. We try longer chunks because this
844            // allows us to delete whole composite elements: e.g. deleting an element from a
845            // generated list requires us to delete both the choice of whether to include it and
846            // also the element itself, which may involve more than one choice.
847            let mut k = 8;
848            while k > 0 {
849                let (mut i, mut underflow) = if self.choices.len() < k {
850                    (0, true)
851                } else {
852                    (self.choices.len() - k, false)
853                };
854
855                while !underflow {
856                    if i >= self.choices.len() {
857                        (i, underflow) = i.overflowing_sub(1);
858                        steps += 1;
859                        continue;
860                    }
861
862                    let j = i + k;
863
864                    let mut choices = [
865                        &self.choices[..i],
866                        if j < self.choices.len() {
867                            &self.choices[j..]
868                        } else {
869                            &[]
870                        },
871                    ]
872                    .concat();
873
874                    if !self.consider(&choices) {
875                        // Perform an extra reduction step that decrease the size of choices near
876                        // the end, to cope with dependencies between choices, e.g. drawing a
877                        // number as a list length, and then drawing that many elements.
878                        //
879                        // This isn't perfect, but allows to make progresses in many cases.
880                        if i > 0 && choices[i - 1] > 0 {
881                            choices[i - 1] -= 1;
882                            if self.consider(&choices) {
883                                i += 1;
884                            };
885                        }
886
887                        (i, underflow) = i.overflowing_sub(1);
888                    }
889
890                    steps += 1;
891                }
892
893                k /= 2
894            }
895
896            if !self.choices.is_empty() {
897                // Now we try replacing region of choices with zeroes. Note that unlike the above we
898                // skip k = 1 because we handle that in the next step. Often (but not always) a block
899                // of all zeroes is the smallest value that a region can be.
900                let mut k = 8;
901                while k > 1 {
902                    let mut i = self.choices.len();
903                    while i >= k {
904                        steps += 1;
905                        let ivs = (i - k..i).map(|j| (j, 0)).collect::<Vec<_>>();
906                        i -= if self.replace(ivs) { k } else { 1 }
907                    }
908                    k /= 2
909                }
910
911                // Replace choices with smaller value, by doing a binary search. This will replace n
912                // with 0 or n - 1, if possible, but will also more efficiently replace it with, a
913                // smaller number than doing multiple subtractions would.
914                let (mut i, mut underflow) = (self.choices.len() - 1, false);
915                while !underflow {
916                    steps += 1;
917                    self.binary_search_replace(0, self.choices[i], |v| vec![(i, v)]);
918                    (i, underflow) = i.overflowing_sub(1);
919                }
920
921                // Sort out of orders chunks in ascending order
922                let mut k = 8;
923                while k > 1 {
924                    let mut i = self.choices.len() - 1;
925                    while i >= k {
926                        steps += 1;
927                        let (from, to) = (i - k, i);
928                        self.replace(
929                            (from..to)
930                                .zip(self.choices[from..to].iter().cloned().sorted())
931                                .collect(),
932                        );
933                        i -= 1;
934                    }
935                    k /= 2
936                }
937
938                // Try adjusting nearby pairs by:
939                //
940                // - Swapping them if they are out-of-order
941                // - Redistributing values between them.
942                for k in [2, 1] {
943                    let mut j = self.choices.len() - 1;
944                    while j >= k {
945                        let i = j - k;
946
947                        // Swap
948                        if self.choices[i] > self.choices[j] {
949                            self.replace(vec![(i, self.choices[j]), (j, self.choices[i])]);
950                        }
951
952                        let iv = self.choices[i];
953                        let jv = self.choices[j];
954
955                        // Replace
956                        if iv > 0 && jv <= u8::MAX - iv {
957                            self.binary_search_replace(0, iv, |v| vec![(i, v), (j, jv + (iv - v))]);
958                        }
959
960                        steps += 1;
961
962                        j -= 1
963                    }
964                }
965            }
966
967            // If we've reached a fixed point, then we cannot shrink further. We've reached a
968            // (local) minimum, which is as good as a counterexample we'll get with this approach.
969            if prev.as_slice() == self.choices.as_slice() {
970                break;
971            }
972        }
973
974        eprintln!(
975            "{}",
976            Event::Simplified {
977                #[cfg(not(target_family = "wasm"))]
978                duration: now.elapsed(),
979                #[cfg(target_family = "wasm")]
980                duration: (),
981                steps,
982            }
983        );
984    }
985
986    /// Try to replace a value with a smaller value by doing a binary search between
987    /// two extremes. This converges relatively fast in order to shrink down values.
988    fn binary_search_replace<F>(&mut self, lo: u8, hi: u8, f: F) -> u8
989    where
990        F: Fn(u8) -> Vec<(usize, u8)>,
991    {
992        if self.replace(f(lo)) {
993            return lo;
994        }
995
996        let mut lo = lo;
997        let mut hi = hi;
998
999        while lo + 1 < hi {
1000            let mid = lo + (hi - lo) / 2;
1001            if self.replace(f(mid)) {
1002                hi = mid;
1003            } else {
1004                lo = mid;
1005            }
1006        }
1007
1008        hi
1009    }
1010
1011    // Replace values in the choices vector, based on the index-value list provided
1012    // and consider the resulting choices.
1013    fn replace(&mut self, ivs: Vec<(usize, u8)>) -> bool {
1014        let mut choices = self.choices.clone();
1015
1016        for (i, v) in ivs {
1017            if i >= choices.len() {
1018                return false;
1019            }
1020            choices[i] = v;
1021        }
1022
1023        self.consider(&choices)
1024    }
1025}
1026
1027/// ----- Cache -----------------------------------------------------------------------
1028///
1029/// A simple cache as a Patricia-trie to look for already explored options. The simplification
1030/// steps does often generate the same paths and the generation of new test values as well as the
1031/// properties can take a significant time.
1032///
1033/// Yet, sequences have interesting properties:
1034///
1035/// 1. The generation and test execution is entirely deterministic.
1036///
1037///
1038pub struct Cache<'a, T> {
1039    db: PatriciaMap<Status<T>>,
1040    #[allow(clippy::type_complexity)]
1041    run: Box<dyn Fn(&[u8]) -> Status<T> + 'a>,
1042}
1043
1044#[derive(Debug, Clone, Copy, PartialEq)]
1045pub enum Status<T> {
1046    Keep(T),
1047    Ignore,
1048    Invalid,
1049}
1050
1051impl<'a, T> Cache<'a, T>
1052where
1053    T: PartialEq + Clone,
1054{
1055    pub fn new<F>(run: F) -> Cache<'a, T>
1056    where
1057        F: Fn(&[u8]) -> Status<T> + 'a,
1058    {
1059        Cache {
1060            db: PatriciaMap::new(),
1061            run: Box::new(run),
1062        }
1063    }
1064
1065    pub fn size(&self) -> usize {
1066        self.db.len()
1067    }
1068
1069    pub fn get(&mut self, choices: &[u8]) -> Status<T> {
1070        if let Some((prefix, status)) = self.db.get_longest_common_prefix(choices) {
1071            let status = status.clone();
1072            if status != Status::Invalid || prefix == choices {
1073                return status;
1074            }
1075        }
1076
1077        let status = self.run.deref()(choices);
1078
1079        // Clear longer path on non-invalid cases, as we will never reach them
1080        // again due to a now-shorter prefix found.
1081        //
1082        // This hopefully keeps the cache under a reasonable size as we prune
1083        // the tree as we discover shorter paths.
1084        if status != Status::Invalid {
1085            let keys = self
1086                .db
1087                .iter_prefix(choices)
1088                .map(|(k, _)| k)
1089                .collect::<Vec<_>>();
1090            for k in keys {
1091                self.db.remove(k);
1092            }
1093        }
1094
1095        self.db.insert(choices, status.clone());
1096
1097        status
1098    }
1099}
1100
1101// ----------------------------------------------------------------------------
1102//
1103// TestResult
1104//
1105// ----------------------------------------------------------------------------
1106
1107#[derive(Debug, Clone)]
1108pub enum TestResult<U, T> {
1109    UnitTestResult(UnitTestResult<U>),
1110    PropertyTestResult(PropertyTestResult<T>),
1111    BenchmarkResult(BenchmarkResult),
1112}
1113
1114unsafe impl<U, T> Send for TestResult<U, T> {}
1115
1116impl TestResult<(Constant, Rc<Type>), PlutusData> {
1117    pub fn reify(
1118        self,
1119        data_types: &IndexMap<&DataTypeKey, &TypedDataType>,
1120    ) -> TestResult<UntypedExpr, UntypedExpr> {
1121        match self {
1122            TestResult::UnitTestResult(test) => TestResult::UnitTestResult(test.reify(data_types)),
1123            TestResult::PropertyTestResult(test) => {
1124                TestResult::PropertyTestResult(test.reify(data_types))
1125            }
1126            TestResult::BenchmarkResult(result) => TestResult::BenchmarkResult(result),
1127        }
1128    }
1129}
1130
1131impl<U, T> TestResult<U, T> {
1132    pub fn is_success(&self) -> bool {
1133        match self {
1134            TestResult::UnitTestResult(UnitTestResult { success, .. }) => *success,
1135            TestResult::PropertyTestResult(PropertyTestResult {
1136                counterexample: Err(..),
1137                ..
1138            }) => false,
1139            TestResult::PropertyTestResult(PropertyTestResult {
1140                counterexample: Ok(counterexample),
1141                test,
1142                ..
1143            }) => match test.on_test_failure {
1144                OnTestFailure::FailImmediately | OnTestFailure::SucceedEventually => {
1145                    counterexample.is_none()
1146                }
1147                OnTestFailure::SucceedImmediately => counterexample.is_some(),
1148            },
1149            TestResult::BenchmarkResult(BenchmarkResult { error, .. }) => error.is_none(),
1150        }
1151    }
1152
1153    pub fn module(&self) -> &str {
1154        match self {
1155            TestResult::UnitTestResult(UnitTestResult { test, .. }) => test.module.as_str(),
1156            TestResult::PropertyTestResult(PropertyTestResult { test, .. }) => test.module.as_str(),
1157            TestResult::BenchmarkResult(BenchmarkResult { bench, .. }) => bench.module.as_str(),
1158        }
1159    }
1160
1161    pub fn title(&self) -> &str {
1162        match self {
1163            TestResult::UnitTestResult(UnitTestResult { test, .. }) => test.name.as_str(),
1164            TestResult::PropertyTestResult(PropertyTestResult { test, .. }) => test.name.as_str(),
1165            TestResult::BenchmarkResult(BenchmarkResult { bench, .. }) => bench.name.as_str(),
1166        }
1167    }
1168
1169    pub fn logs(&self) -> &[String] {
1170        match self {
1171            TestResult::UnitTestResult(UnitTestResult { logs, .. })
1172            | TestResult::PropertyTestResult(PropertyTestResult { logs, .. }) => logs,
1173            TestResult::BenchmarkResult(BenchmarkResult { error, .. }) => {
1174                error.as_ref().map(|e| e.logs()).unwrap_or_default()
1175            }
1176        }
1177    }
1178}
1179
1180#[derive(Debug, Clone)]
1181pub struct UnitTestResult<T> {
1182    pub success: bool,
1183    pub spent_budget: ExBudget,
1184    pub logs: Vec<String>,
1185    pub test: UnitTest,
1186    pub assertion: Option<Assertion<T>>,
1187}
1188
1189unsafe impl<T> Send for UnitTestResult<T> {}
1190
1191impl UnitTestResult<(Constant, Rc<Type>)> {
1192    pub fn reify(
1193        self,
1194        data_types: &IndexMap<&DataTypeKey, &TypedDataType>,
1195    ) -> UnitTestResult<UntypedExpr> {
1196        UnitTestResult {
1197            success: self.success,
1198            spent_budget: self.spent_budget,
1199            logs: self.logs,
1200            test: self.test,
1201            assertion: self.assertion.and_then(|assertion| {
1202                // No need to spend time/cpu on reifying assertions for successful
1203                // tests since they aren't shown.
1204                if self.success {
1205                    return None;
1206                }
1207
1208                Some(Assertion {
1209                    bin_op: assertion.bin_op,
1210                    head: assertion.head.map(|(cst, tipo)| {
1211                        UntypedExpr::reify_constant(data_types, cst, tipo)
1212                            .expect("failed to reify assertion operand?")
1213                    }),
1214                    tail: assertion.tail.map(|xs| {
1215                        xs.mapped(|(cst, tipo)| {
1216                            UntypedExpr::reify_constant(data_types, cst, tipo)
1217                                .expect("failed to reify assertion operand?")
1218                        })
1219                    }),
1220                })
1221            }),
1222        }
1223    }
1224}
1225
1226#[derive(Debug, Clone)]
1227pub struct PropertyTestResult<T> {
1228    pub test: PropertyTest,
1229    pub counterexample: Result<Option<T>, uplc::machine::Error>,
1230    pub iterations: usize,
1231    pub labels: BTreeMap<String, usize>,
1232    pub logs: Vec<String>,
1233}
1234
1235unsafe impl<T> Send for PropertyTestResult<T> {}
1236
1237impl PropertyTestResult<PlutusData> {
1238    pub fn reify(
1239        self,
1240        data_types: &IndexMap<&DataTypeKey, &TypedDataType>,
1241    ) -> PropertyTestResult<UntypedExpr> {
1242        PropertyTestResult {
1243            counterexample: self.counterexample.map(|ok| {
1244                ok.map(|counterexample| {
1245                    UntypedExpr::reify_data(
1246                        data_types,
1247                        counterexample,
1248                        self.test.fuzzer.type_info.clone(),
1249                    )
1250                    .expect("failed to reify counterexample?")
1251                })
1252            }),
1253            iterations: self.iterations,
1254            test: self.test,
1255            labels: self.labels,
1256            logs: self.logs,
1257        }
1258    }
1259}
1260
1261#[derive(Debug, Clone)]
1262pub struct Assertion<T> {
1263    pub bin_op: BinOp,
1264    pub head: Result<T, ()>,
1265    pub tail: Result<Vec1<T>, ()>,
1266}
1267
1268impl TryFrom<TypedExpr> for Assertion<TypedExpr> {
1269    type Error = ();
1270
1271    fn try_from(body: TypedExpr) -> Result<Self, Self::Error> {
1272        match body {
1273            TypedExpr::BinOp {
1274                name,
1275                tipo,
1276                left,
1277                right,
1278                ..
1279            } if tipo == Type::bool() => {
1280                // 'and' and 'or' are left-associative operators.
1281                match (*right).clone().try_into() {
1282                    Ok(Assertion {
1283                        bin_op,
1284                        head: Ok(head),
1285                        tail: Ok(tail),
1286                        ..
1287                    }) if bin_op == name => {
1288                        let mut both = vec1![head];
1289                        both.extend(tail);
1290                        Ok(Assertion {
1291                            bin_op: name,
1292                            head: Ok(*left),
1293                            tail: Ok(both),
1294                        })
1295                    }
1296                    _ => Ok(Assertion {
1297                        bin_op: name,
1298                        head: Ok(*left),
1299                        tail: Ok(vec1![*right]),
1300                    }),
1301                }
1302            }
1303
1304            // NOTE drill through trace-if-false operators for better errors.
1305            TypedExpr::If {
1306                branches,
1307                final_else,
1308                ..
1309            } => {
1310                if let [
1311                    IfBranch {
1312                        condition, body, ..
1313                    },
1314                ] = &branches[..]
1315                {
1316                    let then_is_true = match body {
1317                        TypedExpr::Var {
1318                            name, constructor, ..
1319                        } => name == "True" && constructor.tipo == Type::bool(),
1320                        _ => false,
1321                    };
1322
1323                    let else_is_wrapped_false = match *final_else {
1324                        TypedExpr::Trace { then, .. } => match *then {
1325                            TypedExpr::Var {
1326                                name, constructor, ..
1327                            } => name == "False" && constructor.tipo == Type::bool(),
1328                            _ => false,
1329                        },
1330                        _ => false,
1331                    };
1332
1333                    if then_is_true && else_is_wrapped_false {
1334                        return condition.to_owned().try_into();
1335                    }
1336                }
1337
1338                Err(())
1339            }
1340
1341            TypedExpr::Trace { then, .. } => (*then).try_into(),
1342
1343            TypedExpr::Sequence { expressions, .. } | TypedExpr::Pipeline { expressions, .. } => {
1344                if let Ok(Assertion {
1345                    bin_op,
1346                    head: Ok(head),
1347                    tail: Ok(tail),
1348                }) = expressions.last().unwrap().to_owned().try_into()
1349                {
1350                    let replace = |expr| {
1351                        let mut expressions = expressions.clone();
1352                        expressions.pop();
1353                        expressions.push(expr);
1354                        TypedExpr::Sequence {
1355                            expressions,
1356                            location: Span::empty(),
1357                        }
1358                    };
1359
1360                    Ok(Assertion {
1361                        bin_op,
1362                        head: Ok(replace(head)),
1363                        tail: Ok(tail.mapped(replace)),
1364                    })
1365                } else {
1366                    Err(())
1367                }
1368            }
1369            _ => Err(()),
1370        }
1371    }
1372}
1373
1374pub struct AssertionStyleOptions<'a> {
1375    red: Box<dyn Fn(String) -> String + 'a>,
1376    bold: Box<dyn Fn(String) -> String + 'a>,
1377}
1378
1379impl<'a> AssertionStyleOptions<'a> {
1380    pub fn new(stream: Option<&'a Stream>) -> Self {
1381        match stream {
1382            Some(stream) => Self {
1383                red: Box::new(|s| {
1384                    s.if_supports_color(stream.to_owned(), |s| s.red())
1385                        .to_string()
1386                }),
1387                bold: Box::new(|s| {
1388                    s.if_supports_color(stream.to_owned(), |s| s.bold())
1389                        .to_string()
1390                }),
1391            },
1392            None => Self {
1393                red: Box::new(|s| s),
1394                bold: Box::new(|s| s),
1395            },
1396        }
1397    }
1398}
1399
1400impl Assertion<UntypedExpr> {
1401    #[allow(clippy::just_underscores_and_digits)]
1402    pub fn to_string(&self, expect_failure: bool, style: &AssertionStyleOptions) -> String {
1403        let red = |s: &str| style.red.as_ref()(s.to_string());
1404        let x = |s: &str| style.red.as_ref()(style.bold.as_ref()(format!("× {s}")));
1405
1406        // head did not map to a constant
1407        if self.head.is_err() {
1408            return x("program failed");
1409        }
1410
1411        // any value in tail did not map to a constant
1412        if self.tail.is_err() {
1413            return x("program failed");
1414        }
1415
1416        fn fmt_side(side: &UntypedExpr, red: &dyn Fn(&str) -> String) -> String {
1417            let __ = red("│");
1418
1419            Formatter::new()
1420                .expr(side, false)
1421                .to_pretty_string(60)
1422                .lines()
1423                .map(|line| format!("{__} {line}"))
1424                .collect::<Vec<String>>()
1425                .join("\n")
1426        }
1427
1428        let left = fmt_side(self.head.as_ref().unwrap(), &red);
1429
1430        let tail = self.tail.as_ref().unwrap();
1431
1432        let right = fmt_side(tail.first(), &red);
1433
1434        format!(
1435            "{}{}{}",
1436            x("expected"),
1437            if expect_failure && self.bin_op == BinOp::Or {
1438                x(" neither\n")
1439            } else {
1440                "\n".to_string()
1441            },
1442            if expect_failure {
1443                match self.bin_op {
1444                    BinOp::And => [
1445                        left,
1446                        x("and"),
1447                        [
1448                            tail.mapped_ref(|s| fmt_side(s, &red))
1449                                .join(format!("\n{}\n", x("and")).as_str()),
1450                            if tail.len() > 1 {
1451                                x("to not all be true")
1452                            } else {
1453                                x("to not both be true")
1454                            },
1455                        ]
1456                        .join("\n"),
1457                    ],
1458                    BinOp::Or => [
1459                        left,
1460                        x("nor"),
1461                        [
1462                            tail.mapped_ref(|s| fmt_side(s, &red))
1463                                .join(format!("\n{}\n", x("nor")).as_str()),
1464                            x("to be true"),
1465                        ]
1466                        .join("\n"),
1467                    ],
1468                    BinOp::Eq => [left, x("to not equal"), right],
1469                    BinOp::NotEq => [left, x("to not be different"), right],
1470                    BinOp::LtInt => [left, x("to not be lower than"), right],
1471                    BinOp::LtEqInt => [left, x("to not be lower than or equal to"), right],
1472                    BinOp::GtInt => [left, x("to not be greater than"), right],
1473                    BinOp::GtEqInt => [left, x("to not be greater than or equal to"), right],
1474                    _ => unreachable!("unexpected non-boolean binary operator in assertion?"),
1475                }
1476                .join("\n")
1477            } else {
1478                match self.bin_op {
1479                    BinOp::And => [
1480                        left,
1481                        x("and"),
1482                        [
1483                            tail.mapped_ref(|s| fmt_side(s, &red))
1484                                .join(format!("\n{}\n", x("and")).as_str()),
1485                            if tail.len() > 1 {
1486                                x("to all be true")
1487                            } else {
1488                                x("to both be true")
1489                            },
1490                        ]
1491                        .join("\n"),
1492                    ],
1493                    BinOp::Or => [
1494                        left,
1495                        x("or"),
1496                        [
1497                            tail.mapped_ref(|s| fmt_side(s, &red))
1498                                .join(format!("\n{}\n", x("or")).as_str()),
1499                            x("to be true"),
1500                        ]
1501                        .join("\n"),
1502                    ],
1503                    BinOp::Eq => [left, x("to equal"), right],
1504                    BinOp::NotEq => [left, x("to not equal"), right],
1505                    BinOp::LtInt => [left, x("to be lower than"), right],
1506                    BinOp::LtEqInt => [left, x("to be lower than or equal to"), right],
1507                    BinOp::GtInt => [left, x("to be greater than"), right],
1508                    BinOp::GtEqInt => [left, x("to be greater than or equal to"), right],
1509                    _ => unreachable!("unexpected non-boolean binary operator in assertion?"),
1510                }
1511                .join("\n")
1512            }
1513        )
1514    }
1515}
1516
1517#[derive(Debug, Clone)]
1518pub struct BenchmarkResult {
1519    pub bench: Benchmark,
1520    pub measures: Vec<(usize, ExBudget)>,
1521    pub error: Option<BenchmarkError>,
1522}
1523
1524unsafe impl Send for BenchmarkResult {}
1525unsafe impl Sync for BenchmarkResult {}
1526
1527#[cfg(test)]
1528mod test {
1529    use super::*;
1530
1531    #[test]
1532    fn test_cache() {
1533        let called = std::cell::RefCell::new(0);
1534
1535        let mut cache = Cache::new(|choices| {
1536            called.replace_with(|n| *n + 1);
1537
1538            match choices {
1539                [0, 0, 0] => Status::Keep(true),
1540                _ => {
1541                    if choices.len() <= 2 {
1542                        Status::Invalid
1543                    } else {
1544                        Status::Ignore
1545                    }
1546                }
1547            }
1548        });
1549
1550        assert_eq!(cache.get(&[1, 1]), Status::Invalid); // Fn executed
1551        assert_eq!(cache.get(&[1, 1, 2, 3]), Status::Ignore); // Fn executed
1552        assert_eq!(cache.get(&[1, 1, 2]), Status::Ignore); // Fnexecuted
1553        assert_eq!(cache.get(&[1, 1, 2, 2]), Status::Ignore); // Cached result
1554        assert_eq!(cache.get(&[1, 1, 2, 1]), Status::Ignore); // Cached result
1555        assert_eq!(cache.get(&[0, 1, 2]), Status::Ignore); // Fn executed
1556        assert_eq!(cache.get(&[0, 0, 0]), Status::Keep(true)); // Fn executed
1557        assert_eq!(cache.get(&[0, 0, 0]), Status::Keep(true)); // Cached result
1558
1559        assert_eq!(called.borrow().deref().to_owned(), 5, "execution calls");
1560        assert_eq!(cache.size(), 4, "cache size");
1561    }
1562}