use std::path::PathBuf;
use std::process::Command;
fn sqc_bin() -> PathBuf {
PathBuf::from(env!("CARGO_BIN_EXE_sqc"))
}
fn fixtures() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/cli")
}
fn manifest_msc04() -> PathBuf {
fixtures().join("manifest_msc04.toml")
}
fn manifest_dcl31() -> PathBuf {
fixtures().join("manifest_dcl31.toml")
}
fn run_sqc(args: &[&str]) -> (i32, String, String) {
let output = Command::new(sqc_bin())
.args(args)
.output()
.expect("failed to execute sqc");
let code = output.status.code().unwrap_or(-1);
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
(code, stdout, stderr)
}
#[test]
fn export_json_structure() {
let dir = tempfile::tempdir().unwrap();
let out = dir.path().join("out.json");
let (code, _, _) = run_sqc(&[
fixtures().join("violation.c").to_str().unwrap(),
"-m",
manifest_msc04().to_str().unwrap(),
"-e",
out.to_str().unwrap(),
]);
assert_eq!(code, 0);
let content = std::fs::read_to_string(&out).unwrap();
let violations: Vec<serde_json::Value> = serde_json::from_str(&content).unwrap();
assert_eq!(violations.len(), 1);
let v = &violations[0];
assert_eq!(v["rule_id"], "MSC04-C");
assert_eq!(v["line"], 1);
assert_eq!(v["severity"], "Medium");
assert!(v["message"].as_str().unwrap().contains("infinite"));
}
#[test]
fn export_json_empty_for_clean_file() {
let dir = tempfile::tempdir().unwrap();
let out = dir.path().join("out.json");
let (code, _, _) = run_sqc(&[
fixtures().join("clean.c").to_str().unwrap(),
"-m",
manifest_msc04().to_str().unwrap(),
"-e",
out.to_str().unwrap(),
]);
assert_eq!(code, 0);
let content = std::fs::read_to_string(&out).unwrap();
let violations: Vec<serde_json::Value> = serde_json::from_str(&content).unwrap();
assert!(violations.is_empty());
}
#[test]
fn export_csv_has_header_and_row() {
let dir = tempfile::tempdir().unwrap();
let out = dir.path().join("out.csv");
let (code, _, _) = run_sqc(&[
fixtures().join("violation.c").to_str().unwrap(),
"-m",
manifest_msc04().to_str().unwrap(),
"-e",
out.to_str().unwrap(),
]);
assert_eq!(code, 0);
let content = std::fs::read_to_string(&out).unwrap();
let lines: Vec<&str> = content.lines().collect();
assert!(lines.len() >= 2, "CSV should have header + at least 1 row");
assert!(lines[0].contains("Title"));
assert!(lines[1].contains("MSC04-C"));
}
#[test]
fn export_sarif_structure() {
let dir = tempfile::tempdir().unwrap();
let out = dir.path().join("out.sarif");
let (code, _, _) = run_sqc(&[
fixtures().join("violation.c").to_str().unwrap(),
"-m",
manifest_msc04().to_str().unwrap(),
"-e",
out.to_str().unwrap(),
]);
assert_eq!(code, 0);
let content = std::fs::read_to_string(&out).unwrap();
let sarif: serde_json::Value = serde_json::from_str(&content).unwrap();
assert_eq!(sarif["version"], "2.1.0");
assert!(sarif["$schema"].as_str().unwrap().contains("sarif"));
let results = &sarif["runs"][0]["results"];
assert_eq!(results.as_array().unwrap().len(), 1);
assert_eq!(results[0]["ruleId"], "MSC04-C");
}
#[test]
fn exit_code_zero_no_violations() {
let (code, _, _) = run_sqc(&[
fixtures().join("clean.c").to_str().unwrap(),
"-m",
manifest_msc04().to_str().unwrap(),
]);
assert_eq!(code, 0);
}
#[test]
fn exit_code_zero_without_fail_flag() {
let (code, _, _) = run_sqc(&[
fixtures().join("violation.c").to_str().unwrap(),
"-m",
manifest_msc04().to_str().unwrap(),
]);
assert_eq!(code, 0);
}
#[test]
fn fail_on_violation_exits_one() {
let (code, _, _) = run_sqc(&[
fixtures().join("violation.c").to_str().unwrap(),
"-m",
manifest_msc04().to_str().unwrap(),
"--fail-on-violation",
]);
assert_eq!(code, 1);
}
#[test]
fn fail_on_violation_exits_zero_when_clean() {
let (code, _, _) = run_sqc(&[
fixtures().join("clean.c").to_str().unwrap(),
"-m",
manifest_msc04().to_str().unwrap(),
"--fail-on-violation",
]);
assert_eq!(code, 0);
}
#[test]
fn fail_on_severity_exits_one_when_met() {
let (code, _, _) = run_sqc(&[
fixtures().join("violation.c").to_str().unwrap(),
"-m",
manifest_msc04().to_str().unwrap(),
"--fail-on-severity",
"Medium",
]);
assert_eq!(code, 1);
}
#[test]
fn fail_on_severity_exits_zero_when_below() {
let (code, _, _) = run_sqc(&[
fixtures().join("violation.c").to_str().unwrap(),
"-m",
manifest_msc04().to_str().unwrap(),
"--fail-on-severity",
"High",
]);
assert_eq!(code, 0);
}
#[test]
fn min_severity_filters_below_threshold() {
let dir = tempfile::tempdir().unwrap();
let out = dir.path().join("out.json");
let (code, _, _) = run_sqc(&[
fixtures().join("violation.c").to_str().unwrap(),
"-m",
manifest_msc04().to_str().unwrap(),
"--min-severity",
"High",
"-e",
out.to_str().unwrap(),
]);
assert_eq!(code, 0);
let content = std::fs::read_to_string(&out).unwrap();
let violations: Vec<serde_json::Value> = serde_json::from_str(&content).unwrap();
assert!(
violations.is_empty(),
"Medium violation should be filtered by High threshold"
);
}
#[test]
fn min_severity_passes_at_threshold() {
let dir = tempfile::tempdir().unwrap();
let out = dir.path().join("out.json");
let (code, _, _) = run_sqc(&[
fixtures().join("violation.c").to_str().unwrap(),
"-m",
manifest_msc04().to_str().unwrap(),
"--min-severity",
"Medium",
"-e",
out.to_str().unwrap(),
]);
assert_eq!(code, 0);
let content = std::fs::read_to_string(&out).unwrap();
let violations: Vec<serde_json::Value> = serde_json::from_str(&content).unwrap();
assert_eq!(violations.len(), 1);
}
#[test]
fn rules_filter_includes_matching_rule() {
let dir = tempfile::tempdir().unwrap();
let out = dir.path().join("out.json");
let (code, _, _) = run_sqc(&[
fixtures().join("violation.c").to_str().unwrap(),
"-m",
manifest_msc04().to_str().unwrap(),
"--rules",
"MSC04-C",
"-e",
out.to_str().unwrap(),
]);
assert_eq!(code, 0);
let content = std::fs::read_to_string(&out).unwrap();
let violations: Vec<serde_json::Value> = serde_json::from_str(&content).unwrap();
assert_eq!(violations.len(), 1);
}
#[test]
fn rules_filter_excludes_non_matching() {
let dir = tempfile::tempdir().unwrap();
let out = dir.path().join("out.json");
let (code, _, _) = run_sqc(&[
fixtures().join("violation.c").to_str().unwrap(),
"-m",
manifest_msc04().to_str().unwrap(),
"--rules",
"DCL31-C",
"-e",
out.to_str().unwrap(),
]);
assert_eq!(code, 0);
let content = std::fs::read_to_string(&out).unwrap();
let violations: Vec<serde_json::Value> = serde_json::from_str(&content).unwrap();
assert!(
violations.is_empty(),
"MSC04-C should be excluded by DCL31-C filter"
);
}
#[test]
fn prescan_save_load_round_trip() {
let dir = tempfile::tempdir().unwrap();
let cache = dir.path().join("prescan.bin");
let out1 = dir.path().join("save.json");
let out2 = dir.path().join("load.json");
let project_main = fixtures().join("project/main.c");
let helpers_dir = fixtures().join("project/helpers");
let (code, _, _) = run_sqc(&[
project_main.to_str().unwrap(),
"-m",
manifest_dcl31().to_str().unwrap(),
"-d",
helpers_dir.to_str().unwrap(),
"--save-prescan",
cache.to_str().unwrap(),
"-e",
out1.to_str().unwrap(),
]);
assert_eq!(code, 0);
assert!(cache.exists(), "prescan cache file should be created");
let (code, _, _) = run_sqc(&[
project_main.to_str().unwrap(),
"-m",
manifest_dcl31().to_str().unwrap(),
"--load-prescan",
cache.to_str().unwrap(),
"-e",
out2.to_str().unwrap(),
]);
assert_eq!(code, 0);
let save_violations: Vec<serde_json::Value> =
serde_json::from_str(&std::fs::read_to_string(&out1).unwrap()).unwrap();
let load_violations: Vec<serde_json::Value> =
serde_json::from_str(&std::fs::read_to_string(&out2).unwrap()).unwrap();
assert!(
save_violations.is_empty(),
"With -d, helper_compute should be known"
);
assert_eq!(
save_violations.len(),
load_violations.len(),
"Loaded prescan should produce same results as live prescan"
);
}
#[test]
fn inline_suppression_hides_violation() {
let dir = tempfile::tempdir().unwrap();
let out = dir.path().join("out.json");
let (code, stdout, _) = run_sqc(&[
fixtures().join("suppressed_inline.c").to_str().unwrap(),
"-m",
manifest_msc04().to_str().unwrap(),
"-e",
out.to_str().unwrap(),
]);
assert_eq!(code, 0);
assert!(
stdout.contains("1 suppressed"),
"Should report 1 suppressed violation"
);
let content = std::fs::read_to_string(&out).unwrap();
let violations: Vec<serde_json::Value> = serde_json::from_str(&content).unwrap();
assert!(
violations.is_empty(),
"Suppressed violation should not appear in JSON export"
);
}
#[test]
fn toml_suppression_hides_violation() {
let dir = tempfile::tempdir().unwrap();
let out = dir.path().join("out.json");
let (code, stdout, _) = run_sqc(&[
fixtures().join("violation.c").to_str().unwrap(),
"-m",
manifest_msc04().to_str().unwrap(),
"--suppress-file",
fixtures().join("suppress.toml").to_str().unwrap(),
"-e",
out.to_str().unwrap(),
]);
assert_eq!(code, 0);
assert!(
stdout.contains("1 suppressed"),
"Should report 1 suppressed violation"
);
let content = std::fs::read_to_string(&out).unwrap();
let violations: Vec<serde_json::Value> = serde_json::from_str(&content).unwrap();
assert!(
violations.is_empty(),
"TOML-suppressed violation should not appear in JSON export"
);
}
#[test]
fn fail_on_violation_ignores_suppressed() {
let (code, _, _) = run_sqc(&[
fixtures().join("suppressed_inline.c").to_str().unwrap(),
"-m",
manifest_msc04().to_str().unwrap(),
"--fail-on-violation",
]);
assert_eq!(
code, 0,
"Suppressed violations should not trigger --fail-on-violation"
);
}
#[test]
fn generate_suppression_outputs_hash() {
let (code, stdout, _) = run_sqc(&[
"--generate-suppression",
&format!(
"{}:1:MSC04-C",
fixtures().join("violation.c").to_str().unwrap()
),
"-m",
manifest_msc04().to_str().unwrap(),
]);
assert_eq!(code, 0);
assert!(stdout.contains("SQC-SUPPRESS: MSC04-C"));
assert!(stdout.contains("HASH:745a35718a0e2d31"));
assert!(stdout.contains("[[suppression]]"));
}
#[test]
fn without_d_flag_reports_undeclared_function() {
let dir = tempfile::tempdir().unwrap();
let out = dir.path().join("out.json");
let (code, _, _) = run_sqc(&[
fixtures().join("project/main.c").to_str().unwrap(),
"-m",
manifest_dcl31().to_str().unwrap(),
"-e",
out.to_str().unwrap(),
]);
assert_eq!(code, 0);
let content = std::fs::read_to_string(&out).unwrap();
let violations: Vec<serde_json::Value> = serde_json::from_str(&content).unwrap();
assert_eq!(
violations.len(),
1,
"Without -d, helper_compute should be flagged"
);
assert_eq!(violations[0]["rule_id"], "DCL31-C");
}
#[test]
fn with_d_flag_suppresses_cross_file_function() {
let dir = tempfile::tempdir().unwrap();
let out = dir.path().join("out.json");
let (code, _, _) = run_sqc(&[
fixtures().join("project/main.c").to_str().unwrap(),
"-m",
manifest_dcl31().to_str().unwrap(),
"-d",
fixtures().join("project/helpers").to_str().unwrap(),
"-e",
out.to_str().unwrap(),
]);
assert_eq!(code, 0);
let content = std::fs::read_to_string(&out).unwrap();
let violations: Vec<serde_json::Value> = serde_json::from_str(&content).unwrap();
assert!(
violations.is_empty(),
"With -d helpers/, helper_compute should be known"
);
}
fn manifest_exp34() -> PathBuf {
fixtures().join("manifest_exp34.toml")
}
#[test]
fn crossfile_global_null_deref_detected_with_d_flag() {
let dir = tempfile::tempdir().unwrap();
let out = dir.path().join("out.json");
let (code, _, _) = run_sqc(&[
fixtures().join("crossfile_null/sink.c").to_str().unwrap(),
"-m",
manifest_exp34().to_str().unwrap(),
"-d",
fixtures().join("crossfile_null").to_str().unwrap(),
"-e",
out.to_str().unwrap(),
]);
assert_eq!(code, 0);
let content = std::fs::read_to_string(&out).unwrap();
let violations: Vec<serde_json::Value> = serde_json::from_str(&content).unwrap();
assert!(
!violations.is_empty(),
"With -d, shared_buffer=NULL should be detected from source.c and flagged in sink.c"
);
assert_eq!(violations[0]["rule_id"], "EXP34-C");
}
#[test]
fn crossfile_global_null_guard_not_flagged() {
let dir = tempfile::tempdir().unwrap();
let out = dir.path().join("out.json");
let (code, _, _) = run_sqc(&[
fixtures()
.join("crossfile_null/sink_safe.c")
.to_str()
.unwrap(),
"-m",
manifest_exp34().to_str().unwrap(),
"-d",
fixtures().join("crossfile_null").to_str().unwrap(),
"-e",
out.to_str().unwrap(),
]);
assert_eq!(code, 0);
let content = std::fs::read_to_string(&out).unwrap();
let violations: Vec<serde_json::Value> = serde_json::from_str(&content).unwrap();
let exp34_violations: Vec<&serde_json::Value> = violations
.iter()
.filter(|v| v["rule_id"] == "EXP34-C")
.collect();
assert!(
exp34_violations.is_empty(),
"With null guard, shared_buffer dereference should not be flagged"
);
}
#[test]
fn crossfile_global_null_not_detected_without_d_flag() {
let dir = tempfile::tempdir().unwrap();
let out = dir.path().join("out.json");
let (code, _, _) = run_sqc(&[
fixtures().join("crossfile_null/sink.c").to_str().unwrap(),
"-m",
manifest_exp34().to_str().unwrap(),
"-e",
out.to_str().unwrap(),
]);
assert_eq!(code, 0);
let content = std::fs::read_to_string(&out).unwrap();
let violations: Vec<serde_json::Value> = serde_json::from_str(&content).unwrap();
let exp34_violations: Vec<&serde_json::Value> = violations
.iter()
.filter(|v| v["rule_id"] == "EXP34-C")
.collect();
assert!(
exp34_violations.is_empty(),
"Without -d, cross-file global null state is unknown — no EXP34-C violation expected"
);
}
#[test]
fn diff_mode_only_analyzes_modified_files() {
let dir = tempfile::tempdir().unwrap();
let repo_dir = dir.path();
Command::new("git")
.args(["init"])
.current_dir(repo_dir)
.output()
.unwrap();
Command::new("git")
.args(["config", "user.email", "test@test.com"])
.current_dir(repo_dir)
.output()
.unwrap();
Command::new("git")
.args(["config", "user.name", "Test"])
.current_dir(repo_dir)
.output()
.unwrap();
let clean = repo_dir.join("clean.c");
std::fs::write(&clean, "int add(int a, int b) { return a + b; }\n").unwrap();
Command::new("git")
.args(["add", "clean.c"])
.current_dir(repo_dir)
.output()
.unwrap();
Command::new("git")
.args(["commit", "-m", "initial"])
.current_dir(repo_dir)
.output()
.unwrap();
let violation = repo_dir.join("violation.c");
std::fs::write(&violation, "void infinite(void) {\n infinite();\n}\n").unwrap();
let manifest = repo_dir.join("manifest.toml");
std::fs::copy(manifest_msc04(), &manifest).unwrap();
let out = repo_dir.join("out.json");
let output = Command::new(sqc_bin())
.args([
repo_dir.to_str().unwrap(),
"-m",
manifest.to_str().unwrap(),
"--diff",
"-e",
out.to_str().unwrap(),
])
.current_dir(repo_dir)
.output()
.expect("failed to execute sqc");
let code = output.status.code().unwrap_or(-1);
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
assert_eq!(code, 0);
assert!(
stdout.contains("diff-only"),
"Should indicate diff-only mode"
);
let content = std::fs::read_to_string(&out).unwrap();
let violations: Vec<serde_json::Value> = serde_json::from_str(&content).unwrap();
assert_eq!(violations.len(), 1);
assert_eq!(violations[0]["rule_id"], "MSC04-C");
}
#[test]
fn sarif_includes_suppressed_violations() {
let dir = tempfile::tempdir().unwrap();
let out = dir.path().join("out.sarif");
let (code, _, _) = run_sqc(&[
fixtures().join("suppressed_inline.c").to_str().unwrap(),
"-m",
manifest_msc04().to_str().unwrap(),
"-e",
out.to_str().unwrap(),
]);
assert_eq!(code, 0);
let content = std::fs::read_to_string(&out).unwrap();
let sarif: serde_json::Value = serde_json::from_str(&content).unwrap();
let results = sarif["runs"][0]["results"].as_array().unwrap();
let suppressed: Vec<_> = results
.iter()
.filter(|r| r.get("suppressions").is_some())
.collect();
assert!(
!suppressed.is_empty(),
"SARIF should include suppressed violations with suppressions array"
);
}
#[test]
fn crossfile_callsite_null_detected_with_d_flag() {
let dir = tempfile::tempdir().unwrap();
let out = dir.path().join("out.json");
let fixture_dir = fixtures().join("crossfile_callsite_null");
let (code, _, _) = run_sqc(&[
fixture_dir.join("callee.c").to_str().unwrap(),
"-m",
manifest_exp34().to_str().unwrap(),
"-d",
fixture_dir.to_str().unwrap(),
"-e",
out.to_str().unwrap(),
]);
assert_eq!(code, 0);
let content = std::fs::read_to_string(&out).unwrap();
let violations: Vec<serde_json::Value> = serde_json::from_str(&content).unwrap();
let exp34: Vec<_> = violations
.iter()
.filter(|v| v["rule_id"] == "EXP34-C")
.collect();
assert!(
!exp34.is_empty(),
"With -d, callsite NULL propagation should cause EXP34-C to flag dereference in callee.c"
);
}
#[test]
fn crossfile_callsite_null_not_detected_without_d_flag() {
let dir = tempfile::tempdir().unwrap();
let out = dir.path().join("out.json");
let fixture_dir = fixtures().join("crossfile_callsite_null");
let (code, _, _) = run_sqc(&[
fixture_dir.join("callee.c").to_str().unwrap(),
"-m",
manifest_exp34().to_str().unwrap(),
"-e",
out.to_str().unwrap(),
]);
assert_eq!(code, 0);
let content = std::fs::read_to_string(&out).unwrap();
let violations: Vec<serde_json::Value> = serde_json::from_str(&content).unwrap();
let exp34: Vec<_> = violations
.iter()
.filter(|v| v["rule_id"] == "EXP34-C")
.collect();
assert!(
exp34.is_empty(),
"Without -d, callee.c has no NULL context — no EXP34-C expected"
);
}
#[test]
fn crossfile_callsite_safe_not_flagged() {
let dir = tempfile::tempdir().unwrap();
let safe_dir = dir.path().join("safe_only");
std::fs::create_dir_all(&safe_dir).unwrap();
let fixture_dir = fixtures().join("crossfile_callsite_null");
std::fs::copy(fixture_dir.join("callee.c"), safe_dir.join("callee.c")).unwrap();
std::fs::copy(
fixture_dir.join("caller_safe.c"),
safe_dir.join("caller_safe.c"),
)
.unwrap();
let out = dir.path().join("out.json");
let (code, _, _) = run_sqc(&[
safe_dir.join("callee.c").to_str().unwrap(),
"-m",
manifest_exp34().to_str().unwrap(),
"-d",
safe_dir.to_str().unwrap(),
"-e",
out.to_str().unwrap(),
]);
assert_eq!(code, 0);
let content = std::fs::read_to_string(&out).unwrap();
let violations: Vec<serde_json::Value> = serde_json::from_str(&content).unwrap();
let exp34: Vec<_> = violations
.iter()
.filter(|v| v["rule_id"] == "EXP34-C")
.collect();
assert!(
exp34.is_empty(),
"With only safe callers (non-NULL args), callee.c should not be flagged"
);
}
#[test]
fn crossfile_nullable_return_detected_with_d_flag() {
let dir = tempfile::tempdir().unwrap();
let out = dir.path().join("out.json");
let fixture_dir = fixtures().join("crossfile_callsite_null");
let (code, _, _) = run_sqc(&[
fixture_dir.join("nullable_user_bad.c").to_str().unwrap(),
"-m",
manifest_exp34().to_str().unwrap(),
"-d",
fixture_dir.to_str().unwrap(),
"-e",
out.to_str().unwrap(),
]);
assert_eq!(code, 0);
let content = std::fs::read_to_string(&out).unwrap();
let violations: Vec<serde_json::Value> = serde_json::from_str(&content).unwrap();
let exp34: Vec<_> = violations
.iter()
.filter(|v| v["rule_id"] == "EXP34-C")
.collect();
assert!(
!exp34.is_empty(),
"With -d, get_buffer() can_return_null → dereference without check should flag EXP34-C"
);
}
#[test]
fn crossfile_nullable_return_safe_not_flagged() {
let dir = tempfile::tempdir().unwrap();
let out = dir.path().join("out.json");
let fixture_dir = fixtures().join("crossfile_callsite_null");
let (code, _, _) = run_sqc(&[
fixture_dir.join("nullable_user_safe.c").to_str().unwrap(),
"-m",
manifest_exp34().to_str().unwrap(),
"-d",
fixture_dir.to_str().unwrap(),
"-e",
out.to_str().unwrap(),
]);
assert_eq!(code, 0);
let content = std::fs::read_to_string(&out).unwrap();
let violations: Vec<serde_json::Value> = serde_json::from_str(&content).unwrap();
let exp34: Vec<_> = violations
.iter()
.filter(|v| v["rule_id"] == "EXP34-C")
.collect();
assert!(
exp34.is_empty(),
"NULL check after get_buffer() should suppress EXP34-C"
);
}
fn manifest_mem31() -> PathBuf {
fixtures().join("manifest_mem31.toml")
}
#[test]
fn crossfile_frees_param_suppresses_leak() {
let dir = tempfile::tempdir().unwrap();
let out = dir.path().join("out.json");
let fixture_dir = fixtures().join("crossfile_frees");
let (code, _, _) = run_sqc(&[
fixture_dir.join("caller_good.c").to_str().unwrap(),
"-m",
manifest_mem31().to_str().unwrap(),
"-d",
fixture_dir.to_str().unwrap(),
"-e",
out.to_str().unwrap(),
]);
assert_eq!(code, 0);
let content = std::fs::read_to_string(&out).unwrap();
let violations: Vec<serde_json::Value> = serde_json::from_str(&content).unwrap();
let mem31: Vec<_> = violations
.iter()
.filter(|v| v["rule_id"] == "MEM31-C")
.collect();
assert!(
mem31.is_empty(),
"With -d, cleanup_buffer() frees param 0 → no MEM31-C leak in caller_good.c"
);
}
#[test]
fn crossfile_frees_param_not_suppressed_without_d_flag() {
let dir = tempfile::tempdir().unwrap();
let out = dir.path().join("out.json");
let fixture_dir = fixtures().join("crossfile_frees");
let (code, _, _) = run_sqc(&[
fixture_dir.join("caller_good.c").to_str().unwrap(),
"-m",
manifest_mem31().to_str().unwrap(),
"-e",
out.to_str().unwrap(),
]);
assert_eq!(code, 0);
let content = std::fs::read_to_string(&out).unwrap();
let violations: Vec<serde_json::Value> = serde_json::from_str(&content).unwrap();
let mem31: Vec<_> = violations
.iter()
.filter(|v| v["rule_id"] == "MEM31-C")
.collect();
assert!(
!mem31.is_empty(),
"Without -d, cleanup_buffer() is unknown → MEM31-C should flag leak"
);
}
#[test]
fn crossfile_actual_leak_detected() {
let dir = tempfile::tempdir().unwrap();
let out = dir.path().join("out.json");
let fixture_dir = fixtures().join("crossfile_frees");
let (code, _, _) = run_sqc(&[
fixture_dir.join("caller_leak.c").to_str().unwrap(),
"-m",
manifest_mem31().to_str().unwrap(),
"-d",
fixture_dir.to_str().unwrap(),
"-e",
out.to_str().unwrap(),
]);
assert_eq!(code, 0);
let content = std::fs::read_to_string(&out).unwrap();
let violations: Vec<serde_json::Value> = serde_json::from_str(&content).unwrap();
let mem31: Vec<_> = violations
.iter()
.filter(|v| v["rule_id"] == "MEM31-C")
.collect();
assert!(
!mem31.is_empty(),
"Actual leak (no free, no cleanup call) should be flagged even with -d"
);
}
fn manifest_dcl15() -> PathBuf {
fixtures().join("manifest_dcl15.toml")
}
#[test]
fn crossfile_header_declared_suppresses_dcl15c() {
let dir = tempfile::tempdir().unwrap();
let out = dir.path().join("out.json");
let fixture_dir = fixtures().join("crossfile_header");
let (code, _, _) = run_sqc(&[
fixture_dir.join("impl.c").to_str().unwrap(),
"-m",
manifest_dcl15().to_str().unwrap(),
"-d",
fixture_dir.to_str().unwrap(),
"-e",
out.to_str().unwrap(),
]);
assert_eq!(code, 0);
let content = std::fs::read_to_string(&out).unwrap();
let violations: Vec<serde_json::Value> = serde_json::from_str(&content).unwrap();
let dcl15: Vec<_> = violations
.iter()
.filter(|v| v["rule_id"] == "DCL15-C")
.collect();
let flagged_names: Vec<String> = dcl15
.iter()
.map(|v| v["message"].as_str().unwrap_or("").to_string())
.collect();
assert!(
!flagged_names.iter().any(|m| m.contains("compute_value")),
"compute_value() has header prototype — DCL15-C should not flag it"
);
assert!(
!flagged_names.iter().any(|m| m.contains("print_result")),
"print_result() has header prototype — DCL15-C should not flag it"
);
assert!(
flagged_names.iter().any(|m| m.contains("internal_helper")),
"internal_helper() has no header prototype — DCL15-C should flag it"
);
}
#[test]
fn crossfile_header_not_suppressed_without_d_flag() {
let dir = tempfile::tempdir().unwrap();
let out = dir.path().join("out.json");
let fixture_dir = fixtures().join("crossfile_header");
let (code, _, _) = run_sqc(&[
fixture_dir.join("impl.c").to_str().unwrap(),
"-m",
manifest_dcl15().to_str().unwrap(),
"-e",
out.to_str().unwrap(),
]);
assert_eq!(code, 0);
let content = std::fs::read_to_string(&out).unwrap();
let violations: Vec<serde_json::Value> = serde_json::from_str(&content).unwrap();
let dcl15: Vec<_> = violations
.iter()
.filter(|v| v["rule_id"] == "DCL15-C")
.collect();
assert!(
dcl15.len() >= 3,
"Without -d, all non-static functions should be flagged (got {})",
dcl15.len()
);
}