use rto_exec::{NativeContext, WorktreeSnippets, normalize_native};
use rto_graph::SourceIdentity;
mod fixture;
fn normalize_fixture() -> rto_exec::NormalizedReport {
let snippets = WorktreeSnippets::new(fixture::polyglot_root());
let source = SourceIdentity::default();
let ctx = NativeContext {
started_at: "2026-08-15T09:00:00Z".to_owned(),
ended_at: "2026-08-15T09:00:09Z".to_owned(),
analyzer_version: None,
exit_status: 1,
source: &source,
rules_digest: Some("baseline".to_owned()),
advisory_db: None,
worktree: None,
snippets: &snippets,
};
normalize_native("semgrep", &fixture::semgrep_native(), &ctx).expect("normalise")
}
#[test]
fn every_required_language_yields_a_finding() {
let report = normalize_fixture();
for (language, path) in fixture::REQUIRED_LANGUAGES {
let hits: Vec<&str> = report
.findings
.iter()
.filter(|f| f.path.as_deref() == Some(*path))
.map(|f| f.rule.as_str())
.collect();
assert!(
!hits.is_empty(),
"no finding for {language} ({path}); the coverage claim in ADR-0018 is not met"
);
}
}
#[test]
fn sql_findings_are_produced_and_marked_as_generic_matches() {
let report = normalize_fixture();
let sql: Vec<_> = report
.findings
.iter()
.filter(|f| f.path.as_deref() == Some("sql/schema.sql"))
.collect();
assert!(!sql.is_empty(), "SQL must produce findings");
for finding in sql {
let note = finding.meta["metadata"]["engine-note"]
.as_str()
.unwrap_or_default();
assert!(
note.contains("generic"),
"{} must declare that it matched generically, said: {note:?}",
finding.rule
);
}
}
#[test]
fn findings_carry_a_rule_a_location_and_a_severity() {
let report = normalize_fixture();
assert!(report.findings.len() >= 10, "the fixture has thinned out");
for finding in &report.findings {
assert!(!finding.rule.trim().is_empty());
assert!(!finding.title.trim().is_empty());
let path = finding
.path
.as_deref()
.expect("a semgrep finding has a path");
assert!(!path.starts_with('/'), "{path} must be worktree-relative");
let span = finding.span.expect("a semgrep finding has a span");
assert!(
span.end >= span.start,
"{} has a backwards span",
finding.rule
);
}
}
#[test]
fn rule_ids_are_stable_names_not_filesystem_paths() {
for finding in &normalize_fixture().findings {
assert!(
finding.rule.starts_with("roteiro."),
"unexpected rule id {:?} — is --no-rewrite-rule-ids still passed?",
finding.rule
);
assert!(!finding.rule.contains('/'), "{:?}", finding.rule);
}
}
#[test]
fn runs_the_real_analyzer_when_one_is_installed() {
let Some(semgrep) = which("semgrep") else {
println!(
"SKIP runs_the_real_analyzer_when_one_is_installed: no `semgrep` on PATH. \
The fixture-driven tests in this file carry the coverage; install semgrep to \
re-verify against the tool itself."
);
return;
};
let scratch = std::env::temp_dir().join("rto-exec-polyglot-live");
std::fs::remove_dir_all(&scratch).ok();
copy_tree(&fixture::polyglot_root(), &scratch).expect("copy the fixture tree");
let rules = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("rules/roteiro-baseline.yml");
let output = std::process::Command::new(semgrep)
.args([
"scan",
"--json",
"--quiet",
"--metrics=off",
"--disable-version-check",
"--no-rewrite-rule-ids",
"--config",
])
.arg(&rules)
.arg(".")
.current_dir(&scratch)
.output()
.expect("run semgrep");
assert!(
matches!(output.status.code(), Some(0 | 1)),
"semgrep exited {:?}: {}",
output.status.code(),
String::from_utf8_lossy(&output.stderr)
);
let snippets = WorktreeSnippets::new(&scratch);
let source = SourceIdentity::default();
let ctx = NativeContext {
started_at: "2026-08-15T09:00:00Z".to_owned(),
ended_at: "2026-08-15T09:00:09Z".to_owned(),
analyzer_version: None,
exit_status: output.status.code().unwrap_or(0),
source: &source,
rules_digest: Some("baseline".to_owned()),
advisory_db: None,
worktree: None,
snippets: &snippets,
};
let live = normalize_native("semgrep", &output.stdout, &ctx).expect("normalise live output");
let mut live_rules: Vec<&str> = live.findings.iter().map(|f| f.rule.as_str()).collect();
let fixture_report = normalize_fixture();
let mut fixture_rules: Vec<&str> = fixture_report
.findings
.iter()
.map(|f| f.rule.as_str())
.collect();
live_rules.sort_unstable();
fixture_rules.sort_unstable();
assert_eq!(
live_rules, fixture_rules,
"the committed fixture no longer matches what semgrep emits; re-capture it \
(see tests/fixtures/polyglot/README.md)"
);
std::fs::remove_dir_all(&scratch).ok();
}
fn which(program: &str) -> Option<std::path::PathBuf> {
std::env::var_os("PATH")?
.to_string_lossy()
.split(if cfg!(windows) { ';' } else { ':' })
.map(|dir| std::path::Path::new(dir).join(program))
.find(|candidate| candidate.is_file())
}
fn copy_tree(from: &std::path::Path, to: &std::path::Path) -> std::io::Result<()> {
std::fs::create_dir_all(to)?;
for entry in std::fs::read_dir(from)? {
let entry = entry?;
let target = to.join(entry.file_name());
if entry.file_type()?.is_dir() {
copy_tree(&entry.path(), &target)?;
} else {
std::fs::copy(entry.path(), target)?;
}
}
Ok(())
}