189 lines
5.5 KiB
Rust
189 lines
5.5 KiB
Rust
//! 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:?}"),
|
||
}
|
||
}
|