as-test 1.1.0 → 1.1.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.
package/CHANGELOG.md CHANGED
@@ -1,6 +1,11 @@
1
1
  # Change Log
2
2
 
3
- ## Unreleased
3
+ ## v1.1.1
4
+
5
+ - add `ast clean` command to remove build outputs, coverage outputs, crash reports, and logs.
6
+ - remove deps
7
+
8
+ ## v1.1.0
4
9
 
5
10
  ### Upgrading to 1.1.0
6
11
 
@@ -40,9 +45,13 @@
40
45
  ### Modes & CLI
41
46
 
42
47
  - feat: add per-mode `default: boolean` selection so modes can be included in implicit runs or kept manual-only.
43
- - feat: add `ast clean` to remove configured build outputs, crash reports, and logs for the selected modes.
48
+ - feat: add `ast clean` to remove configured build outputs, coverage outputs, crash reports, and logs.
49
+ - feat: make `ast clean` remove everything by default, prompt with `[Y/n]` before a full clean, and allow `-f` / `--force` to skip that confirmation.
44
50
  - fix: restore unnamed root-config execution alongside named default modes when `--mode` is omitted.
45
- - fix: make `ast clean --mode ...` skip shared output paths that are still owned by unselected modes instead of deleting them.
51
+ - fix: make `ast clean` ignore mode `default: false` flags and treat an omitted `--mode` as a full clean across every configured mode.
52
+ - fix: make `ast clean --mode ...` stay scoped to the selected mode(s) and skip shared output paths that are still owned by unselected modes instead of deleting them.
53
+ - fix: make full `ast clean` remove the configured output roots directly so stale legacy build, coverage, and log directories are removed too.
54
+ - fix: simplify `ast clean` console output so it only prints removed paths plus a final summary.
46
55
 
47
56
  ### Tests
48
57
 
@@ -7,3 +7,22 @@ fuzz("bounded integer addition", (left: i32, right: i32): bool => {
7
7
  }).generate((seed: FuzzSeed, run: (left: i32, right: i32) => bool): void => {
8
8
  run(seed.i32({ min: -1000, max: 1000 }), seed.i32({ min: -1000, max: 1000 }));
9
9
  });
10
+
11
+ fuzz("numeric matchers stay consistent for bounded integers", (value: i32): bool => {
12
+ expect(value).toBeNumber();
13
+ expect(value).toBeInteger();
14
+ expect(value).toBeFinite();
15
+ expect(value).toBe(value);
16
+ expect(value).toBeGreaterOrEqualTo(-1000);
17
+ expect(value).toBeLessThanOrEqualTo(1000);
18
+
19
+ if (value != 0) {
20
+ expect(value).toBeTruthy();
21
+ } else {
22
+ expect(value).toBeFalsy();
23
+ }
24
+
25
+ return true;
26
+ }).generate((seed: FuzzSeed, run: (value: i32) => bool): void => {
27
+ run(seed.i32({ min: -1000, max: 1000 }));
28
+ }, 250);
@@ -19,3 +19,34 @@ fuzz(
19
19
  }),
20
20
  );
21
21
  }, 250);
22
+
23
+ fuzz(
24
+ "string matchers stay consistent on derived slices",
25
+ (input: string): bool => {
26
+ const split = input.length >> 1;
27
+ const prefix = input.substr(0, split);
28
+ const suffix = input.substr(split);
29
+
30
+ expect(input).toBeString();
31
+ expect(input).toContain(prefix);
32
+ expect(input).toMatch(suffix);
33
+
34
+ if (input.length > 0) {
35
+ expect(input).toBeTruthy();
36
+ expect(input).toContain(input.charAt(input.length - 1));
37
+ } else {
38
+ expect(input).toBeFalsy();
39
+ }
40
+
41
+ return true;
42
+ },
43
+ ).generate((seed: FuzzSeed, run: (input: string) => bool): void => {
44
+ run(
45
+ seed.string({
46
+ charset: "ascii",
47
+ min: 0,
48
+ max: 40,
49
+ exclude: [0x00, 0x0a, 0x0d],
50
+ }),
51
+ );
52
+ }, 250);
package/assembly/index.ts CHANGED
@@ -1,6 +1,5 @@
1
1
  import { Suite } from "./src/suite";
2
2
  import { Expectation } from "./src/expectation";
