Merge branch 'json-check-output-2'

This commit is contained in:
KtorZ 2024-11-13 15:09:06 +01:00
commit c523b0153d
No known key found for this signature in database
GPG Key ID: 33173CB6F77F4277
16 changed files with 869 additions and 540 deletions

View File

@ -4,7 +4,8 @@
### Added ### 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 ### 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` 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` 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-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 ### Removed

22
Cargo.lock generated vendored
View File

@ -58,6 +58,7 @@ dependencies = [
"aiken-project", "aiken-project",
"clap", "clap",
"clap_complete", "clap_complete",
"color-print",
"hex", "hex",
"ignore", "ignore",
"indoc", "indoc",
@ -590,6 +591,27 @@ version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" 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]] [[package]]
name = "colorchoice" name = "colorchoice"
version = "1.0.2" version = "1.0.2"

View File

@ -1201,28 +1201,50 @@ impl TryFrom<TypedExpr> for Assertion<TypedExpr> {
} }
} }
pub struct AssertionStyleOptions<'a> {
red: Box<dyn Fn(String) -> String + 'a>,
bold: Box<dyn Fn(String) -> 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<UntypedExpr> { impl Assertion<UntypedExpr> {
#[allow(clippy::just_underscores_and_digits)] #[allow(clippy::just_underscores_and_digits)]
pub fn to_string(&self, stream: Stream, expect_failure: bool) -> String { pub fn to_string(&self, expect_failure: bool, style: &AssertionStyleOptions) -> String {
let red = |s: &str| { let red = |s: &str| style.red.as_ref()(s.to_string());
format!("× {s}") let x = |s: &str| style.red.as_ref()(style.bold.as_ref()(format!("× {s}")));
.if_supports_color(stream, |s| s.red())
.if_supports_color(stream, |s| s.bold())
.to_string()
};
// head did not map to a constant // head did not map to a constant
if self.head.is_err() { if self.head.is_err() {
return red("program failed"); return x("program failed");
} }
// any value in tail did not map to a constant // any value in tail did not map to a constant
if self.tail.is_err() { if self.tail.is_err() {
return red("program failed"); return x("program failed");
} }
fn fmt_side(side: &UntypedExpr, stream: Stream) -> String { fn fmt_side(side: &UntypedExpr, red: &dyn Fn(&str) -> String) -> String {
let __ = "".if_supports_color(stream, |s| s.red()); let __ = red("");
Formatter::new() Formatter::new()
.expr(side, false) .expr(side, false)
@ -1233,20 +1255,17 @@ impl Assertion<UntypedExpr> {
.join("\n") .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 tail = self.tail.as_ref().unwrap();
let right = fmt_side(tail.first(), stream); let right = fmt_side(tail.first(), &red);
format!( format!(
"{}{}{}", "{}{}{}",
red("expected"), x("expected"),
if expect_failure && self.bin_op == BinOp::Or { if expect_failure && self.bin_op == BinOp::Or {
" neither\n" x(" neither\n")
.if_supports_color(stream, |s| s.red())
.if_supports_color(stream, |s| s.bold())
.to_string()
} else { } else {
"\n".to_string() "\n".to_string()
}, },
@ -1254,34 +1273,34 @@ impl Assertion<UntypedExpr> {
match self.bin_op { match self.bin_op {
BinOp::And => [ BinOp::And => [
left, left,
red("and"), x("and"),
[ [
tail.mapped_ref(|s| fmt_side(s, stream)) tail.mapped_ref(|s| fmt_side(s, &red))
.join(format!("\n{}\n", red("and")).as_str()), .join(format!("\n{}\n", x("and")).as_str()),
if tail.len() > 1 { if tail.len() > 1 {
red("to not all be true") x("to not all be true")
} else { } else {
red("to not both be true") x("to not both be true")
}, },
] ]
.join("\n"), .join("\n"),
], ],
BinOp::Or => [ BinOp::Or => [
left, left,
red("nor"), x("nor"),
[ [
tail.mapped_ref(|s| fmt_side(s, stream)) tail.mapped_ref(|s| fmt_side(s, &red))
.join(format!("\n{}\n", red("nor")).as_str()), .join(format!("\n{}\n", x("nor")).as_str()),
red("to be true"), x("to be true"),
] ]
.join("\n"), .join("\n"),
], ],
BinOp::Eq => [left, red("to not equal"), right], BinOp::Eq => [left, x("to not equal"), right],
BinOp::NotEq => [left, red("to not be different"), right], BinOp::NotEq => [left, x("to not be different"), right],
BinOp::LtInt => [left, red("to not be lower than"), right], BinOp::LtInt => [left, x("to not be lower than"), right],
BinOp::LtEqInt => [left, red("to not be lower than or equal to"), right], BinOp::LtEqInt => [left, x("to not be lower than or equal to"), right],
BinOp::GtInt => [left, red("to not be greater than"), right], BinOp::GtInt => [left, x("to not be greater than"), right],
BinOp::GtEqInt => [left, red("to not be greater than or equal to"), right], BinOp::GtEqInt => [left, x("to not be greater than or equal to"), right],
_ => unreachable!("unexpected non-boolean binary operator in assertion?"), _ => unreachable!("unexpected non-boolean binary operator in assertion?"),
} }
.join("\n") .join("\n")
@ -1289,34 +1308,34 @@ impl Assertion<UntypedExpr> {
match self.bin_op { match self.bin_op {
BinOp::And => [ BinOp::And => [
left, left,
red("and"), x("and"),
[ [
tail.mapped_ref(|s| fmt_side(s, stream)) tail.mapped_ref(|s| fmt_side(s, &red))
.join(format!("\n{}\n", red("and")).as_str()), .join(format!("\n{}\n", x("and")).as_str()),
if tail.len() > 1 { if tail.len() > 1 {
red("to all be true") x("to all be true")
} else { } else {
red("to both be true") x("to both be true")
}, },
] ]
.join("\n"), .join("\n"),
], ],
BinOp::Or => [ BinOp::Or => [
left, left,
red("or"), x("or"),
[ [
tail.mapped_ref(|s| fmt_side(s, stream)) tail.mapped_ref(|s| fmt_side(s, &red))
.join(format!("\n{}\n", red("or")).as_str()), .join(format!("\n{}\n", x("or")).as_str()),
red("to be true"), x("to be true"),
] ]
.join("\n"), .join("\n"),
], ],
BinOp::Eq => [left, red("to equal"), right], BinOp::Eq => [left, x("to equal"), right],
BinOp::NotEq => [left, red("to not equal"), right], BinOp::NotEq => [left, x("to not equal"), right],
BinOp::LtInt => [left, red("to be lower than"), right], BinOp::LtInt => [left, x("to be lower than"), right],
BinOp::LtEqInt => [left, red("to be lower than or equal to"), right], BinOp::LtEqInt => [left, x("to be lower than or equal to"), right],
BinOp::GtInt => [left, red("to be greater than"), right], BinOp::GtInt => [left, x("to be greater than"), right],
BinOp::GtEqInt => [left, red("to be greater than or equal to"), right], BinOp::GtEqInt => [left, x("to be greater than or equal to"), right],
_ => unreachable!("unexpected non-boolean binary operator in assertion?"), _ => unreachable!("unexpected non-boolean binary operator in assertion?"),
} }
.join("\n") .join("\n")

View File

@ -1,13 +1,18 @@
use crate::pretty;
use aiken_lang::{ use aiken_lang::{
ast::OnTestFailure,
expr::UntypedExpr, expr::UntypedExpr,
format::Formatter,
test_framework::{PropertyTestResult, TestResult, UnitTestResult}, test_framework::{PropertyTestResult, TestResult, UnitTestResult},
}; };
use owo_colors::{OwoColorize, Stream::Stderr}; pub use json::Json;
use std::{collections::BTreeMap, fmt::Display, path::PathBuf}; use std::{
use uplc::machine::cost_model::ExBudget; collections::BTreeMap,
fmt::Display,
io::{self, IsTerminal},
path::PathBuf,
};
pub use terminal::Terminal;
mod json;
mod terminal;
pub trait EventListener { pub trait EventListener {
fn handle_event(&self, _event: Event) {} fn handle_event(&self, _event: Event) {}
@ -57,6 +62,30 @@ pub enum Event {
ResolvingVersions, 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 { pub enum DownloadSource {
Network, Network,
Cache, Cache,
@ -71,431 +100,9 @@ impl Display for DownloadSource {
} }
} }
#[derive(Debug, Default, Clone, Copy)] pub(crate) fn group_by_module(
pub struct Terminal; results: &[TestResult<UntypedExpr, UntypedExpr>],
) -> BTreeMap<String, Vec<&TestResult<UntypedExpr, UntypedExpr>>> {
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::<Vec<String>>()
.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<UntypedExpr, UntypedExpr>,
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::<Vec<String>>()
.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::<Vec<_>>();
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::<Vec<_>>()
.join("\n")
);
};
test
}
fn fmt_test_summary<T>(tests: &[&TestResult<T, T>], 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<T>(results: &Vec<TestResult<T, T>>) -> BTreeMap<String, Vec<&TestResult<T, T>>> {
let mut modules = BTreeMap::new(); let mut modules = BTreeMap::new();
for r in results { for r in results {
let xs: &mut Vec<&TestResult<_, _>> = modules.entry(r.module().to_string()).or_default(); let xs: &mut Vec<&TestResult<_, _>> = modules.entry(r.module().to_string()).or_default();
@ -504,7 +111,7 @@ fn group_by_module<T>(results: &Vec<TestResult<T, T>>) -> BTreeMap<String, Vec<&
modules modules
} }
fn find_max_execution_units<T>(xs: &[TestResult<T, T>]) -> (usize, usize, usize) { pub(crate) fn find_max_execution_units<T>(xs: &[TestResult<T, T>]) -> (usize, usize, usize) {
let (max_mem, max_cpu, max_iter) = let (max_mem, max_cpu, max_iter) =
xs.iter() xs.iter()
.fold((0, 0, 0), |(max_mem, max_cpu, max_iter), test| match test { .fold((0, 0, 0), |(max_mem, max_cpu, max_iter), test| match test {

View File

@ -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::<Vec<_>>(),
})
}).collect::<Vec<_>>(),
});
println!("{}", serde_json::to_string_pretty(&json_output).unwrap());
}
_ => super::Terminal.handle_event(event),
}
}
}
fn fmt_test_json(result: &TestResult<UntypedExpr, UntypedExpr>) -> 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<UntypedExpr, UntypedExpr>]) -> 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<Item = &'a TestResult<UntypedExpr, UntypedExpr>>,
{
tests
.filter(|t| matches!(t, TestResult::UnitTestResult { .. }))
.count()
}
fn count_property_tests<'a, I>(tests: I) -> usize
where
I: Iterator<Item = &'a TestResult<UntypedExpr, UntypedExpr>>,
{
tests
.filter(|t| matches!(t, TestResult::PropertyTestResult { .. }))
.count()
}

View File

@ -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::<Vec<String>>()
.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<UntypedExpr, UntypedExpr>,
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::<Vec<String>>()
.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::<Vec<_>>();
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::<Vec<_>>()
.join("\n")
);
};
test
}
fn fmt_test_summary<T>(tests: &[&TestResult<T, T>], 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()),
)
}

View File

@ -1,4 +1,4 @@
use crate::{telemetry::Terminal, Project}; use crate::{telemetry::EventTarget, Project};
use miette::{Diagnostic, IntoDiagnostic}; use miette::{Diagnostic, IntoDiagnostic};
use notify::{Event, RecursiveMode, Watcher}; use notify::{Event, RecursiveMode, Watcher};
use owo_colors::{OwoColorize, Stream::Stderr}; use owo_colors::{OwoColorize, Stream::Stderr};
@ -88,9 +88,14 @@ pub fn default_filter(evt: &Event) -> bool {
} }
} }
pub fn with_project<A>(directory: Option<&Path>, deny: bool, mut action: A) -> miette::Result<()> pub fn with_project<A>(
directory: Option<&Path>,
deny: bool,
json: bool,
mut action: A,
) -> miette::Result<()>
where where
A: FnMut(&mut Project<Terminal>) -> Result<(), Vec<crate::error::Error>>, A: FnMut(&mut Project<EventTarget>) -> Result<(), Vec<crate::error::Error>>,
{ {
let project_path = if let Some(d) = directory { let project_path = if let Some(d) = directory {
d.to_path_buf() d.to_path_buf()
@ -102,7 +107,7 @@ where
current_dir 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), Ok(p) => Ok(p),
Err(e) => { Err(e) => {
e.report(); e.report();
@ -116,6 +121,7 @@ where
let warning_count = warnings.len(); let warning_count = warnings.len();
if !json {
for warning in &warnings { for warning in &warnings {
warning.report() warning.report()
} }
@ -145,6 +151,7 @@ where
warning_count warning_count
} }
); );
}
if warning_count > 0 && deny { if warning_count > 0 && deny {
Err(ExitFailure::into_report()) Err(ExitFailure::into_report())
@ -159,7 +166,7 @@ where
/// // Note: doctest disabled, because aiken_project doesn't have an implementation of EventListener I can use /// // 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::watch::{watch_project, default_filter};
/// use aiken_project::{Project}; /// use aiken_project::{Project};
/// watch_project(None, Terminal, default_filter, 500, |project| { /// watch_project(None, default_filter, 500, |project| {
/// println!("Project changed!"); /// println!("Project changed!");
/// Ok(()) /// Ok(())
/// }); /// });
@ -172,7 +179,7 @@ pub fn watch_project<F, A>(
) -> miette::Result<()> ) -> miette::Result<()>
where where
F: Fn(&Event) -> bool, F: Fn(&Event) -> bool,
A: FnMut(&mut Project<Terminal>) -> Result<(), Vec<crate::error::Error>>, A: FnMut(&mut Project<EventTarget>) -> Result<(), Vec<crate::error::Error>>,
{ {
let project_path = directory let project_path = directory
.map(|p| p.to_path_buf()) .map(|p| p.to_path_buf())
@ -239,7 +246,7 @@ where
.if_supports_color(Stderr, |s| s.bold()) .if_supports_color(Stderr, |s| s.bold())
.if_supports_color(Stderr, |s| s.purple()), .if_supports_color(Stderr, |s| s.purple()),
); );
with_project(directory, false, &mut action).unwrap_or(()) with_project(directory, false, false, &mut action).unwrap_or(())
} }
} }
} }

