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.
This commit is contained in:
parent
93e3a48004
commit
b38dea0597
25901 changed files with 9982925 additions and 369 deletions
61
gaia/tests/jsmarionette/runner/marionette-js-runner/lib/applymanifest.js
Executable file
61
gaia/tests/jsmarionette/runner/marionette-js-runner/lib/applymanifest.js
Executable file
|
|
@ -0,0 +1,61 @@
|
|||
'use strict';
|
||||
var fs = require('fs'),
|
||||
path = require('path'),
|
||||
difference = require('array-difference'),
|
||||
intersect = require('intersect');
|
||||
|
||||
function resolvePath(pathStr) {
|
||||
return path.resolve(pathStr);
|
||||
}
|
||||
|
||||
/**
|
||||
* Defensively resolve arguments to absolute paths (in order to avoid
|
||||
* "resolving" arguments that do not actually reference files)
|
||||
*
|
||||
* @private
|
||||
* @param {String} argument from the command-line.
|
||||
* @return {String}
|
||||
*/
|
||||
function defensiveResolvePath(argument) {
|
||||
var resolved = resolvePath(argument);
|
||||
|
||||
if (fs.existsSync(resolved)) {
|
||||
return resolved;
|
||||
}
|
||||
|
||||
return argument;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform the input command-line arguments according to the directives found
|
||||
* in the specified manifest file.
|
||||
*
|
||||
* @param {Object} manifest referencing the manifest file.
|
||||
* @param {Array} files to filter.
|
||||
* @return {Array} filtered files.
|
||||
*/
|
||||
function applyManifest(manifest, files) {
|
||||
var fullPaths = files.map(resolvePath);
|
||||
|
||||
var withoutFiles;
|
||||
if (manifest.whitelist) {
|
||||
manifest.whitelist = manifest.whitelist.map(resolvePath);
|
||||
// Select all file names that have not been specified
|
||||
withoutFiles = difference(fullPaths, manifest.whitelist);
|
||||
} else if (manifest.blacklist) {
|
||||
var blacklist = manifest.blacklist;
|
||||
if (!Array.isArray(blacklist)) {
|
||||
blacklist = Object.keys(blacklist);
|
||||
}
|
||||
blacklist = blacklist.map(resolvePath);
|
||||
// Select only the file names that have been specified
|
||||
withoutFiles = intersect(fullPaths, blacklist);
|
||||
}
|
||||
|
||||
fullPaths = fullPaths.map(defensiveResolvePath);
|
||||
|
||||
// Remove the appropriate file arguments from the list
|
||||
return difference(withoutFiles, fullPaths);
|
||||
}
|
||||
|
||||
module.exports = applyManifest;
|
||||
45
gaia/tests/jsmarionette/runner/marionette-js-runner/lib/error_ipc.js
Executable file
45
gaia/tests/jsmarionette/runner/marionette-js-runner/lib/error_ipc.js
Executable file
|
|
@ -0,0 +1,45 @@
|
|||
'use strict';
|
||||
/**
|
||||
Error passing helpers when doing ipc.
|
||||
*/
|
||||
|
||||
// Some usually non enumerable properties which we need to enumerate (for good
|
||||
// reasons!).
|
||||
var ERROR_KEYS = [
|
||||
'name',
|
||||
'code',
|
||||
'stack',
|
||||
'message'
|
||||
];
|
||||
|
||||
/**
|
||||
Convert something that might be an error into something we can send via IPC.
|
||||
|
||||
@param {?Erorr} maybeError something that might be an error.
|
||||
@return {Object}
|
||||
*/
|
||||
module.exports.serialize = function(maybeError) {
|
||||
if (!maybeError) return maybeError;
|
||||
return ERROR_KEYS.reduce(function(current, key) {
|
||||
// Skip adding the key if the key is not in the original error.
|
||||
// Note intentional use of != null to check for null or undefined.
|
||||
if (current[key] != null) {
|
||||
return current;
|
||||
}
|
||||
current[key] = maybeError[key];
|
||||
return current;
|
||||
}, {});
|
||||
};
|
||||
|
||||
/**
|
||||
Convert something that may have been serialized into an error object.
|
||||
|
||||
@param {?Object} serialized input from a serialize function.
|
||||
@return {Error} deserialized error.
|
||||
*/
|
||||
module.exports.deserialize = function(serialized) {
|
||||
if (!serialized) return;
|
||||
var error = new Error(serialized.message || '');
|
||||
for (var key in serialized) error[key] = serialized[key];
|
||||
return error;
|
||||
};
|
||||
20
gaia/tests/jsmarionette/runner/marionette-js-runner/lib/optsfileparser.js
Executable file
20
gaia/tests/jsmarionette/runner/marionette-js-runner/lib/optsfileparser.js
Executable file
|
|
@ -0,0 +1,20 @@
|
|||
'use strict';
|
||||
var debug = require('debug')('marionette-js-runner:optsfile'),
|
||||
fs = require('fs');
|
||||
|
||||
/**
|
||||
* Reads the contents of given opts file and returns an array.
|
||||
*
|
||||
* @param {String} file to load opts from.
|
||||
* @return {Array} options.
|
||||
*/
|
||||
function loadOptsFile(file) {
|
||||
debug('loading opts file', file);
|
||||
var opts = fs.readFileSync(file, 'utf8')
|
||||
.trim()
|
||||
.split(/\s+/);
|
||||
|
||||
return opts;
|
||||
}
|
||||
|
||||
module.exports = loadOptsFile;
|
||||
21
gaia/tests/jsmarionette/runner/marionette-js-runner/lib/require_reporter.js
Executable file
21
gaia/tests/jsmarionette/runner/marionette-js-runner/lib/require_reporter.js
Executable file
|
|
@ -0,0 +1,21 @@
|
|||
'use strict';
|
||||
/**
|
||||
Helper to import the mocha reporters...
|
||||
*/
|
||||
module.exports = function requireReporter(argv, path) {
|
||||
var reporter;
|
||||
[
|
||||
argv.reporter,
|
||||
path,
|
||||
'mocha/lib/reporters/' + (path || 'dot')
|
||||
].some(function(value) {
|
||||
try {
|
||||
reporter = require(value);
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
return reporter;
|
||||
};
|
||||
11
gaia/tests/jsmarionette/runner/marionette-js-runner/lib/require_resolve.js
Executable file
11
gaia/tests/jsmarionette/runner/marionette-js-runner/lib/require_resolve.js
Executable file
|
|
@ -0,0 +1,11 @@
|
|||
'use strict';
|
||||
var fsPath = require('path');
|
||||
|
||||
module.exports = function requireResolve(path) {
|
||||
try {
|
||||
return require(path);
|
||||
} catch (e) {
|
||||
// require + resolve to current cwd if relative path.
|
||||
return require(fsPath.resolve(path));
|
||||
}
|
||||
};
|
||||
27
gaia/tests/jsmarionette/runner/marionette-js-runner/lib/resolvemodule.js
Executable file
27
gaia/tests/jsmarionette/runner/marionette-js-runner/lib/resolvemodule.js
Executable file
|
|
@ -0,0 +1,27 @@
|
|||
'use strict';
|
||||
var fsPath = require('path');
|
||||
|
||||
/**
|
||||
* Resolves a path to a given module. This only works if the module exports a
|
||||
* module from the root of its package (almost all modules ever).
|
||||
*
|
||||
* resolveModule('mocha', 'bin', '_mocha');
|
||||
* // => /node_modules/(...)/mocha/bin/_mocha
|
||||
*
|
||||
* @private
|
||||
* @param {String} module path.
|
||||
* @param {...*} parts to extend path with.
|
||||
* @return {String}
|
||||
*/
|
||||
function resolveModule(module, parts) {
|
||||
var args = Array.prototype.slice.call(arguments);
|
||||
parts = args.slice(1);
|
||||
|
||||
// resolve the module
|
||||
var modulePath = fsPath.dirname(require.resolve(module));
|
||||
|
||||
// add given parts of the path to the module path.
|
||||
return fsPath.join.apply(fsPath, [modulePath].concat(parts));
|
||||
}
|
||||
|
||||
module.exports = resolveModule;
|
||||
301
gaia/tests/jsmarionette/runner/marionette-js-runner/lib/rpc.js
Executable file
301
gaia/tests/jsmarionette/runner/marionette-js-runner/lib/rpc.js
Executable file
|
|
@ -0,0 +1,301 @@
|
|||
'use strict';
|
||||
var Promise = require('promise');
|
||||
var uuid = require('uuid');
|
||||
var errorIPC = require('./error_ipc');
|
||||
var assert = require('assert');
|
||||
var util = require('util');
|
||||
var debug = require('debug')('marionette-js-runner:rpc');
|
||||
|
||||
var EventEmitter = require('events').EventEmitter;
|
||||
|
||||
/**
|
||||
|
||||
var rpc = new RPC(process.send.bind(process));
|
||||
|
||||
@constructor
|
||||
@param {Function} send method to use.
|
||||
*/
|
||||
function RPC(send) {
|
||||
this.id = uuid.v4();
|
||||
this.objects = {};
|
||||
|
||||
// Internal event handler.
|
||||
this._events = new EventEmitter();
|
||||
this.send = send;
|
||||
}
|
||||
|
||||
RPC.prototype = {
|
||||
|
||||
register: function(name, object) {
|
||||
debug('register', name);
|
||||
assert(!this.objects[name], 'cannot re-register name: ' + name);
|
||||
assert(typeof object === 'object', 'invalid object is a: ' + typeof obj);
|
||||
this.objects[name] = object;
|
||||
},
|
||||
|
||||
/**
|
||||
Note this is currently unsafe action it may result in unfulfilled promises.
|
||||
*/
|
||||
unregister: function(name) {
|
||||
delete this.objects[name];
|
||||
},
|
||||
|
||||
/**
|
||||
Generate the client side for the RPC call.
|
||||
|
||||
```js
|
||||
var objClient = rpc.client('obj', ['doStuff']);
|
||||
|
||||
objClient.doStuff().then(...)
|
||||
```
|
||||
|
||||
@param {String} name of the remote object.
|
||||
@param {Array<String>} methods which remote can handle.
|
||||
@return {Object} client rpc interface.
|
||||
*/
|
||||
client: function(name, methods) {
|
||||
assert(typeof name === 'string', 'client name must be string');
|
||||
debug('create client', name, methods);
|
||||
|
||||
var client = {
|
||||
$rpcConnected: true,
|
||||
|
||||
description: util.format('[RPC Client] <%s>', name),
|
||||
toString: function() {
|
||||
return this.description;
|
||||
}
|
||||
};
|
||||
|
||||
methods.forEach(function(method) {
|
||||
client[method] = (function() {
|
||||
if (!client.$rpcConnected) {
|
||||
return new Promise(function(accept, reject) {
|
||||
return reject(new Error(
|
||||
'Call on destroyed rpc object'
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
var args = Array.prototype.slice.call(arguments);
|
||||
return this.request(name, method, args);
|
||||
}.bind(this));
|
||||
}, this);
|
||||
|
||||
var markDestoyed = (function markDestoyed() {
|
||||
client.$rpcConnected = false;
|
||||
this.send(['rpc-unregister', name]);
|
||||
}.bind(this));
|
||||
|
||||
var destroy = client.destroy;
|
||||
client.destroy = function() {
|
||||
// If the client happens to have a destroy method call that first!
|
||||
var p;
|
||||
if (destroy) {
|
||||
p = destroy.apply(this, Array.prototype.slice.call(arguments));
|
||||
} else {
|
||||
p = Promise.resolve();
|
||||
}
|
||||
markDestoyed();
|
||||
return p;
|
||||
};
|
||||
|
||||
return client;
|
||||
},
|
||||
|
||||
serialize: function(value) {
|
||||
if (value && value instanceof Error) {
|
||||
return errorIPC.serialize(value);
|
||||
}
|
||||
return value;
|
||||
},
|
||||
|
||||
deserialize: function(value) {
|
||||
if (typeof value === 'object' && value.message && value.stack) {
|
||||
return errorIPC.deserialize(value);
|
||||
}
|
||||
return value;
|
||||
},
|
||||
|
||||
request: function(name, method, args) {
|
||||
var id = uuid.v4();
|
||||
debug('issue request', name, method, id);
|
||||
|
||||
return new Promise(function(accept, reject) {
|
||||
this.send([
|
||||
'rpc-request',
|
||||
this.id,
|
||||
{
|
||||
name: name,
|
||||
method: method,
|
||||
id: id,
|
||||
arguments: args.map(this.serialize.bind(this))
|
||||
}
|
||||
]);
|
||||
|
||||
this._events.once('response ' + id, function(payload) {
|
||||
debug('handle request', method, id, payload);
|
||||
switch (payload.result) {
|
||||
case 'resolved':
|
||||
return accept(this.deserialize(payload.value));
|
||||
case 'rejected':
|
||||
return reject(this.deserialize(payload.value));
|
||||
case 'rpc':
|
||||
return accept(this.client(
|
||||
payload.value.name, payload.value.spec.methods
|
||||
));
|
||||
default:
|
||||
throw new Error('Unknown payload format: ' + payload);
|
||||
}
|
||||
}.bind(this));
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
/**
|
||||
Used in conjunction with process.on('message') can handle RPC requests.
|
||||
|
||||
RPC Payload format:
|
||||
|
||||
Request:
|
||||
```
|
||||
[
|
||||
'rpc-request',
|
||||
'<uuid>',
|
||||
{
|
||||
name: '<rpc name>',
|
||||
method: '<method>',
|
||||
id: '<id>',
|
||||
arguments: []
|
||||
}
|
||||
]
|
||||
|
||||
Response:
|
||||
```
|
||||
[
|
||||
'rpc-response',
|
||||
'<uuid>',
|
||||
{
|
||||
id: '<id>',
|
||||
result: '(rejected | resolved)'
|
||||
value: <value>
|
||||
}
|
||||
]
|
||||
|
||||
Unregister:
|
||||
```
|
||||
[
|
||||
'rpc-unregister',
|
||||
'<name>'
|
||||
]
|
||||
```
|
||||
|
||||
@return {Function} handler suitable for process.on('message')
|
||||
*/
|
||||
handle: function() {
|
||||
return function(msg) {
|
||||
if (!Array.isArray(msg)) {
|
||||
return;
|
||||
}
|
||||
var type = msg[0];
|
||||
var id = msg[1];
|
||||
var payload = msg[2];
|
||||
|
||||
switch (type) {
|
||||
case 'rpc-request':
|
||||
return this.handleRequest(id, payload);
|
||||
case 'rpc-unregister':
|
||||
return this.unregister(id);
|
||||
case 'rpc-response':
|
||||
// Do not handle cases which do not belong to this rpc response.
|
||||
if (id !== this.id) {
|
||||
return;
|
||||
}
|
||||
return this.handleResponse(id, payload);
|
||||
}
|
||||
}.bind(this);
|
||||
},
|
||||
|
||||
handleResponse: function(id, payload) {
|
||||
debug('handle response', id, payload);
|
||||
this._events.emit('response ' + payload.id, payload);
|
||||
},
|
||||
|
||||
handleRequest: function(id, payload) {
|
||||
debug('handle request', id, payload);
|
||||
// object check first...
|
||||
var object = this.objects[payload.name];
|
||||
if (!object) {
|
||||
return this.send([
|
||||
'rpc-response',
|
||||
id,
|
||||
{
|
||||
id: payload.id,
|
||||
result: 'rejected',
|
||||
value: { message: 'Unknown object name: ' + payload.name }
|
||||
}
|
||||
]);
|
||||
}
|
||||
|
||||
// Ensure method name is there...
|
||||
if (!object[payload.method]) {
|
||||
return this.send([
|
||||
'rpc-response',
|
||||
id,
|
||||
{
|
||||
id: payload.id,
|
||||
result: 'rejected',
|
||||
value: {
|
||||
message: util.format(
|
||||
'Unknown method "%s" for object name: "%s"',
|
||||
payload.method,
|
||||
payload.name
|
||||
)
|
||||
}
|
||||
}
|
||||
]);
|
||||
}
|
||||
|
||||
// Finally run the method!
|
||||
object[payload.method].apply(object, payload.arguments)
|
||||
.then(function(value) {
|
||||
// if this object is an rpc object we send an id to reference the
|
||||
// object.
|
||||
if (value && typeof value === 'object' && value.$rpc) {
|
||||
var objectId = 'rpc-obj-' + uuid.v4();
|
||||
this.register(objectId, value);
|
||||
this.send([
|
||||
'rpc-response',
|
||||
id,
|
||||
{
|
||||
id: payload.id,
|
||||
result: 'rpc',
|
||||
value: { name: objectId, spec: value.$rpc }
|
||||
}
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
this.send([
|
||||
'rpc-response',
|
||||
id,
|
||||
{
|
||||
id: payload.id,
|
||||
result: 'resolved',
|
||||
value: this.serialize(value || '')
|
||||
}
|
||||
]);
|
||||
}.bind(this))
|
||||
.catch (function(err) {
|
||||
return this.send([
|
||||
'rpc-response',
|
||||
id,
|
||||
{
|
||||
id: payload.id,
|
||||
result: 'rejected',
|
||||
value: errorIPC.serialize(err)
|
||||
}
|
||||
]);
|
||||
}.bind(this));
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = RPC;
|
||||
250
gaia/tests/jsmarionette/runner/marionette-js-runner/lib/runner.js
Executable file
250
gaia/tests/jsmarionette/runner/marionette-js-runner/lib/runner.js
Executable file
|
|
@ -0,0 +1,250 @@
|
|||
'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;
|
||||
172
gaia/tests/jsmarionette/runner/marionette-js-runner/lib/runtime/hostmanager.js
Executable file
172
gaia/tests/jsmarionette/runner/marionette-js-runner/lib/runtime/hostmanager.js
Executable file
|
|
@ -0,0 +1,172 @@
|
|||
'use strict';
|
||||
var Logger = require('marionette-js-logger');
|
||||
var Marionette = require('marionette-client');
|
||||
var Promise = require('promise');
|
||||
var debug = require('debug')('marionette-js-runner:hostmanager');
|
||||
var prettyjson = require('prettyjson');
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
*/
|
||||
function HostManager() {
|
||||
this.plugins = [];
|
||||
}
|
||||
|
||||
|
||||
HostManager.prototype = {
|
||||
/**
|
||||
* Adds a plugin to the stack of plugins.
|
||||
*
|
||||
* @param {String} name of plugin.
|
||||
* @param {Function} plugin function to invoke.
|
||||
* @param {Object} options for plugin.
|
||||
*/
|
||||
addPlugin: function(name, plugin, options) {
|
||||
var plugins = this.plugins;
|
||||
suiteSetup(function() {
|
||||
plugins.push({ plugin: plugin, name: name, options: options });
|
||||
});
|
||||
|
||||
suiteTeardown(function() {
|
||||
plugins.pop();
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Setup a host inside of the current execution context.
|
||||
* @param {Object} options an options object that contains:
|
||||
* {Object} profile settings for host.
|
||||
* {Object} driver for marionette.
|
||||
* {Object} desired capabilities for mariontte
|
||||
* @return {Marionette.Client} instance of a client.
|
||||
*/
|
||||
createHost: function(options) {
|
||||
options = options || {};
|
||||
var profile = options.profile || {};
|
||||
var driver = options.driver || Marionette.Drivers.TcpSync;
|
||||
var desiredCapabilities = options.desiredCapabilities ||
|
||||
this.desiredCapabilities;
|
||||
|
||||
debug('create host', profile);
|
||||
|
||||
// rpc interfaces created...
|
||||
var host, session, profileBuilder;
|
||||
|
||||
var client = new Marionette.Client(null, { lazy: true });
|
||||
var plugins = this.plugins;
|
||||
var verbose = this.verbose;
|
||||
if (verbose) {
|
||||
this.addPlugin('logger', Logger);
|
||||
}
|
||||
|
||||
suiteSetup(function() {
|
||||
var ctx = this;
|
||||
return this.runner.createHost().then(function(_host) {
|
||||
host = _host;
|
||||
// client is tacked on mostly for error handling.
|
||||
host.client = client;
|
||||
ctx.hosts.push(host);
|
||||
});
|
||||
});
|
||||
|
||||
setup(function() {
|
||||
var driverInstance;
|
||||
var pluginReferences = {};
|
||||
var createdProfileConfig;
|
||||
|
||||
return this.runner.createProfile(profile)
|
||||
.then(function(_profileBuilder) {
|
||||
profileBuilder = _profileBuilder;
|
||||
return profileBuilder.getConfig();
|
||||
})
|
||||
.then(function(_createdProfileConfig) {
|
||||
host.profileConfig = createdProfileConfig = _createdProfileConfig;
|
||||
return host.createSession(
|
||||
createdProfileConfig, profile.hostOptions
|
||||
);
|
||||
})
|
||||
.then(function(_session) {
|
||||
session = _session;
|
||||
host.session = session;
|
||||
|
||||
// build object with all plugins
|
||||
plugins.forEach(function(plugin) {
|
||||
pluginReferences[plugin.name] = {
|
||||
plugin: plugin.plugin,
|
||||
options: plugin.options
|
||||
};
|
||||
});
|
||||
|
||||
driverInstance = new driver({
|
||||
port: createdProfileConfig.port,
|
||||
// XXX: make configurable
|
||||
connectionTimeout: (60 * 1000) * 5 // 5 minutes
|
||||
});
|
||||
|
||||
return Promise.denodeify(
|
||||
driverInstance.connect.bind(driverInstance)
|
||||
)();
|
||||
})
|
||||
.then(function() {
|
||||
client.resetWithDriver(driverInstance);
|
||||
|
||||
for (var name in pluginReferences) {
|
||||
debug('add plugin', name);
|
||||
// remove old plugin reference
|
||||
delete client[name];
|
||||
|
||||
// setup new plugin
|
||||
client.plugin(
|
||||
name,
|
||||
pluginReferences[name].plugin,
|
||||
pluginReferences[name].options
|
||||
);
|
||||
}
|
||||
|
||||
if (verbose) {
|
||||
client.logger.pollMessages();
|
||||
client.logger.on('message', function(msg) {
|
||||
console.log(prettyjson.render(msg) + '\n');
|
||||
});
|
||||
}
|
||||
|
||||
return new Promise(function(accept) {
|
||||
client.startSession(accept, desiredCapabilities);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// turn off the client
|
||||
teardown(function() {
|
||||
if (verbose) {
|
||||
// Ensure all messages are drained before shutting down.
|
||||
client.logger.grabLogMessages();
|
||||
}
|
||||
|
||||
var deleteSession = Promise.denodeify(client.deleteSession.bind(client));
|
||||
return deleteSession()
|
||||
.then(function() {
|
||||
return session.destroy();
|
||||
})
|
||||
.catch(function() {
|
||||
// If something goes wrong during teardown we probably don't care
|
||||
// since we've already run the test? We're swallowing this to ignore
|
||||
// cases where we do a bad job of closing a marionette connection
|
||||
// and then fail a test as in
|
||||
// https://bugzilla.mozilla.org/show_bug.cgi?id=1104285
|
||||
});
|
||||
});
|
||||
|
||||
suiteTeardown(function() {
|
||||
if (typeof host.destroy === 'function') {
|
||||
return host.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
return client;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
// expose host manager.
|
||||
module.exports.HostManager = HostManager;
|
||||
220
gaia/tests/jsmarionette/runner/marionette-js-runner/lib/ui.js
Executable file
220
gaia/tests/jsmarionette/runner/marionette-js-runner/lib/ui.js
Executable file
|
|
@ -0,0 +1,220 @@
|
|||
'use strict';
|
||||
/**
|
||||
Custom marionette-runner interface for mocha.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Module dependencies.
|
||||
*/
|
||||
|
||||
var Suite = require('mocha/lib/suite');
|
||||
var HostManager = require('./runtime/hostmanager').HostManager;
|
||||
var RPC = require('./rpc');
|
||||
var Test = require('mocha/lib/test');
|
||||
var escapeRe = require('escape-string-regexp');
|
||||
|
||||
function MarionetteTest(title, hostManager, fn) {
|
||||
this.hostManager = hostManager;
|
||||
Test.call(this, title, fn);
|
||||
}
|
||||
|
||||
MarionetteTest.prototype = {
|
||||
__proto__: Test.prototype,
|
||||
|
||||
run: function(fn) {
|
||||
return Test.prototype.run.call(this, function(err) {
|
||||
if (err) {
|
||||
// In the case of an error we attempt to run host/client specific error
|
||||
// handles where possible so first we attempt to see if there is a host
|
||||
// that matches a client...
|
||||
if (!this.ctx.hosts.length && this.ctx.hosts.length || !err.client) {
|
||||
return fn(err);
|
||||
}
|
||||
|
||||
var hosts = this.ctx.hosts;
|
||||
|
||||
// attempt to find the correct host...
|
||||
var host;
|
||||
for (var i = 0; i < hosts.length; i++) {
|
||||
if (
|
||||
// In the case of direct match then we found it...
|
||||
hosts[i].client === err.client ||
|
||||
// The client's scope method creates prototype chains so if it is in
|
||||
// the same chain it is also a match...
|
||||
hosts[i].client.isPrototypeOf(err.client)
|
||||
) {
|
||||
host = hosts[i];
|
||||
|
||||
host.session.checkError(host.profileConfig, err)
|
||||
.then(fn)
|
||||
.catch(fn);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
fn();
|
||||
}.bind(this));
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
@param {Object} suite root suite for mocha.
|
||||
*/
|
||||
module.exports = function(suite) {
|
||||
// initialize the RPC bridge...
|
||||
var rpc = new RPC(process.send.bind(process));
|
||||
process.on('message', rpc.handle());
|
||||
|
||||
var suites = [suite];
|
||||
var manager = new HostManager();
|
||||
|
||||
// setup global hooks
|
||||
suite.beforeAll('marionette-mocha global init', function() {
|
||||
this.hosts = [];
|
||||
this.runner = rpc.client('runner', ['createHost', 'createProfile']);
|
||||
});
|
||||
|
||||
suite.on('pre-require', function(context, file, mocha) {
|
||||
/**
|
||||
* Execute before each test case.
|
||||
*/
|
||||
context.setup = function(name, fn) {
|
||||
suites[0].beforeEach(name, fn);
|
||||
};
|
||||
|
||||
/**
|
||||
* Execute after each test case.
|
||||
*/
|
||||
context.teardown = function(name, fn) {
|
||||
suites[0].afterEach(name, fn);
|
||||
};
|
||||
|
||||
/**
|
||||
* Execute before the suite.
|
||||
*/
|
||||
context.suiteSetup = function(name, fn) {
|
||||
suites[0].beforeAll(name, fn);
|
||||
};
|
||||
|
||||
/**
|
||||
* Execute after the suite.
|
||||
*/
|
||||
context.suiteTeardown = function(name, fn) {
|
||||
suites[0].afterAll(name, fn);
|
||||
};
|
||||
|
||||
/**
|
||||
* Describe a "suite" with the given `title`
|
||||
* and callback `fn` containing nested suites
|
||||
* and/or tests.
|
||||
*/
|
||||
context.suite = function(title, fn) {
|
||||
var suite = Suite.create(suites[0], title);
|
||||
suite.file = file;
|
||||
suites.unshift(suite);
|
||||
fn.call(suite);
|
||||
suites.shift();
|
||||
return suite;
|
||||
};
|
||||
|
||||
context.marionette = context.suite;
|
||||
|
||||
/**
|
||||
* global state modifies for marionette
|
||||
*
|
||||
* @type {Object}
|
||||
*/
|
||||
// Setup global state manager for the marionette runtime.
|
||||
manager.desiredCapabilities = decode('DESIRED_CAPABILITIES');
|
||||
manager.verbose = decode('VERBOSE') === '1';
|
||||
manager.deviceType = decode('DEVICE_TYPE');
|
||||
context.marionette._manager = manager;
|
||||
context.marionette.client = manager.createHost.bind(manager);
|
||||
context.marionette.plugin = manager.addPlugin.bind(manager);
|
||||
|
||||
/**
|
||||
* Pending suite.
|
||||
*/
|
||||
context.suite.skip = function(title, fn) {
|
||||
var suite = Suite.create(suites[0], title);
|
||||
suite.pending = true;
|
||||
suites.unshift(suite);
|
||||
fn.call(suite);
|
||||
suites.shift();
|
||||
};
|
||||
|
||||
/**
|
||||
* Exclusive test-case.
|
||||
*/
|
||||
context.suite.only = function(title, fn) {
|
||||
var suite = context.suite(title, fn);
|
||||
mocha.grep(suite.fullTitle());
|
||||
};
|
||||
|
||||
/**
|
||||
* Describe a specification or test-case
|
||||
* with the given `title`, `options` list device to run on,
|
||||
* and callback `fn` acting as a thunk.
|
||||
*/
|
||||
context.test = function(title, options, fn) {
|
||||
// Second argument is optional, put default device if it doesn't exist.
|
||||
if (typeof options !== 'object') {
|
||||
if (typeof options === 'function') {
|
||||
fn = options;
|
||||
}
|
||||
|
||||
// Run only phone tests by default.
|
||||
options = { devices: ['phone'] };
|
||||
}
|
||||
|
||||
if (options &&
|
||||
options.devices &&
|
||||
options.devices.indexOf(manager.deviceType) === -1) {
|
||||
// Ignore test.
|
||||
return;
|
||||
}
|
||||
|
||||
var suite = suites[0];
|
||||
if (suite.pending) fn = null;
|
||||
var test = new MarionetteTest(title, manager, fn);
|
||||
test.file = file;
|
||||
suite.addTest(test);
|
||||
return test;
|
||||
};
|
||||
|
||||
/**
|
||||
* Exclusive test-case.
|
||||
*/
|
||||
context.test.only = function(title, fn) {
|
||||
var test = context.test(title, fn);
|
||||
var reString = '^' + escapeRe(test.fullTitle()) + '$';
|
||||
mocha.grep(new RegExp(reString));
|
||||
};
|
||||
|
||||
/**
|
||||
* Pending test case.
|
||||
*/
|
||||
context.test.skip = function(title) {
|
||||
context.test(title);
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
function decode(name) {
|
||||
var encoded = process.env[name];
|
||||
if (!encoded) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var result;
|
||||
try {
|
||||
result = JSON.parse(new Buffer(encoded, 'base64').toString());
|
||||
} catch (error) {
|
||||
console.error('Could not parse', name, encoded);
|
||||
result = null;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
Loading…
Reference in a new issue