as-test 0.5.2 → 0.5.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +54 -0
- package/README.md +98 -8
- package/as-test.config.schema.json +49 -0
- package/assembly/coverage.ts +20 -0
- package/assembly/src/expectation.ts +32 -9
- package/assembly/util/json.ts +2 -3
- package/bin/commands/build-core.js +268 -0
- package/bin/commands/build.js +16 -0
- package/bin/commands/doctor-core.js +335 -0
- package/bin/commands/doctor.js +5 -0
- package/bin/commands/init-core.js +991 -0
- package/bin/commands/init.js +6 -0
- package/bin/{run.js → commands/run-core.js} +147 -29
- package/bin/commands/run.js +20 -0
- package/bin/commands/test.js +23 -0
- package/bin/commands/types.js +1 -0
- package/bin/index.js +848 -59
- package/bin/reporters/default.js +26 -9
- package/bin/types.js +2 -0
- package/bin/util.js +594 -10
- package/package.json +42 -42
- package/transform/lib/coverage.js +130 -124
- package/transform/lib/index.js +58 -23
- package/bin/build.js +0 -136
- package/bin/init.js +0 -496
package/bin/index.js
CHANGED
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import chalk from "chalk";
|
|
3
|
-
import { build } from "./build.js";
|
|
4
|
-
import { createRunReporter, run } from "./run.js";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
3
|
+
import { build } from "./commands/build.js";
|
|
4
|
+
import { createRunReporter, run } from "./commands/run.js";
|
|
5
|
+
import { executeBuildCommand } from "./commands/build.js";
|
|
6
|
+
import { executeRunCommand } from "./commands/run.js";
|
|
7
|
+
import { executeTestCommand } from "./commands/test.js";
|
|
8
|
+
import { executeInitCommand } from "./commands/init.js";
|
|
9
|
+
import { executeDoctorCommand } from "./commands/doctor.js";
|
|
10
|
+
import { applyMode, getCliVersion, loadConfig, resolveModeNames, } from "./util.js";
|
|
7
11
|
import * as path from "path";
|
|
8
12
|
import { glob } from "glob";
|
|
9
13
|
const _args = process.argv.slice(2);
|
|
10
14
|
const flags = [];
|
|
11
15
|
const args = [];
|
|
12
|
-
const COMMANDS = ["run", "build", "test", "init"];
|
|
16
|
+
const COMMANDS = ["run", "build", "test", "init", "doctor"];
|
|
13
17
|
const version = getCliVersion();
|
|
14
18
|
const configPath = resolveConfigPath(_args);
|
|
15
19
|
const selectedModes = resolveModeNames(_args);
|
|
@@ -28,39 +32,69 @@ if (!args.length) {
|
|
|
28
32
|
}
|
|
29
33
|
}
|
|
30
34
|
else if (COMMANDS.includes(args[0])) {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
35
|
+
try {
|
|
36
|
+
const command = args.shift();
|
|
37
|
+
const normalizedCommand = command ?? "";
|
|
38
|
+
if (shouldShowCommandHelp(_args, normalizedCommand)) {
|
|
39
|
+
printCommandHelp(normalizedCommand);
|
|
40
|
+
}
|
|
41
|
+
else if (command === "build") {
|
|
42
|
+
executeBuildCommand(_args, configPath, selectedModes, {
|
|
43
|
+
resolveCommandArgs,
|
|
44
|
+
resolveListFlags,
|
|
45
|
+
resolveFeatureToggles,
|
|
46
|
+
resolveExecutionModes,
|
|
47
|
+
listExecutionPlan,
|
|
48
|
+
runBuildModes,
|
|
49
|
+
}).catch((error) => {
|
|
50
|
+
printCliError(error);
|
|
51
|
+
process.exit(1);
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
else if (command === "run") {
|
|
55
|
+
executeRunCommand(_args, flags, configPath, selectedModes, {
|
|
56
|
+
resolveCommandArgs,
|
|
57
|
+
resolveListFlags,
|
|
58
|
+
resolveFeatureToggles,
|
|
59
|
+
resolveExecutionModes,
|
|
60
|
+
listExecutionPlan,
|
|
61
|
+
runRuntimeModes,
|
|
62
|
+
}).catch((error) => {
|
|
63
|
+
printCliError(error);
|
|
64
|
+
process.exit(1);
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
else if (command === "test") {
|
|
68
|
+
executeTestCommand(_args, flags, configPath, selectedModes, {
|
|
69
|
+
resolveCommandArgs,
|
|
70
|
+
resolveListFlags,
|
|
71
|
+
resolveFeatureToggles,
|
|
72
|
+
resolveExecutionModes,
|
|
73
|
+
listExecutionPlan,
|
|
74
|
+
runTestModes,
|
|
75
|
+
}).catch((error) => {
|
|
76
|
+
printCliError(error);
|
|
77
|
+
process.exit(1);
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
else if (command === "init") {
|
|
81
|
+
executeInitCommand(_args, {
|
|
82
|
+
resolveCommandTokens,
|
|
83
|
+
}).catch((error) => {
|
|
84
|
+
printCliError(error);
|
|
85
|
+
process.exit(1);
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
else if (command === "doctor") {
|
|
89
|
+
executeDoctorCommand(configPath, selectedModes).catch((error) => {
|
|
90
|
+
printCliError(error);
|
|
91
|
+
process.exit(1);
|
|
92
|
+
});
|
|
93
|
+
}
|
|
57
94
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
printCliError(error);
|
|
62
|
-
process.exit(1);
|
|
63
|
-
});
|
|
95
|
+
catch (error) {
|
|
96
|
+
printCliError(error);
|
|
97
|
+
process.exit(1);
|
|
64
98
|
}
|
|
65
99
|
}
|
|
66
100
|
else {
|
|
@@ -111,6 +145,12 @@ function info() {
|
|
|
111
145
|
chalk.dim("<./dir>") +
|
|
112
146
|
" " +
|
|
113
147
|
"Initialize an empty testing template");
|
|
148
|
+
console.log(" " +
|
|
149
|
+
chalk.bold.magentaBright("doctor") +
|
|
150
|
+
" " +
|
|
151
|
+
chalk.dim("<--mode x>") +
|
|
152
|
+
" " +
|
|
153
|
+
"Validate environment/config/runtime setup");
|
|
114
154
|
console.log("");
|
|
115
155
|
console.log(chalk.bold("Flags:"));
|
|
116
156
|
console.log(" " +
|
|
@@ -137,6 +177,14 @@ function info() {
|
|
|
137
177
|
chalk.bold.blue("--show-coverage") +
|
|
138
178
|
" " +
|
|
139
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)");
|
|
140
188
|
console.log(" " +
|
|
141
189
|
chalk.bold.blue("--verbose") +
|
|
142
190
|
" " +
|
|
@@ -145,6 +193,15 @@ function info() {
|
|
|
145
193
|
chalk.bold.blue("--reporter <name|path>") +
|
|
146
194
|
" " +
|
|
147
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") +
|
|
202
|
+
" " +
|
|
203
|
+
"Preview configured and selected mode names");
|
|
204
|
+
console.log(" " + chalk.bold.blue("--help, -h") + " Show help");
|
|
148
205
|
console.log("");
|
|
149
206
|
console.log(chalk.dim("If your using this, consider dropping a star, it would help a lot!") + "\n");
|
|
150
207
|
console.log("View the repo: " +
|
|
@@ -154,6 +211,102 @@ function info() {
|
|
|
154
211
|
// chalk.blue("https://docs.jairus.dev/as-test"),
|
|
155
212
|
// );
|
|
156
213
|
}
|
|
214
|
+
function isHelpFlag(value) {
|
|
215
|
+
return value == "--help" || value == "-h";
|
|
216
|
+
}
|
|
217
|
+
function shouldShowCommandHelp(rawArgs, command) {
|
|
218
|
+
if (!command.length)
|
|
219
|
+
return false;
|
|
220
|
+
const commandIndex = rawArgs.indexOf(command);
|
|
221
|
+
if (commandIndex == -1)
|
|
222
|
+
return false;
|
|
223
|
+
for (let i = 0; i < rawArgs.length; i++) {
|
|
224
|
+
if (i == commandIndex)
|
|
225
|
+
continue;
|
|
226
|
+
if (!isHelpFlag(rawArgs[i]))
|
|
227
|
+
continue;
|
|
228
|
+
return true;
|
|
229
|
+
}
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
function printCommandHelp(command) {
|
|
233
|
+
if (command == "build") {
|
|
234
|
+
process.stdout.write(chalk.bold("Usage: ast build [selectors...] [flags]\n\n"));
|
|
235
|
+
process.stdout.write("Compile selected specs into wasm artifacts.\n\n");
|
|
236
|
+
process.stdout.write(chalk.bold("Flags:\n"));
|
|
237
|
+
process.stdout.write(" --config <path> Use a specific config file\n");
|
|
238
|
+
process.stdout.write(" --mode <name[,name...]> Run one or multiple named config modes\n");
|
|
239
|
+
process.stdout.write(" --enable <feature> Enable build feature (coverage|try-as)\n");
|
|
240
|
+
process.stdout.write(" --disable <feature> Disable build feature (coverage|try-as)\n");
|
|
241
|
+
process.stdout.write(" --list Preview resolved files/artifacts without building\n");
|
|
242
|
+
process.stdout.write(" --list-modes Preview configured and selected mode names\n");
|
|
243
|
+
process.stdout.write(" --help, -h Show this help\n");
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
if (command == "run") {
|
|
247
|
+
process.stdout.write(chalk.bold("Usage: ast run [selectors...] [flags]\n\n"));
|
|
248
|
+
process.stdout.write("Run compiled specs with the configured runtime.\n\n");
|
|
249
|
+
process.stdout.write(chalk.bold("Flags:\n"));
|
|
250
|
+
process.stdout.write(" --config <path> Use a specific config file\n");
|
|
251
|
+
process.stdout.write(" --mode <name[,name...]> Run one or multiple named config modes\n");
|
|
252
|
+
process.stdout.write(" --update-snapshots Create/update snapshot files on mismatch\n");
|
|
253
|
+
process.stdout.write(" --no-snapshot Disable snapshot assertions for this run\n");
|
|
254
|
+
process.stdout.write(" --show-coverage Print uncovered coverage point details\n");
|
|
255
|
+
process.stdout.write(" --enable <feature> Enable feature (coverage|try-as)\n");
|
|
256
|
+
process.stdout.write(" --disable <feature> Disable feature (coverage|try-as)\n");
|
|
257
|
+
process.stdout.write(" --reporter <name|path> Use built-in reporter (default|tap) or custom module path\n");
|
|
258
|
+
process.stdout.write(" --tap Shortcut for --reporter tap\n");
|
|
259
|
+
process.stdout.write(" --verbose Keep expanded suite/test lines and live updates\n");
|
|
260
|
+
process.stdout.write(" --clean Disable in-place TTY updates; print final lines only\n");
|
|
261
|
+
process.stdout.write(" --list Preview resolved files/artifacts/runtime without running\n");
|
|
262
|
+
process.stdout.write(" --list-modes Preview configured and selected mode names\n");
|
|
263
|
+
process.stdout.write(" --help, -h Show this help\n");
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
if (command == "test") {
|
|
267
|
+
process.stdout.write(chalk.bold("Usage: ast test [selectors...] [flags]\n\n"));
|
|
268
|
+
process.stdout.write("Build selected specs, run them, and print a final summary.\n\n");
|
|
269
|
+
process.stdout.write(chalk.bold("Flags:\n"));
|
|
270
|
+
process.stdout.write(" --config <path> Use a specific config file\n");
|
|
271
|
+
process.stdout.write(" --mode <name[,name...]> Run one or multiple named config modes\n");
|
|
272
|
+
process.stdout.write(" --update-snapshots Create/update snapshot files on mismatch\n");
|
|
273
|
+
process.stdout.write(" --no-snapshot Disable snapshot assertions for this run\n");
|
|
274
|
+
process.stdout.write(" --show-coverage Print uncovered coverage point details\n");
|
|
275
|
+
process.stdout.write(" --enable <feature> Enable feature (coverage|try-as)\n");
|
|
276
|
+
process.stdout.write(" --disable <feature> Disable feature (coverage|try-as)\n");
|
|
277
|
+
process.stdout.write(" --reporter <name|path> Use built-in reporter (default|tap) or custom module path\n");
|
|
278
|
+
process.stdout.write(" --tap Shortcut for --reporter tap\n");
|
|
279
|
+
process.stdout.write(" --verbose Keep expanded suite/test lines and live updates\n");
|
|
280
|
+
process.stdout.write(" --clean Disable in-place TTY updates; print final lines only\n");
|
|
281
|
+
process.stdout.write(" --list Preview resolved files/artifacts/runtime without running\n");
|
|
282
|
+
process.stdout.write(" --list-modes Preview configured and selected mode names\n");
|
|
283
|
+
process.stdout.write(" --help, -h Show this help\n");
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
if (command == "init") {
|
|
287
|
+
process.stdout.write(chalk.bold("Usage: ast init [dir] [flags]\n\n"));
|
|
288
|
+
process.stdout.write("Initialize as-test config, default runners, and example specs.\n\n");
|
|
289
|
+
process.stdout.write(chalk.bold("Flags:\n"));
|
|
290
|
+
process.stdout.write(" --target <wasi|bindings> Set build target\n");
|
|
291
|
+
process.stdout.write(" --example <minimal|full|none> Set example template\n");
|
|
292
|
+
process.stdout.write(" --install Install dependencies after scaffolding\n");
|
|
293
|
+
process.stdout.write(" --yes, -y Non-interactive setup with defaults\n");
|
|
294
|
+
process.stdout.write(" --force Overwrite managed files\n");
|
|
295
|
+
process.stdout.write(" --dir <path> Target output directory\n");
|
|
296
|
+
process.stdout.write(" --help, -h Show this help\n");
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
if (command == "doctor") {
|
|
300
|
+
process.stdout.write(chalk.bold("Usage: ast doctor [flags]\n\n"));
|
|
301
|
+
process.stdout.write("Validate config, dependencies, runtime command, and spec discovery.\n\n");
|
|
302
|
+
process.stdout.write(chalk.bold("Flags:\n"));
|
|
303
|
+
process.stdout.write(" --config <path> Use a specific config file\n");
|
|
304
|
+
process.stdout.write(" --mode <name[,name...]> Run checks for one or multiple named modes\n");
|
|
305
|
+
process.stdout.write(" --help, -h Show this help\n");
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
info();
|
|
309
|
+
}
|
|
157
310
|
function resolveConfigPath(rawArgs) {
|
|
158
311
|
for (let i = 0; i < rawArgs.length; i++) {
|
|
159
312
|
const arg = rawArgs[i];
|
|
@@ -208,6 +361,13 @@ function resolveCommandArgs(rawArgs, command) {
|
|
|
208
361
|
if (arg == "--tap") {
|
|
209
362
|
continue;
|
|
210
363
|
}
|
|
364
|
+
if (arg == "--enable" || arg == "--disable") {
|
|
365
|
+
i++;
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
368
|
+
if (arg.startsWith("--enable=") || arg.startsWith("--disable=")) {
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
211
371
|
if (arg.startsWith("-")) {
|
|
212
372
|
continue;
|
|
213
373
|
}
|
|
@@ -215,6 +375,73 @@ function resolveCommandArgs(rawArgs, command) {
|
|
|
215
375
|
}
|
|
216
376
|
return values;
|
|
217
377
|
}
|
|
378
|
+
function resolveFeatureToggles(rawArgs, command) {
|
|
379
|
+
if (command !== "build" && command !== "run" && command !== "test")
|
|
380
|
+
return {};
|
|
381
|
+
const out = {};
|
|
382
|
+
let seenCommand = false;
|
|
383
|
+
for (let i = 0; i < rawArgs.length; i++) {
|
|
384
|
+
const arg = rawArgs[i];
|
|
385
|
+
if (!seenCommand) {
|
|
386
|
+
if (arg == command)
|
|
387
|
+
seenCommand = true;
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
if (arg == "--enable" || arg == "--disable") {
|
|
391
|
+
const enabled = arg == "--enable";
|
|
392
|
+
const next = rawArgs[i + 1];
|
|
393
|
+
if (next && !next.startsWith("-")) {
|
|
394
|
+
applyFeatureToggle(out, next, enabled);
|
|
395
|
+
i++;
|
|
396
|
+
}
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
if (arg.startsWith("--enable=") || arg.startsWith("--disable=")) {
|
|
400
|
+
const enabled = arg.startsWith("--enable=");
|
|
401
|
+
const eq = arg.indexOf("=");
|
|
402
|
+
const value = arg.slice(eq + 1).trim();
|
|
403
|
+
if (value.length) {
|
|
404
|
+
applyFeatureToggle(out, value, enabled);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
return out;
|
|
409
|
+
}
|
|
410
|
+
function resolveListFlags(rawArgs, command) {
|
|
411
|
+
const out = {
|
|
412
|
+
list: false,
|
|
413
|
+
listModes: false,
|
|
414
|
+
};
|
|
415
|
+
if (command !== "build" && command !== "run" && command !== "test") {
|
|
416
|
+
return out;
|
|
417
|
+
}
|
|
418
|
+
let seenCommand = false;
|
|
419
|
+
for (let i = 0; i < rawArgs.length; i++) {
|
|
420
|
+
const arg = rawArgs[i];
|
|
421
|
+
if (!seenCommand) {
|
|
422
|
+
if (arg == command)
|
|
423
|
+
seenCommand = true;
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
426
|
+
if (arg == "--list")
|
|
427
|
+
out.list = true;
|
|
428
|
+
if (arg == "--list-modes")
|
|
429
|
+
out.listModes = true;
|
|
430
|
+
}
|
|
431
|
+
return out;
|
|
432
|
+
}
|
|
433
|
+
function applyFeatureToggle(out, rawFeature, enabled) {
|
|
434
|
+
const key = rawFeature.trim().toLowerCase();
|
|
435
|
+
if (key == "coverage") {
|
|
436
|
+
out.coverage = enabled;
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
if (key == "try-as" || key == "try_as" || key == "tryas") {
|
|
440
|
+
out.tryAs = enabled;
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
throw new Error(`unknown feature "${rawFeature}". Supported features: coverage, try-as`);
|
|
444
|
+
}
|
|
218
445
|
function resolveCommandTokens(rawArgs, command) {
|
|
219
446
|
const values = [];
|
|
220
447
|
let seenCommand = false;
|
|
@@ -229,13 +456,10 @@ function resolveCommandTokens(rawArgs, command) {
|
|
|
229
456
|
}
|
|
230
457
|
return values;
|
|
231
458
|
}
|
|
232
|
-
async function runTestSequential(runFlags, configPath, selectors, modeName) {
|
|
459
|
+
async function runTestSequential(runFlags, configPath, selectors, buildFeatureToggles, modeSummaryTotal, fileSummaryTotal, modeName) {
|
|
233
460
|
const files = await resolveSelectedFiles(configPath, selectors);
|
|
234
461
|
if (!files.length) {
|
|
235
|
-
|
|
236
|
-
? selectors.join(", ")
|
|
237
|
-
: "configured input patterns";
|
|
238
|
-
throw new Error(`No test files matched: ${scope}`);
|
|
462
|
+
throw await buildNoTestFilesMatchedError(configPath, selectors);
|
|
239
463
|
}
|
|
240
464
|
const reporterSession = await createRunReporter(configPath, undefined, modeName);
|
|
241
465
|
const reporter = reporterSession.reporter;
|
|
@@ -249,9 +473,10 @@ async function runTestSequential(runFlags, configPath, selectors, modeName) {
|
|
|
249
473
|
});
|
|
250
474
|
const results = [];
|
|
251
475
|
let failed = false;
|
|
476
|
+
const duplicateSpecBasenames = resolveDuplicateSpecBasenames(files);
|
|
252
477
|
for (const file of files) {
|
|
253
|
-
await build(configPath, [file], modeName);
|
|
254
|
-
const artifactKey =
|
|
478
|
+
await build(configPath, [file], modeName, buildFeatureToggles);
|
|
479
|
+
const artifactKey = resolvePerFileArtifactKey(file, duplicateSpecBasenames);
|
|
255
480
|
const result = await run(runFlags, configPath, [file], false, {
|
|
256
481
|
reporter,
|
|
257
482
|
emitRunStart: false,
|
|
@@ -265,6 +490,7 @@ async function runTestSequential(runFlags, configPath, selectors, modeName) {
|
|
|
265
490
|
failed = true;
|
|
266
491
|
}
|
|
267
492
|
const summary = aggregateRunResults(results);
|
|
493
|
+
summary.stats = applyConfiguredFileTotalToStats(summary.stats, fileSummaryTotal);
|
|
268
494
|
reporter.onRunComplete?.({
|
|
269
495
|
clean: runFlags.clean,
|
|
270
496
|
snapshotEnabled,
|
|
@@ -273,51 +499,474 @@ async function runTestSequential(runFlags, configPath, selectors, modeName) {
|
|
|
273
499
|
coverageSummary: summary.coverageSummary,
|
|
274
500
|
stats: summary.stats,
|
|
275
501
|
reports: summary.reports,
|
|
502
|
+
modeSummary: buildSingleModeSummary(summary.stats, summary.snapshotSummary, modeSummaryTotal),
|
|
276
503
|
});
|
|
277
504
|
return failed;
|
|
278
505
|
}
|
|
279
|
-
async function runBuildModes(configPath, selectors, modes) {
|
|
280
|
-
const
|
|
281
|
-
|
|
282
|
-
await build(configPath, selectors, modeName);
|
|
506
|
+
async function runBuildModes(configPath, selectors, modes, buildFeatureToggles) {
|
|
507
|
+
for (const modeName of modes) {
|
|
508
|
+
await build(configPath, selectors, modeName, buildFeatureToggles);
|
|
283
509
|
}
|
|
284
510
|
}
|
|
285
511
|
async function runRuntimeModes(runFlags, configPath, selectors, modes) {
|
|
286
|
-
const
|
|
512
|
+
const modeSummaryTotal = resolveConfiguredModeTotal(configPath);
|
|
513
|
+
const fileSummaryTotal = await resolveConfiguredFileTotal(configPath);
|
|
514
|
+
if (modes.length > 1) {
|
|
515
|
+
const failed = await runRuntimeMatrix(runFlags, configPath, selectors, modes, modeSummaryTotal, fileSummaryTotal);
|
|
516
|
+
process.exit(failed ? 1 : 0);
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
287
519
|
let failed = false;
|
|
288
|
-
for (const modeName of
|
|
520
|
+
for (const modeName of modes) {
|
|
289
521
|
const result = await run(runFlags, configPath, selectors, false, {
|
|
290
522
|
modeName,
|
|
523
|
+
modeSummaryTotal,
|
|
524
|
+
modeSummaryExecuted: 1,
|
|
525
|
+
fileSummaryTotal,
|
|
291
526
|
});
|
|
292
527
|
if (result.failed)
|
|
293
528
|
failed = true;
|
|
294
529
|
}
|
|
295
530
|
process.exit(failed ? 1 : 0);
|
|
296
531
|
}
|
|
297
|
-
async function
|
|
298
|
-
const
|
|
532
|
+
async function runRuntimeMatrix(runFlags, configPath, selectors, modes, modeSummaryTotal, fileSummaryTotal) {
|
|
533
|
+
const files = await resolveSelectedFiles(configPath, selectors);
|
|
534
|
+
if (!files.length) {
|
|
535
|
+
throw await buildNoTestFilesMatchedError(configPath, selectors);
|
|
536
|
+
}
|
|
537
|
+
const reporterSession = await createRunReporter(configPath);
|
|
538
|
+
const reporter = reporterSession.reporter;
|
|
539
|
+
const snapshotEnabled = runFlags.snapshot !== false;
|
|
540
|
+
reporter.onRunStart?.({
|
|
541
|
+
runtimeName: reporterSession.runtimeName,
|
|
542
|
+
clean: runFlags.clean,
|
|
543
|
+
verbose: runFlags.verbose,
|
|
544
|
+
snapshotEnabled,
|
|
545
|
+
updateSnapshots: runFlags.updateSnapshots,
|
|
546
|
+
});
|
|
547
|
+
const silentReporter = {};
|
|
548
|
+
const allResults = [];
|
|
549
|
+
const modeLabels = modes.map((modeName) => modeName ?? "default");
|
|
550
|
+
const showPerModeTimes = Boolean(runFlags.verbose);
|
|
551
|
+
const liveMatrix = reporterSession.reporterKind == "default" && canRewriteStdout();
|
|
552
|
+
const modeState = modes.map(() => ({
|
|
553
|
+
failed: false,
|
|
554
|
+
passed: false,
|
|
555
|
+
}));
|
|
556
|
+
const fileState = files.map(() => ({
|
|
557
|
+
failed: false,
|
|
558
|
+
passed: false,
|
|
559
|
+
}));
|
|
560
|
+
const duplicateSpecBasenames = resolveDuplicateSpecBasenames(files);
|
|
561
|
+
for (let fileIndex = 0; fileIndex < files.length; fileIndex++) {
|
|
562
|
+
const file = files[fileIndex];
|
|
563
|
+
const fileName = path.basename(file);
|
|
564
|
+
const fileResults = [];
|
|
565
|
+
const modeTimes = modes.map(() => "...");
|
|
566
|
+
if (liveMatrix) {
|
|
567
|
+
renderMatrixLiveLine(fileName, modeLabels, modeTimes, showPerModeTimes);
|
|
568
|
+
}
|
|
569
|
+
for (let i = 0; i < modes.length; i++) {
|
|
570
|
+
const modeName = modes[i];
|
|
571
|
+
try {
|
|
572
|
+
const artifactKey = resolvePerFileArtifactKey(file, duplicateSpecBasenames);
|
|
573
|
+
const result = await run(runFlags, configPath, [file], false, {
|
|
574
|
+
reporter: silentReporter,
|
|
575
|
+
reporterKind: "default",
|
|
576
|
+
emitRunStart: false,
|
|
577
|
+
emitRunComplete: false,
|
|
578
|
+
logFileName: `run.${artifactKey}.log.json`,
|
|
579
|
+
coverageFileName: `coverage.${artifactKey}.log.json`,
|
|
580
|
+
modeName,
|
|
581
|
+
});
|
|
582
|
+
modeTimes[i] = formatMatrixModeTime(result.stats.time);
|
|
583
|
+
if (liveMatrix) {
|
|
584
|
+
renderMatrixLiveLine(fileName, modeLabels, modeTimes, showPerModeTimes);
|
|
585
|
+
}
|
|
586
|
+
if (result.failed) {
|
|
587
|
+
modeState[i].failed = true;
|
|
588
|
+
}
|
|
589
|
+
else if (result.stats.passedFiles > 0) {
|
|
590
|
+
modeState[i].passed = true;
|
|
591
|
+
}
|
|
592
|
+
fileResults.push(result);
|
|
593
|
+
allResults.push(result);
|
|
594
|
+
}
|
|
595
|
+
catch (error) {
|
|
596
|
+
clearLiveLine();
|
|
597
|
+
throw error;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
renderMatrixFileResult(fileName, modeLabels, fileResults, modeTimes, liveMatrix, showPerModeTimes);
|
|
601
|
+
const verdict = resolveMatrixVerdict(fileResults);
|
|
602
|
+
if (verdict == "fail") {
|
|
603
|
+
fileState[fileIndex].failed = true;
|
|
604
|
+
}
|
|
605
|
+
else if (verdict == "ok") {
|
|
606
|
+
fileState[fileIndex].passed = true;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
const summary = aggregateRunResults(allResults);
|
|
610
|
+
summary.stats = applyMatrixFileSummaryToStats(summary.stats, fileState, fileSummaryTotal);
|
|
611
|
+
reporter.onRunComplete?.({
|
|
612
|
+
clean: runFlags.clean,
|
|
613
|
+
snapshotEnabled,
|
|
614
|
+
showCoverage: runFlags.showCoverage,
|
|
615
|
+
snapshotSummary: summary.snapshotSummary,
|
|
616
|
+
coverageSummary: summary.coverageSummary,
|
|
617
|
+
stats: summary.stats,
|
|
618
|
+
reports: summary.reports,
|
|
619
|
+
modeSummary: buildModeSummary(modeState, modeSummaryTotal),
|
|
620
|
+
});
|
|
621
|
+
return allResults.some((result) => result.failed);
|
|
622
|
+
}
|
|
623
|
+
async function runTestModes(runFlags, configPath, selectors, modes, buildFeatureToggles) {
|
|
624
|
+
const modeSummaryTotal = resolveConfiguredModeTotal(configPath);
|
|
625
|
+
const fileSummaryTotal = await resolveConfiguredFileTotal(configPath);
|
|
626
|
+
if (modes.length > 1) {
|
|
627
|
+
const failed = await runTestMatrix(runFlags, configPath, selectors, modes, buildFeatureToggles, modeSummaryTotal, fileSummaryTotal);
|
|
628
|
+
process.exit(failed ? 1 : 0);
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
299
631
|
let failed = false;
|
|
300
|
-
for (const modeName of
|
|
301
|
-
const modeFailed = await runTestSequential(runFlags, configPath, selectors, modeName);
|
|
632
|
+
for (const modeName of modes) {
|
|
633
|
+
const modeFailed = await runTestSequential(runFlags, configPath, selectors, buildFeatureToggles, modeSummaryTotal, fileSummaryTotal, modeName);
|
|
302
634
|
if (modeFailed)
|
|
303
635
|
failed = true;
|
|
304
636
|
}
|
|
305
637
|
process.exit(failed ? 1 : 0);
|
|
306
638
|
}
|
|
307
|
-
async function
|
|
639
|
+
async function runTestMatrix(runFlags, configPath, selectors, modes, buildFeatureToggles, modeSummaryTotal, fileSummaryTotal) {
|
|
640
|
+
const files = await resolveSelectedFiles(configPath, selectors);
|
|
641
|
+
if (!files.length) {
|
|
642
|
+
throw await buildNoTestFilesMatchedError(configPath, selectors);
|
|
643
|
+
}
|
|
644
|
+
const reporterSession = await createRunReporter(configPath);
|
|
645
|
+
const reporter = reporterSession.reporter;
|
|
646
|
+
const snapshotEnabled = runFlags.snapshot !== false;
|
|
647
|
+
reporter.onRunStart?.({
|
|
648
|
+
runtimeName: reporterSession.runtimeName,
|
|
649
|
+
clean: runFlags.clean,
|
|
650
|
+
verbose: runFlags.verbose,
|
|
651
|
+
snapshotEnabled,
|
|
652
|
+
updateSnapshots: runFlags.updateSnapshots,
|
|
653
|
+
});
|
|
654
|
+
const silentReporter = {};
|
|
655
|
+
const allResults = [];
|
|
656
|
+
const modeLabels = modes.map((modeName) => modeName ?? "default");
|
|
657
|
+
const showPerModeTimes = Boolean(runFlags.verbose);
|
|
658
|
+
const liveMatrix = reporterSession.reporterKind == "default" && canRewriteStdout();
|
|
659
|
+
const modeState = modes.map(() => ({
|
|
660
|
+
failed: false,
|
|
661
|
+
passed: false,
|
|
662
|
+
}));
|
|
663
|
+
const fileState = files.map(() => ({
|
|
664
|
+
failed: false,
|
|
665
|
+
passed: false,
|
|
666
|
+
}));
|
|
667
|
+
const duplicateSpecBasenames = resolveDuplicateSpecBasenames(files);
|
|
668
|
+
for (let fileIndex = 0; fileIndex < files.length; fileIndex++) {
|
|
669
|
+
const file = files[fileIndex];
|
|
670
|
+
const fileName = path.basename(file);
|
|
671
|
+
const fileResults = [];
|
|
672
|
+
const modeTimes = modes.map(() => "...");
|
|
673
|
+
if (liveMatrix) {
|
|
674
|
+
renderMatrixLiveLine(fileName, modeLabels, modeTimes, showPerModeTimes);
|
|
675
|
+
}
|
|
676
|
+
for (let i = 0; i < modes.length; i++) {
|
|
677
|
+
const modeName = modes[i];
|
|
678
|
+
try {
|
|
679
|
+
await build(configPath, [file], modeName, buildFeatureToggles);
|
|
680
|
+
const artifactKey = resolvePerFileArtifactKey(file, duplicateSpecBasenames);
|
|
681
|
+
const result = await run(runFlags, configPath, [file], false, {
|
|
682
|
+
reporter: silentReporter,
|
|
683
|
+
reporterKind: "default",
|
|
684
|
+
emitRunStart: false,
|
|
685
|
+
emitRunComplete: false,
|
|
686
|
+
logFileName: `test.${artifactKey}.log.json`,
|
|
687
|
+
coverageFileName: `coverage.${artifactKey}.log.json`,
|
|
688
|
+
modeName,
|
|
689
|
+
});
|
|
690
|
+
modeTimes[i] = formatMatrixModeTime(result.stats.time);
|
|
691
|
+
if (liveMatrix) {
|
|
692
|
+
renderMatrixLiveLine(fileName, modeLabels, modeTimes, showPerModeTimes);
|
|
693
|
+
}
|
|
694
|
+
if (result.failed) {
|
|
695
|
+
modeState[i].failed = true;
|
|
696
|
+
}
|
|
697
|
+
else if (result.stats.passedFiles > 0) {
|
|
698
|
+
modeState[i].passed = true;
|
|
699
|
+
}
|
|
700
|
+
fileResults.push(result);
|
|
701
|
+
allResults.push(result);
|
|
702
|
+
}
|
|
703
|
+
catch (error) {
|
|
704
|
+
clearLiveLine();
|
|
705
|
+
throw error;
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
renderMatrixFileResult(fileName, modeLabels, fileResults, modeTimes, liveMatrix, showPerModeTimes);
|
|
709
|
+
const verdict = resolveMatrixVerdict(fileResults);
|
|
710
|
+
if (verdict == "fail") {
|
|
711
|
+
fileState[fileIndex].failed = true;
|
|
712
|
+
}
|
|
713
|
+
else if (verdict == "ok") {
|
|
714
|
+
fileState[fileIndex].passed = true;
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
const summary = aggregateRunResults(allResults);
|
|
718
|
+
summary.stats = applyMatrixFileSummaryToStats(summary.stats, fileState, fileSummaryTotal);
|
|
719
|
+
reporter.onRunComplete?.({
|
|
720
|
+
clean: runFlags.clean,
|
|
721
|
+
snapshotEnabled,
|
|
722
|
+
showCoverage: runFlags.showCoverage,
|
|
723
|
+
snapshotSummary: summary.snapshotSummary,
|
|
724
|
+
coverageSummary: summary.coverageSummary,
|
|
725
|
+
stats: summary.stats,
|
|
726
|
+
reports: summary.reports,
|
|
727
|
+
modeSummary: buildModeSummary(modeState, modeSummaryTotal),
|
|
728
|
+
});
|
|
729
|
+
return allResults.some((result) => result.failed);
|
|
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");
|
|
763
|
+
}
|
|
764
|
+
function renderMatrixLiveLine(file, modes, modeTimes, showPerModeTimes) {
|
|
765
|
+
if (!canRewriteStdout())
|
|
766
|
+
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
|
+
}
|
|
787
|
+
return `${(total / results.length).toFixed(1)}ms`;
|
|
788
|
+
}
|
|
789
|
+
function buildModeSummary(modeState, totalModes) {
|
|
790
|
+
const total = Math.max(totalModes, modeState.length, 1);
|
|
791
|
+
let skipped = Math.max(0, total - modeState.length);
|
|
792
|
+
let failed = 0;
|
|
793
|
+
for (const mode of modeState) {
|
|
794
|
+
if (mode.failed) {
|
|
795
|
+
failed++;
|
|
796
|
+
}
|
|
797
|
+
else if (!mode.passed) {
|
|
798
|
+
skipped++;
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
return {
|
|
802
|
+
failed,
|
|
803
|
+
skipped,
|
|
804
|
+
total,
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
function buildSingleModeSummary(stats, snapshotSummary, totalModes) {
|
|
808
|
+
const total = Math.max(totalModes, 1);
|
|
809
|
+
const failed = stats.failedFiles > 0 || snapshotSummary.failed > 0 ? 1 : 0;
|
|
810
|
+
const skippedInExecuted = failed ? 0 : stats.passedFiles > 0 ? 0 : 1;
|
|
811
|
+
return {
|
|
812
|
+
failed,
|
|
813
|
+
skipped: Math.max(0, total - 1) + skippedInExecuted,
|
|
814
|
+
total,
|
|
815
|
+
};
|
|
816
|
+
}
|
|
817
|
+
function applyConfiguredFileTotalToStats(stats, fileSummaryTotal) {
|
|
818
|
+
const total = Math.max(fileSummaryTotal, 0);
|
|
819
|
+
const executed = stats.failedFiles + stats.passedFiles + stats.skippedFiles;
|
|
820
|
+
const unexecuted = Math.max(0, total - executed);
|
|
821
|
+
return {
|
|
822
|
+
...stats,
|
|
823
|
+
skippedFiles: stats.skippedFiles + unexecuted,
|
|
824
|
+
};
|
|
825
|
+
}
|
|
826
|
+
function applyMatrixFileSummaryToStats(stats, fileState, fileSummaryTotal) {
|
|
827
|
+
let failedFiles = 0;
|
|
828
|
+
let passedFiles = 0;
|
|
829
|
+
let skippedFiles = 0;
|
|
830
|
+
for (const file of fileState) {
|
|
831
|
+
if (file.failed)
|
|
832
|
+
failedFiles++;
|
|
833
|
+
else if (file.passed)
|
|
834
|
+
passedFiles++;
|
|
835
|
+
else
|
|
836
|
+
skippedFiles++;
|
|
837
|
+
}
|
|
838
|
+
const total = Math.max(fileSummaryTotal, fileState.length, 0);
|
|
839
|
+
const unexecuted = Math.max(0, total - fileState.length);
|
|
840
|
+
return {
|
|
841
|
+
...stats,
|
|
842
|
+
failedFiles,
|
|
843
|
+
passedFiles,
|
|
844
|
+
skippedFiles: skippedFiles + unexecuted,
|
|
845
|
+
};
|
|
846
|
+
}
|
|
847
|
+
function resolveConfiguredModeTotal(configPath) {
|
|
308
848
|
const resolvedConfigPath = configPath ?? path.join(process.cwd(), "./as-test.config.json");
|
|
309
|
-
const config = loadConfig(resolvedConfigPath,
|
|
849
|
+
const config = loadConfig(resolvedConfigPath, false);
|
|
850
|
+
const configuredModes = Object.keys(config.modes).length;
|
|
851
|
+
return configuredModes || 1;
|
|
852
|
+
}
|
|
853
|
+
async function resolveConfiguredFileTotal(configPath) {
|
|
854
|
+
const files = await resolveSelectedFiles(configPath, []);
|
|
855
|
+
return files.length;
|
|
856
|
+
}
|
|
857
|
+
function resolveExecutionModes(configPath, selectedModes) {
|
|
858
|
+
if (selectedModes.length)
|
|
859
|
+
return selectedModes;
|
|
860
|
+
const resolvedConfigPath = configPath ?? path.join(process.cwd(), "./as-test.config.json");
|
|
861
|
+
const config = loadConfig(resolvedConfigPath, false);
|
|
862
|
+
const configuredModes = Object.keys(config.modes);
|
|
863
|
+
if (!configuredModes.length)
|
|
864
|
+
return [undefined];
|
|
865
|
+
return configuredModes;
|
|
866
|
+
}
|
|
867
|
+
async function resolveSelectedFiles(configPath, selectors, warn = true) {
|
|
868
|
+
const resolvedConfigPath = configPath ?? path.join(process.cwd(), "./as-test.config.json");
|
|
869
|
+
const config = loadConfig(resolvedConfigPath, warn);
|
|
310
870
|
const patterns = resolveInputPatterns(config.input, selectors);
|
|
311
871
|
const matches = await glob(patterns);
|
|
312
872
|
const specs = matches.filter((file) => file.endsWith(".spec.ts"));
|
|
313
873
|
return [...new Set(specs)].sort((a, b) => a.localeCompare(b));
|
|
314
874
|
}
|
|
875
|
+
async function buildNoTestFilesMatchedError(configPath, selectors) {
|
|
876
|
+
const scope = selectors.length > 0 ? selectors.join(", ") : "configured input patterns";
|
|
877
|
+
const lines = [`No test files matched: ${scope}`];
|
|
878
|
+
const configuredFiles = await resolveSelectedFiles(configPath, [], false);
|
|
879
|
+
if (!selectors.length) {
|
|
880
|
+
lines.push('No specs were discovered from configured input patterns. Check "input" in config or run "ast doctor".');
|
|
881
|
+
return new Error(lines.join("\n"));
|
|
882
|
+
}
|
|
883
|
+
const suggestions = suggestClosestSuites(selectors, configuredFiles);
|
|
884
|
+
if (suggestions.length) {
|
|
885
|
+
lines.push(`Closest suite names: ${suggestions.join(", ")}`);
|
|
886
|
+
}
|
|
887
|
+
if (configuredFiles.length) {
|
|
888
|
+
const sample = configuredFiles
|
|
889
|
+
.slice(0, 5)
|
|
890
|
+
.map((file) => path.basename(file))
|
|
891
|
+
.join(", ");
|
|
892
|
+
lines.push(`Configured specs (${configuredFiles.length}): ${sample}${configuredFiles.length > 5 ? ", ..." : ""}`);
|
|
893
|
+
}
|
|
894
|
+
else {
|
|
895
|
+
lines.push('No specs were discovered from configured input patterns. Check "input" in config.');
|
|
896
|
+
}
|
|
897
|
+
lines.push('Run "ast test --list" to inspect resolved files.');
|
|
898
|
+
return new Error(lines.join("\n"));
|
|
899
|
+
}
|
|
900
|
+
function suggestClosestSuites(selectors, files) {
|
|
901
|
+
const suites = [
|
|
902
|
+
...new Set(files.map((file) => stripSuiteSuffix(path.basename(file)))),
|
|
903
|
+
];
|
|
904
|
+
if (!suites.length)
|
|
905
|
+
return [];
|
|
906
|
+
const out = new Set();
|
|
907
|
+
for (const selector of expandSelectors(selectors)) {
|
|
908
|
+
if (!isBareSuiteSelector(selector))
|
|
909
|
+
continue;
|
|
910
|
+
const query = stripSuiteSuffix(path.basename(selector));
|
|
911
|
+
const closest = resolveClosestSuiteName(query, suites);
|
|
912
|
+
if (closest)
|
|
913
|
+
out.add(closest);
|
|
914
|
+
}
|
|
915
|
+
return [...out].slice(0, 3);
|
|
916
|
+
}
|
|
917
|
+
function resolveClosestSuiteName(value, candidates) {
|
|
918
|
+
if (!value.length)
|
|
919
|
+
return null;
|
|
920
|
+
let best = null;
|
|
921
|
+
let bestDistance = Number.POSITIVE_INFINITY;
|
|
922
|
+
const lowered = value.toLowerCase();
|
|
923
|
+
for (const candidate of candidates) {
|
|
924
|
+
if (candidate == value)
|
|
925
|
+
return null;
|
|
926
|
+
const normalized = candidate.toLowerCase();
|
|
927
|
+
if (normalized.startsWith(lowered) || normalized.includes(lowered)) {
|
|
928
|
+
return candidate;
|
|
929
|
+
}
|
|
930
|
+
const distance = levenshteinDistance(lowered, normalized);
|
|
931
|
+
if (distance < bestDistance) {
|
|
932
|
+
bestDistance = distance;
|
|
933
|
+
best = candidate;
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
if (best && bestDistance <= 3)
|
|
937
|
+
return best;
|
|
938
|
+
return null;
|
|
939
|
+
}
|
|
940
|
+
function levenshteinDistance(left, right) {
|
|
941
|
+
if (left == right)
|
|
942
|
+
return 0;
|
|
943
|
+
if (!left.length)
|
|
944
|
+
return right.length;
|
|
945
|
+
if (!right.length)
|
|
946
|
+
return left.length;
|
|
947
|
+
const matrix = [];
|
|
948
|
+
for (let i = 0; i <= left.length; i++) {
|
|
949
|
+
matrix[i] = [i];
|
|
950
|
+
}
|
|
951
|
+
for (let j = 0; j <= right.length; j++) {
|
|
952
|
+
matrix[0][j] = j;
|
|
953
|
+
}
|
|
954
|
+
for (let i = 1; i <= left.length; i++) {
|
|
955
|
+
for (let j = 1; j <= right.length; j++) {
|
|
956
|
+
const cost = left[i - 1] == right[j - 1] ? 0 : 1;
|
|
957
|
+
matrix[i][j] = Math.min(matrix[i - 1][j] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j - 1] + cost);
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
return matrix[left.length][right.length];
|
|
961
|
+
}
|
|
315
962
|
function resolveInputPatterns(configured, selectors) {
|
|
316
|
-
const configuredInputs = Array.isArray(configured)
|
|
963
|
+
const configuredInputs = Array.isArray(configured)
|
|
964
|
+
? configured
|
|
965
|
+
: [configured];
|
|
317
966
|
if (!selectors.length)
|
|
318
967
|
return configuredInputs;
|
|
319
968
|
const patterns = new Set();
|
|
320
|
-
for (const selector of selectors) {
|
|
969
|
+
for (const selector of expandSelectors(selectors)) {
|
|
321
970
|
if (!selector)
|
|
322
971
|
continue;
|
|
323
972
|
if (isBareSuiteSelector(selector)) {
|
|
@@ -331,6 +980,30 @@ function resolveInputPatterns(configured, selectors) {
|
|
|
331
980
|
}
|
|
332
981
|
return [...patterns];
|
|
333
982
|
}
|
|
983
|
+
function expandSelectors(selectors) {
|
|
984
|
+
const expanded = [];
|
|
985
|
+
for (const selector of selectors) {
|
|
986
|
+
if (!selector)
|
|
987
|
+
continue;
|
|
988
|
+
if (!shouldSplitSelector(selector)) {
|
|
989
|
+
expanded.push(selector);
|
|
990
|
+
continue;
|
|
991
|
+
}
|
|
992
|
+
for (const token of selector.split(",")) {
|
|
993
|
+
const trimmed = token.trim();
|
|
994
|
+
if (!trimmed.length)
|
|
995
|
+
continue;
|
|
996
|
+
expanded.push(trimmed);
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
return expanded;
|
|
1000
|
+
}
|
|
1001
|
+
function shouldSplitSelector(selector) {
|
|
1002
|
+
return (selector.includes(",") &&
|
|
1003
|
+
!selector.includes("/") &&
|
|
1004
|
+
!selector.includes("\\") &&
|
|
1005
|
+
!/[*?[\]{}]/.test(selector));
|
|
1006
|
+
}
|
|
334
1007
|
function isBareSuiteSelector(selector) {
|
|
335
1008
|
return (!selector.includes("/") &&
|
|
336
1009
|
!selector.includes("\\") &&
|
|
@@ -339,6 +1012,121 @@ function isBareSuiteSelector(selector) {
|
|
|
339
1012
|
function stripSuiteSuffix(selector) {
|
|
340
1013
|
return selector.replace(/\.spec\.ts$/, "").replace(/\.ts$/, "");
|
|
341
1014
|
}
|
|
1015
|
+
function resolveDuplicateSpecBasenames(files) {
|
|
1016
|
+
const counts = new Map();
|
|
1017
|
+
for (const file of files) {
|
|
1018
|
+
const base = path.basename(file);
|
|
1019
|
+
counts.set(base, (counts.get(base) ?? 0) + 1);
|
|
1020
|
+
}
|
|
1021
|
+
const duplicates = new Set();
|
|
1022
|
+
for (const [base, count] of counts) {
|
|
1023
|
+
if (count > 1)
|
|
1024
|
+
duplicates.add(base);
|
|
1025
|
+
}
|
|
1026
|
+
return duplicates;
|
|
1027
|
+
}
|
|
1028
|
+
function resolvePerFileArtifactKey(file, duplicateSpecBasenames) {
|
|
1029
|
+
const base = path.basename(file);
|
|
1030
|
+
let raw = base;
|
|
1031
|
+
if (duplicateSpecBasenames.has(base)) {
|
|
1032
|
+
const disambiguator = resolvePerFileDisambiguator(file);
|
|
1033
|
+
if (disambiguator.length) {
|
|
1034
|
+
raw = `${base}.${disambiguator}`;
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
return raw.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
1038
|
+
}
|
|
1039
|
+
function resolvePerFileDisambiguator(file) {
|
|
1040
|
+
const relDir = path.dirname(path.relative(process.cwd(), file));
|
|
1041
|
+
if (!relDir.length || relDir == ".")
|
|
1042
|
+
return "";
|
|
1043
|
+
return relDir
|
|
1044
|
+
.replace(/[\\/]+/g, "__")
|
|
1045
|
+
.replace(/[^A-Za-z0-9._-]/g, "_")
|
|
1046
|
+
.replace(/^_+|_+$/g, "");
|
|
1047
|
+
}
|
|
1048
|
+
function resolveArtifactFileNameForPreview(file, target, modeName, duplicateSpecBasenames) {
|
|
1049
|
+
const base = path
|
|
1050
|
+
.basename(file)
|
|
1051
|
+
.replace(/\.spec\.ts$/, "")
|
|
1052
|
+
.replace(/\.ts$/, "");
|
|
1053
|
+
const legacy = !modeName
|
|
1054
|
+
? `${path.basename(file).replace(".ts", ".wasm")}`
|
|
1055
|
+
: `${base}.${modeName}.${target}.wasm`;
|
|
1056
|
+
if (!duplicateSpecBasenames.has(path.basename(file))) {
|
|
1057
|
+
return legacy;
|
|
1058
|
+
}
|
|
1059
|
+
const disambiguator = resolvePerFileDisambiguator(file);
|
|
1060
|
+
if (!disambiguator.length) {
|
|
1061
|
+
return legacy;
|
|
1062
|
+
}
|
|
1063
|
+
const ext = path.extname(legacy);
|
|
1064
|
+
const stem = ext.length ? legacy.slice(0, -ext.length) : legacy;
|
|
1065
|
+
return `${stem}.${disambiguator}${ext}`;
|
|
1066
|
+
}
|
|
1067
|
+
async function listExecutionPlan(command, configPath, selectors, modes, listFlags) {
|
|
1068
|
+
const resolvedConfigPath = configPath ?? path.join(process.cwd(), "./as-test.config.json");
|
|
1069
|
+
const config = loadConfig(resolvedConfigPath, true);
|
|
1070
|
+
const configuredModes = Object.keys(config.modes);
|
|
1071
|
+
const configuredModeLabels = configuredModes.length
|
|
1072
|
+
? configuredModes
|
|
1073
|
+
: ["default"];
|
|
1074
|
+
const selectedModeLabels = modes.map((modeName) => modeName ?? "default");
|
|
1075
|
+
const unknownModes = modes.filter((modeName) => Boolean(modeName && !configuredModes.includes(modeName)));
|
|
1076
|
+
if (unknownModes.length) {
|
|
1077
|
+
throw new Error(`unknown mode "${unknownModes[0]}". Available modes: ${configuredModes.join(", ") || "(none)"}`);
|
|
1078
|
+
}
|
|
1079
|
+
process.stdout.write(chalk.bold.blueBright("as-test plan") + "\n");
|
|
1080
|
+
process.stdout.write(chalk.dim(`command: ${command}`) + "\n");
|
|
1081
|
+
process.stdout.write(chalk.dim(`config: ${resolvedConfigPath}`) + "\n");
|
|
1082
|
+
process.stdout.write(chalk.dim(`selectors: ${selectors.length ? selectors.join(", ") : "(configured input patterns)"}`) + "\n\n");
|
|
1083
|
+
if (listFlags.listModes) {
|
|
1084
|
+
process.stdout.write(chalk.bold("Configured modes:\n"));
|
|
1085
|
+
for (const modeName of configuredModeLabels) {
|
|
1086
|
+
process.stdout.write(` - ${modeName}\n`);
|
|
1087
|
+
}
|
|
1088
|
+
process.stdout.write(chalk.bold("\nSelected modes:\n"));
|
|
1089
|
+
for (const modeName of selectedModeLabels) {
|
|
1090
|
+
process.stdout.write(` - ${modeName}\n`);
|
|
1091
|
+
}
|
|
1092
|
+
process.stdout.write("\n");
|
|
1093
|
+
}
|
|
1094
|
+
if (!listFlags.list)
|
|
1095
|
+
return;
|
|
1096
|
+
const files = await resolveSelectedFiles(configPath, selectors);
|
|
1097
|
+
if (!files.length) {
|
|
1098
|
+
const scope = selectors.length > 0 ? selectors.join(", ") : "configured input patterns";
|
|
1099
|
+
throw new Error(`No test files matched: ${scope}`);
|
|
1100
|
+
}
|
|
1101
|
+
const duplicateSpecBasenames = resolveDuplicateSpecBasenames(files);
|
|
1102
|
+
process.stdout.write(chalk.bold("Resolved files:\n"));
|
|
1103
|
+
for (const file of files) {
|
|
1104
|
+
process.stdout.write(` - ${file}\n`);
|
|
1105
|
+
}
|
|
1106
|
+
process.stdout.write("\n");
|
|
1107
|
+
for (const modeName of modes) {
|
|
1108
|
+
const applied = applyMode(config, modeName);
|
|
1109
|
+
const active = applied.config;
|
|
1110
|
+
const modeLabel = modeName ?? "default";
|
|
1111
|
+
process.stdout.write(chalk.bold(`Mode: ${modeLabel}\n`));
|
|
1112
|
+
process.stdout.write(` target: ${active.buildOptions.target}\n`);
|
|
1113
|
+
process.stdout.write(` outDir: ${active.outDir}\n`);
|
|
1114
|
+
if (command != "build") {
|
|
1115
|
+
process.stdout.write(` runtime: ${active.runOptions.runtime.cmd}\n`);
|
|
1116
|
+
}
|
|
1117
|
+
const envOverrides = modeName
|
|
1118
|
+
? (config.modes[modeName]?.env ?? {})
|
|
1119
|
+
: config.env;
|
|
1120
|
+
const envKeys = Object.keys(envOverrides);
|
|
1121
|
+
process.stdout.write(` env overrides: ${envKeys.length}${envKeys.length ? ` (${envKeys.join(", ")})` : ""}\n`);
|
|
1122
|
+
process.stdout.write(" artifacts:\n");
|
|
1123
|
+
for (const file of files) {
|
|
1124
|
+
const artifactName = resolveArtifactFileNameForPreview(file, active.buildOptions.target, modeName, duplicateSpecBasenames);
|
|
1125
|
+
process.stdout.write(` - ${path.join(active.outDir, artifactName)}\n`);
|
|
1126
|
+
}
|
|
1127
|
+
process.stdout.write("\n");
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
342
1130
|
function aggregateRunResults(results) {
|
|
343
1131
|
const stats = {
|
|
344
1132
|
passedFiles: 0,
|
|
@@ -390,7 +1178,8 @@ function aggregateRunResults(results) {
|
|
|
390
1178
|
snapshotSummary.created += result.snapshotSummary.created;
|
|
391
1179
|
snapshotSummary.updated += result.snapshotSummary.updated;
|
|
392
1180
|
snapshotSummary.failed += result.snapshotSummary.failed;
|
|
393
|
-
coverageSummary.enabled =
|
|
1181
|
+
coverageSummary.enabled =
|
|
1182
|
+
coverageSummary.enabled || result.coverageSummary.enabled;
|
|
394
1183
|
coverageSummary.showPoints =
|
|
395
1184
|
coverageSummary.showPoints || result.coverageSummary.showPoints;
|
|
396
1185
|
for (const fileCoverage of result.coverageSummary.files) {
|