as-test 1.0.0 → 1.0.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.
@@ -99,24 +99,55 @@
99
99
  "properties": {
100
100
  "enabled": {
101
101
  "type": "boolean",
102
- "default": true
102
+ "default": false
103
103
  },
104
104
  "includeSpecs": {
105
105
  "type": "boolean",
106
106
  "description": "Include *.spec.ts files in coverage collection.",
107
107
  "default": false
108
+ },
109
+ "include": {
110
+ "type": "array",
111
+ "description": "Only include matching source files in coverage reports when this list is not empty.",
112
+ "items": {
113
+ "type": "string"
114
+ },
115
+ "default": []
116
+ },
117
+ "exclude": {
118
+ "type": "array",
119
+ "description": "Exclude matching source files from coverage reports.",
120
+ "items": {
121
+ "type": "string"
122
+ },
123
+ "default": []
108
124
  }
109
125
  }
110
126
  }
111
127
  ],
112
- "default": true
128
+ "default": false
113
129
  },
114
130
  "env": {
115
- "type": "object",
116
- "description": "Environment variables injected when building/running.",
117
- "additionalProperties": {
118
- "type": "string"
119
- },
131
+ "description": "Environment variables injected when building/running. Accepts a .env file path, an array of KEY=value strings, or an object map.",
132
+ "oneOf": [
133
+ {
134
+ "type": "string",
135
+ "description": "Path to a .env file, resolved relative to the config file."
136
+ },
137
+ {
138
+ "type": "array",
139
+ "description": "Inline environment entries in KEY=value form.",
140
+ "items": {
141
+ "type": "string"
142
+ }
143
+ },
144
+ {
145
+ "type": "object",
146
+ "additionalProperties": {
147
+ "type": "string"
148
+ }
149
+ }
150
+ ],
120
151
  "default": {}
121
152
  },
122
153
  "buildOptions": {
@@ -137,14 +168,91 @@
137
168
  },
138
169
  "target": {
139
170
  "type": "string",
140
- "enum": ["wasi", "bindings"],
171
+ "enum": ["wasi", "bindings", "web"],
141
172
  "default": "wasi"
173
+ },
174
+ "env": {
175
+ "description": "Build-only environment overrides. Merges with top-level env and mode env.",
176
+ "oneOf": [
177
+ {
178
+ "type": "string"
179
+ },
180
+ {
181
+ "type": "array",
182
+ "items": {
183
+ "type": "string"
184
+ }
185
+ },
186
+ {
187
+ "type": "object",
188
+ "additionalProperties": {
189
+ "type": "string"
190
+ }
191
+ }
192
+ ],
193
+ "default": {}
142
194
  }
143
195
  },
144
196
  "default": {
145
197
  "cmd": "",
146
198
  "args": [],
147
- "target": "wasi"
199
+ "target": "wasi",
200
+ "env": {}
201
+ }
202
+ },
203
+ "fuzz": {
204
+ "type": "object",
205
+ "additionalProperties": false,
206
+ "description": "Fuzz target configuration used by `ast fuzz` and `ast test --fuzz`.",
207
+ "properties": {
208
+ "input": {
209
+ "type": "array",
210
+ "description": "Glob patterns for fuzz target files.",
211
+ "items": {
212
+ "type": "string"
213
+ },
214
+ "default": ["./assembly/__fuzz__/*.fuzz.ts"]
215
+ },
216
+ "runs": {
217
+ "type": "number",
218
+ "description": "Number of fuzz iterations per target.",
219
+ "default": 1000
220
+ },
221
+ "seed": {
222
+ "type": "number",
223
+ "description": "Base deterministic seed for input mutation.",
224
+ "default": 1337
225
+ },
226
+ "maxInputBytes": {
227
+ "type": "number",
228
+ "description": "Maximum generated input size in bytes.",
229
+ "default": 4096
230
+ },
231
+ "target": {
232
+ "type": "string",
233
+ "enum": ["bindings"],
234
+ "description": "Fuzz builds currently require bindings output.",
235
+ "default": "bindings"
236
+ },
237
+ "corpusDir": {
238
+ "type": "string",
239
+ "description": "Directory containing per-target seed corpus files.",
240
+ "default": "./.as-test/fuzz/corpus"
241
+ },
242
+ "crashDir": {
243
+ "type": "string",
244
+ "description": "Directory where crashing inputs and metadata are written.",
245
+ "default": "./.as-test/crashes"
246
+ }
247
+ },
248
+ "default": {
249
+ "input": ["./assembly/__fuzz__/*.fuzz.ts"],
250
+ "runs": 1000,
251
+ "seed": 1337,
252
+ "maxInputBytes": 4096,
253
+ "target": "bindings",
254
+ "corpusDir": "./.as-test/fuzz/corpus",
255
+ "crashDir": "./.as-test/crashes"
148
256
  }
149
257
  },
