aiken/crates/aiken-project/src/telemetry.rs

458 lines
16 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

use crate::{
pretty,
test_framework::{PropertyTestResult, TestResult, UnitTestResult},
};
use aiken_lang::{expr::UntypedExpr, format::Formatter};
use owo_colors::{OwoColorize, Stream::Stderr};
use std::{collections::BTreeMap, fmt::Display, path::PathBuf};
use uplc::machine::cost_model::ExBudget;
pub trait EventListener {
fn handle_event(&self, _event: Event) {}
}
pub enum Event {
StartingCompilation {
name: String,
version: String,
root: PathBuf,
},
BuildingDocumentation {
name: String,
version: String,
root: PathBuf,
},
GeneratingDocFiles {
output_path: PathBuf,
},
GeneratingBlueprint {
path: PathBuf,
},
DumpingUPLC {
path: PathBuf,
},
GeneratingUPLCFor {
name: String,
path: PathBuf,
},
RunningTests,
FinishedTests {
seed: u32,
tests: Vec<TestResult<UntypedExpr>>,
},
WaitingForBuildDirLock,
ResolvingPackages {
name: String,
},
PackageResolveFallback {
name: String,
},
PackagesDownloaded {
start: tokio::time::Instant,
count: usize,
source: DownloadSource,
},
ResolvingVersions,
}
pub enum DownloadSource {
Network,
Cache,
}
impl Display for DownloadSource {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
DownloadSource::Network => write!(f, "network"),
DownloadSource::Cache => write!(f, "cache"),
}
}
}
#[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!(
"{} {} {} ({})",
" Generating documentation"
.if_supports_color(Stderr, |s| s.bold())
.if_supports_color(Stderr, |s| s.purple()),
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!(
"{} in {}",
" Generating documentation files"
.if_supports_color(Stderr, |s| s.bold())
.if_supports_color(Stderr, |s| s.purple()),
output_path
.to_str()
.unwrap_or("")
.if_supports_color(Stderr, |s| s.bright_blue())
);
}
Event::GeneratingUPLCFor { name, path } => {
eprintln!(
"{} {}.{{{}}}",
" Generating UPLC for"
.if_supports_color(Stderr, |s| s.bold())
.if_supports_color(Stderr, |s| s.purple()),
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
)
);
}
}
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>,
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(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 {
test: unit_test, ..
}) if !result.is_success() => {
if let Some(ref assertion) = unit_test.assertion {
test = format!(
"{test}\n{}{new_line}",
assertion.to_string(Stderr, unit_test.can_error),
new_line = if result.logs().is_empty() { "\n" } else { "" },
);
}
}
_ => (),
}
// CounterExample
if let TestResult::PropertyTestResult(PropertyTestResult {
counterexample: Some(counterexample),
..
}) = result
{
let is_expected_failure = result.is_success();
test = format!(
"{test}\n{}\n{}{new_line}",
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"),
new_line = if result.logs().is_empty() { "\n" } else { "" },
);
}
// Traces
if !result.logs().is_empty() {
test = format!(
"{test}\n{title}\n{logs}\n",
title = "· with traces".if_supports_color(Stderr, |s| s.bold()),
logs = result
.logs()
.iter()
.map(|line| { format!("| {line}",) })
.collect::<Vec<_>>()
.join("\n")
);
};
test
}
fn fmt_test_summary<T>(tests: &[&TestResult<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>>) -> BTreeMap<String, Vec<&TestResult<T>>> {
let mut modules = BTreeMap::new();
for r in results {
let xs: &mut Vec<&TestResult<_>> = modules.entry(r.module().to_string()).or_default();
xs.push(r);
}
modules
}
fn find_max_execution_units<T>(xs: &[TestResult<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 {
TestResult::PropertyTestResult(PropertyTestResult { iterations, .. }) => {
(max_mem, max_cpu, std::cmp::max(max_iter, *iterations))
}
TestResult::UnitTestResult(UnitTestResult { spent_budget, .. }) => {
if spent_budget.mem >= max_mem && spent_budget.cpu >= max_cpu {
(spent_budget.mem, spent_budget.cpu, max_iter)
} else if spent_budget.mem > max_mem {
(spent_budget.mem, max_cpu, max_iter)
} else if spent_budget.cpu > max_cpu {
(max_mem, spent_budget.cpu, max_iter)
} else {
(max_mem, max_cpu, max_iter)
}
}
});
(
max_mem.to_string().len(),
max_cpu.to_string().len(),
max_iter.to_string().len(),
)
}