#![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, ModelCommand, StatsCommand};
fn parse_compare(extra: &[&str]) -> StatsCommand {
let mut argv: Vec<&str> = vec!["cargo", "ktstr", "stats", "compare"];
argv.extend(extra.iter().copied());
let Cargo {
command: CargoSub::Ktstr(k),
} = Cargo::try_parse_from(argv).unwrap_or_else(|e| panic!("{e}"));
let KtstrCommand::Stats {
command: Some(sc), ..
} = k.command
else {
panic!("expected Stats");
};
assert!(
matches!(sc, StatsCommand::Compare { .. }),
"expected Stats Compare",
);
sc
}
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,
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_eq!(args, expected);
}
KtstrCommand::Coverage {
kernel,
no_perf_mode,
no_skip_mode,
release,
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_eq!(args, expected);
}
KtstrCommand::LlvmCov {
kernel,
no_perf_mode,
no_skip_mode,
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_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_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,
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!(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_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_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,
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_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_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,
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_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_compare() {
let m = Cargo::try_parse_from([
"cargo",
"ktstr",
"stats",
"compare",
"--a-kernel",
"6.14",
"--b-kernel",
"6.15",
]);
assert!(m.is_ok(), "{}", m.err().unwrap());
}
#[test]
fn parse_stats_compare_with_filter() {
let StatsCommand::Compare {
filter,
threshold,
policy,
dir,
a_kernel,
b_kernel,
..
} = parse_compare(&[
"--a-kernel",
"6.14",
"--b-kernel",
"6.15",
"-E",
"cgroup_steady",
])
else {
unreachable!()
};
assert_eq!(a_kernel, vec!["6.14"]);
assert_eq!(b_kernel, vec!["6.15"]);
assert_eq!(filter.as_deref(), Some("cgroup_steady"));
assert!(threshold.is_none());
assert!(policy.is_none());
assert!(dir.is_none());
}
#[test]
fn parse_stats_compare_with_threshold() {
let StatsCommand::Compare {
threshold, filter, ..
} = parse_compare(&[
"--a-kernel",
"6.14",
"--b-kernel",
"6.15",
"--threshold",
"5.0",
])
else {
unreachable!()
};
assert_eq!(threshold, Some(5.0));
assert!(filter.is_none());
}
#[test]
fn parse_stats_compare_with_dir() {
let StatsCommand::Compare {
filter,
threshold,
policy,
dir,
..
} = parse_compare(&[
"--a-kernel",
"6.14",
"--b-kernel",
"6.15",
"--dir",
"/tmp/archived-runs",
])
else {
unreachable!()
};
assert_eq!(
dir.as_deref(),
Some(std::path::Path::new("/tmp/archived-runs")),
"--dir must round-trip to Some(PathBuf); \
parse-scope only — resolver coverage lives \
with compare_partitions' own tests",
);
assert!(
filter.is_none(),
"bare --dir must not spuriously populate filter",
);
assert!(
threshold.is_none(),
"bare --dir must not spuriously populate threshold",
);
assert!(
policy.is_none(),
"bare --dir must not spuriously populate policy",
);
}
#[test]
fn parse_stats_compare_with_policy() {
let StatsCommand::Compare {
threshold, policy, ..
} = parse_compare(&[
"--a-kernel",
"6.14",
"--b-kernel",
"6.15",
"--policy",
"/tmp/policy.json",
])
else {
unreachable!()
};
assert_eq!(
policy.as_deref(),
Some(std::path::Path::new("/tmp/policy.json")),
"--policy must round-trip to Some(PathBuf); got {policy:?}",
);
assert!(
threshold.is_none(),
"bare --policy must not populate --threshold",
);
}
#[test]
fn parse_stats_compare_threshold_conflicts_with_policy() {
let result = Cargo::try_parse_from([
"cargo",
"ktstr",
"stats",
"compare",
"--a-kernel",
"6.14",
"--b-kernel",
"6.15",
"--threshold",
"5.0",
"--policy",
"/tmp/policy.json",
]);
match result {
Ok(_) => panic!("--threshold + --policy must be rejected at parse time"),
Err(err) => assert_eq!(
err.kind(),
clap::error::ErrorKind::ArgumentConflict,
"expected ArgumentConflict — a different ErrorKind would \
signal that the conflicts_with attribute regressed in a way \
the bare is_err() pin would silently mask. Full err: {err}",
),
}
}
#[test]
fn parse_stats_compare_no_average_default_false() {
let StatsCommand::Compare { no_average, .. } =
parse_compare(&["--a-kernel", "6.14", "--b-kernel", "6.15"])
else {
unreachable!()
};
assert!(
!no_average,
"bare compare must default --no-average to false so \
averaging-on remains the default — operators get \
trial-set folding without an explicit flag.",
);
}
#[test]
fn parse_stats_compare_with_no_average() {
let StatsCommand::Compare {
no_average,
threshold,
policy,
dir,
..
} = parse_compare(&["--a-kernel", "6.14", "--b-kernel", "6.15", "--no-average"])
else {
unreachable!()
};
assert!(no_average, "--no-average must lift the flag to true");
assert!(
threshold.is_none(),
"bare --no-average must not spuriously populate --threshold",
);
assert!(
policy.is_none(),
"bare --no-average must not spuriously populate --policy",
);
assert!(
dir.is_none(),
"bare --no-average must not spuriously populate --dir",
);
}
#[test]
fn parse_stats_compare_with_project_commit_single() {
let StatsCommand::Compare {
project_commit,
a_project_commit,
b_project_commit,
..
} = parse_compare(&[
"--project-commit",
"abc1234",
"--a-kernel",
"6.14",
"--b-kernel",
"6.15",
])
else {
unreachable!()
};
assert_eq!(project_commit, vec!["abc1234"]);
assert!(
a_project_commit.is_empty(),
"shared --project-commit must not populate --a-project-commit",
);
assert!(
b_project_commit.is_empty(),
"shared --project-commit must not populate --b-project-commit",
);
}
#[test]
fn parse_stats_compare_with_project_commit_repeatable() {
let StatsCommand::Compare { project_commit, .. } = parse_compare(&[
"--project-commit",
"a",
"--project-commit",
"b",
"--a-kernel",
"6.14",
"--b-kernel",
"6.15",
]) else {
unreachable!()
};
assert_eq!(project_commit, vec!["a", "b"]);
}
#[test]
fn parse_stats_compare_with_kernel_commit_single() {
let StatsCommand::Compare {
kernel_commit,
a_kernel_commit,
b_kernel_commit,
..
} = parse_compare(&[
"--kernel-commit",
"abc1234",
"--a-kernel",
"6.14",
"--b-kernel",
"6.15",
])
else {
unreachable!()
};
assert_eq!(kernel_commit, vec!["abc1234"]);
assert!(
a_kernel_commit.is_empty(),
"shared --kernel-commit must not populate --a-kernel-commit",
);
assert!(
b_kernel_commit.is_empty(),
"shared --kernel-commit must not populate --b-kernel-commit",
);
}
#[test]
fn parse_stats_compare_with_kernel_commit_repeatable() {
let StatsCommand::Compare { kernel_commit, .. } = parse_compare(&[
"--kernel-commit",
"a",
"--kernel-commit",
"b",
"--a-kernel",
"6.14",
"--b-kernel",
"6.15",
]) else {
unreachable!()
};
assert_eq!(kernel_commit, vec!["a", "b"]);
}
#[test]
fn parse_stats_compare_with_scheduler_repeatable() {
let StatsCommand::Compare { scheduler, .. } = parse_compare(&[
"--scheduler",
"scx_alpha",
"--scheduler",
"scx_beta",
"--a-kernel",
"6.14",
"--b-kernel",
"6.15",
]) else {
unreachable!()
};
assert_eq!(scheduler, vec!["scx_alpha", "scx_beta"]);
}
#[test]
fn parse_stats_compare_with_topology_repeatable() {
let StatsCommand::Compare { topology, .. } = parse_compare(&[
"--topology",
"1n2l4c2t",
"--topology",
"1n4l2c1t",
"--a-kernel",
"6.14",
"--b-kernel",
"6.15",
]) else {
unreachable!()
};
assert_eq!(topology, vec!["1n2l4c2t", "1n4l2c1t"]);
}
#[test]
fn parse_stats_compare_with_work_type_repeatable() {
let StatsCommand::Compare { work_type, .. } = parse_compare(&[
"--work-type",
"SpinWait",
"--work-type",
"PageFaultChurn",
"--a-kernel",
"6.14",
"--b-kernel",
"6.15",
]) else {
unreachable!()
};
assert_eq!(work_type, vec!["SpinWait", "PageFaultChurn"]);
}
#[test]
fn parse_stats_compare_with_per_side_kernel_commit() {
let StatsCommand::Compare {
kernel_commit,
a_kernel_commit,
b_kernel_commit,
..
} = parse_compare(&[
"--a-kernel-commit",
"abc1234",
"--b-kernel-commit",
"def5678",
])
else {
unreachable!()
};
assert!(
kernel_commit.is_empty(),
"per-side --a-kernel-commit / --b-kernel-commit must not \
populate the shared --kernel-commit vec",
);
assert_eq!(a_kernel_commit, vec!["abc1234"]);
assert_eq!(b_kernel_commit, vec!["def5678"]);
}
#[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_kernel_list_range() {
let Cargo {
command: CargoSub::Ktstr(k),
} = Cargo::try_parse_from(["cargo", "ktstr", "kernel", "list", "--range", "6.12..6.14"])
.unwrap_or_else(|e| panic!("{e}"));
let KtstrCommand::Kernel { command } = k.command else {
panic!("expected Kernel");
};
let KernelCommand::List { json, range } = command else {
panic!("expected KernelCommand::List, got {command:?}");
};
assert!(!json, "bare --range must not enable --json");
assert_eq!(
range.as_deref(),
Some("6.12..6.14"),
"--range must round-trip the literal spec for \
dispatch to pass to `expand_kernel_range`",
);
}
#[test]
fn parse_kernel_list_range_with_json() {
let Cargo {
command: CargoSub::Ktstr(k),
} = Cargo::try_parse_from([
"cargo",
"ktstr",
"kernel",
"list",
"--range",
"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, range } = command else {
panic!("expected KernelCommand::List, got {command:?}");
};
assert!(json, "--json must round-trip alongside --range");
assert_eq!(range.as_deref(), Some("6.12..6.14"));
}
#[test]
fn parse_stats_compare_with_run_source_single() {
let StatsCommand::Compare {
run_source,
a_run_source,
b_run_source,
..
} = parse_compare(&[
"--a-kernel",
"6.14",
"--b-kernel",
"6.15",
"--run-source",
"ci",
])
else {
unreachable!()
};
assert_eq!(
run_source,
vec!["ci".to_string()],
"shared --run-source must populate the shared vec",
);
assert!(
a_run_source.is_empty(),
"shared --run-source must not populate --a-run-source",
);
assert!(
b_run_source.is_empty(),
"shared --run-source must not populate --b-run-source",
);
}
#[test]
fn parse_stats_compare_with_run_source_per_side() {
let StatsCommand::Compare {
run_source,
a_run_source,
b_run_source,
..
} = parse_compare(&["--a-run-source", "ci", "--b-run-source", "local"])
else {
unreachable!()
};
assert!(
run_source.is_empty(),
"per-side flags must not populate the shared --run-source vec",
);
assert_eq!(a_run_source, vec!["ci".to_string()]);
assert_eq!(b_run_source, vec!["local".to_string()]);
}
#[test]
fn parse_kernel_build_version() {
let m = Cargo::try_parse_from(["cargo", "ktstr", "kernel", "build", "6.14.2"]);
assert!(m.is_ok(), "{}", m.err().unwrap());
}
#[test]
fn parse_kernel_build_source() {
let m = Cargo::try_parse_from(["cargo", "ktstr", "kernel", "build", "--source", "../linux"]);
assert!(m.is_ok(), "{}", m.err().unwrap());
}
#[test]
fn parse_kernel_build_source_conflicts_with_version() {
let result = Cargo::try_parse_from([
"cargo", "ktstr", "kernel", "build", "--source", "../linux", "6.14.2",
]);
match result {
Ok(_) => panic!("--source + positional VERSION must be rejected at parse time"),
Err(err) => assert_eq!(
err.kind(),
clap::error::ErrorKind::ArgumentConflict,
"expected ArgumentConflict — a different ErrorKind would \
signal that the conflicts_with attribute regressed in a way \
the bare is_err() pin would silently mask. Full err: {err}",
),
}
}
#[test]
fn parse_kernel_build_git_requires_ref() {
let result = Cargo::try_parse_from([
"cargo",
"ktstr",
"kernel",
"build",
"--git",
"https://example.com/linux.git",
]);
match result {
Ok(_) => panic!("--git without --ref must be rejected at parse time"),
Err(err) => assert_eq!(
err.kind(),
clap::error::ErrorKind::MissingRequiredArgument,
"expected MissingRequiredArgument — `--git` carries \
`requires = \"git_ref\"` (clap uses the field name, not \
the long flag name), so a regression that dropped the \
attribute would surface as a different ErrorKind that \
the bare is_err() pin would silently mask. Full err: {err}",
),
}
}
#[test]
fn parse_kernel_build_git_with_ref() {
let m = Cargo::try_parse_from([
"cargo",
"ktstr",
"kernel",
"build",
"--git",
"https://example.com/linux.git",
"--ref",
"v6.14",
]);
assert!(m.is_ok(), "{}", m.err().unwrap());
}
#[test]
fn parse_kernel_build_git_conflicts_with_source() {
let result = Cargo::try_parse_from([
"cargo",
"ktstr",
"kernel",
"build",
"--git",
"https://example.com/linux.git",
"--ref",
"v6.14",
"--source",
"../linux",
]);
match result {
Ok(_) => panic!("--git + --source must be rejected at parse time"),
Err(err) => assert_eq!(
err.kind(),
clap::error::ErrorKind::ArgumentConflict,
"expected ArgumentConflict — a different ErrorKind would \
signal that the conflicts_with attribute regressed in a way \
the bare is_err() pin would silently mask. Full err: {err}",
),
}
}
#[test]
fn parse_kernel_build_with_extra_kconfig() {
let Cargo {
command: CargoSub::Ktstr(k),
} = Cargo::try_parse_from([
"cargo",
"ktstr",
"kernel",
"build",
"6.14.2",
"--extra-kconfig",
"/tmp/extra.kconfig",
])
.unwrap_or_else(|e| panic!("{e}"));
let KtstrCommand::Kernel {
command:
KernelCommand::Build {
version,
extra_kconfig,
..
},
} = k.command
else {
panic!("expected KernelCommand::Build");
};
assert_eq!(version.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", "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",
"6.14.2",
"--skip-sha256",
])
.unwrap_or_else(|e| panic!("{e}"));
let KtstrCommand::Kernel {
command:
KernelCommand::Build {
version,
skip_sha256,
..
},
} = k.command
else {
panic!("expected KernelCommand::Build");
};
assert_eq!(version.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", "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_source() {
let Cargo {
command: CargoSub::Ktstr(k),
} = Cargo::try_parse_from([
"cargo",
"ktstr",
"kernel",
"build",
"--source",
"/tmp/src",
"--skip-sha256",
])
.unwrap_or_else(|e| panic!("{e}"));
let KtstrCommand::Kernel {
command:
KernelCommand::Build {
source,
skip_sha256,
..
},
} = k.command
else {
panic!("expected KernelCommand::Build");
};
assert_eq!(source, Some(std::path::PathBuf::from("/tmp/src")));
assert!(
skip_sha256,
"--skip-sha256 must round-trip when combined with --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",
"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",
"6.14.2..6.14.4",
"--skip-sha256",
])
.unwrap_or_else(|e| panic!("{e}"));
let KtstrCommand::Kernel {
command:
KernelCommand::Build {
version,
skip_sha256,
..
},
} = k.command
else {
panic!("expected KernelCommand::Build");
};
assert_eq!(version.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",
"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",
"6.14.2..6.14.4",
"--extra-kconfig",
"/tmp/range-extra.kconfig",
])
.unwrap_or_else(|e| panic!("{e}"));
let KtstrCommand::Kernel {
command:
KernelCommand::Build {
version,
extra_kconfig,
..
},
} = k.command
else {
panic!("expected KernelCommand::Build");
};
assert_eq!(version.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",
"--source",
"../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_rejected_on_verifier_subcommand() {
let m = Cargo::try_parse_from([
"cargo",
"ktstr",
"verifier",
"--kernel",
"../linux",
"--extra-kconfig",
"/tmp/x.kconfig",
]);
assert!(
m.is_err(),
"--extra-kconfig must be rejected on `cargo ktstr verifier` \
(verifier has no trailing_var_arg, so unknown flags fail at parse time)",
);
}
#[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 has no trailing_var_arg, so unknown flags fail at parse time)",
);
}
#[test]
fn parse_extra_kconfig_passes_through_test_subcommand_to_args_vec() {
let Cargo {
command: CargoSub::Ktstr(k),
} = Cargo::try_parse_from([
"cargo",
"ktstr",
"test",
"--extra-kconfig",
"/tmp/x.kconfig",
])
.unwrap_or_else(|e| panic!("{e}"));
let KtstrCommand::Test { args, .. } = k.command else {
panic!("expected KtstrCommand::Test");
};
assert_eq!(
args,
vec!["--extra-kconfig", "/tmp/x.kconfig"],
"--extra-kconfig must passthrough into `args` Vec on test \
subcommand (trailing_var_arg = true). The cargo nextest \
subprocess will reject it as an unknown flag downstream."
);
}
#[test]
fn parse_kernel_build_extra_kconfig_with_source() {
let Cargo {
command: CargoSub::Ktstr(k),
} = Cargo::try_parse_from([
"cargo",
"ktstr",
"kernel",
"build",
"--source",
"../linux",
"--extra-kconfig",
"/tmp/extra.kconfig",
])
.unwrap_or_else(|e| panic!("{e}"));
let KtstrCommand::Kernel {
command:
KernelCommand::Build {
source,
extra_kconfig,
..
},
} = k.command
else {
panic!("expected KernelCommand::Build");
};
assert_eq!(source, Some(std::path::PathBuf::from("../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 } = 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");
}
#[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_flag_rejected() {
let rejected =
Cargo::try_parse_from(["cargo", "ktstr", "verifier", "--scheduler", "scx_rustland"]);
assert!(
rejected.is_err(),
"--scheduler must be rejected — the flag was removed when the \
verifier sweep moved to declare_scheduler!-driven discovery",
);
}
#[test]
fn parse_verifier_all_profiles_rejected() {
let rejected = Cargo::try_parse_from(["cargo", "ktstr", "verifier", "--all-profiles"]);
assert!(
rejected.is_err(),
"--all-profiles must be rejected — the flag-profile sweep \
was removed from the verifier subcommand",
);
}
#[test]
fn parse_verifier_profiles_filter_rejected() {
let rejected = Cargo::try_parse_from([
"cargo",
"ktstr",
"verifier",
"--profiles",
"default,llc,llc+steal",
]);
assert!(
rejected.is_err(),
"--profiles must be rejected — the flag-profile sweep \
was removed from the verifier subcommand",
);
}
#[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".to_string(),
"bzImage".to_string(),
"2026-04-12T10:00:00Z".to_string(),
)
.with_version(Some("6.14.2".to_string()))
}
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".to_string(),
"bzImage".to_string(),
"2026-04-12T10:00:00Z".to_string(),
);
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(Some("old".to_string()));
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(Some("same".to_string()));
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(Some("h".to_string()));
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(Some("old".to_string()));
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("--range"),
"KERNEL_LIST_LONG_ABOUT must reference the `--range` 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_model_fetch() {
let Cargo {
command: CargoSub::Ktstr(k),
} = Cargo::try_parse_from(["cargo", "ktstr", "model", "fetch"])
.unwrap_or_else(|e| panic!("{e}"));
let KtstrCommand::Model { command } = k.command else {
panic!("expected Model");
};
assert!(matches!(command, ModelCommand::Fetch));
}
#[test]
fn parse_model_status() {
let Cargo {
command: CargoSub::Ktstr(k),
} = Cargo::try_parse_from(["cargo", "ktstr", "model", "status"])
.unwrap_or_else(|e| panic!("{e}"));
let KtstrCommand::Model { command } = k.command else {
panic!("expected Model");
};
assert!(matches!(command, ModelCommand::Status));
}
#[test]
fn parse_model_clean() {
let Cargo {
command: CargoSub::Ktstr(k),
} = Cargo::try_parse_from(["cargo", "ktstr", "model", "clean"])
.unwrap_or_else(|e| panic!("{e}"));
let KtstrCommand::Model { command } = k.command else {
panic!("expected Model");
};
assert!(matches!(command, ModelCommand::Clean));
}
#[test]
fn parse_model_missing_subcommand_rejected() {
let rejected = Cargo::try_parse_from(["cargo", "ktstr", "model"]);
assert!(
rejected.is_err(),
"model must require a subcommand (fetch/status/clean)",
);
}
#[test]
fn parse_model_unknown_subcommand_rejected() {
let rejected = Cargo::try_parse_from(["cargo", "ktstr", "model", "wat"]);
assert!(rejected.is_err(), "model must reject unknown subcommands",);
}
#[test]
fn parse_funify_bare() {
let Cargo {
command: CargoSub::Ktstr(k),
} = Cargo::try_parse_from(["cargo", "ktstr", "funify"]).unwrap_or_else(|e| panic!("{e}"));
let KtstrCommand::Funify {
input,
seed,
pretty,
} = k.command
else {
panic!("expected Funify");
};
assert!(input.is_none(), "bare funify must default --input to None");
assert!(seed.is_none(), "bare funify must default --seed to None");
assert!(!pretty, "bare funify must default --pretty to false");
}
#[test]
fn parse_funify_with_input_path() {
let Cargo {
command: CargoSub::Ktstr(k),
} = Cargo::try_parse_from(["cargo", "ktstr", "funify", "/tmp/dump.json"])
.unwrap_or_else(|e| panic!("{e}"));
let KtstrCommand::Funify { input, .. } = k.command else {
panic!("expected Funify");
};
assert_eq!(
input,
Some(std::path::PathBuf::from("/tmp/dump.json")),
"positional INPUT must round-trip to Some(PathBuf)",
);
}
#[test]
fn parse_funify_with_seed_and_pretty() {
let Cargo {
command: CargoSub::Ktstr(k),
} = Cargo::try_parse_from(["cargo", "ktstr", "funify", "--seed", "demo", "--pretty"])
.unwrap_or_else(|e| panic!("{e}"));
let KtstrCommand::Funify {
input,
seed,
pretty,
} = k.command
else {
panic!("expected Funify");
};
assert!(input.is_none(), "no positional INPUT must produce None");
assert_eq!(seed.as_deref(), Some("demo"));
assert!(pretty, "--pretty must lift the flag to true");
}
#[test]
fn parse_funify_costume_alias_dispatches_to_funify() {
let Cargo {
command: CargoSub::Ktstr(k),
} = Cargo::try_parse_from(["cargo", "ktstr", "costume"]).unwrap_or_else(|e| panic!("{e}"));
assert!(
matches!(k.command, KtstrCommand::Funify { .. }),
"`costume` alias must dispatch to the Funify variant",
);
}
#[test]
fn parse_funify_costume_alias_with_seed_and_pretty() {
let Cargo {
command: CargoSub::Ktstr(k),
} = Cargo::try_parse_from([
"cargo",
"ktstr",
"costume",
"/tmp/dump.json",
"--seed",
"demo",
"--pretty",
])
.unwrap_or_else(|e| panic!("{e}"));
let KtstrCommand::Funify {
input,
seed,
pretty,
} = k.command
else {
panic!("expected Funify (via `costume` alias)");
};
assert_eq!(
input,
Some(std::path::PathBuf::from("/tmp/dump.json")),
"costume alias must round-trip the positional INPUT",
);
assert_eq!(seed.as_deref(), Some("demo"));
assert!(
pretty,
"--pretty must lift the flag to true on costume alias"
);
}
#[test]
fn parse_funify_with_dash_input() {
let Cargo {
command: CargoSub::Ktstr(k),
} = Cargo::try_parse_from(["cargo", "ktstr", "funify", "-"]).unwrap_or_else(|e| panic!("{e}"));
let KtstrCommand::Funify { input, .. } = k.command else {
panic!("expected Funify");
};
assert_eq!(
input,
Some(std::path::PathBuf::from("-")),
"the `-` stdin sentinel must round-trip as Some(PathBuf(\"-\")), \
NOT collapse to None — the dispatch path keys on this exact form",
);
}
#[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_mb_valid() {
let Cargo {
command: CargoSub::Ktstr(k),
} = Cargo::try_parse_from(["cargo", "ktstr", "shell", "--memory-mb", "256"])
.unwrap_or_else(|e| panic!("{e}"));
let KtstrCommand::Shell { memory_mb, .. } = k.command else {
panic!("expected Shell");
};
assert_eq!(memory_mb, Some(256), "--memory-mb 256 must round-trip");
}
#[test]
fn parse_shell_memory_mb_at_range_floor() {
let Cargo {
command: CargoSub::Ktstr(k),
} = Cargo::try_parse_from(["cargo", "ktstr", "shell", "--memory-mb", "128"])
.unwrap_or_else(|e| panic!("{e}"));
let KtstrCommand::Shell { memory_mb, .. } = k.command else {
panic!("expected Shell");
};
assert_eq!(
memory_mb,
Some(128),
"--memory-mb 128 must succeed at the inclusive range floor",
);
}
#[test]
fn parse_shell_memory_mb_below_range_rejected() {
let rejected = Cargo::try_parse_from(["cargo", "ktstr", "shell", "--memory-mb", "64"]);
assert!(
rejected.is_err(),
"--memory-mb 64 must be rejected — value_parser range floor is 128",
);
}
#[test]
fn parse_shell_memory_mb_negative_rejected() {
let rejected = Cargo::try_parse_from(["cargo", "ktstr", "shell", "--memory-mb", "-1"]);
assert!(
rejected.is_err(),
"--memory-mb -1 must be rejected — the field is u32",
);
}
#[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_kernel_build_ref_requires_git() {
let result = Cargo::try_parse_from(["cargo", "ktstr", "kernel", "build", "--ref", "v6.14"]);
match result {
Ok(_) => panic!("--ref without --git must be rejected at parse time"),
Err(err) => assert_eq!(
err.kind(),
clap::error::ErrorKind::MissingRequiredArgument,
"expected MissingRequiredArgument — `--ref` carries \
`requires = \"git\"` (clap uses the field name, not \
the long flag name), so a regression that dropped the \
attribute would surface as a different ErrorKind that \
the bare is_err() pin would silently mask. Full err: {err}",
),
}
}
#[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_stats_compare_with_per_side_topology() {
let StatsCommand::Compare {
topology,
a_topology,
b_topology,
..
} = parse_compare(&["--a-topology", "1n2l4c2t", "--b-topology", "1n4l2c1t"])
else {
unreachable!()
};
assert!(
topology.is_empty(),
"per-side --a-topology / --b-topology must not populate the shared --topology vec",
);
assert_eq!(a_topology, vec!["1n2l4c2t".to_string()]);
assert_eq!(b_topology, vec!["1n4l2c1t".to_string()]);
}
#[test]
fn parse_stats_compare_with_per_side_scheduler() {
let StatsCommand::Compare {
scheduler,
a_scheduler,
b_scheduler,
..
} = parse_compare(&["--a-scheduler", "scx_alpha", "--b-scheduler", "scx_beta"])
else {
unreachable!()
};
assert!(
scheduler.is_empty(),
"per-side --a-scheduler / --b-scheduler must not populate the shared --scheduler vec",
);
assert_eq!(a_scheduler, vec!["scx_alpha".to_string()]);
assert_eq!(b_scheduler, vec!["scx_beta".to_string()]);
}
#[test]
fn parse_stats_compare_with_per_side_work_type() {
let StatsCommand::Compare {
work_type,
a_work_type,
b_work_type,
..
} = parse_compare(&[
"--a-work-type",
"SpinWait",
"--b-work-type",
"PageFaultChurn",
])
else {
unreachable!()
};
assert!(
work_type.is_empty(),
"per-side --a-work-type / --b-work-type must not populate the shared --work-type vec",
);
assert_eq!(a_work_type, vec!["SpinWait".to_string()]);
assert_eq!(b_work_type, vec!["PageFaultChurn".to_string()]);
}
#[test]
fn parse_stats_compare_with_per_side_project_commit() {
let StatsCommand::Compare {
project_commit,
a_project_commit,
b_project_commit,
..
} = parse_compare(&[
"--a-project-commit",
"abc1234",
"--b-project-commit",
"def5678",
])
else {
unreachable!()
};
assert!(
project_commit.is_empty(),
"per-side --a-project-commit / --b-project-commit must not \
populate the shared --project-commit vec",
);
assert_eq!(a_project_commit, vec!["abc1234".to_string()]);
assert_eq!(b_project_commit, vec!["def5678".to_string()]);
}
#[test]
fn parse_stats_compare_flag_filters_rejected() {
let outcome = std::panic::catch_unwind(|| {
parse_compare(&[
"--flag",
"shared_flag",
"--a-flag",
"a_only_flag",
"--b-flag",
"b_only_flag",
])
});
assert!(
outcome.is_err(),
"--flag / --a-flag / --b-flag must be rejected — the flag-profile \
filter was removed when the flag infrastructure went away",
);
}