use std::time::Instant;
use crate::ansi;
use crate::replay::recorder::{ReplayEvent, ReplayEventKind};
use crate::terminal::{Grid, TerminalState};
#[derive(Debug, Default)]
pub struct ReplayResult {
pub events_processed: u64,
pub total_bytes: u64,
pub parse_time_ms: f64,
pub final_grid_rows: usize,
pub final_grid_cols: usize,
pub final_cursor: (usize, usize),
pub scrollback_lines: usize,
pub snapshots: Vec<(String, String)>, pub errors: Vec<String>,
}
pub fn run_replay(events: &[ReplayEvent], cols: u16, rows: u16) -> ReplayResult {
let mut terminal = TerminalState::new(cols as usize, rows as usize, 10_000);
let mut parser = ansi::Parser::new();
let mut result = ReplayResult::default();
for event in events {
match &event.kind {
ReplayEventKind::PtyBytes(data) => {
let t0 = Instant::now();
parser.advance(data, &mut terminal);
let dt = t0.elapsed().as_secs_f64() * 1000.0;
result.parse_time_ms += dt;
result.events_processed += 1;
result.total_bytes += data.len() as u64;
}
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 snap = grid_snapshot(&terminal.grid);
result.snapshots.push((label.clone(), snap));
}
}
}
result.final_grid_rows = terminal.grid.rows();
result.final_grid_cols = terminal.grid.cols();
result.final_cursor = (terminal.grid.cursor_row(), terminal.grid.cursor_col());
result.scrollback_lines = terminal.scrollback.len();
result
}
fn grid_snapshot(grid: &Grid) -> String {
let mut out = String::with_capacity(grid.cols * grid.rows + grid.rows);
for r in 0..grid.rows() {
for c in 0..grid.cols() {
if let Some(cell) = grid.cell(r, c) {
cell.push_text(&mut out);
} else {
out.push(' ');
}
}
if r + 1 < grid.rows() {
out.push('\n');
}
}
out
}
pub fn diff_snapshots(label: &str, expected: &str, actual: &str) -> String {
if expected == actual {
return format!("OK: {}", label);
}
let exp_lines: Vec<&str> = expected.lines().collect();
let act_lines: Vec<&str> = actual.lines().collect();
let mut diff = Vec::new();
diff.push(format!("DIFF: {}", label));
for (i, (e, a)) in exp_lines.iter().zip(act_lines.iter()).enumerate() {
if e != a {
diff.push(format!(" row {}: expected {:?} got {:?}", i, e, a));
}
}
if exp_lines.len() != act_lines.len() {
diff.push(format!(
" line count mismatch: expected {} got {}",
exp_lines.len(),
act_lines.len()
));
}
diff.join("\n")
}
#[cfg(test)]
mod tests {
use super::*;
use crate::replay::recorder::*;
#[test]
fn test_run_replay_empty() {
let result = run_replay(&[], 80, 24);
assert_eq!(result.events_processed, 0);
assert_eq!(result.total_bytes, 0);
assert_eq!(result.final_grid_rows, 24);
assert_eq!(result.final_grid_cols, 80);
assert_eq!(result.final_cursor, (0, 0));
assert_eq!(result.scrollback_lines, 0);
}
#[test]
fn test_run_replay_pty_bytes() {
let events = vec![ReplayEvent::pty_bytes(b"Hello World")];
let result = run_replay(&events, 80, 24);
assert_eq!(result.events_processed, 1);
assert_eq!(result.total_bytes, 11);
assert_eq!(result.final_cursor, (0, 11));
}
#[test]
fn test_run_replay_resize_changes_grid() {
let events = vec![ReplayEvent::resize(40, 10)];
let result = run_replay(&events, 80, 24);
assert_eq!(result.final_grid_cols, 40);
assert_eq!(result.final_grid_rows, 10);
}
#[test]
fn test_run_replay_resize_same_dimensions_no_op() {
let events = vec![ReplayEvent::resize(80, 24)];
let result = run_replay(&events, 80, 24);
assert_eq!(result.final_grid_cols, 80);
assert_eq!(result.final_grid_rows, 24);
}
#[test]
fn test_run_replay_multiple_pty_bytes() {
let events = vec![
ReplayEvent::pty_bytes(b"Hello "),
ReplayEvent::pty_bytes(b"World\n"),
ReplayEvent::pty_bytes(b"Line 2"),
];
let result = run_replay(&events, 80, 24);
assert_eq!(result.events_processed, 3);
assert_eq!(result.total_bytes, 18);
assert_eq!(result.final_cursor, (1, 6));
}
#[test]
fn test_run_replay_newline_scrollback() {
let data = b"1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n";
let events = vec![ReplayEvent::pty_bytes(data)];
let result = run_replay(&events, 80, 24);
assert!(result.scrollback_lines > 0, "should have scrollback");
assert_eq!(result.final_cursor.0, 23);
}
#[test]
fn test_snapshot_captures_state() {
let events = vec![
ReplayEvent::pty_bytes(b"Hello"),
ReplayEvent::snapshot("after_hello"),
ReplayEvent::pty_bytes(b"\nWorld"),
ReplayEvent::snapshot("after_world"),
];
let result = run_replay(&events, 80, 24);
assert_eq!(result.snapshots.len(), 2);
assert_eq!(result.snapshots[0].0, "after_hello");
assert_eq!(result.snapshots[1].0, "after_world");
assert_ne!(result.snapshots[0].1, result.snapshots[1].1);
}
#[test]
fn test_diff_snapshots_identical() {
let diff = diff_snapshots("test", "hello", "hello");
assert!(diff.starts_with("OK:"));
}
#[test]
fn test_diff_snapshots_different() {
let diff = diff_snapshots("test", "hello", "world");
assert!(diff.starts_with("DIFF:"));
assert!(diff.contains("row 0"));
}
#[test]
fn test_diff_snapshots_line_count_mismatch() {
let diff = diff_snapshots("test", "a\nb\nc", "a\nb");
assert!(diff.contains("line count mismatch"));
}
#[test]
fn test_grid_snapshot_size() {
let mut terminal = TerminalState::new(5, 3, 10_000);
let mut parser = ansi::Parser::new();
parser.advance(b"Hi!", &mut terminal);
let snap = grid_snapshot(&terminal.grid);
assert_eq!(snap.lines().count(), 3);
assert!(snap.starts_with("Hi! "));
}
#[test]
fn test_replay_newlines_create_scrollback() {
let mut buf = vec![b'\n'; 100];
buf.extend_from_slice(b"LAST LINE");
let events = vec![ReplayEvent::pty_bytes(&buf)];
let result = run_replay(&events, 80, 24);
assert!(result.scrollback_lines >= 76);
assert_eq!(result.final_cursor, (23, 9));
}
}