#![cfg(unix)]
use super::*;
use crate::edge::Edge;
use crate::git;
use crate::layout::Xdg;
use std::fs;
use std::os::unix::fs::PermissionsExt;
use std::path::PathBuf;
use tempfile::TempDir;
fn edge(tmp: &TempDir, exe_dir: Option<PathBuf>) -> Edge {
Edge {
xdg: Xdg::with(tmp.path(), None, Some(&tmp.path().join("state").to_string_lossy())),
invocation_path: tmp.path().join("proj"),
default_actor: "tester".into(),
depth: 0,
held: Vec::new(),
exe_dir,
path_dirs: Vec::new(),
color: false,
log_level: None,
balls_clock: None,
}
}
fn found(e: &Edge) -> (PathBuf, PathBuf) {
let clone = e.xdg.clone_dir(&e.invocation_path);
crate::substrate::found(&clone.landing(), &clone.store(), &e.xdg, e.exe_dir.as_deref()).unwrap();
(clone.landing(), clone.store())
}
fn center(dir: &Path, tasks_branch: &str, plugins: &str) -> String {
let repo = dir.join("center");
fs::create_dir_all(&repo).unwrap();
g(&repo, &["init", "-q", "-b", LANDING_BRANCH]);
g(&repo, &["config", "user.name", "c"]);
g(&repo, &["config", "user.email", "c@c"]);
let config = repo.join("config");
fs::create_dir_all(&config).unwrap();
fs::write(config.join("balls.toml"), format!("tasks_branch = \"{tasks_branch}\"\n")).unwrap();
fs::write(config.join("plugins.toml"), plugins).unwrap();
g(&repo, &["add", "-A"]);
g(&repo, &["commit", "-q", "-m", "center config"]);
repo.to_string_lossy().into_owned()
}
fn g(cwd: &Path, args: &[&str]) {
git::run(cwd, args, None).unwrap();
}
fn sim_fetch(landing: &Path, center: &str) {
g(landing, &["fetch", center, LANDING_BRANCH]);
}
fn head(landing: &Path) -> String {
git::run(landing, &["rev-parse", "HEAD"], None).unwrap()
}
fn plugin(dir: &Path, name: &str, ops: &str) {
let path = dir.join(name);
let body = format!(
"#!/bin/sh\nif [ \"$1\" = protocol ]; then printf '{{\"protocol\":[1],\"ops\":{ops}}}'; fi\n"
);
fs::write(&path, body).unwrap();
fs::set_permissions(&path, fs::Permissions::from_mode(0o755)).unwrap();
}
const NO_HOOKS: &str = "[hooks]\n";
#[test]
fn install_local_mirrors_the_fetched_config_into_the_landing_and_commits() {
let tmp = TempDir::new().unwrap();
let e = edge(&tmp, None);
let (landing, _store) = found(&e);
let before = head(&landing);
sim_fetch(&landing, ¢er(tmp.path(), "balls/shared", NO_HOOKS));
install_local(&e, &landing).unwrap();
let cfg = fs::read_to_string(landing.join("config/balls.toml")).unwrap();
assert!(cfg.contains("balls/shared"), "adopted config: {cfg}");
assert_ne!(before, head(&landing), "a commit landed");
}
#[test]
fn re_adopting_identical_config_skips_the_commit() {
let tmp = TempDir::new().unwrap();
let e = edge(&tmp, None);
let (landing, _store) = found(&e);
let c = center(tmp.path(), "balls/shared", NO_HOOKS);
sim_fetch(&landing, &c);
install_local(&e, &landing).unwrap();
let after_first = head(&landing);
sim_fetch(&landing, &c);
install_local(&e, &landing).unwrap(); assert_eq!(after_first, head(&landing), "idempotent re-adopt skips the commit");
}
#[test]
fn install_local_binds_a_referenced_plugin_whose_binary_is_present() {
let tmp = TempDir::new().unwrap();
let bin = tmp.path().join("bin");
fs::create_dir_all(&bin).unwrap();
plugin(&bin, "adversary", "[\"install\"]");
let e = edge(&tmp, Some(bin.clone()));
let (landing, _store) = found(&e);
sim_fetch(&landing, ¢er(tmp.path(), "balls/shared", "[hooks]\n\"install.pre\" = [\"adversary\"]\n"));
install_local(&e, &landing).unwrap();
let link = landing.join("config/plugins/bin/adversary");
assert_eq!(fs::read_link(&link).unwrap(), bin.join("adversary"));
}
#[test]
fn install_local_refuses_a_referenced_plugin_the_binary_cannot_serve() {
let tmp = TempDir::new().unwrap();
let bin = tmp.path().join("bin");
fs::create_dir_all(&bin).unwrap();
plugin(&bin, "adversary", "[\"install\"]"); let e = edge(&tmp, Some(bin));
let (landing, _store) = found(&e);
sim_fetch(&landing, ¢er(tmp.path(), "balls/shared", "[hooks]\n\"sync.pre\" = [\"adversary\"]\n"));
let err = install_local(&e, &landing).unwrap_err();
assert!(err.to_string().contains("sync"), "{err}");
}
#[test]
fn adopt_canonicalizes_a_stale_centers_retired_name_as_it_lands() {
let tmp = TempDir::new().unwrap();
let bin = tmp.path().join("bin");
fs::create_dir_all(&bin).unwrap();
plugin(&bin, "bl-tracker", "[\"install\"]"); let e = edge(&tmp, Some(bin.clone()));
let (landing, _store) = found(&e);
let c = center(tmp.path(), "balls/shared", "[hooks]\n\"install.pre\" = [\"tracker\"]\n");
sim_fetch(&landing, &c);
install_local(&e, &landing).unwrap();
let toml = fs::read_to_string(landing.join("config/plugins.toml")).unwrap();
assert!(toml.contains("bl-tracker") && !toml.contains("\"tracker\""), "{toml}");
assert_eq!(fs::read_link(landing.join("config/plugins/bin/bl-tracker")).unwrap(), bin.join("bl-tracker"));
let after_first = head(&landing);
sim_fetch(&landing, &c);
install_local(&e, &landing).unwrap();
assert_eq!(after_first, head(&landing), "re-adopt of a stale center is the no-op seal");
}
#[test]
fn adopt_without_an_install_pre_plugin_is_an_error() {
let tmp = TempDir::new().unwrap();
let e = edge(&tmp, None);
let (landing, store) = found(&e);
let c = center(tmp.path(), "balls/shared", NO_HOOKS);
let err = adopt(&e, &landing, &store, "me", &c).unwrap_err();
assert!(err.to_string().contains("install.pre"), "{err}");
}
#[test]
fn adopt_with_nothing_fetched_is_a_no_op_not_a_fatal() {
let tmp = TempDir::new().unwrap();
let bin = tmp.path().join("bin");
fs::create_dir_all(&bin).unwrap();
plugin(&bin, "bl-tracker", "[\"install\"]");
let e = edge(&tmp, Some(bin));
let (landing, store) = found(&e);
let before = head(&landing);
let c = center(tmp.path(), "balls/shared", NO_HOOKS);
adopt(&e, &landing, &store, "me", &c).unwrap();
assert_eq!(before, head(&landing), "nothing sealed");
let cfg = fs::read_to_string(landing.join("config/balls.toml")).unwrap();
assert!(!cfg.contains("balls/shared"), "the seeded config stands: {cfg}");
}