use anyhow::Result;
use ktstr::assert::AssertResult;
use ktstr::ktstr_test;
use ktstr::prelude::{SampleSeries, VmResult};
use ktstr::scenario::Ctx;
use ktstr::test_support::{Scheduler, SchedulerSpec};
const PRIMARY_SCHED: Scheduler =
Scheduler::named("lifecycle_primary").binary(SchedulerSpec::Discover("scx-ktstr"));
const STAGED_ALT_SCHED: Scheduler =
Scheduler::named("lifecycle_alt").binary(SchedulerSpec::Discover("scx-ktstr"));
const COLD_START_BOOT: Scheduler = Scheduler::named("cold_start_boot").binary(SchedulerSpec::Eevdf);
const COLD_START_ALT_SCHED: Scheduler =
Scheduler::named("cold_start_alt").binary(SchedulerSpec::Discover("scx-ktstr"));
const STALL_AFTER_1S_SCHED: Scheduler = Scheduler::named("stall_after_1s")
.binary(SchedulerSpec::Discover("scx-ktstr"))
.sched_args(&["--stall-after", "1"]);
fn assert_post_op_dispatch(result: &VmResult) -> Result<()> {
let series = SampleSeries::from_drained_typed(
result.snapshot_bridge.drain_ordered_with_stats(),
result.monitor.clone(),
)
.periodic_only();
anyhow::ensure!(
!series.is_empty(),
"no periodic samples on the bridge — the freeze coordinator never \
fired (periodic_target={}, periodic_fired={}); cannot prove the \
scheduler dispatched after the lifecycle op",
result.periodic_target,
result.periodic_fired,
);
let bpf_dispatched = series.bpf("nr_dispatched", |snap| snap.var("nr_dispatched").as_u64());
let any_progress = bpf_dispatched
.iter_full()
.any(|(_, _, slot)| matches!(slot, Ok(v) if *v > 0));
anyhow::ensure!(
any_progress,
"scx-ktstr nr_dispatched read 0 across every periodic sample — the \
scheduler bound to sched_ext (the lifecycle op succeeded) but never \
ran its dispatch path. Bind-without-dispatch regression: the post-op \
scheduler attached but isn't scheduling.",
);
Ok(())
}
#[ktstr_test(
scheduler = PRIMARY_SCHED,
staged_schedulers = [STAGED_ALT_SCHED],
llcs = 1,
cores = 2,
threads = 1,
memory_mib = 512,
duration_s = 5,
cleanup_budget_ms = 5000,
num_snapshots = 3,
post_vm = assert_post_op_dispatch,
)]
fn scheduler_replace_mid_experiment_swaps_via_staged_pack(ctx: &Ctx) -> Result<AssertResult> {
use ktstr::scenario::ops::{HoldSpec, Op, Step, execute_steps};
let steps = vec![
Step::new(
vec![],
HoldSpec::fixed(std::time::Duration::from_millis(500)),
),
Step::new(
vec![Op::replace_scheduler(&STAGED_ALT_SCHED)],
HoldSpec::fixed(std::time::Duration::from_millis(500)),
),
Step::new(
vec![],
HoldSpec::fixed(std::time::Duration::from_millis(500)),
),
];
execute_steps(ctx, steps)
}
#[ktstr_test(
scheduler = COLD_START_BOOT,
staged_schedulers = [COLD_START_ALT_SCHED],
llcs = 1,
cores = 2,
threads = 1,
memory_mib = 512,
duration_s = 5,
cleanup_budget_ms = 5000,
num_snapshots = 3,
post_vm = assert_post_op_dispatch,
)]
fn scheduler_attach_from_cold_start_succeeds(ctx: &Ctx) -> Result<AssertResult> {
use ktstr::scenario::ops::{HoldSpec, Op, Step, execute_steps};
let steps = vec![
Step::new(
vec![],
HoldSpec::fixed(std::time::Duration::from_millis(500)),
),
Step::new(
vec![Op::attach_scheduler(&COLD_START_ALT_SCHED)],
HoldSpec::fixed(std::time::Duration::from_millis(500)),
),
Step::new(
vec![],
HoldSpec::fixed(std::time::Duration::from_millis(500)),
),
];
execute_steps(ctx, steps)
}
#[ktstr_test(
scheduler = STALL_AFTER_1S_SCHED,
llcs = 1,
cores = 2,
threads = 1,
memory_mib = 512,
duration_s = 20,
watchdog_timeout_s = 30,
auto_repro = false,
expect_err = true,
cleanup_budget_ms = 5000,
)]
fn dispatch_hold_truncates_when_scheduler_dies_midstep(ctx: &Ctx) -> Result<AssertResult> {
use ktstr::scenario::ops::{HoldSpec, Step, execute_steps};
let t0 = std::time::Instant::now();
let steps = vec![Step::new(
vec![],
HoldSpec::fixed(std::time::Duration::from_secs(15)),
)];
let result = execute_steps(ctx, steps);
let elapsed = t0.elapsed();
let ceiling = std::time::Duration::from_secs(12);
if elapsed >= ceiling {
return Ok(AssertResult::fail_msg(format!(
"dispatch loop did not truncate after scheduler-stall death: \
configured hold = 15 s, scheduler stalled at t≈1 s + watchdog, \
actual elapsed = {elapsed:?} (≥ {ceiling:?} ceiling). \
hold_or_sched_died's mid-hold scheduler-death observation is \
broken — the per-step hold ran to completion despite the \
scheduler dying. Check pidfd_wait_exit + the dispatch loop's \
death-observation branch in src/scenario/ops/mod.rs."
)));
}
result
}
const BROKEN_BINARY_SCHED: Scheduler =
Scheduler::named("broken_binary").binary(SchedulerSpec::Path("/bin/false"));
#[ktstr_test(
scheduler = PRIMARY_SCHED,
staged_schedulers = [BROKEN_BINARY_SCHED],
llcs = 1,
cores = 2,
threads = 1,
memory_mib = 512,
duration_s = 5,
auto_repro = false,
expect_err = true,
cleanup_budget_ms = 5000,
)]
fn replace_with_broken_binary_surfaces_startup_died(ctx: &Ctx) -> Result<AssertResult> {
use ktstr::scenario::ops::{HoldSpec, Op, Step, execute_steps};
let steps = vec![
Step::new(
vec![],
HoldSpec::fixed(std::time::Duration::from_millis(500)),
),
Step::new(
vec![Op::replace_scheduler(&BROKEN_BINARY_SCHED)],
HoldSpec::fixed(std::time::Duration::from_millis(500)),
),
];
let result = execute_steps(ctx, steps);
if let Ok(ref ok_result) = result
&& ok_result.is_pass()
{
return Ok(AssertResult::fail_msg(
"Op::ReplaceScheduler with /bin/false as the scheduler binary DID \
NOT FAIL — try_spawn_scheduler's StartupDied path is no longer being \
exercised (poll_startup is missing the immediate exit, or the spawn \
helper started swallowing exit codes)."
.to_string(),
));
}
result
}
#[ktstr_test(
scheduler = PRIMARY_SCHED,
llcs = 1,
cores = 2,
threads = 1,
memory_mib = 512,
duration_s = 5,
cleanup_budget_ms = 5000,
num_snapshots = 3,
post_vm = assert_post_op_dispatch,
)]
fn scheduler_restart_mid_experiment_reattaches_cleanly(ctx: &Ctx) -> Result<AssertResult> {
use ktstr::scenario::ops::{HoldSpec, Op, Step, execute_steps};
let steps = vec![
Step::new(
vec![],
HoldSpec::fixed(std::time::Duration::from_millis(500)),
),
Step::new(
vec![Op::restart_scheduler()],
HoldSpec::fixed(std::time::Duration::from_millis(500)),
),
Step::new(
vec![],
HoldSpec::fixed(std::time::Duration::from_millis(500)),
),
];
execute_steps(ctx, steps)
}