crates/gunnar-sendpack/tests/silent_success.rs
//! **A push that moved nothing must not exit 0.**
//!
//! `tests/against_real_git.rs` proves the report is parsed correctly when the
//! remote sends a complete one. The hole this file covers is the other case: a
//! remote that sends an *incomplete* report, which the wire permits and which
//! the verdict was computed without noticing.
//!
//! Every case here drives the real [`send_pack`] against a scripted remote, so
//! what is asserted is the verdict the porcelain would act on — not a parser
//! round-trip.
use std::io::{Cursor, Write};
use bstr::BString;
use gix_hash::{Kind, ObjectId};
use gunnar_sendpack::{advertisement, send_pack, PushCommand, SendPackOptions, Transport};
fn oid(c: char) -> ObjectId {
ObjectId::from_hex(c.to_string().repeat(40).as_bytes()).unwrap()
}
/// A transport that replays a scripted report and records what was written.
struct Scripted {
reader: Cursor<Vec<u8>>,
written: Vec<u8>,
}
impl Scripted {
/// `lines` are report payloads; the flush-pkt is added here.
fn new(lines: &[&str]) -> Self {
let mut script = Vec::new();
for line in lines {
let payload = format!("{line}\n");
write!(script, "{:04x}", payload.len() + 4).unwrap();
script.extend_from_slice(payload.as_bytes());
}
script.extend_from_slice(b"0000");
Scripted {
reader: Cursor::new(script),
written: Vec::new(),
}
}
}
impl Transport for Scripted {
fn io(&mut self) -> (&mut dyn std::io::Read, &mut dyn Write) {
(&mut self.reader, &mut self.written)
}
}
/// No `side-band-64k`, so the report is read raw and the script above is the
/// whole of it.
const CAPS: &str = "report-status-v2 delete-refs object-format=sha1";
fn adv() -> gunnar_sendpack::Advertisement {
advertisement::parse(&[
format!("{} refs/heads/main\0{CAPS}\n", oid('1')).into_bytes(),
format!("{} refs/heads/side\n", oid('2')).into_bytes(),
])
.unwrap()
}
fn pack_writer(w: &mut dyn Write) -> gunnar_sendpack::Result<()> {
w.write_all(b"PACK-not-really")?;
Ok(())
}
/// The driver refuses an all-deletes push that carries a pack and an update
/// that does not, so the pack is chosen from the commands rather than passed
/// in — otherwise the deletion case below fails on that check and never
/// reaches the verdict it is about.
fn push(script: &[&str], commands: &[PushCommand]) -> gunnar_sendpack::PushReport {
let mut transport = Scripted::new(script);
let all_deletes = commands.iter().all(PushCommand::is_delete);
send_pack(
&mut transport,
&adv(),
commands,
Kind::Sha1,
(!all_deletes).then_some(pack_writer),
&SendPackOptions::default(),
)
.expect("the report parses")
}
/// **The defect.** `unpack ok` and nothing else is a legal pkt-line report and
/// it says the remote moved no reference. Before this, `refs` was empty,
/// `all(|r| r.accepted)` was vacuously true, and the push was a success.
#[test]
fn a_report_that_names_no_ref_at_all_is_not_a_success() {
let report = push(
&["unpack ok"],
&[PushCommand::update("refs/heads/main", oid('1'), oid('3'))],
);
assert!(
!report.is_ok(),
"a remote that reported on no reference was read as having moved one"
);
assert_eq!(
report.unreported,
vec![BString::from("refs/heads/main")],
"the ref that went unreported is not named"
);
let summary = report.failure_summary().expect("a failure has a summary");
assert!(
summary.contains("refs/heads/main") && summary.contains("failed to report status"),
"the summary does not say which ref or why: {summary}"
);
}
/// The partial report, which is the shape that actually occurs: the remote
/// answers about one ref of two and the other silently does not move.
#[test]
fn a_report_covering_some_of_the_refs_fails_for_the_rest() {
let report = push(
&["unpack ok", "ok refs/heads/main"],
&[
PushCommand::update("refs/heads/main", oid('1'), oid('3')),
PushCommand::update("refs/heads/side", oid('2'), oid('4')),
],
);
assert!(!report.is_ok(), "nine silent no-ops in a ten-ref push");
assert_eq!(report.unreported, vec![BString::from("refs/heads/side")]);
// And the ref that WAS reported is still recorded as accepted — the point
// is to add the missing verdict, not to discard the one that arrived.
assert!(
report
.refs
.iter()
.any(|r| r.name == "refs/heads/main" && r.accepted),
"the reported ref lost its accepted status"
);
}
/// **The identity-value half.** A complete report must still be a success, or
/// the assertions above would hold for a verdict that is simply always false.
#[test]
fn a_complete_report_is_still_a_success() {
let report = push(
&["unpack ok", "ok refs/heads/main", "ok refs/heads/side"],
&[
PushCommand::update("refs/heads/main", oid('1'), oid('3')),
PushCommand::update("refs/heads/side", oid('2'), oid('4')),
],
);
assert!(
report.is_ok(),
"a complete, all-accepted report was read as a failure: {:?}",
report.failure_summary()
);
assert!(report.unreported.is_empty());
assert_eq!(report.failure_summary(), None);
}
/// A remote that answers about a reference nobody pushed cannot be what makes
/// the push succeed. Before this it could: the `ok` line satisfied
/// `all(|r| r.accepted)` while the commanded ref went unmentioned.
#[test]
fn an_ok_for_a_ref_that_was_never_commanded_does_not_stand_in_for_the_one_that_was() {
let report = push(
&["unpack ok", "ok refs/heads/somewhere-else"],
&[PushCommand::update("refs/heads/main", oid('1'), oid('3'))],
);
assert!(!report.is_ok());
assert_eq!(report.unreported, vec![BString::from("refs/heads/main")]);
assert!(
report
.refs
.iter()
.any(|r| r.name == "refs/heads/somewhere-else" && !r.accepted),
"an uncommanded ref was still counted as an acceptance"
);
}
/// A deletion is a command like any other, and an unreported one is the case
/// where "nothing happened" looks most like success — the client has nothing
/// local to compare against afterwards.
#[test]
fn an_unreported_deletion_fails_too() {
let report = push(
&["unpack ok"],
&[PushCommand::delete("refs/heads/side", oid('2'))],
);
assert!(!report.is_ok());
assert_eq!(report.unreported, vec![BString::from("refs/heads/side")]);
}
/// The `unpack` line still decides on its own, unchanged. Here the remote
/// reports on every ref and accepts them, and the push still fails.
#[test]
fn a_failed_unpack_with_a_complete_ref_report_still_fails() {
let report = push(
&["unpack index-pack failed", "ok refs/heads/main"],
&[PushCommand::update("refs/heads/main", oid('1'), oid('3'))],
);
assert!(!report.is_ok());
assert!(report.unreported.is_empty(), "the refs WERE reported on");
assert!(report
.failure_summary()
.expect("a failure has a summary")
.contains("index-pack failed"));
}