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 strFully 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: TopologyBase virtual topology; gauntlet expansion produces additional variants layered on top of this baseline.
constraints: TopologyConstraintsHost-topology constraints (CPU and LLC bounds) that gate whether this entry is eligible on the current machine.
memory_mib: u32Guest 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 SchedulerPrimary 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: boolWhen true, a crash triggers an auto-repro run with BPF probes attached to the crash call chain.
expect_auto_repro: boolWhen 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 = trueenables the auto-repro capability — the path fires when the primary run fails.expect_auto_repro = trueasserts 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: boolRequires 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: AssertPer-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: Durationscx_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: boolPin 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: boolEnable 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: boolDecouple 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
LlcPlanpath), 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_FIFOpromotion, noKVM_HINTS_REALTIMECPUID hint, noKVM_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 declaringnuma_nodes = 3runs 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: DurationWorkload duration.
expect_err: boolWhen true, the test expects run_ktstr_test to return Err. Disables auto_repro (no point probing a deliberately failing test).
survives_storm: boolWhen 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: boolWhen 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: boolWhen 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:
- FRAMEWORK level:
post_vmis skipped entirely when the guest already reported a failedAssertResult(see theguest_already_failedgate insrc/test_support/eval/mod.rs::run_ktstr_test_inner_impl’s post_vm dispatch site).post_vm_unconditionalbypasses that suppression and always runs. - CALLBACK level: the conventional
post_vmcallback body short-circuits viaif !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’sdefault_post_vm_periodic_firedis 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: u32Periodic 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: boolWhether 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
impl KtstrTestEntry
Sourcepub const DEFAULT: Self
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.
Sourcepub const fn new() -> Self
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.
Sourcepub fn all_include_files(&self) -> Vec<&'static str>
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.
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.
Sourcepub fn with_topology(self, topology: Topology) -> Self
pub fn with_topology(self, topology: Topology) -> Self
Override topology.
Sourcepub fn with_constraints(self, constraints: TopologyConstraints) -> Self
pub fn with_constraints(self, constraints: TopologyConstraints) -> Self
Override constraints.
Sourcepub fn with_memory_mib(self, memory_mib: u32) -> Self
pub fn with_memory_mib(self, memory_mib: u32) -> Self
Override memory_mib.
Sourcepub fn with_cpu_budget(self, cpu_budget: u32) -> Self
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.
Sourcepub fn without_cpu_budget(self) -> Self
pub fn without_cpu_budget(self) -> Self
Clear cpu_budget (auto-size the no-perf vCPU mask to the vCPU
count).
Sourcepub fn with_scheduler(self, scheduler: &'static Scheduler) -> Self
pub fn with_scheduler(self, scheduler: &'static Scheduler) -> Self
Override scheduler.
Sourcepub fn with_staged_schedulers(
self,
staged: &'static [&'static Scheduler],
) -> Self
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.
Sourcepub fn with_payload(self, payload: &'static Payload) -> Self
pub fn with_payload(self, payload: &'static Payload) -> Self
Override payload.
Sourcepub fn without_payload(self) -> Self
pub fn without_payload(self) -> Self
Clear payload (run a scheduler-only scenario with no primary
binary).
Sourcepub fn with_workloads(self, workloads: &'static [&'static Payload]) -> Self
pub fn with_workloads(self, workloads: &'static [&'static Payload]) -> Self
Override workloads.
Sourcepub fn with_auto_repro(self, auto_repro: bool) -> Self
pub fn with_auto_repro(self, auto_repro: bool) -> Self
Override auto_repro.
Sourcepub fn with_expect_auto_repro(self, expect_auto_repro: bool) -> Self
pub fn with_expect_auto_repro(self, expect_auto_repro: bool) -> Self
Override expect_auto_repro.
Sourcepub fn with_assert(self, assert: Assert) -> Self
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.
Sourcepub fn with_extra_sched_args(
self,
extra_sched_args: &'static [&'static str],
) -> Self
pub fn with_extra_sched_args( self, extra_sched_args: &'static [&'static str], ) -> Self
Override extra_sched_args.
Sourcepub fn with_watchdog_timeout(self, watchdog_timeout: Duration) -> Self
pub fn with_watchdog_timeout(self, watchdog_timeout: Duration) -> Self
Override watchdog_timeout.
Sourcepub fn with_bpf_map_write(
self,
bpf_map_write: &'static [&'static BpfMapWrite],
) -> Self
pub fn with_bpf_map_write( self, bpf_map_write: &'static [&'static BpfMapWrite], ) -> Self
Override bpf_map_write.
Sourcepub fn with_watch_bpf_maps(
self,
watch_bpf_maps: &'static [&'static WatchBpfMap],
) -> Self
pub fn with_watch_bpf_maps( self, watch_bpf_maps: &'static [&'static WatchBpfMap], ) -> Self
Override watch_bpf_maps.
Sourcepub fn with_performance_mode(self, performance_mode: bool) -> Self
pub fn with_performance_mode(self, performance_mode: bool) -> Self
Override performance_mode.
Sourcepub fn with_no_perf_mode(self, no_perf_mode: bool) -> Self
pub fn with_no_perf_mode(self, no_perf_mode: bool) -> Self
Override no_perf_mode.
Sourcepub fn with_duration(self, duration: Duration) -> Self
pub fn with_duration(self, duration: Duration) -> Self
Override duration.
Sourcepub fn with_expect_err(self, expect_err: bool) -> Self
pub fn with_expect_err(self, expect_err: bool) -> Self
Override expect_err.
Sourcepub fn with_survives_storm(self, survives_storm: bool) -> Self
pub fn with_survives_storm(self, survives_storm: bool) -> Self
Override Self::survives_storm.
Sourcepub fn with_allow_inconclusive(self, allow_inconclusive: bool) -> Self
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.
Sourcepub fn with_host_only(self, host_only: bool) -> Self
pub fn with_host_only(self, host_only: bool) -> Self
Override host_only.
Sourcepub fn with_extra_include_files(
self,
extra_include_files: &'static [&'static str],
) -> Self
pub fn with_extra_include_files( self, extra_include_files: &'static [&'static str], ) -> Self
Override extra_include_files.
Sourcepub fn with_cleanup_budget(self, cleanup_budget: Duration) -> Self
pub fn with_cleanup_budget(self, cleanup_budget: Duration) -> Self
Override cleanup_budget.
Sourcepub fn without_cleanup_budget(self) -> Self
pub fn without_cleanup_budget(self) -> Self
Clear cleanup_budget (leave the host watchdog as the only
guard).
Sourcepub fn with_config_content(self, config_content: &'static str) -> Self
pub fn with_config_content(self, config_content: &'static str) -> Self
Override config_content.
Sourcepub fn without_config_content(self) -> Self
pub fn without_config_content(self) -> Self
Clear config_content.
Sourcepub fn with_disk(self, disk: DiskConfig) -> Self
pub fn with_disk(self, disk: DiskConfig) -> Self
Override disk.
Pairs with the host_only = false requirement enforced by
Self::validate — host_only = true with a Some(..) disk
is rejected because host-only skips the VM boot that owns the
virtio-blk lifecycle.
Sourcepub fn with_networks(self, networks: &'static [NetConfig]) -> Self
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::validate — host_only = true with a non-empty list
is rejected because host-only skips the VM boot that owns the
virtio-net devices.
Sourcepub fn without_networks(self) -> Self
pub fn without_networks(self) -> Self
Clear networks (boot without a virtio-net device).
Sourcepub fn without_disk(self) -> Self
pub fn without_disk(self) -> Self
Clear disk (boot without a virtio-blk device).
Sourcepub fn with_post_vm(self, post_vm: PostVmCallback) -> Self
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.
Sourcepub fn without_post_vm(self) -> Self
pub fn without_post_vm(self) -> Self
Clear post_vm (skip the host-side callback).
Sourcepub fn with_post_vm_unconditional(
self,
post_vm_unconditional: PostVmCallback,
) -> Self
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.
Sourcepub fn without_post_vm_unconditional(self) -> Self
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.
Sourcepub fn with_num_snapshots(self, num_snapshots: u32) -> Self
pub fn with_num_snapshots(self, num_snapshots: u32) -> Self
Override num_snapshots.
Sourcepub const fn with_workload_root_cgroup(self, path: &'static str) -> Self
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
impl KtstrTestEntry
Sourcepub fn validate(&self) -> Result<()>
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:
namemust be non-empty (empty names collapse into each other in nextest output and in sidecar lookups).namemust not contain/or\(path separators embed in sidecar filenames and nextest test IDs; a separator would create a synthetic subdirectory in sidecar output and manglecargo nextest run -E 'test(name)'filtering).memory_mibmust be> 0(a VM with zero memory cannot boot).durationmust be> 0(a zero-duration run never exercises the scheduler and produces no telemetry).
Trait Implementations§
Source§impl Debug for KtstrTestEntry
impl Debug for KtstrTestEntry
Auto Trait Implementations§
impl Freeze for KtstrTestEntry
impl RefUnwindSafe for KtstrTestEntry
impl Send for KtstrTestEntry
impl Sync for KtstrTestEntry
impl Unpin for KtstrTestEntry
impl UnwindSafe for KtstrTestEntry
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
§impl<T> Instrument for T
impl<T> Instrument for T
§fn instrument(self, span: Span) -> Instrumented<Self>
fn instrument(self, span: Span) -> Instrumented<Self>
§fn in_current_span(self) -> Instrumented<Self>
fn in_current_span(self) -> Instrumented<Self>
Source§impl<T> IntoEither for T
impl<T> IntoEither for T
Source§fn into_either(self, into_left: bool) -> Either<Self, Self>
fn into_either(self, into_left: bool) -> Either<Self, Self>
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 moreSource§fn into_either_with<F>(self, into_left: F) -> Either<Self, Self>
fn into_either_with<F>(self, into_left: F) -> Either<Self, Self>
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