ktstr 0.5.2

Test harness for Linux process schedulers
# CgroupGroup

`CgroupGroup` is an RAII guard that removes cgroups on drop. It
prevents cgroup leaks when workload spawning or other operations fail
between cgroup creation and cleanup.

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

#[must_use = "dropping a CgroupGroup immediately destroys the cgroups it manages"]
pub struct CgroupGroup<'a> {
    cgroups: &'a dyn CgroupOps,
    names: Vec<String>,
}
```

## Methods

**`new(cgroups: &dyn CgroupOps) -> Self`** -- creates an empty group
bound to any implementor of `CgroupOps` (e.g.
[`CgroupManager`](cgroup-manager.md) in production, an in-memory fake
in tests).

**`add_cgroup(name, cpuset) -> Result<()>`** -- creates a cgroup and
sets its cpuset. The cgroup is tracked for removal on drop.

**`add_cgroup_no_cpuset(name) -> Result<()>`** -- creates a cgroup
without setting a cpuset. The cgroup is tracked for removal on drop.

**`names() -> &[String]`** -- returns the names of all tracked cgroups.

## Drop behavior

When the `CgroupGroup` is dropped, it calls `remove_cgroup()` on each
tracked cgroup in reverse insertion order so nested children are
removed before their parents (a parent still holding child
directories would fail with `ENOTEMPTY`).

`ENOENT` is the one errno the drop swallows silently — it indicates
the directory is already gone (the post-condition cleanup owes), which
can legitimately happen via a TOCTOU race between the inner
`exists()` check and `remove_dir`. Every other error (`EBUSY` from a
surviving task, `EACCES`, a broken `cgroupfs` mount, etc.) is emitted
as a `tracing::warn!` record carrying the cgroup name, the full error
chain, and — for `EBUSY` or `EACCES` — a short remediation hint. The
drop never panics and never returns an error (it cannot), but
teardown failures are visible in logs rather than silently swallowed.

## Usage

`CgroupGroup` is the standard pattern for cgroup lifecycle management
in custom scenarios and in `run_scenario()` for data-driven scenarios.

```rust,ignore
fn custom_scenario(ctx: &Ctx) -> Result<AssertResult> {
    let mut guard = CgroupGroup::new(ctx.cgroups);
    guard.add_cgroup("cg_0", &cpuset_a)?;
    guard.add_cgroup("cg_1", &cpuset_b)?;

    // If WorkloadHandle::spawn() fails here, guard drops
    // and both cgroups are removed automatically.
    let mut h = WorkloadHandle::spawn(&config)?;
    ctx.cgroups.move_tasks("cg_0", &h.worker_pids_for_cgroup_procs()?)?;
    h.start(); // workers block until start() is called

    // ... run workload ...

    // guard drops at end of scope, removing cg_0 and cg_1.
    Ok(result)
}
```

The helper function [`setup_cgroups()`](../writing-tests/custom-scenarios.md#helper-functions)
returns a `CgroupGroup` alongside the worker handles:

```rust,ignore
let (handles, _guard) = setup_cgroups(ctx, 2, &wl)?;
// _guard lives until end of scope; cgroups are cleaned up on drop.
```

See also: [CgroupManager](cgroup-manager.md) for filesystem operations,
[WorkloadHandle](workload-handle.md) for worker lifecycle,
[TestTopology](../concepts/topology.md) for cpuset generation.