as-test 1.0.12 → 1.0.14
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 +43 -0
- package/as-test.config.schema.json +71 -5
- package/assembly/as-test.intellisense.d.ts +8 -0
- package/assembly/src/fuzz.ts +277 -10
- package/bin/commands/build-core.js +5 -3
- package/bin/commands/fuzz-core.js +46 -14
- package/bin/commands/fuzz.js +2 -1
- package/bin/commands/run-core.js +144 -3
- package/bin/commands/run.js +2 -1
- package/bin/commands/test.js +3 -1
- package/bin/index.js +138 -49
- package/bin/reporters/default.js +18 -8
- package/bin/types.js +1 -3
- package/bin/util.js +347 -256
- package/package.json +8 -8
- package/transform/lib/builder.js +14 -15
- package/transform/lib/coverage.js +11 -12
- package/transform/lib/index.js +0 -1
- package/transform/lib/linker.js +3 -4
- package/transform/lib/location.js +0 -1
- package/transform/lib/log.js +0 -1
- package/transform/lib/mock.js +15 -9
- package/transform/lib/range.js +0 -1
- package/transform/lib/types.js +0 -1
- package/transform/lib/util.js +0 -1
- package/transform/lib/visitor.js +64 -65
package/bin/index.js
CHANGED
|
@@ -62,6 +62,7 @@ else if (COMMANDS.includes(args[0])) {
|
|
|
62
62
|
else if (command === "run") {
|
|
63
63
|
executeRunCommand(_args, flags, configPath, selectedModes, {
|
|
64
64
|
resolveCommandArgs,
|
|
65
|
+
resolveSuiteSelectors,
|
|
65
66
|
resolveListFlags,
|
|
66
67
|
resolveFeatureToggles,
|
|
67
68
|
resolveParallelJobs,
|
|
@@ -78,6 +79,8 @@ else if (COMMANDS.includes(args[0])) {
|
|
|
78
79
|
else if (command === "test") {
|
|
79
80
|
executeTestCommand(_args, flags, configPath, selectedModes, {
|
|
80
81
|
resolveCommandArgs,
|
|
82
|
+
resolveSuiteSelectors,
|
|
83
|
+
resolveFuzzerSelectors,
|
|
81
84
|
resolveListFlags,
|
|
82
85
|
resolveFeatureToggles,
|
|
83
86
|
resolveParallelJobs,
|
|
@@ -95,6 +98,7 @@ else if (COMMANDS.includes(args[0])) {
|
|
|
95
98
|
else if (command === "fuzz") {
|
|
96
99
|
executeFuzzCommand(_args, configPath, selectedModes, {
|
|
97
100
|
resolveCommandArgs,
|
|
101
|
+
resolveFuzzerSelectors,
|
|
98
102
|
resolveListFlags,
|
|
99
103
|
resolveJobs,
|
|
100
104
|
resolveExecutionModes,
|
|
@@ -252,6 +256,8 @@ function printCommandHelp(command) {
|
|
|
252
256
|
process.stdout.write(" --overwrite-snapshots Overwrite existing snapshot entries on mismatch\n");
|
|
253
257
|
process.stdout.write(" --no-snapshot Disable snapshot assertions for this run\n");
|
|
254
258
|
process.stdout.write(" --show-coverage Print uncovered coverage point details\n");
|
|
259
|
+
process.stdout.write(" --suite <name[,name...]> Filter results to matching suite names or suite slug paths\n");
|
|
260
|
+
process.stdout.write(" --suites <name[,name...]> Alias for --suite\n");
|
|
255
261
|
process.stdout.write(" --enable <feature> Enable feature (coverage|try-as)\n");
|
|
256
262
|
process.stdout.write(" --disable <feature> Disable feature (coverage|try-as)\n");
|
|
257
263
|
process.stdout.write(" --reporter <name|path> Use built-in reporter (default|tap) or custom module path\n");
|
|
@@ -278,6 +284,8 @@ function printCommandHelp(command) {
|
|
|
278
284
|
process.stdout.write(" --overwrite-snapshots Overwrite existing snapshot entries on mismatch\n");
|
|
279
285
|
process.stdout.write(" --no-snapshot Disable snapshot assertions for this run\n");
|
|
280
286
|
process.stdout.write(" --show-coverage Print uncovered coverage point details\n");
|
|
287
|
+
process.stdout.write(" --suite <name[,name...]> Filter results to matching suite names or suite slug paths\n");
|
|
288
|
+
process.stdout.write(" --suites <name[,name...]> Alias for --suite\n");
|
|
281
289
|
process.stdout.write(" --enable <feature> Enable feature (coverage|try-as)\n");
|
|
282
290
|
process.stdout.write(" --disable <feature> Disable feature (coverage|try-as)\n");
|
|
283
291
|
process.stdout.write(" --fuzz Run fuzz targets after the normal test pass\n");
|
|
@@ -304,6 +312,10 @@ function printCommandHelp(command) {
|
|
|
304
312
|
process.stdout.write(" --mode <name[,name...]> Run one or multiple named config modes\n");
|
|
305
313
|
process.stdout.write(" --runs <value> Override fuzz iteration count, e.g. 500, 1.5x, +10%, +100000\n");
|
|
306
314
|
process.stdout.write(" --seed <n> Pin fuzz seed (default uses random seed)\n");
|
|
315
|
+
process.stdout.write(" --fuzzer <name[,name...]> Filter results to matching fuzz target names\n");
|
|
316
|
+
process.stdout.write(" --fuzzers <name[,name...]> Alias for --fuzzer\n");
|
|
317
|
+
process.stdout.write(" --suite <name[,name...]> Alias for --fuzzer\n");
|
|
318
|
+
process.stdout.write(" --suites <name[,name...]> Alias for --fuzzer\n");
|
|
307
319
|
process.stdout.write(" --jobs <n> Run files through an ordered worker pool\n");
|
|
308
320
|
process.stdout.write(" --build-jobs <n> Limit concurrent build tasks (defaults to --jobs)\n");
|
|
309
321
|
process.stdout.write(" --run-jobs <n> Limit concurrent run tasks (defaults to --jobs)\n");
|
|
@@ -384,9 +396,22 @@ function resolveCommandArgs(rawArgs, command) {
|
|
|
384
396
|
i++;
|
|
385
397
|
continue;
|
|
386
398
|
}
|
|
399
|
+
if (arg == "--suite" ||
|
|
400
|
+
arg == "--suites" ||
|
|
401
|
+
arg == "--fuzzer" ||
|
|
402
|
+
arg == "--fuzzers") {
|
|
403
|
+
i++;
|
|
404
|
+
continue;
|
|
405
|
+
}
|
|
387
406
|
if (arg.startsWith("--reporter=")) {
|
|
388
407
|
continue;
|
|
389
408
|
}
|
|
409
|
+
if (arg.startsWith("--suite=") ||
|
|
410
|
+
arg.startsWith("--suites=") ||
|
|
411
|
+
arg.startsWith("--fuzzer=") ||
|
|
412
|
+
arg.startsWith("--fuzzers=")) {
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
390
415
|
if (arg == "--tap") {
|
|
391
416
|
continue;
|
|
392
417
|
}
|
|
@@ -500,6 +525,47 @@ function resolveFuzzOverrides(rawArgs, command) {
|
|
|
500
525
|
}
|
|
501
526
|
return out;
|
|
502
527
|
}
|
|
528
|
+
function resolveSuiteSelectors(rawArgs, command) {
|
|
529
|
+
return resolveNamedSelectors(rawArgs, command, ["--suite", "--suites"]);
|
|
530
|
+
}
|
|
531
|
+
function resolveFuzzerSelectors(rawArgs, command) {
|
|
532
|
+
const flags = command == "fuzz"
|
|
533
|
+
? ["--fuzzer", "--fuzzers", "--suite", "--suites"]
|
|
534
|
+
: ["--fuzzer", "--fuzzers"];
|
|
535
|
+
return resolveNamedSelectors(rawArgs, command, flags);
|
|
536
|
+
}
|
|
537
|
+
function resolveNamedSelectors(rawArgs, command, flags) {
|
|
538
|
+
const out = [];
|
|
539
|
+
let seenCommand = false;
|
|
540
|
+
for (let i = 0; i < rawArgs.length; i++) {
|
|
541
|
+
const arg = rawArgs[i];
|
|
542
|
+
if (!seenCommand) {
|
|
543
|
+
if (arg == command)
|
|
544
|
+
seenCommand = true;
|
|
545
|
+
continue;
|
|
546
|
+
}
|
|
547
|
+
let parsed = null;
|
|
548
|
+
for (const flag of flags) {
|
|
549
|
+
parsed = parseStringFlag(rawArgs, i, flag);
|
|
550
|
+
if (parsed)
|
|
551
|
+
break;
|
|
552
|
+
}
|
|
553
|
+
if (!parsed)
|
|
554
|
+
continue;
|
|
555
|
+
appendNamedSelectorTokens(out, parsed.value);
|
|
556
|
+
if (parsed.consumeNext)
|
|
557
|
+
i++;
|
|
558
|
+
}
|
|
559
|
+
return [...new Set(out)];
|
|
560
|
+
}
|
|
561
|
+
function appendNamedSelectorTokens(out, value) {
|
|
562
|
+
for (const token of value.split(",")) {
|
|
563
|
+
const normalized = token.trim();
|
|
564
|
+
if (!normalized.length)
|
|
565
|
+
continue;
|
|
566
|
+
out.push(normalized);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
503
569
|
function parseFuzzRunsFlag(rawArgs, index, flag) {
|
|
504
570
|
const arg = rawArgs[index];
|
|
505
571
|
let value = "";
|
|
@@ -996,7 +1062,7 @@ async function buildFileForMode(cache, args) {
|
|
|
996
1062
|
}
|
|
997
1063
|
return false;
|
|
998
1064
|
}
|
|
999
|
-
async function runTestSequential(runFlags, configPath, selectors, buildFeatureToggles, modeSummaryTotal, fileSummaryTotal, allowNoSpecFiles = false, modeName, reporterOverride, emitRunComplete = true) {
|
|
1065
|
+
async function runTestSequential(runFlags, configPath, selectors, suiteSelectors, buildFeatureToggles, modeSummaryTotal, fileSummaryTotal, allowNoSpecFiles = false, modeName, reporterOverride, emitRunComplete = true) {
|
|
1000
1066
|
const files = await resolveSelectedFiles(configPath, selectors);
|
|
1001
1067
|
if (!files.length) {
|
|
1002
1068
|
if (!allowNoSpecFiles) {
|
|
@@ -1025,6 +1091,7 @@ async function runTestSequential(runFlags, configPath, selectors, buildFeatureTo
|
|
|
1025
1091
|
const artifactKey = resolvePerFileArtifactKey(file, duplicateSpecBasenames);
|
|
1026
1092
|
const result = await run(runFlags, configPath, [file], false, {
|
|
1027
1093
|
reporter,
|
|
1094
|
+
suiteSelectors,
|
|
1028
1095
|
emitRunStart: false,
|
|
1029
1096
|
emitRunComplete: false,
|
|
1030
1097
|
logFileName: `test.${artifactKey}.log.json`,
|
|
@@ -1113,7 +1180,7 @@ async function runBuildModes(configPath, selectors, modes, buildFeatureToggles,
|
|
|
1113
1180
|
}
|
|
1114
1181
|
process.stdout.write(`${chalk.bold("Summary:")} built ${builtCount} file(s) across ${modes.length || 1} mode(s) in ${formatTime(Date.now() - allStartedAt)}\n`);
|
|
1115
1182
|
}
|
|
1116
|
-
async function runRuntimeModes(runFlags, configPath, selectors, modes) {
|
|
1183
|
+
async function runRuntimeModes(runFlags, configPath, selectors, suiteSelectors, modes) {
|
|
1117
1184
|
await ensureWebBrowsersReady(configPath, modes, runFlags.browser);
|
|
1118
1185
|
const modeSummaryTotal = Math.max(modes.length, 1);
|
|
1119
1186
|
const fileSummaryTotal = await resolveConfiguredFileTotal(configPath);
|
|
@@ -1123,13 +1190,13 @@ async function runRuntimeModes(runFlags, configPath, selectors, modes) {
|
|
|
1123
1190
|
};
|
|
1124
1191
|
if (effectiveRunFlags.jobs > 1) {
|
|
1125
1192
|
if (modes.length > 1) {
|
|
1126
|
-
const failed = await runRuntimeMatrixParallel(effectiveRunFlags, configPath, selectors, modes, modeSummaryTotal, fileSummaryTotal);
|
|
1193
|
+
const failed = await runRuntimeMatrixParallel(effectiveRunFlags, configPath, selectors, suiteSelectors, modes, modeSummaryTotal, fileSummaryTotal);
|
|
1127
1194
|
process.exit(failed ? 1 : 0);
|
|
1128
1195
|
return;
|
|
1129
1196
|
}
|
|
1130
1197
|
let failed = false;
|
|
1131
1198
|
for (const modeName of modes) {
|
|
1132
|
-
const result = await runRuntimeSingleParallel(effectiveRunFlags, configPath, selectors, modeName, modeSummaryTotal, fileSummaryTotal);
|
|
1199
|
+
const result = await runRuntimeSingleParallel(effectiveRunFlags, configPath, selectors, suiteSelectors, modeName, modeSummaryTotal, fileSummaryTotal);
|
|
1133
1200
|
if (result)
|
|
1134
1201
|
failed = true;
|
|
1135
1202
|
}
|
|
@@ -1137,7 +1204,7 @@ async function runRuntimeModes(runFlags, configPath, selectors, modes) {
|
|
|
1137
1204
|
return;
|
|
1138
1205
|
}
|
|
1139
1206
|
if (modes.length > 1) {
|
|
1140
|
-
const failed = await runRuntimeMatrix(effectiveRunFlags, configPath, selectors, modes, modeSummaryTotal, fileSummaryTotal);
|
|
1207
|
+
const failed = await runRuntimeMatrix(effectiveRunFlags, configPath, selectors, suiteSelectors, modes, modeSummaryTotal, fileSummaryTotal);
|
|
1141
1208
|
process.exit(failed ? 1 : 0);
|
|
1142
1209
|
return;
|
|
1143
1210
|
}
|
|
@@ -1147,6 +1214,7 @@ async function runRuntimeModes(runFlags, configPath, selectors, modes) {
|
|
|
1147
1214
|
const result = await run(effectiveRunFlags, configPath, selectors, false, {
|
|
1148
1215
|
reporterPath: effectiveRunFlags.reporterPath,
|
|
1149
1216
|
modeName,
|
|
1217
|
+
suiteSelectors,
|
|
1150
1218
|
modeSummaryTotal,
|
|
1151
1219
|
modeSummaryExecuted: 1,
|
|
1152
1220
|
fileSummaryTotal,
|
|
@@ -1157,7 +1225,7 @@ async function runRuntimeModes(runFlags, configPath, selectors, modes) {
|
|
|
1157
1225
|
}
|
|
1158
1226
|
process.exit(failed ? 1 : 0);
|
|
1159
1227
|
}
|
|
1160
|
-
async function runRuntimeMatrix(runFlags, configPath, selectors, modes, modeSummaryTotal, fileSummaryTotal) {
|
|
1228
|
+
async function runRuntimeMatrix(runFlags, configPath, selectors, suiteSelectors, modes, modeSummaryTotal, fileSummaryTotal) {
|
|
1161
1229
|
const files = await resolveSelectedFiles(configPath, selectors);
|
|
1162
1230
|
if (!files.length) {
|
|
1163
1231
|
throw await buildNoTestFilesMatchedError(configPath, selectors);
|
|
@@ -1205,6 +1273,7 @@ async function runRuntimeMatrix(runFlags, configPath, selectors, modes, modeSumm
|
|
|
1205
1273
|
const result = await run(runFlags, configPath, [file], false, {
|
|
1206
1274
|
reporter: silentReporter,
|
|
1207
1275
|
reporterKind: "default",
|
|
1276
|
+
suiteSelectors,
|
|
1208
1277
|
emitRunStart: false,
|
|
1209
1278
|
emitRunComplete: false,
|
|
1210
1279
|
logFileName: `run.${artifactKey}.log.json`,
|
|
@@ -1256,7 +1325,7 @@ async function runRuntimeMatrix(runFlags, configPath, selectors, modes, modeSumm
|
|
|
1256
1325
|
});
|
|
1257
1326
|
return allResults.some((result) => result.failed);
|
|
1258
1327
|
}
|
|
1259
|
-
async function runTestModes(runFlags, configPath, selectors, modes, buildFeatureToggles, fuzzEnabled, fuzzOverrides) {
|
|
1328
|
+
async function runTestModes(runFlags, configPath, selectors, suiteSelectors, fuzzerSelectors, modes, buildFeatureToggles, fuzzEnabled, fuzzOverrides) {
|
|
1260
1329
|
await ensureWebBrowsersReady(configPath, modes, runFlags.browser);
|
|
1261
1330
|
const modeSummaryTotal = Math.max(modes.length, 1);
|
|
1262
1331
|
const fileSummaryTotal = await resolveConfiguredFileTotal(configPath, selectors);
|
|
@@ -1266,13 +1335,13 @@ async function runTestModes(runFlags, configPath, selectors, modes, buildFeature
|
|
|
1266
1335
|
};
|
|
1267
1336
|
if (effectiveRunFlags.jobs > 1) {
|
|
1268
1337
|
if (modes.length > 1) {
|
|
1269
|
-
const failed = await runTestMatrixParallel(effectiveRunFlags, configPath, selectors, modes, buildFeatureToggles, modeSummaryTotal, fileSummaryTotal, fuzzEnabled, fuzzOverrides);
|
|
1338
|
+
const failed = await runTestMatrixParallel(effectiveRunFlags, configPath, selectors, suiteSelectors, fuzzerSelectors, modes, buildFeatureToggles, modeSummaryTotal, fileSummaryTotal, fuzzEnabled, fuzzOverrides);
|
|
1270
1339
|
process.exit(failed ? 1 : 0);
|
|
1271
1340
|
return;
|
|
1272
1341
|
}
|
|
1273
1342
|
let failed = false;
|
|
1274
1343
|
for (const modeName of modes) {
|
|
1275
|
-
const modeFailed = await runTestSingleParallel(effectiveRunFlags, configPath, selectors, buildFeatureToggles, modeSummaryTotal, fileSummaryTotal, fuzzEnabled, fuzzOverrides, modeName);
|
|
1344
|
+
const modeFailed = await runTestSingleParallel(effectiveRunFlags, configPath, selectors, suiteSelectors, fuzzerSelectors, buildFeatureToggles, modeSummaryTotal, fileSummaryTotal, fuzzEnabled, fuzzOverrides, modeName);
|
|
1276
1345
|
if (modeFailed)
|
|
1277
1346
|
failed = true;
|
|
1278
1347
|
}
|
|
@@ -1280,21 +1349,21 @@ async function runTestModes(runFlags, configPath, selectors, modes, buildFeature
|
|
|
1280
1349
|
return;
|
|
1281
1350
|
}
|
|
1282
1351
|
if (modes.length > 1) {
|
|
1283
|
-
const failed = await runTestMatrix(effectiveRunFlags, configPath, selectors, modes, buildFeatureToggles, modeSummaryTotal, fileSummaryTotal, fuzzEnabled, fuzzOverrides);
|
|
1352
|
+
const failed = await runTestMatrix(effectiveRunFlags, configPath, selectors, suiteSelectors, fuzzerSelectors, modes, buildFeatureToggles, modeSummaryTotal, fileSummaryTotal, fuzzEnabled, fuzzOverrides);
|
|
1284
1353
|
process.exit(failed ? 1 : 0);
|
|
1285
1354
|
return;
|
|
1286
1355
|
}
|
|
1287
1356
|
let failed = false;
|
|
1288
1357
|
for (const modeName of modes) {
|
|
1289
1358
|
const reporterSession = await createRunReporter(configPath, effectiveRunFlags.reporterPath, modeName);
|
|
1290
|
-
const modeResult = await runTestSequential(effectiveRunFlags, configPath, selectors, buildFeatureToggles, modeSummaryTotal, fileSummaryTotal, fuzzEnabled, modeName, reporterSession.reporter, !fuzzEnabled);
|
|
1359
|
+
const modeResult = await runTestSequential(effectiveRunFlags, configPath, selectors, suiteSelectors, buildFeatureToggles, modeSummaryTotal, fileSummaryTotal, fuzzEnabled, modeName, reporterSession.reporter, !fuzzEnabled);
|
|
1291
1360
|
if (modeResult.failed)
|
|
1292
1361
|
failed = true;
|
|
1293
1362
|
if (fuzzEnabled) {
|
|
1294
1363
|
if (reporterSession.reporterKind == "default") {
|
|
1295
1364
|
process.stdout.write("\n");
|
|
1296
1365
|
}
|
|
1297
|
-
const fuzzResults = await runFuzzMatrixResults(configPath, selectors, [modeName], fuzzOverrides, reporterSession.reporter);
|
|
1366
|
+
const fuzzResults = await runFuzzMatrixResults(configPath, selectors, fuzzerSelectors, [modeName], fuzzOverrides, reporterSession.reporter);
|
|
1298
1367
|
if (fuzzResults.some(hasFuzzFailures))
|
|
1299
1368
|
failed = true;
|
|
1300
1369
|
reporterSession.reporter.onRunComplete?.({
|
|
@@ -1315,13 +1384,13 @@ async function runTestModes(runFlags, configPath, selectors, modes, buildFeature
|
|
|
1315
1384
|
}
|
|
1316
1385
|
process.exit(failed ? 1 : 0);
|
|
1317
1386
|
}
|
|
1318
|
-
async function runTestMatrix(runFlags, configPath, selectors, modes, buildFeatureToggles, modeSummaryTotal, fileSummaryTotal, fuzzEnabled, fuzzOverrides) {
|
|
1387
|
+
async function runTestMatrix(runFlags, configPath, selectors, suiteSelectors, fuzzerSelectors, modes, buildFeatureToggles, modeSummaryTotal, fileSummaryTotal, fuzzEnabled, fuzzOverrides) {
|
|
1319
1388
|
const files = await resolveSelectedFiles(configPath, selectors);
|
|
1320
1389
|
if (!files.length) {
|
|
1321
1390
|
if (!fuzzEnabled) {
|
|
1322
1391
|
throw await buildNoTestFilesMatchedError(configPath, selectors);
|
|
1323
1392
|
}
|
|
1324
|
-
const fuzzFiles = await resolveSelectedFuzzFiles(configPath, selectors);
|
|
1393
|
+
const fuzzFiles = await resolveSelectedFuzzFiles(configPath, selectors, modes);
|
|
1325
1394
|
if (!fuzzFiles.length) {
|
|
1326
1395
|
throw await buildNoTestFilesMatchedError(configPath, selectors, true);
|
|
1327
1396
|
}
|
|
@@ -1420,7 +1489,7 @@ async function runTestMatrix(runFlags, configPath, selectors, modes, buildFeatur
|
|
|
1420
1489
|
if (reporterSession.reporterKind == "default") {
|
|
1421
1490
|
process.stdout.write("\n");
|
|
1422
1491
|
}
|
|
1423
|
-
const fuzzResults = await runFuzzMatrixResults(configPath, selectors, modes, fuzzOverrides, reporter);
|
|
1492
|
+
const fuzzResults = await runFuzzMatrixResults(configPath, selectors, fuzzerSelectors, modes, fuzzOverrides, reporter);
|
|
1424
1493
|
if (fuzzResults.some(hasFuzzFailures))
|
|
1425
1494
|
failed = true;
|
|
1426
1495
|
fuzzSummary = summarizeFuzzExecutions(fuzzResults);
|
|
@@ -1441,14 +1510,14 @@ async function runTestMatrix(runFlags, configPath, selectors, modes, buildFeatur
|
|
|
1441
1510
|
reporter.flush?.();
|
|
1442
1511
|
return failed;
|
|
1443
1512
|
}
|
|
1444
|
-
async function runFuzzModes(configPath, selectors, modes, rawArgs) {
|
|
1513
|
+
async function runFuzzModes(configPath, selectors, fuzzerSelectors, modes, rawArgs) {
|
|
1445
1514
|
const overrides = resolveFuzzOverrides(rawArgs, "fuzz");
|
|
1446
1515
|
const parallelSettings = resolveFuzzParallelJobs(rawArgs);
|
|
1447
1516
|
const clean = rawArgs.includes("--clean");
|
|
1448
|
-
const fuzzFiles = await resolveSelectedFuzzFiles(configPath, selectors);
|
|
1517
|
+
const fuzzFiles = await resolveSelectedFuzzFiles(configPath, selectors, modes);
|
|
1449
1518
|
const { jobs, buildJobs, runJobs } = resolveEffectiveParallelJobs(parallelSettings, fuzzFiles.length);
|
|
1450
1519
|
if (jobs > 1) {
|
|
1451
|
-
const results = await runFuzzMatrixResultsParallel(configPath, selectors, modes, overrides, jobs, buildJobs, runJobs, clean);
|
|
1520
|
+
const results = await runFuzzMatrixResultsParallel(configPath, selectors, fuzzerSelectors, modes, overrides, jobs, buildJobs, runJobs, clean);
|
|
1452
1521
|
const reporterSession = await createRunReporter(configPath);
|
|
1453
1522
|
reporterSession.reporter.onFuzzComplete?.(buildFuzzCompleteEvent(results, modes));
|
|
1454
1523
|
reporterSession.reporter.flush?.();
|
|
@@ -1456,12 +1525,12 @@ async function runFuzzModes(configPath, selectors, modes, rawArgs) {
|
|
|
1456
1525
|
return;
|
|
1457
1526
|
}
|
|
1458
1527
|
const reporterSession = await createRunReporter(configPath);
|
|
1459
|
-
const results = await runFuzzMatrixResults(configPath, selectors, modes, overrides, reporterSession.reporter);
|
|
1528
|
+
const results = await runFuzzMatrixResults(configPath, selectors, fuzzerSelectors, modes, overrides, reporterSession.reporter);
|
|
1460
1529
|
reporterSession.reporter.onFuzzComplete?.(buildFuzzCompleteEvent(results, modes));
|
|
1461
1530
|
reporterSession.reporter.flush?.();
|
|
1462
1531
|
process.exit(results.some(hasFuzzFailures) ? 1 : 0);
|
|
1463
1532
|
}
|
|
1464
|
-
async function runRuntimeSingleParallel(runFlags, configPath, selectors, modeName, modeSummaryTotal, fileSummaryTotal) {
|
|
1533
|
+
async function runRuntimeSingleParallel(runFlags, configPath, selectors, suiteSelectors, modeName, modeSummaryTotal, fileSummaryTotal) {
|
|
1465
1534
|
const files = await resolveSelectedFiles(configPath, selectors);
|
|
1466
1535
|
if (!files.length) {
|
|
1467
1536
|
throw await buildNoTestFilesMatchedError(configPath, selectors);
|
|
@@ -1493,6 +1562,7 @@ async function runRuntimeSingleParallel(runFlags, configPath, selectors, modeNam
|
|
|
1493
1562
|
reporter: buffered?.reporter,
|
|
1494
1563
|
reporterKind: buffered?.reporterKind,
|
|
1495
1564
|
modeName,
|
|
1565
|
+
suiteSelectors,
|
|
1496
1566
|
emitRunComplete: false,
|
|
1497
1567
|
fileSummaryTotal: 1,
|
|
1498
1568
|
modeSummaryTotal,
|
|
@@ -1522,7 +1592,7 @@ async function runRuntimeSingleParallel(runFlags, configPath, selectors, modeNam
|
|
|
1522
1592
|
reporter.flush?.();
|
|
1523
1593
|
return results.some((result) => result.failed);
|
|
1524
1594
|
}
|
|
1525
|
-
async function runRuntimeMatrixParallel(runFlags, configPath, selectors, modes, modeSummaryTotal, fileSummaryTotal) {
|
|
1595
|
+
async function runRuntimeMatrixParallel(runFlags, configPath, selectors, suiteSelectors, modes, modeSummaryTotal, fileSummaryTotal) {
|
|
1526
1596
|
const files = await resolveSelectedFiles(configPath, selectors);
|
|
1527
1597
|
if (!files.length) {
|
|
1528
1598
|
throw await buildNoTestFilesMatchedError(configPath, selectors);
|
|
@@ -1572,6 +1642,7 @@ async function runRuntimeMatrixParallel(runFlags, configPath, selectors, modes,
|
|
|
1572
1642
|
const result = await run(runFlags, configPath, [file], false, {
|
|
1573
1643
|
reporter: silentReporter,
|
|
1574
1644
|
reporterKind: "default",
|
|
1645
|
+
suiteSelectors,
|
|
1575
1646
|
emitRunStart: false,
|
|
1576
1647
|
emitRunComplete: false,
|
|
1577
1648
|
logFileName: `run.${artifactKey}.log.json`,
|
|
@@ -1627,7 +1698,7 @@ async function runRuntimeMatrixParallel(runFlags, configPath, selectors, modes,
|
|
|
1627
1698
|
reporter.flush?.();
|
|
1628
1699
|
return allResults.some((result) => result.failed);
|
|
1629
1700
|
}
|
|
1630
|
-
async function runTestSingleParallel(runFlags, configPath, selectors, buildFeatureToggles, modeSummaryTotal, fileSummaryTotal, fuzzEnabled, fuzzOverrides, modeName) {
|
|
1701
|
+
async function runTestSingleParallel(runFlags, configPath, selectors, suiteSelectors, fuzzerSelectors, buildFeatureToggles, modeSummaryTotal, fileSummaryTotal, fuzzEnabled, fuzzOverrides, modeName) {
|
|
1631
1702
|
const files = await resolveSelectedFiles(configPath, selectors);
|
|
1632
1703
|
if (!files.length && !fuzzEnabled) {
|
|
1633
1704
|
throw await buildNoTestFilesMatchedError(configPath, selectors);
|
|
@@ -1672,6 +1743,7 @@ async function runTestSingleParallel(runFlags, configPath, selectors, buildFeatu
|
|
|
1672
1743
|
const result = await run({ ...runFlags, clean: true }, configPath, [file], false, {
|
|
1673
1744
|
reporter: buffered?.reporter,
|
|
1674
1745
|
reporterKind: buffered?.reporterKind,
|
|
1746
|
+
suiteSelectors,
|
|
1675
1747
|
emitRunComplete: false,
|
|
1676
1748
|
logFileName: `test.${artifactKey}.log.json`,
|
|
1677
1749
|
coverageFileName: `coverage.${artifactKey}.log.json`,
|
|
@@ -1699,7 +1771,7 @@ async function runTestSingleParallel(runFlags, configPath, selectors, buildFeatu
|
|
|
1699
1771
|
if (reporterSession.reporterKind == "default") {
|
|
1700
1772
|
process.stdout.write("\n");
|
|
1701
1773
|
}
|
|
1702
|
-
const fuzzResults = await runFuzzMatrixResultsParallel(configPath, selectors, [modeName], fuzzOverrides, runFlags.jobs, runFlags.buildJobs, runFlags.runJobs, runFlags.clean);
|
|
1774
|
+
const fuzzResults = await runFuzzMatrixResultsParallel(configPath, selectors, fuzzerSelectors, [modeName], fuzzOverrides, runFlags.jobs, runFlags.buildJobs, runFlags.runJobs, runFlags.clean);
|
|
1703
1775
|
if (fuzzResults.some(hasFuzzFailures))
|
|
1704
1776
|
failed = true;
|
|
1705
1777
|
fuzzSummary = summarizeFuzzExecutions(fuzzResults);
|
|
@@ -1720,13 +1792,13 @@ async function runTestSingleParallel(runFlags, configPath, selectors, buildFeatu
|
|
|
1720
1792
|
reporter.flush?.();
|
|
1721
1793
|
return failed;
|
|
1722
1794
|
}
|
|
1723
|
-
async function runTestMatrixParallel(runFlags, configPath, selectors, modes, buildFeatureToggles, modeSummaryTotal, fileSummaryTotal, fuzzEnabled, fuzzOverrides) {
|
|
1795
|
+
async function runTestMatrixParallel(runFlags, configPath, selectors, suiteSelectors, fuzzerSelectors, modes, buildFeatureToggles, modeSummaryTotal, fileSummaryTotal, fuzzEnabled, fuzzOverrides) {
|
|
1724
1796
|
const files = await resolveSelectedFiles(configPath, selectors);
|
|
1725
1797
|
if (!files.length) {
|
|
1726
1798
|
if (!fuzzEnabled) {
|
|
1727
1799
|
throw await buildNoTestFilesMatchedError(configPath, selectors);
|
|
1728
1800
|
}
|
|
1729
|
-
const fuzzFiles = await resolveSelectedFuzzFiles(configPath, selectors);
|
|
1801
|
+
const fuzzFiles = await resolveSelectedFuzzFiles(configPath, selectors, modes);
|
|
1730
1802
|
if (!fuzzFiles.length) {
|
|
1731
1803
|
throw await buildNoTestFilesMatchedError(configPath, selectors, true);
|
|
1732
1804
|
}
|
|
@@ -1776,6 +1848,7 @@ async function runTestMatrixParallel(runFlags, configPath, selectors, modes, bui
|
|
|
1776
1848
|
const result = await run(runFlags, configPath, [file], false, {
|
|
1777
1849
|
reporter: silentReporter,
|
|
1778
1850
|
reporterKind: "default",
|
|
1851
|
+
suiteSelectors,
|
|
1779
1852
|
emitRunStart: false,
|
|
1780
1853
|
emitRunComplete: false,
|
|
1781
1854
|
logFileName: `test.${artifactKey}.log.json`,
|
|
@@ -1823,7 +1896,7 @@ async function runTestMatrixParallel(runFlags, configPath, selectors, modes, bui
|
|
|
1823
1896
|
if (reporterSession.reporterKind == "default") {
|
|
1824
1897
|
process.stdout.write("\n");
|
|
1825
1898
|
}
|
|
1826
|
-
const fuzzResults = await runFuzzMatrixResultsParallel(configPath, selectors, modes, fuzzOverrides, runFlags.jobs, runFlags.buildJobs, runFlags.runJobs, runFlags.clean);
|
|
1899
|
+
const fuzzResults = await runFuzzMatrixResultsParallel(configPath, selectors, fuzzerSelectors, modes, fuzzOverrides, runFlags.jobs, runFlags.buildJobs, runFlags.runJobs, runFlags.clean);
|
|
1827
1900
|
if (fuzzResults.some(hasFuzzFailures))
|
|
1828
1901
|
failed = true;
|
|
1829
1902
|
fuzzSummary = summarizeFuzzExecutions(fuzzResults);
|
|
@@ -1844,8 +1917,12 @@ async function runTestMatrixParallel(runFlags, configPath, selectors, modes, bui
|
|
|
1844
1917
|
reporter.flush?.();
|
|
1845
1918
|
return failed;
|
|
1846
1919
|
}
|
|
1847
|
-
async function runFuzzMatrixResultsParallel(configPath, selectors, modes, overrides, jobs, buildJobs, runJobs, clean) {
|
|
1848
|
-
const
|
|
1920
|
+
async function runFuzzMatrixResultsParallel(configPath, selectors, fuzzerSelectors, modes, overrides, jobs, buildJobs, runJobs, clean) {
|
|
1921
|
+
const filesByMode = new Map();
|
|
1922
|
+
for (const modeName of modes) {
|
|
1923
|
+
filesByMode.set(modeName, await resolveSelectedFuzzFiles(configPath, selectors, [modeName]));
|
|
1924
|
+
}
|
|
1925
|
+
const files = [...new Set([...filesByMode.values()].flat())].sort((a, b) => a.localeCompare(b));
|
|
1849
1926
|
if (!files.length) {
|
|
1850
1927
|
throw new Error(`No fuzz files matched: ${selectors.length ? selectors.join(", ") : "configured input patterns"}`);
|
|
1851
1928
|
}
|
|
@@ -1856,7 +1933,9 @@ async function runFuzzMatrixResultsParallel(configPath, selectors, modes, overri
|
|
|
1856
1933
|
const token = renderQueuedFileStart(queueDisplay, path.basename(file));
|
|
1857
1934
|
const fileResults = [];
|
|
1858
1935
|
for (const modeName of modes) {
|
|
1859
|
-
|
|
1936
|
+
if (!(filesByMode.get(modeName)?.includes(file) ?? false))
|
|
1937
|
+
continue;
|
|
1938
|
+
const modeResults = await fuzz(configPath, [file], modeName, overrides, fuzzerSelectors);
|
|
1860
1939
|
fileResults.push(...modeResults);
|
|
1861
1940
|
}
|
|
1862
1941
|
ordered[index] = fileResults;
|
|
@@ -1868,20 +1947,23 @@ async function runFuzzMatrixResultsParallel(configPath, selectors, modes, overri
|
|
|
1868
1947
|
queueDisplay.flush();
|
|
1869
1948
|
return ordered.flat();
|
|
1870
1949
|
}
|
|
1871
|
-
async function runFuzzMatrixResults(configPath, selectors, modes, overrides, reporter) {
|
|
1872
|
-
const files = await resolveSelectedFuzzFiles(configPath, selectors);
|
|
1873
|
-
if (!files.length) {
|
|
1874
|
-
throw new Error(`No fuzz files matched: ${selectors.length ? selectors.join(", ") : "configured input patterns"}`);
|
|
1875
|
-
}
|
|
1950
|
+
async function runFuzzMatrixResults(configPath, selectors, fuzzerSelectors, modes, overrides, reporter) {
|
|
1876
1951
|
const results = [];
|
|
1877
|
-
for (const
|
|
1878
|
-
const
|
|
1879
|
-
|
|
1880
|
-
|
|
1952
|
+
for (const modeName of modes) {
|
|
1953
|
+
const files = await resolveSelectedFuzzFiles(configPath, selectors, [modeName]);
|
|
1954
|
+
if (!files.length) {
|
|
1955
|
+
continue;
|
|
1956
|
+
}
|
|
1957
|
+
for (const file of files) {
|
|
1958
|
+
const fileResults = [];
|
|
1959
|
+
const modeResults = await fuzz(configPath, [file], modeName, overrides, fuzzerSelectors);
|
|
1881
1960
|
fileResults.push(...modeResults);
|
|
1882
1961
|
results.push(...modeResults);
|
|
1962
|
+
reporter?.onFuzzFileComplete?.({ file, results: fileResults });
|
|
1883
1963
|
}
|
|
1884
|
-
|
|
1964
|
+
}
|
|
1965
|
+
if (!results.length) {
|
|
1966
|
+
throw new Error(`No fuzz files matched: ${selectors.length ? selectors.join(", ") : "configured input patterns"}`);
|
|
1885
1967
|
}
|
|
1886
1968
|
return results;
|
|
1887
1969
|
}
|
|
@@ -2141,13 +2223,21 @@ async function resolveSelectedFiles(configPath, selectors, warn = true) {
|
|
|
2141
2223
|
const specs = matches.filter((file) => file.endsWith(".spec.ts"));
|
|
2142
2224
|
return [...new Set(specs)].sort((a, b) => a.localeCompare(b));
|
|
2143
2225
|
}
|
|
2144
|
-
async function resolveSelectedFuzzFiles(configPath, selectors) {
|
|
2226
|
+
async function resolveSelectedFuzzFiles(configPath, selectors, modes = [undefined]) {
|
|
2145
2227
|
const resolvedConfigPath = configPath ?? path.join(process.cwd(), "./as-test.config.json");
|
|
2146
|
-
const
|
|
2147
|
-
const
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2228
|
+
const files = new Set();
|
|
2229
|
+
for (const modeName of modes) {
|
|
2230
|
+
const loaded = loadConfig(resolvedConfigPath, false);
|
|
2231
|
+
const applied = applyMode(loaded, modeName);
|
|
2232
|
+
const config = applied.config;
|
|
2233
|
+
const patterns = resolveFuzzPatterns(config.fuzz.input, selectors);
|
|
2234
|
+
const matches = await glob(patterns);
|
|
2235
|
+
for (const file of matches) {
|
|
2236
|
+
if (file.endsWith(".fuzz.ts"))
|
|
2237
|
+
files.add(file);
|
|
2238
|
+
}
|
|
2239
|
+
}
|
|
2240
|
+
return [...files].sort((a, b) => a.localeCompare(b));
|
|
2151
2241
|
}
|
|
2152
2242
|
async function resolveSelectedTestInputs(configPath, selectors) {
|
|
2153
2243
|
const [specs, fuzz] = await Promise.all([
|
|
@@ -2589,9 +2679,9 @@ async function listExecutionPlan(command, configPath, selectors, modes, listFlag
|
|
|
2589
2679
|
return;
|
|
2590
2680
|
const specFiles = command == "fuzz" ? [] : await resolveSelectedFiles(configPath, selectors);
|
|
2591
2681
|
const fuzzFiles = command == "fuzz"
|
|
2592
|
-
? await resolveSelectedFuzzFiles(configPath, selectors)
|
|
2682
|
+
? await resolveSelectedFuzzFiles(configPath, selectors, modes)
|
|
2593
2683
|
: command == "test" && fuzzEnabled
|
|
2594
|
-
? await resolveSelectedFuzzFiles(configPath, selectors)
|
|
2684
|
+
? await resolveSelectedFuzzFiles(configPath, selectors, modes)
|
|
2595
2685
|
: [];
|
|
2596
2686
|
const files = command == "fuzz" ? fuzzFiles : specFiles;
|
|
2597
2687
|
if (!specFiles.length && !fuzzFiles.length) {
|
|
@@ -2637,8 +2727,7 @@ async function listExecutionPlan(command, configPath, selectors, modes, listFlag
|
|
|
2637
2727
|
}
|
|
2638
2728
|
}
|
|
2639
2729
|
const envOverrides = {
|
|
2640
|
-
...
|
|
2641
|
-
...(modeName ? (config.modes[modeName]?.env ?? {}) : {}),
|
|
2730
|
+
...active.env,
|
|
2642
2731
|
...(command == "build"
|
|
2643
2732
|
? active.buildOptions.env
|
|
2644
2733
|
: command == "run" || command == "test"
|
package/bin/reporters/default.js
CHANGED
|
@@ -329,7 +329,7 @@ function renderFailedFuzzers(results) {
|
|
|
329
329
|
for (const result of results) {
|
|
330
330
|
for (const modeResult of result.modes) {
|
|
331
331
|
const relativeFile = toRelativeResultPath(modeResult.file);
|
|
332
|
-
const repro = buildFuzzReproCommand(relativeFile, modeResult.seed, modeResult.modeName);
|
|
332
|
+
const repro = buildFuzzReproCommand(relativeFile, modeResult.seed, modeResult.modeName, modeResult.fuzzers[0]?.selector);
|
|
333
333
|
if (modeResult.crashes > 0 && !modeResult.fuzzers.length) {
|
|
334
334
|
if (!rendered) {
|
|
335
335
|
console.log("");
|
|
@@ -353,18 +353,19 @@ function renderFailedFuzzers(results) {
|
|
|
353
353
|
console.log("");
|
|
354
354
|
rendered = true;
|
|
355
355
|
}
|
|
356
|
+
const fuzzerRepro = buildFuzzReproCommand(relativeFile, modeResult.seed, modeResult.modeName, fuzzer.selector);
|
|
356
357
|
console.log(`${chalk.bgRed(" FAIL ")} ${formatFuzzFailureTitle(modeResult.file, fuzzer.name)}`);
|
|
357
358
|
if (fuzzer.failure) {
|
|
358
359
|
renderAssertionFailureDetails(fuzzer.failure.left, fuzzer.failure.right, fuzzer.failure.message);
|
|
359
360
|
}
|
|
360
361
|
console.log(chalk.dim(`Mode: ${modeResult.modeName}`));
|
|
361
362
|
console.log(chalk.dim(`Runs: ${fuzzer.passed + fuzzer.failed + fuzzer.crashed} completed (${fuzzer.passed} passed, ${fuzzer.failed} failed, ${fuzzer.crashed} crashed)`));
|
|
362
|
-
console.log(chalk.dim(`Repro: ${
|
|
363
|
+
console.log(chalk.dim(`Repro: ${fuzzerRepro}`));
|
|
363
364
|
console.log(chalk.dim(`Seed: ${modeResult.seed}`));
|
|
364
365
|
if (fuzzer.failures?.length) {
|
|
365
366
|
console.log(chalk.dim(`Failing seeds: ${formatFailingSeeds(fuzzer)}`));
|
|
366
367
|
for (const failure of fuzzer.failures) {
|
|
367
|
-
console.log(chalk.dim(`Repro ${failure.run + 1}: ${buildFuzzReproCommand(relativeFile, failure.seed, modeResult.modeName, 1)}`));
|
|
368
|
+
console.log(chalk.dim(`Repro ${failure.run + 1}: ${buildFuzzReproCommand(relativeFile, failure.seed, modeResult.modeName, fuzzer.selector, 1)}`));
|
|
368
369
|
if (failure.input) {
|
|
369
370
|
console.log(chalk.dim(`Input ${failure.run + 1}: ${JSON.stringify(failure.input)}`));
|
|
370
371
|
}
|
|
@@ -405,10 +406,11 @@ function averageFuzzModeTime(results) {
|
|
|
405
406
|
return 0;
|
|
406
407
|
return results.reduce((sum, result) => sum + result.time, 0) / results.length;
|
|
407
408
|
}
|
|
408
|
-
function buildFuzzReproCommand(file, seed, modeName, runs) {
|
|
409
|
+
function buildFuzzReproCommand(file, seed, modeName, fuzzer, runs) {
|
|
409
410
|
const modeArg = modeName != "default" ? ` --mode ${modeName}` : "";
|
|
411
|
+
const fuzzerArg = fuzzer?.length ? ` --fuzzer ${fuzzer}` : "";
|
|
410
412
|
const runsArg = typeof runs == "number" ? ` --runs ${runs}` : "";
|
|
411
|
-
return `ast fuzz ${file}${modeArg} --seed ${seed}${runsArg}`;
|
|
413
|
+
return `ast fuzz ${file}${modeArg}${fuzzerArg} --seed ${seed}${runsArg}`;
|
|
412
414
|
}
|
|
413
415
|
function formatFailingSeeds(fuzzer) {
|
|
414
416
|
return (fuzzer.failures ?? []).map((failure) => String(failure.seed)).join(", ");
|
|
@@ -467,9 +469,10 @@ function renderFailedSuites(failedEntries) {
|
|
|
467
469
|
collectSuiteFailures(failed, file, [], printed);
|
|
468
470
|
}
|
|
469
471
|
}
|
|
470
|
-
function collectSuiteFailures(suite, file, path, printed) {
|
|
472
|
+
function collectSuiteFailures(suite, file, path, printed, inheritedModeName = "") {
|
|
471
473
|
const suiteAny = suite;
|
|
472
474
|
const nextPath = [...path, String(suiteAny.description ?? "unknown")];
|
|
475
|
+
const modeName = String(suiteAny.modeName ?? inheritedModeName);
|
|
473
476
|
const tests = Array.isArray(suiteAny.tests)
|
|
474
477
|
? suiteAny.tests
|
|
475
478
|
: [];
|
|
@@ -481,7 +484,7 @@ function collectSuiteFailures(suite, file, path, printed) {
|
|
|
481
484
|
const title = `${nextPath.join(" > ")}#${assertionIndex}`;
|
|
482
485
|
const loc = String(test.location ?? "");
|
|
483
486
|
const where = loc.length ? `${file}:${loc}` : file;
|
|
484
|
-
const
|
|
487
|
+
const suitePath = String(suiteAny.path ?? "");
|
|
485
488
|
const message = String(test.message ?? "");
|
|
486
489
|
const dedupeKey = `${file}::${modeName}::${title}::${String(test.left)}::${String(test.right)}::${message}`;
|
|
487
490
|
if (printed.has(dedupeKey))
|
|
@@ -491,15 +494,22 @@ function collectSuiteFailures(suite, file, path, printed) {
|
|
|
491
494
|
if (modeName.length) {
|
|
492
495
|
console.log(chalk.dim(`Mode: ${modeName}`));
|
|
493
496
|
}
|
|
497
|
+
if (suitePath.length) {
|
|
498
|
+
console.log(chalk.dim(`Repro: ${buildSuiteReproCommand(toRelativeResultPath(file), suitePath, modeName)}`));
|
|
499
|
+
}
|
|
494
500
|
renderAssertionFailureDetails(test.left, test.right, message);
|
|
495
501
|
}
|
|
496
502
|
const suites = Array.isArray(suiteAny.suites)
|
|
497
503
|
? suiteAny.suites
|
|
498
504
|
: [];
|
|
499
505
|
for (const sub of suites) {
|
|
500
|
-
collectSuiteFailures(sub, file, nextPath, printed);
|
|
506
|
+
collectSuiteFailures(sub, file, nextPath, printed, modeName);
|
|
501
507
|
}
|
|
502
508
|
}
|
|
509
|
+
function buildSuiteReproCommand(file, suitePath, modeName) {
|
|
510
|
+
const modeArg = modeName && modeName != "default" ? ` --mode ${modeName}` : "";
|
|
511
|
+
return `ast run ${file}${modeArg} --suite ${suitePath}`;
|
|
512
|
+
}
|
|
503
513
|
function normalizeFailureMessage(message) {
|
|
504
514
|
return message.replace(/\r\n/g, "\n").trim();
|
|
505
515
|
}
|