ktstr/test_support/mod.rs
1//! Runtime support for `#[ktstr_test]` integration tests.
2//!
3//! Provides the registration type, distributed slice, VM launcher,
4//! and result evaluation. Includes guest-side profraw flush for
5//! coverage-instrumented builds.
6//!
7//! The entry point for test authors is the [`macro@crate::ktstr_test`]
8//! attribute macro; see the user-facing Writing Tests guide shipped
9//! with the crate's mdbook for end-to-end examples and the full
10//! attribute grammar.
11//!
12//! # Consumer API
13//!
14//! Test authors interact primarily with the `#[ktstr_test]` proc
15//! macro; programmatic test generation can instead populate
16//! [`KtstrTestEntry`] values into the [`KTSTR_TESTS`]
17//! `linkme` distributed slice. The remaining items in this module
18//! are runtime glue invoked by the macro-generated code and the
19//! `ktstr` / `cargo-ktstr` binaries.
20//!
21//! # Module layout
22//!
23//! Implementation is split across 19 production submodules
24//! re-exported at `test_support::*` for a flat public API: `args`
25//! (CLI argument extraction), `dispatch` (ktstr / cargo-ktstr CLI
26//! entry points), `entry` (scheduler + test-entry types),
27//! `entry_validate` (`KtstrTestEntry::validate` + phase helpers split
28//! out of `entry.rs`), `eval`
29//! (host-side VM result evaluation), `host_class` (shared
30//! host-insufficiency error classification), `metrics` (payload stdout →
31//! `Metric` list), `output`
32//! (guest-output and console parsing), `payload` (`Payload` /
33//! `MetricCheck` / `Metric` / `Polarity`), `probe` (auto-repro and
34//! BPF probe pipeline), `probe_metrics` (host-side BPF map
35//! introspection), `profraw` (coverage flush), `runtime` (`pub mod`
36//! — neutral home for verbose/shm-size/config-file-parts shared by
37//! eval and probe so they don't circularly depend on each other),
38//! `shell_descriptor` (wire-format struct shared between the test
39//! binary's `--ktstr-shell-test=<NAME>` producer and cargo-ktstr's
40//! shell-mode consumer), `wprof` (`#[cfg(feature = "wprof")]` —
41//! Perfetto-trace wire constants + `.wprof.pb` shape assertions),
42//! `sidecar` (per-run JSON records), `staged`
43//! (`pub(crate) mod` — staged-payload writer), `timefmt` (ISO-8601
44//! + run-id helpers), and `topo` (topology override parsing).
45//!
46//! A `#[cfg(test)] pub(crate) mod test_helpers` exists for cross-file
47//! test wiring; it is not part of the production surface.
48
49#[cfg(test)]
50use crate::assert::AssertResult;
51#[cfg(test)]
52use crate::scenario::Ctx;
53#[cfg(test)]
54use anyhow::Result;
55
56mod args;
57// Re-exported for the workload-side CgroupChurn worker, which resolves the
58// same workload cgroup root the host-side setup uses but lives outside the
59// private `args` module's subtree.
60pub(crate) use args::resolve_cgroup_root;
61mod dispatch;
62mod entry;
63mod entry_validate;
64mod eval;
65mod host_class;
66mod metrics;
67mod output;
68// Reachable crate-wide (vmm::VmResult::guest_assert_result parses the guest
69// AssertResult from its own drained guest_messages via this helper, mirroring
70// the eval-layer use). The fn itself is pub(crate); this just lifts it out of
71// the private `output` module so the vmm layer can call it by a stable path.
72pub(crate) use output::parse_assert_result_from_drain;
73mod payload;
74mod probe;
75mod probe_metrics;
76mod profraw;
77pub use eval::post_vm_skip;
78pub use profraw::current_binary_is_coverage_instrumented;
79pub mod runtime;
80mod shell_descriptor;
81pub use shell_descriptor::{SchedulerKind, ShellTestDescriptor};
82#[cfg(feature = "wprof")]
83pub mod wprof;
84#[cfg(feature = "wprof")]
85pub use wprof::{PERFETTO_TRACE_PACKETS_TAG, WPROF_PB_MIN_BYTES, assert_wprof_pb_shape};
86#[cfg(test)]
87pub(crate) mod btf_blob;
88mod sidecar;
89pub(crate) mod staged;
90#[cfg(test)]
91pub(crate) mod test_helpers;
92mod timefmt;
93mod topo;
94
95/// Shared callback signature for the
96/// [`KtstrTestEntry::post_vm`](entry::KtstrTestEntry::post_vm) and
97/// [`KtstrTestEntry::post_vm_unconditional`](entry::KtstrTestEntry::post_vm_unconditional)
98/// host-side hooks. Both fields wrap this same shape in `Option<_>`;
99/// the alias collapses the open-coded `fn(&crate::vmm::VmResult)
100/// -> anyhow::Result<()>` repetition at the field declarations and
101/// at the matching `with_post_vm{,_unconditional}` builder
102/// parameters. Future post-VM hooks (e.g. an `expect_auto_repro`
103/// artifact-existence checker) plug into the same shape without
104/// triplicating the signature.
105pub type PostVmCallback = fn(&crate::vmm::VmResult) -> anyhow::Result<()>;
106
107// extract_probe_stack_arg and extract_work_type_arg are reached in
108// production via `super::args::` (probe.rs); the re-export here
109// preserves the flat-namespace invariant so `test_support::X` resolves
110// uniformly across all CLI arg extractors.
111#[cfg(feature = "export")]
112pub(crate) use args::extract_export_output_arg;
113#[allow(unused_imports)]
114pub(crate) use args::{
115 CellParentCgroupArg, VERIFIER_WORKLOAD_FLAG, cell_parent_path_is_valid,
116 extract_export_test_arg, extract_probe_stack_arg, extract_shell_test_arg, extract_test_fn_arg,
117 extract_topo_arg, extract_work_type_arg, is_verifier_workload, parse_cell_parent_cgroup,
118};
119#[allow(unused_imports)]
120pub(crate) use runtime::{append_base_sched_args, content_hash, scratch_dir, sys_rdy_budget_ms};
121#[cfg(test)]
122pub(crate) use sidecar::enriched_parse_error_message_for_test;
123pub use sidecar::{
124 PerfDeltaAssertionRecord, SidecarResult, collect_pool, detect_kernel_commit,
125 format_run_artifact_footer, newest_run_dir, repo_is_dirty, runs_root, sidecar_dir,
126 source_dir_for,
127};
128pub(crate) use sidecar::{
129 SidecarIoError, SidecarParseError, apply_archive_source_override, collect_sidecars,
130 collect_sidecars_with_errors, format_callback_profile, format_kvm_stats, format_verifier_stats,
131 is_run_directory, is_sidecar_filename, warn_skipped_sidecars,
132};
133
134pub use dispatch::{
135 DEFAULT_HOST_CGROUP_PARENT, EXIT_FAIL, EXIT_INCONCLUSIVE, EXIT_PASS, analyze_sidecars,
136 is_cpu_budget_unsatisfiable, is_kernel_unavailable, is_perf_mode_unavailable,
137 is_resource_contention, is_topology_insufficient, is_topology_unrepresentable, ktstr_main,
138 ktstr_test_early_dispatch, resolve_host_cgroup_parent, run_ktstr_test, sanitize_kernel_label,
139};
140pub use entry::{
141 BinaryKindJson, BpfMapAgg, BpfMapWrite, CgroupPath, KTSTR_SCHEDULERS, KTSTR_TESTS,
142 KtstrTestEntry, MemSideCache, NumaDistance, NumaNode, PerfDeltaAssertion, Scheduler,
143 SchedulerJson, SchedulerListEntry, SchedulerSpec, SchedulerTestJson, Sysctl, Topology,
144 TopologyConstraints, TopologyConstraintsJson, TopologyJson, WatchBpfMap,
145 default_post_vm_periodic_fired, find_scheduler, find_test,
146};
147pub use eval::{KernelUnavailable, ResolveSource, resolve_scheduler, resolve_test_kernel};
148pub(crate) use eval::{record_skip_sidecar, run_ktstr_test_inner};
149pub use host_class::{HostClass, classify_host_error};
150pub use metrics::{
151 MAX_WALK_DEPTH, WALK_TRUNCATION_SENTINEL_NAME, extract_metrics, is_truncation_sentinel_name,
152 walk_json_leaves,
153};
154pub(crate) use output::extract_panic_message;
155pub use payload::{
156 Metric, MetricCheck, MetricHint, MetricStream, OutputFormat, Payload, PayloadKind,
157 PayloadMetrics, Polarity,
158};
159pub(crate) use probe::maybe_dispatch_vm_test;
160pub(crate) use probe::{
161 PROBE_DRAIN_GRACE, finalize_probe_after_unwind, maybe_dispatch_vm_test_with_args,
162 maybe_dispatch_vm_test_with_phase_a, propagate_rust_env_from_cmdline, start_probe_phase_a,
163};
164pub use probe_metrics::{
165 MAX_SCAN_INDEX, ThreadLookup, count_indexed_metrics, find_metric, find_metric_u64,
166 flat_metrics_dump, has_metric, lookup_thread, snapshot_count, snapshot_worker_allocated,
167 thread_count,
168};
169pub use profraw::target_dir as profraw_target_dir;
170pub(crate) use profraw::{find_symbol_vaddrs, persist_guest_profraw, try_flush_profraw};
171pub(crate) use timefmt::now_iso8601;
172pub(crate) use topo::{TopoOverride, parse_topo_string};
173
174/// Host capacity triple `(cpus, llcs, max_cpus_per_llc)` used to
175/// filter gauntlet topology presets against what the host can actually
176/// schedule. Both `dispatch::list_tests_*` (gauntlet variant filter)
177/// and `dispatch::list_verifier_cells_all` (verifier sweep filter)
178/// share this single source of truth so the two filters never drift.
179/// Reads `available_parallelism()` for CPU count + `HostTopology::from_sysfs()`
180/// for LLC layout; falls back to single-LLC + single-cpu-per-llc when
181/// sysfs is unavailable.
182pub fn host_capacity() -> (u32, u32, u32) {
183 let host_cpus = std::thread::available_parallelism()
184 .map(|n| n.get() as u32)
185 .unwrap_or(1);
186 let host_topo = crate::vmm::host_topology::HostTopology::from_sysfs().ok();
187 let host_llcs = host_topo
188 .as_ref()
189 .map(|t| t.llc_groups.len() as u32)
190 .unwrap_or(1);
191 let host_max_cpus_per_llc = host_topo
192 .as_ref()
193 .map(|t| t.max_cores_per_llc() as u32)
194 .unwrap_or(host_cpus);
195 (host_cpus, host_llcs, host_max_cpus_per_llc)
196}
197
198// ---------------------------------------------------------------------------
199// Test infrastructure requirements
200// ---------------------------------------------------------------------------
201//
202// `require_*` helpers turn missing test infrastructure into a panic with
203// an actionable message instead of a silent skip. Use them when a test
204// is meaningless without the resource -- a missing kernel, vmlinux,
205// scheduler binary, or kernel-symbol resolution means the harness is
206// misconfigured, not that the test should pass quietly. CI silently
207// passing 100 "tests" that all early-returned because no kernel was
208// findable is the failure mode these helpers exist to prevent.
209//
210// For genuine skips (raw BTF at /sys/kernel/btf/vmlinux, host without
211// the architectural dependency the test exercises), call the crate's
212// `skip!("reason: {detail}")` macro (see `src/test_macros.rs`). It
213// emits the canonical `ktstr: SKIP: ...` line and returns from the
214// test.
215
216/// Whether the current test process was launched by a cargo-ktstr
217/// orchestration path (`cargo ktstr test`, `cargo ktstr verifier`)
218/// vs. a raw `cargo nextest run` / `cargo test`.
219///
220/// Reads [`crate::KTSTR_ORCHESTRATED_ENV`]; only checks presence,
221/// not value (cargo-ktstr always sets it to `"1"`, but the marker
222/// semantics are presence-only). Returns `false` when the env var
223/// is unset or unreadable.
224///
225/// Tests that boot real KVM VMs use this to skip when running
226/// under raw nextest, where the 7000+-test concurrency starves
227/// per-VM resource budgets and produces a misleading "kill set by
228/// AP" failure that looks like a real bug. cargo-ktstr's
229/// orchestrator constrains the VM-test concurrency so the budgets
230/// hold; skipping under raw nextest surfaces the operator-error
231/// (wrong runner) without masking real failures during proper
232/// orchestrated runs.
233///
234/// `pub(crate)` — only callers are integration-test helpers under
235/// `src/vmm/mod.rs`'s `#[cfg(test)]` mod. The env-var name itself
236/// is `pub` via [`crate::KTSTR_ORCHESTRATED_ENV`] for
237/// documentation purposes.
238#[cfg(test)]
239#[allow(dead_code)] // called from x86_64-only tests in vmm/mod.rs
240pub(crate) fn cargo_ktstr_orchestrated() -> bool {
241 std::env::var(crate::KTSTR_ORCHESTRATED_ENV).is_ok()
242}
243
244/// Skip-message body for vmm-boot tests that bail when the test
245/// process wasn't launched by cargo-ktstr orchestration. The
246/// canonical extended rationale lives inline at the
247/// `boot_kernel_with_monitor` site; sibling sites reference back
248/// to it via this shared const so a future message tweak lands in
249/// one place instead of four. The 4 sibling sites previously
250/// carried byte-for-byte-identical copies of this string — per
251/// the no-mega-no-dupes policy the 3+-site threshold mandates a
252/// shared const.
253#[cfg(test)]
254#[allow(dead_code)] // referenced from x86_64-only vmm/mod.rs tests
255pub(crate) const SKIP_NOT_ORCHESTRATED_MSG: &str = "raw nextest fan-out starves KVM resource budgets — see \
256 boot_kernel_with_monitor for the shared rationale. Run via \
257 `cargo ktstr test --kernel ../linux`.";
258
259#[cfg(test)]
260mod cargo_ktstr_orchestrated_tests {
261 //! Pin the env-var-presence detection contract. A regression
262 //! that renamed [`crate::KTSTR_ORCHESTRATED_ENV`] silently
263 //! would make every vmm-test skip even under cargo-ktstr
264 //! orchestration (where the env IS set), turning the
265 //! VM-boot test suite into an always-green no-op. Pin the
266 //! two-arm contract (set → true, unset → false) so the rename
267 //! surfaces here.
268 use super::cargo_ktstr_orchestrated;
269 use crate::test_support::test_helpers::{EnvVarGuard, lock_env};
270 #[test]
271 fn cargo_ktstr_orchestrated_true_when_env_set() {
272 let _lock = lock_env();
273 let _guard = EnvVarGuard::set(crate::KTSTR_ORCHESTRATED_ENV, "1");
274 assert!(
275 cargo_ktstr_orchestrated(),
276 "KTSTR_ORCHESTRATED set → orchestrated check must return true"
277 );
278 }
279 #[test]
280 fn cargo_ktstr_orchestrated_false_when_env_unset() {
281 let _lock = lock_env();
282 let _guard = EnvVarGuard::remove(crate::KTSTR_ORCHESTRATED_ENV);
283 assert!(
284 !cargo_ktstr_orchestrated(),
285 "KTSTR_ORCHESTRATED absent → orchestrated check must return false \
286 (otherwise raw nextest invocations would run vmm tests and \
287 starve KVM resource budgets)"
288 );
289 }
290}
291
292/// Resolve a kernel image path or panic with an actionable message.
293///
294/// Wraps [`crate::find_kernel`]: an `Err` (KTSTR_KERNEL points at a
295/// path with no kernel image, cache lookup failed) and a successful
296/// `Ok(None)` (no kernel discoverable) both panic. Tests that boot a
297/// VM cannot proceed without a kernel; silently skipping turns CI
298/// breakage into a green run.
299#[cfg(test)]
300#[allow(dead_code)] // called from x86_64-only tests in vmm/mod.rs
301pub(crate) fn require_kernel() -> std::path::PathBuf {
302 match crate::find_kernel() {
303 Ok(Some(p)) => p,
304 Ok(None) => panic!(
305 "ktstr_test: test requires a kernel but none was found. {}",
306 crate::KTSTR_KERNEL_HINT
307 ),
308 Err(e) => panic!("ktstr_test: kernel resolution failed: {e:#}"),
309 }
310}
311
312/// Resolve a vmlinux path next to a kernel image or panic.
313///
314/// `kernel_path` is the value returned by [`require_kernel`]. The
315/// vmlinux is required for symbol address lookup, BTF, and probe
316/// source resolution -- a kernel image without vmlinux means the
317/// cache entry is corrupt or the build was incomplete, which is an
318/// infrastructure failure rather than a legitimate skip.
319#[cfg(test)]
320#[allow(dead_code)] // called from x86_64-only tests in vmm/mod.rs
321pub(crate) fn require_vmlinux(kernel_path: &std::path::Path) -> std::path::PathBuf {
322 crate::vmm::find_vmlinux(kernel_path).unwrap_or_else(|| {
323 panic!(
324 "ktstr_test: no vmlinux found alongside {}. The cache entry or \
325 kernel build is incomplete. Rebuild with `cargo ktstr kernel \
326 build --force`; the specified kernel must include `vmlinux` \
327 alongside the boot image. {}",
328 kernel_path.display(),
329 crate::KTSTR_KERNEL_HINT,
330 )
331 })
332}
333
334/// Build a workspace package and return its binary path, or panic.
335///
336/// Wraps [`crate::build_and_find_binary`]. A failed build or missing
337/// artifact for a required scheduler binary (e.g. `scx-ktstr`) is an
338/// infrastructure failure -- the workspace is broken, not the test.
339#[cfg(test)]
340pub(crate) fn require_binary(package: &str) -> std::path::PathBuf {
341 crate::build_and_find_binary(package).unwrap_or_else(|e| {
342 panic!(
343 "ktstr_test: build of `{package}` failed: {e:#}. \
344 Run `cargo build -p {package}` to reproduce and diagnose."
345 )
346 })
347}
348
349/// Resolve [`crate::monitor::symbols::KernelSymbols`] from a vmlinux
350/// or panic. The symbol table is required for any host-side memory
351/// introspection; an unparseable vmlinux is an infrastructure failure.
352#[cfg(test)]
353#[allow(dead_code)] // called from x86_64-only tests in vmm/mod.rs
354pub(crate) fn require_kernel_symbols(
355 vmlinux_path: &std::path::Path,
356) -> crate::monitor::symbols::KernelSymbols {
357 crate::monitor::symbols::KernelSymbols::from_vmlinux(vmlinux_path).unwrap_or_else(|e| {
358 panic!(
359 "ktstr_test: kernel symbol resolution from {} failed: {e:#}",
360 vmlinux_path.display(),
361 )
362 })
363}
364
365/// Resolve [`crate::monitor::btf_offsets::KernelOffsets`] from a vmlinux
366/// or panic. BTF resolution is required for any host-side kernel
367/// struct introspection; a vmlinux whose BTF fails to parse is an
368/// infrastructure failure, not a test-skip condition.
369#[cfg(test)]
370pub(crate) fn require_kernel_offsets(
371 vmlinux_path: &std::path::Path,
372) -> crate::monitor::btf_offsets::KernelOffsets {
373 crate::monitor::btf_offsets::KernelOffsets::from_vmlinux(vmlinux_path).unwrap_or_else(|e| {
374 panic!(
375 "ktstr_test: kernel BTF resolution from {} failed: {e:#}. \
376 The kernel must be built with CONFIG_DEBUG_INFO_BTF=y; \
377 rebuild with `cargo ktstr kernel build --force` if the \
378 cache entry was produced without BTF.",
379 vmlinux_path.display(),
380 )
381 })
382}
383
384/// Resolve [`crate::monitor::btf_offsets::BpfMapOffsets`] from a vmlinux
385/// or panic. A vmlinux whose BTF fails to yield BPF map offsets is an
386/// infrastructure failure, not a test-skip condition.
387#[cfg(test)]
388pub(crate) fn require_bpf_map_offsets(
389 vmlinux_path: &std::path::Path,
390) -> crate::monitor::btf_offsets::BpfMapOffsets {
391 crate::monitor::btf_offsets::BpfMapOffsets::from_vmlinux(vmlinux_path).unwrap_or_else(|e| {
392 panic!(
393 "ktstr_test: BpfMapOffsets resolution from {} failed: {e:#}. \
394 The kernel must be built with CONFIG_DEBUG_INFO_BTF=y; \
395 rebuild with `cargo ktstr kernel build --force` if the \
396 cache entry was produced without BTF.",
397 vmlinux_path.display(),
398 )
399 })
400}
401
402/// Resolve [`crate::monitor::btf_offsets::BpfProgOffsets`] from a vmlinux
403/// or panic. A vmlinux whose BTF fails to yield BPF program offsets is
404/// an infrastructure failure, not a test-skip condition.
405#[cfg(test)]
406pub(crate) fn require_bpf_prog_offsets(
407 vmlinux_path: &std::path::Path,
408) -> crate::monitor::btf_offsets::BpfProgOffsets {
409 crate::monitor::btf_offsets::BpfProgOffsets::from_vmlinux(vmlinux_path).unwrap_or_else(|e| {
410 panic!(
411 "ktstr_test: BpfProgOffsets resolution from {} failed: {e:#}. \
412 The kernel must be built with CONFIG_DEBUG_INFO_BTF=y; \
413 rebuild with `cargo ktstr kernel build --force` if the \
414 cache entry was produced without BTF.",
415 vmlinux_path.display(),
416 )
417 })
418}
419
420#[cfg(test)]
421mod tests {
422 use super::*;
423 use crate::ktstr_test;
424 use linkme::distributed_slice;
425
426 // Register a test entry in the distributed slice for unit testing find_test.
427 fn __ktstr_inner_unit_test_dummy(_ctx: &Ctx) -> Result<AssertResult> {
428 Ok(AssertResult::pass())
429 }
430
431 #[distributed_slice(KTSTR_TESTS)]
432 static __KTSTR_ENTRY_UNIT_TEST_DUMMY: KtstrTestEntry = KtstrTestEntry {
433 name: "__unit_test_dummy__",
434 func: __ktstr_inner_unit_test_dummy,
435 ..KtstrTestEntry::DEFAULT
436 };
437
438 #[test]
439 fn find_test_registered_entry() {
440 let entry = find_test("__unit_test_dummy__");
441 assert!(entry.is_some(), "registered entry should be found");
442 let entry = entry.unwrap();
443 assert_eq!(entry.name, "__unit_test_dummy__");
444 assert_eq!(entry.topology.llcs, 1);
445 assert_eq!(entry.topology.cores_per_llc, 2);
446 }
447
448 #[test]
449 fn find_test_nonexistent() {
450 assert!(find_test("__nonexistent_test_xyz__").is_none());
451 }
452
453 #[test]
454 fn find_test_from_distributed_slice() {
455 // KTSTR_TESTS should contain at least the __unit_test_dummy__ entry.
456 assert!(!KTSTR_TESTS.is_empty());
457 }
458
459 // Macro-codegen coverage for the cpu_budget attr: drive the real
460 // #[ktstr_test] expansion (the parse arm + the Some(n)/None field-emit
461 // arms) and assert the emitted KtstrTestEntry carries the right
462 // cpu_budget. host_only keeps the probe from booting a VM; no_perf_mode
463 // satisfies the macro's cpu_budget-requires-no_perf_mode gate. find_test
464 // reads the registered entry from KTSTR_TESTS without invoking the body.
465 // This RUNS in CI (a src/ #[cfg(test)] unit test), unlike the
466 // ktstr-macros crate's own suite which cargo-ktstr does not discover.
467 #[ktstr_test(cpu_budget = 7, host_only, no_perf_mode)]
468 fn cpu_budget_codegen_probe(_ctx: &Ctx) -> Result<AssertResult> {
469 Ok(AssertResult::pass())
470 }
471
472 #[ktstr_test(host_only)]
473 fn cpu_budget_codegen_probe_none(_ctx: &Ctx) -> Result<AssertResult> {
474 Ok(AssertResult::pass())
475 }
476
477 #[test]
478 fn cpu_budget_attr_emits_some() {
479 let e = find_test("cpu_budget_codegen_probe")
480 .expect("the cpu_budget probe entry must be registered");
481 assert_eq!(
482 e.cpu_budget,
483 Some(7),
484 "#[ktstr_test(cpu_budget = 7)] must emit cpu_budget: Some(7)"
485 );
486 }
487
488 #[test]
489 fn omitted_cpu_budget_defaults_none() {
490 let e = find_test("cpu_budget_codegen_probe_none")
491 .expect("the no-cpu_budget probe entry must be registered");
492 assert_eq!(
493 e.cpu_budget, None,
494 "omitting cpu_budget must leave the DEFAULT None (no field emitted)"
495 );
496 }
497}