mod cli_test_utils; use anyhow::Result; use cli_test_utils::*; use std::os::unix::fs::MetadataExt; const REPO_NAME: &str = "my-repository"; const REPO_NAME_2: &str = "my-other-repository"; const DEFAULT_DESCRIPTION: &str = "Unnamed repository; edit this file 'description' to name the repository."; #[test] fn shows_a_prompt() -> Result<()> { TestContext::new_interactive()?; Ok(()) } #[test] fn does_nothing_after_receiving_whitespace_input() -> Result<()> { let mut c = TestContext::new_interactive()?; 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 = TestContext::new_interactive()?; c.p.send_control('d')?; c.p.exp_eof()?; Ok(()) } #[test] fn quits_when_exit_command_is_sent() -> Result<()> { let mut c = TestContext::new_interactive()?; c.p.send_line("exit")?; c.p.exp_eof()?; Ok(()) } #[test] fn reports_error_with_unsupported_shell_commands() -> Result<()> { let mut c = TestContext::new_interactive()?; 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 = TestContext::new_interactive()?; c.p.send_line(" asd fg ")?; c.p.exp_string("error: unrecognized subcommand 'asd'")?; c.expect_prompt()?; Ok(()) } #[test] fn can_init_a_new_git_repo() -> Result<()> { let mut c = TestContext::new_interactive()?; 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 = TestContext::new_interactive()?; 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")); let expected_gid = nix::unistd::Group::from_name(&group) .unwrap() .unwrap() .gid .as_raw(); let group_dir = repo_dir.parent().unwrap(); let group_dir_metadata = group_dir.metadata().unwrap(); assert_eq!(group_dir_metadata.gid(), expected_gid); assert_eq!( group_dir_metadata.mode(), 0o42770, "Mode is {:o}", group_dir_metadata.mode() ); assert_eq!(repo_dir.metadata().unwrap().gid(), expected_gid); Ok(()) } #[test] fn does_not_init_shared_repo_if_the_user_isnt_in_the_group() -> Result<()> { let mut c = TestContext::new_interactive()?; 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 = TestContext::new_batch(&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 = TestContext::new_interactive()?; 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 = TestContext::new_interactive()?; 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 = TestContext::new_interactive()?; 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 = TestContext::new_interactive()?; c.p.send_line(&format!("init \"{repo_name}\""))?; c.expect_successful_init_message(&personal_repo_path(repo_name))?; Ok(()) } #[test] fn list_can_print_an_empty_list() -> Result<()> { let mut c = TestContext::new_interactive()?; c.expect_list_table(&[])?; Ok(()) } #[test] fn list_can_print_a_list_of_personal_repos_with_descriptions() -> Result<()> { let mut c = TestContext::new_interactive()?; c.p.send_line(&format!("init {}", REPO_NAME))?; c.expect_successful_init_message(&personal_repo_path(REPO_NAME))?; c.expect_list_table(&[(&personal_repo_path(REPO_NAME), DEFAULT_DESCRIPTION)])?; Ok(()) } #[test] fn list_can_print_a_list_of_all_repos_with_descriptions() -> Result<()> { let mut c = TestContext::new_interactive()?; 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))?; c.expect_list_table(&[ (&personal_repo_path(REPO_NAME), DEFAULT_DESCRIPTION), (&group_repo_path(&group, REPO_NAME_2), DEFAULT_DESCRIPTION), ])?; Ok(()) } #[test] fn list_can_print_a_verbose_list_of_all_repos() -> Result<()> { let mut c = TestContext::new_interactive()?; 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))?; c.expect_list_table_verbose(&[ (&personal_repo_path(REPO_NAME), DEFAULT_DESCRIPTION), (&group_repo_path(&group, REPO_NAME_2), DEFAULT_DESCRIPTION), ])?; Ok(()) } #[test] fn can_set_the_description_on_a_repo_during_init() -> Result<()> { let mut c = TestContext::new_interactive()?; 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))?; c.expect_list_table(&[(&personal_repo_path(REPO_NAME), description)])?; Ok(()) } #[test] fn can_change_the_description_on_a_repo() -> Result<()> { let mut c = TestContext::new_interactive()?; 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()?; c.expect_list_table(&[(&repo_path, description)])?; Ok(()) } #[test] fn can_set_the_main_branch_of_a_new_git_repo() -> Result<()> { let mut c = TestContext::new_interactive()?; 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 = TestContext::new_interactive()?; 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 = TestContext::new_interactive()?; 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!( "Are you sure you want to delete \"{repo_path}\"? (yes/no)" ))?; c.p.send_line("yes")?; c.p.exp_string(&format!("Successfully deleted \"{repo_path}\""))?; verify_repo_does_not_exist(&repo_dir); Ok(()) } #[test] fn repo_is_not_deleted_if_you_say_youre_not_sure() -> Result<()> { let mut c = TestContext::new_interactive()?; 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!( "Are you sure you want to delete \"{repo_path}\"? (yes/no)" ))?; c.p.send_line("no")?; c.p.exp_string(&format!("Action cancelled"))?; verify_repo_exists(&repo_dir); Ok(()) } #[test] fn git_housekeeping_repacks_objects() -> Result<()> { let mut c = TestContext::new_interactive()?; 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)?; let checkout_dir = create_clone(&c, &repo_dir, REPO_NAME); create_commit(&checkout_dir)?; push(&checkout_dir, "main"); let packs_dir = repo_dir.join("objects").join("pack"); assert_eq!(packs_dir.read_dir()?.count(), 0); c.p.send_line(&format!("housekeeping {repo_path}"))?; c.p.exp_string(&format!("Successfully did housekeeping on \"{repo_path}\""))?; assert!(packs_dir.read_dir()?.count() > 0); Ok(()) } #[test] fn git_housekeeping_cleans_out_stale_refs() -> Result<()> { let mut c = TestContext::new_interactive()?; 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)?; let checkout_dir = create_clone(&c, &repo_dir, REPO_NAME); let commit_hash = create_commit(&checkout_dir)?; push(&checkout_dir, "main:temporary-branch"); push(&checkout_dir, ":temporary-branch"); verify_commit_exists(&repo_dir, &commit_hash); c.p.send_line(&format!("housekeeping"))?; c.p.exp_string(&format!("Successfully did housekeeping on \"{repo_path}\""))?; verify_commit_does_not_exist(&repo_dir, &commit_hash); Ok(()) }