as-test 0.5.2 → 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/CHANGELOG.md CHANGED
@@ -1,5 +1,46 @@
1
1
  # Change Log
2
2
 
3
+ ## 2026-02-25 - v0.5.3
4
+
5
+ ### CLI, Modes & Matrix
6
+
7
+ - feat: support mode fan-out behavior consistently across `ast build`, `ast run`, and `ast test`.
8
+ - feat: when no `--mode` is provided and modes are configured, run using configured modes.
9
+ - feat: add matrix-style per-file output with mode-aware timing:
10
+ - non-verbose: average time
11
+ - verbose: per-mode times.
12
+ - feat: add real-time matrix line updates in the default reporter and normalize timing precision to one decimal.
13
+ - feat: support comma-separated bare selectors (for example `ast test box,custom,generics,string`) across build/run/test selectors.
14
+
15
+ ### Config Merge & Env Behavior
16
+
17
+ - fix: apply mode config as field-level merge over base config instead of replacing entire sections.
18
+ - fix: merge `buildOptions.args` between base config and mode config.
19
+ - fix: pass config env variables to both build and run processes for mode execution.
20
+
21
+ ### Build Pipeline & Feature Flags
22
+
23
+ - feat: include the exact build command in build failure output.
24
+ - feat: allow `buildOptions.cmd` to override default command generation while still appending user build args.
25
+ - feat: support CLI feature toggles:
26
+ - `--enable coverage` / `--disable coverage`
27
+ - `--enable try-as` / `--disable try-as`.
28
+
29
+ ### Reporter & Summaries
30
+
31
+ - fix: move mode summary rendering into the default reporter (via run-complete event payload).
32
+ - feat: include `modeSummary` for single-mode runs.
33
+ - feat: include mode and snapshot totals in final summary output.
34
+ - fix: `--clean` output now behaves as non-TTY in default reporter:
35
+ - no in-place line editing,
36
+ - no suite expand/collapse logs,
37
+ - final per-file verdict lines only.
38
+
39
+ ### Coverage & Transform
40
+
41
+ - fix: ignore AssemblyScript builtin/compiler helper calls during coverage instrumentation (including `isString`, `changetype<T>`, `idof<T>`, `sizeof<T>`).
42
+ - fix: mock transform now collects mocked import targets across sources so WASI mock imports are rewritten reliably (resolves import-shape runtime failures).
43
+
3
44
  ## 2026-02-24 - v0.5.2
4
45
 
5
46
  ### Runtime & Serialization
package/README.md CHANGED
@@ -74,7 +74,7 @@ describe("math", () => {
74
74
  });
75
75
  ```
76
76
 
77
- ### Test file selection (`ast test`)
77
+ ### File selection (`ast run`, `ast build`, `ast test`)
78
78
 
79
79
  No selectors:
80
80
 
@@ -105,6 +105,14 @@ Multiple selectors:
105
105
  ast test sleep array ./assembly/__tests__/snapshot.spec.ts
106
106
  ```
107
107
 
108
+ Comma-separated bare suite names:
109
+
110
+ ```bash
111
+ ast test box,custom,generics,string
112
+ ast run box,custom,generics,string
113
+ ast build box,custom,generics,string
114
+ ```
115
+
108
116
  If nothing matches, `ast test` exits non-zero with:
109
117
 
