const RAW_SETUP_COMMAND: &str = "stty raw -echo 2>/dev/null";
#[cfg(unix)]
pub(super) fn pane_command(slot: &super::FinalSinkSlot) -> Vec<String> {
vec!["/bin/sh".to_owned(), "-c".to_owned(), script(slot)]
}
pub(super) fn script(slot: &super::FinalSinkSlot) -> String {
script_with_raw_setup(slot, RAW_SETUP_COMMAND)
}
pub(super) fn script_with_raw_setup(slot: &super::FinalSinkSlot, raw_setup: &str) -> String {
let quote = crate::test_shell::command_quote;
let announce = if slot.bracket_aware {
r"printf '\033[?2004h'"
} else {
":"
};
format!(
"set -u\n\
captured() {{\n\
if [ -e {partial} ]; then\n\
printf '%s' \"$(($(wc -c < {partial})))\"\n\
else\n\
printf '0'\n\
fi\n\
}}\n\
park() {{\n\
i=0\n\
while [ \"$i\" -lt {ticks} ] && [ ! -e {stop} ]; do\n\
sleep 0.05\n\
i=$((i+1))\n\
done\n\
}}\n\
fail() {{\n\
printf '%s' \"$1\" > {error}\n\
park\n\
printf '' > {done}\n\
exit 1\n\
}}\n\
{raw_setup} || fail 'raw mode could not be established: stty raw -echo failed'\n\
{announce} || fail 'the capability announcement could not be written'\n\
printf '' > {ready} || fail 'readiness could not be signalled'\n\
dd bs=1 count={count} of={partial} 2>/dev/null || \
fail \"the capture read failed after $(captured) of {count} bytes\"\n\
[ \"$(captured)\" -eq {count} ] || \
fail \"standard input ended after $(captured) of {count} bytes\"\n\
mv {partial} {out} || fail 'the exact capture could not be published'\n\
park\n\
printf '' > {done}\n",
ready = quote(&slot.path(super::READY_FILE)),
partial = quote(&slot.path(super::OUT_PARTIAL_FILE)),
out = quote(&slot.path(super::OUT_FILE)),
error = quote(&slot.path(super::ERROR_FILE)),
stop = quote(&slot.path(super::STOP_FILE)),
done = quote(&slot.path(super::DONE_FILE)),
count = slot.expected.len(),
ticks = super::CHILD_PARK_SECONDS * 20,
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_shell::final_sink::{
FinalSinkSlot, DONE_FILE, ERROR_FILE, OUT_FILE, OUT_PARTIAL_FILE, READY_FILE, STOP_FILE,
};
fn quoted(slot: &FinalSinkSlot, file: &str) -> String {
crate::test_shell::command_quote(&slot.path(file))
}
fn line_running<'script>(script: &'script str, needle: &str) -> &'script str {
let mut matches = script.lines().filter(|line| line.contains(needle));
let line = matches
.next()
.unwrap_or_else(|| panic!("the script must run {needle:?}:\n{script}"));
assert!(
matches.next().is_none(),
"{needle:?} must appear on exactly one line:\n{script}"
);
line
}
fn assert_guarded(script: &str, needle: &str, reason: &str) {
let line = line_running(script, needle);
assert!(line.contains("|| fail"), "{needle:?} is unchecked:\n{line}");
assert!(
line.contains(reason),
"the {needle:?} guard must name {reason:?}:\n{line}"
);
}
#[test]
fn raw_setup_the_read_the_exact_size_and_publication_are_each_checked_separately() {
let slot = FinalSinkSlot::new("unix-guards", b"payload", true);
let script = script(&slot);
assert_guarded(
&script,
"stty raw -echo",
"raw mode could not be established",
);
assert_guarded(
&script,
r"printf '\033[?2004h'",
"the capability announcement could not be written",
);
assert_guarded(
&script,
"ed(&slot, READY_FILE),
"readiness could not be signalled",
);
assert_guarded(&script, "dd bs=1 count=7", "the capture read failed after");
assert_guarded(
&script,
r#"[ "$(captured)" -eq 7 ]"#,
"standard input ended after",
);
assert_guarded(
&script,
"ed(&slot, OUT_FILE),
"the exact capture could not be published",
);
}
#[test]
fn setup_precedes_readiness_and_the_size_check_precedes_publication() {
let slot = FinalSinkSlot::new("unix-order", b"0123456789", false);
let script = script(&slot);
let at = |needle: &str| {
script
.find(line_running(&script, needle))
.expect("the line came from this script")
};
let raw_setup = at("stty raw -echo");
let readiness = at("ed(&slot, READY_FILE));
let read = at("dd bs=1 count=10");
let size_check = at(r#"[ "$(captured)" -eq 10 ]"#);
let publication = at("ed(&slot, OUT_FILE));
assert!(
raw_setup < readiness,
"raw mode precedes readiness:\n{script}"
);
assert!(readiness < read, "readiness precedes the read:\n{script}");
assert!(
read < size_check,
"the read precedes its size check:\n{script}"
);
assert!(
size_check < publication,
"`out` must never be published before the exact size is proved:\n{script}"
);
}
#[test]
fn out_is_created_only_by_renaming_the_partial() {
let slot = FinalSinkSlot::new("unix-publication", b"abc", true);
let script = script(&slot);
let publication = line_running(&script, "ed(&slot, OUT_FILE));
assert!(
publication.starts_with(&format!(
"mv {} {}",
quoted(&slot, OUT_PARTIAL_FILE),
quoted(&slot, OUT_FILE)
)),
"`out` may only be created by renaming the partial:\n{publication}"
);
}
#[test]
fn every_failure_keeps_the_partial_capture_and_still_acknowledges_teardown() {
let slot = FinalSinkSlot::new("unix-failure-path", b"abc", true);
let script = script(&slot);
let failure_path = script
.split_once("fail() {\n")
.expect("the script defines a failure path")
.1
.split_once("\n}\n")
.expect("the failure path is a shell function")
.0;
assert!(
failure_path.contains("ed(&slot, ERROR_FILE)),
"a failure must write `error`:\n{failure_path}"
);
assert!(
failure_path.contains("\npark\n"),
"a failure must still park so the pane stays resolvable:\n{failure_path}"
);
assert!(
failure_path.contains("ed(&slot, DONE_FILE)),
"teardown must be acknowledged even when the capture failed:\n{failure_path}"
);
assert!(
!failure_path.contains("ed(&slot, OUT_FILE)),
"a failure must never publish `out`:\n{failure_path}"
);
assert!(
!script.contains(&format!("rm {}", quoted(&slot, OUT_PARTIAL_FILE))),
"the partial capture is the evidence and is never removed:\n{script}"
);
}
#[test]
fn the_real_child_always_establishes_raw_mode_with_stty() {
let slot = FinalSinkSlot::new("unix-raw-setup", b"abc", true);
assert_eq!(RAW_SETUP_COMMAND, "stty raw -echo 2>/dev/null");
assert!(script(&slot).starts_with("set -u\n"));
assert!(script(&slot).contains(&format!("\n{RAW_SETUP_COMMAND} || fail ")));
}
#[test]
fn teardown_is_signalled_by_stop_on_both_paths() {
let slot = FinalSinkSlot::new("unix-teardown", b"abc", false);
let script = script(&slot);
assert!(line_running(&script, "ed(&slot, STOP_FILE)).contains("! -e"));
assert_eq!(
script.matches("\npark\n").count(),
2,
"both the failure path and the success path park:\n{script}"
);
assert_eq!(
script.matches("ed(&slot, DONE_FILE)).count(),
2,
"both paths acknowledge teardown:\n{script}"
);
}
#[test]
fn slot_paths_are_quoted_for_the_shell() {
assert_eq!(crate::test_shell::command_quote("a'b"), r"'a'\''b'");
}
#[cfg(unix)]
mod execution {
use super::*;
use std::io::Write;
use std::os::unix::fs::PermissionsExt;
use std::process::{Command, Stdio};
struct ChildRun {
process_id: u32,
status: std::process::ExitStatus,
diagnostic: String,
}
impl ChildRun {
fn launched_process_id(&self) -> u32 {
self.process_id
}
}
impl std::fmt::Display for ChildRun {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
formatter,
"`/bin/sh` process {} finished with {}, reporting: {}",
self.process_id,
self.status,
if self.diagnostic.trim().is_empty() {
"nothing"
} else {
self.diagnostic.trim_end()
}
)
}
}
fn pre_signal_stop(slot: &FinalSinkSlot) {
std::fs::write(slot.directory.join(STOP_FILE), b"1").expect("pre-signal `stop`");
}
fn launch(slot: &FinalSinkSlot, input: &[u8], raw_setup: &str) -> ChildRun {
let mut child = Command::new("/bin/sh")
.arg("-c")
.arg(script_with_raw_setup(slot, raw_setup))
.stdin(Stdio::piped())
.stdout(Stdio::null())
.stderr(Stdio::piped())
.spawn()
.expect("spawn the final-sink child script");
let process_id = child.id();
let offered = child
.stdin
.take()
.expect("the child has a standard input")
.write_all(input);
if let Err(error) = offered {
assert_eq!(
error.kind(),
std::io::ErrorKind::BrokenPipe,
"the payload could not be written to the child: {error}"
);
}
let finished = child
.wait_with_output()
.expect("wait for the final-sink child script");
ChildRun {
process_id,
status: finished.status,
diagnostic: String::from_utf8_lossy(&finished.stderr).into_owned(),
}
}
fn run(slot: &FinalSinkSlot, input: &[u8], raw_setup: &str) -> ChildRun {
pre_signal_stop(slot);
launch(slot, input, raw_setup)
}
const READ_ONLY_SLOT_MODE: u32 = 0o555;
struct ReadOnlySlot<'slot> {
slot: &'slot FinalSinkSlot,
original: std::fs::Permissions,
}
impl<'slot> ReadOnlySlot<'slot> {
fn new(slot: &'slot FinalSinkSlot) -> Self {
let original = slot_permissions(slot);
let mut read_only = original.clone();
read_only.set_mode(READ_ONLY_SLOT_MODE);
std::fs::set_permissions(&slot.directory, read_only)
.expect("make the slot read-only");
Self { slot, original }
}
}
impl Drop for ReadOnlySlot<'_> {
fn drop(&mut self) {
if let Err(error) =
std::fs::set_permissions(&self.slot.directory, self.original.clone())
{
let report = format!(
"the final-sink slot {} could not be restored to mode {:04o}: {error}",
self.slot.directory.display(),
self.original.mode() & 0o7777,
);
if std::thread::panicking() {
eprintln!("{report}");
} else {
panic!("{report}");
}
}
}
}
fn slot_permissions(slot: &FinalSinkSlot) -> std::fs::Permissions {
std::fs::metadata(&slot.directory)
.expect("the slot exists")
.permissions()
}
fn slot_mode(slot: &FinalSinkSlot) -> u32 {
slot_permissions(slot).mode() & 0o7777
}
fn set_slot_mode(slot: &FinalSinkSlot, mode: u32) {
let mut permissions = slot_permissions(slot);
permissions.set_mode(mode);
std::fs::set_permissions(&slot.directory, permissions).expect("set the slot mode");
}
fn slot_file(slot: &FinalSinkSlot, file: &str) -> Option<Vec<u8>> {
std::fs::read(slot.directory.join(file)).ok()
}
#[test]
fn an_exact_capture_is_published_and_teardown_is_acknowledged() {
let payload = "\u{1b}[200~alpha\r\nβ 😀\u{1b}[201~".as_bytes();
let slot = FinalSinkSlot::new("unix-exact", payload, true);
let child = run(&slot, payload, "true");
assert!(
child.status.success(),
"an exact capture must succeed: {child}"
);
assert_eq!(
slot_file(&slot, OUT_FILE).as_deref(),
Some(payload),
"the published capture must be byte-exact"
);
assert!(
slot_file(&slot, OUT_PARTIAL_FILE).is_none(),
"publication renames the partial rather than copying it"
);
assert!(slot_file(&slot, ERROR_FILE).is_none());
assert!(slot_file(&slot, DONE_FILE).is_some());
}
#[test]
fn end_of_input_before_the_expected_count_never_publishes_out() {
let slot = FinalSinkSlot::new("unix-short-eof", b"0123456789", false);
let child = run(&slot, b"01234", "true");
assert_eq!(
child.status.code(),
Some(1),
"a short capture must fail: {child}"
);
assert_eq!(
slot_file(&slot, ERROR_FILE)
.map(|reason| String::from_utf8(reason).expect("the reason is text")),
Some("standard input ended after 5 of 10 bytes".to_owned())
);
assert!(
slot_file(&slot, OUT_FILE).is_none(),
"a short capture is not a complete capture"
);
assert_eq!(
slot_file(&slot, OUT_PARTIAL_FILE).as_deref(),
Some(&b"01234"[..]),
"the bytes that did arrive are the evidence"
);
assert!(slot_file(&slot, DONE_FILE).is_some());
}
#[test]
fn a_failed_raw_mode_never_signals_readiness() {
let slot = FinalSinkSlot::new("unix-raw-failure", b"abc", true);
let child = run(&slot, b"abc", RAW_SETUP_COMMAND);
assert_eq!(
child.status.code(),
Some(1),
"a failed setup must fail the child: {child}"
);
let reported = String::from_utf8(
slot_file(&slot, ERROR_FILE).expect("a failed setup must write `error`"),
)
.expect("the reason is text");
assert!(
reported.contains("raw mode could not be established"),
"unexpected reason: {reported}"
);
assert!(
slot_file(&slot, READY_FILE).is_none(),
"readiness must never be announced from a cooked terminal"
);
assert!(slot_file(&slot, OUT_FILE).is_none());
assert!(slot_file(&slot, DONE_FILE).is_some());
}
#[test]
fn a_slot_it_cannot_write_is_reported_rather_than_silently_skipped() {
let slot = FinalSinkSlot::new("unix-unwritable", b"abc", false);
pre_signal_stop(&slot);
let original_mode = slot_mode(&slot);
let read_only = ReadOnlySlot::new(&slot);
let child = launch(&slot, b"abc", "true");
println!("unwritable slot: {child}");
assert_ne!(
child.launched_process_id(),
std::process::id(),
"the script must have run in a real child process: {child}"
);
assert_eq!(
child.status.code(),
Some(1),
"an unwritable slot must fail the child: {child}"
);
assert!(
child.diagnostic.contains(&slot.path(READY_FILE)),
"the child must have reached the readiness write and been refused: {child}"
);
assert!(
child.diagnostic.contains(&slot.path(ERROR_FILE)),
"the child must have run its failure path: {child}"
);
assert!(
slot_file(&slot, READY_FILE).is_none(),
"readiness must not be claimed when it could not be written"
);
assert!(
slot_file(&slot, ERROR_FILE).is_none(),
"an unwritable slot cannot even hold the child's own reason, \
which is why the shell's diagnostic is the channel here"
);
assert!(slot_file(&slot, OUT_FILE).is_none());
drop(read_only);
assert_eq!(
slot_mode(&slot),
original_mode,
"the case must leave the slot exactly as it found it"
);
}
#[test]
fn a_read_only_slot_is_restored_to_exactly_the_mode_it_had() {
let slot = FinalSinkSlot::new("unix-restore-exact", b"abc", false);
set_slot_mode(&slot, 0o700);
let read_only = ReadOnlySlot::new(&slot);
assert_eq!(
slot_mode(&slot),
READ_ONLY_SLOT_MODE,
"the guard must make the slot read-only"
);
drop(read_only);
assert_eq!(slot_mode(&slot), 0o700, "exactly the original mode returns");
}
#[test]
fn a_panic_while_the_slot_is_read_only_still_restores_its_mode() {
let slot = FinalSinkSlot::new("unix-restore-unwind", b"abc", false);
set_slot_mode(&slot, 0o700);
let unwound: std::thread::Result<()> =
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let _read_only = ReadOnlySlot::new(&slot);
assert_eq!(slot_mode(&slot), READ_ONLY_SLOT_MODE);
panic!("deliberate: the case fails while the slot is read-only");
}));
assert!(unwound.is_err(), "the deliberate panic must have unwound");
assert_eq!(
slot_mode(&slot),
0o700,
"an unwind must restore the exact mode rather than skip restoration"
);
}
}
}