summaryrefslogtreecommitdiff
path: root/tests/src/parser.rs
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2020-01-16 17:51:04 +0100
committerLaurenz <laurmaedje@gmail.com>2020-01-16 17:51:04 +0100
commit08b91a265fcda74f5463473938ec33873b49a7f7 (patch)
tree747ac6a0b385a14a4aa5adbc3f21ef7b9653bd78 /tests/src/parser.rs
parent15ad30555bdad8e7b192fdcf7d4543c0d3fb18ce (diff)
Powerful parser testing 🐱‍👤
Diffstat (limited to 'tests/src/parser.rs')
-rw-r--r--tests/src/parser.rs311
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"))
+}