@@ -616,3 +616,133 @@ fn test_detect_format_does_not_select_dlt_for_non_dlt() {
616616 let parser = detect_format ( & lines) . unwrap ( ) ;
617617 assert_ne ! ( parser. name( ) , "dlt" ) ;
618618}
619+
620+ // ---------------------------------------------------------------------------
621+ // Multiline / continuation-line tests
622+ // ---------------------------------------------------------------------------
623+
624+ /// Build a FileReader from a multiline log string with Java-style stack traces.
625+ fn make_multiline_log ( ) -> FileReader {
626+ // Line 0: ERROR entry with timestamp (parent)
627+ // Line 1: stack frame (continuation)
628+ // Line 2: stack frame (continuation)
629+ // Line 3: INFO entry with timestamp (standalone parent)
630+ let data = b"\
631+ 2024-01-15 10:00:00 ERROR com.example.App - NullPointerException\n \
632+ at com.example.Foo.bar(Foo.java:42)\n \
633+ at com.example.Main.main(Main.java:10)\n \
634+ 2024-01-16 11:00:00 INFO com.example.App - Application started\n ";
635+ FileReader :: from_bytes ( data. to_vec ( ) )
636+ }
637+
638+ #[ test]
639+ fn test_build_continuation_map_basic ( ) {
640+ use logana:: parser:: detect_format;
641+ use logana:: ui:: build_continuation_map;
642+
643+ let reader = make_multiline_log ( ) ;
644+ let sample: Vec < & [ u8 ] > = ( 0 ..reader. line_count ( ) ) . map ( |i| reader. get_line ( i) ) . collect ( ) ;
645+ let parser = detect_format ( & sample) . expect ( "format should be detected" ) ;
646+
647+ let cmap = build_continuation_map ( & reader, parser. as_ref ( ) ) ;
648+
649+ assert_eq ! ( cmap. len( ) , 4 ) ;
650+ assert_eq ! ( cmap[ 0 ] , 0 , "line 0 is its own parent" ) ;
651+ assert_eq ! ( cmap[ 1 ] , 0 , "line 1 is a continuation of line 0" ) ;
652+ assert_eq ! ( cmap[ 2 ] , 0 , "line 2 is a continuation of line 0" ) ;
653+ assert_eq ! ( cmap[ 3 ] , 3 , "line 3 is its own parent" ) ;
654+ }
655+
656+ #[ test]
657+ fn test_apply_continuation_correction_hides_orphaned_continuations ( ) {
658+ use logana:: ui:: { VisibleLines , apply_continuation_correction} ;
659+
660+ // Simulate: filter kept only line 3 (INFO); lines 0, 1, 2 were filtered.
661+ // Without correction, continuation lines 1 and 2 might still be visible.
662+ // With correction they must be hidden because their parent (line 0) is hidden.
663+ let cmap = vec ! [ 0usize , 0 , 0 , 3 ] ; // lines 1,2 → parent 0; line 3 → parent 3
664+ let mut visible = VisibleLines :: Filtered ( vec ! [ 1 , 2 , 3 ] ) ; // lines 1 & 2 erroneously visible
665+ apply_continuation_correction ( & mut visible, & cmap) ;
666+ // line 0 was NOT in visible, so its continuations (1, 2) must be removed.
667+ // line 3 is its own parent and was visible → stays.
668+ assert_eq ! ( visible, VisibleLines :: Filtered ( vec![ 3 ] ) ) ;
669+ }
670+
671+ #[ test]
672+ fn test_apply_continuation_correction_keeps_continuations_with_parent ( ) {
673+ use logana:: ui:: { VisibleLines , apply_continuation_correction} ;
674+
675+ // Simulate: filter kept line 0 (ERROR parent); continuations 1 and 2 also passed.
676+ let cmap = vec ! [ 0usize , 0 , 0 , 3 ] ;
677+ let mut visible = VisibleLines :: Filtered ( vec ! [ 0 , 1 , 2 , 3 ] ) ;
678+ apply_continuation_correction ( & mut visible, & cmap) ;
679+ // Parent 0 is visible → continuations 1 and 2 stay visible.
680+ assert_eq ! ( visible, VisibleLines :: Filtered ( vec![ 0 , 1 , 2 , 3 ] ) ) ;
681+ }
682+
683+ #[ test]
684+ fn test_apply_continuation_correction_noop_for_all_variant ( ) {
685+ use logana:: ui:: { VisibleLines , apply_continuation_correction} ;
686+
687+ let cmap = vec ! [ 0usize , 0 , 1 ] ;
688+ let mut visible = VisibleLines :: All ( 3 ) ;
689+ apply_continuation_correction ( & mut visible, & cmap) ;
690+ // All-variant must remain unchanged.
691+ assert_eq ! ( visible, VisibleLines :: All ( 3 ) ) ;
692+ }
693+
694+ #[ tokio:: test]
695+ async fn test_exclude_filter_hides_continuation_lines ( ) {
696+ use logana:: filters:: FilterType ;
697+ use logana:: parser:: detect_format;
698+ use logana:: ui:: { VisibleLines , apply_continuation_correction, build_continuation_map} ;
699+
700+ let ( _db, mut manager) = setup ( ) . await ;
701+ let reader = make_multiline_log ( ) ;
702+ let sample: Vec < & [ u8 ] > = ( 0 ..reader. line_count ( ) ) . map ( |i| reader. get_line ( i) ) . collect ( ) ;
703+ let parser = detect_format ( & sample) . expect ( "format detected" ) ;
704+ let cmap = build_continuation_map ( & reader, parser. as_ref ( ) ) ;
705+
706+ // Exclude lines containing "ERROR" — should hide line 0 AND its continuations
707+ manager
708+ . add_filter_with_color ( "ERROR" . into ( ) , FilterType :: Exclude , None , None , true )
709+ . await ;
710+ let ( fm, _, _, _) = manager. build_filter_manager ( ) ;
711+ let mut visible = VisibleLines :: Filtered ( fm. compute_visible ( & reader) ) ;
712+ apply_continuation_correction ( & mut visible, & cmap) ;
713+
714+ // Line 0 (ERROR) excluded; lines 1 & 2 are its continuations → also excluded.
715+ // Line 3 (INFO) should be visible.
716+ assert ! ( !visible. contains( 0 ) , "ERROR entry should be hidden" ) ;
717+ assert ! ( !visible. contains( 1 ) , "continuation 1 should be hidden with parent" ) ;
718+ assert ! ( !visible. contains( 2 ) , "continuation 2 should be hidden with parent" ) ;
719+ assert ! ( visible. contains( 3 ) , "INFO entry should be visible" ) ;
720+ }
721+
722+ #[ tokio:: test]
723+ async fn test_include_filter_shows_continuations_with_parent ( ) {
724+ use logana:: filters:: FilterType ;
725+ use logana:: parser:: detect_format;
726+ use logana:: ui:: { VisibleLines , apply_continuation_correction, build_continuation_map} ;
727+
728+ let ( _db, mut manager) = setup ( ) . await ;
729+ let reader = make_multiline_log ( ) ;
730+ let sample: Vec < & [ u8 ] > = ( 0 ..reader. line_count ( ) ) . map ( |i| reader. get_line ( i) ) . collect ( ) ;
731+ let parser = detect_format ( & sample) . expect ( "format detected" ) ;
732+ let cmap = build_continuation_map ( & reader, parser. as_ref ( ) ) ;
733+
734+ // Include only lines containing "ERROR" — parent matches; continuations should follow.
735+ manager
736+ . add_filter_with_color ( "ERROR" . into ( ) , FilterType :: Include , None , None , true )
737+ . await ;
738+ let ( fm, _, _, _) = manager. build_filter_manager ( ) ;
739+ let mut visible = VisibleLines :: Filtered ( fm. compute_visible ( & reader) ) ;
740+ apply_continuation_correction ( & mut visible, & cmap) ;
741+
742+ // Line 0 matches; its continuations (1, 2) should be shown.
743+ // Line 3 (INFO) does not match include → hidden.
744+ assert ! ( visible. contains( 0 ) , "ERROR entry should be visible" ) ;
745+ assert ! ( visible. contains( 1 ) , "continuation 1 should follow its visible parent" ) ;
746+ assert ! ( visible. contains( 2 ) , "continuation 2 should follow its visible parent" ) ;
747+ assert ! ( !visible. contains( 3 ) , "INFO entry should be hidden (no match)" ) ;
748+ }
0 commit comments