use std::io::Cursor;
use std::ops::ControlFlow;
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use zipatch_rs::test_utils::{MAGIC, make_chunk};
use zipatch_rs::{ApplyConfig, ApplyError, ApplyObserver, ChunkEvent, ZiPatchReader};
struct LoggingObserver {
log: Arc<std::sync::Mutex<Vec<ChunkEvent>>>,
}
impl ApplyObserver for LoggingObserver {
fn on_chunk_applied(&mut self, ev: ChunkEvent) -> ControlFlow<(), ()> {
self.log.lock().unwrap().push(ev);
ControlFlow::Continue(())
}
}
struct CountingBreaker {
count: Arc<AtomicUsize>,
}
impl ApplyObserver for CountingBreaker {
fn on_chunk_applied(&mut self, _ev: ChunkEvent) -> ControlFlow<(), ()> {
self.count.fetch_add(1, Ordering::Relaxed);
ControlFlow::Break(())
}
}
struct BreakAfter {
count: Arc<AtomicUsize>,
threshold: usize,
}
impl ApplyObserver for BreakAfter {
fn on_chunk_applied(&mut self, _ev: ChunkEvent) -> ControlFlow<(), ()> {
let n = self.count.fetch_add(1, Ordering::Relaxed) + 1;
if n >= self.threshold {
ControlFlow::Break(())
} else {
ControlFlow::Continue(())
}
}
}
#[test]
fn observer_fires_for_each_non_eof_chunk_with_correct_fields() {
let log: Arc<std::sync::Mutex<Vec<ChunkEvent>>> = Arc::new(std::sync::Mutex::new(Vec::new()));
let log_clone = log.clone();
let mut a = Vec::new();
a.extend_from_slice(&1u32.to_be_bytes());
a.extend_from_slice(b"a");
let mut b = Vec::new();
b.extend_from_slice(&1u32.to_be_bytes());
b.extend_from_slice(b"b");
let mut patch = Vec::new();
patch.extend_from_slice(&MAGIC);
patch.extend_from_slice(&make_chunk(b"ADIR", &a));
patch.extend_from_slice(&make_chunk(b"ADIR", &b));
patch.extend_from_slice(&make_chunk(b"EOF_", &[]));
let tmp = tempfile::tempdir().unwrap();
let ctx = ApplyConfig::new(tmp.path()).with_observer(LoggingObserver { log: log_clone });
let reader = ZiPatchReader::new(Cursor::new(patch)).unwrap();
ctx.apply_patch(reader).unwrap();
let events = log.lock().unwrap();
assert_eq!(
events.len(),
2,
"two non-EOF chunks must fire exactly two events"
);
assert_eq!(events[0].index, 0, "first event index must be 0");
assert_eq!(events[1].index, 1, "second event index must be 1");
assert_eq!(events[0].kind, *b"ADIR");
assert_eq!(events[1].kind, *b"ADIR");
assert_eq!(
events[0].bytes_read,
12 + 17,
"bytes_read after first ADIR must be magic + one 17-byte frame"
);
assert_eq!(
events[1].bytes_read,
12 + 17 + 17,
"bytes_read after second ADIR must be magic + two 17-byte frames"
);
assert!(
events[0].bytes_read < events[1].bytes_read,
"bytes_read must strictly increase between events"
);
}
#[test]
fn observer_break_on_first_chunk_aborts_immediately_leaving_first_applied() {
let mut a = Vec::new();
a.extend_from_slice(&1u32.to_be_bytes());
a.extend_from_slice(b"a");
let mut b_body = Vec::new();
b_body.extend_from_slice(&1u32.to_be_bytes());
b_body.extend_from_slice(b"b");
let mut c = Vec::new();
c.extend_from_slice(&1u32.to_be_bytes());
c.extend_from_slice(b"c");
let mut patch = Vec::new();
patch.extend_from_slice(&MAGIC);
patch.extend_from_slice(&make_chunk(b"ADIR", &a));
patch.extend_from_slice(&make_chunk(b"ADIR", &b_body));
patch.extend_from_slice(&make_chunk(b"ADIR", &c));
patch.extend_from_slice(&make_chunk(b"EOF_", &[]));
let count = Arc::new(AtomicUsize::new(0));
let count_clone = count.clone();
let tmp = tempfile::tempdir().unwrap();
let ctx = ApplyConfig::new(tmp.path()).with_observer(CountingBreaker { count: count_clone });
let reader = ZiPatchReader::new(Cursor::new(patch)).unwrap();
let err = ctx.apply_patch(reader).unwrap_err();
assert!(
matches!(err, ApplyError::Cancelled),
"observer Break must produce ApplyError::Cancelled, got {err:?}"
);
assert_eq!(
count.load(Ordering::Relaxed),
1,
"exactly one on_chunk_applied call fires before the abort takes effect"
);
assert!(
tmp.path().join("a").is_dir(),
"first ADIR must have been applied before Cancelled was returned"
);
assert!(
!tmp.path().join("b").exists(),
"second ADIR must NOT have been applied after Cancelled"
);
assert!(
!tmp.path().join("c").exists(),
"third ADIR must NOT have been applied after Cancelled"
);
}
#[test]
fn observer_break_on_last_chunk_before_eof_leaves_all_earlier_applied() {
let make_adir_chunk = |name: &[u8]| -> Vec<u8> {
let mut body = Vec::new();
body.extend_from_slice(&(name.len() as u32).to_be_bytes());
body.extend_from_slice(name);
make_chunk(b"ADIR", &body)
};
let mut patch = Vec::new();
patch.extend_from_slice(&MAGIC);
patch.extend_from_slice(&make_adir_chunk(b"a"));
patch.extend_from_slice(&make_adir_chunk(b"b"));
patch.extend_from_slice(&make_adir_chunk(b"c"));
patch.extend_from_slice(&make_chunk(b"EOF_", &[]));
let call_count = Arc::new(AtomicUsize::new(0));
let cc = call_count.clone();
let tmp = tempfile::tempdir().unwrap();
let ctx = ApplyConfig::new(tmp.path()).with_observer(BreakAfter {
count: cc,
threshold: 3,
});
let reader = ZiPatchReader::new(Cursor::new(patch)).unwrap();
let err = ctx.apply_patch(reader).unwrap_err();
assert!(
matches!(err, ApplyError::Cancelled),
"expected Cancelled, got {err:?}"
);
assert!(tmp.path().join("a").is_dir(), "a/ must exist");
assert!(tmp.path().join("b").is_dir(), "b/ must exist");
assert!(
tmp.path().join("c").is_dir(),
"c/ must exist (apply ran before event fired)"
);
}