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