as-test 1.0.16 → 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 +57 -0
- package/README.md +45 -4
- package/as-test.config.schema.json +5 -0
- package/assembly/__fuzz__/math.fuzz.ts +19 -0
- package/assembly/__fuzz__/string.fuzz.ts +31 -0
- package/assembly/index.ts +5 -5
- package/assembly/src/expectation.ts +93 -42
- package/assembly/util/format.ts +104 -0
- package/assembly/util/helpers.ts +7 -13
- package/assembly/util/json.ts +2 -2
- package/assembly/util/wipc.ts +15 -5
- package/bin/commands/clean-core.js +135 -0
- package/bin/commands/clean.js +51 -0
- package/bin/commands/init-core.js +33 -225
- package/bin/commands/run-core.js +433 -289
- package/bin/commands/web-runner-source.js +14 -700
- package/bin/commands/web-session.js +1144 -0
- package/bin/index.js +391 -78
- package/bin/types.js +1 -0
- package/bin/util.js +16 -1
- package/bin/wipc.js +7 -2
- package/lib/build/index.d.ts +1 -0
- package/lib/build/index.js +1116 -0
- package/lib/build/web-runner/client.d.ts +1 -0
- package/lib/build/web-runner/client.js +167 -0
- package/lib/build/web-runner/html.d.ts +1 -0
- package/lib/build/web-runner/html.js +201 -0
- package/lib/build/web-runner/worker.d.ts +1 -0
- package/lib/build/web-runner/worker.js +271 -0
- package/lib/src/index.ts +1266 -0
- package/package.json +14 -6
- package/transform/lib/mock.js +50 -27
|
@@ -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
|
+
}
|
package/assembly/util/helpers.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
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 +=
|
|
65
|
-
rDiff +=
|
|
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 +=
|
|
73
|
-
rDiff +=
|
|
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 +=
|
|
83
|
+
rDiff += bgRed(left.charAt(i));
|
|
84
84
|
}
|
|
85
|
-
for (; i < right.length; i++) lDiff +=
|
|
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
|
-
}
|
package/assembly/util/json.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
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 =
|
|
39
|
+
const formatted = formatValue<T>(value);
|
|
40
40
|
if (formatted != "none") {
|
|
41
41
|
return quote(formatted);
|
|
42
42
|
}
|
package/assembly/util/wipc.ts
CHANGED
|
@@ -34,6 +34,8 @@ const HEADER_SIZE: i32 = 9;
|
|
|
34
34
|
const IOV_SIZE: usize = sizeof<usize>() * 2;
|
|
35
35
|
const U32_SIZE: usize = sizeof<u32>();
|
|
36
36
|
const REPORT_CHUNK_BYTES: i32 = 65536;
|
|
37
|
+
const WASI_ERRNO_AGAIN: i32 = 6;
|
|
38
|
+
const WASI_ERRNO_INTR: i32 = 27;
|
|
37
39
|
|
|
38
40
|
// @ts-ignore
|
|
39
41
|
const IS_BINDINGS: bool = isDefined(AS_TEST_BINDINGS);
|
|
@@ -303,9 +305,12 @@ function wasiWriteAll(data: ArrayBuffer): void {
|
|
|
303
305
|
store<usize>(iovPtr, <usize>left, sizeof<usize>());
|
|
304
306
|
store<u32>(writtenPtr, 0, 0);
|
|
305
307
|
const errno = wasi_fd_write(1, iovPtr, 1, writtenPtr);
|
|
308
|
+
if (errno == WASI_ERRNO_AGAIN || errno == WASI_ERRNO_INTR) {
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
306
311
|
if (errno != 0) return;
|
|
307
312
|
const written = <i32>load<u32>(writtenPtr, 0);
|
|
308
|
-
if (written <= 0)
|
|
313
|
+
if (written <= 0) continue;
|
|
309
314
|
offset += written;
|
|
310
315
|
}
|
|
311
316
|
}
|
|
@@ -319,10 +324,15 @@ function wasiRead(max: i32): ArrayBuffer {
|
|
|
319
324
|
|
|
320
325
|
store<usize>(iovPtr, changetype<usize>(out), 0);
|
|
321
326
|
store<usize>(iovPtr, <usize>max, sizeof<usize>());
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
327
|
+
while (true) {
|
|
328
|
+
store<u32>(readPtr, 0, 0);
|
|
329
|
+
const errno = wasi_fd_read(0, iovPtr, 1, readPtr);
|
|
330
|
+
if (errno == WASI_ERRNO_AGAIN || errno == WASI_ERRNO_INTR) {
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
if (errno != 0) return new ArrayBuffer(0);
|
|
334
|
+
break;
|
|
335
|
+
}
|
|
326
336
|
|
|
327
337
|
const size = <i32>load<u32>(readPtr, 0);
|
|
328
338
|
if (size <= 0) return new ArrayBuffer(0);
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { existsSync, rmSync } from "fs";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
import { applyMode, loadConfig } from "../util.js";
|
|
5
|
+
const DEFAULT_CONFIG_PATH = path.join(process.cwd(), "./as-test.config.json");
|
|
6
|
+
export async function clean(configPath = DEFAULT_CONFIG_PATH, modes = [undefined], fullClean = false) {
|
|
7
|
+
const loadedConfig = loadConfig(configPath, true);
|
|
8
|
+
const targets = new Map();
|
|
9
|
+
const ownership = buildOwnershipMap(loadedConfig);
|
|
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
|
+
}
|
|
32
|
+
}
|
|
33
|
+
let removed = 0;
|
|
34
|
+
for (const [targetPath, owners] of [...targets.entries()].sort((a, b) => a[0].localeCompare(b[0]))) {
|
|
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
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (!existsSync(targetPath)) {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
rmSync(targetPath, { recursive: true, force: true });
|
|
46
|
+
removed++;
|
|
47
|
+
process.stdout.write(`${chalk.bgGreenBright.black(" CLEAN ")} ${toRelativePath(targetPath)} ${chalk.dim(`(${owners.join(", ")})`)}\n`);
|
|
48
|
+
}
|
|
49
|
+
process.stdout.write(`${chalk.bold("Summary:")} removed ${removed} path(s)\n`);
|
|
50
|
+
}
|
|
51
|
+
function buildOwnershipMap(loadedConfig) {
|
|
52
|
+
const ownership = new Map();
|
|
53
|
+
const modeNames = [
|
|
54
|
+
undefined,
|
|
55
|
+
...Object.keys(loadedConfig.modes),
|
|
56
|
+
];
|
|
57
|
+
for (const modeName of modeNames) {
|
|
58
|
+
const active = applyMode(loadedConfig, modeName).config;
|
|
59
|
+
collectOwnership(ownership, active.outDir, modeName, "build");
|
|
60
|
+
collectOwnership(ownership, active.fuzz.crashDir, modeName, "crashes");
|
|
61
|
+
collectOwnership(ownership, active.coverageDir, modeName, "coverage");
|
|
62
|
+
collectOwnership(ownership, active.logs, modeName, "logs");
|
|
63
|
+
}
|
|
64
|
+
return ownership;
|
|
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
|
+
}
|
|
80
|
+
function collectOwnership(ownership, rawPath, modeName, kind) {
|
|
81
|
+
if (!rawPath || rawPath == "none")
|
|
82
|
+
return;
|
|
83
|
+
const resolved = path.resolve(process.cwd(), rawPath);
|
|
84
|
+
ensureSafeCleanPath(resolved, rawPath, kind);
|
|
85
|
+
const owner = `${modeName ?? "default"}:${kind}`;
|
|
86
|
+
const existing = ownership.get(resolved);
|
|
87
|
+
if (existing) {
|
|
88
|
+
if (!existing.includes(owner))
|
|
89
|
+
existing.push(owner);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
ownership.set(resolved, [owner]);
|
|
93
|
+
}
|
|
94
|
+
function collectTarget(targets, rawPath, modeName, kind) {
|
|
95
|
+
if (!rawPath || rawPath == "none")
|
|
96
|
+
return;
|
|
97
|
+
const resolved = path.resolve(process.cwd(), rawPath);
|
|
98
|
+
ensureSafeCleanPath(resolved, rawPath, kind);
|
|
99
|
+
const owner = `${modeName ?? "default"}:${kind}`;
|
|
100
|
+
const existing = targets.get(resolved);
|
|
101
|
+
if (existing) {
|
|
102
|
+
if (!existing.includes(owner))
|
|
103
|
+
existing.push(owner);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
targets.set(resolved, [owner]);
|
|
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
|
+
}
|
|
122
|
+
function ensureSafeCleanPath(resolvedPath, rawPath, kind) {
|
|
123
|
+
const cwd = path.resolve(process.cwd());
|
|
124
|
+
const relative = path.relative(cwd, resolvedPath);
|
|
125
|
+
if (!relative.length ||
|
|
126
|
+
relative == ".." ||
|
|
127
|
+
relative.startsWith(`..${path.sep}`) ||
|
|
128
|
+
path.parse(resolvedPath).root == resolvedPath) {
|
|
129
|
+
throw new Error(`refusing to clean unsafe ${kind} path "${rawPath}" (${resolvedPath})`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
function toRelativePath(targetPath) {
|
|
133
|
+
const relative = path.relative(process.cwd(), targetPath);
|
|
134
|
+
return relative.length ? relative : ".";
|
|
135
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { createInterface } from "readline";
|
|
3
|
+
import { clean } from "./clean-core.js";
|
|
4
|
+
import { loadConfig } from "../util.js";
|
|
5
|
+
export { clean } from "./clean-core.js";
|
|
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
|
+
});
|
|
51
|
+
}
|