use std::io::BufReader;
use gunnar_lfs::{MemLfsStore, Operation, Session};
fn transcript(name: &str) -> Vec<u8> {
let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests/transcripts")
.join(name);
std::fs::read(&path).unwrap_or_else(|err| panic!("{}: {err}", path.display()))
}
const SMALL: &[u8] = b"gunnar lfs\n";
const LARGER: &[u8] = b"the second object, a little longer so the two are plainly different\n";
fn replay(name: &str, store: &MemLfsStore, operation: Operation) -> (Vec<u8>, gunnar_lfs::Summary) {
let recorded = transcript(&format!("{name}.client-to-server.bin"));
let mut input = BufReader::new(std::io::Cursor::new(recorded));
let mut output = Vec::new();
let summary = Session::new(store, operation)
.serve(&mut input, &mut output)
.expect("the recorded conversation is well-framed");
(output, summary)
}
fn assert_wire_eq(actual: &[u8], expected: &[u8], what: &str) {
if actual == expected {
return;
}
let at = actual
.iter()
.zip(expected)
.position(|(a, b)| a != b)
.unwrap_or_else(|| actual.len().min(expected.len()));
let from = at.saturating_sub(24);
panic!(
"{what}: the wire differs from what git-lfs 3.7.1 was answered.\n\
first difference at byte {at} (actual {} bytes, recorded {} bytes)\n\
actual …{}\n\
recorded …{}",
actual.len(),
expected.len(),
String::from_utf8_lossy(&actual[from..(at + 40).min(actual.len())]),
String::from_utf8_lossy(&expected[from..(at + 40).min(expected.len())]),
);
}
#[test]
fn the_recorded_upload_replays_byte_for_byte_and_the_objects_land() {
let store = MemLfsStore::new();
let (output, summary) = replay("upload", &store, Operation::Upload);
assert_wire_eq(
&output,
&transcript("upload.server-to-client.bin"),
"upload",
);
assert_eq!(store.len(), 2, "both objects committed");
let small =
gunnar_lfs::Oid::parse("e2e5ffeb6125407ed45479208193c5e438f1554f0da8cee77a0f10b1f99578c6")
.unwrap();
let larger =
gunnar_lfs::Oid::parse("4e73c693de0152ab39b91a62461943231d70eaa78c84ac1e9f196f202c29c53b")
.unwrap();
assert_eq!(store.get(&small).as_deref(), Some(SMALL));
assert_eq!(store.get(&larger).as_deref(), Some(LARGER));
assert_eq!(summary.objects_received, 2);
assert_eq!(summary.bytes_received, (SMALL.len() + LARGER.len()) as u64);
assert_eq!(summary.batches, 1);
assert_eq!(summary.batch_objects, 2);
assert!(summary.clean_quit, "the client said `quit`");
}
#[test]
fn the_recorded_download_replays_byte_for_byte() {
let store = MemLfsStore::new();
store.insert(SMALL);
store.insert(LARGER);
let (output, summary) = replay("download", &store, Operation::Download);
assert_wire_eq(
&output,
&transcript("download.server-to-client.bin"),
"download",
);
assert_eq!(summary.objects_sent, 2);
assert_eq!(summary.bytes_sent, (SMALL.len() + LARGER.len()) as u64);
assert!(summary.clean_quit);
}
#[test]
fn the_recorded_lock_session_is_answered_501_and_not_405() {
let store = MemLfsStore::new();
let (output, summary) = replay("lock", &store, Operation::Upload);
assert_wire_eq(&output, &transcript("lock.server-to-client.bin"), "lock");
assert!(
String::from_utf8_lossy(&output).contains("status 501"),
"the fixture must be the 501 answer, or this test guards nothing"
);
assert!(!summary.clean_quit);
assert_eq!(summary.commands, 2);
}
#[test]
fn a_server_that_answers_differently_produces_different_bytes() {
let empty = MemLfsStore::new();
let (output, summary) = replay("download", &empty, Operation::Download);
let recorded = transcript("download.server-to-client.bin");
assert_ne!(
output, recorded,
"an empty server answered a fetch with the same bytes as a full one; \
the comparison above is not looking at anything"
);
assert!(
String::from_utf8_lossy(&output).contains("status 404"),
"a missing object must be 404, got: {}",
String::from_utf8_lossy(&output)
);
let wire = String::from_utf8_lossy(&output);
assert!(
wire.contains(" download"),
"the batch must offer `download` for an absent object, or the client \
never asks and never learns it is absent; got: {wire}"
);
assert!(
!wire.contains(" noop"),
"a download batch answered `noop`, which git-lfs reads as \
\"nothing to do\" and reports as success; got: {wire}"
);
assert_eq!(summary.objects_sent, 0);
assert_eq!(summary.bytes_sent, 0);
}