as-test 1.1.6 → 1.1.7
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 +13 -0
- package/README.md +4 -9
- package/assembly/index.ts +10 -15
- package/assembly/src/expectation.ts +11 -11
- package/assembly/src/fuzz.ts +11 -7
- package/assembly/src/log.ts +2 -2
- package/assembly/src/suite.ts +5 -5
- package/assembly/src/tests.ts +8 -8
- package/assembly/util/wipc.ts +5 -1
- package/bin/build-worker-pool.js +146 -142
- package/bin/build-worker.js +37 -34
- package/bin/commands/build-core.js +577 -465
- package/bin/commands/build.js +49 -29
- package/bin/commands/clean-core.js +120 -113
- package/bin/commands/clean.js +14 -8
- package/bin/commands/doctor-core.js +288 -289
- package/bin/commands/doctor.js +1 -1
- package/bin/commands/fuzz-core.js +467 -414
- package/bin/commands/fuzz.js +27 -10
- package/bin/commands/init-core.js +905 -794
- package/bin/commands/init.js +2 -2
- package/bin/commands/run-core.js +2675 -2344
- package/bin/commands/run.js +43 -25
- package/bin/commands/test.js +56 -32
- package/bin/commands/web-runner-source.js +1 -1
- package/bin/commands/web-session.js +516 -525
- package/bin/coverage-points.js +363 -341
- package/bin/crash-store.js +56 -66
- package/bin/index.js +4092 -3150
- package/bin/reporters/default.js +1090 -890
- package/bin/reporters/tap.js +319 -325
- package/bin/types.js +67 -67
- package/bin/util.js +1290 -1239
- package/bin/wipc.js +70 -73
- package/lib/build/index.d.ts +3 -1
- package/lib/build/index.js +1039 -1034
- package/lib/build/web-runner/client.js +1 -1
- package/lib/build/web-runner/html.js +1 -1
- package/lib/build/web-runner/worker.js +1 -1
- package/package.json +6 -3
- package/transform/lib/log.js +9 -5
- package/assembly/util/json.ts +0 -112
package/bin/util.js
CHANGED
|
@@ -1,176 +1,213 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "fs";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
BuildOptions,
|
|
4
|
+
Config,
|
|
5
|
+
CoverageOptions,
|
|
6
|
+
CoverageIgnoreOptions,
|
|
7
|
+
FuzzConfig,
|
|
8
|
+
ModeConfig,
|
|
9
|
+
ReporterConfig,
|
|
10
|
+
RunOptions,
|
|
11
|
+
Runtime,
|
|
12
|
+
} from "./types.js";
|
|
3
13
|
import chalk from "chalk";
|
|
4
14
|
import { createRequire } from "module";
|
|
5
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
basename,
|
|
17
|
+
delimiter,
|
|
18
|
+
dirname,
|
|
19
|
+
join,
|
|
20
|
+
relative,
|
|
21
|
+
resolve,
|
|
22
|
+
sep,
|
|
23
|
+
} from "path";
|
|
6
24
|
import { fileURLToPath } from "url";
|
|
7
25
|
const CONFIG_META = new WeakMap();
|
|
8
26
|
export function formatTime(ms) {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
if (ms < 0) {
|
|
28
|
+
throw new Error("Time should be a non-negative number.");
|
|
29
|
+
}
|
|
30
|
+
// Convert milliseconds to microseconds
|
|
31
|
+
const us = ms * 1000;
|
|
32
|
+
const units = [
|
|
33
|
+
{ name: "μs", divisor: 1 },
|
|
34
|
+
{ name: "ms", divisor: 1000 },
|
|
35
|
+
{ name: "s", divisor: 1000 * 1000 },
|
|
36
|
+
{ name: "m", divisor: 60 * 1000 * 1000 },
|
|
37
|
+
{ name: "h", divisor: 60 * 60 * 1000 * 1000 },
|
|
38
|
+
{ name: "d", divisor: 24 * 60 * 60 * 1000 * 1000 },
|
|
39
|
+
];
|
|
40
|
+
for (let i = units.length - 1; i >= 0; i--) {
|
|
41
|
+
const unit = units[i];
|
|
42
|
+
if (us >= unit.divisor) {
|
|
43
|
+
const value = Math.round((us / unit.divisor) * 1000) / 1000;
|
|
44
|
+
return `${value}${unit.name}`;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return `${us}us`;
|
|
30
48
|
}
|
|
31
49
|
export function formatSpecDisplayPath(file) {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
50
|
+
const resolved = resolve(file);
|
|
51
|
+
const parts = resolved.split(/[/\\]+/);
|
|
52
|
+
const testsIndex = parts.lastIndexOf("__tests__");
|
|
53
|
+
if (testsIndex >= 0 && testsIndex < parts.length - 1) {
|
|
54
|
+
return parts.slice(testsIndex + 1).join("/");
|
|
55
|
+
}
|
|
56
|
+
const rel = relative(process.cwd(), resolved).split(sep).join("/");
|
|
57
|
+
if (rel.length && rel != "." && rel != ".." && !rel.startsWith("../")) {
|
|
58
|
+
return rel;
|
|
59
|
+
}
|
|
60
|
+
return basename(resolved);
|
|
43
61
|
}
|
|
44
62
|
export function loadConfig(CONFIG_PATH, warn = false) {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
63
|
+
const resolvedPath = resolve(CONFIG_PATH);
|
|
64
|
+
const raw = readConfigRaw(resolvedPath, warn);
|
|
65
|
+
return parseConfigRaw(raw, resolvedPath);
|
|
48
66
|
}
|
|
49
67
|
function readConfigRaw(configPath, warn) {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
68
|
+
if (!existsSync(configPath)) {
|
|
69
|
+
if (warn) {
|
|
70
|
+
console.log(
|
|
71
|
+
`${chalk.bgMagentaBright(" WARN ")}${chalk.dim(":")} Could not locate config file in the current directory! Continuing with default config.`,
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
return {};
|
|
75
|
+
}
|
|
76
|
+
const rawText = readFileSync(configPath, "utf8");
|
|
77
|
+
let parsed;
|
|
78
|
+
try {
|
|
79
|
+
parsed = JSON.parse(rawText);
|
|
80
|
+
} catch (error) {
|
|
81
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
82
|
+
throw new Error(
|
|
83
|
+
`invalid config JSON at ${configPath}\n${message}\nfix JSON syntax and rerun.`,
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
if (!parsed || typeof parsed != "object" || Array.isArray(parsed)) {
|
|
87
|
+
throw new Error(
|
|
88
|
+
`invalid config at ${configPath}\nroot value must be an object. Example: { "input": ["./assembly/__tests__/*.spec.ts"] }`,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
const raw = parsed;
|
|
92
|
+
validateConfig(raw, configPath);
|
|
93
|
+
return raw;
|
|
71
94
|
}
|
|
72
95
|
function parseConfigRaw(raw, configPath) {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
config.buildOptions.
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
config.fuzz.
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
96
|
+
const configDir = dirname(configPath);
|
|
97
|
+
const config = Object.assign(new Config(), raw);
|
|
98
|
+
applyOutputConfig(raw.output, raw, config);
|
|
99
|
+
config.env = parseEnvValue(raw.env, configDir, "$.env");
|
|
100
|
+
const runOptionsRaw = raw.runOptions ?? {};
|
|
101
|
+
config.buildOptions = Object.assign(
|
|
102
|
+
new BuildOptions(),
|
|
103
|
+
raw.buildOptions ?? {},
|
|
104
|
+
);
|
|
105
|
+
config.buildOptions.cmd =
|
|
106
|
+
typeof config.buildOptions.cmd == "string" ? config.buildOptions.cmd : "";
|
|
107
|
+
config.buildOptions.args = Array.isArray(config.buildOptions.args)
|
|
108
|
+
? config.buildOptions.args.filter((item) => typeof item == "string")
|
|
109
|
+
: [];
|
|
110
|
+
config.buildOptions.env = parseEnvValue(
|
|
111
|
+
raw.buildOptions?.env,
|
|
112
|
+
configDir,
|
|
113
|
+
"$.buildOptions.env",
|
|
114
|
+
);
|
|
115
|
+
config.buildOptions.target =
|
|
116
|
+
typeof config.buildOptions.target == "string" &&
|
|
117
|
+
config.buildOptions.target.length
|
|
118
|
+
? config.buildOptions.target
|
|
119
|
+
: "wasi";
|
|
120
|
+
config.runOptions = Object.assign(new RunOptions(), runOptionsRaw);
|
|
121
|
+
const reporterRaw = runOptionsRaw.reporter;
|
|
122
|
+
if (typeof reporterRaw == "string") {
|
|
123
|
+
config.runOptions.reporter = reporterRaw;
|
|
124
|
+
} else if (reporterRaw && typeof reporterRaw == "object") {
|
|
125
|
+
const reporterConfig = Object.assign(new ReporterConfig(), reporterRaw);
|
|
126
|
+
reporterConfig.name =
|
|
127
|
+
typeof reporterConfig.name == "string" ? reporterConfig.name : "";
|
|
128
|
+
reporterConfig.options = Array.isArray(reporterConfig.options)
|
|
129
|
+
? reporterConfig.options.filter((value) => typeof value == "string")
|
|
130
|
+
: [];
|
|
131
|
+
reporterConfig.outDir =
|
|
132
|
+
typeof reporterConfig.outDir == "string" ? reporterConfig.outDir : "";
|
|
133
|
+
reporterConfig.outFile =
|
|
134
|
+
typeof reporterConfig.outFile == "string" ? reporterConfig.outFile : "";
|
|
135
|
+
config.runOptions.reporter = reporterConfig;
|
|
136
|
+
} else {
|
|
137
|
+
config.runOptions.reporter = "";
|
|
138
|
+
}
|
|
139
|
+
const runtimeRaw = runOptionsRaw.runtime;
|
|
140
|
+
const runtime = new Runtime();
|
|
141
|
+
const legacyRun =
|
|
142
|
+
typeof runOptionsRaw.run == "string" && runOptionsRaw.run.length
|
|
143
|
+
? runOptionsRaw.run
|
|
144
|
+
: "";
|
|
145
|
+
const cmd =
|
|
146
|
+
runtimeRaw && typeof runtimeRaw.cmd == "string" && runtimeRaw.cmd.length
|
|
147
|
+
? runtimeRaw.cmd
|
|
148
|
+
: runtimeRaw && typeof runtimeRaw.run == "string" && runtimeRaw.run.length
|
|
149
|
+
? runtimeRaw.run
|
|
150
|
+
: legacyRun
|
|
151
|
+
? legacyRun
|
|
152
|
+
: runtime.cmd;
|
|
153
|
+
runtime.cmd = cmd;
|
|
154
|
+
runtime.browser =
|
|
155
|
+
runtimeRaw && typeof runtimeRaw.browser == "string"
|
|
156
|
+
? runtimeRaw.browser
|
|
157
|
+
: "";
|
|
158
|
+
config.runOptions.runtime = runtime;
|
|
159
|
+
config.runOptions.env = parseEnvValue(
|
|
160
|
+
runOptionsRaw.env,
|
|
161
|
+
configDir,
|
|
162
|
+
"$.runOptions.env",
|
|
163
|
+
);
|
|
164
|
+
const fuzzRaw = raw.fuzz ?? {};
|
|
165
|
+
config.fuzz = Object.assign(new FuzzConfig(), fuzzRaw);
|
|
166
|
+
config.fuzz.input = Array.isArray(config.fuzz.input)
|
|
167
|
+
? config.fuzz.input.filter((item) => typeof item == "string")
|
|
168
|
+
: typeof fuzzRaw.input == "string"
|
|
169
|
+
? [fuzzRaw.input]
|
|
170
|
+
: new FuzzConfig().input;
|
|
171
|
+
config.fuzz.runs = normalizePositiveNumber(config.fuzz.runs, 1000);
|
|
172
|
+
config.fuzz.seed = normalizeNonNegativeNumber(config.fuzz.seed, -1);
|
|
173
|
+
config.fuzz.maxInputBytes = normalizePositiveNumber(
|
|
174
|
+
config.fuzz.maxInputBytes,
|
|
175
|
+
4096,
|
|
176
|
+
);
|
|
177
|
+
config.fuzz.target =
|
|
178
|
+
typeof config.fuzz.target == "string" && config.fuzz.target.length
|
|
179
|
+
? config.fuzz.target
|
|
180
|
+
: "bindings";
|
|
181
|
+
config.fuzz.corpusDir =
|
|
182
|
+
typeof config.fuzz.corpusDir == "string" && config.fuzz.corpusDir.length
|
|
183
|
+
? config.fuzz.corpusDir
|
|
184
|
+
: "./.as-test/fuzz/corpus";
|
|
185
|
+
config.fuzz.crashDir =
|
|
186
|
+
typeof config.fuzz.crashDir == "string" && config.fuzz.crashDir.length
|
|
187
|
+
? config.fuzz.crashDir
|
|
188
|
+
: "./.as-test/crashes";
|
|
189
|
+
config.modes = parseModes(raw.modes, configDir);
|
|
190
|
+
CONFIG_META.set(config, {
|
|
191
|
+
sourcePath: configPath,
|
|
192
|
+
raw,
|
|
193
|
+
});
|
|
194
|
+
return config;
|
|
158
195
|
}
|
|
159
196
|
const TOP_LEVEL_KEYS = new Set([
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
197
|
+
"$schema",
|
|
198
|
+
"input",
|
|
199
|
+
"output",
|
|
200
|
+
"outDir",
|
|
201
|
+
"logs",
|
|
202
|
+
"coverageDir",
|
|
203
|
+
"snapshotDir",
|
|
204
|
+
"config",
|
|
205
|
+
"coverage",
|
|
206
|
+
"env",
|
|
207
|
+
"buildOptions",
|
|
208
|
+
"fuzz",
|
|
209
|
+
"modes",
|
|
210
|
+
"runOptions",
|
|
174
211
|
]);
|
|
175
212
|
const BUILD_OPTION_KEYS = new Set(["cmd", "args", "target", "env"]);
|
|
176
213
|
const RUN_OPTION_KEYS = new Set(["runtime", "reporter", "run", "env"]); // includes legacy "run"
|
|
@@ -178,1223 +215,1237 @@ const RUNTIME_OPTION_KEYS = new Set(["cmd", "run", "browser"]); // includes lega
|
|
|
178
215
|
const REPORTER_OPTION_KEYS = new Set(["name", "options", "outDir", "outFile"]);
|
|
179
216
|
const OUTPUT_OPTION_KEYS = new Set(["build", "logs", "coverage", "snapshots"]);
|
|
180
217
|
const FUZZ_OPTION_KEYS = new Set([
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
218
|
+
"input",
|
|
219
|
+
"runs",
|
|
220
|
+
"seed",
|
|
221
|
+
"maxInputBytes",
|
|
222
|
+
"target",
|
|
223
|
+
"corpusDir",
|
|
224
|
+
"crashDir",
|
|
188
225
|
]);
|
|
189
|
-
const MODE_KEYS = new Set(
|
|
226
|
+
const MODE_KEYS = new Set(
|
|
227
|
+
[...TOP_LEVEL_KEYS, "default"].filter((key) => key != "modes"),
|
|
228
|
+
);
|
|
190
229
|
function validateConfig(raw, configPath) {
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
const
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
230
|
+
const issues = [];
|
|
231
|
+
validateUnknownKeys(raw, TOP_LEVEL_KEYS, "$", issues);
|
|
232
|
+
validateStringField(raw, "$schema", "$", issues);
|
|
233
|
+
validateInputField(raw, "input", "$", issues);
|
|
234
|
+
validateOutputField(raw, "output", "$", issues);
|
|
235
|
+
validateStringField(raw, "outDir", "$", issues);
|
|
236
|
+
validateStringField(raw, "logs", "$", issues);
|
|
237
|
+
validateStringField(raw, "coverageDir", "$", issues);
|
|
238
|
+
validateStringField(raw, "snapshotDir", "$", issues);
|
|
239
|
+
validateStringField(raw, "config", "$", issues);
|
|
240
|
+
validateCoverageField(raw, "coverage", "$", issues);
|
|
241
|
+
validateEnvField(raw, "env", "$", issues);
|
|
242
|
+
validateBuildOptionsField(raw, "buildOptions", "$", issues);
|
|
243
|
+
validateFuzzField(raw, "fuzz", "$", issues);
|
|
244
|
+
validateRunOptionsField(raw, "runOptions", "$", issues);
|
|
245
|
+
validateModesField(raw, "modes", "$", issues);
|
|
246
|
+
if (!issues.length) return;
|
|
247
|
+
const lines = issues.map((issue, index) => {
|
|
248
|
+
const suffix = issue.fix ? `\n fix: ${issue.fix}` : "";
|
|
249
|
+
return `${index + 1}. ${issue.path}: ${issue.message}${suffix}`;
|
|
250
|
+
});
|
|
251
|
+
throw new Error(
|
|
252
|
+
`invalid config at ${configPath}\n${lines.join("\n")}\nrun "ast doctor" to check your setup.`,
|
|
253
|
+
);
|
|
214
254
|
}
|
|
215
255
|
function validateUnknownKeys(raw, allowed, pathPrefix, issues) {
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
}
|
|
256
|
+
for (const key of Object.keys(raw)) {
|
|
257
|
+
if (allowed.has(key)) continue;
|
|
258
|
+
const suggestion = resolveClosestKey(key, [...allowed]);
|
|
259
|
+
issues.push({
|
|
260
|
+
path: `${pathPrefix}.${key}`,
|
|
261
|
+
message: "unknown property",
|
|
262
|
+
fix: suggestion
|
|
263
|
+
? `use "${suggestion}" if that was intended, otherwise remove this property`
|
|
264
|
+
: `remove this property`,
|
|
265
|
+
});
|
|
266
|
+
}
|
|
228
267
|
}
|
|
229
268
|
function validateInputField(raw, key, pathPrefix, issues) {
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
if (
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
});
|
|
259
|
-
}
|
|
269
|
+
if (!(key in raw) || raw[key] == undefined) return;
|
|
270
|
+
const value = raw[key];
|
|
271
|
+
if (typeof value == "string") {
|
|
272
|
+
if (!value.length) {
|
|
273
|
+
issues.push({
|
|
274
|
+
path: `${pathPrefix}.${key}`,
|
|
275
|
+
message: "must not be an empty string",
|
|
276
|
+
fix: "set to a glob pattern or remove it to use the default input patterns",
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
if (!Array.isArray(value)) {
|
|
282
|
+
issues.push({
|
|
283
|
+
path: `${pathPrefix}.${key}`,
|
|
284
|
+
message: "must be a string or array of strings",
|
|
285
|
+
fix: 'example: "input": ["./assembly/__tests__/*.spec.ts"]',
|
|
286
|
+
});
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
for (let i = 0; i < value.length; i++) {
|
|
290
|
+
if (typeof value[i] == "string" && value[i].length) continue;
|
|
291
|
+
issues.push({
|
|
292
|
+
path: `${pathPrefix}.${key}[${i}]`,
|
|
293
|
+
message: "must be a non-empty string",
|
|
294
|
+
fix: "remove invalid entries or replace them with valid glob strings",
|
|
295
|
+
});
|
|
296
|
+
}
|
|
260
297
|
}
|
|
261
298
|
function validateStringField(raw, key, pathPrefix, issues) {
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
}
|
|
299
|
+
if (!(key in raw) || raw[key] == undefined) return;
|
|
300
|
+
if (typeof raw[key] != "string") {
|
|
301
|
+
issues.push({
|
|
302
|
+
path: `${pathPrefix}.${key}`,
|
|
303
|
+
message: "must be a string",
|
|
304
|
+
fix: `set "${key}" to a string value`,
|
|
305
|
+
});
|
|
306
|
+
}
|
|
271
307
|
}
|
|
272
308
|
function validateOutputField(raw, key, pathPrefix, issues) {
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
if (
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
309
|
+
if (!(key in raw) || raw[key] == undefined) return;
|
|
310
|
+
const value = raw[key];
|
|
311
|
+
if (typeof value == "string") {
|
|
312
|
+
if (!value.length) {
|
|
313
|
+
issues.push({
|
|
314
|
+
path: `${pathPrefix}.${key}`,
|
|
315
|
+
message: "must not be an empty string",
|
|
316
|
+
fix: 'example: "output": "./.as-test/"',
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
if (!value || typeof value != "object" || Array.isArray(value)) {
|
|
322
|
+
issues.push({
|
|
323
|
+
path: `${pathPrefix}.${key}`,
|
|
324
|
+
message: "must be a string or object",
|
|
325
|
+
fix: 'example: "output": { "logs": "./.as-test/logs", "coverage": "./.as-test/coverage" }',
|
|
326
|
+
});
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
const out = value;
|
|
330
|
+
validateUnknownKeys(out, OUTPUT_OPTION_KEYS, `${pathPrefix}.${key}`, issues);
|
|
331
|
+
if ("build" in out && (typeof out.build != "string" || !out.build.length)) {
|
|
332
|
+
issues.push({
|
|
333
|
+
path: `${pathPrefix}.${key}.build`,
|
|
334
|
+
message: "must be a non-empty string",
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
if (
|
|
338
|
+
"snapshots" in out &&
|
|
339
|
+
(typeof out.snapshots != "string" || !out.snapshots.length)
|
|
340
|
+
) {
|
|
341
|
+
issues.push({
|
|
342
|
+
path: `${pathPrefix}.${key}.snapshots`,
|
|
343
|
+
message: "must be a non-empty string",
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
if ("logs" in out && (typeof out.logs != "string" || !out.logs.length)) {
|
|
347
|
+
issues.push({
|
|
348
|
+
path: `${pathPrefix}.${key}.logs`,
|
|
349
|
+
message: 'must be a non-empty string or "none"',
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
if (
|
|
353
|
+
"coverage" in out &&
|
|
354
|
+
(typeof out.coverage != "string" || !out.coverage.length)
|
|
355
|
+
) {
|
|
356
|
+
issues.push({
|
|
357
|
+
path: `${pathPrefix}.${key}.coverage`,
|
|
358
|
+
message: 'must be a non-empty string or "none"',
|
|
359
|
+
});
|
|
360
|
+
}
|
|
322
361
|
}
|
|
323
362
|
function validateCoverageField(raw, key, pathPrefix, issues) {
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
validateCoverageValue(raw[key], `${pathPrefix}.${key}`, issues);
|
|
363
|
+
if (!(key in raw) || raw[key] == undefined) return;
|
|
364
|
+
validateCoverageValue(raw[key], `${pathPrefix}.${key}`, issues);
|
|
327
365
|
}
|
|
328
366
|
function validateCoverageValue(value, path, issues) {
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
if (
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
}
|
|
367
|
+
if (typeof value == "boolean") return;
|
|
368
|
+
if (!value || typeof value != "object" || Array.isArray(value)) {
|
|
369
|
+
issues.push({
|
|
370
|
+
path,
|
|
371
|
+
message: "must be a boolean or object",
|
|
372
|
+
fix: 'use true/false or { "enabled": true, "mode": "project", "includeSpecs": false, "dependencies": ["json-as"], "include": ["assembly/**/*.ts"], "exclude": ["assembly/__tests__/**/*.spec.ts"] }',
|
|
373
|
+
});
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
const obj = value;
|
|
377
|
+
if ("enabled" in obj && typeof obj.enabled != "boolean") {
|
|
378
|
+
issues.push({
|
|
379
|
+
path: `${path}.enabled`,
|
|
380
|
+
message: "must be a boolean",
|
|
381
|
+
fix: "set to true or false",
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
if ("mode" in obj && obj.mode != undefined) {
|
|
385
|
+
if (typeof obj.mode != "string") {
|
|
386
|
+
issues.push({
|
|
387
|
+
path: `${path}.mode`,
|
|
388
|
+
message: 'must be "project" or "all"',
|
|
389
|
+
fix: 'set "mode" to "project" or "all"',
|
|
390
|
+
});
|
|
391
|
+
} else if (obj.mode != "project" && obj.mode != "all") {
|
|
392
|
+
issues.push({
|
|
393
|
+
path: `${path}.mode`,
|
|
394
|
+
message: 'must be "project" or "all"',
|
|
395
|
+
fix: 'set "mode" to "project" or "all"',
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
if ("includeSpecs" in obj && typeof obj.includeSpecs != "boolean") {
|
|
400
|
+
issues.push({
|
|
401
|
+
path: `${path}.includeSpecs`,
|
|
402
|
+
message: "must be a boolean",
|
|
403
|
+
fix: "set to true or false",
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
validateStringArrayField(obj, "dependencies", path, issues);
|
|
407
|
+
validateStringArrayField(obj, "include", path, issues);
|
|
408
|
+
validateStringArrayField(obj, "exclude", path, issues);
|
|
409
|
+
if ("ignore" in obj && obj.ignore != undefined) {
|
|
410
|
+
if (
|
|
411
|
+
!obj.ignore ||
|
|
412
|
+
typeof obj.ignore != "object" ||
|
|
413
|
+
Array.isArray(obj.ignore)
|
|
414
|
+
) {
|
|
415
|
+
issues.push({
|
|
416
|
+
path: `${path}.ignore`,
|
|
417
|
+
message: "must be an object",
|
|
418
|
+
fix: 'set "ignore" to an object such as { "labels": ["Call"], "names": ["panic"] }',
|
|
419
|
+
});
|
|
420
|
+
} else {
|
|
421
|
+
const ignore = obj.ignore;
|
|
422
|
+
validateStringArrayField(ignore, "labels", `${path}.ignore`, issues);
|
|
423
|
+
validateStringArrayField(ignore, "names", `${path}.ignore`, issues);
|
|
424
|
+
validateStringArrayField(ignore, "locations", `${path}.ignore`, issues);
|
|
425
|
+
validateStringArrayField(ignore, "snippets", `${path}.ignore`, issues);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
391
428
|
}
|
|
392
429
|
function validateStringArrayField(raw, key, pathPrefix, issues) {
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
});
|
|
412
|
-
}
|
|
430
|
+
if (!(key in raw) || raw[key] == undefined) return;
|
|
431
|
+
const value = raw[key];
|
|
432
|
+
if (!Array.isArray(value)) {
|
|
433
|
+
issues.push({
|
|
434
|
+
path: `${pathPrefix}.${key}`,
|
|
435
|
+
message: "must be an array of strings",
|
|
436
|
+
fix: `set "${key}" to an array of glob patterns`,
|
|
437
|
+
});
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
for (let i = 0; i < value.length; i++) {
|
|
441
|
+
if (typeof value[i] == "string" && value[i].length) continue;
|
|
442
|
+
issues.push({
|
|
443
|
+
path: `${pathPrefix}.${key}[${i}]`,
|
|
444
|
+
message: "must be a non-empty string",
|
|
445
|
+
fix: "remove invalid entries or replace them with valid glob strings",
|
|
446
|
+
});
|
|
447
|
+
}
|
|
413
448
|
}
|
|
414
449
|
function validateEnvField(raw, key, pathPrefix, issues) {
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
if (
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
if (typeof item != "string" || !item.length) {
|
|
432
|
-
issues.push({
|
|
433
|
-
path: `${pathPrefix}.${key}[${i}]`,
|
|
434
|
-
message: "must be a non-empty string",
|
|
435
|
-
fix: 'use entries like "FOO=1"',
|
|
436
|
-
});
|
|
437
|
-
continue;
|
|
438
|
-
}
|
|
439
|
-
const separator = item.indexOf("=");
|
|
440
|
-
if (separator <= 0) {
|
|
441
|
-
issues.push({
|
|
442
|
-
path: `${pathPrefix}.${key}[${i}]`,
|
|
443
|
-
message: 'must use "KEY=value" format',
|
|
444
|
-
fix: 'example: "FOO=1"',
|
|
445
|
-
});
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
return;
|
|
449
|
-
}
|
|
450
|
-
if (!value || typeof value != "object") {
|
|
450
|
+
if (!(key in raw) || raw[key] == undefined) return;
|
|
451
|
+
const value = raw[key];
|
|
452
|
+
if (typeof value == "string") {
|
|
453
|
+
if (!value.length) {
|
|
454
|
+
issues.push({
|
|
455
|
+
path: `${pathPrefix}.${key}`,
|
|
456
|
+
message: "must not be an empty string",
|
|
457
|
+
fix: 'use a .env file path like "./secrets/.env"',
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
if (Array.isArray(value)) {
|
|
463
|
+
for (let i = 0; i < value.length; i++) {
|
|
464
|
+
const item = value[i];
|
|
465
|
+
if (typeof item != "string" || !item.length) {
|
|
451
466
|
issues.push({
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
467
|
+
path: `${pathPrefix}.${key}[${i}]`,
|
|
468
|
+
message: "must be a non-empty string",
|
|
469
|
+
fix: 'use entries like "FOO=1"',
|
|
455
470
|
});
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
continue;
|
|
471
|
+
continue;
|
|
472
|
+
}
|
|
473
|
+
const separator = item.indexOf("=");
|
|
474
|
+
if (separator <= 0) {
|
|
461
475
|
issues.push({
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
476
|
+
path: `${pathPrefix}.${key}[${i}]`,
|
|
477
|
+
message: 'must use "KEY=value" format',
|
|
478
|
+
fix: 'example: "FOO=1"',
|
|
465
479
|
});
|
|
466
|
-
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
if (!value || typeof value != "object") {
|
|
485
|
+
issues.push({
|
|
486
|
+
path: `${pathPrefix}.${key}`,
|
|
487
|
+
message:
|
|
488
|
+
"must be a .env file path, array of KEY=value strings, or object of string values",
|
|
489
|
+
fix: 'example: "env": "./secrets/.env" or ["MY_FLAG=1"] or { "MY_FLAG": "1" }',
|
|
490
|
+
});
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
for (const [name, item] of Object.entries(value)) {
|
|
494
|
+
if (typeof item == "string") continue;
|
|
495
|
+
issues.push({
|
|
496
|
+
path: `${pathPrefix}.${key}.${name}`,
|
|
497
|
+
message: "must be a string",
|
|
498
|
+
fix: "set environment values as strings",
|
|
499
|
+
});
|
|
500
|
+
}
|
|
467
501
|
}
|
|
468
502
|
function validateBuildOptionsField(raw, key, pathPrefix, issues) {
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
if (
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
503
|
+
if (!(key in raw) || raw[key] == undefined) return;
|
|
504
|
+
const value = raw[key];
|
|
505
|
+
if (!value || typeof value != "object" || Array.isArray(value)) {
|
|
506
|
+
issues.push({
|
|
507
|
+
path: `${pathPrefix}.${key}`,
|
|
508
|
+
message: "must be an object",
|
|
509
|
+
fix: 'example: "buildOptions": { "cmd": "", "args": [], "target": "wasi" }',
|
|
510
|
+
});
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
const obj = value;
|
|
514
|
+
validateUnknownKeys(obj, BUILD_OPTION_KEYS, `${pathPrefix}.${key}`, issues);
|
|
515
|
+
if ("cmd" in obj && typeof obj.cmd != "string") {
|
|
516
|
+
issues.push({
|
|
517
|
+
path: `${pathPrefix}.${key}.cmd`,
|
|
518
|
+
message: "must be a string",
|
|
519
|
+
fix: "set to an empty string or a command template",
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
if ("args" in obj && !isStringArray(obj.args)) {
|
|
523
|
+
issues.push({
|
|
524
|
+
path: `${pathPrefix}.${key}.args`,
|
|
525
|
+
message: "must be an array of strings",
|
|
526
|
+
fix: 'example: "args": ["--optimize"]',
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
if ("target" in obj) {
|
|
530
|
+
if (typeof obj.target != "string") {
|
|
531
|
+
issues.push({
|
|
532
|
+
path: `${pathPrefix}.${key}.target`,
|
|
533
|
+
message: "must be a string",
|
|
534
|
+
fix: 'set to "wasi", "bindings", or "web"',
|
|
535
|
+
});
|
|
536
|
+
} else if (
|
|
537
|
+
obj.target != "wasi" &&
|
|
538
|
+
obj.target != "bindings" &&
|
|
539
|
+
obj.target != "web"
|
|
540
|
+
) {
|
|
541
|
+
issues.push({
|
|
542
|
+
path: `${pathPrefix}.${key}.target`,
|
|
543
|
+
message: `must be "wasi", "bindings", or "web"`,
|
|
544
|
+
fix: `received "${obj.target}"`,
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
validateEnvField(obj, "env", `${pathPrefix}.${key}`, issues);
|
|
515
549
|
}
|
|
516
550
|
function validateRunOptionsField(raw, key, pathPrefix, issues) {
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
551
|
+
if (!(key in raw) || raw[key] == undefined) return;
|
|
552
|
+
const value = raw[key];
|
|
553
|
+
if (!value || typeof value != "object" || Array.isArray(value)) {
|
|
554
|
+
issues.push({
|
|
555
|
+
path: `${pathPrefix}.${key}`,
|
|
556
|
+
message: "must be an object",
|
|
557
|
+
fix: 'example: "runOptions": { "runtime": { "cmd": "node ... <file>" } }',
|
|
558
|
+
});
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
const obj = value;
|
|
562
|
+
validateUnknownKeys(obj, RUN_OPTION_KEYS, `${pathPrefix}.${key}`, issues);
|
|
563
|
+
if ("run" in obj && typeof obj.run != "string") {
|
|
564
|
+
issues.push({
|
|
565
|
+
path: `${pathPrefix}.${key}.run`,
|
|
566
|
+
message: "must be a string",
|
|
567
|
+
fix: 'prefer "runtime.cmd"; legacy "run" must still be string',
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
if ("runtime" in obj && obj.runtime != undefined) {
|
|
571
|
+
const runtime = obj.runtime;
|
|
572
|
+
if (!runtime || typeof runtime != "object" || Array.isArray(runtime)) {
|
|
573
|
+
issues.push({
|
|
574
|
+
path: `${pathPrefix}.${key}.runtime`,
|
|
575
|
+
message: "must be an object",
|
|
576
|
+
fix: 'example: "runtime": { "cmd": "node ./.as-test/runners/default.wasi.js <file>" }',
|
|
577
|
+
});
|
|
578
|
+
} else {
|
|
579
|
+
const runtimeObj = runtime;
|
|
580
|
+
validateUnknownKeys(
|
|
581
|
+
runtimeObj,
|
|
582
|
+
RUNTIME_OPTION_KEYS,
|
|
583
|
+
`${pathPrefix}.${key}.runtime`,
|
|
584
|
+
issues,
|
|
585
|
+
);
|
|
586
|
+
if ("cmd" in runtimeObj && typeof runtimeObj.cmd != "string") {
|
|
531
587
|
issues.push({
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
588
|
+
path: `${pathPrefix}.${key}.runtime.cmd`,
|
|
589
|
+
message: "must be a string",
|
|
590
|
+
fix: 'set to a runtime command including "<file>"',
|
|
535
591
|
});
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
const runtime = obj.runtime;
|
|
539
|
-
if (!runtime || typeof runtime != "object" || Array.isArray(runtime)) {
|
|
540
|
-
issues.push({
|
|
541
|
-
path: `${pathPrefix}.${key}.runtime`,
|
|
542
|
-
message: "must be an object",
|
|
543
|
-
fix: 'example: "runtime": { "cmd": "node ./.as-test/runners/default.wasi.js <file>" }',
|
|
544
|
-
});
|
|
545
|
-
}
|
|
546
|
-
else {
|
|
547
|
-
const runtimeObj = runtime;
|
|
548
|
-
validateUnknownKeys(runtimeObj, RUNTIME_OPTION_KEYS, `${pathPrefix}.${key}.runtime`, issues);
|
|
549
|
-
if ("cmd" in runtimeObj && typeof runtimeObj.cmd != "string") {
|
|
550
|
-
issues.push({
|
|
551
|
-
path: `${pathPrefix}.${key}.runtime.cmd`,
|
|
552
|
-
message: "must be a string",
|
|
553
|
-
fix: 'set to a runtime command including "<file>"',
|
|
554
|
-
});
|
|
555
|
-
}
|
|
556
|
-
if ("run" in runtimeObj && typeof runtimeObj.run != "string") {
|
|
557
|
-
issues.push({
|
|
558
|
-
path: `${pathPrefix}.${key}.runtime.run`,
|
|
559
|
-
message: "must be a string",
|
|
560
|
-
fix: 'legacy "run" should be a command string',
|
|
561
|
-
});
|
|
562
|
-
}
|
|
563
|
-
if ("browser" in runtimeObj && typeof runtimeObj.browser != "string") {
|
|
564
|
-
issues.push({
|
|
565
|
-
path: `${pathPrefix}.${key}.runtime.browser`,
|
|
566
|
-
message: "must be a string",
|
|
567
|
-
fix: 'set to "chrome", "chromium", "firefox", "webkit", or an executable path',
|
|
568
|
-
});
|
|
569
|
-
}
|
|
570
|
-
}
|
|
571
|
-
}
|
|
572
|
-
if ("reporter" in obj && obj.reporter != undefined) {
|
|
573
|
-
const reporter = obj.reporter;
|
|
574
|
-
if (typeof reporter == "string")
|
|
575
|
-
return;
|
|
576
|
-
if (!reporter || typeof reporter != "object" || Array.isArray(reporter)) {
|
|
577
|
-
issues.push({
|
|
578
|
-
path: `${pathPrefix}.${key}.reporter`,
|
|
579
|
-
message: "must be a string or object",
|
|
580
|
-
fix: 'use "default", "tap", or { "name": "...", ... }',
|
|
581
|
-
});
|
|
582
|
-
return;
|
|
583
|
-
}
|
|
584
|
-
const reporterObj = reporter;
|
|
585
|
-
validateUnknownKeys(reporterObj, REPORTER_OPTION_KEYS, `${pathPrefix}.${key}.reporter`, issues);
|
|
586
|
-
if ("name" in reporterObj && typeof reporterObj.name != "string") {
|
|
587
|
-
issues.push({
|
|
588
|
-
path: `${pathPrefix}.${key}.reporter.name`,
|
|
589
|
-
message: "must be a string",
|
|
590
|
-
fix: 'set to "default", "tap", or module path',
|
|
591
|
-
});
|
|
592
|
-
}
|
|
593
|
-
if (!("name" in reporterObj)) {
|
|
594
|
-
issues.push({
|
|
595
|
-
path: `${pathPrefix}.${key}.reporter`,
|
|
596
|
-
message: 'object reporter config requires "name"',
|
|
597
|
-
fix: 'example: { "name": "tap", "outDir": "./.as-test/reports" }',
|
|
598
|
-
});
|
|
599
|
-
}
|
|
600
|
-
if ("options" in reporterObj && !isStringArray(reporterObj.options)) {
|
|
601
|
-
issues.push({
|
|
602
|
-
path: `${pathPrefix}.${key}.reporter.options`,
|
|
603
|
-
message: "must be an array of strings",
|
|
604
|
-
fix: 'example: "options": ["single-file"]',
|
|
605
|
-
});
|
|
606
|
-
}
|
|
607
|
-
if ("outDir" in reporterObj && typeof reporterObj.outDir != "string") {
|
|
608
|
-
issues.push({
|
|
609
|
-
path: `${pathPrefix}.${key}.reporter.outDir`,
|
|
610
|
-
message: "must be a string",
|
|
611
|
-
});
|
|
612
|
-
}
|
|
613
|
-
if ("outFile" in reporterObj && typeof reporterObj.outFile != "string") {
|
|
614
|
-
issues.push({
|
|
615
|
-
path: `${pathPrefix}.${key}.reporter.outFile`,
|
|
616
|
-
message: "must be a string",
|
|
617
|
-
});
|
|
618
|
-
}
|
|
619
|
-
}
|
|
620
|
-
validateEnvField(obj, "env", `${pathPrefix}.${key}`, issues);
|
|
621
|
-
}
|
|
622
|
-
function validateFuzzField(raw, key, pathPrefix, issues) {
|
|
623
|
-
if (!(key in raw) || raw[key] == undefined)
|
|
624
|
-
return;
|
|
625
|
-
const value = raw[key];
|
|
626
|
-
if (!value || typeof value != "object" || Array.isArray(value)) {
|
|
592
|
+
}
|
|
593
|
+
if ("run" in runtimeObj && typeof runtimeObj.run != "string") {
|
|
627
594
|
issues.push({
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
595
|
+
path: `${pathPrefix}.${key}.runtime.run`,
|
|
596
|
+
message: "must be a string",
|
|
597
|
+
fix: 'legacy "run" should be a command string',
|
|
631
598
|
});
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
const obj = value;
|
|
635
|
-
validateUnknownKeys(obj, FUZZ_OPTION_KEYS, `${pathPrefix}.${key}`, issues);
|
|
636
|
-
validateInputField(obj, "input", `${pathPrefix}.${key}`, issues);
|
|
637
|
-
validateStringField(obj, "target", `${pathPrefix}.${key}`, issues);
|
|
638
|
-
validateStringField(obj, "corpusDir", `${pathPrefix}.${key}`, issues);
|
|
639
|
-
validateStringField(obj, "crashDir", `${pathPrefix}.${key}`, issues);
|
|
640
|
-
validateNumberField(obj, "runs", `${pathPrefix}.${key}`, issues, true);
|
|
641
|
-
validateNumberField(obj, "seed", `${pathPrefix}.${key}`, issues, false);
|
|
642
|
-
validateNumberField(obj, "maxInputBytes", `${pathPrefix}.${key}`, issues, true);
|
|
643
|
-
if ("target" in obj && obj.target != "bindings") {
|
|
599
|
+
}
|
|
600
|
+
if ("browser" in runtimeObj && typeof runtimeObj.browser != "string") {
|
|
644
601
|
issues.push({
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
602
|
+
path: `${pathPrefix}.${key}.runtime.browser`,
|
|
603
|
+
message: "must be a string",
|
|
604
|
+
fix: 'set to "chrome", "chromium", "firefox", "webkit", or an executable path',
|
|
648
605
|
});
|
|
649
|
-
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
if ("reporter" in obj && obj.reporter != undefined) {
|
|
610
|
+
const reporter = obj.reporter;
|
|
611
|
+
if (typeof reporter == "string") return;
|
|
612
|
+
if (!reporter || typeof reporter != "object" || Array.isArray(reporter)) {
|
|
613
|
+
issues.push({
|
|
614
|
+
path: `${pathPrefix}.${key}.reporter`,
|
|
615
|
+
message: "must be a string or object",
|
|
616
|
+
fix: 'use "default", "tap", or { "name": "...", ... }',
|
|
617
|
+
});
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
const reporterObj = reporter;
|
|
621
|
+
validateUnknownKeys(
|
|
622
|
+
reporterObj,
|
|
623
|
+
REPORTER_OPTION_KEYS,
|
|
624
|
+
`${pathPrefix}.${key}.reporter`,
|
|
625
|
+
issues,
|
|
626
|
+
);
|
|
627
|
+
if ("name" in reporterObj && typeof reporterObj.name != "string") {
|
|
628
|
+
issues.push({
|
|
629
|
+
path: `${pathPrefix}.${key}.reporter.name`,
|
|
630
|
+
message: "must be a string",
|
|
631
|
+
fix: 'set to "default", "tap", or module path',
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
if (!("name" in reporterObj)) {
|
|
635
|
+
issues.push({
|
|
636
|
+
path: `${pathPrefix}.${key}.reporter`,
|
|
637
|
+
message: 'object reporter config requires "name"',
|
|
638
|
+
fix: 'example: { "name": "tap", "outDir": "./.as-test/reports" }',
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
if ("options" in reporterObj && !isStringArray(reporterObj.options)) {
|
|
642
|
+
issues.push({
|
|
643
|
+
path: `${pathPrefix}.${key}.reporter.options`,
|
|
644
|
+
message: "must be an array of strings",
|
|
645
|
+
fix: 'example: "options": ["single-file"]',
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
if ("outDir" in reporterObj && typeof reporterObj.outDir != "string") {
|
|
649
|
+
issues.push({
|
|
650
|
+
path: `${pathPrefix}.${key}.reporter.outDir`,
|
|
651
|
+
message: "must be a string",
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
if ("outFile" in reporterObj && typeof reporterObj.outFile != "string") {
|
|
655
|
+
issues.push({
|
|
656
|
+
path: `${pathPrefix}.${key}.reporter.outFile`,
|
|
657
|
+
message: "must be a string",
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
validateEnvField(obj, "env", `${pathPrefix}.${key}`, issues);
|
|
662
|
+
}
|
|
663
|
+
function validateFuzzField(raw, key, pathPrefix, issues) {
|
|
664
|
+
if (!(key in raw) || raw[key] == undefined) return;
|
|
665
|
+
const value = raw[key];
|
|
666
|
+
if (!value || typeof value != "object" || Array.isArray(value)) {
|
|
667
|
+
issues.push({
|
|
668
|
+
path: `${pathPrefix}.${key}`,
|
|
669
|
+
message: "must be an object",
|
|
670
|
+
fix: 'example: "fuzz": { "input": ["./assembly/__fuzz__/*.fuzz.ts"], "runs": 1000 }',
|
|
671
|
+
});
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
const obj = value;
|
|
675
|
+
validateUnknownKeys(obj, FUZZ_OPTION_KEYS, `${pathPrefix}.${key}`, issues);
|
|
676
|
+
validateInputField(obj, "input", `${pathPrefix}.${key}`, issues);
|
|
677
|
+
validateStringField(obj, "target", `${pathPrefix}.${key}`, issues);
|
|
678
|
+
validateStringField(obj, "corpusDir", `${pathPrefix}.${key}`, issues);
|
|
679
|
+
validateStringField(obj, "crashDir", `${pathPrefix}.${key}`, issues);
|
|
680
|
+
validateNumberField(obj, "runs", `${pathPrefix}.${key}`, issues, true);
|
|
681
|
+
validateNumberField(obj, "seed", `${pathPrefix}.${key}`, issues, false);
|
|
682
|
+
validateNumberField(
|
|
683
|
+
obj,
|
|
684
|
+
"maxInputBytes",
|
|
685
|
+
`${pathPrefix}.${key}`,
|
|
686
|
+
issues,
|
|
687
|
+
true,
|
|
688
|
+
);
|
|
689
|
+
if ("target" in obj && obj.target != "bindings") {
|
|
690
|
+
issues.push({
|
|
691
|
+
path: `${pathPrefix}.${key}.target`,
|
|
692
|
+
message: 'must be "bindings"',
|
|
693
|
+
fix: 'set to "bindings"',
|
|
694
|
+
});
|
|
695
|
+
}
|
|
650
696
|
}
|
|
651
697
|
function validateModesField(raw, key, pathPrefix, issues) {
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
698
|
+
if (!(key in raw) || raw[key] == undefined) return;
|
|
699
|
+
const value = raw[key];
|
|
700
|
+
if (!value || typeof value != "object" || Array.isArray(value)) {
|
|
701
|
+
issues.push({
|
|
702
|
+
path: `${pathPrefix}.${key}`,
|
|
703
|
+
message: "must be an object",
|
|
704
|
+
fix: 'example: "modes": { "wasi": { "buildOptions": { "target": "wasi" } } }',
|
|
705
|
+
});
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
for (const [modeName, modeRaw] of Object.entries(value)) {
|
|
709
|
+
if (typeof modeRaw == "string") {
|
|
710
|
+
if (!modeRaw.length) {
|
|
656
711
|
issues.push({
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
712
|
+
path: `${pathPrefix}.${key}.${modeName}`,
|
|
713
|
+
message: "must not be an empty string",
|
|
714
|
+
fix: 'set to a config file path like "./as-test.config.simd.json"',
|
|
660
715
|
});
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
validateStringField(modeObj, "logs", modePath, issues);
|
|
696
|
-
validateStringField(modeObj, "coverageDir", modePath, issues);
|
|
697
|
-
validateStringField(modeObj, "snapshotDir", modePath, issues);
|
|
698
|
-
validateStringField(modeObj, "config", modePath, issues);
|
|
699
|
-
validateCoverageField(modeObj, "coverage", modePath, issues);
|
|
700
|
-
validateFuzzField(modeObj, "fuzz", modePath, issues);
|
|
701
|
-
validateEnvField(modeObj, "env", modePath, issues);
|
|
702
|
-
validateBuildOptionsField(modeObj, "buildOptions", modePath, issues);
|
|
703
|
-
validateRunOptionsField(modeObj, "runOptions", modePath, issues);
|
|
704
|
-
}
|
|
716
|
+
}
|
|
717
|
+
continue;
|
|
718
|
+
}
|
|
719
|
+
if (!modeRaw || typeof modeRaw != "object" || Array.isArray(modeRaw)) {
|
|
720
|
+
issues.push({
|
|
721
|
+
path: `${pathPrefix}.${key}.${modeName}`,
|
|
722
|
+
message: "must be a config object or config file path string",
|
|
723
|
+
});
|
|
724
|
+
continue;
|
|
725
|
+
}
|
|
726
|
+
const modeObj = modeRaw;
|
|
727
|
+
const modePath = `${pathPrefix}.${key}.${modeName}`;
|
|
728
|
+
validateUnknownKeys(modeObj, MODE_KEYS, modePath, issues);
|
|
729
|
+
validateStringField(modeObj, "$schema", modePath, issues);
|
|
730
|
+
validateInputField(modeObj, "input", modePath, issues);
|
|
731
|
+
if ("default" in modeObj && typeof modeObj.default != "boolean") {
|
|
732
|
+
issues.push({
|
|
733
|
+
path: `${modePath}.default`,
|
|
734
|
+
message: "must be a boolean",
|
|
735
|
+
fix: 'set "default" to true or false',
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
validateOutputField(modeObj, "output", modePath, issues);
|
|
739
|
+
validateStringField(modeObj, "outDir", modePath, issues);
|
|
740
|
+
validateStringField(modeObj, "logs", modePath, issues);
|
|
741
|
+
validateStringField(modeObj, "coverageDir", modePath, issues);
|
|
742
|
+
validateStringField(modeObj, "snapshotDir", modePath, issues);
|
|
743
|
+
validateStringField(modeObj, "config", modePath, issues);
|
|
744
|
+
validateCoverageField(modeObj, "coverage", modePath, issues);
|
|
745
|
+
validateFuzzField(modeObj, "fuzz", modePath, issues);
|
|
746
|
+
validateEnvField(modeObj, "env", modePath, issues);
|
|
747
|
+
validateBuildOptionsField(modeObj, "buildOptions", modePath, issues);
|
|
748
|
+
validateRunOptionsField(modeObj, "runOptions", modePath, issues);
|
|
749
|
+
}
|
|
705
750
|
}
|
|
706
751
|
function isStringArray(value) {
|
|
707
|
-
|
|
752
|
+
return Array.isArray(value) && value.every((item) => typeof item == "string");
|
|
708
753
|
}
|
|
709
754
|
function validateNumberField(raw, key, pathPrefix, issues, positiveOnly) {
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
}
|
|
755
|
+
if (!(key in raw) || raw[key] == undefined) return;
|
|
756
|
+
if (typeof raw[key] != "number" || !Number.isFinite(raw[key])) {
|
|
757
|
+
issues.push({
|
|
758
|
+
path: `${pathPrefix}.${key}`,
|
|
759
|
+
message: "must be a finite number",
|
|
760
|
+
fix: `set "${key}" to a numeric value`,
|
|
761
|
+
});
|
|
762
|
+
return;
|
|
763
|
+
}
|
|
764
|
+
if (positiveOnly && Number(raw[key]) <= 0) {
|
|
765
|
+
issues.push({
|
|
766
|
+
path: `${pathPrefix}.${key}`,
|
|
767
|
+
message: "must be greater than zero",
|
|
768
|
+
fix: `set "${key}" to a positive integer`,
|
|
769
|
+
});
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
if (!positiveOnly && Number(raw[key]) < 0) {
|
|
773
|
+
issues.push({
|
|
774
|
+
path: `${pathPrefix}.${key}`,
|
|
775
|
+
message: "must be zero or greater",
|
|
776
|
+
fix: `set "${key}" to a non-negative integer`,
|
|
777
|
+
});
|
|
778
|
+
}
|
|
735
779
|
}
|
|
736
780
|
function resolveClosestKey(value, keys) {
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
return null;
|
|
781
|
+
let best = null;
|
|
782
|
+
let bestDistance = Number.POSITIVE_INFINITY;
|
|
783
|
+
for (const key of keys) {
|
|
784
|
+
const distance = levenshteinDistance(value, key);
|
|
785
|
+
if (distance < bestDistance) {
|
|
786
|
+
bestDistance = distance;
|
|
787
|
+
best = key;
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
if (best && bestDistance <= 3) return best;
|
|
791
|
+
return null;
|
|
749
792
|
}
|
|
750
793
|
function levenshteinDistance(left, right) {
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
}
|
|
770
|
-
|
|
794
|
+
if (left == right) return 0;
|
|
795
|
+
if (!left.length) return right.length;
|
|
796
|
+
if (!right.length) return left.length;
|
|
797
|
+
const matrix = [];
|
|
798
|
+
for (let i = 0; i <= left.length; i++) {
|
|
799
|
+
matrix[i] = [i];
|
|
800
|
+
}
|
|
801
|
+
for (let j = 0; j <= right.length; j++) {
|
|
802
|
+
matrix[0][j] = j;
|
|
803
|
+
}
|
|
804
|
+
for (let i = 1; i <= left.length; i++) {
|
|
805
|
+
for (let j = 1; j <= right.length; j++) {
|
|
806
|
+
const cost = left[i - 1] == right[j - 1] ? 0 : 1;
|
|
807
|
+
matrix[i][j] = Math.min(
|
|
808
|
+
matrix[i - 1][j] + 1,
|
|
809
|
+
matrix[i][j - 1] + 1,
|
|
810
|
+
matrix[i - 1][j - 1] + cost,
|
|
811
|
+
);
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
return matrix[left.length][right.length];
|
|
771
815
|
}
|
|
772
816
|
function applyOutputConfig(rawOutput, rawConfig, config) {
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
817
|
+
if (rawOutput == undefined) return;
|
|
818
|
+
if (typeof rawOutput == "string") {
|
|
819
|
+
applyOutputRoot(rawOutput, rawConfig, config);
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
if (!rawOutput || typeof rawOutput != "object" || Array.isArray(rawOutput)) {
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
825
|
+
const output = rawOutput;
|
|
826
|
+
if (
|
|
827
|
+
"build" in output &&
|
|
828
|
+
typeof output.build == "string" &&
|
|
829
|
+
output.build.length &&
|
|
830
|
+
!("outDir" in rawConfig)
|
|
831
|
+
) {
|
|
832
|
+
config.outDir = output.build;
|
|
833
|
+
}
|
|
834
|
+
if (
|
|
835
|
+
"logs" in output &&
|
|
836
|
+
typeof output.logs == "string" &&
|
|
837
|
+
output.logs.length &&
|
|
838
|
+
!("logs" in rawConfig)
|
|
839
|
+
) {
|
|
840
|
+
config.logs = output.logs;
|
|
841
|
+
}
|
|
842
|
+
if (
|
|
843
|
+
"coverage" in output &&
|
|
844
|
+
typeof output.coverage == "string" &&
|
|
845
|
+
output.coverage.length &&
|
|
846
|
+
!("coverageDir" in rawConfig)
|
|
847
|
+
) {
|
|
848
|
+
config.coverageDir = output.coverage;
|
|
849
|
+
}
|
|
850
|
+
if (
|
|
851
|
+
"snapshots" in output &&
|
|
852
|
+
typeof output.snapshots == "string" &&
|
|
853
|
+
output.snapshots.length &&
|
|
854
|
+
!("snapshotDir" in rawConfig)
|
|
855
|
+
) {
|
|
856
|
+
config.snapshotDir = output.snapshots;
|
|
857
|
+
}
|
|
807
858
|
}
|
|
808
859
|
function applyOutputRoot(root, rawConfig, config) {
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
}
|
|
860
|
+
if (!root.length) return;
|
|
861
|
+
if (!("outDir" in rawConfig)) {
|
|
862
|
+
config.outDir = join(root, "build");
|
|
863
|
+
}
|
|
864
|
+
if (!("logs" in rawConfig)) {
|
|
865
|
+
config.logs = join(root, "logs");
|
|
866
|
+
}
|
|
867
|
+
if (!("coverageDir" in rawConfig)) {
|
|
868
|
+
config.coverageDir = join(root, "coverage");
|
|
869
|
+
}
|
|
870
|
+
if (!("snapshotDir" in rawConfig)) {
|
|
871
|
+
config.snapshotDir = join(root, "snapshots");
|
|
872
|
+
}
|
|
823
873
|
}
|
|
824
874
|
function parseModes(raw, configDir) {
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
875
|
+
if (!raw || typeof raw != "object" || Array.isArray(raw)) return {};
|
|
876
|
+
const out = {};
|
|
877
|
+
const entries = Object.entries(raw);
|
|
878
|
+
for (const [name, value] of entries) {
|
|
879
|
+
const mode = new ModeConfig();
|
|
880
|
+
if (typeof value == "string") {
|
|
881
|
+
mode.path = resolve(configDir, value);
|
|
882
|
+
mode.config = parseConfigRaw(
|
|
883
|
+
{},
|
|
884
|
+
join(configDir, `__mode__.${name}.json`),
|
|
885
|
+
);
|
|
886
|
+
out[name] = mode;
|
|
887
|
+
continue;
|
|
888
|
+
}
|
|
889
|
+
if (!value || typeof value != "object" || Array.isArray(value)) continue;
|
|
890
|
+
mode.default = !("default" in value) || Boolean(value.default);
|
|
891
|
+
mode.config = parseConfigRaw(
|
|
892
|
+
value,
|
|
893
|
+
join(configDir, `__mode__.${name}.json`),
|
|
894
|
+
);
|
|
895
|
+
out[name] = mode;
|
|
896
|
+
}
|
|
897
|
+
return out;
|
|
846
898
|
}
|
|
847
899
|
function parseEnvMap(raw) {
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
}
|
|
855
|
-
return env;
|
|
900
|
+
if (!raw || typeof raw != "object" || Array.isArray(raw)) return {};
|
|
901
|
+
const env = {};
|
|
902
|
+
for (const [key, val] of Object.entries(raw)) {
|
|
903
|
+
if (typeof val == "string") env[key] = val;
|
|
904
|
+
}
|
|
905
|
+
return env;
|
|
856
906
|
}
|
|
857
907
|
function parseEnvValue(raw, configDir, pathLabel) {
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
return parseEnvMap(raw);
|
|
908
|
+
if (raw == undefined) return {};
|
|
909
|
+
if (typeof raw == "string") {
|
|
910
|
+
return parseEnvFile(resolve(configDir, raw), pathLabel);
|
|
911
|
+
}
|
|
912
|
+
if (Array.isArray(raw)) {
|
|
913
|
+
return parseInlineEnvEntries(raw, pathLabel);
|
|
914
|
+
}
|
|
915
|
+
return parseEnvMap(raw);
|
|
867
916
|
}
|
|
868
917
|
function parseInlineEnvEntries(values, pathLabel) {
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
918
|
+
const env = {};
|
|
919
|
+
for (let i = 0; i < values.length; i++) {
|
|
920
|
+
const item = values[i];
|
|
921
|
+
if (typeof item != "string") continue;
|
|
922
|
+
const separator = item.indexOf("=");
|
|
923
|
+
if (separator <= 0) {
|
|
924
|
+
throw new Error(
|
|
925
|
+
`invalid config at ${pathLabel}\nenv entry at index ${i} must use KEY=value format`,
|
|
926
|
+
);
|
|
927
|
+
}
|
|
928
|
+
const key = item.slice(0, separator).trim();
|
|
929
|
+
const value = item.slice(separator + 1);
|
|
930
|
+
if (!key.length) {
|
|
931
|
+
throw new Error(
|
|
932
|
+
`invalid config at ${pathLabel}\nenv entry at index ${i} must use a non-empty key`,
|
|
933
|
+
);
|
|
934
|
+
}
|
|
935
|
+
env[key] = value;
|
|
936
|
+
}
|
|
937
|
+
return env;
|
|
886
938
|
}
|
|
887
939
|
function parseEnvFile(envPath, pathLabel) {
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
940
|
+
if (!existsSync(envPath)) {
|
|
941
|
+
throw new Error(
|
|
942
|
+
`invalid config at ${pathLabel}\nenv file not found: ${envPath}`,
|
|
943
|
+
);
|
|
944
|
+
}
|
|
945
|
+
const env = {};
|
|
946
|
+
const lines = readFileSync(envPath, "utf8").split(/\r?\n/);
|
|
947
|
+
for (let i = 0; i < lines.length; i++) {
|
|
948
|
+
const rawLine = lines[i];
|
|
949
|
+
const line = rawLine.trim();
|
|
950
|
+
if (!line.length || line.startsWith("#")) continue;
|
|
951
|
+
const normalized = line.startsWith("export ") ? line.slice(7).trim() : line;
|
|
952
|
+
const separator = normalized.indexOf("=");
|
|
953
|
+
if (separator <= 0) {
|
|
954
|
+
throw new Error(
|
|
955
|
+
`invalid config at ${pathLabel}\ninvalid env line ${i + 1} in ${envPath}: expected KEY=value`,
|
|
956
|
+
);
|
|
957
|
+
}
|
|
958
|
+
const key = normalized.slice(0, separator).trim();
|
|
959
|
+
const value = normalized.slice(separator + 1).trim();
|
|
960
|
+
env[key] = unquoteEnvValue(value);
|
|
961
|
+
}
|
|
962
|
+
return env;
|
|
908
963
|
}
|
|
909
964
|
function unquoteEnvValue(value) {
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
.replace(/\\"/g, '"')
|
|
924
|
-
.replace(/\\\\/g, "\\");
|
|
965
|
+
if (value.length < 2) return value;
|
|
966
|
+
const quote = value[0];
|
|
967
|
+
if ((quote != '"' && quote != "'") || value[value.length - 1] != quote) {
|
|
968
|
+
return value;
|
|
969
|
+
}
|
|
970
|
+
const inner = value.slice(1, -1);
|
|
971
|
+
if (quote == "'") return inner;
|
|
972
|
+
return inner
|
|
973
|
+
.replace(/\\n/g, "\n")
|
|
974
|
+
.replace(/\\r/g, "\r")
|
|
975
|
+
.replace(/\\t/g, "\t")
|
|
976
|
+
.replace(/\\"/g, '"')
|
|
977
|
+
.replace(/\\\\/g, "\\");
|
|
925
978
|
}
|
|
926
979
|
function normalizePositiveNumber(value, fallback) {
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
980
|
+
if (typeof value != "number" || !Number.isFinite(value) || value <= 0) {
|
|
981
|
+
return fallback;
|
|
982
|
+
}
|
|
983
|
+
return Math.floor(value);
|
|
931
984
|
}
|
|
932
985
|
function normalizeNonNegativeNumber(value, fallback) {
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
986
|
+
if (typeof value != "number" || !Number.isFinite(value) || value < 0) {
|
|
987
|
+
return fallback;
|
|
988
|
+
}
|
|
989
|
+
return Math.floor(value);
|
|
937
990
|
}
|
|
938
991
|
function getConfigMeta(config) {
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
992
|
+
const meta = CONFIG_META.get(config);
|
|
993
|
+
if (!meta) {
|
|
994
|
+
throw new Error("missing config metadata");
|
|
995
|
+
}
|
|
996
|
+
return meta;
|
|
944
997
|
}
|
|
945
998
|
function cloneCoverageOptions(coverage) {
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
return cloned;
|
|
999
|
+
if (typeof coverage == "boolean") return coverage;
|
|
1000
|
+
const cloned = Object.assign(new CoverageOptions(), coverage);
|
|
1001
|
+
const ignore = coverage.ignore ?? new CoverageIgnoreOptions();
|
|
1002
|
+
cloned.mode = coverage.mode ?? "project";
|
|
1003
|
+
cloned.dependencies = [...(coverage.dependencies ?? [])];
|
|
1004
|
+
cloned.include = [...(coverage.include ?? [])];
|
|
1005
|
+
cloned.exclude = [...(coverage.exclude ?? [])];
|
|
1006
|
+
cloned.ignore = Object.assign(new CoverageIgnoreOptions(), ignore);
|
|
1007
|
+
cloned.ignore.labels = [...(ignore.labels ?? [])];
|
|
1008
|
+
cloned.ignore.names = [...(ignore.names ?? [])];
|
|
1009
|
+
cloned.ignore.locations = [...(ignore.locations ?? [])];
|
|
1010
|
+
cloned.ignore.snippets = [...(ignore.snippets ?? [])];
|
|
1011
|
+
return cloned;
|
|
960
1012
|
}
|
|
961
1013
|
function cloneBuildOptions(options) {
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
1014
|
+
const cloned = Object.assign(new BuildOptions(), options);
|
|
1015
|
+
cloned.args = [...options.args];
|
|
1016
|
+
cloned.env = { ...options.env };
|
|
1017
|
+
return cloned;
|
|
966
1018
|
}
|
|
967
1019
|
function cloneRuntime(runtime) {
|
|
968
|
-
|
|
1020
|
+
return Object.assign(new Runtime(), runtime);
|
|
969
1021
|
}
|
|
970
1022
|
function cloneReporterConfig(reporter) {
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
return cloned;
|
|
1023
|
+
if (typeof reporter == "string") return reporter;
|
|
1024
|
+
const cloned = Object.assign(new ReporterConfig(), reporter);
|
|
1025
|
+
cloned.options = [...(reporter.options ?? [])];
|
|
1026
|
+
return cloned;
|
|
976
1027
|
}
|
|
977
1028
|
function cloneRunOptions(options) {
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
1029
|
+
const cloned = Object.assign(new RunOptions(), options);
|
|
1030
|
+
cloned.runtime = cloneRuntime(options.runtime);
|
|
1031
|
+
cloned.reporter = cloneReporterConfig(options.reporter);
|
|
1032
|
+
cloned.env = { ...options.env };
|
|
1033
|
+
return cloned;
|
|
983
1034
|
}
|
|
984
1035
|
function cloneFuzzConfig(config) {
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
1036
|
+
const cloned = Object.assign(new FuzzConfig(), config);
|
|
1037
|
+
cloned.input = [...config.input];
|
|
1038
|
+
return cloned;
|
|
988
1039
|
}
|
|
989
1040
|
function cloneModeConfig(config) {
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
1041
|
+
const cloned = new ModeConfig();
|
|
1042
|
+
cloned.path = config.path;
|
|
1043
|
+
cloned.config = cloneConfig(config.config);
|
|
1044
|
+
return cloned;
|
|
994
1045
|
}
|
|
995
1046
|
function cloneConfig(config) {
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1047
|
+
const cloned = Object.assign(new Config(), config);
|
|
1048
|
+
cloned.input = [...config.input];
|
|
1049
|
+
cloned.env = { ...config.env };
|
|
1050
|
+
cloned.buildOptions = cloneBuildOptions(config.buildOptions);
|
|
1051
|
+
cloned.runOptions = cloneRunOptions(config.runOptions);
|
|
1052
|
+
cloned.fuzz = cloneFuzzConfig(config.fuzz);
|
|
1053
|
+
cloned.coverage = cloneCoverageOptions(config.coverage);
|
|
1054
|
+
cloned.modes = Object.fromEntries(
|
|
1055
|
+
Object.entries(config.modes).map(([name, mode]) => [
|
|
1056
|
+
name,
|
|
1057
|
+
cloneModeConfig(mode),
|
|
1058
|
+
]),
|
|
1059
|
+
);
|
|
1060
|
+
CONFIG_META.set(cloned, getConfigMeta(config));
|
|
1061
|
+
return cloned;
|
|
1009
1062
|
}
|
|
1010
1063
|
function outputOverridesField(raw, field) {
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1064
|
+
if (field in raw) return true;
|
|
1065
|
+
if (
|
|
1066
|
+
!raw.output ||
|
|
1067
|
+
typeof raw.output != "object" ||
|
|
1068
|
+
Array.isArray(raw.output)
|
|
1069
|
+
) {
|
|
1070
|
+
return false;
|
|
1071
|
+
}
|
|
1072
|
+
const output = raw.output;
|
|
1073
|
+
if (field == "outDir")
|
|
1074
|
+
return typeof output.build == "string" && output.build.length > 0;
|
|
1075
|
+
if (field == "logs")
|
|
1076
|
+
return typeof output.logs == "string" && output.logs.length > 0;
|
|
1077
|
+
if (field == "coverageDir") {
|
|
1078
|
+
return typeof output.coverage == "string" && output.coverage.length > 0;
|
|
1079
|
+
}
|
|
1080
|
+
return typeof output.snapshots == "string" && output.snapshots.length > 0;
|
|
1027
1081
|
}
|
|
1028
1082
|
function mergeCoverageIgnoreOptions(base, override, raw) {
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
merged.locations = [...override.locations];
|
|
1040
|
-
if ("snippets" in raw)
|
|
1041
|
-
merged.snippets = [...override.snippets];
|
|
1042
|
-
return merged;
|
|
1083
|
+
const merged = Object.assign(new CoverageIgnoreOptions(), base);
|
|
1084
|
+
merged.labels = [...base.labels];
|
|
1085
|
+
merged.names = [...base.names];
|
|
1086
|
+
merged.locations = [...base.locations];
|
|
1087
|
+
merged.snippets = [...base.snippets];
|
|
1088
|
+
if ("labels" in raw) merged.labels = [...override.labels];
|
|
1089
|
+
if ("names" in raw) merged.names = [...override.names];
|
|
1090
|
+
if ("locations" in raw) merged.locations = [...override.locations];
|
|
1091
|
+
if ("snippets" in raw) merged.snippets = [...override.snippets];
|
|
1092
|
+
return merged;
|
|
1043
1093
|
}
|
|
1044
1094
|
function mergeCoverageConfig(base, override, raw) {
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1095
|
+
if (typeof raw == "boolean") return override;
|
|
1096
|
+
if (!raw || typeof raw != "object" || Array.isArray(raw))
|
|
1097
|
+
return cloneCoverageOptions(base);
|
|
1098
|
+
const mergedBase =
|
|
1099
|
+
typeof base == "boolean"
|
|
1100
|
+
? Object.assign(new CoverageOptions(), { enabled: base })
|
|
1101
|
+
: cloneCoverageOptions(base);
|
|
1102
|
+
const overrideOptions =
|
|
1103
|
+
typeof override == "boolean"
|
|
1104
|
+
? Object.assign(new CoverageOptions(), { enabled: override })
|
|
1105
|
+
: cloneCoverageOptions(override);
|
|
1106
|
+
const rawObject = raw;
|
|
1107
|
+
if ("enabled" in rawObject) mergedBase.enabled = overrideOptions.enabled;
|
|
1108
|
+
if ("mode" in rawObject) mergedBase.mode = overrideOptions.mode;
|
|
1109
|
+
if ("includeSpecs" in rawObject)
|
|
1110
|
+
mergedBase.includeSpecs = overrideOptions.includeSpecs;
|
|
1111
|
+
if ("dependencies" in rawObject)
|
|
1112
|
+
mergedBase.dependencies = [...overrideOptions.dependencies];
|
|
1113
|
+
if ("include" in rawObject) mergedBase.include = [...overrideOptions.include];
|
|
1114
|
+
if ("exclude" in rawObject) mergedBase.exclude = [...overrideOptions.exclude];
|
|
1115
|
+
if (
|
|
1116
|
+
rawObject.ignore &&
|
|
1117
|
+
typeof rawObject.ignore == "object" &&
|
|
1118
|
+
!Array.isArray(rawObject.ignore)
|
|
1119
|
+
) {
|
|
1120
|
+
mergedBase.ignore = mergeCoverageIgnoreOptions(
|
|
1121
|
+
mergedBase.ignore,
|
|
1122
|
+
overrideOptions.ignore,
|
|
1123
|
+
rawObject.ignore,
|
|
1124
|
+
);
|
|
1125
|
+
}
|
|
1126
|
+
return mergedBase;
|
|
1074
1127
|
}
|
|
1075
1128
|
function mergeReporterConfigByRaw(base, override, raw) {
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
if ("outDir" in rawObject)
|
|
1093
|
-
mergedBase.outDir = overrideConfig.outDir;
|
|
1094
|
-
if ("outFile" in rawObject)
|
|
1095
|
-
mergedBase.outFile = overrideConfig.outFile;
|
|
1096
|
-
return mergedBase;
|
|
1129
|
+
if (typeof raw == "string") return override;
|
|
1130
|
+
if (!raw || typeof raw != "object" || Array.isArray(raw)) {
|
|
1131
|
+
return cloneReporterConfig(base);
|
|
1132
|
+
}
|
|
1133
|
+
const mergedBase =
|
|
1134
|
+
typeof base == "string" ? new ReporterConfig() : cloneReporterConfig(base);
|
|
1135
|
+
const overrideConfig =
|
|
1136
|
+
typeof override == "string"
|
|
1137
|
+
? new ReporterConfig()
|
|
1138
|
+
: cloneReporterConfig(override);
|
|
1139
|
+
const rawObject = raw;
|
|
1140
|
+
if ("name" in rawObject) mergedBase.name = overrideConfig.name;
|
|
1141
|
+
if ("options" in rawObject) mergedBase.options = [...overrideConfig.options];
|
|
1142
|
+
if ("outDir" in rawObject) mergedBase.outDir = overrideConfig.outDir;
|
|
1143
|
+
if ("outFile" in rawObject) mergedBase.outFile = overrideConfig.outFile;
|
|
1144
|
+
return mergedBase;
|
|
1097
1145
|
}
|
|
1098
1146
|
function mergeBuildOptions(base, override, raw) {
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
merged.env = { ...override.env };
|
|
1108
|
-
}
|
|
1109
|
-
return merged;
|
|
1147
|
+
const merged = cloneBuildOptions(base);
|
|
1148
|
+
if ("cmd" in raw) merged.cmd = override.cmd;
|
|
1149
|
+
if ("args" in raw) merged.args = [...override.args];
|
|
1150
|
+
if ("target" in raw) merged.target = override.target;
|
|
1151
|
+
if ("env" in raw) {
|
|
1152
|
+
merged.env = { ...override.env };
|
|
1153
|
+
}
|
|
1154
|
+
return merged;
|
|
1110
1155
|
}
|
|
1111
1156
|
function mergeRunOptions(base, override, raw) {
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1157
|
+
const merged = cloneRunOptions(base);
|
|
1158
|
+
if ("runtime" in raw || "run" in raw) {
|
|
1159
|
+
const runtimeRaw = raw.runtime;
|
|
1160
|
+
if (
|
|
1161
|
+
"run" in raw ||
|
|
1162
|
+
(runtimeRaw && ("cmd" in runtimeRaw || "run" in runtimeRaw))
|
|
1163
|
+
) {
|
|
1164
|
+
merged.runtime.cmd = override.runtime.cmd;
|
|
1165
|
+
}
|
|
1166
|
+
if (runtimeRaw && "browser" in runtimeRaw) {
|
|
1167
|
+
merged.runtime.browser = override.runtime.browser;
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
if ("reporter" in raw) {
|
|
1171
|
+
merged.reporter = mergeReporterConfigByRaw(
|
|
1172
|
+
merged.reporter,
|
|
1173
|
+
override.reporter,
|
|
1174
|
+
raw.reporter,
|
|
1175
|
+
);
|
|
1176
|
+
}
|
|
1177
|
+
if ("env" in raw) {
|
|
1178
|
+
merged.env = { ...override.env };
|
|
1179
|
+
}
|
|
1180
|
+
return merged;
|
|
1130
1181
|
}
|
|
1131
1182
|
function mergeFuzzConfig(base, override, raw) {
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
if ("target" in raw)
|
|
1142
|
-
merged.target = override.target;
|
|
1143
|
-
if ("corpusDir" in raw)
|
|
1144
|
-
merged.corpusDir = override.corpusDir;
|
|
1145
|
-
if ("crashDir" in raw)
|
|
1146
|
-
merged.crashDir = override.crashDir;
|
|
1147
|
-
return merged;
|
|
1183
|
+
const merged = cloneFuzzConfig(base);
|
|
1184
|
+
if ("input" in raw) merged.input = [...override.input];
|
|
1185
|
+
if ("runs" in raw) merged.runs = override.runs;
|
|
1186
|
+
if ("seed" in raw) merged.seed = override.seed;
|
|
1187
|
+
if ("maxInputBytes" in raw) merged.maxInputBytes = override.maxInputBytes;
|
|
1188
|
+
if ("target" in raw) merged.target = override.target;
|
|
1189
|
+
if ("corpusDir" in raw) merged.corpusDir = override.corpusDir;
|
|
1190
|
+
if ("crashDir" in raw) merged.crashDir = override.crashDir;
|
|
1191
|
+
return merged;
|
|
1148
1192
|
}
|
|
1149
1193
|
function mergeRootConfig(base, override) {
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1194
|
+
const merged = cloneConfig(base);
|
|
1195
|
+
const raw = getConfigMeta(override).raw;
|
|
1196
|
+
if ("$schema" in raw) merged.$schema = override.$schema;
|
|
1197
|
+
if ("input" in raw) merged.input = [...override.input];
|
|
1198
|
+
if (outputOverridesField(raw, "outDir")) merged.outDir = override.outDir;
|
|
1199
|
+
if (outputOverridesField(raw, "logs")) merged.logs = override.logs;
|
|
1200
|
+
if (outputOverridesField(raw, "coverageDir")) {
|
|
1201
|
+
merged.coverageDir = override.coverageDir;
|
|
1202
|
+
}
|
|
1203
|
+
if (outputOverridesField(raw, "snapshotDir")) {
|
|
1204
|
+
merged.snapshotDir = override.snapshotDir;
|
|
1205
|
+
}
|
|
1206
|
+
if ("config" in raw) merged.config = override.config;
|
|
1207
|
+
if ("coverage" in raw) {
|
|
1208
|
+
merged.coverage = mergeCoverageConfig(
|
|
1209
|
+
merged.coverage,
|
|
1210
|
+
override.coverage,
|
|
1211
|
+
raw.coverage,
|
|
1212
|
+
);
|
|
1213
|
+
}
|
|
1214
|
+
if ("env" in raw) {
|
|
1215
|
+
merged.env = { ...override.env };
|
|
1216
|
+
}
|
|
1217
|
+
if (
|
|
1218
|
+
raw.buildOptions &&
|
|
1219
|
+
typeof raw.buildOptions == "object" &&
|
|
1220
|
+
!Array.isArray(raw.buildOptions)
|
|
1221
|
+
) {
|
|
1222
|
+
merged.buildOptions = mergeBuildOptions(
|
|
1223
|
+
merged.buildOptions,
|
|
1224
|
+
override.buildOptions,
|
|
1225
|
+
raw.buildOptions,
|
|
1226
|
+
);
|
|
1227
|
+
}
|
|
1228
|
+
if (
|
|
1229
|
+
raw.runOptions &&
|
|
1230
|
+
typeof raw.runOptions == "object" &&
|
|
1231
|
+
!Array.isArray(raw.runOptions)
|
|
1232
|
+
) {
|
|
1233
|
+
merged.runOptions = mergeRunOptions(
|
|
1234
|
+
merged.runOptions,
|
|
1235
|
+
override.runOptions,
|
|
1236
|
+
raw.runOptions,
|
|
1237
|
+
);
|
|
1238
|
+
}
|
|
1239
|
+
if (raw.fuzz && typeof raw.fuzz == "object" && !Array.isArray(raw.fuzz)) {
|
|
1240
|
+
merged.fuzz = mergeFuzzConfig(merged.fuzz, override.fuzz, raw.fuzz);
|
|
1241
|
+
}
|
|
1242
|
+
CONFIG_META.set(merged, getConfigMeta(override));
|
|
1243
|
+
return merged;
|
|
1189
1244
|
}
|
|
1190
1245
|
function applyPerModeOutputDefaults(base, merged, override, modeName) {
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1246
|
+
const raw = getConfigMeta(override).raw;
|
|
1247
|
+
if (!outputOverridesField(raw, "outDir")) {
|
|
1248
|
+
merged.outDir = appendPathSegment(base.outDir, modeName);
|
|
1249
|
+
}
|
|
1250
|
+
if (!outputOverridesField(raw, "logs") && base.logs != "none") {
|
|
1251
|
+
merged.logs = appendPathSegment(base.logs, modeName);
|
|
1252
|
+
}
|
|
1253
|
+
if (!outputOverridesField(raw, "coverageDir") && base.coverageDir != "none") {
|
|
1254
|
+
merged.coverageDir = appendPathSegment(base.coverageDir, modeName);
|
|
1255
|
+
}
|
|
1201
1256
|
}
|
|
1202
1257
|
function resolveModeOverrideConfig(root, modeName) {
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
}
|
|
1212
|
-
|
|
1213
|
-
}
|
|
1214
|
-
return
|
|
1258
|
+
const mode = root.modes[modeName];
|
|
1259
|
+
if (!mode) {
|
|
1260
|
+
throw new Error(`unknown mode "${modeName}"`);
|
|
1261
|
+
}
|
|
1262
|
+
if (mode.path) {
|
|
1263
|
+
const override = loadConfig(mode.path, false);
|
|
1264
|
+
if (Object.keys(override.modes).length) {
|
|
1265
|
+
throw new Error(
|
|
1266
|
+
`mode "${modeName}" config file cannot declare nested modes`,
|
|
1267
|
+
);
|
|
1268
|
+
}
|
|
1269
|
+
return override;
|
|
1270
|
+
}
|
|
1271
|
+
return cloneConfig(mode.config);
|
|
1215
1272
|
}
|
|
1216
1273
|
export function resolveModeNames(rawArgs) {
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
return [...new Set(names)];
|
|
1274
|
+
const names = [];
|
|
1275
|
+
for (let i = 0; i < rawArgs.length; i++) {
|
|
1276
|
+
const arg = rawArgs[i];
|
|
1277
|
+
if (arg == "--mode") {
|
|
1278
|
+
const next = rawArgs[i + 1];
|
|
1279
|
+
if (!next || next.startsWith("-")) continue;
|
|
1280
|
+
i++;
|
|
1281
|
+
appendModeTokens(names, next);
|
|
1282
|
+
continue;
|
|
1283
|
+
}
|
|
1284
|
+
if (arg.startsWith("--mode=")) {
|
|
1285
|
+
appendModeTokens(names, arg.slice("--mode=".length));
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
return [...new Set(names)];
|
|
1233
1289
|
}
|
|
1234
1290
|
export function getDefaultModeNames(config) {
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1291
|
+
return Object.entries(config.modes)
|
|
1292
|
+
.filter(([, mode]) => mode.default !== false)
|
|
1293
|
+
.map(([name]) => name);
|
|
1238
1294
|
}
|
|
1239
1295
|
function appendModeTokens(out, value) {
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
}
|
|
1296
|
+
for (const token of value.split(",")) {
|
|
1297
|
+
const mode = token.trim();
|
|
1298
|
+
if (!mode.length) continue;
|
|
1299
|
+
out.push(mode);
|
|
1300
|
+
}
|
|
1246
1301
|
}
|
|
1247
1302
|
export function applyMode(config, modeName) {
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
}
|
|
1254
|
-
if (config.coverageDir != "none") {
|
|
1255
|
-
merged.coverageDir = appendPathSegment(config.coverageDir, "default");
|
|
1256
|
-
}
|
|
1257
|
-
merged.fuzz.crashDir = appendPathSegment(config.fuzz.crashDir, "default");
|
|
1258
|
-
merged.fuzz.corpusDir = appendPathSegment(config.fuzz.corpusDir, "default");
|
|
1259
|
-
const env = {
|
|
1260
|
-
...process.env,
|
|
1261
|
-
...config.env,
|
|
1262
|
-
};
|
|
1263
|
-
if (merged.runOptions.runtime.browser.length) {
|
|
1264
|
-
env.BROWSER = merged.runOptions.runtime.browser;
|
|
1265
|
-
}
|
|
1266
|
-
return {
|
|
1267
|
-
config: merged,
|
|
1268
|
-
env,
|
|
1269
|
-
};
|
|
1303
|
+
if (!modeName) {
|
|
1304
|
+
const merged = cloneConfig(config);
|
|
1305
|
+
merged.outDir = appendPathSegment(config.outDir, "default");
|
|
1306
|
+
if (config.logs != "none") {
|
|
1307
|
+
merged.logs = appendPathSegment(config.logs, "default");
|
|
1270
1308
|
}
|
|
1271
|
-
if (
|
|
1272
|
-
|
|
1273
|
-
const available = known.length ? known.join(", ") : "(none)";
|
|
1274
|
-
throw new Error(`unknown mode "${modeName}". Available modes: ${available}`);
|
|
1309
|
+
if (config.coverageDir != "none") {
|
|
1310
|
+
merged.coverageDir = appendPathSegment(config.coverageDir, "default");
|
|
1275
1311
|
}
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
applyPerModeOutputDefaults(config, merged, modeOverride, modeName);
|
|
1312
|
+
merged.fuzz.crashDir = appendPathSegment(config.fuzz.crashDir, "default");
|
|
1313
|
+
merged.fuzz.corpusDir = appendPathSegment(config.fuzz.corpusDir, "default");
|
|
1279
1314
|
const env = {
|
|
1280
|
-
|
|
1281
|
-
|
|
1315
|
+
...process.env,
|
|
1316
|
+
...config.env,
|
|
1282
1317
|
};
|
|
1283
1318
|
if (merged.runOptions.runtime.browser.length) {
|
|
1284
|
-
|
|
1319
|
+
env.BROWSER = merged.runOptions.runtime.browser;
|
|
1285
1320
|
}
|
|
1286
1321
|
return {
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
modeName,
|
|
1322
|
+
config: merged,
|
|
1323
|
+
env,
|
|
1290
1324
|
};
|
|
1325
|
+
}
|
|
1326
|
+
if (!config.modes[modeName]) {
|
|
1327
|
+
const known = Object.keys(config.modes);
|
|
1328
|
+
const available = known.length ? known.join(", ") : "(none)";
|
|
1329
|
+
throw new Error(
|
|
1330
|
+
`unknown mode "${modeName}". Available modes: ${available}`,
|
|
1331
|
+
);
|
|
1332
|
+
}
|
|
1333
|
+
const modeOverride = resolveModeOverrideConfig(config, modeName);
|
|
1334
|
+
const merged = mergeRootConfig(config, modeOverride);
|
|
1335
|
+
applyPerModeOutputDefaults(config, merged, modeOverride, modeName);
|
|
1336
|
+
const env = {
|
|
1337
|
+
...process.env,
|
|
1338
|
+
...merged.env,
|
|
1339
|
+
};
|
|
1340
|
+
if (merged.runOptions.runtime.browser.length) {
|
|
1341
|
+
env.BROWSER = merged.runOptions.runtime.browser;
|
|
1342
|
+
}
|
|
1343
|
+
return {
|
|
1344
|
+
config: merged,
|
|
1345
|
+
env,
|
|
1346
|
+
modeName,
|
|
1347
|
+
};
|
|
1291
1348
|
}
|
|
1292
1349
|
function appendPathSegment(basePath, segment) {
|
|
1293
|
-
|
|
1350
|
+
return join(basePath, segment);
|
|
1294
1351
|
}
|
|
1295
1352
|
export function getCliVersion() {
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
}
|
|
1308
|
-
catch {
|
|
1309
|
-
// ignore invalid package metadata and continue to fallback candidate
|
|
1310
|
-
}
|
|
1353
|
+
const candidates = [
|
|
1354
|
+
join(dirname(fileURLToPath(import.meta.url)), "..", "package.json"),
|
|
1355
|
+
join(process.cwd(), "package.json"),
|
|
1356
|
+
];
|
|
1357
|
+
for (const pkgPath of candidates) {
|
|
1358
|
+
if (!existsSync(pkgPath)) continue;
|
|
1359
|
+
try {
|
|
1360
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
1361
|
+
if (pkg.version) return pkg.version;
|
|
1362
|
+
} catch {
|
|
1363
|
+
// ignore invalid package metadata and continue to fallback candidate
|
|
1311
1364
|
}
|
|
1312
|
-
|
|
1365
|
+
}
|
|
1366
|
+
return "0.0.0";
|
|
1313
1367
|
}
|
|
1314
1368
|
export function getPkgRunner() {
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
if (userAgent.startsWith("bun"))
|
|
1321
|
-
return "bunx";
|
|
1322
|
-
return "npx";
|
|
1369
|
+
const userAgent = process.env.npm_config_user_agent ?? "";
|
|
1370
|
+
if (userAgent.startsWith("pnpm")) return "pnpx";
|
|
1371
|
+
if (userAgent.startsWith("yarn")) return "yarn";
|
|
1372
|
+
if (userAgent.startsWith("bun")) return "bunx";
|
|
1373
|
+
return "npx";
|
|
1323
1374
|
}
|
|
1324
1375
|
export function getExec(exec) {
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1376
|
+
const PATH = (process.env.PATH ?? "").split(delimiter);
|
|
1377
|
+
for (const pathDir of PATH) {
|
|
1378
|
+
const fullPath = join(
|
|
1379
|
+
pathDir,
|
|
1380
|
+
exec + (process.platform === "win32" ? ".exe" : ""),
|
|
1381
|
+
);
|
|
1382
|
+
if (existsSync(fullPath)) {
|
|
1383
|
+
return fullPath;
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
return null;
|
|
1333
1387
|
}
|
|
1334
1388
|
export function tokenizeCommand(command) {
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
if (escaping) {
|
|
1342
|
-
current += ch;
|
|
1343
|
-
escaping = false;
|
|
1344
|
-
continue;
|
|
1345
|
-
}
|
|
1346
|
-
if (ch == "\\") {
|
|
1347
|
-
if (quote == "'") {
|
|
1348
|
-
current += ch;
|
|
1349
|
-
}
|
|
1350
|
-
else {
|
|
1351
|
-
escaping = true;
|
|
1352
|
-
}
|
|
1353
|
-
continue;
|
|
1354
|
-
}
|
|
1355
|
-
if (quote) {
|
|
1356
|
-
if (ch == quote) {
|
|
1357
|
-
quote = null;
|
|
1358
|
-
}
|
|
1359
|
-
else {
|
|
1360
|
-
current += ch;
|
|
1361
|
-
}
|
|
1362
|
-
continue;
|
|
1363
|
-
}
|
|
1364
|
-
if (ch == '"' || ch == "'") {
|
|
1365
|
-
quote = ch;
|
|
1366
|
-
continue;
|
|
1367
|
-
}
|
|
1368
|
-
if (/\s/.test(ch)) {
|
|
1369
|
-
if (current.length) {
|
|
1370
|
-
out.push(current);
|
|
1371
|
-
current = "";
|
|
1372
|
-
}
|
|
1373
|
-
continue;
|
|
1374
|
-
}
|
|
1375
|
-
current += ch;
|
|
1376
|
-
}
|
|
1389
|
+
const out = [];
|
|
1390
|
+
let current = "";
|
|
1391
|
+
let quote = null;
|
|
1392
|
+
let escaping = false;
|
|
1393
|
+
for (let i = 0; i < command.length; i++) {
|
|
1394
|
+
const ch = command[i];
|
|
1377
1395
|
if (escaping) {
|
|
1378
|
-
|
|
1396
|
+
current += ch;
|
|
1397
|
+
escaping = false;
|
|
1398
|
+
continue;
|
|
1399
|
+
}
|
|
1400
|
+
if (ch == "\\") {
|
|
1401
|
+
if (quote == "'") {
|
|
1402
|
+
current += ch;
|
|
1403
|
+
} else {
|
|
1404
|
+
escaping = true;
|
|
1405
|
+
}
|
|
1406
|
+
continue;
|
|
1379
1407
|
}
|
|
1380
1408
|
if (quote) {
|
|
1381
|
-
|
|
1409
|
+
if (ch == quote) {
|
|
1410
|
+
quote = null;
|
|
1411
|
+
} else {
|
|
1412
|
+
current += ch;
|
|
1413
|
+
}
|
|
1414
|
+
continue;
|
|
1382
1415
|
}
|
|
1383
|
-
if (
|
|
1384
|
-
|
|
1416
|
+
if (ch == '"' || ch == "'") {
|
|
1417
|
+
quote = ch;
|
|
1418
|
+
continue;
|
|
1385
1419
|
}
|
|
1386
|
-
|
|
1420
|
+
if (/\s/.test(ch)) {
|
|
1421
|
+
if (current.length) {
|
|
1422
|
+
out.push(current);
|
|
1423
|
+
current = "";
|
|
1424
|
+
}
|
|
1425
|
+
continue;
|
|
1426
|
+
}
|
|
1427
|
+
current += ch;
|
|
1428
|
+
}
|
|
1429
|
+
if (escaping) {
|
|
1430
|
+
current += "\\";
|
|
1431
|
+
}
|
|
1432
|
+
if (quote) {
|
|
1433
|
+
throw new Error(`unterminated quote in command: ${command}`);
|
|
1434
|
+
}
|
|
1435
|
+
if (current.length) {
|
|
1436
|
+
out.push(current);
|
|
1437
|
+
}
|
|
1438
|
+
return out;
|
|
1387
1439
|
}
|
|
1388
1440
|
export function resolveProjectModule(specifier) {
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
// try next resolver
|
|
1397
|
-
}
|
|
1441
|
+
const cwdRequire = createRequire(join(process.cwd(), "package.json"));
|
|
1442
|
+
const localRequire = createRequire(import.meta.url);
|
|
1443
|
+
for (const req of [cwdRequire, localRequire]) {
|
|
1444
|
+
try {
|
|
1445
|
+
return req.resolve(specifier);
|
|
1446
|
+
} catch {
|
|
1447
|
+
// try next resolver
|
|
1398
1448
|
}
|
|
1399
|
-
|
|
1449
|
+
}
|
|
1450
|
+
return null;
|
|
1400
1451
|
}
|