Skip to content

Commit f10099b

Browse files
committed
Add boilerplate for automated testing of syntax grammar
1 parent cd936f1 commit f10099b

9 files changed

Lines changed: 233 additions & 5 deletions

File tree

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
name: "StrictDoc.tmLanguage on Linux"
2+
3+
on:
4+
pull_request:
5+
branches: [ "**" ]
6+
7+
jobs:
8+
build:
9+
runs-on: ubuntu-latest
10+
11+
steps:
12+
- uses: actions/checkout@v3
13+
14+
- name: Set up Node.js
15+
uses: actions/setup-node@v4
16+
with:
17+
node-version: '22'
18+
19+
- name: Set up Python
20+
uses: actions/setup-python@v1
21+
with:
22+
python-version: 3.12
23+
24+
- name: Upgrade pip
25+
run: |
26+
python -m pip install --upgrade pip
27+
28+
- name: Install minimal Python packages
29+
run: |
30+
pip install -r requirements.txt
31+
32+
- name: Install Node packages
33+
run: |
34+
npm install
35+
36+
- name: Run tests
37+
run: |
38+
invoke test

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
11
.idea
22
node_modules
3+
package-lock.json
34
*.vsix
45
/_*
6+
7+
# tests/integration
8+
.lit_test_times.txt
9+
**/Output/**
10+

package.json

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,17 @@
3333
"configuration": "./language-configuration.json"
3434
}
3535
],
36-
"grammars": [{
37-
"language": "sdoc",
38-
"scopeName": "source.sdoc",
39-
"path": "./syntaxes/sdoc.tmLanguage.json"
40-
}]
36+
"grammars": [
37+
{
38+
"language": "sdoc",
39+
"scopeName": "source.sdoc",
40+
"path": "./syntaxes/sdoc.tmLanguage.json"
41+
}
42+
]
43+
},
44+
"dependencies": {
45+
"onigasm": "^2.2.5",
46+
"vscode-textmate": "^9.2.0",
47+
"vscode-oniguruma": "^1.5.1"
4148
}
4249
}

parse_syntax.js

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
const fs = require('fs');
2+
const path = require('path');
3+
const vsctm = require('vscode-textmate');
4+
const oniguruma = require('vscode-oniguruma');
5+
6+
const wasmBin = fs.readFileSync(path.join(__dirname, './node_modules/vscode-oniguruma/release/onig.wasm')).buffer;
7+
const vscodeOnigurumaLib = oniguruma.loadWASM(wasmBin).then(() => {
8+
return {
9+
createOnigScanner(patterns) { return new oniguruma.OnigScanner(patterns); },
10+
createOnigString(s) { return new oniguruma.OnigString(s); }
11+
};
12+
});
13+
14+
const scopeName = "source.sdoc";
15+
const grammarPath = path.join(__dirname, "syntaxes/sdoc.tmLanguage.json");
16+
const filePath = process.argv[2];
17+
if (!fs.existsSync(filePath)) {
18+
throw('File does NOT exist');
19+
}
20+
21+
// Create a registry that can create a grammar from a scope name.
22+
const registry = new vsctm.Registry({
23+
onigLib: vscodeOnigurumaLib,
24+
loadGrammar: (scope) => {
25+
if (scope === scopeName) {
26+
const grammarData = fs.readFileSync(grammarPath, 'utf-8');
27+
return Promise.resolve(vsctm.parseRawGrammar(grammarData, grammarPath));
28+
}
29+
return null;
30+
}
31+
});
32+
33+
registry.loadGrammar(scopeName).then(grammar => {
34+
const lines = fs.readFileSync(filePath, 'utf-8').split(/\r?\n/);
35+
let ruleStack = vsctm.INITIAL;
36+
37+
lines.forEach((line, lineIndex) => {
38+
const lineTokens = grammar.tokenizeLine(line, ruleStack);
39+
ruleStack = lineTokens.ruleStack;
40+
41+
lineTokens.tokens.forEach(token => {
42+
const tokenText = line.slice(token.startIndex, token.endIndex);
43+
console.log(`[${lineIndex + 1}:${token.startIndex}-${token.endIndex}] "${tokenText}" → ${token.scopes.join(' ')}`);
44+
});
45+
});
46+
});

requirements.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
invoke
2+
lit
3+
filecheck>=0.0.20,<1.0.0

tasks.py

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
# Invoke is broken on Python 3.11
2+
# https://github.com/pyinvoke/invoke/issues/833#issuecomment-1293148106
3+
import inspect
4+
import os
5+
import re
6+
import shutil
7+
import sys
8+
import tempfile
9+
from enum import Enum
10+
from pathlib import Path
11+
from typing import Dict, Optional
12+
13+
if not hasattr(inspect, "getargspec"):
14+
inspect.getargspec = inspect.getfullargspec
15+
16+
import invoke
17+
from invoke import task
18+
19+
# Specifying encoding because Windows crashes otherwise when running Invoke
20+
# tasks below:
21+
# UnicodeEncodeError: 'charmap' codec can't encode character '\ufffd'
22+
# in position 16: character maps to <undefined>
23+
# People say, it might also be possible to export PYTHONIOENCODING=utf8 but this
24+
# seems to work.
25+
# FIXME: If you are a Windows user and expert, please advise on how to do this
26+
# properly.
27+
sys.stdout = open(1, "w", encoding="utf-8", closefd=False, buffering=1)
28+
29+
30+
def run_invoke(
31+
context,
32+
cmd,
33+
environment: Optional[dict] = None,
34+
pty: bool = False,
35+
warn: bool = False,
36+
) -> invoke.runners.Result:
37+
def one_line_command(string):
38+
return re.sub("\\s+", " ", string).strip()
39+
40+
return context.run(
41+
one_line_command(cmd),
42+
env=environment,
43+
hide=False,
44+
warn=warn,
45+
pty=pty,
46+
echo=True,
47+
)
48+
49+
50+
@task(aliases=["ti"])
51+
def test_integration(
52+
context,
53+
focus=None,
54+
debug=False,
55+
no_parallelization=False,
56+
fail_first=False,
57+
):
58+
clean_itest_artifacts(context)
59+
60+
cwd = os.getcwd()
61+
62+
parse_syntax_script = f'node \\"{cwd}/parse_syntax.js\\"'
63+
64+
debug_opts = "-vv --show-all" if debug else ""
65+
focus_or_none = f"--filter {focus}" if focus else ""
66+
fail_first_argument = "--max-failures 1" if fail_first else ""
67+
parallelize_opts = "" if not no_parallelization else "--threads 1"
68+
test_folder = f"{cwd}/tests/integration"
69+
70+
itest_command = f"""
71+
lit
72+
--param PARSE_SYNTAX_EXEC="{parse_syntax_script}"
73+
-v
74+
{debug_opts}
75+
{focus_or_none}
76+
{fail_first_argument}
77+
{parallelize_opts}
78+
{test_folder}
79+
"""
80+
run_invoke(
81+
context,
82+
itest_command,
83+
)
84+
85+
@task
86+
def clean_itest_artifacts(context):
87+
# The command sometimes exits with 1 even if the files are deleted.
88+
# warn=True ensures that the execution continues.
89+
run_invoke(
90+
context,
91+
"""
92+
git clean -dX --force --quiet tests/integration/
93+
""",
94+
warn=True,
95+
)

tests/integration/lit.cfg.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# ruff: noqa: F821
2+
3+
import os
4+
import sys
5+
from typing import Any
6+
7+
import lit.formats
8+
9+
config: Any
10+
lit_config: Any
11+
12+
config.name = "StrictDoc integration tests"
13+
config.test_format = lit.formats.ShTest("0")
14+
config.suffixes = [".itest"]
15+
16+
current_dir = os.getcwd()
17+
18+
parse_syntax_exec = lit_config.params["PARSE_SYNTAX_EXEC"]
19+
20+
# NOTE: All substitutions work for the RUN: statements but they don't for CHECK:.
21+
# That's how LLVM LIT works.
22+
config.substitutions.append(("%THIS_TEST_FOLDER", '$(basename "%S")'))
23+
24+
config.substitutions.append(("%parse_syntax", parse_syntax_exec))
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[DOCUMENT]
2+
TITLE: Document Title
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
RUN: %parse_syntax %S/sample.sdoc | filecheck %s
2+
3+
CHECK: [1:0-10] "[DOCUMENT]" → source.sdoc keyword.sdoc
4+
CHECK: [2:0-5] "TITLE" → source.sdoc keyword.control.sdoc keyword.control.sdoc
5+
CHECK: [2:5-7] ": " → source.sdoc keyword.control.sdoc
6+
CHECK: [2:7-22] "Document Title" → source.sdoc keyword.control.sdoc string.sdoc
7+
CHECK: [3:0-1] "" → source.sdoc

0 commit comments

Comments
 (0)