use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use tempfile::TempDir;
#[path = "integration/test_helpers.rs"]
mod test_helpers;
fn get_binary_path() -> PathBuf {
test_helpers::get_binary_path()
}
fn ca_binary_exists() -> bool {
let binary_path = get_binary_path();
binary_path.exists()
}
fn run_ca_command(args: &[&str], cwd: &Path) -> Result<(bool, String, String), String> {
let binary_path = get_binary_path();
let output = Command::new(&binary_path)
.args(args)
.current_dir(cwd)
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output()
.map_err(|e| format!("Failed to execute ca: {e}"))?;
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
Ok((output.status.success(), stdout, stderr))
}
fn setup_test_repo() -> Result<(TempDir, String), String> {
let temp_dir = TempDir::new().map_err(|e| e.to_string())?;
let repo_path = temp_dir.path();
Command::new("git")
.args(["init"])
.current_dir(repo_path)
.output()
.map_err(|e| format!("Failed to init git: {e}"))?;
Command::new("git")
.args(["config", "user.name", "Test User"])
.current_dir(repo_path)
.output()
.map_err(|e| format!("Failed to set user.name: {e}"))?;
Command::new("git")
.args(["config", "user.email", "test@example.com"])
.current_dir(repo_path)
.output()
.map_err(|e| format!("Failed to set user.email: {e}"))?;
std::fs::write(repo_path.join("README.md"), "# Test Repo").map_err(|e| e.to_string())?;
Command::new("git")
.args(["add", "."])
.current_dir(repo_path)
.output()
.map_err(|e| format!("Failed to add files: {e}"))?;
Command::new("git")
.args(["commit", "-m", "Initial commit"])
.current_dir(repo_path)
.output()
.map_err(|e| format!("Failed to commit: {e}"))?;
let branch_output = Command::new("git")
.args(["branch", "--show-current"])
.current_dir(repo_path)
.output()
.map_err(|e| format!("Failed to get branch: {e}"))?;
let current = String::from_utf8_lossy(&branch_output.stdout)
.trim()
.to_string();
if current != "main" {
Command::new("git")
.args(["branch", "-m", ¤t, "main"])
.current_dir(repo_path)
.output()
.map_err(|e| format!("Failed to rename branch: {e}"))?;
}
let (success, _, _) = run_ca_command(&["init"], repo_path)?;
if !success {
return Err("Failed to initialize cascade".to_string());
}
Ok((temp_dir, "main".to_string()))
}
fn create_conflicting_stack(
repo_path: &Path,
stack_name: &str,
base_branch: &str,
) -> Result<(), String> {
let feature_branch = format!("feature/{}-work", stack_name);
Command::new("git")
.args(["checkout", "-b", &feature_branch])
.current_dir(repo_path)
.output()
.map_err(|e| format!("Failed to create feature branch: {e}"))?;
let (success, _, stderr) = run_ca_command(&["stacks", "create", stack_name], repo_path)?;
if !success {
return Err(format!("Failed to create stack: {stderr}"));
}
std::fs::write(repo_path.join("conflict.txt"), "Stack content").map_err(|e| e.to_string())?;
Command::new("git")
.args(["add", "."])
.current_dir(repo_path)
.output()
.map_err(|e| format!("Failed to add: {e}"))?;
Command::new("git")
.args(["commit", "-m", "Add conflict file"])
.current_dir(repo_path)
.output()
.map_err(|e| format!("Failed to commit: {e}"))?;
let (success, _, stderr) =
run_ca_command(&["push", "--allow-base-branch", "--yes"], repo_path)?;
if !success {
return Err(format!("Failed to push commit: {stderr}"));
}
Command::new("git")
.args(["checkout", base_branch])
.current_dir(repo_path)
.output()
.map_err(|e| format!("Failed to checkout base: {e}"))?;
std::fs::write(repo_path.join("conflict.txt"), "Base branch content")
.map_err(|e| e.to_string())?;
Command::new("git")
.args(["add", "."])
.current_dir(repo_path)
.output()
.map_err(|e| format!("Failed to add: {e}"))?;
Command::new("git")
.args(["commit", "-m", "Conflicting change on base"])
.current_dir(repo_path)
.output()
.map_err(|e| format!("Failed to commit: {e}"))?;
run_ca_command(&["switch", stack_name], repo_path)?;
Ok(())
}
fn has_cherry_pick_in_progress(repo_path: &Path) -> bool {
repo_path.join(".git/CHERRY_PICK_HEAD").exists()
}
fn has_sync_state(repo_path: &Path) -> bool {
repo_path.join(".git/CASCADE_SYNC_STATE").exists()
}
#[test]
fn test_sync_continue_after_manual_conflict_resolution() {
if !ca_binary_exists() {
eprintln!("Skipping test: ca binary not found");
return;
}
let (temp_dir, base_branch) = setup_test_repo().unwrap();
let repo_path = temp_dir.path();
create_conflicting_stack(repo_path, "conflict-stack", &base_branch).unwrap();
let (success, stdout, stderr) = run_ca_command(&["sync", "--force"], repo_path).unwrap();
if success && stdout.contains("Sync completed successfully") {
eprintln!("Skipping test: conflicts were auto-resolved");
return;
}
let has_conflict_message = stdout.contains("conflict")
|| stderr.contains("conflict")
|| stdout.contains("Conflict")
|| stderr.contains("Conflict");
assert!(
has_conflict_message,
"Sync should report conflicts if not auto-resolved. stdout: {}, stderr: {}",
stdout, stderr
);
if has_cherry_pick_in_progress(repo_path) {
std::fs::write(repo_path.join("conflict.txt"), "Resolved content").unwrap();
Command::new("git")
.args(["add", "conflict.txt"])
.current_dir(repo_path)
.output()
.unwrap();
let (success, stdout, stderr) = run_ca_command(&["sync", "continue"], repo_path).unwrap();
assert!(
success,
"sync continue should succeed after resolving conflicts. stdout: {}, stderr: {}",
stdout, stderr
);
assert!(
!has_cherry_pick_in_progress(repo_path),
"Should not have cherry-pick in progress after continue"
);
assert!(
!has_sync_state(repo_path),
"Should not have sync state file after successful continue"
);
}
}
#[test]
fn test_sync_abort_cleans_up_state() {
if !ca_binary_exists() {
eprintln!("Skipping test: ca binary not found");
return;
}
let (temp_dir, base_branch) = setup_test_repo().unwrap();
let repo_path = temp_dir.path();
create_conflicting_stack(repo_path, "abort-stack", &base_branch).unwrap();
let (success, stdout, stderr) = run_ca_command(&["sync", "--force"], repo_path).unwrap();
if success && stdout.contains("Sync completed successfully") {
eprintln!("Skipping test: conflicts were auto-resolved");
return;
}
let has_conflict_message = stdout.contains("conflict")
|| stderr.contains("conflict")
|| stdout.contains("Conflict")
|| stderr.contains("Conflict");
assert!(
has_conflict_message,
"Sync should report conflicts if not auto-resolved"
);
if has_cherry_pick_in_progress(repo_path) {
let (success, stdout, stderr) = run_ca_command(&["sync", "abort"], repo_path).unwrap();
assert!(
success,
"sync abort should succeed. stdout: {}, stderr: {}",
stdout, stderr
);
assert!(
!has_cherry_pick_in_progress(repo_path),
"Should not have cherry-pick in progress after abort"
);
assert!(
!has_sync_state(repo_path),
"Should not have sync state file after abort"
);
let output = Command::new("git")
.args(["branch", "--show-current"])
.current_dir(repo_path)
.output()
.unwrap();
let current_branch = String::from_utf8_lossy(&output.stdout).trim().to_string();
assert!(
!current_branch.contains("-temp-"),
"Should not be on a temp branch after abort, got: {}",
current_branch
);
}
}
#[test]
fn test_sync_continue_without_conflicts_fails() {
if !ca_binary_exists() {
eprintln!("Skipping test: ca binary not found");
return;
}
let (temp_dir, _) = setup_test_repo().unwrap();
let repo_path = temp_dir.path();
let (success, stdout, stderr) = run_ca_command(&["sync", "continue"], repo_path).unwrap();
assert!(
!success,
"sync continue should fail when there's no cherry-pick in progress. stdout: {}, stderr: {}",
stdout, stderr
);
let error_mentions_no_cherry_pick = stdout.contains("cherry-pick")
|| stderr.contains("cherry-pick")
|| stdout.contains("in-progress")
|| stderr.contains("in-progress");
assert!(
error_mentions_no_cherry_pick,
"Error should mention no cherry-pick in progress"
);
}
#[test]
fn test_sync_abort_without_conflicts_fails() {
if !ca_binary_exists() {
eprintln!("Skipping test: ca binary not found");
return;
}
let (temp_dir, _) = setup_test_repo().unwrap();
let repo_path = temp_dir.path();
let (success, stdout, stderr) = run_ca_command(&["sync", "abort"], repo_path).unwrap();
assert!(
!success,
"sync abort should fail when there's no cherry-pick in progress. stdout: {}, stderr: {}",
stdout, stderr
);
let error_mentions_no_cherry_pick = stdout.contains("cherry-pick")
|| stderr.contains("cherry-pick")
|| stdout.contains("in-progress")
|| stderr.contains("in-progress");
assert!(
error_mentions_no_cherry_pick,
"Error should mention no cherry-pick in progress"
);
}
#[test]
fn test_sync_state_persistence() {
if !ca_binary_exists() {
eprintln!("Skipping test: ca binary not found");
return;
}
let (temp_dir, base_branch) = setup_test_repo().unwrap();
let repo_path = temp_dir.path();
create_conflicting_stack(repo_path, "state-stack", &base_branch).unwrap();
let (_success, _stdout, _stderr) = run_ca_command(&["sync", "--force"], repo_path).unwrap();
if has_cherry_pick_in_progress(repo_path) {
assert!(
has_sync_state(repo_path),
"Should have sync state file when cherry-pick is in progress"
);
let state_path = repo_path.join(".git/CASCADE_SYNC_STATE");
let state_content = std::fs::read_to_string(&state_path).unwrap();
let state_json: serde_json::Value = serde_json::from_str(&state_content).unwrap();
assert!(
state_json.get("stack_id").is_some(),
"State should have stack_id"
);
assert!(
state_json.get("stack_name").is_some(),
"State should have stack_name"
);
assert!(
state_json.get("original_branch").is_some(),
"State should have original_branch"
);
assert!(
state_json.get("target_base").is_some(),
"State should have target_base"
);
assert!(
state_json.get("temp_branches").is_some(),
"State should have temp_branches"
);
}
}