From fbfc72bad15667990232bb9ff1da503e03d16230 Mon Sep 17 00:00:00 2001 From: Hank Duan Date: Wed, 4 Jun 2014 10:50:36 -0700 Subject: [PATCH] feat(launcher): Add support for maxSession - add support for maxSession and capability-specific specs - cleaned up launcher (refactored out taskScheduler.js) - (breaking change) changed the config to shard test files; also sharding is specific to capabilities now - Before: config.splitTestsBetweenCapabilities - Now: config.capabilities.shardTestFiles or config.multiCapabilities[index].shardTestFiles --- lib/configParser.js | 4 - lib/launcher.js | 452 ++++++++++---------- lib/taskScheduler.js | 158 +++++++ referenceConf.js | 24 +- scripts/test.js | 2 +- spec/{multiSplitConf.js => shardingConf.js} | 20 +- spec/unit/config_test.js | 4 +- spec/unit/data/config.js | 2 +- spec/unit/data/fakespecC.js | 1 + spec/unit/taskScheduler_test.js | 224 ++++++++++ 10 files changed, 640 insertions(+), 251 deletions(-) create mode 100644 lib/taskScheduler.js rename spec/{multiSplitConf.js => shardingConf.js} (57%) create mode 100644 spec/unit/data/fakespecC.js create mode 100644 spec/unit/taskScheduler_test.js diff --git a/lib/configParser.js b/lib/configParser.js index 98e03bffc..a71b7e95f 100644 --- a/lib/configParser.js +++ b/lib/configParser.js @@ -22,10 +22,6 @@ var ConfigParser = function() { // Default configuration. this.config_= { specs: [], - capabilities: { - browserName: 'chrome' - }, - splitTestsBetweenCapabilities: false, multiCapabilities: [], rootElement: 'body', allScriptsTimeout: 11000, diff --git a/lib/launcher.js b/lib/launcher.js index 5a1e8bdb9..5ebde181e 100644 --- a/lib/launcher.js +++ b/lib/launcher.js @@ -5,7 +5,8 @@ 'use strict'; var child = require('child_process'), - ConfigParser = require('./configParser'); + ConfigParser = require('./configParser'), + TaskScheduler = require('./taskScheduler'); var launcherPrefix = '[launcher] '; @@ -13,9 +14,6 @@ var log_ = function(stuff) { console.log(launcherPrefix + stuff); }; -var noLineLog_ = function(stuff) { - process.stdout.write(launcherPrefix + stuff); -}; /** * Initialize and run the tests. * @@ -24,12 +22,7 @@ var noLineLog_ = function(stuff) { */ var init = function(configFile, additionalConfig) { - // capabilities = array that holds one array per selected capability (Chrome, FF, Canary, ...) - // This array holds all the instances of the capability - var capabilities = [], - launcherExitCode = 0, - allSpecs, - excludes; + var launcherExitCode = 0; var configParser = new ConfigParser(); if (configFile) { @@ -39,205 +32,102 @@ var init = function(configFile, additionalConfig) { configParser.addConfig(additionalConfig); } var config = configParser.getConfig(); - - var countDriverInstances = function() { - var count = 0; - capabilities.forEach(function(capabilityDriverInstances) { - count += capabilityDriverInstances.length; - }); - return count; - }; - var countRunningDriverInstances = function() { - var count = 0; - capabilities.forEach(function(capabilityDriverInstances) { - capabilityDriverInstances.forEach(function(driverInstance) { - if (!driverInstance.done) { - count += 1; - } - }); - }); - return count; - }; - - var listRemainingForks = function() { - var remaining = countRunningDriverInstances(); - if (remaining) { - noLineLog_(remaining + ' instance(s) of WebDriver still running'); - } - }; - - var logSummary = function() { - capabilities.forEach(function(capabilityDriverInstances) { - capabilityDriverInstances.forEach(function(driverInstance) { - var shortChildName = driverInstance.capability.browserName + - (driverInstance.runNumber ? ' #' + driverInstance.runNumber : ''); - if (driverInstance.failedCount) { - log_(shortChildName + ' failed ' + driverInstance.failedCount + ' test(s)'); - } else { - log_(shortChildName + ' passed'); - } - }); - }); - }; - - if (config.multiCapabilities.length) { - if (config.debug) { - throw new Error('Cannot run in debug mode with multiCapabilities'); - } - log_('Running using config.multiCapabilities - ' + - 'config.capabilities will be ignored'); - } - // Use capabilities if multiCapabilities is empty. - if (!config.multiCapabilities.length) { - config.multiCapabilities = [config.capabilities]; - } - - var Fork = function(configFile, additionalConfig, capability, specs, runNumber, single) { - var silent = single ? false: true; - - this.configFile = configFile; - this.additionalConfig = additionalConfig; - this.capability = capability; - this.runNumber = runNumber; - this.single = single; - this.specs = specs; - + var scheduler = new TaskScheduler(config); + + /** + * A fork of a runner for running a specified task. The RunnerFork will + * start a new process that calls on '/runFromLauncher.js' and report the + * result to a reporter. + * + * @constructor + * @param {object} task Task to run. + */ + var RunnerFork = function(task) { + this.capability = task.capability; + this.specs = task.specs; this.process = child.fork( __dirname + '/runFromLauncher.js', - process.argv.slice(2),{ + process.argv.slice(2), { cwd: process.cwd(), - silent: silent + silent: true } ); + this.reporter = reporter.addTaskReporter(task, this.process.pid); }; - Fork.prototype.reportHeader_ = function() { - var capability = this.capability; - var eol = require('os').EOL; - var outputHeader = eol + '------------------------------------' + eol; - outputHeader += 'PID: ' + this.process.pid + ' (capability: '; - outputHeader += (capability.browserName) ? - capability.browserName : ''; - outputHeader += (capability.version) ? - capability.version : ''; - outputHeader += (capability.platform) ? - capability.platform : ''; - outputHeader += (this.runNumber) ? - ' #' + this.runNumber : ''; - outputHeader += ')' + eol; - if (config.splitTestsBetweenCapabilities) { - outputHeader += 'Specs: '+ this.specs.toString() + eol; - } - outputHeader += '------------------------------------' + eol; - - - console.log(outputHeader); - }; - - // If we're launching multiple runners, aggregate output until completion. - // Otherwise, there is a single runner, let's pipe the output straight - // through to maintain realtime reporting. - Fork.prototype.addEventHandlers = function(testsDoneCallback) { + /** + * Add handlers for the RunnerFork for events like stdout, stderr, testsDone, + * testPass, testFail, error, and exit. Optionally, you can pass in a + * callback function to be called when a test completes. + * + * @param {function()} testsDoneCallback Callback function for testsDone events. + */ + RunnerFork.prototype.addEventHandlers = function(testsDoneCallback) { var self = this; - if (this.single) { - this.process.on('error', function(err) { - log_('Runner Process(' + self.process.pid + ') Error: ' + err); - }); - this.process.on('message', function(m) { - switch (m.event) { - case 'testsDone': - this.failedCount = m.failedCount; - break; - } - }); - - this.process.on('exit', function(code) { - if (code) { - log_('Runner Process Exited With Error Code: ' + code); - launcherExitCode = 1; - } - }); - } else { - // Multiple capabilities and/or instances - this.output = ''; - - // stdin pipe - this.process.stdout.on('data', function(chunk) { - self.output += chunk; - }); + // stdout pipe + this.process.stdout.on('data', function(chunk) { + self.reporter.logStdout(chunk); + }); - // stderr pipe - this.process.stderr.on('data', function(chunk) { - self.output += chunk; - }); + // stderr pipe + this.process.stderr.on('data', function(chunk) { + self.reporter.logStderr(chunk); + }); - this.process.on('message', function(m) { - switch (m.event) { - case 'testPass': - process.stdout.write('.'); - break; - case 'testFail': - process.stdout.write('F'); - break; - case 'testsDone': - self.failedCount = m.failedCount; - if (typeof testsDoneCallback === 'function') { - testsDoneCallback(); - } - break; - } - }); + this.process.on('message', function(m) { + switch (m.event) { + case 'testPass': + process.stdout.write('.'); + break; + case 'testFail': + process.stdout.write('F'); + break; + case 'testsDone': + self.reporter.testsDone(m.failedCount); + if (typeof testsDoneCallback === 'function') { + testsDoneCallback(); + } + break; + } + }); - // err handler - this.process.on('error', function(err) { - log_('Runner Process(' + self.process.pid + ') Error: ' + err); - }); + this.process.on('error', function(err) { + log_('Runner Process(' + self.process.pid + ') Error: ' + err); + launcherExitCode = 1; + }); - // exit handlers - this.process.on('exit', function(code) { - if (code) { - log_('Runner Process Exited With Error Code: ' + code); - launcherExitCode = 1; - } - self.reportHeader_(); - console.log(self.output); - self.done = true; - listRemainingForks(); - }); - } + this.process.on('exit', function(code) { + if (code) { + log_('Runner Process Exited With Error Code: ' + code); + launcherExitCode = 1; + } + log_(scheduler.countActiveTasks() + + ' instance(s) of WebDriver still running'); + }); }; - Fork.prototype.run = function() { + /** + * Sends the run command. + */ + RunnerFork.prototype.run = function() { this.process.send({ command: 'run', - configFile: this.configFile, - additionalConfig: this.additionalConfig, + configFile: configFile, + additionalConfig: additionalConfig, capability: this.capability, specs: this.specs }); + this.reporter.reportHeader_(); }; - excludes = ConfigParser.resolveFilePatterns( config.exclude, true, config.configDir); - - allSpecs = ConfigParser.resolveFilePatterns( - ConfigParser.getSpecs(config), false, config.configDir).filter(function(path) { - return excludes.indexOf(path) < 0; - }); - - // If there is a single capability with a single instance, avoid starting a separate process - // and print output directly. - // Otherwise, if we're launching multiple runners, aggregate output until - // completion. - if (config.multiCapabilities.length === 1 - && (config.multiCapabilities[0].count === 1 || !config.multiCapabilities[0].count)) { + // Don't start new process if there is only 1 task. + var totalTasks = scheduler.numTasksRemaining(); + if (totalTasks === 1) { var Runner = require('./runner'); - capabilities[0] = [{ - capability: config.multiCapabilities[0], - runNumber: 0 - }]; - config.capabilities = capabilities[0][0].capability; - config.specs = allSpecs; + var task = scheduler.nextTask(); + config.capabilities = task.capability; + config.specs = task.specs; var runner = new Runner(config); runner.run().then(function(exitCode) { @@ -246,64 +136,152 @@ var init = function(configFile, additionalConfig) { log_('Error: ' + err.message); process.exit(1); }); - - runner.on('testsDone', function(failedCount) { - capabilities[0][0].failedCount = failedCount; - }); } else { - // Loop over different capabilities in config file - // Make array for each of them in capabilities array - // Push driverInstances into this array, each with one spec (file!) - // When a driverInstance finishes it's spec, it will create a new driver (Fork) - // with the next spec in line - config.multiCapabilities.forEach(function(capability, index) { - var forksCounter = 0; - - capability.count = capability.count || 1; - - capabilities[index] = []; // Matrix: Dim 1: Capabilities, Dim 2: Instances of capability - if (allSpecs.length < capability.count && config.splitTestsBetweenCapabilities) { - // When we split the specs over multiple instances, - // we can have maximum as many instances as we have specs - capability.count = allSpecs.length; - } - - while(forksCounter < capability.count) { - if (config.splitTestsBetweenCapabilities) { - var createAndRunPartialSpecFork = function() { - var specs = allSpecs[forksCounter]; - if (!specs) { - return; - } - var fork = new Fork(configFile,additionalConfig, - capability,[specs],forksCounter+1,false); - capabilities[index].push(fork); - fork.run(); - fork.addEventHandlers(createAndRunPartialSpecFork); - forksCounter++; + if (config.debug) { + throw new Error('Cannot run in debug mode with ' + + 'multiCapabilities, count > 1, or sharding'); + } + for (var i = 0; i < scheduler.maxConcurrentTasks(); ++i) { + var createNextRunnerFork = function() { + var task = scheduler.nextTask(); + if (task) { + var done = function() { + task.done(); + createNextRunnerFork(); }; - createAndRunPartialSpecFork(); - } else { - var fork = new Fork(configFile, additionalConfig, - capability, allSpecs, forksCounter+1, false); - capabilities[index].push(fork); - fork.run(); - fork.addEventHandlers(); - forksCounter++; + var runnerFork = new RunnerFork(task); + runnerFork.addEventHandlers(done); + runnerFork.run(); } + }; + createNextRunnerFork(); + } + log_('Running ' + scheduler.countActiveTasks() + ' instances of WebDriver'); + + process.on('exit', function(code) { + launcherExitCode = code ? code : launcherExitCode; + if (launcherExitCode) { + log_('Process exited with error code ' + launcherExitCode); + } else { + if (scheduler.numTasksRemaining() > 0) { + throw new Error('BUG: launcher exited with ' + + scheduler.numTasksRemaining() + ' tasks remaining'); + } + reporter.reportSummary(); } + process.exit(launcherExitCode); }); } - noLineLog_('Running ' + countDriverInstances() + - ' instances of WebDriver'); +}; - process.on('exit', function(code) { - if (code) { - launcherExitCode = code; - } - logSummary(); - process.exit(launcherExitCode); - }); +//###### REPORTER #######// +/** + * Keeps track of a list of task reporters. Provides method to add a new + * reporter and to aggregate the reports into a summary. + */ +var reporter = { + taskReporters_: [], + + addTaskReporter: function(task, pid) { + var taskReporter = new TaskReporter_(task, pid); + this.taskReporters_.push(taskReporter); + return taskReporter; + }, + + reportSummary: function() { + this.taskReporters_.forEach(function(taskReporter) { + var capability = taskReporter.task.capability; + var shortName = (capability.browserName) ? capability.browserName : ''; + shortName += (capability.version) ? capability.version : ''; + shortName += (' #' + taskReporter.task.taskId); + if (taskReporter.failedCount) { + log_(shortName + ' failed ' + taskReporter.failedCount + ' test(s)'); + } else { + log_(shortName + ' passed'); + } + }); + } +}; + +/** + * A reporter for a specific task. + * + * @constructor + * @param {object} task Task that is being reported. + * @param {number} pid PID of process running the task. + */ +var TaskReporter_ = function(task, pid) { + this.task = task; + this.pid = pid; + this.failedCount = 0; + this.buffer = ''; +}; + +/** + * Report the header for the current task including information such as + * PID, browser name/version, task Id, specs being run. + */ +TaskReporter_.prototype.reportHeader_ = function() { + var capability = this.task.capability; + var eol = require('os').EOL; + var output = '------------------------------------' + eol; + output += 'PID: ' + this.pid + ' (capability: '; + output += (capability.browserName) ? + capability.browserName : ''; + output += (capability.version) ? + capability.version : ''; + output += (capability.platform) ? + capability.platform : ''; + output += (' #' + this.task.taskId); + output += ')' + eol; + if (this.task.specs.length === 1) { + output += 'Specs: '+ this.task.specs.toString() + eol; + } + this.log_(output); +}; + +/** + * Log the stdout. The reporter is responsible for reporting this data when + * appropriate. + * + * @param {string} stdout Stdout data to log + */ +TaskReporter_.prototype.logStdout = function(stdout) { + this.log_(stdout); +}; + +/** + * Log the stderr. The reporter is responsible for reporting this data when + * appropriate. + * + * @param {string} stderr Stderr data to log + */ +TaskReporter_.prototype.logStderr = function(stderr) { + this.log_(stderr); +}; + +/** + * Signal that the task is completed. This must be called at the end of a task. + * + * @param {number} failedCount Number of failures + */ +TaskReporter_.prototype.testsDone = function(failedCount) { + this.failedCount = failedCount; + if (this.buffer) { + // Flush buffer if nonempty + process.stdout.write(this.buffer); + } +}; + +/** + * Report the following data. The data will be saved to a buffer + * until it is flushed by the function testsDone. + * + * @private + * @param {string} data + */ +TaskReporter_.prototype.log_ = function(data) { + this.buffer += data; }; exports.init = init; diff --git a/lib/taskScheduler.js b/lib/taskScheduler.js new file mode 100644 index 000000000..2770cacf5 --- /dev/null +++ b/lib/taskScheduler.js @@ -0,0 +1,158 @@ +/** + * The taskScheduler keeps track of the specs that needs to run next + * and which task is running what. + */ +'use strict'; + +var ConfigParser = require('./configParser'); + +// A queue of specs for a particular capacity +var TaskQueue = function(capability, specLists) { + this.capability = capability; + this.numRunningInstances = 0; + this.maxInstance = capability.maxInstances || 1; + this.specsIndex = 0; + this.specLists = specLists; +}; + +/** + * A scheduler to keep track of specs that need running and their associated + * capability. It will suggest a task (combination of capability and spec) + * to run while observing the following config rules: capabilities, + * multiCapabilities, shardTestFiles, and maxInstance. + * + * @constructor + * @param {Object} config parsed from the config file + */ +var TaskScheduler = function(config) { + var excludes = ConfigParser.resolveFilePatterns(config.exclude, true, config.configDir); + var allSpecs = ConfigParser.resolveFilePatterns( + ConfigParser.getSpecs(config), false, config.configDir).filter(function(path) { + return excludes.indexOf(path) < 0; + }); + + if (config.capabilities) { + if (config.multiCapabilities.length) { + console.log('Running using config.multiCapabilities - ' + + 'config.capabilities will be ignored'); + } else { + // Use capabilities if multiCapabilities is empty. + config.multiCapabilities = [config.capabilities]; + } + } else if (!config.multiCapabilities.length) { + // Default to chrome if no capability given + config.multiCapabilities = [{ + browserName: 'chrome' + }]; + } + + var taskQueues = []; + config.multiCapabilities.forEach(function(capability) { + var capabilitySpecs = allSpecs; + if (capability.specs) { + var capabilitySpecificSpecs = ConfigParser.resolveFilePatterns( + capability.specs, false, config.configDir); + capabilitySpecs = capabilitySpecs.concat(capabilitySpecificSpecs); + } + + var specLists = []; + // If we shard, we return an array of one element arrays, each containing + // the spec file. If we don't shard, we return an one element array + // containing an array of all the spec files + if (capability.shardTestFiles) { + capabilitySpecs.forEach(function(spec) { + specLists.push([spec]); + }); + } else { + specLists.push(capabilitySpecs); + } + + capability.count = capability.count || 1; + + for (var i = 0; i < capability.count; ++i) { + taskQueues.push(new TaskQueue(capability, specLists)); + } + }); + this.taskQueues = taskQueues; + this.config = config; + this.rotationIndex = 0; // Helps suggestions to rotate amongst capabilities +}; + +/** + * Get the next task that is allowed to run without going over maxInstance. + * + * @return {{capability: Object, specs: Array., taskId: string, done: function()}} + */ +TaskScheduler.prototype.nextTask = function() { + for (var i = 0; i < this.taskQueues.length; ++i) { + var rotatedIndex = ((i + this.rotationIndex) % this.taskQueues.length); + var queue = this.taskQueues[rotatedIndex]; + if (queue.numRunningInstances < queue.maxInstance && + queue.specsIndex < queue.specLists.length) { + this.rotationIndex = rotatedIndex + 1; + ++queue.numRunningInstances; + var taskId = rotatedIndex + 1; + if (queue.specLists.length > 1) { + taskId += String.fromCharCode(97 + queue.specsIndex); //ascii 97 is 'a' + } + var specs = queue.specLists[queue.specsIndex]; + ++queue.specsIndex; + + return { + capability: queue.capability, + specs: specs, + taskId: taskId, + done: function() { + --queue.numRunningInstances; + } + }; + } + } + + return null; +}; + +/** + * Get the number of tasks left to run. + * + * @return {number} + */ +TaskScheduler.prototype.numTasksRemaining = function() { + var count = 0; + this.taskQueues.forEach(function(queue) { + count += (queue.specLists.length - queue.specsIndex); + }); + return count; +}; + +/** + * Get maximum number of concurrent tasks required/permitted. + * + * @return {number} + */ +TaskScheduler.prototype.maxConcurrentTasks = function() { + if (this.config.maxSessions && this.config.maxSessions > 0) { + return this.config.maxSessions; + } else { + var count = 0; + this.taskQueues.forEach(function(queue) { + count += Math.min(queue.maxInstance, queue.specLists.length); + }); + return count; + } +}; + +/** + * Returns number of tasks currently running. + * + * @return {number} + */ +TaskScheduler.prototype.countActiveTasks = function() { + var count = 0; + this.taskQueues.forEach(function(queue) { + count += queue.numRunningInstances; + }); + return count; +}; + +module.exports = TaskScheduler; diff --git a/referenceConf.js b/referenceConf.js index 47f1026f2..4b9631b31 100644 --- a/referenceConf.js +++ b/referenceConf.js @@ -67,14 +67,34 @@ exports.config = { full: 'spec/*.js' }, + // Maximum number of total browser sessions to run. Tests are queued in + // sequence if number of browser sessions is limited by this parameter. + // Use a number less than 1 to denote unlimited. Default is unlimited. + maxSessions: -1, + // ----- Capabilities to be passed to the webdriver instance ---- // - // For a full list of available capabilities, see + // For a list of available capabilities, see // https://code.google.com/p/selenium/wiki/DesiredCapabilities // and // https://code.google.com/p/selenium/source/browse/javascript/webdriver/capabilities.js + // Additionally, you may specify count, shardTestFiles, and maxInstances. capabilities: { - 'browserName': 'chrome' + browserName: 'chrome', + + // Number of times to run this set of capabilities (in parallel, unless + // limited by maxSessions). Default is 1. + count: 1, + + // If this is set to be true, specs will be sharded by file (i.e. all + // files to be run by this set of capabilities will run in parallel). + // Default is false. + shardTestFiles: false, + + // Maximum number of browser instances that can run in parallel for this + // set of capabilities. This is only needed if shardTestFiles is true. + // Default is 1. + maxInstances: 1 }, // If you would like to run more than one instance of webdriver on the same diff --git a/scripts/test.js b/scripts/test.js index 13d3b7e05..c5d897260 100755 --- a/scripts/test.js +++ b/scripts/test.js @@ -6,7 +6,7 @@ var spawn = require('child_process').spawn; var scripts = [ 'node lib/cli.js spec/basicConf.js', 'node lib/cli.js spec/multiConf.js', - 'node lib/cli.js spec/multiSplitConf.js', + 'node lib/cli.js spec/shardingConf.js', 'node lib/cli.js spec/altRootConf.js', 'node lib/cli.js spec/onPrepareConf.js', 'node lib/cli.js spec/onPrepareFileConf.js', diff --git a/spec/multiSplitConf.js b/spec/shardingConf.js similarity index 57% rename from spec/multiSplitConf.js rename to spec/shardingConf.js index 27bfabe63..0b7599a8b 100644 --- a/spec/multiSplitConf.js +++ b/spec/shardingConf.js @@ -7,7 +7,8 @@ exports.config = { // Spec patterns are relative to this directory. specs: [ - 'basic/*_spec.js' + 'basic/mock*', + 'basic/lib_spec.js' ], // Exclude patterns are relative to this directory. @@ -16,11 +17,22 @@ exports.config = { ], chromeOnly: false, - - splitTestsBetweenCapabilities: true, + framework: 'debugprint', + maxSessions: 3, multiCapabilities: [{ 'browserName': 'chrome', - count: 2 + maxInstances: 2 + }, { + 'browserName': 'chrome', + shardTestFiles: true, + maxInstances: 1, + specs: 'basic/polling*' // Capacity specific specs + }, { + shardTestFiles: true, + 'browserName': 'firefox', + maxInstances: 2, + count: 2, + specs: 'basic/action*', }], baseUrl: env.baseUrl, diff --git a/spec/unit/config_test.js b/spec/unit/config_test.js index 8b523f4f3..6dccb2cb9 100644 --- a/spec/unit/config_test.js +++ b/spec/unit/config_test.js @@ -26,7 +26,7 @@ describe('the config parser', function() { expect(config.rootElement).toEqual('.mycontainer'); expect(config.onPrepare.indexOf(path.normalize('/spec/unit/data/foo/bar.js'))).not.toEqual(-1); expect(config.specs.length).toEqual(1); - expect(config.specs[0]).toEqual('fakespec*.js'); + expect(config.specs[0]).toEqual('fakespec[AB].js'); }); it('should keep filepaths relative to the cwd when merging', function() { @@ -43,7 +43,7 @@ describe('the config parser', function() { it('should resolve relative to the cwd', function() { spyOn(process, 'cwd').andReturn(__dirname + '/'); var toAdd = { - specs: 'data/*spec*.js' + specs: 'data/*spec[AB].js' }; var config = new ConfigParser().addConfig(toAdd).getConfig(); var specs = ConfigParser.resolveFilePatterns(config.specs); diff --git a/spec/unit/data/config.js b/spec/unit/data/config.js index a6ad7f5b4..3baa8d08a 100644 --- a/spec/unit/data/config.js +++ b/spec/unit/data/config.js @@ -1,5 +1,5 @@ exports.config = { onPrepare: 'foo/bar.js', - specs: [ 'fakespec*.js' ], + specs: [ 'fakespec[AB].js' ], rootElement: '.mycontainer' }; diff --git a/spec/unit/data/fakespecC.js b/spec/unit/data/fakespecC.js new file mode 100644 index 000000000..f87af3c19 --- /dev/null +++ b/spec/unit/data/fakespecC.js @@ -0,0 +1 @@ +// Blank diff --git a/spec/unit/taskScheduler_test.js b/spec/unit/taskScheduler_test.js new file mode 100644 index 000000000..f80bb7b47 --- /dev/null +++ b/spec/unit/taskScheduler_test.js @@ -0,0 +1,224 @@ +var TaskScheduler = require('../../lib/taskScheduler.js'); +var ConfigParser = require('../../lib/configParser'); + +describe('the task scheduler', function() { + + it('should schedule single capability tests', function() { + var toAdd = { + specs: [ + 'spec/unit/data/fakespecA.js', + 'spec/unit/data/fakespecB.js' + ], + capabilities: { + browserName: 'chrome' + } + }; + var config = new ConfigParser().addConfig(toAdd).getConfig(); + var scheduler = new TaskScheduler(config); + + var task = scheduler.nextTask(); + expect(task.capability.browserName).toEqual('chrome'); + expect(task.specs.length).toEqual(2); + + expect(scheduler.numTasksRemaining()).toEqual(0); + }); + + it('should schedule single capability tests with sharding', function() { + var toAdd = { + specs: [ + 'spec/unit/data/fakespecA.js', + 'spec/unit/data/fakespecB.js' + ], + capabilities: { + shardTestFiles: true, + maxInstances: 2, + browserName: 'chrome' + } + }; + var config = new ConfigParser().addConfig(toAdd).getConfig(); + var scheduler = new TaskScheduler(config); + + var task1 = scheduler.nextTask(); + expect(task1.capability.browserName).toEqual('chrome'); + expect(task1.specs.length).toEqual(1); + + var task2 = scheduler.nextTask(); + expect(task2.capability.browserName).toEqual('chrome'); + expect(task2.specs.length).toEqual(1); + + expect(scheduler.numTasksRemaining()).toEqual(0); + }); + + it('should schedule single capability tests with count', function() { + var toAdd = { + specs: [ + 'spec/unit/data/fakespecA.js', + 'spec/unit/data/fakespecB.js' + ], + capabilities: { + count: 2, + browserName: 'chrome' + } + }; + var config = new ConfigParser().addConfig(toAdd).getConfig(); + var scheduler = new TaskScheduler(config); + + var task1 = scheduler.nextTask(); + expect(task1.capability.browserName).toEqual('chrome'); + expect(task1.specs.length).toEqual(2); + + var task2 = scheduler.nextTask(); + expect(task2.capability.browserName).toEqual('chrome'); + expect(task2.specs.length).toEqual(2); + + expect(scheduler.numTasksRemaining()).toEqual(0); + }); + + it('should schedule multiCapabilities tests', function() { + var toAdd = { + specs: [ + 'spec/unit/data/fakespecA.js', + 'spec/unit/data/fakespecB.js' + ], + multiCapabilities: [{ + 'browserName': 'chrome' + }, { + 'browserName': 'firefox' + }], + }; + var config = new ConfigParser().addConfig(toAdd).getConfig(); + var scheduler = new TaskScheduler(config); + + var task1 = scheduler.nextTask(); + expect(task1.capability.browserName).toEqual('chrome'); + expect(task1.specs.length).toEqual(2); + + var task2 = scheduler.nextTask(); + expect(task2.capability.browserName).toEqual('firefox'); + expect(task2.specs.length).toEqual(2); + + expect(scheduler.numTasksRemaining()).toEqual(0); + }); + + it('should obey maxInstances', function() { + var toAdd = { + specs: [ + 'spec/unit/data/fakespecA.js', + 'spec/unit/data/fakespecB.js' + ], + capabilities: { + shardTestFiles: true, + maxInstances: 1, + browserName: 'chrome' + } + }; + var config = new ConfigParser().addConfig(toAdd).getConfig(); + var scheduler = new TaskScheduler(config); + + var task1 = scheduler.nextTask(); + expect(task1.capability.browserName).toEqual('chrome'); + expect(task1.specs.length).toEqual(1); + + var task2 = scheduler.nextTask(); + expect(task2).toBeNull(); + expect(scheduler.numTasksRemaining()).toEqual(1); + + task1.done(); + var task3 = scheduler.nextTask(); + expect(task3.capability.browserName).toEqual('chrome'); + expect(task3.specs.length).toEqual(1); + + expect(scheduler.numTasksRemaining()).toEqual(0); + }); + + it('should allow capability-specific specs', function() { + var toAdd = { + specs: [ + 'spec/unit/data/fakespecA.js', + 'spec/unit/data/fakespecB.js' + ], + multiCapabilities: [{ + 'browserName': 'chrome', + specs: 'spec/unit/data/fakespecC.js' + }], + }; + var config = new ConfigParser().addConfig(toAdd).getConfig(); + var scheduler = new TaskScheduler(config); + + var task = scheduler.nextTask(); + expect(task.capability.browserName).toEqual('chrome'); + expect(task.specs.length).toEqual(3); + + expect(scheduler.numTasksRemaining()).toEqual(0); + }); + + it('should handle multiCapabilities with mixture of features', function() { + var toAdd = { + specs: [ + 'spec/unit/data/fakespecA.js', + 'spec/unit/data/fakespecB.js' + ], + multiCapabilities: [{ + 'browserName': 'chrome', + maxInstances: 2, + count: 2 + }, { + 'browserName': 'firefox', + shardTestFiles: true, + maxInstances: 1, + count: 2 + }], + }; + var config = new ConfigParser().addConfig(toAdd).getConfig(); + var scheduler = new TaskScheduler(config); + + var task1 = scheduler.nextTask(); + expect(task1.capability.browserName).toEqual('chrome'); + expect(task1.specs.length).toEqual(2); + task1.done(); + + var task2 = scheduler.nextTask(); + expect(task2.capability.browserName).toEqual('chrome'); + expect(task2.specs.length).toEqual(2); + task2.done(); + + var task3 = scheduler.nextTask(); + expect(task3.capability.browserName).toEqual('firefox'); + expect(task3.specs.length).toEqual(1); + task3.done(); + + var task4 = scheduler.nextTask(); + expect(task4.capability.browserName).toEqual('firefox'); + expect(task4.specs.length).toEqual(1); + task4.done(); + + var task5 = scheduler.nextTask(); + expect(task5.capability.browserName).toEqual('firefox'); + expect(task5.specs.length).toEqual(1); + task5.done(); + + var task6 = scheduler.nextTask(); + expect(task6.capability.browserName).toEqual('firefox'); + expect(task6.specs.length).toEqual(1); + task6.done(); + + expect(scheduler.numTasksRemaining()).toEqual(0); + }); + + it('should default to chrome when no capability is defined', function() { + var toAdd = { + specs: [ + 'spec/unit/data/fakespecA.js', + 'spec/unit/data/fakespecB.js' + ] + }; + var config = new ConfigParser().addConfig(toAdd).getConfig(); + var scheduler = new TaskScheduler(config); + + var task = scheduler.nextTask(); + expect(task.capability.browserName).toEqual('chrome'); + expect(task.specs.length).toEqual(2); + + expect(scheduler.numTasksRemaining()).toEqual(0); + }); +});