diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index b8219b46..1f58427f 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -17,7 +17,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - rust_versions: ["stable", "1.69"] + rust_versions: ["stable", "1.70"] os: [ubuntu-latest, windows-latest] steps: - name: Checkout the source code @@ -63,7 +63,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - rust_versions: ["stable", "1.69"] + rust_versions: ["stable", "1.70"] steps: - name: Checkout the source code uses: actions/checkout@v4 diff --git a/Cargo.toml b/Cargo.toml index e1019686..4e8f213a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ repository = "https://github.com/estk/log4rs" readme = "README.md" keywords = ["log", "logger", "logging", "log4"] edition = "2018" -rust-version = "1.69" +rust-version = "1.70" [features] default = ["all_components", "config_parsing", "yaml_format"] @@ -68,25 +68,12 @@ thread-id = { version = "4", optional = true } typemap-ors = { version = "1.0.0", optional = true } serde_json = { version = "1.0", optional = true } serde_yaml = { version = "0.9", optional = true } -<<<<<<< HEAD -<<<<<<< HEAD -<<<<<<< HEAD toml = { version = "<0.8.10", optional = true } -======= -toml = { version = "0.8.10", optional = true } ->>>>>>> c3d7f42 (chore: clippy fixes + msrv bump) -======= -toml = { version = "0.8.9", optional = true } ->>>>>>> 4486edd (chore: backout msrv bump, knock toml down) -======= -toml = { version = "<0.8.10", optional = true } ->>>>>>> 049e2cc (limit toml) parking_lot = { version = "0.12.0", optional = true } rand = { version = "0.8", optional = true} thiserror = "1.0.15" anyhow = "1.0.28" derivative = "2.2" -once_cell = "1.17.1" [target.'cfg(windows)'.dependencies] winapi = { version = "0.3", optional = true, features = ["handleapi", "minwindef", "processenv", "winbase", "wincon"] } diff --git a/README.md b/README.md index c35baf4d..496a7fa1 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ [![crates.io](https://img.shields.io/crates/v/log4rs.svg)](https://crates.io/crates/log4rs) [![License: MIT OR Apache-2.0](https://img.shields.io/crates/l/clippy.svg)](#license) ![CI](https://github.com/estk/log4rs/workflows/CI/badge.svg) -[![Minimum rustc version](https://img.shields.io/badge/rustc-1.69+-green.svg)](https://github.com/estk/log4rs#rust-version-requirements) +[![Minimum rustc version](https://img.shields.io/badge/rustc-1.70+-green.svg)](https://github.com/estk/log4rs#rust-version-requirements) log4rs is a highly configurable logging framework modeled after Java's Logback and log4j libraries. @@ -54,7 +54,7 @@ fn main() { ## Rust Version Requirements -1.69 +1.70 ## Building for Dev diff --git a/src/encode/json.rs b/src/encode/json.rs index 784a8739..2674db69 100644 --- a/src/encode/json.rs +++ b/src/encode/json.rs @@ -85,7 +85,15 @@ impl JsonEncoder { impl Encode for JsonEncoder { fn encode(&self, w: &mut dyn Write, record: &Record) -> anyhow::Result<()> { - self.encode_inner(w, Local::now(), record) + #[cfg(test)] + let time = DateTime::parse_from_rfc3339("2016-03-20T14:22:20.644420340-08:00") + .unwrap() + .with_timezone(&Local); + + #[cfg(not(test))] + let time = Local::now(); + + self.encode_inner(w, time, record) } } @@ -168,13 +176,17 @@ impl Deserialize for JsonEncoderDeserializer { mod test { #[cfg(feature = "chrono")] use chrono::{DateTime, Local}; - use log::Level; + use log::{Level, Record}; use super::*; + + #[cfg(feature = "config_parsing")] + use crate::config::Deserializers; + use crate::encode::writer::simple::SimpleWriter; #[test] - fn default() { + fn test_json_encode() { let time = DateTime::parse_from_rfc3339("2016-03-20T14:22:20.644420340-08:00") .unwrap() .with_timezone(&Local); @@ -184,16 +196,15 @@ mod test { let file = "file"; let line = 100; let message = "message"; - let thread = "encode::json::test::default"; + let thread = "encode::json::test::test_json_encode"; log_mdc::insert("foo", "bar"); let encoder = JsonEncoder::new(); let mut buf = vec![]; encoder - .encode_inner( + .encode( &mut SimpleWriter(&mut buf), - time, &Record::builder() .level(level) .target(target) @@ -221,4 +232,15 @@ mod test { ); assert_eq!(expected, String::from_utf8(buf).unwrap().trim()); } + + #[test] + #[cfg(feature = "config_parsing")] + fn test_cfg_deserializer() { + let json_cfg = JsonEncoderConfig { _p: () }; + + let deserializer = JsonEncoderDeserializer; + + let res = deserializer.deserialize(json_cfg, &Deserializers::default()); + assert!(res.is_ok()); + } } diff --git a/src/encode/mod.rs b/src/encode/mod.rs index aa290b3c..efe386c4 100644 --- a/src/encode/mod.rs +++ b/src/encode/mod.rs @@ -154,3 +154,80 @@ impl<'a, W: Write + ?Sized> Write for &'a mut W { ::set_style(*self, style) } } + +#[cfg(test)] +mod test { + #[cfg(feature = "config_parsing")] + use serde_test::{assert_de_tokens, assert_de_tokens_error, Token}; + + #[test] + #[cfg(feature = "config_parsing")] + fn test_cfg_deserialize() { + use super::*; + use std::collections::BTreeMap; + + let pattern = "[{d(%Y-%m-%dT%H:%M:%S%.6f)} {h({l}):<5.5} {M}] {m}{n}".to_owned(); + + let mut config = BTreeMap::new(); + config.insert(Value::String("pattern".to_owned()), Value::String(pattern)); + + let encoder_cfg = EncoderConfig { + kind: "pattern".to_owned(), + config: Value::Map(config), + }; + + assert_de_tokens( + &encoder_cfg, + &[ + Token::Struct { + name: "EncoderConfig", + len: 2, + }, + Token::Str("kind"), + Token::Str("pattern"), + Token::Str("pattern"), + Token::Str("[{d(%Y-%m-%dT%H:%M:%S%.6f)} {h({l}):<5.5} {M}] {m}{n}"), + Token::StructEnd, + ], + ); + + // No pattern defined, should fail to deserializez into a map + assert_de_tokens_error::( + &[ + Token::Struct { + name: "EncoderConfig", + len: 2, + }, + Token::Str("kind"), + Token::Str("pattern"), + Token::Str("pattern"), + Token::StructEnd, + ], + "deserialization did not expect this token: StructEnd", + ); + } + + #[test] + #[cfg(feature = "console_writer")] + fn test_set_console_writer_style() { + use super::*; + use crate::encode::writer::console::ConsoleWriter; + + let w = match ConsoleWriter::stdout() { + Some(w) => w, + None => return, + }; + let mut w = w.lock(); + + assert!(w + .set_style( + Style::new() + .text(Color::Red) + .background(Color::Blue) + .intense(true), + ) + .is_ok()); + + w.set_style(&Style::new()).unwrap(); + } +} diff --git a/src/encode/pattern/mod.rs b/src/encode/pattern/mod.rs index 5215f2ec..15c42dc8 100644 --- a/src/encode/pattern/mod.rs +++ b/src/encode/pattern/mod.rs @@ -744,18 +744,16 @@ impl Deserialize for PatternEncoderDeserializer { #[cfg(test)] mod tests { + #[cfg(feature = "config_parsing")] + use crate::config::Deserializers; #[cfg(feature = "simple_writer")] - use log::{Level, Record}; + use crate::encode::{writer::simple::SimpleWriter, Encode, Write as EncodeWrite}; #[cfg(feature = "simple_writer")] - use std::process; + use log::{Level, Record}; #[cfg(feature = "simple_writer")] - use std::thread; + use std::{io::Write, process, thread}; - use super::{Chunk, PatternEncoder}; - #[cfg(feature = "simple_writer")] - use crate::encode::writer::simple::SimpleWriter; - #[cfg(feature = "simple_writer")] - use crate::encode::Encode; + use super::*; fn error_free(encoder: &PatternEncoder) -> bool { encoder.chunks.iter().all(|c| match *c { @@ -765,18 +763,18 @@ mod tests { } #[test] - fn invalid_formatter() { + fn test_invalid_formatter() { assert!(!error_free(&PatternEncoder::new("{x}"))); } #[test] - fn unclosed_delimiter() { + fn test_unclosed_delimiter() { assert!(!error_free(&PatternEncoder::new("{d(%Y-%m-%d)"))); } #[test] #[cfg(feature = "simple_writer")] - fn log() { + fn test_log() { let pw = PatternEncoder::new("{l} {m} at {M} in {f}:{L}"); let mut buf = vec![]; pw.encode( @@ -796,7 +794,7 @@ mod tests { #[test] #[cfg(feature = "simple_writer")] - fn unnamed_thread() { + fn test_unnamed_thread() { thread::spawn(|| { let pw = PatternEncoder::new("{T}"); let mut buf = vec![]; @@ -810,7 +808,7 @@ mod tests { #[test] #[cfg(feature = "simple_writer")] - fn named_thread() { + fn test_named_thread() { thread::Builder::new() .name("foobar".to_string()) .spawn(|| { @@ -827,7 +825,7 @@ mod tests { #[test] #[cfg(feature = "simple_writer")] - fn thread_id_field() { + fn test_thread_id_field() { thread::spawn(|| { let pw = PatternEncoder::new("{I}"); let mut buf = vec![]; @@ -841,7 +839,7 @@ mod tests { #[test] #[cfg(feature = "simple_writer")] - fn process_id() { + fn test_process_id() { let pw = PatternEncoder::new("{P}"); let mut buf = vec![]; @@ -853,7 +851,7 @@ mod tests { #[test] #[cfg(feature = "simple_writer")] - fn system_thread_id() { + fn test_system_thread_id() { let pw = PatternEncoder::new("{i}"); let mut buf = vec![]; @@ -865,13 +863,13 @@ mod tests { #[test] #[cfg(feature = "simple_writer")] - fn default_okay() { + fn test_default_okay() { assert!(error_free(&PatternEncoder::default())); } #[test] #[cfg(feature = "simple_writer")] - fn left_align() { + fn test_left_align() { let pw = PatternEncoder::new("{m:~<5.6}"); let mut buf = vec![]; @@ -893,7 +891,7 @@ mod tests { #[test] #[cfg(feature = "simple_writer")] - fn right_align() { + fn test_right_align() { let pw = PatternEncoder::new("{m:~>5.6}"); let mut buf = vec![]; @@ -915,7 +913,7 @@ mod tests { #[test] #[cfg(feature = "simple_writer")] - fn left_align_formatter() { + fn test_left_align_formatter() { let pw = PatternEncoder::new("{({l} {m}):15}"); let mut buf = vec![]; @@ -932,7 +930,7 @@ mod tests { #[test] #[cfg(feature = "simple_writer")] - fn right_align_formatter() { + fn test_right_align_formatter() { let pw = PatternEncoder::new("{({l} {m}):>15}"); let mut buf = vec![]; @@ -948,27 +946,27 @@ mod tests { } #[test] - fn custom_date_format() { + fn test_custom_date_format() { assert!(error_free(&PatternEncoder::new( "{d(%Y-%m-%d %H:%M:%S)} {m}{n}" ))); } #[test] - fn timezones() { + fn test_timezones() { assert!(error_free(&PatternEncoder::new("{d(%+)(utc)}"))); assert!(error_free(&PatternEncoder::new("{d(%+)(local)}"))); assert!(!error_free(&PatternEncoder::new("{d(%+)(foo)}"))); } #[test] - fn unescaped_parens() { + fn test_unescaped_parens() { assert!(!error_free(&PatternEncoder::new("(hi)"))); } #[test] #[cfg(feature = "simple_writer")] - fn escaped_chars() { + fn test_escaped_chars() { let pw = PatternEncoder::new("{{{m}(())}}"); let mut buf = vec![]; @@ -982,7 +980,7 @@ mod tests { #[test] #[cfg(feature = "simple_writer")] - fn quote_braces_with_backslash() { + fn test_quote_braces_with_backslash() { let pw = PatternEncoder::new(r"\{\({l}\)\}\\"); let mut buf = vec![]; @@ -996,7 +994,7 @@ mod tests { #[test] #[cfg(feature = "simple_writer")] - fn mdc() { + fn test_mdc() { let pw = PatternEncoder::new("{X(user_id)}"); log_mdc::insert("user_id", "mdc value"); @@ -1009,7 +1007,7 @@ mod tests { #[test] #[cfg(feature = "simple_writer")] - fn mdc_missing_default() { + fn test_mdc_missing_default() { let pw = PatternEncoder::new("{X(user_id)}"); let mut buf = vec![]; @@ -1021,7 +1019,7 @@ mod tests { #[test] #[cfg(feature = "simple_writer")] - fn mdc_missing_custom() { + fn test_mdc_missing_custom() { let pw = PatternEncoder::new("{X(user_id)(missing value)}"); let mut buf = vec![]; @@ -1033,7 +1031,7 @@ mod tests { #[test] #[cfg(feature = "simple_writer")] - fn debug_release() { + fn test_debug_release() { let debug_pat = "{D({l})}"; let release_pat = "{R({l})}"; @@ -1063,4 +1061,283 @@ mod tests { assert!(debug_buf.is_empty()); } } + + #[test] + #[cfg(feature = "simple_writer")] + fn test_max_width_writer() { + let mut buf = vec![]; + let mut w = SimpleWriter(&mut buf); + + let mut w = MaxWidthWriter { + remaining: 2, + w: &mut w, + }; + + let res = w.write(b"test write"); + assert!(res.is_ok()); + assert_eq!(res.unwrap(), 2); + assert_eq!(w.remaining, 0); + assert!(w.flush().is_ok()); + assert!(w.set_style(&Style::new()).is_ok()); + assert_eq!(buf, b"te"); + + let mut buf = vec![]; + let mut w = SimpleWriter(&mut buf); + + let mut w = MaxWidthWriter { + remaining: 15, + w: &mut w, + }; + let res = w.write(b"test write"); + assert!(res.is_ok()); + assert_eq!(res.unwrap(), 10); + assert_eq!(w.remaining, 5); + assert_eq!(buf, b"test write"); + } + + #[test] + #[cfg(feature = "simple_writer")] + fn test_left_align_writer() { + let mut buf = vec![]; + let mut w = SimpleWriter(&mut buf); + + let mut w = LeftAlignWriter { + to_fill: 4, + fill: ' ', + w: &mut w, + }; + + let res = w.write(b"test write"); + assert!(res.is_ok()); + assert!(w.flush().is_ok()); + assert!(w.set_style(&Style::new()).is_ok()); + } + + #[test] + #[cfg(feature = "simple_writer")] + fn test_right_align_writer() { + let mut write_buf = vec![]; + let buf = vec![BufferedOutput::Style(Style::new())]; + let mut w = SimpleWriter(&mut write_buf); + + let mut w = RightAlignWriter { + to_fill: 4, + fill: ' ', + w: &mut w, + buf, + }; + + let res = w.write(b"test write"); + assert!(res.is_ok()); + assert!(w.flush().is_ok()); + assert!(w.set_style(&Style::new()).is_ok()); + assert!(w.finish().is_ok()); + } + + #[test] + #[cfg(feature = "config_parsing")] + fn test_cfg_deserializer() { + let pattern_cfg = PatternEncoderConfig { + pattern: Some("[{d(%Y-%m-%dT%H:%M:%S%.6f)} {h({l}):<5.5} {M}] {m}{n}".to_owned()), + }; + + let deserializer = PatternEncoderDeserializer; + + let res = deserializer.deserialize(pattern_cfg, &Deserializers::default()); + assert!(res.is_ok()); + + let pattern_cfg = PatternEncoderConfig { pattern: None }; + + let res = deserializer.deserialize(pattern_cfg, &Deserializers::default()); + assert!(res.is_ok()); + } + + #[test] + #[cfg(feature = "simple_writer")] + fn test_chunk_no_min_width() { + let mut buf = vec![]; + let pattern = "[{h({l}):<.5} {M}]"; + let chunks: Vec = Parser::new(pattern).map(From::from).collect(); + for chunk in chunks { + assert!(chunk + .encode( + &mut SimpleWriter(&mut buf), + &Record::builder() + .level(Level::Debug) + .args(format_args!("the message")) + .module_path(Some("path")) + .file(Some("file")) + .line(Some(132)) + .build() + ) + .is_ok()) + } + assert!(!String::from_utf8(buf).unwrap().contains("ERROR")); + } + + #[test] + #[cfg(feature = "simple_writer")] + fn test_chunk_encode_err() { + let mut buf = vec![]; + let pattern = "[{h({l):<.5}]"; + let chunks: Vec = Parser::new(pattern).map(From::from).collect(); + for chunk in chunks { + assert!(chunk + .encode( + &mut SimpleWriter(&mut buf), + &Record::builder() + .level(Level::Debug) + .args(format_args!("the message")) + .module_path(Some("path")) + .file(Some("file")) + .line(Some(132)) + .build() + ) + .is_ok()) + } + assert!(String::from_utf8(buf).unwrap().contains("ERROR")); + } + + #[test] + fn test_from_piece_to_chunk() { + // Test 3 args passed to date + let pattern = "[{d(%Y-%m-%d %H:%M:%S %Z)(utc)(local)}]"; + let chunks: Vec = Parser::new(pattern).map(From::from).collect(); + match chunks.get(1).unwrap() { + Chunk::Error(err) => assert_eq!(err, "expected at most two arguments"), + _ => assert!(false), + } + + // Test unexepected formatter + let pattern = "[{d({l} %Y-%m-%d %H:%M:%S %Z)}]"; + let chunks: Vec = Parser::new(pattern).map(From::from).collect(); + match chunks.get(1).unwrap() { + Chunk::Formatted { chunk, .. } => match chunk { + FormattedChunk::Time(value, _tz) => { + assert_eq!(value, "{ERROR: unexpected formatter} %Y-%m-%d %H:%M:%S %Z") + } + _ => assert!(false), + }, + _ => assert!(false), + } + + let tests = vec![ + ("[{d(%Y-%m-%d %H:%M:%S %Z)(zulu)}]", "invalid timezone"), + ("[{d(%Y-%m-%d %H:%M:%S %Z)({l})}]", "invalid timezone"), + ("[{d(%Y-%m-%d %H:%M:%S %Z)()}]", "invalid timezone"), + ("[{h({l})({M}):<5.5}]", "expected exactly one argument"), + ( + "[{D({l})({M}):<5.5}{R({l})({M}):<5.5}]", + "expected exactly one argument", + ), + ( + "[{X(user_id)(foobar)(test):<5.5}]", + "expected at most two arguments", + ), + ("[{X({l user_id):<5.5}]", "expected '}'"), + ("[{X({l} user_id):<5.5}]", "invalid MDC key"), + ("[{X:<5.5}]", "missing MDC key"), + ("[{X(user_id)({l):<5.5}]", "expected '}'"), + ("[{X(user_id)({l}):<5.5}]", "invalid MDC default"), + ("[{X(user_id)():<5.5} {M}]", "invalid MDC default"), + ]; + + for (pattern, error_msg) in tests { + let chunks: Vec = Parser::new(pattern).map(From::from).collect(); + match chunks.get(1).unwrap() { + Chunk::Error(err) => assert!(err.contains(error_msg)), + _ => assert!(false), + } + } + + // Test expected 1 arg + let pattern = "{({l} {m})()}"; + let chunks: Vec = Parser::new(pattern).map(From::from).collect(); + match chunks.get(0).unwrap() { + Chunk::Error(err) => assert!(err.contains("expected exactly one argument")), + _ => assert!(false), + } + + // Test no_args + let pattern = "{l()}"; + let chunks: Vec = Parser::new(pattern).map(From::from).collect(); + match chunks.get(0).unwrap() { + Chunk::Error(err) => assert!(err.contains("unexpected arguments")), + _ => assert!(false), + } + } + + #[test] + #[cfg(feature = "simple_writer")] + fn test_encode_formatted_chunk() { + // Each test gets a new buf and writer to allow for checking the + // buffer and utilizing completely clean buffers. + + let record = Record::builder() + .level(Level::Info) + .args(format_args!("the message")) + .module_path(Some("path")) + .file(Some("file")) + .line(None) + .target("target") + .build(); + + // Limit the time tests to the year. Just need to verify that time can + // be written. Don't need to be precise. This should limit potential + // race condition failures. + + // Test UTC Time + let mut write_buf = vec![]; + let mut w = SimpleWriter(&mut write_buf); + let chunk = FormattedChunk::Time("%Y".to_owned(), Timezone::Utc); + chunk.encode(&mut w, &record).unwrap(); + assert_eq!(write_buf, Utc::now().format("%Y").to_string().as_bytes()); + + // Test Local Time + let mut write_buf = vec![]; + let mut w = SimpleWriter(&mut write_buf); + let chunk = FormattedChunk::Time("%Y".to_owned(), Timezone::Local); + chunk.encode(&mut w, &record).unwrap(); + assert_eq!(write_buf, Local::now().format("%Y").to_string().as_bytes()); + + // Test missing Line + let mut write_buf = vec![]; + let mut w = SimpleWriter(&mut write_buf); + let chunk = FormattedChunk::Line; + chunk.encode(&mut w, &record).unwrap(); + assert_eq!(write_buf, b"???"); + + // Test Target + let mut write_buf = vec![]; + let mut w = SimpleWriter(&mut write_buf); + let chunk = FormattedChunk::Target; + chunk.encode(&mut w, &record).unwrap(); + assert_eq!(write_buf, b"target"); + + // Test Newline + let mut write_buf = vec![]; + let mut w = SimpleWriter(&mut write_buf); + let chunk = FormattedChunk::Newline; + chunk.encode(&mut w, &record).unwrap(); + assert_eq!(write_buf, NEWLINE.as_bytes()); + + // Loop over to hit each possible styling + for level in Level::iter() { + let record = Record::builder() + .level(level) + .args(format_args!("the message")) + .module_path(Some("path")) + .file(Some("file")) + .line(None) + .target("target") + .build(); + + let mut write_buf = vec![]; + let mut w = SimpleWriter(&mut write_buf); + let chunk = FormattedChunk::Highlight(vec![Chunk::Text("Text".to_owned())]); + chunk.encode(&mut w, &record).unwrap(); + assert_eq!(write_buf, b"Text"); + // No style updates in the buffer to check for + } + } } diff --git a/src/encode/pattern/parser.rs b/src/encode/pattern/parser.rs index 8e91e8ec..aefa9377 100644 --- a/src/encode/pattern/parser.rs +++ b/src/encode/pattern/parser.rs @@ -268,3 +268,91 @@ impl<'a> Iterator for Parser<'a> { } } } + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_arg_parser() { + let pattern = "(%Y-%m-%dT%H:%M:%S%.6f"; + let mut parser = Parser::new(pattern); + + let arg = parser.arg(); + assert!(arg.is_err()); + + let pattern = "(%Y-%m-%dT%H:%M:%S%.6f)"; + let mut parser = Parser::new(pattern); + + let arg = parser.arg(); + assert!(arg.is_ok()); + + let pattern = "[{d(%Y-%m-%dT%H:%M:%S%.6f)} {h({l}):<5.5} {M}] {m}{n}"; + let mut parser = Parser::new(pattern); + + let arg = parser.arg(); + assert!(arg.is_ok()); + assert!(arg.unwrap().is_empty()); + } + + #[test] + fn test_name() { + // match up to first non alpha numberic + let pattern = "test["; + let mut parser = Parser::new(pattern); + let name = parser.name(); + assert_eq!(name, "test"); + + // match up to first non alpha numberic, so empty string + let pattern = "["; + let mut parser = Parser::new(pattern); + let name = parser.name(); + assert_eq!(name, ""); + + // match up to first non alpha numberic, so empty string + let pattern = "test"; + let mut parser = Parser::new(pattern); + let name = parser.name(); + assert_eq!(name, "test"); + } + + #[test] + fn test_argument_invalid_and_valid() { + let pattern = "(%Y-%m-%dT%H:%M:%S%.6f"; + let mut parser = Parser::new(pattern); + + let piece = parser.argument(); + assert!(match piece { + Piece::Error(_) => true, + _ => false, + }); + + let pattern = "[{d(%Y-%m-%dT%H:%M:%S%.6f)} {h({l}):<5.5} {M}] {m}{n}"; + let mut parser = Parser::new(pattern); + + let piece = parser.argument(); + assert!(match piece { + Piece::Argument { .. } => true, + _ => false, + }); + } + + #[test] + fn test_unmatched_bracket() { + let pattern = "d}"; + let parser = Parser::new(pattern); + let mut iter = parser.into_iter(); + + // First parse the d + assert!(match iter.next().unwrap() { + Piece::Text { .. } => true, + _ => false, + }); + + // Next try and parse the } but it's unmatched + assert!(match iter.next().unwrap() { + Piece::Error { .. } => true, + _ => false, + }); + } +} diff --git a/src/encode/writer/ansi.rs b/src/encode/writer/ansi.rs index 8b8b4226..9d35d247 100644 --- a/src/encode/writer/ansi.rs +++ b/src/encode/writer/ansi.rs @@ -87,11 +87,11 @@ mod test { use crate::encode::{Color, Style, Write as EncodeWrite}; #[test] - fn basic() { + fn test_ansi_writer() { let stdout = io::stdout(); let mut w = AnsiWriter(stdout.lock()); - w.write_all(b"normal ").unwrap(); + w.write(b"normal ").unwrap(); w.set_style( Style::new() .text(Color::Red) @@ -100,10 +100,24 @@ mod test { ) .unwrap(); w.write_all(b"styled").unwrap(); - w.set_style(Style::new().text(Color::Green)).unwrap(); + // Call out intense false here to hit else case + w.set_style(Style::new().text(Color::Green).intense(false)) + .unwrap(); w.write_all(b" styled2").unwrap(); w.set_style(&Style::new()).unwrap(); - w.write_all(b" normal\n").unwrap(); + w.write_fmt(format_args!(" {} \n", "normal")).unwrap(); w.flush().unwrap(); } + + #[test] + fn test_color_enum() { + assert_eq!(color_byte(Color::Black), b'0'); + assert_eq!(color_byte(Color::Red), b'1'); + assert_eq!(color_byte(Color::Green), b'2'); + assert_eq!(color_byte(Color::Yellow), b'3'); + assert_eq!(color_byte(Color::Blue), b'4'); + assert_eq!(color_byte(Color::Magenta), b'5'); + assert_eq!(color_byte(Color::Cyan), b'6'); + assert_eq!(color_byte(Color::White), b'7'); + } } diff --git a/src/encode/writer/console.rs b/src/encode/writer/console.rs index 6209fb97..f9272dc0 100644 --- a/src/encode/writer/console.rs +++ b/src/encode/writer/console.rs @@ -2,36 +2,37 @@ //! //! Requires the `console_writer` feature. -use std::{fmt, io}; +use std::{env, fmt, io}; use crate::encode::{self, Style}; -use once_cell::sync::Lazy; - -static COLOR_MODE: Lazy = Lazy::new(|| { - let no_color = std::env::var("NO_COLOR") - .map(|var| var != "0") - .unwrap_or(false); - let clicolor_force = std::env::var("CLICOLOR_FORCE") - .map(|var| var != "0") - .unwrap_or(false); +use std::sync::OnceLock; + +static COLOR_MODE: OnceLock = OnceLock::new(); + +fn set_color_mode( + no_color: Result, + clicolor_force: Result, + clicolor: Result, +) -> ColorMode { + let no_color = no_color.map(|var| var != "0").unwrap_or(false); + let clicolor_force = clicolor_force.map(|var| var != "0").unwrap_or(false); + if no_color { ColorMode::Never } else if clicolor_force { ColorMode::Always } else { - let clicolor = std::env::var("CLICOLOR") - .map(|var| var != "0") - .unwrap_or(true); + let clicolor = clicolor.map(|var| var != "0").unwrap_or(true); if clicolor { ColorMode::Auto } else { ColorMode::Never } } -}); +} /// The color output mode for a `ConsoleAppender` -#[derive(Clone, Copy, Default)] +#[derive(Clone, Copy, Default, Debug, PartialEq)] pub enum ColorMode { /// Print color only if the output is recognized as a console #[default] @@ -121,14 +122,14 @@ impl<'a> encode::Write for ConsoleWriterLock<'a> { #[cfg(unix)] mod imp { - use std::{fmt, io}; + use std::{env, fmt, io}; use crate::{ encode::{ self, writer::{ ansi::AnsiWriter, - console::{ColorMode, COLOR_MODE}, + console::{set_color_mode, ColorMode, COLOR_MODE}, }, Style, }, @@ -140,7 +141,13 @@ mod imp { impl Writer { pub fn stdout() -> Option { let writer = || Writer(AnsiWriter(StdWriter::stdout())); - match *COLOR_MODE { + let color_mode_init = { + let no_color = env::var("NO_COLOR"); + let clicolor_force = env::var("CLICOLOR_FORCE"); + let clicolor = env::var("CLICOLOR"); + set_color_mode(no_color, clicolor_force, clicolor) + }; + match COLOR_MODE.get_or_init(|| color_mode_init) { ColorMode::Auto => { if unsafe { libc::isatty(libc::STDOUT_FILENO) } != 1 { None @@ -155,7 +162,13 @@ mod imp { pub fn stderr() -> Option { let writer = || Writer(AnsiWriter(StdWriter::stderr())); - match *COLOR_MODE { + let color_mode_init = { + let no_color = env::var("NO_COLOR"); + let clicolor_force = env::var("CLICOLOR_FORCE"); + let clicolor = env::var("CLICOLOR"); + set_color_mode(no_color, clicolor_force, clicolor) + }; + match COLOR_MODE.get_or_init(|| color_mode_init) { ColorMode::Auto => { if unsafe { libc::isatty(libc::STDERR_FILENO) } != 1 { None @@ -227,6 +240,7 @@ mod imp { #[cfg(windows)] mod imp { use std::{ + env, fmt, io::{self, Write}, mem, @@ -239,7 +253,7 @@ mod imp { use crate::{ encode::{ self, - writer::console::{ColorMode, COLOR_MODE}, + writer::console::{set_color_mode, ColorMode, COLOR_MODE}, Color, Style, }, priv_io::{StdWriter, StdWriterLock}, @@ -335,7 +349,13 @@ mod imp { inner: StdWriter::stdout(), }; - match *COLOR_MODE { + let color_mode_init = { + let no_color = env::var("NO_COLOR"); + let clicolor_force = env::var("CLICOLOR_FORCE"); + let clicolor = env::var("CLICOLOR"); + set_color_mode(no_color, clicolor_force, clicolor) + }; + match COLOR_MODE.get_or_init(|| color_mode_init) { ColorMode::Auto | ColorMode::Always => Some(writer), ColorMode::Never => None, } @@ -362,7 +382,13 @@ mod imp { inner: StdWriter::stdout(), }; - match *COLOR_MODE { + let color_mode_init = { + let no_color = env::var("NO_COLOR"); + let clicolor_force = env::var("CLICOLOR_FORCE"); + let clicolor = env::var("CLICOLOR"); + set_color_mode(no_color, clicolor_force, clicolor) + }; + match COLOR_MODE.get_or_init(|| color_mode_init) { ColorMode::Auto | ColorMode::Always => Some(writer), ColorMode::Never => None, } @@ -435,20 +461,60 @@ mod imp { #[cfg(test)] mod test { - use std::io::Write; - use super::*; use crate::encode::{Color, Style, Write as EncodeWrite}; + use std::{env::VarError, io::Write}; + + #[test] + fn test_stdout_console_writer_lock() { + let w = match ConsoleWriter::stdout() { + Some(w) => w, + None => return, + }; + let mut writer = w.lock(); + + writer.write(b"normal ").unwrap(); + writer.set_style( + Style::new() + .text(Color::Red) + .background(Color::Blue) + .intense(true), + ) + .unwrap(); + writer.write_all(b"styled").unwrap(); + writer.set_style(&Style::new().text(Color::Green).intense(false)) + .unwrap(); + writer.write_all(b" styled2").unwrap(); + writer.set_style(&Style::new()).unwrap(); + writer.write_fmt(format_args!(" {} \n", "normal")).unwrap(); + writer.flush().unwrap(); + } + + #[test] + fn test_stderr_console_writer_lock() { + let w = match ConsoleWriter::stderr() { + Some(w) => w, + None => return, + }; + + // Do not operate on the stderr writer. Doing so results in undefined + // test result behavior due to tests running in parallel + let _ = w.lock(); + } + // Unable to test the non locked Console as by definition, the unlocked + // console results in race conditions. Codecov tooling does not seem to + // see this test as coverage of the ConsoleWritterLock or WriterLock + // class, however, it should completely cover either. #[test] - fn basic() { + fn test_console_writer_lock() { let w = match ConsoleWriter::stdout() { Some(w) => w, None => return, }; let mut w = w.lock(); - w.write_all(b"normal ").unwrap(); + w.write(b"normal ").unwrap(); w.set_style( Style::new() .text(Color::Red) @@ -457,10 +523,120 @@ mod test { ) .unwrap(); w.write_all(b"styled").unwrap(); - w.set_style(Style::new().text(Color::Green)).unwrap(); + w.set_style(&Style::new().text(Color::Green).intense(false)) + .unwrap(); w.write_all(b" styled2").unwrap(); w.set_style(&Style::new()).unwrap(); - w.write_all(b" normal\n").unwrap(); + w.write_fmt(format_args!(" {} \n", "normal")).unwrap(); w.flush().unwrap(); } + + #[test] + fn test_color_mode_default() { + let no_color = Err(VarError::NotPresent); + let clicolor_force = Err(VarError::NotPresent); + let clicolor = Err(VarError::NotPresent); + + let color_mode: OnceLock = OnceLock::new(); + assert_eq!( + color_mode.get_or_init(|| set_color_mode(no_color, clicolor_force, clicolor)), + &ColorMode::Auto + ); + } + + // Note that NO_COLOR has priority over all other fields + #[test] + fn test_no_color() { + let no_color = Ok("1".to_owned()); + let clicolor_force = Err(VarError::NotPresent); + let clicolor = Err(VarError::NotPresent); + + let mut color_mode: OnceLock = OnceLock::new(); + assert_eq!( + color_mode.get_or_init(|| set_color_mode(no_color, clicolor_force, clicolor)), + &ColorMode::Never + ); + + let no_color = Ok("1".to_owned()); + let clicolor_force = Ok("1".to_owned()); + let clicolor = Ok("1".to_owned()); + + let _ = color_mode.take(); // Clear the owned value + assert_eq!( + color_mode.get_or_init(|| set_color_mode(no_color, clicolor_force, clicolor)), + &ColorMode::Never + ); + } + + #[test] + fn test_cli_force() { + // CLICOLOR_FORCE is the only set field + let no_color = Err(VarError::NotPresent); + let clicolor_force = Ok("1".to_owned()); + let clicolor = Err(VarError::NotPresent); + + let mut color_mode: OnceLock = OnceLock::new(); + assert_eq!( + color_mode.get_or_init(|| set_color_mode(no_color, clicolor_force, clicolor)), + &ColorMode::Always + ); + + // Although NO_COLOR has priority, when set to 0 next in line + // is CLICOLOR_FORCE which maintains precedence over clicolor + // regardless of how it's set. Attempt both settings below + let no_color = Ok("0".to_owned()); + let clicolor_force = Ok("1".to_owned()); + let clicolor = Ok("1".to_owned()); + + let _ = color_mode.take(); // Clear the owned value + assert_eq!( + color_mode.get_or_init(|| set_color_mode(no_color, clicolor_force, clicolor)), + &ColorMode::Always + ); + + let no_color = Ok("0".to_owned()); + let clicolor_force = Ok("1".to_owned()); + let clicolor = Ok("0".to_owned()); + + let _ = color_mode.take(); // Clear the owned value + assert_eq!( + color_mode.get_or_init(|| set_color_mode(no_color, clicolor_force, clicolor)), + &ColorMode::Always + ); + } + + #[test] + fn test_cli_on() { + // CLICOLOR is the only set field + let no_color = Err(VarError::NotPresent); + let clicolor_force = Err(VarError::NotPresent); + let clicolor = Ok("1".to_owned()); + + let mut color_mode: OnceLock = OnceLock::new(); + assert_eq!( + color_mode.get_or_init(|| set_color_mode(no_color, clicolor_force, clicolor)), + &ColorMode::Auto + ); + + let no_color = Err(VarError::NotPresent); + let clicolor_force = Err(VarError::NotPresent); + let clicolor = Ok("0".to_owned()); + + let _ = color_mode.take(); // Clear the owned value + assert_eq!( + color_mode.get_or_init(|| set_color_mode(no_color, clicolor_force, clicolor)), + &ColorMode::Never + ); + + // CLICOLOR_FORCE is disabled + let no_color = Err(VarError::NotPresent); + let clicolor_force = Ok("0".to_owned()); + let clicolor = Ok("1".to_owned()); + + let _ = color_mode.take(); // Clear the owned value + assert_eq!( + color_mode.get_or_init(|| set_color_mode(no_color, clicolor_force, clicolor)), + &ColorMode::Auto + ); + } } diff --git a/tests/color_control.rs b/tests/color_control.rs deleted file mode 100644 index 344f032f..00000000 --- a/tests/color_control.rs +++ /dev/null @@ -1,24 +0,0 @@ -use std::process::Command; - -fn execute_test(env_key: &str, env_val: &str) { - let mut child_proc = Command::new("cargo") - .args(&["run", "--example", "compile_time_config"]) - .env(env_key, env_val) - .spawn() - .expect("Cargo command failed to start"); - - let ecode = child_proc.wait().expect("failed to wait on child"); - - assert!(ecode.success()); -} - -// Maintaining as a single test to avoid blocking calls to the package cache -#[test] -fn test_no_color() { - let keys = vec!["NO_COLOR", "CLICOLOR_FORCE", "CLICOLOR"]; - - for key in keys { - execute_test(key, "1"); - execute_test(key, "0"); - } -}