as-test 1.0.1 → 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.
- package/CHANGELOG.md +112 -1
- package/README.md +138 -406
- package/as-test.config.schema.json +210 -17
- package/assembly/__fuzz__/array.fuzz.ts +10 -0
- package/assembly/__fuzz__/bytes.fuzz.ts +8 -0
- package/assembly/__fuzz__/math.fuzz.ts +9 -0
- package/assembly/__fuzz__/string.fuzz.ts +21 -0
- package/assembly/index.ts +141 -86
- package/assembly/src/expectation.ts +104 -19
- package/assembly/src/fuzz.ts +723 -0
- package/assembly/src/log.ts +6 -1
- package/assembly/src/suite.ts +45 -3
- package/assembly/util/json.ts +38 -4
- package/assembly/util/wipc.ts +35 -26
- package/bin/build-worker-pool.js +149 -0
- package/bin/build-worker.js +43 -0
- package/bin/commands/build-core.js +214 -28
- package/bin/commands/build.js +1 -0
- package/bin/commands/fuzz-core.js +306 -0
- package/bin/commands/fuzz.js +10 -0
- package/bin/commands/init-core.js +129 -24
- package/bin/commands/run-core.js +525 -123
- package/bin/commands/run.js +4 -1
- package/bin/commands/test.js +8 -3
- package/bin/commands/web-runner-source.js +634 -0
- package/bin/crash-store.js +64 -0
- package/bin/index.js +1484 -169
- package/bin/reporters/default.js +281 -49
- package/bin/reporters/tap.js +83 -2
- package/bin/types.js +19 -2
- package/bin/util.js +315 -33
- package/bin/wipc.js +79 -0
- package/package.json +19 -9
- package/transform/lib/coverage.js +1 -2
- package/transform/lib/index.js +3 -3
- package/transform/lib/log.js +1 -1
package/assembly/src/log.ts
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
import { quote } from "../util/json";
|
|
2
2
|
|
|
3
|
+
import { sendLog } from "../util/wipc";
|
|
4
|
+
|
|
3
5
|
export class Log {
|
|
4
6
|
public order: i32 = 0;
|
|
5
7
|
public depth: i32 = 0;
|
|
8
|
+
public file: string = "unknown";
|
|
6
9
|
public text: string;
|
|
7
10
|
constructor(text: string) {
|
|
8
11
|
this.text = text;
|
|
9
12
|
}
|
|
10
|
-
display(): void {
|
|
13
|
+
display(): void {
|
|
14
|
+
sendLog(this.file, this.depth, this.text);
|
|
15
|
+
}
|
|
11
16
|
|
|
12
17
|
serialize(): string {
|
|
13
18
|
return (
|
package/assembly/src/suite.ts
CHANGED
|
@@ -12,6 +12,7 @@ export class Suite {
|
|
|
12
12
|
public time: Time = new Time();
|
|
13
13
|
public description: string;
|
|
14
14
|
public depth: i32 = 0;
|
|
15
|
+
public snapshotCount: i32 = 0;
|
|
15
16
|
public suites: Suite[] = [];
|
|
16
17
|
public tests: Tests[] = [];
|
|
17
18
|
public logs: Log[] = [];
|
|
@@ -43,6 +44,7 @@ export class Suite {
|
|
|
43
44
|
log.order = this.order++;
|
|
44
45
|
this.logs.push(log);
|
|
45
46
|
log.depth = this.depth + 1;
|
|
47
|
+
log.file = this.file;
|
|
46
48
|
log.display();
|
|
47
49
|
}
|
|
48
50
|
|
|
@@ -54,12 +56,19 @@ export class Suite {
|
|
|
54
56
|
this.time.start = performance.now();
|
|
55
57
|
sendSuiteStart(this.file, this.depth, this.kind, this.description);
|
|
56
58
|
const isSkippedCase =
|
|
57
|
-
this.kind == "xdescribe" ||
|
|
59
|
+
this.kind == "xdescribe" ||
|
|
60
|
+
this.kind == "xtest" ||
|
|
61
|
+
this.kind == "xit" ||
|
|
62
|
+
this.kind == "xonly" ||
|
|
63
|
+
this.kind == "todo";
|
|
58
64
|
const isTestCase =
|
|
59
65
|
this.kind == "test" ||
|
|
60
66
|
this.kind == "it" ||
|
|
67
|
+
this.kind == "only" ||
|
|
61
68
|
this.kind == "xtest" ||
|
|
62
|
-
this.kind == "xit"
|
|
69
|
+
this.kind == "xit" ||
|
|
70
|
+
this.kind == "xonly" ||
|
|
71
|
+
this.kind == "todo";
|
|
63
72
|
|
|
64
73
|
if (isSkippedCase) {
|
|
65
74
|
this.time.end = performance.now();
|
|
@@ -85,12 +94,18 @@ export class Suite {
|
|
|
85
94
|
// @ts-ignore
|
|
86
95
|
depth--;
|
|
87
96
|
|
|
97
|
+
const hasOnlyChildren = this.hasOnlyChildren();
|
|
98
|
+
|
|
88
99
|
let hasFail = false;
|
|
89
100
|
let hasOk = false;
|
|
90
101
|
let hasSkip = false;
|
|
91
102
|
for (let i = 0; i < this.suites.length; i++) {
|
|
92
103
|
const suite = unchecked(this.suites[i]);
|
|
93
|
-
suite.
|
|
104
|
+
if (hasOnlyChildren && suite.kind != "only") {
|
|
105
|
+
suite.skip();
|
|
106
|
+
} else {
|
|
107
|
+
suite.run();
|
|
108
|
+
}
|
|
94
109
|
if (suite.verdict == "fail") {
|
|
95
110
|
hasFail = true;
|
|
96
111
|
} else if (suite.verdict == "ok") {
|
|
@@ -128,6 +143,33 @@ export class Suite {
|
|
|
128
143
|
);
|
|
129
144
|
}
|
|
130
145
|
|
|
146
|
+
skip(): void {
|
|
147
|
+
// @ts-ignore
|
|
148
|
+
current_suite = this;
|
|
149
|
+
// @ts-ignore
|
|
150
|
+
depth++;
|
|
151
|
+
this.time.start = performance.now();
|
|
152
|
+
this.time.end = this.time.start;
|
|
153
|
+
this.verdict = "skip";
|
|
154
|
+
sendSuiteStart(this.file, this.depth, this.kind, this.description);
|
|
155
|
+
// @ts-ignore
|
|
156
|
+
depth--;
|
|
157
|
+
sendSuiteEnd(
|
|
158
|
+
this.file,
|
|
159
|
+
this.depth,
|
|
160
|
+
this.kind,
|
|
161
|
+
this.description,
|
|
162
|
+
this.verdict,
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
private hasOnlyChildren(): bool {
|
|
167
|
+
for (let i = 0; i < this.suites.length; i++) {
|
|
168
|
+
if (unchecked(this.suites[i]).kind == "only") return true;
|
|
169
|
+
}
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
|
|
131
173
|
serialize(): string {
|
|
132
174
|
let out = "{";
|
|
133
175
|
if (this.depth <= 0) {
|
package/assembly/util/json.ts
CHANGED
|
@@ -31,6 +31,11 @@ export function stringifyValue<T>(value: T): string {
|
|
|
31
31
|
return stringifyArray<valueof<T>>(value as valueof<T>[]);
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
if (isManaged<T>()) {
|
|
35
|
+
// @ts-expect-error: method exists
|
|
36
|
+
return value.__as_test_json();
|
|
37
|
+
}
|
|
38
|
+
|
|
34
39
|
const formatted = stringify<T>(value);
|
|
35
40
|
if (formatted != "none") {
|
|
36
41
|
return quote(formatted);
|
|
@@ -39,6 +44,10 @@ export function stringifyValue<T>(value: T): string {
|
|
|
39
44
|
return quote(nameof<T>());
|
|
40
45
|
}
|
|
41
46
|
|
|
47
|
+
export function __as_test_json_value<T>(value: T): string {
|
|
48
|
+
return stringifyValue<T>(value);
|
|
49
|
+
}
|
|
50
|
+
|
|
42
51
|
function stringifyArray<T>(values: T[]): string {
|
|
43
52
|
if (!values.length) return "[]";
|
|
44
53
|
|
|
@@ -59,6 +68,10 @@ function escape(value: string): string {
|
|
|
59
68
|
out += '\\"';
|
|
60
69
|
} else if (ch == 92) {
|
|
61
70
|
out += "\\\\";
|
|
71
|
+
} else if (ch == 8) {
|
|
72
|
+
out += "\\b";
|
|
73
|
+
} else if (ch == 12) {
|
|
74
|
+
out += "\\f";
|
|
62
75
|
} else if (ch == 10) {
|
|
63
76
|
out += "\\n";
|
|
64
77
|
} else if (ch == 13) {
|
|
@@ -66,13 +79,34 @@ function escape(value: string): string {
|
|
|
66
79
|
} else if (ch == 9) {
|
|
67
80
|
out += "\\t";
|
|
68
81
|
} else if (ch < 32) {
|
|
69
|
-
out +=
|
|
70
|
-
|
|
71
|
-
if (
|
|
72
|
-
|
|
82
|
+
out += unicodeEscape(ch);
|
|
83
|
+
} else if (ch >= 0xd800 && ch <= 0xdfff) {
|
|
84
|
+
if (ch <= 0xdbff && i + 1 < value.length) {
|
|
85
|
+
const next = value.charCodeAt(i + 1);
|
|
86
|
+
if (next >= 0xdc00 && next <= 0xdfff) {
|
|
87
|
+
out += value.charAt(i);
|
|
88
|
+
out += value.charAt(i + 1);
|
|
89
|
+
i++;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
out += unicodeEscape(ch);
|
|
73
94
|
} else {
|
|
74
95
|
out += value.charAt(i);
|
|
75
96
|
}
|
|
76
97
|
}
|
|
77
98
|
return out;
|
|
78
99
|
}
|
|
100
|
+
|
|
101
|
+
function unicodeEscape(code: i32): string {
|
|
102
|
+
let out = "\\u";
|
|
103
|
+
out += hexNibble((code >> 12) & 0xf);
|
|
104
|
+
out += hexNibble((code >> 8) & 0xf);
|
|
105
|
+
out += hexNibble((code >> 4) & 0xf);
|
|
106
|
+
out += hexNibble(code & 0xf);
|
|
107
|
+
return out;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function hexNibble(value: i32): string {
|
|
111
|
+
return String.fromCharCode(value < 10 ? 48 + value : 87 + value);
|
|
112
|
+
}
|
package/assembly/util/wipc.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { quote as q } from "./json";
|
|
2
|
+
|
|
1
3
|
// @ts-ignore
|
|
2
4
|
@external("env", "process.stdout.write")
|
|
3
5
|
declare function process_stdout_write(data: ArrayBuffer): void;
|
|
@@ -47,6 +49,11 @@ export class SnapshotReply {
|
|
|
47
49
|
public expected: string = "";
|
|
48
50
|
}
|
|
49
51
|
|
|
52
|
+
export class FuzzConfigReply {
|
|
53
|
+
public runs: i32 = 1000;
|
|
54
|
+
public seed: u64 = 1337;
|
|
55
|
+
}
|
|
56
|
+
|
|
50
57
|
export function sendAssertionFailure(
|
|
51
58
|
key: string,
|
|
52
59
|
instr: string,
|
|
@@ -96,6 +103,13 @@ export function sendSuiteEnd(
|
|
|
96
103
|
);
|
|
97
104
|
}
|
|
98
105
|
|
|
106
|
+
export function sendLog(file: string, depth: i32, text: string): void {
|
|
107
|
+
sendJson(
|
|
108
|
+
MessageType.CALL,
|
|
109
|
+
`{"kind":"event:log","file":${q(file)},"depth":${depth.toString()},"text":${q(text)}}`,
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
99
113
|
export function snapshotAssert(key: string, actual: string): SnapshotReply {
|
|
100
114
|
sendJson(
|
|
101
115
|
MessageType.CALL,
|
|
@@ -117,12 +131,32 @@ export function snapshotAssert(key: string, actual: string): SnapshotReply {
|
|
|
117
131
|
return reply;
|
|
118
132
|
}
|
|
119
133
|
|
|
134
|
+
export function requestFuzzConfig(): FuzzConfigReply {
|
|
135
|
+
sendJson(MessageType.CALL, `{"kind":"fuzz:config"}`);
|
|
136
|
+
const response = readFrame();
|
|
137
|
+
if (response == null || response.type != MessageType.CALL) {
|
|
138
|
+
return new FuzzConfigReply();
|
|
139
|
+
}
|
|
140
|
+
const body = String.UTF8.decode(response.payload);
|
|
141
|
+
if (!body.length) {
|
|
142
|
+
return new FuzzConfigReply();
|
|
143
|
+
}
|
|
144
|
+
const sep = body.indexOf("\n");
|
|
145
|
+
if (sep < 0) return new FuzzConfigReply();
|
|
146
|
+
const reply = new FuzzConfigReply();
|
|
147
|
+
const runs = body.slice(0, sep);
|
|
148
|
+
const seed = body.slice(sep + 1);
|
|
149
|
+
if (runs.length) reply.runs = I32.parseInt(runs);
|
|
150
|
+
if (seed.length) reply.seed = U64.parseInt(seed);
|
|
151
|
+
return reply;
|
|
152
|
+
}
|
|
153
|
+
|
|
120
154
|
export function sendReport(report: string): void {
|
|
121
155
|
sendFrame(MessageType.DATA, String.UTF8.encode(report));
|
|
122
156
|
}
|
|
123
157
|
|
|
124
158
|
export function sendWarning(message: string): void {
|
|
125
|
-
|
|
159
|
+
sendJson(MessageType.CALL, `{"kind":"event:warn","message":${q(message)}}`);
|
|
126
160
|
}
|
|
127
161
|
|
|
128
162
|
function sendJson(type: MessageType, body: string): void {
|
|
@@ -259,28 +293,3 @@ function wasiRead(max: i32): ArrayBuffer {
|
|
|
259
293
|
memory.copy(changetype<usize>(partial), changetype<usize>(out), size);
|
|
260
294
|
return partial;
|
|
261
295
|
}
|
|
262
|
-
|
|
263
|
-
function q(value: string): string {
|
|
264
|
-
return '"' + escape(value) + '"';
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
function escape(value: string): string {
|
|
268
|
-
let out = "";
|
|
269
|
-
for (let i = 0; i < value.length; i++) {
|
|
270
|
-
const ch = value.charCodeAt(i);
|
|
271
|
-
if (ch == 34) {
|
|
272
|
-
out += '\\"';
|
|
273
|
-
} else if (ch == 92) {
|
|
274
|
-
out += "\\\\";
|
|
275
|
-
} else if (ch == 10) {
|
|
276
|
-
out += "\\n";
|
|
277
|
-
} else if (ch == 13) {
|
|
278
|
-
out += "\\r";
|
|
279
|
-
} else if (ch == 9) {
|
|
280
|
-
out += "\\t";
|
|
281
|
-
} else {
|
|
282
|
-
out += String.fromCharCode(ch);
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
return out;
|
|
286
|
-
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { spawn } from "child_process";
|
|
2
|
+
import { fileURLToPath } from "url";
|
|
3
|
+
export class BuildWorkerPool {
|
|
4
|
+
constructor(size) {
|
|
5
|
+
this.pools = new Map();
|
|
6
|
+
this.nextId = 1;
|
|
7
|
+
this.closed = false;
|
|
8
|
+
this.size = Math.max(1, size);
|
|
9
|
+
}
|
|
10
|
+
buildFileMode(args) {
|
|
11
|
+
if (this.closed) {
|
|
12
|
+
return Promise.reject(new Error("build worker pool is closed"));
|
|
13
|
+
}
|
|
14
|
+
const featureToggles = args.featureToggles ?? {};
|
|
15
|
+
const overrides = args.overrides ?? {};
|
|
16
|
+
const signature = buildSignature(args.modeName, featureToggles, overrides);
|
|
17
|
+
const pool = this.getPool(signature);
|
|
18
|
+
return new Promise((resolve, reject) => {
|
|
19
|
+
pool.queue.push({
|
|
20
|
+
id: this.nextId++,
|
|
21
|
+
configPath: args.configPath,
|
|
22
|
+
file: args.file,
|
|
23
|
+
modeName: args.modeName,
|
|
24
|
+
featureToggles,
|
|
25
|
+
overrides,
|
|
26
|
+
resolve,
|
|
27
|
+
reject,
|
|
28
|
+
});
|
|
29
|
+
this.pump(pool);
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
async close() {
|
|
33
|
+
this.closed = true;
|
|
34
|
+
const waits = [];
|
|
35
|
+
for (const pool of this.pools.values()) {
|
|
36
|
+
while (pool.queue.length) {
|
|
37
|
+
const task = pool.queue.shift();
|
|
38
|
+
task.reject(new Error("build worker pool closed"));
|
|
39
|
+
}
|
|
40
|
+
for (const worker of pool.workers) {
|
|
41
|
+
waits.push(new Promise((resolve) => {
|
|
42
|
+
if (worker.child.exitCode != null || worker.child.killed) {
|
|
43
|
+
resolve();
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
worker.child.once("exit", () => resolve());
|
|
47
|
+
worker.child.kill();
|
|
48
|
+
}));
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
await Promise.all(waits);
|
|
52
|
+
}
|
|
53
|
+
getPool(signature) {
|
|
54
|
+
let pool = this.pools.get(signature);
|
|
55
|
+
if (pool)
|
|
56
|
+
return pool;
|
|
57
|
+
pool = {
|
|
58
|
+
workers: Array.from({ length: this.size }, () => this.spawnWorker(signature)),
|
|
59
|
+
queue: [],
|
|
60
|
+
};
|
|
61
|
+
this.pools.set(signature, pool);
|
|
62
|
+
return pool;
|
|
63
|
+
}
|
|
64
|
+
spawnWorker(signature) {
|
|
65
|
+
const workerPath = fileURLToPath(new URL("./build-worker.js", import.meta.url));
|
|
66
|
+
const child = spawn(process.execPath, [workerPath], {
|
|
67
|
+
stdio: ["ignore", "ignore", "ignore", "ipc"],
|
|
68
|
+
});
|
|
69
|
+
const worker = {
|
|
70
|
+
child,
|
|
71
|
+
busy: false,
|
|
72
|
+
task: null,
|
|
73
|
+
};
|
|
74
|
+
child.on("message", (message) => {
|
|
75
|
+
this.onMessage(signature, worker, message);
|
|
76
|
+
});
|
|
77
|
+
child.on("exit", () => {
|
|
78
|
+
const pool = this.pools.get(signature);
|
|
79
|
+
const failedTask = worker.task;
|
|
80
|
+
worker.busy = false;
|
|
81
|
+
worker.task = null;
|
|
82
|
+
if (failedTask) {
|
|
83
|
+
failedTask.reject(new Error("build worker exited unexpectedly"));
|
|
84
|
+
}
|
|
85
|
+
if (!pool || this.closed)
|
|
86
|
+
return;
|
|
87
|
+
const index = pool.workers.indexOf(worker);
|
|
88
|
+
if (index >= 0) {
|
|
89
|
+
pool.workers[index] = this.spawnWorker(signature);
|
|
90
|
+
}
|
|
91
|
+
this.pump(pool);
|
|
92
|
+
});
|
|
93
|
+
return worker;
|
|
94
|
+
}
|
|
95
|
+
onMessage(signature, worker, message) {
|
|
96
|
+
const pool = this.pools.get(signature);
|
|
97
|
+
const task = worker.task;
|
|
98
|
+
if (!pool || !task || task.id !== message.id)
|
|
99
|
+
return;
|
|
100
|
+
worker.busy = false;
|
|
101
|
+
worker.task = null;
|
|
102
|
+
if (message.type == "done") {
|
|
103
|
+
task.resolve();
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
task.reject(deserializeError(message.error));
|
|
107
|
+
}
|
|
108
|
+
this.pump(pool);
|
|
109
|
+
}
|
|
110
|
+
pump(pool) {
|
|
111
|
+
for (const worker of pool.workers) {
|
|
112
|
+
if (worker.busy)
|
|
113
|
+
continue;
|
|
114
|
+
const task = pool.queue.shift();
|
|
115
|
+
if (!task)
|
|
116
|
+
return;
|
|
117
|
+
worker.busy = true;
|
|
118
|
+
worker.task = task;
|
|
119
|
+
worker.child.send({
|
|
120
|
+
type: "build-file",
|
|
121
|
+
id: task.id,
|
|
122
|
+
configPath: task.configPath,
|
|
123
|
+
file: task.file,
|
|
124
|
+
modeName: task.modeName,
|
|
125
|
+
featureToggles: task.featureToggles,
|
|
126
|
+
overrides: task.overrides,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
function buildSignature(modeName, featureToggles, overrides) {
|
|
132
|
+
return JSON.stringify({
|
|
133
|
+
modeName: modeName ?? "default",
|
|
134
|
+
featureToggles,
|
|
135
|
+
overrides,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
function deserializeError(payload) {
|
|
139
|
+
const error = new Error(typeof payload.message == "string" ? payload.message : "unknown error");
|
|
140
|
+
error.name = typeof payload.name == "string" ? payload.name : "Error";
|
|
141
|
+
if (typeof payload.stack == "string")
|
|
142
|
+
error.stack = payload.stack;
|
|
143
|
+
for (const [key, value] of Object.entries(payload)) {
|
|
144
|
+
if (key == "name" || key == "message" || key == "stack")
|
|
145
|
+
continue;
|
|
146
|
+
error[key] = value;
|
|
147
|
+
}
|
|
148
|
+
return error;
|
|
149
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { build } from "./commands/build-core.js";
|
|
2
|
+
process.env.AS_TEST_BUILD_API = "1";
|
|
3
|
+
process.on("message", async (message) => {
|
|
4
|
+
if (!message || message.type != "build-file")
|
|
5
|
+
return;
|
|
6
|
+
try {
|
|
7
|
+
await build(message.configPath, [message.file], message.modeName, message.featureToggles, message.overrides);
|
|
8
|
+
send({
|
|
9
|
+
type: "done",
|
|
10
|
+
id: message.id,
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
catch (error) {
|
|
14
|
+
send({
|
|
15
|
+
type: "error",
|
|
16
|
+
id: message.id,
|
|
17
|
+
error: serializeError(error),
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
function send(message) {
|
|
22
|
+
if (!process.send)
|
|
23
|
+
return;
|
|
24
|
+
process.send(message);
|
|
25
|
+
}
|
|
26
|
+
function serializeError(error) {
|
|
27
|
+
if (!(error instanceof Error)) {
|
|
28
|
+
return {
|
|
29
|
+
name: "Error",
|
|
30
|
+
message: typeof error == "string" ? error : "unknown error",
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
const out = {
|
|
34
|
+
name: error.name,
|
|
35
|
+
message: error.message,
|
|
36
|
+
stack: error.stack,
|
|
37
|
+
};
|
|
38
|
+
const errorRecord = error;
|
|
39
|
+
for (const key of Object.keys(error)) {
|
|
40
|
+
out[key] = errorRecord[key];
|
|
41
|
+
}
|
|
42
|
+
return out;
|
|
43
|
+
}
|