From fbb51f6d7812548763e5a899260d18ccc8e20722 Mon Sep 17 00:00:00 2001 From: egavr Date: Wed, 9 Aug 2017 16:04:47 +0300 Subject: [PATCH 1/3] feat: running of tests in subprocesses --- lib/base-hermione.js | 35 +++ lib/browser-pool/index.js | 5 +- lib/browser.js | 16 +- lib/cli/index.js | 2 +- lib/config/defaults.js | 1 + lib/config/index.js | 37 ++- lib/config/options.js | 4 +- lib/constants/runner-events.js | 2 - lib/hermione.js | 38 +-- lib/runner/index.js | 48 ++- lib/runner/mocha-runner/index.js | 21 +- lib/runner/mocha-runner/mocha-adapter.js | 254 +++++----------- lib/runner/mocha-runner/retry-mocha-runner.js | 10 +- .../mocha-runner/single-test-mocha-adapter.js | 8 +- lib/worker/browser-agent.js | 21 ++ lib/worker/browser-pool.js | 39 +++ lib/worker/constants/runner-events.js | 13 + lib/worker/hermione.js | 28 ++ lib/worker/index.js | 28 ++ lib/worker/runner/index.js | 46 +++ lib/worker/runner/mocha-runner/index.js | 47 +++ .../runner/mocha-runner/mocha-adapter.js | 275 ++++++++++++++++++ .../runner/mocha-runner/mocha-builder.js | 54 ++++ .../mocha-runner/single-test-mocha-adapter.js | 47 +++ package.json | 3 +- 25 files changed, 815 insertions(+), 267 deletions(-) create mode 100644 lib/base-hermione.js create mode 100644 lib/worker/browser-agent.js create mode 100644 lib/worker/browser-pool.js create mode 100644 lib/worker/constants/runner-events.js create mode 100644 lib/worker/hermione.js create mode 100644 lib/worker/index.js create mode 100644 lib/worker/runner/index.js create mode 100644 lib/worker/runner/mocha-runner/index.js create mode 100644 lib/worker/runner/mocha-runner/mocha-adapter.js create mode 100644 lib/worker/runner/mocha-runner/mocha-builder.js create mode 100644 lib/worker/runner/mocha-runner/single-test-mocha-adapter.js diff --git a/lib/base-hermione.js b/lib/base-hermione.js new file mode 100644 index 000000000..37c7d9856 --- /dev/null +++ b/lib/base-hermione.js @@ -0,0 +1,35 @@ +'use strict'; + +const _ = require('lodash'); +const QEmitter = require('qemitter'); +const pluginsLoader = require('plugins-loader'); + +const Config = require('./config'); +const RunnerEvents = require('./constants/runner-events'); +const WorkerRunnerEvents = require('./worker/constants/runner-events'); + +const PREFIX = require('../package').name + '-'; + +module.exports = class BaseHermione extends QEmitter { + static create(configPath) { + return new this(configPath); + } + + constructor(configPath) { + super(); + + this._config = Config.create(configPath); + } + + get config() { + return this._config; + } + + get events() { + return _.extend({}, RunnerEvents, WorkerRunnerEvents); + } + + _loadPlugins() { + pluginsLoader.load(this, this.config.plugins, PREFIX); + } +}; diff --git a/lib/browser-pool/index.js b/lib/browser-pool/index.js index 0a3df3b5d..538fe7ada 100644 --- a/lib/browser-pool/index.js +++ b/lib/browser-pool/index.js @@ -10,10 +10,7 @@ exports.create = function(config, emitter) { create: (id) => Browser.create(config, id), start: (browser) => browser.init(), - onStart: (browser) => { - emitter.emit(Events.NEW_BROWSER, browser.publicAPI, {browserId: browser.id}); - return emitSessionEvent(emitter, browser, Events.SESSION_START); - }, + onStart: (browser) => emitSessionEvent(emitter, browser, Events.SESSION_START), onQuit: (browser) => emitSessionEvent(emitter, browser, Events.SESSION_END), quit: (browser) => browser.quit() diff --git a/lib/browser.js b/lib/browser.js index b5690dd8b..b7d58a6dc 100644 --- a/lib/browser.js +++ b/lib/browser.js @@ -19,14 +19,14 @@ module.exports = class Browser { this._config = config.forBrowser(this.id); this._debug = config.system.debug; - this._session = null; + this._session = this._createSession(); this._meta = _.extend({}, this._config.meta); signalHandler.on('exit', () => this.quit()); } init() { - return q(() => this._session = this._createSession()) + return q(() => this._session) .call() .then(() => this._setHttpTimeout(this._config.sessionRequestTimeout)) .then(() => this._session.init()) @@ -40,11 +40,7 @@ module.exports = class Browser { } quit() { - if (!this._session) { - return q(); - } - - // Не работает без then в виду особенностей реализации в webdriverio.js + // Do not work without 'then' because of webdriverio realization of promise API return this._session .then(() => this._setHttpTimeout(this._config.sessionQuitTimeout)) .then(() => this._switchOffScreenshotOnReject()) @@ -57,7 +53,11 @@ module.exports = class Browser { } get sessionId() { - return _.get(this, '_session.requestHandler.sessionID'); + return this.publicAPI.requestHandler.sessionID; + } + + set sessionId(id) { + this.publicAPI.requestHandler.sessionID = id; } get meta() { diff --git a/lib/cli/index.js b/lib/cli/index.js index fb25ef0da..df6ec6c92 100644 --- a/lib/cli/index.js +++ b/lib/cli/index.js @@ -35,7 +35,7 @@ function collect(newValue, array) { } function runHermione() { - return q.try(() => Hermione.create(program.config || defaults.config, {cli: true, env: true})) + return q.try(() => Hermione.create(program.config || defaults.config)) .then((hermione) => { return hermione.run(program.args, { reporters: program.reporter || defaults.reporters, diff --git a/lib/config/defaults.js b/lib/config/defaults.js index ac6760356..0c9d20293 100644 --- a/lib/config/defaults.js +++ b/lib/config/defaults.js @@ -18,6 +18,7 @@ module.exports = { debug: false, sessionsPerBrowser: 1, testsPerSession: Infinity, + workers: 1, retry: 0, mochaOpts: { slow: 10000, diff --git a/lib/config/index.js b/lib/config/index.js index 294553fa6..c22958a8f 100644 --- a/lib/config/index.js +++ b/lib/config/index.js @@ -6,8 +6,8 @@ const logger = require('../utils').logger; const parseOptions = require('./options'); module.exports = class Config { - static create(configPath, allowOverrides) { - return new Config(configPath, allowOverrides); + static create(configPath) { + return new Config(configPath); } static read(configPath) { @@ -29,20 +29,13 @@ module.exports = class Config { } } - constructor(config, allowOverrides) { - allowOverrides = _.defaults(allowOverrides || {}, { - env: false, - cli: false - }); - - if (_.isString(config)) { - config = Config.read(config); - } + constructor(configPath) { + this.configPath = configPath; _.extend(this, parseOptions({ - options: config, - env: allowOverrides.env ? process.env : {}, - argv: allowOverrides.cli ? process.argv : [] + options: Config.read(configPath), + env: process.env, + argv: process.argv })); } @@ -53,4 +46,20 @@ module.exports = class Config { getBrowserIds() { return _.keys(this.browsers); } + + /** + * This method is used in subrocesses to merge a created config + * in a a subrocess with a config from the main process + */ + mergeWith(config) { + _.mergeWith(this, config, (l, r) => { + if (_.isObjectLike(l)) { + return; + } + + // When passing stringified config from the master to workers + // all functions are transformed to strings and all regular expressions to empty objects + return typeof l === typeof r ? r : l; + }); + } }; diff --git a/lib/config/options.js b/lib/config/options.js index bce7fb358..52717bbed 100644 --- a/lib/config/options.js +++ b/lib/config/options.js @@ -28,7 +28,9 @@ const rootSection = section(_.extend(browserOptions.getTopLevel(), { ctx: options.anyObject(), - patternsOnReject: options.optionalArray('patternsOnReject') + patternsOnReject: options.optionalArray('patternsOnReject'), + + workers: options.positiveInteger('workers') }), plugins: options.anyObject(), diff --git a/lib/constants/runner-events.js b/lib/constants/runner-events.js index fff06f101..992c42356 100644 --- a/lib/constants/runner-events.js +++ b/lib/constants/runner-events.js @@ -18,8 +18,6 @@ const getSyncEvents = () => ({ BEFORE_FILE_READ: 'beforeFileRead', AFTER_FILE_READ: 'afterFileRead', - NEW_BROWSER: 'newBrowser', - SUITE_BEGIN: 'beginSuite', SUITE_END: 'endSuite', diff --git a/lib/hermione.js b/lib/hermione.js index c80d38ae8..9458d0a35 100644 --- a/lib/hermione.js +++ b/lib/hermione.js @@ -1,42 +1,24 @@ 'use strict'; const _ = require('lodash'); -const QEmitter = require('qemitter'); const qUtils = require('qemitter/utils'); -const pluginsLoader = require('plugins-loader'); +const BaseHermione = require('./base-hermione'); +const Runner = require('./runner'); const RunnerEvents = require('./constants/runner-events'); const signalHandler = require('./signal-handler'); -const Runner = require('./runner'); -const Config = require('./config'); const sets = require('./sets'); const validateUnknownBrowsers = require('./validators').validateUnknownBrowsers; -const PREFIX = require('../package').name + '-'; - -module.exports = class Hermione extends QEmitter { - static create(configPath, allowOverrides) { - return new Hermione(configPath, allowOverrides); - } - - constructor(configPath, allowOverrides) { - super(); - - this._config = Config.create(configPath, allowOverrides); +module.exports = class Hermione extends BaseHermione { + constructor(configPath) { + super(configPath); this._failed = false; } - get config() { - return this._config; - } - - get events() { - return _.clone(RunnerEvents); - } - run(testPaths, options) { - options = _.extend({}, options, {paths: testPaths}); + options = options || {}; validateUnknownBrowsers(options.browsers, _.keys(this._config.browsers)); @@ -54,7 +36,7 @@ module.exports = class Hermione extends QEmitter { _.extend(this._config.system.mochaOpts, {grep: options.grep}); - return sets.reveal(this._config.sets, options) + return sets.reveal(this._config.sets, {paths: testPaths, browsers: options.browsers, sets: options.sets}) .then((testFiles) => runner.run(testFiles)) .then(() => !this.isFailed()); } @@ -68,17 +50,13 @@ module.exports = class Hermione extends QEmitter { options.loadPlugins && qUtils.passthroughEvent(runner, this, _.values(RunnerEvents.getSync())); return sets.reveal(this._config.sets, {paths: testPaths, browsers}) - .then((tests) => runner.buildSuiteTree(tests)); + .then((testFiles) => runner.buildSuiteTree(testFiles)); } isFailed() { return this._failed; } - _loadPlugins() { - pluginsLoader.load(this, this.config.plugins, PREFIX); - } - _fail() { this._failed = true; } diff --git a/lib/runner/index.js b/lib/runner/index.js index 8357360c3..e053a8259 100644 --- a/lib/runner/index.js +++ b/lib/runner/index.js @@ -1,9 +1,11 @@ 'use strict'; const _ = require('lodash'); +const q = require('q'); const utils = require('q-promise-utils'); const QEmitter = require('qemitter'); const qUtils = require('qemitter/utils'); +const workerFarm = require('worker-farm'); const BrowserPool = require('../browser-pool'); const RunnerEvents = require('../constants/runner-events'); @@ -26,8 +28,8 @@ module.exports = class MainRunner extends QEmitter { MochaRunner.prepare(); } - buildSuiteTree(tests) { - return _.mapValues(tests, (files, browserId) => { + buildSuiteTree(testFiles) { + return _.mapValues(testFiles, (files, browserId) => { const mochaRunner = MochaRunner.create(browserId, this._config, this._browserPool, this._testSkipper); qUtils.passthroughEvent(mochaRunner, this, [ @@ -39,22 +41,48 @@ module.exports = class MainRunner extends QEmitter { }); } - run(tests) { - return this.emitAndWait(RunnerEvents.RUNNER_START, this) + run(testFiles) { + const workers = this._createWorkerFarm(); + + return q + .all([ + this.emitAndWait(RunnerEvents.RUNNER_START, this), + q.ninvoke(workers, 'init', testFiles, this._config.configPath) + ]) + .then(() => q.ninvoke(workers, 'syncConfig', this._config)) .then(() => { - const mochaRunners = this._initMochaRunners(tests); + const mochaRunners = this._initMochaRunners(testFiles); this.emit(RunnerEvents.BEGIN); - return mochaRunners.map((mochaRunner) => mochaRunner.run()); + return _.map(mochaRunners, (mochaRunner) => mochaRunner.run(workers)); }) .then(utils.waitForResults) - .fin(() => this.emitAndWait(RunnerEvents.RUNNER_END).catch(logger.warn)); + .finally(() => { + workers && workerFarm.end(workers); + + return this.emitAndWait(RunnerEvents.RUNNER_END).catch(logger.warn); + }); + } + + _createWorkerFarm() { + const workerFilepath = require.resolve('../worker'); + const params = { + maxConcurrentWorkers: this._config.system.workers, + maxConcurrentCallsPerWorker: Infinity, + autoStart: true + }; + + return workerFarm(params, workerFilepath, [ + {name: 'init', broadcast: true}, + {name: 'syncConfig', broadcast: true}, + 'runTest' + ]); } - _initMochaRunners(tests) { - return _.map(tests, (files, browserId) => this._initMochaRunner(browserId).init(files)); + _initMochaRunners(testFiles) { + return _.mapValues(testFiles, (files, browserId) => this._createMochaRunner(browserId).init(files)); } - _initMochaRunner(browserId) { + _createMochaRunner(browserId) { const mochaRunner = MochaRunner.create(browserId, this._config, this._browserPool, this._testSkipper); qUtils.passthroughEvent(mochaRunner, this, _.values(RunnerEvents.getSync())); diff --git a/lib/runner/mocha-runner/index.js b/lib/runner/mocha-runner/index.js index ee825bab2..bf9eed89d 100644 --- a/lib/runner/mocha-runner/index.js +++ b/lib/runner/mocha-runner/index.js @@ -1,15 +1,15 @@ 'use strict'; +const EventEmitter = require('events').EventEmitter; const path = require('path'); const utils = require('q-promise-utils'); const qUtils = require('qemitter/utils'); -const QEmitter = require('qemitter'); const _ = require('lodash'); const RunnerEvents = require('../../constants/runner-events'); const RetryMochaRunner = require('./retry-mocha-runner'); const MochaBuilder = require('./mocha-builder'); -module.exports = class MochaRunner extends QEmitter { +module.exports = class MochaRunner extends EventEmitter { static prepare() { MochaBuilder.prepare(); } @@ -30,31 +30,30 @@ module.exports = class MochaRunner extends QEmitter { ]); } - buildSuiteTree(suitePaths) { - const mocha = this._mochaBuilder.buildSingleAdapter(suitePaths); + buildSuiteTree(filepaths) { + const mocha = this._mochaBuilder.buildSingleAdapter(filepaths); validateUniqTitles(mocha); return mocha.suite; } - init(suitePaths) { - this._mochas = this._mochaBuilder.buildAdapters(suitePaths); + init(filepaths) { + this._mochas = this._mochaBuilder.buildAdapters(filepaths); validateUniqTitles(this._mochas); - this._mochas.forEach((mocha) => mocha.disableHooksInSkippedSuites()); return this; } - run() { + run(workers) { return _(this._mochas) - .map((mocha) => this._runMocha(mocha)) + .map((mocha) => this._runMocha(mocha, workers)) .thru(utils.waitForResults) .value(); } - _runMocha(mocha) { + _runMocha(mocha, workers) { const retryMochaRunner = RetryMochaRunner.create(mocha, this._config); qUtils.passthroughEvent(mocha, this, [ @@ -77,7 +76,7 @@ module.exports = class MochaRunner extends QEmitter { RunnerEvents.ERROR ]); - return retryMochaRunner.run(); + return retryMochaRunner.run(workers); } }; diff --git a/lib/runner/mocha-runner/mocha-adapter.js b/lib/runner/mocha-runner/mocha-adapter.js index 542409b5f..edfa3356e 100644 --- a/lib/runner/mocha-runner/mocha-adapter.js +++ b/lib/runner/mocha-runner/mocha-adapter.js @@ -1,17 +1,17 @@ 'use strict'; -const ProxyReporter = require('./proxy-reporter'); -const logger = require('../../utils').logger; -const Skip = require('./skip'); -const SkipBuilder = require('./skip/skip-builder'); -const OnlyBuilder = require('./skip/only-builder'); -const RunnerEvents = require('../../constants/runner-events'); -const Mocha = require('mocha'); +const EventEmitter = require('events').EventEmitter; const path = require('path'); const clearRequire = require('clear-require'); -const q = require('q'); const _ = require('lodash'); -const EventEmitter = require('events').EventEmitter; +const Mocha = require('mocha'); +const q = require('q'); +const RunnerEvents = require('../../constants/runner-events'); +const Skip = require('./skip'); +const SkipBuilder = require('./skip/skip-builder'); +const OnlyBuilder = require('./skip/only-builder'); +const ProxyReporter = require('./proxy-reporter'); +const logger = require('../../utils').logger; // Avoid mochajs warning about possible EventEmitter memory leak // https://nodejs.org/docs/latest/api/events.html#events_emitter_setmaxlisteners_n @@ -31,9 +31,8 @@ module.exports = class MochaAdapter extends EventEmitter { super(); this._mochaOpts = config.mochaOpts; - this._patternsOnReject = initPatternsOnReject(config.patternsOnReject); + this._patternsOnReject = config.patternsOnReject.map((p) => new RegExp(p)); this._browserAgent = browserAgent; - this._brokenSession = false; this._browser = null; this._initMocha(); @@ -44,24 +43,22 @@ module.exports = class MochaAdapter extends EventEmitter { _initMocha() { this._mocha = new Mocha(this._mochaOpts); this._mocha.fullTrace(); - this.suite = this._mocha.suite; + this.suite = this._mocha.suite; this.suite.setMaxListeners(0); + // setting of timeouts can lead to conflicts with timeouts which appear in subprocesses + this.suite.enableTimeouts(false); this._addEventHandler('suite', (suite) => suite.setMaxListeners(0)); this.tests = []; + this._brokenSession = false; + this._errMonitor = new EventEmitter(); - this._replaceTimeouts(); - this._injectBeforeHookErrorHandling(); - this._injectBeforeEachHookErrorHandling(); + this._attachSessionValidation(); this._injectBrowser(); - this._injectPretestFailVerification(); - this._injectExecutionContext(); + this._removeHooks(); // should be executed after injecting of a browser this._injectSkip(); - this._attachSessionValidation(); this._passthroughMochaEvents(); - - this._errMonitor = new EventEmitter(); } _attachSessionValidation() { @@ -70,35 +67,39 @@ module.exports = class MochaAdapter extends EventEmitter { }); } - applySkip(testSkipper) { - testSkipper.applySkip(this.suite, this._browserAgent.browserId); + _injectBrowser() { + this.suite.beforeAll(() => this._requestBrowser()); + this.suite.afterAll(() => this._freeBrowser()); + } - return this; + _requestBrowser() { + return this._browserAgent.getBrowser() + .then((browser) => this._browser = browser) + .catch((e) => markSuiteAsFailed(this.suite, e)); } - loadFiles(files) { - [].concat(files).forEach((filename) => { - clearRequire(path.resolve(filename)); - this._mocha.addFile(filename); - }); + _freeBrowser() { + return this._browser + && this._browserAgent.freeBrowser(this._browser, {force: this._brokenSession}) + .catch((e) => logger.warn('WARNING: can not release browser: ' + e)); + } - this._mocha.loadFiles(); - this._mocha.files = []; + _removeHooks() { + this._addEventHandler('beforeAll', (hook) => hook.parent._beforeAll.pop()); + this._addEventHandler('afterAll', (hook) => hook.parent._afterAll.pop()); - return this; + this._addEventHandler('beforeEach', (hook) => hook.parent._beforeEach.pop()); + this._addEventHandler('afterEach', (hook) => hook.parent._afterEach.pop()); } - attachTestFilter(shouldRunTest) { - this._addEventHandler('test', (test) => { - if (shouldRunTest(test)) { - this.tests.push(test); - return; - } + _injectSkip() { + const skip = new Skip(); + const skipBuilder = new SkipBuilder(skip, this._browserAgent.browserId); + const onlyBuilder = new OnlyBuilder(skipBuilder); - test.parent.tests.pop(); - }); + _.extend(global.hermione, {skip: skipBuilder, only: onlyBuilder}); - return this; + this._addEventHandler(['suite', 'test'], (runnable) => skip.handleEntity(runnable)); } _passthroughMochaEvents() { @@ -120,6 +121,10 @@ module.exports = class MochaAdapter extends EventEmitter { this._mocha.reporter(Reporter); } + _getBrowser() { + return this._browser || {id: this._browserAgent.browserId}; + } + _passthroughFileEvents(emit) { const emit_ = (event, file) => emit(event, { file, @@ -134,102 +139,62 @@ module.exports = class MochaAdapter extends EventEmitter { return this; } - run() { - const defer = q.defer(); - - this._errMonitor.on('err', (err) => defer.reject(err)); - this._mocha.run((failed) => defer.resolve({failed})); - - return defer.promise; - } - reinit() { this._initMocha(); } - _injectSkip() { - const skip = new Skip(); - const skipBuilder = new SkipBuilder(skip, this._browserAgent.browserId); - const onlyBuilder = new OnlyBuilder(skipBuilder); - - _.extend(global.hermione, {skip: skipBuilder, only: onlyBuilder}); - - this._addEventHandler(['suite', 'test'], (runnable) => skip.handleEntity(runnable)); - } + applySkip(testSkipper) { + testSkipper.applySkip(this.suite, this._browserAgent.browserId); - _injectExecutionContext() { - const browserId = this._browserAgent.browserId; - - this._addEventHandler( - ['beforeAll', 'beforeEach', 'test', 'afterEach', 'afterAll'], - this._overrideRunnableFn((runnable, baseFn) => { - const _this = this; - return function() { - const browser = _this.suite.ctx.browser; - if (browser) { - Object.getPrototypeOf(browser).executionContext = _.extend(runnable, {browserId}); - } - return baseFn.apply(this, arguments); - }; - }) - ); + return this; } - _replaceTimeouts() { - this._addEventHandler(['beforeAll', 'beforeEach', 'test', 'afterEach', 'afterAll'], (runnable) => { - if (!runnable.enableTimeouts()) { + attachTestFilter(shouldRunTest) { + this._addEventHandler('test', (test) => { + if (shouldRunTest(test)) { + this.tests.push(test); return; } - const baseFn = runnable.fn; - const timeout = runnable.timeout() || Infinity; - - runnable.enableTimeouts(false); - runnable.fn = function() { - return q(baseFn).apply(this, arguments).timeout(timeout); - }; + test.parent.tests.pop(); }); - } - _injectBeforeHookErrorHandling() { - this._injectHookErrorHandling('beforeAll', (error, hook) => markSuiteAsFailed(hook.parent, error)); + return this; } - _injectBeforeEachHookErrorHandling() { - this._injectHookErrorHandling('beforeEach', (error, hook) => markTestAsFailed(hook.ctx.currentTest, error)); - } + loadFiles(files) { + [].concat(files).forEach((filename) => { + clearRequire(path.resolve(filename)); + this._mocha.addFile(filename); + }); - _injectHookErrorHandling(event, onError) { - this._addEventHandler(event, this._overrideRunnableFn((hook, baseFn) => { - return function() { - const previousBeforeAllHookFail = hook.parent.fail; - const previousBeforeEachHookFail = _.get(hook, 'ctx.currentTest.fail'); - const previousFail = previousBeforeAllHookFail || previousBeforeEachHookFail; + this._mocha.loadFiles(); + this._mocha.files = []; - return previousFail - ? onError(previousFail, hook) - : q(baseFn).apply(this, arguments).catch((error) => onError(error, hook)); - }; - })); + return this; } - _injectPretestFailVerification() { - this._addEventHandler('test', this._overrideRunnableFn((test, baseFn) => { - return function() { - return test.fail - ? q.reject(test.fail) - : baseFn.apply(this, arguments); + run(workers) { + this.suite.eachTest((test) => { + test.fn = () => { + if (test.fail) { + return q.reject(test.fail); + } + + return this._runTest(test.fullTitle(), workers); }; - })); + }); + + const defer = q.defer(); + + this._errMonitor.on('err', (err) => defer.reject(err)); + this._mocha.run(() => defer.resolve()); + + return defer.promise; } - _overrideRunnableFn(overrideFn) { - return (runnable) => { - const baseFn = runnable.fn; - if (baseFn) { - runnable.fn = overrideFn(runnable, baseFn); - } - }; + _runTest(fullTitle, workers) { + return q.ninvoke(workers, 'runTest', fullTitle, {browserId: this._browser.id, sessionId: this._browser.sessionId}); } // Set recursive handler for events triggered by mocha while parsing test file @@ -243,63 +208,8 @@ module.exports = class MochaAdapter extends EventEmitter { listenSuite(this.suite); } - - _injectBrowser() { - const savedEnableTimeouts = this.suite.enableTimeouts(); - - this.suite.enableTimeouts(false); - - this.suite.beforeAll(() => this._requestBrowser()); - this.suite.afterAll(() => this._freeBrowser()); - - this.suite.enableTimeouts(savedEnableTimeouts); - } - - _requestBrowser() { - return this._browserAgent.getBrowser() - .then((browser) => { - this._browser = browser; - - this.suite.ctx.browser = browser.publicAPI; - }); - } - - _freeBrowser() { - return this._browser - && this._browserAgent.freeBrowser(this._browser, {force: this._brokenSession}) - .catch((e) => logger.warn('WARNING: can not release browser: ' + e)); - } - - _getBrowser() { - return this._browser || {id: this._browserAgent.browserId}; - } - - disableHooksInSkippedSuites(suite) { - suite = suite || this.suite; - - if (isSkipped(suite)) { - disableSuiteHooks(suite); - } else { - suite.suites.forEach((s) => this.disableHooksInSkippedSuites(s)); - } - } }; -function initPatternsOnReject(patternsOnReject) { - return patternsOnReject.map((p) => new RegExp(p)); -} - -function isSkipped(suite) { - return _.every(suite.suites, (s) => isSkipped(s)) - && _.every(suite.tests, 'pending'); -} - -function disableSuiteHooks(suite) { - suite._beforeAll = []; - suite._afterAll = []; - suite.suites.forEach((s) => disableSuiteHooks(s)); -} - function markSuiteAsFailed(suite, error) { suite.fail = error; eachSuiteAndTest(suite, (runnable) => runnable.fail = error); @@ -312,7 +222,3 @@ function eachSuiteAndTest(runnable, cb) { eachSuiteAndTest(suite, cb); }); } - -function markTestAsFailed(test, error) { - test.fail = error; -} diff --git a/lib/runner/mocha-runner/retry-mocha-runner.js b/lib/runner/mocha-runner/retry-mocha-runner.js index e9524c407..06f83c202 100644 --- a/lib/runner/mocha-runner/retry-mocha-runner.js +++ b/lib/runner/mocha-runner/retry-mocha-runner.js @@ -51,21 +51,21 @@ module.exports = class RetryMochaRunner extends EventEmitter { this.emit(RunnerEvents.RETRY, _.extend(failed, {retriesLeft: this._retriesLeft - 1})); } - run() { + run(workers) { this._shouldRetry = false; - return this._mocha.run() - .then(() => this._retry()); + return this._mocha.run(workers) + .then(() => this._retry(workers)); } - _retry() { + _retry(workers) { if (!this._shouldRetry) { return; } ++this._retriesPerformed; this._mocha.reinit(); - return this.run(); + return this.run(workers); } _hasRetriesLeft() { diff --git a/lib/runner/mocha-runner/single-test-mocha-adapter.js b/lib/runner/mocha-runner/single-test-mocha-adapter.js index e5a731403..c2fc35174 100644 --- a/lib/runner/mocha-runner/single-test-mocha-adapter.js +++ b/lib/runner/mocha-runner/single-test-mocha-adapter.js @@ -25,8 +25,8 @@ module.exports = class SingleTestMochaAdapter { return this._mocha.suite; } - run() { - return this._mocha.run(); + run(workers) { + return this._mocha.run(workers); } reinit() { @@ -46,8 +46,4 @@ module.exports = class SingleTestMochaAdapter { on() { return this._mocha.on.apply(this._mocha, arguments); } - - disableHooksInSkippedSuites() { - return this._mocha.disableHooksInSkippedSuites(); - } }; diff --git a/lib/worker/browser-agent.js b/lib/worker/browser-agent.js new file mode 100644 index 000000000..cceff948f --- /dev/null +++ b/lib/worker/browser-agent.js @@ -0,0 +1,21 @@ +'use strict'; + +module.exports = class BrowserAgent { + static create(browserId, pool) { + return new BrowserAgent(browserId, pool); + } + + constructor(browserId, pool) { + this.browserId = browserId; + + this._pool = pool; + } + + getBrowser(sessionId) { + return this._pool.getBrowser(this.browserId, sessionId); + } + + freeBrowser(browser) { + this._pool.freeBrowser(browser); + } +}; diff --git a/lib/worker/browser-pool.js b/lib/worker/browser-pool.js new file mode 100644 index 000000000..0e7a52d21 --- /dev/null +++ b/lib/worker/browser-pool.js @@ -0,0 +1,39 @@ +'use strict'; + +const _ = require('lodash'); +const Browser = require('../browser'); +const RunnerEvents = require('./constants/runner-events'); + +module.exports = class BrowserPool { + static create(config, emitter) { + return new BrowserPool(config, emitter); + } + + constructor(config, emitter) { + this._config = config; + this._emitter = emitter; + + this._browsers = {}; + } + + getBrowser(browserId, sessionId) { + this._browsers[browserId] = this._browsers[browserId] || []; + + let browser = _.find(this._browsers[browserId], (browser) => !browser.sessionId); + + if (!browser) { + browser = Browser.create(this._config, browserId); + this._browsers[browserId].push(browser); + + this._emitter.emit(RunnerEvents.NEW_BROWSER, browser.publicAPI, {browserId}); + } + + browser.sessionId = sessionId; + + return browser; + } + + freeBrowser(browser) { + browser.sessionId = null; + } +}; diff --git a/lib/worker/constants/runner-events.js b/lib/worker/constants/runner-events.js new file mode 100644 index 000000000..946b1160a --- /dev/null +++ b/lib/worker/constants/runner-events.js @@ -0,0 +1,13 @@ +'use strict'; + +const MainProcessRunnerEvents = require('../../constants/runner-events'); + +module.exports = { + BEFORE_FILE_READ: MainProcessRunnerEvents.BEFORE_FILE_READ, + AFTER_FILE_READ: MainProcessRunnerEvents.AFTER_FILE_READ, + + TEST_FAIL: MainProcessRunnerEvents.TEST_FAIL, + ERROR: MainProcessRunnerEvents.ERROR, + + NEW_BROWSER: 'newBrowser' +}; diff --git a/lib/worker/hermione.js b/lib/worker/hermione.js new file mode 100644 index 000000000..3ee5e2c19 --- /dev/null +++ b/lib/worker/hermione.js @@ -0,0 +1,28 @@ +'use strict'; + +const qUtils = require('qemitter/utils'); + +const RunnerEvents = require('./constants/runner-events'); +const Runner = require('./runner'); +const BaseHermione = require('../base-hermione'); + +module.exports = class Hermione extends BaseHermione { + init(testFiles) { + this._loadPlugins(); + + this._runner = Runner.create(this._config); + + qUtils.passthroughEvent(this._runner, this, [ + RunnerEvents.BEFORE_FILE_READ, + RunnerEvents.AFTER_FILE_READ, + + RunnerEvents.NEW_BROWSER + ]); + + this._runner.init(testFiles); + } + + runTest(fullTitle, options) { + return this._runner.runTest(fullTitle, options); + } +}; diff --git a/lib/worker/index.js b/lib/worker/index.js new file mode 100644 index 000000000..943f49190 --- /dev/null +++ b/lib/worker/index.js @@ -0,0 +1,28 @@ +'use strict'; + +const Hermione = require('./hermione'); + +let hermione; + +exports.init = (testFiles, configPath, cb) => { + try { + hermione = Hermione.create(configPath); + + hermione.init(testFiles); + cb(); + } catch (err) { + cb(err); + } +}; + +exports.syncConfig = (config, cb) => { + hermione.config.mergeWith(config); + + cb(); +}; + +exports.runTest = (fullTitle, options, cb) => { + hermione.runTest(fullTitle, options) + .then(() => cb()) + .catch((err) => cb(err)); +}; diff --git a/lib/worker/runner/index.js b/lib/worker/runner/index.js new file mode 100644 index 000000000..1553e2508 --- /dev/null +++ b/lib/worker/runner/index.js @@ -0,0 +1,46 @@ +'use strict'; + +const _ = require('lodash'); +const QEmitter = require('qemitter'); +const qUtils = require('qemitter/utils'); +const BrowserPool = require('../browser-pool'); +const RunnerEvents = require('../constants/runner-events'); +const MochaRunner = require('./mocha-runner'); + +module.exports = class MainRunner extends QEmitter { + static create(config) { + return new MainRunner(config); + } + + constructor(config) { + super(); + + this._config = config; + this._browserPool = BrowserPool.create(this._config, this); + + MochaRunner.prepare(); + } + + init(testPaths) { + this._mochaRunners = _.mapValues(testPaths, (files, browserId) => { + return this._createMochaRunner(browserId).init(files); + }); + } + + _createMochaRunner(browserId) { + const mochaRunner = MochaRunner.create(browserId, this._config, this._browserPool); + + qUtils.passthroughEvent(mochaRunner, this, [ + RunnerEvents.BEFORE_FILE_READ, + RunnerEvents.AFTER_FILE_READ, + + RunnerEvents.NEW_BROWSER + ]); + + return mochaRunner; + } + + runTest(fullTitle, options) { + return this._mochaRunners[options.browserId].runTest(fullTitle, options.sessionId); + } +}; diff --git a/lib/worker/runner/mocha-runner/index.js b/lib/worker/runner/mocha-runner/index.js new file mode 100644 index 000000000..11e64c170 --- /dev/null +++ b/lib/worker/runner/mocha-runner/index.js @@ -0,0 +1,47 @@ +'use strict'; + +const EventEmitter = require('events').EventEmitter; +const _ = require('lodash'); +const qUtils = require('qemitter/utils'); + +const RunnerEvents = require('../../constants/runner-events'); +const MochaBuilder = require('./mocha-builder'); + +module.exports = class MochaRunner extends EventEmitter { + static prepare() { + MochaBuilder.prepare(); + } + + static create(browserId, config, browserPool) { + return new MochaRunner(browserId, config, browserPool); + } + + constructor(browserId, config, browserPool) { + super(); + + this._config = config.forBrowser(browserId); + this._mochaBuilder = MochaBuilder.create(browserId, config.system, browserPool); + + qUtils.passthroughEvent(this._mochaBuilder, this, [ + RunnerEvents.BEFORE_FILE_READ, + RunnerEvents.AFTER_FILE_READ + ]); + } + + init(filepaths) { + this._mochas = this._mochaBuilder.buildAdapters(filepaths); + + return this; + } + + runTest(fullTitle, sessionId) { + const index = _.findIndex(this._mochas, (mocha) => { + const titles = mocha ? mocha.tests.map((test) => test.fullTitle()) : []; + + return _.includes(titles, fullTitle); + }); + + return this._mochas[index].runInSession(sessionId) + .then(() => this._mochas[index] = null); // leave instance for retries in case of error + } +}; diff --git a/lib/worker/runner/mocha-runner/mocha-adapter.js b/lib/worker/runner/mocha-runner/mocha-adapter.js new file mode 100644 index 000000000..191699cbc --- /dev/null +++ b/lib/worker/runner/mocha-runner/mocha-adapter.js @@ -0,0 +1,275 @@ +'use strict'; + +const EventEmitter = require('events').EventEmitter; +const path = require('path'); +const clearRequire = require('clear-require'); +const _ = require('lodash'); +const Mocha = require('mocha'); +const q = require('q'); +const RunnerEvents = require('../../../constants/runner-events'); +const Skip = require('../../../runner/mocha-runner/skip'); +const SkipBuilder = require('../../../runner/mocha-runner/skip/skip-builder'); +const OnlyBuilder = require('../../../runner/mocha-runner/skip/only-builder'); +const ProxyReporter = require('../../../runner/mocha-runner/proxy-reporter'); + +// Avoid mochajs warning about possible EventEmitter memory leak +// https://nodejs.org/docs/latest/api/events.html#events_emitter_setmaxlisteners_n +// Reason: each mocha runner sets 'uncaughtException' listener +process.setMaxListeners(0); + +module.exports = class MochaAdapter extends EventEmitter { + static prepare() { + global.hermione = {}; + } + + static create(browserAgent, config) { + return new MochaAdapter(browserAgent, config); + } + + constructor(browserAgent, config) { + super(); + + this._mochaOpts = config.mochaOpts; + this._browserAgent = browserAgent; + this._browser = null; + + this._initMocha(); + + _.extend(global.hermione, {ctx: _.clone(config.ctx)}); + } + + _initMocha() { + this._mocha = new Mocha(this._mochaOpts); + this._mocha.fullTrace(); + + this.suite = this._mocha.suite; + this.suite.setMaxListeners(0); + this._addEventHandler('suite', (suite) => suite.setMaxListeners(0)); + + this.tests = []; + this._fail = null; + this._errMonitor = new EventEmitter(); + + this._replaceTimeouts(); + this._injectBeforeHookErrorHandling(); + this._injectBeforeEachHookErrorHandling(); + this._injectPretestFailVerification(); + this._injectBrowser(); + this._injectSkip(); + this._injectExecutionContext(); + this._passthroughMochaEvents(); + } + + _replaceTimeouts() { + this._addEventHandler(['beforeAll', 'beforeEach', 'test', 'afterEach', 'afterAll'], (runnable) => { + if (!runnable.enableTimeouts()) { + return; + } + + const baseFn = runnable.fn; + const timeout = runnable.timeout() || Infinity; + + runnable.enableTimeouts(false); + runnable.fn = function() { + return q(baseFn).apply(this, arguments).timeout(timeout); + }; + }); + } + + _injectBeforeHookErrorHandling() { + this._injectHookErrorHandling('beforeAll', (error, hook) => markSuiteAsFailed(hook.parent, error)); + } + + _injectBeforeEachHookErrorHandling() { + this._injectHookErrorHandling('beforeEach', (error, hook) => markTestAsFailed(hook.ctx.currentTest, error)); + } + + _injectHookErrorHandling(event, onError) { + this._addEventHandler(event, this._overrideRunnableFn((hook, baseFn) => { + return function() { + const previousBeforeAllHookFail = hook.parent.fail; + const previousBeforeEachHookFail = _.get(hook, 'ctx.currentTest.fail'); + const previousFail = previousBeforeAllHookFail || previousBeforeEachHookFail; + + return previousFail + ? onError(previousFail, hook) + : q(baseFn).apply(this, arguments).catch((error) => onError(error, hook)); + }; + })); + } + + _injectPretestFailVerification() { + this._addEventHandler('test', this._overrideRunnableFn((test, baseFn) => { + return function() { + return test.fail + ? q.reject(test.fail) + : baseFn.apply(this, arguments); + }; + })); + } + + _injectBrowser() { + this.suite.beforeAll(() => this._requestBrowser()); + this.suite.afterAll(() => this._freeBrowser()); + } + + _requestBrowser() { + this._browser = this._browserAgent.getBrowser(this._sessionId); + + this.suite.ctx.browser = this._browser.publicAPI; + } + + _freeBrowser() { + this._browserAgent.freeBrowser(this._browser); + } + + _injectSkip() { + const skip = new Skip(); + const skipBuilder = new SkipBuilder(skip, this._browserAgent.browserId); + const onlyBuilder = new OnlyBuilder(skipBuilder); + + _.extend(global.hermione, {skip: skipBuilder, only: onlyBuilder}); + + this._addEventHandler(['suite', 'test'], (runnable) => skip.handleEntity(runnable)); + } + + _injectExecutionContext() { + const browserId = this._browserAgent.browserId; + + this._addEventHandler( + ['beforeAll', 'beforeEach', 'test', 'afterEach', 'afterAll'], + this._overrideRunnableFn((runnable, baseFn) => { + const _this = this; + return function() { + const browser = _this.suite.ctx.browser; + if (browser) { + Object.getPrototypeOf(browser).executionContext = _.extend(runnable, {browserId}); + } + return baseFn.apply(this, arguments); + }; + }) + ); + } + + _passthroughMochaEvents() { + const _this = this; + function monitoredEmit() { + try { + _this.emit.apply(_this, arguments); + } catch (e) { + _this._errMonitor.emit('err', e); + throw e; + } + } + this._attachProxyReporter(monitoredEmit); + this._passthroughFileEvents(monitoredEmit); + } + + _attachProxyReporter(emit) { + const Reporter = _.partial(ProxyReporter, emit, () => this._getBrowser()); + this._mocha.reporter(Reporter); + } + + _getBrowser() { + return this._browser || {id: this._browserAgent.browserId}; + } + + _passthroughFileEvents(emit) { + const emit_ = (event, file) => emit(event, { + file, + hermione: global.hermione, + browser: this._browserAgent.browserId, + suite: this.suite + }); + + this.suite.on('pre-require', (ctx, file) => emit_(RunnerEvents.BEFORE_FILE_READ, file)); + this.suite.on('post-require', (ctx, file) => emit_(RunnerEvents.AFTER_FILE_READ, file)); + + return this; + } + + _overrideRunnableFn(overrideFn) { + return (runnable) => { + const baseFn = runnable.fn; + if (baseFn) { + runnable.fn = overrideFn(runnable, baseFn); + } + }; + } + + reinit() { + this._initMocha(); + } + + isFailed() { + return Boolean(this._fail); + } + + attachTestFilter(shouldRunTest) { + this._addEventHandler('test', (test) => { + if (shouldRunTest(test)) { + this.tests.push(test); + return; + } + + test.parent.tests.pop(); + }); + + return this; + } + + loadFiles(files) { + [].concat(files).forEach((filename) => { + clearRequire(path.resolve(filename)); + this._mocha.addFile(filename); + }); + + this._mocha.loadFiles(); + this._mocha.files = []; + + return this; + } + + runInSession(sessionId) { + this._sessionId = sessionId; + + const defer = q.defer(); + + this.on(RunnerEvents.ERROR, (err) => this._fail = err); + this.on(RunnerEvents.TEST_FAIL, (data) => this._fail = data.err); + this._errMonitor.on('err', (err) => defer.reject(err)); + + this._mocha.run(() => this._fail ? defer.reject(this._fail) : defer.resolve()); + + return defer.promise; + } + + // Set recursive handler for events triggered by mocha while parsing test file + _addEventHandler(events, cb) { + events = [].concat(events); + + const listenSuite = (suite) => { + suite.on('suite', listenSuite); + events.forEach((e) => suite.on(e, cb)); + }; + + listenSuite(this.suite); + } +}; + +function markSuiteAsFailed(suite, error) { + suite.fail = error; + eachSuiteAndTest(suite, (runnable) => runnable.fail = error); +} + +function eachSuiteAndTest(runnable, cb) { + runnable.tests.forEach((test) => cb(test)); + runnable.suites.forEach((suite) => { + cb(suite); + eachSuiteAndTest(suite, cb); + }); +} + +function markTestAsFailed(test, error) { + test.fail = error; +} diff --git a/lib/worker/runner/mocha-runner/mocha-builder.js b/lib/worker/runner/mocha-runner/mocha-builder.js new file mode 100644 index 000000000..116a223ca --- /dev/null +++ b/lib/worker/runner/mocha-runner/mocha-builder.js @@ -0,0 +1,54 @@ +'use strict'; + +const EventEmitter = require('events').EventEmitter; +const _ = require('lodash'); +const qUtils = require('qemitter/utils'); +const BrowserAgent = require('../../browser-agent'); +const RunnerEvents = require('../../constants/runner-events'); +const MochaAdapter = require('./mocha-adapter'); +const SingleTestMochaAdapter = require('./single-test-mocha-adapter'); + +module.exports = class MochaBuilder extends EventEmitter { + static prepare() { + MochaAdapter.prepare(); + } + + static create(browserId, config, browserPool) { + return new MochaBuilder(browserId, config, browserPool); + } + + constructor(browserId, config, browserPool) { + super(); + + this._browserId = browserId; + this._config = config; + this._browserPool = browserPool; + } + + buildAdapters(filenames) { + const mkAdapters = (filename, testIndex) => { + testIndex = testIndex || 0; + + const mocha = SingleTestMochaAdapter.create(this._createMocha(), filename, testIndex); + + return mocha.tests.length ? [mocha].concat(mkAdapters(filename, testIndex + 1)) : []; + }; + + return _(filenames) + .map((filename) => mkAdapters(filename)) + .flatten() + .value(); + } + + _createMocha() { + const browserAgent = BrowserAgent.create(this._browserId, this._browserPool); + const mocha = MochaAdapter.create(browserAgent, this._config); + + qUtils.passthroughEvent(mocha, this, [ + RunnerEvents.BEFORE_FILE_READ, + RunnerEvents.AFTER_FILE_READ + ]); + + return mocha; + } +}; diff --git a/lib/worker/runner/mocha-runner/single-test-mocha-adapter.js b/lib/worker/runner/mocha-runner/single-test-mocha-adapter.js new file mode 100644 index 000000000..8201a034e --- /dev/null +++ b/lib/worker/runner/mocha-runner/single-test-mocha-adapter.js @@ -0,0 +1,47 @@ +'use strict'; + +/** + * MochaAdapter decorator + * Level of abstraction which implements a mocha adapter with one single test + */ +module.exports = class SingleTestMochaAdapter { + static create(mocha, filename, index) { + return new SingleTestMochaAdapter(mocha, filename, index); + } + + constructor(mocha, filename, index) { + this._mocha = mocha; + this._filename = filename; + this._index = index; + + this._loadTest(); + + this._failed = false; + } + + get tests() { + return this._mocha.tests; + } + + runInSession(sessionId) { + if (this._mocha.isFailed()) { + this._reinit(); + } + + return this._mocha.runInSession(sessionId); + } + + _reinit() { + this._mocha.reinit(); + + this._loadTest(); + } + + _loadTest() { + let currentTestIndex = -1; + + return this._mocha + .attachTestFilter(() => ++currentTestIndex === this._index) + .loadFiles(this._filename); + } +}; diff --git a/package.json b/package.json index 207ebe53f..89ca7edc5 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,8 @@ "qemitter": "^2.0.0", "teamcity-service-messages": "^0.1.6", "urijs": "^1.17.0", - "webdriverio": "^4.6.1" + "webdriverio": "^4.6.1", + "worker-farm": "git://github.com/gemini-testing/node-worker-farm.git#feat/broadcast" }, "devDependencies": { "chai": "^3.4.1", From 5f04d3369311db038c585479375a228e720724b0 Mon Sep 17 00:00:00 2001 From: egavr Date: Mon, 14 Aug 2017 14:12:54 +0300 Subject: [PATCH 2/3] test: some tests on running tests in subprocesses --- package.json | 12 +- test/lib/_mocha/suite.js | 1 + test/lib/browser-pool/index.js | 14 - test/lib/browser.js | 24 +- test/lib/cli/index.js | 5 - test/lib/config/index.js | 62 +-- test/lib/config/options.js | 24 + test/lib/hermione.js | 55 +-- test/lib/runner/index.js | 85 +++- test/lib/runner/mocha-runner/index.js | 17 +- test/lib/runner/mocha-runner/mocha-adapter.js | 418 +----------------- .../runner/mocha-runner/retry-mocha-runner.js | 15 +- .../mocha-runner/single-test-mocha-adapter.js | 13 +- test/lib/worker/browser-agent.js | 26 ++ test/lib/worker/browser-pool.js | 93 ++++ test/lib/worker/hermione.js | 151 +++++++ test/lib/worker/index.js | 120 +++++ test/lib/worker/runner/index.js | 5 + test/lib/worker/runner/mocha-runner/index.js | 5 + .../runner/mocha-runner/mocha-adapter.js | 5 + .../mocha-runner/single-test-mocha-adapter.js | 5 + test/utils.js | 6 +- 22 files changed, 614 insertions(+), 547 deletions(-) create mode 100644 test/lib/worker/browser-agent.js create mode 100644 test/lib/worker/browser-pool.js create mode 100644 test/lib/worker/hermione.js create mode 100644 test/lib/worker/index.js create mode 100644 test/lib/worker/runner/index.js create mode 100644 test/lib/worker/runner/mocha-runner/index.js create mode 100644 test/lib/worker/runner/mocha-runner/mocha-adapter.js create mode 100644 test/lib/worker/runner/mocha-runner/single-test-mocha-adapter.js diff --git a/package.json b/package.json index 89ca7edc5..903585f38 100644 --- a/package.json +++ b/package.json @@ -48,17 +48,17 @@ "worker-farm": "git://github.com/gemini-testing/node-worker-farm.git#feat/broadcast" }, "devDependencies": { - "chai": "^3.4.1", - "chai-as-promised": "^5.1.0", + "chai": "^4.1.1", + "chai-as-promised": "^7.1.1", "conventional-changelog-lint": "^1.0.1", "doctoc": "^1.0.0", + "eslint": "^3.9.0", + "eslint-config-gemini-testing": "^2.4.0", "husky": "^0.11.4", "istanbul": "^0.4.1", "proxyquire": "^1.7.3", "sinon": "^2.1.0", - "sinon-chai": "^2.9.0", - "standard-version": "^3.0.0", - "eslint": "^3.9.0", - "eslint-config-gemini-testing": "^2.4.0" + "sinon-chai": "^2.12.0", + "standard-version": "^3.0.0" } } diff --git a/test/lib/_mocha/suite.js b/test/lib/_mocha/suite.js index eff506a2a..cba96595b 100644 --- a/test/lib/_mocha/suite.js +++ b/test/lib/_mocha/suite.js @@ -140,6 +140,7 @@ module.exports = class Suite extends EventEmitter { eachTest(fn) { this.tests.forEach(fn); + this.suites.forEach((suite) => suite.eachTest(fn)); } run() { diff --git a/test/lib/browser-pool/index.js b/test/lib/browser-pool/index.js index 14b16ef26..408733d19 100644 --- a/test/lib/browser-pool/index.js +++ b/test/lib/browser-pool/index.js @@ -64,20 +64,6 @@ describe('browser-pool', () => { assert.becomes(getBrowserManager().start(browser), {session: 'id'}); }); - it(`onStart should emit NEW_BROWSER event`, () => { - const emitter = new QEmitter(); - const onNewBrowser = sandbox.spy(); - - BrowserPool.create(null, emitter); - - const BrowserManager = getBrowserManager(); - - emitter.on(Events.NEW_BROWSER, onNewBrowser); - - return BrowserManager.onStart(stubBrowser('bro', {public: 'api'})) - .then(() => assert.calledOnceWith(onNewBrowser, {public: 'api'}, {browserId: 'bro'})); - }); - _.forEach({onStart: Events.SESSION_START, onQuit: Events.SESSION_END}, (event, method) => { describe(`${method}`, () => { it(`should emit browser event "${event}"`, () => { diff --git a/test/lib/browser.js b/test/lib/browser.js index 4b491c86c..21c2be66c 100644 --- a/test/lib/browser.js +++ b/test/lib/browser.js @@ -205,8 +205,7 @@ describe('Browser', () => { return browser .init() .then(() => { - assert.deepPropertyVal(session.requestHandler.defaultOptions, - 'screenshotOnReject.connectionRetryTimeout', 666); + assert.equal(session.requestHandler.defaultOptions.screenshotOnReject.connectionRetryTimeout, 666); }); }); }); @@ -314,12 +313,6 @@ describe('Browser', () => { .then(() => assert.called(session.end)); }); - it('should not finalize session if it has not been initialized', () => { - return mkBrowser_() - .quit() - .then(() => assert.notCalled(session.end)); - }); - it('should set custom options before finalizing of a session', () => { return mkBrowser_() .init() @@ -350,10 +343,19 @@ describe('Browser', () => { sessionID: 'foo' }; + assert.equal(mkBrowser_().sessionId, 'foo'); + }); + + it('should set session id', () => { + session.requestHandler = { + sessionID: 'foo' + }; + const browser = mkBrowser_(); - return browser.init() - .then(() => assert.equal(browser.sessionId, 'foo')); + browser.sessionId = 'bar'; + + assert.equal(browser.sessionId, 'bar'); }); }); @@ -370,7 +372,7 @@ describe('Browser', () => { it('should handle an error from prepareBrowser', () => { const prepareBrowser = sandbox.stub().throws(); - return assert.isRejected(mkBrowser_({prepareBrowser}).init()); + assert.throws(() => mkBrowser_({prepareBrowser})); }); }); }); diff --git a/test/lib/cli/index.js b/test/lib/cli/index.js index c2f8b49f5..46a33d54c 100644 --- a/test/lib/cli/index.js +++ b/test/lib/cli/index.js @@ -74,11 +74,6 @@ describe('cli', () => { .then(() => assert.calledWith(Hermione.create, '.conf.hermione.js')); }); - it('should create Hermione which allows to override config from env vars and cli opts', () => { - return hermioneCli.run() - .then(() => assert.calledWith(Hermione.create, any, {cli: true, env: true})); - }); - it('should run hermione', () => { return hermioneCli.run() .then(() => assert.calledOnce(Hermione.prototype.run)); diff --git a/test/lib/config/index.js b/test/lib/config/index.js index c5ba8da54..2248e4009 100644 --- a/test/lib/config/index.js +++ b/test/lib/config/index.js @@ -1,7 +1,6 @@ 'use strict'; const path = require('path'); -const _ = require('lodash'); const proxyquire = require('proxyquire').noCallThru(); const defaults = require('../../../lib/config/defaults'); @@ -21,7 +20,7 @@ describe('config', () => { [resolvedConfigPath]: opts.requireConfigReturns || {} }); - return Config.create(opts.config || configPath, opts.allowOverrides); + return Config.create(configPath, opts.allowOverrides); }; afterEach(() => sandbox.restore()); @@ -36,46 +35,15 @@ describe('config', () => { it('should parse config from file', () => { initConfig({requireConfigReturns: 'some-options'}); - assert.calledWithMatch(parseOptions, {options: 'some-options'}); + assert.calledWithMatch(parseOptions, {options: 'some-options', env: process.env, argv: process.argv}); }); - it('should parse config from object', () => { - initConfig({config: {some: 'config'}}); - - assert.calledWithMatch(parseOptions, {options: {some: 'config'}}); - }); - - it('should not allow to override options from cli options and env variables by default', () => { - initConfig(); - - assert.calledWithMatch(parseOptions, {env: {}, argv: []}); - }); - - it('should allow to override options from env variables', () => { - initConfig({allowOverrides: {env: true}}); - - process.env['hermione_base_url'] = 'http://env.com'; - - const args = parseOptions.lastCall.args[0]; - assert.propertyVal(args.env, 'hermione_base_url', 'http://env.com'); - - delete process.env['hermione_base_url']; - }); - - it('should allow to override options from cli options', () => { - initConfig({allowOverrides: {cli: true}}); - - process.argv.push('--base-url'); - - const args = parseOptions.lastCall.args[0]; - - assert.equal(_.last(args.argv), '--base-url'); - - process.argv.pop(); + it('should create config', () => { + assert.include(initConfig({configParserReturns: {some: 'option'}}), {some: 'option'}); }); - it('should create config', () => { - assert.deepEqual(initConfig({configParserReturns: {some: 'option'}}), {some: 'option'}); + it('should extend config with a config path', () => { + assert.include(initConfig({configPath: 'config-path'}), {configPath: 'config-path'}); }); }); @@ -100,4 +68,22 @@ describe('config', () => { assert.deepEqual(config.getBrowserIds(), ['bro1', 'bro2']); }); }); + + describe('mergeWith', () => { + it('should deeply merge config with another one', () => { + const config = initConfig({configParserReturns: {some: {deep: {option: 'foo'}}}}); + + config.mergeWith({some: {deep: {option: 'bar'}}}); + + assert.deepInclude(config, {some: {deep: {option: 'bar'}}}); + }); + + it('should not merge values of different types', () => { + const config = initConfig({configParserReturns: {option: 100500}}); + + config.mergeWith({option: '100500'}); + + assert.deepInclude(config, {option: 100500}); + }); + }); }); diff --git a/test/lib/config/options.js b/test/lib/config/options.js index 8dc91513c..933c66e50 100644 --- a/test/lib/config/options.js +++ b/test/lib/config/options.js @@ -145,6 +145,30 @@ describe('config options', () => { assert.deepEqual(result.system.patternsOnReject, ['some-pattern']); }); }); + + describe('workers', () => { + it('should throw in case of not positive integer', () => { + [0, -1, 'string', {foo: 'bar'}].forEach((workers) => { + Config.read.returns({system: {workers}}); + + assert.throws(() => createConfig(), '"workers" must be a positive integer'); + }); + }); + + it('should equal one by default', () => { + const config = createConfig(); + + assert.equal(config.system.workers, 1); + }); + + it('should be overriden from a config', () => { + Config.read.returns({system: {workers: 100500}}); + + const config = createConfig(); + + assert.equal(config.system.workers, 100500); + }); + }); }); describe('prepareEnvironment', () => { diff --git a/test/lib/hermione.js b/test/lib/hermione.js index 774a86b62..f3945106e 100644 --- a/test/lib/hermione.js +++ b/test/lib/hermione.js @@ -43,28 +43,10 @@ describe('hermione', () => { sandbox.stub(Runner, 'create').returns(new EventEmitter()); }); - it('should create config', () => { - Hermione.create(); + it('should create a config from the passed path', () => { + Hermione.create('some-config-path.js'); - assert.calledOnce(Config.create); - }); - - it('should create config from passed path', () => { - Hermione.create('.hermione.conf.js'); - - assert.calledWith(Config.create, '.hermione.conf.js'); - }); - - it('should create config from passed object', () => { - Hermione.create({some: 'config'}); - - assert.calledWith(Config.create, {some: 'config'}); - }); - - it('should create config with passed options', () => { - Hermione.create(null, {some: 'options'}); - - assert.calledWith(Config.create, sinon.match.any, {some: 'options'}); + assert.calledOnceWith(Config.create, 'some-config-path.js'); }); }); @@ -74,8 +56,8 @@ describe('hermione', () => { it('should create runner', () => { mkRunnerStub_(); - return Hermione.create(makeConfigStub()) - .run(() => assert.calledOnce(Runner.create)); + return runHermione() + .then(() => assert.calledOnce(Runner.create)); }); it('should create runner with config', () => { @@ -89,11 +71,15 @@ describe('hermione', () => { }); it('should warn about unknown browsers from cli', () => { + mkRunnerStub_(); + return runHermione([], {browsers: ['bro3']}) .then(() => assert.calledWithMatch(logger.warn, /Unknown browser ids: bro3/)); }); describe('loading of plugins', () => { + beforeEach(() => mkRunnerStub_()); + it('should load plugins', () => { return runHermione() .then(() => assert.calledOnce(pluginsLoader.load)); @@ -117,14 +103,14 @@ describe('hermione', () => { }); it('should load plugins before creating any runner', () => { - sandbox.spy(Runner, 'create'); - return runHermione() .then(() => assert.callOrder(pluginsLoader.load, Runner.create)); }); }); describe('sets revealing', () => { + beforeEach(() => mkRunnerStub_()); + it('should reveal sets', () => { return runHermione() .then(() => assert.calledOnce(sets.reveal)); @@ -132,7 +118,9 @@ describe('hermione', () => { it('should reveal sets using passed paths', () => { return runHermione(['first.js', 'second.js']) - .then(() => assert.calledWith(sets.reveal, sinon.match.any, {paths: ['first.js', 'second.js']})); + .then(() => { + assert.calledWith(sets.reveal, sinon.match.any, sinon.match({paths: ['first.js', 'second.js']})); + }); }); it('should reveal sets using passed browsers', () => { @@ -167,6 +155,8 @@ describe('hermione', () => { }); it('should return "true" if there are no failed tests', () => { + mkRunnerStub_(); + return runHermione() .then((success) => assert.isTrue(success)); }); @@ -259,7 +249,7 @@ describe('hermione', () => { return hermione.run() .then(() => { - _.forEach(_.omit(hermione.events, 'EXIT'), (event, name) => { + _.forEach(_.omit(hermione.events, ['EXIT', 'NEW_BROWSER']), (event, name) => { const spy = sinon.spy().named(`${name} handler`); hermione.on(event, spy); @@ -407,10 +397,9 @@ describe('hermione', () => { describe('should provide access to', () => { it('hermione events', () => { - const hermione = Hermione.create(makeConfigStub()); - const expectedEvents = _.extend({EXIT: 'exit'}, RunnerEvents); + const expectedEvents = _.extend({NEW_BROWSER: 'newBrowser'}, RunnerEvents); - assert.deepEqual(hermione.events, expectedEvents); + assert.deepEqual(Hermione.create(makeConfigStub()).events, expectedEvents); }); it('hermione configuration', () => { @@ -418,9 +407,7 @@ describe('hermione', () => { Config.create.returns(config); - const hermione = Hermione.create(); - - assert.deepEqual(hermione.config, config); + assert.deepEqual(Hermione.create().config, config); }); }); @@ -430,6 +417,8 @@ describe('hermione', () => { }); it('should return "false" if there are no failed tests or errors', () => { + mkRunnerStub_(); + const hermione = Hermione.create(makeConfigStub()); return hermione.run() diff --git a/test/lib/runner/index.js b/test/lib/runner/index.js index 813779f37..f161b0ea5 100644 --- a/test/lib/runner/index.js +++ b/test/lib/runner/index.js @@ -1,14 +1,15 @@ 'use strict'; +const EventEmitter = require('events').EventEmitter; +const path = require('path'); const BrowserAgent = require('gemini-core').BrowserAgent; const _ = require('lodash'); +const proxyquire = require('proxyquire'); const q = require('q'); -const QEmitter = require('qemitter'); const qUtils = require('qemitter/utils'); const BrowserPool = require('../../../lib/browser-pool'); const MochaRunner = require('../../../lib/runner/mocha-runner'); -const Runner = require('../../../lib/runner'); const TestSkipper = require('../../../lib/runner/test-skipper'); const RunnerEvents = require('../../../lib/constants/runner-events'); const logger = require('../../../lib/utils').logger; @@ -18,10 +19,12 @@ const makeConfigStub = require('../../utils').makeConfigStub; describe('Runner', () => { const sandbox = sinon.sandbox.create(); + let Runner, workerFarm, workers; + const mkMochaRunner = () => { sandbox.stub(MochaRunner, 'create'); - const mochaRunner = new QEmitter(); + const mochaRunner = new EventEmitter(); mochaRunner.init = sandbox.stub().returnsThis(); mochaRunner.run = sandbox.stub().returns(q()); @@ -43,6 +46,16 @@ describe('Runner', () => { }; beforeEach(() => { + workers = { + init: sandbox.stub().yields(), + syncConfig: sandbox.stub().yields() + }; + + workerFarm = sandbox.stub().returns(workers); + workerFarm.end = sandbox.stub(); + + Runner = proxyquire('../../../lib/runner', {'worker-farm': workerFarm}); + sandbox.stub(BrowserPool, 'create'); sandbox.stub(MochaRunner, 'prepare'); @@ -70,6 +83,64 @@ describe('Runner', () => { }); describe('run', () => { + describe('worker farm', () => { + it('should create a worker farm', () => { + mkMochaRunner(); + + return run_({config: {system: {workers: 100500}}}) + .then(() => { + assert.calledOnceWith(workerFarm, { + maxConcurrentWorkers: 100500, + maxConcurrentCallsPerWorker: Infinity, + autoStart: true + }, path.join(process.cwd(), 'lib/worker/index.js'), [ + {name: 'init', broadcast: true}, + {name: 'syncConfig', broadcast: true}, + 'runTest' + ]); + }); + }); + + it('should create a worker farm before RUNNER_START event', () => { + const onRunnerStart = sinon.stub().named('onRunnerStart').returns(q()); + const runner = new Runner(makeConfigStub()); + + runner.on(RunnerEvents.RUNNER_START, onRunnerStart); + + return run_({runner}) + .then(() => assert.callOrder(workerFarm, onRunnerStart)); + }); + + it('should init workers', () => { + const runner = new Runner(makeConfigStub({configPath: 'some-config-path'})); + + return runner.run({bro: ['file1', 'file2']}) + .then(() => assert.calledOnceWith(workers.init, {bro: ['file1', 'file2']}, 'some-config-path')); + }); + + it('should sync config in workers', () => { + mkMochaRunner(); + + return run_({config: {some: 'config', system: {workers: 100500}}}) + .then(() => assert.calledOnceWith(workers.syncConfig, {some: 'config', system: {workers: 100500}})); + }); + + it('should sync config after all RUNNER_START handler have finished', () => { + const onRunnerStart = sinon.stub().named('onRunnerStart').returns(q().then(() => q.delay(1))); + const runner = new Runner(makeConfigStub()); + + runner.on(RunnerEvents.RUNNER_START, onRunnerStart); + + return run_({runner}) + .then(() => assert.callOrder(onRunnerStart, workers.syncConfig)); + }); + + it('should pass workers to each mocha runner', () => { + return run_({browsers: ['bro1', 'bro2']}) + .then(() => assert.alwaysCalledWith(MochaRunner.prototype.run, workers)); + }); + }); + describe('RUNNER_START event', () => { it('should pass a runner to a RUNNER_START handler', () => { const onRunnerStart = sinon.stub().named('onRunnerStart').returns(q()); @@ -133,8 +204,8 @@ describe('Runner', () => { it('should pass config to a mocha runner', () => { mkMochaRunner(); - return run_({config: {some: 'config'}}) - .then(() => assert.calledOnceWith(MochaRunner.create, sinon.match.any, {some: 'config'})); + return run_({config: {some: 'config', system: {}}}) + .then(() => assert.calledOnceWith(MochaRunner.create, sinon.match.any, sinon.match({some: 'config'}))); }); it('should create mocha runners with the same browser pool', () => { @@ -219,7 +290,7 @@ describe('Runner', () => { return run_({runner}) .then(() => { assert.calledOnceWith(qUtils.passthroughEvent, - sinon.match.instanceOf(QEmitter), + sinon.match.instanceOf(EventEmitter), sinon.match.instanceOf(Runner), mochaRunnerEvents ); @@ -298,7 +369,7 @@ describe('Runner', () => { runner.buildSuiteTree([{}]); assert.calledWith(qUtils.passthroughEvent, - sinon.match.instanceOf(QEmitter), + sinon.match.instanceOf(EventEmitter), sinon.match.instanceOf(Runner), [ RunnerEvents.BEFORE_FILE_READ, diff --git a/test/lib/runner/mocha-runner/index.js b/test/lib/runner/mocha-runner/index.js index b4fd0edd6..c0e7198b2 100644 --- a/test/lib/runner/mocha-runner/index.js +++ b/test/lib/runner/mocha-runner/index.js @@ -153,18 +153,6 @@ describe('mocha-runner', () => { assert.doesNotThrow(() => init_()); }); - - it('should switch of hooks in skipped suites', () => { - const mocha1 = createMochaStub_(); - const mocha2 = createMochaStub_(); - - MochaBuilder.prototype.buildAdapters.returns([mocha1, mocha2]); - - init_(); - - assert.calledOnce(mocha1.disableHooksInSkippedSuites); - assert.calledOnce(mocha2.disableHooksInSkippedSuites); - }); }); describe('run', () => { @@ -198,10 +186,11 @@ describe('mocha-runner', () => { sandbox.stub(mocha, 'run'); MochaBuilder.prototype.buildAdapters.returns([mocha]); - return run_() + return init_() + .run({some: 'workers'}) .then(() => { assert.notCalled(mocha.run); - assert.calledOnce(RetryMochaRunner.prototype.run); + assert.calledOnceWith(RetryMochaRunner.prototype.run, {some: 'workers'}); }); }); diff --git a/test/lib/runner/mocha-runner/mocha-adapter.js b/test/lib/runner/mocha-runner/mocha-adapter.js index 5261381c7..d8c087440 100644 --- a/test/lib/runner/mocha-runner/mocha-adapter.js +++ b/test/lib/runner/mocha-runner/mocha-adapter.js @@ -34,6 +34,7 @@ describe('mocha-runner/mocha-adapter', () => { testSkipper = sinon.createStubInstance(TestSkipper); browserAgent = sinon.createStubInstance(BrowserAgent); browserAgent.getBrowser.returns(q(mkBrowserStub_())); + browserAgent.freeBrowser.returns(q()); clearRequire = sandbox.stub().named('clear-require'); MochaAdapter = proxyquire('../../../../lib/runner/mocha-runner/mocha-adapter', { @@ -68,6 +69,12 @@ describe('mocha-runner/mocha-adapter', () => { assert.called(MochaStub.lastInstance.fullTrace); }); + + it('should disable timeouts', () => { + mkMochaAdapter_(); + + assert.calledOnceWith(MochaStub.lastInstance.suite.enableTimeouts, false); + }); }); describe('loadFiles', () => { @@ -193,23 +200,6 @@ describe('mocha-runner/mocha-adapter', () => { .then(() => assert.calledOnce(browserAgent.getBrowser)); }); - it('should disable mocha timeouts while setting browser hooks', () => { - const suitePrototype = MochaStub.Suite.prototype; - const beforeAllStub = sandbox.stub(suitePrototype, 'beforeAll'); - const afterAllStub = sandbox.stub(suitePrototype, 'afterAll'); - - mkMochaAdapter_(); - const suite = MochaStub.lastInstance.suite; - - assert.callOrder( - suite.enableTimeouts, // get current value of enableTimeouts - suite.enableTimeouts.withArgs(false).named('disableTimeouts'), - beforeAllStub, - afterAllStub, - suite.enableTimeouts.withArgs(true).named('restoreTimeouts') - ); - }); - it('should not be rejected if freeBrowser failed', () => { const browser = mkBrowserStub_(); @@ -266,6 +256,10 @@ describe('mocha-runner/mocha-adapter', () => { }); }); + describe('remove hooks', () => { + it('TODO'); + }); + describe('inject skip', () => { let mochaAdapter; @@ -320,112 +314,6 @@ describe('mocha-runner/mocha-adapter', () => { }); }); - describe('timeouts', () => { - beforeEach(() => { - mkMochaAdapter_(); - }); - - it('should disable mocha timeouts', () => { - const test = new MochaStub.Test(); - test.enableTimeouts(true); - - MochaStub.lastInstance.updateSuiteTree((suite) => suite.addTest(test)); - - assert.isFalse(test.enableTimeouts()); - }); - - it('should set promise timeout', () => { - const test = new MochaStub.Test(null, { - fn: () => q.delay(100) - }); - test.timeout(50); - MochaStub.lastInstance.updateSuiteTree((suite) => suite.addTest(test)); - - return assert.isRejected(test.run(), /Timed out/); - }); - - it('should not fail test if timeout not exceeded', () => { - const test = new MochaStub.Test(null, { - fn: () => q.delay(50) - }); - test.timeout(100); - MochaStub.lastInstance.updateSuiteTree((suite) => suite.addTest(test)); - - return assert.isFulfilled(test.run()); - }); - - it('should not set timeout if it is disabled', () => { - const test = new MochaStub.Test(null, { - fn: () => q.delay(100) - }); - test.timeout(50); - test.enableTimeouts(false); - MochaStub.lastInstance.updateSuiteTree((suite) => suite.addTest(test)); - - return assert.isFulfilled(test.run()); - }); - }); - - describe('inject execution context', () => { - let browser; - let mochaAdapter; - - beforeEach(() => { - browser = mkBrowserStub_(); - browserAgent.getBrowser.returns(q(browser)); - browserAgent.freeBrowser.returns(q()); - - mochaAdapter = mkMochaAdapter_(); - }); - - it('should add execution context to browser', () => { - const test = new MochaStub.Test(); - MochaStub.lastInstance.updateSuiteTree((suite) => suite.addTest(test)); - - return mochaAdapter.run() - .then(() => assert.includeMembers(_.keys(browser.publicAPI.executionContext), _.keys(test))); - }); - - it('should handle nested tests', () => { - let nestedSuite = MochaStub.Suite.create(); - let nestedSuiteTest; - - MochaStub.lastInstance.updateSuiteTree((suite) => { - suite.addSuite(nestedSuite); - - nestedSuiteTest = new MochaStub.Test(); - nestedSuite.addTest(nestedSuiteTest); - return suite; - }); - - return mochaAdapter.run() - .then(() => { - assert.includeMembers( - _.keys(browser.publicAPI.executionContext), - _.keys(nestedSuiteTest) - ); - }); - }); - - it('should add browser id to the context', () => { - BrowserAgent.prototype.browserId = 'some-browser'; - - MochaStub.lastInstance.updateSuiteTree((suite) => suite.addTest()); - - return mochaAdapter.run() - .then(() => assert.property(browser.publicAPI.executionContext, 'browserId', 'some-browser')); - }); - - it('should add execution context to the browser prototype', () => { - BrowserAgent.prototype.browserId = 'some-browser'; - - MochaStub.lastInstance.updateSuiteTree((suite) => suite.addTest()); - - return mochaAdapter.run() - .then(() => assert.property(Object.getPrototypeOf(browser.publicAPI), 'executionContext')); - }); - }); - describe('attachTestFilter', () => { it('should not remove test which expected to be run', () => { const testSpy = sinon.spy(); @@ -616,286 +504,16 @@ describe('mocha-runner/mocha-adapter', () => { }); }); - describe('"before" hook error handling', () => { - let mochaAdapter; - - beforeEach(() => { - browserAgent.freeBrowser.returns(q()); - - mochaAdapter = mkMochaAdapter_(); - }); - - it('should not launch suite original test if "before" hook failed', () => { - const testCb = sinon.spy(); - - MochaStub.lastInstance.updateSuiteTree((suite) => { - return suite - .beforeAll(() => q.reject(new Error())) - .addTest({fn: testCb}); - }); - - return mochaAdapter.run() - .then(() => assert.notCalled(testCb)); - }); - - it('should fail all suite tests with "before" hook error', () => { - const error = new Error(); - const testFailSpy = sinon.spy(); - - MochaStub.lastInstance.updateSuiteTree((rootSuite) => { - return rootSuite - .beforeAll(() => q.reject(error)) - .addTest({title: 'first-test'}) - .addSuite(MochaStub.Suite.create(rootSuite).addTest({title: 'second-test'}).onFail(testFailSpy)) - .onFail(testFailSpy); - }); - - return mochaAdapter.run() - .then(() => { - assert.calledTwice(testFailSpy); - assert.calledWithMatch(testFailSpy, {error, test: {title: 'first-test'}}); - assert.calledWithMatch(testFailSpy, {error, test: {title: 'second-test'}}); - }); - }); - - it('should handle sync "before hook" errors', () => { - const testFailSpy = sinon.spy(); - - MochaStub.lastInstance.updateSuiteTree((suite) => { - return suite - .beforeAll(() => { - throw new Error(); - }) - .addTest({title: 'some-test'}) - .onFail(testFailSpy); - }); - - return mochaAdapter.run() - .then(() => assert.calledOnce(testFailSpy)); - }); - - it('should not execute "before each" hook if "before" hook failed at the same level', () => { - const beforeEachHookFn = sinon.spy(); - - MochaStub.lastInstance.updateSuiteTree((suite) => { - return suite - .beforeAll(() => q.reject(new Error())) - .beforeEach(beforeEachHookFn) - .addTest(); - }); - - return mochaAdapter.run() - .then(() => assert.notCalled(beforeEachHookFn)); - }); - - it('should not execute "before each" hook if "before" hook has already failed on a higher level', () => { - const beforeEachHookFn = sinon.spy(); - - MochaStub.lastInstance.updateSuiteTree((rootSuite) => { - const suite = MochaStub.Suite.create(); - - rootSuite - .beforeAll(() => q.reject(new Error())) - .addSuite(suite); - - suite.beforeEach(beforeEachHookFn).addTest(); - - return rootSuite; - }); - - return mochaAdapter.run() - .then(() => assert.notCalled(beforeEachHookFn)); - }); - - it('should not execute "before" hook if another one has already failed on a higher level', () => { - const beforeAllHookFn = sandbox.spy(); - - MochaStub.lastInstance.updateSuiteTree((rootSuite) => { - const suite = MochaStub.Suite.create(); - - rootSuite - .beforeAll(() => q.reject(new Error())) - .addSuite(suite); - - suite.beforeAll(beforeAllHookFn).addTest(); - - return rootSuite; - }); - - return mochaAdapter.run() - .then(() => assert.notCalled(beforeAllHookFn)); - }); - - it('should not execute "before each" hook if "before" hook has already failed on a lower level', () => { - const beforeEachHookFn = sandbox.spy(); - - MochaStub.lastInstance.updateSuiteTree((rootSuite) => { - const suite = MochaStub.Suite.create(); - - rootSuite - .beforeEach(beforeEachHookFn) - .addSuite(suite); - - suite.beforeAll(() => q.reject(new Error())).addTest(); - - return rootSuite; - }); - - return mochaAdapter.run() - .then(() => assert.notCalled(beforeEachHookFn)); - }); - - it('should fail suite tests with error from "before" hook if "before each" hook is present at the same level', () => { - const error = new Error(); - const hookFailSpy = sinon.spy(); - - MochaStub.lastInstance.updateSuiteTree((suite) => { - return suite - .beforeAll(() => q.reject(error)) - .beforeEach(() => true) - .addTest() - .addTest() - .onFail(hookFailSpy); - }); - - return mochaAdapter.run() - .then(() => { - assert.calledTwice(hookFailSpy); - assert.calledWithMatch(hookFailSpy, {error}); - }); - }); - }); - - describe('"before each" hook error handling', () => { - let mochaAdapter; - - beforeEach(() => { - browserAgent.getBrowser.returns(q(mkBrowserStub_())); - browserAgent.freeBrowser.returns(q()); - - mochaAdapter = mkMochaAdapter_(); - }); - - it('should not execute original suite test if "before each" hook failed', () => { - const testCb = sinon.spy(); - - MochaStub.lastInstance.updateSuiteTree((suite) => { - return suite - .beforeEach(() => q.reject(new Error())) - .addTest({fn: testCb}); - }); - - return mochaAdapter.run() - .then(() => assert.notCalled(testCb)); - }); - - it('should execute original suite test if "before each" hook was executed successfully', () => { - const testCb = sinon.spy(); - - MochaStub.lastInstance.updateSuiteTree((suite) => { - return suite - .beforeEach(_.noop) - .addTest({fn: testCb}); - }); - - return mochaAdapter.run() - .then(() => assert.called(testCb)); - }); - - it('should fail test with error from "before each" hook', () => { - const error = new Error(); - const testFailSpy = sinon.spy(); - - MochaStub.lastInstance.updateSuiteTree((suite) => { - return suite - .beforeEach(() => q.reject(error)) - .addTest({title: 'some-test'}) - .onFail(testFailSpy); - }); - - return mochaAdapter.run() - .then(() => assert.calledWithMatch(testFailSpy, {error, test: {title: 'some-test'}})); - }); - - it('should handle sync "before each" hook errors', () => { - const testFailSpy = sinon.spy(); - - MochaStub.lastInstance.updateSuiteTree((suite) => { - return suite - .beforeEach(() => { - throw new Error(); - }) - .addTest({title: 'some-test'}) - .onFail(testFailSpy); - }); - - return mochaAdapter.run() - .then(() => assert.calledOnce(testFailSpy)); - }); - - it('should not execute "before each" hook if another one has already failed on a higher level', () => { - const beforeEachHookFn = sandbox.spy(); - - MochaStub.lastInstance.updateSuiteTree((rootSuite) => { - const suite = MochaStub.Suite.create(rootSuite); - - rootSuite - .beforeEach(() => q.reject(new Error())) - .addSuite(suite); - - suite.beforeEach(beforeEachHookFn).addTest(); - - return rootSuite; - }); - - return mochaAdapter.run() - .then(() => assert.notCalled(beforeEachHookFn)); - }); - }); - - describe('disableHooksInSkippedSuites', () => { - it('should switch of "beforeAll" and "afterAll" hooks in skipped suites', () => { - const mochaAdapter = mkMochaAdapter_(); - const firstBeforeAll = sinon.spy().named('firstBeforeAll'); - const firstAfterAll = sinon.spy().named('firstAfterAll'); - const secondBeforeAll = sinon.spy().named('secondBeforeAll'); - const secondAfterAll = sinon.spy().named('secondAfterAll'); - - MochaStub.lastInstance.updateSuiteTree((rootSuite) => { - const suite = MochaStub.Suite.create(rootSuite); - - return rootSuite - .beforeAll(firstBeforeAll) - .addTest({pending: false}) - .addSuite(suite.beforeAll(secondBeforeAll).addTest({pending: true}).afterAll(secondAfterAll)) - .afterAll(firstAfterAll); - }); - - mochaAdapter.disableHooksInSkippedSuites(); - - return mochaAdapter.run() - .then(() => { - assert.calledOnce(firstBeforeAll); - assert.calledOnce(firstAfterAll); - - assert.notCalled(secondBeforeAll); - assert.notCalled(secondAfterAll); - }); - }); - - it('should not try to request a browser for a completely skipped suite', () => { + describe('run', () => { + it('should run a test in subprocess using passed workers', () => { const mochaAdapter = mkMochaAdapter_(); + const workers = {runTest: sandbox.stub().yields()}; + browserAgent.getBrowser.returns(q({id: 'bro-id', sessionId: '100-500'})); - MochaStub.lastInstance.updateSuiteTree((rootSuite) => { - return rootSuite - .addTest({pending: true}) - .addSuite(MochaStub.Suite.create(rootSuite).addTest({pending: true})); - }); - - mochaAdapter.disableHooksInSkippedSuites(); + MochaStub.lastInstance.updateSuiteTree((suite) => suite.addTest({title: 'test-title'})); - return mochaAdapter.run() - .then(() => assert.notCalled(browserAgent.getBrowser)); + return mochaAdapter.run(workers) + .then(() => assert.calledOnceWith(workers.runTest, 'test-title', {browserId: 'bro-id', sessionId: '100-500'})); }); }); }); diff --git a/test/lib/runner/mocha-runner/retry-mocha-runner.js b/test/lib/runner/mocha-runner/retry-mocha-runner.js index dacc47cd3..36644c84a 100644 --- a/test/lib/runner/mocha-runner/retry-mocha-runner.js +++ b/test/lib/runner/mocha-runner/retry-mocha-runner.js @@ -68,8 +68,8 @@ describe('mocha-runner/retry-mocha-runner', () => { mochaAdapter.run.callsFake(emitEvent(RunnerEvents.TEST_FAIL, createTestStub())); - return retryMochaRunner.run() - .then(() => assert.calledOnce(mochaAdapter.run)); + return retryMochaRunner.run({some: 'workers'}) + .then(() => assert.calledOnceWith(mochaAdapter.run, {some: 'workers'})); }); it('should emit "TEST_FAIL" event if retries count is below zero', () => { @@ -89,8 +89,8 @@ describe('mocha-runner/retry-mocha-runner', () => { mochaAdapter.run.callsFake(emitEvent(RunnerEvents.TEST_FAIL)); - return retryMochaRunner.run() - .then(() => assert.calledOnce(mochaAdapter.run)); + return retryMochaRunner.run({some: 'workers'}) + .then(() => assert.calledOnceWith(mochaAdapter.run, {some: 'workers'})); }); it('should emit "RETRY" event if retries were set', () => { @@ -146,8 +146,11 @@ describe('mocha-runner/retry-mocha-runner', () => { mochaAdapter.run.callsFake(emitEvent(RunnerEvents.TEST_FAIL)); - return retryMochaRunner.run() - .then(() => assert.calledTwice(mochaAdapter.run)); + return retryMochaRunner.run({some: 'workers'}) + .then(() => { + assert.calledTwice(mochaAdapter.run); + assert.alwaysCalledWith(mochaAdapter.run, {some: 'workers'}); + }); }); it('should reinit mocha before a retry', () => { diff --git a/test/lib/runner/mocha-runner/single-test-mocha-adapter.js b/test/lib/runner/mocha-runner/single-test-mocha-adapter.js index 79594bb31..6a77bda1f 100644 --- a/test/lib/runner/mocha-runner/single-test-mocha-adapter.js +++ b/test/lib/runner/mocha-runner/single-test-mocha-adapter.js @@ -102,9 +102,9 @@ describe('mocha-runner/single-test-mocha-adapter', () => { const mochaAdapter = mkMochaAdapterStub(); const singleTestMochaAdapter = SingleTestMochaAdapter.create(mochaAdapter); - mochaAdapter.run.returns(q({foo: 'bar'})); + mochaAdapter.run.withArgs({some: 'workers'}).returns(q({foo: 'bar'})); - return assert.becomes(singleTestMochaAdapter.run(), {foo: 'bar'}); + return assert.becomes(singleTestMochaAdapter.run({some: 'workers'}), {foo: 'bar'}); }); it('should passthrough "on" method from the decorated mocha adapter', () => { @@ -115,13 +115,4 @@ describe('mocha-runner/single-test-mocha-adapter', () => { assert.deepEqual(singleTestMochaAdapter.on('arg1', 'arg2'), {foo: 'bar'}); }); - - it('should passthrough "disableHooksInSkippedSuites" method from the decorated mocha adapter', () => { - const mochaAdapter = mkMochaAdapterStub(); - const singleTestMochaAdapter = SingleTestMochaAdapter.create(mochaAdapter); - - mochaAdapter.disableHooksInSkippedSuites.returns({foo: 'bar'}); - - assert.deepEqual(singleTestMochaAdapter.disableHooksInSkippedSuites(), {foo: 'bar'}); - }); }); diff --git a/test/lib/worker/browser-agent.js b/test/lib/worker/browser-agent.js new file mode 100644 index 000000000..3cc1d1afa --- /dev/null +++ b/test/lib/worker/browser-agent.js @@ -0,0 +1,26 @@ +'use strict'; + +const BrowserAgent = require('../../../lib/worker/browser-agent'); +const BrowserPool = require('../../../lib/worker/browser-pool'); + +describe('worker/browser-agent', () => { + let browserPool; + + beforeEach(() => browserPool = sinon.createStubInstance(BrowserPool)); + + describe('getBrowser', () => { + it('should get a browser from the pool', () => { + browserPool.getBrowser.withArgs('bro-id', '100-500').returns({some: 'browser'}); + + assert.deepEqual(BrowserAgent.create('bro-id', browserPool).getBrowser('100-500'), {some: 'browser'}); + }); + }); + + describe('freeBrowser', () => { + it('should free the browser in the pool', () => { + BrowserAgent.create(null, browserPool).freeBrowser({some: 'browser'}); + + assert.calledOnceWith(browserPool.freeBrowser, {some: 'browser'}); + }); + }); +}); diff --git a/test/lib/worker/browser-pool.js b/test/lib/worker/browser-pool.js new file mode 100644 index 000000000..1d2862964 --- /dev/null +++ b/test/lib/worker/browser-pool.js @@ -0,0 +1,93 @@ +'use strict'; + +const EventEmitter = require('events').EventEmitter; +const _ = require('lodash'); +const Browser = require('../../../lib/browser'); +const BrowserPool = require('../../../lib/worker/browser-pool'); +const RunnerEvents = require('../../../lib/worker/constants/runner-events'); + +describe('worker/browser-pool', () => { + const sandbox = sinon.sandbox.create(); + + const createPool = (opts) => { + opts = _.defaults(opts || {}, { + config: {}, + emitter: new EventEmitter() + }); + + return BrowserPool.create(opts.config, opts.emitter); + }; + + afterEach(() => sandbox.restore()); + + describe('getBrowser', () => { + beforeEach(() => { + sandbox.stub(Browser, 'create').returns({}); + }); + + it('should create a new browser if there are no free browsers in a cache', () => { + const browserPool = createPool({config: {some: 'config'}}); + + Browser.create.withArgs({some: 'config'}, 'bro-id').returns({browserId: 'bro-id'}); + + const browser = browserPool.getBrowser('bro-id', '100-500'); + + assert.deepEqual(browser, {browserId: 'bro-id', sessionId: '100-500'}); + }); + + it('should emit "NEW_BROWSER" event on creating of a browser', () => { + const emitter = new EventEmitter(); + const onNewBrowser = sandbox.spy().named('onNewBrowser'); + const browserPool = createPool({emitter}); + + emitter.on(RunnerEvents.NEW_BROWSER, onNewBrowser); + + Browser.create.returns({publicAPI: {some: 'api'}}); + + browserPool.getBrowser('bro-id'); + + assert.calledOnceWith(onNewBrowser, {some: 'api'}, {browserId: 'bro-id'}); + }); + + it('should not create a new browser if there is a free browser in a cache', () => { + const browserPool = createPool(); + + const browser = browserPool.getBrowser('bro-id', '100-500'); + browserPool.freeBrowser(browser); + + Browser.create.resetHistory(); + + assert.deepEqual(browserPool.getBrowser('bro-id', '500-100'), browser); + assert.equal(browser.sessionId, '500-100'); + assert.notCalled(Browser.create); + }); + + it('should not emit "NEW_BROWSER" event on getting of a free browser from a cache', () => { + const emitter = new EventEmitter(); + const onNewBrowser = sandbox.spy().named('onNewBrowser'); + const browserPool = createPool({emitter}); + + emitter.on(RunnerEvents.NEW_BROWSER, onNewBrowser); + + const browser = browserPool.getBrowser('bro-id', '100-500'); + browserPool.freeBrowser(browser); + + onNewBrowser.reset(); + + browserPool.getBrowser('bro-id'); + + assert.notCalled(onNewBrowser); + }); + }); + + describe('freeBrowser', () => { + it('should set session id to "null"', () => { + const browserPool = createPool(); + const browser = {sessionId: '100-500'}; + + browserPool.freeBrowser(browser); + + assert.isNull(browser.sessionId); + }); + }); +}); diff --git a/test/lib/worker/hermione.js b/test/lib/worker/hermione.js new file mode 100644 index 000000000..92f69fb34 --- /dev/null +++ b/test/lib/worker/hermione.js @@ -0,0 +1,151 @@ +'use strict'; + +const _ = require('lodash'); +const pluginsLoader = require('plugins-loader'); +const q = require('q'); +const qUtils = require('qemitter/utils'); +const Config = require('../../../lib/config'); +const RunnerEvents = require('../../../lib/constants/runner-events'); +const WorkerRunnerEvents = require('../../../lib/worker/constants/runner-events'); +const Hermione = require('../../../lib/worker/hermione'); +const Runner = require('../../../lib/worker/runner'); +const makeConfigStub = require('../../utils').makeConfigStub; + +describe('worker/hermione', () => { + const sandbox = sinon.sandbox.create(); + + beforeEach(() => { + sandbox.stub(Config, 'create').returns(makeConfigStub()); + + sandbox.stub(pluginsLoader, 'load'); + + sandbox.spy(Runner, 'create'); + sandbox.stub(Runner.prototype, 'init'); + sandbox.stub(Runner.prototype, 'runTest'); + }); + + afterEach(() => sandbox.restore()); + + describe('constructor', () => { + it('should create a config from the passed path', () => { + Hermione.create('some-config-path.js'); + + assert.calledOnceWith(Config.create, 'some-config-path.js'); + }); + }); + + describe('should provide access to', () => { + it('hermione events', () => { + const expectedEvents = _.extend({}, RunnerEvents, WorkerRunnerEvents); + + assert.deepEqual(Hermione.create(makeConfigStub()).events, expectedEvents); + }); + + it('hermione configuration', () => { + const config = {foo: 'bar'}; + + Config.create.returns(config); + + assert.deepEqual(Hermione.create().config, config); + }); + }); + + describe('init', () => { + beforeEach(() => { + sandbox.spy(qUtils, 'passthroughEvent'); + }); + + it('should create a runner instance', () => { + Config.create.returns({some: 'config'}); + + Hermione.create().init(); + + assert.calledOnceWith(Runner.create, {some: 'config'}); + }); + + it('should init a runner instance', () => { + Hermione.create().init({bro: ['file']}); + + assert.calledOnceWith(Runner.prototype.init, {bro: ['file']}); + }); + + describe('loading of plugins', () => { + it('should load plugins', () => { + Hermione.create().init(); + + assert.calledOnce(pluginsLoader.load); + }); + + it('should load plugins for hermione instance', () => { + Hermione.create().init(); + + assert.calledWith(pluginsLoader.load, sinon.match.instanceOf(Hermione)); + }); + + it('should load plugins from config', () => { + Config.create.returns(makeConfigStub({plugins: {'some-plugin': true}})); + + Hermione.create().init(); + + assert.calledWith(pluginsLoader.load, sinon.match.any, {'some-plugin': true}); + }); + + it('should load plugins with appropriate prefix', () => { + Hermione.create().init(); + + assert.calledWith(pluginsLoader.load, sinon.match.any, sinon.match.any, 'hermione-'); + }); + + it('should load plugins before creating of any runner', () => { + Hermione.create().init(); + + assert.callOrder(pluginsLoader.load, Runner.create); + }); + }); + + describe('should passthrough', () => { + it('all subprocess runner events', () => { + const hermione = Hermione.create(); + hermione.init(); + + const events = [ + WorkerRunnerEvents.BEFORE_FILE_READ, + WorkerRunnerEvents.AFTER_FILE_READ, + WorkerRunnerEvents.NEW_BROWSER + ]; + + events.forEach((event, name) => { + const spy = sinon.spy().named(`${name} handler`); + hermione.on(event, spy); + + Runner.create.returnValues[0].emit(event); + + assert.calledOnce(spy); + }); + }); + + it('all subprocess runner event before initialization of a runner', () => { + const hermione = Hermione.create(); + hermione.init(); + + assert.calledOnceWith(qUtils.passthroughEvent, Runner.create.returnValues[0], hermione, [ + WorkerRunnerEvents.BEFORE_FILE_READ, + WorkerRunnerEvents.AFTER_FILE_READ, + WorkerRunnerEvents.NEW_BROWSER + ]); + assert.callOrder(qUtils.passthroughEvent, Runner.prototype.init); + }); + }); + }); + + describe('runTest', () => { + it('should run test', () => { + Runner.prototype.runTest.withArgs('fullTitle', {some: 'options'}).returns(q('foo bar')); + + const hermione = Hermione.create(); + hermione.init(); + + return assert.becomes(hermione.runTest('fullTitle', {some: 'options'}), 'foo bar'); + }); + }); +}); diff --git a/test/lib/worker/index.js b/test/lib/worker/index.js new file mode 100644 index 000000000..5d4cefc62 --- /dev/null +++ b/test/lib/worker/index.js @@ -0,0 +1,120 @@ +'use strict'; + +const q = require('q'); +const Config = require('../../../lib/config'); +const worker = require('../../../lib/worker'); +const Hermione = require('../../../lib/worker/hermione'); + +describe('worker', () => { + const sandbox = sinon.sandbox.create(); + + afterEach(() => sandbox.restore()); + + beforeEach(() => sandbox.stub(Config, 'create').returns({})); + + describe('init', () => { + beforeEach(() => { + sandbox.spy(Hermione, 'create'); + sandbox.stub(Hermione.prototype, 'init'); + }); + + it('should init hermione instance', () => { + worker.init({bro: ['file']}, 'some-config-path.js', () => {}); + + assert.calledOnceWith(Hermione.create, 'some-config-path.js'); + assert.calledOnceWith(Hermione.prototype.init, {bro: ['file']}); + }); + + it('should call callback without arguments if init ends successfully', () => { + const cb = sandbox.spy().named('cb'); + + worker.init(null, null, cb); + + assert.calledOnce(cb); + assert.calledWithExactly(cb); + }); + + it('should call callback with an error as the first argument if init fails', () => { + const cb = sandbox.spy().named('cb'); + const err = new Error(); + + Hermione.prototype.init.throws(err); + + worker.init(null, null, cb); + + assert.calledOnceWith(cb, err); + }); + + it('should call callback after init', () => { + const cb = sandbox.spy().named('cb'); + + worker.init(null, null, cb); + + assert.callOrder(Hermione.prototype.init, cb); + }); + }); + + describe('syncConfig', () => { + it('should sync passed config with a config of inited hermione', () => { + const config = {mergeWith: sandbox.stub()}; + + Config.create.returns(config); + + worker.init(null, null, () => {}); + worker.syncConfig({some: 'config'}, () => {}); + + assert.calledOnceWith(config.mergeWith, {some: 'config'}); + }); + + it('should call callback after merge', () => { + const cb = sandbox.spy().named('cb'); + const config = {mergeWith: sandbox.stub()}; + + Config.create.returns(config); + + worker.init(null, null, () => {}); + + worker.syncConfig({some: 'config'}, cb); + + assert.callOrder(config.mergeWith, cb); + }); + }); + + describe('runTest', () => { + beforeEach(() => sandbox.stub(Hermione.prototype, 'runTest').returns(q())); + + it('should run a test', () => { + worker.init(null, null, () => {}); + + worker.runTest('fullTitle', {some: 'options'}, () => {}); + + assert.calledOnceWith(Hermione.prototype.runTest, 'fullTitle', {some: 'options'}); + }); + + it('should call callback without arguments if running of a test ends successfully', () => { + const cb = sandbox.spy().named('cb'); + + worker.init(null, null, () => {}); + + worker.runTest(null, null, cb); + + return q.delay(1) + .then(() => { + assert.calledOnce(cb); + assert.calledWithExactly(cb); + }); + }); + + it('should call callback with an error as the first argument if running of a test fails', () => { + const err = new Error(); + const cb = sandbox.spy().named('cb'); + + Hermione.prototype.runTest.returns(q.reject(err)); + + worker.runTest(null, null, cb); + + return q.delay(1) + .then(() => assert.calledOnceWith(cb, err)); + }); + }); +}); diff --git a/test/lib/worker/runner/index.js b/test/lib/worker/runner/index.js new file mode 100644 index 000000000..010c84e2e --- /dev/null +++ b/test/lib/worker/runner/index.js @@ -0,0 +1,5 @@ +'use strict'; + +describe('worker/runner', () => { + it('TODO'); +}); diff --git a/test/lib/worker/runner/mocha-runner/index.js b/test/lib/worker/runner/mocha-runner/index.js new file mode 100644 index 000000000..f27a268c8 --- /dev/null +++ b/test/lib/worker/runner/mocha-runner/index.js @@ -0,0 +1,5 @@ +'use strict'; + +describe('worker/mocha-runner', () => { + it('TODO'); +}); diff --git a/test/lib/worker/runner/mocha-runner/mocha-adapter.js b/test/lib/worker/runner/mocha-runner/mocha-adapter.js new file mode 100644 index 000000000..fa89e2819 --- /dev/null +++ b/test/lib/worker/runner/mocha-runner/mocha-adapter.js @@ -0,0 +1,5 @@ +'use strict'; + +describe('worker/mocha-adapter', () => { + it('TODO'); +}); diff --git a/test/lib/worker/runner/mocha-runner/single-test-mocha-adapter.js b/test/lib/worker/runner/mocha-runner/single-test-mocha-adapter.js new file mode 100644 index 000000000..421922e2f --- /dev/null +++ b/test/lib/worker/runner/mocha-runner/single-test-mocha-adapter.js @@ -0,0 +1,5 @@ +'use strict'; + +describe('worker/single-test-mocha-adapter', () => { + it('TODO'); +}); diff --git a/test/utils.js b/test/utils.js index c1f4a3c68..f9cd7c1fc 100644 --- a/test/utils.js +++ b/test/utils.js @@ -16,14 +16,16 @@ function makeConfigStub(opts) { browsers: ['some-default-browser'], retry: 0, sessionsPerBrowser: 1, - testsPerSession: Infinity + testsPerSession: Infinity, + configPath: 'some-default-config-path' }); const config = { browsers: {}, plugins: opts.plugins, system: opts.system || {mochaOpts: {}}, - sets: opts.sets || {} + sets: opts.sets || {}, + configPath: opts.configPath }; opts.browsers.forEach(function(browserId) { From d1a2581201e5b673946d18cd8bc819ef6ee6e92e Mon Sep 17 00:00:00 2001 From: egavr Date: Tue, 15 Aug 2017 09:56:32 +0300 Subject: [PATCH 3/3] docs: update documentation on the API --- README.md | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 0de0040ca..2ae4b5cd2 100644 --- a/README.md +++ b/README.md @@ -670,13 +670,10 @@ With the API, you can use Hermione programmatically in your scripts or build too ```js const Hermione = require('hermione'); -const hermione = new Hermione(config, allowOverrides); +const hermione = new Hermione(config); ``` -* **config** (required) `String|Object` – Path to the configuration file that will be read relative to `process.cwd` or [configuration object](#hermioneconfjs). -* **allowOverrides** (optional) `Object` – Switch on/off [configuration override](#overriding-settings) via environment variables or CLI options: - * **env** (optional) `Boolean` – Switch on/off configuration override via environment variables. Default is `false`. - * **cli** (optional) `Boolean` - Switch on/off configuration override via CLI options. Default is `false`. +* **config** (required) `String` – Path to the configuration file that will be read relative to `process.cwd`. ### run @@ -694,6 +691,7 @@ hermione.run(testPaths, options) * **options** (optional) `Object` * **reporters** (optional) `String[]` – Test result reporters. * **browsers** (optional) `String[]` – Browsers to run tests in. + * **sets** (optional) `String[]`– Sets to run tests in. * **grep** (optional) `RegExp` – Pattern that defines which tests to run. ### readTests