Cherry picked cef3276521ba005fa30af46130dba2836347b1ae

This commit is contained in:
Riley-Kilgore 2024-09-16 08:29:40 -07:00 committed by KtorZ
parent cd42f51f1a
commit 8ac09025f5
No known key found for this signature in database
GPG Key ID: 33173CB6F77F4277
13 changed files with 246 additions and 89 deletions

View File

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

View File

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

View File

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

View File

@ -6,6 +6,7 @@ use aiken_lang::{
test_framework::{PropertyTestResult, TestResult, UnitTestResult}, test_framework::{PropertyTestResult, TestResult, UnitTestResult},
}; };
use owo_colors::{OwoColorize, Stream::Stderr}; use owo_colors::{OwoColorize, Stream::Stderr};
use serde_json::json;
use std::{collections::BTreeMap, fmt::Display, path::PathBuf}; use std::{collections::BTreeMap, fmt::Display, path::PathBuf};
use uplc::machine::cost_model::ExBudget; use uplc::machine::cost_model::ExBudget;
@ -18,6 +19,7 @@ pub enum Event {
name: String, name: String,
version: String, version: String,
root: PathBuf, root: PathBuf,
json: bool,
}, },
BuildingDocumentation { BuildingDocumentation {
name: String, name: String,
@ -37,10 +39,13 @@ pub enum Event {
name: String, name: String,
path: PathBuf, path: PathBuf,
}, },
RunningTests, RunningTests {
json: bool,
},
FinishedTests { FinishedTests {
seed: u32, seed: u32,
tests: Vec<TestResult<UntypedExpr, UntypedExpr>>, tests: Vec<TestResult<UntypedExpr, UntypedExpr>>,
json: bool,
}, },
WaitingForBuildDirLock, WaitingForBuildDirLock,
ResolvingPackages { ResolvingPackages {
@ -81,17 +86,20 @@ impl EventListener for Terminal {
name, name,
version, version,
root, root,
json,
} => { } => {
eprintln!( if !json {
"{} {} {} ({})", eprintln!(
" Compiling" "{} {} {} ({})",
.if_supports_color(Stderr, |s| s.bold()) " Compiling"
.if_supports_color(Stderr, |s| s.purple()), .if_supports_color(Stderr, |s| s.bold())
name.if_supports_color(Stderr, |s| s.bold()), .if_supports_color(Stderr, |s| s.purple()),
version, name.if_supports_color(Stderr, |s| s.bold()),
root.display() version,
.if_supports_color(Stderr, |s| s.bright_blue()) root.display()
); .if_supports_color(Stderr, |s| s.bright_blue())
);
}
} }
Event::BuildingDocumentation { Event::BuildingDocumentation {
name, name,
@ -169,53 +177,70 @@ impl EventListener for Terminal {
name.if_supports_color(Stderr, |s| s.bright_blue()), name.if_supports_color(Stderr, |s| s.bright_blue()),
); );
} }
Event::RunningTests => { Event::RunningTests { json } => {
eprintln!( if !json {
"{} {}", eprintln!(
" Testing" "{} {}\n",
.if_supports_color(Stderr, |s| s.bold()) " Testing"
.if_supports_color(Stderr, |s| s.purple()), .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.bold())
);
}
} }
Event::FinishedTests { seed, tests } => { Event::FinishedTests { seed, tests, json } => {
let (max_mem, max_cpu, max_iter) = find_max_execution_units(&tests); let (max_mem, max_cpu, max_iter) = find_max_execution_units(&tests);
for (module, results) in &group_by_module(&tests) { if json {
let title = module let json_output = serde_json::json!({
.if_supports_color(Stderr, |s| s.bold()) "seed": seed,
.if_supports_color(Stderr, |s| s.blue()) "modules": group_by_module(&tests).iter().map(|(module, results)| {
.to_string(); serde_json::json!({
"name": module,
"tests": results.iter().map(|r| fmt_test_json(r, max_mem, max_cpu, max_iter)).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 let tests = results
.iter() .iter()
.map(|r| fmt_test(r, max_mem, max_cpu, max_iter, true)) .map(|r| fmt_test(r, max_mem, max_cpu, max_iter, true))
.collect::<Vec<String>>() .collect::<Vec<String>>()
.join("\n"); .join("\n");
let seed_info = if results let seed_info = if results
.iter() .iter()
.any(|t| matches!(t, TestResult::PropertyTestResult { .. })) .any(|t| matches!(t, TestResult::PropertyTestResult { .. }))
{ {
format!( format!(
"with {opt}={seed} → ", "with {opt}={seed} → ",
opt = "--seed".if_supports_color(Stderr, |s| s.bold()), opt = "--seed".if_supports_color(Stderr, |s| s.bold()),
seed = format!("{seed}").if_supports_color(Stderr, |s| s.bold()) seed = format!("{seed}").if_supports_color(Stderr, |s| s.bold())
) )
} else { } else {
String::new() String::new()
}; };
let summary = format!("{}{}", seed_info, fmt_test_summary(results, true)); let summary = format!("{}{}", seed_info, fmt_test_summary(results, true));
println!( println!(
"\n{}", "{}\n",
pretty::indent( pretty::indent(
&pretty::open_box(&title, &tests, &summary, |border| border &pretty::open_box(&title, &tests, &summary, |border| border
.if_supports_color(Stderr, |s| s.bright_black()) .if_supports_color(Stderr, |s| s.bright_black())
.to_string()), .to_string()),
4 4
) )
); );
}
} }
if !tests.is_empty() { if !tests.is_empty() {
@ -495,7 +520,105 @@ fn fmt_test_summary<T>(tests: &[&TestResult<T, T>], styled: bool) -> String {
) )
} }
fn group_by_module<T>(results: &Vec<TestResult<T, T>>) -> BTreeMap<String, Vec<&TestResult<T, T>>> { fn fmt_test_json(
result: &TestResult<UntypedExpr, UntypedExpr>,
max_mem: usize,
max_cpu: usize,
max_iter: usize,
) -> 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);
json!({
"total_tests": total,
"passed_tests": passed,
"failed_tests": failed,
"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, max_mem, max_cpu, max_iter)).collect::<Vec<_>>(),
"summary": fmt_test_summary_json(&results),
})
}).collect::<Vec<_>>(),
})
}
fn group_by_module(
results: &[TestResult<UntypedExpr, UntypedExpr>],
) -> BTreeMap<String, Vec<&TestResult<UntypedExpr, UntypedExpr>>> {
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();

View File

@ -88,7 +88,12 @@ 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<Terminal>) -> Result<(), Vec<crate::error::Error>>,
{ {
@ -116,36 +121,38 @@ where
let warning_count = warnings.len(); let warning_count = warnings.len();
for warning in &warnings { if !json {
warning.report() for warning in &warnings {
} warning.report()
}
if let Err(errs) = build_result { if let Err(errs) = build_result {
for err in &errs { for err in &errs {
err.report() err.report()
}
eprintln!(
"{}",
Summary {
check_count: project.checks_count,
warning_count,
error_count: errs.len(),
}
);
return Err(ExitFailure::into_report());
} }
eprintln!( eprintln!(
"{}", "{}",
Summary { Summary {
check_count: project.checks_count, check_count: project.checks_count,
warning_count, error_count: 0,
error_count: errs.len(), warning_count
} }
); );
return Err(ExitFailure::into_report());
} }
eprintln!(
"{}",
Summary {
check_count: project.checks_count,
error_count: 0,
warning_count
}
);
if warning_count > 0 && deny { if warning_count > 0 && deny {
Err(ExitFailure::into_report()) Err(ExitFailure::into_report())
} else { } else {
@ -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

@ -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

@ -79,10 +79,11 @@ pub fn exec(
None => Tracing::All(trace_level), None => Tracing::All(trace_level),
}, },
env.clone(), env.clone(),
false,
) )
}) })
} 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 {
@ -90,6 +91,7 @@ pub fn exec(
None => Tracing::All(trace_level), None => Tracing::All(trace_level),
}, },
env.clone(), env.clone(),
false,
) )
}) })
}; };

View File

@ -84,6 +84,10 @@ pub struct Args {
/// [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)]
trace_level: TraceLevel, trace_level: TraceLevel,
/// Output JSON (useful for scripting & automation)
#[clap(long)]
json: bool,
} }
pub fn exec( pub fn exec(
@ -100,6 +104,7 @@ pub fn exec(
seed, seed,
max_success, max_success,
env, env,
json,
}: Args, }: Args,
) -> miette::Result<()> { ) -> miette::Result<()> {
let mut rng = rand::thread_rng(); let mut rng = rand::thread_rng();
@ -120,10 +125,11 @@ pub fn exec(
None => Tracing::All(trace_level), None => Tracing::All(trace_level),
}, },
env.clone(), env.clone(),
json,
) )
}) })
} else { } else {
with_project(directory.as_deref(), deny, |p| { with_project(directory.as_deref(), deny, json, |p| {
p.check( p.check(
skip_tests, skip_tests,
match_tests.clone(), match_tests.clone(),
@ -136,6 +142,7 @@ pub fn exec(
None => Tracing::All(trace_level), None => Tracing::All(trace_level),
}, },
env.clone(), env.clone(),
json,
) )
}) })
}; };

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(