Skip to content

Commit

Permalink
Merge pull request #167 from gemini-testing/feat/sub-processes
Browse files Browse the repository at this point in the history
feat: running of tests in subprocesses
  • Loading branch information
eGavr authored Aug 15, 2017
2 parents 72cb53c + d1a2581 commit 66a1552
Show file tree
Hide file tree
Showing 47 changed files with 1,432 additions and 819 deletions.
8 changes: 3 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand Down
35 changes: 35 additions & 0 deletions lib/base-hermione.js
Original file line number Diff line number Diff line change
@@ -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);
}
};
5 changes: 1 addition & 4 deletions lib/browser-pool/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
16 changes: 8 additions & 8 deletions lib/browser.js
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand All @@ -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())
Expand All @@ -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() {
Expand Down
2 changes: 1 addition & 1 deletion lib/cli/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions lib/config/defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ module.exports = {
debug: false,
sessionsPerBrowser: 1,
testsPerSession: Infinity,
workers: 1,
retry: 0,
mochaOpts: {
slow: 10000,
Expand Down
37 changes: 23 additions & 14 deletions lib/config/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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
}));
}

Expand All @@ -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;
});
}
};
4 changes: 3 additions & 1 deletion lib/config/options.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
2 changes: 0 additions & 2 deletions lib/constants/runner-events.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,6 @@ const getSyncEvents = () => ({
BEFORE_FILE_READ: 'beforeFileRead',
AFTER_FILE_READ: 'afterFileRead',

NEW_BROWSER: 'newBrowser',

SUITE_BEGIN: 'beginSuite',
SUITE_END: 'endSuite',

Expand Down
38 changes: 8 additions & 30 deletions lib/hermione.js
Original file line number Diff line number Diff line change
@@ -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));

Expand All @@ -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());
}
Expand All @@ -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;
}
Expand Down
48 changes: 38 additions & 10 deletions lib/runner/index.js
Original file line number Diff line number Diff line change
@@ -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');
Expand All @@ -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, [
Expand All @@ -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()));
Expand Down
Loading

0 comments on commit 66a1552

Please sign in to comment.