use justerm_core::{DecodeError, Engine, Frame, FrameKind, MIN_COLUMNS, decode, encode};
const COLS_AT: usize = 5;
const ROWS_AT: usize = 7;
fn bare(cols: u16, rows: u16) -> Frame {
Frame {
cols,
rows,
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(),
}
}
#[test]
fn a_frame_declaring_fewer_than_two_columns_is_rejected() {
for cols in [0u16, 1] {
assert_eq!(
decode(&encode(&bare(cols, 24))),
Err(DecodeError::BadGeometry),
"cols = {cols} is below MIN_COLUMNS, a width the engine clamps away at every \
entry point — no frame it produces can declare it"
);
}
}
#[test]
fn a_frame_declaring_no_rows_is_rejected() {
assert_eq!(
decode(&encode(&bare(80, 0))),
Err(DecodeError::BadGeometry),
"the row floor is 1 — `Term::with_scrollback` clamps `rows` to it, and xterm.js's \
MINIMUM_ROWS is the same number. A 0-row terminal is not a small screen, it is \
not a screen"
);
}
#[test]
fn the_floor_is_exactly_the_engines_own_floor() {
assert_eq!(
MIN_COLUMNS, 2,
"the check is pinned to the engine's constant"
);
let smallest = bare(MIN_COLUMNS as u16, 1);
assert_eq!(
decode(&encode(&smallest)),
Ok(smallest),
"2×1 is the narrowest screen the engine can hold, so it is a frame it can produce"
);
}
#[test]
fn the_upper_end_is_left_alone_because_the_field_already_bounds_it() {
let widest = bare(u16::MAX, u16::MAX);
assert_eq!(
decode(&encode(&widest)),
Ok(widest),
"MAX_COLUMNS/MAX_ROWS are u16::MAX because the header field is u16 — the ceiling \
is representational and needs no check. A rejection here would be the only \
arbitrary constant in the stack"
);
}
#[test]
fn shrinking_a_real_frames_declared_geometry_below_the_floor_is_rejected() {
let mut e = Engine::new(9, 2);
e.feed(b"abcdefghi");
let frame = e.frame();
let clean = encode(&frame);
assert_eq!(decode(&clean), Ok(frame), "unpatched, it round-trips");
let mut narrow = clean.clone();
narrow[COLS_AT..COLS_AT + 2].copy_from_slice(&1u16.to_le_bytes());
assert_eq!(
decode(&narrow),
Err(DecodeError::BadGeometry),
"the header is judged before its payload is: this frame's span is *also* out of \
bounds for a 1-column grid, and reporting BadSpan would name the consequence \
instead of the cause"
);
let mut short = clean;
short[ROWS_AT..ROWS_AT + 2].copy_from_slice(&0u16.to_le_bytes());
assert_eq!(
decode(&short),
Err(DecodeError::BadGeometry),
"same on the row axis, and same reason for the variant"
);
}
#[test]
fn no_geometry_this_engine_produces_is_below_the_floor() {
let mut frames = 0usize;
for cols in 0usize..=4 {
for rows in 0usize..=3 {
let mut e = Engine::new(cols, rows);
e.feed("ab한cd\r\nef".as_bytes());
let frame = e.frame();
assert!(
frame.cols >= MIN_COLUMNS as u16 && frame.rows >= 1,
"Engine::new({cols}, {rows}) produced a frame declaring \
{}x{} — below the floor its own decoder now enforces",
frame.cols,
frame.rows
);
assert!(
decode(&encode(&frame)).is_ok(),
"Engine::new({cols}, {rows}) produced a frame its own decoder rejects"
);
frames += 1;
}
}
assert_eq!(
frames, 20,
"the sweep is the evidence; it must not silently shrink"
);
}