use std::io::Read;
use std::path::PathBuf;
use std::process::Command;
use std::time::{Duration, Instant};
use running_process::{spawn, SpawnStdio, StdioSource};
fn testbin_path(name: &str) -> PathBuf {
let output = Command::new(env!("CARGO"))
.args([
"build",
"-p",
"testbins",
"--bin",
name,
"--message-format=json",
])
.stderr(std::process::Stdio::inherit())
.output()
.expect("failed to run cargo build");
assert!(
output.status.success(),
"`cargo build -p {name}` failed with status {}",
output.status,
);
let stdout = String::from_utf8_lossy(&output.stdout);
for line in stdout.lines() {
if !line.contains("\"compiler-artifact\"") || !line.contains(name) {
continue;
}
if let Ok(v) = serde_json::from_str::<serde_json::Value>(line) {
if v["reason"] == "compiler-artifact"
&& v["target"]["kind"]
.as_array()
.is_some_and(|a| a.iter().any(|k| k == "bin"))
{
if let Some(exe) = v["executable"].as_str() {
let p = PathBuf::from(exe);
let deadline = Instant::now() + Duration::from_secs(5);
while !p.exists() && Instant::now() < deadline {
std::thread::sleep(Duration::from_millis(50));
}
assert!(p.exists(), "cargo reported {p:?} but it does not exist");
return p;
}
}
}
}
panic!("`cargo build -p {name}` succeeded but no binary artifact found");
}
fn pipe_stdio() -> SpawnStdio<'static> {
SpawnStdio {
stdin: StdioSource::Null,
stdout: StdioSource::Pipe,
stderr: StdioSource::Null,
drain_timeout: Some(Duration::from_secs(2)),
show_console: false,
}
}
fn pwd_command() -> Command {
Command::new(testbin_path("testbin-cwd-reporter"))
}
fn echo_env_command(var: &str) -> Command {
#[cfg(windows)]
{
let mut c = Command::new("cmd.exe");
c.arg("/D").arg("/S").arg("/C").arg(format!("echo %{var}%"));
c
}
#[cfg(unix)]
{
let mut c = Command::new("sh");
c.arg("-c").arg(format!("printf '%s' \"${{{var}}}\""));
c
}
}
fn spawn_and_capture(mut cmd: Command, stdio: SpawnStdio<'_>) -> Vec<u8> {
let mut child = spawn(&mut cmd, stdio).expect("spawn");
let mut stdout = child.stdout.take().expect("stdout pipe");
let mut buf = Vec::new();
stdout.read_to_end(&mut buf).expect("read_to_end");
let _ = child.wait();
buf
}
fn canon_lossy(p: &std::path::Path) -> PathBuf {
std::fs::canonicalize(p).unwrap_or_else(|_| p.to_path_buf())
}
#[test]
fn cwd_with_spaces() {
let parent = tempfile::tempdir().expect("tempdir");
let dir = parent.path().join("dir with spaces");
std::fs::create_dir(&dir).expect("create_dir");
let mut cmd = pwd_command();
cmd.current_dir(&dir);
let out = spawn_and_capture(cmd, pipe_stdio());
let observed = String::from_utf8_lossy(&out);
let observed_path = canon_lossy(std::path::Path::new(observed.trim()));
let expected_path = canon_lossy(&dir);
assert_eq!(observed_path, expected_path, "cwd with spaces");
}
#[test]
fn cwd_with_unicode() {
let parent = tempfile::tempdir().expect("tempdir");
let dir = parent.path().join("测试_dir_🌍");
if std::fs::create_dir(&dir).is_err() {
eprintln!("skipping: filesystem rejected non-ASCII directory name");
return;
}
let mut cmd = pwd_command();
cmd.current_dir(&dir);
let out = spawn_and_capture(cmd, pipe_stdio());
let observed = String::from_utf8_lossy(&out);
let observed_path = canon_lossy(std::path::Path::new(observed.trim()));
let expected_path = canon_lossy(&dir);
assert_eq!(observed_path, expected_path, "cwd with non-ASCII");
}
#[test]
fn cwd_with_trailing_dot() {
let parent = tempfile::tempdir().expect("tempdir");
let requested = parent.path().join("foo.");
let normalised_on_windows = parent.path().join("foo");
let r = std::fs::create_dir(&requested);
if r.is_err() && !normalised_on_windows.exists() {
eprintln!("skipping: filesystem rejected trailing-dot directory name");
return;
}
let mut cmd = pwd_command();
cmd.current_dir(&requested);
let out = spawn_and_capture(cmd, pipe_stdio());
let observed = String::from_utf8_lossy(&out);
let observed_path = canon_lossy(std::path::Path::new(observed.trim()));
let unix_expected = canon_lossy(&requested);
let windows_expected = canon_lossy(&normalised_on_windows);
assert!(
observed_path == unix_expected || observed_path == windows_expected,
"cwd trailing-dot landed somewhere unexpected: {observed_path:?} \
(expected {unix_expected:?} or {windows_expected:?})"
);
}
#[test]
fn cwd_with_shell_metacharacters() {
let parent = tempfile::tempdir().expect("tempdir");
let dir = parent.path().join("a&b(c)d;e$f^g");
if std::fs::create_dir(&dir).is_err() {
eprintln!("skipping: filesystem rejected metacharacter directory name");
return;
}
let mut cmd = pwd_command();
cmd.current_dir(&dir);
let out = spawn_and_capture(cmd, pipe_stdio());
let observed = String::from_utf8_lossy(&out);
let observed_path = canon_lossy(std::path::Path::new(observed.trim()));
let expected_path = canon_lossy(&dir);
assert_eq!(observed_path, expected_path, "cwd with shell metachars");
}
#[test]
fn cwd_with_long_path() {
let parent = tempfile::tempdir().expect("tempdir");
let mut path = parent.path().to_path_buf();
let segment = "a".repeat(40);
for _ in 0..3 {
path = path.join(&segment);
}
if std::fs::create_dir_all(&path).is_err() {
eprintln!("skipping: filesystem rejected long path");
return;
}
let mut cmd = pwd_command();
cmd.current_dir(&path);
let out = spawn_and_capture(cmd, pipe_stdio());
let observed = String::from_utf8_lossy(&out);
let observed_path = canon_lossy(std::path::Path::new(observed.trim()));
let expected_path = canon_lossy(&path);
assert_eq!(observed_path, expected_path, "cwd at ~200 chars");
}
#[test]
fn env_value_with_newline() {
let sleeper = testbin_path("testbin-sleeper");
let mut cmd = Command::new(&sleeper);
cmd.env("RP_TEST_MULTILINE", "line1\nline2");
let mut child = spawn(&mut cmd, pipe_stdio()).expect("spawn must accept \\n in env value");
let _ = child.kill();
let _ = child.wait();
}
#[test]
fn env_name_reserved_windows_device() {
let sleeper = testbin_path("testbin-sleeper");
let mut cmd = Command::new(&sleeper);
cmd.env("CON", "console-device-name")
.env("NUL", "null-device-name");
let mut child = spawn(&mut cmd, pipe_stdio()).expect("spawn with reserved-name env vars");
let _ = child.kill();
let _ = child.wait();
}
#[test]
fn env_case_sensitivity_difference() {
let mut cmd = echo_env_command("RP_TEST_CASE_X");
cmd.env("RP_TEST_CASE_X", "lowercase-key-value")
.env("RP_TEST_CASE_x", "different-case-value");
let out = spawn_and_capture(cmd, pipe_stdio());
let observed = String::from_utf8_lossy(&out);
let observed = observed.trim_end_matches(['\r', '\n']);
#[cfg(windows)]
{
assert_eq!(
observed, "different-case-value",
"Windows case-folds env names"
);
}
#[cfg(unix)]
{
assert_eq!(
observed, "lowercase-key-value",
"Unix preserves env-name case"
);
}
}
#[test]
fn env_value_with_equals_signs() {
let mut cmd = echo_env_command("RP_TEST_EQUALS");
cmd.env("RP_TEST_EQUALS", "a=b=c=d");
let out = spawn_and_capture(cmd, pipe_stdio());
let observed = String::from_utf8_lossy(&out);
let observed = observed.trim_end_matches(['\r', '\n']);
assert_eq!(
observed, "a=b=c=d",
"env value with embedded `=` must pass through"
);
}
#[test]
fn argv_with_embedded_null_byte() {
let sleeper = testbin_path("testbin-sleeper");
let mut cmd = Command::new(&sleeper);
use std::ffi::OsString;
#[cfg(unix)]
let evil: OsString = {
use std::os::unix::ffi::OsStringExt;
OsString::from_vec(b"hello\0world".to_vec())
};
#[cfg(windows)]
let evil: OsString = {
use std::os::windows::ffi::OsStringExt;
let units: Vec<u16> = vec![104, 101, 108, 108, 111, 0, 119, 111, 114, 108, 100];
OsString::from_wide(&units)
};
cmd.arg(&evil);
let result = spawn(&mut cmd, pipe_stdio());
if let Ok(mut child) = result {
let _ = child.kill();
let _ = child.wait();
eprintln!("note: platform accepted NUL in argv (no error returned)");
}
}
#[cfg(windows)]
#[test]
fn cwd_with_forward_slashes_on_windows() {
let parent = tempfile::tempdir().expect("tempdir");
let real = parent.path().join("subdir");
std::fs::create_dir(&real).expect("create_dir");
let forward = std::path::PathBuf::from(real.to_string_lossy().replace('\\', "/"));
let mut cmd = pwd_command();
cmd.current_dir(&forward);
let out = spawn_and_capture(cmd, pipe_stdio());
let observed = String::from_utf8_lossy(&out);
let observed_path = canon_lossy(std::path::Path::new(observed.trim()));
let expected_path = canon_lossy(&real);
assert_eq!(
observed_path, expected_path,
"Windows must accept forward-slash cwd path"
);
}
#[test]
fn stdout_pipe_does_not_inject_cr() {
let mut cmd = Command::new({
#[cfg(windows)]
{
"cmd.exe"
}
#[cfg(unix)]
{
"sh"
}
});
#[cfg(windows)]
{
cmd.arg("/D")
.arg("/S")
.arg("/C")
.arg("<NUL set /p=\"x\" & cmd /c exit 0");
}
#[cfg(unix)]
{
cmd.arg("-c").arg("printf 'x\\n'");
}
let out = spawn_and_capture(cmd, pipe_stdio());
#[cfg(unix)]
{
assert_eq!(out, b"x\n", "Unix pipe must not inject CR");
}
#[cfg(windows)]
{
assert!(
!out.contains(&b'\r'),
"Windows pipe must not inject CR (got {out:?})"
);
}
}