From d2d3c45180238fabb42f5fd68a7e96a7fd6d98a7 Mon Sep 17 00:00:00 2001 From: Hammed Abass Date: Wed, 15 Jul 2026 23:20:22 +0200 Subject: [PATCH] fix(core): make import cancellation test deterministic --- crates/vnidrop/tests/lifecycle.rs | 95 +++++++++++++++++++++---------- 1 file changed, 65 insertions(+), 30 deletions(-) diff --git a/crates/vnidrop/tests/lifecycle.rs b/crates/vnidrop/tests/lifecycle.rs index ca8a0a5..a6bdd5f 100644 --- a/crates/vnidrop/tests/lifecycle.rs +++ b/crates/vnidrop/tests/lifecycle.rs @@ -1,10 +1,13 @@ mod support; -use std::sync::Arc; -use std::time::{Duration, Instant}; +use std::sync::{Arc, Condvar, Mutex}; +use std::time::Duration; use support::{share_path, CoreGuard, RecordingSink, TestNode}; -use vnidrop::{CoreLimits, ShareMetadataInput, ShareSource, SourceKind, TransferAccessMode}; +use vnidrop::{ + CoreEvent, CoreEventSink, CoreLimits, ShareMetadataInput, ShareSource, SourceKind, + TransferAccessMode, +}; #[test] fn share_creation_persists_selected_access_mode_atomically() { @@ -324,55 +327,87 @@ fn source_limit_rejection_creates_no_transfer_state() { #[test] fn cancellation_during_import_is_durable() { let source_dir = tempfile::tempdir().unwrap(); - let source_path = source_dir.path().join("large.bin"); - std::fs::File::create(&source_path) - .unwrap() - .set_len(256 * 1024 * 1024) - .unwrap(); - let sender = TestNode::new(); - let core = sender.core.arc(); + let source_path = source_dir.path().join("gated.bin"); + std::fs::write(&source_path, vec![0u8; 256 * 1024]).unwrap(); + let data_dir = tempfile::tempdir().unwrap(); + let import_gate = Arc::new(ImportStartedGate::default()); + let sender = CoreGuard::start(data_dir.path(), import_gate.clone()); + let core = sender.arc(); let worker = std::thread::spawn(move || { core.share_files( vec![ShareSource { kind: SourceKind::Path, value: source_path.to_string_lossy().to_string(), - display_name: Some("large.bin".to_string()), + display_name: Some("gated.bin".to_string()), is_directory: false, }], ShareMetadataInput { transfer_id: 25, - transfer_name: Some("large".to_string()), + transfer_name: Some("gated".to_string()), sender_name: None, access_mode: TransferAccessMode::ApprovalRequired, }, ) }); - let started = Instant::now(); - loop { - if sender - .core - .list_transfers() - .unwrap() - .iter() - .any(|transfer| transfer.transfer_id == 25 && transfer.status == "importing") - { - break; - } - assert!(started.elapsed() < Duration::from_secs(10)); - std::thread::sleep(Duration::from_millis(10)); - } - sender.core.cancel_transfer(25).unwrap(); + // The synchronous event callback holds the worker after active-transfer + // registration, so cancellation cannot race a fast import to completion. + import_gate.wait_until_blocked(); + let cancel_result = sender.cancel_transfer(25); + import_gate.release(); + cancel_result.unwrap(); assert!(worker.join().unwrap().is_err()); let transfer = sender - .core .list_transfers() .unwrap() .into_iter() .find(|transfer| transfer.transfer_id == 25) .unwrap(); assert_eq!(transfer.status, "cancelled"); - assert_eq!(sender.core.status().active_transfers, 0); - assert_eq!(sender.core.status().active_shares, 0); + assert_eq!(sender.status().active_transfers, 0); + assert_eq!(sender.status().active_shares, 0); +} + +#[derive(Default)] +struct ImportStartedGate { + state: Mutex, + changed: Condvar, +} + +#[derive(Default)] +struct ImportGateState { + blocked: bool, + released: bool, +} + +impl ImportStartedGate { + fn wait_until_blocked(&self) { + let state = self.state.lock().unwrap(); + let (state, _) = self + .changed + .wait_timeout_while(state, Duration::from_secs(5), |state| !state.blocked) + .unwrap(); + assert!(state.blocked, "import did not reach the start gate"); + } + + fn release(&self) { + let mut state = self.state.lock().unwrap(); + state.released = true; + self.changed.notify_all(); + } +} + +impl CoreEventSink for ImportStartedGate { + fn on_event(&self, event: CoreEvent) { + if event.phase != "import" || event.kind != "started" { + return; + } + let mut state = self.state.lock().unwrap(); + state.blocked = true; + self.changed.notify_all(); + while !state.released { + state = self.changed.wait(state).unwrap(); + } + } }