use super::*;
use std::collections::BTreeMap;
use std::ffi::OsStr;
use std::fs;
use std::path::Path;
use std::process::Command;
#[cfg(unix)]
use std::os::unix::ffi::OsStrExt;
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
#[cfg(unix)]
fn write_file(dir: &Path, name: &OsStr, mode: u32) {
let path = dir.join(Path::new(name));
fs::write(&path, "#!/bin/sh\nexit 0\n").unwrap();
let mut permissions = fs::metadata(&path).unwrap().permissions();
permissions.set_mode(mode);
fs::set_permissions(path, permissions).unwrap();
}
#[cfg(unix)]
fn write_executable(dir: &Path, name: &OsStr) {
write_file(dir, name, 0o755);
}
#[cfg(unix)]
fn write_non_executable(dir: &Path, name: &OsStr) {
write_file(dir, name, 0o644);
}
#[test]
#[cfg(unix)]
fn discover_subcommands_keeps_valid_utf8_executable_suffixes_sorted() {
let first = tempfile::tempdir().unwrap();
let second = tempfile::tempdir().unwrap();
write_executable(first.path(), OsStr::new("veryl-import"));
write_executable(second.path(), OsStr::new("veryl-import"));
write_executable(second.path(), OsStr::new("veryl-flist"));
let discovered = discover_from_path_entries([first.path(), second.path()]);
assert_eq!(names(&discovered), ["flist", "import"]);
}
#[test]
#[cfg(unix)]
fn discover_subcommands_ignores_official_non_subcommand_suffixes() {
let dir = tempfile::tempdir().unwrap();
write_executable(dir.path(), OsStr::new("veryl-ls"));
write_executable(dir.path(), OsStr::new("veryl-import"));
let discovered = discover_from_path_entries([dir.path()]);
assert_eq!(names(&discovered), ["import"]);
}
#[test]
#[cfg(unix)]
fn discover_subcommands_ignores_non_executable_non_directory_and_malformed_entries() {
let dir = tempfile::tempdir().unwrap();
let plain_file = tempfile::NamedTempFile::new().unwrap();
write_executable(dir.path(), OsStr::new("veryl-flist"));
write_executable(dir.path(), OsStr::new("veryl-"));
write_executable(dir.path(), OsStr::new("veryl-bad\\name"));
#[cfg(not(target_os = "macos"))]
write_executable(dir.path(), OsStr::from_bytes(b"veryl-im\xffort"));
write_non_executable(dir.path(), OsStr::new("veryl-import"));
let discovered = discover_from_path_entries([dir.path(), plain_file.path()]);
assert_eq!(names(&discovered), ["flist"]);
}
#[test]
#[cfg(unix)]
fn discover_subcommands_ignores_unreadable_path_entries() {
let unreadable = tempfile::tempdir().unwrap();
let readable = tempfile::tempdir().unwrap();
let mut permissions = fs::metadata(unreadable.path()).unwrap().permissions();
permissions.set_mode(0o000);
fs::set_permissions(unreadable.path(), permissions).unwrap();
write_executable(readable.path(), OsStr::new("veryl-import"));
let discovered = discover_from_path_entries([unreadable.path(), readable.path()]);
let mut permissions = fs::metadata(unreadable.path()).unwrap().permissions();
permissions.set_mode(0o755);
fs::set_permissions(unreadable.path(), permissions).unwrap();
assert_eq!(names(&discovered), ["import"]);
}
#[test]
fn parse_info_description_accepts_short_single_line_utf8() {
let stdout = "Import Veryl modules\n";
let description = parse_info_description(stdout);
assert_eq!(description.as_deref(), Some("Import Veryl modules"));
}
#[test]
fn parse_info_description_rejects_invalid_output() {
let long = "x".repeat(MAX_INFO_DESCRIPTION_CHARS + 1);
for stdout in ["", "\n", "first\nsecond\n", "bad\tcontrol\n", &long] {
let description = parse_info_description(stdout);
assert!(description.is_none(), "accepted invalid stdout: {stdout:?}");
}
}
#[test]
#[cfg(unix)]
fn probe_info_description_rejects_hanging_commands() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("veryl-slow");
fs::write(
&path,
"#!/bin/sh\nif [ \"$1\" = \"--info\" ]; then sleep 2; fi\n",
)
.unwrap();
let mut permissions = fs::metadata(&path).unwrap().permissions();
permissions.set_mode(0o755);
fs::set_permissions(&path, permissions).unwrap();
let description = probe_info_description(&path);
assert!(description.is_none());
}
#[test]
#[cfg(unix)]
fn probe_info_description_rejects_oversized_output_without_waiting_for_process_exit() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("veryl-verbose");
let oversized_bytes = MAX_INFO_DESCRIPTION_BYTES + 1;
fs::write(
&path,
format!(
"#!/bin/sh\nif [ \"$1\" = \"--info\" ]; then printf '%*s' {oversized_bytes} '' | tr ' ' x; sleep 2; fi\n",
),
)
.unwrap();
let mut permissions = fs::metadata(&path).unwrap().permissions();
permissions.set_mode(0o755);
fs::set_permissions(&path, permissions).unwrap();
let started = Instant::now();
let description = probe_info_description(&path);
let elapsed = started.elapsed();
assert!(description.is_none());
assert!(
elapsed < INFO_TIMEOUT / 2,
"expected oversized output to fall back before waiting for process exit; elapsed: {elapsed:?}"
);
}
#[test]
#[cfg(unix)]
fn probe_info_description_times_out_when_descendant_inherits_stdout() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("veryl-zhold");
let pid_file = dir.path().join("zhold.pid");
fs::write(
&path,
format!(
"#!/bin/sh\nif [ \"$1\" = \"--info\" ]; then bash -c 'exec -a veryl-zhold-inherited-stdout-regression sleep 60' & printf '%s\\n' \"$!\" > {}; printf 'held stdout\\n'; exit 0; fi\n",
pid_file.display()
),
)
.unwrap();
let mut permissions = fs::metadata(&path).unwrap().permissions();
permissions.set_mode(0o755);
fs::set_permissions(&path, permissions).unwrap();
let description = probe_info_description(&path);
let pid = fs::read_to_string(&pid_file).unwrap();
let descendant_alive = process_is_alive(pid.trim());
if descendant_alive {
let _ = Command::new("kill").arg("-TERM").arg(pid.trim()).status();
}
assert!(description.is_none());
assert!(
!descendant_alive,
"expected inherited-stdout descendant pid {} to be dead before test cleanup",
pid.trim()
);
}
fn names(discovered: &BTreeMap<String, CommandInfo>) -> Vec<&str> {
discovered.keys().map(String::as_str).collect()
}
#[cfg(unix)]
fn process_is_alive(pid: &str) -> bool {
Command::new("kill")
.arg("-0")
.arg(pid)
.status()
.map(|status| status.success())
.unwrap_or(false)
}