as-test 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/as-test.config.schema.json +15 -0
- package/assembly/coverage.ts +22 -26
- package/assembly/index.ts +2 -0
- package/assembly/src/expectation.ts +111 -38
- package/assembly/src/mode.ts +55 -0
- package/bin/commands/build-core.js +152 -7
- package/bin/commands/build.js +3 -1
- package/bin/commands/init-core.js +253 -5
- package/bin/commands/test.js +1 -1
- package/bin/index.js +60 -20
- package/bin/types.js +7 -0
- package/bin/util.js +43 -0
- package/package.json +3 -3
- package/transform/lib/index.js +26 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# Change Log
|
|
2
2
|
|
|
3
|
+
## 2026-05-22 - v1.3.0
|
|
4
|
+
|
|
5
|
+
### `features` config array + arbitrary `--enable` passthrough
|
|
6
|
+
|
|
7
|
+
- feat: new top-level `"features": ["try-as", "simd"]` array in `as-test.config.json` (and per-mode override). `try-as` is the only as-test-internal feature today and wires the `try-as/transform` + `AS_TEST_TRY_AS=1` build flags as before. Any other name in the array is passed through to `asc` as `--enable <name>` — so `"simd"`, `"threads"`, `"reference-types"`, `"gc"`, etc. now work without hand-editing `buildOptions.args`.
|
|
8
|
+
- feat: `ast test|run|build --enable <name>` and `--disable <name>` now accept arbitrary feature names. CLI flags override the config array (CLI `--disable simd` removes a config-listed feature; CLI `--enable simd` adds it). The known special-case `coverage` flag still routes to the dedicated top-level `coverage` config field rather than the features array. Both flags accept comma-separated lists too: `--enable try-as,coverage,simd` and `--disable try-as,coverage`. The same syntax is honored by `ast init`.
|
|
9
|
+
- feat: `ast init` interactive prompt now includes a multi-select "Features" step (↑/↓ to move, space to toggle, enter to confirm) with `coverage` and `try-as` options. `--enable`/`--disable` flags on `ast init` skip the prompt with explicit selections. The generated `as-test.config.json` writes `coverage` at the top level and `features` as a string array; when try-as is selected, `try-as` is also added to `devDependencies`.
|
|
10
|
+
- chore: schema validation rejects malformed shapes (object form, non-string array entries) with a fix hint pointing at the new shape.
|
|
11
|
+
|
|
12
|
+
### `mode()` registration gate + `AS_TEST_MODE_NAME`
|
|
13
|
+
|
|
14
|
+
- feat: new `mode(matchers: string[], fn: () => void)` helper in the `as-test` runtime, plus an `AS_TEST_MODE_NAME: string` compile-time constant. Use `mode(["node:bindings"], () => { ... })` to gate suite/test registrations on the active mode name. Matcher semantics: positive entries OR; `!name` entries exclude; `[]` is a no-op; positive + negative entries combine as "any positive matches AND no negative matches."
|
|
15
|
+
- feat: build-side wiring — `build-core.ts` injects `AS_TEST_MODE_NAME=<mode>` into the asc env per-mode build, and the as-test transform rewrites the initializer of `AS_TEST_MODE_NAME` in `assembly/src/mode.ts` (an `afterParse` AST patch) so the value is baked into the wasm at compile time. Default value when no mode is selected is `"default"`.
|
|
16
|
+
- chore: bundled `assembly/__tests__/mode.spec.ts` exercises positive/negative/mixed matchers, empty matchers, and per-mode counter behaviour end-to-end against the project's own `node:bindings` / `node:wasi` modes.
|
|
17
|
+
|
|
18
|
+
### Tooling
|
|
19
|
+
|
|
20
|
+
- chore: `build:transform` now runs `prettier -w ./transform/` after the TypeScript build so generated output stays formatted.
|
|
21
|
+
|
|
3
22
|
## 2026-05-20 - v1.2.0
|
|
4
23
|
|
|
5
24
|
### Directory-preserving artifact layout
|
|
@@ -180,6 +180,14 @@
|
|
|
180
180
|
],
|
|
181
181
|
"default": false
|
|
182
182
|
},
|
|
183
|
+
"features": {
|
|
184
|
+
"type": "array",
|
|
185
|
+
"description": "Enabled feature names. \"try-as\" wires up the try-as transform; any other name is passed through to asc as --enable <name> (e.g. \"simd\", \"threads\", \"reference-types\"). CLI --enable/--disable flags override this list. Coverage has its own top-level \"coverage\" field.",
|
|
186
|
+
"items": {
|
|
187
|
+
"type": "string"
|
|
188
|
+
},
|
|
189
|
+
"default": []
|
|
190
|
+
},
|
|
183
191
|
"env": {
|
|
184
192
|
"description": "Environment variables injected when building/running. Accepts a .env file path, an array of KEY=value strings, or an object map.",
|
|
185
193
|
"oneOf": [
|
|
@@ -400,6 +408,13 @@
|
|
|
400
408
|
}
|
|
401
409
|
]
|
|
402
410
|
},
|
|
411
|
+
"features": {
|
|
412
|
+
"type": "array",
|
|
413
|
+
"description": "Mode-specific feature list. Replaces the base list entirely when set.",
|
|
414
|
+
"items": {
|
|
415
|
+
"type": "string"
|
|
416
|
+
}
|
|
417
|
+
},
|
|
403
418
|
"fuzz": {
|
|
404
419
|
"type": "object",
|
|
405
420
|
"additionalProperties": false,
|
package/assembly/coverage.ts
CHANGED
|
@@ -13,18 +13,17 @@ export class CoverPoint {
|
|
|
13
13
|
|
|
14
14
|
export class Coverage {
|
|
15
15
|
public all: CoverPoint[] = [];
|
|
16
|
-
public
|
|
17
|
-
public
|
|
18
|
-
public points: i32 = 0;
|
|
16
|
+
public byHash: Map<string, CoverPoint> = new Map<string, CoverPoint>();
|
|
17
|
+
public uncovered: i32 = 0;
|
|
19
18
|
static SN: Coverage = new Coverage();
|
|
20
19
|
}
|
|
21
20
|
|
|
22
21
|
export function __REGISTER(point: CoverPoint): void {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
22
|
+
const cov = Coverage.SN;
|
|
23
|
+
if (cov.byHash.has(point.hash)) return;
|
|
24
|
+
cov.byHash.set(point.hash, point);
|
|
25
|
+
cov.all.push(point);
|
|
26
|
+
cov.uncovered++;
|
|
28
27
|
}
|
|
29
28
|
|
|
30
29
|
export function __REGISTER_RAW(
|
|
@@ -38,7 +37,8 @@ export function __REGISTER_RAW(
|
|
|
38
37
|
scopeName: string = "",
|
|
39
38
|
depth: i32 = 0,
|
|
40
39
|
): void {
|
|
41
|
-
|
|
40
|
+
const cov = Coverage.SN;
|
|
41
|
+
if (cov.byHash.has(hash)) return;
|
|
42
42
|
const point = new CoverPoint();
|
|
43
43
|
point.file = file;
|
|
44
44
|
point.hash = hash;
|
|
@@ -49,32 +49,28 @@ export function __REGISTER_RAW(
|
|
|
49
49
|
point.scopeKind = scopeKind;
|
|
50
50
|
point.scopeName = scopeName;
|
|
51
51
|
point.depth = depth;
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
Coverage.SN.hashes.set(hash, point);
|
|
52
|
+
cov.byHash.set(hash, point);
|
|
53
|
+
cov.all.push(point);
|
|
54
|
+
cov.uncovered++;
|
|
56
55
|
}
|
|
57
56
|
|
|
57
|
+
// Hot path: invoked at every instrumented point. After first hit, subsequent
|
|
58
|
+
// hits short-circuit on `executed` before any writes.
|
|
58
59
|
export function __COVER(hash: string): void {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
if (Coverage.SN.hashes.has(hash)) Coverage.SN.hashes.delete(hash);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export function __HASHES(): Map<string, CoverPoint> {
|
|
69
|
-
return Coverage.SN.hashes;
|
|
60
|
+
const cov = Coverage.SN;
|
|
61
|
+
if (!cov.byHash.has(hash)) return;
|
|
62
|
+
const point = cov.byHash.get(hash);
|
|
63
|
+
if (point.executed) return;
|
|
64
|
+
point.executed = true;
|
|
65
|
+
cov.uncovered--;
|
|
70
66
|
}
|
|
71
67
|
|
|
72
68
|
export function __POINTS(): i32 {
|
|
73
|
-
return Coverage.SN.
|
|
69
|
+
return Coverage.SN.all.length;
|
|
74
70
|
}
|
|
75
71
|
|
|
76
72
|
export function __UNCOVERED(): i32 {
|
|
77
|
-
return Coverage.SN.
|
|
73
|
+
return Coverage.SN.uncovered;
|
|
78
74
|
}
|
|
79
75
|
|
|
80
76
|
export function __ALL_POINTS(): CoverPoint[] {
|
package/assembly/index.ts
CHANGED
|
@@ -57,6 +57,41 @@ export class Expectation<T> extends Tests {
|
|
|
57
57
|
return this;
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
/**
|
|
61
|
+
* Asserts that a custom predicate holds. Accepts either a bool or a
|
|
62
|
+
* `() => bool` lambda. Useful when the verdict isn't expressible via the
|
|
63
|
+
* built-in matchers — e.g. delegating to a hand-written comparator.
|
|
64
|
+
*
|
|
65
|
+
* expect(x).where(x > 0 && x < 10);
|
|
66
|
+
* expect(actual).where((): bool => deepEqual(GLOBAL_A, GLOBAL_B));
|
|
67
|
+
*
|
|
68
|
+
* Chains with other matchers as an independent assertion:
|
|
69
|
+
*
|
|
70
|
+
* expect(7).toBe(7).where((): bool => isFresh());
|
|
71
|
+
*
|
|
72
|
+
* Note: AssemblyScript does not implement closures, so the lambda cannot
|
|
73
|
+
* capture local variables from the enclosing scope. Use the bool form when
|
|
74
|
+
* the predicate references locals, or refer to module-level values from
|
|
75
|
+
* inside the lambda.
|
|
76
|
+
*/
|
|
77
|
+
where<W>(predicate: W, message: string = ""): Expectation<T> {
|
|
78
|
+
let passed: bool;
|
|
79
|
+
if (isFunction<W>()) {
|
|
80
|
+
passed = (predicate as () => bool)();
|
|
81
|
+
} else {
|
|
82
|
+
// @ts-ignore: W is a bool-compatible primitive in this branch
|
|
83
|
+
passed = predicate as bool;
|
|
84
|
+
}
|
|
85
|
+
this._resolve(
|
|
86
|
+
passed,
|
|
87
|
+
"where",
|
|
88
|
+
q(passed ? "true" : "false"),
|
|
89
|
+
q("true"),
|
|
90
|
+
message,
|
|
91
|
+
);
|
|
92
|
+
return this;
|
|
93
|
+
}
|
|
94
|
+
|
|
60
95
|
private _resolve(
|
|
61
96
|
passed: bool,
|
|
62
97
|
instr: string,
|
|
@@ -74,14 +109,26 @@ export class Expectation<T> extends Tests {
|
|
|
74
109
|
return;
|
|
75
110
|
}
|
|
76
111
|
const isFail = this._not ? passed : !passed;
|
|
77
|
-
this.verdict = isFail ? "fail" : "ok";
|
|
78
|
-
this.instr = instr;
|
|
79
|
-
this.left = left;
|
|
80
|
-
this.right = right;
|
|
81
112
|
const resolvedMessage = message.length ? message : this._message;
|
|
82
|
-
|
|
113
|
+
// When matchers chain, later ones must not overwrite an earlier failure's
|
|
114
|
+
// recorded state — otherwise a passing matcher after a failed one would
|
|
115
|
+
// flip the suite's verdict back to "ok". Each matcher still fires its own
|
|
116
|
+
// IPC failure independently below.
|
|
117
|
+
if (this.verdict != "fail") {
|
|
118
|
+
this.verdict = isFail ? "fail" : "ok";
|
|
119
|
+
this.instr = instr;
|
|
120
|
+
this.left = left;
|
|
121
|
+
this.right = right;
|
|
122
|
+
this.message = isFail ? resolvedMessage : "";
|
|
123
|
+
}
|
|
83
124
|
if (isFail) {
|
|
84
|
-
sendAssertionFailure(
|
|
125
|
+
sendAssertionFailure(
|
|
126
|
+
this._snapshotKey,
|
|
127
|
+
instr,
|
|
128
|
+
left,
|
|
129
|
+
right,
|
|
130
|
+
resolvedMessage,
|
|
131
|
+
);
|
|
85
132
|
// @ts-ignore
|
|
86
133
|
if (isDefined(AS_TEST_FUZZ)) {
|
|
87
134
|
// @ts-ignore
|
|
@@ -95,7 +142,7 @@ export class Expectation<T> extends Tests {
|
|
|
95
142
|
// @ts-ignore
|
|
96
143
|
__as_test_fuzz_failure_right = right;
|
|
97
144
|
// @ts-ignore
|
|
98
|
-
__as_test_fuzz_failure_message =
|
|
145
|
+
__as_test_fuzz_failure_message = resolvedMessage;
|
|
99
146
|
}
|
|
100
147
|
}
|
|
101
148
|
}
|
|
@@ -105,7 +152,7 @@ export class Expectation<T> extends Tests {
|
|
|
105
152
|
/**
|
|
106
153
|
* Tests if a == null
|
|
107
154
|
*/
|
|
108
|
-
toBeNull(message: string = ""):
|
|
155
|
+
toBeNull(message: string = ""): Expectation<T> {
|
|
109
156
|
const passed =
|
|
110
157
|
(isNullable<T>() && changetype<usize>(this._left) == 0) ||
|
|
111
158
|
(isInteger<T>() && nameof<T>() == "usize" && this._left == 0);
|
|
@@ -121,12 +168,13 @@ export class Expectation<T> extends Tests {
|
|
|
121
168
|
),
|
|
122
169
|
message,
|
|
123
170
|
);
|
|
171
|
+
return this;
|
|
124
172
|
}
|
|
125
173
|
|
|
126
174
|
/**
|
|
127
175
|
* Tests if a > b
|
|
128
176
|
*/
|
|
129
|
-
toBeGreaterThan(value: T, message: string = ""):
|
|
177
|
+
toBeGreaterThan(value: T, message: string = ""): Expectation<T> {
|
|
130
178
|
if (!isInteger<T>() && !isFloat<T>())
|
|
131
179
|
ERROR("toBeGreaterThan() can only be used on number types!");
|
|
132
180
|
|
|
@@ -144,12 +192,13 @@ export class Expectation<T> extends Tests {
|
|
|
144
192
|
),
|
|
145
193
|
message,
|
|
146
194
|
);
|
|
195
|
+
return this;
|
|
147
196
|
}
|
|
148
197
|
|
|
149
198
|
/**
|
|
150
199
|
* Tests if a >= b
|
|
151
200
|
*/
|
|
152
|
-
toBeGreaterOrEqualTo(value: T, message: string = ""):
|
|
201
|
+
toBeGreaterOrEqualTo(value: T, message: string = ""): Expectation<T> {
|
|
153
202
|
if (!isInteger<T>() && !isFloat<T>())
|
|
154
203
|
ERROR("toBeGreaterOrEqualTo() can only be used on number types!");
|
|
155
204
|
|
|
@@ -167,12 +216,13 @@ export class Expectation<T> extends Tests {
|
|
|
167
216
|
),
|
|
168
217
|
message,
|
|
169
218
|
);
|
|
219
|
+
return this;
|
|
170
220
|
}
|
|
171
221
|
|
|
172
222
|
/**
|
|
173
223
|
* Tests if a < b
|
|
174
224
|
*/
|
|
175
|
-
toBeLessThan(value: T, message: string = ""):
|
|
225
|
+
toBeLessThan(value: T, message: string = ""): Expectation<T> {
|
|
176
226
|
if (!isInteger<T>() && !isFloat<T>())
|
|
177
227
|
ERROR("toBeLessThan() can only be used on number types!");
|
|
178
228
|
|
|
@@ -190,12 +240,13 @@ export class Expectation<T> extends Tests {
|
|
|
190
240
|
),
|
|
191
241
|
message,
|
|
192
242
|
);
|
|
243
|
+
return this;
|
|
193
244
|
}
|
|
194
245
|
|
|
195
246
|
/**
|
|
196
247
|
* Tests if a <= b
|
|
197
248
|
*/
|
|
198
|
-
toBeLessThanOrEqualTo(value: T, message: string = ""):
|
|
249
|
+
toBeLessThanOrEqualTo(value: T, message: string = ""): Expectation<T> {
|
|
199
250
|
if (!isInteger<T>() && !isFloat<T>())
|
|
200
251
|
ERROR("toBeLessThanOrEqualTo() can only be used on number types!");
|
|
201
252
|
|
|
@@ -213,12 +264,13 @@ export class Expectation<T> extends Tests {
|
|
|
213
264
|
),
|
|
214
265
|
message,
|
|
215
266
|
);
|
|
267
|
+
return this;
|
|
216
268
|
}
|
|
217
269
|
|
|
218
270
|
/**
|
|
219
271
|
* Tests if a is string
|
|
220
272
|
*/
|
|
221
|
-
toBeString(message: string = ""):
|
|
273
|
+
toBeString(message: string = ""): Expectation<T> {
|
|
222
274
|
this._resolve(
|
|
223
275
|
isString<T>(),
|
|
224
276
|
"toBeString",
|
|
@@ -226,12 +278,13 @@ export class Expectation<T> extends Tests {
|
|
|
226
278
|
q("string"),
|
|
227
279
|
message,
|
|
228
280
|
);
|
|
281
|
+
return this;
|
|
229
282
|
}
|
|
230
283
|
|
|
231
284
|
/**
|
|
232
285
|
* Tests if a is boolean
|
|
233
286
|
*/
|
|
234
|
-
toBeBoolean(message: string = ""):
|
|
287
|
+
toBeBoolean(message: string = ""): Expectation<T> {
|
|
235
288
|
this._resolve(
|
|
236
289
|
isBoolean<T>(),
|
|
237
290
|
"toBeBoolean",
|
|
@@ -239,12 +292,13 @@ export class Expectation<T> extends Tests {
|
|
|
239
292
|
q("boolean"),
|
|
240
293
|
message,
|
|
241
294
|
);
|
|
295
|
+
return this;
|
|
242
296
|
}
|
|
243
297
|
|
|
244
298
|
/**
|
|
245
299
|
* Tests if a is array
|
|
246
300
|
*/
|
|
247
|
-
toBeArray(message: string = ""):
|
|
301
|
+
toBeArray(message: string = ""): Expectation<T> {
|
|
248
302
|
this._resolve(
|
|
249
303
|
isArray<T>(),
|
|
250
304
|
"toBeArray",
|
|
@@ -252,12 +306,13 @@ export class Expectation<T> extends Tests {
|
|
|
252
306
|
q("Array<any>"),
|
|
253
307
|
message,
|
|
254
308
|
);
|
|
309
|
+
return this;
|
|
255
310
|
}
|
|
256
311
|
|
|
257
312
|
/**
|
|
258
313
|
* Tests if a is number
|
|
259
314
|
*/
|
|
260
|
-
toBeNumber(message: string = ""):
|
|
315
|
+
toBeNumber(message: string = ""): Expectation<T> {
|
|
261
316
|
this._resolve(
|
|
262
317
|
isFloat<T>() || isInteger<T>(),
|
|
263
318
|
"toBeNumber",
|
|
@@ -265,12 +320,13 @@ export class Expectation<T> extends Tests {
|
|
|
265
320
|
q("number"),
|
|
266
321
|
message,
|
|
267
322
|
);
|
|
323
|
+
return this;
|
|
268
324
|
}
|
|
269
325
|
|
|
270
326
|
/**
|
|
271
327
|
* Tests if a is integer
|
|
272
328
|
*/
|
|
273
|
-
toBeInteger(message: string = ""):
|
|
329
|
+
toBeInteger(message: string = ""): Expectation<T> {
|
|
274
330
|
this._resolve(
|
|
275
331
|
isInteger<T>(),
|
|
276
332
|
"toBeInteger",
|
|
@@ -278,12 +334,13 @@ export class Expectation<T> extends Tests {
|
|
|
278
334
|
q("integer"),
|
|
279
335
|
message,
|
|
280
336
|
);
|
|
337
|
+
return this;
|
|
281
338
|
}
|
|
282
339
|
|
|
283
340
|
/**
|
|
284
341
|
* Tests if a is float
|
|
285
342
|
*/
|
|
286
|
-
toBeFloat(message: string = ""):
|
|
343
|
+
toBeFloat(message: string = ""): Expectation<T> {
|
|
287
344
|
this._resolve(
|
|
288
345
|
isFloat<T>(),
|
|
289
346
|
"toBeFloat",
|
|
@@ -291,21 +348,23 @@ export class Expectation<T> extends Tests {
|
|
|
291
348
|
q("float"),
|
|
292
349
|
message,
|
|
293
350
|
);
|
|
351
|
+
return this;
|
|
294
352
|
}
|
|
295
353
|
|
|
296
354
|
/**
|
|
297
355
|
* Tests if a is finite
|
|
298
356
|
*/
|
|
299
|
-
toBeFinite(message: string = ""):
|
|
357
|
+
toBeFinite(message: string = ""): Expectation<T> {
|
|
300
358
|
// @ts-ignore
|
|
301
359
|
const passed = (isFloat<T>() || isInteger<T>()) && isFinite(this._left);
|
|
302
360
|
this._resolve(passed, "toBeFinite", q("Infinity"), q("Finite"), message);
|
|
361
|
+
return this;
|
|
303
362
|
}
|
|
304
363
|
|
|
305
364
|
/**
|
|
306
365
|
* Tests if a value is truthy
|
|
307
366
|
*/
|
|
308
|
-
toBeTruthy(message: string = ""):
|
|
367
|
+
toBeTruthy(message: string = ""): Expectation<T> {
|
|
309
368
|
this._resolve(
|
|
310
369
|
isTruthy<T>(this._left),
|
|
311
370
|
"toBeTruthy",
|
|
@@ -313,12 +372,13 @@ export class Expectation<T> extends Tests {
|
|
|
313
372
|
q("truthy"),
|
|
314
373
|
message,
|
|
315
374
|
);
|
|
375
|
+
return this;
|
|
316
376
|
}
|
|
317
377
|
|
|
318
378
|
/**
|
|
319
379
|
* Tests if a value is falsy
|
|
320
380
|
*/
|
|
321
|
-
toBeFalsy(message: string = ""):
|
|
381
|
+
toBeFalsy(message: string = ""): Expectation<T> {
|
|
322
382
|
this._resolve(
|
|
323
383
|
!isTruthy<T>(this._left),
|
|
324
384
|
"toBeFalsy",
|
|
@@ -326,12 +386,14 @@ export class Expectation<T> extends Tests {
|
|
|
326
386
|
q("falsy"),
|
|
327
387
|
message,
|
|
328
388
|
);
|
|
389
|
+
return this;
|
|
329
390
|
}
|
|
330
391
|
|
|
331
392
|
/**
|
|
332
393
|
* Tests if a floating-point number is close to expected
|
|
333
394
|
*/
|
|
334
|
-
|
|
395
|
+
// prettier-ignore
|
|
396
|
+
toBeCloseTo(expected: T, precision: i32 = 2, message: string = ""): Expectation<T> {
|
|
335
397
|
if (!isFloat<T>() && !isInteger<T>())
|
|
336
398
|
ERROR("toBeCloseTo() can only be used on number types!");
|
|
337
399
|
const factor = Math.pow(10, precision as f64);
|
|
@@ -344,12 +406,13 @@ export class Expectation<T> extends Tests {
|
|
|
344
406
|
visualize<T>(expected),
|
|
345
407
|
message,
|
|
346
408
|
);
|
|
409
|
+
return this;
|
|
347
410
|
}
|
|
348
411
|
|
|
349
412
|
/**
|
|
350
413
|
* Tests if a string contains substring
|
|
351
414
|
*/
|
|
352
|
-
toMatch(value: string, message: string = ""):
|
|
415
|
+
toMatch(value: string, message: string = ""): Expectation<T> {
|
|
353
416
|
if (!isString<T>()) ERROR("toMatch() can only be used on string types!");
|
|
354
417
|
// @ts-ignore
|
|
355
418
|
const passed = this._left.indexOf(value) >= 0;
|
|
@@ -361,36 +424,39 @@ export class Expectation<T> extends Tests {
|
|
|
361
424
|
q(value),
|
|
362
425
|
message,
|
|
363
426
|
);
|
|
427
|
+
return this;
|
|
364
428
|
}
|
|
365
429
|
|
|
366
430
|
/**
|
|
367
431
|
* Tests if a string starts with the provided prefix.
|
|
368
432
|
*/
|
|
369
|
-
toStartWith(value: string, message: string = ""):
|
|
433
|
+
toStartWith(value: string, message: string = ""): Expectation<T> {
|
|
370
434
|
if (!isString<T>())
|
|
371
435
|
ERROR("toStartWith() can only be used on string types!");
|
|
372
436
|
// @ts-ignore
|
|
373
437
|
const left = this._left as string;
|
|
374
438
|
const passed = left.indexOf(value) == 0;
|
|
375
439
|
this._resolve(passed, "toStartWith", q(left), q(value), message);
|
|
440
|
+
return this;
|
|
376
441
|
}
|
|
377
442
|
|
|
378
443
|
/**
|
|
379
444
|
* Tests if a string ends with the provided suffix.
|
|
380
445
|
*/
|
|
381
|
-
toEndWith(value: string, message: string = ""):
|
|
446
|
+
toEndWith(value: string, message: string = ""): Expectation<T> {
|
|
382
447
|
if (!isString<T>()) ERROR("toEndWith() can only be used on string types!");
|
|
383
448
|
// @ts-ignore
|
|
384
449
|
const left = this._left as string;
|
|
385
450
|
const idx = left.lastIndexOf(value);
|
|
386
451
|
const passed = idx >= 0 && idx + value.length == left.length;
|
|
387
452
|
this._resolve(passed, "toEndWith", q(left), q(value), message);
|
|
453
|
+
return this;
|
|
388
454
|
}
|
|
389
455
|
|
|
390
456
|
/**
|
|
391
457
|
* Tests if an array has length x
|
|
392
458
|
*/
|
|
393
|
-
toHaveLength(value: i32, message: string = ""):
|
|
459
|
+
toHaveLength(value: i32, message: string = ""): Expectation<T> {
|
|
394
460
|
// @ts-ignore
|
|
395
461
|
const leftLen = this._left.length as i32;
|
|
396
462
|
// @ts-ignore
|
|
@@ -402,13 +468,14 @@ export class Expectation<T> extends Tests {
|
|
|
402
468
|
value.toString(),
|
|
403
469
|
message,
|
|
404
470
|
);
|
|
471
|
+
return this;
|
|
405
472
|
}
|
|
406
473
|
|
|
407
474
|
/**
|
|
408
475
|
* Tests if an array or string contains a value
|
|
409
476
|
*/
|
|
410
477
|
// @ts-ignore
|
|
411
|
-
toContain(value: valueof<T>, message: string = ""):
|
|
478
|
+
toContain(value: valueof<T>, message: string = ""): Expectation<T> {
|
|
412
479
|
if (isString<T>()) {
|
|
413
480
|
// @ts-ignore
|
|
414
481
|
const left = this._left as string;
|
|
@@ -416,7 +483,7 @@ export class Expectation<T> extends Tests {
|
|
|
416
483
|
const needle = value as string;
|
|
417
484
|
const passed = left.indexOf(needle) >= 0;
|
|
418
485
|
this._resolve(passed, "toContain", q(left), q(needle), message);
|
|
419
|
-
return;
|
|
486
|
+
return this;
|
|
420
487
|
}
|
|
421
488
|
|
|
422
489
|
if (isArray<T>()) {
|
|
@@ -429,24 +496,25 @@ export class Expectation<T> extends Tests {
|
|
|
429
496
|
JSON.stringify<valueof<T>>(value),
|
|
430
497
|
message,
|
|
431
498
|
);
|
|
432
|
-
return;
|
|
499
|
+
return this;
|
|
433
500
|
}
|
|
434
501
|
|
|
435
502
|
ERROR("toContain() can only be used on string and array types!");
|
|
503
|
+
return this;
|
|
436
504
|
}
|
|
437
505
|
|
|
438
506
|
/**
|
|
439
507
|
* Alias for toContain().
|
|
440
508
|
*/
|
|
441
509
|
// @ts-ignore
|
|
442
|
-
toContains(value: valueof<T>, message: string = ""):
|
|
443
|
-
this.toContain(value, message);
|
|
510
|
+
toContains(value: valueof<T>, message: string = ""): Expectation<T> {
|
|
511
|
+
return this.toContain(value, message);
|
|
444
512
|
}
|
|
445
513
|
|
|
446
514
|
/**
|
|
447
515
|
* Tests if serialized value matches stored snapshot.
|
|
448
516
|
*/
|
|
449
|
-
toMatchSnapshot(name: string = "", message: string = ""):
|
|
517
|
+
toMatchSnapshot(name: string = "", message: string = ""): Expectation<T> {
|
|
450
518
|
let key = name.length
|
|
451
519
|
? namedSnapshotKey(this._snapshotKey, name)
|
|
452
520
|
: nextUnnamedSnapshotKey(this._snapshotKey);
|
|
@@ -454,6 +522,7 @@ export class Expectation<T> extends Tests {
|
|
|
454
522
|
const actual = JSON.stringify<T>(this._left);
|
|
455
523
|
const res = snapshotAssert(key, actual);
|
|
456
524
|
this._resolve(res.ok, "toMatchSnapshot", actual, res.expected, message);
|
|
525
|
+
return this;
|
|
457
526
|
}
|
|
458
527
|
|
|
459
528
|
/**
|
|
@@ -466,7 +535,7 @@ export class Expectation<T> extends Tests {
|
|
|
466
535
|
* `.toThrow()` on a non-function value records a failure that explains the
|
|
467
536
|
* usage.
|
|
468
537
|
*/
|
|
469
|
-
toThrow(message: string = ""):
|
|
538
|
+
toThrow(message: string = ""): Expectation<T> {
|
|
470
539
|
// @ts-ignore
|
|
471
540
|
if (!isDefined(AS_TEST_TRY_AS)) {
|
|
472
541
|
if (!warnedToThrowDisabled) {
|
|
@@ -476,7 +545,7 @@ export class Expectation<T> extends Tests {
|
|
|
476
545
|
warnedToThrowDisabled = true;
|
|
477
546
|
}
|
|
478
547
|
this._resolve(true, "toThrow", q("disabled"), q("disabled"), message);
|
|
479
|
-
return;
|
|
548
|
+
return this;
|
|
480
549
|
}
|
|
481
550
|
|
|
482
551
|
if (!isFunction<T>()) {
|
|
@@ -489,7 +558,7 @@ export class Expectation<T> extends Tests {
|
|
|
489
558
|
? message
|
|
490
559
|
: "toThrow() requires a function: expect((): void => { ... }).toThrow()",
|
|
491
560
|
);
|
|
492
|
-
return;
|
|
561
|
+
return this;
|
|
493
562
|
}
|
|
494
563
|
|
|
495
564
|
// try-as rewrites the throw inside the callback to bump
|
|
@@ -515,12 +584,13 @@ export class Expectation<T> extends Tests {
|
|
|
515
584
|
q("throws"),
|
|
516
585
|
message,
|
|
517
586
|
);
|
|
587
|
+
return this;
|
|
518
588
|
}
|
|
519
589
|
|
|
520
590
|
/**
|
|
521
591
|
* Tests for equality
|
|
522
592
|
*/
|
|
523
|
-
toBe(equals: T, message: string = ""):
|
|
593
|
+
toBe(equals: T, message: string = ""): Expectation<T> {
|
|
524
594
|
const passed = this._left === equals;
|
|
525
595
|
|
|
526
596
|
this._resolve(
|
|
@@ -530,12 +600,13 @@ export class Expectation<T> extends Tests {
|
|
|
530
600
|
JSON.stringify<T>(equals),
|
|
531
601
|
message,
|
|
532
602
|
);
|
|
603
|
+
return this;
|
|
533
604
|
}
|
|
534
605
|
|
|
535
606
|
/**
|
|
536
607
|
* Tests for deep equality
|
|
537
608
|
*/
|
|
538
|
-
toEqual(equals: T, message: string = ""):
|
|
609
|
+
toEqual(equals: T, message: string = ""): Expectation<T> {
|
|
539
610
|
const passed = valueEquals<T>(this._left, equals, false);
|
|
540
611
|
this._resolve(
|
|
541
612
|
passed,
|
|
@@ -544,12 +615,13 @@ export class Expectation<T> extends Tests {
|
|
|
544
615
|
JSON.stringify<T>(equals),
|
|
545
616
|
message,
|
|
546
617
|
);
|
|
618
|
+
return this;
|
|
547
619
|
}
|
|
548
620
|
|
|
549
621
|
/**
|
|
550
622
|
* Tests for strict deep equality
|
|
551
623
|
*/
|
|
552
|
-
toStrictEqual(equals: T, message: string = ""):
|
|
624
|
+
toStrictEqual(equals: T, message: string = ""): Expectation<T> {
|
|
553
625
|
const passed = valueEquals<T>(this._left, equals, true);
|
|
554
626
|
this._resolve(
|
|
555
627
|
passed,
|
|
@@ -558,6 +630,7 @@ export class Expectation<T> extends Tests {
|
|
|
558
630
|
JSON.stringify<T>(equals),
|
|
559
631
|
message,
|
|
560
632
|
);
|
|
633
|
+
return this;
|
|
561
634
|
}
|
|
562
635
|
}
|
|
563
636
|
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// The current mode name is patched at build time by as-test's transform
|
|
2
|
+
// (transform/src/index.ts) using the AS_TEST_MODE_NAME env var that
|
|
3
|
+
// build-core.ts injects per-mode build. When unset, defaults to "default".
|
|
4
|
+
export const AS_TEST_MODE_NAME: string = "default";
|
|
5
|
+
|
|
6
|
+
function modeMatches(matchers: string[], current: string): bool {
|
|
7
|
+
if (matchers.length == 0) return false;
|
|
8
|
+
let sawPositive = false;
|
|
9
|
+
let positiveHit = false;
|
|
10
|
+
for (let i = 0; i < matchers.length; i++) {
|
|
11
|
+
const m = matchers[i];
|
|
12
|
+
if (m.length == 0) continue;
|
|
13
|
+
if (m.charCodeAt(0) == 33 /* '!' */) {
|
|
14
|
+
if (m.substring(1) == current) return false;
|
|
15
|
+
} else {
|
|
16
|
+
sawPositive = true;
|
|
17
|
+
if (m == current) positiveHit = true;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return sawPositive ? positiveHit : true;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Gate a block of suite/test registrations on the current execution mode.
|
|
25
|
+
*
|
|
26
|
+
* The current mode is the name under `modes.<name>` in `as-test.config.json`
|
|
27
|
+
* (or `"default"` when running the base config). Comparisons are by exact
|
|
28
|
+
* string match.
|
|
29
|
+
*
|
|
30
|
+
* Matcher semantics:
|
|
31
|
+
* - `["a"]` runs when the current mode equals `"a"`.
|
|
32
|
+
* - `["a", "b"]` runs when the current mode is in `{a, b}` (positive OR).
|
|
33
|
+
* - `["!a"]` runs when the current mode is NOT `"a"`.
|
|
34
|
+
* - `["!a", "!b"]` runs when the current mode is neither `{a, b}` (AND).
|
|
35
|
+
* - Mixed `["a", "!b"]` runs when any positive matches AND no negative does.
|
|
36
|
+
* - `[]` and empty entries are skipped (no-op).
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* ```ts
|
|
40
|
+
* mode(["simd"], () => {
|
|
41
|
+
* describe("vectorised path", () => { ... });
|
|
42
|
+
* });
|
|
43
|
+
*
|
|
44
|
+
* mode(["simd", "swar"], () => {
|
|
45
|
+
* describe("fast paths", () => { ... });
|
|
46
|
+
* });
|
|
47
|
+
*
|
|
48
|
+
* mode(["!naive"], () => {
|
|
49
|
+
* describe("anything but naive", () => { ... });
|
|
50
|
+
* });
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
export function mode(matchers: string[], fn: () => void): void {
|
|
54
|
+
if (modeMatches(matchers, AS_TEST_MODE_NAME)) fn();
|
|
55
|
+
}
|