use serde::Serialize;
use crate::ansi;
use crate::replay::format;
use crate::replay::recorder::*;
use crate::terminal::TerminalState;
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct StateSnapshot {
pub label: String,
pub grid_hash: u64,
pub cursor: (usize, usize),
pub alt_screen: bool,
}
impl std::fmt::Display for StateSnapshot {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"Snapshot({}): grid_hash={:016x} cursor=({},{}) alt_screen={}",
self.label, self.grid_hash, self.cursor.0, self.cursor.1, self.alt_screen
)
}
}
#[derive(Debug, Clone, Serialize)]
pub struct RegressionResult {
pub snapshots: Vec<StateSnapshot>,
pub final_grid_hash: u64,
pub final_cursor: (usize, usize),
pub scrollback_hash: u64,
pub alt_screen_state: bool,
pub parser_state_bits: ParserStateBits,
pub events_processed: u64,
pub errors: Vec<String>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
pub struct ParserStateBits {
pub alt_screen: bool,
pub app_cursor: bool,
pub bracketed_paste: bool,
}
impl std::fmt::Display for RegressionResult {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(f, "Regression Result:")?;
writeln!(f, " Events processed: {}", self.events_processed)?;
writeln!(f, " Grid hash: {:016x}", self.final_grid_hash)?;
writeln!(
f,
" Cursor: ({}, {})",
self.final_cursor.0, self.final_cursor.1
)?;
writeln!(f, " Scrollback hash: {:016x}", self.scrollback_hash)?;
writeln!(f, " Alt screen: {}", self.alt_screen_state)?;
writeln!(f, " Snapshots: {}", self.snapshots.len())?;
for snap in &self.snapshots {
writeln!(f, " {}", snap)?;
}
if !self.errors.is_empty() {
writeln!(f, " Errors:")?;
for e in &self.errors {
writeln!(f, " {}", e)?;
}
}
Ok(())
}
}
pub fn run_regression(events: &[ReplayEvent], cols: u16, rows: u16) -> RegressionResult {
let mut terminal = TerminalState::new(cols as usize, rows as usize, 10_000);
let mut parser = ansi::Parser::new();
let mut result = RegressionResult {
snapshots: Vec::new(),
final_grid_hash: 0,
final_cursor: (0, 0),
scrollback_hash: 0,
alt_screen_state: false,
parser_state_bits: ParserStateBits::default(),
events_processed: 0,
errors: Vec::new(),
};
for event in events {
match &event.kind {
ReplayEventKind::PtyBytes(data) => {
parser.advance(data, &mut terminal);
result.events_processed += 1;
}
ReplayEventKind::Resize { cols: c, rows: r } => {
if *c as usize != terminal.grid.cols || *r as usize != terminal.grid.rows {
terminal.resize(*c as usize, *r as usize);
}
}
ReplayEventKind::Snapshot(label) => {
let snapshot = capture_snapshot(label, &terminal);
result.snapshots.push(snapshot);
}
}
}
result.final_grid_hash = format::grid_hash(&terminal.grid);
result.final_cursor = (terminal.grid.cursor_row(), terminal.grid.cursor_col());
result.scrollback_hash = format::scrollback_hash(&terminal.scrollback);
result.alt_screen_state = terminal.in_alt_screen();
result.parser_state_bits = ParserStateBits {
alt_screen: terminal.in_alt_screen(),
app_cursor: terminal.app_cursor(),
bracketed_paste: terminal.bracketed_paste(),
};
result
}
fn capture_snapshot(label: &str, terminal: &TerminalState) -> StateSnapshot {
StateSnapshot {
label: label.to_string(),
grid_hash: format::grid_hash(&terminal.grid),
cursor: (terminal.grid.cursor_row(), terminal.grid.cursor_col()),
alt_screen: terminal.in_alt_screen(),
}
}
pub fn diff_regression(
_label: &str,
expected: &RegressionResult,
actual: &RegressionResult,
) -> Vec<String> {
let mut diffs = Vec::new();
if expected.final_grid_hash != actual.final_grid_hash {
diffs.push(format!(
" grid hash mismatch: expected {:016x}, got {:016x}",
expected.final_grid_hash, actual.final_grid_hash
));
}
if expected.final_cursor != actual.final_cursor {
diffs.push(format!(
" cursor mismatch: expected ({},{}), got ({},{})",
expected.final_cursor.0,
expected.final_cursor.1,
actual.final_cursor.0,
actual.final_cursor.1
));
}
if expected.scrollback_hash != actual.scrollback_hash {
diffs.push(format!(
" scrollback hash mismatch: expected {:016x}, got {:016x}",
expected.scrollback_hash, actual.scrollback_hash
));
}
if expected.alt_screen_state != actual.alt_screen_state {
diffs.push(format!(
" alt screen state mismatch: expected {}, got {}",
expected.alt_screen_state, actual.alt_screen_state
));
}
if expected.parser_state_bits != actual.parser_state_bits {
diffs.push(format!(
" parser state mismatch: expected {:?}, got {:?}",
expected.parser_state_bits, actual.parser_state_bits
));
}
let max_snaps = expected.snapshots.len().max(actual.snapshots.len());
for i in 0..max_snaps {
match (expected.snapshots.get(i), actual.snapshots.get(i)) {
(Some(exp), Some(act)) => {
if exp != act {
diffs.push(format!(
" snapshot[{}] mismatch:\n expected: {}\n actual: {}",
i, exp, act
));
}
}
(Some(exp), None) => {
diffs.push(format!(
" snapshot[{}] missing in actual (expected: {})",
i, exp
));
}
(None, Some(act)) => {
diffs.push(format!(" snapshot[{}] extra in actual: {}", i, act));
}
(None, None) => {}
}
}
diffs
}
pub fn assert_regression(
_label: &str,
events: &[ReplayEvent],
expected: &RegressionResult,
cols: u16,
rows: u16,
) -> Result<(), Vec<String>> {
let actual = run_regression(events, cols, rows);
let diffs = diff_regression(_label, expected, &actual);
if diffs.is_empty() { Ok(()) } else { Err(diffs) }
}
pub fn regression_to_json(result: &RegressionResult) -> Result<String, Box<dyn std::error::Error>> {
Ok(serde_json::to_string_pretty(result)?)
}