tryscript 0.1.2 → 0.1.4
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 +39 -50
- package/dist/bin.cjs +382 -13
- package/dist/bin.cjs.map +1 -1
- package/dist/bin.mjs +384 -15
- package/dist/bin.mjs.map +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +8 -1
- package/dist/index.d.mts +8 -1
- package/dist/index.mjs +1 -1
- package/dist/{src-BR3CZ3tc.cjs → src-BGWMAShO.cjs} +4 -3
- package/dist/src-BGWMAShO.cjs.map +1 -0
- package/dist/{src-DKrim0QL.mjs → src-D60Uy8QA.mjs} +4 -3
- package/dist/src-D60Uy8QA.mjs.map +1 -0
- package/docs/tryscript-reference.md +84 -22
- package/package.json +4 -2
- package/dist/src-BR3CZ3tc.cjs.map +0 -1
- package/dist/src-DKrim0QL.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# tryscript
|
|
2
2
|
|
|
3
|
-
[](https://github.com/jlevy/tryscript/actions/
|
|
4
|
-
[](https://
|
|
3
|
+
[](https://github.com/jlevy/tryscript/actions/runs/20828303484)
|
|
4
|
+
[](https://github.com/jlevy/tryscript/actions/runs/20828303484)
|
|
5
5
|
[](https://www.npmjs.com/package/tryscript)
|
|
6
6
|
[](https://x.com/ojoshe)
|
|
7
7
|
|
|
@@ -22,25 +22,51 @@ Golden testing for CLI applications - a TypeScript port of [trycmd](https://gith
|
|
|
22
22
|
Write CLI tests as Markdown. tryscript runs commands, captures output, and compares against expected results. Tests become documentation; documentation becomes tests.
|
|
23
23
|
|
|
24
24
|
````markdown
|
|
25
|
-
|
|
25
|
+
---
|
|
26
|
+
env:
|
|
27
|
+
NO_COLOR: "1"
|
|
28
|
+
sandbox: true
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
# Test: CLI help
|
|
26
32
|
|
|
27
33
|
```console
|
|
28
|
-
$
|
|
29
|
-
|
|
34
|
+
$ my-cli --help
|
|
35
|
+
Usage: my-cli [options] <command>
|
|
36
|
+
|
|
37
|
+
Options:
|
|
38
|
+
--version Show version
|
|
39
|
+
--help Show this help
|
|
40
|
+
...
|
|
30
41
|
? 0
|
|
31
42
|
```
|
|
32
43
|
|
|
33
|
-
# Test:
|
|
44
|
+
# Test: Version output
|
|
34
45
|
|
|
35
46
|
```console
|
|
36
|
-
$
|
|
37
|
-
[..]
|
|
38
|
-
|
|
47
|
+
$ my-cli --version
|
|
48
|
+
my-cli v[..]
|
|
49
|
+
? 0
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
# Test: Error handling
|
|
53
|
+
|
|
54
|
+
```console
|
|
55
|
+
$ my-cli unknown-command 2>&1
|
|
56
|
+
Error: unknown command 'unknown-command'
|
|
57
|
+
? 1
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
# Test: Check output file contents
|
|
61
|
+
|
|
62
|
+
```console
|
|
63
|
+
$ my-cli process data.json > output.txt && grep "success" output.txt
|
|
64
|
+
[..]success[..]
|
|
39
65
|
? 0
|
|
40
66
|
```
|
|
41
67
|
````
|
|
42
68
|
|
|
43
|
-
The `[..]` matches any text on that line. The `...` matches zero or more lines. These "elision patterns" let tests handle dynamic output gracefully.
|
|
69
|
+
The `[..]` matches any text on that line. The `...` matches zero or more lines. These "elision patterns" let tests handle dynamic output gracefully. Any shell command works - pipes, redirects, environment variables, etc.
|
|
44
70
|
|
|
45
71
|
## Quick Start
|
|
46
72
|
|
|
@@ -68,46 +94,7 @@ npx tryscript run --update tests/
|
|
|
68
94
|
- **Custom patterns** - Define regex patterns for timestamps, versions, UUIDs
|
|
69
95
|
- **Update mode** - Regenerate expected output with `--update`
|
|
70
96
|
- **Sandbox mode** - Isolate tests in temp directories
|
|
71
|
-
- **Code coverage** - Track coverage from subprocess execution with `--coverage`
|
|
72
|
-
|
|
73
|
-
## Example Test File
|
|
74
|
-
|
|
75
|
-
````markdown
|
|
76
|
-
---
|
|
77
|
-
env:
|
|
78
|
-
NO_COLOR: "1"
|
|
79
|
-
sandbox: true
|
|
80
|
-
---
|
|
81
|
-
|
|
82
|
-
# Test: CLI help
|
|
83
|
-
|
|
84
|
-
```console
|
|
85
|
-
$ my-cli --help
|
|
86
|
-
Usage: my-cli [options] <command>
|
|
87
|
-
|
|
88
|
-
Options:
|
|
89
|
-
--version Show version
|
|
90
|
-
--help Show this help
|
|
91
|
-
...
|
|
92
|
-
? 0
|
|
93
|
-
```
|
|
94
|
-
|
|
95
|
-
# Test: Version output
|
|
96
|
-
|
|
97
|
-
```console
|
|
98
|
-
$ my-cli --version
|
|
99
|
-
my-cli v[..]
|
|
100
|
-
? 0
|
|
101
|
-
```
|
|
102
|
-
|
|
103
|
-
# Test: Error handling
|
|
104
|
-
|
|
105
|
-
```console
|
|
106
|
-
$ my-cli unknown-command 2>&1
|
|
107
|
-
Error: unknown command 'unknown-command'
|
|
108
|
-
? 1
|
|
109
|
-
```
|
|
110
|
-
````
|
|
97
|
+
- **Code coverage** - Track coverage from subprocess execution with `--coverage` (experimental; use `--coverage-monocart` for best accuracy)
|
|
111
98
|
|
|
112
99
|
## CLI Reference
|
|
113
100
|
|
|
@@ -134,6 +121,8 @@ For complete syntax reference, run `tryscript docs` or see the [reference docume
|
|
|
134
121
|
| `--coverage-exclude-node-modules` | Exclude node_modules from coverage (default: true) |
|
|
135
122
|
| `--coverage-exclude <pattern>` | Exclude patterns from coverage |
|
|
136
123
|
|
|
124
|
+
> **Note**: Coverage features are experimental. See the [reference documentation](packages/tryscript/docs/tryscript-reference.md#code-coverage) for details on merged coverage, monocart integration, and sourcemap requirements.
|
|
125
|
+
|
|
137
126
|
## Development
|
|
138
127
|
|
|
139
128
|
```bash
|
package/dist/bin.cjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
|
|
4
|
-
const require_src = require('./src-
|
|
4
|
+
const require_src = require('./src-BGWMAShO.cjs');
|
|
5
5
|
let node_url = require("node:url");
|
|
6
6
|
let node_fs = require("node:fs");
|
|
7
7
|
let node_path = require("node:path");
|
|
@@ -170,6 +170,237 @@ function buildUpdatedBlock(block, result) {
|
|
|
170
170
|
return lines.join("\n");
|
|
171
171
|
}
|
|
172
172
|
|
|
173
|
+
//#endregion
|
|
174
|
+
//#region src/lib/lcov.ts
|
|
175
|
+
/**
|
|
176
|
+
* LCOV parsing, merging, and writing utilities.
|
|
177
|
+
*
|
|
178
|
+
* LCOV format reference:
|
|
179
|
+
* - SF: Source file path
|
|
180
|
+
* - DA:linenum,hitcount - Line data
|
|
181
|
+
* - FN:linenum,funcname - Function definition
|
|
182
|
+
* - FNDA:hitcount,funcname - Function hit data
|
|
183
|
+
* - FNF: Functions found count
|
|
184
|
+
* - FNH: Functions hit count
|
|
185
|
+
* - BRF: Branches found count
|
|
186
|
+
* - BRH: Branches hit count
|
|
187
|
+
* - BRDA:line,block,branch,taken - Branch data
|
|
188
|
+
* - LF: Lines found count
|
|
189
|
+
* - LH: Lines hit count
|
|
190
|
+
* - end_of_record - End of file record
|
|
191
|
+
*/
|
|
192
|
+
/**
|
|
193
|
+
* Parse LCOV content into structured data.
|
|
194
|
+
*/
|
|
195
|
+
function parseLcov(content) {
|
|
196
|
+
const files = /* @__PURE__ */ new Map();
|
|
197
|
+
let currentFile = null;
|
|
198
|
+
for (const line of content.split("\n")) {
|
|
199
|
+
const trimmed = line.trim();
|
|
200
|
+
if (trimmed.startsWith("SF:")) {
|
|
201
|
+
const path = trimmed.slice(3);
|
|
202
|
+
currentFile = {
|
|
203
|
+
path,
|
|
204
|
+
lines: /* @__PURE__ */ new Map(),
|
|
205
|
+
functions: /* @__PURE__ */ new Map(),
|
|
206
|
+
branches: []
|
|
207
|
+
};
|
|
208
|
+
files.set(path, currentFile);
|
|
209
|
+
} else if (trimmed.startsWith("DA:") && currentFile) {
|
|
210
|
+
const parts = trimmed.slice(3).split(",");
|
|
211
|
+
const lineNumber = parseInt(parts[0], 10);
|
|
212
|
+
const hitCount = parseInt(parts[1], 10);
|
|
213
|
+
currentFile.lines.set(lineNumber, {
|
|
214
|
+
lineNumber,
|
|
215
|
+
hitCount
|
|
216
|
+
});
|
|
217
|
+
} else if (trimmed.startsWith("FN:") && currentFile) {
|
|
218
|
+
const parts = trimmed.slice(3).split(",");
|
|
219
|
+
const lineNumber = parseInt(parts[0], 10);
|
|
220
|
+
const name = parts.slice(1).join(",");
|
|
221
|
+
if (!currentFile.functions.has(name)) currentFile.functions.set(name, {
|
|
222
|
+
name,
|
|
223
|
+
lineNumber,
|
|
224
|
+
hitCount: 0
|
|
225
|
+
});
|
|
226
|
+
else currentFile.functions.get(name).lineNumber = lineNumber;
|
|
227
|
+
} else if (trimmed.startsWith("FNDA:") && currentFile) {
|
|
228
|
+
const parts = trimmed.slice(5).split(",");
|
|
229
|
+
const hitCount = parseInt(parts[0], 10);
|
|
230
|
+
const name = parts.slice(1).join(",");
|
|
231
|
+
if (currentFile.functions.has(name)) currentFile.functions.get(name).hitCount = hitCount;
|
|
232
|
+
else currentFile.functions.set(name, {
|
|
233
|
+
name,
|
|
234
|
+
lineNumber: 0,
|
|
235
|
+
hitCount
|
|
236
|
+
});
|
|
237
|
+
} else if (trimmed.startsWith("BRDA:") && currentFile) {
|
|
238
|
+
const parts = trimmed.slice(5).split(",");
|
|
239
|
+
currentFile.branches.push({
|
|
240
|
+
line: parseInt(parts[0], 10),
|
|
241
|
+
block: parseInt(parts[1], 10),
|
|
242
|
+
branch: parseInt(parts[2], 10),
|
|
243
|
+
taken: parts[3] === "-" ? -1 : parseInt(parts[3], 10)
|
|
244
|
+
});
|
|
245
|
+
} else if (trimmed === "end_of_record") currentFile = null;
|
|
246
|
+
}
|
|
247
|
+
return { files };
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Merge multiple LCOV data structures, taking max hit counts.
|
|
251
|
+
*/
|
|
252
|
+
function mergeLcov(...lcovs) {
|
|
253
|
+
const merged = /* @__PURE__ */ new Map();
|
|
254
|
+
for (const lcov of lcovs) for (const [path, file] of lcov.files) if (!merged.has(path)) merged.set(path, {
|
|
255
|
+
path,
|
|
256
|
+
lines: new Map(file.lines),
|
|
257
|
+
functions: new Map(file.functions),
|
|
258
|
+
branches: [...file.branches]
|
|
259
|
+
});
|
|
260
|
+
else {
|
|
261
|
+
const existing = merged.get(path);
|
|
262
|
+
for (const [lineNum, lineData] of file.lines) {
|
|
263
|
+
const existingLine = existing.lines.get(lineNum);
|
|
264
|
+
if (existingLine) existingLine.hitCount = Math.max(existingLine.hitCount, lineData.hitCount);
|
|
265
|
+
else existing.lines.set(lineNum, { ...lineData });
|
|
266
|
+
}
|
|
267
|
+
for (const [name, funcData] of file.functions) {
|
|
268
|
+
const existingFunc = existing.functions.get(name);
|
|
269
|
+
if (existingFunc) existingFunc.hitCount = Math.max(existingFunc.hitCount, funcData.hitCount);
|
|
270
|
+
else existing.functions.set(name, { ...funcData });
|
|
271
|
+
}
|
|
272
|
+
for (const branch of file.branches) {
|
|
273
|
+
const existingBranch = existing.branches.find((b) => b.line === branch.line && b.block === branch.block && b.branch === branch.branch);
|
|
274
|
+
if (existingBranch) {
|
|
275
|
+
if (branch.taken >= 0) existingBranch.taken = existingBranch.taken >= 0 ? Math.max(existingBranch.taken, branch.taken) : branch.taken;
|
|
276
|
+
} else existing.branches.push({ ...branch });
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return { files: merged };
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Convert LCOV data back to LCOV format string.
|
|
283
|
+
*/
|
|
284
|
+
function formatLcov(lcov) {
|
|
285
|
+
const lines = [];
|
|
286
|
+
for (const file of lcov.files.values()) {
|
|
287
|
+
lines.push(`SF:${file.path}`);
|
|
288
|
+
const sortedFunctions = [...file.functions.values()].sort((a, b) => a.lineNumber - b.lineNumber);
|
|
289
|
+
for (const func of sortedFunctions) lines.push(`FN:${func.lineNumber},${func.name}`);
|
|
290
|
+
for (const func of sortedFunctions) lines.push(`FNDA:${func.hitCount},${func.name}`);
|
|
291
|
+
const fnf = file.functions.size;
|
|
292
|
+
const fnh = [...file.functions.values()].filter((f) => f.hitCount > 0).length;
|
|
293
|
+
lines.push(`FNF:${fnf}`);
|
|
294
|
+
lines.push(`FNH:${fnh}`);
|
|
295
|
+
for (const branch of file.branches) {
|
|
296
|
+
const taken = branch.taken < 0 ? "-" : branch.taken.toString();
|
|
297
|
+
lines.push(`BRDA:${branch.line},${branch.block},${branch.branch},${taken}`);
|
|
298
|
+
}
|
|
299
|
+
const brf = file.branches.length;
|
|
300
|
+
const brh = file.branches.filter((b) => b.taken > 0).length;
|
|
301
|
+
lines.push(`BRF:${brf}`);
|
|
302
|
+
lines.push(`BRH:${brh}`);
|
|
303
|
+
const sortedLines = [...file.lines.values()].sort((a, b) => a.lineNumber - b.lineNumber);
|
|
304
|
+
for (const line of sortedLines) lines.push(`DA:${line.lineNumber},${line.hitCount}`);
|
|
305
|
+
const lf = file.lines.size;
|
|
306
|
+
const lh = [...file.lines.values()].filter((l) => l.hitCount > 0).length;
|
|
307
|
+
lines.push(`LF:${lf}`);
|
|
308
|
+
lines.push(`LH:${lh}`);
|
|
309
|
+
lines.push("end_of_record");
|
|
310
|
+
}
|
|
311
|
+
return lines.join("\n") + "\n";
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Convert LCOV data to JSON summary format (compatible with istanbul/vitest).
|
|
315
|
+
*/
|
|
316
|
+
function lcovToJsonSummary(lcov) {
|
|
317
|
+
const withPct = (total, covered) => ({
|
|
318
|
+
total,
|
|
319
|
+
covered,
|
|
320
|
+
skipped: 0,
|
|
321
|
+
pct: total > 0 ? parseFloat((covered / total * 100).toFixed(2)) : 100
|
|
322
|
+
});
|
|
323
|
+
const totals = {
|
|
324
|
+
lines: {
|
|
325
|
+
total: 0,
|
|
326
|
+
covered: 0
|
|
327
|
+
},
|
|
328
|
+
functions: {
|
|
329
|
+
total: 0,
|
|
330
|
+
covered: 0
|
|
331
|
+
},
|
|
332
|
+
branches: {
|
|
333
|
+
total: 0,
|
|
334
|
+
covered: 0
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
const summary = { total: {
|
|
338
|
+
lines: withPct(0, 0),
|
|
339
|
+
statements: withPct(0, 0),
|
|
340
|
+
functions: withPct(0, 0),
|
|
341
|
+
branches: withPct(0, 0),
|
|
342
|
+
branchesTrue: {
|
|
343
|
+
total: 0,
|
|
344
|
+
covered: 0,
|
|
345
|
+
skipped: 0,
|
|
346
|
+
pct: 100
|
|
347
|
+
}
|
|
348
|
+
} };
|
|
349
|
+
for (const file of lcov.files.values()) {
|
|
350
|
+
const linesTotal = file.lines.size;
|
|
351
|
+
const linesCovered = [...file.lines.values()].filter((l) => l.hitCount > 0).length;
|
|
352
|
+
const funcsTotal = file.functions.size;
|
|
353
|
+
const funcsCovered = [...file.functions.values()].filter((f) => f.hitCount > 0).length;
|
|
354
|
+
const branchesTotal = file.branches.length;
|
|
355
|
+
const branchesCovered = file.branches.filter((b) => b.taken > 0).length;
|
|
356
|
+
summary[file.path] = {
|
|
357
|
+
lines: withPct(linesTotal, linesCovered),
|
|
358
|
+
statements: withPct(linesTotal, linesCovered),
|
|
359
|
+
functions: withPct(funcsTotal, funcsCovered),
|
|
360
|
+
branches: withPct(branchesTotal, branchesCovered)
|
|
361
|
+
};
|
|
362
|
+
totals.lines.total += linesTotal;
|
|
363
|
+
totals.lines.covered += linesCovered;
|
|
364
|
+
totals.functions.total += funcsTotal;
|
|
365
|
+
totals.functions.covered += funcsCovered;
|
|
366
|
+
totals.branches.total += branchesTotal;
|
|
367
|
+
totals.branches.covered += branchesCovered;
|
|
368
|
+
}
|
|
369
|
+
summary.total = {
|
|
370
|
+
lines: withPct(totals.lines.total, totals.lines.covered),
|
|
371
|
+
statements: withPct(totals.lines.total, totals.lines.covered),
|
|
372
|
+
functions: withPct(totals.functions.total, totals.functions.covered),
|
|
373
|
+
branches: withPct(totals.branches.total, totals.branches.covered),
|
|
374
|
+
branchesTrue: {
|
|
375
|
+
total: 0,
|
|
376
|
+
covered: 0,
|
|
377
|
+
skipped: 0,
|
|
378
|
+
pct: 100
|
|
379
|
+
}
|
|
380
|
+
};
|
|
381
|
+
return summary;
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* Read and parse an LCOV file.
|
|
385
|
+
*/
|
|
386
|
+
function readLcovFile(path) {
|
|
387
|
+
return parseLcov((0, node_fs.readFileSync)(path, "utf8"));
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* Write LCOV data to a file.
|
|
391
|
+
*/
|
|
392
|
+
function writeLcovFile(path, lcov) {
|
|
393
|
+
(0, node_fs.mkdirSync)((0, node_path.dirname)(path), { recursive: true });
|
|
394
|
+
(0, node_fs.writeFileSync)(path, formatLcov(lcov));
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* Write JSON summary to a file.
|
|
398
|
+
*/
|
|
399
|
+
function writeJsonSummary(path, summary) {
|
|
400
|
+
(0, node_fs.mkdirSync)((0, node_path.dirname)(path), { recursive: true });
|
|
401
|
+
(0, node_fs.writeFileSync)(path, JSON.stringify(summary, null, 2));
|
|
402
|
+
}
|
|
403
|
+
|
|
173
404
|
//#endregion
|
|
174
405
|
//#region src/lib/coverage.ts
|
|
175
406
|
/**
|
|
@@ -275,6 +506,33 @@ async function cleanupCoverageContext(ctx) {
|
|
|
275
506
|
});
|
|
276
507
|
} catch {}
|
|
277
508
|
}
|
|
509
|
+
/**
|
|
510
|
+
* Merge external LCOV file with generated coverage.
|
|
511
|
+
* Reads the generated lcov.info, merges with external LCOV, and writes back.
|
|
512
|
+
* Also generates coverage-summary.json for badge generation.
|
|
513
|
+
*
|
|
514
|
+
* @returns Object with merged coverage percentages, or null if merge failed
|
|
515
|
+
*/
|
|
516
|
+
function mergeExternalCoverage(reportsDir, externalLcovPath) {
|
|
517
|
+
const generatedLcovPath = (0, node_path.join)(reportsDir, "lcov.info");
|
|
518
|
+
if (!(0, node_fs.existsSync)(externalLcovPath)) {
|
|
519
|
+
console.error(`External LCOV file not found: ${externalLcovPath}`);
|
|
520
|
+
return null;
|
|
521
|
+
}
|
|
522
|
+
if (!(0, node_fs.existsSync)(generatedLcovPath)) {
|
|
523
|
+
console.error(`Generated LCOV file not found: ${generatedLcovPath}`);
|
|
524
|
+
console.error("Make sure \"lcov\" is included in reporters");
|
|
525
|
+
return null;
|
|
526
|
+
}
|
|
527
|
+
const mergedLcov = mergeLcov(readLcovFile(externalLcovPath), readLcovFile(generatedLcovPath));
|
|
528
|
+
writeLcovFile(generatedLcovPath, mergedLcov);
|
|
529
|
+
const summary = lcovToJsonSummary(mergedLcov);
|
|
530
|
+
writeJsonSummary((0, node_path.join)(reportsDir, "coverage-summary.json"), summary);
|
|
531
|
+
return {
|
|
532
|
+
lines: summary.total.lines.pct,
|
|
533
|
+
functions: summary.total.functions.pct
|
|
534
|
+
};
|
|
535
|
+
}
|
|
278
536
|
|
|
279
537
|
//#endregion
|
|
280
538
|
//#region src/cli/commands/run.ts
|
|
@@ -282,7 +540,7 @@ async function cleanupCoverageContext(ctx) {
|
|
|
282
540
|
* Register the run command.
|
|
283
541
|
*/
|
|
284
542
|
function registerRunCommand(program) {
|
|
285
|
-
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);
|
|
543
|
+
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)").option("--merge-lcov <path>", "Merge coverage from an existing LCOV file (e.g., from vitest --coverage)").action(runCommand$1);
|
|
286
544
|
}
|
|
287
545
|
async function runCommand$1(files, options) {
|
|
288
546
|
const startTime = Date.now();
|
|
@@ -311,16 +569,26 @@ async function runCommand$1(files, options) {
|
|
|
311
569
|
logError("Coverage requires c8. Install with: npm install -D c8");
|
|
312
570
|
process.exit(1);
|
|
313
571
|
}
|
|
572
|
+
let reporters = options.coverageReporter ?? globalConfig.coverage?.reporters;
|
|
573
|
+
if (options.mergeLcov) {
|
|
574
|
+
if (!reporters) reporters = [
|
|
575
|
+
"text",
|
|
576
|
+
"html",
|
|
577
|
+
"lcov"
|
|
578
|
+
];
|
|
579
|
+
else if (!reporters.includes("lcov")) reporters = [...reporters, "lcov"];
|
|
580
|
+
}
|
|
314
581
|
coverageCtx = await createCoverageContext({
|
|
315
582
|
...globalConfig.coverage,
|
|
316
583
|
reportsDir: options.coverageDir ?? globalConfig.coverage?.reportsDir,
|
|
317
|
-
reporters
|
|
584
|
+
reporters,
|
|
318
585
|
exclude: options.coverageExclude ?? globalConfig.coverage?.exclude,
|
|
319
586
|
excludeNodeModules: options.coverageExcludeNodeModules ?? globalConfig.coverage?.excludeNodeModules,
|
|
320
587
|
excludeAfterRemap: options.coverageExcludeAfterRemap ?? globalConfig.coverage?.excludeAfterRemap,
|
|
321
588
|
skipFull: options.coverageSkipFull ?? globalConfig.coverage?.skipFull,
|
|
322
589
|
allowExternal: options.coverageAllowExternal ?? globalConfig.coverage?.allowExternal,
|
|
323
|
-
monocart: options.coverageMonocart ?? globalConfig.coverage?.monocart
|
|
590
|
+
monocart: options.coverageMonocart ?? globalConfig.coverage?.monocart,
|
|
591
|
+
mergeLcov: options.mergeLcov ?? globalConfig.coverage?.mergeLcov
|
|
324
592
|
});
|
|
325
593
|
coverageEnv = getCoverageEnv(coverageCtx);
|
|
326
594
|
}
|
|
@@ -394,6 +662,11 @@ async function runCommand$1(files, options) {
|
|
|
394
662
|
console.error("\nGenerating coverage report...");
|
|
395
663
|
try {
|
|
396
664
|
await generateCoverageReport(coverageCtx);
|
|
665
|
+
if (coverageCtx.options.mergeLcov) {
|
|
666
|
+
console.error(`Merging with external coverage: ${coverageCtx.options.mergeLcov}`);
|
|
667
|
+
const merged = mergeExternalCoverage(coverageCtx.options.reportsDir, coverageCtx.options.mergeLcov);
|
|
668
|
+
if (merged) console.error(colors.success(`Merged coverage: ${merged.lines}% lines, ${merged.functions}% functions`));
|
|
669
|
+
}
|
|
397
670
|
console.error(colors.success(`Coverage report written to ${coverageCtx.options.reportsDir}/`));
|
|
398
671
|
} catch (error) {
|
|
399
672
|
logError(`Failed to generate coverage report: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -410,7 +683,7 @@ async function runCommand$1(files, options) {
|
|
|
410
683
|
* Register the coverage command.
|
|
411
684
|
*/
|
|
412
685
|
function registerCoverageCommand(program) {
|
|
413
|
-
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)").action(coverageCommand);
|
|
686
|
+
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);
|
|
414
687
|
}
|
|
415
688
|
/**
|
|
416
689
|
* Run a command with inherited coverage environment.
|
|
@@ -452,6 +725,74 @@ function findC8Path() {
|
|
|
452
725
|
return null;
|
|
453
726
|
}
|
|
454
727
|
/**
|
|
728
|
+
* Get coverage file statistics from temp directory.
|
|
729
|
+
*/
|
|
730
|
+
async function getCoverageStats(tempDir) {
|
|
731
|
+
try {
|
|
732
|
+
const coverageFiles = (await (0, node_fs_promises.readdir)(tempDir)).filter((f) => f.startsWith("coverage-") && f.endsWith(".json"));
|
|
733
|
+
let totalBytes = 0;
|
|
734
|
+
for (const file of coverageFiles) {
|
|
735
|
+
const fileStat = await (0, node_fs_promises.stat)((0, node_path.join)(tempDir, file));
|
|
736
|
+
totalBytes += fileStat.size;
|
|
737
|
+
}
|
|
738
|
+
return {
|
|
739
|
+
fileCount: coverageFiles.length,
|
|
740
|
+
totalBytes,
|
|
741
|
+
files: coverageFiles
|
|
742
|
+
};
|
|
743
|
+
} catch {
|
|
744
|
+
return {
|
|
745
|
+
fileCount: 0,
|
|
746
|
+
totalBytes: 0,
|
|
747
|
+
files: []
|
|
748
|
+
};
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
/**
|
|
752
|
+
* Generate a text-only coverage report for debugging (doesn't write files).
|
|
753
|
+
*/
|
|
754
|
+
async function generateTextReport(tempDir, options, label) {
|
|
755
|
+
const c8Path = findC8Path();
|
|
756
|
+
if (!c8Path) return;
|
|
757
|
+
const include = options.include ?? ["dist/**"];
|
|
758
|
+
const exclude = options.exclude ?? [];
|
|
759
|
+
const tempReportsDir = await (0, node_fs_promises.mkdtemp)((0, node_path.join)((0, node_os.tmpdir)(), "tryscript-coverage-report-"));
|
|
760
|
+
const reportArgs = [
|
|
761
|
+
"report",
|
|
762
|
+
"--temp-directory",
|
|
763
|
+
tempDir,
|
|
764
|
+
"--reports-dir",
|
|
765
|
+
tempReportsDir,
|
|
766
|
+
"--src",
|
|
767
|
+
options.src ?? "src",
|
|
768
|
+
"--all",
|
|
769
|
+
...include.flatMap((pattern) => ["--include", pattern]),
|
|
770
|
+
...exclude.flatMap((pattern) => ["--exclude", pattern]),
|
|
771
|
+
...options.excludeNodeModules !== false ? ["--exclude-node-modules"] : ["--no-exclude-node-modules"],
|
|
772
|
+
...options.excludeAfterRemap ? ["--exclude-after-remap"] : [],
|
|
773
|
+
...options.monocart ? ["--experimental-monocart"] : [],
|
|
774
|
+
"--reporter",
|
|
775
|
+
"text"
|
|
776
|
+
];
|
|
777
|
+
console.error(colors.info(`\n--- Coverage after: ${label} ---`));
|
|
778
|
+
await new Promise((resolve$2) => {
|
|
779
|
+
const proc = (0, node_child_process.spawn)(c8Path, reportArgs, {
|
|
780
|
+
stdio: "inherit",
|
|
781
|
+
shell: false
|
|
782
|
+
});
|
|
783
|
+
proc.on("close", () => {
|
|
784
|
+
resolve$2();
|
|
785
|
+
});
|
|
786
|
+
proc.on("error", () => {
|
|
787
|
+
resolve$2();
|
|
788
|
+
});
|
|
789
|
+
});
|
|
790
|
+
await (0, node_fs_promises.rm)(tempReportsDir, {
|
|
791
|
+
recursive: true,
|
|
792
|
+
force: true
|
|
793
|
+
});
|
|
794
|
+
}
|
|
795
|
+
/**
|
|
455
796
|
* Generate c8 coverage report.
|
|
456
797
|
*/
|
|
457
798
|
async function generateReport(tempDir, options) {
|
|
@@ -510,10 +851,17 @@ async function coverageCommand(commands, options) {
|
|
|
510
851
|
logError("Coverage requires c8. Install with: npm install -D c8");
|
|
511
852
|
process.exit(1);
|
|
512
853
|
}
|
|
854
|
+
const parsedOptions = {
|
|
855
|
+
...options,
|
|
856
|
+
reporters: options.reporters ? typeof options.reporters === "string" ? options.reporters.split(",") : options.reporters : void 0,
|
|
857
|
+
include: options.include ? typeof options.include === "string" ? options.include.split(",") : options.include : void 0,
|
|
858
|
+
exclude: options.exclude ? typeof options.exclude === "string" ? options.exclude.split(",") : options.exclude : void 0
|
|
859
|
+
};
|
|
513
860
|
const coverageTemp = await (0, node_fs_promises.mkdtemp)((0, node_path.join)((0, node_os.tmpdir)(), "tryscript-coverage-"));
|
|
514
861
|
const coverageEnv = { NODE_V8_COVERAGE: coverageTemp };
|
|
515
862
|
console.error(colors.info(`Collecting V8 coverage to ${coverageTemp}`));
|
|
516
863
|
let hasFailures = false;
|
|
864
|
+
let previousFileCount = 0;
|
|
517
865
|
try {
|
|
518
866
|
for (let i = 0; i < commands.length; i++) {
|
|
519
867
|
const command = commands[i];
|
|
@@ -523,19 +871,40 @@ async function coverageCommand(commands, options) {
|
|
|
523
871
|
logWarn(`Command exited with code ${result.code}: ${command}`);
|
|
524
872
|
hasFailures = true;
|
|
525
873
|
}
|
|
874
|
+
const stats = await getCoverageStats(coverageTemp);
|
|
875
|
+
const newFiles = stats.fileCount - previousFileCount;
|
|
876
|
+
const bytesKB = (stats.totalBytes / 1024).toFixed(1);
|
|
877
|
+
console.error(colors.info(`\nV8 coverage: ${stats.fileCount} files (${newFiles} new), ${bytesKB} KB total`));
|
|
878
|
+
if (newFiles === 0) logWarn("No new coverage files from this command. This may indicate the command doesn't write to NODE_V8_COVERAGE.");
|
|
879
|
+
if (parsedOptions.verbose && stats.fileCount > 0) await generateTextReport(coverageTemp, parsedOptions, command);
|
|
880
|
+
previousFileCount = stats.fileCount;
|
|
526
881
|
}
|
|
527
|
-
console.error(colors.info("\n=== Generating
|
|
528
|
-
const parsedOptions = {
|
|
529
|
-
...options,
|
|
530
|
-
reporters: options.reporters ? typeof options.reporters === "string" ? options.reporters.split(",") : options.reporters : void 0,
|
|
531
|
-
include: options.include ? typeof options.include === "string" ? options.include.split(",") : options.include : void 0,
|
|
532
|
-
exclude: options.exclude ? typeof options.exclude === "string" ? options.exclude.split(",") : options.exclude : void 0
|
|
533
|
-
};
|
|
882
|
+
console.error(colors.info("\n=== Generating coverage report ==="));
|
|
534
883
|
if (!await generateReport(coverageTemp, parsedOptions)) {
|
|
535
884
|
logError("Failed to generate coverage report");
|
|
536
885
|
process.exit(1);
|
|
537
886
|
}
|
|
538
|
-
|
|
887
|
+
const reportsDir = parsedOptions.reportsDir ?? "coverage";
|
|
888
|
+
if (parsedOptions.mergeLcov) {
|
|
889
|
+
const externalLcovPath = parsedOptions.mergeLcov;
|
|
890
|
+
const generatedLcovPath = (0, node_path.join)(reportsDir, "lcov.info");
|
|
891
|
+
if (!(0, node_fs.existsSync)(externalLcovPath)) {
|
|
892
|
+
logError(`External LCOV file not found: ${externalLcovPath}`);
|
|
893
|
+
process.exit(1);
|
|
894
|
+
}
|
|
895
|
+
if (!(0, node_fs.existsSync)(generatedLcovPath)) {
|
|
896
|
+
logError(`Generated LCOV file not found: ${generatedLcovPath}`);
|
|
897
|
+
logError("Make sure \"lcov\" is included in reporters");
|
|
898
|
+
process.exit(1);
|
|
899
|
+
}
|
|
900
|
+
console.error(colors.info(`\nMerging with external coverage: ${externalLcovPath}`));
|
|
901
|
+
const mergedLcov = mergeLcov(readLcovFile(externalLcovPath), readLcovFile(generatedLcovPath));
|
|
902
|
+
writeLcovFile(generatedLcovPath, mergedLcov);
|
|
903
|
+
const summary = lcovToJsonSummary(mergedLcov);
|
|
904
|
+
writeJsonSummary((0, node_path.join)(reportsDir, "coverage-summary.json"), summary);
|
|
905
|
+
console.error(colors.success(`\nMerged coverage: ${summary.total.lines.pct}% lines, ${summary.total.functions.pct}% functions`));
|
|
906
|
+
}
|
|
907
|
+
console.error(colors.success(`\nCoverage report written to ${reportsDir}/`));
|
|
539
908
|
} finally {
|
|
540
909
|
await (0, node_fs_promises.rm)(coverageTemp, {
|
|
541
910
|
recursive: true,
|