mod common;
use assert_cmd::Command;
use std::fs;
use std::path::PathBuf;
fn rusty_sponge() -> Command {
Command::cargo_bin("rusty-sponge").expect("binary should be built")
}
fn target_in(tmpdir: &tempfile::TempDir, name: &str) -> PathBuf {
tmpdir.path().join(name)
}
#[test]
fn original_target_untouched_when_target_is_directory() {
let tmpdir = common::with_tempdir();
let dir = tmpdir.path().join("a-directory");
fs::create_dir(&dir).unwrap();
let before = fs::read_dir(&dir).unwrap().count();
rusty_sponge()
.arg(&dir)
.write_stdin("some bytes\n")
.assert()
.failure();
let after = fs::read_dir(&dir).unwrap().count();
assert!(dir.is_dir(), "the directory MUST still be a directory");
assert_eq!(
before, after,
"no entries created inside the directory by a failed run"
);
}
#[test]
fn no_leftover_tempfile_in_parent_after_successful_run() {
let tmpdir = common::with_tempdir();
let target = target_in(&tmpdir, "out.txt");
rusty_sponge()
.arg(&target)
.write_stdin("data\n")
.assert()
.success();
let stragglers: Vec<_> = fs::read_dir(tmpdir.path())
.unwrap()
.filter_map(|e| e.ok())
.filter(|e| {
e.file_name()
.to_string_lossy()
.starts_with(".rusty-sponge-")
})
.collect();
assert!(
stragglers.is_empty(),
"no rusty-sponge tempfile should remain after success: {stragglers:?}"
);
}
#[test]
fn replacement_is_durable_after_normal_exit() {
let tmpdir = common::with_tempdir();
let target = target_in(&tmpdir, "durable.txt");
fs::write(&target, b"OLD\n").unwrap();
rusty_sponge()
.arg(&target)
.write_stdin("NEW\n")
.assert()
.success();
assert_eq!(fs::read(&target).unwrap(), b"NEW\n");
}
#[cfg(target_os = "linux")]
mod linux_sigkill {
use super::*;
use std::io::Write;
use std::process::{Command as StdCommand, Stdio};
use std::thread;
use std::time::Duration;
#[test]
#[ignore = "linux-only fault-injection: enable with `cargo test --features test-fault-injection`"]
fn sigkill_during_write_leaves_original_intact() {
let tmpdir = common::with_tempdir();
let target = target_in(&tmpdir, "victim.txt");
let original = b"ORIGINAL_BYTES_THAT_MUST_NOT_CHANGE\n";
fs::write(&target, original).unwrap();
let mut child = StdCommand::new(env!("CARGO_BIN_EXE_rusty-sponge"))
.arg(&target)
.stdin(Stdio::piped())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.expect("spawn rusty-sponge");
let mut stdin = child.stdin.take().expect("stdin pipe");
let _ = stdin.write_all(b"REPLACEMENT_BYTES\n");
thread::sleep(Duration::from_millis(100));
unsafe {
libc::kill(child.id() as libc::pid_t, libc::SIGKILL);
}
let _ = child.wait();
assert_eq!(
fs::read(&target).unwrap(),
original,
"FR-006/SC-002: SIGKILL mid-write must leave the target byte-identical to its prior state"
);
let stragglers: Vec<_> = fs::read_dir(tmpdir.path())
.unwrap()
.filter_map(|e| e.ok())
.filter(|e| {
e.file_name()
.to_string_lossy()
.starts_with(".rusty-sponge-")
})
.collect();
assert!(
stragglers.is_empty(),
"no rusty-sponge tempfile should remain after SIGKILL"
);
}
}