From a0e4fc304fbfd712fc0f56c64fb446fe737cb816 Mon Sep 17 00:00:00 2001 From: Bart van der Schoor Date: Sat, 1 Mar 2014 22:40:41 +0100 Subject: [PATCH] parallelized tester added TestQueue class suites now have TestQueue instance that queues and runs Tests defaults to two (or more if more cores available) - Travis has '1.5 virtual cores' (weird) dropped index parameter from suite callback added index parameter to Reporter dropped double Suite::filterTargetFiles calls small fix in bluebird defs also print some system info in tester header --- _infrastructure/tests/runner.js | 134 +++++++++++++----- _infrastructure/tests/runner.ts | 59 +++++++- _infrastructure/tests/src/printer.ts | 19 ++- .../tests/src/reporter/reporter.ts | 19 +-- _infrastructure/tests/src/suite/suite.ts | 21 +-- _infrastructure/tests/src/suite/tscParams.ts | 8 +- _infrastructure/tests/src/tsc.ts | 17 ++- .../tests/typings/bluebird/bluebird.d.ts | 1 + 8 files changed, 210 insertions(+), 68 deletions(-) diff --git a/_infrastructure/tests/runner.js b/_infrastructure/tests/runner.js index e21bbd42be..0512c05e27 100644 --- a/_infrastructure/tests/runner.js +++ b/_infrastructure/tests/runner.js @@ -78,7 +78,8 @@ var DT; function Tsc() { } Tsc.run = function (tsfile, options) { - return Promise.attempt(function () { + var tscPath; + return new Promise.attempt(function () { options = options || {}; options.tscVersion = options.tscVersion || DT.DEFAULT_TSC_VERSION; if (typeof options.checkNoImplicitAny === 'undefined') { @@ -87,15 +88,21 @@ var DT; if (typeof options.useTscParams === 'undefined') { options.useTscParams = true; } - if (!fs.existsSync(tsfile)) { + return DT.fileExists(tsfile); + }).then(function (exists) { + if (!exists) { throw new Error(tsfile + ' not exists'); } - var tscPath = './_infrastructure/tests/typescript/' + options.tscVersion + '/tsc.js'; - if (!fs.existsSync(tscPath)) { + tscPath = './_infrastructure/tests/typescript/' + options.tscVersion + '/tsc.js'; + return DT.fileExists(tscPath); + }).then(function (exists) { + if (!exists) { throw new Error(tscPath + ' is not exists'); } + return DT.fileExists(tsfile + '.tscparams'); + }).then(function (exists) { var command = 'node ' + tscPath + ' --module commonjs '; - if (options.useTscParams && fs.existsSync(tsfile + '.tscparams')) { + if (options.useTscParams && exists) { command += '@' + tsfile + '.tscparams'; } else if (options.checkNoImplicitAny) { command += '--noImplicitAny'; @@ -437,6 +444,8 @@ var DT; /// var DT; (function (DT) { + var os = require('os'); + ///////////////////////////////// // All the common things that we print are functions of this class ///////////////////////////////// @@ -462,11 +471,14 @@ var DT; Print.prototype.printChangeHeader = function () { this.out('=============================================================================\n'); - this.out(' \33[36m\33[1mDefinitelyTyped Diff Detector 0.1.0\33[0m \n'); + this.out(' \33[36m\33[1mDefinitelyTyped Diff Detector 0.1.0\33[0m \n'); this.out('=============================================================================\n'); }; - Print.prototype.printHeader = function () { + Print.prototype.printHeader = function (options) { + var totalMem = Math.round(os.totalmem() / 1024 / 1024) + ' mb'; + var freemem = Math.round(os.freemem() / 1024 / 1024) + ' mb'; + this.out('=============================================================================\n'); this.out(' \33[36m\33[1mDefinitelyTyped Test Runner 0.5.0\33[0m\n'); this.out('=============================================================================\n'); @@ -474,6 +486,10 @@ var DT; this.out(' \33[36m\33[1mTypings :\33[0m ' + this.typings + '\n'); this.out(' \33[36m\33[1mTests :\33[0m ' + this.tests + '\n'); this.out(' \33[36m\33[1mTypeScript files :\33[0m ' + this.tsFiles + '\n'); + this.out(' \33[36m\33[1mTotal Memory :\33[0m ' + totalMem + '\n'); + this.out(' \33[36m\33[1mFree Memory :\33[0m ' + freemem + '\n'); + this.out(' \33[36m\33[1mCores :\33[0m ' + os.cpus().length + '\n'); + this.out(' \33[36m\33[1mConcurrent :\33[0m ' + options.concurrent + '\n'); }; Print.prototype.printSuiteHeader = function (title) { @@ -578,12 +594,12 @@ var DT; } }; - Print.prototype.printTestComplete = function (testResult, index) { + Print.prototype.printTestComplete = function (testResult) { var reporter = testResult.hostedBy.testReporter; if (testResult.success) { - reporter.printPositiveCharacter(index, testResult); + reporter.printPositiveCharacter(testResult); } else { - reporter.printNegativeCharacter(index, testResult); + reporter.printNegativeCharacter(testResult); } }; @@ -709,17 +725,18 @@ var DT; var DefaultTestReporter = (function () { function DefaultTestReporter(print) { this.print = print; + this.index = 0; } - DefaultTestReporter.prototype.printPositiveCharacter = function (index, testResult) { + DefaultTestReporter.prototype.printPositiveCharacter = function (testResult) { this.print.out('\33[36m\33[1m' + '.' + '\33[0m'); - - this.printBreakIfNeeded(index); + this.index++; + this.printBreakIfNeeded(this.index); }; - DefaultTestReporter.prototype.printNegativeCharacter = function (index, testResult) { + DefaultTestReporter.prototype.printNegativeCharacter = function (testResult) { this.print.out('x'); - - this.printBreakIfNeeded(index); + this.index++; + this.printBreakIfNeeded(this.index); }; DefaultTestReporter.prototype.printBreakIfNeeded = function (index) { @@ -751,6 +768,7 @@ var DT; this.timer = new DT.Timer(); this.testResults = []; this.printErrorCount = true; + this.queue = new DT.TestQueue(options.concurrent); } TestSuiteBase.prototype.filterTargetFiles = function (files) { throw new Error('please implement this method'); @@ -759,14 +777,15 @@ var DT; TestSuiteBase.prototype.start = function (targetFiles, testCallback) { var _this = this; this.timer.start(); + return this.filterTargetFiles(targetFiles).then(function (targetFiles) { - return Promise.reduce(targetFiles, function (count, targetFile) { + // tests get queued for multi-threading + return Promise.all(targetFiles.map(function (targetFile) { return _this.runTest(targetFile).then(function (result) { - testCallback(result, count + 1); - return count++; + testCallback(result); }); - }, 0); - }).then(function (count) { + })); + }).then(function () { _this.timer.end(); return _this; }); @@ -774,7 +793,9 @@ var DT; TestSuiteBase.prototype.runTest = function (targetFile) { var _this = this; - return new DT.Test(this, targetFile, { tscVersion: this.options.tscVersion }).run().then(function (result) { + return this.queue.run(new DT.Test(this, targetFile, { + tscVersion: this.options.tscVersion + })).then(function (result) { _this.testResults.push(result); return result; }); @@ -885,10 +906,10 @@ var DT; this.printErrorCount = false; this.testReporter = { - printPositiveCharacter: function (index, testResult) { + printPositiveCharacter: function (testResult) { _this.print.clearCurrentLine().printTypingsWithoutTestName(testResult.targetFile.filePathWithName); }, - printNegativeCharacter: function (index, testResult) { + printNegativeCharacter: function (testResult) { } }; } @@ -904,11 +925,11 @@ var DT; var _this = this; this.print.clearCurrentLine().out(targetFile.filePathWithName); - return new DT.Test(this, targetFile, { + return this.queue.run(new DT.Test(this, targetFile, { tscVersion: this.options.tscVersion, useTscParams: false, checkNoImplicitAny: true - }).run().then(function (result) { + })).then(function (result) { _this.testResults.push(result); _this.print.clearCurrentLine(); return result; @@ -949,6 +970,7 @@ var DT; var Lazy = require('lazy.js'); var Promise = require('bluebird'); + var os = require('os'); var fs = require('fs'); var path = require('path'); var assert = require('assert'); @@ -985,6 +1007,55 @@ var DT; })(); DT.Test = Test; + ///////////////////////////////// + // Parallel execute Tests + ///////////////////////////////// + var TestQueue = (function () { + function TestQueue(concurrent) { + this.queue = []; + this.active = []; + this.concurrent = Math.max(1, concurrent); + } + // add to queue and return a promise + TestQueue.prototype.run = function (test) { + var _this = this; + var defer = Promise.defer(); + + // add a closure to queue + this.queue.push(function () { + // when activate, add test to active list + _this.active.push(test); + + // run it + var p = test.run(); + p.then(defer.resolve.bind(defer), defer.reject.bind(defer)); + p.finally(function () { + var i = _this.active.indexOf(test); + if (i > -1) { + _this.active.splice(i, 1); + } + _this.step(); + }); + }); + this.step(); + + // defer it + return defer.promise; + }; + + TestQueue.prototype.step = function () { + var _this = this; + // setTimeout to make it flush + setTimeout(function () { + while (_this.queue.length > 0 && _this.active.length < _this.concurrent) { + _this.queue.pop().call(null); + } + }, 1); + }; + return TestQueue; + })(); + DT.TestQueue = TestQueue; + ///////////////////////////////// // Test results ///////////////////////////////// @@ -1091,7 +1162,7 @@ var DT; ]); }).spread(function (syntaxFiles, testFiles) { _this.print.init(syntaxFiles.length, testFiles.length, files.length); - _this.print.printHeader(); + _this.print.printHeader(_this.options); if (_this.options.findNotRequiredTscparams) { _this.addSuite(new DT.FindNotRequiredTscparams(_this.options, _this.print)); @@ -1102,10 +1173,8 @@ var DT; _this.print.printSuiteHeader(suite.testSuiteName); - return suite.filterTargetFiles(files).then(function (targetFiles) { - return suite.start(targetFiles, function (testResult, index) { - _this.print.printTestComplete(testResult, index); - }); + return suite.start(files, function (testResult) { + _this.print.printTestComplete(testResult); }).then(function (suite) { _this.print.printSuiteComplete(suite); return count++; @@ -1186,12 +1255,13 @@ var DT; }); var tscVersionIndex = process.argv.indexOf('--tsc-version'); var tscVersion = DT.DEFAULT_TSC_VERSION; + var cpuCores = os.cpus().length; if (tscVersionIndex > -1) { tscVersion = process.argv[tscVersionIndex + 1]; } - var runner = new TestRunner(dtPath, { + concurrent: Math.max(cpuCores, 2), tscVersion: tscVersion, findNotRequiredTscparams: findNotRequiredTscparams }); diff --git a/_infrastructure/tests/runner.ts b/_infrastructure/tests/runner.ts index 4e72001b88..fafe2b1454 100644 --- a/_infrastructure/tests/runner.ts +++ b/_infrastructure/tests/runner.ts @@ -25,6 +25,7 @@ module DT { var Lazy: LazyJS.LazyStatic = require('lazy.js'); var Promise: typeof Promise = require('bluebird'); + var os = require('os'); var fs = require('fs'); var path = require('path'); var assert = require('assert'); @@ -56,6 +57,52 @@ module DT { } } + ///////////////////////////////// + // Parallel execute Tests + ///////////////////////////////// + export class TestQueue { + + private queue: Function[] = []; + private active: Test[] = []; + private concurrent: number; + + constructor(concurrent: number) { + this.concurrent = Math.max(1, concurrent); + } + + // add to queue and return a promise + run(test: Test): Promise { + var defer = Promise.defer(); + // add a closure to queue + this.queue.push(() => { + // when activate, add test to active list + this.active.push(test); + // run it + var p = test.run(); + p.then(defer.resolve.bind(defer), defer.reject.bind(defer)); + p.finally(() => { + var i = this.active.indexOf(test); + if (i > -1) { + this.active.splice(i, 1); + } + this.step(); + }); + }); + this.step(); + // defer it + return defer.promise; + } + + private step(): void { + // setTimeout to make it flush + setTimeout(() => { + while (this.queue.length > 0 && this.active.length < this.concurrent) { + this.queue.pop().call(null); + } + }, 1); + } + } + ///////////////////////////////// // Test results ///////////////////////////////// @@ -75,6 +122,7 @@ module DT { export interface ITestRunnerOptions { tscVersion:string; + concurrent?:number; findNotRequiredTscparams?:boolean; } @@ -166,7 +214,7 @@ module DT { ]); }).spread((syntaxFiles, testFiles) => { this.print.init(syntaxFiles.length, testFiles.length, files.length); - this.print.printHeader(); + this.print.printHeader(this.options); if (this.options.findNotRequiredTscparams) { this.addSuite(new FindNotRequiredTscparams(this.options, this.print)); @@ -177,10 +225,8 @@ module DT { this.print.printSuiteHeader(suite.testSuiteName); - return suite.filterTargetFiles(files).then((targetFiles) => { - return suite.start(targetFiles, (testResult, index) => { - this.print.printTestComplete(testResult, index); - }); + return suite.start(files, (testResult) => { + this.print.printTestComplete(testResult); }).then((suite) => { this.print.printSuiteComplete(suite); return count++; @@ -256,12 +302,13 @@ module DT { var findNotRequiredTscparams = process.argv.some(arg => arg == '--try-without-tscparams'); var tscVersionIndex = process.argv.indexOf('--tsc-version'); var tscVersion = DEFAULT_TSC_VERSION; + var cpuCores = os.cpus().length; if (tscVersionIndex > -1) { tscVersion = process.argv[tscVersionIndex + 1]; } - var runner = new TestRunner(dtPath, { + concurrent: Math.max(cpuCores, 2), tscVersion: tscVersion, findNotRequiredTscparams: findNotRequiredTscparams }); diff --git a/_infrastructure/tests/src/printer.ts b/_infrastructure/tests/src/printer.ts index 962d7b644d..565c4268a7 100644 --- a/_infrastructure/tests/src/printer.ts +++ b/_infrastructure/tests/src/printer.ts @@ -3,6 +3,8 @@ module DT { + var os = require('os'); + ///////////////////////////////// // All the common things that we print are functions of this class ///////////////////////////////// @@ -35,11 +37,14 @@ module DT { public printChangeHeader() { this.out('=============================================================================\n'); - this.out(' \33[36m\33[1mDefinitelyTyped Diff Detector 0.1.0\33[0m \n'); + this.out(' \33[36m\33[1mDefinitelyTyped Diff Detector 0.1.0\33[0m \n'); this.out('=============================================================================\n'); } - public printHeader() { + public printHeader(options: ITestRunnerOptions) { + var totalMem = Math.round(os.totalmem() / 1024 / 1024) + ' mb'; + var freemem = Math.round(os.freemem() / 1024 / 1024) + ' mb'; + this.out('=============================================================================\n'); this.out(' \33[36m\33[1mDefinitelyTyped Test Runner 0.5.0\33[0m\n'); this.out('=============================================================================\n'); @@ -47,6 +52,10 @@ module DT { this.out(' \33[36m\33[1mTypings :\33[0m ' + this.typings + '\n'); this.out(' \33[36m\33[1mTests :\33[0m ' + this.tests + '\n'); this.out(' \33[36m\33[1mTypeScript files :\33[0m ' + this.tsFiles + '\n'); + this.out(' \33[36m\33[1mTotal Memory :\33[0m ' + totalMem + '\n'); + this.out(' \33[36m\33[1mFree Memory :\33[0m ' + freemem + '\n'); + this.out(' \33[36m\33[1mCores :\33[0m ' + os.cpus().length + '\n'); + this.out(' \33[36m\33[1mConcurrent :\33[0m ' + options.concurrent + '\n'); } public printSuiteHeader(title: string) { @@ -150,13 +159,13 @@ module DT { } } - public printTestComplete(testResult: TestResult, index: number): void { + public printTestComplete(testResult: TestResult): void { var reporter = testResult.hostedBy.testReporter; if (testResult.success) { - reporter.printPositiveCharacter(index, testResult); + reporter.printPositiveCharacter(testResult); } else { - reporter.printNegativeCharacter(index, testResult); + reporter.printNegativeCharacter(testResult); } } diff --git a/_infrastructure/tests/src/reporter/reporter.ts b/_infrastructure/tests/src/reporter/reporter.ts index 783eae287a..736ac413cc 100644 --- a/_infrastructure/tests/src/reporter/reporter.ts +++ b/_infrastructure/tests/src/reporter/reporter.ts @@ -7,27 +7,30 @@ module DT { // for example, . and x ///////////////////////////////// export interface ITestReporter { - printPositiveCharacter(index: number, testResult: TestResult):void; - printNegativeCharacter(index: number, testResult: TestResult):void; + printPositiveCharacter(testResult: TestResult):void; + printNegativeCharacter(testResult: TestResult):void; } ///////////////////////////////// // Default test reporter ///////////////////////////////// export class DefaultTestReporter implements ITestReporter { + + index = 0; + constructor(public print: Print) { } - public printPositiveCharacter(index: number, testResult: TestResult) { + public printPositiveCharacter(testResult: TestResult) { this.print.out('\33[36m\33[1m' + '.' + '\33[0m'); - - this.printBreakIfNeeded(index); + this.index++; + this.printBreakIfNeeded(this.index); } - public printNegativeCharacter(index: number, testResult: TestResult) { + public printNegativeCharacter( testResult: TestResult) { this.print.out('x'); - - this.printBreakIfNeeded(index); + this.index++; + this.printBreakIfNeeded(this.index); } private printBreakIfNeeded(index: number) { diff --git a/_infrastructure/tests/src/suite/suite.ts b/_infrastructure/tests/src/suite/suite.ts index 4de10ce1d6..5909f87c63 100644 --- a/_infrastructure/tests/src/suite/suite.ts +++ b/_infrastructure/tests/src/suite/suite.ts @@ -32,31 +32,36 @@ module DT { testResults: TestResult[] = []; testReporter: ITestReporter; printErrorCount = true; + queue: TestQueue; constructor(public options: ITestRunnerOptions, public testSuiteName: string, public errorHeadline: string) { + this.queue = new TestQueue(options.concurrent); } public filterTargetFiles(files: File[]): Promise { throw new Error('please implement this method'); } - public start(targetFiles: File[], testCallback: (result: TestResult, index: number) => void): Promise { + public start(targetFiles: File[], testCallback: (result: TestResult) => void): Promise { this.timer.start(); + return this.filterTargetFiles(targetFiles).then((targetFiles) => { - return Promise.reduce(targetFiles, (count, targetFile) => { + // tests get queued for multi-threading + return Promise.all(targetFiles.map((targetFile) => { return this.runTest(targetFile).then((result) => { - testCallback(result, count + 1); - return count++; - }); - }, 0); - }).then((count: number) => { + testCallback(result); + }) + })); + }).then(() => { this.timer.end(); return this; }); } public runTest(targetFile: File): Promise { - return new Test(this, targetFile, {tscVersion: this.options.tscVersion}).run().then((result) => { + return this.queue.run(new Test(this, targetFile, { + tscVersion: this.options.tscVersion + })).then((result) => { this.testResults.push(result); return result; }); diff --git a/_infrastructure/tests/src/suite/tscParams.ts b/_infrastructure/tests/src/suite/tscParams.ts index bb064dd818..12e3f46017 100644 --- a/_infrastructure/tests/src/suite/tscParams.ts +++ b/_infrastructure/tests/src/suite/tscParams.ts @@ -19,12 +19,12 @@ module DT { super(options, 'Find not required .tscparams files', 'New arrival!'); this.testReporter = { - printPositiveCharacter: (index: number, testResult: TestResult) => { + printPositiveCharacter: (testResult: TestResult) => { this.print .clearCurrentLine() .printTypingsWithoutTestName(testResult.targetFile.filePathWithName); }, - printNegativeCharacter: (index: number, testResult: TestResult) => { + printNegativeCharacter: (testResult: TestResult) => { } } } @@ -40,11 +40,11 @@ module DT { public runTest(targetFile: File): Promise { this.print.clearCurrentLine().out(targetFile.filePathWithName); - return new Test(this, targetFile, { + return this.queue.run(new Test(this, targetFile, { tscVersion: this.options.tscVersion, useTscParams: false, checkNoImplicitAny: true - }).run().then((result: TestResult) => { + })).then((result) => { this.testResults.push(result); this.print.clearCurrentLine(); return result diff --git a/_infrastructure/tests/src/tsc.ts b/_infrastructure/tests/src/tsc.ts index 16287be532..9730a1394c 100644 --- a/_infrastructure/tests/src/tsc.ts +++ b/_infrastructure/tests/src/tsc.ts @@ -17,7 +17,8 @@ module DT { export class Tsc { public static run(tsfile: string, options: TscExecOptions): Promise { - return Promise.attempt(() => { + var tscPath; + return new Promise.attempt(() => { options = options || {}; options.tscVersion = options.tscVersion || DEFAULT_TSC_VERSION; if (typeof options.checkNoImplicitAny === 'undefined') { @@ -26,15 +27,21 @@ module DT { if (typeof options.useTscParams === 'undefined') { options.useTscParams = true; } - if (!fs.existsSync(tsfile)) { + return fileExists(tsfile); + }).then((exists) => { + if (!exists) { throw new Error(tsfile + ' not exists'); } - var tscPath = './_infrastructure/tests/typescript/' + options.tscVersion + '/tsc.js'; - if (!fs.existsSync(tscPath)) { + tscPath = './_infrastructure/tests/typescript/' + options.tscVersion + '/tsc.js'; + return fileExists(tscPath); + }).then((exists) => { + if (!exists) { throw new Error(tscPath + ' is not exists'); } + return fileExists(tsfile + '.tscparams'); + }).then((exists) => { var command = 'node ' + tscPath + ' --module commonjs '; - if (options.useTscParams && fs.existsSync(tsfile + '.tscparams')) { + if (options.useTscParams && exists) { command += '@' + tsfile + '.tscparams'; } else if (options.checkNoImplicitAny) { diff --git a/_infrastructure/tests/typings/bluebird/bluebird.d.ts b/_infrastructure/tests/typings/bluebird/bluebird.d.ts index 2e8de4ca80..45db9d86a3 100644 --- a/_infrastructure/tests/typings/bluebird/bluebird.d.ts +++ b/_infrastructure/tests/typings/bluebird/bluebird.d.ts @@ -75,6 +75,7 @@ declare class Promise implements Promise.Thenable { */ finally(handler: (value: R) => Promise.Thenable): Promise; finally(handler: (value: R) => R): Promise; + finally(handler: (value: R) => void): Promise; lastly(handler: (value: R) => Promise.Thenable): Promise; lastly(handler: (value: R) => R): Promise;