Define new event target for JSON, and revert option passing

For the program to be consistent, the 'EventListener' target that we
  pass to a Project should be responsible for the output format.
  Otherwise, we are contingent on developers to remember passing the
  option at call-site. Plus, it overloads the project code with an extra
  boolean option.

  Instead, since the behaviour is solely driven by the execution
  context, we can instantiate a different event target upfront, and
  simply hold on to it throughout the program.

  As a nice side-effect, we can gently re-organize the code to keep the
  terminal printing logic and the json printing logic separate.
This commit is contained in:
KtorZ 2024-11-13 11:01:58 +01:00
parent 1a75568027
commit d24a71ee04
No known key found for this signature in database
GPG Key ID: 33173CB6F77F4277
9 changed files with 621 additions and 597 deletions

View File

@ -38,7 +38,6 @@ impl LspProject {
PropertyTest::DEFAULT_MAX_SUCCESS,
Tracing::verbose(),
None,
false,
);
self.project.restore(checkpoint);

View File

@ -197,13 +197,11 @@ where
uplc: bool,
tracing: Tracing,
env: Option<String>,
json: bool,
) -> Result<(), Vec<Error>> {
let options = Options {
code_gen_mode: CodeGenMode::Build(uplc),
tracing,
env,
json,
};
self.compile(options)
@ -227,7 +225,7 @@ where
let mut modules = self.parse_sources(self.config.name.clone())?;
self.type_check(&mut modules, Tracing::silent(), None, false, false)?;
self.type_check(&mut modules, Tracing::silent(), None, false)?;
let destination = destination.unwrap_or_else(|| self.root.join("docs"));
@ -269,7 +267,6 @@ where
property_max_success: usize,
tracing: Tracing,
env: Option<String>,
json: bool,
) -> Result<(), Vec<Error>> {
let options = Options {
tracing,
@ -285,7 +282,6 @@ where
property_max_success,
}
},
json,
};
self.compile(options)
@ -347,7 +343,6 @@ where
root: self.root.clone(),
name: self.config.name.to_string(),
version: self.config.version.clone(),
json: options.json,
});
let env = options.env.as_deref();
@ -358,7 +353,7 @@ where
let mut modules = self.parse_sources(self.config.name.clone())?;
self.type_check(&mut modules, options.tracing, env, true, options.json)?;
self.type_check(&mut modules, options.tracing, env, true)?;
match options.code_gen_mode {
CodeGenMode::Build(uplc_dump) => {
@ -405,8 +400,7 @@ where
self.collect_tests(verbose, match_tests, exact_match, options.tracing)?;
if !tests.is_empty() {
self.event_listener
.handle_event(Event::RunningTests { json: options.json });
self.event_listener.handle_event(Event::RunningTests);
}
let tests = self.run_tests(tests, seed, property_max_success);
@ -433,11 +427,8 @@ where
})
.collect();
self.event_listener.handle_event(Event::FinishedTests {
seed,
tests,
json: options.json,
});
self.event_listener
.handle_event(Event::FinishedTests { seed, tests });
if !errors.is_empty() {
Err(errors)
@ -646,11 +637,7 @@ where
Ok(blueprint)
}
fn with_dependencies(
&mut self,
parsed_packages: &mut ParsedModules,
json: bool,
) -> Result<(), Vec<Error>> {
fn with_dependencies(&mut self, parsed_packages: &mut ParsedModules) -> Result<(), Vec<Error>> {
let manifest = deps::download(&self.event_listener, &self.root, &self.config)?;
for package in manifest.packages {
@ -661,7 +648,6 @@ where
root: lib.clone(),
name: package.name.to_string(),
version: package.version.clone(),
json,
});
self.read_package_source_files(&lib.join("lib"))?;
@ -850,11 +836,10 @@ where
tracing: Tracing,
env: Option<&str>,
validate_module_name: bool,
json: bool,
) -> Result<(), Vec<Error>> {
let our_modules: BTreeSet<String> = modules.keys().cloned().collect();
self.with_dependencies(modules, json)?;
self.with_dependencies(modules)?;
for name in modules.sequence(&our_modules)? {
if let Some(module) = modules.remove(&name) {

View File

@ -4,7 +4,6 @@ pub struct Options {
pub code_gen_mode: CodeGenMode,
pub tracing: Tracing,
pub env: Option<String>,
pub json: bool,
}
impl Default for Options {
@ -13,7 +12,6 @@ impl Default for Options {
code_gen_mode: CodeGenMode::NoOp,
tracing: Tracing::silent(),
env: None,
json: false,
}
}
}

View File

@ -1,14 +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 serde_json::json;
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) {}
@ -19,7 +23,6 @@ pub enum Event {
name: String,
version: String,
root: PathBuf,
json: bool,
},
BuildingDocumentation {
name: String,
@ -39,13 +42,10 @@ pub enum Event {
name: String,
path: PathBuf,
},
RunningTests {
json: bool,
},
RunningTests,
FinishedTests {
seed: u32,
tests: Vec<TestResult<UntypedExpr, UntypedExpr>>,
json: bool,
},
WaitingForBuildDirLock,
ResolvingPackages {
@ -62,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,
@ -76,554 +100,7 @@ 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,
json,
} => {
if !json {
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 { json } => {
if !json {
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, json } => {
let (max_mem, max_cpu, max_iter) = find_max_execution_units(&tests);
if json {
let json_output = serde_json::json!({
"seed": seed,
"modules": group_by_module(&tests).iter().map(|(module, results)| {
serde_json::json!({
"name": module,
"tests": results.iter().map(|r| fmt_test_json(r)).collect::<Vec<_>>(),
"summary": fmt_test_summary_json(results)
})
}).collect::<Vec<_>>(),
"summary": fmt_overall_summary_json(&tests)
});
println!("{}", serde_json::to_string_pretty(&json_output).unwrap());
} else {
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 fmt_test_json(result: &TestResult<UntypedExpr, UntypedExpr>) -> serde_json::Value {
let mut test = json!({
"name": result.title(),
"status": if result.is_success() { "PASS" } else { "FAIL" },
});
match result {
TestResult::UnitTestResult(UnitTestResult {
spent_budget,
assertion,
test: unit_test,
..
}) => {
test["execution_units"] = json!({
"memory": spent_budget.mem,
"cpu": spent_budget.cpu,
});
if !result.is_success() {
if let Some(assertion) = assertion {
test["assertion"] = json!({
"message": assertion.to_string(Stderr, false),
"expected_to_fail": matches!(unit_test.on_test_failure, OnTestFailure::SucceedEventually | OnTestFailure::SucceedImmediately),
});
}
}
}
TestResult::PropertyTestResult(PropertyTestResult {
iterations,
labels,
counterexample,
..
}) => {
test["iterations"] = json!(iterations);
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,
})
}
fn fmt_overall_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;
let modules = group_by_module(tests);
let module_count = modules.len();
let (max_mem, max_cpu, max_iter) = find_max_execution_units(tests);
// Separate counts for unit tests and property-based tests
let unit_tests = tests
.iter()
.filter(|t| matches!(t, TestResult::UnitTestResult { .. }))
.count();
let property_tests = tests
.iter()
.filter(|t| matches!(t, TestResult::PropertyTestResult { .. }))
.count();
json!({
"total_tests": total,
"passed_tests": passed,
"failed_tests": failed,
"unit_tests": unit_tests,
"property_tests": property_tests,
"module_count": module_count,
"max_execution_units": {
"memory": max_mem,
"cpu": max_cpu,
},
"max_iterations": max_iter,
"modules": modules.into_iter().map(|(module, results)| {
json!({
"name": module,
"tests": results.iter().map(|r| fmt_test_json(r)).collect::<Vec<_>>(),
"summary": fmt_test_summary_json(&results)
})
}).collect::<Vec<_>>(),
})
}
fn group_by_module(
pub(crate) fn group_by_module(
results: &[TestResult<UntypedExpr, UntypedExpr>],
) -> BTreeMap<String, Vec<&TestResult<UntypedExpr, UntypedExpr>>> {
let mut modules = BTreeMap::new();
@ -634,7 +111,7 @@ fn group_by_module(
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) =
xs.iter()
.fold((0, 0, 0), |(max_mem, max_cpu, max_iter), test| match test {

View File

@ -0,0 +1,137 @@
use super::{find_max_execution_units, group_by_module, Event, EventListener};
use aiken_lang::{
ast::OnTestFailure,
expr::UntypedExpr,
format::Formatter,
test_framework::{PropertyTestResult, TestResult, UnitTestResult},
};
use owo_colors::Stream::Stderr;
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 json_output = serde_json::json!({
"seed": seed,
"modules": group_by_module(&tests).iter().map(|(module, results)| {
serde_json::json!({
"name": module,
"tests": results.iter().map(|r| fmt_test_json(r)).collect::<Vec<_>>(),
"summary": fmt_test_summary_json(results)
})
}).collect::<Vec<_>>(),
"summary": fmt_overall_summary_json(&tests)
});
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 mut test = json!({
"name": result.title(),
"status": if result.is_success() { "PASS" } else { "FAIL" },
});
match result {
TestResult::UnitTestResult(UnitTestResult {
spent_budget,
assertion,
test: unit_test,
..
}) => {
test["execution_units"] = json!({
"memory": spent_budget.mem,
"cpu": spent_budget.cpu,
});
if !result.is_success() {
if let Some(assertion) = assertion {
test["assertion"] = json!({
"message": assertion.to_string(Stderr, false),
"expected_to_fail": matches!(unit_test.on_test_failure, OnTestFailure::SucceedEventually | OnTestFailure::SucceedImmediately),
});
}
}
}
TestResult::PropertyTestResult(PropertyTestResult {
iterations,
labels,
counterexample,
..
}) => {
test["iterations"] = json!(iterations);
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,
})
}
fn fmt_overall_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;
let modules = group_by_module(tests);
let module_count = modules.len();
let (max_mem, max_cpu, max_iter) = find_max_execution_units(tests);
// Separate counts for unit tests and property-based tests
let unit_tests = tests
.iter()
.filter(|t| matches!(t, TestResult::UnitTestResult { .. }))
.count();
let property_tests = tests
.iter()
.filter(|t| matches!(t, TestResult::PropertyTestResult { .. }))
.count();
json!({
"total_tests": total,
"passed_tests": passed,
"failed_tests": failed,
"unit_tests": unit_tests,
"property_tests": property_tests,
"module_count": module_count,
"max_execution_units": {
"memory": max_mem,
"cpu": max_cpu,
},
"max_iterations": max_iter,
"modules": modules.into_iter().map(|(module, results)| {
json!({
"name": module,
"tests": results.iter().map(|r| fmt_test_json(r)).collect::<Vec<_>>(),
"summary": fmt_test_summary_json(&results)
})
}).collect::<Vec<_>>(),
})
}

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::{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(
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()),
)
}

View File

@ -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};
@ -95,7 +95,7 @@ pub fn with_project<A>(
mut action: A,
) -> miette::Result<()>
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 {
d.to_path_buf()
@ -107,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();
@ -166,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(())
/// });
@ -179,7 +179,7 @@ pub fn watch_project<F, A>(
) -> miette::Result<()>
where
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
.map(|p| p.to_path_buf())

View File

@ -79,7 +79,6 @@ pub fn exec(
None => Tracing::All(trace_level),
},
env.clone(),
false,
)
})
} else {
@ -91,7 +90,6 @@ pub fn exec(
None => Tracing::All(trace_level),
},
env.clone(),
false,
)
})
};

View File

@ -110,8 +110,6 @@ pub fn exec(
let seed = seed.unwrap_or_else(|| rng.gen());
let json_output = !io::stdout().is_terminal();
let result = if watch {
watch_project(directory.as_deref(), watch::default_filter, 500, |p| {
p.check(
@ -126,7 +124,6 @@ pub fn exec(
None => Tracing::All(trace_level),
},
env.clone(),
json_output,
)
})
} else {
@ -147,7 +144,6 @@ pub fn exec(
None => Tracing::All(trace_level),
},
env.clone(),
json_output,
)
},
)