ktstr 0.4.2

Test harness for Linux process schedulers
# Custom Scenarios

For dynamic scenarios (cgroup creation/removal, cpuset changes), prefer
the [ops/steps system](../concepts/ops.md) over raw `Action::Custom`.
See [Write a Dynamic Scenario](../recipes/dynamic-scenario.md) for
ops-based examples.

Use `Action::Custom` only when you need logic that the ops system
cannot express.

## Writing a custom scenario

```rust,ignore
use ktstr::prelude::*;
use ktstr::scenario::*;

fn my_custom_scenario(ctx: &Ctx) -> Result<AssertResult> {
    let wl = dfl_wl(ctx);
    let (handles, _guard) = setup_cgroups(ctx, 2, &wl)?;

    // Custom logic: resize cpusets, move workers, etc.
    std::thread::sleep(ctx.duration);

    Ok(collect_all(handles, &ctx.assert))
}
```

## Helper functions

**`setup_cgroups(ctx, n, wl)`** -- creates N cgroups, spawns workers in
each, starts them. Returns `Result<(Vec<WorkloadHandle>, CgroupGroup<'a>)>`. The
`CgroupGroup` is an RAII guard that removes cgroups on drop.

> **Warning:** `let _ = CgroupGroup::new(...)` drops immediately -- the
> guard is destroyed at the end of the statement, not the end of the
> scope. Always bind to a named variable (`let _guard = ...`) to keep
> cgroups alive for the duration of the test.

**`collect_all(handles, checks)`** -- stops all workers, collects reports,
runs `checks.assert_cgroup()` when worker-level checks are configured,
otherwise falls back to `assert_not_starved()`. Merges results: if any
worker group fails, the overall result fails.

**`dfl_wl(ctx)`** -- creates a `WorkloadConfig` with
`ctx.workers_per_cgroup` workers and default settings.

**`spawn_diverse(ctx, cgroup_names)`** -- spawns different work types
(CpuSpin, Bursty, IoSync, Mixed, YieldHeavy) across cgroups.

## The Ctx struct

Custom scenarios receive a `Ctx` reference:

```rust,ignore
pub struct Ctx<'a> {
    pub cgroups: &'a CgroupManager,
    pub topo: &'a TestTopology,
    pub duration: Duration,
    pub workers_per_cgroup: usize,
    pub sched_pid: u32,
    pub settle: Duration,
    pub work_type_override: Option<WorkType>,
    pub assert: Assert,
    pub wait_for_map_write: bool,
}
```

**`cgroups`** -- create/remove cgroups, set cpusets, move tasks.
`move_task(name, tid)` moves a single task; `move_tasks(name, &tids)`
moves all tasks in a slice (calls `move_task` per TID).

**`topo`** -- query CPU topology (LLCs, NUMA nodes, total CPUs).
Key methods:

- `all_cpus() -> &[usize]` -- all CPU IDs, sorted.
- `all_cpuset() -> BTreeSet<usize>` -- all CPU IDs as a set.
- `usable_cpus() -> &[usize]` -- all CPUs except the last (reserved
  for root cgroup) when topology has >2 CPUs.
- `usable_cpuset() -> BTreeSet<usize>` -- usable CPUs as a set.
- `split_by_llc() -> Vec<BTreeSet<usize>>` -- one BTreeSet per LLC.
- `num_llcs()`, `total_cpus()`, `num_numa_nodes()` -- counts.
- `cpus_in_llc(idx) -> &[usize]` -- CPUs in LLC at index.
- `llc_aligned_cpuset(idx) -> BTreeSet<usize>` -- same as
  `cpus_in_llc` but returns a set.

**`sched_pid`** -- scheduler process ID for liveness checks.

**`settle`** -- time to wait after cgroup creation for the scheduler
to stabilize.

## Verification in custom scenarios

Use `Assert` for both direct report checking and ops-based scenarios.
Call `assert.assert_cgroup(reports, cpuset)` for manual report
collection, or use `execute_steps_with()` for ops-based scenarios. See
[Verification](../concepts/verification.md#worker-checks-via-assert).

## Registering a custom scenario (ktstr contributors only)

This section applies to contributing scenarios to ktstr's internal
catalog. External test suites call scenario functions directly from
`#[ktstr_test]` -- no registration needed.

Add it to `all_scenarios()` in `src/scenario/catalog.rs`:

```rust,ignore
Scenario {
    name: "my_scenario",
    category: "dynamic",
    description: "Test dynamic cgroup resizing",
    required_flags: &[],
    excluded_flags: &[],
    num_cgroups: 0,
    cpuset_mode: CpusetMode::None,
    cgroup_works: vec![],
    action: Action::Custom(my_custom_scenario),
}
```