use jiff::SignedDuration;
use linux_kernel_panic_parser::{LineKind, PanicLog, parse};
use std::{borrow::Cow, sync::Arc};
fn round_trip(input: &str) {
let log = parse(input);
assert_eq!(log.to_string(), input);
assert_eq!(parse(log.to_string()), log);
assert_eq!(
log.lines()
.iter()
.map(|l| l.to_string())
.collect::<String>(),
input
);
}
#[test]
#[allow(
clippy::unnecessary_to_owned,
clippy::needless_borrows_for_generic_args
)]
fn string_inputs_and_from_str() {
let text = "Kernel panic - not syncing: test\n";
let expected = parse(text);
assert_eq!(parse(text.to_string()), expected);
assert_eq!(parse(&text.to_string()), expected);
assert_eq!(parse(Box::<str>::from(text)), expected);
assert_eq!(parse(Cow::Borrowed(text)), expected);
assert_eq!(parse(Cow::Owned::<str>(text.to_string())), expected);
assert_eq!(parse(Arc::<str>::from(text)), expected);
assert_eq!(text.parse::<PanicLog>(), Ok(expected.clone()));
assert_eq!(PanicLog::parse(text), expected);
}
#[test]
fn exact_uptime_and_prefixes() {
for (text, seconds, nanos) in [
("[ 1.000000001] message", 1, 1),
("<0>[123.25] message", 123, 250_000_000),
("[0] message", 0, 0),
(
"[9223372036854775807.999999999] message",
i64::MAX,
999_999_999,
),
] {
let log = parse(text);
assert_eq!(
log.lines()[0].uptime(),
Some(SignedDuration::new(seconds, nanos))
);
assert_eq!(log.lines()[0].message(), "message");
round_trip(text);
}
let log = parse("<4>ordinary line");
assert_eq!(log.lines()[0].message(), "ordinary line");
}
#[test]
fn malformed_timestamps_are_retained() {
for text in [
"[",
"[1.] foo",
"[1.1234567890] foo",
"[-1.0] foo",
"[999999999999999999999999] foo",
"<x>[1.0] foo",
"[1.0 foo",
] {
let log = parse(text);
assert_eq!(log.lines()[0].uptime(), None, "{text}");
assert_eq!(log.lines()[0].message(), text);
round_trip(text);
}
}
#[test]
fn panic_cpu_and_multiple_events() {
let text = "[ 12.5] CPU: 3 PID: 123 Comm: task with spaces Tainted: G 6.12\n[ 12.6] Kernel panic - not syncing: fatal exception\nOct 3 host kernel: Kernel panic - not syncing: second panic\n";
let log = parse(text);
assert!(log.has_panic());
assert_eq!(
log.panic_messages().collect::<Vec<_>>(),
["fatal exception", "second panic"]
);
assert_eq!(
log.lines()[0].kind(),
&LineKind::Cpu {
cpu: 3,
uid: None,
pid: 123,
details: "task with spaces Tainted: G 6.12".into()
}
);
assert!(!parse("BUG: unrelated diagnostic").has_panic());
assert_eq!(
parse("Kernel panic - not syncing:")
.panic_messages()
.collect::<Vec<_>>(),
[""]
);
round_trip(text);
}
#[test]
fn architecture_neutral_traces_and_registers() {
for (architecture, registers, frame) in [
(
"x86_64",
"RAX: 0000000000000000 RBX: ffff888000000001",
" ? panic+0x12/0x300",
),
(
"i386",
"EAX: 00000000 EBX: c0000001",
" [<c0000001>] panic+0x12/0x300",
),
(
"arm64",
"x0: 0000000000000000 x30: ffff800000000001",
" panic+0x12/0x300 [module]",
),
(
"arm",
"r0: 00000000 r10: c0000001",
" [<c0000001>] panic+0x12/0x300",
),
(
"riscv",
"a0: 0000000000000000 ra: ffffffff80000001",
" [<ffffffff80000001>] panic+0x12/0x300",
),
(
"powerpc",
"NIP: c000000000000001 LR: c000000000000002",
" .panic+0x12/0x300",
),
(
"s390",
"r0: 0000000000000000 r15: 0000000000000001",
" [<0000000000000001>] panic+0x12/0x300",
),
("mips", "epc: 80000001 ra: 80000002", " panic+0x12/0x300"),
(
"future",
"custom_reg: 0x123456789abcdef0123456789abcdef0",
" [<123456789abcdef0123456789abcdef0>] panic+0x12/0x300",
),
] {
let text =
format!("Call trace:\n{registers}\n{frame}\nKernel panic - not syncing: fatal\n");
let log = parse(&text);
assert_eq!(log.lines()[0].kind(), &LineKind::CallTrace);
assert!(
matches!(log.lines()[1].kind(), LineKind::Registers(_)),
"{architecture}"
);
let LineKind::Frame(parsed) = log.lines()[2].kind() else {
panic!("{architecture}")
};
assert_eq!(parsed.offset, "0x12");
assert_eq!(parsed.size, "0x300");
round_trip(&text);
}
let log = parse(" [<ffffffff12345678>] ? function.isra.0+0x01/0xFF [module]");
let LineKind::Frame(frame) = log.lines()[0].kind() else {
panic!()
};
assert_eq!(frame.address.as_deref(), Some("ffffffff12345678"));
assert!(frame.uncertain);
assert_eq!(frame.symbol, "function.isra.0");
assert_eq!(frame.suffix, "[module]");
}
#[test]
fn unknown_and_truncated_lines_survive() {
for text in [
"RAX: xyz",
"RAX:",
"panic+0x/0x1",
"panic+0x1/0x10garbage",
"CPU: 999999999999999999999 PID: 1 Comm: task",
"[<ffff>]",
"Unrecognized metadata: exotic board",
"λ kernel diagnostic 🐧",
"\0\t binary-looking text",
] {
assert_eq!(parse(text).lines()[0].kind(), &LineKind::Unknown, "{text}");
round_trip(text);
}
}
#[test]
fn line_boundaries_and_whitespace() {
assert!(parse("").lines().is_empty());
assert_eq!(parse("\n").lines().len(), 1);
assert_eq!(parse("a\r\nb\rc\nd").lines().len(), 4);
for text in [
"",
"\n",
"\r\n",
"\r",
" \t\r\n\n",
"a\r\nb\rc\nd",
"Kernel panic - not syncing: reason \r\n",
"no final newline",
] {
round_trip(text);
}
}
#[test]
fn generated_utf8_round_trips() {
let pieces = [
"",
"\n",
"\r",
"\r\n",
" ",
"\t",
"\0",
"λ🐧",
"[1.25] ",
"<0>",
"Kernel panic - not syncing: test",
"r0: ff",
"panic+0x1/0x2",
];
for a in pieces {
for b in pieces {
for c in pieces {
round_trip(&format!("{a}{b}{c}"));
}
}
}
}