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
3
gaia/tests/jsmarionette/runner/marionette-js-runner/.npmignore
Executable file
3
gaia/tests/jsmarionette/runner/marionette-js-runner/.npmignore
Executable file
|
|
@ -0,0 +1,3 @@
|
|||
/crash
|
||||
/symbols
|
||||
/venv
|
||||
15
gaia/tests/jsmarionette/runner/marionette-js-runner/LICENSE
Executable file
15
gaia/tests/jsmarionette/runner/marionette-js-runner/LICENSE
Executable file
|
|
@ -0,0 +1,15 @@
|
|||
Copyright 2015, Mozilla Foundation
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
|
||||
29
gaia/tests/jsmarionette/runner/marionette-js-runner/bin/apply-manifest
Executable file
29
gaia/tests/jsmarionette/runner/marionette-js-runner/bin/apply-manifest
Executable file
|
|
@ -0,0 +1,29 @@
|
|||
#! /usr/bin/env node
|
||||
'use strict';
|
||||
|
||||
var applyManifest = require('../lib/applymanifest');
|
||||
var requireResolve = require('../lib/require_resolve');
|
||||
|
||||
var ArgumentParser = require('argparse').ArgumentParser;
|
||||
|
||||
var parser = new ArgumentParser({
|
||||
version: require('../package').version,
|
||||
description: 'Apply whitelist/blacklist manifests to test files',
|
||||
addHelp: true
|
||||
});
|
||||
|
||||
parser.addArgument(['manifest'], {
|
||||
type: requireResolve,
|
||||
help: 'Manifest file to use to filter given files'
|
||||
});
|
||||
|
||||
parser.addArgument(['files'], {
|
||||
nargs: '*',
|
||||
type: 'string'
|
||||
});
|
||||
|
||||
var args = parser.parseArgs();
|
||||
|
||||
process.stdout.write(
|
||||
applyManifest(args.manifest, args.files).join(' ')
|
||||
)
|
||||
276
gaia/tests/jsmarionette/runner/marionette-js-runner/bin/marionette-mocha
Executable file
276
gaia/tests/jsmarionette/runner/marionette-js-runner/bin/marionette-mocha
Executable file
|
|
@ -0,0 +1,276 @@
|
|||
#! /usr/bin/env node
|
||||
'use strict';
|
||||
|
||||
var ArgumentParser = require('argparse').ArgumentParser;
|
||||
var Runner = require('../lib/runner');
|
||||
var assert = require('assert');
|
||||
var fs = require('fs');
|
||||
var fsPath = require('path');
|
||||
var resolveModule = require('../lib/resolvemodule');
|
||||
var requireResolve = require('../lib/require_resolve');
|
||||
|
||||
var OPTS_FILE = 'marionette-mocha.opts';
|
||||
|
||||
var argv = ['test/', 'tests/', '.'].reduce(function(argv, path) {
|
||||
var optsFile = fsPath.join(path, OPTS_FILE);
|
||||
if (!fs.existsSync(optsFile)) {
|
||||
return argv;
|
||||
}
|
||||
|
||||
argv.splice.apply(
|
||||
argv,
|
||||
[2, 0].concat(require('../lib/optsfileparser')(optsFile))
|
||||
);
|
||||
|
||||
return argv;
|
||||
}, process.argv);
|
||||
|
||||
var requireReporter = require('../lib/require_reporter').bind(null, argv);
|
||||
|
||||
var parser = new ArgumentParser({
|
||||
version: require('../package').version,
|
||||
description: 'Marionette JS Runner',
|
||||
addHelp: false,
|
||||
});
|
||||
|
||||
parser.addArgument(['--help'], {
|
||||
action: 'storeTrue',
|
||||
help: 'Output help',
|
||||
defaultValue: false
|
||||
});
|
||||
|
||||
parser.addArgument(['--ui'], {
|
||||
type: 'string',
|
||||
help: 'Marionette specific mocha ui to use',
|
||||
defaultValue: fsPath.normalize(__dirname + '/../lib/ui'),
|
||||
});
|
||||
|
||||
parser.addArgument(['--host'], {
|
||||
type: require,
|
||||
help: 'host module to use defaults to marionette-b2gdesktop-host',
|
||||
defaultValue: fsPath.normalize(__dirname + '/../host'),
|
||||
});
|
||||
|
||||
parser.addArgument(['--reporter'], {
|
||||
type: requireReporter,
|
||||
help: 'Mocha reporter to use',
|
||||
defaultValue: 'dot'
|
||||
});
|
||||
|
||||
parser.addArgument(['--verbose'], {
|
||||
help: 'Pipe console.* logs from child gecko processes to test process',
|
||||
action: 'storeTrue',
|
||||
defaultValue: false
|
||||
});
|
||||
|
||||
parser.addArgument(['--profile-base'], {
|
||||
type: requireResolve,
|
||||
help: 'default profile options to use in all tests.',
|
||||
dest: 'profileBase'
|
||||
});
|
||||
|
||||
parser.addArgument(['--profile-builder'], {
|
||||
type: requireResolve,
|
||||
help: 'profile generation module',
|
||||
dest: 'profileBuilder',
|
||||
defaultValue: 'marionette-profile-builder'
|
||||
});
|
||||
|
||||
parser.addArgument(['--host-log'], {
|
||||
type: 'string',
|
||||
help: 'Location for gecko/host logs (<file>, stderr, stdout)',
|
||||
dest: 'hostLog',
|
||||
defaultValue: 'marionette-mocha-gecko.log'
|
||||
});
|
||||
|
||||
parser.addArgument(['--mocha-flags'], {
|
||||
help: 'Show mocha specific help/arguments',
|
||||
dest: 'mochaFlags',
|
||||
action: 'storeTrue'
|
||||
});
|
||||
|
||||
parser.addArgument(['--marionette-capabilities'], {
|
||||
type: importDesiredCapabilities,
|
||||
dest: 'desiredCapabilities',
|
||||
help: 'Path tp marionette desired capabilities file (*.json)'
|
||||
});
|
||||
|
||||
function main(argv) {
|
||||
// Initial parser pass to pick up the hosts... This generally won't fail except
|
||||
// in the case of serious argument parsing errors.
|
||||
var initialArgs = parser.parseKnownArgs(argv);
|
||||
|
||||
// Abort early if we just want to show the mocha options...
|
||||
if (initialArgs[0].mochaFlags) return showMochaHelp();
|
||||
|
||||
// Capture the additional arguments from the host.
|
||||
var hostActions = extendParserWithModule(parser, initialArgs[0].host);
|
||||
|
||||
// The second (and "real") parser pass which will include the output needed to
|
||||
// generate a better help message with the extensible options from the host.
|
||||
var args = parser.parseKnownArgs(argv);
|
||||
|
||||
if (args[0].help) {
|
||||
parser.printHelp();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Options to pass on to mocha...
|
||||
var runner = new Runner({
|
||||
host: {
|
||||
module: args[0].host,
|
||||
options: extractOptionsFromModule(args[0], hostActions)
|
||||
},
|
||||
|
||||
profileBuilder: {
|
||||
constructor: args[0].profileBuilder,
|
||||
// Reserved for future when we can allow extensible options like the host.
|
||||
options: {}
|
||||
},
|
||||
|
||||
reporter: {
|
||||
constructor: args[0].reporter
|
||||
},
|
||||
|
||||
profileBase: args[0].profileBase,
|
||||
|
||||
desiredCapabilities: args[0].desiredCapabilities,
|
||||
|
||||
hostLog: hostLogStream(args[0].hostLog),
|
||||
|
||||
verbose: args[0].verbose
|
||||
});
|
||||
|
||||
var mochaArgs = args[1].slice(2);
|
||||
|
||||
// ui is required but only used by the child process...
|
||||
mochaArgs.push('--ui');
|
||||
mochaArgs.push(args[0].ui);
|
||||
|
||||
runner.spawn(mochaArgs);
|
||||
|
||||
var reporter = new args[0].reporter(runner.mochaRunnerProxy);
|
||||
|
||||
// make sure we only output complete lines to stdout and stderr to ensure
|
||||
// cohesive logs.
|
||||
function logLine(outputData, isError) {
|
||||
// direct to the appropriate output.
|
||||
var _log = isError ? console.error : console.log;
|
||||
// search from end to beginning of string, should typically be faster.
|
||||
var lineEnd = outputData.lastIndexOf('\n');
|
||||
if (lineEnd !== -1) {
|
||||
// looks like we found an entire line, write it out and flush the data
|
||||
// from the buffer.
|
||||
var logMessage = outputData.substr(0, lineEnd);
|
||||
// write it out the either stdout or stderr.
|
||||
_log(logMessage);
|
||||
// return the remainder.
|
||||
return outputData.substr(lineEnd + 1);
|
||||
}
|
||||
// looks like we don't have an entire line yet
|
||||
// return the buffer unmodified.
|
||||
return outputData;
|
||||
}
|
||||
|
||||
var stdoutBuffer = '';
|
||||
runner.process.stdout.on('data', function(stdoutData) {
|
||||
// append to the global stdout buffer.
|
||||
stdoutBuffer += stdoutData;
|
||||
// and log it. logLine takes care of managing the buffer.
|
||||
stdoutBuffer = logLine(stdoutBuffer);
|
||||
});
|
||||
var stderrBuffer = '';
|
||||
runner.process.stderr.on('data', function(stderrData) {
|
||||
// append to the global stderr buffer.
|
||||
stderrBuffer += stderrData;
|
||||
stderrBuffer = logLine(stderrBuffer, true);
|
||||
});
|
||||
|
||||
// when the mocha child exits, we want to exit as well.
|
||||
runner.process.on('exit', function(exitCode) {
|
||||
// flush stdout buffer before exiting.
|
||||
if (stdoutBuffer.length) {
|
||||
console.log(stdoutBuffer);
|
||||
}
|
||||
// flush stderr buffer before exiting.
|
||||
if (stderrBuffer.length) {
|
||||
console.error(stderrBuffer);
|
||||
}
|
||||
process.exit(exitCode);
|
||||
});
|
||||
|
||||
process.once('SIGINT', function() {
|
||||
// Let the child die whenever but we need to cleanup hosts in this
|
||||
// process...
|
||||
runner.process.removeListener('exit', process.exit);
|
||||
runner.destroy().catch(function(e) {
|
||||
process.nextTick(function() {
|
||||
// Throw any errors we can't deal with in the course of normal destroy.
|
||||
throw e;
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function showMochaHelp() {
|
||||
var mochaBin = resolveModule('mocha', 'bin', 'mocha');
|
||||
var proc = require('child_process').spawn(
|
||||
mochaBin, ['--help'], { stdio: 'inherit' }
|
||||
);
|
||||
|
||||
proc.once('exit', process.exit);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to import the marionette desired capabilities
|
||||
*/
|
||||
function importDesiredCapabilities(path) {
|
||||
try {
|
||||
return require(path);
|
||||
} catch (e) {
|
||||
// Cannot parse marionette desired capabilities file.
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Convert this host log argument into a stream (not done as a type to avoid
|
||||
destructive operations when calling --help)
|
||||
*/
|
||||
function hostLogStream(value) {
|
||||
switch (value) {
|
||||
case 'stderr':
|
||||
case 'stdout':
|
||||
return process[value];
|
||||
default:
|
||||
return fs.createWriteStream(value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@param {argparse.ArgumentParser} parser to extend.
|
||||
@param {Object} module to use to extend parser.
|
||||
@return {Array[argparse.Action]} list of actions.
|
||||
*/
|
||||
function extendParserWithModule(parser, module) {
|
||||
if (!module.help) return [];
|
||||
var help = module.help;
|
||||
assert(help.group, '.help must contain ".group"');
|
||||
assert(help.arguments, '.help must contain ".arguments"');
|
||||
|
||||
var group = parser.addArgumentGroup(help.group);
|
||||
return Object.keys(help.arguments).map(function(arg) {
|
||||
return group.addArgument([arg], help.arguments[arg]);
|
||||
});
|
||||
}
|
||||
|
||||
function extractOptionsFromModule(parsedArgs, actions) {
|
||||
return actions.reduce(function(result, action) {
|
||||
result[action.dest] = parsedArgs[action.dest];
|
||||
return result;
|
||||
}, {});
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
main(argv);
|
||||
}
|
||||
243
gaia/tests/jsmarionette/runner/marionette-js-runner/host/host.js
Executable file
243
gaia/tests/jsmarionette/runner/marionette-js-runner/host/host.js
Executable file
|
|
@ -0,0 +1,243 @@
|
|||
'use strict';
|
||||
|
||||
var EventEmitter = require('events').EventEmitter;
|
||||
var Promise = require('promise');
|
||||
var debug = require('debug')('host/host');
|
||||
var request = require('./lib/request');
|
||||
var spawn = require('child_process').spawn;
|
||||
var uuid = require('uuid');
|
||||
var _ = require('lodash');
|
||||
|
||||
var VENV = 'VIRTUALENV_PATH' in process.env ?
|
||||
process.env.VIRTUALENV_PATH :
|
||||
__dirname + '/../venv';
|
||||
|
||||
var _FORCE_DEBUG = false;
|
||||
|
||||
function Host(socketPath, process, log) {
|
||||
debug('Created new host', socketPath);
|
||||
this.socketPath = socketPath;
|
||||
this.process = process;
|
||||
this.log = log;
|
||||
this.sessions = {};
|
||||
this.pendingSessions = [];
|
||||
this.error = null;
|
||||
this.restarting = Promise.resolve();
|
||||
this.isRequestInProgress = false;
|
||||
this.didCrashDuringRequest = false;
|
||||
this.onerror = this.onerror.bind(this);
|
||||
this.onexit = this.onexit.bind(this);
|
||||
|
||||
process.on('error', this.onerror);
|
||||
process.on('exit', this.onexit);
|
||||
|
||||
var restart = this.restart.bind(this);
|
||||
|
||||
// XXXDebug Pro Tip: set _FORCE_DEBUG to see this output instead of piping it.
|
||||
if (!_FORCE_DEBUG) {
|
||||
process.stdout.on('data', function(chunk) {
|
||||
var str = chunk.toString();
|
||||
if (str.indexOf('Exception') !== -1) {
|
||||
console.info('Will restart -- Found error in "', str, '"');
|
||||
restart();
|
||||
}
|
||||
});
|
||||
|
||||
process.stderr.on('data', function(chunk) {
|
||||
var str = chunk.toString();
|
||||
|
||||
// Bad file descriptor is a non-issue. Don't restart when this happens.
|
||||
// It simply means that the file descriptor we use to pipe the logs was
|
||||
// closed pre-maturely. We'll recover from this.
|
||||
if (str.indexOf('error') !== -1 &&
|
||||
str.indexOf('Bad file descriptor') === -1) {
|
||||
console.info('Will restart -- Found error in "', str, '"');
|
||||
restart();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
EventEmitter.call(this);
|
||||
}
|
||||
module.exports = Host;
|
||||
|
||||
Host.prototype = {
|
||||
__proto__: EventEmitter.prototype,
|
||||
|
||||
destroy: function() {
|
||||
// If there are any pending session creates wait for those to cleanly finish
|
||||
// first.
|
||||
if (this.pendingSessions.length) {
|
||||
return Promise.all(this.pendingSessions).then(this.destroy.bind(this));
|
||||
}
|
||||
|
||||
return Promise.all(
|
||||
_.map(this.sessions, function(session) {
|
||||
return session.destroy();
|
||||
})
|
||||
)
|
||||
.then(function() {
|
||||
if (Object.keys(this.sessions).length !== 0) {
|
||||
return Promise.reject(new Error('Not all sessions were deleted'));
|
||||
}
|
||||
|
||||
var proc = this.process;
|
||||
proc.removeListener('exit', this.onexit);
|
||||
return new Promise(function(accept, reject) {
|
||||
proc.once('exit', function() {
|
||||
proc.removeListener('error', this.onerror);
|
||||
if (this.error) return reject(this.error);
|
||||
accept();
|
||||
}.bind(this));
|
||||
|
||||
proc.kill();
|
||||
}.bind(this));
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
restart: function() {
|
||||
// Python died so let's restart! Yay! Long live python.
|
||||
console.log('Python crashed! Will restart service...');
|
||||
if (this.isRequestInProgress) {
|
||||
console.log('There was a request in progress!');
|
||||
this.didCrashDuringRequest = true;
|
||||
}
|
||||
|
||||
this.restarting = new Promise(function(resolve) {
|
||||
// Kill ourselves.
|
||||
this.process.removeListener('error', this.onerror);
|
||||
this.process.removeListener('exit', this.onexit);
|
||||
|
||||
if (!this.process.connected) {
|
||||
return resolve();
|
||||
}
|
||||
|
||||
this.process.on('exit', resolve);
|
||||
this.process.kill();
|
||||
}.bind(this))
|
||||
.then(function() {
|
||||
// Start a new python child.
|
||||
return spawnPythonChild();
|
||||
})
|
||||
.then(function(details) {
|
||||
this.socketPath = details.socketPath;
|
||||
this.process = details.process;
|
||||
this.log = details.log;
|
||||
this.process.on('error', this.onerror);
|
||||
this.process.on('exit', this.onexit);
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
/**
|
||||
Issue a request to the hosts underlying python http server.
|
||||
*/
|
||||
request: function(path, options) {
|
||||
if (this.error) {
|
||||
return Promise.reject(this.error);
|
||||
}
|
||||
|
||||
return this.restarting.then(function() {
|
||||
this.isRequestInProgress = true;
|
||||
return request(this.socketPath, path, options);
|
||||
}.bind(this))
|
||||
.catch(function() {}) // TODO(gaye): Replace with finally
|
||||
.then(function(result) {
|
||||
this.isRequestInProgress = false;
|
||||
if (this.didCrashDuringRequest) {
|
||||
console.log('Will retry ' + path + ' request...');
|
||||
this.didCrashDuringRequest = false;
|
||||
return this.request(path, options);
|
||||
}
|
||||
|
||||
return result;
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
onerror: function(error) {
|
||||
debug('Python process error', error.toString());
|
||||
this.error = error;
|
||||
this.process.removeListener('exit', this.onexit);
|
||||
this.process.kill();
|
||||
},
|
||||
|
||||
onexit: function(exit) {
|
||||
debug('Python process exited unexpectedly', JSON.stringify(exit));
|
||||
this.error = new Error('Python unexpected exit ' + JSON.stringify(exit));
|
||||
this.process.removeListener('exit', this.onexit);
|
||||
}
|
||||
};
|
||||
|
||||
Host.create = function() {
|
||||
return spawnPythonChild().then(function(details) {
|
||||
return new Host(details.socketPath, details.process, details.log);
|
||||
});
|
||||
};
|
||||
|
||||
function spawnPythonChild() {
|
||||
var socketPath = '/tmp/marionette-socket-host-' + uuid.v1() + '.sock';
|
||||
|
||||
// XXXDebug Pro Tip: set stdio to [0, 1, 2] to get Python output.
|
||||
var pythonChild = spawnVirtualEnv(
|
||||
'gaia-integration',
|
||||
['--path=' + socketPath],
|
||||
{ stdio: _FORCE_DEBUG === false ?
|
||||
['pipe', 'pipe', 'pipe', 'pipe'] :
|
||||
[0, 1, 2] }
|
||||
);
|
||||
|
||||
var failOnChildError = new Promise(function(accept, reject) {
|
||||
function onError(error) {
|
||||
debug('Python process error during initial connect', error.toString());
|
||||
reject(error);
|
||||
}
|
||||
|
||||
pythonChild.once('error', onError);
|
||||
pythonChild.once('exit', function(exit) {
|
||||
// Ensure we don't call error callback somehow...
|
||||
pythonChild.removeListener('error', onError);
|
||||
reject(new Error(
|
||||
'Unexpected exit during connect: ' +
|
||||
'signal = ' + exit.signal + ', ' +
|
||||
'code = ' + exit.code
|
||||
));
|
||||
});
|
||||
});
|
||||
|
||||
var connect = request(socketPath, '/connect').then(function() {
|
||||
pythonChild.removeAllListeners('error');
|
||||
pythonChild.removeAllListeners('exit');
|
||||
return {
|
||||
socketPath: socketPath,
|
||||
process: pythonChild,
|
||||
log: pythonChild.stdio[3]
|
||||
};
|
||||
});
|
||||
|
||||
return Promise.race([connect, failOnChildError]);
|
||||
}
|
||||
|
||||
/**
|
||||
Wrapper for spawning a python process which expects a venv with particular
|
||||
packages. Same interface as spawn but overrides path and ensures certain env
|
||||
variables are not set which conflict.
|
||||
|
||||
@param {String} bin path to binary to execute.
|
||||
@param {Array} argv list of arguments.
|
||||
@param {Object} opts options for node's spawn.
|
||||
@return {ChildProcess}
|
||||
*/
|
||||
function spawnVirtualEnv(bin, argv, opts) {
|
||||
opts = opts || {};
|
||||
// Clone current environment variables...
|
||||
var env = {};
|
||||
_.assign(env, process.env);
|
||||
opts.env = env;
|
||||
|
||||
// Prepend binary wrappers to path.
|
||||
env.PATH = VENV + '/bin/:' + process.env.PATH;
|
||||
|
||||
// Ensure we don't conflict with other wrappers or package managers.
|
||||
delete env.PYTHONHOME;
|
||||
|
||||
return spawn(bin, argv, opts);
|
||||
}
|
||||
64
gaia/tests/jsmarionette/runner/marionette-js-runner/host/index.js
Executable file
64
gaia/tests/jsmarionette/runner/marionette-js-runner/host/index.js
Executable file
|
|
@ -0,0 +1,64 @@
|
|||
'use strict';
|
||||
var Host = require('./host');
|
||||
var Session = require('./session');
|
||||
|
||||
exports.help = {
|
||||
group: {
|
||||
title: 'Socket Host',
|
||||
description: 'Socket host uses mozbase to manage gecko applications.'
|
||||
},
|
||||
arguments: {
|
||||
'--serial': {
|
||||
dest: 'device_serial',
|
||||
defaultValue: null,
|
||||
help: 'Serial port (only with buildapp=device)',
|
||||
type: 'string'
|
||||
},
|
||||
|
||||
'--symbols-path': {
|
||||
dest: 'symbols_path',
|
||||
help: 'Path to build symbols used by crash reporting.',
|
||||
defaultValue: null,
|
||||
type: 'string'
|
||||
},
|
||||
|
||||
'--b2gpath': {
|
||||
dest: 'b2g_home',
|
||||
help: 'Path to b2g directory.',
|
||||
defaultValue: null,
|
||||
type: 'string'
|
||||
},
|
||||
|
||||
'--buildapp': {
|
||||
type: 'string',
|
||||
choices: ['device', 'desktop', 'emulator'],
|
||||
defaultValue: 'desktop',
|
||||
help: 'Type of gecko application to run.'
|
||||
},
|
||||
|
||||
'--runtime': {
|
||||
defaultValue: null,
|
||||
help: 'Path to b2g-bin when using buildapp=desktop'
|
||||
},
|
||||
|
||||
'--device-type': {
|
||||
dest: 'device_type',
|
||||
defaultValue: 'phone'
|
||||
},
|
||||
|
||||
'--dump-path': {
|
||||
dest: 'dump_path',
|
||||
defaultValue: process.cwd() + '/crash',
|
||||
help: 'path in which to store crash dumps. Will default to ' +
|
||||
'the \'minidumps\' directory in the current working directory'
|
||||
},
|
||||
|
||||
'--chrome': {
|
||||
defaultValue: 'chrome://b2g/content/shell.html',
|
||||
help: '--chrome option to set when starting b2g-desktop'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
exports.createHost = Host.create;
|
||||
exports.createSession = Session.create;
|
||||
99
gaia/tests/jsmarionette/runner/marionette-js-runner/host/lib/request.js
Executable file
99
gaia/tests/jsmarionette/runner/marionette-js-runner/host/lib/request.js
Executable file
|
|
@ -0,0 +1,99 @@
|
|||
'use strict';
|
||||
/**
|
||||
Node http/socket/promise wrapper.
|
||||
*/
|
||||
|
||||
var http = require('http');
|
||||
var debug = require('debug')('marionette-socket-host:request');
|
||||
var Promise = require('promise');
|
||||
|
||||
var RETRIES = 60;
|
||||
var RETRY_TIMER = 5000;
|
||||
|
||||
function PythonError(msg) {
|
||||
Error.call(msg);
|
||||
this.name = 'python_error';
|
||||
}
|
||||
|
||||
PythonError.prototype = {
|
||||
__proto__: Error.prototype
|
||||
};
|
||||
|
||||
module.exports = function request(socketPath, path, json, retry) {
|
||||
retry = retry || 0;
|
||||
var body = JSON.stringify(json || {});
|
||||
|
||||
return new Promise(function(accept, reject) {
|
||||
debug('issue request', socketPath, path, json);
|
||||
var req = http.request({
|
||||
socketPath: socketPath,
|
||||
method: 'POST',
|
||||
path: path,
|
||||
headers: {
|
||||
'Content-Length': Buffer.byteLength(body),
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
req.setTimeout(300 * 1000, function() {
|
||||
reject(new Error('Host request timeout exceeded.'));
|
||||
});
|
||||
|
||||
// The unix socket server may or may not be ready at this point so we retry
|
||||
// up to 5 times to ensure we are connected...
|
||||
req.once('error', function(err) {
|
||||
|
||||
if (retry > RETRIES) {
|
||||
debug('At maximum retries...');
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
debug('Error in socket request retrying...', err);
|
||||
|
||||
setTimeout(function() {
|
||||
request(socketPath, path, json, retry + 1).then(accept, reject);
|
||||
}, RETRY_TIMER);
|
||||
});
|
||||
|
||||
// request sender....
|
||||
req.write(body);
|
||||
|
||||
req.on('abort', function() {
|
||||
debug('Request', retry, 'aborted.');
|
||||
});
|
||||
|
||||
// response handler...
|
||||
req.on('response', function(res) {
|
||||
var data = '';
|
||||
res.on('data', function(buffer) {
|
||||
data += buffer;
|
||||
});
|
||||
|
||||
res.on('end', function() {
|
||||
debug(res.statusCode, data);
|
||||
var json = JSON.parse(data);
|
||||
if (res.statusCode < 200 || res.statusCode > 299) {
|
||||
var err = new PythonError(json.message);
|
||||
err.stack = json.stack;
|
||||
reject(err);
|
||||
}
|
||||
else if (path === '/start_runner') {
|
||||
if (data.length) {
|
||||
accept(json);
|
||||
}
|
||||
else {
|
||||
reject(
|
||||
new Error('Request to start_runner expects data with response!')
|
||||
);
|
||||
}
|
||||
}
|
||||
else {
|
||||
accept(json);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// send the request...
|
||||
req.end();
|
||||
});
|
||||
};
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
'use strict';
|
||||
var Promise = require('promise');
|
||||
|
||||
module.exports = function waitForEvent(listener, event) {
|
||||
return new Promise(function(accept, reject) {
|
||||
listener.on(event, function handler(message) {
|
||||
// accept the message before clearing the listener
|
||||
accept(message);
|
||||
listener.removeListener(event, handler);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
Metadata-Version: 1.1
|
||||
Name: gaia-runner-service
|
||||
Version: 0.3
|
||||
Summary: A utility for running tests over a socket from JS to python
|
||||
Home-page: UNKNOWN
|
||||
Author: Andrew Halberstadt
|
||||
Author-email: ahalberstadt@mozilla.com
|
||||
License: MPL 2.0
|
||||
Description: See https://github.com/mozilla-b2g/gaia/tree/master/tests/python/runner-service
|
||||
Keywords: mozilla
|
||||
Platform: UNKNOWN
|
||||
Classifier: Environment :: Console
|
||||
Classifier: Intended Audience :: Developers
|
||||
Classifier: License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)
|
||||
Classifier: Natural Language :: English
|
||||
Classifier: Operating System :: OS Independent
|
||||
Classifier: Programming Language :: Python
|
||||
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
setup.py
|
||||
gaia_runner_service.egg-info/PKG-INFO
|
||||
gaia_runner_service.egg-info/SOURCES.txt
|
||||
gaia_runner_service.egg-info/dependency_links.txt
|
||||
gaia_runner_service.egg-info/entry_points.txt
|
||||
gaia_runner_service.egg-info/not-zip-safe
|
||||
gaia_runner_service.egg-info/requires.txt
|
||||
gaia_runner_service.egg-info/top_level.txt
|
||||
runner_service/__init__.py
|
||||
runner_service/listener.py
|
||||
runner_service/runintegration.py
|
||||
runner_service/handlers/__init__.py
|
||||
runner_service/handlers/runner.py
|
||||
|
|
@ -0,0 +1 @@
|
|||
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
|
||||
[console_scripts]
|
||||
gaia-integration=runner_service:runintegration.cli
|
||||
|
||||
|
|
@ -0,0 +1 @@
|
|||
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
mozrunner >= 6.11
|
||||
mozprofile >= 0.21
|
||||
mozcrash >= 0.16
|
||||
|
|
@ -0,0 +1 @@
|
|||
runner_service
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
import runintegration
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
from . import runner
|
||||
|
||||
runner_handlers = {
|
||||
'desktop': runner.DesktopHandler,
|
||||
'device': runner.DeviceHandler,
|
||||
'emulator': runner.EmulatorHandler,
|
||||
}
|
||||
|
|
@ -0,0 +1,207 @@
|
|||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
from abc import ABCMeta, abstractmethod, abstractproperty
|
||||
import json
|
||||
import os
|
||||
import posixpath
|
||||
import sys
|
||||
|
||||
from mozprofile import Profile
|
||||
from mozrunner import (
|
||||
B2GDesktopRunner,
|
||||
B2GDeviceRunner,
|
||||
B2GEmulatorRunner
|
||||
)
|
||||
|
||||
# We log over a special pipe (fd3) which node knows to hook up.
|
||||
GECKO_LOG_FD = 3
|
||||
|
||||
class DeviceUtils:
|
||||
def __init__(self, deviceManager):
|
||||
self.dm = deviceManager
|
||||
|
||||
def _purge_data(self):
|
||||
# Clean up data on device.
|
||||
print("cleaning up data on device.")
|
||||
data_paths = ['/cache/*',
|
||||
'/data/local/debug_info_trigger',
|
||||
'/data/local/indexedDB',
|
||||
'/data/local/OfflineCache',
|
||||
'/data/local/permissions.sqlite',
|
||||
'/data/local/storage/persistent',
|
||||
'/data/local/storage/default']
|
||||
for path in data_paths:
|
||||
if self.dm.dirExists(path):
|
||||
self.dm.removeDir(path)
|
||||
|
||||
def _purge_wifi_networks(self):
|
||||
# Remove remembered networks
|
||||
print("removing remembered wifi networks.")
|
||||
self.dm.removeDir('/data/misc/wifi/wpa_supplicant.conf')
|
||||
|
||||
def _purge_storage_data(self):
|
||||
# Clean up storage.
|
||||
print("cleaning up storage on device.")
|
||||
# TODO: Remove hard-coded paths once bug 1018079 is resolved
|
||||
storage_paths = ['/mnt/sdcard/',
|
||||
'/mnt/extsdcard/',
|
||||
'/storage/sdcard/',
|
||||
'/storage/sdcard0/',
|
||||
'/storage/sdcard1/']
|
||||
for path in storage_paths:
|
||||
if self.dm.dirExists(path):
|
||||
for item in self.dm.listFiles(path):
|
||||
self.dm.removeDir('/'.join([path, item]))
|
||||
|
||||
def _purge_installed_apps(self):
|
||||
# Clean up apps on device.
|
||||
apps = json.loads(self.dm.pullFile('/data/local/webapps/webapps.json'))
|
||||
system_install_time = apps['system.gaiamobile.org']['installTime']
|
||||
for app in apps.values():
|
||||
if app.get('installTime') > system_install_time:
|
||||
# removing any webapps installed since build time
|
||||
path = posixpath.join(app.get('basePath'), app.get('id'))
|
||||
print('Removing %s' % path)
|
||||
self.dm.removeDir(path)
|
||||
|
||||
def _stop_b2g(self):
|
||||
print("stopping b2g on device before starting runner.")
|
||||
self.dm.shellCheckOutput(['stop', 'b2g'])
|
||||
|
||||
|
||||
class MozrunnerHandler(object):
|
||||
__metaclass__ = ABCMeta
|
||||
runner = None
|
||||
|
||||
def __init__(self, symbols_path=None, *args, **kwargs):
|
||||
fd = os.fdopen(GECKO_LOG_FD, 'a')
|
||||
process_args = {
|
||||
'stream': fd,
|
||||
'onFinish': self.on_finish
|
||||
}
|
||||
|
||||
dump_path = kwargs.pop('dump_path', os.path.join(os.getcwd(), "minidumps"))
|
||||
self.common_runner_args = {
|
||||
'process_args': process_args,
|
||||
'symbols_path': symbols_path,
|
||||
'dump_save_path': dump_path,
|
||||
|
||||
'env': {
|
||||
# Required for crash reporting...
|
||||
'MOZ_CRASHREPORTER': '1',
|
||||
'MOZ_CRASHREPORTER_NO_REPORT': '1'
|
||||
}
|
||||
}
|
||||
|
||||
# Ensure we set the DISPLAY if it is set in the current environment so
|
||||
# things like Xvfb work correctly...
|
||||
if 'DISPLAY' in os.environ:
|
||||
self.common_runner_args['env']['DISPLAY'] = os.environ['DISPLAY']
|
||||
|
||||
@abstractmethod
|
||||
def start_runner(self, target, options):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def stop_runner(self):
|
||||
pass
|
||||
|
||||
def on_finish(self):
|
||||
pass
|
||||
|
||||
def cleanup(self):
|
||||
if self.runner:
|
||||
self.runner.cleanup()
|
||||
|
||||
|
||||
class DesktopHandler(MozrunnerHandler):
|
||||
|
||||
def start_runner(self, binary, options):
|
||||
profile = Profile(profile=options.get('profile'))
|
||||
cmdargs = options.get('argv', [])
|
||||
|
||||
if 'screen' in options and \
|
||||
'width' in options['screen'] and \
|
||||
'height' in options['screen']:
|
||||
screen = '--screen=%sx%s' % (options['screen']['width'], \
|
||||
options['screen']['height'])
|
||||
|
||||
if 'dpi' in options['screen']:
|
||||
screen = '%s@%s' % (screen, options['screen']['dpi'])
|
||||
cmdargs.append(screen)
|
||||
|
||||
if options.get('noRemote', True):
|
||||
cmdargs.append('-no-remote')
|
||||
|
||||
if options.get('url'):
|
||||
cmdargs.append(options['url'])
|
||||
|
||||
if options.get('chrome'):
|
||||
cmdargs.extend(['-chrome', options['chrome']])
|
||||
|
||||
if options.get('startDebugger'):
|
||||
cmdargs.extend(['-start-debugger-server', options['startDebugger']])
|
||||
|
||||
self.runner = B2GDesktopRunner(binary, cmdargs=cmdargs, profile=profile,
|
||||
**self.common_runner_args)
|
||||
self.runner.start()
|
||||
|
||||
def stop_runner(self):
|
||||
self.runner.stop()
|
||||
|
||||
|
||||
class EmulatorHandler(MozrunnerHandler):
|
||||
|
||||
def __init__(self, b2g_home, *args, **kwargs):
|
||||
MozrunnerHandler.__init__(self, *args, **kwargs)
|
||||
|
||||
self.runner = B2GEmulatorRunner(b2g_home=b2g_home, **self.common_runner_args)
|
||||
self.runner.device.start()
|
||||
|
||||
def start_runner(self, binary, options):
|
||||
port = options.get('port')
|
||||
profile = Profile(profile=options.get('profile'))
|
||||
|
||||
self.runner.device.setup_port_forwarding(local_port=port, remote_port=port)
|
||||
|
||||
self.runner.profile = profile
|
||||
self.runner.start()
|
||||
if not self.runner.device.wait_for_port(port):
|
||||
raise Exception("Wait for port timed out")
|
||||
|
||||
def stop_runner(self):
|
||||
self.runner.stop()
|
||||
|
||||
|
||||
class DeviceHandler(MozrunnerHandler):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
serial = kwargs.pop('serial', None)
|
||||
MozrunnerHandler.__init__(self, *args, **kwargs)
|
||||
|
||||
self.runner = B2GDeviceRunner(serial=serial, **self.common_runner_args)
|
||||
self.runner.device.connect()
|
||||
|
||||
def start_runner(self, target, options):
|
||||
du = DeviceUtils(self.runner.device.dm)
|
||||
device = self.runner.device
|
||||
port = options.get('port')
|
||||
profile = Profile(profile=options.get('profile'))
|
||||
|
||||
device.setup_port_forwarding(local_port=port, remote_port=port)
|
||||
|
||||
du._stop_b2g()
|
||||
du._purge_data()
|
||||
du._purge_wifi_networks()
|
||||
du._purge_storage_data()
|
||||
|
||||
self.runner.profile = profile
|
||||
self.runner.start()
|
||||
|
||||
if not device.wait_for_port(port=port, timeout=600):
|
||||
raise Exception("Wait for port timed out")
|
||||
|
||||
def stop_runner(self):
|
||||
self.runner.stop()
|
||||
|
|
@ -0,0 +1,157 @@
|
|||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
import socket
|
||||
import sys
|
||||
import os
|
||||
import json
|
||||
import uuid
|
||||
import BaseHTTPServer
|
||||
import traceback
|
||||
|
||||
import mozcrash
|
||||
|
||||
from .handlers import runner_handlers
|
||||
|
||||
class RequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
|
||||
REQUESTS = {}
|
||||
INACTIVE = []
|
||||
|
||||
def log_message(self, format, *args):
|
||||
'''
|
||||
Override log message to bypass certain behaviors which assume tcp
|
||||
sockets
|
||||
'''
|
||||
pass
|
||||
|
||||
def log_error(self, format, *args):
|
||||
'''
|
||||
Override log error to bypass certain behaviors which assume tcp
|
||||
sockets
|
||||
'''
|
||||
print(args)
|
||||
|
||||
def send_json(self, status, payload):
|
||||
'''
|
||||
Respond to incoming http request...
|
||||
|
||||
:param int status:
|
||||
:param dict payload: payload to send as json to the socket.
|
||||
'''
|
||||
payload = json.dumps(payload)
|
||||
self.send_response(status)
|
||||
self.send_header('Content-Length', len(payload))
|
||||
self.send_header('Content-Type', 'application/json')
|
||||
self.end_headers()
|
||||
self.wfile.write(payload)
|
||||
|
||||
def do_GET(self):
|
||||
pass
|
||||
|
||||
def do_POST(self):
|
||||
content_length = self.headers.getheader('content-length', 0)
|
||||
payload = json.loads(self.rfile.read(int(content_length)))
|
||||
handler = self.path[1:]
|
||||
method = 'do_' + handler
|
||||
|
||||
# No matter what we send application/json
|
||||
if hasattr(self, method):
|
||||
try:
|
||||
func = getattr(self, method)
|
||||
result = func(payload)
|
||||
except Exception as e:
|
||||
self.send_json(500, {
|
||||
'message': str(e),
|
||||
'stack': traceback.format_exc()
|
||||
})
|
||||
else:
|
||||
self.send_json(200, result)
|
||||
else:
|
||||
self.send_json(404, { message: 'Unknown path :' + self.path })
|
||||
|
||||
|
||||
def do_test_end(self, data):
|
||||
print 'TEST-END | %s' % data['test']
|
||||
|
||||
def do_test_start(self, data):
|
||||
print 'TEST-START | %s' % data['test']
|
||||
|
||||
def do_test_status(self, data):
|
||||
print 'TEST-STATUS | %s | %s' % (data['subtest'], data['status'])
|
||||
|
||||
def do_connect(self, payload):
|
||||
'''
|
||||
Simple connection ready hook to signal socket readiness...
|
||||
'''
|
||||
return {}
|
||||
|
||||
def do_get_crash_info(self, payload):
|
||||
# Somewhat terrible (maybe) hack to get CrashInfo private interface.
|
||||
crash_info = mozcrash.mozcrash.CrashInfo(**payload)
|
||||
|
||||
# Return first error or nothing...
|
||||
for info in crash_info:
|
||||
return {
|
||||
'signature': info.signature,
|
||||
'stackwalk_stdout': info.stackwalk_stdout,
|
||||
'stackwalk_stderr': info.stackwalk_stderr,
|
||||
'stackwalk_retcode': info.stackwalk_retcode,
|
||||
'stackwalk_errors': info.stackwalk_errors
|
||||
}
|
||||
|
||||
return {}
|
||||
|
||||
def do_start_runner(self, payload):
|
||||
'''
|
||||
Begin a runner
|
||||
|
||||
:param str|None binary: Target binary (usually b2g-bin) for desktop.
|
||||
:param dict options: Options specific to the buildapp type.
|
||||
'''
|
||||
binary = payload.get('binary')
|
||||
options = payload.get('options', {})
|
||||
|
||||
handler_args = {
|
||||
'symbols_path': options.get('symbols_path')
|
||||
}
|
||||
|
||||
if 'b2g_home' in options:
|
||||
handler_args['b2g_home'] = options['b2g_home']
|
||||
|
||||
if options['buildapp'] == 'device' and 'serial' in options:
|
||||
handler_args['serial'] = options['serial']
|
||||
|
||||
if 'dump_path' in options:
|
||||
handler_args['dump_path'] = options['dump_path']
|
||||
|
||||
start_id = str(uuid.uuid4())
|
||||
handler = runner_handlers[options['buildapp']](**handler_args)
|
||||
handler.start_runner(binary, options)
|
||||
|
||||
print "Started runner with id = {0}".format(start_id)
|
||||
self.REQUESTS[start_id] = handler
|
||||
|
||||
return { 'id': start_id }
|
||||
|
||||
def do_stop_runner(self, payload):
|
||||
print "Stopping runner with id = {0}".format(payload['id'])
|
||||
payload_id = payload['id']
|
||||
if not payload_id in self.REQUESTS:
|
||||
if payload_id in self.INACTIVE:
|
||||
sys.stderr.write(
|
||||
'Received request to stop ' +
|
||||
payload_id +
|
||||
' but already stopped so nothing to do.\n')
|
||||
return {}
|
||||
runner_ids = str.join(
|
||||
', ',
|
||||
map(lambda x: str(x), self.REQUESTS.keys() + self.INACTIVE))
|
||||
msg = 'Cannot stop %s - service has %s' % (payload_id, runner_ids)
|
||||
raise Exception(msg)
|
||||
|
||||
self.INACTIVE.append(payload_id)
|
||||
handler = self.REQUESTS.pop(payload_id)
|
||||
handler.stop_runner()
|
||||
handler.cleanup()
|
||||
return {}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
from argparse import ArgumentParser
|
||||
import threading
|
||||
import sys
|
||||
import socket
|
||||
import os
|
||||
import time
|
||||
|
||||
import SocketServer
|
||||
|
||||
from .listener import RequestHandler
|
||||
|
||||
class Server(SocketServer.ThreadingMixIn, SocketServer.UnixStreamServer):
|
||||
pass
|
||||
|
||||
def cli(args=sys.argv[1:]):
|
||||
'''
|
||||
Begin the server note that the majority of arguments will be passed via the
|
||||
cli but over the unix socket instead...
|
||||
'''
|
||||
parser = ArgumentParser()
|
||||
parser.add_argument('--path',
|
||||
dest='path',
|
||||
default='/tmp/marionette_socket_host_worker',
|
||||
help='Path to bind unix socket to'
|
||||
)
|
||||
args = parser.parse_args(args)
|
||||
|
||||
# Make sure the socket does not already exist.
|
||||
try:
|
||||
# XXX: This is potentially a bad thing maybe we should raise or warn
|
||||
# here...
|
||||
os.unlink(args.path)
|
||||
except OSError:
|
||||
if os.path.exists(args.path):
|
||||
raise
|
||||
|
||||
server = Server(args.path, RequestHandler)
|
||||
server.timeout = 300
|
||||
server_thread = threading.Thread(target=server.serve_forever)
|
||||
|
||||
server_thread.deamon = True
|
||||
server.running = True
|
||||
server_thread.start()
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(cli())
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
# You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
from setuptools import setup, find_packages
|
||||
# NOTE: import the following due to linux bug http://bugs.python.org/issue15881
|
||||
try:
|
||||
import multiprocessing
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
PACKAGE_VERSION = '0.4'
|
||||
|
||||
deps = ['mozrunner >= 6.11',
|
||||
'mozprofile >= 0.21']
|
||||
|
||||
setup(name='gaia-runner-service',
|
||||
version=PACKAGE_VERSION,
|
||||
description='A utility for running tests over a socket from JS to python',
|
||||
long_description='See https://github.com/mozilla-b2g/gaia/tree/master/tests/python/runner-service',
|
||||
classifiers=['Environment :: Console',
|
||||
'Intended Audience :: Developers',
|
||||
'License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)',
|
||||
'Natural Language :: English',
|
||||
'Operating System :: OS Independent',
|
||||
'Programming Language :: Python',
|
||||
'Topic :: Software Development :: Libraries :: Python Modules',
|
||||
],
|
||||
keywords='mozilla',
|
||||
author='Andrew Halberstadt',
|
||||
author_email='ahalberstadt@mozilla.com',
|
||||
license='MPL 2.0',
|
||||
packages=find_packages(),
|
||||
include_package_data=True,
|
||||
zip_safe=False,
|
||||
install_requires=deps,
|
||||
entry_points="""
|
||||
[console_scripts]
|
||||
gaia-integration=runner_service:runintegration.cli
|
||||
""")
|
||||
127
gaia/tests/jsmarionette/runner/marionette-js-runner/host/session.js
Executable file
127
gaia/tests/jsmarionette/runner/marionette-js-runner/host/session.js
Executable file
|
|
@ -0,0 +1,127 @@
|
|||
'use strict';
|
||||
|
||||
var Promise = require('promise');
|
||||
var assign = require('object-assign');
|
||||
var fsPath = require('path');
|
||||
var indent = require('indent-string');
|
||||
var mozrunner = require('mozilla-runner');
|
||||
var util = require('util');
|
||||
|
||||
var detectBinary = Promise.denodeify(mozrunner.detectBinary);
|
||||
|
||||
var DEFAULT_LOCATION = fsPath.join(process.cwd(), 'firefox');
|
||||
|
||||
function Session(host, id, options) {
|
||||
this.host = host;
|
||||
this.id = id;
|
||||
this.options = options;
|
||||
}
|
||||
module.exports = Session;
|
||||
|
||||
Session.prototype = {
|
||||
$rpc: { methods: ['destroy', 'checkError'] },
|
||||
|
||||
checkError: function(profileConfig, err) {
|
||||
var request = {
|
||||
// TODO: This does not work for devices as is...
|
||||
dump_directory: profileConfig.profile + '/minidumps',
|
||||
symbols_path: this.options.symbols_path,
|
||||
dump_save_path: this.options.dump_path
|
||||
};
|
||||
|
||||
return this.host.request('/get_crash_info', request)
|
||||
.then(function(info) {
|
||||
// If info is empty, we didn't crash the process. We threw an error
|
||||
// in JavaScript code instead.
|
||||
if (Object.keys(info).length === 0) {
|
||||
// The error we were passed originally will suffice in this case.
|
||||
return err;
|
||||
}
|
||||
|
||||
// Otherwise, check to see if the stackwalk binary return cleanly or with
|
||||
// error.
|
||||
if (info.stackwalk_retcode !== 0) {
|
||||
// Error during stackwalk.
|
||||
return parseCrashInfoWithStackwalkError(info);
|
||||
}
|
||||
|
||||
// Everything is OK with the stackwalk, let's format a nice error with it.
|
||||
return parseCrashInfo(info);
|
||||
});
|
||||
},
|
||||
|
||||
destroy: function() {
|
||||
return this.host.request('/stop_runner', { id : this.id })
|
||||
.then(function() {
|
||||
delete this.host.sessions[this.id];
|
||||
this.id = null;
|
||||
}.bind(this));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Session.create = function(host, profile, options) {
|
||||
options = assign({ profile: profile, buildapp: 'desktop' }, options);
|
||||
if (options.b2g_home && options.buildapp === 'desktop') {
|
||||
return Promise.reject('Only specify --b2gpath for device or emulator');
|
||||
}
|
||||
if (!options.b2g_home && options.buildapp === 'emulator') {
|
||||
return Promise.reject('Must specify --b2gpath for emulator');
|
||||
}
|
||||
|
||||
var startSession = resolveBinary(options)
|
||||
.then(function(binary) {
|
||||
return host.request('/start_runner', { binary: binary, options: options });
|
||||
})
|
||||
.then(function(result) {
|
||||
var session = new Session(host, result.id, options);
|
||||
host.sessions[result.id] = session;
|
||||
// Session no longer pending.
|
||||
host.pendingSessions.splice(host.pendingSessions.indexOf(startSession), 1);
|
||||
return session;
|
||||
})
|
||||
.catch(function(error) {
|
||||
console.error(error);
|
||||
});
|
||||
|
||||
host.pendingSessions.push(startSession);
|
||||
return startSession;
|
||||
};
|
||||
|
||||
/**
|
||||
Figure out where the b2g-bin lives based on options.
|
||||
|
||||
@param {Object} options (as described by .help).
|
||||
@return {Promise<Null|String>} null if none is needed or a path.
|
||||
*/
|
||||
function resolveBinary(options) {
|
||||
if (options.buildapp !== 'desktop') return Promise.resolve();
|
||||
if (options.runtime) return Promise.resolve(options.runtime);
|
||||
|
||||
var binary = options.target || DEFAULT_LOCATION;
|
||||
return detectBinary(binary, { product: 'firefox' });
|
||||
}
|
||||
|
||||
function parseCrashInfo(info) {
|
||||
var msg = util.format('Crash detected at: %s', info.signature);
|
||||
var error = new Error(msg);
|
||||
error.stack = msg + '\n' + info.stackwalk_stdout;
|
||||
error.name = 'ProcessCrash Stackwalk';
|
||||
return error;
|
||||
}
|
||||
|
||||
function parseCrashInfoWithStackwalkError(info) {
|
||||
var msg = 'Crash detected but error running stackwalk\n';
|
||||
if (Array.isArray(info.stackwalk_errors)) {
|
||||
info.stackwalk_errors.forEach(function(err) {
|
||||
msg += indent(err, ' ', 2) + '\n';
|
||||
});
|
||||
}
|
||||
|
||||
var error = new Error(msg);
|
||||
error.name = 'ProcessCrash';
|
||||
error.stack = msg +
|
||||
'\n' +
|
||||
indent((info.stackwalk_stderr || ''), ' ', 4);
|
||||
return error;
|
||||
}
|
||||
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;
|
||||
}
|
||||
40
gaia/tests/jsmarionette/runner/marionette-js-runner/package.json
Executable file
40
gaia/tests/jsmarionette/runner/marionette-js-runner/package.json
Executable file
|
|
@ -0,0 +1,40 @@
|
|||
{
|
||||
"name": "marionette-js-runner",
|
||||
"version": "1.1.4",
|
||||
"description": "Javascript ui testing framework built on top of mocha for testing webapps on gecko",
|
||||
|
||||
"contributors": [
|
||||
"James Lal [:lightsofapollo] <jlal@mozilla.com>",
|
||||
"Gareth Aye [:gaye] <gaye@mozilla.com>",
|
||||
"Aus Lacroix [:auswerk] <aus@mozilla.com>"
|
||||
],
|
||||
|
||||
"bin": {
|
||||
"marionette-mocha": "./bin/marionette-mocha",
|
||||
"apply-manifest": "./bin/apply-manifest"
|
||||
},
|
||||
|
||||
"dependencies": {
|
||||
"argparse": "^0.1.15",
|
||||
"array-difference": "0.0.1",
|
||||
"debug": "*",
|
||||
"empty-port": "~0.0.1",
|
||||
"escape-string-regexp": "^1.0.2",
|
||||
"intersect": "~0.0.3",
|
||||
"indent-string": "1.2.1",
|
||||
"lodash": "3.6.0",
|
||||
"marionette-js-logger": "file:../../plugins/marionette-js-logger",
|
||||
"marionette-profile-builder": "file:../marionette-profile-builder",
|
||||
"mocha": "^2",
|
||||
"mocha-json-proxy": "file:../../mocha/mocha-json-proxy",
|
||||
"mozilla-runner": "file:../mozilla-runner",
|
||||
"object-assign": "^1.0.0",
|
||||
"prettyjson": "1.1.2",
|
||||
"promise": "^6.0.1",
|
||||
"uuid": "^2.0.1"
|
||||
},
|
||||
|
||||
"scripts": {
|
||||
"postinstall": "./scripts/setup_python.sh"
|
||||
}
|
||||
}
|
||||
85
gaia/tests/jsmarionette/runner/marionette-js-runner/scripts/setup_python.sh
Executable file
85
gaia/tests/jsmarionette/runner/marionette-js-runner/scripts/setup_python.sh
Executable file
|
|
@ -0,0 +1,85 @@
|
|||
#!/bin/bash -e
|
||||
|
||||
PYTHON_VERSION=2.7.3
|
||||
USE_PYTHON=`which python`
|
||||
|
||||
if [[ ! -z "$VIRTUALENV_EXISTS" ]]; then
|
||||
# If virtualenv already exists then we don't have to setup.
|
||||
# This is for buildbot where all of the packages we need should
|
||||
# already be in the right place since our mozharness script
|
||||
# installs them before we get here.
|
||||
exit
|
||||
fi
|
||||
|
||||
if [[ -z "$(which virtualenv)" ]]; then
|
||||
# If we're not on ci and the user doesn't have virtualenv
|
||||
# then we can try to install it but probably ruh roh...
|
||||
echo "checking python version"
|
||||
|
||||
function notify_sudo {
|
||||
if [ "$SUDO_NOTIFY" = "1" ]; then
|
||||
return
|
||||
fi
|
||||
echo "Sudo access is required to install pip and/or virtualenv "
|
||||
echo "on your system. Please enter your sudo password if prompted. "
|
||||
echo "If you don't have sudo access, you will need a system administrator "
|
||||
echo "to install python, pip and/or virtualenv for you. This is a requirement for marionette-socket-host."
|
||||
SUDO_NOTIFY=1
|
||||
}
|
||||
|
||||
function install_python_warning {
|
||||
echo "Python $PYTHON_VERSION or greater required!"
|
||||
echo "Please install from https://www.python.org/download/releases/2.7.5/"
|
||||
exit 1
|
||||
}
|
||||
|
||||
|
||||
USE_PYTHON=`which python`
|
||||
if [ -n "$PYTHON_27" ]; then
|
||||
USE_PYTHON=$PYTHON_27
|
||||
fi
|
||||
PYTHON_MAJOR=`$USE_PYTHON -c 'import sys; print(sys.version_info[0])'`
|
||||
if [ $? != 0 ]; then
|
||||
install_python_warning
|
||||
fi
|
||||
PYTHON_MINOR=`$USE_PYTHON -c 'import sys; print(sys.version_info[1])'`
|
||||
PYTHON_MICRO=`$USE_PYTHON -c 'import sys; print(sys.version_info[2])'`
|
||||
VER_SPLIT=(${PYTHON_VERSION//./ })
|
||||
printf "Python version found: %s %s %s \n" "$PYTHON_MAJOR" "$PYTHON_MINOR" "$PYTHON_MICRO"
|
||||
if [ $PYTHON_MAJOR -ne ${VER_SPLIT[0]} ] || [ $PYTHON_MINOR -ne ${VER_SPLIT[1]} ] || [ $PYTHON_MICRO -lt ${VER_SPLIT[2]} ] ; then
|
||||
install_python_warning
|
||||
fi
|
||||
|
||||
echo "Setting up virtualenv"
|
||||
|
||||
if ! which pip; then
|
||||
if ! which easy_install; then
|
||||
echo "Neither pip nor easy_install is found in your path"
|
||||
echo "Please install pip directly using: http://pip.readthedocs.org/en/latest/installing"
|
||||
exit 1
|
||||
fi
|
||||
notify_sudo
|
||||
sudo easy_install pip || { echo 'error installing pip' ; exit 1; }
|
||||
fi
|
||||
|
||||
if ! which virtualenv; then
|
||||
notify_sudo
|
||||
sudo pip install virtualenv || { echo 'error installing virtualenv' ; exit 1; }
|
||||
fi
|
||||
fi
|
||||
|
||||
# Detect pip version. We consider by default that we will have to use
|
||||
# --trusted-host and we just disable it for release before 7.0.0
|
||||
PIP_VERSION=`pip --version | cut -d ' ' -f 2 | cut -d '.' -f 1`
|
||||
TRUSTED_HOST="--trusted-host=pypi.pub.build.mozilla.org"
|
||||
if [ $PIP_VERSION -lt 7 ]; then
|
||||
TRUSTED_HOST=""
|
||||
fi;
|
||||
|
||||
virtualenv --python=$USE_PYTHON --no-site-packages $PWD/venv
|
||||
source ./venv/bin/activate
|
||||
cd host/python/runner-service
|
||||
pip install \
|
||||
--find-links=http://pypi.pub.build.mozilla.org/pub $TRUSTED_HOST \
|
||||
-r gaia_runner_service.egg-info/requires.txt
|
||||
python setup.py develop
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
/* global assert */
|
||||
'use strict';
|
||||
suite('apply-manifest', function() {
|
||||
var exec = require('child_process').exec;
|
||||
var ROOT = __dirname + '/../../';
|
||||
var BIN = ROOT + '/bin/apply-manifest';
|
||||
|
||||
test('manifest-blacklist.json', function(done) {
|
||||
var argv = [
|
||||
BIN,
|
||||
__dirname + '/fixtures/manifest-blacklist.json',
|
||||
'test/bin/fixtures/manifest-test-skip.js',
|
||||
'test/bin/fixtures/manifest-test-use.js'
|
||||
];
|
||||
|
||||
exec(argv.join(' '), { cwd: ROOT }, function(err, stdout) {
|
||||
if (err) return done(err);
|
||||
assert.equal(
|
||||
stdout.trim(),
|
||||
__dirname + '/fixtures/manifest-test-use.js'
|
||||
);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
test('manifest-whitelist.json', function(done) {
|
||||
var argv = [
|
||||
BIN,
|
||||
__dirname + '/fixtures/manifest-whitelist.json',
|
||||
'test/bin/fixtures/manifest-test-skip.js',
|
||||
'test/bin/fixtures/manifest-test-use.js'
|
||||
];
|
||||
|
||||
exec(argv.join(' '), { cwd: ROOT }, function(err, stdout) {
|
||||
if (err) return done(err);
|
||||
assert.equal(
|
||||
stdout.trim(),
|
||||
__dirname + '/fixtures/manifest-test-use.js'
|
||||
);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
/* global spawnMarionette */
|
||||
'use strict';
|
||||
test('console.log --verbose', function(done) {
|
||||
this.timeout('10s');
|
||||
// Usual process spawning stuff the important bit here is this fixture will
|
||||
// fail and never complete unless we wait for 100s or kill the process.
|
||||
var proc = spawnMarionette([
|
||||
'--verbose',
|
||||
__dirname + '/fixtures/consolelog.js'
|
||||
]);
|
||||
|
||||
proc.stdout.on('data', function(buffer) {
|
||||
if (buffer.toString().indexOf('the fox say') !== -1) {
|
||||
proc.once('exit', done.bind(null, null));
|
||||
return;
|
||||
}
|
||||
});
|
||||
});
|
||||
25
gaia/tests/jsmarionette/runner/marionette-js-runner/test/bin/crash_test.js
Executable file
25
gaia/tests/jsmarionette/runner/marionette-js-runner/test/bin/crash_test.js
Executable file
|
|
@ -0,0 +1,25 @@
|
|||
/* global assert, spawnMarionette */
|
||||
'use strict';
|
||||
suite('Crash handling', function() {
|
||||
this.timeout('20s');
|
||||
|
||||
var proc;
|
||||
test('handle crash', function(done) {
|
||||
// Usual process spawning stuff the important bit here is this fixture will
|
||||
// fail and never complete unless we wait for 100s or kill the process.
|
||||
proc = spawnMarionette([__dirname + '/fixtures/crash.js']);
|
||||
|
||||
var hasCrash;
|
||||
proc.stderr.on('data', function(buffer) {
|
||||
if (buffer.toString().indexOf('Crash detected ') !== -1) {
|
||||
hasCrash = true;
|
||||
}
|
||||
});
|
||||
|
||||
proc.once('exit', function(code) {
|
||||
assert(hasCrash, 'has crash message');
|
||||
assert(code !== 0, 'must exit in error');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
'use strict';
|
||||
// Test here is only to test SIGINT functionality.
|
||||
marionette('stay blocked', function() {
|
||||
var client = marionette.client();
|
||||
|
||||
test('block', function() {
|
||||
// Ensure we don't die on any startup errors...
|
||||
try {
|
||||
client.executeScript(function() {});
|
||||
} catch (e) {}
|
||||
|
||||
client.setScriptTimeout(1000000);
|
||||
client.executeAsyncScript(function() {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
'use strict';
|
||||
function Builder() {}
|
||||
Builder.prototype = {
|
||||
build: function(callback) {
|
||||
// magic process exit number to indicate that this file was loaded.
|
||||
process.exit(66);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = Builder;
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"desiredCapability": true
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
'use strict';
|
||||
/**
|
||||
* @fileoverview A marionette test that simply logs something in gecko.
|
||||
*/
|
||||
suite('console proxy', function() {
|
||||
this.timeout('10s');
|
||||
var client = marionette.client();
|
||||
|
||||
test('log in host', function(done) {
|
||||
// logging is asynchronous so wait a bit longer then just the execution
|
||||
// so we actually get some logs...
|
||||
client.executeScript(function() {
|
||||
console.log('What does the fox say');
|
||||
});
|
||||
setTimeout(done, 2000);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
/* global Components, ctypes */
|
||||
'use strict';
|
||||
suite('crash', function() {
|
||||
var client = marionette.client();
|
||||
|
||||
test('crash me', function() {
|
||||
client.scope({ context: 'chrome' }).executeScript(function() {
|
||||
const Cu = Components.utils;
|
||||
Cu.import('resource://gre/modules/ctypes.jsm');
|
||||
|
||||
var dies = function() {
|
||||
var zero = new ctypes.intptr_t(8);
|
||||
var badptr = ctypes.cast(zero, ctypes.PointerType(ctypes.int32_t));
|
||||
badptr.contents;
|
||||
};
|
||||
|
||||
dump('Et tu, Brute?');
|
||||
dies();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"phone": "test on phone",
|
||||
"tv": "test on tv",
|
||||
"common": "test on both"
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
'use strict';
|
||||
var Promise = require('promise');
|
||||
var assert = require('assert');
|
||||
|
||||
exports.help = {
|
||||
group: {
|
||||
title: 'CUSTOM XFOO',
|
||||
description: 'CUSTOM DESC'
|
||||
},
|
||||
|
||||
arguments: {
|
||||
'--code': {
|
||||
type: 'int',
|
||||
help: 'exit code'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var expectedHost = {
|
||||
destroy: function() {
|
||||
return Promise.resolve();
|
||||
}
|
||||
};
|
||||
module.exports.createHost = function() {
|
||||
return Promise.resolve(expectedHost);
|
||||
};
|
||||
|
||||
module.exports.createSession = function(host, profile, opts) {
|
||||
assert.equal(host, expectedHost);
|
||||
process.exit(opts.code);
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"blacklist": {
|
||||
"test/bin/fixtures/manifest-test-skip.js": "Bug 12345 - whatever bug I want."
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
'use strict';
|
||||
marionette('test', {}, function() {
|
||||
process.exit(13);
|
||||
});
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
'use strict';
|
||||
marionette('test', {}, function() {
|
||||
process.exit(13);
|
||||
});
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
'use strict';
|
||||
marionette('test', {}, function() {
|
||||
process.exit(23);
|
||||
});
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"whitelist": ["test/bin/fixtures/manifest-test-use.js"]
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
marionette('device test', function() {
|
||||
var client = marionette.client();
|
||||
|
||||
test('test on phone', { devices: ['phone'] }, function() {
|
||||
1 + 1;
|
||||
});
|
||||
|
||||
test('test on tv', { devices: ['tv'] }, function() {
|
||||
1 + 1;
|
||||
});
|
||||
|
||||
test('test on both', { devices: ['phone', 'tv'] }, function() {
|
||||
1 + 1;
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
'use strict';
|
||||
marionette('test', function() {
|
||||
var client = marionette.client();
|
||||
|
||||
test('1+1', function() {
|
||||
1 + 1;
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,257 @@
|
|||
/* global assert, spawnMarionette, spawnMocha */
|
||||
'use strict';
|
||||
suite('mocha integration', function() {
|
||||
this.timeout('30s');
|
||||
var fs = require('fs');
|
||||
var fsPath = require('path');
|
||||
|
||||
function aggregateOutput(childProcess) {
|
||||
var result = {
|
||||
stderr: '',
|
||||
stdout: ''
|
||||
};
|
||||
|
||||
childProcess.stderr.on('data', function(out) {
|
||||
result.stderr += out;
|
||||
});
|
||||
|
||||
childProcess.stdout.on('data', function(out) {
|
||||
result.stdout += out;
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
var MS_REGEXP = /(([0-9]+) ?ms)/;
|
||||
var NEW_LINES = /(\n|(\s{2,}))/g;
|
||||
function waitForProcess(child, done) {
|
||||
var result = aggregateOutput(child);
|
||||
child.on('exit', function(code) {
|
||||
// there are very small newline/whitespace differences between
|
||||
// mocha and our marionette reporter... these probably are not
|
||||
// bugs but prevent us from verifying real content so they are stripped.
|
||||
['stderr', 'stdout'].forEach(function(field) {
|
||||
[MS_REGEXP, NEW_LINES].forEach(function(regex) {
|
||||
result[field + 'Raw'] = result[field];
|
||||
result[field] = result[field].replace(regex, '').trim();
|
||||
});
|
||||
});
|
||||
|
||||
// exit status is _really_ important
|
||||
result.code = code;
|
||||
done();
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
var tests = [
|
||||
// this also tests picking up mocha.opts
|
||||
['test', ['--reporter', 'spec']],
|
||||
['test', [
|
||||
'--reporter',
|
||||
fsPath.resolve(
|
||||
__dirname,
|
||||
'/../../node_modules/mocha/lib/reporters/spec'
|
||||
)
|
||||
]],
|
||||
['pending', ['--reporter', 'spec']],
|
||||
['with-helper', [
|
||||
'--require', fsPath.resolve(__dirname + '/../fixtures/helper.js'),
|
||||
'--reporter', 'spec'
|
||||
]
|
||||
]
|
||||
];
|
||||
|
||||
tests.forEach(function(pair) {
|
||||
var file = pair[0];
|
||||
var path = fsPath.resolve(__dirname + '/../fixtures/' + file);
|
||||
|
||||
var argv = [path].concat(pair[1]);
|
||||
|
||||
// run same test with same options on both mocha & our marionette proxy
|
||||
// runner.
|
||||
suite(file, function() {
|
||||
var mochaOut;
|
||||
var marionetteOut;
|
||||
|
||||
setup(function(done) {
|
||||
var proc = spawnMocha(argv);
|
||||
mochaOut = waitForProcess(proc, done);
|
||||
});
|
||||
|
||||
setup(function(done) {
|
||||
var proc = spawnMarionette(argv);
|
||||
marionetteOut = waitForProcess(proc, done);
|
||||
});
|
||||
|
||||
test('code', function() {
|
||||
assert.equal(
|
||||
JSON.stringify(mochaOut.code), JSON.stringify(marionetteOut.code)
|
||||
);
|
||||
});
|
||||
|
||||
test('stdout', function() {
|
||||
assert.equal(mochaOut.stdout, marionetteOut.stdout);
|
||||
});
|
||||
|
||||
test.skip('stderr', function() {
|
||||
assert.equal(mochaOut.stderr, marionetteOut.stderr);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
suite('--host', function() {
|
||||
var result;
|
||||
var argv = ['--host', __dirname + '/fixtures/host'];
|
||||
var Host = require('./fixtures/host');
|
||||
|
||||
suite('when passing --help', function() {
|
||||
setup(function(done) {
|
||||
var proc = spawnMarionette(argv.concat(['--help']));
|
||||
result = waitForProcess(proc, done);
|
||||
});
|
||||
|
||||
test('custom help is shown', function() {
|
||||
assert.ok(result.stdoutRaw.indexOf(Host.help.group.title) !== -1);
|
||||
assert.ok(result.stdoutRaw.indexOf(Host.help.group.description) !== -1);
|
||||
assert.ok(result.stdoutRaw.indexOf('--code') !== -1);
|
||||
});
|
||||
});
|
||||
|
||||
suite('run a test', function() {
|
||||
setup(function(done) {
|
||||
var proc = spawnMarionette(argv.concat([
|
||||
__dirname + '/fixtures/marionettetest',
|
||||
'--code',
|
||||
'55'
|
||||
]));
|
||||
result = waitForProcess(proc, done);
|
||||
});
|
||||
|
||||
test('exits with magic code', function() {
|
||||
assert.equal(result.code, 55, JSON.stringify(result));
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
suite('--profile-builder', function() {
|
||||
var result,
|
||||
argv = [
|
||||
'--profile-builder', __dirname + '/fixtures/builder',
|
||||
__dirname + '/fixtures/marionettetest'
|
||||
];
|
||||
|
||||
setup(function(done) {
|
||||
var proc = spawnMarionette(argv);
|
||||
result = waitForProcess(proc, done);
|
||||
});
|
||||
|
||||
test('exits with magic code', function() {
|
||||
assert.equal(result.code, 66, JSON.stringify(result));
|
||||
});
|
||||
});
|
||||
|
||||
suite('--marionette-capabilities', function() {
|
||||
var result,
|
||||
capabilitiesPath = __dirname + '/fixtures/capabilities.json',
|
||||
expected = JSON.stringify({ capabilities: require(capabilitiesPath) }),
|
||||
argv = [
|
||||
'--marionette-capabilities', capabilitiesPath,
|
||||
'--host-log', 'stdout',
|
||||
__dirname + '/fixtures/marionettetest'
|
||||
];
|
||||
|
||||
setup(function(done) {
|
||||
var proc = spawnMarionette(argv);
|
||||
result = waitForProcess(proc, done);
|
||||
});
|
||||
|
||||
test('desired capabilities are set', function() {
|
||||
assert.ok(result.stdoutRaw.indexOf(expected) !== -1);
|
||||
});
|
||||
});
|
||||
|
||||
suite('--device-type', function() {
|
||||
var result,
|
||||
devicesPath = __dirname + '/fixtures/devices.json',
|
||||
tests = require(devicesPath),
|
||||
argv = [
|
||||
'--device-type', 'phone',
|
||||
'--host-log', 'stdout',
|
||||
__dirname + '/fixtures/marionettedevicetest'
|
||||
];
|
||||
|
||||
setup(function(done) {
|
||||
var proc = spawnMarionette(argv);
|
||||
result = waitForProcess(proc, done);
|
||||
});
|
||||
|
||||
test('desired devices is being tested', function() {
|
||||
assert.ok(result.stdoutRaw.indexOf(tests.phone) !== -1);
|
||||
assert.ok(result.stdoutRaw.indexOf(tests.common) !== -1);
|
||||
assert.ok(result.stdoutRaw.indexOf(tests.tv) === -1);
|
||||
});
|
||||
});
|
||||
|
||||
suite('--host-log=stdout', function() {
|
||||
var result,
|
||||
argv = [
|
||||
'--host-log', 'stdout',
|
||||
__dirname + '/fixtures/marionettetest'
|
||||
];
|
||||
|
||||
setup(function(done) {
|
||||
var proc = spawnMarionette(argv);
|
||||
result = waitForProcess(proc, done);
|
||||
});
|
||||
|
||||
test('has marionette output', function() {
|
||||
assert.ok(result.stdout.indexOf('newSession') !== -1);
|
||||
assert.ok(result.stderr.indexOf('newSession') === -1);
|
||||
});
|
||||
});
|
||||
|
||||
suite('--host-log=stderr', function() {
|
||||
var result,
|
||||
argv = [
|
||||
'--host-log', 'stderr',
|
||||
__dirname + '/fixtures/marionettetest'
|
||||
];
|
||||
|
||||
setup(function(done) {
|
||||
var proc = spawnMarionette(argv);
|
||||
result = waitForProcess(proc, done);
|
||||
});
|
||||
|
||||
test('has marionette output', function() {
|
||||
assert.ok(result.stderr.indexOf('newSession') !== -1);
|
||||
assert.ok(result.stdout.indexOf('newSession') !== 0);
|
||||
});
|
||||
});
|
||||
|
||||
suite('--host-log=<file>', function() {
|
||||
var file = __dirname + '/log.txt';
|
||||
var result,
|
||||
argv = [
|
||||
'--host-log', file,
|
||||
__dirname + '/fixtures/marionettetest'
|
||||
];
|
||||
|
||||
setup(function(done) {
|
||||
var proc = spawnMarionette(argv);
|
||||
result = waitForProcess(proc, done);
|
||||
});
|
||||
|
||||
teardown(function() {
|
||||
fs.unlinkSync(file);
|
||||
});
|
||||
|
||||
test('has marionette output', function() {
|
||||
assert.ok(fs.existsSync(file));
|
||||
assert.ok(fs.readFileSync(file, 'utf8').indexOf('newSession') !== -1);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
122
gaia/tests/jsmarionette/runner/marionette-js-runner/test/bin/sigint_test.js
Executable file
122
gaia/tests/jsmarionette/runner/marionette-js-runner/test/bin/sigint_test.js
Executable file
|
|
@ -0,0 +1,122 @@
|
|||
/* global spawnMarionette */
|
||||
'use strict';
|
||||
suite('SIGINT', function() {
|
||||
var exec = require('child_process').exec;
|
||||
var assert = require('assert');
|
||||
|
||||
// Cleanup pid line (works for columns and value lines)
|
||||
function splitPidLine(line) {
|
||||
return line.trim().replace(/\s+/g, ' ').split(' ');
|
||||
}
|
||||
|
||||
// Pretty much what you would expect out of a ps parser.
|
||||
function ps(callback) {
|
||||
// ppid is important here for the intent of this test but aside from the
|
||||
// flags this code is actually could be general purpose.
|
||||
exec('ps -o comm -o pid -o ppid', function(err, stdout) {
|
||||
if (err) return callback(err);
|
||||
var lines = stdout.replace('\r', '').split('\n');
|
||||
var columns = splitPidLine(lines.shift());
|
||||
var result = [];
|
||||
|
||||
lines.forEach(function(line) {
|
||||
var lineParts = splitPidLine(line);
|
||||
if (lineParts.length < columns.length) return;
|
||||
var obj = {};
|
||||
lineParts.forEach(function(part, idx) {
|
||||
// lower case to better fit node conventions...
|
||||
if (!columns[idx]) {
|
||||
return;
|
||||
}
|
||||
|
||||
obj[columns[idx].toLowerCase()] = part;
|
||||
});
|
||||
result.push(obj);
|
||||
});
|
||||
callback(null, result);
|
||||
});
|
||||
}
|
||||
|
||||
// Find all processes which share a starting process parent.
|
||||
function recursivePpidSearch(pid, callback) {
|
||||
ps(function(err, list) {
|
||||
if (err) return callback(err);
|
||||
|
||||
var result = [];
|
||||
|
||||
// Yes this can stack overflow be very careful if this is used elsewhere.
|
||||
function findPpid(searchPid) {
|
||||
list.forEach(function(item) {
|
||||
if (item.ppid == searchPid) {
|
||||
findPpid(item.pid);
|
||||
result.push(item);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
findPpid(pid);
|
||||
return callback(null, result);
|
||||
});
|
||||
}
|
||||
|
||||
function ensureKill(testFile, waitTime) {
|
||||
suite('SIGKILL ' + testFile + ' after ' + waitTime + ' ms', function() {
|
||||
var proc, processList, pid;
|
||||
setup(function(done) {
|
||||
// Usual process spawning stuff the important bit here is this fixture
|
||||
// will fail and never complete unless we wait for 100s or kill the
|
||||
// process.
|
||||
proc = spawnMarionette([testFile]);
|
||||
proc.stdout.pipe(process.stdout);
|
||||
proc.stderr.pipe(process.stderr);
|
||||
pid = proc.pid;
|
||||
|
||||
// Wait for all the created processes to finish cleanly
|
||||
// - b2g process
|
||||
// - child _mocha process
|
||||
// - proxy reporter process
|
||||
var processes = 3;
|
||||
|
||||
function search() {
|
||||
recursivePpidSearch(process.pid, function(err, list) {
|
||||
if (err) return done(err);
|
||||
if (list.length < processes) return setTimeout(search);
|
||||
processList = list;
|
||||
setTimeout(done, waitTime);
|
||||
});
|
||||
}
|
||||
search();
|
||||
});
|
||||
|
||||
test('cleanly sigkill with no leftovers', function(done) {
|
||||
proc.kill('SIGINT');
|
||||
proc.once('exit', function(code) {
|
||||
ps(function(err, currentProcesses) {
|
||||
if (err) return done(err);
|
||||
var remaining = processList.filter(function(startProc) {
|
||||
for (var i = 0; i < currentProcesses.length; i++) {
|
||||
if (currentProcesses[i].pid == startProc.pid) return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
assert(
|
||||
!remaining.length,
|
||||
'Process still running: ' + JSON.stringify(remaining)
|
||||
);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Basically instantly.
|
||||
ensureKill(__dirname + '/../integration/multiclient_test', 10);
|
||||
// Very early in the startup.
|
||||
ensureKill(__dirname + '/../integration/multiclient_test', 100);
|
||||
// Sessions are probably being created.
|
||||
ensureKill(__dirname + '/../integration/multiclient_test', 500);
|
||||
// Marionette is blocked.
|
||||
ensureKill(__dirname + '/fixtures/blocked', 500);
|
||||
});
|
||||
|
||||
7
gaia/tests/jsmarionette/runner/marionette-js-runner/test/ci.sh
Executable file
7
gaia/tests/jsmarionette/runner/marionette-js-runner/test/ci.sh
Executable file
|
|
@ -0,0 +1,7 @@
|
|||
curl http://nodejs.org/dist/v0.10.33/node-v0.10.33-linux-x64.tar.gz | tar xz
|
||||
node_path=$(find $PWD -d -name 'node-v*' | head -n 1)
|
||||
export PATH=$node_path:$PATH
|
||||
|
||||
Xvfb :99 &
|
||||
export DISPLAY=:99
|
||||
make test
|
||||
15
gaia/tests/jsmarionette/runner/marionette-js-runner/test/error_ipc_test.js
Executable file
15
gaia/tests/jsmarionette/runner/marionette-js-runner/test/error_ipc_test.js
Executable file
|
|
@ -0,0 +1,15 @@
|
|||
/* global assert */
|
||||
'use strict';
|
||||
suite('error_ipc', function() {
|
||||
var subject = require('../lib/error_ipc');
|
||||
|
||||
test('roundtrip', function() {
|
||||
var error = new Error('xfoo');
|
||||
var out = subject.deserialize(subject.serialize(error));
|
||||
|
||||
assert(out instanceof Error, 'instanceof Error');
|
||||
assert.equal(out.message, error.message);
|
||||
assert.equal(out.stack, error.stack);
|
||||
});
|
||||
|
||||
});
|
||||
1
gaia/tests/jsmarionette/runner/marionette-js-runner/test/fixtures/helper.js
vendored
Executable file
1
gaia/tests/jsmarionette/runner/marionette-js-runner/test/fixtures/helper.js
vendored
Executable file
|
|
@ -0,0 +1 @@
|
|||
global.MAGIC_GLOBAL_VARIABLE = true;
|
||||
0
gaia/tests/jsmarionette/runner/marionette-js-runner/test/fixtures/noop.js
vendored
Executable file
0
gaia/tests/jsmarionette/runner/marionette-js-runner/test/fixtures/noop.js
vendored
Executable file
4
gaia/tests/jsmarionette/runner/marionette-js-runner/test/fixtures/pending.js
vendored
Executable file
4
gaia/tests/jsmarionette/runner/marionette-js-runner/test/fixtures/pending.js
vendored
Executable file
|
|
@ -0,0 +1,4 @@
|
|||
'use strict';
|
||||
suite('pending', function() {
|
||||
test('iampending');
|
||||
});
|
||||
33
gaia/tests/jsmarionette/runner/marionette-js-runner/test/fixtures/rpc.js
vendored
Executable file
33
gaia/tests/jsmarionette/runner/marionette-js-runner/test/fixtures/rpc.js
vendored
Executable file
|
|
@ -0,0 +1,33 @@
|
|||
'use strict';
|
||||
var RPC = require('../../lib/rpc');
|
||||
var Promise = require('promise');
|
||||
|
||||
var rpc = new RPC(process.send.bind(process));
|
||||
|
||||
rpc.register('test', {
|
||||
$rpc: { methods: ['args'] },
|
||||
|
||||
getSelf: function() {
|
||||
return Promise.resolve(this);
|
||||
},
|
||||
|
||||
noArgs: function() {
|
||||
return Promise.resolve();
|
||||
},
|
||||
|
||||
args: function() {
|
||||
var args = Array.prototype.slice.call(arguments);
|
||||
return new Promise(function(accept) {
|
||||
return accept(args);
|
||||
});
|
||||
},
|
||||
|
||||
error: function() {
|
||||
return new Promise(function() {
|
||||
throw new Error('xfoo');
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
var handle = rpc.handle();
|
||||
process.on('message', handle);
|
||||
19
gaia/tests/jsmarionette/runner/marionette-js-runner/test/fixtures/test.js
vendored
Executable file
19
gaia/tests/jsmarionette/runner/marionette-js-runner/test/fixtures/test.js
vendored
Executable file
|
|
@ -0,0 +1,19 @@
|
|||
'use strict';
|
||||
suite('a test', function() {
|
||||
console.log('before running test log');
|
||||
test('test one', function() {
|
||||
});
|
||||
|
||||
suite('another suite fail', function() {
|
||||
test('throw', function() {
|
||||
throw new Error('xxx');
|
||||
});
|
||||
});
|
||||
|
||||
test('multiple console logs', function(done) {
|
||||
setTimeout(function() {
|
||||
console.log('works');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
1
gaia/tests/jsmarionette/runner/marionette-js-runner/test/fixtures/testops.opts
vendored
Executable file
1
gaia/tests/jsmarionette/runner/marionette-js-runner/test/fixtures/testops.opts
vendored
Executable file
|
|
@ -0,0 +1 @@
|
|||
--magicfoobar --dodad
|
||||
2
gaia/tests/jsmarionette/runner/marionette-js-runner/test/fixtures/testopsnewline.opts
vendored
Executable file
2
gaia/tests/jsmarionette/runner/marionette-js-runner/test/fixtures/testopsnewline.opts
vendored
Executable file
|
|
@ -0,0 +1,2 @@
|
|||
--one
|
||||
--two
|
||||
4
gaia/tests/jsmarionette/runner/marionette-js-runner/test/fixtures/with-helper.js
vendored
Executable file
4
gaia/tests/jsmarionette/runner/marionette-js-runner/test/fixtures/with-helper.js
vendored
Executable file
|
|
@ -0,0 +1,4 @@
|
|||
'use strict';
|
||||
test('global variable', function() {
|
||||
assert.ok(MAGIC_GLOBAL_VARIABLE);
|
||||
});
|
||||
61
gaia/tests/jsmarionette/runner/marionette-js-runner/test/helper.js
Executable file
61
gaia/tests/jsmarionette/runner/marionette-js-runner/test/helper.js
Executable file
|
|
@ -0,0 +1,61 @@
|
|||
'use strict';
|
||||
var EventEmitter = require('events').EventEmitter;
|
||||
var assert = require('chai').assert;
|
||||
var spawn = require('child_process').spawn;
|
||||
|
||||
global.assert = assert;
|
||||
global.sinon = require('sinon');
|
||||
|
||||
// so we can call assert.calledWith
|
||||
global.sinon.assert.expose(assert, { prefix: '' });
|
||||
|
||||
function env() {
|
||||
var vars = {};
|
||||
for (var key in process.env) vars[key] = process.env[key];
|
||||
vars.MOCHA_COLORS = '0';
|
||||
return vars;
|
||||
}
|
||||
|
||||
function spawnMocha(argv) {
|
||||
var mocha = __dirname + '/../../.bin/mocha';
|
||||
return spawn(mocha, argv, { env: env() });
|
||||
}
|
||||
|
||||
function spawnMarionette(argv) {
|
||||
var bin = __dirname + '/../bin/marionette-mocha';
|
||||
argv.push('--runtime');
|
||||
argv.push(__dirname + '/../../../b2g/b2g-bin');
|
||||
return spawn(bin, argv, { env: env() });
|
||||
}
|
||||
|
||||
function mockProcessSend() {
|
||||
var refs = {},
|
||||
originalSend;
|
||||
|
||||
// spy on process.send
|
||||
setup(function() {
|
||||
var sent = refs.sent = new EventEmitter();
|
||||
originalSend = process.send;
|
||||
|
||||
process.send = function(event) {
|
||||
if (Array.isArray(event) && typeof event[0] === 'string') {
|
||||
sent.emit.apply(sent, event);
|
||||
}
|
||||
|
||||
// if there is a send method use it.
|
||||
if (originalSend) {
|
||||
originalSend.apply(this, arguments);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
teardown(function() {
|
||||
process.send = originalSend;
|
||||
});
|
||||
|
||||
return refs;
|
||||
}
|
||||
|
||||
global.mockProcessSend = mockProcessSend;
|
||||
global.spawnMocha = spawnMocha;
|
||||
global.spawnMarionette = spawnMarionette;
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
'use strict';
|
||||
var assert = require('assert');
|
||||
|
||||
marionette('default client', function() {
|
||||
// create a client.
|
||||
var client = marionette.client();
|
||||
|
||||
test('is sync', function() {
|
||||
var result = client.executeScript(function() {
|
||||
// attempt to dirty global state
|
||||
window.wrappedJSObject.I_WAS_BAD = true;
|
||||
|
||||
// do complicated math in gecko
|
||||
return 1 + 1;
|
||||
});
|
||||
|
||||
// math works
|
||||
assert.equal(result, 2, 'tests are async');
|
||||
|
||||
// state is saved inside of same test
|
||||
var bad = client.executeScript(function() {
|
||||
return window.wrappedJSObject.I_WAS_BAD;
|
||||
});
|
||||
assert.ok(bad, 'state is saved');
|
||||
});
|
||||
|
||||
test('host is reset is each test', function() {
|
||||
var bad = client.executeScript(function() {
|
||||
return window.wrappedJSObject.I_WAS_BAD;
|
||||
});
|
||||
assert.ok(!bad, 'state is clean');
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
'use strict';
|
||||
var assert = require('assert');
|
||||
|
||||
marionette('client with options', function() {
|
||||
// create a client.
|
||||
var options = {
|
||||
screen: {
|
||||
width: 353,
|
||||
height: 577
|
||||
}
|
||||
};
|
||||
var client = marionette.client({ profile: { hostOptions: options } });
|
||||
|
||||
test('screen size', function() {
|
||||
var result = client.executeScript(function() {
|
||||
return { width: window.innerWidth,
|
||||
height: window.innerHeight};
|
||||
});
|
||||
|
||||
assert.equal(result.width, options.screen.width);
|
||||
assert.equal(result.height, options.screen.height);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
'use strict';
|
||||
var assert = require('assert');
|
||||
|
||||
marionette('client async', function() {
|
||||
var Tcp = require('marionette-client').Drivers.Tcp;
|
||||
var client = marionette.client({ driver: Tcp });
|
||||
|
||||
function onePlusOne() {
|
||||
return 1 + 1;
|
||||
}
|
||||
|
||||
test('async magic', function(done) {
|
||||
var isAsync = false;
|
||||
client.executeScript(onePlusOne, function(err, result) {
|
||||
if (!isAsync) return done(new Error('is sync!'));
|
||||
assert.equal(result, 2);
|
||||
done();
|
||||
});
|
||||
isAsync = true;
|
||||
});
|
||||
|
||||
});
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
'use strict';
|
||||
var assert = require('assert');
|
||||
|
||||
marionette('client with capabilities', function() {
|
||||
// create a client and set desired capabilities.
|
||||
var client = marionette.client({
|
||||
desiredCapabilities: { desiredCapability: true }
|
||||
});
|
||||
|
||||
test('test capabilities', function(done) {
|
||||
client.sessionCapabilities(function(err, capabilities) {
|
||||
// Capabilities are set
|
||||
assert.ok(capabilities.desiredCapability);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
'use strict';
|
||||
var assert = require('assert');
|
||||
|
||||
marionette('multi-client', function() {
|
||||
var one = marionette.client();
|
||||
var two = marionette.client();
|
||||
|
||||
test('ports are not equal', function() {
|
||||
assert.notEqual(
|
||||
one.driver.port,
|
||||
two.driver.port
|
||||
);
|
||||
});
|
||||
|
||||
// run a quick test to verify client works
|
||||
function validateClient(name, client) {
|
||||
test(name, function() {
|
||||
var result = client.executeScript(function() {
|
||||
return 1 + 1;
|
||||
});
|
||||
assert.equal(result, 2);
|
||||
});
|
||||
}
|
||||
|
||||
validateClient('first client', one);
|
||||
validateClient('second client', two);
|
||||
});
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
'use strict';
|
||||
var assert = require('assert');
|
||||
|
||||
// Global plugin
|
||||
marionette.plugin('globalme', function() {
|
||||
return { isGlobal: true };
|
||||
});
|
||||
|
||||
marionette('plugins', function() {
|
||||
function PluginA(client, options) {
|
||||
return PluginA;
|
||||
}
|
||||
|
||||
var pluginId = 0;
|
||||
function PluginB(client, options) {
|
||||
return { id: ++pluginId };
|
||||
}
|
||||
|
||||
// create global plugin
|
||||
marionette.plugin('a', PluginA);
|
||||
marionette.plugin('b', PluginB);
|
||||
|
||||
var client = marionette.client();
|
||||
|
||||
test('global plugin is exposed', function() {
|
||||
assert(client.globalme.isGlobal);
|
||||
});
|
||||
|
||||
test('a is exposed', function() {
|
||||
assert.strictEqual(client.a, PluginA);
|
||||
});
|
||||
|
||||
test('plugins are created for each test', function() {
|
||||
assert.equal(client.b.id, 3);
|
||||
});
|
||||
|
||||
suite('overrides plugin a', function() {
|
||||
function MyA() { return MyA; }
|
||||
|
||||
marionette.plugin('a', MyA);
|
||||
|
||||
test('can override plugins', function() {
|
||||
assert.strictEqual(client.a, MyA);
|
||||
});
|
||||
});
|
||||
|
||||
suite('a is reset after suite to original', function() {
|
||||
test('after other suite', function() {
|
||||
assert.deepEqual(client.a, PluginA);
|
||||
});
|
||||
|
||||
test('b is still plugin b', function() {
|
||||
assert.equal(client.b.id, 6);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
/* global assert */
|
||||
'use strict';
|
||||
suite('optsfile', function() {
|
||||
var optsfile = require('../lib/optsfileparser');
|
||||
|
||||
/**
|
||||
* @param {String} file to load with optsfile relative to fixtures.
|
||||
*/
|
||||
function load(file) {
|
||||
return optsfile(__dirname + '/fixtures/' + file);
|
||||
}
|
||||
|
||||
test('parsing valid optsfile', function() {
|
||||
assert.deepEqual(
|
||||
load('testops.opts'),
|
||||
['--magicfoobar', '--dodad']
|
||||
);
|
||||
});
|
||||
|
||||
test('optsfile with newlines', function() {
|
||||
assert.deepEqual(
|
||||
load('testopsnewline.opts'),
|
||||
['--one', '--two']
|
||||
);
|
||||
});
|
||||
});
|
||||
66
gaia/tests/jsmarionette/runner/marionette-js-runner/test/rpc_test.js
Executable file
66
gaia/tests/jsmarionette/runner/marionette-js-runner/test/rpc_test.js
Executable file
|
|
@ -0,0 +1,66 @@
|
|||
'use strict';
|
||||
suite('rpc', function() {
|
||||
var RPC = require('../lib/rpc');
|
||||
var fork = require('child_process').fork;
|
||||
var assert = require('assert');
|
||||
|
||||
var proc;
|
||||
suiteSetup(function() {
|
||||
proc = fork(__dirname + '/fixtures/rpc');
|
||||
});
|
||||
|
||||
suiteTeardown(function() {
|
||||
proc.kill();
|
||||
});
|
||||
|
||||
var subject, client;
|
||||
setup(function() {
|
||||
subject = new RPC(proc.send.bind(proc));
|
||||
client = subject.client('test', ['noArgs', 'args', 'error', 'getSelf']);
|
||||
proc.on('message', subject.handle());
|
||||
});
|
||||
|
||||
test('noArgs', function() {
|
||||
return client.noArgs();
|
||||
});
|
||||
|
||||
test('args', function() {
|
||||
return client.args(1, 2, 3, 4).then(function(result) {
|
||||
assert.deepEqual(result, [1, 2, 3, 4]);
|
||||
});
|
||||
});
|
||||
|
||||
test('objects', function() {
|
||||
var obj;
|
||||
return client.getSelf().then(function(rpcObj) {
|
||||
obj = rpcObj;
|
||||
return rpcObj.args('woot', 'bar');
|
||||
}).then(function(value) {
|
||||
assert.deepEqual(value, ['woot', 'bar']);
|
||||
}).then(function() {
|
||||
assert(obj.destroy, 'has implicit destroy method');
|
||||
return obj.destroy();
|
||||
}).then(function() {
|
||||
return obj.args();
|
||||
}).then(
|
||||
function() { throw new Error('expected error'); },
|
||||
function(err) {
|
||||
assert.ok(err.message.indexOf('destroyed') !== -1);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
test('error', function() {
|
||||
return client.error()
|
||||
.then(function() {
|
||||
process.nextTick(function() {
|
||||
throw new Error('must not resolve');
|
||||
});
|
||||
})
|
||||
.catch (function(err) {
|
||||
assert.equal(err.message, 'xfoo');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
Loading…
Reference in a new issue