VulpesOS/gaia/tests/jsmarionette/runner/marionette-js-runner/lib/runner.js
OverSu b38dea0597 Publish Vulpes Preview 15 sources for desktop, Android and Tundra
Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
2026-09-30 23:13:03 +02:00

250 lines
7.1 KiB
JavaScript
Executable file

'use strict';
var assert = require('assert');
var assign = require('object-assign');
var fork = require('child_process').fork;
var reporter = require('mocha-json-proxy/reporter');
var resolveModule = require('./resolvemodule');
var RPC = require('./rpc');
var Consumer = require('mocha-json-proxy/consumer');
var Promise = require('promise');
var emptyPort = Promise.denodeify(require('empty-port'));
// resolve module paths (required otherwise stuff breaks when the
// marionette-mocha binary is invoked outside of this package.
// Paths cannot be found via their relative module names. (mocha vs
// ./node_modules/mocha)
var MOCHA_BINARY = resolveModule('mocha', 'bin', '_mocha');
var PROXY_BINARY = resolveModule('mocha-json-proxy', 'reporter.js');
var STARTING_PORT = 1024;
var MAX_PORT = 65535;
var MARIONETTE_ENABLED_PREF = 'marionette.defaultPrefs.enabled';
var MARIONETTE_PORT_PREF = 'marionette.defaultPrefs.port';
function RemoteBuilder(builder, config) {
this.builder = builder;
this.destroy = Promise.denodeify(builder.destroy.bind(builder));
this.config = config;
}
RemoteBuilder.prototype = {
$rpc: { methods: ['destroy', 'getConfig'] },
getConfig: function() {
return Promise.resolve(this.config);
}
};
function RemoteHost(runner, instance) {
this.runner = runner;
this.instance = instance;
}
RemoteHost.prototype = {
$rpc: { methods: ['destroy', 'createSession'] },
destroy: function() {
return this.instance.destroy().then(function() {
// Removal _must_ be async otherwise this can mess up iteration (forEach).
var idx = this.runner.hosts.indexOf(this);
if (idx !== -1) {
this.runner.hosts.splice(idx, 1);
}
}.bind(this));
},
createSession: function(profileConfig, options) {
options = assign({}, this.runner.host.options, options);
if ('port' in profileConfig) {
options.port = profileConfig.port;
}
return this.runner.host.module.createSession(
this.instance,
profileConfig.profile,
options
);
}
};
/**
* Mocha child instance- responsible for managing hosts (in
* this process) and then spawning a separate process for the
* mocha tests to run in...
*
*
* var child = new Child({
* argv: process.argv.slice(1)
* });
*
* var childProcess = child.run();
*
* Options
* - (Array) argv: argv for mocha.
* - (Function) Host: class
* - (Function) ProfileBuilder: class
* - (Object) profileBase: options to use for all builds.
*
* @constructor
* @param {Options} options for process.
*/
function Runner(options) {
assert(options.host, '.host is required');
assert(options.hostLog, '.hostLog is required');
assert(options.profileBuilder, '.profileBuilder is required');
this.host = options.host;
this.hostLog = options.hostLog;
this.profileBuilder = options.profileBuilder;
this.verbose = !!options.verbose;
this.desiredCapabilities = options.desiredCapabilities;
// base details used in every profile
this.profileBase = options.profileBase;
this.hosts = [];
this._creatingHosts = [];
}
Runner.prototype = {
createProfile: function(overrides) {
var self = this;
var config = {};
var builder = new this.profileBuilder.constructor(this.profileBase);
var build = Promise.denodeify(builder.build.bind(builder));
return emptyPort({ startPort: STARTING_PORT, maxPort: MAX_PORT })
.then(function(port) {
config.port = port;
var profileOptions = self.profileOptions(port, overrides);
return build(profileOptions);
})
.then(function(path) {
config.profile = path;
return new RemoteBuilder(builder, config);
});
},
/**
* Generates the default set of profile options.
*
* @param {Number} port for marionette connection.
* @param {Object} options for build.
* @return {Object} full configuration for build.
*/
profileOptions: function(port, options) {
options = options || {};
options.prefs = options.prefs || {};
options.hostOptions = options.hostOptions || {};
options.hostOptions.port = port;
options.prefs[MARIONETTE_ENABLED_PREF] = true;
options.prefs[MARIONETTE_PORT_PREF] = port;
return options;
},
createHost: function() {
var promise = this.host.module.createHost().then(function(host) {
if (host.log) {
host.log.pipe(this.hostLog, { end: false });
}
var remote = new RemoteHost(this, host);
this.hosts.push(remote);
this._creatingHosts.splice(this._creatingHosts.indexOf(promise), 1);
return remote;
}.bind(this));
this._creatingHosts.push(promise);
return promise;
},
destroy: function() {
// To cleanly destroy we must wait until everything is in a known state
// first. This will wait for everything to be properly started then cleanly
// destroy the hosts.
if (this._creatingHosts.length) {
return Promise.all(this._creatingHosts).then(this.destroy.bind(this));
}
var hostsDestroy = this.hosts.map(function(host) {
// Destroy will remove object from .hosts.
return host.destroy();
});
// Immediately try to kill child process...
this.process.kill();
return Promise.all(hostsDestroy).then(function() {
assert(
Object.keys(this.hosts).length === 0,
'all hosts removed.'
);
}.bind(this))
.catch(function(error) {
console.error('One or more hosts encountered issues during shutdown.');
console.error(error.toString());
});
},
/**
* Spawn the process for the mocha child runner.
*/
spawn: function(argv) {
// reporter must come after everything else to override a previous reporter
argv = argv.concat(
['--reporter', PROXY_BINARY]
);
// pass all environment variables to the child...
var env = {};
for (var key in process.env) {
env[key] = process.env[key];
}
var options = {
env: env,
// silent is similar to stdio: ['pipe', 'pipe'] + an ipc channel (send).
silent: true
};
// turn on the fork options so we get ipc messages.
options.env[reporter.FORK_ENV] = 1;
if (this.desiredCapabilities) {
// Encode and pass desired capabilities to mocha process.
options.env.DESIRED_CAPABILITIES =
this._encode(this.desiredCapabilities);
}
options.env.VERBOSE = this._encode(this.verbose ? '1' : '0');
if (this.host.options.device_type) {
// Encode and pass device type to mocha process.
options.env.DEVICE_TYPE = this._encode(this.host.options.device_type);
}
this.process = fork(MOCHA_BINARY, argv, options);
this.mochaRunnerProxy = new Consumer(this.process);
this.rpc = new RPC(this.process.send.bind(this.process));
this.rpc.register('runner', this);
// must come after constructing the consumer otherwise messages
// are sent before the consumer is ready to receive them.
this.process.on('message', this.rpc.handle());
this.process.on('exit', function() {
this.process = null;
}.bind(this));
},
_encode: function(data) {
return new Buffer(JSON.stringify(data)).toString('base64');
}
};
module.exports = Runner;