use errno::Errno;
use hwlocality::{
ProcessId,
bitmap::{Bitmap, BitmapIndex, BitmapKind, BitmapRef, SpecializedBitmap, SpecializedBitmapRef},
cpu::{
binding::{CpuBindingError, CpuBindingFlags, CpuBoundObject},
cpuset::CpuSet,
},
errors::{HybridError, RawHwlocError},
memory::{
binding::{
Bytes, MemoryAllocationError, MemoryBindingError, MemoryBindingFlags,
MemoryBindingPolicy, MemoryBoundObject,
},
nodeset::NodeSet,
},
topology::{
Topology,
support::{FeatureSupport, MemoryBindingSupport},
},
};
use proptest::{prelude::*, test_runner::TestRunner};
use std::{
borrow::{Borrow, BorrowMut},
collections::HashSet,
ffi::c_uint,
fmt::Debug,
ops::{Deref, DerefMut},
ptr,
};
use strum::IntoEnumIterator;
use tracing_error::{ErrorLayer, SpanTrace, SpanTraceStatus};
use tracing_subscriber::prelude::*;
macro_rules! test_deref_set {
($fn_mut_of_deref_set:expr, $topology:expr, $set:expr $(, $other_args:expr)*) => {{
$fn_mut_of_deref_set($topology, &$set $(, $other_args)*)?;
let set_ref = BitmapRef::from(&$set);
$fn_mut_of_deref_set($topology, set_ref $(, $other_args)*)
}};
}
macro_rules! test_specialized_bitmap {
($fn_mut_of_deref_set:expr, $topology:expr, $cpuset:expr, $nodeset:expr $(, $other_args:expr)*) => {{
test_deref_set!($fn_mut_of_deref_set, $topology, $cpuset $(, $other_args)*)?;
test_deref_set!($fn_mut_of_deref_set, $topology, $nodeset $(, $other_args)*)
}};
}
#[test]
fn single_threaded_test() {
if let Err(e) = single_threaded_test_impl() {
panic!("{e}");
}
}
fn single_threaded_test_impl() -> Result<(), TestCaseError> {
let subscriber = tracing_subscriber::Registry::default().with(ErrorLayer::default());
tracing::subscriber::set_global_default(subscriber).unwrap();
let topology = Topology::test_instance();
let my_pid = std::process::id();
let my_tid = hwlocality::current_thread_id();
dbg!(topology.feature_support());
if let Some(cpubind_support) = topology.feature_support().cpu_binding() {
dbg!(topology.cpuset());
dbg!(topology.complete_cpuset());
let curr_proc = cpubind_support.set_current_process();
let curr_thr = cpubind_support.set_current_thread();
let can_bind_self = |flags| match flags & target_cpubind_flags() {
CpuBindingFlags::PROCESS => curr_proc,
CpuBindingFlags::THREAD => curr_thr,
CpuBindingFlags::ASSUME_SINGLE_THREAD => curr_proc | curr_thr,
_ => true, };
TestRunner::default().run(
&(
topology_related_set(Topology::complete_cpuset),
any_cpubind_flags(),
),
|(cpuset, flags)| {
if can_bind_self(flags) {
test_deref_set!(
test_bind_cpu,
topology,
cpuset,
flags,
CpuBoundObject::ThisProgram
)?;
}
if cpubind_support.set_process() {
test_deref_set!(
test_bind_cpu,
topology,
cpuset,
flags,
CpuBoundObject::ProcessOrThread(my_pid)
)?;
}
if cpubind_support.set_thread() {
test_deref_set!(
test_bind_cpu,
topology,
cpuset,
flags,
CpuBoundObject::Thread(my_tid)
)?;
}
Ok(())
},
)?;
TestRunner::default().run(&any_cpubind_flags(), |flags| {
let target_flags = flags & target_cpubind_flags();
let can_query_self = |curr_proc, curr_thr| match target_flags {
CpuBindingFlags::PROCESS => curr_proc,
CpuBindingFlags::THREAD => curr_thr,
CpuBindingFlags::ASSUME_SINGLE_THREAD => curr_proc | curr_thr,
_ => true, };
if can_query_self(
cpubind_support.get_current_process(),
cpubind_support.get_current_thread(),
) {
test_cpu_binding(topology, flags, CpuBoundObject::ThisProgram)?;
}
if cpubind_support.get_process() {
test_cpu_binding(topology, flags, CpuBoundObject::ProcessOrThread(my_pid))?;
}
if cpubind_support.set_thread() {
test_cpu_binding(topology, flags, CpuBoundObject::Thread(my_tid))?;
}
if can_query_self(
cpubind_support.get_current_process_last_cpu_location(),
cpubind_support.get_current_thread_last_cpu_location(),
) {
test_last_cpu_location(topology, flags, CpuBoundObject::ThisProgram)?;
}
if cpubind_support.get_process_last_cpu_location() {
test_last_cpu_location(topology, flags, CpuBoundObject::ProcessOrThread(my_pid))?;
}
Ok(())
})?;
}
if let Some(membind_support) = topology.feature_support().memory_binding() {
dbg!(topology.nodeset());
dbg!(topology.complete_nodeset());
let max_area_size = 48 * 1024 * 1024;
let any_len = prop_oneof![
1 => Just(0usize),
2 => 1usize..max_area_size
];
TestRunner::default().run(&any_len, |len| test_allocate_memory(topology, len))?;
let can_bind_thisproc = membind_support.set_current_process();
let can_bind_thisthread = membind_support.set_current_thread();
let can_bind_self = |flags| match flags & target_membind_flags() {
MemoryBindingFlags::PROCESS => can_bind_thisproc,
MemoryBindingFlags::THREAD => can_bind_thisthread,
MemoryBindingFlags::ASSUME_SINGLE_THREAD => can_bind_thisproc | can_bind_thisthread,
_ => true, };
TestRunner::default().run(
&(
&any_len,
topology_related_set(Topology::complete_cpuset),
topology_related_set(Topology::complete_nodeset),
any_membind_policy(),
any_membind_flags(),
),
|(len, cpuset, nodeset, policy, flags)| {
if membind_support.allocate_bound() {
test_specialized_bitmap!(
test_allocate_bound_memory,
topology,
cpuset,
nodeset,
len,
policy,
flags
)?;
}
if membind_support.allocate_bound() || can_bind_self(flags) {
test_specialized_bitmap!(
test_binding_allocate_memory,
topology,
cpuset,
nodeset,
len,
policy,
flags
)?;
}
Ok(())
},
)?;
TestRunner::default().run(
&(
topology_related_set(Topology::complete_cpuset),
topology_related_set(Topology::complete_nodeset),
any_membind_policy(),
any_membind_flags(),
),
|(cpuset, nodeset, policy, flags)| {
if can_bind_self(flags) {
test_specialized_bitmap!(
test_bind_memory,
topology,
cpuset,
nodeset,
policy,
flags
)?;
test_unbind_memory(topology, flags)?;
}
if membind_support.set_process() {
test_specialized_bitmap!(
test_bind_process_memory,
topology,
cpuset,
nodeset,
my_pid,
policy,
flags
)?;
test_unbind_process_memory(topology, my_pid, flags)?;
}
Ok(())
},
)?;
let can_query_thisproc = membind_support.get_current_process();
let can_query_thisthread = membind_support.get_current_thread();
let can_query_self = |flags| match flags & target_membind_flags() {
MemoryBindingFlags::PROCESS => can_query_thisproc,
MemoryBindingFlags::THREAD => can_query_thisthread,
MemoryBindingFlags::ASSUME_SINGLE_THREAD => can_query_thisproc | can_query_thisthread,
_ => true, };
TestRunner::default().run(&any_membind_flags(), |flags| {
if can_query_self(flags) {
test_query_self_membind(topology, flags)?;
}
if membind_support.get_process() {
test_query_process_membind(topology, my_pid, flags)?;
}
Ok(())
})?;
let bytes = vec![24u8; max_area_size];
let full_index_range = 0usize..max_area_size;
let any_index_range = prop_oneof![
1 => full_index_range.clone().prop_map(|idx| idx..idx),
4 => [full_index_range.clone(), full_index_range].prop_map(|[idx1, idx2]| {
idx1.min(idx2)..idx1.max(idx2)
})
];
TestRunner::default().run(
&(
any_index_range,
topology_related_set(Topology::complete_cpuset),
topology_related_set(Topology::complete_nodeset),
any_membind_policy(),
any_membind_flags(),
),
|(idx_range, cpuset, nodeset, policy, flags)| {
let target = &bytes[idx_range];
if membind_support.set_area() {
test_specialized_bitmap!(
test_bind_memory_area,
topology,
cpuset,
nodeset,
target,
policy,
flags
)?;
test_unbind_memory_area(topology, target, flags)?;
}
if membind_support.get_area() {
test_query_area_membind(topology, target, flags)?;
}
if membind_support.get_area_memory_location() {
test_query_area_memloc(topology, target, flags)?;
}
Ok(())
},
)?;
}
Ok(())
}
trait BoundObject {
fn can_be_process(&self) -> bool;
}
impl BoundObject for MemoryBoundObject {
fn can_be_process(&self) -> bool {
matches!(self, MemoryBoundObject::Process(_))
}
}
impl BoundObject for CpuBoundObject {
fn can_be_process(&self) -> bool {
matches!(self, CpuBoundObject::ProcessOrThread(_))
}
}
macro_rules! handle_windows_edge_cases {
($result:expr, $target:expr, $early_exit:expr) => {
if cfg!(windows) {
if let Err(HybridError::Hwloc(RawHwlocError { errno, .. })) = $result {
match errno {
None => return $early_exit,
Some(Errno(6)) if $target.can_be_process() => return $early_exit,
_ => {}
}
}
}
};
}
macro_rules! fail {
($message_fmt:expr $(, $args:expr)*) => {{
let trace = SpanTrace::capture();
let message = format!($message_fmt $(, $args)*);
let error = if trace.status() == SpanTraceStatus::CAPTURED {
TestCaseError::fail(format!("{}\nSpantrace:\n{trace}", message))
} else {
TestCaseError::fail(message)
};
return Err(error)
}}
}
#[tracing::instrument(skip(topology))]
fn test_allocate_memory(topology: &Topology, len: usize) -> Result<(), TestCaseError> {
let result = topology.allocate_memory(len);
match result {
Ok(bytes) => check_memory_allocation(bytes, len),
Err(HybridError::Rust(MemoryAllocationError::AllocationFailed)) => Ok(()),
#[cfg(windows)]
Err(HybridError::Rust(MemoryAllocationError::Unknown)) => Ok(()),
Err(other) => fail!("Got unexpected memory allocation error: {other}"),
}
}
#[tracing::instrument(skip(topology))]
fn test_allocate_bound_memory(
topology: &Topology,
set: impl SpecializedBitmapRef + Copy + Debug,
len: usize,
policy: MemoryBindingPolicy,
flags: MemoryBindingFlags,
) -> Result<(), TestCaseError> {
let result = topology.allocate_bound_memory(len, set, policy, flags);
check_allocate_bound(
result,
MemoryBoundObject::Area,
topology,
&*set,
len,
policy,
flags,
)
}
#[tracing::instrument(skip(topology))]
fn test_binding_allocate_memory(
topology: &Topology,
set: impl SpecializedBitmapRef + Copy + Debug,
len: usize,
policy: MemoryBindingPolicy,
flags: MemoryBindingFlags,
) -> Result<(), TestCaseError> {
let result = topology.binding_allocate_memory(len, set, policy, flags);
check_allocate_bound(
result,
MemoryBoundObject::ThisProgram,
topology,
&*set,
len,
policy,
flags,
)
}
#[tracing::instrument(skip(result, topology))]
fn check_allocate_bound<Set: SpecializedBitmap>(
result: Result<Bytes<'_>, HybridError<MemoryBindingError<Set>>>,
target: MemoryBoundObject,
topology: &Topology,
set: &Set,
len: usize,
policy: MemoryBindingPolicy,
flags: MemoryBindingFlags,
) -> Result<(), TestCaseError> {
let Some(bytes) = post_bind_checks(result, target, topology, set, policy, flags, |bytes| {
(topology.supports(
FeatureSupport::memory_binding,
MemoryBindingSupport::get_area,
) && len > 0)
.then(|| topology.area_memory_binding::<_, Set>(&bytes[..], MemoryBindingFlags::STRICT))
})?
else {
return Ok(());
};
check_memory_allocation(bytes, len)
}
#[tracing::instrument(skip(bytes))]
fn check_memory_allocation(mut bytes: Bytes<'_>, len: usize) -> Result<(), TestCaseError> {
if bytes.len() != len {
fail!(
"Final allocation has {} bytes but {len} were requested",
bytes.len()
);
}
prop_assert!(ptr::eq(AsRef::as_ref(&bytes), AsMut::as_mut(&mut bytes)));
prop_assert!(ptr::eq(AsRef::as_ref(&bytes), Borrow::borrow(&bytes)));
prop_assert!(ptr::eq(
AsRef::as_ref(&bytes),
BorrowMut::borrow_mut(&mut bytes)
));
prop_assert!(ptr::eq(AsRef::as_ref(&bytes), Deref::deref(&bytes)));
prop_assert!(ptr::eq(
AsRef::as_ref(&bytes),
DerefMut::deref_mut(&mut bytes)
));
if len < 10 {
let bytes_debug = format!("{bytes:?}");
prop_assert!(bytes_debug.starts_with("[") && bytes_debug.ends_with("]"));
}
if len > 0 {
let first_byte = bytes.first_mut().unwrap().as_mut_ptr();
unsafe { first_byte.write_volatile(42) };
let last_byte = bytes.last_mut().unwrap().as_mut_ptr();
unsafe { last_byte.write_volatile(142) };
}
Ok(())
}
#[tracing::instrument(skip(topology))]
fn test_bind_memory(
topology: &Topology,
set: impl SpecializedBitmapRef + Copy + Debug,
policy: MemoryBindingPolicy,
flags: MemoryBindingFlags,
) -> Result<(), TestCaseError> {
let result = topology.bind_memory(set, policy, flags);
post_bind_checks(
result,
MemoryBoundObject::ThisProgram,
topology,
&*set,
policy,
flags,
self_memory_binding_getter(topology, flags),
)?;
Ok(())
}
#[tracing::instrument(skip(topology))]
fn test_bind_process_memory(
topology: &Topology,
set: impl SpecializedBitmapRef + Copy + Debug,
pid: ProcessId,
policy: MemoryBindingPolicy,
flags: MemoryBindingFlags,
) -> Result<(), TestCaseError> {
let result = topology.bind_process_memory(pid, set, policy, flags);
post_bind_checks(
result,
MemoryBoundObject::Process(pid),
topology,
&*set,
policy,
flags,
process_memory_binding_getter(topology, pid),
)?;
Ok(())
}
#[tracing::instrument(skip(topology, target))]
fn test_bind_memory_area(
topology: &Topology,
set: impl SpecializedBitmapRef + Copy + Debug,
target: &[u8],
policy: MemoryBindingPolicy,
flags: MemoryBindingFlags,
) -> Result<(), TestCaseError> {
let result = topology.bind_memory_area(target, set, policy, flags);
if target.is_empty() {
if result == Ok(()) {
return Ok(());
} else {
fail!("Unexpected result upon binding an empty memory area: {result:?}");
}
}
post_bind_checks(
result,
MemoryBoundObject::Area,
topology,
&*set,
policy,
flags,
area_memory_binding_getter(topology, target),
)?;
Ok(())
}
#[tracing::instrument(skip(result, topology, try_get_final_binding))]
fn post_bind_checks<Set: SpecializedBitmap, Res: Debug>(
result: Result<Res, HybridError<MemoryBindingError<Set>>>,
target: MemoryBoundObject,
topology: &Topology,
set: &Set,
policy: MemoryBindingPolicy,
flags: MemoryBindingFlags,
try_get_final_binding: impl FnOnce(&Res) -> FinalBinding<Set>,
) -> Result<Option<Res>, TestCaseError> {
handle_windows_edge_cases!(&result, target, Ok(None));
if let Err(HybridError::Rust(MemoryBindingError::BadSet(_, set2))) = &result {
if set2 != set {
fail!("Set error features set {set2} instead of expected set {set}");
}
let set = set.as_bitmap_ref();
let topology_set = match Set::BITMAP_KIND {
BitmapKind::CpuSet => Bitmap::from(topology.cpuset().clone_target()),
BitmapKind::NodeSet => Bitmap::from(topology.nodeset().clone_target()),
};
if set.is_empty() || !topology_set.includes(set) || !cfg!(target_os = "linux") {
return Ok(None);
} else {
fail!("Got unexpected bad set error {result:?}");
}
}
let forbidden_flags = if target == MemoryBoundObject::ThisProgram {
MemoryBindingFlags::empty()
} else {
MemoryBindingFlags::MIGRATE
};
if let Ok(true) =
check_membind_flags(forbidden_flags, target, false, flags, result.as_ref().err())
{
return Ok(None);
}
if let Err(HybridError::Rust(MemoryBindingError::Unsupported)) = result.as_ref() {
let membind_support = topology.feature_support().memory_binding().unwrap();
let policy_supported = match policy {
MemoryBindingPolicy::Bind => membind_support.bind_policy(),
MemoryBindingPolicy::FirstTouch => membind_support.first_touch_policy(),
MemoryBindingPolicy::Interleave => membind_support.interleave_policy(),
#[cfg(feature = "hwloc-2_11_0")]
MemoryBindingPolicy::WeightedInterleave => membind_support.weighted_interleave_policy(),
MemoryBindingPolicy::NextTouch => membind_support.next_touch_policy(),
MemoryBindingPolicy::Unknown(_) => false,
};
if !policy_supported {
return Ok(None);
}
if !membind_support.migrate_flag() && flags.contains(MemoryBindingFlags::MIGRATE) {
return Ok(None);
}
}
let result = match result {
Ok(result) => result,
Err(e) => {
if cfg!(target_os = "linux") {
fail!("Got unexpected memory binding setup error: {e}")
} else {
return Ok(None);
}
}
};
if flags.contains(MemoryBindingFlags::STRICT) && Set::BITMAP_KIND == BitmapKind::NodeSet {
let final_binding = try_get_final_binding(&result);
if let Some(Ok(set_and_policy)) = &final_binding {
if set_and_policy != &(set.clone(), Some(policy)) {
fail!("Got unexpected final process memory binding {final_binding:?}");
}
}
}
Ok(Some(result))
}
type FinalBinding<Set> =
Option<Result<(Set, Option<MemoryBindingPolicy>), HybridError<MemoryBindingError<Set>>>>;
#[tracing::instrument(skip(topology))]
fn test_unbind_memory(topology: &Topology, flags: MemoryBindingFlags) -> Result<(), TestCaseError> {
let result = topology.unbind_memory(flags);
post_unbind_checks(
result,
MemoryBoundObject::ThisProgram,
topology,
flags,
self_memory_binding_getter(topology, flags),
)
}
#[tracing::instrument(skip(topology))]
fn test_unbind_process_memory(
topology: &Topology,
pid: ProcessId,
flags: MemoryBindingFlags,
) -> Result<(), TestCaseError> {
let result = topology.unbind_process_memory(pid, flags);
post_unbind_checks(
result,
MemoryBoundObject::Process(pid),
topology,
flags,
process_memory_binding_getter(topology, pid),
)
}
#[tracing::instrument(skip(topology, target))]
fn test_unbind_memory_area(
topology: &Topology,
target: &[u8],
flags: MemoryBindingFlags,
) -> Result<(), TestCaseError> {
let result = topology.unbind_memory_area(target, flags);
if target.is_empty() {
if result == Ok(()) {
return Ok(());
} else {
fail!("Unexpected result upon unbinding an empty memory area: {result:?}");
}
}
post_unbind_checks(
result,
MemoryBoundObject::Area,
topology,
flags,
area_memory_binding_getter(topology, target),
)
}
#[tracing::instrument(skip(topology, try_get_final_binding))]
fn post_unbind_checks(
result: Result<(), HybridError<MemoryBindingError<NodeSet>>>,
target: MemoryBoundObject,
topology: &Topology,
flags: MemoryBindingFlags,
try_get_final_binding: impl FnOnce(&()) -> FinalBinding<NodeSet>,
) -> Result<(), TestCaseError> {
handle_windows_edge_cases!(&result, target, Ok(()));
if let Ok(true) = check_membind_flags(
MemoryBindingFlags::MIGRATE | MemoryBindingFlags::STRICT,
target,
false,
flags,
result.as_ref().err(),
) {
return Ok(());
}
if let Err(e) = result {
if cfg!(target_os = "linux") {
fail!("Got unexpected memory binding setup error: {e}");
} else {
return Ok(());
}
}
let query_result = try_get_final_binding(&());
if let Some(Ok((nodeset, _policy))) = &query_result {
if nodeset != &*topology.allowed_nodeset() {
fail!("Got unexpected unbound process binding: {query_result:?}");
}
}
Ok(())
}
fn self_memory_binding_getter<Set: SpecializedBitmap>(
topology: &Topology,
flags: MemoryBindingFlags,
) -> impl FnOnce(&()) -> FinalBinding<Set> + '_ {
move |()| {
let membind_support = topology.feature_support().memory_binding()?;
let can_query_thisproc = membind_support.get_current_process();
let can_query_thisthread = membind_support.get_current_thread();
let can_query_self = match flags & target_membind_flags() {
MemoryBindingFlags::PROCESS => can_query_thisproc,
MemoryBindingFlags::THREAD => can_query_thisthread,
MemoryBindingFlags::ASSUME_SINGLE_THREAD => can_query_thisproc | can_query_thisthread,
_ => false, };
can_query_self.then(|| {
topology.memory_binding((flags & target_membind_flags()) | MemoryBindingFlags::STRICT)
})
}
}
fn process_memory_binding_getter<Set: SpecializedBitmap>(
topology: &Topology,
pid: ProcessId,
) -> impl FnOnce(&()) -> FinalBinding<Set> + '_ {
move |()| {
(topology.supports(
FeatureSupport::memory_binding,
MemoryBindingSupport::get_process,
))
.then(|| topology.process_memory_binding(pid, MemoryBindingFlags::STRICT))
}
}
fn area_memory_binding_getter<'out, Set: SpecializedBitmap, Res>(
topology: &'out Topology,
bytes: &'out [u8],
) -> impl FnOnce(&Res) -> FinalBinding<Set> + 'out {
move |_| {
(topology.supports(
FeatureSupport::memory_binding,
MemoryBindingSupport::get_area,
) && bytes.is_empty())
.then(|| topology.area_memory_binding::<_, Set>(bytes, MemoryBindingFlags::STRICT))
}
}
#[tracing::instrument(skip(topology))]
fn test_query_self_membind(
topology: &Topology,
flags: MemoryBindingFlags,
) -> Result<(), TestCaseError> {
check_membind_query_result::<NodeSet>(
MemoryBindingFlags::empty(),
topology,
topology.memory_binding(flags),
MemoryBoundObject::ThisProgram,
flags,
)?;
check_membind_query_result::<CpuSet>(
MemoryBindingFlags::empty(),
topology,
topology.memory_binding(flags),
MemoryBoundObject::ThisProgram,
flags,
)
}
#[tracing::instrument(skip(topology))]
fn test_query_process_membind(
topology: &Topology,
pid: ProcessId,
flags: MemoryBindingFlags,
) -> Result<(), TestCaseError> {
check_membind_query_result::<NodeSet>(
MemoryBindingFlags::empty(),
topology,
topology.process_memory_binding(pid, flags),
MemoryBoundObject::Process(pid),
flags,
)?;
check_membind_query_result::<CpuSet>(
MemoryBindingFlags::empty(),
topology,
topology.process_memory_binding(pid, flags),
MemoryBoundObject::Process(pid),
flags,
)
}
#[tracing::instrument(skip(topology, target))]
fn test_query_area_membind(
topology: &Topology,
target: &[u8],
flags: MemoryBindingFlags,
) -> Result<(), TestCaseError> {
check_area_membind_query_result::<NodeSet>(
MemoryBindingFlags::empty(),
topology,
topology.area_memory_binding(target, flags),
flags,
target.is_empty(),
)?;
check_area_membind_query_result::<CpuSet>(
MemoryBindingFlags::empty(),
topology,
topology.area_memory_binding(target, flags),
flags,
target.is_empty(),
)
}
#[tracing::instrument(skip(topology, target))]
fn test_query_area_memloc(
topology: &Topology,
target: &[u8],
flags: MemoryBindingFlags,
) -> Result<(), TestCaseError> {
check_area_memloc_query_result::<NodeSet>(
topology,
topology.area_memory_location(target, flags),
flags,
target.is_empty(),
)?;
check_area_memloc_query_result::<CpuSet>(
topology,
topology.area_memory_location(target, flags),
flags,
target.is_empty(),
)
}
#[tracing::instrument(skip(topology))]
fn check_area_memloc_query_result<Set: SpecializedBitmap>(
topology: &Topology,
result: Result<Set, HybridError<MemoryBindingError<Set>>>,
flags: MemoryBindingFlags,
area_is_empty: bool,
) -> Result<(), TestCaseError> {
check_area_membind_query_result(
MemoryBindingFlags::STRICT,
topology,
result.map(|set| (set, None)),
flags,
area_is_empty,
)
}
#[tracing::instrument(skip(topology))]
fn check_area_membind_query_result<Set: SpecializedBitmap>(
extra_forbidden_flags: MemoryBindingFlags,
topology: &Topology,
result: Result<(Set, Option<MemoryBindingPolicy>), HybridError<MemoryBindingError<Set>>>,
flags: MemoryBindingFlags,
area_is_empty: bool,
) -> Result<(), TestCaseError> {
if area_is_empty {
if result == Err(MemoryBindingError::BadArea.into()) {
return Ok(());
} else {
fail!("Got unexpected result {result:?} to invalid empty area binding query");
}
}
check_membind_query_result(
extra_forbidden_flags,
topology,
result,
MemoryBoundObject::Area,
flags,
)
}
#[tracing::instrument(skip(topology))]
fn check_membind_query_result<Set: SpecializedBitmap>(
extra_forbidden_flags: MemoryBindingFlags,
topology: &Topology,
result: Result<(Set, Option<MemoryBindingPolicy>), HybridError<MemoryBindingError<Set>>>,
target: MemoryBoundObject,
flags: MemoryBindingFlags,
) -> Result<(), TestCaseError> {
#[cfg(windows)]
if let Err(HybridError::Rust(MemoryBindingError::Unknown)) = &result {
return Ok(());
}
handle_windows_edge_cases!(&result, target, Ok(()));
if check_membind_flags(
MemoryBindingFlags::MIGRATE | MemoryBindingFlags::NO_CPU_BINDING | extra_forbidden_flags,
target,
true,
flags,
result.as_ref().err(),
)? {
return Ok(());
}
let (set, policy) = match result {
Ok(tuple) => tuple,
Err(e) => {
if cfg!(target_os = "linux") {
fail!("Unexpected memory binding query failure on Linux: {e}");
} else {
return Ok(());
}
}
};
let erased_set: Bitmap = set.into();
let erased_ref: Bitmap = match Set::BITMAP_KIND {
BitmapKind::CpuSet => topology.complete_cpuset().clone_target().into(),
BitmapKind::NodeSet => topology.complete_nodeset().clone_target().into(),
};
if erased_set.is_empty() || !erased_ref.includes(&erased_set) {
fail!("Memory binding {erased_set} is invalid wrt reference {erased_ref}");
}
if policy.is_none()
&& !(flags.contains(MemoryBindingFlags::PROCESS) || target == MemoryBoundObject::Area)
{
fail!("Did not expect a mixed policy in this configuration");
}
Ok(())
}
#[tracing::instrument]
fn check_membind_flags<Set: SpecializedBitmap>(
forbidden_flags: MemoryBindingFlags,
target: MemoryBoundObject,
is_binding_query: bool,
actual_flags: MemoryBindingFlags,
error: Option<&HybridError<MemoryBindingError<Set>>>,
) -> Result<bool, TestCaseError> {
let bad_flags_outcome = |failed_assumption: &str| {
let expected_error = HybridError::Rust(MemoryBindingError::from(actual_flags));
if error != Some(&expected_error) {
fail!("Expected {expected_error} error due to {failed_assumption}, but got {error:?}");
}
Ok(true)
};
if actual_flags.intersects(forbidden_flags) {
return bad_flags_outcome("use of forbidden flags");
}
let can_bind_thisprogram = target == MemoryBoundObject::ThisProgram;
if !can_bind_thisprogram && actual_flags.contains(MemoryBindingFlags::THREAD) {
return bad_flags_outcome(
"use of the THREAD flag when the target isn't the current process",
);
}
let is_process_binding_query = is_binding_query && target != MemoryBoundObject::Area;
if is_process_binding_query
&& actual_flags.contains(MemoryBindingFlags::STRICT)
&& !actual_flags.contains(MemoryBindingFlags::PROCESS)
{
return bad_flags_outcome(
"use of the STRICT flag without the PROCESS flag in a process binding query",
);
}
let num_target_flags = (actual_flags & target_membind_flags()).iter().count();
if num_target_flags != (can_bind_thisprogram || is_process_binding_query) as usize {
return bad_flags_outcome("use of an incorrect number of target flags for this query");
}
Ok(false)
}
fn target_membind_flags() -> MemoryBindingFlags {
MemoryBindingFlags::ASSUME_SINGLE_THREAD
| MemoryBindingFlags::THREAD
| MemoryBindingFlags::PROCESS
}
#[tracing::instrument(skip(topology))]
fn test_bind_cpu(
topology: &Topology,
set: impl Deref<Target = CpuSet> + Copy + Debug,
flags: CpuBindingFlags,
target: CpuBoundObject,
) -> Result<(), TestCaseError> {
let set2 = set;
let result = match target {
CpuBoundObject::ThisProgram => topology.bind_cpu(set, flags),
CpuBoundObject::ProcessOrThread(pid) => topology.bind_process_cpu(pid, set, flags),
CpuBoundObject::Thread(pid) => topology.bind_thread_cpu(pid, set, flags),
};
let set = set2.deref();
handle_windows_edge_cases!(&result, target, Ok(()));
if let Err(HybridError::Rust(CpuBindingError::BadCpuSet(set2))) = &result {
if set2 != set {
fail!("Set error features set {set2} instead of expected set {set}");
}
if set.is_empty() || !topology.cpuset().includes(set) || !cfg!(target_os = "linux") {
return Ok(());
} else {
fail!("Got unexpected bad set error {result:?}");
}
}
if check_cpubind_flags(
CpuBindingFlags::empty(),
target,
flags,
result.as_ref().err(),
)? {
return Ok(());
}
if let Err(e) = result {
fail!("Got unexpected CPU binding setup error: {e}");
}
if flags.contains(CpuBindingFlags::STRICT) {
let target_flags = flags & target_cpubind_flags();
let cpubind_support = topology.feature_support().cpu_binding().unwrap();
let actual_binding = match target {
CpuBoundObject::ThisProgram => (cpubind_support.get_current_process()
|| cpubind_support.get_current_thread())
.then(|| topology.cpu_binding(target_flags)),
CpuBoundObject::ProcessOrThread(pid) => cpubind_support
.get_process()
.then(|| topology.process_cpu_binding(pid, target_flags)),
CpuBoundObject::Thread(tid) => cpubind_support
.get_thread()
.then(|| topology.thread_cpu_binding(tid, target_flags)),
};
if let Some(actual_binding) = actual_binding {
if actual_binding != Ok(set.clone()) {
fail!("Unexpected final process/thread CPU binding {actual_binding:?}");
}
}
}
Ok(())
}
#[tracing::instrument(skip(topology))]
fn test_cpu_binding(
topology: &Topology,
flags: CpuBindingFlags,
target: CpuBoundObject,
) -> Result<(), TestCaseError> {
let result = match target {
CpuBoundObject::ThisProgram => topology.cpu_binding(flags),
CpuBoundObject::ProcessOrThread(pid) => topology.process_cpu_binding(pid, flags),
CpuBoundObject::Thread(tid) => topology.thread_cpu_binding(tid, flags),
};
let forbidden_flags =
if matches!(target, CpuBoundObject::Thread(_)) || flags.contains(CpuBindingFlags::THREAD) {
CpuBindingFlags::STRICT
} else {
CpuBindingFlags::empty()
};
check_cpubind_query_result(result, topology, flags, target, forbidden_flags)
}
#[tracing::instrument(skip(topology))]
fn test_last_cpu_location(
topology: &Topology,
flags: CpuBindingFlags,
target: CpuBoundObject,
) -> Result<(), TestCaseError> {
let result = match target {
CpuBoundObject::ThisProgram => topology.last_cpu_location(flags),
CpuBoundObject::ProcessOrThread(pid) => topology.last_process_cpu_location(pid, flags),
CpuBoundObject::Thread(_) => panic!("Not currently supported by hwloc"),
};
check_cpubind_query_result(result, topology, flags, target, CpuBindingFlags::STRICT)
}
#[tracing::instrument(skip(topology))]
fn check_cpubind_query_result(
result: Result<CpuSet, HybridError<CpuBindingError>>,
topology: &Topology,
flags: CpuBindingFlags,
target: CpuBoundObject,
forbidden_flags: CpuBindingFlags,
) -> Result<(), TestCaseError> {
handle_windows_edge_cases!(&result, target, Ok(()));
if check_cpubind_flags(forbidden_flags, target, flags, result.as_ref().err())? {
return Ok(());
}
if flags.contains(CpuBindingFlags::NO_MEMORY_BINDING) {
let expected_error = HybridError::Rust(CpuBindingError::from(flags));
if result.as_ref().err() != Some(&expected_error) {
fail!("Expected bad target flags error, got {result:?}");
}
return Ok(());
}
let cpuset = match result {
Ok(cpuset) => cpuset,
Err(e) => {
fail!("Got unexpected CPU binding query error: {e}");
}
};
let reference_set = topology.complete_cpuset();
if cpuset.is_empty() || !reference_set.includes(&cpuset) {
fail!("Final CpuSet {cpuset} does not make sense wrt reference {reference_set}");
}
Ok(())
}
#[tracing::instrument]
fn check_cpubind_flags(
forbidden_flags: CpuBindingFlags,
target: CpuBoundObject,
actual_flags: CpuBindingFlags,
error: Option<&HybridError<CpuBindingError>>,
) -> Result<bool, TestCaseError> {
let expected_err = Some(HybridError::Rust(CpuBindingError::from(actual_flags)));
let expected_err = expected_err.as_ref();
let mut bad_flags = actual_flags.intersects(forbidden_flags);
let target_flags = actual_flags & target_cpubind_flags();
let is_linux_special_case = target_flags.contains(CpuBindingFlags::THREAD)
&& matches!(target, CpuBoundObject::ProcessOrThread(_));
bad_flags |= is_linux_special_case && cfg!(not(target_os = "linux"));
bad_flags |= (actual_flags & target_flags).iter().count()
!= (target == CpuBoundObject::ThisProgram || is_linux_special_case) as usize;
if bad_flags {
if error != expected_err {
fail!("Expected bad target flags error, got {error:?}");
}
return Ok(true);
}
Ok(false)
}
fn target_cpubind_flags() -> CpuBindingFlags {
CpuBindingFlags::ASSUME_SINGLE_THREAD | CpuBindingFlags::THREAD | CpuBindingFlags::PROCESS
}
fn topology_related_set<Set: SpecializedBitmap>(
topology_set: impl FnOnce(&Topology) -> BitmapRef<'_, Set>,
) -> impl Strategy<Value = Set> {
set_with_reference(topology_set(Topology::test_instance()))
}
fn set_with_reference<SetRef: SpecializedBitmapRef>(
reference: SetRef,
) -> impl Strategy<Value = SetRef::Owned> {
let reference = reference.as_bitmap_ref();
let finite_set = if reference.weight().is_some() {
reference.clone()
} else {
!reference
};
assert!(
finite_set.weight().is_some(),
"since bitmaps can only be infinite in one direction, \
the complement of an infinite bitmap must be finite"
);
let finite_elems = finite_set.iter_set().collect::<Vec<_>>();
let num_finite_elems = finite_elems.len();
let inside_elems = prop_oneof![
3 => prop::sample::subsequence(finite_elems.clone(), 0..=num_finite_elems),
1 => Just(Vec::new()),
1 => Just(finite_elems)
]
.prop_map(|seq| seq.into_iter().collect::<Bitmap>());
let outside_elems = prop_oneof![
3 => Just(Bitmap::new()),
2 => any_bitmap().prop_map(move |any_elems| any_elems - &finite_set),
];
(inside_elems, outside_elems)
.prop_map(|(inside_elems, outside_elems)| SetRef::Owned::from(inside_elems | outside_elems))
}
fn any_bitmap() -> impl Strategy<Value = Bitmap> {
let finite_bitmap = prop_oneof![
1 => Just(Bitmap::new()),
4 => any::<HashSet<c_uint>>().prop_map(|overflowing_indices| {
overflowing_indices
.into_iter()
.map(|idx| {
BitmapIndex::try_from(idx as usize % 256).unwrap()
})
.collect::<Bitmap>()
}),
];
(finite_bitmap, prop::bool::ANY).prop_map(|(mut bitmap, invert)| {
if invert {
bitmap.invert();
}
bitmap
})
}
fn any_cpubind_flags() -> impl Strategy<Value = CpuBindingFlags> {
let all_flags = CpuBindingFlags::all().iter().collect::<Vec<_>>();
let num_flags = all_flags.len();
prop::sample::subsequence(all_flags, 0..=num_flags)
.prop_map(|some_flags| some_flags.into_iter().collect())
}
fn any_membind_policy() -> impl Strategy<Value = MemoryBindingPolicy> {
let policies = MemoryBindingPolicy::iter().collect::<Vec<_>>();
prop::sample::select(policies)
}
fn any_membind_flags() -> impl Strategy<Value = MemoryBindingFlags> {
let all_flags = MemoryBindingFlags::all()
.iter()
.filter(|flag| *flag != MemoryBindingFlags::BY_NODE_SET)
.collect::<Vec<_>>();
let num_flags = all_flags.len();
prop::sample::subsequence(all_flags, 0..=num_flags)
.prop_map(|some_flags| some_flags.into_iter().collect())
}