diff --git a/README.md b/README.md index adafac532..f9fb187d2 100644 --- a/README.md +++ b/README.md @@ -148,8 +148,14 @@ module.exports = { For example, ```javascript specs: [ - 'tests/desktop', - 'tests/touch' + { // run tests associated with this path in all browsers + files: 'tests/desktop' // which are configured in option `browsers` + }, + 'tests/deskpad', // the alias for the previous case + { + files: 'tests/touch', // run tests associated with this path in a browser with id `browser` + browsers: ['browser'] // which is configured in option `browsers` + } ] ``` diff --git a/lib/hermione.js b/lib/hermione.js index e0f52ef03..0c6cea844 100644 --- a/lib/hermione.js +++ b/lib/hermione.js @@ -3,12 +3,9 @@ var Runner = require('./runner'), HermioneFacade = require('./hermione-facade'), RunnerEvents = require('./constants/runner-events'), - pathUtils = require('./path-utils'), - logger = require('./utils').logger, + readTests = require('./tests-reader'), _ = require('lodash'), - inherit = require('inherit'), - util = require('util'), - chalk = require('chalk'); + inherit = require('inherit'); // Hack for node@0.10 and lower // Remove restriction for maximum open concurrent sockets @@ -25,8 +22,6 @@ module.exports = inherit({ }, run: function(testPaths, browsers) { - browsers = this._filterBrowsers(browsers, _.keys(this._config.browsers)); - var runner = Runner.create(this._config); runner.on(RunnerEvents.TEST_FAIL, this._fail.bind(this)); runner.on(RunnerEvents.ERROR, this._fail.bind(this)); @@ -35,10 +30,8 @@ module.exports = inherit({ this._loadPlugins(runner); - return this._getTests(testPaths) - .then(function(tests) { - return runner.run(tests, browsers); - }) + return readTests(testPaths, browsers, this._config) + .then(runner.run.bind(runner)) .then(function() { return !this._failed; }.bind(this)); @@ -52,30 +45,6 @@ module.exports = inherit({ _fail: function() { this._failed = true; - }, - - _filterBrowsers: function(browsers, allBrowsers) { - if (!browsers) { - return allBrowsers; - } - - var unknownBrowsers = _.difference(browsers, allBrowsers); - if (unknownBrowsers.length) { - logger.warn(util.format( - '%s Unknown browsers id: %s. Use one of the browser ids specified in config file: %s', - chalk.yellow('WARNING:'), unknownBrowsers.join(', '), allBrowsers.join(', ') - )); - } - - return _.intersection(browsers, allBrowsers); - }, - - _getTests: function(paths) { - if (_.isEmpty(paths)) { - paths = this._config.specs; - } - - return pathUtils.expandPaths(paths); } }); diff --git a/lib/runner/index.js b/lib/runner/index.js index 5a3d47fd7..7225d1eda 100644 --- a/lib/runner/index.js +++ b/lib/runner/index.js @@ -27,13 +27,13 @@ var MainRunner = inherit(QEmitter, { this._pool = new BrowserPool(this._config); }, - run: function(suites, browsers) { + run: function(tests) { var _this = this, anyTest = _.identity.bind(null, true); return this.emitAndWait(RunnerEvents.RUNNER_START) .then(function() { - return _this._runTestSession(suites, browsers, anyTest); + return _this._runTestSession(tests, anyTest); }) .finally(function() { return _this.emitAndWait(RunnerEvents.RUNNER_END) @@ -41,12 +41,12 @@ var MainRunner = inherit(QEmitter, { }); }, - _runTestSession: function(suites, browsers, filterFn) { + _runTestSession: function(tests, filterFn) { var _this = this; - return _(browsers) - .map(function(browserId) { - return _this._runInBrowser(browserId, suites, filterFn); + return _(tests) + .map(function(files, browserId) { + return _this._runInBrowser(browserId, files, filterFn); }) .thru(utils.waitForResults) .value() @@ -55,7 +55,7 @@ var MainRunner = inherit(QEmitter, { }); }, - _runInBrowser: function(browserId, suites, filterFn) { + _runInBrowser: function(browserId, files, filterFn) { var browserAgent = new BrowserAgent(browserId, this._pool), mochaRunner = MochaRunner.create(this._config, browserAgent); @@ -76,7 +76,7 @@ var MainRunner = inherit(QEmitter, { mochaRunner.on(RunnerEvents.TEST_FAIL, this._retryMgr.handleTestFail.bind(this._retryMgr)); mochaRunner.on(RunnerEvents.ERROR, this._retryMgr.handleError.bind(this._retryMgr)); - return mochaRunner.run(suites, filterFn); + return mochaRunner.run(files, filterFn); } }, { create: function(config) { diff --git a/lib/tests-reader.js b/lib/tests-reader.js new file mode 100644 index 000000000..ec160c40f --- /dev/null +++ b/lib/tests-reader.js @@ -0,0 +1,117 @@ +'use strict'; + +var pathUtils = require('./path-utils'), + logger = require('./utils').logger, + chalk = require('chalk'), + _ = require('lodash'), + q = require('q'), + format = require('util').format; + +module.exports = function(testPaths, browsers, config) { + var specs = config.specs, + configBrowsers = _.keys(config.browsers); + + validateUnknownBrowsers(getBrowsersFromSpecs(specs), browsers, configBrowsers); + + return q.all([ + expandSpecs(specs, configBrowsers), + pathUtils.expandPaths(testPaths) + ]) + .spread(function(specs, testFiles) { + return filterSpecs(specs, testFiles, browsers); + }) + .then(assignBrowsersToTestFiles); +}; + +function validateUnknownBrowsers(specsBrowsers, cliBrowsers, configBrowsers) { + var unknownBrowsers = getUnknownBrowsers_(); + + if (_.isEmpty(unknownBrowsers)) { + return; + } + + logger.warn(format( + '%s Unknown browsers id: %s. Use one of the browser ids specified in config file: %s', + chalk.yellow('WARNING:'), unknownBrowsers.join(', '), configBrowsers.join(', ') + )); + + function getUnknownBrowsers_() { + return _(specsBrowsers) + .concat(cliBrowsers) + .compact() + .uniq() + .difference(configBrowsers) + .value(); + } +} + +function expandSpecs(specs, configBrowsers) { + return _(specs) + .map(revealSpec_) + .thru(q.all) + .value(); + + function revealSpec_(spec) { + if (!_.isString(spec) && !_.isPlainObject(spec)) { + throw new TypeError('config.specs must be an array of strings or/and plain objects'); + } + + var paths = _.isString(spec) ? [spec] : spec.files; + + return pathUtils.expandPaths(paths) + .then(function(files) { + return { + files: files, + browsers: spec.browsers ? _.intersection(spec.browsers, configBrowsers) : configBrowsers + }; + }); + } +} + +function filterSpecs(specs, testFiles, browsers) { + return specs.map(function(spec) { + return { + files: filterSpec_(spec.files, testFiles), + browsers: filterSpec_(spec.browsers, browsers) + }; + }); + + function filterSpec_(specValue, value) { + return _.isEmpty(value) ? specValue : _.intersection(specValue, value); + } +} + +function assignBrowsersToTestFiles(specs) { + var browsers = getBrowsersFromSpecs(specs); + + return _(browsers) + .map(getTestFilesForBrowser_) + .thru(_.zipObject.bind(null, browsers)) + .omit(_.isEmpty) + .value(); + + function getTestFilesForBrowser_(browser) { + return _(specs) + .filter(function(spec) { + return _.contains(spec.browsers, browser); + }) + .thru(getFilesFromSpecs) + .value(); + } +} + +function getFilesFromSpecs(specs) { + return getDataFromSpecs(specs, 'files'); +} + +function getBrowsersFromSpecs(specs) { + return getDataFromSpecs(specs, 'browsers'); +} + +function getDataFromSpecs(specs, prop) { + return _(specs) + .map(prop) + .flatten() + .uniq() + .value(); +} diff --git a/test/lib/runner/index.js b/test/lib/runner/index.js index 746fd974a..8c15ef22e 100644 --- a/test/lib/runner/index.js +++ b/test/lib/runner/index.js @@ -19,11 +19,13 @@ describe('Runner', function() { function run_(opts) { opts = _.defaults(opts || {}, { browsers: ['default-browser'], - tests: [] + files: ['default-file'] }); - var runner = opts.runner || new Runner(makeConfigStub({browsers: opts.browsers})); - return runner.run(opts.tests, opts.browsers); + var tests = _.zipObject(opts.browsers, _.fill(Array(opts.browsers.length), opts.files)), + runner = opts.runner || new Runner(makeConfigStub({browsers: opts.browsers})); + + return runner.run(tests); } beforeEach(function() { @@ -103,7 +105,7 @@ describe('Runner', function() { }); it('should run mocha runner with passed tests and filter function', function() { - return run_({tests: ['test1', 'test2']}) + return run_({files: ['test1', 'test2']}) .then(function() { assert.calledWith(MochaRunner.prototype.run, ['test1', 'test2'], sinon.match.func); }); diff --git a/test/lib/tests-reader.js b/test/lib/tests-reader.js new file mode 100644 index 000000000..18de89962 --- /dev/null +++ b/test/lib/tests-reader.js @@ -0,0 +1,281 @@ +'use strict'; + +var pathUtils = require('../../lib/path-utils'), + readTests = require('../../lib/tests-reader'), + logger = require('../../lib/utils').logger, + makeConfigStub = require('../utils').makeConfigStub, + _ = require('lodash'), + q = require('q'); + +describe('tests-reader', function() { + var sandbox = sinon.sandbox.create(); + + beforeEach(function() { + sandbox.stub(pathUtils, 'expandPaths'); + sandbox.stub(logger, 'warn'); + + pathUtils.expandPaths.returns(q([])); + }); + + afterEach(function() { + sandbox.restore(); + }); + + function readTests_(opts) { + opts = _.defaultsDeep(opts || {}, { + testPaths: [], + browsers: [], + config: { + specs: ['default-spec'] + } + }); + + return readTests(opts.testPaths, opts.browsers, makeConfigStub(opts.config)); + } + + it('should throw in case of invalid specs declaration', function() { + return assert.throws(function() { + readTests_({config: {specs: [12345]}}); + }, /array of strings or\/and plain objects/); + }); + + it('should assign specified browsers to specified test files in specs', function() { + pathUtils.expandPaths + .withArgs(['test1']).returns(q(['/test1'])) + .withArgs(['dir']).returns(q(['/dir/test2', '/dir/test3'])); + + var params = { + config: { + specs: [ + {files: ['test1'], browsers: ['browser1']}, + {files: ['dir'], browsers: ['browser2']} + ], + browsers: ['browser1', 'browser2'] + } + }; + + return assert.becomes(readTests_(params), { + browser1: ['/test1'], + browser2: ['/dir/test2', '/dir/test3'] + }); + }); + + it('should support intersection of test paths in specs', function() { + pathUtils.expandPaths + .withArgs(['dir', 'dir1']).returns(q(['/dir/test', '/dir1/test'])) + .withArgs(['dir', 'dir2']).returns(q(['/dir/test', '/dir2/test'])); + + var params = { + config: { + specs: [ + {files: ['dir', 'dir1'], browsers: ['browser1']}, + {files: ['dir', 'dir2'], browsers: ['browser2']} + ], + browsers: ['browser1', 'browser2'] + } + }; + + return assert.becomes(readTests_(params), { + browser1: ['/dir/test', '/dir1/test'], + browser2: ['/dir/test', '/dir2/test'] + }); + }); + + it('should support intersection of subpaths in specs', function() { + pathUtils.expandPaths + .withArgs(['dir']).returns(q(['/dir/sub-dir/test'])) + .withArgs(['dir/sub-dir']).returns(q(['/dir/sub-dir/test'])); + + var params = { + config: { + specs: [ + {files: ['dir'], browsers: ['browser1']}, + {files: ['dir/sub-dir'], browsers: ['browser2']} + ], + browsers: ['browser1', 'browser2'] + } + }; + + return assert.becomes(readTests_(params), { + browser1: ['/dir/sub-dir/test'], + browser2: ['/dir/sub-dir/test'] + }); + }); + + it('should support intersection of browsers in specs', function() { + pathUtils.expandPaths + .withArgs(['test1']).returns(q(['/test1'])) + .withArgs(['test2']).returns(q(['/test2'])); + + var params = { + config: { + specs: [ + {files: ['test1'], browsers: ['browser', 'browser1']}, + {files: ['test2'], browsers: ['browser', 'browser2']} + ], + browsers: ['browser', 'browser1', 'browser2'] + } + }; + + return assert.becomes(readTests_(params), { + browser: ['/test1', '/test2'], + browser1: ['/test1'], + browser2: ['/test2'] + }); + }); + + it('should assign all browsers to test files which are specified as strings in specs', function() { + pathUtils.expandPaths + .withArgs(['test1']).returns(q(['/test1'])) + .withArgs(['test2']).returns(q(['/test2'])); + + var params = { + config: { + specs: ['test1', 'test2'], + browsers: ['browser1', 'browser2'] + } + }; + + return assert.becomes(readTests_(params), { + browser1: ['/test1', '/test2'], + browser2: ['/test1', '/test2'] + }); + }); + + it('should assign all browsers to test files which are specified as objects without `browsers` property', function() { + pathUtils.expandPaths + .withArgs(['test1']).returns(q(['/test1'])) + .withArgs(['test2']).returns(q(['/test2'])); + + var params = { + config: { + specs: [{files: ['test1']}, {files: ['test2']}], + browsers: ['browser1', 'browser2'] + } + }; + + return assert.becomes(readTests_(params), { + browser1: ['/test1', '/test2'], + browser2: ['/test1', '/test2'] + }); + }); + + it('should support string and object notations in specs', function() { + pathUtils.expandPaths + .withArgs(['test1']).returns(q(['/test1'])) + .withArgs(['test2']).returns(q(['/test2'])); + + var params = { + config: { + specs: ['test1', {files: ['test2']}], + browsers: ['browser1', 'browser2'] + } + }; + + return assert.becomes(readTests_(params), { + browser1: ['/test1', '/test2'], + browser2: ['/test1', '/test2'] + }); + }); + + it('should not assign unknown browsers to test files', function() { + pathUtils.expandPaths.withArgs(['test']).returns(q(['/test'])); + + var params = { + config: { + specs: [{files: ['test'], browsers: ['unknown-browser']}], + browsers: ['browser'] + } + }; + + return assert.becomes(readTests_(params), {}); + }); + + it('should log warning in case of unknown browsers in specs', function() { + var params = { + config: { + specs: [{browsers: 'unknown-browser'}], + browsers: ['browser'] + } + }; + + return readTests_(params) + .then(function() { + assert.calledWithMatch(logger.warn, /id: unknown-browser.+browser/); + }); + }); + + it('should filter browsers from specs in case of input browsers', function() { + pathUtils.expandPaths.withArgs(['test']).returns(q(['/test'])); + + var params = { + config: { + specs: ['test'], + browsers: ['browser1', 'browser2'] + }, + browsers: ['browser1'] + }; + + return assert.becomes(readTests_(params), {browser1: ['/test']}); + }); + + it('should not assign unknown input browsers to test files', function() { + pathUtils.expandPaths.withArgs(['test']).returns(q(['/test'])); + + var params = { + config: { + specs: ['test'], + browsers: ['browser'] + }, + browsers: ['unknown-browser'] + }; + + return assert.becomes(readTests_(params), {}); + }); + + it('should log warning in case of unknown input browsers', function() { + var params = { + config: { + browsers: ['browser'] + }, + browsers: ['unknown-browser'] + }; + + return readTests_(params) + .then(function() { + assert.calledWithMatch(logger.warn, /id: unknown-browser.+browser/); + }); + }); + + it('should filter test files from specs in case of input test paths', function() { + pathUtils.expandPaths + .withArgs(['test1']).returns(q(['/test1'])) + .withArgs(['test2']).returns(q(['/test2'])); + + var params = { + config: { + specs: ['test1', 'test2'], + browsers: ['browser'] + }, + testPaths: ['test1'] + }; + + return assert.becomes(readTests_(params), {browser: ['/test1']}); + }); + + it('should not assign browsers to unknown input test paths', function() { + pathUtils.expandPaths + .withArgs(['test']).returns(q(['/test'])) + .withArgs(['unknown-test']).returns(q(['/unknown-test'])); + + var params = { + config: { + specs: ['test'], + browsers: ['browser'] + }, + testPaths: ['unknown-test'] + }; + + return assert.becomes(readTests_(params), {}); + }); +}); diff --git a/test/utils.js b/test/utils.js index b2e089ff3..c656cb751 100644 --- a/test/utils.js +++ b/test/utils.js @@ -15,11 +15,13 @@ function browserWithId(id) { function makeConfigStub(opts) { opts = _.defaults(opts || {}, { + specs: [], browsers: ['some-default-browser'], retry: 0 }); var config = { + specs: opts.specs, browsers: {}, reporters: [], plugins: opts.plugins