use std::path::PathBuf;
use std::process::{Command, Output};
use std::sync::OnceLock;
fn inspect_bin() -> &'static PathBuf {
static BIN: OnceLock<PathBuf> = OnceLock::new();
BIN.get_or_init(|| {
let cargo = std::env::var_os("CARGO").unwrap_or_else(|| "cargo".into());
let status = Command::new(cargo)
.args(["build", "-p", "termlens", "--example", "inspect"])
.status()
.expect("failed to run cargo build for the inspect example");
assert!(status.success(), "cargo build --example inspect failed");
let test_exe = std::env::current_exe().expect("test executable path is available");
let profile_dir = test_exe
.parent()
.and_then(|deps| deps.parent())
.expect("test executable is under target/<profile>/deps");
profile_dir
.join("examples")
.join(format!("inspect{}", std::env::consts::EXE_SUFFIX))
})
}
fn run_inspect(bin: &PathBuf, args: &[&str]) -> Output {
Command::new(bin)
.args(args)
.output()
.expect("failed to run the inspect example")
}
#[test]
fn inspect_runs_and_reports_cli_failures() {
let bin = inspect_bin();
let sized = run_inspect(bin, &["--size", "12x3", "sh", "-c", "stty size"]);
assert!(
sized.status.success(),
"inspect failed: {}",
String::from_utf8_lossy(&sized.stderr)
);
let stdout = String::from_utf8_lossy(&sized.stdout);
assert!(
stdout.contains("3 12"),
"terminal size missing from:\n{stdout}"
);
assert!(
stdout.contains("--- exited: exit code 0 ---"),
"exit status missing from:\n{stdout}"
);
let bad_size = run_inspect(bin, &["--size", "12", "sh"]);
assert_eq!(bad_size.status.code(), Some(1));
assert!(
String::from_utf8_lossy(&bad_size.stderr).contains("expected e.g. 120x40"),
"malformed-size error missing from:\n{}",
String::from_utf8_lossy(&bad_size.stderr)
);
let missing_program = run_inspect(bin, &["/definitely/not/a/program"]);
assert_eq!(missing_program.status.code(), Some(1));
assert!(
String::from_utf8_lossy(&missing_program.stderr).starts_with("inspect:"),
"spawn error missing from:\n{}",
String::from_utf8_lossy(&missing_program.stderr)
);
}
#[test]
fn inspect_clears_and_selectively_sets_the_child_environment() {
let bin = inspect_bin();
let output = Command::new(bin)
.env("TERMLENS_INSPECT_LEAK", "secret")
.args(["--env", "KEPT=yes"])
.args(["sh", "-c", "printf ${TERMLENS_INSPECT_LEAK-unset}:$KEPT"])
.output()
.expect("inspect runs");
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("unset:yes"),
"cleared/selected environment missing from:\n{stdout}"
);
let inherited = Command::new(bin)
.env("TERMLENS_INSPECT_INHERITED", "yes")
.args(["--inherit-env"])
.args(["sh", "-c", "printf inherited:$TERMLENS_INSPECT_INHERITED"])
.output()
.expect("inspect runs");
let stdout = String::from_utf8_lossy(&inherited.stdout);
assert!(
stdout.contains("inherited:yes"),
"inherited environment missing from:\n{stdout}"
);
}
#[test]
fn inspect_survives_a_reader_that_closes_early() {
use std::io::Read;
use std::process::Stdio;
let bin = inspect_bin();
let mut child = Command::new(bin)
.args(["--size", "200x1000", "sh", "-c", "yes | head -n 2000"])
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("failed to spawn the inspect example");
let mut stdout = child.stdout.take().expect("stdout is piped");
let mut first = [0u8; 16];
let _ = stdout.read(&mut first);
drop(stdout);
let status = child.wait().expect("inspect did not exit");
let mut stderr = String::new();
child
.stderr
.take()
.expect("stderr is piped")
.read_to_string(&mut stderr)
.expect("stderr is readable");
assert!(!stderr.contains("panicked"), "inspect panicked:\n{stderr}");
assert_eq!(status.code(), Some(0), "stderr:\n{stderr}");
}
#[test]
fn inspect_prints_its_usage_for_help_and_for_a_missing_program() {
let bin = inspect_bin();
for flag in ["--help", "-h"] {
let help = run_inspect(bin, &[flag]);
assert_eq!(
help.status.code(),
Some(0),
"{flag} is a successful request"
);
let stdout = String::from_utf8_lossy(&help.stdout);
assert!(
stdout.starts_with(
"usage: inspect [--size COLSxROWS] [--timeout SECONDS] [--idle MILLIS]"
),
"{flag} must print the usage to stdout, got:\n{stdout}"
);
assert!(help.stderr.is_empty(), "{flag} wrote to stderr");
}
let version = run_inspect(bin, &["--version"]);
assert_eq!(version.status.code(), Some(0));
assert!(
String::from_utf8_lossy(&version.stdout)
.contains(&format!("termlens {}", env!("CARGO_PKG_VERSION"))),
"--version names the crate version"
);
let none = run_inspect(bin, &[]);
assert_eq!(
none.status.code(),
Some(1),
"no program is still a usage error"
);
assert!(none.stdout.is_empty());
assert!(
String::from_utf8_lossy(&none.stderr).starts_with("usage: inspect"),
"the same usage goes to stderr:\n{}",
String::from_utf8_lossy(&none.stderr)
);
let unknown = run_inspect(bin, &["--bogus", "sh"]);
assert_eq!(unknown.status.code(), Some(1));
assert!(
String::from_utf8_lossy(&unknown.stderr).contains("unknown option \"--bogus\""),
"an unknown option is refused rather than spawned:\n{}",
String::from_utf8_lossy(&unknown.stderr)
);
}
#[test]
fn inspect_takes_its_deadline_and_silence_window_from_flags() {
let bin = inspect_bin();
for (args, expect) in [
(
&["--timeout", "abc", "sh"][..],
"bad --timeout \"abc\", expected e.g. 30",
),
(
&["--idle", "1.5", "sh"][..],
"bad --idle \"1.5\", expected e.g. 1000",
),
(&["--timeout"][..], "--timeout needs a SECONDS argument"),
(&["--idle"][..], "--idle needs a MILLIS argument"),
] {
let out = run_inspect(bin, args);
assert_eq!(out.status.code(), Some(1), "{args:?}");
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(stderr.contains(expect), "{args:?}: got {stderr:?}");
assert_eq!(
stderr.lines().count(),
1,
"one line, like --size: {stderr:?}"
);
}
let started = std::time::Instant::now();
let cut = run_inspect(
bin,
&[
"--timeout",
"1",
"--idle",
"50",
"sh",
"-c",
"echo painted; sleep 30",
],
);
let elapsed = started.elapsed();
assert!(
cut.status.success(),
"{}",
String::from_utf8_lossy(&cut.stderr)
);
let stdout = String::from_utf8_lossy(&cut.stdout);
assert!(stdout.contains("painted"), "{stdout}");
assert!(
stdout.contains("--- still running at the deadline (killed on exit) ---"),
"{stdout}"
);
assert!(
elapsed < std::time::Duration::from_secs(4),
"a 1s deadline took {elapsed:?}; the flag was not honoured"
);
}
#[cfg(unix)]
#[test]
fn inspect_resolves_a_relative_program_path_from_its_working_directory() {
let bin = inspect_bin();
let scratch = std::path::Path::new(env!("CARGO_TARGET_TMPDIR")).join("inspect-relative");
std::fs::create_dir_all(&scratch).expect("scratch directory");
let echo = scratch.join("echo");
let _ = std::fs::remove_file(&echo);
std::os::unix::fs::symlink("/bin/echo", &echo)
.expect("link /bin/echo into the scratch directory");
let out = Command::new(bin)
.current_dir(&scratch)
.args(["./echo", "relative path resolved"])
.output()
.expect("failed to run the inspect example");
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
out.status.success(),
"inspect failed: {}",
String::from_utf8_lossy(&out.stderr)
);
assert!(stdout.contains("relative path resolved"), "{stdout}");
assert!(stdout.contains("--- exited: exit code 0 ---"), "{stdout}");
}