use std::path::PathBuf;
use std::process::Command;
use std::sync::atomic::{AtomicU64, Ordering};
fn bin() -> PathBuf {
PathBuf::from(env!("CARGO_BIN_EXE_rustyfi"))
}
fn tmpdir(tag: &str) -> PathBuf {
static COUNTER: AtomicU64 = AtomicU64::new(0);
let n = COUNTER.fetch_add(1, Ordering::Relaxed);
let p = std::env::temp_dir().join(format!(
"rustyfi-dispatch-{tag}-{}-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos(),
n
));
std::fs::create_dir_all(&p).unwrap();
p
}
#[cfg(unix)]
fn run_as(arg0: &str, args: &[&str]) -> std::process::Output {
use std::os::unix::process::CommandExt as _;
Command::new(bin())
.arg0(arg0)
.args(args)
.output()
.expect("spawn binary")
}
#[cfg(unix)]
#[test]
fn argv0_rustyfi_is_compiler_and_package_manager() {
let out = run_as("rustyfi", &["--help"]);
assert!(out.status.success(), "rustyfi --help should succeed");
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.contains("Compile a SATySFi"),
"rustyfi --help should describe the compiler:\n{stdout}"
);
assert!(
stdout.contains("install") && stdout.contains("search"),
"rustyfi --help should offer the package commands:\n{stdout}"
);
}
#[cfg(unix)]
#[test]
fn argv0_satyrographos_is_package_manager() {
let root = tmpdir("sg-personality");
let out = run_as("satyrographos", &["list", "--dest", root.to_str().unwrap()]);
assert!(
out.status.success(),
"satyrographos list should succeed on empty root"
);
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.contains("(no packages installed)"),
"expected empty-list message:\n{stdout}"
);
}
#[cfg(unix)]
#[test]
fn package_commands_are_top_level() {
let root = tmpdir("sg-subcommand");
let out = run_as("rustyfi", &["list", "--dest", root.to_str().unwrap()]);
assert!(out.status.success());
assert!(String::from_utf8_lossy(&out.stdout).contains("(no packages installed)"));
let out = run_as(
"rustyfi",
&["satyrographos", "list", "--dest", root.to_str().unwrap()],
);
assert!(
!out.status.success(),
"`rustyfi satyrographos …` should no longer parse"
);
}
#[cfg(unix)]
#[test]
fn bare_rustyfi_without_input_is_usage_error() {
let out = run_as("rustyfi", &[]);
assert!(!out.status.success(), "bare invocation must fail");
assert_eq!(out.status.code(), Some(2), "clap usage error is exit 2");
}
#[cfg(unix)]
#[test]
fn multicall_install_creates_working_aliases() {
use std::os::unix::process::CommandExt as _;
let dir = tmpdir("aliases");
let out = Command::new(bin())
.args(["multicall", "install", "--dir", dir.to_str().unwrap()])
.output()
.expect("spawn");
assert!(out.status.success(), "multicall install should succeed");
let rustyfi_alias = dir.join("rustyfi");
let satyro_alias = dir.join("satyrographos");
assert!(rustyfi_alias.exists(), "rustyfi alias created");
assert!(satyro_alias.exists(), "satyrographos alias created");
let root = tmpdir("aliases-root");
let out = Command::new(&satyro_alias)
.arg0("satyrographos")
.args(["list", "--dest", root.to_str().unwrap()])
.output()
.expect("spawn alias");
assert!(out.status.success());
assert!(String::from_utf8_lossy(&out.stdout).contains("(no packages installed)"));
let out = Command::new(bin())
.args(["multicall", "install", "--dir", dir.to_str().unwrap()])
.output()
.expect("spawn");
assert!(
out.status.success(),
"re-install of same aliases is idempotent"
);
}
#[cfg(unix)]
#[test]
fn manifest_mode_install_reconciles_and_writes_lock() {
use std::os::unix::process::CommandExt as _;
let work = tmpdir("manifest-mode");
let pkg = work.join("vendor/mylib");
std::fs::create_dir_all(pkg.join("packages")).unwrap();
std::fs::write(
pkg.join("rustyfi-package.toml"),
"[package]\n\
name = \"mylib\"\n\
version = \"1.0.0\"\n\
rustyfi-version-compat = \">=0.0.6, <0.1\"\n\
\n\
[[files]]\n\
kind = \"package-dir\"\n\
src = \"packages\"\n",
)
.unwrap();
std::fs::write(pkg.join("packages/mylib.satyh"), "let mylib = 1\n").unwrap();
let proj = work.join("proj");
std::fs::create_dir_all(&proj).unwrap();
std::fs::write(
proj.join("Satyristes"),
"(version 0.0.2)\n\
(library (name \"proj\") (version \"0.1.0\")\n\
(sources ((packageDir \"src\")))\n\
(dependencies ((mylib ((path \"../vendor/mylib\"))))))\n",
)
.unwrap();
let root = work.join("root");
let run = |extra_help: &str| {
Command::new(bin())
.arg0("satyrographos")
.current_dir(&proj)
.args(["install", "--dest", root.to_str().unwrap()])
.output()
.unwrap_or_else(|e| panic!("spawn ({extra_help}): {e}"))
};
let out = run("first");
assert!(out.status.success(), "manifest install should succeed");
assert!(root.join("dist/packages/mylib/mylib.satyh").is_file());
assert!(proj.join("Satyristes.lock").is_file(), "lockfile written");
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.contains("installed mylib"), "{stdout}");
let out = run("second");
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.contains("unchanged mylib"), "{stdout}");
let _ = std::fs::remove_dir_all(&work);
}
#[cfg(unix)]
#[test]
fn manifest_mode_without_a_manifest_exits_3() {
use std::os::unix::process::CommandExt as _;
let empty = tmpdir("no-satyrfile");
let root = tmpdir("no-satyrfile-root");
let out = Command::new(bin())
.arg0("satyrographos")
.current_dir(&empty)
.args(["install", "--dest", root.to_str().unwrap()])
.output()
.expect("spawn");
assert!(!out.status.success());
assert_eq!(out.status.code(), Some(3), "no Satyristes → exit 3");
}
#[cfg(unix)]
#[test]
fn leading_global_flag_reaches_a_subcommand() {
let work = tmpdir("leading-global");
let config = work.join("config.toml");
std::fs::write(&config, "").unwrap();
let root = tmpdir("leading-global-root");
let out = run_as(
"rustyfi",
&[
"--config",
config.to_str().unwrap(),
"list",
"--dest",
root.to_str().unwrap(),
],
);
assert!(
out.status.success(),
"leading --config before `list` should parse and dispatch:\n{}",
String::from_utf8_lossy(&out.stderr)
);
assert!(String::from_utf8_lossy(&out.stdout).contains("(no packages installed)"));
let out_after = run_as(
"rustyfi",
&[
"list",
"--config",
config.to_str().unwrap(),
"--dest",
root.to_str().unwrap(),
],
);
assert_eq!(out.stdout, out_after.stdout);
}
#[cfg(unix)]
#[test]
fn leading_global_flag_does_not_swallow_a_bare_subcommand_name() {
use std::os::unix::process::CommandExt as _;
let empty = tmpdir("leading-global-bare");
let config = empty.join("config.toml");
std::fs::write(&config, "").unwrap();
let with_leading_flag = Command::new(bin())
.arg0("rustyfi")
.current_dir(&empty)
.args(["--config", config.to_str().unwrap(), "install"])
.output()
.expect("spawn");
let bare = Command::new(bin())
.arg0("rustyfi")
.current_dir(&empty)
.args(["install"])
.output()
.expect("spawn");
assert_eq!(
with_leading_flag.status.code(),
bare.status.code(),
"leading --config must not change what bare `install` means:\n{}",
String::from_utf8_lossy(&with_leading_flag.stderr)
);
assert_eq!(with_leading_flag.status.code(), Some(3));
assert_eq!(with_leading_flag.stderr, bare.stderr);
}
#[cfg(unix)]
#[test]
fn leading_global_flag_does_not_revive_the_old_nesting() {
let root = tmpdir("leading-global-old-nesting");
let config_dir = tmpdir("leading-global-old-nesting-cfg");
let config = config_dir.join("config.toml");
std::fs::write(&config, "").unwrap();
let out = run_as(
"rustyfi",
&[
"--config",
config.to_str().unwrap(),
"satyrographos",
"list",
"--dest",
root.to_str().unwrap(),
],
);
assert!(
!out.status.success(),
"`rustyfi --config F satyrographos list …` should still not parse"
);
}
#[cfg(unix)]
#[test]
fn leading_global_flag_help_is_subcommand_specific() {
let config_dir = tmpdir("leading-global-help-cfg");
let config = config_dir.join("config.toml");
std::fs::write(&config, "").unwrap();
let with_leading_flag = run_as(
"rustyfi",
&["--config", config.to_str().unwrap(), "install", "--help"],
);
let direct = run_as("rustyfi", &["install", "--help"]);
assert!(with_leading_flag.status.success());
assert_eq!(with_leading_flag.stdout, direct.stdout);
}
#[cfg(unix)]
#[test]
fn leading_flag_value_matching_a_subcommand_name_is_not_the_split_point() {
use std::os::unix::process::CommandExt as _;
let work = tmpdir("value-aware-hoist");
std::fs::create_dir_all(work.join("pkgsrc/packages")).unwrap();
std::fs::write(
work.join("pkgsrc/rustyfi-package.toml"),
"[package]\n\
name = \"mylib\"\n\
version = \"1.0.0\"\n\
rustyfi-version-compat = \">=0.0.6, <0.1\"\n\
\n\
[[files]]\n\
kind = \"package-dir\"\n\
src = \"packages\"\n",
)
.unwrap();
std::fs::write(work.join("pkgsrc/packages/mylib.satyh"), "let mylib = 1\n").unwrap();
let out = Command::new(bin())
.arg0("rustyfi")
.current_dir(&work)
.args(["--lib-root", "search", "install", "pkgsrc"])
.output()
.expect("spawn");
assert!(
out.status.success(),
"install should dispatch, with `search` consumed as --lib-root's value:\n{}",
String::from_utf8_lossy(&out.stderr)
);
assert!(
work.join("search/dist/packages/mylib/mylib.satyh").is_file(),
"package should land under the `search` lib-root, proving `search` was \
--lib-root's VALUE and `install` was the dispatched subcommand"
);
let _ = std::fs::remove_dir_all(&work);
}
#[cfg(unix)]
#[test]
fn leading_global_flag_equals_spelling_reaches_a_subcommand() {
let work = tmpdir("leading-global-equals");
let config = work.join("config.toml");
std::fs::write(&config, "").unwrap();
let root = tmpdir("leading-global-equals-root");
let out = run_as(
"rustyfi",
&[
&format!("--config={}", config.to_str().unwrap()),
"list",
"--dest",
root.to_str().unwrap(),
],
);
assert!(
out.status.success(),
"leading --config=F before `list` should parse and dispatch:\n{}",
String::from_utf8_lossy(&out.stderr)
);
assert!(String::from_utf8_lossy(&out.stdout).contains("(no packages installed)"));
}
#[test]
fn install_then_loader_resolves_the_package() {
let work = tmpdir("roundtrip");
let src = work.join("src");
std::fs::create_dir_all(src.join("packages")).unwrap();
std::fs::write(
src.join("rustyfi-package.toml"),
"[package]\n\
name = \"greetlib\"\n\
version = \"1.0.0\"\n\
rustyfi-version-compat = \">=0.0.6, <0.1\"\n\
\n\
[[files]]\n\
kind = \"package-dir\"\n\
src = \"packages\"\n",
)
.unwrap();
std::fs::write(
src.join("packages/greetlib.satyh"),
"% library: bindings only\nlet greeting-word = `Hello`\n",
)
.unwrap();
let root = work.join("root");
rustyfi_satyrographos::install(
&src,
&rustyfi_satyrographos::InstallOptions {
dest: Some(root.clone()),
..Default::default()
},
)
.expect("install ok");
assert!(root.join("dist/packages/greetlib/greetlib.satyh").is_file());
let doc = work.join("doc.saty");
std::fs::write(&doc, "@require: greetlib\ndocument (||) '<>\n").unwrap();
let program = rustyfi_loader::load(
&doc,
&rustyfi_loader::LoadOptions {
lib_root: Some(root.clone()),
..Default::default()
},
)
.expect("loader must resolve @require: greetlib against the install root");
let names: Vec<String> = program
.files
.iter()
.map(|f| f.path.file_name().unwrap().to_string_lossy().into_owned())
.collect();
assert!(
names.iter().any(|n| n == "greetlib.satyh"),
"loaded files should include the installed library: {names:?}"
);
assert!(names.iter().any(|n| n == "doc.saty"));
let _ = std::fs::remove_dir_all(&work);
}