ktstr 0.3.1

Test harness for Linux process schedulers
# Scheduler Definitions

A `Scheduler` tells the test framework how to launch and configure the
scheduler under test.

## The Scheduler type

```rust,ignore
pub struct Scheduler {
    pub name: &'static str,
    pub binary: SchedulerSpec,
    pub flags: &'static [&'static FlagDecl],
    pub sysctls: &'static [(&'static str, &'static str)],
    pub kargs: &'static [&'static str],
    pub assert: Assert,
    pub cgroup_parent: Option<&'static str>,
    pub sched_args: &'static [&'static str],
    pub topology: Topology,
    pub constraints: TopologyConstraints,
}
```

## SchedulerSpec

How to find the scheduler binary:

```rust,ignore
pub enum SchedulerSpec {
    None,                // No binary -- use EEVDF (kernel default)
    Name(&'static str),  // Auto-discover by name
    Path(&'static str),  // Explicit path
    KernelBuiltin {      // Kernel-built scheduler (no binary)
        enable: &'static [&'static str],
        disable: &'static [&'static str],
    },
}
```

`KernelBuiltin` is for schedulers compiled into the kernel (e.g.
BPF-less sched_ext or debugfs-tuned variants). The `enable` commands
run in the guest to activate the scheduler; `disable` commands run
to deactivate it. No binary is injected into the VM.

`SchedulerSpec::has_active_scheduling()` returns `true` for all
variants except `None`. When `true`, the framework runs monitor
threshold evaluation after the scenario and enables auto-repro on
crash.

## Built-in: EEVDF

`Scheduler::EEVDF` runs tests without a sched_ext scheduler, using the
kernel's default EEVDF scheduler. Its binary is `SchedulerSpec::None`.

## Defining a scheduler

Use `#[derive(Scheduler)]` on an enum whose variants are the
scheduler's flags:

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

#[derive(Scheduler)]
#[scheduler(
    name = "my_sched",
    binary = "scx_my_sched",
    topology(1, 2, 4, 1),
    sched_args = ["--exit-dump-len", "1048576"]
)]
#[allow(dead_code)]
enum MySchedFlag {
    #[flag(args = ["--enable-llc"])]
    Llc,
    #[flag(args = ["--enable-stealing"], requires = [Llc])]
    Steal,
}
```

This generates:

- `static` `FlagDecl` entries for each variant
- A `&[&FlagDecl]` flags array
- `const MY_SCHED: Scheduler` with all builder methods applied
- `impl MySchedFlag` with `&'static str` constants for each variant's
  kebab-case name (e.g. `MySchedFlag::LLC`, `MySchedFlag::STEAL`)

The const name is derived from the enum name by stripping a trailing
`Flag`/`Flags` suffix and converting to SCREAMING_SNAKE_CASE:
`MySchedFlag` -> `MY_SCHED`, `EevdfFlags` -> `EEVDF`.

Variant names are converted to kebab-case for the flag name:
`RejectPin` -> `"reject-pin"`, `NoCtrl` -> `"no-ctrl"`.

### Manual definition

The const builder pattern still works for cases where the derive
doesn't fit:

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

static MY_LLC: FlagDecl = FlagDecl {
    name: "llc",
    args: &["--enable-llc"],
    requires: &[],
};

static MY_STEAL: FlagDecl = FlagDecl {
    name: "steal",
    args: &["--enable-stealing"],
    requires: &[&MY_LLC],
};

const MY_SCHED: Scheduler = Scheduler::new("my_sched")
    .binary(SchedulerSpec::Name("scx_my_sched"))
    .flags(&[&MY_LLC, &MY_STEAL])
    .topology(1, 2, 4, 1)
    .assert(Assert::NONE.max_imbalance_ratio(2.0));
```

## Cgroup parent

`Scheduler.cgroup_parent` specifies a cgroup subtree under
`/sys/fs/cgroup` for the scheduler to manage. When set, the VM init
creates the directory before starting the scheduler, and
`--cell-parent-cgroup <path>` is injected into the scheduler args.

In the derive:

```rust,ignore
#[scheduler(cgroup_parent = "/ktstr")]
```

Or manually:

```rust,ignore
const MITOSIS: Scheduler = Scheduler::new("scx_mitosis")
    .binary(SchedulerSpec::Name("scx_mitosis"))
    .topology(1, 2, 4, 1)
    .cgroup_parent("/ktstr");
