ktstr/
lib.rs

1// ctor 1.0's `#[ctor::ctor(...)]` macro expansion is a deep
2// TT-muncher whose recursion depth on this crate's ctor sites
3// exceeds Rust's default 128-frame macro-expansion budget. 256 is
4// what the rustc lint's own help message recommends; ctor itself
5// declares the same bump at the top of its lib.rs.
6#![recursion_limit = "256"]
7
8//! VM-based test framework for Linux kernel subsystems, with a focus on sched_ext.
9//!
10//! ktstr boots lightweight KVM virtual machines with controlled CPU topologies,
11//! runs scheduler test scenarios inside them, and evaluates results from the
12//! host via guest memory introspection. Each test creates cgroups, spawns
13//! worker processes, and checks that the scheduler handled the workload
14//! correctly. The same scenarios also run under the kernel's default
15//! EEVDF scheduler, so a test can baseline sched_ext behavior against
16//! stock scheduling.
17//!
18//! # Quick start
19//!
20//! Declare cgroups and workloads as data, let the framework handle
21//! lifecycle and checking:
22//!
23//! ```rust
24//! use ktstr::prelude::*;
25//!
26//! #[ktstr_test(llcs = 1, cores = 2, threads = 1)]
27//! fn my_scheduler_test(ctx: &Ctx) -> Result<AssertResult> {
28//!     execute_defs(ctx, vec![
29//!         CgroupDef::named("cg_0").workers(2),
30//!         CgroupDef::named("cg_1").workers(2),
31//!     ])
32//! }
33//! ```
34//!
35//! Requires a kernel image; see [`find_kernel()`] for the resolution chain.
36//!
37//! For multi-phase scenarios with dynamic topology changes:
38//!
39//! ```rust
40//! use ktstr::prelude::*;
41//!
42//! #[ktstr_test(llcs = 1, cores = 2, threads = 1)]
43//! fn my_dynamic_test(ctx: &Ctx) -> Result<AssertResult> {
44//!     let steps = vec![
45//!         Step::with_defs(
46//!             vec![CgroupDef::named("cg_0").workers(4)],
47//!             HoldSpec::frac(0.5),
48//!         ),
49//!         Step::new(
50//!             vec![Op::stop_cgroup("cg_0"), Op::remove_cgroup("cg_0")],
51//!             HoldSpec::frac(0.5),
52//!         ),
53//!     ];
54//!     execute_steps(ctx, steps)
55//! }
56//! ```
57//!
58//! # Scheduler definition
59//!
60//! Tests work with just topology parameters (as above). When multiple
61//! tests share a scheduler, use `declare_scheduler!` to declare it
62//! once with a binary, default topology, and any always-on args. Tests
63//! reference the generated const and inherit its configuration:
64//!
65//! ```rust,no_run
66//! use ktstr::prelude::*;
67//!
68//! declare_scheduler!(MY_SCHED, {
69//!     name = "my_sched",
70//!     binary = "scx_my_sched",
71//! });
72//!
73//! #[ktstr_test(scheduler = MY_SCHED)]
74//! fn basic(ctx: &Ctx) -> Result<AssertResult> {
75//!     execute_defs(ctx, vec![
76//!         CgroupDef::named("cg_0").workers(2),
77//!         CgroupDef::named("cg_1").workers(2),
78//!     ])
79//! }
80//! ```
81//!
82//! For full control over cgroup setup, worker spawning, and assertion
83//! you can use the low-level API directly:
84//!
85//! ```rust
86//! use ktstr::prelude::*;
87//!
88//! #[ktstr_test(llcs = 1, cores = 2, threads = 1)]
89//! fn my_low_level_test(ctx: &Ctx) -> Result<AssertResult> {
90//!     let mut group = CgroupGroup::new(ctx.cgroups);
91//!     group.add_cgroup_no_cpuset("workers")?;
92//!     let cpus = ctx.topo.all_cpuset();
93//!     ctx.cgroups.set_cpuset("workers", &cpus)?;
94//!
95//!     let cfg = WorkloadConfig {
96//!         num_workers: 2,
97//!         work_type: WorkType::SpinWait,
98//!         ..Default::default()
99//!     };
100//!     let mut handle = WorkloadHandle::spawn(&cfg)?;
101//!     ctx.cgroups.move_tasks("workers", &handle.worker_pids_for_cgroup_procs()?)?;
102//!     handle.start();
103//!
104//!     std::thread::sleep(ctx.duration);
105//!     let reports = handle.stop_and_collect();
106//!
107//!     let a = Assert::default_checks();
108//!     Ok(a.assert_cgroup(&reports, None))
109//! }
110//! ```
111//!
112//! For pointwise assertions against captured stats — the most direct
113//! way to express "this counter is at least N", "this rate is between
114//! A and B", "this metric is finite" — use `Verdict` +
115//! `#[derive(Claim)]` accessors and the [`claim!`] macro:
116//!
117//! ```rust
118//! use ktstr::prelude::*;
119//!
120//! // A test author would obtain `cg` and `report` from `ctx`-driven
121//! // execution; the literal here just illustrates the assertion shape.
122//! let cg = CgroupStats {
123//!     num_workers: 2,
124//!     num_cpus: 2,
125//!     max_gap_ms: 50,
126//!     p99_wake_latency_us: 25.0,
127//!     median_wake_latency_us: 10.0,
128//!     total_iterations: 5_000,
129//!     ..Default::default()
130//! };
131//! let work_units = 10_000u64;
132//! let throughput = work_units as f64 / 5.0;
133//!
134//! let mut v = Assert::default_checks().verdict();
135//! cg.claim_max_gap_ms(&mut v).at_most(100);          // typed CgroupStats accessor
136//! cg.claim_p99_wake_latency_us(&mut v).at_most(50.0);
137//! cg.claim_total_iterations(&mut v).at_least(1_000);
138//! claim!(v, work_units).at_least(5_000);             // local-binding label
139//! claim!(v, throughput).is_finite();                  // expression label
140//! claim!(v, cg.wake_latency_tail_ratio()).between(1.0, 5.0);
141//! let r = v.into_result();
142//! assert!(r.is_pass());
143//! ```
144//!
145//! Every claim is labeled by `stringify!` on either a struct field name
146//! (via the derive) or an identifier/expression (via the macro), so a
147//! rename or refactor updates the failure-message label automatically
148//! and a stale call site fails to compile. There is no manual-string
149//! escape hatch — by design, every label is source-text-grounded.
150//!
151//! Run with `cargo nextest run` (requires `/dev/kvm`).
152//!
153//! See the [`prelude`] module for the full set of re-exports.
154//!
155//! # Library usage
156//!
157//! Default install (full feature set — includes the installed
158//! `ktstr` / `cargo-ktstr` bins' deps):
159//!
160//! ```toml
161//! [dev-dependencies]
162//! ktstr = "0.23.0"
163//! ```
164//!
165//! Lean dev-dep (drops the host-tooling crates: tikv-jemallocator,
166//! clap_complete, tree-sitter, tree-sitter-c, base64):
167//!
168//! ```toml
169//! [dev-dependencies]
170//! ktstr = { version = "0.23.0", default-features = false }
171//! ```
172//!
173//! # Feature flags
174//!
175//! - **`cli-bins`** (default) — umbrella for deps used only by the
176//!   four `src/bin/*.rs` entry points and the matching test-binary
177//!   dispatch hooks. Pulls in `tikv-jemallocator`, `clap_complete`,
178//!   `tree-sitter`, `tree-sitter-c`, and the `export` feature.
179//! - **`export`** (pulled in by `cli-bins`) — gates
180//!   [`mod@export`] and the `cargo ktstr export` dispatch path in
181//!   the test binary. Drops `base64` from the manifest when off.
182//! - **`wprof`** — embed the wprof BPF tracer in shell-mode VMs.
183//!   First build clones `github.com/anakryiko/wprof` (requires git,
184//!   make, gcc, clang, elfutils-devel, zlib-devel).
185//! - **`pretty-labels`** — grex-based regex synthesis for
186//!   `ctprof_compare` display labels. With the feature off,
187//!   labels fall back to the deterministic join key.
188//! - **`remote-cache`** — GitHub Actions cache backend for blob
189//!   storage. CI-only; off-by-default. Pulls in `opendal` + minimal
190//!   `tokio` runtime.
191//! - **`integration`** — gates `resolve_func_ip` visibility for
192//!   integration tests.
193//!
194//! # Crate organization
195//!
196//! - [`cache`] -- kernel image cache (XDG directories, metadata, atomic writes)
197//! - [`cgroup`] -- cgroup v2 filesystem operations
198//! - [`cli`] -- shared helpers backing the `ktstr` and `cargo-ktstr` binaries
199//! - [`fetch`] -- kernel tarball and git source acquisition
200//! - [`flock`] -- advisory file-locking primitives used by cache + LLC reservations
201//! - [`kernel_path`] -- kernel ID parsing and filesystem image discovery
202//! - [`remote_cache`] -- GitHub Actions cache integration
203//! - [`scenario`] -- declarative ops API (`CgroupDef`, `Step`, `Op`, `Backdrop`, `execute_defs`, `execute_steps`, `execute_scenario`)
204//! - [`scenario::scenarios`] -- curated canned scenarios for common patterns
205//! - [`mod@assert`] -- pass/fail assertions (starvation, isolation, fairness)
206//! - [`test_support`] -- `#[ktstr_test]` runtime and registration
207//! - [`topology`] -- CPU topology abstraction (LLCs, NUMA nodes)
208//! - [`verifier`] -- BPF verifier log parsing, cycle detection, and output formatting
209//! - [`worker_ready`] / [`worker_ready_wait`] -- pid-scoped ready-marker the alloc/test worker writes once it is work-ready, polled (`worker_ready_wait`) before the probe is launched against it
210//! - [`workload`] -- worker process types and telemetry collection
211//!
212//! ## ctprof subsystem
213//!
214//! Per-thread + per-process runtime profile, captured via
215//! `ktstr ctprof capture` and compared via
216//! `ktstr ctprof compare`:
217//!
218//! - [`host_context`] -- one-shot host snapshot (kernel, CPU, memory, tunables)
219//! - [`host_heap`] -- jemalloc global heap counters (mallctl)
220//! - [`ctprof`] -- per-thread procfs walk + cumulative scheduling, I/O, page-fault, jemalloc TSD counters
221//! - [`ctprof_compare`] -- two-snapshot diff engine (group-by + delta tables)
222//!
223//! `host_thread_probe` (the ELF/DWARF + ptrace + `process_vm_readv`
224//! engine that pulls per-thread jemalloc TSD counters) is
225//! `pub(crate)`-only and consumed exclusively by `ctprof` plus
226//! the source-shared standalone `ktstr-jemalloc-probe` binary.
227//! Direct probe access from downstream is intentionally not part
228//! of the surface — scheduler authors get the captured counters
229//! through `ctprof::ThreadState`.
230//!
231//! Internal modules (not re-exported): `host_thread_probe` reads
232//! per-thread jemalloc TSD counters via ptrace, `monitor` reads
233//! live guest state, `probe` attaches BPF probes to traced
234//! functions, and `vmm` owns the KVM VM lifecycle.
235//!
236//! [`timeline`] is a public module (its `StimulusEvent` appears in
237//! `assert::build_phase_buckets_with_stimulus`'s signature and is
238//! re-exported in the [`prelude`]); it correlates stimulus events
239//! with monitor samples for phase-aligned reporting.
240
241// `#[derive(Payload)]` expands into `::ktstr::test_support::...`
242// paths so downstream crates can use it without a `use` import.
243// This alias lets the same derive be used inside the ktstr crate
244// itself — for example by doctests and by integration-test modules
245// under `tests/common/` that pull the derive through the same
246// public path downstream authors take. No runtime cost:
247// `extern crate self as ktstr` is a pure name-binding.
248extern crate self as ktstr;
249
250// Global allocator for every binary linking this crate — the shipped
251// bins (ktstr, cargo-ktstr, the jemalloc fixtures; all carry
252// `required-features = ["cli-bins"]`) and, since `integration` pulls
253// `cli-bins` (Cargo.toml), every `#[ktstr_test]` integration-test binary
254// (the framework packs it as the guest `/init` — vmm::initramfs
255// strip+packs the test binary as rdinit). The guest-`/init` use is
256// incidental, NOT a fix: a memory-constrained guest `/init` is kept alive
257// by `vm.overcommit_memory=1` on the guest cmdline
258// (vmm::setup::base_guest_cmdline), not by this allocator — the System
259// allocator boots the guest fine under that sysctl (verified by running
260// the shell-lifecycle suite with jemalloc disabled: 12/12 pass). Gated on
261// `cli-bins`, which provides `tikv-jemallocator`; lean
262// `default-features = false` library consumers (which never boot guests)
263// keep the System allocator. The bins/probe that previously declared
264// their own jemalloc now inherit this one; `jemalloc_alloc_worker`
265// keeps its own because it does not link this crate (pure `#[path]`).
266#[cfg(feature = "cli-bins")]
267#[global_allocator]
268static GLOBAL: tikv_jemallocator::Jemalloc = tikv_jemallocator::Jemalloc;
269
270// Defense-in-depth for the e2e jemalloc-fixture invariant: `integration`
271// pulls `cli-bins` (Cargo.toml), so the jemalloc introspection fixtures
272// (`ktstr-jemalloc-probe` / `-alloc-worker`, gated on cli-bins +
273// integration) build when their e2es do — those e2es
274// `env!(CARGO_BIN_EXE_…)` the fixture bins. If the implication is ever
275// severed the fixtures don't build and their e2es fail to compile. Fail
276// the build loudly with the cause rather than ship the foot-gun.
277#[cfg(all(feature = "integration", not(feature = "cli-bins")))]
278compile_error!(
279    "feature `integration` requires `cli-bins`: the jemalloc \
280     introspection fixtures (ktstr-jemalloc-probe / -alloc-worker) are \
281     gated on cli-bins and their e2es env!(CARGO_BIN_EXE_…) them. Build \
282     with `--features integration` (which pulls cli-bins) or add cli-bins."
283);
284
285#[allow(
286    clippy::all,
287    dead_code,
288    non_camel_case_types,
289    non_snake_case,
290    non_upper_case_globals
291)]
292mod bpf_skel;
293
294#[cfg(test)]
295#[macro_use]
296mod test_macros;
297
298/// Shared guidance for every `#[non_exhaustive]` type in this
299/// crate. Individual types link here instead of repeating the
300/// same migration rules in every doc block.
301///
302/// # `#[non_exhaustive]` conventions in ktstr
303///
304/// Most of ktstr's public structs and enums carry `#[non_exhaustive]`
305/// so that adding a field or variant is not a breaking change for
306/// downstream crates. The attribute has two consequences downstream
307/// consumers must account for:
308///
309/// ## Pattern matching
310///
311/// Matches on a `#[non_exhaustive]` struct or enum from outside this
312/// crate must end with a wildcard `..` (for structs) or `_ =>` arm
313/// (for enums). Without it, a future addition to the type forces
314/// every matcher into a compile break even when the new field or
315/// variant is irrelevant to the caller.
316///
317/// ```ignore
318/// // Good: `..` absorbs future fields.
319/// if let MyStruct { name, .. } = value { /* ... */ }
320/// match my_enum {
321///     MyEnum::A => {}
322///     MyEnum::B => {}
323///     _ => {}          // absorbs future variants
324/// }
325/// ```
326///
327/// ## Construction
328///
329/// Cross-crate consumers **cannot** use any struct-expression form
330/// for a `#[non_exhaustive]` struct — bare literals
331/// (`MyStruct { name: "x", .. }`) and functional-update spreads
332/// (`MyStruct { name: "x", ..Default::default() }`) are both
333/// rejected by the compiler (E0639). Construction must go through
334/// one of:
335///
336/// 1. A dedicated constructor (`MyStruct::new(...)`,
337///    `MyStruct::from_*(...)`) exposed by the defining crate.
338/// 2. A [`Default`] instance followed by field mutation, when the
339///    type derives `Default`.
340/// 3. A named `test_fixture` or equivalent associated function for
341///    types that expose a populated baseline instead of the
342///    all-default minimum.
343///
344/// The per-type doc picks whichever of these the type actually
345/// supports; see [`host_context::HostContext`],
346/// [`host_heap::HostHeapState`], and the Op/CpusetSpec docs in
347/// [`scenario::ops`] for worked examples across the different
348/// shapes.
349///
350/// ## Pattern matching inside this crate
351///
352/// `#[non_exhaustive]` is enforced only across crate boundaries.
353/// In-crate matchers can remain exhaustive (and should, so the
354/// compiler flags forgotten variants at the definition site), and
355/// in-crate struct-literal construction still works for the tests
356/// and fixtures that live alongside the type.
357#[doc(hidden)]
358pub mod non_exhaustive {}
359
360pub mod cache;
361pub mod cgroup;
362pub mod flock;
363
364/// Map a raw errno value to its C constant name.
365///
366/// Returns `None` for unrecognized values. [`nix::errno::Errno`] has
367/// `#[derive(Debug)]`, but `format!("{:?}", e)` allocates a fresh
368/// `String` on every call; the hand-rolled match below returns a
369/// `&'static str` pointing at a literal instead. [`nix::errno::Errno`]
370/// is used here to gate unknown errnos via
371/// `matches!(e, UnknownErrno)`. Adding a new errno means extending
372/// both nix's port-constants table (for the UnknownErrno gate) and
373/// this match; the test suite pins a representative subset so a
374/// stale arm surfaces at build time.
375pub(crate) fn errno_name(errno: i32) -> Option<&'static str> {
376    let e = nix::errno::Errno::from_raw(errno);
377    if matches!(e, nix::errno::Errno::UnknownErrno) {
378        return None;
379    }
380    // Hand-rolled match: returns a `&'static str` pointing at a
381    // literal, avoiding the allocation that `format!("{:?}", e)` would
382    // incur. Callers that compare these against string literals in
383    // error formatting paths rely on the stable symbolic names below.
384    Some(match e {
385        nix::errno::Errno::EPERM => "EPERM",
386        nix::errno::Errno::ENOENT => "ENOENT",
387        nix::errno::Errno::ESRCH => "ESRCH",
388        nix::errno::Errno::EINTR => "EINTR",
389        nix::errno::Errno::EIO => "EIO",
390        nix::errno::Errno::ENXIO => "ENXIO",
391        nix::errno::Errno::E2BIG => "E2BIG",
392        nix::errno::Errno::ENOEXEC => "ENOEXEC",
393        nix::errno::Errno::EBADF => "EBADF",
394        nix::errno::Errno::ECHILD => "ECHILD",
395        nix::errno::Errno::EAGAIN => "EAGAIN",
396        nix::errno::Errno::ENOMEM => "ENOMEM",
397        nix::errno::Errno::EACCES => "EACCES",
398        nix::errno::Errno::EFAULT => "EFAULT",
399        nix::errno::Errno::EBUSY => "EBUSY",
400        nix::errno::Errno::EEXIST => "EEXIST",
401        nix::errno::Errno::ENODEV => "ENODEV",
402        nix::errno::Errno::ENOTDIR => "ENOTDIR",
403        nix::errno::Errno::EISDIR => "EISDIR",
404        nix::errno::Errno::EINVAL => "EINVAL",
405        nix::errno::Errno::ENFILE => "ENFILE",
406        nix::errno::Errno::EMFILE => "EMFILE",
407        nix::errno::Errno::ENOSPC => "ENOSPC",
408        nix::errno::Errno::ESPIPE => "ESPIPE",
409        nix::errno::Errno::EROFS => "EROFS",
410        nix::errno::Errno::EPIPE => "EPIPE",
411        nix::errno::Errno::EDOM => "EDOM",
412        nix::errno::Errno::ERANGE => "ERANGE",
413        nix::errno::Errno::EDEADLK => "EDEADLK",
414        nix::errno::Errno::ENAMETOOLONG => "ENAMETOOLONG",
415        nix::errno::Errno::ENOSYS => "ENOSYS",
416        nix::errno::Errno::ENOTEMPTY => "ENOTEMPTY",
417        nix::errno::Errno::ELOOP => "ELOOP",
418        nix::errno::Errno::ENOTSUP => "ENOTSUP",
419        nix::errno::Errno::EADDRINUSE => "EADDRINUSE",
420        nix::errno::Errno::ECONNREFUSED => "ECONNREFUSED",
421        nix::errno::Errno::ETIMEDOUT => "ETIMEDOUT",
422        // Other well-defined constants exist on nix::errno::Errno
423        // but were not in the previous curated list. Return None for
424        // them to preserve the prior contract — callers that want
425        // more coverage can extend this match explicitly.
426        _ => return None,
427    })
428}
429
430/// Read the kernel ring buffer (equivalent to `dmesg --notime`).
431/// Exposed as `pub` so scenario tests that need to assert on
432/// kernel-log content (e.g. the sched_ext stall duration emitted
433/// by `scx_exit(SCX_EXIT_ERROR_STALL)` in `kernel/sched/ext.c`)
434/// can read the same buffer the framework captures into
435/// `AssertResult::details` on scheduler-died failures.
436pub fn read_kmsg() -> String {
437    match rmesg::log_entries(rmesg::Backend::Default, false) {
438        Ok(entries) => entries
439            .iter()
440            .map(|e| e.message.as_str())
441            .collect::<Vec<_>>()
442            .join("\n"),
443        Err(_) => String::new(),
444    }
445}
446
447/// Forward the guest's `/dev/kmsg` ring buffer to the host over the
448/// bulk port, so a host-side post_vm callback can read it via
449/// `VmResult::guest_kmsg`. Mirrors `read_kmsg`: that reads the ring
450/// buffer in the guest process; this forwards the bytes (typically
451/// `read_kmsg().as_bytes()`) to the host. The scx_exit
452/// SCX_EXIT_ERROR_STALL printk lands in /dev/kmsg but is suppressed
453/// from the COM1 console at the default `loglevel=0`, so it never
454/// reaches `VmResult::stderr`; this forward is the only host-visible
455/// path to it.
456pub fn send_kmsg(buf: &[u8]) {
457    crate::vmm::guest_comms::send_dmesg(buf);
458}
459
460/// The host watchdog-override readback, surfaced via
461/// `VmResult::watchdog_observation`. Hidden from rustdoc at its
462/// definition, so this re-export adds no public doc surface.
463pub use crate::monitor::WatchdogObservation;
464
465pub mod assert;
466pub(crate) mod budget;
467pub(crate) mod cargo_test_mode;
468pub mod cli;
469pub mod cpu_util;
470pub mod ctprof;
471pub mod ctprof_compare;
472pub(crate) mod elf_strip;
473#[cfg(feature = "export")]
474pub mod export;
475pub mod fetch;
476pub mod fun;
477pub mod host_context;
478pub mod host_heap;
479pub(crate) mod host_thread_probe;
480pub mod kernel_path;
481// build_helpers.rs is `include!`d into build.rs at build-script
482// compile time. Mounting it as a `#[cfg(test)]` mod here lets its
483// unit tests run under `cargo nextest` / `cargo ktstr test`
484// without exposing the helper on the public API (the `mod` is
485// test-only; `cargo build` doesn't compile it as a lib module).
486#[cfg(test)]
487mod build_helpers;
488pub mod metric_types;
489pub(crate) mod monitor;
490pub(crate) mod probe;
491pub(crate) mod report;
492pub mod scenario;
493pub(crate) mod stats;
494pub(crate) mod taskstats;
495pub mod test_support;
496// `pub` (not `pub(crate)`): `assert::build_phase_buckets_with_stimulus`
497// takes `timeline::StimulusEvent` in its public signature, and result
498// analyzers (post_vm callbacks folding `VmResult::stimulus_timeline()`
499// through that fn) need to name the type.
500pub mod timeline;
501pub mod topology;
502
503/// Public surface for the live-host introspection pipeline.
504///
505/// Re-exports from the otherwise-internal `monitor` module so the
506/// live-host capture binary, integration tests, and downstream
507/// consumers can invoke the bpf()-syscall data path, kernel
508/// auto-discovery, kallsyms parser, and dmesg-scx parser without the
509/// `monitor` module's frozen-VM internals leaking into the public API.
510///
511/// This module is the entry point for binaries and tests that
512/// consume the live-host capture pipeline.
513pub mod live_host {
514    pub use crate::monitor::bpf_map::{
515        BPF_MAP_TYPE_ARENA, BPF_MAP_TYPE_ARRAY, BPF_MAP_TYPE_HASH, BPF_MAP_TYPE_PERCPU_ARRAY,
516        BpfMapAccessor, BpfMapInfo,
517    };
518    pub use crate::monitor::bpf_syscall::BpfSyscallAccessor;
519    pub use crate::monitor::dmesg_scx::{
520        ScxExitEvent, ScxExitKind, StackSymbol, extract_stack_symbols, parse_kmsg_window,
521    };
522    pub use crate::monitor::live_host_kernel::{KallsymsTable, LiveHostKernelEnv, uname_release};
523    pub use crate::monitor::timeline::{
524        DEFAULT_SNAPSHOT_RING_DEPTH, IncrementalCapture, IncrementalSnapshot, SnapshotRing,
525        TimelineCapture, TimelineEvent, TimelineEventRaw, parse_timeline_buf,
526        parse_timeline_record, tl_evt,
527    };
528}
529
530#[cfg(feature = "remote-cache")]
531pub mod remote_cache;
532
533/// No-op stub for the `remote_cache` module when the `remote-cache`
534/// feature is disabled. Exposes the same three entry points consumed
535/// elsewhere in the crate — `is_enabled()`, `remote_lookup()`,
536/// `remote_store()` — so call sites in `cli/resolve.rs` and
537/// `cli/kernel_build/build.rs` compile and behave correctly without
538/// any `#[cfg]` gating at the call site: `is_enabled()` always
539/// returns false, the lookup/store entry points are unreachable in
540/// practice, and the stubs satisfy the trait surface so type-checking
541/// stays uniform across feature configurations.
542#[cfg(not(feature = "remote-cache"))]
543pub mod remote_cache {
544    use crate::cache::{CacheDir, CacheEntry};
545
546    pub fn is_enabled() -> bool {
547        false
548    }
549
550    pub fn remote_lookup(
551        _cache: &CacheDir,
552        _cache_key: &str,
553        _cli_label: &str,
554    ) -> Option<CacheEntry> {
555        None
556    }
557
558    pub fn remote_store(_entry: &CacheEntry, _cli_label: &str) {}
559}
560pub mod gauntlet;
561pub(crate) mod reflink;
562pub(crate) mod sync;
563#[cfg(any(feature = "export", feature = "remote-cache"))]
564pub(crate) mod tar_util;
565pub mod verifier;
566pub(crate) mod vmm;
567pub mod worker_ready;
568
569#[cfg(feature = "wprof")]
570pub use vmm::wprof::{WPROF_MIN_MEMORY_MIB, apply_wprof_memory_floor};
571
572/// Re-export of [`test_support::runtime::bypass_llc_locks_active`]
573/// so the bin/cargo_ktstr + bin/ktstr CLI surfaces (separate
574/// crates linking against this lib) can apply the canonical
575/// empty-string-aware bypass check at their parse-time
576/// `--cpu-cap` conflict guards. Mirrors the in-crate readers at
577/// vmm/builder.rs + cli/kernel_build/build.rs.
578pub use test_support::runtime::bypass_llc_locks_active;
579
580/// Pre-populate the on-disk cast analysis cache for a scheduler binary.
581///
582/// Called by cargo-ktstr before spawning nextest so test processes
583/// find a warm cache instead of each independently running the 30s
584/// analysis. Safe to call from a background thread — the function
585/// is idempotent (content-hash-keyed) and writes atomically.
586pub fn precompute_cast_analysis(path: &std::path::Path) {
587    vmm::cast_analysis_load::cached_cast_analysis_for_scheduler(path);
588}
589pub mod worker_ready_wait;
590pub mod workload;
591
592/// Contents of `ktstr.kconfig` (the kernel-config fragment that
593/// enables sched_ext, BPF, kprobes, cgroups, and the other options
594/// ktstr requires) baked into the binary at build time via
595/// `include_str!`. Consumed by the kernel build pipeline to
596/// `olddefconfig` a kernel source tree, and used to derive the
597/// cache key suffix so a kconfig change produces a fresh cache
598/// entry.
599pub const EMBEDDED_KCONFIG: &str = include_str!("../ktstr.kconfig");
600
601/// CRC32 hash of the embedded kconfig fragment (8 hex chars).
602pub fn kconfig_hash() -> String {
603    format!("{:08x}", crc32fast::hash(EMBEDDED_KCONFIG.as_bytes()))
604}
605
606/// CRC32 hash (8 hex chars) of a user-supplied `--extra-kconfig`
607/// fragment, hashed verbatim.
608///
609/// Hashes raw bytes — no comment stripping, no CRLF
610/// canonicalization. Two semantically-equivalent inputs with
611/// different comments or line endings produce different hashes and
612/// therefore land at distinct cache entries — accept the disk waste
613/// in exchange for byte-deterministic cache discrimination.
614pub fn extra_kconfig_hash(extra: &str) -> String {
615    format!("{:08x}", crc32fast::hash(extra.as_bytes()))
616}
617
618/// Cache key suffix derived from the embedded kconfig fragment.
619/// Used in kernel cache keys so a kconfig change produces a distinct
620/// cache entry. The kernel binary is independent of ktstr userspace
621/// source, so no ktstr or consumer build identity feeds this suffix.
622pub fn cache_key_suffix() -> String {
623    kconfig_hash()
624}
625
626/// Two-segment cache key suffix accounting for an optional
627/// `--extra-kconfig` fragment.
628///
629/// The suffix uses TWO segments instead of folding both inputs into
630/// one hash:
631///
632/// - `extra = None` → `kconfig_hash()` only — byte-identical to
633///   [`cache_key_suffix`], so paths that don't expose
634///   `--extra-kconfig` (test / coverage / shell / verifier) keep
635///   resolving the existing keyspace and pre-1.0 cached kernels are
636///   not orphaned.
637/// - `extra = Some(content)` → `{kconfig_hash()}-xkc{extra_hash}`,
638///   making `kernel list` self-describing: a reader can see at a
639///   glance which entries carry user extras and which are pure
640///   baked-in builds. Different extra content yields different
641///   `xkc{...}` segments, so cache discrimination across distinct
642///   `--extra-kconfig` invocations is structural rather than
643///   collapsed into a single opaque hash.
644pub fn cache_key_suffix_with_extra(extra: Option<&str>) -> String {
645    match extra {
646        None => kconfig_hash(),
647        Some(content) => format!("{}-xkc{}", kconfig_hash(), extra_kconfig_hash(content)),
648    }
649}
650
651/// Merge the user-supplied `--extra-kconfig` fragment on top of
652/// [`EMBEDDED_KCONFIG`] for the configure pass. Returns a
653/// [`std::borrow::Cow`] so the no-extras branch borrows `baked`
654/// without allocating; only the `Some` branch heaps the merged
655/// String.
656///
657/// The user fragment is appended AFTER the baked-in fragment so
658/// kbuild's last-wins rule
659/// (`scripts/kconfig/confdata.c::conf_read_simple` —
660/// "If conflicting CONFIG options are given from an input file,
661/// the last one wins.") makes user values override baked-in ones
662/// on conflict.
663///
664/// A single `\n` separator is interleaved between the two
665/// fragments. EMBEDDED_KCONFIG ends in a newline today, so the
666/// interleaved `\n` produces a blank line between the segments —
667/// kbuild's `.config` parser ignores blank lines (every
668/// `if (!line[0])` short-circuit in `conf_read_simple`), so the
669/// blank line is harmless. The separator is mandatory for the
670/// adversarial case where the operator hand-crafts an
671/// EMBEDDED_KCONFIG without a trailing newline AND a user
672/// fragment that starts with `CONFIG_X` — without the
673/// interleaved `\n`, the two would concatenate into a single
674/// malformed line. Always emit the separator so the merge is
675/// safe regardless of either side's terminator.
676///
677/// The production configure path in
678/// [`crate::cli::kernel_build_pipeline`] calls this helper to build
679/// the bytes handed to `configure_kernel`. Tests that assert
680/// merge-ordering invariants call it directly so the production
681/// byte sequence is what kbuild's last-wins rule operates on.
682/// (Note: [`cache_key_suffix_with_extra`] hashes `extra` ALONE for
683/// its `xkc{...}` segment — it doesn't pass through this helper —
684/// so the cache-key suffix and the merged-fragment content evolve
685/// independently. The cache-key segment exists to discriminate
686/// extras-vs-no-extras at the cache layer; the merge ordering
687/// exists to give kbuild the right final value.)
688pub fn merge_kconfig_fragments<'a>(
689    baked: &'a str,
690    extra: Option<&str>,
691) -> std::borrow::Cow<'a, str> {
692    match extra {
693        None => std::borrow::Cow::Borrowed(baked),
694        Some(content) => std::borrow::Cow::Owned(format!("{baked}\n{content}")),
695    }
696}
697
698// Derive macros. `Payload` here is the `#[derive(Payload)]` proc
699// macro; the same-named `Payload` struct (to which the derive
700// applies) lives at `crate::test_support::Payload`. Rust's
701// macro-vs-type namespace separation lets both coexist under the
702// identifier `Payload` in `use ktstr::prelude::*;` — the derive
703// position resolves to the macro, type position resolves to the
704// struct.
705pub use ktstr_macros::Claim;
706pub use ktstr_macros::Payload;
707pub use ktstr_macros::declare_scheduler;
708pub use ktstr_macros::json;
709pub use ktstr_macros::ktstr_test;
710
711/// Internal re-exports for proc-macro-generated code. Not public API.
712///
713/// Grouped into a single hidden module so that `use ktstr::*;` pulls
714/// in one module name instead of two leading-underscore items.
715/// Consumers of `#[ktstr_test]` should not reference anything under
716/// this path — the `#[ktstr_test]` macro registers via
717/// `::ktstr::distributed_slice` / `::ktstr::linkme`; the `ctor` /
718/// `serde_json` re-exports here serve downstream test-author code
719/// (pre-`main()` setup and sidecar parsing) and ktstr's own
720/// declarative-ctor sites, and the set may change without notice.
721/// (`linkme` lives at the public crate root —
722/// [`ktstr::linkme`](crate::linkme) — since the macro now emits the
723/// public path.)
724#[doc(hidden)]
725pub mod __private {
726    pub use ctor;
727    pub use serde_json;
728}
729
730#[cfg(feature = "integration")]
731pub use crate::probe::process::resolve_func_ip;
732
733/// The `linkme` crate, re-exported as part of ktstr's public surface
734/// so downstream code can reference it via [`ktstr::linkme`](crate::linkme)
735/// in the `#[linkme(crate = ...)]` annotation that
736/// [`distributed_slice`] registrations
737/// require — without having to add `linkme` as a direct Cargo
738/// dependency. See [`distributed_slice`]
739/// for the usage pattern.
740pub use ::linkme;
741
742/// `linkme::distributed_slice` re-exported as part of ktstr's public
743/// surface. Combined with [`crate::linkme`] for the
744/// `#[linkme(crate = ...)]` annotation, this lets a downstream crate
745/// register entries into
746/// [`KTSTR_TESTS`](crate::test_support::KTSTR_TESTS) or
747/// [`KTSTR_SCHEDULERS`](crate::test_support::KTSTR_SCHEDULERS)
748/// without adding `linkme` as a direct Cargo dependency:
749///
750/// ```ignore
751/// use ktstr::prelude::*;
752///
753/// fn my_test_fn(_ctx: &Ctx) -> Result<AssertResult> {
754///     Ok(AssertResult::pass())
755/// }
756///
757/// #[distributed_slice(KTSTR_TESTS)]
758/// #[linkme(crate = ktstr::linkme)]
759/// static MY_ENTRY: KtstrTestEntry = KtstrTestEntry {
760///     name: "my_test",
761///     func: my_test_fn,
762///     ..KtstrTestEntry::DEFAULT
763/// };
764/// ```
765///
766/// The `#[linkme(crate = ...)]` annotation is REQUIRED because the
767/// `linkme` proc-macro expansion hardcodes `::linkme::DistributedSlice`
768/// — without the annotation, downstream crates without `linkme` in
769/// their `Cargo.toml` get an unresolved-import error.
770/// The annotation tells the macro to resolve type references through
771/// `ktstr::linkme` instead, which IS reachable from downstream by
772/// transitive dependency.
773///
774/// Downstream crates that already depend on `linkme = "0.3"` directly
775/// can omit the annotation. The `#[ktstr_test]` proc macro emits both
776/// attributes internally so test authors using the standard macro
777/// surface never have to spell either out.
778pub use linkme::distributed_slice;
779
780/// Re-exports for writing `#[ktstr_test]` functions.
781///
782/// ```rust
783/// use ktstr::prelude::*;
784///
785/// #[ktstr_test(llcs = 1, cores = 2, threads = 1)]
786/// fn my_test(ctx: &Ctx) -> Result<AssertResult> {
787///     Ok(AssertResult::pass())
788/// }
789/// ```
790///
791/// For curated canned scenarios, see [`scenario::scenarios`].
792pub mod prelude {
793    pub use anyhow::Result;
794
795    // `Scheduler` is the `test_support::Scheduler` struct — the
796    // scheduler-definition record test authors build via the
797    // `declare_scheduler!` macro.
798    // The `#[derive(Claim)]`-generated `<Type>Claim` extension traits carry the
799    // typed `claim_<field>(&mut verdict)` accessors. Every derive(Claim) stats
800    // type is preluded, so its Claim trait is preluded alongside it and
801    // `use ktstr::prelude::*` makes the accessors callable without a per-type
802    // trait import: the four assert-module traits below, plus `WorkerReportClaim`
803    // in the workload export block.
804    pub use crate::assert::{
805        AbsoluteThresholds, Assert, AssertDetail, AssertResult, COMPARATOR_VOCABULARY, CgroupStats,
806        CgroupStatsClaim, ClaimBuilder, DetailKind, EachClaim, FracPair, InfoNote,
807        MAX_RECORDED_PASSES, NoteValue, Outcome, OutcomeRef, PASSES_TRUNCATION_SENTINEL_COMPARATOR,
808        PASSES_TRUNCATION_SENTINEL_NAME, PassDetail, PhaseBucket, PhaseBucketClaim,
809        PhaseCgroupStats, PhaseCgroupStatsClaim, PhaseMapExt, ScenarioStats, ScenarioStatsClaim,
810        SeqClaim, SeriesField, SetClaim, Verdict, assert_scx_events_clean, assert_thresholds,
811        build_phase_buckets_with_stimulus,
812    };
813    // Per-phase-metric building blocks for `post_vm` callbacks doing
814    // custom per-phase assertions: `StimulusEvent` is the timeline event
815    // type `build_phase_buckets_with_stimulus` consumes, and
816    // `VmResult::stimulus_timeline()` returns a `Vec<StimulusEvent>`
817    // (step frames + scenario-end terminal) ready to fold through it.
818    // The non-stimulus sibling `assert::build_phase_buckets` is
819    // INTENTIONALLY not preluded: it groups by the raw bridge-stamped
820    // step_index, which can collapse under a deferred-fire burst (see
821    // `SampleSeries::by_stamped_phase`); the stimulus-aware variant above
822    // is the collapse-immune common path, so the prelude surfaces only
823    // it. The plain variant remains reachable by full path for the rare
824    // no-stimulus-timeline case.
825    pub use crate::cgroup::CgroupManager;
826    pub use crate::claim;
827    pub use crate::claim_present;
828    pub use crate::declare_scheduler;
829    pub use crate::distributed_slice;
830    pub use crate::host_context::HostContext;
831    pub use crate::host_heap::HostHeapState;
832    pub use crate::ktstr_test;
833    pub use crate::scenario::backdrop::Backdrop;
834    pub use crate::scenario::ops::{
835        CgroupDef, CpusetSpec, HoldSpec, IrqSelector, KernelTarget, KernelValue, KernelValueWidth,
836        Op, Setup, SpawnPlacement, Step, execute_defs, execute_scenario, execute_scenario_with,
837        execute_steps, execute_steps_with,
838    };
839    pub use crate::scenario::payload_run::{PayloadHandle, PayloadRun};
840    pub use crate::scenario::scenarios;
841    pub use crate::test_support::post_vm_skip;
842    pub use crate::timeline::StimulusEvent;
843    // Snapshot accessor surface and the underlying report shapes
844    // a test author needs to inspect the captured BTF-rendered
845    // bytes. The renderer types come from monitor::btf_render and
846    // monitor::dump (otherwise crate-private modules); re-exported
847    // here so an out-of-crate caller can build synthetic
848    // FailureDumpReports for unit-testing their assertions
849    // against the snapshot accessor without booting a VM.
850    //
851    // Re-export of the `Payload` derive macro from the crate root.
852    // The same identifier names the `Payload` struct re-exported a
853    // few lines below from `crate::test_support`; the two live in
854    // separate Rust namespaces (macro vs type) so they coexist in
855    // `use ktstr::prelude::*;` without conflict.
856    pub use crate::Payload;
857    pub use crate::monitor::arena::{ArenaPage, ArenaSnapshot};
858    pub use crate::monitor::bpf_prog::ProgRuntimeStats;
859    pub use crate::monitor::btf_render::{RenderedMember, RenderedValue};
860    pub use crate::monitor::dump::{
861        DegradedFailureDumpReport, DualFailureDumpReport, EventCounterSample,
862        FailureDumpArrayEntry, FailureDumpEntry, FailureDumpFdArray, FailureDumpMap,
863        FailureDumpPercpuEntry, FailureDumpPercpuHashEntry, FailureDumpReport,
864        FailureDumpReportAny, FailureDumpRingbuf, FailureDumpStackTrace,
865        FailureDumpStackTraceEntry, PerCpuTimeStats, PerNodeNumaStats, ProbeBssCounters,
866        REASON_DEGRADED_RENDEZVOUS_TIMEOUT, SCHEMA_DEGRADED, SCHEMA_DUAL, SCHEMA_SINGLE,
867        SNAPSHOT_TAG_EARLY_DEGRADED, SNAPSHOT_TAG_EARLY_ONLY_LATE_NEVER_FIRED,
868        SNAPSHOT_TAG_EARLY_ONLY_LATE_SUPPRESSED, SNAPSHOT_TAG_EARLY_PRE_LATE_DEGRADED,
869    };
870    pub use crate::monitor::scx_walker::{DsqState, RqScxState, ScxSchedState};
871    pub use crate::monitor::task_enrichment::TaskEnrichment;
872    pub use crate::scenario::sample::{
873        BpfMapCpuProjector, BpfMapProjector, Sample, SampleSeries, StatsPathProjector, StatsValue,
874    };
875    pub use crate::scenario::snapshot::{
876        BridgeGuard, CaptureCallback, CgroupProcsSnapshot, JsonField, MAX_WATCH_SNAPSHOTS,
877        Snapshot, SnapshotBridge, SnapshotEntry, SnapshotError, SnapshotField, SnapshotMap,
878        SnapshotResult, WatchRegisterCallback, pickers, stats_path,
879    };
880    pub use crate::scenario::{CgroupGroup, Ctx, collect_all, spawn_diverse};
881    // `Payload` in this group is the struct on which
882    // `#[derive(Payload)]` is applied; it occupies the type
883    // namespace, distinct from the derive macro re-exported above.
884    pub use crate::test_support::{
885        BpfMapAgg, BpfMapWrite, CgroupPath, EXIT_FAIL, EXIT_INCONCLUSIVE, EXIT_PASS,
886        KTSTR_SCHEDULERS, KTSTR_TESTS, KtstrTestEntry, MemSideCache, Metric, MetricCheck,
887        MetricHint, MetricStream, NumaDistance, NumaNode, OutputFormat, Payload, PayloadKind,
888        PayloadMetrics, PerfDeltaAssertion, Polarity, Scheduler, SchedulerSpec, SidecarResult,
889        Sysctl, Topology, TopologyConstraints, WatchBpfMap, extract_metrics, find_scheduler,
890        find_test, sidecar_dir,
891    };
892    // The following items are intentionally NOT in the prelude. They
893    // are binary-entry helpers (the `ktstr` / `cargo-ktstr` bins) or
894    // macro-generated glue the `#[ktstr_test]` expansion consumes —
895    // audiences distinct from the test-author surface this module
896    // provides. Import directly from `ktstr::test_support::<item>`
897    // when needed:
898    // `newest_run_dir`, `runs_root`, `analyze_sidecars`, `ktstr_main`,
899    // `ktstr_test_early_dispatch`, `run_ktstr_test`,
900    // `resolve_scheduler`, `resolve_test_kernel`.
901    //
902    // `build_nodemask` (the low-level `set_mempolicy(2)` / `mbind(2)`
903    // bitmask builder) is also excluded: test authors express NUMA
904    // placement through the `MemPolicy` enum, not raw nodemask
905    // construction. The helper itself lives in the crate-private
906    // `workload::spawn` submodule with a `pub(crate)` re-export at
907    // `crate::workload::build_nodemask` for `vmm::host_topology`
908    // internal use.
909    pub use crate::topology::{LlcInfo, NodeMemInfo, TestTopology};
910    pub use crate::vmm::{VirtioBlkCountersSnapshot, VirtioNetCountersSnapshot};
911    // `VmResult` is the host-side return value from booting a VM.
912    // Surfaced for `#[ktstr_test(post_vm = ...)]` callbacks: the
913    // hook signature is `fn(&VmResult) -> anyhow::Result<()>`, and
914    // a test author writing the callback needs the type in scope
915    // to declare the parameter.
916    pub use crate::vmm::VmResult;
917    pub use crate::vmm::disk_config::{
918        DiskConfig, DiskThrottle, DiskThrottleValidationError, Filesystem, ThrottleDimension,
919    };
920    pub use crate::vmm::net_config::NetConfig;
921    // Surfaced for `post_vm` callbacks that drain the snapshot
922    // bridge's per-tag kernel-op reply log via
923    // `VmResult::snapshot_bridge::drain_kernel_ops`: the returned
924    // `Vec<(String, KernelOpReplyPayload)>` carries `read_values`
925    // of `KernelOpValue` variants the callback pattern-matches to
926    // assert on a read-back u32 / u64 / Bytes payload from
927    // `Op::ReadKernel{Hot,Cold}`. Mirrors the existing exports for
928    // `VirtioBlkCountersSnapshot` etc. — observability types the
929    // post_vm contract requires in scope.
930    pub use crate::scenario::host_stall::{StallDiagnostic, StallReport};
931    pub use crate::vmm::wire::{KernelOpReplyPayload, KernelOpValue};
932    pub use crate::workload::{
933        AffinityIntent, AluWidth, CloneMode, CustomCfg, CustomFn, FutexLockMode, MemPolicy,
934        Migration, MpolFlags, ReapMode, ResolvedAffinity, SchbenchConfig, SchedClass, SchedPolicy,
935        TaobenchConfig, TaobenchStats, WakeMechanism, WorkPhase, WorkSpec, WorkType,
936        WorkTypeValidationError, WorkerCtx, WorkerReport, WorkerReportClaim, WorkloadConfig,
937        WorkloadHandle,
938    };
939    // Surface `Phase` from the assert module (the scenario-step
940    // bucket) so test authors can write `Phase::step(0)` /
941    // `Phase::baseline()` without disambiguating against the
942    // formerly-named workload variant. The workload's compound-
943    // pattern enum is now `WorkPhase` (above) so `Phase` alone
944    // unambiguously means the scenario-phase bucket type users
945    // reach for in `field.value_at_phase(Phase::step(0))` style.
946    pub use crate::assert::Phase;
947    // Typed built-in metric ids (the discoverable, typo-proof catalog) + the
948    // `MetricId` hybrid that ALSO accepts a scheduler-runtime string — both flow
949    // through every metric accessor via `impl Into<MetricId>`. A misspelled
950    // built-in is a compile error, not a silent `None`.
951    pub use crate::stats::{BuiltinMetric, MetricId};
952}
953
954/// # KTSTR_* env-var empty-string contract
955///
956/// Default policy across `KTSTR_*` env vars: **empty string is
957/// treated as unset** (falls back to the same default the var
958/// would use if absent). This prevents the "stale shell export"
959/// footgun where an operator's previous `KTSTR_FOO=...` export
960/// gets cleared by the new shell (`KTSTR_FOO=`) but the empty
961/// value is still observed by child processes — without the
962/// empty-as-unset rule, child code would see "set" via
963/// `env::var(...).is_ok()` and try to use the empty value as
964/// data, producing confusing failures far from the export site.
965///
966/// Per-const docs flag deviations from this default explicitly:
967/// presence-only markers (e.g. [`KTSTR_ORCHESTRATED_ENV`]) treat
968/// empty as set per documented contract; value-typed vars (e.g.
969/// path overrides like [`KTSTR_HOST_CGROUP_PARENT_ENV`]) follow
970/// the empty-as-unset default and surface the fallback at the
971/// resolver site.
972///
973/// New `KTSTR_*` env vars must pick a policy at the const-decl
974/// doc and the reader site must honor it; mixed empty-treatment
975/// within one var is a footgun.
976///
977/// Name of the environment variable that selects a kernel for every
978/// ktstr entry point (`ktstr run`, `ktstr shell`, `cargo ktstr test`,
979/// in-process tests, post-run analysis). Single source of truth so
980/// the name is not spelled by hand at each reader; if the name ever
981/// changes, the change lands in one place instead of fanning out to
982/// every call site.
983pub const KTSTR_KERNEL_ENV: &str = "KTSTR_KERNEL";
984
985/// Name of the environment variable that carries the multi-kernel
986/// fan-out list across the `cargo ktstr` → `cargo nextest` → test-
987/// binary boundary. Format: `label1=path1;label2=path2;…` (semicolon
988/// entry separator, `=` separates label from absolute kernel-dir
989/// path). Empty / unset means "single-kernel mode" — the test binary
990/// honours `KTSTR_KERNEL_ENV` directly.
991///
992/// Set by `cargo ktstr test --kernel A --kernel B` (or any
993/// `--kernel` value that expands to ≥ 2 entries — repeated
994/// `--kernel` flags, or a single `--kernel START..END` range that
995/// expands to multiple stable releases via
996/// [`crate::kernel_path::KernelId::Range`]) before the `exec` into
997/// `cargo nextest`. Read by the test binary's `--list` /
998/// `--exact` handlers in `crate::test_support::dispatch` to fan
999/// the gauntlet across kernels: each (test × scenario × topology ×
1000/// kernel) tuple becomes a distinct nextest test case so
1001/// nextest's parallelism, retries, and `-E` filtering work
1002/// natively. Per-variant subprocesses re-export `KTSTR_KERNEL` to
1003/// the kernel directory selected by the test name's `kernel_…`
1004/// suffix.
1005///
1006/// `KTSTR_KERNEL_ENV` is always set in tandem (to the first entry's
1007/// path) so downstream code that reads `KTSTR_KERNEL` directly —
1008/// budget-listing's vmlinux probe in `dispatch.rs` for example —
1009/// still observes a valid kernel even when running under multi-
1010/// kernel mode.
1011///
1012/// Single source of truth so the name is not spelled by hand at
1013/// each reader; if the name ever changes, the change lands in one
1014/// place instead of fanning out to every call site.
1015pub const KTSTR_KERNEL_LIST_ENV: &str = "KTSTR_KERNEL_LIST";
1016
1017/// Name of the environment variable cargo-ktstr sets to a
1018/// `dir=commit;dir=commit;...` map of each resolved SOURCE kernel's
1019/// short commit hash (with a `-dirty` suffix when the tree is dirty),
1020/// keyed by the same directory string exported in [`KTSTR_KERNEL_ENV`]
1021/// / [`KTSTR_KERNEL_LIST_ENV`].
1022///
1023/// cargo-ktstr probes each kernel's git HEAD ONCE in the orchestrator
1024/// and records it here. The sidecar writer reads this map and looks
1025/// itself up (by its own `KTSTR_KERNEL` dir) instead of re-running a
1026/// gix HEAD read + dirty-walk over the kernel tree in every per-test
1027/// nextest process. That walk is memoized per process but NOT across
1028/// processes, so without this map each of N test processes re-pays the
1029/// full dirty-walk (seconds on a large kernel checkout).
1030///
1031/// Optimization only: a missing entry, absent env, or decode failure
1032/// falls back to the in-process resolve-and-walk, which is always
1033/// correct. Kernels with no recoverable source tree (transient
1034/// Range/Git specs, or a Version/CacheKey cache miss) are absent from
1035/// the map, and the fallback yields the same `None` for them.
1036pub const KTSTR_KERNEL_COMMIT_ENV: &str = "KTSTR_KERNEL_COMMIT";
1037
1038/// Name of the environment variable cargo-ktstr's perf-delta sets to the
1039/// PROJECT tree's short commit hash (with a `-dirty` suffix when the tree
1040/// is dirty) that the child's sidecars must record as their
1041/// `project_commit`. Unlike [`KTSTR_KERNEL_COMMIT_ENV`] this is a single
1042/// value, not a `dir=commit` map — a child has exactly one project tree.
1043///
1044/// perf-delta computes the A/B commit labels ONCE in the orchestrator
1045/// (`short_hash`) and both the pool FILTER and the two run children read
1046/// the same value: the filter partitions on it, and each child records it
1047/// verbatim (the sidecar writer's `detect_project_commit` returns this
1048/// value when the env is set), so the recorded `project_commit` and the
1049/// filter can never diverge on the `-dirty` suffix. It also lets the
1050/// baseline run, whose tree is a plain gix checkout with no `.git`, skip a
1051/// `gix::discover` that would otherwise resolve to the wrong repo (or none).
1052///
1053/// Override only: an absent or empty env falls back to the in-process
1054/// `gix::discover` + dirty-walk, which is correct for a normal (non
1055/// perf-delta) test run.
1056pub const KTSTR_PROJECT_COMMIT_ENV: &str = "KTSTR_PROJECT_COMMIT";
1057
1058/// Name of the environment variable cargo-ktstr sets to signal
1059/// "this test process was launched by a cargo-ktstr orchestration
1060/// path, not raw `cargo nextest`". cargo-ktstr's `test` and
1061/// `verifier` subcommands set it to `"1"` before spawning the
1062/// nextest child; the value content does not matter, only the
1063/// presence — `std::env::var(KTSTR_ORCHESTRATED_ENV).is_ok()`.
1064///
1065/// Tests that boot real KVM VMs (`src/vmm/*` integration tests)
1066/// use this signal to skip when an operator runs the test binary
1067/// directly via `cargo nextest run --lib`. Raw nextest fans
1068/// 7000+ tests at full host parallelism, which starves the
1069/// per-VM resource budgets these tests depend on (KVM page
1070/// allocation, vCPU thread scheduling, freeze rendezvous timing).
1071/// Failure shape is `kill set by AP` + watchdog-deadline timeout
1072/// shortly after VM start. cargo-ktstr's orchestrator constrains
1073/// the VM-test concurrency so the budgets hold; raw nextest
1074/// doesn't, so the skip surfaces operator-error (wrong runner)
1075/// rather than dismissing a real bug.
1076///
1077/// `KTSTR_KERNEL_ENV` alone is not sufficient: an operator may
1078/// have it set in their shell from a prior cargo-ktstr session
1079/// and then invoke raw nextest. The dedicated orchestration
1080/// marker discriminates the two cases.
1081pub const KTSTR_ORCHESTRATED_ENV: &str = "KTSTR_ORCHESTRATED";
1082
1083/// Name of the environment variable carrying the `cargo ktstr test`
1084/// SESSION EPOCH: nanoseconds since the Unix epoch, stamped ONCE by
1085/// the orchestrator (`cargo_ktstr::run_cargo`) before it spawns
1086/// nextest, inherited by every per-test child process.
1087///
1088/// `test_support::sidecar::pre_clear_run_dir_once` uses it as an
1089/// opaque per-invocation session token: nextest is process-per-test
1090/// and every test sharing one `{kernel}-{project_commit}` run
1091/// directory would otherwise have a later process's pre-clear delete
1092/// an earlier peer's freshly-written `{test}-{hash}.ktstr.json`
1093/// (silent stats loss). The first process to clear a dir records
1094/// this token in a `.ktstr_run_epoch` sentinel; a later peer whose
1095/// token matches skips its wipe, sparing the peers' sidecars. Unset
1096/// under raw `cargo nextest run` (no orchestrator) — pre-clear then
1097/// falls back to its per-process wipe-everything behavior.
1098pub const KTSTR_RUN_EPOCH_ENV: &str = "KTSTR_RUN_EPOCH";
1099
1100/// Name of the environment variable that pins the sidecar runs-root
1101/// to an ABSOLUTE path, overriding the CWD-relative
1102/// `{CARGO_TARGET_DIR or "target"}/ktstr` default of
1103/// [`test_support::runs_root`].
1104///
1105/// The `cargo ktstr` orchestrator (`cargo-ktstr` main) stamps this
1106/// once at startup to the cargo target dir's `ktstr` subdir (resolved
1107/// via `cargo metadata`), so its post-run footer / `stats` / `replay`
1108/// reads AND the child test processes' sidecar writes resolve the
1109/// SAME directory regardless of CWD. Without it, in a Cargo workspace
1110/// the test binaries (CWD = package dir, set by nextest) write to
1111/// `{package}/target/ktstr` while the orchestrator (CWD = invocation
1112/// dir) scans elsewhere, so the post-run footer finds nothing.
1113///
1114/// Set ONCE by the parent and inherited by every child test process —
1115/// children never re-run `cargo metadata` (it would be one subprocess
1116/// spawn per test process on the hot path). Unset under raw `cargo
1117/// nextest run` (no orchestrator): [`test_support::runs_root`] falls
1118/// back to its CWD-relative default, which is fine because raw nextest
1119/// has no footer to mismatch.
1120pub const KTSTR_RUNS_ROOT_ENV: &str = "KTSTR_RUNS_ROOT";
1121
1122/// Name of the environment variable that overrides the default
1123/// host-mode cgroup parent (where `host_only` tests' workload
1124/// cgroups land). Empty / unset falls back to the canonical
1125/// default; a non-empty value must be rooted under
1126/// `/sys/fs/cgroup` and name a non-root subdirectory.
1127///
1128/// Single source of truth so the name is not spelled by hand at
1129/// each reader. Mirrors the sibling `KTSTR_KERNEL_ENV` /
1130/// `KTSTR_KERNEL_LIST_ENV` / `KTSTR_KERNEL_PARALLELISM_ENV` /
1131/// `KTSTR_VERIFIER_RAW_ENV` / `KTSTR_ORCHESTRATED_ENV`
1132/// constant-defined naming convention; a single grep across
1133/// `KTSTR_*_ENV` consts gives the operator the complete env-var
1134/// inventory.
1135///
1136/// Read by `crate::test_support::dispatch::resolve_host_cgroup_parent`.
1137pub const KTSTR_HOST_CGROUP_PARENT_ENV: &str = "KTSTR_HOST_CGROUP_PARENT";
1138
1139/// Name of the environment variable that overrides the cgroup-fs
1140/// root [`crate::cgroup::CgroupManager::setup`] walks down from when
1141/// enabling controllers in every ancestor's `cgroup.subtree_control`.
1142/// Empty / unset falls back to `/sys/fs/cgroup` (the canonical
1143/// cgroup-v2 mount). Non-empty value must be a prefix of
1144/// [`KTSTR_HOST_CGROUP_PARENT_ENV`]'s configured parent so the walk
1145/// stays inside the directory the operator owns; values that do not
1146/// satisfy the prefix invariant are rejected upfront by
1147/// [`crate::cgroup::CgroupManager::with_walk_root`].
1148///
1149/// Exists for cgroup-v2 user delegation (Mode B/C: systemd
1150/// `Delegate=yes`, container `nsdelegate`): the operator owns
1151/// `subtree_control` writes only inside the delegated subtree, and a
1152/// blind walk from `/sys/fs/cgroup` down would EACCES at the
1153/// `user.slice` / container-root boundary. Setting the walk root to
1154/// the delegation boundary makes the setup-time controller-enable
1155/// walk stop there.
1156///
1157/// # Empty-string contract
1158///
1159/// Empty string is observationally identical to unset: both fall
1160/// back to `/sys/fs/cgroup` (the [`crate::cgroup::CgroupManager::new`]
1161/// default). This mirrors the sibling env vars; a shell that exports
1162/// `KTSTR_CGROUP_WALK_ROOT=` (without a value) explicitly opts back
1163/// into the default rather than passing an empty path down to
1164/// [`crate::cgroup::CgroupManager::with_walk_root`] (which would
1165/// always fail the prefix invariant).
1166///
1167/// Single source of truth so the name is not spelled by hand at each
1168/// reader. Mirrors the sibling [`KTSTR_HOST_CGROUP_PARENT_ENV`]
1169/// constant-defined naming convention; a single grep across
1170/// `KTSTR_*_ENV` consts gives the operator the complete env-var
1171/// inventory.
1172///
1173/// Read by `crate::test_support::dispatch::resolve_host_cgroup_parent`.
1174pub const KTSTR_CGROUP_WALK_ROOT_ENV: &str = "KTSTR_CGROUP_WALK_ROOT";
1175
1176/// Name of the environment variable that overrides the poll cadence
1177/// (in milliseconds) of the host-mode stall monitor in
1178/// [`crate::scenario::host_stall`].
1179///
1180/// The monitor runs in a background thread and samples
1181/// `/proc/<pid>/sched` every N ms for every worker pid the scenario
1182/// spawned; W consecutive samples with `Δnr_switches == 0` AND
1183/// `Δsum_exec_runtime == 0` flip the stall predicate. Default
1184/// cadence is 500 ms × W=4 = 2 s detection latency.
1185///
1186/// # **Empty = unset** (also: `0` / unparseable)
1187///
1188/// Empty / unset / `0` / unparseable falls back to the default
1189/// ([`crate::scenario::host_stall::DEFAULT_POLL_INTERVAL_MS`]).
1190/// Mirrors the empty-as-unset contract documented on the sibling
1191/// `KTSTR_*_ENV` constants so a shell `KTSTR_STALL_POLL_MS=` quirk
1192/// silently degrades to default behavior rather than poisoning the
1193/// poller with a zero interval (which would either busy-loop or be
1194/// no-op-rejected).
1195///
1196/// Read once at `crate::scenario::host_stall::spawn_monitor` when the
1197/// scenario engine spawns the monitor; mid-scenario env mutations
1198/// are NOT observed by the running thread.
1199///
1200/// Single source of truth so the name is not spelled by hand at
1201/// each reader. Mirrors the sibling [`KTSTR_HOST_CGROUP_PARENT_ENV`]
1202/// constant-defined naming convention; a single grep across
1203/// `KTSTR_*_ENV` consts gives the operator the complete env-var
1204/// inventory.
1205pub const KTSTR_STALL_POLL_MS_ENV: &str = "KTSTR_STALL_POLL_MS";
1206
1207/// Name of the environment variable that overrides the rayon
1208/// pool width used by `cargo ktstr`'s `resolve_kernel_set` to
1209/// fan out per-spec kernel resolves (download / git-clone /
1210/// build) in parallel. Default cap is `available_parallelism()`
1211/// — the host's logical CPU count — chosen so download streams
1212/// do not outnumber threads the host can drive without
1213/// thrashing a contended local network (kernel.org CDN
1214/// per-IP throttle, developer ISP, CI shared NIC).
1215///
1216/// Operators override when the default is wrong for their
1217/// environment: a fast NIC + slow CPU benefits from raising
1218/// the cap above logical-CPU count to keep more downloads
1219/// in flight; a contended CI runner with concurrent jobs
1220/// benefits from lowering it to 1 or 2 to leave bandwidth
1221/// for siblings; a multi-version `--kernel A..Z` resolve on
1222/// a workstation may want a hand-tuned middle value to
1223/// balance throughput against background load.
1224///
1225/// Parsed as `usize`; 0 and unparseable values fall through
1226/// to the default cap so a typoed export does not silently
1227/// disable parallelism. Leading/trailing whitespace is trimmed
1228/// before parsing so a shell-quoted `=" 8 "` behaves the same
1229/// as the unquoted form. Read by
1230/// [`crate::cli::resolve_kernel_parallelism`] (the helper
1231/// that combines this env value with the
1232/// `available_parallelism()` fallback) so the parsing rules
1233/// live in one place.
1234///
1235/// Single source of truth so the name is not spelled by hand at
1236/// each reader; if the name ever changes, the change lands in one
1237/// place instead of fanning out to every call site.
1238pub const KTSTR_KERNEL_PARALLELISM_ENV: &str = "KTSTR_KERNEL_PARALLELISM";
1239
1240/// Name of the environment variable that switches the `cargo ktstr
1241/// verifier` per-cell handler from the cycle-collapsed default
1242/// rendering to a raw scheduler-log dump. Set to any value (the
1243/// presence of the variable is what matters; the value is ignored)
1244/// by the dispatcher in `src/bin/cargo_ktstr/verifier.rs` when the
1245/// operator passes `--raw`, and read by
1246/// `crate::test_support::dispatch::run_verifier_cell` before
1247/// formatting via [`crate::verifier::format_verifier_output`].
1248///
1249/// Single source of truth so the name is not spelled by hand at
1250/// each reader; if the name ever changes, the change lands in one
1251/// place instead of fanning out to every call site.
1252pub const KTSTR_VERIFIER_RAW_ENV: &str = "KTSTR_VERIFIER_RAW";
1253
1254/// Name of the environment variable carrying the directory that each
1255/// `cargo ktstr verifier` cell writes its per-cell PASS/FAIL record to.
1256/// The `cargo ktstr verifier` dispatcher creates the dir, exports this
1257/// var (inherited by the spawned `cargo nextest run` and thus by every
1258/// cell process), and after nextest returns reads the records back to
1259/// render the per-(topology × scheduler) summary grid. Unset
1260/// when a verifier cell runs outside the dispatcher (a hand-driven
1261/// `--exact verifier/...`): the cell then simply skips the record write.
1262/// Single source of truth so the name is not spelled by hand at the
1263/// writer (cell) and reader (dispatcher) ends.
1264pub const KTSTR_VERIFIER_RESULT_DIR_ENV: &str = "KTSTR_VERIFIER_RESULT_DIR";
1265
1266/// Name of the environment variable carrying the operator's
1267/// `cargo ktstr verifier --scheduler <NAME>` filter. Set by the
1268/// dispatcher in `src/bin/cargo_ktstr/verifier.rs`; read by
1269/// `crate::test_support::dispatch`'s verifier cell emission, which
1270/// skips every declared scheduler whose `name` does not equal the value
1271/// so the sweep runs one scheduler across topologies instead of the
1272/// full declared-scheduler matrix. Unset, every declared scheduler is
1273/// swept. Single source of truth so the writer (dispatcher) and reader
1274/// (emission) do not spell the name by hand.
1275pub const KTSTR_VERIFIER_SCHEDULER_ENV: &str = "KTSTR_VERIFIER_SCHEDULER";
1276
1277/// Name of the environment variable that forces ktstr to skip the
1278/// `perf_event_open` access check + the
1279/// `perf_event_paranoid`-relaxation gate. Read at scenario-engine
1280/// startup ([`crate::test_support::runtime`]) and by the
1281/// `cargo ktstr shell` / verifier dispatch sites that disable
1282/// perf collection when the operator passes `--no-perf-mode`.
1283///
1284/// **Empty = unset** per the default contract — empty value is
1285/// treated as not set. The canonical reader at
1286/// `crate::test_support::runtime::no_perf_mode_active` uses
1287/// `.map(|v| !v.is_empty()).unwrap_or(false)` after a prior
1288/// regression where CI shells exporting `KTSTR_NO_PERF_MODE=`
1289/// silently disabled perf mode for every `performance_mode` test.
1290/// Any non-empty value (`"1"`, `"yes"`, `"0"`, `"true"`) enables
1291/// no-perf-mode. All readers (shell-mode VM builder in
1292/// `lib.rs`, verifier dispatch in `verifier.rs`, dispatch
1293/// gauntlet + eval entry in `test_support/dispatch.rs` and
1294/// `test_support/eval/mod.rs`)
1295/// route through the canonical helper so the empty-string
1296/// contract holds uniformly.
1297pub const KTSTR_NO_PERF_MODE_ENV: &str = "KTSTR_NO_PERF_MODE";
1298
1299/// Name of the environment variable that restricts a run to ONLY
1300/// `performance_mode` tests: when set to a non-empty value, every
1301/// test whose entry does not have `performance_mode` is skipped
1302/// (skip sidecar recorded, libtest sees pass) before any VM boot.
1303/// The mergebase perf-delta subcommand sets this so a regression run
1304/// measures only the tests configured for clean performance numbers;
1305/// an explicit nextest `-E` filter narrows further within the
1306/// perf-mode set.
1307///
1308/// **Empty = unset** per the default contract, matching
1309/// [`KTSTR_NO_PERF_MODE_ENV`]. The canonical reader
1310/// `test_support::runtime::perf_only_active` (pub(crate)) uses
1311/// `.map(|v| !v.is_empty()).unwrap_or(false)` so a stray
1312/// `KTSTR_PERF_ONLY=` pass-through does not silently skip every
1313/// non-perf test. Readers route through the helper (dispatch
1314/// gauntlet + named routes in `test_support/dispatch.rs` and the
1315/// eval entry in `test_support/eval/mod.rs`).
1316pub const KTSTR_PERF_ONLY_ENV: &str = "KTSTR_PERF_ONLY";
1317
1318/// Name of the environment variable that enables the GitHub Actions
1319/// remote-cache backend in [`crate::remote_cache`]. Read at cache-
1320/// init time; value-typed — only the exact string `"1"` enables;
1321/// unset / empty / any other value (including `"true"`, `"yes"`,
1322/// `"0"`, `"false"`) is disabled. Set explicitly by GitHub Actions
1323/// workflows when the runner has cache-API credentials; absent in
1324/// dev environments where local-only caching is the right default.
1325pub const KTSTR_GHA_CACHE_ENV: &str = "KTSTR_GHA_CACHE";
1326
1327/// Name of the environment variable that signals ktstr is running
1328/// in "cargo test" mode (raw test binary launched by cargo's test
1329/// harness, no orchestrator). Distinct from
1330/// [`KTSTR_ORCHESTRATED_ENV`] which marks cargo-ktstr orchestration;
1331/// `KTSTR_CARGO_TEST_MODE` is for narrower cases like in-process
1332/// VMM tests that adapt their resource budgets when run via
1333/// `cargo test` / `cargo nextest`. Read via
1334/// `crate::cargo_test_mode::cargo_test_mode_active`: treats
1335/// unset and empty as disabled; ANY non-empty value enables —
1336/// no trim, no special-case strings (`"0"` and `"false"` ENABLE
1337/// because they're non-empty).
1338pub const KTSTR_CARGO_TEST_MODE_ENV: &str = "KTSTR_CARGO_TEST_MODE";
1339
1340/// Name of the environment variable that overrides ktstr's cache
1341/// root directory (kernel-build cache, btf-anchor cache, blob
1342/// cache, etc.). Empty / unset falls back to the per-user default
1343/// (typically `$HOME/.cache/ktstr`). Heavy test usage —
1344/// `crate::test_support::test_helpers::IsolatedCacheDir` sets it
1345/// to a temp dir per-test so cache reads don't leak host state
1346/// into the test, and post-test the original value is restored
1347/// via `crate::test_support::test_helpers::EnvVarGuard`.
1348pub const KTSTR_CACHE_DIR_ENV: &str = "KTSTR_CACHE_DIR";
1349
1350/// Name of the environment variable that overrides ktstr's flock
1351/// directory for inter-process resource locking (cpuset / LLC
1352/// reservation locks). Empty / unset falls back to the hardcoded
1353/// `/tmp` default at `crate::cache::resolve::resolve_lock_dir`
1354/// (the literal string, not `std::env::temp_dir()` / `TMPDIR`
1355/// resolution — historical default kept for stability). Used by
1356/// tests + CI environments that need isolated lock-dirs.
1357pub const KTSTR_LOCK_DIR_ENV: &str = "KTSTR_LOCK_DIR";
1358
1359/// Name of the environment variable that triggers verbose logging
1360/// in the VMM setup phase. Strict `v == "1"` semantics (only the
1361/// literal `"1"` enables; unset / empty / any other value —
1362/// including `"true"`, `"yes"`, `"0"` — is disabled). Read in
1363/// `crate::vmm::setup` at the two cmdline-assembly sites (one per
1364/// arch: x86_64 and aarch64); both readers identical.
1365pub const KTSTR_VERBOSE_ENV: &str = "KTSTR_VERBOSE";
1366
1367/// Name of the environment variable that bypasses LLC resource
1368/// locks at scenario setup (test_support::dispatch / cargo-ktstr
1369/// shell). Set by the `--bypass-llc-locks` CLI flag.
1370///
1371/// Reader sites use the canonical
1372/// [`crate::bypass_llc_locks_active`] helper (re-export of
1373/// [`test_support::runtime::bypass_llc_locks_active`]) which
1374/// applies the empty-string-as-unset contract uniformly across
1375/// all 7 callers (vmm/builder.rs, cli/kernel_build/build.rs ×2,
1376/// bin/ktstr.rs ×2, bin/cargo_ktstr/{kernel/mod, misc/shell}).
1377pub const KTSTR_BYPASS_LLC_LOCKS_ENV: &str = "KTSTR_BYPASS_LLC_LOCKS";
1378
1379/// Name of the environment variable that caps the host CPU count
1380/// the scenario engine sees, for testing scaling logic without a
1381/// real CPU narrowing. Read at
1382/// `crate::vmm::host_topology` and set by the `--cpu-cap` CLI
1383/// flag in the bin entry points. Empty falls back to the
1384/// host's actual CPU count; non-empty numeric value caps the
1385/// observed count.
1386pub const KTSTR_CPU_CAP_ENV: &str = "KTSTR_CPU_CAP";
1387
1388/// Name of the environment variable that bypasses the contention
1389/// guard at scenario setup. Strict `v == "1"` semantics (only
1390/// the literal `"1"` enables; everything else disables). Used
1391/// by tests that need to provoke contention scenarios without
1392/// the production guard kicking in.
1393pub const KTSTR_CONTENTION_BYPASS_ENV: &str = "KTSTR_CONTENTION_BYPASS";
1394
1395/// Name of the environment variable cargo-ktstr's test
1396/// dispatcher sets to disable the skip-on-contention test
1397/// behavior. Presence check via `var_os(...).is_some()` — set
1398/// to "1" by `cargo ktstr test --no-skip-mode`, absent
1399/// otherwise.
1400///
1401/// **Deviates from the contract-default empty-as-unset rule**:
1402/// `var_os` does not distinguish empty from non-empty, so
1403/// `KTSTR_NO_SKIP_MODE=` (empty) ENABLES the bypass. Same
1404/// shape as [`KTSTR_GUEST_INIT_ENV`].
1405pub const KTSTR_NO_SKIP_MODE_ENV: &str = "KTSTR_NO_SKIP_MODE";
1406
1407/// Name of the environment variable that overrides the per-test
1408/// budget in seconds for VM-boot dispatch. Empty / unset falls
1409/// back to the dispatcher's default. Parsed as f64 seconds at
1410/// `crate::test_support::dispatch` (accepts fractional values
1411/// like `2.5`); invalid or non-positive values surface a warn
1412/// and the default applies.
1413pub const KTSTR_BUDGET_SECS_ENV: &str = "KTSTR_BUDGET_SECS";
1414
1415/// Name of the environment variable that overrides the sidecar
1416/// output directory (the per-test `*.ktstr.json` write target).
1417/// Empty / unset falls back to the per-test
1418/// `target/ktstr/<run-id>` location. Read at
1419/// `crate::test_support::sidecar` +
1420/// `crate::cli::stats_cmds::dispatch` (the stats reader).
1421pub const KTSTR_SIDECAR_DIR_ENV: &str = "KTSTR_SIDECAR_DIR";
1422
1423/// Name of the environment variable that overrides the scheduler
1424/// binary path test_support::eval uses for in-process scheduler
1425/// dispatch. Read at `crate::test_support::eval`. This is the COARSE
1426/// (global) override: it applies to EVERY `SchedulerSpec::Discover`
1427/// scheduler regardless of name, so a test declaring multiple distinct
1428/// schedulers can't point them at different binaries through it — use
1429/// the per-name variant ([`per_name_scheduler_env`]) for that.
1430///
1431/// Resolution precedence: the per-name override is checked FIRST; this
1432/// global var is the fallback when no per-name var is set; if neither
1433/// resolves, the cascade falls through to the workspace build.
1434pub const KTSTR_SCHEDULER_ENV: &str = "KTSTR_SCHEDULER";
1435
1436/// Per-scheduler-NAME override environment variable for a
1437/// `SchedulerSpec::Discover(name)` scheduler:
1438/// `KTSTR_SCHEDULER_BIN_<NAME>`, where `<NAME>` is the discover name
1439/// uppercased with every non-alphanumeric character replaced by `_`
1440/// (e.g. `scx_layered` -> `KTSTR_SCHEDULER_BIN_SCX_LAYERED`,
1441/// `scx-ktstr` -> `KTSTR_SCHEDULER_BIN_SCX_KTSTR`).
1442///
1443/// The `BIN` infix keeps the per-name namespace disjoint from the
1444/// `KTSTR_SCHEDULER_*` meta-variables ([`KTSTR_SCHEDULER_ENV`] = the
1445/// global override, [`KTSTR_SCHEDULER_PROFILE_ENV`] = the build
1446/// profile): without it a scheduler named `profile` would derive
1447/// `KTSTR_SCHEDULER_PROFILE` and shadow the build-profile selector.
1448/// `-` and `_` both map to `_` (env-var names can't contain `-`), so
1449/// `scx-foo` and `scx_foo` derive the same var — not a practical
1450/// ambiguity, as a scheduler is referred to by one canonical spelling
1451/// per run.
1452///
1453/// Checked BEFORE the global [`KTSTR_SCHEDULER_ENV`] in the Discover
1454/// resolution cascade, so a test that declares several distinct
1455/// Discover schedulers (one `entry.scheduler` plus staged schedulers)
1456/// can point each at its own pre-built binary. The global var remains
1457/// the coarse fallback for the common single-scheduler case. A set
1458/// per-name var whose path does not exist falls through to the global
1459/// var and then the build cascade (lenient, matching the global var's
1460/// own missing-path behavior).
1461pub fn per_name_scheduler_env(name: &str) -> String {
1462    let suffix: String = name
1463        .chars()
1464        .map(|c| {
1465            if c.is_ascii_alphanumeric() {
1466                c.to_ascii_uppercase()
1467            } else {
1468                '_'
1469            }
1470        })
1471        .collect();
1472    format!("KTSTR_SCHEDULER_BIN_{suffix}")
1473}
1474
1475/// Name of the environment variable that overrides the kernel
1476/// path the eval dispatch reads (orthogonal to
1477/// [`KTSTR_KERNEL_ENV`] which the main entry points use). Read
1478/// at `crate::test_support::eval::resolve_test_kernel`: a
1479/// set-but-empty `KTSTR_TEST_KERNEL=` surfaces a
1480/// `KTSTR_TEST_KERNEL not found:` hard error (typo-loud per
1481/// reader comment); ONLY the unset / `Err(NotPresent)` case
1482/// falls through to `crate::find_kernel()` (cache + sysroot
1483/// probes), which themselves fall through to a
1484/// `KernelUnavailable` error hinting at both
1485/// `KTSTR_TEST_KERNEL` and `KTSTR_KERNEL`.
1486pub const KTSTR_TEST_KERNEL_ENV: &str = "KTSTR_TEST_KERNEL";
1487
1488/// Name of the environment variable cargo-ktstr's spawn-pipeline
1489/// sets to "1" inside the guest-side init binary so the binary
1490/// detects it's running as the guest init. Presence check via
1491/// `var_os(...).is_none()` at `crate::workload::spawn` —
1492/// absent in host-side dispatch.
1493///
1494/// **Deviates from the contract-default empty-as-unset rule**:
1495/// `var_os` does not distinguish empty from non-empty, so
1496/// `KTSTR_GUEST_INIT=` (empty) is observed as SET and disables
1497/// the orphan-detection fast-path. Same shape as
1498/// [`KTSTR_NO_SKIP_MODE_ENV`].
1499pub const KTSTR_GUEST_INIT_ENV: &str = "KTSTR_GUEST_INIT";
1500
1501/// Name of the environment variable that points at a probe binary
1502/// for jemalloc-feature detection. Empty / unset leaves the probe
1503/// binary unwired (the [`crate::test_support::runtime`]
1504/// builder-wiring site calls `.jemalloc_probe_binary()` only on
1505/// set+non-empty — there is no `which`-based fallback). Tests
1506/// that need the probe set this var via `#[ctor]` before the
1507/// harness runs (see `tests/jemalloc_probe_tests.rs`).
1508pub const KTSTR_JEMALLOC_PROBE_BINARY_ENV: &str = "KTSTR_JEMALLOC_PROBE_BINARY";
1509
1510/// Name of the environment variable that points at a worker
1511/// binary for jemalloc allocation-probe runs. Empty / unset
1512/// leaves the worker binary unwired — same shape as
1513/// [`KTSTR_JEMALLOC_PROBE_BINARY_ENV`]; the
1514/// [`crate::test_support::runtime`] builder-wiring site calls
1515/// `.jemalloc_alloc_worker_binary()` only on set+non-empty, no
1516/// `which`-based fallback. Set alongside the probe via `#[ctor]`
1517/// in `tests/jemalloc_probe_tests.rs`.
1518pub const KTSTR_JEMALLOC_ALLOC_WORKER_BINARY_ENV: &str = "KTSTR_JEMALLOC_ALLOC_WORKER_BINARY";
1519
1520/// Name of the environment variable that opts into per-assertion
1521/// PASS logging in the verdict pipeline. Read once per call at
1522/// [`crate::assert::claim::Verdict::new`] via the
1523/// `log_passes_default` helper in src/assert/claim.rs:
1524/// the reader is `!(v.is_empty() || v == "0")`, so empty and
1525/// the literal `"0"` disable; any other value (`"1"`, `"true"`,
1526/// `"yes"`, even `"false"` because it isn't `"0"`) enables.
1527/// Unset → disabled. Default-off keeps the PASS path
1528/// unallocated under normal runs.
1529pub const KTSTR_LOG_PASSES_ENV: &str = "KTSTR_LOG_PASSES";
1530
1531/// Name of the environment variable that points at the busybox
1532/// blob on-disk. Exported by `cargo-ktstr`'s startup
1533/// `install_env` (see `bin/cargo_ktstr/blobs.rs`) which extracts
1534/// the embedded `BUSYBOX_BYTES` to a tempfile and sets this var
1535/// to the absolute path; read by
1536/// `crate::vmm::blobs::load_busybox_bytes`. Both unset and
1537/// set-but-empty surface a hard error, BUT the diagnostic
1538/// differs: unset hits the `Err(_)` arm and surfaces the
1539/// "blob is provided by `cargo-ktstr` at startup" install-env
1540/// hint; empty hits the `Ok("")` arm and falls through to a
1541/// generic `fs::read("")` ENOENT — less actionable. Operators
1542/// invoking `cargo ktstr <SUB>` see neither case; raw
1543/// `cargo nextest run` reliably triggers the unset diagnostic.
1544/// Busybox is load-bearing for shell-mode VMs + disk-template
1545/// builds.
1546pub const KTSTR_BUSYBOX_PATH_ENV: &str = "KTSTR_BUSYBOX_PATH";
1547
1548#[cfg(feature = "wprof")]
1549pub const KTSTR_WPROF_PATH_ENV: &str = "KTSTR_WPROF_PATH";
1550
1551/// Shared skip / error hint for call sites that cannot proceed
1552/// without a resolvable kernel. Phrased so the user sees the same
1553/// wording regardless of which layer surfaced the failure — tests,
1554/// CLI, monitor probes, and sidecar writers all point the operator
1555/// at the same remediation. Referenced by the non-VM-boot skip
1556/// paths in `cache.rs`, `probe/btf.rs`, `monitor/mod.rs`,
1557/// `test_support/eval/mod.rs`, and `test_support/mod.rs`.
1558///
1559/// Format: caller prefixes the actionable first clause (e.g.
1560/// "no vmlinux found") and appends this constant as the
1561/// remediation tail. Keeping the prefix per-caller lets each site
1562/// name the specific artifact it needs while the `KTSTR_KERNEL`
1563/// wording stays consistent.
1564// NOTE: the "accepted forms" enumeration here mirrors
1565// [`kernel_path::KERNEL_ID_GRAMMAR`] verbatim — keep in sync when
1566// either changes. (Composition at const time needs `concat!`-of-
1567// literals, and `KERNEL_ID_GRAMMAR` is a `const &str` not a literal.)
1568pub const KTSTR_KERNEL_HINT: &str = "set KTSTR_KERNEL to one of: \
1569    exact version (`6.14`), inclusive range (`6.14..7.0` or \
1570    `6.14..=7.0`), git source (`git+URL#tag=NAME`, \
1571    `git+URL#branch=NAME`, or `git+URL#sha=<40-hex>`), absolute or \
1572    `~`-prefixed path, or cache key. List cached keys with \
1573    `cargo ktstr kernel list`; build new ones with \
1574    `cargo ktstr kernel build`";
1575
1576/// Read [`KTSTR_KERNEL_ENV`] once, normalizing the raw value:
1577/// missing / empty / whitespace-only reads collapse to `None`, and
1578/// a surrounding-whitespace trim is applied so a shell-quoted
1579/// `KTSTR_KERNEL=" ../linux"` behaves the same as the unquoted
1580/// form. Every caller that reads the env var should route through
1581/// this helper so the normalization rules live in one place; a
1582/// future change to the rules (e.g. accepting a trailing slash)
1583/// propagates to every site automatically.
1584///
1585/// Returns the raw string; callers that need a structured
1586/// identifier parse with [`kernel_path::KernelId::parse`].
1587pub fn ktstr_kernel_env() -> Option<String> {
1588    std::env::var(KTSTR_KERNEL_ENV)
1589        .ok()
1590        .map(|v| v.trim().to_string())
1591        .filter(|v| !v.is_empty())
1592}
1593
1594/// Find a bootable kernel image on the host.
1595///
1596/// Resolution chain:
1597/// 1. `KTSTR_KERNEL` env var, parsed via `KernelId`:
1598///    - Path: search that directory for an arch-specific image
1599///    - Version/CacheKey: require cache access (error if cache
1600///      directory cannot be opened); on cache miss, skip the
1601///      general cache scan (step 2) and fall to filesystem
1602/// 2. XDG cache: most recent cached image (newest first)
1603/// 3. Local build trees (`./linux`, `../linux`,
1604///    `/lib/modules/{release}/build`)
1605/// 4. Host paths (`/lib/modules/{release}/vmlinuz`,
1606///    `/boot/vmlinuz-{release}`, `/boot/vmlinuz`)
1607///
1608/// Returns `Err` when `KTSTR_KERNEL` is a path that does not contain
1609/// a kernel image, or when it is a version/cache key and the cache
1610/// directory cannot be opened. Returns `Ok(None)` when no kernel is
1611/// found.
1612pub fn find_kernel() -> anyhow::Result<Option<std::path::PathBuf>> {
1613    use kernel_path::KernelId;
1614
1615    let release = rustix::system::uname()
1616        .release()
1617        .to_str()
1618        .ok()
1619        .map(str::to_owned);
1620    let release_ref = release.as_deref();
1621
1622    // Track whether KTSTR_KERNEL was set with a non-path value.
1623    // When the user explicitly requests a version or cache key that
1624    // misses cache, the general cache scan (step 2) must be skipped
1625    // to avoid silently returning a different kernel.
1626    let mut skip_cache_scan = false;
1627
1628    // 1. KTSTR_KERNEL env var with KernelId parsing. Route through
1629    // `ktstr_kernel_env()` so the empty/whitespace normalization
1630    // matches every other reader in the crate.
1631    if let Some(val) = ktstr_kernel_env() {
1632        match KernelId::parse(&val) {
1633            KernelId::Path(ref p) => {
1634                // `KernelId::parse` already routed `val` through
1635                // `expand_tilde`, producing the resolved `PathBuf`
1636                // here. Pass that — not the raw `val` — into
1637                // `find_image` so a `~/...` env value resolves
1638                // against `$HOME`. Lossy `to_str` would silently
1639                // mishandle non-UTF-8 paths; bail explicitly with
1640                // the same hint shape as the not-found arm.
1641                let Some(s) = p.to_str() else {
1642                    anyhow::bail!(
1643                        "KTSTR_KERNEL={val} expands to a non-UTF-8 path. \
1644                         {KTSTR_KERNEL_HINT}"
1645                    );
1646                };
1647                match kernel_path::find_image(Some(s), release_ref) {
1648                    Some(found) => return Ok(Some(found)),
1649                    None => anyhow::bail!(
1650                        "KTSTR_KERNEL={val} does not contain a kernel image. {KTSTR_KERNEL_HINT}"
1651                    ),
1652                }
1653            }
1654            KernelId::Version(ref ver) => {
1655                // Only tarball keys use the {ver}-tarball-{arch}-kc{suffix} pattern.
1656                // Git keys are {ref}-git-{hash}-{arch}-kc{suffix} and local keys
1657                // are local-{hash}-{arch}-kc{suffix} — neither contains the
1658                // version as a prefix, so only tarball lookup is valid here.
1659                let cache = cache::CacheDir::new().map_err(|e| {
1660                    anyhow::anyhow!(
1661                        "KTSTR_KERNEL={val} requires cache access, \
1662                         but cache directory could not be opened: {e}"
1663                    )
1664                })?;
1665                let arch = std::env::consts::ARCH;
1666                let key = format!("{ver}-tarball-{arch}-kc{}", cache_key_suffix());
1667                if let Some(entry) = cache.lookup(&key) {
1668                    return Ok(Some(entry.image_path()));
1669                }
1670                // Version not in cache — skip general cache scan to
1671                // avoid returning a different kernel version.
1672                skip_cache_scan = true;
1673            }
1674            KernelId::CacheKey(ref key) => {
1675                let cache = cache::CacheDir::new().map_err(|e| {
1676                    anyhow::anyhow!(
1677                        "KTSTR_KERNEL={val} requires cache access, \
1678                         but cache directory could not be opened: {e}"
1679                    )
1680                })?;
1681                if let Some(entry) = cache.lookup(key) {
1682                    return Ok(Some(entry.image_path()));
1683                }
1684                // Explicit cache key not found — skip general cache scan.
1685                skip_cache_scan = true;
1686            }
1687            // Multi-kernel specs (`A..B` ranges; git sources like
1688            // `git+URL#branch=main` are single-kernel but share this arm)
1689            // are only meaningful at the test/coverage/verifier
1690            // subcommand entry points where the runner can fan out
1691            // across kernels. The KTSTR_KERNEL env reader resolves a
1692            // single kernel image for in-process use (BTF lookup,
1693            // direct boot path) and has no dispatch loop, so a range
1694            // or git spec here cannot be expanded.
1695            //
1696            // Run `validate()` first so an inverted range surfaces
1697            // the specific "swap the endpoints" diagnostic instead
1698            // of getting masked by the generic "not supported in
1699            // env-var form" bail below — operators with a typo see
1700            // the actionable fix; valid-but-unsupported specs get
1701            // the generic redirect.
1702            id @ (KernelId::Range { .. } | KernelId::Git { .. }) => {
1703                if let Err(e) = id.validate() {
1704                    anyhow::bail!("KTSTR_KERNEL={val}: {e}");
1705                }
1706                anyhow::bail!(
1707                    "KTSTR_KERNEL={val}: multi-kernel specs (ranges, \
1708                     git sources) are not supported in env-var form. \
1709                     Use --kernel on the test/coverage/verifier \
1710                     subcommands, or set KTSTR_KERNEL to a single \
1711                     version, cache key, or path."
1712                );
1713            }
1714        }
1715    }
1716
1717    // 2. XDG cache: most recent cached image.
1718    // Skipped when KTSTR_KERNEL was an explicit version or cache key
1719    // that missed — returning a different kernel would be surprising.
1720    if !skip_cache_scan
1721        && let Ok(cache) = cache::CacheDir::new()
1722        && let Ok(entries) = cache.list()
1723    {
1724        let kc_hash = kconfig_hash();
1725        for listed in &entries {
1726            let cache::ListedEntry::Valid(entry) = listed else {
1727                continue;
1728            };
1729            // Skip entries built with a different kconfig. Untracked
1730            // (pre-kconfig-tracking) entries are reused — their image
1731            // could still boot correctly, and skipping them would
1732            // permanently orphan legacy cache entries.
1733            if entry.kconfig_status(&kc_hash).is_stale() {
1734                continue;
1735            }
1736            let image = entry.image_path();
1737            // TOCTOU guard: list() guarantees image existence at scan time,
1738            // but a concurrent cache-clean could delete between scan and use.
1739            if !image.exists() {
1740                continue;
1741            }
1742            // Guard: if a cached vmlinux is present but is missing
1743            // the symbols monitor code requires, skip the entry so
1744            // the caller falls through to a source tree. Older
1745            // caches built by a strip pipeline that dropped data
1746            // sections would pass the image-exists check but fail
1747            // downstream when the monitor initializes.
1748            if let Some(vmlinux) = entry.vmlinux_path()
1749                && let Err(e) = monitor::symbols::KernelSymbols::from_vmlinux(&vmlinux)
1750            {
1751                tracing::warn!(
1752                    entry = %entry.path.display(),
1753                    error = %e,
1754                    "skipping cached kernel with unusable vmlinux"
1755                );
1756                continue;
1757            }
1758            return Ok(Some(image));
1759        }
1760    }
1761
1762    // 3-4. Filesystem fallbacks (local build trees, host paths).
1763    Ok(kernel_path::find_image(None, release_ref))
1764}
1765
1766/// Name of the environment variable selecting the cargo build profile
1767/// for a `SchedulerSpec::Discover` scheduler built on demand by
1768/// [`build_and_find_binary`]. Holds a cargo profile NAME; unset / empty
1769/// means the RELEASE default (see `build_and_find_binary` — a debug
1770/// sched_ext scheduler is never the intended thing to test). Set by
1771/// `cargo ktstr <cmd> --profile <NAME>` (on `test` / `coverage` /
1772/// `verifier` / `perf-delta` / `replay`), or exported directly to pick a
1773/// non-default profile. This is INDEPENDENT of the harness `--release`
1774/// (`--cargo-profile release` to nextest), which selects the harness/test
1775/// binary's compile profile and does NOT touch this var — DECOUPLING the
1776/// scheduler-under-test's profile from the harness profile: the scheduler
1777/// runs optimized by default while the harness keeps its dev-profile
1778/// assertion thresholds and `catch_unwind` behavior unless its own build
1779/// profile is set separately.
1780pub const KTSTR_SCHEDULER_PROFILE_ENV: &str = "KTSTR_SCHEDULER_PROFILE";
1781
1782/// Name of the presence-only opt-out env var that re-enables the
1783/// pre-built-binary fallback after a FAILED orchestrated scheduler
1784/// build. When set to a NON-EMPTY value, a failed `cargo build -p
1785/// <sched>` in the non-cargo-test `Discover` path falls back to a
1786/// sibling / `target/{debug,release}/` binary AS-IS instead of failing
1787/// the test. Default (unset / empty) REFUSES the stale fallback so a
1788/// build that fails for a new reason cannot silently validate the test
1789/// against an old scheduler. Empty-string rejection mirrors
1790/// `KTSTR_CARGO_TEST_MODE` (`cargo_test_mode_active`) — NOT the
1791/// presence-only [`KTSTR_ORCHESTRATED_ENV`], which activates on an empty
1792/// value — so a stray `KTSTR_SCHEDULER_ALLOW_STALE_FALLBACK=` cannot
1793/// re-enable the hazard.
1794pub const KTSTR_SCHEDULER_ALLOW_STALE_FALLBACK_ENV: &str = "KTSTR_SCHEDULER_ALLOW_STALE_FALLBACK";
1795
1796/// The cargo build profile NAME for the scheduler-under-test:
1797/// [`KTSTR_SCHEDULER_PROFILE_ENV`] when set non-empty, else the
1798/// `"release"` default. Single source of the default so
1799/// [`build_and_find_binary`] (which builds it) and the `Discover`
1800/// fallback probe (which locates a pre-built one) never disagree on
1801/// which `target/<dir>/` the scheduler lands in.
1802pub fn scheduler_profile_name() -> String {
1803    resolve_scheduler_profile(std::env::var(KTSTR_SCHEDULER_PROFILE_ENV).ok())
1804}
1805
1806/// Pure resolution of the scheduler build-profile NAME from the raw
1807/// [`KTSTR_SCHEDULER_PROFILE_ENV`] value: a non-empty value verbatim,
1808/// else the `"release"` default. An empty string is treated as UNSET —
1809/// so a stray `--profile ""` / `KTSTR_SCHEDULER_PROFILE=` can never make
1810/// [`build_and_find_binary`] run `cargo build --profile ""` (which would
1811/// resolve the artifact under an empty-named profile dir). Split from
1812/// [`scheduler_profile_name`] so the empty / unset / named cases are
1813/// unit-testable without mutating process env.
1814fn resolve_scheduler_profile(env_val: Option<String>) -> String {
1815    env_val
1816        .filter(|p| !p.is_empty())
1817        .unwrap_or_else(|| "release".to_string())
1818}
1819
1820#[cfg(test)]
1821mod scheduler_profile_tests {
1822    use super::resolve_scheduler_profile;
1823
1824    /// The scheduler build-profile default: unset AND empty both fall
1825    /// back to `release` (empty is treated as unset so a stray
1826    /// `--profile ""` / `KTSTR_SCHEDULER_PROFILE=` cannot build under an
1827    /// empty-named profile dir); a non-empty name passes through verbatim
1828    /// (`dev`, `release`, or any custom `[profile.<name>]`). Pins the
1829    /// exact regressions [`resolve_scheduler_profile`] guards — dropping
1830    /// the `.filter(!is_empty)` (empty → empty name) or flipping the
1831    /// default flips a case.
1832    #[test]
1833    fn resolve_scheduler_profile_defaults_and_passthrough() {
1834        assert_eq!(
1835            resolve_scheduler_profile(None),
1836            "release",
1837            "unset -> release default"
1838        );
1839        assert_eq!(
1840            resolve_scheduler_profile(Some(String::new())),
1841            "release",
1842            "empty string is treated as unset -> release, never an empty profile name",
1843        );
1844        assert_eq!(
1845            resolve_scheduler_profile(Some("dev".into())),
1846            "dev",
1847            "named profile passes through verbatim"
1848        );
1849        assert_eq!(resolve_scheduler_profile(Some("release".into())), "release");
1850        assert_eq!(
1851            resolve_scheduler_profile(Some("custom".into())),
1852            "custom",
1853            "custom [profile.<name>] passes through unchanged",
1854        );
1855    }
1856}
1857
1858/// Build a cargo binary package and return its output path.
1859///
1860/// Runs from the ktstr crate's manifest directory (which is also the
1861/// workspace root in this repo) so that workspace-level feature
1862/// unification (e.g. vendored libbpf-sys) is always in effect,
1863/// regardless of the calling process's working directory.
1864///
1865/// The scheduler-under-test is built with the RELEASE profile by
1866/// DEFAULT: a debug sched_ext scheduler is far slower and its BPF
1867/// verifier instruction counts differ, so it is never the intended thing
1868/// to test. [`KTSTR_SCHEDULER_PROFILE_ENV`] overrides the profile name
1869/// (set by `cargo ktstr <cmd> --profile <NAME>`); an unset / empty value
1870/// keeps the release default. The build passes `cargo build --profile
1871/// <name>` verbatim (`dev` = the default profile, `release` == `--release`,
1872/// any custom `[profile.<name>]`), so the returned artifact path resolves
1873/// under the matching `target/<dir>/`.
1874pub fn build_and_find_binary(package: &str) -> anyhow::Result<std::path::PathBuf> {
1875    let profile = scheduler_profile_name();
1876    let build_args: Vec<String> = vec![
1877        "build".into(),
1878        "-p".into(),
1879        package.into(),
1880        "--message-format=json".into(),
1881        "--profile".into(),
1882        profile,
1883    ];
1884    let output = std::process::Command::new("cargo")
1885        .args(&build_args)
1886        .current_dir(env!("CARGO_MANIFEST_DIR"))
1887        .stdout(std::process::Stdio::piped())
1888        .stderr(std::process::Stdio::piped())
1889        .output()
1890        .map_err(|e| anyhow::anyhow!("cargo build: {e}"))?;
1891
1892    if !output.status.success() {
1893        let stderr = String::from_utf8_lossy(&output.stderr);
1894        anyhow::bail!("cargo build -p {package} failed:\n{stderr}");
1895    }
1896
1897    let stdout = String::from_utf8_lossy(&output.stdout);
1898    for line in stdout.lines() {
1899        if let Ok(msg) = serde_json::from_str::<serde_json::Value>(line)
1900            && msg.get("reason").and_then(|r| r.as_str()) == Some("compiler-artifact")
1901            && msg
1902                .get("profile")
1903                .and_then(|p| p.get("test"))
1904                .and_then(|t| t.as_bool())
1905                == Some(false)
1906            && msg
1907                .get("target")
1908                .and_then(|t| t.get("kind"))
1909                .and_then(|k| k.as_array())
1910                .is_some_and(|kinds| kinds.iter().any(|k| k.as_str() == Some("bin")))
1911            && let Some(filenames) = msg.get("filenames").and_then(|f| f.as_array())
1912            && let Some(path) = filenames.first().and_then(|f| f.as_str())
1913        {
1914            return Ok(std::path::PathBuf::from(path));
1915        }
1916    }
1917    anyhow::bail!(
1918        "no binary artifact found for package '{package}' — cargo build \
1919         succeeded but no compiler-artifact JSON line declared a [[bin]] \
1920         target. Two common causes: (1) the package has no [[bin]] target \
1921         (library-only, or only [[example]] / [[bench]] targets); (2) the \
1922         cargo --message-format=json output shape changed and the \
1923         artifact walker missed the matching line. Run `cargo build -p \
1924         {package} --message-format=json` and check for a `compiler-artifact` \
1925         line with `\"target\":{{\"kind\":[\"bin\"],...}}` to confirm."
1926    )
1927}
1928
1929/// Resolve the current executable path, falling back to `/proc/self/exe`
1930/// when the binary has been deleted (e.g. by `cargo llvm-cov`).
1931///
1932/// On Linux, `std::env::current_exe()` reads `/proc/self/exe`.  When the
1933/// binary is unlinked while running, the kernel appends ` (deleted)` to
1934/// the readlink target, producing a path that does not exist on disk.
1935/// `/proc/self/exe` itself remains usable as a file path because the
1936/// kernel keeps the inode alive, so we fall back to it.
1937pub(crate) fn resolve_current_exe() -> anyhow::Result<std::path::PathBuf> {
1938    use anyhow::Context;
1939    let exe = std::env::current_exe().context("resolve current exe")?;
1940    if exe.exists() {
1941        return Ok(exe);
1942    }
1943    let proc_exe = std::path::PathBuf::from("/proc/self/exe");
1944    anyhow::ensure!(
1945        proc_exe.exists(),
1946        "current exe not found: {}",
1947        exe.display()
1948    );
1949    Ok(proc_exe)
1950}
1951
1952/// Boot a KVM VM in interactive shell mode.
1953///
1954/// Builds an initramfs with busybox and optional include files, then
1955/// launches a VM with bidirectional stdin/stdout forwarding. The guest
1956/// runs a shell via busybox; user-provided files are available at
1957/// `/include-files/<name>`.
1958///
1959/// `kernel`: path to the kernel image (bzImage/Image).
1960/// `numa_nodes`, `llcs`, `cores`, `threads`: guest CPU topology.
1961/// `include_files`: `(archive_path, host_path)` pairs for files to
1962///   include in the guest.
1963/// `memory_mib`: explicit guest memory override in MiB; conversion
1964///   at VM-launch is `value << 20` bytes. When `None`, memory is
1965///   computed from actual initramfs size after build.
1966/// `disk`: optional virtio-blk device backing for `/dev/vda`. When
1967///   `Some`, the framework calls
1968///   `vmm::KtstrVm::builder`'s `.disk(..)` so the guest probes a
1969///   raw block device sized per `disk.capacity_mib`.
1970/// `wprof_args`: requires the `wprof` cargo feature. When the
1971///   feature is enabled and `Some`, replaces `WprofConfig::args`
1972///   with the tokenised value before booting; `None` keeps the
1973///   defaults. Without the feature, this parameter is ignored
1974///   (a warning is emitted to stderr if `Some`).
1975/// `performance_mode`: forwarded to
1976///   `vmm::KtstrVmBuilder::performance_mode`; when `true`, the
1977///   builder pins vCPU threads, applies hugepages, NUMA mbinds, and
1978///   promotes vCPU threads to SCHED_FIFO (host-side optimizations).
1979/// `sched_enable_cmds` / `sched_disable_cmds`: forwarded to
1980///   `vmm::KtstrVmBuilder::sched_enable_cmds` /
1981///   `vmm::KtstrVmBuilder::sched_disable_cmds`. Non-empty when the
1982///   shell is reproducing a test whose scheduler is a
1983///   [`test_support::SchedulerSpec::KernelBuiltin`] variant —
1984///   guest init runs the enable cmds before drop-to-busybox and the
1985///   disable cmds on shell exit, so the operator gets the same
1986///   scheduler-loaded environment the test would see. Empty slices
1987///   mean "no scheduler-lifecycle commands."
1988#[allow(clippy::too_many_arguments)]
1989pub fn run_shell(
1990    kernel: std::path::PathBuf,
1991    numa_nodes: u32,
1992    llcs: u32,
1993    cores: u32,
1994    threads: u32,
1995    include_files: &[(&str, &std::path::Path)],
1996    memory_mib: Option<u32>,
1997    dmesg: bool,
1998    exec: Option<&str>,
1999    exec_timeout: std::time::Duration,
2000    disk: Option<vmm::disk_config::DiskConfig>,
2001    wprof_args: Option<&str>,
2002    performance_mode: bool,
2003    sched_enable_cmds: &[&str],
2004    sched_disable_cmds: &[&str],
2005) -> anyhow::Result<Option<i32>> {
2006    // Re-ignore SIGPIPE for the lifetime of the shell-mode VM. The
2007    // `cargo-ktstr` main installs SIG_DFL on SIGPIPE (so streaming
2008    // subcommands like `ktstr kernel list | head` exit cleanly rather
2009    // than panicking inside `print!`), but shell-mode owns a multi-
2010    // thread VM whose stdout/stderr writers (virtio-console TX
2011    // forwarder, COM1 console dump, banner / cleanup `eprintln!`s)
2012    // each issue blocking writes against the caller's stdio. When a
2013    // caller spawns `cargo ktstr shell --exec ...` via
2014    // `Command::output()` and reads the captured streams only after
2015    // the child exits, intermediate write contention can surface as
2016    // EPIPE on the next byte — under SIG_DFL that races the
2017    // `write_all().is_err()` BrokenPipe-handling branches in the
2018    // forwarder threads and kills the process before they can flip
2019    // `kill` and exit cleanly. SIG_IGN lets every writer observe the
2020    // EPIPE return value and propagate it through normal Rust error
2021    // handling, which is what shell mode already handles.
2022    //
2023    // SAFETY: `libc::signal` is async-signal-safe and only updates a
2024    // process-wide table entry; SIG_IGN is a well-known constant.
2025    // The change is intentionally permanent for the rest of the
2026    // process — the only caller that left SIG_DFL active was the
2027    // streaming-subcommand path, and that path never reaches
2028    // `run_shell`.
2029    unsafe {
2030        libc::signal(libc::SIGPIPE, libc::SIG_IGN);
2031    }
2032
2033    let payload = resolve_current_exe()?;
2034
2035    let owned_includes: Vec<(String, std::path::PathBuf)> = include_files
2036        .iter()
2037        .map(|(a, p)| (a.to_string(), p.to_path_buf()))
2038        .collect();
2039
2040    let mut cmdline = format!("KTSTR_MODE=shell KTSTR_TOPO={numa_nodes},{llcs},{cores},{threads}");
2041    if dmesg {
2042        cmdline.push_str(" loglevel=7");
2043    }
2044    if let Ok(val) = std::env::var("RUST_LOG") {
2045        cmdline.push_str(&format!(" RUST_LOG={val}"));
2046    }
2047
2048    // Pass host terminal environment to guest.
2049    if let Ok(term) = std::env::var("TERM") {
2050        cmdline.push_str(&format!(" KTSTR_TERM={term}"));
2051    }
2052    if let Ok(ct) = std::env::var("COLORTERM") {
2053        cmdline.push_str(&format!(" KTSTR_COLORTERM={ct}"));
2054    }
2055
2056    // Pass host terminal dimensions to guest for correct line wrapping.
2057    unsafe {
2058        let mut ws: libc::winsize = std::mem::zeroed();
2059        if libc::ioctl(libc::STDIN_FILENO, libc::TIOCGWINSZ, &mut ws) == 0
2060            && ws.ws_col > 0
2061            && ws.ws_row > 0
2062        {
2063            cmdline.push_str(&format!(
2064                " KTSTR_COLS={} KTSTR_ROWS={}",
2065                ws.ws_col, ws.ws_row
2066            ));
2067        }
2068    }
2069
2070    let no_perf_mode = crate::test_support::runtime::no_perf_mode_active();
2071    use anyhow::Context;
2072    let busybox_bytes =
2073        vmm::blobs::load_busybox_bytes().context("load busybox blob for shell-mode VM")?;
2074    #[cfg(feature = "wprof")]
2075    let wprof_config = {
2076        // A `wprof`-feature build provisions the blob via cargo-ktstr's
2077        // install_env (KTSTR_WPROF_PATH) UNLESS it was built with
2078        // KTSTR_SKIP_WPROF_BUILD=1 (the documented escape hatch, which
2079        // leaves the blob empty so install_env exports no path). A failed
2080        // resolve here therefore means that hatch is set or install_env
2081        // was bypassed — fail loud either way rather than silently
2082        // shipping a wprof-less shell VM, which surfaces downstream as a
2083        // confusing "/bin/wprof: No such file" inside the guest.
2084        let mut c = vmm::wprof::WprofConfig::from_env()
2085            .context("resolve wprof for shell mode (the `wprof` feature is enabled)")?;
2086        if let Some(args_str) = wprof_args {
2087            c.args = args_str.split_whitespace().map(String::from).collect();
2088        }
2089        Some(c)
2090    };
2091    #[cfg(not(feature = "wprof"))]
2092    if wprof_args.is_some() {
2093        eprintln!(
2094            "ktstr: wprof_args ignored — ktstr was built without the \
2095             `wprof` cargo feature; /bin/wprof will not be available \
2096             in the guest"
2097        );
2098    }
2099    let mut builder = vmm::KtstrVm::builder()
2100        .kernel(&kernel)
2101        .init_binary(&payload)
2102        .topology(vmm::Topology::new(numa_nodes, llcs, cores, threads))
2103        .cmdline(&cmdline)
2104        .include_files(owned_includes)
2105        .busybox(Some(busybox_bytes))
2106        .dmesg(dmesg)
2107        .no_perf_mode(no_perf_mode)
2108        .performance_mode(performance_mode)
2109        .sched_enable_cmds(sched_enable_cmds)
2110        .sched_disable_cmds(sched_disable_cmds);
2111
2112    #[cfg(feature = "wprof")]
2113    {
2114        builder = builder.wprof(wprof_config);
2115    }
2116
2117    if let Some(cmd) = exec {
2118        // exec_timeout bounds the payload's wall-clock (panic-less hang
2119        // guard); only meaningful in exec mode, so set it alongside.
2120        builder = builder.exec_cmd(cmd).exec_timeout(exec_timeout);
2121    }
2122
2123    if let Some(d) = disk {
2124        builder = builder.disk(d);
2125    }
2126
2127    // Shell-mode initramfs (busybox, operator includes, and wprof
2128    // when the `wprof` feature is enabled) can exceed a test's
2129    // declared `memory_mib`. Treat the caller's value as a FLOOR;
2130    // the deferred path takes the max of it and the actual
2131    // initramfs size.
2132    builder = match memory_mib {
2133        Some(mib) => builder.memory_deferred_min(mib),
2134        None => builder.memory_deferred(),
2135    };
2136
2137    let vm = builder.build()?;
2138
2139    vm.run_interactive()
2140}
2141
2142#[cfg(test)]
2143mod tests;