tryscript 0.1.5 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +41 -8
- package/dist/bin.cjs +356 -13
- package/dist/bin.cjs.map +1 -1
- package/dist/bin.mjs +357 -14
- package/dist/bin.mjs.map +1 -1
- package/dist/index.cjs +10 -2
- package/dist/index.d.cts +148 -2
- package/dist/index.d.mts +148 -2
- package/dist/index.mjs +2 -2
- package/dist/{src-D-bd-j9T.cjs → src-BIZMxxIt.cjs} +487 -12
- package/dist/src-BIZMxxIt.cjs.map +1 -0
- package/dist/{src-CC3xA1cp.mjs → src-BQxIhzgF.mjs} +441 -14
- package/dist/src-BQxIhzgF.mjs.map +1 -0
- package/docs/tryscript-reference.md +116 -5
- package/package.json +4 -2
- package/dist/src-CC3xA1cp.mjs.map +0 -1
- package/dist/src-D-bd-j9T.cjs.map +0 -1
package/dist/bin.mjs
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import { b as resolveCoverageConfig, f as cleanupExecutionContext, g as parseTestFile, h as runBlock, m as runAfterHook, p as createExecutionContext, r as writeCaptureLog, s as expandTestFile, t as VERSION, u as matchOutput, v as loadConfig, y as mergeConfig } from "./src-BQxIhzgF.mjs";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
6
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
7
7
|
import { dirname, join, resolve } from "node:path";
|
|
8
8
|
import { spawn } from "node:child_process";
|
|
9
9
|
import { access, mkdtemp, readFile, readdir, rm, stat } from "node:fs/promises";
|
|
10
10
|
import { tmpdir } from "node:os";
|
|
11
|
+
import { writeFile } from "atomically";
|
|
11
12
|
import { Command } from "commander";
|
|
12
13
|
import fg from "fast-glob";
|
|
13
14
|
import pc from "picocolors";
|
|
14
15
|
import { createPatch } from "diff";
|
|
15
|
-
import { writeFile } from "atomically";
|
|
16
16
|
|
|
17
17
|
//#region src/cli/lib/shared.ts
|
|
18
18
|
/**
|
|
@@ -134,9 +134,10 @@ function reportSummary(summary, _options) {
|
|
|
134
134
|
async function updateTestFile(file, results) {
|
|
135
135
|
let content = file.rawContent;
|
|
136
136
|
const changes = [];
|
|
137
|
-
const
|
|
137
|
+
const resultByBlock = new Map(results.map((result) => [result.block, result]));
|
|
138
|
+
const blocksWithResults = [...file.blocks].map((block) => ({
|
|
138
139
|
block,
|
|
139
|
-
result:
|
|
140
|
+
result: resultByBlock.get(block)
|
|
140
141
|
})).reverse();
|
|
141
142
|
for (const { block, result } of blocksWithResults) {
|
|
142
143
|
if (!result) continue;
|
|
@@ -159,15 +160,248 @@ async function updateTestFile(file, results) {
|
|
|
159
160
|
* Build an updated console block with new expected output.
|
|
160
161
|
*/
|
|
161
162
|
function buildUpdatedBlock(block, result) {
|
|
162
|
-
const
|
|
163
|
+
const fence = "`".repeat(/^(`+)/.exec(block.rawContent)?.[1]?.length ?? 3);
|
|
164
|
+
const commandLines = block.command.split("\n").map((line, i) => {
|
|
163
165
|
return i === 0 ? `$ ${line}` : `> ${line}`;
|
|
164
|
-
})
|
|
166
|
+
});
|
|
167
|
+
const lines = [`${fence}console`, ...commandLines];
|
|
165
168
|
const trimmedOutput = result.actualOutput.trimEnd();
|
|
166
169
|
if (trimmedOutput) lines.push(trimmedOutput);
|
|
167
|
-
lines.push(`? ${result.actualExitCode}`,
|
|
170
|
+
lines.push(`? ${result.actualExitCode}`, fence);
|
|
168
171
|
return lines.join("\n");
|
|
169
172
|
}
|
|
170
173
|
|
|
174
|
+
//#endregion
|
|
175
|
+
//#region src/lib/lcov.ts
|
|
176
|
+
/**
|
|
177
|
+
* LCOV parsing, merging, and writing utilities.
|
|
178
|
+
*
|
|
179
|
+
* LCOV format reference:
|
|
180
|
+
* - SF: Source file path
|
|
181
|
+
* - DA:linenum,hitcount - Line data
|
|
182
|
+
* - FN:linenum,funcname - Function definition
|
|
183
|
+
* - FNDA:hitcount,funcname - Function hit data
|
|
184
|
+
* - FNF: Functions found count
|
|
185
|
+
* - FNH: Functions hit count
|
|
186
|
+
* - BRF: Branches found count
|
|
187
|
+
* - BRH: Branches hit count
|
|
188
|
+
* - BRDA:line,block,branch,taken - Branch data
|
|
189
|
+
* - LF: Lines found count
|
|
190
|
+
* - LH: Lines hit count
|
|
191
|
+
* - end_of_record - End of file record
|
|
192
|
+
*/
|
|
193
|
+
/**
|
|
194
|
+
* Parse LCOV content into structured data.
|
|
195
|
+
*/
|
|
196
|
+
function parseLcov(content) {
|
|
197
|
+
const files = /* @__PURE__ */ new Map();
|
|
198
|
+
let currentFile = null;
|
|
199
|
+
for (const line of content.split("\n")) {
|
|
200
|
+
const trimmed = line.trim();
|
|
201
|
+
if (trimmed.startsWith("SF:")) {
|
|
202
|
+
const path = trimmed.slice(3);
|
|
203
|
+
currentFile = {
|
|
204
|
+
path,
|
|
205
|
+
lines: /* @__PURE__ */ new Map(),
|
|
206
|
+
functions: /* @__PURE__ */ new Map(),
|
|
207
|
+
branches: []
|
|
208
|
+
};
|
|
209
|
+
files.set(path, currentFile);
|
|
210
|
+
} else if (trimmed.startsWith("DA:") && currentFile) {
|
|
211
|
+
const parts = trimmed.slice(3).split(",");
|
|
212
|
+
const lineNumber = parseInt(parts[0], 10);
|
|
213
|
+
const hitCount = parseInt(parts[1], 10);
|
|
214
|
+
currentFile.lines.set(lineNumber, {
|
|
215
|
+
lineNumber,
|
|
216
|
+
hitCount
|
|
217
|
+
});
|
|
218
|
+
} else if (trimmed.startsWith("FN:") && currentFile) {
|
|
219
|
+
const parts = trimmed.slice(3).split(",");
|
|
220
|
+
const lineNumber = parseInt(parts[0], 10);
|
|
221
|
+
const name = parts.slice(1).join(",");
|
|
222
|
+
if (!currentFile.functions.has(name)) currentFile.functions.set(name, {
|
|
223
|
+
name,
|
|
224
|
+
lineNumber,
|
|
225
|
+
hitCount: 0
|
|
226
|
+
});
|
|
227
|
+
else currentFile.functions.get(name).lineNumber = lineNumber;
|
|
228
|
+
} else if (trimmed.startsWith("FNDA:") && currentFile) {
|
|
229
|
+
const parts = trimmed.slice(5).split(",");
|
|
230
|
+
const hitCount = parseInt(parts[0], 10);
|
|
231
|
+
const name = parts.slice(1).join(",");
|
|
232
|
+
if (currentFile.functions.has(name)) currentFile.functions.get(name).hitCount = hitCount;
|
|
233
|
+
else currentFile.functions.set(name, {
|
|
234
|
+
name,
|
|
235
|
+
lineNumber: 0,
|
|
236
|
+
hitCount
|
|
237
|
+
});
|
|
238
|
+
} else if (trimmed.startsWith("BRDA:") && currentFile) {
|
|
239
|
+
const parts = trimmed.slice(5).split(",");
|
|
240
|
+
currentFile.branches.push({
|
|
241
|
+
line: parseInt(parts[0], 10),
|
|
242
|
+
block: parseInt(parts[1], 10),
|
|
243
|
+
branch: parseInt(parts[2], 10),
|
|
244
|
+
taken: parts[3] === "-" ? -1 : parseInt(parts[3], 10)
|
|
245
|
+
});
|
|
246
|
+
} else if (trimmed === "end_of_record") currentFile = null;
|
|
247
|
+
}
|
|
248
|
+
return { files };
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Merge multiple LCOV data structures, taking max hit counts.
|
|
252
|
+
*/
|
|
253
|
+
function mergeLcov(...lcovs) {
|
|
254
|
+
const merged = /* @__PURE__ */ new Map();
|
|
255
|
+
for (const lcov of lcovs) for (const [path, file] of lcov.files) if (!merged.has(path)) merged.set(path, {
|
|
256
|
+
path,
|
|
257
|
+
lines: new Map(file.lines),
|
|
258
|
+
functions: new Map(file.functions),
|
|
259
|
+
branches: [...file.branches]
|
|
260
|
+
});
|
|
261
|
+
else {
|
|
262
|
+
const existing = merged.get(path);
|
|
263
|
+
for (const [lineNum, lineData] of file.lines) {
|
|
264
|
+
const existingLine = existing.lines.get(lineNum);
|
|
265
|
+
if (existingLine) existingLine.hitCount = Math.max(existingLine.hitCount, lineData.hitCount);
|
|
266
|
+
else existing.lines.set(lineNum, { ...lineData });
|
|
267
|
+
}
|
|
268
|
+
for (const [name, funcData] of file.functions) {
|
|
269
|
+
const existingFunc = existing.functions.get(name);
|
|
270
|
+
if (existingFunc) existingFunc.hitCount = Math.max(existingFunc.hitCount, funcData.hitCount);
|
|
271
|
+
else existing.functions.set(name, { ...funcData });
|
|
272
|
+
}
|
|
273
|
+
for (const branch of file.branches) {
|
|
274
|
+
const existingBranch = existing.branches.find((b) => b.line === branch.line && b.block === branch.block && b.branch === branch.branch);
|
|
275
|
+
if (existingBranch) {
|
|
276
|
+
if (branch.taken >= 0) existingBranch.taken = existingBranch.taken >= 0 ? Math.max(existingBranch.taken, branch.taken) : branch.taken;
|
|
277
|
+
} else existing.branches.push({ ...branch });
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return { files: merged };
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Convert LCOV data back to LCOV format string.
|
|
284
|
+
*/
|
|
285
|
+
function formatLcov(lcov) {
|
|
286
|
+
const lines = [];
|
|
287
|
+
for (const file of lcov.files.values()) {
|
|
288
|
+
lines.push(`SF:${file.path}`);
|
|
289
|
+
const sortedFunctions = [...file.functions.values()].sort((a, b) => a.lineNumber - b.lineNumber);
|
|
290
|
+
for (const func of sortedFunctions) lines.push(`FN:${func.lineNumber},${func.name}`);
|
|
291
|
+
for (const func of sortedFunctions) lines.push(`FNDA:${func.hitCount},${func.name}`);
|
|
292
|
+
const fnf = file.functions.size;
|
|
293
|
+
const fnh = [...file.functions.values()].filter((f) => f.hitCount > 0).length;
|
|
294
|
+
lines.push(`FNF:${fnf}`);
|
|
295
|
+
lines.push(`FNH:${fnh}`);
|
|
296
|
+
for (const branch of file.branches) {
|
|
297
|
+
const taken = branch.taken < 0 ? "-" : branch.taken.toString();
|
|
298
|
+
lines.push(`BRDA:${branch.line},${branch.block},${branch.branch},${taken}`);
|
|
299
|
+
}
|
|
300
|
+
const brf = file.branches.length;
|
|
301
|
+
const brh = file.branches.filter((b) => b.taken > 0).length;
|
|
302
|
+
lines.push(`BRF:${brf}`);
|
|
303
|
+
lines.push(`BRH:${brh}`);
|
|
304
|
+
const sortedLines = [...file.lines.values()].sort((a, b) => a.lineNumber - b.lineNumber);
|
|
305
|
+
for (const line of sortedLines) lines.push(`DA:${line.lineNumber},${line.hitCount}`);
|
|
306
|
+
const lf = file.lines.size;
|
|
307
|
+
const lh = [...file.lines.values()].filter((l) => l.hitCount > 0).length;
|
|
308
|
+
lines.push(`LF:${lf}`);
|
|
309
|
+
lines.push(`LH:${lh}`);
|
|
310
|
+
lines.push("end_of_record");
|
|
311
|
+
}
|
|
312
|
+
return lines.join("\n") + "\n";
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Convert LCOV data to JSON summary format (compatible with istanbul/vitest).
|
|
316
|
+
*/
|
|
317
|
+
function lcovToJsonSummary(lcov) {
|
|
318
|
+
const withPct = (total, covered) => ({
|
|
319
|
+
total,
|
|
320
|
+
covered,
|
|
321
|
+
skipped: 0,
|
|
322
|
+
pct: total > 0 ? parseFloat((covered / total * 100).toFixed(2)) : 100
|
|
323
|
+
});
|
|
324
|
+
const totals = {
|
|
325
|
+
lines: {
|
|
326
|
+
total: 0,
|
|
327
|
+
covered: 0
|
|
328
|
+
},
|
|
329
|
+
functions: {
|
|
330
|
+
total: 0,
|
|
331
|
+
covered: 0
|
|
332
|
+
},
|
|
333
|
+
branches: {
|
|
334
|
+
total: 0,
|
|
335
|
+
covered: 0
|
|
336
|
+
}
|
|
337
|
+
};
|
|
338
|
+
const summary = { total: {
|
|
339
|
+
lines: withPct(0, 0),
|
|
340
|
+
statements: withPct(0, 0),
|
|
341
|
+
functions: withPct(0, 0),
|
|
342
|
+
branches: withPct(0, 0),
|
|
343
|
+
branchesTrue: {
|
|
344
|
+
total: 0,
|
|
345
|
+
covered: 0,
|
|
346
|
+
skipped: 0,
|
|
347
|
+
pct: 100
|
|
348
|
+
}
|
|
349
|
+
} };
|
|
350
|
+
for (const file of lcov.files.values()) {
|
|
351
|
+
const linesTotal = file.lines.size;
|
|
352
|
+
const linesCovered = [...file.lines.values()].filter((l) => l.hitCount > 0).length;
|
|
353
|
+
const funcsTotal = file.functions.size;
|
|
354
|
+
const funcsCovered = [...file.functions.values()].filter((f) => f.hitCount > 0).length;
|
|
355
|
+
const branchesTotal = file.branches.length;
|
|
356
|
+
const branchesCovered = file.branches.filter((b) => b.taken > 0).length;
|
|
357
|
+
summary[file.path] = {
|
|
358
|
+
lines: withPct(linesTotal, linesCovered),
|
|
359
|
+
statements: withPct(linesTotal, linesCovered),
|
|
360
|
+
functions: withPct(funcsTotal, funcsCovered),
|
|
361
|
+
branches: withPct(branchesTotal, branchesCovered)
|
|
362
|
+
};
|
|
363
|
+
totals.lines.total += linesTotal;
|
|
364
|
+
totals.lines.covered += linesCovered;
|
|
365
|
+
totals.functions.total += funcsTotal;
|
|
366
|
+
totals.functions.covered += funcsCovered;
|
|
367
|
+
totals.branches.total += branchesTotal;
|
|
368
|
+
totals.branches.covered += branchesCovered;
|
|
369
|
+
}
|
|
370
|
+
summary.total = {
|
|
371
|
+
lines: withPct(totals.lines.total, totals.lines.covered),
|
|
372
|
+
statements: withPct(totals.lines.total, totals.lines.covered),
|
|
373
|
+
functions: withPct(totals.functions.total, totals.functions.covered),
|
|
374
|
+
branches: withPct(totals.branches.total, totals.branches.covered),
|
|
375
|
+
branchesTrue: {
|
|
376
|
+
total: 0,
|
|
377
|
+
covered: 0,
|
|
378
|
+
skipped: 0,
|
|
379
|
+
pct: 100
|
|
380
|
+
}
|
|
381
|
+
};
|
|
382
|
+
return summary;
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Read and parse an LCOV file.
|
|
386
|
+
*/
|
|
387
|
+
function readLcovFile(path) {
|
|
388
|
+
return parseLcov(readFileSync(path, "utf8"));
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* Write LCOV data to a file.
|
|
392
|
+
*/
|
|
393
|
+
function writeLcovFile(path, lcov) {
|
|
394
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
395
|
+
writeFileSync(path, formatLcov(lcov));
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* Write JSON summary to a file.
|
|
399
|
+
*/
|
|
400
|
+
function writeJsonSummary(path, summary) {
|
|
401
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
402
|
+
writeFileSync(path, JSON.stringify(summary, null, 2));
|
|
403
|
+
}
|
|
404
|
+
|
|
171
405
|
//#endregion
|
|
172
406
|
//#region src/lib/coverage.ts
|
|
173
407
|
/**
|
|
@@ -273,6 +507,33 @@ async function cleanupCoverageContext(ctx) {
|
|
|
273
507
|
});
|
|
274
508
|
} catch {}
|
|
275
509
|
}
|
|
510
|
+
/**
|
|
511
|
+
* Merge external LCOV file with generated coverage.
|
|
512
|
+
* Reads the generated lcov.info, merges with external LCOV, and writes back.
|
|
513
|
+
* Also generates coverage-summary.json for badge generation.
|
|
514
|
+
*
|
|
515
|
+
* @returns Object with merged coverage percentages, or null if merge failed
|
|
516
|
+
*/
|
|
517
|
+
function mergeExternalCoverage(reportsDir, externalLcovPath) {
|
|
518
|
+
const generatedLcovPath = join(reportsDir, "lcov.info");
|
|
519
|
+
if (!existsSync(externalLcovPath)) {
|
|
520
|
+
console.error(`External LCOV file not found: ${externalLcovPath}`);
|
|
521
|
+
return null;
|
|
522
|
+
}
|
|
523
|
+
if (!existsSync(generatedLcovPath)) {
|
|
524
|
+
console.error(`Generated LCOV file not found: ${generatedLcovPath}`);
|
|
525
|
+
console.error("Make sure \"lcov\" is included in reporters");
|
|
526
|
+
return null;
|
|
527
|
+
}
|
|
528
|
+
const mergedLcov = mergeLcov(readLcovFile(externalLcovPath), readLcovFile(generatedLcovPath));
|
|
529
|
+
writeLcovFile(generatedLcovPath, mergedLcov);
|
|
530
|
+
const summary = lcovToJsonSummary(mergedLcov);
|
|
531
|
+
writeJsonSummary(join(reportsDir, "coverage-summary.json"), summary);
|
|
532
|
+
return {
|
|
533
|
+
lines: summary.total.lines.pct,
|
|
534
|
+
functions: summary.total.functions.pct
|
|
535
|
+
};
|
|
536
|
+
}
|
|
276
537
|
|
|
277
538
|
//#endregion
|
|
278
539
|
//#region src/cli/commands/run.ts
|
|
@@ -280,10 +541,32 @@ async function cleanupCoverageContext(ctx) {
|
|
|
280
541
|
* Register the run command.
|
|
281
542
|
*/
|
|
282
543
|
function registerRunCommand(program) {
|
|
283
|
-
program.command("run").description("Run golden tests").argument("[files...]", "Test files to run (default: **/*.tryscript.md)").option("--update", "Update golden files with actual output").option("--diff", "Show diff on failure (default: true)").option("--no-diff", "Hide diff on failure").option("--fail-fast", "Stop on first failure").option("--filter <pattern>", "Filter tests by name pattern").option("--verbose", "Show detailed output including passing test output").option("--quiet", "Suppress non-essential output (only show failures)").option("--coverage", "Enable code coverage collection (requires c8)").option("--coverage-dir <dir>", "Coverage output directory (default: coverage-tryscript)").option("--coverage-reporter <reporter...>", "Coverage reporters (default: text, html). Can be specified multiple times.").option("--coverage-exclude <pattern...>", "Patterns to exclude from coverage (c8 --exclude). Can be specified multiple times.").option("--coverage-exclude-node-modules", "Exclude node_modules from coverage (c8 --exclude-node-modules, default: true)").option("--no-coverage-exclude-node-modules", "Include node_modules in coverage (c8 --no-exclude-node-modules)").option("--coverage-exclude-after-remap", "Apply exclude logic after sourcemap remapping (c8 --exclude-after-remap)").option("--coverage-skip-full", "Hide files with 100% coverage (c8 --skip-full)").option("--coverage-allow-external", "Allow files from outside cwd (c8 --allowExternal)").option("--coverage-monocart", "Use monocart for accurate line counts, better for merging with vitest (c8 --experimental-monocart)").action(runCommand$1);
|
|
544
|
+
program.command("run").description("Run golden tests").argument("[files...]", "Test files to run (default: **/*.tryscript.md)").option("--update", "Update golden files with actual output").option("--diff", "Show diff on failure (default: true)").option("--no-diff", "Hide diff on failure").option("--fail-fast", "Stop on first failure").option("--filter <pattern>", "Filter tests by name pattern").option("--verbose", "Show detailed output including passing test output").option("--quiet", "Suppress non-essential output (only show failures)").option("--expand", "Expand unknown wildcards (??? and [??]) with actual output").option("--expand-generic", "Expand unknown and generic wildcards with actual output").option("--expand-all", "Expand all wildcards (including named patterns) with actual output").option("--capture-log <path>", "Write wildcard capture log to YAML file").option("--coverage", "Enable code coverage collection (requires c8)").option("--coverage-dir <dir>", "Coverage output directory (default: coverage-tryscript)").option("--coverage-reporter <reporter...>", "Coverage reporters (default: text, html). Can be specified multiple times.").option("--coverage-exclude <pattern...>", "Patterns to exclude from coverage (c8 --exclude). Can be specified multiple times.").option("--coverage-exclude-node-modules", "Exclude node_modules from coverage (c8 --exclude-node-modules, default: true)").option("--no-coverage-exclude-node-modules", "Include node_modules in coverage (c8 --no-exclude-node-modules)").option("--coverage-exclude-after-remap", "Apply exclude logic after sourcemap remapping (c8 --exclude-after-remap)").option("--coverage-skip-full", "Hide files with 100% coverage (c8 --skip-full)").option("--coverage-allow-external", "Allow files from outside cwd (c8 --allowExternal)").option("--coverage-monocart", "Use monocart for accurate line counts, better for merging with vitest (c8 --experimental-monocart)").option("--merge-lcov <path>", "Merge coverage from an existing LCOV file (e.g., from vitest --coverage)").action(runCommand$1);
|
|
545
|
+
}
|
|
546
|
+
/**
|
|
547
|
+
* Count unknown wildcard tokens (`???` and `[??]`) in expected output.
|
|
548
|
+
*/
|
|
549
|
+
function countUnknownWildcards(expectedOutput) {
|
|
550
|
+
return (expectedOutput.match(/\[\?\?]/g) ?? []).length + (expectedOutput.match(/\?\?\?\n/g) ?? []).length;
|
|
284
551
|
}
|
|
285
552
|
async function runCommand$1(files, options) {
|
|
286
553
|
const startTime = Date.now();
|
|
554
|
+
if ([
|
|
555
|
+
options.expand,
|
|
556
|
+
options.expandGeneric,
|
|
557
|
+
options.expandAll
|
|
558
|
+
].filter(Boolean).length > 1) {
|
|
559
|
+
logError("--expand, --expand-generic, and --expand-all are mutually exclusive");
|
|
560
|
+
process.exit(1);
|
|
561
|
+
}
|
|
562
|
+
let expandLevel;
|
|
563
|
+
if (options.expand) expandLevel = "unknown";
|
|
564
|
+
else if (options.expandGeneric) expandLevel = "generic";
|
|
565
|
+
else if (options.expandAll) expandLevel = "all";
|
|
566
|
+
if (expandLevel && options.update) {
|
|
567
|
+
logError("--expand* flags and --update are mutually exclusive");
|
|
568
|
+
process.exit(1);
|
|
569
|
+
}
|
|
287
570
|
const opts = {
|
|
288
571
|
diff: options.diff !== false,
|
|
289
572
|
verbose: options.verbose ?? false,
|
|
@@ -309,20 +592,32 @@ async function runCommand$1(files, options) {
|
|
|
309
592
|
logError("Coverage requires c8. Install with: npm install -D c8");
|
|
310
593
|
process.exit(1);
|
|
311
594
|
}
|
|
595
|
+
let reporters = options.coverageReporter ?? globalConfig.coverage?.reporters;
|
|
596
|
+
if (options.mergeLcov) {
|
|
597
|
+
if (!reporters) reporters = [
|
|
598
|
+
"text",
|
|
599
|
+
"html",
|
|
600
|
+
"lcov"
|
|
601
|
+
];
|
|
602
|
+
else if (!reporters.includes("lcov")) reporters = [...reporters, "lcov"];
|
|
603
|
+
}
|
|
312
604
|
coverageCtx = await createCoverageContext({
|
|
313
605
|
...globalConfig.coverage,
|
|
314
606
|
reportsDir: options.coverageDir ?? globalConfig.coverage?.reportsDir,
|
|
315
|
-
reporters
|
|
607
|
+
reporters,
|
|
316
608
|
exclude: options.coverageExclude ?? globalConfig.coverage?.exclude,
|
|
317
609
|
excludeNodeModules: options.coverageExcludeNodeModules ?? globalConfig.coverage?.excludeNodeModules,
|
|
318
610
|
excludeAfterRemap: options.coverageExcludeAfterRemap ?? globalConfig.coverage?.excludeAfterRemap,
|
|
319
611
|
skipFull: options.coverageSkipFull ?? globalConfig.coverage?.skipFull,
|
|
320
612
|
allowExternal: options.coverageAllowExternal ?? globalConfig.coverage?.allowExternal,
|
|
321
|
-
monocart: options.coverageMonocart ?? globalConfig.coverage?.monocart
|
|
613
|
+
monocart: options.coverageMonocart ?? globalConfig.coverage?.monocart,
|
|
614
|
+
mergeLcov: options.mergeLcov ?? globalConfig.coverage?.mergeLcov
|
|
322
615
|
});
|
|
323
616
|
coverageEnv = getCoverageEnv(coverageCtx);
|
|
324
617
|
}
|
|
325
618
|
const fileResults = [];
|
|
619
|
+
const fileContexts = /* @__PURE__ */ new Map();
|
|
620
|
+
const filePatterns = /* @__PURE__ */ new Map();
|
|
326
621
|
let shouldStop = false;
|
|
327
622
|
for (const filePath of testFiles) {
|
|
328
623
|
if (shouldStop) break;
|
|
@@ -338,6 +633,7 @@ async function runCommand$1(files, options) {
|
|
|
338
633
|
if (blocksToRun.length === 0) continue;
|
|
339
634
|
const ctx = await createExecutionContext(config, filePath, coverageEnv);
|
|
340
635
|
const results = [];
|
|
636
|
+
let fileContext;
|
|
341
637
|
try {
|
|
342
638
|
for (const block of blocksToRun) {
|
|
343
639
|
const result = await runBlock(block, ctx);
|
|
@@ -364,6 +660,12 @@ async function runCommand$1(files, options) {
|
|
|
364
660
|
}
|
|
365
661
|
}
|
|
366
662
|
await runAfterHook(ctx);
|
|
663
|
+
fileContext = {
|
|
664
|
+
root: ctx.testDir,
|
|
665
|
+
cwd: ctx.cwd
|
|
666
|
+
};
|
|
667
|
+
fileContexts.set(filePath, fileContext);
|
|
668
|
+
filePatterns.set(filePath, config.patterns ?? {});
|
|
367
669
|
} finally {
|
|
368
670
|
await cleanupExecutionContext(ctx);
|
|
369
671
|
}
|
|
@@ -379,7 +681,14 @@ async function runCommand$1(files, options) {
|
|
|
379
681
|
const { updated, changes } = await updateTestFile(testFile, results);
|
|
380
682
|
if (updated) console.error(colors.warn(` ${status.update} Updated: ${changes.join(", ")}`));
|
|
381
683
|
}
|
|
684
|
+
if (expandLevel && fileContext) {
|
|
685
|
+
const { expanded, expandedCount, changes } = await expandTestFile(testFile, results, expandLevel, fileContext, config.patterns ?? {});
|
|
686
|
+
if (expanded) console.error(colors.warn(` ${status.update} Expanded ${expandedCount} wildcard(s): ${changes.join(", ")}`));
|
|
687
|
+
}
|
|
382
688
|
}
|
|
689
|
+
let totalUnknownWildcards = 0;
|
|
690
|
+
for (const fr of fileResults) for (const block of fr.file.blocks) totalUnknownWildcards += countUnknownWildcards(block.expectedOutput);
|
|
691
|
+
if (totalUnknownWildcards > 0) logWarn(`${totalUnknownWildcards} unknown wildcard(s) found (??? or [??]). These are temporary and should be expanded. Use --expand to fill them in.`);
|
|
383
692
|
const summary = {
|
|
384
693
|
files: fileResults,
|
|
385
694
|
totalPassed: fileResults.reduce((sum, f) => sum + f.results.filter((r) => r.passed).length, 0),
|
|
@@ -388,11 +697,25 @@ async function runCommand$1(files, options) {
|
|
|
388
697
|
duration: Date.now() - startTime
|
|
389
698
|
};
|
|
390
699
|
reportSummary(summary, opts);
|
|
700
|
+
if (options.captureLog) try {
|
|
701
|
+
await writeCaptureLog(options.captureLog, fileResults, (file) => fileContexts.get(file.path) ?? {
|
|
702
|
+
root: process.cwd(),
|
|
703
|
+
cwd: process.cwd()
|
|
704
|
+
}, (file) => filePatterns.get(file.path) ?? {});
|
|
705
|
+
console.error(colors.info(`Capture log written to ${options.captureLog}`));
|
|
706
|
+
} catch (error) {
|
|
707
|
+
logError(`Failed to write capture log: ${error instanceof Error ? error.message : String(error)}`);
|
|
708
|
+
}
|
|
391
709
|
if (coverageCtx) {
|
|
392
710
|
console.error("\nGenerating coverage report...");
|
|
393
711
|
try {
|
|
394
712
|
await generateCoverageReport(coverageCtx);
|
|
395
713
|
console.error(colors.success(`Coverage report written to ${coverageCtx.options.reportsDir}/`));
|
|
714
|
+
if (coverageCtx.options.mergeLcov) {
|
|
715
|
+
console.error(`Merging with external coverage: ${coverageCtx.options.mergeLcov}`);
|
|
716
|
+
const merged = mergeExternalCoverage(coverageCtx.options.reportsDir, coverageCtx.options.mergeLcov);
|
|
717
|
+
if (merged) console.error(colors.success(`Merged coverage: ${merged.lines}% lines, ${merged.functions}% functions`));
|
|
718
|
+
}
|
|
396
719
|
} catch (error) {
|
|
397
720
|
logError(`Failed to generate coverage report: ${error instanceof Error ? error.message : String(error)}`);
|
|
398
721
|
} finally {
|
|
@@ -408,7 +731,7 @@ async function runCommand$1(files, options) {
|
|
|
408
731
|
* Register the coverage command.
|
|
409
732
|
*/
|
|
410
733
|
function registerCoverageCommand(program) {
|
|
411
|
-
program.command("coverage").description("Run commands with merged V8 coverage").argument("<commands...>", "Commands to run (each will inherit coverage environment)").option("--reports-dir <dir>", "Coverage output directory (default: coverage)").option("--reporters <reporters>", "Comma-separated coverage reporters (default: text,json,json-summary,lcov,html)").option("--include <patterns>", "Comma-separated patterns to include in coverage").option("--exclude <patterns>", "Comma-separated patterns to exclude from coverage").option("--exclude-node-modules", "Exclude node_modules from coverage (default: true)", true).option("--no-exclude-node-modules", "Include node_modules in coverage").option("--exclude-after-remap", "Apply exclude logic after sourcemap remapping").option("--skip-full", "Hide files with 100% coverage").option("--allow-external", "Allow files from outside cwd").option("--monocart", "Use monocart for accurate line counts (recommended for merging)").option("--src <dir>", "Source directory for sourcemap remapping (default: src)").option("--verbose", "Show coverage summary after each command for debugging").action(coverageCommand);
|
|
734
|
+
program.command("coverage").description("Run commands with merged V8 coverage").argument("<commands...>", "Commands to run (each will inherit coverage environment)").option("--reports-dir <dir>", "Coverage output directory (default: coverage)").option("--reporters <reporters>", "Comma-separated coverage reporters (default: text,json,json-summary,lcov,html)").option("--include <patterns>", "Comma-separated patterns to include in coverage").option("--exclude <patterns>", "Comma-separated patterns to exclude from coverage").option("--exclude-node-modules", "Exclude node_modules from coverage (default: true)", true).option("--no-exclude-node-modules", "Include node_modules in coverage").option("--exclude-after-remap", "Apply exclude logic after sourcemap remapping").option("--skip-full", "Hide files with 100% coverage").option("--allow-external", "Allow files from outside cwd").option("--monocart", "Use monocart for accurate line counts (recommended for merging)").option("--src <dir>", "Source directory for sourcemap remapping (default: src)").option("--verbose", "Show coverage summary after each command for debugging").option("--merge-lcov <path>", "Merge coverage from an existing LCOV file (e.g., from vitest --coverage)").action(coverageCommand);
|
|
412
735
|
}
|
|
413
736
|
/**
|
|
414
737
|
* Run a command with inherited coverage environment.
|
|
@@ -604,12 +927,32 @@ async function coverageCommand(commands, options) {
|
|
|
604
927
|
if (parsedOptions.verbose && stats.fileCount > 0) await generateTextReport(coverageTemp, parsedOptions, command);
|
|
605
928
|
previousFileCount = stats.fileCount;
|
|
606
929
|
}
|
|
607
|
-
console.error(colors.info("\n=== Generating
|
|
930
|
+
console.error(colors.info("\n=== Generating coverage report ==="));
|
|
608
931
|
if (!await generateReport(coverageTemp, parsedOptions)) {
|
|
609
932
|
logError("Failed to generate coverage report");
|
|
610
933
|
process.exit(1);
|
|
611
934
|
}
|
|
612
|
-
|
|
935
|
+
const reportsDir = parsedOptions.reportsDir ?? "coverage";
|
|
936
|
+
if (parsedOptions.mergeLcov) {
|
|
937
|
+
const externalLcovPath = parsedOptions.mergeLcov;
|
|
938
|
+
const generatedLcovPath = join(reportsDir, "lcov.info");
|
|
939
|
+
if (!existsSync(externalLcovPath)) {
|
|
940
|
+
logError(`External LCOV file not found: ${externalLcovPath}`);
|
|
941
|
+
process.exit(1);
|
|
942
|
+
}
|
|
943
|
+
if (!existsSync(generatedLcovPath)) {
|
|
944
|
+
logError(`Generated LCOV file not found: ${generatedLcovPath}`);
|
|
945
|
+
logError("Make sure \"lcov\" is included in reporters");
|
|
946
|
+
process.exit(1);
|
|
947
|
+
}
|
|
948
|
+
console.error(colors.info(`\nMerging with external coverage: ${externalLcovPath}`));
|
|
949
|
+
const mergedLcov = mergeLcov(readLcovFile(externalLcovPath), readLcovFile(generatedLcovPath));
|
|
950
|
+
writeLcovFile(generatedLcovPath, mergedLcov);
|
|
951
|
+
const summary = lcovToJsonSummary(mergedLcov);
|
|
952
|
+
writeJsonSummary(join(reportsDir, "coverage-summary.json"), summary);
|
|
953
|
+
console.error(colors.success(`\nMerged coverage: ${summary.total.lines.pct}% lines, ${summary.total.functions.pct}% functions`));
|
|
954
|
+
}
|
|
955
|
+
console.error(colors.success(`\nCoverage report written to ${reportsDir}/`));
|
|
613
956
|
} finally {
|
|
614
957
|
await rm(coverageTemp, {
|
|
615
958
|
recursive: true,
|