modified: .gitignore
modified: .vscode/launch.json new file: e2e/playwright-collect-coverage.spec.ts modified: package-lock.json modified: package.json modified: playwright.config.ts new file: scripts/merge-lcov.cjs new file: scripts/normalize-lcov.cjs new file: scripts/playwright-collect-coverage.cjs new file: scripts/remap-and-report.cjs modified: vite.config.ts new file: vitest.config.ts
This commit is contained in:
1 parent
d0eb4e953c
commit
6192bd20eb
12 files changed
+1134
-10
No files matched your search
Executable
+64
@@ -0,0 +1,64 @@
|
||||
#!/usr/bin/env node
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
|
||||
const files = process.argv.slice(2);
|
||||
if (files.length < 2) {
|
||||
console.error('Usage: node merge-lcov.cjs <file1.lcov> <file2.lcov> [<file3.lcov> ...]');
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
function ensureEntry(map, sf) {
|
||||
if (!map.has(sf)) map.set(sf, { other: new Set(), da: new Map() });
|
||||
return map.get(sf);
|
||||
}
|
||||
|
||||
const combined = new Map();
|
||||
|
||||
for (const f of files) {
|
||||
let content;
|
||||
try {
|
||||
content = fs.readFileSync(f, 'utf8');
|
||||
} catch (err) {
|
||||
console.error('Could not read', f, err.message);
|
||||
process.exit(3);
|
||||
}
|
||||
const lines = content.split(/\r?\n/);
|
||||
let currentSF = null;
|
||||
for (let raw of lines) {
|
||||
const line = raw.trim();
|
||||
if (!line) continue;
|
||||
if (line.startsWith('SF:')) {
|
||||
currentSF = line.slice(3);
|
||||
ensureEntry(combined, currentSF);
|
||||
} else if (!currentSF) {
|
||||
// ignore lines before first SF
|
||||
} else if (line.startsWith('DA:')) {
|
||||
const parts = line.slice(3).split(',');
|
||||
const lnNum = parseInt(parts[0], 10);
|
||||
const hitsNum = parseInt(parts[1] || '0', 10);
|
||||
const entry = ensureEntry(combined, currentSF);
|
||||
entry.da.set(lnNum, (entry.da.get(lnNum) || 0) + hitsNum);
|
||||
} else if (line.startsWith('end_of_record')) {
|
||||
// ignore
|
||||
} else {
|
||||
const entry = ensureEntry(combined, currentSF);
|
||||
entry.other.add(line);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let out = '';
|
||||
for (const [sf, data] of combined.entries()) {
|
||||
out += `SF:${sf}\n`;
|
||||
const other = Array.from(data.other).sort();
|
||||
for (const l of other) out += l + '\n';
|
||||
const daEntries = Array.from(data.da.entries()).sort((a, b) => a[0] - b[0]);
|
||||
for (const [ln, hits] of daEntries) out += `DA:${ln},${hits}\n`;
|
||||
const lf = daEntries.length;
|
||||
const lh = daEntries.reduce((acc, [, hits]) => acc + (hits > 0 ? 1 : 0), 0);
|
||||
out += `LF:${lf}\nLH:${lh}\nend_of_record\n`;
|
||||
}
|
||||
|
||||
process.stdout.write(out);
|
||||
Executable
+134
@@ -0,0 +1,134 @@
|
||||
#!/usr/bin/env node
|
||||
// scripts/normalize-lcov.cjs
|
||||
// Usage: node scripts/normalize-lcov.cjs input.lcov [output.lcov]
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const repoRoot = process.cwd();
|
||||
const inFile = process.argv[2] || 'coverage-reports/merged/merged.lcov';
|
||||
const outFile = process.argv[3] || inFile.replace(/\.lcov$/, '.normalized.lcov');
|
||||
|
||||
if (!fs.existsSync(inFile)) {
|
||||
console.error('Input file not found:', inFile);
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
const content = fs.readFileSync(inFile, 'utf8');
|
||||
const lines = content.split(/\r?\n/);
|
||||
|
||||
// split into records separated by "end_of_record"
|
||||
const records = [];
|
||||
let cur = [];
|
||||
for (const L of lines) {
|
||||
cur.push(L);
|
||||
if (L.trim() === 'end_of_record') {
|
||||
records.push(cur.join('\n'));
|
||||
cur = [];
|
||||
}
|
||||
}
|
||||
// if file didn't end with end_of_record, push remainder
|
||||
if (cur.length) records.push(cur.join('\n'));
|
||||
|
||||
function exists(p) {
|
||||
try { return fs.existsSync(p); } catch { return false; }
|
||||
}
|
||||
|
||||
function findInSrc(basename) {
|
||||
const srcRoot = path.join(repoRoot, 'src');
|
||||
if (!fs.existsSync(srcRoot)) return null;
|
||||
// recursive search (stop when found)
|
||||
function walk(dir) {
|
||||
const items = fs.readdirSync(dir, { withFileTypes: true });
|
||||
for (const it of items) {
|
||||
const full = path.join(dir, it.name);
|
||||
if (it.isFile() && it.name === basename) return full;
|
||||
if (it.isDirectory() && it.name !== 'node_modules' && it.name !== '.git') {
|
||||
const found = walk(full);
|
||||
if (found) return found;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
return walk(srcRoot);
|
||||
}
|
||||
|
||||
function normalizeSF(sfLine) {
|
||||
// sfLine is the part after 'SF:'
|
||||
let sf = sfLine.trim();
|
||||
|
||||
// Skip clearly virtual / query-string entries
|
||||
if (sf.startsWith('@') || sf.includes('?v=') || sf === '@react-refresh') {
|
||||
return null;
|
||||
}
|
||||
|
||||
// If it's an absolute path that exists, make it repo-relative.
|
||||
if (path.isAbsolute(sf)) {
|
||||
if (exists(sf)) return path.relative(repoRoot, sf);
|
||||
// Try chopping off any leading workspace prefix and look again
|
||||
const repoName = path.basename(repoRoot);
|
||||
const idx = sf.indexOf(`/${repoName}/`);
|
||||
if (idx !== -1) {
|
||||
const candidate = sf.slice(idx + repoName.length + 2); // after /repoName/
|
||||
if (exists(path.join(repoRoot, candidate))) return candidate;
|
||||
if (exists(path.join(repoRoot, 'src', candidate))) return path.posix.join('src', candidate);
|
||||
}
|
||||
}
|
||||
|
||||
// If it's already relative, check relative to repo root
|
||||
if (!path.isAbsolute(sf)) {
|
||||
if (exists(path.join(repoRoot, sf))) return sf;
|
||||
// If it looks like "stockfill/..." or "<repoName>/..." try to strip the repo prefix
|
||||
const repoName = path.basename(repoRoot);
|
||||
if (sf.startsWith(`${repoName}/`) || sf.startsWith('stockfill/')) {
|
||||
const candidate = sf.replace(new RegExp(`^(${repoName}|stockfill)/`), '');
|
||||
if (exists(path.join(repoRoot, candidate))) return candidate;
|
||||
if (exists(path.join(repoRoot, 'src', candidate))) return path.posix.join('src', candidate);
|
||||
}
|
||||
// If it is a bare filename "DBProvider.tsx" try src/
|
||||
if (!sf.includes('/') && exists(path.join(repoRoot, 'src', sf))) {
|
||||
return path.posix.join('src', sf);
|
||||
}
|
||||
}
|
||||
|
||||
// If it contains '/src/' somewhere, try to extract the src/... suffix
|
||||
const srcIdx = sf.indexOf('/src/');
|
||||
if (srcIdx !== -1) {
|
||||
const candidate = sf.slice(srcIdx + 1); // remove leading slash
|
||||
if (exists(path.join(repoRoot, candidate))) return candidate;
|
||||
}
|
||||
|
||||
// Finally, try finding by basename under src
|
||||
const base = path.basename(sf);
|
||||
const found = findInSrc(base);
|
||||
if (found) return path.relative(repoRoot, found);
|
||||
|
||||
// give up
|
||||
return null;
|
||||
}
|
||||
|
||||
let kept = 0, dropped = 0;
|
||||
|
||||
const outRecords = records.map(rec => {
|
||||
// find SF line
|
||||
const m = rec.match(/^SF:(.*)$/m);
|
||||
if (!m) {
|
||||
// no SF line? keep it (unlikely)
|
||||
kept++;
|
||||
return rec;
|
||||
}
|
||||
const sfRaw = m[1].trim();
|
||||
const normalized = normalizeSF(sfRaw);
|
||||
if (!normalized) {
|
||||
dropped++;
|
||||
return null; // drop entire record
|
||||
}
|
||||
// replace SF:... with normalized relative path using posix separators
|
||||
const normalizedPosix = normalized.split(path.sep).join(path.posix.sep);
|
||||
const newRec = rec.replace(/^SF:.*$/m, `SF:${normalizedPosix}`);
|
||||
kept++;
|
||||
return newRec;
|
||||
}).filter(Boolean);
|
||||
|
||||
fs.writeFileSync(outFile, outRecords.join('\n') + '\n');
|
||||
console.log(`Wrote ${outFile} (kept ${kept}, dropped ${dropped} records)`);
|
||||
Executable
+23
@@ -0,0 +1,23 @@
|
||||
// tests/playwright-collect-coverage.js
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { test } = require('@playwright/test');
|
||||
|
||||
test.afterEach(async ({ page }, testInfo) => {
|
||||
try {
|
||||
// evaluate coverage from the page
|
||||
const coverage = await page.evaluate(() => (globalThis.__coverage__ || {}));
|
||||
if (coverage && Object.keys(coverage).length) {
|
||||
const dir = path.join(process.cwd(), '.nyc_output');
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
|
||||
// unique file per worker/test
|
||||
const filename = path.join(dir, `coverage-e2e-${process.pid}-${Date.now()}.json`);
|
||||
fs.writeFileSync(filename, JSON.stringify(coverage));
|
||||
console.log('Wrote coverage:', filename);
|
||||
}
|
||||
} catch (err) {
|
||||
// don't fail tests if coverage collection fails
|
||||
console.warn('Failed to collect coverage from page:', err.message || err);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,35 @@
|
||||
// scripts/remap-and-report.cjs
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const libCoverage = require('istanbul-lib-coverage');
|
||||
const libSourceMaps = require('istanbul-lib-source-maps');
|
||||
const libReport = require('istanbul-lib-report');
|
||||
const reports = require('istanbul-reports');
|
||||
|
||||
const out = libCoverage.createCoverageMap({});
|
||||
const dir = path.join(process.cwd(), '.nyc_output');
|
||||
if (!fs.existsSync(dir)) {
|
||||
console.error('.nyc_output not found');
|
||||
process.exit(2);
|
||||
}
|
||||
const files = fs.readdirSync(dir).filter(f => f.endsWith('.json'));
|
||||
for (const f of files) {
|
||||
const data = JSON.parse(fs.readFileSync(path.join(dir, f), 'utf8'));
|
||||
out.merge(data);
|
||||
}
|
||||
|
||||
// Remap using source maps
|
||||
const sourceMapStore = libSourceMaps.createSourceMapStore();
|
||||
const transformed = sourceMapStore.transformCoverage(out);
|
||||
const remapped = transformed.map;
|
||||
|
||||
// Write reports
|
||||
const outDir = path.join(process.cwd(), 'coverage-reports', 'e2e');
|
||||
fs.mkdirSync(outDir, { recursive: true });
|
||||
const context = libReport.createContext({ dir: outDir, coverageMap: remapped });
|
||||
|
||||
// lcov + html
|
||||
reports.create('lcovonly', {}).execute(context);
|
||||
reports.create('html', {}).execute(context);
|
||||
|
||||
console.log('Wrote remapped e2e coverage to', outDir);
|
||||
Reference in new issue
Block a user