use super::*;
use std::fs;
use std::os::unix::fs::PermissionsExt;
use std::path::{Path, PathBuf};
use std::process::Command;
fn sandbox(name: &str) -> PathBuf {
let dir = std::env::temp_dir().join(format!("forjar-shell-install-{name}"));
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).expect("sandbox");
dir
}
fn sh(script: &str) -> std::process::Output {
Command::new("sh")
.arg("-c")
.arg(script)
.output()
.expect("run shell")
}
fn write_exec(path: &Path, body: &str) {
fs::write(path, body).expect("write");
fs::set_permissions(path, fs::Permissions::from_mode(0o755)).expect("chmod");
}
#[test]
fn the_function_name_matches_what_is_emitted() {
assert!(atomic_install_fn().contains(&format!("{ATOMIC_INSTALL_FN}() {{")));
}
#[test]
fn install_bin_lands_the_bytes_and_makes_them_executable() {
let dir = sandbox("basic");
let src = dir.join("src");
write_exec(&src, "#!/bin/sh\necho hello\n");
fs::create_dir_all(dir.join("sub")).expect("dest dir");
let dst = dir.join("sub/dst");
let out = sh(&format!(
"set -eu\n{}\n_fj_install_bin '{}' '{}'\n'{}'",
atomic_install_fn(),
src.display(),
dst.display(),
dst.display()
));
assert!(
out.status.success(),
"install failed: {}",
String::from_utf8_lossy(&out.stderr)
);
assert_eq!(String::from_utf8_lossy(&out.stdout).trim(), "hello");
let mode = fs::metadata(&dst).expect("dst").permissions().mode() & 0o777;
assert_eq!(mode, 0o755, "destination is not executable");
}
#[test]
fn a_missing_destination_directory_is_a_failure_not_a_silent_pass() {
let dir = sandbox("nodir");
let src = dir.join("src");
write_exec(&src, "#!/bin/sh\n:\n");
let dst = dir.join("absent/dst");
let out = sh(&format!(
"set -eu\n{}\nif _fj_install_bin '{}' '{}'; then echo UNEXPECTED_OK; else echo REPORTED_FAILURE; fi",
atomic_install_fn(),
src.display(),
dst.display()
));
assert!(
String::from_utf8_lossy(&out.stdout).contains("REPORTED_FAILURE"),
"a missing destination directory must fail: {}",
String::from_utf8_lossy(&out.stdout)
);
assert!(!dst.exists());
}
#[test]
fn staging_happens_in_the_destination_directory() {
assert!(
atomic_install_fn().contains(r#"_fji_tmp="$_fji_dir/.forjar-install.$$""#),
"staging file must be a sibling of the destination"
);
assert!(
!atomic_install_fn().contains("/tmp"),
"staging must not be placed on a possibly-different filesystem"
);
}
#[test]
fn nothing_opens_the_destination_in_place() {
let f = atomic_install_fn();
assert!(
!f.contains(r#"cp -f "$_fji_src" "$_fji_dst""#),
"the destination must never be a copy target"
);
assert!(
f.contains(r#"mv -f "$_fji_tmp" "$_fji_dst""#),
"the destination must be reached by rename(2)"
);
}
#[test]
fn a_failed_copy_leaves_no_staging_litter_and_reports_failure() {
let dir = sandbox("failure");
let dst = dir.join("dst");
let out = sh(&format!(
"set -eu\n{}\nif _fj_install_bin '{}/does-not-exist' '{}'; then echo UNEXPECTED_OK; else echo REPORTED_FAILURE; fi",
atomic_install_fn(),
dir.display(),
dst.display()
));
assert!(
String::from_utf8_lossy(&out.stdout).contains("REPORTED_FAILURE"),
"a missing source must fail the function: {}",
String::from_utf8_lossy(&out.stdout)
);
assert!(!dst.exists(), "a failed install must not create the target");
let litter: Vec<_> = fs::read_dir(&dir)
.expect("readdir")
.filter_map(|e| e.ok())
.filter(|e| {
e.file_name()
.to_string_lossy()
.starts_with(".forjar-install")
})
.collect();
assert!(litter.is_empty(), "staging file left behind: {litter:?}");
}
#[test]
fn install_bins_lands_every_binary_in_the_staging_dir() {
let dir = sandbox("many");
let staging = dir.join("staging/bin");
fs::create_dir_all(&staging).expect("staging");
write_exec(&staging.join("alpha"), "#!/bin/sh\necho alpha\n");
write_exec(&staging.join("beta"), "#!/bin/sh\necho beta\n");
let dest = dir.join("dest");
fs::create_dir_all(&dest).expect("dest");
let out = sh(&format!(
"set -eu\n{}\n_fj_install_bins '{}' '{}'\n'{}/alpha'\n'{}/beta'",
atomic_install_dir_fn(),
staging.display(),
dest.display(),
dest.display(),
dest.display()
));
assert!(
out.status.success(),
"install_bins failed: {}",
String::from_utf8_lossy(&out.stderr)
);
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.contains("alpha") && stdout.contains("beta"),
"{stdout}"
);
}
#[test]
fn install_bins_over_an_empty_staging_dir_installs_nothing() {
let dir = sandbox("empty");
let staging = dir.join("staging/bin");
fs::create_dir_all(&staging).expect("staging");
let dest = dir.join("dest");
fs::create_dir_all(&dest).expect("dest");
let out = sh(&format!(
"set -eu\n{}\n_fj_install_bins '{}' '{}'",
atomic_install_dir_fn(),
staging.display(),
dest.display()
));
assert!(out.status.success(), "empty staging must not be an error");
assert_eq!(
fs::read_dir(&dest).expect("readdir").count(),
0,
"nothing should have been installed"
);
}
#[test]
fn an_empty_runner_prefix_contributes_no_argument() {
let dir = sandbox("runner");
let src = dir.join("src");
write_exec(&src, "#!/bin/sh\necho ok\n");
let dst = dir.join("dst");
let out = sh(&format!(
"set -eu\n{}\n_fj_install_bin '{}' '{}' ''\n'{}'",
atomic_install_fn(),
src.display(),
dst.display(),
dst.display()
));
assert!(
out.status.success(),
"explicit empty runner broke the call: {}",
String::from_utf8_lossy(&out.stderr)
);
assert_eq!(String::from_utf8_lossy(&out.stdout).trim(), "ok");
}