import type { ParserOptions, TreeAdapter, TreeAdapterTypeMap, ParserError } from 'parse5'; import * as fs from 'node:fs'; import * as path from 'node:path'; import * as assert from 'node:assert'; import { serializeToDatFileFormat } from './serialize-to-dat-file-format.js'; import { generateTestsForEachTreeAdapter } from './common.js'; import { parseDatFile, type DatFile } from './parse-dat-file.js'; export interface TreeConstructionTestData extends DatFile { idx: number; setName: string; dirName: string; } export function loadTreeConstructionTestData( dataDir: URL, treeAdapter: TreeAdapter ): TreeConstructionTestData[] { const tests: TreeConstructionTestData[] = []; const dataDirPath = dataDir.pathname; const testSetFileNames = fs.readdirSync(dataDir); const dirName = path.basename(dataDirPath); for (const fileName of testSetFileNames) { if (path.extname(fileName) !== '.dat') { continue; } const filePath = path.join(dataDirPath, fileName); const testSet = fs.readFileSync(filePath, 'utf8'); const setName = fileName.replace('.dat', ''); for (const test of parseDatFile(testSet, treeAdapter)) { tests.push({ ...test, idx: tests.length, setName, dirName, }); } } return tests; } function prettyPrintParserAssertionArgs(actual: string, expected: string, chunks?: string[]): string { let msg = '\nExpected:\n'; msg += '-----------------\n'; msg += `${expected}\n`; msg += '\nActual:\n'; msg += '-----------------\n'; msg += `${actual}\n`; if (chunks) { msg += 'Chunks:\n'; msg += JSON.stringify(chunks); } return msg; } interface ParseMethodOptions extends ParserOptions { treeAdapter: TreeAdapter; } interface ParseResult { node: T['node']; chunks?: string[]; } type ParseMethod = ( input: TreeConstructionTestData, options: ParseMethodOptions ) => ParseResult | Promise>; function createParsingTest( test: TreeConstructionTestData, treeAdapter: TreeAdapter, parse: ParseMethod, { withoutErrors, expectError }: { withoutErrors?: boolean; expectError?: boolean } = {} ): () => Promise { return async (): Promise => { const errs: string[] = []; const opts = { scriptingEnabled: test.scriptingEnabled, treeAdapter, onParseError: (err: ParserError): void => { let errStr = `(${err.startLine}:${err.startCol}`; // NOTE: use ranges for token errors if (err.startLine !== err.endLine || err.startCol !== err.endCol) { errStr += `-${err.endLine}:${err.endCol}`; } errStr += `) ${err.code}`; errs.push(errStr); }, }; const { node, chunks } = await parse(test, opts); const actual = serializeToDatFileFormat(node, opts.treeAdapter); const msg = prettyPrintParserAssertionArgs(actual, test.expected, chunks); let sawError = false; try { assert.ok(actual === test.expected, msg); if (!withoutErrors) { assert.deepEqual(errs.sort(), test.expectedErrors.sort()); } } catch (error) { if (expectError) { return; } sawError = true; throw error; } if (!sawError && expectError) { throw new Error(`Expected error but none was thrown`); } }; } // TODO: Stop using the fork here. const treePath = new URL('../data/html5lib-tests-fork/tree-construction', import.meta.url); export function generateParsingTests( name: string, prefix: string, { withoutErrors, expectErrors: expectError = [], suitePath = treePath, }: { withoutErrors?: boolean; expectErrors?: string[]; suitePath?: URL }, parse: ParseMethod ): void { generateTestsForEachTreeAdapter(name, (treeAdapter) => { const errorsToExpect = new Set(expectError); for (const test of loadTreeConstructionTestData(suitePath, treeAdapter)) { const expectError = errorsToExpect.delete(`${test.idx}.${test.setName}`); it( `${prefix}(${test.dirName}) - ${test.idx}.${test.setName} - \`${test.input}\` (line ${test.lineNum})`, createParsingTest(test, treeAdapter, parse, { withoutErrors, expectError, }) ); } if (errorsToExpect.size > 0) { throw new Error(`Expected errors were not found: ${[...errorsToExpect].join(', ')}`); } }); }