3
- import { stringify } from "as-console/stringify";
4
3
  import {
5
4
  __COVER,
6
5
  __POINTS,
@@ -16,6 +15,7 @@ import {
16
15
  sendReport,
17
16
  } from "./util/wipc";
18
17
  import { quote } from "./util/json";
18
+ import { bold, formatValue, green, red } from "./util/format";
19
19
  import {
20
20
  createFuzzer,
21
21
  FuzzerBase,
@@ -245,7 +245,7 @@ export function log<T>(data: T): void {
245
245
  }
246
246
 
247
247
  export function __as_test_log_default<T>(data: T): string {
248
- return stringify(data);
248
+ return formatValue(data);
249
249
  }
250
250
 
251
251
  export function __as_test_log_is_enabled(): bool {
@@ -673,11 +673,11 @@ export class Result {
673
673
  }
674
674
  display(): string {
675
675
  let out = "";
676
- out += `${rainbow.boldMk(this.name)} `;
676
+ out += `${bold(this.name)} `;
677
677
  if (this.arg1) {
678
- out += `${rainbow.boldMk(rainbow.red(this.arg1.toString() + " " + "failed"))}`;
678
+ out += `${bold(red(this.arg1.toString() + " failed"))}`;
679
679
  } else {
680
- out += `${rainbow.boldMk(rainbow.green("0 failed"))}`;
680
+ out += `${bold(green("0 failed"))}`;
681
681
  }
682
682
  out += ` ${this.arg1 + this.arg2} total\n`;
683
683
  return out;
@@ -62,6 +62,7 @@ export class Expectation<T> extends Tests {
62
62
  instr: string,
63
63
  left: string,
64
64
  right: string,
65
+ message: string = "",
65
66
  ): void {
66
67
  if (this._skip) {
67
68
  this.verdict = "skip";
@@ -77,7 +78,8 @@ export class Expectation<T> extends Tests {
77
78
  this.instr = instr;
78
79
  this.left = left;
79
80
  this.right = right;
80
- this.message = isFail ? this._message : "";
81
+ const resolvedMessage = message.length ? message : this._message;
82
+ this.message = isFail ? resolvedMessage : "";
81
83
  if (isFail) {
82
84
  sendAssertionFailure(this._snapshotKey, instr, left, right, this.message);
83
85
  // @ts-ignore
@@ -103,7 +105,7 @@ export class Expectation<T> extends Tests {
103
105
  /**
104
106
  * Tests if a == null
105
107
  */
106
- toBeNull(): void {
108
+ toBeNull(message: string = ""): void {
107
109
  const passed =
108
110
  (isNullable<T>() && changetype<usize>(this._left) == 0) ||
109
111
  (isInteger<T>() && nameof<T>() == "usize" && this._left == 0);
@@ -117,13 +119,14 @@ export class Expectation<T> extends Tests {
117
119
  visualize<T>(
118
120
  load<T>(changetype<usize>(this), offsetof<Expectation<T>>("_right")),
119
121
  ),
122
+ message,
120
123
  );
121
124
  }
122
125
 
123
126
  /**
124
127
  * Tests if a > b
125
128
  */
126
- toBeGreaterThan(value: T): void {
129
+ toBeGreaterThan(value: T, message: string = ""): void {
127
130
  if (!isInteger<T>() && !isFloat<T>())
128
131
  ERROR("toBeGreaterThan() can only be used on number types!");
129
132
 
@@ -139,13 +142,14 @@ export class Expectation<T> extends Tests {
139
142
  visualize<T>(
140
143
  load<T>(changetype<usize>(this), offsetof<Expectation<T>>("_right")),
141
144
  ),
145
+ message,
142
146
  );
143
147
  }
144
148
 
145
149
  /**
146
150
  * Tests if a >= b
147
151
  */
148
- toBeGreaterOrEqualTo(value: T): void {
152
+ toBeGreaterOrEqualTo(value: T, message: string = ""): void {
149
153
  if (!isInteger<T>() && !isFloat<T>())
150
154
  ERROR("toBeGreaterOrEqualTo() can only be used on number types!");
151
155
 
@@ -161,13 +165,14 @@ export class Expectation<T> extends Tests {
161
165
  visualize<T>(
162
166
  load<T>(changetype<usize>(this), offsetof<Expectation<T>>("_right")),
163
167
  ),
168
+ message,
164
169
  );
165
170
  }
166
171
 
167
172
  /**
168
173
  * Tests if a < b
169
174
  */
170
- toBeLessThan(value: T): void {
175
+ toBeLessThan(value: T, message: string = ""): void {
171
176
  if (!isInteger<T>() && !isFloat<T>())
172
177
  ERROR("toBeLessThan() can only be used on number types!");
173
178
 
@@ -183,13 +188,14 @@ export class Expectation<T> extends Tests {
183
188
  visualize<T>(
184
189
  load<T>(changetype<usize>(this), offsetof<Expectation<T>>("_right")),
185
190
  ),
191
+ message,
186
192
  );
187
193
  }
188
194
 
189
195
  /**
190
196
  * Tests if a <= b
191
197
  */
192
- toBeLessThanOrEqualTo(value: T): void {
198
+ toBeLessThanOrEqualTo(value: T, message: string = ""): void {
193
199
  if (!isInteger<T>() && !isFloat<T>())
194
200
  ERROR("toBeLessThanOrEqualTo() can only be used on number types!");
195
201
 
@@ -205,93 +211,127 @@ export class Expectation<T> extends Tests {
205
211
  visualize<T>(
206
212
  load<T>(changetype<usize>(this), offsetof<Expectation<T>>("_right")),
207
213
  ),
214
+ message,
208
215
  );
209
216
  }
210
217
 
211
218
  /**
212
219
  * Tests if a is string
213
220
  */
214
- toBeString(): void {
215
- this._resolve(isString<T>(), "toBeString", q(nameof<T>()), q("string"));
221
+ toBeString(message: string = ""): void {
222
+ this._resolve(
223
+ isString<T>(),
224
+ "toBeString",
225
+ q(nameof<T>()),
226
+ q("string"),
227
+ message,
228
+ );
216
229
  }
217
230
 
218
231
  /**
219
232
  * Tests if a is boolean
220
233
  */
221
- toBeBoolean(): void {
222
- this._resolve(isBoolean<T>(), "toBeBoolean", q(nameof<T>()), q("boolean"));
234
+ toBeBoolean(message: string = ""): void {
235
+ this._resolve(
236
+ isBoolean<T>(),
237
+ "toBeBoolean",
238
+ q(nameof<T>()),
239
+ q("boolean"),
240
+ message,
241
+ );
223
242
  }
224
243
 
225
244
  /**
226
245
  * Tests if a is array
227
246
  */
228
- toBeArray(): void {
229
- this._resolve(isArray<T>(), "toBeArray", q(nameof<T>()), q("Array<any>"));
247
+ toBeArray(message: string = ""): void {
248
+ this._resolve(
249
+ isArray<T>(),
250
+ "toBeArray",
251
+ q(nameof<T>()),
252
+ q("Array<any>"),
253
+ message,
254
+ );
230
255
  }
231
256
 
232
257
  /**
233
258
  * Tests if a is number
234
259
  */
235
- toBeNumber(): void {
260
+ toBeNumber(message: string = ""): void {
236
261
  this._resolve(
237
262
  isFloat<T>() || isInteger<T>(),
238
263
  "toBeNumber",
239
264
  q(nameof<T>()),
240
265
  q("number"),
266
+ message,
241
267
  );
242
268
  }
243
269
 
244
270
  /**
245
271
  * Tests if a is integer
246
272
  */
247
- toBeInteger(): void {
248
- this._resolve(isInteger<T>(), "toBeInteger", q(nameof<T>()), q("integer"));
273
+ toBeInteger(message: string = ""): void {
274
+ this._resolve(
275
+ isInteger<T>(),
276
+ "toBeInteger",
277
+ q(nameof<T>()),
278
+ q("integer"),
279
+ message,
280
+ );
249
281
  }
250
282
 
251
283
  /**
252
284
  * Tests if a is float
253
285
  */
254
- toBeFloat(): void {
255
- this._resolve(isFloat<T>(), "toBeFloat", q(nameof<T>()), q("float"));
286
+ toBeFloat(message: string = ""): void {
287
+ this._resolve(
288
+ isFloat<T>(),
289
+ "toBeFloat",
290
+ q(nameof<T>()),
291
+ q("float"),
292
+ message,
293
+ );
256
294
  }
257
295
 
258
296
  /**
259
297
  * Tests if a is finite
260
298
  */
261
- toBeFinite(): void {
299
+ toBeFinite(message: string = ""): void {
262
300
  // @ts-ignore
263
301
  const passed = (isFloat<T>() || isInteger<T>()) && isFinite(this._left);
264
- this._resolve(passed, "toBeFinite", q("Infinity"), q("Finite"));
302
+ this._resolve(passed, "toBeFinite", q("Infinity"), q("Finite"), message);
265
303
  }
266
304
 
267
305
  /**
268
306
  * Tests if a value is truthy
269
307
  */
270
- toBeTruthy(): void {
308
+ toBeTruthy(message: string = ""): void {
271
309
  this._resolve(
272
310
  isTruthy<T>(this._left),
273
311
  "toBeTruthy",
274
312
  q("falsy"),
275
313
  q("truthy"),
314
+ message,
276
315
  );
277
316
  }
278
317
 
279
318
  /**
280
319
  * Tests if a value is falsy
281
320
  */
282
- toBeFalsy(): void {
321
+ toBeFalsy(message: string = ""): void {
283
322
  this._resolve(
284
323
  !isTruthy<T>(this._left),
285
324
  "toBeFalsy",
286
325
  q("truthy"),
287
326
  q("falsy"),
327
+ message,
288
328
  );
289
329
  }
290
330
 
291
331
  /**
292
332
  * Tests if a floating-point number is close to expected
293
333
  */
294
- toBeCloseTo(expected: T, precision: i32 = 2): void {
334
+ toBeCloseTo(expected: T, precision: i32 = 2, message: string = ""): void {
295
335
  if (!isFloat<T>() && !isInteger<T>())
296
336
  ERROR("toBeCloseTo() can only be used on number types!");
297
337
  const factor = Math.pow(10, precision as f64);
@@ -302,67 +342,74 @@ export class Expectation<T> extends Tests {
302
342
  "toBeCloseTo",
303
343
  visualize<T>(this._left),
304
344
  visualize<T>(expected),
345
+ message,
305
346
  );
306
347
  }
307
348
 
308
349
  /**
309
350
  * Tests if a string contains substring
310
351
  */
311
- toMatch(value: string): void {
352
+ toMatch(value: string, message: string = ""): void {
312
353
  if (!isString<T>()) ERROR("toMatch() can only be used on string types!");
313
354
  // @ts-ignore
314
355
  const passed = this._left.indexOf(value) >= 0;
315
356
  // @ts-ignore
316
- this._resolve(passed, "toMatch", q(this._left as string), q(value));
357
+ this._resolve(passed, "toMatch", q(this._left as string), q(value), message);
317
358
  }
318
359
 
319
360
  /**
320
361
  * Tests if a string starts with the provided prefix.
321
362
  */
322
- toStartWith(value: string): void {
363
+ toStartWith(value: string, message: string = ""): void {
323
364
  if (!isString<T>())
324
365
  ERROR("toStartWith() can only be used on string types!");
325
366
  // @ts-ignore
326
367
  const left = this._left as string;
327
368
  const passed = left.indexOf(value) == 0;
328
- this._resolve(passed, "toStartWith", q(left), q(value));
369
+ this._resolve(passed, "toStartWith", q(left), q(value), message);
329
370
  }
330
371
 
331
372
  /**
332
373
  * Tests if a string ends with the provided suffix.
333
374
  */
334
- toEndWith(value: string): void {
375
+ toEndWith(value: string, message: string = ""): void {
335
376
  if (!isString<T>()) ERROR("toEndWith() can only be used on string types!");
336
377
  // @ts-ignore
337
378
  const left = this._left as string;
338
379
  const idx = left.lastIndexOf(value);
339
380
  const passed = idx >= 0 && idx + value.length == left.length;
340
- this._resolve(passed, "toEndWith", q(left), q(value));
381
+ this._resolve(passed, "toEndWith", q(left), q(value), message);
341
382
  }
342
383
 
343
384
  /**
344
385
  * Tests if an array has length x
345
386
  */
346
- toHaveLength(value: i32): void {
387
+ toHaveLength(value: i32, message: string = ""): void {
347
388
  // @ts-ignore
348
389
  const leftLen = this._left.length as i32;
349
390
  // @ts-ignore
350
391
  const passed = isArray<T>() && leftLen == value;
351
- this._resolve(passed, "toHaveLength", leftLen.toString(), value.toString());
392
+ this._resolve(
393
+ passed,
394
+ "toHaveLength",
395
+ leftLen.toString(),
396
+ value.toString(),
397
+ message,
398
+ );
352
399
  }
353
400
 
354
401
  /**
355
402
  * Tests if an array or string contains a value
356
403
  */
357
404
  // @ts-ignore
358
- toContain(value: valueof<T>): void {
405
+ toContain(value: valueof<T>, message: string = ""): void {
359
406
  if (isString<T>()) {
360
407
  // @ts-ignore
361
408
  const left = this._left as string;
362
409
  // @ts-ignore
363
410
  const needle = value as string;
364
411
  const passed = left.indexOf(needle) >= 0;
365
- this._resolve(passed, "toContain", q(left), q(needle));
412
+ this._resolve(passed, "toContain", q(left), q(needle), message);
366
413
  return;
367
414
  }
368
415
 
@@ -374,6 +421,7 @@ export class Expectation<T> extends Tests {
374
421
  "toContain",
375
422
  stringifyValue<T>(this._left),
376
423
  stringifyValue<valueof<T>>(value),
424
+ message,
377
425
  );
378
426
  return;
379
427
  }
@@ -385,28 +433,28 @@ export class Expectation<T> extends Tests {
385
433
  * Alias for toContain().
386
434
  */
387
435
  // @ts-ignore
388
- toContains(value: valueof<T>): void {
389
- this.toContain(value);
436
+ toContains(value: valueof<T>, message: string = ""): void {
437
+ this.toContain(value, message);
390
438
  }
391
439
 
392
440
  /**
393
441
  * Tests if serialized value matches stored snapshot.
394
442
  */
395
- toMatchSnapshot(name: string = ""): void {
443
+ toMatchSnapshot(name: string = "", message: string = ""): void {
396
444
  let key = name.length
397
445
  ? namedSnapshotKey(this._snapshotKey, name)
398
446
  : nextUnnamedSnapshotKey(this._snapshotKey);
399
447
 
400
448
  const actual = stringifyValue<T>(this._left);
401
449
  const res = snapshotAssert(key, actual);
402
- this._resolve(res.ok, "toMatchSnapshot", actual, res.expected);
450
+ this._resolve(res.ok, "toMatchSnapshot", actual, res.expected, message);
403
451
  }
404
452
 
405
453
  /**
406
454
  * Delegates throw assertions to try-as when available.
407
455
  * If try-as is unavailable, this matcher is disabled and warns once.
408
456
  */
409
- toThrow(): void {
457
+ toThrow(message: string = ""): void {
410
458
  // @ts-ignore
411
459
  if (!isDefined(AS_TEST_TRY_AS)) {
412
460
  if (!warnedToThrowDisabled) {
@@ -415,7 +463,7 @@ export class Expectation<T> extends Tests {
415
463
  );
416
464
  warnedToThrowDisabled = true;
417
465
  }
418
- this._resolve(true, "toThrow", q("disabled"), q("disabled"));
466
+ this._resolve(true, "toThrow", q("disabled"), q("disabled"), message);
419
467
  return;
420
468
  }
421
469
 
@@ -425,13 +473,13 @@ export class Expectation<T> extends Tests {
425
473
  // @ts-ignore
426
474
  __ExceptionState.Failures--;
427
475
  }
428
- this._resolve(passed, "toThrow", q("throws"), q("throws"));
476
+ this._resolve(passed, "toThrow", q("throws"), q("throws"), message);
429
477
  }
430
478
 
431
479
  /**
432
480
  * Tests for equality
433
481
  */
434
- toBe(equals: T): void {
482
+ toBe(equals: T, message: string = ""): void {
435
483
  const passed = this._left === equals;
436
484
 
437
485
  this._resolve(
@@ -439,32 +487,35 @@ export class Expectation<T> extends Tests {
439
487
  "toBe",
440
488
  stringifyValue<T>(this._left),
441
489
  stringifyValue<T>(equals),
490
+ message,
442
491
  );
443
492
  }
444
493
 
445
494
  /**
446
495
  * Tests for deep equality
447
496
  */
448
- toEqual(equals: T): void {
497
+ toEqual(equals: T, message: string = ""): void {
449
498
  const passed = valueEquals<T>(this._left, equals, false);
450
499
  this._resolve(
451
500
  passed,
452
501
  "toEqual",
453
502
  stringifyValue<T>(this._left),
454
503
  stringifyValue<T>(equals),
504
+ message,
455
505
  );
456
506
  }
457
507
 
458
508
  /**
459
509
  * Tests for strict deep equality
460
510
  */
461
- toStrictEqual(equals: T): void {
511
+ toStrictEqual(equals: T, message: string = ""): void {
462
512
  const passed = valueEquals<T>(this._left, equals, true);
463
513
  this._resolve(
464
514
  passed,
465
515
  "toStrictEqual",
466
516
  stringifyValue<T>(this._left),
467
517
  stringifyValue<T>(equals),
518
+ message,
468
519
  );
469
520
  }
470
521
  }
@@ -0,0 +1,104 @@
1
+ export function formatValue<T>(value: T, deep: boolean = false): string {
2
+ if (isNullable<T>() && changetype<usize>(value) == <usize>0) {
3
+ return "null";
4
+ }
5
+
6
+ if (isString<T>()) {
7
+ const text = value as string;
8
+ return deep ? "'" + text + "'" : text;
9
+ }
10
+
11
+ if (isBoolean<T>() || isInteger<T>() || isFloat<T>()) {
12
+ // @ts-expect-error: primitive formatting
13
+ return value.toString();
14
+ }
15
+
16
+ if (isArray<T>()) {
17
+ // @ts-expect-error: array-like handling
18
+ const values = value as valueof<T>[];
19
+ if (!values.length) return "[]";
20
+ let out = "[";
21
+ for (let i = 0; i < values.length; i++) {
22
+ if (i) out += ", ";
23
+ out += formatValue<valueof<T>>(unchecked(values[i]), true);
24
+ }
25
+ out += "]";
26
+ return out;
27
+ }
28
+
29
+ if (value instanceof Map) {
30
+ // @ts-expect-error: generic runtime access
31
+ const keys = value.keys();
32
+ if (!keys.length) return "Map(0) {}";
33
+ // @ts-expect-error: generic runtime access
34
+ const values = value.values();
35
+ let out = "Map(" + keys.length.toString() + ") { ";
36
+ for (let i = 0; i < keys.length; i++) {
37
+ if (i) out += ", ";
38
+ out += formatValue(changetype<valueof<typeof keys>>(unchecked(keys[i])), true);
39
+ out += " => ";
40
+ out += formatValue(
41
+ changetype<valueof<typeof values>>(unchecked(values[i])),
42
+ true,
43
+ );
44
+ }
45
+ out += " }";
46
+ return out;
47
+ }
48
+
49
+ if (value instanceof Set) {
50
+ // @ts-expect-error: generic runtime access
51
+ const values = value.values();
52
+ if (!values.length) return "Set(0) {}";
53
+ let out = "Set(" + values.length.toString() + ") { ";
54
+ for (let i = 0; i < values.length; i++) {
55
+ if (i) out += ", ";
56
+ out += formatValue(
57
+ changetype<valueof<typeof values>>(unchecked(values[i])),
58
+ true,
59
+ );
60
+ }
61
+ out += " }";
62
+ return out;
63
+ }
64
+
65
+ if (isManaged<T>()) {
66
+ // @ts-expect-error: custom serializer when provided
67
+ if (isDefined(value.__as_test_json)) {
68
+ // @ts-expect-error: dynamic method dispatch
69
+ return value.__as_test_json();
70
+ }
71
+ }
72
+
73
+ return nameof<T>();
74
+ }
75
+
76
+ @inline
77
+ export function colorText(format: i32[], text: string): string {
78
+ return `\u001b[${format[0].toString()}m${text}\u001b[${format[1].toString()}m`;
79
+ }
80
+
81
+ @inline
82
+ export function red(text: string): string {
83
+ return colorText([31, 39], text);
84
+ }
85
+
86
+ @inline
87
+ export function green(text: string): string {
88
+ return colorText([32, 39], text);
89
+ }
90
+
91
+ @inline
92
+ export function bgRed(text: string): string {
93
+ return colorText([41, 49], text);
94
+ }
95
+
96
+ @inline
97
+ export function bgGreen(text: string): string {
98
+ return colorText([42, 49], text);
99
+ }
100
+
101
+ @inline
102
+ export function bold(text: string): string {
103
+ return colorText([1, 22], text);
104
+ }
@@ -1,4 +1,4 @@
1
- import { rainbow } from "as-rainbow";
1
+ import { bgGreen, bgRed } from "./format";
2
2
 
3
3
  export function visualize<T>(value: T): string {
4
4
  if (isNullable<T>() && changetype<usize>(value) == <usize>0) {
@@ -61,16 +61,16 @@ export function diff(left: string, right: string, not: boolean = false): Diff {
61
61
  const rChar = right.charAt(i);
62
62
  if (not) {
63
63
  if (lChar == rChar) {
64
- lDiff += rainbow.bgGreen(rChar);
65
- rDiff += rainbow.bgRed(lChar);
64
+ lDiff += bgGreen(rChar);
65
+ rDiff += bgRed(lChar);
66
66
  } else {
67
67
  lDiff += rChar;
68
68
  rDiff += lChar;
69
69
  }
70
70
  } else {
71
71
  if (lChar != rChar) {
72
- lDiff += rainbow.bgGreen(rChar);
73
- rDiff += rainbow.bgRed(lChar);
72
+ lDiff += bgGreen(rChar);
73
+ rDiff += bgRed(lChar);
74
74
  } else {
75
75
  lDiff += rChar;
76
76
  rDiff += lChar;
@@ -80,9 +80,9 @@ export function diff(left: string, right: string, not: boolean = false): Diff {
80
80
 
81
81
  if (!not) {
82
82
  for (; i < left.length; i++) {
83
- rDiff += rainbow.bgRed(left.charAt(i));
83
+ rDiff += bgRed(left.charAt(i));
84
84
  }
85
- for (; i < right.length; i++) lDiff += rainbow.bgRed(right.charAt(i));
85
+ for (; i < right.length; i++) lDiff += bgRed(right.charAt(i));
86
86
  }
87
87
 
88
88
  return {
@@ -90,9 +90,3 @@ export function diff(left: string, right: string, not: boolean = false): Diff {
90
90
  right: rDiff,
91
91
  };
92
92
  }
93
-
94
- // @ts-ignore
95
- @inline
96
- export function colorText(format: i32[], text: string): string {
97
- return `\u001b[${format[0].toString()}m${text}\u001b[${format[1].toString()}m`;
98
- }
@@ -1,4 +1,4 @@
1
- import { stringify } from "as-console/stringify";
1
+ import { formatValue } from "./format";
2
2
 
3
3
  export function quote(value: string): string {
4
4
  return '"' + escape(value) + '"';
@@ -36,7 +36,7 @@ export function stringifyValue<T>(value: T): string {
36
36
  return value.__as_test_json();
37
37
  }
38
38
 
39
- const formatted = stringify<T>(value);
39
+ const formatted = formatValue<T>(value);
40
40
  if (formatted != "none") {
41
41
  return quote(formatted);
42
42
  }
@@ -305,9 +305,12 @@ function wasiWriteAll(data: ArrayBuffer): void {
305
305
  store<usize>(iovPtr, <usize>left, sizeof<usize>());
306
306
  store<u32>(writtenPtr, 0, 0);
307
307
  const errno = wasi_fd_write(1, iovPtr, 1, writtenPtr);
308
+ if (errno == WASI_ERRNO_AGAIN || errno == WASI_ERRNO_INTR) {
309
+ continue;
310
+ }
308
311
  if (errno != 0) return;
309
312
  const written = <i32>load<u32>(writtenPtr, 0);
310
- if (written <= 0) return;
313
+ if (written <= 0) continue;
311
314
  offset += written;
312
315
  }
313
316
  }
@@ -3,36 +3,50 @@ import { existsSync, rmSync } from "fs";
3
3
  import * as path from "path";
4
4
  import { applyMode, loadConfig } from "../util.js";
5
5
  const DEFAULT_CONFIG_PATH = path.join(process.cwd(), "./as-test.config.json");
6
- export async function clean(configPath = DEFAULT_CONFIG_PATH, modes = [undefined]) {
6
+ export async function clean(configPath = DEFAULT_CONFIG_PATH, modes = [undefined], fullClean = false) {
7
7
  const loadedConfig = loadConfig(configPath, true);
8
8
  const targets = new Map();
9
9
  const ownership = buildOwnershipMap(loadedConfig);
10
- for (const modeName of modes) {
11
- const active = applyMode(loadedConfig, modeName).config;
12
- collectTarget(targets, active.outDir, modeName, "build");
13
- collectTarget(targets, active.fuzz.crashDir, modeName, "crashes");
14
- collectTarget(targets, active.logs, modeName, "logs");
10
+ if (fullClean) {
11
+ collectRootTarget(targets, loadedConfig.outDir, "build");
12
+ collectRootTarget(targets, loadedConfig.fuzz.crashDir, "crashes");
13
+ collectRootTarget(targets, loadedConfig.coverageDir, "coverage");
14
+ collectRootTarget(targets, loadedConfig.logs, "logs");
15
+ for (const modeName of modes) {
16
+ const active = applyMode(loadedConfig, modeName).config;
17
+ collectTarget(targets, active.outDir, modeName, "build");
18
+ collectTarget(targets, active.fuzz.crashDir, modeName, "crashes");
19
+ collectTarget(targets, active.coverageDir, modeName, "coverage");
20
+ collectTarget(targets, active.logs, modeName, "logs");
21
+ }
22
+ pruneNestedTargets(targets);
23
+ }
24
+ else {
25
+ for (const modeName of modes) {
26
+ const active = applyMode(loadedConfig, modeName).config;
27
+ collectTarget(targets, active.outDir, modeName, "build");
28
+ collectTarget(targets, active.fuzz.crashDir, modeName, "crashes");
29
+ collectTarget(targets, active.coverageDir, modeName, "coverage");
30
+ collectTarget(targets, active.logs, modeName, "logs");
31
+ }
15
32
  }
16
33
  let removed = 0;
17
- let skipped = 0;
18
34
  for (const [targetPath, owners] of [...targets.entries()].sort((a, b) => a[0].localeCompare(b[0]))) {
19
- const allOwners = ownership.get(targetPath) ?? owners;
20
- const unselectedOwners = allOwners.filter((owner) => !owners.includes(owner));
21
- if (unselectedOwners.length) {
22
- skipped++;
23
- process.stdout.write(`${chalk.dim("skip")} ${toRelativePath(targetPath)} ${chalk.dim(`(shared with ${unselectedOwners.join(", ")})`)}\n`);
24
- continue;
35
+ if (!fullClean) {
36
+ const allOwners = ownership.get(targetPath) ?? owners;
37
+ const unselectedOwners = allOwners.filter((owner) => !owners.includes(owner));
38
+ if (unselectedOwners.length) {
39
+ continue;
40
+ }
25
41
  }
26
42
  if (!existsSync(targetPath)) {
27
- skipped++;
28
- process.stdout.write(`${chalk.dim("skip")} ${toRelativePath(targetPath)} ${chalk.dim(`(${owners.join(", ")})`)}\n`);
29
43
  continue;
30
44
  }
31
45
  rmSync(targetPath, { recursive: true, force: true });
32
46
  removed++;
33
47
  process.stdout.write(`${chalk.bgGreenBright.black(" CLEAN ")} ${toRelativePath(targetPath)} ${chalk.dim(`(${owners.join(", ")})`)}\n`);
34
48
  }
35
- process.stdout.write(`${chalk.bold("Summary:")} removed ${removed} path(s), skipped ${skipped} missing path(s)\n`);
49
+ process.stdout.write(`${chalk.bold("Summary:")} removed ${removed} path(s)\n`);
36
50
  }
37
51
  function buildOwnershipMap(loadedConfig) {
38
52
  const ownership = new Map();
@@ -44,10 +58,25 @@ function buildOwnershipMap(loadedConfig) {
44
58
  const active = applyMode(loadedConfig, modeName).config;
45
59
  collectOwnership(ownership, active.outDir, modeName, "build");
46
60
  collectOwnership(ownership, active.fuzz.crashDir, modeName, "crashes");
61
+ collectOwnership(ownership, active.coverageDir, modeName, "coverage");
47
62
  collectOwnership(ownership, active.logs, modeName, "logs");
48
63
  }
49
64
  return ownership;
50
65
  }
66
+ function collectRootTarget(targets, rawPath, kind) {
67
+ if (!rawPath || rawPath == "none")
68
+ return;
69
+ const resolved = path.resolve(process.cwd(), rawPath);
70
+ ensureSafeCleanPath(resolved, rawPath, kind);
71
+ const owner = `all:${kind}`;
72
+ const existing = targets.get(resolved);
73
+ if (existing) {
74
+ if (!existing.includes(owner))
75
+ existing.push(owner);
76
+ return;
77
+ }
78
+ targets.set(resolved, [owner]);
79
+ }
51
80
  function collectOwnership(ownership, rawPath, modeName, kind) {
52
81
  if (!rawPath || rawPath == "none")
53
82
  return;
@@ -76,6 +105,20 @@ function collectTarget(targets, rawPath, modeName, kind) {
76
105
  }
77
106
  targets.set(resolved, [owner]);
78
107
  }
108
+ function pruneNestedTargets(targets) {
109
+ const paths = [...targets.keys()].sort((a, b) => a.length - b.length);
110
+ for (const targetPath of paths) {
111
+ for (const otherPath of paths) {
112
+ if (targetPath == otherPath)
113
+ continue;
114
+ const relative = path.relative(targetPath, otherPath);
115
+ if (!relative.length || relative == ".." || relative.startsWith(`..${path.sep}`)) {
116
+ continue;
117
+ }
118
+ targets.delete(otherPath);
119
+ }
120
+ }
121
+ }
79
122
  function ensureSafeCleanPath(resolvedPath, rawPath, kind) {
80
123
  const cwd = path.resolve(process.cwd());
81
124
  const relative = path.relative(cwd, resolvedPath);
@@ -1,6 +1,51 @@
1
+ import chalk from "chalk";
2
+ import { createInterface } from "readline";
1
3
  import { clean } from "./clean-core.js";
4
+ import { loadConfig } from "../util.js";
2
5
  export { clean } from "./clean-core.js";
3
- export async function executeCleanCommand(configPath, selectedModes, resolveExecutionModes) {
4
- const modeTargets = resolveExecutionModes(configPath, selectedModes);
5
- await clean(configPath, modeTargets);
6
+ export async function executeCleanCommand(rawArgs, configPath, selectedModes, resolveExecutionModes) {
7
+ const force = rawArgs.includes("-f") || rawArgs.includes("--force");
8
+ const modeTargets = selectedModes.length > 0
9
+ ? resolveExecutionModes(configPath, selectedModes)
10
+ : resolveAllCleanModes(configPath);
11
+ if (!force && selectedModes.length == 0) {
12
+ await confirmFullClean(configPath);
13
+ }
14
+ await clean(configPath, modeTargets, selectedModes.length == 0);
15
+ }
16
+ function resolveAllCleanModes(configPath) {
17
+ const resolvedConfigPath = configPath ?? "./as-test.config.json";
18
+ const config = loadConfig(resolvedConfigPath, true);
19
+ return [undefined, ...Object.keys(config.modes)];
20
+ }
21
+ async function confirmFullClean(configPath) {
22
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
23
+ throw new Error('clean without --mode requires confirmation. Re-run with "-f" or "--force" to skip the prompt.');
24
+ }
25
+ const target = configPath ? ` in ${configPath}` : "";
26
+ process.stdout.write(chalk.bold.blue("◇ Confirm Clean") +
27
+ "\n" +
28
+ `│ This will remove configured build outputs, crash reports, and logs for every mode${target}.\n` +
29
+ "│\n");
30
+ const answer = await promptLine("Continue? [Y/n] ");
31
+ const normalized = answer.trim().toLowerCase();
32
+ if (normalized == "" || normalized == "y" || normalized == "yes")
33
+ return;
34
+ if (normalized == "n" || normalized == "no") {
35
+ process.stdout.write(chalk.dim("clean cancelled\n"));
36
+ process.exit(0);
37
+ }
38
+ throw new Error(`invalid answer "${answer}". Expected yes or no.`);
39
+ }
40
+ function promptLine(question) {
41
+ return new Promise((resolve) => {
42
+ const rl = createInterface({
43
+ input: process.stdin,
44
+ output: process.stdout,
45
+ });
46
+ rl.question(question, (answer) => {
47
+ rl.close();
48
+ resolve(answer);
49
+ });
50
+ });
6
51
  }
package/bin/index.js CHANGED
@@ -134,7 +134,7 @@ else if (COMMANDS.includes(args[0])) {
134
134
  });
135
135
  }
136
136
  else if (command === "clean") {
137
- executeCleanCommand(configPath, selectedModes, resolveExecutionModes).catch((error) => {
137
+ executeCleanCommand(_args, configPath, selectedModes, resolveExecutionModes).catch((error) => {
138
138
  printCliError(error);
139
139
  process.exit(1);
140
140
  });
@@ -370,10 +370,11 @@ function printCommandHelp(command) {
370
370
  }
371
371
  if (command == "clean") {
372
372
  process.stdout.write(chalk.bold("Usage: ast clean [flags]\n\n"));
373
- process.stdout.write("Remove configured build outputs, crash reports, and logs for the selected modes.\n\n");
373
+ process.stdout.write("Remove configured build outputs, crash reports, and logs.\n\n");
374
374
  process.stdout.write(chalk.bold("Flags:\n"));
375
375
  process.stdout.write(" --config <path> Use a specific config file\n");
376
376
  process.stdout.write(" --mode <name[,name...]> Clean one or multiple named modes\n");
377
+ process.stdout.write(" -f, --force Skip the full-clean confirmation prompt\n");
377
378
  process.stdout.write(" --help, -h Show this help\n");
378
379
  return;
379
380
  }
package/bin/wipc.js CHANGED
@@ -31,8 +31,12 @@ export class Channel {
31
31
  return;
32
32
  const idx = this.buffer.indexOf(Channel.MAGIC);
33
33
  if (idx === -1) {
34
- this.onPassthrough(this.buffer);
35
- this.buffer = Buffer.alloc(0);
34
+ const keep = Math.min(this.buffer.length, Channel.MAGIC_PREFIX_MAX);
35
+ const flushLength = this.buffer.length - keep;
36
+ if (flushLength > 0) {
37
+ this.onPassthrough(this.buffer.subarray(0, flushLength));
38
+ this.buffer = this.buffer.subarray(flushLength);
39
+ }
36
40
  return;
37
41
  }
38
42
  if (idx > 0) {
@@ -77,3 +81,4 @@ export class Channel {
77
81
  }
78
82
  Channel.MAGIC = Buffer.from("WIPC");
79
83
  Channel.HEADER_SIZE = 9;
84
+ Channel.MAGIC_PREFIX_MAX = Channel.MAGIC.length - 1;
@@ -96,7 +96,25 @@ function readExact(length) {
96
96
  }
97
97
  function writeRaw(data) {
98
98
  const view = Buffer.from(data);
99
- fs.writeSync(1, view);
99
+ let offset = 0;
100
+ while (offset < view.byteLength) {
101
+ let written = 0;
102
+ try {
103
+ written = fs.writeSync(1, view, offset, view.byteLength - offset);
104
+ }
105
+ catch (error) {
106
+ if (error &&
107
+ typeof error == "object" &&
108
+ "code" in error &&
109
+ error.code == "EAGAIN") {
110
+ continue;
111
+ }
112
+ throw error;
113
+ }
114
+ if (!written)
115
+ continue;
116
+ offset += written;
117
+ }
100
118
  }
101
119
  function mergeImports(...groups) {
102
120
  const out = {};
package/lib/src/index.ts CHANGED
@@ -130,7 +130,25 @@ function readExact(length: number): ArrayBuffer {
130
130
 
131
131
  function writeRaw(data: ArrayBuffer): void {
132
132
  const view = Buffer.from(data);
133
- fs.writeSync(1, view);
133
+ let offset = 0;
134
+ while (offset < view.byteLength) {
135
+ let written = 0;
136
+ try {
137
+ written = fs.writeSync(1, view, offset, view.byteLength - offset);
138
+ } catch (error) {
139
+ if (
140
+ error &&
141
+ typeof error == "object" &&
142
+ "code" in error &&
143
+ error.code == "EAGAIN"
144
+ ) {
145
+ continue;
146
+ }
147
+ throw error;
148
+ }
149
+ if (!written) continue;
150
+ offset += written;
151
+ }
134
152
  }
135
153
 
136
154
  function mergeImports(...groups: unknown[]): AnyImports {
package/package.json CHANGED
@@ -1,14 +1,12 @@
1
1
  {
2
2
  "name": "as-test",
3
- "version": "1.1.0",
3
+ "version": "1.1.1",
4
4
  "author": "Jairus Tanaka",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/JairusSW/as-test.git"
8
8
  },
9
9
  "dependencies": {
10
- "as-rainbow": "^0.1.0",
11
- "as-console": "^7.0.0",
12
10
  "chalk": "^5.6.2",
13
11
  "glob": "^13.0.6",
14
12
  "typer-diff": "^1.1.1",