150
258
  "modes": {
@@ -210,7 +318,27 @@
210
318
  },
211
319
  "target": {
212
320
  "type": "string",
213
- "enum": ["wasi", "bindings"]
321
+ "enum": ["wasi", "bindings", "web"]
322
+ },
323
+ "env": {
324
+ "description": "Mode-specific build environment overrides.",
325
+ "oneOf": [
326
+ {
327
+ "type": "string"
328
+ },
329
+ {
330
+ "type": "array",
331
+ "items": {
332
+ "type": "string"
333
+ }
334
+ },
335
+ {
336
+ "type": "object",
337
+ "additionalProperties": {
338
+ "type": "string"
339
+ }
340
+ }
341
+ ]
214
342
  }
215
343
  }
216
344
  },
@@ -225,6 +353,10 @@
225
353
  "cmd": {
226
354
  "type": "string",
227
355
  "description": "Mode-specific runtime command."
356
+ },
357
+ "browser": {
358
+ "type": "string",
359
+ "description": "Mode-specific browser for web targets. Use chrome, chromium, firefox, webkit, or an executable path."
228
360
  }
229
361
  }
230
362
  },
@@ -257,15 +389,48 @@
257
389
  "required": ["name"]
258
390
  }
259
391
  ]
392
+ },
393
+ "env": {
394
+ "description": "Mode-specific runtime environment overrides.",
395
+ "oneOf": [
396
+ {
397
+ "type": "string"
398
+ },
399
+ {
400
+ "type": "array",
401
+ "items": {
402
+ "type": "string"
403
+ }
404
+ },
405
+ {
406
+ "type": "object",
407
+ "additionalProperties": {
408
+ "type": "string"
409
+ }
410
+ }
411
+ ]
260
412
  }
261
413
  }
262
414
  },
263
415
  "env": {
264
- "type": "object",
265
- "description": "Environment variables injected when building/running this mode.",
266
- "additionalProperties": {
267
- "type": "string"
268
- }
416
+ "description": "Mode-wide environment variables merged before build/run-specific env.",
417
+ "oneOf": [
418
+ {
419
+ "type": "string"
420
+ },
421
+ {
422
+ "type": "array",
423
+ "items": {
424
+ "type": "string"
425
+ }
426
+ },
427
+ {
428
+ "type": "object",
429
+ "additionalProperties": {
430
+ "type": "string"
431
+ }
432
+ }
433
+ ]
269
434
  }
270
435
  }
271
436
  },
@@ -283,10 +448,16 @@
283
448
  "type": "string",
284
449
  "description": "Runtime command; supports placeholders like <file> and <name>.",
285
450
  "default": "node ./.as-test/runners/default.wasi.js <file>"
451
+ },
452
+ "browser": {
453
+ "type": "string",
454
+ "description": "Browser for web targets. Use chrome, chromium, firefox, webkit, or an executable path.",
455
+ "default": ""
286
456
  }
287
457
  },
