//! Differential test: the anstyle-parse-based `split_into_line_segments` //! against a reference copy of the previous termwiz-based implementation. //! //! Production code uses `anstyle-parse` for ANSI line splitting. This test //! embeds the prior termwiz-based splitter as a reference and asserts //! identical observable output so the rewrite cannot drift. `termwiz` is a //! dev-dependency only (powers this test; not linked into shipped binaries). //! //! Inputs are restricted to what the production splitter actually sees: //! complete escape sequences (the old implementation `debug_assert`ed on //! trailing incomplete ones) and no raw C1 controls encoded as UTF-8 (termwiz //! maps e.g. U+0085 to a control action while VTE prints it; that corner was //! unspecified before and is not exercised by terminal output we render). use xai_ratatui_inline::split_into_line_segments; // ─── Reference: the previous termwiz-based implementation, verbatim ──────── struct RefSegment<'a> { content: &'a str, ends_with_crlf: bool, } fn reference_split<'a>(input: &'a str, term_width: usize) -> Vec> { use termwiz::escape::{Action, ControlCode, parser::Parser}; use unicode_width::UnicodeWidthChar as _; let mut parser = Parser::new(); let mut remaining_bytes = input.as_bytes(); let mut segments = Vec::::new(); let mut segment_start = 0_usize; let mut segment_end = 0_usize; let mut visual_width = 0_usize; let mut has_visual = false; let mut prev_is_cr = false; macro_rules! push_segment { ($end:expr, $crlf:expr) => { #[allow(unused_assignments)] { segments.push(RefSegment { content: &input[segment_start..$end], ends_with_crlf: $crlf, }); visual_width = 0; has_visual = false; } }; } while let Some((ansi_action, consumed)) = parser.parse_first(remaining_bytes) { remaining_bytes = &remaining_bytes[consumed..]; let mut is_cr = false; match ansi_action { Action::Control(ControlCode::LineFeed) => { push_segment!(segment_end - usize::from(prev_is_cr), true); segment_end += consumed; segment_start = segment_end; } Action::Control(ControlCode::CarriageReturn) => { segment_end += consumed; visual_width = 0; is_cr = true; } Action::Print(ch) => { let char_width = ch.width().unwrap_or(0); let new_width = visual_width + char_width; if new_width > term_width && has_visual { push_segment!(segment_end, false); segment_start = segment_end; segment_end += consumed; visual_width = char_width; has_visual = true; if char_width > term_width { push_segment!(segment_end, false); segment_start = segment_end; } } else { segment_end += consumed; visual_width = new_width; has_visual = true; } } Action::PrintString(_) => unreachable!(), _ => { segment_end += consumed; } } prev_is_cr = is_cr; } assert!(remaining_bytes.is_empty(), "{remaining_bytes:?}"); if segment_end > segment_start { let input_start = input.as_ptr(); if let Some(last) = segments.last_mut() { let last_start = last.content.as_ptr(); let last_end = unsafe { last_start.add(last.content.len()) }; if !last.ends_with_crlf && !has_visual { assert_eq!(segment_start, (last_end as usize - input_start as usize)); let last_offset = last_start as usize - input_start as usize; last.content = &input[last_offset..]; } else { push_segment!(segment_end, false); } } else { push_segment!(segment_end, false); } } segments } // ─── Comparison harness ───────────────────────────────────────────────────── #[track_caller] fn assert_same(input: &str, widths: &[usize]) { for &width in widths { let actual = split_into_line_segments(input, width); let expected = reference_split(input, width); let actual_view: Vec<(&str, bool)> = actual .iter() .map(|s| (s.content, s.ends_with_crlf)) .collect(); let expected_view: Vec<(&str, bool)> = expected .iter() .map(|s| (s.content, s.ends_with_crlf)) .collect(); assert_eq!( actual_view, expected_view, "divergence for width {width}, input: {input:?}" ); } } const WIDTHS: &[usize] = &[1, 2, 3, 5, 8, 10, 20, 80, 200]; #[test] fn corpus_matches_reference() { let corpus: &[&str] = &[ "", "hello", "hello world, this is a longer line that will wrap several times", "line1\nline2\nline3", "line1\r\nline2\r\n", "12345\r67", "\r\r\n\n\r", "😊😊😊 emoji wall 😊😊😊", "hello 你好 混合 width", "\x1b[31mred\x1b[0m plain \x1b[1;32;44mstyled\x1b[m", "\x1b[31m\x1b[1m\x1b[4mnested styles no text\x1b[0m", "12345678\x1b[0m90", "text\x1b]8;;https://example.com\x07link\x1b]8;;\x07 after", "osc title\x1b]0;window title\x07body", "cursor \x1b[2Amoves \x1b[10;20H everywhere", "tab\tand\x08backspace and \x07bell", "\x1b[38;5;196mext colors\x1b[38;2;10;20;30m truecolor\x1b[0m", "interrupted \x1b[3\nmid-sequence", "\x1b[31m\nstyle then newline", "trailing style 12345678\x1b[0m", "\x1b(Bcharset\x1b)0 escapes", "zero\u{200b}width\u{fe0f}chars", "combining a\u{0301}e\u{0301} accents", ]; for input in corpus { assert_same(input, WIDTHS); } } /// Deterministic pseudo-random ANSI soup (xorshift, no extra deps). #[test] fn randomized_ansi_soup_matches_reference() { let mut state = 0x243F_6A88_85A3_08D3_u64; // seed: pi digits let mut next = move || { state ^= state << 13; state ^= state >> 7; state ^= state << 17; state }; const PIECES: &[&str] = &[ "word", "a", "longer-token", " ", " ", "\n", "\r", "\r\n", "😊", "你好", "é", "\u{200b}", "\x1b[31m", "\x1b[0m", "\x1b[1;44;38;5;10m", "\x1b[2K", "\x1b[10D", "\x1b]0;title\x07", "\x1b]8;;http://x\x07", "\t", "\x07", ]; for _ in 0..2000 { let mut input = String::new(); let len = (next() % 30) as usize; for _ in 0..len { input.push_str(PIECES[(next() % PIECES.len() as u64) as usize]); } let width = 1 + (next() % 40) as usize; assert_same(&input, &[width]); } }