• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1import type { ParserOptions, TreeAdapter, TreeAdapterTypeMap, ParserError } from 'parse5';
2import * as fs from 'node:fs';
3import * as path from 'node:path';
4import * as assert from 'node:assert';
5import { serializeToDatFileFormat } from './serialize-to-dat-file-format.js';
6import { generateTestsForEachTreeAdapter } from './common.js';
7import { parseDatFile, type DatFile } from './parse-dat-file.js';
8
9export interface TreeConstructionTestData<T extends TreeAdapterTypeMap> extends DatFile<T> {
10    idx: number;
11    setName: string;
12    dirName: string;
13}
14
15export function loadTreeConstructionTestData<T extends TreeAdapterTypeMap>(
16    dataDir: URL,
17    treeAdapter: TreeAdapter<T>
18): TreeConstructionTestData<T>[] {
19    const tests: TreeConstructionTestData<T>[] = [];
20
21    const dataDirPath = dataDir.pathname;
22    const testSetFileNames = fs.readdirSync(dataDir);
23    const dirName = path.basename(dataDirPath);
24
25    for (const fileName of testSetFileNames) {
26        if (path.extname(fileName) !== '.dat') {
27            continue;
28        }
29
30        const filePath = path.join(dataDirPath, fileName);
31        const testSet = fs.readFileSync(filePath, 'utf8');
32        const setName = fileName.replace('.dat', '');
33
34        for (const test of parseDatFile(testSet, treeAdapter)) {
35            tests.push({
36                ...test,
37                idx: tests.length,
38                setName,
39                dirName,
40            });
41        }
42    }
43
44    return tests;
45}
46
47function prettyPrintParserAssertionArgs(actual: string, expected: string, chunks?: string[]): string {
48    let msg = '\nExpected:\n';
49
50    msg += '-----------------\n';
51    msg += `${expected}\n`;
52    msg += '\nActual:\n';
53    msg += '-----------------\n';
54    msg += `${actual}\n`;
55
56    if (chunks) {
57        msg += 'Chunks:\n';
58        msg += JSON.stringify(chunks);
59    }
60
61    return msg;
62}
63
64interface ParseMethodOptions<T extends TreeAdapterTypeMap> extends ParserOptions<T> {
65    treeAdapter: TreeAdapter<T>;
66}
67
68interface ParseResult<T extends TreeAdapterTypeMap> {
69    node: T['node'];
70    chunks?: string[];
71}
72
73type ParseMethod<T extends TreeAdapterTypeMap> = (
74    input: TreeConstructionTestData<T>,
75    options: ParseMethodOptions<T>
76) => ParseResult<T> | Promise<ParseResult<T>>;
77
78function createParsingTest<T extends TreeAdapterTypeMap>(
79    test: TreeConstructionTestData<T>,
80    treeAdapter: TreeAdapter<T>,
81    parse: ParseMethod<T>,
82    { withoutErrors, expectError }: { withoutErrors?: boolean; expectError?: boolean } = {}
83): () => Promise<void> {
84    return async (): Promise<void> => {
85        const errs: string[] = [];
86
87        const opts = {
88            scriptingEnabled: test.scriptingEnabled,
89            treeAdapter,
90
91            onParseError: (err: ParserError): void => {
92                let errStr = `(${err.startLine}:${err.startCol}`;
93
94                // NOTE: use ranges for token errors
95                if (err.startLine !== err.endLine || err.startCol !== err.endCol) {
96                    errStr += `-${err.endLine}:${err.endCol}`;
97                }
98
99                errStr += `) ${err.code}`;
100
101                errs.push(errStr);
102            },
103        };
104
105        const { node, chunks } = await parse(test, opts);
106        const actual = serializeToDatFileFormat(node, opts.treeAdapter);
107        const msg = prettyPrintParserAssertionArgs(actual, test.expected, chunks);
108        let sawError = false;
109
110        try {
111            assert.ok(actual === test.expected, msg);
112
113            if (!withoutErrors) {
114                assert.deepEqual(errs.sort(), test.expectedErrors.sort());
115            }
116        } catch (error) {
117            if (expectError) {
118                return;
119            }
120            sawError = true;
121
122            throw error;
123        }
124
125        if (!sawError && expectError) {
126            throw new Error(`Expected error but none was thrown`);
127        }
128    };
129}
130
131// TODO: Stop using the fork here.
132const treePath = new URL('../data/html5lib-tests-fork/tree-construction', import.meta.url);
133
134export function generateParsingTests(
135    name: string,
136    prefix: string,
137    {
138        withoutErrors,
139        expectErrors: expectError = [],
140        suitePath = treePath,
141    }: { withoutErrors?: boolean; expectErrors?: string[]; suitePath?: URL },
142    parse: ParseMethod<TreeAdapterTypeMap>
143): void {
144    generateTestsForEachTreeAdapter(name, (treeAdapter) => {
145        const errorsToExpect = new Set(expectError);
146
147        for (const test of loadTreeConstructionTestData(suitePath, treeAdapter)) {
148            const expectError = errorsToExpect.delete(`${test.idx}.${test.setName}`);
149
150            it(
151                `${prefix}(${test.dirName}) - ${test.idx}.${test.setName} - \`${test.input}\` (line ${test.lineNum})`,
152                createParsingTest<TreeAdapterTypeMap>(test, treeAdapter, parse, {
153                    withoutErrors,
154                    expectError,
155                })
156            );
157        }
158
159        if (errorsToExpect.size > 0) {
160            throw new Error(`Expected errors were not found: ${[...errorsToExpect].join(', ')}`);
161        }
162    });
163}
164