Borrow integrated shrinking approach from MiniThesis.
This commit is contained in:
parent
3762473a60
commit
a703db4d14
|
@ -339,10 +339,24 @@ fn infer_definition(
|
||||||
Type::Fn { ret, .. } => {
|
Type::Fn { ret, .. } => {
|
||||||
let ann = tipo_to_annotation(ret, location)?;
|
let ann = tipo_to_annotation(ret, location)?;
|
||||||
match ann {
|
match ann {
|
||||||
Annotation::Tuple { elems, .. } if elems.len() == 2 => {
|
Annotation::Constructor {
|
||||||
|
module,
|
||||||
|
name,
|
||||||
|
arguments,
|
||||||
|
..
|
||||||
|
} if module.as_ref().unwrap_or(&String::new()).is_empty()
|
||||||
|
&& name == "Option" =>
|
||||||
|
{
|
||||||
|
match &arguments[..] {
|
||||||
|
[Annotation::Tuple { elems, .. }] if elems.len() == 2 => {
|
||||||
Ok(elems.get(1).expect("Tuple has two elements").to_owned())
|
Ok(elems.get(1).expect("Tuple has two elements").to_owned())
|
||||||
}
|
}
|
||||||
_ => todo!("Fuzzer returns something else than a 2-tuple? "),
|
_ => {
|
||||||
|
todo!("expected a single generic argument unifying as 2-tuple")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => todo!("expected an Option<a>"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Type::Var { .. } | Type::App { .. } | Type::Tuple { .. } => {
|
Type::Var { .. } | Type::App { .. } | Type::Tuple { .. } => {
|
||||||
|
|
|
@ -93,7 +93,7 @@ pub enum Error {
|
||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
verbose: bool,
|
verbose: bool,
|
||||||
src: String,
|
src: String,
|
||||||
evaluation_hint: Option<String>,
|
assertion: Option<String>,
|
||||||
},
|
},
|
||||||
|
|
||||||
#[error(
|
#[error(
|
||||||
|
@ -323,7 +323,7 @@ impl Diagnostic for Error {
|
||||||
Error::MissingManifest { .. } => Some(Box::new("Try running `aiken new <REPOSITORY/PROJECT>` to initialise a project with an example manifest.")),
|
Error::MissingManifest { .. } => Some(Box::new("Try running `aiken new <REPOSITORY/PROJECT>` to initialise a project with an example manifest.")),
|
||||||
Error::TomlLoading { .. } => None,
|
Error::TomlLoading { .. } => None,
|
||||||
Error::Format { .. } => None,
|
Error::Format { .. } => None,
|
||||||
Error::TestFailure { evaluation_hint, .. } => match evaluation_hint {
|
Error::TestFailure { assertion, .. } => match assertion {
|
||||||
None => None,
|
None => None,
|
||||||
Some(hint) => Some(Box::new(hint.to_string()))
|
Some(hint) => Some(Box::new(hint.to_string()))
|
||||||
},
|
},
|
||||||
|
|
|
@ -21,13 +21,19 @@ use crate::blueprint::{
|
||||||
schema::{Annotated, Schema},
|
schema::{Annotated, Schema},
|
||||||
Blueprint,
|
Blueprint,
|
||||||
};
|
};
|
||||||
|
use crate::{
|
||||||
|
config::Config,
|
||||||
|
error::{Error, Warning},
|
||||||
|
module::{CheckedModule, CheckedModules, ParsedModule, ParsedModules},
|
||||||
|
telemetry::Event,
|
||||||
|
};
|
||||||
use aiken_lang::{
|
use aiken_lang::{
|
||||||
ast::{
|
ast::{
|
||||||
Definition, Function, ModuleKind, Span, Tracing, TypedDataType, TypedFunction, Validator,
|
Definition, Function, ModuleKind, Span, Tracing, TypedDataType, TypedFunction, Validator,
|
||||||
},
|
},
|
||||||
builtins,
|
builtins,
|
||||||
expr::TypedExpr,
|
expr::TypedExpr,
|
||||||
gen_uplc::builder::{cast_validator_args, DataTypeKey, FunctionAccessKey},
|
gen_uplc::builder::{DataTypeKey, FunctionAccessKey},
|
||||||
tipo::{Type, TypeInfo},
|
tipo::{Type, TypeInfo},
|
||||||
IdGenerator,
|
IdGenerator,
|
||||||
};
|
};
|
||||||
|
@ -40,8 +46,7 @@ use pallas::ledger::{
|
||||||
primitives::babbage::{self as cardano, PolicyId},
|
primitives::babbage::{self as cardano, PolicyId},
|
||||||
traverse::ComputeHash,
|
traverse::ComputeHash,
|
||||||
};
|
};
|
||||||
|
use script::{Assertion, Test, TestResult};
|
||||||
use script::{EvalHint, EvalInfo, PropertyTest, Test};
|
|
||||||
use std::{
|
use std::{
|
||||||
collections::HashMap,
|
collections::HashMap,
|
||||||
fs::{self, File},
|
fs::{self, File},
|
||||||
|
@ -56,13 +61,6 @@ use uplc::{
|
||||||
PlutusData,
|
PlutusData,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
|
||||||
config::Config,
|
|
||||||
error::{Error, Warning},
|
|
||||||
module::{CheckedModule, CheckedModules, ParsedModule, ParsedModules},
|
|
||||||
telemetry::Event,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Source {
|
pub struct Source {
|
||||||
pub path: PathBuf,
|
pub path: PathBuf,
|
||||||
|
@ -323,24 +321,15 @@ where
|
||||||
self.event_listener.handle_event(Event::RunningTests);
|
self.event_listener.handle_event(Event::RunningTests);
|
||||||
}
|
}
|
||||||
|
|
||||||
let results = self.run_tests(tests.iter().collect());
|
let results = self.run_tests(tests);
|
||||||
|
|
||||||
let errors: Vec<Error> = results
|
let errors: Vec<Error> = results
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|e| {
|
.filter_map(|e| {
|
||||||
if e.success {
|
if e.is_success() {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
Some(Error::TestFailure {
|
Some(e.into_error(verbose))
|
||||||
name: e.test.name().to_string(),
|
|
||||||
path: e.test.input_path().to_path_buf(),
|
|
||||||
evaluation_hint: e
|
|
||||||
.test
|
|
||||||
.evaluation_hint()
|
|
||||||
.map(|hint| hint.to_string()),
|
|
||||||
src: e.test.program().to_pretty(),
|
|
||||||
verbose,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
@ -816,7 +805,7 @@ where
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
let evaluation_hint = func_def.test_hint().map(|(bin_op, left_src, right_src)| {
|
let assertion = func_def.test_hint().map(|(bin_op, left_src, right_src)| {
|
||||||
let left = generator
|
let left = generator
|
||||||
.clone()
|
.clone()
|
||||||
.generate_raw(&left_src, &module_name)
|
.generate_raw(&left_src, &module_name)
|
||||||
|
@ -829,7 +818,7 @@ where
|
||||||
.try_into()
|
.try_into()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
EvalHint {
|
Assertion {
|
||||||
bin_op,
|
bin_op,
|
||||||
left,
|
left,
|
||||||
right,
|
right,
|
||||||
|
@ -846,7 +835,7 @@ where
|
||||||
name.to_string(),
|
name.to_string(),
|
||||||
*can_error,
|
*can_error,
|
||||||
program.try_into().unwrap(),
|
program.try_into().unwrap(),
|
||||||
evaluation_hint,
|
assertion,
|
||||||
);
|
);
|
||||||
|
|
||||||
programs.push(test);
|
programs.push(test);
|
||||||
|
@ -862,14 +851,16 @@ where
|
||||||
ret: body.tipo(),
|
ret: body.tipo(),
|
||||||
}),
|
}),
|
||||||
is_capture: false,
|
is_capture: false,
|
||||||
args: vec![parameter.clone().into()],
|
args: vec![parameter.into()],
|
||||||
body: Box::new(body.clone()),
|
body: Box::new(body.clone()),
|
||||||
return_annotation: None,
|
return_annotation: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let program = generator.clone().generate_raw(&body, &module_name);
|
let program = generator
|
||||||
|
.clone()
|
||||||
let term = cast_validator_args(program.term, &[parameter.into()]);
|
.generate_raw(&body, &module_name)
|
||||||
|
.try_into()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
let fuzzer: Program<NamedDeBruijn> = generator
|
let fuzzer: Program<NamedDeBruijn> = generator
|
||||||
.clone()
|
.clone()
|
||||||
|
@ -882,7 +873,7 @@ where
|
||||||
module_name,
|
module_name,
|
||||||
name.to_string(),
|
name.to_string(),
|
||||||
*can_error,
|
*can_error,
|
||||||
Program { term, ..program }.try_into().unwrap(),
|
program,
|
||||||
fuzzer,
|
fuzzer,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -893,36 +884,16 @@ where
|
||||||
Ok(programs)
|
Ok(programs)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run_tests<'a>(&'a self, tests: Vec<&'a Test>) -> Vec<EvalInfo<'a>> {
|
fn run_tests(&self, tests: Vec<Test>) -> Vec<TestResult> {
|
||||||
// FIXME: Find a way to re-introduce parallel testing despite the references (which aren't
|
use rayon::prelude::*;
|
||||||
// sizeable).
|
|
||||||
// We do now hold references to tests because the property tests results are all pointing
|
|
||||||
// to the same test, so we end up copying the same test over and over.
|
|
||||||
//
|
|
||||||
// So we might want to rework the evaluation result to avoid that and keep parallel testing
|
|
||||||
// possible.
|
|
||||||
// use rayon::prelude::*;
|
|
||||||
|
|
||||||
tests
|
tests
|
||||||
.iter()
|
.into_par_iter()
|
||||||
.flat_map(|test| match test {
|
.map(|test| match test {
|
||||||
Test::UnitTest(unit_test) => {
|
Test::UnitTest(unit_test) => unit_test.run(),
|
||||||
let mut result = unit_test.run();
|
// TODO: Get the seed from the command-line, defaulting to a random one when not
|
||||||
vec![test.report(&mut result)]
|
// provided.
|
||||||
}
|
Test::PropertyTest(property_test) => property_test.run(42),
|
||||||
Test::PropertyTest(ref property_test) => {
|
|
||||||
let mut seed = PropertyTest::new_seed(42);
|
|
||||||
|
|
||||||
let mut results = vec![];
|
|
||||||
for _ in 0..100 {
|
|
||||||
let (new_seed, sample) = property_test.sample(seed);
|
|
||||||
seed = new_seed;
|
|
||||||
let mut result = property_test.run(&sample);
|
|
||||||
results.push(test.report(&mut result));
|
|
||||||
}
|
|
||||||
|
|
||||||
results
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,8 +7,8 @@ pub fn ansi_len(s: &str) -> usize {
|
||||||
.count()
|
.count()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn len_longest_line(s: &str) -> usize {
|
pub fn len_longest_line(zero: usize, s: &str) -> usize {
|
||||||
s.lines().fold(0, |max, l| {
|
s.lines().fold(zero, |max, l| {
|
||||||
let n = ansi_len(l);
|
let n = ansi_len(l);
|
||||||
if n > max {
|
if n > max {
|
||||||
n
|
n
|
||||||
|
@ -23,7 +23,7 @@ pub fn boxed(title: &str, content: &str) -> String {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn boxed_with(title: &str, content: &str, border_style: fn(&str) -> String) -> String {
|
pub fn boxed_with(title: &str, content: &str, border_style: fn(&str) -> String) -> String {
|
||||||
let n = len_longest_line(content);
|
let n = len_longest_line(ansi_len(title) + 1, content);
|
||||||
|
|
||||||
let content = content
|
let content = content
|
||||||
.lines()
|
.lines()
|
||||||
|
@ -62,7 +62,7 @@ pub fn open_box(
|
||||||
border_style: fn(&str) -> String,
|
border_style: fn(&str) -> String,
|
||||||
) -> String {
|
) -> String {
|
||||||
let i = ansi_len(content.lines().collect::<Vec<_>>().first().unwrap());
|
let i = ansi_len(content.lines().collect::<Vec<_>>().first().unwrap());
|
||||||
let j = len_longest_line(content);
|
let j = len_longest_line(ansi_len(title) + 1, content);
|
||||||
let k = ansi_len(footer);
|
let k = ansi_len(footer);
|
||||||
|
|
||||||
let content = content
|
let content = content
|
||||||
|
@ -79,7 +79,11 @@ pub fn open_box(
|
||||||
|
|
||||||
let bottom = format!(
|
let bottom = format!(
|
||||||
"{} {}",
|
"{} {}",
|
||||||
pad_right(border_style("┕"), j - k + 1, &border_style("━")),
|
pad_right(
|
||||||
|
border_style("┕"),
|
||||||
|
if j < k { 0 } else { j + 1 - k },
|
||||||
|
&border_style("━")
|
||||||
|
),
|
||||||
footer
|
footer
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -1,16 +1,38 @@
|
||||||
use crate::{pretty, ExBudget};
|
use crate::{pretty, ExBudget};
|
||||||
use aiken_lang::ast::BinOp;
|
use aiken_lang::ast::BinOp;
|
||||||
|
use pallas::codec::utils::Int;
|
||||||
|
use pallas::ledger::primitives::alonzo::{BigInt, Constr, PlutusData};
|
||||||
use std::{
|
use std::{
|
||||||
borrow::Borrow,
|
borrow::Borrow,
|
||||||
fmt::{self, Display},
|
fmt::{self, Display},
|
||||||
path::{Path, PathBuf},
|
path::PathBuf,
|
||||||
rc::Rc,
|
rc::Rc,
|
||||||
};
|
};
|
||||||
use uplc::{
|
use uplc::{
|
||||||
ast::{Constant, Data, NamedDeBruijn, Program, Term},
|
ast::{Constant, Data, NamedDeBruijn, Program, Term},
|
||||||
machine::eval_result::EvalResult,
|
machine::{eval_result::EvalResult, value::from_pallas_bigint},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
//
|
||||||
|
// Test
|
||||||
|
//
|
||||||
|
// Aiken supports two kinds of tests: unit and property. A unit test is a simply
|
||||||
|
// UPLC program which returns must be a lambda that returns a boolean.
|
||||||
|
//
|
||||||
|
// A property on the other-hand is a template for generating tests, which is also
|
||||||
|
// a lambda but that takes an extra argument. The argument is generated from a
|
||||||
|
// fuzzer which is meant to yield random values in a pseudo-random (albeit seeded)
|
||||||
|
// sequence. On failures, the value that caused a failure is simplified using an
|
||||||
|
// approach similar to what's described in MiniThesis<https://github.com/DRMacIver/minithesis>,
|
||||||
|
// which is a simplified version of Hypothesis, a property-based testing framework
|
||||||
|
// with integrated shrinking.
|
||||||
|
//
|
||||||
|
// Our approach could perhaps be called "microthesis", as it implements a subset of
|
||||||
|
// minithesis. More specifically, we do not currently support pre-conditions, nor
|
||||||
|
// targets.
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum Test {
|
pub enum Test {
|
||||||
UnitTest(UnitTest),
|
UnitTest(UnitTest),
|
||||||
|
@ -26,7 +48,7 @@ impl Test {
|
||||||
name: String,
|
name: String,
|
||||||
can_error: bool,
|
can_error: bool,
|
||||||
program: Program<NamedDeBruijn>,
|
program: Program<NamedDeBruijn>,
|
||||||
evaluation_hint: Option<EvalHint>,
|
assertion: Option<Assertion>,
|
||||||
) -> Test {
|
) -> Test {
|
||||||
Test::UnitTest(UnitTest {
|
Test::UnitTest(UnitTest {
|
||||||
input_path,
|
input_path,
|
||||||
|
@ -34,7 +56,7 @@ impl Test {
|
||||||
name,
|
name,
|
||||||
program,
|
program,
|
||||||
can_error,
|
can_error,
|
||||||
evaluation_hint,
|
assertion,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -55,57 +77,13 @@ impl Test {
|
||||||
fuzzer,
|
fuzzer,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn name(&self) -> &str {
|
|
||||||
match self {
|
|
||||||
Test::UnitTest(test) => &test.name,
|
|
||||||
Test::PropertyTest(test) => &test.name,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn module(&self) -> &str {
|
// ----------------------------------------------------------------------------
|
||||||
match self {
|
//
|
||||||
Test::UnitTest(test) => &test.module,
|
// UnitTest
|
||||||
Test::PropertyTest(test) => &test.module,
|
//
|
||||||
}
|
// ----------------------------------------------------------------------------
|
||||||
}
|
|
||||||
|
|
||||||
pub fn input_path(&self) -> &Path {
|
|
||||||
match self {
|
|
||||||
Test::UnitTest(test) => &test.input_path,
|
|
||||||
Test::PropertyTest(test) => &test.input_path,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn program(&self) -> &Program<NamedDeBruijn> {
|
|
||||||
match self {
|
|
||||||
Test::UnitTest(test) => &test.program,
|
|
||||||
Test::PropertyTest(test) => &test.program,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn evaluation_hint(&self) -> Option<&EvalHint> {
|
|
||||||
match self {
|
|
||||||
Test::UnitTest(test) => test.evaluation_hint.as_ref(),
|
|
||||||
Test::PropertyTest(_) => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn report<'a>(&'a self, eval_result: &mut EvalResult) -> EvalInfo<'a> {
|
|
||||||
let can_error = match self {
|
|
||||||
Test::UnitTest(test) => test.can_error,
|
|
||||||
Test::PropertyTest(test) => test.can_error,
|
|
||||||
};
|
|
||||||
|
|
||||||
EvalInfo {
|
|
||||||
test: self,
|
|
||||||
success: !eval_result.failed(can_error),
|
|
||||||
spent_budget: eval_result.cost(),
|
|
||||||
logs: eval_result.logs(),
|
|
||||||
output: eval_result.result().ok(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct UnitTest {
|
pub struct UnitTest {
|
||||||
|
@ -114,17 +92,30 @@ pub struct UnitTest {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub can_error: bool,
|
pub can_error: bool,
|
||||||
pub program: Program<NamedDeBruijn>,
|
pub program: Program<NamedDeBruijn>,
|
||||||
pub evaluation_hint: Option<EvalHint>,
|
pub assertion: Option<Assertion>,
|
||||||
}
|
}
|
||||||
|
|
||||||
unsafe impl Send for UnitTest {}
|
unsafe impl Send for UnitTest {}
|
||||||
|
|
||||||
impl UnitTest {
|
impl UnitTest {
|
||||||
pub fn run(&self) -> EvalResult {
|
pub fn run(self) -> TestResult {
|
||||||
self.program.clone().eval(ExBudget::max())
|
let mut eval_result = self.program.clone().eval(ExBudget::max());
|
||||||
|
TestResult::UnitTestResult(UnitTestResult {
|
||||||
|
test: self.to_owned(),
|
||||||
|
success: !eval_result.failed(self.can_error),
|
||||||
|
spent_budget: eval_result.cost(),
|
||||||
|
logs: eval_result.logs(),
|
||||||
|
output: eval_result.result().ok(),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
//
|
||||||
|
// PropertyTest
|
||||||
|
//
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct PropertyTest {
|
pub struct PropertyTest {
|
||||||
pub input_path: PathBuf,
|
pub input_path: PathBuf,
|
||||||
|
@ -138,46 +129,514 @@ pub struct PropertyTest {
|
||||||
unsafe impl Send for PropertyTest {}
|
unsafe impl Send for PropertyTest {}
|
||||||
|
|
||||||
impl PropertyTest {
|
impl PropertyTest {
|
||||||
pub fn new_seed(seed: u32) -> Term<NamedDeBruijn> {
|
const MAX_TEST_RUN: usize = 100;
|
||||||
Term::Constant(Rc::new(Constant::Data(Data::constr(
|
|
||||||
0,
|
/// Run a property test from a given seed. The property is run at most MAX_TEST_RUN times. It
|
||||||
vec![
|
/// may stops earlier on failure; in which case a 'counterexample' is returned.
|
||||||
Data::integer(seed.into()),
|
pub fn run(self, seed: u32) -> TestResult {
|
||||||
Data::integer(0.into()), // Size
|
let n = PropertyTest::MAX_TEST_RUN;
|
||||||
],
|
|
||||||
))))
|
let (counterexample, iterations) = match self.run_n_times(n, seed, None) {
|
||||||
|
None => (None, n),
|
||||||
|
Some((remaining, counterexample)) => (Some(counterexample), n - remaining + 1),
|
||||||
|
};
|
||||||
|
|
||||||
|
TestResult::PropertyTestResult(PropertyTestResult {
|
||||||
|
test: self,
|
||||||
|
counterexample,
|
||||||
|
iterations,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn sample(&self, seed: Term<NamedDeBruijn>) -> (Term<NamedDeBruijn>, Term<NamedDeBruijn>) {
|
fn run_n_times(
|
||||||
let term = self.fuzzer.apply_term(&seed).eval(ExBudget::max()).result();
|
&self,
|
||||||
|
remaining: usize,
|
||||||
|
seed: u32,
|
||||||
|
counterexample: Option<(usize, Term<NamedDeBruijn>)>,
|
||||||
|
) -> Option<(usize, Term<NamedDeBruijn>)> {
|
||||||
|
// We short-circuit failures in case we have any. The counterexample is already simplified
|
||||||
|
// at this point.
|
||||||
|
if remaining > 0 && counterexample.is_none() {
|
||||||
|
let (next_seed, counterexample) = self.run_once(seed);
|
||||||
|
self.run_n_times(
|
||||||
|
remaining - 1,
|
||||||
|
next_seed,
|
||||||
|
counterexample.map(|c| (remaining, c)),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
counterexample
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if let Ok(Term::Constant(rc)) = term {
|
fn run_once(&self, seed: u32) -> (u32, Option<Term<NamedDeBruijn>>) {
|
||||||
match &rc.borrow() {
|
let (next_prng, value) = Prng::from_seed(seed)
|
||||||
Constant::ProtoPair(_, _, new_seed, value) => (
|
.sample(&self.fuzzer)
|
||||||
Term::Constant(new_seed.clone()),
|
.expect("running seeded Prng cannot fail.");
|
||||||
Term::Constant(value.clone()),
|
|
||||||
),
|
let result = self.program.apply_term(&value).eval(ExBudget::max());
|
||||||
_ => todo!("Fuzzer yielded a new seed that isn't an integer?"),
|
|
||||||
|
if let Prng::Seeded {
|
||||||
|
seed: next_seed, ..
|
||||||
|
} = next_prng
|
||||||
|
{
|
||||||
|
if result.failed(self.can_error) {
|
||||||
|
let mut counterexample = Counterexample {
|
||||||
|
result,
|
||||||
|
value,
|
||||||
|
choices: next_prng.choices(),
|
||||||
|
can_error: self.can_error,
|
||||||
|
program: &self.program,
|
||||||
|
fuzzer: &self.fuzzer,
|
||||||
|
};
|
||||||
|
|
||||||
|
if !counterexample.choices.is_empty() {
|
||||||
|
counterexample.simplify();
|
||||||
|
}
|
||||||
|
|
||||||
|
(next_seed, Some(counterexample.value))
|
||||||
|
} else {
|
||||||
|
(next_seed, None)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
todo!("Fuzzer yielded something else than a pair? {:#?}", term)
|
unreachable!("Prng constructed from a seed necessarily yield a seed.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn run(&self, sample: &Term<NamedDeBruijn>) -> EvalResult {
|
// ----------------------------------------------------------------------------
|
||||||
self.program.apply_term(sample).eval(ExBudget::max())
|
//
|
||||||
|
// Prng
|
||||||
|
//
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum Prng {
|
||||||
|
Seeded {
|
||||||
|
seed: u32,
|
||||||
|
choices: Vec<u32>,
|
||||||
|
uplc: PlutusData,
|
||||||
|
},
|
||||||
|
Replayed {
|
||||||
|
choices: Vec<u32>,
|
||||||
|
uplc: PlutusData,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Prng {
|
||||||
|
/// Constructor tag for Prng's 'Seeded'
|
||||||
|
const SEEDED: u64 = 0;
|
||||||
|
/// Constructor tag for Prng's 'Replayed'
|
||||||
|
const REPLAYED: u64 = 1;
|
||||||
|
|
||||||
|
/// Constructor tag for Option's 'Some'
|
||||||
|
const OK: u64 = 0;
|
||||||
|
/// Constructor tag for Option's 'None'
|
||||||
|
const ERR: u64 = 1;
|
||||||
|
|
||||||
|
pub fn uplc(&self) -> PlutusData {
|
||||||
|
match self {
|
||||||
|
Prng::Seeded { uplc, .. } => uplc.clone(),
|
||||||
|
Prng::Replayed { uplc, .. } => uplc.clone(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn choices(&self) -> Vec<u32> {
|
||||||
|
match self {
|
||||||
|
Prng::Seeded { choices, .. } => {
|
||||||
|
let mut choices = choices.to_vec();
|
||||||
|
choices.reverse();
|
||||||
|
choices
|
||||||
|
}
|
||||||
|
Prng::Replayed { choices, .. } => choices.to_vec(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Construct a Pseudo-random number generator from a seed.
|
||||||
|
pub fn from_seed(seed: u32) -> Prng {
|
||||||
|
Prng::Seeded {
|
||||||
|
seed,
|
||||||
|
choices: vec![],
|
||||||
|
uplc: Data::constr(
|
||||||
|
Prng::SEEDED,
|
||||||
|
vec![
|
||||||
|
Data::integer(seed.into()), // Prng's seed
|
||||||
|
Data::list(vec![]), // Random choices
|
||||||
|
],
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Construct a Pseudo-random number generator from a pre-defined list of choices.
|
||||||
|
pub fn from_choices(choices: &[u32]) -> Prng {
|
||||||
|
Prng::Replayed {
|
||||||
|
choices: choices.to_vec(),
|
||||||
|
uplc: Data::constr(
|
||||||
|
Prng::REPLAYED,
|
||||||
|
vec![Data::list(
|
||||||
|
choices.iter().map(|i| Data::integer((*i).into())).collect(),
|
||||||
|
)],
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate a pseudo-random value from a fuzzer using the given PRNG.
|
||||||
|
pub fn sample(&self, fuzzer: &Program<NamedDeBruijn>) -> Option<(Prng, Term<NamedDeBruijn>)> {
|
||||||
|
let result = fuzzer
|
||||||
|
.apply_data(self.uplc())
|
||||||
|
.eval(ExBudget::max())
|
||||||
|
.result()
|
||||||
|
.expect("Fuzzer crashed?");
|
||||||
|
|
||||||
|
Prng::from_result(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Obtain a Prng back from a fuzzer execution. As a reminder, fuzzers have the following
|
||||||
|
/// signature:
|
||||||
|
///
|
||||||
|
/// type Fuzzer<a> = fn(Prng) -> Option<(Prng, a)>
|
||||||
|
///
|
||||||
|
/// In nominal scenarios (i.e. when the fuzzer is made from a seed and evolve pseudo-randomly),
|
||||||
|
/// it cannot yield 'None'. When replayed however, we can't easily guarantee that the changes
|
||||||
|
/// made during shrinking aren't breaking underlying invariants (if only, because we run out of
|
||||||
|
/// values to replay). In such case, the replayed sequence is simply invalid and the fuzzer
|
||||||
|
/// aborted altogether with 'None'.
|
||||||
|
pub fn from_result(result: Term<NamedDeBruijn>) -> Option<(Self, Term<NamedDeBruijn>)> {
|
||||||
|
/// Interpret the given 'PlutusData' as one of two Prng constructors.
|
||||||
|
fn as_prng(cst: &PlutusData) -> Prng {
|
||||||
|
if let PlutusData::Constr(Constr { tag, fields, .. }) = cst {
|
||||||
|
if *tag == 121 + Prng::SEEDED {
|
||||||
|
if let [seed, PlutusData::Array(choices)] = &fields[..] {
|
||||||
|
return Prng::Seeded {
|
||||||
|
seed: as_u32(seed),
|
||||||
|
choices: choices.iter().map(as_u32).collect(),
|
||||||
|
uplc: cst.clone(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if *tag == 121 + Prng::REPLAYED {
|
||||||
|
return Prng::Replayed {
|
||||||
|
choices: fields.iter().map(as_u32).collect(),
|
||||||
|
uplc: cst.clone(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
panic!("Malformed Prng: {cst:#?}")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn as_u32(field: &PlutusData) -> u32 {
|
||||||
|
if let PlutusData::BigInt(BigInt::Int(Int(i))) = field {
|
||||||
|
return u32::try_from(*i).expect("Choice doesn't fit in u32?");
|
||||||
|
}
|
||||||
|
|
||||||
|
panic!("Malformed choice's value: {field:#?}")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert wrapped integer & bytearrays as raw constant terms. Because fuzzer
|
||||||
|
/// return a pair, those values end up being wrapped in 'Data', but test
|
||||||
|
/// functions will expect them in their raw constant form.
|
||||||
|
///
|
||||||
|
/// Anything else is Data, so we're good.
|
||||||
|
fn as_value(data: &PlutusData) -> Term<NamedDeBruijn> {
|
||||||
|
Term::Constant(Rc::new(match data {
|
||||||
|
PlutusData::BigInt(n) => Constant::Integer(from_pallas_bigint(n)),
|
||||||
|
PlutusData::BoundedBytes(bytes) => Constant::ByteString(bytes.clone().into()),
|
||||||
|
_ => Constant::Data(data.clone()),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Term::Constant(rc) = &result {
|
||||||
|
if let Constant::Data(PlutusData::Constr(Constr { tag, fields, .. })) = &rc.borrow() {
|
||||||
|
if *tag == 121 + Prng::OK {
|
||||||
|
if let [PlutusData::Array(elems)] = &fields[..] {
|
||||||
|
if let [new_seed, value] = &elems[..] {
|
||||||
|
return Some((as_prng(new_seed), as_value(value)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// May occurs when replaying a fuzzer from a shrinked sequence of
|
||||||
|
// choices. If we run out of choices, or a choice end up being
|
||||||
|
// invalid as per the expectation, the fuzzer can't go further and
|
||||||
|
// fail.
|
||||||
|
if *tag == 121 + Prng::ERR {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// In principle, this cannot happen provided that the 'result' was produced from a
|
||||||
|
// type-checked fuzzer. The type-checker enforces that fuzzers are of the right shape
|
||||||
|
// describe above.
|
||||||
|
unreachable!("Fuzzer yielded a malformed result? {result:#?}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
//
|
||||||
|
// Counterexample
|
||||||
|
//
|
||||||
|
// A counterexample is constructed on test failures.
|
||||||
|
//
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Counterexample<'a> {
|
||||||
|
pub value: Term<NamedDeBruijn>,
|
||||||
|
pub choices: Vec<u32>,
|
||||||
|
pub result: EvalResult,
|
||||||
|
pub can_error: bool,
|
||||||
|
pub program: &'a Program<NamedDeBruijn>,
|
||||||
|
pub fuzzer: &'a Program<NamedDeBruijn>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Counterexample<'a> {
|
||||||
|
fn consider(&mut self, choices: &[u32]) -> bool {
|
||||||
|
if choices == self.choices {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Memoize test cases & choices in a cache. Due to the nature of
|
||||||
|
// our integrated shrinking approach, we may end up re-executing the same
|
||||||
|
// test cases many times. Given that tests are fully deterministic, we can
|
||||||
|
// memoize the already seen choices to avoid re-running the generators and
|
||||||
|
// the test (which can be quite expensive).
|
||||||
|
match Prng::from_choices(choices).sample(self.fuzzer) {
|
||||||
|
// Shrinked choices led to an impossible generation.
|
||||||
|
None => false,
|
||||||
|
|
||||||
|
// Shrinked choices let to a new valid generated value, now, is it better?
|
||||||
|
Some((_, value)) => {
|
||||||
|
let result = self.program.apply_term(&value).eval(ExBudget::max());
|
||||||
|
|
||||||
|
// If the test no longer fails, it isn't better as we're only
|
||||||
|
// interested in counterexamples.
|
||||||
|
if !result.failed(self.can_error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If these new choices are shorter or smaller, then we pick them
|
||||||
|
// as new choices and inform that it's been an improvement.
|
||||||
|
if choices.len() <= self.choices.len() || choices < &self.choices {
|
||||||
|
self.value = value;
|
||||||
|
self.choices = choices.to_vec();
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Try to simplify a 'Counterexample' by manipulating the random sequence of generated values
|
||||||
|
/// (a.k.a. choices). While the implementation is quite involved, the strategy is rather simple
|
||||||
|
/// at least conceptually:
|
||||||
|
///
|
||||||
|
/// Each time a (seeded) fuzzer generates a new value and a new seed, it also stores the
|
||||||
|
/// generated value in a vector, which we call 'choices'. If we re-run the test case with this
|
||||||
|
/// exact choice sequence, we end up with the exact same outcome.
|
||||||
|
///
|
||||||
|
/// But, we can tweak chunks of this sequence in hope to generate a _smaller sequence_, thus
|
||||||
|
/// generally resulting in a _smaller counterexample_. Each transformations is applied on
|
||||||
|
/// chunks of size 8, 4, 2 and 1; until we no longer make progress (i.e. hit a fix point).
|
||||||
|
///
|
||||||
|
/// As per MiniThesis, we consider the following transformations:
|
||||||
|
///
|
||||||
|
/// - Deleting chunks
|
||||||
|
/// - Transforming chunks into sequence of zeroes
|
||||||
|
/// - Decrementing chunks of values
|
||||||
|
/// - Replacing chunks of values
|
||||||
|
/// - Sorting chunks
|
||||||
|
/// - Redistribute values between nearby pairs
|
||||||
|
fn simplify(&mut self) {
|
||||||
|
let mut prev;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
prev = self.choices.clone();
|
||||||
|
|
||||||
|
// Delete choices by chunks of size 8, 4, 2, 1.
|
||||||
|
let mut k: isize = 8;
|
||||||
|
while k > 0 {
|
||||||
|
let mut i: isize = (self.choices.len() as isize) - k - 1;
|
||||||
|
while i >= 0 {
|
||||||
|
if i >= self.choices.len() as isize {
|
||||||
|
i -= 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let mut choices = self.choices[0..(i + k) as usize].to_vec();
|
||||||
|
if !self.consider(&choices) {
|
||||||
|
// Perform an extra reduction step that decrease the size of choices near
|
||||||
|
// the end, to cope with dependencies between choices, e.g. drawing a
|
||||||
|
// number as a list length, and then drawing that many elements.
|
||||||
|
//
|
||||||
|
// This isn't perfect, but allows to make progresses in many cases.
|
||||||
|
if i > 0 && *choices.get((i - 1) as usize).unwrap_or(&0) > 0 {
|
||||||
|
choices[(i - 1) as usize] -= 1;
|
||||||
|
if self.consider(&choices) {
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
k /= 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now we try replacing region of choices with zeroes. Note that unlike the above we
|
||||||
|
// skip k = 1 because we handle that in the next step. Often (but not always) a block
|
||||||
|
// of all zeroes is the smallest value that a region can be.
|
||||||
|
let mut k: isize = 8;
|
||||||
|
while k > 1 {
|
||||||
|
let mut i: isize = self.choices.len() as isize - k;
|
||||||
|
|
||||||
|
while i >= 0 {
|
||||||
|
i -= if self.zeroes(i, k) { k } else { 1 }
|
||||||
|
}
|
||||||
|
|
||||||
|
k /= 2
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Remaining shrinking strategies...
|
||||||
|
//
|
||||||
|
// - Swaps
|
||||||
|
// - Sorting
|
||||||
|
// - Pair adjustments
|
||||||
|
|
||||||
|
// If we've reached a fixed point, then we cannot shrink further. We've reached a
|
||||||
|
// (local) minimum, which is as good as a counterexample we'll get with this approach.
|
||||||
|
if prev.as_slice() == self.choices.as_slice() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace a block between indices 'i' and 'k' by zeroes.
|
||||||
|
fn zeroes(&mut self, i: isize, k: isize) -> bool {
|
||||||
|
let mut choices = self.choices.clone();
|
||||||
|
|
||||||
|
for j in i..(i + k) {
|
||||||
|
if j >= self.choices.len() as isize {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
choices[j as usize] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.consider(&choices)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
//
|
||||||
|
// TestResult
|
||||||
|
//
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum TestResult {
|
||||||
|
UnitTestResult(UnitTestResult),
|
||||||
|
PropertyTestResult(PropertyTestResult),
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe impl Send for TestResult {}
|
||||||
|
|
||||||
|
impl TestResult {
|
||||||
|
pub fn is_success(&self) -> bool {
|
||||||
|
match self {
|
||||||
|
TestResult::UnitTestResult(UnitTestResult { success, .. }) => *success,
|
||||||
|
TestResult::PropertyTestResult(PropertyTestResult {
|
||||||
|
counterexample,
|
||||||
|
test,
|
||||||
|
..
|
||||||
|
}) => {
|
||||||
|
if test.can_error {
|
||||||
|
counterexample.is_some()
|
||||||
|
} else {
|
||||||
|
counterexample.is_none()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn module(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
TestResult::UnitTestResult(UnitTestResult { ref test, .. }) => test.module.as_str(),
|
||||||
|
TestResult::PropertyTestResult(PropertyTestResult { ref test, .. }) => {
|
||||||
|
test.module.as_str()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn title(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
TestResult::UnitTestResult(UnitTestResult { ref test, .. }) => test.name.as_str(),
|
||||||
|
TestResult::PropertyTestResult(PropertyTestResult { ref test, .. }) => {
|
||||||
|
test.name.as_str()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn logs(&self) -> &[String] {
|
||||||
|
match self {
|
||||||
|
TestResult::UnitTestResult(UnitTestResult { ref logs, .. }) => logs.as_slice(),
|
||||||
|
TestResult::PropertyTestResult(..) => &[],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn into_error(&self, verbose: bool) -> crate::Error {
|
||||||
|
let (name, path, assertion, src) = match self {
|
||||||
|
TestResult::UnitTestResult(UnitTestResult { test, .. }) => (
|
||||||
|
test.name.to_string(),
|
||||||
|
test.input_path.to_path_buf(),
|
||||||
|
test.assertion.as_ref().map(|hint| hint.to_string()),
|
||||||
|
test.program.to_pretty(),
|
||||||
|
),
|
||||||
|
TestResult::PropertyTestResult(PropertyTestResult { test, .. }) => (
|
||||||
|
test.name.to_string(),
|
||||||
|
test.input_path.to_path_buf(),
|
||||||
|
None,
|
||||||
|
test.program.to_pretty(),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
crate::Error::TestFailure {
|
||||||
|
name,
|
||||||
|
path,
|
||||||
|
assertion,
|
||||||
|
src,
|
||||||
|
verbose,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct UnitTestResult {
|
||||||
|
pub success: bool,
|
||||||
|
pub spent_budget: ExBudget,
|
||||||
|
pub output: Option<Term<NamedDeBruijn>>,
|
||||||
|
pub logs: Vec<String>,
|
||||||
|
pub test: UnitTest,
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe impl Send for UnitTestResult {}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct PropertyTestResult {
|
||||||
|
pub test: PropertyTest,
|
||||||
|
pub counterexample: Option<Term<NamedDeBruijn>>,
|
||||||
|
pub iterations: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe impl Send for PropertyTestResult {}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct EvalHint {
|
pub struct Assertion {
|
||||||
pub bin_op: BinOp,
|
pub bin_op: BinOp,
|
||||||
pub left: Program<NamedDeBruijn>,
|
pub left: Program<NamedDeBruijn>,
|
||||||
pub right: Program<NamedDeBruijn>,
|
pub right: Program<NamedDeBruijn>,
|
||||||
pub can_error: bool,
|
pub can_error: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for EvalHint {
|
impl Display for Assertion {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
let unlimited_budget = ExBudget {
|
let unlimited_budget = ExBudget {
|
||||||
mem: i64::MAX,
|
mem: i64::MAX,
|
||||||
|
@ -240,12 +699,3 @@ impl Display for EvalHint {
|
||||||
f.write_str(&msg)
|
f.write_str(&msg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct EvalInfo<'a> {
|
|
||||||
pub success: bool,
|
|
||||||
pub spent_budget: ExBudget,
|
|
||||||
pub output: Option<Term<NamedDeBruijn>>,
|
|
||||||
pub logs: Vec<String>,
|
|
||||||
pub test: &'a Test,
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,9 +1,6 @@
|
||||||
use crate::pretty;
|
use crate::pretty;
|
||||||
use crate::script::EvalInfo;
|
use crate::script::{PropertyTestResult, TestResult, UnitTestResult};
|
||||||
use owo_colors::{
|
use owo_colors::{OwoColorize, Stream::Stderr};
|
||||||
OwoColorize,
|
|
||||||
Stream::{self, Stderr},
|
|
||||||
};
|
|
||||||
use std::{collections::BTreeMap, fmt::Display, path::PathBuf};
|
use std::{collections::BTreeMap, fmt::Display, path::PathBuf};
|
||||||
use uplc::machine::cost_model::ExBudget;
|
use uplc::machine::cost_model::ExBudget;
|
||||||
|
|
||||||
|
@ -11,7 +8,7 @@ pub trait EventListener {
|
||||||
fn handle_event(&self, _event: Event) {}
|
fn handle_event(&self, _event: Event) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum Event<'a> {
|
pub enum Event {
|
||||||
StartingCompilation {
|
StartingCompilation {
|
||||||
name: String,
|
name: String,
|
||||||
version: String,
|
version: String,
|
||||||
|
@ -35,12 +32,9 @@ pub enum Event<'a> {
|
||||||
name: String,
|
name: String,
|
||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
},
|
},
|
||||||
EvaluatingFunction {
|
|
||||||
results: Vec<EvalInfo<'a>>,
|
|
||||||
},
|
|
||||||
RunningTests,
|
RunningTests,
|
||||||
FinishedTests {
|
FinishedTests {
|
||||||
tests: Vec<EvalInfo<'a>>,
|
tests: Vec<TestResult>,
|
||||||
},
|
},
|
||||||
WaitingForBuildDirLock,
|
WaitingForBuildDirLock,
|
||||||
ResolvingPackages {
|
ResolvingPackages {
|
||||||
|
@ -164,20 +158,6 @@ impl EventListener for Terminal {
|
||||||
name.if_supports_color(Stderr, |s| s.bright_blue()),
|
name.if_supports_color(Stderr, |s| s.bright_blue()),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Event::EvaluatingFunction { results } => {
|
|
||||||
eprintln!(
|
|
||||||
"{}\n",
|
|
||||||
" Evaluating function ..."
|
|
||||||
.if_supports_color(Stderr, |s| s.bold())
|
|
||||||
.if_supports_color(Stderr, |s| s.purple())
|
|
||||||
);
|
|
||||||
|
|
||||||
let (max_mem, max_cpu) = find_max_execution_units(&results);
|
|
||||||
|
|
||||||
for eval_info in &results {
|
|
||||||
println!(" {}", fmt_eval(eval_info, max_mem, max_cpu, Stderr))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Event::RunningTests => {
|
Event::RunningTests => {
|
||||||
eprintln!(
|
eprintln!(
|
||||||
"{} {}\n",
|
"{} {}\n",
|
||||||
|
@ -190,19 +170,19 @@ impl EventListener for Terminal {
|
||||||
Event::FinishedTests { tests } => {
|
Event::FinishedTests { tests } => {
|
||||||
let (max_mem, max_cpu) = find_max_execution_units(&tests);
|
let (max_mem, max_cpu) = find_max_execution_units(&tests);
|
||||||
|
|
||||||
for (module, infos) in &group_by_module(&tests) {
|
for (module, results) in &group_by_module(&tests) {
|
||||||
let title = module
|
let title = module
|
||||||
.if_supports_color(Stderr, |s| s.bold())
|
.if_supports_color(Stderr, |s| s.bold())
|
||||||
.if_supports_color(Stderr, |s| s.blue())
|
.if_supports_color(Stderr, |s| s.blue())
|
||||||
.to_string();
|
.to_string();
|
||||||
|
|
||||||
let tests = infos
|
let tests = results
|
||||||
.iter()
|
.iter()
|
||||||
.map(|eval_info| fmt_test(eval_info, max_mem, max_cpu, true))
|
.map(|r| fmt_test(r, max_mem, max_cpu, true))
|
||||||
.collect::<Vec<String>>()
|
.collect::<Vec<String>>()
|
||||||
.join("\n");
|
.join("\n");
|
||||||
|
|
||||||
let summary = fmt_test_summary(infos, true);
|
let summary = fmt_test_summary(results, true);
|
||||||
|
|
||||||
eprintln!(
|
eprintln!(
|
||||||
"{}\n",
|
"{}\n",
|
||||||
|
@ -269,22 +249,9 @@ impl EventListener for Terminal {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn fmt_test(eval_info: &EvalInfo, max_mem: usize, max_cpu: usize, styled: bool) -> String {
|
fn fmt_test(result: &TestResult, max_mem: usize, max_cpu: usize, styled: bool) -> String {
|
||||||
let EvalInfo {
|
// Status
|
||||||
success,
|
let mut test = if result.is_success() {
|
||||||
test,
|
|
||||||
spent_budget,
|
|
||||||
logs,
|
|
||||||
..
|
|
||||||
} = eval_info;
|
|
||||||
|
|
||||||
let ExBudget { mem, cpu } = spent_budget;
|
|
||||||
let mem_pad = pretty::pad_left(mem.to_string(), max_mem, " ");
|
|
||||||
let cpu_pad = pretty::pad_left(cpu.to_string(), max_cpu, " ");
|
|
||||||
|
|
||||||
let test = format!(
|
|
||||||
"{status} [mem: {mem_unit}, cpu: {cpu_unit}] {module}",
|
|
||||||
status = if *success {
|
|
||||||
pretty::style_if(styled, "PASS".to_string(), |s| {
|
pretty::style_if(styled, "PASS".to_string(), |s| {
|
||||||
s.if_supports_color(Stderr, |s| s.bold())
|
s.if_supports_color(Stderr, |s| s.bold())
|
||||||
.if_supports_color(Stderr, |s| s.green())
|
.if_supports_color(Stderr, |s| s.green())
|
||||||
|
@ -296,49 +263,97 @@ fn fmt_test(eval_info: &EvalInfo, max_mem: usize, max_cpu: usize, styled: bool)
|
||||||
.if_supports_color(Stderr, |s| s.red())
|
.if_supports_color(Stderr, |s| s.red())
|
||||||
.to_string()
|
.to_string()
|
||||||
})
|
})
|
||||||
},
|
};
|
||||||
|
|
||||||
|
// Execution units / iteration steps
|
||||||
|
match result {
|
||||||
|
TestResult::UnitTestResult(UnitTestResult { spent_budget, .. }) => {
|
||||||
|
let ExBudget { mem, cpu } = spent_budget;
|
||||||
|
let mem_pad = pretty::pad_left(mem.to_string(), max_mem, " ");
|
||||||
|
let cpu_pad = pretty::pad_left(cpu.to_string(), max_cpu, " ");
|
||||||
|
|
||||||
|
test = format!(
|
||||||
|
"{test} [mem: {mem_unit}, cpu: {cpu_unit}]",
|
||||||
mem_unit = pretty::style_if(styled, mem_pad, |s| s
|
mem_unit = pretty::style_if(styled, mem_pad, |s| s
|
||||||
.if_supports_color(Stderr, |s| s.cyan())
|
.if_supports_color(Stderr, |s| s.cyan())
|
||||||
.to_string()),
|
.to_string()),
|
||||||
cpu_unit = pretty::style_if(styled, cpu_pad, |s| s
|
cpu_unit = pretty::style_if(styled, cpu_pad, |s| s
|
||||||
.if_supports_color(Stderr, |s| s.cyan())
|
.if_supports_color(Stderr, |s| s.cyan())
|
||||||
.to_string()),
|
.to_string()),
|
||||||
module = pretty::style_if(styled, test.name().to_string(), |s| s
|
);
|
||||||
|
}
|
||||||
|
TestResult::PropertyTestResult(PropertyTestResult { iterations, .. }) => {
|
||||||
|
test = pretty::pad_right(
|
||||||
|
format!(
|
||||||
|
"{test} [after {} test{}]",
|
||||||
|
pretty::pad_left(iterations.to_string(), 3, " "),
|
||||||
|
if *iterations > 1 { "s" } else { "" }
|
||||||
|
),
|
||||||
|
14 + max_mem + max_cpu,
|
||||||
|
" ",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Title
|
||||||
|
test = format!(
|
||||||
|
"{test} {title}",
|
||||||
|
title = pretty::style_if(styled, result.title().to_string(), |s| s
|
||||||
.if_supports_color(Stderr, |s| s.bright_blue())
|
.if_supports_color(Stderr, |s| s.bright_blue())
|
||||||
.to_string()),
|
.to_string())
|
||||||
);
|
);
|
||||||
|
|
||||||
let logs = if logs.is_empty() {
|
// CounterExample
|
||||||
String::new()
|
// if let TestResult::PropertyTestResult(PropertyTestResult {
|
||||||
} else {
|
// counterexample: Some(counterexample),
|
||||||
logs.iter()
|
// ..
|
||||||
|
// }) = result
|
||||||
|
// {
|
||||||
|
// test = format!(
|
||||||
|
// "{test}\n{}",
|
||||||
|
// pretty::boxed_with(
|
||||||
|
// &pretty::style_if(styled, "counterexample".to_string(), |s| s
|
||||||
|
// .if_supports_color(Stderr, |s| s.red())
|
||||||
|
// .if_supports_color(Stderr, |s| s.bold())
|
||||||
|
// .to_string()),
|
||||||
|
// &counterexample.to_pretty(),
|
||||||
|
// |s| s.red().to_string()
|
||||||
|
// )
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Traces
|
||||||
|
if !result.logs().is_empty() {
|
||||||
|
test = format!(
|
||||||
|
"{test}\n{logs}",
|
||||||
|
logs = result
|
||||||
|
.logs()
|
||||||
|
.iter()
|
||||||
.map(|line| {
|
.map(|line| {
|
||||||
format!(
|
format!(
|
||||||
"{arrow} {styled_line}",
|
"{arrow} {styled_line}",
|
||||||
arrow = "↳".if_supports_color(Stderr, |s| s.bright_yellow()),
|
arrow = "↳".if_supports_color(Stderr, |s| s.bright_yellow()),
|
||||||
styled_line = line
|
styled_line = line
|
||||||
.split('\n')
|
.split('\n')
|
||||||
.map(|l| format!("{}", l.if_supports_color(Stderr, |s| s.bright_black())))
|
.map(|l| format!(
|
||||||
|
"{}",
|
||||||
|
l.if_supports_color(Stderr, |s| s.bright_black())
|
||||||
|
))
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
.join("\n")
|
.join("\n")
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
.join("\n")
|
.join("\n")
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
if logs.is_empty() {
|
|
||||||
test
|
test
|
||||||
} else {
|
|
||||||
[test, logs].join("\n")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn fmt_test_summary(tests: &[&EvalInfo], styled: bool) -> String {
|
fn fmt_test_summary(tests: &[&TestResult], styled: bool) -> String {
|
||||||
let (n_passed, n_failed) = tests
|
let (n_passed, n_failed) = tests.iter().fold((0, 0), |(n_passed, n_failed), result| {
|
||||||
.iter()
|
if result.is_success() {
|
||||||
.fold((0, 0), |(n_passed, n_failed), test_info| {
|
|
||||||
if test_info.success {
|
|
||||||
(n_passed + 1, n_failed)
|
(n_passed + 1, n_failed)
|
||||||
} else {
|
} else {
|
||||||
(n_passed, n_failed + 1)
|
(n_passed, n_failed + 1)
|
||||||
|
@ -360,44 +375,21 @@ fn fmt_test_summary(tests: &[&EvalInfo], styled: bool) -> String {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn fmt_eval(eval_info: &EvalInfo, max_mem: usize, max_cpu: usize, stream: Stream) -> String {
|
fn group_by_module(results: &Vec<TestResult>) -> BTreeMap<String, Vec<&TestResult>> {
|
||||||
let EvalInfo {
|
|
||||||
output,
|
|
||||||
test,
|
|
||||||
spent_budget,
|
|
||||||
..
|
|
||||||
} = eval_info;
|
|
||||||
|
|
||||||
let ExBudget { mem, cpu } = spent_budget;
|
|
||||||
|
|
||||||
format!(
|
|
||||||
" {}::{} [mem: {}, cpu: {}]\n │\n ╰─▶ {}",
|
|
||||||
test.module().if_supports_color(stream, |s| s.blue()),
|
|
||||||
test.name().if_supports_color(stream, |s| s.bright_blue()),
|
|
||||||
pretty::pad_left(mem.to_string(), max_mem, " "),
|
|
||||||
pretty::pad_left(cpu.to_string(), max_cpu, " "),
|
|
||||||
output
|
|
||||||
.as_ref()
|
|
||||||
.map(|x| format!("{x}"))
|
|
||||||
.unwrap_or_else(|| "Error.".to_string()),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn group_by_module<'a>(infos: &'a Vec<EvalInfo<'a>>) -> BTreeMap<String, Vec<&'a EvalInfo<'a>>> {
|
|
||||||
let mut modules = BTreeMap::new();
|
let mut modules = BTreeMap::new();
|
||||||
for eval_info in infos {
|
for r in results {
|
||||||
let xs: &mut Vec<&EvalInfo> = modules
|
let xs: &mut Vec<&TestResult> = modules.entry(r.module().to_string()).or_default();
|
||||||
.entry(eval_info.test.module().to_string())
|
xs.push(r);
|
||||||
.or_default();
|
|
||||||
xs.push(eval_info);
|
|
||||||
}
|
}
|
||||||
modules
|
modules
|
||||||
}
|
}
|
||||||
|
|
||||||
fn find_max_execution_units(xs: &[EvalInfo]) -> (usize, usize) {
|
fn find_max_execution_units(xs: &[TestResult]) -> (usize, usize) {
|
||||||
let (max_mem, max_cpu) = xs.iter().fold(
|
let (max_mem, max_cpu) = xs
|
||||||
(0, 0),
|
.iter()
|
||||||
|(max_mem, max_cpu), EvalInfo { spent_budget, .. }| {
|
.fold((0, 0), |(max_mem, max_cpu), test| match test {
|
||||||
|
TestResult::PropertyTestResult(..) => (max_mem, max_cpu),
|
||||||
|
TestResult::UnitTestResult(UnitTestResult { spent_budget, .. }) => {
|
||||||
if spent_budget.mem >= max_mem && spent_budget.cpu >= max_cpu {
|
if spent_budget.mem >= max_mem && spent_budget.cpu >= max_cpu {
|
||||||
(spent_budget.mem, spent_budget.cpu)
|
(spent_budget.mem, spent_budget.cpu)
|
||||||
} else if spent_budget.mem > max_mem {
|
} else if spent_budget.mem > max_mem {
|
||||||
|
@ -407,8 +399,8 @@ fn find_max_execution_units(xs: &[EvalInfo]) -> (usize, usize) {
|
||||||
} else {
|
} else {
|
||||||
(max_mem, max_cpu)
|
(max_mem, max_cpu)
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
);
|
});
|
||||||
|
|
||||||
(max_mem.to_string().len(), max_cpu.to_string().len())
|
(max_mem.to_string().len(), max_cpu.to_string().len())
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,7 +40,7 @@ impl EvalResult {
|
||||||
} else {
|
} else {
|
||||||
self.result.is_err()
|
self.result.is_err()
|
||||||
|| matches!(self.result, Ok(Term::Error))
|
|| matches!(self.result, Ok(Term::Error))
|
||||||
|| matches!(self.result, Ok(Term::Constant(ref con)) if matches!(con.as_ref(), Constant::Bool(false)))
|
|| !matches!(self.result, Ok(Term::Constant(ref con)) if matches!(con.as_ref(), Constant::Bool(true)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,49 @@
|
||||||
|
use aiken/builtin
|
||||||
|
|
||||||
|
const max_int: Int = 255
|
||||||
|
|
||||||
type PRNG {
|
type PRNG {
|
||||||
seed: Int,
|
Seeded { seed: Int, choices: List<Int> }
|
||||||
size: Int,
|
Replayed { choices: List<Int> }
|
||||||
}
|
}
|
||||||
|
|
||||||
fn any_int(prng: PRNG) {
|
fn any_int(prng: PRNG) -> Option<(PRNG, Int)> {
|
||||||
(prng, prng.seed)
|
when prng is {
|
||||||
|
Seeded { seed, choices } -> {
|
||||||
|
let digest =
|
||||||
|
seed
|
||||||
|
|> builtin.integer_to_bytearray(True, 32, _)
|
||||||
|
|> builtin.blake2b_256()
|
||||||
|
|
||||||
|
let choice =
|
||||||
|
digest
|
||||||
|
|> builtin.index_bytearray(0)
|
||||||
|
|
||||||
|
let new_seed =
|
||||||
|
digest
|
||||||
|
|> builtin.slice_bytearray(1, 4, _)
|
||||||
|
|> builtin.bytearray_to_integer(True, _)
|
||||||
|
|
||||||
|
Some((Seeded { seed: new_seed, choices: [choice, ..choices] }, choice))
|
||||||
}
|
}
|
||||||
|
|
||||||
test prop_test_foo(n via any_int) {
|
Replayed { choices } ->
|
||||||
n > 0
|
when choices is {
|
||||||
|
[] -> None
|
||||||
|
[head, ..tail] ->
|
||||||
|
if head >= 0 && head <= max_int {
|
||||||
|
Some((Replayed { choices: tail }, head))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test prop_foo_1(n via any_int) {
|
||||||
|
n >= 0 && n <= 255
|
||||||
|
}
|
||||||
|
|
||||||
|
test prop_foo_2(n via any_int) fail {
|
||||||
|
n < 100
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue