Files
ArtPlayer/scripts/coverage-report.mjs

59 lines
3.3 KiB
JavaScript

import assert from 'node:assert/strict'
import { createHash } from 'node:crypto'
import fs from 'node:fs'
import path from 'node:path'
import ts from 'typescript'
const walk = directory => fs.readdirSync(directory, { withFileTypes: true }).flatMap(entry => entry.isDirectory() ? walk(path.join(directory, entry.name)) : [path.join(directory, entry.name)])
const relative = (root, file) => path.relative(root, file).split(path.sep).join('/')
export function sourceInventory(root, policy) {
assert.equal(policy.schemaVersion, 1)
assert(policy.packages.length > 0)
const files = []
const excluded = []
for (const name of policy.packages) {
assert(/^artplayer(?:-[a-z-]+)?$/.test(name), `Invalid coverage package: ${name}`)
for (const file of walk(path.join(root, 'packages', name, 'src')).filter(file => /\.[jt]s$/.test(file))) {
const name = relative(root, file)
const source = fs.readFileSync(file, 'utf8').replaceAll('\r\n', '\n')
const sha256 = createHash('sha256').update(source).digest('hex')
const emitted = file.endsWith('.d.ts') ? '' : ts.transpileModule(source, { compilerOptions: { target: ts.ScriptTarget.ESNext, module: ts.ModuleKind.ESNext } }).outputText.trim()
const reason = policy.exclude[name] || (/^(?:export\s*\{\s*\}\s*;?)?$/.test(emitted) ? 'Type-only module; no emitted runtime code.' : undefined)
if (reason) {
excluded.push({ file: name, sha256, reason })
}
else {
assert(!/\b(?:c8|v8|istanbul)\s+ignore\b/i.test(source), `Undocumented coverage suppression: ${name}`)
files.push({ file: name, sha256 })
}
}
}
for (const name of Object.keys(policy.exclude))
assert(excluded.some(item => item.file === name), `Stale coverage exclusion: ${name}`)
for (const name of Object.keys(policy.minimum))
assert(files.some(item => item.file === name), `Critical coverage file is absent or excluded: ${name}`)
return { files: files.sort((a, b) => a.file.localeCompare(b.file)), excluded }
}
export function analyzeCoverage(root, summary, inventory, policy) {
const normalized = new Map(Object.entries(summary).filter(([file]) => file !== 'total').map(([file, value]) => [relative(root, file), value]))
assert.deepEqual([...normalized.keys()].sort(), inventory.files.map(item => item.file).sort(), 'Coverage source inventory differs; a source map or zero-coverage file is missing')
const violations = []
const files = inventory.files.map((source) => {
const metrics = normalized.get(source.file)
const values = {}
for (const metric of ['lines', 'branches', 'functions', 'statements']) {
const { covered, total, skipped } = metrics[metric]
assert(Number.isInteger(total) && total >= 0 && Number.isInteger(covered) && covered >= 0 && covered <= total && skipped === 0, `Invalid or ignored ${metric}: ${source.file}`)
const percent = total ? covered * 100 / total : 100
values[metric] = { covered, total, percent }
const minimum = policy.minimum[source.file]?.[metric]
if (minimum !== undefined && (total === 0 || percent < minimum))
violations.push({ file: source.file, metric, minimum, actual: percent, total })
}
return { ...source, metrics: values }
})
return { files, excluded: inventory.excluded, minimum: policy.minimum, violations }
}