diff --git a/Cargo.lock b/Cargo.lock index fa46816..cafa289 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -120,6 +120,17 @@ dependencies = [ "syn 2.0.66", ] +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + [[package]] name = "autocfg" version = "1.1.0" @@ -254,10 +265,13 @@ version = "2.4.3" dependencies = [ "anyhow", "async-compression", + "atty", "base64", "clap", + "console", "git2", "gql_client", + "indicatif", "insta", "itertools", "lazy_static", @@ -290,14 +304,15 @@ checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" [[package]] name = "console" -version = "0.15.7" +version = "0.15.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c926e00cc70edefdc64d3a5ff31cc65bb97a3460097762bd23afb4d8145fccf8" +checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" dependencies = [ "encode_unicode", "lazy_static", "libc", - "windows-sys 0.45.0", + "unicode-width", + "windows-sys 0.52.0", ] [[package]] @@ -664,6 +679,15 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + [[package]] name = "hex" version = "0.4.3" @@ -794,6 +818,19 @@ dependencies = [ "hashbrown 0.14.3", ] +[[package]] +name = "indicatif" +version = "0.17.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "763a5a8f45087d6bcea4222e7b72c291a054edf80e4ef6efd2a4979878c7bea3" +dependencies = [ + "console", + "instant", + "number_prefix", + "portable-atomic", + "unicode-width", +] + [[package]] name = "insta" version = "1.34.0" @@ -1049,6 +1086,12 @@ dependencies = [ "autocfg", ] +[[package]] +name = "number_prefix" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" + [[package]] name = "object" version = "0.32.1" @@ -1235,6 +1278,12 @@ version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" +[[package]] +name = "portable-atomic" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7170ef9988bc169ba16dd36a7fa041e5c4cbeb6a35b76d4c03daded371eae7c0" + [[package]] name = "powerfmt" version = "0.2.0" @@ -2013,6 +2062,12 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "unicode-width" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" + [[package]] name = "unsafe-libyaml" version = "0.2.11" @@ -2226,15 +2281,6 @@ dependencies = [ "windows-targets 0.52.0", ] -[[package]] -name = "windows-sys" -version = "0.45.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" -dependencies = [ - "windows-targets 0.42.2", -] - [[package]] name = "windows-sys" version = "0.48.0" @@ -2253,21 +2299,6 @@ dependencies = [ "windows-targets 0.52.0", ] -[[package]] -name = "windows-targets" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" -dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", -] - [[package]] name = "windows-targets" version = "0.48.5" @@ -2298,12 +2329,6 @@ dependencies = [ "windows_x86_64_msvc 0.52.0", ] -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" - [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -2316,12 +2341,6 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" -[[package]] -name = "windows_aarch64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" - [[package]] name = "windows_aarch64_msvc" version = "0.48.5" @@ -2334,12 +2353,6 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" -[[package]] -name = "windows_i686_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" - [[package]] name = "windows_i686_gnu" version = "0.48.5" @@ -2352,12 +2365,6 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" -[[package]] -name = "windows_i686_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" - [[package]] name = "windows_i686_msvc" version = "0.48.5" @@ -2370,12 +2377,6 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" -[[package]] -name = "windows_x86_64_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" - [[package]] name = "windows_x86_64_gnu" version = "0.48.5" @@ -2388,12 +2389,6 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" - [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" @@ -2406,12 +2401,6 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" -[[package]] -name = "windows_x86_64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" - [[package]] name = "windows_x86_64_msvc" version = "0.48.5" diff --git a/Cargo.toml b/Cargo.toml index b251b72..3c0409e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,6 +43,9 @@ nestify = "0.3.3" gql_client = { git = "https://github.com/adriencaccia/gql-client-rs" } serde_yaml = "0.9.34" sysinfo = { version = "0.30.12", features = ["serde"] } +indicatif = "0.17.8" +atty = "0.2.14" +console = "0.15.8" [dev-dependencies] temp-env = { version = "0.3.6", features = ["async_closure"] } diff --git a/src/api_client.rs b/src/api_client.rs index cbaf8c1..13ec059 100644 --- a/src/api_client.rs +++ b/src/api_client.rs @@ -1,5 +1,8 @@ +use std::fmt::Display; + use crate::prelude::*; use crate::{app::Cli, config::CodSpeedConfig}; +use console::style; use gql_client::{Client as GQLClient, ClientConfig}; use nestify::nest; use serde::{Deserialize, Serialize}; @@ -77,6 +80,22 @@ pub enum ReportConclusion { MissingBaseRun, Success, } + +impl Display for ReportConclusion { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ReportConclusion::AcknowledgedFailure => { + write!(f, "{}", style("Acknowledged Failure").yellow().bold()) + } + ReportConclusion::Failure => write!(f, "{}", style("Failure").red().bold()), + ReportConclusion::MissingBaseRun => { + write!(f, "{}", style("Missing Base Run").yellow().bold()) + } + ReportConclusion::Success => write!(f, "{}", style("Success").green().bold()), + } + } +} + #[derive(Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct FetchLocalRunReportHeadReport { @@ -103,11 +122,19 @@ nest! { #[serde(rename_all = "camelCase")]* struct FetchLocalRunReportData { repository: pub struct FetchLocalRunReportRepository { - pub runs: Vec, + settings: struct FetchLocalRunReportSettings { + allowed_regression: f64, + }, + runs: Vec, } } } +pub struct FetchLocalRunReportResponse { + pub allowed_regression: f64, + pub run: FetchLocalRunReportRun, +} + impl CodSpeedAPIClient { pub async fn create_login_session(&self) -> Result { let response = self @@ -142,7 +169,7 @@ impl CodSpeedAPIClient { pub async fn fetch_local_run_report( &self, vars: FetchLocalRunReportVars, - ) -> Result { + ) -> Result { let response = self .gql_client .query_with_vars_unwrap::( @@ -151,15 +178,22 @@ impl CodSpeedAPIClient { ) .await; match response { - Ok(response) => match response.repository.runs.into_iter().next() { - Some(run) => Ok(run), - None => bail!( - "No runs found for owner: {}, name: {}, run_id: {}", - vars.owner, - vars.name, - vars.run_id - ), - }, + Ok(response) => { + let allowed_regression = response.repository.settings.allowed_regression; + + match response.repository.runs.into_iter().next() { + Some(run) => Ok(FetchLocalRunReportResponse { + allowed_regression, + run, + }), + None => bail!( + "No runs found for owner: {}, name: {}, run_id: {}", + vars.owner, + vars.name, + vars.run_id + ), + } + } Err(err) => bail!("Failed to fetch local run report: {}", err), } } diff --git a/src/auth.rs b/src/auth.rs index bf51727..633fd21 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -3,6 +3,7 @@ use std::time::Duration; use crate::logger::get_local_logger; use crate::{api_client::CodSpeedAPIClient, config::CodSpeedConfig, prelude::*}; use clap::{Args, Subcommand}; +use console::style; use simplelog::CombinedLogger; use tokio::time::{sleep, Instant}; @@ -18,7 +19,6 @@ enum AuthCommands { Login, } -// TODO: tweak the logger to make it more user-friendly fn init_logger() -> Result<()> { let logger = get_local_logger(); CombinedLogger::init(vec![logger])?; @@ -38,14 +38,19 @@ const LOGIN_SESSION_MAX_DURATION: Duration = Duration::from_secs(60 * 5); // 5 m async fn login(api_client: &CodSpeedAPIClient) -> Result<()> { debug!("Login to CodSpeed"); - debug!("Creating login session..."); + start_group!("Creating login session"); let login_session_payload = api_client.create_login_session().await?; + end_group!(); + info!( - "Login session created, open the following URL in your browser: {}", - login_session_payload.callback_url + "Login session created, open the following URL in your browser: {}\n", + style(login_session_payload.callback_url) + .blue() + .bold() + .underlined() ); - info!("Waiting for the login to be completed..."); + start_group!("Waiting for the login to be completed"); let token; let start = Instant::now(); loop { @@ -65,7 +70,7 @@ async fn login(api_client: &CodSpeedAPIClient) -> Result<()> { None => sleep(Duration::from_secs(5)).await, } } - debug!("Login completed"); + end_group!(); let mut config = CodSpeedConfig::load()?; config.auth.token = Some(token); diff --git a/src/logger.rs b/src/logger.rs index bd3639a..c87b9c0 100644 --- a/src/logger.rs +++ b/src/logger.rs @@ -1,6 +1,15 @@ -use std::env; +use std::{ + env, + sync::{Arc, Mutex}, + time::Duration, +}; -use simplelog::{ConfigBuilder, SharedLogger}; +use console::Style; +use indicatif::{ProgressBar, ProgressStyle}; +use lazy_static::lazy_static; +use log::Log; +use simplelog::SharedLogger; +use std::io::Write; /// This target is used exclusively to handle group events. pub const GROUP_TARGET: &str = "codspeed::group"; @@ -76,20 +85,131 @@ pub(super) fn get_group_event(record: &log::Record) -> Option { } } +lazy_static! { + pub static ref SPINNER: Arc>> = Arc::new(Mutex::new(None)); + pub static ref IS_TTY: bool = atty::is(atty::Stream::Stdout); +} + +/// Hide the progress bar temporarily, execute `f`, then redraw the progress bar. +/// +/// If the output is not a TTY, `f` will be executed without hiding the progress bar. +pub fn suspend_progress_bar R, R>(f: F) -> R { + // If the output is a TTY, and there is a spinner, suspend it + if *IS_TTY { + if let Ok(mut spinner) = SPINNER.lock() { + if let Some(spinner) = spinner.as_mut() { + return spinner.suspend(f); + } + } + } + + // Otherwise, just run the function + f() +} + +pub struct LocalLogger { + log_level: log::LevelFilter, +} + +impl LocalLogger { + pub fn new() -> Self { + let log_level = env::var("CODSPEED_LOG") + .ok() + .and_then(|log_level| log_level.parse::().ok()) + .unwrap_or(log::LevelFilter::Info); + + LocalLogger { log_level } + } +} + +impl Log for LocalLogger { + fn enabled(&self, metadata: &log::Metadata) -> bool { + metadata.level() <= self.log_level + } + + fn log(&self, record: &log::Record) { + if !self.enabled(record.metadata()) { + return; + } + + if let Some(group_event) = get_group_event(record) { + match group_event { + GroupEvent::Start(name) | GroupEvent::StartOpened(name) => { + if *IS_TTY { + let spinner = ProgressBar::new_spinner(); + spinner.set_style( + ProgressStyle::with_template( + " {spinner:>.cyan} {wide_msg:.cyan.bold}", + ) + .unwrap(), + ); + spinner.set_message(format!("{}...", name)); + spinner.enable_steady_tick(Duration::from_millis(100)); + SPINNER.lock().unwrap().replace(spinner); + } else { + println!("{}...", name); + } + } + GroupEvent::End => { + if *IS_TTY { + let mut spinner = SPINNER.lock().unwrap(); + if let Some(spinner) = spinner.as_mut() { + spinner.finish_and_clear(); + // Separate groups with a newline + println!(); + } + } + } + } + + return; + } + + suspend_progress_bar(|| print_record(record)); + } + + fn flush(&self) { + std::io::stdout().flush().unwrap(); + } +} + +/// Print a log record to the console with the appropriate style +fn print_record(record: &log::Record) { + let error_style = Style::new().red(); + let info_style = Style::new().white(); + let warn_style = Style::new().yellow(); + let debug_style = Style::new().blue().dim(); + let trace_style = Style::new().black().dim(); + + match record.level() { + log::Level::Error => eprintln!("{}", error_style.apply_to(record.args())), + log::Level::Warn => eprintln!("{}", warn_style.apply_to(record.args())), + log::Level::Info => println!("{}", info_style.apply_to(record.args())), + log::Level::Debug => println!( + "{}", + debug_style.apply_to(format!("[DEBUG::{}] {}", record.target(), record.args())), + ), + log::Level::Trace => println!( + "{}", + trace_style.apply_to(format!("[TRACE::{}] {}", record.target(), record.args())) + ), + } +} + +impl SharedLogger for LocalLogger { + fn level(&self) -> log::LevelFilter { + self.log_level + } + + fn config(&self) -> Option<&simplelog::Config> { + None + } + + fn as_log(self: Box) -> Box { + Box::new(*self) + } +} + pub fn get_local_logger() -> Box { - let log_level = env::var("CODSPEED_LOG") - .ok() - .and_then(|log_level| log_level.parse::().ok()) - .unwrap_or(log::LevelFilter::Info); - - let config = ConfigBuilder::new() - .set_time_level(log::LevelFilter::Debug) - .build(); - - simplelog::TermLogger::new( - log_level, - config, - simplelog::TerminalMode::Mixed, - simplelog::ColorChoice::Auto, - ) + Box::new(LocalLogger::new()) } diff --git a/src/queries/FetchLocalRunReport.gql b/src/queries/FetchLocalRunReport.gql index 9f0959a..e0b1f63 100644 --- a/src/queries/FetchLocalRunReport.gql +++ b/src/queries/FetchLocalRunReport.gql @@ -1,5 +1,8 @@ query FetchLocalRunReport($owner: String!, $name: String!, $runId: String!) { repository(owner: $owner, name: $name) { + settings { + allowedRegression + } runs(where: { id: { equals: $runId } }) { id status diff --git a/src/run/poll_results.rs b/src/run/poll_results.rs index 1ee72ba..d09dbe7 100644 --- a/src/run/poll_results.rs +++ b/src/run/poll_results.rs @@ -1,10 +1,11 @@ use std::time::Duration; +use console::style; use tokio::time::{sleep, Instant}; use url::Url; use crate::api_client::{ - CodSpeedAPIClient, FetchLocalRunReportRun, FetchLocalRunReportVars, RunStatus, + CodSpeedAPIClient, FetchLocalRunReportResponse, FetchLocalRunReportVars, RunStatus, }; use crate::prelude::*; @@ -12,6 +13,7 @@ use super::ci_provider::CIProvider; use super::config::Config; const RUN_PROCESSING_MAX_DURATION: Duration = Duration::from_secs(60 * 5); // 5 minutes +const POLLING_INTERVAL: Duration = Duration::from_secs(1); #[allow(clippy::borrowed_box)] pub async fn poll_results( @@ -30,8 +32,7 @@ pub async fn poll_results( run_id: run_id.clone(), }; - let run; - info!("Polling results..."); + let response; loop { if start.elapsed() > RUN_PROCESSING_MAX_DURATION { bail!("Polling results timed out"); @@ -41,17 +42,18 @@ pub async fn poll_results( .fetch_local_run_report(fetch_local_run_report_vars.clone()) .await? { - FetchLocalRunReportRun { status, .. } if status != RunStatus::Completed => { - sleep(Duration::from_secs(5)).await; + FetchLocalRunReportResponse { run, .. } if run.status != RunStatus::Completed => { + sleep(POLLING_INTERVAL).await; } - run_from_api => { - run = run_from_api; + response_from_api => { + response = response_from_api; break; } } } - let report = run + let report = response + .run .head_reports .into_iter() .next() @@ -59,14 +61,28 @@ pub async fn poll_results( info!("Report completed, here are the results:"); if let Some(impact) = report.impact { - info!("Impact: {}%", (impact * 100.0).round()); + let rounded_impact = (impact * 100.0).round(); + let impact_text = if impact > 0.0 { + style(format!("+{}%", rounded_impact)).green().bold() + } else { + style(format!("{}%", rounded_impact)).red().bold() + }; + + info!( + "Impact: {} (allowed regression: -{}%)", + impact_text, + (response.allowed_regression * 100.0).round() + ); } - info!("Conclusion: {:?}", report.conclusion); + info!("Conclusion: {}", report.conclusion); let mut report_url = Url::parse(config.frontend_url.as_str())?; - report_url.set_path(format!("{}/{}/runs/{}", owner, name, run.id).as_str()); + report_url.set_path(format!("{}/{}/runs/{}", owner, name, response.run.id).as_str()); - info!("\nTo see the full report, visit: {}", report_url); + info!( + "\nTo see the full report, visit: {}", + style(report_url).blue().bold().underlined() + ); Ok(()) } diff --git a/src/run/runner/valgrind.rs b/src/run/runner/valgrind.rs index 962fd83..e7f8909 100644 --- a/src/run/runner/valgrind.rs +++ b/src/run/runner/valgrind.rs @@ -1,3 +1,4 @@ +use crate::logger::suspend_progress_bar; use crate::prelude::*; use crate::run::{ config::Config, instruments::mongo_tracer::MongoTracer, @@ -72,7 +73,9 @@ fn run_command_with_log_pipe(mut cmd: Command) -> Result { if bytes_read == 0 { break; } - writer.write_all(&buffer[..bytes_read])?; + suspend_progress_bar(|| { + writer.write_all(&buffer[..bytes_read]).unwrap(); + }); trace!(target: VALGRIND_EXECUTION_TARGET, "{}{}", prefix, String::from_utf8_lossy(&buffer[..bytes_read])); } Ok(())