use std::collections::HashMap; use std::io::{self, Read}; use std::time::{Duration, SystemTime}; use log::Level; use std_logger_parser::{parse, ParseErrorKind, Record, Value}; const BUF_SIZE: usize = 4096; #[track_caller] fn test_parser(logs: R, expected: Vec) { let mut got = parse(logs); let mut expected = expected.into_iter(); loop { match (got.next(), expected.next()) { (Some(got), Some(expected)) => { assert_eq!(got.expect("unexpected parsing error"), expected) } (Some(got), None) => panic!("unexpected additional record: {:?}", got), (None, Some(record)) => { panic!("missing records: {:?}, {:?}", record, expected.as_slice()) } (None, None) => break, } } } fn new_record( timestamp: Option, level: Level, msg: &str, target: &str, module: Option<&str>, file: Option<(&str, u32)>, key_values: HashMap, ) -> Record { let mut record = Record::empty(); record.timestamp = timestamp; record.level = level; record.msg = msg.to_owned(); record.target = target.to_owned(); record.module = module.map(|m| m.to_owned()); record.file = file.map(|(f, l)| (f.to_owned(), l)); record.key_values = key_values; record } #[track_caller] fn new_timestamp(ts: &str) -> SystemTime { let mut tm = libc::tm { tm_sec: ts[17..19].parse().unwrap(), tm_min: ts[14..16].parse().unwrap(), tm_hour: ts[11..13].parse().unwrap(), tm_mday: ts[8..10].parse().unwrap(), tm_mon: (ts[5..7].parse::().unwrap()) - 1, tm_year: (ts[0..4].parse::().unwrap()) - 1900, tm_wday: 0, tm_yday: 0, tm_isdst: 0, tm_gmtoff: 0, tm_zone: std::ptr::null_mut(), }; let time_offset = unsafe { libc::timegm(&mut tm) }; // Create the timestamp from the time offset and the nanosecond precision. let nanos: u32 = ts[20..26].parse().unwrap(); SystemTime::UNIX_EPOCH + Duration::new(time_offset as u64, nanos) } struct MultiSlice<'a> { slices: &'a mut [&'a [u8]], } impl<'a> Read for MultiSlice<'a> { fn read(&mut self, buf: &mut [u8]) -> io::Result { let mut remove = 0; let mut accumulated_len = 0; for slice in self.slices.iter_mut() { match slice.read(&mut buf[accumulated_len..]) { Ok(n) => { accumulated_len += n; if n == (&*slice).len() { remove += 1; } else { break; } } Err(err) => return Err(err), } } let slices = std::mem::replace(&mut self.slices, &mut []); self.slices = &mut slices[remove..]; return Ok(accumulated_len); } } #[test] fn smoke() { #[rustfmt::skip] let lines: &mut [&[u8]] = &mut [ // Qouted values. b"ts=\"2021-02-23T13:15:48.624447Z\" lvl=\"INFO\" msg=\"Hello world\" target=\"target\" module=\"module\"\n", // Naked values. b"ts=2021-02-23T13:15:48.624447Z lvl=INFO msg=Hello target=target module=module\n", // File. b"ts=2021-02-23T13:15:48.624447Z lvl=warn msg=with_file target=key_value module=key_value file=some_file.rs:123\n", // No module. b"ts=2021-02-23T13:15:48.624447Z lvl=Error msg=\"No module\" target=target\n", // Nested qoute. b"ts=2021-02-23T13:15:48.624447Z lvl=TraCE msg=\"Some \"great\" message\" target= target \n", // Key-value pairs. b"ts=2021-02-23T13:15:48.624447Z lvl=DEBUG msg=\"key value pairs\" target=key_value module=key_value key1=value1 \"key2\"=\"value2\" key3 = 3 key4=-4 \"key5\" = 5.0 key6=true key7=false\n", // Panic with backtrace, multi-line qouted values. b"ts=\"2021-02-23T13:16:09.576227Z\" lvl=\"ERROR\" msg=\"thread 'main' panicked at 'oops', examples/panic.rs:15\" target=\"panic\" module=\"\" backtrace=\" 0: std::backtrace_rs::backtrace::libunwind::trace\n at /rustc/a143517d44cac50b20cbd3a0b579addab40dd399/library/std/src/../../backtrace/src/backtrace/libunwind.rs:90:5\n std::backtrace_rs::backtrace::trace_unsynchronized\n at /rustc/a143517d44cac50b20cbd3a0b579addab40dd399/library/std/src/../../backtrace/src/backtrace/mod.rs:66:5\n std::backtrace::Backtrace::create\n at /rustc/a143517d44cac50b20cbd3a0b579addab40dd399/library/std/src/backtrace.rs:327:13\n 1: std::backtrace::Backtrace::force_capture\n at /rustc/a143517d44cac50b20cbd3a0b579addab40dd399/library/std/src/backtrace.rs:310:9\n 2: std_logger::log_panic\n at ./src/lib.rs:346:21\n 3: core::ops::function::Fn::call\n at /Users/thomas/.rustup/toolchains/nightly-x86_64-apple-darwin/lib/rustlib/src/rust/library/core/src/ops/function.rs:70:5\n 4: std::panicking::rust_panic_with_hook\n at /rustc/a143517d44cac50b20cbd3a0b579addab40dd399/library/std/src/panicking.rs:595:17\n 5: std::panicking::begin_panic::{{closure}}\n at /Users/thomas/.rustup/toolchains/nightly-x86_64-apple-darwin/lib/rustlib/src/rust/library/std/src/panicking.rs:520:9\n 6: std::sys_common::backtrace::__rust_end_short_backtrace\n at /Users/thomas/.rustup/toolchains/nightly-x86_64-apple-darwin/lib/rustlib/src/rust/library/std/src/sys_common/backtrace.rs:141:18\n 7: std::panicking::begin_panic\n at /Users/thomas/.rustup/toolchains/nightly-x86_64-apple-darwin/lib/rustlib/src/rust/library/std/src/panicking.rs:519:12\n 8: panic::main\n at ./examples/panic.rs:15:5\n 9: core::ops::function::FnOnce::call_once\n at /Users/thomas/.rustup/toolchains/nightly-x86_64-apple-darwin/lib/rustlib/src/rust/library/core/src/ops/function.rs:227:5\n 10: std::sys_common::backtrace::__rust_begin_short_backtrace\n at /Users/thomas/.rustup/toolchains/nightly-x86_64-apple-darwin/lib/rustlib/src/rust/library/std/src/sys_common/backtrace.rs:125:18\n 11: std::rt::lang_start::{{closure}}\n at /Users/thomas/.rustup/toolchains/nightly-x86_64-apple-darwin/lib/rustlib/src/rust/library/std/src/rt.rs:66:18\n 12: core::ops::function::impls:: for &F>::call_once\n at /rustc/a143517d44cac50b20cbd3a0b579addab40dd399/library/core/src/ops/function.rs:259:13\n std::panicking::try::do_call\n at /rustc/a143517d44cac50b20cbd3a0b579addab40dd399/library/std/src/panicking.rs:379:40\n std::panicking::try\n at /rustc/a143517d44cac50b20cbd3a0b579addab40dd399/library/std/src/panicking.rs:343:19\n std::panic::catch_unwind\n at /rustc/a143517d44cac50b20cbd3a0b579addab40dd399/library/std/src/panic.rs:431:14\n std::rt::lang_start_internal\n at /rustc/a143517d44cac50b20cbd3a0b579addab40dd399/library/std/src/rt.rs:51:25\n 13: std::rt::lang_start\n at /Users/thomas/.rustup/toolchains/nightly-x86_64-apple-darwin/lib/rustlib/src/rust/library/std/src/rt.rs:65:5\n 14: _main\n\"\n", // No timestamp. b"lvl=\"INFO\" msg=\"Hello world\" \n", ]; let expected = vec![ new_record( Some(new_timestamp("2021-02-23T13:15:48.624447Z")), Level::Info, "Hello world", "target", Some("module"), None, HashMap::new(), ), new_record( Some(new_timestamp("2021-02-23T13:15:48.624447Z")), Level::Info, "Hello", "target", Some("module"), None, HashMap::new(), ), new_record( Some(new_timestamp("2021-02-23T13:15:48.624447Z")), Level::Warn, "with_file", "key_value", Some("key_value"), Some(("some_file.rs", 123)), HashMap::new(), ), new_record( Some(new_timestamp("2021-02-23T13:15:48.624447Z")), Level::Error, "No module", "target", None, None, HashMap::new(), ), new_record( Some(new_timestamp("2021-02-23T13:15:48.624447Z")), Level::Trace, "Some \"great\" message", "target", None, None, HashMap::new(), ), new_record( Some(new_timestamp("2021-02-23T13:15:48.624447Z")), Level::Debug, "key value pairs", "key_value", Some("key_value"), None, { let mut m = HashMap::new(); m.insert("key1".to_owned(), Value::String("value1".to_owned())); m.insert("key2".to_owned(), Value::String("value2".to_owned())); m.insert("key3".to_owned(), Value::Int(3)); m.insert("key4".to_owned(), Value::Int(-4)); m.insert("key5".to_owned(), Value::Float(5.0)); m.insert("key6".to_owned(), Value::Bool(true)); m.insert("key7".to_owned(), Value::Bool(false)); m }, ), new_record( Some(new_timestamp("2021-02-23T13:16:09.576227Z")), Level::Error, "thread 'main' panicked at 'oops', examples/panic.rs:15", "panic", None, None, { let mut m = HashMap::new(); m.insert("backtrace".to_owned(), Value::String(" 0: std::backtrace_rs::backtrace::libunwind::trace\n at /rustc/a143517d44cac50b20cbd3a0b579addab40dd399/library/std/src/../../backtrace/src/backtrace/libunwind.rs:90:5\n std::backtrace_rs::backtrace::trace_unsynchronized\n at /rustc/a143517d44cac50b20cbd3a0b579addab40dd399/library/std/src/../../backtrace/src/backtrace/mod.rs:66:5\n std::backtrace::Backtrace::create\n at /rustc/a143517d44cac50b20cbd3a0b579addab40dd399/library/std/src/backtrace.rs:327:13\n 1: std::backtrace::Backtrace::force_capture\n at /rustc/a143517d44cac50b20cbd3a0b579addab40dd399/library/std/src/backtrace.rs:310:9\n 2: std_logger::log_panic\n at ./src/lib.rs:346:21\n 3: core::ops::function::Fn::call\n at /Users/thomas/.rustup/toolchains/nightly-x86_64-apple-darwin/lib/rustlib/src/rust/library/core/src/ops/function.rs:70:5\n 4: std::panicking::rust_panic_with_hook\n at /rustc/a143517d44cac50b20cbd3a0b579addab40dd399/library/std/src/panicking.rs:595:17\n 5: std::panicking::begin_panic::{{closure}}\n at /Users/thomas/.rustup/toolchains/nightly-x86_64-apple-darwin/lib/rustlib/src/rust/library/std/src/panicking.rs:520:9\n 6: std::sys_common::backtrace::__rust_end_short_backtrace\n at /Users/thomas/.rustup/toolchains/nightly-x86_64-apple-darwin/lib/rustlib/src/rust/library/std/src/sys_common/backtrace.rs:141:18\n 7: std::panicking::begin_panic\n at /Users/thomas/.rustup/toolchains/nightly-x86_64-apple-darwin/lib/rustlib/src/rust/library/std/src/panicking.rs:519:12\n 8: panic::main\n at ./examples/panic.rs:15:5\n 9: core::ops::function::FnOnce::call_once\n at /Users/thomas/.rustup/toolchains/nightly-x86_64-apple-darwin/lib/rustlib/src/rust/library/core/src/ops/function.rs:227:5\n 10: std::sys_common::backtrace::__rust_begin_short_backtrace\n at /Users/thomas/.rustup/toolchains/nightly-x86_64-apple-darwin/lib/rustlib/src/rust/library/std/src/sys_common/backtrace.rs:125:18\n 11: std::rt::lang_start::{{closure}}\n at /Users/thomas/.rustup/toolchains/nightly-x86_64-apple-darwin/lib/rustlib/src/rust/library/std/src/rt.rs:66:18\n 12: core::ops::function::impls:: for &F>::call_once\n at /rustc/a143517d44cac50b20cbd3a0b579addab40dd399/library/core/src/ops/function.rs:259:13\n std::panicking::try::do_call\n at /rustc/a143517d44cac50b20cbd3a0b579addab40dd399/library/std/src/panicking.rs:379:40\n std::panicking::try\n at /rustc/a143517d44cac50b20cbd3a0b579addab40dd399/library/std/src/panicking.rs:343:19\n std::panic::catch_unwind\n at /rustc/a143517d44cac50b20cbd3a0b579addab40dd399/library/std/src/panic.rs:431:14\n std::rt::lang_start_internal\n at /rustc/a143517d44cac50b20cbd3a0b579addab40dd399/library/std/src/rt.rs:51:25\n 13: std::rt::lang_start\n at /Users/thomas/.rustup/toolchains/nightly-x86_64-apple-darwin/lib/rustlib/src/rust/library/std/src/rt.rs:65:5\n 14: _main\n".to_owned())); m }, ), new_record( None, Level::Info, "Hello world", "", None, None, HashMap::new(), ), ]; test_parser(MultiSlice { slices: lines }, expected); } #[test] fn no_new_line() { let logs = b"ts=\"2021-02-23T13:15:48.624447Z\" lvl=\"INFO\" msg=\"Hello world\" target=\"key_value\" module=\"key_value\""; let expected = vec![new_record( Some(new_timestamp("2021-02-23T13:15:48.624447Z")), Level::Info, "Hello world", "key_value", Some("key_value"), None, HashMap::new(), )]; test_parser::<&[u8]>(logs, expected); } #[test] fn ignore_whitespace() { let logs = b" \n\n \n"; let expected = Vec::new(); test_parser::<&[u8]>(logs, expected); } #[test] fn partial_input_key() { let mut logs = Vec::with_capacity(BUF_SIZE + 200); // Make sure the log is partially read, split the `msg` key. logs.resize(BUF_SIZE - 46, b' '); logs.extend_from_slice(b"ts=\"2021-02-23T13:15:48.624447Z\" lvl=\"INFO\" msg=\"Hello world\" target=\"key_value\" module=\"key_value\""); assert_eq!(&logs[BUF_SIZE - 2..BUF_SIZE], b"ms"); let expected = vec![new_record( Some(new_timestamp("2021-02-23T13:15:48.624447Z")), Level::Info, "Hello world", "key_value", Some("key_value"), None, HashMap::new(), )]; test_parser(&*logs, expected); } #[test] fn partial_input_value() { let mut logs = Vec::with_capacity(BUF_SIZE + 200); // Make sure the log is partially read, split the `msg` value. logs.resize(BUF_SIZE - 59, b' '); logs.extend_from_slice(b"ts=\"2021-02-23T13:15:48.624447Z\" lvl=\"INFO\" msg=\"Hello world\" target=\"key_value\" module=\"key_value\""); assert_eq!(&logs[BUF_SIZE - 4..BUF_SIZE], b"worl"); let expected = vec![new_record( Some(new_timestamp("2021-02-23T13:15:48.624447Z")), Level::Info, "Hello world", "key_value", Some("key_value"), None, HashMap::new(), )]; test_parser(&*logs, expected); } #[test] fn buffer_too_small() { let mut logs = Vec::with_capacity(BUF_SIZE + 200); let msg = "a".repeat(BUF_SIZE); logs.extend_from_slice( b"ts=\"2021-02-23T13:15:48.624447Z\" lvl=\"INFO\" target=\"test\" msg=\"", ); logs.extend_from_slice(msg.as_bytes()); logs.extend_from_slice(b"\""); let expected = vec![new_record( Some(new_timestamp("2021-02-23T13:15:48.624447Z")), Level::Info, &msg, "test", None, None, HashMap::new(), )]; test_parser(&*logs, expected); } #[test] fn invalid_lines() { #[rustfmt::skip] let lines: &mut [&[u8]] = &mut [ // Invalid key. &[115, 111, 109, 101, 0x80, 107, 101, 121, 61, 49, 50, 51, 10], // Invalid UTF-8. // Invalid timestamp. b"ts=2021-02-23T13:15:48.62444Z\n", // Invalid length (too short). b"ts=2021-02-23T13:15:48.624447ZA\n", // Invalid length (too long). // Incorrect formatting of delimiters. b"ts=2021A02-23T13:15:48.624447Z\n", // Year-month. b"ts=2021-02A23T13:15:48.624447Z\n", // Month-day. b"ts=2021-02-23A13:15:48.624447Z\n", // Date-time. b"ts=2021-02-23T13A15:48.624447Z\n", // Hour:minute. b"ts=2021-02-23T13:15A48.624447Z\n", // Minute:second. b"ts=2021-02-23T13:15:48A624447Z\n", // Second.nanosecond. b"ts=2021-02-23T13:15:48.624447A\n", // Timezone. &[116, 115, 61, 0x80, 48, 50, 49, 45, 48, 50, 45, 50, 51, 84, 49, 51, 58, 49, 53, 58, 52, 56, 46, 54, 50, 52, 52, 52, 55, 90, 10], // Invalid UTF-8. // Invalid numbers. b"ts=A021-02-23T13:15:48.624447Z\n", // Year. b"ts=2021-A2-23T13:15:48.624447Z\n", // Month. b"ts=2021-02-A3TA3:15:48.624447Z\n", // Day. b"ts=2021-02-23TA3:15:48.624447Z\n", // Hour. b"ts=2021-02-23T13:A5:48.624447Z\n", // Minute. b"ts=2021-02-23T13:15:A8.624447Z\n", // Second. b"ts=2021-02-23T13:15:48.A24447Z\n", // Nanosecond. // Invalid level. b"lvl=NOT_INFO\n", // Not a level. &[108, 118, 108, 61, 0x80, 79, 84, 95, 73, 78, 70, 79, 10], // Invalid UTF-8. // Invalid key. &[107, 101, 121, 61, 0x80, 97, 108, 117, 101, 10], // Invalid UTF-8. // Invalid file. &[102, 105, 108, 101, 61, 0x80, 111, 109, 101, 95, 102, 105, 108, 101, 46, 114, 115, 58, 49, 48, 10], // Invalid UTF-8. b"file=some_file.rs10\n", // Missing colon (`:`). b"file=some_file.rs:A0\n", // Invalid line number. // Invalid msg value. &[109, 115, 103, 61, 0x80, 111, 109, 101, 95, 109, 101, 115, 115, 97, 103, 101, 10], // Invalid target value. &[116, 97, 114, 103, 101, 116, 61, 0x80, 97, 114, 103, 101, 116, 10], // Invalid module value. &[109, 111, 100, 117, 108, 101, 61, 0x80, 111, 100, 117, 108, 101, 10], // Invalid key value. &[107, 101, 121, 61, 0x80, 97, 108, 117, 101, 10], // Error message with panic backtrace, which includes invalid UTF-8. &[108, 118, 108, 61, 34, 69, 82, 82, 79, 82, 34, 32, 109, 115, 103, 61, 34, 116, 104, 114, 101, 97, 100, 32, 39, 109, 97, 105, 110, 39, 32, 112, 97, 110, 105, 99, 107, 101, 100, 32, 97, 116, 32, 39, 111, 111, 112, 115, 39, 44, 32, 101, 120, 97, 109, 112, 108, 101, 115, 47, 112, 97, 110, 105, 99, 46, 114, 115, 58, 49, 53, 34, 32, 116, 97, 114, 103, 101, 116, 61, 34, 112, 97, 110, 105, 99, 34, 32, 98, 97, 99, 107, 116, 114, 97, 99, 101, 61, 34, 32, 32, 32, 48, 58, 32, 115, 116, 100, 58, 58, 98, 97, 99, 107, 116, 114, 97, 99, 101, 95, 114, 115, 58, 58, 98, 97, 99, 107, 116, 114, 97, 99, 101, 58, 58, 108, 105, 98, 117, 110, 119, 105, 110, 100, 58, 58, 116, 114, 97, 99, 101, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 97, 116, 32, 47, 114, 117, 115, 116, 99, 47, 97, 49, 52, 51, 53, 49, 55, 100, 52, 52, 99, 97, 99, 53, 48, 98, 50, 48, 99, 98, 100, 51, 97, 48, 98, 53, 55, 57, 97, 100, 100, 97, 98, 52, 48, 100, 100, 51, 57, 57, 47, 108, 105, 98, 114, 97, 114, 121, 47, 115, 116, 100, 47, 115, 114, 99, 47, 46, 46, 47, 46, 46, 47, 98, 97, 99, 107, 116, 114, 97, 99, 101, 47, 115, 114, 99, 47, 98, 97, 99, 107, 116, 114, 97, 99, 101, 47, 108, 105, 98, 117, 110, 119, 105, 110, 100, 46, 114, 115, 58, 57, 48, 58, 53, 10, 32, 32, 32, 32, 32, 32, 115, 116, 100, 58, 58, 98, 97, 99, 107, 116, 114, 97, 99, 101, 95, 114, 115, 58, 58, 98, 97, 99, 107, 116, 114, 97, 99, 101, 58, 58, 116, 114, 97, 99, 101, 95, 117, 110, 115, 121, 110, 99, 104, 114, 111, 110, 105, 122, 101, 100, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 97, 116, 32, 47, 114, 117, 115, 116, 99, 47, 97, 49, 52, 51, 53, 49, 55, 100, 52, 52, 99, 97, 99, 53, 48, 98, 50, 48, 99, 98, 100, 51, 97, 48, 98, 53, 55, 57, 97, 100, 100, 97, 98, 52, 48, 100, 100, 51, 57, 57, 47, 108, 105, 98, 114, 97, 114, 121, 47, 115, 116, 100, 47, 115, 114, 99, 47, 46, 46, 47, 46, 46, 47, 98, 97, 99, 107, 116, 114, 97, 99, 101, 47, 115, 114, 99, 47, 98, 97, 99, 107, 116, 114, 97, 99, 101, 47, 109, 111, 100, 46, 114, 115, 58, 54, 54, 58, 53, 10, 32, 32, 32, 32, 32, 32, 115, 116, 100, 58, 58, 98, 97, 99, 107, 116, 114, 97, 99, 101, 58, 58, 66, 97, 99, 107, 116, 114, 97, 99, 101, 58, 58, 99, 114, 101, 97, 116, 101, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 97, 116, 32, 47, 114, 117, 115, 116, 99, 47, 97, 49, 52, 51, 53, 49, 55, 100, 52, 52, 99, 97, 99, 53, 48, 98, 50, 48, 99, 98, 100, 51, 97, 48, 98, 53, 55, 57, 97, 100, 100, 97, 98, 52, 48, 100, 100, 51, 57, 57, 47, 108, 105, 98, 114, 97, 114, 121, 47, 115, 116, 100, 47, 115, 114, 99, 47, 98, 97, 99, 107, 116, 114, 97, 99, 101, 46, 114, 115, 58, 51, 50, 55, 58, 49, 0x80, 10, 34, 10], // Test if the parsed count is increased correctly if the input doesn't // end with a new line (like this line). // NOTE: must be last. &[109, 115, 103, 61, 0x80, 111, 109, 101, 95, 109, 101, 115, 115, 97, 103, 101], ]; // I know... it's not great. let expected = &[ ParseErrorKind::KeyInvalidUt8, ParseErrorKind::InvalidTimestamp, ParseErrorKind::InvalidTimestamp, ParseErrorKind::InvalidTimestamp, ParseErrorKind::InvalidTimestamp, ParseErrorKind::InvalidTimestamp, ParseErrorKind::InvalidTimestamp, ParseErrorKind::InvalidTimestamp, ParseErrorKind::InvalidTimestamp, ParseErrorKind::InvalidTimestamp, ParseErrorKind::InvalidTimestamp, ParseErrorKind::InvalidTimestamp, ParseErrorKind::InvalidTimestamp, ParseErrorKind::InvalidTimestamp, ParseErrorKind::InvalidTimestamp, ParseErrorKind::InvalidTimestamp, ParseErrorKind::InvalidTimestamp, ParseErrorKind::InvalidTimestamp, ParseErrorKind::InvalidLevel, ParseErrorKind::InvalidLevel, ParseErrorKind::InvalidValue, ParseErrorKind::InvalidFile, ParseErrorKind::InvalidFile, ParseErrorKind::InvalidFile, ParseErrorKind::InvalidValue, ParseErrorKind::InvalidValue, ParseErrorKind::InvalidValue, ParseErrorKind::InvalidValue, ParseErrorKind::InvalidValue, ParseErrorKind::InvalidValue, ]; assert_eq!(lines.len(), expected.len()); let mut expected = lines .iter() .zip(expected.iter()) .take(expected.len() - 1) .map(|(l, e)| (Some((&l[..l.len() - 1]).into()), e)) .collect::>(); // Last line doesn't include new line. expected.push(( Some((*lines.last().unwrap()).into()), &ParseErrorKind::InvalidValue, )); let logs = MultiSlice { slices: lines }; let mut expected = expected.into_iter(); for record in parse(logs) { let err = record.unwrap_err(); let expected = expected.next().unwrap(); assert_eq!( err.line, expected.0, "got: {}, expected: {}", String::from_utf8_lossy(&*err.line.as_ref().unwrap()), String::from_utf8_lossy(&*expected.0.as_ref().unwrap()), ); assert_eq!(err.kind, *expected.1); } assert!(expected.len() == 0, "left: {:?}", expected.as_slice()); } #[test] fn io_error_and_continue() { struct ErrReading<'a> { first: &'a [u8], err: Option, second: &'a [u8], } impl<'a> Read for ErrReading<'a> { fn read(&mut self, buf: &mut [u8]) -> io::Result { if !self.first.is_empty() { self.first.read(buf) } else if let Some(err) = self.err.take() { Err(err.into()) } else { self.second.read(buf) } } } let logs = ErrReading { first: b"lvl=INFO msg=Hello target=", err: Some(io::ErrorKind::WouldBlock), second: b"target", }; let mut parser = parse(logs); match parser.next() { Some(Err(err)) => match err.kind { ParseErrorKind::Io(ref io_err) if io_err.kind() == io::ErrorKind::WouldBlock => { assert!(err.line.is_none()); } _ => panic!("unexpected error: {}", err), }, _ => panic!("unexpected result"), } let got = parser.next().unwrap().unwrap(); let expected = new_record( None, Level::Info, "Hello", "target", None, None, HashMap::new(), ); assert_eq!(got, expected); assert!(parser.next().is_none()); }