Add `npm run fuzz` to run parsing fuzz test (#462)

This commit is contained in:
Nick Guerrera 2021-04-20 15:17:56 -07:00 коммит произвёл GitHub
Родитель db4535a2f3
Коммит bc216196db
2 изменённых файлов: 223 добавлений и 1 удалений

Просмотреть файл

@ -37,7 +37,8 @@
"watch": "tsc -p . --watch",
"dogfood": "node scripts/dogfood.js",
"test": "mocha --require source-map-support/register 'dist/test/**/*.js'",
"regen-samples": "node scripts/regen-samples.js"
"regen-samples": "node scripts/regen-samples.js",
"fuzz": "node dist/test/fuzz.js"
},
"dependencies": {
"autorest": "~3.0.6335",

221
packages/adl/test/fuzz.ts Normal file
Просмотреть файл

@ -0,0 +1,221 @@
import { logVerboseTestOutput } from "../compiler/diagnostics.js";
import { parse } from "../compiler/parser.js";
interface FuzzedFile extends FuzzedScope {
name: string;
contents: string[];
}
interface FuzzedScope {
bindings: string[];
scopes: FuzzedScope[];
}
const odds = {
newFileInProgram: 0.5,
newStatementInFile: 0.5,
createNamespace: 0.5,
mangleTokens: 1,
mangleToken: 0.1,
newModelProperty: 0.5,
};
const tokens = ["|", "&", ":", "{", "}", '"', '"""', "op", "model", "?", "...", "(", ")", ";", ","];
const weights = {
scriptItemKind: {
namespace: 1,
model: 1,
operation: 1,
},
statementKind: {
namespace: 1,
model: 1,
operation: 1,
},
tokenmangle: {
delete: 1,
replaceWithOther: 5,
replaceWithRandom: 1,
},
};
main();
function main() {
const iterations = 10000;
console.log("Running parser fuzz test with 1000 iterations...");
fuzzTest(iterations);
console.log("Fuzz test completed successfully without issues.");
}
function fuzzTest(iterations: number) {
let fileCount: number = 0;
for (let i = 0; i < iterations; i++) {
const files = generateAdlProgram();
maybe(() => {
for (const f of files) {
for (const [i] of f.contents.entries()) {
maybe(() => {
roll(
{
delete() {
f.contents[i] = "";
},
replaceWithOther() {
f.contents[i] = randomItem(tokens);
},
replaceWithRandom() {
f.contents[i] = String.fromCharCode(Math.random() * 96 + 32);
},
},
weights.tokenmangle
);
}, odds.mangleToken);
}
}
}, odds.mangleTokens);
logVerboseTestOutput(">>>");
for (const f of files) {
const source = f.contents.join(" ");
logVerboseTestOutput(f.name + ": " + f.contents.join(" "));
try {
parse(source);
} catch (err) {
console.error("Failed to parse generated source:");
console.error(source);
throw err;
}
}
function generateAdlProgram(): FuzzedFile[] {
fileCount = 0;
const files: FuzzedFile[] = [];
repeatBinomial(
() => {
files.push(generateAdlFile());
},
odds.newFileInProgram,
{ atLeastOnce: true }
);
return files;
}
function generateAdlFile(): FuzzedFile {
const file: FuzzedFile = {
name: `f${fileCount++}.adl`,
contents: [],
bindings: [],
scopes: [],
};
repeatBinomial(
() => {
file.contents.push(...generateAdlScriptItem());
},
odds.newStatementInFile,
{ atLeastOnce: true }
);
return file;
}
function generateAdlScriptItem(): string[] {
let stmt: string[] = [];
roll(
{
namespace() {
stmt = stmt.concat(["namespace", "Foo", "{", ...generateNamespaceBody(), "}"]);
},
model() {
stmt = stmt.concat(["model", "Foo", "{", ...generateModelBody(), "}"]);
},
operation() {
stmt = stmt.concat(["op", "Foo", "(", ")", ":", "{}"]);
},
},
weights.scriptItemKind
);
return stmt;
}
function generateStatement(): string[] {
let stmt: string[] = [];
roll(
{
namespace() {
stmt = stmt.concat(["namespace", "Foo", "{", ...generateNamespaceBody(), "}"]);
},
model() {
stmt = stmt.concat(["model", "Foo", "{", ...generateModelBody(), "}"]);
},
operation() {
stmt = stmt.concat(["op", "Foo", "(", ")", ";"]);
},
},
weights.statementKind
);
return stmt;
}
function generateModelBody() {
let contents: string[] = [];
repeatBinomial(() => {
contents = contents.concat(["foo", ":", "ref", ","]);
}, odds.newModelProperty);
return contents;
}
function generateNamespaceBody(): string[] {
let contents: string[] = [];
repeatBinomial(() => {
contents = contents.concat(generateStatement());
}, odds.newStatementInFile);
return contents;
}
function repeatBinomial(fn: Function, p: number, opts: { atLeastOnce?: boolean } = {}) {
if (opts.atLeastOnce) {
fn();
}
while (Math.random() < p) {
fn();
}
}
function roll<T>(opts: Record<keyof T, Function>, weights: Record<keyof T, number>): void {
let sum = 0;
for (const w of Object.values<number>(weights)) {
sum += w;
}
let n = Math.random() * sum;
for (const [k, w] of Object.entries<number>(weights)) {
if (n < w) {
(opts as any)[k]();
}
n -= w as number;
}
}
function randomItem(arr: any[]) {
const index = Math.floor(Math.random() * arr.length);
return arr[index];
}
function maybe(fn: Function, p: number) {
if (Math.random() < p) {
fn();
}
}
}
}