#![cfg_attr(test, allow(clippy::unwrap_used, reason = "test scope"))]
#[cfg(test)]
#[allow(clippy::module_inception)]
mod tests {
use std::{sync::Arc, time::Instant};
use moirai_core::Priority;
use super::super::directory::BlockLookup;
use super::super::registry::{CancelOutcome, TaskRegistry};
use super::super::state::{PRIORITY_FROM_INDEX, task_location};
#[test]
fn lifecycle_token_records_started_and_completed_metadata() {
let registry = TaskRegistry::new();
let (task_id, lifecycle) = registry.register_next_task();
let running = lifecycle.start(3);
let started = registry.get_metadata(task_id).unwrap();
assert_eq!(started.id, task_id);
assert_eq!(started.worker_id, Some(3));
let started_at = started.started_at.expect("start() must stamp started_at");
assert!(started.completed_at.is_none());
let execution_time = running.complete();
let completed = registry.get_metadata(task_id).unwrap();
let completed_at = completed
.completed_at
.expect("complete() must stamp completed_at");
assert!(
completed_at >= started_at,
"completion must not precede start"
);
assert_eq!(completed.started_at, Some(started_at));
assert_eq!(completed.execution_duration(), Some(execution_time));
assert!(registry.is_completed(task_id));
}
#[test]
fn register_next_task_returns_id_and_lifecycle_token() {
let registry = TaskRegistry::new();
let (task_id, lifecycle) = registry.register_next_task();
let running = lifecycle.start(2);
let execution_time = running.complete();
let metadata = registry.get_metadata(task_id).unwrap();
assert_eq!(metadata.id, task_id);
assert_eq!(metadata.worker_id, Some(2));
let started_at = metadata.started_at.expect("start() must stamp started_at");
let completed_at = metadata
.completed_at
.expect("complete() must stamp completed_at");
assert!(
completed_at >= started_at,
"completion must not precede start"
);
assert_eq!(metadata.execution_duration(), Some(execution_time));
}
#[test]
fn lifecycle_token_keeps_its_dense_block_alive_after_registry_drop() {
let (lifecycle, block) = {
let registry = TaskRegistry::new();
let (task_id, lifecycle) = registry.register_next_task();
let (block_index, _) = task_location(task_id);
let block = match registry
.blocks
.read()
.expect("test registry lock")
.lookup(block_index)
{
BlockLookup::Live(block) => Arc::downgrade(block),
_ => panic!("the registered task block must be resident"),
};
assert_eq!(block.strong_count(), 2);
(lifecycle, block)
};
assert_eq!(block.strong_count(), 1);
lifecycle.start(5).complete();
assert!(
block.upgrade().is_none(),
"the token must release its sole block owner after completion"
);
}
#[test]
fn unstarted_lifecycle_token_drop_records_the_task_cancelled() {
let before = Instant::now();
let registry = TaskRegistry::new();
let (task_id, lifecycle) = registry.register_next_task();
drop(lifecycle);
let metadata = registry
.get_metadata(task_id)
.expect("registered task metadata must remain readable");
assert_eq!(metadata.started_at, None);
assert!(metadata.cancelled);
assert!(
metadata.completed_at >= Some(before),
"the drop must stamp a completion time from this test's window"
);
assert_eq!(registry.active_count(), 0);
assert_eq!(registry.completed_count(), 1);
}
#[test]
fn running_lifecycle_token_dropped_unfinished_records_the_task_cancelled() {
let registry = TaskRegistry::new();
let (task_id, lifecycle) = registry.register_next_task();
drop(lifecycle.start(1));
let metadata = registry.get_metadata(task_id).unwrap();
assert!(registry.is_completed(task_id));
assert!(metadata.cancelled);
assert_eq!(metadata.worker_id, Some(1));
}
#[test]
fn running_lifecycle_token_cancel_records_cancelled_and_completed() {
let registry = TaskRegistry::new();
let (task_id, lifecycle) = registry.register_next_task();
lifecycle.start(2).cancel();
let metadata = registry.get_metadata(task_id).unwrap();
assert!(metadata.cancelled);
assert!(registry.is_completed(task_id));
let started_at = metadata.started_at.expect("start() stamps started_at");
let completed_at = metadata.completed_at.expect("cancel stamps completed_at");
assert!(completed_at >= started_at);
assert_eq!(metadata.worker_id, Some(2));
}
#[test]
fn completed_lifecycle_token_is_not_recorded_cancelled() {
let registry = TaskRegistry::new();
let (task_id, lifecycle) = registry.register_next_task();
lifecycle.start(0).complete();
let metadata = registry.get_metadata(task_id).unwrap();
assert!(registry.is_completed(task_id));
assert!(!metadata.cancelled);
}
#[test]
fn register_task_leaves_the_task_queued_for_the_caller_to_drive() {
let registry = TaskRegistry::new();
let task_id = registry.register_task();
let queued = registry.get_metadata(task_id).unwrap();
assert!(!registry.is_completed(task_id));
assert_eq!(queued.started_at, None);
assert_eq!(queued.completed_at, None);
assert!(!queued.cancelled);
assert_eq!(registry.active_count(), 1);
registry.mark_started(task_id, 6);
assert_eq!(registry.get_metadata(task_id).unwrap().worker_id, Some(6));
assert!(!registry.is_completed(task_id));
registry.mark_completed(task_id);
let finished = registry.get_metadata(task_id).unwrap();
assert!(registry.is_completed(task_id));
let started_at = finished.started_at.expect("mark_started stamps started_at");
let completed_at = finished
.completed_at
.expect("mark_completed stamps completed_at");
assert!(completed_at >= started_at);
assert!(!finished.cancelled);
assert_eq!(registry.active_count(), 0);
}
#[test]
fn register_task_pins_its_block_until_the_caller_completes_it() {
use std::time::Duration;
use super::super::state::TASK_STATE_BLOCK_SIZE;
let registry = TaskRegistry::new();
for _ in 1..TASK_STATE_BLOCK_SIZE - 1 {
let (_, lifecycle) = registry.register_next_task();
lifecycle.start(0).complete();
}
let task_id = registry.register_task();
assert_eq!(task_id, (TASK_STATE_BLOCK_SIZE - 1) as u64);
assert_eq!(registry.cleanup_completed(Duration::ZERO), 0);
registry.mark_started(task_id, 3);
assert_eq!(registry.get_metadata(task_id).unwrap().worker_id, Some(3));
registry.mark_completed(task_id);
assert_eq!(registry.cleanup_completed(Duration::ZERO), 1);
assert!(registry.is_completed(task_id));
}
#[test]
fn priority_index_round_trips() {
for priority in [
Priority::Low,
Priority::Normal,
Priority::High,
Priority::Critical,
] {
assert_eq!(PRIORITY_FROM_INDEX[priority.index()], priority);
}
assert_eq!(Priority::Low.index(), 0);
assert_eq!(Priority::Normal.index(), 1);
assert_eq!(Priority::High.index(), 2);
assert_eq!(Priority::Critical.index(), 3);
}
#[test]
fn lifecycle_token_records_spawn_priority() {
let registry = TaskRegistry::new();
let (task_id, lifecycle) = registry.register_next_task();
lifecycle.set_priority(Priority::Critical);
assert_eq!(
registry.get_metadata(task_id).unwrap().priority,
Priority::Critical
);
lifecycle.start(0).complete();
assert_eq!(
registry.get_metadata(task_id).unwrap().priority,
Priority::Critical
);
}
#[test]
fn cancel_before_start_skips_body_and_completes_as_cancelled() {
let before = Instant::now();
let registry = TaskRegistry::new();
let (task_id, lifecycle) = registry.register_next_task();
assert_eq!(
registry.request_cancel(task_id),
Some(CancelOutcome::Requested)
);
assert!(lifecycle.start_unless_cancelled(4).is_none());
let metadata = registry.get_metadata(task_id).unwrap();
assert!(metadata.cancelled);
assert!(
metadata.completed_at >= Some(before),
"cancellation must stamp a completion time from this test's window"
);
assert!(metadata.started_at.is_none());
assert!(registry.is_completed(task_id));
}
#[test]
fn cancel_after_completion_reports_already_completed() {
let registry = TaskRegistry::new();
let (task_id, lifecycle) = registry.register_next_task();
lifecycle.start(0).complete();
assert_eq!(
registry.request_cancel(task_id),
Some(CancelOutcome::AlreadyCompleted)
);
assert!(!registry.get_metadata(task_id).unwrap().cancelled);
}
#[test]
fn cancel_unknown_task_returns_none() {
let registry = TaskRegistry::new();
assert_eq!(registry.request_cancel(999), None);
}
#[test]
fn concurrent_registration_yields_distinct_ids_across_block_growth() {
use std::collections::BTreeSet;
use std::sync::Mutex;
const PRODUCERS: usize = 8;
const PER_PRODUCER: usize = 400;
let registry = Arc::new(TaskRegistry::new());
let ids: Mutex<Vec<u64>> = Mutex::new(Vec::with_capacity(PRODUCERS * PER_PRODUCER));
std::thread::scope(|scope| {
for _ in 0..PRODUCERS {
let registry = Arc::clone(®istry);
let ids = &ids;
scope.spawn(move || {
let mut local = Vec::with_capacity(PER_PRODUCER);
for _ in 0..PER_PRODUCER {
local.push(registry.register_task());
}
ids.lock().expect("test id collector").extend(local);
});
}
});
let ids = ids.into_inner().expect("test id collector");
assert_eq!(ids.len(), PRODUCERS * PER_PRODUCER);
let unique: BTreeSet<u64> = ids.iter().copied().collect();
assert_eq!(
unique.len(),
ids.len(),
"concurrent registration handed out a duplicate task id"
);
for id in ids {
assert!(
registry.get_metadata(id).is_some(),
"registered task {id} has no slot"
);
}
}
#[test]
fn waker_registered_after_completion_is_not_retained() {
use std::sync::atomic::{AtomicUsize, Ordering};
use std::task::{RawWaker, RawWakerVTable, Waker};
static DROPPED: AtomicUsize = AtomicUsize::new(0);
unsafe fn clone_raw(data: *const ()) -> RawWaker {
let arc = unsafe { Arc::from_raw(data.cast::<usize>()) };
let cloned = Arc::clone(&arc);
core::mem::forget(arc);
RawWaker::new(Arc::into_raw(cloned).cast(), &VTABLE)
}
unsafe fn wake_raw(data: *const ()) {
drop(unsafe { Arc::from_raw(data.cast::<usize>()) });
DROPPED.fetch_add(1, Ordering::SeqCst);
}
unsafe fn wake_by_ref_raw(_: *const ()) {}
unsafe fn drop_raw(data: *const ()) {
drop(unsafe { Arc::from_raw(data.cast::<usize>()) });
DROPPED.fetch_add(1, Ordering::SeqCst);
}
static VTABLE: RawWakerVTable =
RawWakerVTable::new(clone_raw, wake_raw, wake_by_ref_raw, drop_raw);
let payload = Arc::new(0usize);
let probe = Arc::clone(&payload);
let waker = unsafe { Waker::from_raw(RawWaker::new(Arc::into_raw(probe).cast(), &VTABLE)) };
let registry = TaskRegistry::new();
let id = registry.register_task();
registry.mark_started(id, 0);
registry.mark_completed(id);
assert!(
registry.register_waker(id, &waker),
"the slot is still registered, so registration reports success"
);
drop(waker);
assert_eq!(
Arc::strong_count(&payload),
1,
"a waker registered after completion is still held by the registry slot; nothing will ever take it, so it leaks for the life of the slot"
);
}
}