use std::collections::BTreeSet;
use std::time::Duration;
use serde::{Deserialize, Serialize};
use super::debug_capture::{
AffinityHint, CgroupHint, DebugCapture, SchedPolicyHint, WorkTypeHint, WorkloadFingerprint,
WorkloadGroupHint,
};
use crate::workload::{AffinityIntent, SchedPolicy, WorkType, WorkloadConfig};
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[allow(dead_code)] pub struct ReproducerSpec {
#[serde(skip)]
pub config: WorkloadConfig,
pub cgroup_hints: Vec<CgroupHint>,
pub notes: Vec<ReproducerNote>,
pub scheduler_name: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "kind", content = "message", rename_all = "snake_case")]
#[non_exhaustive]
pub enum ReproducerNote {
Informational(String),
Resolved(String),
UnresolvedAffinity(String),
UnmappedWorkType(String),
}
impl ReproducerNote {
#[allow(dead_code)]
pub fn message(&self) -> &str {
match self {
ReproducerNote::Informational(s)
| ReproducerNote::Resolved(s)
| ReproducerNote::UnresolvedAffinity(s)
| ReproducerNote::UnmappedWorkType(s) => s,
}
}
fn is_unresolved(&self) -> bool {
matches!(
self,
ReproducerNote::UnresolvedAffinity(_) | ReproducerNote::UnmappedWorkType(_)
)
}
}
impl std::fmt::Display for ReproducerNote {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.message())
}
}
impl ReproducerSpec {
#[allow(dead_code)]
pub fn is_runnable(&self) -> bool {
self.unresolved_count() == 0 && !is_unmapped_work_type(&self.config.work_type)
}
#[allow(dead_code)]
pub fn unresolved_count(&self) -> usize {
self.notes.iter().filter(|n| n.is_unresolved()).count()
}
}
#[allow(dead_code)]
pub fn generate_spec(capture: &DebugCapture) -> ReproducerSpec {
let mut spec = ReproducerSpec {
scheduler_name: failure_scheduler_name(capture),
..Default::default()
};
map_workload_groups(&capture.fingerprint, &mut spec);
map_affinity(&capture.fingerprint, &mut spec);
map_work_type(&capture.fingerprint, &mut spec);
map_sched_policy(&capture.fingerprint, &mut spec);
spec.cgroup_hints = capture.fingerprint.cgroup_hints.clone();
for gap in &capture.fingerprint.gaps {
spec.notes.push(ReproducerNote::Informational(format!(
"fingerprint gap: {gap}"
)));
}
spec
}
fn failure_scheduler_name(capture: &DebugCapture) -> String {
let _ = capture;
String::new()
}
fn map_workload_groups(fp: &WorkloadFingerprint, spec: &mut ReproducerSpec) {
let Some(primary) = fp.workload_groups.first() else {
spec.notes.push(ReproducerNote::Informational(
"no workload groups in fingerprint — defaulting num_workers=1".into(),
));
return;
};
spec.config.num_workers = primary.thread_count.max(1) as usize;
push_extras_note(
&mut spec.notes,
"additional workload groups not modeled in primary spec",
fp.workload_groups
.iter()
.skip(1)
.map(|g: &WorkloadGroupHint| format!("{} ({} threads)", g.cgroup_path, g.thread_count)),
);
}
fn push_extras_note(
notes: &mut Vec<ReproducerNote>,
header: &str,
entries: impl Iterator<Item = String>,
) {
let alts: Vec<String> = entries.collect();
if !alts.is_empty() {
notes.push(ReproducerNote::Informational(format!(
"{header}: {}",
alts.join(", ")
)));
}
}
fn topology_aware_note(variant: &str, engine_action: &str, hand_edit_target: &str) -> String {
format!(
"AffinityHint::{variant} observed without resolved CPUs; \
emitting AffinityIntent::{variant} — the scenario engine \
{engine_action} at apply time. The spawn-time affinity gate \
rejects this variant (no topology context); use the \
scenario engine or hand-edit to {hand_edit_target}"
)
}
fn topology_resolved_note(variant: &str, cpus: &[u32]) -> String {
format!(
"AffinityHint::{variant} observed with resolved CPUs {cpus:?}; \
emitting AffinityIntent::Exact directly so the spec runs \
without scenario-engine resolution",
)
}
fn cpus_to_set(cpus: &[u32]) -> BTreeSet<usize> {
cpus.iter().map(|&c| c as usize).collect()
}
fn map_topology_aware_affinity(
cpus: &[u32],
variant: &str,
topology_intent: AffinityIntent,
engine_action: &str,
hand_edit_target: &str,
spec: &mut ReproducerSpec,
) -> AffinityIntent {
if cpus.is_empty() {
spec.notes
.push(ReproducerNote::UnresolvedAffinity(topology_aware_note(
variant,
engine_action,
hand_edit_target,
)));
topology_intent
} else {
spec.notes
.push(ReproducerNote::Resolved(topology_resolved_note(
variant, cpus,
)));
AffinityIntent::Exact(cpus_to_set(cpus))
}
}
fn map_affinity(fp: &WorkloadFingerprint, spec: &mut ReproducerSpec) {
let Some(primary) = fp.affinity_hints.first() else {
return;
};
spec.config.affinity = match primary {
AffinityHint::Inherit => AffinityIntent::Inherit,
AffinityHint::SingleCpu { cpus } => map_topology_aware_affinity(
cpus,
"SingleCpu",
AffinityIntent::SingleCpu,
"picks the concrete CPU from the cgroup's cpuset",
"AffinityIntent::exact([<cpu>])",
spec,
),
AffinityHint::LlcAligned { cpus } => map_topology_aware_affinity(
cpus,
"LlcAligned",
AffinityIntent::LlcAligned,
"resolves the LLC mask from the cgroup's cpuset",
"AffinityIntent::exact([<llc_cpu_0>, <llc_cpu_1>, ...])",
spec,
),
AffinityHint::CrossCgroup { cpus } => map_topology_aware_affinity(
cpus,
"CrossCgroup",
AffinityIntent::CrossCgroup,
"expands to the full topology",
"AffinityIntent::exact([<cpu_0>, <cpu_1>, ...])",
spec,
),
AffinityHint::SmtSiblingPair { cpus } => map_topology_aware_affinity(
cpus,
"SmtSiblingPair",
AffinityIntent::SmtSiblingPair,
"picks an SMT-sibling pair from the cgroup's effective cpuset, \
or the full topology when no cpuset is active",
"AffinityIntent::exact([<sibling_a>, <sibling_b>])",
spec,
),
AffinityHint::Exact { cpus } => {
if cpus.is_empty() {
spec.notes.push(ReproducerNote::UnresolvedAffinity(
"AffinityHint::Exact observed with no CPUs; emitting \
AffinityIntent::Exact(empty) — the spawn-time \
affinity gate rejects an empty Exact set, so this \
spec is NOT runnable as-is. Hand-edit to \
AffinityIntent::exact([<cpu_0>, <cpu_1>, ...]) \
with the observed CPUs, or change to \
AffinityIntent::Inherit."
.into(),
));
} else {
spec.notes
.push(ReproducerNote::Resolved(topology_resolved_note(
"Exact", cpus,
)));
}
AffinityIntent::Exact(cpus_to_set(cpus))
}
AffinityHint::RandomSubset { from, count } => {
if from.is_empty() || *count == 0 {
spec.notes.push(ReproducerNote::UnresolvedAffinity(
"AffinityHint::RandomSubset observed without a \
resolved pool / count; emitting \
AffinityIntent::RandomSubset { from: empty, count: 0 } \
as a placeholder — the spawn-time affinity gate \
rejects empty-pool / zero-count RandomSubset, so \
this spec is NOT runnable as-is. Hand-edit `from` \
to the actual CPU pool and `count` to the desired \
sample size before running, or change to \
AffinityIntent::Inherit."
.into(),
));
AffinityIntent::RandomSubset {
from: BTreeSet::new(),
count: 0,
}
} else {
spec.notes.push(ReproducerNote::Resolved(format!(
"AffinityHint::RandomSubset observed with resolved \
pool {from:?} count={count}; emitting \
AffinityIntent::RandomSubset directly so the \
spawn-time affinity gate accepts it without \
hand-editing",
)));
AffinityIntent::RandomSubset {
from: cpus_to_set(from),
count: *count as usize,
}
}
}
};
push_extras_note(
&mut spec.notes,
"additional affinity hints not modeled",
fp.affinity_hints.iter().skip(1).map(|a| format!("{a:?}")),
);
}
fn map_work_type(fp: &WorkloadFingerprint, spec: &mut ReproducerSpec) {
let Some(primary) = fp.work_type_hints.first() else {
spec.notes.push(ReproducerNote::Informational(
"no work-type hint in fingerprint — defaulting to \
WorkType::SpinWait"
.into(),
));
return;
};
let work_type = match primary {
WorkTypeHint::SpinWait => WorkType::SpinWait,
WorkTypeHint::YieldHeavy => WorkType::YieldHeavy,
WorkTypeHint::Mixed => WorkType::Mixed,
WorkTypeHint::Bursty {
burst_duration,
sleep_duration,
} => WorkType::Bursty {
burst_duration: *burst_duration,
sleep_duration: *sleep_duration,
},
WorkTypeHint::PipeIo => WorkType::PipeIo { burst_iters: 1024 },
WorkTypeHint::FutexPingPong => WorkType::FutexPingPong { spin_iters: 1024 },
WorkTypeHint::CachePressure { size_kb, stride } => WorkType::CachePressure {
size_kb: *size_kb as usize,
stride: *stride as usize,
},
WorkTypeHint::IoSyncWrite => WorkType::IoSyncWrite,
WorkTypeHint::IoRandRead => WorkType::IoRandRead,
WorkTypeHint::IoConvoy => WorkType::IoConvoy,
};
record_work_type(work_type, spec);
push_extras_note(
&mut spec.notes,
"additional work-type hints observed",
fp.work_type_hints.iter().skip(1).map(|w| format!("{w:?}")),
);
}
fn record_work_type(work_type: WorkType, spec: &mut ReproducerSpec) {
spec.config.work_type = work_type;
if is_unmapped_work_type(&spec.config.work_type) {
spec.notes.push(ReproducerNote::UnmappedWorkType(format!(
"no fingerprint mapping for WorkType::{:?} — \
render_run_file_source emits a TODO-decorated \
SpinWait placeholder; hand-edit the rendered source to \
a real builder call before running",
spec.config.work_type,
)));
}
}
fn is_unmapped_work_type(w: &WorkType) -> bool {
match w {
WorkType::SpinWait
| WorkType::YieldHeavy
| WorkType::Mixed
| WorkType::IoSyncWrite
| WorkType::IoRandRead
| WorkType::IoConvoy
| WorkType::Bursty { .. }
| WorkType::PipeIo { .. }
| WorkType::FutexPingPong { .. }
| WorkType::CachePressure { .. } => false,
WorkType::CacheYield { .. }
| WorkType::CachePipe { .. }
| WorkType::FutexFanOut { .. }
| WorkType::Sequence { .. }
| WorkType::ForkExit
| WorkType::NiceSweep
| WorkType::AffinityChurn { .. }
| WorkType::PolicyChurn { .. }
| WorkType::FanOutCompute { .. }
| WorkType::PageFaultChurn { .. }
| WorkType::MutexContention { .. }
| WorkType::Custom { .. }
| WorkType::ThunderingHerd { .. }
| WorkType::PriorityInversion { .. }
| WorkType::ProducerConsumerImbalance { .. }
| WorkType::RtStarvation { .. }
| WorkType::AsymmetricWaker { .. }
| WorkType::WakeChain { .. }
| WorkType::NumaWorkingSetSweep { .. }
| WorkType::CgroupChurn { .. }
| WorkType::SignalStorm { .. }
| WorkType::PreemptStorm { .. }
| WorkType::EpollStorm { .. }
| WorkType::NumaMigrationChurn { .. }
| WorkType::IdleChurn { .. }
| WorkType::AluHot { .. }
| WorkType::SmtSiblingSpin
| WorkType::IpcVariance { .. } => true,
}
}
fn map_sched_policy(fp: &WorkloadFingerprint, spec: &mut ReproducerSpec) {
let Some(primary) = fp.sched_policy_hints.first() else {
return;
};
match primary {
SchedPolicyHint::Other { nice } => {
spec.config.sched_policy = SchedPolicy::Normal;
spec.config.nice = *nice;
}
SchedPolicyHint::Fifo { priority } => {
spec.config.sched_policy = SchedPolicy::Fifo(*priority);
}
SchedPolicyHint::RoundRobin { priority } => {
spec.config.sched_policy = SchedPolicy::RoundRobin(*priority);
}
SchedPolicyHint::Deadline {
runtime_ns,
deadline_ns,
period_ns,
} => {
spec.config.sched_policy = SchedPolicy::deadline(
Duration::from_nanos(*runtime_ns),
Duration::from_nanos(*deadline_ns),
Duration::from_nanos(*period_ns),
);
}
SchedPolicyHint::Batch => spec.config.sched_policy = SchedPolicy::Batch,
SchedPolicyHint::Idle => spec.config.sched_policy = SchedPolicy::Idle,
SchedPolicyHint::Ext => {
spec.notes.push(ReproducerNote::Informational(
"SchedPolicyHint::Ext observed; framework defaults to \
scx routing — no policy override emitted"
.into(),
));
}
}
push_extras_note(
&mut spec.notes,
"additional sched-policy hints observed",
fp.sched_policy_hints
.iter()
.skip(1)
.map(|s| format!("{s:?}")),
);
}
#[allow(dead_code)]
pub fn render_run_file_source(spec: &ReproducerSpec, template_name: &str) -> String {
let mut s = String::new();
s.push_str("// Auto-generated reproducer from a debug capture.\n");
s.push_str("// Edit the WorkloadConfig builder calls to refine\n");
s.push_str("// the projection.\n\n");
if !spec.scheduler_name.is_empty() {
s.push_str(&format!("// Scheduler: {}\n", spec.scheduler_name));
}
if !spec.notes.is_empty() {
s.push_str("//\n// Generator notes:\n");
for note in &spec.notes {
s.push_str(&format!("// - {note}\n"));
}
s.push('\n');
}
s.push_str("use ktstr::workload::*;\n");
s.push_str("use std::collections::BTreeSet;\n");
s.push_str("use std::time::Duration;\n\n");
s.push_str(&format!("pub fn {template_name}() -> WorkloadConfig {{\n"));
s.push_str(" WorkloadConfig::default()\n");
s.push_str(&format!(" .workers({})\n", spec.config.num_workers));
s.push_str(&format!(
" .affinity({})\n",
render_affinity(&spec.config.affinity)
));
s.push_str(&format!(
" .work_type({})\n",
render_work_type(&spec.config.work_type)
));
s.push_str(&format!(
" .sched_policy({})\n",
render_sched_policy(&spec.config.sched_policy)
));
s.push_str(&format!(" .nice({})\n", spec.config.nice));
s.push_str("}\n");
if !spec.cgroup_hints.is_empty() {
s.push_str("\n// Cgroup hints — apply at harness setup:\n");
for h in &spec.cgroup_hints {
s.push_str(&format!(
"// {} (weight={:?}, mem_max={:?}, cpuset={:?}, cpu_max_quota_us={:?})\n",
h.path, h.cpu_weight, h.memory_max_bytes, h.cpuset_cpus, h.cpu_max_quota_us,
));
}
}
s
}
#[allow(dead_code)]
pub fn render_ktstr_test_source(spec: &ReproducerSpec, template_name: &str) -> String {
let body = render_run_file_source(spec, template_name);
body.replace(
&format!("pub fn {template_name}"),
&format!("#[ktstr::ktstr_test]\npub fn {template_name}"),
)
}
fn render_affinity(a: &AffinityIntent) -> String {
match a {
AffinityIntent::Inherit => "AffinityIntent::Inherit".into(),
AffinityIntent::SingleCpu => "AffinityIntent::SingleCpu".into(),
AffinityIntent::LlcAligned => "AffinityIntent::LlcAligned".into(),
AffinityIntent::CrossCgroup => "AffinityIntent::CrossCgroup".into(),
AffinityIntent::SmtSiblingPair => "AffinityIntent::SmtSiblingPair".into(),
AffinityIntent::RandomSubset { from, count } => {
let cpus: Vec<String> = from.iter().map(|c| c.to_string()).collect();
format!(
"AffinityIntent::RandomSubset {{ from: BTreeSet::from([{}]), count: {} }}",
cpus.join(", "),
count
)
}
AffinityIntent::Exact(set) => {
let cpus: Vec<String> = set.iter().map(|c| c.to_string()).collect();
format!(
"AffinityIntent::Exact(BTreeSet::from([{}]))",
cpus.join(", ")
)
}
}
}
fn render_work_type(w: &WorkType) -> String {
match w {
WorkType::SpinWait => "WorkType::SpinWait".into(),
WorkType::YieldHeavy => "WorkType::YieldHeavy".into(),
WorkType::Mixed => "WorkType::Mixed".into(),
WorkType::IoSyncWrite => "WorkType::IoSyncWrite".into(),
WorkType::IoRandRead => "WorkType::IoRandRead".into(),
WorkType::IoConvoy => "WorkType::IoConvoy".into(),
WorkType::Bursty {
burst_duration,
sleep_duration,
} => format!(
"WorkType::Bursty {{ \
burst_duration: Duration::from_millis({}), \
sleep_duration: Duration::from_millis({}) \
}}",
burst_duration.as_millis(),
sleep_duration.as_millis(),
),
WorkType::PipeIo { burst_iters } => {
format!("WorkType::PipeIo {{ burst_iters: {burst_iters} }}")
}
WorkType::FutexPingPong { spin_iters } => {
format!("WorkType::FutexPingPong {{ spin_iters: {spin_iters} }}")
}
WorkType::CachePressure { size_kb, stride } => {
format!("WorkType::CachePressure {{ size_kb: {size_kb}, stride: {stride} }}")
}
WorkType::CacheYield { .. } => render_work_type_todo("CacheYield"),
WorkType::CachePipe { .. } => render_work_type_todo("CachePipe"),
WorkType::FutexFanOut { .. } => render_work_type_todo("FutexFanOut"),
WorkType::Sequence { .. } => render_work_type_todo("Sequence"),
WorkType::ForkExit => render_work_type_todo("ForkExit"),
WorkType::NiceSweep => render_work_type_todo("NiceSweep"),
WorkType::AffinityChurn { .. } => render_work_type_todo("AffinityChurn"),
WorkType::PolicyChurn { .. } => render_work_type_todo("PolicyChurn"),
WorkType::FanOutCompute { .. } => render_work_type_todo("FanOutCompute"),
WorkType::PageFaultChurn { .. } => render_work_type_todo("PageFaultChurn"),
WorkType::MutexContention { .. } => render_work_type_todo("MutexContention"),
WorkType::Custom { .. } => render_work_type_todo("Custom"),
WorkType::ThunderingHerd { .. } => render_work_type_todo("ThunderingHerd"),
WorkType::PriorityInversion { .. } => render_work_type_todo("PriorityInversion"),
WorkType::ProducerConsumerImbalance { .. } => {
render_work_type_todo("ProducerConsumerImbalance")
}
WorkType::RtStarvation { .. } => render_work_type_todo("RtStarvation"),
WorkType::AsymmetricWaker { .. } => render_work_type_todo("AsymmetricWaker"),
WorkType::WakeChain { .. } => render_work_type_todo("WakeChain"),
WorkType::NumaWorkingSetSweep { .. } => render_work_type_todo("NumaWorkingSetSweep"),
WorkType::CgroupChurn { .. } => render_work_type_todo("CgroupChurn"),
WorkType::SignalStorm { .. } => render_work_type_todo("SignalStorm"),
WorkType::PreemptStorm { .. } => render_work_type_todo("PreemptStorm"),
WorkType::EpollStorm { .. } => render_work_type_todo("EpollStorm"),
WorkType::NumaMigrationChurn { .. } => render_work_type_todo("NumaMigrationChurn"),
WorkType::IdleChurn { .. } => render_work_type_todo("IdleChurn"),
WorkType::AluHot { .. } => render_work_type_todo("AluHot"),
WorkType::SmtSiblingSpin => render_work_type_todo("SmtSiblingSpin"),
WorkType::IpcVariance { .. } => render_work_type_todo("IpcVariance"),
}
}
fn render_work_type_todo(variant: &str) -> String {
format!(
"WorkType::SpinWait /* TODO: no fingerprint mapping for \
WorkType::{variant} — refine from capture */"
)
}
fn render_sched_policy(p: &SchedPolicy) -> String {
match p {
SchedPolicy::Normal => "SchedPolicy::Normal".into(),
SchedPolicy::Batch => "SchedPolicy::Batch".into(),
SchedPolicy::Idle => "SchedPolicy::Idle".into(),
SchedPolicy::Fifo(prio) => format!("SchedPolicy::Fifo({prio})"),
SchedPolicy::RoundRobin(prio) => format!("SchedPolicy::RoundRobin({prio})"),
SchedPolicy::Deadline {
runtime,
deadline,
period,
} => format!(
"SchedPolicy::Deadline {{ runtime: Duration::from_nanos({}), \
deadline: Duration::from_nanos({}), period: Duration::from_nanos({}) }}",
runtime.as_nanos(),
deadline.as_nanos(),
period.as_nanos(),
),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::monitor::debug_capture::WorkloadGroupHint;
#[test]
fn generate_spec_empty_fingerprint() {
let cap = DebugCapture::default();
let spec = generate_spec(&cap);
assert_eq!(spec.config.num_workers, 1);
assert!(matches!(spec.config.affinity, AffinityIntent::Inherit));
assert!(matches!(spec.config.work_type, WorkType::SpinWait));
assert!(
spec.notes
.iter()
.any(|n| n.message().contains("no workload groups"))
);
assert!(
spec.notes
.iter()
.any(|n| n.message().contains("no work-type hint"))
);
}
#[test]
fn generate_spec_thread_count_to_workers() {
let mut cap = DebugCapture::default();
cap.fingerprint.workload_groups = vec![WorkloadGroupHint {
cgroup_path: "/test".into(),
thread_count: 8,
cpu_time_fraction: 0.5,
wakeups_per_sec: 100.0,
}];
let spec = generate_spec(&cap);
assert_eq!(spec.config.num_workers, 8);
}
#[test]
fn generate_spec_exact_affinity() {
let mut cap = DebugCapture::default();
cap.fingerprint.affinity_hints = vec![AffinityHint::Exact {
cpus: vec![0, 1, 4, 5],
}];
let spec = generate_spec(&cap);
match spec.config.affinity {
AffinityIntent::Exact(set) => {
let v: Vec<usize> = set.into_iter().collect();
assert_eq!(v, vec![0, 1, 4, 5]);
}
other => panic!("expected Exact, got {other:?}"),
}
assert!(
spec.notes
.iter()
.any(|n| n.message().contains("AffinityHint::Exact")
&& n.message().contains("with resolved CPUs")),
"populated Exact must emit a resolved-collapse note for surface consistency: {:?}",
spec.notes,
);
}
#[test]
fn generate_spec_exact_empty_emits_unresolved_note() {
let mut cap = DebugCapture::default();
cap.fingerprint.affinity_hints = vec![AffinityHint::Exact { cpus: Vec::new() }];
let spec = generate_spec(&cap);
match &spec.config.affinity {
AffinityIntent::Exact(set) => {
assert!(
set.is_empty(),
"empty Exact must propagate through to AffinityIntent: {set:?}"
);
}
other => panic!("expected empty Exact, got {other:?}"),
}
assert!(
spec.notes
.iter()
.any(|n| n.message().contains("AffinityHint::Exact")
&& n.message().contains("no CPUs")),
"empty Exact must surface a hand-edit-required note: {:?}",
spec.notes,
);
assert!(
spec.notes.iter().any(|n| n
.message()
.contains("AffinityIntent::exact([<cpu_0>, <cpu_1>, ...])")),
"empty Exact note must include paste-ready Rust hand-edit target: {:?}",
spec.notes,
);
}
#[test]
fn generate_spec_bursty_passthrough() {
let mut cap = DebugCapture::default();
cap.fingerprint.work_type_hints = vec![WorkTypeHint::Bursty {
burst_duration: Duration::from_millis(5),
sleep_duration: Duration::from_millis(95),
}];
let spec = generate_spec(&cap);
match spec.config.work_type {
WorkType::Bursty {
burst_duration,
sleep_duration,
} => {
assert_eq!(burst_duration, Duration::from_millis(5));
assert_eq!(sleep_duration, Duration::from_millis(95));
}
other => panic!("expected Bursty, got {other:?}"),
}
}
#[test]
fn generate_spec_fifo_priority() {
let mut cap = DebugCapture::default();
cap.fingerprint.sched_policy_hints = vec![SchedPolicyHint::Fifo { priority: 50 }];
let spec = generate_spec(&cap);
match spec.config.sched_policy {
SchedPolicy::Fifo(prio) => assert_eq!(prio, 50),
other => panic!("expected Fifo, got {other:?}"),
}
}
#[test]
fn generate_spec_nice_applied() {
let mut cap = DebugCapture::default();
cap.fingerprint.sched_policy_hints = vec![SchedPolicyHint::Other { nice: 5 }];
let spec = generate_spec(&cap);
assert!(matches!(spec.config.sched_policy, SchedPolicy::Normal));
assert_eq!(spec.config.nice, 5);
}
#[test]
fn generate_spec_multiple_hints_first_wins() {
let mut cap = DebugCapture::default();
cap.fingerprint.work_type_hints = vec![WorkTypeHint::SpinWait, WorkTypeHint::IoSyncWrite];
let spec = generate_spec(&cap);
assert!(matches!(spec.config.work_type, WorkType::SpinWait));
assert!(
spec.notes
.iter()
.any(|n| n.message().contains("additional work-type hints"))
);
}
#[test]
fn generate_spec_maps_each_io_hint_directly() {
let mut cap = DebugCapture::default();
cap.fingerprint.work_type_hints = vec![WorkTypeHint::IoRandRead];
let spec = generate_spec(&cap);
assert!(
matches!(spec.config.work_type, WorkType::IoRandRead),
"IoRandRead hint must map to WorkType::IoRandRead, got {:?}",
spec.config.work_type,
);
let mut cap = DebugCapture::default();
cap.fingerprint.work_type_hints = vec![WorkTypeHint::IoConvoy];
let spec = generate_spec(&cap);
assert!(
matches!(spec.config.work_type, WorkType::IoConvoy),
"IoConvoy hint must map to WorkType::IoConvoy, got {:?}",
spec.config.work_type,
);
let mut cap = DebugCapture::default();
cap.fingerprint.work_type_hints = vec![WorkTypeHint::IoSyncWrite];
let spec = generate_spec(&cap);
assert!(
matches!(spec.config.work_type, WorkType::IoSyncWrite),
"IoSyncWrite hint must map to WorkType::IoSyncWrite, got {:?}",
spec.config.work_type,
);
}
#[test]
fn generate_spec_propagates_gaps() {
let mut cap = DebugCapture::default();
cap.fingerprint
.gaps
.push("affinity hint backed by 1 sample".into());
let spec = generate_spec(&cap);
assert!(
spec.notes
.iter()
.any(|n| n.message().contains("affinity hint backed by 1 sample"))
);
}
#[test]
fn generate_spec_llc_aligned_unresolved_emits_topology_aware() {
let mut cap = DebugCapture::default();
cap.fingerprint.affinity_hints = vec![AffinityHint::LlcAligned { cpus: Vec::new() }];
let spec = generate_spec(&cap);
assert!(matches!(spec.config.affinity, AffinityIntent::LlcAligned));
assert!(
spec.notes
.iter()
.any(|n| n.message().contains("AffinityHint::LlcAligned")
&& n.message().contains("without resolved CPUs")),
"unresolved LlcAligned must surface a topology-aware-fallback note: {:?}",
spec.notes,
);
assert!(
spec.notes.iter().any(|n| n
.message()
.contains("AffinityIntent::exact([<llc_cpu_0>, <llc_cpu_1>, ...])")),
"unresolved LlcAligned note must include paste-ready Rust hand-edit target: {:?}",
spec.notes,
);
}
#[test]
fn generate_spec_llc_aligned_resolved_emits_exact() {
let mut cap = DebugCapture::default();
cap.fingerprint.affinity_hints = vec![AffinityHint::LlcAligned {
cpus: vec![0, 1, 2, 3],
}];
let spec = generate_spec(&cap);
match &spec.config.affinity {
AffinityIntent::Exact(set) => {
let v: Vec<usize> = set.iter().copied().collect();
assert_eq!(
v,
vec![0, 1, 2, 3],
"resolved LlcAligned must collapse to Exact with the observed CPUs: got {v:?}",
);
}
other => panic!("expected Exact, got {other:?}"),
}
assert!(
spec.notes
.iter()
.any(|n| n.message().contains("AffinityHint::LlcAligned")
&& n.message().contains("with resolved CPUs")),
"resolved LlcAligned must surface a resolved-collapse note: {:?}",
spec.notes,
);
}
#[test]
fn generate_spec_single_cpu_resolved_emits_exact() {
let mut cap = DebugCapture::default();
cap.fingerprint.affinity_hints = vec![AffinityHint::SingleCpu { cpus: vec![7] }];
let spec = generate_spec(&cap);
match &spec.config.affinity {
AffinityIntent::Exact(set) => {
let v: Vec<usize> = set.iter().copied().collect();
assert_eq!(v, vec![7]);
}
other => panic!("expected Exact, got {other:?}"),
}
assert!(
spec.notes
.iter()
.any(|n| n.message().contains("AffinityHint::SingleCpu")
&& n.message().contains("with resolved CPUs")),
"resolved SingleCpu must surface a resolved-collapse note: {:?}",
spec.notes,
);
}
#[test]
fn generate_spec_single_cpu_unresolved_emits_topology_aware() {
let mut cap = DebugCapture::default();
cap.fingerprint.affinity_hints = vec![AffinityHint::SingleCpu { cpus: Vec::new() }];
let spec = generate_spec(&cap);
assert!(matches!(spec.config.affinity, AffinityIntent::SingleCpu));
assert!(
spec.notes
.iter()
.any(|n| n.message().contains("AffinityHint::SingleCpu")
&& n.message().contains("without resolved CPUs")),
);
assert!(
spec.notes
.iter()
.any(|n| n.message().contains("AffinityIntent::exact([<cpu>])")),
"unresolved SingleCpu note must include paste-ready Rust hand-edit target: {:?}",
spec.notes,
);
}
#[test]
fn generate_spec_cross_cgroup_resolved_emits_exact() {
let mut cap = DebugCapture::default();
cap.fingerprint.affinity_hints = vec![AffinityHint::CrossCgroup {
cpus: vec![2, 4, 6, 8],
}];
let spec = generate_spec(&cap);
match &spec.config.affinity {
AffinityIntent::Exact(set) => {
let v: Vec<usize> = set.iter().copied().collect();
assert_eq!(v, vec![2, 4, 6, 8]);
}
other => panic!("expected Exact, got {other:?}"),
}
assert!(
spec.notes
.iter()
.any(|n| n.message().contains("AffinityHint::CrossCgroup")
&& n.message().contains("with resolved CPUs")),
);
}
#[test]
fn generate_spec_cross_cgroup_unresolved_emits_topology_aware() {
let mut cap = DebugCapture::default();
cap.fingerprint.affinity_hints = vec![AffinityHint::CrossCgroup { cpus: Vec::new() }];
let spec = generate_spec(&cap);
assert!(matches!(spec.config.affinity, AffinityIntent::CrossCgroup));
assert!(
spec.notes
.iter()
.any(|n| n.message().contains("AffinityHint::CrossCgroup")
&& n.message().contains("without resolved CPUs")),
"unresolved CrossCgroup must surface a topology-aware-fallback note: {:?}",
spec.notes,
);
assert!(
spec.notes.iter().any(|n| n
.message()
.contains("AffinityIntent::exact([<cpu_0>, <cpu_1>, ...])")),
"unresolved CrossCgroup note must include paste-ready Rust hand-edit target: {:?}",
spec.notes,
);
}
#[test]
fn generate_spec_random_subset_resolved_emits_populated() {
let mut cap = DebugCapture::default();
cap.fingerprint.affinity_hints = vec![AffinityHint::RandomSubset {
from: vec![0, 1, 2, 3, 4, 5],
count: 3,
}];
let spec = generate_spec(&cap);
match &spec.config.affinity {
AffinityIntent::RandomSubset { from, count } => {
let v: Vec<usize> = from.iter().copied().collect();
assert_eq!(v, vec![0, 1, 2, 3, 4, 5]);
assert_eq!(*count, 3);
}
other => panic!("expected populated RandomSubset, got {other:?}"),
}
assert!(
spec.notes
.iter()
.any(|n| n.message().contains("AffinityHint::RandomSubset")
&& n.message().contains("with resolved pool")),
);
}
#[test]
fn generate_spec_random_subset_unresolved_emits_placeholder() {
let mut cap = DebugCapture::default();
cap.fingerprint.affinity_hints = vec![AffinityHint::RandomSubset {
from: Vec::new(),
count: 0,
}];
let spec = generate_spec(&cap);
match &spec.config.affinity {
AffinityIntent::RandomSubset { from, count } => {
assert!(
from.is_empty(),
"unresolved RandomSubset must emit empty pool"
);
assert_eq!(*count, 0);
}
other => panic!("expected placeholder RandomSubset, got {other:?}"),
}
assert!(
spec.notes
.iter()
.any(|n| n.message().contains("AffinityHint::RandomSubset")
&& n.message().contains("without a resolved pool")),
);
}
#[test]
fn generate_spec_random_subset_pool_without_count_is_placeholder() {
let mut cap = DebugCapture::default();
cap.fingerprint.affinity_hints = vec![AffinityHint::RandomSubset {
from: vec![0, 1, 2],
count: 0,
}];
let spec = generate_spec(&cap);
match &spec.config.affinity {
AffinityIntent::RandomSubset { from, count } => {
assert!(
from.is_empty(),
"(non_empty, 0) must drop pool to placeholder: got {from:?}",
);
assert_eq!(*count, 0);
}
other => panic!("expected placeholder RandomSubset, got {other:?}"),
}
assert!(
spec.notes
.iter()
.any(|n| n.message().contains("AffinityHint::RandomSubset")
&& n.message().contains("without a resolved pool")),
"(non_empty, 0) must surface unresolved-pool note: {:?}",
spec.notes,
);
}
#[test]
fn generate_spec_random_subset_count_without_pool_is_placeholder() {
let mut cap = DebugCapture::default();
cap.fingerprint.affinity_hints = vec![AffinityHint::RandomSubset {
from: Vec::new(),
count: 3,
}];
let spec = generate_spec(&cap);
match &spec.config.affinity {
AffinityIntent::RandomSubset { from, count } => {
assert!(from.is_empty());
assert_eq!(
*count, 0,
"([], non_zero) must drop count to placeholder: got {count}",
);
}
other => panic!("expected placeholder RandomSubset, got {other:?}"),
}
assert!(
spec.notes
.iter()
.any(|n| n.message().contains("AffinityHint::RandomSubset")
&& n.message().contains("without a resolved pool")),
"([], non_zero) must surface unresolved-pool note: {:?}",
spec.notes,
);
}
#[test]
fn generate_spec_random_subset_count_exceeds_pool_is_populated() {
let mut cap = DebugCapture::default();
cap.fingerprint.affinity_hints = vec![AffinityHint::RandomSubset {
from: vec![0, 1],
count: 10,
}];
let spec = generate_spec(&cap);
match &spec.config.affinity {
AffinityIntent::RandomSubset { from, count } => {
let v: Vec<usize> = from.iter().copied().collect();
assert_eq!(v, vec![0, 1]);
assert_eq!(
*count, 10,
"count > pool.len() must passthrough verbatim: got {count}",
);
}
other => panic!("expected populated RandomSubset, got {other:?}"),
}
assert!(
spec.notes
.iter()
.any(|n| n.message().contains("AffinityHint::RandomSubset")
&& n.message().contains("with resolved pool")),
"count > pool.len() must take the resolved path: {:?}",
spec.notes,
);
}
#[test]
fn generate_spec_smt_sibling_pair_resolved_emits_exact() {
let mut cap = DebugCapture::default();
cap.fingerprint.affinity_hints = vec![AffinityHint::SmtSiblingPair { cpus: vec![2, 3] }];
let spec = generate_spec(&cap);
match &spec.config.affinity {
AffinityIntent::Exact(set) => {
let v: Vec<usize> = set.iter().copied().collect();
assert_eq!(v, vec![2, 3]);
}
other => panic!("expected Exact, got {other:?}"),
}
assert!(
spec.notes
.iter()
.any(|n| n.message().contains("AffinityHint::SmtSiblingPair")
&& n.message().contains("with resolved CPUs")),
"resolved SmtSiblingPair must surface a resolved-collapse note: {:?}",
spec.notes,
);
}
#[test]
fn generate_spec_smt_sibling_pair_unresolved_emits_topology_aware() {
let mut cap = DebugCapture::default();
cap.fingerprint.affinity_hints = vec![AffinityHint::SmtSiblingPair { cpus: Vec::new() }];
let spec = generate_spec(&cap);
assert!(matches!(
spec.config.affinity,
AffinityIntent::SmtSiblingPair
));
assert!(
spec.notes
.iter()
.any(|n| n.message().contains("AffinityHint::SmtSiblingPair")
&& n.message().contains("without resolved CPUs")),
"unresolved SmtSiblingPair must surface a topology-aware-fallback note: {:?}",
spec.notes,
);
assert!(
spec.notes.iter().any(|n| n
.message()
.contains("AffinityIntent::exact([<sibling_a>, <sibling_b>])")),
"unresolved SmtSiblingPair note must include paste-ready Rust hand-edit target: {:?}",
spec.notes,
);
}
#[test]
fn render_run_file_source_basic_shape() {
let mut cap = DebugCapture::default();
cap.fingerprint.workload_groups = vec![WorkloadGroupHint {
cgroup_path: "/test".into(),
thread_count: 4,
cpu_time_fraction: 0.0,
wakeups_per_sec: 0.0,
}];
cap.fingerprint.work_type_hints = vec![WorkTypeHint::SpinWait];
let spec = generate_spec(&cap);
assert!(
spec.notes.is_empty(),
"basic-shape fingerprint must produce no notes; got {:?}",
spec.notes,
);
let src = render_run_file_source(&spec, "regression_repro");
assert!(src.contains("use ktstr::workload::*;"));
assert!(src.contains("pub fn regression_repro"));
assert!(src.contains(".workers(4)"));
assert!(src.contains(".work_type(WorkType::SpinWait)"));
assert!(!src.contains("Generator notes:"));
}
#[test]
fn render_run_file_source_renders_notes() {
let mut cap = DebugCapture::default();
cap.fingerprint.gaps = vec!["test gap from fingerprint".into()];
let spec = generate_spec(&cap);
assert!(
!spec.notes.is_empty(),
"fingerprint gap must propagate to spec.notes",
);
let src = render_run_file_source(&spec, "with_notes");
assert!(src.contains("Generator notes:"));
assert!(src.contains("// - fingerprint gap: test gap from fingerprint"));
}
#[test]
fn render_ktstr_test_source_has_attribute() {
let cap = DebugCapture::default();
let spec = generate_spec(&cap);
let src = render_ktstr_test_source(&spec, "auto_repro");
assert!(src.contains("#[ktstr::ktstr_test]"));
assert!(src.contains("pub fn auto_repro"));
}
#[test]
fn render_run_file_source_emits_nice_zero_explicitly() {
let cap = DebugCapture::default();
let spec = generate_spec(&cap);
assert_eq!(spec.config.nice, 0);
let src = render_run_file_source(&spec, "explicit_nice");
assert!(
src.contains(".nice(0)"),
"rendered source must always emit `.nice(0)` (no silent \
inheritance of upstream defaults): {src}",
);
}
#[test]
fn render_ktstr_test_source_template_name_substring_in_body() {
let cap = DebugCapture::default();
let spec = generate_spec(&cap);
let src = render_ktstr_test_source(&spec, "default");
let attribute = "#[ktstr::ktstr_test]";
let attribute_count = src.matches(attribute).count();
assert_eq!(
attribute_count, 1,
"attribute must be inserted exactly once, got {attribute_count} \
occurrences in: {src}",
);
assert!(
src.contains("WorkloadConfig::default()"),
"WorkloadConfig::default() must remain intact (substring \
replace must not match the `default()` body call): {src}",
);
assert!(
src.contains("#[ktstr::ktstr_test]\npub fn default"),
"rewritten `pub fn default` must carry the attribute: {src}",
);
}
#[test]
fn is_runnable_resolved_random_subset() {
let mut cap = DebugCapture::default();
cap.fingerprint.affinity_hints = vec![AffinityHint::RandomSubset {
from: vec![0, 1, 2],
count: 2,
}];
let spec = generate_spec(&cap);
assert!(
spec.is_runnable(),
"resolved RandomSubset must be runnable; notes: {:?}",
spec.notes,
);
assert_eq!(spec.unresolved_count(), 0);
}
#[test]
fn is_runnable_unresolved_single_cpu() {
let mut cap = DebugCapture::default();
cap.fingerprint.affinity_hints = vec![AffinityHint::SingleCpu { cpus: Vec::new() }];
let spec = generate_spec(&cap);
assert!(
!spec.is_runnable(),
"unresolved SingleCpu must NOT be runnable; notes: {:?}",
spec.notes,
);
assert_eq!(spec.unresolved_count(), 1);
}
#[test]
fn is_runnable_empty_exact() {
let mut cap = DebugCapture::default();
cap.fingerprint.affinity_hints = vec![AffinityHint::Exact { cpus: Vec::new() }];
let spec = generate_spec(&cap);
assert!(
!spec.is_runnable(),
"empty Exact must NOT be runnable; notes: {:?}",
spec.notes,
);
assert_eq!(spec.unresolved_count(), 1);
}
#[test]
fn is_runnable_unresolved_random_subset() {
let mut cap = DebugCapture::default();
cap.fingerprint.affinity_hints = vec![AffinityHint::RandomSubset {
from: Vec::new(),
count: 0,
}];
let spec = generate_spec(&cap);
assert!(
!spec.is_runnable(),
"unresolved RandomSubset must NOT be runnable; notes: {:?}",
spec.notes,
);
assert_eq!(spec.unresolved_count(), 1);
}
#[test]
fn is_runnable_empty_fingerprint() {
let cap = DebugCapture::default();
let spec = generate_spec(&cap);
assert!(
spec.is_runnable(),
"empty fingerprint must be runnable (default Inherit); notes: {:?}",
spec.notes,
);
assert_eq!(spec.unresolved_count(), 0);
}
#[test]
fn render_run_file_source_includes_cgroup_hints_as_comments() {
let mut cap = DebugCapture::default();
cap.fingerprint.cgroup_hints = vec![
CgroupHint {
path: "/system.slice/foo.service".into(),
cpu_weight: Some(200),
memory_max_bytes: Some(8 * 1024 * 1024 * 1024),
cpuset_cpus: vec![0, 1, 2, 3],
cpu_max_quota_us: None,
},
CgroupHint {
path: "/system.slice/bar.service".into(),
cpu_weight: Some(100),
memory_max_bytes: None,
cpuset_cpus: vec![4, 5],
cpu_max_quota_us: Some(75_000),
},
];
let spec = generate_spec(&cap);
let src = render_run_file_source(&spec, "with_cgroup");
assert!(src.contains("Cgroup hints"));
assert!(src.contains("/system.slice/foo.service"));
assert!(src.contains("weight=Some(200)"));
assert!(
src.contains("cpu_max_quota_us=None"),
"unlimited cpu_max_quota_us must render as None: {src}",
);
assert!(
src.contains("/system.slice/bar.service"),
"second cgroup hint must render: {src}",
);
assert!(
src.contains("cpu_max_quota_us=Some(75000)"),
"bounded cpu_max_quota_us must render its value: {src}",
);
}
#[test]
fn render_run_file_source_e2e_smoke() {
let mut cap = DebugCapture::default();
cap.fingerprint.workload_groups = vec![WorkloadGroupHint {
cgroup_path: "/system.slice/foo.service".into(),
thread_count: 16,
cpu_time_fraction: 0.65,
wakeups_per_sec: 850.0,
}];
cap.fingerprint.affinity_hints = vec![
AffinityHint::SmtSiblingPair { cpus: vec![4, 5] },
AffinityHint::Exact {
cpus: vec![0, 1, 2, 3],
},
];
cap.fingerprint.work_type_hints = vec![WorkTypeHint::Bursty {
burst_duration: Duration::from_millis(7),
sleep_duration: Duration::from_millis(43),
}];
cap.fingerprint.cgroup_hints = vec![CgroupHint {
path: "/system.slice/foo.service".into(),
cpu_weight: Some(150),
memory_max_bytes: Some(4 * 1024 * 1024 * 1024),
cpuset_cpus: vec![4, 5],
cpu_max_quota_us: Some(50_000),
}];
cap.fingerprint.sched_policy_hints = vec![SchedPolicyHint::Fifo { priority: 60 }];
cap.fingerprint.gaps = vec!["sample window had 2 dropouts".into()];
let spec1 = generate_spec(&cap);
let src1 = render_run_file_source(&spec1, "e2e_repro");
let spec2 = generate_spec(&cap);
let src2 = render_run_file_source(&spec2, "e2e_repro");
assert_eq!(
src1, src2,
"render_run_file_source must be deterministic for the same capture",
);
assert!(src1.contains("use ktstr::workload::*;"));
assert!(src1.contains("use std::collections::BTreeSet;"));
assert!(src1.contains("use std::time::Duration;"));
assert!(src1.contains("pub fn e2e_repro"));
assert!(
src1.contains(".workers(16)"),
"thread_count=16 must surface as .workers(16): {src1}",
);
assert!(
src1.contains(".affinity(AffinityIntent::Exact"),
"first affinity hint (SmtSiblingPair) must collapse to Exact: {src1}",
);
assert!(
src1.contains("BTreeSet::from([4, 5])"),
"Exact pool must contain the SmtSiblingPair CPUs: {src1}",
);
assert!(
src1.contains(".work_type(WorkType::Bursty"),
"Bursty work-type hint must reach the builder: {src1}",
);
assert!(
src1.contains("Duration::from_millis(7)") && src1.contains("Duration::from_millis(43)"),
"Bursty durations must surface in the rendered call: {src1}",
);
assert!(
src1.contains(".sched_policy(SchedPolicy::Fifo(60))"),
"Fifo priority must surface: {src1}",
);
assert!(
src1.contains("Cgroup hints"),
"cgroup hints must render as comments: {src1}",
);
assert!(
src1.contains("/system.slice/foo.service"),
"cgroup path must appear in the rendered comments: {src1}",
);
assert!(
src1.contains("cpu_max_quota_us=Some(50000)"),
"cpu_max_quota_us must render alongside the other cgroup \
fields (weight/mem_max/cpuset): {src1}",
);
assert!(
src1.contains("Generator notes:"),
"non-empty notes must trigger the comment block: {src1}",
);
assert!(
src1.contains("fingerprint gap: sample window had 2 dropouts"),
"fingerprint gap must propagate verbatim: {src1}",
);
assert!(
src1.contains("AffinityHint::SmtSiblingPair"),
"resolved-collapse note must cite the original variant: {src1}",
);
assert!(
src1.contains("additional affinity hints not modeled"),
"second affinity hint must surface as an additional-hints note: {src1}",
);
assert!(
!src1.contains("/* TODO:"),
"implemented work-type variants must not render any TODO placeholder: {src1}",
);
}
#[test]
fn render_affinity_all_variants() {
assert_eq!(
render_affinity(&AffinityIntent::Inherit),
"AffinityIntent::Inherit"
);
assert_eq!(
render_affinity(&AffinityIntent::SingleCpu),
"AffinityIntent::SingleCpu"
);
assert_eq!(
render_affinity(&AffinityIntent::LlcAligned),
"AffinityIntent::LlcAligned"
);
assert_eq!(
render_affinity(&AffinityIntent::CrossCgroup),
"AffinityIntent::CrossCgroup"
);
assert_eq!(
render_affinity(&AffinityIntent::SmtSiblingPair),
"AffinityIntent::SmtSiblingPair"
);
let random = AffinityIntent::RandomSubset {
from: BTreeSet::from([0usize, 1, 2, 3]),
count: 2,
};
assert_eq!(
render_affinity(&random),
"AffinityIntent::RandomSubset { from: BTreeSet::from([0, 1, 2, 3]), count: 2 }"
);
let exact = AffinityIntent::Exact(BTreeSet::from([0usize, 1, 2]));
assert_eq!(
render_affinity(&exact),
"AffinityIntent::Exact(BTreeSet::from([0, 1, 2]))"
);
}
#[test]
fn is_unmapped_work_type_split_matches_render() {
use crate::workload::{Phase, WorkerReport};
use std::sync::atomic::AtomicBool;
fn stub_custom_fn(_: &AtomicBool) -> WorkerReport {
WorkerReport::default()
}
let mut all_variants: Vec<WorkType> = WorkType::ALL_NAMES
.iter()
.filter_map(|n| WorkType::from_name(n))
.collect();
all_variants.push(WorkType::Sequence {
first: Phase::Spin(Duration::from_millis(1)),
rest: Vec::new(),
});
all_variants.push(WorkType::Custom {
name: "stub-custom".into(),
run: stub_custom_fn,
});
assert_eq!(
all_variants.len(),
WorkType::ALL_NAMES.len(),
"every WorkType variant must be constructed; \
ALL_NAMES = {:?}, constructed {} variants",
WorkType::ALL_NAMES,
all_variants.len(),
);
let mut runnable_count = 0usize;
let mut unmapped_count = 0usize;
for w in &all_variants {
let rendered = render_work_type(w);
if is_unmapped_work_type(w) {
unmapped_count += 1;
assert!(
rendered.contains("/* TODO:"),
"{w:?} classifies as unmapped — render_work_type \
must dispatch through render_work_type_todo: {rendered}",
);
} else {
runnable_count += 1;
assert!(
!rendered.contains("/* TODO:"),
"{w:?} classifies as runnable — render_work_type \
must produce a builder call without a TODO \
placeholder: {rendered}",
);
}
}
assert!(runnable_count > 0, "runnable partition must not be empty",);
assert!(unmapped_count > 0, "unmapped partition must not be empty",);
}
#[test]
fn is_runnable_unmapped_work_type_via_direct_config() {
let mut spec = ReproducerSpec::default();
spec.config.work_type = WorkType::CacheYield {
size_kb: 256,
stride: 64,
};
assert!(
spec.notes.is_empty(),
"fixture must not add notes; got {:?}",
spec.notes,
);
assert!(
!spec.is_runnable(),
"spec with unmapped work_type must NOT be runnable, even \
without projection notes; config.work_type: {:?}",
spec.config.work_type,
);
assert_eq!(
spec.unresolved_count(),
0,
"unresolved_count covers typed unresolved notes only; got {}",
spec.unresolved_count(),
);
}
#[test]
fn record_work_type_emits_note_for_unmapped_projection() {
let mut spec = ReproducerSpec::default();
record_work_type(WorkType::ForkExit, &mut spec);
assert!(
matches!(spec.config.work_type, WorkType::ForkExit),
"record_work_type must assign the variant to spec.config.work_type",
);
assert!(
spec.notes
.iter()
.any(|n| matches!(n, ReproducerNote::UnmappedWorkType(_))),
"unmapped projection must push a ReproducerNote::UnmappedWorkType: {:?}",
spec.notes,
);
assert_eq!(
spec.unresolved_count(),
1,
"unresolved_count must include the UnmappedWorkType note",
);
assert!(
!spec.is_runnable(),
"spec with TODO note + unmapped work_type must be NOT runnable",
);
}
#[test]
fn record_work_type_does_not_emit_note_for_runnable_projection() {
let mut spec = ReproducerSpec::default();
record_work_type(WorkType::SpinWait, &mut spec);
assert!(
spec.notes.is_empty(),
"runnable projection must NOT push an unmapped note: {:?}",
spec.notes,
);
assert_eq!(spec.unresolved_count(), 0);
}
#[test]
fn is_runnable_combines_work_type_and_affinity_signals() {
let mut cap = DebugCapture::default();
cap.fingerprint.affinity_hints = vec![AffinityHint::Exact { cpus: Vec::new() }];
let mut spec = generate_spec(&cap);
record_work_type(WorkType::ForkExit, &mut spec);
assert!(!spec.is_runnable());
assert_eq!(
spec.unresolved_count(),
2,
"expected 2 unresolved notes (1 affinity + 1 work-type), got {}: {:?}",
spec.unresolved_count(),
spec.notes,
);
}
#[test]
fn reproducer_note_wire_format_is_snake_case() {
let cases: &[(ReproducerNote, &str)] = &[
(
ReproducerNote::Informational("info".into()),
"informational",
),
(ReproducerNote::Resolved("res".into()), "resolved"),
(
ReproducerNote::UnresolvedAffinity("ua".into()),
"unresolved_affinity",
),
(
ReproducerNote::UnmappedWorkType("uwt".into()),
"unmapped_work_type",
),
];
for (note, expected_kind) in cases {
let json = serde_json::to_string(note)
.expect("ReproducerNote must serialize via the derive impl");
let kind_pin = format!(r#""kind":"{expected_kind}""#);
assert!(
json.contains(&kind_pin),
"wire format must encode kind={expected_kind:?} (snake_case) — \
a regression that drops `#[serde(rename_all = \"snake_case\")]` \
from the enum would revert to PascalCase. note={note:?}, json={json}",
);
let round_tripped: ReproducerNote = serde_json::from_str(&json)
.expect("ReproducerNote must deserialize from its own serialized form");
assert_eq!(
std::mem::discriminant(note),
std::mem::discriminant(&round_tripped),
"roundtrip must preserve the variant — asymmetric rename \
between Serialize and Deserialize would surface here. \
sent={note:?}, got={round_tripped:?}",
);
assert_eq!(
note.message(),
round_tripped.message(),
"roundtrip must preserve the message payload",
);
}
}
#[test]
fn render_run_file_source_compiles_via_rustc() {
let mut cap = DebugCapture::default();
cap.fingerprint.workload_groups = vec![WorkloadGroupHint {
cgroup_path: "/system.slice/foo.service".into(),
thread_count: 16,
cpu_time_fraction: 0.65,
wakeups_per_sec: 850.0,
}];
cap.fingerprint.affinity_hints = vec![AffinityHint::Exact {
cpus: vec![0, 1, 4, 5],
}];
cap.fingerprint.work_type_hints = vec![WorkTypeHint::Bursty {
burst_duration: Duration::from_millis(7),
sleep_duration: Duration::from_millis(43),
}];
cap.fingerprint.cgroup_hints = vec![CgroupHint {
path: "/system.slice/foo.service".into(),
cpu_weight: Some(150),
memory_max_bytes: Some(4 * 1024 * 1024 * 1024),
cpuset_cpus: vec![0, 1, 4, 5],
cpu_max_quota_us: Some(50_000),
}];
cap.fingerprint.sched_policy_hints = vec![SchedPolicyHint::Deadline {
runtime_ns: 1_000_000,
deadline_ns: 5_000_000,
period_ns: 10_000_000,
}];
cap.fingerprint.gaps = vec!["sample window had 2 dropouts".into()];
let spec = generate_spec(&cap);
let rendered = render_run_file_source(&spec, "compile_check_repro");
let stub = r#"
#[allow(dead_code, unused_variables, unused_imports)]
mod ktstr { pub mod workload {
use std::collections::BTreeSet;
use std::time::Duration;
pub struct WorkloadConfig;
impl WorkloadConfig {
pub fn default() -> Self { Self }
pub fn workers(self, _: usize) -> Self { self }
pub fn affinity(self, _: AffinityIntent) -> Self { self }
pub fn work_type(self, _: WorkType) -> Self { self }
pub fn sched_policy(self, _: SchedPolicy) -> Self { self }
pub fn nice(self, _: i32) -> Self { self }
}
pub enum AffinityIntent {
Inherit,
SingleCpu,
LlcAligned,
CrossCgroup,
SmtSiblingPair,
RandomSubset { from: BTreeSet<usize>, count: usize },
Exact(BTreeSet<usize>),
}
pub enum WorkType {
SpinWait,
YieldHeavy,
Mixed,
IoSyncWrite,
IoRandRead,
IoConvoy,
Bursty { burst_duration: Duration, sleep_duration: Duration },
PipeIo { burst_iters: u64 },
FutexPingPong { spin_iters: u64 },
CachePressure { size_kb: usize, stride: usize },
}
pub enum SchedPolicy {
Normal,
Batch,
Idle,
Fifo(u32),
RoundRobin(u32),
Deadline { runtime: Duration, deadline: Duration, period: Duration },
}
}}
"#;
let combined = format!("{stub}\n{rendered}");
use std::io::Write as _;
let mut tmp = tempfile::Builder::new()
.prefix("ktstr_reproducer_compile_check_")
.suffix(".rs")
.tempfile()
.expect("create tempfile for rendered source");
tmp.write_all(combined.as_bytes())
.expect("write rendered source");
tmp.flush().expect("flush rendered source");
let out_dir = tempfile::TempDir::new().expect("rustc out tempdir");
let rustc = std::env::var_os("RUSTC").unwrap_or_else(|| "rustc".into());
let output = std::process::Command::new(&rustc)
.arg("--edition")
.arg("2021")
.arg("--crate-type")
.arg("lib")
.arg("--out-dir")
.arg(out_dir.path())
.arg(tmp.path())
.output()
.unwrap_or_else(|e| {
panic!(
"render_run_file_source_compiles_via_rustc requires rustc \
(resolved via $RUSTC, then $PATH) — failed to spawn {rustc:?}: {e}. \
Cargo sets $RUSTC for cargo-test / cargo-nextest invocations; if \
you are running this test outside of cargo, ensure rustc is on \
$PATH or set $RUSTC explicitly. The test does NOT silently skip \
when rustc is missing — silent-skip would falsely report green \
when the rendered-source compile-check never ran.",
)
});
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
panic!(
"rustc rejected rendered source\n\
---- rustc stderr ----\n\
{stderr}\n\
---- combined source ----\n\
{combined}",
);
}
}
}