use std::collections::{BTreeMap, BTreeSet};
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
const BINARY: &str = env!("CARGO_BIN_EXE_regex-le");
const FIXTURES: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/fixtures");
const UNKNOWN: [&str; 13] = [
"md", "txt", "yaml", "yml", "toml", "ini", "cfg", "sql", "sh", "kt", "swift", "scala", "dart",
];
const SPELLINGS: [(&str, &str); 9] = [
("javascript", "const bad = /(a+)+/g;\n"),
("typescript", "const bad: RegExp = /(a+)+/;\n"),
("python", "BAD = re.compile(r\"(a+)+\")\n"),
("rust", "let bad = Regex::new(r\"(a+)+\");\n"),
("go", "var bad = regexp.MustCompile(`(a+)+`)\n"),
("java", "Pattern bad = Pattern.compile(\"(a+)+\");\n"),
("ruby", "BAD = /(a+)+/\n"),
("php", "preg_match('/(a+)+/', $s);\n"),
("csharp", "var bad = new Regex(@\"(a+)+\");\n"),
];
struct Tree {
root: PathBuf,
}
impl Tree {
fn new(name: &str) -> Self {
let root =
std::env::temp_dir().join(format!("regex-le-coverage-{name}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&root);
std::fs::create_dir_all(&root).expect("a temporary directory");
Self {
root: std::fs::canonicalize(&root).expect("a canonical directory"),
}
}
fn path(&self) -> &Path {
&self.root
}
fn write(&self, name: &str, contents: &str) {
std::fs::write(self.root.join(name), contents).expect("a file");
}
}
impl Drop for Tree {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.root);
}
}
fn aliases() -> BTreeMap<String, String> {
let path = Path::new(FIXTURES).join("aliases.json");
let text = std::fs::read_to_string(&path).expect("the shared alias table");
serde_json::from_str(&text).expect("the alias table is valid JSON")
}
fn spelling(language: &str) -> &'static str {
SPELLINGS
.iter()
.find(|(name, _)| *name == language)
.map_or_else(
|| panic!("{language} is in the alias table with no call site written for it here"),
|(_, source)| *source,
)
}
fn scan(root: &Path) -> Vec<serde_json::Value> {
let output = Command::new(BINARY)
.args(["--all", &root.to_string_lossy()])
.stdin(Stdio::null())
.output()
.expect("the binary runs");
assert!(
output
.status
.code()
.is_some_and(|code| (0..=2).contains(&code)),
"the scan did not answer: {}",
String::from_utf8_lossy(&output.stderr)
);
String::from_utf8_lossy(&output.stdout)
.lines()
.filter(|line| !line.trim().is_empty())
.map(|line| serde_json::from_str(line).expect("stdout carries only JSON"))
.collect()
}
#[test]
fn every_extension_in_the_table_is_opened_and_reported() {
let table = aliases();
assert!(!table.is_empty(), "the alias table is empty");
let tree = Tree::new("aliases");
let mut expected: BTreeSet<String> = BTreeSet::new();
for (index, (alias, language)) in table.iter().enumerate() {
let name = format!("alias{index:02}.{alias}");
tree.write(&name, spelling(language));
expected.insert(name);
}
for (index, extension) in UNKNOWN.iter().enumerate() {
let name = format!("unknown{index:02}.{extension}");
tree.write(&name, "const bad = /(a+)+/g;\n");
expected.insert(name);
}
tree.write("Makefile", "PATTERN = /(a+)+/\n");
expected.insert("Makefile".to_string());
let reported: BTreeSet<String> = scan(tree.path())
.iter()
.filter_map(|report| report["file"].as_str())
.filter_map(|file| file.rsplit('/').next())
.map(str::to_string)
.collect();
let missing: Vec<&String> = expected.difference(&reported).collect();
assert!(
missing.is_empty(),
"the walk skipped {} files it should have opened: {missing:?}",
missing.len()
);
}
#[test]
fn every_language_in_the_table_finds_its_own_spelling() {
let table = aliases();
let tree = Tree::new("languages");
let mut expected: BTreeMap<String, String> = BTreeMap::new();
for (index, (alias, language)) in table.iter().enumerate() {
let name = format!("alias{index:02}.{alias}");
tree.write(&name, spelling(language));
expected.insert(name, language.clone());
}
let mut silent: Vec<String> = Vec::new();
let reports = scan(tree.path());
for (name, language) in expected {
let report = reports
.iter()
.find(|report| {
report["file"]
.as_str()
.is_some_and(|file| file.ends_with(&name))
})
.unwrap_or_else(|| panic!("{name}: no report line"));
if report["summary"]["findings"] != 1 {
silent.push(format!("{name} ({language})"));
}
}
assert!(
silent.is_empty(),
"these names resolve to a language whose spellings were not found: {silent:?}"
);
}
#[test]
fn every_language_in_the_table_has_a_corpus_document() {
let table = aliases();
let languages: BTreeSet<&String> = table.values().collect();
let documents = Path::new(FIXTURES).join("documents");
let covered: BTreeSet<String> = std::fs::read_dir(&documents)
.expect("the corpus documents")
.filter_map(Result::ok)
.filter_map(|entry| {
let name = entry.file_name().to_string_lossy().into_owned();
let extension = name.rsplit_once('.')?.1.to_lowercase();
table.get(&extension).cloned()
})
.collect();
let uncovered: Vec<&&String> = languages
.iter()
.filter(|language| !covered.contains(**language))
.collect();
assert!(
uncovered.is_empty(),
"these languages are advertised and no corpus document exercises them: {uncovered:?}"
);
}