Source: test.js

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