From 7874195bbbe17badaa0a380df3fc94959b87cffd Mon Sep 17 00:00:00 2001 From: Matthias Endler Date: Fri, 24 Feb 2023 23:53:09 +0100 Subject: [PATCH] Customize verbosity (#956) --- fixtures/configs/smoketest.toml | 2 +- lychee-bin/src/commands/check.rs | 33 +++- lychee-bin/src/commands/dump.rs | 15 +- lychee-bin/src/main.rs | 2 +- lychee-bin/src/options.rs | 8 +- lychee-bin/src/verbosity.rs | 252 ++++++++++++++----------------- lychee-lib/src/types/base.rs | 10 +- 7 files changed, 159 insertions(+), 163 deletions(-) diff --git a/fixtures/configs/smoketest.toml b/fixtures/configs/smoketest.toml index 7eb0d93..696ed2a 100644 --- a/fixtures/configs/smoketest.toml +++ b/fixtures/configs/smoketest.toml @@ -1,7 +1,7 @@ ############################# Display ############################# # Verbose program output -verbose = true +verbose = "warn" # Don't show interactive progress bar while checking links. no_progress = true diff --git a/lychee-bin/src/commands/check.rs b/lychee-bin/src/commands/check.rs index c9b5c02..ce97a62 100644 --- a/lychee-bin/src/commands/check.rs +++ b/lychee-bin/src/commands/check.rs @@ -12,7 +12,7 @@ use tokio_stream::wrappers::ReceiverStream; use tokio_stream::StreamExt; use crate::formatters::response::ResponseFormatter; -use crate::verbosity::{Verbosity, WarnLevel}; +use crate::verbosity::Verbosity; use crate::{cache::Cache, stats::ResponseStats, ExitCode}; use lychee_lib::{Client, Request, Response}; @@ -28,7 +28,7 @@ where let (send_req, recv_req) = mpsc::channel(params.cfg.max_concurrency); let (send_resp, recv_resp) = mpsc::channel(params.cfg.max_concurrency); let max_concurrency = params.cfg.max_concurrency; - let stats = if params.cfg.verbose.is_verbose() { + let stats = if params.cfg.verbose.log_level() >= log::Level::Info { ResponseStats::extended() } else { ResponseStats::default() @@ -113,7 +113,7 @@ where /// Reads from the request channel and updates the progress bar status async fn progress_bar_task( mut recv_resp: mpsc::Receiver, - verbose: Verbosity, + verbose: Verbosity, pb: Option, formatter: Arc>, mut stats: ResponseStats, @@ -212,16 +212,16 @@ fn show_progress( progress_bar: &Option, response: &Response, formatter: &Arc>, - verbose: &Verbosity, + verbose: &Verbosity, ) -> Result<()> { let out = formatter.write_response(response)?; if let Some(pb) = progress_bar { pb.inc(1); pb.set_message(out.clone()); - if verbose.is_verbose() { + if verbose.log_level() >= log::Level::Info { pb.println(out); } - } else if verbose.is_verbose() + } else if verbose.log_level() >= log::Level::Info || (!response.status().is_success() && !response.status().is_excluded()) { writeln!(output, "{out}")?; @@ -255,11 +255,30 @@ mod tests { &None, &response, &formatter, - &Verbosity::new(0, 0), + &Verbosity::default(), ) .unwrap(); info!("{:?}", String::from_utf8_lossy(&buf)); assert!(buf.is_empty()); } + + #[test] + fn test_show_cached_responses_in_progress_debug_output() { + let mut buf = Vec::new(); + let response = Response( + InputSource::Stdin, + ResponseBody { + uri: Uri::try_from("http://127.0.0.1").unwrap(), + status: Status::Cached(CacheStatus::Ok(200)), + }, + ); + let formatter: Arc> = + Arc::new(Box::new(formatters::response::Raw::new())); + show_progress(&mut buf, &None, &response, &formatter, &Verbosity::debug()).unwrap(); + + assert!(!buf.is_empty()); + let buf = String::from_utf8_lossy(&buf); + assert_eq!(buf, "↻ [200] http://127.0.0.1/ | Cached: OK (cached)\n"); + } } diff --git a/lychee-bin/src/commands/dump.rs b/lychee-bin/src/commands/dump.rs index 9137106..818a9cf 100644 --- a/lychee-bin/src/commands/dump.rs +++ b/lychee-bin/src/commands/dump.rs @@ -7,7 +7,6 @@ use std::path::PathBuf; use tokio_stream::StreamExt; use crate::verbosity::Verbosity; -use crate::verbosity::WarnLevel; use crate::ExitCode; use super::CommandParams; @@ -57,7 +56,7 @@ where let excluded = params.client.is_excluded(&request.uri); - if excluded && !params.cfg.verbose.is_verbose() { + if excluded && params.cfg.verbose.log_level() < log::Level::Info { continue; } if let Err(e) = write(&mut writer, &request, ¶ms.cfg.verbose, excluded) { @@ -75,17 +74,17 @@ where fn write( writer: &mut Box, request: &Request, - verbosity: &Verbosity, + verbosity: &Verbosity, excluded: bool, ) -> io::Result<()> { - // Only print `data:` URIs if verbose mode is enabled - if request.uri.is_data() && !verbosity.is_verbose() { + // Only print `data:` URIs if verbose mode is at least `info`. + if request.uri.is_data() && verbosity.log_level() < log::Level::Info { return Ok(()); } - let request = if verbosity.is_verbose() { - // Only print source in verbose mode. This way the normal link output - // can be fed into another tool without data mangling. + // Only print source if verbose mode is at least `info`. This way the normal + // link output can be fed into another tool without data mangling. + let request = if verbosity.log_level() >= log::Level::Info { request.to_string() } else { request.uri.to_string() diff --git a/lychee-bin/src/main.rs b/lychee-bin/src/main.rs index 9a6f9f4..5b81ed7 100644 --- a/lychee-bin/src/main.rs +++ b/lychee-bin/src/main.rs @@ -303,7 +303,7 @@ async fn run(opts: &LycheeOptions) -> Result { if let Some(output) = &opts.config.output { fs::write(output, formatted).context("Cannot write status output to file")?; } else { - if opts.config.verbose.log_level() == Some(log::Level::Debug) && !is_empty { + if opts.config.verbose.log_level() >= log::Level::Info && !is_empty { // separate summary from the verbose list of links above // with a newline writeln!(io::stdout())?; diff --git a/lychee-bin/src/options.rs b/lychee-bin/src/options.rs index d662a59..33411be 100644 --- a/lychee-bin/src/options.rs +++ b/lychee-bin/src/options.rs @@ -1,5 +1,5 @@ use crate::parse::{parse_base, parse_statuscodes}; -use crate::verbosity::{Verbosity, WarnLevel}; +use crate::verbosity::Verbosity; use anyhow::{anyhow, Context, Error, Result}; use clap::{arg, Parser}; use const_format::{concatcp, formatcp}; @@ -83,7 +83,7 @@ default_function! { timeout: usize = DEFAULT_TIMEOUT_SECS; retry_wait_time: usize = DEFAULT_RETRY_WAIT_TIME_SECS; method: String = DEFAULT_METHOD.to_string(); - verbosity: Verbosity = Verbosity::new(1, 0); + verbosity: Verbosity = Verbosity::default(); } // Macro for merging configuration values @@ -144,7 +144,7 @@ pub(crate) struct Config { /// Verbose program output #[clap(flatten)] #[serde(default = "verbosity")] - pub(crate) verbose: Verbosity, + pub(crate) verbose: Verbosity, /// Do not show progress bar. /// This is recommended for non-interactive shells (e.g. for continuous integration) @@ -365,7 +365,7 @@ impl Config { self, toml; // Keys with defaults to assign - verbose: Verbosity::new(1, 0); + verbose: Verbosity::default(); cache: false; no_progress: false; max_redirects: DEFAULT_MAX_REDIRECTS; diff --git a/lychee-bin/src/verbosity.rs b/lychee-bin/src/verbosity.rs index c7a6314..eac67b1 100644 --- a/lychee-bin/src/verbosity.rs +++ b/lychee-bin/src/verbosity.rs @@ -1,202 +1,165 @@ -//! Control `log` level with a `--verbose` flag for your CLI -//! -//! The original source is from -//! [this crate](https://github.com/clap-rs/clap-verbosity-flag). -//! Modifications were made to add support for serializing the `Verbosity` -//! struct and to add a convenience method to get the verbosity status. +//! Easily add a `--verbose` flag to CLIs using Structopt //! //! # Examples //! -//! To get `--quiet` and `--verbose` flags through your entire program, just `flatten` -//! [`Verbosity`]: //! ```rust,no_run -//! # use clap::Parser; -//! # use clap_verbosity_flag::Verbosity; -//! # -//! # /// Le CLI -//! # #[derive(Debug, Parser)] -//! # struct Cli { -//! #[command(flatten)] -//! verbose: Verbosity, -//! # } -//! ``` +//! use clap::Parser; +//! use clap_verbosity_flag::Verbosity; +//! +//! /// Le CLI +//! #[derive(Debug, Parser)] +//! struct Cli { +//! #[command(flatten)] +//! verbose: Verbosity, +//! } //! -//! You can then use this to configure your logger: -//! ```rust,no_run -//! # use clap::Parser; -//! # use clap_verbosity_flag::Verbosity; -//! # -//! # /// Le CLI -//! # #[derive(Debug, Parser)] -//! # struct Cli { -//! # #[command(flatten)] -//! # verbose: Verbosity, -//! # } //! let cli = Cli::parse(); //! env_logger::Builder::new() //! .filter_level(cli.verbose.log_level_filter()) //! .init(); //! ``` //! -//! By default, this will only report errors. +//! This will only report errors. //! - `-q` silences output //! - `-v` show warnings //! - `-vv` show info //! - `-vvv` show debug //! - `-vvvv` show trace -//! -//! You can also customize the default logging level: -//! ```rust,no_run -//! # use clap::Parser; -//! use clap_verbosity_flag::{Verbosity, InfoLevel}; -//! -//! /// Le CLI -//! #[derive(Debug, Parser)] -//! struct Cli { -//! #[command(flatten)] -//! verbose: Verbosity, -//! } -//! ``` -//! -//! Or implement [`LogLevel`] yourself for more control. -#[derive(clap::Args, Eq, PartialEq, Debug, Deserialize, Clone)] -pub(crate) struct Verbosity { - #[clap( +use log::Level; +use log::LevelFilter; +use serde::Deserialize; + +#[derive(clap::Args, Debug, Default, Clone, PartialEq, Eq)] +pub(crate) struct Verbosity { + /// Pass many times for more log output + /// + /// By default, it'll only report errors and warnings. + /// Passing `-v` one time also prints info, + /// `-vv` enables debugging logging, `-vvv` trace. + #[arg( long, short = 'v', action = clap::ArgAction::Count, global = true, - help = L::verbose_help(), - long_help = L::verbose_long_help(), + help = Self::verbose_help(), + long_help = Self::verbose_long_help(), + conflicts_with = "quiet", )] verbose: u8, - #[clap( + #[arg( long, short = 'q', action = clap::ArgAction::Count, global = true, - help = L::quiet_help(), - long_help = L::quiet_long_help(), + help = Self::quiet_help(), + long_help = Self::quiet_long_help(), conflicts_with = "verbose", )] quiet: u8, - - #[clap(skip)] - phantom: std::marker::PhantomData, } -impl Verbosity { - /// Create a new verbosity instance by explicitly setting the values - pub(crate) const fn new(verbose: u8, quiet: u8) -> Self { - Verbosity { - verbose, - quiet, - phantom: std::marker::PhantomData, - } - } - +impl Verbosity { /// Get the log level. /// /// `None` means all output is disabled. - pub(crate) fn log_level(&self) -> Option { + pub(crate) const fn log_level(&self) -> Level { level_enum(self.verbosity()) } /// Get the log level filter. - pub(crate) fn log_level_filter(&self) -> log::LevelFilter { - level_enum(self.verbosity()).map_or(log::LevelFilter::Off, |l| l.to_level_filter()) + pub(crate) fn log_level_filter(&self) -> LevelFilter { + level_enum(self.verbosity()).to_level_filter() } - /// Shorthand to check if the user requested "more verbose" output - /// (assuming the default log level is `Warn`) - pub(crate) fn is_verbose(&self) -> bool { - self.log_level() >= log::LevelFilter::Info.to_level() + #[allow(clippy::cast_possible_wrap)] + const fn verbosity(&self) -> i8 { + level_value(log::Level::Warn) - (self.quiet as i8) + (self.verbose as i8) } - fn verbosity(&self) -> i8 { - #![allow(clippy::cast_possible_wrap)] - level_value(L::default()) - (self.quiet as i8) + (self.verbose as i8) + const fn verbose_help() -> &'static str { + "Set verbosity level; more output per occurrence (e.g. `-v` or `-vv`)" + } + + const fn verbose_long_help() -> Option<&'static str> { + None + } + + const fn quiet_help() -> &'static str { + "Less output per occurrence (e.g. `-q` or `-qq`)" + } + + const fn quiet_long_help() -> Option<&'static str> { + None } } -const fn level_value(level: Option) -> i8 { +#[cfg(test)] +impl Verbosity { + pub(crate) const fn debug() -> Self { + Self { + #[allow(clippy::cast_sign_loss)] + verbose: level_value(log::Level::Debug) as u8, + quiet: 0, + } + } +} + +// Implement Deserialize for `Verbosity` +// This can be deserialized from a string like "warn", "warning", or "Warning" +// for example +impl<'de> Deserialize<'de> for Verbosity { + #[allow(clippy::cast_sign_loss)] + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + let level = match s.to_lowercase().as_str() { + "error" => Level::Error, + "warn" | "warning" => Level::Warn, + "info" => Level::Info, + "debug" => Level::Debug, + "trace" => Level::Trace, + level => { + return Err(serde::de::Error::custom(format!( + "invalid log level `{level}`" + ))) + } + }; + Ok(Verbosity { + verbose: level_value(level) as u8, + quiet: 0, + }) + } +} + +const fn level_value(level: Level) -> i8 { match level { - None => -1, - Some(log::Level::Error) => 0, - Some(log::Level::Warn) => 1, - Some(log::Level::Info) => 2, - Some(log::Level::Debug) => 3, - Some(log::Level::Trace) => 4, + log::Level::Error => 0, + log::Level::Warn => 1, + log::Level::Info => 2, + log::Level::Debug => 3, + log::Level::Trace => 4, } } -const fn level_enum(verbosity: i8) -> Option { +const fn level_enum(verbosity: i8) -> log::Level { match verbosity { - std::i8::MIN..=-1 => None, - 0 => Some(log::Level::Error), - 1 => Some(log::Level::Warn), - 2 => Some(log::Level::Info), - 3 => Some(log::Level::Debug), - 4..=std::i8::MAX => Some(log::Level::Trace), + 0 => log::Level::Error, + 1 => log::Level::Warn, + 2 => log::Level::Info, + 3 => log::Level::Debug, + _ => log::Level::Trace, } } use std::fmt; -use serde::Deserialize; - -impl fmt::Display for Verbosity { +impl fmt::Display for Verbosity { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.verbosity()) - } -} - -pub(crate) trait LogLevel { - fn default() -> Option; - - fn verbose_help() -> Option<&'static str> { - Some("Set verbosity level; more output per occurrence (e.g. `-v` or `-vv`)") - } - - fn verbose_long_help() -> Option<&'static str> { - None - } - - fn quiet_help() -> Option<&'static str> { - Some("Less output per occurrence (e.g. `-q` or `-qq`)") - } - - fn quiet_long_help() -> Option<&'static str> { - None - } -} - -#[derive(Copy, Clone, Debug, Default, PartialEq)] -pub(crate) struct ErrorLevel; - -impl LogLevel for ErrorLevel { - fn default() -> Option { - Some(log::Level::Error) - } -} - -#[derive(Copy, Clone, Debug, Default, PartialEq)] -pub(crate) struct WarnLevel; - -impl LogLevel for WarnLevel { - fn default() -> Option { - Some(log::Level::Warn) - } -} - -#[derive(Copy, Clone, Debug, Default, PartialEq)] -pub(crate) struct InfoLevel; - -impl LogLevel for InfoLevel { - fn default() -> Option { - Some(log::Level::Info) + write!(f, "{}", self.verbose) } } @@ -215,4 +178,11 @@ mod test { use clap::CommandFactory; Cli::command().debug_assert(); } + + #[test] + fn test_default_log_level() { + let verbosity = Verbosity::default(); + assert_eq!(verbosity.log_level(), Level::Warn); + assert!(verbosity.log_level() >= Level::Warn); + } } diff --git a/lychee-lib/src/types/base.rs b/lychee-lib/src/types/base.rs index 8eb978e..73a4097 100644 --- a/lychee-lib/src/types/base.rs +++ b/lychee-lib/src/types/base.rs @@ -9,7 +9,7 @@ use crate::{ErrorKind, InputSource}; /// Both, local and remote targets are supported. #[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Clone)] #[allow(variant_size_differences)] -#[serde(try_from = "&str")] +#[serde(try_from = "String")] pub enum Base { /// Local file path pointing to root directory Local(PathBuf), @@ -72,6 +72,14 @@ impl TryFrom<&str> for Base { } } +impl TryFrom for Base { + type Error = ErrorKind; + + fn try_from(value: String) -> Result { + Self::try_from(value.as_str()) + } +} + #[cfg(test)] mod test_base { use crate::Result;