From 4ee288174e74950103603789abd6bc80da9f6017 Mon Sep 17 00:00:00 2001 From: bconn98 Date: Sat, 2 Mar 2024 21:56:23 -0500 Subject: [PATCH] tests: add appender tests --- Cargo.toml | 1 + src/append/console.rs | 114 ++++++++++++++++ src/append/file.rs | 61 ++++++++- src/append/mod.rs | 60 ++++++++- src/append/rolling_file/mod.rs | 231 ++++++++++++++++++++++++++------- 5 files changed, 413 insertions(+), 54 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 8f7134ca..9f648269 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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..065b1297 100644 --- a/src/append/rolling_file/mod.rs +++ b/src/append/rolling_file/mod.rs @@ -367,17 +367,54 @@ 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 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 +450,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,49 +465,146 @@ 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(); - - 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"); + 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("{m}{n}"))) + .build(&tmp_file.path(), policy) + .unwrap(); + + assert!(appender.append(&record).is_ok()); + + // No-op method, but get the test coverage :) + appender.flush(); + } } #[test] - fn truncate() { - let dir = tempfile::tempdir().unwrap(); - let path = dir.path().join("truncate.log"); - RollingFileAppender::builder() - .append(false) - .build(&path, Box::new(NopPolicy)) - .unwrap(); - assert!(path.exists()); - File::create(&path).unwrap().write_all(b"hello").unwrap(); - - RollingFileAppender::builder() - .append(false) - .build(&path, Box::new(NopPolicy)) - .unwrap(); - let mut contents = vec![]; - File::open(&path) - .unwrap() - .read_to_end(&mut contents) - .unwrap(); - assert_eq!(contents, b""); + fn test_logfile() { + let tmp_file = NamedTempFile::new().unwrap(); + let mut logfile = LogFile { + writer: &mut None, + path: tmp_file.path(), + len: 0, + }; + + assert_eq!(logfile.path(), tmp_file.path()); + assert_eq!(logfile.len_estimate(), 0); + + // No actions to take here, the writer becomes inaccessible but theres + // no getter to verify + logfile.roll(); + } + + #[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")); } }