use std::os::unix::fs::{MetadataExt, PermissionsExt};
use std::path::{Path, PathBuf};
use std::time::SystemTime;
use super::super::tests::NO_TMUX;
use super::super::{init, resolve_dir, user_config_path};
use super::{DirRefusal, check};
struct Scratch {
outer: tempfile::TempDir,
}
impl Scratch {
fn new() -> Self {
let outer = tempfile::tempdir().expect("scratch dir");
std::fs::create_dir(outer.path().join("home")).expect("scratch home");
Self { outer }
}
fn outer(&self) -> PathBuf {
std::fs::canonicalize(self.outer.path()).unwrap()
}
fn home(&self) -> PathBuf {
self.outer().join("home")
}
fn repo(&self, path: &Path, remote: bool) -> PathBuf {
std::fs::create_dir_all(path).unwrap();
git(path, &["init", "--quiet"]);
if remote {
git(
path,
&["remote", "add", "origin", "https://example.invalid/r.git"],
);
}
path.to_path_buf()
}
fn refused(&self, dir: &Path) -> DirRefusal {
refused_under(dir, &self.home())
}
}
fn git(dir: &Path, args: &[&str]) {
let out = super::git().arg("-C").arg(dir).args(args).output().unwrap();
assert!(out.status.success(), "git {args:?}: {out:?}");
}
fn write_set(dir: &Path, home: &Path) -> Vec<(PathBuf, Option<(u64, SystemTime)>)> {
[
dir.to_path_buf(),
dir.join(".git"),
dir.join(".trusty-mpm.toml"),
user_config_path(home),
]
.into_iter()
.map(|path| {
let meta = std::fs::symlink_metadata(&path).ok();
let stamp = meta.map(|m| (m.len(), m.modified().expect("mtime")));
(path, stamp)
})
.collect()
}
fn refused_under(dir: &Path, home: &Path) -> DirRefusal {
let before = write_set(dir, home);
let err =
init(dir, home, false, NO_TMUX).expect_err(&format!("{} must be refused", dir.display()));
let refusal = err
.downcast_ref::<DirRefusal>()
.unwrap_or_else(|| panic!("{}: not a preflight refusal: {err:#}", dir.display()))
.clone();
assert_eq!(
write_set(dir, home),
before,
"{}: init changed what it writes",
dir.display()
);
refusal
}
fn assert_workspace_parent(refusal: DirRefusal, dir: &Path, why: &str) {
match refusal {
DirRefusal::WorkspaceParent { path, reason } => {
assert_eq!(path, dir);
assert!(reason.contains(why), "{dir:?}: {reason}");
}
other => panic!("{dir:?}: expected a workspace parent, got {other:?}"),
}
}
fn assert_unresolvable(refusal: DirRefusal, why: &str) {
assert!(
matches!(&refusal, DirRefusal::Unresolvable { reason, .. } if reason.contains(why)),
"expected Unresolvable({why}), got {refusal:?}"
);
}
#[test]
fn the_filesystem_root_is_refused() {
let s = Scratch::new();
for root in ["/", "/usr/.."] {
assert_eq!(
s.refused(Path::new(root)),
DirRefusal::FilesystemRoot(PathBuf::from("/"))
);
}
let dev = Path::new("/dev");
let device = |p: &Path| std::fs::metadata(p).map(|m| m.dev()).unwrap();
if device(dev) != device(Path::new("/")) {
assert_eq!(
s.refused(dev),
DirRefusal::FilesystemRoot(dev.to_path_buf())
);
}
}
#[test]
fn the_home_directory_is_refused_however_it_is_spelled() {
let s = Scratch::new();
let home = s.home();
std::fs::create_dir(home.join("sub")).unwrap();
let link = s.outer().join("home-link");
std::os::unix::fs::symlink(&home, &link).unwrap();
for spelled in [home.clone(), home.join("sub/.."), link] {
assert_eq!(
s.refused(&spelled),
DirRefusal::Home(home.clone()),
"{spelled:?}"
);
}
}
#[test]
fn a_path_inside_another_work_tree_is_refused() {
let s = Scratch::new();
let repo = s.repo(&s.home().join("code/app"), false);
std::fs::create_dir(repo.join("docs")).unwrap();
for inner in [repo.join("docs"), repo.join("new/arch")] {
assert_eq!(
s.refused(&inner),
DirRefusal::InsideWorkTree {
path: inner.clone(),
work_tree: repo.clone()
}
);
}
}
#[test]
fn a_repository_with_a_remote_is_refused() {
let s = Scratch::new();
let cloned = s.repo(&s.home().join("clone"), true);
assert_eq!(
s.refused(&cloned),
DirRefusal::HasRemote {
path: cloned,
remotes: "origin".to_owned()
}
);
}
#[test]
fn the_projects_root_is_refused_populated_or_not() {
let s = Scratch::new();
let root = s.home().join("trusty-mpm-projects");
assert_workspace_parent(s.refused(&root), &root, "projects root");
s.repo(&root.join("bobmatnyc/trusty-tools"), true);
assert_workspace_parent(s.refused(&root), &root, "projects root");
}
#[test]
fn an_owner_directory_under_the_projects_root_is_refused() {
let s = Scratch::new();
let owner = s.home().join("trusty-mpm-projects/bobmatnyc");
s.repo(&owner.join("trusty-tools"), true);
assert_workspace_parent(s.refused(&owner), &owner, "contains the repository");
}
#[test]
fn a_directory_holding_a_deep_repository_is_refused() {
let s = Scratch::new();
let code = s.home().join("code");
s.repo(&code.join("deep/nested/app"), false);
assert_workspace_parent(s.refused(&code), &code, "contains the repository");
}
#[test]
fn an_ancestor_of_the_home_directory_is_refused() {
let s = Scratch::new();
let outer = s.outer();
assert_workspace_parent(s.refused(&outer), &outer, "home directory");
}
#[test]
fn a_dangling_symlink_refuses() {
let s = Scratch::new();
let dangling = s.home().join("dangling");
std::os::unix::fs::symlink(s.home().join("missing"), &dangling).unwrap();
assert_unresolvable(s.refused(&dangling), "dangling symlink");
}
#[test]
fn dot_dot_below_a_missing_directory_refuses() {
let s = Scratch::new();
assert_unresolvable(s.refused(&s.home().join("nope/../arch")), "`..`");
}
#[test]
fn an_unresolvable_home_refuses() {
let s = Scratch::new();
let dir = s.outer().join("arch");
let refusal = refused_under(&dir, &s.outer().join("no-such-home"));
assert_unresolvable(refusal, "No such file");
assert!(!dir.exists(), "init created the directory before refusing");
}
struct Unlock(PathBuf);
impl Drop for Unlock {
fn drop(&mut self) {
let _ = std::fs::set_permissions(&self.0, std::fs::Permissions::from_mode(0o755));
}
}
#[test]
fn an_unreadable_target_refuses() {
let s = Scratch::new();
let unreadable = s.home().join("unreadable");
std::fs::create_dir(&unreadable).unwrap();
std::fs::set_permissions(&unreadable, std::fs::Permissions::from_mode(0o300)).unwrap();
let _unlock = Unlock(unreadable.clone());
assert_unresolvable(s.refused(&unreadable), "did not finish");
}
#[test]
fn a_failing_remote_listing_refuses() {
let s = Scratch::new();
let broken = s.home().join("broken");
std::fs::create_dir(&broken).unwrap();
std::fs::write(broken.join(".git"), "not a gitdir\n").unwrap();
let refusal = s.refused(&broken);
assert!(
matches!(refusal, DirRefusal::GitProbeFailed { .. }),
"{refusal:?}"
);
}
fn write_tm_config(home: &Path, yaml: &str) -> PathBuf {
let config = home.join(".trusty-tools/trusty-mpm/config.yaml");
std::fs::create_dir_all(config.parent().unwrap()).unwrap();
std::fs::write(&config, yaml).unwrap();
config
}
#[test]
fn a_malformed_tm_config_refuses() {
let s = Scratch::new();
let config = write_tm_config(&s.home(), "workspace_root_template: [unclosed\n");
let refusal = s.refused(&s.home().join("arch"));
assert_unresolvable(refusal, &config.display().to_string());
}
#[test]
fn the_configured_projects_root_is_refused() {
let s = Scratch::new();
let root = s.outer().join("elsewhere");
write_tm_config(
&s.home(),
&format!("workspace_root_template: {}\n", root.display()),
);
assert_workspace_parent(s.refused(&root), &root, "projects root");
}
#[test]
fn the_tm_config_directory_is_refused() {
let s = Scratch::new();
let config_dir = s.home().join(".trusty-mpm");
std::fs::create_dir_all(config_dir.join("sub")).unwrap();
for dir in [
config_dir.clone(),
config_dir.join("sub"),
config_dir.join("arch"),
] {
assert_eq!(
s.refused(&dir),
DirRefusal::ConfigDir {
path: dir.clone(),
config_dir: config_dir.clone()
}
);
}
}
#[test]
fn a_directory_holding_a_symlinked_tm_config_directory_is_refused() {
let s = Scratch::new();
let dotfiles = s.outer().join("dotfiles");
let real = dotfiles.join("trusty-mpm");
std::fs::create_dir_all(&real).unwrap();
std::os::unix::fs::symlink(&real, s.home().join(".trusty-mpm")).unwrap();
assert_eq!(
s.refused(&dotfiles),
DirRefusal::ConfigDir {
path: dotfiles.clone(),
config_dir: real
}
);
}
#[test]
fn a_looping_ancestor_git_entry_refuses() {
let s = Scratch::new();
let code = s.home().join("code");
std::fs::create_dir(&code).unwrap();
std::os::unix::fs::symlink(code.join(".git"), code.join(".git")).unwrap();
match s.refused(&code.join("arch")) {
DirRefusal::Unresolvable { path, .. } => assert_eq!(path, code.join(".git")),
other => panic!("expected Unresolvable for the looping .git, got {other:?}"),
}
}
#[test]
fn the_default_and_a_fresh_directory_pass() {
let s = Scratch::new();
let home = s.home();
s.repo(
&home.join("trusty-mpm-projects/bobmatnyc/trusty-tools"),
true,
);
let default = resolve_dir(None, &home).unwrap();
assert_eq!(check(&default, &home), Ok(default.clone()));
let fresh = s.outer().join("fresh");
std::fs::create_dir(&fresh).unwrap();
assert_eq!(check(&fresh, &home), Ok(fresh.clone()));
assert_eq!(check(&fresh.join("a/b"), &home), Ok(fresh.join("a/b")));
let link = s.outer().join("fresh-link");
std::os::unix::fs::symlink(&fresh, &link).unwrap();
assert_eq!(check(&link, &home), Ok(fresh.clone()));
init(&default, &home, false, NO_TMUX).unwrap();
assert_eq!(check(&default, &home), Ok(default.clone()));
let again = init(&default, &home, false, NO_TMUX).unwrap();
assert!(!again.changed(), "{}", again.render());
}
fn supervisor_with_local(s: &Scratch) -> PathBuf {
let dir = s.home().join("code/supervisor");
s.repo(&dir.join("local"), false);
dir
}
#[test]
fn a_private_local_repository_without_a_remote_passes() {
let s = Scratch::new();
let dir = supervisor_with_local(&s);
assert_eq!(check(&dir, &s.home()), Ok(dir.clone()));
init(&dir, &s.home(), false, NO_TMUX).unwrap_or_else(|e| panic!("{e:#}"));
assert!(dir.join(".git").is_dir(), "init made no repository");
assert_eq!(check(&dir, &s.home()), Ok(dir.clone()));
}
#[test]
fn a_private_local_repository_with_a_remote_is_refused() {
let s = Scratch::new();
let dir = s.home().join("code/supervisor");
s.repo(&dir.join("local"), true);
assert_workspace_parent(s.refused(&dir), &dir, "has remote(s) origin");
}
#[test]
fn a_repository_nested_in_the_private_local_repository_is_refused() {
let s = Scratch::new();
let dir = supervisor_with_local(&s);
let nested = s.repo(&dir.join("local/vendor-app/app"), false);
assert_workspace_parent(
s.refused(&dir),
&dir,
&format!("contains the repository {}", nested.display()),
);
}
#[test]
fn an_unstatable_private_repository_refuses() {
let s = Scratch::new();
let dir = supervisor_with_local(&s);
let local = dir.join("local");
std::fs::set_permissions(&local, std::fs::Permissions::from_mode(0o600)).unwrap();
let _unlock = Unlock(local.clone());
let refusal = s.refused(&dir);
assert!(
matches!(&refusal, DirRefusal::Unresolvable { path, reason }
if *path == local.join(".git") && reason.contains("Permission denied")),
"{refusal:?}"
);
}
#[test]
fn a_failing_remote_listing_on_the_private_repository_refuses() {
let s = Scratch::new();
let dir = s.home().join("code/supervisor");
std::fs::create_dir_all(dir.join("local/.git")).unwrap();
let refusal = s.refused(&dir);
assert!(
matches!(&refusal, DirRefusal::GitProbeFailed { path, .. } if *path == dir.join("local")),
"{refusal:?}"
);
}
#[test]
fn a_symlinked_local_repository_is_refused() {
let s = Scratch::new();
let elsewhere = s.repo(&s.outer().join("elsewhere"), false);
let linked = s.home().join("code/linked");
std::fs::create_dir_all(&linked).unwrap();
std::os::unix::fs::symlink(&elsewhere, linked.join("local")).unwrap();
let gitfile = s.home().join("code/gitfile");
std::fs::create_dir_all(gitfile.join("local")).unwrap();
std::fs::write(gitfile.join("local/.git"), "gitdir: /elsewhere\n").unwrap();
let gitlink = s.home().join("code/gitlink");
std::fs::create_dir_all(gitlink.join("local")).unwrap();
std::os::unix::fs::symlink(elsewhere.join(".git"), gitlink.join("local/.git")).unwrap();
for dir in [linked, gitfile, gitlink] {
assert_workspace_parent(s.refused(&dir), &dir, "contains the repository");
}
}
#[test]
fn every_other_child_repository_is_still_refused() {
let s = Scratch::new();
let beside = supervisor_with_local(&s);
let notes = s.repo(&beside.join("notes"), false);
assert_workspace_parent(
s.refused(&beside),
&beside,
&format!("contains the repository {}", notes.display()),
);
let deeper = s.home().join("code/deeper");
s.repo(&deeper.join("sub/local"), false);
assert_workspace_parent(s.refused(&deeper), &deeper, "contains the repository");
let named = s.home().join("code/named");
s.repo(&named.join("private"), false);
assert_workspace_parent(s.refused(&named), &named, "contains the repository");
}
#[test]
fn a_private_local_repository_changes_no_other_refusal() {
let s = Scratch::new();
let home = s.home();
s.repo(&home.join("local"), false);
assert_eq!(s.refused(&home), DirRefusal::Home(home.clone()));
let config_dir = home.join(".trusty-mpm");
s.repo(&config_dir.join("local"), false);
assert_eq!(
s.refused(&config_dir),
DirRefusal::ConfigDir {
path: config_dir.clone(),
config_dir: config_dir.clone()
}
);
let repo = s.repo(&home.join("work/app"), false);
let inner = repo.join("arch");
s.repo(&inner.join("local"), false);
assert_eq!(
s.refused(&inner),
DirRefusal::InsideWorkTree {
path: inner.clone(),
work_tree: repo
}
);
let cloned = s.repo(&home.join("clone"), true);
s.repo(&cloned.join("local"), false);
assert_eq!(
s.refused(&cloned),
DirRefusal::HasRemote {
path: cloned,
remotes: "origin".to_owned()
}
);
}