bugfixing and testing

This commit is contained in:
Jana Dönszelmann 2026-03-31 15:06:56 +02:00
parent e490a2ce04
commit d0bc7e952c
No known key found for this signature in database
5 changed files with 411 additions and 109 deletions

View file

@ -1,15 +1,12 @@
use std::{
cell::RefCell,
fs::File,
io::{self, BufRead, BufReader},
io::{self, BufRead, BufReader, Read},
mem,
path::{Path, PathBuf},
rc::Rc,
sync::{
OnceLock,
mpsc::{Receiver, channel},
},
thread,
sync::OnceLock,
thread::{self, JoinHandle},
};
use dumpster::sync::Gc;
@ -17,7 +14,7 @@ use dumpster::sync::Gc;
use crate::tui::model::{ChildInfo, LogEntry, RawLogEntry};
struct LogFileEntryGenerator {
file: BufReader<File>,
file: BufReader<Box<dyn Read + Send + Sync + 'static>>,
}
impl LogFileEntryGenerator {
@ -38,7 +35,11 @@ impl LogFileEntryGenerator {
match serde_json::from_str(&line) {
Ok(i) => Some(i),
Err(e) => {
#[cfg(test)]
panic!("deserializing: {e:?} in {line}");
#[cfg(not(test))]
eprintln!("deserializing: {e:?} in {line}");
#[cfg(not(test))]
None
}
}
@ -106,7 +107,7 @@ impl LogFileEntryGenerator {
struct Inner {
pub path: PathBuf,
pub jh: Option<JoinHandle<()>>,
first: Option<Gc<LogEntry>>,
}
@ -114,32 +115,38 @@ struct Inner {
pub struct LogfileReader(Rc<RefCell<Inner>>);
impl LogfileReader {
pub fn new(p: &Path) -> io::Result<Self> {
let file = File::open(p)?;
pub fn new(p: impl AsRef<Path>) -> io::Result<Self> {
Ok(Self::new_from_read(File::open(p.as_ref())?, p))
}
pub fn new_from_read(r: impl Read + Send + Sync + 'static, p: impl AsRef<Path>) -> Self {
let mut generator = LogFileEntryGenerator {
file: BufReader::new(file),
file: BufReader::new(Box::new(r)),
};
let first = generator.next_entry(None);
if let Some(mut curr_last) = first.clone() {
thread::spawn(move || {
while let Some(new) = generator.next_entry(Some(curr_last.clone())) {
assert!(new.prev().is_some());
curr_last.initialize_next().get_or_init({
let new = new.clone();
|| new
});
curr_last = new;
let jh = thread::spawn({
let first = first.clone();
move || {
if let Some(mut curr_last) = first.clone() {
while let Some(new) = generator.next_entry(Some(curr_last.clone())) {
assert!(new.prev().is_some());
curr_last.initialize_next().get_or_init({
let new = new.clone();
|| new
});
curr_last = new;
}
}
});
}
}
});
Ok(Self(Rc::new(RefCell::new(Inner {
path: p.to_path_buf(),
Self(Rc::new(RefCell::new(Inner {
path: p.as_ref().to_path_buf(),
first,
}))))
jh: Some(jh),
})))
}
pub fn path(&self) -> PathBuf {
@ -150,3 +157,282 @@ impl LogfileReader {
self.0.borrow().first.clone()
}
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use super::*;
use crate::tui::{
filter::{Filter, FilterKind, Matcher, MatcherValue},
log_viewer::filters::Filters,
processing::Cursor,
widgets::last_error::LastError,
};
fn parse(data: &str) -> Cursor {
let r = LogfileReader::new_from_read(
std::io::Cursor::new(data.trim().as_bytes().to_vec()),
"test",
);
r.0.borrow_mut().jh.take().unwrap().join().unwrap();
Cursor::new(r.first().unwrap())
}
fn with_fields(fields: &str) -> String {
format!(
r#"{{"timestamp": "2026-03-30 23:47Z", "level": "DEBUG", "filename": "foo", "line_number": 30, "fields": {fields}}}"#
)
}
fn filters() -> Filters {
let le = LastError::new();
Filters::new(None, le)
}
#[test]
fn get_message() {
let c = parse(&[with_fields(r#"{"message": "foo"}"#)].join("\n"));
assert_eq!(c.curr().message_or_name(), Some("foo".to_string()));
}
#[test]
fn bounds() {
let mut c = parse(&[with_fields(r#"{"message": "foo"}"#)].join("\n"));
let f = filters();
assert_eq!(c.curr().message_or_name(), Some("foo".to_string()));
assert!(!c.next(&f));
assert!(!c.prev(&f));
assert!(!c.enter(&f));
assert!(!c.exit(&f));
assert_eq!(c.curr().message_or_name(), Some("foo".to_string()));
}
#[test]
fn two_singles() {
let mut c = parse(
&[
with_fields(r#"{"message": "foo"}"#),
with_fields(r#"{"message": "bar"}"#),
]
.join("\n"),
);
let f = filters();
assert_eq!(c.curr().message_or_name(), Some("foo".to_string()));
assert!(c.next(&f));
assert_eq!(c.curr().message_or_name(), Some("bar".to_string()));
assert!(!c.next(&f));
assert_eq!(c.curr().message_or_name(), Some("bar".to_string()));
assert!(c.prev(&f));
assert_eq!(c.curr().message_or_name(), Some("foo".to_string()));
assert!(!c.prev(&f));
assert!(!c.enter(&f));
assert!(!c.exit(&f));
assert_eq!(c.curr().message_or_name(), Some("foo".to_string()));
}
#[test]
fn enter_exit() {
let mut c = parse(
&[
with_fields(r#"{"message": "foo"}"#),
with_fields(r#"{"message": "enter"}"#),
with_fields(r#"{"message": "baz"}"#),
with_fields(r#"{"message": "meow"}"#),
with_fields(r#"{"message": "exit"}"#),
with_fields(r#"{"message": "bar"}"#),
]
.join("\n"),
);
let f = filters();
assert_eq!(c.curr().message_or_name(), Some("foo".to_string()));
assert!(c.next(&f));
assert!(matches!(Gc::as_ref(&c.curr()), LogEntry::Sub { .. }));
assert!(c.next(&f));
assert_eq!(c.curr().message_or_name(), Some("bar".to_string()));
assert!(c.prev(&f));
assert!(c.enter(&f));
assert_eq!(c.curr().message_or_name(), Some("baz".to_string()));
assert!(!c.enter(&f));
assert!(!c.prev(&f));
assert!(c.exit(&f));
assert!(matches!(Gc::as_ref(&c.curr()), LogEntry::Sub { .. }));
assert!(c.enter(&f));
assert!(c.next(&f));
assert!(!c.enter(&f));
assert!(!c.next(&f));
assert_eq!(c.curr().message_or_name(), Some("meow".to_string()));
assert!(c.exit(&f));
}
#[test]
fn empty_enter() {
let mut c = parse(
&[
with_fields(r#"{"message": "foo"}"#),
with_fields(r#"{"message": "enter"}"#),
with_fields(r#"{"message": "exit"}"#),
with_fields(r#"{"message": "bar"}"#),
]
.join("\n"),
);
let f = filters();
assert_eq!(c.curr().message_or_name(), Some("foo".to_string()));
assert!(c.next(&f));
assert!(
matches!(Gc::as_ref(&c.curr()), LogEntry::Sub { children, .. } if children.first_child.is_none() )
);
assert!(!c.enter(&f));
assert!(!c.exit(&f));
assert!(matches!(Gc::as_ref(&c.curr()), LogEntry::Sub { .. }));
assert!(c.next(&f));
assert_eq!(c.curr().message_or_name(), Some("bar".to_string()));
}
#[test]
fn remove_first() {
let mut c = parse(
&[
with_fields(r#"{"message": "foo"}"#),
with_fields(r#"{"message": "baz"}"#),
with_fields(r#"{"message": "meow"}"#),
with_fields(r#"{"message": "bar"}"#),
]
.join("\n"),
);
let mut f = filters();
f.push(Arc::new(Filter {
matcher: Matcher::Field {
name: "message".to_string(),
value: MatcherValue::Exact("foo".to_string()),
},
kind: FilterKind::Remove,
}));
assert!(c.next(&f));
assert_eq!(c.curr().message_or_name(), Some("baz".to_string()));
assert!(!c.prev(&f));
}
#[test]
fn remove_middle() {
let mut c = parse(
&[
with_fields(r#"{"message": "foo"}"#),
with_fields(r#"{"message": "baz"}"#),
with_fields(r#"{"message": "meow"}"#),
with_fields(r#"{"message": "bar"}"#),
]
.join("\n"),
);
let mut f = filters();
f.push(Arc::new(Filter {
matcher: Matcher::Field {
name: "message".to_string(),
value: MatcherValue::Exact("baz".to_string()),
},
kind: FilterKind::Remove,
}));
assert_eq!(c.curr().message_or_name(), Some("foo".to_string()));
assert!(c.next(&f));
assert_eq!(c.curr().message_or_name(), Some("meow".to_string()));
assert!(c.prev(&f));
assert_eq!(c.curr().message_or_name(), Some("foo".to_string()));
assert!(!c.prev(&f));
}
#[test]
fn inline() {
let mut c = parse(
&[
with_fields(r#"{"message": "foo"}"#),
with_fields(r#"{"message": "enter", "name": "nest"}"#),
with_fields(r#"{"message": "baz"}"#),
with_fields(r#"{"message": "meow"}"#),
with_fields(r#"{"message": "exit"}"#),
with_fields(r#"{"message": "bar"}"#),
]
.join("\n"),
);
let mut f = filters();
f.push(Arc::new(Filter {
matcher: Matcher::Field {
name: "name".to_string(),
value: MatcherValue::Exact("nest".to_string()),
},
kind: FilterKind::Inline,
}));
assert_eq!(c.curr().message_or_name(), Some("foo".to_string()));
assert!(c.next(&f));
assert_eq!(c.curr().message_or_name(), Some("baz".to_string()));
assert!(c.next(&f));
assert!(!c.exit(&f));
assert_eq!(c.curr().message_or_name(), Some("meow".to_string()));
assert!(c.next(&f));
assert_eq!(c.curr().message_or_name(), Some("bar".to_string()));
assert!(c.prev(&f));
assert_eq!(c.curr().message_or_name(), Some("meow".to_string()));
assert!(!c.exit(&f));
assert!(c.prev(&f));
assert_eq!(c.curr().message_or_name(), Some("baz".to_string()));
assert!(c.prev(&f));
assert_eq!(c.curr().message_or_name(), Some("foo".to_string()));
assert!(!c.prev(&f));
}
#[test]
fn inline_while_inside() {
let mut c = parse(
&[
with_fields(r#"{"message": "foo"}"#),
with_fields(r#"{"message": "enter", "name": "nest"}"#),
with_fields(r#"{"message": "baz"}"#),
with_fields(r#"{"message": "meow"}"#),
with_fields(r#"{"message": "exit"}"#),
with_fields(r#"{"message": "bar"}"#),
]
.join("\n"),
);
let mut f = filters();
assert_eq!(c.curr().message_or_name(), Some("foo".to_string()));
assert!(c.next(&f));
assert!(c.enter(&f));
assert_eq!(c.curr().message_or_name(), Some("baz".to_string()));
// inline the current item
f.push(Arc::new(Filter {
matcher: Matcher::Field {
name: "name".to_string(),
value: MatcherValue::Exact("nest".to_string()),
},
kind: FilterKind::Inline,
}));
c.update_with_parents(&f);
assert_eq!(c.curr().message_or_name(), Some("baz".to_string()));
println!("undo");
f.undo();
c.update_with_parents(&f);
assert_eq!(c.curr().message_or_name(), Some("nest".to_string()));
assert!(c.next(&f));
assert_eq!(c.curr().message_or_name(), Some("bar".to_string()));
assert!(!c.next(&f));
assert!(c.prev(&f));
assert!(c.enter(&f));
assert_eq!(c.curr().message_or_name(), Some("baz".to_string()));
assert!(c.next(&f));
assert_eq!(c.curr().message_or_name(), Some("meow".to_string()));
f.redo();
c.update_with_parents(&f);
// redo inlines, and goes to start of inlined part
assert_eq!(c.curr().message_or_name(), Some("baz".to_string()));
assert!(c.prev(&f));
assert_eq!(c.curr().message_or_name(), Some("foo".to_string()));
assert!(!c.prev(&f));
}
}