blob: 71d498c9a4eafd89d6ff0ad9a136cd9dee339e8d [file] [log] [blame]
use std::cell::RefCell;
use gix_negotiate::Algorithm;
use gix_object::{bstr, bstr::ByteSlice};
use gix_odb::{Find, FindExt};
use gix_ref::{file::ReferenceExt, store::WriteReflog};
#[test]
fn run() -> crate::Result {
let root = gix_testtools::scripted_fixture_read_only("make_repos.sh")?;
for case in [
"no_parents",
"clock_skew",
"two_colliding_skips",
"multi_round",
"advertisement_as_filter",
] {
let base = root.join(case);
for (algo_name, algo) in [
("noop", Algorithm::Noop),
("consecutive", Algorithm::Consecutive),
("skipping", Algorithm::Skipping),
] {
let obj_buf = RefCell::new(Vec::new());
let buf = std::fs::read(base.join(format!("baseline.{algo_name}")))?;
let store = gix_odb::at(base.join("client").join(".git/objects"))?;
let refs = gix_ref::file::Store::at(
base.join("client").join(".git"),
WriteReflog::Disable,
gix_hash::Kind::Sha1,
);
let lookup_names = |names: &[&str]| -> Vec<gix_hash::ObjectId> {
names
.iter()
.filter_map(|name| {
refs.try_find(*name).expect("one tag per commit").map(|mut r| {
r.peel_to_id_in_place(&refs, |id, buf| {
store.try_find(id, buf).map(|d| d.map(|d| (d.kind, d.data)))
})
.expect("works");
r.target.into_id()
})
})
.collect()
};
let message = |id| {
store
.find_commit(id, obj_buf.borrow_mut().as_mut())
.expect("present")
.message
.trim()
.as_bstr()
.to_owned()
};
let debug = false;
for use_cache in [false, true] {
let cache = use_cache
.then(|| gix_commitgraph::at(store.store_ref().path().join("info")).ok())
.flatten();
let mut graph = gix_revwalk::Graph::new(
|id, buf| {
store
.try_find(id, buf)
.map(|r| r.and_then(gix_object::Data::try_into_commit_iter))
},
cache,
);
let mut negotiator = algo.into_negotiator();
if debug {
eprintln!("ALGO {algo_name} CASE {case}");
}
// // In --negotiate-only mode, which seems to be the only thing that's working after trying --dry-run, we unfortunately
// // don't get to see what happens if known-common commits are added as git itself doesn't do that in this mode
// // for some reason.
// for common in lookup_names(&["origin/main"]) {
// eprintln!("COMMON {name} {common}", name = message(common));
// negotiator.known_common(common)?;
// }
for tip in lookup_names(&["HEAD"]).into_iter().chain(
refs.iter()?
.prefixed("refs/heads")?
.filter_map(Result::ok)
.map(|r| r.target.into_id()),
) {
if debug {
eprintln!("TIP {name} {tip}", name = message(tip));
}
negotiator.add_tip(tip, &mut graph)?;
}
for (round, Round { mut haves, common }) in ParseRounds::new(buf.lines()).enumerate() {
if algo == Algorithm::Skipping {
if case == "clock_skew" {
// Here for some reason the prio-queue of git manages to not sort the parent of C2, which is in the future, to be
// ahead of old4 that is in the past. In the git version of this test, they say to expect exactly this sequence
// as well even though it's not actually happening (but that they can't see due to the way they are testing).
haves = lookup_names(&["c2", "c1", "old4", "old2", "old1"]);
} else if case == "two_colliding_skips" {
// The same thing, we actually get exactly the right order, whereas git for some reason doesn't.
// This is the order expected in the git tests.
haves = lookup_names(&["c5side", "c11", "c9", "c6", "c1"]);
} else if case == "multi_round" && round == 1 {
// Here, probably also because of priority queue quirks, `git` emits the commits out of order, with only one
// branch, b5 I think, being out of place. This list puts the expectation in the right order, which is ordered
// by commit date.
haves = lookup_names(&[
"b8.c14", "b7.c14", "b6.c14", "b5.c14", "b4.c14", "b3.c14", "b2.c14", "b8.c9", "b7.c9",
"b6.c9", "b5.c9", "b4.c9", "b3.c9", "b2.c9", "b8.c1", "b7.c1", "b6.c1", "b5.c1",
"b4.c1", "b3.c1", "b2.c1", "b8.c0", "b7.c0", "b6.c0", "b5.c0", "b4.c0", "b3.c0",
"b2.c0",
]);
} else if case == "advertisement_as_filter" {
haves = lookup_names(&["c2side", "c5", "origin/main"])
.into_iter()
.chain(Some(
gix_hash::ObjectId::from_hex(b"f36cefa0be2ac180d360a54b1cc4214985cea60a").unwrap(),
))
.collect();
}
}
for have in haves {
let actual = negotiator.next_have(&mut graph).unwrap_or_else(|| {
panic!("{algo_name}:cache={use_cache}: one have per baseline: {have} missing or in wrong order", have = message(have))
})?;
assert_eq!(
actual,
have,
"{algo_name}:cache={use_cache}: order and commit matches exactly, wanted {expected}, got {actual}, commits left: {:?}",
std::iter::from_fn(|| negotiator.next_have(&mut graph)).map(|id| message(id.unwrap())).collect::<Vec<_>>(),
actual = message(actual),
expected = message(have)
);
if debug {
eprintln!("have {}", message(actual));
}
}
for common_revision in common {
if debug {
eprintln!("ACK {}", message(common_revision));
}
negotiator.in_common_with_remote(common_revision, &mut graph)?;
}
}
assert!(
negotiator.next_have(&mut graph).is_none(),
"{algo_name}:cache={use_cache}: negotiator should be depleted after all recorded baseline rounds"
);
}
}
}
Ok(())
}
struct ParseRounds<'a> {
lines: bstr::Lines<'a>,
}
impl<'a> ParseRounds<'a> {
pub fn new(mut lines: bstr::Lines<'a>) -> Self {
parse::command(&mut lines, parse::Command::Incoming).expect("handshake");
Self { lines }
}
}
impl<'a> Iterator for ParseRounds<'a> {
type Item = Round;
fn next(&mut self) -> Option<Self::Item> {
let haves = parse::object_ids("have", parse::command(&mut self.lines, parse::Command::Outgoing)?);
let common = parse::object_ids("ACK", parse::command(&mut self.lines, parse::Command::Incoming)?);
if haves.is_empty() {
assert!(common.is_empty(), "cannot ack what's not there");
return None;
}
Round { haves, common }.into()
}
}
struct Round {
pub haves: Vec<gix_hash::ObjectId>,
pub common: Vec<gix_hash::ObjectId>,
}
mod parse {
use gix_object::{
bstr,
bstr::{BStr, ByteSlice},
};
#[derive(Debug, Eq, PartialEq, Copy, Clone)]
pub enum Command {
Incoming,
Outgoing,
}
pub fn object_ids(prefix: &str, lines: impl IntoIterator<Item = impl AsRef<[u8]>>) -> Vec<gix_hash::ObjectId> {
lines
.into_iter()
.filter_map(|line| {
line.as_ref()
.strip_prefix(prefix.as_bytes())
.map(|id| gix_hash::ObjectId::from_hex(id.trim()).expect("valid hash"))
})
.collect()
}
pub fn command<'a>(lines: &mut bstr::Lines<'a>, wanted: Command) -> Option<Vec<&'a BStr>> {
let mut out = Vec::new();
for line in lines {
let pos = line.find(b"fetch").expect("fetch token");
let line_mode = match &line[pos + 5..][..2] {
b"< " => Command::Incoming,
b"> " => Command::Outgoing,
invalid => unreachable!("invalid fetch token: {:?}", invalid.as_bstr()),
};
assert_eq!(line_mode, wanted, "command with unexpected mode");
let line = line[pos + 7..].as_bstr();
if line == "0000" {
break;
}
out.push(line);
}
(!out.is_empty()).then_some(out)
}
}