diff --git a/CHANGELOG.md b/CHANGELOG.md index 2215fae3..fbff65f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,11 +8,13 @@ - **aiken-lang**: New types `PRNG` and `Fuzzer` in the prelude. @KtorZ - **aiken-lang**: Test definitions now accept an (optional) argument alongside a new keyword `via` to specify fuzzers. @KtorZ - **aiken-project**: Property-based testing framework with integrated shrinking. @KtorZ +- **aiken-project**: Unit tests now show assertion operands as Aiken expression instead of raw UPLC . @KtorZ - **aiken**: The `check` command now accept an extra arg `--seed` to provide an initial seed for the pseudo-random generator of properties. @KtorZ ### Fixed - **uplc**: `serialise_data` builtin wrongly encoding some larger ints as tagged CBOR bigints, instead of plain integers over 9 bytes. @KtorZ +- **aiken-project**: Unit tests reports are now inline with the test with less noise. @KtorZ ### Changed diff --git a/Cargo.lock b/Cargo.lock index e3ed4c10..998a1627 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -30,9 +30,9 @@ dependencies = [ [[package]] name = "ahash" -version = "0.8.10" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b79b82693f705137f8fb9b37871d99e4f9a7df12b917eed79c3d3954830a60b" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" dependencies = [ "cfg-if", "once_cell", @@ -156,6 +156,7 @@ dependencies = [ "tokio", "toml", "uplc", + "vec1", "walkdir", "zip", ] @@ -520,10 +521,11 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.88" +version = "1.0.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02f341c093d19155a6e41631ce5971aac4e9a868262212153124c15fa22d1cdc" +checksum = "8cd6604a82acf3039f1144f54b8eb34e91ffba622051189e71b781822d5ee1f5" dependencies = [ + "jobserver", "libc", ] @@ -535,9 +537,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.34" +version = "0.4.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bc015644b92d5890fab7489e49d21f879d5c990186827d42ec511919404f38b" +checksum = "8eaf5903dcbc0a39312feb77df2ff4c76387d591b9fc7b04a238dcf8bb62639a" dependencies = [ "num-traits", ] @@ -564,9 +566,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.1" +version = "4.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c918d541ef2913577a0f9566e9ce27cb35b6df072075769e0b26cb5a554520da" +checksum = "b230ab84b0ffdf890d5a10abdbc8b83ae1c4918275daea1ab8801f71536b2651" dependencies = [ "clap_builder", "clap_derive", @@ -574,9 +576,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.1" +version = "4.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f3e7391dad68afb0c2ede1bf619f579a3dc9c2ec67f089baa397123a2f3d1eb" +checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4" dependencies = [ "anstream", "anstyle", @@ -1254,9 +1256,9 @@ dependencies = [ [[package]] name = "http" -version = "0.2.11" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8947b1a6fad4393052c7ba1f4cd97bed3e953a95c79c92ad9b051a04611d9fbb" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" dependencies = [ "bytes", "fnv", @@ -1443,9 +1445,9 @@ dependencies = [ [[package]] name = "insta" -version = "1.35.1" +version = "1.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c985c1bef99cf13c58fade470483d81a2bfe846ebde60ed28cc2dddec2df9e2" +checksum = "0a7c22c4d34ef4788c351e971c52bfdfe7ea2766f8c5466bc175dd46e52ac22e" dependencies = [ "console", "lazy_static", @@ -1494,10 +1496,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" [[package]] -name = "js-sys" -version = "0.3.68" +name = "jobserver" +version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "406cda4b368d531c842222cf9d2600a9a4acce8d29423695379c6868a143a9ee" +checksum = "ab46a6e9526ddef3ae7f787c06f0f2600639ba80ea3eade3d8e670a2230f51d6" +dependencies = [ + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" dependencies = [ "wasm-bindgen", ] @@ -2230,18 +2241,18 @@ dependencies = [ [[package]] name = "pin-project" -version = "1.1.4" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0302c4a0442c456bd56f841aee5c3bfd17967563f6fadc9ceb9f9c23cf3807e0" +checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.4" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "266c042b60c9c76b8d53061e52b2e0d1116abc57cefc8c5cd671619a56ac3690" +checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", @@ -2531,9 +2542,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.5" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bb987efffd3c6d0d8f5f89510bb458559eab11e4f869acb20bf845e016259cd" +checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" dependencies = [ "aho-corasick", "memchr", @@ -3570,9 +3581,9 @@ dependencies = [ [[package]] name = "walkdir" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" dependencies = [ "same-file", "winapi-util", @@ -3595,9 +3606,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.91" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1e124130aee3fb58c5bdd6b639a0509486b0338acaaae0c84a5124b0f588b7f" +checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -3605,9 +3616,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.91" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e7e1900c352b609c8488ad12639a311045f40a35491fb69ba8c12f758af70b" +checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" dependencies = [ "bumpalo", "log", @@ -3620,9 +3631,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.41" +version = "0.4.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877b9c3f61ceea0e56331985743b13f3d25c406a7098d45180fb5f09bc19ed97" +checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0" dependencies = [ "cfg-if", "js-sys", @@ -3632,9 +3643,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.91" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b30af9e2d358182b5c7449424f017eba305ed32a7010509ede96cdc4696c46ed" +checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3642,9 +3653,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.91" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "642f325be6301eb8107a83d12a8ac6c1e1c54345a7ef1a9261962dfefda09e66" +checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", @@ -3655,15 +3666,15 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.91" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f186bd2dcf04330886ce82d6f33dd75a7bfcf69ecf5763b89fcde53b6ac9838" +checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" [[package]] name = "web-sys" -version = "0.3.68" +version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96565907687f7aceb35bc5fc03770a8a0471d82e479f25832f54a0e3f4b28446" +checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" dependencies = [ "js-sys", "wasm-bindgen", diff --git a/crates/aiken-lang/src/ast.rs b/crates/aiken-lang/src/ast.rs index b3b95ab8..904c05b7 100644 --- a/crates/aiken-lang/src/ast.rs +++ b/crates/aiken-lang/src/ast.rs @@ -273,56 +273,6 @@ impl From for TypedFunction { } } -impl TypedTest { - pub fn test_hint(&self) -> Option<(BinOp, Box, Box)> { - if self.arguments.is_empty() { - do_test_hint(&self.body) - } else { - None - } - } -} - -pub fn do_test_hint(body: &TypedExpr) -> Option<(BinOp, Box, Box)> { - match body { - TypedExpr::BinOp { - name, - tipo, - left, - right, - .. - } if tipo == &bool() => Some((*name, left.clone(), right.clone())), - TypedExpr::Sequence { expressions, .. } | TypedExpr::Pipeline { expressions, .. } => { - if let Some((binop, left, right)) = do_test_hint(&expressions[expressions.len() - 1]) { - let mut new_left_expressions = expressions.clone(); - new_left_expressions.pop(); - new_left_expressions.push(*left); - - let mut new_right_expressions = expressions.clone(); - new_right_expressions.pop(); - new_right_expressions.push(*right); - - Some(( - binop, - TypedExpr::Sequence { - expressions: new_left_expressions, - location: Span::empty(), - } - .into(), - TypedExpr::Sequence { - expressions: new_right_expressions, - location: Span::empty(), - } - .into(), - )) - } else { - None - } - } - _ => None, - } -} - #[derive(Debug, Clone, PartialEq)] pub struct TypeAlias { pub alias: String, diff --git a/crates/aiken-project/Cargo.toml b/crates/aiken-project/Cargo.toml index 1279c0f9..fc5f6f1a 100644 --- a/crates/aiken-project/Cargo.toml +++ b/crates/aiken-project/Cargo.toml @@ -45,6 +45,7 @@ aiken-lang = { path = "../aiken-lang", version = "1.0.24-alpha" } uplc = { path = '../uplc', version = "1.0.24-alpha" } num-bigint = "0.4.4" cryptoxide = "0.4.4" +vec1 = "1.10.1" [dev-dependencies] blst = "0.3.11" diff --git a/crates/aiken-project/src/error.rs b/crates/aiken-project/src/error.rs index 94553372..a29b36c3 100644 --- a/crates/aiken-project/src/error.rs +++ b/crates/aiken-project/src/error.rs @@ -93,7 +93,6 @@ pub enum Error { path: PathBuf, verbose: bool, src: String, - assertion: Option, }, #[error( @@ -331,10 +330,7 @@ impl Diagnostic for Error { )), Error::TomlLoading { .. } => None, Error::Format { .. } => None, - Error::TestFailure { assertion, .. } => match assertion { - None => None, - Some(hint) => Some(Box::new(hint.to_string())), - }, + Error::TestFailure { .. } => None, Error::Http(_) => None, Error::ZipExtract(_) => None, Error::JoinError(_) => None, diff --git a/crates/aiken-project/src/lib.rs b/crates/aiken-project/src/lib.rs index d9b27731..e362534f 100644 --- a/crates/aiken-project/src/lib.rs +++ b/crates/aiken-project/src/lib.rs @@ -843,8 +843,6 @@ where .into_par_iter() .map(|test| match test { Test::UnitTest(unit_test) => unit_test.run(), - // TODO: Get the seed from the command-line, defaulting to a random one when not - // provided. Test::PropertyTest(property_test) => property_test.run(seed), }) .collect::>>() diff --git a/crates/aiken-project/src/telemetry.rs b/crates/aiken-project/src/telemetry.rs index b986de04..8b22ebc9 100644 --- a/crates/aiken-project/src/telemetry.rs +++ b/crates/aiken-project/src/telemetry.rs @@ -305,14 +305,10 @@ fn fmt_test( ); } TestResult::PropertyTestResult(PropertyTestResult { iterations, .. }) => { - test = pretty::pad_right( - format!( - "{test} [after {} test{}]", - pretty::pad_left(iterations.to_string(), max_iter, " "), - if *iterations > 1 { "s" } else { "" } - ), - 18 + max_mem + max_cpu + max_iter, - " ", + test = format!( + "{test} [after {} test{}]", + pretty::pad_left(iterations.to_string(), max_iter, " "), + if *iterations > 1 { "s" } else { "" } ); } } @@ -325,6 +321,22 @@ fn fmt_test( .to_string()) ); + // Annotations + match result { + TestResult::UnitTestResult(UnitTestResult { + test: unit_test, .. + }) if !result.is_success() => { + if let Some(ref assertion) = unit_test.assertion { + test = format!( + "{test}\n{}{new_line}", + assertion.to_string(Stderr, unit_test.can_error), + new_line = if result.logs().is_empty() { "\n" } else { "" }, + ); + } + } + _ => (), + } + // CounterExample if let TestResult::PropertyTestResult(PropertyTestResult { counterexample: Some(counterexample), @@ -334,7 +346,7 @@ fn fmt_test( let is_expected_failure = result.is_success(); test = format!( - "{test}\n{}\n{}\n", + "{test}\n{}\n{}{new_line}", if is_expected_failure { "★ counterexample" .if_supports_color(Stderr, |s| s.green()) @@ -362,31 +374,20 @@ fn fmt_test( ) }) .collect::>() - .join("\n") + .join("\n"), + new_line = if result.logs().is_empty() { "\n" } else { "" }, ); } // Traces if !result.logs().is_empty() { test = format!( - "{test}\n{logs}", + "{test}\n{title}\n{logs}\n", + title = "· with traces".if_supports_color(Stderr, |s| s.bold()), logs = result .logs() .iter() - .map(|line| { - format!( - "{arrow} {styled_line}", - arrow = "↳".if_supports_color(Stderr, |s| s.bright_yellow()), - styled_line = line - .split('\n') - .map(|l| format!( - "{}", - l.if_supports_color(Stderr, |s| s.bright_black()) - )) - .collect::>() - .join("\n") - ) - }) + .map(|line| { format!("| {line}",) }) .collect::>() .join("\n") ); diff --git a/crates/aiken-project/src/test_framework.rs b/crates/aiken-project/src/test_framework.rs index 219b3706..14ab2a64 100644 --- a/crates/aiken-project/src/test_framework.rs +++ b/crates/aiken-project/src/test_framework.rs @@ -1,23 +1,21 @@ -use crate::pretty; use aiken_lang::{ - ast::{Arg, BinOp, DataTypeKey, TypedDataType, TypedTest}, - expr::UntypedExpr, + ast::{Arg, BinOp, DataTypeKey, IfBranch, Span, TypedDataType, TypedTest}, + builtins::bool, + expr::{TypedExpr, UntypedExpr}, + format::Formatter, gen_uplc::{builder::convert_opaque_type, CodeGenerator}, tipo::Type, }; use cryptoxide::{blake2b::Blake2b, digest::Digest}; use indexmap::IndexMap; +use owo_colors::{OwoColorize, Stream}; use pallas::ledger::primitives::alonzo::{Constr, PlutusData}; -use std::{ - borrow::Borrow, - fmt::{self, Display}, - path::PathBuf, - rc::Rc, -}; +use std::{borrow::Borrow, convert::TryFrom, path::PathBuf, rc::Rc}; use uplc::{ ast::{Constant, Data, Name, NamedDeBruijn, Program, Term}, machine::{cost_model::ExBudget, eval_result::EvalResult}, }; +use vec1::{vec1, Vec1}; /// ----- Test ----------------------------------------------------------------- /// @@ -46,20 +44,50 @@ unsafe impl Send for Test {} impl Test { pub fn unit_test( + generator: &mut CodeGenerator<'_>, + test: TypedTest, + module_name: String, input_path: PathBuf, - module: String, - name: String, - can_error: bool, - program: Program, - assertion: Option, ) -> Test { + let data_types = generator.data_types().clone(); + + let program = generator.generate_raw(&test.body, &[], &module_name); + + let assertion = match test.body.try_into() { + Err(..) => None, + Ok(Assertion { bin_op, head, tail }) => { + let as_constant = |generator: &mut CodeGenerator<'_>, side| { + Program::::try_from(generator.generate_raw( + &side, + &[], + &module_name, + )) + .expect("failed to convert assertion operaand to NamedDeBruijn") + .eval(ExBudget::max()) + .unwrap_constant() + .map(|cst| { + UntypedExpr::reify_constant(&data_types, cst, &side.tipo()) + .expect("failed to reify assertion operand?") + }) + }; + + Some(Assertion { + bin_op, + head: as_constant(generator, head.expect("cannot be Err at this point")), + tail: tail + .expect("cannot be Err at this point") + .try_mapped(|e| as_constant(generator, e)), + }) + } + }; + Test::UnitTest(UnitTest { input_path, - module, - name, + module: module_name, + name: test.name, program, - can_error, assertion, + can_error: test.can_error, }) } @@ -88,39 +116,7 @@ impl Test { input_path: PathBuf, ) -> Test { if test.arguments.is_empty() { - let program = generator.generate_raw(&test.body, &[], &module_name); - - // TODO: Check whether we really need to clone the _entire_ generator, or whether we - // can mostly copy the generator and only clone parts that matters. - let assertion = test.test_hint().map(|(bin_op, left_src, right_src)| { - let left = generator - .clone() - .generate_raw(&left_src, &[], &module_name) - .try_into() - .unwrap(); - - let right = generator - .clone() - .generate_raw(&right_src, &[], &module_name) - .try_into() - .unwrap(); - - Assertion { - bin_op, - left, - right, - can_error: test.can_error, - } - }); - - Self::unit_test( - input_path, - module_name, - test.name, - test.can_error, - program.try_into().unwrap(), - assertion, - ) + Self::unit_test(generator, test, module_name, input_path) } else { let parameter = test.arguments.first().unwrap().to_owned(); @@ -168,18 +164,23 @@ pub struct UnitTest { pub module: String, pub name: String, pub can_error: bool, - pub program: Program, - pub assertion: Option, + pub program: Program, + pub assertion: Option>, } unsafe impl Send for UnitTest {} impl UnitTest { pub fn run(self) -> TestResult { - let mut eval_result = self.program.clone().eval(ExBudget::max()); + let mut eval_result = Program::::try_from(self.program.clone()) + .unwrap() + .eval(ExBudget::max()); + + let success = !eval_result.failed(self.can_error); + TestResult::UnitTestResult(UnitTestResult { + success, 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(), @@ -399,8 +400,10 @@ impl Prng { fn as_prng(cst: &PlutusData) -> Prng { if let PlutusData::Constr(Constr { tag, fields, .. }) = cst { if *tag == 121 + Prng::SEEDED { - if let [PlutusData::BoundedBytes(bytes), PlutusData::BoundedBytes(choices)] = - &fields[..] + if let [ + PlutusData::BoundedBytes(bytes), + PlutusData::BoundedBytes(choices), + ] = &fields[..] { return Prng::Seeded { choices: choices.to_vec(), @@ -741,24 +744,21 @@ impl TestResult { } pub fn into_error(&self, verbose: bool) -> crate::Error { - let (name, path, assertion, src) = match self { + let (name, path, 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, } @@ -809,74 +809,233 @@ impl PropertyTestResult { } #[derive(Debug, Clone)] -pub struct Assertion { +pub struct Assertion { pub bin_op: BinOp, - pub left: Program, - pub right: Program, - pub can_error: bool, + pub head: Result, + pub tail: Result, ()>, } -impl Display for Assertion { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let unlimited_budget = ExBudget { - mem: i64::MAX, - cpu: i64::MAX, +impl TryFrom for Assertion { + type Error = (); + + fn try_from(body: TypedExpr) -> Result { + match body { + TypedExpr::BinOp { + name, + tipo, + left, + right, + .. + } if tipo == bool() => { + // 'and' and 'or' are left-associative operators. + match (*right).clone().try_into() { + Ok(Assertion { + bin_op, + head: Ok(head), + tail: Ok(tail), + .. + }) if bin_op == name => { + let mut both = vec1![head]; + both.extend(tail); + Ok(Assertion { + bin_op: name, + head: Ok(*left), + tail: Ok(both), + }) + } + _ => Ok(Assertion { + bin_op: name, + head: Ok(*left), + tail: Ok(vec1![*right]), + }), + } + } + + // NOTE drill through trace-if-false operators for better errors. + TypedExpr::If { + branches, + final_else, + .. + } => { + if let [ + IfBranch { + condition, body, .. + }, + ] = &branches[..] + { + let then_is_true = match body { + TypedExpr::Var { + name, constructor, .. + } => name == "True" && constructor.tipo == bool(), + _ => false, + }; + + let else_is_wrapped_false = match *final_else { + TypedExpr::Trace { then, .. } => match *then { + TypedExpr::Var { + name, constructor, .. + } => name == "False" && constructor.tipo == bool(), + _ => false, + }, + _ => false, + }; + + if then_is_true && else_is_wrapped_false { + return condition.to_owned().try_into(); + } + } + + Err(()) + } + + TypedExpr::Trace { then, .. } => (*then).try_into(), + + TypedExpr::Sequence { expressions, .. } | TypedExpr::Pipeline { expressions, .. } => { + if let Ok(Assertion { + bin_op, + head: Ok(head), + tail: Ok(tail), + }) = expressions.last().unwrap().to_owned().try_into() + { + let replace = |expr| { + let mut expressions = expressions.clone(); + expressions.pop(); + expressions.push(expr); + TypedExpr::Sequence { + expressions, + location: Span::empty(), + } + }; + + Ok(Assertion { + bin_op, + head: Ok(replace(head)), + tail: Ok(tail.mapped(replace)), + }) + } else { + Err(()) + } + } + _ => Err(()), + } + } +} + +impl Assertion { + #[allow(clippy::just_underscores_and_digits)] + pub fn to_string(&self, stream: Stream, expect_failure: bool) -> String { + let red = |s: &str| { + format!("× {s}") + .if_supports_color(stream, |s| s.red()) + .if_supports_color(stream, |s| s.bold()) + .to_string() }; - let left = pretty::boxed( - "left", - &match self.left.clone().eval(unlimited_budget).result() { - Ok(term) => format!("{term}"), - Err(err) => format!("{err}"), - }, - ); - let right = pretty::boxed( - "right", - &match self.right.clone().eval(unlimited_budget).result() { - Ok(term) => format!("{term}"), - Err(err) => format!("{err}"), - }, - ); - let msg = if self.can_error { - match self.bin_op { - BinOp::And => Some(format!( - "{left}\n\nand\n\n{right}\n\nare both true but shouldn't." - )), - BinOp::Or => Some(format!( - "neither\n\n{left}\n\nnor\n\n{right}\n\nshould be true." - )), - BinOp::Eq => Some(format!("{left}\n\nshould not be equal to\n\n{right}")), - BinOp::NotEq => Some(format!("{left}\n\nshould be equal to\n\n{right}")), - BinOp::LtInt => Some(format!( - "{left}\n\nshould be greater than or equal to\n\n{right}" - )), - BinOp::LtEqInt => Some(format!("{left}\n\nshould be greater than\n\n{right}")), - BinOp::GtEqInt => Some(format!( - "{left}\n\nshould be lower than or equal\n\n{right}" - )), - BinOp::GtInt => Some(format!("{left}\n\nshould be lower than\n\n{right}")), - _ => None, - } - } else { - match self.bin_op { - BinOp::And => Some(format!("{left}\n\nand\n\n{right}\n\nshould both be true.")), - BinOp::Or => Some(format!("{left}\n\nor\n\n{right}\n\nshould be true.")), - BinOp::Eq => Some(format!("{left}\n\nshould be equal to\n\n{right}")), - BinOp::NotEq => Some(format!("{left}\n\nshould not be equal to\n\n{right}")), - BinOp::LtInt => Some(format!("{left}\n\nshould be lower than\n\n{right}")), - BinOp::LtEqInt => Some(format!( - "{left}\n\nshould be lower than or equal to\n\n{right}" - )), - BinOp::GtEqInt => Some(format!("{left}\n\nshould be greater than\n\n{right}")), - BinOp::GtInt => Some(format!( - "{left}\n\nshould be greater than or equal to\n\n{right}" - )), - _ => None, - } + if self.head.is_err() { + return red("program failed"); } - .ok_or(fmt::Error)?; - f.write_str(&msg) + fn fmt_side(side: &UntypedExpr, stream: Stream) -> String { + let __ = "│".if_supports_color(stream, |s| s.red()); + + Formatter::new() + .expr(side, false) + .to_pretty_string(60) + .lines() + .map(|line| format!("{__} {line}")) + .collect::>() + .join("\n") + } + + let left = fmt_side(self.head.as_ref().unwrap(), stream); + + let tail = self.tail.as_ref().unwrap(); + + let right = fmt_side(tail.first(), stream); + + format!( + "{}{}{}", + red("expected"), + if expect_failure && self.bin_op == BinOp::Or { + " neither\n" + .if_supports_color(stream, |s| s.red()) + .if_supports_color(stream, |s| s.bold()) + .to_string() + } else { + "\n".to_string() + }, + if expect_failure { + match self.bin_op { + BinOp::And => [ + left, + red("and"), + [ + tail.mapped_ref(|s| fmt_side(s, stream)) + .join(format!("\n{}\n", red("and")).as_str()), + if tail.len() > 1 { + red("to not all be true") + } else { + red("to not both be true") + }, + ] + .join("\n"), + ], + BinOp::Or => [ + left, + red("nor"), + [ + tail.mapped_ref(|s| fmt_side(s, stream)) + .join(format!("\n{}\n", red("nor")).as_str()), + red("to be true"), + ] + .join("\n"), + ], + BinOp::Eq => [left, red("to not equal"), right], + BinOp::NotEq => [left, red("to not be different"), right], + BinOp::LtInt => [left, red("to not be lower than"), right], + BinOp::LtEqInt => [left, red("to not be lower than or equal to"), right], + BinOp::GtInt => [left, red("to not be greater than"), right], + BinOp::GtEqInt => [left, red("to not be greater than or equal to"), right], + _ => unreachable!("unexpected non-boolean binary operator in assertion?"), + } + .join("\n") + } else { + match self.bin_op { + BinOp::And => [ + left, + red("and"), + [ + tail.mapped_ref(|s| fmt_side(s, stream)) + .join(format!("\n{}\n", red("and")).as_str()), + if tail.len() > 1 { + red("to all be true") + } else { + red("to both be true") + }, + ] + .join("\n"), + ], + BinOp::Or => [ + left, + red("or"), + [ + tail.mapped_ref(|s| fmt_side(s, stream)) + .join(format!("\n{}\n", red("or")).as_str()), + red("to be true"), + ] + .join("\n"), + ], + BinOp::Eq => [left, red("to equal"), right], + BinOp::NotEq => [left, red("to not equal"), right], + BinOp::LtInt => [left, red("to be lower than"), right], + BinOp::LtEqInt => [left, red("to be lower than or equal to"), right], + BinOp::GtInt => [left, red("to be greater than"), right], + BinOp::GtEqInt => [left, red("to be greater than or equal to"), right], + _ => unreachable!("unexpected non-boolean binary operator in assertion?"), + } + .join("\n") + } + ) } } diff --git a/crates/aiken-project/src/watch.rs b/crates/aiken-project/src/watch.rs index fb405903..591ec8e3 100644 --- a/crates/aiken-project/src/watch.rs +++ b/crates/aiken-project/src/watch.rs @@ -1,4 +1,4 @@ -use crate::{telemetry::Terminal, Error, Project}; +use crate::{telemetry::Terminal, Project}; use miette::{Diagnostic, IntoDiagnostic}; use notify::{Event, RecursiveMode, Watcher}; use owo_colors::{OwoColorize, Stream::Stderr}; @@ -108,15 +108,13 @@ where err.report() } - if !errs.iter().any(|e| matches!(e, Error::TestFailure { .. })) { - eprintln!( - "{}", - Summary { - warning_count, - error_count: errs.len(), - } - ); - } + eprintln!( + "{}", + Summary { + warning_count, + error_count: errs.len(), + } + ); return Err(ExitFailure::into_report()); } diff --git a/crates/uplc/src/machine/eval_result.rs b/crates/uplc/src/machine/eval_result.rs index 9e4e764e..572d2276 100644 --- a/crates/uplc/src/machine/eval_result.rs +++ b/crates/uplc/src/machine/eval_result.rs @@ -1,6 +1,5 @@ -use crate::ast::{Constant, NamedDeBruijn, Term}; - use super::{cost_model::ExBudget, Error}; +use crate::ast::{Constant, NamedDeBruijn, Term}; #[derive(Debug)] pub struct EvalResult { @@ -44,6 +43,14 @@ impl EvalResult { } } + #[allow(clippy::result_unit_err)] + pub fn unwrap_constant(self) -> Result { + match self.result { + Ok(Term::Constant(cst)) => Ok(cst.as_ref().to_owned()), + _ => Err(()), + } + } + pub fn result(&self) -> Result, Error> { self.result.clone() }