"use strict"; /** * Glace tools. * * @module */ const util = require("util"); require("colors"); const _ = require("lodash"); const expect = require("chai").expect; const highlight = require("cli-highlight").highlight; const Testrail = require("testrail-api"); const U = require("glace-utils"); const classifier = require("./classifier")(); const CONF = require("./config"); const plugins = require("./plugins"); const utils = require("./utils"); const d = U.switchColor(); let stepsCache = null; /** * Get list of steps data, where step data is an object with keys: * `name` - name of step, `decription` - short details of steps, * `doc` - documentation of step. * * @memberOf module:tools * @function * @arg {string} [filter] - String chunk to filter steps. * @arg {boolean} [namesOnly=false] - Flag to filter by step names only. * @return {array<object>} */ const listSteps = (filter, namesOnly=false) => { if (!stepsCache) { stepsCache = getStepsData(getStepNames()); learnClassifier(stepsCache); } if (!filter) return stepsCache; return filterSteps(stepsCache, filter, namesOnly); }; /** * Print list of steps in stdout. * * @memberOf module:tools * @function * @arg {string} [filter] - String chunk to filter steps. * @arg {boolean} [namesOnly=false] - Flag to filter by step names only. */ const printSteps = (filter, namesOnly=false) => { const steps = listSteps(filter, namesOnly); if (!steps.length) { console.log("No steps are found".yellow); return; } steps.reverse().forEach((step, i) => { console.log(d(`${i+1}. ${step.name}:`)); console.log(d(step.description)); if (step.doc) console.log(highlight(step.doc, { language: "js" })); }); }; /** * Print list of implemented test cases. * * <img src="./list_tests.gif" title="listTests example" /> * * @memberOf module:tools * @name listTests * @function * @arg {string} [filter] - String chunk to filter test cases. */ const printTests = filter => { fakeLoad(); let cases = CONF.test.cases; if (filter) cases = cases.filter(c => U.textContains(c.name, filter)); if (!cases.length) { console.log("No tests are found".yellow); return; } cases.forEach((c, i) => { console.log(d(`${i+1}. ${c.name}`)); }); }; /** * Print list of available plugins. * * <img src="./list_plugins.gif" title="listPlugins example" /> * * @memberOf module:tools * @name listPlugins * @function */ const printPlugins = () => { const pluginsList = plugins.get(); if (!pluginsList.length) { console.log("No plugins are detected".yellow); return; } pluginsList.forEach((p, i) => { console.log(`${i+1}. ${p.name}`.yellow, p.path); }); }; /** * Get list of available fixtures. * * @memberOf module:tools * @function * @arg {string} [filter] - String chunk to filter fixtures. * @arg {boolean} [namesOnly=false] - Flag to filter by fixture names only. * @return {array<object>} */ const listFixtures = (filter, namesOnly=false) => { fakeLoad(); const fixtures = getFixtures(); if (!filter) return fixtures; return filterFixtures(fixtures, filter, namesOnly); }; /** * Print list of fixtures in stdout. * * @memberOf module:tools * @function * @arg {string} [filter] - String chunk to filter fixtures. * @arg {boolean} [namesOnly=false] - Flag to filter by fixture names only. */ const printFixtures = (filter, namesOnly=false) => { const fixtures = listFixtures(filter, namesOnly); if (!fixtures.length) { console.log("No fixtures are found".yellow); return; } fixtures.forEach((fx, i) => { console.log(d(`${i+1}. ${fx.name}`)); if (fx.doc) console.log(highlight(fx.doc, { language: "js" })); }); }; /** * Make fake load of tests in order to collect tests, fixtures, steps, etc. * * @memberOf module:tools * @function */ const fakeLoad = () => { const dummy = () => {}; global.before = dummy; global.after = dummy; global.beforeEach = dummy; global.afterEach = dummy; global.it = dummy; global.describe = (name, cb) => { cb.call({ retries: dummy, timeout: dummy, }); }; require("./globals"); require("./loader"); }; /** * Check testrail cases consistency with implemented tests. * * @memberOf module:tools * @function * @arg {function} cb - Callback function. */ const checkTestrail = cb => { checkTestrailOpts(); fakeLoad(); console.log("TestRail connecting...".yellow); const testrail = new Testrail({ host: CONF.testrail.host, user: CONF.testrail.user, password: CONF.testrail.token }); checkTestrailCases(testrail, cb); }; module.exports = { checkTestrail, fakeLoad, listSteps, printSteps, printTests, printPlugins, listFixtures, printFixtures, }; /** * Check testrail options. * @ignore */ const checkTestrailOpts = () => { for (const opt in CONF.testrail) { expect(CONF.testrail[opt], `TestRail option '${opt}' isn't specified in config`) .to.exist; } }; /** * Check testrail missed cases which are implemented. * @ignore */ const checkTestrailMissed = cases => { const testrailNames = cases.map(case_ => case_.title); const testNames = CONF.test.cases.map(case_ => case_.name); const missed = _.difference(testNames, testrailNames); if (!missed.length) return 0; console.log("\nMissed TestRail cases:".magenta.bold); missed.forEach((title, i) => { console.log(`${i+1}. ${title}`.cyan.bold); }); return 1; }; /** * Check testrail cases which are not implemented yet. * @ignore */ const checkTestrailNotImplemented = cases => { const testrailNames = cases.map(case_ => case_.title); const testNames = CONF.test.cases.map(case_ => case_.name); const notImplemented = _.difference(testrailNames, testNames); if (!notImplemented.length) return 0; console.log("\nNot implemented TestRail cases:".magenta.bold); notImplemented.forEach((title, i) => { console.log(`${i+1}. ${title}`.cyan.bold); }); return 1; }; /** * Check testrail duplicated cases. * @ignore */ const checkTestrailDuplicates = cases => { const testrailNames = []; let testrailDups = []; cases.forEach(case_ => { if (testrailNames.includes(case_.title)) { testrailDups.push(case_.title); } else { testrailNames.push(case_.title); } }); testrailDups = _.uniq(testrailDups); if (!testrailDups.length) return 0; console.log("\nTestRail duplicated cases:".magenta.bold); testrailDups.forEach((title, i) => { console.log(`${i+1}. ${title}`.cyan.bold); }); return 1; }; /** * Check testrail cases. * @ignore */ const checkTestrailCases = (client, cb) => { client.getCases( CONF.testrail.projectId, { suite_id: CONF.testrail.suiteId }, (err, response, cases) => { if (err) { console.log(err); cb(1); return; } let errorCode = 0; errorCode += checkTestrailDuplicates(cases); errorCode += checkTestrailNotImplemented(cases); errorCode += checkTestrailMissed(cases); if (!errorCode) { console.log("TestRail cases correspond current tests".green.bold); } console.log( "\nTestRail suite is", `${CONF.testrail.host}/index.php?/suites/view/${CONF.testrail.suiteId}`.yellow); cb(errorCode); }); }; /** * Get fixtures. * @ignore */ const getFixtures = () => { const fixtures = []; for (const name in global) { if (!name.startsWith("fx")) continue; const func = global[name]; if (!util.isFunction(func)) continue; fixtures.push({ name: name, doc: utils.getDoc(func), }); } return fixtures; }; /** * Filter fixtures. * @ignore */ const filterFixtures = (fixtures, filter, namesOnly) => { const filtered = []; for (const fx of fixtures) { if (namesOnly) { if (!U.textContains(fx.name, filter)) continue; } else { if (!U.textContains(fx.name, filter) && !U.textContains(fx.doc, filter)) continue; } filtered.push(fx); } return filtered; }; /** * Get list of step names. * * @ignore * @function * @return {array<string>} */ const getStepNames = () => { global.$ || fakeLoad(); const NOT_STEPS = [ "constructor", "hasOwnProperty", "isPrototypeOf", "propertyIsEnumerable", "toLocaleString", "toString", "valueOf", ]; let names = []; for (const key in $) { names.push(key); }; names = _.union( names, Object.getOwnPropertyNames($), Object.getOwnPropertyNames(Object.getPrototypeOf($)) ).sort() .filter(i => !i.startsWith("_")) .filter(i => /^\w+$/.test(i)) .filter(i => !NOT_STEPS.includes(i)) .filter(i => /^\D/.test(i)); return names; }; /** * Get list of step data, where step data is an object with keys: * `name` - name of step, `decription` - short details of steps, * `doc` - documentation of step. * * @ignore * @function * @arg {array<string>} names - List of step names. * @return {array<object>} */ const getStepsData = names => { const result = []; for (const name of names) { const func = $[name]; if (!util.isFunction(func)) continue; result.push({ name: name, description: funcDescription(func), doc: utils.getDoc(func), }); }; return result; }; /** * Learn classifier on step names and description. * @ignore */ const learnClassifier = steps => { steps.forEach(step => classifier.learn(step.doc, step.name)); }; /** * Filter steps and return relevant result merged with ML predictions. * @ignore */ const filterSteps = (steps, filter, namesOnly) => { let filtered = []; for (const step of steps) { if (namesOnly) { if (!U.textContains(step.name, filter)) continue; } else { if (!U.textContains(step.name, filter) && !U.textContains(step.doc, filter)) continue; } filtered.push(step); } if (!namesOnly) { const classified = classifySteps(steps, filter); filtered = mergeSteps(classified, filtered); } return filtered; }; /** * Get classified steps based on ML. * @ignore */ const classifySteps = (steps, filter) => { const result = []; const sampling = classifier.classify(filter); for (const sample of sampling) { for (const step of steps) { if (sample.label === step.name) result.push(step); } } return result; }; /** * Merge steps. * @ignore */ const mergeSteps = (base, merging) => { const usedNames = base.map(s => s.name); const result = _.clone(base); for (const step of merging) { if (!usedNames.includes(step.name)) result.push(step); } return result; }; /** * Get function description. * @ignore */ const funcDescription = func => { return " " + func.toString().replace("\n", " ").split(/\) *\{/)[0] + ") {...}"; };