110
118
  ```text
@@ -114,11 +122,21 @@ No test files matched: ...
114
122
  ### Useful flags
115
123
 
116
124
  - `--config <path>`: use another config file
117
- - `--mode <name[,name...]>`: run one or multiple named config modes
125
+ - `--mode <name[,name...]>`: run one or multiple named config modes (if omitted and `modes` is configured, as-test runs all configured modes)
118
126
  - `--update-snapshots`: write snapshot updates
119
127
  - `--no-snapshot`: disable snapshot assertions for the run
120
128
  - `--show-coverage`: print uncovered coverage points
129
+ - `--enable <feature>`: enable as-test feature (`coverage`, `try-as`)
130
+ - `--disable <feature>`: disable as-test feature (`coverage`, `try-as`)
121
131
  - `--verbose`: keep expanded suite/test lines and update running `....` statuses in place
132
+ - `--clean`: disable in-place TTY updates and print only final per-file verdict lines. Useful for CI/CD.
133
+
134
+ Example:
135
+
136
+ ```bash
137
+ ast build --enable try-as
138
+ ast test --disable coverage
139
+ ```
122
140
 
123
141
  ## Mocking
124
142
 
@@ -236,7 +254,9 @@ Example:
236
254
  "snapshotDir": "./.as-test/snapshots",
237
255
  "config": "none",
238
256
  "coverage": true,
257
+ "env": {},
239
258
  "buildOptions": {
259
+ "cmd": "",
240
260
  "args": [],
241
261
  "target": "wasi"
242
262
  },
@@ -257,8 +277,10 @@ Key fields:
257
277
  - `logs`: log output dir or `"none"`
258
278
  - `coverageDir`: coverage output dir or `"none"`
259
279
  - `snapshotDir`: snapshot storage dir
280
+ - `env`: environment variables injected into build and runtime processes
281
+ - `buildOptions.cmd`: optional custom build command template; when set it replaces default build command and flags. Supports `<file>`, `<name>`, `<outFile>`, `<target>`, `<mode>`
260
282
  - `buildOptions.target`: `wasi` or `bindings`
261
- - `modes`: named overrides for target/args/runtime/env/artifact directories (selected via `--mode`)
283
+ - `modes`: named overrides for command/target/args/runtime/env/artifact directories (selected via `--mode`); `mode.env` overrides top-level `env`
262
284
  - `runOptions.runtime.cmd`: runtime command, supports `<file>` and `<name>`; if its script path is missing, as-test falls back to the default runner for the selected target
263
285
  - `runOptions.reporter`: reporter selection as a string or object
264
286
 
@@ -309,6 +331,13 @@ Run all modes:
309
331
  ast test --mode wasi-simd,wasi-nosimd,bindings-node-simd
310
332
  ```
311
333
 
334
+ Summary totals:
335
+
336
+ - `Modes` in the default reporter is config-scoped (`total` is all configured modes)
337
+ - when selecting fewer modes with `--mode`, unselected modes are counted as `skipped`
338
+ - `Files` in the default reporter is also config-scoped (`total` is all files from configured input patterns)
339
+ - when selecting fewer files, unselected files are counted as `skipped`
340
+
312
341
  When using `--mode`, compiled artifacts are emitted as:
313
342
 
