"use strict"; /** * Creates instance of test. * * @class * @classdesc Test case data structure which contains its current state and steps. * @name Test * @prop {boolean} [isChanged=false] - Flag whether test case is changed, * for example, after step addition. * @prop {object} [state={}] - Current test case state. * @prop {Step[]} [steps=[]] - Test case steps. * @prop {string[]} [incomplete=[]] - Test case incompletenesses. */ var _ = require("lodash"); var isSub = require("./utils").isSub; var CONF = require("./config"); var Test = function () { this.isChanged = false; this.state = {}; this.steps = []; this.incomplete = []; this.weight = 0; }; /** * Detects whether it's possible to add step to test case or no. * * @method * @arg {Step} step - Step to check. * @return {boolean} `true` if it's possible, `false` otherwise. */ Test.prototype.mayAdd = function (step) { /* add step if it should after last step in test */ if (this.steps.length && this.steps[this.steps.length - 1].name === step.after) { return true; }; if (this.steps.length && this.steps[this.steps.length - 1].before === step.name) { return true; }; /* don't add step if test case is finished */ if (this.state === null && this.steps.length) return false; /* don't add step if its limit is exhausted */ if (this._amount(step) >= (CONF.gen.stepsUsage || step.usage)) return false; /* if step has completeness it should be matches test incompleteness */ if (step.complete) { var isEqual = true; var i = 0; var incomplete = _.clone(this.incomplete).reverse(); for (var cmpl of step.complete) { var testCmpl = incomplete[i++]; if (cmpl !== testCmpl) { isEqual = false; break; }; }; if (!isEqual) return false; }; return isSub(this.state, step.income || {}); }; /** * Adds step to test case. * * @method * @arg {Step} step - Added step. */ Test.prototype.add = function (step) { this.steps.push(step); this.isChanged = true; /** * Don't merge states, if there were steps before addition and state is * finished. It means, that this step was added explicitly via keywords * `before` or `after`. */ if (this.state === null && this.steps.length > 1) return; var outcome = step.outcome === undefined ? {} : step.outcome; if (_.isObject(this.state) && _.isObject(outcome)) { _.merge(this.state, outcome); } else { this.state = outcome; }; if (step.complete) { this.incomplete = this.incomplete.slice(-this.incomplete.length, this.incomplete.length - step.complete.length); }; if (step.incomplete) { this.incomplete = this.incomplete.concat(step.incomplete); }; var weight = step.weight; if (Test.pretrain) { var x = 0; var prevStep = _.nth(this.steps, -2); if (prevStep) x += (Test.pretrain[prevStep.name + " | " + step.name] || 0); var prevPrevStep = _.nth(this.steps, -3); if (prevPrevStep) x += (Test.pretrain[prevPrevStep.name + " | " + prevStep.name + " | " + step.name] || 0); if (x > 0) weight *= (1 + activate(x)); }; this.weight = _.round(this.weight + weight, 3); }; var activate = x => (Math.exp(x) - Math.exp(-x)) / (Math.exp(x) + Math.exp(-x)); /** * Clones test case. * * @method * @return {Test} New instance of test with the same parameters. */ Test.prototype.clone = function () { var c = new this.constructor(); c.isChanged = this.isChanged; c.state = _.cloneDeep(this.state); c.steps = this.steps.map(s => s.clone()); c.incomplete = _.clone(this.incomplete); c.weight = this.weight; return c; }; /** * Commits test case and flushes changes state. * * @method */ Test.prototype.commit = function () { this.isChanged = false; }; /** * Calculates number of step usage in test case. * * @method * @arg {Step} step - Step which usage is calculated in test case. * @return {number} Number of step usage. */ Test.prototype._amount = function (step) { return this.steps.filter(s => s.name === step.name).length; }; module.exports = Test;