KtstrTestEntry

Struct KtstrTestEntry 

Source
pub struct KtstrTestEntry {
Show 38 fields pub name: &'static str, pub func: fn(&Ctx<'_>) -> Result<AssertResult>, pub topology: Topology, pub constraints: TopologyConstraints, pub memory_mib: u32, pub cpu_budget: Option<u32>, pub scheduler: &'static Scheduler, pub staged_schedulers: &'static [&'static Scheduler], pub payload: Option<&'static Payload>, pub workloads: &'static [&'static Payload], pub auto_repro: bool, pub expect_auto_repro: bool, pub wprof: bool, pub wprof_args: Option<&'static str>, pub assert: Assert, pub extra_sched_args: &'static [&'static str], pub watchdog_timeout: Duration, pub bpf_map_write: &'static [&'static BpfMapWrite], pub watch_bpf_maps: &'static [&'static WatchBpfMap], pub performance_mode: bool, pub perf_delta_assertions: &'static [&'static PerfDeltaAssertion], pub pci: bool, pub no_perf_mode: bool, pub duration: Duration, pub expect_err: bool, pub survives_storm: bool, pub allow_inconclusive: bool, pub host_only: bool, pub extra_include_files: &'static [&'static str], pub cleanup_budget: Option<Duration>, pub config_content: Option<&'static str>, pub disk: Option<DiskConfig>, pub networks: &'static [NetConfig], pub post_vm: Option<PostVmCallback>, pub post_vm_unconditional: Option<PostVmCallback>, pub num_snapshots: u32, pub workload_root_cgroup: Option<CgroupPath>, pub kaslr: bool,
}
Expand description

Registration entry for an #[ktstr_test]-annotated function.

Construct via the [#[ktstr_test]] macro (the production path) or struct-literal with ..KtstrTestEntry::DEFAULT (the integration-test / gauntlet rewriter path). The macro emits the linkme distributed-slice registration; programmatic callers register via KTSTR_TESTS.

Fields§

§name: &'static str

Fully qualified test name as it appears in nextest output.

§func: fn(&Ctx<'_>) -> Result<AssertResult>

Entry point invoked once per replica, inside the guest VM when host_only is false and on the host when it is true.

§topology: Topology

Base virtual topology; gauntlet expansion produces additional variants layered on top of this baseline.

§constraints: TopologyConstraints

Host-topology constraints (CPU and LLC bounds) that gate whether this entry is eligible on the current machine.

§memory_mib: u32

Guest memory in MiB (binary mebibytes; conversion at VM-launch is value << 20 bytes, not value * 1_000_000).

§cpu_budget: Option<u32>

Host-CPU budget for the no-perf vCPU mask — the number of host CPUs the VM’s vCPU threads share. None auto-sizes to the vCPU count (so a wide VM’s parallel AP bringup isn’t throttled); Some(n) with n < vCPU count forces CPU overcommit for contention testing. Some(0) is rejected (a zero budget cannot run a VM). Requires no_perf_mode: the budget sizes only the no-perf vCPU-thread mask, so setting it without no_perf_mode is rejected (by the #[ktstr_test] macro and by validate) rather than silently ignored. An explicit --cpu-cap / KTSTR_CPU_CAP overrides it. Set by #[ktstr_test(cpu_budget = N)].

§scheduler: &'static Scheduler

Primary scheduler that drives the test. Defaults to Scheduler::EEVDF (the no-scx-scheduler placeholder; tests then run under the kernel’s default scheduling class).

Reference to a static Scheduler — typically the const emitted by declare_scheduler!. Pure-binary-workload tests use Scheduler::EEVDF here and supply their binary via the payload field below.

§staged_schedulers: &'static [&'static Scheduler]

Additional schedulers staged into the guest initramfs alongside Self::scheduler so the scheduler-lifecycle ops (Op::ReplaceScheduler and siblings) can swap to a different scheduler mid-experiment without a VM reboot. The boot-time scheduler is always Self::scheduler — entries here are the candidate set the test will swap TO via Op::AttachScheduler / Op::ReplaceScheduler.

Each staged scheduler must have a Scheduler::name that is unique within the set AND distinct from a small reserved-name list (currently scheduler, sched_args, init, args, exec_cmd, sched_enable, sched_disable) that the framework uses for boot-time initramfs entries. Name collisions OR reserved-name collisions bail at Self::validate time with an actionable diagnostic naming the offending entries.

The boot-time Self::scheduler is NOT auto-included in this set AND cannot be repeated here — Self::validate rejects any staged entry whose name matches the boot scheduler’s name. Keep the boot scheduler in Self::scheduler and stage only the additional candidates the test wants to swap to. Tests that don’t use scheduler-lifecycle ops leave this field at its &[] default and pay no initramfs cost for the staging machinery.

Staged binary content is hashed into the initramfs cache key (see BaseKey in crate::vmm::initramfs_cache) — rebuilding a staged scheduler between test runs invalidates the cache automatically; no manual cache clean is needed.

§payload: Option<&'static Payload>

Optional binary payload to run as the primary workload. When Some, the test runs the referenced Payload (which must be PayloadKind::Binary) alongside the configured scheduler. When None, the test runs a scheduler-only scenario.

Populated by #[ktstr_test(payload = SOME_BIN)]; direct programmatic callers may also set this.

§workloads: &'static [&'static Payload]

Additional binary payloads composed with the primary. Each entry is launched via Ctx::payload in the test body.

Populated by #[ktstr_test(workloads = [A, B])].

§auto_repro: bool

When true, a crash triggers an auto-repro run with BPF probes attached to the crash call chain.

§expect_auto_repro: bool

When the downstream eval-layer wiring is in place AND this field is set to true, the test ASSERTS that the auto-repro path fired during the run — the intended end-state is a verdict inversion converting fail-with-repro-artifact into PASS. The canonical pattern: a test deliberately triggers a scheduler stall, the auto-repro path captures the repro artifact, and the test PASSES because the expected behavior (auto-repro firing) happened.

Distinct from Self::auto_repro:

  • auto_repro = true enables the auto-repro capability — the path fires when the primary run fails.
  • expect_auto_repro = true asserts the path FIRED.

Current wiring state (HEAD): macro-parser support is in place (#[ktstr_test(expect_auto_repro)] / #[ktstr_test(expect_auto_repro = true)] both parse and set this field), cross-attribute validation rejects structurally-nonsensical combinations at macro-parse time (auto_repro = false, expect_err = true, host_only = true, wprof = false, missing scheduler), AND the eval-layer inversion that makes the assertion observable is wired: crate::test_support::eval::apply_expect_auto_repro_inversion runs after evaluate_vm_result, probes the auto-repro .repro.wprof.pb artifact via the shape validator, and (when satisfied) wraps the failure Err with the ExpectAutoReproSatisfied marker; the dispatch arm at crate::test_support::dispatch::result_to_exit_code downcasts the marker and routes the verdict to EXIT_PASS. Default false matches prior behavior (no inversion, original verdict stands).

§wprof: bool

Requires the wprof cargo feature; without it, the #[ktstr_test(wprof)] attribute fails at macro expansion. When true, the VM spawns /bin/wprof concurrently with the workload and ships the Perfetto .pb trace to the host.

§wprof_args: Option<&'static str>

Custom wprof CLI args (requires the wprof cargo feature). Overrides WprofConfig::default_args when Self::wprof is true. Populated by #[ktstr_test(wprof_args = "...")].

§assert: Assert

Per-entry assertion overrides merged on top of Assert::default_checks() and the scheduler’s assert.

§extra_sched_args: &'static [&'static str]

Extra CLI arguments appended to the scheduler invocation.

§watchdog_timeout: Duration

scx_sched.watchdog_timeout override applied to the guest kernel.

§bpf_map_write: &'static [&'static BpfMapWrite]

Host-side BPF map writes to perform during VM execution.

Empty slice (the default) means “no writes.” The host thread (freeze_coord::start_bpf_map_write) runs three phases: (1) build the guest-memory BPF-map accessor once the kernel page tables are up; (2) resolve every entry’s field NAME to a byte offset + width from the target map’s program BTF; (3) write each resolved entry. A phase-1/2 failure (accessor init, or a field that never resolves within the deadline) aborts before phase 3 without signalling the guest, which self-unblocks on its own wait_for_map_write timeout. In phase 3 every resolved entry is attempted — a field whose width is not the 4 bytes the u32 value stores is skipped (logged), and a failed write is logged — then SIGNAL_BPF_WRITE_DONE is pushed to the guest UNCONDITIONALLY after the loop, so a skipped or failed entry still unblocks the guest rather than leaving it to time out.

§watch_bpf_maps: &'static [&'static WatchBpfMap]

Named scheduler BPF-map fields to read observer-effect-free into assertable run-level metrics. The free-running host monitor resolves each WatchBpfMap lazily (after the scheduler attaches and its maps appear) and reads it each tick without freezing the guest; the value is folded run-level and exposed via crate::vmm::result::VmResult::run_metric under <scheduler-obj>_<label> (scalar/scalar-counter/per-CPU-counter) or _<label>_{avg,max} (per-CPU gauge).

§performance_mode: bool

Pin vCPU threads to host cores matching the virtual topology’s LLC structure, use 2MB hugepages for guest memory, NUMA mbind guest memory to pinned vCPU nodes, and promote vCPU threads to SCHED_FIFO. Validates that the host has enough CPUs and LLCs to satisfy the request without oversubscription.

On x86_64, additionally: set KVM_HINTS_REALTIME CPUID hint (disables PV spinlocks, PV TLB flush, PV sched_yield; enables haltpoll cpuidle), disable PAUSE and HLT VM exits via KVM_CAP_X86_DISABLE_EXITS (HLT falls back to PAUSE-only when mitigate_smt_rsb is active), skip KVM_CAP_HALT_POLL (guest haltpoll cpuidle disables host halt polling via MSR_KVM_POLL_CONTROL), and check TSC stability.

On aarch64, KVM exit suppression and CPUID hints are not available. The four host-side optimizations (vCPU pinning, hugepages, NUMA mbind, RT scheduling) apply.

§perf_delta_assertions: &'static [&'static PerfDeltaAssertion]

Per-test performance-regression assertions enforced ONLY by cargo ktstr perf-delta --noise-adjust (inert under a normal cargo ktstr test run — the in-VM verdict never consults them — and skipped-with-a-warning under a scalar perf-delta without --noise-adjust, since single-run gating would flip CI on noise). Each PerfDeltaAssertion names a registry metric and overrides the confident-regression gate for it (relative / absolute threshold, direction, phase scope), LAYERED ON TOP of the --noise-adjust default all-metrics net. Serialized into the sidecar so the host-side compare can read the declaration. &[] = no per-test assertions. Requires performance_mode (checked by Self::validate and the macro) — a declaration on a non-perf test would compare unpinned, noisy data.

§pci: bool

Enable the virtio-PCI transport: a host bridge at 00:00.0 plus the PCI0 ECAM/CAM config-access windows. When false, no PCI host bridge is exposed and the guest cmdline carries pci=off. Surfaced by #[ktstr_test(pci)]; named to match the attribute and the bare-bool-field convention (host_only, auto_repro).

§no_perf_mode: bool

Decouple virtual topology from host hardware. When set:

  • The VM’s virtual topology (numa_nodes, llcs, cores, threads) is built as declared — the guest sees the full requested topology via KVM vCPU layout, ACPI SRAT/SLIT tables, etc.
  • Host-side cpuset/LLC locking still applies (the no-perf LlcPlan path), so concurrent perf-mode peers are still serialised against this VM.
  • Host-side performance_mode optimisations are skipped: no vCPU-to-host-core pinning, no 2 MB hugepages, no NUMA mbind, no SCHED_FIFO promotion, no KVM_HINTS_REALTIME CPUID hint, no KVM_CAP_X86_DISABLE_EXITS.
  • Host topology constraints are relaxed during gauntlet preset filtering — the entry’s min_numa_nodes / min_llcs / requires_smt / per-LLC CPU limits are not compared against host hardware. The only host check that stays is “total host CPUs >= total vCPUs”, so a test declaring numa_nodes = 3 runs on a 1-NUMA-node host.

Equivalent to setting KTSTR_NO_PERF_MODE=1 per-test — either source forces the no-perf path. Mutually exclusive with performance_mode = true; KtstrTestEntry::validate rejects the combination because “I want pinning” and “I explicitly don’t want pinning” are contradictory.

§duration: Duration

Workload duration.

§expect_err: bool

When true, the test expects run_ktstr_test to return Err. Disables auto_repro (no point probing a deliberately failing test).

§survives_storm: bool

When true, assert the scx scheduler SURVIVES the run — it must NOT die or get ejected during any hold. The positive inverse of Self::expect_err.

Survival is otherwise IMPLICIT: a scheduler that dies during a hold records a DetailKind::Scheduler* fail via the scenario liveness probe (crate::scenario::ops build_sched_died_*), which already fails the run through the failure→Err→EXIT_FAIL path. Setting this flag makes the intent explicit in source and attaches a survival- specific failure explainer that names the asserted intent. It is enforced for EVERY scenario: those driven through execute_defs/execute_steps/execute_scenario re-check liveness between steps and inside every hold, and a guest-side post-function probe (enforce_survives_storm_liveness) re-checks once more after the test function returns — so even a scenario that hand-rolls Op dispatch without an execute_* driver actively fails if the scheduler died or went down (scx state disabling/disabled) during the run.

Mutually exclusive with both Self::expect_err (one demands failure, the other survival) and Self::expect_auto_repro (both invert the scheduler-death-fail signal — survives_storm forces it to EXIT_FAIL, expect_auto_repro inverts a crash-with-repro fail to PASS), and requires an active scheduler (SchedulerSpec::has_active_scheduling); all three are rejected by validate.

§allow_inconclusive: bool

When true, a terminal Inconclusive verdict (e.g. zero-denominator ratio gate that couldn’t evaluate) routes to EXIT_PASS instead of EXIT_INCONCLUSIVE at the dispatch layer. The test process exits 0 and CI gates keying off the per-test exit code see no failure. Use only when the test author has reason to accept an Inconclusive arm as not-a-failure for this specific test — e.g. an exploratory benchmark whose ratio gate may legitimately see no signal under certain host topologies. Inconclusive is still recorded in the sidecar so stats tooling can surface it, and the operator-facing failure dump still renders the Inconclusive diagnostic. This flag changes only the dispatch exit-code projection.

Mutually orthogonal with Self::expect_err: when both are true and the result is Inconclusive, expect_err still wins (expect_err demands a real Fail; Inconclusive doesn’t satisfy that and routes to EXIT_FAIL with the expect_err unsatisfied explainer).

Populated by #[ktstr_test(allow_inconclusive)] / #[ktstr_test(allow_inconclusive = true)] or by direct entry construction.

§host_only: bool

When true, the test runs directly on the host instead of booting a VM. Used for tests that need host tools (cargo, nested VMs) unavailable in the guest initramfs.

§extra_include_files: &'static [&'static str]

Extra host-side file specs beyond what the entry’s scheduler / payload / workloads declare. Unions with those per-payload specs at run_ktstr_test time; see all_include_files for the aggregation contract. Use this slot for test-level dependencies that don’t belong on a specific Payload — auxiliary data files, per-test helper scripts, fixtures.

§cleanup_budget: Option<Duration>

Maximum acceptable wall-clock duration of host-side VM teardown (BSP exit through SHM drain). Compared against VmResult::cleanup_duration in evaluate_vm_result; when the budget is exceeded the test’s AssertResult is folded with a failing AssertDetail. Catches sub-watchdog cleanup regressions (e.g. a 30s teardown that the 60s host watchdog would silently absorb) at the test that declares the budget rather than at gross-timeout failure. None (the default) disables the check, leaving the watchdog as the only guard. Populated by #[ktstr_test(cleanup_budget_ms = N)] or by direct entry construction.

§config_content: Option<&'static str>

Inline config content (JSON string) written to the guest path declared by the scheduler’s config_file_def. The framework writes this string to a temp file, packs it into the initramfs, and passes the scheduler’s arg template with {file} replaced.

Populated by #[ktstr_test(config = EXPR)] (literal or path to a const &'static str) or by direct entry construction.

Pairing gate: config_content and the scheduler’s config_file_def must both be Some(_) or both be None. KtstrTestEntry::validate enforces this at runtime so direct programmatic-entry callers see the misconfiguration before VM boot, and the #[ktstr_test] macro emits a const _: () = { assert!(...) }; block that catches the same mismatch at compile time for attribute-built entries. A Some here without a scheduler config_file_def would be silently dropped at dispatch (no --config flag derives from it); a None here against a scheduler that declares config_file_def would launch the scheduler binary without --config. Both are rejected.

§disk: Option<DiskConfig>

Optional virtio-blk disk attached to the VM at /dev/vda. None (the default) boots without a disk; Some(cfg) calls crate::vmm::KtstrVmBuilder::disk in crate::test_support::runtime::build_vm_builder_base so the guest sees a raw block device sized per cfg.capacity_mib. Surfaced by #[ktstr_test(disk = PATH)], with_disk, or direct construction via ..KtstrTestEntry::DEFAULT. Mutually exclusive with host_only: validate rejects the combination because host_only skips the VM boot that owns the disk lifecycle.

§networks: &'static [NetConfig]

virtio-net devices attached to the VM (in-VMM loopback backend), one per element. Empty (the default) boots without a NIC; each element calls crate::vmm::KtstrVmBuilder::network in crate::test_support::runtime::build_vm_builder_base. On x86_64 every element gets its own virtio-pci function (PCI slots 1..=N, one INTx GSI apiece); aarch64 takes a single virtio-MMIO NIC (build() errors on more than one). Surfaced by #[ktstr_test(networks = [PATH, ...])], with_networks, or direct construction. Mutually exclusive with host_only: validate rejects a non-empty list because host_only skips the VM boot that owns the virtio-net devices.

§post_vm: Option<PostVmCallback>

Host-side callback invoked after vm.run() returns, with access to the full VmResult. Runs on the HOST, not inside the guest. Use for assertions that need host-side state (e.g., VmResult.snapshot_bridge content after a snapshot capture pipeline fires inside the VM).

None (the default) skips the callback. When Some, the closure receives &VmResult and returns Result<()> — an Err fails the test with the returned message.

Skipped when the guest already reported a failed AssertResult — the existing guest-side fail diagnostic is more useful than a derivative post_vm Err that asserts on workload-derived state the crashed guest never produced. Tests that need the host-side check to run even on guest-fail should use post_vm_unconditional instead.

§post_vm_unconditional: Option<PostVmCallback>

Host-side callback invoked after vm.run() returns — like post_vm — but UNCONDITIONAL: runs even when the guest already reported a failed AssertResult.

Use this when the callback must observe host-side state regardless of guest-side outcome — e.g. verifying that an artifact landed in the sidecar directory even on a deliberately-failing fault-injection test.

Setting post_vm_unconditional does NOT invert the test verdict — a guest-reported fail still fails the test even when the unconditional callback returns Ok. The primitive lets the callback OBSERVE host-side state despite the fail; flipping a forced-fail test to PASS requires a separate framework primitive (e.g. an expect_auto_repro = true attribute that converts fail-plus-artifacts-present to PASS) which is intentionally out of scope here.

Canonical callback shape (mirrors the framework’s default_post_vm_periodic_fired self-guard):

fn my_unconditional_check(result: &VmResult) -> anyhow::Result<()> {
    // Skip when the VM run itself crashed (scheduler
    // died, watchdog fired, KVM exit during boot) — the
    // underlying failure already drives the test
    // diagnostic; layering a "missing state" error on
    // top obscures the cause.
    if !result.success {
        return Ok(());
    }
    // Now assert on whatever host-side artifact must
    // exist when the workload ran to completion.
    // ...
    Ok(())
}

Two suppression layers to be aware of:

  1. FRAMEWORK level: post_vm is skipped entirely when the guest already reported a failed AssertResult (see the guest_already_failed gate in src/test_support/eval/mod.rs::run_ktstr_test_inner_impl’s post_vm dispatch site). post_vm_unconditional bypasses that suppression and always runs.
  2. CALLBACK level: the conventional post_vm callback body short-circuits via if !result.success { return Ok(()); } to silence on crash/watchdog/KVM-boot-exit cases where the guest never produced an AssertResult for the framework gate to fire on; the framework’s default_post_vm_periodic_fired is the canonical example. An unconditional callback that asserts on workload-derived state without an equivalent callback-side guard will surface a misleading “missing state” error on scheduler crashes.

None (the default) skips the unconditional callback. Both post_vm and post_vm_unconditional may be set on the same entry — post_vm is suppressed on guest-fail per its existing contract, post_vm_unconditional always runs. If both callbacks set on the same entry both return Err, the framework surfaces both via combine_post_vm_errs chained as post_vm: <conditional_err>; post_vm_unconditional: <unconditional_err> so a debugging operator sees both regressions on the first pass rather than discovering the second one only after fixing the first.

Setting the SAME callback fn pointer on BOTH slots will invoke it twice on the guest-success path (conditional + unconditional) and once on the guest-fail path (unconditional only); idempotent callbacks (read-only assertions) tolerate this fine, but a side-effecting callback (counter increment, file open) should pick exactly one slot.

§num_snapshots: u32

Periodic snapshot count: when non-zero, the freeze coordinator divides the 10 %–90 % slice of the capturable window into num_snapshots + 1 equal intervals and fires a host-side freeze_and_dispatch(FreezeMode::Capture { gate_on_exit_kind: false }) at each of the num_snapshots interior boundaries — e.g. N = 1 lands a single capture at the window midpoint; N = 3 lands at 0.3 / 0.5 / 0.7 of the window. No boundary lands at exactly the 0.1 / 0.9 edges — the buffers reserve those for workload ramp-up / ramp-down. Each boundary is stored under "periodic_NNN" (zero-padded 3-digit index) on the host’s crate::scenario::snapshot::SnapshotBridge. The window is [max(scenario_start, prereqs_ready), scenario_start + duration]: the start floats to the prereq-ready moment (kaslr + BPF-accessor attach) so cold-boot latency cannot strand boundaries pre-ready, and the end is CLAMPED to the workload end so captures never spill into post-workload idle. On a WARM boot the start equals scenario_start, so the window is the full [start, start + d] and the fractions above apply as documented (start + 0.3·d etc., modulo integer-ns truncation); on a COLD boot the window starts later and is shorter, so the landings shift — cross-run compares of the window-averaged keys avg_cpu_util_comp_scale / avg_task_lat_cri should use --noise-adjust. Boot + verifier time before ScenarioStart does not eat the budget. Pauses observed via MSG_TYPE_SCENARIO_PAUSE / MSG_TYPE_SCENARIO_RESUME shift every un-fired boundary by the cumulative pause duration — the boundary clock is workload-time, not wall-clock, so a guest that pauses for P ns delays each remaining boundary by P ns. 0 (the default) disables periodic capture entirely; the coordinator’s run-loop never even computes boundary timestamps.

Capture cost. Each periodic boundary fires the same host-side freeze_and_dispatch(FreezeMode::Capture { gate_on_exit_kind: false }) path that crate::scenario::ops::Op::CaptureSnapshot dispatches: every vCPU is parked under FREEZE_RENDEZVOUS_TIMEOUT (30 s hard ceiling), BPF maps are walked, the dump is serialised to JSON, and the report is stored on the crate::scenario::snapshot::SnapshotBridge. On a healthy guest with a typical scheduler-state map size the freeze is tens of milliseconds (10–100 ms is the steady-state observation; cold-cache and large guest-memory walks can push higher). The host-side watchdog deadline is extended by the freeze duration after each fire, so periodic captures do not eat into the workload’s wall-clock budget.

Best-effort delivery. Up to N captures fire; an early VM exit (kill flag, BSP done, rendezvous timeout, watchdog deadline) can cut the periodic sequence short, and the run-loop stops servicing periodic boundaries the moment the kill flag fires. Tests should assert >= some_lower_bound rather than == num_snapshots. Op::CaptureSnapshot captures composed by the test author land on the same bridge alongside the periodic_NNN tags; total bridge occupancy is num_snapshots + user_captures and the bridge FIFO-evicts past crate::scenario::snapshot::MAX_STORED_SNAPSHOTS. Additionally, the run-loop abandons the remaining sequence after 2 consecutive rendezvous timeouts and emits a tracing::warn naming the consecutive-timeout count.

Minimum spacing. Each capture freezes every vCPU for tens of milliseconds at minimum (see “Capture cost” above), so boundaries scheduled closer than ~100 ms apart would fire back-to-back without any workload progress in between. validate() rejects entries where 0.8 · duration / (N + 1) < 100 ms — choose N and duration so the resulting interval clears that floor.

Ordering. Periodic captures are stored in order (periodic_000 first, periodic_NNN last). Tests that need to walk them in time order should call crate::scenario::snapshot::SnapshotBridge::drain_ordered rather than crate::scenario::snapshot::SnapshotBridge::drain — the latter returns a HashMap and loses ordering.

validate() rejects num_snapshots > crate::scenario::snapshot::MAX_STORED_SNAPSHOTS (= 64 today): the bridge enforces FIFO eviction at that cap, so a higher count would silently drop the earliest periodic samples once store() started evicting. Refusing the configuration is more honest than half-delivering it. The 64 cap also ensures the 3-digit :03 width on periodic_NNN is always sufficient.

§workload_root_cgroup: Option<CgroupPath>

Cgroup directory that the framework creates BEFORE the scheduler starts and uses as the parent for every workload cgroup the test author declares via Ctx::cgroup_def (ctx.cgroup_def("cg_0") etc.). When Some(path), the guest mkdir’s /sys/fs/cgroup{path} and the per-test CgroupManager places its children there (/sys/fs/cgroup{path}/cg_0 etc.); when None, the framework falls back to the legacy resolution (crate::test_support::args::resolve_cgroup_root--cell-parent-cgroup in sched_args overrides; default /sys/fs/cgroup/ktstr).

Distinct from crate::test_support::Scheduler::cgroup_parent: cgroup_parent is a scheduler-only knob that controls the scheduler argv (--cell-parent-cgroup flag, only when the scheduler declaration explicitly carries it in sched_args); workload_root_cgroup is a framework knob for the workload side and never reaches the scheduler argv. A test can set workload_root_cgroup without affecting the scheduler’s cgroup placement, and a scheduler can set cgroup_parent without affecting where workloads land.

§kaslr: bool

Whether KASLR is enabled in the guest kernel for this test. true (the default) lets the guest randomize kernel virt + direct-map addresses (CONFIG_RANDOMIZE_BASE=y + CONFIG_RANDOMIZE_MEMORY=y in ktstr.kconfig); false appends nokaslr to the guest cmdline so the kernel-image slide and the page_offset_base direct-map randomization both stay at compile-time defaults. KASLR-off is the determinism escape for tests that depend on fixed kernel addresses or that need to reproduce bugs masked by randomization; the default-on case exercises ktstr’s host-side derivation chain (MSR_LSTAR readback, KERN_ADDRS guest channel, /proc/kallsyms page_offset_base lookup) end-to-end. The kaslr_*_e2e regression tests guard the derivation; the kaslr_disabled_via_macro_attribute regression guards this opt-out. Operator-level alternative: kargs = ["nokaslr"] on the scheduler decl — same effect, declared once for every test that uses the scheduler. Combining kaslr = false with kargs = ["nokaslr"] is redundant but harmless — nokaslr appearing twice on the cmdline is a no-op (kernel parses it as a bool flag, not a value).

Implementations§

Source§

impl KtstrTestEntry

Source

pub const DEFAULT: Self

Sensible defaults for all fields. Override name, func, and scheduler (at minimum) via struct update syntax. Manual consumers should also set auto_repro explicitly: the default true boots a second VM with BPF probes attached on failure, which roughly doubles a failing test’s wall-clock time.

Self::DEFAULT is the source of truth (struct-literal const); Self::new() is a delegating alias for method-style use and Default::default() is the trait-shim — both equivalent in non-const contexts. For static / const initializer spread sites (e.g. #[distributed_slice(KTSTR_TESTS)] macro expansions), ..Self::DEFAULT is the canonical shape — it spreads the struct-literal const directly without taking a detour through a const-fn return.

use ktstr::prelude::*;

fn my_test_fn(_ctx: &Ctx) -> Result<AssertResult> {
    Ok(AssertResult::pass())
}

#[distributed_slice(KTSTR_TESTS)]
#[linkme(crate = ktstr::linkme)]
static ENTRY: KtstrTestEntry = KtstrTestEntry {
    name: "my_test",
    func: my_test_fn,
    scheduler: &Scheduler::EEVDF,
    ..KtstrTestEntry::DEFAULT
};

The #[linkme(crate = ktstr::linkme)] annotation is required when the downstream crate does not depend on linkme directly — see crate::distributed_slice for the full rationale.

Source

pub const fn new() -> Self

Build the default entry. Equivalent to Self::DEFAULT. Either ..Self::DEFAULT or ..Self::new() works in static / const initializer spread sites since new() is const fn and KtstrTestEntry has no Drop-bearing fields. Default::default() is the trait-shim equivalent for non-const contexts.

Source

pub fn all_include_files(&self) -> Vec<&'static str>

Aggregate every declared include-file spec: the entry’s primary payload (if present) contributes its Payload::include_files, each entry in workloads contributes its own, and extra_include_files contributes test-level extras. Pre-dedupe aggregation order: payload → workloads (in declaration order) → extras. The scheduler tier does not contribute — Scheduler has no include_files field, and the scheduler binary path is resolved separately at run time. Duplicate spec strings at this layer are NOT deduped — the framework’s include-file pipeline at run_ktstr_test resolves each entry to a (archive_path, host_path) pair and dedupes on identical pairs while erroring on archive_path collisions with conflicting host_paths. This aggregation order does NOT survive downstream: the final resolved list is sorted alphabetically by archive_path after deduplication. Alphabetical ordering ensures deterministic initramfs layout regardless of declaration order.

Source§

impl KtstrTestEntry

Programmatic builder methods. Use at runtime (let bindings, fn returns). For static / const initializers and #[distributed_slice(KTSTR_TESTS)] registration, prefer the struct-literal ..KtstrTestEntry::DEFAULT spread — chained with_X calls fail in const context with E0015 (“cannot call non-const fn in constants”) because these setters are declared pub fn, not pub const fn. See Self::DEFAULT for the worked struct-literal example.

Source

pub fn with_name(self, name: &'static str) -> Self

Override name.

Source

pub fn with_func(self, func: fn(&Ctx<'_>) -> Result<AssertResult>) -> Self

Override func.

Source

pub fn with_topology(self, topology: Topology) -> Self

Override topology.

Source

pub fn with_constraints(self, constraints: TopologyConstraints) -> Self

Override constraints.

Source

pub fn with_memory_mib(self, memory_mib: u32) -> Self

Override memory_mib.

Source

pub fn with_cpu_budget(self, cpu_budget: u32) -> Self

Override cpu_budget (the no-perf host-CPU budget; n below the vCPU count forces overcommit). Sets Some(n); the default is None (auto-size to the vCPU count). validate rejects Some(0) and rejects a budget set without no_perf_mode.

Source

pub fn without_cpu_budget(self) -> Self

Clear cpu_budget (auto-size the no-perf vCPU mask to the vCPU count).

Source

pub fn with_scheduler(self, scheduler: &'static Scheduler) -> Self

Override scheduler.

Source

pub fn with_staged_schedulers( self, staged: &'static [&'static Scheduler], ) -> Self

Override staged_schedulers — the candidate set the test can swap to mid-experiment via the scheduler-lifecycle ops (Op::AttachScheduler / Op::ReplaceScheduler). See the field doc on Self::staged_schedulers for the per-scheduler-name uniqueness + reserved-name validation contract.

Source

pub fn with_payload(self, payload: &'static Payload) -> Self

Override payload.

Source

pub fn without_payload(self) -> Self

Clear payload (run a scheduler-only scenario with no primary binary).

Source

pub fn with_workloads(self, workloads: &'static [&'static Payload]) -> Self

Override workloads.

Source

pub fn with_auto_repro(self, auto_repro: bool) -> Self

Override auto_repro.

Source

pub fn with_expect_auto_repro(self, expect_auto_repro: bool) -> Self

Override expect_auto_repro.

Source

pub fn with_assert(self, assert: Assert) -> Self

Override assert.

Replaces the entry’s per-test overrides wholesale; the assertion resolution at run time still layers Assert::default_checks() and the scheduler-level assert underneath.

Source

pub fn with_extra_sched_args( self, extra_sched_args: &'static [&'static str], ) -> Self

Override extra_sched_args.

Source

pub fn with_watchdog_timeout(self, watchdog_timeout: Duration) -> Self

Override watchdog_timeout.

Source

pub fn with_bpf_map_write( self, bpf_map_write: &'static [&'static BpfMapWrite], ) -> Self

Override bpf_map_write.

Source

pub fn with_watch_bpf_maps( self, watch_bpf_maps: &'static [&'static WatchBpfMap], ) -> Self

Override watch_bpf_maps.

Source

pub fn with_performance_mode(self, performance_mode: bool) -> Self

Override performance_mode.

Source

pub fn with_no_perf_mode(self, no_perf_mode: bool) -> Self

Override no_perf_mode.

Source

pub fn with_duration(self, duration: Duration) -> Self

Override duration.

Source

pub fn with_expect_err(self, expect_err: bool) -> Self

Override expect_err.

Source

pub fn with_survives_storm(self, survives_storm: bool) -> Self

Source

pub fn with_allow_inconclusive(self, allow_inconclusive: bool) -> Self

Override Self::allow_inconclusive. When true, an Inconclusive terminal verdict routes to EXIT_PASS instead of EXIT_INCONCLUSIVE at the dispatch layer.

Source

pub fn with_host_only(self, host_only: bool) -> Self

Override host_only.

Source

pub fn with_extra_include_files( self, extra_include_files: &'static [&'static str], ) -> Self

Override extra_include_files.

Source

pub fn with_cleanup_budget(self, cleanup_budget: Duration) -> Self

Override cleanup_budget.

Source

pub fn without_cleanup_budget(self) -> Self

Clear cleanup_budget (leave the host watchdog as the only guard).

Source

pub fn with_config_content(self, config_content: &'static str) -> Self

Override config_content.

Source

pub fn without_config_content(self) -> Self

Clear config_content.

Source

pub fn with_disk(self, disk: DiskConfig) -> Self

Override disk.

Pairs with the host_only = false requirement enforced by Self::validatehost_only = true with a Some(..) disk is rejected because host-only skips the VM boot that owns the virtio-blk lifecycle.

Source

pub fn with_networks(self, networks: &'static [NetConfig]) -> Self

Override networks (one virtio-net device per element).

Pairs with the host_only = false requirement enforced by Self::validatehost_only = true with a non-empty list is rejected because host-only skips the VM boot that owns the virtio-net devices.

Source

pub fn without_networks(self) -> Self

Clear networks (boot without a virtio-net device).

Source

pub fn without_disk(self) -> Self

Clear disk (boot without a virtio-blk device).

Source

pub fn with_post_vm(self, post_vm: PostVmCallback) -> Self

Override post_vm.

The closure runs on the host after vm.run() returns with access to the full VmResult; an Err from the closure fails the test with the returned message. Skipped when the guest already reported a failed AssertResult — see Self::post_vm for the suppression contract; use Self::with_post_vm_unconditional when the host-side check must run even on guest-fail.

Source

pub fn without_post_vm(self) -> Self

Clear post_vm (skip the host-side callback).

Source

pub fn with_post_vm_unconditional( self, post_vm_unconditional: PostVmCallback, ) -> Self

Override post_vm_unconditional.

The closure runs on the host after vm.run() returns — like Self::with_post_vm — but bypasses the guest-fail suppression that gates the conditional Self::with_post_vm callback. An Err from the closure fails the test with the returned message.

Source

pub fn without_post_vm_unconditional(self) -> Self

Clear post_vm_unconditional (skip the unconditional host-side callback). Does not affect the conditional Self::with_post_vm callback if one is set.

Source

pub fn with_num_snapshots(self, num_snapshots: u32) -> Self

Override num_snapshots.

Source

pub const fn with_workload_root_cgroup(self, path: &'static str) -> Self

Override workload_root_cgroup with a validated path. path must satisfy CgroupPath::new’s requirements (starts with /, not bare /, no .. components); the const-eval gate panics on invalid input so programmatic callers see the same validation as the macro path.

Source§

impl KtstrTestEntry

Source

pub fn validate(&self) -> Result<()>

Reject values that would boot a broken VM or leave assertions vacuously passing. The #[ktstr_test] proc macro enforces the same constraints at compile time for attribute-built entries; this method covers directly-constructed entries (library callers building KtstrTestEntry values to push into KTSTR_TESTS programmatically).

Rules:

  • name must be non-empty (empty names collapse into each other in nextest output and in sidecar lookups).
  • name must not contain / or \ (path separators embed in sidecar filenames and nextest test IDs; a separator would create a synthetic subdirectory in sidecar output and mangle cargo nextest run -E 'test(name)' filtering).
  • memory_mib must be > 0 (a VM with zero memory cannot boot).
  • duration must be > 0 (a zero-duration run never exercises the scheduler and produces no telemetry).

Trait Implementations§

Source§

impl Debug for KtstrTestEntry

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more
Source§

impl Default for KtstrTestEntry

Source§

fn default() -> Self

Returns the “default value” for a type. Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

§

impl<T> Instrument for T

§

fn instrument(self, span: Span) -> Instrumented<Self>

Instruments this type with the provided [Span], returning an Instrumented wrapper. Read more
§

fn in_current_span(self) -> Instrumented<Self>

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> IntoEither for T

Source§

fn into_either(self, into_left: bool) -> Either<Self, Self>

Converts self into a Left variant of Either<Self, Self> if into_left is true. Converts self into a Right variant of Either<Self, Self> otherwise. Read more
Source§

fn into_either_with<F>(self, into_left: F) -> Either<Self, Self>
where F: FnOnce(&Self) -> bool,

Converts self into a Left variant of Either<Self, Self> if into_left(&self) returns true. Converts self into a Right variant of Either<Self, Self> otherwise. Read more
§

impl<T> Pointable for T

§

const ALIGN: usize

The alignment of pointer.
§

type Init = T

The type for initializers.
§

unsafe fn init(init: <T as Pointable>::Init) -> usize

Initializes a with the given initializer. Read more
§

unsafe fn deref<'a>(ptr: usize) -> &'a T

Dereferences the given pointer. Read more
§

unsafe fn deref_mut<'a>(ptr: usize) -> &'a mut T

Mutably dereferences the given pointer. Read more
§

unsafe fn drop(ptr: usize)

Drops the object pointed to by the given pointer. Read more
§

impl<T> PolicyExt for T
where T: ?Sized,

§

fn and<P, B, E>(self, other: P) -> And<T, P>
where T: Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns [Action::Follow] only if self and other return Action::Follow. Read more
§

fn or<P, B, E>(self, other: P) -> Or<T, P>
where T: Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns [Action::Follow] if either self or other returns Action::Follow. Read more
Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
§

impl<V, T> VZip<V> for T
where V: MultiLane<T>,

§

fn vzip(self) -> V

§

impl<T> WithSubscriber for T

§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self>
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a [WithDispatch] wrapper. Read more
§

fn with_current_subscriber(self) -> WithDispatch<Self>

Attaches the current default Subscriber to this type, returning a [WithDispatch] wrapper. Read more
§

impl<T> MaybeSend for T
where T: Send,

§

impl<T> MaybeSend for T
where T: Send,