use std::path::{Path, PathBuf};
const LEX_RATE_FLOOR: f64 = 0.80;
const PARSE_RATE_FLOOR: f64 = 0.0;
fn collect(dir: &Path, out: &mut Vec<PathBuf>) {
let Ok(rd) = std::fs::read_dir(dir) else { return };
for entry in rd.flatten() {
let p = entry.path();
let s = p.to_string_lossy();
if s.contains("/_opam/") || s.contains("/.git/") || s.contains("/target/") {
continue;
}
if p.is_dir() {
collect(&p, out);
} else if matches!(
p.extension().and_then(|e| e.to_str()),
Some("saty") | Some("satyh") | Some("satyg")
) {
out.push(p);
}
}
}
fn guarded<T>(f: impl FnOnce() -> Result<T, String> + std::panic::UnwindSafe) -> Result<T, String> {
let prev = std::panic::take_hook();
std::panic::set_hook(Box::new(|_| {}));
let r = std::panic::catch_unwind(f);
std::panic::set_hook(prev);
match r {
Ok(inner) => inner,
Err(_) => Err("PANIC".to_string()),
}
}
#[test]
fn corpus_lex_and_parse() {
let Ok(roots) = std::env::var("RUSTYFI_CORPUS_DIR") else {
eprintln!(
"corpus: RUSTYFI_CORPUS_DIR unset — skipping (set it to a ':'-separated \
list of rustyfi-* repo roots to run the corpus regression)"
);
return;
};
let mut files = Vec::new();
for root in roots.split(':').filter(|s| !s.is_empty()) {
collect(Path::new(root), &mut files);
}
files.sort();
assert!(
!files.is_empty(),
"RUSTYFI_CORPUS_DIR was set but no .saty/.satyh/.satyg files were found under {roots:?}"
);
let mut lex_ok = 0usize;
let mut parse_ok = 0usize;
let mut panics: Vec<String> = Vec::new();
let mut lex_fail: Vec<String> = Vec::new();
let mut parse_fail: Vec<String> = Vec::new();
for f in &files {
let src = match std::fs::read_to_string(f) {
Ok(s) => s,
Err(_) => continue, };
let rel = f.display().to_string();
match guarded(|| rustyfi_syntax::lex(&src).map_err(|e| e.to_string())) {
Ok(_) => {
lex_ok += 1;
match guarded(|| rustyfi_syntax::parse_file(&src).map(|_| ()).map_err(|e| e.to_string())) {
Ok(()) => parse_ok += 1,
Err(ref m) if m == "PANIC" => panics.push(format!("parse {rel}")),
Err(m) => parse_fail.push(format!("{rel}: {}", first_line(&m))),
}
}
Err(ref m) if m == "PANIC" => panics.push(format!("lex {rel}")),
Err(m) => lex_fail.push(format!("{rel}: {}", first_line(&m))),
}
}
let total = files.len();
let lex_rate = lex_ok as f64 / total as f64;
let parse_rate = parse_ok as f64 / total as f64;
eprintln!("\n==== SATySFi corpus regression ====");
eprintln!("files: {total}");
eprintln!("lex: {lex_ok}/{total} ({:.1}%)", lex_rate * 100.0);
eprintln!("parse: {parse_ok}/{total} ({:.1}%)", parse_rate * 100.0);
if !lex_fail.is_empty() {
eprintln!("\n-- lex failures ({}) --", lex_fail.len());
for l in &lex_fail {
eprintln!(" {l}");
}
}
if !parse_fail.is_empty() {
eprintln!("\n-- parse failures ({}, first 20) --", parse_fail.len());
for l in parse_fail.iter().take(20) {
eprintln!(" {l}");
}
}
eprintln!("===================================\n");
assert!(
panics.is_empty(),
"the frontend PANICKED on real SATySFi source (always a bug):\n {}",
panics.join("\n ")
);
assert!(
lex_rate >= LEX_RATE_FLOOR,
"lex success rate {:.1}% fell below the floor {:.0}% — a lexer regression \
(was ~89% at baseline). See the failures above.",
lex_rate * 100.0,
LEX_RATE_FLOOR * 100.0,
);
assert!(
parse_rate >= PARSE_RATE_FLOOR,
"parse success rate {:.1}% fell below the floor {:.0}%.",
parse_rate * 100.0,
PARSE_RATE_FLOOR * 100.0,
);
}
fn first_line(s: &str) -> &str {
s.lines().next().unwrap_or(s)
}