use std::fs; use std::path::{Path, PathBuf}; use std::process::Command; use tempfile::TempDir; // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- struct TestEnv { home: TempDir, themes: TempDir, state_file: PathBuf, } impl TestEnv { fn new() -> Self { let home = TempDir::new().unwrap(); let themes = TempDir::new().unwrap(); let state_file = home.path().join(".config/theme-manager/state.json"); TestEnv { home, themes, state_file, } } fn make_theme_file(&self, theme: &str, rel: &str, content: &str) -> PathBuf { let p = self.themes.path().join(theme).join(rel); fs::create_dir_all(p.parent().unwrap()).unwrap(); fs::write(&p, content).unwrap(); p } fn target(&self, rel: &str) -> PathBuf { self.home.path().join(rel) } fn dm(&self, args: &[&str]) -> std::process::Output { let bin = env!("CARGO_BIN_EXE_dm"); Command::new(bin) .args(args) .env("DM_HOME", self.home.path()) .env("DM_THEMES_ROOT", self.themes.path()) .env("DM_STATE_FILE", &self.state_file) .output() .unwrap() } fn dm_stdout(&self, args: &[&str]) -> String { let out = self.dm(args); String::from_utf8_lossy(&out.stdout).to_string() } fn dm_success(&self, args: &[&str]) { let out = self.dm(args); if !out.status.success() { panic!( "dm {:?} failed:\nstdout: {}\nstderr: {}", args, String::from_utf8_lossy(&out.stdout), String::from_utf8_lossy(&out.stderr), ); } } } fn is_symlink(p: &Path) -> bool { p.symlink_metadata() .map(|m| m.file_type().is_symlink()) .unwrap_or(false) } // --------------------------------------------------------------------------- // list // --------------------------------------------------------------------------- #[test] fn list_shows_available_themes() { let env = TestEnv::new(); fs::create_dir(env.themes.path().join("dracula")).unwrap(); fs::create_dir(env.themes.path().join("catppuccin")).unwrap(); let out = env.dm_stdout(&["list"]); assert!(out.contains("dracula"), "expected dracula in: {out}"); assert!(out.contains("catppuccin"), "expected catppuccin in: {out}"); } // --------------------------------------------------------------------------- // status // --------------------------------------------------------------------------- #[test] fn status_before_apply_reports_no_theme() { let env = TestEnv::new(); let out = env.dm_stdout(&["status"]); assert!(out.contains("No theme"), "got: {out}"); } #[test] fn status_after_apply_shows_theme_name() { let env = TestEnv::new(); env.make_theme_file("dracula", ".zshrc", "# dracula"); env.dm_success(&["apply", "dracula"]); let out = env.dm_stdout(&["status"]); assert!(out.contains("dracula"), "got: {out}"); } // --------------------------------------------------------------------------- // apply from empty state // --------------------------------------------------------------------------- #[test] fn apply_creates_symlinks_under_home() { let env = TestEnv::new(); env.make_theme_file("dracula", ".zshrc", "# zsh"); env.make_theme_file("dracula", ".config/nvim/init.lua", "-- nvim"); env.dm_success(&["apply", "dracula"]); assert!(is_symlink(&env.target(".zshrc"))); assert!(is_symlink(&env.target(".config/nvim/init.lua"))); } #[test] fn apply_output_shows_summary_counts() { let env = TestEnv::new(); env.make_theme_file("dracula", ".zshrc", ""); let out = env.dm_stdout(&["apply", "dracula"]); assert!(out.contains("created=1"), "expected created=1 in: {out}"); } // --------------------------------------------------------------------------- // dry-run // --------------------------------------------------------------------------- #[test] fn dry_run_makes_no_changes() { let env = TestEnv::new(); env.make_theme_file("dracula", ".zshrc", "# zsh"); let out = env.dm_stdout(&["apply", "dracula", "--dry-run"]); assert!(out.contains("Dry run"), "got: {out}"); assert!( !env.target(".zshrc").exists(), "symlink should not exist after dry-run" ); } #[test] fn dry_run_shows_create_action() { let env = TestEnv::new(); env.make_theme_file("dracula", ".zshrc", ""); let out = env.dm_stdout(&["apply", "dracula", "--dry-run"]); assert!(out.contains("create"), "got: {out}"); } // --------------------------------------------------------------------------- // conflict without --force // --------------------------------------------------------------------------- #[test] fn apply_conflicts_on_unmanaged_file() { let env = TestEnv::new(); env.make_theme_file("dracula", ".zshrc", "# theme"); fs::write(env.target(".zshrc"), "original").unwrap(); let out = env.dm(&["apply", "dracula"]); assert!(!out.status.success(), "should fail on conflict"); // Original file must be untouched. let content = fs::read_to_string(env.target(".zshrc")).unwrap(); assert_eq!(content, "original"); } #[test] fn apply_force_backs_up_and_replaces() { let env = TestEnv::new(); env.make_theme_file("dracula", ".zshrc", "# theme"); fs::write(env.target(".zshrc"), "original").unwrap(); env.dm_success(&["apply", "dracula", "--force"]); assert!(is_symlink(&env.target(".zshrc"))); } // --------------------------------------------------------------------------- // switch dracula → catppuccin // --------------------------------------------------------------------------- #[test] fn switch_themes_replaces_managed_symlinks() { let env = TestEnv::new(); env.make_theme_file("dracula", ".zshrc", "# dracula"); env.dm_success(&["apply", "dracula"]); env.make_theme_file("catppuccin", ".zshrc", "# catppuccin"); env.dm_success(&["apply", "catppuccin"]); let link = fs::read_link(env.target(".zshrc")).unwrap(); assert!( link.to_string_lossy().contains("catppuccin"), "link should point to catppuccin, got: {}", link.display() ); } #[test] fn status_updates_after_theme_switch() { let env = TestEnv::new(); env.make_theme_file("dracula", ".zshrc", ""); env.dm_success(&["apply", "dracula"]); env.make_theme_file("catppuccin", ".zshrc", ""); env.dm_success(&["apply", "catppuccin"]); let out = env.dm_stdout(&["status"]); assert!(out.contains("catppuccin"), "got: {out}"); } // --------------------------------------------------------------------------- // rollback // --------------------------------------------------------------------------- #[test] fn rollback_restores_backed_up_file() { let env = TestEnv::new(); env.make_theme_file("dracula", ".zshrc", "# theme"); fs::write(env.target(".zshrc"), "original content").unwrap(); env.dm_success(&["apply", "dracula", "--force"]); assert!(is_symlink(&env.target(".zshrc"))); env.dm_success(&["rollback"]); let content = fs::read_to_string(env.target(".zshrc")).unwrap(); assert_eq!(content, "original content"); } #[test] fn rollback_removes_newly_created_symlinks() { let env = TestEnv::new(); env.make_theme_file("dracula", ".zshrc", "# theme"); env.dm_success(&["apply", "dracula"]); assert!(is_symlink(&env.target(".zshrc"))); env.dm_success(&["rollback"]); assert!(env.target(".zshrc").symlink_metadata().is_err()); } #[test] fn rollback_clears_state() { let env = TestEnv::new(); env.make_theme_file("dracula", ".zshrc", ""); env.dm_success(&["apply", "dracula"]); env.dm_success(&["rollback"]); let out = env.dm_stdout(&["status"]); assert!(out.contains("No theme"), "got: {out}"); } // --------------------------------------------------------------------------- // backup command // --------------------------------------------------------------------------- #[test] fn backup_command_copies_managed_files() { let env = TestEnv::new(); env.make_theme_file("dracula", ".zshrc", "# theme"); fs::write(env.target(".zshrc"), "original").unwrap(); env.dm_success(&["apply", "dracula", "--force"]); // Apply already backed up; now apply a second time to switch, creating backup dir // Just check the backup command runs without error when state exists. let out = env.dm(&["backup"]); assert!(out.status.success(), "backup should succeed"); }