use keyhog_core::Allowlist;
use std::time::Instant;
fn build_allowlist(count: usize) -> Allowlist {
let base = [
"**/*.md",
"**/*.log",
"node_modules/**",
"vendor/**/*.json",
"target/**",
"**/test/fixtures/**",
"docs/**/*.png",
"**/.env.example",
"build/**",
"dist/**/*.js",
"**/*.lock",
"coverage/**",
"**/snapshots/*.snap",
"tmp/**",
"**/*.min.js",
];
let mut content = String::new();
let mut dir = 0usize;
'outer: loop {
for b in &base {
if content.lines().count() >= count {
break 'outer;
}
content.push_str("path:dir");
content.push_str(&dir.to_string());
content.push('/');
content.push_str(b);
content.push('\n');
}
dir += 1;
}
let al = keyhog_core::testing::CoreTestApi::allowlist_parse(
&keyhog_core::testing::TestApi,
&content,
);
assert_eq!(
al.ignored_paths.len(),
count,
"fixture must produce exactly {count} path rules (got {})",
al.ignored_paths.len()
);
al
}
fn build_findings() -> Vec<String> {
(0..2000)
.map(|i| format!("src/app/m{}/handler_{}.rs", i % 50, i))
.collect()
}
fn best_of_k(al: &Allowlist, findings: &[String], k: usize) -> u128 {
let mut best = u128::MAX;
for _ in 0..k {
let start = Instant::now();
let mut hits = 0u64;
for f in findings {
if al.is_path_ignored(f) {
hits += 1;
}
}
let elapsed = start.elapsed().as_nanos();
std::hint::black_box(hits);
if elapsed < best {
best = elapsed;
}
}
best
}
#[test]
fn suppression_path_walk_must_scale_sublinearly_in_rule_count() {
const N: usize = 200;
const FACTOR: usize = 4; const K: usize = 5;
let findings = build_findings();
let small = build_allowlist(N);
let large = build_allowlist(N * FACTOR);
let _ = best_of_k(&small, &findings, 2);
let _ = best_of_k(&large, &findings, 2);
let t_small = best_of_k(&small, &findings, K);
let t_large = best_of_k(&large, &findings, K);
assert!(
t_small > 0,
"small workload measured 0 ns, timer resolution too coarse to form a ratio"
);
let ratio = t_large as f64 / t_small as f64;
const MAX_RATIO: f64 = 3.0;
assert!(
ratio < MAX_RATIO,
"PERF REGRESSION (suppression rule walk is O(findings·rules)):\n\
4x-rules / 1x-rules wall-time ratio = {ratio:.3} (>= {MAX_RATIO} trips)\n\
rules={N} ({K}x best) = {:.3} ms\n\
rules={} ({K}x best) = {:.3} ms\n\
findings = {}\n\
current behavior: is_path_ignored (allowlist.rs:267-272) does\n\
`ignored_paths.iter().any(|p| glob_match_normalized(p, ..))` with NO\n\
prefix/set/automaton, and glob_match_normalized (allowlist.rs:287-288)\n\
re-runs normalize_path(pattern)+split_segments(pattern) PER finding.\n\
target: precompile pattern segments once in Allowlist::parse and index\n\
globs by literal prefix/set so per-finding cost is sub-linear in rule\n\
count -> ratio < 2.0 (ideal ~1.0-1.5).",
t_small as f64 / 1e6,
N * FACTOR,
t_large as f64 / 1e6,
findings.len(),
);
}
#[test]
fn suppression_recall_guard_exact_decisions() {
let al = keyhog_core::testing::CoreTestApi::allowlist_parse(
&keyhog_core::testing::TestApi,
"path:**/*.md\n\
path:node_modules/**\n\
path:vendor/**/*.json\n\
path:**/.env.example\n\
path:src/secrets/*.rs\n",
);
for p in [
"docs/README.md",
"a/b/c/CHANGELOG.md",
"node_modules/left-pad/index.js",
"vendor/aws/creds.json", "vendor/creds.json", "app/.env.example",
".env.example",
"src/secrets/keys.rs",
] {
assert!(
al.is_path_ignored(p),
"recall guard: '{p}' must stay suppressed under any optimized index"
);
}
for p in [
"src/main.rs",
"README.mdx",
"node_modules_real/index.js",
"vendor/aws/creds.yaml", "deep/vendor/x/y/z.json", "src/secrets/sub/keys.rs", "app/.env",
] {
assert!(
!al.is_path_ignored(p),
"recall guard: '{p}' must NOT be suppressed under any optimized index"
);
}
}