use crate::conf;
use crate::edge::Edge;
use crate::git;
use crate::layout::{CloneDir, Xdg};
use crate::substrate;
use std::fs;
use std::os::unix::fs::PermissionsExt;
use std::path::Path;
use tempfile::TempDir;
fn edge(tmp: &TempDir) -> 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,
exe_dir: None,
path_dirs: Vec::new(),
color: false,
log_level: None,
balls_clock: None,
}
}
fn founded(e: &Edge) -> CloneDir {
let clone = e.xdg.clone_dir(&e.invocation_path);
substrate::found(&clone.landing(), &clone.store(), &e.xdg, None).unwrap();
clone
}
fn conf(e: &Edge, argv: &[&str]) -> std::io::Result<()> {
conf::run(e, &argv.iter().map(ToString::to_string).collect::<Vec<_>>())
}
fn commits(landing: &Path) -> usize {
git::run(landing, &["rev-list", "--count", "HEAD"], None).unwrap().trim().parse().unwrap()
}
fn subject(landing: &Path) -> String {
git::run(landing, &["log", "-1", "--format=%s"], None).unwrap().trim().to_string()
}
fn temps_beside(clone: &CloneDir) -> bool {
fs::read_dir(clone.root())
.unwrap()
.any(|entry| entry.unwrap().file_name().to_string_lossy().ends_with(".tmp"))
}
fn landing_text(clone: &CloneDir, name: &str) -> String {
fs::read_to_string(clone.landing().join("config").join(name)).unwrap()
}
#[test]
fn set_task_branch_seals_one_commit_and_converges_on_a_repeat() {
let tmp = TempDir::new().unwrap();
let e = edge(&tmp);
let clone = founded(&e);
let before = commits(&clone.landing());
conf(&e, &["set", "task-branch", "balls/x"]).unwrap();
assert!(landing_text(&clone, "balls.toml").contains("tasks_branch = \"balls/x\""));
assert_eq!(commits(&clone.landing()), before + 1);
assert_eq!(subject(&clone.landing()), "balls: conf set task-branch balls/x");
conf(&e, &["set", "task-branch", "balls/x"]).unwrap();
assert_eq!(commits(&clone.landing()), before + 1);
}
#[test]
fn set_task_branch_to_the_landing_is_refused_and_writes_nothing() {
let tmp = TempDir::new().unwrap();
let e = edge(&tmp);
let clone = founded(&e);
let before = commits(&clone.landing());
let err = conf(&e, &["set", "task-branch", "balls/config"]).unwrap_err().to_string();
assert!(err.contains("names the landing"), "{err}");
assert_eq!(commits(&clone.landing()), before);
}
#[test]
fn a_conf_seal_carries_the_checkout_scoped_trailers() {
let tmp = TempDir::new().unwrap();
let e = edge(&tmp);
let clone = founded(&e);
conf(&e, &["set", "task-branch", "balls/x"]).unwrap();
let msg = git::run(&clone.landing(), &["log", "-1", "--format=%B"], None).unwrap();
let md = crate::message::parse(&msg).unwrap();
assert_eq!(md["bl-protocol"], ["1"], "{msg}");
assert_eq!(md["bl-op"], ["conf"], "{msg}");
assert_eq!(md["bl-actor"], ["tester"], "{msg}");
assert!(!md.contains_key("bl-id"), "{msg}");
}
#[test]
fn set_log_level_validates_against_the_ladder() {
let tmp = TempDir::new().unwrap();
let e = edge(&tmp);
let clone = founded(&e);
assert!(conf(&e, &["set", "log-level", "noisy"]).is_err()); conf(&e, &["set", "log-level", "debug"]).unwrap();
assert!(landing_text(&clone, "balls.toml").contains("log_level = \"debug\""));
}
#[test]
fn set_task_remote_writes_the_per_clone_binding_preserving_other_keys() {
let tmp = TempDir::new().unwrap();
let e = edge(&tmp);
let clone = founded(&e);
conf(&e, &["set", "task-remote", "git@hub:r"]).unwrap();
let body = fs::read_to_string(clone.binding()).unwrap();
assert!(body.contains("remote = \"git@hub:r\""), "{body}");
fs::write(clone.binding(), "tasks_branch = \"balls/x\"\nremote = \"old\"\n").unwrap();
conf(&e, &["set", "task-remote", "git@hub:new"]).unwrap();
let body = fs::read_to_string(clone.binding()).unwrap();
assert!(body.contains("remote = \"git@hub:new\"") && body.contains("tasks_branch = \"balls/x\""), "{body}");
assert!(!e.xdg.user_config().exists(), "the global XDG config must not be written");
}
#[test]
fn set_clock_provider_writes_the_per_clone_binding_not_the_landing() {
let tmp = TempDir::new().unwrap();
let e = edge(&tmp);
let clone = founded(&e);
let before = commits(&clone.landing());
conf(&e, &["set", "clock-provider", "/opt/bl-workhours"]).unwrap();
let body = fs::read_to_string(clone.binding()).unwrap();
assert!(body.contains("clock_provider = \"/opt/bl-workhours\""), "{body}");
assert_eq!(commits(&clone.landing()), before, "a binding write seals no landing commit");
conf(&e, &["set", "task-remote", "git@hub:r"]).unwrap();
let body = fs::read_to_string(clone.binding()).unwrap();
assert!(body.contains("clock_provider = \"/opt/bl-workhours\"") && body.contains("remote = \"git@hub:r\""), "{body}");
}
#[test]
fn a_binding_write_replaces_by_rename_so_a_failed_write_loses_nothing() {
let tmp = TempDir::new().unwrap();
let e = edge(&tmp);
let clone = founded(&e);
conf(&e, &["set", "clock-provider", "/opt/bl-workhours"]).unwrap();
let established = fs::read_to_string(clone.binding()).unwrap();
assert!(!temps_beside(&clone), "a landed write leaves no temp behind");
let denied = fs::Permissions::from_mode(0o555);
let restored = fs::metadata(clone.root()).unwrap().permissions();
fs::set_permissions(clone.root(), denied).unwrap();
let failed = conf(&e, &["set", "clock-provider", "/opt/other"]);
fs::set_permissions(clone.root(), restored).unwrap();
assert!(failed.is_err(), "an unwritable clone dir must fail the binding write");
assert_eq!(fs::read_to_string(clone.binding()).unwrap(), established, "the established binding survived");
assert!(!temps_beside(&clone), "a failed write leaves no temp behind either");
}
#[test]
fn the_write_grammar_is_enforced() {
let tmp = TempDir::new().unwrap();
let e = edge(&tmp);
founded(&e);
for (argv, expect) in [
(vec!["set"], "needs <key>"),
(vec!["set", "bogus", "v"], "unknown key"),
(vec!["set", "task-branch", "a", "b"], "exactly one value"),
(vec!["set", "task-remote"], "exactly one value"),
(vec!["append", "task-branch", "x"], "is a scalar"),
(vec!["remove", "log-level", "x"], "is a scalar"),
(vec!["append", "close.pre"], "exactly one value"),
(vec!["append", "close.pre", "a", "b"], "exactly one value"),
] {
let err = conf(&e, &argv).unwrap_err().to_string();
assert!(err.contains(expect), "{argv:?}: {err}");
}
}
#[test]
fn set_task_remote_sentinel_is_a_landing_policy_write_a_url_clears_it() {
let tmp = TempDir::new().unwrap();
let e = edge(&tmp);
let clone = founded(&e);
conf(&e, &["set", "task-remote", "none"]).unwrap();
assert!(landing_text(&clone, "balls.toml").contains("task_remote = \"none\""));
assert_eq!(subject(&clone.landing()), "balls: conf set task-remote none");
conf(&e, &["set", "task-remote", "git@hub:r"]).unwrap();
assert!(!landing_text(&clone, "balls.toml").contains("task_remote"));
assert_eq!(subject(&clone.landing()), "balls: conf set task-remote git@hub:r");
let binding = fs::read_to_string(clone.binding()).unwrap();
assert!(binding.contains("remote = \"git@hub:r\""), "{binding}");
}
#[path = "conf_write_hooks_tests.rs"]
mod hooks;