var _ = require("./lodash-wrapper.js"); var traverse = require("traverse"); var pd = require("probability-distributions"); var { MandGTypeManager } = require("./mandg.js"); /** * takes a `thing` object and a bunch of random numbers and * spits out a Recipe */ class Cooker { /** * Cooker constructor * @param {object} opts - options for the new cooker * @param {string} opts.numPathsDistribution - when selecting the number of paths from `thing` to mangle, this is the distribution. Options are "low", "uniform" or "split". Low is more likely to pick a low number of paths (e.g. - 1 or 2), uniform is likely to pick any number of paths, and split is likely to pick a low number or the max number. */ constructor(opts) { opts = opts || {}; var defaults = { numPathsDistribution: "low", weightMap: new Map([ ["generate", 3], ["mutate", 3], ["generateAny", 1], ["mutateByParent", 1] ]) }; _.defaultsDeep(opts, defaults); this.numPathsDistribution = opts.numPathsDistribution; this.weightMap = opts.weightMap; // calculated in init this.weightedOpList = []; } /** * initializes the Cooker with a `thing` object and does any configuration necessary * @param {any} thing - the object, string, array, null, or whatever that will be mangled by the fuzzer */ init(thing) { // save thing for later this.thing = _.cloneDeep(thing); // adds one entry of a function for every weight count for (let weightType of this.weightMap) { var weight = weightType[1]; var functionName = weightType[0]; for (let i = 0; i < weight; i++) { this.weightedOpList.push(functionName); } } // create the path list this.pathList = _.invokeMap(traverse(thing).paths(), Array.prototype.join, "."); // mark initialization as complete this.initialized = true; // add generateAny to the context of any calling function new MandGTypeManager().addToContext("generateAny", this.generateAny); return this; } getRandomPathCount(max) { pd.prng = Math.random; // three options: switch (this.numPathsDistribution) { case "uniform": // 1. uniform distribution return _.random(1, max); case "low": // 2. low-number heavy distribution return pd.rbinom(1, max - 1, 0.1)[0] + 1; case "split": // 3. split between min and max, with an even sprinkling inbetween return (Math.ceil(pd.rbeta(1, 0.5, 0.6)[0] * max - 1)) + 1; } } selectPaths() { var list = this.pathList; if (list.length === 1) return list; var count = this.getRandomPathCount(list.length); return _.sampleSize(list, count); } selectOp(mandg) { var op; var opGenerator; do { opGenerator = _.sample(this.weightedOpList); op = this[opGenerator](mandg); } while (!op); return op; } generate(mandg) { return _.sample(mandg.generator); } mutate(mandg) { return _.sample(mandg.mutator); } generateAny() { if (this.generateAnyCache) return _.sample(this.generateAnyCache); var mgr = new MandGTypeManager(); var genList = []; gatherGenerators (mgr.root); // recursively iterates all MandG to create a list of all generators function gatherGenerators(mandg) { // add all generators from this MandG to the list for (let gen of Object.keys(mandg.generator)) { genList.push (mandg.generator[gen]); } // iterate all subtypes for (let sub of Object.keys(mandg.subtype)) { gatherGenerators (mandg.subtype[sub]); } } this.generateAnyCache = genList; new MandGTypeManager().addToContext("generateAnyCache", this.generateAnyCache); return _.sample(this.generateAnyCache); } mutateByParent(mandg) { var mutateList = []; while (mandg) { for (let mut of Object.keys(mandg.mutator)) { mutateList.push(mandg.mutator[mut]); } mandg = mandg.parent; } return _.sample(mutateList); } createRecipe() { var mgr = new MandGTypeManager(); if (!this.initialized) { throw new Error("Initialize cooker before calling createRecipe"); } // console.log ("path list:", this.pathList); var selectedPaths = this.selectPaths(); // console.log ("selected paths:", selectedPaths); var recipe = new Recipe(); for (let path of selectedPaths) { // extract the specific item from the path in the thing object var item = (path === "") ? this.thing : _.get(this.thing, path); // find the matching mandg for the thing var mandg = mgr.resolveType(item); // find the operation we want to perform var op = this.selectOp(mandg); recipe.addStep(path, op); } return recipe; } } /** * A single step in a Recipe */ class Step { constructor(path, op) { this.path = path; this.op = op; } } /** * The recipe instructs the Fuzzer what steps to take to mutate a `thing` */ class Recipe { constructor() { // this.length this.steps = new Set(); // this.steps = [{path: "", operation: ""}] this.recipeIterator = function*() { for (let step of this.steps) { yield step; } }; } /** * Get the number of steps in the recipe * @returns {Number} length of the recipe */ get length() { return this.steps.size; } /** * Adds a step to this receipe * @param pathOrStep {String|Step} - the path in `thing` that `op` will run on. Or if a Step has already been created (which constains a path and op) just use that instead. * @param op {Function} - if pathOrStep is a path, this is the function that will be run against that path */ addStep(pathOrStep, op) { console.log(`adding step :: path: "${pathOrStep}" ; op: ${op.name}`); var step; if (pathOrStep instanceof Step) { step = pathOrStep; } else { step = new Step(pathOrStep, op); } this.steps.add(step); } /** * Implements the next() part of the iterator protocol */ [Symbol.iterator]() { return this.recipeIterator(); } // toString } module.exports = { Cooker: Cooker, Recipe: Recipe };