mirror of
https://github.com/l5yth/potato-mesh.git
synced 2026-08-07 09:22:53 +02:00
Fix frontend coverage export for Codecov (#376)
* fix: export frontend coverage for codecov * Merge V8 file coverages across workers
This commit is contained in:
Generated
+162
-1
@@ -6,7 +6,168 @@
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "potato-mesh",
|
||||
"version": "0.5.0"
|
||||
"version": "0.5.0",
|
||||
"devDependencies": {
|
||||
"istanbul-lib-coverage": "^3.2.2",
|
||||
"istanbul-lib-report": "^3.0.1",
|
||||
"istanbul-reports": "^3.2.0",
|
||||
"v8-to-istanbul": "^9.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/resolve-uri": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
|
||||
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/sourcemap-codec": {
|
||||
"version": "1.5.5",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
||||
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@jridgewell/trace-mapping": {
|
||||
"version": "0.3.31",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
|
||||
"integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/resolve-uri": "^3.1.0",
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/istanbul-lib-coverage": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz",
|
||||
"integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/convert-source-map": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
|
||||
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/has-flag": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/html-escaper": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
|
||||
"integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/istanbul-lib-coverage": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz",
|
||||
"integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/istanbul-lib-report": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz",
|
||||
"integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"istanbul-lib-coverage": "^3.0.0",
|
||||
"make-dir": "^4.0.0",
|
||||
"supports-color": "^7.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/istanbul-reports": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz",
|
||||
"integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"html-escaper": "^2.0.0",
|
||||
"istanbul-lib-report": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/make-dir": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz",
|
||||
"integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"semver": "^7.5.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "7.7.3",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
|
||||
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/supports-color": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"has-flag": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/v8-to-istanbul": {
|
||||
"version": "9.3.0",
|
||||
"resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz",
|
||||
"integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@jridgewell/trace-mapping": "^0.3.12",
|
||||
"@types/istanbul-lib-coverage": "^2.0.1",
|
||||
"convert-source-map": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.12.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,5 +5,11 @@
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"test": "mkdir -p reports coverage && NODE_V8_COVERAGE=coverage node --test --experimental-test-coverage --test-reporter=spec --test-reporter-destination=stdout --test-reporter=junit --test-reporter-destination=reports/javascript-junit.xml && node ./scripts/export-coverage.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"istanbul-lib-coverage": "^3.2.2",
|
||||
"istanbul-lib-report": "^3.0.1",
|
||||
"istanbul-reports": "^3.2.0",
|
||||
"v8-to-istanbul": "^9.3.0"
|
||||
}
|
||||
}
|
||||
|
||||
+147
-14
@@ -15,10 +15,25 @@
|
||||
import { promises as fs } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
const coverageDir = 'coverage';
|
||||
const reportsDir = 'reports';
|
||||
const outputPath = path.join(reportsDir, 'javascript-coverage.json');
|
||||
import istanbulLibCoverage from 'istanbul-lib-coverage';
|
||||
import istanbulLibReport from 'istanbul-lib-report';
|
||||
import istanbulReports from 'istanbul-reports';
|
||||
import v8toIstanbul from 'v8-to-istanbul';
|
||||
|
||||
const { createCoverageMap } = istanbulLibCoverage;
|
||||
const { createContext } = istanbulLibReport;
|
||||
|
||||
const coverageDir = path.resolve('coverage');
|
||||
const reportsDir = path.resolve('reports');
|
||||
const jsonOutputName = 'javascript-coverage.json';
|
||||
const lcovOutputName = 'javascript-coverage.lcov';
|
||||
const projectRoot = process.cwd();
|
||||
|
||||
/**
|
||||
* Ensure the reports directory exists so that coverage artefacts can be written.
|
||||
*
|
||||
* @returns {Promise<void>} A promise that resolves when the directory is available.
|
||||
*/
|
||||
async function ensureReportsDir() {
|
||||
try {
|
||||
await fs.mkdir(reportsDir, { recursive: true });
|
||||
@@ -28,32 +43,150 @@ async function ensureReportsDir() {
|
||||
}
|
||||
}
|
||||
|
||||
async function copyLatestCoverage() {
|
||||
/**
|
||||
* Read the coverage directory and return a deterministically ordered list of JSON files.
|
||||
*
|
||||
* @returns {Promise<string[]>} The absolute paths of available coverage JSON artefacts.
|
||||
*/
|
||||
async function listCoverageFiles() {
|
||||
let entries;
|
||||
try {
|
||||
entries = await fs.readdir(coverageDir);
|
||||
} catch (error) {
|
||||
if (error.code === 'ENOENT') {
|
||||
console.warn('Coverage directory not found; skipping export.');
|
||||
return;
|
||||
return [];
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
const coverageFiles = entries.filter(name => name.endsWith('.json'));
|
||||
const coverageFiles = entries
|
||||
.filter(name => name.endsWith('.json'))
|
||||
.map(name => path.join(coverageDir, name))
|
||||
.sort();
|
||||
|
||||
if (!coverageFiles.length) {
|
||||
console.warn('No coverage files generated; skipping export.');
|
||||
return;
|
||||
return [];
|
||||
}
|
||||
|
||||
// Sort to pick the most recent entry deterministically.
|
||||
coverageFiles.sort();
|
||||
const latest = coverageFiles[coverageFiles.length - 1];
|
||||
const source = path.join(coverageDir, latest);
|
||||
return coverageFiles;
|
||||
}
|
||||
|
||||
await fs.copyFile(source, outputPath);
|
||||
console.log(`Copied coverage report to ${outputPath}`);
|
||||
/**
|
||||
* Convert a V8 coverage URL to a project-local filesystem path.
|
||||
*
|
||||
* @param {string | undefined} url The coverage URL emitted by V8.
|
||||
* @returns {string | null} A normalised absolute path, or null when the URL should be ignored.
|
||||
*/
|
||||
function normaliseFileUrl(url) {
|
||||
if (!url || url.startsWith('node:')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!url.startsWith('file://')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let filePath;
|
||||
try {
|
||||
filePath = decodeURIComponent(new URL(url).pathname);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!filePath.startsWith(projectRoot)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (filePath.includes('node_modules')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return filePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform the raw V8 coverage reports into an Istanbul coverage map.
|
||||
*
|
||||
* @param {string[]} coverageFiles A list of coverage artefacts to consume.
|
||||
* @returns {Promise<import('istanbul-lib-coverage').CoverageMap>} The aggregated coverage map.
|
||||
*/
|
||||
async function buildCoverageMap(coverageFiles) {
|
||||
const coverageMap = createCoverageMap({});
|
||||
|
||||
for (const file of coverageFiles) {
|
||||
const raw = await fs.readFile(file, 'utf8');
|
||||
const parsed = JSON.parse(raw);
|
||||
const entries = Array.isArray(parsed.result) ? parsed.result : [];
|
||||
|
||||
for (const entry of entries) {
|
||||
const { url, functions } = entry;
|
||||
const filePath = normaliseFileUrl(url);
|
||||
if (!filePath) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const converter = v8toIstanbul(filePath, 0, {
|
||||
source: await fs.readFile(filePath, 'utf8'),
|
||||
});
|
||||
await converter.load();
|
||||
converter.applyCoverage(functions);
|
||||
const fileCoverages = converter.toIstanbul();
|
||||
for (const coverage of Object.values(fileCoverages)) {
|
||||
if (coverage.path) {
|
||||
const relativePath = path.relative(projectRoot, coverage.path);
|
||||
coverage.path = relativePath || coverage.path;
|
||||
}
|
||||
try {
|
||||
const existingCoverage = coverageMap.fileCoverageFor(coverage.path);
|
||||
existingCoverage.merge(coverage);
|
||||
} catch (error) {
|
||||
if (error && typeof error.message === 'string' && error.message.includes('No file coverage')) {
|
||||
coverageMap.addFileCoverage(coverage);
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`Failed to translate coverage for ${filePath}:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return coverageMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist the Istanbul coverage map as JSON and LCOV artefacts for downstream tooling.
|
||||
*
|
||||
* @param {import('istanbul-lib-coverage').CoverageMap} coverageMap The populated coverage map.
|
||||
* @returns {Promise<void>} A promise that resolves when the outputs are written.
|
||||
*/
|
||||
async function writeCoverageOutputs(coverageMap) {
|
||||
const jsonOutputPath = path.join(reportsDir, jsonOutputName);
|
||||
const lcovOutputPath = path.join(reportsDir, lcovOutputName);
|
||||
|
||||
await fs.writeFile(jsonOutputPath, `${JSON.stringify(coverageMap.toJSON(), null, 2)}\n`);
|
||||
|
||||
const context = createContext({ dir: reportsDir, coverageMap });
|
||||
istanbulReports.create('lcovonly', { file: lcovOutputName }).execute(context);
|
||||
|
||||
console.log(`Wrote coverage reports to ${jsonOutputPath} and ${lcovOutputPath}`);
|
||||
}
|
||||
|
||||
await ensureReportsDir();
|
||||
await copyLatestCoverage();
|
||||
const coverageFiles = await listCoverageFiles();
|
||||
if (!coverageFiles.length) {
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const coverageMap = await buildCoverageMap(coverageFiles);
|
||||
if (!coverageMap.files().length) {
|
||||
console.warn('No project coverage entries were recognised; skipping export.');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
await writeCoverageOutputs(coverageMap);
|
||||
|
||||
Reference in New Issue
Block a user