use std::{ cell::RefCell, fs::File, io::{self, BufRead, BufReader, Read}, mem, path::{Path, PathBuf}, rc::Rc, sync::OnceLock, thread::{self, JoinHandle}, }; use dumpster::sync::Gc; use crate::tui::model::{ChildInfo, LogEntry, RawLogEntry}; struct LogFileEntryGenerator { file: BufReader>, } impl LogFileEntryGenerator { fn next_line(&mut self) -> Option { let mut res = String::new(); match self.file.read_line(&mut res) { Err(e) => { eprintln!("error: {e:?}"); None } Ok(0) => None, Ok(_) => Some(res), } } fn next_raw_entry(&mut self) -> Option { let line = self.next_line()?; 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 } } } fn next_entry(&mut self, prev: Option>) -> Option> { let mut stack = Vec::<(RawLogEntry, Option>, Option>)>::new(); let mut curr_first = None; let mut curr_last = prev; loop { let entry = self.next_raw_entry()?; let new_entry = Gc::new(match entry.fields.message() { Some("enter") => { stack.push((entry, curr_first.take(), curr_last.take())); continue; } Some("exit") => { // TODO: does it match? let Some((enter, prev_first, prev_last)) = stack.pop() else { panic!("exit before entry"); }; let first_child = mem::replace(&mut curr_first, prev_first); let last_child = mem::replace(&mut curr_last, prev_last); let prev = curr_last.clone(); let next = OnceLock::new(); LogEntry::Sub { enter, exit: entry, children: Gc::new(ChildInfo { first_child, last_child, prev, next, }), } } _ => { let prev = curr_last.clone(); let next = OnceLock::new(); LogEntry::Single { entry, prev, next } } }); if stack.is_empty() { return Some(new_entry); } else if let Some(last) = &mut curr_last { last.initialize_next().get_or_init({ let new_entry = new_entry.clone(); move || new_entry }); *last = new_entry; } else { assert!(curr_first.is_none()); curr_first = Some(new_entry.clone()); curr_last = Some(new_entry); } } } } struct Inner { pub path: PathBuf, #[allow(unused)] pub jh: Option>, first: Option>, } #[derive(Clone)] pub struct LogfileReader(Rc>); impl LogfileReader { pub fn new(p: impl AsRef) -> io::Result { 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) -> Self { let mut generator = LogFileEntryGenerator { file: BufReader::new(Box::new(r)), }; let first = generator.next_entry(None); 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; } } } }); Self(Rc::new(RefCell::new(Inner { path: p.as_ref().to_path_buf(), first, jh: Some(jh), }))) } pub fn path(&self) -> PathBuf { self.0.borrow().path.clone() } pub fn first(&self) -> Option> { 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, model::SpanDescriptor, 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")); } #[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")); 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")); } #[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")); assert!(c.next(&f)); assert_eq!(c.curr().message_or_name(), Some("bar")); assert!(!c.next(&f)); assert_eq!(c.curr().message_or_name(), Some("bar")); assert!(c.prev(&f)); assert_eq!(c.curr().message_or_name(), Some("foo")); assert!(!c.prev(&f)); assert!(!c.enter(&f)); assert!(!c.exit(&f)); assert_eq!(c.curr().message_or_name(), Some("foo")); } #[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")); 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")); assert!(c.prev(&f)); assert!(c.enter(&f)); assert_eq!(c.curr().message_or_name(), Some("baz")); 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")); 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")); 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")); } #[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()), span: SpanDescriptor::Main, }, kind: FilterKind::Remove, })); assert!(c.next(&f)); assert_eq!(c.curr().message_or_name(), Some("baz")); 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()), span: SpanDescriptor::Main, }, kind: FilterKind::Remove, })); assert_eq!(c.curr().message_or_name(), Some("foo")); assert!(c.next(&f)); assert_eq!(c.curr().message_or_name(), Some("meow")); assert!(c.prev(&f)); assert_eq!(c.curr().message_or_name(), Some("foo")); 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()), span: SpanDescriptor::Main, }, kind: FilterKind::Inline, })); assert_eq!(c.curr().message_or_name(), Some("foo")); assert!(c.next(&f)); assert_eq!(c.curr().message_or_name(), Some("baz")); assert!(c.next(&f)); assert!(!c.exit(&f)); assert_eq!(c.curr().message_or_name(), Some("meow")); assert!(c.next(&f)); assert_eq!(c.curr().message_or_name(), Some("bar")); assert!(c.prev(&f)); assert_eq!(c.curr().message_or_name(), Some("meow")); assert!(!c.exit(&f)); assert!(c.prev(&f)); assert_eq!(c.curr().message_or_name(), Some("baz")); assert!(c.prev(&f)); assert_eq!(c.curr().message_or_name(), Some("foo")); 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")); assert!(c.next(&f)); assert!(c.enter(&f)); assert_eq!(c.curr().message_or_name(), Some("baz")); // inline the current item f.push(Arc::new(Filter { matcher: Matcher::Field { name: "name".to_string(), value: MatcherValue::Exact("nest".to_string()), span: SpanDescriptor::Main, }, kind: FilterKind::Inline, })); c.update_with_parents(&f); assert_eq!(c.curr().message_or_name(), Some("baz")); println!("undo"); f.undo(); c.update_with_parents(&f); assert_eq!(c.curr().message_or_name(), Some("enter")); assert!(c.next(&f)); assert_eq!(c.curr().message_or_name(), Some("bar")); assert!(!c.next(&f)); assert!(c.prev(&f)); assert!(c.enter(&f)); assert_eq!(c.curr().message_or_name(), Some("baz")); assert!(c.next(&f)); assert_eq!(c.curr().message_or_name(), Some("meow")); 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")); assert!(c.prev(&f)); assert_eq!(c.curr().message_or_name(), Some("foo")); assert!(!c.prev(&f)); } #[test] fn inline_depth() { let mut c = parse( &[ with_fields(r#"{"message": "enter", "name": "nest1"}"#), with_fields(r#"{"message": "enter", "name": "nest2"}"#), with_fields(r#"{"message": "baz"}"#), with_fields(r#"{"message": "exit"}"#), with_fields(r#"{"message": "exit"}"#), ] .join("\n"), ); let mut f = filters(); c.enter(&f); c.enter(&f); assert_eq!(c.curr().message_or_name(), Some("baz")); c.exit(&f); c.exit(&f); f.push(Arc::new(Filter { matcher: Matcher::Field { name: "name".to_string(), value: MatcherValue::Exact("nest1".to_string()), span: SpanDescriptor::Main, }, kind: FilterKind::Inline, })); f.push(Arc::new(Filter { matcher: Matcher::Field { name: "name".to_string(), value: MatcherValue::Exact("nest2".to_string()), span: SpanDescriptor::Main, }, kind: FilterKind::Inline, })); c.update_with_parents(&f); assert_eq!(c.curr().message_or_name(), Some("baz")); } }