arc/tests/stash.rs
hanna 2e0952f9fb
feat: add colored output with distinct symbols using the colored crate
Add a centralized ui module with Arc's visual identity: colored commit
IDs (magenta), bookmarks (cyan), tags (yellow), status symbols, and
diff highlighting. Update all command output and tests accordingly.
2026-02-09 03:51:59 +00:00

390 lines
11 KiB
Rust

use std::process::Command;
use tempfile::TempDir;
fn arc_cmd() -> Command {
let mut cmd = Command::new(env!("CARGO_BIN_EXE_arc"));
cmd.env("NO_COLOR", "1");
cmd
}
fn init_repo() -> TempDir {
let dir = TempDir::new().unwrap();
arc_cmd()
.arg("init")
.current_dir(dir.path())
.output()
.expect("failed to init");
dir
}
fn commit_file(dir: &TempDir, name: &str, content: &str, msg: &str) {
std::fs::write(dir.path().join(name), content).unwrap();
let output = arc_cmd()
.args(["commit", msg])
.current_dir(dir.path())
.output()
.expect("failed to commit");
assert!(
output.status.success(),
"commit failed: {}",
String::from_utf8_lossy(&output.stderr)
);
}
fn run_ok(dir: &TempDir, args: &[&str]) -> String {
let output = arc_cmd()
.args(args)
.current_dir(dir.path())
.output()
.expect("failed to run");
assert!(
output.status.success(),
"command {:?} failed: {}",
args,
String::from_utf8_lossy(&output.stderr)
);
String::from_utf8_lossy(&output.stdout).trim().to_string()
}
fn run_fail(dir: &TempDir, args: &[&str]) -> String {
let output = arc_cmd()
.args(args)
.current_dir(dir.path())
.output()
.expect("failed to run");
assert!(
!output.status.success(),
"command {:?} should have failed but succeeded: {}",
args,
String::from_utf8_lossy(&output.stdout)
);
String::from_utf8_lossy(&output.stderr).trim().to_string()
}
#[test]
fn stash_create_creates_stash() {
let dir = init_repo();
let stdout = run_ok(&dir, &["stash", "create", "wip"]);
assert!(stdout.contains("stash 'wip' created"));
let stash_path = dir.path().join(".arc/stashes/named/wip.yml");
assert!(stash_path.exists());
}
#[test]
fn stash_create_sets_active() {
let dir = init_repo();
run_ok(&dir, &["stash", "create", "wip"]);
let state = std::fs::read_to_string(dir.path().join(".arc/stashes/state.yml")).unwrap();
assert!(state.contains("wip"));
}
#[test]
fn stash_create_fails_if_exists() {
let dir = init_repo();
run_ok(&dir, &["stash", "create", "wip"]);
let stderr = run_fail(&dir, &["stash", "create", "wip"]);
assert!(stderr.contains("stash already exists"));
}
#[test]
fn stash_create_fails_invalid_name() {
let dir = init_repo();
let stderr = run_fail(&dir, &["stash", "create", "../escape"]);
assert!(stderr.contains("invalid ref name"));
}
#[test]
fn stash_use_switches_active() {
let dir = init_repo();
run_ok(&dir, &["stash", "create", "first"]);
run_ok(&dir, &["stash", "create", "second"]);
run_ok(&dir, &["stash", "use", "first"]);
let state = std::fs::read_to_string(dir.path().join(".arc/stashes/state.yml")).unwrap();
assert!(state.contains("first"));
}
#[test]
fn stash_use_fails_nonexistent() {
let dir = init_repo();
let stderr = run_fail(&dir, &["stash", "use", "nope"]);
assert!(stderr.contains("stash not found"));
}
#[test]
fn stash_push_saves_and_resets() {
let dir = init_repo();
commit_file(&dir, "a.txt", "hello\n", "initial");
run_ok(&dir, &["stash", "create", "wip"]);
std::fs::write(dir.path().join("a.txt"), "modified\n").unwrap();
let stdout = run_ok(&dir, &["stash", "push"]);
assert!(stdout.contains("pushed"));
assert!(stdout.contains("change(s)"));
let content = std::fs::read_to_string(dir.path().join("a.txt")).unwrap();
assert_eq!(content, "hello\n");
}
#[test]
fn stash_push_handles_added_files() {
let dir = init_repo();
commit_file(&dir, "a.txt", "hello\n", "initial");
run_ok(&dir, &["stash", "create", "wip"]);
std::fs::write(dir.path().join("new.txt"), "added\n").unwrap();
run_ok(&dir, &["stash", "push"]);
assert!(!dir.path().join("new.txt").exists());
}
#[test]
fn stash_push_handles_deleted_files() {
let dir = init_repo();
commit_file(&dir, "a.txt", "hello\n", "initial");
run_ok(&dir, &["stash", "create", "wip"]);
std::fs::remove_file(dir.path().join("a.txt")).unwrap();
run_ok(&dir, &["stash", "push"]);
let content = std::fs::read_to_string(dir.path().join("a.txt")).unwrap();
assert_eq!(content, "hello\n");
}
#[test]
fn stash_push_fails_no_active() {
let dir = init_repo();
commit_file(&dir, "a.txt", "hello\n", "initial");
std::fs::write(dir.path().join("a.txt"), "changed\n").unwrap();
let stderr = run_fail(&dir, &["stash", "push"]);
assert!(stderr.contains("no active stash"));
}
#[test]
fn stash_push_fails_clean_worktree() {
let dir = init_repo();
commit_file(&dir, "a.txt", "hello\n", "initial");
run_ok(&dir, &["stash", "create", "wip"]);
let stderr = run_fail(&dir, &["stash", "push"]);
assert!(stderr.contains("nothing to stash"));
}
#[test]
fn stash_pop_restores_changes() {
let dir = init_repo();
commit_file(&dir, "a.txt", "hello\n", "initial");
run_ok(&dir, &["stash", "create", "wip"]);
std::fs::write(dir.path().join("a.txt"), "modified\n").unwrap();
run_ok(&dir, &["stash", "push"]);
let stdout = run_ok(&dir, &["stash", "pop"]);
assert!(stdout.contains("popped"));
let content = std::fs::read_to_string(dir.path().join("a.txt")).unwrap();
assert_eq!(content, "modified\n");
}
#[test]
fn stash_pop_restores_added_files() {
let dir = init_repo();
commit_file(&dir, "a.txt", "hello\n", "initial");
run_ok(&dir, &["stash", "create", "wip"]);
std::fs::write(dir.path().join("new.txt"), "added\n").unwrap();
run_ok(&dir, &["stash", "push"]);
assert!(!dir.path().join("new.txt").exists());
run_ok(&dir, &["stash", "pop"]);
let content = std::fs::read_to_string(dir.path().join("new.txt")).unwrap();
assert_eq!(content, "added\n");
}
#[test]
fn stash_pop_restores_deleted_files() {
let dir = init_repo();
commit_file(&dir, "a.txt", "hello\n", "initial");
run_ok(&dir, &["stash", "create", "wip"]);
std::fs::remove_file(dir.path().join("a.txt")).unwrap();
run_ok(&dir, &["stash", "push"]);
run_ok(&dir, &["stash", "pop"]);
assert!(!dir.path().join("a.txt").exists());
}
#[test]
fn stash_pop_fails_no_active() {
let dir = init_repo();
let stderr = run_fail(&dir, &["stash", "pop"]);
assert!(stderr.contains("no active stash"));
}
#[test]
fn stash_pop_fails_empty_stash() {
let dir = init_repo();
run_ok(&dir, &["stash", "create", "wip"]);
let stderr = run_fail(&dir, &["stash", "pop"]);
assert!(stderr.contains("stash is empty"));
}
#[test]
fn stash_pop_fails_dirty_worktree() {
let dir = init_repo();
commit_file(&dir, "a.txt", "hello\n", "initial");
run_ok(&dir, &["stash", "create", "wip"]);
std::fs::write(dir.path().join("a.txt"), "modified\n").unwrap();
run_ok(&dir, &["stash", "push"]);
std::fs::write(dir.path().join("a.txt"), "dirty\n").unwrap();
let stderr = run_fail(&dir, &["stash", "pop"]);
assert!(stderr.contains("uncommitted changes"));
}
#[test]
fn stash_pop_fails_base_mismatch() {
let dir = init_repo();
commit_file(&dir, "a.txt", "hello\n", "initial");
run_ok(&dir, &["stash", "create", "wip"]);
std::fs::write(dir.path().join("a.txt"), "modified\n").unwrap();
run_ok(&dir, &["stash", "push"]);
commit_file(&dir, "b.txt", "new file\n", "second commit");
let stderr = run_fail(&dir, &["stash", "pop"]);
assert!(stderr.contains("stash base does not match"));
}
#[test]
fn stash_rm_removes_stash() {
let dir = init_repo();
run_ok(&dir, &["stash", "create", "wip"]);
let stdout = run_ok(&dir, &["stash", "rm", "wip"]);
assert!(stdout.contains("stash 'wip' removed"));
let stash_path = dir.path().join(".arc/stashes/named/wip.yml");
assert!(!stash_path.exists());
}
#[test]
fn stash_rm_clears_active_if_removed() {
let dir = init_repo();
run_ok(&dir, &["stash", "create", "wip"]);
run_ok(&dir, &["stash", "rm", "wip"]);
let state = std::fs::read_to_string(dir.path().join(".arc/stashes/state.yml")).unwrap();
assert!(state.contains("null") || !state.contains("wip"));
}
#[test]
fn stash_rm_fails_nonexistent() {
let dir = init_repo();
let stderr = run_fail(&dir, &["stash", "rm", "nope"]);
assert!(stderr.contains("stash not found"));
}
#[test]
fn stash_list_shows_stashes() {
let dir = init_repo();
run_ok(&dir, &["stash", "create", "alpha"]);
run_ok(&dir, &["stash", "create", "beta"]);
let stdout = run_ok(&dir, &["stash", "list"]);
assert!(stdout.contains("alpha"));
assert!(stdout.contains("beta"));
}
#[test]
fn stash_list_marks_active() {
let dir = init_repo();
run_ok(&dir, &["stash", "create", "alpha"]);
run_ok(&dir, &["stash", "create", "beta"]);
let stdout = run_ok(&dir, &["stash", "list"]);
assert!(stdout.contains("★ beta"));
let has_inactive_alpha = stdout.lines().any(|l| l.trim_start().starts_with("alpha"));
assert!(has_inactive_alpha);
assert!(!stdout.contains("★ alpha"));
}
#[test]
fn stash_list_sorted() {
let dir = init_repo();
run_ok(&dir, &["stash", "create", "zebra"]);
run_ok(&dir, &["stash", "create", "alpha"]);
let stdout = run_ok(&dir, &["stash", "list"]);
let alpha_pos = stdout.find("alpha").unwrap();
let zebra_pos = stdout.find("zebra").unwrap();
assert!(alpha_pos < zebra_pos);
}
#[test]
fn stash_list_shows_entry_count() {
let dir = init_repo();
commit_file(&dir, "a.txt", "hello\n", "initial");
run_ok(&dir, &["stash", "create", "wip"]);
let stdout = run_ok(&dir, &["stash", "list"]);
assert!(stdout.contains("0 entries"));
std::fs::write(dir.path().join("a.txt"), "modified\n").unwrap();
run_ok(&dir, &["stash", "push"]);
let stdout = run_ok(&dir, &["stash", "list"]);
assert!(stdout.contains("1 entries"));
}
#[test]
fn stash_list_empty() {
let dir = init_repo();
let stdout = run_ok(&dir, &["stash", "list"]);
assert!(stdout.contains("no stashes"));
}
#[test]
fn stash_push_pop_multiple() {
let dir = init_repo();
commit_file(&dir, "a.txt", "v1\n", "initial");
run_ok(&dir, &["stash", "create", "wip"]);
std::fs::write(dir.path().join("a.txt"), "v2\n").unwrap();
run_ok(&dir, &["stash", "push"]);
std::fs::write(dir.path().join("a.txt"), "v3\n").unwrap();
run_ok(&dir, &["stash", "push"]);
run_ok(&dir, &["stash", "pop"]);
let content = std::fs::read_to_string(dir.path().join("a.txt")).unwrap();
assert_eq!(content, "v3\n");
run_ok(&dir, &["reset"]);
run_ok(&dir, &["stash", "pop"]);
let content = std::fs::read_to_string(dir.path().join("a.txt")).unwrap();
assert_eq!(content, "v2\n");
}
#[test]
fn stash_push_on_unborn() {
let dir = init_repo();
run_ok(&dir, &["stash", "create", "wip"]);
std::fs::write(dir.path().join("new.txt"), "content\n").unwrap();
run_ok(&dir, &["stash", "push"]);
assert!(!dir.path().join("new.txt").exists());
run_ok(&dir, &["stash", "pop"]);
let content = std::fs::read_to_string(dir.path().join("new.txt")).unwrap();
assert_eq!(content, "content\n");
}