288
458
  "default": {
289
- "cmd": "node ./.as-test/runners/default.wasi.js <file>"
459
+ "cmd": "node ./.as-test/runners/default.wasi.js <file>",
460
+ "browser": ""
290
461
  }
291
462
  },
292
463
  "reporter": {
@@ -324,13 +495,35 @@
324
495
  }
325
496
  ],
326
497
  "default": ""
498
+ },
499
+ "env": {
500
+ "description": "Run-only environment overrides. Merges with top-level env and mode env.",
501
+ "oneOf": [
502
+ {
503
+ "type": "string"
504
+ },
505
+ {
506
+ "type": "array",
507
+ "items": {
508
+ "type": "string"
509
+ }
510
+ },
511
+ {
512
+ "type": "object",
513
+ "additionalProperties": {
514
+ "type": "string"
515
+ }
516
+ }
517
+ ],
518
+ "default": {}
327
519
  }
328
520
  },
329
521
  "default": {
330
522
  "runtime": {
331
523
  "cmd": "node ./.as-test/runners/default.wasi.js <file>"
332
524
  },
333
- "reporter": ""
525
+ "reporter": "",
526
+ "env": {}
334
527
  }
335
528
  }
336
529
  }
@@ -0,0 +1,10 @@
1
+ import { expect, fuzz, FuzzSeed } from "as-test";
2
+
3
+ fuzz("index windows stay ordered", (start: i32, end: i32): bool => {
4
+ expect(start <= end).toBe(true);
5
+ return end - start <= 64;
6
+ }).generate((seed: FuzzSeed, run: (start: i32, end: i32) => bool): void => {
7
+ const start = seed.i32({ min: 0, max: 64 });
8
+ const width = seed.i32({ min: 0, max: 64 });
9
+ run(start, start + width);
10
+ });
@@ -0,0 +1,8 @@
1
+ import { expect, fuzz, FuzzSeed } from "..";
2
+
3
+ fuzz("byte-sized integers stay within range", (value: i32): bool => {
4
+ expect(value >= 0).toBe(true);
5
+ return value <= 255;
6
+ }).generate((seed: FuzzSeed, run: (value: i32) => bool): void => {
7
+ run(seed.i32({ min: 0, max: 255 }));
8
+ });
@@ -0,0 +1,9 @@
1
+ import { expect, fuzz, FuzzSeed } from "as-test";
2
+
3
+ fuzz("bounded integer addition", (left: i32, right: i32): bool => {
4
+ const sum = left + right;
5
+ expect(sum - right).toBe(left);
6
+ return sum >= i32.MIN_VALUE; // Fails if an expectation fails or false is returned
7
+ }).generate((seed: FuzzSeed, run: (left: i32, right: i32) => bool): void => {
8
+ run(seed.i32({ min: -1000, max: 1000 }), seed.i32({ min: -1000, max: 1000 }));
9
+ });
@@ -0,0 +1,21 @@
1
+ import { expect, fuzz, FuzzSeed } from "as-test";
2
+
3
+ fuzz(
4
+ "ascii strings survive concatenation boundaries",
5
+ (input: string): bool => {
6
+ const wrapped = "[" + input + "]";
7
+ const restored = wrapped.substr(1, input.length);
8
+
9
+ expect(restored).toBe(input);
10
+ return input.length <= 40;
11
+ },
12
+ ).generate((seed: FuzzSeed, run: (input: string) => bool): void => {
13
+ run(
14
+ seed.string({
15
+ charset: "ascii",
16
+ min: 0,
17
+ max: 40,
18
+ exclude: [0x00, 0x0a, 0x0d],
19
+ }),
20
+ );
21
+ });
package/assembly/index.ts CHANGED
@@ -9,32 +9,48 @@ import {
9
9
  CoverPoint,
10
10
  } from "as-test/assembly/coverage";
11
11
  import { Log } from "./src/log";
