Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/8355-split-timer-tests.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Split `crates/perry-runtime/src/timer.rs`'s inline unit tests into `timer/tests_inline.rs`: #8328 pushed the file to 2001 lines, one over the 2000-line cap enforced by `scripts/check_file_size.sh`.
236 changes: 5 additions & 231 deletions crates/perry-runtime/src/timer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1766,236 +1766,10 @@ impl TimerRootScanState {
}
}

#[path = "timer/tests_inline.rs"]
#[cfg(test)]
const TEST_CALLBACK_TIMER_ID: i64 = i64::MIN + 101;
mod tests_inline;
// Helpers other modules reach as `crate::timer::…`; the extraction above
// moved their definitions, so re-export them at the original path.
#[cfg(test)]
const TEST_INTERVAL_TIMER_ID: i64 = i64::MIN + 102;

#[cfg(test)]
#[derive(Debug, Default)]
pub(crate) struct TestTimerScannerSnapshot {
pub timeout_promise_ptr: usize,
pub timeout_value_bits: u64,
pub callback_ptr: usize,
pub callback_arg_bits: u64,
pub callback_context_store_bits: u64,
pub interval_callback_ptr: usize,
pub interval_context_store_bits: u64,
}

#[cfg(test)]
pub(crate) fn test_seed_timer_scanner_roots(
promise: *mut Promise,
value: f64,
callback: i64,
arg: f64,
context_store: f64,
) {
let context = crate::async_context::test_snapshot_with_store(context_store);
let deadline = Instant::now() + Duration::from_secs(86_400);
TIMER_QUEUE.lock().unwrap().push(Timer {
// #6185: test scaffolding runs on the primary agent.
owner: crate::agent::current_agent(),
deadline,
promise,
value,
has_ref: true,
});
CALLBACK_TIMERS.lock().unwrap().push(CallbackTimer {
// #6185: test scaffolding runs on the primary agent.
owner: crate::agent::current_agent(),
id: TEST_CALLBACK_TIMER_ID,
kind: CallbackTimerKind::Timeout,
deadline,
delay_ms: 86_400_000,
callback,
args: vec![arg],
context: context.clone(),
async_id: 0,
trigger_async_id: 0,
cleared: false,
});
INTERVAL_TIMERS.lock().unwrap().push(IntervalTimer {
// #6185: test scaffolding runs on the primary agent.
owner: crate::agent::current_agent(),
id: TEST_INTERVAL_TIMER_ID,
callback,
interval_ms: 86_400_000,
next_deadline: deadline,
args: Vec::new(),
context,
async_id: 0,
trigger_async_id: 0,
cleared: false,
});
}

#[cfg(test)]
pub(crate) fn test_seed_many_timeout_roots(values: &[f64]) {
let deadline = Instant::now() + Duration::from_secs(86_400);
let mut q = TIMER_QUEUE.lock().unwrap();
q.clear();
for &value in values {
q.push(Timer {
// #6185: test scaffolding runs on the primary agent.
owner: crate::agent::current_agent(),
deadline,
promise: std::ptr::null_mut(),
value,
has_ref: true,
});
}
}

#[cfg(test)]
pub(crate) fn test_clear_all_timer_scanner_roots() {
TIMER_QUEUE.lock().unwrap().clear();
CALLBACK_TIMERS.lock().unwrap().clear();
INTERVAL_TIMERS.lock().unwrap().clear();
}

#[cfg(test)]
pub(crate) fn test_timer_scanner_snapshot() -> TestTimerScannerSnapshot {
let mut snapshot = TestTimerScannerSnapshot::default();
if let Some(timer) = TIMER_QUEUE.lock().unwrap().last() {
snapshot.timeout_promise_ptr = timer.promise as usize;
snapshot.timeout_value_bits = timer.value.to_bits();
}
if let Some(timer) = CALLBACK_TIMERS
.lock()
.unwrap()
.iter()
.find(|timer| timer.id == TEST_CALLBACK_TIMER_ID)
{
snapshot.callback_ptr = timer.callback as usize;
snapshot.callback_arg_bits = timer.args.first().copied().map(f64::to_bits).unwrap_or(0);
snapshot.callback_context_store_bits =
crate::async_context::test_snapshot_first_store(&timer.context)
.map(f64::to_bits)
.unwrap_or(0);
}
if let Some(timer) = INTERVAL_TIMERS
.lock()
.unwrap()
.iter()
.find(|timer| timer.id == TEST_INTERVAL_TIMER_ID)
{
snapshot.interval_callback_ptr = timer.callback as usize;
snapshot.interval_context_store_bits =
crate::async_context::test_snapshot_first_store(&timer.context)
.map(f64::to_bits)
.unwrap_or(0);
}
snapshot
}

