281 lines
8.4 KiB
Rust
281 lines
8.4 KiB
Rust
use std::path::PathBuf;
|
|
use std::process::Command;
|
|
|
|
struct Sandbox {
|
|
path: PathBuf,
|
|
}
|
|
|
|
impl Sandbox {
|
|
fn new(name: &str) -> Self {
|
|
let path = std::env::temp_dir().join(format!("doot-test-{}", name));
|
|
if path.exists() {
|
|
std::fs::remove_dir_all(&path).unwrap();
|
|
}
|
|
std::fs::create_dir_all(&path).unwrap();
|
|
Self { path }
|
|
}
|
|
|
|
fn run(&self, args: &[&str]) -> std::process::Output {
|
|
let doot = env!("CARGO_BIN_EXE_doot");
|
|
Command::new(doot)
|
|
.args(args)
|
|
.env("DOOT_HOME", &self.path)
|
|
.env("DOOT_TEST_MODE", "1")
|
|
.output()
|
|
.expect("failed to run doot")
|
|
}
|
|
|
|
fn config_dir(&self) -> PathBuf {
|
|
self.path.join(".config/doot")
|
|
}
|
|
|
|
fn state_dir(&self) -> PathBuf {
|
|
self.path.join(".local/state/doot")
|
|
}
|
|
|
|
fn config_file(&self) -> PathBuf {
|
|
self.config_dir().join("doot.doot")
|
|
}
|
|
|
|
fn write_config(&self, content: &str) {
|
|
std::fs::create_dir_all(self.config_dir()).unwrap();
|
|
std::fs::write(self.config_file(), content).unwrap();
|
|
}
|
|
|
|
fn write_source(&self, path: &str, content: &str) {
|
|
let full_path = self.config_dir().join(path);
|
|
if let Some(parent) = full_path.parent() {
|
|
std::fs::create_dir_all(parent).unwrap();
|
|
}
|
|
std::fs::write(full_path, content).unwrap();
|
|
}
|
|
|
|
fn is_symlink(&self, path: &PathBuf) -> bool {
|
|
path.is_symlink()
|
|
}
|
|
|
|
fn symlink_target(&self, path: &PathBuf) -> Option<PathBuf> {
|
|
std::fs::read_link(path).ok()
|
|
}
|
|
}
|
|
|
|
impl Drop for Sandbox {
|
|
fn drop(&mut self) {
|
|
let _ = std::fs::remove_dir_all(&self.path);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_init_creates_structure() {
|
|
let sandbox = Sandbox::new("init");
|
|
let output = sandbox.run(&["init"]);
|
|
|
|
assert!(output.status.success(), "init failed: {:?}", output);
|
|
assert!(sandbox.config_file().exists(), "config file not created");
|
|
assert!(sandbox.config_dir().join("config").exists(), "config dir not created");
|
|
assert!(sandbox.state_dir().join("backups").exists(), "backups dir not created");
|
|
assert!(sandbox.state_dir().join("snapshots").exists(), "snapshots dir not created");
|
|
}
|
|
|
|
#[test]
|
|
fn test_check_valid_config() {
|
|
let sandbox = Sandbox::new("check-valid");
|
|
sandbox.write_config(r#"
|
|
package: "ripgrep"
|
|
package: "fd"
|
|
"#);
|
|
|
|
let output = sandbox.run(&["check"]);
|
|
assert!(output.status.success(), "check failed: {:?}", output);
|
|
}
|
|
|
|
#[test]
|
|
fn test_apply_dry_run() {
|
|
let sandbox = Sandbox::new("apply-dry");
|
|
sandbox.write_config(r#"
|
|
dotfile:
|
|
source = "config/test.conf"
|
|
target = "~/.config/test/test.conf"
|
|
"#);
|
|
sandbox.write_source("config/test.conf", "test content");
|
|
|
|
let output = sandbox.run(&["apply", "-n"]);
|
|
assert!(output.status.success(), "apply -n failed: {:?}", output);
|
|
|
|
let target = sandbox.path.join(".config/test/test.conf");
|
|
assert!(!target.exists(), "dry run should not create files");
|
|
}
|
|
|
|
#[test]
|
|
fn test_apply_creates_symlink() {
|
|
let sandbox = Sandbox::new("apply-symlink");
|
|
sandbox.write_config(r#"
|
|
dotfile:
|
|
source = "config/app.conf"
|
|
target = "~/.config/app/app.conf"
|
|
deploy = "link"
|
|
"#);
|
|
sandbox.write_source("config/app.conf", "app config content");
|
|
|
|
let output = sandbox.run(&["apply"]);
|
|
assert!(output.status.success(), "apply failed: {:?}", output);
|
|
|
|
let target = sandbox.path.join(".config/app/app.conf");
|
|
assert!(sandbox.is_symlink(&target), "target should be symlink");
|
|
|
|
let expected_source = sandbox.config_dir().join("config/app.conf");
|
|
assert_eq!(
|
|
sandbox.symlink_target(&target),
|
|
Some(expected_source),
|
|
"symlink should point to source"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_apply_unchanged_on_rerun() {
|
|
let sandbox = Sandbox::new("apply-unchanged");
|
|
sandbox.write_config(
|
|
"dotfile:\n source = \"config/app.conf\"\n target = \"~/.config/app/app.conf\"\n deploy = \"link\"\n",
|
|
);
|
|
sandbox.write_source("config/app.conf", "content");
|
|
|
|
let first = sandbox.run(&["apply"]);
|
|
assert!(first.status.success(), "first apply failed");
|
|
|
|
let second = sandbox.run(&["apply"]);
|
|
assert!(second.status.success(), "second apply failed");
|
|
|
|
// Second apply should succeed (symlink already exists and points correctly)
|
|
let target = sandbox.path.join(".config/app/app.conf");
|
|
assert!(target.is_symlink(), "target should still be symlink after second apply");
|
|
}
|
|
|
|
#[test]
|
|
fn test_apply_creates_copy() {
|
|
let sandbox = Sandbox::new("apply-copy");
|
|
sandbox.write_config(r#"
|
|
dotfile:
|
|
source = "config/app.conf"
|
|
target = "~/.config/app/app.conf"
|
|
"#);
|
|
sandbox.write_source("config/app.conf", "app config content");
|
|
|
|
let output = sandbox.run(&["apply"]);
|
|
assert!(output.status.success(), "apply failed: {:?}", output);
|
|
|
|
let target = sandbox.path.join(".config/app/app.conf");
|
|
assert!(target.exists(), "target should exist");
|
|
assert!(!target.is_symlink(), "target should be a copy, not a symlink");
|
|
|
|
let content = std::fs::read_to_string(&target).unwrap();
|
|
assert_eq!(content, "app config content", "content should match source");
|
|
}
|
|
|
|
#[test]
|
|
fn test_apply_copy_unchanged_on_rerun() {
|
|
let sandbox = Sandbox::new("apply-copy-unchanged");
|
|
sandbox.write_config(
|
|
"dotfile:\n source = \"config/app.conf\"\n target = \"~/.config/app/app.conf\"\n",
|
|
);
|
|
sandbox.write_source("config/app.conf", "content");
|
|
|
|
let first = sandbox.run(&["apply"]);
|
|
assert!(first.status.success(), "first apply failed");
|
|
|
|
let second = sandbox.run(&["apply"]);
|
|
assert!(second.status.success(), "second apply failed");
|
|
|
|
let target = sandbox.path.join(".config/app/app.conf");
|
|
assert!(target.exists(), "target should exist after second apply");
|
|
assert!(!target.is_symlink(), "target should still be a copy");
|
|
}
|
|
|
|
#[test]
|
|
fn test_status_shows_state() {
|
|
let sandbox = Sandbox::new("status");
|
|
sandbox.write_config(r#"
|
|
dotfile:
|
|
source = "config/app.conf"
|
|
target = "~/.config/app/app.conf"
|
|
"#);
|
|
sandbox.write_source("config/app.conf", "content");
|
|
sandbox.run(&["apply"]);
|
|
|
|
let output = sandbox.run(&["status"]);
|
|
assert!(output.status.success(), "status failed: {:?}", output);
|
|
}
|
|
|
|
#[test]
|
|
fn test_snapshot_and_rollback() {
|
|
let sandbox = Sandbox::new("snapshot");
|
|
sandbox.write_config(r#"
|
|
dotfile:
|
|
source = "config/app.conf"
|
|
target = "~/.config/app/app.conf"
|
|
"#);
|
|
sandbox.write_source("config/app.conf", "v1");
|
|
sandbox.run(&["apply"]);
|
|
|
|
let snap_output = sandbox.run(&["snapshot", "v1"]);
|
|
assert!(snap_output.status.success(), "snapshot failed: {:?}", snap_output);
|
|
|
|
let snapshot_file = sandbox.state_dir().join("snapshots/v1.json");
|
|
assert!(snapshot_file.exists(), "snapshot file not created");
|
|
}
|
|
|
|
#[test]
|
|
fn test_dotfile_with_when_condition() {
|
|
let sandbox = Sandbox::new("conditional");
|
|
|
|
// Test that 'when' condition works - only deploy if condition is true
|
|
let config = r#"dotfile:
|
|
source = "config/test.conf"
|
|
target = "~/.config/test.conf"
|
|
when = true
|
|
"#;
|
|
sandbox.write_config(config);
|
|
sandbox.write_source("config/test.conf", "test content");
|
|
|
|
let output = sandbox.run(&["apply"]);
|
|
assert!(output.status.success(), "apply failed: {:?}", output);
|
|
|
|
let target = sandbox.path.join(".config/test.conf");
|
|
assert!(target.exists(), "file should be deployed when condition is true");
|
|
}
|
|
|
|
#[test]
|
|
fn test_dotfile_when_false_skips() {
|
|
let sandbox = Sandbox::new("when-false");
|
|
|
|
let config = r#"dotfile:
|
|
source = "config/skip.conf"
|
|
target = "~/.config/skip.conf"
|
|
when = false
|
|
"#;
|
|
sandbox.write_config(config);
|
|
sandbox.write_source("config/skip.conf", "should not deploy");
|
|
|
|
let output = sandbox.run(&["apply"]);
|
|
assert!(output.status.success(), "apply failed: {:?}", output);
|
|
|
|
let target = sandbox.path.join(".config/skip.conf");
|
|
assert!(!target.exists(), "file should NOT be deployed when condition is false");
|
|
}
|
|
|
|
#[test]
|
|
fn test_diff_shows_changes() {
|
|
let sandbox = Sandbox::new("diff");
|
|
sandbox.write_config(r#"
|
|
dotfile:
|
|
source = "config/app.conf"
|
|
target = "~/.config/app/app.conf"
|
|
"#);
|
|
sandbox.write_source("config/app.conf", "new content");
|
|
|
|
let target_dir = sandbox.path.join(".config/app");
|
|
std::fs::create_dir_all(&target_dir).unwrap();
|
|
std::fs::write(target_dir.join("app.conf"), "old content").unwrap();
|
|
|
|
let output = sandbox.run(&["diff"]);
|
|
assert!(output.status.success(), "diff failed: {:?}", output);
|
|
}
|