use std::path::{Path, PathBuf};
use std::process::Command;
use ktstr::cli;
use crate::kernel::{resolve_kernel_image, resolve_kernel_set};
fn query_scheduler_flags(
sched_bin: &Path,
) -> Result<Vec<ktstr::scenario::flags::FlagDeclJson>, String> {
let output = Command::new(sched_bin)
.arg("--ktstr-list-flags")
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.output()
.map_err(|e| format!("run scheduler --ktstr-list-flags: {e:#}"))?;
if !output.status.success() {
return Ok(Vec::new());
}
let stdout = String::from_utf8_lossy(&output.stdout);
let trimmed = stdout.trim();
if trimmed.is_empty() {
return Ok(Vec::new());
}
serde_json::from_str(trimmed).map_err(|e| format!("parse --ktstr-list-flags output: {e:#}"))
}
fn generate_flag_profiles(
flags: &[ktstr::scenario::flags::FlagDeclJson],
) -> Vec<(String, Vec<String>)> {
let n = flags.len();
if n > 31 {
eprintln!(
"cargo ktstr: error: scheduler has {n} flags, power set too large (2^{n}). \
Use --profiles to select specific profiles."
);
return Vec::new();
}
let all: Vec<String> = flags.iter().map(|f| f.name.clone()).collect();
let requires_fn = |name: &String| -> Vec<String> {
flags
.iter()
.find(|f| f.name == *name)
.map(|f| f.requires.clone())
.unwrap_or_default()
};
ktstr::scenario::compute_flag_profiles(&all, requires_fn, &[], &[])
.into_iter()
.map(|flag_names| {
let name = if flag_names.is_empty() {
"default".to_string()
} else {
flag_names.join("+")
};
(name, flag_names)
})
.collect()
}
fn profile_sched_args(
active_flags: &[String],
all_flags: &[ktstr::scenario::flags::FlagDeclJson],
) -> Result<Vec<String>, String> {
let mut args = Vec::new();
for flag_name in active_flags {
match all_flags.iter().find(|f| f.name == *flag_name) {
Some(decl) => args.extend(decl.args.iter().cloned()),
None => {
let known: Vec<&str> = all_flags.iter().map(|f| f.name.as_str()).collect();
return Err(format!(
"unknown flag {flag_name:?} (known: {})",
known.join(", ")
));
}
}
}
Ok(args)
}
pub(crate) fn run_verifier(
scheduler: Option<String>,
scheduler_bin: Option<PathBuf>,
kernel: Vec<String>,
raw: bool,
all_profiles: bool,
profiles_filter: Vec<String>,
) -> Result<(), String> {
cli::check_kvm().map_err(|e| format!("{e:#}"))?;
let sched_bin = match (scheduler, scheduler_bin) {
(Some(package), None) => {
ktstr::build_and_find_binary(&package).map_err(|e| format!("build scheduler: {e:#}"))?
}
(None, Some(path)) => {
if !path.exists() {
return Err(format!("scheduler binary not found: {}", path.display()));
}
path
}
(None, None) => {
return Err("either --scheduler or --scheduler-bin is required".to_string());
}
(Some(_), Some(_)) => unreachable!(),
};
let kernel_paths: Vec<(String, PathBuf)> = if kernel.is_empty() {
let path = resolve_kernel_image(None)?;
vec![("auto".to_string(), path)]
} else {
let resolved = resolve_kernel_set(&kernel)?;
if resolved.is_empty() {
return Err(
"--kernel: every supplied value parsed to empty / whitespace; \
omit the flag for auto-discovery, or supply a kernel \
identifier"
.to_string(),
);
}
let mut out: Vec<(String, PathBuf)> = Vec::with_capacity(resolved.len());
for (label, dir) in resolved {
let image = ktstr::kernel_path::find_image_in_dir(&dir).ok_or_else(|| {
format!(
"no kernel image found in {} (resolved from --kernel {label})",
dir.display()
)
})?;
out.push((label, image));
}
out
};
let ktstr_bin =
ktstr::build_and_find_binary("ktstr").map_err(|e| format!("build ktstr: {e:#}"))?;
let multi_kernel = kernel_paths.len() > 1;
for (i, (label, kernel_path)) in kernel_paths.iter().enumerate() {
if multi_kernel {
eprintln!(
"cargo ktstr: [kernel {}/{}] {label}",
i + 1,
kernel_paths.len(),
);
println!("\n=== kernel: {label} ===");
}
if all_profiles || !profiles_filter.is_empty() {
run_verifier_all_profiles(&sched_bin, &ktstr_bin, kernel_path, raw, &profiles_filter)?;
continue;
}
eprintln!("cargo ktstr: collecting verifier stats");
let result =
ktstr::verifier::collect_verifier_output(&sched_bin, &ktstr_bin, kernel_path, &[])
.map_err(|e| format!("collect verifier output: {e:#}"))?;
let output = ktstr::verifier::format_verifier_output("verifier", &result, raw);
print!("{output}");
}
Ok(())
}
fn run_verifier_all_profiles(
sched_bin: &Path,
ktstr_bin: &Path,
kernel_path: &Path,
raw: bool,
profiles_filter: &[String],
) -> Result<(), String> {
let flags = query_scheduler_flags(sched_bin)?;
if flags.is_empty() {
eprintln!(
"cargo ktstr: scheduler does not support --ktstr-list-flags, \
running with default profile only"
);
let result =
ktstr::verifier::collect_verifier_output(sched_bin, ktstr_bin, kernel_path, &[])
.map_err(|e| format!("collect verifier output: {e:#}"))?;
let output = ktstr::verifier::format_verifier_output("default", &result, raw);
print!("{output}");
return Ok(());
}
let all_profiles = generate_flag_profiles(&flags);
let profiles: Vec<&(String, Vec<String>)> = if profiles_filter.is_empty() {
all_profiles.iter().collect()
} else {
let filtered: Vec<_> = all_profiles
.iter()
.filter(|(name, _)| profiles_filter.iter().any(|f| f == name))
.collect();
if filtered.is_empty() {
return Err(format!(
"no matching profiles found. Available: {}",
all_profiles
.iter()
.map(|(n, _)| n.as_str())
.collect::<Vec<_>>()
.join(", ")
));
}
filtered
};
let total = profiles.len();
if total == 0 {
return Err(if flags.len() > 31 {
format!(
"no profiles to verify: power-set generation is capped at \
31 flags (found {}); use --profiles to select a subset",
flags.len(),
)
} else {
format!(
"no profiles to verify: {} flag(s) advertised but profile \
generation produced 0 profiles — check `requires` \
dependencies and exclusions for cycles or conflicts",
flags.len(),
)
});
}
if total > 32 {
eprintln!(
"cargo ktstr: warning: {total} profiles to verify (>32). \
Use --profiles to select a subset."
);
}
eprintln!(
"cargo ktstr: verifying {total} profile{}",
if total == 1 { "" } else { "s" }
);
let mut summary: Vec<(String, Vec<(String, u32)>)> = Vec::new();
for (i, (profile_name, active_flags)) in profiles.iter().enumerate() {
eprintln!(
"cargo ktstr: [{}/{}] profile: {}",
i + 1,
total,
profile_name
);
let extra_args = profile_sched_args(active_flags, &flags)
.map_err(|e| format!("profile {profile_name}: {e}"))?;
let result = ktstr::verifier::collect_verifier_output(
sched_bin,
ktstr_bin,
kernel_path,
&extra_args,
)
.map_err(|e| format!("profile {profile_name}: {e:#}"))?;
let output = ktstr::verifier::format_verifier_output(profile_name, &result, raw);
print!("{output}");
let prog_stats: Vec<(String, u32)> = result
.stats
.iter()
.map(|ps| (ps.name.clone(), ps.verified_insns))
.collect();
summary.push((profile_name.clone(), prog_stats));
}
if summary.len() > 1 {
print_profile_summary(&summary);
}
Ok(())
}
fn print_profile_summary(summary: &[(String, Vec<(String, u32)>)]) {
let mut prog_names: Vec<String> = Vec::new();
for (_, progs) in summary {
for (name, _) in progs {
if !prog_names.contains(name) {
prog_names.push(name.clone());
}
}
}
println!("\n--- profile summary ---");
let profile_names: Vec<&str> = summary.iter().map(|(n, _)| n.as_str()).collect();
let mut table = ktstr::cli::new_table();
let mut header: Vec<&str> = Vec::with_capacity(1 + profile_names.len());
header.push("program");
header.extend(profile_names.iter().copied());
table.set_header(header);
for prog in &prog_names {
let mut row: Vec<String> = Vec::with_capacity(1 + profile_names.len());
row.push(prog.clone());
for (_, progs) in summary {
let insns = progs
.iter()
.find(|(n, _)| n == prog)
.map(|(_, v)| *v)
.unwrap_or(0);
row.push(insns.to_string());
}
table.add_row(row);
}
println!("{table}");
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn generate_flag_profiles_empty() {
let profiles = generate_flag_profiles(&[]);
assert_eq!(profiles.len(), 1);
assert_eq!(profiles[0].0, "default");
assert!(profiles[0].1.is_empty());
}
#[test]
fn generate_flag_profiles_single_flag() {
let flags = vec![ktstr::scenario::flags::FlagDeclJson {
name: "llc".to_string(),
args: vec!["--llc".to_string()],
requires: vec![],
}];
let profiles = generate_flag_profiles(&flags);
assert_eq!(profiles.len(), 2);
assert_eq!(profiles[0].0, "default");
assert_eq!(profiles[1].0, "llc");
}
#[test]
fn generate_flag_profiles_requires_constraint() {
let flags = vec![
ktstr::scenario::flags::FlagDeclJson {
name: "llc".to_string(),
args: vec!["--llc".to_string()],
requires: vec![],
},
ktstr::scenario::flags::FlagDeclJson {
name: "steal".to_string(),
args: vec!["--steal".to_string()],
requires: vec!["llc".to_string()],
},
];
let profiles = generate_flag_profiles(&flags);
let names: Vec<&str> = profiles.iter().map(|(n, _)| n.as_str()).collect();
assert_eq!(profiles.len(), 3);
assert!(names.contains(&"default"));
assert!(names.contains(&"llc"));
assert!(names.contains(&"llc+steal"));
assert!(!names.contains(&"steal"));
}
#[test]
fn profile_sched_args_collects_args() {
let flags = vec![
ktstr::scenario::flags::FlagDeclJson {
name: "llc".to_string(),
args: vec!["--llc".to_string()],
requires: vec![],
},
ktstr::scenario::flags::FlagDeclJson {
name: "steal".to_string(),
args: vec!["--steal".to_string(), "--aggressive".to_string()],
requires: vec![],
},
];
let active = vec!["llc".to_string(), "steal".to_string()];
let args = profile_sched_args(&active, &flags).unwrap();
assert_eq!(args, vec!["--llc", "--steal", "--aggressive"]);
}
#[test]
fn profile_sched_args_empty() {
let flags = vec![ktstr::scenario::flags::FlagDeclJson {
name: "llc".to_string(),
args: vec!["--llc".to_string()],
requires: vec![],
}];
let active: Vec<String> = vec![];
let args = profile_sched_args(&active, &flags).unwrap();
assert!(args.is_empty());
}
#[test]
fn profile_sched_args_unknown_flag_errors() {
let flags = vec![ktstr::scenario::flags::FlagDeclJson {
name: "llc".to_string(),
args: vec!["--llc".to_string()],
requires: vec![],
}];
let active = vec!["llc".to_string(), "unknown_flag".to_string()];
let err = profile_sched_args(&active, &flags).unwrap_err();
assert!(
err.contains("unknown_flag"),
"error should cite flag: {err}"
);
assert!(err.contains("llc"), "error should list known flags: {err}");
}
}