diff --git a/Cargo.toml b/Cargo.toml index ff9c9fd3..9f648269 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -62,13 +62,13 @@ fnv = "1.0" humantime = { version = "2.1", optional = true } log = { version = "0.4.20", features = ["std"] } log-mdc = { version = "0.1", optional = true } -serde = { version = "1.0", optional = true, features = ["derive"] } +serde = { version = "1.0.196", optional = true, features = ["derive"] } serde-value = { version = "0.7", optional = true } 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 } -toml = { version = "0.8", optional = true } +toml = { version = "<0.8.10", optional = true } parking_lot = { version = "0.12.0", optional = true } rand = { version = "0.8", optional = true} thiserror = "1.0.15" @@ -88,6 +88,7 @@ streaming-stats = "0.2.3" humantime = "2.1" tempfile = "3.8" mock_instant = "0.3" +serde_test = "1.0.176" [[example]] name = "json_logger" diff --git a/src/append/console.rs b/src/append/console.rs index cf089518..a5a2a963 100644 --- a/src/append/console.rs +++ b/src/append/console.rs @@ -263,3 +263,117 @@ impl Deserialize for ConsoleAppenderDeserializer { Ok(Box::new(appender.build())) } } + +#[cfg(test)] +mod test { + use super::*; + use crate::encode::Write; + + #[test] + fn test_console_append() { + use log::Level; + + // Build a std out appender + let appender = ConsoleAppender::builder() + .tty_only(false) + .target(Target::Stdout) + .encoder(Box::new(PatternEncoder::new("{m}{n}"))) + .build(); + + assert!(appender + .append( + &Record::builder() + .level(Level::Debug) + .target("target") + .module_path(Some("module_path")) + .file(Some("file")) + .line(Some(100)) + .args(format_args!("{}", "message")) + .build() + ) + .is_ok()); + + // No op, but test coverage :) + appender.flush(); + } + + #[test] + fn test_appender_builder() { + // Build a std out appender + let _appender = ConsoleAppender::builder() + .tty_only(false) + .target(Target::Stdout) + .encoder(Box::new(PatternEncoder::new("{m}{n}"))) + .build(); + + // Build a std err appender + let _appender = ConsoleAppender::builder() + .tty_only(false) + .target(Target::Stderr) + .encoder(Box::new(PatternEncoder::new("{m}{n}"))) + .build(); + + // Build a default encoder appender + let _appender = ConsoleAppender::builder() + .tty_only(true) + .target(Target::Stderr) + .build(); + } + + #[test] + #[cfg(feature = "config_parsing")] + fn test_config_deserializer() { + use crate::{config::Deserializers, encode::EncoderConfig}; + use serde_value::Value; + use std::collections::BTreeMap; + let deserializer = ConsoleAppenderDeserializer; + + let targets = vec![ConfigTarget::Stdout, ConfigTarget::Stderr]; + + for target in targets { + let console_cfg = ConsoleAppenderConfig { + target: Some(target), + encoder: Some(EncoderConfig { + kind: "pattern".to_owned(), + config: Value::Map(BTreeMap::new()), + }), + tty_only: Some(true), + }; + assert!(deserializer + .deserialize(console_cfg, &Deserializers::default()) + .is_ok()); + } + } + + fn write_test(mut writer: WriterLock) { + use std::io::Write; + + assert_eq!(writer.write(b"Write log\n").unwrap(), 10); + assert!(writer.set_style(&Style::new()).is_ok()); + assert!(writer.write_all(b"Write All log\n").is_ok()); + assert!(writer.write_fmt(format_args!("{} \n", "normal")).is_ok()); + assert!(writer.flush().is_ok()); + } + + #[test] + fn test_tty_writer() { + // Note that this fails in GitHub Actions and therefore does not + // show as covered. + let w = match ConsoleWriter::stdout() { + Some(w) => w, + None => return, + }; + + let tty = Writer::Tty(w); + assert!(tty.is_tty()); + + write_test(tty.lock()); + } + + #[test] + fn test_raw_writer() { + let raw = Writer::Raw(StdWriter::stdout()); + assert!(!raw.is_tty()); + write_test(raw.lock()); + } +} diff --git a/src/append/file.rs b/src/append/file.rs index 3f345e7d..d3d910d0 100644 --- a/src/append/file.rs +++ b/src/append/file.rs @@ -164,7 +164,7 @@ mod test { use super::*; #[test] - fn create_directories() { + fn test_create_directories() { let tempdir = tempfile::tempdir().unwrap(); FileAppender::builder() @@ -173,11 +173,64 @@ mod test { } #[test] - fn append_false() { + fn test_append_trait() { + use log::Level; + let tempdir = tempfile::tempdir().unwrap(); - FileAppender::builder() - .append(false) + let appender = FileAppender::builder() .build(tempdir.path().join("foo.log")) .unwrap(); + + log_mdc::insert("foo", "bar"); + let res = appender.append( + &Record::builder() + .level(Level::Debug) + .target("target") + .module_path(Some("module_path")) + .file(Some("file")) + .line(Some(100)) + .args(format_args!("{}", "message")) + .build(), + ); + assert!(res.is_ok()); + + appender.flush(); + } + + #[test] + fn test_appender_builder() { + let append_choices = vec![true, false]; + let tempdir = tempfile::tempdir().unwrap(); + + for do_append in append_choices { + // No actionable test + FileAppender::builder() + .append(do_append) + .build(tempdir.path().join("foo.log")) + .unwrap(); + } + } + + #[test] + #[cfg(feature = "config_parsing")] + fn test_config_deserializer() { + use crate::config::Deserializers; + use serde_value::Value; + use std::collections::BTreeMap; + + let tempdir = tempfile::tempdir().unwrap(); + let file_cfg = FileAppenderConfig { + path: tempdir.path().join("foo.log").to_str().unwrap().to_owned(), + encoder: Some(EncoderConfig { + kind: "pattern".to_owned(), + config: Value::Map(BTreeMap::new()), + }), + append: Some(true), + }; + + let deserializer = FileAppenderDeserializer; + + let res = deserializer.deserialize(file_cfg, &Deserializers::default()); + assert!(res.is_ok()); } } diff --git a/src/append/mod.rs b/src/append/mod.rs index 73570423..acc07b68 100644 --- a/src/append/mod.rs +++ b/src/append/mod.rs @@ -153,12 +153,15 @@ impl<'de> Deserialize<'de> for AppenderConfig { #[cfg(test)] mod test { + #[cfg(feature = "config_parsing")] + use super::*; + #[cfg(any(feature = "file_appender", feature = "rolling_file_appender"))] use std::env::{set_var, var}; #[test] #[cfg(any(feature = "file_appender", feature = "rolling_file_appender"))] - fn expand_env_vars_tests() { + fn test_expand_env_vars() { set_var("HELLO_WORLD", "GOOD BYE"); #[cfg(not(target_os = "windows"))] let test_cases = vec![ @@ -250,4 +253,59 @@ mod test { assert_eq!(res, expected) } } + + #[test] + #[cfg(feature = "config_parsing")] + fn test_config_deserialize() { + use std::collections::BTreeMap; + + use serde_test::{assert_de_tokens, assert_de_tokens_error, Token}; + use serde_value::Value; + + use crate::filter::FilterConfig; + + let appender = AppenderConfig { + kind: "file".to_owned(), + filters: vec![FilterConfig { + kind: "threshold".to_owned(), + config: Value::Map({ + let mut map = BTreeMap::new(); + map.insert( + Value::String("level".to_owned()), + Value::String("error".to_owned()), + ); + map + }), + }], + config: Value::Map(BTreeMap::new()), + }; + + let mut cfg = vec![ + Token::Struct { + name: "AppenderConfig", + len: 3, + }, + Token::Str("kind"), + Token::Str("file"), + Token::Str("filters"), + Token::Seq { len: Some(1) }, + Token::Struct { + name: "FilterConfig", + len: 2, + }, + Token::Str("kind"), + Token::Str("threshold"), + Token::Str("level"), + Token::Str("error"), + Token::StructEnd, + Token::SeqEnd, + Token::StructEnd, + ]; + + assert_de_tokens(&appender, &cfg); + + // Intentional typo on expected field + cfg[1] = Token::Str("kid"); + assert_de_tokens_error::(&cfg, "missing field `kind`"); + } } diff --git a/src/append/rolling_file/mod.rs b/src/append/rolling_file/mod.rs index 9e6d35ee..8023c86a 100644 --- a/src/append/rolling_file/mod.rs +++ b/src/append/rolling_file/mod.rs @@ -367,17 +367,55 @@ impl Deserialize for RollingFileAppenderDeserializer { #[cfg(test)] mod test { - use std::{ - fs::File, - io::{Read, Write}, - }; - use super::*; use crate::append::rolling_file::policy::Policy; + use std::{fs::read_to_string, io::Read}; + use tempfile::NamedTempFile; + + #[cfg(feature = "config_parsing")] + use serde_test::{assert_de_tokens, Token}; + + #[test] + #[cfg(feature = "config_parsing")] + fn test_config_deserialize() { + use super::*; + use serde_value::Value; + use std::collections::BTreeMap; + + let policy = Policy { + kind: "compound".to_owned(), + config: Value::Map(BTreeMap::new()), + }; + + assert_de_tokens( + &policy, + &[ + Token::Struct { + name: "Policy", + len: 1, + }, + Token::Str("kind"), + Token::Str("compound"), + Token::StructEnd, + ], + ); + + assert_de_tokens( + &policy, + &[ + Token::Struct { + name: "Policy", + len: 0, + }, + Token::StructEnd, + ], + ); + } + #[test] #[cfg(feature = "yaml_format")] - fn deserialize() { + fn test_deserialize_appenders() { use crate::config::{Deserializers, RawConfig}; let dir = tempfile::tempdir().unwrap(); @@ -413,14 +451,13 @@ appenders: let config = ::serde_yaml::from_str::(&config).unwrap(); let errors = config.appenders_lossy(&Deserializers::new()).1; - println!("{:?}", errors); assert!(errors.is_empty()); } #[derive(Debug)] - struct NopPolicy; + struct NopPostPolicy; - impl Policy for NopPolicy { + impl Policy for NopPostPolicy { fn process(&self, _: &mut LogFile) -> anyhow::Result<()> { Ok(()) } @@ -429,43 +466,67 @@ appenders: } } + #[derive(Debug)] + struct NopPrePolicy; + + impl Policy for NopPrePolicy { + fn process(&self, _: &mut LogFile) -> anyhow::Result<()> { + Ok(()) + } + fn is_pre_process(&self) -> bool { + true + } + } + #[test] - fn append() { - let dir = tempfile::tempdir().unwrap(); - let path = dir.path().join("append.log"); - RollingFileAppender::builder() - .append(true) - .build(&path, Box::new(NopPolicy)) - .unwrap(); - assert!(path.exists()); - File::create(&path).unwrap().write_all(b"hello").unwrap(); + fn test_rolling_append() { + use log::Level; + + let tmp_file = NamedTempFile::new().unwrap(); + let policies: Vec> = vec![Box::new(NopPrePolicy), Box::new(NopPostPolicy)]; + let record = Record::builder() + .level(Level::Debug) + .target("target") + .module_path(Some("module_path")) + .file(Some("file")) + .line(Some(100)) + .build(); + log_mdc::insert("foo", "bar"); + + for policy in policies { + let appender = RollingFileAppender::builder() + .append(true) + .encoder(Box::new(PatternEncoder::new("{l}: {L} "))) + .build(&tmp_file.path(), policy) + .unwrap(); + + assert!(appender.append(&record).is_ok()); + + // No-op method, but get the test coverage :) + appender.flush(); + } - RollingFileAppender::builder() - .append(true) - .build(&path, Box::new(NopPolicy)) - .unwrap(); - let mut contents = vec![]; - File::open(&path) - .unwrap() - .read_to_end(&mut contents) - .unwrap(); - assert_eq!(contents, b"hello"); + let contents = read_to_string(&tmp_file.path()).unwrap(); + + // Pattern is 'Level: Line Number ' + let expected = "DEBUG: 100 DEBUG: 100 ".to_owned(); + assert_eq!(expected, contents); } #[test] - fn truncate() { + fn test_truncate() { let dir = tempfile::tempdir().unwrap(); let path = dir.path().join("truncate.log"); RollingFileAppender::builder() .append(false) - .build(&path, Box::new(NopPolicy)) + .build(&path, Box::new(NopPostPolicy)) .unwrap(); assert!(path.exists()); File::create(&path).unwrap().write_all(b"hello").unwrap(); RollingFileAppender::builder() .append(false) - .build(&path, Box::new(NopPolicy)) + .build(&path, Box::new(NopPostPolicy)) .unwrap(); let mut contents = vec![]; File::open(&path) @@ -474,4 +535,120 @@ appenders: .unwrap(); assert_eq!(contents, b""); } + + #[test] + fn test_logfile() { + let tmp_file = NamedTempFile::new().unwrap(); + let (file, path) = tmp_file.into_parts(); + let buf_writer = BufWriter::new(file); + let log_writer = LogWriter { + file: buf_writer, + len: 0, + }; + + let path = path.keep().unwrap(); + let path = path.as_path(); + let mut logfile = LogFile { + writer: &mut Some(log_writer), + path: path, + len: 0, + }; + + // Keep the temporary file from being deleted. + assert_eq!(logfile.path(), path); + assert_eq!(logfile.len_estimate(), 0); + assert!(logfile.writer.is_some()); + + logfile.roll(); + + // Needed because of the call to keep + fs::remove_file(path).unwrap(); + + assert!(logfile.writer.is_none()); + } + + #[test] + #[cfg(feature = "config_parsing")] + fn test_cfg_deserializer() { + use super::*; + use crate::config::Deserializers; + use serde_value::Value; + use std::collections::BTreeMap; + + let tmp_file = NamedTempFile::new().unwrap(); + + let append_cfg = RollingFileAppenderConfig { + path: tmp_file.path().to_str().unwrap().to_owned(), + append: Some(true), + encoder: Some(EncoderConfig { + kind: "pattern".to_owned(), + config: Value::Map(BTreeMap::new()), + }), + policy: Policy { + kind: "compound".to_owned(), + config: Value::Map({ + let mut map = BTreeMap::new(); + map.insert( + Value::String("trigger".to_owned()), + Value::Map({ + let mut map = BTreeMap::new(); + map.insert( + Value::String("kind".to_owned()), + Value::String("size".to_owned()), + ); + map.insert( + Value::String("limit".to_owned()), + Value::String("1mb".to_owned()), + ); + map + }), + ); + map.insert( + Value::String("roller".to_owned()), + Value::Map({ + let mut map = BTreeMap::new(); + map.insert( + Value::String("kind".to_owned()), + Value::String("fixed_window".to_owned()), + ); + map.insert(Value::String("base".to_owned()), Value::I32(1)); + map.insert(Value::String("count".to_owned()), Value::I32(5)); + map.insert( + Value::String("pattern".to_owned()), + Value::String("logs/test.{}.log".to_owned()), + ); + map + }), + ); + map + }), + }, + }; + + let deserializer = RollingFileAppenderDeserializer; + + let res = deserializer.deserialize(append_cfg, &Deserializers::default()); + assert!(res.is_ok()); + } + + #[test] + fn test_logwriter() { + // Can't use named or unnamed temp file here because of opening + // the file multiple times for reading + let file = tempfile::tempdir().unwrap(); + let file_path = file.path().join("writer.log"); + let file = File::create(&file_path).unwrap(); + let buf_writer = BufWriter::new(file); + let mut log_writer = LogWriter { + file: buf_writer, + len: 0, + }; + + let contents = fs::read_to_string(&file_path).unwrap(); + assert!(contents.is_empty()); + assert_eq!(log_writer.write(b"test").unwrap(), 4); + assert!(log_writer.flush().is_ok()); + let contents = fs::read_to_string(file_path).unwrap(); + assert!(contents.contains("test")); + } } diff --git a/src/append/rolling_file/policy/compound/mod.rs b/src/append/rolling_file/policy/compound/mod.rs index 484af19c..3e14ae06 100644 --- a/src/append/rolling_file/policy/compound/mod.rs +++ b/src/append/rolling_file/policy/compound/mod.rs @@ -2,7 +2,7 @@ //! //! Requires the `compound_policy` feature. #[cfg(feature = "config_parsing")] -use serde::{self, de}; +use serde::de; #[cfg(feature = "config_parsing")] use serde_value::Value; #[cfg(feature = "config_parsing")] diff --git a/src/config/raw.rs b/src/config/raw.rs index a092d56b..9868b495 100644 --- a/src/config/raw.rs +++ b/src/config/raw.rs @@ -89,9 +89,7 @@ //! ``` #![allow(deprecated)] -use std::{ - borrow::ToOwned, collections::HashMap, fmt, marker::PhantomData, sync::Arc, time::Duration, -}; +use std::{collections::HashMap, fmt, marker::PhantomData, sync::Arc, time::Duration}; use anyhow::anyhow; use derivative::Derivative; @@ -288,9 +286,9 @@ impl Deserializers { } /// Deserializes a value of a specific type and kind. - pub fn deserialize(&self, kind: &str, config: Value) -> anyhow::Result> + pub fn deserialize(&self, kind: &str, config: Value) -> anyhow::Result> where - T: Deserializable, + T: Deserializable + ?Sized, { match self.0.get::>().and_then(|m| m.get(kind)) { Some(b) => b.deserialize(config, self), diff --git a/src/config/runtime.rs b/src/config/runtime.rs index 6b80019f..3d04947e 100644 --- a/src/config/runtime.rs +++ b/src/config/runtime.rs @@ -1,7 +1,7 @@ //! log4rs configuration use log::LevelFilter; -use std::{collections::HashSet, iter::IntoIterator}; +use std::collections::HashSet; use thiserror::Error; use crate::{append::Append, filter::Filter};