use anyhow::Result; use assert_cmd::{cargo::cargo_bin, Command}; use rexpect::session::{spawn_command, PtySession}; use shackle_shell::user_info::{get_user_groups, get_username}; use std::path::{Path, PathBuf}; use tempfile::TempDir; const REPO_NAME: &str = "my-repository"; const REPO_NAME_2: &str = "my-other-repository"; struct TestContext { p: PtySession, workdir: TempDir, } impl TestContext { fn personal_repo_dir(&self, repo_name: &str) -> PathBuf { let username = get_username().unwrap(); self.workdir .as_ref() .join("git") .join(username) .join(&format!("{}.git", repo_name)) } fn group_repo_dir(&self, group: &str, repo_name: &str) -> PathBuf { self.workdir .as_ref() .join("git") .join(group) .join(&format!("{}.git", repo_name)) } fn expect_prompt(&mut self) -> Result<()> { self.p.exp_string("> ")?; Ok(()) } fn expect_successful_init_message(&mut self, repo_path: &str) -> Result<()> { self.p .exp_string(&format!("Successfully created \"{repo_path}\""))?; self.expect_prompt() } } fn personal_repo_path(repo_name: &str) -> String { let username = get_username().unwrap(); format!("git/{username}/{repo_name}.git") } fn arbitrary_user_group() -> String { get_user_groups().into_iter().next().unwrap() } fn group_repo_path(group: &str, repo_name: &str) -> String { format!("git/{group}/{repo_name}.git") } fn spawn_interactive_process() -> Result { let workdir = tempfile::tempdir()?; let path = cargo_bin(env!("CARGO_PKG_NAME")); let mut command = std::process::Command::new(&path); command.current_dir(&workdir); let p = spawn_command(command, Some(3000))?; let mut c = TestContext { p, workdir }; c.expect_prompt()?; Ok(c) } fn run_batch_command(batch_command: &str) -> Result { let workdir = tempfile::tempdir()?; let path = cargo_bin(env!("CARGO_PKG_NAME")); let mut command = std::process::Command::new(&path); command.current_dir(&workdir); command.args(["-c", batch_command]); let p = spawn_command(command, Some(3000))?; Ok(TestContext { p, workdir }) } #[test] fn shows_a_prompt() -> Result<()> { spawn_interactive_process()?; Ok(()) } #[test] fn does_nothing_after_receiving_whitespace_input() -> Result<()> { let mut c = spawn_interactive_process()?; c.p.send_line("")?; c.expect_prompt()?; c.p.send_line(" ")?; c.expect_prompt()?; Ok(()) } #[test] fn quits_when_eof_is_sent() -> Result<()> { let mut c = spawn_interactive_process()?; c.p.send_control('d')?; c.p.exp_eof()?; Ok(()) } #[test] fn quits_when_exit_command_is_sent() -> Result<()> { let mut c = spawn_interactive_process()?; c.p.send_line("exit")?; c.p.exp_eof()?; Ok(()) } #[test] fn reports_error_with_unsupported_shell_commands() -> Result<()> { let mut c = spawn_interactive_process()?; c.p.send_line("ls")?; c.p.exp_string("error: unrecognized subcommand 'ls'")?; c.expect_prompt()?; Ok(()) } #[test] fn reports_error_with_nonsense_input() -> Result<()> { let mut c = spawn_interactive_process()?; c.p.send_line(" asd fg ")?; c.p.exp_string("error: unrecognized subcommand 'asd'")?; c.expect_prompt()?; Ok(()) } fn verify_repo_exists(repo_dir: &Path) { Command::new("git") .arg("rev-list") .arg("--all") .current_dir(repo_dir) .assert() .success() .stdout(""); } fn verify_repo_does_not_exist(repo_dir: &Path) { assert!(!repo_dir.exists()); } fn verify_current_branch(repo_dir: &Path, expected_ref: &str) { Command::new("git") .arg("symbolic-ref") .arg("HEAD") .current_dir(repo_dir) .assert() .success() .stdout(format!("{expected_ref}\n")); } fn verify_repo_config_value(repo_dir: &Path, config_key: &str, config_value: Option<&str>) { let assert = Command::new("git") .args(["config", "--local", config_key]) .current_dir(repo_dir) .assert(); match config_value { Some(value) => { assert.success().stdout(format!("{value}\n")); } None => { assert.failure().code(1); } } } #[test] fn can_init_a_new_git_repo() -> Result<()> { let mut c = spawn_interactive_process()?; c.p.send_line(&format!("init {}", REPO_NAME))?; c.expect_successful_init_message(&personal_repo_path(REPO_NAME))?; let repo_dir = c.personal_repo_dir(REPO_NAME); verify_repo_exists(&repo_dir); verify_current_branch(&repo_dir, "refs/heads/main"); verify_repo_config_value(&repo_dir, "core.sharedrepository", None); Ok(()) } #[test] fn can_init_a_new_shared_git_repo() -> Result<()> { let mut c = spawn_interactive_process()?; let group = arbitrary_user_group(); c.p.send_line(&format!("init --group {} {}", group, REPO_NAME))?; c.expect_successful_init_message(&group_repo_path(&group, REPO_NAME))?; let repo_dir = c.group_repo_dir(&group, REPO_NAME); verify_repo_exists(&repo_dir); verify_repo_config_value(&repo_dir, "core.sharedrepository", Some("1")); Ok(()) } #[test] fn does_not_init_shared_repo_if_the_user_isnt_in_the_group() -> Result<()> { let mut c = spawn_interactive_process()?; let group = "not-a-real-group"; c.p.send_line(&format!("init --group {} {}", group, REPO_NAME))?; c.p.exp_string("Unknown group")?; Ok(()) } #[test] fn runs_a_single_command_and_exit_with_cli_flag() -> Result<()> { let mut c = run_batch_command(&format!("init {}", REPO_NAME))?; c.p.exp_string(&format!( "Successfully created \"{}\"", personal_repo_path(REPO_NAME) ))?; c.p.exp_eof()?; Ok(()) } #[test] fn allows_quotes_arguments() -> Result<()> { let mut c = spawn_interactive_process()?; c.p.send_line(&format!("\"init\" '{REPO_NAME}'"))?; c.expect_successful_init_message(&personal_repo_path(REPO_NAME))?; Ok(()) } #[test] fn errors_with_an_open_double_quote() -> Result<()> { let mut c = spawn_interactive_process()?; c.p.send_line("\"init 'another-new-repo'")?; c.p.exp_string("Incomplete input")?; Ok(()) } #[test] fn errors_with_an_open_single_quote() -> Result<()> { let mut c = spawn_interactive_process()?; c.p.send_line("'init 'another-new-repo'")?; c.p.exp_string("Incomplete input")?; Ok(()) } #[test] fn allows_single_quotes_and_spaces_inside_double_quotes() -> Result<()> { let repo_name = "shukkie's new repo"; let mut c = spawn_interactive_process()?; c.p.send_line(&format!("init \"{repo_name}\""))?; c.expect_successful_init_message(&personal_repo_path(repo_name))?; Ok(()) } const DEFAULT_DESCRIPTION: &str = "Unnamed repository; edit this file 'description' to name the repository."; fn expect_list_table(c: &mut TestContext, repos: &[(String, String)]) -> Result<()> { c.p.send_line("list")?; c.p.exp_regex(r"\+-+\+-+\+")?; c.p.exp_regex(r"\| path +\| description +\|")?; c.p.exp_regex(r"\+=+\+")?; for (path, description) in repos { c.p.exp_string("| ")?; c.p.exp_string(path)?; c.p.exp_regex(r" +\| ")?; c.p.exp_string(&description)?; c.p.exp_regex(r" +\|")?; } c.p.exp_regex(r"\+-+\+-+\+")?; c.expect_prompt()?; Ok(()) } #[test] fn list_can_print_an_empty_list() -> Result<()> { let mut c = spawn_interactive_process()?; expect_list_table(&mut c, &[])?; Ok(()) } #[test] fn list_can_print_a_list_of_personal_repos_with_descriptions() -> Result<()> { let mut c = spawn_interactive_process()?; c.p.send_line(&format!("init {}", REPO_NAME))?; c.expect_successful_init_message(&personal_repo_path(REPO_NAME))?; expect_list_table( &mut c, &[( personal_repo_path(REPO_NAME), DEFAULT_DESCRIPTION.to_owned(), )], )?; Ok(()) } #[test] fn list_can_print_a_list_of_all_repos_with_descriptions() -> Result<()> { let mut c = spawn_interactive_process()?; c.p.send_line(&format!("init {}", REPO_NAME))?; c.expect_successful_init_message(&personal_repo_path(REPO_NAME))?; let group = arbitrary_user_group(); c.p.send_line(&format!("init --group {} {}", group, REPO_NAME_2))?; c.expect_successful_init_message(&group_repo_path(&group, REPO_NAME_2))?; expect_list_table( &mut c, &[ ( personal_repo_path(REPO_NAME), DEFAULT_DESCRIPTION.to_owned(), ), ( group_repo_path(&group, REPO_NAME_2), DEFAULT_DESCRIPTION.to_owned(), ), ], )?; Ok(()) } #[test] fn can_set_the_description_on_a_repo_during_init() -> Result<()> { let mut c = spawn_interactive_process()?; let description = "A cool repo that does cool things"; c.p.send_line(&format!("init --description \"{description}\" {REPO_NAME}"))?; c.expect_successful_init_message(&personal_repo_path(REPO_NAME))?; expect_list_table( &mut c, &[(personal_repo_path(REPO_NAME), description.to_owned())], )?; Ok(()) } #[test] fn can_change_the_description_on_a_repo() -> Result<()> { let mut c = spawn_interactive_process()?; let description = "A cool repo that does cool things"; let repo_path = personal_repo_path(REPO_NAME); c.p.send_line(&format!("init {REPO_NAME}"))?; c.p.exp_string(&format!("Successfully created \"{repo_path}\"",))?; c.p.send_line(&format!( "set-description \"{repo_path}\" \"{description}\"" ))?; c.p.exp_string("Successfully updated description")?; c.expect_prompt()?; expect_list_table(&mut c, &[(repo_path, description.to_owned())])?; Ok(()) } #[test] fn can_set_the_main_branch_of_a_new_git_repo() -> Result<()> { let mut c = spawn_interactive_process()?; let main_branch = "foobar"; c.p.send_line(&format!("init --branch {} {}", main_branch, REPO_NAME))?; c.expect_successful_init_message(&personal_repo_path(REPO_NAME))?; let repo_dir = c.personal_repo_dir(REPO_NAME); verify_current_branch(&repo_dir, &format!("refs/heads/{main_branch}")); Ok(()) } #[test] fn can_change_the_main_branch_on_a_repo() -> Result<()> { let mut c = spawn_interactive_process()?; let main_branch = "foobar"; let repo_path = personal_repo_path(REPO_NAME); c.p.send_line(&format!("init {}", REPO_NAME))?; c.expect_successful_init_message(&repo_path)?; c.p.send_line(&format!("set-branch \"{repo_path}\" \"{main_branch}\""))?; c.p.exp_string("Successfully updated branch")?; let repo_dir = c.personal_repo_dir(REPO_NAME); verify_current_branch(&repo_dir, &format!("refs/heads/{main_branch}")); Ok(()) } #[test] fn can_delete_a_repo() -> Result<()> { let mut c = spawn_interactive_process()?; let repo_path = personal_repo_path(REPO_NAME); let repo_dir = c.personal_repo_dir(REPO_NAME); c.p.send_line(&format!("init {}", REPO_NAME))?; c.expect_successful_init_message(&repo_path)?; verify_repo_exists(&repo_dir); c.p.send_line(&format!("delete \"{repo_path}\""))?; c.p.exp_string(&format!("Successfully deleted \"{repo_path}\""))?; verify_repo_does_not_exist(&repo_dir); Ok(()) }