use justerm_core::{
Cell, CellFlags, Color, Engine, Frame, FrameKind, MarkerId, MarkerKind, MarkerPosition,
MouseEvents, Overlay, ScrollOp, SelectionSpan, SelectionType, Side, Span, decode, encode,
};
use std::collections::BTreeMap;
#[test]
fn round_trip_empty_partial_frame() {
let frame = Frame {
cols: 80,
rows: 24,
kind: FrameKind::Partial,
cursor_row: 0,
cursor_col: 0,
cursor_visible: true,
cursor_shape: justerm_core::CursorShape::Block,
cursor_blink: false,
display_offset: 0,
scrollback_len: 0,
evicted_total: 0,
marker_epoch: 0,
marker_count: 0,
mouse_events: Default::default(),
alt_screen: false,
scroll: None,
spans: vec![],
link_table: vec![],
overlay: Default::default(),
};
let bytes = encode(&frame);
assert_eq!(decode(&bytes).expect("decode"), frame);
}
#[test]
fn round_trip_overlay_selection_and_match_spans() {
let frame = Frame {
cols: 80,
rows: 24,
kind: FrameKind::Partial,
cursor_row: 0,
cursor_col: 0,
cursor_visible: true,
cursor_shape: justerm_core::CursorShape::Block,
cursor_blink: false,
display_offset: 0,
scrollback_len: 0,
evicted_total: 0,
marker_epoch: 0,
marker_count: 0,
mouse_events: Default::default(),
alt_screen: false,
scroll: None,
spans: vec![],
link_table: vec![],
overlay: Overlay {
selection: vec![
SelectionSpan {
row: 0,
left: 2,
right: 7,
},
SelectionSpan {
row: 1,
left: 0,
right: 4,
},
],
matches: vec![SelectionSpan {
row: 3,
left: 10,
right: 12,
}],
markers: vec![],
active_match: vec![],
},
};
let bytes = encode(&frame);
assert_eq!(decode(&bytes).expect("decode"), frame);
}
#[test]
fn round_trip_overlay_active_match_spans() {
let mut frame = Frame {
cols: 80,
rows: 24,
kind: FrameKind::Partial,
cursor_row: 0,
cursor_col: 0,
cursor_visible: true,
cursor_shape: justerm_core::CursorShape::Block,
cursor_blink: false,
display_offset: 0,
scrollback_len: 0,
evicted_total: 0,
marker_epoch: 0,
marker_count: 0,
mouse_events: Default::default(),
alt_screen: false,
scroll: None,
spans: vec![],
link_table: vec![],
overlay: Overlay::default(),
};
let active = vec![
SelectionSpan {
row: 2,
left: 5,
right: 9,
},
SelectionSpan {
row: 3,
left: 0,
right: 1,
},
];
frame.overlay.matches = active.clone();
frame.overlay.active_match = active;
let bytes = encode(&frame);
let decoded = decode(&bytes).expect("decode");
assert_eq!(decoded, frame);
assert_eq!(decoded.overlay.active_match, decoded.overlay.matches);
assert_eq!(decoded.overlay.active_match.len(), 2);
}
#[test]
fn round_trip_full_frame_all_fields_present() {
let mut term = Engine::with_scrollback(20, 3, 100);
term.feed(b"L0\r\nL1\r\nL2\r\nL3\r\nL4"); term.feed(b"\x1b[?1002h"); term.selection_begin(0, 0, Side::Left, SelectionType::Char);
term.selection_extend(0, 2, Side::Right);
term.add_marker(1);
let frame = term.frame();
assert!(frame.scroll.is_some() && !frame.mouse_events.is_empty());
assert!(!frame.overlay.selection.is_empty() && !frame.overlay.markers.is_empty());
let bytes = encode(&frame);
assert_eq!(decode(&bytes).expect("decode"), frame);
}
#[test]
fn round_trip_mouse_events_mask() {
let mut frame = Frame {
cols: 80,
rows: 24,
kind: FrameKind::Partial,
cursor_row: 0,
cursor_col: 0,
cursor_visible: true,
cursor_shape: justerm_core::CursorShape::Block,
cursor_blink: false,
display_offset: 0,
scrollback_len: 0,
evicted_total: 0,
marker_epoch: 0,
marker_count: 0,
mouse_events: MouseEvents::empty(),
alt_screen: false,
scroll: None,
spans: vec![],
link_table: vec![],
overlay: Overlay::default(),
};
frame.mouse_events = MouseEvents::DOWN | MouseEvents::UP | MouseEvents::WHEEL;
let bytes = encode(&frame);
assert_eq!(decode(&bytes).expect("decode"), frame);
}
#[test]
fn round_trip_alt_screen_flag() {
let frame = Frame {
cols: 80,
rows: 24,
kind: FrameKind::Partial,
cursor_row: 0,
cursor_col: 0,
cursor_visible: true,
cursor_shape: justerm_core::CursorShape::Block,
cursor_blink: false,
display_offset: 0,
scrollback_len: 0,
evicted_total: 0,
marker_epoch: 0,
marker_count: 0,
mouse_events: MouseEvents::empty(),
alt_screen: true,
scroll: None,
spans: vec![],
link_table: vec![],
overlay: Overlay::default(),
};
let decoded = decode(&encode(&frame)).expect("decode");
assert!(decoded.alt_screen);
assert_eq!(decoded, frame);
}
#[test]
fn round_trip_overlay_marker_positions() {
let mut frame = Frame {
cols: 80,
rows: 24,
kind: FrameKind::Partial,
cursor_row: 0,
cursor_col: 0,
cursor_visible: true,
cursor_shape: justerm_core::CursorShape::Block,
cursor_blink: false,
display_offset: 0,
scrollback_len: 0,
evicted_total: 0,
marker_epoch: 0,
marker_count: 0,
mouse_events: Default::default(),
alt_screen: false,
scroll: None,
spans: vec![],
link_table: vec![],
overlay: Overlay::default(),
};
frame.overlay.markers = vec![
MarkerPosition {
id: MarkerId(5),
row: 3,
kind: MarkerKind::Plain,
},
MarkerPosition {
id: MarkerId(99),
row: 0,
kind: MarkerKind::Plain,
},
];
let bytes = encode(&frame);
assert_eq!(decode(&bytes).expect("decode"), frame);
}
#[test]
fn round_trip_overlay_marker_kinds() {
let mut frame = Frame {
cols: 80,
rows: 24,
kind: FrameKind::Partial,
cursor_row: 0,
cursor_col: 0,
cursor_visible: true,
cursor_shape: justerm_core::CursorShape::Block,
cursor_blink: false,
display_offset: 0,
scrollback_len: 0,
evicted_total: 0,
marker_epoch: 0,
marker_count: 0,
mouse_events: Default::default(),
alt_screen: false,
scroll: None,
spans: vec![],
link_table: vec![],
overlay: Overlay::default(),
};
frame.overlay.markers = vec![
MarkerPosition {
id: MarkerId(1),
row: 0,
kind: MarkerKind::Plain,
},
MarkerPosition {
id: MarkerId(2),
row: 1,
kind: MarkerKind::PromptStart,
},
MarkerPosition {
id: MarkerId(3),
row: 2,
kind: MarkerKind::CommandStart,
},
MarkerPosition {
id: MarkerId(4),
row: 3,
kind: MarkerKind::OutputStart,
},
MarkerPosition {
id: MarkerId(5),
row: 4,
kind: MarkerKind::CommandFinished(Some(130)),
},
MarkerPosition {
id: MarkerId(6),
row: 5,
kind: MarkerKind::CommandFinished(None),
},
];
let bytes = encode(&frame);
assert_eq!(decode(&bytes).expect("decode"), frame);
}
#[test]
fn decode_rejects_bad_marker_kind_and_truncated_exit() {
let mut frame = Frame {
cols: 80,
rows: 24,
kind: FrameKind::Partial,
cursor_row: 0,
cursor_col: 0,
cursor_visible: true,
cursor_shape: justerm_core::CursorShape::Block,
cursor_blink: false,
display_offset: 0,
scrollback_len: 0,
evicted_total: 0,
marker_epoch: 0,
marker_count: 0,
mouse_events: Default::default(),
alt_screen: false,
scroll: None,
spans: vec![],
link_table: vec![],
overlay: Overlay::default(),
};
frame.overlay.markers = vec![MarkerPosition {
id: MarkerId(1),
row: 0,
kind: MarkerKind::CommandFinished(Some(7)),
}];
let bytes = encode(&frame);
let mut bad_kind = bytes.clone();
let k = bad_kind.len() - 10;
bad_kind[k] = 5; assert!(
decode(&bad_kind).is_err(),
"an unknown marker kind must be rejected, not misread"
);
let mut truncated = bytes.clone();
truncated.truncate(truncated.len() - 6);
assert!(
decode(&truncated).is_err(),
"a truncated exit must error, not panic"
);
}
#[test]
fn round_trip_scroll_position() {
let frame = Frame {
cols: 80,
rows: 24,
kind: FrameKind::Partial,
cursor_row: 0,
cursor_col: 0,
cursor_visible: true,
cursor_shape: justerm_core::CursorShape::Block,
cursor_blink: false,
display_offset: 7,
scrollback_len: 250,
evicted_total: 0,
marker_epoch: 0,
marker_count: 0,
mouse_events: Default::default(),
alt_screen: false,
scroll: None,
spans: vec![],
link_table: vec![],
overlay: Default::default(),
};
let decoded = decode(&encode(&frame)).expect("decode");
assert_eq!(decoded.display_offset, 7);
assert_eq!(decoded.scrollback_len, 250);
assert_eq!(decoded, frame);
}
#[test]
fn round_trip_cursor_position_and_visibility() {
let frame = Frame {
cols: 80,
rows: 24,
kind: FrameKind::Partial,
cursor_row: 9,
cursor_col: 19,
cursor_visible: false,
cursor_shape: justerm_core::CursorShape::Block,
cursor_blink: false,
display_offset: 0,
scrollback_len: 0,
evicted_total: 0,
marker_epoch: 0,
marker_count: 0,
mouse_events: Default::default(),
alt_screen: false,
scroll: None,
spans: vec![],
link_table: vec![],
overlay: Default::default(),
};
assert_eq!(decode(&encode(&frame)).expect("decode"), frame);
}
#[test]
fn round_trip_span_of_plain_cells() {
let cells: Vec<Cell> = "hi!"
.chars()
.map(|c| Cell::from_parts(c, Color::Default, Color::Default, CellFlags::empty()))
.collect();
let frame = Frame {
cols: 80,
rows: 24,
kind: FrameKind::Partial,
cursor_row: 0,
cursor_col: 0,
cursor_visible: true,
cursor_shape: justerm_core::CursorShape::Block,
cursor_blink: false,
display_offset: 0,
scrollback_len: 0,
evicted_total: 0,
marker_epoch: 0,
marker_count: 0,
mouse_events: Default::default(),
alt_screen: false,
scroll: None,
spans: vec![Span {
line: 3,
left: 10,
right: 12,
cells,
combining: BTreeMap::new(),
links: BTreeMap::new(),
ucolors: BTreeMap::new(),
}],
link_table: vec![],
overlay: Default::default(),
};
assert_eq!(decode(&encode(&frame)).expect("decode"), frame);
}
#[test]
fn round_trip_distinct_colour_references() {
let mk = |fg, bg| Cell::from_parts('x', fg, bg, CellFlags::empty());
let cells = vec![
mk(Color::Default, Color::Default),
mk(Color::Indexed(0), Color::Indexed(255)),
mk(Color::Rgb(0, 0, 0), Color::Rgb(1, 2, 3)),
];
let frame = Frame {
cols: 80,
rows: 24,
kind: FrameKind::Partial,
cursor_row: 0,
cursor_col: 0,
cursor_visible: true,
cursor_shape: justerm_core::CursorShape::Block,
cursor_blink: false,
display_offset: 0,
scrollback_len: 0,
evicted_total: 0,
marker_epoch: 0,
marker_count: 0,
mouse_events: Default::default(),
alt_screen: false,
scroll: None,
spans: vec![Span {
line: 0,
left: 0,
right: 2,
cells,
combining: BTreeMap::new(),
links: BTreeMap::new(),
ucolors: BTreeMap::new(),
}],
link_table: vec![],
overlay: Default::default(),
};
let d = decode(&encode(&frame)).expect("decode");
assert_eq!(d, frame);
let row = &d.spans[0].cells;
assert_ne!(
row[0].fg(),
row[1].fg(),
"Default must differ from Indexed(0)"
);
assert_ne!(
row[1].fg(),
row[2].fg(),
"Indexed(0) must differ from Rgb(0,0,0)"
);
}
#[test]
fn round_trip_cell_flags_incl_layout_markers() {
let lead = Cell::from_parts(
'한',
Color::Default,
Color::Default,
CellFlags::BOLD | CellFlags::WIDE_CHAR,
);
let spacer = Cell::from_parts(
' ',
Color::Default,
Color::Default,
CellFlags::WIDE_CHAR_SPACER,
);
let frame = Frame {
cols: 80,
rows: 24,
kind: FrameKind::Partial,
cursor_row: 0,
cursor_col: 0,
cursor_visible: true,
cursor_shape: justerm_core::CursorShape::Block,
cursor_blink: false,
display_offset: 0,
scrollback_len: 0,
evicted_total: 0,
marker_epoch: 0,
marker_count: 0,
mouse_events: Default::default(),
alt_screen: false,
scroll: None,
spans: vec![Span {
line: 5,
left: 0,
right: 1,
cells: vec![lead, spacer],
combining: BTreeMap::new(),
links: BTreeMap::new(),
ucolors: BTreeMap::new(),
}],
link_table: vec![],
overlay: Default::default(),
};
assert_eq!(decode(&encode(&frame)).expect("decode"), frame);
}
#[test]
fn decode_rejects_malformed_input() {
assert!(decode(&[]).is_err(), "empty");
assert!(decode(b"XXxxxxxxx").is_err(), "bad magic");
assert!(decode(b"JT").is_err(), "truncated mid-header");
}
#[test]
fn decode_rejects_superseded_version() {
let frame = Frame {
cols: 1,
rows: 1,
kind: FrameKind::Partial,
cursor_row: 0,
cursor_col: 0,
cursor_visible: true,
cursor_shape: justerm_core::CursorShape::Block,
cursor_blink: false,
display_offset: 0,
scrollback_len: 0,
evicted_total: 0,
marker_epoch: 0,
marker_count: 0,
mouse_events: Default::default(),
alt_screen: false,
scroll: None,
spans: vec![],
link_table: vec![],
overlay: Default::default(),
};
let mut bytes = encode(&frame);
bytes[2] = 2; assert!(matches!(
decode(&bytes),
Err(justerm_core::DecodeError::BadVersion(2))
));
}
#[test]
fn wire_version_is_sixteen() {
assert_eq!(justerm_core::WIRE_VERSION, 16);
let mut term = Engine::new(1, 1);
term.feed(b"x");
let bytes = encode(&term.frame());
assert_eq!(bytes[2], justerm_core::WIRE_VERSION); }
#[test]
fn round_trip_grapheme_side_table() {
let mut accented = Cell::from_parts('e', Color::Default, Color::Default, CellFlags::empty());
accented.set_combined(true);
let plain = Cell::from_parts('x', Color::Default, Color::Default, CellFlags::empty());
let frame = Frame {
cols: 80,
rows: 24,
kind: FrameKind::Partial,
cursor_row: 0,
cursor_col: 0,
cursor_visible: true,
cursor_shape: justerm_core::CursorShape::Block,
cursor_blink: false,
display_offset: 0,
scrollback_len: 0,
evicted_total: 0,
marker_epoch: 0,
marker_count: 0,
mouse_events: Default::default(),
alt_screen: false,
scroll: None,
spans: vec![Span {
line: 0,
left: 0,
right: 1,
cells: vec![accented, plain],
combining: BTreeMap::from([(0, vec!['\u{0301}'])]),
links: BTreeMap::new(),
ucolors: BTreeMap::new(),
}],
link_table: vec![],
overlay: Default::default(),
};
assert_eq!(decode(&encode(&frame)).expect("decode"), frame);
}
#[test]
fn cell_record_is_fixed_14_bytes() {
let span_of = |n: usize| Frame {
cols: 1,
rows: 1,
kind: FrameKind::Partial,
cursor_row: 0,
cursor_col: 0,
cursor_visible: true,
cursor_shape: justerm_core::CursorShape::Block,
cursor_blink: false,
display_offset: 0,
scrollback_len: 0,
evicted_total: 0,
marker_epoch: 0,
marker_count: 0,
mouse_events: Default::default(),
alt_screen: false,
scroll: None,
spans: vec![Span {
line: 0,
left: 0,
right: (n - 1) as u16,
cells: vec![Cell::default(); n],
combining: BTreeMap::new(),
links: BTreeMap::new(),
ucolors: BTreeMap::new(),
}],
link_table: vec![],
overlay: Default::default(),
};
let one = encode(&span_of(1)).len();
let two = encode(&span_of(2)).len();
assert_eq!(two - one, 14, "each added cell must cost exactly 14 bytes");
assert_eq!(
two - one,
justerm_core::CELL_RECORD_LEN,
"the measured stride and the exported constant are the same number — an \
alternate consumer lays cells out by that constant (ADR-0008), so the two \
drifting apart is the failure this pins",
);
}
#[test]
fn round_trip_scroll_op() {
let frame = Frame {
cols: 80,
rows: 24,
kind: FrameKind::Partial,
cursor_row: 0,
cursor_col: 0,
cursor_visible: true,
cursor_shape: justerm_core::CursorShape::Block,
cursor_blink: false,
display_offset: 0,
scrollback_len: 0,
evicted_total: 0,
marker_epoch: 0,
marker_count: 0,
mouse_events: Default::default(),
alt_screen: false,
scroll: Some(ScrollOp {
top: 0,
bottom: 23,
count: 3,
}),
spans: vec![],
link_table: vec![],
overlay: Default::default(),
};
assert_eq!(decode(&encode(&frame)).expect("decode"), frame);
}
#[test]
fn round_trip_full_frame_kind() {
let frame = Frame {
cols: 40,
rows: 12,
kind: FrameKind::Full,
cursor_row: 0,
cursor_col: 0,
cursor_visible: true,
cursor_shape: justerm_core::CursorShape::Block,
cursor_blink: false,
display_offset: 0,
scrollback_len: 0,
evicted_total: 0,
marker_epoch: 0,
marker_count: 0,
mouse_events: Default::default(),
alt_screen: false,
scroll: None,
spans: vec![],
link_table: vec![],
overlay: Default::default(),
};
assert_eq!(decode(&encode(&frame)).expect("decode"), frame);
}
#[test]
fn engine_frame_captures_written_cells() {
let mut term = Engine::new(5, 2);
term.feed(b"hi");
let f = term.frame();
assert_eq!((f.cols, f.rows), (5, 2));
assert_eq!(f.kind, FrameKind::Partial);
let chars: String = f
.spans
.iter()
.filter(|s| s.line == 0)
.flat_map(|s| s.cells.iter().map(|c| c.c()))
.collect();
assert!(chars.contains('h') && chars.contains('i'), "got {chars:?}");
}
#[test]
fn engine_frame_reports_cursor_position() {
let mut term = Engine::new(80, 24);
term.feed(b"\x1b[10;20H"); let f = term.frame();
assert_eq!((f.cursor_row, f.cursor_col), (9, 19));
assert!(f.cursor_visible, "cursor is visible by default");
}
#[test]
fn engine_frame_reports_cursor_visibility_via_dectcem() {
let mut term = Engine::new(80, 24);
term.feed(b"\x1b[?25l"); assert!(!term.frame().cursor_visible, "hidden after ?25l");
term.feed(b"\x1b[?25h"); assert!(term.frame().cursor_visible, "visible again after ?25h");
}
#[test]
fn engine_cursor_visibility_is_independent_of_alt_screen() {
let mut term = Engine::new(80, 24);
term.feed(b"\x1b[?1049h\x1b[?25l\x1b[?1049l");
assert!(
!term.frame().cursor_visible,
"?25l on alt must persist after ?1049l"
);
let mut term = Engine::new(80, 24);
term.feed(b"\x1b[?25l\x1b[?1049h\x1b[?25h\x1b[?1049l");
assert!(
term.frame().cursor_visible,
"?25h on alt must persist after ?1049l"
);
}
#[test]
fn engine_frame_cursor_survives_resize_shrink() {
let mut term = Engine::new(80, 24);
term.feed(b"\x1b[20;70H"); term.reset_damage(); term.resize(10, 5); let f = term.frame(); assert!(
f.cursor_row < 5 && f.cursor_col < 10,
"cursor clamped to new bounds, got ({}, {})",
f.cursor_row,
f.cursor_col
);
}
#[test]
fn engine_frame_carries_scroll_op() {
let mut term = Engine::new(5, 2);
term.feed(b"x\r\ny\r\nz"); assert!(term.frame().scroll.is_some());
}
#[test]
fn engine_frame_is_full_after_resize() {
let mut term = Engine::new(5, 2);
term.feed(b"hi");
term.resize(6, 3);
let f = term.frame();
assert_eq!(f.kind, FrameKind::Full);
assert_eq!((f.cols, f.rows), (6, 3));
assert_eq!(f.spans.len(), 3, "Full ships every row");
}
#[test]
fn engine_frame_ships_only_live_combining_clusters() {
let mut term = Engine::new(5, 1);
term.feed("e\u{0301}".as_bytes()); term.feed(b"\rx"); term.feed("o\u{0308}".as_bytes()); let f = term.frame();
let carried: Vec<_> = f
.spans
.iter()
.flat_map(|s| s.combining.iter().map(move |(&col, c)| (s, col, c)))
.collect();
assert_eq!(carried.len(), 1, "only the live cluster ships");
let (span, col, cluster) = &carried[0];
assert_eq!(*cluster, &vec!['\u{0308}'], "the diaeresis, not the acute");
assert_eq!(span.cells[*col].c(), 'o');
assert!(span.cells[*col].is_combined());
}
#[test]
fn engine_frame_round_trips_through_bytes() {
let mut term = Engine::new(8, 1);
term.feed("\x1b[31m한e\u{0301}".as_bytes());
let f = term.frame();
assert_eq!(decode(&encode(&f)).expect("decode"), f);
}
#[test]
fn engine_frame_round_trips_real_captures() {
for raw in [
include_bytes!("fixtures/vim_redraw.raw").as_slice(),
include_bytes!("fixtures/top.raw").as_slice(),
include_bytes!("fixtures/htop.raw").as_slice(),
] {
let mut term = Engine::new(80, 24);
term.feed(raw);
let f = term.frame();
assert_eq!(
decode(&encode(&f)).expect("decode"),
f,
"real-capture round-trip"
);
}
}
#[test]
fn engine_frame_with_a_long_scroll_run_round_trips() {
let mut term = Engine::new(80, 24);
term.reset_damage();
term.feed(&vec![b'\n'; 40_000]);
let f = term.frame();
let op = f.scroll.expect("the run must still report a scroll op");
assert_eq!((op.top, op.bottom), (0, 23));
assert_eq!(decode(&encode(&f)).expect("decode"), f);
}
#[test]
fn a_scroll_count_at_the_wire_bound_round_trips() {
let frame = Frame {
cols: 4,
rows: 40_000,
kind: FrameKind::Partial,
cursor_row: 0,
cursor_col: 0,
cursor_visible: true,
cursor_shape: justerm_core::CursorShape::Block,
cursor_blink: false,
display_offset: 0,
scrollback_len: 0,
evicted_total: 0,
marker_epoch: 0,
marker_count: 0,
mouse_events: Default::default(),
alt_screen: false,
scroll: Some(ScrollOp {
top: 0,
bottom: 39_999,
count: i16::MAX as isize,
}),
spans: vec![],
link_table: vec![],
overlay: Default::default(),
};
assert_eq!(decode(&encode(&frame)).expect("decode"), frame);
let down = Frame {
scroll: Some(ScrollOp {
top: 0,
bottom: 39_999,
count: -(i16::MAX as isize),
}),
..frame
};
assert_eq!(decode(&encode(&down)).expect("decode"), down);
}
#[test]
fn engine_frame_round_trips_hyperlink_and_combining_captures() {
let mut term = Engine::new(80, 24);
term.feed(include_bytes!("fixtures/hyperlink_combining.raw"));
let f = term.frame();
let linked: usize = f.spans.iter().map(|s| s.links.len()).sum();
let combining: usize = f.spans.iter().map(|s| s.combining.len()).sum();
assert!(linked > 0 && combining > 0, "capture lost its material");
assert!(
f.link_table.len() < linked,
"one URI is deliberately repeated across cells and rows, so the interned \
table must be strictly smaller than the linked-cell count — this ratio is \
what makes the capture able to tell the two candidate wire shapes apart",
);
assert!(
f.spans
.iter()
.any(|s| s.links.keys().any(|c| s.combining.contains_key(c))),
"capture must carry a cell that is both linked and combining",
);
assert_eq!(decode(&encode(&f)).expect("decode"), f);
let mut term = Engine::new(80, 24);
term.feed(include_bytes!("fixtures/ls_hyperlink.raw"));
let f = term.frame();
assert!(
f.spans.iter().map(|s| s.links.len()).sum::<usize>() > 0,
"real `ls --hyperlink` capture lost its links",
);
assert_eq!(decode(&encode(&f)).expect("decode"), f);
}
fn header_with_one_span(cols: u16, rows: u16, line: u16, left: u16, right: u16) -> Vec<u8> {
let mut b = Vec::new();
b.extend_from_slice(b"JT"); b.push(justerm_core::WIRE_VERSION); b.push(0); b.push(1); b.extend_from_slice(&cols.to_le_bytes());
b.extend_from_slice(&rows.to_le_bytes());
b.extend_from_slice(&0u16.to_le_bytes()); b.extend_from_slice(&0u16.to_le_bytes()); b.push(1); b.push(0); b.push(0); b.extend_from_slice(&0u32.to_le_bytes()); b.extend_from_slice(&0u32.to_le_bytes()); b.extend_from_slice(&0u64.to_le_bytes()); b.extend_from_slice(&0u32.to_le_bytes()); b.extend_from_slice(&0u32.to_le_bytes()); b.push(0); b.push(0); b.extend_from_slice(&1u16.to_le_bytes()); b.extend_from_slice(&line.to_le_bytes());
b.extend_from_slice(&left.to_le_bytes());
b.extend_from_slice(&right.to_le_bytes());
b
}
#[test]
fn decode_rejects_span_with_left_past_right() {
let b = header_with_one_span(80, 24, 0, 5, 0); assert_eq!(
decode(&b),
Err(justerm_core::DecodeError::BadSpan),
"left>right must error, not panic"
);
}
#[test]
fn decode_rejects_a_span_outside_the_declared_geometry() {
assert_eq!(
decode(&header_with_one_span(4, 2, 0, 0, 8)),
Err(justerm_core::DecodeError::BadSpan),
"right = 8 past cols = 4"
);
assert_eq!(
decode(&header_with_one_span(80, 2, 99, 0, 3)),
Err(justerm_core::DecodeError::BadSpan),
"line = 99 past rows = 2"
);
assert_eq!(
decode(&header_with_one_span(4, 2, 1, 0, 3)),
Err(justerm_core::DecodeError::Truncated),
"the last legal span of the frame is accepted, then runs out of cells"
);
}
#[test]
fn decode_rejects_span_length_u16_overflow() {
let b = header_with_one_span(80, 24, 0, 0, 65535); assert_eq!(
decode(&b),
Err(justerm_core::DecodeError::BadSpan),
"a span past the frame's own width is rejected before its length is computed"
);
}
#[test]
fn round_trip_full_frame_with_cells() {
let row = |line| Span {
line,
left: 0,
right: 2,
cells: "abc"
.chars()
.map(|c| Cell::from_parts(c, Color::Default, Color::Default, CellFlags::empty()))
.collect(),
combining: BTreeMap::new(),
links: BTreeMap::new(),
ucolors: BTreeMap::new(),
};
let frame = Frame {
cols: 3,
rows: 2,
kind: FrameKind::Full,
cursor_row: 0,
cursor_col: 0,
cursor_visible: true,
cursor_shape: justerm_core::CursorShape::Block,
cursor_blink: false,
display_offset: 0,
scrollback_len: 0,
evicted_total: 0,
marker_epoch: 0,
marker_count: 0,
mouse_events: Default::default(),
alt_screen: false,
scroll: None,
spans: vec![row(0), row(1)],
link_table: vec![],
overlay: Default::default(),
};
assert_eq!(decode(&encode(&frame)).expect("decode"), frame);
}
#[test]
fn round_trip_negative_scroll_count() {
let frame = Frame {
cols: 80,
rows: 24,
kind: FrameKind::Partial,
cursor_row: 0,
cursor_col: 0,
cursor_visible: true,
cursor_shape: justerm_core::CursorShape::Block,
cursor_blink: false,
display_offset: 0,
scrollback_len: 0,
evicted_total: 0,
marker_epoch: 0,
marker_count: 0,
mouse_events: Default::default(),
alt_screen: false,
scroll: Some(ScrollOp {
top: 2,
bottom: 23,
count: -4,
}),
spans: vec![],
link_table: vec![],
overlay: Default::default(),
};
assert_eq!(decode(&encode(&frame)).expect("decode"), frame);
}
#[test]
fn engine_frame_undamaged_is_empty_partial_not_full() {
let mut term = Engine::new(5, 2);
term.feed(b"hi");
term.reset_damage(); let f = term.frame();
assert_eq!(f.kind, FrameKind::Partial);
assert!(f.spans.is_empty(), "no damage since ack -> no spans");
assert!(f.scroll.is_none());
}