use std::fs;
use tempfile::{Builder, TempDir};
use super::{
SuppressionDirective, apply_repository_config, is_generated, next_code_line, parse_rule_ids,
parse_suppression_directives, scan_repository,
};
use crate::RepoConfig;
use crate::analysis::{AnalysisConfig, Language, ParsedFile};
use crate::heuristics::{evaluate_file, evaluate_repo};
use crate::index::build_repository_index;
use crate::model::ScanOptions;
use crate::model::{Finding, Severity};
macro_rules! scan_fixture {
($path:literal) => {
include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/fixtures/",
$path
))
};
}
fn sample_finding(rule_id: &str, severity: Severity) -> Finding {
Finding {
rule_id: rule_id.to_string(),
severity,
path: std::path::PathBuf::from("src/lib.rs"),
function_name: Some("demo".to_string()),
start_line: 1,
end_line: 1,
message: "demo".to_string(),
evidence: Vec::new(),
}
}
fn temp_dir(name: &str) -> TempDir {
Builder::new()
.prefix(&format!("deslop-scan-{name}-"))
.tempdir()
.expect("scan temp dir should be created")
}
#[test]
fn test_is_generated() {
let generated = "// Code generated by mockery. DO NOT EDIT.\npackage sample\n";
assert!(is_generated(generated));
}
#[test]
fn parses_rule_ids_from_inline_directive() {
assert_eq!(
parse_rule_ids("unwrap_in_non_test_code, panic_macro_leftover */"),
vec![
"unwrap_in_non_test_code".to_string(),
"panic_macro_leftover".to_string()
]
);
}
#[test]
fn finds_next_code_line_after_directive_comments() {
let lines = vec![
"// deslop-ignore:unwrap_in_non_test_code",
"",
"// note",
"value.unwrap();",
];
assert_eq!(next_code_line(&lines, 1), Some(4));
}
#[test]
fn parses_same_line_and_next_line_suppressions() {
let source = scan_fixture!("rust/scan/suppressions_same_line.txt");
assert_eq!(
parse_suppression_directives(source),
vec![
SuppressionDirective {
rule_id: "unwrap_in_non_test_code".to_string(),
line: 2,
next_code_line: Some(4),
},
SuppressionDirective {
rule_id: "panic_macro_leftover".to_string(),
line: 3,
next_code_line: Some(4),
}
]
);
}
#[test]
fn applies_disabled_rules_and_severity_overrides() {
let mut findings = vec![
sample_finding("panic_macro_leftover", Severity::Warning),
sample_finding("unwrap_in_non_test_code", Severity::Warning),
];
let mut repo_config = RepoConfig {
disabled_rules: vec!["panic_macro_leftover".to_string()],
suppressed_paths: Vec::new(),
severity_overrides: std::collections::BTreeMap::new(),
..RepoConfig::default()
};
repo_config
.severity_overrides
.insert("unwrap_in_non_test_code".to_string(), Severity::Error);
apply_repository_config(&mut findings, &repo_config, std::path::Path::new("."));
assert_eq!(findings.len(), 1);
assert_eq!(findings[0].rule_id, "unwrap_in_non_test_code");
assert_eq!(findings[0].severity, Severity::Error);
}
#[test]
fn disables_async_rollout_rules_when_flag_is_off() {
let mut findings = vec![
sample_finding("rust_async_std_mutex_await", Severity::Error),
sample_finding("rust_lock_across_await", Severity::Warning),
sample_finding("unwrap_in_non_test_code", Severity::Warning),
];
let repo_config = RepoConfig {
rust_async_experimental: false,
..RepoConfig::default()
};
apply_repository_config(&mut findings, &repo_config, std::path::Path::new("."));
assert_eq!(findings.len(), 1);
assert_eq!(findings[0].rule_id, "unwrap_in_non_test_code");
}
#[test]
fn suppresses_findings_under_configured_paths() {
let root = temp_dir("suppressed-paths");
let mut findings = vec![
Finding {
path: root.path().join("tests/fixtures/rust/async/positive.rs"),
..sample_finding("rust_blocking_io_in_async", Severity::Warning)
},
Finding {
path: root.path().join("src/lib.rs"),
..sample_finding("unwrap_in_non_test_code", Severity::Warning)
},
];
let repo_config = RepoConfig {
suppressed_paths: vec![std::path::PathBuf::from("tests/fixtures")],
..RepoConfig::default()
};
apply_repository_config(&mut findings, &repo_config, root.path());
assert_eq!(findings.len(), 1);
assert_eq!(findings[0].path, root.path().join("src/lib.rs"));
}
#[test]
fn scan_uses_canonical_root_for_index_resolution() {
let root = temp_dir("canonical-root");
let src = root.path().join("src");
let config = src.join("config");
fs::create_dir_all(&config).expect("config dir should be created");
fs::write(
src.join("lib.rs"),
scan_fixture!("rust/scan/canonical_root_lib.txt"),
)
.expect("lib fixture should be written");
fs::write(
config.join("render.rs"),
scan_fixture!("rust/scan/canonical_root_render.txt"),
)
.expect("render fixture should be written");
let report = scan_repository(&ScanOptions {
root: root.path().join("."),
respect_ignore: true,
})
.expect("scan should succeed");
assert!(!report.findings.iter().any(|finding| {
finding.rule_id == "hallucinated_import_call"
&& finding.function_name.as_deref() == Some("run")
}));
}
#[test]
fn exact_duplicate_findings_are_collapsed_by_scan_sorting() {
let finding = sample_finding("unwrap_in_non_test_code", Severity::Warning);
let mut findings = vec![finding.clone(), finding];
findings.sort_by(|left, right| {
left.path
.cmp(&right.path)
.then(left.start_line.cmp(&right.start_line))
.then(left.rule_id.cmp(&right.rule_id))
});
findings.dedup_by(|a, b| {
a.path == b.path && a.start_line == b.start_line && a.rule_id == b.rule_id
});
assert_eq!(findings.len(), 1);
}
#[test]
fn scan_dispatch_uses_heuristics_instead_of_backend_evaluators() {
let source = scan_fixture!("rust/backend/grouped_imported_function.txt");
let files = source
.split("=== file:")
.filter_map(|chunk| {
let chunk = chunk.trim();
if chunk.is_empty() {
return None;
}
let (header, body) = chunk.split_once('\n')?;
let path = header.trim().trim_end_matches("===").trim();
Some((path, body.trim_start_matches('\n')))
})
.map(|(path, body)| {
crate::analysis::parse_source_file(std::path::Path::new(path), body)
.expect("fixture source should parse")
})
.collect::<Vec<ParsedFile>>();
let index = build_repository_index(std::path::Path::new("/repo"), &files);
let analysis_config = AnalysisConfig::default();
let file = files
.iter()
.find(|file| file.language == Language::Rust)
.expect("fixture should include a rust file");
let file_findings = evaluate_file(file, &index, &analysis_config);
let repo_findings = evaluate_repo(Language::Rust, &[file], &index, &analysis_config);
assert!(
!file_findings.is_empty() || repo_findings.is_empty(),
"the scan layer should dispatch through heuristics for file evaluation"
);
}