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