mirror of
https://github.com/cloud-hypervisor/cloud-hypervisor.git
synced 2026-08-05 02:19:16 +00:00
main: Add unit testing for logger infrastructure
Assisted-by: Claude:Opus-4.7 Signed-off-by: Rob Bradford <rbradford@meta.com>
This commit is contained in:
@@ -144,3 +144,207 @@ impl log::Log for Logger {
|
|||||||
|
|
||||||
fn flush(&self) {}
|
fn flush(&self) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::io;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use log::Log;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
/// A `Write` sink that appends to a shared byte buffer so tests can
|
||||||
|
/// inspect what the logger wrote.
|
||||||
|
#[derive(Clone, Default)]
|
||||||
|
struct SharedBuffer(Arc<Mutex<Vec<u8>>>);
|
||||||
|
|
||||||
|
impl SharedBuffer {
|
||||||
|
fn contents(&self) -> String {
|
||||||
|
String::from_utf8(self.0.lock().unwrap().clone()).unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Write for SharedBuffer {
|
||||||
|
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||||
|
self.0.lock().unwrap().extend_from_slice(buf);
|
||||||
|
Ok(buf.len())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flush(&mut self) -> io::Result<()> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render(tokens: &[Token]) -> String {
|
||||||
|
tokens
|
||||||
|
.iter()
|
||||||
|
.map(|t| match t {
|
||||||
|
Token::Literal(s) => format!("L({s})"),
|
||||||
|
Token::BootTime => "B".to_string(),
|
||||||
|
Token::Thread => "T".to_string(),
|
||||||
|
Token::Level => "V".to_string(),
|
||||||
|
Token::Location => "O".to_string(),
|
||||||
|
Token::Msg => "M".to_string(),
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join("|")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_plain_literal() {
|
||||||
|
let tokens = parse_format("hello world").unwrap();
|
||||||
|
assert_eq!(render(&tokens), "L(hello world)");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_empty_string() {
|
||||||
|
let tokens = parse_format("").unwrap();
|
||||||
|
assert!(tokens.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_all_known_tokens() {
|
||||||
|
let tokens = parse_format("[{boottime}] <{thread}> {level} {location} -- {msg}").unwrap();
|
||||||
|
assert_eq!(render(&tokens), "L([)|B|L(] <)|T|L(> )|V|L( )|O|L( -- )|M");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_default_format_succeeds() {
|
||||||
|
let tokens = parse_format(DEFAULT_FORMAT).unwrap();
|
||||||
|
// Default format has 5 tokens interleaved with literals.
|
||||||
|
assert!(tokens.iter().any(|t| matches!(t, Token::BootTime)));
|
||||||
|
assert!(tokens.iter().any(|t| matches!(t, Token::Thread)));
|
||||||
|
assert!(tokens.iter().any(|t| matches!(t, Token::Level)));
|
||||||
|
assert!(tokens.iter().any(|t| matches!(t, Token::Location)));
|
||||||
|
assert!(tokens.iter().any(|t| matches!(t, Token::Msg)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_escaped_braces() {
|
||||||
|
let tokens = parse_format("{{not-a-token}}").unwrap();
|
||||||
|
assert_eq!(render(&tokens), "L({not-a-token})");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_escaped_braces_around_token() {
|
||||||
|
let tokens = parse_format("{{{level}}}").unwrap();
|
||||||
|
assert_eq!(render(&tokens), "L({)|V|L(})");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_unterminated_brace_errors() {
|
||||||
|
match parse_format("hello {level") {
|
||||||
|
Err(Error::UnterminatedBrace) => {}
|
||||||
|
Err(other) => panic!("unexpected error: {other:?}"),
|
||||||
|
Ok(_) => panic!("expected error"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_unmatched_close_brace_errors() {
|
||||||
|
match parse_format("hello }") {
|
||||||
|
Err(Error::UnmatchedBrace) => {}
|
||||||
|
Err(other) => panic!("unexpected error: {other:?}"),
|
||||||
|
Ok(_) => panic!("expected error"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_unknown_token_errors() {
|
||||||
|
match parse_format("{nope}") {
|
||||||
|
Err(Error::UnknownToken(name)) => assert_eq!(name, "nope"),
|
||||||
|
Err(other) => panic!("unexpected error: {other:?}"),
|
||||||
|
Ok(_) => panic!("expected error"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn logger_new_uses_default_format() {
|
||||||
|
let buf = SharedBuffer::default();
|
||||||
|
let logger = Logger::new(Box::new(buf.clone())).unwrap();
|
||||||
|
// The default format has all 5 dynamic tokens.
|
||||||
|
assert_eq!(
|
||||||
|
logger
|
||||||
|
.tokens
|
||||||
|
.iter()
|
||||||
|
.filter(|t| !matches!(t, Token::Literal(_)))
|
||||||
|
.count(),
|
||||||
|
5
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn logger_enabled_always_true() {
|
||||||
|
let buf = SharedBuffer::default();
|
||||||
|
let logger = Logger::new(Box::new(buf)).unwrap();
|
||||||
|
let metadata = log::Metadata::builder()
|
||||||
|
.level(log::Level::Trace)
|
||||||
|
.target("anything")
|
||||||
|
.build();
|
||||||
|
assert!(logger.enabled(&metadata));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn logger_writes_expected_fields() {
|
||||||
|
let buf = SharedBuffer::default();
|
||||||
|
let logger = Logger::new(Box::new(buf.clone())).unwrap();
|
||||||
|
|
||||||
|
logger.log(
|
||||||
|
&log::Record::builder()
|
||||||
|
.args(format_args!("hello {}", "world"))
|
||||||
|
.level(log::Level::Info)
|
||||||
|
.target("unit_test_target")
|
||||||
|
.file(Some("foo.rs"))
|
||||||
|
.line(Some(42))
|
||||||
|
.build(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let out = buf.contents();
|
||||||
|
assert!(out.starts_with("cloud-hypervisor: "), "got: {out}");
|
||||||
|
assert!(out.contains("INFO"), "got: {out}");
|
||||||
|
assert!(out.contains("foo.rs:42"), "got: {out}");
|
||||||
|
assert!(out.contains("hello world"), "got: {out}");
|
||||||
|
assert!(out.ends_with("\r\n"), "got: {out}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn logger_uses_target_when_no_file() {
|
||||||
|
let buf = SharedBuffer::default();
|
||||||
|
let logger = Logger::new(Box::new(buf.clone())).unwrap();
|
||||||
|
|
||||||
|
logger.log(
|
||||||
|
&log::Record::builder()
|
||||||
|
.args(format_args!("no location"))
|
||||||
|
.level(log::Level::Warn)
|
||||||
|
.target("my_target")
|
||||||
|
.file(None)
|
||||||
|
.line(None)
|
||||||
|
.build(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let out = buf.contents();
|
||||||
|
assert!(out.contains("my_target"), "got: {out}");
|
||||||
|
assert!(!out.contains("foo.rs"), "got: {out}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn logger_appends_each_record() {
|
||||||
|
let buf = SharedBuffer::default();
|
||||||
|
let logger = Logger::new(Box::new(buf.clone())).unwrap();
|
||||||
|
|
||||||
|
for i in 0..3 {
|
||||||
|
logger.log(
|
||||||
|
&log::Record::builder()
|
||||||
|
.args(format_args!("entry-{i}"))
|
||||||
|
.level(log::Level::Debug)
|
||||||
|
.target("t")
|
||||||
|
.build(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let out = buf.contents();
|
||||||
|
assert_eq!(out.matches("entry-").count(), 3, "got: {out}");
|
||||||
|
assert_eq!(out.matches("\r\n").count(), 3, "got: {out}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user