mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-12 22:13:05 +00:00
140 lines
5.1 KiB
JavaScript
140 lines
5.1 KiB
JavaScript
/**
|
|
* Pure core for testing-audit metrics (OPS96) — normalization of native
|
|
* runner reports into the audit's per-layer portrait. No I/O here: the CLI
|
|
* (scripts/testing-audit-metrics.mjs) spawns the suites and feeds these
|
|
* functions; the unit spec pins their behavior against fixture payloads.
|
|
*/
|
|
|
|
export const DEFAULT_TOP = 15
|
|
|
|
export const ms = (value) => `${(value / 1000).toFixed(1)}s`
|
|
|
|
/** Vitest's jest-compatible JSON payload starts with this key. */
|
|
const VITEST_JSON_ANCHOR = '{"numTotal'
|
|
|
|
/**
|
|
* Extract the Vitest JSON report from the runner's raw stdout. pnpm echoes the
|
|
* script line BEFORE the JSON and appends its own noise (e.g. the `ELIFECYCLE`
|
|
* notice) AFTER it when the suite is red; tests may also print console blobs
|
|
* with braces around the payload. Walking back from the last `}` finds the
|
|
* payload boundary, so a red suite still yields its portrait.
|
|
*
|
|
* @param {string} stdout raw stdout of `vitest run --reporter=json`
|
|
* @returns {Record<string, unknown>} parsed JSON report
|
|
*/
|
|
export const parseVitestStdout = (stdout) => {
|
|
if (typeof stdout !== 'string' || stdout.length === 0) {
|
|
throw new Error('vitest não produziu relatório JSON no stdout')
|
|
}
|
|
const jsonStart = stdout.indexOf(VITEST_JSON_ANCHOR)
|
|
if (jsonStart === -1) {
|
|
throw new Error('vitest não produziu relatório JSON no stdout')
|
|
}
|
|
let end = stdout.length
|
|
while (end > jsonStart) {
|
|
end = stdout.lastIndexOf('}', end - 1)
|
|
if (end < jsonStart) break
|
|
try {
|
|
return JSON.parse(stdout.slice(jsonStart, end + 1))
|
|
} catch {
|
|
// not the payload boundary yet — keep walking back
|
|
}
|
|
}
|
|
throw new Error('relatório JSON inválido')
|
|
}
|
|
|
|
/** @returns {number} */
|
|
const fileDurationMs = (entry) => {
|
|
if (typeof entry.endTime === 'number' && typeof entry.startTime === 'number') {
|
|
return Math.max(0, entry.endTime - entry.startTime)
|
|
}
|
|
return entry.assertionResults?.reduce((sum, result) => sum + (result.duration ?? 0), 0) ?? 0
|
|
}
|
|
|
|
const slowestAssertion = (entry) =>
|
|
(entry.assertionResults ?? [])
|
|
.filter((result) => typeof result.duration === 'number')
|
|
.reduce(
|
|
(slowest, result) =>
|
|
slowest === null || result.duration > slowest.duration ? result : slowest,
|
|
null,
|
|
)
|
|
|
|
/**
|
|
* Normalize a Vitest JSON reporter payload (jest-compatible shape) into a
|
|
* layer summary: totals plus per-file rows sorted by duration desc.
|
|
*
|
|
* @param {unknown} report parsed JSON from `vitest run --reporter=json`
|
|
* @returns {{
|
|
* totals: { files: number, tests: number, failedTests: number, durationMs: number },
|
|
* rows: Array<{ file: string, tests: number, durationMs: number, slowestTest: string | null, slowestMs: number | null }>,
|
|
* }}
|
|
*/
|
|
export const summarizeVitestReport = (report) => {
|
|
if (!report || !Array.isArray(report.testResults)) {
|
|
throw new Error('invalid vitest JSON report: missing testResults')
|
|
}
|
|
const rows = report.testResults.map((entry) => {
|
|
const slowest = slowestAssertion(entry)
|
|
return {
|
|
file: String(entry.name ?? '(desconhecido)').replace(/^.*tests\//, 'tests/'),
|
|
tests: entry.assertionResults?.length ?? 0,
|
|
durationMs: fileDurationMs(entry),
|
|
slowestTest: slowest ? slowest.title : null,
|
|
slowestMs: slowest ? slowest.duration : null,
|
|
}
|
|
})
|
|
rows.sort((a, b) => b.durationMs - a.durationMs)
|
|
return {
|
|
totals: {
|
|
files: rows.length,
|
|
tests: Number(report.numTotalTests ?? rows.reduce((sum, row) => sum + row.tests, 0)),
|
|
failedTests: Number(report.numFailedTests ?? 0),
|
|
durationMs: rows.reduce((sum, row) => sum + row.durationMs, 0),
|
|
},
|
|
rows,
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Markdown table of the slowest spec files of a layer.
|
|
*
|
|
* @param {ReturnType<typeof summarizeVitestReport>} summary
|
|
* @param {{ top?: number }} [options]
|
|
*/
|
|
export const renderSlowestFilesTable = (summary, { top = DEFAULT_TOP } = {}) => {
|
|
const lines = ['| arquivo | testes | duração | teste mais lento |', '|---|---:|---:|---|']
|
|
for (const row of summary.rows.slice(0, Math.max(0, top))) {
|
|
const slowest =
|
|
row.slowestTest === null ? '—' : `${row.slowestTest} (${ms(row.slowestMs ?? 0)})`
|
|
lines.push(`| ${row.file} | ${row.tests} | ${ms(row.durationMs)} | ${slowest} |`)
|
|
}
|
|
return lines.join('\n')
|
|
}
|
|
|
|
/**
|
|
* Static inventory of an e2e spec source — counting only, never execution.
|
|
* Deliberate approximation: `test.each`/`test.skip` variants are not counted
|
|
* (the audit's portrait is structural, not exhaustive).
|
|
*
|
|
* @param {{ file: string, content: string }} source
|
|
* @returns {{ file: string, describes: number, tests: number }}
|
|
*/
|
|
export const inventoryE2ESource = ({ file, content }) => ({
|
|
file,
|
|
describes: (content.match(/\bdescribe\s*\(/g) ?? []).length,
|
|
tests: (content.match(/\btest\s*\(/g) ?? []).length,
|
|
})
|
|
|
|
/** Markdown table of the static e2e inventory (durations deliberately absent). */
|
|
export const renderE2EInventoryTable = (rows) => {
|
|
const lines = [
|
|
'| arquivo | describes | testes | duração |',
|
|
'|---|---:|---:|---|',
|
|
...rows.map(
|
|
(row) =>
|
|
`| ${row.file} | ${row.describes} | ${row.tests} | não medido — execução noturna proibida |`,
|
|
),
|
|
]
|
|
return lines.join('\n')
|
|
}
|