Files
notescraft/crates/math/tests/errors.rs
T
2026-10-14 22:39:13 -03:00

189 lines
5.5 KiB
Rust
Raw 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.
//! Error cases with byte positions (positions computed with `str::find`, never by hand).
use notescraft_math::{Error, parse};
fn err(src: &str) -> Error {
match parse(src) {
Ok(_) => panic!("expected error for {src:?}"),
Err(e) => e,
}
}
fn pos_of(src: &str, needle: &str) -> usize {
src.find(needle).unwrap_or(0)
}
fn unknown(src: &str) {
let e = err(src);
let Error::Unknown { pos, .. } = e else { panic!("{src:?}: {e:?}") };
assert_eq!(pos, pos_of(src, "\\"), "{src:?}");
}
#[test]
fn unknown_commands_carry_backslash_position() {
unknown("\\frax");
unknown("a\\foo{b}");
unknown("\\underbrace");
}
#[test]
fn unbalanced_braces() {
let e = err("{x");
let Error::Unbalanced { pos } = e else { panic!("{e:?}") };
assert_eq!(pos, 0);
let e = err("\\sqrt{x");
let Error::Unbalanced { pos } = e else { panic!("{e:?}") };
assert_eq!(pos, 5);
let e = err("ab{c{d}");
let Error::Unbalanced { pos } = e else { panic!("{e:?}") };
assert_eq!(pos, pos_of("ab{c{d}", "{"));
}
#[test]
fn unexpected_tokens() {
let e = err("x}");
let Error::Unexpected { what, pos } = e else { panic!("{e:?}") };
assert_eq!(what, "}");
assert_eq!(pos, 1);
let e = err("a & b");
let Error::Unexpected { what, pos } = e else { panic!("{e:?}") };
assert_eq!(what, "&");
assert_eq!(pos, pos_of("a & b", "&"));
}
#[test]
fn left_right_errors() {
let e = err("\\left(x");
let Error::MissingRight { pos } = e else { panic!("{e:?}") };
assert_eq!(pos, 0);
let e = err("\\right)");
let Error::Unexpected { what, .. } = e else { panic!("{e:?}") };
assert_eq!(what, "\\right outside \\left..\\right");
let e = err("\\left(x\\right");
let Error::Unexpected { what, pos } = e else { panic!("{e:?}") };
assert_eq!(what, "missing delimiter");
assert_eq!(pos, pos_of("\\left(x\\right", "\\right"));
}
#[test]
fn script_errors() {
let e = err("x^^y");
let Error::DoubleScript { pos } = e else { panic!("{e:?}") };
assert_eq!(pos, 2);
let e = err("x^");
let Error::Unexpected { what, pos } = e else { panic!("{e:?}") };
assert_eq!(what, "missing script argument");
assert_eq!(pos, 1);
let e = err("x__y");
let Error::DoubleScript { pos } = e else { panic!("{e:?}") };
assert_eq!(pos, 2);
}
#[test]
fn command_argument_errors() {
let e = err("\\frac{a}");
let Error::Unexpected { what, pos } = e else { panic!("{e:?}") };
assert_eq!(what, "missing command argument");
assert_eq!(pos, 0);
let e = err("\\hat");
let Error::Unexpected { what, pos } = e else { panic!("{e:?}") };
assert_eq!(what, "missing command argument");
assert_eq!(pos, 0);
}
#[test]
fn environment_errors() {
let e = err("\\begin{matrix} x \\end{pmatrix}");
let Error::EnvMismatch { begin, end, pos } = e else { panic!("{e:?}") };
assert_eq!(begin, "matrix");
assert_eq!(end, "pmatrix");
assert_eq!(pos, pos_of("\\begin{matrix} x \\end{pmatrix}", "\\end"));
let e = err("\\begin{matrix} x");
let Error::Unbalanced { pos } = e else { panic!("{e:?}") };
// The unclosed `{` opened at byte 6.
assert_eq!(pos, 6);
let e = err("\\begin{qmatrix} x \\end{qmatrix}");
let Error::Unknown { cmd, pos } = e else { panic!("{e:?}") };
assert_eq!(cmd, "qmatrix");
assert_eq!(pos, 0);
let e = err("\\begin x\\end x");
match e {
Error::Unexpected { what, .. } => assert_eq!(what, "\\begin environment name"),
other => panic!("{other:?}"),
}
}
#[test]
fn limits_are_bounded() {
// Depth: 65 nested groups exceed the limit.
let deep = "{".repeat(65);
let e = err(&deep);
let Error::DepthLimit { pos } = e else { panic!("{e:?}") };
assert_eq!(pos, 64);
// 63 deep with matching closes parses fine.
let mut ok = "{".repeat(63);
ok.push_str(&"}".repeat(63));
assert!(parse(&ok).is_ok(), "63 deep should parse");
}
#[test]
fn input_cap() {
let src = "x".repeat(20_001);
let e = err(&src);
let Error::TooLong { len } = e else { panic!("{e:?}") };
assert_eq!(len, 20_001);
let src = "x".repeat(20_000);
assert!(parse(&src).is_ok(), "exactly 20000 should parse");
}
#[test]
fn lone_and_escaped_tokens() {
// `\\` outside an environment: unknown command `\\`.
let e = err("\\\\");
let Error::Unknown { cmd, pos } = e else { panic!("{e:?}") };
assert_eq!(cmd, "\\");
assert_eq!(pos, 0);
// Empty input parses to an empty row.
match parse("") {
Ok(notescraft_math::Node::Row(list)) => assert_eq!(list, []),
other => panic!("{other:?}"),
}
}
#[test]
fn errors_do_not_consume_past_positions() {
// `αé`: the first non-ASCII source char is rejected (`Unexpected`), and the position
// is the BYTE offset: `α` spans 2 bytes, so `é`... wait, `α` itself errors first at
// byte 0. To prove byte offsets, consume multibytes through `\text{}` (legal), then
// error: the `\\` after `\text{a é}` is at byte 10 (char index 9 would be wrong).
match parse("αé") {
Err(Error::Unexpected { what, pos }) => {
assert_eq!(what, "α");
assert_eq!(pos, 0);
}
other => panic!("{other:?}"),
}
let src = "\\text{a é}\\\\foo";
match parse(src) {
Err(Error::Unknown { pos, cmd: _ }) => {
// Byte 11 (the `}` is byte 10, then the first `\\`); a char-index
// implementation would report 10.
assert_eq!(pos, pos_of(src, "\\\\foo"));
assert_eq!(pos, 11);
}
other => panic!("{other:?}"),
}
}