use assert_cmd::Command; use predicates::prelude::*; use std::fs; use std::path::PathBuf; use tempfile::TempDir; fn create_git_repo(dir: &PathBuf) -> std::process::Output { std::process::Command::new("git") .args(&["init"]) .current_dir(dir) .output() .expect("Failed to init git repo") } fn configure_git_user(dir: &PathBuf) { std::process::Command::new("git") .args(&["config", "user.name", "Test User"]) .current_dir(dir) .output() .expect("Failed to configure git user name"); std::process::Command::new("git") .args(&["config", "user.email", "test@example.com"]) .current_dir(dir) .output() .expect("Failed to configure git user email"); } fn create_test_file(dir: &PathBuf, name: &str, content: &str) { let file_path = dir.join(name); fs::write(&file_path, content).expect("Failed to create test file"); } fn stage_file(dir: &PathBuf, name: &str) { std::process::Command::new("git") .args(&["add", name]) .current_dir(dir) .output() .expect("Failed to stage file"); } fn create_commit(dir: &PathBuf, message: &str) { std::process::Command::new("git") .args(&["commit", "-m", message]) .current_dir(dir) .output() .expect("Failed to create commit"); } mod cli_basic { use super::*; #[test] fn test_help() { let mut cmd = Command::cargo_bin("quicommit").unwrap(); cmd.arg("--help"); cmd.assert() .success() .stdout(predicate::str::contains("QuiCommit")) .stdout(predicate::str::contains("AI-powered Git assistant")); } #[test] fn test_version() { let mut cmd = Command::cargo_bin("quicommit").unwrap(); cmd.arg("--version"); cmd.assert() .success() .stdout(predicate::str::contains("quicommit")); } #[test] fn test_no_args_shows_help() { let mut cmd = Command::cargo_bin("quicommit").unwrap(); cmd.assert() .failure() .stderr(predicate::str::contains("Usage:")); } #[test] fn test_verbose_flag() { let temp_dir = TempDir::new().unwrap(); let repo_path = temp_dir.path().to_path_buf(); let config_path = repo_path.join("config.toml"); create_git_repo(&repo_path); configure_git_user(&repo_path); let mut cmd = Command::cargo_bin("quicommit").unwrap(); cmd.args(&["-vv", "init", "--yes", "--config", config_path.to_str().unwrap()]) .current_dir(&repo_path); cmd.assert().success(); } } mod init_command { use super::*; #[test] fn test_init_quick() { let temp_dir = TempDir::new().unwrap(); let config_path = temp_dir.path().join("config.toml"); let mut cmd = Command::cargo_bin("quicommit").unwrap(); cmd.args(&["init", "--yes", "--config", config_path.to_str().unwrap()]); cmd.assert() .success() .stdout(predicate::str::contains("initialized successfully")); } #[test] fn test_init_creates_config_file() { let temp_dir = TempDir::new().unwrap(); let config_path = temp_dir.path().join("config.toml"); let mut cmd = Command::cargo_bin("quicommit").unwrap(); cmd.args(&["init", "--yes", "--config", config_path.to_str().unwrap()]); cmd.assert().success(); assert!(config_path.exists(), "Config file should be created"); } #[test] fn test_init_in_git_repo() { let temp_dir = TempDir::new().unwrap(); let repo_path = temp_dir.path().to_path_buf(); create_git_repo(&repo_path); configure_git_user(&repo_path); let config_path = repo_path.join("test_config.toml"); let mut cmd = Command::cargo_bin("quicommit").unwrap(); cmd.args(&["init", "--yes", "--config", config_path.to_str().unwrap()]) .current_dir(&repo_path); cmd.assert().success(); } #[test] fn test_init_reset() { let temp_dir = TempDir::new().unwrap(); let config_path = temp_dir.path().join("config.toml"); let mut cmd = Command::cargo_bin("quicommit").unwrap(); cmd.args(&["init", "--yes", "--config", config_path.to_str().unwrap()]); cmd.assert().success(); let mut cmd = Command::cargo_bin("quicommit").unwrap(); cmd.args(&["init", "--yes", "--reset", "--config", config_path.to_str().unwrap()]); cmd.assert() .success() .stdout(predicate::str::contains("initialized successfully")); } } mod profile_command { use super::*; #[test] fn test_profile_list_empty() { let temp_dir = TempDir::new().unwrap(); let config_path = temp_dir.path().join("config.toml"); let mut cmd = Command::cargo_bin("quicommit").unwrap(); cmd.args(&["profile", "list", "--config", config_path.to_str().unwrap()]); cmd.assert() .success() .stdout(predicate::str::contains("No profiles")); } #[test] fn test_profile_list_with_profile() { let temp_dir = TempDir::new().unwrap(); let config_path = temp_dir.path().join("config.toml"); let mut cmd = Command::cargo_bin("quicommit").unwrap(); cmd.args(&["init", "--yes", "--config", config_path.to_str().unwrap()]); cmd.assert().success(); let mut cmd = Command::cargo_bin("quicommit").unwrap(); cmd.args(&["profile", "list", "--config", config_path.to_str().unwrap()]); cmd.assert() .success() .stdout(predicate::str::contains("default")); } } mod config_command { use super::*; #[test] fn test_config_show() { let temp_dir = TempDir::new().unwrap(); let config_path = temp_dir.path().join("config.toml"); let mut cmd = Command::cargo_bin("quicommit").unwrap(); cmd.args(&["init", "--yes", "--config", config_path.to_str().unwrap()]); cmd.assert().success(); let mut cmd = Command::cargo_bin("quicommit").unwrap(); cmd.args(&["config", "show", "--config", config_path.to_str().unwrap()]); cmd.assert() .success() .stdout(predicate::str::contains("Configuration")); } #[test] fn test_config_path() { let temp_dir = TempDir::new().unwrap(); let config_path = temp_dir.path().join("config.toml"); let mut cmd = Command::cargo_bin("quicommit").unwrap(); cmd.args(&["init", "--yes", "--config", config_path.to_str().unwrap()]); cmd.assert().success(); let mut cmd = Command::cargo_bin("quicommit").unwrap(); cmd.args(&["config", "path", "--config", config_path.to_str().unwrap()]); cmd.assert() .success() .stdout(predicate::str::contains("config.toml")); } } mod commit_command { use super::*; #[test] fn test_commit_no_repo() { let temp_dir = TempDir::new().unwrap(); let config_path = temp_dir.path().join("config.toml"); let mut cmd = Command::cargo_bin("quicommit").unwrap(); cmd.args(&["init", "--yes", "--config", config_path.to_str().unwrap()]); cmd.assert().success(); let mut cmd = Command::cargo_bin("quicommit").unwrap(); cmd.args(&["commit", "--dry-run", "--yes", "--config", config_path.to_str().unwrap()]) .current_dir(temp_dir.path()); cmd.assert() .failure() .stderr(predicate::str::contains("git").or(predicate::str::contains("repository"))); } #[test] fn test_commit_no_changes() { let temp_dir = TempDir::new().unwrap(); let repo_path = temp_dir.path().to_path_buf(); create_git_repo(&repo_path); configure_git_user(&repo_path); let config_path = repo_path.join("config.toml"); let mut cmd = Command::cargo_bin("quicommit").unwrap(); cmd.args(&["init", "--yes", "--config", config_path.to_str().unwrap()]) .current_dir(&repo_path); cmd.assert().success(); let mut cmd = Command::cargo_bin("quicommit").unwrap(); cmd.args(&["commit", "--manual", "-m", "test: empty", "--dry-run", "--yes", "--config", config_path.to_str().unwrap()]) .current_dir(&repo_path); cmd.assert() .success() .stdout(predicate::str::contains("Dry run")); } #[test] fn test_commit_with_staged_changes() { let temp_dir = TempDir::new().unwrap(); let repo_path = temp_dir.path().to_path_buf(); create_git_repo(&repo_path); configure_git_user(&repo_path); create_test_file(&repo_path, "test.txt", "Hello, World!"); stage_file(&repo_path, "test.txt"); let config_path = repo_path.join("config.toml"); let mut cmd = Command::cargo_bin("quicommit").unwrap(); cmd.args(&["init", "--yes", "--config", config_path.to_str().unwrap()]) .current_dir(&repo_path); cmd.assert().success(); let mut cmd = Command::cargo_bin("quicommit").unwrap(); cmd.args(&["commit", "--manual", "-m", "test: add test file", "--dry-run", "--yes", "--config", config_path.to_str().unwrap()]) .current_dir(&repo_path); cmd.assert() .success() .stdout(predicate::str::contains("Dry run")); } #[test] fn test_commit_date_mode() { let temp_dir = TempDir::new().unwrap(); let repo_path = temp_dir.path().to_path_buf(); create_git_repo(&repo_path); configure_git_user(&repo_path); create_test_file(&repo_path, "daily.txt", "Daily update"); stage_file(&repo_path, "daily.txt"); let config_path = repo_path.join("config.toml"); let mut cmd = Command::cargo_bin("quicommit").unwrap(); cmd.args(&["init", "--yes", "--config", config_path.to_str().unwrap()]) .current_dir(&repo_path); cmd.assert().success(); let mut cmd = Command::cargo_bin("quicommit").unwrap(); cmd.args(&["commit", "--date", "--dry-run", "--yes", "--config", config_path.to_str().unwrap()]) .current_dir(&repo_path); cmd.assert() .success() .stdout(predicate::str::contains("Dry run")); } } mod tag_command { use super::*; #[test] fn test_tag_no_repo() { let temp_dir = TempDir::new().unwrap(); let config_path = temp_dir.path().join("config.toml"); let mut cmd = Command::cargo_bin("quicommit").unwrap(); cmd.args(&["init", "--yes", "--config", config_path.to_str().unwrap()]); cmd.assert().success(); let mut cmd = Command::cargo_bin("quicommit").unwrap(); cmd.args(&["tag", "--dry-run", "--yes", "--config", config_path.to_str().unwrap()]) .current_dir(temp_dir.path()); cmd.assert() .failure() .stderr(predicate::str::contains("git").or(predicate::str::contains("repository"))); } #[test] fn test_tag_list_empty() { let temp_dir = TempDir::new().unwrap(); let repo_path = temp_dir.path().to_path_buf(); create_git_repo(&repo_path); configure_git_user(&repo_path); create_test_file(&repo_path, "test.txt", "content"); stage_file(&repo_path, "test.txt"); create_commit(&repo_path, "feat: initial commit"); let config_path = repo_path.join("config.toml"); let mut cmd = Command::cargo_bin("quicommit").unwrap(); cmd.args(&["init", "--yes", "--config", config_path.to_str().unwrap()]) .current_dir(&repo_path); cmd.assert().success(); let mut cmd = Command::cargo_bin("quicommit").unwrap(); cmd.args(&["tag", "--name", "v0.1.0", "--dry-run", "--yes", "--config", config_path.to_str().unwrap()]) .current_dir(&repo_path); cmd.assert() .success() .stdout(predicate::str::contains("v0.1.0")); } } mod changelog_command { use super::*; #[test] fn test_changelog_init() { let temp_dir = TempDir::new().unwrap(); let repo_path = temp_dir.path().to_path_buf(); create_git_repo(&repo_path); configure_git_user(&repo_path); let config_path = repo_path.join("config.toml"); let changelog_path = repo_path.join("CHANGELOG.md"); let mut cmd = Command::cargo_bin("quicommit").unwrap(); cmd.args(&["init", "--yes", "--config", config_path.to_str().unwrap()]) .current_dir(&repo_path); cmd.assert().success(); let mut cmd = Command::cargo_bin("quicommit").unwrap(); cmd.args(&["changelog", "--init", "--output", changelog_path.to_str().unwrap(), "--config", config_path.to_str().unwrap()]) .current_dir(&repo_path); cmd.assert().success(); assert!(changelog_path.exists(), "Changelog file should be created"); } #[test] fn test_changelog_dry_run() { let temp_dir = TempDir::new().unwrap(); let repo_path = temp_dir.path().to_path_buf(); create_git_repo(&repo_path); configure_git_user(&repo_path); create_test_file(&repo_path, "test.txt", "content"); stage_file(&repo_path, "test.txt"); create_commit(&repo_path, "feat: add feature"); let config_path = repo_path.join("config.toml"); let mut cmd = Command::cargo_bin("quicommit").unwrap(); cmd.args(&["init", "--yes", "--config", config_path.to_str().unwrap()]) .current_dir(&repo_path); cmd.assert().success(); let mut cmd = Command::cargo_bin("quicommit").unwrap(); cmd.args(&["changelog", "--dry-run", "--yes", "--config", config_path.to_str().unwrap()]) .current_dir(&repo_path); cmd.assert() .success(); } } mod cross_platform { use super::*; #[test] fn test_path_handling_windows_style() { let temp_dir = TempDir::new().unwrap(); let config_path = temp_dir.path().join("subdir").join("config.toml"); let mut cmd = Command::cargo_bin("quicommit").unwrap(); cmd.args(&["init", "--yes", "--config", config_path.to_str().unwrap()]); cmd.assert().success(); assert!(config_path.exists()); } #[test] fn test_config_with_spaces_in_path() { let temp_dir = TempDir::new().unwrap(); let space_dir = temp_dir.path().join("path with spaces"); fs::create_dir_all(&space_dir).unwrap(); let config_path = space_dir.join("config.toml"); let mut cmd = Command::cargo_bin("quicommit").unwrap(); cmd.args(&["init", "--yes", "--config", config_path.to_str().unwrap()]); cmd.assert().success(); assert!(config_path.exists()); } #[test] fn test_config_with_unicode_path() { let temp_dir = TempDir::new().unwrap(); let unicode_dir = temp_dir.path().join("路径测试"); fs::create_dir_all(&unicode_dir).unwrap(); let config_path = unicode_dir.join("config.toml"); let mut cmd = Command::cargo_bin("quicommit").unwrap(); cmd.args(&["init", "--yes", "--config", config_path.to_str().unwrap()]); cmd.assert().success(); assert!(config_path.exists()); } } mod git_operations { use super::*; #[test] fn test_git_repo_detection() { let temp_dir = TempDir::new().unwrap(); let repo_path = temp_dir.path().to_path_buf(); create_git_repo(&repo_path); configure_git_user(&repo_path); let git_dir = repo_path.join(".git"); assert!(git_dir.exists(), ".git directory should exist"); } #[test] fn test_git_status_clean() { let temp_dir = TempDir::new().unwrap(); let repo_path = temp_dir.path().to_path_buf(); create_git_repo(&repo_path); configure_git_user(&repo_path); let output = std::process::Command::new("git") .args(&["status", "--porcelain"]) .current_dir(&repo_path) .output() .expect("Failed to run git status"); assert!(output.status.success()); assert!(String::from_utf8_lossy(&output.stdout).is_empty()); } #[test] fn test_git_commit_creation() { let temp_dir = TempDir::new().unwrap(); let repo_path = temp_dir.path().to_path_buf(); create_git_repo(&repo_path); configure_git_user(&repo_path); create_test_file(&repo_path, "test.txt", "content"); stage_file(&repo_path, "test.txt"); create_commit(&repo_path, "feat: initial commit"); let output = std::process::Command::new("git") .args(&["log", "--oneline"]) .current_dir(&repo_path) .output() .expect("Failed to run git log"); assert!(output.status.success()); let log = String::from_utf8_lossy(&output.stdout); assert!(log.contains("initial commit")); } } mod validators { use super::*; #[test] fn test_commit_message_validation() { let temp_dir = TempDir::new().unwrap(); let repo_path = temp_dir.path().to_path_buf(); create_git_repo(&repo_path); configure_git_user(&repo_path); create_test_file(&repo_path, "test.txt", "content"); stage_file(&repo_path, "test.txt"); let config_path = repo_path.join("config.toml"); let mut cmd = Command::cargo_bin("quicommit").unwrap(); cmd.args(&["init", "--yes", "--config", config_path.to_str().unwrap()]) .current_dir(&repo_path); cmd.assert().success(); let mut cmd = Command::cargo_bin("quicommit").unwrap(); cmd.args(&["commit", "--manual", "-m", "invalid commit message without type", "--dry-run", "--yes", "--config", config_path.to_str().unwrap()]) .current_dir(&repo_path); cmd.assert() .failure() .stderr(predicate::str::contains("Invalid").or(predicate::str::contains("format"))); } #[test] fn test_valid_conventional_commit() { let temp_dir = TempDir::new().unwrap(); let repo_path = temp_dir.path().to_path_buf(); create_git_repo(&repo_path); configure_git_user(&repo_path); create_test_file(&repo_path, "test.txt", "content"); stage_file(&repo_path, "test.txt"); let config_path = repo_path.join("config.toml"); let mut cmd = Command::cargo_bin("quicommit").unwrap(); cmd.args(&["init", "--yes", "--config", config_path.to_str().unwrap()]) .current_dir(&repo_path); cmd.assert().success(); let mut cmd = Command::cargo_bin("quicommit").unwrap(); cmd.args(&["commit", "--manual", "-m", "feat: add new feature", "--dry-run", "--yes", "--config", config_path.to_str().unwrap()]) .current_dir(&repo_path); cmd.assert() .success() .stdout(predicate::str::contains("Dry run")); } } mod subcommands { use super::*; #[test] fn test_commit_alias() { let temp_dir = TempDir::new().unwrap(); let repo_path = temp_dir.path().to_path_buf(); create_git_repo(&repo_path); configure_git_user(&repo_path); create_test_file(&repo_path, "test.txt", "content"); stage_file(&repo_path, "test.txt"); let config_path = repo_path.join("config.toml"); let mut cmd = Command::cargo_bin("quicommit").unwrap(); cmd.args(&["init", "--yes", "--config", config_path.to_str().unwrap()]) .current_dir(&repo_path); cmd.assert().success(); let mut cmd = Command::cargo_bin("quicommit").unwrap(); cmd.args(&["c", "--manual", "-m", "fix: test", "--dry-run", "--yes", "--config", config_path.to_str().unwrap()]) .current_dir(&repo_path); cmd.assert() .success() .stdout(predicate::str::contains("Dry run")); } #[test] fn test_init_alias() { let temp_dir = TempDir::new().unwrap(); let config_path = temp_dir.path().join("config.toml"); let mut cmd = Command::cargo_bin("quicommit").unwrap(); cmd.args(&["i", "--yes", "--config", config_path.to_str().unwrap()]); cmd.assert() .success() .stdout(predicate::str::contains("initialized successfully")); } #[test] fn test_profile_alias() { let temp_dir = TempDir::new().unwrap(); let config_path = temp_dir.path().join("config.toml"); let mut cmd = Command::cargo_bin("quicommit").unwrap(); cmd.args(&["init", "--yes", "--config", config_path.to_str().unwrap()]); cmd.assert().success(); let mut cmd = Command::cargo_bin("quicommit").unwrap(); cmd.args(&["p", "list", "--config", config_path.to_str().unwrap()]); cmd.assert() .success() .stdout(predicate::str::contains("default")); } }