Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/edda

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