12
- import { sendFileEnd, sendFileStart, sendReport } from "./util/wipc";
12
+ import {
13
+ requestFuzzConfig as requestHostFuzzConfig,
14
+ sendFileEnd,
15
+ sendFileStart,
16
+ sendReport,
17
+ } from "./util/wipc";
13
18
  import { quote } from "./util/json";
19
+ import {
20
+ createFuzzer,
21
+ FuzzerBase,
22
+ FuzzerResult,
23
+ prepareFuzzIteration,
24
+ } from "./src/fuzz";
25
+ export {
26
+ ArrayOptions,
27
+ BytesOptions,
28
+ FloatOptions,
29
+ Fuzzer0,
30
+ Fuzzer1,
31
+ Fuzzer2,
32
+ Fuzzer3,
33
+ FuzzerBase,
34
+ FuzzerResult,
35
+ FuzzSeed,
36
+ IntegerOptions,
37
+ StringOptions,
38
+ } from "./src/fuzz";
39
+ export { __as_test_deep_equal } from "./src/expectation";
40
+ export { __as_test_json_value } from "./util/json";
14
41
 
15
42
  let entrySuites: Suite[] = [];
43
+ let entryFuzzers: FuzzerBase[] = [];
16
44
 
17
45
  // @ts-ignore
18
46
  const FILE = isDefined(ENTRY_FILE) ? ENTRY_FILE : "unknown";
19
47
 
20
- class ImportSnapshot {
21
- hasValue: bool = false;
22
- value: u32 = 0;
23
- }
24
-
25
- const DEFAULT_IMPORT_SNAPSHOT_VERSION = "default";
26
-
27
48
  // Globals
28
49
  // @ts-ignore
29
50
  @global let __mock_global: Map<string, u32> = new Map<string, u32>();
30
51
  // @ts-ignore
31
52
  @global let __mock_import: Map<string, u32> = new Map<string, u32>();
32
53
  // @ts-ignore
33
- @global let __mock_import_snapshots: Map<string, ImportSnapshot> = new Map<
34
- string,
35
- ImportSnapshot
36
- >();
37
- // @ts-ignore
38
54
  @global let __mock_import_target_by_index: Map<u32, string> = new Map<
39
55
  u32,
40
56
  string
@@ -108,6 +124,27 @@ export function it(description: string, callback: () => void): void {
108
124
  registerSuite(description, callback, "it");
109
125
  }
110
126
 
127
+ /**
128
+ * Creates a focused test case.
129
+ */
130
+ export function only(description: string, callback: () => void): void {
131
+ registerSuite(description, callback, "only");
132
+ }
133
+
134
+ /**
135
+ * Creates a skipped focused test case.
136
+ */
137
+ export function xonly(description: string, callback: () => void): void {
138
+ registerSuite(description, callback, "xonly");
139
+ }
140
+
141
+ /**
142
+ * Creates a todo test case placeholder.
143
+ */
144
+ export function todo(description: string): void {
145
+ registerSuite(description, (): void => {}, "todo");
146
+ }
147
+
111
148
  /**
112
149
  * Creates a skipped test group.
113
150
  */
@@ -129,6 +166,24 @@ export function xit(description: string, callback: () => void): void {
129
166
  registerSuite(description, callback, "xit");
130
167
  }
131
168
 
169
+ export function fuzz<T extends Function>(
170
+ description: string,
171
+ callback: T,
172
+ ): FuzzerBase {
173
+ const entry = createFuzzer(description, callback);
174
+ entryFuzzers.push(entry);
175
+ return entry;
176
+ }
177
+
178
+ export function xfuzz<T extends Function>(
179
+ description: string,
180
+ callback: T,
181
+ ): FuzzerBase {
182
+ const entry = createFuzzer(description, callback, true);
183
+ entryFuzzers.push(entry);
184
+ return entry;
185
+ }
186
+
132
187
  /**
133
188
  * Creates an expectation object for making assertions within a test case.
134
189
  *
@@ -270,40 +325,12 @@ export function unmockImport(oldFn: string): void {
270
325
  }
271
326
 
272
327
  /**
273
- * Save a single import mock value for the given version.
274
- *
275
- * Accepts either:
276
- * - `snapshotImport(importOrPath, version)`
277
- * - `snapshotImport(importOrPath, () => { ... })` (uses default version)
278
- *
279
- * `imp` accepts either a string import path (e.g. "mock.foo") or the imported function.
328
+ * Capture the current return value of a zero-arg callback and return a new function
329
+ * that always returns the captured value.
280
330
  */
