#![cfg(test)]
use clap::{CommandFactory, Parser};
use ktstr::cache::{CacheArtifacts, CacheDir, CacheEntry, KernelMetadata};
use ktstr::cli;
use ktstr::cli::KernelCommand;
use crate::cli::{Cargo, CargoSub, KtstrCommand, StatsCommand};
fn parse_via_split(argv: &[&str]) -> KtstrCommand {
let raw: Vec<std::ffi::OsString> = argv.iter().map(std::ffi::OsString::from).collect();
let rewritten = crate::argsplit::rewrite(&Cargo::command(), &raw);
let Cargo {
command: CargoSub::Ktstr(k),
} = Cargo::try_parse_from(&rewritten).unwrap_or_else(|e| panic!("{e}"));
k.command
}
#[test]
fn parse_perf_delta_flags_and_defaults() {
let Cargo {
command: CargoSub::Ktstr(k),
} = Cargo::try_parse_from([
"cargo",
"ktstr",
"perf-delta",
"--base",
"abc123",
"--base-ref",
"release",
"-E",
"perf::",
"--kernel",
"6.14",
"--threshold",
"12.5",
])
.unwrap_or_else(|e| panic!("{e}"));
match k.command {
KtstrCommand::PerfDelta {
base,
base_ref,
filter,
relevant,
default_branch,
kernel,
threshold,
policy,
noise_adjust,
noise_spread_threshold,
no_phases,
phases_only,
steps_only,
phase,
phase_threshold,
profile,
nextest_profile,
all_metrics,
fail_threshold,
must_fail,
args,
} => {
assert!(args.is_empty(), "no cargo passthrough on this invocation");
assert!(
!all_metrics && fail_threshold.is_none() && must_fail.is_none(),
"gating / render flags default off when unset",
);
assert_eq!(base.as_deref(), Some("abc123"));
assert_eq!(base_ref.as_deref(), Some("release"));
assert_eq!(filter.as_deref(), Some("perf::"));
assert!(!relevant, "--relevant defaults off");
assert_eq!(default_branch, "main", "default branch defaults to main");
assert_eq!(kernel.as_deref(), Some("6.14"));
assert_eq!(threshold, Some(12.5));
assert!(policy.is_none());
assert!(
!no_phases
&& !phases_only
&& !steps_only
&& phase.is_none()
&& phase_threshold.is_none(),
"phase flags require --noise-adjust and are absent on the scalar path",
);
assert!(
noise_adjust.is_none() && noise_spread_threshold.is_none(),
"no --noise-adjust on this invocation",
);
assert!(
profile.is_none() && nextest_profile.is_none(),
"no --profile / --nextest-profile on this invocation",
);
}
_ => panic!("expected PerfDelta"),
}
let Cargo {
command: CargoSub::Ktstr(k),
} = Cargo::try_parse_from(["cargo", "ktstr", "perf-delta"]).unwrap_or_else(|e| panic!("{e}"));
match k.command {
KtstrCommand::PerfDelta {
base,
base_ref,
filter,
relevant,
default_branch,
kernel,
threshold,
policy,
noise_adjust,
noise_spread_threshold,
no_phases,
phases_only,
steps_only,
phase,
phase_threshold,
profile,
nextest_profile,
all_metrics,
fail_threshold,
must_fail,
args,
} => {
assert!(
args.is_empty(),
"bare perf-delta parses no cargo passthrough"
);
assert!(
!all_metrics && fail_threshold.is_none() && must_fail.is_none(),
"bare perf-delta defaults gating / render flags off",
);
assert!(base.is_none() && base_ref.is_none() && filter.is_none());
assert!(!relevant, "bare perf-delta defaults --relevant off");
assert_eq!(default_branch, "main");
assert!(kernel.is_none());
assert!(threshold.is_none() && policy.is_none());
assert!(
!no_phases
&& !phases_only
&& !steps_only
&& phase.is_none()
&& phase_threshold.is_none(),
"phase flags default off (meaningful rows shown by default)",
);
assert!(noise_adjust.is_none() && noise_spread_threshold.is_none());
assert!(
profile.is_none() && nextest_profile.is_none(),
"bare perf-delta defaults --profile / --nextest-profile to None",
);
}
_ => panic!("expected PerfDelta"),
}
let Cargo {
command: CargoSub::Ktstr(k),
} = Cargo::try_parse_from([
"cargo",
"ktstr",
"perf-delta",
"--noise-adjust",
"3",
"--noise-spread-threshold",
"1.5",
"--phases-only",
"--steps-only",
"--phase-threshold",
"5",
"--all-metrics",
"--fail-threshold",
"3",
"--must-fail",
"worst_spread",
])
.unwrap_or_else(|e| panic!("{e}"));
match k.command {
KtstrCommand::PerfDelta {
noise_adjust,
noise_spread_threshold,
phases_only,
steps_only,
phase_threshold,
all_metrics,
fail_threshold,
must_fail,
..
} => {
assert_eq!(noise_adjust, Some(3));
assert_eq!(noise_spread_threshold, Some(1.5));
assert!(
phases_only,
"--phases-only round-trips under --noise-adjust"
);
assert!(steps_only, "--steps-only round-trips under --noise-adjust");
assert_eq!(
phase_threshold,
Some(5.0),
"--phase-threshold round-trips under --noise-adjust",
);
assert!(all_metrics, "--all-metrics round-trips");
assert_eq!(fail_threshold, Some(3), "--fail-threshold N round-trips");
assert_eq!(
must_fail.as_deref(),
Some("worst_spread"),
"--must-fail round-trips the raw csv (validated in run())",
);
}
_ => panic!("expected PerfDelta"),
}
assert!(
Cargo::try_parse_from(["cargo", "ktstr", "perf-delta", "--noise-adjust", "1"]).is_err(),
"--noise-adjust 1 must be rejected at parse time (needs >= 2 runs)",
);
assert!(
Cargo::try_parse_from([
"cargo",
"ktstr",
"perf-delta",
"--noise-spread-threshold",
"1.0"
])
.is_err(),
"--noise-spread-threshold alone must fail (requires --noise-adjust)",
);
assert!(
Cargo::try_parse_from([
"cargo",
"ktstr",
"perf-delta",
"--noise-adjust",
"3",
"--threshold",
"10",
])
.is_err(),
"--noise-adjust must conflict with --threshold",
);
assert!(
Cargo::try_parse_from([
"cargo",
"ktstr",
"perf-delta",
"--threshold",
"10",
"--policy",
"/tmp/p.json",
])
.is_err(),
"--threshold and --policy must conflict at parse time",
);
assert!(
Cargo::try_parse_from([
"cargo",
"ktstr",
"perf-delta",
"--noise-adjust",
"3",
"--no-phases",
"--phases-only"
])
.is_err(),
"--no-phases must conflict with --phases-only",
);
assert!(
Cargo::try_parse_from([
"cargo",
"ktstr",
"perf-delta",
"--noise-adjust",
"3",
"--steps-only",
"--phase",
"1"
])
.is_err(),
"--steps-only must conflict with --phase",
);
assert!(
Cargo::try_parse_from(["cargo", "ktstr", "perf-delta", "--no-phases"]).is_err(),
"--no-phases without --noise-adjust must be rejected (requires = noise_adjust)",
);
assert!(
Cargo::try_parse_from(["cargo", "ktstr", "perf-delta", "--phases-only"]).is_err(),
"--phases-only without --noise-adjust must be rejected (requires = noise_adjust)",
);
assert!(
Cargo::try_parse_from(["cargo", "ktstr", "perf-delta", "--steps-only"]).is_err(),
"--steps-only without --noise-adjust must be rejected (requires = noise_adjust)",
);
assert!(
Cargo::try_parse_from(["cargo", "ktstr", "perf-delta", "--phase", "1"]).is_err(),
"--phase without --noise-adjust must be rejected (requires = noise_adjust)",
);
assert!(
Cargo::try_parse_from(["cargo", "ktstr", "perf-delta", "--phase-threshold", "5"]).is_err(),
"--phase-threshold without --noise-adjust must be rejected (requires = noise_adjust)",
);
}
#[test]
fn parse_perf_delta_with_profiles() {
let Cargo {
command: CargoSub::Ktstr(k),
} = Cargo::try_parse_from([
"cargo",
"ktstr",
"perf-delta",
"--noise-adjust",
"3",
"--kernel",
"6.14",
"--profile",
"dev",
"--nextest-profile",
"ci",
])
.unwrap_or_else(|e| panic!("{e}"));
let KtstrCommand::PerfDelta {
profile,
nextest_profile,
..
} = k.command
else {
panic!("expected PerfDelta");
};
assert_eq!(profile.as_deref(), Some("dev"), "--profile round-trips");
assert_eq!(
nextest_profile.as_deref(),
Some("ci"),
"--nextest-profile round-trips"
);
}
fn assert_passthrough_args(subcommand: &str, passthrough: &[&str]) {
let mut argv: Vec<&str> = vec!["cargo", "ktstr", subcommand, "--"];
argv.extend(passthrough.iter().copied());
let Cargo {
command: CargoSub::Ktstr(k),
} = Cargo::try_parse_from(argv).unwrap_or_else(|e| panic!("{e}"));
let expected: Vec<String> = passthrough.iter().map(|s| s.to_string()).collect();
match k.command {
KtstrCommand::Test {
kernel,
no_perf_mode,
no_skip_mode,
release,
profile,
nextest_profile,
include_eol,
relevant,
base,
base_ref,
default_branch,
args,
} => {
assert!(
kernel.is_empty(),
"bare `--` passthrough must not spuriously populate --kernel",
);
assert!(
!no_perf_mode,
"bare `--` passthrough must not spuriously set --no-perf-mode",
);
assert!(
!no_skip_mode,
"bare `--` passthrough must not spuriously set --no-skip-mode",
);
assert!(
!release,
"bare `--` passthrough must not spuriously set --release",
);
assert!(
profile.is_none(),
"bare `--` passthrough must not spuriously set --profile",
);
assert!(
nextest_profile.is_none(),
"bare `--` passthrough must not spuriously set --nextest-profile",
);
assert!(
!include_eol,
"bare `--` passthrough must not spuriously set --include-eol",
);
assert!(
!relevant && base.is_none() && base_ref.is_none() && default_branch == "main",
"bare `--` passthrough must not spuriously set --relevant / base flags",
);
assert_eq!(args, expected);
}
KtstrCommand::Coverage {
kernel,
no_perf_mode,
no_skip_mode,
release,
profile,
nextest_profile,
include_eol,
relevant,
base,
base_ref,
default_branch,
args,
} => {
assert!(
kernel.is_empty(),
"bare `--` passthrough must not spuriously populate --kernel",
);
assert!(
!no_perf_mode,
"bare `--` passthrough must not spuriously set --no-perf-mode",
);
assert!(
!no_skip_mode,
"bare `--` passthrough must not spuriously set --no-skip-mode",
);
assert!(
!release,
"bare `--` passthrough must not spuriously set --release",
);
assert!(
profile.is_none(),
"bare `--` passthrough must not spuriously set --profile",
);
assert!(
nextest_profile.is_none(),
"bare `--` passthrough must not spuriously set --nextest-profile",
);
assert!(
!include_eol,
"bare `--` passthrough must not spuriously set --include-eol",
);
assert!(
!relevant && base.is_none() && base_ref.is_none() && default_branch == "main",
"bare `--` passthrough must not spuriously set --relevant / base flags",
);
assert_eq!(args, expected);
}
KtstrCommand::LlvmCov {
kernel,
no_perf_mode,
no_skip_mode,
include_eol,
args,
} => {
assert!(
kernel.is_empty(),
"bare `--` passthrough must not spuriously populate --kernel",
);
assert!(
!no_perf_mode,
"bare `--` passthrough must not spuriously set --no-perf-mode",
);
assert!(
!no_skip_mode,
"bare `--` passthrough must not spuriously set --no-skip-mode",
);
assert!(
!include_eol,
"bare `--` passthrough must not spuriously set --include-eol",
);
assert_eq!(args, expected);
}
_ => panic!("expected passthrough-bearing variant for `{subcommand}`"),
}
}
#[test]
fn cli_debug_assert() {
Cargo::command().debug_assert();
}
#[test]
fn parse_test_minimal() {
let m = Cargo::try_parse_from(["cargo", "ktstr", "test"]);
assert!(m.is_ok(), "{}", m.err().unwrap());
}
#[test]
fn parse_test_with_kernel() {
let m = Cargo::try_parse_from(["cargo", "ktstr", "test", "--kernel", "6.14.2"]);
assert!(m.is_ok(), "{}", m.err().unwrap());
}
#[test]
fn parse_test_with_release_flag() {
let Cargo {
command: CargoSub::Ktstr(k),
} = Cargo::try_parse_from(["cargo", "ktstr", "test", "--release"])
.unwrap_or_else(|e| panic!("{e}"));
let KtstrCommand::Test { release, .. } = k.command else {
panic!("expected Test");
};
assert!(release, "`--release` must set `release=true`");
}
#[test]
fn parse_test_with_profile_flag() {
let Cargo {
command: CargoSub::Ktstr(k),
} = Cargo::try_parse_from(["cargo", "ktstr", "test", "--profile", "dev"])
.unwrap_or_else(|e| panic!("{e}"));
let KtstrCommand::Test {
release, profile, ..
} = k.command
else {
panic!("expected Test");
};
assert_eq!(
profile.as_deref(),
Some("dev"),
"`--profile dev` must set profile=Some(\"dev\")"
);
assert!(
!release,
"`--profile` alone must NOT set --release (the harness stays on its default)"
);
}
#[test]
fn parse_test_with_nextest_profile_flag() {
let Cargo {
command: CargoSub::Ktstr(k),
} = Cargo::try_parse_from(["cargo", "ktstr", "test", "--nextest-profile", "ci"])
.unwrap_or_else(|e| panic!("{e}"));
let KtstrCommand::Test {
release,
profile,
nextest_profile,
..
} = k.command
else {
panic!("expected Test");
};
assert_eq!(
nextest_profile.as_deref(),
Some("ci"),
"`--nextest-profile ci` must set nextest_profile=Some(\"ci\")"
);
assert!(
profile.is_none(),
"`--nextest-profile` must NOT set --profile (the scheduler build profile)"
);
assert!(!release, "`--nextest-profile` alone must NOT set --release");
}
#[test]
fn parse_test_include_eol_flag() {
let Cargo {
command: CargoSub::Ktstr(k),
} = Cargo::try_parse_from([
"cargo",
"ktstr",
"test",
"--kernel",
"6.11..6.14",
"--include-eol",
])
.unwrap_or_else(|e| panic!("{e}"));
let KtstrCommand::Test {
kernel,
include_eol,
..
} = k.command
else {
panic!("expected Test");
};
assert_eq!(kernel, vec!["6.11..6.14".to_string()]);
assert!(
include_eol,
"`--include-eol` must round-trip as true so the range expands EOL series"
);
}
#[test]
fn parse_test_relevant_flags() {
let command = parse_via_split(&[
"cargo",
"ktstr",
"test",
"--relevant",
"--base",
"abc123",
"--base-ref",
"release",
"--default-branch",
"dev",
"-E",
"test(foo)",
]);
let KtstrCommand::Test {
relevant,
base,
base_ref,
default_branch,
args,
..
} = command
else {
panic!("expected Test");
};
assert!(relevant, "--relevant round-trips");
assert_eq!(base.as_deref(), Some("abc123"));
assert_eq!(base_ref.as_deref(), Some("release"));
assert_eq!(default_branch, "dev");
assert_eq!(
args,
vec!["-E".to_string(), "test(foo)".to_string()],
"a user -E is not ktstr-owned on `test`; it stays in the passthrough",
);
}
#[test]
fn parse_test_with_passthrough_args() {
assert_passthrough_args("test", &["-p", "ktstr", "--no-capture"]);
}
#[test]
fn parse_nextest_alias_dispatches_to_test() {
let Cargo {
command: CargoSub::Ktstr(k),
} = Cargo::try_parse_from(["cargo", "ktstr", "nextest"]).unwrap_or_else(|e| panic!("{e}"));
assert!(
matches!(k.command, KtstrCommand::Test { .. }),
"`nextest` alias must dispatch to the Test variant",
);
}
#[test]
fn parse_nextest_alias_with_passthrough_args() {
assert_passthrough_args("nextest", &["-p", "ktstr", "--no-capture"]);
}
#[test]
fn parse_nextest_alias_with_kernel_and_no_perf_mode() {
let Cargo {
command: CargoSub::Ktstr(k),
} = Cargo::try_parse_from([
"cargo",
"ktstr",
"nextest",
"--kernel",
"6.14.2",
"--no-perf-mode",
])
.unwrap_or_else(|e| panic!("{e}"));
let KtstrCommand::Test {
kernel,
no_perf_mode,
no_skip_mode,
release,
profile,
nextest_profile,
include_eol,
args,
..
} = k.command
else {
panic!("expected Test (via `nextest` alias)");
};
assert_eq!(kernel, vec!["6.14.2".to_string()]);
assert!(no_perf_mode);
assert!(!no_skip_mode);
assert!(!release, "bare invocation must default --release to false");
assert!(
profile.is_none(),
"bare invocation must default --profile to None"
);
assert!(
nextest_profile.is_none(),
"bare invocation must default --nextest-profile to None"
);
assert!(
!include_eol,
"bare invocation must default --include-eol to false"
);
assert!(args.is_empty());
}
#[test]
fn parse_coverage_minimal() {
let m = Cargo::try_parse_from(["cargo", "ktstr", "coverage"]);
assert!(m.is_ok(), "{}", m.err().unwrap());
}
#[test]
fn parse_coverage_with_kernel() {
let m = Cargo::try_parse_from(["cargo", "ktstr", "coverage", "--kernel", "6.14.2"]);
assert!(m.is_ok(), "{}", m.err().unwrap());
}
#[test]
fn parse_coverage_include_eol_flag() {
let Cargo {
command: CargoSub::Ktstr(k),
} = Cargo::try_parse_from([
"cargo",
"ktstr",
"coverage",
"--kernel",
"6.11..6.14",
"--include-eol",
])
.unwrap_or_else(|e| panic!("{e}"));
let KtstrCommand::Coverage {
kernel,
include_eol,
..
} = k.command
else {
panic!("expected Coverage");
};
assert_eq!(kernel, vec!["6.11..6.14".to_string()]);
assert!(
include_eol,
"`--include-eol` must round-trip as true on coverage"
);
}
#[test]
fn parse_coverage_with_release_flag() {
let Cargo {
command: CargoSub::Ktstr(k),
} = Cargo::try_parse_from(["cargo", "ktstr", "coverage", "--release"])
.unwrap_or_else(|e| panic!("{e}"));
let KtstrCommand::Coverage { release, .. } = k.command else {
panic!("expected Coverage");
};
assert!(release, "`--release` must set `release=true`");
}
#[test]
fn parse_coverage_with_profile_flags() {
let Cargo {
command: CargoSub::Ktstr(k),
} = Cargo::try_parse_from([
"cargo",
"ktstr",
"coverage",
"--profile",
"dev",
"--nextest-profile",
"ci",
])
.unwrap_or_else(|e| panic!("{e}"));
let KtstrCommand::Coverage {
release,
profile,
nextest_profile,
..
} = k.command
else {
panic!("expected Coverage");
};
assert_eq!(
profile.as_deref(),
Some("dev"),
"`--profile dev` must set profile=Some(\"dev\") on coverage"
);
assert_eq!(
nextest_profile.as_deref(),
Some("ci"),
"`--nextest-profile ci` must set nextest_profile=Some(\"ci\") on coverage"
);
assert!(
!release,
"`--profile`/`--nextest-profile` alone must NOT set --release"
);
}
#[test]
fn parse_coverage_with_passthrough_args() {
assert_passthrough_args(
"coverage",
&["--workspace", "--lcov", "--output-path", "lcov.info"],
);
}
#[test]
fn parse_coverage_with_kernel_and_no_perf_mode() {
let Cargo {
command: CargoSub::Ktstr(k),
} = Cargo::try_parse_from([
"cargo",
"ktstr",
"coverage",
"--kernel",
"6.14.2",
"--no-perf-mode",
"--",
"--workspace",
])
.unwrap_or_else(|e| panic!("{e}"));
let KtstrCommand::Coverage {
kernel,
no_perf_mode,
no_skip_mode,
release,
profile,
nextest_profile,
include_eol,
args,
..
} = k.command
else {
panic!("expected Coverage");
};
assert_eq!(kernel, vec!["6.14.2".to_string()]);
assert!(no_perf_mode);
assert!(!no_skip_mode);
assert!(!release, "bare invocation must default --release to false");
assert!(
profile.is_none(),
"bare invocation must default --profile to None"
);
assert!(
nextest_profile.is_none(),
"bare invocation must default --nextest-profile to None"
);
assert!(
!include_eol,
"bare invocation must default --include-eol to false"
);
assert_eq!(args, vec!["--workspace"]);
}
#[test]
fn parse_llvm_cov_minimal() {
let m = Cargo::try_parse_from(["cargo", "ktstr", "llvm-cov"]);
assert!(m.is_ok(), "{}", m.err().unwrap());
}
#[test]
fn parse_llvm_cov_with_kernel() {
let Cargo {
command: CargoSub::Ktstr(k),
} = Cargo::try_parse_from(["cargo", "ktstr", "llvm-cov", "--kernel", "6.14.2"])
.unwrap_or_else(|e| panic!("{e}"));
let KtstrCommand::LlvmCov { kernel, .. } = k.command else {
panic!("expected LlvmCov");
};
assert_eq!(kernel, vec!["6.14.2".to_string()]);
}
#[test]
fn parse_llvm_cov_include_eol_flag() {
let Cargo {
command: CargoSub::Ktstr(k),
} = Cargo::try_parse_from([
"cargo",
"ktstr",
"llvm-cov",
"--kernel",
"6.11..6.14",
"--include-eol",
])
.unwrap_or_else(|e| panic!("{e}"));
let KtstrCommand::LlvmCov {
kernel,
include_eol,
..
} = k.command
else {
panic!("expected LlvmCov");
};
assert_eq!(kernel, vec!["6.11..6.14".to_string()]);
assert!(
include_eol,
"`--include-eol` must round-trip as true on llvm-cov"
);
}
#[test]
fn parse_llvm_cov_with_passthrough_args() {
assert_passthrough_args(
"llvm-cov",
&["report", "--lcov", "--output-path", "lcov.info"],
);
}
#[test]
fn parse_llvm_cov_with_kernel_and_no_perf_mode() {
let Cargo {
command: CargoSub::Ktstr(k),
} = Cargo::try_parse_from([
"cargo",
"ktstr",
"llvm-cov",
"--kernel",
"6.14.2",
"--no-perf-mode",
"--",
"report",
"--lcov",
])
.unwrap_or_else(|e| panic!("{e}"));
let KtstrCommand::LlvmCov {
kernel,
no_perf_mode,
no_skip_mode,
include_eol,
args,
} = k.command
else {
panic!("expected LlvmCov");
};
assert_eq!(kernel, vec!["6.14.2".to_string()]);
assert!(no_perf_mode);
assert!(!no_skip_mode);
assert!(
!include_eol,
"bare invocation must default --include-eol to false"
);
assert_eq!(args, vec!["report", "--lcov"]);
}
#[test]
fn parse_llvm_cov_underscore_rejected() {
let rejected = Cargo::try_parse_from(["cargo", "ktstr", "llvm_cov"]);
assert!(
rejected.is_err(),
"`llvm_cov` (underscore) must be rejected — the \
canonical name is `llvm-cov` (kebab-case)",
);
}
#[test]
fn parse_llvm_cov_kebab_accepted() {
let Cargo {
command: CargoSub::Ktstr(k),
} = Cargo::try_parse_from(["cargo", "ktstr", "llvm-cov"]).unwrap_or_else(|e| panic!("{e}"));
assert!(
matches!(k.command, KtstrCommand::LlvmCov { .. }),
"kebab `llvm-cov` must bind to KtstrCommand::LlvmCov",
);
}
#[test]
fn parse_shell_minimal() {
let m = Cargo::try_parse_from(["cargo", "ktstr", "shell"]);
assert!(m.is_ok(), "{}", m.err().unwrap());
}
#[test]
fn parse_shell_with_topology() {
let Cargo {
command: CargoSub::Ktstr(k),
} = Cargo::try_parse_from(["cargo", "ktstr", "shell", "--topology", "1,2,4,1"])
.unwrap_or_else(|e| panic!("{e}"));
let KtstrCommand::Shell { topology, .. } = k.command else {
panic!("expected Shell");
};
assert_eq!(topology, "1,2,4,1");
}
#[test]
fn parse_shell_default_topology() {
let Cargo {
command: CargoSub::Ktstr(k),
} = Cargo::try_parse_from(["cargo", "ktstr", "shell"]).unwrap_or_else(|e| panic!("{e}"));
let KtstrCommand::Shell { topology, .. } = k.command else {
panic!("expected Shell");
};
assert_eq!(topology, "1,1,1,1");
}
#[test]
fn parse_shell_include_files() {
let Cargo {
command: CargoSub::Ktstr(k),
} = Cargo::try_parse_from(["cargo", "ktstr", "shell", "-i", "/tmp/a", "-i", "/tmp/b"])
.unwrap_or_else(|e| panic!("{e}"));
let KtstrCommand::Shell { include_files, .. } = k.command else {
panic!("expected Shell");
};
assert_eq!(
include_files,
vec![
std::path::PathBuf::from("/tmp/a"),
std::path::PathBuf::from("/tmp/b"),
],
"-i flag must accumulate paths in order via ArgAction::Append",
);
}
#[test]
fn parse_shell_disk_arg() {
let Cargo {
command: CargoSub::Ktstr(k),
} = Cargo::try_parse_from(["cargo", "ktstr", "shell", "--disk", "256mib"])
.unwrap_or_else(|e| panic!("{e}"));
let KtstrCommand::Shell { disk, .. } = k.command else {
panic!("expected Shell");
};
assert_eq!(disk.as_deref(), Some("256mib"));
}
#[test]
fn parse_shell_disk_arg_omitted() {
let Cargo {
command: CargoSub::Ktstr(k),
} = Cargo::try_parse_from(["cargo", "ktstr", "shell"]).unwrap_or_else(|e| panic!("{e}"));
let KtstrCommand::Shell { disk, .. } = k.command else {
panic!("expected Shell");
};
assert!(disk.is_none(), "no --disk must produce None");
}
#[test]
fn parse_stats_bare() {
let m = Cargo::try_parse_from(["cargo", "ktstr", "stats"]);
assert!(m.is_ok(), "{}", m.err().unwrap());
}
#[test]
fn parse_stats_list() {
let m = Cargo::try_parse_from(["cargo", "ktstr", "stats", "list"]);
assert!(m.is_ok(), "{}", m.err().unwrap());
}
#[test]
fn parse_stats_list_metrics_bare() {
let Cargo {
command: CargoSub::Ktstr(k),
} = Cargo::try_parse_from(["cargo", "ktstr", "stats", "list-metrics"])
.unwrap_or_else(|e| panic!("{e}"));
let KtstrCommand::Stats {
command: Some(StatsCommand::ListMetrics { json }),
..
} = k.command
else {
panic!("expected Stats ListMetrics");
};
assert!(
!json,
"bare `list-metrics` must default to text mode (json=false)",
);
}
#[test]
fn parse_stats_list_metrics_json() {
let Cargo {
command: CargoSub::Ktstr(k),
} = Cargo::try_parse_from(["cargo", "ktstr", "stats", "list-metrics", "--json"])
.unwrap_or_else(|e| panic!("{e}"));
let KtstrCommand::Stats {
command: Some(StatsCommand::ListMetrics { json }),
..
} = k.command
else {
panic!("expected Stats ListMetrics");
};
assert!(json, "--json must set the flag true");
}
#[test]
fn parse_stats_list_metrics_rejects_positional() {
let rejected =
Cargo::try_parse_from(["cargo", "ktstr", "stats", "list-metrics", "worst_spread"]);
assert!(
rejected.is_err(),
"list-metrics must reject positional arguments",
);
}
#[test]
fn parse_stats_list_values_bare() {
let Cargo {
command: CargoSub::Ktstr(k),
} = Cargo::try_parse_from(["cargo", "ktstr", "stats", "list-values"])
.unwrap_or_else(|e| panic!("{e}"));
let KtstrCommand::Stats {
command: Some(StatsCommand::ListValues { json, dir }),
..
} = k.command
else {
panic!("expected Stats ListValues");
};
assert!(!json, "bare `list-values` must default to text mode");
assert!(
dir.is_none(),
"bare `list-values` must default to no --dir override"
);
}
#[test]
fn parse_stats_list_values_json() {
let Cargo {
command: CargoSub::Ktstr(k),
} = Cargo::try_parse_from(["cargo", "ktstr", "stats", "list-values", "--json"])
.unwrap_or_else(|e| panic!("{e}"));
let KtstrCommand::Stats {
command: Some(StatsCommand::ListValues { json, .. }),
..
} = k.command
else {
panic!("expected Stats ListValues");
};
assert!(json, "--json must set the flag true");
}
#[test]
fn parse_stats_list_values_with_dir() {
let Cargo {
command: CargoSub::Ktstr(k),
} = Cargo::try_parse_from([
"cargo",
"ktstr",
"stats",
"list-values",
"--dir",
"/tmp/archived-runs",
])
.unwrap_or_else(|e| panic!("{e}"));
let KtstrCommand::Stats {
command: Some(StatsCommand::ListValues { dir, json }),
..
} = k.command
else {
panic!("expected Stats ListValues");
};
assert_eq!(
dir.as_deref(),
Some(std::path::Path::new("/tmp/archived-runs")),
"--dir must round-trip to Some(PathBuf)",
);
assert!(!json, "bare --dir must not spuriously set --json");
}
#[test]
fn parse_stats_list_values_rejects_positional() {
let rejected = Cargo::try_parse_from(["cargo", "ktstr", "stats", "list-values", "kernel"]);
assert!(
rejected.is_err(),
"list-values must reject positional arguments",
);
}
#[test]
fn parse_stats_show_host_with_run() {
let Cargo {
command: CargoSub::Ktstr(k),
} = Cargo::try_parse_from(["cargo", "ktstr", "stats", "show-host", "--run", "my-run-id"])
.unwrap_or_else(|e| panic!("{e}"));
let KtstrCommand::Stats {
command: Some(StatsCommand::ShowHost { run, dir }),
..
} = k.command
else {
panic!("expected Stats ShowHost");
};
assert_eq!(run, "my-run-id");
assert!(dir.is_none(), "bare --run must not populate --dir");
}
#[test]
fn parse_stats_show_host_with_dir() {
let Cargo {
command: CargoSub::Ktstr(k),
} = Cargo::try_parse_from([
"cargo",
"ktstr",
"stats",
"show-host",
"--run",
"archive-2024-01-15",
"--dir",
"/tmp/archived-runs",
])
.unwrap_or_else(|e| panic!("{e}"));
let KtstrCommand::Stats {
command: Some(StatsCommand::ShowHost { run, dir }),
..
} = k.command
else {
panic!("expected Stats ShowHost");
};
assert_eq!(run, "archive-2024-01-15");
assert_eq!(
dir.as_deref(),
Some(std::path::Path::new("/tmp/archived-runs")),
);
}
#[test]
fn parse_stats_show_host_missing_run_rejected() {
let rejected = Cargo::try_parse_from(["cargo", "ktstr", "stats", "show-host"]);
assert!(rejected.is_err(), "stats show-host must require --run",);
}
#[test]
fn parse_stats_explain_sidecar_with_run() {
let Cargo {
command: CargoSub::Ktstr(k),
} = Cargo::try_parse_from([
"cargo",
"ktstr",
"stats",
"explain-sidecar",
"--run",
"my-run-id",
])
.unwrap_or_else(|e| panic!("{e}"));
let KtstrCommand::Stats {
command: Some(StatsCommand::ExplainSidecar { run, dir, json }),
..
} = k.command
else {
panic!("expected Stats ExplainSidecar");
};
assert_eq!(run, "my-run-id");
assert!(dir.is_none(), "bare --run must not populate --dir");
assert!(!json, "default output is text, not json");
}
#[test]
fn parse_stats_explain_sidecar_with_dir_and_json() {
let Cargo {
command: CargoSub::Ktstr(k),
} = Cargo::try_parse_from([
"cargo",
"ktstr",
"stats",
"explain-sidecar",
"--run",
"archive-2024-01-15",
"--dir",
"/tmp/archived-runs",
"--json",
])
.unwrap_or_else(|e| panic!("{e}"));
let KtstrCommand::Stats {
command: Some(StatsCommand::ExplainSidecar { run, dir, json }),
..
} = k.command
else {
panic!("expected Stats ExplainSidecar");
};
assert_eq!(run, "archive-2024-01-15");
assert_eq!(
dir.as_deref(),
Some(std::path::Path::new("/tmp/archived-runs")),
);
assert!(json, "--json must toggle aggregate JSON output");
}
#[test]
fn parse_stats_explain_sidecar_missing_run_rejected() {
let rejected = Cargo::try_parse_from(["cargo", "ktstr", "stats", "explain-sidecar"]);
assert!(
rejected.is_err(),
"stats explain-sidecar must require --run",
);
}
#[test]
fn parse_kernel_list() {
let m = Cargo::try_parse_from(["cargo", "ktstr", "kernel", "list"]);
assert!(m.is_ok(), "{}", m.err().unwrap());
}
#[test]
fn parse_kernel_list_json() {
let m = Cargo::try_parse_from(["cargo", "ktstr", "kernel", "list", "--json"]);
assert!(m.is_ok(), "{}", m.err().unwrap());
}
#[test]
fn parse_affected_flags_and_defaults() {
let Cargo {
command: CargoSub::Ktstr(k),
} = Cargo::try_parse_from(["cargo", "ktstr", "affected"]).unwrap_or_else(|e| panic!("{e}"));
match k.command {
KtstrCommand::Affected {
base,
base_ref,
default_branch,
} => {
assert!(base.is_none() && base_ref.is_none());
assert_eq!(default_branch, "main");
}
_ => panic!("expected Affected"),
}
let Cargo {
command: CargoSub::Ktstr(k),
} = Cargo::try_parse_from([
"cargo",
"ktstr",
"affected",
"--base",
"abc123",
"--base-ref",
"release",
"--default-branch",
"dev",
])
.unwrap_or_else(|e| panic!("{e}"));
match k.command {
KtstrCommand::Affected {
base,
base_ref,
default_branch,
} => {
assert_eq!(base.as_deref(), Some("abc123"));
assert_eq!(base_ref.as_deref(), Some("release"));
assert_eq!(default_branch, "dev");
}
_ => panic!("expected Affected"),
}
}
#[test]
fn parse_affected_rejects_positional() {
assert!(
Cargo::try_parse_from(["cargo", "ktstr", "affected", "stray-positional"]).is_err(),
"affected must reject a bare positional argument",
);
}
#[test]
fn parse_kernel_list_range() {
let Cargo {
command: CargoSub::Ktstr(k),
} = Cargo::try_parse_from(["cargo", "ktstr", "kernel", "list", "--kernel", "6.12..6.14"])
.unwrap_or_else(|e| panic!("{e}"));
let KtstrCommand::Kernel { command } = k.command else {
panic!("expected Kernel");
};
let KernelCommand::List {
json,
kernel,
include_eol,
} = command
else {
panic!("expected KernelCommand::List, got {command:?}");
};
assert!(!json, "bare --kernel must not enable --json");
assert_eq!(
kernel.as_deref(),
Some("6.12..6.14"),
"--kernel must round-trip the literal spec for \
dispatch to pass to `expand_kernel_range`",
);
assert!(
!include_eol,
"bare --kernel range must default --include-eol to false"
);
}
#[test]
fn parse_kernel_list_range_with_json() {
let Cargo {
command: CargoSub::Ktstr(k),
} = Cargo::try_parse_from([
"cargo",
"ktstr",
"kernel",
"list",
"--kernel",
"6.12..6.14",
"--json",
])
.unwrap_or_else(|e| panic!("{e}"));
let KtstrCommand::Kernel { command } = k.command else {
panic!("expected Kernel");
};
let KernelCommand::List {
json,
kernel,
include_eol,
} = command
else {
panic!("expected KernelCommand::List, got {command:?}");
};
assert!(json, "--json must round-trip alongside --kernel");
assert_eq!(kernel.as_deref(), Some("6.12..6.14"));
assert!(
!include_eol,
"--kernel --json without --include-eol must default it to false"
);
}
#[test]
fn parse_kernel_list_range_include_eol() {
let Cargo {
command: CargoSub::Ktstr(k),
} = Cargo::try_parse_from([
"cargo",
"ktstr",
"kernel",
"list",
"--kernel",
"6.11..6.14",
"--include-eol",
])
.unwrap_or_else(|e| panic!("{e}"));
let KtstrCommand::Kernel { command } = k.command else {
panic!("expected Kernel");
};
let KernelCommand::List {
kernel,
include_eol,
..
} = command
else {
panic!("expected KernelCommand::List, got {command:?}");
};
assert_eq!(kernel.as_deref(), Some("6.11..6.14"));
assert!(
include_eol,
"`--include-eol` must round-trip as true on kernel list --kernel"
);
}
#[test]
fn parse_kernel_build_version() {
let m = Cargo::try_parse_from(["cargo", "ktstr", "kernel", "build", "--kernel", "6.14.2"]);
assert!(m.is_ok(), "{}", m.err().unwrap());
}
#[test]
fn parse_kernel_build_range_include_eol() {
let Cargo {
command: CargoSub::Ktstr(k),
} = Cargo::try_parse_from([
"cargo",
"ktstr",
"kernel",
"build",
"--kernel",
"6.11..6.14",
"--include-eol",
])
.unwrap_or_else(|e| panic!("{e}"));
let KtstrCommand::Kernel { command } = k.command else {
panic!("expected Kernel");
};
let KernelCommand::Build {
kernel,
include_eol,
..
} = command
else {
panic!("expected KernelCommand::Build, got {command:?}");
};
assert_eq!(kernel.as_deref(), Some("6.11..6.14"));
assert!(
include_eol,
"`--include-eol` must round-trip as true on kernel build --kernel RANGE"
);
}
#[test]
fn parse_kernel_build_path() {
let m = Cargo::try_parse_from(["cargo", "ktstr", "kernel", "build", "--kernel", "../linux"]);
assert!(m.is_ok(), "{}", m.err().unwrap());
}
#[test]
fn parse_kernel_build_git() {
let Cargo {
command: CargoSub::Ktstr(k),
} = Cargo::try_parse_from([
"cargo",
"ktstr",
"kernel",
"build",
"--kernel",
"git+https://example.com/linux.git#tag=v6.14",
])
.unwrap_or_else(|e| panic!("{e}"));
let KtstrCommand::Kernel {
command: KernelCommand::Build { kernel, .. },
} = k.command
else {
panic!("expected KernelCommand::Build");
};
assert_eq!(
kernel.as_deref(),
Some("git+https://example.com/linux.git#tag=v6.14"),
"a git source must round-trip verbatim through --kernel",
);
}
#[test]
fn parse_kernel_build_with_extra_kconfig() {
let Cargo {
command: CargoSub::Ktstr(k),
} = Cargo::try_parse_from([
"cargo",
"ktstr",
"kernel",
"build",
"--kernel",
"6.14.2",
"--extra-kconfig",
"/tmp/extra.kconfig",
])
.unwrap_or_else(|e| panic!("{e}"));
let KtstrCommand::Kernel {
command:
KernelCommand::Build {
kernel,
extra_kconfig,
..
},
} = k.command
else {
panic!("expected KernelCommand::Build");
};
assert_eq!(kernel.as_deref(), Some("6.14.2"));
assert_eq!(
extra_kconfig,
Some(std::path::PathBuf::from("/tmp/extra.kconfig")),
"--extra-kconfig must round-trip the literal path",
);
}
#[test]
fn parse_kernel_build_without_extra_kconfig_is_none() {
let Cargo {
command: CargoSub::Ktstr(k),
} = Cargo::try_parse_from(["cargo", "ktstr", "kernel", "build", "--kernel", "6.14.2"])
.unwrap_or_else(|e| panic!("{e}"));
let KtstrCommand::Kernel {
command: KernelCommand::Build { extra_kconfig, .. },
} = k.command
else {
panic!("expected KernelCommand::Build");
};
assert!(
extra_kconfig.is_none(),
"no --extra-kconfig must produce None, got {extra_kconfig:?}",
);
}
#[test]
fn parse_kernel_build_with_skip_sha256() {
let Cargo {
command: CargoSub::Ktstr(k),
} = Cargo::try_parse_from([
"cargo",
"ktstr",
"kernel",
"build",
"--kernel",
"6.14.2",
"--skip-sha256",
])
.unwrap_or_else(|e| panic!("{e}"));
let KtstrCommand::Kernel {
command:
KernelCommand::Build {
kernel,
skip_sha256,
..
},
} = k.command
else {
panic!("expected KernelCommand::Build");
};
assert_eq!(kernel.as_deref(), Some("6.14.2"));
assert!(
skip_sha256,
"--skip-sha256 must round-trip as true; without this the \
download path would still verify against sha256sums.asc"
);
}
#[test]
fn parse_kernel_build_without_skip_sha256_is_false() {
let Cargo {
command: CargoSub::Ktstr(k),
} = Cargo::try_parse_from(["cargo", "ktstr", "kernel", "build", "--kernel", "6.14.2"])
.unwrap_or_else(|e| panic!("{e}"));
let KtstrCommand::Kernel {
command: KernelCommand::Build { skip_sha256, .. },
} = k.command
else {
panic!("expected KernelCommand::Build");
};
assert!(
!skip_sha256,
"absent --skip-sha256 must produce skip_sha256: false — \
the default must keep checksum verification enabled, \
got skip_sha256={skip_sha256}"
);
}
#[test]
fn parse_kernel_build_skip_sha256_with_path() {
let Cargo {
command: CargoSub::Ktstr(k),
} = Cargo::try_parse_from([
"cargo",
"ktstr",
"kernel",
"build",
"--kernel",
"/tmp/src",
"--skip-sha256",
])
.unwrap_or_else(|e| panic!("{e}"));
let KtstrCommand::Kernel {
command:
KernelCommand::Build {
kernel,
skip_sha256,
..
},
} = k.command
else {
panic!("expected KernelCommand::Build");
};
assert_eq!(kernel.as_deref(), Some("/tmp/src"));
assert!(
skip_sha256,
"--skip-sha256 must round-trip when combined with a --kernel \
<path> source (the help text promises the flag is a no-op \
there, but clap must still accept the combination)"
);
}
#[test]
fn parse_kernel_build_skip_sha256_underscore_rejected() {
let rejected = Cargo::try_parse_from([
"cargo",
"ktstr",
"kernel",
"build",
"--kernel",
"6.14.2",
"--skip_sha256",
]);
assert!(
rejected.is_err(),
"`--skip_sha256` (underscore) must be rejected — the \
canonical name is `--skip-sha256` (kebab-case)",
);
}
#[test]
fn parse_kernel_build_skip_sha256_range_compose() {
let Cargo {
command: CargoSub::Ktstr(k),
} = Cargo::try_parse_from([
"cargo",
"ktstr",
"kernel",
"build",
"--kernel",
"6.14.2..6.14.4",
"--skip-sha256",
])
.unwrap_or_else(|e| panic!("{e}"));
let KtstrCommand::Kernel {
command:
KernelCommand::Build {
kernel,
skip_sha256,
..
},
} = k.command
else {
panic!("expected KernelCommand::Build");
};
assert_eq!(kernel.as_deref(), Some("6.14.2..6.14.4"));
assert!(
skip_sha256,
"--skip-sha256 must round-trip on a range version so every \
per-version `kernel_build_one` invocation sees the bypass \
flag"
);
}
#[test]
fn parse_kernel_build_skip_sha256_with_extra_kconfig_and_force_clean() {
let Cargo {
command: CargoSub::Ktstr(k),
} = Cargo::try_parse_from([
"cargo",
"ktstr",
"kernel",
"build",
"--kernel",
"6.14.2",
"--skip-sha256",
"--extra-kconfig",
"/tmp/k",
"--force",
"--clean",
])
.unwrap_or_else(|e| panic!("{e}"));
let KtstrCommand::Kernel {
command:
KernelCommand::Build {
force,
clean,
extra_kconfig,
skip_sha256,
..
},
} = k.command
else {
panic!("expected KernelCommand::Build");
};
assert!(
force,
"--force must round-trip alongside --skip-sha256 + --clean + --extra-kconfig"
);
assert!(
clean,
"--clean must round-trip alongside --skip-sha256 + --force + --extra-kconfig"
);
assert_eq!(
extra_kconfig,
Some(std::path::PathBuf::from("/tmp/k")),
"--extra-kconfig must round-trip alongside --skip-sha256 + --force + --clean"
);
assert!(
skip_sha256,
"--skip-sha256 must round-trip alongside --force + --clean + --extra-kconfig"
);
}
#[test]
fn parse_kernel_build_range_with_extra_kconfig() {
let Cargo {
command: CargoSub::Ktstr(k),
} = Cargo::try_parse_from([
"cargo",
"ktstr",
"kernel",
"build",
"--kernel",
"6.14.2..6.14.4",
"--extra-kconfig",
"/tmp/range-extra.kconfig",
])
.unwrap_or_else(|e| panic!("{e}"));
let KtstrCommand::Kernel {
command:
KernelCommand::Build {
kernel,
extra_kconfig,
..
},
} = k.command
else {
panic!("expected KernelCommand::Build");
};
assert_eq!(kernel.as_deref(), Some("6.14.2..6.14.4"));
assert_eq!(
extra_kconfig,
Some(std::path::PathBuf::from("/tmp/range-extra.kconfig")),
);
}
#[test]
fn parse_kernel_build_force_clean_and_extra_kconfig_compose() {
let Cargo {
command: CargoSub::Ktstr(k),
} = Cargo::try_parse_from([
"cargo",
"ktstr",
"kernel",
"build",
"--kernel",
"../linux",
"--force",
"--clean",
"--extra-kconfig",
"/tmp/extra.kconfig",
])
.unwrap_or_else(|e| panic!("{e}"));
let KtstrCommand::Kernel {
command:
KernelCommand::Build {
force,
clean,
extra_kconfig,
..
},
} = k.command
else {
panic!("expected KernelCommand::Build");
};
assert!(
force,
"--force must round-trip alongside --clean and --extra-kconfig"
);
assert!(
clean,
"--clean must round-trip alongside --force and --extra-kconfig"
);
assert_eq!(
extra_kconfig,
Some(std::path::PathBuf::from("/tmp/extra.kconfig")),
"--extra-kconfig must round-trip when combined with --force + --clean",
);
}
#[test]
fn parse_extra_kconfig_passes_through_verifier_subcommand_to_args_vec() {
let KtstrCommand::Verifier { args, .. } = parse_via_split(&[
"cargo",
"ktstr",
"verifier",
"--kernel",
"../linux",
"--extra-kconfig",
"/tmp/x.kconfig",
]) else {
panic!("expected KtstrCommand::Verifier");
};
assert_eq!(
args,
vec!["--extra-kconfig", "/tmp/x.kconfig"],
"verifier has no native --extra-kconfig, so the argv split routes \
it into `args` (the inner cargo nextest run rejects it downstream) — \
verifier is a passthrough subcommand like test/coverage, NOT a \
clean-surface reject like shell",
);
}
#[test]
fn parse_extra_kconfig_rejected_on_shell_subcommand() {
let m = Cargo::try_parse_from([
"cargo",
"ktstr",
"shell",
"--extra-kconfig",
"/tmp/x.kconfig",
]);
assert!(
m.is_err(),
"--extra-kconfig must be rejected on `cargo ktstr shell` \
(shell is not a passthrough subcommand — no `last = true` args \
field and argsplit leaves its argv unchanged — so unknown flags \
fail at parse time)",
);
}
#[test]
fn parse_extra_kconfig_passes_through_test_subcommand_to_args_vec() {
let KtstrCommand::Test { args, .. } = parse_via_split(&[
"cargo",
"ktstr",
"test",
"--extra-kconfig",
"/tmp/x.kconfig",
]) else {
panic!("expected KtstrCommand::Test");
};
assert_eq!(
args,
vec!["--extra-kconfig", "/tmp/x.kconfig"],
"--extra-kconfig must passthrough into `args` Vec on test \
subcommand (the argv split routes it there). The cargo nextest \
subprocess will reject it as an unknown flag downstream."
);
}
#[test]
fn parse_kernel_build_extra_kconfig_with_path() {
let Cargo {
command: CargoSub::Ktstr(k),
} = Cargo::try_parse_from([
"cargo",
"ktstr",
"kernel",
"build",
"--kernel",
"../linux",
"--extra-kconfig",
"/tmp/extra.kconfig",
])
.unwrap_or_else(|e| panic!("{e}"));
let KtstrCommand::Kernel {
command:
KernelCommand::Build {
kernel,
extra_kconfig,
..
},
} = k.command
else {
panic!("expected KernelCommand::Build");
};
assert_eq!(kernel.as_deref(), Some("../linux"));
assert_eq!(
extra_kconfig,
Some(std::path::PathBuf::from("/tmp/extra.kconfig")),
);
}
#[test]
fn parse_kernel_clean() {
let m = Cargo::try_parse_from(["cargo", "ktstr", "kernel", "clean"]);
assert!(m.is_ok(), "{}", m.err().unwrap());
}
#[test]
fn parse_kernel_clean_keep() {
let Cargo {
command: CargoSub::Ktstr(k),
} = Cargo::try_parse_from(["cargo", "ktstr", "kernel", "clean", "--keep", "3"])
.unwrap_or_else(|e| panic!("{e}"));
let KtstrCommand::Kernel {
command: KernelCommand::Clean { keep, .. },
} = k.command
else {
panic!("expected Kernel Clean");
};
assert_eq!(keep, Some(3));
}
#[test]
fn parse_verifier_bare() {
let Cargo {
command: CargoSub::Ktstr(k),
} = Cargo::try_parse_from(["cargo", "ktstr", "verifier"]).unwrap_or_else(|e| panic!("{e}"));
let KtstrCommand::Verifier {
kernel,
raw,
include_eol,
..
} = k.command
else {
panic!("expected Verifier");
};
assert!(
kernel.is_empty(),
"bare verifier must default --kernel to empty Vec"
);
assert!(!raw, "bare verifier must default --raw to false");
assert!(
!include_eol,
"bare verifier must default --include-eol to false"
);
}
#[test]
fn parse_verifier_include_eol_flag() {
let Cargo {
command: CargoSub::Ktstr(k),
} = Cargo::try_parse_from([
"cargo",
"ktstr",
"verifier",
"--kernel",
"6.11..6.14",
"--include-eol",
])
.unwrap_or_else(|e| panic!("{e}"));
let KtstrCommand::Verifier {
kernel,
include_eol,
..
} = k.command
else {
panic!("expected Verifier");
};
assert_eq!(kernel, vec!["6.11..6.14".to_string()]);
assert!(
include_eol,
"`--include-eol` must round-trip as true on verifier"
);
}
#[test]
fn parse_verifier_with_kernel_single() {
let Cargo {
command: CargoSub::Ktstr(k),
} = Cargo::try_parse_from(["cargo", "ktstr", "verifier", "--kernel", "6.14.2"])
.unwrap_or_else(|e| panic!("{e}"));
let KtstrCommand::Verifier { kernel, raw, .. } = k.command else {
panic!("expected Verifier");
};
assert_eq!(kernel, vec!["6.14.2"]);
assert!(!raw);
}
#[test]
fn parse_verifier_with_kernel_repeatable() {
let Cargo {
command: CargoSub::Ktstr(k),
} = Cargo::try_parse_from([
"cargo", "ktstr", "verifier", "--kernel", "6.14.2", "--kernel", "6.15.0",
])
.unwrap_or_else(|e| panic!("{e}"));
let KtstrCommand::Verifier { kernel, raw, .. } = k.command else {
panic!("expected Verifier");
};
assert_eq!(kernel, vec!["6.14.2", "6.15.0"]);
assert!(!raw);
}
#[test]
fn parse_verifier_with_raw() {
let Cargo {
command: CargoSub::Ktstr(k),
} = Cargo::try_parse_from(["cargo", "ktstr", "verifier", "--raw"])
.unwrap_or_else(|e| panic!("{e}"));
let KtstrCommand::Verifier { kernel, raw, .. } = k.command else {
panic!("expected Verifier");
};
assert!(kernel.is_empty());
assert!(raw, "--raw must lift the flag to true");
}
#[test]
fn parse_verifier_scheduler_defaults_none() {
let Cargo {
command: CargoSub::Ktstr(k),
} = Cargo::try_parse_from(["cargo", "ktstr", "verifier"]).unwrap_or_else(|e| panic!("{e}"));
let KtstrCommand::Verifier { scheduler, .. } = k.command else {
panic!("expected Verifier");
};
assert!(
scheduler.is_none(),
"bare verifier must default --scheduler to None (full declared-scheduler sweep)",
);
}
#[test]
fn parse_verifier_with_scheduler() {
let Cargo {
command: CargoSub::Ktstr(k),
} = Cargo::try_parse_from(["cargo", "ktstr", "verifier", "--scheduler", "scx-ktstr"])
.unwrap_or_else(|e| panic!("{e}"));
let KtstrCommand::Verifier { scheduler, .. } = k.command else {
panic!("expected Verifier");
};
assert_eq!(
scheduler.as_deref(),
Some("scx-ktstr"),
"--scheduler lifts the single-scheduler sweep filter",
);
}
#[test]
fn parse_verifier_forwards_flags_without_separator() {
let KtstrCommand::Verifier {
kernel, raw, args, ..
} = parse_via_split(&[
"cargo",
"ktstr",
"verifier",
"--kernel",
"../linux",
"--features",
"integration",
])
else {
panic!("expected Verifier");
};
assert_eq!(kernel, vec!["../linux"], "--kernel parsed as a native flag");
assert!(!raw);
assert_eq!(
args,
vec!["--features".to_string(), "integration".to_string()],
"cargo/nextest flags forward into `args` with no `--` separator",
);
}
#[test]
fn parse_verifier_flag_after_passthrough_is_native() {
let KtstrCommand::Verifier { kernel, args, .. } = parse_via_split(&[
"cargo",
"ktstr",
"verifier",
"--features",
"integration",
"--kernel",
"../linux",
]) else {
panic!("expected Verifier");
};
assert_eq!(
kernel,
vec!["../linux"],
"--kernel placed AFTER a passthrough flag parses as a native flag, not swallowed",
);
assert_eq!(
args,
vec!["--features".to_string(), "integration".to_string()],
"only the genuine passthrough lands in args",
);
}
#[test]
fn parse_verifier_with_profiles() {
let KtstrCommand::Verifier {
kernel,
profile,
nextest_profile,
args,
..
} = parse_via_split(&[
"cargo",
"ktstr",
"verifier",
"--kernel",
"../linux",
"--profile",
"dev",
"--nextest-profile",
"ci",
"--features",
"integration",
])
else {
panic!("expected Verifier");
};
assert_eq!(kernel, vec!["../linux"]);
assert_eq!(profile.as_deref(), Some("dev"), "--profile round-trips");
assert_eq!(
nextest_profile.as_deref(),
Some("ci"),
"--nextest-profile round-trips"
);
assert_eq!(
args,
vec!["--features".to_string(), "integration".to_string()],
"a passthrough flag AFTER the native profiles still lands in args",
);
}
#[test]
fn parse_verifier_all_profiles_forwarded_not_native() {
let KtstrCommand::Verifier { args, .. } =
parse_via_split(&["cargo", "ktstr", "verifier", "--all-profiles"])
else {
panic!("expected Verifier");
};
assert_eq!(
args,
vec!["--all-profiles"],
"--all-profiles is not a native verifier flag; the argv split \
routes it into `args`. A future native re-add would empty `args`.",
);
}
#[test]
fn parse_verifier_profiles_filter_forwarded_not_native() {
let KtstrCommand::Verifier { args, .. } = parse_via_split(&[
"cargo",
"ktstr",
"verifier",
"--profiles",
"default,llc,llc+steal",
]) else {
panic!("expected Verifier");
};
assert_eq!(
args,
vec!["--profiles", "default,llc,llc+steal"],
"--profiles is not a native verifier flag; the argv split routes it \
and its value into `args`. A future native re-add would consume the \
value and empty `args`.",
);
}
#[test]
fn parse_test_flag_after_passthrough_is_native() {
let KtstrCommand::Test {
include_eol,
kernel,
args,
..
} = parse_via_split(&[
"cargo",
"ktstr",
"test",
"--kernel",
"6.11..6.14",
"--features",
"integration",
"--include-eol",
])
else {
panic!("expected Test");
};
assert!(
include_eol,
"--include-eol after a passthrough flag is consumed by ktstr"
);
assert_eq!(kernel, vec!["6.11..6.14"]);
assert_eq!(
args,
vec!["--features".to_string(), "integration".to_string()]
);
}
#[test]
fn parse_test_flag_before_passthrough_is_native() {
let KtstrCommand::Test {
include_eol,
kernel,
args,
..
} = parse_via_split(&[
"cargo",
"ktstr",
"test",
"--kernel",
"6.11..6.14",
"--include-eol",
"--features",
"integration",
])
else {
panic!("expected Test");
};
assert!(include_eol);
assert_eq!(kernel, vec!["6.11..6.14"]);
assert_eq!(
args,
vec!["--features".to_string(), "integration".to_string()]
);
}
#[test]
fn parse_test_interleaved_ktstr_flag() {
let KtstrCommand::Test { kernel, args, .. } = parse_via_split(&[
"cargo",
"ktstr",
"test",
"--features",
"integration",
"--kernel",
"6.14",
"--no-capture",
]) else {
panic!("expected Test");
};
assert_eq!(kernel, vec!["6.14"]);
assert_eq!(
args,
vec![
"--features".to_string(),
"integration".to_string(),
"--no-capture".to_string()
]
);
}
#[test]
fn parse_test_kernel_eq_value_after_passthrough() {
let KtstrCommand::Test { kernel, args, .. } = parse_via_split(&[
"cargo",
"ktstr",
"test",
"--features",
"integration",
"--kernel=6.14",
]) else {
panic!("expected Test");
};
assert_eq!(kernel, vec!["6.14"]);
assert_eq!(
args,
vec!["--features".to_string(), "integration".to_string()]
);
}
#[test]
fn parse_test_repeated_kernel_after_passthrough() {
let KtstrCommand::Test { kernel, args, .. } = parse_via_split(&[
"cargo",
"ktstr",
"test",
"--features",
"x",
"--kernel",
"6.14",
"--kernel",
"6.15",
]) else {
panic!("expected Test");
};
assert_eq!(kernel, vec!["6.14", "6.15"]);
assert_eq!(args, vec!["--features".to_string(), "x".to_string()]);
}
#[test]
fn parse_coverage_flag_after_passthrough_is_native() {
let KtstrCommand::Coverage {
include_eol, args, ..
} = parse_via_split(&[
"cargo",
"ktstr",
"coverage",
"--features",
"integration",
"--include-eol",
])
else {
panic!("expected Coverage");
};
assert!(include_eol);
assert_eq!(
args,
vec!["--features".to_string(), "integration".to_string()]
);
}
#[test]
fn parse_llvm_cov_flag_after_passthrough_is_native() {
let KtstrCommand::LlvmCov {
include_eol, args, ..
} = parse_via_split(&[
"cargo",
"ktstr",
"llvm-cov",
"--features",
"integration",
"--include-eol",
])
else {
panic!("expected LlvmCov");
};
assert!(include_eol);
assert_eq!(
args,
vec!["--features".to_string(), "integration".to_string()]
);
}
#[test]
fn parse_test_double_dash_forces_passthrough() {
let KtstrCommand::Test {
include_eol,
kernel,
args,
..
} = parse_via_split(&[
"cargo",
"ktstr",
"test",
"--kernel",
"6.14",
"--",
"--include-eol",
])
else {
panic!("expected Test");
};
assert!(
!include_eol,
"--include-eol after -- is passthrough, not the native flag"
);
assert_eq!(kernel, vec!["6.14"]);
assert_eq!(args, vec!["--include-eol".to_string()]);
}
#[test]
fn parse_test_profile_native_vs_passthrough_by_double_dash() {
let KtstrCommand::Test { profile, args, .. } = parse_via_split(&[
"cargo",
"ktstr",
"test",
"--profile",
"dev",
"--features",
"x",
]) else {
panic!("expected Test");
};
assert_eq!(
profile.as_deref(),
Some("dev"),
"--profile before -- is the native ktstr flag"
);
assert_eq!(args, vec!["--features".to_string(), "x".to_string()]);
let KtstrCommand::Test { profile, args, .. } = parse_via_split(&[
"cargo",
"ktstr",
"test",
"--kernel",
"6.14",
"--",
"--profile",
"nextest-ci",
]) else {
panic!("expected Test");
};
assert_eq!(
profile, None,
"--profile after -- forwards to the inner tool"
);
assert_eq!(
args,
vec!["--profile".to_string(), "nextest-ci".to_string()]
);
}
#[test]
fn parse_replay_short_e_after_passthrough() {
let KtstrCommand::Replay { filter, args, .. } = parse_via_split(&[
"cargo",
"ktstr",
"replay",
"--exec",
"--features",
"x",
"-E",
"scheduler_",
]) else {
panic!("expected Replay");
};
assert_eq!(
filter.as_deref(),
Some("scheduler_"),
"-E value after a passthrough flag is native"
);
assert_eq!(args, vec!["--features".to_string(), "x".to_string()]);
let KtstrCommand::Replay { filter, args, .. } = parse_via_split(&[
"cargo",
"ktstr",
"replay",
"--exec",
"--features",
"x",
"-Escheduler_",
]) else {
panic!("expected Replay");
};
assert_eq!(
filter.as_deref(),
Some("scheduler_"),
"glued -Evalue after a passthrough flag is native"
);
assert_eq!(args, vec!["--features".to_string(), "x".to_string()]);
}
#[test]
fn parse_test_passthrough_only_steals_nothing() {
let KtstrCommand::Test { kernel, args, .. } = parse_via_split(&[
"cargo",
"ktstr",
"test",
"--kernel",
"6.14",
"--no-capture",
"-j4",
]) else {
panic!("expected Test");
};
assert_eq!(kernel, vec!["6.14"]);
assert_eq!(args, vec!["--no-capture".to_string(), "-j4".to_string()]);
}
#[test]
fn parse_test_help_stays_native() {
let raw: Vec<std::ffi::OsString> = ["cargo", "ktstr", "test", "--help"]
.iter()
.map(std::ffi::OsString::from)
.collect();
let rewritten = crate::argsplit::rewrite(&Cargo::command(), &raw);
let err = Cargo::try_parse_from(&rewritten)
.err()
.expect("--help must not parse as a command");
assert_eq!(
err.kind(),
clap::error::ErrorKind::DisplayHelp,
"--help stays native (clap help), not forwarded to args",
);
}
#[test]
fn rewrite_leaves_non_passthrough_subcommand_unchanged() {
let raw: Vec<std::ffi::OsString> = ["cargo", "ktstr", "shell", "--topology", "1,1,2,1"]
.iter()
.map(std::ffi::OsString::from)
.collect();
let rewritten = crate::argsplit::rewrite(&Cargo::command(), &raw);
assert_eq!(
rewritten, raw,
"shell is not a passthrough subcommand; argv is untouched"
);
}
#[test]
fn parse_replay_defaults() {
let Cargo {
command: CargoSub::Ktstr(k),
} = Cargo::try_parse_from(["cargo", "ktstr", "replay"]).unwrap_or_else(|e| panic!("{e}"));
let KtstrCommand::Replay {
dir,
filter,
exec,
profile,
nextest_profile,
args,
} = k.command
else {
panic!("expected Replay");
};
assert!(dir.is_none(), "bare replay defaults --dir to None");
assert!(filter.is_none(), "bare replay defaults --filter to None");
assert!(!exec, "bare replay defaults --exec off (dry-run)");
assert!(profile.is_none(), "bare replay defaults --profile to None");
assert!(
nextest_profile.is_none(),
"bare replay defaults --nextest-profile to None"
);
assert!(args.is_empty(), "bare replay parses no passthrough");
}
#[test]
fn parse_replay_all_flags_and_passthrough() {
let KtstrCommand::Replay {
dir,
filter,
exec,
profile,
nextest_profile,
args,
} = parse_via_split(&[
"cargo",
"ktstr",
"replay",
"--dir",
"/tmp/archived-runs",
"-E",
"scheduler_",
"--exec",
"--profile",
"dev",
"--nextest-profile",
"ci",
"--features",
"integration",
])
else {
panic!("expected Replay");
};
assert_eq!(
dir.as_deref(),
Some(std::path::Path::new("/tmp/archived-runs")),
"--dir round-trips to Some(PathBuf)"
);
assert_eq!(
filter.as_deref(),
Some("scheduler_"),
"-E filter round-trips"
);
assert!(exec, "--exec lifts the flag to true");
assert_eq!(profile.as_deref(), Some("dev"), "--profile round-trips");
assert_eq!(
nextest_profile.as_deref(),
Some("ci"),
"--nextest-profile round-trips"
);
assert_eq!(
args,
vec!["--features".to_string(), "integration".to_string()],
"a passthrough flag AFTER the native flags lands in args",
);
}
#[test]
fn parse_completions_bash() {
let m = Cargo::try_parse_from(["cargo", "ktstr", "completions", "bash"]);
assert!(m.is_ok(), "{}", m.err().unwrap());
}
#[test]
fn parse_completions_invalid_shell() {
let m = Cargo::try_parse_from(["cargo", "ktstr", "completions", "noshell"]);
assert!(m.is_err());
}
#[test]
fn parse_missing_subcommand() {
let m = Cargo::try_parse_from(["cargo", "ktstr"]);
assert!(m.is_err());
}
#[test]
fn parse_unknown_subcommand() {
let m = Cargo::try_parse_from(["cargo", "ktstr", "nonexistent"]);
assert!(m.is_err());
}
#[test]
fn completions_bash_non_empty() {
let mut buf = Vec::new();
let mut cmd = Cargo::command();
clap_complete::generate(clap_complete::Shell::Bash, &mut cmd, "cargo", &mut buf);
assert!(!buf.is_empty());
}
#[test]
fn completions_zsh_contains_subcommands() {
let mut buf = Vec::new();
let mut cmd = Cargo::command();
clap_complete::generate(clap_complete::Shell::Zsh, &mut cmd, "cargo", &mut buf);
let output = String::from_utf8(buf).expect("completions should be valid UTF-8");
assert!(
output.contains("'test:"),
"zsh completions missing 'test:' describe-list entry"
);
assert!(
output.contains("'coverage:"),
"zsh completions missing 'coverage:' describe-list entry"
);
assert!(
output.contains("'shell:"),
"zsh completions missing 'shell:' describe-list entry"
);
assert!(
output.contains("'kernel:"),
"zsh completions missing 'kernel:' describe-list entry"
);
assert!(
output.contains("'nextest:"),
"zsh completions missing 'nextest:' describe-list \
entry (visible alias of `test`)"
);
assert!(
output.contains("'llvm-cov:"),
"zsh completions missing 'llvm-cov:' describe-list entry"
);
}
fn test_metadata() -> KernelMetadata {
KernelMetadata::new(
ktstr::cache::KernelSource::Tarball,
"x86_64",
"bzImage",
"2026-04-12T10:00:00Z",
)
.with_version("6.14.2")
}
fn store_test_entry(cache: &CacheDir, key: &str, meta: &KernelMetadata) -> CacheEntry {
let src = tempfile::TempDir::new().unwrap();
let image = src.path().join(&meta.image_name);
std::fs::write(&image, b"fake kernel").unwrap();
cache
.store(key, &CacheArtifacts::new(&image), meta)
.unwrap()
}
#[test]
fn format_entry_row_no_version() {
let tmp = tempfile::TempDir::new().unwrap();
let cache = CacheDir::with_root(tmp.path().join("cache"));
let meta = KernelMetadata::new(
ktstr::cache::KernelSource::Local {
source_tree_path: None,
git_hash: None,
},
"x86_64",
"bzImage",
"2026-04-12T10:00:00Z",
);
let entry = store_test_entry(&cache, "local-key", &meta);
let row = cli::format_entry_row(&entry, "hash", &[]);
let tokens: Vec<&str> = row.split_whitespace().collect();
assert!(
tokens.len() >= 2,
"row must have at least key + version columns: {row:?}",
);
assert_eq!(
tokens[1], "-",
"missing version must render as `-` in the version column: {row:?}",
);
}
#[test]
fn kconfig_status_reports_stale_on_hash_mismatch() {
let tmp = tempfile::TempDir::new().unwrap();
let cache = CacheDir::with_root(tmp.path().join("cache"));
let meta = test_metadata().with_ktstr_kconfig_hash("old");
let entry = store_test_entry(&cache, "stale", &meta);
assert_eq!(
entry.kconfig_status("new"),
ktstr::cache::KconfigStatus::Stale {
cached: "old".to_string(),
current: "new".to_string(),
}
);
}
#[test]
fn kconfig_status_reports_matches_on_hash_equality() {
let tmp = tempfile::TempDir::new().unwrap();
let cache = CacheDir::with_root(tmp.path().join("cache"));
let meta = test_metadata().with_ktstr_kconfig_hash("same");
let entry = store_test_entry(&cache, "fresh", &meta);
assert_eq!(
entry.kconfig_status("same"),
ktstr::cache::KconfigStatus::Matches
);
}
#[test]
fn kconfig_status_reports_untracked_when_entry_has_no_hash() {
let tmp = tempfile::TempDir::new().unwrap();
let cache = CacheDir::with_root(tmp.path().join("cache"));
let meta = test_metadata();
let entry = store_test_entry(&cache, "no-hash", &meta);
assert_eq!(
entry.kconfig_status("anything"),
ktstr::cache::KconfigStatus::Untracked
);
}
#[test]
fn kconfig_status_json_string_pins_all_three_variants() {
use ktstr::cache::KconfigStatus;
let tmp = tempfile::TempDir::new().unwrap();
let cache = CacheDir::with_root(tmp.path().join("cache"));
let matches_meta = test_metadata().with_ktstr_kconfig_hash("h");
let matches_entry = store_test_entry(&cache, "matches-key", &matches_meta);
let matches_status = matches_entry.kconfig_status("h");
assert!(
matches!(matches_status, KconfigStatus::Matches),
"hash equality must yield KconfigStatus::Matches"
);
assert_eq!(matches_status.to_string(), "matches");
let stale_meta = test_metadata().with_ktstr_kconfig_hash("old");
let stale_entry = store_test_entry(&cache, "stale-key", &stale_meta);
let stale_status = stale_entry.kconfig_status("new");
assert!(
matches!(stale_status, KconfigStatus::Stale { .. }),
"hash mismatch must yield KconfigStatus::Stale"
);
assert_eq!(stale_status.to_string(), "stale");
let untracked_meta = test_metadata();
let untracked_entry = store_test_entry(&cache, "untracked-key", &untracked_meta);
let untracked_status = untracked_entry.kconfig_status("anything");
assert!(
matches!(untracked_status, KconfigStatus::Untracked),
"entry without hash must yield KconfigStatus::Untracked"
);
assert_eq!(untracked_status.to_string(), "untracked");
}
#[test]
fn embedded_kconfig_hash_deterministic() {
let h1 = cli::embedded_kconfig_hash();
let h2 = cli::embedded_kconfig_hash();
assert_eq!(h1, h2);
}
#[test]
fn embedded_kconfig_hash_is_hex() {
let h = cli::embedded_kconfig_hash();
assert_eq!(h.len(), 8, "CRC32 hex should be 8 chars");
assert!(
h.chars().all(|c| c.is_ascii_hexdigit()),
"should be hex digits: {h}"
);
}
#[test]
fn embedded_kconfig_hash_matches_manual_crc32() {
let expected = format!("{:08x}", crc32fast::hash(cli::EMBEDDED_KCONFIG.as_bytes()));
assert_eq!(cli::embedded_kconfig_hash(), expected);
}
#[test]
fn parse_show_host_minimal() {
let Cargo {
command: CargoSub::Ktstr(k),
} = Cargo::try_parse_from(["cargo", "ktstr", "show-host"]).unwrap_or_else(|e| panic!("{e}"));
assert!(matches!(k.command, KtstrCommand::ShowHost));
}
#[test]
fn parse_show_host_rejects_positional() {
let rejected = Cargo::try_parse_from(["cargo", "ktstr", "show-host", "stray"]);
assert!(
rejected.is_err(),
"show-host must reject positional arguments",
);
}
#[test]
fn parse_show_thresholds_with_test_arg() {
let Cargo {
command: CargoSub::Ktstr(k),
} = Cargo::try_parse_from(["cargo", "ktstr", "show-thresholds", "my_test_fn"])
.unwrap_or_else(|e| panic!("{e}"));
let KtstrCommand::ShowThresholds { test } = k.command else {
panic!("expected ShowThresholds");
};
assert_eq!(test, "my_test_fn");
}
#[test]
fn parse_show_thresholds_without_arg_rejected() {
let rejected = Cargo::try_parse_from(["cargo", "ktstr", "show-thresholds"]);
assert!(
rejected.is_err(),
"show-thresholds requires a test-name argument",
);
}
#[test]
fn parse_show_thresholds_extra_arg_rejected() {
let rejected = Cargo::try_parse_from(["cargo", "ktstr", "show-thresholds", "a", "b"]);
assert!(
rejected.is_err(),
"show-thresholds must accept exactly one positional arg",
);
}
#[test]
fn show_host_helper_produces_non_empty_output() {
let out = cli::show_host();
assert!(
!out.is_empty(),
"show_host must return a non-empty report under normal Linux CI",
);
assert!(
out.contains("kernel_release"),
"show_host output must include the stable `kernel_release` row: {out}",
);
}
#[test]
fn show_thresholds_helper_unknown_test_returns_error() {
let err = cli::show_thresholds("definitely_not_a_registered_test_xyz").unwrap_err();
let msg = format!("{err:#}");
assert!(
msg.contains("no registered ktstr test named"),
"error path must preserve the actionable diagnostic: {msg}",
);
}
#[test]
fn parse_shell_cpu_cap_with_no_perf_mode_succeeds() {
let Cargo {
command: CargoSub::Ktstr(k),
} = Cargo::try_parse_from([
"cargo",
"ktstr",
"shell",
"--cpu-cap",
"4",
"--no-perf-mode",
])
.unwrap_or_else(|e| panic!("{e}"));
let KtstrCommand::Shell {
cpu_cap,
no_perf_mode,
..
} = k.command
else {
panic!("expected Shell");
};
assert_eq!(cpu_cap, Some(4));
assert!(no_perf_mode, "--no-perf-mode must be set");
}
#[test]
fn parse_shell_cpu_cap_without_no_perf_mode_fails() {
let msg = match Cargo::try_parse_from(["cargo", "ktstr", "shell", "--cpu-cap", "4"]) {
Err(e) => e.to_string(),
Ok(_) => panic!("--cpu-cap without --no-perf-mode must fail the parse"),
};
assert!(
msg.to_ascii_lowercase().contains("no-perf-mode")
|| msg.to_ascii_lowercase().contains("no_perf_mode"),
"clap error must name the missing --no-perf-mode flag, got: {msg}",
);
}
#[test]
fn parse_shell_no_perf_mode_without_cpu_cap_succeeds() {
let Cargo {
command: CargoSub::Ktstr(k),
} = Cargo::try_parse_from(["cargo", "ktstr", "shell", "--no-perf-mode"])
.unwrap_or_else(|e| panic!("{e}"));
let KtstrCommand::Shell {
cpu_cap,
no_perf_mode,
..
} = k.command
else {
panic!("expected Shell");
};
assert_eq!(cpu_cap, None, "no --cpu-cap must produce None");
assert!(no_perf_mode);
}
#[test]
fn kernel_list_long_about_exposes_range_mode_json_keys() {
let about = ktstr::cli::KERNEL_LIST_LONG_ABOUT;
assert!(
about.contains(" range literal"),
"KERNEL_LIST_LONG_ABOUT must carry the `range` row from the \
range-mode schema block: got: {about:?}",
);
assert!(
about.contains(" start parsed start endpoint"),
"KERNEL_LIST_LONG_ABOUT must carry the `start` row from the \
range-mode schema block: got: {about:?}",
);
assert!(
about.contains(" end parsed end endpoint"),
"KERNEL_LIST_LONG_ABOUT must carry the `end` row from the \
range-mode schema block: got: {about:?}",
);
assert!(
about.contains(" versions array of resolved version strings"),
"KERNEL_LIST_LONG_ABOUT must carry the `versions` row from the \
range-mode schema block: got: {about:?}",
);
assert!(
about.contains("Range-mode output never carries cache metadata"),
"KERNEL_LIST_LONG_ABOUT must call out the `Range-mode output \
never carries cache metadata` contract so scripted consumers \
know to dispatch on the presence of the `range` key versus \
the `entries` key: got: {about:?}",
);
assert!(
about.contains("--kernel"),
"KERNEL_LIST_LONG_ABOUT must reference the `--kernel` flag \
so a `kernel list --help` reader sees the range-mode \
entry point: got: {about:?}",
);
assert!(
about.contains("range-preview"),
"KERNEL_LIST_LONG_ABOUT must use the `range-preview` term so \
scripted consumers know to dispatch on the presence of the \
`range` key: got: {about:?}",
);
}
#[test]
fn parse_export_with_test_arg() {
let Cargo {
command: CargoSub::Ktstr(k),
} = Cargo::try_parse_from([
"cargo",
"ktstr",
"export",
"preempt_regression_fault_under_load",
])
.unwrap_or_else(|e| panic!("{e}"));
let KtstrCommand::Export {
test,
output,
package,
release,
} = k.command
else {
panic!("expected Export");
};
assert_eq!(test, "preempt_regression_fault_under_load");
assert!(
output.is_none(),
"bare export must default --output to None"
);
assert!(
package.is_none(),
"bare export must default --package to None"
);
assert!(!release, "bare export must default --release to false");
}
#[test]
fn parse_export_with_all_flags() {
let Cargo {
command: CargoSub::Ktstr(k),
} = Cargo::try_parse_from([
"cargo",
"ktstr",
"export",
"my_test_fn",
"-o",
"/tmp/out.run",
"--package",
"scx_rusty",
"--release",
])
.unwrap_or_else(|e| panic!("{e}"));
let KtstrCommand::Export {
test,
output,
package,
release,
} = k.command
else {
panic!("expected Export");
};
assert_eq!(test, "my_test_fn");
assert_eq!(
output,
Some(std::path::PathBuf::from("/tmp/out.run")),
"-o must round-trip to Some(PathBuf)",
);
assert_eq!(package.as_deref(), Some("scx_rusty"));
assert!(release, "--release must lift the flag to true");
}
#[test]
fn parse_export_with_output_long_form() {
let Cargo {
command: CargoSub::Ktstr(k),
} = Cargo::try_parse_from([
"cargo",
"ktstr",
"export",
"test_fn",
"--output",
"/tmp/long.run",
])
.unwrap_or_else(|e| panic!("{e}"));
let KtstrCommand::Export { output, .. } = k.command else {
panic!("expected Export");
};
assert_eq!(output, Some(std::path::PathBuf::from("/tmp/long.run")));
}
#[test]
fn parse_export_with_package_short_form() {
let Cargo {
command: CargoSub::Ktstr(k),
} = Cargo::try_parse_from(["cargo", "ktstr", "export", "test_fn", "-p", "ktstr"])
.unwrap_or_else(|e| panic!("{e}"));
let KtstrCommand::Export { package, .. } = k.command else {
panic!("expected Export");
};
assert_eq!(package.as_deref(), Some("ktstr"));
}
#[test]
fn parse_export_missing_test_arg_rejected() {
let rejected = Cargo::try_parse_from(["cargo", "ktstr", "export"]);
assert!(
rejected.is_err(),
"export must require a positional test-name argument",
);
}
#[test]
fn parse_export_extra_arg_rejected() {
let rejected = Cargo::try_parse_from(["cargo", "ktstr", "export", "a", "b"]);
assert!(
rejected.is_err(),
"export must accept exactly one positional arg",
);
}
#[test]
fn parse_locks_bare() {
let Cargo {
command: CargoSub::Ktstr(k),
} = Cargo::try_parse_from(["cargo", "ktstr", "locks"]).unwrap_or_else(|e| panic!("{e}"));
let KtstrCommand::Locks { json, watch } = k.command else {
panic!("expected Locks");
};
assert!(!json, "bare locks must default --json to false");
assert!(watch.is_none(), "bare locks must default --watch to None");
}
#[test]
fn parse_locks_with_json() {
let Cargo {
command: CargoSub::Ktstr(k),
} = Cargo::try_parse_from(["cargo", "ktstr", "locks", "--json"])
.unwrap_or_else(|e| panic!("{e}"));
let KtstrCommand::Locks { json, watch } = k.command else {
panic!("expected Locks");
};
assert!(json, "--json must lift the flag to true");
assert!(watch.is_none(), "bare --json must not populate --watch");
}
#[test]
fn parse_locks_with_watch_duration() {
let Cargo {
command: CargoSub::Ktstr(k),
} = Cargo::try_parse_from(["cargo", "ktstr", "locks", "--watch", "500ms"])
.unwrap_or_else(|e| panic!("{e}"));
let KtstrCommand::Locks { json, watch } = k.command else {
panic!("expected Locks");
};
assert!(!json, "bare --watch must not populate --json");
assert_eq!(
watch,
Some(std::time::Duration::from_millis(500)),
"--watch 500ms must round-trip to Duration::from_millis(500)",
);
}
#[test]
fn parse_locks_with_watch_and_json() {
let Cargo {
command: CargoSub::Ktstr(k),
} = Cargo::try_parse_from(["cargo", "ktstr", "locks", "--watch", "5s", "--json"])
.unwrap_or_else(|e| panic!("{e}"));
let KtstrCommand::Locks { json, watch } = k.command else {
panic!("expected Locks");
};
assert!(json, "--json must lift the flag to true alongside --watch");
assert_eq!(
watch,
Some(std::time::Duration::from_secs(5)),
"--watch 5s must round-trip to Duration::from_secs(5)",
);
}
#[test]
fn parse_locks_watch_rejects_malformed_duration() {
let rejected = Cargo::try_parse_from(["cargo", "ktstr", "locks", "--watch", "not-a-duration"]);
assert!(
rejected.is_err(),
"--watch must reject malformed humantime input via the \
value_parser = humantime::parse_duration attribute",
);
}
#[test]
fn parse_shell_memory_mib_valid() {
let Cargo {
command: CargoSub::Ktstr(k),
} = Cargo::try_parse_from(["cargo", "ktstr", "shell", "--memory-mib", "256"])
.unwrap_or_else(|e| panic!("{e}"));
let KtstrCommand::Shell { memory_mib, .. } = k.command else {
panic!("expected Shell");
};
assert_eq!(memory_mib, Some(256), "--memory-mib 256 must round-trip");
}
#[test]
fn parse_shell_memory_mib_at_range_floor() {
let Cargo {
command: CargoSub::Ktstr(k),
} = Cargo::try_parse_from(["cargo", "ktstr", "shell", "--memory-mib", "128"])
.unwrap_or_else(|e| panic!("{e}"));
let KtstrCommand::Shell { memory_mib, .. } = k.command else {
panic!("expected Shell");
};
assert_eq!(
memory_mib,
Some(128),
"--memory-mib 128 must succeed at the inclusive range floor",
);
}
#[test]
fn parse_shell_memory_mib_below_range_rejected() {
let rejected = Cargo::try_parse_from(["cargo", "ktstr", "shell", "--memory-mib", "64"]);
assert!(
rejected.is_err(),
"--memory-mib 64 must be rejected — value_parser range floor is 128",
);
}
#[test]
fn parse_shell_memory_mib_negative_rejected() {
let rejected = Cargo::try_parse_from(["cargo", "ktstr", "shell", "--memory-mib", "-1"]);
assert!(
rejected.is_err(),
"--memory-mib -1 must be rejected — the field is u32",
);
}
#[test]
fn parse_shell_memory_mb_rejected() {
let rejected = Cargo::try_parse_from(["cargo", "ktstr", "shell", "--memory-mb", "256"]);
assert!(
rejected.is_err(),
"`--memory-mb` (old flag name) must be rejected — the \
canonical name is `--memory-mib`. A regression that \
re-added an alias for the old form would surface here.",
);
}
#[test]
fn parse_shell_with_exec() {
let Cargo {
command: CargoSub::Ktstr(k),
} = Cargo::try_parse_from(["cargo", "ktstr", "shell", "--exec", "uname -a"])
.unwrap_or_else(|e| panic!("{e}"));
let KtstrCommand::Shell { exec, .. } = k.command else {
panic!("expected Shell");
};
assert_eq!(exec.as_deref(), Some("uname -a"));
}
#[test]
fn parse_shell_with_dmesg() {
let Cargo {
command: CargoSub::Ktstr(k),
} = Cargo::try_parse_from(["cargo", "ktstr", "shell", "--dmesg"])
.unwrap_or_else(|e| panic!("{e}"));
let KtstrCommand::Shell { dmesg, .. } = k.command else {
panic!("expected Shell");
};
assert!(dmesg, "--dmesg must lift the flag to true");
}
#[test]
fn parse_shell_dmesg_and_exec_default_unset() {
let Cargo {
command: CargoSub::Ktstr(k),
} = Cargo::try_parse_from(["cargo", "ktstr", "shell"]).unwrap_or_else(|e| panic!("{e}"));
let KtstrCommand::Shell { dmesg, exec, .. } = k.command else {
panic!("expected Shell");
};
assert!(!dmesg, "bare shell must default --dmesg to false");
assert!(exec.is_none(), "bare shell must default --exec to None");
}
#[test]
fn parse_test_kernel_repeatable() {
let Cargo {
command: CargoSub::Ktstr(k),
} = Cargo::try_parse_from([
"cargo", "ktstr", "test", "--kernel", "6.14.2", "--kernel", "6.15-rc3",
])
.unwrap_or_else(|e| panic!("{e}"));
let KtstrCommand::Test { kernel, .. } = k.command else {
panic!("expected Test");
};
assert_eq!(
kernel,
vec!["6.14.2".to_string(), "6.15-rc3".to_string()],
"test --kernel must accumulate via ArgAction::Append",
);
}
#[test]
fn parse_coverage_kernel_repeatable() {
let Cargo {
command: CargoSub::Ktstr(k),
} = Cargo::try_parse_from([
"cargo", "ktstr", "coverage", "--kernel", "6.14.2", "--kernel", "6.15-rc3",
])
.unwrap_or_else(|e| panic!("{e}"));
let KtstrCommand::Coverage { kernel, .. } = k.command else {
panic!("expected Coverage");
};
assert_eq!(
kernel,
vec!["6.14.2".to_string(), "6.15-rc3".to_string()],
"coverage --kernel must accumulate via ArgAction::Append",
);
}
#[test]
fn parse_llvm_cov_kernel_repeatable() {
let Cargo {
command: CargoSub::Ktstr(k),
} = Cargo::try_parse_from([
"cargo", "ktstr", "llvm-cov", "--kernel", "6.14.2", "--kernel", "6.15-rc3",
])
.unwrap_or_else(|e| panic!("{e}"));
let KtstrCommand::LlvmCov { kernel, .. } = k.command else {
panic!("expected LlvmCov");
};
assert_eq!(
kernel,
vec!["6.14.2".to_string(), "6.15-rc3".to_string()],
"llvm-cov --kernel must accumulate via ArgAction::Append",
);
}
#[test]
fn parse_verifier_kernel_repeatable() {
let Cargo {
command: CargoSub::Ktstr(k),
} = Cargo::try_parse_from([
"cargo", "ktstr", "verifier", "--kernel", "6.14.2", "--kernel", "6.15-rc3",
])
.unwrap_or_else(|e| panic!("{e}"));
let KtstrCommand::Verifier { kernel, .. } = k.command else {
panic!("expected Verifier");
};
assert_eq!(
kernel,
vec!["6.14.2".to_string(), "6.15-rc3".to_string()],
"verifier --kernel must accumulate via ArgAction::Append",
);
}
#[test]
fn parse_completions_binary_default_is_cargo() {
let Cargo {
command: CargoSub::Ktstr(k),
} = Cargo::try_parse_from(["cargo", "ktstr", "completions", "bash"])
.unwrap_or_else(|e| panic!("{e}"));
let KtstrCommand::Completions { binary, .. } = k.command else {
panic!("expected Completions");
};
assert_eq!(binary, "cargo", "default --binary must be `cargo`",);
}
#[test]
fn parse_completions_binary_override() {
let Cargo {
command: CargoSub::Ktstr(k),
} = Cargo::try_parse_from(["cargo", "ktstr", "completions", "bash", "--binary", "ktstr"])
.unwrap_or_else(|e| panic!("{e}"));
let KtstrCommand::Completions { binary, .. } = k.command else {
panic!("expected Completions");
};
assert_eq!(binary, "ktstr");
}
#[test]
fn parse_perf_delta_removed_ab_flag_is_clean_unknown_flag_error() {
for flag in [
"--a-scheduler",
"--b-scheduler",
"--a-topology",
"--b-topology",
] {
let eq_form = format!("{flag}=scx_foo");
let forms: [Vec<&str>; 2] = [
vec!["cargo", "ktstr", "perf-delta", flag, "scx_foo"],
vec!["cargo", "ktstr", "perf-delta", eq_form.as_str()],
];
for argv in forms {
let raw: Vec<std::ffi::OsString> = argv.iter().map(std::ffi::OsString::from).collect();
let rewritten = crate::argsplit::rewrite(&Cargo::command(), &raw);
let err = Cargo::try_parse_from(&rewritten).err().unwrap_or_else(|| {
panic!("removed flag {flag} ({argv:?}) must be rejected, not forwarded to nextest")
});
assert_eq!(
err.kind(),
clap::error::ErrorKind::UnknownArgument,
"removed flag {flag} ({argv:?}) must yield a clean UnknownArgument error; got {:?}",
err.kind(),
);
assert!(
err.to_string().contains(flag),
"the error must name the offending flag {flag} ({argv:?}); got: {err}",
);
}
}
}
#[test]
fn parse_perf_delta_dual_run_removed_is_unknown_flag_error() {
let raw: Vec<std::ffi::OsString> = [
"cargo",
"ktstr",
"perf-delta",
"--dual-run",
"--kernel",
"6.14",
]
.iter()
.map(std::ffi::OsString::from)
.collect();
let rewritten = crate::argsplit::rewrite(&Cargo::command(), &raw);
let err = Cargo::try_parse_from(&rewritten)
.err()
.unwrap_or_else(|| panic!("--dual-run must be rejected, not forwarded to nextest"));
assert_eq!(
err.kind(),
clap::error::ErrorKind::UnknownArgument,
"--dual-run must yield a clean UnknownArgument error; got {:?}",
err.kind(),
);
assert!(
err.to_string().contains("--dual-run"),
"the error must name --dual-run; got: {err}",
);
}