Files
ArtPlayer/test/coverage.test.js

127 lines
7.3 KiB
JavaScript

import assert from 'node:assert/strict'
import { spawnSync } from 'node:child_process'
import fs from 'node:fs'
import { createRequire } from 'node:module'
import path from 'node:path'
import process from 'node:process'
// eslint-disable-next-line test/no-import-node-test -- Exercise the real V8 collection and remapping pipeline.
import { test } from 'node:test'
import { fileURLToPath, pathToFileURL } from 'node:url'
import { normalizeCoverageReports } from '../scripts/coverage-maps.mjs'
import { analyzeCoverage, sourceInventory } from '../scripts/coverage-report.mjs'
const root = fileURLToPath(new URL('../', import.meta.url))
const require = createRequire(import.meta.url)
const read = file => JSON.parse(fs.readFileSync(file, 'utf8'))
const write = (file, value) => fs.writeFileSync(file, `${JSON.stringify(value)}\n`)
test('real TS source maps retain unexecuted branches and files across both test loaders', () => {
const cache = path.join(root, 'refactor/.cache/coverage')
fs.mkdirSync(cache, { recursive: true })
const fixture = fs.mkdtempSync(path.join(cache, 'canary-'))
const source = path.join(fixture, 'packages/artplayer/src')
fs.mkdirSync(source, { recursive: true })
fs.writeFileSync(path.join(source, 'probe.ts'), `export default function probe(value: boolean): string {
if (value) {
return 'taken'
}
return 'missed'
}
`)
fs.writeFileSync(path.join(source, 'support.ts'), 'export default function support(): number { return 42 }\n')
fs.writeFileSync(path.join(source, 'index.ts'), 'export { default } from \'./probe\'\n')
fs.writeFileSync(path.join(source, 'never.ts'), 'export default function never(): number { return 99 }\n')
fs.writeFileSync(path.join(source, 'types.ts'), 'export interface Example { value: boolean }\n')
const entry = path.join(fixture, 'run.mjs')
const loader = pathToFileURL(path.join(root, 'test/helpers/load.js')).href
fs.writeFileSync(entry, `import assert from 'node:assert/strict'
import { loadModules, loadPackage } from ${JSON.stringify(loader)}
const { probe, support } = await loadModules({ probe: 'packages/artplayer/src/probe', support: 'packages/artplayer/src/support' }, ${JSON.stringify(fixture)})
assert.equal(support(), 42)
const packaged = (await loadPackage('artplayer', ${JSON.stringify(fixture)})).default
for (const fn of [probe, packaged]) {
assert.equal(fn(true), 'taken')
if (process.argv[2] === 'complete') assert.equal(fn(false), 'missed')
}
`)
const policy = { schemaVersion: 1, packages: ['artplayer'], exclude: {}, minimum: { 'packages/artplayer/src/probe.ts': { lines: 100, branches: 100, functions: 100 } } }
const inventory = sourceInventory(fixture, policy)
assert.deepEqual(inventory.excluded.map(item => item.file), ['packages/artplayer/src/types.ts'])
for (const mode of ['incomplete', 'complete']) {
const output = path.join(fixture, mode)
const rawDirectory = path.join(output, 'raw')
const moduleDirectory = path.join(output, 'modules')
const mappedDirectory = path.join(output, 'normalized')
const collected = spawnSync(process.execPath, [entry, mode], {
cwd: fixture,
encoding: 'utf8',
env: { ...process.env, NODE_V8_COVERAGE: rawDirectory, ARTPLAYER_COVERAGE_DIR: moduleDirectory },
})
assert.equal(collected.status, 0, collected.stderr)
const maps = normalizeCoverageReports(rawDirectory, mappedDirectory, { root: fixture, moduleDirectory, inventory })
assert(maps.modules >= 2 && maps.reports.some(item => item.normalizedSources > 0))
const config = path.join(output, 'c8.json')
write(config, {
'all': true,
'exclude-after-remap': true,
'include': ['packages/artplayer/src/**'],
'exclude': inventory.excluded.map(item => item.file),
'src': ['packages/artplayer/src'],
'extension': ['.ts'],
'reporter': ['json', 'json-summary'],
'temp-directory': mappedDirectory,
'reports-dir': path.join(output, 'report'),
})
const rendered = spawnSync(process.execPath, [require.resolve('c8/bin/c8.js'), 'report', '--config', config], { cwd: fixture, encoding: 'utf8' })
assert.equal(rendered.status, 0, rendered.stderr)
const summary = read(path.join(output, 'report/coverage-summary.json'))
const analyzed = analyzeCoverage(fixture, summary, inventory, policy)
const probe = analyzed.files.find(item => item.file.endsWith('/probe.ts'))
assert(probe.metrics.functions.total > 0 && probe.metrics.branches.total > 0)
assert.equal(analyzed.files.find(item => item.file.endsWith('/never.ts')).metrics.lines.covered, 0, 'Never imported file must remain in the denominator with zero coverage')
if (mode === 'complete') {
assert.deepEqual(analyzed.violations, [])
}
else {
assert(analyzed.violations.some(item => item.metric === 'branches'))
const full = read(path.join(output, 'report/coverage-final.json'))[path.join(source, 'probe.ts')]
const missed = Object.entries(full.statementMap).filter(([, location]) => location.start.line === 5)
assert(missed.length > 0 && missed.every(([id]) => full.s[id] === 0), 'Unexecuted TS return must not become 100% covered')
const reportFile = maps.reports.find(item => item.modules > 0).file
const original = read(path.join(rawDirectory, reportFile))
const module = original.result.find(item => item.url.startsWith(pathToFileURL(moduleDirectory).href))
const mutated = structuredClone(original)
const mapped = mutated['source-map-cache'][module.url].data
const index = mapped.sources.findIndex(item => item.endsWith('/probe.ts'))
assert(index >= 0)
mapped.sourcesContent[index] += '\n// stale source'
const corrupt = path.join(output, 'corrupt')
fs.mkdirSync(corrupt)
write(path.join(corrupt, reportFile), mutated)
assert.throws(() => normalizeCoverageReports(corrupt, path.join(output, 'stale'), { root: fixture, moduleDirectory, inventory }), /Stale mapped source/)
delete mutated['source-map-cache'][module.url]
write(path.join(corrupt, reportFile), mutated)
assert.throws(() => normalizeCoverageReports(corrupt, path.join(output, 'missing'), { root: fixture, moduleDirectory, inventory }), /Missing source map/)
}
}
})
test('coverage gates reject missing files, empty branch counts and ignored counters', () => {
const file = 'packages/artplayer/src/lifecycle/scope.ts'
const inventory = { files: [{ file, sha256: 'test' }], excluded: [] }
const policy = { minimum: { [file]: { branches: 90 } } }
const counters = () => Object.fromEntries(['lines', 'branches', 'functions', 'statements'].map(metric => [metric, { covered: 10, total: 10, skipped: 0, pct: 100 }]))
const key = path.join(root, file)
assert.deepEqual(analyzeCoverage(root, { [key]: counters() }, inventory, policy).violations, [])
assert.throws(() => analyzeCoverage(root, {}, inventory, policy), /source inventory differs/)
const empty = counters()
empty.branches = { covered: 0, total: 0, skipped: 0, pct: 100 }
assert.equal(analyzeCoverage(root, { [key]: empty }, inventory, policy).violations.length, 1)
const ignored = counters()
ignored.lines.skipped = 1
assert.throws(() => analyzeCoverage(root, { [key]: ignored }, inventory, policy), /Invalid or ignored lines/)
const low = counters()
low.branches.covered = 1
assert.equal(analyzeCoverage(root, { [key]: low }, inventory, policy).violations[0].actual, 10, 'Recompute percentage instead of trusting a stale pct field')
})