checklist.ts (5172B)
1 import * as fs from 'fs'; 2 import * as process from 'process'; 3 4 import { DefaultTestFileLoader } from '../internal/file_loader.js'; 5 import { Ordering, compareQueries } from '../internal/query/compare.js'; 6 import { parseQuery } from '../internal/query/parseQuery.js'; 7 import { TestQuery, TestQueryMultiFile } from '../internal/query/query.js'; 8 import { loadTreeForQuery, TestTree } from '../internal/tree.js'; 9 import { StacklessError } from '../internal/util.js'; 10 import { assert } from '../util/util.js'; 11 12 function usage(rc: number): void { 13 console.error('Usage:'); 14 console.error(' tools/checklist FILE'); 15 console.error(' tools/checklist my/list.txt'); 16 process.exit(rc); 17 } 18 19 if (process.argv.length === 2) usage(0); 20 if (process.argv.length !== 3) usage(1); 21 22 type QueryInSuite = { readonly query: TestQuery; readonly done: boolean }; 23 type QueriesInSuite = QueryInSuite[]; 24 type QueriesBySuite = Map<string, QueriesInSuite>; 25 async function loadQueryListFromTextFile(filename: string): Promise<QueriesBySuite> { 26 const lines = (await fs.promises.readFile(filename, 'utf8')).split(/\r?\n/); 27 const allQueries = lines 28 .filter(l => l) 29 .map(l => { 30 const [doneStr, q] = l.split(/\s+/); 31 assert(doneStr === 'DONE' || doneStr === 'TODO', 'first column must be DONE or TODO'); 32 return { query: parseQuery(q), done: doneStr === 'DONE' } as const; 33 }); 34 35 const queriesBySuite: QueriesBySuite = new Map(); 36 for (const q of allQueries) { 37 let suiteQueries = queriesBySuite.get(q.query.suite); 38 if (suiteQueries === undefined) { 39 suiteQueries = []; 40 queriesBySuite.set(q.query.suite, suiteQueries); 41 } 42 43 suiteQueries.push(q); 44 } 45 46 return queriesBySuite; 47 } 48 49 function checkForOverlappingQueries(queries: QueriesInSuite): void { 50 for (let i1 = 0; i1 < queries.length; ++i1) { 51 for (let i2 = i1 + 1; i2 < queries.length; ++i2) { 52 const q1 = queries[i1].query; 53 const q2 = queries[i2].query; 54 if (compareQueries(q1, q2) !== Ordering.Unordered) { 55 console.log(` FYI, the following checklist items overlap:\n ${q1}\n ${q2}`); 56 } 57 } 58 } 59 } 60 61 function checkForUnmatchedSubtreesAndDoneness( 62 tree: TestTree, 63 matchQueries: QueriesInSuite 64 ): number { 65 let subtreeCount = 0; 66 const unmatchedSubtrees: TestQuery[] = []; 67 const overbroadMatches: [TestQuery, TestQuery][] = []; 68 const donenessMismatches: QueryInSuite[] = []; 69 const alwaysExpandThroughLevel = 1; // expand to, at minimum, every file. 70 for (const subtree of tree.iterateCollapsedNodes({ 71 includeIntermediateNodes: true, 72 includeEmptySubtrees: true, 73 alwaysExpandThroughLevel, 74 })) { 75 subtreeCount++; 76 const subtreeDone = !subtree.subtreeCounts?.nodesWithTODO; 77 78 let subtreeMatched = false; 79 for (const q of matchQueries) { 80 const comparison = compareQueries(q.query, subtree.query); 81 if (comparison !== Ordering.Unordered) subtreeMatched = true; 82 if (comparison === Ordering.StrictSubset) continue; 83 if (comparison === Ordering.StrictSuperset) overbroadMatches.push([q.query, subtree.query]); 84 if (comparison === Ordering.Equal && q.done !== subtreeDone) donenessMismatches.push(q); 85 } 86 if (!subtreeMatched) unmatchedSubtrees.push(subtree.query); 87 } 88 89 if (overbroadMatches.length) { 90 // (note, this doesn't show ALL multi-test queries - just ones that actually match any .spec.ts) 91 console.log(` FYI, the following checklist items were broader than one file:`); 92 for (const [q, collapsedSubtree] of overbroadMatches) { 93 console.log(` ${q} > ${collapsedSubtree}`); 94 } 95 } 96 97 if (unmatchedSubtrees.length) { 98 throw new StacklessError(`Found unmatched tests:\n ${unmatchedSubtrees.join('\n ')}`); 99 } 100 101 if (donenessMismatches.length) { 102 throw new StacklessError( 103 'Found done/todo mismatches:\n ' + 104 donenessMismatches 105 .map(q => `marked ${q.done ? 'DONE, but is TODO' : 'TODO, but is DONE'}: ${q.query}`) 106 .join('\n ') 107 ); 108 } 109 110 return subtreeCount; 111 } 112 113 (async () => { 114 console.log('Loading queries...'); 115 const queriesBySuite = await loadQueryListFromTextFile(process.argv[2]); 116 console.log(' Found suites: ' + Array.from(queriesBySuite.keys()).join(' ')); 117 118 const loader = new DefaultTestFileLoader(); 119 for (const [suite, queriesInSuite] of queriesBySuite.entries()) { 120 console.log(`Suite "${suite}":`); 121 console.log(` Checking overlaps between ${queriesInSuite.length} checklist items...`); 122 checkForOverlappingQueries(queriesInSuite); 123 const suiteQuery = new TestQueryMultiFile(suite, []); 124 console.log(` Loading tree ${suiteQuery}...`); 125 const tree = await loadTreeForQuery(loader, suiteQuery, { 126 subqueriesToExpand: queriesInSuite.map(q => q.query), 127 }); 128 console.log(' Found no invalid queries in the checklist. Checking for unmatched tests...'); 129 const subtreeCount = checkForUnmatchedSubtreesAndDoneness(tree, queriesInSuite); 130 console.log(` No unmatched tests or done/todo mismatches among ${subtreeCount} subtrees!`); 131 } 132 console.log(`Checklist looks good!`); 133 })().catch(ex => { 134 console.log(ex.stack ?? ex.toString()); 135 process.exit(1); 136 });