Files
2026-10-14 22:39:13 -03:00

197 lines
5.0 KiB
Rust
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Fuzz: 20 000 adversarial/mutated inputs; parse + layout must never panic, never
//! loop forever, and only produce finite, non-negative sizes. Deterministic (xorshift).
use notescraft_math::{Style, layout, parse, testutil::FakeMetrics};
/// Deterministic xorshift64*; seeds are fixed so failures reproduce.
struct Rng(u64);
impl Rng {
fn next(&mut self) -> u64 {
let mut x = self.0;
x ^= x >> 12;
x ^= x << 25;
x ^= x >> 27;
self.0 = x;
x.wrapping_mul(0x2545_F491_4F6C_DD1D)
}
fn below(&mut self, hi: usize) -> usize {
let r = self.next() >> 32; // top bits: no modulo bias for the small ranges used
let t = usize::try_from(r).unwrap_or(usize::MAX);
t % hi
}
}
const TOKENS: &[&str] = &[
"x",
"y",
"1",
"2",
"+",
"-",
"=",
"(",
")",
"[",
"]",
"{",
"}",
"^",
"_",
"&",
"\\",
",",
".",
"!",
"~",
" ",
"'",
"é",
"α",
"𝔸",
"\\frac",
"\\sqrt",
"\\sum",
"\\lim",
"\\max",
"\\int",
"\\iota",
"_i",
"^n",
"\\left",
"\\right",
"\\begin{matrix}",
"\\end{matrix}",
"\\begin{aligned}",
"\\end{aligned}",
"\\begin{cases}",
"\\end{cases}",
"\\text{a b}",
"\\alpha",
"\\hat",
"\\mathbb{R}",
"\\mathrm{x}",
"\\,",
"\\quad",
"\\frac{a}{b}",
"\\sqrt[3]{x}",
"\\sum_i",
"\\lim_{x}",
"\\cdot",
"\\infty",
"\\to",
"{a}",
"\\end",
"\\begin",
"\\right)",
"&",
"x^^\\",
"\\frac{a}",
"\\left(a",
"%",
"#",
"\\\\",
"\\end{x}",
];
const VALID: &[&str] = &[
"\\frac{a}{b}+\\sqrt[3]{x^2}",
"\\begin{pmatrix}a&b\\\\c&d\\end{pmatrix}_{i\\,j}^{n}",
"\\sum_{k=1}^{n}\\frac{k!}{2^k} = \\lim_{n\\to\\infty}\\left(\\sum...\\right)",
"\\left|\\frac{\\alpha}{\\beta}\\right|^2 + \\hat x \\vec v",
"\\begin{cases} x^2 & x \\geq 0 \\\\ -x & \\text{else} \\end{cases}",
"f(x) = \\int_{-\\infty}^{\\infty} \\mathrm{e}^{-x^2} dx",
];
fn mutate(rng: &mut Rng, src: &str) -> String {
let chars: Vec<char> = src.chars().collect();
let pos = rng.below(chars.len().max(1));
let mut out = String::new();
let pick = rng.below(3);
for (i, ch) in chars.iter().enumerate() {
if i == pos && pick == 0 {
// Insert a random token.
let tok = TOKENS[rng.below(TOKENS.len())];
out.push_str(tok);
}
if i == pos && pick == 1 {
continue; // Delete the character.
}
out.push(*ch);
if i == pos && pick == 2 {
out.push(*ch); // Duplicate it.
}
}
out
}
fn random_token_soup(rng: &mut Rng, out: &mut String) {
let n = 1 + rng.below(12);
for _ in 0..n {
let tok = TOKENS[rng.below(TOKENS.len())];
out.push_str(tok);
if rng.below(4) == 0 {
out.push(' ');
}
}
}
fn check(src: &str, tag: &str) {
match parse(src) {
Err(_) => { /* Malformed input is always an Err, never a panic. */ }
Ok(node) => {
for display in [false, true] {
let b = layout(&node, &FakeMetrics, Style { size_pt: 10.0, display });
assert!(b.width.is_finite(), "{tag}: width");
assert!(b.height.is_finite(), "{tag}: height");
assert!(b.depth.is_finite(), "{tag}: depth");
assert!(b.width >= -0.01, "{tag}: {}", b.width);
assert!(b.height >= -0.01, "{tag}: {}", b.height);
assert!(b.depth >= -0.01, "{tag}: {}", b.depth);
for item in &b.items {
check_item(item, tag);
}
}
}
}
}
fn check_item(item: &notescraft_math::Item, tag: &str) {
match item {
notescraft_math::Item::Glyphs { x, baseline_y, size, text, .. } => {
let (x, by, size) = (*x, *baseline_y, *size);
assert!(x.is_finite() && by.is_finite() && size.is_finite(), "{tag}");
assert!(size > 0.0, "{tag}");
assert!(!text.is_empty(), "{tag}: empty run");
}
notescraft_math::Item::Rule { w, h, .. } => {
let (w, h) = (*w, *h);
assert!(w >= 0.0 && h > 0.0 && w.is_finite() && h.is_finite(), "{tag}");
}
notescraft_math::Item::Path { points, stroke } => {
assert!(*stroke > 0.0 && stroke.is_finite(), "{tag}");
for [x, y] in points {
assert!((*x).is_finite() && (*y).is_finite(), "{tag}");
}
}
}
}
#[test]
fn fuzz_parse_and_layout_never_panics() {
let mut rng = Rng(0x1234_5678_9ABC_DEF0);
for n in 0..10_000u64 {
let mut soup = String::new();
random_token_soup(&mut rng, &mut soup);
check(&soup, "soup");
if n % 1000 == 0 {
check(&soup.repeat(400), "repeated soup");
}
}
for _n in 0..10_000u64 {
let tmpl = VALID[rng.below(VALID.len())];
check(&mutate(&mut rng, tmpl), "mutate");
}
}