ktstr/test_support/shell_descriptor.rs
1//! Shared `ShellTestDescriptor` for the `--ktstr-shell-test=<NAME>`
2//! probe wire format.
3//!
4//! Producer: `dispatch::maybe_dispatch_shell_test` serializes a
5//! fully-populated descriptor to stdout when a test binary is probed.
6//!
7//! Consumer: `cargo_ktstr::misc::shell::resolve_shell_from_test_entry`
8//! deserializes the stdout, then `run_shell` passes the descriptor's
9//! fields to [`crate::run_shell`] so the shell VM mirrors the test's
10//! topology, scheduler, wprof, and performance settings.
11//!
12//! Older `cargo-ktstr` binaries can deserialize JSON emitted by a
13//! newer test binary that adds a field because the derived
14//! `Deserialize` impl ignores unknown fields (no
15//! `#[serde(deny_unknown_fields)]`): the field the old struct doesn't
16//! know about is dropped, and existing fields remain populated. The
17//! reverse direction (newer `cargo-ktstr` reading older JSON that
18//! lacks a field the new code added) works because every field carries
19//! `#[serde(default)]`, which supplies a default for each missing
20//! field.
21
22use serde::{Deserialize, Serialize};
23
24/// Discriminator for the test's scheduler-spec shape on the
25/// `--ktstr-shell-test=<NAME>` wire format. Wire-byte-compatible with
26/// the prior stringly-typed `"eevdf" | "discover" | "path" |
27/// "kernel_builtin"` values via `#[serde(rename_all = "snake_case")]`;
28/// the typed enum replaces the stringly-typed boundary so a rename on
29/// either side (producer in `dispatch::maybe_dispatch_shell_test`,
30/// consumer in `cargo_ktstr::misc::shell`) is a compile error instead
31/// of a silent banner-emit-gate regression.
32///
33/// 1:1 with [`crate::test_support::SchedulerSpec`]'s 4 variants; the
34/// payload data (scheduler binary name, KernelBuiltin enable/disable
35/// commands) rides separately on the descriptor's `scheduler_name` /
36/// `scheduler_enable_cmds` / `scheduler_disable_cmds` fields so this
37/// type carries only the discriminator.
38#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
39#[serde(rename_all = "snake_case")]
40pub enum SchedulerKind {
41 /// Kernel-default scheduling — no userspace binary, no
42 /// kernel-builtin lifecycle. The no-scx control.
43 #[default]
44 Eevdf,
45 /// Userspace scx binary located via `resolve_scheduler` cascade
46 /// (name-only, path is discovered at run time).
47 Discover,
48 /// Userspace scx binary at a fully-qualified path.
49 Path,
50 /// In-kernel scheduling class activated via `scheduler_enable_cmds`
51 /// before workload start, torn down via `scheduler_disable_cmds`
52 /// on shell exit / test teardown.
53 KernelBuiltin,
54}
55
56impl std::fmt::Display for SchedulerKind {
57 /// Renders the snake_case wire form (`"eevdf"`, `"discover"`,
58 /// `"path"`, `"kernel_builtin"`) so banner format strings + log
59 /// lines stay byte-compatible with the prior stringly-typed
60 /// behavior.
61 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
62 let s = match self {
63 SchedulerKind::Eevdf => "eevdf",
64 SchedulerKind::Discover => "discover",
65 SchedulerKind::Path => "path",
66 SchedulerKind::KernelBuiltin => "kernel_builtin",
67 };
68 f.write_str(s)
69 }
70}
71
72impl From<&crate::test_support::SchedulerSpec> for SchedulerKind {
73 /// Single source of truth for the SchedulerSpec → SchedulerKind
74 /// mapping. Exhaustive match so adding a 5th SchedulerSpec
75 /// variant triggers a compile error here, forcing the
76 /// discriminator to grow in lockstep.
77 fn from(spec: &crate::test_support::SchedulerSpec) -> Self {
78 match spec {
79 crate::test_support::SchedulerSpec::Eevdf => SchedulerKind::Eevdf,
80 crate::test_support::SchedulerSpec::Discover(_) => SchedulerKind::Discover,
81 crate::test_support::SchedulerSpec::Path(_) => SchedulerKind::Path,
82 crate::test_support::SchedulerSpec::KernelBuiltin { .. } => {
83 SchedulerKind::KernelBuiltin
84 }
85 }
86 }
87}
88
89/// Wire-format descriptor exchanged between a test binary and
90/// `cargo ktstr shell --test <NAME>` to let the shell VM mirror the
91/// named `#[ktstr_test]`'s topology, scheduler, wprof config, and
92/// performance mode.
93///
94/// `scheduler_kind` is a typed [`SchedulerKind`] discriminator so the
95/// banner can hint at how to repro the scheduler (Discover/Path =
96/// userspace binary at `/bin/<n>`; KernelBuiltin = no binary, runs
97/// `scheduler_enable_cmds` before drop-to-shell and
98/// `scheduler_disable_cmds` on shell exit; Eevdf = no setup needed).
99///
100/// `scheduler_enable_cmds` and `scheduler_disable_cmds` are extracted
101/// from the [`crate::test_support::SchedulerSpec::KernelBuiltin`]
102/// variant's `enable` and `disable` slices respectively; the other
103/// three variants emit empty vecs (no kernel-builtin shell ops to
104/// invoke).
105///
106/// `wprof_args`: requires the `wprof` cargo feature. When the
107/// feature is enabled and `Some`, replaces `WprofConfig::args`;
108/// without the feature, the value is ignored by `run_shell`.
109/// `None` means "use the default wprof args."
110///
111/// `performance_mode` mirrors the test's
112/// `#[ktstr_test(performance_mode)]` attribute so the shell VM
113/// reproduces vCPU pinning, hugepages, NUMA mbind, and SCHED_FIFO
114/// promotion when the test requested them.
115#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
116pub struct ShellTestDescriptor {
117 #[serde(default)]
118 pub numa_nodes: u32,
119 #[serde(default)]
120 pub llcs: u32,
121 #[serde(default)]
122 pub cores: u32,
123 #[serde(default)]
124 pub threads: u32,
125 #[serde(default)]
126 pub memory_mib: u32,
127 #[serde(default)]
128 pub wprof: bool,
129 #[serde(default)]
130 pub extra_include_files: Vec<String>,
131 #[serde(default)]
132 pub scheduler_name: String,
133 #[serde(default)]
134 pub scheduler_kind: SchedulerKind,
135 /// Custom wprof CLI args (requires the `wprof` cargo feature).
136 /// When `Some` and the feature is enabled, the shell VM
137 /// overrides `WprofConfig::args` with the space-tokenised
138 /// value before booting.
139 #[serde(default)]
140 pub wprof_args: Option<String>,
141 /// Mirrors `KtstrTestEntry::performance_mode`. The shell VM
142 /// forwards this to
143 /// `crate::vmm::KtstrVmBuilder::performance_mode`.
144 #[serde(default)]
145 pub performance_mode: bool,
146 /// Shell commands invoked before drop-to-busybox when the test's
147 /// scheduler is a
148 /// [`crate::test_support::SchedulerSpec::KernelBuiltin`] variant —
149 /// empty vec for the other three variants. Populated from the
150 /// variant's `enable` slice.
151 #[serde(default)]
152 pub scheduler_enable_cmds: Vec<String>,
153 /// Shell commands invoked on shell exit when the test's scheduler
154 /// is a [`crate::test_support::SchedulerSpec::KernelBuiltin`]
155 /// variant — empty vec for the other three variants. Populated
156 /// from the variant's `disable` slice.
157 #[serde(default)]
158 pub scheduler_disable_cmds: Vec<String>,
159}
160
161#[cfg(test)]
162mod tests {
163 use super::*;
164
165 fn fully_populated() -> ShellTestDescriptor {
166 ShellTestDescriptor {
167 numa_nodes: 2,
168 llcs: 4,
169 cores: 6,
170 threads: 2,
171 memory_mib: 4096,
172 wprof: true,
173 extra_include_files: vec!["a:/x".to_string(), "b:/y".to_string()],
174 scheduler_name: "scx_test".to_string(),
175 scheduler_kind: SchedulerKind::KernelBuiltin,
176 wprof_args: Some("-d 2000 -e sched,irq".to_string()),
177 performance_mode: true,
178 scheduler_enable_cmds: vec!["echo on > /sys/kernel/debug/foo".to_string()],
179 scheduler_disable_cmds: vec!["echo off > /sys/kernel/debug/foo".to_string()],
180 }
181 }
182
183 #[test]
184 fn roundtrip_preserves_every_field() {
185 let original = fully_populated();
186 let json = serde_json::to_string(&original).expect("serialize fully-populated descriptor");
187 let parsed: ShellTestDescriptor =
188 serde_json::from_str(&json).expect("deserialize fully-populated descriptor");
189 assert_eq!(parsed, original);
190 }
191
192 #[test]
193 fn missing_new_fields_default_to_empty() {
194 // JSON shape emitted by an older test binary that doesn't
195 // know about wprof_args, performance_mode,
196 // scheduler_enable_cmds, or scheduler_disable_cmds. The
197 // four new fields are absent — each must take its serde
198 // default (None, false, empty vec) rather than failing the
199 // deserialize.
200 let legacy_json = r#"{
201 "numa_nodes": 1,
202 "llcs": 1,
203 "cores": 2,
204 "threads": 1,
205 "memory_mib": 1024,
206 "wprof": false,
207 "extra_include_files": [],
208 "scheduler_name": "scx_legacy",
209 "scheduler_kind": "discover"
210 }"#;
211 let parsed: ShellTestDescriptor =
212 serde_json::from_str(legacy_json).expect("legacy JSON missing new fields must parse");
213 assert_eq!(parsed.wprof_args, None);
214 assert!(!parsed.performance_mode);
215 assert!(parsed.scheduler_enable_cmds.is_empty());
216 assert!(parsed.scheduler_disable_cmds.is_empty());
217 // Existing fields still populate correctly.
218 assert_eq!(parsed.numa_nodes, 1);
219 assert_eq!(parsed.scheduler_name, "scx_legacy");
220 // Pin the snake_case-string → enum conversion at the
221 // legacy-JSON forward-compat boundary: a regression that
222 // dropped the rename-all gate would deserialize Discover
223 // as Eevdf default and break the consumer's banner branch.
224 assert_eq!(parsed.scheduler_kind, SchedulerKind::Discover);
225 }
226
227 /// Per-variant wire-roundtrip pin. Catches regressions that
228 /// rename a variant's snake_case form (e.g. accidentally
229 /// dropping the `_` in `kernel_builtin`) or break a single
230 /// variant's serde codepath while leaving the others working.
231 #[test]
232 fn scheduler_kind_serde_each_variant_roundtrips_snake_case() {
233 for (variant, wire) in [
234 (SchedulerKind::Eevdf, "\"eevdf\""),
235 (SchedulerKind::Discover, "\"discover\""),
236 (SchedulerKind::Path, "\"path\""),
237 (SchedulerKind::KernelBuiltin, "\"kernel_builtin\""),
238 ] {
239 let json = serde_json::to_string(&variant).expect("serialize");
240 assert_eq!(json, wire, "serialize mismatch for {variant:?}");
241 let back: SchedulerKind = serde_json::from_str(wire).expect("deserialize");
242 assert_eq!(back, variant, "roundtrip mismatch for {variant:?}");
243 }
244 }
245
246 /// Display impl must produce the same lowercase strings the
247 /// wire format does. Banner formatting depends on this
248 /// equivalence — a drift between Display and serde would
249 /// silently print a string the operator can't grep-correlate
250 /// with the JSON shape they'd see in any tooling output.
251 #[test]
252 fn scheduler_kind_display_matches_serde_snake_case() {
253 for variant in [
254 SchedulerKind::Eevdf,
255 SchedulerKind::Discover,
256 SchedulerKind::Path,
257 SchedulerKind::KernelBuiltin,
258 ] {
259 let json = serde_json::to_string(&variant).expect("serialize");
260 let unquoted = json.trim_matches('"');
261 assert_eq!(
262 variant.to_string(),
263 unquoted,
264 "Display must equal serde wire form for {variant:?}",
265 );
266 }
267 }
268
269 /// From<&SchedulerSpec> exhaustive coverage — pins each
270 /// variant's mapping. Catches typo regressions like
271 /// `SchedulerSpec::Path → SchedulerKind::Discover` that the
272 /// compile-time exhaustive match wouldn't catch (both arms
273 /// would still type-check; the wrong-mapping is silent).
274 #[test]
275 fn scheduler_kind_from_scheduler_spec_per_variant() {
276 use crate::test_support::SchedulerSpec;
277 assert_eq!(
278 SchedulerKind::from(&SchedulerSpec::Eevdf),
279 SchedulerKind::Eevdf,
280 );
281 assert_eq!(
282 SchedulerKind::from(&SchedulerSpec::Discover("scx_test")),
283 SchedulerKind::Discover,
284 );
285 assert_eq!(
286 SchedulerKind::from(&SchedulerSpec::Path("/bin/scx_test")),
287 SchedulerKind::Path,
288 );
289 assert_eq!(
290 SchedulerKind::from(&SchedulerSpec::KernelBuiltin {
291 enable: &[],
292 disable: &[],
293 }),
294 SchedulerKind::KernelBuiltin,
295 );
296 }
297
298 /// Unknown wire values MUST fail deserialize rather than
299 /// silently fall back to a default. Pins the typed-enum
300 /// strictness: a regression to `#[serde(other)]` catchall or
301 /// re-introduction of a String field would silently accept the
302 /// junk value and break consumer banner gates.
303 #[test]
304 fn scheduler_kind_unknown_variant_fails_deserialize() {
305 let r: Result<SchedulerKind, _> = serde_json::from_str("\"rust\"");
306 assert!(r.is_err(), "unknown variant 'rust' must reject; got {r:?}",);
307 }
308
309 #[test]
310 fn missing_every_field_yields_full_defaults() {
311 // Pathological case: an empty object. Every field is
312 // #[serde(default)] so deserialize succeeds and every
313 // field becomes its type default. Forward-compat applies to
314 // existing fields too — a future cargo-ktstr that adds yet
315 // another field shouldn't choke on an old JSON missing the
316 // new field, and the same logic protects existing fields
317 // against a JSON shape they happen to be missing from.
318 let empty_json = "{}";
319 let parsed: ShellTestDescriptor =
320 serde_json::from_str(empty_json).expect("empty JSON must parse with all defaults");
321 assert_eq!(parsed.numa_nodes, 0);
322 assert_eq!(parsed.scheduler_name, "");
323 // SchedulerKind::Default = Eevdf, the no-scx control — the
324 // safest fallback for an empty object: the consumer's
325 // KernelBuiltin/non-Eevdf banner gates won't fire, matching
326 // the prior empty-string behavior where the !="kernel_builtin"
327 // and !="eevdf" string compares fell through.
328 assert_eq!(parsed.scheduler_kind, SchedulerKind::Eevdf);
329 assert!(parsed.extra_include_files.is_empty());
330 assert_eq!(parsed.wprof_args, None);
331 assert!(!parsed.performance_mode);
332 assert!(parsed.scheduler_enable_cmds.is_empty());
333 assert!(parsed.scheduler_disable_cmds.is_empty());
334 }
335}