diff --git a/Cargo.lock b/Cargo.lock index a51736fb..7180c73f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -246,7 +246,6 @@ dependencies = [ "indicatif", "interim", "itertools", - "log", "norm", "open", "ratatui", @@ -351,7 +350,6 @@ dependencies = [ "indicatif", "interim", "itertools", - "log", "memchr", "minspan", "notify", @@ -375,11 +373,11 @@ dependencies = [ "strum", "strum_macros", "tempfile", - "testing_logger", "thiserror 2.0.18", "time", "tiny-bip39", "tokio", + "tracing", "typed-builder 0.18.2", "urlencoding", "uuid", @@ -403,6 +401,8 @@ dependencies = [ "sysinfo", "thiserror 2.0.18", "time", + "tracing", + "tracing-subscriber", "typed-builder 0.18.2", "uuid", ] @@ -5156,15 +5156,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "testing_logger" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d92b727cb45d33ae956f7f46b966b25f1bc712092aeef9dba5ac798fc89f720" -dependencies = [ - "log", -] - [[package]] name = "tests-database" version = "18.17.0" diff --git a/Cargo.toml b/Cargo.toml index ea025b15..1f641d98 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,7 +34,6 @@ atuin-nucleo = { path = "crates/atuin-nucleo", version = "0.6.0" } atuin-nucleo-matcher = { path = "crates/atuin-nucleo/matcher", version = "0.3.1" } base64 = "0.22" crossterm = "0.29.0" -log = "0.4" time = { version = "0.3.47", features = [ "serde-human-readable", "macros", diff --git a/crates/atuin-client/Cargo.toml b/crates/atuin-client/Cargo.toml index e0bbe3ef..025d94ac 100644 --- a/crates/atuin-client/Cargo.toml +++ b/crates/atuin-client/Cargo.toml @@ -24,7 +24,7 @@ check-update = [] atuin-common = { path = "../atuin-common", version = "18.17.0" } derive_more = { workspace = true } -log = { workspace = true } +tracing = { workspace = true } base64 = { workspace = true } time = { workspace = true, features = ["macros", "formatting", "parsing"] } clap = { workspace = true } @@ -83,10 +83,15 @@ strum = { version = "0.27", features = ["strum_macros"] } [dev-dependencies] tokio = { version = "1", features = ["full"] } pretty_assertions = { workspace = true } -testing_logger = "0.1.1" divan = "0.1.14" tempfile = "3" +# This crate's tests require the `test-utils` feature in atuin-common. +[dev-dependencies.atuin-common] +path = "../atuin-common" +version = "18.17.0" +features = ["test-utils"] + [[bench]] name = "benchmarks" harness = false diff --git a/crates/atuin-client/src/hub.rs b/crates/atuin-client/src/hub.rs index 2e40aad4..babef35e 100644 --- a/crates/atuin-client/src/hub.rs +++ b/crates/atuin-client/src/hub.rs @@ -95,7 +95,7 @@ impl HubAuthSession { } Err(e) => { // Transient errors shouldn't fail the whole flow - log::debug!("Verification poll failed: {}", e); + tracing::debug!("Verification poll failed: {}", e); Ok(HubAuthStatus::Pending) } } diff --git a/crates/atuin-client/src/lib.rs b/crates/atuin-client/src/lib.rs index 7938176a..e0430999 100644 --- a/crates/atuin-client/src/lib.rs +++ b/crates/atuin-client/src/lib.rs @@ -1,7 +1,7 @@ #![deny(unsafe_code)] #[macro_use] -extern crate log; +extern crate tracing; #[cfg(feature = "sync")] pub mod api_client; diff --git a/crates/atuin-client/src/settings.rs b/crates/atuin-client/src/settings.rs index d80375a3..0d0f21ca 100644 --- a/crates/atuin-client/src/settings.rs +++ b/crates/atuin-client/src/settings.rs @@ -1697,7 +1697,7 @@ impl Settings { .filter_map(|(key, value)| match Self::expand_path(value) { Ok(expanded) => Some((key, expanded)), Err(e) => { - log::warn!("failed to expand path for {key}: {e}"); + tracing::warn!("failed to expand path for {key}: {e}"); None } }) diff --git a/crates/atuin-client/src/settings/watcher.rs b/crates/atuin-client/src/settings/watcher.rs index 740b8d12..782527cf 100644 --- a/crates/atuin-client/src/settings/watcher.rs +++ b/crates/atuin-client/src/settings/watcher.rs @@ -28,12 +28,12 @@ use std::{ }; use eyre::{Result, WrapErr}; -use log::{debug, error, info, warn}; use notify::{ Config as NotifyConfig, RecommendedWatcher, RecursiveMode, Watcher, event::{EventKind, ModifyKind}, }; use tokio::sync::watch; +use tracing::{debug, error, info, warn}; use super::Settings; diff --git a/crates/atuin-client/src/theme.rs b/crates/atuin-client/src/theme.rs index a277ac13..5a63090b 100644 --- a/crates/atuin-client/src/theme.rs +++ b/crates/atuin-client/src/theme.rs @@ -1,5 +1,4 @@ use config::{Config, File as ConfigFile, FileFormat}; -use log; use palette::named; use serde::{Deserialize, Serialize}; use serde_json; @@ -71,20 +70,20 @@ impl Theme { } pub fn get_info(&self) -> ContentStyle { - self.get_alert(log::Level::Info) + self.get_alert(tracing::Level::INFO) } pub fn get_warning(&self) -> ContentStyle { - self.get_alert(log::Level::Warn) + self.get_alert(tracing::Level::WARN) } pub fn get_error(&self) -> ContentStyle { - self.get_alert(log::Level::Error) + self.get_alert(tracing::Level::ERROR) } // The alert meanings may be chosen by the Level enum, rather than the methods above // or the full Meaning enum, to simplify programmatic selection of a log-level. - pub fn get_alert(&self, severity: log::Level) -> ContentStyle { + pub fn get_alert(&self, severity: tracing::Level) -> ContentStyle { self.styles[ALERT_TYPES.get(&severity).unwrap()] } @@ -133,7 +132,7 @@ impl Theme { *name, StyleFactory::from_fg_string(color).unwrap_or_else(|err| { if debug { - log::warn!("Tried to load string as a color unsuccessfully: ({name}={color}) {err}"); + tracing::warn!("Tried to load string as a color unsuccessfully: ({name}={color}) {err}"); } ContentStyle::default() }), @@ -241,11 +240,11 @@ impl StyleFactory { // Built-in themes. Rather than having extra files added before any theming // is available, this gives a couple of basic options, demonstrating the use // of themes: autumn and marine -static ALERT_TYPES: LazyLock> = LazyLock::new(|| { +static ALERT_TYPES: LazyLock> = LazyLock::new(|| { HashMap::from([ - (log::Level::Info, Meaning::AlertInfo), - (log::Level::Warn, Meaning::AlertWarn), - (log::Level::Error, Meaning::AlertError), + (tracing::Level::INFO, Meaning::AlertInfo), + (tracing::Level::WARN, Meaning::AlertWarn), + (tracing::Level::ERROR, Meaning::AlertError), ]) }); @@ -459,7 +458,7 @@ impl ThemeManager { }; if debug && name != theme_config.theme.name { - log::warn!( + tracing::warn!( "Your theme config name is not the name of your loaded theme {} != {}", name, theme_config.theme.name @@ -485,7 +484,7 @@ impl ThemeManager { None => match self.load_theme_from_file(name, max_depth.unwrap_or(DEFAULT_MAX_DEPTH)) { Ok(theme) => theme, Err(err) => { - log::warn!("Could not load theme {name}: {err}"); + tracing::warn!("Could not load theme {name}: {err}"); built_ins.get("(none)").unwrap() } }, @@ -496,6 +495,7 @@ impl ThemeManager { #[cfg(test)] mod theme_tests { use super::*; + use atuin_common::test_utils::capture_logs; #[test] fn test_can_load_builtin_theme() { @@ -620,14 +620,17 @@ mod theme_tests { assert_eq!(theme.get_info().foreground_color.unwrap(), Color::DarkGreen); assert_eq!(theme.get_base().foreground_color, None); assert_eq!( - theme.get_alert(log::Level::Error).foreground_color.unwrap(), + theme + .get_alert(tracing::Level::ERROR) + .foreground_color + .unwrap(), Color::DarkRed ) } #[test] fn test_can_use_parent_theme_for_fallbacks() { - testing_logger::setup(); + let logs = capture_logs(); let mut manager = ThemeManager::new(Some(false), Some("".to_string())); @@ -692,7 +695,7 @@ mod theme_tests { from_string("white").ok() ); - testing_logger::validate(|captured_logs| assert_eq!(captured_logs.len(), 0)); + assert_eq!(logs.get().len(), 0); // If the parent is not found, we end up with the no theme colors or styling // as this is considered a (soft) error state. @@ -721,20 +724,20 @@ mod theme_tests { None ); - testing_logger::validate(|captured_logs| { - assert_eq!(captured_logs.len(), 1); - assert_eq!( - captured_logs[0].body, - "Could not load theme nonsolarized: Empty theme directory override and could not find theme elsewhere" - ); - assert_eq!(captured_logs[0].level, log::Level::Warn) - }); + let captured_logs = logs.get(); + assert_eq!(captured_logs.len(), 1); + assert_eq!( + captured_logs[0].message, + "Could not load theme nonsolarized: Empty theme directory override and could not find theme elsewhere" + ); + assert_eq!(captured_logs[0].level, tracing::Level::WARN) } #[test] fn test_can_debug_theme() { - testing_logger::setup(); [true, false].iter().for_each(|debug| { + let logs = capture_logs(); + let mut manager = ThemeManager::new(Some(*debug), Some("".to_string())); let config = Config::builder() .add_source(ConfigFile::from_str( @@ -753,23 +756,22 @@ mod theme_tests { manager .load_theme_from_config("config_theme", config, 1) .unwrap(); - testing_logger::validate(|captured_logs| { - if *debug { - assert_eq!(captured_logs.len(), 2); - assert_eq!( - captured_logs[0].body, - "Your theme config name is not the name of your loaded theme config_theme != mytheme" - ); - assert_eq!(captured_logs[0].level, log::Level::Warn); - assert_eq!( - captured_logs[1].body, - "Tried to load string as a color unsuccessfully: (AlertInfo=xinetic) No such color in palette" - ); - assert_eq!(captured_logs[1].level, log::Level::Warn) - } else { - assert_eq!(captured_logs.len(), 0) - } - }) + let captured_logs = logs.get(); + if *debug { + assert_eq!(captured_logs.len(), 2); + assert_eq!( + captured_logs[0].message, + "Your theme config name is not the name of your loaded theme config_theme != mytheme" + ); + assert_eq!(captured_logs[0].level, tracing::Level::WARN); + assert_eq!( + captured_logs[1].message, + "Tried to load string as a color unsuccessfully: (AlertInfo=xinetic) No such color in palette" + ); + assert_eq!(captured_logs[1].level, tracing::Level::WARN) + } else { + assert_eq!(captured_logs.len(), 0) + } }) } diff --git a/crates/atuin-common/Cargo.toml b/crates/atuin-common/Cargo.toml index 3c005689..113cd799 100644 --- a/crates/atuin-common/Cargo.toml +++ b/crates/atuin-common/Cargo.toml @@ -12,6 +12,10 @@ repository = { workspace = true } # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[features] +# Enables the `test_utils` module. +test-utils = ["dep:tracing", "dep:tracing-subscriber"] + [dependencies] derive_more = { workspace = true } time = { workspace = true } @@ -27,6 +31,8 @@ sysinfo = "0.30.7" base64 = { workspace = true } getrandom = "0.2" rustls = { workspace = true } +tracing = { workspace = true, optional = true } +tracing-subscriber = { workspace = true, optional = true } [dev-dependencies] pretty_assertions = { workspace = true } diff --git a/crates/atuin-common/src/lib.rs b/crates/atuin-common/src/lib.rs index 1ed334dd..607353d8 100644 --- a/crates/atuin-common/src/lib.rs +++ b/crates/atuin-common/src/lib.rs @@ -60,5 +60,7 @@ macro_rules! new_uuid { pub mod api; pub mod record; pub mod shell; +#[cfg(feature = "test-utils")] +pub mod test_utils; pub mod tls; pub mod utils; diff --git a/crates/atuin-common/src/test_utils.rs b/crates/atuin-common/src/test_utils.rs new file mode 100644 index 00000000..79d4c4e5 --- /dev/null +++ b/crates/atuin-common/src/test_utils.rs @@ -0,0 +1,3 @@ +//! Utilities for use in tests. +pub mod capture; +pub use capture::capture_logs; diff --git a/crates/atuin-common/src/test_utils/capture.rs b/crates/atuin-common/src/test_utils/capture.rs new file mode 100644 index 00000000..b574b730 --- /dev/null +++ b/crates/atuin-common/src/test_utils/capture.rs @@ -0,0 +1,68 @@ +//! Utilities for capturing logs emitted by [`tracing`]. +use std::sync::{Arc, Mutex, MutexGuard}; + +use tracing::field::{Field, Visit}; +use tracing::subscriber::DefaultGuard; +use tracing::{Event, Level, Subscriber}; +use tracing_subscriber::layer::{Context, Layer, SubscriberExt}; + +/// Start capturing logs from [`tracing`]. +/// +/// Logs will continue to be captured until the returned [`CapturedLogs`] object is dropped. +pub fn capture_logs() -> CapturedLogs { + let logs: Arc>> = Arc::default(); + let subscriber = tracing_subscriber::registry().with(CaptureLayer { logs: logs.clone() }); + let guard = tracing::subscriber::set_default(subscriber); + CapturedLogs { + logs, + _guard: guard, + } +} + +/// An individual log item. +pub struct LogItem { + pub level: Level, + pub message: String, +} + +/// Provides access to captured logs. +pub struct CapturedLogs { + logs: Arc>>, + _guard: DefaultGuard, +} + +impl CapturedLogs { + /// Get the captured logs. + pub fn get(&self) -> MutexGuard<'_, Vec> { + self.logs.lock().unwrap() + } +} + +struct CaptureLayer { + logs: Arc>>, +} + +impl Layer for CaptureLayer { + fn on_event(&self, event: &Event<'_>, _ctx: Context<'_, S>) { + struct Visitor(Option); + + impl Visit for Visitor { + fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) { + if field.name() == "message" { + // `value` is most likely of type `std::fmt::Arguments`, whose + // `Debug` impl is the same as `Display`. + self.0 = Some(format!("{value:?}")); + } + } + } + + let mut visitor = Visitor(None); + event.record(&mut visitor); + if let Some(message) = visitor.0 { + self.logs.lock().unwrap().push(LogItem { + level: *event.metadata().level(), + message, + }); + } + } +} diff --git a/crates/atuin/Cargo.toml b/crates/atuin/Cargo.toml index 7893916c..90ff969f 100644 --- a/crates/atuin/Cargo.toml +++ b/crates/atuin/Cargo.toml @@ -55,7 +55,6 @@ atuin-scripts = { workspace = true } atuin-kv = { workspace = true } derive_more = { workspace = true } -log = { workspace = true } time = { workspace = true } eyre = { workspace = true } indicatif = "0.18.0" diff --git a/crates/atuin/src/command/client/history.rs b/crates/atuin/src/command/client/history.rs index 91ee27e8..99ee19f1 100644 --- a/crates/atuin/src/command/client/history.rs +++ b/crates/atuin/src/command/client/history.rs @@ -34,8 +34,8 @@ use atuin_client::{ #[cfg(feature = "sync")] use atuin_client::{record, sync}; -use log::{debug, warn}; use time::{OffsetDateTime, macros::format_description}; +use tracing::{debug, warn}; #[cfg(feature = "daemon")] use super::daemon;