aiken/crates/aiken-project/src/test_framework.rs

550 lines
16 KiB
Rust

#[cfg(test)]
mod test {
use crate::{
module::{CheckedModule, CheckedModules},
utils,
};
use aiken_lang::{
ast::{DataTypeKey, Definition, ModuleKind, TraceLevel, Tracing, TypedDataType},
builtins,
expr::UntypedExpr,
format::Formatter,
gen_uplc::CodeGenerator,
line_numbers::LineNumbers,
parser::{self, extra::ModuleExtra},
plutus_version::PlutusVersion,
test_framework::*,
IdGenerator,
};
use indexmap::IndexMap;
use indoc::indoc;
use std::{
collections::{BTreeMap, HashMap},
path::PathBuf,
};
use uplc::PlutusData;
const TEST_KIND: ModuleKind = ModuleKind::Lib;
pub fn test_from_source(src: &str) -> (Test, IndexMap<DataTypeKey, TypedDataType>) {
let id_gen = IdGenerator::new();
let module_name = "";
let mut module_types = HashMap::new();
module_types.insert("aiken".to_string(), builtins::prelude(&id_gen));
module_types.insert("aiken/builtin".to_string(), builtins::plutus(&id_gen));
let mut warnings = vec![];
let (ast, _) = parser::module(src, TEST_KIND).expect("Failed to parse module");
let ast = ast
.infer(
&id_gen,
TEST_KIND,
module_name,
&module_types,
Tracing::All(TraceLevel::Verbose),
&mut warnings,
None,
)
.expect("Failed to type-check module.");
module_types.insert(module_name.to_string(), ast.type_info.clone());
let test = ast
.definitions()
.filter_map(|def| match def {
Definition::Test(test) => Some(test.clone()),
_ => None,
})
.last()
.expect("No test found in declared src?");
let mut functions = builtins::prelude_functions(&id_gen, &module_types);
let mut data_types = builtins::prelude_data_types(&id_gen);
let mut constants = IndexMap::new();
ast.register_definitions(&mut functions, &mut constants, &mut data_types);
let mut module_sources = HashMap::new();
module_sources.insert(
module_name.to_string(),
(src.to_string(), LineNumbers::new(src)),
);
let mut modules = CheckedModules::default();
modules.insert(
module_name.to_string(),
CheckedModule {
kind: TEST_KIND,
extra: ModuleExtra::default(),
name: module_name.to_string(),
code: src.to_string(),
ast,
package: String::new(),
input_path: PathBuf::new(),
},
);
let mut generator = CodeGenerator::new(
PlutusVersion::default(),
utils::indexmap::as_ref_values(&functions),
utils::indexmap::as_ref_values(&constants),
utils::indexmap::as_ref_values(&data_types),
utils::indexmap::as_str_ref_values(&module_types),
utils::indexmap::as_str_ref_values(&module_sources),
Tracing::All(TraceLevel::Verbose),
);
(
Test::from_function_definition(
&mut generator,
test.to_owned(),
module_name.to_string(),
PathBuf::new(),
),
data_types,
)
}
fn property(src: &str) -> (PropertyTest, impl Fn(PlutusData) -> String) {
let prelude = indoc! { r#"
use aiken/builtin
const max_int: Int = 255
pub fn int() -> Fuzzer<Int> {
fn(prng: PRNG) -> Option<(PRNG, Int)> {
when prng is {
Seeded { seed, choices } -> {
let choice =
seed
|> builtin.index_bytearray(0)
Some((
Seeded {
seed: builtin.blake2b_256(seed),
choices: builtin.cons_bytearray(choice, choices)
},
choice
))
}
Replayed { cursor, choices } -> {
if cursor >= 1 {
let cursor = cursor - 1
Some((
Replayed { choices, cursor },
builtin.index_bytearray(choices, cursor)
))
} else {
None
}
}
}
}
}
fn bool() -> Fuzzer<Bool> {
int() |> map(fn(n) { n % 2 == 0 })
}
fn bytearray() -> Fuzzer<ByteArray> {
int()
|> map(
fn(n) {
n
|> builtin.integer_to_bytearray(True, 32, _)
|> builtin.blake2b_256()
|> builtin.slice_bytearray(8, 4, _)
},
)
}
pub fn constant(a: a) -> Fuzzer<a> {
fn(s0) { Some((s0, a)) }
}
pub fn and_then(fuzz_a: Fuzzer<a>, f: fn(a) -> Fuzzer<b>) -> Fuzzer<b> {
fn(s0) {
when fuzz_a(s0) is {
Some((s1, a)) -> f(a)(s1)
None -> None
}
}
}
pub fn map(fuzz_a: Fuzzer<a>, f: fn(a) -> b) -> Fuzzer<b> {
fn(s0) {
when fuzz_a(s0) is {
Some((s1, a)) -> Some((s1, f(a)))
None -> None
}
}
}
pub fn map2(fuzz_a: Fuzzer<a>, fuzz_b: Fuzzer<b>, f: fn(a, b) -> c) -> Fuzzer<c> {
fn(s0) {
when fuzz_a(s0) is {
Some((s1, a)) ->
when fuzz_b(s1) is {
Some((s2, b)) -> Some((s2, f(a, b)))
None -> None
}
None -> None
}
}
}
"#};
let src = format!("{prelude}\n{src}");
match test_from_source(&src) {
(Test::PropertyTest(test), data_types) => {
let type_info = test.fuzzer.type_info.clone();
let reify = move |counterexample| {
let data_type_refs = utils::indexmap::as_ref_values(&data_types);
let expr = UntypedExpr::reify_data(&data_type_refs, counterexample, &type_info)
.expect("Failed to reify value.");
Formatter::new().expr(&expr, false).to_pretty_string(70)
};
(test, reify)
}
(Test::UnitTest(..), _) => {
panic!("Expected to yield a PropertyTest but found a UnitTest")
}
}
}
fn expect_failure<'a>(
prop: &'a PropertyTest,
plutus_version: &'a PlutusVersion,
) -> Counterexample<'a> {
let mut labels = BTreeMap::new();
let mut remaining = PropertyTest::DEFAULT_MAX_SUCCESS;
match prop.run_n_times(
&mut remaining,
Prng::from_seed(42),
&mut labels,
plutus_version,
) {
Ok(Some(counterexample)) => counterexample,
_ => panic!("expected property to fail but it didn't."),
}
}
#[test]
fn test_prop_basic() {
let (prop, _) = property(indoc! { r#"
test foo(n: Int via int()) {
n >= 0
}
"#});
assert!(prop
.run::<()>(
42,
PropertyTest::DEFAULT_MAX_SUCCESS,
&PlutusVersion::default()
)
.is_success());
}
#[test]
fn test_prop_labels() {
let (prop, _) = property(indoc! { r#"
fn label(str: String) -> Void {
str
|> builtin.append_string(@"\0", _)
|> builtin.debug(Void)
}
test foo(head_or_tail via bool()) {
if head_or_tail {
label(@"head")
} else {
label(@"tail")
}
True
}
"#});
match prop.run::<()>(
42,
PropertyTest::DEFAULT_MAX_SUCCESS,
&PlutusVersion::default(),
) {
TestResult::UnitTestResult(..) => unreachable!("property returned unit-test result ?!"),
TestResult::PropertyTestResult(result) => {
assert!(
result
.labels
.iter()
.eq(vec![(&"head".to_string(), &53), (&"tail".to_string(), &47)]),
"labels: {:#?}",
result.labels
)
}
}
}
#[test]
fn test_prop_always_odd() {
let (prop, reify) = property(indoc! { r#"
test foo(n: Int via int()) {
n % 2 == 0
}
"#});
let plutus_version = PlutusVersion::default();
let mut counterexample = expect_failure(&prop, &plutus_version);
counterexample.simplify();
assert_eq!(counterexample.choices, vec![1]);
assert_eq!(reify(counterexample.value), "1");
}
#[test]
fn test_prop_combine() {
let (prop, reify) = property(indoc! { r#"
fn pair(fuzz_a: Fuzzer<a>, fuzz_b: Fuzzer<b>) -> Fuzzer<(a, b)> {
fuzz_a
|> and_then(fn(a) {
fuzz_b
|> map(fn(b) {
(a, b)
})
})
}
test foo(t: (Int, Int) via pair(int(), int())) {
t.1st + t.2nd <= 400
}
"#});
let plutus_version = PlutusVersion::default();
let mut counterexample = expect_failure(&prop, &plutus_version);
counterexample.simplify();
assert_eq!(counterexample.choices, vec![149, 252]);
assert_eq!(reify(counterexample.value), "(149, 252)");
}
#[test]
fn test_prop_enum_bool() {
let (prop, reify) = property(indoc! { r#"
test foo(predicate via bool()) {
predicate
}
"#});
let plutus_version = PlutusVersion::default();
let mut counterexample = expect_failure(&prop, &plutus_version);
counterexample.simplify();
assert_eq!(counterexample.choices, vec![1]);
assert_eq!(reify(counterexample.value), "False");
}
#[test]
fn test_prop_enum_custom() {
let (prop, reify) = property(indoc! { r#"
type Temperature {
Hot
Cold
}
fn temperature() -> Fuzzer<Temperature> {
bool() |> map(fn(is_cold) {
if is_cold { Cold } else { Hot }
})
}
test foo(t via temperature()) {
t == Hot
}
"#});
let plutus_version = PlutusVersion::default();
let mut counterexample = expect_failure(&prop, &plutus_version);
counterexample.simplify();
assert_eq!(counterexample.choices, vec![0]);
assert_eq!(reify(counterexample.value), "Cold");
}
#[test]
fn test_prop_opaque() {
let (prop, reify) = property(indoc! { r#"
opaque type Temperature {
Hot
Cold
}
fn temperature() -> Fuzzer<Temperature> {
bool() |> map(fn(is_cold) {
if is_cold { Cold } else { Hot }
})
}
test foo(t via temperature()) {
t == Hot
}
"#});
let plutus_version = PlutusVersion::default();
let mut counterexample = expect_failure(&prop, &plutus_version);
counterexample.simplify();
assert_eq!(counterexample.choices, vec![0]);
assert_eq!(reify(counterexample.value), "Cold");
}
#[test]
fn test_prop_private_enum() {
let (prop, reify) = property(indoc! { r#"
type Vehicle {
Car { wheels: Int }
Bike { wheels: Int }
}
fn vehicle() -> Fuzzer<Vehicle> {
bool() |> map(fn(is_car) {
if is_car { Car(4) } else { Bike(2) }
})
}
test foo(v via vehicle()) {
when v is {
Car { wheels } -> wheels
Bike { wheels } -> wheels
} == 4
}
"#});
let plutus_version = PlutusVersion::default();
let mut counterexample = expect_failure(&prop, &plutus_version);
counterexample.simplify();
assert_eq!(counterexample.choices, vec![1]);
assert_eq!(reify(counterexample.value), "Bike { wheels: 2 }");
}
#[test]
fn test_prop_list() {
let (prop, reify) = property(indoc! { r#"
fn list(elem: Fuzzer<a>) -> Fuzzer<List<a>> {
bool()
|> and_then(fn(continue) {
if continue {
map2(elem, list(elem), fn(head, tail) { [head, ..tail] })
} else {
constant([])
}
})
}
fn length(es: List<a>) -> Int {
when es is {
[] -> 0
[_, ..tail] -> 1 + length(tail)
}
}
test foo(es: List<Int> via list(int())) {
length(es) < 3
}
"#});
let plutus_version = PlutusVersion::default();
let mut counterexample = expect_failure(&prop, &plutus_version);
counterexample.simplify();
assert_eq!(counterexample.choices, vec![0, 0, 0, 0, 0, 0, 1]);
assert_eq!(reify(counterexample.value), "[0, 0, 0]");
}
#[test]
fn test_prop_opaque_dict() {
let (prop, reify) = property(indoc! { r#"
pub opaque type Dict<a> {
inner: List<(ByteArray, a)>,
}
fn dict(elem: Fuzzer<a>) -> Fuzzer<Dict<a>> {
bool()
|> and_then(
fn(continue) {
if continue {
let kv = map2(bytearray(), elem, fn(k, v) { (k, v) })
map2(kv, dict(elem), fn(head, tail) { Dict([head, ..tail.inner]) })
} else {
constant(Dict([]))
}
},
)
}
test foo(d via dict(bool())) {
d == Dict([])
}
"#});
let plutus_version = PlutusVersion::default();
let mut counterexample = expect_failure(&prop, &plutus_version);
counterexample.simplify();
assert_eq!(counterexample.choices, vec![0, 0, 0, 1]);
assert_eq!(reify(counterexample.value), "Dict([(#\"2cd15ed0\", True)])");
}
#[test]
fn test_prop_opaque_nested_dict() {
let (prop, reify) = property(indoc! { r#"
pub opaque type Dict<a> {
inner: List<(ByteArray, a)>,
}
fn dict(elem: Fuzzer<a>) -> Fuzzer<Dict<a>> {
bool()
|> and_then(
fn(continue) {
if continue {
let kv = map2(bytearray(), elem, fn(k, v) { (k, v) })
map2(kv, dict(elem), fn(head, tail) { Dict([head, ..tail.inner]) })
} else {
constant(Dict([]))
}
},
)
}
test foo(d via dict(dict(int()))) {
d == Dict([])
}
"#});
let plutus_version = PlutusVersion::default();
let mut counterexample = expect_failure(&prop, &plutus_version);
counterexample.simplify();
assert_eq!(counterexample.choices, vec![0, 0, 1, 1]);
assert_eq!(
reify(counterexample.value),
"Dict([(#\"2cd15ed0\", Dict([]))])"
);
}
}