#![allow(clippy::pedantic)]
use std::path::{Path, PathBuf};
use lang_check::config::Config;
use lang_check::prose::{self, latex::LatexExtras};
struct Example {
path: PathBuf,
language_id: &'static str,
}
fn examples_root() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.expect("rust-core has a parent")
.join("examples")
}
fn examples() -> Vec<Example> {
[
("typst/thesis.typ", "typst"),
("typst-overlapping-checkers/overlap.typ", "typst"),
("latex/paper.tex", "latex"),
("markdown/notes.md", "markdown"),
]
.into_iter()
.map(|(relative, language_id)| Example {
path: examples_root().join(relative),
language_id,
})
.collect()
}
fn render(example: &Example) -> String {
let text = std::fs::read_to_string(&example.path)
.unwrap_or_else(|e| panic!("read {}: {e}", example.path.display()));
let config = Config::default();
let latex_extras = LatexExtras {
skip_envs: &config.languages.latex.skip_environments,
skip_commands: &config.languages.latex.skip_commands,
};
let extraction =
prose::extract_reporting_syntax(&text, example.language_id, None, None, &latex_extras)
.expect("extraction");
let units = prose::range_units(&extraction.ranges, &text, "en-GB");
let mut out = format!("syntax: {}\n", extraction.syntax);
for (range, unit) in extraction.ranges.iter().zip(&units) {
let line = text[..range.start_byte].lines().count();
let body: String = unit.text.split_whitespace().collect::<Vec<_>>().join(" ");
let body = if body.chars().count() > 72 {
let head: String = body.chars().take(69).collect();
format!("{head}...")
} else {
body
};
out.push_str(&format!("L{line:<4} {:<8} {body}\n", unit.language));
}
out
}
#[test]
fn every_example_extracts_and_routes_as_recorded() {
for example in examples() {
let name = example
.path
.parent()
.and_then(|p| p.file_name())
.and_then(|n| n.to_str())
.expect("an example lives in a named directory");
insta::assert_snapshot!(format!("example_{name}"), render(&example));
}
}
#[test]
fn every_example_directory_carries_the_config_it_demonstrates() {
for example in examples() {
let dir = example.path.parent().expect("example directory");
assert!(
dir.join(".languagecheck.yaml").is_file(),
"{} has no .languagecheck.yaml; the config is half the example",
dir.display()
);
assert!(
dir.join("expected.md").is_file(),
"{} has no expected.md saying what a check should report",
dir.display()
);
}
}
#[test]
fn every_example_config_parses() {
for example in examples() {
let dir = example.path.parent().expect("example directory");
let path = dir.join(".languagecheck.yaml");
let raw = std::fs::read_to_string(&path).expect("read config");
serde_yaml::from_str::<Config>(&raw)
.unwrap_or_else(|e| panic!("{} does not parse: {e}", path.display()));
}
}
fn grammar_for(language_id: &str) -> tree_sitter::Language {
lang_check::languages::resolve_ts_language(language_id)
}
fn parse_errors(node: tree_sitter::Node, text: &str, out: &mut Vec<String>) {
if node.is_error() || node.is_missing() {
let line = text[..node.start_byte()].lines().count();
let snippet: String = text[node.byte_range()].chars().take(60).collect();
out.push(format!("L{line}: {snippet:?}"));
return;
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
parse_errors(child, text, out);
}
}
#[test]
fn every_example_parses_without_errors() {
for example in examples() {
let text = std::fs::read_to_string(&example.path).expect("read example");
let mut parser = tree_sitter::Parser::new();
parser
.set_language(&grammar_for(example.language_id))
.expect("grammar");
let tree = parser.parse(&text, None).expect("parse");
let mut errors = Vec::new();
parse_errors(tree.root_node(), &text, &mut errors);
assert!(
errors.is_empty(),
"{} does not parse as {}:\n {}",
example.path.display(),
example.language_id,
errors.join("\n ")
);
}
}
#[test]
fn every_example_compiles() {
for (relative, program, args) in [
(
"typst/thesis.typ",
"typst",
vec!["compile", "thesis.typ", "-"],
),
(
"latex/paper.tex",
"latexmk",
vec![
"-pdf",
"-interaction=nonstopmode",
"-halt-on-error",
"paper.tex",
],
),
] {
let path = examples_root().join(relative);
let dir = path.parent().expect("example directory");
let out_dir = std::env::temp_dir().join(format!(
"lang_check_example_build_{}_{}",
std::process::id(),
program
));
std::fs::create_dir_all(&out_dir).expect("build directory");
let mut command = std::process::Command::new(program);
command.current_dir(dir).args(&args);
if program == "latexmk" {
command.arg(format!("-outdir={}", out_dir.display()));
}
let output = match command.output() {
Ok(output) => output,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
eprintln!("skipping {relative}: {program} is not installed");
continue;
}
Err(e) => panic!("running {program}: {e}"),
};
let _ = std::fs::remove_dir_all(&out_dir);
assert!(
output.status.success(),
"{relative} does not compile:\n{}",
String::from_utf8_lossy(&output.stderr)
);
}
}