as-test 1.0.0 → 1.0.3
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 +116 -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 -29
- package/bin/commands/build.js +1 -0
- 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 +1484 -169
- 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";
|
|
11
|
+
import { fuzz } from "./commands/fuzz-core.js";
|
|
10
12
|
import { applyMode, 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();
|
|
@@ -56,6 +63,8 @@ else if (COMMANDS.includes(args[0])) {
|
|
|
56
63
|
resolveCommandArgs,
|
|
57
64
|
resolveListFlags,
|
|
58
65
|
resolveFeatureToggles,
|
|
66
|
+
resolveParallelJobs,
|
|
67
|
+
resolveBrowserOverride,
|
|
59
68
|
resolveExecutionModes,
|
|
60
69
|
listExecutionPlan,
|
|
61
70
|
runRuntimeModes,
|
|
@@ -69,6 +78,9 @@ else if (COMMANDS.includes(args[0])) {
|
|
|
69
78
|
resolveCommandArgs,
|
|
70
79
|
resolveListFlags,
|
|
71
80
|
resolveFeatureToggles,
|
|
81
|
+
resolveParallelJobs,
|
|
82
|
+
resolveBrowserOverride,
|
|
83
|
+
resolveFuzzOverrides,
|
|
72
84
|
resolveExecutionModes,
|
|
73
85
|
listExecutionPlan,
|
|
74
86
|
runTestModes,
|
|
@@ -77,6 +89,19 @@ else if (COMMANDS.includes(args[0])) {
|
|
|
77
89
|
process.exit(1);
|
|
78
90
|
});
|
|
79
91
|
}
|
|
92
|
+
else if (command === "fuzz") {
|
|
93
|
+
executeFuzzCommand(_args, configPath, selectedModes, {
|
|
94
|
+
resolveCommandArgs,
|
|
95
|
+
resolveListFlags,
|
|
96
|
+
resolveJobs,
|
|
97
|
+
resolveExecutionModes,
|
|
98
|
+
listExecutionPlan,
|
|
99
|
+
runFuzzModes,
|
|
100
|
+
}).catch((error) => {
|
|
101
|
+
printCliError(error);
|
|
102
|
+
process.exit(1);
|
|
103
|
+
});
|
|
104
|
+
}
|
|
80
105
|
else if (command === "init") {
|
|
81
106
|
executeInitCommand(_args, {
|
|
82
107
|
resolveCommandTokens,
|
|
@@ -139,6 +164,13 @@ function info() {
|
|
|
139
164
|
" " +
|
|
140
165
|
"Build and run unit tests with selected runtime" +
|
|
141
166
|
"\n");
|
|
167
|
+
console.log(" " +
|
|
168
|
+
chalk.bold.blueBright("fuzz") +
|
|
169
|
+
" " +
|
|
170
|
+
chalk.dim("<name>|<path-or-glob>") +
|
|
171
|
+
" " +
|
|
172
|
+
"Build and run fuzz targets" +
|
|
173
|
+
"\n");
|
|
142
174
|
console.log(" " +
|
|
143
175
|
chalk.bold.magentaBright("init") +
|
|
144
176
|
" " +
|
|
@@ -153,63 +185,19 @@ function info() {
|
|
|
153
185
|
"Validate environment/config/runtime setup");
|
|
154
186
|
console.log("");
|
|
155
187
|
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") +
|
|
188
|
+
console.log(" " +
|
|
189
|
+
chalk.bold.blue("--version, -v") +
|
|
202
190
|
" " +
|
|
203
|
-
"
|
|
204
|
-
console.log("
|
|
191
|
+
"Print current cli version");
|
|
192
|
+
console.log(" " +
|
|
193
|
+
chalk.bold.blue("--help, -h") +
|
|
194
|
+
" Show help menu");
|
|
205
195
|
console.log("");
|
|
206
|
-
console.log(chalk.dim("If
|
|
196
|
+
console.log(chalk.dim("If this tool provides value, please consider sponsoring my open-source work! https://jairus.dev/sponsor") + "\n");
|
|
197
|
+
console.log("View the docs: " +
|
|
198
|
+
chalk.blue("https://docs.jairus.dev/as-test"));
|
|
207
199
|
console.log("View the repo: " +
|
|
208
|
-
chalk.
|
|
209
|
-
// console.log(
|
|
210
|
-
// "View the docs: " +
|
|
211
|
-
// chalk.blue("https://docs.jairus.dev/as-test"),
|
|
212
|
-
// );
|
|
200
|
+
chalk.blue("https://github.com/JairusSW/as-test"));
|
|
213
201
|
}
|
|
214
202
|
function isHelpFlag(value) {
|
|
215
203
|
return value == "--help" || value == "-h";
|
|
@@ -249,7 +237,13 @@ function printCommandHelp(command) {
|
|
|
249
237
|
process.stdout.write(chalk.bold("Flags:\n"));
|
|
250
238
|
process.stdout.write(" --config <path> Use a specific config file\n");
|
|
251
239
|
process.stdout.write(" --mode <name[,name...]> Run one or multiple named config modes\n");
|
|
252
|
-
process.stdout.write(" --
|
|
240
|
+
process.stdout.write(" --browser <name|path> Use chrome, chromium, firefox, webkit, or an executable path for web modes\n");
|
|
241
|
+
process.stdout.write(" --parallel Run files through an ordered worker pool using an automatic worker count\n");
|
|
242
|
+
process.stdout.write(" --jobs <n> Run files through an ordered worker pool\n");
|
|
243
|
+
process.stdout.write(" --build-jobs <n> Limit concurrent build tasks (defaults to --jobs)\n");
|
|
244
|
+
process.stdout.write(" --run-jobs <n> Limit concurrent run tasks (defaults to --jobs)\n");
|
|
245
|
+
process.stdout.write(" --create-snapshots Create missing snapshot entries\n");
|
|
246
|
+
process.stdout.write(" --overwrite-snapshots Overwrite existing snapshot entries on mismatch\n");
|
|
253
247
|
process.stdout.write(" --no-snapshot Disable snapshot assertions for this run\n");
|
|
254
248
|
process.stdout.write(" --show-coverage Print uncovered coverage point details\n");
|
|
255
249
|
process.stdout.write(" --enable <feature> Enable feature (coverage|try-as)\n");
|
|
@@ -269,11 +263,24 @@ function printCommandHelp(command) {
|
|
|
269
263
|
process.stdout.write(chalk.bold("Flags:\n"));
|
|
270
264
|
process.stdout.write(" --config <path> Use a specific config file\n");
|
|
271
265
|
process.stdout.write(" --mode <name[,name...]> Run one or multiple named config modes\n");
|
|
272
|
-
process.stdout.write(" --
|
|
266
|
+
process.stdout.write(" --browser <name|path> Use chrome, chromium, firefox, webkit, or an executable path for web modes\n");
|
|
267
|
+
process.stdout.write(" --parallel Run files through an ordered worker pool using an automatic worker count\n");
|
|
268
|
+
process.stdout.write(" --jobs <n> Run files through an ordered worker pool\n");
|
|
269
|
+
process.stdout.write(" --build-jobs <n> Limit concurrent build tasks (defaults to --jobs)\n");
|
|
270
|
+
process.stdout.write(" --run-jobs <n> Limit concurrent run tasks (defaults to --jobs)\n");
|
|
271
|
+
process.stdout.write(" --create-snapshots Create missing snapshot entries\n");
|
|
272
|
+
process.stdout.write(" --overwrite-snapshots Overwrite existing snapshot entries on mismatch\n");
|
|
273
273
|
process.stdout.write(" --no-snapshot Disable snapshot assertions for this run\n");
|
|
274
274
|
process.stdout.write(" --show-coverage Print uncovered coverage point details\n");
|
|
275
275
|
process.stdout.write(" --enable <feature> Enable feature (coverage|try-as)\n");
|
|
276
276
|
process.stdout.write(" --disable <feature> Disable feature (coverage|try-as)\n");
|
|
277
|
+
process.stdout.write(" --fuzz Run fuzz targets after the normal test pass\n");
|
|
278
|
+
process.stdout.write(" --fuzz-runs <n> Override fuzz iteration count for this run\n");
|
|
279
|
+
process.stdout.write(" --fuzz-seed <n> Override fuzz seed for this run\n");
|
|
280
|
+
process.stdout.write(" --parallel Run files through an ordered worker pool using an automatic worker count\n");
|
|
281
|
+
process.stdout.write(" --jobs <n> Run files through an ordered worker pool\n");
|
|
282
|
+
process.stdout.write(" --build-jobs <n> Limit concurrent build tasks (defaults to --jobs)\n");
|
|
283
|
+
process.stdout.write(" --run-jobs <n> Limit concurrent run tasks (defaults to --jobs)\n");
|
|
277
284
|
process.stdout.write(" --reporter <name|path> Use built-in reporter (default|tap) or custom module path\n");
|
|
278
285
|
process.stdout.write(" --tap Shortcut for --reporter tap\n");
|
|
279
286
|
process.stdout.write(" --verbose Keep expanded suite/test lines and live updates\n");
|
|
@@ -283,11 +290,27 @@ function printCommandHelp(command) {
|
|
|
283
290
|
process.stdout.write(" --help, -h Show this help\n");
|
|
284
291
|
return;
|
|
285
292
|
}
|
|
293
|
+
if (command == "fuzz") {
|
|
294
|
+
process.stdout.write(chalk.bold("Usage: ast fuzz [selectors...] [flags]\n\n"));
|
|
295
|
+
process.stdout.write("Build selected fuzz targets with bindings and execute them with generated inputs.\n\n");
|
|
296
|
+
process.stdout.write(chalk.bold("Flags:\n"));
|
|
297
|
+
process.stdout.write(" --config <path> Use a specific config file\n");
|
|
298
|
+
process.stdout.write(" --mode <name[,name...]> Run one or multiple named config modes\n");
|
|
299
|
+
process.stdout.write(" --runs <n> Override fuzz iteration count\n");
|
|
300
|
+
process.stdout.write(" --seed <n> Override fuzz seed\n");
|
|
301
|
+
process.stdout.write(" --jobs <n> Run files through an ordered worker pool\n");
|
|
302
|
+
process.stdout.write(" --build-jobs <n> Limit concurrent build tasks (defaults to --jobs)\n");
|
|
303
|
+
process.stdout.write(" --run-jobs <n> Limit concurrent run tasks (defaults to --jobs)\n");
|
|
304
|
+
process.stdout.write(" --list Preview resolved fuzz files without running\n");
|
|
305
|
+
process.stdout.write(" --list-modes Preview configured and selected mode names\n");
|
|
306
|
+
process.stdout.write(" --help, -h Show this help\n");
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
286
309
|
if (command == "init") {
|
|
287
310
|
process.stdout.write(chalk.bold("Usage: ast init [dir] [flags]\n\n"));
|
|
288
311
|
process.stdout.write("Initialize as-test config, default runners, and example specs.\n\n");
|
|
289
312
|
process.stdout.write(chalk.bold("Flags:\n"));
|
|
290
|
-
process.stdout.write(" --target <wasi|bindings>
|
|
313
|
+
process.stdout.write(" --target <wasi|bindings|web> Set build target\n");
|
|
291
314
|
process.stdout.write(" --example <minimal|full|none> Set example template\n");
|
|
292
315
|
process.stdout.write(" --install Install dependencies after scaffolding\n");
|
|
293
316
|
process.stdout.write(" --yes, -y Non-interactive setup with defaults\n");
|
|
@@ -361,6 +384,9 @@ function resolveCommandArgs(rawArgs, command) {
|
|
|
361
384
|
if (arg == "--tap") {
|
|
362
385
|
continue;
|
|
363
386
|
}
|
|
387
|
+
if (arg == "--fuzz") {
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
364
390
|
if (arg == "--enable" || arg == "--disable") {
|
|
365
391
|
i++;
|
|
366
392
|
continue;
|
|
@@ -368,6 +394,28 @@ function resolveCommandArgs(rawArgs, command) {
|
|
|
368
394
|
if (arg.startsWith("--enable=") || arg.startsWith("--disable=")) {
|
|
369
395
|
continue;
|
|
370
396
|
}
|
|
397
|
+
if (arg == "--runs" ||
|
|
398
|
+
arg == "--seed" ||
|
|
399
|
+
arg == "--parallel" ||
|
|
400
|
+
arg == "--jobs" ||
|
|
401
|
+
arg == "--build-jobs" ||
|
|
402
|
+
arg == "--run-jobs" ||
|
|
403
|
+
arg == "--browser" ||
|
|
404
|
+
arg == "--fuzz-runs" ||
|
|
405
|
+
arg == "--fuzz-seed") {
|
|
406
|
+
i++;
|
|
407
|
+
continue;
|
|
408
|
+
}
|
|
409
|
+
if (arg.startsWith("--runs=") ||
|
|
410
|
+
arg.startsWith("--seed=") ||
|
|
411
|
+
arg.startsWith("--jobs=") ||
|
|
412
|
+
arg.startsWith("--build-jobs=") ||
|
|
413
|
+
arg.startsWith("--run-jobs=") ||
|
|
414
|
+
arg.startsWith("--browser=") ||
|
|
415
|
+
arg.startsWith("--fuzz-runs=") ||
|
|
416
|
+
arg.startsWith("--fuzz-seed=")) {
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
371
419
|
if (arg.startsWith("-")) {
|
|
372
420
|
continue;
|
|
373
421
|
}
|
|
@@ -407,12 +455,51 @@ function resolveFeatureToggles(rawArgs, command) {
|
|
|
407
455
|
}
|
|
408
456
|
return out;
|
|
409
457
|
}
|
|
458
|
+
function resolveFuzzOverrides(rawArgs, command) {
|
|
459
|
+
const out = {};
|
|
460
|
+
let seenCommand = false;
|
|
461
|
+
for (let i = 0; i < rawArgs.length; i++) {
|
|
462
|
+
const arg = rawArgs[i];
|
|
463
|
+
if (!seenCommand) {
|
|
464
|
+
if (arg == command)
|
|
465
|
+
seenCommand = true;
|
|
466
|
+
continue;
|
|
467
|
+
}
|
|
468
|
+
const direct = command == "fuzz"
|
|
469
|
+
? {
|
|
470
|
+
runs: "--runs",
|
|
471
|
+
seed: "--seed",
|
|
472
|
+
}
|
|
473
|
+
: {
|
|
474
|
+
runs: "--fuzz-runs",
|
|
475
|
+
seed: "--fuzz-seed",
|
|
476
|
+
};
|
|
477
|
+
const runs = parseNumberFlag(rawArgs, i, direct.runs);
|
|
478
|
+
if (runs) {
|
|
479
|
+
out.runs = runs.number;
|
|
480
|
+
if (runs.consumeNext)
|
|
481
|
+
i++;
|
|
482
|
+
continue;
|
|
483
|
+
}
|
|
484
|
+
const seed = parseNumberFlag(rawArgs, i, direct.seed);
|
|
485
|
+
if (seed) {
|
|
486
|
+
out.seed = seed.number;
|
|
487
|
+
if (seed.consumeNext)
|
|
488
|
+
i++;
|
|
489
|
+
continue;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
return out;
|
|
493
|
+
}
|
|
410
494
|
function resolveListFlags(rawArgs, command) {
|
|
411
495
|
const out = {
|
|
412
496
|
list: false,
|
|
413
497
|
listModes: false,
|
|
414
498
|
};
|
|
415
|
-
if (command !== "build" &&
|
|
499
|
+
if (command !== "build" &&
|
|
500
|
+
command !== "run" &&
|
|
501
|
+
command !== "test" &&
|
|
502
|
+
command !== "fuzz") {
|
|
416
503
|
return out;
|
|
417
504
|
}
|
|
418
505
|
let seenCommand = false;
|
|
@@ -430,6 +517,318 @@ function resolveListFlags(rawArgs, command) {
|
|
|
430
517
|
}
|
|
431
518
|
return out;
|
|
432
519
|
}
|
|
520
|
+
function parseNumberFlag(rawArgs, index, flag) {
|
|
521
|
+
const arg = rawArgs[index];
|
|
522
|
+
if (arg == flag) {
|
|
523
|
+
const next = rawArgs[index + 1];
|
|
524
|
+
if (!next || next.startsWith("-")) {
|
|
525
|
+
throw new Error(`${flag} requires a numeric value`);
|
|
526
|
+
}
|
|
527
|
+
return {
|
|
528
|
+
key: flag,
|
|
529
|
+
number: parseIntegerFlag(flag, next),
|
|
530
|
+
consumeNext: true,
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
if (arg.startsWith(`${flag}=`)) {
|
|
534
|
+
return {
|
|
535
|
+
key: flag,
|
|
536
|
+
number: parseIntegerFlag(flag, arg.slice(flag.length + 1)),
|
|
537
|
+
consumeNext: false,
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
return null;
|
|
541
|
+
}
|
|
542
|
+
function parseStringFlag(rawArgs, index, flag) {
|
|
543
|
+
const arg = rawArgs[index];
|
|
544
|
+
if (arg == flag) {
|
|
545
|
+
const next = rawArgs[index + 1];
|
|
546
|
+
if (!next || next.startsWith("-")) {
|
|
547
|
+
throw new Error(`${flag} requires a value`);
|
|
548
|
+
}
|
|
549
|
+
return { key: flag, value: next, consumeNext: true };
|
|
550
|
+
}
|
|
551
|
+
if (arg.startsWith(`${flag}=`)) {
|
|
552
|
+
const value = arg.slice(flag.length + 1);
|
|
553
|
+
if (!value.length) {
|
|
554
|
+
throw new Error(`${flag} requires a value`);
|
|
555
|
+
}
|
|
556
|
+
return { key: flag, value, consumeNext: false };
|
|
557
|
+
}
|
|
558
|
+
return null;
|
|
559
|
+
}
|
|
560
|
+
function resolveBrowserOverride(rawArgs, command) {
|
|
561
|
+
let seenCommand = false;
|
|
562
|
+
for (let i = 0; i < rawArgs.length; i++) {
|
|
563
|
+
const arg = rawArgs[i];
|
|
564
|
+
if (!seenCommand) {
|
|
565
|
+
if (arg == command)
|
|
566
|
+
seenCommand = true;
|
|
567
|
+
continue;
|
|
568
|
+
}
|
|
569
|
+
const parsed = parseStringFlag(rawArgs, i, "--browser");
|
|
570
|
+
if (!parsed)
|
|
571
|
+
continue;
|
|
572
|
+
return parsed.value.trim() || undefined;
|
|
573
|
+
}
|
|
574
|
+
return undefined;
|
|
575
|
+
}
|
|
576
|
+
function resolveJobs(rawArgs, command) {
|
|
577
|
+
let seenCommand = false;
|
|
578
|
+
let parallel = false;
|
|
579
|
+
for (let i = 0; i < rawArgs.length; i++) {
|
|
580
|
+
const arg = rawArgs[i];
|
|
581
|
+
if (!seenCommand) {
|
|
582
|
+
if (arg == command)
|
|
583
|
+
seenCommand = true;
|
|
584
|
+
continue;
|
|
585
|
+
}
|
|
586
|
+
if (arg == "--parallel") {
|
|
587
|
+
parallel = true;
|
|
588
|
+
continue;
|
|
589
|
+
}
|
|
590
|
+
const parsed = parseNumberFlag(rawArgs, i, "--jobs");
|
|
591
|
+
if (!parsed)
|
|
592
|
+
continue;
|
|
593
|
+
if (parsed.number < 1) {
|
|
594
|
+
throw new Error("--jobs requires a positive integer");
|
|
595
|
+
}
|
|
596
|
+
return parsed.number;
|
|
597
|
+
}
|
|
598
|
+
return parallel ? 0 : 1;
|
|
599
|
+
}
|
|
600
|
+
function resolveParallelJobs(rawArgs, command) {
|
|
601
|
+
const baseJobs = resolveJobs(rawArgs, command);
|
|
602
|
+
let buildJobs = baseJobs;
|
|
603
|
+
let runJobs = baseJobs;
|
|
604
|
+
let seenCommand = false;
|
|
605
|
+
for (let i = 0; i < rawArgs.length; i++) {
|
|
606
|
+
const arg = rawArgs[i];
|
|
607
|
+
if (!seenCommand) {
|
|
608
|
+
if (arg == command)
|
|
609
|
+
seenCommand = true;
|
|
610
|
+
continue;
|
|
611
|
+
}
|
|
612
|
+
const buildParsed = parseNumberFlag(rawArgs, i, "--build-jobs");
|
|
613
|
+
if (buildParsed) {
|
|
614
|
+
if (buildParsed.number < 1) {
|
|
615
|
+
throw new Error("--build-jobs requires a positive integer");
|
|
616
|
+
}
|
|
617
|
+
buildJobs = buildParsed.number;
|
|
618
|
+
continue;
|
|
619
|
+
}
|
|
620
|
+
const runParsed = parseNumberFlag(rawArgs, i, "--run-jobs");
|
|
621
|
+
if (runParsed) {
|
|
622
|
+
if (runParsed.number < 1) {
|
|
623
|
+
throw new Error("--run-jobs requires a positive integer");
|
|
624
|
+
}
|
|
625
|
+
runJobs = runParsed.number;
|
|
626
|
+
continue;
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
const jobs = Math.max(baseJobs, buildJobs, runJobs);
|
|
630
|
+
return { jobs, buildJobs, runJobs };
|
|
631
|
+
}
|
|
632
|
+
function resolveFuzzParallelJobs(rawArgs) {
|
|
633
|
+
const baseJobs = resolveJobs(rawArgs, "fuzz");
|
|
634
|
+
let buildJobs = baseJobs;
|
|
635
|
+
let runJobs = baseJobs;
|
|
636
|
+
let seenCommand = false;
|
|
637
|
+
for (let i = 0; i < rawArgs.length; i++) {
|
|
638
|
+
const arg = rawArgs[i];
|
|
639
|
+
if (!seenCommand) {
|
|
640
|
+
if (arg == "fuzz")
|
|
641
|
+
seenCommand = true;
|
|
642
|
+
continue;
|
|
643
|
+
}
|
|
644
|
+
const buildParsed = parseNumberFlag(rawArgs, i, "--build-jobs");
|
|
645
|
+
if (buildParsed) {
|
|
646
|
+
if (buildParsed.number < 1) {
|
|
647
|
+
throw new Error("--build-jobs requires a positive integer");
|
|
648
|
+
}
|
|
649
|
+
buildJobs = buildParsed.number;
|
|
650
|
+
continue;
|
|
651
|
+
}
|
|
652
|
+
const runParsed = parseNumberFlag(rawArgs, i, "--run-jobs");
|
|
653
|
+
if (runParsed) {
|
|
654
|
+
if (runParsed.number < 1) {
|
|
655
|
+
throw new Error("--run-jobs requires a positive integer");
|
|
656
|
+
}
|
|
657
|
+
runJobs = runParsed.number;
|
|
658
|
+
continue;
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
const jobs = Math.max(baseJobs, buildJobs, runJobs);
|
|
662
|
+
return { jobs, buildJobs, runJobs };
|
|
663
|
+
}
|
|
664
|
+
function resolveEffectiveParallelJobs(settings, totalFiles) {
|
|
665
|
+
if (settings.jobs > 0) {
|
|
666
|
+
return {
|
|
667
|
+
jobs: Math.max(settings.jobs, settings.buildJobs, settings.runJobs),
|
|
668
|
+
buildJobs: settings.buildJobs > 0 ? settings.buildJobs : settings.jobs,
|
|
669
|
+
runJobs: settings.runJobs > 0 ? settings.runJobs : settings.jobs,
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
const autoJobs = resolveAutoJobs(totalFiles);
|
|
673
|
+
return {
|
|
674
|
+
jobs: Math.max(autoJobs, settings.buildJobs, settings.runJobs),
|
|
675
|
+
buildJobs: settings.buildJobs > 0 ? settings.buildJobs : autoJobs,
|
|
676
|
+
runJobs: settings.runJobs > 0 ? settings.runJobs : autoJobs,
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
function resolveAutoJobs(totalFiles) {
|
|
680
|
+
const cpuCount = typeof availableParallelism == "function"
|
|
681
|
+
? availableParallelism()
|
|
682
|
+
: cpus().length;
|
|
683
|
+
const cpuBudget = Math.max(1, cpuCount - 1);
|
|
684
|
+
if (totalFiles <= 1)
|
|
685
|
+
return 1;
|
|
686
|
+
if (totalFiles <= 4)
|
|
687
|
+
return Math.min(2, cpuBudget, totalFiles);
|
|
688
|
+
if (totalFiles <= 12)
|
|
689
|
+
return Math.min(3, cpuBudget);
|
|
690
|
+
if (totalFiles <= 32)
|
|
691
|
+
return Math.min(4, cpuBudget);
|
|
692
|
+
return Math.min(Math.max(4, Math.ceil(totalFiles / 12)), cpuBudget);
|
|
693
|
+
}
|
|
694
|
+
function createBufferedStream() {
|
|
695
|
+
const chunks = [];
|
|
696
|
+
return {
|
|
697
|
+
isTTY: false,
|
|
698
|
+
write(chunk) {
|
|
699
|
+
chunks.push(typeof chunk == "string" ? chunk : Buffer.from(chunk).toString("utf8"));
|
|
700
|
+
return true;
|
|
701
|
+
},
|
|
702
|
+
read() {
|
|
703
|
+
return chunks.join("");
|
|
704
|
+
},
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
async function createBufferedReporter(configPath, modeName) {
|
|
708
|
+
const stream = createBufferedStream();
|
|
709
|
+
const session = await createRunReporter(configPath, undefined, modeName, {
|
|
710
|
+
stdout: stream,
|
|
711
|
+
stderr: stream,
|
|
712
|
+
});
|
|
713
|
+
return {
|
|
714
|
+
reporter: session.reporter,
|
|
715
|
+
reporterKind: session.reporterKind,
|
|
716
|
+
runtimeName: session.runtimeName,
|
|
717
|
+
output: () => stream.read(),
|
|
718
|
+
};
|
|
719
|
+
}
|
|
720
|
+
async function runOrderedPool(items, jobs, worker) {
|
|
721
|
+
const width = Math.max(1, jobs);
|
|
722
|
+
let nextIndex = 0;
|
|
723
|
+
let firstError = null;
|
|
724
|
+
async function runWorker() {
|
|
725
|
+
while (true) {
|
|
726
|
+
if (firstError != null)
|
|
727
|
+
return;
|
|
728
|
+
const index = nextIndex++;
|
|
729
|
+
if (index >= items.length)
|
|
730
|
+
return;
|
|
731
|
+
try {
|
|
732
|
+
await worker(items[index], index);
|
|
733
|
+
}
|
|
734
|
+
catch (error) {
|
|
735
|
+
if (firstError == null)
|
|
736
|
+
firstError = error;
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
await Promise.all(Array.from({ length: Math.min(width, items.length) }, () => runWorker()));
|
|
742
|
+
if (firstError != null)
|
|
743
|
+
throw firstError;
|
|
744
|
+
}
|
|
745
|
+
function createAsyncLimiter(limit) {
|
|
746
|
+
const width = Math.max(1, limit);
|
|
747
|
+
let active = 0;
|
|
748
|
+
const queue = [];
|
|
749
|
+
return async function withLimit(task) {
|
|
750
|
+
if (active >= width) {
|
|
751
|
+
await new Promise((resolve) => queue.push(resolve));
|
|
752
|
+
}
|
|
753
|
+
active++;
|
|
754
|
+
try {
|
|
755
|
+
return await task();
|
|
756
|
+
}
|
|
757
|
+
finally {
|
|
758
|
+
active--;
|
|
759
|
+
const next = queue.shift();
|
|
760
|
+
if (next)
|
|
761
|
+
next();
|
|
762
|
+
}
|
|
763
|
+
};
|
|
764
|
+
}
|
|
765
|
+
function canRewriteParallelQueue() {
|
|
766
|
+
return Boolean(process.stdout.isTTY);
|
|
767
|
+
}
|
|
768
|
+
class ParallelQueueDisplay {
|
|
769
|
+
constructor(showStartLines) {
|
|
770
|
+
this.showStartLines = showStartLines;
|
|
771
|
+
this.active = new Map();
|
|
772
|
+
this.renderedLines = 0;
|
|
773
|
+
this.enabled = showStartLines && canRewriteParallelQueue();
|
|
774
|
+
}
|
|
775
|
+
start(file) {
|
|
776
|
+
const token = Symbol(file);
|
|
777
|
+
if (!this.showStartLines)
|
|
778
|
+
return token;
|
|
779
|
+
const line = `${chalk.bgBlackBright.white(" .... ")} ${file}`;
|
|
780
|
+
if (!this.enabled)
|
|
781
|
+
return token;
|
|
782
|
+
this.clear();
|
|
783
|
+
this.active.set(token, line);
|
|
784
|
+
this.render();
|
|
785
|
+
return token;
|
|
786
|
+
}
|
|
787
|
+
complete(token, output) {
|
|
788
|
+
if (!this.showStartLines || !this.enabled) {
|
|
789
|
+
process.stdout.write(output);
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
792
|
+
this.clear();
|
|
793
|
+
process.stdout.write(output);
|
|
794
|
+
this.active.delete(token);
|
|
795
|
+
this.render();
|
|
796
|
+
}
|
|
797
|
+
flush() {
|
|
798
|
+
if (!this.enabled)
|
|
799
|
+
return;
|
|
800
|
+
this.clear();
|
|
801
|
+
}
|
|
802
|
+
clear() {
|
|
803
|
+
if (!this.renderedLines)
|
|
804
|
+
return;
|
|
805
|
+
for (let i = 0; i < this.renderedLines; i++) {
|
|
806
|
+
process.stdout.write("\r\x1b[2K");
|
|
807
|
+
if (i < this.renderedLines - 1)
|
|
808
|
+
process.stdout.write("\x1b[1A");
|
|
809
|
+
}
|
|
810
|
+
this.renderedLines = 0;
|
|
811
|
+
}
|
|
812
|
+
render() {
|
|
813
|
+
if (!this.enabled)
|
|
814
|
+
return;
|
|
815
|
+
const lines = Array.from(this.active.values());
|
|
816
|
+
if (!lines.length)
|
|
817
|
+
return;
|
|
818
|
+
process.stdout.write(lines.join("\n"));
|
|
819
|
+
this.renderedLines = lines.length;
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
function renderQueuedFileStart(display, file) {
|
|
823
|
+
return display.start(file);
|
|
824
|
+
}
|
|
825
|
+
function parseIntegerFlag(flag, value) {
|
|
826
|
+
const parsed = Number(value);
|
|
827
|
+
if (!Number.isFinite(parsed) || parsed < 0) {
|
|
828
|
+
throw new Error(`${flag} requires a non-negative integer`);
|
|
829
|
+
}
|
|
830
|
+
return Math.floor(parsed);
|
|
831
|
+
}
|
|
433
832
|
function applyFeatureToggle(out, rawFeature, enabled) {
|
|
434
833
|
const key = rawFeature.trim().toLowerCase();
|
|
435
834
|
if (key == "coverage") {
|
|
@@ -456,26 +855,32 @@ function resolveCommandTokens(rawArgs, command) {
|
|
|
456
855
|
}
|
|
457
856
|
return values;
|
|
458
857
|
}
|
|
459
|
-
async function runTestSequential(runFlags, configPath, selectors, buildFeatureToggles, modeSummaryTotal, fileSummaryTotal, modeName) {
|
|
858
|
+
async function runTestSequential(runFlags, configPath, selectors, buildFeatureToggles, modeSummaryTotal, fileSummaryTotal, allowNoSpecFiles = false, modeName, reporterOverride, emitRunComplete = true) {
|
|
460
859
|
const files = await resolveSelectedFiles(configPath, selectors);
|
|
461
860
|
if (!files.length) {
|
|
462
|
-
|
|
861
|
+
if (!allowNoSpecFiles) {
|
|
862
|
+
throw await buildNoTestFilesMatchedError(configPath, selectors);
|
|
863
|
+
}
|
|
463
864
|
}
|
|
464
865
|
const reporterSession = await createRunReporter(configPath, undefined, modeName);
|
|
465
|
-
const reporter = reporterSession.reporter;
|
|
866
|
+
const reporter = reporterOverride ?? reporterSession.reporter;
|
|
466
867
|
const snapshotEnabled = runFlags.snapshot !== false;
|
|
467
868
|
reporter.onRunStart?.({
|
|
468
869
|
runtimeName: reporterSession.runtimeName,
|
|
469
870
|
clean: runFlags.clean,
|
|
470
871
|
verbose: runFlags.verbose,
|
|
471
872
|
snapshotEnabled,
|
|
472
|
-
|
|
873
|
+
createSnapshots: runFlags.createSnapshots,
|
|
473
874
|
});
|
|
474
875
|
const results = [];
|
|
475
876
|
let failed = false;
|
|
877
|
+
const buildIntervals = [];
|
|
476
878
|
const duplicateSpecBasenames = resolveDuplicateSpecBasenames(files);
|
|
477
879
|
for (const file of files) {
|
|
880
|
+
const buildStartedAt = Date.now();
|
|
478
881
|
await build(configPath, [file], modeName, buildFeatureToggles);
|
|
882
|
+
buildIntervals.push({ start: buildStartedAt, end: Date.now() });
|
|
883
|
+
const buildInvocation = await getBuildInvocationPreview(configPath, file, modeName, buildFeatureToggles);
|
|
479
884
|
const artifactKey = resolvePerFileArtifactKey(file, duplicateSpecBasenames);
|
|
480
885
|
const result = await run(runFlags, configPath, [file], false, {
|
|
481
886
|
reporter,
|
|
@@ -483,6 +888,7 @@ async function runTestSequential(runFlags, configPath, selectors, buildFeatureTo
|
|
|
483
888
|
emitRunComplete: false,
|
|
484
889
|
logFileName: `test.${artifactKey}.log.json`,
|
|
485
890
|
coverageFileName: `coverage.${artifactKey}.log.json`,
|
|
891
|
+
buildCommand: formatBuildInvocation(buildInvocation),
|
|
486
892
|
modeName,
|
|
487
893
|
});
|
|
488
894
|
results.push(result);
|
|
@@ -491,17 +897,30 @@ async function runTestSequential(runFlags, configPath, selectors, buildFeatureTo
|
|
|
491
897
|
}
|
|
492
898
|
const summary = aggregateRunResults(results);
|
|
493
899
|
summary.stats = applyConfiguredFileTotalToStats(summary.stats, fileSummaryTotal);
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
900
|
+
if (emitRunComplete) {
|
|
901
|
+
reporter.onRunComplete?.({
|
|
902
|
+
clean: runFlags.clean,
|
|
903
|
+
snapshotEnabled,
|
|
904
|
+
showCoverage: runFlags.showCoverage,
|
|
905
|
+
buildTime: getMergedIntervalDuration(buildIntervals),
|
|
906
|
+
snapshotSummary: summary.snapshotSummary,
|
|
907
|
+
coverageSummary: summary.coverageSummary,
|
|
908
|
+
stats: summary.stats,
|
|
909
|
+
reports: summary.reports,
|
|
910
|
+
modeSummary: buildSingleModeSummary(summary.stats, summary.snapshotSummary, modeSummaryTotal),
|
|
911
|
+
});
|
|
912
|
+
reporter.flush?.();
|
|
913
|
+
}
|
|
914
|
+
return {
|
|
915
|
+
failed,
|
|
916
|
+
summary: {
|
|
917
|
+
buildTime: getMergedIntervalDuration(buildIntervals),
|
|
918
|
+
snapshotSummary: summary.snapshotSummary,
|
|
919
|
+
coverageSummary: summary.coverageSummary,
|
|
920
|
+
stats: summary.stats,
|
|
921
|
+
reports: summary.reports,
|
|
922
|
+
},
|
|
923
|
+
};
|
|
505
924
|
}
|
|
506
925
|
async function runBuildModes(configPath, selectors, modes, buildFeatureToggles) {
|
|
507
926
|
for (const modeName of modes) {
|
|
@@ -509,20 +928,42 @@ async function runBuildModes(configPath, selectors, modes, buildFeatureToggles)
|
|
|
509
928
|
}
|
|
510
929
|
}
|
|
511
930
|
async function runRuntimeModes(runFlags, configPath, selectors, modes) {
|
|
512
|
-
|
|
931
|
+
await ensureWebBrowsersReady(configPath, modes, runFlags.browser);
|
|
932
|
+
const modeSummaryTotal = Math.max(modes.length, 1);
|
|
513
933
|
const fileSummaryTotal = await resolveConfiguredFileTotal(configPath);
|
|
934
|
+
const effectiveRunFlags = {
|
|
935
|
+
...runFlags,
|
|
936
|
+
...resolveEffectiveParallelJobs(runFlags, fileSummaryTotal),
|
|
937
|
+
};
|
|
938
|
+
if (effectiveRunFlags.jobs > 1) {
|
|
939
|
+
if (modes.length > 1) {
|
|
940
|
+
const failed = await runRuntimeMatrixParallel(effectiveRunFlags, configPath, selectors, modes, modeSummaryTotal, fileSummaryTotal);
|
|
941
|
+
process.exit(failed ? 1 : 0);
|
|
942
|
+
return;
|
|
943
|
+
}
|
|
944
|
+
let failed = false;
|
|
945
|
+
for (const modeName of modes) {
|
|
946
|
+
const result = await runRuntimeSingleParallel(effectiveRunFlags, configPath, selectors, modeName, modeSummaryTotal, fileSummaryTotal);
|
|
947
|
+
if (result)
|
|
948
|
+
failed = true;
|
|
949
|
+
}
|
|
950
|
+
process.exit(failed ? 1 : 0);
|
|
951
|
+
return;
|
|
952
|
+
}
|
|
514
953
|
if (modes.length > 1) {
|
|
515
|
-
const failed = await runRuntimeMatrix(
|
|
954
|
+
const failed = await runRuntimeMatrix(effectiveRunFlags, configPath, selectors, modes, modeSummaryTotal, fileSummaryTotal);
|
|
516
955
|
process.exit(failed ? 1 : 0);
|
|
517
956
|
return;
|
|
518
957
|
}
|
|
519
958
|
let failed = false;
|
|
959
|
+
const buildCommandsByFile = await previewBuildCommands(configPath, selectors, modes[0], {});
|
|
520
960
|
for (const modeName of modes) {
|
|
521
|
-
const result = await run(
|
|
961
|
+
const result = await run(effectiveRunFlags, configPath, selectors, false, {
|
|
522
962
|
modeName,
|
|
523
963
|
modeSummaryTotal,
|
|
524
964
|
modeSummaryExecuted: 1,
|
|
525
965
|
fileSummaryTotal,
|
|
966
|
+
buildCommandsByFile,
|
|
526
967
|
});
|
|
527
968
|
if (result.failed)
|
|
528
969
|
failed = true;
|
|
@@ -542,7 +983,7 @@ async function runRuntimeMatrix(runFlags, configPath, selectors, modes, modeSumm
|
|
|
542
983
|
clean: runFlags.clean,
|
|
543
984
|
verbose: runFlags.verbose,
|
|
544
985
|
snapshotEnabled,
|
|
545
|
-
|
|
986
|
+
createSnapshots: runFlags.createSnapshots,
|
|
546
987
|
});
|
|
547
988
|
const silentReporter = {};
|
|
548
989
|
const allResults = [];
|
|
@@ -558,6 +999,7 @@ async function runRuntimeMatrix(runFlags, configPath, selectors, modes, modeSumm
|
|
|
558
999
|
passed: false,
|
|
559
1000
|
}));
|
|
560
1001
|
const duplicateSpecBasenames = resolveDuplicateSpecBasenames(files);
|
|
1002
|
+
const buildIntervals = [];
|
|
561
1003
|
for (let fileIndex = 0; fileIndex < files.length; fileIndex++) {
|
|
562
1004
|
const file = files[fileIndex];
|
|
563
1005
|
const fileName = path.basename(file);
|
|
@@ -569,6 +1011,7 @@ async function runRuntimeMatrix(runFlags, configPath, selectors, modes, modeSumm
|
|
|
569
1011
|
for (let i = 0; i < modes.length; i++) {
|
|
570
1012
|
const modeName = modes[i];
|
|
571
1013
|
try {
|
|
1014
|
+
const buildInvocation = await getBuildInvocationPreview(configPath, file, modeName, {});
|
|
572
1015
|
const artifactKey = resolvePerFileArtifactKey(file, duplicateSpecBasenames);
|
|
573
1016
|
const result = await run(runFlags, configPath, [file], false, {
|
|
574
1017
|
reporter: silentReporter,
|
|
@@ -577,6 +1020,7 @@ async function runRuntimeMatrix(runFlags, configPath, selectors, modes, modeSumm
|
|
|
577
1020
|
emitRunComplete: false,
|
|
578
1021
|
logFileName: `run.${artifactKey}.log.json`,
|
|
579
1022
|
coverageFileName: `coverage.${artifactKey}.log.json`,
|
|
1023
|
+
buildCommand: formatBuildInvocation(buildInvocation),
|
|
580
1024
|
modeName,
|
|
581
1025
|
});
|
|
582
1026
|
modeTimes[i] = formatMatrixModeTime(result.stats.time);
|
|
@@ -612,6 +1056,7 @@ async function runRuntimeMatrix(runFlags, configPath, selectors, modes, modeSumm
|
|
|
612
1056
|
clean: runFlags.clean,
|
|
613
1057
|
snapshotEnabled,
|
|
614
1058
|
showCoverage: runFlags.showCoverage,
|
|
1059
|
+
buildTime: 0,
|
|
615
1060
|
snapshotSummary: summary.snapshotSummary,
|
|
616
1061
|
coverageSummary: summary.coverageSummary,
|
|
617
1062
|
stats: summary.stats,
|
|
@@ -620,26 +1065,75 @@ async function runRuntimeMatrix(runFlags, configPath, selectors, modes, modeSumm
|
|
|
620
1065
|
});
|
|
621
1066
|
return allResults.some((result) => result.failed);
|
|
622
1067
|
}
|
|
623
|
-
async function runTestModes(runFlags, configPath, selectors, modes, buildFeatureToggles) {
|
|
624
|
-
|
|
625
|
-
const
|
|
1068
|
+
async function runTestModes(runFlags, configPath, selectors, modes, buildFeatureToggles, fuzzEnabled, fuzzOverrides) {
|
|
1069
|
+
await ensureWebBrowsersReady(configPath, modes, runFlags.browser);
|
|
1070
|
+
const modeSummaryTotal = Math.max(modes.length, 1);
|
|
1071
|
+
const fileSummaryTotal = await resolveConfiguredFileTotal(configPath, selectors);
|
|
1072
|
+
const effectiveRunFlags = {
|
|
1073
|
+
...runFlags,
|
|
1074
|
+
...resolveEffectiveParallelJobs(runFlags, fileSummaryTotal),
|
|
1075
|
+
};
|
|
1076
|
+
if (effectiveRunFlags.jobs > 1) {
|
|
1077
|
+
if (modes.length > 1) {
|
|
1078
|
+
const failed = await runTestMatrixParallel(effectiveRunFlags, configPath, selectors, modes, buildFeatureToggles, modeSummaryTotal, fileSummaryTotal, fuzzEnabled, fuzzOverrides);
|
|
1079
|
+
process.exit(failed ? 1 : 0);
|
|
1080
|
+
return;
|
|
1081
|
+
}
|
|
1082
|
+
let failed = false;
|
|
1083
|
+
for (const modeName of modes) {
|
|
1084
|
+
const modeFailed = await runTestSingleParallel(effectiveRunFlags, configPath, selectors, buildFeatureToggles, modeSummaryTotal, fileSummaryTotal, fuzzEnabled, fuzzOverrides, modeName);
|
|
1085
|
+
if (modeFailed)
|
|
1086
|
+
failed = true;
|
|
1087
|
+
}
|
|
1088
|
+
process.exit(failed ? 1 : 0);
|
|
1089
|
+
return;
|
|
1090
|
+
}
|
|
626
1091
|
if (modes.length > 1) {
|
|
627
|
-
const failed = await runTestMatrix(
|
|
1092
|
+
const failed = await runTestMatrix(effectiveRunFlags, configPath, selectors, modes, buildFeatureToggles, modeSummaryTotal, fileSummaryTotal, fuzzEnabled, fuzzOverrides);
|
|
628
1093
|
process.exit(failed ? 1 : 0);
|
|
629
1094
|
return;
|
|
630
1095
|
}
|
|
631
1096
|
let failed = false;
|
|
632
1097
|
for (const modeName of modes) {
|
|
633
|
-
const
|
|
634
|
-
|
|
1098
|
+
const reporterSession = await createRunReporter(configPath, undefined, modeName);
|
|
1099
|
+
const modeResult = await runTestSequential(effectiveRunFlags, configPath, selectors, buildFeatureToggles, modeSummaryTotal, fileSummaryTotal, fuzzEnabled, modeName, reporterSession.reporter, !fuzzEnabled);
|
|
1100
|
+
if (modeResult.failed)
|
|
635
1101
|
failed = true;
|
|
1102
|
+
if (fuzzEnabled) {
|
|
1103
|
+
if (reporterSession.reporterKind == "default") {
|
|
1104
|
+
process.stdout.write("\n");
|
|
1105
|
+
}
|
|
1106
|
+
const fuzzResults = await runFuzzMatrixResults(configPath, selectors, [modeName], fuzzOverrides, reporterSession.reporter);
|
|
1107
|
+
if (fuzzResults.some(hasFuzzFailures))
|
|
1108
|
+
failed = true;
|
|
1109
|
+
reporterSession.reporter.onRunComplete?.({
|
|
1110
|
+
clean: runFlags.clean,
|
|
1111
|
+
snapshotEnabled: effectiveRunFlags.snapshot !== false,
|
|
1112
|
+
showCoverage: effectiveRunFlags.showCoverage,
|
|
1113
|
+
buildTime: modeResult.summary.buildTime +
|
|
1114
|
+
getMergedIntervalDuration(collectFuzzBuildIntervals(fuzzResults)),
|
|
1115
|
+
snapshotSummary: modeResult.summary.snapshotSummary,
|
|
1116
|
+
coverageSummary: modeResult.summary.coverageSummary,
|
|
1117
|
+
stats: modeResult.summary.stats,
|
|
1118
|
+
reports: modeResult.summary.reports,
|
|
1119
|
+
fuzzSummary: summarizeFuzzExecutions(fuzzResults),
|
|
1120
|
+
modeSummary: buildSingleModeSummary(modeResult.summary.stats, modeResult.summary.snapshotSummary, modeSummaryTotal),
|
|
1121
|
+
});
|
|
1122
|
+
reporterSession.reporter.flush?.();
|
|
1123
|
+
}
|
|
636
1124
|
}
|
|
637
1125
|
process.exit(failed ? 1 : 0);
|
|
638
1126
|
}
|
|
639
|
-
async function runTestMatrix(runFlags, configPath, selectors, modes, buildFeatureToggles, modeSummaryTotal, fileSummaryTotal) {
|
|
1127
|
+
async function runTestMatrix(runFlags, configPath, selectors, modes, buildFeatureToggles, modeSummaryTotal, fileSummaryTotal, fuzzEnabled, fuzzOverrides) {
|
|
640
1128
|
const files = await resolveSelectedFiles(configPath, selectors);
|
|
641
1129
|
if (!files.length) {
|
|
642
|
-
|
|
1130
|
+
if (!fuzzEnabled) {
|
|
1131
|
+
throw await buildNoTestFilesMatchedError(configPath, selectors);
|
|
1132
|
+
}
|
|
1133
|
+
const fuzzFiles = await resolveSelectedFuzzFiles(configPath, selectors);
|
|
1134
|
+
if (!fuzzFiles.length) {
|
|
1135
|
+
throw await buildNoTestFilesMatchedError(configPath, selectors, true);
|
|
1136
|
+
}
|
|
643
1137
|
}
|
|
644
1138
|
const reporterSession = await createRunReporter(configPath);
|
|
645
1139
|
const reporter = reporterSession.reporter;
|
|
@@ -649,7 +1143,7 @@ async function runTestMatrix(runFlags, configPath, selectors, modes, buildFeatur
|
|
|
649
1143
|
clean: runFlags.clean,
|
|
650
1144
|
verbose: runFlags.verbose,
|
|
651
1145
|
snapshotEnabled,
|
|
652
|
-
|
|
1146
|
+
createSnapshots: runFlags.createSnapshots,
|
|
653
1147
|
});
|
|
654
1148
|
const silentReporter = {};
|
|
655
1149
|
const allResults = [];
|
|
@@ -665,6 +1159,7 @@ async function runTestMatrix(runFlags, configPath, selectors, modes, buildFeatur
|
|
|
665
1159
|
passed: false,
|
|
666
1160
|
}));
|
|
667
1161
|
const duplicateSpecBasenames = resolveDuplicateSpecBasenames(files);
|
|
1162
|
+
const buildIntervals = [];
|
|
668
1163
|
for (let fileIndex = 0; fileIndex < files.length; fileIndex++) {
|
|
669
1164
|
const file = files[fileIndex];
|
|
670
1165
|
const fileName = path.basename(file);
|
|
@@ -676,7 +1171,10 @@ async function runTestMatrix(runFlags, configPath, selectors, modes, buildFeatur
|
|
|
676
1171
|
for (let i = 0; i < modes.length; i++) {
|
|
677
1172
|
const modeName = modes[i];
|
|
678
1173
|
try {
|
|
1174
|
+
const buildStartedAt = Date.now();
|
|
679
1175
|
await build(configPath, [file], modeName, buildFeatureToggles);
|
|
1176
|
+
buildIntervals.push({ start: buildStartedAt, end: Date.now() });
|
|
1177
|
+
const buildInvocation = await getBuildInvocationPreview(configPath, file, modeName, buildFeatureToggles);
|
|
680
1178
|
const artifactKey = resolvePerFileArtifactKey(file, duplicateSpecBasenames);
|
|
681
1179
|
const result = await run(runFlags, configPath, [file], false, {
|
|
682
1180
|
reporter: silentReporter,
|
|
@@ -685,6 +1183,7 @@ async function runTestMatrix(runFlags, configPath, selectors, modes, buildFeatur
|
|
|
685
1183
|
emitRunComplete: false,
|
|
686
1184
|
logFileName: `test.${artifactKey}.log.json`,
|
|
687
1185
|
coverageFileName: `coverage.${artifactKey}.log.json`,
|
|
1186
|
+
buildCommand: formatBuildInvocation(buildInvocation),
|
|
688
1187
|
modeName,
|
|
689
1188
|
});
|
|
690
1189
|
modeTimes[i] = formatMatrixModeTime(result.stats.time);
|
|
@@ -716,78 +1215,610 @@ async function runTestMatrix(runFlags, configPath, selectors, modes, buildFeatur
|
|
|
716
1215
|
}
|
|
717
1216
|
const summary = aggregateRunResults(allResults);
|
|
718
1217
|
summary.stats = applyMatrixFileSummaryToStats(summary.stats, fileState, fileSummaryTotal);
|
|
1218
|
+
let failed = allResults.some((result) => result.failed);
|
|
1219
|
+
let fuzzSummary;
|
|
1220
|
+
if (fuzzEnabled) {
|
|
1221
|
+
if (reporterSession.reporterKind == "default") {
|
|
1222
|
+
process.stdout.write("\n");
|
|
1223
|
+
}
|
|
1224
|
+
const fuzzResults = await runFuzzMatrixResults(configPath, selectors, modes, fuzzOverrides, reporter);
|
|
1225
|
+
if (fuzzResults.some(hasFuzzFailures))
|
|
1226
|
+
failed = true;
|
|
1227
|
+
fuzzSummary = summarizeFuzzExecutions(fuzzResults);
|
|
1228
|
+
buildIntervals.push(...collectFuzzBuildIntervals(fuzzResults));
|
|
1229
|
+
}
|
|
719
1230
|
reporter.onRunComplete?.({
|
|
720
1231
|
clean: runFlags.clean,
|
|
721
1232
|
snapshotEnabled,
|
|
722
1233
|
showCoverage: runFlags.showCoverage,
|
|
1234
|
+
buildTime: getMergedIntervalDuration(buildIntervals),
|
|
723
1235
|
snapshotSummary: summary.snapshotSummary,
|
|
724
1236
|
coverageSummary: summary.coverageSummary,
|
|
725
1237
|
stats: summary.stats,
|
|
726
1238
|
reports: summary.reports,
|
|
1239
|
+
fuzzSummary,
|
|
727
1240
|
modeSummary: buildModeSummary(modeState, modeSummaryTotal),
|
|
728
1241
|
});
|
|
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);
|
|
758
|
-
}
|
|
759
|
-
function clearLiveLine() {
|
|
760
|
-
if (!canRewriteStdout())
|
|
761
|
-
return;
|
|
762
|
-
process.stdout.write("\r\x1b[2K");
|
|
1242
|
+
reporter.flush?.();
|
|
1243
|
+
return failed;
|
|
763
1244
|
}
|
|
764
|
-
function
|
|
765
|
-
|
|
1245
|
+
async function runFuzzModes(configPath, selectors, modes, rawArgs) {
|
|
1246
|
+
const overrides = resolveFuzzOverrides(rawArgs, "fuzz");
|
|
1247
|
+
const parallelSettings = resolveFuzzParallelJobs(rawArgs);
|
|
1248
|
+
const clean = rawArgs.includes("--clean");
|
|
1249
|
+
const fuzzFiles = await resolveSelectedFuzzFiles(configPath, selectors);
|
|
1250
|
+
const { jobs, buildJobs, runJobs } = resolveEffectiveParallelJobs(parallelSettings, fuzzFiles.length);
|
|
1251
|
+
if (jobs > 1) {
|
|
1252
|
+
const results = await runFuzzMatrixResultsParallel(configPath, selectors, modes, overrides, jobs, buildJobs, runJobs, clean);
|
|
1253
|
+
const reporterSession = await createRunReporter(configPath);
|
|
1254
|
+
reporterSession.reporter.onFuzzComplete?.(buildFuzzCompleteEvent(results, modes));
|
|
1255
|
+
reporterSession.reporter.flush?.();
|
|
1256
|
+
process.exit(results.some(hasFuzzFailures) ? 1 : 0);
|
|
766
1257
|
return;
|
|
767
|
-
const timingText = showPerModeTimes ? modeTimes.join(",") : "...";
|
|
768
|
-
const suffix = showPerModeTimes
|
|
769
|
-
? ` ${chalk.dim(`(${modes.join(",")})`)}`
|
|
770
|
-
: "";
|
|
771
|
-
const line = `${chalk.bgBlackBright.white(" .... ")} ${file} ${chalk.dim(timingText)}${suffix}`;
|
|
772
|
-
process.stdout.write(`\r\x1b[2K${line}`);
|
|
773
|
-
}
|
|
774
|
-
function formatMatrixModeTime(ms) {
|
|
775
|
-
const safeMs = Number.isFinite(ms) ? Math.max(0, ms) : 0;
|
|
776
|
-
return `${safeMs.toFixed(1)}ms`;
|
|
777
|
-
}
|
|
778
|
-
function formatMatrixAverageTime(results) {
|
|
779
|
-
if (!results.length)
|
|
780
|
-
return "0.0ms";
|
|
781
|
-
let total = 0;
|
|
782
|
-
for (const result of results) {
|
|
783
|
-
total += Number.isFinite(result.stats.time)
|
|
784
|
-
? Math.max(0, result.stats.time)
|
|
785
|
-
: 0;
|
|
786
1258
|
}
|
|
787
|
-
|
|
1259
|
+
const reporterSession = await createRunReporter(configPath);
|
|
1260
|
+
const results = await runFuzzMatrixResults(configPath, selectors, modes, overrides, reporterSession.reporter);
|
|
1261
|
+
reporterSession.reporter.onFuzzComplete?.(buildFuzzCompleteEvent(results, modes));
|
|
1262
|
+
reporterSession.reporter.flush?.();
|
|
1263
|
+
process.exit(results.some(hasFuzzFailures) ? 1 : 0);
|
|
788
1264
|
}
|
|
789
|
-
function
|
|
790
|
-
const
|
|
1265
|
+
async function runRuntimeSingleParallel(runFlags, configPath, selectors, modeName, modeSummaryTotal, fileSummaryTotal) {
|
|
1266
|
+
const files = await resolveSelectedFiles(configPath, selectors);
|
|
1267
|
+
if (!files.length) {
|
|
1268
|
+
throw await buildNoTestFilesMatchedError(configPath, selectors);
|
|
1269
|
+
}
|
|
1270
|
+
const reporterSession = await createRunReporter(configPath, undefined, modeName);
|
|
1271
|
+
const reporter = reporterSession.reporter;
|
|
1272
|
+
const snapshotEnabled = runFlags.snapshot !== false;
|
|
1273
|
+
reporter.onRunStart?.({
|
|
1274
|
+
runtimeName: reporterSession.runtimeName,
|
|
1275
|
+
clean: runFlags.clean,
|
|
1276
|
+
verbose: runFlags.verbose,
|
|
1277
|
+
snapshotEnabled,
|
|
1278
|
+
createSnapshots: runFlags.createSnapshots,
|
|
1279
|
+
});
|
|
1280
|
+
const buildCommandsByFile = await previewBuildCommands(configPath, selectors, modeName, {});
|
|
1281
|
+
const results = new Array(files.length);
|
|
1282
|
+
const queueDisplay = new ParallelQueueDisplay(!runFlags.clean);
|
|
1283
|
+
const runLimit = createAsyncLimiter(runFlags.runJobs);
|
|
1284
|
+
const poolWidth = Math.max(runFlags.buildJobs, runFlags.runJobs);
|
|
1285
|
+
await runOrderedPool(files, poolWidth, async (file, index) => {
|
|
1286
|
+
const token = renderQueuedFileStart(queueDisplay, path.basename(file));
|
|
1287
|
+
const buffered = await createBufferedReporter(configPath, modeName);
|
|
1288
|
+
const result = await runLimit(() => run({ ...runFlags, clean: true }, configPath, [file], false, {
|
|
1289
|
+
reporter: buffered.reporter,
|
|
1290
|
+
reporterKind: buffered.reporterKind,
|
|
1291
|
+
modeName,
|
|
1292
|
+
emitRunComplete: false,
|
|
1293
|
+
fileSummaryTotal: 1,
|
|
1294
|
+
modeSummaryTotal,
|
|
1295
|
+
modeSummaryExecuted: 1,
|
|
1296
|
+
buildCommandsByFile: { [file]: buildCommandsByFile[file] ?? "" },
|
|
1297
|
+
}));
|
|
1298
|
+
buffered.reporter.flush?.();
|
|
1299
|
+
results[index] = result;
|
|
1300
|
+
queueDisplay.complete(token, buffered.output());
|
|
1301
|
+
});
|
|
1302
|
+
queueDisplay.flush();
|
|
1303
|
+
const summary = aggregateRunResults(results);
|
|
1304
|
+
summary.stats = applyConfiguredFileTotalToStats(summary.stats, fileSummaryTotal);
|
|
1305
|
+
reporter.onRunComplete?.({
|
|
1306
|
+
clean: runFlags.clean,
|
|
1307
|
+
snapshotEnabled,
|
|
1308
|
+
showCoverage: runFlags.showCoverage,
|
|
1309
|
+
buildTime: 0,
|
|
1310
|
+
snapshotSummary: summary.snapshotSummary,
|
|
1311
|
+
coverageSummary: summary.coverageSummary,
|
|
1312
|
+
stats: summary.stats,
|
|
1313
|
+
reports: summary.reports,
|
|
1314
|
+
modeSummary: buildSingleModeSummary(summary.stats, summary.snapshotSummary, modeSummaryTotal),
|
|
1315
|
+
});
|
|
1316
|
+
reporter.flush?.();
|
|
1317
|
+
return results.some((result) => result.failed);
|
|
1318
|
+
}
|
|
1319
|
+
async function runRuntimeMatrixParallel(runFlags, configPath, selectors, modes, modeSummaryTotal, fileSummaryTotal) {
|
|
1320
|
+
const files = await resolveSelectedFiles(configPath, selectors);
|
|
1321
|
+
if (!files.length) {
|
|
1322
|
+
throw await buildNoTestFilesMatchedError(configPath, selectors);
|
|
1323
|
+
}
|
|
1324
|
+
const reporterSession = await createRunReporter(configPath);
|
|
1325
|
+
const reporter = reporterSession.reporter;
|
|
1326
|
+
const snapshotEnabled = runFlags.snapshot !== false;
|
|
1327
|
+
reporter.onRunStart?.({
|
|
1328
|
+
runtimeName: reporterSession.runtimeName,
|
|
1329
|
+
clean: runFlags.clean,
|
|
1330
|
+
verbose: runFlags.verbose,
|
|
1331
|
+
snapshotEnabled,
|
|
1332
|
+
createSnapshots: runFlags.createSnapshots,
|
|
1333
|
+
});
|
|
1334
|
+
const silentReporter = {};
|
|
1335
|
+
const modeLabels = modes.map((modeName) => modeName ?? "default");
|
|
1336
|
+
const showPerModeTimes = Boolean(runFlags.verbose);
|
|
1337
|
+
const duplicateSpecBasenames = resolveDuplicateSpecBasenames(files);
|
|
1338
|
+
const ordered = new Array(files.length);
|
|
1339
|
+
const queueDisplay = new ParallelQueueDisplay(!runFlags.clean);
|
|
1340
|
+
const poolWidth = Math.max(runFlags.jobs, runFlags.buildJobs, runFlags.runJobs);
|
|
1341
|
+
const buildPool = new BuildWorkerPool(runFlags.buildJobs);
|
|
1342
|
+
const buildIntervals = [];
|
|
1343
|
+
try {
|
|
1344
|
+
await runOrderedPool(files, poolWidth, async (file, fileIndex) => {
|
|
1345
|
+
const fileName = path.basename(file);
|
|
1346
|
+
const token = renderQueuedFileStart(queueDisplay, fileName);
|
|
1347
|
+
const fileResults = [];
|
|
1348
|
+
const modeTimes = modes.map(() => "...");
|
|
1349
|
+
for (let i = 0; i < modes.length; i++) {
|
|
1350
|
+
const modeName = modes[i];
|
|
1351
|
+
const buildStartedAt = Date.now();
|
|
1352
|
+
await buildPool.buildFileMode({
|
|
1353
|
+
configPath,
|
|
1354
|
+
file,
|
|
1355
|
+
modeName,
|
|
1356
|
+
});
|
|
1357
|
+
buildIntervals.push({ start: buildStartedAt, end: Date.now() });
|
|
1358
|
+
const buildInvocation = await getBuildInvocationPreview(configPath, file, modeName, {});
|
|
1359
|
+
const artifactKey = resolvePerFileArtifactKey(file, duplicateSpecBasenames);
|
|
1360
|
+
const result = await run(runFlags, configPath, [file], false, {
|
|
1361
|
+
reporter: silentReporter,
|
|
1362
|
+
reporterKind: "default",
|
|
1363
|
+
emitRunStart: false,
|
|
1364
|
+
emitRunComplete: false,
|
|
1365
|
+
logFileName: `run.${artifactKey}.log.json`,
|
|
1366
|
+
coverageFileName: `coverage.${artifactKey}.log.json`,
|
|
1367
|
+
buildCommand: formatBuildInvocation(buildInvocation),
|
|
1368
|
+
modeName,
|
|
1369
|
+
});
|
|
1370
|
+
modeTimes[i] = formatMatrixModeTime(result.stats.time);
|
|
1371
|
+
fileResults.push(result);
|
|
1372
|
+
}
|
|
1373
|
+
ordered[fileIndex] = { fileName, fileResults, modeTimes };
|
|
1374
|
+
queueDisplay.complete(token, formatMatrixFileResultLine(fileName, modeLabels, fileResults, modeTimes, showPerModeTimes) + "\n");
|
|
1375
|
+
});
|
|
1376
|
+
}
|
|
1377
|
+
finally {
|
|
1378
|
+
await buildPool.close();
|
|
1379
|
+
}
|
|
1380
|
+
queueDisplay.flush();
|
|
1381
|
+
const allResults = [];
|
|
1382
|
+
const modeState = modes.map(() => ({ failed: false, passed: false }));
|
|
1383
|
+
const fileState = files.map(() => ({ failed: false, passed: false }));
|
|
1384
|
+
for (let fileIndex = 0; fileIndex < ordered.length; fileIndex++) {
|
|
1385
|
+
const fileResults = ordered[fileIndex].fileResults;
|
|
1386
|
+
for (let i = 0; i < fileResults.length; i++) {
|
|
1387
|
+
const result = fileResults[i];
|
|
1388
|
+
allResults.push(result);
|
|
1389
|
+
if (result.failed)
|
|
1390
|
+
modeState[i].failed = true;
|
|
1391
|
+
else if (result.stats.passedFiles > 0)
|
|
1392
|
+
modeState[i].passed = true;
|
|
1393
|
+
}
|
|
1394
|
+
const verdict = resolveMatrixVerdict(fileResults);
|
|
1395
|
+
if (verdict == "fail")
|
|
1396
|
+
fileState[fileIndex].failed = true;
|
|
1397
|
+
else if (verdict == "ok")
|
|
1398
|
+
fileState[fileIndex].passed = true;
|
|
1399
|
+
}
|
|
1400
|
+
const summary = aggregateRunResults(allResults);
|
|
1401
|
+
summary.stats = applyMatrixFileSummaryToStats(summary.stats, fileState, fileSummaryTotal);
|
|
1402
|
+
reporter.onRunComplete?.({
|
|
1403
|
+
clean: runFlags.clean,
|
|
1404
|
+
snapshotEnabled,
|
|
1405
|
+
showCoverage: runFlags.showCoverage,
|
|
1406
|
+
buildTime: getMergedIntervalDuration(buildIntervals),
|
|
1407
|
+
snapshotSummary: summary.snapshotSummary,
|
|
1408
|
+
coverageSummary: summary.coverageSummary,
|
|
1409
|
+
stats: summary.stats,
|
|
1410
|
+
reports: summary.reports,
|
|
1411
|
+
modeSummary: buildModeSummary(modeState, modeSummaryTotal),
|
|
1412
|
+
});
|
|
1413
|
+
reporter.flush?.();
|
|
1414
|
+
return allResults.some((result) => result.failed);
|
|
1415
|
+
}
|
|
1416
|
+
async function runTestSingleParallel(runFlags, configPath, selectors, buildFeatureToggles, modeSummaryTotal, fileSummaryTotal, fuzzEnabled, fuzzOverrides, modeName) {
|
|
1417
|
+
const files = await resolveSelectedFiles(configPath, selectors);
|
|
1418
|
+
if (!files.length && !fuzzEnabled) {
|
|
1419
|
+
throw await buildNoTestFilesMatchedError(configPath, selectors);
|
|
1420
|
+
}
|
|
1421
|
+
const reporterSession = await createRunReporter(configPath, undefined, modeName);
|
|
1422
|
+
const reporter = reporterSession.reporter;
|
|
1423
|
+
const snapshotEnabled = runFlags.snapshot !== false;
|
|
1424
|
+
reporter.onRunStart?.({
|
|
1425
|
+
runtimeName: reporterSession.runtimeName,
|
|
1426
|
+
clean: runFlags.clean,
|
|
1427
|
+
verbose: runFlags.verbose,
|
|
1428
|
+
snapshotEnabled,
|
|
1429
|
+
createSnapshots: runFlags.createSnapshots,
|
|
1430
|
+
});
|
|
1431
|
+
const duplicateSpecBasenames = resolveDuplicateSpecBasenames(files);
|
|
1432
|
+
const results = new Array(files.length);
|
|
1433
|
+
const queueDisplay = new ParallelQueueDisplay(!runFlags.clean);
|
|
1434
|
+
const poolWidth = Math.max(runFlags.jobs, runFlags.buildJobs, runFlags.runJobs);
|
|
1435
|
+
const buildIntervals = [];
|
|
1436
|
+
if (files.length) {
|
|
1437
|
+
const buildPool = new BuildWorkerPool(runFlags.buildJobs);
|
|
1438
|
+
try {
|
|
1439
|
+
await runOrderedPool(files, poolWidth, async (file, index) => {
|
|
1440
|
+
const token = renderQueuedFileStart(queueDisplay, path.basename(file));
|
|
1441
|
+
const buildStartedAt = Date.now();
|
|
1442
|
+
await buildPool.buildFileMode({
|
|
1443
|
+
configPath,
|
|
1444
|
+
file,
|
|
1445
|
+
modeName,
|
|
1446
|
+
featureToggles: buildFeatureToggles,
|
|
1447
|
+
});
|
|
1448
|
+
buildIntervals.push({ start: buildStartedAt, end: Date.now() });
|
|
1449
|
+
const buildInvocation = await getBuildInvocationPreview(configPath, file, modeName, buildFeatureToggles);
|
|
1450
|
+
const artifactKey = resolvePerFileArtifactKey(file, duplicateSpecBasenames);
|
|
1451
|
+
const buffered = await createBufferedReporter(configPath, modeName);
|
|
1452
|
+
const result = await run({ ...runFlags, clean: true }, configPath, [file], false, {
|
|
1453
|
+
reporter: buffered.reporter,
|
|
1454
|
+
reporterKind: buffered.reporterKind,
|
|
1455
|
+
emitRunComplete: false,
|
|
1456
|
+
logFileName: `test.${artifactKey}.log.json`,
|
|
1457
|
+
coverageFileName: `coverage.${artifactKey}.log.json`,
|
|
1458
|
+
buildCommand: formatBuildInvocation(buildInvocation),
|
|
1459
|
+
modeName,
|
|
1460
|
+
});
|
|
1461
|
+
buffered.reporter.flush?.();
|
|
1462
|
+
results[index] = result;
|
|
1463
|
+
queueDisplay.complete(token, buffered.output());
|
|
1464
|
+
});
|
|
1465
|
+
}
|
|
1466
|
+
finally {
|
|
1467
|
+
await buildPool.close();
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
queueDisplay.flush();
|
|
1471
|
+
const runResults = results.filter(Boolean);
|
|
1472
|
+
const summary = aggregateRunResults(runResults);
|
|
1473
|
+
summary.stats = applyConfiguredFileTotalToStats(summary.stats, fileSummaryTotal);
|
|
1474
|
+
let failed = runResults.some((result) => result.failed);
|
|
1475
|
+
let fuzzSummary;
|
|
1476
|
+
if (fuzzEnabled) {
|
|
1477
|
+
if (reporterSession.reporterKind == "default") {
|
|
1478
|
+
process.stdout.write("\n");
|
|
1479
|
+
}
|
|
1480
|
+
const fuzzResults = await runFuzzMatrixResultsParallel(configPath, selectors, [modeName], fuzzOverrides, runFlags.jobs, runFlags.buildJobs, runFlags.runJobs, runFlags.clean);
|
|
1481
|
+
if (fuzzResults.some(hasFuzzFailures))
|
|
1482
|
+
failed = true;
|
|
1483
|
+
fuzzSummary = summarizeFuzzExecutions(fuzzResults);
|
|
1484
|
+
buildIntervals.push(...collectFuzzBuildIntervals(fuzzResults));
|
|
1485
|
+
}
|
|
1486
|
+
reporter.onRunComplete?.({
|
|
1487
|
+
clean: runFlags.clean,
|
|
1488
|
+
snapshotEnabled,
|
|
1489
|
+
showCoverage: runFlags.showCoverage,
|
|
1490
|
+
buildTime: getMergedIntervalDuration(buildIntervals),
|
|
1491
|
+
snapshotSummary: summary.snapshotSummary,
|
|
1492
|
+
coverageSummary: summary.coverageSummary,
|
|
1493
|
+
stats: summary.stats,
|
|
1494
|
+
reports: summary.reports,
|
|
1495
|
+
fuzzSummary,
|
|
1496
|
+
modeSummary: buildSingleModeSummary(summary.stats, summary.snapshotSummary, modeSummaryTotal),
|
|
1497
|
+
});
|
|
1498
|
+
reporter.flush?.();
|
|
1499
|
+
return failed;
|
|
1500
|
+
}
|
|
1501
|
+
async function runTestMatrixParallel(runFlags, configPath, selectors, modes, buildFeatureToggles, modeSummaryTotal, fileSummaryTotal, fuzzEnabled, fuzzOverrides) {
|
|
1502
|
+
const files = await resolveSelectedFiles(configPath, selectors);
|
|
1503
|
+
if (!files.length) {
|
|
1504
|
+
if (!fuzzEnabled) {
|
|
1505
|
+
throw await buildNoTestFilesMatchedError(configPath, selectors);
|
|
1506
|
+
}
|
|
1507
|
+
const fuzzFiles = await resolveSelectedFuzzFiles(configPath, selectors);
|
|
1508
|
+
if (!fuzzFiles.length) {
|
|
1509
|
+
throw await buildNoTestFilesMatchedError(configPath, selectors, true);
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
const reporterSession = await createRunReporter(configPath);
|
|
1513
|
+
const reporter = reporterSession.reporter;
|
|
1514
|
+
const snapshotEnabled = runFlags.snapshot !== false;
|
|
1515
|
+
reporter.onRunStart?.({
|
|
1516
|
+
runtimeName: reporterSession.runtimeName,
|
|
1517
|
+
clean: runFlags.clean,
|
|
1518
|
+
verbose: runFlags.verbose,
|
|
1519
|
+
snapshotEnabled,
|
|
1520
|
+
createSnapshots: runFlags.createSnapshots,
|
|
1521
|
+
});
|
|
1522
|
+
const silentReporter = {};
|
|
1523
|
+
const modeLabels = modes.map((modeName) => modeName ?? "default");
|
|
1524
|
+
const showPerModeTimes = Boolean(runFlags.verbose);
|
|
1525
|
+
const duplicateSpecBasenames = resolveDuplicateSpecBasenames(files);
|
|
1526
|
+
const ordered = new Array(files.length);
|
|
1527
|
+
const queueDisplay = new ParallelQueueDisplay(!runFlags.clean);
|
|
1528
|
+
const poolWidth = Math.max(runFlags.jobs, runFlags.buildJobs, runFlags.runJobs);
|
|
1529
|
+
const buildPool = new BuildWorkerPool(runFlags.buildJobs);
|
|
1530
|
+
const buildIntervals = [];
|
|
1531
|
+
try {
|
|
1532
|
+
await runOrderedPool(files, poolWidth, async (file, fileIndex) => {
|
|
1533
|
+
const fileName = path.basename(file);
|
|
1534
|
+
const token = renderQueuedFileStart(queueDisplay, fileName);
|
|
1535
|
+
const fileResults = [];
|
|
1536
|
+
const modeTimes = modes.map(() => "...");
|
|
1537
|
+
for (let i = 0; i < modes.length; i++) {
|
|
1538
|
+
const modeName = modes[i];
|
|
1539
|
+
const buildStartedAt = Date.now();
|
|
1540
|
+
await buildPool.buildFileMode({
|
|
1541
|
+
configPath,
|
|
1542
|
+
file,
|
|
1543
|
+
modeName,
|
|
1544
|
+
featureToggles: buildFeatureToggles,
|
|
1545
|
+
});
|
|
1546
|
+
buildIntervals.push({ start: buildStartedAt, end: Date.now() });
|
|
1547
|
+
const buildInvocation = await getBuildInvocationPreview(configPath, file, modeName, buildFeatureToggles);
|
|
1548
|
+
const artifactKey = resolvePerFileArtifactKey(file, duplicateSpecBasenames);
|
|
1549
|
+
const result = await run(runFlags, configPath, [file], false, {
|
|
1550
|
+
reporter: silentReporter,
|
|
1551
|
+
reporterKind: "default",
|
|
1552
|
+
emitRunStart: false,
|
|
1553
|
+
emitRunComplete: false,
|
|
1554
|
+
logFileName: `test.${artifactKey}.log.json`,
|
|
1555
|
+
coverageFileName: `coverage.${artifactKey}.log.json`,
|
|
1556
|
+
buildCommand: formatBuildInvocation(buildInvocation),
|
|
1557
|
+
modeName,
|
|
1558
|
+
});
|
|
1559
|
+
modeTimes[i] = formatMatrixModeTime(result.stats.time);
|
|
1560
|
+
fileResults.push(result);
|
|
1561
|
+
}
|
|
1562
|
+
ordered[fileIndex] = { fileName, fileResults, modeTimes };
|
|
1563
|
+
queueDisplay.complete(token, formatMatrixFileResultLine(fileName, modeLabels, fileResults, modeTimes, showPerModeTimes) + "\n");
|
|
1564
|
+
});
|
|
1565
|
+
}
|
|
1566
|
+
finally {
|
|
1567
|
+
await buildPool.close();
|
|
1568
|
+
}
|
|
1569
|
+
queueDisplay.flush();
|
|
1570
|
+
const allResults = [];
|
|
1571
|
+
const modeState = modes.map(() => ({ failed: false, passed: false }));
|
|
1572
|
+
const fileState = files.map(() => ({ failed: false, passed: false }));
|
|
1573
|
+
for (let fileIndex = 0; fileIndex < ordered.length; fileIndex++) {
|
|
1574
|
+
const entry = ordered[fileIndex];
|
|
1575
|
+
for (let i = 0; i < entry.fileResults.length; i++) {
|
|
1576
|
+
const result = entry.fileResults[i];
|
|
1577
|
+
allResults.push(result);
|
|
1578
|
+
if (result.failed)
|
|
1579
|
+
modeState[i].failed = true;
|
|
1580
|
+
else if (result.stats.passedFiles > 0)
|
|
1581
|
+
modeState[i].passed = true;
|
|
1582
|
+
}
|
|
1583
|
+
const verdict = resolveMatrixVerdict(entry.fileResults);
|
|
1584
|
+
if (verdict == "fail")
|
|
1585
|
+
fileState[fileIndex].failed = true;
|
|
1586
|
+
else if (verdict == "ok")
|
|
1587
|
+
fileState[fileIndex].passed = true;
|
|
1588
|
+
}
|
|
1589
|
+
const summary = aggregateRunResults(allResults);
|
|
1590
|
+
summary.stats = applyMatrixFileSummaryToStats(summary.stats, fileState, fileSummaryTotal);
|
|
1591
|
+
let failed = allResults.some((result) => result.failed);
|
|
1592
|
+
let fuzzSummary;
|
|
1593
|
+
if (fuzzEnabled) {
|
|
1594
|
+
if (reporterSession.reporterKind == "default") {
|
|
1595
|
+
process.stdout.write("\n");
|
|
1596
|
+
}
|
|
1597
|
+
const fuzzResults = await runFuzzMatrixResultsParallel(configPath, selectors, modes, fuzzOverrides, runFlags.jobs, runFlags.buildJobs, runFlags.runJobs, runFlags.clean);
|
|
1598
|
+
if (fuzzResults.some(hasFuzzFailures))
|
|
1599
|
+
failed = true;
|
|
1600
|
+
fuzzSummary = summarizeFuzzExecutions(fuzzResults);
|
|
1601
|
+
buildIntervals.push(...collectFuzzBuildIntervals(fuzzResults));
|
|
1602
|
+
}
|
|
1603
|
+
reporter.onRunComplete?.({
|
|
1604
|
+
clean: runFlags.clean,
|
|
1605
|
+
snapshotEnabled,
|
|
1606
|
+
showCoverage: runFlags.showCoverage,
|
|
1607
|
+
buildTime: getMergedIntervalDuration(buildIntervals),
|
|
1608
|
+
snapshotSummary: summary.snapshotSummary,
|
|
1609
|
+
coverageSummary: summary.coverageSummary,
|
|
1610
|
+
stats: summary.stats,
|
|
1611
|
+
reports: summary.reports,
|
|
1612
|
+
fuzzSummary,
|
|
1613
|
+
modeSummary: buildModeSummary(modeState, modeSummaryTotal),
|
|
1614
|
+
});
|
|
1615
|
+
reporter.flush?.();
|
|
1616
|
+
return failed;
|
|
1617
|
+
}
|
|
1618
|
+
async function runFuzzMatrixResultsParallel(configPath, selectors, modes, overrides, jobs, buildJobs, runJobs, clean) {
|
|
1619
|
+
const files = await resolveSelectedFuzzFiles(configPath, selectors);
|
|
1620
|
+
if (!files.length) {
|
|
1621
|
+
throw new Error(`No fuzz files matched: ${selectors.length ? selectors.join(", ") : "configured input patterns"}`);
|
|
1622
|
+
}
|
|
1623
|
+
const ordered = new Array(files.length);
|
|
1624
|
+
const queueDisplay = new ParallelQueueDisplay(!clean);
|
|
1625
|
+
const poolWidth = Math.max(jobs, buildJobs, runJobs);
|
|
1626
|
+
await runOrderedPool(files, poolWidth, async (file, index) => {
|
|
1627
|
+
const token = renderQueuedFileStart(queueDisplay, path.basename(file));
|
|
1628
|
+
const fileResults = [];
|
|
1629
|
+
for (const modeName of modes) {
|
|
1630
|
+
const modeResults = await fuzz(configPath, [file], modeName, overrides);
|
|
1631
|
+
fileResults.push(...modeResults);
|
|
1632
|
+
}
|
|
1633
|
+
ordered[index] = fileResults;
|
|
1634
|
+
const buffered = await createBufferedReporter(configPath);
|
|
1635
|
+
buffered.reporter.onFuzzFileComplete?.({ file, results: fileResults });
|
|
1636
|
+
buffered.reporter.flush?.();
|
|
1637
|
+
queueDisplay.complete(token, buffered.output());
|
|
1638
|
+
});
|
|
1639
|
+
queueDisplay.flush();
|
|
1640
|
+
return ordered.flat();
|
|
1641
|
+
}
|
|
1642
|
+
async function runFuzzMatrixResults(configPath, selectors, modes, overrides, reporter) {
|
|
1643
|
+
const files = await resolveSelectedFuzzFiles(configPath, selectors);
|
|
1644
|
+
if (!files.length) {
|
|
1645
|
+
throw new Error(`No fuzz files matched: ${selectors.length ? selectors.join(", ") : "configured input patterns"}`);
|
|
1646
|
+
}
|
|
1647
|
+
const results = [];
|
|
1648
|
+
for (const file of files) {
|
|
1649
|
+
const fileResults = [];
|
|
1650
|
+
for (const modeName of modes) {
|
|
1651
|
+
const modeResults = await fuzz(configPath, [file], modeName, overrides);
|
|
1652
|
+
fileResults.push(...modeResults);
|
|
1653
|
+
results.push(...modeResults);
|
|
1654
|
+
}
|
|
1655
|
+
reporter?.onFuzzFileComplete?.({ file, results: fileResults });
|
|
1656
|
+
}
|
|
1657
|
+
return results;
|
|
1658
|
+
}
|
|
1659
|
+
function hasFuzzFailures(result) {
|
|
1660
|
+
if (result.crashes > 0)
|
|
1661
|
+
return true;
|
|
1662
|
+
return result.fuzzers.some((fuzzer) => fuzzer.failed > 0);
|
|
1663
|
+
}
|
|
1664
|
+
function buildFuzzCompleteEvent(results, modes) {
|
|
1665
|
+
return {
|
|
1666
|
+
results,
|
|
1667
|
+
time: results.reduce((sum, item) => sum + item.time, 0),
|
|
1668
|
+
buildTime: getMergedIntervalDuration(collectFuzzBuildIntervals(results)),
|
|
1669
|
+
fuzzingSummary: summarizeFuzzExecutions(results),
|
|
1670
|
+
suiteSummary: summarizeFuzzSuites(results),
|
|
1671
|
+
modeSummary: summarizeFuzzModes(results, modes),
|
|
1672
|
+
};
|
|
1673
|
+
}
|
|
1674
|
+
function collectFuzzBuildIntervals(results) {
|
|
1675
|
+
return results.map((result) => ({
|
|
1676
|
+
start: result.buildStartedAt,
|
|
1677
|
+
end: result.buildFinishedAt,
|
|
1678
|
+
}));
|
|
1679
|
+
}
|
|
1680
|
+
function getMergedIntervalDuration(intervals) {
|
|
1681
|
+
if (!intervals.length)
|
|
1682
|
+
return 0;
|
|
1683
|
+
const sorted = intervals
|
|
1684
|
+
.map((interval) => ({
|
|
1685
|
+
start: Math.min(interval.start, interval.end),
|
|
1686
|
+
end: Math.max(interval.start, interval.end),
|
|
1687
|
+
}))
|
|
1688
|
+
.sort((a, b) => a.start - b.start);
|
|
1689
|
+
let total = 0;
|
|
1690
|
+
let currentStart = sorted[0].start;
|
|
1691
|
+
let currentEnd = sorted[0].end;
|
|
1692
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
1693
|
+
const interval = sorted[i];
|
|
1694
|
+
if (interval.start <= currentEnd) {
|
|
1695
|
+
currentEnd = Math.max(currentEnd, interval.end);
|
|
1696
|
+
continue;
|
|
1697
|
+
}
|
|
1698
|
+
total += currentEnd - currentStart;
|
|
1699
|
+
currentStart = interval.start;
|
|
1700
|
+
currentEnd = interval.end;
|
|
1701
|
+
}
|
|
1702
|
+
total += currentEnd - currentStart;
|
|
1703
|
+
return total;
|
|
1704
|
+
}
|
|
1705
|
+
function summarizeFuzzExecutions(results) {
|
|
1706
|
+
return {
|
|
1707
|
+
failed: results.reduce((sum, item) => sum +
|
|
1708
|
+
item.fuzzers.reduce((inner, fuzzer) => inner + fuzzer.failed + fuzzer.crashed, 0), 0),
|
|
1709
|
+
skipped: results.reduce((sum, item) => sum + item.fuzzers.reduce((inner, fuzzer) => inner + fuzzer.skipped, 0), 0),
|
|
1710
|
+
total: results.reduce((sum, item) => sum + item.fuzzers.reduce((inner, fuzzer) => inner + fuzzer.runs, 0), 0),
|
|
1711
|
+
};
|
|
1712
|
+
}
|
|
1713
|
+
function summarizeFuzzSuites(results) {
|
|
1714
|
+
return {
|
|
1715
|
+
failed: results.reduce((sum, item) => sum +
|
|
1716
|
+
item.fuzzers.filter((fuzzer) => fuzzer.failed > 0 || fuzzer.crashed > 0)
|
|
1717
|
+
.length, 0),
|
|
1718
|
+
skipped: results.reduce((sum, item) => sum + item.fuzzers.filter((fuzzer) => fuzzer.skipped > 0).length, 0),
|
|
1719
|
+
total: results.reduce((sum, item) => sum + item.fuzzers.length, 0),
|
|
1720
|
+
};
|
|
1721
|
+
}
|
|
1722
|
+
function summarizeFuzzModes(results, modes) {
|
|
1723
|
+
const total = Math.max(modes.length, 1);
|
|
1724
|
+
const state = new Map();
|
|
1725
|
+
for (const modeName of modes) {
|
|
1726
|
+
state.set(modeName ?? "default", { failed: false, passed: false });
|
|
1727
|
+
}
|
|
1728
|
+
for (const result of results) {
|
|
1729
|
+
const current = state.get(result.modeName) ?? {
|
|
1730
|
+
failed: false,
|
|
1731
|
+
passed: false,
|
|
1732
|
+
};
|
|
1733
|
+
if (hasFuzzFailures(result))
|
|
1734
|
+
current.failed = true;
|
|
1735
|
+
else if (!isSkippedFuzzResult(result))
|
|
1736
|
+
current.passed = true;
|
|
1737
|
+
state.set(result.modeName, current);
|
|
1738
|
+
}
|
|
1739
|
+
let failed = 0;
|
|
1740
|
+
let skipped = 0;
|
|
1741
|
+
for (const mode of state.values()) {
|
|
1742
|
+
if (mode.failed)
|
|
1743
|
+
failed++;
|
|
1744
|
+
else if (!mode.passed)
|
|
1745
|
+
skipped++;
|
|
1746
|
+
}
|
|
1747
|
+
return { failed, skipped, total };
|
|
1748
|
+
}
|
|
1749
|
+
function isSkippedFuzzResult(result) {
|
|
1750
|
+
return (result.crashes == 0 &&
|
|
1751
|
+
result.fuzzers.length > 0 &&
|
|
1752
|
+
result.fuzzers.every((fuzzer) => fuzzer.skipped > 0));
|
|
1753
|
+
}
|
|
1754
|
+
function renderMatrixFileResult(file, modes, results, modeTimes, liveMatrix, showPerModeTimes) {
|
|
1755
|
+
const line = formatMatrixFileResultLine(file, modes, results, modeTimes, showPerModeTimes);
|
|
1756
|
+
if (liveMatrix)
|
|
1757
|
+
clearLiveLine();
|
|
1758
|
+
process.stdout.write(line + "\n");
|
|
1759
|
+
}
|
|
1760
|
+
function formatMatrixFileResultLine(file, modes, results, modeTimes, showPerModeTimes) {
|
|
1761
|
+
const verdict = resolveMatrixVerdict(results);
|
|
1762
|
+
const badge = verdict == "fail"
|
|
1763
|
+
? chalk.bgRed.white(" FAIL ")
|
|
1764
|
+
: verdict == "ok"
|
|
1765
|
+
? chalk.bgGreenBright.black(" PASS ")
|
|
1766
|
+
: chalk.bgBlackBright.white(" SKIP ");
|
|
1767
|
+
const avg = formatMatrixAverageTime(results);
|
|
1768
|
+
const timingText = showPerModeTimes ? modeTimes.join(",") : avg;
|
|
1769
|
+
const failedModes = results
|
|
1770
|
+
.map((result, index) => (result.failed ? modes[index] : null))
|
|
1771
|
+
.filter((mode) => Boolean(mode));
|
|
1772
|
+
const suffix = showPerModeTimes
|
|
1773
|
+
? ` ${chalk.dim(`(${modes.join(",")})`)}`
|
|
1774
|
+
: failedModes.length
|
|
1775
|
+
? ` ${chalk.dim(`(failed: ${failedModes.join(", ")})`)}`
|
|
1776
|
+
: "";
|
|
1777
|
+
return `${badge} ${file} ${chalk.dim(timingText)}${suffix}`;
|
|
1778
|
+
}
|
|
1779
|
+
function resolveMatrixVerdict(results) {
|
|
1780
|
+
if (results.some((result) => result.failed))
|
|
1781
|
+
return "fail";
|
|
1782
|
+
const hasPass = results.some((result) => result.stats.passedFiles > 0);
|
|
1783
|
+
if (hasPass)
|
|
1784
|
+
return "ok";
|
|
1785
|
+
return "skip";
|
|
1786
|
+
}
|
|
1787
|
+
function canRewriteStdout() {
|
|
1788
|
+
return Boolean(process.stdout.isTTY);
|
|
1789
|
+
}
|
|
1790
|
+
function clearLiveLine() {
|
|
1791
|
+
if (!canRewriteStdout())
|
|
1792
|
+
return;
|
|
1793
|
+
process.stdout.write("\r\x1b[2K");
|
|
1794
|
+
}
|
|
1795
|
+
function renderMatrixLiveLine(file, modes, modeTimes, showPerModeTimes) {
|
|
1796
|
+
if (!canRewriteStdout())
|
|
1797
|
+
return;
|
|
1798
|
+
const timingText = showPerModeTimes ? modeTimes.join(",") : "...";
|
|
1799
|
+
const suffix = showPerModeTimes
|
|
1800
|
+
? ` ${chalk.dim(`(${modes.join(",")})`)}`
|
|
1801
|
+
: "";
|
|
1802
|
+
const line = `${chalk.bgBlackBright.white(" .... ")} ${file} ${chalk.dim(timingText)}${suffix}`;
|
|
1803
|
+
process.stdout.write(`\r\x1b[2K${line}`);
|
|
1804
|
+
}
|
|
1805
|
+
function formatMatrixModeTime(ms) {
|
|
1806
|
+
const safeMs = Number.isFinite(ms) ? Math.max(0, ms) : 0;
|
|
1807
|
+
return `${safeMs.toFixed(1)}ms`;
|
|
1808
|
+
}
|
|
1809
|
+
function formatMatrixAverageTime(results) {
|
|
1810
|
+
if (!results.length)
|
|
1811
|
+
return "0.0ms";
|
|
1812
|
+
let total = 0;
|
|
1813
|
+
for (const result of results) {
|
|
1814
|
+
total += Number.isFinite(result.stats.time)
|
|
1815
|
+
? Math.max(0, result.stats.time)
|
|
1816
|
+
: 0;
|
|
1817
|
+
}
|
|
1818
|
+
return `${(total / results.length).toFixed(1)}ms`;
|
|
1819
|
+
}
|
|
1820
|
+
function buildModeSummary(modeState, totalModes) {
|
|
1821
|
+
const total = Math.max(totalModes, modeState.length, 1);
|
|
791
1822
|
let skipped = Math.max(0, total - modeState.length);
|
|
792
1823
|
let failed = 0;
|
|
793
1824
|
for (const mode of modeState) {
|
|
@@ -850,10 +1881,19 @@ function resolveConfiguredModeTotal(configPath) {
|
|
|
850
1881
|
const configuredModes = Object.keys(config.modes).length;
|
|
851
1882
|
return configuredModes || 1;
|
|
852
1883
|
}
|
|
853
|
-
async function resolveConfiguredFileTotal(configPath) {
|
|
854
|
-
const files = await resolveSelectedFiles(configPath,
|
|
1884
|
+
async function resolveConfiguredFileTotal(configPath, selectors = []) {
|
|
1885
|
+
const files = await resolveSelectedFiles(configPath, selectors);
|
|
855
1886
|
return files.length;
|
|
856
1887
|
}
|
|
1888
|
+
async function previewBuildCommands(configPath, selectors, modeName, featureToggles) {
|
|
1889
|
+
const files = await resolveSelectedFiles(configPath, selectors);
|
|
1890
|
+
const out = {};
|
|
1891
|
+
for (const file of files) {
|
|
1892
|
+
const invocation = await getBuildInvocationPreview(configPath, file, modeName, featureToggles);
|
|
1893
|
+
out[file] = formatBuildInvocation(invocation);
|
|
1894
|
+
}
|
|
1895
|
+
return out;
|
|
1896
|
+
}
|
|
857
1897
|
function resolveExecutionModes(configPath, selectedModes) {
|
|
858
1898
|
if (selectedModes.length)
|
|
859
1899
|
return selectedModes;
|
|
@@ -872,15 +1912,35 @@ async function resolveSelectedFiles(configPath, selectors, warn = true) {
|
|
|
872
1912
|
const specs = matches.filter((file) => file.endsWith(".spec.ts"));
|
|
873
1913
|
return [...new Set(specs)].sort((a, b) => a.localeCompare(b));
|
|
874
1914
|
}
|
|
875
|
-
async function
|
|
1915
|
+
async function resolveSelectedFuzzFiles(configPath, selectors) {
|
|
1916
|
+
const resolvedConfigPath = configPath ?? path.join(process.cwd(), "./as-test.config.json");
|
|
1917
|
+
const config = loadConfig(resolvedConfigPath, false);
|
|
1918
|
+
const patterns = resolveFuzzPatterns(config.fuzz.input, selectors);
|
|
1919
|
+
const matches = await glob(patterns);
|
|
1920
|
+
const fuzzFiles = matches.filter((file) => file.endsWith(".fuzz.ts"));
|
|
1921
|
+
return [...new Set(fuzzFiles)].sort((a, b) => a.localeCompare(b));
|
|
1922
|
+
}
|
|
1923
|
+
async function resolveSelectedTestInputs(configPath, selectors) {
|
|
1924
|
+
const [specs, fuzz] = await Promise.all([
|
|
1925
|
+
resolveSelectedFiles(configPath, selectors),
|
|
1926
|
+
resolveSelectedFuzzFiles(configPath, selectors),
|
|
1927
|
+
]);
|
|
1928
|
+
return { specs, fuzz };
|
|
1929
|
+
}
|
|
1930
|
+
async function buildNoTestFilesMatchedError(configPath, selectors, includeFuzz = false) {
|
|
876
1931
|
const scope = selectors.length > 0 ? selectors.join(", ") : "configured input patterns";
|
|
877
1932
|
const lines = [`No test files matched: ${scope}`];
|
|
878
1933
|
const configuredFiles = await resolveSelectedFiles(configPath, [], false);
|
|
1934
|
+
const configuredFuzzFiles = includeFuzz
|
|
1935
|
+
? await resolveSelectedFuzzFiles(configPath, [])
|
|
1936
|
+
: [];
|
|
879
1937
|
if (!selectors.length) {
|
|
880
1938
|
lines.push('No specs were discovered from configured input patterns. Check "input" in config or run "ast doctor".');
|
|
881
1939
|
return new Error(lines.join("\n"));
|
|
882
1940
|
}
|
|
883
|
-
const suggestions = suggestClosestSuites(selectors,
|
|
1941
|
+
const suggestions = suggestClosestSuites(selectors, includeFuzz
|
|
1942
|
+
? [...configuredFiles, ...configuredFuzzFiles]
|
|
1943
|
+
: configuredFiles);
|
|
884
1944
|
if (suggestions.length) {
|
|
885
1945
|
lines.push(`Closest suite names: ${suggestions.join(", ")}`);
|
|
886
1946
|
}
|
|
@@ -894,6 +1954,13 @@ async function buildNoTestFilesMatchedError(configPath, selectors) {
|
|
|
894
1954
|
else {
|
|
895
1955
|
lines.push('No specs were discovered from configured input patterns. Check "input" in config.');
|
|
896
1956
|
}
|
|
1957
|
+
if (includeFuzz && configuredFuzzFiles.length) {
|
|
1958
|
+
const sample = configuredFuzzFiles
|
|
1959
|
+
.slice(0, 5)
|
|
1960
|
+
.map((file) => path.basename(file))
|
|
1961
|
+
.join(", ");
|
|
1962
|
+
lines.push(`Configured fuzzers (${configuredFuzzFiles.length}): ${sample}${configuredFuzzFiles.length > 5 ? ", ..." : ""}`);
|
|
1963
|
+
}
|
|
897
1964
|
lines.push('Run "ast test --list" to inspect resolved files.');
|
|
898
1965
|
return new Error(lines.join("\n"));
|
|
899
1966
|
}
|
|
@@ -980,6 +2047,27 @@ function resolveInputPatterns(configured, selectors) {
|
|
|
980
2047
|
}
|
|
981
2048
|
return [...patterns];
|
|
982
2049
|
}
|
|
2050
|
+
function resolveFuzzPatterns(configured, selectors) {
|
|
2051
|
+
const configuredInputs = Array.isArray(configured)
|
|
2052
|
+
? configured
|
|
2053
|
+
: [configured];
|
|
2054
|
+
if (!selectors.length)
|
|
2055
|
+
return configuredInputs;
|
|
2056
|
+
const patterns = new Set();
|
|
2057
|
+
for (const selector of expandSelectors(selectors)) {
|
|
2058
|
+
if (!selector)
|
|
2059
|
+
continue;
|
|
2060
|
+
if (isBareSuiteSelector(selector)) {
|
|
2061
|
+
const base = selector.replace(/\.fuzz\.ts$/, "").replace(/\.ts$/, "");
|
|
2062
|
+
for (const configuredInput of configuredInputs) {
|
|
2063
|
+
patterns.add(path.join(path.dirname(configuredInput), `${base}.fuzz.ts`));
|
|
2064
|
+
}
|
|
2065
|
+
continue;
|
|
2066
|
+
}
|
|
2067
|
+
patterns.add(selector);
|
|
2068
|
+
}
|
|
2069
|
+
return [...patterns];
|
|
2070
|
+
}
|
|
983
2071
|
function expandSelectors(selectors) {
|
|
984
2072
|
const expanded = [];
|
|
985
2073
|
for (const selector of selectors) {
|
|
@@ -1064,7 +2152,184 @@ function resolveArtifactFileNameForPreview(file, target, modeName, duplicateSpec
|
|
|
1064
2152
|
const stem = ext.length ? legacy.slice(0, -ext.length) : legacy;
|
|
1065
2153
|
return `${stem}.${disambiguator}${ext}`;
|
|
1066
2154
|
}
|
|
1067
|
-
async function
|
|
2155
|
+
async function ensureWebBrowsersReady(configPath, modes, browserOverride) {
|
|
2156
|
+
const resolvedConfigPath = configPath ?? path.join(process.cwd(), "./as-test.config.json");
|
|
2157
|
+
const config = loadConfig(resolvedConfigPath, true);
|
|
2158
|
+
const missing = [];
|
|
2159
|
+
for (const modeName of modes) {
|
|
2160
|
+
const applied = applyMode(config, modeName);
|
|
2161
|
+
const active = applied.config;
|
|
2162
|
+
if (!usesWebBrowser(active))
|
|
2163
|
+
continue;
|
|
2164
|
+
const requestedBrowser = browserOverride?.trim() || active.runOptions.runtime.browser.trim();
|
|
2165
|
+
const resolved = resolveBrowserSelection(requestedBrowser);
|
|
2166
|
+
if (!resolved) {
|
|
2167
|
+
missing.push({ modeName, browser: requestedBrowser });
|
|
2168
|
+
continue;
|
|
2169
|
+
}
|
|
2170
|
+
active.runOptions.runtime.browser = resolved.browser;
|
|
2171
|
+
process.env.BROWSER = resolved.browser;
|
|
2172
|
+
}
|
|
2173
|
+
if (!missing.length)
|
|
2174
|
+
return;
|
|
2175
|
+
await handleMissingWebBrowsers(missing);
|
|
2176
|
+
}
|
|
2177
|
+
function resolveBrowserSelection(requested = "") {
|
|
2178
|
+
if (requested.trim().length) {
|
|
2179
|
+
return resolveNamedBrowser(requested);
|
|
2180
|
+
}
|
|
2181
|
+
const envBrowser = process.env.BROWSER?.trim() ?? "";
|
|
2182
|
+
if (envBrowser.length) {
|
|
2183
|
+
return resolveNamedBrowser(envBrowser);
|
|
2184
|
+
}
|
|
2185
|
+
const candidates = [
|
|
2186
|
+
"chromium",
|
|
2187
|
+
"chromium-browser",
|
|
2188
|
+
"google-chrome",
|
|
2189
|
+
"google-chrome-stable",
|
|
2190
|
+
"chrome",
|
|
2191
|
+
"msedge",
|
|
2192
|
+
"firefox",
|
|
2193
|
+
];
|
|
2194
|
+
for (const candidate of candidates) {
|
|
2195
|
+
if (hasExecutable(candidate)) {
|
|
2196
|
+
return { browser: candidate };
|
|
2197
|
+
}
|
|
2198
|
+
}
|
|
2199
|
+
const playwrightFallback = resolvePlaywrightBrowserExecutable("chromium") ??
|
|
2200
|
+
resolvePlaywrightBrowserExecutable("firefox");
|
|
2201
|
+
if (playwrightFallback) {
|
|
2202
|
+
return { browser: playwrightFallback };
|
|
2203
|
+
}
|
|
2204
|
+
return null;
|
|
2205
|
+
}
|
|
2206
|
+
function resolveNamedBrowser(browser) {
|
|
2207
|
+
const normalized = browser.trim().toLowerCase();
|
|
2208
|
+
if (!normalized.length)
|
|
2209
|
+
return null;
|
|
2210
|
+
if (browser.includes("/") ||
|
|
2211
|
+
browser.includes("\\") ||
|
|
2212
|
+
path.isAbsolute(browser)) {
|
|
2213
|
+
return hasExecutable(browser) ? { browser } : null;
|
|
2214
|
+
}
|
|
2215
|
+
const aliases = {
|
|
2216
|
+
chromium: ["chromium", "chromium-browser"],
|
|
2217
|
+
chrome: [
|
|
2218
|
+
"google-chrome",
|
|
2219
|
+
"google-chrome-stable",
|
|
2220
|
+
"chrome",
|
|
2221
|
+
"chromium",
|
|
2222
|
+
"chromium-browser",
|
|
2223
|
+
],
|
|
2224
|
+
firefox: ["firefox"],
|
|
2225
|
+
webkit: [],
|
|
2226
|
+
};
|
|
2227
|
+
const candidates = aliases[normalized] ?? [browser];
|
|
2228
|
+
for (const candidate of candidates) {
|
|
2229
|
+
if (hasExecutable(candidate)) {
|
|
2230
|
+
return { browser: candidate };
|
|
2231
|
+
}
|
|
2232
|
+
}
|
|
2233
|
+
const playwrightFallback = resolvePlaywrightBrowserExecutable(normalized);
|
|
2234
|
+
if (playwrightFallback) {
|
|
2235
|
+
return { browser: playwrightFallback };
|
|
2236
|
+
}
|
|
2237
|
+
return null;
|
|
2238
|
+
}
|
|
2239
|
+
function usesWebBrowser(config) {
|
|
2240
|
+
return (config.buildOptions.target == "web" ||
|
|
2241
|
+
config.runOptions.runtime.browser.length > 0 ||
|
|
2242
|
+
config.runOptions.runtime.cmd.includes("default.web.js"));
|
|
2243
|
+
}
|
|
2244
|
+
async function handleMissingWebBrowsers(missing) {
|
|
2245
|
+
const scope = missing
|
|
2246
|
+
.map((entry) => entry.browser?.length
|
|
2247
|
+
? `${entry.modeName ?? "default"} (${entry.browser})`
|
|
2248
|
+
: (entry.modeName ?? "default"))
|
|
2249
|
+
.join(", ");
|
|
2250
|
+
const details = "no web-capable browser was found in PATH, BROWSER, or Playwright cache";
|
|
2251
|
+
if (!canPromptForWebInstall()) {
|
|
2252
|
+
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".`);
|
|
2253
|
+
}
|
|
2254
|
+
process.stdout.write(chalk.bold.blue("◇ Browser Setup Needed") +
|
|
2255
|
+
"\n" +
|
|
2256
|
+
`│ ${details}\n` +
|
|
2257
|
+
"│\n");
|
|
2258
|
+
const choice = await promptLine("Install Chromium with Playwright now? [Y/n] ");
|
|
2259
|
+
const normalized = choice.trim().toLowerCase();
|
|
2260
|
+
if (normalized == "n" || normalized == "no") {
|
|
2261
|
+
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.');
|
|
2262
|
+
}
|
|
2263
|
+
if (normalized != "" && normalized != "y" && normalized != "yes") {
|
|
2264
|
+
throw new Error(`invalid answer "${choice}". Expected yes or no.`);
|
|
2265
|
+
}
|
|
2266
|
+
const selected = "chromium";
|
|
2267
|
+
process.stdout.write(chalk.dim(`installing ${selected} via Playwright...\n`));
|
|
2268
|
+
const install = spawnSync("npx", ["-y", "playwright", "install", selected], {
|
|
2269
|
+
stdio: "inherit",
|
|
2270
|
+
shell: false,
|
|
2271
|
+
});
|
|
2272
|
+
if (install.status !== 0) {
|
|
2273
|
+
throw new Error(`Playwright browser install failed for ${selected}`);
|
|
2274
|
+
}
|
|
2275
|
+
const browserPath = resolvePlaywrightBrowserExecutable(selected);
|
|
2276
|
+
if (!browserPath) {
|
|
2277
|
+
throw new Error(`Playwright installed ${selected}, but as-test could not locate the browser executable`);
|
|
2278
|
+
}
|
|
2279
|
+
process.env.BROWSER = browserPath;
|
|
2280
|
+
}
|
|
2281
|
+
function canPromptForWebInstall() {
|
|
2282
|
+
return Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
2283
|
+
}
|
|
2284
|
+
function promptLine(question) {
|
|
2285
|
+
return new Promise((resolve) => {
|
|
2286
|
+
const rl = createInterface({
|
|
2287
|
+
input: process.stdin,
|
|
2288
|
+
output: process.stdout,
|
|
2289
|
+
});
|
|
2290
|
+
rl.question(question, (answer) => {
|
|
2291
|
+
rl.close();
|
|
2292
|
+
resolve(answer);
|
|
2293
|
+
});
|
|
2294
|
+
});
|
|
2295
|
+
}
|
|
2296
|
+
function resolvePlaywrightBrowserExecutable(browser) {
|
|
2297
|
+
const cacheRoot = path.join(process.env.HOME ?? "", ".cache", "ms-playwright");
|
|
2298
|
+
if (!cacheRoot.length || !existsSync(cacheRoot))
|
|
2299
|
+
return null;
|
|
2300
|
+
const map = {
|
|
2301
|
+
chromium: ["chromium-*/chrome-linux64/chrome"],
|
|
2302
|
+
chrome: ["chromium-*/chrome-linux64/chrome"],
|
|
2303
|
+
firefox: ["firefox-*/firefox/firefox"],
|
|
2304
|
+
webkit: ["webkit-*/pw_run.sh"],
|
|
2305
|
+
};
|
|
2306
|
+
const patterns = map[browser] ?? [];
|
|
2307
|
+
for (const pattern of patterns) {
|
|
2308
|
+
const matches = glob.sync(path.join(cacheRoot, pattern)).sort();
|
|
2309
|
+
if (matches.length)
|
|
2310
|
+
return matches[matches.length - 1];
|
|
2311
|
+
}
|
|
2312
|
+
return null;
|
|
2313
|
+
}
|
|
2314
|
+
function hasExecutable(command) {
|
|
2315
|
+
if (!command.length)
|
|
2316
|
+
return false;
|
|
2317
|
+
if (command.includes("/") || command.includes("\\")) {
|
|
2318
|
+
return existsSync(command);
|
|
2319
|
+
}
|
|
2320
|
+
const pathValue = process.env.PATH ?? "";
|
|
2321
|
+
const suffixes = process.platform == "win32" ? ["", ".exe", ".cmd", ".bat"] : [""];
|
|
2322
|
+
for (const base of pathValue.split(path.delimiter)) {
|
|
2323
|
+
if (!base.length)
|
|
2324
|
+
continue;
|
|
2325
|
+
for (const suffix of suffixes) {
|
|
2326
|
+
if (existsSync(path.join(base, command + suffix)))
|
|
2327
|
+
return true;
|
|
2328
|
+
}
|
|
2329
|
+
}
|
|
2330
|
+
return false;
|
|
2331
|
+
}
|
|
2332
|
+
async function listExecutionPlan(command, configPath, selectors, modes, listFlags, fuzzEnabled = false) {
|
|
1068
2333
|
const resolvedConfigPath = configPath ?? path.join(process.cwd(), "./as-test.config.json");
|
|
1069
2334
|
const config = loadConfig(resolvedConfigPath, true);
|
|
1070
2335
|
const configuredModes = Object.keys(config.modes);
|
|
@@ -1093,36 +2358,86 @@ async function listExecutionPlan(command, configPath, selectors, modes, listFlag
|
|
|
1093
2358
|
}
|
|
1094
2359
|
if (!listFlags.list)
|
|
1095
2360
|
return;
|
|
1096
|
-
const
|
|
1097
|
-
|
|
2361
|
+
const specFiles = command == "fuzz" ? [] : await resolveSelectedFiles(configPath, selectors);
|
|
2362
|
+
const fuzzFiles = command == "fuzz"
|
|
2363
|
+
? await resolveSelectedFuzzFiles(configPath, selectors)
|
|
2364
|
+
: command == "test" && fuzzEnabled
|
|
2365
|
+
? await resolveSelectedFuzzFiles(configPath, selectors)
|
|
2366
|
+
: [];
|
|
2367
|
+
const files = command == "fuzz" ? fuzzFiles : specFiles;
|
|
2368
|
+
if (!specFiles.length && !fuzzFiles.length) {
|
|
1098
2369
|
const scope = selectors.length > 0 ? selectors.join(", ") : "configured input patterns";
|
|
1099
|
-
throw new Error(
|
|
2370
|
+
throw new Error(command == "fuzz"
|
|
2371
|
+
? `No fuzz files matched: ${scope}`
|
|
2372
|
+
: `No test files matched: ${scope}`);
|
|
1100
2373
|
}
|
|
1101
|
-
const duplicateSpecBasenames = resolveDuplicateSpecBasenames(
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
process.stdout.write(
|
|
2374
|
+
const duplicateSpecBasenames = resolveDuplicateSpecBasenames(specFiles);
|
|
2375
|
+
const duplicateFuzzBasenames = resolveDuplicateSpecBasenames(fuzzFiles);
|
|
2376
|
+
if (specFiles.length) {
|
|
2377
|
+
process.stdout.write(chalk.bold("Resolved files:\n"));
|
|
2378
|
+
for (const file of specFiles) {
|
|
2379
|
+
process.stdout.write(` - ${file}\n`);
|
|
2380
|
+
}
|
|
2381
|
+
process.stdout.write("\n");
|
|
2382
|
+
}
|
|
2383
|
+
if (fuzzFiles.length && command == "test") {
|
|
2384
|
+
process.stdout.write(chalk.bold("Resolved fuzz files:\n"));
|
|
2385
|
+
for (const file of fuzzFiles) {
|
|
2386
|
+
process.stdout.write(` - ${file}\n`);
|
|
2387
|
+
}
|
|
2388
|
+
process.stdout.write("\n");
|
|
2389
|
+
}
|
|
2390
|
+
if (command == "fuzz" && fuzzFiles.length) {
|
|
2391
|
+
process.stdout.write(chalk.bold("Resolved files:\n"));
|
|
2392
|
+
for (const file of fuzzFiles) {
|
|
2393
|
+
process.stdout.write(` - ${file}\n`);
|
|
2394
|
+
}
|
|
2395
|
+
process.stdout.write("\n");
|
|
1105
2396
|
}
|
|
1106
|
-
process.stdout.write("\n");
|
|
1107
2397
|
for (const modeName of modes) {
|
|
1108
2398
|
const applied = applyMode(config, modeName);
|
|
1109
2399
|
const active = applied.config;
|
|
1110
2400
|
const modeLabel = modeName ?? "default";
|
|
1111
2401
|
process.stdout.write(chalk.bold(`Mode: ${modeLabel}\n`));
|
|
1112
|
-
process.stdout.write(` target: ${active.buildOptions.target}\n`);
|
|
2402
|
+
process.stdout.write(` target: ${command == "fuzz" ? "bindings" : active.buildOptions.target}\n`);
|
|
1113
2403
|
process.stdout.write(` outDir: ${active.outDir}\n`);
|
|
1114
|
-
if (command
|
|
2404
|
+
if (command == "run" || command == "test") {
|
|
1115
2405
|
process.stdout.write(` runtime: ${active.runOptions.runtime.cmd}\n`);
|
|
2406
|
+
if (usesWebBrowser(active)) {
|
|
2407
|
+
process.stdout.write(` browser: ${active.runOptions.runtime.browser || "(auto)"}\n`);
|
|
2408
|
+
}
|
|
1116
2409
|
}
|
|
1117
|
-
const envOverrides =
|
|
1118
|
-
|
|
1119
|
-
|
|
2410
|
+
const envOverrides = {
|
|
2411
|
+
...config.env,
|
|
2412
|
+
...(modeName ? (config.modes[modeName]?.env ?? {}) : {}),
|
|
2413
|
+
...(command == "build"
|
|
2414
|
+
? active.buildOptions.env
|
|
2415
|
+
: command == "run" || command == "test"
|
|
2416
|
+
? active.runOptions.env
|
|
2417
|
+
: {}),
|
|
2418
|
+
};
|
|
1120
2419
|
const envKeys = Object.keys(envOverrides);
|
|
1121
2420
|
process.stdout.write(` env overrides: ${envKeys.length}${envKeys.length ? ` (${envKeys.join(", ")})` : ""}\n`);
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
const
|
|
1125
|
-
|
|
2421
|
+
if (specFiles.length) {
|
|
2422
|
+
process.stdout.write(" artifacts:\n");
|
|
2423
|
+
for (const file of specFiles) {
|
|
2424
|
+
const artifactName = resolveArtifactFileNameForPreview(file, active.buildOptions.target, modeName, duplicateSpecBasenames);
|
|
2425
|
+
process.stdout.write(` - ${path.join(active.outDir, artifactName)}\n`);
|
|
2426
|
+
}
|
|
2427
|
+
}
|
|
2428
|
+
if (fuzzFiles.length && command == "test") {
|
|
2429
|
+
process.stdout.write(" fuzz artifacts:\n");
|
|
2430
|
+
for (const file of fuzzFiles) {
|
|
2431
|
+
const artifactName = resolveArtifactFileNameForPreview(file, "bindings", modeName, duplicateFuzzBasenames);
|
|
2432
|
+
process.stdout.write(` - ${path.join(active.outDir, artifactName)}\n`);
|
|
2433
|
+
}
|
|
2434
|
+
}
|
|
2435
|
+
else if (command == "fuzz") {
|
|
2436
|
+
process.stdout.write(" artifacts:\n");
|
|
2437
|
+
for (const file of fuzzFiles) {
|
|
2438
|
+
const artifactName = resolveArtifactFileNameForPreview(file, "bindings", modeName, duplicateFuzzBasenames);
|
|
2439
|
+
process.stdout.write(` - ${path.join(active.outDir, artifactName)}\n`);
|
|
2440
|
+
}
|
|
1126
2441
|
}
|
|
1127
2442
|
process.stdout.write("\n");
|
|
1128
2443
|
}
|