as-test 1.0.1 → 1.0.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/CHANGELOG.md +118 -1
- package/README.md +138 -406
- package/as-test.config.schema.json +210 -17
- package/assembly/__fuzz__/array.fuzz.ts +10 -0
- package/assembly/__fuzz__/bytes.fuzz.ts +8 -0
- package/assembly/__fuzz__/math.fuzz.ts +9 -0
- package/assembly/__fuzz__/string.fuzz.ts +21 -0
- package/assembly/index.ts +141 -86
- package/assembly/src/expectation.ts +104 -19
- package/assembly/src/fuzz.ts +723 -0
- package/assembly/src/log.ts +6 -1
- package/assembly/src/suite.ts +45 -3
- package/assembly/util/json.ts +38 -4
- package/assembly/util/wipc.ts +35 -26
- package/bin/build-worker-pool.js +149 -0
- package/bin/build-worker.js +43 -0
- package/bin/commands/build-core.js +221 -28
- package/bin/commands/build.js +17 -1
- package/bin/commands/fuzz-core.js +306 -0
- package/bin/commands/fuzz.js +10 -0
- package/bin/commands/init-core.js +129 -24
- package/bin/commands/run-core.js +525 -123
- package/bin/commands/run.js +4 -1
- package/bin/commands/test.js +8 -3
- package/bin/commands/web-runner-source.js +634 -0
- package/bin/crash-store.js +64 -0
- package/bin/index.js +1526 -148
- package/bin/reporters/default.js +281 -49
- package/bin/reporters/tap.js +83 -2
- package/bin/types.js +19 -2
- package/bin/util.js +315 -33
- package/bin/wipc.js +79 -0
- package/package.json +19 -9
- package/transform/lib/coverage.js +1 -2
- package/transform/lib/index.js +3 -3
- package/transform/lib/log.js +1 -1
package/bin/index.js
CHANGED
|
@@ -1,19 +1,26 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import chalk from "chalk";
|
|
3
|
-
import { build } from "./commands/build.js";
|
|
3
|
+
import { build, formatInvocation as formatBuildInvocation, getBuildInvocationPreview, } from "./commands/build.js";
|
|
4
4
|
import { createRunReporter, run } from "./commands/run.js";
|
|
5
5
|
import { executeBuildCommand } from "./commands/build.js";
|
|
6
6
|
import { executeRunCommand } from "./commands/run.js";
|
|
7
7
|
import { executeTestCommand } from "./commands/test.js";
|
|
8
|
+
import { executeFuzzCommand } from "./commands/fuzz.js";
|
|
8
9
|
import { executeInitCommand } from "./commands/init.js";
|
|
9
10
|
import { executeDoctorCommand } from "./commands/doctor.js";
|
|
10
|
-
import {
|
|
11
|
+
import { fuzz } from "./commands/fuzz-core.js";
|
|
12
|
+
import { applyMode, formatTime, getCliVersion, loadConfig, resolveModeNames, } from "./util.js";
|
|
11
13
|
import * as path from "path";
|
|
14
|
+
import { spawnSync } from "child_process";
|
|
12
15
|
import { glob } from "glob";
|
|
16
|
+
import { createInterface } from "readline";
|
|
17
|
+
import { existsSync } from "fs";
|
|
18
|
+
import { availableParallelism, cpus } from "os";
|
|
19
|
+
import { BuildWorkerPool } from "./build-worker-pool.js";
|
|
13
20
|
const _args = process.argv.slice(2);
|
|
14
21
|
const flags = [];
|
|
15
22
|
const args = [];
|
|
16
|
-
const COMMANDS = ["run", "build", "test", "init", "doctor"];
|
|
23
|
+
const COMMANDS = ["run", "build", "test", "fuzz", "init", "doctor"];
|
|
17
24
|
const version = getCliVersion();
|
|
18
25
|
const configPath = resolveConfigPath(_args);
|
|
19
26
|
const selectedModes = resolveModeNames(_args);
|
|
@@ -25,7 +32,7 @@ for (const arg of _args) {
|
|
|
25
32
|
}
|
|
26
33
|
if (!args.length) {
|
|
27
34
|
if (flags.includes("--version") || flags.includes("-v")) {
|
|
28
|
-
console.log(
|
|
35
|
+
console.log(version.toString());
|
|
29
36
|
}
|
|
30
37
|
else {
|
|
31
38
|
info();
|
|
@@ -43,6 +50,7 @@ else if (COMMANDS.includes(args[0])) {
|
|
|
43
50
|
resolveCommandArgs,
|
|
44
51
|
resolveListFlags,
|
|
45
52
|
resolveFeatureToggles,
|
|
53
|
+
resolveBuildParallelJobs,
|
|
46
54
|
resolveExecutionModes,
|
|
47
55
|
listExecutionPlan,
|
|
48
56
|
runBuildModes,
|
|
@@ -56,6 +64,8 @@ else if (COMMANDS.includes(args[0])) {
|
|
|
56
64
|
resolveCommandArgs,
|
|
57
65
|
resolveListFlags,
|
|
58
66
|
resolveFeatureToggles,
|
|
67
|
+
resolveParallelJobs,
|
|
68
|
+
resolveBrowserOverride,
|
|
59
69
|
resolveExecutionModes,
|
|
60
70
|
listExecutionPlan,
|
|
61
71
|
runRuntimeModes,
|
|
@@ -69,6 +79,9 @@ else if (COMMANDS.includes(args[0])) {
|
|
|
69
79
|
resolveCommandArgs,
|
|
70
80
|
resolveListFlags,
|
|
71
81
|
resolveFeatureToggles,
|
|
82
|
+
resolveParallelJobs,
|
|
83
|
+
resolveBrowserOverride,
|
|
84
|
+
resolveFuzzOverrides,
|
|
72
85
|
resolveExecutionModes,
|
|
73
86
|
listExecutionPlan,
|
|
74
87
|
runTestModes,
|
|
@@ -77,6 +90,19 @@ else if (COMMANDS.includes(args[0])) {
|
|
|
77
90
|
process.exit(1);
|
|
78
91
|
});
|
|
79
92
|
}
|
|
93
|
+
else if (command === "fuzz") {
|
|
94
|
+
executeFuzzCommand(_args, configPath, selectedModes, {
|
|
95
|
+
resolveCommandArgs,
|
|
96
|
+
resolveListFlags,
|
|
97
|
+
resolveJobs,
|
|
98
|
+
resolveExecutionModes,
|
|
99
|
+
listExecutionPlan,
|
|
100
|
+
runFuzzModes,
|
|
101
|
+
}).catch((error) => {
|
|
102
|
+
printCliError(error);
|
|
103
|
+
process.exit(1);
|
|
104
|
+
});
|
|
105
|
+
}
|
|
80
106
|
else if (command === "init") {
|
|
81
107
|
executeInitCommand(_args, {
|
|
82
108
|
resolveCommandTokens,
|
|
@@ -139,6 +165,13 @@ function info() {
|
|
|
139
165
|
" " +
|
|
140
166
|
"Build and run unit tests with selected runtime" +
|
|
141
167
|
"\n");
|
|
168
|
+
console.log(" " +
|
|
169
|
+
chalk.bold.blueBright("fuzz") +
|
|
170
|
+
" " +
|
|
171
|
+
chalk.dim("<name>|<path-or-glob>") +
|
|
172
|
+
" " +
|
|
173
|
+
"Build and run fuzz targets" +
|
|
174
|
+
"\n");
|
|
142
175
|
console.log(" " +
|
|
143
176
|
chalk.bold.magentaBright("init") +
|
|
144
177
|
" " +
|
|
@@ -153,63 +186,19 @@ function info() {
|
|
|
153
186
|
"Validate environment/config/runtime setup");
|
|
154
187
|
console.log("");
|
|
155
188
|
console.log(chalk.bold("Flags:"));
|
|
156
|
-
console.log("
|
|
157
|
-
chalk.bold.blue("--
|
|
158
|
-
" " +
|
|
159
|
-
"Run one or multiple named config modes");
|
|
160
|
-
console.log(" " +
|
|
161
|
-
chalk.bold.blue("--config <path>") +
|
|
162
|
-
" " +
|
|
163
|
-
"Use a specific config file");
|
|
164
|
-
console.log(" " +
|
|
165
|
-
chalk.bold.blue("--snapshot") +
|
|
166
|
-
" " +
|
|
167
|
-
"Snapshot assertions (enabled by default)");
|
|
168
|
-
console.log(" " +
|
|
169
|
-
chalk.bold.blue("--update-snapshots") +
|
|
170
|
-
" " +
|
|
171
|
-
"Create/update snapshot files on mismatch");
|
|
172
|
-
console.log(" " +
|
|
173
|
-
chalk.bold.blue("--no-snapshot") +
|
|
174
|
-
" " +
|
|
175
|
-
"Disable snapshot assertions for this run");
|
|
176
|
-
console.log(" " +
|
|
177
|
-
chalk.bold.blue("--show-coverage") +
|
|
178
|
-
" " +
|
|
179
|
-
"Print all coverage points with line:column refs");
|
|
180
|
-
console.log(" " +
|
|
181
|
-
chalk.bold.blue("--enable <feature>") +
|
|
182
|
-
" " +
|
|
183
|
-
"Enable as-test feature (coverage|try-as)");
|
|
184
|
-
console.log(" " +
|
|
185
|
-
chalk.bold.blue("--disable <feature>") +
|
|
186
|
-
" " +
|
|
187
|
-
"Disable as-test feature (coverage|try-as)");
|
|
188
|
-
console.log(" " +
|
|
189
|
-
chalk.bold.blue("--verbose") +
|
|
190
|
-
" " +
|
|
191
|
-
"Print each suite start/end line");
|
|
192
|
-
console.log(" " +
|
|
193
|
-
chalk.bold.blue("--reporter <name|path>") +
|
|
194
|
-
" " +
|
|
195
|
-
"Use built-in reporter (default|tap) or custom module path");
|
|
196
|
-
console.log(" " +
|
|
197
|
-
chalk.bold.blue("--list") +
|
|
198
|
-
" " +
|
|
199
|
-
"Preview resolved files/modes/artifacts without running");
|
|
200
|
-
console.log(" " +
|
|
201
|
-
chalk.bold.blue("--list-modes") +
|
|
189
|
+
console.log(" " +
|
|
190
|
+
chalk.bold.blue("--version, -v") +
|
|
202
191
|
" " +
|
|
203
|
-
"
|
|
204
|
-
console.log("
|
|
192
|
+
"Print current cli version");
|
|
193
|
+
console.log(" " +
|
|
194
|
+
chalk.bold.blue("--help, -h") +
|
|
195
|
+
" Show help menu");
|
|
205
196
|
console.log("");
|
|
206
|
-
console.log(chalk.dim("If
|
|
197
|
+
console.log(chalk.dim("If this tool provides value, please consider sponsoring my open-source work! https://jairus.dev/sponsor") + "\n");
|
|
198
|
+
console.log("View the docs: " +
|
|
199
|
+
chalk.blue("https://docs.jairus.dev/as-test"));
|
|
207
200
|
console.log("View the repo: " +
|
|
208
|
-
chalk.
|
|
209
|
-
// console.log(
|
|
210
|
-
// "View the docs: " +
|
|
211
|
-
// chalk.blue("https://docs.jairus.dev/as-test"),
|
|
212
|
-
// );
|
|
201
|
+
chalk.blue("https://github.com/JairusSW/as-test"));
|
|
213
202
|
}
|
|
214
203
|
function isHelpFlag(value) {
|
|
215
204
|
return value == "--help" || value == "-h";
|
|
@@ -238,6 +227,9 @@ function printCommandHelp(command) {
|
|
|
238
227
|
process.stdout.write(" --mode <name[,name...]> Run one or multiple named config modes\n");
|
|
239
228
|
process.stdout.write(" --enable <feature> Enable build feature (coverage|try-as)\n");
|
|
240
229
|
process.stdout.write(" --disable <feature> Disable build feature (coverage|try-as)\n");
|
|
230
|
+
process.stdout.write(" --parallel Run files through an ordered worker pool using an automatic worker count\n");
|
|
231
|
+
process.stdout.write(" --jobs <n> Run files through an ordered worker pool\n");
|
|
232
|
+
process.stdout.write(" --build-jobs <n> Limit concurrent build tasks (defaults to --jobs)\n");
|
|
241
233
|
process.stdout.write(" --list Preview resolved files/artifacts without building\n");
|
|
242
234
|
process.stdout.write(" --list-modes Preview configured and selected mode names\n");
|
|
243
235
|
process.stdout.write(" --help, -h Show this help\n");
|
|
@@ -249,7 +241,13 @@ function printCommandHelp(command) {
|
|
|
249
241
|
process.stdout.write(chalk.bold("Flags:\n"));
|
|
250
242
|
process.stdout.write(" --config <path> Use a specific config file\n");
|
|
251
243
|
process.stdout.write(" --mode <name[,name...]> Run one or multiple named config modes\n");
|
|
252
|
-
process.stdout.write(" --
|
|
244
|
+
process.stdout.write(" --browser <name|path> Use chrome, chromium, firefox, webkit, or an executable path for web modes\n");
|
|
245
|
+
process.stdout.write(" --parallel Run files through an ordered worker pool using an automatic worker count\n");
|
|
246
|
+
process.stdout.write(" --jobs <n> Run files through an ordered worker pool\n");
|
|
247
|
+
process.stdout.write(" --build-jobs <n> Limit concurrent build tasks (defaults to --jobs)\n");
|
|
248
|
+
process.stdout.write(" --run-jobs <n> Limit concurrent run tasks (defaults to --jobs)\n");
|
|
249
|
+
process.stdout.write(" --create-snapshots Create missing snapshot entries\n");
|
|
250
|
+
process.stdout.write(" --overwrite-snapshots Overwrite existing snapshot entries on mismatch\n");
|
|
253
251
|
process.stdout.write(" --no-snapshot Disable snapshot assertions for this run\n");
|
|
254
252
|
process.stdout.write(" --show-coverage Print uncovered coverage point details\n");
|
|
255
253
|
process.stdout.write(" --enable <feature> Enable feature (coverage|try-as)\n");
|
|
@@ -269,11 +267,24 @@ function printCommandHelp(command) {
|
|
|
269
267
|
process.stdout.write(chalk.bold("Flags:\n"));
|
|
270
268
|
process.stdout.write(" --config <path> Use a specific config file\n");
|
|
271
269
|
process.stdout.write(" --mode <name[,name...]> Run one or multiple named config modes\n");
|
|
272
|
-
process.stdout.write(" --
|
|
270
|
+
process.stdout.write(" --browser <name|path> Use chrome, chromium, firefox, webkit, or an executable path for web modes\n");
|
|
271
|
+
process.stdout.write(" --parallel Run files through an ordered worker pool using an automatic worker count\n");
|
|
272
|
+
process.stdout.write(" --jobs <n> Run files through an ordered worker pool\n");
|
|
273
|
+
process.stdout.write(" --build-jobs <n> Limit concurrent build tasks (defaults to --jobs)\n");
|
|
274
|
+
process.stdout.write(" --run-jobs <n> Limit concurrent run tasks (defaults to --jobs)\n");
|
|
275
|
+
process.stdout.write(" --create-snapshots Create missing snapshot entries\n");
|
|
276
|
+
process.stdout.write(" --overwrite-snapshots Overwrite existing snapshot entries on mismatch\n");
|
|
273
277
|
process.stdout.write(" --no-snapshot Disable snapshot assertions for this run\n");
|
|
274
278
|
process.stdout.write(" --show-coverage Print uncovered coverage point details\n");
|
|
275
279
|
process.stdout.write(" --enable <feature> Enable feature (coverage|try-as)\n");
|
|
276
280
|
process.stdout.write(" --disable <feature> Disable feature (coverage|try-as)\n");
|
|
281
|
+
process.stdout.write(" --fuzz Run fuzz targets after the normal test pass\n");
|
|
282
|
+
process.stdout.write(" --fuzz-runs <n> Override fuzz iteration count for this run\n");
|
|
283
|
+
process.stdout.write(" --fuzz-seed <n> Override fuzz seed for this run\n");
|
|
284
|
+
process.stdout.write(" --parallel Run files through an ordered worker pool using an automatic worker count\n");
|
|
285
|
+
process.stdout.write(" --jobs <n> Run files through an ordered worker pool\n");
|
|
286
|
+
process.stdout.write(" --build-jobs <n> Limit concurrent build tasks (defaults to --jobs)\n");
|
|
287
|
+
process.stdout.write(" --run-jobs <n> Limit concurrent run tasks (defaults to --jobs)\n");
|
|
277
288
|
process.stdout.write(" --reporter <name|path> Use built-in reporter (default|tap) or custom module path\n");
|
|
278
289
|
process.stdout.write(" --tap Shortcut for --reporter tap\n");
|
|
279
290
|
process.stdout.write(" --verbose Keep expanded suite/test lines and live updates\n");
|
|
@@ -283,11 +294,27 @@ function printCommandHelp(command) {
|
|
|
283
294
|
process.stdout.write(" --help, -h Show this help\n");
|
|
284
295
|
return;
|
|
285
296
|
}
|
|
297
|
+
if (command == "fuzz") {
|
|
298
|
+
process.stdout.write(chalk.bold("Usage: ast fuzz [selectors...] [flags]\n\n"));
|
|
299
|
+
process.stdout.write("Build selected fuzz targets with bindings and execute them with generated inputs.\n\n");
|
|
300
|
+
process.stdout.write(chalk.bold("Flags:\n"));
|
|
301
|
+
process.stdout.write(" --config <path> Use a specific config file\n");
|
|
302
|
+
process.stdout.write(" --mode <name[,name...]> Run one or multiple named config modes\n");
|
|
303
|
+
process.stdout.write(" --runs <n> Override fuzz iteration count\n");
|
|
304
|
+
process.stdout.write(" --seed <n> Override fuzz seed\n");
|
|
305
|
+
process.stdout.write(" --jobs <n> Run files through an ordered worker pool\n");
|
|
306
|
+
process.stdout.write(" --build-jobs <n> Limit concurrent build tasks (defaults to --jobs)\n");
|
|
307
|
+
process.stdout.write(" --run-jobs <n> Limit concurrent run tasks (defaults to --jobs)\n");
|
|
308
|
+
process.stdout.write(" --list Preview resolved fuzz files without running\n");
|
|
309
|
+
process.stdout.write(" --list-modes Preview configured and selected mode names\n");
|
|
310
|
+
process.stdout.write(" --help, -h Show this help\n");
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
286
313
|
if (command == "init") {
|
|
287
314
|
process.stdout.write(chalk.bold("Usage: ast init [dir] [flags]\n\n"));
|
|
288
315
|
process.stdout.write("Initialize as-test config, default runners, and example specs.\n\n");
|
|
289
316
|
process.stdout.write(chalk.bold("Flags:\n"));
|
|
290
|
-
process.stdout.write(" --target <wasi|bindings>
|
|
317
|
+
process.stdout.write(" --target <wasi|bindings|web> Set build target\n");
|
|
291
318
|
process.stdout.write(" --example <minimal|full|none> Set example template\n");
|
|
292
319
|
process.stdout.write(" --install Install dependencies after scaffolding\n");
|
|
293
320
|
process.stdout.write(" --yes, -y Non-interactive setup with defaults\n");
|
|
@@ -361,6 +388,9 @@ function resolveCommandArgs(rawArgs, command) {
|
|
|
361
388
|
if (arg == "--tap") {
|
|
362
389
|
continue;
|
|
363
390
|
}
|
|
391
|
+
if (arg == "--fuzz") {
|
|
392
|
+
continue;
|
|
393
|
+
}
|
|
364
394
|
if (arg == "--enable" || arg == "--disable") {
|
|
365
395
|
i++;
|
|
366
396
|
continue;
|
|
@@ -368,6 +398,28 @@ function resolveCommandArgs(rawArgs, command) {
|
|
|
368
398
|
if (arg.startsWith("--enable=") || arg.startsWith("--disable=")) {
|
|
369
399
|
continue;
|
|
370
400
|
}
|
|
401
|
+
if (arg == "--runs" ||
|
|
402
|
+
arg == "--seed" ||
|
|
403
|
+
arg == "--parallel" ||
|
|
404
|
+
arg == "--jobs" ||
|
|
405
|
+
arg == "--build-jobs" ||
|
|
406
|
+
arg == "--run-jobs" ||
|
|
407
|
+
arg == "--browser" ||
|
|
408
|
+
arg == "--fuzz-runs" ||
|
|
409
|
+
arg == "--fuzz-seed") {
|
|
410
|
+
i++;
|
|
411
|
+
continue;
|
|
412
|
+
}
|
|
413
|
+
if (arg.startsWith("--runs=") ||
|
|
414
|
+
arg.startsWith("--seed=") ||
|
|
415
|
+
arg.startsWith("--jobs=") ||
|
|
416
|
+
arg.startsWith("--build-jobs=") ||
|
|
417
|
+
arg.startsWith("--run-jobs=") ||
|
|
418
|
+
arg.startsWith("--browser=") ||
|
|
419
|
+
arg.startsWith("--fuzz-runs=") ||
|
|
420
|
+
arg.startsWith("--fuzz-seed=")) {
|
|
421
|
+
continue;
|
|
422
|
+
}
|
|
371
423
|
if (arg.startsWith("-")) {
|
|
372
424
|
continue;
|
|
373
425
|
}
|
|
@@ -407,12 +459,51 @@ function resolveFeatureToggles(rawArgs, command) {
|
|
|
407
459
|
}
|
|
408
460
|
return out;
|
|
409
461
|
}
|
|
462
|
+
function resolveFuzzOverrides(rawArgs, command) {
|
|
463
|
+
const out = {};
|
|
464
|
+
let seenCommand = false;
|
|
465
|
+
for (let i = 0; i < rawArgs.length; i++) {
|
|
466
|
+
const arg = rawArgs[i];
|
|
467
|
+
if (!seenCommand) {
|
|
468
|
+
if (arg == command)
|
|
469
|
+
seenCommand = true;
|
|
470
|
+
continue;
|
|
471
|
+
}
|
|
472
|
+
const direct = command == "fuzz"
|
|
473
|
+
? {
|
|
474
|
+
runs: "--runs",
|
|
475
|
+
seed: "--seed",
|
|
476
|
+
}
|
|
477
|
+
: {
|
|
478
|
+
runs: "--fuzz-runs",
|
|
479
|
+
seed: "--fuzz-seed",
|
|
480
|
+
};
|
|
481
|
+
const runs = parseNumberFlag(rawArgs, i, direct.runs);
|
|
482
|
+
if (runs) {
|
|
483
|
+
out.runs = runs.number;
|
|
484
|
+
if (runs.consumeNext)
|
|
485
|
+
i++;
|
|
486
|
+
continue;
|
|
487
|
+
}
|
|
488
|
+
const seed = parseNumberFlag(rawArgs, i, direct.seed);
|
|
489
|
+
if (seed) {
|
|
490
|
+
out.seed = seed.number;
|
|
491
|
+
if (seed.consumeNext)
|
|
492
|
+
i++;
|
|
493
|
+
continue;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
return out;
|
|
497
|
+
}
|
|
410
498
|
function resolveListFlags(rawArgs, command) {
|
|
411
499
|
const out = {
|
|
412
500
|
list: false,
|
|
413
501
|
listModes: false,
|
|
414
502
|
};
|
|
415
|
-
if (command !== "build" &&
|
|
503
|
+
if (command !== "build" &&
|
|
504
|
+
command !== "run" &&
|
|
505
|
+
command !== "test" &&
|
|
506
|
+
command !== "fuzz") {
|
|
416
507
|
return out;
|
|
417
508
|
}
|
|
418
509
|
let seenCommand = false;
|
|
@@ -430,6 +521,341 @@ function resolveListFlags(rawArgs, command) {
|
|
|
430
521
|
}
|
|
431
522
|
return out;
|
|
432
523
|
}
|
|
524
|
+
function parseNumberFlag(rawArgs, index, flag) {
|
|
525
|
+
const arg = rawArgs[index];
|
|
526
|
+
if (arg == flag) {
|
|
527
|
+
const next = rawArgs[index + 1];
|
|
528
|
+
if (!next || next.startsWith("-")) {
|
|
529
|
+
throw new Error(`${flag} requires a numeric value`);
|
|
530
|
+
}
|
|
531
|
+
return {
|
|
532
|
+
key: flag,
|
|
533
|
+
number: parseIntegerFlag(flag, next),
|
|
534
|
+
consumeNext: true,
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
if (arg.startsWith(`${flag}=`)) {
|
|
538
|
+
return {
|
|
539
|
+
key: flag,
|
|
540
|
+
number: parseIntegerFlag(flag, arg.slice(flag.length + 1)),
|
|
541
|
+
consumeNext: false,
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
return null;
|
|
545
|
+
}
|
|
546
|
+
function parseStringFlag(rawArgs, index, flag) {
|
|
547
|
+
const arg = rawArgs[index];
|
|
548
|
+
if (arg == flag) {
|
|
549
|
+
const next = rawArgs[index + 1];
|
|
550
|
+
if (!next || next.startsWith("-")) {
|
|
551
|
+
throw new Error(`${flag} requires a value`);
|
|
552
|
+
}
|
|
553
|
+
return { key: flag, value: next, consumeNext: true };
|
|
554
|
+
}
|
|
555
|
+
if (arg.startsWith(`${flag}=`)) {
|
|
556
|
+
const value = arg.slice(flag.length + 1);
|
|
557
|
+
if (!value.length) {
|
|
558
|
+
throw new Error(`${flag} requires a value`);
|
|
559
|
+
}
|
|
560
|
+
return { key: flag, value, consumeNext: false };
|
|
561
|
+
}
|
|
562
|
+
return null;
|
|
563
|
+
}
|
|
564
|
+
function resolveBrowserOverride(rawArgs, command) {
|
|
565
|
+
let seenCommand = false;
|
|
566
|
+
for (let i = 0; i < rawArgs.length; i++) {
|
|
567
|
+
const arg = rawArgs[i];
|
|
568
|
+
if (!seenCommand) {
|
|
569
|
+
if (arg == command)
|
|
570
|
+
seenCommand = true;
|
|
571
|
+
continue;
|
|
572
|
+
}
|
|
573
|
+
const parsed = parseStringFlag(rawArgs, i, "--browser");
|
|
574
|
+
if (!parsed)
|
|
575
|
+
continue;
|
|
576
|
+
return parsed.value.trim() || undefined;
|
|
577
|
+
}
|
|
578
|
+
return undefined;
|
|
579
|
+
}
|
|
580
|
+
function resolveJobs(rawArgs, command) {
|
|
581
|
+
let seenCommand = false;
|
|
582
|
+
let parallel = false;
|
|
583
|
+
for (let i = 0; i < rawArgs.length; i++) {
|
|
584
|
+
const arg = rawArgs[i];
|
|
585
|
+
if (!seenCommand) {
|
|
586
|
+
if (arg == command)
|
|
587
|
+
seenCommand = true;
|
|
588
|
+
continue;
|
|
589
|
+
}
|
|
590
|
+
if (arg == "--parallel") {
|
|
591
|
+
parallel = true;
|
|
592
|
+
continue;
|
|
593
|
+
}
|
|
594
|
+
const parsed = parseNumberFlag(rawArgs, i, "--jobs");
|
|
595
|
+
if (!parsed)
|
|
596
|
+
continue;
|
|
597
|
+
if (parsed.number < 1) {
|
|
598
|
+
throw new Error("--jobs requires a positive integer");
|
|
599
|
+
}
|
|
600
|
+
return parsed.number;
|
|
601
|
+
}
|
|
602
|
+
return parallel ? 0 : 1;
|
|
603
|
+
}
|
|
604
|
+
function resolveBuildParallelJobs(rawArgs) {
|
|
605
|
+
const baseJobs = resolveJobs(rawArgs, "build");
|
|
606
|
+
let buildJobs = baseJobs;
|
|
607
|
+
let seenCommand = false;
|
|
608
|
+
for (let i = 0; i < rawArgs.length; i++) {
|
|
609
|
+
const arg = rawArgs[i];
|
|
610
|
+
if (!seenCommand) {
|
|
611
|
+
if (arg == "build")
|
|
612
|
+
seenCommand = true;
|
|
613
|
+
continue;
|
|
614
|
+
}
|
|
615
|
+
const buildParsed = parseNumberFlag(rawArgs, i, "--build-jobs");
|
|
616
|
+
if (buildParsed) {
|
|
617
|
+
if (buildParsed.number < 1) {
|
|
618
|
+
throw new Error("--build-jobs requires a positive integer");
|
|
619
|
+
}
|
|
620
|
+
buildJobs = buildParsed.number;
|
|
621
|
+
continue;
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
const jobs = Math.max(baseJobs, buildJobs);
|
|
625
|
+
return { jobs, buildJobs };
|
|
626
|
+
}
|
|
627
|
+
function resolveParallelJobs(rawArgs, command) {
|
|
628
|
+
const baseJobs = resolveJobs(rawArgs, command);
|
|
629
|
+
let buildJobs = baseJobs;
|
|
630
|
+
let runJobs = baseJobs;
|
|
631
|
+
let seenCommand = false;
|
|
632
|
+
for (let i = 0; i < rawArgs.length; i++) {
|
|
633
|
+
const arg = rawArgs[i];
|
|
634
|
+
if (!seenCommand) {
|
|
635
|
+
if (arg == command)
|
|
636
|
+
seenCommand = true;
|
|
637
|
+
continue;
|
|
638
|
+
}
|
|
639
|
+
const buildParsed = parseNumberFlag(rawArgs, i, "--build-jobs");
|
|
640
|
+
if (buildParsed) {
|
|
641
|
+
if (buildParsed.number < 1) {
|
|
642
|
+
throw new Error("--build-jobs requires a positive integer");
|
|
643
|
+
}
|
|
644
|
+
buildJobs = buildParsed.number;
|
|
645
|
+
continue;
|
|
646
|
+
}
|
|
647
|
+
const runParsed = parseNumberFlag(rawArgs, i, "--run-jobs");
|
|
648
|
+
if (runParsed) {
|
|
649
|
+
if (runParsed.number < 1) {
|
|
650
|
+
throw new Error("--run-jobs requires a positive integer");
|
|
651
|
+
}
|
|
652
|
+
runJobs = runParsed.number;
|
|
653
|
+
continue;
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
const jobs = Math.max(baseJobs, buildJobs, runJobs);
|
|
657
|
+
return { jobs, buildJobs, runJobs };
|
|
658
|
+
}
|
|
659
|
+
function resolveFuzzParallelJobs(rawArgs) {
|
|
660
|
+
const baseJobs = resolveJobs(rawArgs, "fuzz");
|
|
661
|
+
let buildJobs = baseJobs;
|
|
662
|
+
let runJobs = baseJobs;
|
|
663
|
+
let seenCommand = false;
|
|
664
|
+
for (let i = 0; i < rawArgs.length; i++) {
|
|
665
|
+
const arg = rawArgs[i];
|
|
666
|
+
if (!seenCommand) {
|
|
667
|
+
if (arg == "fuzz")
|
|
668
|
+
seenCommand = true;
|
|
669
|
+
continue;
|
|
670
|
+
}
|
|
671
|
+
const buildParsed = parseNumberFlag(rawArgs, i, "--build-jobs");
|
|
672
|
+
if (buildParsed) {
|
|
673
|
+
if (buildParsed.number < 1) {
|
|
674
|
+
throw new Error("--build-jobs requires a positive integer");
|
|
675
|
+
}
|
|
676
|
+
buildJobs = buildParsed.number;
|
|
677
|
+
continue;
|
|
678
|
+
}
|
|
679
|
+
const runParsed = parseNumberFlag(rawArgs, i, "--run-jobs");
|
|
680
|
+
if (runParsed) {
|
|
681
|
+
if (runParsed.number < 1) {
|
|
682
|
+
throw new Error("--run-jobs requires a positive integer");
|
|
683
|
+
}
|
|
684
|
+
runJobs = runParsed.number;
|
|
685
|
+
continue;
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
const jobs = Math.max(baseJobs, buildJobs, runJobs);
|
|
689
|
+
return { jobs, buildJobs, runJobs };
|
|
690
|
+
}
|
|
691
|
+
function resolveEffectiveParallelJobs(settings, totalFiles) {
|
|
692
|
+
if (settings.jobs > 0) {
|
|
693
|
+
return {
|
|
694
|
+
jobs: Math.max(settings.jobs, settings.buildJobs, settings.runJobs),
|
|
695
|
+
buildJobs: settings.buildJobs > 0 ? settings.buildJobs : settings.jobs,
|
|
696
|
+
runJobs: settings.runJobs > 0 ? settings.runJobs : settings.jobs,
|
|
697
|
+
};
|
|
698
|
+
}
|
|
699
|
+
const autoJobs = resolveAutoJobs(totalFiles);
|
|
700
|
+
return {
|
|
701
|
+
jobs: Math.max(autoJobs, settings.buildJobs, settings.runJobs),
|
|
702
|
+
buildJobs: settings.buildJobs > 0 ? settings.buildJobs : autoJobs,
|
|
703
|
+
runJobs: settings.runJobs > 0 ? settings.runJobs : autoJobs,
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
function resolveAutoJobs(totalFiles) {
|
|
707
|
+
const cpuCount = typeof availableParallelism == "function"
|
|
708
|
+
? availableParallelism()
|
|
709
|
+
: cpus().length;
|
|
710
|
+
const cpuBudget = Math.max(1, cpuCount - 1);
|
|
711
|
+
if (totalFiles <= 1)
|
|
712
|
+
return 1;
|
|
713
|
+
if (totalFiles <= 4)
|
|
714
|
+
return Math.min(2, cpuBudget, totalFiles);
|
|
715
|
+
if (totalFiles <= 12)
|
|
716
|
+
return Math.min(3, cpuBudget);
|
|
717
|
+
if (totalFiles <= 32)
|
|
718
|
+
return Math.min(4, cpuBudget);
|
|
719
|
+
return Math.min(Math.max(4, Math.ceil(totalFiles / 12)), cpuBudget);
|
|
720
|
+
}
|
|
721
|
+
function createBufferedStream() {
|
|
722
|
+
const chunks = [];
|
|
723
|
+
return {
|
|
724
|
+
isTTY: false,
|
|
725
|
+
write(chunk) {
|
|
726
|
+
chunks.push(typeof chunk == "string" ? chunk : Buffer.from(chunk).toString("utf8"));
|
|
727
|
+
return true;
|
|
728
|
+
},
|
|
729
|
+
read() {
|
|
730
|
+
return chunks.join("");
|
|
731
|
+
},
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
async function createBufferedReporter(configPath, modeName) {
|
|
735
|
+
const stream = createBufferedStream();
|
|
736
|
+
const session = await createRunReporter(configPath, undefined, modeName, {
|
|
737
|
+
stdout: stream,
|
|
738
|
+
stderr: stream,
|
|
739
|
+
});
|
|
740
|
+
return {
|
|
741
|
+
reporter: session.reporter,
|
|
742
|
+
reporterKind: session.reporterKind,
|
|
743
|
+
runtimeName: session.runtimeName,
|
|
744
|
+
output: () => stream.read(),
|
|
745
|
+
};
|
|
746
|
+
}
|
|
747
|
+
async function runOrderedPool(items, jobs, worker) {
|
|
748
|
+
const width = Math.max(1, jobs);
|
|
749
|
+
let nextIndex = 0;
|
|
750
|
+
let firstError = null;
|
|
751
|
+
async function runWorker() {
|
|
752
|
+
while (true) {
|
|
753
|
+
if (firstError != null)
|
|
754
|
+
return;
|
|
755
|
+
const index = nextIndex++;
|
|
756
|
+
if (index >= items.length)
|
|
757
|
+
return;
|
|
758
|
+
try {
|
|
759
|
+
await worker(items[index], index);
|
|
760
|
+
}
|
|
761
|
+
catch (error) {
|
|
762
|
+
if (firstError == null)
|
|
763
|
+
firstError = error;
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
await Promise.all(Array.from({ length: Math.min(width, items.length) }, () => runWorker()));
|
|
769
|
+
if (firstError != null)
|
|
770
|
+
throw firstError;
|
|
771
|
+
}
|
|
772
|
+
function createAsyncLimiter(limit) {
|
|
773
|
+
const width = Math.max(1, limit);
|
|
774
|
+
let active = 0;
|
|
775
|
+
const queue = [];
|
|
776
|
+
return async function withLimit(task) {
|
|
777
|
+
if (active >= width) {
|
|
778
|
+
await new Promise((resolve) => queue.push(resolve));
|
|
779
|
+
}
|
|
780
|
+
active++;
|
|
781
|
+
try {
|
|
782
|
+
return await task();
|
|
783
|
+
}
|
|
784
|
+
finally {
|
|
785
|
+
active--;
|
|
786
|
+
const next = queue.shift();
|
|
787
|
+
if (next)
|
|
788
|
+
next();
|
|
789
|
+
}
|
|
790
|
+
};
|
|
791
|
+
}
|
|
792
|
+
function canRewriteParallelQueue() {
|
|
793
|
+
return Boolean(process.stdout.isTTY);
|
|
794
|
+
}
|
|
795
|
+
class ParallelQueueDisplay {
|
|
796
|
+
constructor(showStartLines) {
|
|
797
|
+
this.showStartLines = showStartLines;
|
|
798
|
+
this.active = new Map();
|
|
799
|
+
this.renderedLines = 0;
|
|
800
|
+
this.enabled = showStartLines && canRewriteParallelQueue();
|
|
801
|
+
}
|
|
802
|
+
start(file) {
|
|
803
|
+
const token = Symbol(file);
|
|
804
|
+
if (!this.showStartLines)
|
|
805
|
+
return token;
|
|
806
|
+
const line = `${chalk.bgBlackBright.white(" .... ")} ${file}`;
|
|
807
|
+
if (!this.enabled)
|
|
808
|
+
return token;
|
|
809
|
+
this.clear();
|
|
810
|
+
this.active.set(token, line);
|
|
811
|
+
this.render();
|
|
812
|
+
return token;
|
|
813
|
+
}
|
|
814
|
+
complete(token, output) {
|
|
815
|
+
if (!this.showStartLines || !this.enabled) {
|
|
816
|
+
process.stdout.write(output);
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
819
|
+
this.clear();
|
|
820
|
+
process.stdout.write(output);
|
|
821
|
+
this.active.delete(token);
|
|
822
|
+
this.render();
|
|
823
|
+
}
|
|
824
|
+
flush() {
|
|
825
|
+
if (!this.enabled)
|
|
826
|
+
return;
|
|
827
|
+
this.clear();
|
|
828
|
+
}
|
|
829
|
+
clear() {
|
|
830
|
+
if (!this.renderedLines)
|
|
831
|
+
return;
|
|
832
|
+
for (let i = 0; i < this.renderedLines; i++) {
|
|
833
|
+
process.stdout.write("\r\x1b[2K");
|
|
834
|
+
if (i < this.renderedLines - 1)
|
|
835
|
+
process.stdout.write("\x1b[1A");
|
|
836
|
+
}
|
|
837
|
+
this.renderedLines = 0;
|
|
838
|
+
}
|
|
839
|
+
render() {
|
|
840
|
+
if (!this.enabled)
|
|
841
|
+
return;
|
|
842
|
+
const lines = Array.from(this.active.values());
|
|
843
|
+
if (!lines.length)
|
|
844
|
+
return;
|
|
845
|
+
process.stdout.write(lines.join("\n"));
|
|
846
|
+
this.renderedLines = lines.length;
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
function renderQueuedFileStart(display, file) {
|
|
850
|
+
return display.start(file);
|
|
851
|
+
}
|
|
852
|
+
function parseIntegerFlag(flag, value) {
|
|
853
|
+
const parsed = Number(value);
|
|
854
|
+
if (!Number.isFinite(parsed) || parsed < 0) {
|
|
855
|
+
throw new Error(`${flag} requires a non-negative integer`);
|
|
856
|
+
}
|
|
857
|
+
return Math.floor(parsed);
|
|
858
|
+
}
|
|
433
859
|
function applyFeatureToggle(out, rawFeature, enabled) {
|
|
434
860
|
const key = rawFeature.trim().toLowerCase();
|
|
435
861
|
if (key == "coverage") {
|
|
@@ -456,26 +882,32 @@ function resolveCommandTokens(rawArgs, command) {
|
|
|
456
882
|
}
|
|
457
883
|
return values;
|
|
458
884
|
}
|
|
459
|
-
async function runTestSequential(runFlags, configPath, selectors, buildFeatureToggles, modeSummaryTotal, fileSummaryTotal, modeName) {
|
|
885
|
+
async function runTestSequential(runFlags, configPath, selectors, buildFeatureToggles, modeSummaryTotal, fileSummaryTotal, allowNoSpecFiles = false, modeName, reporterOverride, emitRunComplete = true) {
|
|
460
886
|
const files = await resolveSelectedFiles(configPath, selectors);
|
|
461
887
|
if (!files.length) {
|
|
462
|
-
|
|
888
|
+
if (!allowNoSpecFiles) {
|
|
889
|
+
throw await buildNoTestFilesMatchedError(configPath, selectors);
|
|
890
|
+
}
|
|
463
891
|
}
|
|
464
892
|
const reporterSession = await createRunReporter(configPath, undefined, modeName);
|
|
465
|
-
const reporter = reporterSession.reporter;
|
|
893
|
+
const reporter = reporterOverride ?? reporterSession.reporter;
|
|
466
894
|
const snapshotEnabled = runFlags.snapshot !== false;
|
|
467
895
|
reporter.onRunStart?.({
|
|
468
896
|
runtimeName: reporterSession.runtimeName,
|
|
469
897
|
clean: runFlags.clean,
|
|
470
898
|
verbose: runFlags.verbose,
|
|
471
899
|
snapshotEnabled,
|
|
472
|
-
|
|
900
|
+
createSnapshots: runFlags.createSnapshots,
|
|
473
901
|
});
|
|
474
902
|
const results = [];
|
|
475
903
|
let failed = false;
|
|
904
|
+
const buildIntervals = [];
|
|
476
905
|
const duplicateSpecBasenames = resolveDuplicateSpecBasenames(files);
|
|
477
906
|
for (const file of files) {
|
|
907
|
+
const buildStartedAt = Date.now();
|
|
478
908
|
await build(configPath, [file], modeName, buildFeatureToggles);
|
|
909
|
+
buildIntervals.push({ start: buildStartedAt, end: Date.now() });
|
|
910
|
+
const buildInvocation = await getBuildInvocationPreview(configPath, file, modeName, buildFeatureToggles);
|
|
479
911
|
const artifactKey = resolvePerFileArtifactKey(file, duplicateSpecBasenames);
|
|
480
912
|
const result = await run(runFlags, configPath, [file], false, {
|
|
481
913
|
reporter,
|
|
@@ -483,6 +915,7 @@ async function runTestSequential(runFlags, configPath, selectors, buildFeatureTo
|
|
|
483
915
|
emitRunComplete: false,
|
|
484
916
|
logFileName: `test.${artifactKey}.log.json`,
|
|
485
917
|
coverageFileName: `coverage.${artifactKey}.log.json`,
|
|
918
|
+
buildCommand: formatBuildInvocation(buildInvocation),
|
|
486
919
|
modeName,
|
|
487
920
|
});
|
|
488
921
|
results.push(result);
|
|
@@ -491,38 +924,109 @@ async function runTestSequential(runFlags, configPath, selectors, buildFeatureTo
|
|
|
491
924
|
}
|
|
492
925
|
const summary = aggregateRunResults(results);
|
|
493
926
|
summary.stats = applyConfiguredFileTotalToStats(summary.stats, fileSummaryTotal);
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
927
|
+
if (emitRunComplete) {
|
|
928
|
+
reporter.onRunComplete?.({
|
|
929
|
+
clean: runFlags.clean,
|
|
930
|
+
snapshotEnabled,
|
|
931
|
+
showCoverage: runFlags.showCoverage,
|
|
932
|
+
buildTime: getMergedIntervalDuration(buildIntervals),
|
|
933
|
+
snapshotSummary: summary.snapshotSummary,
|
|
934
|
+
coverageSummary: summary.coverageSummary,
|
|
935
|
+
stats: summary.stats,
|
|
936
|
+
reports: summary.reports,
|
|
937
|
+
modeSummary: buildSingleModeSummary(summary.stats, summary.snapshotSummary, modeSummaryTotal),
|
|
938
|
+
});
|
|
939
|
+
reporter.flush?.();
|
|
940
|
+
}
|
|
941
|
+
return {
|
|
942
|
+
failed,
|
|
943
|
+
summary: {
|
|
944
|
+
buildTime: getMergedIntervalDuration(buildIntervals),
|
|
945
|
+
snapshotSummary: summary.snapshotSummary,
|
|
946
|
+
coverageSummary: summary.coverageSummary,
|
|
947
|
+
stats: summary.stats,
|
|
948
|
+
reports: summary.reports,
|
|
949
|
+
},
|
|
950
|
+
};
|
|
505
951
|
}
|
|
506
|
-
async function runBuildModes(configPath, selectors, modes, buildFeatureToggles) {
|
|
952
|
+
async function runBuildModes(configPath, selectors, modes, buildFeatureToggles, parallel) {
|
|
953
|
+
const files = await resolveSelectedFiles(configPath, selectors);
|
|
954
|
+
if (!files.length) {
|
|
955
|
+
throw await buildNoTestFilesMatchedError(configPath, selectors);
|
|
956
|
+
}
|
|
957
|
+
const effective = resolveEffectiveParallelJobs({
|
|
958
|
+
jobs: parallel.jobs,
|
|
959
|
+
buildJobs: parallel.buildJobs,
|
|
960
|
+
runJobs: parallel.buildJobs,
|
|
961
|
+
}, files.length);
|
|
962
|
+
const resolvedConfigPath = configPath ?? path.join(process.cwd(), "./as-test.config.json");
|
|
963
|
+
const loadedConfig = loadConfig(resolvedConfigPath, true);
|
|
964
|
+
const allStartedAt = Date.now();
|
|
965
|
+
let builtCount = 0;
|
|
507
966
|
for (const modeName of modes) {
|
|
508
|
-
|
|
967
|
+
const startedAt = Date.now();
|
|
968
|
+
if (effective.buildJobs > 1) {
|
|
969
|
+
const pool = new BuildWorkerPool(effective.buildJobs);
|
|
970
|
+
try {
|
|
971
|
+
await runOrderedPool(files, effective.buildJobs, async (file) => {
|
|
972
|
+
await pool.buildFileMode({
|
|
973
|
+
configPath,
|
|
974
|
+
file,
|
|
975
|
+
modeName,
|
|
976
|
+
featureToggles: buildFeatureToggles,
|
|
977
|
+
});
|
|
978
|
+
});
|
|
979
|
+
}
|
|
980
|
+
finally {
|
|
981
|
+
await pool.close();
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
else {
|
|
985
|
+
await build(configPath, selectors, modeName, buildFeatureToggles);
|
|
986
|
+
}
|
|
987
|
+
builtCount += files.length;
|
|
988
|
+
const active = applyMode(loadedConfig, modeName).config;
|
|
989
|
+
process.stdout.write(`${chalk.bgGreenBright.black(" BUILT ")} ${modeName ?? "default"} ${chalk.dim(`(${active.buildOptions.target})`)} ${files.length} file(s) -> ${active.outDir} ${chalk.dim(formatTime(Date.now() - startedAt))}\n`);
|
|
509
990
|
}
|
|
991
|
+
process.stdout.write(`${chalk.bold("Summary:")} built ${builtCount} file(s) across ${modes.length || 1} mode(s) in ${formatTime(Date.now() - allStartedAt)}\n`);
|
|
510
992
|
}
|
|
511
993
|
async function runRuntimeModes(runFlags, configPath, selectors, modes) {
|
|
512
|
-
|
|
994
|
+
await ensureWebBrowsersReady(configPath, modes, runFlags.browser);
|
|
995
|
+
const modeSummaryTotal = Math.max(modes.length, 1);
|
|
513
996
|
const fileSummaryTotal = await resolveConfiguredFileTotal(configPath);
|
|
997
|
+
const effectiveRunFlags = {
|
|
998
|
+
...runFlags,
|
|
999
|
+
...resolveEffectiveParallelJobs(runFlags, fileSummaryTotal),
|
|
1000
|
+
};
|
|
1001
|
+
if (effectiveRunFlags.jobs > 1) {
|
|
1002
|
+
if (modes.length > 1) {
|
|
1003
|
+
const failed = await runRuntimeMatrixParallel(effectiveRunFlags, configPath, selectors, modes, modeSummaryTotal, fileSummaryTotal);
|
|
1004
|
+
process.exit(failed ? 1 : 0);
|
|
1005
|
+
return;
|
|
1006
|
+
}
|
|
1007
|
+
let failed = false;
|
|
1008
|
+
for (const modeName of modes) {
|
|
1009
|
+
const result = await runRuntimeSingleParallel(effectiveRunFlags, configPath, selectors, modeName, modeSummaryTotal, fileSummaryTotal);
|
|
1010
|
+
if (result)
|
|
1011
|
+
failed = true;
|
|
1012
|
+
}
|
|
1013
|
+
process.exit(failed ? 1 : 0);
|
|
1014
|
+
return;
|
|
1015
|
+
}
|
|
514
1016
|
if (modes.length > 1) {
|
|
515
|
-
const failed = await runRuntimeMatrix(
|
|
1017
|
+
const failed = await runRuntimeMatrix(effectiveRunFlags, configPath, selectors, modes, modeSummaryTotal, fileSummaryTotal);
|
|
516
1018
|
process.exit(failed ? 1 : 0);
|
|
517
1019
|
return;
|
|
518
1020
|
}
|
|
519
1021
|
let failed = false;
|
|
1022
|
+
const buildCommandsByFile = await previewBuildCommands(configPath, selectors, modes[0], {});
|
|
520
1023
|
for (const modeName of modes) {
|
|
521
|
-
const result = await run(
|
|
1024
|
+
const result = await run(effectiveRunFlags, configPath, selectors, false, {
|
|
522
1025
|
modeName,
|
|
523
1026
|
modeSummaryTotal,
|
|
524
1027
|
modeSummaryExecuted: 1,
|
|
525
1028
|
fileSummaryTotal,
|
|
1029
|
+
buildCommandsByFile,
|
|
526
1030
|
});
|
|
527
1031
|
if (result.failed)
|
|
528
1032
|
failed = true;
|
|
@@ -542,7 +1046,7 @@ async function runRuntimeMatrix(runFlags, configPath, selectors, modes, modeSumm
|
|
|
542
1046
|
clean: runFlags.clean,
|
|
543
1047
|
verbose: runFlags.verbose,
|
|
544
1048
|
snapshotEnabled,
|
|
545
|
-
|
|
1049
|
+
createSnapshots: runFlags.createSnapshots,
|
|
546
1050
|
});
|
|
547
1051
|
const silentReporter = {};
|
|
548
1052
|
const allResults = [];
|
|
@@ -558,6 +1062,7 @@ async function runRuntimeMatrix(runFlags, configPath, selectors, modes, modeSumm
|
|
|
558
1062
|
passed: false,
|
|
559
1063
|
}));
|
|
560
1064
|
const duplicateSpecBasenames = resolveDuplicateSpecBasenames(files);
|
|
1065
|
+
const buildIntervals = [];
|
|
561
1066
|
for (let fileIndex = 0; fileIndex < files.length; fileIndex++) {
|
|
562
1067
|
const file = files[fileIndex];
|
|
563
1068
|
const fileName = path.basename(file);
|
|
@@ -569,6 +1074,7 @@ async function runRuntimeMatrix(runFlags, configPath, selectors, modes, modeSumm
|
|
|
569
1074
|
for (let i = 0; i < modes.length; i++) {
|
|
570
1075
|
const modeName = modes[i];
|
|
571
1076
|
try {
|
|
1077
|
+
const buildInvocation = await getBuildInvocationPreview(configPath, file, modeName, {});
|
|
572
1078
|
const artifactKey = resolvePerFileArtifactKey(file, duplicateSpecBasenames);
|
|
573
1079
|
const result = await run(runFlags, configPath, [file], false, {
|
|
574
1080
|
reporter: silentReporter,
|
|
@@ -577,6 +1083,7 @@ async function runRuntimeMatrix(runFlags, configPath, selectors, modes, modeSumm
|
|
|
577
1083
|
emitRunComplete: false,
|
|
578
1084
|
logFileName: `run.${artifactKey}.log.json`,
|
|
579
1085
|
coverageFileName: `coverage.${artifactKey}.log.json`,
|
|
1086
|
+
buildCommand: formatBuildInvocation(buildInvocation),
|
|
580
1087
|
modeName,
|
|
581
1088
|
});
|
|
582
1089
|
modeTimes[i] = formatMatrixModeTime(result.stats.time);
|
|
@@ -612,6 +1119,7 @@ async function runRuntimeMatrix(runFlags, configPath, selectors, modes, modeSumm
|
|
|
612
1119
|
clean: runFlags.clean,
|
|
613
1120
|
snapshotEnabled,
|
|
614
1121
|
showCoverage: runFlags.showCoverage,
|
|
1122
|
+
buildTime: 0,
|
|
615
1123
|
snapshotSummary: summary.snapshotSummary,
|
|
616
1124
|
coverageSummary: summary.coverageSummary,
|
|
617
1125
|
stats: summary.stats,
|
|
@@ -620,26 +1128,75 @@ async function runRuntimeMatrix(runFlags, configPath, selectors, modes, modeSumm
|
|
|
620
1128
|
});
|
|
621
1129
|
return allResults.some((result) => result.failed);
|
|
622
1130
|
}
|
|
623
|
-
async function runTestModes(runFlags, configPath, selectors, modes, buildFeatureToggles) {
|
|
624
|
-
|
|
625
|
-
const
|
|
1131
|
+
async function runTestModes(runFlags, configPath, selectors, modes, buildFeatureToggles, fuzzEnabled, fuzzOverrides) {
|
|
1132
|
+
await ensureWebBrowsersReady(configPath, modes, runFlags.browser);
|
|
1133
|
+
const modeSummaryTotal = Math.max(modes.length, 1);
|
|
1134
|
+
const fileSummaryTotal = await resolveConfiguredFileTotal(configPath, selectors);
|
|
1135
|
+
const effectiveRunFlags = {
|
|
1136
|
+
...runFlags,
|
|
1137
|
+
...resolveEffectiveParallelJobs(runFlags, fileSummaryTotal),
|
|
1138
|
+
};
|
|
1139
|
+
if (effectiveRunFlags.jobs > 1) {
|
|
1140
|
+
if (modes.length > 1) {
|
|
1141
|
+
const failed = await runTestMatrixParallel(effectiveRunFlags, configPath, selectors, modes, buildFeatureToggles, modeSummaryTotal, fileSummaryTotal, fuzzEnabled, fuzzOverrides);
|
|
1142
|
+
process.exit(failed ? 1 : 0);
|
|
1143
|
+
return;
|
|
1144
|
+
}
|
|
1145
|
+
let failed = false;
|
|
1146
|
+
for (const modeName of modes) {
|
|
1147
|
+
const modeFailed = await runTestSingleParallel(effectiveRunFlags, configPath, selectors, buildFeatureToggles, modeSummaryTotal, fileSummaryTotal, fuzzEnabled, fuzzOverrides, modeName);
|
|
1148
|
+
if (modeFailed)
|
|
1149
|
+
failed = true;
|
|
1150
|
+
}
|
|
1151
|
+
process.exit(failed ? 1 : 0);
|
|
1152
|
+
return;
|
|
1153
|
+
}
|
|
626
1154
|
if (modes.length > 1) {
|
|
627
|
-
const failed = await runTestMatrix(
|
|
1155
|
+
const failed = await runTestMatrix(effectiveRunFlags, configPath, selectors, modes, buildFeatureToggles, modeSummaryTotal, fileSummaryTotal, fuzzEnabled, fuzzOverrides);
|
|
628
1156
|
process.exit(failed ? 1 : 0);
|
|
629
1157
|
return;
|
|
630
1158
|
}
|
|
631
1159
|
let failed = false;
|
|
632
1160
|
for (const modeName of modes) {
|
|
633
|
-
const
|
|
634
|
-
|
|
1161
|
+
const reporterSession = await createRunReporter(configPath, undefined, modeName);
|
|
1162
|
+
const modeResult = await runTestSequential(effectiveRunFlags, configPath, selectors, buildFeatureToggles, modeSummaryTotal, fileSummaryTotal, fuzzEnabled, modeName, reporterSession.reporter, !fuzzEnabled);
|
|
1163
|
+
if (modeResult.failed)
|
|
635
1164
|
failed = true;
|
|
1165
|
+
if (fuzzEnabled) {
|
|
1166
|
+
if (reporterSession.reporterKind == "default") {
|
|
1167
|
+
process.stdout.write("\n");
|
|
1168
|
+
}
|
|
1169
|
+
const fuzzResults = await runFuzzMatrixResults(configPath, selectors, [modeName], fuzzOverrides, reporterSession.reporter);
|
|
1170
|
+
if (fuzzResults.some(hasFuzzFailures))
|
|
1171
|
+
failed = true;
|
|
1172
|
+
reporterSession.reporter.onRunComplete?.({
|
|
1173
|
+
clean: runFlags.clean,
|
|
1174
|
+
snapshotEnabled: effectiveRunFlags.snapshot !== false,
|
|
1175
|
+
showCoverage: effectiveRunFlags.showCoverage,
|
|
1176
|
+
buildTime: modeResult.summary.buildTime +
|
|
1177
|
+
getMergedIntervalDuration(collectFuzzBuildIntervals(fuzzResults)),
|
|
1178
|
+
snapshotSummary: modeResult.summary.snapshotSummary,
|
|
1179
|
+
coverageSummary: modeResult.summary.coverageSummary,
|
|
1180
|
+
stats: modeResult.summary.stats,
|
|
1181
|
+
reports: modeResult.summary.reports,
|
|
1182
|
+
fuzzSummary: summarizeFuzzExecutions(fuzzResults),
|
|
1183
|
+
modeSummary: buildSingleModeSummary(modeResult.summary.stats, modeResult.summary.snapshotSummary, modeSummaryTotal),
|
|
1184
|
+
});
|
|
1185
|
+
reporterSession.reporter.flush?.();
|
|
1186
|
+
}
|
|
636
1187
|
}
|
|
637
1188
|
process.exit(failed ? 1 : 0);
|
|
638
1189
|
}
|
|
639
|
-
async function runTestMatrix(runFlags, configPath, selectors, modes, buildFeatureToggles, modeSummaryTotal, fileSummaryTotal) {
|
|
1190
|
+
async function runTestMatrix(runFlags, configPath, selectors, modes, buildFeatureToggles, modeSummaryTotal, fileSummaryTotal, fuzzEnabled, fuzzOverrides) {
|
|
640
1191
|
const files = await resolveSelectedFiles(configPath, selectors);
|
|
641
1192
|
if (!files.length) {
|
|
642
|
-
|
|
1193
|
+
if (!fuzzEnabled) {
|
|
1194
|
+
throw await buildNoTestFilesMatchedError(configPath, selectors);
|
|
1195
|
+
}
|
|
1196
|
+
const fuzzFiles = await resolveSelectedFuzzFiles(configPath, selectors);
|
|
1197
|
+
if (!fuzzFiles.length) {
|
|
1198
|
+
throw await buildNoTestFilesMatchedError(configPath, selectors, true);
|
|
1199
|
+
}
|
|
643
1200
|
}
|
|
644
1201
|
const reporterSession = await createRunReporter(configPath);
|
|
645
1202
|
const reporter = reporterSession.reporter;
|
|
@@ -649,7 +1206,7 @@ async function runTestMatrix(runFlags, configPath, selectors, modes, buildFeatur
|
|
|
649
1206
|
clean: runFlags.clean,
|
|
650
1207
|
verbose: runFlags.verbose,
|
|
651
1208
|
snapshotEnabled,
|
|
652
|
-
|
|
1209
|
+
createSnapshots: runFlags.createSnapshots,
|
|
653
1210
|
});
|
|
654
1211
|
const silentReporter = {};
|
|
655
1212
|
const allResults = [];
|
|
@@ -665,6 +1222,7 @@ async function runTestMatrix(runFlags, configPath, selectors, modes, buildFeatur
|
|
|
665
1222
|
passed: false,
|
|
666
1223
|
}));
|
|
667
1224
|
const duplicateSpecBasenames = resolveDuplicateSpecBasenames(files);
|
|
1225
|
+
const buildIntervals = [];
|
|
668
1226
|
for (let fileIndex = 0; fileIndex < files.length; fileIndex++) {
|
|
669
1227
|
const file = files[fileIndex];
|
|
670
1228
|
const fileName = path.basename(file);
|
|
@@ -676,7 +1234,10 @@ async function runTestMatrix(runFlags, configPath, selectors, modes, buildFeatur
|
|
|
676
1234
|
for (let i = 0; i < modes.length; i++) {
|
|
677
1235
|
const modeName = modes[i];
|
|
678
1236
|
try {
|
|
1237
|
+
const buildStartedAt = Date.now();
|
|
679
1238
|
await build(configPath, [file], modeName, buildFeatureToggles);
|
|
1239
|
+
buildIntervals.push({ start: buildStartedAt, end: Date.now() });
|
|
1240
|
+
const buildInvocation = await getBuildInvocationPreview(configPath, file, modeName, buildFeatureToggles);
|
|
680
1241
|
const artifactKey = resolvePerFileArtifactKey(file, duplicateSpecBasenames);
|
|
681
1242
|
const result = await run(runFlags, configPath, [file], false, {
|
|
682
1243
|
reporter: silentReporter,
|
|
@@ -685,6 +1246,7 @@ async function runTestMatrix(runFlags, configPath, selectors, modes, buildFeatur
|
|
|
685
1246
|
emitRunComplete: false,
|
|
686
1247
|
logFileName: `test.${artifactKey}.log.json`,
|
|
687
1248
|
coverageFileName: `coverage.${artifactKey}.log.json`,
|
|
1249
|
+
buildCommand: formatBuildInvocation(buildInvocation),
|
|
688
1250
|
modeName,
|
|
689
1251
|
});
|
|
690
1252
|
modeTimes[i] = formatMatrixModeTime(result.stats.time);
|
|
@@ -716,53 +1278,585 @@ async function runTestMatrix(runFlags, configPath, selectors, modes, buildFeatur
|
|
|
716
1278
|
}
|
|
717
1279
|
const summary = aggregateRunResults(allResults);
|
|
718
1280
|
summary.stats = applyMatrixFileSummaryToStats(summary.stats, fileState, fileSummaryTotal);
|
|
1281
|
+
let failed = allResults.some((result) => result.failed);
|
|
1282
|
+
let fuzzSummary;
|
|
1283
|
+
if (fuzzEnabled) {
|
|
1284
|
+
if (reporterSession.reporterKind == "default") {
|
|
1285
|
+
process.stdout.write("\n");
|
|
1286
|
+
}
|
|
1287
|
+
const fuzzResults = await runFuzzMatrixResults(configPath, selectors, modes, fuzzOverrides, reporter);
|
|
1288
|
+
if (fuzzResults.some(hasFuzzFailures))
|
|
1289
|
+
failed = true;
|
|
1290
|
+
fuzzSummary = summarizeFuzzExecutions(fuzzResults);
|
|
1291
|
+
buildIntervals.push(...collectFuzzBuildIntervals(fuzzResults));
|
|
1292
|
+
}
|
|
719
1293
|
reporter.onRunComplete?.({
|
|
720
1294
|
clean: runFlags.clean,
|
|
721
1295
|
snapshotEnabled,
|
|
722
1296
|
showCoverage: runFlags.showCoverage,
|
|
1297
|
+
buildTime: getMergedIntervalDuration(buildIntervals),
|
|
723
1298
|
snapshotSummary: summary.snapshotSummary,
|
|
724
1299
|
coverageSummary: summary.coverageSummary,
|
|
725
1300
|
stats: summary.stats,
|
|
726
1301
|
reports: summary.reports,
|
|
1302
|
+
fuzzSummary,
|
|
727
1303
|
modeSummary: buildModeSummary(modeState, modeSummaryTotal),
|
|
728
1304
|
});
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
function renderMatrixFileResult(file, modes, results, modeTimes, liveMatrix, showPerModeTimes) {
|
|
732
|
-
const verdict = resolveMatrixVerdict(results);
|
|
733
|
-
const badge = verdict == "fail"
|
|
734
|
-
? chalk.bgRed.white(" FAIL ")
|
|
735
|
-
: verdict == "ok"
|
|
736
|
-
? chalk.bgGreenBright.black(" PASS ")
|
|
737
|
-
: chalk.bgBlackBright.white(" SKIP ");
|
|
738
|
-
const avg = formatMatrixAverageTime(results);
|
|
739
|
-
const timingText = showPerModeTimes ? modeTimes.join(",") : avg;
|
|
740
|
-
const suffix = showPerModeTimes
|
|
741
|
-
? ` ${chalk.dim(`(${modes.join(",")})`)}`
|
|
742
|
-
: "";
|
|
743
|
-
const line = `${badge} ${file} ${chalk.dim(timingText)}${suffix}`;
|
|
744
|
-
if (liveMatrix)
|
|
745
|
-
clearLiveLine();
|
|
746
|
-
process.stdout.write(line + "\n");
|
|
747
|
-
}
|
|
748
|
-
function resolveMatrixVerdict(results) {
|
|
749
|
-
if (results.some((result) => result.failed))
|
|
750
|
-
return "fail";
|
|
751
|
-
const hasPass = results.some((result) => result.stats.passedFiles > 0);
|
|
752
|
-
if (hasPass)
|
|
753
|
-
return "ok";
|
|
754
|
-
return "skip";
|
|
755
|
-
}
|
|
756
|
-
function canRewriteStdout() {
|
|
757
|
-
return Boolean(process.stdout.isTTY);
|
|
1305
|
+
reporter.flush?.();
|
|
1306
|
+
return failed;
|
|
758
1307
|
}
|
|
759
|
-
function
|
|
760
|
-
|
|
1308
|
+
async function runFuzzModes(configPath, selectors, modes, rawArgs) {
|
|
1309
|
+
const overrides = resolveFuzzOverrides(rawArgs, "fuzz");
|
|
1310
|
+
const parallelSettings = resolveFuzzParallelJobs(rawArgs);
|
|
1311
|
+
const clean = rawArgs.includes("--clean");
|
|
1312
|
+
const fuzzFiles = await resolveSelectedFuzzFiles(configPath, selectors);
|
|
1313
|
+
const { jobs, buildJobs, runJobs } = resolveEffectiveParallelJobs(parallelSettings, fuzzFiles.length);
|
|
1314
|
+
if (jobs > 1) {
|
|
1315
|
+
const results = await runFuzzMatrixResultsParallel(configPath, selectors, modes, overrides, jobs, buildJobs, runJobs, clean);
|
|
1316
|
+
const reporterSession = await createRunReporter(configPath);
|
|
1317
|
+
reporterSession.reporter.onFuzzComplete?.(buildFuzzCompleteEvent(results, modes));
|
|
1318
|
+
reporterSession.reporter.flush?.();
|
|
1319
|
+
process.exit(results.some(hasFuzzFailures) ? 1 : 0);
|
|
761
1320
|
return;
|
|
762
|
-
|
|
1321
|
+
}
|
|
1322
|
+
const reporterSession = await createRunReporter(configPath);
|
|
1323
|
+
const results = await runFuzzMatrixResults(configPath, selectors, modes, overrides, reporterSession.reporter);
|
|
1324
|
+
reporterSession.reporter.onFuzzComplete?.(buildFuzzCompleteEvent(results, modes));
|
|
1325
|
+
reporterSession.reporter.flush?.();
|
|
1326
|
+
process.exit(results.some(hasFuzzFailures) ? 1 : 0);
|
|
763
1327
|
}
|
|
764
|
-
function
|
|
765
|
-
|
|
1328
|
+
async function runRuntimeSingleParallel(runFlags, configPath, selectors, modeName, modeSummaryTotal, fileSummaryTotal) {
|
|
1329
|
+
const files = await resolveSelectedFiles(configPath, selectors);
|
|
1330
|
+
if (!files.length) {
|
|
1331
|
+
throw await buildNoTestFilesMatchedError(configPath, selectors);
|
|
1332
|
+
}
|
|
1333
|
+
const reporterSession = await createRunReporter(configPath, undefined, modeName);
|
|
1334
|
+
const reporter = reporterSession.reporter;
|
|
1335
|
+
const snapshotEnabled = runFlags.snapshot !== false;
|
|
1336
|
+
reporter.onRunStart?.({
|
|
1337
|
+
runtimeName: reporterSession.runtimeName,
|
|
1338
|
+
clean: runFlags.clean,
|
|
1339
|
+
verbose: runFlags.verbose,
|
|
1340
|
+
snapshotEnabled,
|
|
1341
|
+
createSnapshots: runFlags.createSnapshots,
|
|
1342
|
+
});
|
|
1343
|
+
const buildCommandsByFile = await previewBuildCommands(configPath, selectors, modeName, {});
|
|
1344
|
+
const results = new Array(files.length);
|
|
1345
|
+
const queueDisplay = new ParallelQueueDisplay(!runFlags.clean);
|
|
1346
|
+
const runLimit = createAsyncLimiter(runFlags.runJobs);
|
|
1347
|
+
const poolWidth = Math.max(runFlags.buildJobs, runFlags.runJobs);
|
|
1348
|
+
await runOrderedPool(files, poolWidth, async (file, index) => {
|
|
1349
|
+
const token = renderQueuedFileStart(queueDisplay, path.basename(file));
|
|
1350
|
+
const buffered = await createBufferedReporter(configPath, modeName);
|
|
1351
|
+
const result = await runLimit(() => run({ ...runFlags, clean: true }, configPath, [file], false, {
|
|
1352
|
+
reporter: buffered.reporter,
|
|
1353
|
+
reporterKind: buffered.reporterKind,
|
|
1354
|
+
modeName,
|
|
1355
|
+
emitRunComplete: false,
|
|
1356
|
+
fileSummaryTotal: 1,
|
|
1357
|
+
modeSummaryTotal,
|
|
1358
|
+
modeSummaryExecuted: 1,
|
|
1359
|
+
buildCommandsByFile: { [file]: buildCommandsByFile[file] ?? "" },
|
|
1360
|
+
}));
|
|
1361
|
+
buffered.reporter.flush?.();
|
|
1362
|
+
results[index] = result;
|
|
1363
|
+
queueDisplay.complete(token, buffered.output());
|
|
1364
|
+
});
|
|
1365
|
+
queueDisplay.flush();
|
|
1366
|
+
const summary = aggregateRunResults(results);
|
|
1367
|
+
summary.stats = applyConfiguredFileTotalToStats(summary.stats, fileSummaryTotal);
|
|
1368
|
+
reporter.onRunComplete?.({
|
|
1369
|
+
clean: runFlags.clean,
|
|
1370
|
+
snapshotEnabled,
|
|
1371
|
+
showCoverage: runFlags.showCoverage,
|
|
1372
|
+
buildTime: 0,
|
|
1373
|
+
snapshotSummary: summary.snapshotSummary,
|
|
1374
|
+
coverageSummary: summary.coverageSummary,
|
|
1375
|
+
stats: summary.stats,
|
|
1376
|
+
reports: summary.reports,
|
|
1377
|
+
modeSummary: buildSingleModeSummary(summary.stats, summary.snapshotSummary, modeSummaryTotal),
|
|
1378
|
+
});
|
|
1379
|
+
reporter.flush?.();
|
|
1380
|
+
return results.some((result) => result.failed);
|
|
1381
|
+
}
|
|
1382
|
+
async function runRuntimeMatrixParallel(runFlags, configPath, selectors, modes, modeSummaryTotal, fileSummaryTotal) {
|
|
1383
|
+
const files = await resolveSelectedFiles(configPath, selectors);
|
|
1384
|
+
if (!files.length) {
|
|
1385
|
+
throw await buildNoTestFilesMatchedError(configPath, selectors);
|
|
1386
|
+
}
|
|
1387
|
+
const reporterSession = await createRunReporter(configPath);
|
|
1388
|
+
const reporter = reporterSession.reporter;
|
|
1389
|
+
const snapshotEnabled = runFlags.snapshot !== false;
|
|
1390
|
+
reporter.onRunStart?.({
|
|
1391
|
+
runtimeName: reporterSession.runtimeName,
|
|
1392
|
+
clean: runFlags.clean,
|
|
1393
|
+
verbose: runFlags.verbose,
|
|
1394
|
+
snapshotEnabled,
|
|
1395
|
+
createSnapshots: runFlags.createSnapshots,
|
|
1396
|
+
});
|
|
1397
|
+
const silentReporter = {};
|
|
1398
|
+
const modeLabels = modes.map((modeName) => modeName ?? "default");
|
|
1399
|
+
const showPerModeTimes = Boolean(runFlags.verbose);
|
|
1400
|
+
const duplicateSpecBasenames = resolveDuplicateSpecBasenames(files);
|
|
1401
|
+
const ordered = new Array(files.length);
|
|
1402
|
+
const queueDisplay = new ParallelQueueDisplay(!runFlags.clean);
|
|
1403
|
+
const poolWidth = Math.max(runFlags.jobs, runFlags.buildJobs, runFlags.runJobs);
|
|
1404
|
+
const buildPool = new BuildWorkerPool(runFlags.buildJobs);
|
|
1405
|
+
const buildIntervals = [];
|
|
1406
|
+
try {
|
|
1407
|
+
await runOrderedPool(files, poolWidth, async (file, fileIndex) => {
|
|
1408
|
+
const fileName = path.basename(file);
|
|
1409
|
+
const token = renderQueuedFileStart(queueDisplay, fileName);
|
|
1410
|
+
const fileResults = [];
|
|
1411
|
+
const modeTimes = modes.map(() => "...");
|
|
1412
|
+
for (let i = 0; i < modes.length; i++) {
|
|
1413
|
+
const modeName = modes[i];
|
|
1414
|
+
const buildStartedAt = Date.now();
|
|
1415
|
+
await buildPool.buildFileMode({
|
|
1416
|
+
configPath,
|
|
1417
|
+
file,
|
|
1418
|
+
modeName,
|
|
1419
|
+
});
|
|
1420
|
+
buildIntervals.push({ start: buildStartedAt, end: Date.now() });
|
|
1421
|
+
const buildInvocation = await getBuildInvocationPreview(configPath, file, modeName, {});
|
|
1422
|
+
const artifactKey = resolvePerFileArtifactKey(file, duplicateSpecBasenames);
|
|
1423
|
+
const result = await run(runFlags, configPath, [file], false, {
|
|
1424
|
+
reporter: silentReporter,
|
|
1425
|
+
reporterKind: "default",
|
|
1426
|
+
emitRunStart: false,
|
|
1427
|
+
emitRunComplete: false,
|
|
1428
|
+
logFileName: `run.${artifactKey}.log.json`,
|
|
1429
|
+
coverageFileName: `coverage.${artifactKey}.log.json`,
|
|
1430
|
+
buildCommand: formatBuildInvocation(buildInvocation),
|
|
1431
|
+
modeName,
|
|
1432
|
+
});
|
|
1433
|
+
modeTimes[i] = formatMatrixModeTime(result.stats.time);
|
|
1434
|
+
fileResults.push(result);
|
|
1435
|
+
}
|
|
1436
|
+
ordered[fileIndex] = { fileName, fileResults, modeTimes };
|
|
1437
|
+
queueDisplay.complete(token, formatMatrixFileResultLine(fileName, modeLabels, fileResults, modeTimes, showPerModeTimes) + "\n");
|
|
1438
|
+
});
|
|
1439
|
+
}
|
|
1440
|
+
finally {
|
|
1441
|
+
await buildPool.close();
|
|
1442
|
+
}
|
|
1443
|
+
queueDisplay.flush();
|
|
1444
|
+
const allResults = [];
|
|
1445
|
+
const modeState = modes.map(() => ({ failed: false, passed: false }));
|
|
1446
|
+
const fileState = files.map(() => ({ failed: false, passed: false }));
|
|
1447
|
+
for (let fileIndex = 0; fileIndex < ordered.length; fileIndex++) {
|
|
1448
|
+
const fileResults = ordered[fileIndex].fileResults;
|
|
1449
|
+
for (let i = 0; i < fileResults.length; i++) {
|
|
1450
|
+
const result = fileResults[i];
|
|
1451
|
+
allResults.push(result);
|
|
1452
|
+
if (result.failed)
|
|
1453
|
+
modeState[i].failed = true;
|
|
1454
|
+
else if (result.stats.passedFiles > 0)
|
|
1455
|
+
modeState[i].passed = true;
|
|
1456
|
+
}
|
|
1457
|
+
const verdict = resolveMatrixVerdict(fileResults);
|
|
1458
|
+
if (verdict == "fail")
|
|
1459
|
+
fileState[fileIndex].failed = true;
|
|
1460
|
+
else if (verdict == "ok")
|
|
1461
|
+
fileState[fileIndex].passed = true;
|
|
1462
|
+
}
|
|
1463
|
+
const summary = aggregateRunResults(allResults);
|
|
1464
|
+
summary.stats = applyMatrixFileSummaryToStats(summary.stats, fileState, fileSummaryTotal);
|
|
1465
|
+
reporter.onRunComplete?.({
|
|
1466
|
+
clean: runFlags.clean,
|
|
1467
|
+
snapshotEnabled,
|
|
1468
|
+
showCoverage: runFlags.showCoverage,
|
|
1469
|
+
buildTime: getMergedIntervalDuration(buildIntervals),
|
|
1470
|
+
snapshotSummary: summary.snapshotSummary,
|
|
1471
|
+
coverageSummary: summary.coverageSummary,
|
|
1472
|
+
stats: summary.stats,
|
|
1473
|
+
reports: summary.reports,
|
|
1474
|
+
modeSummary: buildModeSummary(modeState, modeSummaryTotal),
|
|
1475
|
+
});
|
|
1476
|
+
reporter.flush?.();
|
|
1477
|
+
return allResults.some((result) => result.failed);
|
|
1478
|
+
}
|
|
1479
|
+
async function runTestSingleParallel(runFlags, configPath, selectors, buildFeatureToggles, modeSummaryTotal, fileSummaryTotal, fuzzEnabled, fuzzOverrides, modeName) {
|
|
1480
|
+
const files = await resolveSelectedFiles(configPath, selectors);
|
|
1481
|
+
if (!files.length && !fuzzEnabled) {
|
|
1482
|
+
throw await buildNoTestFilesMatchedError(configPath, selectors);
|
|
1483
|
+
}
|
|
1484
|
+
const reporterSession = await createRunReporter(configPath, undefined, modeName);
|
|
1485
|
+
const reporter = reporterSession.reporter;
|
|
1486
|
+
const snapshotEnabled = runFlags.snapshot !== false;
|
|
1487
|
+
reporter.onRunStart?.({
|
|
1488
|
+
runtimeName: reporterSession.runtimeName,
|
|
1489
|
+
clean: runFlags.clean,
|
|
1490
|
+
verbose: runFlags.verbose,
|
|
1491
|
+
snapshotEnabled,
|
|
1492
|
+
createSnapshots: runFlags.createSnapshots,
|
|
1493
|
+
});
|
|
1494
|
+
const duplicateSpecBasenames = resolveDuplicateSpecBasenames(files);
|
|
1495
|
+
const results = new Array(files.length);
|
|
1496
|
+
const queueDisplay = new ParallelQueueDisplay(!runFlags.clean);
|
|
1497
|
+
const poolWidth = Math.max(runFlags.jobs, runFlags.buildJobs, runFlags.runJobs);
|
|
1498
|
+
const buildIntervals = [];
|
|
1499
|
+
if (files.length) {
|
|
1500
|
+
const buildPool = new BuildWorkerPool(runFlags.buildJobs);
|
|
1501
|
+
try {
|
|
1502
|
+
await runOrderedPool(files, poolWidth, async (file, index) => {
|
|
1503
|
+
const token = renderQueuedFileStart(queueDisplay, path.basename(file));
|
|
1504
|
+
const buildStartedAt = Date.now();
|
|
1505
|
+
await buildPool.buildFileMode({
|
|
1506
|
+
configPath,
|
|
1507
|
+
file,
|
|
1508
|
+
modeName,
|
|
1509
|
+
featureToggles: buildFeatureToggles,
|
|
1510
|
+
});
|
|
1511
|
+
buildIntervals.push({ start: buildStartedAt, end: Date.now() });
|
|
1512
|
+
const buildInvocation = await getBuildInvocationPreview(configPath, file, modeName, buildFeatureToggles);
|
|
1513
|
+
const artifactKey = resolvePerFileArtifactKey(file, duplicateSpecBasenames);
|
|
1514
|
+
const buffered = await createBufferedReporter(configPath, modeName);
|
|
1515
|
+
const result = await run({ ...runFlags, clean: true }, configPath, [file], false, {
|
|
1516
|
+
reporter: buffered.reporter,
|
|
1517
|
+
reporterKind: buffered.reporterKind,
|
|
1518
|
+
emitRunComplete: false,
|
|
1519
|
+
logFileName: `test.${artifactKey}.log.json`,
|
|
1520
|
+
coverageFileName: `coverage.${artifactKey}.log.json`,
|
|
1521
|
+
buildCommand: formatBuildInvocation(buildInvocation),
|
|
1522
|
+
modeName,
|
|
1523
|
+
});
|
|
1524
|
+
buffered.reporter.flush?.();
|
|
1525
|
+
results[index] = result;
|
|
1526
|
+
queueDisplay.complete(token, buffered.output());
|
|
1527
|
+
});
|
|
1528
|
+
}
|
|
1529
|
+
finally {
|
|
1530
|
+
await buildPool.close();
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
queueDisplay.flush();
|
|
1534
|
+
const runResults = results.filter(Boolean);
|
|
1535
|
+
const summary = aggregateRunResults(runResults);
|
|
1536
|
+
summary.stats = applyConfiguredFileTotalToStats(summary.stats, fileSummaryTotal);
|
|
1537
|
+
let failed = runResults.some((result) => result.failed);
|
|
1538
|
+
let fuzzSummary;
|
|
1539
|
+
if (fuzzEnabled) {
|
|
1540
|
+
if (reporterSession.reporterKind == "default") {
|
|
1541
|
+
process.stdout.write("\n");
|
|
1542
|
+
}
|
|
1543
|
+
const fuzzResults = await runFuzzMatrixResultsParallel(configPath, selectors, [modeName], fuzzOverrides, runFlags.jobs, runFlags.buildJobs, runFlags.runJobs, runFlags.clean);
|
|
1544
|
+
if (fuzzResults.some(hasFuzzFailures))
|
|
1545
|
+
failed = true;
|
|
1546
|
+
fuzzSummary = summarizeFuzzExecutions(fuzzResults);
|
|
1547
|
+
buildIntervals.push(...collectFuzzBuildIntervals(fuzzResults));
|
|
1548
|
+
}
|
|
1549
|
+
reporter.onRunComplete?.({
|
|
1550
|
+
clean: runFlags.clean,
|
|
1551
|
+
snapshotEnabled,
|
|
1552
|
+
showCoverage: runFlags.showCoverage,
|
|
1553
|
+
buildTime: getMergedIntervalDuration(buildIntervals),
|
|
1554
|
+
snapshotSummary: summary.snapshotSummary,
|
|
1555
|
+
coverageSummary: summary.coverageSummary,
|
|
1556
|
+
stats: summary.stats,
|
|
1557
|
+
reports: summary.reports,
|
|
1558
|
+
fuzzSummary,
|
|
1559
|
+
modeSummary: buildSingleModeSummary(summary.stats, summary.snapshotSummary, modeSummaryTotal),
|
|
1560
|
+
});
|
|
1561
|
+
reporter.flush?.();
|
|
1562
|
+
return failed;
|
|
1563
|
+
}
|
|
1564
|
+
async function runTestMatrixParallel(runFlags, configPath, selectors, modes, buildFeatureToggles, modeSummaryTotal, fileSummaryTotal, fuzzEnabled, fuzzOverrides) {
|
|
1565
|
+
const files = await resolveSelectedFiles(configPath, selectors);
|
|
1566
|
+
if (!files.length) {
|
|
1567
|
+
if (!fuzzEnabled) {
|
|
1568
|
+
throw await buildNoTestFilesMatchedError(configPath, selectors);
|
|
1569
|
+
}
|
|
1570
|
+
const fuzzFiles = await resolveSelectedFuzzFiles(configPath, selectors);
|
|
1571
|
+
if (!fuzzFiles.length) {
|
|
1572
|
+
throw await buildNoTestFilesMatchedError(configPath, selectors, true);
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
const reporterSession = await createRunReporter(configPath);
|
|
1576
|
+
const reporter = reporterSession.reporter;
|
|
1577
|
+
const snapshotEnabled = runFlags.snapshot !== false;
|
|
1578
|
+
reporter.onRunStart?.({
|
|
1579
|
+
runtimeName: reporterSession.runtimeName,
|
|
1580
|
+
clean: runFlags.clean,
|
|
1581
|
+
verbose: runFlags.verbose,
|
|
1582
|
+
snapshotEnabled,
|
|
1583
|
+
createSnapshots: runFlags.createSnapshots,
|
|
1584
|
+
});
|
|
1585
|
+
const silentReporter = {};
|
|
1586
|
+
const modeLabels = modes.map((modeName) => modeName ?? "default");
|
|
1587
|
+
const showPerModeTimes = Boolean(runFlags.verbose);
|
|
1588
|
+
const duplicateSpecBasenames = resolveDuplicateSpecBasenames(files);
|
|
1589
|
+
const ordered = new Array(files.length);
|
|
1590
|
+
const queueDisplay = new ParallelQueueDisplay(!runFlags.clean);
|
|
1591
|
+
const poolWidth = Math.max(runFlags.jobs, runFlags.buildJobs, runFlags.runJobs);
|
|
1592
|
+
const buildPool = new BuildWorkerPool(runFlags.buildJobs);
|
|
1593
|
+
const buildIntervals = [];
|
|
1594
|
+
try {
|
|
1595
|
+
await runOrderedPool(files, poolWidth, async (file, fileIndex) => {
|
|
1596
|
+
const fileName = path.basename(file);
|
|
1597
|
+
const token = renderQueuedFileStart(queueDisplay, fileName);
|
|
1598
|
+
const fileResults = [];
|
|
1599
|
+
const modeTimes = modes.map(() => "...");
|
|
1600
|
+
for (let i = 0; i < modes.length; i++) {
|
|
1601
|
+
const modeName = modes[i];
|
|
1602
|
+
const buildStartedAt = Date.now();
|
|
1603
|
+
await buildPool.buildFileMode({
|
|
1604
|
+
configPath,
|
|
1605
|
+
file,
|
|
1606
|
+
modeName,
|
|
1607
|
+
featureToggles: buildFeatureToggles,
|
|
1608
|
+
});
|
|
1609
|
+
buildIntervals.push({ start: buildStartedAt, end: Date.now() });
|
|
1610
|
+
const buildInvocation = await getBuildInvocationPreview(configPath, file, modeName, buildFeatureToggles);
|
|
1611
|
+
const artifactKey = resolvePerFileArtifactKey(file, duplicateSpecBasenames);
|
|
1612
|
+
const result = await run(runFlags, configPath, [file], false, {
|
|
1613
|
+
reporter: silentReporter,
|
|
1614
|
+
reporterKind: "default",
|
|
1615
|
+
emitRunStart: false,
|
|
1616
|
+
emitRunComplete: false,
|
|
1617
|
+
logFileName: `test.${artifactKey}.log.json`,
|
|
1618
|
+
coverageFileName: `coverage.${artifactKey}.log.json`,
|
|
1619
|
+
buildCommand: formatBuildInvocation(buildInvocation),
|
|
1620
|
+
modeName,
|
|
1621
|
+
});
|
|
1622
|
+
modeTimes[i] = formatMatrixModeTime(result.stats.time);
|
|
1623
|
+
fileResults.push(result);
|
|
1624
|
+
}
|
|
1625
|
+
ordered[fileIndex] = { fileName, fileResults, modeTimes };
|
|
1626
|
+
queueDisplay.complete(token, formatMatrixFileResultLine(fileName, modeLabels, fileResults, modeTimes, showPerModeTimes) + "\n");
|
|
1627
|
+
});
|
|
1628
|
+
}
|
|
1629
|
+
finally {
|
|
1630
|
+
await buildPool.close();
|
|
1631
|
+
}
|
|
1632
|
+
queueDisplay.flush();
|
|
1633
|
+
const allResults = [];
|
|
1634
|
+
const modeState = modes.map(() => ({ failed: false, passed: false }));
|
|
1635
|
+
const fileState = files.map(() => ({ failed: false, passed: false }));
|
|
1636
|
+
for (let fileIndex = 0; fileIndex < ordered.length; fileIndex++) {
|
|
1637
|
+
const entry = ordered[fileIndex];
|
|
1638
|
+
for (let i = 0; i < entry.fileResults.length; i++) {
|
|
1639
|
+
const result = entry.fileResults[i];
|
|
1640
|
+
allResults.push(result);
|
|
1641
|
+
if (result.failed)
|
|
1642
|
+
modeState[i].failed = true;
|
|
1643
|
+
else if (result.stats.passedFiles > 0)
|
|
1644
|
+
modeState[i].passed = true;
|
|
1645
|
+
}
|
|
1646
|
+
const verdict = resolveMatrixVerdict(entry.fileResults);
|
|
1647
|
+
if (verdict == "fail")
|
|
1648
|
+
fileState[fileIndex].failed = true;
|
|
1649
|
+
else if (verdict == "ok")
|
|
1650
|
+
fileState[fileIndex].passed = true;
|
|
1651
|
+
}
|
|
1652
|
+
const summary = aggregateRunResults(allResults);
|
|
1653
|
+
summary.stats = applyMatrixFileSummaryToStats(summary.stats, fileState, fileSummaryTotal);
|
|
1654
|
+
let failed = allResults.some((result) => result.failed);
|
|
1655
|
+
let fuzzSummary;
|
|
1656
|
+
if (fuzzEnabled) {
|
|
1657
|
+
if (reporterSession.reporterKind == "default") {
|
|
1658
|
+
process.stdout.write("\n");
|
|
1659
|
+
}
|
|
1660
|
+
const fuzzResults = await runFuzzMatrixResultsParallel(configPath, selectors, modes, fuzzOverrides, runFlags.jobs, runFlags.buildJobs, runFlags.runJobs, runFlags.clean);
|
|
1661
|
+
if (fuzzResults.some(hasFuzzFailures))
|
|
1662
|
+
failed = true;
|
|
1663
|
+
fuzzSummary = summarizeFuzzExecutions(fuzzResults);
|
|
1664
|
+
buildIntervals.push(...collectFuzzBuildIntervals(fuzzResults));
|
|
1665
|
+
}
|
|
1666
|
+
reporter.onRunComplete?.({
|
|
1667
|
+
clean: runFlags.clean,
|
|
1668
|
+
snapshotEnabled,
|
|
1669
|
+
showCoverage: runFlags.showCoverage,
|
|
1670
|
+
buildTime: getMergedIntervalDuration(buildIntervals),
|
|
1671
|
+
snapshotSummary: summary.snapshotSummary,
|
|
1672
|
+
coverageSummary: summary.coverageSummary,
|
|
1673
|
+
stats: summary.stats,
|
|
1674
|
+
reports: summary.reports,
|
|
1675
|
+
fuzzSummary,
|
|
1676
|
+
modeSummary: buildModeSummary(modeState, modeSummaryTotal),
|
|
1677
|
+
});
|
|
1678
|
+
reporter.flush?.();
|
|
1679
|
+
return failed;
|
|
1680
|
+
}
|
|
1681
|
+
async function runFuzzMatrixResultsParallel(configPath, selectors, modes, overrides, jobs, buildJobs, runJobs, clean) {
|
|
1682
|
+
const files = await resolveSelectedFuzzFiles(configPath, selectors);
|
|
1683
|
+
if (!files.length) {
|
|
1684
|
+
throw new Error(`No fuzz files matched: ${selectors.length ? selectors.join(", ") : "configured input patterns"}`);
|
|
1685
|
+
}
|
|
1686
|
+
const ordered = new Array(files.length);
|
|
1687
|
+
const queueDisplay = new ParallelQueueDisplay(!clean);
|
|
1688
|
+
const poolWidth = Math.max(jobs, buildJobs, runJobs);
|
|
1689
|
+
await runOrderedPool(files, poolWidth, async (file, index) => {
|
|
1690
|
+
const token = renderQueuedFileStart(queueDisplay, path.basename(file));
|
|
1691
|
+
const fileResults = [];
|
|
1692
|
+
for (const modeName of modes) {
|
|
1693
|
+
const modeResults = await fuzz(configPath, [file], modeName, overrides);
|
|
1694
|
+
fileResults.push(...modeResults);
|
|
1695
|
+
}
|
|
1696
|
+
ordered[index] = fileResults;
|
|
1697
|
+
const buffered = await createBufferedReporter(configPath);
|
|
1698
|
+
buffered.reporter.onFuzzFileComplete?.({ file, results: fileResults });
|
|
1699
|
+
buffered.reporter.flush?.();
|
|
1700
|
+
queueDisplay.complete(token, buffered.output());
|
|
1701
|
+
});
|
|
1702
|
+
queueDisplay.flush();
|
|
1703
|
+
return ordered.flat();
|
|
1704
|
+
}
|
|
1705
|
+
async function runFuzzMatrixResults(configPath, selectors, modes, overrides, reporter) {
|
|
1706
|
+
const files = await resolveSelectedFuzzFiles(configPath, selectors);
|
|
1707
|
+
if (!files.length) {
|
|
1708
|
+
throw new Error(`No fuzz files matched: ${selectors.length ? selectors.join(", ") : "configured input patterns"}`);
|
|
1709
|
+
}
|
|
1710
|
+
const results = [];
|
|
1711
|
+
for (const file of files) {
|
|
1712
|
+
const fileResults = [];
|
|
1713
|
+
for (const modeName of modes) {
|
|
1714
|
+
const modeResults = await fuzz(configPath, [file], modeName, overrides);
|
|
1715
|
+
fileResults.push(...modeResults);
|
|
1716
|
+
results.push(...modeResults);
|
|
1717
|
+
}
|
|
1718
|
+
reporter?.onFuzzFileComplete?.({ file, results: fileResults });
|
|
1719
|
+
}
|
|
1720
|
+
return results;
|
|
1721
|
+
}
|
|
1722
|
+
function hasFuzzFailures(result) {
|
|
1723
|
+
if (result.crashes > 0)
|
|
1724
|
+
return true;
|
|
1725
|
+
return result.fuzzers.some((fuzzer) => fuzzer.failed > 0);
|
|
1726
|
+
}
|
|
1727
|
+
function buildFuzzCompleteEvent(results, modes) {
|
|
1728
|
+
return {
|
|
1729
|
+
results,
|
|
1730
|
+
time: results.reduce((sum, item) => sum + item.time, 0),
|
|
1731
|
+
buildTime: getMergedIntervalDuration(collectFuzzBuildIntervals(results)),
|
|
1732
|
+
fuzzingSummary: summarizeFuzzExecutions(results),
|
|
1733
|
+
suiteSummary: summarizeFuzzSuites(results),
|
|
1734
|
+
modeSummary: summarizeFuzzModes(results, modes),
|
|
1735
|
+
};
|
|
1736
|
+
}
|
|
1737
|
+
function collectFuzzBuildIntervals(results) {
|
|
1738
|
+
return results.map((result) => ({
|
|
1739
|
+
start: result.buildStartedAt,
|
|
1740
|
+
end: result.buildFinishedAt,
|
|
1741
|
+
}));
|
|
1742
|
+
}
|
|
1743
|
+
function getMergedIntervalDuration(intervals) {
|
|
1744
|
+
if (!intervals.length)
|
|
1745
|
+
return 0;
|
|
1746
|
+
const sorted = intervals
|
|
1747
|
+
.map((interval) => ({
|
|
1748
|
+
start: Math.min(interval.start, interval.end),
|
|
1749
|
+
end: Math.max(interval.start, interval.end),
|
|
1750
|
+
}))
|
|
1751
|
+
.sort((a, b) => a.start - b.start);
|
|
1752
|
+
let total = 0;
|
|
1753
|
+
let currentStart = sorted[0].start;
|
|
1754
|
+
let currentEnd = sorted[0].end;
|
|
1755
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
1756
|
+
const interval = sorted[i];
|
|
1757
|
+
if (interval.start <= currentEnd) {
|
|
1758
|
+
currentEnd = Math.max(currentEnd, interval.end);
|
|
1759
|
+
continue;
|
|
1760
|
+
}
|
|
1761
|
+
total += currentEnd - currentStart;
|
|
1762
|
+
currentStart = interval.start;
|
|
1763
|
+
currentEnd = interval.end;
|
|
1764
|
+
}
|
|
1765
|
+
total += currentEnd - currentStart;
|
|
1766
|
+
return total;
|
|
1767
|
+
}
|
|
1768
|
+
function summarizeFuzzExecutions(results) {
|
|
1769
|
+
return {
|
|
1770
|
+
failed: results.reduce((sum, item) => sum +
|
|
1771
|
+
item.fuzzers.reduce((inner, fuzzer) => inner + fuzzer.failed + fuzzer.crashed, 0), 0),
|
|
1772
|
+
skipped: results.reduce((sum, item) => sum + item.fuzzers.reduce((inner, fuzzer) => inner + fuzzer.skipped, 0), 0),
|
|
1773
|
+
total: results.reduce((sum, item) => sum + item.fuzzers.reduce((inner, fuzzer) => inner + fuzzer.runs, 0), 0),
|
|
1774
|
+
};
|
|
1775
|
+
}
|
|
1776
|
+
function summarizeFuzzSuites(results) {
|
|
1777
|
+
return {
|
|
1778
|
+
failed: results.reduce((sum, item) => sum +
|
|
1779
|
+
item.fuzzers.filter((fuzzer) => fuzzer.failed > 0 || fuzzer.crashed > 0)
|
|
1780
|
+
.length, 0),
|
|
1781
|
+
skipped: results.reduce((sum, item) => sum + item.fuzzers.filter((fuzzer) => fuzzer.skipped > 0).length, 0),
|
|
1782
|
+
total: results.reduce((sum, item) => sum + item.fuzzers.length, 0),
|
|
1783
|
+
};
|
|
1784
|
+
}
|
|
1785
|
+
function summarizeFuzzModes(results, modes) {
|
|
1786
|
+
const total = Math.max(modes.length, 1);
|
|
1787
|
+
const state = new Map();
|
|
1788
|
+
for (const modeName of modes) {
|
|
1789
|
+
state.set(modeName ?? "default", { failed: false, passed: false });
|
|
1790
|
+
}
|
|
1791
|
+
for (const result of results) {
|
|
1792
|
+
const current = state.get(result.modeName) ?? {
|
|
1793
|
+
failed: false,
|
|
1794
|
+
passed: false,
|
|
1795
|
+
};
|
|
1796
|
+
if (hasFuzzFailures(result))
|
|
1797
|
+
current.failed = true;
|
|
1798
|
+
else if (!isSkippedFuzzResult(result))
|
|
1799
|
+
current.passed = true;
|
|
1800
|
+
state.set(result.modeName, current);
|
|
1801
|
+
}
|
|
1802
|
+
let failed = 0;
|
|
1803
|
+
let skipped = 0;
|
|
1804
|
+
for (const mode of state.values()) {
|
|
1805
|
+
if (mode.failed)
|
|
1806
|
+
failed++;
|
|
1807
|
+
else if (!mode.passed)
|
|
1808
|
+
skipped++;
|
|
1809
|
+
}
|
|
1810
|
+
return { failed, skipped, total };
|
|
1811
|
+
}
|
|
1812
|
+
function isSkippedFuzzResult(result) {
|
|
1813
|
+
return (result.crashes == 0 &&
|
|
1814
|
+
result.fuzzers.length > 0 &&
|
|
1815
|
+
result.fuzzers.every((fuzzer) => fuzzer.skipped > 0));
|
|
1816
|
+
}
|
|
1817
|
+
function renderMatrixFileResult(file, modes, results, modeTimes, liveMatrix, showPerModeTimes) {
|
|
1818
|
+
const line = formatMatrixFileResultLine(file, modes, results, modeTimes, showPerModeTimes);
|
|
1819
|
+
if (liveMatrix)
|
|
1820
|
+
clearLiveLine();
|
|
1821
|
+
process.stdout.write(line + "\n");
|
|
1822
|
+
}
|
|
1823
|
+
function formatMatrixFileResultLine(file, modes, results, modeTimes, showPerModeTimes) {
|
|
1824
|
+
const verdict = resolveMatrixVerdict(results);
|
|
1825
|
+
const badge = verdict == "fail"
|
|
1826
|
+
? chalk.bgRed.white(" FAIL ")
|
|
1827
|
+
: verdict == "ok"
|
|
1828
|
+
? chalk.bgGreenBright.black(" PASS ")
|
|
1829
|
+
: chalk.bgBlackBright.white(" SKIP ");
|
|
1830
|
+
const avg = formatMatrixAverageTime(results);
|
|
1831
|
+
const timingText = showPerModeTimes ? modeTimes.join(",") : avg;
|
|
1832
|
+
const failedModes = results
|
|
1833
|
+
.map((result, index) => (result.failed ? modes[index] : null))
|
|
1834
|
+
.filter((mode) => Boolean(mode));
|
|
1835
|
+
const suffix = showPerModeTimes
|
|
1836
|
+
? ` ${chalk.dim(`(${modes.join(",")})`)}`
|
|
1837
|
+
: failedModes.length
|
|
1838
|
+
? ` ${chalk.dim(`(failed: ${failedModes.join(", ")})`)}`
|
|
1839
|
+
: "";
|
|
1840
|
+
return `${badge} ${file} ${chalk.dim(timingText)}${suffix}`;
|
|
1841
|
+
}
|
|
1842
|
+
function resolveMatrixVerdict(results) {
|
|
1843
|
+
if (results.some((result) => result.failed))
|
|
1844
|
+
return "fail";
|
|
1845
|
+
const hasPass = results.some((result) => result.stats.passedFiles > 0);
|
|
1846
|
+
if (hasPass)
|
|
1847
|
+
return "ok";
|
|
1848
|
+
return "skip";
|
|
1849
|
+
}
|
|
1850
|
+
function canRewriteStdout() {
|
|
1851
|
+
return Boolean(process.stdout.isTTY);
|
|
1852
|
+
}
|
|
1853
|
+
function clearLiveLine() {
|
|
1854
|
+
if (!canRewriteStdout())
|
|
1855
|
+
return;
|
|
1856
|
+
process.stdout.write("\r\x1b[2K");
|
|
1857
|
+
}
|
|
1858
|
+
function renderMatrixLiveLine(file, modes, modeTimes, showPerModeTimes) {
|
|
1859
|
+
if (!canRewriteStdout())
|
|
766
1860
|
return;
|
|
767
1861
|
const timingText = showPerModeTimes ? modeTimes.join(",") : "...";
|
|
768
1862
|
const suffix = showPerModeTimes
|
|
@@ -850,10 +1944,19 @@ function resolveConfiguredModeTotal(configPath) {
|
|
|
850
1944
|
const configuredModes = Object.keys(config.modes).length;
|
|
851
1945
|
return configuredModes || 1;
|
|
852
1946
|
}
|
|
853
|
-
async function resolveConfiguredFileTotal(configPath) {
|
|
854
|
-
const files = await resolveSelectedFiles(configPath,
|
|
1947
|
+
async function resolveConfiguredFileTotal(configPath, selectors = []) {
|
|
1948
|
+
const files = await resolveSelectedFiles(configPath, selectors);
|
|
855
1949
|
return files.length;
|
|
856
1950
|
}
|
|
1951
|
+
async function previewBuildCommands(configPath, selectors, modeName, featureToggles) {
|
|
1952
|
+
const files = await resolveSelectedFiles(configPath, selectors);
|
|
1953
|
+
const out = {};
|
|
1954
|
+
for (const file of files) {
|
|
1955
|
+
const invocation = await getBuildInvocationPreview(configPath, file, modeName, featureToggles);
|
|
1956
|
+
out[file] = formatBuildInvocation(invocation);
|
|
1957
|
+
}
|
|
1958
|
+
return out;
|
|
1959
|
+
}
|
|
857
1960
|
function resolveExecutionModes(configPath, selectedModes) {
|
|
858
1961
|
if (selectedModes.length)
|
|
859
1962
|
return selectedModes;
|
|
@@ -872,15 +1975,35 @@ async function resolveSelectedFiles(configPath, selectors, warn = true) {
|
|
|
872
1975
|
const specs = matches.filter((file) => file.endsWith(".spec.ts"));
|
|
873
1976
|
return [...new Set(specs)].sort((a, b) => a.localeCompare(b));
|
|
874
1977
|
}
|
|
875
|
-
async function
|
|
1978
|
+
async function resolveSelectedFuzzFiles(configPath, selectors) {
|
|
1979
|
+
const resolvedConfigPath = configPath ?? path.join(process.cwd(), "./as-test.config.json");
|
|
1980
|
+
const config = loadConfig(resolvedConfigPath, false);
|
|
1981
|
+
const patterns = resolveFuzzPatterns(config.fuzz.input, selectors);
|
|
1982
|
+
const matches = await glob(patterns);
|
|
1983
|
+
const fuzzFiles = matches.filter((file) => file.endsWith(".fuzz.ts"));
|
|
1984
|
+
return [...new Set(fuzzFiles)].sort((a, b) => a.localeCompare(b));
|
|
1985
|
+
}
|
|
1986
|
+
async function resolveSelectedTestInputs(configPath, selectors) {
|
|
1987
|
+
const [specs, fuzz] = await Promise.all([
|
|
1988
|
+
resolveSelectedFiles(configPath, selectors),
|
|
1989
|
+
resolveSelectedFuzzFiles(configPath, selectors),
|
|
1990
|
+
]);
|
|
1991
|
+
return { specs, fuzz };
|
|
1992
|
+
}
|
|
1993
|
+
async function buildNoTestFilesMatchedError(configPath, selectors, includeFuzz = false) {
|
|
876
1994
|
const scope = selectors.length > 0 ? selectors.join(", ") : "configured input patterns";
|
|
877
1995
|
const lines = [`No test files matched: ${scope}`];
|
|
878
1996
|
const configuredFiles = await resolveSelectedFiles(configPath, [], false);
|
|
1997
|
+
const configuredFuzzFiles = includeFuzz
|
|
1998
|
+
? await resolveSelectedFuzzFiles(configPath, [])
|
|
1999
|
+
: [];
|
|
879
2000
|
if (!selectors.length) {
|
|
880
2001
|
lines.push('No specs were discovered from configured input patterns. Check "input" in config or run "ast doctor".');
|
|
881
2002
|
return new Error(lines.join("\n"));
|
|
882
2003
|
}
|
|
883
|
-
const suggestions = suggestClosestSuites(selectors,
|
|
2004
|
+
const suggestions = suggestClosestSuites(selectors, includeFuzz
|
|
2005
|
+
? [...configuredFiles, ...configuredFuzzFiles]
|
|
2006
|
+
: configuredFiles);
|
|
884
2007
|
if (suggestions.length) {
|
|
885
2008
|
lines.push(`Closest suite names: ${suggestions.join(", ")}`);
|
|
886
2009
|
}
|
|
@@ -894,6 +2017,13 @@ async function buildNoTestFilesMatchedError(configPath, selectors) {
|
|
|
894
2017
|
else {
|
|
895
2018
|
lines.push('No specs were discovered from configured input patterns. Check "input" in config.');
|
|
896
2019
|
}
|
|
2020
|
+
if (includeFuzz && configuredFuzzFiles.length) {
|
|
2021
|
+
const sample = configuredFuzzFiles
|
|
2022
|
+
.slice(0, 5)
|
|
2023
|
+
.map((file) => path.basename(file))
|
|
2024
|
+
.join(", ");
|
|
2025
|
+
lines.push(`Configured fuzzers (${configuredFuzzFiles.length}): ${sample}${configuredFuzzFiles.length > 5 ? ", ..." : ""}`);
|
|
2026
|
+
}
|
|
897
2027
|
lines.push('Run "ast test --list" to inspect resolved files.');
|
|
898
2028
|
return new Error(lines.join("\n"));
|
|
899
2029
|
}
|
|
@@ -980,6 +2110,27 @@ function resolveInputPatterns(configured, selectors) {
|
|
|
980
2110
|
}
|
|
981
2111
|
return [...patterns];
|
|
982
2112
|
}
|
|
2113
|
+
function resolveFuzzPatterns(configured, selectors) {
|
|
2114
|
+
const configuredInputs = Array.isArray(configured)
|
|
2115
|
+
? configured
|
|
2116
|
+
: [configured];
|
|
2117
|
+
if (!selectors.length)
|
|
2118
|
+
return configuredInputs;
|
|
2119
|
+
const patterns = new Set();
|
|
2120
|
+
for (const selector of expandSelectors(selectors)) {
|
|
2121
|
+
if (!selector)
|
|
2122
|
+
continue;
|
|
2123
|
+
if (isBareSuiteSelector(selector)) {
|
|
2124
|
+
const base = selector.replace(/\.fuzz\.ts$/, "").replace(/\.ts$/, "");
|
|
2125
|
+
for (const configuredInput of configuredInputs) {
|
|
2126
|
+
patterns.add(path.join(path.dirname(configuredInput), `${base}.fuzz.ts`));
|
|
2127
|
+
}
|
|
2128
|
+
continue;
|
|
2129
|
+
}
|
|
2130
|
+
patterns.add(selector);
|
|
2131
|
+
}
|
|
2132
|
+
return [...patterns];
|
|
2133
|
+
}
|
|
983
2134
|
function expandSelectors(selectors) {
|
|
984
2135
|
const expanded = [];
|
|
985
2136
|
for (const selector of selectors) {
|
|
@@ -1064,7 +2215,184 @@ function resolveArtifactFileNameForPreview(file, target, modeName, duplicateSpec
|
|
|
1064
2215
|
const stem = ext.length ? legacy.slice(0, -ext.length) : legacy;
|
|
1065
2216
|
return `${stem}.${disambiguator}${ext}`;
|
|
1066
2217
|
}
|
|
1067
|
-
async function
|
|
2218
|
+
async function ensureWebBrowsersReady(configPath, modes, browserOverride) {
|
|
2219
|
+
const resolvedConfigPath = configPath ?? path.join(process.cwd(), "./as-test.config.json");
|
|
2220
|
+
const config = loadConfig(resolvedConfigPath, true);
|
|
2221
|
+
const missing = [];
|
|
2222
|
+
for (const modeName of modes) {
|
|
2223
|
+
const applied = applyMode(config, modeName);
|
|
2224
|
+
const active = applied.config;
|
|
2225
|
+
if (!usesWebBrowser(active))
|
|
2226
|
+
continue;
|
|
2227
|
+
const requestedBrowser = browserOverride?.trim() || active.runOptions.runtime.browser.trim();
|
|
2228
|
+
const resolved = resolveBrowserSelection(requestedBrowser);
|
|
2229
|
+
if (!resolved) {
|
|
2230
|
+
missing.push({ modeName, browser: requestedBrowser });
|
|
2231
|
+
continue;
|
|
2232
|
+
}
|
|
2233
|
+
active.runOptions.runtime.browser = resolved.browser;
|
|
2234
|
+
process.env.BROWSER = resolved.browser;
|
|
2235
|
+
}
|
|
2236
|
+
if (!missing.length)
|
|
2237
|
+
return;
|
|
2238
|
+
await handleMissingWebBrowsers(missing);
|
|
2239
|
+
}
|
|
2240
|
+
function resolveBrowserSelection(requested = "") {
|
|
2241
|
+
if (requested.trim().length) {
|
|
2242
|
+
return resolveNamedBrowser(requested);
|
|
2243
|
+
}
|
|
2244
|
+
const envBrowser = process.env.BROWSER?.trim() ?? "";
|
|
2245
|
+
if (envBrowser.length) {
|
|
2246
|
+
return resolveNamedBrowser(envBrowser);
|
|
2247
|
+
}
|
|
2248
|
+
const candidates = [
|
|
2249
|
+
"chromium",
|
|
2250
|
+
"chromium-browser",
|
|
2251
|
+
"google-chrome",
|
|
2252
|
+
"google-chrome-stable",
|
|
2253
|
+
"chrome",
|
|
2254
|
+
"msedge",
|
|
2255
|
+
"firefox",
|
|
2256
|
+
];
|
|
2257
|
+
for (const candidate of candidates) {
|
|
2258
|
+
if (hasExecutable(candidate)) {
|
|
2259
|
+
return { browser: candidate };
|
|
2260
|
+
}
|
|
2261
|
+
}
|
|
2262
|
+
const playwrightFallback = resolvePlaywrightBrowserExecutable("chromium") ??
|
|
2263
|
+
resolvePlaywrightBrowserExecutable("firefox");
|
|
2264
|
+
if (playwrightFallback) {
|
|
2265
|
+
return { browser: playwrightFallback };
|
|
2266
|
+
}
|
|
2267
|
+
return null;
|
|
2268
|
+
}
|
|
2269
|
+
function resolveNamedBrowser(browser) {
|
|
2270
|
+
const normalized = browser.trim().toLowerCase();
|
|
2271
|
+
if (!normalized.length)
|
|
2272
|
+
return null;
|
|
2273
|
+
if (browser.includes("/") ||
|
|
2274
|
+
browser.includes("\\") ||
|
|
2275
|
+
path.isAbsolute(browser)) {
|
|
2276
|
+
return hasExecutable(browser) ? { browser } : null;
|
|
2277
|
+
}
|
|
2278
|
+
const aliases = {
|
|
2279
|
+
chromium: ["chromium", "chromium-browser"],
|
|
2280
|
+
chrome: [
|
|
2281
|
+
"google-chrome",
|
|
2282
|
+
"google-chrome-stable",
|
|
2283
|
+
"chrome",
|
|
2284
|
+
"chromium",
|
|
2285
|
+
"chromium-browser",
|
|
2286
|
+
],
|
|
2287
|
+
firefox: ["firefox"],
|
|
2288
|
+
webkit: [],
|
|
2289
|
+
};
|
|
2290
|
+
const candidates = aliases[normalized] ?? [browser];
|
|
2291
|
+
for (const candidate of candidates) {
|
|
2292
|
+
if (hasExecutable(candidate)) {
|
|
2293
|
+
return { browser: candidate };
|
|
2294
|
+
}
|
|
2295
|
+
}
|
|
2296
|
+
const playwrightFallback = resolvePlaywrightBrowserExecutable(normalized);
|
|
2297
|
+
if (playwrightFallback) {
|
|
2298
|
+
return { browser: playwrightFallback };
|
|
2299
|
+
}
|
|
2300
|
+
return null;
|
|
2301
|
+
}
|
|
2302
|
+
function usesWebBrowser(config) {
|
|
2303
|
+
return (config.buildOptions.target == "web" ||
|
|
2304
|
+
config.runOptions.runtime.browser.length > 0 ||
|
|
2305
|
+
config.runOptions.runtime.cmd.includes("default.web.js"));
|
|
2306
|
+
}
|
|
2307
|
+
async function handleMissingWebBrowsers(missing) {
|
|
2308
|
+
const scope = missing
|
|
2309
|
+
.map((entry) => entry.browser?.length
|
|
2310
|
+
? `${entry.modeName ?? "default"} (${entry.browser})`
|
|
2311
|
+
: (entry.modeName ?? "default"))
|
|
2312
|
+
.join(", ");
|
|
2313
|
+
const details = "no web-capable browser was found in PATH, BROWSER, or Playwright cache";
|
|
2314
|
+
if (!canPromptForWebInstall()) {
|
|
2315
|
+
throw new Error(`web target requires a browser for mode(s) ${scope}; ${details}. Export BROWSER or install one with "npx -y playwright install chromium" or "npx -y playwright install webkit".`);
|
|
2316
|
+
}
|
|
2317
|
+
process.stdout.write(chalk.bold.blue("◇ Browser Setup Needed") +
|
|
2318
|
+
"\n" +
|
|
2319
|
+
`│ ${details}\n` +
|
|
2320
|
+
"│\n");
|
|
2321
|
+
const choice = await promptLine("Install Chromium with Playwright now? [Y/n] ");
|
|
2322
|
+
const normalized = choice.trim().toLowerCase();
|
|
2323
|
+
if (normalized == "n" || normalized == "no") {
|
|
2324
|
+
throw new Error('browser install skipped. Export BROWSER or install one with "npx -y playwright install chromium" or "npx -y playwright install webkit", then rerun.');
|
|
2325
|
+
}
|
|
2326
|
+
if (normalized != "" && normalized != "y" && normalized != "yes") {
|
|
2327
|
+
throw new Error(`invalid answer "${choice}". Expected yes or no.`);
|
|
2328
|
+
}
|
|
2329
|
+
const selected = "chromium";
|
|
2330
|
+
process.stdout.write(chalk.dim(`installing ${selected} via Playwright...\n`));
|
|
2331
|
+
const install = spawnSync("npx", ["-y", "playwright", "install", selected], {
|
|
2332
|
+
stdio: "inherit",
|
|
2333
|
+
shell: false,
|
|
2334
|
+
});
|
|
2335
|
+
if (install.status !== 0) {
|
|
2336
|
+
throw new Error(`Playwright browser install failed for ${selected}`);
|
|
2337
|
+
}
|
|
2338
|
+
const browserPath = resolvePlaywrightBrowserExecutable(selected);
|
|
2339
|
+
if (!browserPath) {
|
|
2340
|
+
throw new Error(`Playwright installed ${selected}, but as-test could not locate the browser executable`);
|
|
2341
|
+
}
|
|
2342
|
+
process.env.BROWSER = browserPath;
|
|
2343
|
+
}
|
|
2344
|
+
function canPromptForWebInstall() {
|
|
2345
|
+
return Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
2346
|
+
}
|
|
2347
|
+
function promptLine(question) {
|
|
2348
|
+
return new Promise((resolve) => {
|
|
2349
|
+
const rl = createInterface({
|
|
2350
|
+
input: process.stdin,
|
|
2351
|
+
output: process.stdout,
|
|
2352
|
+
});
|
|
2353
|
+
rl.question(question, (answer) => {
|
|
2354
|
+
rl.close();
|
|
2355
|
+
resolve(answer);
|
|
2356
|
+
});
|
|
2357
|
+
});
|
|
2358
|
+
}
|
|
2359
|
+
function resolvePlaywrightBrowserExecutable(browser) {
|
|
2360
|
+
const cacheRoot = path.join(process.env.HOME ?? "", ".cache", "ms-playwright");
|
|
2361
|
+
if (!cacheRoot.length || !existsSync(cacheRoot))
|
|
2362
|
+
return null;
|
|
2363
|
+
const map = {
|
|
2364
|
+
chromium: ["chromium-*/chrome-linux64/chrome"],
|
|
2365
|
+
chrome: ["chromium-*/chrome-linux64/chrome"],
|
|
2366
|
+
firefox: ["firefox-*/firefox/firefox"],
|
|
2367
|
+
webkit: ["webkit-*/pw_run.sh"],
|
|
2368
|
+
};
|
|
2369
|
+
const patterns = map[browser] ?? [];
|
|
2370
|
+
for (const pattern of patterns) {
|
|
2371
|
+
const matches = glob.sync(path.join(cacheRoot, pattern)).sort();
|
|
2372
|
+
if (matches.length)
|
|
2373
|
+
return matches[matches.length - 1];
|
|
2374
|
+
}
|
|
2375
|
+
return null;
|
|
2376
|
+
}
|
|
2377
|
+
function hasExecutable(command) {
|
|
2378
|
+
if (!command.length)
|
|
2379
|
+
return false;
|
|
2380
|
+
if (command.includes("/") || command.includes("\\")) {
|
|
2381
|
+
return existsSync(command);
|
|
2382
|
+
}
|
|
2383
|
+
const pathValue = process.env.PATH ?? "";
|
|
2384
|
+
const suffixes = process.platform == "win32" ? ["", ".exe", ".cmd", ".bat"] : [""];
|
|
2385
|
+
for (const base of pathValue.split(path.delimiter)) {
|
|
2386
|
+
if (!base.length)
|
|
2387
|
+
continue;
|
|
2388
|
+
for (const suffix of suffixes) {
|
|
2389
|
+
if (existsSync(path.join(base, command + suffix)))
|
|
2390
|
+
return true;
|
|
2391
|
+
}
|
|
2392
|
+
}
|
|
2393
|
+
return false;
|
|
2394
|
+
}
|
|
2395
|
+
async function listExecutionPlan(command, configPath, selectors, modes, listFlags, fuzzEnabled = false) {
|
|
1068
2396
|
const resolvedConfigPath = configPath ?? path.join(process.cwd(), "./as-test.config.json");
|
|
1069
2397
|
const config = loadConfig(resolvedConfigPath, true);
|
|
1070
2398
|
const configuredModes = Object.keys(config.modes);
|
|
@@ -1093,36 +2421,86 @@ async function listExecutionPlan(command, configPath, selectors, modes, listFlag
|
|
|
1093
2421
|
}
|
|
1094
2422
|
if (!listFlags.list)
|
|
1095
2423
|
return;
|
|
1096
|
-
const
|
|
1097
|
-
|
|
2424
|
+
const specFiles = command == "fuzz" ? [] : await resolveSelectedFiles(configPath, selectors);
|
|
2425
|
+
const fuzzFiles = command == "fuzz"
|
|
2426
|
+
? await resolveSelectedFuzzFiles(configPath, selectors)
|
|
2427
|
+
: command == "test" && fuzzEnabled
|
|
2428
|
+
? await resolveSelectedFuzzFiles(configPath, selectors)
|
|
2429
|
+
: [];
|
|
2430
|
+
const files = command == "fuzz" ? fuzzFiles : specFiles;
|
|
2431
|
+
if (!specFiles.length && !fuzzFiles.length) {
|
|
1098
2432
|
const scope = selectors.length > 0 ? selectors.join(", ") : "configured input patterns";
|
|
1099
|
-
throw new Error(
|
|
2433
|
+
throw new Error(command == "fuzz"
|
|
2434
|
+
? `No fuzz files matched: ${scope}`
|
|
2435
|
+
: `No test files matched: ${scope}`);
|
|
1100
2436
|
}
|
|
1101
|
-
const duplicateSpecBasenames = resolveDuplicateSpecBasenames(
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
process.stdout.write(
|
|
2437
|
+
const duplicateSpecBasenames = resolveDuplicateSpecBasenames(specFiles);
|
|
2438
|
+
const duplicateFuzzBasenames = resolveDuplicateSpecBasenames(fuzzFiles);
|
|
2439
|
+
if (specFiles.length) {
|
|
2440
|
+
process.stdout.write(chalk.bold("Resolved files:\n"));
|
|
2441
|
+
for (const file of specFiles) {
|
|
2442
|
+
process.stdout.write(` - ${file}\n`);
|
|
2443
|
+
}
|
|
2444
|
+
process.stdout.write("\n");
|
|
2445
|
+
}
|
|
2446
|
+
if (fuzzFiles.length && command == "test") {
|
|
2447
|
+
process.stdout.write(chalk.bold("Resolved fuzz files:\n"));
|
|
2448
|
+
for (const file of fuzzFiles) {
|
|
2449
|
+
process.stdout.write(` - ${file}\n`);
|
|
2450
|
+
}
|
|
2451
|
+
process.stdout.write("\n");
|
|
2452
|
+
}
|
|
2453
|
+
if (command == "fuzz" && fuzzFiles.length) {
|
|
2454
|
+
process.stdout.write(chalk.bold("Resolved files:\n"));
|
|
2455
|
+
for (const file of fuzzFiles) {
|
|
2456
|
+
process.stdout.write(` - ${file}\n`);
|
|
2457
|
+
}
|
|
2458
|
+
process.stdout.write("\n");
|
|
1105
2459
|
}
|
|
1106
|
-
process.stdout.write("\n");
|
|
1107
2460
|
for (const modeName of modes) {
|
|
1108
2461
|
const applied = applyMode(config, modeName);
|
|
1109
2462
|
const active = applied.config;
|
|
1110
2463
|
const modeLabel = modeName ?? "default";
|
|
1111
2464
|
process.stdout.write(chalk.bold(`Mode: ${modeLabel}\n`));
|
|
1112
|
-
process.stdout.write(` target: ${active.buildOptions.target}\n`);
|
|
2465
|
+
process.stdout.write(` target: ${command == "fuzz" ? "bindings" : active.buildOptions.target}\n`);
|
|
1113
2466
|
process.stdout.write(` outDir: ${active.outDir}\n`);
|
|
1114
|
-
if (command
|
|
2467
|
+
if (command == "run" || command == "test") {
|
|
1115
2468
|
process.stdout.write(` runtime: ${active.runOptions.runtime.cmd}\n`);
|
|
2469
|
+
if (usesWebBrowser(active)) {
|
|
2470
|
+
process.stdout.write(` browser: ${active.runOptions.runtime.browser || "(auto)"}\n`);
|
|
2471
|
+
}
|
|
1116
2472
|
}
|
|
1117
|
-
const envOverrides =
|
|
1118
|
-
|
|
1119
|
-
|
|
2473
|
+
const envOverrides = {
|
|
2474
|
+
...config.env,
|
|
2475
|
+
...(modeName ? (config.modes[modeName]?.env ?? {}) : {}),
|
|
2476
|
+
...(command == "build"
|
|
2477
|
+
? active.buildOptions.env
|
|
2478
|
+
: command == "run" || command == "test"
|
|
2479
|
+
? active.runOptions.env
|
|
2480
|
+
: {}),
|
|
2481
|
+
};
|
|
1120
2482
|
const envKeys = Object.keys(envOverrides);
|
|
1121
2483
|
process.stdout.write(` env overrides: ${envKeys.length}${envKeys.length ? ` (${envKeys.join(", ")})` : ""}\n`);
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
const
|
|
1125
|
-
|
|
2484
|
+
if (specFiles.length) {
|
|
2485
|
+
process.stdout.write(" artifacts:\n");
|
|
2486
|
+
for (const file of specFiles) {
|
|
2487
|
+
const artifactName = resolveArtifactFileNameForPreview(file, active.buildOptions.target, modeName, duplicateSpecBasenames);
|
|
2488
|
+
process.stdout.write(` - ${path.join(active.outDir, artifactName)}\n`);
|
|
2489
|
+
}
|
|
2490
|
+
}
|
|
2491
|
+
if (fuzzFiles.length && command == "test") {
|
|
2492
|
+
process.stdout.write(" fuzz artifacts:\n");
|
|
2493
|
+
for (const file of fuzzFiles) {
|
|
2494
|
+
const artifactName = resolveArtifactFileNameForPreview(file, "bindings", modeName, duplicateFuzzBasenames);
|
|
2495
|
+
process.stdout.write(` - ${path.join(active.outDir, artifactName)}\n`);
|
|
2496
|
+
}
|
|
2497
|
+
}
|
|
2498
|
+
else if (command == "fuzz") {
|
|
2499
|
+
process.stdout.write(" artifacts:\n");
|
|
2500
|
+
for (const file of fuzzFiles) {
|
|
2501
|
+
const artifactName = resolveArtifactFileNameForPreview(file, "bindings", modeName, duplicateFuzzBasenames);
|
|
2502
|
+
process.stdout.write(` - ${path.join(active.outDir, artifactName)}\n`);
|
|
2503
|
+
}
|
|
1126
2504
|
}
|
|
1127
2505
|
process.stdout.write("\n");
|
|
1128
2506
|
}
|