as-test 1.2.0 → 1.4.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 +60 -0
- package/README.md +1 -4
- package/as-test.config.schema.json +15 -0
- package/assembly/coverage.ts +22 -26
- package/assembly/index.ts +68 -47
- package/assembly/src/expectation.ts +154 -123
- package/assembly/src/fuzz.ts +10 -10
- package/assembly/src/log.ts +3 -3
- package/assembly/src/mode.ts +55 -0
- package/assembly/src/reflect.ts +122 -0
- package/assembly/src/stringify.ts +240 -0
- package/assembly/src/suite.ts +48 -27
- package/assembly/src/tests.ts +7 -7
- package/assembly/util/wipc.ts +2 -2
- package/bin/build-worker-pool.js +9 -0
- package/bin/build-worker.js +27 -3
- package/bin/commands/build-core.js +293 -86
- package/bin/commands/build.js +3 -1
- package/bin/commands/init-core.js +253 -8
- package/bin/commands/run-core.js +165 -41
- package/bin/commands/run.js +2 -1
- package/bin/commands/test.js +3 -2
- package/bin/dependency-graph.js +0 -0
- package/bin/index.js +592 -97
- package/bin/reporters/default.js +34 -0
- package/bin/types.js +7 -0
- package/bin/util.js +52 -0
- package/package.json +4 -8
- package/transform/lib/equals.js +388 -0
- package/transform/lib/index.js +28 -0
- package/transform/lib/log.js +3 -7
- package/transform/lib/types.js +4 -2
- package/transform/lib/transform.js +0 -502
|
@@ -7,6 +7,12 @@ import { getCliVersion } from "../util.js";
|
|
|
7
7
|
import { buildWebRunnerSource } from "./web-runner-source.js";
|
|
8
8
|
const TARGETS = ["wasi", "bindings", "web"];
|
|
9
9
|
const EXAMPLE_MODES = ["minimal", "full", "none"];
|
|
10
|
+
const FEATURE_KEYS = ["coverage", "tryAs"];
|
|
11
|
+
const FEATURE_LABELS = {
|
|
12
|
+
coverage: "coverage (runtime coverage points + report)",
|
|
13
|
+
tryAs:
|
|
14
|
+
"try-as (try/catch/finally + toThrow assertions + throwable rewriting)",
|
|
15
|
+
};
|
|
10
16
|
export async function init(rawArgs) {
|
|
11
17
|
const options = parseInitArgs(rawArgs);
|
|
12
18
|
const rl = options.yes
|
|
@@ -23,6 +29,10 @@ export async function init(rawArgs) {
|
|
|
23
29
|
target: options.target ?? "wasi",
|
|
24
30
|
example: options.example ?? "minimal",
|
|
25
31
|
fuzzExample: options.fuzzExample ?? false,
|
|
32
|
+
features: resolveFeatures(options.features, {
|
|
33
|
+
coverage: false,
|
|
34
|
+
tryAs: false,
|
|
35
|
+
}),
|
|
26
36
|
installDependenciesNow: options.install ?? false,
|
|
27
37
|
}
|
|
28
38
|
: await runInteractiveOnboarding(options, rl);
|
|
@@ -35,6 +45,7 @@ export async function init(rawArgs) {
|
|
|
35
45
|
answers.target,
|
|
36
46
|
answers.example,
|
|
37
47
|
answers.fuzzExample,
|
|
48
|
+
answers.features,
|
|
38
49
|
answers.installDependenciesNow,
|
|
39
50
|
);
|
|
40
51
|
if (!options.yes) {
|
|
@@ -49,6 +60,7 @@ export async function init(rawArgs) {
|
|
|
49
60
|
answers.target,
|
|
50
61
|
answers.example,
|
|
51
62
|
answers.fuzzExample,
|
|
63
|
+
answers.features,
|
|
52
64
|
options.force,
|
|
53
65
|
);
|
|
54
66
|
printSummary(summary);
|
|
@@ -67,6 +79,7 @@ export async function init(rawArgs) {
|
|
|
67
79
|
}
|
|
68
80
|
function parseInitArgs(rawArgs) {
|
|
69
81
|
const options = {
|
|
82
|
+
features: {},
|
|
70
83
|
yes: false,
|
|
71
84
|
force: false,
|
|
72
85
|
dir: ".",
|
|
@@ -95,6 +108,30 @@ function parseInitArgs(rawArgs) {
|
|
|
95
108
|
options.fuzzExample = false;
|
|
96
109
|
continue;
|
|
97
110
|
}
|
|
111
|
+
if (arg == "--enable" || arg == "--disable") {
|
|
112
|
+
const next = rawArgs[i + 1];
|
|
113
|
+
if (!next || next.startsWith("-")) {
|
|
114
|
+
throw new Error(`${arg} requires a value: coverage|try-as`);
|
|
115
|
+
}
|
|
116
|
+
for (const name of splitInitFeatureList(next)) {
|
|
117
|
+
applyInitFeatureToggle(options.features, name, arg == "--enable");
|
|
118
|
+
}
|
|
119
|
+
i++;
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
if (arg.startsWith("--enable=") || arg.startsWith("--disable=")) {
|
|
123
|
+
const eq = arg.indexOf("=");
|
|
124
|
+
const flag = arg.slice(0, eq);
|
|
125
|
+
const value = arg.slice(eq + 1);
|
|
126
|
+
const names = splitInitFeatureList(value);
|
|
127
|
+
if (!names.length) {
|
|
128
|
+
throw new Error(`${flag} requires a value: coverage|try-as`);
|
|
129
|
+
}
|
|
130
|
+
for (const name of names) {
|
|
131
|
+
applyInitFeatureToggle(options.features, name, flag == "--enable");
|
|
132
|
+
}
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
98
135
|
if (arg == "--target") {
|
|
99
136
|
const next = rawArgs[i + 1];
|
|
100
137
|
if (next && !next.startsWith("-")) {
|
|
@@ -158,11 +195,37 @@ function parseInitArgs(rawArgs) {
|
|
|
158
195
|
}
|
|
159
196
|
if (positional.length > 0) {
|
|
160
197
|
throw new Error(
|
|
161
|
-
`Unknown init argument(s): ${positional.join(", ")}. Usage: init [dir] [--target wasi|bindings|web] [--example minimal|full|none] [--fuzz-example|--no-fuzz-example] [--install] [--yes] [--force] [--dir <path>]`,
|
|
198
|
+
`Unknown init argument(s): ${positional.join(", ")}. Usage: init [dir] [--target wasi|bindings|web] [--example minimal|full|none] [--fuzz-example|--no-fuzz-example] [--enable coverage|try-as] [--disable coverage|try-as] [--install] [--yes] [--force] [--dir <path>]`,
|
|
162
199
|
);
|
|
163
200
|
}
|
|
164
201
|
return options;
|
|
165
202
|
}
|
|
203
|
+
function splitInitFeatureList(value) {
|
|
204
|
+
return value
|
|
205
|
+
.split(",")
|
|
206
|
+
.map((part) => part.trim())
|
|
207
|
+
.filter((part) => part.length > 0);
|
|
208
|
+
}
|
|
209
|
+
function applyInitFeatureToggle(out, rawFeature, enabled) {
|
|
210
|
+
const key = rawFeature.trim().toLowerCase();
|
|
211
|
+
if (key == "coverage") {
|
|
212
|
+
out.coverage = enabled;
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
if (key == "try-as" || key == "try_as" || key == "tryas") {
|
|
216
|
+
out.tryAs = enabled;
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
throw new Error(
|
|
220
|
+
`unknown feature "${rawFeature}". Supported features: coverage, try-as`,
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
function resolveFeatures(overrides, defaults) {
|
|
224
|
+
return {
|
|
225
|
+
coverage: overrides.coverage ?? defaults.coverage,
|
|
226
|
+
tryAs: overrides.tryAs ?? defaults.tryAs,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
166
229
|
async function runInteractiveOnboarding(options, face) {
|
|
167
230
|
printOnboardingIntro();
|
|
168
231
|
const acknowledged = await askYesNo(
|
|
@@ -231,6 +294,25 @@ async function runInteractiveOnboarding(options, face) {
|
|
|
231
294
|
if (options.target || onboardingMode == "quick") {
|
|
232
295
|
printPromptAndSelectionLine("Build target", target);
|
|
233
296
|
}
|
|
297
|
+
const featureDefaults = { coverage: false, tryAs: false };
|
|
298
|
+
const explicitFeatures =
|
|
299
|
+
options.features.coverage !== undefined ||
|
|
300
|
+
options.features.tryAs !== undefined;
|
|
301
|
+
const features =
|
|
302
|
+
explicitFeatures || onboardingMode == "quick"
|
|
303
|
+
? resolveFeatures(options.features, featureDefaults)
|
|
304
|
+
: await askMultiToggle(
|
|
305
|
+
"Features (↑/↓ to move, space to toggle, enter to confirm)",
|
|
306
|
+
FEATURE_KEYS.map((key) => ({
|
|
307
|
+
value: key,
|
|
308
|
+
label: FEATURE_LABELS[key],
|
|
309
|
+
})),
|
|
310
|
+
face,
|
|
311
|
+
resolveFeatures(options.features, featureDefaults),
|
|
312
|
+
);
|
|
313
|
+
if (explicitFeatures || onboardingMode == "quick") {
|
|
314
|
+
printPromptAndSelectionLine("Features", formatFeatureSelection(features));
|
|
315
|
+
}
|
|
234
316
|
const example =
|
|
235
317
|
options.example ??
|
|
236
318
|
(onboardingMode == "quick"
|
|
@@ -275,9 +357,16 @@ async function runInteractiveOnboarding(options, face) {
|
|
|
275
357
|
target,
|
|
276
358
|
example,
|
|
277
359
|
fuzzExample,
|
|
360
|
+
features,
|
|
278
361
|
installDependenciesNow,
|
|
279
362
|
};
|
|
280
363
|
}
|
|
364
|
+
function formatFeatureSelection(features) {
|
|
365
|
+
const labels = [];
|
|
366
|
+
if (features.coverage) labels.push("coverage");
|
|
367
|
+
if (features.tryAs) labels.push("try-as");
|
|
368
|
+
return labels.length ? labels.join(", ") : "none";
|
|
369
|
+
}
|
|
281
370
|
function printOnboardingHeader() {
|
|
282
371
|
// console.log(
|
|
283
372
|
// chalk.bold.cyan(
|
|
@@ -375,7 +464,7 @@ function isTarget(value) {
|
|
|
375
464
|
function isExampleMode(value) {
|
|
376
465
|
return EXAMPLE_MODES.includes(value);
|
|
377
466
|
}
|
|
378
|
-
function printPlan(root, target, example, fuzzExample, install) {
|
|
467
|
+
function printPlan(root, target, example, fuzzExample, features, install) {
|
|
379
468
|
const displayRoot = () => {
|
|
380
469
|
const rel = path.relative(process.cwd(), root).split(path.sep).join("/");
|
|
381
470
|
if (!rel || rel == ".") return "./";
|
|
@@ -483,6 +572,9 @@ function printPlan(root, target, example, fuzzExample, install) {
|
|
|
483
572
|
console.log(
|
|
484
573
|
"│" + chalk.dim(` - Fuzzer example: ${fuzzExample ? "yes" : "no"}`),
|
|
485
574
|
);
|
|
575
|
+
console.log(
|
|
576
|
+
"│" + chalk.dim(` - Features: ${formatFeatureSelection(features)}`),
|
|
577
|
+
);
|
|
486
578
|
console.log("│" + chalk.dim(` - Directory: ${displayRoot()}`));
|
|
487
579
|
console.log(
|
|
488
580
|
"│" + chalk.dim(` - Install dependencies: ${install ? "yes" : "no"}`),
|
|
@@ -494,7 +586,7 @@ function printPlan(root, target, example, fuzzExample, install) {
|
|
|
494
586
|
}
|
|
495
587
|
console.log("│");
|
|
496
588
|
}
|
|
497
|
-
function applyInit(root, target, example, fuzzExample, force) {
|
|
589
|
+
function applyInit(root, target, example, fuzzExample, features, force) {
|
|
498
590
|
const summary = {
|
|
499
591
|
created: [],
|
|
500
592
|
updated: [],
|
|
@@ -518,13 +610,16 @@ function applyInit(root, target, example, fuzzExample, force) {
|
|
|
518
610
|
summary,
|
|
519
611
|
"assembly/tsconfig.json",
|
|
520
612
|
);
|
|
613
|
+
const featuresArray = [];
|
|
614
|
+
if (features.tryAs) featuresArray.push("try-as");
|
|
521
615
|
const configPath = path.join(root, "as-test.config.json");
|
|
522
616
|
const config = {
|
|
523
617
|
$schema: "node_modules/as-test/as-test.config.schema.json",
|
|
524
618
|
input: ["assembly/__tests__/*.spec.ts"],
|
|
525
619
|
output: ".as-test/",
|
|
526
620
|
config: "none",
|
|
527
|
-
coverage:
|
|
621
|
+
coverage: features.coverage,
|
|
622
|
+
features: featuresArray,
|
|
528
623
|
env: {},
|
|
529
624
|
...(fuzzExample
|
|
530
625
|
? {
|
|
@@ -650,15 +745,15 @@ function applyInit(root, target, example, fuzzExample, force) {
|
|
|
650
745
|
if (!devDependencies["as-test"]) {
|
|
651
746
|
devDependencies["as-test"] = "^" + getCliVersion();
|
|
652
747
|
}
|
|
653
|
-
if (!hasDependency(pkg, "json-as")) {
|
|
654
|
-
devDependencies["json-as"] = "^1.3.5";
|
|
655
|
-
}
|
|
656
748
|
if (!hasDependency(pkg, "assemblyscript")) {
|
|
657
749
|
devDependencies["assemblyscript"] = "^0.28.9";
|
|
658
750
|
}
|
|
659
751
|
if (target == "wasi" && !devDependencies["@assemblyscript/wasi-shim"]) {
|
|
660
752
|
devDependencies["@assemblyscript/wasi-shim"] = "^0.1.0";
|
|
661
753
|
}
|
|
754
|
+
if (features.tryAs && !hasDependency(pkg, "try-as")) {
|
|
755
|
+
devDependencies["try-as"] = "^1.1.0";
|
|
756
|
+
}
|
|
662
757
|
if (target == "bindings" && !pkg.type) {
|
|
663
758
|
pkg.type = "module";
|
|
664
759
|
}
|
|
@@ -814,6 +909,21 @@ async function askMenuChoice(label, choices, face, fallback) {
|
|
|
814
909
|
}
|
|
815
910
|
return askMenuChoiceWithArrows(label, choices, face, fallbackValue);
|
|
816
911
|
}
|
|
912
|
+
async function askMultiToggle(label, choices, face, initial) {
|
|
913
|
+
if (!face) return { ...initial };
|
|
914
|
+
if (canUseArrowMenu(face)) {
|
|
915
|
+
return askMultiToggleWithArrows(label, choices, face, initial);
|
|
916
|
+
}
|
|
917
|
+
const result = { ...initial };
|
|
918
|
+
for (const choice of choices) {
|
|
919
|
+
result[choice.value] = await askYesNo(
|
|
920
|
+
`${label} — enable ${choice.label}?`,
|
|
921
|
+
face,
|
|
922
|
+
initial[choice.value],
|
|
923
|
+
);
|
|
924
|
+
}
|
|
925
|
+
return result;
|
|
926
|
+
}
|
|
817
927
|
async function askYesNo(label, face, fallback) {
|
|
818
928
|
if (!face) return fallback;
|
|
819
929
|
if (canUseArrowMenu(face)) {
|
|
@@ -887,7 +997,7 @@ async function askMenuChoiceWithArrows(label, choices, face, fallback) {
|
|
|
887
997
|
}
|
|
888
998
|
const totalLineCount = Math.max(lines.length, renderedLineCount);
|
|
889
999
|
for (let i = 0; i < totalLineCount; i++) {
|
|
890
|
-
process.stdout.write("\x1b[2K");
|
|
1000
|
+
process.stdout.write("\r\x1b[2K");
|
|
891
1001
|
if (i < lines.length) {
|
|
892
1002
|
process.stdout.write(lines[i]);
|
|
893
1003
|
}
|
|
@@ -987,6 +1097,141 @@ async function askMenuChoiceWithArrows(label, choices, face, fallback) {
|
|
|
987
1097
|
writeLines(menuLines());
|
|
988
1098
|
});
|
|
989
1099
|
}
|
|
1100
|
+
async function askMultiToggleWithArrows(label, choices, face, initial) {
|
|
1101
|
+
const stdin = process.stdin;
|
|
1102
|
+
const stdout = process.stdout;
|
|
1103
|
+
const selected = { ...initial };
|
|
1104
|
+
let cursorIndex = 0;
|
|
1105
|
+
let renderedLineCount = 0;
|
|
1106
|
+
const previousRawMode = Boolean(stdin.isRaw);
|
|
1107
|
+
const lineWidth = Math.max(20, (stdout.columns ?? 80) - 2);
|
|
1108
|
+
const clamp = (value, max) => {
|
|
1109
|
+
if (value.length <= max) return value;
|
|
1110
|
+
if (max <= 1) return value.slice(0, max);
|
|
1111
|
+
return `${value.slice(0, max - 1)}…`;
|
|
1112
|
+
};
|
|
1113
|
+
const titleLine = () =>
|
|
1114
|
+
chalk.bold.blue(`◆ ${clamp(label, Math.max(8, lineWidth - 3))}`);
|
|
1115
|
+
const menuLines = () => {
|
|
1116
|
+
const lines = [titleLine()];
|
|
1117
|
+
for (let i = 0; i < choices.length; i++) {
|
|
1118
|
+
const choice = choices[i];
|
|
1119
|
+
const isOn = Boolean(selected[choice.value]);
|
|
1120
|
+
const cursor = i == cursorIndex ? chalk.blue("›") : " ";
|
|
1121
|
+
const marker = isOn ? chalk.blue("●") : chalk.dim("○");
|
|
1122
|
+
const text = clamp(choice.label, Math.max(8, lineWidth - 6));
|
|
1123
|
+
const painted = i == cursorIndex ? chalk.bold(text) : text;
|
|
1124
|
+
lines.push(`│ ${cursor} ${marker} ${painted}`);
|
|
1125
|
+
}
|
|
1126
|
+
lines.push("│");
|
|
1127
|
+
return lines;
|
|
1128
|
+
};
|
|
1129
|
+
const collapsedLines = () => {
|
|
1130
|
+
const enabled = choices
|
|
1131
|
+
.filter((choice) => selected[choice.value])
|
|
1132
|
+
.map((choice) => choice.value);
|
|
1133
|
+
const summary = enabled.length ? enabled.join(", ") : "none";
|
|
1134
|
+
return [`│ ${chalk.gray(clamp(summary, Math.max(8, lineWidth - 4)))}`];
|
|
1135
|
+
};
|
|
1136
|
+
const writeLines = (lines) => {
|
|
1137
|
+
if (renderedLineCount > 0) {
|
|
1138
|
+
process.stdout.write(`\x1b[${renderedLineCount}A`);
|
|
1139
|
+
}
|
|
1140
|
+
const totalLineCount = Math.max(lines.length, renderedLineCount);
|
|
1141
|
+
for (let i = 0; i < totalLineCount; i++) {
|
|
1142
|
+
process.stdout.write("\r\x1b[2K");
|
|
1143
|
+
if (i < lines.length) {
|
|
1144
|
+
process.stdout.write(lines[i]);
|
|
1145
|
+
}
|
|
1146
|
+
process.stdout.write("\n");
|
|
1147
|
+
}
|
|
1148
|
+
renderedLineCount = lines.length;
|
|
1149
|
+
};
|
|
1150
|
+
const collapseInPlace = () => {
|
|
1151
|
+
const lines = collapsedLines();
|
|
1152
|
+
if (renderedLineCount > 0) {
|
|
1153
|
+
process.stdout.write(`\x1b[${renderedLineCount}A`);
|
|
1154
|
+
}
|
|
1155
|
+
const totalLineCount = Math.max(renderedLineCount, lines.length);
|
|
1156
|
+
for (let i = 0; i < totalLineCount; i++) {
|
|
1157
|
+
process.stdout.write("\r\x1b[2K");
|
|
1158
|
+
if (i < lines.length) {
|
|
1159
|
+
process.stdout.write(lines[i]);
|
|
1160
|
+
}
|
|
1161
|
+
process.stdout.write("\n");
|
|
1162
|
+
}
|
|
1163
|
+
const extraLines = totalLineCount - lines.length;
|
|
1164
|
+
if (extraLines > 0) {
|
|
1165
|
+
process.stdout.write(`\x1b[${extraLines}A`);
|
|
1166
|
+
}
|
|
1167
|
+
renderedLineCount = 0;
|
|
1168
|
+
};
|
|
1169
|
+
return new Promise((resolve, reject) => {
|
|
1170
|
+
let settled = false;
|
|
1171
|
+
const cleanup = () => {
|
|
1172
|
+
stdin.off("data", onData);
|
|
1173
|
+
if (stdin.isTTY) {
|
|
1174
|
+
stdin.setRawMode(previousRawMode);
|
|
1175
|
+
}
|
|
1176
|
+
const isClosed = Boolean(face.closed);
|
|
1177
|
+
if (!isClosed) {
|
|
1178
|
+
try {
|
|
1179
|
+
face.resume();
|
|
1180
|
+
} catch {
|
|
1181
|
+
// noop: readline may already be closed during shutdown/cancel paths.
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
};
|
|
1185
|
+
const finish = () => {
|
|
1186
|
+
if (settled) return;
|
|
1187
|
+
settled = true;
|
|
1188
|
+
collapseInPlace();
|
|
1189
|
+
cleanup();
|
|
1190
|
+
resolve(selected);
|
|
1191
|
+
};
|
|
1192
|
+
const fail = (error) => {
|
|
1193
|
+
if (settled) return;
|
|
1194
|
+
settled = true;
|
|
1195
|
+
cleanup();
|
|
1196
|
+
reject(error);
|
|
1197
|
+
};
|
|
1198
|
+
const onData = (chunk) => {
|
|
1199
|
+
const input = typeof chunk == "string" ? chunk : chunk.toString("utf8");
|
|
1200
|
+
if (!input.length) return;
|
|
1201
|
+
if (input == "\u0003") {
|
|
1202
|
+
fail(new Error(chalk.bold.red("◆ Cancelled")));
|
|
1203
|
+
return;
|
|
1204
|
+
}
|
|
1205
|
+
if (input == "\x1b[A" || input == "\x1bOA") {
|
|
1206
|
+
cursorIndex = (cursorIndex - 1 + choices.length) % choices.length;
|
|
1207
|
+
writeLines(menuLines());
|
|
1208
|
+
return;
|
|
1209
|
+
}
|
|
1210
|
+
if (input == "\x1b[B" || input == "\x1bOB") {
|
|
1211
|
+
cursorIndex = (cursorIndex + 1) % choices.length;
|
|
1212
|
+
writeLines(menuLines());
|
|
1213
|
+
return;
|
|
1214
|
+
}
|
|
1215
|
+
if (input == " ") {
|
|
1216
|
+
const key = choices[cursorIndex].value;
|
|
1217
|
+
selected[key] = !selected[key];
|
|
1218
|
+
writeLines(menuLines());
|
|
1219
|
+
return;
|
|
1220
|
+
}
|
|
1221
|
+
if (input == "\r" || input == "\n") {
|
|
1222
|
+
finish();
|
|
1223
|
+
return;
|
|
1224
|
+
}
|
|
1225
|
+
};
|
|
1226
|
+
face.pause();
|
|
1227
|
+
if (stdin.isTTY) {
|
|
1228
|
+
stdin.setRawMode(true);
|
|
1229
|
+
}
|
|
1230
|
+
stdin.resume();
|
|
1231
|
+
stdin.on("data", onData);
|
|
1232
|
+
writeLines(menuLines());
|
|
1233
|
+
});
|
|
1234
|
+
}
|
|
990
1235
|
function installDependencies(root) {
|
|
991
1236
|
const install = resolveInstallCommand(root);
|
|
992
1237
|
console.log(
|
package/bin/commands/run-core.js
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
getExec,
|
|
10
10
|
loadConfig,
|
|
11
11
|
resolveArtifactPath,
|
|
12
|
+
resolveSnapshotPath,
|
|
12
13
|
resolveSpecRelativePath,
|
|
13
14
|
tokenizeCommand,
|
|
14
15
|
} from "../util.js";
|
|
@@ -33,12 +34,7 @@ class SnapshotStore {
|
|
|
33
34
|
this.failed = 0;
|
|
34
35
|
this.warnedMissing = new Set();
|
|
35
36
|
this.specBasename = path.basename(specFile);
|
|
36
|
-
|
|
37
|
-
const relative = resolveSpecRelativePath(specFile, inputPatterns).replace(
|
|
38
|
-
/\.ts$/i,
|
|
39
|
-
".snap",
|
|
40
|
-
);
|
|
41
|
-
this.filePath = path.join(dir, relative);
|
|
37
|
+
this.filePath = resolveSnapshotPath(specFile, snapshotDir, inputPatterns);
|
|
42
38
|
const sourcePath = existsSync(this.filePath) ? this.filePath : null;
|
|
43
39
|
const loaded = sourcePath
|
|
44
40
|
? readSnapshotFile(sourcePath, specFile)
|
|
@@ -543,7 +539,7 @@ function collectReadableLogs(suites) {
|
|
|
543
539
|
const suiteAny = suite;
|
|
544
540
|
const logs = Array.isArray(suiteAny.logs) ? suiteAny.logs : [];
|
|
545
541
|
for (const log of logs) {
|
|
546
|
-
const value = String(log.value ?? log.message ?? "");
|
|
542
|
+
const value = String(log.text ?? log.value ?? log.message ?? "");
|
|
547
543
|
if (value.length) out.push(value);
|
|
548
544
|
}
|
|
549
545
|
const childSuites = Array.isArray(suiteAny.suites) ? suiteAny.suites : [];
|
|
@@ -551,6 +547,133 @@ function collectReadableLogs(suites) {
|
|
|
551
547
|
}
|
|
552
548
|
return out;
|
|
553
549
|
}
|
|
550
|
+
// Walk a suite tree, accumulating each suite's `log()` output keyed by the
|
|
551
|
+
// describe/test description path it was emitted under.
|
|
552
|
+
function walkSuiteLogs(suites, pathParts, out) {
|
|
553
|
+
for (const suite of suites) {
|
|
554
|
+
const suiteAny = suite;
|
|
555
|
+
const description = String(suiteAny.description ?? "");
|
|
556
|
+
const nextPath = description.length
|
|
557
|
+
? [...pathParts, description]
|
|
558
|
+
: pathParts;
|
|
559
|
+
const logs = Array.isArray(suiteAny.logs) ? suiteAny.logs : [];
|
|
560
|
+
const lines = logs.map((log) =>
|
|
561
|
+
String(log.text ?? log.value ?? log.message ?? ""),
|
|
562
|
+
);
|
|
563
|
+
if (lines.length) out.push({ path: nextPath, lines });
|
|
564
|
+
const childSuites = Array.isArray(suiteAny.suites) ? suiteAny.suites : [];
|
|
565
|
+
walkSuiteLogs(childSuites, nextPath, out);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
// Group every captured log across all file reports into a per-spec tree (one
|
|
569
|
+
// entry per `log()` call). Feeds the process-wide collector that backs the
|
|
570
|
+
// aggregated `latest.log` and the `--show-logs` dump.
|
|
571
|
+
function collectGroupedLogs(reports) {
|
|
572
|
+
let count = 0;
|
|
573
|
+
const groups = [];
|
|
574
|
+
for (const report of reports) {
|
|
575
|
+
const reportAny = report;
|
|
576
|
+
const suites = Array.isArray(reportAny.suites) ? reportAny.suites : [];
|
|
577
|
+
const entries = [];
|
|
578
|
+
walkSuiteLogs(suites, [], entries);
|
|
579
|
+
if (!entries.length) continue;
|
|
580
|
+
for (const entry of entries) count += entry.lines.length;
|
|
581
|
+
groups.push({ file: String(reportAny.file ?? "unknown"), entries });
|
|
582
|
+
}
|
|
583
|
+
return { count, groups };
|
|
584
|
+
}
|
|
585
|
+
// Process-lived collector backing the aggregated `latest.log`. Keyed by spec
|
|
586
|
+
// file, then by mode label, holding that mode's flat list of `log()` lines.
|
|
587
|
+
// Persisting across run() calls lets a multi-mode run accumulate every mode
|
|
588
|
+
// before the file is rendered, so identical output can be de-duplicated.
|
|
589
|
+
const collectedLogsBySpec = new Map();
|
|
590
|
+
// Clear the collector. Useful for watch mode, where each cycle should start
|
|
591
|
+
// fresh rather than accumulate stale specs.
|
|
592
|
+
export function resetCollectedLogs() {
|
|
593
|
+
collectedLogsBySpec.clear();
|
|
594
|
+
}
|
|
595
|
+
function recordModeLogs(modeLabel, groups) {
|
|
596
|
+
for (const group of groups) {
|
|
597
|
+
const lines = [];
|
|
598
|
+
for (const entry of group.entries) lines.push(...entry.lines);
|
|
599
|
+
if (!lines.length) continue;
|
|
600
|
+
let byMode = collectedLogsBySpec.get(group.file);
|
|
601
|
+
if (!byMode) {
|
|
602
|
+
byMode = new Map();
|
|
603
|
+
collectedLogsBySpec.set(group.file, byMode);
|
|
604
|
+
}
|
|
605
|
+
byMode.set(modeLabel, lines);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
// Render the collected logs as the `latest.log` body. Within a spec, modes that
|
|
609
|
+
// produced byte-identical output are merged into one block tagged with every
|
|
610
|
+
// mode that emitted it:
|
|
611
|
+
//
|
|
612
|
+
// [LOG] log.spec.ts (node:bindings, node:wasi):
|
|
613
|
+
//
|
|
614
|
+
// {"a":1}
|
|
615
|
+
// ...
|
|
616
|
+
//
|
|
617
|
+
// `count` is the number of de-duplicated `log()` calls (one entry per call —
|
|
618
|
+
// stringify escapes newlines, so a call is a single line), not counting the
|
|
619
|
+
// same call again per mode.
|
|
620
|
+
function renderCollectedLogs() {
|
|
621
|
+
const blocks = [];
|
|
622
|
+
let count = 0;
|
|
623
|
+
const specs = [...collectedLogsBySpec.keys()].sort((a, b) =>
|
|
624
|
+
a.localeCompare(b),
|
|
625
|
+
);
|
|
626
|
+
for (const spec of specs) {
|
|
627
|
+
const byMode = collectedLogsBySpec.get(spec);
|
|
628
|
+
// Group modes by identical content so duplicate output collapses into one
|
|
629
|
+
// block; `calls` is that block's log() count, tallied once regardless of
|
|
630
|
+
// how many modes produced it.
|
|
631
|
+
const byContent = new Map();
|
|
632
|
+
for (const [mode, lines] of byMode) {
|
|
633
|
+
const content = lines.join("\n");
|
|
634
|
+
const existing = byContent.get(content);
|
|
635
|
+
if (existing) existing.modes.push(mode);
|
|
636
|
+
else byContent.set(content, { modes: [mode], calls: lines.length });
|
|
637
|
+
}
|
|
638
|
+
for (const [content, { modes, calls }] of byContent) {
|
|
639
|
+
const named = modes.filter((mode) => mode !== "default").sort();
|
|
640
|
+
const suffix = named.length ? ` (${named.join(", ")})` : "";
|
|
641
|
+
count += calls;
|
|
642
|
+
blocks.push(
|
|
643
|
+
`[LOG] ${formatSpecDisplayPath(spec)}${suffix}:\n\n${content}`,
|
|
644
|
+
);
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
return { text: blocks.length ? blocks.join("\n\n") + "\n" : "", count };
|
|
648
|
+
}
|
|
649
|
+
// Render the collector and (re)write the single aggregated `latest.log` at the
|
|
650
|
+
// base (un-mode-qualified) logs dir, so every mode shares one file. Returns the
|
|
651
|
+
// resulting LogSummary. Called by run() after recording its own logs, so the
|
|
652
|
+
// last run() of a multi-mode pass leaves a file covering — and de-duplicating —
|
|
653
|
+
// every mode.
|
|
654
|
+
function flushLatestLog(baseLogsDir) {
|
|
655
|
+
const rendered = renderCollectedLogs();
|
|
656
|
+
if (rendered.count <= 0)
|
|
657
|
+
return { count: 0, file: null, groups: [], text: "" };
|
|
658
|
+
if (!baseLogsDir || baseLogsDir === "none") {
|
|
659
|
+
return {
|
|
660
|
+
count: rendered.count,
|
|
661
|
+
file: null,
|
|
662
|
+
groups: [],
|
|
663
|
+
text: rendered.text,
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
const logRoot = path.join(process.cwd(), baseLogsDir);
|
|
667
|
+
if (!existsSync(logRoot)) mkdirSync(logRoot, { recursive: true });
|
|
668
|
+
const latestLogPath = path.join(logRoot, "latest.log");
|
|
669
|
+
writeFileSync(latestLogPath, rendered.text);
|
|
670
|
+
return {
|
|
671
|
+
count: rendered.count,
|
|
672
|
+
file: path.relative(process.cwd(), latestLogPath) || latestLogPath,
|
|
673
|
+
groups: [],
|
|
674
|
+
text: rendered.text,
|
|
675
|
+
};
|
|
676
|
+
}
|
|
554
677
|
export async function run(
|
|
555
678
|
flags = {},
|
|
556
679
|
configPath = DEFAULT_CONFIG_PATH,
|
|
@@ -766,6 +889,7 @@ export async function run(
|
|
|
766
889
|
} finally {
|
|
767
890
|
await ownedWebSession?.close();
|
|
768
891
|
}
|
|
892
|
+
const groupedLogs = collectGroupedLogs(reports);
|
|
769
893
|
if (config.logs && config.logs != "none") {
|
|
770
894
|
const logRoot = path.join(process.cwd(), config.logs);
|
|
771
895
|
if (!existsSync(logRoot)) {
|
|
@@ -786,6 +910,14 @@ export async function run(
|
|
|
786
910
|
);
|
|
787
911
|
}
|
|
788
912
|
}
|
|
913
|
+
// Record this run's logs (tagged with its mode) into the process-wide
|
|
914
|
+
// collector, then rewrite the single aggregated `latest.log` covering every
|
|
915
|
+
// mode seen so far. The collector persists across run() calls, so the last
|
|
916
|
+
// run() of a multi-mode pass produces the complete, de-duplicated file. The
|
|
917
|
+
// file lives at the base (un-mode-qualified) logs dir — `loadedConfig.logs`
|
|
918
|
+
// before `applyMode` appended the per-mode subdirectory.
|
|
919
|
+
recordModeLogs(options.modeName ?? "default", groupedLogs.groups);
|
|
920
|
+
const logSummary = flushLatestLog(loadedConfig.logs);
|
|
789
921
|
const stats = collectRunStats(reports);
|
|
790
922
|
if (options.fileSummaryTotal != undefined) {
|
|
791
923
|
applyConfiguredFileTotalToStats(stats, options.fileSummaryTotal);
|
|
@@ -824,6 +956,8 @@ export async function run(
|
|
|
824
956
|
showCoverage,
|
|
825
957
|
showCoverageAll: Boolean(flags.showCoverageAll),
|
|
826
958
|
verbose: Boolean(flags.verbose),
|
|
959
|
+
showLogs: Boolean(flags.showLogs),
|
|
960
|
+
logSummary,
|
|
827
961
|
buildTime,
|
|
828
962
|
snapshotSummary,
|
|
829
963
|
coverageSummary,
|
|
@@ -849,6 +983,7 @@ export async function run(
|
|
|
849
983
|
snapshotSummary,
|
|
850
984
|
coverageSummary,
|
|
851
985
|
reports,
|
|
986
|
+
logSummary,
|
|
852
987
|
};
|
|
853
988
|
}
|
|
854
989
|
function applyConfiguredFileTotalToStats(stats, fileSummaryTotal) {
|
|
@@ -2593,10 +2728,27 @@ function readFileReport(stats, fileReport) {
|
|
|
2593
2728
|
const buildCommand = String(fileReportAny.buildCommand ?? "");
|
|
2594
2729
|
let fileVerdict = "none";
|
|
2595
2730
|
for (const suite of suites) {
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2731
|
+
const suiteVerdict = readSuite(
|
|
2732
|
+
stats,
|
|
2733
|
+
suite,
|
|
2734
|
+
file,
|
|
2735
|
+
modeName,
|
|
2736
|
+
runCommand,
|
|
2737
|
+
buildCommand,
|
|
2599
2738
|
);
|
|
2739
|
+
fileVerdict = mergeVerdict(fileVerdict, suiteVerdict);
|
|
2740
|
+
// Record each failed top-level suite once. The failure summary recurses into
|
|
2741
|
+
// it to find every failed assertion (so nested failures aren't pushed again,
|
|
2742
|
+
// and a top-level it()/test() failure is captured too).
|
|
2743
|
+
if (suiteVerdict == "fail") {
|
|
2744
|
+
stats.failedEntries.push({
|
|
2745
|
+
...suite,
|
|
2746
|
+
file,
|
|
2747
|
+
modeName,
|
|
2748
|
+
runCommand,
|
|
2749
|
+
buildCommand,
|
|
2750
|
+
});
|
|
2751
|
+
}
|
|
2600
2752
|
}
|
|
2601
2753
|
if (fileVerdict == "fail") {
|
|
2602
2754
|
stats.failedFiles++;
|
|
@@ -2608,7 +2760,6 @@ function readFileReport(stats, fileReport) {
|
|
|
2608
2760
|
}
|
|
2609
2761
|
function readSuite(stats, suite, file, modeName, runCommand, buildCommand) {
|
|
2610
2762
|
const suiteAny = suite;
|
|
2611
|
-
const kind = String(suiteAny.kind ?? "");
|
|
2612
2763
|
let verdict = normalizeVerdict(suiteAny.verdict);
|
|
2613
2764
|
const time = suiteAny.time;
|
|
2614
2765
|
const start = Number(time?.start ?? 0);
|
|
@@ -2633,27 +2784,11 @@ function readSuite(stats, suite, file, modeName, runCommand, buildCommand) {
|
|
|
2633
2784
|
stats.skippedTests++;
|
|
2634
2785
|
}
|
|
2635
2786
|
}
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
stats.failedTests++;
|
|
2640
|
-
} else if (verdict == "ok") {
|
|
2641
|
-
stats.passedTests++;
|
|
2642
|
-
} else if (verdict == "skip") {
|
|
2643
|
-
stats.skippedTests++;
|
|
2644
|
-
}
|
|
2645
|
-
}
|
|
2646
|
-
return verdict;
|
|
2647
|
-
}
|
|
2787
|
+
// Every grouping block — describe, test, it, only and their skip variants —
|
|
2788
|
+
// is a suite; the expect() assertions counted above are the tests. (Failed
|
|
2789
|
+
// entries for the summary are collected per top-level suite in readFileReport.)
|
|
2648
2790
|
if (verdict == "fail") {
|
|
2649
2791
|
stats.failedSuites++;
|
|
2650
|
-
stats.failedEntries.push({
|
|
2651
|
-
...suiteAny,
|
|
2652
|
-
file,
|
|
2653
|
-
modeName,
|
|
2654
|
-
runCommand,
|
|
2655
|
-
buildCommand,
|
|
2656
|
-
});
|
|
2657
2792
|
} else if (verdict == "ok") {
|
|
2658
2793
|
stats.passedSuites++;
|
|
2659
2794
|
} else {
|
|
@@ -2661,17 +2796,6 @@ function readSuite(stats, suite, file, modeName, runCommand, buildCommand) {
|
|
|
2661
2796
|
}
|
|
2662
2797
|
return verdict;
|
|
2663
2798
|
}
|
|
2664
|
-
function isTestCaseSuiteKind(kind) {
|
|
2665
|
-
return (
|
|
2666
|
-
kind == "test" ||
|
|
2667
|
-
kind == "it" ||
|
|
2668
|
-
kind == "only" ||
|
|
2669
|
-
kind == "xtest" ||
|
|
2670
|
-
kind == "xit" ||
|
|
2671
|
-
kind == "xonly" ||
|
|
2672
|
-
kind == "todo"
|
|
2673
|
-
);
|
|
2674
|
-
}
|
|
2675
2799
|
function normalizeVerdict(value) {
|
|
2676
2800
|
const verdict = String(value ?? "none");
|
|
2677
2801
|
if (verdict == "fail") return "fail";
|
package/bin/commands/run.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { createRunReporter, run } from "./run-core.js";
|
|
1
|
+
export { createRunReporter, resetCollectedLogs, run } from "./run-core.js";
|
|
2
2
|
export async function executeRunCommand(
|
|
3
3
|
rawArgs,
|
|
4
4
|
flags,
|
|
@@ -19,6 +19,7 @@ export async function executeRunCommand(
|
|
|
19
19
|
showCoverage: showCoverageMode != undefined,
|
|
20
20
|
showCoverageAll: showCoverageMode == "all",
|
|
21
21
|
verbose: flags.includes("--verbose"),
|
|
22
|
+
showLogs: flags.includes("--show-logs"),
|
|
22
23
|
...deps.resolveParallelJobs(rawArgs, "run"),
|
|
23
24
|
coverage: featureToggles.coverage,
|
|
24
25
|
browser: deps.resolveBrowserOverride(rawArgs, "run"),
|