From 1c627270c7581a4a20f6948ca697cd9b77328683 Mon Sep 17 00:00:00 2001 From: Chohee Kim Date: Thu, 4 Oct 2018 15:39:41 -0700 Subject: [PATCH 01/28] [Feature] Test Load Balancing --- README.md | 4 +- .../-private/patch-test-loader.js | 37 +- .../-private/patch-testem-output.js | 13 +- .../-private/weight-test-modules.js | 37 ++ addon-test-support/load.js | 76 ++- index.js | 6 + lib/commands/exam.js | 282 ++++++++-- lib/commands/exam/iterate.js | 4 +- lib/commands/task/test-server.js | 12 + lib/commands/task/test.js | 13 + lib/utils/tests-options-validator.js | 107 +++- node-tests/acceptance/exam-iterate-test.js | 13 +- node-tests/acceptance/exam-test.js | 4 + node-tests/unit/commands/exam-test.js | 225 ++++++-- node-tests/unit/lint-test.js | 4 +- .../utils/tests-options-validator-test.js | 65 ++- package.json | 1 + test-execution-0000000.json | 11 + tests/test-helper.js | 5 +- tests/unit/mocha/test-loader-test.js | 2 +- tests/unit/mocha/weight-test-modules-test.js | 49 ++ tests/unit/qunit/test-loader-test.js | 52 +- tests/unit/qunit/weight-test-modules-test.js | 48 ++ yarn.lock | 523 +++++++++--------- 24 files changed, 1162 insertions(+), 431 deletions(-) create mode 100644 addon-test-support/-private/weight-test-modules.js create mode 100644 lib/commands/task/test-server.js create mode 100644 lib/commands/task/test.js create mode 100644 test-execution-0000000.json create mode 100644 tests/unit/mocha/weight-test-modules-test.js create mode 100644 tests/unit/qunit/weight-test-modules-test.js diff --git a/README.md b/README.md index c935d55c7..2efed6d2a 100644 --- a/README.md +++ b/README.md @@ -188,8 +188,8 @@ module.exports = { If you are working with [Travis CI](https://travis-ci.org/) then you can also easily set up seeded-random runs based on PR numbers. Similar to the following: ```js -var command = [ 'ember', 'exam', '--random' ]; -var pr = process.env.TRAVIS_PULL_REQUEST; +const command = [ 'ember', 'exam', '--random' ]; +const pr = process.env.TRAVIS_PULL_REQUEST; if (pr) { command.push(pr); diff --git a/addon-test-support/-private/patch-test-loader.js b/addon-test-support/-private/patch-test-loader.js index 7c5d5870c..74c719a8f 100644 --- a/addon-test-support/-private/patch-test-loader.js +++ b/addon-test-support/-private/patch-test-loader.js @@ -1,5 +1,7 @@ +/* globals Testem */ import getUrlParams from './get-url-params'; import splitTestModules from './split-test-modules'; +import weightTestModules from './weight-test-modules'; export default function patchTestLoader(TestLoader) { TestLoader._urlParams = getUrlParams(); @@ -20,8 +22,9 @@ export default function patchTestLoader(TestLoader) { TestLoader.prototype.loadModules = function _emberExamLoadModules() { const urlParams = TestLoader._urlParams; - let partitions = urlParams._partition; - let split = parseInt(urlParams._split, 10); + const loadBalance = urlParams.loadBalance; + let partitions = urlParams.partition; + let split = parseInt(urlParams.split, 10); split = isNaN(split) ? 1 : split; @@ -33,14 +36,32 @@ export default function patchTestLoader(TestLoader) { const testLoader = this; - testLoader._testModules = []; + this.modules = testLoader._testModules = []; _super.loadModules.apply(testLoader, arguments); - const splitModules = splitTestModules(testLoader._testModules, split, partitions); + if (loadBalance) { + this.modules = weightTestModules(this.modules); + } + + this.modules = splitTestModules(this.modules, split, partitions); + + if (loadBalance) { + Testem.emit('set-modules-queue', this.modules); + } else { + this.modules.forEach((moduleName) => { + _super.require.call(testLoader, moduleName); + _super.unsee.call(testLoader, moduleName); + }); + } + }; - splitModules.forEach((moduleName) => { + TestLoader.prototype.loadIndividualModule = function _emberExamLoadIndividualModule(moduleName) { + const testLoader = this; + if (moduleName) { _super.require.call(testLoader, moduleName); _super.unsee.call(testLoader, moduleName); - }); - }; -} + return moduleName; + } + return null; + } +} \ No newline at end of file diff --git a/addon-test-support/-private/patch-testem-output.js b/addon-test-support/-private/patch-testem-output.js index e16101459..9688d82cd 100644 --- a/addon-test-support/-private/patch-testem-output.js +++ b/addon-test-support/-private/patch-testem-output.js @@ -4,11 +4,18 @@ export default function patchTestemOutput(TestLoader) { Testem.on('test-result', function prependPartition(test) { const urlParams = TestLoader._urlParams; - const split = urlParams._split; + const split = urlParams.split; + const loadBalance = urlParams.loadBalance; - if (split) { - const partition = urlParams._partition || 1; + const partition = urlParams.partition || 1; + const browser = urlParams.browser || 1; + + if (split && loadBalance) { + test.name = `Exam Partition ${partition} - Browser Id ${browser} - ${test.name}` + } else if (split) { test.name = `Exam Partition ${partition} - ${test.name}`; + } else if (loadBalance) { + test.name = `Browser Id ${browser} - ${test.name}`; } }); } diff --git a/addon-test-support/-private/weight-test-modules.js b/addon-test-support/-private/weight-test-modules.js new file mode 100644 index 000000000..0b48c645e --- /dev/null +++ b/addon-test-support/-private/weight-test-modules.js @@ -0,0 +1,37 @@ +const TEST_TYPE_WEIGHT = {jshint:1, eslint:1, unit:10, integration:20, acceptance:150}; +const DEFAULT_WEIGHT = 50; + +/** +* Return the weight for a given module name, a file path to the module +* Ember tests consist of Acceptance, Integration, Unit, and lint tests. In general, acceptance takes +* longest time to execute, followed by integration and unit. +* The weight assigned to a module corresponds to its test type execution speed, with slowest being the highest in weight. +* If the test type is not identifiable from the modulePath, weight default to 50 (ordered after acceptance, but before integration) +* +* @param {*} modulePath File path to a module + */ +function getWeight(modulePath) { + const [, key] = /\/(jshint|unit|integration|acceptance)\//.exec(modulePath) || []; + return TEST_TYPE_WEIGHT[key] || DEFAULT_WEIGHT; +} + +export default function weightTestModules(modules) { + const groups = {}; + + modules.forEach(function(module) { + const moduleWeight = getWeight(module); + + if (!groups[moduleWeight]) { + groups[moduleWeight] = []; + } + groups[moduleWeight].push(module); + }); + + // returns modules sorted by weight and alphabetically within its weighted groups + return Object.keys(groups) + .sort(function(a, b){return b-a}) + .reduce(function(accumulatedArray, weight) { + const sortedModuleArr = groups[weight].sort(); + return accumulatedArray.concat(sortedModuleArr); + }, []); +} diff --git a/addon-test-support/load.js b/addon-test-support/load.js index 74c8a3e70..ac3cf3a1a 100644 --- a/addon-test-support/load.js +++ b/addon-test-support/load.js @@ -1,10 +1,13 @@ -import getTestLoader from './-private/get-test-loader'; +/* globals Testem */ + import patchTestLoader from './-private/patch-test-loader'; import patchTestemOutput from './-private/patch-testem-output'; +import getTestLoader from './-private/get-test-loader'; let loaded = false; +let testLoader; -export default function loadEmberExam() { +function loadEmberExam() { if (loaded) { // eslint-disable-next-line no-console console.warn('Attempted to load Ember Exam more than once.'); @@ -19,4 +22,73 @@ export default function loadEmberExam() { if (window.Testem) { patchTestemOutput(TestLoader); } + + testLoader = new TestLoader(); } + +// loadTests() is equivalent to ember-qunit's loadTest() except this does not create a new TestLoader instance +function loadTests() { + if (testLoader === undefined) { + new Error('testLoader is not defined. It must call `loadEmberExam()` before calling `loadTest()`.') + } + + testLoader.loadModules(); +} + +// setupQUnitCallBacks() registers QUnit callbacks neeeded for the load-balance option. +function setupQUnitCallbacks(qunit) { + if (!location.search.includes('loadBalance')) { + return; + } + qunit.begin(function() { + return new self.Promise(function(resolve) { + Testem.on('testem:next-module-response', function getTestModule(moduleName) { + if (moduleName !== undefined) { + loadIndividualModule(moduleName); + } + + // if no tests were added, request the next module + if (qunit.config.queue.length === 0) { + Testem.emit('get-next-module'); + } else { + Testem.removeEventCallbacks('testem:next-module-response', getTestModule); + resolve(); + } + }); + Testem.emit('get-next-module'); + }); + }); + qunit.moduleDone(function() { + return new self.Promise(function(resolve) { + Testem.on('testem:next-module-response', function getTestModule(moduleName) { + if (moduleName !== undefined) { + loadIndividualModule(moduleName); + } + + // if no tests were added, request the next module + if (qunit.config.queue.length === 0) { + Testem.emit('get-next-module'); + } else { + // `removeCallback` removes if the event queue contains the same callback for an event. + Testem.removeEventCallbacks('testem:next-module-response', getTestModule); + Testem.removeEventCallbacks('testem:module-queue-complete', resolve); + resolve(); + } + }); + Testem.on('testem:module-queue-complete', resolve); + Testem.emit('get-next-module'); + }); + }); +} + +// loadIndividualModule() executes loadIndividualModule() defined in patch-test-loader. It enables to load an AMD module one by one. +function loadIndividualModule(moduleName) { + testLoader.loadIndividualModule(moduleName); +} + +export default { + loadEmberExam, + loadTests, + setupQUnitCallbacks, + loadIndividualModule, +}; diff --git a/index.js b/index.js index 5ca73fe5a..7f027de75 100644 --- a/index.js +++ b/index.js @@ -7,5 +7,11 @@ module.exports = { includedCommands: function() { return require('./lib/commands'); + }, + + checkDevDependencies: function() { + const VersionChecker = require('ember-cli-version-checker'); + const checker = new VersionChecker(this); + return checker.for('ember-cli', 'npm').gte('3.2.0'); } }; diff --git a/lib/commands/exam.js b/lib/commands/exam.js index 71c86eed6..cb24b680a 100644 --- a/lib/commands/exam.js +++ b/lib/commands/exam.js @@ -3,7 +3,12 @@ const fs = require('fs-extra'); const path = require('path'); const TestCommand = require('ember-cli/lib/commands/test'); // eslint-disable-line node/no-unpublished-require +const TestServerTask = require('./task/test-server'); +const TestTask = require('./task/test'); const debug = require('debug')('exam'); +const moment = require('moment'); + +const executionMapping = Object.create(null); function addToQuery(query, param, value) { if (!value) { @@ -35,11 +40,19 @@ module.exports = TestCommand.extend({ availableOptions: [ { name: 'split', type: Number, description: 'A number of files to split your tests across.' }, { name: 'partition', type: [Array, Number], description: 'The number of the partition(s) to run after splitting.' }, - { name: 'weighted', type: Boolean, description: 'Weights the type of tests to help equal splits.' }, { name: 'parallel', type: Boolean, default: false, description: 'Runs your split tests on parallel child processes.' }, - { name: 'random', type: String, default: false, description: 'Randomizes your modules and tests while running your test suite.' } + { name: 'load-balance', type: Number, description: 'The number of browser(s) to load balance test files against. Test files will be sorted by weight from slowest (acceptance) to fastest (eslint).' }, + { name: 'random', type: String, default: false, description: 'Randomizes your modules and tests while running your test suite.' }, + { name: 'replay-execution', type: String, default: false, description: 'A JSON file path which maps from browser number to a list of modules'}, + { name: 'replay-browser', type: [Array, Number], description: 'The number of the browser(s) to run from a specified file path'} ].concat(TestCommand.prototype.availableOptions), + init() { + this._super(...arguments); + this.tasks.Test = TestTask; + this.tasks.TestServer = TestServerTask; + }, + /** * Creates an options validator object. * @@ -79,7 +92,7 @@ module.exports = TestCommand.extend({ this.validator = this._createValidator(commandOptions); if (this.validator.shouldSplit) { - commandOptions.query = addToQuery(commandOptions.query, '_split', commandOptions.split); + commandOptions.query = addToQuery(commandOptions.query, 'split', commandOptions.split); process.env.EMBER_EXAM_SPLIT_COUNT = commandOptions.split; @@ -88,12 +101,14 @@ module.exports = TestCommand.extend({ const partitions = commandOptions.partition; if (partitions) { for (let i = 0; i < partitions.length; i++) { - commandOptions.query = addToQuery(commandOptions.query, '_partition', partitions[i]); + commandOptions.query = addToQuery(commandOptions.query, 'partition', partitions[i]); } } } + } - commandOptions.query = addToQuery(commandOptions.query, '_weighted', commandOptions.weighted); + if (this.validator.shouldLoadBalance) { + commandOptions.query = addToQuery(commandOptions.query, 'loadBalance', commandOptions.loadBalance); } if (this.validator.shouldRandomize) { @@ -103,6 +118,46 @@ module.exports = TestCommand.extend({ return this._super.run.apply(this, arguments); }, + /** + * Returns an execution mapping object that contains number of browsers and modules ran per browsers. + * + * @param {Object} option + */ + getExecutionMappingInstance(options) { + if (executionMapping !== undefined && executionMapping.numberOfBrowsers !== undefined && executionMapping.browserToModuleMap !== undefined) { + return executionMapping; + } + const executionFilePath = options.replayExecution; + const browserIdsToReplay = options.replayBrowser; + + let testMapping = {}; + + try { + // Read the replay execution json file. + const executionToReplay = fs.readFileSync(executionFilePath); + testMapping = JSON.parse(executionToReplay); + } catch (err) { + throw new Error(`Error reading reply execution JSON file - ${err}`); + } + + const browserModuleMapping = testMapping.executionMapping; + + executionMapping.numberOfBrowsers = testMapping.numberOfBrowsers; + executionMapping.browserToModuleMap = ((browserIdsToReplay, browserModuleMapping) => { + const browserModuleMap = {}; + + for (let i = 0; i < browserIdsToReplay.length; i++) { + const browserId = browserIdsToReplay[i]; + const listOfModules = browserModuleMapping[browserId]; + browserModuleMap[browserId] = listOfModules; + } + + return browserModuleMap; + })(browserIdsToReplay, browserModuleMapping); + + return executionMapping; + }, + /** * Adds a `seed` param to the `query` to support randomization. Currently * only works with QUnit. @@ -127,34 +182,99 @@ module.exports = TestCommand.extend({ */ _generateCustomConfigs(commandOptions) { const config = this._super._generateCustomConfigs.apply(this, arguments); + const customBaseUrl = this._getCustomBaseUrl(config, commandOptions); - if (this.validator.shouldParallelize) { - // Get the testPage from the generated config or the Testem config and - // use it as the baseUrl for the parallelized test pages - const baseUrl = config.testPage || this._getTestPage(commandOptions.configFile); - const count = commandOptions.split; - let partitions = commandOptions.partition; - - if (!partitions) { - partitions = []; - for (let i = 0; i < commandOptions.split; i++) { - partitions.push(i + 1); - } - } + const shouldParallelize = this.validator.shouldParallelize; + const shouldLoadBalance = this.validator.shouldLoadBalance; + const shouldReplayExecution = this.validator.shouldReplayExecution; - if (Array.isArray(baseUrl)) { - const command = this; - config.testPage = baseUrl.reduce(function(testPages, baseUrl) { - return testPages.concat(command._generateTestPages(baseUrl, partitions, count)); - }, []); - } else { - config.testPage = this._generateTestPages(baseUrl, partitions, count); + if (!shouldParallelize && !shouldLoadBalance && !shouldReplayExecution) { + return config; + } + + let browserIds = commandOptions.partition; + let appendingParam = 'partition'; + + if (shouldParallelize && !browserIds) { + browserIds = []; + for (let i = 0; i < commandOptions.split; i++) { + browserIds.push(i + 1); } + } else if (shouldLoadBalance) { + config.custom_browser_socket_events = this._addLoadBalancingBrowserSocketEvents(config); + + const browserCount = commandOptions.loadBalance; + appendingParam = 'browser'; + + // Creates an array starting from 1 to browserCount. e.g. if browserCount is 3, the returned array is [1, 2, 3] + browserIds = Array.from({length: browserCount}, (v,k) => k+1); + } else if (shouldReplayExecution) { + const executionMapping = this.getExecutionMappingInstance(commandOptions); + config.custom_browser_socket_events = this._addLoadBalancingBrowserSocketEvents(config); + config.browser_module_mapping = executionMapping.browserToModuleMap; + + appendingParam = 'browser'; + browserIds = commandOptions.replayBrowser; + } + + if (Array.isArray(customBaseUrl)) { + const command = this; + config.testPage = customBaseUrl.reduce(function(testPages, customBaseUrl) { + return testPages.concat(command._generateTestPages(customBaseUrl, appendingParam, browserIds)); + }, []); + } else { + config.testPage = this._generateTestPages(customBaseUrl, appendingParam, browserIds); } return config; }, + /** + * Customizes the base url by specified test splitting options - parellel or loadBalance. + * + * @param {String} config + * @param {Object} commandOptions + * @return {Object} + * @example tests/index.html?hidepassed&split=3 if parallel + * tests/index.html?hidepassed&split=3&loadBalance if load-balance + * tests/index.html?hidepassed&split=3&loadBalance&partition=1&partition=2 if load-balance and partitions are specified + */ + _getCustomBaseUrl(config, commandOptions) { + // Get the testPage from the generated config or the Testem config and + // use it as the baseUrl to customize for the parallelized test pages or test load balancing + const baseUrl = config.testPage || this._getTestPage(commandOptions.configFile); + const splitCount = commandOptions.split; + + const command = this; + + const appendParamToBaseUrl = function(baseUrl) { + if (command.validator.shouldParallelize || command.validator.shouldSplit) { + baseUrl = addToUrl(baseUrl, 'split', splitCount); + } + // `loadBalance` is added to url when running replay-execution in order to emit `set-module-queue` in patch-test-loader. + if (command.validator.shouldLoadBalance || command.validator.shouldReplayExecution) { + baseUrl = addToUrl(baseUrl, 'loadBalance', true) + + const partitions = commandOptions.partition; + if (partitions) { + for (let i = 0; i < partitions.length; i++) { + baseUrl = addToUrl(baseUrl, 'partition', partitions[i]); + } + } + } + + return baseUrl; + } + + if (Array.isArray(baseUrl)) { + return baseUrl.map((currentUrl) => { + return appendParamToBaseUrl(currentUrl); + }) + } else { + return appendParamToBaseUrl(baseUrl); + } + }, + /** * Gets the test page specified by the application's Testem config. * @@ -178,21 +298,19 @@ module.exports = TestCommand.extend({ }, /** - * Generates the test pages to use for parallel runs. For a given baseUrl, - * it appends the total split count and the partition numbers each page is - * running as query params. + * Generates multiple test pages: for a given baseUrl, it appends the partition numbers + * or the browserId each page is running as query params. * - * @param {String} baseUrl - * @param {Array} partitions - * @param {Number} count + * @param {String} customBaseUrl + * @param {String} appendingParam + * @param {Array} testPages */ - _generateTestPages(baseUrl, partitions, count) { - const splitUrl = addToUrl(baseUrl, '_split', count); + _generateTestPages(customBaseUrl, appendingParam, browserIds) { const testPages = []; - - for (let i = 0; i < partitions.length; i++) { - testPages.push(addToUrl(splitUrl, '_partition', partitions[i])); + for (let i = 0; i < browserIds.length; i++) { + const url = addToUrl(customBaseUrl, appendingParam, browserIds[i]); + testPages.push(url); } return testPages; @@ -256,5 +374,97 @@ module.exports = TestCommand.extend({ case 'json': return fs.readJsonSync(file); } + }, + + /** + * Adds additional event handlers to config to enable load balancing. + * + * @param {object} config + * @return {object} events + */ + _addLoadBalancingBrowserSocketEvents(config) { + const events = config.custom_browser_socket_events || {}; + + events['set-modules-queue'] = function(modules) { + // When `load-balance` option is valid we want to have one static list of modules on server side to send a module path to browsers. + const proto = Object.getPrototypeOf(this); + + // browserModuleMapping is defined if `replay-execution` option is being used. + const browserModuleMapping = this.config.progOptions.browser_module_mapping; + + // Updating browserModuleMapping which maps from browser ids to a list of AMD modules ran per the browser + // is needed when running test suite in ci. + if (this.config.appMode === 'ci' && browserModuleMapping && !this.moduleQueue) { + const browserId = /browser=\s*([0-9]*)/.exec(this.launcher.settings.test_page)[1]; + + if (browserId === undefined) { + throw new Error('Browser Id can\'t be found.'); + } + + this.moduleQueue = browserModuleMapping[browserId]; + } else if (!proto.moduleQueue) { + proto.moduleQueue = modules; + proto.moduleMap = {}; + } + } + + events['get-next-module'] = function() { + const proto = Object.getPrototypeOf(this); + const replayExecution = this.config.progOptions.browser_module_mapping; + const moduleQueue = this.moduleQueue; + const moduleName = moduleQueue.shift(); + + if (!moduleName) { + this.socket.emit('testem:module-queue-complete'); + } else { + this.socket.emit('testem:next-module-response', moduleName); + // keep track of the modules ran per browserId when appMode is ci and replayExecution flag is false + if (this.config.appMode === 'ci' && !replayExecution) { + const browserId = /browser=\s*([0-9]*)/.exec(this.launcher.settings.test_page)[1]; + + if (browserId === undefined) { + throw new Error('Browser Id can\'t be found.'); + } + + proto.moduleMap[browserId] = proto.moduleMap[browserId] || []; + proto.moduleMap[browserId].push(moduleName); + } + } + + } + + if (this.validator.shouldReplayExecution) { + return events; + } + + // We only need to register `after-tests-complete` event to create json file when the test execution run is not for replay-execution. + events['after-tests-complete'] = function() { + const proto = Object.getPrototypeOf(this); + const browserCount = Object.keys(config.testPage).length; + // custom_browser_socket_events is set in config when _loadBalance is in test pages and browser_module_mapping is set when running `replay-execution`. + const shouldLoadBalance = !!this.config.progOptions.custom_browser_socket_events && !this.config.progOptions.browser_module_mapping + + proto.finishedBrowsers = proto.finishedBrowsers ? proto.finishedBrowsers + 1 : 1; + + if (proto.finishedBrowsers == browserCount) { + // Clean the moduleQueue and finishedBrowser. This guarantees new test run can set the moduleQueue. Otherwise, the server will return no more test to run. + // TODO: Investigate not using proto + delete proto.moduleQueue; + delete proto.finishedBrowser; + if (this.config.appMode === 'ci' && shouldLoadBalance) { + const fileName = `test-execution-${moment().format('YYYY-MM-DD__HH-MM-SS')}.json`; + const moduleMapJson = { + numberOfBrowsers: browserCount, + executionMapping: proto.moduleMap + }; + const testExecutionJson = JSON.stringify(moduleMapJson); + fs.writeFileSync(fileName, testExecutionJson, (err) => { + if (err) throw err; + }); + } + } + } + + return events; } }); diff --git a/lib/commands/exam/iterate.js b/lib/commands/exam/iterate.js index 87875f2aa..8a3590f87 100644 --- a/lib/commands/exam/iterate.js +++ b/lib/commands/exam/iterate.js @@ -71,7 +71,7 @@ module.exports = { */ _buildForTests() { // TODO: execa - var execSync = require('child_process').execSync; + const execSync = require('child_process').execSync; this._write('\nBuilding app for test iterations.'); execSync('./node_modules/.bin/ember build --output-path ' + this._outputDir, { stdio: 'inherit' }); @@ -82,7 +82,7 @@ module.exports = { */ _cleanupBuild() { // TODO: execa - var rimraf = require('rimraf'); + const rimraf = require('rimraf'); this._write('\nCleaning up test iterations.\n'); rimraf.sync(this._outputDir); diff --git a/lib/commands/task/test-server.js b/lib/commands/task/test-server.js new file mode 100644 index 000000000..ead1454e5 --- /dev/null +++ b/lib/commands/task/test-server.js @@ -0,0 +1,12 @@ +// eslint-disable-next-line node/no-unpublished-require +const TestServerTask = require('ember-cli/lib/tasks/test-server'); + +module.exports = TestServerTask.extend({ + transformOptions(options) { + const transformOptions = this._super(...arguments); + transformOptions.custom_browser_socket_events = options.custom_browser_socket_events; + transformOptions.browser_module_mapping = options.browser_module_mapping; + + return transformOptions; + } +}); diff --git a/lib/commands/task/test.js b/lib/commands/task/test.js new file mode 100644 index 000000000..3bdc296a3 --- /dev/null +++ b/lib/commands/task/test.js @@ -0,0 +1,13 @@ +// eslint-disable-next-line node/no-unpublished-require +const TestTask = require('ember-cli/lib/tasks/test'); + +module.exports = TestTask.extend({ + + transformOptions(options) { + const transformOptions = this._super(...arguments); + transformOptions.custom_browser_socket_events = options.custom_browser_socket_events; + transformOptions.browser_module_mapping = options.browser_module_mapping; + + return transformOptions; + } +}); diff --git a/lib/utils/tests-options-validator.js b/lib/utils/tests-options-validator.js index a7635931f..0e95ad404 100644 --- a/lib/utils/tests-options-validator.js +++ b/lib/utils/tests-options-validator.js @@ -1,4 +1,8 @@ 'use strict'; + +const exam = require('../commands/exam'); +const checkDevDependencies = require('../../index').checkDevDependencies(); + /** * Validates the specified partitions * @@ -8,7 +12,34 @@ */ function validatePartitions(partitions, split) { validatePartitionSplit(partitions, split); - validatePartitionsUnique(partitions); + validateElementsUnique(partitions, 'partition'); +} + +/** + * Validates the specified replay-browser + * + * @param {*} replayBrowser + * @param {*} replayExecution + */ +function validateReplayBrowser(replayBrowser, replayExecution, options) { + exam.prototype.executionMapping = exam.prototype.getExecutionMappingInstance(options); + const numberOfBrowsers = exam.prototype.executionMapping.numberOfBrowsers; + + if (!replayExecution) { + throw new Error('You must specify a file path when using the \'replay-browser\' option.'); + } + + for (const i in replayBrowser) { + const browserId = replayBrowser[i] + if (browserId < 1) { + throw new Error('You must specify replay-browser values greater than or equal to 1.'); + } + if (browserId > numberOfBrowsers) { + throw new Error('You must specify replayBrowser value smaller than a number of browsers in the specified json file.'); + } + } + + validateElementsUnique(replayBrowser, 'replayBrowser'); } /** @@ -35,16 +66,18 @@ function validatePartitionSplit(partitions, split) { } /** - * Ensures that there is no partition listed twice + * Ensures that there is no value duplicated in a given array. * * @private - * @param {Array} partitions + * @param {Array} arr + * @param {String} typeOfValue */ -function validatePartitionsUnique(partitions) { - partitions = partitions.sort(); - for (let i = 0; i < partitions.length - 1; i++) { - if (partitions[i] === partitions[i + 1]) { - throw new Error('You cannot specify the same partition twice.'); +function validateElementsUnique(arr, typeOfValue) { + arr = arr.sort(); + for (let i = 0; i < arr.length - 1; i++) { + if (arr[i] === arr[i + 1]) { + const errorMsg = `You cannot specify the same ${typeOfValue} value twice. ${arr[i].toString()} is repeated.`; + throw new Error(errorMsg); } } } @@ -86,13 +119,10 @@ module.exports = class TestsOptionsValidator { validatePartitions(partitions, split); } - if (options.weighted && !split) { - throw new Error('You used the \'weighted\' option but are not splitting your tests.'); - } - return !!split; } + /** * Determines if we should randomize the tests and validates associated options * (`random`). @@ -101,7 +131,7 @@ module.exports = class TestsOptionsValidator { * @type {Boolean} */ get shouldRandomize() { - var shouldRandomize = (typeof this.options.random === 'string'); + const shouldRandomize = (typeof this.options.random === 'string'); if (shouldRandomize && this.framework === 'mocha') { // eslint-disable-next-line no-console @@ -125,10 +155,61 @@ module.exports = class TestsOptionsValidator { return false; } + if (typeof this.options.loadBalance !== 'undefined') { + throw new Error('You must not use the `load-balance` option with the `parallel` option.'); + } + if (!this.shouldSplit) { throw new Error('You must specify the `split` option in order to run your tests in parallel.'); } return true; } + + get shouldLoadBalance() { + let loadBalance = this.options.loadBalance; + + if (typeof loadBalance == 'undefined') { + return false; + } + + // It's required to use ember-cli version 3.2.0 or greater to support the `load-balance` feature. + if (!checkDevDependencies) { + throw new Error('You must be using ember-cli version 3.2.0 or greater for this feature to work properly.'); + } + + if (loadBalance < 1) { + throw new Error('You must specify a load-balance value greater than or equal to 1.'); + } + + if (this.options.parallel) { + throw new Error('You must not use the `parallel` option with the `load-balance` option.'); + } + + return true; + } + + /** + * Determines if we should replay execution for reproduction. + * options (`replay-execution`). + * + * @public + * @type {Boolean} + */ + get shouldReplayExecution() { + const replayBrowser = this.options.replayBrowser; + const replayExecution = this.options.replayExecution; + + if (!replayExecution) { + return false; + } + + if (!replayBrowser) { + throw new Error('You must specify the `replay-browser` option in order to use `replay-execution` option.'); + } + + validateReplayBrowser(replayBrowser, replayExecution, this.options); + + return true; + } }; diff --git a/node-tests/acceptance/exam-iterate-test.js b/node-tests/acceptance/exam-iterate-test.js index ee4d6baf1..0f6e6cba6 100644 --- a/node-tests/acceptance/exam-iterate-test.js +++ b/node-tests/acceptance/exam-iterate-test.js @@ -22,10 +22,10 @@ describe('Acceptance | Exam Iterate Command', function() { assert.ok(stdout.includes('Running iteration #1.'), 'Logs first iteration'); assert.ok(stdout.includes('Running iteration #2.'), 'Logs second iteration'); - var seedRE = /Randomizing tests with seed: (.*)/g; + const seedRE = /Randomizing tests with seed: (.*)/g; - var firstSeed = seedRE.exec(stdout)[1]; - var secondSeed = seedRE.exec(stdout)[1]; + const firstSeed = seedRE.exec(stdout)[1]; + const secondSeed = seedRE.exec(stdout)[1]; assert.ok(firstSeed, 'first seed exists'); assert.ok(secondSeed, 'second seed exists'); @@ -38,7 +38,6 @@ describe('Acceptance | Exam Iterate Command', function() { it('should test the app with additional options passed in and catch failure cases', function() { const execution = execa('ember', ['exam:iterate', '2' ,'--options' ,'--parallel']); - return execution.then(assertExpectRejection, error => { const splitErrorRE = /You must specify the `split` option in order to run your tests in parallel./g; @@ -66,10 +65,10 @@ describe('Acceptance | Exam Iterate Command', function() { assert.ok(stdout.includes('Running iteration #1.'), 'Logs first iteration'); assert.ok(stdout.includes('Running iteration #2.'), 'Logs second iteration'); - var seedRE = /Randomizing tests with seed: (.*)/g; + const seedRE = /Randomizing tests with seed: (.*)/g; - var firstSeed = seedRE.exec(stdout)[1]; - var secondSeed = seedRE.exec(stdout)[1]; + const firstSeed = seedRE.exec(stdout)[1]; + const secondSeed = seedRE.exec(stdout)[1]; assert.ok(firstSeed, 'first seed exists'); assert.ok(secondSeed, 'second seed exists'); diff --git a/node-tests/acceptance/exam-test.js b/node-tests/acceptance/exam-test.js index aa15155ee..d4f8f1aae 100644 --- a/node-tests/acceptance/exam-test.js +++ b/node-tests/acceptance/exam-test.js @@ -13,7 +13,11 @@ function getNumberOfTests(str) { return match && parseInt(match[1], 10); } +<<<<<<< HEAD const TOTAL_NUM_TESTS = 33; // Total Number of tests without the global "Ember.onerror validation tests" +======= +const TOTAL_NUM_TESTS = 35; // Total Number of tests without the global "Ember.onerror validation tests" +>>>>>>> [Feature] Test Load Balancing function getTotalNumberOfTests(output) { // In ember-qunit 3.4.0, this new check was added: https://github.com/emberjs/ember-qunit/commit/a7e93c4b4b535dae62fed992b46c00b62bfc83f4 diff --git a/node-tests/unit/commands/exam-test.js b/node-tests/unit/commands/exam-test.js index 578638e22..c985dae51 100644 --- a/node-tests/unit/commands/exam-test.js +++ b/node-tests/unit/commands/exam-test.js @@ -1,20 +1,22 @@ -var assert = require('assert'); -var MockProject = require('ember-cli/tests/helpers/mock-project'); -var Task = require('ember-cli/lib/models/task'); -var RSVP = require('rsvp'); -var sinon = require('sinon'); +'use strict'; -var ExamCommand = require('../../../lib/commands/exam'); -var TestOptionsValidator = require('../../../lib/utils/tests-options-validator'); +const assert = require('assert'); +const MockProject = require('ember-cli/tests/helpers/mock-project'); +const Task = require('ember-cli/lib/models/task'); +const RSVP = require('rsvp'); +const sinon = require('sinon'); + +const ExamCommand = require('../../../lib/commands/exam'); +const TestOptionsValidator = require('../../../lib/utils/tests-options-validator'); describe('ExamCommand', function() { function createCommand() { - var tasks = { + const tasks = { Build: Task.extend(), Test: Task.extend() }; - var project = new MockProject(); + const project = new MockProject(); project.isEmberCLIProject = function() { return true; }; @@ -28,8 +30,8 @@ describe('ExamCommand', function() { } describe('run', function() { - var command; - var called; + let command; + let called; beforeEach(function() { command = createCommand(); @@ -55,37 +57,43 @@ describe('ExamCommand', function() { }); }); - it('should set \'partition\' on the query option with one partition', function() { + it('should set \'partition\' in the query option with one partition', function() { return command.run({ split: 2, partition: [2] }).then(function() { - assert.equal(called.testRunOptions.query, '_split=2&_partition=2'); + assert.equal(called.testRunOptions.query, 'split=2&partition=2'); + }); + }); + + it('should set \'load-balance\' in the query option', function() { + return command.run({ 'loadBalance': 1 }).then(function() { + assert.equal(called.testRunOptions.query, 'loadBalance=1'); }); }); - it('should set \'partition\' on the query option with multiple partitions', function() { + it('should set \'partition\' in the query option with multiple partitions', function() { return command.run({ split: 2, partition: [1, 2] }).then(function() { - assert.equal(called.testRunOptions.query, '_split=2&_partition=1&_partition=2'); + assert.equal(called.testRunOptions.query, 'split=2&partition=1&partition=2'); }); }); it('should append \'partition\' to the query option', function() { return command.run({ split: 2, partition: [2], query: 'someQuery=derp&hidepassed' }).then(function() { - assert.equal(called.testRunOptions.query, 'someQuery=derp&hidepassed&_split=2&_partition=2'); + assert.equal(called.testRunOptions.query, 'someQuery=derp&hidepassed&split=2&partition=2'); }); }); it('should not append \'partition\' to the query option when parallelizing', function() { return command.run({ split: 2, partition: [1, 2], parallel: true }).then(function() { - assert.equal(called.testRunOptions.query, '_split=2'); + assert.equal(called.testRunOptions.query, 'split=2'); }); }); it('should not append \'partition\' to the query option when not parallelizing without partitions', function() { return command.run({ split: 2 }).then(function() { - assert.equal(called.testRunOptions.query, '_split=2'); + assert.equal(called.testRunOptions.query, 'split=2'); }); }); - it('should set \'seed=1337\' on the query option', function() { + it('should set \'seed=1337\' in the query option', function() { return command.run({ random: '1337' }).then(function() { assert.equal(called.testRunOptions.query, 'seed=1337'); }); @@ -97,18 +105,46 @@ describe('ExamCommand', function() { }); }); - it('should set \'seed=random_seed\' on the query option', function() { - var randomStub = sinon.stub(Math, 'random').returns(' random_seed'); + it('should set \'seed=random_seed\' in the query option', function() { + const randomStub = sinon.stub(Math, 'random').returns(' random_seed'); return command.run({ random: '' }).then(function() { assert.equal(called.testRunOptions.query, 'seed=random_seed'); randomStub.restore(); }); }); + }); + + describe('_getCustomBaseUrl', function() { + function getCustomBaseUrl(config, options) { + const project = new MockProject(); + project.isEmberCLIProject = function() { return true; }; - it('should set \'weighted\' on the query option', function() { - return command.run({ split: 2, partition: [2], weighted: true }).then(function() { - assert.equal(called.testRunOptions.query, '_split=2&_partition=2&_weighted'); + const command = new ExamCommand({ + project: project, + tasks: {} }); + + command.validator = new TestOptionsValidator(options); + + return command._getCustomBaseUrl(config, options) + } + + it('should add `loadBalance` to a base url with load-balance', function() { + const baseUrl = getCustomBaseUrl({ testPage: 'tests/index.html?hidepassed' }, { loadBalance: 3 }); + + assert.deepEqual(baseUrl, 'tests/index.html?hidepassed&loadBalance'); + }); + + it('should add `split` to a base url with split and parallelizing.', function() { + const baseUrl = getCustomBaseUrl({ testPage: 'tests/index.html?hidepassed' }, { parallel: true, split: 2 }); + + assert.deepEqual(baseUrl, 'tests/index.html?hidepassed&split=2'); + }); + + it('should add `split` and `loadBalance` to a base url with split and load-balance.', function() { + const baseUrl = getCustomBaseUrl({ testPage: 'tests/index.html?hidepassed' }, { loadBalance: 3, split: 2 }); + + assert.deepEqual(baseUrl, 'tests/index.html?hidepassed&split=2&loadBalance'); }); it('should set split env var', function() { @@ -120,12 +156,13 @@ describe('ExamCommand', function() { describe('_generateCustomConfigs', function() { function generateConfig(options) { - var project = new MockProject(); + const project = new MockProject(); project.isEmberCLIProject = function() { return true; }; - var command = new ExamCommand({ - project: project + const command = new ExamCommand({ + project: project, + tasks: {} }); command.validator = new TestOptionsValidator(options); @@ -134,53 +171,53 @@ describe('ExamCommand', function() { } it('should have a null test page when not parallelizing', function() { - var config = generateConfig({}); + const config = generateConfig({}); assert.deepEqual(config.testPage, undefined); }); it('should modify the config to have multiple test pages with no partitions specified', function() { - var config = generateConfig({ + const config = generateConfig({ parallel: true, split: 2 }); assert.deepEqual(config.testPage, [ - 'tests/index.html?hidepassed&_split=2&_partition=1', - 'tests/index.html?hidepassed&_split=2&_partition=2' + 'tests/index.html?hidepassed&split=2&partition=1', + 'tests/index.html?hidepassed&split=2&partition=2' ]); }); it('should modify the config to have multiple test pages with specified partitions', function() { - var config = generateConfig({ + const config = generateConfig({ parallel: true, split: 4, partition: [3, 4] }); assert.deepEqual(config.testPage, [ - 'tests/index.html?hidepassed&_split=4&_partition=3', - 'tests/index.html?hidepassed&_split=4&_partition=4' + 'tests/index.html?hidepassed&split=4&partition=3', + 'tests/index.html?hidepassed&split=4&partition=4' ]); }); it('should modify the config to have multiple test pages for each test_page in the config file with no partitions specified', function() { - var config = generateConfig({ + const config = generateConfig({ parallel: true, split: 2, configFile: 'testem.multiple-test-page.js' }); assert.deepEqual(config.testPage, [ - 'tests/index.html?hidepassed&derp=herp&_split=2&_partition=1', - 'tests/index.html?hidepassed&derp=herp&_split=2&_partition=2', - 'tests/index.html?hidepassed&foo=bar&_split=2&_partition=1', - 'tests/index.html?hidepassed&foo=bar&_split=2&_partition=2' + 'tests/index.html?hidepassed&derp=herp&split=2&partition=1', + 'tests/index.html?hidepassed&derp=herp&split=2&partition=2', + 'tests/index.html?hidepassed&foo=bar&split=2&partition=1', + 'tests/index.html?hidepassed&foo=bar&split=2&partition=2' ]); }); it('should modify the config to have multiple test pages for each test_page in the config file with partitions specified', function() { - var config = generateConfig({ + const config = generateConfig({ parallel: true, split: 4, partition: [3, 4], @@ -188,15 +225,95 @@ describe('ExamCommand', function() { }); assert.deepEqual(config.testPage, [ - 'tests/index.html?hidepassed&derp=herp&_split=4&_partition=3', - 'tests/index.html?hidepassed&derp=herp&_split=4&_partition=4', - 'tests/index.html?hidepassed&foo=bar&_split=4&_partition=3', - 'tests/index.html?hidepassed&foo=bar&_split=4&_partition=4' + 'tests/index.html?hidepassed&derp=herp&split=4&partition=3', + 'tests/index.html?hidepassed&derp=herp&split=4&partition=4', + 'tests/index.html?hidepassed&foo=bar&split=4&partition=3', + 'tests/index.html?hidepassed&foo=bar&split=4&partition=4' + ]); + }); + + it('should have a custom test page', function() { + const config = generateConfig({ + query: 'foo=bar', + 'test-page': 'tests.html' + }); + + assert.equal(config.testPage, 'tests.html?foo=bar'); + }); + + it('should modify the config to have a test page with \'loadBalance\' when no specified number of browser', function() { + const config = generateConfig({ + 'loadBalance': 1 + }); + + assert.deepEqual(config.testPage, [ + 'tests/index.html?hidepassed&loadBalance&browser=1' + ]) + }); + + it('should modify the config to have a test page with \'loadBalance\' with splitting when no specified number of browser', function() { + const config = generateConfig({ + 'loadBalance': 1, + split: 2, + }); + + assert.deepEqual(config.testPage, [ + 'tests/index.html?hidepassed&split=2&loadBalance&browser=1' + ]) + }); + + it('should modify the config to have multiple test pages with test loading balanced, no specified partitions and no splitting ', function(){ + const config = generateConfig({ + 'loadBalance': 2, + }); + + assert.deepEqual(config.testPage, [ + 'tests/index.html?hidepassed&loadBalance&browser=1', + 'tests/index.html?hidepassed&loadBalance&browser=2' + ]) + }); + + it('should modify the config to have multiple test pages with splitting when loading test load-balanced', function(){ + const config = generateConfig({ + 'loadBalance': 2, + split: 2, + }); + + assert.deepEqual(config.testPage, [ + 'tests/index.html?hidepassed&split=2&loadBalance&browser=1', + 'tests/index.html?hidepassed&split=2&loadBalance&browser=2' + ]) + }); + + it('should modify the config to have multiple test pages with specified partitions when loading test balanced', function(){ + const config = generateConfig({ + 'loadBalance': 2, + split: 3, + partition: [2, 3], + }); + + assert.deepEqual(config.testPage, [ + 'tests/index.html?hidepassed&split=3&loadBalance&partition=2&partition=3&browser=1', + 'tests/index.html?hidepassed&split=3&loadBalance&partition=2&partition=3&browser=2' + ]) + }); + + it('should modify the config to have multiple test pages for each test_page in the config file with partitions specified and test loading balanced', function() { + const config = generateConfig({ + 'loadBalance': 1, + split: 4, + partition: [3, 4], + configFile: 'testem.multiple-test-page.js' + }); + + assert.deepEqual(config.testPage, [ + 'tests/index.html?hidepassed&derp=herp&split=4&loadBalance&partition=3&partition=4&browser=1', + 'tests/index.html?hidepassed&foo=bar&split=4&loadBalance&partition=3&partition=4&browser=1' ]); }); it('should have a custom test page', function() { - var config = generateConfig({ + const config = generateConfig({ query: 'foo=bar', 'test-page': 'tests.html' }); @@ -205,7 +322,7 @@ describe('ExamCommand', function() { }); it('should modify the config to have multiple test pages with a custom base url', function() { - var config = generateConfig({ + const config = generateConfig({ parallel: true, split: 2, query: 'foo=bar', @@ -213,23 +330,23 @@ describe('ExamCommand', function() { }); assert.deepEqual(config.testPage, [ - 'tests.html?foo=bar&_split=2&_partition=1', - 'tests.html?foo=bar&_split=2&_partition=2' + 'tests.html?foo=bar&split=2&partition=1', + 'tests.html?foo=bar&split=2&partition=2' ]); }); it('should warn if no test_page is defined but use a default', function() { - var warnStub = sinon.stub(console, 'warn'); + const warnStub = sinon.stub(console, 'warn'); - var config = generateConfig({ + const config = generateConfig({ parallel: true, split: 2, configFile: 'testem.no-test-page.js' }); assert.deepEqual(config.testPage, [ - 'tests/index.html?hidepassed&_split=2&_partition=1', - 'tests/index.html?hidepassed&_split=2&_partition=2' + 'tests/index.html?hidepassed&split=2&partition=1', + 'tests/index.html?hidepassed&split=2&partition=2' ]); sinon.assert.calledOnce(warnStub); @@ -240,7 +357,7 @@ describe('ExamCommand', function() { }); describe('_getTestFramework', function() { - var command; + let command; function assertFramework(command, name) { assert.equal(command._getTestFramework(), name); @@ -265,7 +382,7 @@ describe('ExamCommand', function() { }); it('returns qunit if ember-cli-mocha is not a dependency of any kind', function() { - var command = createCommand(); + command = createCommand(); assertFramework(command, 'qunit'); }); }); diff --git a/node-tests/unit/lint-test.js b/node-tests/unit/lint-test.js index 585eb4187..173b4a030 100644 --- a/node-tests/unit/lint-test.js +++ b/node-tests/unit/lint-test.js @@ -1,4 +1,6 @@ -var lint = require('mocha-eslint'); +'use strict'; + +const lint = require('mocha-eslint'); lint([ 'lib', diff --git a/node-tests/unit/utils/tests-options-validator-test.js b/node-tests/unit/utils/tests-options-validator-test.js index 7c5ba4cae..fe58bd09c 100644 --- a/node-tests/unit/utils/tests-options-validator-test.js +++ b/node-tests/unit/utils/tests-options-validator-test.js @@ -1,29 +1,30 @@ -var assert = require('assert'); +'use strict'; -var TestOptionsValidator = require('../../../lib/utils/tests-options-validator'); +const assert = require('assert'); +const TestOptionsValidator = require('../../../lib/utils/tests-options-validator'); describe('TestOptionsValidator', function() { function shouldThrow(prop, options, message) { - var validator = new TestOptionsValidator(options); - assert.throws(function() { return validator['should' + prop]; }, message); + const validator = new TestOptionsValidator(options); + assert.throws(() => validator['should' + prop], message); } function shouldEqual(prop, options, value) { - var validator = new TestOptionsValidator(options); + const validator = new TestOptionsValidator(options); assert.equal(validator['should' + prop], value); } function shouldWarn(prop, options, value) { /* eslint-disable no-console */ - var originalWarn = console.warn; - var warnCalled = 0; - var warnMessage = ''; + let originalWarn = console.warn; + let warnCalled = 0; + let warnMessage = ''; console.warn = function(message) { warnCalled++; warnMessage = message; }; - var validator = new TestOptionsValidator(options, options.framework); + const validator = new TestOptionsValidator(options, options.framework); assert.notEqual(validator['should' + prop], undefined); assert.equal(warnCalled, 1); assert.equal(warnMessage, value); @@ -58,11 +59,7 @@ describe('TestOptionsValidator', function() { }); it('should throw an error if `partition` contains duplicate values', function() { - shouldSplitThrows({ split: 2, partition: [1, 2, 1] }, /You cannot specify the same partition twice./); - }); - - it('should throw an error if `weighted` is being used without `split`', function() { - shouldSplitThrows({ weighted: true }, /You used the 'weighted' option but are not splitting your tests/); + shouldSplitThrows({ split: 2, partition: [1, 2, 1] }, /You cannot specify the same partition value twice. 1 is repeated./); }); it('should return true if using `split`', function() { @@ -109,6 +106,10 @@ describe('TestOptionsValidator', function() { shouldThrow('Parallelize', { parallel: true }, /You must specify the `split` option in order to run your tests in parallel/); }); + it('it should throw an error if `parallel` is being used with `load-balance`', function() { + shouldThrow('Parallelize', { parallel: true, loadBalance: 1, split:2 }, /You must not use the `load-balance` option with the `parallel` option/); + }) + it('should return false', function() { shouldEqual('Parallelize', { parallel: false }, false); }); @@ -117,4 +118,40 @@ describe('TestOptionsValidator', function() { shouldEqual('Parallelize', { split: 2, parallel: true }, true); }); }); + + describe('shouldLoadBalance', function() { + it('should throw an error if `load-balance` contains a value less than 1', function() { + shouldThrow('LoadBalance', { loadBalance: -1}, /You must specify a load-balance value greater than or equal to 1/); + }); + + it('should throw an error if `load-balance` is being used with `parallel', function() { + shouldThrow('LoadBalance', { loadBalance: 2, parallel: true}, /You must not use the `parallel` option with the `load-balance` option/); + }); + + it('should return true', function() { + shouldEqual('LoadBalance', { loadBalance: 3 }, true); + }); + }); + + describe('shouldReplayExecution', function() { + it('should throw an error if `replay-execution` is being used without `replay-browser`', function() { + shouldThrow('ReplayExecution', { replayExecution: 'test-execution-0000000.json'}, /You must specify the `replay-browser` option in order to use `replay-execution` option./); + }); + + it('should throw an error if `replay-browser` contains a value less than 1', function() { + shouldThrow('ReplayExecution', { replayExecution: 'test-execution-0000000.json', replayBrowser: [1, 0]}, /You must specify replay-browser values greater than or equal to 1./); + }); + + it('should throw an error if `replay-browser` contains duplicate values', function() { + shouldThrow('ReplayExecution', { replayExecution: 'test-execution-0000000.json', replayBrowser: [1, 2, 1]}, /You cannot specify the same replayBrowser value twice. 1 is repeated./); + }) + + it('should throw an error if `replay-browser` contains an invalid browser number', function() { + shouldThrow('ReplayExecution', { replayExecution: 'test-execution-0000000.json', replayBrowser: [3, 1]}, /You must specify replayBrowser value smaller than a number of browsers in the specified json file./); + }) + + it('should return true', function() { + shouldEqual('ReplayExecution', { replayExecution: 'test-execution-0000000.json', replayBrowser: [1, 2] }, true); + }); + }); }); diff --git a/package.json b/package.json index 80f422ef8..5a546468c 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "ember-cli-babel": "^7.5.0", "execa": "^1.0.0", "fs-extra": "^7.0.1", + "moment": "^2.22.2", "rimraf": "^2.6.2" }, "devDependencies": { diff --git a/test-execution-0000000.json b/test-execution-0000000.json new file mode 100644 index 000000000..e9ac1a328 --- /dev/null +++ b/test-execution-0000000.json @@ -0,0 +1,11 @@ +{ + "numberOfBrowsers": 2, + "executionMapping":{ + "1":[ + "/tests/integration/components/my-component-test" + ], + "2":[ + "/tests/integration/components/navigating-component-test" + ] + } +} diff --git a/tests/test-helper.js b/tests/test-helper.js index 50817fd61..aeb9334a3 100644 --- a/tests/test-helper.js +++ b/tests/test-helper.js @@ -2,7 +2,7 @@ import { setResolver } from '@ember/test-helpers'; import resolver from './helpers/resolver'; -import loadEmberExam from 'ember-exam/test-support/load'; +import emberExam from 'ember-exam/test-support/load'; const framework = require.has('ember-qunit') ? 'qunit' : 'mocha'; const oppositeFramework = !require.has('ember-qunit') ? 'qunit' : 'mocha'; @@ -14,8 +14,7 @@ Object.keys(require.entries).forEach((entry) => { }); setResolver(resolver); - -loadEmberExam(); +emberExam.loadEmberExam(); // ember-qunit >= v3 support if (framework === 'qunit') { diff --git a/tests/unit/mocha/test-loader-test.js b/tests/unit/mocha/test-loader-test.js index 4e73632d7..861cb8e5d 100644 --- a/tests/unit/mocha/test-loader-test.js +++ b/tests/unit/mocha/test-loader-test.js @@ -1,4 +1,4 @@ -import TestLoader from 'ember-cli-test-loader/test-support/index'; +import TestLoader from 'ember-cli-test-loader/test-support'; import { describe, it, beforeEach, afterEach } from 'mocha'; import { expect } from 'chai'; diff --git a/tests/unit/mocha/weight-test-modules-test.js b/tests/unit/mocha/weight-test-modules-test.js new file mode 100644 index 000000000..dabfd3bd8 --- /dev/null +++ b/tests/unit/mocha/weight-test-modules-test.js @@ -0,0 +1,49 @@ +import weightTestModules from 'ember-exam/test-support/-private/weight-test-modules'; +import { describe, it } from 'mocha'; +import { expect } from 'chai'; + +describe('Unit | weight-test-modules', () => { + it('should sort a list of file paths by weight', function() { + const listOfModules = [ + '/jshint/test-1-test', + '/acceptance/test-1-test', + '/unit/test-1-test', + '/integration/test-1-test', + 'test-1-test']; + + expect(weightTestModules(listOfModules)).to.deep.equal([ + '/acceptance/test-1-test', + 'test-1-test', + '/integration/test-1-test', + '/unit/test-1-test', + '/jshint/test-1-test' + ]); + }); + + it('should sort a list of file paths by weight and alphbetical order', function() { + const listOfModules = [ + 'test-b-test', + 'test-a-test', + '/jshint/test-b-test', + '/integration/test-b-test', + '/integration/test-a-test', + '/unit/test-b-test', + '/acceptance/test-b-test', + '/acceptance/test-a-test', + '/unit/test-a-test', + '/jshint/test-a-test']; + + expect(weightTestModules(listOfModules)).to.deep.equal([ + '/acceptance/test-a-test', + '/acceptance/test-b-test', + 'test-a-test', + 'test-b-test', + '/integration/test-a-test', + '/integration/test-b-test', + '/unit/test-a-test', + '/unit/test-b-test', + '/jshint/test-a-test', + '/jshint/test-b-test' + ]); + }); +}); diff --git a/tests/unit/qunit/test-loader-test.js b/tests/unit/qunit/test-loader-test.js index c8e01fe22..68e089e06 100644 --- a/tests/unit/qunit/test-loader-test.js +++ b/tests/unit/qunit/test-loader-test.js @@ -48,8 +48,8 @@ test('loads all test modules by default', function(assert) { test('loads modules from a specified partition', function(assert) { TestLoader._urlParams = { - _partition: 3, - _split: 4, + partition: 3, + split: 4, }; TestLoader.load(); @@ -62,8 +62,8 @@ test('loads modules from a specified partition', function(assert) { test('loads modules from multiple specified partitions', function(assert) { TestLoader._urlParams = { - _partition: [1, 3], - _split: 4, + partition: [1, 3], + split: 4, }; TestLoader.load(); @@ -78,7 +78,7 @@ test('loads modules from multiple specified partitions', function(assert) { test('loads modules from the first partition by default', function(assert) { TestLoader._urlParams = { - _split: 4, + split: 4, }; TestLoader.load(); @@ -91,8 +91,8 @@ test('loads modules from the first partition by default', function(assert) { test('handles params as strings', function(assert) { TestLoader._urlParams = { - _partition: '3', - _split: '4', + partition: '3', + split: '4', }; TestLoader.load(); @@ -105,7 +105,7 @@ test('handles params as strings', function(assert) { test('throws an error if splitting less than one', function(assert) { TestLoader._urlParams = { - _split: 0, + split: 0, }; assert.throws(() => { @@ -115,8 +115,8 @@ test('throws an error if splitting less than one', function(assert) { test('throws an error if partition isn\'t a number', function(assert) { TestLoader._urlParams = { - _split: 2, - _partition: 'foo', + split: 2, + partition: 'foo', }; assert.throws(() => { @@ -126,8 +126,8 @@ test('throws an error if partition isn\'t a number', function(assert) { test('throws an error if partition isn\'t a number with multiple partitions', function(assert) { TestLoader._urlParams = { - _split: 2, - _partition: [1, 'foo'], + split: 2, + partition: [1, 'foo'], }; assert.throws(() => { @@ -137,8 +137,8 @@ test('throws an error if partition isn\'t a number with multiple partitions', fu test('throws an error if loading partition greater than split number', function(assert) { TestLoader._urlParams = { - _split: 2, - _partition: 3, + split: 2, + partition: 3, }; assert.throws(() => { @@ -148,8 +148,8 @@ test('throws an error if loading partition greater than split number', function( test('throws an error if loading partition greater than split number with multiple partitions', function(assert) { TestLoader._urlParams = { - _split: 2, - _partition: [2, 3], + split: 2, + partition: [2, 3], }; assert.throws(() => { @@ -159,8 +159,8 @@ test('throws an error if loading partition greater than split number with multip test('throws an error if loading partition less than one', function(assert) { TestLoader._urlParams = { - _split: 2, - _partition: 0, + split: 2, + partition: 0, }; assert.throws(() => { @@ -171,8 +171,8 @@ test('throws an error if loading partition less than one', function(assert) { test('load works without lint tests', function(assert) { QUnit.urlParams.nolint = true; TestLoader._urlParams = { - _partition: 4, - _split: 4, + partition: 4, + split: 4, }; TestLoader.load(); @@ -193,8 +193,8 @@ test('load works without non-lint tests', function(assert) { }; TestLoader._urlParams = { - _partition: 4, - _split: 4, + partition: 4, + split: 4, }; TestLoader.load(); @@ -219,13 +219,13 @@ test('load works with a double-digit single partition', function(assert) { }; TestLoader._urlParams = { - _partition: '10', - _split: 10, + partition: '10', + split: 10, }; TestLoader.load(); assert.deepEqual(this.requiredModules, [ 'test-10-test', - ]); -}); + ]); +}); \ No newline at end of file diff --git a/tests/unit/qunit/weight-test-modules-test.js b/tests/unit/qunit/weight-test-modules-test.js new file mode 100644 index 000000000..a1c88182f --- /dev/null +++ b/tests/unit/qunit/weight-test-modules-test.js @@ -0,0 +1,48 @@ +import { module, test } from 'qunit'; +import weightTestModules from 'ember-exam/test-support/-private/weight-test-modules'; + + +module('Unit | weight-test-modules', () => { + test('should sort a list of file paths by weight', function(assert) { + const listOfModules = [ + '/jshint/test-1-test', + '/acceptance/test-1-test', + '/unit/test-1-test', + '/integration/test-1-test', + 'test-1-test']; + + assert.deepEqual([ + '/acceptance/test-1-test', + 'test-1-test', + '/integration/test-1-test', + '/unit/test-1-test', + '/jshint/test-1-test'], weightTestModules(listOfModules)); + }); + + test('should sort a list of file paths by weight and alphbetical order', function(assert) { + const listOfModules = [ + 'test-b-test', + 'test-a-test', + '/jshint/test-b-test', + '/integration/test-b-test', + '/integration/test-a-test', + '/unit/test-b-test', + '/acceptance/test-b-test', + '/acceptance/test-a-test', + '/unit/test-a-test', + '/jshint/test-a-test']; + + assert.deepEqual([ + '/acceptance/test-a-test', + '/acceptance/test-b-test', + 'test-a-test', + 'test-b-test', + '/integration/test-a-test', + '/integration/test-b-test', + '/unit/test-a-test', + '/unit/test-b-test', + '/jshint/test-a-test', + '/jshint/test-b-test'], weightTestModules(listOfModules)); + }); + +}); diff --git a/yarn.lock b/yarn.lock index 8941d3cee..a102e40f6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10,17 +10,17 @@ "@babel/highlight" "^7.0.0" "@babel/core@^7.0.0": - version "7.1.6" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.1.6.tgz#3733cbee4317429bc87c62b29cf8587dba7baeb3" - integrity sha512-Hz6PJT6e44iUNpAn8AoyAs6B3bl60g7MJQaI0rZEar6ECzh6+srYO1xlIdssio34mPaUtAb1y+XlkkSJzok3yw== + version "7.2.0" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.2.0.tgz#a4dd3814901998e93340f0086e9867fefa163ada" + integrity sha512-7pvAdC4B+iKjFFp9Ztj0QgBndJ++qaMeonT185wAqUnhipw8idm9Rv1UMyBuKtYjfl6ORNkgEgcsYLfHX/GpLw== dependencies: "@babel/code-frame" "^7.0.0" - "@babel/generator" "^7.1.6" - "@babel/helpers" "^7.1.5" - "@babel/parser" "^7.1.6" + "@babel/generator" "^7.2.0" + "@babel/helpers" "^7.2.0" + "@babel/parser" "^7.2.0" "@babel/template" "^7.1.2" "@babel/traverse" "^7.1.6" - "@babel/types" "^7.1.6" + "@babel/types" "^7.2.0" convert-source-map "^1.1.0" debug "^4.1.0" json5 "^2.1.0" @@ -29,12 +29,12 @@ semver "^5.4.1" source-map "^0.5.0" -"@babel/generator@^7.0.0", "@babel/generator@^7.1.6": - version "7.1.6" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.1.6.tgz#001303cf87a5b9d093494a4bf251d7b5d03d3999" - integrity sha512-brwPBtVvdYdGxtenbQgfCdDPmtkmUBZPjUoK5SXJEBuHaA5BCubh9ly65fzXz7R6o5rA76Rs22ES8Z+HCc0YIQ== +"@babel/generator@^7.0.0", "@babel/generator@^7.1.6", "@babel/generator@^7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.2.0.tgz#eaf3821fa0301d9d4aef88e63d4bcc19b73ba16c" + integrity sha512-BA75MVfRlFQG2EZgFYIwyT1r6xSkwfP2bdkY/kLZusEYWiJs4xCowab/alaEaT0wSvmVuXGqiefeBlP+7V1yKg== dependencies: - "@babel/types" "^7.1.6" + "@babel/types" "^7.2.0" jsesc "^2.5.1" lodash "^4.17.10" source-map "^0.5.0" @@ -186,23 +186,23 @@ "@babel/types" "^7.0.0" "@babel/helper-wrap-function@^7.1.0": - version "7.1.0" - resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.1.0.tgz#8cf54e9190706067f016af8f75cb3df829cc8c66" - integrity sha512-R6HU3dete+rwsdAfrOzTlE9Mcpk4RjU3aX3gi9grtmugQY0u79X7eogUvfXA5sI81Mfq1cn6AgxihfN33STjJA== + version "7.2.0" + resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.2.0.tgz#c4e0012445769e2815b55296ead43a958549f6fa" + integrity sha512-o9fP1BZLLSrYlxYEYyl2aS+Flun5gtjTIG8iln+XuEzQTs0PLagAGSXUcqruJwD5fM48jzIEggCKpIfWTcR7pQ== dependencies: "@babel/helper-function-name" "^7.1.0" "@babel/template" "^7.1.0" "@babel/traverse" "^7.1.0" - "@babel/types" "^7.0.0" + "@babel/types" "^7.2.0" -"@babel/helpers@^7.1.5": - version "7.1.5" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.1.5.tgz#68bfc1895d685f2b8f1995e788dbfe1f6ccb1996" - integrity sha512-2jkcdL02ywNBry1YNFAH/fViq4fXG0vdckHqeJk+75fpQ2OH+Az6076tX/M0835zA45E0Cqa6pV5Kiv9YOqjEg== +"@babel/helpers@^7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.2.0.tgz#8335f3140f3144270dc63c4732a4f8b0a50b7a21" + integrity sha512-Fr07N+ea0dMcMN8nFpuK6dUIT7/ivt9yKQdEEnjVS83tG2pHwPi03gYmk/tyuwONnZ+sY+GFFPlWGgCtW1hF9A== dependencies: "@babel/template" "^7.1.2" "@babel/traverse" "^7.1.5" - "@babel/types" "^7.1.5" + "@babel/types" "^7.2.0" "@babel/highlight@^7.0.0": version "7.0.0" @@ -213,116 +213,116 @@ esutils "^2.0.2" js-tokens "^4.0.0" -"@babel/parser@^7.0.0", "@babel/parser@^7.1.2", "@babel/parser@^7.1.6": - version "7.1.6" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.1.6.tgz#16e97aca1ec1062324a01c5a6a7d0df8dd189854" - integrity sha512-dWP6LJm9nKT6ALaa+bnL247GHHMWir3vSlZ2+IHgHgktZQx0L3Uvq2uAWcuzIe+fujRsYWBW2q622C5UvGK9iQ== +"@babel/parser@^7.0.0", "@babel/parser@^7.1.2", "@babel/parser@^7.1.6", "@babel/parser@^7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.2.0.tgz#02d01dbc330b6cbf36b76ac93c50752c69027065" + integrity sha512-M74+GvK4hn1eejD9lZ7967qAwvqTZayQa3g10ag4s9uewgR7TKjeaT0YMyoq+gVfKYABiWZ4MQD701/t5e1Jhg== -"@babel/plugin-proposal-async-generator-functions@^7.1.0": - version "7.1.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.1.0.tgz#41c1a702e10081456e23a7b74d891922dd1bb6ce" - integrity sha512-Fq803F3Jcxo20MXUSDdmZZXrPe6BWyGcWBPPNB/M7WaUYESKDeKMOGIxEzQOjGSmW/NWb6UaPZrtTB2ekhB/ew== +"@babel/plugin-proposal-async-generator-functions@^7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.2.0.tgz#b289b306669dce4ad20b0252889a15768c9d417e" + integrity sha512-+Dfo/SCQqrwx48ptLVGLdE39YtWRuKc/Y9I5Fy0P1DDBB9lsAHpjcEJQt+4IifuSOSTLBKJObJqMvaO1pIE8LQ== dependencies: "@babel/helper-plugin-utils" "^7.0.0" "@babel/helper-remap-async-to-generator" "^7.1.0" - "@babel/plugin-syntax-async-generators" "^7.0.0" + "@babel/plugin-syntax-async-generators" "^7.2.0" -"@babel/plugin-proposal-json-strings@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.0.0.tgz#3b4d7b5cf51e1f2e70f52351d28d44fc2970d01e" - integrity sha512-kfVdUkIAGJIVmHmtS/40i/fg/AGnw/rsZBCaapY5yjeO5RA9m165Xbw9KMOu2nqXP5dTFjEjHdfNdoVcHv133Q== +"@babel/plugin-proposal-json-strings@^7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.2.0.tgz#568ecc446c6148ae6b267f02551130891e29f317" + integrity sha512-MAFV1CA/YVmYwZG0fBQyXhmj0BHCB5egZHCKWIFVv/XCxAeVGIHfos3SwDck4LvCllENIAg7xMKOG5kH0dzyUg== dependencies: "@babel/helper-plugin-utils" "^7.0.0" - "@babel/plugin-syntax-json-strings" "^7.0.0" + "@babel/plugin-syntax-json-strings" "^7.2.0" -"@babel/plugin-proposal-object-rest-spread@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.0.0.tgz#9a17b547f64d0676b6c9cecd4edf74a82ab85e7e" - integrity sha512-14fhfoPcNu7itSen7Py1iGN0gEm87hX/B+8nZPqkdmANyyYWYMY2pjA3r8WXbWVKMzfnSNS0xY8GVS0IjXi/iw== +"@babel/plugin-proposal-object-rest-spread@^7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.2.0.tgz#88f5fec3e7ad019014c97f7ee3c992f0adbf7fb8" + integrity sha512-1L5mWLSvR76XYUQJXkd/EEQgjq8HHRP6lQuZTTg0VA4tTGPpGemmCdAfQIz1rzEuWAm+ecP8PyyEm30jC1eQCg== dependencies: "@babel/helper-plugin-utils" "^7.0.0" - "@babel/plugin-syntax-object-rest-spread" "^7.0.0" + "@babel/plugin-syntax-object-rest-spread" "^7.2.0" -"@babel/plugin-proposal-optional-catch-binding@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.0.0.tgz#b610d928fe551ff7117d42c8bb410eec312a6425" - integrity sha512-JPqAvLG1s13B/AuoBjdBYvn38RqW6n1TzrQO839/sIpqLpbnXKacsAgpZHzLD83Sm8SDXMkkrAvEnJ25+0yIpw== +"@babel/plugin-proposal-optional-catch-binding@^7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.2.0.tgz#135d81edb68a081e55e56ec48541ece8065c38f5" + integrity sha512-mgYj3jCcxug6KUcX4OBoOJz3CMrwRfQELPQ5560F70YQUBZB7uac9fqaWamKR1iWUzGiK2t0ygzjTScZnVz75g== dependencies: "@babel/helper-plugin-utils" "^7.0.0" - "@babel/plugin-syntax-optional-catch-binding" "^7.0.0" + "@babel/plugin-syntax-optional-catch-binding" "^7.2.0" -"@babel/plugin-proposal-unicode-property-regex@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.0.0.tgz#498b39cd72536cd7c4b26177d030226eba08cd33" - integrity sha512-tM3icA6GhC3ch2SkmSxv7J/hCWKISzwycub6eGsDrFDgukD4dZ/I+x81XgW0YslS6mzNuQ1Cbzh5osjIMgepPQ== +"@babel/plugin-proposal-unicode-property-regex@^7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.2.0.tgz#abe7281fe46c95ddc143a65e5358647792039520" + integrity sha512-LvRVYb7kikuOtIoUeWTkOxQEV1kYvL5B6U3iWEGCzPNRus1MzJweFqORTj+0jkxozkTSYNJozPOddxmqdqsRpw== dependencies: "@babel/helper-plugin-utils" "^7.0.0" "@babel/helper-regex" "^7.0.0" regexpu-core "^4.2.0" -"@babel/plugin-syntax-async-generators@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.0.0.tgz#bf0891dcdbf59558359d0c626fdc9490e20bc13c" - integrity sha512-im7ged00ddGKAjcZgewXmp1vxSZQQywuQXe2B1A7kajjZmDeY/ekMPmWr9zJgveSaQH0k7BcGrojQhcK06l0zA== +"@babel/plugin-syntax-async-generators@^7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.2.0.tgz#69e1f0db34c6f5a0cf7e2b3323bf159a76c8cb7f" + integrity sha512-1ZrIRBv2t0GSlcwVoQ6VgSLpLgiN/FVQUzt9znxo7v2Ov4jJrs8RY8tv0wvDmFN3qIdMKWrmMMW6yZ0G19MfGg== dependencies: "@babel/helper-plugin-utils" "^7.0.0" -"@babel/plugin-syntax-json-strings@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.0.0.tgz#0d259a68090e15b383ce3710e01d5b23f3770cbd" - integrity sha512-UlSfNydC+XLj4bw7ijpldc1uZ/HB84vw+U6BTuqMdIEmz/LDe63w/GHtpQMdXWdqQZFeAI9PjnHe/vDhwirhKA== +"@babel/plugin-syntax-json-strings@^7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.2.0.tgz#72bd13f6ffe1d25938129d2a186b11fd62951470" + integrity sha512-5UGYnMSLRE1dqqZwug+1LISpA403HzlSfsg6P9VXU6TBjcSHeNlw4DxDx7LgpF+iKZoOG/+uzqoRHTdcUpiZNg== dependencies: "@babel/helper-plugin-utils" "^7.0.0" -"@babel/plugin-syntax-object-rest-spread@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.0.0.tgz#37d8fbcaf216bd658ea1aebbeb8b75e88ebc549b" - integrity sha512-5A0n4p6bIiVe5OvQPxBnesezsgFJdHhSs3uFSvaPdMqtsovajLZ+G2vZyvNe10EzJBWWo3AcHGKhAFUxqwp2dw== +"@babel/plugin-syntax-object-rest-spread@^7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.2.0.tgz#3b7a3e733510c57e820b9142a6579ac8b0dfad2e" + integrity sha512-t0JKGgqk2We+9may3t0xDdmneaXmyxq0xieYcKHxIsrJO64n1OiMWNUtc5gQK1PA0NpdCRrtZp4z+IUaKugrSA== dependencies: "@babel/helper-plugin-utils" "^7.0.0" -"@babel/plugin-syntax-optional-catch-binding@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.0.0.tgz#886f72008b3a8b185977f7cb70713b45e51ee475" - integrity sha512-Wc+HVvwjcq5qBg1w5RG9o9RVzmCaAg/Vp0erHCKpAYV8La6I94o4GQAmFYNmkzoMO6gzoOSulpKeSSz6mPEoZw== +"@babel/plugin-syntax-optional-catch-binding@^7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.2.0.tgz#a94013d6eda8908dfe6a477e7f9eda85656ecf5c" + integrity sha512-bDe4xKNhb0LI7IvZHiA13kff0KEfaGX/Hv4lMA9+7TEc63hMNvfKo6ZFpXhKuEp+II/q35Gc4NoMeDZyaUbj9w== dependencies: "@babel/helper-plugin-utils" "^7.0.0" -"@babel/plugin-transform-arrow-functions@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.0.0.tgz#a6c14875848c68a3b4b3163a486535ef25c7e749" - integrity sha512-2EZDBl1WIO/q4DIkIp4s86sdp4ZifL51MoIviLY/gG/mLSuOIEg7J8o6mhbxOTvUJkaN50n+8u41FVsr5KLy/w== +"@babel/plugin-transform-arrow-functions@^7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.2.0.tgz#9aeafbe4d6ffc6563bf8f8372091628f00779550" + integrity sha512-ER77Cax1+8/8jCB9fo4Ud161OZzWN5qawi4GusDuRLcDbDG+bIGYY20zb2dfAFdTRGzrfq2xZPvF0R64EHnimg== dependencies: "@babel/helper-plugin-utils" "^7.0.0" -"@babel/plugin-transform-async-to-generator@^7.1.0": - version "7.1.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.1.0.tgz#109e036496c51dd65857e16acab3bafdf3c57811" - integrity sha512-rNmcmoQ78IrvNCIt/R9U+cixUHeYAzgusTFgIAv+wQb9HJU4szhpDD6e5GCACmj/JP5KxuCwM96bX3L9v4ZN/g== +"@babel/plugin-transform-async-to-generator@^7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.2.0.tgz#68b8a438663e88519e65b776f8938f3445b1a2ff" + integrity sha512-CEHzg4g5UraReozI9D4fblBYABs7IM6UerAVG7EJVrTLC5keh00aEuLUT+O40+mJCEzaXkYfTCUKIyeDfMOFFQ== dependencies: "@babel/helper-module-imports" "^7.0.0" "@babel/helper-plugin-utils" "^7.0.0" "@babel/helper-remap-async-to-generator" "^7.1.0" -"@babel/plugin-transform-block-scoped-functions@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.0.0.tgz#482b3f75103927e37288b3b67b65f848e2aa0d07" - integrity sha512-AOBiyUp7vYTqz2Jibe1UaAWL0Hl9JUXEgjFvvvcSc9MVDItv46ViXFw2F7SVt1B5k+KWjl44eeXOAk3UDEaJjQ== +"@babel/plugin-transform-block-scoped-functions@^7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.2.0.tgz#5d3cc11e8d5ddd752aa64c9148d0db6cb79fd190" + integrity sha512-ntQPR6q1/NKuphly49+QiQiTN0O63uOwjdD6dhIjSWBI5xlrbUFh720TIpzBhpnrLfv2tNH/BXvLIab1+BAI0w== dependencies: "@babel/helper-plugin-utils" "^7.0.0" -"@babel/plugin-transform-block-scoping@^7.1.5": - version "7.1.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.1.5.tgz#3e8e0bc9a5104519923302a24f748f72f2f61f37" - integrity sha512-jlYcDrz+5ayWC7mxgpn1Wj8zj0mmjCT2w0mPIMSwO926eXBRxpEgoN/uQVRBfjtr8ayjcmS+xk2G1jaP8JjMJQ== +"@babel/plugin-transform-block-scoping@^7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.2.0.tgz#f17c49d91eedbcdf5dd50597d16f5f2f770132d4" + integrity sha512-vDTgf19ZEV6mx35yiPJe4fS02mPQUUcBNwWQSZFXSzTSbsJFQvHt7DqyS3LK8oOWALFOsJ+8bbqBgkirZteD5Q== dependencies: "@babel/helper-plugin-utils" "^7.0.0" lodash "^4.17.10" -"@babel/plugin-transform-classes@^7.1.0": - version "7.1.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.1.0.tgz#ab3f8a564361800cbc8ab1ca6f21108038432249" - integrity sha512-rNaqoD+4OCBZjM7VaskladgqnZ1LO6o2UxuWSDzljzW21pN1KXkB7BstAVweZdxQkHAujps5QMNOTWesBciKFg== +"@babel/plugin-transform-classes@^7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.2.0.tgz#374f8876075d7d21fea55aeb5c53561259163f96" + integrity sha512-aPCEkrhJYebDXcGTAP+cdUENkH7zqOlgbKwLbghjjHpJRJBWM/FSlCjMoPGA8oUdiMfOrk3+8EFPLLb5r7zj2w== dependencies: "@babel/helper-annotate-as-pure" "^7.0.0" "@babel/helper-define-map" "^7.1.0" @@ -333,63 +333,63 @@ "@babel/helper-split-export-declaration" "^7.0.0" globals "^11.1.0" -"@babel/plugin-transform-computed-properties@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.0.0.tgz#2fbb8900cd3e8258f2a2ede909b90e7556185e31" - integrity sha512-ubouZdChNAv4AAWAgU7QKbB93NU5sHwInEWfp+/OzJKA02E6Woh9RVoX4sZrbRwtybky/d7baTUqwFx+HgbvMA== +"@babel/plugin-transform-computed-properties@^7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.2.0.tgz#83a7df6a658865b1c8f641d510c6f3af220216da" + integrity sha512-kP/drqTxY6Xt3NNpKiMomfgkNn4o7+vKxK2DDKcBG9sHj51vHqMBGy8wbDS/J4lMxnqs153/T3+DmCEAkC5cpA== dependencies: "@babel/helper-plugin-utils" "^7.0.0" -"@babel/plugin-transform-destructuring@^7.0.0": - version "7.1.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.1.3.tgz#e69ff50ca01fac6cb72863c544e516c2b193012f" - integrity sha512-Mb9M4DGIOspH1ExHOUnn2UUXFOyVTiX84fXCd+6B5iWrQg/QMeeRmSwpZ9lnjYLSXtZwiw80ytVMr3zue0ucYw== +"@babel/plugin-transform-destructuring@^7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.2.0.tgz#e75269b4b7889ec3a332cd0d0c8cff8fed0dc6f3" + integrity sha512-coVO2Ayv7g0qdDbrNiadE4bU7lvCd9H539m2gMknyVjjMdwF/iCOM7R+E8PkntoqLkltO0rk+3axhpp/0v68VQ== dependencies: "@babel/helper-plugin-utils" "^7.0.0" -"@babel/plugin-transform-dotall-regex@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.0.0.tgz#73a24da69bc3c370251f43a3d048198546115e58" - integrity sha512-00THs8eJxOJUFVx1w8i1MBF4XH4PsAjKjQ1eqN/uCH3YKwP21GCKfrn6YZFZswbOk9+0cw1zGQPHVc1KBlSxig== +"@babel/plugin-transform-dotall-regex@^7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.2.0.tgz#f0aabb93d120a8ac61e925ea0ba440812dbe0e49" + integrity sha512-sKxnyHfizweTgKZf7XsXu/CNupKhzijptfTM+bozonIuyVrLWVUvYjE2bhuSBML8VQeMxq4Mm63Q9qvcvUcciQ== dependencies: "@babel/helper-plugin-utils" "^7.0.0" "@babel/helper-regex" "^7.0.0" regexpu-core "^4.1.3" -"@babel/plugin-transform-duplicate-keys@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.0.0.tgz#a0601e580991e7cace080e4cf919cfd58da74e86" - integrity sha512-w2vfPkMqRkdxx+C71ATLJG30PpwtTpW7DDdLqYt2acXU7YjztzeWW2Jk1T6hKqCLYCcEA5UQM/+xTAm+QCSnuQ== +"@babel/plugin-transform-duplicate-keys@^7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.2.0.tgz#d952c4930f312a4dbfff18f0b2914e60c35530b3" + integrity sha512-q+yuxW4DsTjNceUiTzK0L+AfQ0zD9rWaTLiUqHA8p0gxx7lu1EylenfzjeIWNkPy6e/0VG/Wjw9uf9LueQwLOw== dependencies: "@babel/helper-plugin-utils" "^7.0.0" -"@babel/plugin-transform-exponentiation-operator@^7.1.0": - version "7.1.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.1.0.tgz#9c34c2ee7fd77e02779cfa37e403a2e1003ccc73" - integrity sha512-uZt9kD1Pp/JubkukOGQml9tqAeI8NkE98oZnHZ2qHRElmeKCodbTZgOEUtujSCSLhHSBWbzNiFSDIMC4/RBTLQ== +"@babel/plugin-transform-exponentiation-operator@^7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.2.0.tgz#a63868289e5b4007f7054d46491af51435766008" + integrity sha512-umh4hR6N7mu4Elq9GG8TOu9M0bakvlsREEC+ialrQN6ABS4oDQ69qJv1VtR3uxlKMCQMCvzk7vr17RHKcjx68A== dependencies: "@babel/helper-builder-binary-assignment-operator-visitor" "^7.1.0" "@babel/helper-plugin-utils" "^7.0.0" -"@babel/plugin-transform-for-of@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.0.0.tgz#f2ba4eadb83bd17dc3c7e9b30f4707365e1c3e39" - integrity sha512-TlxKecN20X2tt2UEr2LNE6aqA0oPeMT1Y3cgz8k4Dn1j5ObT8M3nl9aA37LLklx0PBZKETC9ZAf9n/6SujTuXA== +"@babel/plugin-transform-for-of@^7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.2.0.tgz#ab7468befa80f764bb03d3cb5eef8cc998e1cad9" + integrity sha512-Kz7Mt0SsV2tQk6jG5bBv5phVbkd0gd27SgYD4hH1aLMJRchM0dzHaXvrWhVZ+WxAlDoAKZ7Uy3jVTW2mKXQ1WQ== dependencies: "@babel/helper-plugin-utils" "^7.0.0" -"@babel/plugin-transform-function-name@^7.1.0": - version "7.1.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.1.0.tgz#29c5550d5c46208e7f730516d41eeddd4affadbb" - integrity sha512-VxOa1TMlFMtqPW2IDYZQaHsFrq/dDoIjgN098NowhexhZcz3UGlvPgZXuE1jEvNygyWyxRacqDpCZt+par1FNg== +"@babel/plugin-transform-function-name@^7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.2.0.tgz#f7930362829ff99a3174c39f0afcc024ef59731a" + integrity sha512-kWgksow9lHdvBC2Z4mxTsvc7YdY7w/V6B2vy9cTIPtLEE9NhwoWivaxdNM/S37elu5bqlLP/qOY906LukO9lkQ== dependencies: "@babel/helper-function-name" "^7.1.0" "@babel/helper-plugin-utils" "^7.0.0" -"@babel/plugin-transform-literals@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.0.0.tgz#2aec1d29cdd24c407359c930cdd89e914ee8ff86" - integrity sha512-1NTDBWkeNXgpUcyoVFxbr9hS57EpZYXpje92zv0SUzjdu3enaRwF/l3cmyRnXLtIdyJASyiS6PtybK+CgKf7jA== +"@babel/plugin-transform-literals@^7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.2.0.tgz#690353e81f9267dad4fd8cfd77eafa86aba53ea1" + integrity sha512-2ThDhm4lI4oV7fVQ6pNNK+sx+c/GM5/SaML0w/r4ZB7sAneD/piDJtwdKlNckXeyGK7wlwg2E2w33C/Hh+VFCg== dependencies: "@babel/helper-plugin-utils" "^7.0.0" @@ -401,27 +401,27 @@ "@babel/helper-module-transforms" "^7.1.0" "@babel/helper-plugin-utils" "^7.0.0" -"@babel/plugin-transform-modules-commonjs@^7.1.0": - version "7.1.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.1.0.tgz#0a9d86451cbbfb29bd15186306897c67f6f9a05c" - integrity sha512-wtNwtMjn1XGwM0AXPspQgvmE6msSJP15CX2RVfpTSTNPLhKhaOjaIfBaVfj4iUZ/VrFSodcFedwtPg/NxwQlPA== +"@babel/plugin-transform-modules-commonjs@^7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.2.0.tgz#c4f1933f5991d5145e9cfad1dfd848ea1727f404" + integrity sha512-V6y0uaUQrQPXUrmj+hgnks8va2L0zcZymeU7TtWEgdRLNkceafKXEduv7QzgQAE4lT+suwooG9dC7LFhdRAbVQ== dependencies: "@babel/helper-module-transforms" "^7.1.0" "@babel/helper-plugin-utils" "^7.0.0" "@babel/helper-simple-access" "^7.1.0" -"@babel/plugin-transform-modules-systemjs@^7.0.0": - version "7.1.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.1.3.tgz#2119a3e3db612fd74a19d88652efbfe9613a5db0" - integrity sha512-PvTxgjxQAq4pvVUZF3mD5gEtVDuId8NtWkJsZLEJZMZAW3TvgQl1pmydLLN1bM8huHFVVU43lf0uvjQj9FRkKw== +"@babel/plugin-transform-modules-systemjs@^7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.2.0.tgz#912bfe9e5ff982924c81d0937c92d24994bb9068" + integrity sha512-aYJwpAhoK9a+1+O625WIjvMY11wkB/ok0WClVwmeo3mCjcNRjt+/8gHWrB5i+00mUju0gWsBkQnPpdvQ7PImmQ== dependencies: "@babel/helper-hoist-variables" "^7.0.0" "@babel/helper-plugin-utils" "^7.0.0" -"@babel/plugin-transform-modules-umd@^7.1.0": - version "7.1.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.1.0.tgz#a29a7d85d6f28c3561c33964442257cc6a21f2a8" - integrity sha512-enrRtn5TfRhMmbRwm7F8qOj0qEYByqUvTttPEGimcBH4CJHphjyK1Vg7sdU7JjeEmgSpM890IT/efS2nMHwYig== +"@babel/plugin-transform-modules-umd@^7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.2.0.tgz#7678ce75169f0877b8eb2235538c074268dd01ae" + integrity sha512-BV3bw6MyUH1iIsGhXlOK6sXhmSarZjtJ/vMiD9dNmpY8QXFFQTj+6v92pcfy1iqa8DeAfJFwoxcrS/TUZda6sw== dependencies: "@babel/helper-module-transforms" "^7.1.0" "@babel/helper-plugin-utils" "^7.0.0" @@ -433,18 +433,18 @@ dependencies: "@babel/helper-plugin-utils" "^7.0.0" -"@babel/plugin-transform-object-super@^7.1.0": - version "7.1.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.1.0.tgz#b1ae194a054b826d8d4ba7ca91486d4ada0f91bb" - integrity sha512-/O02Je1CRTSk2SSJaq0xjwQ8hG4zhZGNjE8psTsSNPXyLRCODv7/PBozqT5AmQMzp7MI3ndvMhGdqp9c96tTEw== +"@babel/plugin-transform-object-super@^7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.2.0.tgz#b35d4c10f56bab5d650047dad0f1d8e8814b6598" + integrity sha512-VMyhPYZISFZAqAPVkiYb7dUe2AsVi2/wCT5+wZdsNO31FojQJa9ns40hzZ6U9f50Jlq4w6qwzdBB2uwqZ00ebg== dependencies: "@babel/helper-plugin-utils" "^7.0.0" "@babel/helper-replace-supers" "^7.1.0" -"@babel/plugin-transform-parameters@^7.1.0": - version "7.1.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.1.0.tgz#44f492f9d618c9124026e62301c296bf606a7aed" - integrity sha512-vHV7oxkEJ8IHxTfRr3hNGzV446GAb+0hgbA7o/0Jd76s+YzccdWuTU296FOCOl/xweU4t/Ya4g41yWz80RFCRw== +"@babel/plugin-transform-parameters@^7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.2.0.tgz#0d5ad15dc805e2ea866df4dd6682bfe76d1408c2" + integrity sha512-kB9+hhUidIgUoBQ0MsxMewhzr8i60nMa2KgeJKQWYrqQpqcBYtnpR+JgkadZVZoaEZ/eKu9mclFaVwhRpLNSzA== dependencies: "@babel/helper-call-delegate" "^7.1.0" "@babel/helper-get-function-arity" "^7.0.0" @@ -474,40 +474,40 @@ dependencies: "@babel/helper-plugin-utils" "^7.0.0" -"@babel/plugin-transform-spread@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.0.0.tgz#93583ce48dd8c85e53f3a46056c856e4af30b49b" - integrity sha512-L702YFy2EvirrR4shTj0g2xQp7aNwZoWNCkNu2mcoU0uyzMl0XRwDSwzB/xp6DSUFiBmEXuyAyEN16LsgVqGGQ== +"@babel/plugin-transform-spread@^7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.2.0.tgz#0c76c12a3b5826130078ee8ec84a7a8e4afd79c4" + integrity sha512-7TtPIdwjS/i5ZBlNiQePQCovDh9pAhVbp/nGVRBZuUdBiVRThyyLend3OHobc0G+RLCPPAN70+z/MAMhsgJd/A== dependencies: "@babel/helper-plugin-utils" "^7.0.0" -"@babel/plugin-transform-sticky-regex@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.0.0.tgz#30a9d64ac2ab46eec087b8530535becd90e73366" - integrity sha512-LFUToxiyS/WD+XEWpkx/XJBrUXKewSZpzX68s+yEOtIbdnsRjpryDw9U06gYc6klYEij/+KQVRnD3nz3AoKmjw== +"@babel/plugin-transform-sticky-regex@^7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.2.0.tgz#a1e454b5995560a9c1e0d537dfc15061fd2687e1" + integrity sha512-KKYCoGaRAf+ckH8gEL3JHUaFVyNHKe3ASNsZ+AlktgHevvxGigoIttrEJb8iKN03Q7Eazlv1s6cx2B2cQ3Jabw== dependencies: "@babel/helper-plugin-utils" "^7.0.0" "@babel/helper-regex" "^7.0.0" -"@babel/plugin-transform-template-literals@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.0.0.tgz#084f1952efe5b153ddae69eb8945f882c7a97c65" - integrity sha512-vA6rkTCabRZu7Nbl9DfLZE1imj4tzdWcg5vtdQGvj+OH9itNNB6hxuRMHuIY8SGnEt1T9g5foqs9LnrHzsqEFg== +"@babel/plugin-transform-template-literals@^7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.2.0.tgz#d87ed01b8eaac7a92473f608c97c089de2ba1e5b" + integrity sha512-FkPix00J9A/XWXv4VoKJBMeSkyY9x/TqIh76wzcdfl57RJJcf8CehQ08uwfhCDNtRQYtHQKBTwKZDEyjE13Lwg== dependencies: "@babel/helper-annotate-as-pure" "^7.0.0" "@babel/helper-plugin-utils" "^7.0.0" -"@babel/plugin-transform-typeof-symbol@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.0.0.tgz#4dcf1e52e943e5267b7313bff347fdbe0f81cec9" - integrity sha512-1r1X5DO78WnaAIvs5uC48t41LLckxsYklJrZjNKcevyz83sF2l4RHbw29qrCPr/6ksFsdfRpT/ZgxNWHXRnffg== +"@babel/plugin-transform-typeof-symbol@^7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.2.0.tgz#117d2bcec2fbf64b4b59d1f9819894682d29f2b2" + integrity sha512-2LNhETWYxiYysBtrBTqL8+La0jIoQQnIScUJc74OYvUGRmkskNY4EzLCnjHBzdmb38wqtTaixpo1NctEcvMDZw== dependencies: "@babel/helper-plugin-utils" "^7.0.0" -"@babel/plugin-transform-unicode-regex@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.0.0.tgz#c6780e5b1863a76fe792d90eded9fcd5b51d68fc" - integrity sha512-uJBrJhBOEa3D033P95nPHu3nbFwFE9ZgXsfEitzoIXIwqAZWk7uXcg06yFKXz9FSxBH5ucgU/cYdX0IV8ldHKw== +"@babel/plugin-transform-unicode-regex@^7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.2.0.tgz#4eb8db16f972f8abb5062c161b8b115546ade08b" + integrity sha512-m48Y0lMhrbXEJnVUaYly29jRXbQ3ksxPrS1Tg8t+MHqzXhtBYAvI51euOBaoAlZLPHsieY9XPVMf80a5x0cPcA== dependencies: "@babel/helper-plugin-utils" "^7.0.0" "@babel/helper-regex" "^7.0.0" @@ -522,48 +522,48 @@ regenerator-runtime "^0.11.1" "@babel/preset-env@^7.0.0": - version "7.1.6" - resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.1.6.tgz#a0bf4b96b6bfcf6e000afc5b72b4abe7cc13ae97" - integrity sha512-YIBfpJNQMBkb6MCkjz/A9J76SNCSuGVamOVBgoUkLzpJD/z8ghHi9I42LQ4pulVX68N/MmImz6ZTixt7Azgexw== + version "7.2.0" + resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.2.0.tgz#a5030e7e4306af5a295dd5d7c78dc5464af3fee2" + integrity sha512-haGR38j5vOGVeBatrQPr3l0xHbs14505DcM57cbJy48kgMFvvHHoYEhHuRV+7vi559yyAUAVbTWzbK/B/pzJng== dependencies: "@babel/helper-module-imports" "^7.0.0" "@babel/helper-plugin-utils" "^7.0.0" - "@babel/plugin-proposal-async-generator-functions" "^7.1.0" - "@babel/plugin-proposal-json-strings" "^7.0.0" - "@babel/plugin-proposal-object-rest-spread" "^7.0.0" - "@babel/plugin-proposal-optional-catch-binding" "^7.0.0" - "@babel/plugin-proposal-unicode-property-regex" "^7.0.0" - "@babel/plugin-syntax-async-generators" "^7.0.0" - "@babel/plugin-syntax-object-rest-spread" "^7.0.0" - "@babel/plugin-syntax-optional-catch-binding" "^7.0.0" - "@babel/plugin-transform-arrow-functions" "^7.0.0" - "@babel/plugin-transform-async-to-generator" "^7.1.0" - "@babel/plugin-transform-block-scoped-functions" "^7.0.0" - "@babel/plugin-transform-block-scoping" "^7.1.5" - "@babel/plugin-transform-classes" "^7.1.0" - "@babel/plugin-transform-computed-properties" "^7.0.0" - "@babel/plugin-transform-destructuring" "^7.0.0" - "@babel/plugin-transform-dotall-regex" "^7.0.0" - "@babel/plugin-transform-duplicate-keys" "^7.0.0" - "@babel/plugin-transform-exponentiation-operator" "^7.1.0" - "@babel/plugin-transform-for-of" "^7.0.0" - "@babel/plugin-transform-function-name" "^7.1.0" - "@babel/plugin-transform-literals" "^7.0.0" - "@babel/plugin-transform-modules-amd" "^7.1.0" - "@babel/plugin-transform-modules-commonjs" "^7.1.0" - "@babel/plugin-transform-modules-systemjs" "^7.0.0" - "@babel/plugin-transform-modules-umd" "^7.1.0" + "@babel/plugin-proposal-async-generator-functions" "^7.2.0" + "@babel/plugin-proposal-json-strings" "^7.2.0" + "@babel/plugin-proposal-object-rest-spread" "^7.2.0" + "@babel/plugin-proposal-optional-catch-binding" "^7.2.0" + "@babel/plugin-proposal-unicode-property-regex" "^7.2.0" + "@babel/plugin-syntax-async-generators" "^7.2.0" + "@babel/plugin-syntax-object-rest-spread" "^7.2.0" + "@babel/plugin-syntax-optional-catch-binding" "^7.2.0" + "@babel/plugin-transform-arrow-functions" "^7.2.0" + "@babel/plugin-transform-async-to-generator" "^7.2.0" + "@babel/plugin-transform-block-scoped-functions" "^7.2.0" + "@babel/plugin-transform-block-scoping" "^7.2.0" + "@babel/plugin-transform-classes" "^7.2.0" + "@babel/plugin-transform-computed-properties" "^7.2.0" + "@babel/plugin-transform-destructuring" "^7.2.0" + "@babel/plugin-transform-dotall-regex" "^7.2.0" + "@babel/plugin-transform-duplicate-keys" "^7.2.0" + "@babel/plugin-transform-exponentiation-operator" "^7.2.0" + "@babel/plugin-transform-for-of" "^7.2.0" + "@babel/plugin-transform-function-name" "^7.2.0" + "@babel/plugin-transform-literals" "^7.2.0" + "@babel/plugin-transform-modules-amd" "^7.2.0" + "@babel/plugin-transform-modules-commonjs" "^7.2.0" + "@babel/plugin-transform-modules-systemjs" "^7.2.0" + "@babel/plugin-transform-modules-umd" "^7.2.0" "@babel/plugin-transform-new-target" "^7.0.0" - "@babel/plugin-transform-object-super" "^7.1.0" - "@babel/plugin-transform-parameters" "^7.1.0" + "@babel/plugin-transform-object-super" "^7.2.0" + "@babel/plugin-transform-parameters" "^7.2.0" "@babel/plugin-transform-regenerator" "^7.0.0" - "@babel/plugin-transform-shorthand-properties" "^7.0.0" - "@babel/plugin-transform-spread" "^7.0.0" - "@babel/plugin-transform-sticky-regex" "^7.0.0" - "@babel/plugin-transform-template-literals" "^7.0.0" - "@babel/plugin-transform-typeof-symbol" "^7.0.0" - "@babel/plugin-transform-unicode-regex" "^7.0.0" - browserslist "^4.1.0" + "@babel/plugin-transform-shorthand-properties" "^7.2.0" + "@babel/plugin-transform-spread" "^7.2.0" + "@babel/plugin-transform-sticky-regex" "^7.2.0" + "@babel/plugin-transform-template-literals" "^7.2.0" + "@babel/plugin-transform-typeof-symbol" "^7.2.0" + "@babel/plugin-transform-unicode-regex" "^7.2.0" + browserslist "^4.3.4" invariant "^2.2.2" js-levenshtein "^1.1.3" semver "^5.3.0" @@ -599,10 +599,10 @@ globals "^11.1.0" lodash "^4.17.10" -"@babel/types@^7.0.0", "@babel/types@^7.1.2", "@babel/types@^7.1.5", "@babel/types@^7.1.6": - version "7.1.6" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.1.6.tgz#0adb330c3a281348a190263aceb540e10f04bcce" - integrity sha512-DMiUzlY9DSjVsOylJssxLHSgj6tWM9PRFJOGW/RaOglVOK9nzTxoOMfTfRQXGUCUQ/HmlG2efwC+XqUEJ5ay4w== +"@babel/types@^7.0.0", "@babel/types@^7.1.2", "@babel/types@^7.1.6", "@babel/types@^7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.2.0.tgz#7941c5b2d8060e06f9601d6be7c223eef906d5d8" + integrity sha512-b4v7dyfApuKDvmPb+O488UlGuR1WbwMXFsO/cyqMrnfvRAChZKJAYeeglWTjUO1b9UghKKgepAQM5tsvBJca6A== dependencies: esutils "^2.0.2" lodash "^4.17.10" @@ -1795,9 +1795,9 @@ broccoli-amd-funnel@^2.0.1: symlink-or-copy "^1.2.0" broccoli-babel-transpiler@^6.5.0: - version "6.5.0" - resolved "https://registry.yarnpkg.com/broccoli-babel-transpiler/-/broccoli-babel-transpiler-6.5.0.tgz#aa501a227b298a99742fdd0309b1eaad7124bba0" - integrity sha512-c5OLGY40Sdmv6rP230Jt8yoK49BHfOw1LXiDMu9EC9k2U6sqlpNRK78SzvByQ8IzKtBYUfeWCxeZHcvW+gH7VQ== + version "6.5.1" + resolved "https://registry.yarnpkg.com/broccoli-babel-transpiler/-/broccoli-babel-transpiler-6.5.1.tgz#a4afc8d3b59b441518eb9a07bd44149476e30738" + integrity sha512-w6GcnkxvHcNCte5FcLGEG1hUdQvlfvSN/6PtGWU/otg69Ugk8rUk51h41R0Ugoc+TNxyeFG1opRt2RlA87XzNw== dependencies: babel-core "^6.26.0" broccoli-funnel "^2.0.1" @@ -2148,14 +2148,14 @@ browserslist@^3.2.6: caniuse-lite "^1.0.30000844" electron-to-chromium "^1.3.47" -browserslist@^4.1.0: - version "4.3.4" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.3.4.tgz#4477b737db6a1b07077275b24791e680d4300425" - integrity sha512-u5iz+ijIMUlmV8blX82VGFrB9ecnUg5qEt55CMZ/YJEhha+d8qpBfOFuutJ6F/VKRXjZoD33b6uvarpPxcl3RA== +browserslist@^4.3.4: + version "4.3.5" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.3.5.tgz#1a917678acc07b55606748ea1adf9846ea8920f7" + integrity sha512-z9ZhGc3d9e/sJ9dIx5NFXkKoaiQTnrvrMsN3R1fGb1tkWWNSz12UewJn9TNxGo1l7J23h0MRaPmk7jfeTZYs1w== dependencies: - caniuse-lite "^1.0.30000899" - electron-to-chromium "^1.3.82" - node-releases "^1.0.1" + caniuse-lite "^1.0.30000912" + electron-to-chromium "^1.3.86" + node-releases "^1.0.5" bser@^2.0.0: version "2.0.0" @@ -2299,10 +2299,10 @@ can-symlink@^1.0.0: dependencies: tmp "0.0.28" -caniuse-lite@^1.0.30000844, caniuse-lite@^1.0.30000899: - version "1.0.30000912" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000912.tgz#08e650d4090a9c0ab06bfd2b46b7d3ad6dcaea28" - integrity sha512-M3zAtV36U+xw5mMROlTXpAHClmPAor6GPKAMD5Yi7glCB5sbMPFtnQ3rGpk4XqPdUrrTIaVYSJZxREZWNy8QJg== +caniuse-lite@^1.0.30000844, caniuse-lite@^1.0.30000912: + version "1.0.30000916" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000916.tgz#3428d3f529f0a7b2bfaaec65e796037bdd433aab" + integrity sha512-D6J9jloPm2MPkg0PXcODLMQAJKkeixKO9xhqTUMvtd44MtTYMyyDXPQ2Lk9IgBq5FH0frwiPa/N/w8ncQf7kIQ== capture-exit@^1.2.0: version "1.2.0" @@ -2705,9 +2705,9 @@ copy-descriptor@^0.1.0: integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40= core-js@^2.4.0, core-js@^2.5.0, core-js@^2.5.7: - version "2.5.7" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.7.tgz#f972608ff0cead68b841a16a932d0b183791814e" - integrity sha512-RszJCAxg/PP6uzXVXL6BsxSXx/B05oJAQ2vkJRjyjrEcNVycaqOmNb5OTxZPE3xa5gwZduqza6L9JOCenh/Ecw== + version "2.6.0" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.0.tgz#1e30793e9ee5782b307e37ffa22da0eacddd84d4" + integrity sha512-kLRC6ncVpuEW/1kwrOXYX6KQASCVtrh1gQr/UiaVgFlf9WE5Vp+lNe5+h3LuMr5PAucWnnEXwH0nQHRH/gpGtw== core-object@^3.1.5: version "3.1.5" @@ -2969,10 +2969,10 @@ ee-first@1.1.1: resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= -electron-to-chromium@^1.3.47, electron-to-chromium@^1.3.82: - version "1.3.85" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.85.tgz#5c46f790aa96445cabc57eb9d17346b1e46476fe" - integrity sha512-kWSDVVF9t3mft2OHVZy4K85X2beP6c6mFm3teFS/mLSDJpQwuFIWHrULCX+w6H1E55ZYmFRlT+ATAFRwhrYzsw== +electron-to-chromium@^1.3.47, electron-to-chromium@^1.3.86: + version "1.3.88" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.88.tgz#f36ab32634f49ef2b0fdc1e82e2d1cc17feb29e7" + integrity sha512-UPV4NuQMKeUh1S0OWRvwg0PI8ASHN9kBC8yDTk1ROXLC85W5GnhTRu/MZu3Teqx3JjlQYuckuHYXSUSgtb3J+A== ember-assign-polyfill@^2.6.0: version "2.6.0" @@ -3434,9 +3434,9 @@ engine.io-parser@~2.1.0, engine.io-parser@~2.1.1: has-binary2 "~1.0.2" engine.io@~3.3.1: - version "3.3.1" - resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-3.3.1.tgz#e076d9d2d6c075dda4623253b80fa045c81dd3a4" - integrity sha512-p0njqQo5QWVxJauKcnp5IO+LBeE5JD1tAf+UxPU8ASEUHSpsSSfYR+kVb8XGGH8AEDUa1Dk5jCvPQShNBL5BdQ== + version "3.3.2" + resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-3.3.2.tgz#18cbc8b6f36e9461c5c0f81df2b830de16058a59" + integrity sha512-AsaA9KG7cWPXWHp5FvHdDWY3AMWeZ8x+2pUVLcn71qE5AtAzgGbxuclOytygskw8XGmiQafTmnI9Bix3uihu2w== dependencies: accepts "~1.3.4" base64id "1.0.0" @@ -5367,7 +5367,7 @@ keyv@3.0.0: dependencies: json-buffer "3.0.0" -kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0: +kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.0.4, kind-of@^3.2.0: version "3.2.2" resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" integrity sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ= @@ -6218,6 +6218,11 @@ mocha@^6.0.1: yargs-parser "11.1.1" yargs-unparser "1.5.0" +moment@^2.22.2: + version "2.22.2" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.22.2.tgz#3c257f9839fc0e93ff53149632239eb90783ff66" + integrity sha1-PCV/mDn8DpP/UxSWMiOeuQeD/2Y= + morgan@^1.9.0: version "1.9.1" resolved "https://registry.yarnpkg.com/morgan/-/morgan-1.9.1.tgz#0a8d16734a1d9afbc824b99df87e738e58e2da59" @@ -6872,9 +6877,9 @@ pluralize@^7.0.0: integrity sha512-ARhBOdzS3e41FbkW/XWrTEtukqqLoK5+Z/4UeDaLuSW+39JPeFgs4gCGqsrJHVZX0fUrx//4OF0K1CUGwlIFow== portfinder@^1.0.15: - version "1.0.19" - resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.19.tgz#07e87914a55242dcda5b833d42f018d6875b595f" - integrity sha512-23aeQKW9KgHe6citUrG3r9HjeX6vls0h713TAa+CwTKZwNIr/pD2ApaxYF4Um3ZZyq4ar+Siv3+fhoHaIwSOSw== + version "1.0.20" + resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.20.tgz#bea68632e54b2e13ab7b0c4775e9b41bf270e44a" + integrity sha512-Yxe4mTyDzTd59PZJY4ojZR8F+E5e97iq2ZOHPz3HDgSvYC5siNad2tLooQ5y5QHyQhc3xVqvyk/eNA3wuoa7Sw== dependencies: async "^1.5.2" debug "^2.2.0" @@ -6923,9 +6928,9 @@ process-relative-require@^1.0.0: node-modules-path "^1.0.0" progress@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.1.tgz#c9242169342b1c29d275889c95734621b1952e31" - integrity sha512-OE+a6vzqazc+K6LxJrX5UPyKFvGnL5CYmq2jFGNIBWHpc4QyE49/YOumcrpQFJpfejmvRtbJzgO1zPmMCqlbBg== + version "2.0.3" + resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" + integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== promise-map-series@^0.2.1, promise-map-series@^0.2.3: version "0.2.3" @@ -7212,14 +7217,14 @@ regexpu-core@^2.0.0: regjsparser "^0.1.4" regexpu-core@^4.1.3, regexpu-core@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-4.2.0.tgz#a3744fa03806cffe146dea4421a3e73bdcc47b1d" - integrity sha512-Z835VSnJJ46CNBttalHD/dB+Sj2ezmY6Xp38npwU87peK6mqOzOpV8eYktdkLTEkzzD+JsTcxd84ozd8I14+rw== + version "4.4.0" + resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-4.4.0.tgz#8d43e0d1266883969720345e70c275ee0aec0d32" + integrity sha512-eDDWElbwwI3K0Lo6CqbQbA6FwgtCz4kYTarrri1okfkRLZAqstU+B3voZBCjg8Fl6iq0gXrJG6MvRgLthfvgOA== dependencies: regenerate "^1.4.0" regenerate-unicode-properties "^7.0.0" - regjsgen "^0.4.0" - regjsparser "^0.3.0" + regjsgen "^0.5.0" + regjsparser "^0.6.0" unicode-match-property-ecmascript "^1.0.4" unicode-match-property-value-ecmascript "^1.0.2" @@ -7243,10 +7248,10 @@ regjsgen@^0.2.0: resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.2.0.tgz#6c016adeac554f75823fe37ac05b92d5a4edb1f7" integrity sha1-bAFq3qxVT3WCP+N6wFuS1aTtsfc= -regjsgen@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.4.0.tgz#c1eb4c89a209263f8717c782591523913ede2561" - integrity sha512-X51Lte1gCYUdlwhF28+2YMO0U6WeN0GLpgpA7LK7mbdDnkQYiwvEpmpe0F/cv5L14EbxgrdayAG3JETBv0dbXA== +regjsgen@^0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.5.0.tgz#a7634dc08f89209c2049adda3525711fb97265dd" + integrity sha512-RnIrLhrXCX5ow/E5/Mh2O4e/oa1/jW0eaBKTSy3LaCj+M3Bqvm97GWDp2yUtzIs4LEn65zR2yiYGFqb2ApnzDA== regjsparser@^0.1.4: version "0.1.5" @@ -7255,10 +7260,10 @@ regjsparser@^0.1.4: dependencies: jsesc "~0.5.0" -regjsparser@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.3.0.tgz#3c326da7fcfd69fa0d332575a41c8c0cdf588c96" - integrity sha512-zza72oZBBHzt64G7DxdqrOo/30bhHkwMUoT0WqfGu98XLd7N+1tsy5MJ96Bk4MD0y74n629RhmrGW6XlnLLwCA== +regjsparser@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.6.0.tgz#f1e6ae8b7da2bae96c99399b868cd6c933a2ba9c" + integrity sha512-RQ7YyokLiQBomUJuUG8iGVvkgOLxwyZM8k6d3q5SAXpg4r5TZJZigKFvC6PpD+qQ98bCDC5YelPeA3EucDoNeQ== dependencies: jsesc "~0.5.0" @@ -7748,9 +7753,9 @@ sort-object-keys@^1.1.2: integrity sha1-06bEjcKsl+a8lDZ2luA/bQnTeVI= sort-package-json@^1.15.0: - version "1.16.0" - resolved "https://registry.yarnpkg.com/sort-package-json/-/sort-package-json-1.16.0.tgz#087f5ce05b6faca373312e7124918e0d68492d7b" - integrity sha512-QFJNxdpp7zZgSkmAIaMrteqqxGP4TkooKrGtslM2qYiML92PTYDOFOk+lG+TdvJzjheD502UFIys2qSvQljKaw== + version "1.17.0" + resolved "https://registry.yarnpkg.com/sort-package-json/-/sort-package-json-1.17.0.tgz#1dfe40b5632e28919a09e80f3da555090ec30cd7" + integrity sha512-pVEdEI3w5dlXVJ1N0W/SwSfdrUFWVeB/zcRruQVf999TlCAZ1fS+Aw+EPZpdoHjoALPx6jNNtU9batTrbeVlYg== dependencies: detect-indent "^5.0.0" sort-object-keys "^1.1.2" @@ -7850,9 +7855,9 @@ spawn-wrap@^1.4.2: which "^1.3.0" spdx-correct@^3.0.0: - version "3.0.2" - resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.0.2.tgz#19bb409e91b47b1ad54159243f7312a858db3c2e" - integrity sha512-q9hedtzyXHr5S0A1vEPoK/7l8NpfkFYTq6iCY+Pno2ZbdZR6WexZFtqeVGkGxW3TEJMN914Z55EnAGMmenlIQQ== + version "3.1.0" + resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.0.tgz#fb83e504445268f154b074e218c87c003cd31df4" + integrity sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q== dependencies: spdx-expression-parse "^3.0.0" spdx-license-ids "^3.0.0" @@ -8275,15 +8280,15 @@ tough-cookie@~2.4.3: punycode "^1.4.1" tree-sync@^1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/tree-sync/-/tree-sync-1.2.2.tgz#2cf76b8589f59ffedb58db5a3ac7cb013d0158b7" - integrity sha1-LPdrhYn1n/7bWNtaOsfLAT0BWLc= + version "1.4.0" + resolved "https://registry.yarnpkg.com/tree-sync/-/tree-sync-1.4.0.tgz#314598d13abaf752547d9335b8f95d9a137100d6" + integrity sha512-YvYllqh3qrR5TAYZZTXdspnIhlKAYezPYw11ntmweoceu4VK+keN356phHRIIo1d+RDmLpHZrUlmxga2gc9kSQ== dependencies: debug "^2.2.0" fs-tree-diff "^0.5.6" mkdirp "^0.5.1" quick-temp "^0.1.5" - walk-sync "^0.2.7" + walk-sync "^0.3.3" trim-newlines@^1.0.0: version "1.0.0" @@ -8537,18 +8542,18 @@ verror@1.10.0: core-util-is "1.0.2" extsprintf "^1.2.0" -walk-sync@^0.2.7: - version "0.2.7" - resolved "https://registry.yarnpkg.com/walk-sync/-/walk-sync-0.2.7.tgz#b49be4ee6867657aeb736978b56a29d10fa39969" - integrity sha1-tJvk7mhnZXrrc2l4tWop0Q+jmWk= +walk-sync@0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/walk-sync/-/walk-sync-0.3.2.tgz#4827280afc42d0e035367c4a4e31eeac0d136f75" + integrity sha512-FMB5VqpLqOCcqrzA9okZFc0wq0Qbmdm396qJxvQZhDpyu0W95G9JCmp74tx7iyYnyOcBtUuKJsgIKAqjozvmmQ== dependencies: ensure-posix-path "^1.0.0" matcher-collection "^1.0.0" walk-sync@^0.3.0, walk-sync@^0.3.1, walk-sync@^0.3.2, walk-sync@^0.3.3: - version "0.3.3" - resolved "https://registry.yarnpkg.com/walk-sync/-/walk-sync-0.3.3.tgz#1e9f12cd4fe6e0e6d4a0715b5cc7e30711d43cd1" - integrity sha512-jQgTHmCazUngGqvHZFlr30u2VLKEKErBMLFe+fBl5mn4rh9aI/QVRog8PT1hv2vaOu4EBwigfmpRTyZrbnpRVA== + version "0.3.4" + resolved "https://registry.yarnpkg.com/walk-sync/-/walk-sync-0.3.4.tgz#cf78486cc567d3a96b5b2237c6108017a5ffb9a4" + integrity sha512-ttGcuHA/OBnN2pcM6johpYlEms7XpO5/fyKIr48541xXedan4roO8cS1Q2S/zbbjGH/BarYDAMeS2Mi9HE5Tig== dependencies: ensure-posix-path "^1.0.0" matcher-collection "^1.0.0" From 6143f272bc25301e54300b8c2e39651fc44c8379 Mon Sep 17 00:00:00 2001 From: Chohee Kim Date: Wed, 12 Dec 2018 09:41:10 -0800 Subject: [PATCH 02/28] 1. Create EmberExamTestLoader that extends TestLoader. 2. Clean up code: - Remove return statement where no functionality depends on the return statement. - Create a dictionary mode object. - Use arrow function. 3. Extract function in patch-testem-output to be unit tested. 4. Wrap Qunit callback method in try-catch. 5. Add unit tests --- .travis.yml | 1 - .../-private/patch-test-loader.js | 84 +++++++------- .../-private/patch-testem-output.js | 39 ++++--- .../-private/weight-test-modules.js | 19 ++-- addon-test-support/load.js | 75 ++++++------- index.js | 4 +- lib/commands/exam.js | 105 ++++++++++++------ lib/commands/task/test-server.js | 1 + lib/utils/tests-options-validator.js | 32 +++++- node-tests/acceptance/exam-test.js | 6 +- node-tests/unit/commands/exam-test.js | 62 ++++++++++- package.json | 1 - tests/unit/mocha/test-loader-test.js | 66 ++++++----- tests/unit/mocha/testem-output-test.js | 25 +++++ tests/unit/mocha/weight-test-modules-test.js | 12 +- tests/unit/qunit/test-loader-test.js | 68 ++++++------ tests/unit/qunit/testem-output-test.js | 28 +++++ tests/unit/qunit/weight-test-modules-test.js | 13 +-- 18 files changed, 402 insertions(+), 239 deletions(-) create mode 100644 tests/unit/mocha/testem-output-test.js create mode 100644 tests/unit/qunit/testem-output-test.js diff --git a/.travis.yml b/.travis.yml index db1d9d5e3..97d9f7a00 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,3 @@ ---- language: node_js node_js: - "6" diff --git a/addon-test-support/-private/patch-test-loader.js b/addon-test-support/-private/patch-test-loader.js index 74c719a8f..c4370d6e4 100644 --- a/addon-test-support/-private/patch-test-loader.js +++ b/addon-test-support/-private/patch-test-loader.js @@ -2,29 +2,41 @@ import getUrlParams from './get-url-params'; import splitTestModules from './split-test-modules'; import weightTestModules from './weight-test-modules'; +import getTestLoader from './get-test-loader'; -export default function patchTestLoader(TestLoader) { - TestLoader._urlParams = getUrlParams(); +const TestLoader = getTestLoader(); - const _super = { - require: TestLoader.prototype.require, - unsee: TestLoader.prototype.unsee, - loadModules: TestLoader.prototype.loadModules, - }; +TestLoader.prototype.actualRequire = TestLoader.prototype.require; +TestLoader.prototype.actualUnsee = TestLoader.prototype.unsee; - // "Require" the module by adding it to the array of test modules to load - TestLoader.prototype.require = function _emberExamRequire(name) { - this._testModules.push(name); - }; +export default class EmberExamTestLoader extends TestLoader { - // Make unsee a no-op to avoid any unwanted resets - TestLoader.prototype.unsee = function _emberExamUnsee() {}; + constructor() { + super(); + this._testModules = []; + this._urlParams = getUrlParams(); + } + + get urlParams() { + return this._urlParams; + } - TestLoader.prototype.loadModules = function _emberExamLoadModules() { - const urlParams = TestLoader._urlParams; - const loadBalance = urlParams.loadBalance; - let partitions = urlParams.partition; - let split = parseInt(urlParams.split, 10); + static load() { + throw new Error('`EmberExamTestLoader` doesn\'t support `load()`.'); + } + + // EmberExamTestLoader requires and unsees modules after splitting or sorting a list of modules instead of + // requiring and unseeing as loading modules. + require(moduleName) { + this._testModules.push(moduleName); + } + + unsee() {} + + loadModules() { + const loadBalance = this._urlParams.loadBalance; + let partitions = this._urlParams.partition; + let split = parseInt(this._urlParams.split, 10); split = isNaN(split) ? 1 : split; @@ -34,34 +46,30 @@ export default function patchTestLoader(TestLoader) { partitions = [partitions]; } - const testLoader = this; - - this.modules = testLoader._testModules = []; - _super.loadModules.apply(testLoader, arguments); + super.loadModules(); if (loadBalance) { - this.modules = weightTestModules(this.modules); + this._testModules = weightTestModules(this._testModules); } - this.modules = splitTestModules(this.modules, split, partitions); + this._testModules = splitTestModules(this._testModules, split, partitions); if (loadBalance) { - Testem.emit('set-modules-queue', this.modules); + Testem.emit('testem:set-modules-queue', this._testModules); } else { - this.modules.forEach((moduleName) => { - _super.require.call(testLoader, moduleName); - _super.unsee.call(testLoader, moduleName); + this._testModules.forEach((moduleName) => { + this.actualRequire(moduleName); + this.actualUnsee(moduleName); }); } - }; - - TestLoader.prototype.loadIndividualModule = function _emberExamLoadIndividualModule(moduleName) { - const testLoader = this; - if (moduleName) { - _super.require.call(testLoader, moduleName); - _super.unsee.call(testLoader, moduleName); - return moduleName; + } + + // enables to load a module one at a time. + loadIndividualModule(moduleName) { + if (moduleName === undefined) { + throw new Error('Failed to load a test module. `moduleName` is undefined in `loadIndividualModule`.') } - return null; + this.actualRequire(moduleName); + this.actualUnsee(moduleName); } -} \ No newline at end of file +} diff --git a/addon-test-support/-private/patch-testem-output.js b/addon-test-support/-private/patch-testem-output.js index 9688d82cd..4e33c67af 100644 --- a/addon-test-support/-private/patch-testem-output.js +++ b/addon-test-support/-private/patch-testem-output.js @@ -1,21 +1,30 @@ /* globals Testem */ -// Add the partition number for better debugging when reading the reporter -export default function patchTestemOutput(TestLoader) { - Testem.on('test-result', function prependPartition(test) { - const urlParams = TestLoader._urlParams; - const split = urlParams.split; - const loadBalance = urlParams.loadBalance; +function updateTestName(urlParams, testName) { + const split = urlParams.split; + const loadBalance = urlParams.loadBalance; - const partition = urlParams.partition || 1; - const browser = urlParams.browser || 1; + const partition = urlParams.partition || 1; + const browser = urlParams.browser || 1; - if (split && loadBalance) { - test.name = `Exam Partition ${partition} - Browser Id ${browser} - ${test.name}` - } else if (split) { - test.name = `Exam Partition ${partition} - ${test.name}`; - } else if (loadBalance) { - test.name = `Browser Id ${browser} - ${test.name}`; - } + if (split && loadBalance) { + testName = `Exam Partition ${partition} - Browser Id ${browser} - ${testName}` + } else if (split) { + testName = `Exam Partition ${partition} - ${testName}`; + } else if (loadBalance) { + testName = `Browser Id ${browser} - ${testName}`; + } + + return testName; +} + +function patchTestemOutput(urlParams) { + Testem.on('test-result', (test) => { + test.name = updateTestName(urlParams, test.name); }); } + +export default { + updateTestName, + patchTestemOutput +}; diff --git a/addon-test-support/-private/weight-test-modules.js b/addon-test-support/-private/weight-test-modules.js index 0b48c645e..06147fe93 100644 --- a/addon-test-support/-private/weight-test-modules.js +++ b/addon-test-support/-private/weight-test-modules.js @@ -1,4 +1,5 @@ -const TEST_TYPE_WEIGHT = {jshint:1, eslint:1, unit:10, integration:20, acceptance:150}; +const TEST_TYPE_WEIGHT = {eslint:1, unit:10, integration:20, acceptance:150}; +const WEIGHT_REGEX = /\/(eslint|unit|integration|acceptance)\// const DEFAULT_WEIGHT = 50; /** @@ -11,14 +12,18 @@ const DEFAULT_WEIGHT = 50; * @param {*} modulePath File path to a module */ function getWeight(modulePath) { - const [, key] = /\/(jshint|unit|integration|acceptance)\//.exec(modulePath) || []; - return TEST_TYPE_WEIGHT[key] || DEFAULT_WEIGHT; + const [, key] = WEIGHT_REGEX.exec(modulePath) || []; + if (typeof TEST_TYPE_WEIGHT[key] === 'number') { + return TEST_TYPE_WEIGHT[key] ; + } else { + return DEFAULT_WEIGHT; + } } export default function weightTestModules(modules) { - const groups = {}; + const groups = Object.create(null); - modules.forEach(function(module) { + modules.forEach((module) => { const moduleWeight = getWeight(module); if (!groups[moduleWeight]) { @@ -29,8 +34,8 @@ export default function weightTestModules(modules) { // returns modules sorted by weight and alphabetically within its weighted groups return Object.keys(groups) - .sort(function(a, b){return b-a}) - .reduce(function(accumulatedArray, weight) { + .sort((a, b) => { return b-a }) + .reduce((accumulatedArray, weight) => { const sortedModuleArr = groups[weight].sort(); return accumulatedArray.concat(sortedModuleArr); }, []); diff --git a/addon-test-support/load.js b/addon-test-support/load.js index ac3cf3a1a..bb0283525 100644 --- a/addon-test-support/load.js +++ b/addon-test-support/load.js @@ -1,8 +1,7 @@ /* globals Testem */ -import patchTestLoader from './-private/patch-test-loader'; -import patchTestemOutput from './-private/patch-testem-output'; -import getTestLoader from './-private/get-test-loader'; +import TestemOutput from './-private/patch-testem-output'; +import EmberExamTestLoader from './-private/patch-test-loader'; let loaded = false; let testLoader; @@ -16,14 +15,11 @@ function loadEmberExam() { loaded = true; - const TestLoader = getTestLoader(); - patchTestLoader(TestLoader); + testLoader = new EmberExamTestLoader(); if (window.Testem) { - patchTestemOutput(TestLoader); + TestemOutput.patchTestemOutput(testLoader.urlParams); } - - testLoader = new TestLoader(); } // loadTests() is equivalent to ember-qunit's loadTest() except this does not create a new TestLoader instance @@ -40,55 +36,46 @@ function setupQUnitCallbacks(qunit) { if (!location.search.includes('loadBalance')) { return; } - qunit.begin(function() { - return new self.Promise(function(resolve) { - Testem.on('testem:next-module-response', function getTestModule(moduleName) { - if (moduleName !== undefined) { - loadIndividualModule(moduleName); - } - // if no tests were added, request the next module - if (qunit.config.queue.length === 0) { - Testem.emit('get-next-module'); - } else { - Testem.removeEventCallbacks('testem:next-module-response', getTestModule); - resolve(); - } - }); - Testem.emit('get-next-module'); - }); - }); - qunit.moduleDone(function() { - return new self.Promise(function(resolve) { - Testem.on('testem:next-module-response', function getTestModule(moduleName) { - if (moduleName !== undefined) { - loadIndividualModule(moduleName); - } + const nextModuleHandler = (resolve , reject) => { + const moduleComplete = () => { + Testem.removeEventCallbacks('testem:next-module-response', getTestModule); + resolve(); + } + const getTestModule = (moduleName) => { + try { + testLoader.loadIndividualModule(moduleName); // if no tests were added, request the next module if (qunit.config.queue.length === 0) { - Testem.emit('get-next-module'); + Testem.emit('testem:get-next-module'); } else { // `removeCallback` removes if the event queue contains the same callback for an event. Testem.removeEventCallbacks('testem:next-module-response', getTestModule); - Testem.removeEventCallbacks('testem:module-queue-complete', resolve); + Testem.removeEventCallbacks('testem:module-queue-complete', moduleComplete); resolve(); } - }); - Testem.on('testem:module-queue-complete', resolve); - Testem.emit('get-next-module'); - }); + } catch (err) { + reject(err); + } + } + + Testem.on('testem:next-module-response', getTestModule); + Testem.on('testem:module-queue-complete', moduleComplete); + Testem.emit('testem:get-next-module'); + } + + qunit.begin(() => { + return new self.Promise(nextModuleHandler); }); -} -// loadIndividualModule() executes loadIndividualModule() defined in patch-test-loader. It enables to load an AMD module one by one. -function loadIndividualModule(moduleName) { - testLoader.loadIndividualModule(moduleName); + qunit.moduleDone(() =>{ + return new self.Promise(nextModuleHandler); + }); } export default { loadEmberExam, loadTests, - setupQUnitCallbacks, - loadIndividualModule, -}; + setupQUnitCallbacks +}; \ No newline at end of file diff --git a/index.js b/index.js index 7f027de75..2b7688589 100644 --- a/index.js +++ b/index.js @@ -5,11 +5,11 @@ module.exports = { name: require('./package').name, - includedCommands: function() { + includedCommands() { return require('./lib/commands'); }, - checkDevDependencies: function() { + checkDevDependencies() { const VersionChecker = require('ember-cli-version-checker'); const checker = new VersionChecker(this); return checker.for('ember-cli', 'npm').gte('3.2.0'); diff --git a/lib/commands/exam.js b/lib/commands/exam.js index cb24b680a..20b316525 100644 --- a/lib/commands/exam.js +++ b/lib/commands/exam.js @@ -6,7 +6,6 @@ const TestCommand = require('ember-cli/lib/commands/test'); // eslint-disable-li const TestServerTask = require('./task/test-server'); const TestTask = require('./task/test'); const debug = require('debug')('exam'); -const moment = require('moment'); const executionMapping = Object.create(null); @@ -44,7 +43,7 @@ module.exports = TestCommand.extend({ { name: 'load-balance', type: Number, description: 'The number of browser(s) to load balance test files against. Test files will be sorted by weight from slowest (acceptance) to fastest (eslint).' }, { name: 'random', type: String, default: false, description: 'Randomizes your modules and tests while running your test suite.' }, { name: 'replay-execution', type: String, default: false, description: 'A JSON file path which maps from browser number to a list of modules'}, - { name: 'replay-browser', type: [Array, Number], description: 'The number of the browser(s) to run from a specified file path'} + { name: 'replay-browser', type: [String, Number, Array], description: 'The number of the browser(s) to run from a specified file path'} ].concat(TestCommand.prototype.availableOptions), init() { @@ -91,6 +90,9 @@ module.exports = TestCommand.extend({ run(commandOptions) { this.validator = this._createValidator(commandOptions); + // convert replayBrowser value to array + commandOptions.replayBrowser = this._convertToArray(commandOptions.replayBrowser); + if (this.validator.shouldSplit) { commandOptions.query = addToQuery(commandOptions.query, 'split', commandOptions.split); @@ -130,14 +132,16 @@ module.exports = TestCommand.extend({ const executionFilePath = options.replayExecution; const browserIdsToReplay = options.replayBrowser; - let testMapping = {}; + let testMapping; + try { // Read the replay execution json file. const executionToReplay = fs.readFileSync(executionFilePath); testMapping = JSON.parse(executionToReplay); } catch (err) { - throw new Error(`Error reading reply execution JSON file - ${err}`); + const errorMessage = `Failed to read ember-exam replay file: ${executionFilePath} ` + err.message; + throw new Error(errorMessage); } const browserModuleMapping = testMapping.executionMapping; @@ -173,6 +177,33 @@ module.exports = TestCommand.extend({ return addToQuery(query, 'seed', seed); }, + /** + * Returns an array converted from a string. + * e.g. '1, 2, 3' => [1, 2, 3] and '1, 2, [3, 4]' => [1, 2, 3, 4] + * + * @param {*} optionValue + */ + _convertToArray(optionValue) { + if (typeof optionValue === 'undefined') { + return; + } + if (typeof optionValue === 'number') { + return optionValue; + } + const optionArray = []; + if (Array.isArray(optionValue)) { + optionValue.forEach((element) => { + if (typeof element === 'string') { + Array.prototype.push.apply(optionArray, element.split(',')); + } else { + optionArray.push(element); + } + }); + } + + return optionArray; + }, + /** * Customizes the Testem config to have multiple test pages if attempting to * run in parallel. It is important that the user specifies the number of @@ -183,7 +214,6 @@ module.exports = TestCommand.extend({ _generateCustomConfigs(commandOptions) { const config = this._super._generateCustomConfigs.apply(this, arguments); const customBaseUrl = this._getCustomBaseUrl(config, commandOptions); - const shouldParallelize = this.validator.shouldParallelize; const shouldLoadBalance = this.validator.shouldLoadBalance; const shouldReplayExecution = this.validator.shouldReplayExecution; @@ -192,6 +222,7 @@ module.exports = TestCommand.extend({ return config; } + let browserIds = commandOptions.partition; let appendingParam = 'partition'; @@ -229,6 +260,29 @@ module.exports = TestCommand.extend({ return config; }, + /** + * Appends parameter to baseUrl. + * + * @param {*} baseUrl + */ + _appendParamToBaseUrl(commandOptions, baseUrl) { + if (this.validator.shouldParallelize || this.validator.shouldSplit) { + baseUrl = addToUrl(baseUrl, 'split', commandOptions.split); + } + // `loadBalance` is added to url when running replay-execution in order to emit `set-module-queue` in patch-test-loader. + if (this.validator.shouldLoadBalance || this.validator.shouldReplayExecution) { + const partitions = commandOptions.partition; + baseUrl = addToUrl(baseUrl, 'loadBalance', true) + if (partitions) { + for (let i = 0; i < partitions.length; i++) { + baseUrl = addToUrl(baseUrl, 'partition', partitions[i]); + } + } + } + + return baseUrl; + }, + /** * Customizes the base url by specified test splitting options - parellel or loadBalance. * @@ -243,35 +297,13 @@ module.exports = TestCommand.extend({ // Get the testPage from the generated config or the Testem config and // use it as the baseUrl to customize for the parallelized test pages or test load balancing const baseUrl = config.testPage || this._getTestPage(commandOptions.configFile); - const splitCount = commandOptions.split; - - const command = this; - - const appendParamToBaseUrl = function(baseUrl) { - if (command.validator.shouldParallelize || command.validator.shouldSplit) { - baseUrl = addToUrl(baseUrl, 'split', splitCount); - } - // `loadBalance` is added to url when running replay-execution in order to emit `set-module-queue` in patch-test-loader. - if (command.validator.shouldLoadBalance || command.validator.shouldReplayExecution) { - baseUrl = addToUrl(baseUrl, 'loadBalance', true) - - const partitions = commandOptions.partition; - if (partitions) { - for (let i = 0; i < partitions.length; i++) { - baseUrl = addToUrl(baseUrl, 'partition', partitions[i]); - } - } - } - - return baseUrl; - } if (Array.isArray(baseUrl)) { return baseUrl.map((currentUrl) => { - return appendParamToBaseUrl(currentUrl); + return this._appendParamToBaseUrl(commandOptions, currentUrl); }) } else { - return appendParamToBaseUrl(baseUrl); + return this._appendParamToBaseUrl(commandOptions, baseUrl); } }, @@ -385,7 +417,7 @@ module.exports = TestCommand.extend({ _addLoadBalancingBrowserSocketEvents(config) { const events = config.custom_browser_socket_events || {}; - events['set-modules-queue'] = function(modules) { + events['testem:set-modules-queue'] = function(modules) { // When `load-balance` option is valid we want to have one static list of modules on server side to send a module path to browsers. const proto = Object.getPrototypeOf(this); @@ -408,7 +440,7 @@ module.exports = TestCommand.extend({ } } - events['get-next-module'] = function() { + events['testem:get-next-module'] = function() { const proto = Object.getPrototypeOf(this); const replayExecution = this.config.progOptions.browser_module_mapping; const moduleQueue = this.moduleQueue; @@ -452,15 +484,18 @@ module.exports = TestCommand.extend({ delete proto.moduleQueue; delete proto.finishedBrowser; if (this.config.appMode === 'ci' && shouldLoadBalance) { - const fileName = `test-execution-${moment().format('YYYY-MM-DD__HH-MM-SS')}.json`; + const fileName = `test-execution-${new Date().toJSON().replace(/[:.]/g,'-')}.json`; const moduleMapJson = { numberOfBrowsers: browserCount, executionMapping: proto.moduleMap }; const testExecutionJson = JSON.stringify(moduleMapJson); - fs.writeFileSync(fileName, testExecutionJson, (err) => { - if (err) throw err; - }); + try { + fs.writeFileSync(fileName, testExecutionJson); + } catch (err) { + const errorMessage = `Failed to write a test execution json file: ${fileName} ` + err.message; + throw new Error(errorMessage); + } } } } diff --git a/lib/commands/task/test-server.js b/lib/commands/task/test-server.js index ead1454e5..19f7880d6 100644 --- a/lib/commands/task/test-server.js +++ b/lib/commands/task/test-server.js @@ -2,6 +2,7 @@ const TestServerTask = require('ember-cli/lib/tasks/test-server'); module.exports = TestServerTask.extend({ + transformOptions(options) { const transformOptions = this._super(...arguments); transformOptions.custom_browser_socket_events = options.custom_browser_socket_events; diff --git a/lib/utils/tests-options-validator.js b/lib/utils/tests-options-validator.js index 0e95ad404..6e7b9ac79 100644 --- a/lib/utils/tests-options-validator.js +++ b/lib/utils/tests-options-validator.js @@ -73,10 +73,10 @@ function validatePartitionSplit(partitions, split) { * @param {String} typeOfValue */ function validateElementsUnique(arr, typeOfValue) { - arr = arr.sort(); - for (let i = 0; i < arr.length - 1; i++) { - if (arr[i] === arr[i + 1]) { - const errorMsg = `You cannot specify the same ${typeOfValue} value twice. ${arr[i].toString()} is repeated.`; + const sorted = arr.slice().sort() + for (let i = 0; i < sorted.length - 1; i++) { + if (sorted[i] === sorted[i + 1]) { + const errorMsg = `You cannot specify the same ${typeOfValue} value twice. ${sorted[i]} is repeated.`; throw new Error(errorMsg); } } @@ -113,6 +113,14 @@ module.exports = class TestsOptionsValidator { split = 1; } + if (typeof split !== 'undefined' && typeof this.options.replayBrowser !== 'undefined') { + throw new Error('You must not use the `replay-browser` option with the `split` option.'); + } + + if (typeof split !== 'undefined' && this.options.replayExecution) { + throw new Error('You must not use the `replay-execution` option with the `split` option.'); + } + const partitions = options.partition; if (typeof partitions !== 'undefined') { @@ -155,6 +163,14 @@ module.exports = class TestsOptionsValidator { return false; } + if (typeof this.options.replayBrowser !== 'undefined') { + throw new Error('You must not use the `replay-browser` option with the `parallel` option.'); + } + + if (this.options.replayExecution) { + throw new Error('You must not use the `replay-execution` option with the `parallel` option.'); + } + if (typeof this.options.loadBalance !== 'undefined') { throw new Error('You must not use the `load-balance` option with the `parallel` option.'); } @@ -186,6 +202,14 @@ module.exports = class TestsOptionsValidator { throw new Error('You must not use the `parallel` option with the `load-balance` option.'); } + if (typeof this.options.replayBrowser !== 'undefined') { + throw new Error('You must not use the `replay-browser` option with the `load-balance` option.'); + } + + if (this.options.replayExecution) { + throw new Error('You must not use the `replay-execution` option with the `load-balance` option.'); + } + return true; } diff --git a/node-tests/acceptance/exam-test.js b/node-tests/acceptance/exam-test.js index d4f8f1aae..7a649be58 100644 --- a/node-tests/acceptance/exam-test.js +++ b/node-tests/acceptance/exam-test.js @@ -13,11 +13,7 @@ function getNumberOfTests(str) { return match && parseInt(match[1], 10); } -<<<<<<< HEAD -const TOTAL_NUM_TESTS = 33; // Total Number of tests without the global "Ember.onerror validation tests" -======= -const TOTAL_NUM_TESTS = 35; // Total Number of tests without the global "Ember.onerror validation tests" ->>>>>>> [Feature] Test Load Balancing +const TOTAL_NUM_TESTS = 39; // Total Number of tests without the global "Ember.onerror validation tests" function getTotalNumberOfTests(output) { // In ember-qunit 3.4.0, this new check was added: https://github.com/emberjs/ember-qunit/commit/a7e93c4b4b535dae62fed992b46c00b62bfc83f4 diff --git a/node-tests/unit/commands/exam-test.js b/node-tests/unit/commands/exam-test.js index c985dae51..3939f59ad 100644 --- a/node-tests/unit/commands/exam-test.js +++ b/node-tests/unit/commands/exam-test.js @@ -114,6 +114,52 @@ describe('ExamCommand', function() { }); }); + describe('_appendParamToBaseUrl', function() { + function appendParamToBaseUrl(commandOptions, baseUrl) { + const project = new MockProject(); + project.isEmberCLIProject = function() { return true; }; + + const command = new ExamCommand({ + project: project, + tasks: {} + }); + + command.validator = new TestOptionsValidator(commandOptions); + + return command._appendParamToBaseUrl(commandOptions, baseUrl); + } + + it('should add `split` when `split` option is used.', function() { + const appendedUrl = appendParamToBaseUrl({ split: 3 }, 'tests/index.html?hidepassed'); + + assert.deepEqual(appendedUrl, 'tests/index.html?hidepassed&split=3'); + }); + + it('should add `split` when `split` and `parallel` option are used.', function() { + const appendedUrl = appendParamToBaseUrl({ split: 5, parallel: true }, 'tests/index.html?hidepassed'); + + assert.deepEqual(appendedUrl, 'tests/index.html?hidepassed&split=5'); + }); + + it('should add `loadBalance` when `load-balance` option is used.', function() { + const appendedUrl = appendParamToBaseUrl({ loadBalance: 2 }, 'tests/index.html?hidepassed'); + + assert.deepEqual(appendedUrl, 'tests/index.html?hidepassed&loadBalance'); + }); + + it('should add `split`, `loadBalance`, and `partition` when `split`, `loadBalance`, and `partition` are used.', function() { + const appendedUrl = appendParamToBaseUrl({ split: 5, partition: [1,2,3], loadBalance: 2 }, 'tests/index.html?hidepassed'); + + assert.deepEqual(appendedUrl, 'tests/index.html?hidepassed&split=5&loadBalance&partition=1&partition=2&partition=3'); + }); + + it('should add `loadBalance` when `replay-execution` and `replay-browser` are used.', function() { + const appendedUrl = appendParamToBaseUrl({ replayExecution: 'test-execution-0000000.json', replayBrowser: [1, 2] }, 'tests/index.html?hidepassed'); + + assert.deepEqual(appendedUrl, 'tests/index.html?hidepassed&loadBalance'); + }); + }) + describe('_getCustomBaseUrl', function() { function getCustomBaseUrl(config, options) { const project = new MockProject(); @@ -147,12 +193,18 @@ describe('ExamCommand', function() { assert.deepEqual(baseUrl, 'tests/index.html?hidepassed&split=2&loadBalance'); }); - it('should set split env var', function() { - return command.run({ split: 5 }).then(function() { - assert.equal(process.env.EMBER_EXAM_SPLIT_COUNT, '5'); - }); + it('should add `split`, `loadBalance`, and `partitions` to a base url with split, partition, and load-balance', function() { + const baseUrl = getCustomBaseUrl({ testPage: 'tests/index.html?hidepassed' }, { loadBalance: 3, split: 3, partition: [2, 3] }); + + assert.deepEqual(baseUrl, 'tests/index.html?hidepassed&split=3&loadBalance&partition=2&partition=3'); }); - }); + + it('should add `loadBalance` to a base url with replay-execution', function() { + const baseUrl = getCustomBaseUrl({ testPage: 'tests/index.html?hidepassed' }, { replayExecution: 'test-execution-0000000.json', replayBrowser: [1, 2] }); + + assert.deepEqual(baseUrl, 'tests/index.html?hidepassed&loadBalance'); + }); + }) describe('_generateCustomConfigs', function() { function generateConfig(options) { diff --git a/package.json b/package.json index 5a546468c..80f422ef8 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,6 @@ "ember-cli-babel": "^7.5.0", "execa": "^1.0.0", "fs-extra": "^7.0.1", - "moment": "^2.22.2", "rimraf": "^2.6.2" }, "devDependencies": { diff --git a/tests/unit/mocha/test-loader-test.js b/tests/unit/mocha/test-loader-test.js index 861cb8e5d..666304626 100644 --- a/tests/unit/mocha/test-loader-test.js +++ b/tests/unit/mocha/test-loader-test.js @@ -1,4 +1,4 @@ -import TestLoader from 'ember-cli-test-loader/test-support'; +import EmberExamTestLoader from 'ember-exam/test-support/-private/patch-test-loader'; import { describe, it, beforeEach, afterEach } from 'mocha'; import { expect } from 'chai'; @@ -20,19 +20,18 @@ describe('Unit | test-loader', function() { 'test-4-test': true, 'test-4-test.jshint': true, }; - - this.originalURLParams = TestLoader._urlParams; + this.testLoader = new EmberExamTestLoader(); + this.originalURLParams = EmberExamTestLoader._urlParams; }); afterEach(function() { - TestLoader._urlParams = this.originalURLParams; + EmberExamTestLoader._urlParams = this.originalURLParams; window.require = this.originalRequire; }); it('loads all test modules by default', function() { - TestLoader._urlParams = {}; - - TestLoader.load(); + this.testLoader._urlParams = {}; + this.testLoader.loadModules(); expect(this.requiredModules).to.deep.equal([ 'test-1-test.jshint', @@ -47,12 +46,12 @@ describe('Unit | test-loader', function() { }); it('loads modules from a specified partition', function() { - TestLoader._urlParams = { + this.testLoader._urlParams = { _partition: 3, _split: 4, }; - TestLoader.load(); + this.testLoader.loadModules(); expect(this.requiredModules).to.deep.equal([ 'test-3-test.jshint', @@ -61,12 +60,12 @@ describe('Unit | test-loader', function() { }); it('loads modules from multiple specified partitions', function() { - TestLoader._urlParams = { + this.testLoader._urlParams = { _partition: [1, 3], _split: 4, }; - TestLoader.load(); + this.testLoader.loadModules(); expect(this.requiredModules).to.deep.equal([ 'test-1-test.jshint', @@ -77,11 +76,11 @@ describe('Unit | test-loader', function() { }); it('loads modules from the first partition by default', function() { - TestLoader._urlParams = { + this.testLoader._urlParams = { _split: 4, }; - TestLoader.load(); + this.testLoader.loadModules(); expect(this.requiredModules).to.deep.equal([ 'test-1-test.jshint', @@ -90,12 +89,12 @@ describe('Unit | test-loader', function() { }); it('handles params as strings', function() { - TestLoader._urlParams = { + this.testLoader._urlParams = { _partition: '3', _split: '4', }; - TestLoader.load(); + this.testLoader.loadModules(); expect(this.requiredModules).to.deep.equal([ 'test-3-test.jshint', @@ -104,78 +103,78 @@ describe('Unit | test-loader', function() { }); it('throws an error if splitting less than one', function() { - TestLoader._urlParams = { + this.testLoader._urlParams = { _split: 0, }; expect(() => { - TestLoader.load(); + this.testLoader.loadModules(); }).to.throw(/You must specify a split greater than 0/); }); it('throws an error if partition isn\'t a number', function() { - TestLoader._urlParams = { + this.testLoader._urlParams = { _split: 2, _partition: 'foo', }; expect(() => { - TestLoader.load(); + this.testLoader.loadModules(); }).to.throw(/You must specify numbers for partition \(you specified 'foo'\)/); }); it('throws an error if partition isn\'t a number with multiple partitions', function() { - TestLoader._urlParams = { + this.testLoader._urlParams = { _split: 2, _partition: [1, 'foo'], }; expect(() => { - TestLoader.load(); + this.testLoader.loadModules(); }).to.throw(/You must specify numbers for partition \(you specified '1,foo'\)/); }); it('throws an error if loading partition greater than split number', function() { - TestLoader._urlParams = { + this.testLoader._urlParams = { _split: 2, _partition: 3, }; expect(() => { - TestLoader.load(); + this.testLoader.loadModules(); }).to.throw(/You must specify partitions numbered less than or equal to your split value/); }); it('throws an error if loading partition greater than split number with multiple partitions', function() { - TestLoader._urlParams = { + this.testLoader._urlParams = { _split: 2, _partition: [2, 3], }; expect(() => { - TestLoader.load(); + this.testLoader.loadModules(); }).to.throw(/You must specify partitions numbered less than or equal to your split value/); }); it('throws an error if loading partition less than one', function() { - TestLoader._urlParams = { + this.testLoader._urlParams = { _split: 2, _partition: 0, }; expect(() => { - TestLoader.load(); + this.testLoader.loadModules(); }).to.throw(/You must specify partitions numbered greater than 0/); }); it('load works without lint tests', function() { - TestLoader._urlParams = { + this.testLoader._urlParams = { nolint: true, _partition: 4, _split: 4, }; - TestLoader.load(); + this.testLoader.loadModules(); // ember-cli-mocha doesn't support disabling linting by url param expect(this.requiredModules).to.deep.equal([ @@ -192,12 +191,12 @@ describe('Unit | test-loader', function() { 'test-4-test.jshint': true, }; - TestLoader._urlParams = { + this.testLoader._urlParams = { _partition: 4, _split: 4, }; - TestLoader.load(); + this.testLoader.loadModules(); expect(this.requiredModules).to.deep.equal([ 'test-4-test.jshint', @@ -218,16 +217,15 @@ describe('Unit | test-loader', function() { 'test-10-test': true, }; - TestLoader._urlParams = { + this.testLoader._urlParams = { _partition: '10', _split: 10, }; - TestLoader.load(); + this.testLoader.loadModules(); expect(this.requiredModules).to.deep.equal([ 'test-10-test', ]); }); }); - diff --git a/tests/unit/mocha/testem-output-test.js b/tests/unit/mocha/testem-output-test.js new file mode 100644 index 000000000..544aa9702 --- /dev/null +++ b/tests/unit/mocha/testem-output-test.js @@ -0,0 +1,25 @@ +import { describe, it } from 'mocha'; +import { expect } from 'chai'; +import TestemOutput from 'ember-exam/test-support/-private/patch-testem-output'; + +describe('Unit | patch-testem-output', () => { + it('add partition number to test name when `split` is passed', function() { + expect(TestemOutput.updateTestName({split: 2}, 'test_module | test_name')) + .to.deepEqual('Exam Partition 1 - test_module | test_name'); + }); + + it('add partition number to test name when `split` and `partition` are passed', function() { + expect(TestemOutput.updateTestName({split: 2, partition: 2}, 'test_module | test_name')) + .to.deepEqual('Exam Partition 2 - test_module | test_name'); + }); + + it('add browser number to test name when `loadBalance` and `browser` are passed', function() { + expect(TestemOutput.updateTestName({loadBalance: 2, browser: 1}, 'test_module | test_name')) + .to.deepEqual('Browser Id 1 - test_module | test_name'); + }); + + it('add partition number, browser number to test name when `split`, `partition`, `browser`, and `loadBalance` are passed', function() { + expect(TestemOutput.updateTestName({split: 2, partition: 2, browser:1, loadBalance: 2}, 'test_module | test_name')) + .to.deepEqual('Exam Partition 2 - Browser Id 1 - test_module | test_name'); + }); +}); diff --git a/tests/unit/mocha/weight-test-modules-test.js b/tests/unit/mocha/weight-test-modules-test.js index dabfd3bd8..4db05e207 100644 --- a/tests/unit/mocha/weight-test-modules-test.js +++ b/tests/unit/mocha/weight-test-modules-test.js @@ -5,7 +5,7 @@ import { expect } from 'chai'; describe('Unit | weight-test-modules', () => { it('should sort a list of file paths by weight', function() { const listOfModules = [ - '/jshint/test-1-test', + '/eslint/test-1-test', '/acceptance/test-1-test', '/unit/test-1-test', '/integration/test-1-test', @@ -16,7 +16,7 @@ describe('Unit | weight-test-modules', () => { 'test-1-test', '/integration/test-1-test', '/unit/test-1-test', - '/jshint/test-1-test' + '/eslint/test-1-test' ]); }); @@ -24,14 +24,14 @@ describe('Unit | weight-test-modules', () => { const listOfModules = [ 'test-b-test', 'test-a-test', - '/jshint/test-b-test', + '/eslint/test-b-test', '/integration/test-b-test', '/integration/test-a-test', '/unit/test-b-test', '/acceptance/test-b-test', '/acceptance/test-a-test', '/unit/test-a-test', - '/jshint/test-a-test']; + '/eslint/test-a-test']; expect(weightTestModules(listOfModules)).to.deep.equal([ '/acceptance/test-a-test', @@ -42,8 +42,8 @@ describe('Unit | weight-test-modules', () => { '/integration/test-b-test', '/unit/test-a-test', '/unit/test-b-test', - '/jshint/test-a-test', - '/jshint/test-b-test' + '/eslint/test-a-test', + '/eslint/test-b-test' ]); }); }); diff --git a/tests/unit/qunit/test-loader-test.js b/tests/unit/qunit/test-loader-test.js index 68e089e06..66a3609f9 100644 --- a/tests/unit/qunit/test-loader-test.js +++ b/tests/unit/qunit/test-loader-test.js @@ -1,4 +1,4 @@ -import TestLoader from 'ember-cli-test-loader/test-support/index'; +import EmberExamTestLoader from 'ember-exam/test-support/-private/patch-test-loader'; import QUnit, { module, test } from 'qunit'; module('Unit | test-loader', { @@ -19,20 +19,19 @@ module('Unit | test-loader', { 'test-4-test': true, 'test-4-test.jshint': true, }; - - this.originalURLParams = TestLoader._urlParams; + this.testLoader = new EmberExamTestLoader(); + this.originalURLParams = EmberExamTestLoader._urlParams; }, afterEach() { - TestLoader._urlParams = this.originalURLParams; + EmberExamTestLoader._urlParams = this.originalURLParams; window.require = this.originalRequire; } }); test('loads all test modules by default', function(assert) { - TestLoader._urlParams = {}; - - TestLoader.load(); + this.testLoader._urlParams = {}; + this.testLoader.loadModules(); assert.deepEqual(this.requiredModules, [ 'test-1-test.jshint', @@ -47,12 +46,11 @@ test('loads all test modules by default', function(assert) { }); test('loads modules from a specified partition', function(assert) { - TestLoader._urlParams = { + this.testLoader._urlParams = { partition: 3, split: 4, }; - - TestLoader.load(); + this.testLoader.loadModules(); assert.deepEqual(this.requiredModules, [ 'test-3-test.jshint', @@ -61,12 +59,12 @@ test('loads modules from a specified partition', function(assert) { }); test('loads modules from multiple specified partitions', function(assert) { - TestLoader._urlParams = { + this.testLoader._urlParams = { partition: [1, 3], split: 4, }; - TestLoader.load(); + this.testLoader.loadModules(); assert.deepEqual(this.requiredModules, [ 'test-1-test.jshint', @@ -77,11 +75,11 @@ test('loads modules from multiple specified partitions', function(assert) { }); test('loads modules from the first partition by default', function(assert) { - TestLoader._urlParams = { + this.testLoader._urlParams = { split: 4, }; - TestLoader.load(); + this.testLoader.loadModules(); assert.deepEqual(this.requiredModules, [ 'test-1-test.jshint', @@ -90,12 +88,12 @@ test('loads modules from the first partition by default', function(assert) { }); test('handles params as strings', function(assert) { - TestLoader._urlParams = { + this.testLoader._urlParams = { partition: '3', split: '4', }; - TestLoader.load(); + this.testLoader.loadModules(); assert.deepEqual(this.requiredModules, [ 'test-3-test.jshint', @@ -104,78 +102,78 @@ test('handles params as strings', function(assert) { }); test('throws an error if splitting less than one', function(assert) { - TestLoader._urlParams = { + this.testLoader._urlParams = { split: 0, }; assert.throws(() => { - TestLoader.load(); + this.testLoader.loadModules(); }, /You must specify a split greater than 0/); }); test('throws an error if partition isn\'t a number', function(assert) { - TestLoader._urlParams = { + this.testLoader._urlParams = { split: 2, partition: 'foo', }; assert.throws(() => { - TestLoader.load(); + this.testLoader.loadModules(); }, /You must specify numbers for partition \(you specified 'foo'\)/); }); test('throws an error if partition isn\'t a number with multiple partitions', function(assert) { - TestLoader._urlParams = { + this.testLoader._urlParams = { split: 2, partition: [1, 'foo'], }; assert.throws(() => { - TestLoader.load(); + this.testLoader.loadModules(); }, /You must specify numbers for partition \(you specified '1,foo'\)/); }); test('throws an error if loading partition greater than split number', function(assert) { - TestLoader._urlParams = { + this.testLoader._urlParams = { split: 2, partition: 3, }; assert.throws(() => { - TestLoader.load(); + this.testLoader.loadModules(); }, /You must specify partitions numbered less than or equal to your split value/); }); test('throws an error if loading partition greater than split number with multiple partitions', function(assert) { - TestLoader._urlParams = { + this.testLoader._urlParams = { split: 2, partition: [2, 3], }; assert.throws(() => { - TestLoader.load(); + this.testLoader.loadModules(); }, /You must specify partitions numbered less than or equal to your split value/); }); test('throws an error if loading partition less than one', function(assert) { - TestLoader._urlParams = { + this.testLoader._urlParams = { split: 2, partition: 0, }; assert.throws(() => { - TestLoader.load(); + this.testLoader.loadModules(); }, /You must specify partitions numbered greater than 0/); }); test('load works without lint tests', function(assert) { QUnit.urlParams.nolint = true; - TestLoader._urlParams = { + this.testLoader._urlParams = { partition: 4, split: 4, }; - TestLoader.load(); + this.testLoader.loadModules(); assert.deepEqual(this.requiredModules, [ 'test-4-test', @@ -192,12 +190,12 @@ test('load works without non-lint tests', function(assert) { 'test-4-test.jshint': true, }; - TestLoader._urlParams = { + this.testLoader._urlParams = { partition: 4, split: 4, }; - TestLoader.load(); + this.testLoader.loadModules(); assert.deepEqual(this.requiredModules, [ 'test-4-test.jshint', @@ -218,14 +216,14 @@ test('load works with a double-digit single partition', function(assert) { 'test-10-test': true, }; - TestLoader._urlParams = { + this.testLoader._urlParams = { partition: '10', split: 10, }; - TestLoader.load(); + this.testLoader.loadModules(); assert.deepEqual(this.requiredModules, [ 'test-10-test', - ]); + ]); }); \ No newline at end of file diff --git a/tests/unit/qunit/testem-output-test.js b/tests/unit/qunit/testem-output-test.js new file mode 100644 index 000000000..6ad419d1e --- /dev/null +++ b/tests/unit/qunit/testem-output-test.js @@ -0,0 +1,28 @@ +import { module, test } from 'qunit'; +import TestemOutput from 'ember-exam/test-support/-private/patch-testem-output'; + +module('Unit | patch-testem-output', () => { + test('add partition number to test name when `split` is passed', function(assert) { + assert.deepEqual( + 'Exam Partition 1 - test_module | test_name', + TestemOutput.updateTestName({split: 2}, 'test_module | test_name')); + }); + + test('add partition number to test name when `split` and `partition` are passed', function(assert) { + assert.deepEqual( + 'Exam Partition 2 - test_module | test_name', + TestemOutput.updateTestName({split: 2, partition: 2}, 'test_module | test_name')); + }); + + test('add browser number to test name when `loadBalance` and `browser` are passed', function(assert) { + assert.deepEqual( + 'Browser Id 1 - test_module | test_name', + TestemOutput.updateTestName({loadBalance: 2, browser: 1}, 'test_module | test_name')); + }); + + test('add partition number, browser number to test name when `split`, `partition`, `browser`, and `loadBalance` are passed', function(assert) { + assert.deepEqual( + 'Exam Partition 2 - Browser Id 1 - test_module | test_name', + TestemOutput.updateTestName({split: 2, partition: 2, browser:1, loadBalance: 2}, 'test_module | test_name')); + }); +}); diff --git a/tests/unit/qunit/weight-test-modules-test.js b/tests/unit/qunit/weight-test-modules-test.js index a1c88182f..022599cd8 100644 --- a/tests/unit/qunit/weight-test-modules-test.js +++ b/tests/unit/qunit/weight-test-modules-test.js @@ -1,11 +1,10 @@ import { module, test } from 'qunit'; import weightTestModules from 'ember-exam/test-support/-private/weight-test-modules'; - module('Unit | weight-test-modules', () => { test('should sort a list of file paths by weight', function(assert) { const listOfModules = [ - '/jshint/test-1-test', + '/eslint/test-1-test', '/acceptance/test-1-test', '/unit/test-1-test', '/integration/test-1-test', @@ -16,21 +15,21 @@ module('Unit | weight-test-modules', () => { 'test-1-test', '/integration/test-1-test', '/unit/test-1-test', - '/jshint/test-1-test'], weightTestModules(listOfModules)); + '/eslint/test-1-test'], weightTestModules(listOfModules)); }); test('should sort a list of file paths by weight and alphbetical order', function(assert) { const listOfModules = [ 'test-b-test', 'test-a-test', - '/jshint/test-b-test', + '/eslint/test-b-test', '/integration/test-b-test', '/integration/test-a-test', '/unit/test-b-test', '/acceptance/test-b-test', '/acceptance/test-a-test', '/unit/test-a-test', - '/jshint/test-a-test']; + '/eslint/test-a-test']; assert.deepEqual([ '/acceptance/test-a-test', @@ -41,8 +40,8 @@ module('Unit | weight-test-modules', () => { '/integration/test-b-test', '/unit/test-a-test', '/unit/test-b-test', - '/jshint/test-a-test', - '/jshint/test-b-test'], weightTestModules(listOfModules)); + '/eslint/test-a-test', + '/eslint/test-b-test'], weightTestModules(listOfModules)); }); }); From a2be9ea6e2b620adc1abdd2207a561bf63cede2e Mon Sep 17 00:00:00 2001 From: Chohee Kim Date: Thu, 20 Dec 2018 14:21:58 -0800 Subject: [PATCH 03/28] =?UTF-8?q?1.=20Resolve=20mixing=20usage=20of=C2=A0t?= =?UTF-8?q?his=C2=A0and=C2=A0testLoader=C2=A0=202.=20Add=20prefix=C2=A0'te?= =?UTF-8?q?stem:'=C2=A0for=20every=20events=203.=20Instantiate=20a=20new?= =?UTF-8?q?=20EmberExamTestLoader=20class=20for=20each=20test=20in=20test-?= =?UTF-8?q?loader-test=204.=20Refactor=C2=A0exam.js=C2=A0so=20that=20it's?= =?UTF-8?q?=20more=20testable=20and=20add=20unit=20tests=205.=20Address=20?= =?UTF-8?q?not=20to=20use=C2=A0prototype=C2=A0in=20test-options-validator.?= =?UTF-8?q?js?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../-private/patch-test-loader.js | 11 +- addon-test-support/load.js | 2 +- lib/commands/exam.js | 331 +++--------------- lib/utils/config-reader.js | 70 ++++ lib/utils/query-helper.js | 40 +++ lib/utils/test-execution-json-reader.js | 19 + lib/utils/test-page-helper.js | 182 ++++++++++ lib/utils/tests-options-validator.js | 173 +++++---- node-tests/unit/commands/exam-test.js | 260 +------------- node-tests/unit/utils/config-reader-test.js | 26 ++ node-tests/unit/utils/query-helper-test.js | 58 +++ .../unit/utils/test-page-helper-test.js | 251 +++++++++++++ test-execution-0000000.json | 4 +- testem.simple-test-page.js | 3 + tests/unit/qunit/test-loader-test.js | 100 ++---- 15 files changed, 847 insertions(+), 683 deletions(-) create mode 100644 lib/utils/config-reader.js create mode 100644 lib/utils/query-helper.js create mode 100644 lib/utils/test-execution-json-reader.js create mode 100644 lib/utils/test-page-helper.js create mode 100644 node-tests/unit/utils/config-reader-test.js create mode 100644 node-tests/unit/utils/query-helper-test.js create mode 100644 node-tests/unit/utils/test-page-helper-test.js create mode 100644 testem.simple-test-page.js diff --git a/addon-test-support/-private/patch-test-loader.js b/addon-test-support/-private/patch-test-loader.js index c4370d6e4..681a61399 100644 --- a/addon-test-support/-private/patch-test-loader.js +++ b/addon-test-support/-private/patch-test-loader.js @@ -1,4 +1,3 @@ -/* globals Testem */ import getUrlParams from './get-url-params'; import splitTestModules from './split-test-modules'; import weightTestModules from './weight-test-modules'; @@ -11,16 +10,20 @@ TestLoader.prototype.actualUnsee = TestLoader.prototype.unsee; export default class EmberExamTestLoader extends TestLoader { - constructor() { + constructor(testem, urlParams) { super(); this._testModules = []; - this._urlParams = getUrlParams(); + // testem can't be null. needs to throw an error if testem is null saying you can't construct the object without passing testem + this._testem = testem; + this._urlParams = urlParams || getUrlParams(); } get urlParams() { return this._urlParams; } + // ember-cli-test-loaer instanciates new TestLoader instance and loadModules. As a purpose of + // having EmberExamTestLoader is not to create an instance as loading modules. EmberExamTestLoader does not support load(). static load() { throw new Error('`EmberExamTestLoader` doesn\'t support `load()`.'); } @@ -55,7 +58,7 @@ export default class EmberExamTestLoader extends TestLoader { this._testModules = splitTestModules(this._testModules, split, partitions); if (loadBalance) { - Testem.emit('testem:set-modules-queue', this._testModules); + this._testem.emit('testem:set-modules-queue', this._testModules); } else { this._testModules.forEach((moduleName) => { this.actualRequire(moduleName); diff --git a/addon-test-support/load.js b/addon-test-support/load.js index bb0283525..84fd4565b 100644 --- a/addon-test-support/load.js +++ b/addon-test-support/load.js @@ -15,7 +15,7 @@ function loadEmberExam() { loaded = true; - testLoader = new EmberExamTestLoader(); + testLoader = new EmberExamTestLoader(Testem); if (window.Testem) { TestemOutput.patchTestemOutput(testLoader.urlParams); diff --git a/lib/commands/exam.js b/lib/commands/exam.js index 20b316525..6606447ff 100644 --- a/lib/commands/exam.js +++ b/lib/commands/exam.js @@ -1,33 +1,12 @@ 'use strict'; const fs = require('fs-extra'); -const path = require('path'); +const { addToQuery } = require('../utils/query-helper'); +const { convertOptionValueToArray, getMultipleTestPages } = require('../utils/test-page-helper'); +const readTestExecutionJsonFile = require('../utils/test-execution-json-reader'); const TestCommand = require('ember-cli/lib/commands/test'); // eslint-disable-line node/no-unpublished-require const TestServerTask = require('./task/test-server'); const TestTask = require('./task/test'); -const debug = require('debug')('exam'); - -const executionMapping = Object.create(null); - -function addToQuery(query, param, value) { - if (!value) { - return query; - } - - const queryAddParam = query ? query + '&' + param : param; - - return value !== true ? - queryAddParam + '=' + value : - queryAddParam; -} - -function addToUrl(url, param, value) { - const urlParts = url.split('?'); - const base = urlParts[0]; - const query = urlParts[1]; - - return base + '?' + addToQuery(query, param, value); -} module.exports = TestCommand.extend({ name: 'exam', @@ -82,24 +61,54 @@ module.exports = TestCommand.extend({ } }, + /** + * Validates all commandOptions + * + * @param {*} commandOptions + */ + _validateOptions(commandOptions) { + this.validator = this._createValidator(commandOptions); + + if (commandOptions.split || commandOptions.partition) { + this.validator.shouldSplit; + } + + if (commandOptions.parallel) { + this.validator.shouldParallelize; + } + + // As random option can be an empty string it should check a type of random option rather than the option is not empty. + if (typeof commandOptions.random !== 'undefined') { + this.validator.shouldRandomize; + } + + if (commandOptions.loadBalance) { + this.validator.shouldLoadBalance; + } + + if (commandOptions.replayExecution || commandOptions.replayExecution) { + this.validator.shouldReplayExecution; + } + }, + /** * Validates the command options and then runs the original test command. * * @override */ run(commandOptions) { - this.validator = this._createValidator(commandOptions); + this._validateOptions(commandOptions); // convert replayBrowser value to array - commandOptions.replayBrowser = this._convertToArray(commandOptions.replayBrowser); + commandOptions.replayBrowser = convertOptionValueToArray(commandOptions.replayBrowser); - if (this.validator.shouldSplit) { + if (commandOptions.split) { commandOptions.query = addToQuery(commandOptions.query, 'split', commandOptions.split); process.env.EMBER_EXAM_SPLIT_COUNT = commandOptions.split; // Ignore the partition option when paralleling (we'll fill it in later) - if (!this.validator.shouldParallelize) { + if (!commandOptions.parallel) { const partitions = commandOptions.partition; if (partitions) { for (let i = 0; i < partitions.length; i++) { @@ -109,11 +118,11 @@ module.exports = TestCommand.extend({ } } - if (this.validator.shouldLoadBalance) { + if (commandOptions.loadBalance) { commandOptions.query = addToQuery(commandOptions.query, 'loadBalance', commandOptions.loadBalance); } - if (this.validator.shouldRandomize) { + if (typeof commandOptions.random !== 'undefined') { commandOptions.query = this._randomize(commandOptions.random, commandOptions.query); } @@ -125,41 +134,17 @@ module.exports = TestCommand.extend({ * * @param {Object} option */ - getExecutionMappingInstance(options) { - if (executionMapping !== undefined && executionMapping.numberOfBrowsers !== undefined && executionMapping.browserToModuleMap !== undefined) { - return executionMapping; - } - const executionFilePath = options.replayExecution; - const browserIdsToReplay = options.replayBrowser; - - let testMapping; + getExecutionMappingInstance(executionFilePath, browserIdsToReplay) { + const browserModuleMap = Object.create(null); + const executionJson = readTestExecutionJsonFile(executionFilePath); - try { - // Read the replay execution json file. - const executionToReplay = fs.readFileSync(executionFilePath); - testMapping = JSON.parse(executionToReplay); - } catch (err) { - const errorMessage = `Failed to read ember-exam replay file: ${executionFilePath} ` + err.message; - throw new Error(errorMessage); - } + browserIdsToReplay.forEach((browserId) => { + //what if browserId is not in the test file? + browserModuleMap[browserId] = executionJson.browserToModuleMap[browserId]; + }) - const browserModuleMapping = testMapping.executionMapping; - - executionMapping.numberOfBrowsers = testMapping.numberOfBrowsers; - executionMapping.browserToModuleMap = ((browserIdsToReplay, browserModuleMapping) => { - const browserModuleMap = {}; - - for (let i = 0; i < browserIdsToReplay.length; i++) { - const browserId = browserIdsToReplay[i]; - const listOfModules = browserModuleMapping[browserId]; - browserModuleMap[browserId] = listOfModules; - } - - return browserModuleMap; - })(browserIdsToReplay, browserModuleMapping); - - return executionMapping; + return browserModuleMap; }, /** @@ -177,33 +162,6 @@ module.exports = TestCommand.extend({ return addToQuery(query, 'seed', seed); }, - /** - * Returns an array converted from a string. - * e.g. '1, 2, 3' => [1, 2, 3] and '1, 2, [3, 4]' => [1, 2, 3, 4] - * - * @param {*} optionValue - */ - _convertToArray(optionValue) { - if (typeof optionValue === 'undefined') { - return; - } - if (typeof optionValue === 'number') { - return optionValue; - } - const optionArray = []; - if (Array.isArray(optionValue)) { - optionValue.forEach((element) => { - if (typeof element === 'string') { - Array.prototype.push.apply(optionArray, element.split(',')); - } else { - optionArray.push(element); - } - }); - } - - return optionArray; - }, - /** * Customizes the Testem config to have multiple test pages if attempting to * run in parallel. It is important that the user specifies the number of @@ -213,199 +171,20 @@ module.exports = TestCommand.extend({ */ _generateCustomConfigs(commandOptions) { const config = this._super._generateCustomConfigs.apply(this, arguments); - const customBaseUrl = this._getCustomBaseUrl(config, commandOptions); - const shouldParallelize = this.validator.shouldParallelize; - const shouldLoadBalance = this.validator.shouldLoadBalance; - const shouldReplayExecution = this.validator.shouldReplayExecution; - - if (!shouldParallelize && !shouldLoadBalance && !shouldReplayExecution) { - return config; - } + if (!commandOptions.loadBalance && !commandOptions.parallel && !commandOptions.replayExecution) return config; - let browserIds = commandOptions.partition; - let appendingParam = 'partition'; + config.testPage = getMultipleTestPages(config, commandOptions); - if (shouldParallelize && !browserIds) { - browserIds = []; - for (let i = 0; i < commandOptions.split; i++) { - browserIds.push(i + 1); - } - } else if (shouldLoadBalance) { + if (commandOptions.loadBalance || commandOptions.replayExecution) { config.custom_browser_socket_events = this._addLoadBalancingBrowserSocketEvents(config); - - const browserCount = commandOptions.loadBalance; - appendingParam = 'browser'; - - // Creates an array starting from 1 to browserCount. e.g. if browserCount is 3, the returned array is [1, 2, 3] - browserIds = Array.from({length: browserCount}, (v,k) => k+1); - } else if (shouldReplayExecution) { - const executionMapping = this.getExecutionMappingInstance(commandOptions); - config.custom_browser_socket_events = this._addLoadBalancingBrowserSocketEvents(config); - config.browser_module_mapping = executionMapping.browserToModuleMap; - - appendingParam = 'browser'; - browserIds = commandOptions.replayBrowser; - } - - if (Array.isArray(customBaseUrl)) { - const command = this; - config.testPage = customBaseUrl.reduce(function(testPages, customBaseUrl) { - return testPages.concat(command._generateTestPages(customBaseUrl, appendingParam, browserIds)); - }, []); - } else { - config.testPage = this._generateTestPages(customBaseUrl, appendingParam, browserIds); - } - - return config; - }, - - /** - * Appends parameter to baseUrl. - * - * @param {*} baseUrl - */ - _appendParamToBaseUrl(commandOptions, baseUrl) { - if (this.validator.shouldParallelize || this.validator.shouldSplit) { - baseUrl = addToUrl(baseUrl, 'split', commandOptions.split); - } - // `loadBalance` is added to url when running replay-execution in order to emit `set-module-queue` in patch-test-loader. - if (this.validator.shouldLoadBalance || this.validator.shouldReplayExecution) { - const partitions = commandOptions.partition; - baseUrl = addToUrl(baseUrl, 'loadBalance', true) - if (partitions) { - for (let i = 0; i < partitions.length; i++) { - baseUrl = addToUrl(baseUrl, 'partition', partitions[i]); - } + if (commandOptions.replayExecution) { + const executionMapping = this.getExecutionMappingInstance(commandOptions.replayExecution, commandOptions.replayBrowser); + config.browser_module_mapping = executionMapping.browserToModuleMap; } } - return baseUrl; - }, - - /** - * Customizes the base url by specified test splitting options - parellel or loadBalance. - * - * @param {String} config - * @param {Object} commandOptions - * @return {Object} - * @example tests/index.html?hidepassed&split=3 if parallel - * tests/index.html?hidepassed&split=3&loadBalance if load-balance - * tests/index.html?hidepassed&split=3&loadBalance&partition=1&partition=2 if load-balance and partitions are specified - */ - _getCustomBaseUrl(config, commandOptions) { - // Get the testPage from the generated config or the Testem config and - // use it as the baseUrl to customize for the parallelized test pages or test load balancing - const baseUrl = config.testPage || this._getTestPage(commandOptions.configFile); - - if (Array.isArray(baseUrl)) { - return baseUrl.map((currentUrl) => { - return this._appendParamToBaseUrl(commandOptions, currentUrl); - }) - } else { - return this._appendParamToBaseUrl(commandOptions, baseUrl); - } - }, - - /** - * Gets the test page specified by the application's Testem config. - * - * @param {String} [configFile] - Path to the config file to use - * @return {String} testPage - */ - _getTestPage(configFile) { - // Attempt to read in the testem config and use the test_page definition - const testemConfig = this._readTestemConfig(configFile); - let testPage = testemConfig && testemConfig.test_page; - - // If there is no test_page to use as the testPage, we warn that we're using - // a default value - if (!testPage) { - // eslint-disable-next-line no-console - console.warn('No test_page value found in the config. Defaulting to "tests/index.html?hidepassed"'); - testPage = 'tests/index.html?hidepassed'; - } - - return testPage; - }, - - /** - * Generates multiple test pages: for a given baseUrl, it appends the partition numbers - * or the browserId each page is running as query params. - * - * @param {String} customBaseUrl - * @param {String} appendingParam - * @param {Array} testPages - */ - _generateTestPages(customBaseUrl, appendingParam, browserIds) { - const testPages = []; - for (let i = 0; i < browserIds.length; i++) { - const url = addToUrl(customBaseUrl, appendingParam, browserIds[i]); - testPages.push(url); - } - - return testPages; - }, - - /** - * Gets the application's testem config by trying a custom file first and then - * defaulting to either `testem.js` or `testem.json`. - * - * @param {String} file - * @return {Object} config - */ - _readTestemConfig(file) { - const potentialFiles = [ - 'testem.js', - 'testem.json' - ]; - - if (file) { - potentialFiles.unshift(file); - } - - const configFile = this._findValidFile(potentialFiles); - - return configFile && this._readFileByType(configFile); - }, - - /** - * Given an array of file paths, returns the first one that exists and is - * accessible. Paths are relative to the process' cwd. - * - * @param {Array} files - * @return {String} file - */ - _findValidFile(files) { - for (let i = 0; i < files.length; i++) { - // TODO: investigate this cwd() usually they are in-error... - const file = path.join(process.cwd(), files[i]); - try { - fs.accessSync(file, fs.F_OK); - return file; - } catch (error) { - debug('Failed to find ' + file + ' due to error: ' + error); - continue; - } - } - }, - - /** - * Reads in a given file according to it's "type" as determined by file - * extension. Supported types are `js` and `json`. - * - * @param {String} file - * @return {Object} fileContents - */ - _readFileByType(file) { - const fileType = file.split('.').pop(); - switch (fileType) { - case 'js': - return require(file); - case 'json': - return fs.readJsonSync(file); - } + return config; }, /** @@ -487,7 +266,7 @@ module.exports = TestCommand.extend({ const fileName = `test-execution-${new Date().toJSON().replace(/[:.]/g,'-')}.json`; const moduleMapJson = { numberOfBrowsers: browserCount, - executionMapping: proto.moduleMap + browserToModuleMap: proto.moduleMap }; const testExecutionJson = JSON.stringify(moduleMapJson); try { diff --git a/lib/utils/config-reader.js b/lib/utils/config-reader.js new file mode 100644 index 000000000..2c2e9d87a --- /dev/null +++ b/lib/utils/config-reader.js @@ -0,0 +1,70 @@ +const fs = require('fs-extra'); +const path = require('path'); +const debug = require('debug')('exam:config-reader'); + +const potentialConfigFiles = [ + 'testem.js', + 'testem.json' +]; + +/** + * Given an array of file paths, returns the first one that exists and is + * accessible. Paths are relative to the process' cwd. + * + * @param {Array} files + * @return {String} file + */ +function _findValidFile(files) { + for (let i = 0; i < files.length; i++) { + // TODO: investigate this cwd() usually they are in-error... + const file = path.join(process.cwd(), files[i]); + try { + fs.accessSync(file, fs.F_OK); + return file; + } catch (error) { + debug('Failed to find ' + file + ' due to error: ' + error); + continue; + } + } +} + +/** + * Gets the application's testem config by trying a custom file first and then + * defaulting to either `testem.js` or `testem.json`. + * + * @param {String} file + * @return {Object} config + */ +function readTestemConfig(file, potentialFiles = potentialConfigFiles) { + if (file) { + potentialFiles.unshift(file); + } + + const configFile = _findValidFile(potentialFiles); + + return configFile && readFileByType(configFile); +} + +/** + * Reads in a given file according to it's "type" as determined by file + * extension. Supported types are `js` and `json`. + * + * @param {String} file + * @return {Object} fileContents + */ +function readFileByType(file) { + if (typeof file === 'string' ) { + const fileType = file.split('.').pop(); + switch (fileType) { + case 'js': + return require(file); + case 'json': + return fs.readJsonSync(file); + } + } +} + +module.exports = { + readTestemConfig, + readFileByType +} diff --git a/lib/utils/query-helper.js b/lib/utils/query-helper.js new file mode 100644 index 000000000..19d98dc38 --- /dev/null +++ b/lib/utils/query-helper.js @@ -0,0 +1,40 @@ + + +/** + * Creates a valid query string by appending a given param and value to query. + * + * @param {*} query + * @param {*} param + * @param {*} value + */ +function addToQuery(query, param, value) { + if (!value) { + return query; + } + + const queryAddParam = query ? query + '&' + param : param; + + return value !== true ? + queryAddParam + '=' + value : + queryAddParam; +} + +/** + * Addes a valid query string to a given url. + * + * @param {*} url + * @param {*} param + * @param {*} value + */ +function addToUrl(url, param, value) { + const urlParts = url.split('?'); + const base = urlParts[0]; + const query = urlParts[1]; + + return base + '?' + addToQuery(query, param, value); +} + +module.exports = { + addToQuery, + addToUrl, +}; diff --git a/lib/utils/test-execution-json-reader.js b/lib/utils/test-execution-json-reader.js new file mode 100644 index 000000000..6d0f665a0 --- /dev/null +++ b/lib/utils/test-execution-json-reader.js @@ -0,0 +1,19 @@ +const { readFileByType } = require('./config-reader'); + +let executionModuleMap = null; + +module.exports = function readTestExecutionJsonFile(fileName) { + if (executionModuleMap !== null) { + return executionModuleMap; + } + + try { + // Read the replay execution json file. + executionModuleMap = readFileByType(fileName); + } catch (err) { + const errorMessage = `Failed to read ember-exam replay file: ${fileName} ` + err.message; + throw new Error(errorMessage); + } + + return executionModuleMap; +} diff --git a/lib/utils/test-page-helper.js b/lib/utils/test-page-helper.js new file mode 100644 index 000000000..c9c9d5391 --- /dev/null +++ b/lib/utils/test-page-helper.js @@ -0,0 +1,182 @@ + +const { readTestemConfig } = require('../utils/config-reader'); +const { addToUrl } = require('./query-helper'); + +/** + * Generates multiple test pages: for a given baseUrl, it appends the partition numbers + * or the browserId each page is running as query params. + * + * @param {String} customBaseUrl + * @param {String} appendingParam + * @param {Array} testPages + */ +function _generateTestPages(customBaseUrl, appendingParam, browserIds) { + const testPages = []; + for (let i = 0; i < browserIds.length; i++) { + const url = addToUrl(customBaseUrl, appendingParam, browserIds[i]); + testPages.push(url); + } + + return testPages; +} + + /** + * Creates an array starting from start to end. + * e.g. if browserCount is 3, the returned array is [1, 2, 3] + *.. + * @param {Number} len + * @return {Array} + */ +function _getFilledArray(start, end) { + const length = end - start + 1; + return Array.from({ length }, (_, i) => i + Number(start)); +} + +/** + * Add paramater such as split, loadbalance or partition to a base url if options are valid. + * + * @param {*} baseUrl + */ +function _appendParamToBaseUrl(commandOptions, baseUrl) { + if (commandOptions.parallel || commandOptions.split) { + baseUrl = addToUrl(baseUrl, 'split', commandOptions.split); + } + // `loadBalance` is added to url when running replay-execution in order to emit `set-module-queue` in patch-test-loader. + if (commandOptions.loadBalance || commandOptions.replayExecution) { + const partitions = commandOptions.partition; + baseUrl = addToUrl(baseUrl, 'loadBalance', true) + if (partitions) { + for (let i = 0; i < partitions.length; i++) { + baseUrl = addToUrl(baseUrl, 'partition', partitions[i]); + } + } + } + + return baseUrl; +} + +/** + * Generates an array by parsing a given string optionValue as an optionValue can be in a string form of '1,2' + * or '2..5', where an input '2..5' indicates a number sequence starting from 2 to 5. + * + * @param {*} optionValue + */ +function _formatStringOptionValue(optionValue) { + if (optionValue.indexOf('..') > 0) { + const arr = optionValue.split('..') + return _getFilledArray(arr.shift(), arr.pop()); + } else { + return optionValue.split(','); + } +} + +/** + * Returns an array converted from a string. + * e.g. '1, 2, 3' => [1, 2, 3] and '1, 2, [3, 4]' => [1, 2, 3, 4] + * + * @param {*} optionValue + */ +function convertOptionValueToArray(optionValue) { + if (typeof optionValue === 'undefined' || typeof optionValue === 'number') { + return optionValue; + } + + let optionArray = []; + if (typeof optionValue === 'string') { + optionArray = _formatStringOptionValue(optionValue); + } + if (Array.isArray(optionValue)) { + optionValue.forEach((element) => { + if (typeof element === 'string') { + Array.prototype.push.apply(optionArray, _formatStringOptionValue(element)); + } else { + optionArray.push(element); + } + }); + } + + return optionArray; +} + +/** + * Gets a test url in testem config to modify the url in order to generate multiple test pages + * + * @param {*} configFile + */ +function getTestUrlFromTestemConfig(configFile) { + // Attempt to read in the testem config and use the test_page definition + const testemConfig = readTestemConfig(configFile); + let testPage = testemConfig && testemConfig.test_page; + + // If there is no test_page to use as the testPage, we warn that we're using + // a default value + if (!testPage) { + // eslint-disable-next-line no-console + console.warn('No test_page value found in the config. Defaulting to "tests/index.html?hidepassed"'); + testPage = 'tests/index.html?hidepassed'; + } + + // Get the testPage from the generated config or the Testem config and + // use it as the baseUrl to customize for the parallelized test pages or test load balancing + return testPage; +} + +/** + * Creates an array of custom base urls by appending options that are specified + * + * @param {*} commandOptions + * @param {*} baseUrl + */ +function getCustomBaseUrl(commandOptions, baseUrl) { + if (Array.isArray(baseUrl)) { + return baseUrl.map((currentUrl) => { + return _appendParamToBaseUrl(commandOptions, currentUrl); + }); + } else { + return _appendParamToBaseUrl(commandOptions, baseUrl); + } +} + +/** + * Ember-exam allows serving multiple browsers to run test suite. In order to acheive that test_page in testem config + * has to be set with an array of multiple urls reflecting to command passed. + * + * @param {*} config + * @param {*} commandOptions + */ +function getMultipleTestPages(config, commandOptions) { + let testPages = Object.create(null); + let browserIds = commandOptions.partition; + let appendingParam = 'partition'; + + if (commandOptions.parallel && !browserIds) { + browserIds = _getFilledArray(1, commandOptions.split); + } else if (commandOptions.loadBalance) { + appendingParam = 'browser'; + browserIds = _getFilledArray(1, commandOptions.loadBalance); + } else if (commandOptions.replayExecution) { + appendingParam = 'browser'; + browserIds = commandOptions.replayBrowser; + } + + const baseUrl = config.testPage || getTestUrlFromTestemConfig(commandOptions.configFile); + const customBaseUrl = getCustomBaseUrl(commandOptions, baseUrl); + + if (Array.isArray(customBaseUrl)) { + testPages = customBaseUrl.reduce(function(testPages, customBaseUrl) { + return testPages.concat(_generateTestPages(customBaseUrl, appendingParam, browserIds)); + }, []); + } else { + testPages = _generateTestPages(customBaseUrl, appendingParam, browserIds); + } + + return testPages; +} + +module.exports = { + convertOptionValueToArray, + getTestUrlFromTestemConfig, + getCustomBaseUrl, + getMultipleTestPages +} diff --git a/lib/utils/tests-options-validator.js b/lib/utils/tests-options-validator.js index 6e7b9ac79..c820323e7 100644 --- a/lib/utils/tests-options-validator.js +++ b/lib/utils/tests-options-validator.js @@ -1,6 +1,6 @@ 'use strict'; -const exam = require('../commands/exam'); +const readTestExecutionJsonFile= require('./test-execution-json-reader'); const checkDevDependencies = require('../../index').checkDevDependencies(); /** @@ -15,15 +15,19 @@ function validatePartitions(partitions, split) { validateElementsUnique(partitions, 'partition'); } +function getNumberOfBrowser(fileName) { + const executionJson = readTestExecutionJsonFile(fileName); + return executionJson.numberOfBrowsers; +} + /** * Validates the specified replay-browser * * @param {*} replayBrowser * @param {*} replayExecution */ -function validateReplayBrowser(replayBrowser, replayExecution, options) { - exam.prototype.executionMapping = exam.prototype.getExecutionMappingInstance(options); - const numberOfBrowsers = exam.prototype.executionMapping.numberOfBrowsers; +function validateReplayBrowser(replayBrowser, replayExecution) { + const numberOfBrowsers = getNumberOfBrowser(replayExecution); if (!replayExecution) { throw new Error('You must specify a file path when using the \'replay-browser\' option.'); @@ -89,11 +93,11 @@ function validateElementsUnique(arr, typeOfValue) { * * @class TestsOptionsValidator */ - module.exports = class TestsOptionsValidator { constructor(options, framework) { this.options = options; this.framework = framework; + this.validatedOption = Object.create(null); } /** @@ -104,30 +108,34 @@ module.exports = class TestsOptionsValidator { * @type {Boolean} */ get shouldSplit() { - const options = this.options; - let split = options.split; + if (!this.validatedOption.shouldSplit) { + const options = this.options; + let split = options.split; - if (typeof split !== 'undefined' && split < 2) { - // eslint-disable-next-line no-console - console.warn('You should specify a number of files greater than 1 to split your tests across. Defaulting to 1 split which is the same as not using `--split`.'); - split = 1; - } + if (typeof split !== 'undefined' && split < 2) { + // eslint-disable-next-line no-console + console.warn('You should specify a number of files greater than 1 to split your tests across. Defaulting to 1 split which is the same as not using `--split`.'); + split = 1; + } - if (typeof split !== 'undefined' && typeof this.options.replayBrowser !== 'undefined') { - throw new Error('You must not use the `replay-browser` option with the `split` option.'); - } + if (typeof split !== 'undefined' && typeof this.options.replayBrowser !== 'undefined') { + throw new Error('You must not use the `replay-browser` option with the `split` option.'); + } - if (typeof split !== 'undefined' && this.options.replayExecution) { - throw new Error('You must not use the `replay-execution` option with the `split` option.'); - } + if (typeof split !== 'undefined' && this.options.replayExecution) { + throw new Error('You must not use the `replay-execution` option with the `split` option.'); + } - const partitions = options.partition; + const partitions = options.partition; - if (typeof partitions !== 'undefined') { - validatePartitions(partitions, split); + if (typeof partitions !== 'undefined') { + validatePartitions(partitions, split); + } + + this.validatedOption.shouldSplit = !!split; } - return !!split; + return this.validatedOption.shouldSplit; } @@ -139,14 +147,18 @@ module.exports = class TestsOptionsValidator { * @type {Boolean} */ get shouldRandomize() { - const shouldRandomize = (typeof this.options.random === 'string'); + if (!this.validatedOption.shouldRandomize) { + const shouldRandomize = (typeof this.options.random === 'string'); + + if (shouldRandomize && this.framework === 'mocha') { + // eslint-disable-next-line no-console + console.warn('Mocha does not currently support randomizing test order, so tests will run in normal order. Please see https://github.com/mochajs/mocha/issues/902 for more info.'); + } - if (shouldRandomize && this.framework === 'mocha') { - // eslint-disable-next-line no-console - console.warn('Mocha does not currently support randomizing test order, so tests will run in normal order. Please see https://github.com/mochajs/mocha/issues/902 for more info.'); + this.validatedOption.shouldRandomize = shouldRandomize; } - return shouldRandomize; + return this.validatedOption.shouldRandomize; } /** @@ -157,60 +169,70 @@ module.exports = class TestsOptionsValidator { * @type {Boolean} */ get shouldParallelize() { - const parallel = this.options.parallel; + if (!this.validatedOption.shouldParallelize) { + const parallel = this.options.parallel; - if (!parallel) { - return false; - } + if (!parallel) { + this.validatedOption.shouldParallelize = false; + } else { + if (typeof this.options.replayBrowser !== 'undefined') { + throw new Error('You must not use the `replay-browser` option with the `parallel` option.'); + } - if (typeof this.options.replayBrowser !== 'undefined') { - throw new Error('You must not use the `replay-browser` option with the `parallel` option.'); - } + if (this.options.replayExecution) { + throw new Error('You must not use the `replay-execution` option with the `parallel` option.'); + } - if (this.options.replayExecution) { - throw new Error('You must not use the `replay-execution` option with the `parallel` option.'); - } + if (typeof this.options.loadBalance !== 'undefined') { + throw new Error('You must not use the `load-balance` option with the `parallel` option.'); + } - if (typeof this.options.loadBalance !== 'undefined') { - throw new Error('You must not use the `load-balance` option with the `parallel` option.'); - } + if (!this.shouldSplit) { + throw new Error('You must specify the `split` option in order to run your tests in parallel.'); + } + + this.validatedOption.shouldParallelize = true; + } - if (!this.shouldSplit) { - throw new Error('You must specify the `split` option in order to run your tests in parallel.'); } - return true; + return this.validatedOption.shouldParallelize; } get shouldLoadBalance() { - let loadBalance = this.options.loadBalance; + if (!this.validatedOption.loadBalance) { + let loadBalance = this.options.loadBalance; - if (typeof loadBalance == 'undefined') { - return false; - } + if (typeof loadBalance == 'undefined') { + this.validatedOption.loadBalance = false; + return this.validatedOption.loadBalance; + } - // It's required to use ember-cli version 3.2.0 or greater to support the `load-balance` feature. - if (!checkDevDependencies) { - throw new Error('You must be using ember-cli version 3.2.0 or greater for this feature to work properly.'); - } + // It's required to use ember-cli version 3.2.0 or greater to support the `load-balance` feature. + if (!checkDevDependencies) { + throw new Error('You must be using ember-cli version 3.2.0 or greater for this feature to work properly.'); + } - if (loadBalance < 1) { - throw new Error('You must specify a load-balance value greater than or equal to 1.'); - } + if (loadBalance < 1) { + throw new Error('You must specify a load-balance value greater than or equal to 1.'); + } - if (this.options.parallel) { - throw new Error('You must not use the `parallel` option with the `load-balance` option.'); - } + if (this.options.parallel) { + throw new Error('You must not use the `parallel` option with the `load-balance` option.'); + } - if (typeof this.options.replayBrowser !== 'undefined') { - throw new Error('You must not use the `replay-browser` option with the `load-balance` option.'); - } + if (typeof this.options.replayBrowser !== 'undefined') { + throw new Error('You must not use the `replay-browser` option with the `load-balance` option.'); + } - if (this.options.replayExecution) { - throw new Error('You must not use the `replay-execution` option with the `load-balance` option.'); + if (this.options.replayExecution) { + throw new Error('You must not use the `replay-execution` option with the `load-balance` option.'); + } + + this.validatedOption.loadBalance = true; } - return true; + return this.validatedOption.loadBalance; } /** @@ -221,19 +243,24 @@ module.exports = class TestsOptionsValidator { * @type {Boolean} */ get shouldReplayExecution() { - const replayBrowser = this.options.replayBrowser; - const replayExecution = this.options.replayExecution; + if (!this.validatedOption.replayExecution) { + const replayBrowser = this.options.replayBrowser; + const replayExecution = this.options.replayExecution; - if (!replayExecution) { - return false; - } + if (!replayExecution) { + this.validatedOption.shouldReplayExecution = false; + return this.validatedOption.shouldReplayExecution; + } - if (!replayBrowser) { - throw new Error('You must specify the `replay-browser` option in order to use `replay-execution` option.'); - } + if (!replayBrowser) { + throw new Error('You must specify the `replay-browser` option in order to use `replay-execution` option.'); + } - validateReplayBrowser(replayBrowser, replayExecution, this.options); + validateReplayBrowser(replayBrowser, replayExecution, this.options); + + this.validatedOption.replayExecution = true; + } - return true; + return this.validatedOption.replayExecution; } }; diff --git a/node-tests/unit/commands/exam-test.js b/node-tests/unit/commands/exam-test.js index 3939f59ad..c78870e46 100644 --- a/node-tests/unit/commands/exam-test.js +++ b/node-tests/unit/commands/exam-test.js @@ -112,99 +112,13 @@ describe('ExamCommand', function() { randomStub.restore(); }); }); - }); - describe('_appendParamToBaseUrl', function() { - function appendParamToBaseUrl(commandOptions, baseUrl) { - const project = new MockProject(); - project.isEmberCLIProject = function() { return true; }; - - const command = new ExamCommand({ - project: project, - tasks: {} + it('should set split env var', function() { + return command.run({ split: 5 }).then(function() { + assert.equal(process.env.EMBER_EXAM_SPLIT_COUNT, '5'); }); - - command.validator = new TestOptionsValidator(commandOptions); - - return command._appendParamToBaseUrl(commandOptions, baseUrl); - } - - it('should add `split` when `split` option is used.', function() { - const appendedUrl = appendParamToBaseUrl({ split: 3 }, 'tests/index.html?hidepassed'); - - assert.deepEqual(appendedUrl, 'tests/index.html?hidepassed&split=3'); - }); - - it('should add `split` when `split` and `parallel` option are used.', function() { - const appendedUrl = appendParamToBaseUrl({ split: 5, parallel: true }, 'tests/index.html?hidepassed'); - - assert.deepEqual(appendedUrl, 'tests/index.html?hidepassed&split=5'); - }); - - it('should add `loadBalance` when `load-balance` option is used.', function() { - const appendedUrl = appendParamToBaseUrl({ loadBalance: 2 }, 'tests/index.html?hidepassed'); - - assert.deepEqual(appendedUrl, 'tests/index.html?hidepassed&loadBalance'); - }); - - it('should add `split`, `loadBalance`, and `partition` when `split`, `loadBalance`, and `partition` are used.', function() { - const appendedUrl = appendParamToBaseUrl({ split: 5, partition: [1,2,3], loadBalance: 2 }, 'tests/index.html?hidepassed'); - - assert.deepEqual(appendedUrl, 'tests/index.html?hidepassed&split=5&loadBalance&partition=1&partition=2&partition=3'); - }); - - it('should add `loadBalance` when `replay-execution` and `replay-browser` are used.', function() { - const appendedUrl = appendParamToBaseUrl({ replayExecution: 'test-execution-0000000.json', replayBrowser: [1, 2] }, 'tests/index.html?hidepassed'); - - assert.deepEqual(appendedUrl, 'tests/index.html?hidepassed&loadBalance'); }); - }) - - describe('_getCustomBaseUrl', function() { - function getCustomBaseUrl(config, options) { - const project = new MockProject(); - project.isEmberCLIProject = function() { return true; }; - - const command = new ExamCommand({ - project: project, - tasks: {} - }); - - command.validator = new TestOptionsValidator(options); - - return command._getCustomBaseUrl(config, options) - } - - it('should add `loadBalance` to a base url with load-balance', function() { - const baseUrl = getCustomBaseUrl({ testPage: 'tests/index.html?hidepassed' }, { loadBalance: 3 }); - - assert.deepEqual(baseUrl, 'tests/index.html?hidepassed&loadBalance'); - }); - - it('should add `split` to a base url with split and parallelizing.', function() { - const baseUrl = getCustomBaseUrl({ testPage: 'tests/index.html?hidepassed' }, { parallel: true, split: 2 }); - - assert.deepEqual(baseUrl, 'tests/index.html?hidepassed&split=2'); - }); - - it('should add `split` and `loadBalance` to a base url with split and load-balance.', function() { - const baseUrl = getCustomBaseUrl({ testPage: 'tests/index.html?hidepassed' }, { loadBalance: 3, split: 2 }); - - assert.deepEqual(baseUrl, 'tests/index.html?hidepassed&split=2&loadBalance'); - }); - - it('should add `split`, `loadBalance`, and `partitions` to a base url with split, partition, and load-balance', function() { - const baseUrl = getCustomBaseUrl({ testPage: 'tests/index.html?hidepassed' }, { loadBalance: 3, split: 3, partition: [2, 3] }); - - assert.deepEqual(baseUrl, 'tests/index.html?hidepassed&split=3&loadBalance&partition=2&partition=3'); - }); - - it('should add `loadBalance` to a base url with replay-execution', function() { - const baseUrl = getCustomBaseUrl({ testPage: 'tests/index.html?hidepassed' }, { replayExecution: 'test-execution-0000000.json', replayBrowser: [1, 2] }); - - assert.deepEqual(baseUrl, 'tests/index.html?hidepassed&loadBalance'); - }); - }) + }); describe('_generateCustomConfigs', function() { function generateConfig(options) { @@ -221,172 +135,6 @@ describe('ExamCommand', function() { return command._generateCustomConfigs(options); } - - it('should have a null test page when not parallelizing', function() { - const config = generateConfig({}); - - assert.deepEqual(config.testPage, undefined); - }); - - it('should modify the config to have multiple test pages with no partitions specified', function() { - const config = generateConfig({ - parallel: true, - split: 2 - }); - - assert.deepEqual(config.testPage, [ - 'tests/index.html?hidepassed&split=2&partition=1', - 'tests/index.html?hidepassed&split=2&partition=2' - ]); - }); - - it('should modify the config to have multiple test pages with specified partitions', function() { - const config = generateConfig({ - parallel: true, - split: 4, - partition: [3, 4] - }); - - assert.deepEqual(config.testPage, [ - 'tests/index.html?hidepassed&split=4&partition=3', - 'tests/index.html?hidepassed&split=4&partition=4' - ]); - }); - - it('should modify the config to have multiple test pages for each test_page in the config file with no partitions specified', function() { - const config = generateConfig({ - parallel: true, - split: 2, - configFile: 'testem.multiple-test-page.js' - }); - - assert.deepEqual(config.testPage, [ - 'tests/index.html?hidepassed&derp=herp&split=2&partition=1', - 'tests/index.html?hidepassed&derp=herp&split=2&partition=2', - 'tests/index.html?hidepassed&foo=bar&split=2&partition=1', - 'tests/index.html?hidepassed&foo=bar&split=2&partition=2' - ]); - }); - - it('should modify the config to have multiple test pages for each test_page in the config file with partitions specified', function() { - const config = generateConfig({ - parallel: true, - split: 4, - partition: [3, 4], - configFile: 'testem.multiple-test-page.js' - }); - - assert.deepEqual(config.testPage, [ - 'tests/index.html?hidepassed&derp=herp&split=4&partition=3', - 'tests/index.html?hidepassed&derp=herp&split=4&partition=4', - 'tests/index.html?hidepassed&foo=bar&split=4&partition=3', - 'tests/index.html?hidepassed&foo=bar&split=4&partition=4' - ]); - }); - - it('should have a custom test page', function() { - const config = generateConfig({ - query: 'foo=bar', - 'test-page': 'tests.html' - }); - - assert.equal(config.testPage, 'tests.html?foo=bar'); - }); - - it('should modify the config to have a test page with \'loadBalance\' when no specified number of browser', function() { - const config = generateConfig({ - 'loadBalance': 1 - }); - - assert.deepEqual(config.testPage, [ - 'tests/index.html?hidepassed&loadBalance&browser=1' - ]) - }); - - it('should modify the config to have a test page with \'loadBalance\' with splitting when no specified number of browser', function() { - const config = generateConfig({ - 'loadBalance': 1, - split: 2, - }); - - assert.deepEqual(config.testPage, [ - 'tests/index.html?hidepassed&split=2&loadBalance&browser=1' - ]) - }); - - it('should modify the config to have multiple test pages with test loading balanced, no specified partitions and no splitting ', function(){ - const config = generateConfig({ - 'loadBalance': 2, - }); - - assert.deepEqual(config.testPage, [ - 'tests/index.html?hidepassed&loadBalance&browser=1', - 'tests/index.html?hidepassed&loadBalance&browser=2' - ]) - }); - - it('should modify the config to have multiple test pages with splitting when loading test load-balanced', function(){ - const config = generateConfig({ - 'loadBalance': 2, - split: 2, - }); - - assert.deepEqual(config.testPage, [ - 'tests/index.html?hidepassed&split=2&loadBalance&browser=1', - 'tests/index.html?hidepassed&split=2&loadBalance&browser=2' - ]) - }); - - it('should modify the config to have multiple test pages with specified partitions when loading test balanced', function(){ - const config = generateConfig({ - 'loadBalance': 2, - split: 3, - partition: [2, 3], - }); - - assert.deepEqual(config.testPage, [ - 'tests/index.html?hidepassed&split=3&loadBalance&partition=2&partition=3&browser=1', - 'tests/index.html?hidepassed&split=3&loadBalance&partition=2&partition=3&browser=2' - ]) - }); - - it('should modify the config to have multiple test pages for each test_page in the config file with partitions specified and test loading balanced', function() { - const config = generateConfig({ - 'loadBalance': 1, - split: 4, - partition: [3, 4], - configFile: 'testem.multiple-test-page.js' - }); - - assert.deepEqual(config.testPage, [ - 'tests/index.html?hidepassed&derp=herp&split=4&loadBalance&partition=3&partition=4&browser=1', - 'tests/index.html?hidepassed&foo=bar&split=4&loadBalance&partition=3&partition=4&browser=1' - ]); - }); - - it('should have a custom test page', function() { - const config = generateConfig({ - query: 'foo=bar', - 'test-page': 'tests.html' - }); - - assert.equal(config.testPage, 'tests.html?foo=bar'); - }); - - it('should modify the config to have multiple test pages with a custom base url', function() { - const config = generateConfig({ - parallel: true, - split: 2, - query: 'foo=bar', - 'test-page': 'tests.html' - }); - - assert.deepEqual(config.testPage, [ - 'tests.html?foo=bar&split=2&partition=1', - 'tests.html?foo=bar&split=2&partition=2' - ]); - }); - it('should warn if no test_page is defined but use a default', function() { const warnStub = sinon.stub(console, 'warn'); diff --git a/node-tests/unit/utils/config-reader-test.js b/node-tests/unit/utils/config-reader-test.js new file mode 100644 index 000000000..ac25a5762 --- /dev/null +++ b/node-tests/unit/utils/config-reader-test.js @@ -0,0 +1,26 @@ +'use strict'; + +const assert = require('assert'); +const { readTestemConfig } = require('../../../lib/utils/config-reader'); + +describe('ConfigReader | readTestemConfig', function() { + it('should find `testem.js` file by default and return `true` when no file name and no potential files specified', function() { + assert.ok(readTestemConfig()); + }); + + it('should return `false` if file doesn\'t exsit when potential files are empty list', function() { + assert.ok(!readTestemConfig('this-file-do-not-exsit.json', [])); + }); + + it('should find `testem.js` file by default and return `true` when file specified doesn\'t exist', function() { + assert.ok(readTestemConfig('this-file-do-not-exsit.json')); + }); + + it('should require a specified `js` file and return an object in the module when no potential files specified', function() { + assert.deepEqual(readTestemConfig('testem.simple-test-page.js').foo, 'bar'); + }); + + it('should require a specified `js` file and return an object in the module when the file exsits and potential files are empty list', function() { + assert.deepEqual(readTestemConfig('testem.simple-test-page.js', []).foo, 'bar'); + }); +}); diff --git a/node-tests/unit/utils/query-helper-test.js b/node-tests/unit/utils/query-helper-test.js new file mode 100644 index 000000000..447e9aa4b --- /dev/null +++ b/node-tests/unit/utils/query-helper-test.js @@ -0,0 +1,58 @@ +'use strict'; + +const assert = require('assert'); +const { addToQuery, addToUrl } = require('../../../lib/utils/query-helper'); + +describe('QueryHelper', function() { + describe('addToQuery', function() { + it('should add param when no query and value is true', function() { + const validQuery = addToQuery(null, 'foo', true); + + assert.deepEqual(validQuery, 'foo') + }); + + it('should add param and value when no query and value is string', function() { + const validQuery = addToQuery(null, 'foo', 'bar'); + + assert.deepEqual(validQuery, 'foo=bar') + }); + + it('should add param to query when value is boolean', function() { + const validQuery = addToQuery('foo', 'bar', true); + + assert.deepEqual(validQuery, 'foo&bar') + }); + + it('should add param and value to query when value is string', function() { + const validQuery = addToQuery('foo', 'bar', 'baz'); + + assert.deepEqual(validQuery, 'foo&bar=baz') + }); + + it('should not add param when value is false', function() { + const validQuery = addToQuery('foo', 'bar', false); + + assert.deepEqual(validQuery, 'foo') + }); + }); + + describe('addToUrl', function() { + it('should add param to url when value is true', function() { + const url = addToUrl('tests/index.html?hidepassed', 'foo', true); + + assert.deepEqual(url, 'tests/index.html?hidepassed&foo'); + }); + + it('should not add param to url when value is false', function() { + const url = addToUrl('tests/index.html?hidepassed', 'foo', false); + + assert.deepEqual(url, 'tests/index.html?hidepassed'); + }); + + it('should add param and value to url when value is string', function() { + const url = addToUrl('tests/index.html?hidepassed', 'foo', 'bar'); + + assert.deepEqual(url, 'tests/index.html?hidepassed&foo=bar') + }); + }); +}); diff --git a/node-tests/unit/utils/test-page-helper-test.js b/node-tests/unit/utils/test-page-helper-test.js new file mode 100644 index 000000000..3b536b783 --- /dev/null +++ b/node-tests/unit/utils/test-page-helper-test.js @@ -0,0 +1,251 @@ +'use strict'; + +const assert = require('assert'); +const { convertOptionValueToArray, getTestUrlFromTestemConfig, getCustomBaseUrl, getMultipleTestPages } = require('../../../lib/utils/test-page-helper'); + +describe('TestPageHelper', function() { + describe('convertOptionValueToArray', function() { + it('should return `null` when no optionValue specified', function() { + assert.deepEqual(convertOptionValueToArray(), null); + }); + + it('should have a specified option number when the option is number', function() { + assert.deepEqual(convertOptionValueToArray(3), 3); + }); + + it('should have a number of array when a specified option is string', function() { + assert.deepEqual(convertOptionValueToArray('2,3'), [2, 3]); + }); + + it('should have a numbe of array when a specified option is a combination of number and string ', function() { + assert.deepEqual(convertOptionValueToArray([1, '2,3']), [1, 2, 3]); + }); + + it('should have a sequence number of array when a specified option is in range', function() { + assert.deepEqual(convertOptionValueToArray('1..5'), [1, 2, 3, 4, 5]); + }); + + it('should have a number of array when a specified option is a combination of number and string in range', function() { + assert.deepEqual(convertOptionValueToArray([1, '3..6']), [1,3, 4, 5, 6]); + }); + + it('should return `null` when no pamarater specified', function() { + assert.deepEqual(convertOptionValueToArray(), null); + }); + }); + + describe('getTestUrlFromTestemConfig', function() { + it('should have a default test page with no config file', function() { + const testPage = getTestUrlFromTestemConfig(''); + + assert.deepEqual(testPage, 'tests/index.html?hidepassed') + }); + + it('should have a default test page with no test-page specified in a testem config file', function() { + const testPage = getTestUrlFromTestemConfig('testem.no-test-page.js'); + + assert.deepEqual(testPage, 'tests/index.html?hidepassed') + }); + + it('should have multiple test pages specified in testem config file with test-page specified in the file', function() { + const testPages = getTestUrlFromTestemConfig('testem.multiple-test-page.js'); + + assert.deepEqual(testPages, [ + 'tests/index.html?hidepassed&derp=herp', + 'tests/index.html?hidepassed&foo=bar' + ]); + }) + }); + + describe('getCustomBaseUrl', function() { + it('should add `split` when `split` option is used', function() { + const appendedUrl = getCustomBaseUrl({ split: 3 }, 'tests/index.html?hidepassed'); + + assert.deepEqual(appendedUrl, 'tests/index.html?hidepassed&split=3'); + }); + + it('should add `split` when `split` and `parallel` option are used', function() { + const appendedUrl = getCustomBaseUrl({ split: 5, parallel: true }, 'tests/index.html?hidepassed'); + + assert.deepEqual(appendedUrl, 'tests/index.html?hidepassed&split=5'); + }); + + it('should add `loadBalance` when `load-balance` option is used', function() { + const appendedUrl = getCustomBaseUrl({ loadBalance: 2 }, 'tests/index.html?hidepassed'); + + assert.deepEqual(appendedUrl, 'tests/index.html?hidepassed&loadBalance'); + }); + + it('should add `split`, `loadBalance`, and `partition` when `split`, `loadBalance`, and `partition` are used.', function() { + const appendedUrl = getCustomBaseUrl({ split: 5, partition: [1,2,3], loadBalance: 2 }, 'tests/index.html?hidepassed'); + + assert.deepEqual(appendedUrl, 'tests/index.html?hidepassed&split=5&loadBalance&partition=1&partition=2&partition=3'); + }); + + it('should add `loadBalance` when `replay-execution` and `replay-browser` are used', function() { + const appendedUrl = getCustomBaseUrl({ replayExecution: 'test-execution-0000000.json', replayBrowser: [1, 2] }, 'tests/index.html?hidepassed'); + + assert.deepEqual(appendedUrl, 'tests/index.html?hidepassed&loadBalance'); + }); + + it('should add `split` to multiple test pages when `split` option is used', function() { + const appendedUrl = getCustomBaseUrl({ split: 3 }, [ + 'tests/index.html?hidepassed&derp=herp', + 'tests/index.html?hidepassed&foo=bar' ]); + + assert.deepEqual(appendedUrl, ['tests/index.html?hidepassed&derp=herp&split=3', 'tests/index.html?hidepassed&foo=bar&split=3']); + }); + + it('should add `split` when `split` to multiple test pages and `parallel` option are used', function() { + const appendedUrl = getCustomBaseUrl({ split: 5, parallel: true }, [ + 'tests/index.html?hidepassed&derp=herp', + 'tests/index.html?hidepassed&foo=bar' ]); + + assert.deepEqual(appendedUrl, [ + 'tests/index.html?hidepassed&derp=herp&split=5', + 'tests/index.html?hidepassed&foo=bar&split=5']); + }); + + it('should add `loadBalance` to multiple test pages when `load-balance` option is used', function() { + const appendedUrl = getCustomBaseUrl({ loadBalance: 2 }, [ + 'tests/index.html?hidepassed&derp=herp', + 'tests/index.html?hidepassed&foo=bar' ]); + + assert.deepEqual(appendedUrl, [ + 'tests/index.html?hidepassed&derp=herp&loadBalance', + 'tests/index.html?hidepassed&foo=bar&loadBalance' ]); + }); + + it('should add `split`, `loadBalance`, and `partition` to multiple test pages when `split`, `loadBalance`, and `partition` are used.', function() { + const appendedUrl = getCustomBaseUrl({ split: 5, partition: [1,2,3], loadBalance: 2 }, [ + 'tests/index.html?hidepassed&derp=herp', + 'tests/index.html?hidepassed&foo=bar' ]); + + assert.deepEqual(appendedUrl, [ + 'tests/index.html?hidepassed&derp=herp&split=5&loadBalance&partition=1&partition=2&partition=3', + 'tests/index.html?hidepassed&foo=bar&split=5&loadBalance&partition=1&partition=2&partition=3' ]); + }); + + it('should add `loadBalance` to multiple test pages when `replay-execution` and `replay-browser` are used', function() { + const appendedUrl = getCustomBaseUrl({ replayExecution: 'test-execution-0000000.json', replayBrowser: [1, 2] }, [ + 'tests/index.html?hidepassed&derp=herp', + 'tests/index.html?hidepassed&foo=bar' ]); + + assert.deepEqual(appendedUrl, [ + 'tests/index.html?hidepassed&derp=herp&loadBalance', + 'tests/index.html?hidepassed&foo=bar&loadBalance' ]); + }); + }); + + describe('getMultipleTestPages', function() { + it('should have multiple test pages with no partitions specified', function() { + const testPages = getMultipleTestPages( + { testPage: 'tests/index.html?hidepassed' }, + { parallel: true, + split: 2 }); + + assert.deepEqual(testPages, [ + 'tests/index.html?hidepassed&split=2&partition=1', + 'tests/index.html?hidepassed&split=2&partition=2' + ]); + }); + + it('should have multiple test pages with specified partitions', function() { + const testPages = getMultipleTestPages( + { testPage: 'tests/index.html?hidepassed' }, + { parallel: true, + split: 4, + partition: [3, 4]}); + + assert.deepEqual(testPages, [ + 'tests/index.html?hidepassed&split=4&partition=3', + 'tests/index.html?hidepassed&split=4&partition=4' + ]); + }); + + it('should have multiple test pages for each test_page in the config file with no partitions specified', function() { + const testPages = getMultipleTestPages( + { configFile: 'testem.multiple-test-page.js' }, + { parallel: true, + split: 2 }); + + assert.deepEqual(testPages, [ + 'tests/index.html?hidepassed&derp=herp&split=2&partition=1', + 'tests/index.html?hidepassed&derp=herp&split=2&partition=2', + 'tests/index.html?hidepassed&foo=bar&split=2&partition=1', + 'tests/index.html?hidepassed&foo=bar&split=2&partition=2' + ]); + }); + + it('should have multiple test pages for each test_page in the config file with partitions specified', function() { + const testPages = getMultipleTestPages( + { configFile: 'testem.multiple-test-page.js' }, + { parallel: true, + split: 4, + partition: [3, 4] }); + + assert.deepEqual(testPages, [ + 'tests/index.html?hidepassed&derp=herp&split=4&partition=3', + 'tests/index.html?hidepassed&derp=herp&split=4&partition=4', + 'tests/index.html?hidepassed&foo=bar&split=4&partition=3', + 'tests/index.html?hidepassed&foo=bar&split=4&partition=4' + ]); + }); + + it('should have a test page with \'loadBalance\' when no specified number of browser', function() { + const testPages = getMultipleTestPages( + { testPage: 'tests/index.html?hidepassed' }, + { 'loadBalance': 1 }); + + assert.deepEqual(testPages, ['tests/index.html?hidepassed&loadBalance&browser=1']); + }); + + it('should have multiple test page with \'loadBalance\' with splitting when no specified number of browser', function() { + const testPages = getMultipleTestPages( + { testPage: 'tests/index.html?hidepassed' }, + { 'loadBalance': 1, + split: 2 }); + + assert.deepEqual(testPages, [ + 'tests/index.html?hidepassed&split=2&loadBalance&browser=1' + ]); + }); + + it('should have multiple test pages with test loading balanced, no specified partitions and no splitting', function() { + const testPages = getMultipleTestPages( + { testPage: 'tests/index.html?hidepassed' }, + { 'loadBalance': 2 }); + + assert.deepEqual(testPages, [ + 'tests/index.html?hidepassed&loadBalance&browser=1', + 'tests/index.html?hidepassed&loadBalance&browser=2' + ]); + }); + + it('should have multiple test pages with test loading balanced, no specified partitions and no splitting', function() { + const testPages = getMultipleTestPages( + { testPage: 'tests/index.html?hidepassed' }, + { 'loadBalance': 2, + split: 3, + partition: [2, 3] }); + + assert.deepEqual(testPages, [ + 'tests/index.html?hidepassed&split=3&loadBalance&partition=2&partition=3&browser=1', + 'tests/index.html?hidepassed&split=3&loadBalance&partition=2&partition=3&browser=2' + ]); + }); + + it('should have multiple test pages for each test_page in the config file with partitions specified and test loading balanced', function() { + const testPages = getMultipleTestPages( + { configFile: 'testem.multiple-test-page.js' }, + { 'loadBalance': 1, + split: 4, + partition: [3, 4] }); + + assert.deepEqual(testPages, [ + 'tests/index.html?hidepassed&derp=herp&split=4&loadBalance&partition=3&partition=4&browser=1', + 'tests/index.html?hidepassed&foo=bar&split=4&loadBalance&partition=3&partition=4&browser=1' + ]); + }); + }); +}); diff --git a/test-execution-0000000.json b/test-execution-0000000.json index e9ac1a328..723e752f2 100644 --- a/test-execution-0000000.json +++ b/test-execution-0000000.json @@ -1,6 +1,6 @@ { "numberOfBrowsers": 2, - "executionMapping":{ + "browserToModuleMap":{ "1":[ "/tests/integration/components/my-component-test" ], @@ -8,4 +8,4 @@ "/tests/integration/components/navigating-component-test" ] } -} +} diff --git a/testem.simple-test-page.js b/testem.simple-test-page.js new file mode 100644 index 000000000..76741b558 --- /dev/null +++ b/testem.simple-test-page.js @@ -0,0 +1,3 @@ +module.exports = { + 'foo': 'bar' +}; \ No newline at end of file diff --git a/tests/unit/qunit/test-loader-test.js b/tests/unit/qunit/test-loader-test.js index 66a3609f9..d877dc9e1 100644 --- a/tests/unit/qunit/test-loader-test.js +++ b/tests/unit/qunit/test-loader-test.js @@ -19,19 +19,17 @@ module('Unit | test-loader', { 'test-4-test': true, 'test-4-test.jshint': true, }; - this.testLoader = new EmberExamTestLoader(); - this.originalURLParams = EmberExamTestLoader._urlParams; + this.testem = { emit: () => {} }; }, afterEach() { - EmberExamTestLoader._urlParams = this.originalURLParams; window.require = this.originalRequire; } }); test('loads all test modules by default', function(assert) { - this.testLoader._urlParams = {}; - this.testLoader.loadModules(); + const testLoader = new EmberExamTestLoader(this.testem, {}); + testLoader.loadModules(); assert.deepEqual(this.requiredModules, [ 'test-1-test.jshint', @@ -46,11 +44,8 @@ test('loads all test modules by default', function(assert) { }); test('loads modules from a specified partition', function(assert) { - this.testLoader._urlParams = { - partition: 3, - split: 4, - }; - this.testLoader.loadModules(); + const testLoader = new EmberExamTestLoader(this.testem, { partition: 3, split: 4 }); + testLoader.loadModules(); assert.deepEqual(this.requiredModules, [ 'test-3-test.jshint', @@ -59,12 +54,8 @@ test('loads modules from a specified partition', function(assert) { }); test('loads modules from multiple specified partitions', function(assert) { - this.testLoader._urlParams = { - partition: [1, 3], - split: 4, - }; - - this.testLoader.loadModules(); + const testLoader = new EmberExamTestLoader(this.testem, { partition: [1, 3], split: 4 }); + testLoader.loadModules(); assert.deepEqual(this.requiredModules, [ 'test-1-test.jshint', @@ -75,11 +66,8 @@ test('loads modules from multiple specified partitions', function(assert) { }); test('loads modules from the first partition by default', function(assert) { - this.testLoader._urlParams = { - split: 4, - }; - - this.testLoader.loadModules(); + const testLoader = new EmberExamTestLoader(this.testem, { split: 4 }); + testLoader.loadModules(); assert.deepEqual(this.requiredModules, [ 'test-1-test.jshint', @@ -88,12 +76,8 @@ test('loads modules from the first partition by default', function(assert) { }); test('handles params as strings', function(assert) { - this.testLoader._urlParams = { - partition: '3', - split: '4', - }; - - this.testLoader.loadModules(); + const testLoader = new EmberExamTestLoader(this.testem, { partition: '3', split: '4' }); + testLoader.loadModules(); assert.deepEqual(this.requiredModules, [ 'test-3-test.jshint', @@ -102,78 +86,58 @@ test('handles params as strings', function(assert) { }); test('throws an error if splitting less than one', function(assert) { - this.testLoader._urlParams = { - split: 0, - }; + const testLoader = new EmberExamTestLoader(this.testem, { split: 0 }); assert.throws(() => { - this.testLoader.loadModules(); + testLoader.loadModules(); }, /You must specify a split greater than 0/); }); test('throws an error if partition isn\'t a number', function(assert) { - this.testLoader._urlParams = { - split: 2, - partition: 'foo', - }; + const testLoader = new EmberExamTestLoader(this.testem, { split: 2, partition: 'foo' }); assert.throws(() => { - this.testLoader.loadModules(); + testLoader.loadModules(); }, /You must specify numbers for partition \(you specified 'foo'\)/); }); test('throws an error if partition isn\'t a number with multiple partitions', function(assert) { - this.testLoader._urlParams = { - split: 2, - partition: [1, 'foo'], - }; + const testLoader = new EmberExamTestLoader(this.testem, { split: 2, partition: [1, 'foo'] }); assert.throws(() => { - this.testLoader.loadModules(); + testLoader.loadModules(); }, /You must specify numbers for partition \(you specified '1,foo'\)/); }); test('throws an error if loading partition greater than split number', function(assert) { - this.testLoader._urlParams = { - split: 2, - partition: 3, - }; + const testLoader = new EmberExamTestLoader(this.testem, { split: 2, partition: 3 }); assert.throws(() => { - this.testLoader.loadModules(); + testLoader.loadModules(); }, /You must specify partitions numbered less than or equal to your split value/); }); test('throws an error if loading partition greater than split number with multiple partitions', function(assert) { - this.testLoader._urlParams = { - split: 2, - partition: [2, 3], - }; + const testLoader = new EmberExamTestLoader(this.testem, { split: 2, partition: [2, 3] }); assert.throws(() => { - this.testLoader.loadModules(); + testLoader.loadModules(); }, /You must specify partitions numbered less than or equal to your split value/); }); test('throws an error if loading partition less than one', function(assert) { - this.testLoader._urlParams = { - split: 2, - partition: 0, - }; + const testLoader = new EmberExamTestLoader(this.testem, { split: 2, partition: 0 }); assert.throws(() => { - this.testLoader.loadModules(); + testLoader.loadModules(); }, /You must specify partitions numbered greater than 0/); }); test('load works without lint tests', function(assert) { QUnit.urlParams.nolint = true; - this.testLoader._urlParams = { - partition: 4, - split: 4, - }; + const testLoader = new EmberExamTestLoader(this.testem, { partition: 4, split: 4 }); - this.testLoader.loadModules(); + testLoader.loadModules(); assert.deepEqual(this.requiredModules, [ 'test-4-test', @@ -190,12 +154,9 @@ test('load works without non-lint tests', function(assert) { 'test-4-test.jshint': true, }; - this.testLoader._urlParams = { - partition: 4, - split: 4, - }; + const testLoader = new EmberExamTestLoader(this.testem, { partition: 4, split: 4 }); - this.testLoader.loadModules(); + testLoader.loadModules(); assert.deepEqual(this.requiredModules, [ 'test-4-test.jshint', @@ -216,12 +177,9 @@ test('load works with a double-digit single partition', function(assert) { 'test-10-test': true, }; - this.testLoader._urlParams = { - partition: '10', - split: 10, - }; + const testLoader = new EmberExamTestLoader(this.testem, { partition: '10', split: 10 }); - this.testLoader.loadModules(); + testLoader.loadModules(); assert.deepEqual(this.requiredModules, [ 'test-10-test', From 45fad804b9744c7aaec9219911a6be0c7f019b42 Mon Sep 17 00:00:00 2001 From: step2yeung Date: Wed, 2 Jan 2019 15:03:30 -0800 Subject: [PATCH 04/28] Adding testem-event.js and execution-state-manager.js, handle string input for partition & replay-browser, fix TODOs --- .eslintrc.js | 1 + README.md | 10 +- ...st-loader.js => ember-exam-test-loader.js} | 33 +- .../-private/get-test-loader.js | 12 - .../-private/weight-test-modules.js | 2 +- addon-test-support/load.js | 6 +- lib/commands/exam.js | 202 +++-------- lib/utils/config-reader.js | 46 ++- lib/utils/execution-state-manager.js | 190 +++++++++++ lib/utils/query-helper.js | 22 +- lib/utils/test-execution-json-reader.js | 19 -- lib/utils/test-page-helper.js | 118 ++++--- lib/utils/testem-events.js | 155 +++++++++ lib/utils/tests-options-validator.js | 313 ++++++++++-------- node-tests/unit/utils/config-reader-test.js | 2 +- .../utils/execution-state-manager-test.js | 57 ++++ .../unit/utils/test-page-helper-test.js | 10 +- node-tests/unit/utils/testem-events-test.js | 185 +++++++++++ .../utils/tests-options-validator-test.js | 63 +++- test-execution-0000000.json | 11 - tests/test-helper.js | 3 +- tests/unit/mocha/test-loader-test.js | 2 +- tests/unit/qunit/test-loader-test.js | 2 +- yarn.lock | 5 - 24 files changed, 1006 insertions(+), 463 deletions(-) rename addon-test-support/-private/{patch-test-loader.js => ember-exam-test-loader.js} (57%) delete mode 100644 addon-test-support/-private/get-test-loader.js create mode 100644 lib/utils/execution-state-manager.js delete mode 100644 lib/utils/test-execution-json-reader.js create mode 100644 lib/utils/testem-events.js create mode 100644 node-tests/unit/utils/execution-state-manager-test.js create mode 100644 node-tests/unit/utils/testem-events-test.js delete mode 100644 test-execution-0000000.json diff --git a/.eslintrc.js b/.eslintrc.js index 795298849..bd87baa9b 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -27,6 +27,7 @@ module.exports = { 'testem.js', 'testem.multiple-test-page.js', 'testem.no-test-page.js', + 'testem.simple-test-page.js', 'blueprints/*/index.js', 'config/**/*.js', 'tests/dummy/config/**/*.js', diff --git a/README.md b/README.md index 2efed6d2a..86ad44043 100644 --- a/README.md +++ b/README.md @@ -115,10 +115,14 @@ $ ember exam --split= --partition= The `partition` option allows you to specify which test group to run after using the `split` option. It is one-indexed, so if you specify a split of 3, the last group you could run is 3 as well. You can also run multiple partitions, e.g.: ```bash -$ ember exam --split=4 --partition=1 --partition=2 +# comma delimited +$ ember exam --split=4 --partition=1,2 + +# ranged input +$ ember exam --split=4 --partition=2..4 ``` -_Note: Ember Exam splits test by modifying the Ember-CLI `TestLoader`, which means that tests are split up according to AMD modules, so it is possible to have unbalanced partitions. For more info, see [issue #60](https://github.com/trentmwillis/ember-exam/issues/60)._ +_Note: Ember Exam splits test by modifying the Ember-QUnit's `TestLoader`, which means that tests are split up according to AMD modules, so it is possible to have unbalanced partitions. For more info, see [issue #60](https://github.com/trentmwillis/ember-exam/issues/60)._