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
};