use std::collections::HashMap;
use std::process::ExitCode;
use idakit::corpus::{self, Fixture, WorkingCopy};
use idakit::prelude::Ida;
use idakit_runner::{Case, CaseResult, Outcome, Runner, Status, expecting_panic, serve};
use super::TestDb;
use super::checks::{CHECKS, Check};
use super::registry::{Isolation, KernelTest, Warm};
pub const WORKER_FLAG: &str = "--idakit-worker";
const CORPUS: &str = "corpus";
const SCOPE: &str = "IDAKIT_TEST_SCOPE";
const WORKERS: &str = "IDAKIT_TEST_WORKERS";
#[derive(Clone, Copy, PartialEq, Eq)]
enum Scope {
All,
Registered,
Corpus,
}
impl Scope {
fn from_env() -> Result<Self, String> {
match std::env::var(SCOPE) {
Err(_) => Ok(Self::All),
Ok(value) => match value.as_str() {
"all" | "" => Ok(Self::All),
"registered" => Ok(Self::Registered),
"corpus" => Ok(Self::Corpus),
other => Err(format!(
"{SCOPE}={other:?} is not one of all, registered, corpus"
)),
},
}
}
}
fn worker_cap() -> Result<Option<usize>, String> {
match std::env::var(WORKERS) {
Err(_) => Ok(None),
Ok(value) => value
.parse()
.map(Some)
.map_err(|_| format!("{WORKERS}={value:?} is not a worker count")),
}
}
#[must_use]
pub fn run() -> ExitCode {
let args: Vec<String> = std::env::args().skip(1).collect();
if args.iter().any(|arg| arg == WORKER_FLAG) {
return work();
}
let list = args.iter().any(|arg| arg == "--list");
let filters: Vec<&str> = args
.iter()
.filter(|arg| !arg.starts_with('-'))
.map(String::as_str)
.collect();
drive(&filters, list)
}
struct Site {
fixture: Fixture,
canonical: bool,
checks: bool,
}
struct Planned {
name: String,
site: usize,
body: Body,
isolation: Isolation,
skip: Option<String>,
}
enum Body {
Check(Check),
Test(&'static KernelTest),
}
fn sites() -> Vec<Site> {
let mut sites: Vec<Site> = corpus::fixtures()
.into_iter()
.map(|fixture| Site {
fixture,
canonical: false,
checks: true,
})
.collect();
let Some(path) = TestDb::source() else {
return sites;
};
if let Some(site) = sites.iter_mut().find(|s| s.fixture.path == path) {
site.canonical = true;
} else {
sites.push(Site {
fixture: Fixture {
name: "canonical".to_owned(),
path,
skip_checks: Vec::new(),
decompiler: true,
},
canonical: true,
checks: false,
});
}
sites
}
fn plan(sites: &[Site], scope: Scope) -> Vec<Planned> {
let mut planned = Vec::new();
if scope != Scope::Registered {
for (index, site) in sites.iter().enumerate().filter(|(_, s)| s.checks) {
for &(check_name, check) in CHECKS {
planned.push(Planned {
name: format!("{CORPUS}::{}::{check_name}", site.fixture.name),
site: index,
body: Body::Check(check),
isolation: Isolation::ReadOnly,
skip: effective_skip(&site.fixture, check_name),
});
}
}
}
if scope != Scope::Corpus
&& let Some(index) = sites.iter().position(|s| s.canonical)
{
for test in inventory::iter::<KernelTest> {
planned.push(Planned {
name: test.case_name(),
site: index,
body: Body::Test(test),
isolation: test.isolation,
skip: None,
});
}
}
planned
}
fn drive(filters: &[&str], list: bool) -> ExitCode {
if let Err(reason) = corpus::validate() {
println!("manifest_is_valid ... FAILED\n {reason}");
return ExitCode::FAILURE;
}
let (scope, cap) = match (Scope::from_env(), worker_cap()) {
(Ok(scope), Ok(cap)) => (scope, cap),
(Err(reason), _) | (_, Err(reason)) => {
println!("kernel: {reason}");
return ExitCode::FAILURE;
}
};
let sites = sites();
if let Some(dup) = first_duplicate(sites.iter().map(|s| s.fixture.name.as_str())) {
println!("fixtures collide on display name {dup:?}");
return ExitCode::FAILURE;
}
let planned = plan(&sites, scope);
if let Some(dup) = first_duplicate(planned.iter().map(|p| p.name.as_str())) {
println!("cases collide on name {dup:?}");
return ExitCode::FAILURE;
}
if planned.is_empty() {
println!("kernel: no corpus configured, skipping");
return ExitCode::SUCCESS;
}
let cases: Vec<Case> = planned
.iter()
.filter(|case| filters.is_empty() || filters.iter().any(|f| case.name.contains(f)))
.map(|case| Case::new(case.name.clone()).group(sites[case.site].fixture.name.clone()))
.collect();
if list {
for case in &cases {
println!("{}", case.name);
}
return ExitCode::SUCCESS;
}
if cases.is_empty() {
println!("kernel: no cases matched, skipping");
return ExitCode::SUCCESS;
}
let program = match std::env::current_exe() {
Ok(path) => path,
Err(err) => {
println!("kernel: cannot resolve this executable: {err}");
return ExitCode::FAILURE;
}
};
let total = cases.len();
let mut runner = Runner::new(program, &[WORKER_FLAG]);
if let Some(cap) = cap {
runner = runner.workers(cap);
}
match runner.run(cases) {
Ok(results) => report(&results, total),
Err(err) => {
println!("kernel: could not start workers: {err}");
ExitCode::FAILURE
}
}
}
fn work() -> ExitCode {
let Ok(scope) = Scope::from_env() else {
eprintln!("worker: bad {SCOPE}");
return ExitCode::FAILURE;
};
let sites = sites();
let plan: HashMap<String, Planned> = plan(&sites, scope)
.into_iter()
.map(|case| (case.name.clone(), case))
.collect();
let outcome = Ida::run(move |ida| {
let mut open: Option<Open> = None;
serve(move |name| {
let Some(case) = plan.get(name) else {
return Outcome::Failed(format!("no such case: {name}"));
};
if let Some(reason) = &case.skip {
return Outcome::Skipped(reason.clone());
}
if let Err(reason) = ensure_open(&ida, &mut open, case.site, &sites[case.site]) {
return Outcome::Failed(reason);
}
if case.isolation == Isolation::Writes {
open.as_mut().expect("ensure_open left one open").clean = false;
}
match &case.body {
Body::Check(check) => {
let check = *check;
ida.call(move |idb| check(&*idb))
.map_or_else(|err| Outcome::Failed(err.to_string()), Outcome::from)
}
Body::Test(test) => {
let _warm = Warm::new(&ida);
invoke(test)
}
}
})
});
match outcome {
Ok(Ok(())) => ExitCode::SUCCESS,
Ok(Err(err)) => {
eprintln!("worker stream ended: {err}");
ExitCode::FAILURE
}
Err(err) => {
eprintln!("kernel init failed: {err}");
ExitCode::FAILURE
}
}
}
fn invoke(test: &KernelTest) -> Outcome {
let Some(expected) = test.should_panic else {
(test.run)();
return Outcome::Passed(None);
};
expecting_panic(expected, test.run)
}
struct Open {
site: usize,
copy: Scratch,
clean: bool,
}
enum Scratch {
Ram(TestDb),
Corpus(WorkingCopy),
}
impl Scratch {
fn path(&self) -> &str {
match self {
Self::Ram(db) => db.path(),
Self::Corpus(copy) => copy.path(),
}
}
}
fn ensure_open(
ida: &Ida,
open: &mut Option<Open>,
index: usize,
site: &Site,
) -> Result<(), String> {
if open.as_ref().is_some_and(|current| current.site == index) {
let current = open.as_mut().expect("just checked");
if current.clean {
return Ok(());
}
let path = current.copy.path().to_owned();
close(ida);
open_at(ida, &path)?;
current.clean = true;
return Ok(());
}
if open.is_some() {
close(ida);
*open = None;
}
let copy = if site.canonical {
Scratch::Ram(TestDb::copy_of(&site.fixture.path))
} else {
Scratch::Corpus(corpus::working_copy(&site.fixture.path).map_err(|e| e.to_string())?)
};
open_at(ida, copy.path())?;
*open = Some(Open {
site: index,
copy,
clean: true,
});
Ok(())
}
fn close(ida: &Ida) {
let _ = ida.call(|idb| idb.close(false));
}
fn open_at(ida: &Ida, path: &str) -> Result<(), String> {
let path = path.to_owned();
ida.call(move |idb| idb.open(&path).call().map_err(|e| e.to_string()))
.map_err(|e| e.to_string())?
}
fn effective_skip(fixture: &Fixture, check: &str) -> Option<String> {
if fixture.skips(check) {
return Some("manifest".to_owned());
}
if !fixture.decompiler && matches!(check, "decompile" | "argloc") {
return Some("no decompiler".to_owned());
}
None
}
fn first_duplicate<'a>(names: impl Iterator<Item = &'a str>) -> Option<String> {
let mut names: Vec<&str> = names.collect();
names.sort_unstable();
names
.windows(2)
.find(|w| w[0] == w[1])
.map(|w| w[0].to_owned())
}
fn report(results: &[CaseResult], total: usize) -> ExitCode {
let mut ordered: Vec<&CaseResult> = results.iter().collect();
ordered.sort_by(|a, b| a.name.cmp(&b.name));
let mut passed = 0usize;
let mut skipped = 0usize;
let mut failed = Vec::new();
for result in &ordered {
let (label, detail) = match result.status {
Status::Passed => {
passed += 1;
("ok", result.message.as_str())
}
Status::Skipped => {
skipped += 1;
("skipped", result.message.as_str())
}
Status::Failed => {
failed.push(*result);
("FAILED", result.message.as_str())
}
};
if detail.is_empty() {
println!("{} ... {label} ({}ms)", result.name, result.millis);
} else {
println!("{} ... {label} ({}ms) {detail}", result.name, result.millis);
}
}
for result in &failed {
println!("\nfailure: {}\n{}", result.name, result.message);
for line in &result.output {
println!("{line}");
}
}
println!(
"\nsummary: {passed} passed, {} failed, {skipped} skipped, {total} total",
failed.len()
);
if failed.is_empty() {
ExitCode::SUCCESS
} else {
ExitCode::FAILURE
}
}