314
343
  ```text
@@ -80,10 +80,23 @@
80
80
  ],
81
81
  "default": true
82
82
  },
83
+ "env": {
84
+ "type": "object",
85
+ "description": "Environment variables injected when building/running.",
86
+ "additionalProperties": {
87
+ "type": "string"
88
+ },
89
+ "default": {}
90
+ },
83
91
  "buildOptions": {
84
92
  "type": "object",
85
93
  "additionalProperties": false,
86
94
  "properties": {
95
+ "cmd": {
96
+ "type": "string",
97
+ "description": "Custom build command template. When set, this replaces the default asc command/flags/output handling. Supports placeholders: <file>, <name>, <outFile>, <target>, <mode>.",
98
+ "default": ""
99
+ },
87
100
  "args": {
88
101
  "type": "array",
89
102
  "items": {
@@ -98,6 +111,7 @@
98
111
  }
99
112
  },
100
113
  "default": {
114
+ "cmd": "",
101
115
  "args": [],
102
116
  "target": "wasi"
103
117
  }
@@ -153,6 +167,10 @@
153
167
  "type": "object",
154
168
  "additionalProperties": false,
155
169
  "properties": {
170
+ "cmd": {
171
+ "type": "string",
172
+ "description": "Mode-specific custom build command template."
173
+ },
156
174
  "args": {
157
175
  "type": "array",
158
176
  "items": {
@@ -23,6 +23,26 @@ export function __REGISTER(point: CoverPoint): void {
23
23
  Coverage.SN.hashes.set(point.hash, point);
24
24
  }
25
25
 
26
+ export function __REGISTER_RAW(
27
+ file: string,
28
+ hash: string,
29
+ line: i32,
30
+ column: i32,
31
+ type: string,
32
+ ): void {
33
+ if (Coverage.SN.allIndex.has(hash)) return;
34
+ const point = new CoverPoint();
35
+ point.file = file;
36
+ point.hash = hash;
37
+ point.line = line;
38
+ point.column = column;
39
+ point.type = type;
40
+ Coverage.SN.points++;
41
+ Coverage.SN.allIndex.set(hash, Coverage.SN.all.length);
42
+ Coverage.SN.all.push(point);
43
+ Coverage.SN.hashes.set(hash, point);
44
+ }
45
+
26
46
  export function __COVER(hash: string): void {
27
47
  if (Coverage.SN.allIndex.has(hash)) {
28
48
  const index = Coverage.SN.allIndex.get(hash);
@@ -18,17 +18,16 @@ export function stringifyValue<T>(value: T): string {
18
18
  }
19
19
 
20
20
  if (isInteger<T>() || isFloat<T>()) {
21
- // @ts-ignore
21
+ // @ts-expect-error: type
22
22
  return value.toString();
23
23
  }
24
24
 
25
25
  if (isString<T>()) {
26
- // @ts-ignore
27
26
  return quote(value as string);
28
27
  }
29
28
 
30
29
  if (isArray<T>()) {
31
- // @ts-ignore
30
+ // @ts-expect-error: type
32
31
  return stringifyArray<valueof<T>>(value as valueof<T>[]);
33
32
  }
34
33
 
package/bin/build.js CHANGED
@@ -5,29 +5,67 @@ import { execSync } from "child_process";
5
5
  import * as path from "path";
6
6
  import { applyMode, getPkgRunner, loadConfig } from "./util.js";
7
7
  const DEFAULT_CONFIG_PATH = path.join(process.cwd(), "./as-test.config.json");
8
- export async function build(configPath = DEFAULT_CONFIG_PATH, selectors = [], modeName) {
8
+ export async function build(configPath = DEFAULT_CONFIG_PATH, selectors = [], modeName, featureToggles = {}) {
9
9
  const loadedConfig = loadConfig(configPath, false);
10
10
  const mode = applyMode(loadedConfig, modeName);
11
11
  const config = mode.config;
12
- ensureDeps(config);
12
+ if (!hasCustomBuildCommand(config)) {
13
+ ensureDeps(config);
14
+ }
13
15
  const pkgRunner = getPkgRunner();
14
16
  const inputPatterns = resolveInputPatterns(config.input, selectors);
15
17
  const inputFiles = (await glob(inputPatterns)).sort((a, b) => a.localeCompare(b));
16
- const buildArgs = getBuildArgs(config);
18
+ const coverageEnabled = resolveCoverageEnabled(config.coverage, featureToggles.coverage);
19
+ const buildEnv = {
20
+ ...mode.env,
21
+ AS_TEST_COVERAGE_ENABLED: coverageEnabled ? "1" : "0",
22
+ };
17
23
  for (const file of inputFiles) {
18
- let cmd = `${pkgRunner} asc ${file}${buildArgs}`;
19
24
  const outFile = `${config.outDir}/${resolveArtifactFileName(file, config.buildOptions.target, modeName)}`;
20
- if (config.outDir) {
21
- cmd += " -o " + outFile;
22
- }
25
+ const cmd = getBuildCommand(config, pkgRunner, file, outFile, modeName, featureToggles);
23
26
  try {
24
- buildFile(cmd, mode.env);
27
+ buildFile(cmd, buildEnv);
25
28
  }
26
29
  catch (error) {
27
- throw new Error(`Failed to build ${path.basename(file)} with ${getBuildStderr(error)}`);
30
+ const modeLabel = modeName ?? "default";
31
+ throw new Error(`Failed to build ${path.basename(file)} in mode ${modeLabel} with ${getBuildStderr(error)}\nBuild command: ${cmd}`);
28
32
  }
29
33
  }
30
34
  }
35
+ function hasCustomBuildCommand(config) {
36
+ return !!config.buildOptions.cmd.trim().length;
37
+ }
38
+ function getBuildCommand(config, pkgRunner, file, outFile, modeName, featureToggles = {}) {
39
+ const userArgs = getUserBuildArgs(config);
40
+ if (hasCustomBuildCommand(config)) {
41
+ return `${expandBuildCommand(config.buildOptions.cmd, file, outFile, config.buildOptions.target, modeName)}${userArgs}`;
42
+ }
43
+ const defaultArgs = getDefaultBuildArgs(config, featureToggles);
44
+ let cmd = `${pkgRunner} asc ${file}${userArgs}${defaultArgs}`;
45
+ if (config.outDir) {
46
+ cmd += " -o " + outFile;
47
+ }
48
+ return cmd;
49
+ }
50
+ function getUserBuildArgs(config) {
51
+ const args = config.buildOptions.args.filter((value) => value.length > 0);
52
+ if (args.length) {
53
+ return " " + args.join(" ");
54
+ }
55
+ return "";
56
+ }
57
+ function expandBuildCommand(template, file, outFile, target, modeName) {
58
+ const name = path
59
+ .basename(file)
60
+ .replace(/\.spec\.ts$/, "")
61
+ .replace(/\.ts$/, "");
62
+ return template
63
+ .replace(/<file>/g, file)
64
+ .replace(/<name>/g, name)
65
+ .replace(/<outFile>/g, outFile)
66
+ .replace(/<target>/g, target)
67
+ .replace(/<mode>/g, modeName ?? "");
68
+ }
31
69
  function resolveArtifactFileName(file, target, modeName) {
32
70
  const base = path
33
71
  .basename(file)
@@ -45,7 +83,7 @@ function resolveInputPatterns(configured, selectors) {
45
83
  if (!selectors.length)
46
84
  return configuredInputs;
47
85
  const patterns = new Set();
48
- for (const selector of selectors) {
86
+ for (const selector of expandSelectors(selectors)) {
49
87
  if (!selector)
50
88
  continue;
51
89
  if (isBareSuiteSelector(selector)) {
@@ -59,6 +97,30 @@ function resolveInputPatterns(configured, selectors) {
59
97
  }
60
98
  return [...patterns];
61
99
  }
100
+ function expandSelectors(selectors) {
101
+ const expanded = [];
102
+ for (const selector of selectors) {
103
+ if (!selector)
104
+ continue;
105
+ if (!shouldSplitSelector(selector)) {
106
+ expanded.push(selector);
107
+ continue;
108
+ }
109
+ for (const token of selector.split(",")) {
110
+ const trimmed = token.trim();
111
+ if (!trimmed.length)
112
+ continue;
113
+ expanded.push(trimmed);
114
+ }
115
+ }
116
+ return expanded;
117
+ }
118
+ function shouldSplitSelector(selector) {
119
+ return (selector.includes(",") &&
120
+ !selector.includes("/") &&
121
+ !selector.includes("\\") &&
122
+ !/[*?[\]{}]/.test(selector));
123
+ }
62
124
  function isBareSuiteSelector(selector) {
63
125
  return (!selector.includes("/") &&
64
126
  !selector.includes("\\") &&
@@ -98,16 +160,17 @@ function getBuildStderr(error) {
98
160
  const message = typeof err?.message == "string" ? err.message.trim() : "";
99
161
  return message || "unknown error";
100
162
  }
101
- function getBuildArgs(config) {
163
+ function getDefaultBuildArgs(config, featureToggles) {
102
164
  let buildArgs = "";
165
+ const tryAsEnabled = resolveTryAsEnabled(featureToggles.tryAs);
103
166
  buildArgs += " --transform as-test/transform";
104
- if (hasTryAsRuntime()) {
167
+ if (tryAsEnabled) {
105
168
  buildArgs += " --transform try-as/transform";
106
169
  }
107
170
  if (config.config && config.config !== "none") {
108
171
  buildArgs += " --config " + config.config;
109
172
  }
110
- if (hasTryAsRuntime()) {
173
+ if (tryAsEnabled) {
111
174
  buildArgs += " --use AS_TEST_TRY_AS=1";
112
175
  }
113
176
  // Should also strip any bindings-enabling from asconfig
@@ -124,12 +187,29 @@ function getBuildArgs(config) {
124
187
  console.log(`${chalk.bgRed(" ERROR ")}${chalk.dim(":")} could not determine target in config! Set target to 'bindings' or 'wasi'`);
125
188
  process.exit(1);
126
189
  }
127
- if (config.buildOptions.args.length &&
128
- config.buildOptions.args.find((v) => v.length > 0)) {
129
- buildArgs += " " + config.buildOptions.args.join(" ");
130
- }
131
190
  return buildArgs;
132
191
  }
192
+ function resolveTryAsEnabled(override) {
193
+ const installed = hasTryAsRuntime();
194
+ if (override === false)
195
+ return false;
196
+ if (override === true && !installed) {
197
+ throw new Error('try-as feature was enabled, but package "try-as" is not installed');
198
+ }
199
+ return installed;
200
+ }
201
+ function resolveCoverageEnabled(rawCoverage, override) {
202
+ if (override != undefined)
203
+ return override;
204
+ if (typeof rawCoverage == "boolean")
205
+ return rawCoverage;
206
+ if (rawCoverage && typeof rawCoverage == "object") {
207
+ const enabled = rawCoverage.enabled;
208
+ if (typeof enabled == "boolean")
209
+ return enabled;
210
+ }
211
+ return true;
212
+ }
133
213
  function hasTryAsRuntime() {
134
214
  return (existsSync(path.join(process.cwd(), "node_modules/try-as")) ||
135
215
  existsSync(path.join(process.cwd(), "node_modules/try-as/package.json")));