diff --git a/Jakefile.js b/Jakefile.js index a449dfcfa9..3b75dfb6ad 100644 --- a/Jakefile.js +++ b/Jakefile.js @@ -5,6 +5,7 @@ var os = require("os"); var path = require("path"); var child_process = require("child_process"); var Linter = require("tslint"); +var runTestsInParallel = require("./scripts/mocha-parallel").runTestsInParallel; // Variables var compilerDirectory = "src/compiler/"; @@ -683,7 +684,6 @@ function cleanTestDirs() { // used to pass data from jake command line directly to run.js function writeTestConfigFile(tests, light, taskConfigsFolder, workerCount) { var testConfigContents = JSON.stringify({ test: tests ? [tests] : undefined, light: light, workerCount: workerCount, taskConfigsFolder: taskConfigsFolder }); - console.log('Running tests with config: ' + testConfigContents); fs.writeFileSync('test.config', testConfigContents); } @@ -744,42 +744,16 @@ function runConsoleTests(defaultReporter, runInParallel) { } else { - // run task to load all tests and partition them between workers - var cmd = "mocha " + " -R min " + colors + run; - console.log(cmd); - exec(cmd, function() { - // read all configuration files and spawn a worker for every config - var configFiles = fs.readdirSync(taskConfigsFolder); - var counter = configFiles.length; - var firstErrorStatus; - // schedule work for chunks - configFiles.forEach(function (f) { - var configPath = path.join(taskConfigsFolder, f); - var workerCmd = "mocha" + " -t " + testTimeout + " -R " + reporter + " " + colors + " " + run + " --config='" + configPath + "'"; - console.log(workerCmd); - exec(workerCmd, finishWorker, finishWorker) - }); - - function finishWorker(e, errorStatus) { - counter--; - if (firstErrorStatus === undefined && errorStatus !== undefined) { - firstErrorStatus = errorStatus; - } - if (counter !== 0) { - complete(); - } - else { - // last worker clean everything and runs linter in case if there were no errors - deleteTemporaryProjectOutput(); - jake.rmRf(taskConfigsFolder); - if (firstErrorStatus === undefined) { - runLinter(); - complete(); - } - else { - failWithStatus(firstErrorStatus); - } - } + runTestsInParallel(taskConfigsFolder, run, { testTimeout: testTimeout, noColors: colors === " --no-colors " }, function (err) { + // last worker clean everything and runs linter in case if there were no errors + deleteTemporaryProjectOutput(); + jake.rmRf(taskConfigsFolder); + if (err) { + fail(err); + } + else { + runLinter(); + complete(); } }); } diff --git a/scripts/mocha-none-reporter.js b/scripts/mocha-none-reporter.js new file mode 100644 index 0000000000..5787b0c042 --- /dev/null +++ b/scripts/mocha-none-reporter.js @@ -0,0 +1,26 @@ +/** + * Module dependencies. + */ + +var Base = require('mocha').reporters.Base; + +/** + * Expose `None`. + */ + +exports = module.exports = None; + +/** + * Initialize a new `None` test reporter. + * + * @api public + * @param {Runner} runner + */ +function None(runner) { + Base.call(this); +} + +/** + * Inherit from `Base.prototype`. + */ +None.prototype.__proto__ = Base.prototype; diff --git a/scripts/mocha-parallel.js b/scripts/mocha-parallel.js new file mode 100644 index 0000000000..adba8ebf57 --- /dev/null +++ b/scripts/mocha-parallel.js @@ -0,0 +1,367 @@ +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 = child_process.spawn( + process.platform === "win32" ? "cmd" : "/bin/sh", + process.platform === "win32" ? ["/c", cmd] : ["-c", cmd], { + windowsVerbatimArguments: true, + env: { NODE_ENV: "development" } + }); + + p.on("exit", function (status) { + if (status) { + cb(new Error("Process exited width 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 = child_process.spawn( + process.platform === "win32" ? "cmd" : "/bin/sh", + process.platform === "win32" ? ["/c", cmd] : ["-c", cmd], { + windowsVerbatimArguments: true, + env: { NODE_ENV: "development" } + }); + + 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 failed = 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 (failed) { + return cb(new Error("Test failures reported: " + failed)); + } + else { + return cb(); + } + } + } + + function makeMochaTest(test) { + return { + fullTitle: function() { + return test.name; + }, + err: { + message: test.output[0], + stack: test.output.join(os.EOL) + } + }; + } +} + +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; +} \ No newline at end of file