| 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) |
| } |
| } |