#[cfg(unix)]
mod unix {
use std::path::{Path, PathBuf};
use std::process::Command;
fn link_chain(dir: &Path) -> PathBuf {
let stuffr = dir.join("stuffr");
std::os::unix::fs::symlink(env!("CARGO_BIN_EXE_stuffr"), &stuffr).unwrap();
let bzip2 = dir.join("bzip2");
std::os::unix::fs::symlink(&stuffr, &bzip2).unwrap();
let bzcat = dir.join("bzcat");
std::os::unix::fs::symlink(&bzip2, &bzcat).unwrap();
bzcat
}
#[test]
fn symlink_named_bzcat_dispatches_to_bzcat() {
let dir = std::env::temp_dir().join(format!("stuffr-compat-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let bzcat = link_chain(&dir);
let out = Command::new(&bzcat).arg("-h").output().unwrap();
let _ = std::fs::remove_dir_all(&dir);
assert_eq!(out.status.code(), Some(0));
let err = String::from_utf8_lossy(&out.stderr);
assert!(
err.contains("usage: bzcat [flags and input files in any order]"),
"stderr: {err}"
);
}
const STUFFR: &str = env!("CARGO_BIN_EXE_stuffr");
const ALL: [&str; 4] = ["bzip2", "bunzip2", "bzcat", "bzip2recover"];
struct Scratch(PathBuf);
impl Scratch {
fn new(tag: &str) -> Self {
let d = std::env::temp_dir().join(format!("stuffr-il-{tag}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&d);
std::fs::create_dir_all(&d).unwrap();
Scratch(d)
}
}
impl Drop for Scratch {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.0);
}
}
fn canon_stuffr() -> PathBuf {
Path::new(STUFFR).canonicalize().unwrap()
}
fn il(dir: &Path, args: &[&str]) -> std::process::Output {
Command::new(STUFFR)
.arg("install-links")
.arg("--dir")
.arg(dir)
.args(args)
.output()
.unwrap()
}
fn stdout(o: &std::process::Output) -> String {
String::from_utf8_lossy(&o.stdout).into_owned()
}
fn stderr(o: &std::process::Output) -> String {
String::from_utf8_lossy(&o.stderr).into_owned()
}
#[test]
fn install_links_creates_every_name_and_is_idempotent() {
let s = Scratch::new("all");
let o = il(&s.0, &[]);
assert_eq!(o.status.code(), Some(0), "{}", stderr(&o));
let dir = s.0.canonicalize().unwrap();
for n in ALL {
let p = dir.join(n);
assert_eq!(std::fs::read_link(&p).unwrap(), canon_stuffr());
assert!(stdout(&o).contains(&format!("created {} -> ", p.display())));
}
let o = il(&s.0, &[]);
assert_eq!(o.status.code(), Some(0));
assert_eq!(
stdout(&o).matches("already present").count(),
4,
"{}",
stdout(&o)
);
assert!(!stdout(&o).contains("created"));
}
#[test]
fn install_links_refuses_a_regular_file_unless_forced() {
let s = Scratch::new("force");
let f = s.0.join("bzip2");
std::fs::write(&f, b"mine").unwrap();
let o = il(&s.0, &[]);
assert_eq!(o.status.code(), Some(2));
assert!(stderr(&o).contains("bzip2"), "{}", stderr(&o));
assert_eq!(std::fs::read(&f).unwrap(), b"mine");
assert!(!s.0.join("bzcat").exists(), "a refusal must change nothing");
let o = il(&s.0, &["--force"]);
assert_eq!(o.status.code(), Some(0), "{}", stderr(&o));
assert!(stdout(&o).contains("replaced"));
assert_eq!(std::fs::read_link(&f).unwrap(), canon_stuffr());
}
#[test]
fn install_links_names_selects_and_rejects() {
let s = Scratch::new("names");
let o = il(&s.0, &["--names", "bzcat"]);
assert_eq!(o.status.code(), Some(0), "{}", stderr(&o));
assert!(s.0.join("bzcat").symlink_metadata().is_ok());
assert!(s.0.join("bzip2").symlink_metadata().is_err());
let o = il(&s.0, &["--names", "gzip"]);
assert_eq!(o.status.code(), Some(2));
assert!(stderr(&o).contains("gzip"));
}
#[test]
fn install_links_remove_leaves_foreign_entries() {
let s = Scratch::new("remove");
assert_eq!(il(&s.0, &[]).status.code(), Some(0));
std::fs::remove_file(s.0.join("bzcat")).unwrap();
std::os::unix::fs::symlink("/bin/ls", s.0.join("bzcat")).unwrap();
std::fs::remove_file(s.0.join("bunzip2")).unwrap();
std::fs::write(s.0.join("bunzip2"), b"x").unwrap();
let o = il(&s.0, &["--remove"]);
assert_eq!(o.status.code(), Some(0), "{}", stderr(&o));
assert!(s.0.join("bzip2").symlink_metadata().is_err());
assert!(s.0.join("bzip2recover").symlink_metadata().is_err());
assert_eq!(
std::fs::read_link(s.0.join("bzcat")).unwrap(),
Path::new("/bin/ls")
);
assert_eq!(std::fs::read(s.0.join("bunzip2")).unwrap(), b"x");
let dir = s.0.canonicalize().unwrap();
for (n, why) in [("bzcat", "not a stuffr link"), ("bunzip2", "not a link")] {
let line = format!("left {}: {why}", dir.join(n).display());
assert!(stdout(&o).lines().any(|l| l == line), "{}", stdout(&o));
}
}
#[test]
fn install_links_remove_tells_this_stuffr_from_elsewhere() {
let s = Scratch::new("stale");
let canon = s.0.canonicalize().unwrap();
std::os::unix::fs::symlink(canon.join("gone/stuffr"), s.0.join("bzip2")).unwrap();
std::os::unix::fs::symlink(canon_stuffr(), s.0.join("bzcat")).unwrap();
std::os::unix::fs::symlink(canon.join("gone/other-tool"), s.0.join("bunzip2")).unwrap();
let o = il(&s.0, &["--remove"]);
assert_eq!(o.status.code(), Some(0), "{}", stderr(&o));
assert!(s.0.join("bzip2").symlink_metadata().is_err());
assert!(s.0.join("bzcat").symlink_metadata().is_err());
assert_eq!(
std::fs::read_link(s.0.join("bunzip2")).unwrap(),
canon.join("gone/other-tool")
);
assert!(
stdout(&o).contains(&format!(
"left {}: not a stuffr link",
canon.join("bunzip2").display()
)),
"{}",
stdout(&o)
);
}
#[test]
fn install_links_treats_a_link_through_another_link_as_ours() {
let s = Scratch::new("via-link");
let canon = s.0.canonicalize().unwrap();
let l = canon.join("L");
std::os::unix::fs::symlink(canon_stuffr(), &l).unwrap();
std::os::unix::fs::symlink(&l, canon.join("bzip2")).unwrap();
let o = il(&s.0, &["--names", "bzip2"]);
assert!(stdout(&o).contains("already present"), "{}", stdout(&o));
assert_eq!(std::fs::read_link(canon.join("bzip2")).unwrap(), l);
let o = il(&s.0, &["--remove"]);
assert_eq!(o.status.code(), Some(0), "{}", stderr(&o));
assert!(canon.join("bzip2").symlink_metadata().is_err());
assert_eq!(std::fs::read_link(&l).unwrap(), canon_stuffr());
}
#[test]
fn install_links_recovers_links_left_dangling_by_an_upgrade() {
let s = Scratch::new("upgrade");
let canon = s.0.canonicalize().unwrap();
let bin = canon.join("Cellar/stuffr/0.10.4/bin");
let links = canon.join("links");
std::fs::create_dir_all(&bin).unwrap();
std::fs::create_dir_all(&links).unwrap();
let old = bin.join("stuffr");
let make_stale = || {
std::fs::write(&old, b"old stuffr").unwrap();
for n in ALL {
let _ = std::fs::remove_file(links.join(n));
std::os::unix::fs::symlink(&old, links.join(n)).unwrap();
}
std::fs::remove_file(&old).unwrap();
};
make_stale();
let o = il(&links, &["--remove"]);
assert_eq!(o.status.code(), Some(0), "{}", stderr(&o));
assert_eq!(stdout(&o).matches("removed").count(), 4, "{}", stdout(&o));
assert_eq!(std::fs::read_dir(&links).unwrap().count(), 0);
make_stale();
let o = il(&links, &["--dry-run"]);
assert_eq!(o.status.code(), Some(0), "{}", stderr(&o));
assert_eq!(
stdout(&o).matches("would replace").count(),
4,
"{}",
stdout(&o)
);
let o = il(&links, &[]);
assert_eq!(o.status.code(), Some(0), "{}", stderr(&o));
assert_eq!(stdout(&o).matches("replaced").count(), 4, "{}", stdout(&o));
for n in ALL {
assert_eq!(std::fs::read_link(links.join(n)).unwrap(), canon_stuffr());
}
}
#[test]
fn install_links_refuses_a_dangling_link_to_another_tool() {
let s = Scratch::new("other-dangling");
let canon = s.0.canonicalize().unwrap();
let t = canon.join("gone/other-tool");
std::os::unix::fs::symlink(&t, canon.join("bzip2")).unwrap();
let o = il(&s.0, &[]);
assert_eq!(o.status.code(), Some(2), "{}", stdout(&o));
assert!(stderr(&o).contains("bzip2"), "{}", stderr(&o));
assert_eq!(std::fs::read_link(canon.join("bzip2")).unwrap(), t);
assert!(canon.join("bzcat").symlink_metadata().is_err());
}
#[test]
fn install_links_leaves_a_link_to_another_live_binary() {
let s = Scratch::new("other-live");
let canon = s.0.canonicalize().unwrap();
let other = canon.join("stuffr-not");
std::fs::write(&other, b"another program").unwrap();
std::fs::create_dir_all(canon.join("elsewhere")).unwrap();
let named = canon.join("elsewhere/stuffr");
std::fs::write(&named, b"another stuffr").unwrap();
std::os::unix::fs::symlink(&other, canon.join("bzip2")).unwrap();
std::os::unix::fs::symlink(&named, canon.join("bzcat")).unwrap();
let o = il(&s.0, &["--names", "bzip2"]);
assert_eq!(o.status.code(), Some(2));
let o = il(&s.0, &["--names", "bzcat"]);
assert_eq!(o.status.code(), Some(2));
let o = il(&s.0, &["--remove"]);
assert_eq!(o.status.code(), Some(0), "{}", stderr(&o));
assert_eq!(
stdout(&o).matches("not a stuffr link").count(),
2,
"{}",
stdout(&o)
);
assert_eq!(std::fs::read_link(canon.join("bzip2")).unwrap(), other);
assert_eq!(std::fs::read_link(canon.join("bzcat")).unwrap(), named);
}
#[test]
fn install_links_takes_the_directory_only_as_dir() {
let s = Scratch::new("positional");
let o = Command::new(STUFFR)
.arg("install-links")
.arg(&s.0)
.output()
.unwrap();
assert_eq!(o.status.code(), Some(2), "{}", stderr(&o));
assert_eq!(std::fs::read_dir(&s.0).unwrap().count(), 0);
let o = Command::new(STUFFR).arg("install-links").output().unwrap();
assert_eq!(o.status.code(), Some(2));
assert!(stderr(&o).contains("--dir"), "{}", stderr(&o));
}
#[test]
fn install_links_duplicate_names_equal_one_name() {
let s = Scratch::new("dup");
let o = il(&s.0, &["--names", "bzcat,bzcat"]);
assert_eq!(o.status.code(), Some(0), "{}", stderr(&o));
assert_eq!(stdout(&o).lines().count(), 1, "{}", stdout(&o));
assert_eq!(std::fs::read_dir(&s.0).unwrap().count(), 1);
}
#[test]
fn install_links_replace_leaves_no_temp_files() {
let s = Scratch::new("atomic");
std::fs::write(s.0.join("bzip2"), b"x").unwrap();
let o = il(&s.0, &["--force", "--names", "bzip2"]);
assert_eq!(o.status.code(), Some(0), "{}", stderr(&o));
let names: Vec<_> = std::fs::read_dir(&s.0).unwrap().collect();
assert_eq!(names.len(), 1);
}
#[test]
fn install_links_dry_run_changes_nothing() {
let s = Scratch::new("dry");
let o = il(&s.0, &["--dry-run"]);
assert_eq!(o.status.code(), Some(0));
assert_eq!(
stdout(&o).matches("would create").count(),
4,
"{}",
stdout(&o)
);
assert_eq!(std::fs::read_dir(&s.0).unwrap().count(), 0);
assert_eq!(il(&s.0, &[]).status.code(), Some(0));
std::fs::remove_file(s.0.join("bzip2")).unwrap();
std::fs::write(s.0.join("bzip2"), b"x").unwrap();
let o = il(&s.0, &["--force", "--dry-run"]);
assert!(stdout(&o).contains("would replace"), "{}", stdout(&o));
assert_eq!(std::fs::read(s.0.join("bzip2")).unwrap(), b"x");
std::fs::remove_file(s.0.join("bzip2")).unwrap();
std::os::unix::fs::symlink(canon_stuffr(), s.0.join("bzip2")).unwrap();
let o = il(&s.0, &["--remove", "--dry-run"]);
assert_eq!(stdout(&o).matches("would remove").count(), 4);
assert_eq!(std::fs::read_dir(&s.0).unwrap().count(), 4);
}
#[test]
fn install_links_missing_dir_is_a_usage_error() {
let s = Scratch::new("missing");
let o = il(&s.0.join("nope"), &[]);
assert_eq!(o.status.code(), Some(2));
assert!(stderr(&o).contains("nope"), "{}", stderr(&o));
}
#[test]
fn install_links_unwritable_dir_is_exit_1_with_the_cause() {
use std::os::unix::fs::PermissionsExt;
let s = Scratch::new("ro");
std::fs::set_permissions(&s.0, std::fs::Permissions::from_mode(0o555)).unwrap();
let o = il(&s.0, &[]);
std::fs::set_permissions(&s.0, std::fs::Permissions::from_mode(0o755)).unwrap();
if std::fs::write(s.0.join("probe"), b"").is_ok() {
return;
}
assert_eq!(o.status.code(), Some(1), "{}", stderr(&o));
assert!(stderr(&o).contains("ermission denied"), "{}", stderr(&o));
let link = s.0.canonicalize().unwrap().join("bzip2");
assert!(
stderr(&o).contains(&link.display().to_string()),
"{}",
stderr(&o)
);
}
#[test]
fn install_links_force_through_a_symlinked_dir_stays_inside_it() {
let s = Scratch::new("viadir");
let real = s.0.join("real");
let other = s.0.join("other");
std::fs::create_dir_all(&real).unwrap();
std::fs::create_dir_all(&other).unwrap();
std::fs::write(other.join("bzip2"), b"keep").unwrap();
std::fs::write(real.join("bzip2"), b"old").unwrap();
let via = s.0.join("via");
std::os::unix::fs::symlink(&real, &via).unwrap();
let o = il(&via, &["--force"]);
assert_eq!(o.status.code(), Some(0), "{}", stderr(&o));
assert_eq!(
std::fs::read_link(real.join("bzip2")).unwrap(),
canon_stuffr()
);
assert_eq!(std::fs::read(other.join("bzip2")).unwrap(), b"keep");
assert!(stdout(&o).contains(&real.canonicalize().unwrap().display().to_string()));
}
#[test]
fn install_links_is_documented_on_the_examples_page() {
let out = Command::new(STUFFR).arg("--examples").output().unwrap();
let text = stdout(&out);
for needle in [
"install-links --dir",
"--names",
"--force",
"--remove",
"--dry-run",
] {
assert!(text.contains(needle), "examples page lacks {needle}");
}
}
}