use clap::Parser;
use keyhog::args::ScanArgs;
use keyhog::testing::{CliTestApi as _, API};
use keyhog_core::{MatchLocation, RawMatch, Severity};
use keyhog_profile::{Stage, StageMeasurement};
use std::path::Path;
use std::sync::Arc;
fn measure(f: impl FnOnce()) -> Vec<StageMeasurement> {
keyhog_profile::reset();
let runtime = keyhog_profile::Runtime::new();
let guard = runtime.enter();
f();
let measurements = keyhog_profile::take_stage_measurements();
drop(guard);
keyhog_profile::reset();
measurements
}
fn stage_calls(measurements: &[StageMeasurement], stage: Stage) -> u64 {
measurements
.iter()
.filter(|measurement| measurement.stage == stage)
.map(|measurement| measurement.calls)
.sum()
}
fn raw_match(detector_id: &str) -> RawMatch {
RawMatch {
detector_id: Arc::from(detector_id),
detector_name: Arc::from("Test Detector"),
service: Arc::from("test"),
severity: Severity::High,
credential: keyhog_core::SensitiveString::from("secret"),
credential_hash: [0u8; 32].into(),
companions: std::collections::HashMap::new(),
location: MatchLocation {
source: Arc::from("filesystem"),
file_path: Some(Arc::from("demo.txt")),
line: Some(1),
offset: 0,
commit: None,
author: None,
date: None,
},
entropy: None,
confidence: None,
evidence: keyhog_core::EvidenceVerdict::review_unattributed(),
}
}
fn write_detector_policy_config(dir: &Path) {
std::fs::write(
dir.join(".keyhog.toml"),
"[detector.demo-detector]\nenabled = false\n",
)
.expect("write config fixture");
}
#[test]
fn args_try_parse_from_records_one_preprocess_span() {
let measurements = measure(|| {
keyhog::args::try_parse_from(["keyhog", "scan", ".", "--no-config"])
.expect("parse must succeed");
});
assert_eq!(
measurements.len(),
1,
"parse must record exactly one stage, got {measurements:?}"
);
assert_eq!(stage_calls(&measurements, Stage::Preprocess), 1);
}
#[test]
fn config_merge_records_preprocess_and_detector_policy_backend_select() {
let dir = tempfile::TempDir::new().expect("tempdir");
write_detector_policy_config(dir.path());
let scan_path = dir.path().to_string_lossy().to_string();
let mut args =
ScanArgs::try_parse_from(["scan", "--path", scan_path.as_str()]).expect("parse scan args");
let measurements = measure(|| {
API.apply_config_file_quiet(&mut args);
});
assert_eq!(
measurements.len(),
2,
"config merge must record Preprocess and BackendSelect only, got {measurements:?}"
);
assert_eq!(stage_calls(&measurements, Stage::Preprocess), 5);
assert_eq!(stage_calls(&measurements, Stage::BackendSelect), 1);
}
#[test]
fn allowlist_rule_suppressor_load_records_one_preprocess_span() {
let dir = tempfile::TempDir::new().expect("tempdir");
std::fs::write(
dir.path().join(".keyhogignore.toml"),
"[[suppress]]\ndetector = \"demo\"\n",
)
.expect("write suppressor fixture");
let measurements = measure(|| {
keyhog::profiling_test_seams::load_rule_suppressor(Some(dir.path()))
.expect("suppressor must load");
});
assert_eq!(
measurements.len(),
1,
"suppressor load must record exactly one stage, got {measurements:?}"
);
assert_eq!(stage_calls(&measurements, Stage::Preprocess), 1);
}
#[test]
fn allowlist_eval_records_one_suppression_span_and_filters() {
let suppressor: keyhog_core::RuleSuppressor = "[[suppress]]\ndetector = \"demo\"\n"
.parse()
.expect("suppressor fixture parses");
let mut kept_len = usize::MAX;
let measurements = measure(|| {
let kept = keyhog::profiling_test_seams::filter_rule_suppressed(
&suppressor,
vec![raw_match("demo")],
);
kept_len = kept.len();
});
assert_eq!(kept_len, 0, "matching rule must suppress the finding");
assert_eq!(
measurements.len(),
1,
"suppression eval must record exactly one stage, got {measurements:?}"
);
assert_eq!(stage_calls(&measurements, Stage::Suppression), 1);
}
#[test]
fn detector_corpus_load_records_one_backend_select_span() {
let mut loaded = 0usize;
let measurements = measure(|| {
loaded = keyhog::profiling_test_seams::load_detector_corpus(Path::new("detectors"))
.expect("embedded corpus must load")
.len();
});
assert!(loaded > 0, "embedded corpus must be non-empty");
assert_eq!(
measurements.len(),
1,
"detector load must record exactly one stage, got {measurements:?}"
);
assert_eq!(stage_calls(&measurements, Stage::BackendSelect), 1);
}
#[test]
fn doctor_host_probe_records_one_preprocess_span() {
let measurements = measure(|| {
let caps = keyhog::profiling_test_seams::doctor_host_probe();
assert!(caps.logical_cores > 0, "probe must report host cores");
});
assert_eq!(
measurements.len(),
1,
"doctor probe must record exactly one stage, got {measurements:?}"
);
assert_eq!(stage_calls(&measurements, Stage::Preprocess), 1);
}
#[test]
fn paths_are_silent_without_runtime() {
keyhog_profile::reset();
keyhog::args::try_parse_from(["keyhog", "scan", ".", "--no-config"])
.expect("parse must succeed");
let config_dir = tempfile::TempDir::new().expect("tempdir");
write_detector_policy_config(config_dir.path());
let scan_path = config_dir.path().to_string_lossy().to_string();
let mut args =
ScanArgs::try_parse_from(["scan", "--path", scan_path.as_str()]).expect("parse scan args");
API.apply_config_file_quiet(&mut args);
let suppressor_dir = tempfile::TempDir::new().expect("tempdir");
std::fs::write(
suppressor_dir.path().join(".keyhogignore.toml"),
"[[suppress]]\ndetector = \"demo\"\n",
)
.expect("write suppressor fixture");
let suppressor =
keyhog::profiling_test_seams::load_rule_suppressor(Some(suppressor_dir.path()))
.expect("suppressor must load");
let kept =
keyhog::profiling_test_seams::filter_rule_suppressed(&suppressor, vec![raw_match("demo")]);
assert!(kept.is_empty());
keyhog::profiling_test_seams::load_detector_corpus(Path::new("detectors"))
.expect("embedded corpus must load");
keyhog::profiling_test_seams::doctor_host_probe();
let measurements = keyhog_profile::take_stage_measurements();
keyhog_profile::reset();
assert_eq!(stage_calls(&measurements, Stage::Preprocess), 0);
assert_eq!(stage_calls(&measurements, Stage::BackendSelect), 0);
assert_eq!(stage_calls(&measurements, Stage::Suppression), 0);
assert_eq!(stage_calls(&measurements, Stage::Reporting), 0);
assert_eq!(stage_calls(&measurements, Stage::SourceAcquire), 0);
assert_eq!(stage_calls(&measurements, Stage::IncrementalLookup), 0);
}