#[cfg(test)]
pub(crate) fn test_callback_timer_snapshot(timer_id: i64) -> Option<(usize, u64)> {
CALLBACK_TIMERS
.lock()
.unwrap()
.iter()
.find(|timer| timer.id == timer_id)
.map(|timer| {
(
timer.callback as usize,
timer.args.first().copied().map(f64::to_bits).unwrap_or(0),
)
})
}

#[cfg(test)]
pub(crate) fn test_clear_timer_scanner_roots(promise_before: usize, promise_after: usize) {
TIMER_QUEUE.lock().unwrap().retain(|timer| {
let promise = timer.promise as usize;
promise != promise_before && promise != promise_after
});
CALLBACK_TIMERS
.lock()
.unwrap()
.retain(|timer| timer.id != TEST_CALLBACK_TIMER_ID);
INTERVAL_TIMERS
.lock()
.unwrap()
.retain(|timer| timer.id != TEST_INTERVAL_TIMER_ID);
}

#[cfg(test)]
mod drain_expired_tests;

#[cfg(test)]
mod expired_batch_order_tests {
use super::{order_expired_callback_batch, CallbackTimer, CallbackTimerKind};
use std::time::{Duration, Instant};

fn timer(id: i64, kind: CallbackTimerKind, base: Instant, delay_ms: u64) -> CallbackTimer {
CallbackTimer {
// #6185: test scaffolding runs on the primary agent.
owner: crate::agent::current_agent(),
id,
kind,
deadline: base + Duration::from_millis(delay_ms),
delay_ms,
callback: 0,
args: Vec::new(),
context: crate::async_context::AsyncContextSnapshot::default(),
async_id: 0,
trigger_async_id: 0,
cleared: false,
}
}

/// #6287 case 1: the batch fires in DEADLINE order, not creation order —
/// a 5 ms timer created after a 10 ms one still fires first. Ground truth
/// from node: `setTimeout(f,10); setTimeout(g,5)` runs g then f.
#[test]
fn expired_timeouts_fire_in_deadline_order() {
let base = Instant::now();
let mut batch = vec![
timer(1, CallbackTimerKind::Timeout, base, 10),
timer(2, CallbackTimerKind::Timeout, base, 5),
timer(3, CallbackTimerKind::Timeout, base, 1),
];
order_expired_callback_batch(&mut batch);
let ids: Vec<i64> = batch.iter().map(|t| t.id).collect();
assert_eq!(ids, vec![3, 2, 1], "earliest deadline first");
}

/// Same-deadline timers must STILL fire in creation order — the ordering
/// Perry already got right, preserved by the sort being stable.
#[test]
fn same_deadline_timeouts_keep_creation_order() {
let base = Instant::now();
let mut batch = vec![
timer(1, CallbackTimerKind::Timeout, base, 3),
timer(2, CallbackTimerKind::Timeout, base, 3),
timer(3, CallbackTimerKind::Timeout, base, 3),
];
order_expired_callback_batch(&mut batch);
let ids: Vec<i64> = batch.iter().map(|t| t.id).collect();
assert_eq!(ids, vec![1, 2, 3], "stable sort keeps creation order");
}

/// #6287 case 2: setImmediate runs in the CHECK phase, so an expired
/// setTimeout fires ahead of an immediate scheduled earlier — and this is
/// exactly why a naive sort by deadline alone is wrong (an immediate's
/// deadline is ~now, so it would sort ahead of the timeout). Immediates
/// keep FIFO order among themselves.
#[test]
fn expired_timeouts_precede_immediates_which_stay_fifo() {
let base = Instant::now();
let mut batch = vec![
timer(1, CallbackTimerKind::Immediate, base, 0),
timer(2, CallbackTimerKind::Timeout, base, 5),
timer(3, CallbackTimerKind::Immediate, base, 0),
timer(4, CallbackTimerKind::Timeout, base, 1),
];
order_expired_callback_batch(&mut batch);
let ids: Vec<i64> = batch.iter().map(|t| t.id).collect();
assert_eq!(
ids,
vec![4, 2, 1, 3],
"timeouts by deadline (4 then 2), then immediates FIFO (1 then 3)"
);
}
}
pub(crate) use tests_inline::*;
Loading
Loading