1use regex::Regex;
19
20use crate::util::truncate_for_log;
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24#[non_exhaustive]
25pub enum EntryHeader {
26 UnityCrossThreadLogger,
29 ClientGre,
31 ConnectionManager,
36 Matchmaking,
41 Metadata,
46}
47
48impl EntryHeader {
49 pub fn as_str(self) -> &'static str {
55 match self {
56 Self::UnityCrossThreadLogger => "[UnityCrossThreadLogger]",
57 Self::ClientGre => "[Client GRE]",
58 Self::ConnectionManager => "[ConnectionManager]",
59 Self::Matchmaking => "Matchmaking:",
60 Self::Metadata => "METADATA",
61 }
62 }
63}
64
65impl std::fmt::Display for EntryHeader {
66 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
67 f.write_str(self.as_str())
68 }
69}
70
71#[derive(Debug, Clone, PartialEq, Eq)]
76pub struct LogEntry {
77 pub header: EntryHeader,
79 pub body: String,
82}
83
84pub struct LineBuffer {
118 header_re: Regex,
120 current_header: Option<EntryHeader>,
122 lines: Vec<String>,
124}
125
126impl LineBuffer {
127 pub fn new() -> Self {
129 let header_re =
133 match Regex::new(r"^\[(UnityCrossThreadLogger|Client GRE|ConnectionManager)\]") {
134 Ok(re) => re,
135 Err(e) => unreachable!("invalid header regex: {e}"),
136 };
137 Self {
138 header_re,
139 current_header: None,
140 lines: Vec::new(),
141 }
142 }
143
144 pub fn push_line(&mut self, line: &str) -> Option<LogEntry> {
161 if Self::is_metadata_line(line) {
163 let flushed = self.take_entry();
164 let metadata_entry = LogEntry {
165 header: EntryHeader::Metadata,
166 body: line.to_owned(),
167 };
168 if flushed.is_some() {
173 self.current_header = Some(EntryHeader::Metadata);
174 self.lines.push(line.to_owned());
175 return flushed;
176 }
177 return Some(metadata_entry);
179 }
180
181 if let Some(header) = self.detect_header(line) {
182 let flushed = self.take_entry();
183 self.current_header = Some(header);
184 self.lines.push(line.to_owned());
185 flushed
186 } else if self.current_header.is_some() {
187 self.lines.push(line.to_owned());
189 None
190 } else {
191 ::log::warn!(
193 "Discarding headerless line at start of input: {:?}",
194 truncate_for_log(line, 120),
195 );
196 None
197 }
198 }
199
200 pub fn flush(&mut self) -> Option<LogEntry> {
205 self.take_entry()
206 }
207
208 pub fn reset(&mut self) {
213 self.current_header = None;
214 self.lines.clear();
215 }
216
217 pub fn is_empty(&self) -> bool {
219 self.current_header.is_none()
220 }
221
222 fn is_metadata_line(line: &str) -> bool {
228 let trimmed = line.trim();
229 trimmed == "DETAILED LOGS: ENABLED" || trimmed == "DETAILED LOGS: DISABLED"
230 }
231
232 fn detect_header(&self, line: &str) -> Option<EntryHeader> {
239 if let Some(caps) = self.header_re.captures(line) {
240 let prefix = caps.get(1)?.as_str();
241 return match prefix {
242 "UnityCrossThreadLogger" => Some(EntryHeader::UnityCrossThreadLogger),
243 "Client GRE" => Some(EntryHeader::ClientGre),
244 "ConnectionManager" => Some(EntryHeader::ConnectionManager),
245 _ => None,
246 };
247 }
248 if line.starts_with("Matchmaking: ") {
249 return Some(EntryHeader::Matchmaking);
250 }
251 None
252 }
253
254 fn take_entry(&mut self) -> Option<LogEntry> {
256 let header = self.current_header.take()?;
257 let body = self.lines.join("\n");
258 self.lines.clear();
259 Some(LogEntry { header, body })
260 }
261}
262
263impl Default for LineBuffer {
264 fn default() -> Self {
265 Self::new()
266 }
267}
268
269#[cfg(test)]
274mod tests {
275 use super::*;
276
277 fn expected(header: EntryHeader, body: &str) -> LogEntry {
282 LogEntry {
283 header,
284 body: body.to_owned(),
285 }
286 }
287
288 mod entry_header {
291 use super::*;
292
293 #[test]
294 fn test_as_str_unity() {
295 assert_eq!(
296 EntryHeader::UnityCrossThreadLogger.as_str(),
297 "[UnityCrossThreadLogger]"
298 );
299 }
300
301 #[test]
302 fn test_as_str_client_gre() {
303 assert_eq!(EntryHeader::ClientGre.as_str(), "[Client GRE]");
304 }
305
306 #[test]
307 fn test_display_unity() {
308 assert_eq!(
309 EntryHeader::UnityCrossThreadLogger.to_string(),
310 "[UnityCrossThreadLogger]"
311 );
312 }
313
314 #[test]
315 fn test_display_client_gre() {
316 assert_eq!(EntryHeader::ClientGre.to_string(), "[Client GRE]");
317 }
318
319 #[test]
320 fn test_clone_and_eq() {
321 let a = EntryHeader::UnityCrossThreadLogger;
322 let b = a;
323 assert_eq!(a, b);
324 }
325 }
326
327 mod push_line {
330 use super::*;
331
332 #[test]
333 fn test_push_line_first_header_returns_none() {
334 let mut buf = LineBuffer::new();
335 assert!(buf
336 .push_line("[UnityCrossThreadLogger] 1/1/2025 12:00:00 Event")
337 .is_none());
338 }
339
340 #[test]
341 fn test_push_line_second_header_flushes_first_entry() {
342 let mut buf = LineBuffer::new();
343 buf.push_line("[UnityCrossThreadLogger] 1/1/2025 Event1");
344 assert_eq!(
345 buf.push_line("[Client GRE] 1/1/2025 Event2"),
346 Some(expected(
347 EntryHeader::UnityCrossThreadLogger,
348 "[UnityCrossThreadLogger] 1/1/2025 Event1",
349 )),
350 );
351 }
352
353 #[test]
354 fn test_push_line_continuation_appended() {
355 let mut buf = LineBuffer::new();
356 buf.push_line("[UnityCrossThreadLogger] 1/1/2025 Event1");
357 buf.push_line(r#"{"key": "value"}"#);
358 buf.push_line(r#"{"more": "data"}"#);
359 assert_eq!(
360 buf.push_line("[UnityCrossThreadLogger] 1/1/2025 Event2"),
361 Some(expected(
362 EntryHeader::UnityCrossThreadLogger,
363 "[UnityCrossThreadLogger] 1/1/2025 Event1\n\
364 {\"key\": \"value\"}\n\
365 {\"more\": \"data\"}",
366 )),
367 );
368 }
369
370 #[test]
371 fn test_push_line_client_gre_header_detected() {
372 let mut buf = LineBuffer::new();
373 buf.push_line("[Client GRE] GreMessage");
374 assert_eq!(
375 buf.flush(),
376 Some(expected(EntryHeader::ClientGre, "[Client GRE] GreMessage")),
377 );
378 }
379
380 #[test]
381 fn test_push_line_alternating_headers() {
382 let mut buf = LineBuffer::new();
383 buf.push_line("[UnityCrossThreadLogger] Event1");
384
385 assert_eq!(
386 buf.push_line("[Client GRE] Event2"),
387 Some(expected(
388 EntryHeader::UnityCrossThreadLogger,
389 "[UnityCrossThreadLogger] Event1",
390 )),
391 );
392
393 assert_eq!(
394 buf.push_line("[UnityCrossThreadLogger] Event3"),
395 Some(expected(EntryHeader::ClientGre, "[Client GRE] Event2")),
396 );
397
398 assert_eq!(
399 buf.flush(),
400 Some(expected(
401 EntryHeader::UnityCrossThreadLogger,
402 "[UnityCrossThreadLogger] Event3",
403 )),
404 );
405 }
406 }
407
408 mod headerless {
411 use super::*;
412
413 #[test]
414 fn test_push_line_headerless_before_first_header_returns_none() {
415 let mut buf = LineBuffer::new();
416 assert!(buf.push_line("some random line").is_none());
417 assert!(buf.push_line("another orphan").is_none());
418 buf.push_line("[UnityCrossThreadLogger] Real entry");
420 assert_eq!(
421 buf.flush(),
422 Some(expected(
423 EntryHeader::UnityCrossThreadLogger,
424 "[UnityCrossThreadLogger] Real entry",
425 )),
426 );
427 }
428
429 #[test]
430 fn test_push_line_empty_line_as_continuation() {
431 let mut buf = LineBuffer::new();
432 buf.push_line("[UnityCrossThreadLogger] Event");
433 buf.push_line("");
434 buf.push_line("continuation");
435 assert_eq!(
436 buf.flush(),
437 Some(expected(
438 EntryHeader::UnityCrossThreadLogger,
439 "[UnityCrossThreadLogger] Event\n\ncontinuation",
440 )),
441 );
442 }
443 }
444
445 mod flush {
448 use super::*;
449
450 #[test]
451 fn test_flush_empty_buffer_returns_none() {
452 let mut buf = LineBuffer::new();
453 assert!(buf.flush().is_none());
454 }
455
456 #[test]
457 fn test_flush_returns_buffered_entry() {
458 let mut buf = LineBuffer::new();
459 buf.push_line("[UnityCrossThreadLogger] Event");
460 assert_eq!(
461 buf.flush(),
462 Some(expected(
463 EntryHeader::UnityCrossThreadLogger,
464 "[UnityCrossThreadLogger] Event",
465 )),
466 );
467 }
468
469 #[test]
470 fn test_flush_clears_buffer() {
471 let mut buf = LineBuffer::new();
472 buf.push_line("[UnityCrossThreadLogger] Event");
473 buf.flush();
474 assert!(buf.flush().is_none());
475 assert!(buf.is_empty());
476 }
477
478 #[test]
479 fn test_flush_multi_line_entry() {
480 let mut buf = LineBuffer::new();
481 buf.push_line("[Client GRE] GreToClientEvent");
482 buf.push_line("{");
483 buf.push_line(r#" "gameObjects": ["obj1", "obj2"],"#);
484 buf.push_line(r#" "actions": []"#);
485 buf.push_line("}");
486 let expected_body = [
487 "[Client GRE] GreToClientEvent",
488 "{",
489 r#" "gameObjects": ["obj1", "obj2"],"#,
490 r#" "actions": []"#,
491 "}",
492 ]
493 .join("\n");
494 assert_eq!(
495 buf.flush(),
496 Some(expected(EntryHeader::ClientGre, &expected_body)),
497 );
498 }
499 }
500
501 mod reset {
504 use super::*;
505
506 #[test]
507 fn test_reset_clears_in_progress_entry() {
508 let mut buf = LineBuffer::new();
509 buf.push_line("[UnityCrossThreadLogger] Event");
510 buf.push_line("continuation");
511 buf.reset();
512 assert!(buf.is_empty());
513 assert!(buf.flush().is_none());
514 }
515
516 #[test]
517 fn test_reset_allows_fresh_accumulation() {
518 let mut buf = LineBuffer::new();
519 buf.push_line("[UnityCrossThreadLogger] Old");
520 buf.reset();
521 buf.push_line("[Client GRE] New");
522 assert_eq!(
523 buf.flush(),
524 Some(expected(EntryHeader::ClientGre, "[Client GRE] New")),
525 );
526 }
527 }
528
529 mod is_empty {
532 use super::*;
533
534 #[test]
535 fn test_is_empty_on_new_buffer() {
536 let buf = LineBuffer::new();
537 assert!(buf.is_empty());
538 }
539
540 #[test]
541 fn test_is_empty_false_after_header() {
542 let mut buf = LineBuffer::new();
543 buf.push_line("[UnityCrossThreadLogger] Event");
544 assert!(!buf.is_empty());
545 }
546
547 #[test]
548 fn test_is_empty_true_after_flush() {
549 let mut buf = LineBuffer::new();
550 buf.push_line("[UnityCrossThreadLogger] Event");
551 buf.flush();
552 assert!(buf.is_empty());
553 }
554
555 #[test]
556 fn test_is_empty_true_after_headerless_lines() {
557 let mut buf = LineBuffer::new();
558 buf.push_line("orphan line");
559 assert!(buf.is_empty());
560 }
561 }
562
563 mod default_impl {
566 use super::*;
567
568 #[test]
569 fn test_default_creates_functional_buffer() {
570 let mut buf = LineBuffer::default();
571 buf.push_line("[UnityCrossThreadLogger] Event");
572 assert_eq!(
573 buf.flush(),
574 Some(expected(
575 EntryHeader::UnityCrossThreadLogger,
576 "[UnityCrossThreadLogger] Event",
577 )),
578 );
579 }
580 }
581
582 mod header_detection {
585 use super::*;
586
587 #[test]
588 fn test_header_not_at_start_of_line_is_continuation() {
589 let mut buf = LineBuffer::new();
590 buf.push_line("[UnityCrossThreadLogger] Event");
591 buf.push_line("some text [UnityCrossThreadLogger] not a header");
593 assert_eq!(
594 buf.flush(),
595 Some(expected(
596 EntryHeader::UnityCrossThreadLogger,
597 "[UnityCrossThreadLogger] Event\n\
598 some text [UnityCrossThreadLogger] not a header",
599 )),
600 );
601 }
602
603 #[test]
604 fn test_similar_but_wrong_header_is_continuation() {
605 let mut buf = LineBuffer::new();
606 buf.push_line("[UnityCrossThreadLogger] Event");
607 buf.push_line("[UnityMainThreadLogger] not a valid header");
608 let result = buf.flush();
609 assert!(result.is_some());
610 if let Some(e) = result {
611 assert!(e.body.contains("[UnityMainThreadLogger]"));
612 }
613 }
614
615 #[test]
616 fn test_bracket_only_is_not_header() {
617 let mut buf = LineBuffer::new();
618 buf.push_line("[UnityCrossThreadLogger] Event");
619 buf.push_line("[]");
620 assert_eq!(
621 buf.flush(),
622 Some(expected(
623 EntryHeader::UnityCrossThreadLogger,
624 "[UnityCrossThreadLogger] Event\n[]",
625 )),
626 );
627 }
628
629 #[test]
630 fn test_header_with_nothing_after_bracket() {
631 let mut buf = LineBuffer::new();
632 buf.push_line("[UnityCrossThreadLogger]");
634 assert_eq!(
635 buf.flush(),
636 Some(expected(
637 EntryHeader::UnityCrossThreadLogger,
638 "[UnityCrossThreadLogger]",
639 )),
640 );
641 }
642 }
643
644 mod realistic_entries {
647 use super::*;
648
649 #[test]
650 fn test_realistic_game_state_message() {
651 let mut buf = LineBuffer::new();
652 buf.push_line(
653 "[UnityCrossThreadLogger]1/15/2025 3:42:17 PM \
654 greToClientEvent",
655 );
656 buf.push_line("{");
657 buf.push_line(r#" "greToClientMessages": ["#);
658 buf.push_line(r" {");
659 buf.push_line(r#" "type": "GREMessageType_GameStateMessage","#);
660 buf.push_line(r#" "gameStateMessage": {"#);
661 buf.push_line(r#" "gameObjects": []"#);
662 buf.push_line(r" }");
663 buf.push_line(r" }");
664 buf.push_line(r" ]");
665 buf.push_line("}");
666
667 let unity_entry = buf.push_line("[Client GRE] Next event");
669 assert!(unity_entry.is_some());
670 if let Some(e) = unity_entry {
671 assert_eq!(e.header, EntryHeader::UnityCrossThreadLogger);
672 assert!(e.body.contains("greToClientMessages"));
673 assert!(e.body.contains("GameStateMessage"));
674 }
675
676 assert_eq!(
678 buf.push_line("[UnityCrossThreadLogger] After"),
679 Some(expected(EntryHeader::ClientGre, "[Client GRE] Next event",)),
680 );
681 }
682
683 #[test]
684 fn test_many_entries_in_sequence() {
685 let mut buf = LineBuffer::new();
686 let mut entries = Vec::new();
687
688 for i in 0..5 {
689 if let Some(e) = buf.push_line(&format!("[UnityCrossThreadLogger] Event{i}")) {
690 entries.push(e);
691 }
692 }
693 if let Some(e) = buf.flush() {
694 entries.push(e);
695 }
696
697 assert_eq!(entries.len(), 5);
698 for (i, e) in entries.iter().enumerate() {
699 assert_eq!(e.header, EntryHeader::UnityCrossThreadLogger);
700 assert_eq!(e.body, format!("[UnityCrossThreadLogger] Event{i}"));
701 }
702 }
703 }
704
705 mod metadata_lines {
708 use super::*;
709
710 #[test]
711 fn test_push_line_detailed_logs_enabled_as_first_line() {
712 let mut buf = LineBuffer::new();
713 let result = buf.push_line("DETAILED LOGS: ENABLED");
714
715 assert_eq!(
716 result,
717 Some(expected(EntryHeader::Metadata, "DETAILED LOGS: ENABLED")),
718 );
719 assert!(buf.is_empty());
721 }
722
723 #[test]
724 fn test_push_line_detailed_logs_disabled_as_first_line() {
725 let mut buf = LineBuffer::new();
726 let result = buf.push_line("DETAILED LOGS: DISABLED");
727
728 assert_eq!(
729 result,
730 Some(expected(EntryHeader::Metadata, "DETAILED LOGS: DISABLED")),
731 );
732 }
733
734 #[test]
735 fn test_push_line_metadata_flushes_buffered_entry() {
736 let mut buf = LineBuffer::new();
737 buf.push_line("[UnityCrossThreadLogger] Event1");
738
739 let flushed = buf.push_line("DETAILED LOGS: ENABLED");
741 assert_eq!(
742 flushed,
743 Some(expected(
744 EntryHeader::UnityCrossThreadLogger,
745 "[UnityCrossThreadLogger] Event1",
746 )),
747 );
748
749 let metadata = buf.flush();
751 assert_eq!(
752 metadata,
753 Some(expected(EntryHeader::Metadata, "DETAILED LOGS: ENABLED")),
754 );
755 }
756
757 #[test]
758 fn test_push_line_metadata_then_header_flushes_metadata() {
759 let mut buf = LineBuffer::new();
760 buf.push_line("DETAILED LOGS: ENABLED");
761
762 assert!(buf.push_line("[UnityCrossThreadLogger] Event").is_none());
765 assert_eq!(
766 buf.flush(),
767 Some(expected(
768 EntryHeader::UnityCrossThreadLogger,
769 "[UnityCrossThreadLogger] Event",
770 )),
771 );
772 }
773
774 #[test]
775 fn test_push_line_metadata_buffered_then_next_header_flushes() {
776 let mut buf = LineBuffer::new();
777 buf.push_line("[UnityCrossThreadLogger] Event1");
778
779 buf.push_line("DETAILED LOGS: DISABLED");
781
782 let flushed = buf.push_line("[UnityCrossThreadLogger] Event2");
784 assert_eq!(
785 flushed,
786 Some(expected(EntryHeader::Metadata, "DETAILED LOGS: DISABLED")),
787 );
788 }
789
790 #[test]
791 fn test_push_line_metadata_similar_text_not_matched() {
792 let mut buf = LineBuffer::new();
793 assert!(buf.push_line("DETAILED LOGS: UNKNOWN").is_none());
795 assert!(buf.push_line("detailed logs: enabled").is_none());
796 assert!(buf.push_line("DETAILED LOGS:ENABLED").is_none());
797 }
798
799 #[test]
800 fn test_push_line_metadata_with_leading_trailing_whitespace() {
801 let mut buf = LineBuffer::new();
802 let result = buf.push_line(" DETAILED LOGS: ENABLED ");
804 assert!(result.is_some());
805 if let Some(entry) = result {
806 assert_eq!(entry.header, EntryHeader::Metadata);
807 }
808 }
809
810 #[test]
811 fn test_entry_header_metadata_as_str() {
812 assert_eq!(EntryHeader::Metadata.as_str(), "METADATA");
813 }
814
815 #[test]
816 fn test_entry_header_metadata_display() {
817 assert_eq!(EntryHeader::Metadata.to_string(), "METADATA");
818 }
819 }
820
821 mod connection_and_matchmaking_headers {
824 use super::*;
825
826 #[test]
827 fn test_as_str_connection_manager() {
828 assert_eq!(
829 EntryHeader::ConnectionManager.as_str(),
830 "[ConnectionManager]"
831 );
832 }
833
834 #[test]
835 fn test_as_str_matchmaking() {
836 assert_eq!(EntryHeader::Matchmaking.as_str(), "Matchmaking:");
839 }
840
841 #[test]
842 fn test_display_connection_manager() {
843 assert_eq!(
844 EntryHeader::ConnectionManager.to_string(),
845 "[ConnectionManager]"
846 );
847 }
848
849 #[test]
850 fn test_display_matchmaking() {
851 assert_eq!(EntryHeader::Matchmaking.to_string(), "Matchmaking:");
852 }
853
854 #[test]
855 fn test_connection_manager_header_mid_stream_flushes_unity() {
856 let mut buf = LineBuffer::new();
857 buf.push_line("[UnityCrossThreadLogger] 1/1/2025 Event1");
858
859 let flushed = buf.push_line("[ConnectionManager] Reconnect result : Error");
860 assert_eq!(
861 flushed,
862 Some(expected(
863 EntryHeader::UnityCrossThreadLogger,
864 "[UnityCrossThreadLogger] 1/1/2025 Event1",
865 )),
866 );
867
868 assert_eq!(
870 buf.flush(),
871 Some(expected(
872 EntryHeader::ConnectionManager,
873 "[ConnectionManager] Reconnect result : Error",
874 )),
875 );
876 }
877
878 #[test]
879 fn test_matchmaking_header_mid_stream_flushes_unity() {
880 let mut buf = LineBuffer::new();
881 buf.push_line("[UnityCrossThreadLogger] 1/1/2025 Event1");
882
883 let flushed = buf.push_line("Matchmaking: GRE connection lost");
884 assert_eq!(
885 flushed,
886 Some(expected(
887 EntryHeader::UnityCrossThreadLogger,
888 "[UnityCrossThreadLogger] 1/1/2025 Event1",
889 )),
890 );
891
892 assert_eq!(
893 buf.flush(),
894 Some(expected(
895 EntryHeader::Matchmaking,
896 "Matchmaking: GRE connection lost",
897 )),
898 );
899 }
900
901 #[test]
902 fn test_connection_manager_as_first_line_no_warning_emitted() {
903 let mut buf = LineBuffer::new();
908 assert!(buf
909 .push_line("[ConnectionManager] Reconnect succeeded")
910 .is_none());
911 assert!(!buf.is_empty());
912 assert_eq!(
913 buf.flush(),
914 Some(expected(
915 EntryHeader::ConnectionManager,
916 "[ConnectionManager] Reconnect succeeded",
917 )),
918 );
919 }
920
921 #[test]
922 fn test_matchmaking_as_first_line_no_warning_emitted() {
923 let mut buf = LineBuffer::new();
924 assert!(buf.push_line("Matchmaking: GRE connection lost").is_none());
925 assert!(!buf.is_empty());
926 assert_eq!(
927 buf.flush(),
928 Some(expected(
929 EntryHeader::Matchmaking,
930 "Matchmaking: GRE connection lost",
931 )),
932 );
933 }
934
935 #[test]
936 fn test_four_way_interleave_yields_four_entries() {
937 let mut buf = LineBuffer::new();
941 let mut entries = Vec::new();
942
943 if let Some(e) = buf.push_line(
944 "[UnityCrossThreadLogger]STATE CHANGED {\"old\":\"Playing\",\"new\":\"Disconnected\"}",
945 ) {
946 entries.push(e);
947 }
948 if let Some(e) = buf.push_line("Matchmaking: GRE connection lost") {
949 entries.push(e);
950 }
951 if let Some(e) = buf.push_line("[ConnectionManager] Reconnect result : Error") {
952 entries.push(e);
953 }
954 if let Some(e) = buf.push_line("[UnityCrossThreadLogger] Next event") {
955 entries.push(e);
956 }
957 if let Some(e) = buf.flush() {
958 entries.push(e);
959 }
960
961 assert_eq!(entries.len(), 4);
962 assert_eq!(entries[0].header, EntryHeader::UnityCrossThreadLogger);
963 assert!(entries[0].body.contains("STATE CHANGED"));
964 assert_eq!(entries[1].header, EntryHeader::Matchmaking);
965 assert_eq!(entries[1].body, "Matchmaking: GRE connection lost");
966 assert_eq!(entries[2].header, EntryHeader::ConnectionManager);
967 assert_eq!(
968 entries[2].body,
969 "[ConnectionManager] Reconnect result : Error"
970 );
971 assert_eq!(entries[3].header, EntryHeader::UnityCrossThreadLogger);
972 assert_eq!(entries[3].body, "[UnityCrossThreadLogger] Next event");
973 }
974
975 #[test]
976 fn test_connection_manager_accumulates_continuation_lines() {
977 let mut buf = LineBuffer::new();
980 buf.push_line("[ConnectionManager] Reconnect result : Error");
981 buf.push_line(" extra detail line");
982 buf.push_line(" another detail line");
983
984 assert_eq!(
985 buf.flush(),
986 Some(expected(
987 EntryHeader::ConnectionManager,
988 "[ConnectionManager] Reconnect result : Error\n extra detail line\n another detail line",
989 )),
990 );
991 }
992
993 #[test]
994 fn test_matchmaking_accumulates_continuation_lines() {
995 let mut buf = LineBuffer::new();
996 buf.push_line("Matchmaking: GRE connection lost");
997 buf.push_line("extra continuation");
998
999 assert_eq!(
1000 buf.flush(),
1001 Some(expected(
1002 EntryHeader::Matchmaking,
1003 "Matchmaking: GRE connection lost\nextra continuation",
1004 )),
1005 );
1006 }
1007
1008 #[test]
1009 fn test_matchmaking_without_trailing_space_is_not_header() {
1010 let mut buf = LineBuffer::new();
1015 assert!(buf.push_line("Matchmaking:compact-no-space").is_none());
1016 assert!(buf.is_empty());
1017 }
1018
1019 #[test]
1020 fn test_connection_manager_mid_line_is_continuation() {
1021 let mut buf = LineBuffer::new();
1022 buf.push_line("[UnityCrossThreadLogger] Event");
1023 buf.push_line("some text [ConnectionManager] not a header");
1026 assert_eq!(
1027 buf.flush(),
1028 Some(expected(
1029 EntryHeader::UnityCrossThreadLogger,
1030 "[UnityCrossThreadLogger] Event\n\
1031 some text [ConnectionManager] not a header",
1032 )),
1033 );
1034 }
1035 }
1036}