as-test 0.4.4 → 0.5.1

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.
Files changed (67) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/README.md +196 -82
  3. package/as-test.config.schema.json +137 -0
  4. package/assembly/coverage.ts +19 -0
  5. package/assembly/index.ts +172 -85
  6. package/assembly/src/expectation.ts +263 -199
  7. package/assembly/src/log.ts +1 -9
  8. package/assembly/src/suite.ts +61 -25
  9. package/assembly/src/tests.ts +2 -0
  10. package/assembly/util/wipc.ts +286 -0
  11. package/bin/build.js +86 -41
  12. package/bin/index.js +337 -68
  13. package/bin/init.js +441 -183
  14. package/bin/reporter.js +1 -1
  15. package/bin/reporters/default.js +379 -0
  16. package/bin/reporters/types.js +1 -0
  17. package/bin/run.js +882 -194
  18. package/bin/types.js +14 -7
  19. package/bin/util.js +54 -3
  20. package/package.json +34 -16
  21. package/transform/lib/builder.js +169 -169
  22. package/transform/lib/builder.js.map +1 -1
  23. package/transform/lib/coverage.js +47 -1
  24. package/transform/lib/coverage.js.map +1 -1
  25. package/transform/lib/index.js +70 -0
  26. package/transform/lib/index.js.map +1 -1
  27. package/transform/lib/location.js +20 -0
  28. package/transform/lib/location.js.map +1 -0
  29. package/transform/lib/log.js +118 -0
  30. package/transform/lib/log.js.map +1 -0
  31. package/transform/lib/mock.js +2 -2
  32. package/transform/lib/mock.js.map +1 -1
  33. package/transform/lib/util.js +3 -3
  34. package/transform/lib/util.js.map +1 -1
  35. package/.github/workflows/as-test.yml +0 -26
  36. package/.prettierrc +0 -3
  37. package/as-test.config.json +0 -19
  38. package/assembly/__tests__/array.spec.ts +0 -25
  39. package/assembly/__tests__/math.spec.ts +0 -16
  40. package/assembly/__tests__/mock.spec.ts +0 -22
  41. package/assembly/__tests__/mock.ts +0 -7
  42. package/assembly/__tests__/sleep.spec.ts +0 -28
  43. package/assembly/tsconfig.json +0 -97
  44. package/assets/img/screenshot.png +0 -0
  45. package/cli/build.ts +0 -117
  46. package/cli/index.ts +0 -190
  47. package/cli/init.ts +0 -247
  48. package/cli/reporter.ts +0 -1
  49. package/cli/run.ts +0 -286
  50. package/cli/tsconfig.json +0 -9
  51. package/cli/types.ts +0 -29
  52. package/cli/util.ts +0 -65
  53. package/run/package.json +0 -27
  54. package/tests/array.run.js +0 -7
  55. package/tests/math.run.js +0 -7
  56. package/tests/mock.run.js +0 -14
  57. package/tests/sleep.run.js +0 -7
  58. package/transform/src/builder.ts +0 -1474
  59. package/transform/src/coverage.ts +0 -580
  60. package/transform/src/index.ts +0 -73
  61. package/transform/src/linker.ts +0 -41
  62. package/transform/src/mock.ts +0 -163
  63. package/transform/src/range.ts +0 -12
  64. package/transform/src/types.ts +0 -35
  65. package/transform/src/util.ts +0 -81
  66. package/transform/src/visitor.ts +0 -744
  67. package/transform/tsconfig.json +0 -10
package/bin/run.js CHANGED
@@ -1,236 +1,924 @@
1
1
  import chalk from "chalk";
2
- import { exec } from "child_process";
2
+ import { spawn } from "child_process";
3
3
  import { glob } from "glob";
4
- import { formatTime, getExec, loadConfig } from "./util.js";
4
+ import { getExec, loadConfig } from "./util.js";
5
5
  import * as path from "path";
