mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-09 03:27:43 -08:00
127 lines
7.3 KiB
JavaScript
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')
|
|
})
|