use anyhow::Result;
use lang_check::prose::{ProseExtractor, latex::LatexExtras};
use rand::SeedableRng;
use rand::rngs::SmallRng;
use rand::seq::SliceRandom;
struct Fragment {
template: &'static str,
prose: bool,
}
const fn prose(template: &'static str) -> Fragment {
Fragment {
template,
prose: true,
}
}
const fn code(template: &'static str) -> Fragment {
Fragment {
template,
prose: false,
}
}
const TYPST: &[Fragment] = &[
prose("A plain paragraph mentions %% in passing."),
prose("= A heading naming %%"),
prose("#columns(2)[\n The two-column body mentions %% here.\n]"),
prose("#align(center)[\n The centred block mentions %% here.\n]"),
prose("#figure(\n image(\"plot.png\"),\n caption: [The caption mentions %% here.],\n)"),
prose("#box[A box body mentions %% here.]"),
prose("- A list item mentions %% here."),
prose("Text before #emph[an emphasis mentioning %% inside] and after."),
prose("#text(size: 9pt)[A styled run mentions %% here.]"),
prose("#align(center)[#emph[A doubly nested run mentions %% here.]]"),
prose("#figure(caption: [A caption mentioning %% here.])[Figure body text.]"),
prose("#table(\n columns: 2,\n [A cell mentioning %% here.], [Second cell.],\n)"),
prose("#link(\"https://example.com\")[A link label mentioning %% here.]"),
code("```rust\nfn %%() {}\n```"),
code("A paragraph with `%%` inline code."),
code("$ \"%%\" + 1 = 2 $"),
code("// A comment naming %%"),
code("#let %% = 1"),
code("#import \"%%.typ\": *"),
code("#set text(font: \"%%\")"),
code("A paragraph with a link to https://example.com/%%/page."),
code("#box[Prose around `%%` raw text inside a content block.]"),
code("#align(center)[A centred run around $\"%%\" + 1$ math.]"),
code("#show heading: set text(font: \"%%\")"),
code("A paragraph followed by a label. <%%>"),
];
const MARKDOWN: &[Fragment] = &[
prose("A plain paragraph mentions %% in passing."),
prose("# A heading naming %%"),
prose("A setext heading naming %%\n========================"),
prose("> A blockquote mentions %% here."),
prose("- A list item mentions %% here."),
prose("A paragraph with **bold %% inside** and after."),
prose("| A table cell mentions %% | second |\n| --- | --- |\n| body | cell |"),
code("```rust\nfn %%() {}\n```"),
code("<!-- A comment naming %% -->"),
];
const LATEX: &[Fragment] = &[
prose("A plain paragraph mentions %% in passing."),
prose("\\section{A heading naming %%}"),
prose("\\begin{itemize}\n \\item A list item mentions %% here.\n\\end{itemize}"),
prose("\\footnote{A footnote mentions %% here.}"),
prose("\\emph{An emphasised run mentions %% here.}"),
prose("\\begin{quote}\n A quoted paragraph mentions %% here.\n\\end{quote}"),
prose("\\begin{figure}\n \\caption{The caption mentions %% here.}\n\\end{figure}"),
code("\\begin{verbatim}\n%%\n\\end{verbatim}"),
code("\\begin{equation}\n x = %% + 1\n\\end{equation}"),
code("% A comment naming %%"),
code("\\label{sec:%%}"),
code("A paragraph with \\verb|%%| inline verbatim."),
code("A line with \\[%%\\] \\[y + 1\\] inside."),
code("\\begin{figure}\n \\includegraphics{%%.png}\n\\end{figure}"),
];
const ORG: &[Fragment] = &[
prose("A plain paragraph mentions %% in passing."),
prose("* A heading naming %%"),
prose("#+begin_quote\nA quoted paragraph mentions %% here.\n#+end_quote"),
prose("[fn:9] A footnote mentions %% here."),
prose("| A table cell mentions %% | second |"),
prose("#+title: A document title naming %%"),
code("#+begin_src rust\nfn %%() {}\n#+end_src"),
code("# A comment naming %%"),
code("#+options: %%:nil"),
code(":PROPERTIES:\n:CUSTOM_ID: %%\n:END:"),
];
const RST: &[Fragment] = &[
prose("A plain paragraph mentions %% in passing."),
prose(".. note::\n\n An admonition mentions %% here."),
prose(".. figure:: plot.png\n\n The caption mentions %% here."),
prose(".. code-block:: rust\n :caption: The caption mentions %% here.\n\n fn main() {}"),
prose("- A list item mentions %% here."),
code(".. code-block:: rust\n\n fn %%() {}"),
code(".. math::\n\n x = %% + 1"),
code(".. toctree::\n\n guide/%%"),
code(
".. note::\n\n An admonition around a code block.\n\n .. code-block:: rust\n\n fn %%() {}",
),
code(".. warning::\n\n An admonition introducing a literal block::\n\n %% --flag"),
];
fn marker(index: usize) -> String {
format!("zqvurg{index}zz")
}
fn compose(fragments: &[&Fragment]) -> (String, Vec<(String, bool)>) {
let mut doc = String::new();
let mut markers = Vec::with_capacity(fragments.len());
for (index, fragment) in fragments.iter().enumerate() {
let word = marker(index);
doc.push_str(&fragment.template.replace("%%", &word));
doc.push_str("\n\n");
markers.push((word, fragment.prose));
}
(doc, markers)
}
fn fuzz_language(lang_id: &str, bank: &'static [Fragment], iterations: u64) -> Result<()> {
let language = lang_check::languages::resolve_ts_language(lang_id);
let mut extractor = ProseExtractor::new(language.clone())?;
let mut parser = tree_sitter::Parser::new();
parser.set_language(&language)?;
for seed in 0..iterations {
let mut rng = SmallRng::seed_from_u64(seed);
let mut picks: Vec<&Fragment> = bank.iter().collect();
picks.shuffle(&mut rng);
picks.truncate(3 + (seed as usize % (bank.len() - 2)));
let (doc, markers) = compose(&picks);
let tree = parser.parse(&doc, None).expect("parser returned no tree");
assert!(
!tree.root_node().has_error(),
"{lang_id} seed {seed}: composed document does not parse\n{doc}"
);
let ranges = extractor.extract(&doc, lang_id, &LatexExtras::default())?;
let extracted: String = ranges
.iter()
.map(|range| range.extract_text(&doc).into_owned())
.collect::<Vec<_>>()
.join("\n");
for range in &ranges {
assert!(
range.start_byte <= range.end_byte && range.end_byte <= doc.len(),
"{lang_id} seed {seed}: range {}..{} is out of bounds for a {}-byte document",
range.start_byte,
range.end_byte,
doc.len()
);
let mut previous_end = range.start_byte;
for &(start, end) in &range.exclusions {
assert!(
start <= end,
"{lang_id} seed {seed}: exclusion {start}..{end} is inverted\n{doc}"
);
assert!(
start >= previous_end,
"{lang_id} seed {seed}: exclusion {start}..{end} overlaps or precedes the \
one ending at {previous_end}; exclusions must be sorted and disjoint\n\
--- document ---\n{doc}\n--- exclusions ---\n{:?}",
range.exclusions
);
assert!(
end <= range.end_byte,
"{lang_id} seed {seed}: exclusion {start}..{end} runs past the range end {}\n{doc}",
range.end_byte
);
previous_end = end;
}
}
for (word, is_prose) in markers {
assert_eq!(
extracted.contains(&word),
is_prose,
"{lang_id} seed {seed}: marker {word:?} should{} be in the prose\n\
--- document ---\n{doc}\n--- extracted ---\n{extracted}",
if is_prose { "" } else { " not" }
);
}
}
Ok(())
}
#[test]
fn typst_composition() -> Result<()> {
fuzz_language("typst", TYPST, 500)
}
#[test]
fn markdown_composition() -> Result<()> {
fuzz_language("markdown", MARKDOWN, 500)
}
#[test]
fn latex_composition() -> Result<()> {
fuzz_language("latex", LATEX, 500)
}
#[test]
fn org_composition() -> Result<()> {
fuzz_language("org", ORG, 500)
}
#[test]
fn rst_composition() -> Result<()> {
fuzz_language("rst", RST, 500)
}