//! The remembered list of repositories. use std::path::PathBuf; use gigit_git::testing::Fixture; use gigit_lib::{RecentsHandle, RepositoryId, WorkspaceHandle}; use tempfile::TempDir; /// A recents actor writing into a directory that cleans itself up. fn recents() -> (RecentsHandle, TempDir) { let directory = tempfile::tempdir().expect("could not create a temporary directory"); let (handle, actor) = RecentsHandle::new(directory.path().join("recent-repositories.json")); tokio::spawn(actor); (handle, directory) } /// A workspace wired to a given recents actor, so opening records. fn workspace(recents: RecentsHandle) -> WorkspaceHandle { let (handle, actor) = WorkspaceHandle::new(recents); tokio::spawn(actor); handle } fn id(path: &str) -> RepositoryId { serde_json::from_str(&format!("\"{path}\"")).expect("should deserialise") } #[tokio::test] async fn starts_empty_when_there_is_nothing_remembered() { let (recents, _directory) = recents(); assert_eq!(recents.list().await.expect("should list"), Vec::new()); } #[tokio::test] async fn remembers_a_repository_that_was_opened() { let fixture = Fixture::with_one_commit(); let (recents, _directory) = recents(); let workspace = workspace(recents.clone()); workspace .open(fixture.path().to_path_buf()) .await .expect("should open"); let remembered = recents.list().await.expect("should list"); assert_eq!(remembered.len(), 1); assert!(remembered[0].exists); assert_eq!( remembered[0].path.canonicalize().ok(), fixture.path().canonicalize().ok(), "the working tree is what a person recognises, not the git directory" ); } #[tokio::test] async fn names_a_repository_after_its_folder() { let fixture = Fixture::with_one_commit(); let (recents, _directory) = recents(); let workspace = workspace(recents.clone()); workspace .open(fixture.path().to_path_buf()) .await .expect("should open"); let remembered = recents.list().await.expect("should list"); let expected = fixture .path() .file_name() .expect("the fixture has a folder name") .to_string_lossy() .into_owned(); assert_eq!(remembered[0].name, expected); } #[tokio::test] async fn does_not_remember_a_repository_that_failed_to_open() { let directory = tempfile::tempdir().expect("could not create a temporary directory"); let (recents, _home) = recents(); let workspace = workspace(recents.clone()); workspace .open(directory.path().to_path_buf()) .await .expect_err("there is no repository there"); assert!(recents.list().await.expect("should list").is_empty()); } #[tokio::test] async fn opening_again_moves_a_repository_to_the_front_rather_than_duplicating_it() { let first = Fixture::with_one_commit(); let second = Fixture::with_one_commit(); let (recents, _directory) = recents(); let workspace = workspace(recents.clone()); workspace .open(first.path().to_path_buf()) .await .expect("should open"); workspace .open(second.path().to_path_buf()) .await .expect("should open"); workspace .open(first.path().to_path_buf()) .await .expect("should reopen"); let remembered = recents.list().await.expect("should list"); assert_eq!(remembered.len(), 2, "reopening must not add a second entry"); assert_eq!( remembered[0].path.canonicalize().ok(), first.path().canonicalize().ok(), "the most recently opened comes first" ); } #[tokio::test] async fn keeps_only_the_most_recent_twenty() { let (recents, _directory) = recents(); for index in 0..25 { recents .record(entry(&format!("/repositories/number-{index}"))) .await .expect("should record"); } let remembered = recents.list().await.expect("should list"); assert_eq!(remembered.len(), 20); assert_eq!(remembered[0].name, "number-24", "newest first"); assert_eq!(remembered[19].name, "number-5", "the oldest five fell off"); } #[tokio::test] async fn forgetting_removes_a_repository() { let (recents, _directory) = recents(); recents .record(entry("/repositories/one")) .await .expect("should record"); recents .record(entry("/repositories/two")) .await .expect("should record"); let forgotten = recents .forget(id("/repositories/one/.git")) .await .expect("should forget"); assert!(forgotten); let remembered = recents.list().await.expect("should list"); assert_eq!(remembered.len(), 1); assert_eq!(remembered[0].name, "two"); } #[tokio::test] async fn forgetting_something_unknown_is_not_an_error() { // The UI may well ask twice, or ask about something already gone. let (recents, _directory) = recents(); assert!(!recents .forget(id("/never/seen/.git")) .await .expect("should not fail")); } #[tokio::test] async fn the_list_survives_a_restart() { let directory = tempfile::tempdir().expect("could not create a temporary directory"); let file = directory.path().join("recent-repositories.json"); let (first, actor) = RecentsHandle::new(file.clone()); tokio::spawn(actor); first .record(entry("/repositories/kept")) .await .expect("should record"); // Wait for the write by asking for something the actor answers after it. first.list().await.expect("should list"); drop(first); let (second, actor) = RecentsHandle::new(file); tokio::spawn(actor); let remembered = second.list().await.expect("should list"); assert_eq!(remembered.len(), 1); assert_eq!(remembered[0].name, "kept"); } #[tokio::test] async fn a_repository_that_has_gone_away_is_marked_rather_than_dropped() { // Deleting a folder should not silently erase it from the list — the user // may want to see it was there, and to forget it deliberately. let (recents, _directory) = recents(); let fixture = Fixture::with_one_commit(); let workspace = workspace(recents.clone()); workspace .open(fixture.path().to_path_buf()) .await .expect("should open"); assert!(recents.list().await.expect("should list")[0].exists); drop(fixture); let remembered = recents.list().await.expect("should list"); assert_eq!(remembered.len(), 1); assert!( !remembered[0].exists, "a missing repository should be flagged" ); } #[tokio::test] async fn an_unreadable_file_is_treated_as_an_empty_list() { // Better to start over than to refuse to run because a convenience cache // got mangled. let directory = tempfile::tempdir().expect("could not create a temporary directory"); let file = directory.path().join("recent-repositories.json"); std::fs::write(&file, "this is not json").expect("should write"); let (recents, actor) = RecentsHandle::new(file); tokio::spawn(actor); assert!(recents.list().await.expect("should list").is_empty()); } #[tokio::test] async fn entries_cross_the_boundary_in_the_shape_the_frontend_expects() { let (recents, _directory) = recents(); recents .record(entry("/repositories/one")) .await .expect("should record"); let remembered = recents.list().await.expect("should list"); let json = serde_json::to_value(&remembered[0]).expect("should serialise"); assert!(json["id"].is_string()); assert!(json["path"].is_string()); assert_eq!(json["name"], "one"); assert!(json["lastOpened"].is_number()); assert!(json["exists"].is_boolean()); } /// A recent entry for a path that need not exist. fn entry(path: &str) -> gigit_lib::RecentRepository { let json = serde_json::json!({ "id": format!("{path}/.git"), "path": path, "name": PathBuf::from(path) .file_name() .expect("a path with a last segment") .to_string_lossy(), "lastOpened": 1_700_000_000, "exists": true, }); serde_json::from_value(json).expect("should deserialise") }