#![cfg(test)]
use super::*;
use crate::assert;
#[test]
fn resolve_affinity_inherit() {
let t = crate::topology::TestTopology::synthetic(8, 2);
assert!(matches!(
resolve_affinity_for_cgroup(&AffinityIntent::Inherit, None, &t).unwrap(),
ResolvedAffinity::None
));
}
#[test]
fn resolve_affinity_single_cpu() {
let t = crate::topology::TestTopology::synthetic(8, 2);
match resolve_affinity_for_cgroup(&AffinityIntent::SingleCpu, None, &t).unwrap() {
ResolvedAffinity::SingleCpu(c) => assert_eq!(c, 0),
other => panic!("expected SingleCpu, got {:?}", other),
}
}
#[test]
fn resolve_affinity_cross_cgroup() {
let t = crate::topology::TestTopology::synthetic(8, 2);
match resolve_affinity_for_cgroup(&AffinityIntent::CrossCgroup, None, &t).unwrap() {
ResolvedAffinity::Fixed(cpus) => assert_eq!(cpus.len(), 8),
other => panic!("expected Fixed, got {:?}", other),
}
}
#[test]
fn resolve_affinity_llc_aligned() {
let t = crate::topology::TestTopology::synthetic(8, 2);
match resolve_affinity_for_cgroup(&AffinityIntent::LlcAligned, None, &t).unwrap() {
ResolvedAffinity::Fixed(cpus) => assert_eq!(cpus, [0, 1, 2, 3].into_iter().collect()),
other => panic!("expected Fixed, got {:?}", other),
}
}
#[test]
fn resolve_affinity_llc_aligned_with_cpuset() {
let t = crate::topology::TestTopology::synthetic(8, 2);
let cpusets: Vec<BTreeSet<usize>> = vec![
[0, 1, 2, 3].into_iter().collect(),
[4, 5, 6, 7].into_iter().collect(),
];
match resolve_affinity_for_cgroup(&AffinityIntent::LlcAligned, cpusets.get(1), &t).unwrap() {
ResolvedAffinity::Fixed(cpus) => assert_eq!(cpus, [4, 5, 6, 7].into_iter().collect()),
other => panic!("expected Fixed, got {:?}", other),
}
}
#[test]
fn resolve_affinity_random_subset() {
let t = crate::topology::TestTopology::synthetic(8, 2);
let cpusets: Vec<BTreeSet<usize>> = vec![[0, 1, 2, 3].into_iter().collect()];
let intent = AffinityIntent::random_subset(t.all_cpus().iter().copied(), 2);
match resolve_affinity_for_cgroup(&intent, cpusets.first(), &t).unwrap() {
ResolvedAffinity::Random { from, count } => {
assert_eq!(from, cpusets[0]);
assert_eq!(count, 2); }
other => panic!("expected Random, got {:?}", other),
}
}
#[test]
fn cgroup_work_default() {
let cw = WorkSpec::default();
assert_eq!(cw.num_workers, None);
assert!(matches!(cw.work_type, WorkType::SpinWait));
assert!(matches!(cw.sched_policy, SchedPolicy::Normal));
assert!(matches!(cw.affinity, AffinityIntent::Inherit));
assert!(matches!(cw.mem_policy, MemPolicy::Default));
}
#[test]
fn resolve_affinity_random_no_cpusets() {
let t = crate::topology::TestTopology::synthetic(8, 2);
let intent = AffinityIntent::random_subset(t.all_cpus().iter().copied(), 4);
match resolve_affinity_for_cgroup(&intent, None, &t).unwrap() {
ResolvedAffinity::Random { from, count } => {
assert_eq!(from.len(), 8); assert_eq!(count, 4); }
other => panic!("expected Random, got {:?}", other),
}
}
#[test]
fn resolve_affinity_random_subset_empty_pool_bails() {
let t = crate::topology::TestTopology::synthetic(4, 1);
let empty: BTreeSet<usize> = BTreeSet::new();
let intent = AffinityIntent::random_subset(t.all_cpus().iter().copied(), 1);
let err = resolve_affinity_for_cgroup(&intent, Some(&empty), &t)
.expect_err("empty cpuset intersection must bail");
let msg = format!("{err:#}");
assert!(
msg.contains("AffinityIntent::RandomSubset"),
"diagnostic must name the intent variant: got {msg}",
);
assert!(
msg.contains("empty cpuset"),
"diagnostic must name what narrowed the pool (empty cpuset): got {msg}",
);
assert!(
msg.contains("AffinityIntent::Inherit"),
"diagnostic must name the recommended fix (Inherit fallback): got {msg}",
);
}
#[test]
fn resolve_affinity_random_subset_empty_from_no_cpuset_bails() {
let t = crate::topology::TestTopology::synthetic(4, 1);
let intent = AffinityIntent::random_subset(std::iter::empty(), 1);
let err = resolve_affinity_for_cgroup(&intent, None, &t)
.expect_err("empty from-pool with no cpuset must bail");
let msg = format!("{err:#}");
assert!(
msg.contains("AffinityIntent::RandomSubset"),
"diagnostic must name the intent variant: got {msg}",
);
assert!(
msg.contains("empty `from` pool with no cgroup cpuset"),
"diagnostic must distinguish this case from the cpuset-intersection \
case so the operator sees the right remediation: got {msg}",
);
assert!(
msg.contains("AffinityIntent::Inherit"),
"diagnostic must name the recommended fix (Inherit fallback): got {msg}",
);
}
#[test]
fn resolve_affinity_random_subset_zero_count_bails() {
let t = crate::topology::TestTopology::synthetic(4, 1);
let intent = AffinityIntent::random_subset(t.all_cpus().iter().copied(), 0);
let err = resolve_affinity_for_cgroup(&intent, None, &t).expect_err("count=0 must bail");
let msg = format!("{err:#}");
assert!(
msg.contains("RandomSubset count=0"),
"diagnostic must name count=0: got {msg}",
);
}
#[test]
fn resolve_affinity_llc_aligned_disjoint_cpuset_bails() {
let t = crate::topology::TestTopology::synthetic(8, 2);
let empty: BTreeSet<usize> = BTreeSet::new();
let err = resolve_affinity_for_cgroup(&AffinityIntent::LlcAligned, Some(&empty), &t)
.expect_err("empty cpuset on LlcAligned must bail");
let msg = format!("{err:#}");
assert!(
msg.contains("AffinityIntent::LlcAligned"),
"diagnostic must name the intent variant: got {msg}",
);
assert!(
msg.contains("No LLC has any CPU"),
"diagnostic must name the failure mode: got {msg}",
);
assert!(
msg.contains("AffinityIntent::Inherit"),
"diagnostic must name the recommended fix: got {msg}",
);
}
#[test]
fn resolve_affinity_single_cpu_empty_cpuset_bails() {
let t = crate::topology::TestTopology::synthetic(4, 1);
let empty: BTreeSet<usize> = BTreeSet::new();
let err = resolve_affinity_for_cgroup(&AffinityIntent::SingleCpu, Some(&empty), &t)
.expect_err("empty cpuset on SingleCpu must bail");
let msg = format!("{err:#}");
assert!(
msg.contains("AffinityIntent::SingleCpu"),
"diagnostic must name the intent variant: got {msg}",
);
assert!(
msg.contains("empty cgroup cpuset"),
"diagnostic must name the failure mode (empty cpuset): got {msg}",
);
assert!(
msg.contains("AffinityIntent::Inherit"),
"diagnostic must name the recommended fix: got {msg}",
);
}
#[test]
fn resolve_affinity_exact_disjoint_from_cpuset_bails() {
let t = crate::topology::TestTopology::synthetic(4, 1);
let cpuset: BTreeSet<usize> = [0, 1].into_iter().collect();
let exact: BTreeSet<usize> = [2, 3].into_iter().collect();
let err = resolve_affinity_for_cgroup(&AffinityIntent::Exact(exact), Some(&cpuset), &t)
.expect_err("disjoint Exact must bail");
let msg = format!("{err:#}");
assert!(
msg.contains("AffinityIntent::Exact"),
"diagnostic must name the intent variant: got {msg}",
);
assert!(
msg.contains("disjoint"),
"diagnostic must name the failure mode (disjoint): got {msg}",
);
assert!(
msg.contains("AffinityIntent::Inherit"),
"diagnostic must name the recommended fix: got {msg}",
);
}
#[test]
fn resolve_affinity_exact_empty_bails_regardless_of_cpuset() {
let t = crate::topology::TestTopology::synthetic(4, 1);
let empty: BTreeSet<usize> = BTreeSet::new();
let err_no_cs = resolve_affinity_for_cgroup(&AffinityIntent::Exact(empty.clone()), None, &t)
.expect_err("empty Exact must bail even without cpuset");
let msg_no_cs = format!("{err_no_cs:#}");
assert!(
msg_no_cs.contains("AffinityIntent::Exact(BTreeSet::new())"),
"diagnostic must name the empty-Exact form: got {msg_no_cs}",
);
assert!(
msg_no_cs.contains("unsatisfiable"),
"diagnostic must name the failure mode: got {msg_no_cs}",
);
assert!(
msg_no_cs.contains("AffinityIntent::Inherit"),
"diagnostic must name the recommended fix: got {msg_no_cs}",
);
let cpuset: BTreeSet<usize> = [0, 1].into_iter().collect();
let err_with_cs = resolve_affinity_for_cgroup(&AffinityIntent::Exact(empty), Some(&cpuset), &t)
.expect_err("empty Exact must bail with cpuset too");
let msg_with_cs = format!("{err_with_cs:#}");
assert!(
msg_with_cs.contains("AffinityIntent::Exact(BTreeSet::new())"),
"diagnostic must name the empty-Exact form (not the disjoint message): got {msg_with_cs}",
);
}
#[test]
fn resolve_affinity_oob_cgroup_idx_falls_back_to_unrestricted() {
let t = crate::topology::TestTopology::synthetic(4, 1);
let cpusets: Vec<BTreeSet<usize>> = vec![[0, 1].into_iter().collect()];
let oob_idx = 5;
let cpuset = cpusets.get(oob_idx);
assert!(cpuset.is_none(), "OOB index must yield None cpuset");
let intent = AffinityIntent::random_subset(t.all_cpus().iter().copied(), 2);
match resolve_affinity_for_cgroup(&intent, cpuset, &t).unwrap() {
ResolvedAffinity::Random { from, count } => {
assert_eq!(from.len(), 4, "OOB idx falls back to full topology");
assert_eq!(count, 2);
}
other => panic!("expected Random with full pool, got {:?}", other),
}
}
#[test]
fn resolve_affinity_smt_sibling_pair_uses_first_core() {
let vmt = crate::vmm::topology::Topology::new(1, 1, 2, 2);
let t = crate::topology::TestTopology::from_vm_topology(&vmt);
match resolve_affinity_for_cgroup(&AffinityIntent::SmtSiblingPair, None, &t).unwrap() {
ResolvedAffinity::Fixed(cpus) => {
assert_eq!(
cpus,
[0usize, 1].into_iter().collect(),
"SmtSiblingPair must pick the first core's siblings"
);
}
other => panic!("expected Fixed({{0, 1}}), got {:?}", other),
}
}
#[test]
fn resolve_affinity_smt_sibling_pair_skips_partial_cores() {
let vmt = crate::vmm::topology::Topology::new(1, 1, 2, 2);
let t = crate::topology::TestTopology::from_vm_topology(&vmt);
let cpuset: BTreeSet<usize> = [2usize, 3].into_iter().collect();
match resolve_affinity_for_cgroup(&AffinityIntent::SmtSiblingPair, Some(&cpuset), &t).unwrap() {
ResolvedAffinity::Fixed(cpus) => {
assert_eq!(
cpus,
[2usize, 3].into_iter().collect(),
"SmtSiblingPair must skip core 0 when cpuset excludes one of its \
siblings and pick the next eligible pair"
);
}
other => panic!("expected Fixed({{2, 3}}), got {:?}", other),
}
}
#[test]
fn resolve_affinity_smt_sibling_pair_errors_without_smt() {
let vmt = crate::vmm::topology::Topology::new(1, 1, 4, 1);
let t = crate::topology::TestTopology::from_vm_topology(&vmt);
let err = resolve_affinity_for_cgroup(&AffinityIntent::SmtSiblingPair, None, &t)
.expect_err("threads_per_core=1 must produce an error, not silent fallback");
let msg = err.to_string();
assert!(
msg.contains("SmtSiblingPair"),
"diagnostic must name the variant, got: {msg}"
);
assert!(
msg.contains("two SMT siblings"),
"diagnostic must explain the missing precondition, got: {msg}"
);
assert!(
msg.contains("full topology"),
"diagnostic with no cpuset must name the topology as the \
search scope (not '<no cpuset>'), got: {msg}",
);
}
#[test]
fn resolve_affinity_smt_sibling_pair_errors_when_cpuset_breaks_pairs() {
let vmt = crate::vmm::topology::Topology::new(1, 1, 2, 2);
let t = crate::topology::TestTopology::from_vm_topology(&vmt);
let cpuset: BTreeSet<usize> = [0usize, 2].into_iter().collect();
let err = resolve_affinity_for_cgroup(&AffinityIntent::SmtSiblingPair, Some(&cpuset), &t)
.expect_err("cpuset that breaks every sibling pair must error");
let msg = err.to_string();
assert!(
msg.contains("SmtSiblingPair"),
"diagnostic must name the variant, got: {msg}"
);
assert!(
msg.contains("effective cpuset (cpuset {0, 2})"),
"diagnostic with cpuset=Some must echo the cpuset value \
verbatim via format_cpuset_for_diag, got: {msg}",
);
}
#[test]
fn resolve_affinity_smt_sibling_pair_errors_with_empty_cpuset() {
let vmt = crate::vmm::topology::Topology::new(1, 1, 2, 2);
let t = crate::topology::TestTopology::from_vm_topology(&vmt);
let empty_cpuset: BTreeSet<usize> = BTreeSet::new();
let err = resolve_affinity_for_cgroup(&AffinityIntent::SmtSiblingPair, Some(&empty_cpuset), &t)
.expect_err("SmtSiblingPair with empty cpuset must bail");
let msg = err.to_string();
assert!(
msg.contains("SmtSiblingPair"),
"diagnostic must name the variant, got: {msg}"
);
assert!(
msg.contains("effective cpuset (empty cpuset") && !msg.contains("full topology"),
"diagnostic with cpuset=Some(empty) must take the cpuset \
branch (not the topology branch), got: {msg}",
);
}
#[test]
fn resolve_affinity_smt_sibling_pair_errors_on_synthetic_topology() {
let t = crate::topology::TestTopology::synthetic(8, 2);
let err = resolve_affinity_for_cgroup(&AffinityIntent::SmtSiblingPair, None, &t)
.expect_err("synthetic topology has no per-core sibling data — must error");
let msg = err.to_string();
assert!(
msg.contains("SmtSiblingPair"),
"diagnostic must name the variant, got: {msg}"
);
assert!(
msg.contains("full topology"),
"diagnostic with no cpuset must name the topology as the \
search scope, got: {msg}",
);
}
#[test]
fn split_half_even() {
let t = crate::topology::TestTopology::synthetic(8, 2);
let ctx_cg = crate::cgroup::CgroupManager::new("/nonexistent");
let ctx = Ctx {
cgroups: &ctx_cg,
topo: &t,
duration: std::time::Duration::from_secs(1),
workers_per_cgroup: 4,
sched_pid: None,
settle: Duration::from_millis(3000),
work_type_override: None,
assert: assert::Assert::default_checks(),
wait_for_map_write: false,
current_step: std::sync::Arc::new(std::sync::atomic::AtomicU16::new(0)),
};
let (a, b) = split_half(&ctx);
assert_eq!(a.len() + b.len(), 7);
assert!(a.intersection(&b).count() == 0, "halves should not overlap");
}
#[test]
fn split_half_small() {
let t = crate::topology::TestTopology::synthetic(2, 1);
let ctx_cg = crate::cgroup::CgroupManager::new("/nonexistent");
let ctx = Ctx {
cgroups: &ctx_cg,
topo: &t,
duration: std::time::Duration::from_secs(1),
workers_per_cgroup: 1,
sched_pid: None,
settle: Duration::from_millis(3000),
work_type_override: None,
assert: assert::Assert::default_checks(),
wait_for_map_write: false,
current_step: std::sync::Arc::new(std::sync::atomic::AtomicU16::new(0)),
};
let (a, b) = split_half(&ctx);
assert_eq!(a.len() + b.len(), 2);
}
#[test]
fn dfl_wl_propagates_workers() {
let t = crate::topology::TestTopology::synthetic(8, 2);
let ctx_cg = crate::cgroup::CgroupManager::new("/nonexistent");
let ctx = Ctx {
cgroups: &ctx_cg,
topo: &t,
duration: std::time::Duration::from_secs(1),
workers_per_cgroup: 7,
sched_pid: None,
settle: Duration::from_millis(3000),
work_type_override: None,
assert: assert::Assert::default_checks(),
wait_for_map_write: false,
current_step: std::sync::Arc::new(std::sync::atomic::AtomicU16::new(0)),
};
let wl = dfl_wl(&ctx);
assert_eq!(wl.num_workers, 7);
assert!(matches!(wl.work_type, WorkType::SpinWait));
}
#[test]
fn process_alive_self_is_true() {
let pid: libc::pid_t = unsafe { libc::getpid() };
assert!(process_alive(pid));
}
#[test]
fn ctx_active_sched_pid_treats_nonpositive_as_unconfigured() {
let cg = crate::cgroup::CgroupManager::new("/nonexistent");
let topo = crate::topology::TestTopology::synthetic(1, 1);
let ctx_zero = Ctx::builder(&cg, &topo).sched_pid(Some(0)).build();
assert_eq!(
ctx_zero.sched_pid,
Some(0),
"builder must preserve the literal value — the gate lives in the accessor",
);
assert_eq!(
ctx_zero.active_sched_pid(),
None,
"Some(0) must be treated as unconfigured, otherwise the liveness \
bails fire on tests that never ran a scheduler",
);
let ctx_neg = Ctx::builder(&cg, &topo).sched_pid(Some(-1)).build();
assert_eq!(
ctx_neg.active_sched_pid(),
None,
"negative pid must be treated as unconfigured",
);
let ctx_min = Ctx::builder(&cg, &topo)
.sched_pid(Some(libc::pid_t::MIN))
.build();
assert_eq!(
ctx_min.active_sched_pid(),
None,
"pid_t::MIN must be treated as unconfigured — the filter \
is `p > 0`, and the most-negative pid_t stays unconfigured \
under that predicate by construction",
);
let ctx_pos = Ctx::builder(&cg, &topo).sched_pid(Some(1234)).build();
assert_eq!(
ctx_pos.active_sched_pid(),
Some(1234),
"positive pid must pass through unchanged",
);
let ctx_max = Ctx::builder(&cg, &topo)
.sched_pid(Some(libc::pid_t::MAX))
.build();
assert_eq!(
ctx_max.active_sched_pid(),
Some(libc::pid_t::MAX),
"pid_t::MAX must pass the filter — `p > 0` accepts it. \
Liveness determination is the responsibility of the \
downstream `process_alive` call, not this accessor.",
);
let ctx_none = Ctx::builder(&cg, &topo).sched_pid(None).build();
assert_eq!(
ctx_none.active_sched_pid(),
None,
"None must pass through unchanged",
);
}
#[test]
fn process_alive_zero_is_false() {
assert!(!process_alive(0));
}
#[test]
fn process_alive_negative_is_false() {
assert!(!process_alive(-1));
assert!(!process_alive(libc::pid_t::MIN));
}
#[test]
fn process_alive_nonexistent_pid() {
assert!(!process_alive(libc::pid_t::MAX));
}
#[test]
fn cgroup_group_new_empty() {
let cg = crate::cgroup::CgroupManager::new("/nonexistent");
let group = CgroupGroup::new(&cg);
assert!(group.names().is_empty());
}
#[test]
fn resolve_affinity_single_cpu_with_cpuset() {
let t = crate::topology::TestTopology::synthetic(4, 1);
let cpusets: Vec<BTreeSet<usize>> = vec![[2, 3].into_iter().collect()];
match resolve_affinity_for_cgroup(&AffinityIntent::SingleCpu, cpusets.first(), &t).unwrap() {
ResolvedAffinity::SingleCpu(c) => assert_eq!(c, 2),
other => panic!("expected SingleCpu, got {:?}", other),
}
}
#[test]
fn resolve_affinity_llc_aligned_picks_best_overlap() {
let t = crate::topology::TestTopology::synthetic(8, 2);
let cpusets: Vec<BTreeSet<usize>> = vec![[3, 4, 5, 6, 7].into_iter().collect()];
match resolve_affinity_for_cgroup(&AffinityIntent::LlcAligned, cpusets.first(), &t).unwrap() {
ResolvedAffinity::Fixed(cpus) => {
assert_eq!(cpus, [4, 5, 6, 7].into_iter().collect());
}
other => panic!("expected Fixed, got {:?}", other),
}
}
#[test]
fn resolve_num_workers_zero_rejected_with_label() {
let w = WorkSpec {
num_workers: Some(0),
..Default::default()
};
let err = resolve_num_workers(&w, 4, "victim").unwrap_err();
let msg = format!("{err}");
assert!(
msg.contains("cgroup 'victim'"),
"label must appear in error: {msg}"
);
assert!(
msg.contains("num_workers=0"),
"error must name the offending field: {msg}"
);
}
#[test]
fn resolve_num_workers_zero_default_also_rejected() {
let w = WorkSpec {
num_workers: None,
..Default::default()
};
assert!(resolve_num_workers(&w, 0, "cg").is_err());
}
#[test]
fn resolve_num_workers_falls_back_to_default() {
let w = WorkSpec {
num_workers: None,
..Default::default()
};
assert_eq!(resolve_num_workers(&w, 3, "cg").unwrap(), 3);
}
#[test]
fn resolve_num_workers_explicit_wins_over_default() {
let w = WorkSpec {
num_workers: Some(7),
..Default::default()
};
assert_eq!(resolve_num_workers(&w, 3, "cg").unwrap(), 7);
}
struct DropErrCgroupOps {
parent: std::path::PathBuf,
remove_kind: std::io::ErrorKind,
raw_os_error: Option<i32>,
remove_calls: std::sync::Mutex<Vec<String>>,
}
impl DropErrCgroupOps {
fn new(kind: std::io::ErrorKind, raw: Option<i32>) -> Self {
Self {
parent: std::path::PathBuf::from("/mock/cgroup"),
remove_kind: kind,
raw_os_error: raw,
remove_calls: std::sync::Mutex::new(Vec::new()),
}
}
fn calls(&self) -> Vec<String> {
self.remove_calls.lock().unwrap().clone()
}
}
impl crate::cgroup::CgroupOps for DropErrCgroupOps {
fn parent_path(&self) -> &std::path::Path {
&self.parent
}
fn setup(&self, _: &std::collections::BTreeSet<crate::cgroup::Controller>) -> Result<()> {
Ok(())
}
fn create_cgroup(&self, _: &str) -> Result<()> {
Ok(())
}
fn remove_cgroup(&self, name: &str) -> Result<()> {
self.remove_calls.lock().unwrap().push(name.to_string());
let io = match self.raw_os_error {
Some(errno) => std::io::Error::from_raw_os_error(errno),
None => std::io::Error::from(self.remove_kind),
};
Err(anyhow::Error::new(io).context("remove_dir cgroup"))
}
fn set_cpuset(&self, _: &str, _: &BTreeSet<usize>) -> Result<()> {
Ok(())
}
fn clear_cpuset(&self, _: &str) -> Result<()> {
Ok(())
}
fn set_cpuset_mems(&self, _: &str, _: &BTreeSet<usize>) -> Result<()> {
Ok(())
}
fn clear_cpuset_mems(&self, _: &str) -> Result<()> {
Ok(())
}
fn set_cpu_max(&self, _: &str, _: Option<u64>, _: u64) -> Result<()> {
Ok(())
}
fn set_cpu_weight(&self, _: &str, _: u32) -> Result<()> {
Ok(())
}
fn set_memory_max(&self, _: &str, _: Option<u64>) -> Result<()> {
Ok(())
}
fn set_memory_high(&self, _: &str, _: Option<u64>) -> Result<()> {
Ok(())
}
fn set_memory_low(&self, _: &str, _: Option<u64>) -> Result<()> {
Ok(())
}
fn set_io_weight(&self, _: &str, _: u16) -> Result<()> {
Ok(())
}
fn set_freeze(&self, _: &str, _: bool) -> Result<()> {
Ok(())
}
fn set_pids_max(&self, _: &str, _: Option<u64>) -> Result<()> {
Ok(())
}
fn set_memory_swap_max(&self, _: &str, _: Option<u64>) -> Result<()> {
Ok(())
}
fn move_task(&self, _: &str, _: libc::pid_t) -> Result<()> {
Ok(())
}
fn move_tasks(&self, _: &str, _: &[libc::pid_t]) -> Result<()> {
Ok(())
}
fn clear_subtree_control(&self, _: &str) -> Result<()> {
Ok(())
}
fn drain_tasks(&self, _: &str) -> Result<()> {
Ok(())
}
fn cleanup_all(&self) -> Result<()> {
Ok(())
}
}
#[test]
fn cgroup_group_drop_is_panic_free_on_every_error_kind() {
for (label, kind, raw) in [
("ENOENT", std::io::ErrorKind::NotFound, Some(libc::ENOENT)),
("EBUSY", std::io::ErrorKind::Other, Some(libc::EBUSY)),
(
"EACCES",
std::io::ErrorKind::PermissionDenied,
Some(libc::EACCES),
),
("generic-IO", std::io::ErrorKind::Other, None),
] {
let mock = DropErrCgroupOps::new(kind, raw);
{
let mut group = CgroupGroup::new(&mock);
group.names.push("child-a".to_string());
group.names.push("child-b".to_string());
}
let calls = mock.calls();
assert_eq!(
calls,
vec!["child-b".to_string(), "child-a".to_string()],
"[{label}] Drop must call remove_cgroup for every tracked name in reverse order",
);
}
}
#[test]
fn is_io_not_found_matches_only_notfound() {
let wrap = |k: std::io::ErrorKind| -> anyhow::Error {
anyhow::Error::new(std::io::Error::from(k)).context("wrap")
};
assert!(is_io_not_found(&wrap(std::io::ErrorKind::NotFound)));
assert!(!is_io_not_found(&wrap(
std::io::ErrorKind::PermissionDenied
)));
assert!(!is_io_not_found(&wrap(std::io::ErrorKind::Other)));
let no_io = anyhow::anyhow!("cgroup not found in parent");
assert!(!is_io_not_found(&no_io));
}
#[test]
fn remove_cgroup_errno_hint_covers_ebusy_and_eacces() {
let busy = anyhow::Error::new(std::io::Error::from_raw_os_error(libc::EBUSY)).context("wrap");
let acces = anyhow::Error::new(std::io::Error::from_raw_os_error(libc::EACCES)).context("wrap");
let enotempty =
anyhow::Error::new(std::io::Error::from_raw_os_error(libc::ENOTEMPTY)).context("wrap");
let non_io = anyhow::anyhow!("not an io error");
assert!(
remove_cgroup_errno_hint(&busy).is_some_and(|h| h.contains("EBUSY") && h.contains("drain")),
"EBUSY hint must name the errno and the drain remediation",
);
assert!(
remove_cgroup_errno_hint(&acces)
.is_some_and(|h| h.contains("EACCES") && h.contains("permission")),
"EACCES hint must name the errno and the permission angle",
);
assert_eq!(
remove_cgroup_errno_hint(&enotempty),
None,
"unclassified errnos must yield no hint so warn stays terse",
);
assert_eq!(
remove_cgroup_errno_hint(&non_io),
None,
"non-io root causes must yield no hint",
);
}
#[test]
fn flatten_for_spawn_none_to_inherit() {
let out = flatten_for_spawn(ResolvedAffinity::None);
assert!(
matches!(out, AffinityIntent::Inherit),
"ResolvedAffinity::None must flatten to Inherit, got {out:?}"
);
}
#[test]
fn flatten_for_spawn_fixed_to_exact() {
let set: BTreeSet<usize> = [1usize, 3, 5].into_iter().collect();
let out = flatten_for_spawn(ResolvedAffinity::Fixed(set.clone()));
match out {
AffinityIntent::Exact(got) => {
assert_eq!(got, set, "Fixed payload must round-trip into Exact");
}
other => panic!("expected Exact, got {other:?}"),
}
}
#[test]
#[should_panic(expected = "ResolvedAffinity::Fixed(empty) reached flatten_for_spawn")]
fn flatten_for_spawn_fixed_empty_panics() {
let _ = flatten_for_spawn(ResolvedAffinity::Fixed(BTreeSet::new()));
}
#[test]
fn flatten_for_spawn_single_cpu_to_exact_singleton() {
let out = flatten_for_spawn(ResolvedAffinity::SingleCpu(7));
match out {
AffinityIntent::Exact(got) => {
let expected: BTreeSet<usize> = [7usize].into_iter().collect();
assert_eq!(got, expected, "SingleCpu must flatten to a 1-CPU Exact set");
}
other => panic!("expected Exact({{7}}), got {other:?}"),
}
}
#[test]
fn flatten_for_spawn_random_to_random_subset() {
let from: BTreeSet<usize> = [0usize, 1, 2, 3].into_iter().collect();
let out = flatten_for_spawn(ResolvedAffinity::Random {
from: from.clone(),
count: 2,
});
match out {
AffinityIntent::RandomSubset {
from: got_from,
count: got_count,
} => {
assert_eq!(got_from, from, "Random.from must round-trip verbatim");
assert_eq!(got_count, 2, "Random.count must round-trip verbatim");
}
other => panic!("expected RandomSubset, got {other:?}"),
}
}
#[test]
#[should_panic(expected = "reached flatten_for_spawn with count==0 or empty pool")]
fn flatten_for_spawn_random_empty_pool_panics() {
let _ = flatten_for_spawn(ResolvedAffinity::Random {
from: BTreeSet::new(),
count: 4,
});
}
#[test]
#[should_panic(expected = "reached flatten_for_spawn with count==0 or empty pool")]
fn flatten_for_spawn_random_zero_count_panics() {
let from: BTreeSet<usize> = [0usize, 1, 2, 3].into_iter().collect();
let _ = flatten_for_spawn(ResolvedAffinity::Random { from, count: 0 });
}
#[test]
fn intent_for_spawn_full_pipeline() {
let vmt = crate::vmm::topology::Topology::new(1, 2, 2, 2);
let t = crate::topology::TestTopology::from_vm_topology(&vmt);
let out = intent_for_spawn(&AffinityIntent::Inherit, None, &t).unwrap();
assert!(
matches!(out, AffinityIntent::Inherit),
"Inherit must round-trip, got {out:?}"
);
let out = intent_for_spawn(&AffinityIntent::SingleCpu, None, &t).unwrap();
match out {
AffinityIntent::Exact(set) => {
assert_eq!(set.len(), 1, "SingleCpu flattens to a 1-CPU Exact set");
}
other => panic!("expected Exact, got {other:?}"),
}
let out = intent_for_spawn(&AffinityIntent::CrossCgroup, None, &t).unwrap();
match out {
AffinityIntent::Exact(set) => {
assert_eq!(set.len(), 8, "CrossCgroup flattens to all-CPU Exact set");
}
other => panic!("expected Exact, got {other:?}"),
}
let out = intent_for_spawn(&AffinityIntent::SmtSiblingPair, None, &t).unwrap();
match out {
AffinityIntent::Exact(set) => {
assert_eq!(set.len(), 2, "SmtSiblingPair flattens to a 2-CPU Exact set");
assert_eq!(
set,
[0usize, 1].into_iter().collect(),
"SmtSiblingPair must pick the first core's siblings"
);
}
other => panic!("expected Exact, got {other:?}"),
}
let out = intent_for_spawn(&AffinityIntent::LlcAligned, None, &t).unwrap();
match out {
AffinityIntent::Exact(set) => {
assert_eq!(
set.len(),
4,
"LlcAligned flattens to one LLC's worth of CPUs"
);
}
other => panic!("expected Exact, got {other:?}"),
}
let exact_cpus: BTreeSet<usize> = [0usize, 2, 4].into_iter().collect();
let out = intent_for_spawn(&AffinityIntent::Exact(exact_cpus.clone()), None, &t).unwrap();
match out {
AffinityIntent::Exact(set) => {
assert_eq!(
set, exact_cpus,
"Exact round-trip must preserve the CPU set"
);
}
other => panic!("expected Exact, got {other:?}"),
}
let pool: BTreeSet<usize> = [0usize, 1, 2, 3].into_iter().collect();
let intent = AffinityIntent::random_subset(pool.iter().copied(), 2);
let out = intent_for_spawn(&intent, None, &t).unwrap();
match out {
AffinityIntent::RandomSubset { from, count } => {
assert_eq!(from, pool, "RandomSubset.from must round-trip");
assert_eq!(count, 2, "RandomSubset.count must round-trip");
}
other => panic!("expected RandomSubset, got {other:?}"),
}
}
#[test]
fn intent_for_spawn_propagates_random_empty_intersection_bail() {
let vmt = crate::vmm::topology::Topology::new(1, 2, 2, 2);
let t = crate::topology::TestTopology::from_vm_topology(&vmt);
let empty_cpuset: BTreeSet<usize> = BTreeSet::new();
let intent = AffinityIntent::random_subset(t.all_cpus().iter().copied(), 1);
let err = intent_for_spawn(&intent, Some(&empty_cpuset), &t)
.expect_err("RandomSubset with empty cpuset intersection must bail");
let msg = err.to_string();
assert!(
msg.contains("AffinityIntent::RandomSubset") && msg.contains("after intersecting"),
"bail diag should name the intent and the intersection, got: {msg}"
);
}
#[test]
fn intent_for_spawn_propagates_random_zero_count_bail() {
let vmt = crate::vmm::topology::Topology::new(1, 2, 2, 2);
let t = crate::topology::TestTopology::from_vm_topology(&vmt);
let pool: BTreeSet<usize> = [0usize, 1].into_iter().collect();
let intent = AffinityIntent::random_subset(pool.iter().copied(), 0);
let err = intent_for_spawn(&intent, None, &t).expect_err("RandomSubset { count: 0 } must bail");
let msg = err.to_string();
assert!(
msg.contains("count=0"),
"bail diag should name the count=0 condition, got: {msg}"
);
}
#[test]
fn intent_for_spawn_propagates_exact_empty_bail() {
let vmt = crate::vmm::topology::Topology::new(1, 2, 2, 2);
let t = crate::topology::TestTopology::from_vm_topology(&vmt);
let intent = AffinityIntent::Exact(BTreeSet::new());
let err = intent_for_spawn(&intent, None, &t).expect_err("Exact(empty) must bail");
let msg = err.to_string();
assert!(
msg.contains("AffinityIntent::Exact(BTreeSet::new())"),
"bail diag should name the empty Exact, got: {msg}"
);
}
#[test]
fn intent_for_spawn_propagates_single_cpu_empty_cpuset_bail() {
let vmt = crate::vmm::topology::Topology::new(1, 2, 2, 2);
let t = crate::topology::TestTopology::from_vm_topology(&vmt);
let empty_cpuset: BTreeSet<usize> = BTreeSet::new();
let err = intent_for_spawn(&AffinityIntent::SingleCpu, Some(&empty_cpuset), &t)
.expect_err("SingleCpu with empty cpuset must bail");
let msg = err.to_string();
assert!(
msg.contains("AffinityIntent::SingleCpu") && msg.contains("empty"),
"bail diag should name the intent and the empty cpuset, got: {msg}"
);
}
#[test]
fn intent_for_spawn_propagates_llc_aligned_no_overlap_bail() {
let vmt = crate::vmm::topology::Topology::new(1, 2, 2, 2);
let t = crate::topology::TestTopology::from_vm_topology(&vmt);
let empty_cpuset: BTreeSet<usize> = BTreeSet::new();
let err = intent_for_spawn(&AffinityIntent::LlcAligned, Some(&empty_cpuset), &t)
.expect_err("LlcAligned with empty cpuset must bail");
let msg = err.to_string();
assert!(
msg.contains("AffinityIntent::LlcAligned") && msg.contains("No LLC has any CPU"),
"bail diag should name the intent and the no-overlap failure mode, got: {msg}"
);
}
#[test]
fn intent_for_spawn_propagates_exact_disjoint_bail() {
let vmt = crate::vmm::topology::Topology::new(1, 2, 2, 2);
let t = crate::topology::TestTopology::from_vm_topology(&vmt);
let cpuset: BTreeSet<usize> = [0, 1].into_iter().collect();
let exact: BTreeSet<usize> = [6, 7].into_iter().collect();
let err = intent_for_spawn(&AffinityIntent::Exact(exact), Some(&cpuset), &t)
.expect_err("Exact disjoint from cpuset must bail");
let msg = err.to_string();
assert!(
msg.contains("AffinityIntent::Exact") && msg.contains("disjoint"),
"bail diag should name the intent and the disjoint failure mode, got: {msg}"
);
}
#[test]
fn intent_for_spawn_propagates_smt_sibling_pair_bail() {
let t = crate::topology::TestTopology::synthetic(8, 2);
let err = intent_for_spawn(&AffinityIntent::SmtSiblingPair, None, &t)
.expect_err("SmtSiblingPair on a topology without per-core sibling data must bail");
let msg = err.to_string();
assert!(
msg.contains("AffinityIntent::SmtSiblingPair") && msg.contains("two SMT siblings"),
"bail diag should name the intent and the no-SMT-pair failure mode, got: {msg}"
);
}
#[test]
fn resolve_affinity_exact_nonempty_disjoint_from_empty_cpuset_bails() {
let t = crate::topology::TestTopology::synthetic(4, 1);
let empty_cpuset: BTreeSet<usize> = BTreeSet::new();
let exact: BTreeSet<usize> = [0usize, 1].into_iter().collect();
let err = resolve_affinity_for_cgroup(
&AffinityIntent::Exact(exact.clone()),
Some(&empty_cpuset),
&t,
)
.expect_err("Exact non-empty against empty cpuset must bail");
let msg = format!("{err:#}");
assert!(
msg.contains("disjoint"),
"bail diag should name the disjoint failure mode (NOT the empty-Exact form, since the Exact set itself is non-empty): got {msg}",
);
assert!(
msg.contains("empty cpuset"),
"bail diag should render the empty cpuset via format_cpuset_for_diag: got {msg}",
);
}
#[test]
fn resolve_affinity_llc_aligned_nonempty_disjoint_cpuset_bails() {
let t = crate::topology::TestTopology::synthetic(8, 2);
let cpuset: BTreeSet<usize> = [99usize].into_iter().collect();
let err = resolve_affinity_for_cgroup(&AffinityIntent::LlcAligned, Some(&cpuset), &t)
.expect_err("LlcAligned with non-empty disjoint cpuset must bail");
let msg = format!("{err:#}");
assert!(
msg.contains("AffinityIntent::LlcAligned"),
"diagnostic must name the intent variant: got {msg}",
);
assert!(
msg.contains("No LLC has any CPU"),
"diagnostic must name the failure mode: got {msg}",
);
assert!(
msg.contains("99"),
"bail diag should render the non-empty cpuset's CPU IDs via format_cpuset_for_diag: got {msg}",
);
}
#[test]
fn settled_hold_returns_settle_plus_fraction_of_duration() {
let cg = crate::cgroup::CgroupManager::new("/nonexistent");
let topo = crate::topology::TestTopology::synthetic(1, 1);
let ctx = Ctx::builder(&cg, &topo)
.settle(Duration::from_millis(200))
.duration(Duration::from_secs(1))
.build();
assert_eq!(
ctx.settled_hold(0.5),
crate::scenario::ops::HoldSpec::fixed(Duration::from_millis(700)),
);
}
#[test]
fn settled_hold_full_fraction_returns_settle_plus_full_duration() {
let cg = crate::cgroup::CgroupManager::new("/nonexistent");
let topo = crate::topology::TestTopology::synthetic(1, 1);
let ctx = Ctx::builder(&cg, &topo)
.settle(Duration::from_millis(100))
.duration(Duration::from_secs(2))
.build();
assert_eq!(
ctx.settled_hold(1.0),
crate::scenario::ops::HoldSpec::fixed(Duration::from_millis(2100)),
);
}
#[test]
fn settled_hold_zero_fraction_returns_settle_only() {
let cg = crate::cgroup::CgroupManager::new("/nonexistent");
let topo = crate::topology::TestTopology::synthetic(1, 1);
let ctx = Ctx::builder(&cg, &topo)
.settle(Duration::from_millis(500))
.duration(Duration::from_secs(1))
.build();
assert_eq!(
ctx.settled_hold(0.0),
crate::scenario::ops::HoldSpec::fixed(Duration::from_millis(500)),
);
}
#[test]
fn settled_hold_third_fraction_matches_integer_division() {
let cg = crate::cgroup::CgroupManager::new("/nonexistent");
let topo = crate::topology::TestTopology::synthetic(1, 1);
let ctx = Ctx::builder(&cg, &topo)
.settle(Duration::from_millis(100))
.duration(Duration::from_secs(3))
.build();
let hold = ctx.settled_hold(1.0 / 3.0);
let crate::scenario::ops::HoldSpec::Fixed(d) = hold else {
panic!("expected Fixed variant, got {hold:?}");
};
let expected = Duration::from_millis(1100);
let diff = d.abs_diff(expected);
assert!(
diff <= Duration::from_nanos(1),
"settled_hold(1.0/3.0) drift > 1ns: {d:?} vs {expected:?}",
);
}
#[test]
#[should_panic(expected = "cannot convert float seconds to Duration")]
fn settled_hold_panics_on_nan() {
let cg = crate::cgroup::CgroupManager::new("/nonexistent");
let topo = crate::topology::TestTopology::synthetic(1, 1);
let ctx = Ctx::builder(&cg, &topo)
.settle(Duration::from_millis(100))
.duration(Duration::from_secs(1))
.build();
let _ = ctx.settled_hold(f64::NAN);
}
#[test]
fn cgroup_def_carries_workers_per_cgroup() {
let cg = crate::cgroup::CgroupManager::new("/nonexistent");
let topo = crate::topology::TestTopology::synthetic(2, 1);
let ctx = Ctx::builder(&cg, &topo).workers_per_cgroup(7).build();
let def = ctx.cgroup_def("cg_0");
assert_eq!(def.name, "cg_0", "name must thread through verbatim");
assert_eq!(
def.works.len(),
1,
"the default workers(n) call seeds exactly one WorkSpec; \
a regression that doubled or skipped the seeding would \
surface here: {:?}",
def.works,
);
assert_eq!(
def.works[0].num_workers,
Some(7),
"WorkSpec must carry ctx.workers_per_cgroup as num_workers: {:?}",
def.works[0],
);
}
#[test]
fn cgroup_def_default_workers_per_cgroup_is_one() {
let cg = crate::cgroup::CgroupManager::new("/nonexistent");
let topo = crate::topology::TestTopology::synthetic(2, 1);
let ctx = Ctx::builder(&cg, &topo).build();
let def = ctx.cgroup_def("cg_default");
assert_eq!(def.works[0].num_workers, Some(1));
}
#[test]
fn cgroup_def_accepts_static_str_and_string() {
let cg = crate::cgroup::CgroupManager::new("/nonexistent");
let topo = crate::topology::TestTopology::synthetic(2, 1);
let ctx = Ctx::builder(&cg, &topo).build();
let from_static = ctx.cgroup_def("static_name");
let from_string = ctx.cgroup_def(String::from("owned_name"));
assert_eq!(from_static.name, "static_name");
assert_eq!(from_string.name, "owned_name");
}
#[test]
fn cgroup_def_chains_further_builders() {
use crate::scenario::ops::CpusetSpec;
use crate::workload::WorkType;
let cg = crate::cgroup::CgroupManager::new("/nonexistent");
let topo = crate::topology::TestTopology::synthetic(4, 1);
let ctx = Ctx::builder(&cg, &topo).workers_per_cgroup(3).build();
let def = ctx
.cgroup_def("cg_chained")
.cpuset(CpusetSpec::range(0.0, 1.0))
.work_type(WorkType::SpinWait);
assert_eq!(def.name, "cg_chained");
assert_eq!(
def.works[0].num_workers,
Some(3),
"ctx default propagates through subsequent chained builders",
);
assert!(
def.cpuset.is_some(),
"cpuset chains correctly after the helper",
);
assert!(
matches!(def.works[0].work_type, WorkType::SpinWait),
"work_type chains after the helper-seeded WorkSpec; got {:?}",
def.works[0].work_type,
);
}
#[test]
fn cgroup_def_passes_through_zero_workers_per_cgroup() {
let cg = crate::cgroup::CgroupManager::new("/nonexistent");
let topo = crate::topology::TestTopology::synthetic(2, 1);
let ctx = Ctx::builder(&cg, &topo).workers_per_cgroup(0).build();
let def = ctx.cgroup_def("zero_workers");
assert_eq!(
def.works[0].num_workers,
Some(0),
"helper preserves Ctx::workers_per_cgroup=0 verbatim; \
for an empty move-target cgroup use Op::AddCgroup per \
CgroupDef::named's docstring",
);
}
#[test]
fn cgroup_def_override_workers_replaces_default() {
let cg = crate::cgroup::CgroupManager::new("/nonexistent");
let topo = crate::topology::TestTopology::synthetic(2, 1);
let ctx = Ctx::builder(&cg, &topo).workers_per_cgroup(3).build();
let def = ctx.cgroup_def("cg_override").workers(99);
assert_eq!(
def.works[0].num_workers,
Some(99),
"explicit workers(N) overrides ctx default",
);
assert_eq!(def.works.len(), 1, "no second WorkSpec is added");
}
#[test]
fn cgroup_def_with_second_workspec_preserves_helper_seed() {
use crate::workload::WorkSpec;
let cg = crate::cgroup::CgroupManager::new("/nonexistent");
let topo = crate::topology::TestTopology::synthetic(2, 1);
let ctx = Ctx::builder(&cg, &topo).workers_per_cgroup(3).build();
let def = ctx
.cgroup_def("cg_two_works")
.work(WorkSpec::default().workers(5));
assert_eq!(
def.works.len(),
2,
"second WorkSpec appended, not replaced; helper seed stays at works[0]",
);
assert_eq!(
def.works[0].num_workers,
Some(3),
"helper's ctx default preserved as works[0]",
);
assert_eq!(
def.works[1].num_workers,
Some(5),
"appended WorkSpec lands as works[1]",
);
}