6
- import { existsSync, mkdirSync, writeFileSync } from "fs";
7
- import { diff } from "typer-diff";
8
- import gradient from "gradient-string";
9
- const CONFIG_PATH = path.join(process.cwd(), "./as-test.config.json");
10
- const version = "0.4.0";
11
- export async function run() {
6
+ import { pathToFileURL } from "url";
7
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
8
+ import { createReporter as createDefaultReporter } from "./reporters/default.js";
9
+ const DEFAULT_CONFIG_PATH = path.join(process.cwd(), "./as-test.config.json");
10
+ var MessageType;
11
+ (function (MessageType) {
12
+ MessageType[MessageType["OPEN"] = 0] = "OPEN";
13
+ MessageType[MessageType["CLOSE"] = 1] = "CLOSE";
14
+ MessageType[MessageType["CALL"] = 2] = "CALL";
15
+ MessageType[MessageType["DATA"] = 3] = "DATA";
16
+ })(MessageType || (MessageType = {}));
17
+ class Channel {
18
+ constructor(input, output) {
19
+ this.input = input;
20
+ this.output = output;
21
+ this.buffer = Buffer.alloc(0);
22
+ this.input.on("data", (chunk) => this.onData(chunk));
23
+ }
24
+ send(type, payload) {
25
+ const body = payload ?? Buffer.alloc(0);
26
+ const header = Buffer.alloc(Channel.HEADER_SIZE);
27
+ Channel.MAGIC.copy(header, 0);
28
+ header.writeUInt8(type, 4);
29
+ header.writeUInt32LE(body.length, 5);
30
+ this.output.write(Buffer.concat([header, body]));
31
+ }
32
+ sendJSON(type, msg) {
33
+ this.send(type, Buffer.from(JSON.stringify(msg), "utf8"));
34
+ }
35
+ onData(chunk) {
36
+ this.buffer = Buffer.concat([this.buffer, chunk]);
37
+ while (true) {
38
+ if (this.buffer.length === 0)
39
+ return;
40
+ const idx = this.buffer.indexOf(Channel.MAGIC);
41
+ if (idx === -1) {
42
+ this.onPassthrough(this.buffer);
43
+ this.buffer = Buffer.alloc(0);
44
+ return;
45
+ }
46
+ if (idx > 0) {
47
+ this.onPassthrough(this.buffer.subarray(0, idx));
48
+ this.buffer = this.buffer.subarray(idx);
49
+ }
50
+ if (this.buffer.length < Channel.HEADER_SIZE)
51
+ return;
52
+ const type = this.buffer.readUInt8(4);
53
+ const length = this.buffer.readUInt32LE(5);
54
+ const frameSize = Channel.HEADER_SIZE + length;
55
+ if (this.buffer.length < frameSize)
56
+ return;
57
+ const payload = this.buffer.subarray(Channel.HEADER_SIZE, frameSize);
58
+ this.buffer = this.buffer.subarray(frameSize);
59
+ this.handleFrame(type, payload);
60
+ }
61
+ }
62
+ handleFrame(type, payload) {
63
+ switch (type) {
64
+ case MessageType.OPEN:
65
+ this.onOpen();
66
+ break;
67
+ case MessageType.CLOSE:
68
+ this.onClose();
69
+ break;
70
+ case MessageType.CALL:
71
+ this.onCall(JSON.parse(payload.toString("utf8")));
72
+ break;
73
+ case MessageType.DATA:
74
+ this.onDataMessage(payload);
75
+ break;
76
+ default:
77
+ this.onPassthrough(payload);
78
+ }
79
+ }
80
+ onPassthrough(_data) { }
81
+ onOpen() { }
82
+ onClose() { }
83
+ onCall(_msg) { }
84
+ onDataMessage(_data) { }
85
+ }
86
+ Channel.MAGIC = Buffer.from("WIPC");
87
+ Channel.HEADER_SIZE = 9;
88
+ class SnapshotStore {
89
+ constructor(specFile, snapshotDir) {
90
+ this.dirty = false;
91
+ this.created = 0;
92
+ this.updated = 0;
93
+ this.matched = 0;
94
+ this.failed = 0;
95
+ this.warnedMissing = new Set();
96
+ const base = path.basename(specFile, ".ts");
97
+ const dir = path.join(process.cwd(), snapshotDir);
98
+ if (!existsSync(dir))
99
+ mkdirSync(dir, { recursive: true });
100
+ this.filePath = path.join(dir, `${base}.snap.json`);
101
+ this.data = existsSync(this.filePath)
102
+ ? JSON.parse(readFileSync(this.filePath, "utf8"))
103
+ : {};
104
+ }
105
+ assert(key, actual, allowSnapshot, updateSnapshots) {
106
+ if (!allowSnapshot)
107
+ return { ok: true, expected: actual, warnMissing: false };
108
+ if (!(key in this.data)) {
109
+ if (!updateSnapshots) {
110
+ this.failed++;
111
+ const warnMissing = !this.warnedMissing.has(key);
112
+ if (warnMissing)
113
+ this.warnedMissing.add(key);
114
+ return {
115
+ ok: false,
116
+ expected: JSON.stringify("<missing snapshot>"),
117
+ warnMissing,
118
+ };
119
+ }
120
+ this.created++;
121
+ this.dirty = true;
122
+ this.data[key] = actual;
123
+ return { ok: true, expected: actual, warnMissing: false };
124
+ }
125
+ const expected = this.data[key];
126
+ if (expected === actual) {
127
+ this.matched++;
128
+ return { ok: true, expected, warnMissing: false };
129
+ }
130
+ if (!updateSnapshots) {
131
+ this.failed++;
132
+ return { ok: false, expected, warnMissing: false };
133
+ }
134
+ this.updated++;
135
+ this.dirty = true;
136
+ this.data[key] = actual;
137
+ return { ok: true, expected: actual, warnMissing: false };
138
+ }
139
+ flush() {
140
+ if (!this.dirty)
141
+ return;
142
+ writeFileSync(this.filePath, JSON.stringify(this.data, null, 2));
143
+ }
144
+ }
145
+ export async function run(flags = {}, configPath = DEFAULT_CONFIG_PATH, selectors = [], shouldExit = true, options = {}) {
146
+ const resolvedConfigPath = configPath ?? DEFAULT_CONFIG_PATH;
12
147
  const reports = [];
13
- const config = loadConfig(CONFIG_PATH);
14
- const inputFiles = await glob(config.input);
15
- console.log(chalk.dim("Running tests using " + config.runOptions.runtime.name + ""));
16
- const command = config.runOptions.runtime.run.split(" ")[0];
17
- let execPath = getExec(command);
148
+ const config = loadConfig(resolvedConfigPath);
149
+ const inputPatterns = resolveInputPatterns(config.input, selectors);
150
+ const inputFiles = await glob(inputPatterns);
151
+ const snapshotEnabled = flags.snapshot !== false;
152
+ const updateSnapshots = Boolean(flags.updateSnapshots);
153
+ const cleanOutput = Boolean(flags.clean);
154
+ const showCoverage = Boolean(flags.showCoverage);
155
+ const coverage = resolveCoverageOptions(config.coverage);
156
+ const coverageEnabled = coverage.enabled;
157
+ const coverageDir = config.coverageDir ?? "./.as-test/coverage";
158
+ const runtimeCommand = resolveRuntimeCommand(getConfiguredRuntimeCmd(config), config.buildOptions.target);
159
+ const reporter = options.reporter ??
160
+ (await loadReporter(config.runOptions.reporter, resolvedConfigPath, {
161
+ stdout: process.stdout,
162
+ stderr: process.stderr,
163
+ }));
164
+ const command = runtimeCommand.split(" ")[0];
165
+ const execPath = getExec(command);
18
166
  if (!execPath) {
19
- console.log(`${chalk.bgRed(" ERROR ")}${chalk.dim(":")} could not locate ${command} in PATH variable!`);
20
- process.exit(0);
21
- }
22
- if (inputFiles.length) {
23
- console.log("\n" +
24
- gradient(["#87afff", "#3a5fcd"])
25
- .multiline(` █████ ███████ ████████ ███████ ███████ ████████
26
- ██ ██ ██ ██ ██ ██ ██
27
- ███████ ███████ █████ ██ █████ ███████ ██
28
- ██ ██ ██ ██ ██ ██ ██
29
- ██ ██ ███████ ██ ███████ ███████ ██ `));
30
- console.log(chalk.dim(`\n----------------------- v${version} -----------------------\n`));
31
- }
32
- for (const plugin of Object.keys(config.plugins)) {
33
- if (!config.plugins[plugin])
34
- continue;
35
- console.log(chalk.bgBlueBright(" PLUGIN ") +
36
- " " +
37
- chalk.dim("Using " + plugin.slice(0, 1).toUpperCase() + plugin.slice(1)) +
38
- "\n");
167
+ const message = `${chalk.bgRed(" ERROR ")}${chalk.dim(":")} could not locate ${command} in PATH variable!`;
168
+ if (shouldExit) {
169
+ console.log(message);
170
+ process.exit(1);
171
+ }
172
+ throw new Error(message);
173
+ }
174
+ if (options.emitRunStart !== false) {
175
+ reporter.onRunStart?.({
176
+ runtimeName: runtimeNameFromCommand(runtimeCommand),
177
+ clean: cleanOutput,
178
+ verbose: Boolean(flags.verbose),
179
+ snapshotEnabled,
180
+ updateSnapshots,
181
+ });
39
182
  }
183
+ if (showCoverage && !coverageEnabled) {
184
+ process.stderr.write(chalk.dim("coverage point output requested with --show-coverage, but coverage is disabled\n"));
185
+ }
186
+ const snapshotSummary = {
187
+ matched: 0,
188
+ created: 0,
189
+ updated: 0,
190
+ failed: 0,
191
+ };
40
192
  for (let i = 0; i < inputFiles.length; i++) {
41
193
  const file = inputFiles[i];
42
194
  const outFile = path.join(config.outDir, file.slice(file.lastIndexOf("/") + 1).replace(".ts", ".wasm"));
43
- let cmd = config.runOptions.runtime.run.replace(command, execPath);
44
- if (config.buildOptions.target == "bindings") {
45
- cmd = config.runOptions.runtime.run.replace(command, execPath);
46
- if (cmd.includes("<name>")) {
47
- cmd = cmd.replace("<name>", file
48
- .slice(file.lastIndexOf("/") + 1)
49
- .replace(".ts", "")
50
- .replace(".spec", ""));
51
- }
52
- else {
53
- cmd = cmd.replace("<file>", outFile
54
- .replace("build", "tests")
55
- .replace(".spec", "")
56
- .replace(".wasm", ".run.js"));
57
- }
195
+ const fileBase = file
196
+ .slice(file.lastIndexOf("/") + 1)
197
+ .replace(".ts", "")
198
+ .replace(".spec", "");
199
+ let cmd = runtimeCommand.replace(command, execPath);
200
+ cmd = cmd.replace("<name>", fileBase);
201
+ if (config.buildOptions.target == "bindings" && !cmd.includes("<file>")) {
202
+ cmd = cmd.replace("<file>", outFile
203
+ .replace("build", "tests")
204
+ .replace(".spec", "")
205
+ .replace(".wasm", ".run.js"));
58
206
  }
59
207
  else {
60
208
  cmd = cmd.replace("<file>", outFile);
61
209
  }
62
- const report = JSON.parse(await (() => {
63
- return new Promise((res, _) => {
64
- let stdout = "";
65
- const io = exec(cmd);
66
- io.stdout.pipe(process.stdout);
67
- io.stderr.pipe(process.stderr);
68
- io.stdout.on("data", (data) => {
69
- stdout += readData(data);
70
- });
71
- io.stdout.on("close", () => {
72
- res(stdout);
73
- });
74
- });
75
- })());
76
- reports.push(report);
210
+ const snapshotStore = new SnapshotStore(file, config.snapshotDir);
211
+ const report = await runProcess(cmd, snapshotStore, snapshotEnabled, updateSnapshots, reporter);
212
+ const normalized = normalizeReport(report);
213
+ snapshotStore.flush();
214
+ snapshotSummary.matched += snapshotStore.matched;
215
+ snapshotSummary.created += snapshotStore.created;
216
+ snapshotSummary.updated += snapshotStore.updated;
217
+ snapshotSummary.failed += snapshotStore.failed;
218
+ reports.push({
219
+ file,
220
+ suites: normalized.suites,
221
+ coverage: normalized.coverage,
222
+ });
77
223
  }
78
224
  if (config.logs && config.logs != "none") {
79
225
  if (!existsSync(path.join(process.cwd(), config.logs))) {
80
- mkdirSync(path.join(process.cwd(), config.logs));
81
- }
82
- writeFileSync(path.join(process.cwd(), config.logs, "test.log.json"), JSON.stringify(reports, null, 2));
83
- }
84
- const reporter = new Reporter(reports);
85
- if (reporter.failed.length) {
86
- console.log(chalk.dim("----------------- [FAILED] -------------------\n"));
87
- for (const failed of reporter.failed) {
88
- console.log(`${chalk.bgRed(" FAIL ")} ${chalk.dim(failed.description)}\n`);
89
- for (const test of failed.tests) {
90
- const diffResult = diff(JSON.stringify(test.left), JSON.stringify(test.right));
91
- let expected = "";
92
- let received = chalk.dim(JSON.stringify(test.left));
93
- for (const res of diffResult.diff) {
94
- switch (res.type) {
95
- case "correct": {
96
- expected += chalk.dim(res.value);
97
- continue;
98
- }
99
- case "extra": {
100
- expected += chalk.red.strikethrough(res.value);
101
- continue;
102
- }
103
- case "missing": {
104
- expected += chalk.bgBlack(res.value);
105
- continue;
106
- }
107
- case "wrong": {
108
- expected += chalk.bgRed(res.value);
109
- continue;
110
- }
111
- case "untouched": {
112
- //received += chalk.bgBlackBright(res.value);
113
- continue;
114
- }
115
- case "spacer": {
116
- //received += chalk.bgBlackBright(res.value);
117
- continue;
118
- }
119
- }
120
- }
121
- if (test.verdict == "fail") {
122
- console.log(`${chalk.dim("(expected) ->")} ${expected}`);
123
- console.log(`${chalk.dim("(received) ->")} ${received}\n`);
124
- }
226
+ mkdirSync(path.join(process.cwd(), config.logs), { recursive: true });
227
+ }
228
+ const logReports = reports.map((report) => ({
229
+ file: report.file,
230
+ suites: report.suites,
231
+ }));
232
+ writeFileSync(path.join(process.cwd(), config.logs, options.logFileName ?? "test.log.json"), JSON.stringify(logReports, null, 2));
233
+ }
234
+ const stats = collectRunStats(reports.map((report) => report.suites));
235
+ const coverageSummary = collectCoverageSummary(reports, coverageEnabled, showCoverage, coverage);
236
+ if (coverageEnabled &&
237
+ coverageDir &&
238
+ coverageDir != "none" &&
239
+ coverageSummary.files.length > 0) {
240
+ const resolvedCoverageDir = path.join(process.cwd(), coverageDir);
241
+ if (!existsSync(resolvedCoverageDir)) {
242
+ mkdirSync(resolvedCoverageDir, { recursive: true });
243
+ }
244
+ writeFileSync(path.join(resolvedCoverageDir, options.coverageFileName ?? "coverage.log.json"), JSON.stringify(coverageSummary, null, 2));
245
+ }
246
+ if (options.emitRunComplete !== false) {
247
+ reporter.onRunComplete?.({
248
+ clean: cleanOutput,
249
+ snapshotEnabled,
250
+ showCoverage,
251
+ snapshotSummary,
252
+ coverageSummary,
253
+ stats,
254
+ reports,
255
+ });
256
+ }
257
+ const failed = Boolean(stats.failedFiles || snapshotSummary.failed);
258
+ if (shouldExit) {
259
+ process.exit(failed ? 1 : 0);
260
+ }
261
+ return {
262
+ failed,
263
+ stats,
264
+ snapshotSummary,
265
+ coverageSummary,
266
+ reports,
267
+ };
268
+ }
269
+ function resolveRuntimeCommand(runtimeRun, target, emitWarnings = true) {
270
+ const normalized = resolveLegacyWasiRuntime(runtimeRun, target, emitWarnings);
271
+ return fallbackToDefaultRuntime(normalized, target, emitWarnings);
272
+ }
273
+ function resolveLegacyWasiRuntime(runtimeRun, target, emitWarnings) {
274
+ if (target != "wasi")
275
+ return runtimeRun;
276
+ const preferredPath = "./.as-test/runners/default.wasi.js";
277
+ const legacyPaths = ["./bin/wasi-run.js", "./.as-test/wasi/wasi.run.js"];
278
+ if (runtimeRun.includes(preferredPath)) {
279
+ const resolvedPreferredPath = path.join(process.cwd(), preferredPath);
280
+ if (existsSync(resolvedPreferredPath))
281
+ return runtimeRun;
282
+ throw new Error(`could not locate WASI runner at ${preferredPath}. Run "ast init --target wasi --force --yes" to scaffold the local runner.`);
283
+ }
284
+ for (const legacyPath of legacyPaths) {
285
+ if (!runtimeRun.includes(legacyPath))
286
+ continue;
287
+ const resolvedLegacyPath = path.join(process.cwd(), legacyPath);
288
+ if (existsSync(resolvedLegacyPath))
289
+ return runtimeRun;
290
+ const resolvedPreferredPath = path.join(process.cwd(), preferredPath);
291
+ if (existsSync(resolvedPreferredPath)) {
292
+ if (emitWarnings) {
293
+ process.stderr.write(chalk.dim(`legacy WASI runtime path detected (${legacyPath}); using ${preferredPath}\n`));
125
294
  }
295
+ return runtimeRun.replace(legacyPath, preferredPath);
126
296
  }
297
+ throw new Error(`could not locate WASI runner. Expected ${legacyPath} or ${preferredPath}. Run "ast init --target wasi --force --yes" to scaffold the local runner.`);
127
298
  }
128
- console.log(chalk.dim("----------------- [RESULTS] ------------------\n"));
129
- process.stdout.write(chalk.bold("Files: "));
130
- if (reporter.failedFiles) {
131
- process.stdout.write(chalk.bold.red(reporter.failedFiles + " failed"));
299
+ return runtimeRun;
300
+ }
301
+ function fallbackToDefaultRuntime(runtimeRun, target, emitWarnings) {
302
+ const scriptPath = extractRuntimeScriptPath(runtimeRun);
303
+ if (!scriptPath)
304
+ return runtimeRun;
305
+ const resolvedScriptPath = path.isAbsolute(scriptPath)
306
+ ? scriptPath
307
+ : path.join(process.cwd(), scriptPath);
308
+ if (existsSync(resolvedScriptPath))
309
+ return runtimeRun;
310
+ const fallback = getDefaultRuntimeFallback(target);
311
+ if (!fallback)
312
+ return runtimeRun;
313
+ const resolvedFallbackPath = path.join(process.cwd(), fallback.scriptPath);
314
+ if (!existsSync(resolvedFallbackPath)) {
315
+ if (scriptPath == fallback.scriptPath) {
316
+ throw new Error(`could not locate runtime script at ${fallback.scriptPath}. Run "ast init --target ${target} --force --yes" to scaffold the local runner.`);
317
+ }
318
+ throw new Error(`could not locate runtime script at ${scriptPath}. Default runner ${fallback.scriptPath} is also missing. Run "ast init --target ${target} --force --yes" to scaffold it.`);
132
319
  }
133
- else {
134
- process.stdout.write(chalk.bold.greenBright("0 failed"));
320
+ if (emitWarnings) {
321
+ process.stderr.write(chalk.dim(`runtime script not found (${scriptPath}); using ${fallback.scriptPath}\n`));
135
322
  }
136
- process.stdout.write(", " + (reporter.failedFiles + reporter.passedFiles) + " total\n");
137
- process.stdout.write(chalk.bold("Suites: "));
138
- if (reporter.failedSuites) {
139
- process.stdout.write(chalk.bold.red(reporter.failedSuites + " failed"));
323
+ return fallback.command;
324
+ }
325
+ function getDefaultRuntimeFallback(target) {
326
+ if (target == "wasi") {
327
+ return {
328
+ command: "node ./.as-test/runners/default.wasi.js <file>",
329
+ scriptPath: "./.as-test/runners/default.wasi.js",
330
+ };
140
331
  }
141
- else {
142
- process.stdout.write(chalk.bold.greenBright("0 failed"));
332
+ if (target == "bindings") {
333
+ return {
334
+ command: "node ./.as-test/runners/default.run.js <file>",
335
+ scriptPath: "./.as-test/runners/default.run.js",
336
+ };
143
337
  }
144
- process.stdout.write(", " + (reporter.failedSuites + reporter.passedSuites) + " total\n");
145
- process.stdout.write(chalk.bold("Tests: "));
146
- if (reporter.failedTests) {
147
- process.stdout.write(chalk.bold.red(reporter.failedTests + " failed"));
338
+ return null;
339
+ }
340
+ function extractRuntimeScriptPath(runtimeRun) {
341
+ const tokens = runtimeRun.trim().split(/\s+/).filter((token) => token.length > 0);
342
+ if (tokens.length < 2)
343
+ return null;
344
+ const execToken = path.basename(tokens[0]).toLowerCase();
345
+ if (!isScriptHostRuntime(execToken))
346
+ return null;
347
+ for (let i = 1; i < tokens.length; i++) {
348
+ const token = tokens[i];
349
+ if (token == "--") {
350
+ const next = tokens[i + 1];
351
+ if (next && isLikelyRuntimeScriptPath(next))
352
+ return next;
353
+ return null;
354
+ }
355
+ if (token.startsWith("-"))
356
+ continue;
357
+ if (isLikelyRuntimeScriptPath(token))
358
+ return token;
359
+ return null;
148
360
  }
149
- else {
150
- process.stdout.write(chalk.bold.greenBright("0 failed"));
151
- }
152
- process.stdout.write(", " + (reporter.failedTests + reporter.passedTests) + " total\n");
153
- process.stdout.write(chalk.bold("Time: ") + formatTime(reporter.time) + "\n");
154
- if (reporter.failedFiles)
155
- process.exit(1);
156
- process.exit(0);
157
- }
158
- class Reporter {
159
- constructor(reports) {
160
- this.passedFiles = 0;
161
- this.failedFiles = 0;
162
- this.passedSuites = 0;
163
- this.failedSuites = 0;
164
- this.passedTests = 0;
165
- this.failedTests = 0;
166
- this.failed = [];
167
- this.time = 0.0;
168
- this.readReports(reports);
169
- }
170
- readReports(reports) {
171
- for (const file of reports) {
172
- this.readFile(file);
173
- }
174
- }
175
- readFile(file) {
176
- let failed = false;
177
- for (const suite of file) {
178
- if (suite.verdict == "fail") {
179
- failed = true;
180
- this.failedSuites++;
361
+ return null;
362
+ }
363
+ function isScriptHostRuntime(execToken) {
364
+ return (execToken == "node" ||
365
+ execToken == "node.exe" ||
366
+ execToken == "node.cmd" ||
367
+ execToken == "bun" ||
368
+ execToken == "bun.exe" ||
369
+ execToken == "bun.cmd" ||
370
+ execToken == "deno" ||
371
+ execToken == "deno.exe" ||
372
+ execToken == "deno.cmd" ||
373
+ execToken == "tsx" ||
374
+ execToken == "tsx.cmd" ||
375
+ execToken == "ts-node" ||
376
+ execToken == "ts-node.cmd");
377
+ }
378
+ function isLikelyRuntimeScriptPath(token) {
379
+ if (!token.length)
380
+ return false;
381
+ if (token == "<file>" || token == "<name>")
382
+ return false;
383
+ if (token.includes("://"))
384
+ return false;
385
+ if (token.startsWith("-"))
386
+ return false;
387
+ if (token.startsWith("./"))
388
+ return true;
389
+ if (token.startsWith("../"))
390
+ return true;
391
+ if (token.startsWith("/"))
392
+ return true;
393
+ if (token.startsWith(".\\"))
394
+ return true;
395
+ if (token.startsWith("..\\"))
396
+ return true;
397
+ if (/^[A-Za-z]:[\\/]/.test(token))
398
+ return true;
399
+ return /\.(mjs|cjs|js|ts)$/.test(token);
400
+ }
401
+ function getConfiguredRuntimeCmd(config) {
402
+ const runtime = config.runOptions.runtime;
403
+ if (runtime.cmd && runtime.cmd.length)
404
+ return runtime.cmd;
405
+ if (runtime.run && runtime.run.length)
406
+ return runtime.run;
407
+ throw new Error(`runtime command is missing. Set "runOptions.runtime.cmd" in as-test.config.json`);
408
+ }
409
+ function runtimeNameFromCommand(command) {
410
+ const token = command.trim().split(/\s+/)[0];
411
+ return token && token.length ? token : "runtime";
412
+ }
413
+ function resolveInputPatterns(configured, selectors) {
414
+ const configuredInputs = Array.isArray(configured) ? configured : [configured];
415
+ if (!selectors.length)
416
+ return configuredInputs;
417
+ const patterns = new Set();
418
+ for (const selector of selectors) {
419
+ if (!selector)
420
+ continue;
421
+ if (isBareSuiteSelector(selector)) {
422
+ const base = stripSuiteSuffix(selector);
423
+ for (const configuredInput of configuredInputs) {
424
+ patterns.add(path.join(path.dirname(configuredInput), `${base}.spec.ts`));
181
425
  }
182
- else {
183
- this.passedSuites++;
426
+ continue;
427
+ }
428
+ patterns.add(selector);
429
+ }
430
+ return [...patterns];
431
+ }
432
+ function isBareSuiteSelector(selector) {
433
+ return (!selector.includes("/") &&
434
+ !selector.includes("\\") &&
435
+ !/[*?[\]{}]/.test(selector));
436
+ }
437
+ function stripSuiteSuffix(selector) {
438
+ return selector.replace(/\.spec\.ts$/, "").replace(/\.ts$/, "");
439
+ }
440
+ function normalizeReport(raw) {
441
+ if (Array.isArray(raw)) {
442
+ return {
443
+ suites: raw,
444
+ coverage: {
445
+ total: 0,
446
+ covered: 0,
447
+ uncovered: 0,
448
+ percent: 100,
449
+ points: [],
450
+ },
451
+ };
452
+ }
453
+ const value = raw;
454
+ if (!value) {
455
+ return {
456
+ suites: [],
457
+ coverage: {
458
+ total: 0,
459
+ covered: 0,
460
+ uncovered: 0,
461
+ percent: 100,
462
+ points: [],
463
+ },
464
+ };
465
+ }
466
+ const suites = Array.isArray(value.suites) ? value.suites : [];
467
+ const coverage = normalizeCoverage(value.coverage);
468
+ return { suites, coverage };
469
+ }
470
+ function normalizeCoverage(value) {
471
+ const raw = value;
472
+ const total = Number(raw?.total ?? 0);
473
+ const uncovered = Number(raw?.uncovered ?? 0);
474
+ const covered = raw?.covered != null ? Number(raw.covered) : Math.max(total - uncovered, 0);
475
+ const percent = raw?.percent != null
476
+ ? Number(raw.percent)
477
+ : total
478
+ ? (covered * 100) / total
479
+ : 100;
480
+ const pointsRaw = Array.isArray(raw?.points)
481
+ ? raw?.points
482
+ : [];
483
+ const points = pointsRaw
484
+ .map((point) => {
485
+ const p = point;
486
+ return {
487
+ hash: String(p.hash ?? ""),
488
+ file: String(p.file ?? ""),
489
+ line: Number(p.line ?? 0),
490
+ column: Number(p.column ?? 0),
491
+ type: String(p.type ?? ""),
492
+ executed: Boolean(p.executed),
493
+ };
494
+ })
495
+ .filter((point) => point.file.length > 0);
496
+ return {
497
+ total,
498
+ covered,
499
+ uncovered,
500
+ percent,
501
+ points,
502
+ };
503
+ }
504
+ function collectCoverageSummary(reports, enabled, showPoints, coverage) {
505
+ const summary = {
506
+ enabled,
507
+ showPoints,
508
+ total: 0,
509
+ covered: 0,
510
+ uncovered: 0,
511
+ percent: 100,
512
+ files: [],
513
+ };
514
+ const uniquePoints = new Map();
515
+ const hasDetailedPoints = reports.some((report) => report.coverage.points.length > 0);
516
+ for (const report of reports) {
517
+ for (const point of report.coverage.points) {
518
+ if (isIgnoredCoverageFile(point.file, coverage))
519
+ continue;
520
+ const key = `${point.file}::${point.hash}`;
521
+ const existing = uniquePoints.get(key);
522
+ if (!existing) {
523
+ uniquePoints.set(key, { ...point });
184
524
  }
185
- this.time += suite.time.end - suite.time.start;
186
- for (const subSuite of suite.suites) {
187
- this.readSuite(subSuite);
525
+ else if (point.executed) {
526
+ existing.executed = true;
188
527
  }
189
- for (const test of suite.tests) {
190
- if (test.verdict == "fail")
191
- this.failed.push(suite);
192
- this.readTest(test);
528
+ }
529
+ }
530
+ if (uniquePoints.size > 0) {
531
+ const byFile = new Map();
532
+ for (const point of uniquePoints.values()) {
533
+ if (!byFile.has(point.file))
534
+ byFile.set(point.file, []);
535
+ byFile.get(point.file).push(point);
536
+ summary.total++;
537
+ if (point.executed)
538
+ summary.covered++;
539
+ else
540
+ summary.uncovered++;
541
+ }
542
+ const sortedFiles = [...byFile.keys()].sort((a, b) => a.localeCompare(b));
543
+ for (const file of sortedFiles) {
544
+ const points = byFile.get(file);
545
+ points.sort(compareCoveragePoints);
546
+ let covered = 0;
547
+ for (const point of points) {
548
+ if (point.executed)
549
+ covered++;
193
550
  }
551
+ const total = points.length;
552
+ if (!total)
553
+ continue;
554
+ const uncovered = total - covered;
555
+ summary.files.push({
556
+ file,
557
+ total,
558
+ covered,
559
+ uncovered,
560
+ percent: total ? (covered * 100) / total : 100,
561
+ points,
562
+ });
194
563
  }
195
- if (failed)
196
- this.failedFiles++;
197
- else
198
- this.passedFiles++;
199
564
  }
200
- readSuite(suite) {
201
- if (suite.verdict == "fail") {
202
- this.failedSuites++;
565
+ else if (!hasDetailedPoints) {
566
+ // Compatibility fallback for reports without detailed point payloads.
567
+ for (const report of reports) {
568
+ if (isIgnoredCoverageFile(report.file, coverage))
569
+ continue;
570
+ if (report.coverage.total <= 0)
571
+ continue;
572
+ summary.total += report.coverage.total;
573
+ summary.covered += report.coverage.covered;
574
+ summary.uncovered += report.coverage.uncovered;
575
+ summary.files.push({
576
+ file: report.file,
577
+ total: report.coverage.total,
578
+ covered: report.coverage.covered,
579
+ uncovered: report.coverage.uncovered,
580
+ percent: report.coverage.percent,
581
+ points: report.coverage.points,
582
+ });
203
583
  }
204
- else {
205
- this.passedSuites++;
584
+ }
585
+ summary.percent = summary.total
586
+ ? (summary.covered * 100) / summary.total
587
+ : 100;
588
+ return summary;
589
+ }
590
+ function isIgnoredCoverageFile(file, coverage) {
591
+ const normalized = file.replace(/\\/g, "/");
592
+ if (!isAllowedCoverageSourceFile(normalized))
593
+ return true;
594
+ if (normalized.startsWith("node_modules/"))
595
+ return true;
596
+ if (normalized.includes("/node_modules/"))
597
+ return true;
598
+ if (isAssemblyScriptStdlibFile(normalized))
599
+ return true;
600
+ if (!coverage.includeSpecs && normalized.endsWith(".spec.ts"))
601
+ return true;
602
+ return false;
603
+ }
604
+ function isAllowedCoverageSourceFile(file) {
605
+ const lower = file.toLowerCase();
606
+ return lower.endsWith(".ts") || lower.endsWith(".as");
607
+ }
608
+ function isAssemblyScriptStdlibFile(file) {
609
+ if (file.startsWith("~lib/"))
610
+ return true;
611
+ if (file.includes("/~lib/"))
612
+ return true;
613
+ if (file.startsWith("assemblyscript/std/"))
614
+ return true;
615
+ if (file.includes("/assemblyscript/std/"))
616
+ return true;
617
+ return false;
618
+ }
619
+ function resolveCoverageOptions(raw) {
620
+ if (typeof raw == "boolean") {
621
+ return {
622
+ enabled: raw,
623
+ includeSpecs: false,
624
+ };
625
+ }
626
+ if (raw && typeof raw == "object") {
627
+ const obj = raw;
628
+ return {
629
+ enabled: obj.enabled == null ? true : Boolean(obj.enabled),
630
+ includeSpecs: Boolean(obj.includeSpecs),
631
+ };
632
+ }
633
+ return {
634
+ enabled: false,
635
+ includeSpecs: false,
636
+ };
637
+ }
638
+ function compareCoveragePoints(a, b) {
639
+ if (a.line !== b.line)
640
+ return a.line - b.line;
641
+ if (a.column !== b.column)
642
+ return a.column - b.column;
643
+ if (a.type !== b.type)
644
+ return a.type.localeCompare(b.type);
645
+ return a.hash.localeCompare(b.hash);
646
+ }
647
+ async function runProcess(cmd, snapshots, snapshotEnabled, updateSnapshots, reporter) {
648
+ const child = spawn(cmd, {
649
+ stdio: ["pipe", "pipe", "pipe"],
650
+ shell: true,
651
+ });
652
+ let report = null;
653
+ let parseError = null;
654
+ let stderrBuffer = "";
655
+ let suppressTraceWarningLine = false;
656
+ child.stderr.on("data", (chunk) => {
657
+ stderrBuffer += chunk.toString("utf8");
658
+ let newline = stderrBuffer.indexOf("\n");
659
+ while (newline >= 0) {
660
+ const line = stderrBuffer.slice(0, newline + 1);
661
+ stderrBuffer = stderrBuffer.slice(newline + 1);
662
+ if (shouldSuppressWasiWarningLine(line, suppressTraceWarningLine)) {
663
+ suppressTraceWarningLine = true;
664
+ }
665
+ else {
666
+ suppressTraceWarningLine = false;
667
+ process.stderr.write(line);
668
+ }
669
+ newline = stderrBuffer.indexOf("\n");
670
+ }
671
+ });
672
+ class TestChannel extends Channel {
673
+ onPassthrough(data) {
674
+ process.stdout.write(data);
675
+ }
676
+ onCall(msg) {
677
+ const event = msg;
678
+ const kind = String(event.kind ?? "");
679
+ if (kind === "event:assert-fail") {
680
+ reporter.onAssertionFail?.({
681
+ key: String(event.key ?? ""),
682
+ instr: String(event.instr ?? ""),
683
+ left: String(event.left ?? ""),
684
+ right: String(event.right ?? ""),
685
+ message: String(event.message ?? ""),
686
+ });
687
+ return;
688
+ }
689
+ if (kind === "event:file-start") {
690
+ reporter.onFileStart?.({
691
+ file: String(event.file ?? "unknown"),
692
+ depth: 0,
693
+ suiteKind: "file",
694
+ description: String(event.file ?? "unknown"),
695
+ });
696
+ return;
697
+ }
698
+ if (kind === "event:file-end") {
699
+ reporter.onFileEnd?.({
700
+ file: String(event.file ?? "unknown"),
701
+ depth: 0,
702
+ suiteKind: "file",
703
+ description: String(event.file ?? "unknown"),
704
+ verdict: String(event.verdict ?? "none"),
705
+ time: String(event.time ?? ""),
706
+ });
707
+ return;
708
+ }
709
+ if (kind === "event:suite-start") {
710
+ reporter.onSuiteStart?.({
711
+ file: String(event.file ?? "unknown"),
712
+ depth: Number(event.depth ?? 0),
713
+ suiteKind: String(event.suiteKind ?? ""),
714
+ description: String(event.description ?? ""),
715
+ });
716
+ return;
717
+ }
718
+ if (kind === "event:suite-end") {
719
+ reporter.onSuiteEnd?.({
720
+ file: String(event.file ?? "unknown"),
721
+ depth: Number(event.depth ?? 0),
722
+ suiteKind: String(event.suiteKind ?? ""),
723
+ description: String(event.description ?? ""),
724
+ verdict: String(event.verdict ?? "none"),
725
+ });
726
+ return;
727
+ }
728
+ if (kind === "snapshot:assert") {
729
+ const key = String(event.key ?? "");
730
+ const actual = String(event.actual ?? "");
731
+ const result = snapshots.assert(key, actual, snapshotEnabled, updateSnapshots);
732
+ if (result.warnMissing) {
733
+ reporter.onSnapshotMissing?.({ key });
734
+ }
735
+ this.send(MessageType.CALL, Buffer.from(`${result.ok ? "1" : "0"}\n${result.expected}`, "utf8"));
736
+ return;
737
+ }
738
+ this.sendJSON(MessageType.CALL, { ok: true, expected: "" });
206
739
  }
207
- this.time += suite.time.end - suite.time.start;
208
- for (const subSuite of suite.suites) {
209
- this.readSuite(subSuite);
740
+ onDataMessage(data) {
741
+ try {
742
+ report = JSON.parse(data.toString("utf8"));
743
+ }
744
+ catch (error) {
745
+ parseError = String(error);
746
+ }
210
747
  }
211
- for (const test of suite.tests) {
212
- if (test.verdict == "fail")
213
- this.failed.push(suite);
214
- this.readTest(test);
748
+ }
749
+ const _channel = new TestChannel(child.stdout, child.stdin);
750
+ const code = await new Promise((resolve) => {
751
+ child.on("close", (exitCode) => resolve(exitCode ?? 1));
752
+ });
753
+ if (stderrBuffer.length) {
754
+ if (!shouldSuppressWasiWarningLine(stderrBuffer, suppressTraceWarningLine)) {
755
+ process.stderr.write(stderrBuffer);
215
756
  }
216
757
  }
217
- readTest(test) {
218
- if (test.verdict == "fail") {
219
- this.failedTests++;
758
+ if (parseError) {
759
+ throw new Error(`could not parse report payload: ${parseError}`);
760
+ }
761
+ if (!report) {
762
+ throw new Error("missing report payload from test runtime");
763
+ }
764
+ if (code !== 0) {
765
+ // Let report determine failure counts, but keep non-zero child exits visible.
766
+ process.stderr.write(chalk.dim(`child process exited with code ${code}\n`));
767
+ }
768
+ return report;
769
+ }
770
+ function shouldSuppressWasiWarningLine(line, suppressTraceWarningLine) {
771
+ if (line.includes("ExperimentalWarning: WASI is an experimental feature")) {
772
+ return true;
773
+ }
774
+ if (suppressTraceWarningLine && line.includes("--trace-warnings")) {
775
+ return true;
776
+ }
777
+ return false;
778
+ }
779
+ function collectRunStats(reports) {
780
+ const stats = {
781
+ passedFiles: 0,
782
+ failedFiles: 0,
783
+ skippedFiles: 0,
784
+ passedSuites: 0,
785
+ failedSuites: 0,
786
+ skippedSuites: 0,
787
+ passedTests: 0,
788
+ failedTests: 0,
789
+ skippedTests: 0,
790
+ time: 0.0,
791
+ failedEntries: [],
792
+ };
793
+ for (const fileReport of reports) {
794
+ readFileReport(stats, fileReport);
795
+ }
796
+ return stats;
797
+ }
798
+ function readFileReport(stats, fileReport) {
799
+ const suites = Array.isArray(fileReport) ? fileReport : [];
800
+ let fileVerdict = "none";
801
+ for (const suite of suites) {
802
+ fileVerdict = mergeVerdict(fileVerdict, readSuite(stats, suite));
803
+ }
804
+ if (fileVerdict == "fail") {
805
+ stats.failedFiles++;
806
+ }
807
+ else if (fileVerdict == "ok") {
808
+ stats.passedFiles++;
809
+ }
810
+ else {
811
+ stats.skippedFiles++;
812
+ }
813
+ }
814
+ function readSuite(stats, suite) {
815
+ const suiteAny = suite;
816
+ let verdict = normalizeVerdict(suiteAny.verdict);
817
+ const time = suiteAny.time;
818
+ const start = Number(time?.start ?? 0);
819
+ const end = Number(time?.end ?? 0);
820
+ stats.time += end - start;
821
+ const subSuites = Array.isArray(suiteAny.suites)
822
+ ? suiteAny.suites
823
+ : [];
824
+ for (const subSuite of subSuites) {
825
+ verdict = mergeVerdict(verdict, readSuite(stats, subSuite));
826
+ }
827
+ const tests = Array.isArray(suiteAny.tests)
828
+ ? suiteAny.tests
829
+ : [];
830
+ for (const test of tests) {
831
+ const testVerdict = normalizeVerdict(test.verdict);
832
+ verdict = mergeVerdict(verdict, testVerdict);
833
+ if (testVerdict == "fail") {
834
+ stats.failedTests++;
835
+ }
836
+ else if (testVerdict == "ok") {
837
+ stats.passedTests++;
220
838
  }
221
839
  else {
222
- this.passedTests++;
840
+ stats.skippedTests++;
223
841
  }
224
842
  }
843
+ if (verdict == "fail") {
844
+ stats.failedSuites++;
845
+ stats.failedEntries.push(suite);
846
+ }
847
+ else if (verdict == "ok") {
848
+ stats.passedSuites++;
849
+ }
850
+ else {
851
+ stats.skippedSuites++;
852
+ }
853
+ return verdict;
854
+ }
855
+ function normalizeVerdict(value) {
856
+ const verdict = String(value ?? "none");
857
+ if (verdict == "fail")
858
+ return "fail";
859
+ if (verdict == "ok")
860
+ return "ok";
861
+ if (verdict == "skip")
862
+ return "skip";
863
+ return "none";
864
+ }
865
+ function mergeVerdict(current, next) {
866
+ if (current == "fail" || next == "fail")
867
+ return "fail";
868
+ if (current == "ok" || next == "ok")
869
+ return "ok";
870
+ if (current == "skip" || next == "skip")
871
+ return "skip";
872
+ return "none";
873
+ }
874
+ export async function createRunReporter(configPath = DEFAULT_CONFIG_PATH) {
875
+ const resolvedConfigPath = configPath ?? DEFAULT_CONFIG_PATH;
876
+ const config = loadConfig(resolvedConfigPath);
877
+ const reporter = await loadReporter(config.runOptions.reporter, resolvedConfigPath, {
878
+ stdout: process.stdout,
879
+ stderr: process.stderr,
880
+ });
881
+ const runtimeCommand = resolveRuntimeCommand(getConfiguredRuntimeCmd(config), config.buildOptions.target, false);
882
+ return {
883
+ reporter,
884
+ runtimeName: runtimeNameFromCommand(runtimeCommand),
885
+ resolvedConfigPath,
886
+ };
887
+ }
888
+ async function loadReporter(reporterPath, configPath, context) {
889
+ if (!reporterPath) {
890
+ return createDefaultReporter(context);
891
+ }
892
+ const resolved = path.isAbsolute(reporterPath)
893
+ ? reporterPath
894
+ : path.resolve(path.dirname(configPath), reporterPath);
895
+ try {
896
+ const mod = (await import(pathToFileURL(resolved).href));
897
+ const factory = resolveReporterFactory(mod);
898
+ return factory(context);
899
+ }
900
+ catch (error) {
901
+ const reporterError = new Error(`could not load reporter "${reporterPath}": ${String(error)}`);
902
+ reporterError.cause = error;
903
+ throw reporterError;
904
+ }
225
905
  }
226
- function readData(data) {
227
- let out = "";
228
- const start = data.indexOf("READ_LINE");
229
- if (start >= 0) {
230
- const slice = data.slice(start + 9);
231
- const end = slice.indexOf("END_LINE");
232
- out += slice.slice(0, end);
233
- out += readData(slice);
906
+ function resolveReporterFactory(mod) {
907
+ const fromNamed = mod.createReporter;
908
+ if (typeof fromNamed == "function") {
909
+ return fromNamed;
910
+ }
911
+ const fromDefault = mod.default;
912
+ if (typeof fromDefault == "function") {
913
+ return fromDefault;
914
+ }
915
+ if (fromDefault &&
916
+ typeof fromDefault == "object" &&
917
+ "createReporter" in fromDefault) {
918
+ const nested = fromDefault.createReporter;
919
+ if (typeof nested == "function") {
920
+ return nested;
921
+ }
234
922
  }
235
- return out;
923
+ throw new Error(`reporter module must export a factory as "createReporter" or default`);
236
924
  }