use std::ffi::c_void;
use std::ptr;
use windows_sys::Win32::Storage::FileSystem::{
IORING_BUFFER_REF, IORING_BUFFER_REF_0, IORING_CQE, IORING_HANDLE_REF, IORING_HANDLE_REF_0,
IORING_REF_RAW, IOSQE_FLAGS_DRAIN_PRECEDING_OPS,
};
use super::{Resolver, ResolverConfig, ResolverStats};
use crate::sys::Responses;
const SEEDS: std::ops::Range<u64> = 0..2048;
const SEED_COUNT: usize = 2048;
const DISTINCT_ORDERS_OF_SIX: usize = 720;
fn handle() -> IORING_HANDLE_REF {
IORING_HANDLE_REF {
Kind: IORING_REF_RAW,
Handle: IORING_HANDLE_REF_0 {
Handle: ptr::null_mut(),
},
}
}
fn build_read(resolver: &mut Resolver, user_data: usize, bytes: u32) {
let hr = unsafe {
resolver.build_read(
ptr::null_mut(),
handle(),
IORING_BUFFER_REF {
Kind: IORING_REF_RAW,
Buffer: IORING_BUFFER_REF_0 {
Address: ptr::null_mut(),
},
},
bytes,
0,
user_data,
0,
)
};
assert_eq!(hr, 0, "a build should report success");
}
fn build_flush(resolver: &mut Resolver, user_data: usize, barrier: bool) {
let flags = if barrier {
IOSQE_FLAGS_DRAIN_PRECEDING_OPS
} else {
0
};
let hr = unsafe { resolver.build_flush(ptr::null_mut(), handle(), 0, user_data, flags) };
assert_eq!(hr, 0, "a build should report success");
}
fn submit(resolver: &mut Resolver) -> i32 {
let mut submitted = 0_u32;
unsafe { resolver.submit(ptr::null_mut(), 0, 0, &raw mut submitted) }
}
fn submit_waiting(resolver: &mut Resolver) -> i32 {
let mut submitted = 0_u32;
unsafe { resolver.submit(ptr::null_mut(), 1, 50, &raw mut submitted) }
}
fn pop_full(resolver: &mut Resolver) -> Option<(usize, i32, usize)> {
let mut cqe = IORING_CQE {
UserData: 0,
ResultCode: 0,
Information: 0,
};
let hr = unsafe { resolver.pop(ptr::null_mut(), &raw mut cqe) };
if hr == 1 {
return None;
}
assert_eq!(
hr, 0,
"a pop should either succeed or report an empty queue"
);
Some((cqe.UserData, cqe.ResultCode, cqe.Information))
}
fn pop(resolver: &mut Resolver) -> Option<(usize, i32)> {
let mut cqe = IORING_CQE {
UserData: 0,
ResultCode: 0,
Information: 0,
};
let hr = unsafe { resolver.pop(ptr::null_mut(), &raw mut cqe) };
if hr == 1 {
return None;
}
assert_eq!(
hr, 0,
"a pop should either succeed or report an empty queue"
);
Some((cqe.UserData, cqe.ResultCode))
}
fn drain(resolver: &mut Resolver, expected: usize) -> Vec<(usize, i32)> {
let mut seen = Vec::new();
for _ in 0..(expected + 2) * (super::MAX_DEFERRALS as usize + 2) {
while let Some(completion) = pop(resolver) {
seen.push(completion);
}
if seen.len() == expected {
return seen;
}
submit(resolver);
}
panic!(
"resolution did not finish: {} of {expected} completions after the bound, which is \
RS-C-1 not holding in finite time",
seen.len()
);
}
fn sweep(config: ResolverConfig, mut body: impl FnMut(&mut Resolver)) -> Vec<ResolverStats> {
SEEDS
.map(|seed| {
let mut resolver = Resolver::with_config(seed, config);
let watch = resolver.watch();
body(&mut resolver);
watch.stats()
})
.collect()
}
#[test]
fn every_submitted_operation_completes_exactly_once_carrying_its_own_identity() {
for seed in SEEDS {
let mut resolver = Resolver::new(seed);
for id in 1..=8_usize {
build_flush(&mut resolver, id, false);
}
submit(&mut resolver);
let mut ids: Vec<usize> = drain(&mut resolver, 8)
.into_iter()
.map(|(id, _)| id)
.collect();
ids.sort_unstable();
assert_eq!(
ids,
(1..=8).collect::<Vec<_>>(),
"seed {seed}: every operation must complete exactly once, carrying the user data \
it was built with"
);
}
}
#[test]
fn an_operation_that_was_never_submitted_does_not_complete() {
let mut resolver = Resolver::new(1);
for id in 1..=4_usize {
build_flush(&mut resolver, id, false);
}
for poll in 0..=(super::MAX_DEFERRALS + 2) {
assert_eq!(
pop(&mut resolver),
None,
"poll {poll}: nothing may complete before a submit carries it"
);
}
let stats = resolver.watch().stats();
assert_eq!(stats.built, 4, "four operations were built");
assert_eq!(
stats.submitted, 0,
"none was submitted, so none may have become eligible"
);
assert_eq!(stats.posted, 0, "and none may have been posted");
}
#[test]
fn nothing_queued_before_a_drained_flush_completes_after_it() {
for seed in SEEDS {
let mut resolver = Resolver::new(seed);
for id in 1..=3_usize {
build_flush(&mut resolver, id, false);
}
build_flush(&mut resolver, 100, true);
for id in 4..=6_usize {
build_flush(&mut resolver, id, false);
}
submit(&mut resolver);
let order: Vec<usize> = drain(&mut resolver, 7)
.into_iter()
.map(|(id, _)| id)
.collect();
let barrier_at = order
.iter()
.position(|&id| id == 100)
.expect("the barrier must complete");
for id in 1..=3_usize {
let at = order
.iter()
.position(|&candidate| candidate == id)
.expect("every operation completes");
assert!(
at < barrier_at,
"seed {seed}: operation {id} was queued before the drained flush and completed \
after it, which RS-C-4 forbids -- order was {order:?}"
);
}
}
}
#[test]
fn the_hold_back_half_of_the_drain_flag_is_not_constrained() {
let overtook = SEEDS
.filter(|&seed| {
let mut resolver = Resolver::new(seed);
build_flush(&mut resolver, 1, false);
build_flush(&mut resolver, 100, true);
build_flush(&mut resolver, 2, false);
submit(&mut resolver);
let order: Vec<usize> = drain(&mut resolver, 3)
.into_iter()
.map(|(id, _)| id)
.collect();
let barrier_at = order.iter().position(|&id| id == 100).unwrap();
let after_at = order.iter().position(|&id| id == 2).unwrap();
after_at < barrier_at
})
.count();
assert!(
overtook > 0,
"no seed let an operation queued after a drained flush complete before it, so the \
resolver is enforcing a hold-back D-47 withdrew"
);
}
#[test]
fn a_barrier_is_actually_held_back_rather_than_vacuously_satisfied() {
let stats = sweep(ResolverConfig::default(), |resolver| {
build_flush(resolver, 1, false);
build_flush(resolver, 2, false);
build_flush(resolver, 100, true);
submit(resolver);
drain(resolver, 3);
});
let held: usize = stats.iter().map(|s| s.barrier_holds).sum();
assert!(
held > 0,
"no seed ever held a barrier back, so RS-C-4 was satisfied vacuously and this suite \
proves nothing about it"
);
}
#[test]
fn an_operation_may_pend_rather_than_complete_inside_the_submit() {
let stats = sweep(ResolverConfig::default(), |resolver| {
for id in 1..=6_usize {
build_flush(resolver, id, false);
}
submit(resolver);
drain(resolver, 6);
});
assert!(
stats.iter().any(|s| s.deferrals > 0),
"no seed left an operation unresolved by the submit that carried it, so RS-P-1's \
pending case is permitted but never exercised"
);
}
#[test]
fn narrowing_rs_p_1_completes_everything_inside_the_submit() {
for seed in SEEDS {
let mut resolver = Resolver::with_config(
seed,
ResolverConfig {
may_pend: false,
..ResolverConfig::narrowest()
},
);
for id in 1..=5_usize {
build_flush(&mut resolver, id, false);
}
submit(&mut resolver);
let mut seen = Vec::new();
while let Some((id, _)) = pop(&mut resolver) {
seen.push(id);
}
assert_eq!(
seen.len(),
5,
"seed {seed}: with RS-P-1 narrowed, every operation completes inside its submit"
);
}
}
#[test]
fn completion_order_is_unconstrained() {
let reordered = SEEDS
.filter(|&seed| {
let mut resolver = Resolver::with_config(
seed,
ResolverConfig {
may_reorder: true,
..ResolverConfig::narrowest()
},
);
for id in 1..=5_usize {
build_flush(&mut resolver, id, false);
}
submit(&mut resolver);
let order: Vec<usize> = drain(&mut resolver, 5)
.into_iter()
.map(|(id, _)| id)
.collect();
order != (1..=5).collect::<Vec<_>>()
})
.count();
assert!(
reordered > SEED_COUNT / 2,
"only {reordered} of {SEED_COUNT} seeds reordered, which is too few for a clause that \
permits any permutation"
);
}
#[test]
fn narrowing_rs_p_2_posts_in_submission_order() {
for seed in SEEDS {
let mut resolver = Resolver::with_config(seed, ResolverConfig::narrowest());
for id in 1..=5_usize {
build_flush(&mut resolver, id, false);
}
submit(&mut resolver);
let order: Vec<usize> = drain(&mut resolver, 5)
.into_iter()
.map(|(id, _)| id)
.collect();
assert_eq!(
order,
(1..=5).collect::<Vec<_>>(),
"seed {seed}: with RS-P-2 narrowed the order is submission order"
);
}
}
#[test]
fn an_operation_may_fail_while_its_neighbours_succeed() {
let mixed = SEEDS
.filter(|&seed| {
let mut resolver = Resolver::with_config(
seed,
ResolverConfig {
may_fail_operations: true,
..ResolverConfig::narrowest()
},
);
for id in 1..=6_usize {
build_flush(&mut resolver, id, false);
}
submit(&mut resolver);
let results = drain(&mut resolver, 6);
let failures = results.iter().filter(|(_, hr)| *hr != 0).count();
failures > 0 && failures < results.len()
})
.count();
assert!(
mixed > 0,
"no seed produced a batch in which some operations failed and others succeeded"
);
}
#[test]
fn failure_codes_are_not_drawn_from_a_small_fixed_set() {
let mut codes = std::collections::BTreeSet::new();
for seed in SEEDS {
let mut resolver = Resolver::with_config(
seed,
ResolverConfig {
may_fail_operations: true,
..ResolverConfig::narrowest()
},
);
for id in 1..=4_usize {
build_flush(&mut resolver, id, false);
}
submit(&mut resolver);
for (_, hr) in drain(&mut resolver, 4) {
if hr != 0 {
codes.insert(hr);
}
}
}
assert!(
codes.len() > 16,
"only {} distinct failure codes across {SEED_COUNT} seeds, which is a small fixed set \
in all but name",
codes.len()
);
}
#[test]
fn a_wait_may_expire() {
let expired = SEEDS
.filter(|&seed| {
let mut resolver = Resolver::with_config(
seed,
ResolverConfig {
may_expire_waits: true,
..ResolverConfig::narrowest()
},
);
build_flush(&mut resolver, 1, false);
let hr = submit_waiting(&mut resolver);
let expired = hr == super::IORING_E_WAIT_TIMEOUT;
drain(&mut resolver, 1);
expired
})
.count();
assert!(
expired > 0,
"no seed expired a wait, so RS-P-4 is permitted but never exercised"
);
}
#[test]
fn a_wait_may_expire_even_when_a_completion_is_available() {
let expired_with_work = SEEDS
.filter(|&seed| {
let mut resolver = Resolver::with_config(
seed,
ResolverConfig {
may_expire_waits: true,
..ResolverConfig::narrowest()
},
);
build_flush(&mut resolver, 1, false);
let hr = submit_waiting(&mut resolver);
let available = !resolver.posted.is_empty();
drain(&mut resolver, 1);
hr == super::IORING_E_WAIT_TIMEOUT && available
})
.count();
assert!(
expired_with_work > 0,
"no seed expired a wait while a completion sat in the queue, so RS-P-4's \
over-provision is unexercised and a consumer could still read an expiry as emptiness"
);
}
#[test]
fn a_successful_wait_promises_nothing_about_poppability() {
let stats = sweep(
ResolverConfig {
may_wake_empty: true,
may_pend: true,
..ResolverConfig::narrowest()
},
|resolver| {
for id in 1..=3_usize {
build_flush(resolver, id, false);
}
submit_waiting(resolver);
drain(resolver, 3);
},
);
assert!(
stats.iter().any(|s| s.empty_wakes > 0),
"no seed returned a successful wait with an empty queue, so RS-P-5 is unexercised"
);
}
#[test]
fn a_completion_posted_behind_another_raises_no_signal_of_its_own() {
let event = unsafe {
windows_sys::Win32::System::Threading::CreateEventW(ptr::null(), 1, 0, ptr::null())
};
assert!(!event.is_null(), "CreateEventW failed");
let mut resolver = Resolver::with_config(
7,
ResolverConfig {
may_pend: false,
..ResolverConfig::narrowest()
},
);
let hr = unsafe { resolver.set_completion_event(ptr::null_mut(), event.cast::<c_void>()) };
assert_eq!(hr, 0);
for id in 1..=4_usize {
build_flush(&mut resolver, id, false);
}
submit(&mut resolver);
assert_eq!(
resolver.watch().stats().signals,
1,
"four completions posted back to back must raise exactly one signal -- the \
empty-to-non-empty edge"
);
while pop(&mut resolver).is_some() {}
build_flush(&mut resolver, 5, false);
submit(&mut resolver);
assert_eq!(
resolver.watch().stats().signals,
2,
"a post onto a queue that had drained is a fresh edge and must signal"
);
unsafe { windows_sys::Win32::Foundation::CloseHandle(event) };
}
#[test]
fn a_failed_submit_leaves_operations_queued_for_a_later_one() {
let stats = sweep(
ResolverConfig {
may_fail_submits: true,
..ResolverConfig::narrowest()
},
|resolver| {
for id in 1..=4_usize {
build_flush(resolver, id, false);
}
submit(resolver);
drain(resolver, 4);
},
);
let failed: usize = stats.iter().map(|s| s.failed_submits).sum();
assert!(
failed > 0,
"no seed declined a submit, so RS-P-7 is unexercised"
);
for (seed, s) in stats.iter().enumerate() {
assert_eq!(
s.popped, 4,
"seed {seed}: a declined submit must leave the operations queued, not lose them"
);
}
}
#[test]
fn one_seed_replays_a_whole_resolution() {
let run = |seed| {
let mut resolver = Resolver::new(seed);
for id in 1..=6_usize {
build_flush(&mut resolver, id, id == 3);
}
submit(&mut resolver);
(drain(&mut resolver, 6), resolver.watch().stats())
};
for seed in SEEDS {
assert_eq!(
run(seed),
run(seed),
"seed {seed} did not replay identically"
);
}
}
#[test]
fn different_seeds_reach_different_resolutions() {
let orders: std::collections::BTreeSet<Vec<usize>> = SEEDS
.map(|seed| {
let mut resolver = Resolver::with_config(
seed,
ResolverConfig {
may_reorder: true,
..ResolverConfig::narrowest()
},
);
for id in 1..=6_usize {
build_flush(&mut resolver, id, false);
}
submit(&mut resolver);
drain(&mut resolver, 6)
.into_iter()
.map(|(id, _)| id)
.collect()
})
.collect();
assert!(
orders.len() > DISTINCT_ORDERS_OF_SIX / 2,
"only {} of {DISTINCT_ORDERS_OF_SIX} possible orders across {SEED_COUNT} seeds",
orders.len()
);
}
#[test]
fn the_environment_seed_is_parsed_in_both_spellings_and_rejected_when_malformed() {
let parse = |text: &str| -> Option<u64> {
let trimmed = text.trim();
trimmed
.strip_prefix("0x")
.or_else(|| trimmed.strip_prefix("0X"))
.map_or_else(
|| trimmed.parse::<u64>().ok(),
|hex| u64::from_str_radix(hex, 16).ok(),
)
};
assert_eq!(parse("42"), Some(42));
assert_eq!(parse("0x2A"), Some(42));
assert_eq!(parse("0X2a"), Some(42));
assert_eq!(parse(" 0x2A "), Some(42));
assert_eq!(
parse("2A"),
None,
"bare hex is not decimal and must not parse"
);
assert_eq!(parse("banana"), None);
assert_eq!(parse(""), None);
}
#[test]
fn the_replay_hint_names_this_axis_and_the_seed_it_pins() {
let resolver = Resolver::new(0x2A);
assert_eq!(
resolver.replay_hint(),
format!("$env:{}='0x000000000000002A'", super::SEED_VAR)
);
assert_eq!(resolver.seed(), 0x2A);
}
#[test]
fn the_default_configuration_is_the_widest_point_in_the_space() {
let wide = ResolverConfig::default();
assert!(wide.may_pend);
assert!(wide.may_reorder);
assert!(wide.may_fail_operations);
assert!(wide.may_expire_waits);
assert!(wide.may_wake_empty);
assert!(wide.may_fail_submits);
let narrow = ResolverConfig::narrowest();
assert!(!narrow.may_pend);
assert!(!narrow.may_reorder);
assert!(!narrow.may_fail_operations);
assert!(!narrow.may_expire_waits);
assert!(!narrow.may_wake_empty);
assert!(!narrow.may_fail_submits);
assert!(wide.edge_triggered_signal);
assert!(narrow.edge_triggered_signal);
}
#[test]
fn a_successful_transfer_may_report_fewer_bytes_than_requested() {
const LEN: u32 = 4096;
let mut short_seen = 0_usize;
let mut full_seen = 0_usize;
for seed in SEEDS {
let mut resolver = Resolver::with_config(
seed,
ResolverConfig {
may_transfer_partially: true,
..ResolverConfig::narrowest()
},
);
for id in 1..=8_usize {
build_read(&mut resolver, id, LEN);
}
submit(&mut resolver);
while let Some((_, result, information)) = pop_full(&mut resolver) {
assert_eq!(result, 0, "narrowest() forbids a failed operation");
assert!(
information <= LEN as usize,
"a transfer reported {information} bytes for a {LEN}-byte request, \
which is over-delivery rather than a short count; seed {seed:#x}"
);
if information == LEN as usize {
full_seen += 1;
} else {
short_seen += 1;
}
}
}
assert!(
short_seen > 0,
"no seed reported a short transfer, so RS-P-8 is unexercised"
);
assert!(
full_seen > 0,
"every transfer was short, so the clause is being applied unconditionally \
rather than as a permission"
);
for seed in SEEDS {
let mut resolver = Resolver::with_config(seed, ResolverConfig::narrowest());
for id in 1..=8_usize {
build_read(&mut resolver, id, LEN);
}
submit(&mut resolver);
while let Some((_, _, information)) = pop_full(&mut resolver) {
assert_eq!(
information, LEN as usize,
"a resolver with may_transfer_partially off reported a short \
transfer; seed {seed:#x}"
);
}
}
}
#[test]
fn the_configuration_has_a_switch_for_every_permission_and_none_for_any_constraint() {
let rendered = format!("{:?}", ResolverConfig::default());
let fields = rendered.matches(": ").count();
assert_eq!(
fields, 8,
"ResolverConfig has {fields} fields; RESPONSE-SPACE.md specifies eight permissions \
(RS-P-1..8), and a constraint must never become a switch -- {rendered}"
);
}