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; use tempfile::TempDir; struct TestContext { p: PtySession, workdir: TempDir, } 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 mut p = spawn_command(command, Some(3000))?; expect_prompt(&mut p)?; Ok(TestContext { p, workdir }) } 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 }) } fn expect_prompt(p: &mut PtySession) -> Result<()> { p.exp_string("> ")?; Ok(()) } #[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("")?; expect_prompt(&mut c.p)?; c.p.send_line(" ")?; expect_prompt(&mut c.p)?; 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'")?; expect_prompt(&mut c.p)?; 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'")?; expect_prompt(&mut c.p)?; 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_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()?; let username = get_username().unwrap(); let repo_name = "my-new-repo"; c.p.send_line(&format!("init {}", repo_name))?; c.p.exp_string(&format!( "Successfully created \"git/{}/{}.git\"", username, repo_name ))?; expect_prompt(&mut c.p)?; let repo_dir = c .workdir .as_ref() .join("git") .join(username) .join(&format!("{}.git", 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 = get_user_groups().into_iter().next().unwrap(); let repo_name = "my-new-shared-repo"; c.p.send_line(&format!("init --group {} {}", group, repo_name))?; c.p.exp_string(&format!( "Successfully created \"git/{}/{}.git\"", group, repo_name ))?; expect_prompt(&mut c.p)?; let repo_dir = c .workdir .as_ref() .join("git") .join(&group) .join(&format!("{}.git", 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"; let repo_name = "my-new-shared-repo"; 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 username = get_username().unwrap(); let repo_name = "another-new-repo"; let mut c = run_batch_command(&format!("init {}", repo_name))?; c.p.exp_string(&format!( "Successfully created \"git/{}/{}.git\"", username, repo_name ))?; c.p.exp_eof()?; Ok(()) } #[test] fn allows_quotes_arguments() -> Result<()> { let username = get_username().unwrap(); let mut c = spawn_interactive_process()?; c.p.send_line("\"init\" 'another-new-repo'")?; c.p.exp_string(&format!( "Successfully created \"git/{}/another-new-repo.git\"", username ))?; 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 username = get_username().unwrap(); let mut c = spawn_interactive_process()?; c.p.send_line("init \"shukkie's new repo\"")?; c.p.exp_string(&format!( "Successfully created \"git/{}/shukkie's new repo.git\"", username ))?; 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"\+-+\+-+\+")?; expect_prompt(&mut c.p)?; 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()?; let username = get_username().unwrap(); let repo_name = "my-personal-repo"; c.p.send_line(&format!("init {}", repo_name))?; c.p.exp_string(&format!( "Successfully created \"git/{}/{}.git\"", username, repo_name ))?; expect_prompt(&mut c.p)?; expect_list_table( &mut c, &[( format!("git/{username}/{repo_name}.git"), DEFAULT_DESCRIPTION.to_owned(), )], )?; Ok(()) } #[test] fn list_can_print_a_list_of_all_repos_with_descriptions() -> Result<()> { let mut c = spawn_interactive_process()?; let username = get_username().unwrap(); let personal_repo_name = "my-personal-repo"; c.p.send_line(&format!("init {}", personal_repo_name))?; c.p.exp_string(&format!( "Successfully created \"git/{}/{}.git\"", username, personal_repo_name ))?; expect_prompt(&mut c.p)?; let group = get_user_groups().into_iter().next().unwrap(); let shared_repo_name = "my-shared-repo"; c.p.send_line(&format!("init --group {} {}", group, shared_repo_name))?; c.p.exp_string(&format!( "Successfully created \"git/{}/{}.git\"", group, shared_repo_name ))?; expect_prompt(&mut c.p)?; expect_list_table( &mut c, &[ ( format!("git/{username}/{personal_repo_name}.git"), DEFAULT_DESCRIPTION.to_owned(), ), ( format!("git/{group}/{shared_repo_name}.git"), DEFAULT_DESCRIPTION.to_owned(), ), ], )?; Ok(()) } #[test] fn can_set_the_description_on_a_repo_during_init() -> Result<()> { let mut c = spawn_interactive_process()?; let user = get_username().unwrap(); let repo_name = "my-personal-repo"; let description = "A cool repo that does cool things"; c.p.send_line(&format!("init --description \"{description}\" {repo_name}"))?; c.p.exp_string(&format!( "Successfully created \"git/{user}/{repo_name}.git\"", ))?; expect_list_table( &mut c, &[( format!("git/{user}/{repo_name}.git"), description.to_owned(), )], )?; Ok(()) } #[test] fn can_change_the_description_on_a_repo() -> Result<()> { let mut c = spawn_interactive_process()?; let user = get_username().unwrap(); let repo_name = "my-personal-repo"; let description = "A cool repo that does cool things"; c.p.send_line(&format!("init {repo_name}"))?; c.p.exp_string(&format!( "Successfully created \"git/{user}/{repo_name}.git\"", ))?; c.p.send_line(&format!( "set-description \"git/{user}/{repo_name}.git\" \"{description}\"" ))?; c.p.exp_string("Successfully updated description")?; expect_prompt(&mut c.p)?; expect_list_table( &mut c, &[( format!("git/{user}/{repo_name}.git"), description.to_owned(), )], )?; Ok(()) } #[test] fn can_set_the_main_branch_of_a_new_git_repo() -> Result<()> { let mut c = spawn_interactive_process()?; let username = get_username().unwrap(); let repo_name = "my-new-repo"; let main_branch = "foobar"; c.p.send_line(&format!("init --branch {} {}", main_branch, repo_name))?; c.p.exp_string(&format!( "Successfully created \"git/{}/{}.git\"", username, repo_name ))?; let repo_dir = c .workdir .as_ref() .join("git") .join(username) .join(&format!("{}.git", 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 username = get_username().unwrap(); let repo_name = "my-new-repo"; let main_branch = "foobar"; c.p.send_line(&format!("init {}", repo_name))?; c.p.exp_string(&format!( "Successfully created \"git/{}/{}.git\"", username, repo_name ))?; c.p.send_line(&format!( "set-branch \"git/{username}/{repo_name}.git\" \"{main_branch}\"" ))?; c.p.exp_string("Successfully updated branch")?; let repo_dir = c .workdir .as_ref() .join("git") .join(username) .join(&format!("{}.git", repo_name)); verify_current_branch(&repo_dir, &format!("refs/heads/{main_branch}")); Ok(()) }