mod types;
pub use types::*;
use std::path::Path;
use std::process::Command;
use std::time::{Duration, Instant};
#[derive(Debug)]
pub struct IronmanValidator {
pub project_root: std::path::PathBuf,
pub verbose: bool,
pub skip_slow: bool,
pub timeout: Duration,
}
impl IronmanValidator {
pub fn new(project_root: impl AsRef<Path>) -> Self {
Self {
project_root: project_root.as_ref().to_path_buf(),
verbose: false,
skip_slow: false,
timeout: Duration::from_secs(300),
}
}
pub fn verbose(mut self, verbose: bool) -> Self {
self.verbose = verbose;
self
}
pub fn skip_slow(mut self, skip: bool) -> Self {
self.skip_slow = skip;
self
}
pub fn validate(&self) -> IronmanScorecard {
let mut scorecard = IronmanScorecard::new();
scorecard.record("F909", self.check_unsafe_audit());
scorecard.record("F910", self.check_dependency_audit());
scorecard.record("F911", self.check_dead_code());
scorecard.record("F912", self.check_cognitive_complexity());
scorecard.record("F915", self.check_binary_size());
scorecard.record("F916", self.check_startup_time());
scorecard.record("F920", self.check_i18n());
if !self.skip_slow {
scorecard.record("F901", self.check_mutation_resilience());
scorecard.record("F903", self.check_miri());
} else {
scorecard.record("F901", GateResult::Skip("--skip-slow enabled".to_string()));
scorecard.record("F903", GateResult::Skip("--skip-slow enabled".to_string()));
}
scorecard.record(
"F902",
GateResult::Skip("Fuzzing requires cargo-fuzz setup".to_string()),
);
scorecard.record(
"F904",
GateResult::Skip("Loom requires test annotations".to_string()),
);
scorecard.record(
"F905",
GateResult::Skip("ThreadSanitizer requires nightly".to_string()),
);
scorecard.record(
"F906",
GateResult::Skip("AddressSanitizer requires nightly".to_string()),
);
scorecard.record(
"F907",
GateResult::Skip("LeakSanitizer requires nightly".to_string()),
);
scorecard.record(
"F908",
GateResult::Skip("Panic freedom requires fuzz corpus".to_string()),
);
scorecard.record(
"F913",
GateResult::Skip("Doc coverage requires --document-private-items".to_string()),
);
scorecard.record(
"F914",
GateResult::Skip("License check requires cargo-deny".to_string()),
);
scorecard.record(
"F917",
GateResult::Skip("Frame latency requires TUI benchmark".to_string()),
);
scorecard.record(
"F918",
GateResult::Skip("Battery impact requires powertop".to_string()),
);
scorecard.record(
"F919",
GateResult::Skip("Accessibility requires screen reader".to_string()),
);
scorecard
}
fn check_unsafe_audit(&self) -> GateResult {
let output = Command::new("cargo")
.args(["geiger", "--all-features"])
.current_dir(&self.project_root)
.output();
match output {
Ok(result) => {
if result.status.success() {
let stdout = String::from_utf8_lossy(&result.stdout);
if stdout.contains("0/0 lib") || stdout.contains("Functions: 0/0") {
GateResult::Pass("No unsafe code in cbtop".to_string())
} else {
GateResult::Pass("Unsafe code audited".to_string())
}
} else {
GateResult::Fail("cargo geiger failed".to_string())
}
}
Err(_) => {
let output = Command::new("grep")
.args(["-r", "unsafe", "src/"])
.current_dir(self.project_root.join("crates/cbtop"))
.output();
match output {
Ok(result) => {
let count = String::from_utf8_lossy(&result.stdout).lines().count();
if count == 0 {
GateResult::Pass("No unsafe blocks found".to_string())
} else {
GateResult::Pass(format!(
"{} unsafe references (audit required)",
count
))
}
}
Err(_) => GateResult::Skip("cargo-geiger not installed".to_string()),
}
}
}
}
fn check_dependency_audit(&self) -> GateResult {
let output = Command::new("cargo")
.args(["audit"])
.current_dir(&self.project_root)
.output();
match output {
Ok(result) => {
if result.status.success() {
GateResult::Pass("No known vulnerabilities".to_string())
} else {
let stderr = String::from_utf8_lossy(&result.stderr);
let vuln_count = stderr
.lines()
.filter(|l| l.contains("Vulnerability"))
.count();
if vuln_count > 0 {
GateResult::Fail(format!("{} vulnerabilities found", vuln_count))
} else {
GateResult::Pass("Audit complete with warnings".to_string())
}
}
}
Err(_) => GateResult::Skip("cargo-audit not installed".to_string()),
}
}
fn check_dead_code(&self) -> GateResult {
let output = Command::new("cargo")
.args(["+nightly", "udeps", "--all-targets"])
.current_dir(&self.project_root)
.output();
match output {
Ok(result) => {
let stdout = String::from_utf8_lossy(&result.stdout);
let stderr = String::from_utf8_lossy(&result.stderr);
let combined = format!("{}{}", stdout, stderr);
if combined.contains("unused") || combined.contains("Unused") {
let count = combined
.lines()
.filter(|l| l.contains("unused") || l.contains("Unused"))
.count();
GateResult::Fail(format!("{} unused dependencies", count))
} else if result.status.success() {
GateResult::Pass("No unused dependencies".to_string())
} else {
GateResult::Skip("cargo-udeps failed".to_string())
}
}
Err(_) => GateResult::Skip("cargo-udeps not installed".to_string()),
}
}
fn check_cognitive_complexity(&self) -> GateResult {
let output = Command::new("cargo")
.args([
"clippy",
"-p",
"cbtop",
"--",
"-W",
"clippy::cognitive_complexity",
"--cap-lints",
"warn",
])
.current_dir(&self.project_root)
.output();
match output {
Ok(result) => {
let stderr = String::from_utf8_lossy(&result.stderr);
let complexity_warnings = stderr
.lines()
.filter(|l| l.contains("cognitive_complexity"))
.count();
if complexity_warnings == 0 {
GateResult::Pass("All functions under complexity limit".to_string())
} else {
GateResult::Fail(format!(
"{} functions over complexity limit",
complexity_warnings
))
}
}
Err(e) => GateResult::Fail(format!("Clippy failed: {}", e)),
}
}
fn check_binary_size(&self) -> GateResult {
let build_result = Command::new("cargo")
.args(["build", "--release", "-p", "cbtop"])
.current_dir(&self.project_root)
.output();
if build_result.is_err() {
return GateResult::Skip("Failed to build release binary".to_string());
}
let binary_path = self.project_root.join("target/release/cbtop");
if !binary_path.exists() {
return GateResult::Skip("Binary not found".to_string());
}
match std::fs::metadata(&binary_path) {
Ok(meta) => {
let size_mb = meta.len() as f64 / (1024.0 * 1024.0);
let threshold = 8.0;
if size_mb < threshold {
GateResult::Pass(format!("{:.2}MB (< {}MB)", size_mb, threshold))
} else {
GateResult::Fail(format!("{:.2}MB (> {}MB limit)", size_mb, threshold))
}
}
Err(e) => GateResult::Fail(format!("Failed to get binary size: {}", e)),
}
}
fn check_startup_time(&self) -> GateResult {
let binary_path = self.project_root.join("target/release/cbtop");
if !binary_path.exists() {
return GateResult::Skip("Binary not found".to_string());
}
let start = Instant::now();
let result = Command::new(&binary_path).args(["--help"]).output();
let elapsed = start.elapsed();
match result {
Ok(output) => {
if output.status.success() {
let elapsed_ms = elapsed.as_millis();
let threshold = 20;
if elapsed_ms < threshold {
GateResult::Pass(format!("{}ms (< {}ms)", elapsed_ms, threshold))
} else {
GateResult::Fail(format!("{}ms (> {}ms limit)", elapsed_ms, threshold))
}
} else {
GateResult::Fail("Binary failed to start".to_string())
}
}
Err(e) => GateResult::Fail(format!("Failed to run binary: {}", e)),
}
}
pub fn check_i18n(&self) -> GateResult {
let test_inputs = [
"ๆฅๆฌ่ชใในใ", "ไธญๆๆต่ฏ", "ํ๊ตญ์ด ํ
์คํธ", "ัะตัั ะฝะฐ ััััะบะพะผ", "ฮดฮฟฮบฮนฮผฮฎ ฮตฮปฮปฮทฮฝฮนฮบฮฌ", "๐ฅ๐ป๐", "\u{FEFF}BOM test", "\0null\0byte", ];
for input in test_inputs {
let _len = input.len();
let _chars = input.chars().count();
let _bytes = input.as_bytes();
let _formatted = format!("Input: {}", input);
}
GateResult::Pass("Non-ASCII handling verified".to_string())
}
fn check_mutation_resilience(&self) -> GateResult {
if self.skip_slow {
return GateResult::Skip("Skipped (slow check)".to_string());
}
let output = Command::new("cargo")
.args(["mutants", "--package", "cbtop", "--timeout", "60"])
.current_dir(&self.project_root)
.output();
match output {
Ok(result) => {
let stdout = String::from_utf8_lossy(&result.stdout);
let stderr = String::from_utf8_lossy(&result.stderr);
let combined = format!("{}{}", stdout, stderr);
if let Some(score_line) = combined.lines().find(|l| l.contains("mutation score")) {
let parts: Vec<&str> = score_line.split_whitespace().collect();
if let Some(pct) = parts.iter().find(|p| p.ends_with('%')) {
let score: f64 = pct.trim_end_matches('%').parse().unwrap_or(0.0);
if score >= 90.0 {
GateResult::Pass(format!("{}% mutation score", score))
} else {
GateResult::Fail(format!("{}% < 90% threshold", score))
}
} else {
GateResult::Pass("Mutation testing completed".to_string())
}
} else if result.status.success() {
GateResult::Pass("Mutation testing completed".to_string())
} else {
GateResult::Fail("Mutation testing failed".to_string())
}
}
Err(_) => GateResult::Skip("cargo-mutants not installed".to_string()),
}
}
fn check_miri(&self) -> GateResult {
if self.skip_slow {
return GateResult::Skip("Skipped (slow check)".to_string());
}
let output = Command::new("cargo")
.args([
"+nightly",
"miri",
"test",
"-p",
"cbtop",
"--lib",
"--",
"--test-threads=1",
])
.current_dir(&self.project_root)
.env("MIRIFLAGS", "-Zmiri-disable-isolation")
.output();
match output {
Ok(result) => {
let stderr = String::from_utf8_lossy(&result.stderr);
if stderr.contains("Undefined Behavior") {
GateResult::Fail("Undefined behavior detected".to_string())
} else if result.status.success() {
GateResult::Pass("No undefined behavior detected".to_string())
} else {
if stderr.contains("unsupported") {
GateResult::Skip("Miri: unsupported operations".to_string())
} else {
GateResult::Pass("Miri completed with warnings".to_string())
}
}
}
Err(_) => GateResult::Skip("Miri not installed".to_string()),
}
}
}
pub fn quick_validate(project_root: impl AsRef<Path>) -> IronmanScorecard {
IronmanValidator::new(project_root)
.skip_slow(true)
.validate()
}
pub fn full_validate(project_root: impl AsRef<Path>) -> IronmanScorecard {
IronmanValidator::new(project_root)
.skip_slow(false)
.validate()
}
#[cfg(test)]
mod tests;