use scosh_core::{
AcknowledgeResult, CoreError, Dimensions, InputOutcome, Session, SessionEvent, SessionOptions,
StateRevision, TerminalCell, TerminalCellStyle, TerminalCursor, TerminalDelta,
TerminalSnapshot, TerminalState, TerminalStateDelta, TerminalStateRow,
};
fn blank_state(dimensions: Dimensions) -> TerminalState {
let blank = TerminalCell::new(Vec::new(), TerminalCellStyle::default()).unwrap();
TerminalState::new(
dimensions,
vec![blank; usize::from(dimensions.columns()) * usize::from(dimensions.rows())],
TerminalCursor {
column: 0,
row: 0,
visible: true,
},
false,
0,
)
.unwrap()
}
fn delta(base: StateRevision, revision: StateRevision, state: &TerminalState) -> TerminalDelta {
TerminalDelta::new(
base,
revision,
TerminalStateDelta::new(
state.dimensions,
vec![TerminalStateRow {
row: 0,
cells: state.cells.clone(),
}],
state.cursor,
state.alternate,
state.modes,
)
.unwrap(),
)
.unwrap()
}
#[test]
fn snapshot_and_contiguous_delta_are_authoritative() {
let dimensions = Dimensions::new(2, 1).unwrap();
let mut state = blank_state(dimensions);
let mut session = Session::new(SessionOptions::default());
session
.install_snapshot(TerminalSnapshot::new(StateRevision::new(0), state.clone()).unwrap())
.unwrap();
assert!(matches!(
session.next_event(),
Some(SessionEvent::Snapshot { .. })
));
assert!(matches!(
session.next_event(),
Some(SessionEvent::StatusChanged(_))
));
state.cells[0] = TerminalCell::new(b"x".to_vec(), TerminalCellStyle::default()).unwrap();
session
.apply_delta(&delta(StateRevision::new(0), StateRevision::new(1), &state))
.unwrap();
let event = session.next_event().unwrap();
assert!(matches!(event, SessionEvent::Delta { receipt: None, .. }));
assert_eq!(session.revision(), StateRevision::new(1));
assert_eq!(session.state().unwrap().cells[0].text, b"x");
}
#[test]
fn a_revision_gap_requests_a_snapshot_without_mutating_state() {
let dimensions = Dimensions::new(1, 1).unwrap();
let state = blank_state(dimensions);
let mut session = Session::new(SessionOptions::default());
session
.install_snapshot(TerminalSnapshot::new(StateRevision::new(4), state.clone()).unwrap())
.unwrap();
session.drain_events();
let mut gapped = delta(StateRevision::new(4), StateRevision::new(5), &state);
gapped.revision = StateRevision::new(6);
let error = session.apply_delta(&gapped).unwrap_err();
assert!(matches!(error, CoreError::RevisionGap { .. }));
assert!(matches!(
session.next_event(),
Some(SessionEvent::SnapshotRequired { .. })
));
assert_eq!(session.revision(), StateRevision::new(4));
assert_eq!(session.state().unwrap(), &state);
}
#[test]
fn reliable_effects_need_one_idempotent_commit() {
let dimensions = Dimensions::new(1, 1).unwrap();
let mut state = blank_state(dimensions);
let mut session = Session::new(SessionOptions::default());
session
.install_snapshot(TerminalSnapshot::new(StateRevision::new(0), state.clone()).unwrap())
.unwrap();
session.drain_events();
state.alternate = true;
session
.apply_delta(&delta(StateRevision::new(0), StateRevision::new(1), &state))
.unwrap();
let receipt = match session.next_event().unwrap() {
SessionEvent::Delta {
receipt: Some(receipt),
..
} => receipt,
event => panic!("unexpected event: {event:?}"),
};
assert_eq!(session.acknowledge(receipt), AcknowledgeResult::Committed);
assert_eq!(session.acknowledge(receipt), AcknowledgeResult::Duplicate);
}
#[test]
fn recovery_supersedes_pending_input_instead_of_replaying_it() {
let dimensions = Dimensions::new(1, 1).unwrap();
let mut session = Session::new(SessionOptions::default());
session
.install_snapshot(
TerminalSnapshot::new(StateRevision::new(0), blank_state(dimensions)).unwrap(),
)
.unwrap();
session.drain_events();
session.submit_input(b"echo hi").unwrap();
assert!(matches!(
session.next_event(),
Some(SessionEvent::InputQueued { .. })
));
session.begin_recovery().unwrap();
assert!(matches!(
session.next_event(),
Some(SessionEvent::InputCompleted {
outcome: InputOutcome::Superseded,
..
})
));
assert!(session.pending_input_count() == 0);
}
#[test]
fn a_stalled_consumer_is_bounded() {
let dimensions = Dimensions::new(1, 1).unwrap();
let mut session = Session::new(SessionOptions { event_capacity: 1 });
let snapshot = TerminalSnapshot::new(StateRevision::new(0), blank_state(dimensions)).unwrap();
session.install_snapshot(snapshot).unwrap();
assert!(matches!(
session.install_snapshot(
TerminalSnapshot::new(StateRevision::new(1), blank_state(dimensions)).unwrap()
),
Err(CoreError::ConsumerStalled)
));
}
#[test]
fn close_clears_terminal_state_and_pending_input() {
let dimensions = Dimensions::new(1, 1).unwrap();
let mut session = Session::new(SessionOptions::default());
session
.install_snapshot(
TerminalSnapshot::new(StateRevision::new(0), blank_state(dimensions)).unwrap(),
)
.unwrap();
session.drain_events();
session.submit_input(b"secret").unwrap();
session.close();
assert_eq!(session.status(), scosh_core::SessionStatus::Closed);
assert!(session.state().is_none());
assert_eq!(session.pending_input_count(), 0);
assert!(session.next_event().is_none());
}