ktstr 0.4.2

Test harness for Linux process schedulers
# Write a Dynamic Scenario

Use [ops/steps](../concepts/ops.md) to express cgroup topology changes
without hand-written `Action::Custom` functions.

## Basic: two phases with cpuset resize

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

fn my_resize_scenario(ctx: &Ctx) -> Result<AssertResult> {
    let steps = vec![
        // Phase 1: two disjoint cgroups, hold for half the duration
        Step::with_defs(
            vec![
                CgroupDef::named("cg_0")
                    .with_cpuset(CpusetSpec::disjoint(0, 2))
                    .workers(4),
                CgroupDef::named("cg_1")
                    .with_cpuset(CpusetSpec::disjoint(1, 2))
                    .workers(4),
            ],
            HoldSpec::Frac(0.5),
        ),
        // Phase 2: resize cpusets to overlap
        Step::new(
            vec![
                Op::set_cpuset("cg_0", CpusetSpec::overlap(0, 2, 0.5)),
                Op::set_cpuset("cg_1", CpusetSpec::overlap(1, 2, 0.5)),
            ],
            HoldSpec::Frac(0.5),
        ),
    ];
    execute_steps(ctx, steps)
}
```

## Registering (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.

Register the scenario in `all_scenarios()`. Set `num_cgroups` to 0 and
`action` to `Custom` -- the step executor handles all cgroup creation
via `CgroupDef`:

```rust,ignore
Scenario {
    name: "my_resize",
    category: "dynamic",
    description: "Resize cpusets from disjoint to overlapping",
    required_flags: &[],
    excluded_flags: &[],
    num_cgroups: 0,
    cpuset_mode: CpusetMode::None,
    cgroup_works: vec![],
    action: Action::Custom(my_resize_scenario),
}
```

See [Scenarios](../concepts/scenarios.md) for the full `Scenario` struct
and [Ops and Steps](../concepts/ops.md) for `CpusetSpec` and `HoldSpec`.