Source: cluster.js

"use strict";

/**
 * **Is used to execute tests concurrently in subprocesses.**
 *
 * Scheduler process is named `master`, subprocesses with tests are `slaves`.
 * `Master` uses simple scheduling, splitting tests on parts by `slaves` number.
 *
 * @module
 */

const fs = require("fs");
const path = require("path");

require("colors");
const _ = require("lodash");
const fse = require("fs-extra");
const spawn = require("cross-spawn");
const U = require("glace-utils");

const CONF = require("./config");
const tools = require("./tools");

module.exports = {
    /**
     * Launches tests in subprocesses.
     *
     * @async
     * @instance
     * @function
     * @arg {function} [cb] - Callback function executing at the end.
     * @arg {number} cb.exitCode - Subprocesses summary exit code.
     * @return {Promise<number|*>} Exit code if callback isn't passed,
     * or result providing by callback.
     */
    launch: async cb => {
        cb = cb || (o => o);

        resetArtifactsDir();
        await killProcs();

        const argv = _.clone(process.argv);
        const cmd = argv.shift();
        const testIds = getTestIds();
        const procs = [];

        _.range(1, CONF.cluster.slavesNum + 1).forEach(i => {
            procs.push(new Promise(launchSlave(i, cmd, argv, testIds)));
        });

        const codes = await Promise.all(procs);
        if (fs.existsSync(CONF.report.dir)) U.clearEmptyFolders(CONF.report.dir);
        printArtifactsDir();

        const resultCode = calcExitCode(codes);
        return cb(resultCode);
    },
};

/**
 * Kills requested processes one time in `master`. In `slaves` killing is disabled.
 *
 * @ignore
 */
const killProcs = async () => {
    for (const procName of (CONF.session.killProcs || [])) {
        await U.killProcs(procName);
    }
};

/**
 * Artifacts folder includes `master` report and `slaves` reports.
 *
 * @ignore
 */
const resetArtifactsDir = () => {
    if (CONF.report.clear && fs.existsSync(CONF.cluster.artifactsDir)) {
        fse.removeSync(CONF.cluster.artifactsDir);
    }
    fse.mkdirsSync(CONF.cluster.artifactsDir);
};

const printArtifactsDir = () => {
    console.log();
    const reportMsg = "Artifacts are in " + CONF.cluster.artifactsDir;
    console.log(Array(reportMsg.length + 1).join("-").yellow);
    console.log(reportMsg.yellow);
};

const calcExitCode = codes => {
    let exitCode = 0;
    for (const code of codes) {
        if (code === 0) continue;
        exitCode = Math.min(exitCode + code, 255);
    }
    return exitCode;
};

const getTestIds = () => {
    tools.fakeLoad();
    let testIds = _.shuffle(CONF.test.cases.map(c => c.id));
    return _.chunk(testIds, Math.ceil(testIds.length / CONF.cluster.slavesNum));
};

const launchSlave = (i, cmd, argv, testIds) => resolve => {

    const env = _.clone(process.env);
    env.GLACE_SLAVE_ID = i;
    env.GLACE_TEST_IDS = testIds[i - 1];
    const opts = { env };

    const stream = fs.createWriteStream(
        path.resolve(CONF.cluster.artifactsDir, `slave-${i}.stdout`));

    console.log(`Slave #${i} is working...`.yellow);
    const proc = spawn(cmd, argv, opts);
    proc.stdout.pipe(stream);
    proc.stderr.pipe(stream);
    proc.on("close", endSlave(resolve, i));
};

const endSlave = (resolve, i) => code => {
    if (code === 0) {
        console.log(`Slave #${i} is succeeded`.green);
    } else {
        console.log(`Slave #${i} is failed with code ${code}`.red);
    }
    resolve(code);
};