121 lines
3.2 KiB
Rust
121 lines
3.2 KiB
Rust
//! Terminal lifecycle wrapper that restores state on success, error, and panic.
|
|
|
|
use std::{
|
|
io,
|
|
panic::{AssertUnwindSafe, catch_unwind, resume_unwind},
|
|
};
|
|
|
|
pub fn run<T, R, I, C, F>(initialize: I, mut restore: C, operation: F) -> io::Result<R>
|
|
where
|
|
I: FnOnce() -> io::Result<T>,
|
|
C: FnMut() -> io::Result<()>,
|
|
F: FnOnce(&mut T) -> io::Result<R>,
|
|
{
|
|
let mut terminal = initialize()?;
|
|
let outcome = catch_unwind(AssertUnwindSafe(|| operation(&mut terminal)));
|
|
let restore_result = restore();
|
|
|
|
match outcome {
|
|
Ok(Ok(value)) => restore_result.map(|()| value),
|
|
Ok(Err(error)) => Err(error),
|
|
Err(payload) => resume_unwind(payload),
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use std::{
|
|
cell::Cell,
|
|
io::{self, ErrorKind},
|
|
panic,
|
|
rc::Rc,
|
|
};
|
|
|
|
use super::run;
|
|
|
|
#[test]
|
|
fn restores_after_normal_exit() {
|
|
let restored = Rc::new(Cell::new(false));
|
|
let observed = Rc::clone(&restored);
|
|
let result = run(
|
|
|| Ok(()),
|
|
move || {
|
|
observed.set(true);
|
|
Ok(())
|
|
},
|
|
|_| Ok(42),
|
|
);
|
|
assert_eq!(result.expect("normal run"), 42);
|
|
assert!(restored.get());
|
|
}
|
|
|
|
#[test]
|
|
fn restores_after_application_error() {
|
|
let restored = Rc::new(Cell::new(false));
|
|
let observed = Rc::clone(&restored);
|
|
let result = run(
|
|
|| Ok(()),
|
|
move || {
|
|
observed.set(true);
|
|
Ok(())
|
|
},
|
|
|_| Err::<(), _>(io::Error::other("application failed")),
|
|
);
|
|
assert_eq!(
|
|
result.expect_err("error must be preserved").kind(),
|
|
ErrorKind::Other
|
|
);
|
|
assert!(restored.get());
|
|
}
|
|
|
|
#[test]
|
|
fn restores_before_resuming_a_panic() {
|
|
let restored = Rc::new(Cell::new(false));
|
|
let observed = Rc::clone(&restored);
|
|
let outcome = panic::catch_unwind(panic::AssertUnwindSafe(|| {
|
|
let _ = run(
|
|
|| Ok(()),
|
|
move || {
|
|
observed.set(true);
|
|
Ok(())
|
|
},
|
|
|_| -> io::Result<()> { panic!("application panicked") },
|
|
);
|
|
}));
|
|
assert!(outcome.is_err());
|
|
assert!(restored.get());
|
|
}
|
|
|
|
#[test]
|
|
fn initialization_failure_does_not_claim_an_active_terminal() {
|
|
let restored = Rc::new(Cell::new(false));
|
|
let observed = Rc::clone(&restored);
|
|
let result = run(
|
|
|| Err::<(), _>(io::Error::new(ErrorKind::NotFound, "no terminal")),
|
|
move || {
|
|
observed.set(true);
|
|
Ok(())
|
|
},
|
|
|_| Ok(()),
|
|
);
|
|
assert_eq!(
|
|
result.expect_err("initialization must fail").kind(),
|
|
ErrorKind::NotFound
|
|
);
|
|
assert!(!restored.get());
|
|
}
|
|
|
|
#[test]
|
|
fn restoration_failure_is_reported_after_success() {
|
|
let result = run(
|
|
|| Ok(()),
|
|
|| Err(io::Error::new(ErrorKind::BrokenPipe, "restore failed")),
|
|
|_| Ok(()),
|
|
);
|
|
assert_eq!(
|
|
result.expect_err("restore must fail").kind(),
|
|
ErrorKind::BrokenPipe
|
|
);
|
|
}
|
|
}
|