View File

@ -30,6 +30,7 @@ clap = { version = "4.1.8", features = [
"string", "string",
] } ] }
clap_complete = "4.3.2" clap_complete = "4.3.2"
color-print = "0.3.7"
hex = "0.4.3" hex = "0.4.3"
ignore = "0.4.20" ignore = "0.4.20"
indoc = "2.0" indoc = "2.0"

View File

@ -33,7 +33,7 @@ pub fn exec(
mainnet, mainnet,
}: Args, }: Args,
) -> miette::Result<()> { ) -> miette::Result<()> {
with_project(directory.as_deref(), false, |p| { with_project(directory.as_deref(), false, false, |p| {
let title = module.as_ref().map(|m| { let title = module.as_ref().map(|m| {
format!( format!(
"{m}{}", "{m}{}",

View File

@ -54,7 +54,7 @@ pub fn exec(
validator, validator,
}: Args, }: Args,
) -> miette::Result<()> { ) -> miette::Result<()> {
with_project(None, false, |p| { with_project(None, false, false, |p| {
let title = module.as_ref().map(|m| { let title = module.as_ref().map(|m| {
format!( format!(
"{m}{}", "{m}{}",

View File

@ -23,7 +23,7 @@ pub fn exec(
validator, validator,
}: Args, }: Args,
) -> miette::Result<()> { ) -> miette::Result<()> {
with_project(directory.as_deref(), false, |p| { with_project(directory.as_deref(), false, false, |p| {
let title = module.as_ref().map(|m| { let title = module.as_ref().map(|m| {
format!( format!(
"{m}{}", "{m}{}",

View File

@ -23,7 +23,7 @@ pub fn exec(
validator, validator,
}: Args, }: Args,
) -> miette::Result<()> { ) -> miette::Result<()> {
with_project(directory.as_deref(), false, |p| { with_project(directory.as_deref(), false, false, |p| {
let title = module.as_ref().map(|m| { let title = module.as_ref().map(|m| {
format!( format!(
"{m}{}", "{m}{}",

View File

@ -82,7 +82,7 @@ pub fn exec(
) )
}) })
} else { } else {
with_project(directory.as_deref(), deny, |p| { with_project(directory.as_deref(), deny, false, |p| {
p.build( p.build(
uplc, uplc,
match trace_filter { match trace_filter {

View File

@ -5,10 +5,108 @@ use aiken_lang::{
}; };
use aiken_project::watch::{self, watch_project, with_project}; use aiken_project::watch::{self, watch_project, with_project};
use rand::prelude::*; use rand::prelude::*;
use std::{path::PathBuf, process}; use std::{
io::{self, IsTerminal},
path::PathBuf,
process,
};
#[derive(clap::Args)] #[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#"<bold><underline>Output JSON schema:</underline></bold>
<bold>type</bold>: object
<bold>properties</bold>:
<bold>seed</bold>: <cyan>&type_integer</cyan>
<bold>type</bold>: integer
<bold>summary</bold>:
<bold>type</bold>: object
<bold>properties</bold>: <cyan>&type_summary</cyan>
<bold>total</bold>: *type_integer
<bold>passed</bold>: *type_integer
<bold>failed</bold>: *type_integer
<bold>kind</bold>:
<bold>type</bold>: object
<bold>properties</bold>:
<bold>unit</bold>: *type_integer
<bold>property</bold>: *type_integer
<bold>modules</bold>:
<bold>type</bold>: array
<bold>items</bold>:
<bold>type</bold>: object
<bold>properties</bold>:
<bold>name</bold>: <cyan>&type_string</cyan>
<bold>type</bold>: string
<bold>summary</bold>: *type_summary
<bold>test</bold>:
<bold>type</bold>: array
<bold>items</bold>:
<bold>oneOf</bold>:
- <bold>type</bold>: object
<bold>required</bold>:
- kind
- title
- status
- on_failure
- execution_units
<bold>properties</bold>:
<bold>kind</bold>
<bold>type</bold>: string
<bold>enum</bold>: [ "unit" ]
<bold>title</bold>: *type_string
<bold>status</bold>: <cyan>&type_status</cyan>
<bold>type</bold>: string
<bold>enum</bold>: [ "pass", "fail" ]
<bold>on_failure</bold>: <cyan>&type_on_failure</cyan>
<bold>type</bold>: string
<bold>enum</bold>:
- fail_immediately
- succeed_immediately
- succeed_eventually
<bold>execution_units</bold>:
<bold>type</bold>: object
<bold>properties</bold>:
<bold>mem</bold>: *type_integer
<bold>cpu</bold>: *type_integer
<bold>assertion</bold>: *type_string
- <bold>type</bold>: object
<bold>required</bold>:
- kind
- title
- status
- on_failure
- iterations
- counterexample
<bold>properties</bold>:
<bold>kind</bold>
<bold>type</bold>: string
<bold>enum</bold>: [ "property" ]
<bold>title</bold>: *type_string
<bold>status</bold>: *type_status
<bold>on_failure</bold>: *type_on_failure
<bold>iterations</bold>: *type_integer
<bold>labels</bold>:
<bold>type</bold>: object
<bold>additionalProperties</bold>: *type_integer
<bold>counterexample</bold>:
<bold>oneOf</bold>:
- *type_string
- <bold>type</bold>: "null"
- <bold>type</bold>: object
<bold>properties</bold>:
<bold>error</bold>: *type_string
<bold><underline>Note:</underline></bold>
You are seeing the extended help. Use `-h` instead of `--help` for a more compact view.
"#
))]
pub struct Args { pub struct Args {
/// Path to project /// Path to project
directory: Option<PathBuf>, directory: Option<PathBuf>,
@ -40,7 +138,7 @@ pub struct Args {
/// Only run tests if they match any of these strings. /// 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 module with `-m aiken/list` or `-m list`.
/// You can match a test with `-m "aiken/list.{map}"` or `-m "aiken/option.{flatten_1}"` /// 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<Vec<String>>, match_tests: Option<Vec<String>>,
/// This is meant to be used with `--match-tests`. /// This is meant to be used with `--match-tests`.
@ -72,14 +170,9 @@ pub struct Args {
/// Choose the verbosity level of traces: /// Choose the verbosity level of traces:
/// ///
/// - silent: /// - silent: disable traces altogether
/// 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
/// - compact:
/// only culprit line numbers are shown on failures
///
/// - verbose:
/// enable full verbose traces as provided by the user or the compiler
/// ///
/// [optional] /// [optional]
#[clap(short, long, value_parser=trace_level_parser(), default_value_t=TraceLevel::Verbose, verbatim_doc_comment)] #[clap(short, long, value_parser=trace_level_parser(), default_value_t=TraceLevel::Verbose, verbatim_doc_comment)]
@ -123,7 +216,11 @@ pub fn exec(
) )
}) })
} else { } else {
with_project(directory.as_deref(), deny, |p| { with_project(
directory.as_deref(),
deny,
!io::stdout().is_terminal(),
|p| {
p.check( p.check(
skip_tests, skip_tests,
match_tests.clone(), match_tests.clone(),
@ -137,7 +234,8 @@ pub fn exec(
}, },
env.clone(), env.clone(),
) )
}) },
)
}; };
result.map_err(|_| process::exit(1)) result.map_err(|_| process::exit(1))

View File

@ -38,7 +38,7 @@ pub fn exec(
p.docs(destination.clone(), include_dependencies) p.docs(destination.clone(), include_dependencies)
}) })
} else { } else {
with_project(directory.as_deref(), deny, |p| { with_project(directory.as_deref(), deny, false, |p| {
p.docs(destination.clone(), include_dependencies) p.docs(destination.clone(), include_dependencies)
}) })
}; };

View File

@ -61,7 +61,7 @@ pub fn exec(
trace_level, trace_level,
}: Args, }: Args,
) -> miette::Result<()> { ) -> miette::Result<()> {
with_project(directory.as_deref(), false, |p| { with_project(directory.as_deref(), false, false, |p| {
p.compile(Options::default())?; p.compile(Options::default())?;
let export = p.export( let export = p.export(