diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a72b077..91d856e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,8 @@ ### Added -- **aiken**: Optionally provide blueprint file location when using `blueprint apply` @Riley-Kilgore +- **aiken**: Optionally provide blueprint file location when using `blueprint apply`. @Riley-Kilgore +- **aiken**: Output test results as structured JSON when the target output is not a TTY terminal. @Riley-Kilgore, @KtorZ ### Changed @@ -12,6 +13,7 @@ - **aiken-project**: Fix `aiken docs` wrongly formatting list constants as tuples. See [#1048](https://github.com/aiken-lang/aiken/issues/1048). @KtorZ - **aiken-project**: Fix `aiken docs` source linking crashing when generating docs for config modules. See [#1044](https://github.com/aiken-lang/aiken/issues/1044). @KtorZ - **aiken-project**: Fix `aiken docs` generating very long lines for constants. @KtorZ +- **aiken-lang**: Leverage [Decision Trees](https://www.cs.tufts.edu/comp/150FP/archive/luc-maranget/jun08.pdf) for compiling pattern matches to UPLC. @MicroProofs ### Removed diff --git a/Cargo.lock b/Cargo.lock index b4b18deb..4729dd39 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -58,6 +58,7 @@ dependencies = [ "aiken-project", "clap", "clap_complete", + "color-print", "hex", "ignore", "indoc", @@ -590,6 +591,27 @@ version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" +[[package]] +name = "color-print" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3aa954171903797d5623e047d9ab69d91b493657917bdfb8c2c80ecaf9cdb6f4" +dependencies = [ + "color-print-proc-macro", +] + +[[package]] +name = "color-print-proc-macro" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "692186b5ebe54007e45a59aea47ece9eb4108e141326c304cdc91699a7118a22" +dependencies = [ + "nom", + "proc-macro2", + "quote", + "syn 2.0.77", +] + [[package]] name = "colorchoice" version = "1.0.2" diff --git a/crates/aiken-lang/src/test_framework.rs b/crates/aiken-lang/src/test_framework.rs index 84396e75..2aa97e5a 100644 --- a/crates/aiken-lang/src/test_framework.rs +++ b/crates/aiken-lang/src/test_framework.rs @@ -1201,28 +1201,50 @@ impl TryFrom for Assertion { } } +pub struct AssertionStyleOptions<'a> { + red: Box String + 'a>, + bold: Box String + 'a>, +} + +impl<'a> AssertionStyleOptions<'a> { + pub fn new(stream: Option<&'a Stream>) -> Self { + match stream { + Some(stream) => Self { + red: Box::new(|s| { + s.if_supports_color(stream.to_owned(), |s| s.red()) + .to_string() + }), + bold: Box::new(|s| { + s.if_supports_color(stream.to_owned(), |s| s.bold()) + .to_string() + }), + }, + None => Self { + red: Box::new(|s| s), + bold: Box::new(|s| s), + }, + } + } +} + 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() - }; + pub fn to_string(&self, expect_failure: bool, style: &AssertionStyleOptions) -> String { + let red = |s: &str| style.red.as_ref()(s.to_string()); + let x = |s: &str| style.red.as_ref()(style.bold.as_ref()(format!("× {s}"))); // head did not map to a constant if self.head.is_err() { - return red("program failed"); + return x("program failed"); } // any value in tail did not map to a constant if self.tail.is_err() { - return red("program failed"); + return x("program failed"); } - fn fmt_side(side: &UntypedExpr, stream: Stream) -> String { - let __ = "│".if_supports_color(stream, |s| s.red()); + fn fmt_side(side: &UntypedExpr, red: &dyn Fn(&str) -> String) -> String { + let __ = red("│"); Formatter::new() .expr(side, false) @@ -1233,20 +1255,17 @@ impl Assertion { .join("\n") } - let left = fmt_side(self.head.as_ref().unwrap(), stream); + let left = fmt_side(self.head.as_ref().unwrap(), &red); let tail = self.tail.as_ref().unwrap(); - let right = fmt_side(tail.first(), stream); + let right = fmt_side(tail.first(), &red); format!( "{}{}{}", - red("expected"), + x("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() + x(" neither\n") } else { "\n".to_string() }, @@ -1254,34 +1273,34 @@ impl Assertion { match self.bin_op { BinOp::And => [ left, - red("and"), + x("and"), [ - tail.mapped_ref(|s| fmt_side(s, stream)) - .join(format!("\n{}\n", red("and")).as_str()), + tail.mapped_ref(|s| fmt_side(s, &red)) + .join(format!("\n{}\n", x("and")).as_str()), if tail.len() > 1 { - red("to not all be true") + x("to not all be true") } else { - red("to not both be true") + x("to not both be true") }, ] .join("\n"), ], BinOp::Or => [ left, - red("nor"), + x("nor"), [ - tail.mapped_ref(|s| fmt_side(s, stream)) - .join(format!("\n{}\n", red("nor")).as_str()), - red("to be true"), + tail.mapped_ref(|s| fmt_side(s, &red)) + .join(format!("\n{}\n", x("nor")).as_str()), + x("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], + BinOp::Eq => [left, x("to not equal"), right], + BinOp::NotEq => [left, x("to not be different"), right], + BinOp::LtInt => [left, x("to not be lower than"), right], + BinOp::LtEqInt => [left, x("to not be lower than or equal to"), right], + BinOp::GtInt => [left, x("to not be greater than"), right], + BinOp::GtEqInt => [left, x("to not be greater than or equal to"), right], _ => unreachable!("unexpected non-boolean binary operator in assertion?"), } .join("\n") @@ -1289,34 +1308,34 @@ impl Assertion { match self.bin_op { BinOp::And => [ left, - red("and"), + x("and"), [ - tail.mapped_ref(|s| fmt_side(s, stream)) - .join(format!("\n{}\n", red("and")).as_str()), + tail.mapped_ref(|s| fmt_side(s, &red)) + .join(format!("\n{}\n", x("and")).as_str()), if tail.len() > 1 { - red("to all be true") + x("to all be true") } else { - red("to both be true") + x("to both be true") }, ] .join("\n"), ], BinOp::Or => [ left, - red("or"), + x("or"), [ - tail.mapped_ref(|s| fmt_side(s, stream)) - .join(format!("\n{}\n", red("or")).as_str()), - red("to be true"), + tail.mapped_ref(|s| fmt_side(s, &red)) + .join(format!("\n{}\n", x("or")).as_str()), + x("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], + BinOp::Eq => [left, x("to equal"), right], + BinOp::NotEq => [left, x("to not equal"), right], + BinOp::LtInt => [left, x("to be lower than"), right], + BinOp::LtEqInt => [left, x("to be lower than or equal to"), right], + BinOp::GtInt => [left, x("to be greater than"), right], + BinOp::GtEqInt => [left, x("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/telemetry.rs b/crates/aiken-project/src/telemetry.rs index daed509e..3a8dee75 100644 --- a/crates/aiken-project/src/telemetry.rs +++ b/crates/aiken-project/src/telemetry.rs @@ -1,13 +1,18 @@ -use crate::pretty; use aiken_lang::{ - ast::OnTestFailure, expr::UntypedExpr, - format::Formatter, test_framework::{PropertyTestResult, TestResult, UnitTestResult}, }; -use owo_colors::{OwoColorize, Stream::Stderr}; -use std::{collections::BTreeMap, fmt::Display, path::PathBuf}; -use uplc::machine::cost_model::ExBudget; +pub use json::Json; +use std::{ + collections::BTreeMap, + fmt::Display, + io::{self, IsTerminal}, + path::PathBuf, +}; +pub use terminal::Terminal; + +mod json; +mod terminal; pub trait EventListener { fn handle_event(&self, _event: Event) {} @@ -57,6 +62,30 @@ pub enum Event { ResolvingVersions, } +pub enum EventTarget { + Json(Json), + Terminal(Terminal), +} + +impl Default for EventTarget { + fn default() -> Self { + if io::stdout().is_terminal() { + EventTarget::Terminal(Terminal) + } else { + EventTarget::Json(Json) + } + } +} + +impl EventListener for EventTarget { + fn handle_event(&self, event: Event) { + match self { + EventTarget::Terminal(term) => term.handle_event(event), + EventTarget::Json(json) => json.handle_event(event), + } + } +} + pub enum DownloadSource { Network, Cache, @@ -71,431 +100,9 @@ impl Display for DownloadSource { } } -#[derive(Debug, Default, Clone, Copy)] -pub struct Terminal; - -impl EventListener for Terminal { - fn handle_event(&self, event: Event) { - match event { - Event::StartingCompilation { - name, - version, - root, - } => { - eprintln!( - "{} {} {} ({})", - " Compiling" - .if_supports_color(Stderr, |s| s.bold()) - .if_supports_color(Stderr, |s| s.purple()), - name.if_supports_color(Stderr, |s| s.bold()), - version, - root.display() - .if_supports_color(Stderr, |s| s.bright_blue()) - ); - } - Event::BuildingDocumentation { - name, - version, - root, - } => { - eprintln!( - "{} {} for {} {} ({})", - " Generating" - .if_supports_color(Stderr, |s| s.bold()) - .if_supports_color(Stderr, |s| s.purple()), - "documentation".if_supports_color(Stderr, |s| s.bold()), - name.if_supports_color(Stderr, |s| s.bold()), - version, - root.to_str() - .unwrap_or("") - .if_supports_color(Stderr, |s| s.bright_blue()) - ); - } - Event::WaitingForBuildDirLock => { - eprintln!( - "{}", - "Waiting for build directory lock ..." - .if_supports_color(Stderr, |s| s.bold()) - .if_supports_color(Stderr, |s| s.purple()) - ); - } - Event::DumpingUPLC { path } => { - eprintln!( - "{} {} ({})", - " Exporting" - .if_supports_color(Stderr, |s| s.bold()) - .if_supports_color(Stderr, |s| s.purple()), - "UPLC".if_supports_color(Stderr, |s| s.bold()), - path.display() - .if_supports_color(Stderr, |s| s.bright_blue()) - ); - } - Event::GeneratingBlueprint { path } => { - eprintln!( - "{} {} ({})", - " Generating" - .if_supports_color(Stderr, |s| s.bold()) - .if_supports_color(Stderr, |s| s.purple()), - "project's blueprint".if_supports_color(Stderr, |s| s.bold()), - path.display() - .if_supports_color(Stderr, |s| s.bright_blue()) - ); - } - Event::GeneratingDocFiles { output_path } => { - eprintln!( - "{} {} to {}", - " Writing" - .if_supports_color(Stderr, |s| s.bold()) - .if_supports_color(Stderr, |s| s.purple()), - "documentation files".if_supports_color(Stderr, |s| s.bold()), - output_path - .to_str() - .unwrap_or("") - .if_supports_color(Stderr, |s| s.bright_blue()) - ); - } - Event::GeneratingUPLCFor { name, path } => { - eprintln!( - "{} {} {}.{{{}}}", - " Generating" - .if_supports_color(Stderr, |s| s.bold()) - .if_supports_color(Stderr, |s| s.purple()), - "UPLC for" - .if_supports_color(Stderr, |s| s.bold()) - .if_supports_color(Stderr, |s| s.white()), - path.to_str() - .unwrap_or("") - .if_supports_color(Stderr, |s| s.blue()), - name.if_supports_color(Stderr, |s| s.bright_blue()), - ); - } - Event::RunningTests => { - eprintln!( - "{} {}", - " Testing" - .if_supports_color(Stderr, |s| s.bold()) - .if_supports_color(Stderr, |s| s.purple()), - "...".if_supports_color(Stderr, |s| s.bold()) - ); - } - Event::FinishedTests { seed, tests } => { - let (max_mem, max_cpu, max_iter) = find_max_execution_units(&tests); - - for (module, results) in &group_by_module(&tests) { - let title = module - .if_supports_color(Stderr, |s| s.bold()) - .if_supports_color(Stderr, |s| s.blue()) - .to_string(); - - let tests = results - .iter() - .map(|r| fmt_test(r, max_mem, max_cpu, max_iter, true)) - .collect::>() - .join("\n"); - - let seed_info = if results - .iter() - .any(|t| matches!(t, TestResult::PropertyTestResult { .. })) - { - format!( - "with {opt}={seed} → ", - opt = "--seed".if_supports_color(Stderr, |s| s.bold()), - seed = format!("{seed}").if_supports_color(Stderr, |s| s.bold()) - ) - } else { - String::new() - }; - - let summary = format!("{}{}", seed_info, fmt_test_summary(results, true)); - println!( - "\n{}", - pretty::indent( - &pretty::open_box(&title, &tests, &summary, |border| border - .if_supports_color(Stderr, |s| s.bright_black()) - .to_string()), - 4 - ) - ); - } - - if !tests.is_empty() { - println!(); - } - } - Event::ResolvingPackages { name } => { - eprintln!( - "{} {}", - " Resolving" - .if_supports_color(Stderr, |s| s.bold()) - .if_supports_color(Stderr, |s| s.purple()), - name.if_supports_color(Stderr, |s| s.bold()) - ) - } - Event::PackageResolveFallback { name } => { - eprintln!( - "{} {}\n ↳ You're seeing this message because the package version is unpinned and the network is not accessible.", - " Using" - .if_supports_color(Stderr, |s| s.bold()) - .if_supports_color(Stderr, |s| s.yellow()), - format!("uncertain local version for {name}") - .if_supports_color(Stderr, |s| s.yellow()) - ) - } - Event::PackagesDownloaded { - start, - count, - source, - } => { - let elapsed = format!("{:.2}s", start.elapsed().as_millis() as f32 / 1000.); - - let msg = match count { - 1 => format!("1 package in {elapsed}"), - _ => format!("{count} packages in {elapsed}"), - }; - - eprintln!( - "{} {} from {source}", - match source { - DownloadSource::Network => " Downloaded", - DownloadSource::Cache => " Fetched", - } - .if_supports_color(Stderr, |s| s.bold()) - .if_supports_color(Stderr, |s| s.purple()), - msg.if_supports_color(Stderr, |s| s.bold()) - ) - } - Event::ResolvingVersions => { - eprintln!( - "{}", - " Resolving dependencies" - .if_supports_color(Stderr, |s| s.bold()) - .if_supports_color(Stderr, |s| s.purple()), - ) - } - } - } -} - -fn fmt_test( - result: &TestResult, - max_mem: usize, - max_cpu: usize, - max_iter: usize, - styled: bool, -) -> String { - // Status - let mut test = if result.is_success() { - pretty::style_if(styled, "PASS".to_string(), |s| { - s.if_supports_color(Stderr, |s| s.bold()) - .if_supports_color(Stderr, |s| s.green()) - .to_string() - }) - } else { - pretty::style_if(styled, "FAIL".to_string(), |s| { - s.if_supports_color(Stderr, |s| s.bold()) - .if_supports_color(Stderr, |s| s.red()) - .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 - .if_supports_color(Stderr, |s| s.cyan()) - .to_string()), - cpu_unit = pretty::style_if(styled, cpu_pad, |s| s - .if_supports_color(Stderr, |s| s.cyan()) - .to_string()), - ); - } - TestResult::PropertyTestResult(PropertyTestResult { iterations, .. }) => { - test = format!( - "{test} [after {} test{}]", - pretty::pad_left( - if *iterations == 0 { - "?".to_string() - } else { - iterations.to_string() - }, - max_iter, - " " - ), - if *iterations > 1 { "s" } else { "" } - ); - } - } - - // Title - test = format!( - "{test} {title}", - title = pretty::style_if(styled, result.title().to_string(), |s| s - .if_supports_color(Stderr, |s| s.bright_blue()) - .to_string()) - ); - - // Annotations - match result { - TestResult::UnitTestResult(UnitTestResult { - assertion: Some(assertion), - test: unit_test, - .. - }) if !result.is_success() => { - test = format!( - "{test}\n{}", - assertion.to_string( - Stderr, - match unit_test.on_test_failure { - OnTestFailure::FailImmediately => false, - OnTestFailure::SucceedEventually | OnTestFailure::SucceedImmediately => - true, - } - ), - ); - } - _ => (), - } - - // CounterExamples - if let TestResult::PropertyTestResult(PropertyTestResult { counterexample, .. }) = result { - match counterexample { - Err(err) => { - test = format!( - "{test}\n{}\n{}", - "× fuzzer failed unexpectedly" - .if_supports_color(Stderr, |s| s.red()) - .if_supports_color(Stderr, |s| s.bold()), - format!("| {err}").if_supports_color(Stderr, |s| s.red()) - ); - } - - Ok(None) => { - if !result.is_success() { - test = format!( - "{test}\n{}", - "× no counterexample found" - .if_supports_color(Stderr, |s| s.red()) - .if_supports_color(Stderr, |s| s.bold()) - ); - } - } - - Ok(Some(counterexample)) => { - let is_expected_failure = result.is_success(); - - test = format!( - "{test}\n{}\n{}", - if is_expected_failure { - "★ counterexample" - .if_supports_color(Stderr, |s| s.green()) - .if_supports_color(Stderr, |s| s.bold()) - .to_string() - } else { - "× counterexample" - .if_supports_color(Stderr, |s| s.red()) - .if_supports_color(Stderr, |s| s.bold()) - .to_string() - }, - &Formatter::new() - .expr(counterexample, false) - .to_pretty_string(60) - .lines() - .map(|line| { - format!( - "{} {}", - "│".if_supports_color(Stderr, |s| if is_expected_failure { - s.green().to_string() - } else { - s.red().to_string() - }), - line - ) - }) - .collect::>() - .join("\n"), - ); - } - } - } - - // Labels - if let TestResult::PropertyTestResult(PropertyTestResult { labels, .. }) = result { - if !labels.is_empty() && result.is_success() { - test = format!( - "{test}\n{title}", - title = "· with coverage".if_supports_color(Stderr, |s| s.bold()) - ); - let mut total = 0; - let mut pad = 0; - for (k, v) in labels { - total += v; - if k.len() > pad { - pad = k.len(); - } - } - - let mut labels = labels.iter().collect::>(); - labels.sort_by(|a, b| b.1.cmp(a.1)); - - for (k, v) in labels { - test = format!( - "{test}\n| {} {:>5.1}%", - pretty::pad_right(k.to_owned(), pad, " ") - .if_supports_color(Stderr, |s| s.bold()), - 100.0 * (*v as f64) / (total as f64), - ); - } - } - } - - // Traces - if !result.traces().is_empty() { - test = format!( - "{test}\n{title}\n{traces}", - title = "· with traces".if_supports_color(Stderr, |s| s.bold()), - traces = result - .traces() - .iter() - .map(|line| { format!("| {line}",) }) - .collect::>() - .join("\n") - ); - }; - - test -} - -fn fmt_test_summary(tests: &[&TestResult], styled: bool) -> String { - let (n_passed, n_failed) = tests.iter().fold((0, 0), |(n_passed, n_failed), result| { - if result.is_success() { - (n_passed + 1, n_failed) - } else { - (n_passed, n_failed + 1) - } - }); - format!( - "{} | {} | {}", - pretty::style_if(styled, format!("{} tests", tests.len()), |s| s - .if_supports_color(Stderr, |s| s.bold()) - .to_string()), - pretty::style_if(styled, format!("{n_passed} passed"), |s| s - .if_supports_color(Stderr, |s| s.bright_green()) - .if_supports_color(Stderr, |s| s.bold()) - .to_string()), - pretty::style_if(styled, format!("{n_failed} failed"), |s| s - .if_supports_color(Stderr, |s| s.bright_red()) - .if_supports_color(Stderr, |s| s.bold()) - .to_string()), - ) -} - -fn group_by_module(results: &Vec>) -> BTreeMap>> { +pub(crate) fn group_by_module( + results: &[TestResult], +) -> BTreeMap>> { let mut modules = BTreeMap::new(); for r in results { let xs: &mut Vec<&TestResult<_, _>> = modules.entry(r.module().to_string()).or_default(); @@ -504,7 +111,7 @@ fn group_by_module(results: &Vec>) -> BTreeMap(xs: &[TestResult]) -> (usize, usize, usize) { +pub(crate) fn find_max_execution_units(xs: &[TestResult]) -> (usize, usize, usize) { let (max_mem, max_cpu, max_iter) = xs.iter() .fold((0, 0, 0), |(max_mem, max_cpu, max_iter), test| match test { diff --git a/crates/aiken-project/src/telemetry/json.rs b/crates/aiken-project/src/telemetry/json.rs new file mode 100644 index 00000000..52aa01fa --- /dev/null +++ b/crates/aiken-project/src/telemetry/json.rs @@ -0,0 +1,139 @@ +use super::{group_by_module, Event, EventListener}; +use aiken_lang::{ + ast::OnTestFailure, + expr::UntypedExpr, + format::Formatter, + test_framework::{AssertionStyleOptions, PropertyTestResult, TestResult, UnitTestResult}, +}; +use serde_json::json; + +#[derive(Debug, Default, Clone, Copy)] +pub struct Json; + +impl EventListener for Json { + fn handle_event(&self, event: Event) { + match event { + Event::FinishedTests { seed, tests, .. } => { + let total = tests.len(); + let passed = tests.iter().filter(|t| t.is_success()).count(); + let failed = total - passed; + + let json_output = serde_json::json!({ + "seed": seed, + "summary": json!({ + "total": total, + "passed": passed, + "failed": failed, + "kind": json!({ + "unit": count_unit_tests(tests.iter()), + "property": count_property_tests(tests.iter()), + }) + }), + "modules": group_by_module(&tests).iter().map(|(module, results)| { + serde_json::json!({ + "name": module, + "summary": fmt_test_summary_json(results), + "tests": results.iter().map(|r| fmt_test_json(r)).collect::>(), + }) + }).collect::>(), + }); + println!("{}", serde_json::to_string_pretty(&json_output).unwrap()); + } + _ => super::Terminal.handle_event(event), + } + } +} + +fn fmt_test_json(result: &TestResult) -> serde_json::Value { + let on_test_failure = match result { + TestResult::UnitTestResult(UnitTestResult { ref test, .. }) => &test.on_test_failure, + TestResult::PropertyTestResult(PropertyTestResult { ref test, .. }) => { + &test.on_test_failure + } + }; + + let mut test = json!({ + "title": result.title(), + "status": if result.is_success() { "pass" } else { "fail" }, + "on_failure": match on_test_failure { + OnTestFailure::FailImmediately => "fail_immediately" , + OnTestFailure::SucceedEventually => "succeed_eventually" , + OnTestFailure::SucceedImmediately => "succeed_immediately", + } + }); + + match result { + TestResult::UnitTestResult(UnitTestResult { + spent_budget, + assertion, + .. + }) => { + test["execution_units"] = json!({ + "mem": spent_budget.mem, + "cpu": spent_budget.cpu, + }); + if !result.is_success() { + if let Some(assertion) = assertion { + test["assertion"] = + json!(assertion.to_string(false, &AssertionStyleOptions::new(None))); + } + } + } + TestResult::PropertyTestResult(PropertyTestResult { + iterations, + labels, + counterexample, + .. + }) => { + test["iterations"] = json!(iterations); + if !labels.is_empty() { + test["labels"] = json!(labels); + } + test["counterexample"] = match counterexample { + Ok(Some(expr)) => json!(Formatter::new().expr(expr, false).to_pretty_string(60)), + Ok(None) => json!(null), + Err(err) => json!({"error": err.to_string()}), + }; + } + } + + if !result.traces().is_empty() { + test["traces"] = json!(result.traces()); + } + + test +} + +fn fmt_test_summary_json(tests: &[&TestResult]) -> serde_json::Value { + let total = tests.len(); + let passed = tests.iter().filter(|t| t.is_success()).count(); + let failed = total - passed; + + json!({ + "total": total, + "passed": passed, + "failed": failed, + "kind": json!({ + "unit": count_unit_tests(tests.iter().copied()), + "property": count_property_tests(tests.iter().copied()) + }) + }) +} + +fn count_unit_tests<'a, I>(tests: I) -> usize +where + I: Iterator>, +{ + tests + .filter(|t| matches!(t, TestResult::UnitTestResult { .. })) + .count() +} + +fn count_property_tests<'a, I>(tests: I) -> usize +where + I: Iterator>, +{ + tests + .filter(|t| matches!(t, TestResult::PropertyTestResult { .. })) + .count() +} diff --git a/crates/aiken-project/src/telemetry/terminal.rs b/crates/aiken-project/src/telemetry/terminal.rs new file mode 100644 index 00000000..b7d472e3 --- /dev/null +++ b/crates/aiken-project/src/telemetry/terminal.rs @@ -0,0 +1,434 @@ +use super::{find_max_execution_units, group_by_module, DownloadSource, Event, EventListener}; +use crate::pretty; +use aiken_lang::{ + ast::OnTestFailure, + expr::UntypedExpr, + format::Formatter, + test_framework::{AssertionStyleOptions, PropertyTestResult, TestResult, UnitTestResult}, +}; +use owo_colors::{OwoColorize, Stream::Stderr}; +use uplc::machine::cost_model::ExBudget; + +#[derive(Debug, Default, Clone, Copy)] +pub struct Terminal; + +impl EventListener for Terminal { + fn handle_event(&self, event: Event) { + match event { + Event::StartingCompilation { + name, + version, + root, + } => { + eprintln!( + "{} {} {} ({})", + " Compiling" + .if_supports_color(Stderr, |s| s.bold()) + .if_supports_color(Stderr, |s| s.purple()), + name.if_supports_color(Stderr, |s| s.bold()), + version, + root.display() + .if_supports_color(Stderr, |s| s.bright_blue()) + ); + } + Event::BuildingDocumentation { + name, + version, + root, + } => { + eprintln!( + "{} {} for {} {} ({})", + " Generating" + .if_supports_color(Stderr, |s| s.bold()) + .if_supports_color(Stderr, |s| s.purple()), + "documentation".if_supports_color(Stderr, |s| s.bold()), + name.if_supports_color(Stderr, |s| s.bold()), + version, + root.to_str() + .unwrap_or("") + .if_supports_color(Stderr, |s| s.bright_blue()) + ); + } + Event::WaitingForBuildDirLock => { + eprintln!( + "{}", + "Waiting for build directory lock ..." + .if_supports_color(Stderr, |s| s.bold()) + .if_supports_color(Stderr, |s| s.purple()) + ); + } + Event::DumpingUPLC { path } => { + eprintln!( + "{} {} ({})", + " Exporting" + .if_supports_color(Stderr, |s| s.bold()) + .if_supports_color(Stderr, |s| s.purple()), + "UPLC".if_supports_color(Stderr, |s| s.bold()), + path.display() + .if_supports_color(Stderr, |s| s.bright_blue()) + ); + } + Event::GeneratingBlueprint { path } => { + eprintln!( + "{} {} ({})", + " Generating" + .if_supports_color(Stderr, |s| s.bold()) + .if_supports_color(Stderr, |s| s.purple()), + "project's blueprint".if_supports_color(Stderr, |s| s.bold()), + path.display() + .if_supports_color(Stderr, |s| s.bright_blue()) + ); + } + Event::GeneratingDocFiles { output_path } => { + eprintln!( + "{} {} to {}", + " Writing" + .if_supports_color(Stderr, |s| s.bold()) + .if_supports_color(Stderr, |s| s.purple()), + "documentation files".if_supports_color(Stderr, |s| s.bold()), + output_path + .to_str() + .unwrap_or("") + .if_supports_color(Stderr, |s| s.bright_blue()) + ); + } + Event::GeneratingUPLCFor { name, path } => { + eprintln!( + "{} {} {}.{{{}}}", + " Generating" + .if_supports_color(Stderr, |s| s.bold()) + .if_supports_color(Stderr, |s| s.purple()), + "UPLC for" + .if_supports_color(Stderr, |s| s.bold()) + .if_supports_color(Stderr, |s| s.white()), + path.to_str() + .unwrap_or("") + .if_supports_color(Stderr, |s| s.blue()), + name.if_supports_color(Stderr, |s| s.bright_blue()), + ); + } + Event::RunningTests => { + eprintln!( + "{} {}\n", + " Testing" + .if_supports_color(Stderr, |s| s.bold()) + .if_supports_color(Stderr, |s| s.purple()), + "...".if_supports_color(Stderr, |s| s.bold()) + ); + } + Event::FinishedTests { seed, tests } => { + let (max_mem, max_cpu, max_iter) = find_max_execution_units(&tests); + + for (module, results) in &group_by_module(&tests) { + let title = module + .if_supports_color(Stderr, |s| s.bold()) + .if_supports_color(Stderr, |s| s.blue()) + .to_string(); + + let tests = results + .iter() + .map(|r| fmt_test(r, max_mem, max_cpu, max_iter, true)) + .collect::>() + .join("\n"); + + let seed_info = if results + .iter() + .any(|t| matches!(t, TestResult::PropertyTestResult { .. })) + { + format!( + "with {opt}={seed} → ", + opt = "--seed".if_supports_color(Stderr, |s| s.bold()), + seed = format!("{seed}").if_supports_color(Stderr, |s| s.bold()) + ) + } else { + String::new() + }; + + let summary = format!("{}{}", seed_info, fmt_test_summary(results, true)); + println!( + "{}\n", + pretty::indent( + &pretty::open_box(&title, &tests, &summary, |border| border + .if_supports_color(Stderr, |s| s.bright_black()) + .to_string()), + 4 + ) + ); + } + + if !tests.is_empty() { + println!(); + } + } + Event::ResolvingPackages { name } => { + eprintln!( + "{} {}", + " Resolving" + .if_supports_color(Stderr, |s| s.bold()) + .if_supports_color(Stderr, |s| s.purple()), + name.if_supports_color(Stderr, |s| s.bold()) + ) + } + Event::PackageResolveFallback { name } => { + eprintln!( + "{} {}\n ↳ You're seeing this message because the package version is unpinned and the network is not accessible.", + " Using" + .if_supports_color(Stderr, |s| s.bold()) + .if_supports_color(Stderr, |s| s.yellow()), + format!("uncertain local version for {name}") + .if_supports_color(Stderr, |s| s.yellow()) + ) + } + Event::PackagesDownloaded { + start, + count, + source, + } => { + let elapsed = format!("{:.2}s", start.elapsed().as_millis() as f32 / 1000.); + + let msg = match count { + 1 => format!("1 package in {elapsed}"), + _ => format!("{count} packages in {elapsed}"), + }; + + eprintln!( + "{} {} from {source}", + match source { + DownloadSource::Network => " Downloaded", + DownloadSource::Cache => " Fetched", + } + .if_supports_color(Stderr, |s| s.bold()) + .if_supports_color(Stderr, |s| s.purple()), + msg.if_supports_color(Stderr, |s| s.bold()) + ) + } + Event::ResolvingVersions => { + eprintln!( + "{}", + " Resolving dependencies" + .if_supports_color(Stderr, |s| s.bold()) + .if_supports_color(Stderr, |s| s.purple()), + ) + } + } + } +} + +fn fmt_test( + result: &TestResult, + max_mem: usize, + max_cpu: usize, + max_iter: usize, + styled: bool, +) -> String { + // Status + let mut test = if result.is_success() { + pretty::style_if(styled, "PASS".to_string(), |s| { + s.if_supports_color(Stderr, |s| s.bold()) + .if_supports_color(Stderr, |s| s.green()) + .to_string() + }) + } else { + pretty::style_if(styled, "FAIL".to_string(), |s| { + s.if_supports_color(Stderr, |s| s.bold()) + .if_supports_color(Stderr, |s| s.red()) + .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 + .if_supports_color(Stderr, |s| s.cyan()) + .to_string()), + cpu_unit = pretty::style_if(styled, cpu_pad, |s| s + .if_supports_color(Stderr, |s| s.cyan()) + .to_string()), + ); + } + TestResult::PropertyTestResult(PropertyTestResult { iterations, .. }) => { + test = format!( + "{test} [after {} test{}]", + pretty::pad_left( + if *iterations == 0 { + "?".to_string() + } else { + iterations.to_string() + }, + max_iter, + " " + ), + if *iterations > 1 { "s" } else { "" } + ); + } + } + + // Title + test = format!( + "{test} {title}", + title = pretty::style_if(styled, result.title().to_string(), |s| s + .if_supports_color(Stderr, |s| s.bright_blue()) + .to_string()) + ); + + // Annotations + match result { + TestResult::UnitTestResult(UnitTestResult { + assertion: Some(assertion), + test: unit_test, + .. + }) if !result.is_success() => { + test = format!( + "{test}\n{}", + assertion.to_string( + match unit_test.on_test_failure { + OnTestFailure::FailImmediately => false, + OnTestFailure::SucceedEventually | OnTestFailure::SucceedImmediately => + true, + }, + &AssertionStyleOptions::new(Some(&Stderr)) + ), + ); + } + _ => (), + } + + // CounterExamples + if let TestResult::PropertyTestResult(PropertyTestResult { counterexample, .. }) = result { + match counterexample { + Err(err) => { + test = format!( + "{test}\n{}\n{}", + "× fuzzer failed unexpectedly" + .if_supports_color(Stderr, |s| s.red()) + .if_supports_color(Stderr, |s| s.bold()), + format!("| {err}").if_supports_color(Stderr, |s| s.red()) + ); + } + + Ok(None) => { + if !result.is_success() { + test = format!( + "{test}\n{}", + "× no counterexample found" + .if_supports_color(Stderr, |s| s.red()) + .if_supports_color(Stderr, |s| s.bold()) + ); + } + } + + Ok(Some(counterexample)) => { + let is_expected_failure = result.is_success(); + + test = format!( + "{test}\n{}\n{}", + if is_expected_failure { + "★ counterexample" + .if_supports_color(Stderr, |s| s.green()) + .if_supports_color(Stderr, |s| s.bold()) + .to_string() + } else { + "× counterexample" + .if_supports_color(Stderr, |s| s.red()) + .if_supports_color(Stderr, |s| s.bold()) + .to_string() + }, + &Formatter::new() + .expr(counterexample, false) + .to_pretty_string(60) + .lines() + .map(|line| { + format!( + "{} {}", + "│".if_supports_color(Stderr, |s| if is_expected_failure { + s.green().to_string() + } else { + s.red().to_string() + }), + line + ) + }) + .collect::>() + .join("\n"), + ); + } + } + } + + // Labels + if let TestResult::PropertyTestResult(PropertyTestResult { labels, .. }) = result { + if !labels.is_empty() && result.is_success() { + test = format!( + "{test}\n{title}", + title = "· with coverage".if_supports_color(Stderr, |s| s.bold()) + ); + let mut total = 0; + let mut pad = 0; + for (k, v) in labels { + total += v; + if k.len() > pad { + pad = k.len(); + } + } + + let mut labels = labels.iter().collect::>(); + labels.sort_by(|a, b| b.1.cmp(a.1)); + + for (k, v) in labels { + test = format!( + "{test}\n| {} {:>5.1}%", + pretty::pad_right(k.to_owned(), pad, " ") + .if_supports_color(Stderr, |s| s.bold()), + 100.0 * (*v as f64) / (total as f64), + ); + } + } + } + + // Traces + if !result.traces().is_empty() { + test = format!( + "{test}\n{title}\n{traces}", + title = "· with traces".if_supports_color(Stderr, |s| s.bold()), + traces = result + .traces() + .iter() + .map(|line| { format!("| {line}",) }) + .collect::>() + .join("\n") + ); + }; + + test +} + +fn fmt_test_summary(tests: &[&TestResult], styled: bool) -> String { + let (n_passed, n_failed) = tests.iter().fold((0, 0), |(n_passed, n_failed), result| { + if result.is_success() { + (n_passed + 1, n_failed) + } else { + (n_passed, n_failed + 1) + } + }); + format!( + "{} | {} | {}", + pretty::style_if(styled, format!("{} tests", tests.len()), |s| s + .if_supports_color(Stderr, |s| s.bold()) + .to_string()), + pretty::style_if(styled, format!("{n_passed} passed"), |s| s + .if_supports_color(Stderr, |s| s.bright_green()) + .if_supports_color(Stderr, |s| s.bold()) + .to_string()), + pretty::style_if(styled, format!("{n_failed} failed"), |s| s + .if_supports_color(Stderr, |s| s.bright_red()) + .if_supports_color(Stderr, |s| s.bold()) + .to_string()), + ) +} diff --git a/crates/aiken-project/src/watch.rs b/crates/aiken-project/src/watch.rs index 91da47c4..89940668 100644 --- a/crates/aiken-project/src/watch.rs +++ b/crates/aiken-project/src/watch.rs @@ -1,4 +1,4 @@ -use crate::{telemetry::Terminal, Project}; +use crate::{telemetry::EventTarget, Project}; use miette::{Diagnostic, IntoDiagnostic}; use notify::{Event, RecursiveMode, Watcher}; use owo_colors::{OwoColorize, Stream::Stderr}; @@ -88,9 +88,14 @@ pub fn default_filter(evt: &Event) -> bool { } } -pub fn with_project(directory: Option<&Path>, deny: bool, mut action: A) -> miette::Result<()> +pub fn with_project( + directory: Option<&Path>, + deny: bool, + json: bool, + mut action: A, +) -> miette::Result<()> where - A: FnMut(&mut Project) -> Result<(), Vec>, + A: FnMut(&mut Project) -> Result<(), Vec>, { let project_path = if let Some(d) = directory { d.to_path_buf() @@ -102,7 +107,7 @@ where current_dir }; - let mut project = match Project::new(project_path, Terminal) { + let mut project = match Project::new(project_path, EventTarget::default()) { Ok(p) => Ok(p), Err(e) => { e.report(); @@ -116,36 +121,38 @@ where let warning_count = warnings.len(); - for warning in &warnings { - warning.report() - } + if !json { + for warning in &warnings { + warning.report() + } - if let Err(errs) = build_result { - for err in &errs { - err.report() + if let Err(errs) = build_result { + for err in &errs { + err.report() + } + + eprintln!( + "{}", + Summary { + check_count: project.checks_count, + warning_count, + error_count: errs.len(), + } + ); + + return Err(ExitFailure::into_report()); } eprintln!( "{}", Summary { check_count: project.checks_count, - warning_count, - error_count: errs.len(), + error_count: 0, + warning_count } ); - - return Err(ExitFailure::into_report()); } - eprintln!( - "{}", - Summary { - check_count: project.checks_count, - error_count: 0, - warning_count - } - ); - if warning_count > 0 && deny { Err(ExitFailure::into_report()) } else { @@ -159,7 +166,7 @@ where /// // Note: doctest disabled, because aiken_project doesn't have an implementation of EventListener I can use /// use aiken_project::watch::{watch_project, default_filter}; /// use aiken_project::{Project}; -/// watch_project(None, Terminal, default_filter, 500, |project| { +/// watch_project(None, default_filter, 500, |project| { /// println!("Project changed!"); /// Ok(()) /// }); @@ -172,7 +179,7 @@ pub fn watch_project( ) -> miette::Result<()> where F: Fn(&Event) -> bool, - A: FnMut(&mut Project) -> Result<(), Vec>, + A: FnMut(&mut Project) -> Result<(), Vec>, { let project_path = directory .map(|p| p.to_path_buf()) @@ -239,7 +246,7 @@ where .if_supports_color(Stderr, |s| s.bold()) .if_supports_color(Stderr, |s| s.purple()), ); - with_project(directory, false, &mut action).unwrap_or(()) + with_project(directory, false, false, &mut action).unwrap_or(()) } } } diff --git a/crates/aiken/Cargo.toml b/crates/aiken/Cargo.toml index a5ad93f3..ea2fe41d 100644 --- a/crates/aiken/Cargo.toml +++ b/crates/aiken/Cargo.toml @@ -30,6 +30,7 @@ clap = { version = "4.1.8", features = [ "string", ] } clap_complete = "4.3.2" +color-print = "0.3.7" hex = "0.4.3" ignore = "0.4.20" indoc = "2.0" diff --git a/crates/aiken/src/cmd/blueprint/address.rs b/crates/aiken/src/cmd/blueprint/address.rs index df54879e..f29ead1d 100644 --- a/crates/aiken/src/cmd/blueprint/address.rs +++ b/crates/aiken/src/cmd/blueprint/address.rs @@ -33,7 +33,7 @@ pub fn exec( mainnet, }: Args, ) -> miette::Result<()> { - with_project(directory.as_deref(), false, |p| { + with_project(directory.as_deref(), false, false, |p| { let title = module.as_ref().map(|m| { format!( "{m}{}", diff --git a/crates/aiken/src/cmd/blueprint/apply.rs b/crates/aiken/src/cmd/blueprint/apply.rs index aaae374e..997f2ce0 100644 --- a/crates/aiken/src/cmd/blueprint/apply.rs +++ b/crates/aiken/src/cmd/blueprint/apply.rs @@ -54,7 +54,7 @@ pub fn exec( validator, }: Args, ) -> miette::Result<()> { - with_project(None, false, |p| { + with_project(None, false, false, |p| { let title = module.as_ref().map(|m| { format!( "{m}{}", diff --git a/crates/aiken/src/cmd/blueprint/hash.rs b/crates/aiken/src/cmd/blueprint/hash.rs index c7370890..f5410584 100644 --- a/crates/aiken/src/cmd/blueprint/hash.rs +++ b/crates/aiken/src/cmd/blueprint/hash.rs @@ -23,7 +23,7 @@ pub fn exec( validator, }: Args, ) -> miette::Result<()> { - with_project(directory.as_deref(), false, |p| { + with_project(directory.as_deref(), false, false, |p| { let title = module.as_ref().map(|m| { format!( "{m}{}", diff --git a/crates/aiken/src/cmd/blueprint/policy.rs b/crates/aiken/src/cmd/blueprint/policy.rs index e23f118c..080926d5 100644 --- a/crates/aiken/src/cmd/blueprint/policy.rs +++ b/crates/aiken/src/cmd/blueprint/policy.rs @@ -23,7 +23,7 @@ pub fn exec( validator, }: Args, ) -> miette::Result<()> { - with_project(directory.as_deref(), false, |p| { + with_project(directory.as_deref(), false, false, |p| { let title = module.as_ref().map(|m| { format!( "{m}{}", diff --git a/crates/aiken/src/cmd/build.rs b/crates/aiken/src/cmd/build.rs index 9c6b1454..7774975b 100644 --- a/crates/aiken/src/cmd/build.rs +++ b/crates/aiken/src/cmd/build.rs @@ -82,7 +82,7 @@ pub fn exec( ) }) } else { - with_project(directory.as_deref(), deny, |p| { + with_project(directory.as_deref(), deny, false, |p| { p.build( uplc, match trace_filter { diff --git a/crates/aiken/src/cmd/check.rs b/crates/aiken/src/cmd/check.rs index ed3fe619..b145b8ea 100644 --- a/crates/aiken/src/cmd/check.rs +++ b/crates/aiken/src/cmd/check.rs @@ -5,10 +5,108 @@ use aiken_lang::{ }; use aiken_project::watch::{self, watch_project, with_project}; use rand::prelude::*; -use std::{path::PathBuf, process}; +use std::{ + io::{self, IsTerminal}, + path::PathBuf, + process, +}; #[derive(clap::Args)] -/// Type-check an Aiken project +#[command( + verbatim_doc_comment, + about = color_print::cstr!(r#" +Type-check an Aiken project and run any tests found. + +Test results are printed as stylized outputs when `stdout` is a TTY-capable terminal. If it +isn't, (e.g. because you are redirecting the output to a file), test results are printed as +a JSON structured object. Use `--help` to see the whole schema. +"#), + after_long_help = color_print::cstr!(r#"Output JSON schema: + type: object + properties: + seed: &type_integer + type: integer + summary: + type: object + properties: &type_summary + total: *type_integer + passed: *type_integer + failed: *type_integer + kind: + type: object + properties: + unit: *type_integer + property: *type_integer + modules: + type: array + items: + type: object + properties: + name: &type_string + type: string + summary: *type_summary + test: + type: array + items: + oneOf: + - type: object + required: + - kind + - title + - status + - on_failure + - execution_units + properties: + kind + type: string + enum: [ "unit" ] + title: *type_string + status: &type_status + type: string + enum: [ "pass", "fail" ] + on_failure: &type_on_failure + type: string + enum: + - fail_immediately + - succeed_immediately + - succeed_eventually + execution_units: + type: object + properties: + mem: *type_integer + cpu: *type_integer + assertion: *type_string + - type: object + required: + - kind + - title + - status + - on_failure + - iterations + - counterexample + properties: + kind + type: string + enum: [ "property" ] + title: *type_string + status: *type_status + on_failure: *type_on_failure + iterations: *type_integer + labels: + type: object + additionalProperties: *type_integer + counterexample: + oneOf: + - *type_string + - type: "null" + - type: object + properties: + error: *type_string + +Note: + You are seeing the extended help. Use `-h` instead of `--help` for a more compact view. +"# +))] pub struct Args { /// Path to project directory: Option, @@ -40,7 +138,7 @@ pub struct Args { /// Only run tests if they match any of these strings. /// You can match a module with `-m aiken/list` or `-m list`. /// You can match a test with `-m "aiken/list.{map}"` or `-m "aiken/option.{flatten_1}"` - #[clap(short, long)] + #[clap(short, long, verbatim_doc_comment)] match_tests: Option>, /// This is meant to be used with `--match-tests`. @@ -72,14 +170,9 @@ pub struct Args { /// Choose the verbosity level of traces: /// - /// - silent: - /// disable traces altogether - /// - /// - compact: - /// only culprit line numbers are shown on failures - /// - /// - verbose: - /// enable full verbose traces as provided by the user or the compiler + /// - silent: disable traces altogether + /// - compact: only culprit line numbers are shown on failures + /// - verbose: enable full verbose traces as provided by the user or the compiler /// /// [optional] #[clap(short, long, value_parser=trace_level_parser(), default_value_t=TraceLevel::Verbose, verbatim_doc_comment)] @@ -123,21 +216,26 @@ pub fn exec( ) }) } else { - with_project(directory.as_deref(), deny, |p| { - p.check( - skip_tests, - match_tests.clone(), - debug, - exact_match, - seed, - max_success, - match trace_filter { - Some(trace_filter) => trace_filter(trace_level), - None => Tracing::All(trace_level), - }, - env.clone(), - ) - }) + with_project( + directory.as_deref(), + deny, + !io::stdout().is_terminal(), + |p| { + p.check( + skip_tests, + match_tests.clone(), + debug, + exact_match, + seed, + max_success, + match trace_filter { + Some(trace_filter) => trace_filter(trace_level), + None => Tracing::All(trace_level), + }, + env.clone(), + ) + }, + ) }; result.map_err(|_| process::exit(1)) diff --git a/crates/aiken/src/cmd/docs.rs b/crates/aiken/src/cmd/docs.rs index fbc3ac18..b17c0c35 100644 --- a/crates/aiken/src/cmd/docs.rs +++ b/crates/aiken/src/cmd/docs.rs @@ -38,7 +38,7 @@ pub fn exec( p.docs(destination.clone(), include_dependencies) }) } else { - with_project(directory.as_deref(), deny, |p| { + with_project(directory.as_deref(), deny, false, |p| { p.docs(destination.clone(), include_dependencies) }) }; diff --git a/crates/aiken/src/cmd/export.rs b/crates/aiken/src/cmd/export.rs index 1b8dcc4c..902820e5 100644 --- a/crates/aiken/src/cmd/export.rs +++ b/crates/aiken/src/cmd/export.rs @@ -61,7 +61,7 @@ pub fn exec( trace_level, }: Args, ) -> miette::Result<()> { - with_project(directory.as_deref(), false, |p| { + with_project(directory.as_deref(), false, false, |p| { p.compile(Options::default())?; let export = p.export(