1use std::cell::RefCell;
8use std::io::{self, IsTerminal, Stdout};
9use std::rc::Rc;
10
11#[cfg(any(test, feature = "test-utils"))]
12use std::io::Stderr;
13
14pub trait Printable: std::io::Write {
20 fn is_terminal(&self) -> bool;
25}
26
27#[derive(Debug)]
29pub struct StdoutPrinter {
30 stdout: Stdout,
31 is_terminal: bool,
32}
33
34impl StdoutPrinter {
35 pub fn new() -> Self {
37 let is_terminal = std::io::stdout().is_terminal();
38 Self {
39 stdout: std::io::stdout(),
40 is_terminal,
41 }
42 }
43}
44
45impl Default for StdoutPrinter {
46 fn default() -> Self {
47 Self::new()
48 }
49}
50
51impl std::io::Write for StdoutPrinter {
52 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
53 self.stdout.write(buf)
54 }
55
56 fn flush(&mut self) -> io::Result<()> {
57 self.stdout.flush()
58 }
59}
60
61impl Printable for StdoutPrinter {
62 fn is_terminal(&self) -> bool {
63 self.is_terminal
64 }
65}
66
67#[derive(Debug)]
69#[cfg(any(test, feature = "test-utils"))]
70pub struct StderrPrinter {
71 stderr: Stderr,
72 is_terminal: bool,
73}
74
75#[cfg(any(test, feature = "test-utils"))]
76impl StderrPrinter {
77 pub fn new() -> Self {
79 let is_terminal = std::io::stderr().is_terminal();
80 Self {
81 stderr: std::io::stderr(),
82 is_terminal,
83 }
84 }
85}
86
87#[cfg(any(test, feature = "test-utils"))]
88impl Default for StderrPrinter {
89 fn default() -> Self {
90 Self::new()
91 }
92}
93
94#[cfg(any(test, feature = "test-utils"))]
95impl std::io::Write for StderrPrinter {
96 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
97 self.stderr.write(buf)
98 }
99
100 fn flush(&mut self) -> io::Result<()> {
101 self.stderr.flush()
102 }
103}
104
105#[cfg(any(test, feature = "test-utils"))]
106impl Printable for StderrPrinter {
107 fn is_terminal(&self) -> bool {
108 self.is_terminal
109 }
110}
111
112#[cfg(any(test, feature = "test-utils"))]
117#[derive(Debug, Default)]
118pub struct TestPrinter {
119 output: RefCell<Vec<String>>,
121 buffer: RefCell<String>,
123}
124
125#[cfg(any(test, feature = "test-utils"))]
126impl TestPrinter {
127 pub fn new() -> Self {
129 Self::default()
130 }
131
132 pub fn get_output(&self) -> String {
134 let mut result = self.buffer.borrow().clone();
135 for line in self.output.borrow().iter() {
136 result.push_str(line);
137 }
138 result
139 }
140
141 pub fn get_lines(&self) -> Vec<String> {
143 let mut result: Vec<String> = self.output.borrow().clone();
144 let buffer = self.buffer.borrow();
145 if !buffer.is_empty() {
146 result.push(buffer.clone());
147 }
148 result
149 }
150
151 pub fn clear(&self) {
153 self.output.borrow_mut().clear();
154 self.buffer.borrow_mut().clear();
155 }
156
157 pub fn has_line(&self, line: &str) -> bool {
159 self.get_lines().iter().any(|l| l.contains(line))
160 }
161
162 pub fn count_pattern(&self, pattern: &str) -> usize {
164 self.get_lines()
165 .iter()
166 .filter(|l| l.contains(pattern))
167 .count()
168 }
169
170 pub fn has_duplicate_consecutive_lines(&self) -> bool {
172 let lines = self.get_lines();
173 for i in 1..lines.len() {
174 if lines[i] == lines[i - 1] && !lines[i].is_empty() {
175 return true;
176 }
177 }
178 false
179 }
180
181 pub fn find_duplicate_consecutive_lines(&self) -> Vec<(usize, String)> {
183 let mut duplicates = Vec::new();
184 let lines = self.get_lines();
185 for i in 1..lines.len() {
186 if lines[i] == lines[i - 1] && !lines[i].is_empty() {
187 duplicates.push((i - 1, lines[i - 1].clone()));
188 }
189 }
190 duplicates
191 }
192
193 pub fn get_stats(&self) -> (usize, usize) {
197 let lines = self.get_lines();
198 let char_count: usize = lines.iter().map(String::len).sum();
199 (lines.len(), char_count)
200 }
201}
202
203#[cfg(any(test, feature = "test-utils"))]
204impl std::io::Write for TestPrinter {
205 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
206 let s =
207 std::str::from_utf8(buf).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
208 let mut buffer = self.buffer.borrow_mut();
209 buffer.push_str(s);
210
211 while let Some(newline_pos) = buffer.find('\n') {
213 let line = buffer.drain(..=newline_pos).collect::<String>();
214 self.output.borrow_mut().push(line);
215 }
216
217 Ok(buf.len())
218 }
219
220 fn flush(&mut self) -> io::Result<()> {
221 let mut buffer = self.buffer.borrow_mut();
223 if !buffer.is_empty() {
224 self.output.borrow_mut().push(buffer.clone());
225 buffer.clear();
226 }
227 Ok(())
228 }
229}
230
231#[cfg(any(test, feature = "test-utils"))]
232impl Printable for TestPrinter {
233 fn is_terminal(&self) -> bool {
234 false
236 }
237}
238
239#[cfg(any(test, feature = "test-utils"))]
244#[derive(Debug, Clone)]
245pub struct WriteCall {
246 pub content: String,
248 pub timestamp: std::time::Instant,
250}
251
252#[cfg(any(test, feature = "test-utils"))]
254#[derive(Debug, Clone)]
255pub struct FlushCall {
256 pub last_write_index: Option<usize>,
258 pub timestamp: std::time::Instant,
260}
261
262#[cfg(any(test, feature = "test-utils"))]
290#[derive(Debug)]
291pub struct StreamingTestPrinter {
292 write_calls: RefCell<Vec<WriteCall>>,
294 flush_calls: RefCell<Vec<FlushCall>>,
296 simulated_is_terminal: bool,
298}
299
300#[cfg(any(test, feature = "test-utils"))]
301impl StreamingTestPrinter {
302 pub fn new() -> Self {
304 Self {
305 write_calls: RefCell::new(Vec::new()),
306 flush_calls: RefCell::new(Vec::new()),
307 simulated_is_terminal: false,
308 }
309 }
310
311 pub fn new_with_terminal(is_terminal: bool) -> Self {
316 Self {
317 write_calls: RefCell::new(Vec::new()),
318 flush_calls: RefCell::new(Vec::new()),
319 simulated_is_terminal: is_terminal,
320 }
321 }
322
323 pub fn get_write_calls(&self) -> Vec<WriteCall> {
325 self.write_calls.borrow().clone()
326 }
327
328 pub fn write_count(&self) -> usize {
330 self.write_calls.borrow().len()
331 }
332
333 pub fn get_full_output(&self) -> String {
335 self.write_calls
336 .borrow()
337 .iter()
338 .map(|w| w.content.clone())
339 .collect()
340 }
341
342 pub fn get_content_at_write(&self, index: usize) -> Option<String> {
344 self.write_calls
345 .borrow()
346 .get(index)
347 .map(|w| w.content.clone())
348 }
349
350 pub fn verify_incremental_writes(&self, min_expected: usize) -> Result<(), String> {
358 let count = self.write_count();
359 if count >= min_expected {
360 Ok(())
361 } else {
362 Err(format!(
363 "Expected at least {} incremental writes, but only {} occurred. \
364 This suggests output is batched rather than streamed.",
365 min_expected, count
366 ))
367 }
368 }
369
370 pub fn contains_escape_sequence(&self, seq: &str) -> bool {
372 self.get_full_output().contains(seq)
373 }
374
375 pub fn has_any_escape_sequences(&self) -> bool {
377 self.get_full_output().contains('\x1b')
378 }
379
380 pub fn strip_ansi(s: &str) -> String {
384 let mut result = String::with_capacity(s.len());
385 let mut chars = s.chars().peekable();
386
387 while let Some(c) = chars.next() {
388 if c == '\x1b' {
389 if chars.peek() == Some(&'[') {
391 chars.next(); while let Some(&next) = chars.peek() {
394 chars.next();
395 if next.is_ascii_alphabetic() {
396 break;
397 }
398 }
399 }
400 } else {
401 result.push(c);
402 }
403 }
404 result
405 }
406
407 pub fn get_content_progression(&self) -> Vec<String> {
412 let mut accumulated = String::new();
413 let mut progression = Vec::new();
414
415 for call in self.write_calls.borrow().iter() {
416 accumulated.push_str(&call.content);
417 let clean = Self::strip_ansi(&accumulated)
419 .replace('\r', "")
420 .replace('\n', " ")
421 .trim()
422 .to_string();
423 if !clean.is_empty() {
424 progression.push(clean);
425 }
426 }
427 progression
428 }
429
430 pub fn clear(&self) {
432 self.write_calls.borrow_mut().clear();
433 self.flush_calls.borrow_mut().clear();
434 }
435
436 pub fn get_flush_calls(&self) -> Vec<FlushCall> {
438 self.flush_calls.borrow().clone()
439 }
440
441 pub fn flush_count(&self) -> usize {
443 self.flush_calls.borrow().len()
444 }
445
446 pub fn verify_flush_after_writes(&self) -> Result<(), String> {
454 let writes = self.write_calls.borrow();
455 let flushes = self.flush_calls.borrow();
456
457 if writes.is_empty() {
458 return Err("No writes occurred".to_string());
459 }
460
461 if flushes.is_empty() {
462 return Err(format!(
463 "No flush() calls occurred after {} write(s). \
464 This means output is buffered and will appear 'all at once' \
465 instead of streaming incrementally.",
466 writes.len()
467 ));
468 }
469
470 Ok(())
471 }
472
473 pub fn verify_flush_count(&self, min_expected: usize) -> Result<(), String> {
478 let count = self.flush_count();
479 if count >= min_expected {
480 Ok(())
481 } else {
482 Err(format!(
483 "Expected at least {} flush() calls, but only {} occurred. \
484 This suggests output is not being flushed frequently enough for streaming.",
485 min_expected, count
486 ))
487 }
488 }
489}
490
491#[cfg(any(test, feature = "test-utils"))]
492impl Default for StreamingTestPrinter {
493 fn default() -> Self {
494 Self::new()
495 }
496}
497
498#[cfg(any(test, feature = "test-utils"))]
499impl std::io::Write for StreamingTestPrinter {
500 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
501 let content =
502 std::str::from_utf8(buf).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
503
504 self.write_calls.borrow_mut().push(WriteCall {
505 content: content.to_string(),
506 timestamp: std::time::Instant::now(),
507 });
508
509 Ok(buf.len())
510 }
511
512 fn flush(&mut self) -> io::Result<()> {
513 let last_write_index = if self.write_calls.borrow().is_empty() {
514 None
515 } else {
516 Some(self.write_calls.borrow().len() - 1)
517 };
518 self.flush_calls.borrow_mut().push(FlushCall {
519 last_write_index,
520 timestamp: std::time::Instant::now(),
521 });
522 Ok(())
523 }
524}
525
526#[cfg(any(test, feature = "test-utils"))]
527impl Printable for StreamingTestPrinter {
528 fn is_terminal(&self) -> bool {
529 self.simulated_is_terminal
530 }
531}
532
533#[cfg(any(test, feature = "test-utils"))]
560#[derive(Debug)]
561pub struct VirtualTerminal {
562 lines: RefCell<Vec<String>>,
564 cursor_row: RefCell<usize>,
566 cursor_col: RefCell<usize>,
568 simulated_is_terminal: bool,
570 write_history: RefCell<Vec<String>>,
572}
573
574#[cfg(any(test, feature = "test-utils"))]
575impl VirtualTerminal {
576 pub fn new() -> Self {
578 Self {
579 lines: RefCell::new(vec![String::new()]),
580 cursor_row: RefCell::new(0),
581 cursor_col: RefCell::new(0),
582 simulated_is_terminal: true,
583 write_history: RefCell::new(Vec::new()),
584 }
585 }
586
587 pub fn new_with_terminal(is_terminal: bool) -> Self {
589 Self {
590 lines: RefCell::new(vec![String::new()]),
591 cursor_row: RefCell::new(0),
592 cursor_col: RefCell::new(0),
593 simulated_is_terminal: is_terminal,
594 write_history: RefCell::new(Vec::new()),
595 }
596 }
597
598 pub fn get_visible_output(&self) -> String {
603 let lines = self.lines.borrow();
604 lines
606 .iter()
607 .map(|line| line.trim_end().to_string())
608 .collect::<Vec<_>>()
609 .join("\n")
610 }
611
612 pub fn get_visible_lines(&self) -> Vec<String> {
614 self.lines
615 .borrow()
616 .iter()
617 .map(|line| line.trim_end().to_string())
618 .filter(|line| !line.is_empty())
619 .collect()
620 }
621
622 pub fn get_write_history(&self) -> Vec<String> {
624 self.write_history.borrow().clone()
625 }
626
627 pub fn cursor_position(&self) -> (usize, usize) {
629 (*self.cursor_row.borrow(), *self.cursor_col.borrow())
630 }
631
632 pub fn clear(&self) {
634 self.lines.borrow_mut().clear();
635 self.lines.borrow_mut().push(String::new());
636 *self.cursor_row.borrow_mut() = 0;
637 *self.cursor_col.borrow_mut() = 0;
638 self.write_history.borrow_mut().clear();
639 }
640
641 fn ensure_row_exists(&self) {
643 let row = *self.cursor_row.borrow();
644 let mut lines = self.lines.borrow_mut();
645 while lines.len() <= row {
646 lines.push(String::new());
647 }
648 }
649
650 fn write_str(&self, s: &str) {
654 if s.is_empty() {
655 return;
656 }
657
658 self.ensure_row_exists();
659 let row = *self.cursor_row.borrow();
660 let col = *self.cursor_col.borrow();
661 let mut lines = self.lines.borrow_mut();
662 let line = &mut lines[row];
663
664 while line.chars().count() < col {
666 line.push(' ');
667 }
668
669 let prefix: String = line.chars().take(col).collect();
671 let suffix: String = line.chars().skip(col + s.chars().count()).collect();
672 *line = format!("{}{}{}", prefix, s, suffix);
673
674 *self.cursor_col.borrow_mut() = col + s.chars().count();
676 }
677
678 fn clear_line(&self) {
680 self.ensure_row_exists();
681 let row = *self.cursor_row.borrow();
682 let mut lines = self.lines.borrow_mut();
683 lines[row].clear();
684 }
686
687 fn cursor_up(&self, n: usize) {
689 let mut row = self.cursor_row.borrow_mut();
690 *row = row.saturating_sub(n);
691 }
692
693 fn cursor_down(&self, n: usize) {
695 *self.cursor_row.borrow_mut() += n;
696 self.ensure_row_exists();
697 }
698
699 fn process_string(&self, s: &str) {
701 let mut chars = s.chars().peekable();
702 let mut text_buffer = String::new();
703
704 let flush_text = |term: &Self, buf: &mut String| {
706 if !buf.is_empty() {
707 term.write_str(buf);
708 buf.clear();
709 }
710 };
711
712 while let Some(c) = chars.next() {
713 match c {
714 '\r' => {
715 flush_text(self, &mut text_buffer);
716 *self.cursor_col.borrow_mut() = 0;
718 }
719 '\n' => {
720 flush_text(self, &mut text_buffer);
721 *self.cursor_row.borrow_mut() += 1;
723 *self.cursor_col.borrow_mut() = 0;
724 self.ensure_row_exists();
725 }
726 '\x1b' => {
727 flush_text(self, &mut text_buffer);
728 if chars.peek() == Some(&'[') {
730 chars.next(); let mut param = String::new();
734 while let Some(&c) = chars.peek() {
735 if c.is_ascii_digit() {
736 param.push(c);
737 chars.next();
738 } else {
739 break;
740 }
741 }
742
743 if let Some(cmd) = chars.next() {
745 let n: usize = param.parse().unwrap_or(1);
746 match cmd {
747 'A' => self.cursor_up(n), 'B' => self.cursor_down(n), 'K' => {
750 let mode: usize = param.parse().unwrap_or(0);
755 if mode == 2 {
756 self.clear_line();
757 }
758 }
761 'm' => {
762 }
765 _ => {
766 }
768 }
769 }
770 }
771 }
772 _ => {
773 text_buffer.push(c);
775 }
776 }
777 }
778
779 flush_text(self, &mut text_buffer);
781 }
782
783 pub fn has_duplicate_lines(&self) -> bool {
785 let lines = self.get_visible_lines();
786 for i in 1..lines.len() {
787 if !lines[i].is_empty() && lines[i] == lines[i - 1] {
788 return true;
789 }
790 }
791 false
792 }
793
794 pub fn count_visible_pattern(&self, pattern: &str) -> usize {
796 self.get_visible_output().matches(pattern).count()
797 }
798}
799
800#[cfg(any(test, feature = "test-utils"))]
801impl Default for VirtualTerminal {
802 fn default() -> Self {
803 Self::new()
804 }
805}
806
807#[cfg(any(test, feature = "test-utils"))]
808impl std::io::Write for VirtualTerminal {
809 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
810 let s =
811 std::str::from_utf8(buf).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
812
813 self.write_history.borrow_mut().push(s.to_string());
815
816 self.process_string(s);
818
819 Ok(buf.len())
820 }
821
822 fn flush(&mut self) -> io::Result<()> {
823 Ok(())
825 }
826}
827
828#[cfg(any(test, feature = "test-utils"))]
829impl Printable for VirtualTerminal {
830 fn is_terminal(&self) -> bool {
831 self.simulated_is_terminal
832 }
833}
834
835pub type SharedPrinter = Rc<RefCell<dyn Printable>>;
840
841pub fn shared_stdout() -> SharedPrinter {
843 Rc::new(RefCell::new(StdoutPrinter::new()))
844}
845
846#[cfg(test)]
847mod tests {
848 use super::*;
849 use std::io::Write;
850
851 #[test]
852 fn test_stdout_printer() {
853 let mut printer = StdoutPrinter::new();
854 let result = printer.write_all(b"test\n");
856 assert!(result.is_ok());
857 assert!(printer.flush().is_ok());
858
859 let _is_term = printer.is_terminal();
861 }
862
863 #[cfg(test)]
864 #[test]
865 fn test_printable_trait_is_terminal() {
866 let printer = StdoutPrinter::new();
867 let _should_use_colors = printer.is_terminal();
869 }
870
871 #[test]
872 #[cfg(any(test, feature = "test-utils"))]
873 fn test_stderr_printer() {
874 let mut printer = StderrPrinter::new();
875 let result = printer.write_all(b"test\n");
877 assert!(result.is_ok());
878 assert!(printer.flush().is_ok());
879 }
880
881 #[test]
882 #[cfg(any(test, feature = "test-utils"))]
883 fn test_printer_captures_output() {
884 let mut printer = TestPrinter::new();
885
886 printer
887 .write_all(b"Hello World\n")
888 .expect("Failed to write");
889 printer.flush().expect("Failed to flush");
890
891 let output = printer.get_output();
892 assert!(output.contains("Hello World"));
893 }
894
895 #[test]
896 #[cfg(any(test, feature = "test-utils"))]
897 fn test_printer_get_lines() {
898 let mut printer = TestPrinter::new();
899
900 printer.write_all(b"Line 1\nLine 2\n").unwrap();
901 printer.flush().unwrap();
902
903 let lines = printer.get_lines();
904 assert_eq!(lines.len(), 2);
905 assert!(lines[0].contains("Line 1"));
906 assert!(lines[1].contains("Line 2"));
907 }
908
909 #[test]
910 #[cfg(any(test, feature = "test-utils"))]
911 fn test_printer_clear() {
912 let mut printer = TestPrinter::new();
913
914 printer.write_all(b"Before\n").unwrap();
915 printer.flush().unwrap();
916
917 assert!(!printer.get_output().is_empty());
918
919 printer.clear();
920 assert!(printer.get_output().is_empty());
921 }
922
923 #[cfg(any(test, feature = "test-utils"))]
924 #[test]
925 fn test_printer_has_line() {
926 let mut printer = TestPrinter::new();
927
928 printer.write_all(b"Hello World\n").unwrap();
929 printer.flush().unwrap();
930
931 assert!(printer.has_line("Hello"));
932 assert!(printer.has_line("World"));
933 assert!(!printer.has_line("Goodbye"));
934 }
935
936 #[cfg(any(test, feature = "test-utils"))]
937 #[test]
938 fn test_printer_count_pattern() {
939 let mut printer = TestPrinter::new();
940
941 printer.write_all(b"test\nmore test\ntest again\n").unwrap();
942 printer.flush().unwrap();
943
944 assert_eq!(printer.count_pattern("test"), 3);
945 }
946
947 #[cfg(any(test, feature = "test-utils"))]
948 #[test]
949 fn test_printer_detects_duplicates() {
950 let mut printer = TestPrinter::new();
951
952 printer.write_all(b"Line 1\nLine 1\nLine 2\n").unwrap();
953 printer.flush().unwrap();
954
955 assert!(printer.has_duplicate_consecutive_lines());
956 }
957
958 #[cfg(any(test, feature = "test-utils"))]
959 #[test]
960 fn test_printer_finds_duplicates() {
961 let mut printer = TestPrinter::new();
962
963 printer
964 .write_all(b"Line 1\nLine 1\nLine 2\nLine 3\nLine 3\n")
965 .unwrap();
966 printer.flush().unwrap();
967
968 let duplicates = printer.find_duplicate_consecutive_lines();
969 assert_eq!(duplicates.len(), 2);
970 assert_eq!(duplicates[0].0, 0); assert_eq!(duplicates[0].1, "Line 1\n");
972 assert_eq!(duplicates[1].0, 3); assert_eq!(duplicates[1].1, "Line 3\n");
974 }
975
976 #[cfg(any(test, feature = "test-utils"))]
977 #[test]
978 fn test_printer_no_false_positives() {
979 let mut printer = TestPrinter::new();
980
981 printer.write_all(b"Line 1\nLine 2\nLine 3\n").unwrap();
982 printer.flush().unwrap();
983
984 assert!(!printer.has_duplicate_consecutive_lines());
985 }
986
987 #[cfg(any(test, feature = "test-utils"))]
988 #[test]
989 fn test_printer_buffer_handling() {
990 let mut printer = TestPrinter::new();
991
992 printer.write_all(b"Partial").unwrap();
994
995 assert!(printer.get_output().contains("Partial"));
998
999 printer.write_all(b" content\n").unwrap();
1001 printer.flush().unwrap();
1002
1003 assert!(printer.has_line("Partial content"));
1005
1006 let output = printer.get_output();
1008 assert!(output.contains("Partial content\n"));
1009 }
1010
1011 #[cfg(any(test, feature = "test-utils"))]
1012 #[test]
1013 fn test_printer_get_stats() {
1014 let mut printer = TestPrinter::new();
1015
1016 printer.write_all(b"Line 1\nLine 2\n").unwrap();
1017 printer.flush().unwrap();
1018
1019 let (line_count, char_count) = printer.get_stats();
1020 assert_eq!(line_count, 2);
1021 assert!(char_count > 0);
1022 }
1023
1024 #[test]
1025 fn test_shared_stdout() {
1026 let printer = shared_stdout();
1027 let _borrowed = printer.borrow();
1029 }
1030
1031 #[cfg(any(test, feature = "test-utils"))]
1032 #[test]
1033 fn test_streaming_printer_captures_individual_writes() {
1034 let mut printer = StreamingTestPrinter::new();
1035
1036 printer.write_all(b"Hello").unwrap();
1037 printer.write_all(b" ").unwrap();
1038 printer.write_all(b"World").unwrap();
1039
1040 assert_eq!(printer.write_count(), 3);
1041 assert_eq!(printer.get_full_output(), "Hello World");
1042 }
1043
1044 #[cfg(any(test, feature = "test-utils"))]
1045 #[test]
1046 fn test_streaming_printer_verify_incremental_writes() {
1047 let mut printer = StreamingTestPrinter::new();
1048
1049 printer.write_all(b"A").unwrap();
1050 printer.write_all(b"B").unwrap();
1051 printer.write_all(b"C").unwrap();
1052 printer.write_all(b"D").unwrap();
1053
1054 assert!(printer.verify_incremental_writes(4).is_ok());
1055 assert!(printer.verify_incremental_writes(5).is_err());
1056 }
1057
1058 #[cfg(any(test, feature = "test-utils"))]
1059 #[test]
1060 fn test_streaming_printer_detects_escape_sequences() {
1061 let mut printer = StreamingTestPrinter::new();
1062
1063 printer.write_all(b"Normal text").unwrap();
1064 assert!(!printer.has_any_escape_sequences());
1065
1066 printer.clear();
1067 printer.write_all(b"\x1b[2K\rUpdated").unwrap();
1068 assert!(printer.has_any_escape_sequences());
1069 assert!(printer.contains_escape_sequence("\x1b[2K"));
1070 }
1071
1072 #[cfg(any(test, feature = "test-utils"))]
1073 #[test]
1074 fn test_streaming_printer_strip_ansi() {
1075 let input = "\x1b[2K\r\x1b[1mBold\x1b[0m text\x1b[1A";
1076 let stripped = StreamingTestPrinter::strip_ansi(input);
1077 assert_eq!(stripped, "\rBold text");
1078 }
1079
1080 #[cfg(any(test, feature = "test-utils"))]
1081 #[test]
1082 fn test_streaming_printer_content_progression() {
1083 let mut printer = StreamingTestPrinter::new();
1084
1085 printer.write_all(b"[agent] Hello\n").unwrap();
1086 printer
1087 .write_all(b"\x1b[2K\r[agent] Hello World\n")
1088 .unwrap();
1089
1090 let progression = printer.get_content_progression();
1091 assert!(progression.len() >= 1);
1092 if progression.len() >= 2 {
1094 assert!(progression[1].len() >= progression[0].len());
1095 }
1096 }
1097
1098 #[cfg(any(test, feature = "test-utils"))]
1099 #[test]
1100 fn test_streaming_printer_terminal_simulation() {
1101 let printer_non_term = StreamingTestPrinter::new();
1102 assert!(!printer_non_term.is_terminal());
1103
1104 let printer_term = StreamingTestPrinter::new_with_terminal(true);
1105 assert!(printer_term.is_terminal());
1106 }
1107
1108 #[cfg(any(test, feature = "test-utils"))]
1109 #[test]
1110 fn test_streaming_printer_get_content_at_write() {
1111 let mut printer = StreamingTestPrinter::new();
1112
1113 printer.write_all(b"First").unwrap();
1114 printer.write_all(b"Second").unwrap();
1115 printer.write_all(b"Third").unwrap();
1116
1117 assert_eq!(printer.get_content_at_write(0), Some("First".to_string()));
1118 assert_eq!(printer.get_content_at_write(1), Some("Second".to_string()));
1119 assert_eq!(printer.get_content_at_write(2), Some("Third".to_string()));
1120 assert_eq!(printer.get_content_at_write(3), None);
1121 }
1122
1123 #[cfg(any(test, feature = "test-utils"))]
1124 #[test]
1125 fn test_streaming_printer_clear() {
1126 let mut printer = StreamingTestPrinter::new();
1127
1128 printer.write_all(b"Some content").unwrap();
1129 assert_eq!(printer.write_count(), 1);
1130
1131 printer.clear();
1132 assert_eq!(printer.write_count(), 0);
1133 assert!(printer.get_full_output().is_empty());
1134 }
1135
1136 #[cfg(any(test, feature = "test-utils"))]
1141 #[test]
1142 fn test_virtual_terminal_simple_text() {
1143 let mut term = VirtualTerminal::new();
1144 write!(term, "Hello World").unwrap();
1145 assert_eq!(term.get_visible_output(), "Hello World");
1146 }
1147
1148 #[cfg(any(test, feature = "test-utils"))]
1149 #[test]
1150 fn test_virtual_terminal_newlines() {
1151 let mut term = VirtualTerminal::new();
1152 write!(term, "Line 1\nLine 2\nLine 3").unwrap();
1153 assert_eq!(term.get_visible_output(), "Line 1\nLine 2\nLine 3");
1154 }
1155
1156 #[cfg(any(test, feature = "test-utils"))]
1157 #[test]
1158 fn test_virtual_terminal_carriage_return_overwrites() {
1159 let mut term = VirtualTerminal::new();
1160 write!(term, "Hello\rWorld").unwrap();
1162 assert_eq!(term.get_visible_output(), "World");
1163 }
1164
1165 #[cfg(any(test, feature = "test-utils"))]
1166 #[test]
1167 fn test_virtual_terminal_carriage_return_partial_overwrite() {
1168 let mut term = VirtualTerminal::new();
1169 write!(term, "Hello World\rHi").unwrap();
1171 assert_eq!(term.get_visible_output(), "Hillo World");
1173 }
1174
1175 #[cfg(any(test, feature = "test-utils"))]
1176 #[test]
1177 fn test_virtual_terminal_ansi_clear_line() {
1178 let mut term = VirtualTerminal::new();
1179 write!(term, "Old text\x1b[2K\rNew text").unwrap();
1181 assert_eq!(term.get_visible_output(), "New text");
1182 }
1183
1184 #[cfg(any(test, feature = "test-utils"))]
1185 #[test]
1186 fn test_virtual_terminal_cursor_up() {
1187 let mut term = VirtualTerminal::new();
1188 write!(term, "Line 1\nLine 2\x1b[1A\rOverwritten").unwrap();
1190 let lines = term.get_visible_lines();
1191 assert_eq!(lines.len(), 2);
1192 assert_eq!(lines[0], "Overwritten");
1193 assert_eq!(lines[1], "Line 2");
1194 }
1195
1196 #[cfg(any(test, feature = "test-utils"))]
1197 #[test]
1198 fn test_virtual_terminal_cursor_down() {
1199 let mut term = VirtualTerminal::new();
1200 write!(term, "Row 0\x1b[1B\rRow 1").unwrap();
1202 let output = term.get_visible_output();
1203 assert!(output.contains("Row 0"));
1204 assert!(output.contains("Row 1"));
1205 }
1206
1207 #[cfg(any(test, feature = "test-utils"))]
1208 #[test]
1209 fn test_virtual_terminal_streaming_simulation() {
1210 let mut term = VirtualTerminal::new();
1215
1216 write!(term, "[agent] Hello\n\x1b[1A").unwrap();
1218 assert_eq!(term.get_visible_lines(), vec!["[agent] Hello"]);
1219
1220 write!(term, "\x1b[2K\r[agent] Hello World\n\x1b[1A").unwrap();
1222 assert_eq!(term.get_visible_lines(), vec!["[agent] Hello World"]);
1223
1224 write!(term, "\x1b[1B\n").unwrap();
1226 assert!(term.get_visible_output().contains("[agent] Hello World"));
1228 }
1229
1230 #[cfg(any(test, feature = "test-utils"))]
1231 #[test]
1232 fn test_virtual_terminal_no_duplicate_lines_in_streaming() {
1233 let mut term = VirtualTerminal::new();
1234
1235 write!(term, "[agent] A\n\x1b[1A").unwrap();
1237 write!(term, "\x1b[2K\r[agent] AB\n\x1b[1A").unwrap();
1238 write!(term, "\x1b[2K\r[agent] ABC\n\x1b[1A").unwrap();
1239 write!(term, "\x1b[1B\n").unwrap();
1240
1241 assert!(
1243 !term.has_duplicate_lines(),
1244 "Virtual terminal should not show duplicate lines after streaming. Got: {:?}",
1245 term.get_visible_lines()
1246 );
1247
1248 assert!(term.get_visible_output().contains("[agent] ABC"));
1250 }
1251
1252 #[cfg(any(test, feature = "test-utils"))]
1253 #[test]
1254 fn test_virtual_terminal_ignores_color_codes() {
1255 let mut term = VirtualTerminal::new();
1256 write!(term, "\x1b[32mGreen\x1b[0m Normal").unwrap();
1258 assert_eq!(term.get_visible_output(), "Green Normal");
1259 }
1260
1261 #[cfg(any(test, feature = "test-utils"))]
1262 #[test]
1263 fn test_virtual_terminal_is_terminal() {
1264 let term_tty = VirtualTerminal::new();
1265 assert!(term_tty.is_terminal());
1266
1267 let term_non_tty = VirtualTerminal::new_with_terminal(false);
1268 assert!(!term_non_tty.is_terminal());
1269 }
1270
1271 #[cfg(any(test, feature = "test-utils"))]
1272 #[test]
1273 fn test_virtual_terminal_cursor_position() {
1274 let mut term = VirtualTerminal::new();
1275
1276 assert_eq!(term.cursor_position(), (0, 0));
1277
1278 write!(term, "Hello").unwrap();
1279 assert_eq!(term.cursor_position(), (0, 5));
1280
1281 write!(term, "\n").unwrap();
1282 assert_eq!(term.cursor_position(), (1, 0));
1283
1284 write!(term, "World").unwrap();
1285 assert_eq!(term.cursor_position(), (1, 5));
1286
1287 write!(term, "\r").unwrap();
1288 assert_eq!(term.cursor_position(), (1, 0));
1289 }
1290
1291 #[cfg(any(test, feature = "test-utils"))]
1292 #[test]
1293 fn test_virtual_terminal_count_pattern() {
1294 let mut term = VirtualTerminal::new();
1295 write!(term, "Hello World\nHello Again\nGoodbye").unwrap();
1296 assert_eq!(term.count_visible_pattern("Hello"), 2);
1297 assert_eq!(term.count_visible_pattern("Goodbye"), 1);
1298 assert_eq!(term.count_visible_pattern("NotFound"), 0);
1299 }
1300
1301 #[cfg(any(test, feature = "test-utils"))]
1302 #[test]
1303 fn test_virtual_terminal_clear() {
1304 let mut term = VirtualTerminal::new();
1305 write!(term, "Some content\nMore content").unwrap();
1306 assert!(!term.get_visible_output().is_empty());
1307
1308 term.clear();
1309 assert!(term.get_visible_output().is_empty());
1310 assert_eq!(term.cursor_position(), (0, 0));
1311 }
1312
1313 #[cfg(any(test, feature = "test-utils"))]
1314 #[test]
1315 fn test_virtual_terminal_write_history() {
1316 let mut term = VirtualTerminal::new();
1317 write!(term, "First").unwrap();
1318 write!(term, "Second").unwrap();
1319 write!(term, "Third").unwrap();
1320
1321 let history = term.get_write_history();
1322 assert_eq!(history.len(), 3);
1323 assert_eq!(history[0], "First");
1324 assert_eq!(history[1], "Second");
1325 assert_eq!(history[2], "Third");
1326 }
1327}