as-test 0.5.1 → 0.5.3

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/bin/run.js CHANGED
@@ -1,11 +1,12 @@
1
1
  import chalk from "chalk";
2
2
  import { spawn } from "child_process";
3
3
  import { glob } from "glob";
4
- import { getExec, loadConfig } from "./util.js";
4
+ import { applyMode, getExec, loadConfig } from "./util.js";
5
5
  import * as path from "path";
6
6
  import { pathToFileURL } from "url";
7
7
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
8
8
  import { createReporter as createDefaultReporter } from "./reporters/default.js";
9
+ import { createTapReporter } from "./reporters/tap.js";
9
10
  const DEFAULT_CONFIG_PATH = path.join(process.cwd(), "./as-test.config.json");
10
11
  var MessageType;
11
12
  (function (MessageType) {
@@ -145,19 +146,26 @@ class SnapshotStore {
145
146
  export async function run(flags = {}, configPath = DEFAULT_CONFIG_PATH, selectors = [], shouldExit = true, options = {}) {
146
147
  const resolvedConfigPath = configPath ?? DEFAULT_CONFIG_PATH;
147
148
  const reports = [];
148
- const config = loadConfig(resolvedConfigPath);
149
+ const loadedConfig = loadConfig(resolvedConfigPath);
150
+ const mode = applyMode(loadedConfig, options.modeName);
151
+ const config = mode.config;
149
152
  const inputPatterns = resolveInputPatterns(config.input, selectors);
150
- const inputFiles = await glob(inputPatterns);
153
+ const inputFiles = (await glob(inputPatterns)).sort((a, b) => a.localeCompare(b));
151
154
  const snapshotEnabled = flags.snapshot !== false;
152
155
  const updateSnapshots = Boolean(flags.updateSnapshots);
153
156
  const cleanOutput = Boolean(flags.clean);
154
157
  const showCoverage = Boolean(flags.showCoverage);
155
158
  const coverage = resolveCoverageOptions(config.coverage);
159
+ if (flags.coverage != undefined) {
160
+ coverage.enabled = Boolean(flags.coverage);
161
+ }
156
162
  const coverageEnabled = coverage.enabled;
157
163
  const coverageDir = config.coverageDir ?? "./.as-test/coverage";
158
164
  const runtimeCommand = resolveRuntimeCommand(getConfiguredRuntimeCmd(config), config.buildOptions.target);
165
+ const reporterSelection = resolveReporterSelection(options.reporterPath, config.runOptions.reporter);
166
+ const reporterKind = options.reporterKind ?? reporterSelection.kind;
159
167
  const reporter = options.reporter ??
160
- (await loadReporter(config.runOptions.reporter, resolvedConfigPath, {
168
+ (await loadReporter(reporterSelection, resolvedConfigPath, {
161
169
  stdout: process.stdout,
162
170
  stderr: process.stderr,
163
171
  }));
@@ -191,7 +199,7 @@ export async function run(flags = {}, configPath = DEFAULT_CONFIG_PATH, selector
191
199
  };
192
200
  for (let i = 0; i < inputFiles.length; i++) {
193
201
  const file = inputFiles[i];
194
- const outFile = path.join(config.outDir, file.slice(file.lastIndexOf("/") + 1).replace(".ts", ".wasm"));
202
+ const outFile = path.join(config.outDir, resolveArtifactFileName(file, config.buildOptions.target, options.modeName));
195
203
  const fileBase = file
196
204
  .slice(file.lastIndexOf("/") + 1)
197
205
  .replace(".ts", "")
@@ -199,16 +207,21 @@ export async function run(flags = {}, configPath = DEFAULT_CONFIG_PATH, selector
199
207
  let cmd = runtimeCommand.replace(command, execPath);
200
208
  cmd = cmd.replace("<name>", fileBase);
201
209
  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"));
210
+ cmd = cmd.replace("<file>", resolveBindingsHelperPath(outFile));
206
211
  }
207
212
  else {
208
213
  cmd = cmd.replace("<file>", outFile);
209
214
  }
210
215
  const snapshotStore = new SnapshotStore(file, config.snapshotDir);
211
- const report = await runProcess(cmd, snapshotStore, snapshotEnabled, updateSnapshots, reporter);
216
+ let report;
217
+ try {
218
+ report = await runProcess(cmd, snapshotStore, snapshotEnabled, updateSnapshots, reporter, reporterKind == "tap", mode.env);
219
+ }
220
+ catch (error) {
221
+ const modeLabel = options.modeName ?? "default";
222
+ const details = error instanceof Error ? error.message : String(error);
223
+ throw new Error(`Failed to run ${path.basename(file)} in mode ${modeLabel} with ${details}`);
224
+ }
212
225
  const normalized = normalizeReport(report);
213
226
  snapshotStore.flush();
214
227
  snapshotSummary.matched += snapshotStore.matched;
@@ -232,6 +245,9 @@ export async function run(flags = {}, configPath = DEFAULT_CONFIG_PATH, selector
232
245
  writeFileSync(path.join(process.cwd(), config.logs, options.logFileName ?? "test.log.json"), JSON.stringify(logReports, null, 2));
233
246
  }
234
247
  const stats = collectRunStats(reports.map((report) => report.suites));
248
+ if (options.fileSummaryTotal != undefined) {
249
+ applyConfiguredFileTotalToStats(stats, options.fileSummaryTotal);
250
+ }
235
251
  const coverageSummary = collectCoverageSummary(reports, coverageEnabled, showCoverage, coverage);
236
252
  if (coverageEnabled &&
237
253
  coverageDir &&
@@ -244,6 +260,10 @@ export async function run(flags = {}, configPath = DEFAULT_CONFIG_PATH, selector
244
260
  writeFileSync(path.join(resolvedCoverageDir, options.coverageFileName ?? "coverage.log.json"), JSON.stringify(coverageSummary, null, 2));
245
261
  }
246
262
  if (options.emitRunComplete !== false) {
263
+ const totalModes = Math.max(options.modeSummaryTotal ?? 1, 1);
264
+ const executedModes = Math.min(Math.max(options.modeSummaryExecuted ?? 1, 1), totalModes);
265
+ const unexecutedModes = Math.max(0, totalModes - executedModes);
266
+ const modeFailed = Boolean(stats.failedFiles || snapshotSummary.failed);
247
267
  reporter.onRunComplete?.({
248
268
  clean: cleanOutput,
249
269
  snapshotEnabled,
@@ -252,6 +272,11 @@ export async function run(flags = {}, configPath = DEFAULT_CONFIG_PATH, selector
252
272
  coverageSummary,
253
273
  stats,
254
274
  reports,
275
+ modeSummary: {
276
+ failed: modeFailed ? 1 : 0,
277
+ skipped: unexecutedModes + (modeFailed || stats.passedFiles > 0 ? 0 : 1),
278
+ total: totalModes,
279
+ },
255
280
  });
256
281
  }
257
282
  const failed = Boolean(stats.failedFiles || snapshotSummary.failed);
@@ -266,35 +291,59 @@ export async function run(flags = {}, configPath = DEFAULT_CONFIG_PATH, selector
266
291
  reports,
267
292
  };
268
293
  }
294
+ function applyConfiguredFileTotalToStats(stats, fileSummaryTotal) {
295
+ const total = Math.max(fileSummaryTotal, 0);
296
+ const executed = stats.failedFiles + stats.passedFiles + stats.skippedFiles;
297
+ const unexecuted = Math.max(0, total - executed);
298
+ stats.skippedFiles += unexecuted;
299
+ }
269
300
  function resolveRuntimeCommand(runtimeRun, target, emitWarnings = true) {
270
- const normalized = resolveLegacyWasiRuntime(runtimeRun, target, emitWarnings);
301
+ const normalized = resolveLegacyRuntime(runtimeRun, target, emitWarnings);
271
302
  return fallbackToDefaultRuntime(normalized, target, emitWarnings);
272
303
  }
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))
304
+ function resolveLegacyRuntime(runtimeRun, target, emitWarnings) {
305
+ if (target == "wasi") {
306
+ const preferredPath = "./.as-test/runners/default.wasi.js";
307
+ const legacyPaths = ["./bin/wasi-run.js", "./.as-test/wasi/wasi.run.js"];
308
+ if (runtimeRun.includes(preferredPath)) {
309
+ ensureDefaultRuntimeRunner("wasi", emitWarnings);
281
310
  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.`);
311
+ }
312
+ for (const legacyPath of legacyPaths) {
313
+ if (!runtimeRun.includes(legacyPath))
314
+ continue;
315
+ const resolvedLegacyPath = path.join(process.cwd(), legacyPath);
316
+ if (existsSync(resolvedLegacyPath))
317
+ return runtimeRun;
318
+ ensureDefaultRuntimeRunner("wasi", emitWarnings);
319
+ if (emitWarnings) {
320
+ process.stderr.write(chalk.dim(`legacy WASI runtime path detected (${legacyPath}); using ${preferredPath}\n`));
321
+ }
322
+ return runtimeRun.replace(legacyPath, preferredPath);
323
+ }
324
+ return runtimeRun;
283
325
  }
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))
326
+ if (target == "bindings") {
327
+ const preferredPath = "./.as-test/runners/default.bindings.js";
328
+ const legacyPath = "./.as-test/runners/default.run.js";
329
+ if (runtimeRun.includes(preferredPath)) {
330
+ ensureDefaultRuntimeRunner("bindings", emitWarnings);
289
331
  return runtimeRun;
290
- const resolvedPreferredPath = path.join(process.cwd(), preferredPath);
291
- if (existsSync(resolvedPreferredPath)) {
332
+ }
333
+ if (runtimeRun.includes(legacyPath)) {
334
+ const resolvedLegacyPath = path.join(process.cwd(), legacyPath);
335
+ if (existsSync(resolvedLegacyPath)) {
336
+ if (emitWarnings) {
337
+ process.stderr.write(chalk.dim(`deprecated runtime script (${legacyPath}) detected; prefer ${preferredPath}\n`));
338
+ }
339
+ return runtimeRun;
340
+ }
341
+ ensureDefaultRuntimeRunner("bindings", emitWarnings);
292
342
  if (emitWarnings) {
293
- process.stderr.write(chalk.dim(`legacy WASI runtime path detected (${legacyPath}); using ${preferredPath}\n`));
343
+ process.stderr.write(chalk.dim(`legacy bindings runtime path detected (${legacyPath}); using ${preferredPath}\n`));
294
344
  }
295
345
  return runtimeRun.replace(legacyPath, preferredPath);
296
346
  }
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.`);
298
347
  }
299
348
  return runtimeRun;
300
349
  }
@@ -307,15 +356,12 @@ function fallbackToDefaultRuntime(runtimeRun, target, emitWarnings) {
307
356
  : path.join(process.cwd(), scriptPath);
308
357
  if (existsSync(resolvedScriptPath))
309
358
  return runtimeRun;
310
- const fallback = getDefaultRuntimeFallback(target);
359
+ const fallback = ensureDefaultRuntimeRunner(target, emitWarnings);
311
360
  if (!fallback)
312
361
  return runtimeRun;
313
362
  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.`);
363
+ if (resolvedScriptPath == resolvedFallbackPath || scriptPath == fallback.scriptPath) {
364
+ return runtimeRun;
319
365
  }
320
366
  if (emitWarnings) {
321
367
  process.stderr.write(chalk.dim(`runtime script not found (${scriptPath}); using ${fallback.scriptPath}\n`));
@@ -331,12 +377,169 @@ function getDefaultRuntimeFallback(target) {
331
377
  }
332
378
  if (target == "bindings") {
333
379
  return {
334
- command: "node ./.as-test/runners/default.run.js <file>",
335
- scriptPath: "./.as-test/runners/default.run.js",
380
+ command: "node ./.as-test/runners/default.bindings.js <file>",
381
+ scriptPath: "./.as-test/runners/default.bindings.js",
336
382
  };
337
383
  }
338
384
  return null;
339
385
  }
386
+ function ensureDefaultRuntimeRunner(target, emitWarnings) {
387
+ const fallback = getDefaultRuntimeFallback(target);
388
+ if (!fallback)
389
+ return null;
390
+ const resolvedScriptPath = path.join(process.cwd(), fallback.scriptPath);
391
+ if (existsSync(resolvedScriptPath))
392
+ return fallback;
393
+ const source = getDefaultRuntimeRunnerSource(target);
394
+ if (!source)
395
+ return fallback;
396
+ if (!existsSync(path.dirname(resolvedScriptPath))) {
397
+ mkdirSync(path.dirname(resolvedScriptPath), { recursive: true });
398
+ }
399
+ writeFileSync(resolvedScriptPath, source);
400
+ if (emitWarnings) {
401
+ process.stderr.write(chalk.dim(`runtime script missing; created ${fallback.scriptPath}\n`));
402
+ }
403
+ return fallback;
404
+ }
405
+ function getDefaultRuntimeRunnerSource(target) {
406
+ if (target == "wasi") {
407
+ return `import { readFileSync } from "fs";
408
+ import { WASI } from "wasi";
409
+
410
+ const originalEmitWarning = process.emitWarning.bind(process);
411
+ process.emitWarning = ((warning, ...args) => {
412
+ const type = typeof args[0] == "string" ? args[0] : "";
413
+ const name = typeof warning?.name == "string" ? warning.name : type;
414
+ const message =
415
+ typeof warning == "string" ? warning : String(warning?.message ?? "");
416
+ if (
417
+ name == "ExperimentalWarning" &&
418
+ message.includes("WASI is an experimental feature")
419
+ ) {
420
+ return;
421
+ }
422
+ return originalEmitWarning(warning, ...args);
423
+ });
424
+
425
+ const wasmPath = process.argv[2];
426
+ if (!wasmPath) {
427
+ process.stderr.write("usage: node ./.as-test/runners/default.wasi.js <file.wasm>\\n");
428
+ process.exit(1);
429
+ }
430
+
431
+ try {
432
+ const wasi = new WASI({
433
+ version: "preview1",
434
+ args: [wasmPath],
435
+ env: process.env,
436
+ preopens: {},
437
+ });
438
+
439
+ const binary = readFileSync(wasmPath);
440
+ const module = new WebAssembly.Module(binary);
441
+ const instance = new WebAssembly.Instance(module, {
442
+ wasi_snapshot_preview1: wasi.wasiImport,
443
+ });
444
+ wasi.start(instance);
445
+ } catch (error) {
446
+ process.stderr.write("failed to run WASI module: " + String(error) + "\\n");
447
+ process.exit(1);
448
+ }
449
+ `;
450
+ }
451
+ if (target == "bindings") {
452
+ return `import fs from "fs";
453
+ import path from "path";
454
+ import { pathToFileURL } from "url";
455
+
456
+ let patched = false;
457
+
458
+ function readExact(length) {
459
+ const out = Buffer.alloc(length);
460
+ let offset = 0;
461
+ while (offset < length) {
462
+ let read = 0;
463
+ try {
464
+ read = fs.readSync(0, out, offset, length - offset, null);
465
+ } catch (error) {
466
+ if (error && error.code === "EAGAIN") {
467
+ continue;
468
+ }
469
+ throw error;
470
+ }
471
+ if (!read) break;
472
+ offset += read;
473
+ }
474
+ const view = out.subarray(0, offset);
475
+ return view.buffer.slice(view.byteOffset, view.byteOffset + view.byteLength);
476
+ }
477
+
478
+ function writeRaw(data) {
479
+ const view = Buffer.from(data);
480
+ fs.writeSync(1, view);
481
+ }
482
+
483
+ function withNodeIo(imports = {}) {
484
+ if (!patched) {
485
+ patched = true;
486
+ const originalWrite = process.stdout.write.bind(process.stdout);
487
+ process.stdout.write = (chunk, ...args) => {
488
+ if (chunk instanceof ArrayBuffer) {
489
+ writeRaw(chunk);
490
+ return true;
491
+ }
492
+ return originalWrite(chunk, ...args);
493
+ };
494
+ process.stdin.read = (size) => readExact(Number(size ?? 0));
495
+ }
496
+ return imports;
497
+ }
498
+
499
+ const wasmPathArg = process.argv[2];
500
+ if (!wasmPathArg) {
501
+ process.stderr.write("usage: node ./.as-test/runners/default.bindings.js <file.wasm>\\n");
502
+ process.exit(1);
503
+ }
504
+
505
+ const wasmPath = path.resolve(process.cwd(), wasmPathArg);
506
+ const jsPath = wasmPath.replace(/\\.wasm$/, ".js");
507
+
508
+ try {
509
+ const binary = fs.readFileSync(wasmPath);
510
+ const module = new WebAssembly.Module(binary);
511
+ const mod = await import(pathToFileURL(jsPath).href);
512
+ if (typeof mod.instantiate !== "function") {
513
+ throw new Error("bindings helper missing instantiate export");
514
+ }
515
+ mod.instantiate(module, withNodeIo({}));
516
+ } catch (error) {
517
+ process.stderr.write("failed to run bindings module: " + String(error) + "\\n");
518
+ process.exit(1);
519
+ }
520
+ `;
521
+ }
522
+ return null;
523
+ }
524
+ function resolveArtifactFileName(file, target, modeName) {
525
+ const base = path
526
+ .basename(file)
527
+ .replace(/\.spec\.ts$/, "")
528
+ .replace(/\.ts$/, "");
529
+ if (!modeName) {
530
+ return `${path.basename(file).replace(".ts", ".wasm")}`;
531
+ }
532
+ return `${base}.${modeName}.${target}.wasm`;
533
+ }
534
+ function resolveBindingsHelperPath(wasmPath) {
535
+ const bindingsPath = wasmPath.replace(/\.wasm$/, ".bindings.js");
536
+ if (existsSync(bindingsPath))
537
+ return bindingsPath;
538
+ const legacyRunPath = wasmPath.replace(/\.wasm$/, ".run.js");
539
+ if (existsSync(legacyRunPath))
540
+ return legacyRunPath;
541
+ return bindingsPath;
542
+ }
340
543
  function extractRuntimeScriptPath(runtimeRun) {
341
544
  const tokens = runtimeRun.trim().split(/\s+/).filter((token) => token.length > 0);
342
545
  if (tokens.length < 2)
@@ -415,7 +618,7 @@ function resolveInputPatterns(configured, selectors) {
415
618
  if (!selectors.length)
416
619
  return configuredInputs;
417
620
  const patterns = new Set();
418
- for (const selector of selectors) {
621
+ for (const selector of expandSelectors(selectors)) {
419
622
  if (!selector)
420
623
  continue;
421
624
  if (isBareSuiteSelector(selector)) {
@@ -429,6 +632,30 @@ function resolveInputPatterns(configured, selectors) {
429
632
  }
430
633
  return [...patterns];
431
634
  }
635
+ function expandSelectors(selectors) {
636
+ const expanded = [];
637
+ for (const selector of selectors) {
638
+ if (!selector)
639
+ continue;
640
+ if (!shouldSplitSelector(selector)) {
641
+ expanded.push(selector);
642
+ continue;
643
+ }
644
+ for (const token of selector.split(",")) {
645
+ const trimmed = token.trim();
646
+ if (!trimmed.length)
647
+ continue;
648
+ expanded.push(trimmed);
649
+ }
650
+ }
651
+ return expanded;
652
+ }
653
+ function shouldSplitSelector(selector) {
654
+ return (selector.includes(",") &&
655
+ !selector.includes("/") &&
656
+ !selector.includes("\\") &&
657
+ !/[*?[\]{}]/.test(selector));
658
+ }
432
659
  function isBareSuiteSelector(selector) {
433
660
  return (!selector.includes("/") &&
434
661
  !selector.includes("\\") &&
@@ -644,10 +871,11 @@ function compareCoveragePoints(a, b) {
644
871
  return a.type.localeCompare(b.type);
645
872
  return a.hash.localeCompare(b.hash);
646
873
  }
647
- async function runProcess(cmd, snapshots, snapshotEnabled, updateSnapshots, reporter) {
874
+ async function runProcess(cmd, snapshots, snapshotEnabled, updateSnapshots, reporter, tapMode = false, env = process.env) {
648
875
  const child = spawn(cmd, {
649
876
  stdio: ["pipe", "pipe", "pipe"],
650
877
  shell: true,
878
+ env,
651
879
  });
652
880
  let report = null;
653
881
  let parseError = null;
@@ -671,7 +899,12 @@ async function runProcess(cmd, snapshots, snapshotEnabled, updateSnapshots, repo
671
899
  });
672
900
  class TestChannel extends Channel {
673
901
  onPassthrough(data) {
674
- process.stdout.write(data);
902
+ if (tapMode) {
903
+ process.stderr.write(data);
904
+ }
905
+ else {
906
+ process.stdout.write(data);
907
+ }
675
908
  }
676
909
  onCall(msg) {
677
910
  const event = msg;
@@ -871,21 +1104,32 @@ function mergeVerdict(current, next) {
871
1104
  return "skip";
872
1105
  return "none";
873
1106
  }
874
- export async function createRunReporter(configPath = DEFAULT_CONFIG_PATH) {
1107
+ export async function createRunReporter(configPath = DEFAULT_CONFIG_PATH, reporterPath, modeName) {
875
1108
  const resolvedConfigPath = configPath ?? DEFAULT_CONFIG_PATH;
876
- const config = loadConfig(resolvedConfigPath);
877
- const reporter = await loadReporter(config.runOptions.reporter, resolvedConfigPath, {
1109
+ const loadedConfig = loadConfig(resolvedConfigPath);
1110
+ const mode = applyMode(loadedConfig, modeName);
1111
+ const config = mode.config;
1112
+ const selection = resolveReporterSelection(reporterPath, config.runOptions.reporter);
1113
+ const reporter = await loadReporter(selection, resolvedConfigPath, {
878
1114
  stdout: process.stdout,
879
1115
  stderr: process.stderr,
880
1116
  });
881
1117
  const runtimeCommand = resolveRuntimeCommand(getConfiguredRuntimeCmd(config), config.buildOptions.target, false);
882
1118
  return {
883
1119
  reporter,
1120
+ reporterKind: selection.kind,
884
1121
  runtimeName: runtimeNameFromCommand(runtimeCommand),
885
1122
  resolvedConfigPath,
886
1123
  };
887
1124
  }
888
- async function loadReporter(reporterPath, configPath, context) {
1125
+ async function loadReporter(selection, configPath, context) {
1126
+ if (selection.kind == "default") {
1127
+ return createDefaultReporter(context);
1128
+ }
1129
+ if (selection.kind == "tap") {
1130
+ return createTapReporter(context, selection.tap);
1131
+ }
1132
+ const reporterPath = selection.reporterPath;
889
1133
  if (!reporterPath) {
890
1134
  return createDefaultReporter(context);
891
1135
  }
@@ -903,6 +1147,86 @@ async function loadReporter(reporterPath, configPath, context) {
903
1147
  throw reporterError;
904
1148
  }
905
1149
  }
1150
+ function resolveReporterSelection(cliValue, configValue) {
1151
+ const parsed = parseReporterConfig(configValue);
1152
+ const raw = resolveCliReporter(process.argv.slice(2), cliValue ?? parsed.name);
1153
+ const normalized = raw.trim();
1154
+ const canonical = normalized.toLowerCase();
1155
+ if (!normalized.length || canonical == "default") {
1156
+ return { kind: "default", reporterPath: "", tap: parsed.tap };
1157
+ }
1158
+ if (canonical == "tap" || canonical == "tap13") {
1159
+ return { kind: "tap", reporterPath: "", tap: parsed.tap };
1160
+ }
1161
+ return { kind: "custom", reporterPath: normalized, tap: parsed.tap };
1162
+ }
1163
+ function resolveCliReporter(argv, fallback) {
1164
+ let resolved = fallback;
1165
+ for (let i = 0; i < argv.length; i++) {
1166
+ const token = argv[i];
1167
+ if (token == "--tap") {
1168
+ resolved = "tap";
1169
+ continue;
1170
+ }
1171
+ if (token == "--reporter") {
1172
+ const value = argv[i + 1];
1173
+ if (!value || value.startsWith("-")) {
1174
+ throw new Error(`--reporter requires a value`);
1175
+ }
1176
+ resolved = value;
1177
+ i++;
1178
+ continue;
1179
+ }
1180
+ if (token.startsWith("--reporter=")) {
1181
+ const value = token.slice("--reporter=".length);
1182
+ if (!value.length) {
1183
+ throw new Error(`--reporter requires a value`);
1184
+ }
1185
+ resolved = value;
1186
+ continue;
1187
+ }
1188
+ }
1189
+ return resolved;
1190
+ }
1191
+ function parseReporterConfig(value) {
1192
+ const tap = {
1193
+ mode: "single-file",
1194
+ outDir: "./.as-test/reports",
1195
+ outFile: "./.as-test/reports/report.tap",
1196
+ };
1197
+ if (typeof value == "string") {
1198
+ return { name: value, tap };
1199
+ }
1200
+ if (!value || typeof value != "object") {
1201
+ return { name: "", tap };
1202
+ }
1203
+ const config = value;
1204
+ const name = typeof config.name == "string" ? config.name : "";
1205
+ if (typeof config.outDir == "string" && config.outDir.trim().length) {
1206
+ tap.outDir = config.outDir.trim();
1207
+ }
1208
+ if (typeof config.outFile == "string" && config.outFile.trim().length) {
1209
+ tap.outFile = config.outFile.trim();
1210
+ }
1211
+ else if (tap.outDir && tap.outDir.length) {
1212
+ tap.outFile = path.join(tap.outDir, "report.tap");
1213
+ }
1214
+ if (Array.isArray(config.options)) {
1215
+ const options = config.options
1216
+ .filter((option) => typeof option == "string")
1217
+ .map((option) => option.toLowerCase());
1218
+ if (options.includes("per-file")) {
1219
+ tap.mode = "per-file";
1220
+ if (!config.outFile && tap.outDir && tap.outDir.length) {
1221
+ tap.outFile = path.join(tap.outDir, "report.tap");
1222
+ }
1223
+ }
1224
+ else {
1225
+ tap.mode = "single-file";
1226
+ }
1227
+ }
1228
+ return { name, tap };
1229
+ }
906
1230
  function resolveReporterFactory(mod) {
907
1231
  const fromNamed = mod.createReporter;
908
1232
  if (typeof fromNamed == "function") {
package/bin/types.js CHANGED
@@ -8,8 +8,10 @@ export class Config {
8
8
  this.snapshotDir = "./.as-test/snapshots";
9
9
  this.config = "none";
10
10
  this.coverage = true;
11
+ this.env = {};
11
12
  this.buildOptions = new BuildOptions();
12
13
  this.runOptions = new RunOptions();
14
+ this.modes = {};
13
15
  }
14
16
  }
15
17
  export class CoverageOptions {
@@ -25,6 +27,7 @@ export class Suite {
25
27
  }
26
28
  export class BuildOptions {
27
29
  constructor() {
30
+ this.cmd = "";
28
31
  this.args = [];
29
32
  this.target = "wasi";
30
33
  }
@@ -40,3 +43,18 @@ export class Runtime {
40
43
  this.cmd = "node ./.as-test/runners/default.wasi.js <file>";
41
44
  }
42
45
  }
46
+ export class ModeConfig {
47
+ constructor() {
48
+ this.buildOptions = {};
49
+ this.runOptions = {};
50
+ this.env = {};
51
+ }
52
+ }
53
+ export class ReporterConfig {
54
+ constructor() {
55
+ this.name = "";
56
+ this.options = [];
57
+ this.outDir = "";
58
+ this.outFile = "";
59
+ }
60
+ }