Testing Example

When working on a grammar it can be useful to have a set of tests that verify that it parses various input in the intended way.

The way the parser maintained alongside Lezer do this is through a module exported by @lezer/generator, which can compare a parse tree to a string describing a tree, and tell you when they differ.

Let's use this to test the mini-JavaScript parser from the other example.

import {parser} from "./javascript-parser.js"
import {testTree} from "@lezer/generator/test"

let tree = parser.parse("function plus1(a) { return a + 1 }")
let spec = `Script(FunctionDeclaration(
  function,
  Identifier,
  ParamList(Identifier),
  Block(
    ReturnStatement(
      return,
      BinaryExpression(Identifier, ArithOp, Number)))))`
testTree(tree, spec)

If the tree matches the spec, the function will return normally. If not, it raises an error describing the mismatch.

By default, you can leave tokens whose names are not words (like "(" and "{") out of the spec string, though you can also include them if you want to ensure they are present. Error nodes can be included as a character. When a node's child list is specified a (...), its children will not be looked at, which can be useful for abbreviating uninteresting parts of the tree.

To help with setting up a collection of tests, the @lezer/generator/test module also exports a fileTests function that, given a file in a format like below, returns an array of tests.

# Block comments

/* A */
let x /* B
 C */ = 1

==>

Script(
  BlockComment,
  VariableDeclaration(let, Identifier, BlockComment, "=", Number))

# Semicolon insertion

let x
x()

==>

Script(
  VariableDeclaration(let, Identifier),
  ExpressionStatement(CallExpression(Identifier, ArgList)))

Each test has a name property holding the name written after the # sign, and a run property holding a function that, given a parser, parses the test's text (before the ==> marker) and checks whether the tree matches the spec after the marker.

If a test requires additional parser configuration (as in LRParser.configure), you can put a JSON object with configuration options after the test title.

# A test {"dialect": "somedialect"}

To load such a test suite and wire it up to a test runner like Mocha, you do something like this in Node.

import {parser} from "./parser.js"
import {fileTests} from "@lezer/generator/test"
import {readdirSync, readFileSync} from "fs"
import {join} from "path"

export function parseTests(dir) {
  for (let file of readdirSync(dir)) {
    let tests = fileTests(readFileSync(join(dir, file), "utf8"), file)
    describe(file, () => {
      for (let {name, run} of tests) it(name, () => run(parser))
    })
  }
}