as-test 0.5.3 → 1.0.0
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 +22 -0
- package/README.md +74 -7
- package/as-test.config.schema.json +31 -0
- package/assembly/src/expectation.ts +32 -9
- package/bin/{build.js → commands/build-core.js} +113 -35
- 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} +95 -30
- 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 +410 -52
- package/bin/util.js +573 -8
- package/package.json +8 -5
- package/transform/lib/index.js +2 -1
- package/bin/init.js +0 -497
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);
|
|
@@ -30,44 +34,59 @@ if (!args.length) {
|
|
|
30
34
|
else if (COMMANDS.includes(args[0])) {
|
|
31
35
|
try {
|
|
32
36
|
const command = args.shift();
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
};
|
|
47
|
-
if (command === "build") {
|
|
48
|
-
const modeTargets = resolveExecutionModes(configPath, selectedModes);
|
|
49
|
-
runBuildModes(configPath, commandArgs, modeTargets, buildFeatureToggles).catch((error) => {
|
|
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
50
|
printCliError(error);
|
|
51
51
|
process.exit(1);
|
|
52
52
|
});
|
|
53
53
|
}
|
|
54
54
|
else if (command === "run") {
|
|
55
|
-
|
|
56
|
-
|
|
55
|
+
executeRunCommand(_args, flags, configPath, selectedModes, {
|
|
56
|
+
resolveCommandArgs,
|
|
57
|
+
resolveListFlags,
|
|
58
|
+
resolveFeatureToggles,
|
|
59
|
+
resolveExecutionModes,
|
|
60
|
+
listExecutionPlan,
|
|
61
|
+
runRuntimeModes,
|
|
62
|
+
}).catch((error) => {
|
|
57
63
|
printCliError(error);
|
|
58
64
|
process.exit(1);
|
|
59
65
|
});
|
|
60
66
|
}
|
|
61
67
|
else if (command === "test") {
|
|
62
|
-
|
|
63
|
-
|
|
68
|
+
executeTestCommand(_args, flags, configPath, selectedModes, {
|
|
69
|
+
resolveCommandArgs,
|
|
70
|
+
resolveListFlags,
|
|
71
|
+
resolveFeatureToggles,
|
|
72
|
+
resolveExecutionModes,
|
|
73
|
+
listExecutionPlan,
|
|
74
|
+
runTestModes,
|
|
75
|
+
}).catch((error) => {
|
|
64
76
|
printCliError(error);
|
|
65
77
|
process.exit(1);
|
|
66
78
|
});
|
|
67
79
|
}
|
|
68
80
|
else if (command === "init") {
|
|
69
|
-
|
|
70
|
-
|
|
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) => {
|
|
71
90
|
printCliError(error);
|
|
72
91
|
process.exit(1);
|
|
73
92
|
});
|
|
@@ -126,6 +145,12 @@ function info() {
|
|
|
126
145
|
chalk.dim("<./dir>") +
|
|
127
146
|
" " +
|
|
128
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");
|
|
129
154
|
console.log("");
|
|
130
155
|
console.log(chalk.bold("Flags:"));
|
|
131
156
|
console.log(" " +
|
|
@@ -154,11 +179,11 @@ function info() {
|
|
|
154
179
|
"Print all coverage points with line:column refs");
|
|
155
180
|
console.log(" " +
|
|
156
181
|
chalk.bold.blue("--enable <feature>") +
|
|
157
|
-
"
|
|
182
|
+
" " +
|
|
158
183
|
"Enable as-test feature (coverage|try-as)");
|
|
159
184
|
console.log(" " +
|
|
160
185
|
chalk.bold.blue("--disable <feature>") +
|
|
161
|
-
"
|
|
186
|
+
" " +
|
|
162
187
|
"Disable as-test feature (coverage|try-as)");
|
|
163
188
|
console.log(" " +
|
|
164
189
|
chalk.bold.blue("--verbose") +
|
|
@@ -168,6 +193,15 @@ function info() {
|
|
|
168
193
|
chalk.bold.blue("--reporter <name|path>") +
|
|
169
194
|
" " +
|
|
170
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");
|
|
171
205
|
console.log("");
|
|
172
206
|
console.log(chalk.dim("If your using this, consider dropping a star, it would help a lot!") + "\n");
|
|
173
207
|
console.log("View the repo: " +
|
|
@@ -177,6 +211,102 @@ function info() {
|
|
|
177
211
|
// chalk.blue("https://docs.jairus.dev/as-test"),
|
|
178
212
|
// );
|
|
179
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
|
+
}
|
|
180
310
|
function resolveConfigPath(rawArgs) {
|
|
181
311
|
for (let i = 0; i < rawArgs.length; i++) {
|
|
182
312
|
const arg = rawArgs[i];
|
|
@@ -277,6 +407,29 @@ function resolveFeatureToggles(rawArgs, command) {
|
|
|
277
407
|
}
|
|
278
408
|
return out;
|
|
279
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
|
+
}
|
|
280
433
|
function applyFeatureToggle(out, rawFeature, enabled) {
|
|
281
434
|
const key = rawFeature.trim().toLowerCase();
|
|
282
435
|
if (key == "coverage") {
|
|
@@ -306,10 +459,7 @@ function resolveCommandTokens(rawArgs, command) {
|
|
|
306
459
|
async function runTestSequential(runFlags, configPath, selectors, buildFeatureToggles, modeSummaryTotal, fileSummaryTotal, modeName) {
|
|
307
460
|
const files = await resolveSelectedFiles(configPath, selectors);
|
|
308
461
|
if (!files.length) {
|
|
309
|
-
|
|
310
|
-
? selectors.join(", ")
|
|
311
|
-
: "configured input patterns";
|
|
312
|
-
throw new Error(`No test files matched: ${scope}`);
|
|
462
|
+
throw await buildNoTestFilesMatchedError(configPath, selectors);
|
|
313
463
|
}
|
|
314
464
|
const reporterSession = await createRunReporter(configPath, undefined, modeName);
|
|
315
465
|
const reporter = reporterSession.reporter;
|
|
@@ -323,9 +473,10 @@ async function runTestSequential(runFlags, configPath, selectors, buildFeatureTo
|
|
|
323
473
|
});
|
|
324
474
|
const results = [];
|
|
325
475
|
let failed = false;
|
|
476
|
+
const duplicateSpecBasenames = resolveDuplicateSpecBasenames(files);
|
|
326
477
|
for (const file of files) {
|
|
327
478
|
await build(configPath, [file], modeName, buildFeatureToggles);
|
|
328
|
-
const artifactKey =
|
|
479
|
+
const artifactKey = resolvePerFileArtifactKey(file, duplicateSpecBasenames);
|
|
329
480
|
const result = await run(runFlags, configPath, [file], false, {
|
|
330
481
|
reporter,
|
|
331
482
|
emitRunStart: false,
|
|
@@ -381,10 +532,7 @@ async function runRuntimeModes(runFlags, configPath, selectors, modes) {
|
|
|
381
532
|
async function runRuntimeMatrix(runFlags, configPath, selectors, modes, modeSummaryTotal, fileSummaryTotal) {
|
|
382
533
|
const files = await resolveSelectedFiles(configPath, selectors);
|
|
383
534
|
if (!files.length) {
|
|
384
|
-
|
|
385
|
-
? selectors.join(", ")
|
|
386
|
-
: "configured input patterns";
|
|
387
|
-
throw new Error(`No test files matched: ${scope}`);
|
|
535
|
+
throw await buildNoTestFilesMatchedError(configPath, selectors);
|
|
388
536
|
}
|
|
389
537
|
const reporterSession = await createRunReporter(configPath);
|
|
390
538
|
const reporter = reporterSession.reporter;
|
|
@@ -409,6 +557,7 @@ async function runRuntimeMatrix(runFlags, configPath, selectors, modes, modeSumm
|
|
|
409
557
|
failed: false,
|
|
410
558
|
passed: false,
|
|
411
559
|
}));
|
|
560
|
+
const duplicateSpecBasenames = resolveDuplicateSpecBasenames(files);
|
|
412
561
|
for (let fileIndex = 0; fileIndex < files.length; fileIndex++) {
|
|
413
562
|
const file = files[fileIndex];
|
|
414
563
|
const fileName = path.basename(file);
|
|
@@ -420,7 +569,7 @@ async function runRuntimeMatrix(runFlags, configPath, selectors, modes, modeSumm
|
|
|
420
569
|
for (let i = 0; i < modes.length; i++) {
|
|
421
570
|
const modeName = modes[i];
|
|
422
571
|
try {
|
|
423
|
-
const artifactKey =
|
|
572
|
+
const artifactKey = resolvePerFileArtifactKey(file, duplicateSpecBasenames);
|
|
424
573
|
const result = await run(runFlags, configPath, [file], false, {
|
|
425
574
|
reporter: silentReporter,
|
|
426
575
|
reporterKind: "default",
|
|
@@ -490,10 +639,7 @@ async function runTestModes(runFlags, configPath, selectors, modes, buildFeature
|
|
|
490
639
|
async function runTestMatrix(runFlags, configPath, selectors, modes, buildFeatureToggles, modeSummaryTotal, fileSummaryTotal) {
|
|
491
640
|
const files = await resolveSelectedFiles(configPath, selectors);
|
|
492
641
|
if (!files.length) {
|
|
493
|
-
|
|
494
|
-
? selectors.join(", ")
|
|
495
|
-
: "configured input patterns";
|
|
496
|
-
throw new Error(`No test files matched: ${scope}`);
|
|
642
|
+
throw await buildNoTestFilesMatchedError(configPath, selectors);
|
|
497
643
|
}
|
|
498
644
|
const reporterSession = await createRunReporter(configPath);
|
|
499
645
|
const reporter = reporterSession.reporter;
|
|
@@ -518,6 +664,7 @@ async function runTestMatrix(runFlags, configPath, selectors, modes, buildFeatur
|
|
|
518
664
|
failed: false,
|
|
519
665
|
passed: false,
|
|
520
666
|
}));
|
|
667
|
+
const duplicateSpecBasenames = resolveDuplicateSpecBasenames(files);
|
|
521
668
|
for (let fileIndex = 0; fileIndex < files.length; fileIndex++) {
|
|
522
669
|
const file = files[fileIndex];
|
|
523
670
|
const fileName = path.basename(file);
|
|
@@ -530,7 +677,7 @@ async function runTestMatrix(runFlags, configPath, selectors, modes, buildFeatur
|
|
|
530
677
|
const modeName = modes[i];
|
|
531
678
|
try {
|
|
532
679
|
await build(configPath, [file], modeName, buildFeatureToggles);
|
|
533
|
-
const artifactKey =
|
|
680
|
+
const artifactKey = resolvePerFileArtifactKey(file, duplicateSpecBasenames);
|
|
534
681
|
const result = await run(runFlags, configPath, [file], false, {
|
|
535
682
|
reporter: silentReporter,
|
|
536
683
|
reporterKind: "default",
|
|
@@ -590,7 +737,9 @@ function renderMatrixFileResult(file, modes, results, modeTimes, liveMatrix, sho
|
|
|
590
737
|
: chalk.bgBlackBright.white(" SKIP ");
|
|
591
738
|
const avg = formatMatrixAverageTime(results);
|
|
592
739
|
const timingText = showPerModeTimes ? modeTimes.join(",") : avg;
|
|
593
|
-
const suffix = showPerModeTimes
|
|
740
|
+
const suffix = showPerModeTimes
|
|
741
|
+
? ` ${chalk.dim(`(${modes.join(",")})`)}`
|
|
742
|
+
: "";
|
|
594
743
|
const line = `${badge} ${file} ${chalk.dim(timingText)}${suffix}`;
|
|
595
744
|
if (liveMatrix)
|
|
596
745
|
clearLiveLine();
|
|
@@ -616,7 +765,9 @@ function renderMatrixLiveLine(file, modes, modeTimes, showPerModeTimes) {
|
|
|
616
765
|
if (!canRewriteStdout())
|
|
617
766
|
return;
|
|
618
767
|
const timingText = showPerModeTimes ? modeTimes.join(",") : "...";
|
|
619
|
-
const suffix = showPerModeTimes
|
|
768
|
+
const suffix = showPerModeTimes
|
|
769
|
+
? ` ${chalk.dim(`(${modes.join(",")})`)}`
|
|
770
|
+
: "";
|
|
620
771
|
const line = `${chalk.bgBlackBright.white(" .... ")} ${file} ${chalk.dim(timingText)}${suffix}`;
|
|
621
772
|
process.stdout.write(`\r\x1b[2K${line}`);
|
|
622
773
|
}
|
|
@@ -629,7 +780,9 @@ function formatMatrixAverageTime(results) {
|
|
|
629
780
|
return "0.0ms";
|
|
630
781
|
let total = 0;
|
|
631
782
|
for (const result of results) {
|
|
632
|
-
total += Number.isFinite(result.stats.time)
|
|
783
|
+
total += Number.isFinite(result.stats.time)
|
|
784
|
+
? Math.max(0, result.stats.time)
|
|
785
|
+
: 0;
|
|
633
786
|
}
|
|
634
787
|
return `${(total / results.length).toFixed(1)}ms`;
|
|
635
788
|
}
|
|
@@ -711,16 +864,105 @@ function resolveExecutionModes(configPath, selectedModes) {
|
|
|
711
864
|
return [undefined];
|
|
712
865
|
return configuredModes;
|
|
713
866
|
}
|
|
714
|
-
async function resolveSelectedFiles(configPath, selectors) {
|
|
867
|
+
async function resolveSelectedFiles(configPath, selectors, warn = true) {
|
|
715
868
|
const resolvedConfigPath = configPath ?? path.join(process.cwd(), "./as-test.config.json");
|
|
716
|
-
const config = loadConfig(resolvedConfigPath,
|
|
869
|
+
const config = loadConfig(resolvedConfigPath, warn);
|
|
717
870
|
const patterns = resolveInputPatterns(config.input, selectors);
|
|
718
871
|
const matches = await glob(patterns);
|
|
719
872
|
const specs = matches.filter((file) => file.endsWith(".spec.ts"));
|
|
720
873
|
return [...new Set(specs)].sort((a, b) => a.localeCompare(b));
|
|
721
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
|
+
}
|
|
722
962
|
function resolveInputPatterns(configured, selectors) {
|
|
723
|
-
const configuredInputs = Array.isArray(configured)
|
|
963
|
+
const configuredInputs = Array.isArray(configured)
|
|
964
|
+
? configured
|
|
965
|
+
: [configured];
|
|
724
966
|
if (!selectors.length)
|
|
725
967
|
return configuredInputs;
|
|
726
968
|
const patterns = new Set();
|
|
@@ -770,6 +1012,121 @@ function isBareSuiteSelector(selector) {
|
|
|
770
1012
|
function stripSuiteSuffix(selector) {
|
|
771
1013
|
return selector.replace(/\.spec\.ts$/, "").replace(/\.ts$/, "");
|
|
772
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
|
+
}
|
|
773
1130
|
function aggregateRunResults(results) {
|
|
774
1131
|
const stats = {
|
|
775
1132
|
passedFiles: 0,
|
|
@@ -821,7 +1178,8 @@ function aggregateRunResults(results) {
|
|
|
821
1178
|
snapshotSummary.created += result.snapshotSummary.created;
|
|
822
1179
|
snapshotSummary.updated += result.snapshotSummary.updated;
|
|
823
1180
|
snapshotSummary.failed += result.snapshotSummary.failed;
|
|
824
|
-
coverageSummary.enabled =
|
|
1181
|
+
coverageSummary.enabled =
|
|
1182
|
+
coverageSummary.enabled || result.coverageSummary.enabled;
|
|
825
1183
|
coverageSummary.showPoints =
|
|
826
1184
|
coverageSummary.showPoints || result.coverageSummary.showPoints;
|
|
827
1185
|
for (const fileCoverage of result.coverageSummary.files) {
|