import { readdirSync, readFileSync } from 'node:fs'; import { join, relative } from 'node:path'; function suiteName(relFile) { const stem = relFile.replace(/^test\//, '').replace(/\.test\.mjs$/, ''); const words = stem.replace(/\//g, ': ').replace(/-/g, ' '); return words.charAt(0).toUpperCase() + words.slice(1); } export function scanSuites(rootDir) { const testDir = join(rootDir, 'test'); const files = []; const walk = (dir) => { let entries; try { entries = readdirSync(dir, { withFileTypes: true }); } catch { return; } for (const e of entries) { if (e.isDirectory()) walk(join(dir, e.name)); else if (e.name.endsWith('.test.mjs')) files.push(join(dir, e.name)); } }; walk(testDir); return files.sort().map((abs) => { const file = relative(rootDir, abs).split('\\').join('/'); const src = readFileSync(abs, 'utf8'); const cases = []; for (const m of src.matchAll(/^[ \t]*test\(\s*(['"`])((?:\\.|(?!\1)[\s\S])*?)\1/gm)) { cases.push(m[2].replace(/\\(['"`])/g, '$1')); } return { name: suiteName(file), file, cases: [...new Set(cases)] }; }).filter((s) => s.cases.length); } export function caseResults(shards) { const newest = new Map(); for (const shard of shards) { const held = newest.get(shard.id); if (!held || (shard.attempt ?? 1) > (held.attempt ?? 1)) newest.set(shard.id, shard); } const files = new Map(); const entry = (file) => files.get(file) ?? files.set(file, { ok: false, runs: new Map() }).get(file); for (const shard of newest.values()) { for (const s of shard.summaries ?? []) if (s.success) entry(s.file).ok = true; for (const c of shard.cases ?? []) { const runs = entry(c.file).runs; runs.set(c.name, [...(runs.get(c.name) ?? []), c.pass]); } } return new Map([...files].map(([file, { ok, runs }]) => [file, { ok, cases: new Map([...runs].map(([name, passes]) => [name, passes.every(Boolean) ? 'Passing' : passes.some(Boolean) ? 'Flaky' : 'Failing'])), }])); } function statusFor(results, file, name) { const r = results?.get(file); if (!r) return null; return r.cases.get(name) ?? (r.ok ? 'Passing' : null); } export function planQualityMirror(rows, scanned, results = null) { const summary = { suites: scanned.length, createdSuites: 0, createdCases: 0, removedSuites: 0, removedCases: 0, renamedSuites: 0, statusChanges: 0, missingFiles: [] }; const ops = []; const byFile = new Map(); const doomed = []; for (const r of [...rows].sort((a, b) => a.publicId - b.publicId)) { if (byFile.has(r.file)) doomed.push(r); else byFile.set(r.file, r); } for (const s of scanned) { const row = byFile.get(s.file); if (!row) { summary.createdSuites += 1; summary.createdCases += s.cases.length; ops.push({ op: 'createSuite', name: s.name, file: s.file, cases: s.cases.map((name) => ({ name, status: statusFor(results, s.file, name) })) }); continue; } if (row.name !== s.name) { summary.renamedSuites += 1; ops.push({ op: 'rename', id: row.id, name: s.name }); } const have = new Map(row.cases.map((c) => [c.name, c])); for (const name of new Set(s.cases)) { const status = statusFor(results, s.file, name); const held = have.get(name); if (!held) { summary.createdCases += 1; ops.push({ op: 'createCase', suite: row.id, name, status }); } else if (status && held.status !== status) { summary.statusChanges += 1; ops.push({ op: 'status', id: held.id, status }); } } const want = new Set(s.cases); for (const c of row.cases) { if (want.has(c.name)) continue; summary.removedCases += 1; ops.push({ op: 'deleteCase', id: c.id }); } } const scannedFiles = new Set(scanned.map((s) => s.file)); for (const [file, row] of byFile) { if (scannedFiles.has(file)) continue; summary.missingFiles.push(file); doomed.push(row); } for (const row of doomed) { summary.removedSuites += 1; summary.removedCases += row.cases.length; for (const c of row.cases) ops.push({ op: 'deleteCase', id: c.id }); ops.push({ op: 'deleteSuite', id: row.id }); } return { ops, summary }; } export function syncQualityMirror(w, scanned, { dryRun = false, results = null } = {}) { const suiteTable = w.getTable('Quality/Suite'); const caseTable = w.getTable('Quality/Case'); const fileField = Object.values(suiteTable.fields).find((f) => f.name === 'File'); const rows = w.listEntities(suiteTable.id).map((r) => ({ id: r.id, publicId: r.publicId, name: w.entityName(r), file: r.values[fileField.id], cases: w.readEntity(r.id).fields.Cases.map((c) => ({ id: c.id, name: c.name, status: w.readEntity(c.id).fields.Status })), })); const { ops, summary } = planQualityMirror(rows, scanned, results); if (dryRun) return summary; for (const o of ops) { if (o.op === 'createSuite') { const row = w.createEntity(suiteTable, { name: o.name, values: { File: o.file } }); for (const c of o.cases) w.createEntity(caseTable, { name: c.name, values: { Suite: row.id, ...(c.status ? { Status: c.status } : {}) } }); } else if (o.op === 'createCase') w.createEntity(caseTable, { name: o.name, values: { Suite: o.suite, ...(o.status ? { Status: o.status } : {}) } }); else if (o.op === 'rename') w.updateEntity(o.id, { Name: o.name }); else if (o.op === 'status') w.updateEntity(o.id, { Status: o.status }); else w.deleteEntity(o.id, { hard: true }); } return summary; }