use super::backend::{SpawnRequest, spawn};
use super::wait::wait_pid_status;
use super::*;
use std::sync::{Mutex, OnceLock};
use std::time::Duration;
fn signal_test_lock() -> &'static Mutex<()> {
static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
LOCK.get_or_init(|| Mutex::new(()))
}
extern "C" fn test_signal_handler(_: i32) {}
fn wait_pid_for_test(pid: libc::pid_t) -> i32 {
wait_pid_status(pid).unwrap_or(128)
}
#[test]
fn string_stdio_in_provides_fd() {
let input = StringStdioIn::new("line1\nline2\n");
let fd = input.fd();
let line1 = fd.read_line().unwrap();
assert_eq!(line1, Some("line1".to_string()));
let line2 = fd.read_line().unwrap();
assert_eq!(line2, Some("line2".to_string()));
let eof = fd.read_line().unwrap();
assert_eq!(eof, None);
input.join();
println!("StringStdioIn provides readable fd");
}
#[test]
fn string_stdio_out_collects_output() {
let output = StringStdioOut::new();
let fd = output.fd();
fd.write_str("hello").unwrap();
fd.write_line(" world").unwrap();
let collected = output.collect();
assert_eq!(collected, "hello world\n");
println!("StringStdioOut collects output from fd");
}
#[test]
fn string_stdio_in_large_input() {
let big = "x".repeat(200_000) + "\n";
let input = StringStdioIn::new(&big);
let fd = input.fd();
let line = fd.read_line().unwrap();
assert_eq!(line, Some("x".repeat(200_000)));
input.join();
println!("StringStdioIn handles input larger than pipe buffer");
}
#[test]
fn string_stdio_out_large_output() {
let output = StringStdioOut::new();
let fd = output.fd();
let big = "y".repeat(200_000);
fd.write_str(&big).unwrap();
let collected = output.collect();
assert_eq!(collected.len(), 200_000);
println!("StringStdioOut handles output larger than pipe buffer");
}
#[test]
fn os_pipe_read_write() {
let pipe = OsPipe::new().unwrap();
pipe.write_fd.write_line("test line").unwrap();
pipe.write_fd.close();
let line = pipe.read_fd.read_line().unwrap();
assert_eq!(line, Some("test line".to_string()));
let eof = pipe.read_fd.read_line().unwrap();
assert_eq!(eof, None);
pipe.read_fd.close();
println!("OsPipe read/write works");
}
#[test]
fn read_line_fd_shares_buffer_with_dup() {
let pipe = OsPipe::new().unwrap();
pipe.write_fd.write_str("first\nsecond\n").unwrap();
pipe.write_fd.close();
let dup = pipe.read_fd.dup().unwrap();
let first = pipe.read_fd.read_line().unwrap();
let second = dup.read_line().unwrap();
let eof = pipe.read_fd.read_line().unwrap();
assert_eq!(first, Some("first".to_string()));
assert_eq!(second, Some("second".to_string()));
assert_eq!(eof, None);
dup.close();
pipe.read_fd.close();
}
#[test]
fn write_all_fd_reports_closed_fd() {
let pipe = OsPipe::new().unwrap();
pipe.write_fd.close();
let err = pipe.write_fd.write_all(b"test").unwrap_err();
assert_eq!(err.raw_os_error(), Some(libc::EBADF));
pipe.read_fd.close();
}
#[test]
fn read_line_fd_retries_after_eintr() {
let _guard = signal_test_lock()
.lock()
.unwrap_or_else(|err| err.into_inner());
let pipe = OsPipe::new().unwrap();
let reader_thread = unsafe { libc::pthread_self() };
let writer = std::thread::spawn(move || {
std::thread::sleep(Duration::from_millis(20));
unsafe {
libc::pthread_kill(reader_thread, libc::SIGUSR1);
}
std::thread::sleep(Duration::from_millis(20));
pipe.write_fd.write_line("after-signal").unwrap();
pipe.write_fd.close();
});
let old_handler = unsafe {
libc::signal(
libc::SIGUSR1,
test_signal_handler as *const () as libc::sighandler_t,
)
};
let line = pipe.read_fd.read_line().unwrap();
unsafe {
libc::signal(libc::SIGUSR1, old_handler);
}
assert_eq!(line, Some("after-signal".to_string()));
pipe.read_fd.close();
writer.join().unwrap();
}
#[test]
fn spawn_echo() {
let pipe = OsPipe::new().unwrap();
let pid = spawn(SpawnRequest {
program: "echo",
argv: &["echo".to_string(), "hello".to_string()],
env: &[],
cwd: Path::new("/"),
create_process_group: false,
passed_fds: &[],
stdio: SpawnStdio {
stdout_fd: pipe.write_fd,
..SpawnStdio::default()
},
fd_actions: &[
FdAction::Close(pipe.read_fd),
FdAction::Close(pipe.write_fd),
],
})
.unwrap();
pipe.write_fd.close();
let output = pipe.read_fd.read_all();
let code = wait_pid_for_test(pid);
assert_eq!(code, 0);
assert_eq!(output.trim(), "hello");
println!("posix_spawn echo: output={output:?}");
}
#[test]
fn spawn_false_returns_nonzero() {
let pid = spawn(SpawnRequest {
program: "false",
argv: &["false".to_string()],
env: &[],
cwd: Path::new("/"),
create_process_group: false,
passed_fds: &[],
stdio: SpawnStdio::default(),
fd_actions: &[],
})
.unwrap();
let code = wait_pid_for_test(pid);
assert_ne!(code, 0);
println!("posix_spawn false returns {code}");
}
#[test]
fn spawn_nonexistent_fails() {
let result = spawn(SpawnRequest {
program: "/nonexistent_binary_12345",
argv: &["/nonexistent_binary_12345".to_string()],
env: &[],
cwd: Path::new("/"),
create_process_group: false,
passed_fds: &[],
stdio: SpawnStdio::default(),
fd_actions: &[],
});
assert!(result.is_err());
println!("spawn nonexistent fails: {}", result.unwrap_err());
}
#[test]
fn spawn_rejects_nul_in_argv() {
let result = spawn(SpawnRequest {
program: "echo",
argv: &["echo\0bad".to_string()],
env: &[],
cwd: Path::new("/"),
create_process_group: false,
passed_fds: &[],
stdio: SpawnStdio::default(),
fd_actions: &[],
});
assert!(result.is_err());
assert_eq!(result.unwrap_err().kind(), std::io::ErrorKind::InvalidInput);
}
#[test]
fn spawn_rejects_nul_in_env() {
let result = spawn(SpawnRequest {
program: "echo",
argv: &["echo".to_string()],
env: &[("K".to_string(), "V\0bad".to_string())],
cwd: Path::new("/"),
create_process_group: false,
passed_fds: &[],
stdio: SpawnStdio::default(),
fd_actions: &[],
});
assert!(result.is_err());
assert_eq!(result.unwrap_err().kind(), std::io::ErrorKind::InvalidInput);
}
#[test]
fn wait_pid_reports_error_for_non_child() {
assert_eq!(wait_pid_for_test(libc::pid_t::MAX), 128);
}
#[test]
fn spawn_with_string_stdio() {
let input = StringStdioIn::new("hello from stdin\n");
let output = StringStdioOut::new();
let pid = spawn(SpawnRequest {
program: "cat",
argv: &["cat".to_string()],
env: &[],
cwd: Path::new("/"),
create_process_group: false,
passed_fds: &[],
stdio: SpawnStdio {
stdin_fd: input.fd(),
stdout_fd: output.fd(),
..SpawnStdio::default()
},
fd_actions: &[],
})
.unwrap();
let collected = output.collect();
input.join();
let code = wait_pid_for_test(pid);
assert_eq!(code, 0);
assert_eq!(collected.trim(), "hello from stdin");
println!("spawn with StringStdio works: {collected:?}");
}
#[test]
fn spawn_pipeline() {
let pipe1 = OsPipe::new().unwrap();
let pipe2 = OsPipe::new().unwrap();
let pid1 = spawn(SpawnRequest {
program: "/bin/echo",
argv: &["echo".to_string(), "hello world".to_string()],
env: &[("PATH".to_string(), "/bin:/usr/bin".to_string())],
cwd: Path::new("/"),
create_process_group: false,
passed_fds: &[],
stdio: SpawnStdio {
stdout_fd: pipe1.write_fd,
..SpawnStdio::default()
},
fd_actions: &[
FdAction::Close(pipe1.read_fd),
FdAction::Close(pipe1.write_fd),
FdAction::Close(pipe2.read_fd),
FdAction::Close(pipe2.write_fd),
],
})
.unwrap();
let pid2 = spawn(SpawnRequest {
program: "/usr/bin/tr",
argv: &["tr".to_string(), "a-z".to_string(), "A-Z".to_string()],
env: &[("PATH".to_string(), "/bin:/usr/bin".to_string())],
cwd: Path::new("/"),
create_process_group: false,
passed_fds: &[],
stdio: SpawnStdio {
stdin_fd: pipe1.read_fd,
stdout_fd: pipe2.write_fd,
..SpawnStdio::default()
},
fd_actions: &[
FdAction::Close(pipe1.read_fd),
FdAction::Close(pipe1.write_fd),
FdAction::Close(pipe2.read_fd),
FdAction::Close(pipe2.write_fd),
],
})
.unwrap();
pipe1.read_fd.close();
pipe1.write_fd.close();
pipe2.write_fd.close();
let output = pipe2.read_fd.read_all();
let code1 = wait_pid_for_test(pid1);
let code2 = wait_pid_for_test(pid2);
assert_eq!(code1, 0);
assert_eq!(code2, 0);
assert_eq!(output.trim(), "HELLO WORLD");
println!("pipeline: {output:?}");
}
#[test]
fn resolve_command_path_finds_known_command() {
let runtime = UnixRuntime::new();
let path = runtime
.resolve_command_path("sh", "/bin:/usr/bin")
.expect("sh should resolve");
assert!(path.is_absolute());
assert!(path.ends_with("sh"));
}
#[test]
fn resolve_command_path_reports_missing_command() {
let runtime = UnixRuntime::new();
let err = runtime
.resolve_command_path("definitely-missing-mxsh-command", "/bin:/usr/bin")
.expect_err("missing command should fail");
assert_eq!(err.kind(), io::ErrorKind::NotFound);
}
#[test]
fn resolve_command_path_reports_permission_denied_for_non_executable_file() {
let runtime = UnixRuntime::new();
let path = std::env::temp_dir().join(format!(
"mxsh-resolve-noexec-{}-{}",
std::process::id(),
std::thread::current().name().unwrap_or("unnamed")
));
std::fs::write(&path, "#!/bin/sh\nexit 0\n").unwrap();
let mut perms = std::fs::metadata(&path).unwrap().permissions();
perms.set_mode(0o644);
std::fs::set_permissions(&path, perms).unwrap();
let err = runtime
.resolve_command_path(
path.to_str().expect("temp path should be utf-8"),
"/bin:/usr/bin",
)
.expect_err("non-executable path should fail");
let _ = std::fs::remove_file(&path);
assert_eq!(err.kind(), io::ErrorKind::PermissionDenied);
}
#[test]
fn resolve_command_path_accepts_explicit_relative_path() {
let runtime = UnixRuntime::new();
let dir = std::env::temp_dir().join(format!("mxsh-resolve-rel-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("run-me");
std::fs::write(&path, "#!/bin/sh\nexit 0\n").unwrap();
let mut perms = std::fs::metadata(&path).unwrap().permissions();
perms.set_mode(0o755);
std::fs::set_permissions(&path, perms).unwrap();
let cwd = std::env::current_dir().unwrap();
std::env::set_current_dir(&dir).unwrap();
let resolved = runtime
.resolve_command_path("./run-me", "/bin:/usr/bin")
.expect("explicit relative path should resolve");
std::env::set_current_dir(cwd).unwrap();
let _ = std::fs::remove_file(&path);
let _ = std::fs::remove_dir(&dir);
assert_eq!(resolved, PathBuf::from("./run-me"));
}
#[test]
fn non_exec_runtimes_report_exec_replace_as_unsupported() {
let in_memory = InMemoryRuntime::new();
let err = in_memory
.exec_replace("echo", &["echo".to_string()], &[], Path::new("/"))
.expect_err("in-memory runtime should not support exec");
assert_eq!(err.kind(), io::ErrorKind::Unsupported);
let deterministic = DeterministicRuntime::new();
let err = deterministic
.exec_replace("echo", &["echo".to_string()], &[], Path::new("/"))
.expect_err("deterministic runtime should not support exec");
assert_eq!(err.kind(), io::ErrorKind::Unsupported);
}