```

This creates `/sys/fs/cgroup/ktstr` in the guest and passes
`--cell-parent-cgroup /ktstr` to the scheduler binary.

## Scheduler args

`Scheduler.sched_args` provides default CLI args that apply to every
test using this scheduler. They are prepended before per-test
`extra_sched_args` and flag-derived args.

In the derive:

```rust,ignore
#[scheduler(sched_args = ["--exit-dump-len", "1048576"])]
```

Or manually:

```rust,ignore
const MITOSIS: Scheduler = Scheduler::new("scx_mitosis")
    .binary(SchedulerSpec::Name("scx_mitosis"))
    .topology(1, 2, 4, 1)
    .cgroup_parent("/ktstr")
    .sched_args(&["--exit-dump-len", "1048576"]);
```

Merge order: `cgroup_parent` injection, then `sched_args`, then
per-test `extra_sched_args`, then flag-derived args.

## Default topology

`Scheduler.topology` sets the default VM topology for all tests using
this scheduler. When `#[ktstr_test]` omits `llcs`, `cores`, and
`threads`, the scheduler's topology is used. Explicit attributes on
`#[ktstr_test]` override the scheduler default.

In the derive:

```rust,ignore
//              numa_nodes, llcs, cores_per_llc, threads_per_core
#[scheduler(topology(1, 2, 4, 1))]
```

Arguments are `(numa_nodes, llcs, cores_per_llc, threads_per_core)`.
Most schedulers use `numa_nodes = 1` (single NUMA node).
`Scheduler::new()` defaults to `(1, 1, 2, 1)`.

Tests that need a different topology (e.g. SMT) override individual
dimensions. Unset dimensions still inherit from the scheduler:

```rust,ignore
// Inherits llcs=2, cores=4 from MITOSIS; overrides threads to 2
#[ktstr_test(scheduler = MITOSIS, threads = 2)]
fn smt_test(ctx: &Ctx) -> Result<AssertResult> { /* ... */ }
```

## Defining flags

Each enum variant is a flag. `#[flag(args)]` lists CLI arguments
passed when the flag is active. `#[flag(requires)]` declares
dependencies. See [Flags](../concepts/flags.md) for dependency
semantics, profile generation, and using flags in
[`#[ktstr_test]`](ktstr-test-macro.md#flag-constraints).

## Flag scoping

`Scheduler.flags` defines which flags the scheduler supports.
`generate_profiles()` on the scheduler only considers these flags,
not the global set. This prevents testing with flags the scheduler
doesn't implement.

## Verification overrides

`Scheduler.assert` provides scheduler-level verification defaults.
These sit between `Assert::default_checks()` and per-test overrides in
the merge chain.

A scheduler that tolerates higher imbalance:

```rust,ignore
const RELAXED: Scheduler = Scheduler::new("relaxed")
    .binary(SchedulerSpec::Name("scx_relaxed"))
    .assert(Assert::NONE.max_imbalance_ratio(5.0));
```

## Kernel-built scheduler example

For schedulers compiled into the kernel (no userspace binary),
use `SchedulerSpec::KernelBuiltin` with shell commands to
activate/deactivate the scheduler:

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

static MINLAT_LLC: FlagDecl = FlagDecl {
    name: "llc",
    args: &[],
    requires: &[],
};

const MINLAT: Scheduler = Scheduler::new("minlat")
    .binary(SchedulerSpec::KernelBuiltin {
        enable: &["echo minlat > /sys/kernel/debug/sched/ext/root/ops"],
        disable: &["echo none > /sys/kernel/debug/sched/ext/root/ops"],
    })
    .flags(&[&MINLAT_LLC]);
```

The `enable` commands run in the guest before scenarios start.
The `disable` commands run after scenarios complete.

For an end-to-end workflow from building a scheduler to running the
gauntlet, see [Test a New Scheduler](../recipes/test-new-scheduler.md).