From 0c941740c84874f46ad4821dddad586a91948fd2 Mon Sep 17 00:00:00 2001 From: Han Xu Date: Mon, 26 Sep 2022 10:37:12 -0700 Subject: [PATCH] radicle-surf: test structure compliance. The test structure should be that all tests are gathered under a 't' sub-crate, followed by the 'test' crate depending on this new crate. --- radicle-surf/Cargo.toml | 4 + radicle-surf/src/diff.rs | 236 ------- radicle-surf/src/diff/git.rs | 75 -- radicle-surf/src/file_system/directory.rs | 199 +----- radicle-surf/src/file_system/path.rs | 28 - radicle-surf/src/lib.rs | 3 +- radicle-surf/src/tree.rs | 700 +------------------ radicle-surf/src/vcs.rs | 5 + radicle-surf/src/vcs/git.rs | 592 +--------------- radicle-surf/src/vcs/git/branch.rs | 31 - radicle-surf/src/vcs/git/commit.rs | 39 +- radicle-surf/src/vcs/git/ext.rs | 47 +- radicle-surf/src/vcs/git/reference.rs | 106 +-- radicle-surf/src/vcs/git/repo.rs | 6 +- radicle-surf/t/Cargo.toml | 35 + radicle-surf/t/src/diff.rs | 298 ++++++++ radicle-surf/t/src/file_system.rs | 222 ++++++ radicle-surf/t/src/git.rs | 811 ++++++++++++++++++++++ radicle-surf/t/src/lib.rs | 14 + radicle-surf/t/src/tree.rs | 692 ++++++++++++++++++ test/Cargo.toml | 4 + 21 files changed, 2105 insertions(+), 2042 deletions(-) create mode 100644 radicle-surf/t/Cargo.toml create mode 100644 radicle-surf/t/src/diff.rs create mode 100644 radicle-surf/t/src/file_system.rs create mode 100644 radicle-surf/t/src/git.rs create mode 100644 radicle-surf/t/src/lib.rs create mode 100644 radicle-surf/t/src/tree.rs diff --git a/radicle-surf/Cargo.toml b/radicle-surf/Cargo.toml index 458f988..bbba54c 100644 --- a/radicle-surf/Cargo.toml +++ b/radicle-surf/Cargo.toml @@ -15,6 +15,10 @@ include = [ "data/git-platinum.tgz", ] +[lib] +test = false +doctest = false + [features] serialize = ["serde"] # NOTE: testing `test_submodule_failure` on GH actions diff --git a/radicle-surf/src/diff.rs b/radicle-surf/src/diff.rs index 353b5f9..b6d3217 100644 --- a/radicle-surf/src/diff.rs +++ b/radicle-surf/src/diff.rs @@ -594,239 +594,3 @@ impl Diff { } } } - -#[cfg(test)] -mod tests { - use crate::{ - diff::*, - file_system::{unsound, *}, - }; - use pretty_assertions::assert_eq; - - #[test] - fn test_create_file() { - let directory = Directory::root(); - - let mut new_directory = Directory::root(); - new_directory.insert_file(unsound::path::new("banana.rs"), File::new(b"use banana")); - - let diff = Diff::diff(directory, new_directory); - - let expected_diff = Diff { - created: vec![CreateFile { - path: Path::with_root(&[unsound::label::new("banana.rs")]), - diff: FileDiff::Plain { - hunks: Hunks::default(), - }, - }], - deleted: vec![], - copied: vec![], - moved: vec![], - modified: vec![], - }; - - assert_eq!(diff, expected_diff) - } - - #[test] - fn test_delete_file() { - let mut directory = Directory::root(); - directory.insert_file(unsound::path::new("banana.rs"), File::new(b"use banana")); - - let new_directory = Directory::root(); - - let diff = Diff::diff(directory, new_directory); - - let expected_diff = Diff { - created: vec![], - deleted: vec![DeleteFile { - path: Path::with_root(&[unsound::label::new("banana.rs")]), - diff: FileDiff::Plain { - hunks: Hunks::default(), - }, - }], - moved: vec![], - copied: vec![], - modified: vec![], - }; - - assert_eq!(diff, expected_diff) - } - - /* TODO(fintan): Move is not detected yet - #[test] - fn test_moved_file() { - let mut directory = Directory::root(); - directory.insert_file(&unsound::path::new("mod.rs"), File::new(b"use banana")); - - let mut new_directory = Directory::root(); - new_directory.insert_file(&unsound::path::new("banana.rs"), File::new(b"use banana")); - - let diff = Diff::diff(directory, new_directory).expect("diff failed"); - - assert_eq!(diff, Diff::new()) - } - */ - - #[test] - fn test_modify_file() { - let mut directory = Directory::root(); - directory.insert_file(unsound::path::new("banana.rs"), File::new(b"use banana")); - - let mut new_directory = Directory::root(); - new_directory.insert_file(unsound::path::new("banana.rs"), File::new(b"use banana;")); - - let diff = Diff::diff(directory, new_directory); - - let expected_diff = Diff { - created: vec![], - deleted: vec![], - moved: vec![], - copied: vec![], - modified: vec![ModifiedFile { - path: Path::with_root(&[unsound::label::new("banana.rs")]), - diff: FileDiff::Plain { - hunks: Hunks::default(), - }, - eof: None, - }], - }; - - assert_eq!(diff, expected_diff) - } - - #[test] - fn test_create_directory() { - let directory = Directory::root(); - - let mut new_directory = Directory::root(); - new_directory.insert_file( - unsound::path::new("src/banana.rs"), - File::new(b"use banana"), - ); - - let diff = Diff::diff(directory, new_directory); - - let expected_diff = Diff { - created: vec![CreateFile { - path: Path::with_root(&[ - unsound::label::new("src"), - unsound::label::new("banana.rs"), - ]), - diff: FileDiff::Plain { - hunks: Hunks::default(), - }, - }], - deleted: vec![], - moved: vec![], - copied: vec![], - modified: vec![], - }; - - assert_eq!(diff, expected_diff) - } - - #[test] - fn test_delete_directory() { - let mut directory = Directory::root(); - directory.insert_file( - unsound::path::new("src/banana.rs"), - File::new(b"use banana"), - ); - - let new_directory = Directory::root(); - - let diff = Diff::diff(directory, new_directory); - - let expected_diff = Diff { - created: vec![], - deleted: vec![DeleteFile { - path: Path::with_root(&[ - unsound::label::new("src"), - unsound::label::new("banana.rs"), - ]), - diff: FileDiff::Plain { - hunks: Hunks::default(), - }, - }], - moved: vec![], - copied: vec![], - modified: vec![], - }; - - assert_eq!(diff, expected_diff) - } - - #[test] - fn test_modify_file_directory() { - let mut directory = Directory::root(); - directory.insert_file( - unsound::path::new("src/banana.rs"), - File::new(b"use banana"), - ); - - let mut new_directory = Directory::root(); - new_directory.insert_file( - unsound::path::new("src/banana.rs"), - File::new(b"use banana;"), - ); - - let diff = Diff::diff(directory, new_directory); - - let expected_diff = Diff { - created: vec![], - deleted: vec![], - moved: vec![], - copied: vec![], - modified: vec![ModifiedFile { - path: Path::with_root(&[ - unsound::label::new("src"), - unsound::label::new("banana.rs"), - ]), - diff: FileDiff::Plain { - hunks: Hunks::default(), - }, - eof: None, - }], - }; - - assert_eq!(diff, expected_diff) - } - - /* TODO(fintan): Tricky stuff - #[test] - fn test_disjoint_directories() { - let mut directory = Directory::root(); - directory.insert_file( - &unsound::path::new("foo/src/banana.rs"), - File::new(b"use banana"), - ); - - let mut other_directory = Directory::root(); - other_directory.insert_file( - &unsound::path::new("bar/src/pineapple.rs"), - File::new(b"use pineapple"), - ); - - let diff = Diff::diff(directory, other_directory).expect("diff failed"); - - let expected_diff = Diff { - created: vec![CreateFile(Path::from_labels( - unsound::label::new("bar"), - &[ - unsound::label::new("src"), - unsound::label::new("pineapple.rs"), - ], - ))], - deleted: vec![DeleteFile(Path::from_labels( - unsound::label::new("foo"), - &[unsound::label::new("src"), unsound::label::new("banana.rs")], - ))], - moved: vec![], - modified: vec![], - }; - - assert_eq!(diff, expected_diff) - } - */ -} diff --git a/radicle-surf/src/diff/git.rs b/radicle-surf/src/diff/git.rs index b0bd7bd..d4d6831 100644 --- a/radicle-surf/src/diff/git.rs +++ b/radicle-surf/src/diff/git.rs @@ -229,78 +229,3 @@ impl<'a> TryFrom> for Diff { Ok(diff) } } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_both_missing_eof_newline() { - let buf = r#" -diff --git a/.env b/.env -index f89e4c0..7c56eb7 100644 ---- a/.env -+++ b/.env -@@ -1 +1 @@ --hello=123 -\ No newline at end of file -+hello=1234 -\ No newline at end of file -"#; - let diff = git2::Diff::from_buffer(buf.as_bytes()).unwrap(); - let diff = Diff::try_from(diff).unwrap(); - assert_eq!(diff.modified[0].eof, Some(EofNewLine::BothMissing)); - } - - #[test] - fn test_none_missing_eof_newline() { - let buf = r#" -diff --git a/.env b/.env -index f89e4c0..7c56eb7 100644 ---- a/.env -+++ b/.env -@@ -1 +1 @@ --hello=123 -+hello=1234 -"#; - let diff = git2::Diff::from_buffer(buf.as_bytes()).unwrap(); - let diff = Diff::try_from(diff).unwrap(); - assert_eq!(diff.modified[0].eof, None); - } - - // TODO(xphoniex): uncomment once libgit2 has fixed the bug - //#[test] - fn test_old_missing_eof_newline() { - let buf = r#" -diff --git a/.env b/.env -index f89e4c0..7c56eb7 100644 ---- a/.env -+++ b/.env -@@ -1 +1 @@ --hello=123 -\ No newline at end of file -+hello=1234 -"#; - let diff = git2::Diff::from_buffer(buf.as_bytes()).unwrap(); - let diff = Diff::try_from(diff).unwrap(); - assert_eq!(diff.modified[0].eof, Some(EofNewLine::OldMissing)); - } - - // TODO(xphoniex): uncomment once libgit2 has fixed the bug - //#[test] - fn test_new_missing_eof_newline() { - let buf = r#" -diff --git a/.env b/.env -index f89e4c0..7c56eb7 100644 ---- a/.env -+++ b/.env -@@ -1 +1 @@ --hello=123 -+hello=1234 -\ No newline at end of file -"#; - let diff = git2::Diff::from_buffer(buf.as_bytes()).unwrap(); - let diff = Diff::try_from(diff).unwrap(); - assert_eq!(diff.modified[0].eof, Some(EofNewLine::NewMissing)); - } -} diff --git a/radicle-surf/src/file_system/directory.rs b/radicle-surf/src/file_system/directory.rs index 0b64494..a8d3d4d 100644 --- a/radicle-surf/src/file_system/directory.rs +++ b/radicle-surf/src/file_system/directory.rs @@ -504,7 +504,8 @@ impl Directory { } } - pub(crate) fn from_hash_map(files: HashMap>) -> Self { + /// Creates a `Directory` from a HashMap `files`. + pub fn from_hash_map(files: HashMap>) -> Self { let mut directory: Self = Directory::root(); for (path, files) in files.into_iter() { @@ -524,199 +525,3 @@ impl Directory { directory } } - -#[cfg(test)] -pub mod tests { - #[cfg(test)] - mod list_directory { - use crate::file_system::{unsound, Directory, File, SystemType}; - - #[test] - fn root_files() { - let mut directory = Directory::root(); - directory.insert_file( - unsound::path::new("foo.hs"), - File::new(b"module BananaFoo ..."), - ); - directory.insert_file( - unsound::path::new("bar.hs"), - File::new(b"module BananaBar ..."), - ); - directory.insert_file( - unsound::path::new("baz.hs"), - File::new(b"module BananaBaz ..."), - ); - - assert_eq!( - directory.list_directory(), - vec![ - SystemType::file(unsound::label::new("bar.hs")), - SystemType::file(unsound::label::new("baz.hs")), - SystemType::file(unsound::label::new("foo.hs")), - ] - ); - } - } - - #[cfg(test)] - mod find_file { - use crate::file_system::{unsound, *}; - - #[test] - fn in_root() { - let file = File::new(b"module Banana ..."); - let mut directory = Directory::root(); - directory.insert_file(unsound::path::new("foo.hs"), file.clone()); - - assert_eq!( - directory.find_file(unsound::path::new("foo.hs")), - Some(file) - ); - } - - #[test] - fn file_does_not_exist() { - let file_path = unsound::path::new("bar.hs"); - - let file = File::new(b"module Banana ..."); - - let mut directory = Directory::root(); - directory.insert_file(unsound::path::new("foo.hs"), file); - - assert_eq!(directory.find_file(file_path), None) - } - } - - #[cfg(test)] - mod directory_size { - use crate::file_system::{unsound, Directory, File}; - use nonempty::NonEmpty; - - #[test] - fn root_directory_files() { - let mut root = Directory::root(); - root.insert_files( - &[], - NonEmpty::from(( - ( - unsound::label::new("main.rs"), - File::new(b"println!(\"Hello, world!\")"), - ), - vec![( - unsound::label::new("lib.rs"), - File::new(b"struct Hello(String)"), - )], - )), - ); - - assert_eq!(root.size(), 45); - } - } - - #[cfg(test)] - mod properties { - use crate::file_system::{unsound, *}; - use nonempty::NonEmpty; - use proptest::{collection, prelude::*}; - use std::collections::HashMap; - - #[test] - fn test_all_directories_and_files() { - let mut directory_map = HashMap::new(); - - let path1 = unsound::path::new("foo/bar/baz"); - let file1 = (unsound::label::new("monadic.rs"), File::new(&[])); - let file2 = (unsound::label::new("oscoin.rs"), File::new(&[])); - directory_map.insert(path1, NonEmpty::from((file1, vec![file2]))); - - let path2 = unsound::path::new("foor/bar/quuz"); - let file3 = (unsound::label::new("radicle.rs"), File::new(&[])); - - directory_map.insert(path2, NonEmpty::new(file3)); - - assert!(prop_all_directories_and_files(directory_map)) - } - - fn label_strategy() -> impl Strategy { - // ASCII regex, excluding '/' because of posix file paths - "[ -.|0-~]+".prop_map(|label| unsound::label::new(&label)) - } - - fn path_strategy(max_size: usize) -> impl Strategy { - ( - label_strategy(), - collection::vec(label_strategy(), 0..max_size), - ) - .prop_map(|(label, labels)| Path((label, labels).into())) - } - - fn file_strategy() -> impl Strategy { - // ASCII regex, see: https://catonmat.net/my-favorite-regex - (label_strategy(), "[ -~]*") - .prop_map(|(name, contents)| (name, File::new(contents.as_bytes()))) - } - - fn directory_map_strategy( - path_size: usize, - n_files: usize, - map_size: usize, - ) -> impl Strategy>> { - collection::hash_map( - path_strategy(path_size), - collection::vec(file_strategy(), 1..n_files).prop_map(|files| { - NonEmpty::from_slice(&files).expect("Strategy generated files of length 0") - }), - 0..map_size, - ) - } - - // TODO(fintan): This is a bit slow. Could be time to benchmark some functions. - proptest! { - #[test] - fn prop_test_all_directories_and_files(directory_map in directory_map_strategy(10, 10, 10)) { - prop_all_directories_and_files(directory_map); - } - } - - fn prop_all_directories_and_files( - directory_map: HashMap>, - ) -> bool { - let mut new_directory_map = HashMap::new(); - for (path, files) in directory_map { - new_directory_map.insert(path.clone(), files); - } - - let directory = Directory::from_hash_map(new_directory_map.clone()); - - for (directory_path, files) in new_directory_map { - for (file_name, _) in files.iter() { - let mut path = directory_path.clone(); - if directory.find_directory(path.clone()).is_none() { - eprintln!("Search Directory: {:#?}", directory); - eprintln!("Path to find: {:#?}", path); - return false; - } - - path.push(file_name.clone()); - if directory.find_file(path.clone()).is_none() { - eprintln!("Search Directory: {:#?}", directory); - eprintln!("Path to find: {:#?}", path); - return false; - } - } - } - true - } - - #[test] - fn test_file_name_is_same_as_root() { - // This test ensures that if the name is the same the root of the - // directory, that search_path.split_last() doesn't toss away the prefix. - let path = unsound::path::new("foo/bar/~"); - let mut directory_map = HashMap::new(); - directory_map.insert(path, NonEmpty::new((Label::root(), File::new(b"root")))); - - assert!(prop_all_directories_and_files(directory_map)); - } - } -} diff --git a/radicle-surf/src/file_system/path.rs b/radicle-surf/src/file_system/path.rs index a86556f..f1d97ba 100644 --- a/radicle-surf/src/file_system/path.rs +++ b/radicle-surf/src/file_system/path.rs @@ -440,31 +440,3 @@ impl TryFrom for Path { Ok(path) } } - -#[cfg(test)] -mod tests { - #[cfg(test)] - mod path { - use crate::file_system::unsound; - - #[test] - fn split_last_root_and_foo() { - let path = unsound::path::new("foo"); - assert_eq!(path.split_last(), (vec![], unsound::label::new("foo"))); - } - - #[test] - fn split_last_same_labels() { - // An interesting case for when first == last, but doesn't imply a singleton - // Path. - let path = unsound::path::new("foo/bar/foo"); - assert_eq!( - path.split_last(), - ( - vec![unsound::label::new("foo"), unsound::label::new("bar")], - unsound::label::new("foo") - ) - ); - } - } -} diff --git a/radicle-surf/src/lib.rs b/radicle-surf/src/lib.rs index f85fefc..c7d6016 100644 --- a/radicle-surf/src/lib.rs +++ b/radicle-surf/src/lib.rs @@ -103,8 +103,9 @@ pub mod syntax; #[cfg(feature = "syntax")] pub use syntax::SYNTAX_SET; +pub mod tree; + // Private modules mod nonempty; -mod tree; pub use crate::vcs::git; diff --git a/radicle-surf/src/tree.rs b/radicle-surf/src/tree.rs index b5cceab..541e755 100644 --- a/radicle-surf/src/tree.rs +++ b/radicle-surf/src/tree.rs @@ -15,6 +15,8 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . +#![allow(missing_docs)] + use crate::nonempty::split_last; use nonempty::NonEmpty; use std::cmp::Ordering; @@ -152,7 +154,7 @@ impl SubTree { } #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] -pub struct Tree(pub(crate) NonEmpty>); +pub struct Tree(pub NonEmpty>); impl From> for Forest { fn from(tree: Tree) -> Self { @@ -161,17 +163,17 @@ impl From> for Forest { } #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] -pub struct Forest(pub(crate) Option>); +pub struct Forest(pub Option>); impl Tree { /// Create a new `Tree` containing a single `Branch` given /// the key and sub-tree. - fn branch(key: K, forest: Self) -> Self { + pub fn branch(key: K, forest: Self) -> Self { Tree(NonEmpty::new(SubTree::branch(key, forest))) } /// Create a new `Tree` containing a single `Node`. - fn node(key: K, value: A) -> Self { + pub fn node(key: K, value: A) -> Self { Tree(NonEmpty::new(SubTree::Node { key, value })) } @@ -499,693 +501,3 @@ impl Forest { self.0.iter().flat_map(|trees| trees.iter_keys()) } } - -#[cfg(test)] -mod tests { - use super::*; - use pretty_assertions::assert_eq; - - #[derive(Debug, Clone, PartialEq, Eq)] - struct TestNode { - id: u32, - } - - #[test] - fn test_is_empty() { - let mut tree = Forest::root(); - assert!(tree.is_empty()); - - let a_node = TestNode { id: 1 }; - - tree.insert(NonEmpty::new(String::from("a")), a_node); - assert!(!tree.is_empty()); - } - - #[test] - fn test_insert_root_node() { - let a_label = String::from("a"); - - let mut tree = Forest::root(); - - let a_node = TestNode { id: 1 }; - - tree.insert(NonEmpty::new(a_label), a_node.clone()); - - assert_eq!(tree, Forest(Some(Tree::node(String::from("a"), a_node)))); - } - - #[test] - fn test_insert_with_prepending_root_nodes() { - let a_label = String::from("a"); - - let mut tree = Forest::root(); - - let a_node = TestNode { id: 1 }; - let b_node = TestNode { id: 2 }; - - tree.insert_with( - NonEmpty::new(a_label.clone()), - NonEmpty::new(a_node.clone()), - |nodes| nodes.insert(0, a_node.clone()), - ); - tree.insert_with( - NonEmpty::new(a_label), - NonEmpty::new(b_node.clone()), - |nodes| nodes.insert(0, b_node.clone()), - ); - - assert_eq!( - tree, - Forest(Some(Tree::node( - String::from("a"), - NonEmpty::from((b_node, vec![a_node])) - ))) - ); - } - - #[test] - fn test_insert_with_prepending_branch_nodes() { - let a_label = String::from("a"); - let b_label = String::from("b"); - let path = NonEmpty::from((a_label, vec![b_label])); - - let mut tree = Forest::root(); - - let a_node = TestNode { id: 1 }; - let b_node = TestNode { id: 2 }; - - tree.insert_with(path.clone(), NonEmpty::new(a_node.clone()), |nodes| { - nodes.insert(0, a_node.clone()) - }); - tree.insert_with(path, NonEmpty::new(b_node.clone()), |nodes| { - nodes.insert(0, b_node.clone()) - }); - - assert_eq!( - tree, - Forest(Some(Tree::branch( - String::from("a"), - Tree::node(String::from("b"), NonEmpty::from((b_node, vec![a_node]))) - ))) - ); - } - - #[test] - fn test_insert_single_node() { - let a_label = String::from("a"); - let b_label = String::from("b"); - let c_label = String::from("c"); - let path = NonEmpty::from((a_label, vec![b_label, c_label])); - - let mut tree = Forest::root(); - - let c_node = TestNode { id: 1 }; - - tree.insert(path, c_node.clone()); - - assert_eq!( - tree, - Forest(Some(Tree::branch( - String::from("a"), - Tree::branch(String::from("b"), Tree::node(String::from("c"), c_node)) - ))) - ); - } - - #[test] - fn test_insert_two_nodes() { - let a_label = String::from("a"); - let b_label = String::from("b"); - let c_label = String::from("c"); - let d_label = String::from("d"); - let c_path = NonEmpty::from((a_label.clone(), vec![b_label.clone(), c_label])); - let d_path = NonEmpty::from((a_label, vec![b_label, d_label])); - - let mut tree = Forest::root(); - - let c_node = TestNode { id: 1 }; - - tree.insert(c_path, c_node.clone()); - - let d_node = TestNode { id: 3 }; - - tree.insert(d_path, d_node.clone()); - - assert_eq!( - tree, - Forest(Some(Tree::branch( - String::from("a"), - Tree::branch( - String::from("b"), - Tree(NonEmpty::from(( - SubTree::Node { - key: String::from("c"), - value: c_node - }, - vec![SubTree::Node { - key: String::from("d"), - value: d_node - }] - ))) - ) - ))) - ); - } - - #[test] - fn test_insert_replaces_node() { - let a_label = String::from("a"); - let b_label = String::from("b"); - let c_label = String::from("c"); - let c_path = NonEmpty::from((a_label, vec![b_label, c_label])); - - let mut tree = Forest::root(); - - let c_node = TestNode { id: 1 }; - - tree.insert(c_path.clone(), c_node); - - let new_c_node = TestNode { id: 3 }; - - tree.insert(c_path, new_c_node.clone()); - - assert_eq!( - tree, - Forest(Some(Tree::branch( - String::from("a"), - Tree::branch( - String::from("b"), - Tree(NonEmpty::new(SubTree::Node { - key: String::from("c"), - value: new_c_node - },)) - ) - ))) - ); - } - - #[test] - fn test_insert_replaces_root_node() { - let c_label = String::from("c"); - - let mut tree = Forest::root(); - - let c_node = TestNode { id: 1 }; - - tree.insert(NonEmpty::new(c_label.clone()), c_node); - - let new_c_node = TestNode { id: 3 }; - - tree.insert(NonEmpty::new(c_label), new_c_node.clone()); - - assert_eq!( - tree, - Forest(Some(Tree::node(String::from("c"), new_c_node))) - ); - } - - #[test] - fn test_insert_replaces_branch_node() { - let a_label = String::from("a"); - let c_label = String::from("c"); - let c_path = NonEmpty::from((a_label, vec![c_label])); - - let mut tree = Forest::root(); - - let c_node = TestNode { id: 1 }; - - tree.insert(c_path.clone(), c_node); - - let new_c_node = TestNode { id: 3 }; - - tree.insert(c_path, new_c_node.clone()); - - assert_eq!( - tree, - Forest(Some(Tree::branch( - String::from("a"), - Tree::node(String::from("c"), new_c_node), - ))) - ); - } - - #[test] - fn test_insert_replaces_branch_with_node() { - let a_label = String::from("a"); - let b_label = String::from("b"); - let c_label = String::from("c"); - let c_path = NonEmpty::from((a_label.clone(), vec![b_label.clone(), c_label])); - - let mut tree = Forest::root(); - - let c_node = TestNode { id: 1 }; - - tree.insert(c_path, c_node); - - let new_c_node = TestNode { id: 3 }; - - tree.insert(NonEmpty::from((a_label, vec![b_label])), new_c_node.clone()); - - assert_eq!( - tree, - Forest(Some(Tree::branch( - String::from("a"), - Tree::node(String::from("b"), new_c_node), - ))) - ); - } - - #[test] - fn test_insert_replaces_node_with_branch() { - let a_label = String::from("a"); - let b_label = String::from("b"); - let c_label = String::from("c"); - let b_path = NonEmpty::from((a_label.clone(), vec![b_label.clone()])); - let c_path = NonEmpty::from((a_label, vec![b_label, c_label])); - - let mut tree = Forest::root(); - - let b_node = TestNode { id: 1 }; - - tree.insert(b_path, b_node); - - let new_c_node = TestNode { id: 3 }; - - tree.insert(c_path, new_c_node.clone()); - - assert_eq!( - tree, - Forest(Some(Tree::branch( - String::from("a"), - Tree::branch( - String::from("b"), - Tree(NonEmpty::new(SubTree::Node { - key: String::from("c"), - value: new_c_node - },)) - ) - ))) - ); - } - - #[test] - fn test_insert_replaces_node_with_branch_foo() { - let a_label = String::from("a"); - let b_label = String::from("b"); - let c_label = String::from("c"); - let d_label = String::from("d"); - let b_path = NonEmpty::from((a_label.clone(), vec![b_label.clone()])); - let d_path = NonEmpty::from((a_label, vec![b_label, c_label, d_label])); - - let mut tree = Forest::root(); - - let b_node = TestNode { id: 1 }; - - tree.insert(b_path, b_node); - - let d_node = TestNode { id: 3 }; - - tree.insert(d_path, d_node.clone()); - - assert_eq!( - tree, - Forest(Some(Tree::branch( - String::from("a"), - Tree::branch( - String::from("b"), - Tree::branch(String::from("c"), Tree::node(String::from("d"), d_node)) - ) - ))) - ); - } - - #[test] - fn test_insert_two_nodes_out_of_order() { - let a_label = String::from("a"); - let b_label = String::from("b"); - let c_label = String::from("c"); - let d_label = String::from("d"); - let c_path = NonEmpty::from((a_label.clone(), vec![b_label.clone(), c_label])); - let d_path = NonEmpty::from((a_label, vec![b_label, d_label])); - - let mut tree = Forest::root(); - - let d_node = TestNode { id: 3 }; - - tree.insert(d_path, d_node.clone()); - - let c_node = TestNode { id: 1 }; - - tree.insert(c_path, c_node.clone()); - - assert_eq!( - tree, - Forest(Some(Tree::branch( - String::from("a"), - Tree::branch( - String::from("b"), - Tree(NonEmpty::from(( - SubTree::Node { - key: String::from("c"), - value: c_node - }, - vec![SubTree::Node { - key: String::from("d"), - value: d_node - }] - ))) - ) - ))) - ); - } - - #[test] - fn test_insert_branch() { - let a_label = String::from("a"); - let b_label = String::from("b"); - let c_label = String::from("c"); - let d_label = String::from("d"); - let e_label = String::from("e"); - let f_label = String::from("f"); - - let c_path = NonEmpty::from((a_label.clone(), vec![b_label.clone(), c_label])); - let d_path = NonEmpty::from((a_label.clone(), vec![b_label, d_label])); - let f_path = NonEmpty::from((a_label, vec![e_label, f_label])); - - let mut tree = Forest::root(); - - let c_node = TestNode { id: 1 }; - - let d_node = TestNode { id: 3 }; - - let f_node = TestNode { id: 2 }; - - tree.insert(d_path, d_node.clone()); - tree.insert(c_path, c_node.clone()); - tree.insert(f_path, f_node.clone()); - - assert_eq!( - tree, - Forest(Some(Tree::branch( - String::from("a"), - Tree(NonEmpty::from(( - SubTree::Branch { - key: String::from("b"), - forest: Box::new(Tree(NonEmpty::from(( - SubTree::Node { - key: String::from("c"), - value: c_node - }, - vec![SubTree::Node { - key: String::from("d"), - value: d_node - }] - )))) - }, - vec![SubTree::Branch { - key: String::from("e"), - forest: Box::new(Tree::node(String::from("f"), f_node)) - },] - ))) - ))) - ); - } - - #[test] - fn test_insert_two_branches() { - let a_label = String::from("a"); - let b_label = String::from("b"); - let c_label = String::from("c"); - let d_label = String::from("d"); - let e_label = String::from("e"); - let f_label = String::from("f"); - - let c_path = NonEmpty::from((a_label, vec![b_label, c_label])); - let f_path = NonEmpty::from((d_label, vec![e_label, f_label])); - - let mut tree = Forest::root(); - - let c_node = TestNode { id: 1 }; - - let f_node = TestNode { id: 2 }; - - tree.insert(c_path, c_node.clone()); - tree.insert(f_path, f_node.clone()); - - assert_eq!( - tree, - Forest(Some(Tree(NonEmpty::from(( - SubTree::Branch { - key: String::from("a"), - forest: Box::new(Tree::branch( - String::from("b"), - Tree::node(String::from("c"), c_node) - )), - }, - vec![SubTree::Branch { - key: String::from("d"), - forest: Box::new(Tree::branch( - String::from("e"), - Tree::node(String::from("f"), f_node) - )) - }] - ))))) - ); - } - - #[test] - fn test_insert_branches_and_node() { - let a_label = String::from("a"); - let b_label = String::from("b"); - let c_label = String::from("c"); - let d_label = String::from("d"); - let e_label = String::from("e"); - let f_label = String::from("f"); - let g_label = String::from("g"); - - let c_path = NonEmpty::from((a_label.clone(), vec![b_label.clone(), c_label])); - let d_path = NonEmpty::from((a_label.clone(), vec![b_label, d_label])); - let e_path = NonEmpty::from((a_label.clone(), vec![e_label])); - let g_path = NonEmpty::from((a_label, vec![f_label, g_label])); - - let mut tree = Forest::root(); - - let c_node = TestNode { id: 1 }; - - let d_node = TestNode { id: 3 }; - - let e_node = TestNode { id: 2 }; - - let g_node = TestNode { id: 2 }; - - tree.insert(d_path, d_node.clone()); - tree.insert(c_path, c_node.clone()); - tree.insert(e_path, e_node.clone()); - tree.insert(g_path, g_node.clone()); - - assert_eq!( - tree, - Forest(Some(Tree::branch( - String::from("a"), - Tree(NonEmpty::from(( - SubTree::Branch { - key: String::from("b"), - forest: Box::new(Tree(NonEmpty::from(( - SubTree::Node { - key: String::from("c"), - value: c_node - }, - vec![SubTree::Node { - key: String::from("d"), - value: d_node - }] - )))) - }, - vec![ - SubTree::Node { - key: String::from("e"), - value: e_node - }, - SubTree::Branch { - key: String::from("f"), - forest: Box::new(Tree::node(String::from("g"), g_node)) - }, - ] - ))) - ))) - ); - } - - #[test] - fn test_find_root_node() { - let a_label = String::from("a"); - - let mut tree = Forest::root(); - - let a_node = TestNode { id: 1 }; - - tree.insert(NonEmpty::new(a_label), a_node.clone()); - - assert_eq!( - tree.find(NonEmpty::new(String::from("a"))), - Some(&SubTree::Node { - key: String::from("a"), - value: a_node - }) - ); - - assert_eq!(tree.find(NonEmpty::new(String::from("b"))), None); - } - - #[test] - fn test_find_branch_and_node() { - let a_label = String::from("a"); - let b_label = String::from("b"); - let c_label = String::from("c"); - let path = NonEmpty::from((a_label, vec![b_label, c_label])); - - let mut tree = Forest::root(); - - let c_node = TestNode { id: 1 }; - - tree.insert(path, c_node.clone()); - - assert_eq!( - tree.find(NonEmpty::new(String::from("a"))), - Some(&SubTree::Branch { - key: String::from("a"), - forest: Box::new(Tree::branch( - String::from("b"), - Tree::node(String::from("c"), c_node.clone()) - )) - }) - ); - - assert_eq!( - tree.find(NonEmpty::from((String::from("a"), vec![String::from("b")]))), - Some(&SubTree::Branch { - key: String::from("b"), - forest: Box::new(Tree::node(String::from("c"), c_node.clone())) - }) - ); - - assert_eq!( - tree.find(NonEmpty::from(( - String::from("a"), - vec![String::from("b"), String::from("c")] - ))), - Some(&SubTree::Node { - key: String::from("c"), - value: c_node - }) - ); - - assert_eq!(tree.find(NonEmpty::new(String::from("b"))), None); - - assert_eq!( - tree.find(NonEmpty::from((String::from("a"), vec![String::from("c")]))), - None - ); - } - - #[test] - fn test_maximum_by_root_nodes() { - let mut tree = Forest::root(); - - let a_node = TestNode { id: 1 }; - - let b_node = TestNode { id: 3 }; - - tree.insert(NonEmpty::new(String::from("a")), a_node.clone()); - tree.insert(NonEmpty::new(String::from("b")), b_node.clone()); - - assert_eq!(tree.maximum_by(|a, b| a.id.cmp(&b.id)), Some(&b_node)); - assert_eq!( - tree.maximum_by(|a, b| a.id.cmp(&b.id).reverse()), - Some(&a_node) - ); - } - - #[test] - fn test_maximum_by_branch_and_node() { - let mut tree = Forest::root(); - - let a_node = TestNode { id: 1 }; - - let b_node = TestNode { id: 3 }; - - tree.insert( - NonEmpty::from((String::from("c"), vec![String::from("a")])), - a_node.clone(), - ); - tree.insert(NonEmpty::new(String::from("b")), b_node.clone()); - - assert_eq!(tree.maximum_by(|a, b| a.id.cmp(&b.id)), Some(&b_node)); - assert_eq!( - tree.maximum_by(|a, b| a.id.cmp(&b.id).reverse()), - Some(&a_node) - ); - } - - #[test] - fn test_maximum_by_branch_and_branch() { - let mut tree = Forest::root(); - - let a_node = TestNode { id: 1 }; - - let b_node = TestNode { id: 3 }; - - tree.insert( - NonEmpty::from((String::from("c"), vec![String::from("a")])), - a_node.clone(), - ); - tree.insert( - NonEmpty::from((String::from("d"), vec![String::from("a")])), - b_node.clone(), - ); - - assert_eq!(tree.maximum_by(|a, b| a.id.cmp(&b.id)), Some(&b_node)); - assert_eq!( - tree.maximum_by(|a, b| a.id.cmp(&b.id).reverse()), - Some(&a_node) - ); - } - - #[test] - fn test_maximum_by_branch_nodes() { - let mut tree = Forest::root(); - - let a_node = TestNode { id: 1 }; - - let b_node = TestNode { id: 3 }; - - tree.insert( - NonEmpty::from((String::from("c"), vec![String::from("a")])), - a_node.clone(), - ); - tree.insert( - NonEmpty::from((String::from("c"), vec![String::from("b")])), - b_node.clone(), - ); - - assert_eq!(tree.maximum_by(|a, b| a.id.cmp(&b.id)), Some(&b_node)); - assert_eq!( - tree.maximum_by(|a, b| a.id.cmp(&b.id).reverse()), - Some(&a_node) - ); - } - - #[test] - fn test_fold_root_nodes() { - let mut tree = Forest::root(); - - let a_node = TestNode { id: 1 }; - - let b_node = TestNode { id: 3 }; - - tree.insert(NonEmpty::new(String::from("a")), a_node); - tree.insert(NonEmpty::new(String::from("b")), b_node); - - assert_eq!(tree.iter().fold(0, |b, a| a.id + b), 4); - } -} diff --git a/radicle-surf/src/vcs.rs b/radicle-surf/src/vcs.rs index de146ef..49ea0da 100644 --- a/radicle-surf/src/vcs.rs +++ b/radicle-surf/src/vcs.rs @@ -157,6 +157,11 @@ impl Browser { self.history.clone() } + /// Get the current `History` the `Browser` is viewing as a ref. + pub fn as_history(&self) -> &History { + &self.history + } + /// Set the `History` the `Browser` should view. pub fn set(&mut self, history: History) { self.history = history; diff --git a/radicle-surf/src/vcs/git.rs b/radicle-surf/src/vcs/git.rs index 59c15d1..cde8f81 100644 --- a/radicle-surf/src/vcs/git.rs +++ b/radicle-surf/src/vcs/git.rs @@ -69,14 +69,14 @@ pub use radicle_git_ext::Oid; /// Provides ways of selecting a particular reference/revision. mod reference; -pub use reference::{Ref, Rev}; +pub use reference::{ParseError, Ref, Rev}; mod repo; pub use repo::{History, Repository, RepositoryRef}; pub mod error; -mod ext; +pub mod ext; /// Provides the data for talking about branches. pub mod branch; @@ -1181,591 +1181,3 @@ impl<'a> Browser<'a> { )) } } - -#[cfg(test)] -mod tests { - use super::*; - - #[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(); - let browser = Browser::new(&repo, Branch::local("main")).unwrap(); - - browser.get_directory().unwrap(); - } - - #[cfg(test)] - mod namespace { - use super::*; - use pretty_assertions::{assert_eq, assert_ne}; - - #[test] - fn switch_to_banana() -> Result<(), Error> { - let repo = Repository::new("./data/git-platinum")?; - let mut browser = Browser::new_with_namespace( - &repo, - &Namespace::try_from("golden")?, - Branch::local("master"), - )?; - let history = browser.history.clone(); - - browser.branch(Branch::local("banana"))?; - - assert_ne!(history, browser.history); - - Ok(()) - } - - #[test] - fn me_namespace() -> Result<(), Error> { - let repo = Repository::new("./data/git-platinum")?; - let browser = Browser::new(&repo, Branch::local("master"))?; - let history = browser.history.clone(); - - assert_eq!(browser.which_namespace(), Ok(None)); - - let browser = browser - .switch_namespace(&Namespace::try_from("me")?, Branch::local("feature/#1194"))?; - - assert_eq!( - browser.which_namespace(), - Ok(Some(Namespace::try_from("me")?)) - ); - assert_eq!(history, browser.history); - - let expected_branches: Vec = vec![Branch::local("feature/#1194")]; - let mut branches = browser.list_branches(RefScope::Local)?; - branches.sort(); - - assert_eq!(expected_branches, branches); - - let expected_branches: Vec = vec![Branch::remote("feature/#1194", "fein")]; - let mut branches = browser.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("./data/git-platinum")?; - let browser = Browser::new(&repo, Branch::local("master"))?; - let history = browser.history.clone(); - - assert_eq!(browser.which_namespace(), Ok(None)); - - let golden_browser = browser - .switch_namespace(&Namespace::try_from("golden")?, Branch::local("master"))?; - - assert_eq!( - golden_browser.which_namespace(), - Ok(Some(Namespace::try_from("golden")?)) - ); - assert_eq!(history, golden_browser.history); - - let expected_branches: Vec = - vec![Branch::local("banana"), Branch::local("master")]; - let mut branches = golden_browser.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 = golden_browser.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("./data/git-platinum")?; - let browser = Browser::new(&repo, Branch::local("master"))?; - let history = browser.history.clone(); - - assert_eq!(browser.which_namespace(), Ok(None)); - - let silver_browser = browser.switch_namespace( - &Namespace::try_from("golden/silver")?, - Branch::local("master"), - )?; - - assert_eq!( - silver_browser.which_namespace(), - Ok(Some(Namespace::try_from("golden/silver")?)) - ); - assert_ne!(history, silver_browser.history); - - let expected_branches: Vec = vec![Branch::local("master")]; - let mut branches = silver_browser.list_branches(RefScope::All)?; - branches.sort(); - - assert_eq!(expected_branches, branches); - - Ok(()) - } - } - - #[cfg(test)] - mod rev { - use super::{Branch, Browser, Error, Oid, Repository, TagName}; - 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("./data/git-platinum")?; - let mut browser = Browser::new(&repo, Branch::local("master"))?; - browser.rev(Branch::remote("master", "origin"))?; - - let commit1 = Oid::from_str("3873745c8f6ffb45c990eb23b491d4b4b6182f95")?; - assert!( - browser - .history - .find(|commit| if commit.id == commit1 { - Some(commit.clone()) - } else { - None - }) - .is_some(), - "commit_id={}, history =\n{:#?}", - commit1, - browser.history - ); - - let commit2 = Oid::from_str("d6880352fc7fda8f521ae9b7357668b17bb5bad5")?; - assert!( - browser - .history - .find(|commit| if commit.id == commit2 { - Some(commit.clone()) - } else { - None - }) - .is_some(), - "commit_id={}, history =\n{:#?}", - commit2, - browser.history - ); - - Ok(()) - } - - #[test] - fn commit() -> Result<(), Error> { - let repo = Repository::new("./data/git-platinum")?; - let mut browser = Browser::new(&repo, Branch::local("master"))?; - browser.rev(Oid::from_str("3873745c8f6ffb45c990eb23b491d4b4b6182f95")?)?; - - let commit1 = Oid::from_str("3873745c8f6ffb45c990eb23b491d4b4b6182f95")?; - assert!(browser - .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("./data/git-platinum")?; - let mut browser = Browser::new(&repo, Branch::local("master"))?; - browser.rev(Oid::from_str("3873745c8f6ffb45c990eb23b491d4b4b6182f95")?)?; - let commit = browser.history.first(); - - assert_eq!( - commit.parents, - vec![Oid::from_str("d6880352fc7fda8f521ae9b7357668b17bb5bad5")?] - ); - - Ok(()) - } - - #[test] - fn commit_short() -> Result<(), Error> { - let repo = Repository::new("./data/git-platinum")?; - let mut browser = Browser::new(&repo, Branch::local("master"))?; - browser.rev(browser.oid("3873745c8")?)?; - - let commit1 = Oid::from_str("3873745c8f6ffb45c990eb23b491d4b4b6182f95")?; - assert!(browser - .history - .find(|commit| if commit.id == commit1 { - Some(commit.clone()) - } else { - None - }) - .is_some()); - - Ok(()) - } - - #[test] - fn tag() -> Result<(), Error> { - let repo = Repository::new("./data/git-platinum")?; - let mut browser = Browser::new(&repo, Branch::local("master"))?; - browser.rev(TagName::new("v0.2.0"))?; - - let commit1 = Oid::from_str("2429f097664f9af0c5b7b389ab998b2199ffa977")?; - assert_eq!(browser.history.first().id, commit1); - - Ok(()) - } - } - - #[cfg(test)] - mod last_commit { - use crate::{ - file_system::{unsound, Path}, - vcs::git::{Branch, Browser, Oid, Repository}, - }; - use std::str::FromStr; - - #[test] - fn readme_missing_and_memory() { - let repo = Repository::new("./data/git-platinum") - .expect("Could not retrieve ./data/git-platinum as git repository"); - let mut browser = - Browser::new(&repo, Branch::local("master")).expect("Could not initialise Browser"); - - // Set the browser history to the initial commit - let commit = Oid::from_str("d3464e33d75c75c99bfb90fa2e9d16efc0b7d0e3") - .expect("Failed to parse SHA"); - browser.commit(commit).unwrap(); - - let head_commit = browser.get().0.first().clone(); - - // memory.rs is commited later so it should not exist here. - let memory_last_commit = browser - .last_commit(Path::with_root(&[ - unsound::label::new("src"), - unsound::label::new("memory.rs"), - ])) - .expect("Failed to get last commit") - .map(|commit| commit.id); - - assert_eq!(memory_last_commit, None); - - // README.md exists in this commit. - let readme_last_commit = browser - .last_commit(Path::with_root(&[unsound::label::new("README.md")])) - .expect("Failed to get last commit") - .map(|commit| commit.id); - - assert_eq!(readme_last_commit, Some(head_commit.id)); - } - - #[test] - fn folder_svelte() { - let repo = Repository::new("./data/git-platinum") - .expect("Could not retrieve ./data/git-platinum as git repository"); - let mut browser = - Browser::new(&repo, Branch::local("master")).expect("Could not initialise Browser"); - - // Check that last commit is the actual last commit even if head commit differs. - let commit = Oid::from_str("19bec071db6474af89c866a1bd0e4b1ff76e2b97") - .expect("Could not parse SHA"); - browser.commit(commit).unwrap(); - - let expected_commit_id = - Oid::from_str("f3a089488f4cfd1a240a9c01b3fcc4c34a4e97b2").unwrap(); - - let folder_svelte = browser - .last_commit(unsound::path::new("~/examples/Folder.svelte")) - .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("./data/git-platinum") - .expect("Could not retrieve ./data/git-platinum as git repository"); - let mut browser = - Browser::new(&repo, Branch::local("master")).expect("Could not initialise Browser"); - - // Check that last commit is the actual last commit even if head commit differs. - let commit = Oid::from_str("19bec071db6474af89c866a1bd0e4b1ff76e2b97") - .expect("Failed to parse SHA"); - browser.commit(commit).unwrap(); - - let expected_commit_id = - Oid::from_str("2429f097664f9af0c5b7b389ab998b2199ffa977").unwrap(); - - let nested_directory_tree_commit_id = browser - .last_commit(unsound::path::new( - "~/this/is/a/really/deeply/nested/directory/tree", - )) - .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("./data/git-platinum") - .expect("Could not retrieve ./data/git-platinum as git repository"); - let mut browser = - Browser::new(&repo, Branch::local("master")).expect("Could not initialise Browser"); - - // Check that last commit is the actual last commit even if head commit differs. - let commit = Oid::from_str("a0dd9122d33dff2a35f564d564db127152c88e02") - .expect("Failed to parse SHA"); - browser.commit(commit).unwrap(); - - let expected_commit_id = - Oid::from_str("a0dd9122d33dff2a35f564d564db127152c88e02").unwrap(); - - let backslash_commit_id = browser - .last_commit(unsound::path::new("~/special/faux\\path")) - .expect("Failed to get last commit") - .map(|commit| commit.id); - assert_eq!(backslash_commit_id, Some(expected_commit_id)); - - let ogre_commit_id = browser - .last_commit(unsound::path::new("~/special/👹👹👹")) - .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("./data/git-platinum") - .expect("Could not retrieve ./data/git-platinum as git repository"); - let browser = - Browser::new(&repo, Branch::local("master")).expect("Could not initialise Browser"); - - let root_last_commit_id = browser - .last_commit(Path::root()) - .expect("Failed to get last commit") - .map(|commit| commit.id); - - assert_eq!(root_last_commit_id, Some(browser.get().first().id)); - } - } - - #[cfg(test)] - mod diff { - use crate::{diff::*, vcs::git::*}; - use std::str::FromStr; - - #[test] - fn test_initial_diff() -> Result<(), Error> { - use file_system::*; - use pretty_assertions::assert_eq; - - let oid = Oid::from_str("d3464e33d75c75c99bfb90fa2e9d16efc0b7d0e3")?; - let repo = Repository::new("./data/git-platinum")?; - let commit = repo.0.find_commit(oid.into()).unwrap(); - - assert!(commit.parents().count() == 0); - assert!(commit.parent(0).is_err()); - - let bro = Browser::new(&repo, Branch::local("master"))?; - let diff = bro.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(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> { - use file_system::*; - use pretty_assertions::assert_eq; - - let repo = Repository::new("./data/git-platinum")?; - let commit = repo - .0 - .find_commit(git2::Oid::from_str( - "80bacafba303bf0cdf6142921f430ff265f25095", - )?) - .unwrap(); - let parent = commit.parent(0)?; - - let bro = Browser::new(&repo, Branch::local("master"))?; - - let diff = bro.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(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> { - use file_system::*; - - 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(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 crate::vcs::git::*; - use pretty_assertions::assert_eq; - use std::sync::{Mutex, MutexGuard}; - - #[test] - fn basic_test() -> Result<(), Error> { - let shared_repo = Mutex::new(Repository::new("./data/git-platinum")?); - let locked_repo: MutexGuard = shared_repo.lock().unwrap(); - let bro = Browser::new(&*locked_repo, Branch::local("master"))?; - let mut branches = bro.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(()) - } - } -} diff --git a/radicle-surf/src/vcs/git/branch.rs b/radicle-surf/src/vcs/git/branch.rs index 423a06f..f7c2b60 100644 --- a/radicle-surf/src/vcs/git/branch.rs +++ b/radicle-surf/src/vcs/git/branch.rs @@ -197,34 +197,3 @@ impl<'repo> TryFrom> for Branch { } } } - -#[cfg(feature = "serialize")] -#[cfg(test)] -pub mod tests { - use proptest::prelude::*; - use test_helpers::roundtrip; - - use super::{Branch, BranchName, BranchType}; - - 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), - }, - }) - ] - } -} diff --git a/radicle-surf/src/vcs/git/commit.rs b/radicle-surf/src/vcs/git/commit.rs index 84b36ee..82fc1aa 100644 --- a/radicle-surf/src/vcs/git/commit.rs +++ b/radicle-surf/src/vcs/git/commit.rs @@ -20,7 +20,7 @@ use radicle_git_ext::Oid; use std::{convert::TryFrom, str}; #[cfg(feature = "serialize")] -use serde::{de, ser::SerializeStruct, Deserialize, Deserializer, Serialize, Serializer}; +use serde::{ser::SerializeStruct, Deserialize, Deserializer, Serialize, Serializer}; /// `Author` is the static information of a [`git2::Signature`]. #[cfg_attr(feature = "serialize", derive(Deserialize, Serialize))] @@ -166,40 +166,3 @@ impl<'repo> TryFrom> for Commit { }) } } - -#[cfg(feature = "serialize")] -#[cfg(test)] -pub mod tests { - use proptest::prelude::*; - use radicle_git_ext::Oid; - use test_helpers::roundtrip; - - use super::{Author, Commit}; - - #[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()], - }) - } -} diff --git a/radicle-surf/src/vcs/git/ext.rs b/radicle-surf/src/vcs/git/ext.rs index 6994b63..d065881 100644 --- a/radicle-surf/src/vcs/git/ext.rs +++ b/radicle-surf/src/vcs/git/ext.rs @@ -15,6 +15,8 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . +#![allow(missing_docs)] + /// Try to strip any refs/namespaces, refs/heads, refs/remotes, and /// refs/tags. If this fails we return the original string. pub fn try_extract_refname(spec: &str) -> Result { @@ -64,48 +66,3 @@ pub fn is_remote(reference: &git2::Reference) -> bool { None => false, } } - -#[cfg(test)] -mod tests { - use super::*; - - #[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()) - ); - } -} diff --git a/radicle-surf/src/vcs/git/reference.rs b/radicle-surf/src/vcs/git/reference.rs index 3b93eaa..0c1d423 100644 --- a/radicle-surf/src/vcs/git/reference.rs +++ b/radicle-surf/src/vcs/git/reference.rs @@ -118,8 +118,10 @@ impl fmt::Display for Ref { } } +/// Error when parsing a ref. #[derive(Debug, PartialEq, Eq, Error)] pub enum ParseError { + /// The parsed ref is malformed. #[error("the ref provided '{0}' was malformed")] MalformedRef(String), } @@ -196,107 +198,3 @@ impl str::FromStr for Ref { parser::parse(reference).map_err(|_| ParseError::MalformedRef(reference.to_owned())) } } - -#[cfg(test)] -mod tests { - use super::*; - 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(()) - } -} diff --git a/radicle-surf/src/vcs/git/repo.rs b/radicle-surf/src/vcs/git/repo.rs index 54afa79..47f08f1 100644 --- a/radicle-surf/src/vcs/git/repo.rs +++ b/radicle-surf/src/vcs/git/repo.rs @@ -182,8 +182,8 @@ impl<'a> RepositoryRef<'a> { Ok(self.repo_ref.set_namespace(namespace)?) } - /// Get a particular `Commit`. - pub(super) fn get_commit(&self, oid: Oid) -> Result, Error> { + /// Get a particular `git2::Commit` of `oid`. + pub fn get_git2_commit(&self, oid: Oid) -> Result, Error> { let commit = self.repo_ref.find_commit(oid.into())?; Ok(commit) } @@ -365,7 +365,7 @@ impl<'a> Vcs for RepositoryRef<'a> { match history_id { Rev::Ref(reference) => self.reference(reference, |_| None), Rev::Oid(oid) => { - let commit = self.get_commit(oid)?; + let commit = self.get_git2_commit(oid)?; self.commit_to_history(commit) }, } diff --git a/radicle-surf/t/Cargo.toml b/radicle-surf/t/Cargo.toml new file mode 100644 index 0000000..19ce3f2 --- /dev/null +++ b/radicle-surf/t/Cargo.toml @@ -0,0 +1,35 @@ +[package] +name = "radicle-surf-test" +version = "0.1.0" +edition = "2021" +license = "GPL-3.0-or-later" + +publish = false + +[lib] +test = true + +[features] +serialize = [] +test = [] + +[dev-dependencies] +nonempty = "0.5" +pretty_assertions = "1.3.0" +proptest = "0.9" +serde_json = "1" + +[dev-dependencies.git2] +version = "0.15.0" +default-features = false +features = ["vendored-libgit2"] + +[dev-dependencies.radicle-git-ext] +path = "../../radicle-git-ext" + +[dev-dependencies.radicle-surf] +path = ".." +features = ["serialize"] + +[dev-dependencies.test-helpers] +path = "../../test/test-helpers" diff --git a/radicle-surf/t/src/diff.rs b/radicle-surf/t/src/diff.rs new file mode 100644 index 0000000..02d83d1 --- /dev/null +++ b/radicle-surf/t/src/diff.rs @@ -0,0 +1,298 @@ +// Copyright © 2022 The Radicle Git Contributors +// SPDX-License-Identifier: GPL-3.0-or-later + +//! Unit tests for radicle_surf::diff + +use pretty_assertions::assert_eq; +use radicle_surf::{ + diff::*, + file_system::{unsound, *}, +}; + +#[test] +fn test_create_file() { + let directory = Directory::root(); + + let mut new_directory = Directory::root(); + new_directory.insert_file(unsound::path::new("banana.rs"), File::new(b"use banana")); + + let diff = Diff::diff(directory, new_directory); + + let expected_diff = Diff { + created: vec![CreateFile { + path: Path::with_root(&[unsound::label::new("banana.rs")]), + diff: FileDiff::Plain { + hunks: Hunks::default(), + }, + }], + deleted: vec![], + copied: vec![], + moved: vec![], + modified: vec![], + }; + + assert_eq!(diff, expected_diff) +} + +#[test] +fn test_delete_file() { + let mut directory = Directory::root(); + directory.insert_file(unsound::path::new("banana.rs"), File::new(b"use banana")); + + let new_directory = Directory::root(); + + let diff = Diff::diff(directory, new_directory); + + let expected_diff = Diff { + created: vec![], + deleted: vec![DeleteFile { + path: Path::with_root(&[unsound::label::new("banana.rs")]), + diff: FileDiff::Plain { + hunks: Hunks::default(), + }, + }], + moved: vec![], + copied: vec![], + modified: vec![], + }; + + assert_eq!(diff, expected_diff) +} + +/* TODO(fintan): Move is not detected yet +#[test] +fn test_moved_file() { + let mut directory = Directory::root(); + directory.insert_file(&unsound::path::new("mod.rs"), File::new(b"use banana")); + + let mut new_directory = Directory::root(); + new_directory.insert_file(&unsound::path::new("banana.rs"), File::new(b"use banana")); + + let diff = Diff::diff(directory, new_directory).expect("diff failed"); + + assert_eq!(diff, Diff::new()) +} +*/ + +#[test] +fn test_modify_file() { + let mut directory = Directory::root(); + directory.insert_file(unsound::path::new("banana.rs"), File::new(b"use banana")); + + let mut new_directory = Directory::root(); + new_directory.insert_file(unsound::path::new("banana.rs"), File::new(b"use banana;")); + + let diff = Diff::diff(directory, new_directory); + + let expected_diff = Diff { + created: vec![], + deleted: vec![], + moved: vec![], + copied: vec![], + modified: vec![ModifiedFile { + path: Path::with_root(&[unsound::label::new("banana.rs")]), + diff: FileDiff::Plain { + hunks: Hunks::default(), + }, + eof: None, + }], + }; + + assert_eq!(diff, expected_diff) +} + +#[test] +fn test_create_directory() { + let directory = Directory::root(); + + let mut new_directory = Directory::root(); + new_directory.insert_file( + unsound::path::new("src/banana.rs"), + File::new(b"use banana"), + ); + + let diff = Diff::diff(directory, new_directory); + + let expected_diff = Diff { + created: vec![CreateFile { + path: Path::with_root(&[unsound::label::new("src"), unsound::label::new("banana.rs")]), + diff: FileDiff::Plain { + hunks: Hunks::default(), + }, + }], + deleted: vec![], + moved: vec![], + copied: vec![], + modified: vec![], + }; + + assert_eq!(diff, expected_diff) +} + +#[test] +fn test_delete_directory() { + let mut directory = Directory::root(); + directory.insert_file( + unsound::path::new("src/banana.rs"), + File::new(b"use banana"), + ); + + let new_directory = Directory::root(); + + let diff = Diff::diff(directory, new_directory); + + let expected_diff = Diff { + created: vec![], + deleted: vec![DeleteFile { + path: Path::with_root(&[unsound::label::new("src"), unsound::label::new("banana.rs")]), + diff: FileDiff::Plain { + hunks: Hunks::default(), + }, + }], + moved: vec![], + copied: vec![], + modified: vec![], + }; + + assert_eq!(diff, expected_diff) +} + +#[test] +fn test_modify_file_directory() { + let mut directory = Directory::root(); + directory.insert_file( + unsound::path::new("src/banana.rs"), + File::new(b"use banana"), + ); + + let mut new_directory = Directory::root(); + new_directory.insert_file( + unsound::path::new("src/banana.rs"), + File::new(b"use banana;"), + ); + + let diff = Diff::diff(directory, new_directory); + + let expected_diff = Diff { + created: vec![], + deleted: vec![], + moved: vec![], + copied: vec![], + modified: vec![ModifiedFile { + path: Path::with_root(&[unsound::label::new("src"), unsound::label::new("banana.rs")]), + diff: FileDiff::Plain { + hunks: Hunks::default(), + }, + eof: None, + }], + }; + + assert_eq!(diff, expected_diff) +} + +/* TODO(fintan): Tricky stuff +#[test] +fn test_disjoint_directories() { + let mut directory = Directory::root(); + directory.insert_file( + &unsound::path::new("foo/src/banana.rs"), + File::new(b"use banana"), + ); + + let mut other_directory = Directory::root(); + other_directory.insert_file( + &unsound::path::new("bar/src/pineapple.rs"), + File::new(b"use pineapple"), + ); + + let diff = Diff::diff(directory, other_directory).expect("diff failed"); + + let expected_diff = Diff { + created: vec![CreateFile(Path::from_labels( + unsound::label::new("bar"), + &[ + unsound::label::new("src"), + unsound::label::new("pineapple.rs"), + ], + ))], + deleted: vec![DeleteFile(Path::from_labels( + unsound::label::new("foo"), + &[unsound::label::new("src"), unsound::label::new("banana.rs")], + ))], + moved: vec![], + modified: vec![], + }; + + assert_eq!(diff, expected_diff) +} +*/ + +#[test] +fn test_both_missing_eof_newline() { + let buf = r#" +diff --git a/.env b/.env +index f89e4c0..7c56eb7 100644 +--- a/.env ++++ b/.env +@@ -1 +1 @@ +-hello=123 +\ No newline at end of file ++hello=1234 +\ No newline at end of file +"#; + let diff = git2::Diff::from_buffer(buf.as_bytes()).unwrap(); + let diff = Diff::try_from(diff).unwrap(); + assert_eq!(diff.modified[0].eof, Some(EofNewLine::BothMissing)); +} + +#[test] +fn test_none_missing_eof_newline() { + let buf = r#" +diff --git a/.env b/.env +index f89e4c0..7c56eb7 100644 +--- a/.env ++++ b/.env +@@ -1 +1 @@ +-hello=123 ++hello=1234 +"#; + let diff = git2::Diff::from_buffer(buf.as_bytes()).unwrap(); + let diff = Diff::try_from(diff).unwrap(); + assert_eq!(diff.modified[0].eof, None); +} + +// TODO(xphoniex): uncomment once libgit2 has fixed the bug +//#[test] +// fn test_old_missing_eof_newline() { +// let buf = r#" +// diff --git a/.env b/.env +// index f89e4c0..7c56eb7 100644 +// --- a/.env +// +++ b/.env +// @@ -1 +1 @@ +// -hello=123 +// \ No newline at end of file +// +hello=1234 +// "#; +// let diff = git2::Diff::from_buffer(buf.as_bytes()).unwrap(); +// let diff = Diff::try_from(diff).unwrap(); +// assert_eq!(diff.modified[0].eof, Some(EofNewLine::OldMissing)); +// } + +// TODO(xphoniex): uncomment once libgit2 has fixed the bug +//#[test] +// fn test_new_missing_eof_newline() { +// let buf = r#" +// diff --git a/.env b/.env +// index f89e4c0..7c56eb7 100644 +// --- a/.env +// +++ b/.env +// @@ -1 +1 @@ +// -hello=123 +// +hello=1234 +// \ No newline at end of file +// "#; +// let diff = git2::Diff::from_buffer(buf.as_bytes()).unwrap(); +// let diff = Diff::try_from(diff).unwrap(); +// assert_eq!(diff.modified[0].eof, Some(EofNewLine::NewMissing)); +// } diff --git a/radicle-surf/t/src/file_system.rs b/radicle-surf/t/src/file_system.rs new file mode 100644 index 0000000..76479da --- /dev/null +++ b/radicle-surf/t/src/file_system.rs @@ -0,0 +1,222 @@ +// Copyright © 2022 The Radicle Git Contributors +// SPDX-License-Identifier: GPL-3.0-or-later + +//! Unit tests for radicle_surf::file_system + +#[cfg(test)] +mod list_directory { + use radicle_surf::file_system::{unsound, Directory, File, SystemType}; + + #[test] + fn root_files() { + let mut directory = Directory::root(); + directory.insert_file( + unsound::path::new("foo.hs"), + File::new(b"module BananaFoo ..."), + ); + directory.insert_file( + unsound::path::new("bar.hs"), + File::new(b"module BananaBar ..."), + ); + directory.insert_file( + unsound::path::new("baz.hs"), + File::new(b"module BananaBaz ..."), + ); + + assert_eq!( + directory.list_directory(), + vec![ + SystemType::file(unsound::label::new("bar.hs")), + SystemType::file(unsound::label::new("baz.hs")), + SystemType::file(unsound::label::new("foo.hs")), + ] + ); + } +} + +#[cfg(test)] +mod find_file { + use radicle_surf::file_system::{unsound, *}; + + #[test] + fn in_root() { + let file = File::new(b"module Banana ..."); + let mut directory = Directory::root(); + directory.insert_file(unsound::path::new("foo.hs"), file.clone()); + + assert_eq!( + directory.find_file(unsound::path::new("foo.hs")), + Some(file) + ); + } + + #[test] + fn file_does_not_exist() { + let file_path = unsound::path::new("bar.hs"); + + let file = File::new(b"module Banana ..."); + + let mut directory = Directory::root(); + directory.insert_file(unsound::path::new("foo.hs"), file); + + assert_eq!(directory.find_file(file_path), None) + } +} + +#[cfg(test)] +mod directory_size { + use nonempty::NonEmpty; + use radicle_surf::file_system::{unsound, Directory, File}; + + #[test] + fn root_directory_files() { + let mut root = Directory::root(); + root.insert_files( + &[], + NonEmpty::from(( + ( + unsound::label::new("main.rs"), + File::new(b"println!(\"Hello, world!\")"), + ), + vec![( + unsound::label::new("lib.rs"), + File::new(b"struct Hello(String)"), + )], + )), + ); + + assert_eq!(root.size(), 45); + } +} + +#[cfg(test)] +mod properties { + use nonempty::NonEmpty; + use proptest::{collection, prelude::*}; + use radicle_surf::file_system::{unsound, *}; + use std::collections::HashMap; + + #[test] + fn test_all_directories_and_files() { + let mut directory_map = HashMap::new(); + + let path1 = unsound::path::new("foo/bar/baz"); + let file1 = (unsound::label::new("monadic.rs"), File::new(&[])); + let file2 = (unsound::label::new("oscoin.rs"), File::new(&[])); + directory_map.insert(path1, NonEmpty::from((file1, vec![file2]))); + + let path2 = unsound::path::new("foor/bar/quuz"); + let file3 = (unsound::label::new("radicle.rs"), File::new(&[])); + + directory_map.insert(path2, NonEmpty::new(file3)); + + assert!(prop_all_directories_and_files(directory_map)) + } + + fn label_strategy() -> impl Strategy { + // ASCII regex, excluding '/' because of posix file paths + "[ -.|0-~]+".prop_map(|label| unsound::label::new(&label)) + } + + fn path_strategy(max_size: usize) -> impl Strategy { + ( + label_strategy(), + collection::vec(label_strategy(), 0..max_size), + ) + .prop_map(|(label, labels)| Path((label, labels).into())) + } + + fn file_strategy() -> impl Strategy { + // ASCII regex, see: https://catonmat.net/my-favorite-regex + (label_strategy(), "[ -~]*") + .prop_map(|(name, contents)| (name, File::new(contents.as_bytes()))) + } + + fn directory_map_strategy( + path_size: usize, + n_files: usize, + map_size: usize, + ) -> impl Strategy>> { + collection::hash_map( + path_strategy(path_size), + collection::vec(file_strategy(), 1..n_files).prop_map(|files| { + NonEmpty::from_slice(&files).expect("Strategy generated files of length 0") + }), + 0..map_size, + ) + } + + // TODO(fintan): This is a bit slow. Could be time to benchmark some functions. + proptest! { + #[test] + fn prop_test_all_directories_and_files(directory_map in directory_map_strategy(10, 10, 10)) { + prop_all_directories_and_files(directory_map); + } + } + + fn prop_all_directories_and_files( + directory_map: HashMap>, + ) -> bool { + let mut new_directory_map = HashMap::new(); + for (path, files) in directory_map { + new_directory_map.insert(path.clone(), files); + } + + let directory = Directory::from_hash_map(new_directory_map.clone()); + + for (directory_path, files) in new_directory_map { + for (file_name, _) in files.iter() { + let mut path = directory_path.clone(); + if directory.find_directory(path.clone()).is_none() { + eprintln!("Search Directory: {:#?}", directory); + eprintln!("Path to find: {:#?}", path); + return false; + } + + path.push(file_name.clone()); + if directory.find_file(path.clone()).is_none() { + eprintln!("Search Directory: {:#?}", directory); + eprintln!("Path to find: {:#?}", path); + return false; + } + } + } + true + } + + #[test] + fn test_file_name_is_same_as_root() { + // This test ensures that if the name is the same the root of the + // directory, that search_path.split_last() doesn't toss away the prefix. + let path = unsound::path::new("foo/bar/~"); + let mut directory_map = HashMap::new(); + directory_map.insert(path, NonEmpty::new((Label::root(), File::new(b"root")))); + + assert!(prop_all_directories_and_files(directory_map)); + } +} + +#[cfg(test)] +mod path { + use radicle_surf::file_system::unsound; + + #[test] + fn split_last_root_and_foo() { + let path = unsound::path::new("foo"); + assert_eq!(path.split_last(), (vec![], unsound::label::new("foo"))); + } + + #[test] + fn split_last_same_labels() { + // An interesting case for when first == last, but doesn't imply a singleton + // Path. + let path = unsound::path::new("foo/bar/foo"); + assert_eq!( + path.split_last(), + ( + vec![unsound::label::new("foo"), unsound::label::new("bar")], + unsound::label::new("foo") + ) + ); + } +} diff --git a/radicle-surf/t/src/git.rs b/radicle-surf/t/src/git.rs new file mode 100644 index 0000000..1f09f1b --- /dev/null +++ b/radicle-surf/t/src/git.rs @@ -0,0 +1,811 @@ +// 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, + Browser, + Namespace, + Oid, + RefScope, + Repository, + 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(); + let browser = Browser::new(&repo, Branch::local("main")).unwrap(); + + browser.get_directory().unwrap(); +} + +#[cfg(test)] +mod namespace { + use super::*; + use pretty_assertions::{assert_eq, assert_ne}; + + #[test] + fn switch_to_banana() -> Result<(), Error> { + let repo = Repository::new(GIT_PLATINUM)?; + let mut browser = Browser::new_with_namespace( + &repo, + &Namespace::try_from("golden")?, + Branch::local("master"), + )?; + let history = browser.get(); + + browser.branch(Branch::local("banana"))?; + + assert_ne!(history, browser.get()); + + Ok(()) + } + + #[test] + fn me_namespace() -> Result<(), Error> { + let repo = Repository::new(GIT_PLATINUM)?; + let browser = Browser::new(&repo, Branch::local("master"))?; + let history = browser.get(); + + assert_eq!(browser.which_namespace(), Ok(None)); + + let browser = browser + .switch_namespace(&Namespace::try_from("me")?, Branch::local("feature/#1194"))?; + + assert_eq!( + browser.which_namespace(), + Ok(Some(Namespace::try_from("me")?)) + ); + assert_eq!(history, browser.get()); + + let expected_branches: Vec = vec![Branch::local("feature/#1194")]; + let mut branches = browser.list_branches(RefScope::Local)?; + branches.sort(); + + assert_eq!(expected_branches, branches); + + let expected_branches: Vec = vec![Branch::remote("feature/#1194", "fein")]; + let mut branches = browser.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 browser = Browser::new(&repo, Branch::local("master"))?; + let history = browser.get(); + + assert_eq!(browser.which_namespace(), Ok(None)); + + let golden_browser = + browser.switch_namespace(&Namespace::try_from("golden")?, Branch::local("master"))?; + + assert_eq!( + golden_browser.which_namespace(), + Ok(Some(Namespace::try_from("golden")?)) + ); + assert_eq!(history, golden_browser.get()); + + let expected_branches: Vec = vec![Branch::local("banana"), Branch::local("master")]; + let mut branches = golden_browser.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 = golden_browser.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 browser = Browser::new(&repo, Branch::local("master"))?; + let history = browser.get(); + + assert_eq!(browser.which_namespace(), Ok(None)); + + let silver_browser = browser.switch_namespace( + &Namespace::try_from("golden/silver")?, + Branch::local("master"), + )?; + + assert_eq!( + silver_browser.which_namespace(), + Ok(Some(Namespace::try_from("golden/silver")?)) + ); + assert_ne!(history, silver_browser.get()); + + let expected_branches: Vec = vec![Branch::local("master")]; + let mut branches = silver_browser.list_branches(RefScope::All)?; + branches.sort(); + + assert_eq!(expected_branches, branches); + + Ok(()) + } +} + +#[cfg(test)] +mod rev { + 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 mut browser = Browser::new(&repo, Branch::local("master"))?; + browser.rev(Branch::remote("master", "origin"))?; + + let commit1 = Oid::from_str("3873745c8f6ffb45c990eb23b491d4b4b6182f95")?; + assert!( + browser + .as_history() + .find(|commit| if commit.id == commit1 { + Some(commit.clone()) + } else { + None + }) + .is_some(), + "commit_id={}, history =\n{:#?}", + commit1, + browser.as_history() + ); + + let commit2 = Oid::from_str("d6880352fc7fda8f521ae9b7357668b17bb5bad5")?; + assert!( + browser + .as_history() + .find(|commit| if commit.id == commit2 { + Some(commit.clone()) + } else { + None + }) + .is_some(), + "commit_id={}, history =\n{:#?}", + commit2, + browser.as_history() + ); + + Ok(()) + } + + #[test] + fn commit() -> Result<(), Error> { + let repo = Repository::new(GIT_PLATINUM)?; + let mut browser = Browser::new(&repo, Branch::local("master"))?; + browser.rev(Oid::from_str("3873745c8f6ffb45c990eb23b491d4b4b6182f95")?)?; + + let commit1 = Oid::from_str("3873745c8f6ffb45c990eb23b491d4b4b6182f95")?; + assert!(browser + .as_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 mut browser = Browser::new(&repo, Branch::local("master"))?; + browser.rev(Oid::from_str("3873745c8f6ffb45c990eb23b491d4b4b6182f95")?)?; + let commit = browser.as_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 mut browser = Browser::new(&repo, Branch::local("master"))?; + browser.rev(browser.oid("3873745c8")?)?; + + let commit1 = Oid::from_str("3873745c8f6ffb45c990eb23b491d4b4b6182f95")?; + assert!(browser + .as_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 mut browser = Browser::new(&repo, Branch::local("master"))?; + browser.rev(TagName::new("v0.2.0"))?; + + let commit1 = Oid::from_str("2429f097664f9af0c5b7b389ab998b2199ffa977")?; + assert_eq!(browser.as_history().first().id, commit1); + + Ok(()) + } +} + +#[cfg(test)] +mod last_commit { + 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 mut browser = + Browser::new(&repo, Branch::local("master")).expect("Could not initialise Browser"); + + // Set the browser history to the initial commit + let commit = + Oid::from_str("d3464e33d75c75c99bfb90fa2e9d16efc0b7d0e3").expect("Failed to parse SHA"); + browser.commit(commit).unwrap(); + + let head_commit = browser.get().0.first().clone(); + + // memory.rs is commited later so it should not exist here. + let memory_last_commit = browser + .last_commit(Path::with_root(&[ + unsound::label::new("src"), + unsound::label::new("memory.rs"), + ])) + .expect("Failed to get last commit") + .map(|commit| commit.id); + + assert_eq!(memory_last_commit, None); + + // README.md exists in this commit. + let readme_last_commit = browser + .last_commit(Path::with_root(&[unsound::label::new("README.md")])) + .expect("Failed to get last commit") + .map(|commit| commit.id); + + assert_eq!(readme_last_commit, Some(head_commit.id)); + } + + #[test] + fn folder_svelte() { + let repo = Repository::new(GIT_PLATINUM) + .expect("Could not retrieve ./data/git-platinum as git repository"); + let mut browser = + Browser::new(&repo, Branch::local("master")).expect("Could not initialise Browser"); + + // Check that last commit is the actual last commit even if head commit differs. + let commit = + Oid::from_str("19bec071db6474af89c866a1bd0e4b1ff76e2b97").expect("Could not parse SHA"); + browser.commit(commit).unwrap(); + + let expected_commit_id = Oid::from_str("f3a089488f4cfd1a240a9c01b3fcc4c34a4e97b2").unwrap(); + + let folder_svelte = browser + .last_commit(unsound::path::new("~/examples/Folder.svelte")) + .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"); + let mut browser = + Browser::new(&repo, Branch::local("master")).expect("Could not initialise Browser"); + + // Check that last commit is the actual last commit even if head commit differs. + let commit = + Oid::from_str("19bec071db6474af89c866a1bd0e4b1ff76e2b97").expect("Failed to parse SHA"); + browser.commit(commit).unwrap(); + + let expected_commit_id = Oid::from_str("2429f097664f9af0c5b7b389ab998b2199ffa977").unwrap(); + + let nested_directory_tree_commit_id = browser + .last_commit(unsound::path::new( + "~/this/is/a/really/deeply/nested/directory/tree", + )) + .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"); + let mut browser = + Browser::new(&repo, Branch::local("master")).expect("Could not initialise Browser"); + + // Check that last commit is the actual last commit even if head commit differs. + let commit = + Oid::from_str("a0dd9122d33dff2a35f564d564db127152c88e02").expect("Failed to parse SHA"); + browser.commit(commit).unwrap(); + + let expected_commit_id = Oid::from_str("a0dd9122d33dff2a35f564d564db127152c88e02").unwrap(); + + let backslash_commit_id = browser + .last_commit(unsound::path::new("~/special/faux\\path")) + .expect("Failed to get last commit") + .map(|commit| commit.id); + assert_eq!(backslash_commit_id, Some(expected_commit_id)); + + let ogre_commit_id = browser + .last_commit(unsound::path::new("~/special/👹👹👹")) + .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 browser = + Browser::new(&repo, Branch::local("master")).expect("Could not initialise Browser"); + + let root_last_commit_id = browser + .last_commit(Path::root()) + .expect("Failed to get last commit") + .map(|commit| commit.id); + + assert_eq!(root_last_commit_id, Some(browser.get().first().id)); + } +} + +#[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 commit = repo.0.find_commit(oid).unwrap(); + let commit = repo.as_ref().get_git2_commit(oid).unwrap(); + + assert!(commit.parents().count() == 0); + assert!(commit.parent(0).is_err()); + + let bro = Browser::new(&repo, Branch::local("master"))?; + let diff = bro.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 commit = repo + .as_ref() + .get_git2_commit(Oid::from_str("80bacafba303bf0cdf6142921f430ff265f25095")?) + .unwrap(); + let parent = commit.parent(0)?; + + let bro = Browser::new(&repo, Branch::local("master"))?; + + let diff = bro.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 bro = Browser::new(&*locked_repo, Branch::local("master"))?; + let mut branches = bro.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(()) + } +} diff --git a/radicle-surf/t/src/lib.rs b/radicle-surf/t/src/lib.rs new file mode 100644 index 0000000..e3259f4 --- /dev/null +++ b/radicle-surf/t/src/lib.rs @@ -0,0 +1,14 @@ +// Copyright © 2022 The Radicle Git Contributors +// SPDX-License-Identifier: GPL-3.0-or-later + +#[cfg(test)] +mod git; + +#[cfg(test)] +mod diff; + +#[cfg(test)] +mod file_system; + +#[cfg(test)] +mod tree; diff --git a/radicle-surf/t/src/tree.rs b/radicle-surf/t/src/tree.rs new file mode 100644 index 0000000..0124022 --- /dev/null +++ b/radicle-surf/t/src/tree.rs @@ -0,0 +1,692 @@ +// Copyright © 2022 The Radicle Git Contributors +// SPDX-License-Identifier: GPL-3.0-or-later + +//! Unit tests for radicle_surf::tree. + +use nonempty::NonEmpty; +use pretty_assertions::assert_eq; +use radicle_surf::tree::{Forest, SubTree, Tree}; + +#[derive(Debug, Clone, PartialEq, Eq)] +struct TestNode { + id: u32, +} + +#[test] +fn test_is_empty() { + let mut tree = Forest::root(); + assert!(tree.is_empty()); + + let a_node = TestNode { id: 1 }; + + tree.insert(NonEmpty::new(String::from("a")), a_node); + assert!(!tree.is_empty()); +} + +#[test] +fn test_insert_root_node() { + let a_label = String::from("a"); + + let mut tree = Forest::root(); + + let a_node = TestNode { id: 1 }; + + tree.insert(NonEmpty::new(a_label), a_node.clone()); + + assert_eq!(tree, Forest(Some(Tree::node(String::from("a"), a_node)))); +} + +#[test] +fn test_insert_with_prepending_root_nodes() { + let a_label = String::from("a"); + + let mut tree = Forest::root(); + + let a_node = TestNode { id: 1 }; + let b_node = TestNode { id: 2 }; + + tree.insert_with( + NonEmpty::new(a_label.clone()), + NonEmpty::new(a_node.clone()), + |nodes| nodes.insert(0, a_node.clone()), + ); + tree.insert_with( + NonEmpty::new(a_label), + NonEmpty::new(b_node.clone()), + |nodes| nodes.insert(0, b_node.clone()), + ); + + assert_eq!( + tree, + Forest(Some(Tree::node( + String::from("a"), + NonEmpty::from((b_node, vec![a_node])) + ))) + ); +} + +#[test] +fn test_insert_with_prepending_branch_nodes() { + let a_label = String::from("a"); + let b_label = String::from("b"); + let path = NonEmpty::from((a_label, vec![b_label])); + + let mut tree = Forest::root(); + + let a_node = TestNode { id: 1 }; + let b_node = TestNode { id: 2 }; + + tree.insert_with(path.clone(), NonEmpty::new(a_node.clone()), |nodes| { + nodes.insert(0, a_node.clone()) + }); + tree.insert_with(path, NonEmpty::new(b_node.clone()), |nodes| { + nodes.insert(0, b_node.clone()) + }); + + assert_eq!( + tree, + Forest(Some(Tree::branch( + String::from("a"), + Tree::node(String::from("b"), NonEmpty::from((b_node, vec![a_node]))) + ))) + ); +} + +#[test] +fn test_insert_single_node() { + let a_label = String::from("a"); + let b_label = String::from("b"); + let c_label = String::from("c"); + let path = NonEmpty::from((a_label, vec![b_label, c_label])); + + let mut tree = Forest::root(); + + let c_node = TestNode { id: 1 }; + + tree.insert(path, c_node.clone()); + + assert_eq!( + tree, + Forest(Some(Tree::branch( + String::from("a"), + Tree::branch(String::from("b"), Tree::node(String::from("c"), c_node)) + ))) + ); +} + +#[test] +fn test_insert_two_nodes() { + let a_label = String::from("a"); + let b_label = String::from("b"); + let c_label = String::from("c"); + let d_label = String::from("d"); + let c_path = NonEmpty::from((a_label.clone(), vec![b_label.clone(), c_label])); + let d_path = NonEmpty::from((a_label, vec![b_label, d_label])); + + let mut tree = Forest::root(); + + let c_node = TestNode { id: 1 }; + + tree.insert(c_path, c_node.clone()); + + let d_node = TestNode { id: 3 }; + + tree.insert(d_path, d_node.clone()); + + assert_eq!( + tree, + Forest(Some(Tree::branch( + String::from("a"), + Tree::branch( + String::from("b"), + Tree(NonEmpty::from(( + SubTree::Node { + key: String::from("c"), + value: c_node + }, + vec![SubTree::Node { + key: String::from("d"), + value: d_node + }] + ))) + ) + ))) + ); +} + +#[test] +fn test_insert_replaces_node() { + let a_label = String::from("a"); + let b_label = String::from("b"); + let c_label = String::from("c"); + let c_path = NonEmpty::from((a_label, vec![b_label, c_label])); + + let mut tree = Forest::root(); + + let c_node = TestNode { id: 1 }; + + tree.insert(c_path.clone(), c_node); + + let new_c_node = TestNode { id: 3 }; + + tree.insert(c_path, new_c_node.clone()); + + assert_eq!( + tree, + Forest(Some(Tree::branch( + String::from("a"), + Tree::branch( + String::from("b"), + Tree(NonEmpty::new(SubTree::Node { + key: String::from("c"), + value: new_c_node + },)) + ) + ))) + ); +} + +#[test] +fn test_insert_replaces_root_node() { + let c_label = String::from("c"); + + let mut tree = Forest::root(); + + let c_node = TestNode { id: 1 }; + + tree.insert(NonEmpty::new(c_label.clone()), c_node); + + let new_c_node = TestNode { id: 3 }; + + tree.insert(NonEmpty::new(c_label), new_c_node.clone()); + + assert_eq!( + tree, + Forest(Some(Tree::node(String::from("c"), new_c_node))) + ); +} + +#[test] +fn test_insert_replaces_branch_node() { + let a_label = String::from("a"); + let c_label = String::from("c"); + let c_path = NonEmpty::from((a_label, vec![c_label])); + + let mut tree = Forest::root(); + + let c_node = TestNode { id: 1 }; + + tree.insert(c_path.clone(), c_node); + + let new_c_node = TestNode { id: 3 }; + + tree.insert(c_path, new_c_node.clone()); + + assert_eq!( + tree, + Forest(Some(Tree::branch( + String::from("a"), + Tree::node(String::from("c"), new_c_node), + ))) + ); +} + +#[test] +fn test_insert_replaces_branch_with_node() { + let a_label = String::from("a"); + let b_label = String::from("b"); + let c_label = String::from("c"); + let c_path = NonEmpty::from((a_label.clone(), vec![b_label.clone(), c_label])); + + let mut tree = Forest::root(); + + let c_node = TestNode { id: 1 }; + + tree.insert(c_path, c_node); + + let new_c_node = TestNode { id: 3 }; + + tree.insert(NonEmpty::from((a_label, vec![b_label])), new_c_node.clone()); + + assert_eq!( + tree, + Forest(Some(Tree::branch( + String::from("a"), + Tree::node(String::from("b"), new_c_node), + ))) + ); +} + +#[test] +fn test_insert_replaces_node_with_branch() { + let a_label = String::from("a"); + let b_label = String::from("b"); + let c_label = String::from("c"); + let b_path = NonEmpty::from((a_label.clone(), vec![b_label.clone()])); + let c_path = NonEmpty::from((a_label, vec![b_label, c_label])); + + let mut tree = Forest::root(); + + let b_node = TestNode { id: 1 }; + + tree.insert(b_path, b_node); + + let new_c_node = TestNode { id: 3 }; + + tree.insert(c_path, new_c_node.clone()); + + assert_eq!( + tree, + Forest(Some(Tree::branch( + String::from("a"), + Tree::branch( + String::from("b"), + Tree(NonEmpty::new(SubTree::Node { + key: String::from("c"), + value: new_c_node + },)) + ) + ))) + ); +} + +#[test] +fn test_insert_replaces_node_with_branch_foo() { + let a_label = String::from("a"); + let b_label = String::from("b"); + let c_label = String::from("c"); + let d_label = String::from("d"); + let b_path = NonEmpty::from((a_label.clone(), vec![b_label.clone()])); + let d_path = NonEmpty::from((a_label, vec![b_label, c_label, d_label])); + + let mut tree = Forest::root(); + + let b_node = TestNode { id: 1 }; + + tree.insert(b_path, b_node); + + let d_node = TestNode { id: 3 }; + + tree.insert(d_path, d_node.clone()); + + assert_eq!( + tree, + Forest(Some(Tree::branch( + String::from("a"), + Tree::branch( + String::from("b"), + Tree::branch(String::from("c"), Tree::node(String::from("d"), d_node)) + ) + ))) + ); +} + +#[test] +fn test_insert_two_nodes_out_of_order() { + let a_label = String::from("a"); + let b_label = String::from("b"); + let c_label = String::from("c"); + let d_label = String::from("d"); + let c_path = NonEmpty::from((a_label.clone(), vec![b_label.clone(), c_label])); + let d_path = NonEmpty::from((a_label, vec![b_label, d_label])); + + let mut tree = Forest::root(); + + let d_node = TestNode { id: 3 }; + + tree.insert(d_path, d_node.clone()); + + let c_node = TestNode { id: 1 }; + + tree.insert(c_path, c_node.clone()); + + assert_eq!( + tree, + Forest(Some(Tree::branch( + String::from("a"), + Tree::branch( + String::from("b"), + Tree(NonEmpty::from(( + SubTree::Node { + key: String::from("c"), + value: c_node + }, + vec![SubTree::Node { + key: String::from("d"), + value: d_node + }] + ))) + ) + ))) + ); +} + +#[test] +fn test_insert_branch() { + let a_label = String::from("a"); + let b_label = String::from("b"); + let c_label = String::from("c"); + let d_label = String::from("d"); + let e_label = String::from("e"); + let f_label = String::from("f"); + + let c_path = NonEmpty::from((a_label.clone(), vec![b_label.clone(), c_label])); + let d_path = NonEmpty::from((a_label.clone(), vec![b_label, d_label])); + let f_path = NonEmpty::from((a_label, vec![e_label, f_label])); + + let mut tree = Forest::root(); + + let c_node = TestNode { id: 1 }; + + let d_node = TestNode { id: 3 }; + + let f_node = TestNode { id: 2 }; + + tree.insert(d_path, d_node.clone()); + tree.insert(c_path, c_node.clone()); + tree.insert(f_path, f_node.clone()); + + assert_eq!( + tree, + Forest(Some(Tree::branch( + String::from("a"), + Tree(NonEmpty::from(( + SubTree::Branch { + key: String::from("b"), + forest: Box::new(Tree(NonEmpty::from(( + SubTree::Node { + key: String::from("c"), + value: c_node + }, + vec![SubTree::Node { + key: String::from("d"), + value: d_node + }] + )))) + }, + vec![SubTree::Branch { + key: String::from("e"), + forest: Box::new(Tree::node(String::from("f"), f_node)) + },] + ))) + ))) + ); +} + +#[test] +fn test_insert_two_branches() { + let a_label = String::from("a"); + let b_label = String::from("b"); + let c_label = String::from("c"); + let d_label = String::from("d"); + let e_label = String::from("e"); + let f_label = String::from("f"); + + let c_path = NonEmpty::from((a_label, vec![b_label, c_label])); + let f_path = NonEmpty::from((d_label, vec![e_label, f_label])); + + let mut tree = Forest::root(); + + let c_node = TestNode { id: 1 }; + + let f_node = TestNode { id: 2 }; + + tree.insert(c_path, c_node.clone()); + tree.insert(f_path, f_node.clone()); + + assert_eq!( + tree, + Forest(Some(Tree(NonEmpty::from(( + SubTree::Branch { + key: String::from("a"), + forest: Box::new(Tree::branch( + String::from("b"), + Tree::node(String::from("c"), c_node) + )), + }, + vec![SubTree::Branch { + key: String::from("d"), + forest: Box::new(Tree::branch( + String::from("e"), + Tree::node(String::from("f"), f_node) + )) + }] + ))))) + ); +} + +#[test] +fn test_insert_branches_and_node() { + let a_label = String::from("a"); + let b_label = String::from("b"); + let c_label = String::from("c"); + let d_label = String::from("d"); + let e_label = String::from("e"); + let f_label = String::from("f"); + let g_label = String::from("g"); + + let c_path = NonEmpty::from((a_label.clone(), vec![b_label.clone(), c_label])); + let d_path = NonEmpty::from((a_label.clone(), vec![b_label, d_label])); + let e_path = NonEmpty::from((a_label.clone(), vec![e_label])); + let g_path = NonEmpty::from((a_label, vec![f_label, g_label])); + + let mut tree = Forest::root(); + + let c_node = TestNode { id: 1 }; + + let d_node = TestNode { id: 3 }; + + let e_node = TestNode { id: 2 }; + + let g_node = TestNode { id: 2 }; + + tree.insert(d_path, d_node.clone()); + tree.insert(c_path, c_node.clone()); + tree.insert(e_path, e_node.clone()); + tree.insert(g_path, g_node.clone()); + + assert_eq!( + tree, + Forest(Some(Tree::branch( + String::from("a"), + Tree(NonEmpty::from(( + SubTree::Branch { + key: String::from("b"), + forest: Box::new(Tree(NonEmpty::from(( + SubTree::Node { + key: String::from("c"), + value: c_node + }, + vec![SubTree::Node { + key: String::from("d"), + value: d_node + }] + )))) + }, + vec![ + SubTree::Node { + key: String::from("e"), + value: e_node + }, + SubTree::Branch { + key: String::from("f"), + forest: Box::new(Tree::node(String::from("g"), g_node)) + }, + ] + ))) + ))) + ); +} + +#[test] +fn test_find_root_node() { + let a_label = String::from("a"); + + let mut tree = Forest::root(); + + let a_node = TestNode { id: 1 }; + + tree.insert(NonEmpty::new(a_label), a_node.clone()); + + assert_eq!( + tree.find(NonEmpty::new(String::from("a"))), + Some(&SubTree::Node { + key: String::from("a"), + value: a_node + }) + ); + + assert_eq!(tree.find(NonEmpty::new(String::from("b"))), None); +} + +#[test] +fn test_find_branch_and_node() { + let a_label = String::from("a"); + let b_label = String::from("b"); + let c_label = String::from("c"); + let path = NonEmpty::from((a_label, vec![b_label, c_label])); + + let mut tree = Forest::root(); + + let c_node = TestNode { id: 1 }; + + tree.insert(path, c_node.clone()); + + assert_eq!( + tree.find(NonEmpty::new(String::from("a"))), + Some(&SubTree::Branch { + key: String::from("a"), + forest: Box::new(Tree::branch( + String::from("b"), + Tree::node(String::from("c"), c_node.clone()) + )) + }) + ); + + assert_eq!( + tree.find(NonEmpty::from((String::from("a"), vec![String::from("b")]))), + Some(&SubTree::Branch { + key: String::from("b"), + forest: Box::new(Tree::node(String::from("c"), c_node.clone())) + }) + ); + + assert_eq!( + tree.find(NonEmpty::from(( + String::from("a"), + vec![String::from("b"), String::from("c")] + ))), + Some(&SubTree::Node { + key: String::from("c"), + value: c_node + }) + ); + + assert_eq!(tree.find(NonEmpty::new(String::from("b"))), None); + + assert_eq!( + tree.find(NonEmpty::from((String::from("a"), vec![String::from("c")]))), + None + ); +} + +#[test] +fn test_maximum_by_root_nodes() { + let mut tree = Forest::root(); + + let a_node = TestNode { id: 1 }; + + let b_node = TestNode { id: 3 }; + + tree.insert(NonEmpty::new(String::from("a")), a_node.clone()); + tree.insert(NonEmpty::new(String::from("b")), b_node.clone()); + + assert_eq!(tree.maximum_by(|a, b| a.id.cmp(&b.id)), Some(&b_node)); + assert_eq!( + tree.maximum_by(|a, b| a.id.cmp(&b.id).reverse()), + Some(&a_node) + ); +} + +#[test] +fn test_maximum_by_branch_and_node() { + let mut tree = Forest::root(); + + let a_node = TestNode { id: 1 }; + + let b_node = TestNode { id: 3 }; + + tree.insert( + NonEmpty::from((String::from("c"), vec![String::from("a")])), + a_node.clone(), + ); + tree.insert(NonEmpty::new(String::from("b")), b_node.clone()); + + assert_eq!(tree.maximum_by(|a, b| a.id.cmp(&b.id)), Some(&b_node)); + assert_eq!( + tree.maximum_by(|a, b| a.id.cmp(&b.id).reverse()), + Some(&a_node) + ); +} + +#[test] +fn test_maximum_by_branch_and_branch() { + let mut tree = Forest::root(); + + let a_node = TestNode { id: 1 }; + + let b_node = TestNode { id: 3 }; + + tree.insert( + NonEmpty::from((String::from("c"), vec![String::from("a")])), + a_node.clone(), + ); + tree.insert( + NonEmpty::from((String::from("d"), vec![String::from("a")])), + b_node.clone(), + ); + + assert_eq!(tree.maximum_by(|a, b| a.id.cmp(&b.id)), Some(&b_node)); + assert_eq!( + tree.maximum_by(|a, b| a.id.cmp(&b.id).reverse()), + Some(&a_node) + ); +} + +#[test] +fn test_maximum_by_branch_nodes() { + let mut tree = Forest::root(); + + let a_node = TestNode { id: 1 }; + + let b_node = TestNode { id: 3 }; + + tree.insert( + NonEmpty::from((String::from("c"), vec![String::from("a")])), + a_node.clone(), + ); + tree.insert( + NonEmpty::from((String::from("c"), vec![String::from("b")])), + b_node.clone(), + ); + + assert_eq!(tree.maximum_by(|a, b| a.id.cmp(&b.id)), Some(&b_node)); + assert_eq!( + tree.maximum_by(|a, b| a.id.cmp(&b.id).reverse()), + Some(&a_node) + ); +} + +#[test] +fn test_fold_root_nodes() { + let mut tree = Forest::root(); + + let a_node = TestNode { id: 1 }; + + let b_node = TestNode { id: 3 }; + + tree.insert(NonEmpty::new(String::from("a")), a_node); + tree.insert(NonEmpty::new(String::from("b")), b_node); + + assert_eq!(tree.iter().fold(0, |b, a| a.id + b), 4); +} diff --git a/test/Cargo.toml b/test/Cargo.toml index ce6816c..0241032 100644 --- a/test/Cargo.toml +++ b/test/Cargo.toml @@ -29,3 +29,7 @@ features = ["test"] [dev-dependencies.link-git-test] path = "../link-git/t" features = ["test"] + +[dev-dependencies.radicle-surf-test] +path = "../radicle-surf/t" +features = ["test"]