use std::path::{Path, PathBuf};
use std::process::Command;
const DIALECTS: [&str; 10] =
["c89", "c99", "c11", "c17", "c23", "gnu89", "gnu99", "gnu11", "gnu17", "gnu23"];
const KNOWN_GAPS: usize = 34;
fn accept_dir() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.ancestors()
.nth(2)
.expect("the crate is two levels under the repository root")
.join("tests")
.join("accept")
}
struct Expected {
accept: Vec<String>,
reject: Vec<String>,
message: Option<String>,
warns: Option<String>,
gaps: Vec<(String, String)>,
}
fn dialects_in(list: &str, path: &Path) -> Vec<String> {
let mut named = Vec::new();
for word in list.split_whitespace() {
let group: &[&str] = match word {
"all" => &DIALECTS,
"iso" => &["c89", "c99", "c11", "c17", "c23"],
"gnu" => &["gnu89", "gnu99", "gnu11", "gnu17", "gnu23"],
one if DIALECTS.contains(&one) => &[],
other => panic!("{}: `{other}` is not a dialect", path.display()),
};
if group.is_empty() {
named.push(word.to_owned());
} else {
named.extend(group.iter().map(|&d| d.to_owned()));
}
}
named
}
fn expectations(path: &Path, source: &str) -> Expected {
let mut expected = Expected {
accept: Vec::new(),
reject: Vec::new(),
message: None,
warns: None,
gaps: Vec::new(),
};
for line in source.lines() {
let line = line.trim();
let Some(rest) = line.strip_prefix("/*").and_then(|rest| rest.strip_suffix("*/")) else {
continue;
};
let Some((keyword, value)) = rest.split_once(':') else { continue };
let value = value.trim();
match keyword.trim() {
"accept" => expected.accept.extend(dialects_in(value, path)),
"reject" => expected.reject.extend(dialects_in(value, path)),
"message" => expected.message = Some(value.to_owned()),
"warns" => expected.warns = Some(value.to_owned()),
"gap" => {
let (issue, list) = value.split_once(char::is_whitespace).unwrap_or_else(|| {
panic!("{}: a gap needs an issue and a dialect", path.display())
});
for dialect in dialects_in(list, path) {
expected.gaps.push((dialect, issue.to_owned()));
}
}
_ => {}
}
}
for dialect in DIALECTS {
let accepted = expected.accept.iter().any(|d| d == dialect);
let rejected = expected.reject.iter().any(|d| d == dialect);
assert!(
accepted != rejected,
"{}: {dialect} is {}, and every dialect has to be one or the other",
path.display(),
if accepted { "both accepted and rejected" } else { "neither accepted nor rejected" }
);
}
expected
}
struct Ran {
accepted: bool,
said: String,
}
fn run(source: &Path, dialect: &str) -> Ran {
let out = Command::new(env!("CARGO_BIN_EXE_rucc"))
.args(["--target=x86_64-unknown-linux-gnu", "--emit=tast", "-o", "-"])
.arg(format!("-std={dialect}"))
.arg(source)
.output()
.expect("the compiler is built before its own tests run");
Ran { accepted: out.status.success(), said: String::from_utf8_lossy(&out.stderr).into_owned() }
}
#[test]
fn every_case_is_taken_the_way_its_directives_say_under_every_dialect() {
let dir = accept_dir();
let mut cases: Vec<PathBuf> = std::fs::read_dir(&dir)
.unwrap_or_else(|e| panic!("{}: {e}", dir.display()))
.map(|entry| entry.expect("a readable directory entry").path())
.filter(|path| path.extension().is_some_and(|ext| ext == "c"))
.collect();
cases.sort();
assert!(!cases.is_empty(), "{}: no cases", dir.display());
let mut wrong = Vec::new();
let mut closed = Vec::new();
let mut gaps = 0;
for case in &cases {
let name = case.file_name().unwrap_or(case.as_os_str()).to_string_lossy().into_owned();
let source = std::fs::read_to_string(case).unwrap_or_else(|e| panic!("{name}: {e}"));
let expected = expectations(case, &source);
for dialect in DIALECTS {
let should_accept = expected.accept.iter().any(|d| d == dialect);
let gap = expected.gaps.iter().find(|(d, _)| d == dialect);
let ran = run(case, dialect);
if let Some((_, issue)) = gap {
gaps += 1;
if ran.accepted == should_accept {
closed.push(format!(
"{name} at -std={dialect}: works now, so the gap on {issue} can go"
));
}
continue;
}
if ran.accepted != should_accept {
let happened = if ran.accepted { "was accepted" } else { "was rejected" };
wrong.push(format!(
"{name} at -std={dialect}: {happened} and should not have been. {}",
ran.said.trim().replace('\n', "; ")
));
continue;
}
if should_accept {
match &expected.warns {
Some(wanted) => assert!(
ran.said.contains(wanted),
"{name} at -std={dialect}: expected a warning saying `{wanted}`, said `{}`",
ran.said.trim()
),
None => assert!(
ran.said.is_empty(),
"{name} at -std={dialect}: compiled but said `{}`, and an acceptance with \
no `warns` directive has to be silent",
ran.said.trim()
),
}
} else if let Some(wanted) = &expected.message {
assert!(
ran.said.contains(wanted),
"{name} at -std={dialect}: rejected for the wrong reason. Expected \
`{wanted}`, said `{}`",
ran.said.trim()
);
}
}
}
assert!(closed.is_empty(), "{}", closed.join("\n"));
assert!(wrong.is_empty(), "{} wrong answer(s):\n{}", wrong.len(), wrong.join("\n"));
assert_eq!(
gaps, KNOWN_GAPS,
"the number of known gaps changed. Update KNOWN_GAPS, and say in the pull request which \
way it went and why."
);
}