#![cfg(test)]
use crate::adapters::analyzers::architecture::call_parity_rule;
use crate::adapters::analyzers::architecture::compiled::compile_architecture;
use crate::config::Config;
use crate::ports::{AnalysisContext, ParsedFile};
use std::fs;
use std::path::{Path, PathBuf};
use std::time::Instant;
const BENCH_SOFT_LIMIT: std::time::Duration = std::time::Duration::from_secs(3);
fn rustqual_src_root() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).join("src")
}
fn load_rust_files(root: &Path) -> Vec<ParsedFile> {
let mut out = Vec::new();
collect_rs_paths(root, &mut out);
out
}
fn collect_rs_paths(current: &Path, out: &mut Vec<ParsedFile>) {
let dir = fs::read_dir(current)
.unwrap_or_else(|e| panic!("failed to read dir {}: {e}", current.display()));
for entry in dir {
let entry =
entry.unwrap_or_else(|e| panic!("failed to read entry in {}: {e}", current.display()));
let path = entry.path();
if path.is_dir() {
collect_rs_paths(&path, out);
} else if path.extension().and_then(|s| s.to_str()) == Some("rs") {
out.push(parse_one(&path));
}
}
}
fn parse_one(abs: &Path) -> ParsedFile {
let content =
fs::read_to_string(abs).unwrap_or_else(|e| panic!("failed to read {}: {e}", abs.display()));
let ast: syn::File = syn::parse_str(&content)
.unwrap_or_else(|e| panic!("failed to parse {}: {e}", abs.display()));
let manifest = Path::new(env!("CARGO_MANIFEST_DIR"));
let rel = abs
.strip_prefix(manifest)
.unwrap_or_else(|e| panic!("{} is not under {}: {e}", abs.display(), manifest.display()))
.to_string_lossy()
.replace('\\', "/");
ParsedFile {
path: rel,
content,
ast,
}
}
fn bench_config() -> Config {
let toml_str = r#"
[architecture]
enabled = true
[architecture.layers]
order = ["domain", "ports", "adapters", "app", "cli"]
[architecture.layers.domain]
paths = ["src/domain/**"]
[architecture.layers.ports]
paths = ["src/ports/**"]
[architecture.layers.adapters]
paths = ["src/adapters/**"]
[architecture.layers.app]
paths = ["src/app/**"]
[architecture.layers.cli]
paths = ["src/cli/**"]
[architecture.call_parity]
adapters = ["cli", "adapters"]
target = "app"
call_depth = 3
"#;
let mut config: Config = toml::from_str(toml_str).expect("parse bench config");
config.compile();
config
}
#[test]
#[ignore = "performance regression guard — run via `cargo test -- --ignored` before release"]
fn benchmark_call_parity_on_self_analysis() {
let files = load_rust_files(&rustqual_src_root());
assert!(
files.len() > 50,
"self-analysis fixture looks empty — found {} files",
files.len()
);
let config = bench_config();
let ctx = AnalysisContext {
files: &files,
config: &config,
};
let compiled = compile_architecture(&config.architecture).expect("compile");
let start = Instant::now();
let _findings = call_parity_rule::collect_findings(&ctx, &compiled);
let elapsed = start.elapsed();
assert!(
elapsed < BENCH_SOFT_LIMIT,
"call_parity pass regressed: {elapsed:?} > {BENCH_SOFT_LIMIT:?} — \
likely an O(n²) schleicher in the BFS / reverse-graph code"
);
eprintln!(
"call_parity self-analysis: {} files, {elapsed:?}",
files.len()
);
}