use std::ffi::OsString;
use std::path::PathBuf;
use super::environment::{resolve_environment_root, EnvironmentRootInput};
use super::error::Result;
use super::files::{resolve_file_root, FileRootInput};
use super::roots::ResolvedRoot;
use super::util::PathProbe;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RootSelectionInput {
pub dotfiles_root: Option<OsString>,
pub current_dir: PathBuf,
pub home_dir: PathBuf,
pub git_top_level: Option<PathBuf>,
}
impl RootSelectionInput {
pub fn new(current_dir: impl Into<PathBuf>, home_dir: impl Into<PathBuf>) -> Self {
Self {
dotfiles_root: None,
current_dir: current_dir.into(),
home_dir: home_dir.into(),
git_top_level: None,
}
}
pub fn with_dotfiles_root(mut self, value: impl Into<OsString>) -> Self {
self.dotfiles_root = Some(value.into());
self
}
pub fn with_git_top_level(mut self, git_top_level: impl Into<PathBuf>) -> Self {
self.git_top_level = Some(git_top_level.into());
self
}
pub fn environment(&self) -> EnvironmentRootInput {
EnvironmentRootInput {
raw_value: self.dotfiles_root.clone(),
current_dir: self.current_dir.clone(),
home_dir: self.home_dir.clone(),
}
}
pub fn files(&self) -> FileRootInput {
FileRootInput {
current_dir: self.current_dir.clone(),
git_top_level: self.git_top_level.clone(),
}
}
}
pub fn resolve_root(input: &RootSelectionInput, probe: &dyn PathProbe) -> Result<ResolvedRoot> {
match resolve_environment_root(&input.environment(), probe)? {
Some(root) => Ok(root),
None => resolve_file_root(&input.files(), probe),
}
}
#[cfg(test)]
mod tests {
use std::path::Path;
use super::super::error::SafetyLockError;
use super::super::roots::RootSource;
use super::super::test_probe::{Entry, FakeProbe};
use super::super::util::OsPathProbe;
use super::*;
const CWD: &str = "/srv/dots/vim";
const HOME: &str = "/home/alice";
fn invocation() -> RootSelectionInput {
RootSelectionInput::new(CWD, HOME)
}
#[test]
fn an_explicit_root_outranks_both_implicit_mechanisms() {
let probe = FakeProbe::dir("/srv/explicit").and_dir("/srv/dots");
let input = invocation()
.with_git_top_level("/srv/dots")
.with_dotfiles_root("/srv/explicit");
let root = resolve_root(&input, &probe).unwrap();
assert_eq!(root.as_path(), Path::new("/srv/explicit"));
assert_eq!(root.source(), RootSource::Environment);
assert!(!root.requires_approval());
}
#[test]
fn without_an_explicit_root_the_git_top_level_wins() {
let probe = FakeProbe::dir("/srv/dots");
let root = resolve_root(&invocation().with_git_top_level("/srv/dots"), &probe).unwrap();
assert_eq!(root.as_path(), Path::new("/srv/dots"));
assert_eq!(root.source(), RootSource::Git);
assert!(root.requires_approval());
}
#[test]
fn without_git_the_current_directory_is_the_root() {
let probe = FakeProbe::dir(CWD).and_dir("/home/alice/dotfiles");
let root = resolve_root(&invocation(), &probe).unwrap();
assert_eq!(root.as_path(), Path::new(CWD));
assert_eq!(root.source(), RootSource::CurrentDirectory);
assert_eq!(
probe.canonicalized(),
vec![PathBuf::from(CWD)],
"the conventional home path was probed as a candidate"
);
}
#[test]
fn a_broken_explicit_root_never_reaches_implicit_selection() {
let probe = FakeProbe::dir("/srv/dots");
let input = invocation()
.with_git_top_level("/srv/dots")
.with_dotfiles_root("/srv/missing");
let error = resolve_root(&input, &probe).unwrap_err();
assert!(
matches!(error, SafetyLockError::EnvironmentRootUnusable { .. }),
"unexpected error: {error}"
);
assert_eq!(
probe.canonicalized(),
vec![PathBuf::from("/srv/missing")],
"an implicit candidate was probed after an explicit value failed"
);
}
#[test]
fn one_invocation_canonicalizes_exactly_once() {
for (input, probe) in [
(
invocation().with_dotfiles_root("~/dots"),
FakeProbe::dir("/home/alice/dots"),
),
(
invocation().with_git_top_level("/srv/link"),
FakeProbe::default().link("/srv/link", "/srv/dots"),
),
(invocation(), FakeProbe::dir(CWD)),
] {
resolve_root(&input, &probe).unwrap();
assert_eq!(
probe.canonicalized().len(),
1,
"resolution asked the filesystem more than once: {:?}",
probe.canonicalized()
);
}
}
#[test]
fn both_selection_paths_see_one_captured_invocation_directory() {
let input = invocation().with_dotfiles_root("dots");
assert_eq!(input.environment().current_dir, input.files().current_dir);
let probe = FakeProbe::dir("/srv/dots/vim/dots");
let root = resolve_root(&input, &probe).unwrap();
assert_eq!(root.as_path(), Path::new("/srv/dots/vim/dots"));
}
#[test]
fn the_resolved_root_is_carried_rather_than_rediscovered() {
let probe = FakeProbe::dir("/srv/dots");
let root = resolve_root(&invocation().with_git_top_level("/srv/dots"), &probe).unwrap();
let carried: Vec<ResolvedRoot> = (0..3).map(|_| root.clone()).collect();
assert!(carried.iter().all(|held| *held == root));
assert_eq!(
probe.canonicalized().len(),
1,
"carrying the root re-consulted the filesystem"
);
}
#[test]
fn the_whole_order_holds_against_a_real_filesystem() {
let home = tempfile::tempdir().unwrap();
let repository = home.path().join("dotfiles");
std::fs::create_dir_all(repository.join("vim")).unwrap();
let explicit = home.path().join("explicit");
std::fs::create_dir(&explicit).unwrap();
let canonical_repository = std::fs::canonicalize(&repository).unwrap();
let canonical_explicit = std::fs::canonicalize(&explicit).unwrap();
let inside = RootSelectionInput::new(repository.join("vim"), home.path())
.with_git_top_level(&repository);
let root = resolve_root(&inside, &OsPathProbe).unwrap();
assert_eq!(root.as_path(), canonical_repository);
assert_eq!(root.source(), RootSource::Git);
let root = resolve_root(
&inside.clone().with_dotfiles_root("~/explicit"),
&OsPathProbe,
)
.unwrap();
assert_eq!(root.as_path(), canonical_explicit);
assert_eq!(root.source(), RootSource::Environment);
let outside = RootSelectionInput::new(repository.join("vim"), home.path());
let root = resolve_root(&outside, &OsPathProbe).unwrap();
assert_eq!(root.as_path(), canonical_repository.join("vim"));
assert_eq!(root.source(), RootSource::CurrentDirectory);
}
#[test]
fn selection_never_reads_the_process_environment() {
for source in [
include_str!("selection.rs"),
include_str!("files.rs"),
include_str!("environment.rs"),
] {
let implementation = source
.split_once("#[cfg(test)]")
.expect("this file carries a test module")
.0;
let code = implementation
.lines()
.filter(|line| !line.trim_start().starts_with("//"))
.collect::<Vec<_>>()
.join("\n");
for forbidden in ["std::env", "env::var", "current_dir()", "Command::new"] {
assert!(
!code.contains(forbidden),
"root selection reads process state through `{forbidden}`"
);
}
}
}
#[test]
fn an_unusable_implicit_candidate_fails_the_whole_selection() {
let probe = FakeProbe::with("/srv/dots", Entry::NotADirectory);
let error =
resolve_root(&invocation().with_git_top_level("/srv/dots"), &probe).unwrap_err();
assert!(
matches!(error, SafetyLockError::ImplicitRootUnusable { .. }),
"unexpected error: {error}"
);
}
}