359 lines
No EOL
11 KiB
TypeScript
359 lines
No EOL
11 KiB
TypeScript
var tty = require("tty")
|
|
, readline = require("readline")
|
|
, fs = require("fs")
|
|
, path = require("path")
|
|
, child_process = require("child_process")
|
|
, os = require("os")
|
|
, mocha = require("mocha")
|
|
, Base = mocha.reporters.Base
|
|
, color = Base.color
|
|
, cursor = Base.cursor
|
|
, ms = require("mocha/lib/ms");
|
|
|
|
var isatty = tty.isatty(1) && tty.isatty(2);
|
|
var tapRangePattern = /^(\d+)\.\.(\d+)(?:$|\r\n?|\n)/;
|
|
var tapTestPattern = /^(not\sok|ok)\s+(\d+)\s+(?:-\s+)?(.*)$/;
|
|
var tapCommentPattern = /^#(?: (tests|pass|fail) (\d+)$)?/;
|
|
|
|
exports.runTestsInParallel = runTestsInParallel;
|
|
exports.ProgressBars = ProgressBars;
|
|
|
|
function runTestsInParallel(taskConfigsFolder, run, options, cb) {
|
|
if (options === undefined) options = { };
|
|
|
|
return discoverTests(run, options, function (error) {
|
|
if (error) {
|
|
return cb(error);
|
|
}
|
|
|
|
return runTests(taskConfigsFolder, run, options, cb);
|
|
});
|
|
}
|
|
|
|
function discoverTests(run, options, cb) {
|
|
console.log("Discovering tests...");
|
|
|
|
var cmd = "mocha -R " + require.resolve("./mocha-none-reporter.js") + " " + run;
|
|
var p = spawnProcess(cmd);
|
|
p.on("exit", function (status) {
|
|
if (status) {
|
|
cb(new Error("Process exited with code " + status));
|
|
}
|
|
else {
|
|
cb();
|
|
}
|
|
});
|
|
}
|
|
|
|
function runTests(taskConfigsFolder, run, options, cb) {
|
|
var configFiles = fs.readdirSync(taskConfigsFolder);
|
|
var numPartitions = configFiles.length;
|
|
if (numPartitions <= 0) {
|
|
cb();
|
|
return;
|
|
}
|
|
|
|
console.log("Running tests on " + numPartitions + " threads...");
|
|
|
|
var partitions = Array(numPartitions);
|
|
var progressBars = new ProgressBars();
|
|
progressBars.enable();
|
|
|
|
var counter = numPartitions;
|
|
configFiles.forEach(runTestsInPartition);
|
|
|
|
function runTestsInPartition(file, index) {
|
|
var partition = {
|
|
file: path.join(taskConfigsFolder, file),
|
|
tests: 0,
|
|
passed: 0,
|
|
failed: 0,
|
|
completed: 0,
|
|
current: undefined,
|
|
start: undefined,
|
|
end: undefined,
|
|
failures: []
|
|
};
|
|
partitions[index] = partition;
|
|
|
|
// Set up the progress bar.
|
|
updateProgress(0);
|
|
|
|
// Start the background process.
|
|
var cmd = "mocha -t " + (options.testTimeout || 20000) + " -R tap --no-colors " + run + " --config='" + partition.file + "'";
|
|
var p = spawnProcess(cmd);
|
|
var rl = readline.createInterface({
|
|
input: p.stdout,
|
|
terminal: false
|
|
});
|
|
rl.on("line", onmessage);
|
|
p.on("exit", onexit)
|
|
|
|
function onmessage(line) {
|
|
if (partition.start === undefined) {
|
|
partition.start = Date.now();
|
|
}
|
|
|
|
var rangeMatch = tapRangePattern.exec(line);
|
|
if (rangeMatch) {
|
|
partition.tests = parseInt(rangeMatch[2]);
|
|
return;
|
|
}
|
|
|
|
var testMatch = tapTestPattern.exec(line);
|
|
if (testMatch) {
|
|
var test = {
|
|
result: testMatch[1],
|
|
id: parseInt(testMatch[2]),
|
|
name: testMatch[3],
|
|
output: []
|
|
};
|
|
|
|
partition.current = test;
|
|
partition.completed++;
|
|
|
|
if (test.result === "ok") {
|
|
partition.passed++;
|
|
}
|
|
else {
|
|
partition.failed++;
|
|
partition.failures.push(test);
|
|
}
|
|
|
|
var progress = partition.completed / partition.tests;
|
|
if (progress < 1) {
|
|
updateProgress(progress);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
var commentMatch = tapCommentPattern.exec(line);
|
|
if (commentMatch) {
|
|
switch (commentMatch[1]) {
|
|
case "tests":
|
|
partition.current = undefined;
|
|
partition.tests = parseInt(commentMatch[2]);
|
|
break;
|
|
|
|
case "pass":
|
|
partition.passed = parseInt(commentMatch[2]);
|
|
break;
|
|
|
|
case "fail":
|
|
partition.failed = parseInt(commentMatch[2]);
|
|
break;
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
if (partition.current) {
|
|
partition.current.output.push(line);
|
|
}
|
|
}
|
|
|
|
function onexit() {
|
|
if (partition.end === undefined) {
|
|
partition.end = Date.now();
|
|
}
|
|
|
|
partition.duration = partition.end - partition.start;
|
|
var summaryColor = partition.failed ? "fail" : "green";
|
|
var summarySymbol = partition.failed ? Base.symbols.err : Base.symbols.ok;
|
|
var summaryTests = (partition.passed === partition.tests ? partition.passed : partition.passed + "/" + partition.tests) + " passing";
|
|
var summaryDuration = "(" + ms(partition.duration) + ")";
|
|
var savedUseColors = Base.useColors;
|
|
Base.useColors = !options.noColors;
|
|
|
|
var summary = color(summaryColor, summarySymbol + " " + summaryTests) + " " + color("light", summaryDuration);
|
|
Base.useColors = savedUseColors;
|
|
|
|
updateProgress(1, summary);
|
|
|
|
signal();
|
|
}
|
|
|
|
function updateProgress(percentComplete, title) {
|
|
var progressColor = "pending";
|
|
if (partition.failed) {
|
|
progressColor = "fail";
|
|
}
|
|
|
|
progressBars.update(
|
|
index,
|
|
percentComplete,
|
|
progressColor,
|
|
title
|
|
);
|
|
}
|
|
}
|
|
|
|
function signal() {
|
|
counter--;
|
|
|
|
if (counter <= 0) {
|
|
var reporter = new Base(),
|
|
stats = reporter.stats,
|
|
failures = reporter.failures;
|
|
|
|
var duration = 0;
|
|
for (var i = 0; i < numPartitions; i++) {
|
|
var partition = partitions[i];
|
|
stats.passes += partition.passed;
|
|
stats.failures += partition.failed;
|
|
stats.tests += partition.tests;
|
|
duration += partition.duration;
|
|
for (var j = 0; j < partition.failures.length; j++) {
|
|
var failure = partition.failures[j];
|
|
failures.push(makeMochaTest(failure));
|
|
}
|
|
}
|
|
|
|
stats.duration = duration;
|
|
progressBars.disable();
|
|
|
|
if (options.noColors) {
|
|
var savedUseColors = Base.useColors;
|
|
Base.useColors = false;
|
|
reporter.epilogue();
|
|
Base.useColors = savedUseColors;
|
|
}
|
|
else {
|
|
reporter.epilogue();
|
|
}
|
|
|
|
if (stats.failures) {
|
|
return cb(new Error("Test failures reported: " + stats.failures));
|
|
}
|
|
else {
|
|
return cb();
|
|
}
|
|
}
|
|
}
|
|
|
|
function makeMochaTest(test) {
|
|
return {
|
|
fullTitle: function() {
|
|
return test.name;
|
|
},
|
|
err: {
|
|
message: test.output[0],
|
|
stack: test.output.join(os.EOL)
|
|
}
|
|
};
|
|
}
|
|
}
|
|
|
|
function spawnProcess(cmd, options) {
|
|
var shell = process.platform === "win32" ? "cmd" : "/bin/sh";
|
|
var prefix = process.platform === "win32" ? "/c" : "-c";
|
|
return child_process.spawn(shell, [prefix, cmd], { windowsVerbatimArguments: true });
|
|
}
|
|
|
|
function ProgressBars(options) {
|
|
if (!options) options = {};
|
|
var open = options.open || '[';
|
|
var close = options.close || ']';
|
|
var complete = options.complete || '▬';
|
|
var incomplete = options.incomplete || Base.symbols.dot;
|
|
var maxWidth = Math.floor(Base.window.width * .30) - open.length - close.length - 2;
|
|
var width = minMax(options.width || maxWidth, 10, maxWidth);
|
|
this._options = {
|
|
open: open,
|
|
complete: complete,
|
|
incomplete: incomplete,
|
|
close: close,
|
|
width: width
|
|
};
|
|
|
|
this._progressBars = [];
|
|
this._lineCount = 0;
|
|
this._enabled = false;
|
|
}
|
|
ProgressBars.prototype = {
|
|
enable: function () {
|
|
if (!this._enabled) {
|
|
process.stdout.write(os.EOL);
|
|
this._enabled = true;
|
|
}
|
|
},
|
|
disable: function () {
|
|
if (this._enabled) {
|
|
process.stdout.write(os.EOL);
|
|
this._enabled = false;
|
|
}
|
|
},
|
|
update: function (index, percentComplete, color, title) {
|
|
percentComplete = minMax(percentComplete, 0, 1);
|
|
|
|
var progressBar = this._progressBars[index] || (this._progressBars[index] = { });
|
|
var width = this._options.width;
|
|
var n = Math.floor(width * percentComplete);
|
|
var i = width - n;
|
|
if (n === progressBar.lastN && title === progressBar.title && color === progressBar.progressColor) {
|
|
return;
|
|
}
|
|
|
|
progressBar.lastN = n;
|
|
progressBar.title = title;
|
|
progressBar.progressColor = color;
|
|
|
|
var progress = " ";
|
|
progress += this._color('progress', this._options.open);
|
|
progress += this._color(color, fill(this._options.complete, n));
|
|
progress += this._color('progress', fill(this._options.incomplete, i));
|
|
progress += this._color('progress', this._options.close);
|
|
|
|
if (title) {
|
|
progress += this._color('progress', ' ' + title);
|
|
}
|
|
|
|
if (progressBar.text !== progress) {
|
|
progressBar.text = progress;
|
|
this._render(index);
|
|
}
|
|
},
|
|
_render: function (index) {
|
|
if (!this._enabled || !isatty) {
|
|
return;
|
|
}
|
|
|
|
cursor.hide();
|
|
readline.moveCursor(process.stdout, -process.stdout.columns, -this._lineCount);
|
|
var lineCount = 0;
|
|
var numProgressBars = this._progressBars.length;
|
|
for (var i = 0; i < numProgressBars; i++) {
|
|
if (i === index) {
|
|
readline.clearLine(process.stdout, 1);
|
|
process.stdout.write(this._progressBars[i].text + os.EOL);
|
|
}
|
|
else {
|
|
readline.moveCursor(process.stdout, -process.stdout.columns, +1);
|
|
}
|
|
|
|
lineCount++;
|
|
}
|
|
|
|
this._lineCount = lineCount;
|
|
cursor.show();
|
|
},
|
|
_color: function (type, text) {
|
|
return type && !this._options.noColors ? color(type, text) : text;
|
|
}
|
|
};
|
|
|
|
function fill(ch, size) {
|
|
var s = "";
|
|
while (s.length < size) {
|
|
s += ch;
|
|
}
|
|
|
|
return s.length > size ? s.substr(0, size) : s;
|
|
}
|
|
|
|
function minMax(value, min, max) {
|
|
if (value < min) return min;
|
|
if (value > max) return max;
|
|
return value;
|
|
} |