use super::*;
use crate::test_support::spawn_guard;
use std::fs;
use std::os::unix::fs::PermissionsExt;
use std::path::PathBuf;
use tempfile::tempdir;
struct Fake {
remote: Outcome,
branch: Outcome,
write: Outcome,
err: bool,
}
impl Fake {
fn ok(remote_out: &str, remote_prov: &str, branch_out: &str) -> Self {
Self {
remote: out(0, remote_out, remote_prov),
branch: out(0, branch_out, "conf: task-branch from landing"),
write: out(0, "", "conf set"),
err: false,
}
}
}
impl ConfRunner for Fake {
fn conf(&self, _project: &Path, args: &[&str]) -> io::Result<Outcome> {
if self.err {
return Err(io::Error::other("boom"));
}
Ok(match args {
["conf", "task-remote"] => self.remote.clone(),
["conf", "task-branch"] => self.branch.clone(),
_ => self.write.clone(),
})
}
}
fn out(exit: i32, stdout: &str, stderr: &str) -> Outcome {
Outcome {
exit,
stdout: stdout.to_owned(),
stderr: stderr.to_owned(),
}
}
fn argvs(p: &[Vec<String>]) -> Vec<Vec<&str>> {
p.iter()
.map(|a| a.iter().map(String::as_str).collect())
.collect()
}
#[test]
fn custom_branch_rejects_empty_and_default() {
assert_eq!(Mode::custom_branch(""), None);
assert_eq!(Mode::custom_branch(" "), None);
assert_eq!(Mode::custom_branch("balls/tasks"), None);
assert_eq!(
Mode::custom_branch(" my/universe "),
Some(Mode::CustomBranch("my/universe".to_owned()))
);
}
#[test]
fn plan_maps_each_mode_to_its_conf_writes() {
assert_eq!(
argvs(&plan(&Mode::Shared)),
vec![
vec!["conf", "set", "task-remote", "origin"],
vec!["conf", "set", "task-branch", "balls/tasks"],
]
);
assert_eq!(
argvs(&plan(&Mode::Stealth)),
vec![vec!["conf", "set", "task-remote", "none"]]
);
assert_eq!(
argvs(&plan(&Mode::CustomBranch("my/x".to_owned()))),
vec![
vec!["conf", "set", "task-branch", "my/x"],
vec!["conf", "set", "task-remote", "origin"],
]
);
}
#[test]
fn read_unprimed_project_is_none() {
let fake = Fake {
remote: out(1, "", "bl: no balls checkout here — run `bl prime` first"),
branch: out(0, "balls/tasks", ""),
write: out(0, "", ""),
err: false,
};
assert_eq!(read(&fake, Path::new("/p")).unwrap(), None);
}
#[test]
fn read_classifies_stealth_shared_and_custom() {
let s = read(
&Fake::ok("(none)", "conf: task-remote from stealth", "balls/tasks"),
Path::new("/p"),
)
.unwrap()
.unwrap();
assert_eq!(s.mode, Mode::Stealth);
assert_eq!(s.provenance, "stealth");
let s = read(
&Fake::ok("", "conf: task-remote from binding", "balls/tasks"),
Path::new("/p"),
)
.unwrap()
.unwrap();
assert_eq!(s.mode, Mode::Stealth);
let s = read(
&Fake::ok("origin", "conf: task-remote from binding", "balls/tasks"),
Path::new("/p"),
)
.unwrap()
.unwrap();
assert_eq!(s.mode, Mode::Shared);
assert_eq!(s.provenance, "binding");
let s = read(
&Fake::ok("origin", "conf: task-remote from origin", "team/parallel"),
Path::new("/p"),
)
.unwrap()
.unwrap();
assert_eq!(s.mode, Mode::CustomBranch("team/parallel".to_owned()));
}
#[test]
fn read_provenance_falls_back_to_the_whole_line() {
let s = read(
&Fake::ok("origin", "unexpected shape", "balls/tasks"),
Path::new("/p"),
)
.unwrap()
.unwrap();
assert_eq!(s.provenance, "unexpected shape");
}
#[test]
fn read_propagates_a_spawn_error() {
let fake = Fake {
remote: out(0, "", ""),
branch: out(0, "", ""),
write: out(0, "", ""),
err: true,
};
assert!(read(&fake, Path::new("/p")).is_err());
}
#[test]
fn apply_stealth_logs_one_write_and_rereads() {
let dir = tempdir().unwrap();
let sr = dir.path();
let fake = Fake::ok("(none)", "conf: task-remote from landing", "balls/tasks");
let after = apply(&fake, sr, "42", Path::new("/proj"), &Mode::Stealth)
.unwrap()
.unwrap();
assert_eq!(after.mode, Mode::Stealth);
let ops = opslog::tail(sr, 10);
assert_eq!(ops.len(), 1);
assert_eq!(
ops[0].argv,
vec!["bl", "conf", "set", "task-remote", "none"]
);
assert_eq!(ops[0].cwd, "/proj");
assert_eq!(ops[0].exit, 0);
assert_eq!(ops[0].ts, "42");
}
#[test]
fn apply_shared_logs_both_writes() {
let dir = tempdir().unwrap();
let sr = dir.path();
let fake = Fake::ok("origin", "conf: task-remote from binding", "balls/tasks");
let after = apply(&fake, sr, "1", Path::new("/proj"), &Mode::Shared)
.unwrap()
.unwrap();
assert_eq!(after.mode, Mode::Shared);
let ops = opslog::tail(sr, 10);
assert_eq!(ops.len(), 2);
assert_eq!(
ops[0].argv,
vec!["bl", "conf", "set", "task-remote", "origin"]
);
assert_eq!(
ops[1].argv,
vec!["bl", "conf", "set", "task-branch", "balls/tasks"]
);
}
#[test]
fn apply_stops_at_the_first_failed_write() {
let dir = tempdir().unwrap();
let sr = dir.path();
let fake = Fake {
remote: out(0, "origin", "conf: task-remote from binding"),
branch: out(0, "balls/tasks", "conf: task-branch from landing"),
write: out(1, "", "conf set: refused"),
err: false,
};
let after = apply(&fake, sr, "9", Path::new("/proj"), &Mode::Shared).unwrap();
assert!(after.is_some());
let ops = opslog::tail(sr, 10);
assert_eq!(ops.len(), 1);
assert_eq!(ops[0].exit, 1);
assert_eq!(ops[0].stderr, "conf set: refused");
}
#[test]
fn apply_propagates_a_spawn_error() {
let dir = tempdir().unwrap();
let fake = Fake {
remote: out(0, "", ""),
branch: out(0, "", ""),
write: out(0, "", ""),
err: true,
};
assert!(apply(&fake, dir.path(), "0", Path::new("/proj"), &Mode::Stealth).is_err());
}
fn recorder(dir: &Path, log: &Path) -> PathBuf {
let path = dir.join("bl");
let body = format!(
"#!/bin/sh\nfor a in \"$@\"; do printf '%s\\n' \"$a\" >> {}; done\n\
: > here\nprintf 'origin'\nprintf 'conf: task-remote from binding' 1>&2\nexit 0\n",
log.display()
);
fs::write(&path, body).unwrap();
let mut perms = fs::metadata(&path).unwrap().permissions();
perms.set_mode(0o755);
fs::set_permissions(&path, perms).unwrap();
path
}
#[test]
fn blconf_runs_in_project_and_captures_output() {
let g = spawn_guard();
let dir = tempdir().unwrap();
let proj = tempdir().unwrap();
let log = dir.path().join("argv");
let bin = recorder(dir.path(), &log);
let runner = BlConf::new(Cli::new(bin));
let out = runner.conf(proj.path(), &["conf", "task-remote"]).unwrap();
drop(g);
assert_eq!(fs::read_to_string(&log).unwrap(), "conf\ntask-remote\n");
assert!(proj.path().join("here").exists());
assert_eq!(out.stdout, "origin");
assert_eq!(out.stderr, "conf: task-remote from binding");
assert!(out.ok());
}
#[test]
fn blconf_resolve_uses_the_bl_binary_name() {
let world = crate::xdg::Env::from_pairs([("HOME", "/h"), ("XDG_DATA_HOME", "/d")]);
assert_eq!(BlConf::resolve(&world).cli.binary(), Path::new("bl"));
}