281
- export function snapshotImport<T, V>(imp: T, versionOrCapture: V): void {
282
- const importKey = resolveImportKey<T>(imp);
283
- if (isFunction<V>(versionOrCapture)) {
284
- // @ts-ignore
285
- versionOrCapture();
286
- saveImportSnapshot(importKey, DEFAULT_IMPORT_SNAPSHOT_VERSION);
287
- return;
288
- }
289
- saveImportSnapshot(importKey, versionKey<V>(versionOrCapture));
290
- }
291
-
292
- /**
293
- * Restore a single import mock value for the given version.
294
- *
295
- * Accepts either a string import path (e.g. "mock.foo") or the imported function.
296
- */
297
- export function restoreImport<T, V>(imp: T, version: V): void {
298
- const importKey = resolveImportKey<T>(imp);
299
- const snapshotKey = importSnapshotKey(importKey, versionKey<V>(version));
300
- if (!__mock_import_snapshots.has(snapshotKey)) return;
301
- const snapshot = __mock_import_snapshots.get(snapshotKey);
302
- if (snapshot.hasValue) {
303
- __mock_import.set(importKey, snapshot.value);
304
- } else {
305
- __mock_import.delete(importKey);
306
- }
331
+ export function snapshotFn<T>(callback: () => T): () => T {
332
+ const value = callback();
333
+ return (): T => value;
307
334
  }
308
335
 
309
336
  /**
@@ -339,9 +366,15 @@ class RunOptions {
339
366
  * ```
340
367
  */
341
368
  export function run(options: RunOptions = new RunOptions()): void {
369
+ // @ts-ignore
370
+ if (isDefined(AS_TEST_FUZZ)) {
371
+ runFuzzers();
372
+ return;
373
+ }
342
374
  __test_options = options;
343
375
  const time = new Time();
344
376
  let fileVerdict = "none";
377
+ const hasTopLevelOnly = containsOnlySuites(entrySuites);
345
378
  sendFileStart(FILE);
346
379
  time.start = performance.now();
347
380
  for (let i = 0; i < entrySuites.length; i++) {
@@ -353,7 +386,11 @@ export function run(options: RunOptions = new RunOptions()): void {
353
386
  depth = -1;
354
387
  current_suite = null;
355
388
 
356
- suite.run();
389
+ if (hasTopLevelOnly && suite.kind != "only") {
390
+ suite.skip();
391
+ } else {
392
+ suite.run();
393
+ }
357
394
  if (suite.verdict == "fail") {
358
395
  fileVerdict = "fail";
359
396
  } else if (fileVerdict != "fail" && suite.verdict == "ok") {
@@ -374,6 +411,53 @@ export function run(options: RunOptions = new RunOptions()): void {
374
411
  sendReport(report.serialize());
375
412
  }
376
413
 
414
+ function containsOnlySuites(values: Suite[]): bool {
415
+ for (let i = 0; i < values.length; i++) {
416
+ if (unchecked(values[i]).kind == "only") return true;
417
+ }
418
+ return false;
419
+ }
420
+
421
+ class FuzzConfig {
422
+ runs: i32 = 1000;
423
+ seed: u64 = 1337;
424
+ }
425
+
426
+ class FuzzReport {
427
+ fuzzers: FuzzerResult[] = [];
428
+
429
+ serialize(): string {
430
+ let out = '{"fuzzers":[';
431
+ for (let i = 0; i < this.fuzzers.length; i++) {
432
+ if (i) out += ",";
433
+ out += unchecked(this.fuzzers[i]).serialize();
434
+ }
435
+ out += "]}";
436
+ return out;
437
+ }
438
+ }
439
+
440
+ function runFuzzers(): void {
441
+ __test_options = new RunOptions();
442
+ const config = requestFuzzConfig();
443
+ const report = new FuzzReport();
444
+ for (let i = 0; i < entryFuzzers.length; i++) {
445
+ const fuzzer = unchecked(entryFuzzers[i]);
446
+ prepareFuzzIteration();
447
+ const result = fuzzer.run(config.seed, config.runs);
448
+ report.fuzzers.push(result);
449
+ }
450
+ sendReport(report.serialize());
451
+ }
452
+
453
+ function requestFuzzConfig(): FuzzConfig {
454
+ const out = new FuzzConfig();
455
+ const reply = requestHostFuzzConfig();
456
+ out.runs = reply.runs;
457
+ out.seed = reply.seed;
458
+ return out;
459
+ }
460
+
377
461
  function registerSuite(
378
462
  description: string,
379
463
  callback: () => void,
@@ -515,7 +599,7 @@ function toCoveragePointReport(point: CoverPoint): CoveragePointReport {
515
599
  }
516
600
 
517
601
  function snapshotKey(): string {
518
- if (!current_suite) return FILE + "::global::0";
602
+ if (!current_suite) return FILE + "::global";
519
603
  const suite = current_suite!;
520
604
  const parts = new Array<string>();
521
605
  let cursor: Suite | null = suite;
@@ -523,48 +607,19 @@ function snapshotKey(): string {
523
607
  parts.unshift(cursor.description);
524
608
  cursor = cursor.parent;
525
609
  }
526
- const path = parts.join(" > ");
527
- return FILE + "::" + path + "::" + suite.tests.length.toString();
610
+ return FILE + "::" + parts.join(" > ");
528
611
  }
529
612
 
530
- function resolveImportKey<T>(imp: T): string {
531
- if (isString<T>()) {
532
- // @ts-ignore
533
- return imp as string;
534
- }
535
- // @ts-ignore
536
- const index = imp.index as u32;
537
- if (__mock_import_target_by_index.has(index)) {
538
- return __mock_import_target_by_index.get(index);
539
- }
540
- return index.toString();
541
- }
542
-
543
- function importSnapshotKey(importKey: string, version: string): string {
544
- return importKey + "::" + version;
545
- }
546
-
547
- function versionKey<V>(version: V): string {
548
- if (isString<V>()) {
549
- // @ts-ignore
550
- return version as string;
551
- }
552
- if (isInteger<V>()) {
553
- // @ts-ignore
554
- return (<i64>version).toString();
555
- }
556
- ERROR("snapshot/restore version must be string or integer");
557
- return "";
613
+ export function nextUnnamedSnapshotKey(baseKey: string): string {
614
+ if (!current_suite) return baseKey;
615
+ const suite = current_suite!;
616
+ suite.snapshotCount++;
617
+ if (suite.snapshotCount <= 1) return baseKey;
618
+ return baseKey + " #" + suite.snapshotCount.toString();
558
619
  }
559
620
 
560
- function saveImportSnapshot(importKey: string, version: string): void {
561
- const snapshotKey = importSnapshotKey(importKey, version);
562
- const snapshot = new ImportSnapshot();
563
- if (__mock_import.has(importKey)) {
564
- snapshot.hasValue = true;
565
- snapshot.value = __mock_import.get(importKey);
566
- }
567
- __mock_import_snapshots.set(snapshotKey, snapshot);
621
+ export function namedSnapshotKey(baseKey: string, name: string): string {
622
+ return baseKey + " [" + name + "]";
568
623
  }
569
624
 
570
625
  export class Result {