as-test 1.0.3 → 1.0.5
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 +15 -1
- package/README.md +17 -0
- package/bin/commands/build-core.js +57 -0
- package/bin/commands/build.js +17 -2
- package/bin/commands/run-core.js +130 -27
- package/bin/commands/run.js +1 -0
- package/bin/commands/test.js +1 -0
- package/bin/index.js +213 -42
- package/bin/reporters/default.js +22 -1
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,20 @@
|
|
|
1
1
|
# Change Log
|
|
2
2
|
|
|
3
|
-
## 2026-03-
|
|
3
|
+
## 2026-03-30 - v1.0.5
|
|
4
|
+
|
|
5
|
+
### CLI
|
|
6
|
+
|
|
7
|
+
- fix: preserve selectors passed after `--parallel` so commands like `ast test --parallel math` still target the requested suite.
|
|
8
|
+
- fix: treat uncaught runtime stderr / missing report payloads as normal failed test results instead of transport-level crashes, with cleaner default reporter output.
|
|
9
|
+
- perf: reuse build artifacts across modes when the resolved non-custom build invocation and build environment are identical, copying the first artifact instead of recompiling.
|
|
10
|
+
|
|
11
|
+
## 2026-03-27 - v1.0.4
|
|
12
|
+
|
|
13
|
+
### Build Command
|
|
14
|
+
|
|
15
|
+
- fix: make `ast build` exit cleanly instead of hanging after work completes.
|
|
16
|
+
- feat: make `ast build` print per-mode build results and a final summary.
|
|
17
|
+
- feat: make `ast build` support `--parallel`, `--jobs`, and `--build-jobs`.
|
|
4
18
|
|
|
5
19
|
### Parallel Execution
|
|
6
20
|
|
package/README.md
CHANGED
|
@@ -66,6 +66,23 @@ By default, `as-test` looks for:
|
|
|
66
66
|
|
|
67
67
|
Generated files go into `.as-test/`.
|
|
68
68
|
|
|
69
|
+
Minimal `as-test.config.json`:
|
|
70
|
+
|
|
71
|
+
```json
|
|
72
|
+
{
|
|
73
|
+
"input": ["assembly/__tests__/*.spec.ts"],
|
|
74
|
+
"output": ".as-test/",
|
|
75
|
+
"buildOptions": {
|
|
76
|
+
"target": "wasi"
|
|
77
|
+
},
|
|
78
|
+
"runOptions": {
|
|
79
|
+
"runtime": {
|
|
80
|
+
"cmd": "node .as-test/runners/default.wasi.js <file>"
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
69
86
|
## Writing Tests
|
|
70
87
|
|
|
71
88
|
Tests usually live in `assembly/__tests__/*.spec.ts`.
|
|
@@ -102,6 +102,13 @@ function getSerialBuildWorkerPool() {
|
|
|
102
102
|
}
|
|
103
103
|
return serialBuildWorkerPool;
|
|
104
104
|
}
|
|
105
|
+
export async function closeSerialBuildWorkerPool() {
|
|
106
|
+
if (!serialBuildWorkerPool)
|
|
107
|
+
return;
|
|
108
|
+
const pool = serialBuildWorkerPool;
|
|
109
|
+
serialBuildWorkerPool = null;
|
|
110
|
+
await pool.close();
|
|
111
|
+
}
|
|
105
112
|
export async function getBuildInvocationPreview(configPath = DEFAULT_CONFIG_PATH, file, modeName, featureToggles = {}, overrides = {}) {
|
|
106
113
|
const loadedConfig = loadConfig(configPath, false);
|
|
107
114
|
const mode = applyMode(loadedConfig, modeName);
|
|
@@ -117,6 +124,39 @@ export async function getBuildInvocationPreview(configPath = DEFAULT_CONFIG_PATH
|
|
|
117
124
|
const outFile = `${config.outDir}/${resolveArtifactFileName(file, config.buildOptions.target, modeName, duplicateSpecBasenames)}`;
|
|
118
125
|
return getBuildCommand(config, getPkgRunner(), file, outFile, modeName, featureToggles);
|
|
119
126
|
}
|
|
127
|
+
export async function getBuildReuseInfo(configPath = DEFAULT_CONFIG_PATH, file, modeName, featureToggles = {}, overrides = {}) {
|
|
128
|
+
const loadedConfig = loadConfig(configPath, false);
|
|
129
|
+
const mode = applyMode(loadedConfig, modeName);
|
|
130
|
+
const config = Object.assign(Object.create(Object.getPrototypeOf(mode.config)), mode.config);
|
|
131
|
+
config.buildOptions = Object.assign(Object.create(Object.getPrototypeOf(mode.config.buildOptions)), mode.config.buildOptions);
|
|
132
|
+
if (overrides.target) {
|
|
133
|
+
config.buildOptions.target = overrides.target;
|
|
134
|
+
}
|
|
135
|
+
if (overrides.args?.length) {
|
|
136
|
+
config.buildOptions.args = [...config.buildOptions.args, ...overrides.args];
|
|
137
|
+
}
|
|
138
|
+
if (hasCustomBuildCommand(config)) {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
const duplicateSpecBasenames = resolveDuplicateBasenames([file]);
|
|
142
|
+
const outFile = `${config.outDir}/${resolveArtifactFileName(file, config.buildOptions.target, modeName, duplicateSpecBasenames)}`;
|
|
143
|
+
const invocation = getBuildCommand(config, getPkgRunner(), file, outFile, modeName, featureToggles);
|
|
144
|
+
const coverageEnabled = resolveCoverageEnabled(config.coverage, featureToggles.coverage);
|
|
145
|
+
const buildEnv = {
|
|
146
|
+
...mode.env,
|
|
147
|
+
...config.buildOptions.env,
|
|
148
|
+
AS_TEST_COVERAGE_ENABLED: coverageEnabled ? "1" : "0",
|
|
149
|
+
};
|
|
150
|
+
return {
|
|
151
|
+
signature: JSON.stringify({
|
|
152
|
+
command: invocation.command,
|
|
153
|
+
args: stripOutputArgs(invocation.args),
|
|
154
|
+
apiArgs: invocation.apiArgs ? stripOutputArgs(invocation.apiArgs) : [],
|
|
155
|
+
env: sortRecord(buildEnv),
|
|
156
|
+
}),
|
|
157
|
+
outFile,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
120
160
|
function hasCustomBuildCommand(config) {
|
|
121
161
|
return !!config.buildOptions.cmd.trim().length;
|
|
122
162
|
}
|
|
@@ -169,6 +209,23 @@ function getUserBuildArgs(config) {
|
|
|
169
209
|
}
|
|
170
210
|
return args;
|
|
171
211
|
}
|
|
212
|
+
function stripOutputArgs(args) {
|
|
213
|
+
const out = [];
|
|
214
|
+
for (let i = 0; i < args.length; i++) {
|
|
215
|
+
const arg = args[i];
|
|
216
|
+
if (arg == "-o" || arg == "--outFile") {
|
|
217
|
+
i++;
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
out.push(arg);
|
|
221
|
+
}
|
|
222
|
+
return out;
|
|
223
|
+
}
|
|
224
|
+
function sortRecord(record) {
|
|
225
|
+
return Object.fromEntries(Object.entries(record)
|
|
226
|
+
.filter((entry) => typeof entry[1] == "string")
|
|
227
|
+
.sort((a, b) => a[0].localeCompare(b[0])));
|
|
228
|
+
}
|
|
172
229
|
function expandBuildCommand(template, file, outFile, target, modeName) {
|
|
173
230
|
const name = path
|
|
174
231
|
.basename(file)
|
package/bin/commands/build.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
import { closeSerialBuildWorkerPool, } from "./build-core.js";
|
|
1
2
|
export { build } from "./build-core.js";
|
|
2
|
-
export { formatInvocation, getBuildInvocationPreview } from "./build-core.js";
|
|
3
|
+
export { formatInvocation, getBuildInvocationPreview, getBuildReuseInfo, } from "./build-core.js";
|
|
3
4
|
export async function executeBuildCommand(rawArgs, configPath, selectedModes, deps) {
|
|
4
5
|
const commandArgs = deps.resolveCommandArgs(rawArgs, "build");
|
|
5
6
|
const listFlags = deps.resolveListFlags(rawArgs, "build");
|
|
6
7
|
const featureToggles = deps.resolveFeatureToggles(rawArgs, "build");
|
|
8
|
+
const parallel = deps.resolveBuildParallelJobs(rawArgs);
|
|
7
9
|
const buildFeatureToggles = {
|
|
8
10
|
tryAs: featureToggles.tryAs,
|
|
9
11
|
coverage: featureToggles.coverage,
|
|
@@ -13,5 +15,18 @@ export async function executeBuildCommand(rawArgs, configPath, selectedModes, de
|
|
|
13
15
|
await deps.listExecutionPlan("build", configPath, commandArgs, modeTargets, listFlags);
|
|
14
16
|
return;
|
|
15
17
|
}
|
|
16
|
-
|
|
18
|
+
const previousBuildApi = process.env.AS_TEST_BUILD_API;
|
|
19
|
+
process.env.AS_TEST_BUILD_API = "1";
|
|
20
|
+
try {
|
|
21
|
+
await deps.runBuildModes(configPath, commandArgs, modeTargets, buildFeatureToggles, parallel);
|
|
22
|
+
}
|
|
23
|
+
finally {
|
|
24
|
+
if (previousBuildApi == undefined) {
|
|
25
|
+
delete process.env.AS_TEST_BUILD_API;
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
process.env.AS_TEST_BUILD_API = previousBuildApi;
|
|
29
|
+
}
|
|
30
|
+
await closeSerialBuildWorkerPool();
|
|
31
|
+
}
|
|
17
32
|
}
|
package/bin/commands/run-core.js
CHANGED
|
@@ -1286,26 +1286,22 @@ async function runProcess(invocation, specFile, crashDir, modeName, snapshots, s
|
|
|
1286
1286
|
let report = null;
|
|
1287
1287
|
let parseError = null;
|
|
1288
1288
|
let stderrBuffer = "";
|
|
1289
|
+
let stderrPendingLine = "";
|
|
1289
1290
|
let stdoutBuffer = "";
|
|
1290
|
-
let suppressTraceWarningLine = false;
|
|
1291
1291
|
let spawnError = null;
|
|
1292
1292
|
child.on("error", (error) => {
|
|
1293
1293
|
spawnError = error;
|
|
1294
1294
|
});
|
|
1295
1295
|
child.stderr.on("data", (chunk) => {
|
|
1296
|
-
|
|
1297
|
-
let newline =
|
|
1296
|
+
stderrPendingLine += chunk.toString("utf8");
|
|
1297
|
+
let newline = stderrPendingLine.indexOf("\n");
|
|
1298
1298
|
while (newline >= 0) {
|
|
1299
|
-
const line =
|
|
1300
|
-
|
|
1301
|
-
if (shouldSuppressWasiWarningLine(line
|
|
1302
|
-
|
|
1299
|
+
const line = stderrPendingLine.slice(0, newline + 1);
|
|
1300
|
+
stderrPendingLine = stderrPendingLine.slice(newline + 1);
|
|
1301
|
+
if (!shouldSuppressWasiWarningLine(line)) {
|
|
1302
|
+
stderrBuffer += line;
|
|
1303
1303
|
}
|
|
1304
|
-
|
|
1305
|
-
suppressTraceWarningLine = false;
|
|
1306
|
-
process.stderr.write(line);
|
|
1307
|
-
}
|
|
1308
|
-
newline = stderrBuffer.indexOf("\n");
|
|
1304
|
+
newline = stderrPendingLine.indexOf("\n");
|
|
1309
1305
|
}
|
|
1310
1306
|
});
|
|
1311
1307
|
class TestChannel extends Channel {
|
|
@@ -1409,55 +1405,138 @@ async function runProcess(invocation, specFile, crashDir, modeName, snapshots, s
|
|
|
1409
1405
|
const code = await new Promise((resolve) => {
|
|
1410
1406
|
child.on("close", (exitCode) => resolve(exitCode ?? 1));
|
|
1411
1407
|
});
|
|
1412
|
-
if (
|
|
1413
|
-
|
|
1414
|
-
process.stderr.write(stderrBuffer);
|
|
1415
|
-
}
|
|
1408
|
+
if (stderrPendingLine.length && !shouldSuppressWasiWarningLine(stderrPendingLine)) {
|
|
1409
|
+
stderrBuffer += stderrPendingLine;
|
|
1416
1410
|
}
|
|
1417
1411
|
if (spawnError) {
|
|
1412
|
+
const errorText = spawnError.stack ?? spawnError.message;
|
|
1418
1413
|
persistCrashRecord(crashDir, {
|
|
1419
1414
|
kind: "test",
|
|
1420
1415
|
file: specFile,
|
|
1421
1416
|
mode: modeName ?? "default",
|
|
1422
|
-
error:
|
|
1417
|
+
error: errorText,
|
|
1423
1418
|
stdout: stdoutBuffer,
|
|
1424
1419
|
stderr: stderrBuffer,
|
|
1425
1420
|
});
|
|
1426
|
-
|
|
1421
|
+
return createRuntimeFailureReport(specFile, modeName, "failed to start test runtime", errorText, stdoutBuffer, stderrBuffer);
|
|
1427
1422
|
}
|
|
1428
1423
|
if (parseError) {
|
|
1424
|
+
const errorText = `could not parse report payload: ${parseError}`;
|
|
1429
1425
|
persistCrashRecord(crashDir, {
|
|
1430
1426
|
kind: "test",
|
|
1431
1427
|
file: specFile,
|
|
1432
1428
|
mode: modeName ?? "default",
|
|
1433
|
-
error:
|
|
1429
|
+
error: errorText,
|
|
1434
1430
|
stdout: stdoutBuffer,
|
|
1435
1431
|
stderr: stderrBuffer,
|
|
1436
1432
|
});
|
|
1437
|
-
|
|
1433
|
+
return createRuntimeFailureReport(specFile, modeName, "runtime returned an invalid report payload", errorText, stdoutBuffer, stderrBuffer);
|
|
1438
1434
|
}
|
|
1439
1435
|
if (!report) {
|
|
1436
|
+
const errorText = "missing report payload from test runtime";
|
|
1440
1437
|
persistCrashRecord(crashDir, {
|
|
1441
1438
|
kind: "test",
|
|
1442
1439
|
file: specFile,
|
|
1443
1440
|
mode: modeName ?? "default",
|
|
1444
|
-
error:
|
|
1441
|
+
error: errorText,
|
|
1445
1442
|
stdout: stdoutBuffer,
|
|
1446
1443
|
stderr: stderrBuffer,
|
|
1447
1444
|
});
|
|
1448
|
-
|
|
1445
|
+
return createRuntimeFailureReport(specFile, modeName, "test runtime exited without sending a report", errorText, stdoutBuffer, stderrBuffer);
|
|
1449
1446
|
}
|
|
1450
|
-
if (code !== 0) {
|
|
1451
|
-
|
|
1452
|
-
|
|
1447
|
+
if (code !== 0 || hasMeaningfulRuntimeOutput(stderrBuffer)) {
|
|
1448
|
+
const errorParts = [];
|
|
1449
|
+
if (code !== 0) {
|
|
1450
|
+
errorParts.push(`child process exited with code ${code}`);
|
|
1451
|
+
}
|
|
1452
|
+
const stderrText = normalizeRuntimeOutput(stderrBuffer);
|
|
1453
|
+
if (stderrText.length) {
|
|
1454
|
+
errorParts.push(stderrText);
|
|
1455
|
+
}
|
|
1456
|
+
const errorText = errorParts.join("\n\n");
|
|
1457
|
+
persistCrashRecord(crashDir, {
|
|
1458
|
+
kind: "test",
|
|
1459
|
+
file: specFile,
|
|
1460
|
+
mode: modeName ?? "default",
|
|
1461
|
+
error: errorText || "runtime reported an unknown error",
|
|
1462
|
+
stdout: stdoutBuffer,
|
|
1463
|
+
stderr: stderrBuffer,
|
|
1464
|
+
});
|
|
1465
|
+
return appendRuntimeFailureReport(report, specFile, modeName, code !== 0
|
|
1466
|
+
? `test runtime failed with exit code ${code}`
|
|
1467
|
+
: "test runtime wrote to stderr", errorText, stdoutBuffer, stderrBuffer);
|
|
1453
1468
|
}
|
|
1454
1469
|
return report;
|
|
1455
1470
|
}
|
|
1456
|
-
function
|
|
1471
|
+
function createRuntimeFailureReport(specFile, modeName, title, details, stdout, stderr) {
|
|
1472
|
+
return appendRuntimeFailureReport({
|
|
1473
|
+
suites: [],
|
|
1474
|
+
coverage: {
|
|
1475
|
+
total: 0,
|
|
1476
|
+
covered: 0,
|
|
1477
|
+
uncovered: 0,
|
|
1478
|
+
percent: 100,
|
|
1479
|
+
points: [],
|
|
1480
|
+
},
|
|
1481
|
+
}, specFile, modeName, title, details, stdout, stderr);
|
|
1482
|
+
}
|
|
1483
|
+
function appendRuntimeFailureReport(report, specFile, modeName, title, details, stdout, stderr) {
|
|
1484
|
+
const suites = Array.isArray(report?.suites) ? report.suites : [];
|
|
1485
|
+
suites.push({
|
|
1486
|
+
file: specFile,
|
|
1487
|
+
description: path.basename(specFile),
|
|
1488
|
+
depth: 0,
|
|
1489
|
+
kind: "runtime-error",
|
|
1490
|
+
verdict: "fail",
|
|
1491
|
+
time: {
|
|
1492
|
+
start: 0,
|
|
1493
|
+
end: 0,
|
|
1494
|
+
},
|
|
1495
|
+
suites: [],
|
|
1496
|
+
logs: [],
|
|
1497
|
+
tests: [
|
|
1498
|
+
{
|
|
1499
|
+
order: 0,
|
|
1500
|
+
type: "runtime-error",
|
|
1501
|
+
verdict: "fail",
|
|
1502
|
+
left: null,
|
|
1503
|
+
right: null,
|
|
1504
|
+
instr: title,
|
|
1505
|
+
message: formatRuntimeFailureMessage(details, stdout, stderr),
|
|
1506
|
+
location: "",
|
|
1507
|
+
},
|
|
1508
|
+
],
|
|
1509
|
+
modeName: modeName ?? "default",
|
|
1510
|
+
});
|
|
1511
|
+
report.suites = suites;
|
|
1512
|
+
return report;
|
|
1513
|
+
}
|
|
1514
|
+
function formatRuntimeFailureMessage(details, stdout, stderr) {
|
|
1515
|
+
const parts = [];
|
|
1516
|
+
const normalizedDetails = normalizeRuntimeOutput(details);
|
|
1517
|
+
const normalizedStderr = normalizeRuntimeOutput(stderr);
|
|
1518
|
+
const normalizedStdout = normalizeRuntimeOutput(stdout);
|
|
1519
|
+
if (normalizedDetails.length)
|
|
1520
|
+
parts.push(normalizedDetails);
|
|
1521
|
+
if (normalizedStderr.length && normalizedStderr !== normalizedDetails) {
|
|
1522
|
+
parts.push(`stderr:\n${normalizedStderr}`);
|
|
1523
|
+
}
|
|
1524
|
+
if (normalizedStdout.length) {
|
|
1525
|
+
parts.push(`stdout:\n${normalizedStdout}`);
|
|
1526
|
+
}
|
|
1527
|
+
return parts.join("\n\n");
|
|
1528
|
+
}
|
|
1529
|
+
function normalizeRuntimeOutput(value) {
|
|
1530
|
+
return value.replace(/\r\n/g, "\n").trim();
|
|
1531
|
+
}
|
|
1532
|
+
function hasMeaningfulRuntimeOutput(value) {
|
|
1533
|
+
return normalizeRuntimeOutput(value).length > 0;
|
|
1534
|
+
}
|
|
1535
|
+
function shouldSuppressWasiWarningLine(line) {
|
|
1457
1536
|
if (line.includes("ExperimentalWarning: WASI is an experimental feature")) {
|
|
1458
1537
|
return true;
|
|
1459
1538
|
}
|
|
1460
|
-
if (
|
|
1539
|
+
if (line.includes("--trace-warnings")) {
|
|
1461
1540
|
return true;
|
|
1462
1541
|
}
|
|
1463
1542
|
return false;
|
|
@@ -1506,6 +1585,7 @@ function readFileReport(stats, fileReport) {
|
|
|
1506
1585
|
}
|
|
1507
1586
|
function readSuite(stats, suite, file, modeName) {
|
|
1508
1587
|
const suiteAny = suite;
|
|
1588
|
+
const kind = String(suiteAny.kind ?? "");
|
|
1509
1589
|
let verdict = normalizeVerdict(suiteAny.verdict);
|
|
1510
1590
|
const time = suiteAny.time;
|
|
1511
1591
|
const start = Number(time?.start ?? 0);
|
|
@@ -1533,6 +1613,20 @@ function readSuite(stats, suite, file, modeName) {
|
|
|
1533
1613
|
stats.skippedTests++;
|
|
1534
1614
|
}
|
|
1535
1615
|
}
|
|
1616
|
+
if (isTestCaseSuiteKind(kind)) {
|
|
1617
|
+
if (!subSuites.length && !tests.length) {
|
|
1618
|
+
if (verdict == "fail") {
|
|
1619
|
+
stats.failedTests++;
|
|
1620
|
+
}
|
|
1621
|
+
else if (verdict == "ok") {
|
|
1622
|
+
stats.passedTests++;
|
|
1623
|
+
}
|
|
1624
|
+
else if (verdict == "skip") {
|
|
1625
|
+
stats.skippedTests++;
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
return verdict;
|
|
1629
|
+
}
|
|
1536
1630
|
if (verdict == "fail") {
|
|
1537
1631
|
stats.failedSuites++;
|
|
1538
1632
|
stats.failedEntries.push({
|
|
@@ -1549,6 +1643,15 @@ function readSuite(stats, suite, file, modeName) {
|
|
|
1549
1643
|
}
|
|
1550
1644
|
return verdict;
|
|
1551
1645
|
}
|
|
1646
|
+
function isTestCaseSuiteKind(kind) {
|
|
1647
|
+
return (kind == "test" ||
|
|
1648
|
+
kind == "it" ||
|
|
1649
|
+
kind == "only" ||
|
|
1650
|
+
kind == "xtest" ||
|
|
1651
|
+
kind == "xit" ||
|
|
1652
|
+
kind == "xonly" ||
|
|
1653
|
+
kind == "todo");
|
|
1654
|
+
}
|
|
1552
1655
|
function normalizeVerdict(value) {
|
|
1553
1656
|
const verdict = String(value ?? "none");
|
|
1554
1657
|
if (verdict == "fail")
|
package/bin/commands/run.js
CHANGED
|
@@ -13,6 +13,7 @@ export async function executeRunCommand(rawArgs, flags, configPath, selectedMode
|
|
|
13
13
|
...deps.resolveParallelJobs(rawArgs, "run"),
|
|
14
14
|
coverage: featureToggles.coverage,
|
|
15
15
|
browser: deps.resolveBrowserOverride(rawArgs, "run"),
|
|
16
|
+
reporterPath: deps.resolveReporterOverride(rawArgs, "run"),
|
|
16
17
|
};
|
|
17
18
|
const modeTargets = deps.resolveExecutionModes(configPath, selectedModes);
|
|
18
19
|
if (listFlags.list || listFlags.listModes) {
|
package/bin/commands/test.js
CHANGED
|
@@ -16,6 +16,7 @@ export async function executeTestCommand(rawArgs, flags, configPath, selectedMod
|
|
|
16
16
|
...deps.resolveParallelJobs(rawArgs, "test"),
|
|
17
17
|
coverage: featureToggles.coverage,
|
|
18
18
|
browser: deps.resolveBrowserOverride(rawArgs, "test"),
|
|
19
|
+
reporterPath: deps.resolveReporterOverride(rawArgs, "test"),
|
|
19
20
|
};
|
|
20
21
|
const fuzzEnabled = flags.includes("--fuzz");
|
|
21
22
|
const fuzzOverrides = deps.resolveFuzzOverrides(rawArgs, "test");
|
package/bin/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import chalk from "chalk";
|
|
3
|
-
import { build, formatInvocation as formatBuildInvocation, getBuildInvocationPreview, } from "./commands/build.js";
|
|
3
|
+
import { build, formatInvocation as formatBuildInvocation, getBuildInvocationPreview, getBuildReuseInfo, } from "./commands/build.js";
|
|
4
4
|
import { createRunReporter, run } from "./commands/run.js";
|
|
5
5
|
import { executeBuildCommand } from "./commands/build.js";
|
|
6
6
|
import { executeRunCommand } from "./commands/run.js";
|
|
@@ -9,12 +9,12 @@ import { executeFuzzCommand } from "./commands/fuzz.js";
|
|
|
9
9
|
import { executeInitCommand } from "./commands/init.js";
|
|
10
10
|
import { executeDoctorCommand } from "./commands/doctor.js";
|
|
11
11
|
import { fuzz } from "./commands/fuzz-core.js";
|
|
12
|
-
import { applyMode, getCliVersion, loadConfig, resolveModeNames, } from "./util.js";
|
|
12
|
+
import { applyMode, formatTime, getCliVersion, loadConfig, resolveModeNames, } from "./util.js";
|
|
13
13
|
import * as path from "path";
|
|
14
14
|
import { spawnSync } from "child_process";
|
|
15
15
|
import { glob } from "glob";
|
|
16
16
|
import { createInterface } from "readline";
|
|
17
|
-
import { existsSync } from "fs";
|
|
17
|
+
import { copyFileSync, existsSync, mkdirSync } from "fs";
|
|
18
18
|
import { availableParallelism, cpus } from "os";
|
|
19
19
|
import { BuildWorkerPool } from "./build-worker-pool.js";
|
|
20
20
|
const _args = process.argv.slice(2);
|
|
@@ -50,6 +50,7 @@ else if (COMMANDS.includes(args[0])) {
|
|
|
50
50
|
resolveCommandArgs,
|
|
51
51
|
resolveListFlags,
|
|
52
52
|
resolveFeatureToggles,
|
|
53
|
+
resolveBuildParallelJobs,
|
|
53
54
|
resolveExecutionModes,
|
|
54
55
|
listExecutionPlan,
|
|
55
56
|
runBuildModes,
|
|
@@ -65,6 +66,7 @@ else if (COMMANDS.includes(args[0])) {
|
|
|
65
66
|
resolveFeatureToggles,
|
|
66
67
|
resolveParallelJobs,
|
|
67
68
|
resolveBrowserOverride,
|
|
69
|
+
resolveReporterOverride,
|
|
68
70
|
resolveExecutionModes,
|
|
69
71
|
listExecutionPlan,
|
|
70
72
|
runRuntimeModes,
|
|
@@ -80,6 +82,7 @@ else if (COMMANDS.includes(args[0])) {
|
|
|
80
82
|
resolveFeatureToggles,
|
|
81
83
|
resolveParallelJobs,
|
|
82
84
|
resolveBrowserOverride,
|
|
85
|
+
resolveReporterOverride,
|
|
83
86
|
resolveFuzzOverrides,
|
|
84
87
|
resolveExecutionModes,
|
|
85
88
|
listExecutionPlan,
|
|
@@ -226,6 +229,9 @@ function printCommandHelp(command) {
|
|
|
226
229
|
process.stdout.write(" --mode <name[,name...]> Run one or multiple named config modes\n");
|
|
227
230
|
process.stdout.write(" --enable <feature> Enable build feature (coverage|try-as)\n");
|
|
228
231
|
process.stdout.write(" --disable <feature> Disable build feature (coverage|try-as)\n");
|
|
232
|
+
process.stdout.write(" --parallel Run files through an ordered worker pool using an automatic worker count\n");
|
|
233
|
+
process.stdout.write(" --jobs <n> Run files through an ordered worker pool\n");
|
|
234
|
+
process.stdout.write(" --build-jobs <n> Limit concurrent build tasks (defaults to --jobs)\n");
|
|
229
235
|
process.stdout.write(" --list Preview resolved files/artifacts without building\n");
|
|
230
236
|
process.stdout.write(" --list-modes Preview configured and selected mode names\n");
|
|
231
237
|
process.stdout.write(" --help, -h Show this help\n");
|
|
@@ -396,7 +402,6 @@ function resolveCommandArgs(rawArgs, command) {
|
|
|
396
402
|
}
|
|
397
403
|
if (arg == "--runs" ||
|
|
398
404
|
arg == "--seed" ||
|
|
399
|
-
arg == "--parallel" ||
|
|
400
405
|
arg == "--jobs" ||
|
|
401
406
|
arg == "--build-jobs" ||
|
|
402
407
|
arg == "--run-jobs" ||
|
|
@@ -406,6 +411,9 @@ function resolveCommandArgs(rawArgs, command) {
|
|
|
406
411
|
i++;
|
|
407
412
|
continue;
|
|
408
413
|
}
|
|
414
|
+
if (arg == "--parallel") {
|
|
415
|
+
continue;
|
|
416
|
+
}
|
|
409
417
|
if (arg.startsWith("--runs=") ||
|
|
410
418
|
arg.startsWith("--seed=") ||
|
|
411
419
|
arg.startsWith("--jobs=") ||
|
|
@@ -573,6 +581,32 @@ function resolveBrowserOverride(rawArgs, command) {
|
|
|
573
581
|
}
|
|
574
582
|
return undefined;
|
|
575
583
|
}
|
|
584
|
+
function resolveReporterOverride(rawArgs, command) {
|
|
585
|
+
let seenCommand = false;
|
|
586
|
+
for (let i = 0; i < rawArgs.length; i++) {
|
|
587
|
+
const arg = rawArgs[i];
|
|
588
|
+
if (!seenCommand) {
|
|
589
|
+
if (arg == command)
|
|
590
|
+
seenCommand = true;
|
|
591
|
+
continue;
|
|
592
|
+
}
|
|
593
|
+
if (arg == "--reporter") {
|
|
594
|
+
const next = rawArgs[i + 1];
|
|
595
|
+
if (next && !next.startsWith("-")) {
|
|
596
|
+
return next;
|
|
597
|
+
}
|
|
598
|
+
return undefined;
|
|
599
|
+
}
|
|
600
|
+
if (arg.startsWith("--reporter=")) {
|
|
601
|
+
const value = arg.slice("--reporter=".length);
|
|
602
|
+
return value.length ? value : undefined;
|
|
603
|
+
}
|
|
604
|
+
if (arg == "--tap") {
|
|
605
|
+
return "tap";
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
return undefined;
|
|
609
|
+
}
|
|
576
610
|
function resolveJobs(rawArgs, command) {
|
|
577
611
|
let seenCommand = false;
|
|
578
612
|
let parallel = false;
|
|
@@ -597,6 +631,29 @@ function resolveJobs(rawArgs, command) {
|
|
|
597
631
|
}
|
|
598
632
|
return parallel ? 0 : 1;
|
|
599
633
|
}
|
|
634
|
+
function resolveBuildParallelJobs(rawArgs) {
|
|
635
|
+
const baseJobs = resolveJobs(rawArgs, "build");
|
|
636
|
+
let buildJobs = baseJobs;
|
|
637
|
+
let seenCommand = false;
|
|
638
|
+
for (let i = 0; i < rawArgs.length; i++) {
|
|
639
|
+
const arg = rawArgs[i];
|
|
640
|
+
if (!seenCommand) {
|
|
641
|
+
if (arg == "build")
|
|
642
|
+
seenCommand = true;
|
|
643
|
+
continue;
|
|
644
|
+
}
|
|
645
|
+
const buildParsed = parseNumberFlag(rawArgs, i, "--build-jobs");
|
|
646
|
+
if (buildParsed) {
|
|
647
|
+
if (buildParsed.number < 1) {
|
|
648
|
+
throw new Error("--build-jobs requires a positive integer");
|
|
649
|
+
}
|
|
650
|
+
buildJobs = buildParsed.number;
|
|
651
|
+
continue;
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
const jobs = Math.max(baseJobs, buildJobs);
|
|
655
|
+
return { jobs, buildJobs };
|
|
656
|
+
}
|
|
600
657
|
function resolveParallelJobs(rawArgs, command) {
|
|
601
658
|
const baseJobs = resolveJobs(rawArgs, command);
|
|
602
659
|
let buildJobs = baseJobs;
|
|
@@ -704,9 +761,9 @@ function createBufferedStream() {
|
|
|
704
761
|
},
|
|
705
762
|
};
|
|
706
763
|
}
|
|
707
|
-
async function createBufferedReporter(configPath, modeName) {
|
|
764
|
+
async function createBufferedReporter(configPath, reporterPath, modeName) {
|
|
708
765
|
const stream = createBufferedStream();
|
|
709
|
-
const session = await createRunReporter(configPath,
|
|
766
|
+
const session = await createRunReporter(configPath, reporterPath, modeName, {
|
|
710
767
|
stdout: stream,
|
|
711
768
|
stderr: stream,
|
|
712
769
|
});
|
|
@@ -855,6 +912,32 @@ function resolveCommandTokens(rawArgs, command) {
|
|
|
855
912
|
}
|
|
856
913
|
return values;
|
|
857
914
|
}
|
|
915
|
+
async function buildFileForMode(cache, args) {
|
|
916
|
+
const reuse = await getBuildReuseInfo(args.configPath, args.file, args.modeName, args.buildFeatureToggles);
|
|
917
|
+
if (reuse) {
|
|
918
|
+
const source = cache.get(reuse.signature);
|
|
919
|
+
if (source && source != reuse.outFile && existsSync(source)) {
|
|
920
|
+
mkdirSync(path.dirname(reuse.outFile), { recursive: true });
|
|
921
|
+
copyFileSync(source, reuse.outFile);
|
|
922
|
+
return true;
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
if (args.buildPool) {
|
|
926
|
+
await args.buildPool.buildFileMode({
|
|
927
|
+
configPath: args.configPath,
|
|
928
|
+
file: args.file,
|
|
929
|
+
modeName: args.modeName,
|
|
930
|
+
featureToggles: args.buildFeatureToggles,
|
|
931
|
+
});
|
|
932
|
+
}
|
|
933
|
+
else {
|
|
934
|
+
await build(args.configPath, [args.file], args.modeName, args.buildFeatureToggles);
|
|
935
|
+
}
|
|
936
|
+
if (reuse) {
|
|
937
|
+
cache.set(reuse.signature, reuse.outFile);
|
|
938
|
+
}
|
|
939
|
+
return false;
|
|
940
|
+
}
|
|
858
941
|
async function runTestSequential(runFlags, configPath, selectors, buildFeatureToggles, modeSummaryTotal, fileSummaryTotal, allowNoSpecFiles = false, modeName, reporterOverride, emitRunComplete = true) {
|
|
859
942
|
const files = await resolveSelectedFiles(configPath, selectors);
|
|
860
943
|
if (!files.length) {
|
|
@@ -862,7 +945,7 @@ async function runTestSequential(runFlags, configPath, selectors, buildFeatureTo
|
|
|
862
945
|
throw await buildNoTestFilesMatchedError(configPath, selectors);
|
|
863
946
|
}
|
|
864
947
|
}
|
|
865
|
-
const reporterSession = await createRunReporter(configPath,
|
|
948
|
+
const reporterSession = await createRunReporter(configPath, runFlags.reporterPath, modeName);
|
|
866
949
|
const reporter = reporterOverride ?? reporterSession.reporter;
|
|
867
950
|
const snapshotEnabled = runFlags.snapshot !== false;
|
|
868
951
|
reporter.onRunStart?.({
|
|
@@ -922,10 +1005,55 @@ async function runTestSequential(runFlags, configPath, selectors, buildFeatureTo
|
|
|
922
1005
|
},
|
|
923
1006
|
};
|
|
924
1007
|
}
|
|
925
|
-
async function runBuildModes(configPath, selectors, modes, buildFeatureToggles) {
|
|
1008
|
+
async function runBuildModes(configPath, selectors, modes, buildFeatureToggles, parallel) {
|
|
1009
|
+
const files = await resolveSelectedFiles(configPath, selectors);
|
|
1010
|
+
if (!files.length) {
|
|
1011
|
+
throw await buildNoTestFilesMatchedError(configPath, selectors);
|
|
1012
|
+
}
|
|
1013
|
+
const effective = resolveEffectiveParallelJobs({
|
|
1014
|
+
jobs: parallel.jobs,
|
|
1015
|
+
buildJobs: parallel.buildJobs,
|
|
1016
|
+
runJobs: parallel.buildJobs,
|
|
1017
|
+
}, files.length);
|
|
1018
|
+
const resolvedConfigPath = configPath ?? path.join(process.cwd(), "./as-test.config.json");
|
|
1019
|
+
const loadedConfig = loadConfig(resolvedConfigPath, true);
|
|
1020
|
+
const allStartedAt = Date.now();
|
|
1021
|
+
let builtCount = 0;
|
|
1022
|
+
const buildReuseCache = new Map();
|
|
926
1023
|
for (const modeName of modes) {
|
|
927
|
-
|
|
1024
|
+
const startedAt = Date.now();
|
|
1025
|
+
if (effective.buildJobs > 1) {
|
|
1026
|
+
const pool = new BuildWorkerPool(effective.buildJobs);
|
|
1027
|
+
try {
|
|
1028
|
+
await runOrderedPool(files, effective.buildJobs, async (file) => {
|
|
1029
|
+
await buildFileForMode(buildReuseCache, {
|
|
1030
|
+
configPath,
|
|
1031
|
+
file,
|
|
1032
|
+
modeName,
|
|
1033
|
+
buildFeatureToggles,
|
|
1034
|
+
buildPool: pool,
|
|
1035
|
+
});
|
|
1036
|
+
});
|
|
1037
|
+
}
|
|
1038
|
+
finally {
|
|
1039
|
+
await pool.close();
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
else {
|
|
1043
|
+
for (const file of files) {
|
|
1044
|
+
await buildFileForMode(buildReuseCache, {
|
|
1045
|
+
configPath,
|
|
1046
|
+
file,
|
|
1047
|
+
modeName,
|
|
1048
|
+
buildFeatureToggles,
|
|
1049
|
+
});
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
builtCount += files.length;
|
|
1053
|
+
const active = applyMode(loadedConfig, modeName).config;
|
|
1054
|
+
process.stdout.write(`${chalk.bgGreenBright.black(" BUILT ")} ${modeName ?? "default"} ${chalk.dim(`(${active.buildOptions.target})`)} ${files.length} file(s) -> ${active.outDir} ${chalk.dim(formatTime(Date.now() - startedAt))}\n`);
|
|
928
1055
|
}
|
|
1056
|
+
process.stdout.write(`${chalk.bold("Summary:")} built ${builtCount} file(s) across ${modes.length || 1} mode(s) in ${formatTime(Date.now() - allStartedAt)}\n`);
|
|
929
1057
|
}
|
|
930
1058
|
async function runRuntimeModes(runFlags, configPath, selectors, modes) {
|
|
931
1059
|
await ensureWebBrowsersReady(configPath, modes, runFlags.browser);
|
|
@@ -959,6 +1087,7 @@ async function runRuntimeModes(runFlags, configPath, selectors, modes) {
|
|
|
959
1087
|
const buildCommandsByFile = await previewBuildCommands(configPath, selectors, modes[0], {});
|
|
960
1088
|
for (const modeName of modes) {
|
|
961
1089
|
const result = await run(effectiveRunFlags, configPath, selectors, false, {
|
|
1090
|
+
reporterPath: effectiveRunFlags.reporterPath,
|
|
962
1091
|
modeName,
|
|
963
1092
|
modeSummaryTotal,
|
|
964
1093
|
modeSummaryExecuted: 1,
|
|
@@ -1000,11 +1129,13 @@ async function runRuntimeMatrix(runFlags, configPath, selectors, modes, modeSumm
|
|
|
1000
1129
|
}));
|
|
1001
1130
|
const duplicateSpecBasenames = resolveDuplicateSpecBasenames(files);
|
|
1002
1131
|
const buildIntervals = [];
|
|
1132
|
+
const buildReuseCache = new Map();
|
|
1003
1133
|
for (let fileIndex = 0; fileIndex < files.length; fileIndex++) {
|
|
1004
1134
|
const file = files[fileIndex];
|
|
1005
1135
|
const fileName = path.basename(file);
|
|
1006
1136
|
const fileResults = [];
|
|
1007
1137
|
const modeTimes = modes.map(() => "...");
|
|
1138
|
+
const buildReuseCache = new Map();
|
|
1008
1139
|
if (liveMatrix) {
|
|
1009
1140
|
renderMatrixLiveLine(fileName, modeLabels, modeTimes, showPerModeTimes);
|
|
1010
1141
|
}
|
|
@@ -1041,7 +1172,9 @@ async function runRuntimeMatrix(runFlags, configPath, selectors, modes, modeSumm
|
|
|
1041
1172
|
throw error;
|
|
1042
1173
|
}
|
|
1043
1174
|
}
|
|
1044
|
-
|
|
1175
|
+
if (reporterSession.reporterKind == "default") {
|
|
1176
|
+
renderMatrixFileResult(fileName, modeLabels, fileResults, modeTimes, liveMatrix, showPerModeTimes);
|
|
1177
|
+
}
|
|
1045
1178
|
const verdict = resolveMatrixVerdict(fileResults);
|
|
1046
1179
|
if (verdict == "fail") {
|
|
1047
1180
|
fileState[fileIndex].failed = true;
|
|
@@ -1095,7 +1228,7 @@ async function runTestModes(runFlags, configPath, selectors, modes, buildFeature
|
|
|
1095
1228
|
}
|
|
1096
1229
|
let failed = false;
|
|
1097
1230
|
for (const modeName of modes) {
|
|
1098
|
-
const reporterSession = await createRunReporter(configPath,
|
|
1231
|
+
const reporterSession = await createRunReporter(configPath, effectiveRunFlags.reporterPath, modeName);
|
|
1099
1232
|
const modeResult = await runTestSequential(effectiveRunFlags, configPath, selectors, buildFeatureToggles, modeSummaryTotal, fileSummaryTotal, fuzzEnabled, modeName, reporterSession.reporter, !fuzzEnabled);
|
|
1100
1233
|
if (modeResult.failed)
|
|
1101
1234
|
failed = true;
|
|
@@ -1165,6 +1298,7 @@ async function runTestMatrix(runFlags, configPath, selectors, modes, buildFeatur
|
|
|
1165
1298
|
const fileName = path.basename(file);
|
|
1166
1299
|
const fileResults = [];
|
|
1167
1300
|
const modeTimes = modes.map(() => "...");
|
|
1301
|
+
const buildReuseCache = new Map();
|
|
1168
1302
|
if (liveMatrix) {
|
|
1169
1303
|
renderMatrixLiveLine(fileName, modeLabels, modeTimes, showPerModeTimes);
|
|
1170
1304
|
}
|
|
@@ -1172,7 +1306,12 @@ async function runTestMatrix(runFlags, configPath, selectors, modes, buildFeatur
|
|
|
1172
1306
|
const modeName = modes[i];
|
|
1173
1307
|
try {
|
|
1174
1308
|
const buildStartedAt = Date.now();
|
|
1175
|
-
await
|
|
1309
|
+
await buildFileForMode(buildReuseCache, {
|
|
1310
|
+
configPath,
|
|
1311
|
+
file,
|
|
1312
|
+
modeName,
|
|
1313
|
+
buildFeatureToggles,
|
|
1314
|
+
});
|
|
1176
1315
|
buildIntervals.push({ start: buildStartedAt, end: Date.now() });
|
|
1177
1316
|
const buildInvocation = await getBuildInvocationPreview(configPath, file, modeName, buildFeatureToggles);
|
|
1178
1317
|
const artifactKey = resolvePerFileArtifactKey(file, duplicateSpecBasenames);
|
|
@@ -1204,7 +1343,9 @@ async function runTestMatrix(runFlags, configPath, selectors, modes, buildFeatur
|
|
|
1204
1343
|
throw error;
|
|
1205
1344
|
}
|
|
1206
1345
|
}
|
|
1207
|
-
|
|
1346
|
+
if (reporterSession.reporterKind == "default") {
|
|
1347
|
+
renderMatrixFileResult(fileName, modeLabels, fileResults, modeTimes, liveMatrix, showPerModeTimes);
|
|
1348
|
+
}
|
|
1208
1349
|
const verdict = resolveMatrixVerdict(fileResults);
|
|
1209
1350
|
if (verdict == "fail") {
|
|
1210
1351
|
fileState[fileIndex].failed = true;
|
|
@@ -1267,7 +1408,7 @@ async function runRuntimeSingleParallel(runFlags, configPath, selectors, modeNam
|
|
|
1267
1408
|
if (!files.length) {
|
|
1268
1409
|
throw await buildNoTestFilesMatchedError(configPath, selectors);
|
|
1269
1410
|
}
|
|
1270
|
-
const reporterSession = await createRunReporter(configPath,
|
|
1411
|
+
const reporterSession = await createRunReporter(configPath, runFlags.reporterPath, modeName);
|
|
1271
1412
|
const reporter = reporterSession.reporter;
|
|
1272
1413
|
const snapshotEnabled = runFlags.snapshot !== false;
|
|
1273
1414
|
reporter.onRunStart?.({
|
|
@@ -1279,15 +1420,20 @@ async function runRuntimeSingleParallel(runFlags, configPath, selectors, modeNam
|
|
|
1279
1420
|
});
|
|
1280
1421
|
const buildCommandsByFile = await previewBuildCommands(configPath, selectors, modeName, {});
|
|
1281
1422
|
const results = new Array(files.length);
|
|
1282
|
-
const
|
|
1423
|
+
const useQueueDisplay = reporterSession.reporterKind == "default";
|
|
1424
|
+
const queueDisplay = new ParallelQueueDisplay(useQueueDisplay && !runFlags.clean);
|
|
1283
1425
|
const runLimit = createAsyncLimiter(runFlags.runJobs);
|
|
1284
1426
|
const poolWidth = Math.max(runFlags.buildJobs, runFlags.runJobs);
|
|
1285
1427
|
await runOrderedPool(files, poolWidth, async (file, index) => {
|
|
1286
|
-
const token =
|
|
1287
|
-
|
|
1428
|
+
const token = useQueueDisplay
|
|
1429
|
+
? renderQueuedFileStart(queueDisplay, path.basename(file))
|
|
1430
|
+
: null;
|
|
1431
|
+
const buffered = useQueueDisplay
|
|
1432
|
+
? await createBufferedReporter(configPath, runFlags.reporterPath, modeName)
|
|
1433
|
+
: null;
|
|
1288
1434
|
const result = await runLimit(() => run({ ...runFlags, clean: true }, configPath, [file], false, {
|
|
1289
|
-
reporter: buffered
|
|
1290
|
-
reporterKind: buffered
|
|
1435
|
+
reporter: buffered?.reporter,
|
|
1436
|
+
reporterKind: buffered?.reporterKind,
|
|
1291
1437
|
modeName,
|
|
1292
1438
|
emitRunComplete: false,
|
|
1293
1439
|
fileSummaryTotal: 1,
|
|
@@ -1295,9 +1441,11 @@ async function runRuntimeSingleParallel(runFlags, configPath, selectors, modeNam
|
|
|
1295
1441
|
modeSummaryExecuted: 1,
|
|
1296
1442
|
buildCommandsByFile: { [file]: buildCommandsByFile[file] ?? "" },
|
|
1297
1443
|
}));
|
|
1298
|
-
buffered
|
|
1444
|
+
buffered?.reporter.flush?.();
|
|
1299
1445
|
results[index] = result;
|
|
1300
|
-
|
|
1446
|
+
if (buffered && token != null) {
|
|
1447
|
+
queueDisplay.complete(token, buffered.output());
|
|
1448
|
+
}
|
|
1301
1449
|
});
|
|
1302
1450
|
queueDisplay.flush();
|
|
1303
1451
|
const summary = aggregateRunResults(results);
|
|
@@ -1321,7 +1469,7 @@ async function runRuntimeMatrixParallel(runFlags, configPath, selectors, modes,
|
|
|
1321
1469
|
if (!files.length) {
|
|
1322
1470
|
throw await buildNoTestFilesMatchedError(configPath, selectors);
|
|
1323
1471
|
}
|
|
1324
|
-
const reporterSession = await createRunReporter(configPath);
|
|
1472
|
+
const reporterSession = await createRunReporter(configPath, runFlags.reporterPath);
|
|
1325
1473
|
const reporter = reporterSession.reporter;
|
|
1326
1474
|
const snapshotEnabled = runFlags.snapshot !== false;
|
|
1327
1475
|
reporter.onRunStart?.({
|
|
@@ -1336,23 +1484,29 @@ async function runRuntimeMatrixParallel(runFlags, configPath, selectors, modes,
|
|
|
1336
1484
|
const showPerModeTimes = Boolean(runFlags.verbose);
|
|
1337
1485
|
const duplicateSpecBasenames = resolveDuplicateSpecBasenames(files);
|
|
1338
1486
|
const ordered = new Array(files.length);
|
|
1339
|
-
const
|
|
1487
|
+
const useQueueDisplay = reporterSession.reporterKind == "default";
|
|
1488
|
+
const queueDisplay = new ParallelQueueDisplay(useQueueDisplay && !runFlags.clean);
|
|
1340
1489
|
const poolWidth = Math.max(runFlags.jobs, runFlags.buildJobs, runFlags.runJobs);
|
|
1341
1490
|
const buildPool = new BuildWorkerPool(runFlags.buildJobs);
|
|
1342
1491
|
const buildIntervals = [];
|
|
1343
1492
|
try {
|
|
1344
1493
|
await runOrderedPool(files, poolWidth, async (file, fileIndex) => {
|
|
1345
1494
|
const fileName = path.basename(file);
|
|
1346
|
-
const token =
|
|
1495
|
+
const token = useQueueDisplay
|
|
1496
|
+
? renderQueuedFileStart(queueDisplay, fileName)
|
|
1497
|
+
: null;
|
|
1347
1498
|
const fileResults = [];
|
|
1348
1499
|
const modeTimes = modes.map(() => "...");
|
|
1500
|
+
const buildReuseCache = new Map();
|
|
1349
1501
|
for (let i = 0; i < modes.length; i++) {
|
|
1350
1502
|
const modeName = modes[i];
|
|
1351
1503
|
const buildStartedAt = Date.now();
|
|
1352
|
-
await
|
|
1504
|
+
await buildFileForMode(buildReuseCache, {
|
|
1353
1505
|
configPath,
|
|
1354
1506
|
file,
|
|
1355
1507
|
modeName,
|
|
1508
|
+
buildFeatureToggles: {},
|
|
1509
|
+
buildPool,
|
|
1356
1510
|
});
|
|
1357
1511
|
buildIntervals.push({ start: buildStartedAt, end: Date.now() });
|
|
1358
1512
|
const buildInvocation = await getBuildInvocationPreview(configPath, file, modeName, {});
|
|
@@ -1371,7 +1525,9 @@ async function runRuntimeMatrixParallel(runFlags, configPath, selectors, modes,
|
|
|
1371
1525
|
fileResults.push(result);
|
|
1372
1526
|
}
|
|
1373
1527
|
ordered[fileIndex] = { fileName, fileResults, modeTimes };
|
|
1374
|
-
|
|
1528
|
+
if (token != null) {
|
|
1529
|
+
queueDisplay.complete(token, formatMatrixFileResultLine(fileName, modeLabels, fileResults, modeTimes, showPerModeTimes) + "\n");
|
|
1530
|
+
}
|
|
1375
1531
|
});
|
|
1376
1532
|
}
|
|
1377
1533
|
finally {
|
|
@@ -1418,7 +1574,7 @@ async function runTestSingleParallel(runFlags, configPath, selectors, buildFeatu
|
|
|
1418
1574
|
if (!files.length && !fuzzEnabled) {
|
|
1419
1575
|
throw await buildNoTestFilesMatchedError(configPath, selectors);
|
|
1420
1576
|
}
|
|
1421
|
-
const reporterSession = await createRunReporter(configPath,
|
|
1577
|
+
const reporterSession = await createRunReporter(configPath, runFlags.reporterPath, modeName);
|
|
1422
1578
|
const reporter = reporterSession.reporter;
|
|
1423
1579
|
const snapshotEnabled = runFlags.snapshot !== false;
|
|
1424
1580
|
reporter.onRunStart?.({
|
|
@@ -1430,37 +1586,45 @@ async function runTestSingleParallel(runFlags, configPath, selectors, buildFeatu
|
|
|
1430
1586
|
});
|
|
1431
1587
|
const duplicateSpecBasenames = resolveDuplicateSpecBasenames(files);
|
|
1432
1588
|
const results = new Array(files.length);
|
|
1433
|
-
const
|
|
1589
|
+
const useQueueDisplay = reporterSession.reporterKind == "default";
|
|
1590
|
+
const queueDisplay = new ParallelQueueDisplay(useQueueDisplay && !runFlags.clean);
|
|
1434
1591
|
const poolWidth = Math.max(runFlags.jobs, runFlags.buildJobs, runFlags.runJobs);
|
|
1435
1592
|
const buildIntervals = [];
|
|
1436
1593
|
if (files.length) {
|
|
1437
1594
|
const buildPool = new BuildWorkerPool(runFlags.buildJobs);
|
|
1438
1595
|
try {
|
|
1439
1596
|
await runOrderedPool(files, poolWidth, async (file, index) => {
|
|
1440
|
-
const token =
|
|
1597
|
+
const token = useQueueDisplay
|
|
1598
|
+
? renderQueuedFileStart(queueDisplay, path.basename(file))
|
|
1599
|
+
: null;
|
|
1441
1600
|
const buildStartedAt = Date.now();
|
|
1442
|
-
await
|
|
1601
|
+
await buildFileForMode(new Map(), {
|
|
1443
1602
|
configPath,
|
|
1444
1603
|
file,
|
|
1445
1604
|
modeName,
|
|
1446
|
-
|
|
1605
|
+
buildFeatureToggles,
|
|
1606
|
+
buildPool,
|
|
1447
1607
|
});
|
|
1448
1608
|
buildIntervals.push({ start: buildStartedAt, end: Date.now() });
|
|
1449
1609
|
const buildInvocation = await getBuildInvocationPreview(configPath, file, modeName, buildFeatureToggles);
|
|
1450
1610
|
const artifactKey = resolvePerFileArtifactKey(file, duplicateSpecBasenames);
|
|
1451
|
-
const buffered =
|
|
1611
|
+
const buffered = useQueueDisplay
|
|
1612
|
+
? await createBufferedReporter(configPath, runFlags.reporterPath, modeName)
|
|
1613
|
+
: null;
|
|
1452
1614
|
const result = await run({ ...runFlags, clean: true }, configPath, [file], false, {
|
|
1453
|
-
reporter: buffered
|
|
1454
|
-
reporterKind: buffered
|
|
1615
|
+
reporter: buffered?.reporter,
|
|
1616
|
+
reporterKind: buffered?.reporterKind,
|
|
1455
1617
|
emitRunComplete: false,
|
|
1456
1618
|
logFileName: `test.${artifactKey}.log.json`,
|
|
1457
1619
|
coverageFileName: `coverage.${artifactKey}.log.json`,
|
|
1458
1620
|
buildCommand: formatBuildInvocation(buildInvocation),
|
|
1459
1621
|
modeName,
|
|
1460
1622
|
});
|
|
1461
|
-
buffered
|
|
1623
|
+
buffered?.reporter.flush?.();
|
|
1462
1624
|
results[index] = result;
|
|
1463
|
-
|
|
1625
|
+
if (buffered && token != null) {
|
|
1626
|
+
queueDisplay.complete(token, buffered.output());
|
|
1627
|
+
}
|
|
1464
1628
|
});
|
|
1465
1629
|
}
|
|
1466
1630
|
finally {
|
|
@@ -1509,7 +1673,7 @@ async function runTestMatrixParallel(runFlags, configPath, selectors, modes, bui
|
|
|
1509
1673
|
throw await buildNoTestFilesMatchedError(configPath, selectors, true);
|
|
1510
1674
|
}
|
|
1511
1675
|
}
|
|
1512
|
-
const reporterSession = await createRunReporter(configPath);
|
|
1676
|
+
const reporterSession = await createRunReporter(configPath, runFlags.reporterPath);
|
|
1513
1677
|
const reporter = reporterSession.reporter;
|
|
1514
1678
|
const snapshotEnabled = runFlags.snapshot !== false;
|
|
1515
1679
|
reporter.onRunStart?.({
|
|
@@ -1524,24 +1688,29 @@ async function runTestMatrixParallel(runFlags, configPath, selectors, modes, bui
|
|
|
1524
1688
|
const showPerModeTimes = Boolean(runFlags.verbose);
|
|
1525
1689
|
const duplicateSpecBasenames = resolveDuplicateSpecBasenames(files);
|
|
1526
1690
|
const ordered = new Array(files.length);
|
|
1527
|
-
const
|
|
1691
|
+
const useQueueDisplay = reporterSession.reporterKind == "default";
|
|
1692
|
+
const queueDisplay = new ParallelQueueDisplay(useQueueDisplay && !runFlags.clean);
|
|
1528
1693
|
const poolWidth = Math.max(runFlags.jobs, runFlags.buildJobs, runFlags.runJobs);
|
|
1529
1694
|
const buildPool = new BuildWorkerPool(runFlags.buildJobs);
|
|
1530
1695
|
const buildIntervals = [];
|
|
1531
1696
|
try {
|
|
1532
1697
|
await runOrderedPool(files, poolWidth, async (file, fileIndex) => {
|
|
1533
1698
|
const fileName = path.basename(file);
|
|
1534
|
-
const token =
|
|
1699
|
+
const token = useQueueDisplay
|
|
1700
|
+
? renderQueuedFileStart(queueDisplay, fileName)
|
|
1701
|
+
: null;
|
|
1535
1702
|
const fileResults = [];
|
|
1536
1703
|
const modeTimes = modes.map(() => "...");
|
|
1704
|
+
const buildReuseCache = new Map();
|
|
1537
1705
|
for (let i = 0; i < modes.length; i++) {
|
|
1538
1706
|
const modeName = modes[i];
|
|
1539
1707
|
const buildStartedAt = Date.now();
|
|
1540
|
-
await
|
|
1708
|
+
await buildFileForMode(buildReuseCache, {
|
|
1541
1709
|
configPath,
|
|
1542
1710
|
file,
|
|
1543
1711
|
modeName,
|
|
1544
|
-
|
|
1712
|
+
buildFeatureToggles,
|
|
1713
|
+
buildPool,
|
|
1545
1714
|
});
|
|
1546
1715
|
buildIntervals.push({ start: buildStartedAt, end: Date.now() });
|
|
1547
1716
|
const buildInvocation = await getBuildInvocationPreview(configPath, file, modeName, buildFeatureToggles);
|
|
@@ -1560,7 +1729,9 @@ async function runTestMatrixParallel(runFlags, configPath, selectors, modes, bui
|
|
|
1560
1729
|
fileResults.push(result);
|
|
1561
1730
|
}
|
|
1562
1731
|
ordered[fileIndex] = { fileName, fileResults, modeTimes };
|
|
1563
|
-
|
|
1732
|
+
if (token != null) {
|
|
1733
|
+
queueDisplay.complete(token, formatMatrixFileResultLine(fileName, modeLabels, fileResults, modeTimes, showPerModeTimes) + "\n");
|
|
1734
|
+
}
|
|
1564
1735
|
});
|
|
1565
1736
|
}
|
|
1566
1737
|
finally {
|
package/bin/reporters/default.js
CHANGED
|
@@ -465,12 +465,30 @@ function collectSuiteFailures(suite, file, path, printed) {
|
|
|
465
465
|
const loc = String(test.location ?? "");
|
|
466
466
|
const where = loc.length ? `${file}:${loc}` : file;
|
|
467
467
|
const modeName = String(suiteAny.modeName ?? "");
|
|
468
|
-
const
|
|
468
|
+
const message = String(test.message ?? "");
|
|
469
|
+
const dedupeKey = `${file}::${modeName}::${title}::${String(test.left)}::${String(test.right)}::${message}`;
|
|
469
470
|
if (printed.has(dedupeKey))
|
|
470
471
|
continue;
|
|
471
472
|
printed.add(dedupeKey);
|
|
472
473
|
const left = JSON.stringify(test.left);
|
|
473
474
|
const right = JSON.stringify(test.right);
|
|
475
|
+
if (left == "null" && right == "null") {
|
|
476
|
+
console.log(`${chalk.bgRed(" FAIL ")} ${chalk.dim(title)} ${chalk.dim("(" + where + ")")}`);
|
|
477
|
+
if (modeName.length) {
|
|
478
|
+
console.log(chalk.dim(`Mode: ${modeName}`));
|
|
479
|
+
}
|
|
480
|
+
const normalizedMessage = normalizeFailureMessage(message);
|
|
481
|
+
if (normalizedMessage.length) {
|
|
482
|
+
for (const line of normalizedMessage.split("\n")) {
|
|
483
|
+
console.log(chalk.dim(line));
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
else {
|
|
487
|
+
console.log(chalk.dim("runtime error"));
|
|
488
|
+
}
|
|
489
|
+
console.log("");
|
|
490
|
+
continue;
|
|
491
|
+
}
|
|
474
492
|
const diffResult = diff(left, right);
|
|
475
493
|
let expected = "";
|
|
476
494
|
for (const res of diffResult.diff) {
|
|
@@ -506,6 +524,9 @@ function collectSuiteFailures(suite, file, path, printed) {
|
|
|
506
524
|
collectSuiteFailures(sub, file, nextPath, printed);
|
|
507
525
|
}
|
|
508
526
|
}
|
|
527
|
+
function normalizeFailureMessage(message) {
|
|
528
|
+
return message.replace(/\r\n/g, "\n").trim();
|
|
529
|
+
}
|
|
509
530
|
function renderSnapshotSummary(snapshotSummary, leadingGap = true) {
|
|
510
531
|
if (leadingGap) {
|
|
511
532
|
console.log("");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "as-test",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.5",
|
|
4
4
|
"author": "Jairus Tanaka",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -64,9 +64,9 @@
|
|
|
64
64
|
"access": "public"
|
|
65
65
|
},
|
|
66
66
|
"scripts": {
|
|
67
|
-
"test": "node ./bin/index.js test",
|
|
67
|
+
"test": "node ./bin/index.js test --parallel",
|
|
68
|
+
"test:ci": "node ./bin/index.js test --parallel --tap --config ./as-test.ci.config.json",
|
|
68
69
|
"fuzz": "node ./bin/index.js fuzz",
|
|
69
|
-
"test:ci": "node ./bin/index.js test --tap --config ./as-test.ci.config.json",
|
|
70
70
|
"test:examples": "npm --prefix ./examples run test",
|
|
71
71
|
"ci:act": "bash ./tools/act.sh push",
|
|
72
72
|
"ci:act:pr": "bash ./tools/act.sh pull_request",
|