crates/gunnar-delta/tests/limits.rs
//! An absurd request is refused, not attempted.
//!
//! A test in this tree took a 499 GB machine out on 2026-08-03 by crossing a
//! degenerate parameter with a large input, and a sliding window over a large
//! object set is exactly that shape. Every one of these assertions is that the
//! crate came back with [`gunnar_delta::Error::Refused`] naming the field —
//! *before* allocating — rather than succeeding slowly, clamping quietly or
//! being killed.
//!
//! Silent clamping is the failure mode worth naming: a caller that asks for a
//! window of 100,000 and gets 1,024 without being told has been given wrong
//! output under a right-looking API. Nothing here clamps.
use std::sync::atomic::AtomicBool;
use gix_object::Kind;
use gunnar_delta::{delta_into, deltify, Encode, Error, Limits, Object, Options};
fn objects(n: usize, size: usize) -> Vec<Vec<u8>> {
(0..n)
.map(|i| {
(0..size)
.map(|k| ((k * 31 + i * 7) % 251) as u8)
.collect::<Vec<u8>>()
})
.collect()
}
fn borrow(v: &[Vec<u8>]) -> Vec<Object<'_>> {
v.iter()
.enumerate()
.map(|(i, c)| {
let mut id = [0u8; 20];
id[..8].copy_from_slice(&(i as u64).to_be_bytes());
Object {
id: gix_hash::ObjectId::from(id),
kind: Kind::Blob,
name_hash: 0,
epoch: 0,
data: c.as_slice(),
}
})
.collect()
}
#[track_caller]
fn expect_refusal(r: Result<impl std::fmt::Debug, Error>, field: &str) {
match r {
Err(Error::Refused { what, .. }) => assert_eq!(what, field),
Err(other) => panic!("expected a refusal naming {field}, got {other}"),
Ok(v) => panic!("expected a refusal naming {field}, the call SUCCEEDED with {v:?}"),
}
}
#[test]
fn a_window_past_the_cap_is_refused() {
let data = objects(4, 128);
let opts = Options {
window: 100_000,
..Options::default()
};
expect_refusal(
deltify(
&borrow(&data),
&opts,
&mut gix_utils::progress::Discard,
&AtomicBool::default(),
),
"window",
);
}
#[test]
fn a_depth_past_the_cap_is_refused() {
let data = objects(4, 128);
let opts = Options {
depth: 10_000_000,
..Options::default()
};
expect_refusal(
deltify(
&borrow(&data),
&opts,
&mut gix_utils::progress::Discard,
&AtomicBool::default(),
),
"depth",
);
}
#[test]
fn too_many_objects_is_refused() {
let data = objects(40, 64);
let opts = Options {
limits: Limits {
max_objects: 10,
..Limits::default()
},
..Options::default()
};
expect_refusal(
deltify(
&borrow(&data),
&opts,
&mut gix_utils::progress::Discard,
&AtomicBool::default(),
),
"objects",
);
}
#[test]
fn too_much_total_input_is_refused() {
let data = objects(20, 4_096);
let opts = Options {
limits: Limits {
max_total_input: 1_000,
..Limits::default()
},
..Options::default()
};
expect_refusal(
deltify(
&borrow(&data),
&opts,
&mut gix_utils::progress::Discard,
&AtomicBool::default(),
),
"total input bytes",
);
}
#[test]
fn an_index_above_the_byte_cap_is_refused_rather_than_built() {
let base = vec![0x11u8; 2 * 1024 * 1024];
let mut out = Vec::new();
let opts = Encode {
limits: Limits {
max_index_bytes: 4_096,
..Limits::default()
},
..Encode::default()
};
expect_refusal(delta_into(&base, &base, &mut out, &opts), "base index size");
assert!(
out.is_empty(),
"a refused call still wrote {} bytes",
out.len()
);
}
/// A base too large to *index* is skipped as a candidate, not refused: a repack
/// must not fail because one blob is big. The object still comes back, whole.
#[test]
fn an_oversized_base_is_skipped_as_a_candidate_and_the_object_still_appears() {
let big = vec![7u8; 300_000];
let data = vec![big.clone(), big[..299_000].to_vec()];
let opts = Options {
limits: Limits {
max_base_size: 1_024,
..Limits::default()
},
..Options::default()
};
let out = deltify(
&borrow(&data),
&opts,
&mut gix_utils::progress::Discard,
&AtomicBool::default(),
)
.expect("an oversized base is a skip, not an error");
assert_eq!(out.len(), 2);
assert!(
out.iter().all(|d| !d.is_delta()),
"an object above max_base_size was used as a base anyway"
);
// Red-first: with the cap raised, the very same input DOES delta. Without
// this the assertion above would also pass on an encoder that never deltas.
let out = deltify(
&borrow(&data),
&Options::default(),
&mut gix_utils::progress::Discard,
&AtomicBool::default(),
)
.expect("deltify");
assert!(
out.iter().any(|d| d.is_delta()),
"the same input does not delta even with the default cap, so the skip test is vacuous"
);
}
#[test]
fn an_oversized_target_is_emitted_whole_rather_than_searched() {
let data = vec![vec![3u8; 200_000], vec![3u8; 199_000]];
let opts = Options {
limits: Limits {
max_target_size: 1_024,
..Limits::default()
},
..Options::default()
};
let out = deltify(
&borrow(&data),
&opts,
&mut gix_utils::progress::Discard,
&AtomicBool::default(),
)
.expect("deltify");
assert!(out.iter().all(|d| !d.is_delta()));
}
#[test]
fn a_degenerate_all_identical_input_finishes_under_the_chain_cap() {
// Every 16-byte anchor of this base hashes to the same bucket, which without
// `Encode::max_chain` is a quadratic scan. The assertion is that the call
// returns at all, and correctly, in a test that would otherwise hang.
let base = vec![0xa5u8; 4 * 1024 * 1024];
let started = std::time::Instant::now();
let d = gunnar_delta::delta(&base, &base).expect("encode");
assert!(
started.elapsed() < std::time::Duration::from_secs(30),
"a degenerate all-identical base took {:?}, so the candidate chain is not bounded",
started.elapsed()
);
assert!(
d.len() < 64,
"delta of an identical 4 MiB object is {} bytes",
d.len()
);
}
#[test]
fn an_interrupted_run_returns_interrupted_rather_than_a_partial_result() {
let data = objects(4_000, 8_192);
let flag = AtomicBool::new(true);
match deltify(
&borrow(&data),
&Options::default(),
&mut gix_utils::progress::Discard,
&flag,
) {
Err(Error::Interrupted) => {}
Err(other) => panic!("expected Interrupted, got {other}"),
Ok(v) => panic!("expected Interrupted, got {} entries", v.len()),
}
}