From 6c039708c3aa89707760a2bf81b6211236af5847 Mon Sep 17 00:00:00 2001 From: KtorZ Date: Sat, 25 Nov 2023 14:48:22 +0100 Subject: [PATCH] Rework 'watch_project' to reuse 'with_project' Also removed the 'clear' flag to do it by default instead of clogging the terminal view. This now works pretty nicely, and the logic is back under `aiken_project`. --- crates/aiken-project/src/telemetry.rs | 344 +++++++++++++++- crates/aiken-project/src/watch.rs | 140 ++++++- crates/aiken/src/cmd/blueprint/address.rs | 4 +- crates/aiken/src/cmd/blueprint/apply.rs | 2 +- crates/aiken/src/cmd/blueprint/hash.rs | 4 +- crates/aiken/src/cmd/blueprint/policy.rs | 4 +- crates/aiken/src/cmd/build.rs | 5 +- crates/aiken/src/cmd/check.rs | 74 +--- crates/aiken/src/cmd/docs.rs | 3 +- crates/aiken/src/lib.rs | 458 ---------------------- crates/aiken/src/main.rs | 6 +- 11 files changed, 484 insertions(+), 560 deletions(-) delete mode 100644 crates/aiken/src/lib.rs diff --git a/crates/aiken-project/src/telemetry.rs b/crates/aiken-project/src/telemetry.rs index 97b23f5c..3440fbe3 100644 --- a/crates/aiken-project/src/telemetry.rs +++ b/crates/aiken-project/src/telemetry.rs @@ -1,5 +1,11 @@ +use crate::pretty; use crate::script::EvalInfo; -use std::{fmt::Display, path::PathBuf}; +use owo_colors::{ + OwoColorize, + Stream::{self, Stderr}, +}; +use std::{collections::BTreeMap, fmt::Display, path::PathBuf}; +use uplc::machine::cost_model::ExBudget; pub trait EventListener { fn handle_event(&self, _event: Event) {} @@ -64,3 +70,339 @@ impl Display for DownloadSource { } } } + +#[derive(Debug, Default, Clone, Copy)] +pub struct Terminal; + +impl EventListener for Terminal { + fn handle_event(&self, event: Event) { + match event { + Event::StartingCompilation { + name, + version, + root, + } => { + eprintln!( + "{} {} {} ({})", + " Compiling" + .if_supports_color(Stderr, |s| s.bold()) + .if_supports_color(Stderr, |s| s.purple()), + name.if_supports_color(Stderr, |s| s.bold()), + version, + root.display() + .if_supports_color(Stderr, |s| s.bright_blue()) + ); + } + Event::BuildingDocumentation { + name, + version, + root, + } => { + eprintln!( + "{} {} {} ({})", + " 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::EvaluatingFunction { results } => { + eprintln!( + "{}\n", + " Evaluating function ..." + .if_supports_color(Stderr, |s| s.bold()) + .if_supports_color(Stderr, |s| s.purple()) + ); + + let (max_mem, max_cpu) = find_max_execution_units(&results); + + for eval_info in &results { + println!(" {}", fmt_eval(eval_info, max_mem, max_cpu, Stderr)) + } + } + 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 { tests } => { + let (max_mem, max_cpu) = find_max_execution_units(&tests); + + for (module, infos) 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 = infos + .iter() + .map(|eval_info| fmt_test(eval_info, max_mem, max_cpu, true)) + .collect::>() + .join("\n"); + + let summary = fmt_test_summary(infos, true); + + eprintln!( + "{}\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(eval_info: &EvalInfo, max_mem: usize, max_cpu: usize, styled: bool) -> String { + let EvalInfo { + success, + script, + spent_budget, + logs, + .. + } = eval_info; + + 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, " "); + + let test = format!( + "{status} [mem: {mem_unit}, cpu: {cpu_unit}] {module}", + status = if *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() + }) + }, + 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()), + module = pretty::style_if(styled, script.name.clone(), |s| s + .if_supports_color(Stderr, |s| s.bright_blue()) + .to_string()), + ); + + let logs = if logs.is_empty() { + String::new() + } else { + logs.iter() + .map(|line| { + format!( + "{arrow} {styled_line}", + arrow = "↳".if_supports_color(Stderr, |s| s.bright_yellow()), + styled_line = line.if_supports_color(Stderr, |s| s.bright_black()) + ) + }) + .collect::>() + .join("\n") + }; + + if logs.is_empty() { + test + } else { + [test, logs].join("\n") + } +} + +fn fmt_test_summary(tests: &Vec<&EvalInfo>, styled: bool) -> String { + let (n_passed, n_failed) = tests + .iter() + .fold((0, 0), |(n_passed, n_failed), test_info| { + if test_info.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_eval(eval_info: &EvalInfo, max_mem: usize, max_cpu: usize, stream: Stream) -> String { + let EvalInfo { + output, + script, + spent_budget, + .. + } = eval_info; + + let ExBudget { mem, cpu } = spent_budget; + + format!( + " {}::{} [mem: {}, cpu: {}]\n │\n ╰─▶ {}", + script.module.if_supports_color(stream, |s| s.blue()), + script.name.if_supports_color(stream, |s| s.bright_blue()), + pretty::pad_left(mem.to_string(), max_mem, " "), + pretty::pad_left(cpu.to_string(), max_cpu, " "), + output + .as_ref() + .map(|x| format!("{x}")) + .unwrap_or_else(|| "Error.".to_string()), + ) +} + +fn group_by_module(infos: &Vec) -> BTreeMap> { + let mut modules = BTreeMap::new(); + for eval_info in infos { + let xs: &mut Vec<&EvalInfo> = modules.entry(eval_info.script.module.clone()).or_default(); + xs.push(eval_info); + } + modules +} + +fn find_max_execution_units(xs: &[EvalInfo]) -> (usize, usize) { + let (max_mem, max_cpu) = xs.iter().fold( + (0, 0), + |(max_mem, max_cpu), EvalInfo { spent_budget, .. }| { + if spent_budget.mem >= max_mem && spent_budget.cpu >= max_cpu { + (spent_budget.mem, spent_budget.cpu) + } else if spent_budget.mem > max_mem { + (spent_budget.mem, max_cpu) + } else if spent_budget.cpu > max_cpu { + (max_mem, spent_budget.cpu) + } else { + (max_mem, max_cpu) + } + }, + ); + + (max_mem.to_string().len(), max_cpu.to_string().len()) +} diff --git a/crates/aiken-project/src/watch.rs b/crates/aiken-project/src/watch.rs index 61dda460..f3709ca5 100644 --- a/crates/aiken-project/src/watch.rs +++ b/crates/aiken-project/src/watch.rs @@ -1,14 +1,57 @@ -use miette::IntoDiagnostic; +use crate::{telemetry::Terminal, Project}; +use miette::{Diagnostic, IntoDiagnostic}; use notify::{Event, RecursiveMode, Watcher}; +use owo_colors::{OwoColorize, Stream::Stderr}; use std::{ collections::VecDeque, env, ffi::OsStr, - path::PathBuf, + fmt::{self, Display}, + path::Path, sync::{Arc, Mutex}, }; -use crate::{telemetry, Project}; +#[derive(Debug, Diagnostic, thiserror::Error)] +enum ExitFailure { + #[error("")] + ExitFailure, +} + +impl ExitFailure { + fn into_report() -> miette::Report { + ExitFailure::ExitFailure.into() + } +} + +struct Summary { + warning_count: usize, + error_count: usize, +} + +impl Display for Summary { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str(&format!( + "{}\n {} {}, {} {}", + "Summary" + .if_supports_color(Stderr, |s| s.purple()) + .if_supports_color(Stderr, |s| s.bold()), + self.error_count, + if self.error_count == 1 { + "error" + } else { + "errors" + } + .if_supports_color(Stderr, |s| s.red()), + self.warning_count, + if self.warning_count == 1 { + "warning" + } else { + "warnings" + } + .if_supports_color(Stderr, |s| s.yellow()), + )) + } +} /// A default filter for file events that catches the most relevant "source" changes pub fn default_filter(evt: &Event) -> bool { @@ -30,6 +73,65 @@ pub fn default_filter(evt: &Event) -> bool { } } +pub fn with_project(directory: Option<&Path>, deny: bool, mut action: A) -> miette::Result<()> +where + A: FnMut(&mut Project) -> Result<(), Vec>, +{ + let project_path = if let Some(d) = directory { + d.to_path_buf() + } else { + env::current_dir().into_diagnostic()? + }; + + let mut project = match Project::new(project_path, Terminal) { + Ok(p) => Ok(p), + Err(e) => { + e.report(); + Err(ExitFailure::into_report()) + } + }?; + + let build_result = action(&mut project); + + let warnings = project.warnings(); + + let warning_count = warnings.len(); + + for warning in &warnings { + warning.report() + } + + if let Err(errs) = build_result { + for err in &errs { + err.report() + } + + eprintln!( + "\n{}", + Summary { + warning_count, + error_count: errs.len(), + } + ); + + return Err(ExitFailure::into_report()); + } else { + eprintln!( + "\n{}", + Summary { + error_count: 0, + warning_count + } + ); + } + + if warning_count > 0 && deny { + Err(ExitFailure::into_report()) + } else { + Ok(()) + } +} + /// Run a function each time a file in the project changes /// /// ```text @@ -41,19 +143,19 @@ pub fn default_filter(evt: &Event) -> bool { /// Ok(()) /// }); /// ``` -pub fn watch_project( - directory: Option, - events: T, +pub fn watch_project( + directory: Option<&Path>, filter: F, debounce: u32, mut action: A, ) -> miette::Result<()> where - T: Copy + telemetry::EventListener, F: Fn(&Event) -> bool, - A: FnMut(&mut Project) -> Result<(), Vec>, + A: FnMut(&mut Project) -> Result<(), Vec>, { - let project_path = directory.unwrap_or(env::current_dir().into_diagnostic()?); + let project_path = directory + .map(|p| p.to_path_buf()) + .unwrap_or(env::current_dir().into_diagnostic()?); // Set up a queue for events, primarily so we can debounce on related events let queue = Arc::new(Mutex::new(VecDeque::new())); @@ -108,18 +210,14 @@ where // If we have an event that survived the filter, then we can construct the project and invoke the action if latest.is_some() { - let mut project = match Project::new(project_path.clone(), events) { - Ok(p) => p, - Err(e) => { - // TODO: what should we actually do here? - e.report(); - return Err(miette::Report::msg("??")); - } - }; - - // Invoke the action, and abort on an error - // TODO: what should we actually do with the error here? - action(&mut project).or(Err(miette::Report::msg("??")))?; + print!("{esc}c", esc = 27 as char); + println!( + "{} ...", + " Watching" + .if_supports_color(Stderr, |s| s.bold()) + .if_supports_color(Stderr, |s| s.purple()), + ); + with_project(directory, false, &mut action).unwrap_or(()) } } } diff --git a/crates/aiken/src/cmd/blueprint/address.rs b/crates/aiken/src/cmd/blueprint/address.rs index 7f7ebbe5..b90aef17 100644 --- a/crates/aiken/src/cmd/blueprint/address.rs +++ b/crates/aiken/src/cmd/blueprint/address.rs @@ -1,5 +1,5 @@ -use crate::with_project; use aiken_lang::ast::Tracing; +use aiken_project::watch::with_project; use std::path::PathBuf; /// Compute a validator's address. @@ -34,7 +34,7 @@ pub fn exec( rebuild, }: Args, ) -> miette::Result<()> { - with_project(directory, false, |p| { + with_project(directory.as_deref(), false, |p| { if rebuild { p.build(false, Tracing::NoTraces)?; } diff --git a/crates/aiken/src/cmd/blueprint/apply.rs b/crates/aiken/src/cmd/blueprint/apply.rs index e8555f44..b86418f5 100644 --- a/crates/aiken/src/cmd/blueprint/apply.rs +++ b/crates/aiken/src/cmd/blueprint/apply.rs @@ -1,4 +1,3 @@ -use crate::with_project; use aiken_project::{ blueprint::{ self, @@ -7,6 +6,7 @@ use aiken_project::{ }, error::Error, pretty::multiline, + watch::with_project, }; use inquire; use num_bigint::BigInt; diff --git a/crates/aiken/src/cmd/blueprint/hash.rs b/crates/aiken/src/cmd/blueprint/hash.rs index 95471be8..e4a8840b 100644 --- a/crates/aiken/src/cmd/blueprint/hash.rs +++ b/crates/aiken/src/cmd/blueprint/hash.rs @@ -1,5 +1,5 @@ -use crate::with_project; use aiken_lang::ast::Tracing; +use aiken_project::watch::with_project; use std::path::PathBuf; /// Compute a validator's hash @@ -29,7 +29,7 @@ pub fn exec( rebuild, }: Args, ) -> miette::Result<()> { - with_project(directory, false, |p| { + with_project(directory.as_deref(), false, |p| { if rebuild { p.build(false, Tracing::NoTraces)?; } diff --git a/crates/aiken/src/cmd/blueprint/policy.rs b/crates/aiken/src/cmd/blueprint/policy.rs index 549236f7..f6a90f31 100644 --- a/crates/aiken/src/cmd/blueprint/policy.rs +++ b/crates/aiken/src/cmd/blueprint/policy.rs @@ -1,5 +1,5 @@ -use crate::with_project; use aiken_lang::ast::Tracing; +use aiken_project::watch::with_project; use std::path::PathBuf; /// Compute a minting scripts Policy ID @@ -29,7 +29,7 @@ pub fn exec( rebuild, }: Args, ) -> miette::Result<()> { - with_project(directory, false, |p| { + with_project(directory.as_deref(), false, |p| { if rebuild { p.build(false, Tracing::NoTraces)?; } diff --git a/crates/aiken/src/cmd/build.rs b/crates/aiken/src/cmd/build.rs index 898c0796..92007a6a 100644 --- a/crates/aiken/src/cmd/build.rs +++ b/crates/aiken/src/cmd/build.rs @@ -1,3 +1,4 @@ +use aiken_project::watch::with_project; use std::path::PathBuf; #[derive(clap::Args)] @@ -27,5 +28,7 @@ pub fn exec( keep_traces, }: Args, ) -> miette::Result<()> { - crate::with_project(directory, deny, |p| p.build(uplc, keep_traces.into())) + with_project(directory.as_deref(), deny, |p| { + p.build(uplc, keep_traces.into()) + }) } diff --git a/crates/aiken/src/cmd/check.rs b/crates/aiken/src/cmd/check.rs index 59e7c5fb..63fc4068 100644 --- a/crates/aiken/src/cmd/check.rs +++ b/crates/aiken/src/cmd/check.rs @@ -1,10 +1,6 @@ +use aiken_project::watch::{self, watch_project, with_project}; use std::path::PathBuf; -use aiken_project::watch; -use owo_colors::{OwoColorize, Stream::Stderr}; - -use crate::Terminal; - #[derive(clap::Args)] /// Type-check an Aiken project pub struct Args { @@ -27,10 +23,6 @@ pub struct Args { #[clap(long)] watch: bool, - /// When enabled, clear the screen before running - #[clap(long)] - clear: bool, - /// Only run tests if they match any of these strings. /// You can match a module with `-m aiken/list` or `-m list`. /// You can match a test with `-m "aiken/list.{map}"` or `-m "aiken/option.{flatten_1}"` @@ -57,77 +49,21 @@ pub fn exec( exact_match, no_traces, watch, - clear, .. }: Args, ) -> miette::Result<()> { if watch { - watch::watch_project(directory, Terminal, watch::default_filter, 500, |p| { - if clear { - println!("{esc}c", esc = 27 as char); - } - let build_result = p.check( + watch_project(directory.as_deref(), watch::default_filter, 500, |p| { + p.check( skip_tests, match_tests.clone(), debug, exact_match, (!no_traces).into(), - ); - - let warnings = p.warnings(); - - let warning_count = warnings.len(); - - for warning in &warnings { - warning.report() - } - - let plural = if warning_count == 1 { "" } else { "s" }; - - if let Err(errs) = build_result { - for err in &errs { - err.report() - } - - eprintln!( - "\n{}", - "Summary" - .if_supports_color(Stderr, |s| s.purple()) - .if_supports_color(Stderr, |s| s.bold()) - ); - - let warning_text = format!("{warning_count} warning{plural}"); - - let plural = if errs.len() == 1 { "" } else { "s" }; - - let error_text = format!("{} error{}", errs.len(), plural); - - let full_summary = format!( - " {}, {}", - error_text.if_supports_color(Stderr, |s| s.red()), - warning_text.if_supports_color(Stderr, |s| s.yellow()) - ); - - eprintln!("{full_summary}"); - } else { - eprintln!( - "\n{}", - "Summary" - .if_supports_color(Stderr, |s| s.purple()) - .if_supports_color(Stderr, |s| s.bold()) - ); - - let warning_text = format!("{warning_count} warning{plural}"); - - eprintln!( - " 0 errors, {}", - warning_text.if_supports_color(Stderr, |s| s.yellow()), - ); - } - Ok(()) + ) }) } else { - crate::with_project(directory, deny, |p| { + with_project(directory.as_deref(), deny, |p| { p.check( skip_tests, match_tests.clone(), diff --git a/crates/aiken/src/cmd/docs.rs b/crates/aiken/src/cmd/docs.rs index 9b3e94a7..f03d2e7b 100644 --- a/crates/aiken/src/cmd/docs.rs +++ b/crates/aiken/src/cmd/docs.rs @@ -1,3 +1,4 @@ +use aiken_project::watch::with_project; use std::path::PathBuf; #[derive(clap::Args)] @@ -22,5 +23,5 @@ pub fn exec( destination, }: Args, ) -> miette::Result<()> { - crate::with_project(directory, deny, |p| p.docs(destination.clone())) + with_project(directory.as_deref(), deny, |p| p.docs(destination.clone())) } diff --git a/crates/aiken/src/lib.rs b/crates/aiken/src/lib.rs deleted file mode 100644 index a2627f4c..00000000 --- a/crates/aiken/src/lib.rs +++ /dev/null @@ -1,458 +0,0 @@ -use aiken_project::{ - pretty, - script::EvalInfo, - telemetry::{self, DownloadSource}, - Project, -}; -use miette::Diagnostic; -use miette::IntoDiagnostic; -use owo_colors::{ - OwoColorize, - Stream::{self, Stderr}, -}; -use std::{ - collections::BTreeMap, - env, - fmt::{self, Display}, - path::PathBuf, -}; -use uplc::machine::cost_model::ExBudget; - -pub mod cmd; - -#[derive(Debug, Diagnostic, thiserror::Error)] -enum ExitFailure { - #[error("")] - ExitFailure, -} - -impl ExitFailure { - fn into_report() -> miette::Report { - ExitFailure::ExitFailure.into() - } -} - -pub fn with_project(directory: Option, deny: bool, mut action: A) -> miette::Result<()> -where - A: FnMut(&mut Project) -> Result<(), Vec>, -{ - let project_path = if let Some(d) = directory { - d - } else { - env::current_dir().into_diagnostic()? - }; - - let mut project = match Project::new(project_path, Terminal) { - Ok(p) => Ok(p), - Err(e) => { - e.report(); - Err(ExitFailure::into_report()) - } - }?; - - let build_result = action(&mut project); - - let warnings = project.warnings(); - - let warning_count = warnings.len(); - - for warning in &warnings { - warning.report() - } - - if let Err(errs) = build_result { - for err in &errs { - err.report() - } - - eprintln!( - "\n{}", - Summary { - warning_count, - error_count: errs.len(), - } - ); - - return Err(ExitFailure::into_report()); - } else { - eprintln!( - "\n{}", - Summary { - error_count: 0, - warning_count - } - ); - } - - if warning_count > 0 && deny { - Err(ExitFailure::into_report()) - } else { - Ok(()) - } -} - -struct Summary { - warning_count: usize, - error_count: usize, -} - -impl Display for Summary { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - f.write_str(&format!( - "{}\n {} {}, {} {}", - "Summary" - .if_supports_color(Stderr, |s| s.purple()) - .if_supports_color(Stderr, |s| s.bold()), - self.error_count, - if self.error_count == 1 { - "error" - } else { - "errors" - } - .if_supports_color(Stderr, |s| s.red()), - self.warning_count, - if self.warning_count == 1 { - "warning" - } else { - "warnings" - } - .if_supports_color(Stderr, |s| s.yellow()), - )) - } -} - -#[derive(Debug, Default, Clone, Copy)] -pub struct Terminal; - -impl telemetry::EventListener for Terminal { - fn handle_event(&self, event: telemetry::Event) { - match event { - telemetry::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()) - ); - } - telemetry::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()) - ); - } - telemetry::Event::WaitingForBuildDirLock => { - eprintln!( - "{}", - "Waiting for build directory lock ..." - .if_supports_color(Stderr, |s| s.bold()) - .if_supports_color(Stderr, |s| s.purple()) - ); - } - telemetry::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()) - ); - } - telemetry::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()) - ); - } - telemetry::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()) - ); - } - telemetry::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()), - ); - } - telemetry::Event::EvaluatingFunction { results } => { - eprintln!( - "{}\n", - " Evaluating function ..." - .if_supports_color(Stderr, |s| s.bold()) - .if_supports_color(Stderr, |s| s.purple()) - ); - - let (max_mem, max_cpu) = find_max_execution_units(&results); - - for eval_info in &results { - println!(" {}", fmt_eval(eval_info, max_mem, max_cpu, Stderr)) - } - } - telemetry::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()) - ); - } - telemetry::Event::FinishedTests { tests } => { - let (max_mem, max_cpu) = find_max_execution_units(&tests); - - for (module, infos) 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 = infos - .iter() - .map(|eval_info| fmt_test(eval_info, max_mem, max_cpu, true)) - .collect::>() - .join("\n"); - - let summary = fmt_test_summary(infos, true); - - eprintln!( - "{}\n", - pretty::indent( - &pretty::open_box(&title, &tests, &summary, |border| border - .if_supports_color(Stderr, |s| s.bright_black()) - .to_string()), - 4 - ) - ); - } - } - telemetry::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()) - ) - } - telemetry::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()) - ) - } - telemetry::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()) - ) - } - telemetry::Event::ResolvingVersions => { - eprintln!( - "{}", - " Resolving dependencies" - .if_supports_color(Stderr, |s| s.bold()) - .if_supports_color(Stderr, |s| s.purple()), - ) - } - } - } -} - -fn fmt_test(eval_info: &EvalInfo, max_mem: usize, max_cpu: usize, styled: bool) -> String { - let EvalInfo { - success, - script, - spent_budget, - logs, - .. - } = eval_info; - - 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, " "); - - let test = format!( - "{status} [mem: {mem_unit}, cpu: {cpu_unit}] {module}", - status = if *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() - }) - }, - 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()), - module = pretty::style_if(styled, script.name.clone(), |s| s - .if_supports_color(Stderr, |s| s.bright_blue()) - .to_string()), - ); - - let logs = if logs.is_empty() { - String::new() - } else { - logs.iter() - .map(|line| { - format!( - "{arrow} {styled_line}", - arrow = "↳".if_supports_color(Stderr, |s| s.bright_yellow()), - styled_line = line.if_supports_color(Stderr, |s| s.bright_black()) - ) - }) - .collect::>() - .join("\n") - }; - - if logs.is_empty() { - test - } else { - [test, logs].join("\n") - } -} - -fn fmt_test_summary(tests: &Vec<&EvalInfo>, styled: bool) -> String { - let (n_passed, n_failed) = tests - .iter() - .fold((0, 0), |(n_passed, n_failed), test_info| { - if test_info.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_eval(eval_info: &EvalInfo, max_mem: usize, max_cpu: usize, stream: Stream) -> String { - let EvalInfo { - output, - script, - spent_budget, - .. - } = eval_info; - - let ExBudget { mem, cpu } = spent_budget; - - format!( - " {}::{} [mem: {}, cpu: {}]\n │\n ╰─▶ {}", - script.module.if_supports_color(stream, |s| s.blue()), - script.name.if_supports_color(stream, |s| s.bright_blue()), - pretty::pad_left(mem.to_string(), max_mem, " "), - pretty::pad_left(cpu.to_string(), max_cpu, " "), - output - .as_ref() - .map(|x| format!("{x}")) - .unwrap_or_else(|| "Error.".to_string()), - ) -} - -fn group_by_module(infos: &Vec) -> BTreeMap> { - let mut modules = BTreeMap::new(); - for eval_info in infos { - let xs: &mut Vec<&EvalInfo> = modules.entry(eval_info.script.module.clone()).or_default(); - xs.push(eval_info); - } - modules -} - -fn find_max_execution_units(xs: &[EvalInfo]) -> (usize, usize) { - let (max_mem, max_cpu) = xs.iter().fold( - (0, 0), - |(max_mem, max_cpu), EvalInfo { spent_budget, .. }| { - if spent_budget.mem >= max_mem && spent_budget.cpu >= max_cpu { - (spent_budget.mem, spent_budget.cpu) - } else if spent_budget.mem > max_mem { - (spent_budget.mem, max_cpu) - } else if spent_budget.cpu > max_cpu { - (max_mem, spent_budget.cpu) - } else { - (max_mem, max_cpu) - } - }, - ); - - (max_mem.to_string().len(), max_cpu.to_string().len()) -} diff --git a/crates/aiken/src/main.rs b/crates/aiken/src/main.rs index 7ca09dbe..e57d03a2 100644 --- a/crates/aiken/src/main.rs +++ b/crates/aiken/src/main.rs @@ -1,13 +1,15 @@ -use aiken::cmd::{ +use aiken_project::{config, pretty}; +use cmd::{ blueprint::{self, address}, build, check, completion, docs, fmt, lsp, new, packages::{self, add}, tx, uplc, Cmd, }; -use aiken_project::{config, pretty}; use owo_colors::OwoColorize; use std::process; +mod cmd; + fn main() { panic_handler();