use std::env;
use std::error::Error;
use std::io::{BufRead, BufReader, Read, Write};
use std::path::{Path, PathBuf};
use std::process::{Child, ChildStdout, Command, Stdio};
use std::sync::mpsc::{self, RecvTimeoutError};
use std::thread;
use std::time::Duration;
pub type TestResult = Result<(), Box<dyn Error>>;
const ROLE_ENV: &str = "HAEMATITE_FENCE_PIN_ROLE";
const DIR_ENV: &str = "HAEMATITE_FENCE_PIN_DIR";
const READY_LINE: &str = "HAEMATITE_FENCE_PIN_READY";
const DEADLINE: Duration = Duration::from_secs(30);
const MAX_OUTPUT: u64 = 64 * 1024;
pub struct PinCase {
pub test_path: &'static str,
pub write: fn(&Path) -> TestResult,
pub recover: fn(&Path) -> TestResult,
}
fn harness_error(message: impl Into<String>) -> Box<dyn Error> {
message.into().into()
}
pub fn run_pin(case: &PinCase) -> TestResult {
match env::var(ROLE_ENV).ok().as_deref() {
None => run_parent(case),
Some("writer") => run_writer(case),
Some("recovery") => (case.recover)(&child_dir()?),
Some(other) => Err(harness_error(format!("unknown fence-pin role {other:?}"))),
}
}
fn child_dir() -> Result<PathBuf, Box<dyn Error>> {
env::var_os(DIR_ENV)
.map(PathBuf::from)
.ok_or_else(|| harness_error("missing fence-pin child directory"))
}
fn run_writer(case: &PinCase) -> TestResult {
let dir = child_dir()?;
(case.write)(&dir)?;
let mut stdout = std::io::stdout().lock();
writeln!(stdout, "{READY_LINE}")?;
stdout.flush()?;
drop(stdout);
loop {
thread::park();
}
}
fn run_parent(case: &PinCase) -> TestResult {
let temp = tempfile::tempdir()?;
let dir = temp.path().join("world");
std::fs::create_dir(&dir)?;
let mut writer = spawn_child(case.test_path, "writer", &dir)?;
let readiness = receive_readiness(&mut writer);
let killed = kill_and_wait(writer);
readiness?;
let status = killed?;
if status.success() {
return Err(harness_error(
"writer exited normally instead of being killed",
));
}
run_recovery_child(case.test_path, &dir)
}
fn spawn_child(test_path: &str, role: &str, dir: &Path) -> Result<Child, std::io::Error> {
Command::new(env::current_exe()?)
.arg("--exact")
.arg(test_path)
.arg("--nocapture")
.arg("--test-threads=1")
.env(ROLE_ENV, role)
.env(DIR_ENV, dir)
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::inherit())
.spawn()
}
fn receive_readiness(child: &mut Child) -> TestResult {
let stdout = child
.stdout
.take()
.ok_or_else(|| harness_error("writer readiness pipe is missing"))?;
let (sender, receiver) = mpsc::channel();
let _reader = thread::spawn(move || {
let _ = sender.send(read_readiness(stdout));
});
match receiver.recv_timeout(DEADLINE) {
Ok(Ok(())) => Ok(()),
Ok(Err(message)) => Err(harness_error(message)),
Err(RecvTimeoutError::Timeout) => Err(harness_error("writer readiness deadline expired")),
Err(RecvTimeoutError::Disconnected) => Err(harness_error("writer readiness disconnected")),
}
}
fn read_readiness(stdout: ChildStdout) -> Result<(), String> {
let mut reader = BufReader::new(stdout);
let mut seen = String::new();
loop {
let mut line = String::new();
let bytes = reader
.read_line(&mut line)
.map_err(|error| format!("readiness read failed: {error}"))?;
if bytes == 0 {
return Err(format!(
"writer reached EOF before readiness; output={seen:?}"
));
}
if line.contains(READY_LINE) {
return Ok(());
}
seen.push_str(&line);
if seen.len() as u64 > MAX_OUTPUT {
return Err("writer output exceeded the bounded limit".to_owned());
}
}
}
fn kill_and_wait(mut child: Child) -> Result<std::process::ExitStatus, Box<dyn Error>> {
Child::kill(&mut child).map_err(|error| harness_error(format!("kill failed: {error}")))?;
let (sender, receiver) = mpsc::channel();
let _waiter = thread::spawn(move || {
let _ = sender.send(child.wait());
});
match receiver.recv_timeout(DEADLINE) {
Ok(Ok(status)) => Ok(status),
Ok(Err(error)) => Err(harness_error(format!("child wait failed: {error}"))),
Err(_) => Err(harness_error("child wait deadline expired")),
}
}
fn run_recovery_child(test_path: &str, dir: &Path) -> TestResult {
let mut child = spawn_child(test_path, "recovery", dir)?;
let stdout = child
.stdout
.take()
.ok_or_else(|| harness_error("recovery output pipe is missing"))?;
let (sender, receiver) = mpsc::channel();
let _reader = thread::spawn(move || {
let mut output = Vec::new();
let _ = stdout.take(MAX_OUTPUT + 1).read_to_end(&mut output);
let _ = sender.send(output);
});
let output = receiver
.recv_timeout(DEADLINE)
.map_err(|_| harness_error("recovery output deadline expired"))?;
let status = {
let (sender, receiver) = mpsc::channel();
let _waiter = thread::spawn(move || {
let _ = sender.send(child.wait());
});
receiver
.recv_timeout(DEADLINE)
.map_err(|_| harness_error("recovery wait deadline expired"))?
.map_err(|error| harness_error(format!("recovery wait failed: {error}")))?
};
if !status.success() {
return Err(harness_error(format!(
"recovery child failed with {status}; output:\n{}",
String::from_utf8_lossy(&output)
)));
}
Ok(())
}