mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17:06:15 -08:00
* add scoped coverage reporting * fix coverage review feedback --------- Co-authored-by: 4gray <fourgray@proton.me>
235 lines
6.9 KiB
JavaScript
235 lines
6.9 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
import { execFileSync } from 'node:child_process';
|
|
import {
|
|
existsSync,
|
|
readFileSync,
|
|
readdirSync,
|
|
statSync,
|
|
} from 'node:fs';
|
|
import path from 'node:path';
|
|
import process from 'node:process';
|
|
|
|
const workspaceRoot = process.cwd();
|
|
const args = new Set(process.argv.slice(2));
|
|
const requireReport = args.has('--require-report');
|
|
const policy = JSON.parse(
|
|
readFileSync(path.join(workspaceRoot, 'tools/coverage/coverage-policy.json'), 'utf8')
|
|
);
|
|
|
|
const warnings = [];
|
|
const errors = [];
|
|
|
|
function warn(message) {
|
|
warnings.push(message);
|
|
if (process.env.GITHUB_ACTIONS) {
|
|
console.log(`::warning::${message}`);
|
|
} else {
|
|
console.warn(`Warning: ${message}`);
|
|
}
|
|
}
|
|
|
|
function readJson(filePath) {
|
|
return JSON.parse(readFileSync(filePath, 'utf8'));
|
|
}
|
|
|
|
function projectJsonPath(project) {
|
|
return path.join(workspaceRoot, project.root, 'project.json');
|
|
}
|
|
|
|
function verifyTierAProjects() {
|
|
for (const project of policy.unitCoverage.tierA) {
|
|
const filePath = projectJsonPath(project);
|
|
if (!existsSync(filePath)) {
|
|
errors.push(`Tier A project ${project.name} is missing ${project.root}/project.json.`);
|
|
continue;
|
|
}
|
|
|
|
const projectJson = readJson(filePath);
|
|
if (!projectJson.targets?.test) {
|
|
errors.push(`Tier A project ${project.name} must have a test target.`);
|
|
}
|
|
if (!project.sourceRoot || !existsSync(path.join(workspaceRoot, project.sourceRoot))) {
|
|
errors.push(`Tier A project ${project.name} has an invalid sourceRoot: ${project.sourceRoot}`);
|
|
}
|
|
if (project.sourceRoot && !hasSpecUnder(project.sourceRoot)) {
|
|
errors.push(`Tier A project ${project.name} has no unit specs under ${project.sourceRoot}. Move it to Tier B/C or add focused tests.`);
|
|
}
|
|
}
|
|
}
|
|
|
|
function verifyCoverageReport() {
|
|
const summaryPath = path.join(
|
|
workspaceRoot,
|
|
policy.reporting.mergedCoverageDir,
|
|
'coverage-summary.json'
|
|
);
|
|
|
|
if (!existsSync(summaryPath)) {
|
|
const message = `Merged coverage summary not found at ${path.relative(workspaceRoot, summaryPath)}.`;
|
|
if (requireReport) {
|
|
errors.push(message);
|
|
} else {
|
|
warn(message);
|
|
}
|
|
return;
|
|
}
|
|
|
|
const summary = readJson(summaryPath).total;
|
|
console.log(
|
|
`Merged coverage: statements ${summary.statements.pct}%, branches ${summary.branches.pct}%, functions ${summary.functions.pct}%, lines ${summary.lines.pct}%.`
|
|
);
|
|
|
|
if (requireReport) {
|
|
for (const project of policy.unitCoverage.tierA) {
|
|
const projectCoveragePath = path.join(
|
|
workspaceRoot,
|
|
'coverage',
|
|
project.root,
|
|
'coverage-final.json'
|
|
);
|
|
if (!existsSync(projectCoveragePath)) {
|
|
errors.push(
|
|
`Tier A project ${project.name} did not produce ${path.relative(workspaceRoot, projectCoveragePath)}.`
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function listFiles(directory, predicate) {
|
|
if (!existsSync(directory)) {
|
|
return [];
|
|
}
|
|
|
|
const files = [];
|
|
for (const entry of readdirSync(directory)) {
|
|
const fullPath = path.join(directory, entry);
|
|
const stats = statSync(fullPath);
|
|
if (stats.isDirectory()) {
|
|
files.push(...listFiles(fullPath, predicate));
|
|
} else if (predicate(fullPath)) {
|
|
files.push(fullPath);
|
|
}
|
|
}
|
|
return files;
|
|
}
|
|
|
|
function hasSpecUnder(sourceRoot) {
|
|
return listFiles(path.join(workspaceRoot, sourceRoot), (file) =>
|
|
/\.(spec|test)\.ts$/.test(file)
|
|
).length > 0;
|
|
}
|
|
|
|
function scanE2ETags() {
|
|
const e2eFiles = [
|
|
...listFiles(path.join(workspaceRoot, 'apps/web-e2e/src'), (file) => file.endsWith('.e2e.ts')),
|
|
...listFiles(path.join(workspaceRoot, 'apps/electron-backend-e2e/src'), (file) =>
|
|
file.endsWith('.e2e.ts')
|
|
),
|
|
];
|
|
const tags = new Set();
|
|
|
|
for (const file of e2eFiles) {
|
|
const contents = readFileSync(file, 'utf8');
|
|
for (const match of contents.matchAll(/@[a-z0-9-]+/gi)) {
|
|
tags.add(match[0]);
|
|
}
|
|
}
|
|
|
|
for (const tag of policy.e2eSemanticCoverage.trackedTags) {
|
|
if (!tags.has(tag)) {
|
|
warn(`Tracked E2E tag ${tag} is not present in current Playwright specs yet.`);
|
|
}
|
|
}
|
|
}
|
|
|
|
function changedFiles() {
|
|
const changedFileSet = new Set();
|
|
const commands = [];
|
|
if (process.env.GITHUB_BASE_REF) {
|
|
commands.push(['git', ['diff', '--name-only', `origin/${process.env.GITHUB_BASE_REF}...HEAD`]]);
|
|
} else {
|
|
commands.push(['git', ['diff', '--name-only', 'HEAD']]);
|
|
commands.push(['git', ['ls-files', '--others', '--exclude-standard']]);
|
|
commands.push(['git', ['diff', '--name-only', 'HEAD~1...HEAD']]);
|
|
}
|
|
|
|
for (const [command, commandArgs] of commands) {
|
|
try {
|
|
const output = execFileSync(command, commandArgs, {
|
|
cwd: workspaceRoot,
|
|
encoding: 'utf8',
|
|
stdio: ['ignore', 'pipe', 'ignore'],
|
|
});
|
|
const outputFiles = output
|
|
.split('\n')
|
|
.map((file) => file.trim())
|
|
.filter(Boolean);
|
|
for (const file of outputFiles) {
|
|
changedFileSet.add(file);
|
|
}
|
|
} catch {
|
|
// Try the next diff source.
|
|
}
|
|
}
|
|
|
|
return Array.from(changedFileSet);
|
|
}
|
|
|
|
function sourceOwner(file) {
|
|
return policy.unitCoverage.tierA.find((project) =>
|
|
file.startsWith(`${project.sourceRoot}/`)
|
|
);
|
|
}
|
|
|
|
function hasNearbySpec(file) {
|
|
const parsed = path.parse(file);
|
|
const candidateNames = [
|
|
path.join(parsed.dir, `${parsed.name}.spec.ts`),
|
|
path.join(parsed.dir, `${parsed.name}.test.ts`),
|
|
];
|
|
|
|
return candidateNames.some((candidate) => existsSync(path.join(workspaceRoot, candidate)));
|
|
}
|
|
|
|
function reportChangedCriticalFiles() {
|
|
const files = changedFiles().filter(
|
|
(file) =>
|
|
/\.(ts|html)$/.test(file) &&
|
|
!/\.(spec|test)\.ts$/.test(file) &&
|
|
!file.endsWith('test-setup.ts')
|
|
);
|
|
|
|
for (const file of files) {
|
|
const owner = sourceOwner(file);
|
|
if (!owner) {
|
|
continue;
|
|
}
|
|
|
|
if (!hasNearbySpec(file)) {
|
|
warn(
|
|
`${file} is Tier A source without a same-name unit spec. Preferred validation: ${owner.validationCommand}; related E2E tags: ${owner.e2eTags.join(', ')}.`
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
verifyTierAProjects();
|
|
verifyCoverageReport();
|
|
scanE2ETags();
|
|
reportChangedCriticalFiles();
|
|
|
|
if (errors.length > 0) {
|
|
for (const error of errors) {
|
|
console.error(`Error: ${error}`);
|
|
}
|
|
process.exit(1);
|
|
}
|
|
|
|
console.log(
|
|
warnings.length === 0
|
|
? 'Coverage health checks passed.'
|
|
: `Coverage health completed with ${warnings.length} warning(s).`
|
|
);
|