#![allow(clippy::print_stdout, clippy::print_stderr)]
use std::io::{BufRead, BufReader, Read, Write};
use std::path::Path;
use std::process::{Child, Command, ExitStatus, Stdio};
use std::time::{Duration, Instant};
use capsule_runtime::diagnostic;
const PROBE_DIR: &str = "MUR_TEST_CLOSED_STDOUT_PROBE_DIR";
const READY: &str = "closed-stdout-probe-ready";
const CLOSED: &str = "reader-has-gone";
const OUTCOME: &str = "outcome.txt";
const RELAYED: &str = "a delegated child's line, written after the parent's reader went\n";
const DIAGNOSTIC: &str = "a diagnostic raised after the readiness line";
const HANDSHAKE_TIMEOUT: Duration = Duration::from_secs(30);
const CASE: &str = "the_runtimes_writers_survive_a_real_closed_standard_output";
#[test]
fn the_runtimes_writers_survive_a_real_closed_standard_output() {
match std::env::var_os(PROBE_DIR) {
Some(shared) => probe(Path::new(&shared)),
None => supervise(),
}
}
fn supervise() {
let shared = tempfile::tempdir().expect("shared directory");
let mut probe = Command::new(std::env::current_exe().expect("this test binary"))
.args(["--exact", CASE, "--nocapture", "--test-threads=1"])
.env(PROBE_DIR, shared.path())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("re-execute this test binary");
let stderr = probe.stderr.take().expect("the probe's stderr");
let notes = std::thread::spawn(move || {
let mut text = String::new();
let _ = BufReader::new(stderr).read_to_string(&mut text);
text
});
let stdout = probe.stdout.take().expect("the probe's stdout");
let mut lines = BufReader::new(stdout).lines();
let announced = lines.any(|line| line.is_ok_and(|text| text.contains(READY)));
drop(lines);
let notes = || notes.join().unwrap_or_default();
assert!(
announced,
"the probe never reported itself ready:\n{}",
notes()
);
std::fs::write(shared.path().join(CLOSED), b"").expect("release the probe");
let status = wait_for_exit(&mut probe);
let notes = notes();
assert!(
status.success(),
"the probe did not survive writing to its closed stdout ({status}):\n{notes}"
);
#[cfg(unix)]
{
use std::os::unix::process::ExitStatusExt;
assert_eq!(
status.signal(),
None,
"the probe died by signal rather than exiting:\n{notes}"
);
}
assert_eq!(
std::fs::read_to_string(shared.path().join(OUTCOME))
.unwrap_or_else(|error| panic!("the probe wrote no outcome file: {error}\n{notes}")),
"raw=Logged\nline=Logged\n",
"a refused write must report itself kept, not written"
);
let log = shared
.path()
.join("session")
.join("logs")
.join("bootstrap.log");
let kept = std::fs::read_to_string(&log)
.unwrap_or_else(|error| panic!("no bootstrap.log at {}: {error}", log.display()));
assert!(
kept.contains(RELAYED.trim_end_matches('\n')),
"the relayed child line is not in bootstrap.log:\n{kept}"
);
assert!(
kept.contains(DIAGNOSTIC),
"the runtime diagnostic is not in bootstrap.log:\n{kept}"
);
}
fn probe(shared: &Path) -> ! {
println!("{READY}");
let _ = std::io::stdout().flush();
wait_for_closed_pipe(&shared.join(CLOSED));
diagnostic::set_diagnostic_workdir(&shared.join("session"));
let raw = diagnostic::raw_to_stdout(RELAYED);
let line = diagnostic::to_stdout(DIAGNOSTIC);
std::fs::write(
shared.join(OUTCOME),
format!("raw={raw:?}\nline={line:?}\n"),
)
.expect("write the outcome file");
std::process::exit(0);
}
fn wait_for_closed_pipe(flag: &Path) {
let deadline = Instant::now() + HANDSHAKE_TIMEOUT;
while Instant::now() < deadline {
if flag.exists() {
return;
}
std::thread::sleep(Duration::from_millis(10));
}
panic!("the supervisor never closed its end of the pipe");
}
fn wait_for_exit(probe: &mut Child) -> ExitStatus {
let deadline = Instant::now() + HANDSHAKE_TIMEOUT;
while Instant::now() < deadline {
if let Some(status) = probe.try_wait().expect("read the probe's status") {
return status;
}
std::thread::sleep(Duration::from_millis(20));
}
let _ = probe.kill();
panic!("the probe neither finished nor died within {HANDSHAKE_TIMEOUT:?}");
}