use anyhow::Result;
use linkme::distributed_slice;
use std::time::Duration;
use crate::assert::AssertResult;
use crate::scenario::Ctx;
use crate::scenario::flags::FlagDecl;
pub use crate::vmm::topology::{MemSideCache, NumaDistance, NumaNode, Topology};
pub enum SchedulerSpec {
Eevdf,
Discover(&'static str),
Path(&'static str),
KernelBuiltin {
enable: &'static [&'static str],
disable: &'static [&'static str],
},
}
impl SchedulerSpec {
pub const fn has_active_scheduling(&self) -> bool {
!matches!(self, SchedulerSpec::Eevdf)
}
pub const fn display_name(&self) -> &'static str {
match self {
SchedulerSpec::Eevdf => "eevdf",
SchedulerSpec::Discover(n) => n,
SchedulerSpec::Path(p) => p,
SchedulerSpec::KernelBuiltin { .. } => "kernel",
}
}
pub const fn scheduler_commit(&self) -> Option<&'static str> {
match self {
SchedulerSpec::Eevdf
| SchedulerSpec::Discover(_)
| SchedulerSpec::Path(_)
| SchedulerSpec::KernelBuiltin { .. } => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Sysctl {
pub key: &'static str,
pub value: &'static str,
}
impl Sysctl {
pub const fn new(key: &'static str, value: &'static str) -> Self {
Self { key, value }
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CgroupPath(&'static str);
impl CgroupPath {
pub const fn new(path: &'static str) -> Self {
assert!(
!path.is_empty() && path.as_bytes()[0] == b'/',
"CgroupPath must begin with '/' (e.g. \"/ktstr\")"
);
assert!(
path.len() > 1,
"CgroupPath must not be \"/\" alone (that is the cgroup root)"
);
Self(path)
}
pub const fn as_str(&self) -> &'static str {
self.0
}
pub fn sysfs_path(&self) -> String {
format!("/sys/fs/cgroup{}", self.0)
}
}
impl std::ops::Deref for CgroupPath {
type Target = str;
fn deref(&self) -> &str {
self.0
}
}
impl std::fmt::Display for CgroupPath {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.0)
}
}
pub struct BpfMapWrite {
pub map_name_suffix: &'static str,
pub offset: usize,
pub value: u32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TopologyConstraints {
pub min_numa_nodes: u32,
pub max_numa_nodes: Option<u32>,
pub min_llcs: u32,
pub max_llcs: Option<u32>,
pub requires_smt: bool,
pub min_cpus: u32,
pub max_cpus: Option<u32>,
}
impl TopologyConstraints {
pub const DEFAULT: TopologyConstraints = TopologyConstraints {
min_numa_nodes: 1,
max_numa_nodes: Some(1),
min_llcs: 1,
max_llcs: Some(12),
requires_smt: false,
min_cpus: 1,
max_cpus: Some(192),
};
pub fn accepts(
&self,
topo: &Topology,
host_cpus: u32,
host_llcs: u32,
host_max_cpus_per_llc: u32,
) -> bool {
topo.num_numa_nodes() >= self.min_numa_nodes
&& self
.max_numa_nodes
.is_none_or(|max| topo.num_numa_nodes() <= max)
&& topo.num_llcs() >= self.min_llcs
&& self.max_llcs.is_none_or(|max| topo.num_llcs() <= max)
&& (!self.requires_smt || topo.threads_per_core >= 2)
&& topo.total_cpus() >= self.min_cpus
&& self.max_cpus.is_none_or(|max| topo.total_cpus() <= max)
&& topo.total_cpus() <= host_cpus
&& topo.num_llcs() <= host_llcs
&& topo.cores_per_llc * topo.threads_per_core <= host_max_cpus_per_llc
}
}
pub struct Scheduler {
pub name: &'static str,
pub binary: SchedulerSpec,
pub flags: &'static [&'static FlagDecl],
pub sysctls: &'static [Sysctl],
pub kargs: &'static [&'static str],
pub assert: crate::assert::Assert,
pub cgroup_parent: Option<CgroupPath>,
pub sched_args: &'static [&'static str],
pub topology: Topology,
pub constraints: TopologyConstraints,
pub config_file: Option<&'static str>,
}
impl Scheduler {
pub const EEVDF: Scheduler = Scheduler {
name: "eevdf",
binary: SchedulerSpec::Eevdf,
flags: &[],
sysctls: &[],
kargs: &[],
assert: crate::assert::Assert::NO_OVERRIDES,
cgroup_parent: None,
sched_args: &[],
topology: Topology {
llcs: 1,
cores_per_llc: 2,
threads_per_core: 1,
numa_nodes: 1,
nodes: None,
distances: None,
},
constraints: TopologyConstraints::DEFAULT,
config_file: None,
};
pub const fn new(name: &'static str) -> Scheduler {
Scheduler {
name,
binary: SchedulerSpec::Eevdf,
flags: &[],
sysctls: &[],
kargs: &[],
assert: crate::assert::Assert::NO_OVERRIDES,
cgroup_parent: None,
sched_args: &[],
topology: Topology {
llcs: 1,
cores_per_llc: 2,
threads_per_core: 1,
numa_nodes: 1,
nodes: None,
distances: None,
},
constraints: TopologyConstraints::DEFAULT,
config_file: None,
}
}
pub const fn binary(mut self, binary: SchedulerSpec) -> Self {
self.binary = binary;
self
}
pub const fn flags(mut self, flags: &'static [&'static FlagDecl]) -> Self {
self.flags = flags;
self
}
pub const fn sysctls(mut self, sysctls: &'static [Sysctl]) -> Self {
self.sysctls = sysctls;
self
}
pub const fn kargs(mut self, kargs: &'static [&'static str]) -> Self {
self.kargs = kargs;
self
}
pub const fn assert(mut self, assert: crate::assert::Assert) -> Self {
self.assert = assert;
self
}
pub const fn cgroup_parent(mut self, path: &'static str) -> Self {
self.cgroup_parent = Some(CgroupPath::new(path));
self
}
pub const fn sched_args(mut self, args: &'static [&'static str]) -> Self {
self.sched_args = args;
self
}
pub const fn topology(mut self, numa_nodes: u32, llcs: u32, cores: u32, threads: u32) -> Self {
self.topology = Topology {
llcs,
cores_per_llc: cores,
threads_per_core: threads,
numa_nodes,
nodes: None,
distances: None,
};
self
}
pub const fn constraints(mut self, constraints: TopologyConstraints) -> Self {
self.constraints = constraints;
self
}
pub const fn min_numa_nodes(mut self, n: u32) -> Self {
self.constraints.min_numa_nodes = n;
self
}
pub const fn max_numa_nodes(mut self, n: u32) -> Self {
self.constraints.max_numa_nodes = Some(n);
self
}
pub const fn min_llcs(mut self, n: u32) -> Self {
self.constraints.min_llcs = n;
self
}
pub const fn max_llcs(mut self, n: u32) -> Self {
self.constraints.max_llcs = Some(n);
self
}
pub const fn requires_smt(mut self, v: bool) -> Self {
self.constraints.requires_smt = v;
self
}
pub const fn min_cpus(mut self, n: u32) -> Self {
self.constraints.min_cpus = n;
self
}
pub const fn max_cpus(mut self, n: u32) -> Self {
self.constraints.max_cpus = Some(n);
self
}
pub const fn config_file(mut self, path: &'static str) -> Self {
self.config_file = Some(path);
self
}
pub fn supported_flag_names(&self) -> Vec<&str> {
self.flags.iter().map(|f| f.name).collect()
}
pub fn flag_requires(&self, name: &str) -> Vec<&str> {
self.flags
.iter()
.find(|f| f.name == name)
.map(|f| f.requires.iter().map(|r| r.name).collect())
.unwrap_or_default()
}
pub fn flag_args(&self, name: &str) -> Option<&'static [&'static str]> {
self.flags.iter().find(|f| f.name == name).map(|f| f.args)
}
pub fn generate_profiles(
&self,
required: &[&'static str],
excluded: &[&'static str],
) -> Vec<crate::scenario::FlagProfile> {
let all: Vec<&'static str> = self.flags.iter().map(|f| f.name).collect();
let requires_fn = |&f: &&'static str| -> Vec<&'static str> {
self.flag_requires(f)
.into_iter()
.filter_map(|name| self.flags.iter().map(|d| d.name).find(|n| *n == name))
.collect()
};
crate::scenario::compute_flag_profiles(&all, requires_fn, required, excluded)
.into_iter()
.map(|flags| crate::scenario::FlagProfile { flags })
.collect()
}
}
pub struct KtstrTestEntry {
pub name: &'static str,
pub func: fn(&Ctx) -> Result<AssertResult>,
pub topology: Topology,
pub constraints: TopologyConstraints,
pub memory_mb: u32,
pub scheduler: &'static crate::test_support::Payload,
pub payload: Option<&'static crate::test_support::Payload>,
pub workloads: &'static [&'static crate::test_support::Payload],
pub auto_repro: bool,
pub assert: crate::assert::Assert,
pub extra_sched_args: &'static [&'static str],
pub watchdog_timeout: Duration,
pub bpf_map_write: &'static [&'static BpfMapWrite],
pub required_flags: &'static [&'static str],
pub excluded_flags: &'static [&'static str],
pub performance_mode: bool,
pub duration: Duration,
pub workers_per_cgroup: u32,
pub expect_err: bool,
pub host_only: bool,
pub extra_include_files: &'static [&'static str],
pub cleanup_budget: Option<Duration>,
pub disk: Option<crate::vmm::disk_config::DiskConfig>,
pub post_vm: Option<fn(&crate::vmm::VmResult) -> Result<()>>,
}
fn default_test_func(_ctx: &Ctx) -> Result<AssertResult> {
anyhow::bail!("KtstrTestEntry::DEFAULT func called — override func before use")
}
impl KtstrTestEntry {
pub const DEFAULT: KtstrTestEntry = KtstrTestEntry {
name: "",
func: default_test_func,
topology: Topology {
llcs: 1,
cores_per_llc: 2,
threads_per_core: 1,
numa_nodes: 1,
nodes: None,
distances: None,
},
constraints: TopologyConstraints::DEFAULT,
memory_mb: 2048,
scheduler: &crate::test_support::Payload::KERNEL_DEFAULT,
payload: None,
workloads: &[],
auto_repro: true,
assert: crate::assert::Assert::NO_OVERRIDES,
extra_sched_args: &[],
watchdog_timeout: Duration::from_secs(5),
bpf_map_write: &[],
required_flags: &[],
excluded_flags: &[],
performance_mode: false,
duration: Duration::from_secs(12),
workers_per_cgroup: 2,
expect_err: false,
host_only: false,
extra_include_files: &[],
cleanup_budget: None,
disk: None,
post_vm: None,
};
pub fn validate(&self) -> anyhow::Result<()> {
if self.name.is_empty() {
anyhow::bail!(
"KtstrTestEntry.name must be non-empty (empty names \
collide in nextest output and sidecar lookups)"
);
}
if self.name.contains('/') || self.name.contains('\\') {
anyhow::bail!(
"KtstrTestEntry '{}' name must not contain path \
separators ('/' or '\\') — they embed in sidecar \
filenames and nextest test IDs, creating synthetic \
subdirectories in sidecar output and mangling \
nextest -E 'test(name)' filtering",
self.name,
);
}
if self.memory_mb == 0 {
anyhow::bail!(
"KtstrTestEntry '{}'.memory_mb must be > 0 (a VM with \
zero memory cannot boot)",
self.name,
);
}
if self.duration.is_zero() {
anyhow::bail!(
"KtstrTestEntry '{}'.duration must be > 0 (a zero-duration \
run never exercises the scheduler and produces no data \
for assertions)",
self.name,
);
}
if self.workers_per_cgroup == 0 {
anyhow::bail!(
"KtstrTestEntry '{}'.workers_per_cgroup must be > 0 (a \
zero-worker cgroup emits no WorkerReports and assertions \
vacuously pass)",
self.name,
);
}
if let Some(p) = self.payload
&& p.is_scheduler()
{
anyhow::bail!(
"KtstrTestEntry '{}'.payload must be PayloadKind::Binary, \
not Scheduler-kind (schedulers belong in the `scheduler` \
slot; the `payload` slot is for userspace binaries \
composed under the scheduler)",
self.name,
);
}
if self.host_only && self.disk.is_some() {
anyhow::bail!(
"KtstrTestEntry '{}'.host_only=true with disk=Some(..) — \
host_only skips the VM boot that owns the virtio-blk \
device lifecycle, so the disk would never be attached. \
Drop one of host_only or disk.",
self.name,
);
}
for (idx, w) in self.workloads.iter().enumerate() {
if w.is_scheduler() {
anyhow::bail!(
"KtstrTestEntry '{}'.workloads[{idx}] (name='{}') must be \
PayloadKind::Binary, not Scheduler-kind (schedulers belong \
in the `scheduler` slot; the `workloads` slot is for \
userspace binaries composed under the scheduler)",
self.name,
w.name,
);
}
}
Ok(())
}
pub fn all_include_files(&self) -> Vec<&'static str> {
let mut out: Vec<&'static str> = Vec::new();
out.extend(self.scheduler.include_files.iter().copied());
if let Some(p) = self.payload {
out.extend(p.include_files.iter().copied());
}
for w in self.workloads {
out.extend(w.include_files.iter().copied());
}
out.extend(self.extra_include_files.iter().copied());
out
}
}
#[distributed_slice]
pub static KTSTR_TESTS: [KtstrTestEntry];
pub fn find_test(name: &str) -> Option<&'static KtstrTestEntry> {
KTSTR_TESTS.iter().find(|e| e.name == name)
}
pub(crate) fn validate_entry_flags(entry: &KtstrTestEntry) {
if entry.scheduler.flags().is_empty() {
if !entry.required_flags.is_empty() || !entry.excluded_flags.is_empty() {
panic!(
"ktstr_test: '{}' specifies flags but scheduler '{}' has no flag declarations",
entry.name,
entry.scheduler.scheduler_name(),
);
}
return;
}
let valid: Vec<&str> = entry.scheduler.supported_flag_names();
for &flag in entry.required_flags {
if !valid.contains(&flag) {
panic!(
"ktstr_test: '{}' references unknown required_flag '{}'; valid flags for scheduler '{}': {}",
entry.name,
flag,
entry.scheduler.scheduler_name(),
valid.join(", "),
);
}
}
for &flag in entry.excluded_flags {
if !valid.contains(&flag) {
panic!(
"ktstr_test: '{}' references unknown excluded_flag '{}'; valid flags for scheduler '{}': {}",
entry.name,
flag,
entry.scheduler.scheduler_name(),
valid.join(", "),
);
}
}
for &flag in entry.required_flags {
if entry.excluded_flags.contains(&flag) {
panic!(
"ktstr_test: '{}' has flag '{}' in both required_flags and excluded_flags",
entry.name, flag,
);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::scenario::Ctx;
fn dummy_ctx() -> (crate::cgroup::CgroupManager, crate::topology::TestTopology) {
let cgroups = crate::cgroup::CgroupManager::new("/sys/fs/cgroup/ktstr-dummy");
let topo = crate::topology::TestTopology::from_vm_topology(&Topology {
llcs: 1,
cores_per_llc: 1,
threads_per_core: 1,
numa_nodes: 1,
nodes: None,
distances: None,
});
(cgroups, topo)
}
#[test]
fn ktstr_test_entry_default_fields() {
let d = KtstrTestEntry::DEFAULT;
assert_eq!(d.name, "");
assert_eq!(d.topology.llcs, 1);
assert_eq!(d.topology.cores_per_llc, 2);
assert_eq!(d.topology.threads_per_core, 1);
assert_eq!(d.topology.numa_nodes, 1);
assert!(d.topology.nodes.is_none());
assert!(d.topology.distances.is_none());
assert_eq!(d.constraints, TopologyConstraints::DEFAULT);
assert_eq!(d.memory_mb, 2048);
assert_eq!(d.scheduler.scheduler_name(), "eevdf");
assert!(!d.scheduler.has_active_scheduling());
assert!(d.auto_repro);
assert!(d.extra_sched_args.is_empty());
assert_eq!(d.watchdog_timeout, Duration::from_secs(5));
assert!(d.bpf_map_write.is_empty());
assert!(d.required_flags.is_empty());
assert!(d.excluded_flags.is_empty());
assert!(!d.performance_mode);
assert_eq!(d.duration, Duration::from_secs(12));
assert_eq!(d.workers_per_cgroup, 2);
assert!(!d.expect_err);
assert!(!d.host_only);
assert!(d.payload.is_none());
assert!(d.workloads.is_empty());
}
#[test]
fn ktstr_test_entry_payload_slot_can_be_populated() {
use crate::test_support::{OutputFormat, Payload, PayloadKind};
const FIO: Payload = Payload {
name: "fio",
kind: PayloadKind::Binary("fio"),
output: OutputFormat::Json,
default_args: &[],
default_checks: &[],
metrics: &[],
include_files: &[],
uses_parent_pgrp: false,
known_flags: None,
metric_bounds: None,
};
let entry = KtstrTestEntry {
name: "payload_entry",
payload: Some(&FIO),
..KtstrTestEntry::DEFAULT
};
let p = entry.payload.expect("payload set");
assert_eq!(p.name, "fio");
assert!(!p.is_scheduler());
}
#[test]
fn ktstr_test_entry_workloads_slot_accepts_multiple_payloads() {
use crate::test_support::{OutputFormat, Payload, PayloadKind};
const FIO: Payload = Payload {
name: "fio",
kind: PayloadKind::Binary("fio"),
output: OutputFormat::Json,
default_args: &[],
default_checks: &[],
metrics: &[],
include_files: &[],
uses_parent_pgrp: false,
known_flags: None,
metric_bounds: None,
};
const STRESS_NG: Payload = Payload {
name: "stress-ng",
kind: PayloadKind::Binary("stress-ng"),
output: OutputFormat::ExitCode,
default_args: &[],
default_checks: &[],
metrics: &[],
include_files: &[],
uses_parent_pgrp: false,
known_flags: None,
metric_bounds: None,
};
let entry = KtstrTestEntry {
name: "multi_workload",
workloads: &[&FIO, &STRESS_NG],
..KtstrTestEntry::DEFAULT
};
assert_eq!(entry.workloads.len(), 2);
assert_eq!(entry.workloads[0].name, "fio");
assert_eq!(entry.workloads[1].name, "stress-ng");
}
#[test]
fn validate_rejects_scheduler_kind_in_workloads() {
use crate::test_support::{OutputFormat, Payload, PayloadKind};
const GOOD: Payload = Payload {
name: "fio",
kind: PayloadKind::Binary("fio"),
output: OutputFormat::Json,
default_args: &[],
default_checks: &[],
metrics: &[],
include_files: &[],
uses_parent_pgrp: false,
known_flags: None,
metric_bounds: None,
};
fn good_test_func(_: &Ctx) -> Result<AssertResult> {
Ok(AssertResult::pass())
}
let entry = KtstrTestEntry {
name: "mixed_kinds",
func: good_test_func,
workloads: &[&GOOD, &Payload::KERNEL_DEFAULT],
..KtstrTestEntry::DEFAULT
};
let err = entry.validate().unwrap_err();
let msg = format!("{err}");
assert!(
msg.contains("workloads[1]") && msg.contains("Scheduler-kind"),
"expected workloads[1] Scheduler-kind bail, got: {msg}"
);
assert!(
msg.contains("kernel_default"),
"error must name the offending workload entry, got: {msg}"
);
}
#[test]
fn validate_accepts_binary_only_workloads() {
use crate::test_support::{OutputFormat, Payload, PayloadKind};
const FIO: Payload = Payload {
name: "fio",
kind: PayloadKind::Binary("fio"),
output: OutputFormat::Json,
default_args: &[],
default_checks: &[],
metrics: &[],
include_files: &[],
uses_parent_pgrp: false,
known_flags: None,
metric_bounds: None,
};
const STRESS_NG: Payload = Payload {
name: "stress-ng",
kind: PayloadKind::Binary("stress-ng"),
output: OutputFormat::ExitCode,
default_args: &[],
default_checks: &[],
metrics: &[],
include_files: &[],
uses_parent_pgrp: false,
known_flags: None,
metric_bounds: None,
};
fn good_test_func(_: &Ctx) -> Result<AssertResult> {
Ok(AssertResult::pass())
}
let entry = KtstrTestEntry {
name: "all_binary",
func: good_test_func,
workloads: &[&FIO, &STRESS_NG],
..KtstrTestEntry::DEFAULT
};
entry.validate().expect("binary-only workloads must pass");
}
#[test]
fn validate_rejects_host_only_with_disk() {
fn good_test_func(_: &Ctx) -> Result<AssertResult> {
Ok(AssertResult::pass())
}
let entry = KtstrTestEntry {
name: "host_only_with_disk",
func: good_test_func,
host_only: true,
disk: Some(crate::vmm::disk_config::DiskConfig::default()),
..KtstrTestEntry::DEFAULT
};
let err = entry
.validate()
.expect_err("host_only=true + disk=Some must be rejected");
let msg = format!("{err}");
assert!(
msg.contains("host_only=true") && msg.contains("disk"),
"expected host_only+disk diagnostic, got: {msg}",
);
assert!(
msg.contains("host_only_with_disk"),
"error must name the offending entry, got: {msg}",
);
}
#[test]
fn validate_accepts_host_only_without_disk() {
fn good_test_func(_: &Ctx) -> Result<AssertResult> {
Ok(AssertResult::pass())
}
let entry = KtstrTestEntry {
name: "host_only_no_disk",
func: good_test_func,
host_only: true,
disk: None,
..KtstrTestEntry::DEFAULT
};
entry
.validate()
.expect("host_only=true + disk=None must validate");
}
#[test]
fn validate_accepts_vm_with_disk() {
fn good_test_func(_: &Ctx) -> Result<AssertResult> {
Ok(AssertResult::pass())
}
let entry = KtstrTestEntry {
name: "vm_with_disk",
func: good_test_func,
host_only: false,
disk: Some(crate::vmm::disk_config::DiskConfig::default()),
..KtstrTestEntry::DEFAULT
};
entry
.validate()
.expect("host_only=false + disk=Some must validate");
}
#[test]
fn ktstr_test_entry_default_rejected_by_empty_name() {
let err = KtstrTestEntry::DEFAULT.validate().unwrap_err();
let msg = format!("{err}");
assert!(
msg.contains("name") && msg.contains("non-empty"),
"expected name-non-empty bail, got: {msg}"
);
}
#[test]
fn default_test_func_returns_err() {
let (cgroups, topo) = dummy_ctx();
let ctx = Ctx::builder(&cgroups, &topo)
.duration(Duration::from_millis(1))
.assert(crate::assert::Assert::NO_OVERRIDES)
.build();
let result = default_test_func(&ctx);
let err = result.expect_err("default_test_func must return Err, not Ok");
let msg = format!("{err}");
assert!(
msg.contains("KtstrTestEntry::DEFAULT func called"),
"expected DEFAULT-called bail message, got: {msg}"
);
assert!(
msg.contains("override func before use"),
"expected actionable hint, got: {msg}"
);
}
use super::super::test_helpers::{
FLAGS_A, FLAGS_AB, FLAGS_BORROW_LONG, FLAGS_BORROW_REBAL, FLAGS_LLC_STEAL, FLAGS_STEAL_LLC,
validate_entry,
};
#[test]
fn scheduler_eevdf_defaults() {
let s = &Scheduler::EEVDF;
assert_eq!(s.name, "eevdf");
assert!(s.flags.is_empty());
assert!(s.sysctls.is_empty());
assert!(s.kargs.is_empty());
assert!(s.assert.not_starved.is_none());
assert!(s.assert.max_imbalance_ratio.is_none());
}
#[test]
fn scheduler_new_builder() {
static TEST_SYSCTLS: &[Sysctl] =
&[Sysctl::new("kernel.sched_cfs_bandwidth_slice_us", "1000")];
let s = Scheduler::new("test_sched")
.binary(SchedulerSpec::Discover("test_bin"))
.flags(FLAGS_A)
.sysctls(TEST_SYSCTLS)
.kargs(&["nosmt"]);
assert_eq!(s.name, "test_sched");
assert_eq!(s.flags.len(), 1);
assert_eq!(s.sysctls.len(), 1);
assert_eq!(s.kargs.len(), 1);
}
#[test]
fn scheduler_supported_flag_names() {
let s = Scheduler::new("sched").flags(FLAGS_BORROW_REBAL);
let names = s.supported_flag_names();
assert_eq!(names, vec!["borrow", "rebal"]);
}
#[test]
fn scheduler_flag_requires_found() {
let s = Scheduler::new("sched").flags(FLAGS_STEAL_LLC);
assert_eq!(s.flag_requires("steal"), vec!["llc"]);
assert!(s.flag_requires("llc").is_empty());
}
#[test]
fn scheduler_flag_requires_not_found() {
let s = Scheduler::new("sched").flags(&[]);
assert!(s.flag_requires("nonexistent").is_empty());
}
#[test]
fn scheduler_flag_args_found() {
let s = Scheduler::new("sched").flags(FLAGS_BORROW_LONG);
assert_eq!(s.flag_args("borrow"), Some(["--enable-borrow"].as_slice()));
}
#[test]
fn scheduler_flag_args_not_found() {
let s = Scheduler::new("sched").flags(&[]);
assert!(s.flag_args("nonexistent").is_none());
}
#[test]
fn scheduler_generate_profiles_no_flags() {
let s = Scheduler::new("sched");
let profiles = s.generate_profiles(&[], &[]);
assert_eq!(profiles.len(), 1);
assert!(profiles[0].flags.is_empty());
}
#[test]
fn scheduler_generate_profiles_all_optional() {
let s = Scheduler::new("sched").flags(FLAGS_AB);
let profiles = s.generate_profiles(&[], &[]);
assert_eq!(profiles.len(), 4);
}
#[test]
fn scheduler_generate_profiles_with_required() {
let s = Scheduler::new("sched").flags(FLAGS_AB);
let profiles = s.generate_profiles(&["a"], &[]);
assert_eq!(profiles.len(), 2);
for p in &profiles {
assert!(p.flags.contains(&"a"));
}
}
#[test]
fn scheduler_generate_profiles_with_excluded() {
let s = Scheduler::new("sched").flags(FLAGS_AB);
let profiles = s.generate_profiles(&[], &["a"]);
assert_eq!(profiles.len(), 2);
for p in &profiles {
assert!(!p.flags.contains(&"a"));
}
}
#[test]
fn scheduler_generate_profiles_dependency_filter() {
let s = Scheduler::new("sched").flags(FLAGS_LLC_STEAL);
let profiles = s.generate_profiles(&[], &[]);
assert_eq!(profiles.len(), 3);
let steal_alone = profiles
.iter()
.any(|p| p.flags.contains(&"steal") && !p.flags.contains(&"llc"));
assert!(!steal_alone);
}
#[test]
fn scheduler_with_check() {
let v = crate::assert::Assert::NO_OVERRIDES
.check_not_starved()
.max_imbalance_ratio(3.0);
let s = Scheduler::new("sched").assert(v);
assert_eq!(s.assert.not_starved, Some(true));
assert_eq!(s.assert.max_imbalance_ratio, Some(3.0));
}
#[test]
fn ktstr_test_entry_validate_accepts_defaults() {
let e = validate_entry("ok", 512, Duration::from_secs(2), 2);
e.validate().unwrap();
}
#[test]
fn ktstr_test_entry_validate_rejects_empty_name() {
let e = validate_entry("", 512, Duration::from_secs(2), 2);
let err = e.validate().unwrap_err();
let msg = format!("{err}");
assert!(
msg.contains("name") && msg.contains("non-empty"),
"got: {msg}"
);
}
#[test]
fn ktstr_test_entry_validate_rejects_zero_memory() {
let e = validate_entry("t", 0, Duration::from_secs(2), 2);
let err = e.validate().unwrap_err();
let msg = format!("{err}");
assert!(
msg.contains("memory_mb") && msg.contains("> 0") && msg.contains("'t'"),
"got: {msg}"
);
}
#[test]
fn ktstr_test_entry_validate_rejects_zero_duration() {
let e = validate_entry("t", 512, Duration::ZERO, 2);
let err = e.validate().unwrap_err();
let msg = format!("{err}");
assert!(
msg.contains("duration") && msg.contains("> 0"),
"got: {msg}"
);
}
#[test]
fn ktstr_test_entry_validate_rejects_zero_workers() {
let e = validate_entry("t", 512, Duration::from_secs(2), 0);
let err = e.validate().unwrap_err();
let msg = format!("{err}");
assert!(
msg.contains("workers_per_cgroup") && msg.contains("> 0"),
"got: {msg}"
);
}
#[test]
fn topology_constraints_default_has_max_values() {
let c = TopologyConstraints::DEFAULT;
assert_eq!(c.max_llcs, Some(12));
assert_eq!(c.max_numa_nodes, Some(1));
assert_eq!(c.max_cpus, Some(192));
}
#[test]
fn topology_constraints_max_fields_set() {
let c = TopologyConstraints {
max_llcs: Some(16),
max_numa_nodes: Some(4),
max_cpus: Some(128),
..TopologyConstraints::DEFAULT
};
assert_eq!(c.max_llcs, Some(16));
assert_eq!(c.max_numa_nodes, Some(4));
assert_eq!(c.max_cpus, Some(128));
assert_eq!(c.min_numa_nodes, 1);
assert_eq!(c.min_llcs, 1);
assert_eq!(c.min_cpus, 1);
}
#[test]
fn topology_constraints_equality() {
let a = TopologyConstraints::DEFAULT;
let b = TopologyConstraints::DEFAULT;
assert_eq!(a, b);
let c = TopologyConstraints {
max_llcs: Some(8),
..TopologyConstraints::DEFAULT
};
assert_ne!(a, c);
}
#[test]
fn accepts_default_allows_within_limits() {
let c = TopologyConstraints::DEFAULT;
let t = Topology::new(1, 8, 4, 2);
assert!(c.accepts(&t, 128, 16, 32));
}
#[test]
fn accepts_default_rejects_multi_numa() {
let c = TopologyConstraints::DEFAULT;
let t = Topology::new(2, 8, 4, 2);
assert!(!c.accepts(&t, 128, 16, 32));
}
#[test]
fn accepts_default_rejects_too_many_llcs() {
let c = TopologyConstraints::DEFAULT;
let t = Topology::new(1, 16, 2, 1);
assert!(!c.accepts(&t, 128, 32, 32));
}
#[test]
fn accepts_none_means_no_limit() {
let c = TopologyConstraints {
max_llcs: None,
max_numa_nodes: None,
max_cpus: None,
..TopologyConstraints::DEFAULT
};
let t = Topology::new(4, 16, 8, 2);
assert!(c.accepts(&t, 512, 32, 32));
}
#[test]
fn accepts_rejects_too_many_llcs() {
let c = TopologyConstraints {
max_llcs: Some(4),
..TopologyConstraints::DEFAULT
};
let t = Topology::new(1, 8, 2, 1);
assert!(!c.accepts(&t, 128, 16, 32));
}
#[test]
fn accepts_allows_llcs_at_max() {
let c = TopologyConstraints {
max_llcs: Some(4),
..TopologyConstraints::DEFAULT
};
let t = Topology::new(1, 4, 2, 1);
assert!(c.accepts(&t, 128, 16, 32));
}
#[test]
fn accepts_rejects_too_many_numa_nodes() {
let c = TopologyConstraints {
max_numa_nodes: Some(2),
..TopologyConstraints::DEFAULT
};
let t = Topology::new(4, 4, 2, 1);
assert!(!c.accepts(&t, 128, 16, 32));
}
#[test]
fn accepts_allows_numa_at_max() {
let c = TopologyConstraints {
max_numa_nodes: Some(2),
..TopologyConstraints::DEFAULT
};
let t = Topology::new(2, 4, 2, 1);
assert!(c.accepts(&t, 128, 16, 32));
}
#[test]
fn accepts_rejects_too_many_cpus() {
let c = TopologyConstraints {
max_cpus: Some(16),
..TopologyConstraints::DEFAULT
};
let t = Topology::new(1, 4, 4, 2);
assert!(!c.accepts(&t, 128, 16, 32));
}
#[test]
fn accepts_allows_cpus_at_max() {
let c = TopologyConstraints {
max_cpus: Some(16),
..TopologyConstraints::DEFAULT
};
let t = Topology::new(1, 2, 4, 2);
assert!(c.accepts(&t, 128, 16, 32));
}
#[test]
fn accepts_rejects_too_few_llcs() {
let c = TopologyConstraints {
min_llcs: 4,
..TopologyConstraints::DEFAULT
};
let t = Topology::new(1, 2, 4, 1);
assert!(!c.accepts(&t, 128, 16, 32));
}
#[test]
fn accepts_rejects_exceeding_host_cpus() {
let c = TopologyConstraints::DEFAULT;
let t = Topology::new(1, 4, 4, 2); assert!(!c.accepts(&t, 16, 16, 32)); }
#[test]
fn accepts_rejects_exceeding_host_llcs() {
let c = TopologyConstraints::DEFAULT;
let t = Topology::new(1, 8, 2, 1);
assert!(!c.accepts(&t, 128, 4, 32)); }
#[test]
fn accepts_combined_min_and_max() {
let c = TopologyConstraints {
min_llcs: 2,
max_llcs: Some(8),
min_cpus: 4,
max_cpus: Some(32),
..TopologyConstraints::DEFAULT
};
assert!(!c.accepts(&Topology::new(1, 1, 4, 1), 128, 16, 32));
assert!(c.accepts(&Topology::new(1, 2, 2, 1), 128, 16, 32));
assert!(!c.accepts(&Topology::new(1, 16, 2, 1), 128, 16, 32));
assert!(c.accepts(&Topology::new(1, 8, 2, 1), 128, 16, 32));
}
#[test]
fn accepts_requires_smt() {
let c = TopologyConstraints {
requires_smt: true,
..TopologyConstraints::DEFAULT
};
let no_smt = Topology::new(1, 2, 4, 1);
let with_smt = Topology::new(1, 2, 4, 2);
assert!(!c.accepts(&no_smt, 128, 16, 32));
assert!(c.accepts(&with_smt, 128, 16, 32));
}
#[test]
fn accepts_rejects_too_few_numa_nodes() {
let c = TopologyConstraints {
min_numa_nodes: 2,
max_numa_nodes: None,
..TopologyConstraints::DEFAULT
};
let t = Topology::new(1, 4, 4, 1);
assert!(!c.accepts(&t, 128, 16, 32));
}
#[test]
fn accepts_rejects_too_few_cpus() {
let c = TopologyConstraints {
min_cpus: 32,
..TopologyConstraints::DEFAULT
};
let t = Topology::new(1, 2, 4, 2);
assert!(!c.accepts(&t, 128, 16, 32));
}
#[test]
fn accepts_rejects_exceeding_host_cpus_per_llc() {
let c = TopologyConstraints::DEFAULT;
let t = Topology::new(1, 2, 8, 2);
assert!(!c.accepts(&t, 128, 16, 8));
}
#[test]
#[should_panic(expected = "unknown required_flag")]
fn validate_entry_flags_unknown_required() {
static SCHED: Scheduler = Scheduler::new("sched").flags(FLAGS_AB);
static SCHED_PAYLOAD: crate::test_support::Payload = crate::test_support::Payload {
name: "sched",
kind: crate::test_support::PayloadKind::Scheduler(&SCHED),
output: crate::test_support::OutputFormat::ExitCode,
default_args: &[],
default_checks: &[],
metrics: &[],
include_files: &[],
uses_parent_pgrp: false,
known_flags: None,
metric_bounds: None,
};
let entry = KtstrTestEntry {
name: "bad_required",
scheduler: &SCHED_PAYLOAD,
required_flags: &["nonexistent"],
..KtstrTestEntry::DEFAULT
};
validate_entry_flags(&entry);
}
#[test]
#[should_panic(expected = "in both required_flags and excluded_flags")]
fn validate_entry_flags_both_required_and_excluded() {
static SCHED: Scheduler = Scheduler::new("sched").flags(FLAGS_AB);
static SCHED_PAYLOAD: crate::test_support::Payload = crate::test_support::Payload {
name: "sched",
kind: crate::test_support::PayloadKind::Scheduler(&SCHED),
output: crate::test_support::OutputFormat::ExitCode,
default_args: &[],
default_checks: &[],
metrics: &[],
include_files: &[],
uses_parent_pgrp: false,
known_flags: None,
metric_bounds: None,
};
let entry = KtstrTestEntry {
name: "bad_both",
scheduler: &SCHED_PAYLOAD,
required_flags: &["a"],
excluded_flags: &["a"],
..KtstrTestEntry::DEFAULT
};
validate_entry_flags(&entry);
}
#[test]
fn display_name_eevdf() {
assert_eq!(SchedulerSpec::Eevdf.display_name(), "eevdf");
}
#[test]
fn display_name_discover_returns_binary_name() {
assert_eq!(
SchedulerSpec::Discover("scx_mitosis").display_name(),
"scx_mitosis"
);
}
#[test]
fn display_name_path_returns_path_string() {
assert_eq!(
SchedulerSpec::Path("/usr/bin/scx_my_sched").display_name(),
"/usr/bin/scx_my_sched"
);
}
#[test]
fn display_name_kernel_builtin_returns_kernel() {
assert_eq!(
SchedulerSpec::KernelBuiltin {
enable: &[],
disable: &[],
}
.display_name(),
"kernel"
);
}
#[test]
fn scheduler_commit_eevdf_returns_none() {
assert!(
SchedulerSpec::Eevdf.scheduler_commit().is_none(),
"Eevdf has no userspace binary — scheduler_commit must \
be None so the sidecar field distinguishes this case \
from `Path(_)` (external, unknown commit). Got: {:?}",
SchedulerSpec::Eevdf.scheduler_commit(),
);
}
#[test]
fn scheduler_commit_discover_returns_none() {
assert!(
SchedulerSpec::Discover("scx_mitosis")
.scheduler_commit()
.is_none(),
"Discover(_) must return None — resolve_scheduler's \
cascade can pick up a binary whose commit doesn't \
match the workspace. Got: {:?}",
SchedulerSpec::Discover("scx_mitosis").scheduler_commit(),
);
}
#[test]
fn scheduler_commit_path_returns_none() {
assert!(
SchedulerSpec::Path("/usr/bin/scx_external")
.scheduler_commit()
.is_none(),
"Path(_) points at an externally-built binary — \
scheduler_commit must be None so consumers don't treat \
a fabricated commit as authoritative. Got: {:?}",
SchedulerSpec::Path("/usr/bin/scx_external").scheduler_commit(),
);
}
#[test]
fn scheduler_commit_kernel_builtin_returns_none() {
let spec = SchedulerSpec::KernelBuiltin {
enable: &[],
disable: &[],
};
assert!(
spec.scheduler_commit().is_none(),
"KernelBuiltin has no userspace binary — \
scheduler_commit must be None. Got: {:?}",
spec.scheduler_commit(),
);
}
#[test]
fn all_include_files_empty_when_nothing_declared() {
let entry = KtstrTestEntry {
name: "t",
..KtstrTestEntry::DEFAULT
};
assert!(entry.all_include_files().is_empty());
}
#[test]
fn all_include_files_merges_sources_in_order() {
static SCHED_PAYLOAD: crate::test_support::Payload = crate::test_support::Payload {
name: "sched",
kind: crate::test_support::PayloadKind::Scheduler(
&crate::test_support::Scheduler::EEVDF,
),
output: crate::test_support::OutputFormat::ExitCode,
default_args: &[],
default_checks: &[],
metrics: &[],
include_files: &["sched-helper"],
uses_parent_pgrp: false,
known_flags: None,
metric_bounds: None,
};
static PRIMARY: crate::test_support::Payload = crate::test_support::Payload {
name: "primary",
kind: crate::test_support::PayloadKind::Binary("fio"),
output: crate::test_support::OutputFormat::ExitCode,
default_args: &[],
default_checks: &[],
metrics: &[],
include_files: &["fio"],
uses_parent_pgrp: false,
known_flags: None,
metric_bounds: None,
};
static WL_A: crate::test_support::Payload = crate::test_support::Payload {
name: "wl_a",
kind: crate::test_support::PayloadKind::Binary("stress-ng"),
output: crate::test_support::OutputFormat::ExitCode,
default_args: &[],
default_checks: &[],
metrics: &[],
include_files: &["stress-ng"],
uses_parent_pgrp: false,
known_flags: None,
metric_bounds: None,
};
static WL_B: crate::test_support::Payload = crate::test_support::Payload {
name: "wl_b",
kind: crate::test_support::PayloadKind::Binary("schbench"),
output: crate::test_support::OutputFormat::ExitCode,
default_args: &[],
default_checks: &[],
metrics: &[],
include_files: &["schbench"],
uses_parent_pgrp: false,
known_flags: None,
metric_bounds: None,
};
static WORKLOADS: &[&crate::test_support::Payload] = &[&WL_A, &WL_B];
let entry = KtstrTestEntry {
name: "t",
scheduler: &SCHED_PAYLOAD,
payload: Some(&PRIMARY),
workloads: WORKLOADS,
extra_include_files: &["test-fixture.json"],
..KtstrTestEntry::DEFAULT
};
let got = entry.all_include_files();
assert_eq!(
got,
vec![
"sched-helper",
"fio",
"stress-ng",
"schbench",
"test-fixture.json",
],
"aggregation order must be scheduler → payload → workloads → extras",
);
}
#[test]
fn all_include_files_skips_absent_payload() {
static SCHED_PAYLOAD: crate::test_support::Payload = crate::test_support::Payload {
name: "sched",
kind: crate::test_support::PayloadKind::Scheduler(
&crate::test_support::Scheduler::EEVDF,
),
output: crate::test_support::OutputFormat::ExitCode,
default_args: &[],
default_checks: &[],
metrics: &[],
include_files: &["sched-helper"],
uses_parent_pgrp: false,
known_flags: None,
metric_bounds: None,
};
let entry = KtstrTestEntry {
name: "t",
scheduler: &SCHED_PAYLOAD,
payload: None,
workloads: &[],
extra_include_files: &[],
..KtstrTestEntry::DEFAULT
};
assert_eq!(entry.all_include_files(), vec!["sched-helper"]);
}
}