// Copyright © 2022 The Radicle Git Contributors // SPDX-License-Identifier: GPL-3.0-or-later //! Unit tests for radicle_surf::vcs::git and its submodules. #[cfg(feature = "serialize")] use radicle_surf::git::{Author, BranchType, Commit}; use radicle_surf::{ diff::*, file_system::{unsound, Path}, git::{error::Error, Branch, BranchName, Namespace, Oid, RefScope, Repository, Rev, TagName}, }; const GIT_PLATINUM: &str = "../data/git-platinum"; #[cfg(not(feature = "gh-actions"))] #[test] // An issue with submodules, see: https://github.com/radicle-dev/radicle-surf/issues/54 fn test_submodule_failure() { let repo = Repository::new("../..").unwrap(); repo.as_ref() .snapshot(&Branch::local("main").into()) .unwrap(); } #[cfg(test)] mod namespace { use super::*; use pretty_assertions::{assert_eq, assert_ne}; use radicle_surf::vcs::Vcs; #[test] fn switch_to_banana() -> Result<(), Error> { let repo = Repository::new(GIT_PLATINUM)?; let repo = repo.as_ref(); let history_master = repo.get_history(Branch::local("master").into())?; repo.switch_namespace("golden")?; let history_banana = repo.get_history(Branch::local("banana").into())?; assert_ne!(history_master, history_banana); Ok(()) } #[test] fn me_namespace() -> Result<(), Error> { let repo = Repository::new(GIT_PLATINUM)?; let repo = repo.as_ref(); let history = repo.get_history(Branch::local("master").into())?; assert_eq!(repo.which_namespace(), Ok(None)); repo.switch_namespace("me")?; assert_eq!(repo.which_namespace(), Ok(Some(Namespace::try_from("me")?))); let history_feature = repo.get_history(Branch::local("feature/#1194").into())?; assert_eq!(history, history_feature); let expected_branches: Vec = vec![Branch::local("feature/#1194")]; let mut branches = repo.list_branches(RefScope::Local)?; branches.sort(); assert_eq!(expected_branches, branches); let expected_branches: Vec = vec![Branch::remote("feature/#1194", "fein")]; let mut branches = repo.list_branches(RefScope::Remote { name: Some("fein".to_string()), })?; branches.sort(); assert_eq!(expected_branches, branches); Ok(()) } #[test] fn golden_namespace() -> Result<(), Error> { let repo = Repository::new(GIT_PLATINUM)?; let repo = repo.as_ref(); let history = repo.get_history(Branch::local("master").into())?; assert_eq!(repo.which_namespace(), Ok(None)); repo.switch_namespace("golden")?; assert_eq!( repo.which_namespace(), Ok(Some(Namespace::try_from("golden")?)) ); let golden_history = repo.get_history(Branch::local("master").into())?; assert_eq!(history, golden_history); let expected_branches: Vec = vec![Branch::local("banana"), Branch::local("master")]; let mut branches = repo.list_branches(RefScope::Local)?; branches.sort(); assert_eq!(expected_branches, branches); let expected_branches: Vec = vec![ Branch::remote("fakie/bigspin", "kickflip"), Branch::remote("heelflip", "kickflip"), Branch::remote("v0.1.0", "kickflip"), ]; let mut branches = repo.list_branches(RefScope::Remote { name: Some("kickflip".to_string()), })?; branches.sort(); assert_eq!(expected_branches, branches); Ok(()) } #[test] fn silver_namespace() -> Result<(), Error> { let repo = Repository::new(GIT_PLATINUM)?; let repo = repo.as_ref(); let history = repo.get_history(Branch::local("master").into())?; assert_eq!(repo.which_namespace(), Ok(None)); repo.switch_namespace("golden/silver")?; assert_eq!( repo.which_namespace(), Ok(Some(Namespace::try_from("golden/silver")?)) ); let silver_history = repo.get_history(Branch::local("master").into())?; assert_ne!(history, silver_history); let expected_branches: Vec = vec![Branch::local("master")]; let mut branches = repo.list_branches(RefScope::All)?; branches.sort(); assert_eq!(expected_branches, branches); Ok(()) } } #[cfg(test)] mod rev { use radicle_surf::vcs::Vcs; use super::*; use std::str::FromStr; // **FIXME**: This seems to break occasionally on // buildkite. For some reason the commit // 3873745c8f6ffb45c990eb23b491d4b4b6182f95, which is on master // (currently HEAD), is not found. It seems to load the history // with d6880352fc7fda8f521ae9b7357668b17bb5bad5 as the HEAD. // // To temporarily fix this, we need to select "New Build" from the build kite // build page that's failing. // * Under "Message" put whatever you want. // * Under "Branch" put in the branch you're working on. // * Expand "Options" and select "clean checkout". #[test] fn _master() -> Result<(), Error> { let repo = Repository::new(GIT_PLATINUM)?; let repo = repo.as_ref(); let history = repo.get_history(Branch::remote("master", "origin").into())?; let commit1 = Oid::from_str("3873745c8f6ffb45c990eb23b491d4b4b6182f95")?; assert!( history .find(|commit| if commit.id == commit1 { Some(commit.clone()) } else { None }) .is_some(), "commit_id={}, history =\n{:#?}", commit1, &history ); let commit2 = Oid::from_str("d6880352fc7fda8f521ae9b7357668b17bb5bad5")?; assert!( history .find(|commit| if commit.id == commit2 { Some(commit.clone()) } else { None }) .is_some(), "commit_id={}, history =\n{:#?}", commit2, &history ); Ok(()) } #[test] fn commit() -> Result<(), Error> { let repo = Repository::new(GIT_PLATINUM)?; let rev: Rev = Oid::from_str("3873745c8f6ffb45c990eb23b491d4b4b6182f95")?.into(); let history = repo.as_ref().get_history(rev)?; let commit1 = Oid::from_str("3873745c8f6ffb45c990eb23b491d4b4b6182f95")?; assert!(history .find(|commit| if commit.id == commit1 { Some(commit.clone()) } else { None }) .is_some()); Ok(()) } #[test] fn commit_parents() -> Result<(), Error> { let repo = Repository::new(GIT_PLATINUM)?; let rev: Rev = Oid::from_str("3873745c8f6ffb45c990eb23b491d4b4b6182f95")?.into(); let history = repo.as_ref().get_history(rev)?; let commit = history.first(); assert_eq!( commit.parents, vec![Oid::from_str("d6880352fc7fda8f521ae9b7357668b17bb5bad5")?] ); Ok(()) } #[test] fn commit_short() -> Result<(), Error> { let repo = Repository::new(GIT_PLATINUM)?; let rev: Rev = repo.as_ref().oid("3873745c8")?.into(); let history = repo.as_ref().get_history(rev)?; let commit1 = Oid::from_str("3873745c8f6ffb45c990eb23b491d4b4b6182f95")?; assert!(history .find(|commit| if commit.id == commit1 { Some(commit.clone()) } else { None }) .is_some()); Ok(()) } #[test] fn tag() -> Result<(), Error> { let repo = Repository::new(GIT_PLATINUM)?; let rev: Rev = TagName::new("v0.2.0").into(); let history = repo.as_ref().get_history(rev)?; let commit1 = Oid::from_str("2429f097664f9af0c5b7b389ab998b2199ffa977")?; assert_eq!(history.first().id, commit1); Ok(()) } } #[cfg(test)] mod last_commit { use radicle_surf::vcs::Vcs; use super::*; use std::str::FromStr; #[test] fn readme_missing_and_memory() { let repo = Repository::new(GIT_PLATINUM) .expect("Could not retrieve ./data/git-platinum as git repository"); let oid = Oid::from_str("d3464e33d75c75c99bfb90fa2e9d16efc0b7d0e3").expect("Failed to parse SHA"); // memory.rs is commited later so it should not exist here. let rev: Rev = oid.into(); let memory_last_commit_oid = repo .as_ref() .last_commit( Path::with_root(&[unsound::label::new("src"), unsound::label::new("memory.rs")]), &rev, ) .expect("Failed to get last commit") .map(|commit| commit.id); assert_eq!(memory_last_commit_oid, None); // README.md exists in this commit. let readme_last_commit = repo .as_ref() .last_commit(Path::with_root(&[unsound::label::new("README.md")]), &rev) .expect("Failed to get last commit") .map(|commit| commit.id); assert_eq!(readme_last_commit, Some(oid)); } #[test] fn folder_svelte() { let repo = Repository::new(GIT_PLATINUM) .expect("Could not retrieve ./data/git-platinum as git repository"); // Check that last commit is the actual last commit even if head commit differs. let oid = Oid::from_str("19bec071db6474af89c866a1bd0e4b1ff76e2b97").expect("Could not parse SHA"); let rev: Rev = oid.into(); let expected_commit_id = Oid::from_str("f3a089488f4cfd1a240a9c01b3fcc4c34a4e97b2").unwrap(); let folder_svelte = repo .as_ref() .last_commit(unsound::path::new("~/examples/Folder.svelte"), &rev) .expect("Failed to get last commit") .map(|commit| commit.id); assert_eq!(folder_svelte, Some(expected_commit_id)); } #[test] fn nest_directory() { let repo = Repository::new(GIT_PLATINUM) .expect("Could not retrieve ./data/git-platinum as git repository"); // Check that last commit is the actual last commit even if head commit differs. let oid = Oid::from_str("19bec071db6474af89c866a1bd0e4b1ff76e2b97").expect("Failed to parse SHA"); let rev: Rev = oid.into(); let expected_commit_id = Oid::from_str("2429f097664f9af0c5b7b389ab998b2199ffa977").unwrap(); let nested_directory_tree_commit_id = repo .as_ref() .last_commit( unsound::path::new("~/this/is/a/really/deeply/nested/directory/tree"), &rev, ) .expect("Failed to get last commit") .map(|commit| commit.id); assert_eq!(nested_directory_tree_commit_id, Some(expected_commit_id)); } #[test] #[cfg(not(windows))] fn can_get_last_commit_for_special_filenames() { let repo = Repository::new(GIT_PLATINUM) .expect("Could not retrieve ./data/git-platinum as git repository"); // Check that last commit is the actual last commit even if head commit differs. let oid = Oid::from_str("a0dd9122d33dff2a35f564d564db127152c88e02").expect("Failed to parse SHA"); let rev: Rev = oid.into(); let expected_commit_id = Oid::from_str("a0dd9122d33dff2a35f564d564db127152c88e02").unwrap(); let backslash_commit_id = repo .as_ref() .last_commit(unsound::path::new("~/special/faux\\path"), &rev) .expect("Failed to get last commit") .map(|commit| commit.id); assert_eq!(backslash_commit_id, Some(expected_commit_id)); let ogre_commit_id = repo .as_ref() .last_commit(unsound::path::new("~/special/👹👹👹"), &rev) .expect("Failed to get last commit") .map(|commit| commit.id); assert_eq!(ogre_commit_id, Some(expected_commit_id)); } #[test] fn root() { let repo = Repository::new(GIT_PLATINUM) .expect("Could not retrieve ./data/git-platinum as git repository"); let rev: Rev = Branch::local("master").into(); let root_last_commit_id = repo .as_ref() .last_commit(Path::root(), &rev) .expect("Failed to get last commit") .map(|commit| commit.id); let expected_oid = repo .as_ref() .get_history(Branch::local("master").into()) .unwrap() .first() .id; assert_eq!(root_last_commit_id, Some(expected_oid)); } } #[cfg(test)] mod diff { use super::*; use pretty_assertions::assert_eq; use std::str::FromStr; #[test] fn test_initial_diff() -> Result<(), Error> { let oid = Oid::from_str("d3464e33d75c75c99bfb90fa2e9d16efc0b7d0e3")?; let repo = Repository::new(GIT_PLATINUM)?; let repo = repo.as_ref(); let commit = repo.get_git2_commit(oid).unwrap(); assert!(commit.parents().count() == 0); assert!(commit.parent(0).is_err()); let diff = repo.initial_diff(oid)?; let expected_diff = Diff { created: vec![CreateFile { path: Path::with_root(&[unsound::label::new("README.md")]), diff: FileDiff::Plain { hunks: vec![Hunk { header: Line::from(b"@@ -0,0 +1 @@\n".to_vec()), lines: vec![LineDiff::addition( b"This repository is a data source for the Upstream front-end tests.\n" .to_vec(), 1, )], }] .into(), }, }], deleted: vec![], moved: vec![], copied: vec![], modified: vec![], }; assert_eq!(expected_diff, diff); Ok(()) } #[test] fn test_diff() -> Result<(), Error> { let repo = Repository::new(GIT_PLATINUM)?; let repo = repo.as_ref(); let commit = repo .get_git2_commit(Oid::from_str("80bacafba303bf0cdf6142921f430ff265f25095")?) .unwrap(); let parent = commit.parent(0)?; let diff = repo.diff(parent.id().into(), commit.id().into())?; let expected_diff = Diff { created: vec![], deleted: vec![], moved: vec![], copied: vec![], modified: vec![ModifiedFile { path: Path::with_root(&[unsound::label::new("README.md")]), diff: FileDiff::Plain { hunks: vec![Hunk { header: Line::from(b"@@ -1 +1,2 @@\n".to_vec()), lines: vec![ LineDiff::deletion(b"This repository is a data source for the Upstream front-end tests.\n".to_vec(), 1), LineDiff::addition(b"This repository is a data source for the Upstream front-end tests and the\n".to_vec(), 1), LineDiff::addition(b"[`radicle-surf`](https://github.com/radicle-dev/git-platinum) unit tests.\n".to_vec(), 2), ] }].into() }, eof: None, }] }; assert_eq!(expected_diff, diff); Ok(()) } #[cfg(feature = "serialize")] #[test] fn test_diff_serde() -> Result<(), Error> { let diff = Diff { created: vec![CreateFile{path: unsound::path::new("LICENSE"), diff: FileDiff::Plain { hunks: Hunks::default() }}], deleted: vec![], moved: vec![ MoveFile { old_path: unsound::path::new("CONTRIBUTING"), new_path: unsound::path::new("CONTRIBUTING.md") } ], copied: vec![], modified: vec![ModifiedFile { path: Path::with_root(&[unsound::label::new("README.md")]), diff: FileDiff::Plain { hunks: vec![Hunk { header: Line::from(b"@@ -1 +1,2 @@\n".to_vec()), lines: vec![ LineDiff::deletion(b"This repository is a data source for the Upstream front-end tests.\n".to_vec(), 1), LineDiff::addition(b"This repository is a data source for the Upstream front-end tests and the\n".to_vec(), 1), LineDiff::addition(b"[`radicle-surf`](https://github.com/radicle-dev/git-platinum) unit tests.\n".to_vec(), 2), LineDiff::context(b"\n".to_vec(), 3, 4), ] }].into() }, eof: None, }] }; let eof: Option = None; let json = serde_json::json!({ "created": [{"path": "LICENSE", "diff": { "type": "plain", "hunks": [] }, }], "deleted": [], "moved": [{ "oldPath": "CONTRIBUTING", "newPath": "CONTRIBUTING.md" }], "copied": [], "modified": [{ "path": "README.md", "diff": { "type": "plain", "hunks": [{ "header": "@@ -1 +1,2 @@\n", "lines": [ { "lineNum": 1, "line": "This repository is a data source for the Upstream front-end tests.\n", "type": "deletion" }, { "lineNum": 1, "line": "This repository is a data source for the Upstream front-end tests and the\n", "type": "addition" }, { "lineNum": 2, "line": "[`radicle-surf`](https://github.com/radicle-dev/git-platinum) unit tests.\n", "type": "addition" }, { "lineNumOld": 3, "lineNumNew": 4, "line": "\n", "type": "context" } ] }] }, "eof" : eof, }] }); assert_eq!(serde_json::to_value(&diff).unwrap(), json); Ok(()) } } #[cfg(test)] mod threading { use super::*; use std::sync::{Mutex, MutexGuard}; #[test] fn basic_test() -> Result<(), Error> { let shared_repo = Mutex::new(Repository::new(GIT_PLATINUM)?); let locked_repo: MutexGuard = shared_repo.lock().unwrap(); let mut branches = locked_repo.as_ref().list_branches(RefScope::All)?; branches.sort(); assert_eq!( branches, vec![ Branch::remote("HEAD", "origin"), Branch::remote("dev", "origin"), Branch::local("dev"), Branch::remote("master", "origin"), Branch::local("master"), Branch::remote("orange/pineapple", "banana"), Branch::remote("pineapple", "banana"), ] ); Ok(()) } } #[cfg(feature = "serialize")] #[cfg(test)] mod commit { use super::{Author, Commit}; use proptest::prelude::*; use radicle_git_ext::Oid; use std::str::FromStr; use test_helpers::roundtrip; #[cfg(feature = "serialize")] proptest! { #[test] fn prop_test_commits(commit in commits_strategy()) { roundtrip::json(commit) } } fn commits_strategy() -> impl Strategy { ("[a-fA-F0-9]{40}", any::(), any::()).prop_map(|(id, text, time)| Commit { id: Oid::from_str(&id).unwrap(), author: Author { name: text.clone(), email: text.clone(), time: git2::Time::new(time, 0), }, committer: Author { name: text.clone(), email: text.clone(), time: git2::Time::new(time, 0), }, message: text.clone(), summary: text, parents: vec![Oid::from_str(&id).unwrap(), Oid::from_str(&id).unwrap()], }) } } #[cfg(feature = "serialize")] #[cfg(test)] mod branch { use super::*; use proptest::prelude::*; use test_helpers::roundtrip; proptest! { #[test] fn prop_test_branch(branch in branch_strategy()) { roundtrip::json(branch) } } fn branch_strategy() -> impl Strategy { prop_oneof![ any::().prop_map(|name| Branch { name: BranchName::new(&name), locality: BranchType::Local }), (any::(), any::()).prop_map(|(name, remote_name)| Branch { name: BranchName::new(&name), locality: BranchType::Remote { name: Some(remote_name), }, }) ] } } #[cfg(test)] mod ext { use radicle_surf::vcs::git::ext::*; #[test] fn test_try_extract_refname() { assert_eq!(try_extract_refname("refs/heads/dev"), Ok("dev".to_string())); assert_eq!( try_extract_refname("refs/heads/master"), Ok("master".to_string()) ); assert_eq!( try_extract_refname("refs/remotes/banana/pineapple"), Ok("banana/pineapple".to_string()) ); assert_eq!( try_extract_refname("refs/remotes/origin/master"), Ok("origin/master".to_string()) ); assert_eq!( try_extract_refname("refs/namespaces/golden/refs/heads/banana"), Ok("banana".to_string()) ); assert_eq!( try_extract_refname("refs/namespaces/golden/refs/tags/v0.1.0"), Ok("v0.1.0".to_string()) ); assert_eq!( try_extract_refname("refs/namespaces/golden/refs/namespaces/silver/refs/heads/master"), Ok("master".to_string()) ); assert_eq!( try_extract_refname("refs/namespaces/golden/refs/remotes/kickflip/heads/heelflip"), Ok("kickflip/heelflip".to_string()) ); } } #[cfg(test)] mod reference { use super::*; use radicle_surf::vcs::git::{ParseError, Ref}; use std::str::FromStr; #[test] fn parse_ref() -> Result<(), ParseError> { assert_eq!( Ref::from_str("refs/remotes/origin/master"), Ok(Ref::RemoteBranch { remote: "origin".to_string(), name: BranchName::new("master") }) ); assert_eq!( Ref::from_str("refs/heads/master"), Ok(Ref::LocalBranch { name: BranchName::new("master"), }) ); assert_eq!( Ref::from_str("refs/heads/i-am-hyphenated"), Ok(Ref::LocalBranch { name: BranchName::new("i-am-hyphenated"), }) ); assert_eq!( Ref::from_str("refs/heads/prefix/i-am-hyphenated"), Ok(Ref::LocalBranch { name: BranchName::new("prefix/i-am-hyphenated"), }) ); assert_eq!( Ref::from_str("refs/tags/v0.0.1"), Ok(Ref::Tag { name: TagName::new("v0.0.1") }) ); assert_eq!( Ref::from_str("refs/namespaces/moi/refs/remotes/origin/master"), Ok(Ref::Namespace { namespace: "moi".to_string(), reference: Box::new(Ref::RemoteBranch { remote: "origin".to_string(), name: BranchName::new("master") }) }) ); assert_eq!( Ref::from_str("refs/namespaces/moi/refs/namespaces/toi/refs/tags/v1.0.0"), Ok(Ref::Namespace { namespace: "moi".to_string(), reference: Box::new(Ref::Namespace { namespace: "toi".to_string(), reference: Box::new(Ref::Tag { name: TagName::new("v1.0.0") }) }) }) ); assert_eq!( Ref::from_str("refs/namespaces/me/refs/heads/feature/#1194"), Ok(Ref::Namespace { namespace: "me".to_string(), reference: Box::new(Ref::LocalBranch { name: BranchName::new("feature/#1194"), }) }) ); assert_eq!( Ref::from_str("refs/namespaces/me/refs/remotes/fein/heads/feature/#1194"), Ok(Ref::Namespace { namespace: "me".to_string(), reference: Box::new(Ref::RemoteBranch { remote: "fein".to_string(), name: BranchName::new("heads/feature/#1194"), }) }) ); assert_eq!( Ref::from_str("refs/remotes/master"), Err(ParseError::MalformedRef("refs/remotes/master".to_owned())), ); assert_eq!( Ref::from_str("refs/namespaces/refs/remotes/origin/master"), Err(ParseError::MalformedRef( "refs/namespaces/refs/remotes/origin/master".to_owned() )), ); Ok(()) } }