test(integration): make halt-on-skip-forward test deterministic
The wallclock-based halt timing failed on fast CI runners where a 2 GB synthetic-disc skip-forward sweep finishes in <100 ms — well under the 200 ms halt fire delay. Reader now signals halt on first read; the inner-loop halt check on iteration 2 breaks 'outer. No wallclock race.
This commit is contained in:
@@ -355,20 +355,28 @@ fn test_file_sector_reader_round_trip() {
|
|||||||
// - halted = false (no user stop)
|
// - halted = false (no user stop)
|
||||||
// - ISO file is `total_bytes` size on disk (sparse zeros)
|
// - ISO file is `total_bytes` size on disk (sparse zeros)
|
||||||
|
|
||||||
/// Reader that returns Err for every read. Models the worst case where the
|
/// Reader that returns Err for every read. Optionally signals a halt
|
||||||
/// drive can read nothing on this disc — Pass 1 must still walk to end of disc.
|
/// flag on the first read so tests can exercise the halt-during-skip-forward
|
||||||
|
/// path deterministically (no wallclock dependency).
|
||||||
struct FailingSectorReader {
|
struct FailingSectorReader {
|
||||||
capacity: u32,
|
capacity: u32,
|
||||||
/// Per-call delay so the test exercises the skip-forward path realistically
|
/// If set, signals halt on the first `read_sectors` call. Cleared after
|
||||||
/// without burning real wallclock.
|
/// the first signal so subsequent reads are plain Err.
|
||||||
err_delay_ms: u64,
|
halt_on_first_read: Option<Arc<AtomicBool>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FailingSectorReader {
|
impl FailingSectorReader {
|
||||||
fn new(capacity: u32) -> Self {
|
fn new(capacity: u32) -> Self {
|
||||||
Self {
|
Self {
|
||||||
capacity,
|
capacity,
|
||||||
err_delay_ms: 0,
|
halt_on_first_read: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn with_halt_on_first_read(capacity: u32, halt: Arc<AtomicBool>) -> Self {
|
||||||
|
Self {
|
||||||
|
capacity,
|
||||||
|
halt_on_first_read: Some(halt),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -381,8 +389,8 @@ impl SectorReader for FailingSectorReader {
|
|||||||
_buf: &mut [u8],
|
_buf: &mut [u8],
|
||||||
_recovery: bool,
|
_recovery: bool,
|
||||||
) -> Result<usize> {
|
) -> Result<usize> {
|
||||||
if self.err_delay_ms > 0 {
|
if let Some(h) = self.halt_on_first_read.take() {
|
||||||
std::thread::sleep(Duration::from_millis(self.err_delay_ms));
|
h.store(true, Ordering::Relaxed);
|
||||||
}
|
}
|
||||||
Err(libfreemkv::error::Error::DiscRead { sector: lba as u64 })
|
Err(libfreemkv::error::Error::DiscRead { sector: lba as u64 })
|
||||||
}
|
}
|
||||||
@@ -469,34 +477,30 @@ fn test_disc_copy_completes_full_disc_with_failing_reader() {
|
|||||||
// the CopyResult.
|
// the CopyResult.
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── 7. Halt during Pass 1 of an all-failing-read sweep returns promptly ───
|
// ── 7. Halt during Pass 1 skip-forward path returns promptly (deterministic) ─
|
||||||
//
|
//
|
||||||
// Per RIP_DESIGN.md §3: halt is the only legitimate early exit from Pass 1.
|
// Per RIP_DESIGN.md §3: halt is the only legitimate early exit from Pass 1.
|
||||||
// Even when every read is failing (skip-forward path), a halt must be
|
// Even when every read is failing (skip-forward path), a halt must be
|
||||||
// honored within a small bounded time.
|
// honored within a small bounded time.
|
||||||
|
//
|
||||||
|
// Deterministic fixture: the reader signals halt on its FIRST read. The
|
||||||
|
// inner copy loop's halt check fires on the next iteration, breaking out
|
||||||
|
// of 'outer. This avoids any wallclock race on fast CI runners (where a
|
||||||
|
// 2 GB synthetic disc can sweep skip-forward in <100 ms).
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_disc_copy_halts_promptly_on_failing_reader() {
|
fn test_disc_copy_halts_promptly_on_failing_reader() {
|
||||||
let capacity_sectors: u32 = 1024 * 1024; // 2 GB synthetic disc — plenty of work
|
let capacity_sectors: u32 = 1024 * 1024; // 2 GB synthetic disc
|
||||||
let mut reader = FailingSectorReader {
|
|
||||||
capacity: capacity_sectors,
|
let halt = Arc::new(AtomicBool::new(false));
|
||||||
err_delay_ms: 1, // small per-read delay so halt has something to interrupt
|
let mut reader =
|
||||||
};
|
FailingSectorReader::with_halt_on_first_read(capacity_sectors, halt.clone());
|
||||||
let disc = synthetic_disc(capacity_sectors);
|
let disc = synthetic_disc(capacity_sectors);
|
||||||
|
|
||||||
let tmp = tempfile::NamedTempFile::new().expect("tempfile create");
|
let tmp = tempfile::NamedTempFile::new().expect("tempfile create");
|
||||||
let iso_path = tmp.path().to_path_buf();
|
let iso_path = tmp.path().to_path_buf();
|
||||||
drop(tmp);
|
drop(tmp);
|
||||||
|
|
||||||
let halt = Arc::new(AtomicBool::new(false));
|
|
||||||
let halt_setter = halt.clone();
|
|
||||||
|
|
||||||
// Trigger halt after 200 ms.
|
|
||||||
std::thread::spawn(move || {
|
|
||||||
std::thread::sleep(Duration::from_millis(200));
|
|
||||||
halt_setter.store(true, Ordering::Relaxed);
|
|
||||||
});
|
|
||||||
|
|
||||||
let opts = CopyOptions {
|
let opts = CopyOptions {
|
||||||
decrypt: false,
|
decrypt: false,
|
||||||
skip_on_error: true,
|
skip_on_error: true,
|
||||||
@@ -522,6 +526,10 @@ fn test_disc_copy_halts_promptly_on_failing_reader() {
|
|||||||
assert!(result.halted, "result.halted must be true");
|
assert!(result.halted, "result.halted must be true");
|
||||||
assert!(
|
assert!(
|
||||||
!result.complete,
|
!result.complete,
|
||||||
"halted run cannot be complete (bytes_pending > 0 likely)"
|
"halted run cannot be complete (bytes_pending > 0 expected)"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
result.bytes_pending > 0,
|
||||||
|
"halt fired before sweep completed; bytes_pending must be > 0"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user