use std::{
fs,
path::{Path, PathBuf},
process::Command,
sync::atomic::{AtomicU64, Ordering},
time::{SystemTime, UNIX_EPOCH},
};
use weavatrix_git::{ChangeKind, HistoryOptions, RepositorySet};
struct Fixture {
root: PathBuf,
left: PathBuf,
right: PathBuf,
shared: String,
}
static FIXTURE_SEQUENCE: AtomicU64 = AtomicU64::new(0);
impl Fixture {
fn new() -> Self {
let unique = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos();
let root = std::env::temp_dir().join(format!(
"weavatrix-set-{}-{unique}-{}",
std::process::id(),
FIXTURE_SEQUENCE.fetch_add(1, Ordering::Relaxed)
));
let left = root.join("left");
let right = root.join("right");
fs::create_dir_all(&left).unwrap();
git(&left, &["init", "-q"]);
configure(&left);
fs::write(left.join("shared.txt"), "shared").unwrap();
commit(&left, "shared");
let shared = git_output(&left, &["rev-parse", "HEAD"]).trim().to_owned();
git(&root, &["clone", "-q", path_text(&left), path_text(&right)]);
configure(&right);
fs::write(left.join("left.txt"), "left").unwrap();
commit(&left, "left");
fs::write(right.join("right.txt"), "right").unwrap();
commit(&right, "right");
for path in [&left, &right] {
git(path, &["commit-graph", "write", "--reachable"]);
}
Self {
root,
left,
right,
shared,
}
}
}
impl Drop for Fixture {
fn drop(&mut self) {
fs::remove_dir_all(&self.root).ok();
}
}
#[test]
fn correlates_histories_objects_and_diffs_across_repositories() {
let fixture = Fixture::new();
let set = RepositorySet::open([
("left", fixture.left.as_path()),
("right", fixture.right.as_path()),
])
.unwrap();
assert_eq!(set.len(), 2);
assert!(!set.is_empty());
let left = set.id("left").unwrap();
let right = set.id("right").unwrap();
assert_eq!(set.name(left), Some("left"));
assert!(set.repository(left).is_some());
let options = HistoryOptions {
max_commits: 10,
..HistoryOptions::default()
};
let serial = set.histories(options).unwrap();
let parallel = set.histories_parallel(options).unwrap();
assert_eq!(serial[0].commits, parallel[0].commits);
assert_eq!(serial[1].commits, parallel[1].commits);
assert_eq!(set.resolve(left, "HEAD").unwrap(), serial[0].head);
let shared = fixture.shared.parse().unwrap();
assert_eq!(set.find_object(shared), vec![left, right]);
let common = set.shared_commits(options).unwrap();
assert!(
common
.iter()
.any(|commit| { commit.id == shared && commit.repositories == vec![left, right] })
);
let changes = set.diff_commits(left, "HEAD", right, "HEAD").unwrap();
assert!(
changes
.iter()
.any(|change| { change.path == b"left.txt" && change.kind == ChangeKind::Deleted })
);
assert!(
changes
.iter()
.any(|change| { change.path == b"right.txt" && change.kind == ChangeKind::Added })
);
}
#[test]
fn rejects_invalid_repository_names_and_ids() {
let fixture = Fixture::new();
assert!(
RepositorySet::open([
("same", fixture.left.as_path()),
("same", fixture.right.as_path()),
])
.is_err()
);
assert!(RepositorySet::open([("bad:name", fixture.left.as_path())]).is_err());
let empty = RepositorySet::open(std::iter::empty::<(&str, &Path)>()).unwrap();
assert!(empty.is_empty());
}
#[test]
fn builds_revision_aware_snapshots_timeline_and_change_sets() {
let fixture = Fixture::new();
let set = RepositorySet::open([
("left", fixture.left.as_path()),
("right", fixture.right.as_path()),
])
.unwrap();
let options = HistoryOptions {
max_commits: 10,
first_parent: true,
..HistoryOptions::default()
};
let histories = set.histories_from(&fixture.shared, options).unwrap();
assert_eq!(histories.len(), 2);
assert!(histories.iter().all(|history| history.commits.len() == 1));
let snapshots = set.snapshots_parallel("HEAD").unwrap();
assert_eq!(snapshots.len(), 2);
assert!(
snapshots[0]
.snapshot
.entries
.iter()
.any(|entry| entry.path == b"left.txt")
);
assert!(
snapshots[1]
.snapshot
.entries
.iter()
.any(|entry| entry.path == b"right.txt")
);
let timeline = set.timeline("HEAD", options).unwrap();
assert_eq!(timeline.len(), 4);
assert!(
timeline
.windows(2)
.all(|pair| pair[0].committer_time >= pair[1].committer_time)
);
let changes = set.changes_parallel(&fixture.shared, "HEAD").unwrap();
assert_eq!(changes.len(), 2);
assert!(changes.iter().all(|change_set| {
change_set.changes.len() == 1 && change_set.changes[0].kind == ChangeKind::Added
}));
}
#[test]
fn parallel_open_preserves_repository_order() {
let fixture = Fixture::new();
let set = RepositorySet::open_parallel([
("left", fixture.left.as_path()),
("right", fixture.right.as_path()),
])
.unwrap();
assert_eq!(
set.ids()
.map(|id| set.name(id).unwrap())
.collect::<Vec<_>>(),
["left", "right"]
);
}
fn configure(path: &Path) {
git(path, &["config", "user.name", "Weavatrix Test"]);
git(path, &["config", "user.email", "test@weavatrix.local"]);
}
fn commit(path: &Path, message: &str) {
git(path, &["add", "-A"]);
git(path, &["commit", "-q", "-m", message]);
}
fn git(path: &Path, args: &[&str]) {
let output = Command::new("git")
.args(args)
.current_dir(path)
.output()
.unwrap();
assert!(
output.status.success(),
"git {args:?}: {}",
String::from_utf8_lossy(&output.stderr)
);
}
fn git_output(path: &Path, args: &[&str]) -> String {
let output = Command::new("git")
.args(args)
.current_dir(path)
.output()
.unwrap();
assert!(output.status.success());
String::from_utf8(output.stdout).unwrap()
}
fn path_text(path: &Path) -> &str {
path.to_str().expect("test path is Unicode")
}