//! 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:?}"), } } #[test] fn named_operators_parse() { for src in [ r"\log(xy)=\log x+\log y,\qquad \log\frac{x}{y}=\log x-\log y", r"\log_b x=\frac{\ln x}{\ln b}", r"\operatorname{Var}(X)=a\bmod b,\ a\equiv 1\pmod{n},\ \iint_D f\,dA\xrightarrow{d}N(0,1)", r"\det\begin{pmatrix} a & b \\ c & d \end{pmatrix}=ad-bc", ] { assert!(parse(src).is_ok(), "{src}"); } }