diff options
Diffstat (limited to 'tests/src/parser.rs')
| -rw-r--r-- | tests/src/parser.rs | 311 |
1 files changed, 311 insertions, 0 deletions
diff --git a/tests/src/parser.rs b/tests/src/parser.rs new file mode 100644 index 00000000..ecf1544c --- /dev/null +++ b/tests/src/parser.rs @@ -0,0 +1,311 @@ +use std::fmt::Debug; + +use typstc::func::Scope; +use typstc::size::Size; +use typstc::syntax::*; +use typstc::{function, parse}; + +mod spanless; +use spanless::SpanlessEq; + + +/// The result of a single test case. +enum Case { + Okay, + Failed { + line: usize, + src: &'static str, + expected: String, + found: String, + } +} + +/// Test all tests. +fn test(tests: Vec<(&str, Vec<Case>)>) { + println!(); + + let mut errors = false; + + let len = tests.len(); + println!("Running {} test{}", len, if len > 1 { "s" } else { "" }); + + for (file, cases) in tests { + print!("Testing: {}. ", file); + + let mut okay = 0; + let mut failed = 0; + + for case in cases { + match case { + Case::Okay => okay += 1, + Case::Failed { line, src, expected, found } => { + println!(); + println!(" - Case failed in file {}.rs in line {}.", file, line); + println!(" - Source: {:?}", src); + println!(" - Expected: {}", expected); + println!(" - Found: {}", found); + + failed += 1; + } + } + } + + // Print a small summary. + print!("{} okay, {} failed.", okay, failed); + if failed == 0 { + print!(" ✔") + } else { + errors = true; + } + + println!(); + } + + println!(); + + if errors { + std::process::exit(-1); + } +} + +/// The main test macro. +macro_rules! tokens { + ($($task:ident $src:expr =>($line:expr)=> [$($e:tt)*])*) => ({ + vec![$({ + let (okay, expected, found) = case!($task $src, [$($e)*]); + if okay { + Case::Okay + } else { + Case::Failed { + line: $line, + src: $src, + expected: format(expected), + found: format(found), + } + } + }),*] + }); +} + +//// Indented formatting for failed cases. +fn format(thing: impl Debug) -> String { + format!("{:#?}", thing).replace('\n', "\n ") +} + +/// Evaluates a single test. +macro_rules! case { + (t $($rest:tt)*) => (case!(@tokenize SpanlessEq::spanless_eq, $($rest)*)); + (ts $($rest:tt)*) => (case!(@tokenize PartialEq::eq, $($rest)*)); + + (@tokenize $cmp:expr, $src:expr, [$($e:tt)*]) => ({ + let expected = list!(tokens [$($e)*]); + let found = tokenize($src).collect::<Vec<_>>(); + ($cmp(&found, &expected), expected, found) + }); + + (p $($rest:tt)*) => (case!(@parse SpanlessEq::spanless_eq, $($rest)*)); + (ps $($rest:tt)*) => (case!(@parse PartialEq::eq, $($rest)*)); + + (@parse $cmp:expr, $src:expr, [$($e:tt)*]) => ({ + let expected = SyntaxTree { nodes: list!(nodes [$($e)*]) }; + let found = parse($src, ParseContext { scope: &scope() }).0; + ($cmp(&found, &expected), expected, found) + }); + + (c $src:expr, [$($e:tt)*]) => ({ + let expected = Colorization { tokens: list!(colors [$($e)*]) }; + let found = parse($src, ParseContext { scope: &scope() }).1; + (expected == found, expected, found) + }); + + (e $src:expr, [$($e:tt)*]) => ({ + let expected = ErrorMap { errors: list!([$($e)*]) }; + let found = parse($src, ParseContext { scope: &scope() }).2; + (expected == found, expected, found) + }); +} + +/// A scope containing the `DebugFn` as a fallback. +fn scope() -> Scope { + Scope::with_debug::<DebugFn>() +} + +/// Parses possibly-spanned lists of token or node expressions. +macro_rules! list { + (expr [$($item:expr),* $(,)?]) => ({ + #[allow(unused_imports)] + use cuts::expr::*; + Tuple { items: vec![$(zspan($item)),*] } + }); + + (expr [$($key:expr =>($_:expr)=> $value:expr),* $(,)?]) => ({ + #[allow(unused_imports)] + use cuts::expr::*; + Object { + pairs: vec![$(Pair { + key: zspan(Ident($key.to_string())), + value: zspan($value), + }),*] + } + }); + + ($cut:ident [$($e:tt)*]) => ({ + #[allow(unused_imports)] + use cuts::$cut::*; + list!([$($e)*]) + }); + + ([$(($sl:tt:$sc:tt, $el:tt:$ec:tt, $v:expr)),* $(,)?]) => ({ + vec![ + $(Spanned { v: $v, span: Span { + start: Position { line: $sl, column: $sc }, + end: Position { line: $el, column: $ec }, + }}),* + ] + }); + + ([$($e:tt)*]) => (vec![$($e)*].into_iter().map(zspan).collect::<Vec<_>>()); +} + +/// Composes a function expression. +macro_rules! func { + ($name:expr $(,pos: [$($p:tt)*])? $(,key: [$($k:tt)*])?; $($b:tt)*) => ({ + #![allow(unused_mut, unused_assignments)] + + let mut positional = Tuple::new(); + let mut keyword = Object::new(); + + $(positional = list!(expr [$($p)*]);)? + $(keyword = list!(expr [$($k)*]);)? + + Node::Func(FuncCall(Box::new(DebugFn { + header: FuncHeader { + name: zspan(Ident($name.to_string())), + args: FuncArgs { + positional, + keyword, + }, + }, + body: func!(@body $($b)*), + }))) + }); + + (@body Some($($b:tt)*)) => (Some(SyntaxTree { nodes: list!(nodes $($b)*) })); + (@body None) => (None); +} + +function! { + /// Most functions in the tests are parsed into the debug function for easy + /// inspection of arguments and body. + #[derive(Debug, PartialEq)] + pub struct DebugFn { + header: FuncHeader, + body: Option<SyntaxTree>, + } + + parse(header, body, ctx) { + DebugFn { + header: header.clone(), + body: parse!(optional: body, ctx), + } + } + + layout() { vec![] } +} + +/// Span an element with a zero span. +fn zspan<T>(v: T) -> Spanned<T> { + Spanned { v, span: Span::ZERO } +} + +/// Abbreviations for tokens, nodes, colors and expressions. +#[allow(non_snake_case, dead_code)] +mod cuts { + pub mod tokens { + pub use typstc::syntax::Token::{ + Whitespace as W, + LineComment as LC, + BlockComment as BC, + StarSlash as SS, + LeftBracket as LB, + RightBracket as RB, + LeftParen as LP, + RightParen as RP, + LeftBrace as LBR, + RightBrace as RBR, + Colon as CL, + Comma as CM, + Equals as EQ, + ExprIdent as ID, + ExprStr as STR, + ExprSize as SIZE, + ExprNumber as NUM, + ExprBool as BOOL, + Star as S, + Underscore as U, + Backtick as B, + Text as T, + }; + } + + pub mod nodes { + use typstc::syntax::Node; + + pub use Node::{ + Space as S, + Newline as N, + ToggleItalic as I, + ToggleBolder as B, + ToggleMonospace as M, + }; + + pub fn T(text: &str) -> Node { + Node::Text(text.to_string()) + } + } + + pub mod colors { + pub use typstc::syntax::ColorToken::{ + Comment as C, + Bracket as B, + FuncName as FN, + Colon as CL, + Key as K, + Equals as EQ, + Comma as CM, + Paren as P, + Brace as BR, + ExprIdent as ID, + ExprStr as STR, + ExprNumber as NUM, + ExprSize as SIZE, + ExprBool as BOOL, + Bold as BD, + Italic as IT, + Monospace as MS, + Invalid as INV, + }; + } + + pub mod expr { + use typstc::syntax::{Expression, Ident}; + + pub use Expression::{ + Number as NUM, + Size as SIZE, + Bool as BOOL, + }; + + pub fn ID(text: &str) -> Expression { + Expression::Ident(Ident(text.to_string())) + } + + pub fn STR(text: &str) -> Expression { + Expression::Str(text.to_string()) + } + } +} + +fn main() { + test(include!("../cache/parser-tests.rs")) +} |
