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:
OverSu 2026-09-30 23:13:03 +02:00
commit b38dea0597
25901 changed files with 9982925 additions and 369 deletions

View file

@ -0,0 +1,3 @@
/crash
/symbols
/venv

View 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.

View 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(' ')
)

View 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);
}

View 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);
}

View 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;

View 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();
});
};

View file

@ -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);
});
});
};

View file

@ -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

View file

@ -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

View file

@ -0,0 +1,4 @@
[console_scripts]
gaia-integration=runner_service:runintegration.cli

View file

@ -0,0 +1,3 @@
mozrunner >= 6.11
mozprofile >= 0.21
mozcrash >= 0.16

View file

@ -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

View file

@ -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,
}

View file

@ -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()

View file

@ -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 {}

View file

@ -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())

View file

@ -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
""")

View 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;
}

View 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;

View 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;
};

View 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;

View 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;
};

View 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));
}
};

View 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;

View 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;

View 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;

View 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;

View 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;
}

View 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"
}
}

View 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

View file

@ -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();
});
});
});

View file

@ -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;
}
});
});

View 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();
});
});
});

View file

@ -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() {
});
});
});

View file

@ -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;

View file

@ -0,0 +1,3 @@
{
"desiredCapability": true
}

View file

@ -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);
});
});

View file

@ -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();
});
});
});

View file

@ -0,0 +1,5 @@
{
"phone": "test on phone",
"tv": "test on tv",
"common": "test on both"
}

View file

@ -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();
};

View file

@ -0,0 +1,5 @@
{
"blacklist": {
"test/bin/fixtures/manifest-test-skip.js": "Bug 12345 - whatever bug I want."
}
}

View file

@ -0,0 +1,4 @@
'use strict';
marionette('test', {}, function() {
process.exit(13);
});

View file

@ -0,0 +1,4 @@
'use strict';
marionette('test', {}, function() {
process.exit(13);
});

View file

@ -0,0 +1,4 @@
'use strict';
marionette('test', {}, function() {
process.exit(23);
});

View file

@ -0,0 +1,3 @@
{
"whitelist": ["test/bin/fixtures/manifest-test-use.js"]
}

View file

@ -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;
});
});

View file

@ -0,0 +1,8 @@
'use strict';
marionette('test', function() {
var client = marionette.client();
test('1+1', function() {
1 + 1;
});
});

View file

@ -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);
});
});
});

View 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);
});

View 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

View 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);
});
});

View file

@ -0,0 +1 @@
global.MAGIC_GLOBAL_VARIABLE = true;

View file

@ -0,0 +1,4 @@
'use strict';
suite('pending', function() {
test('iampending');
});

View 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);

View 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();
});
});
});

View file

@ -0,0 +1 @@
--magicfoobar --dodad

View file

@ -0,0 +1,2 @@
--one
--two

View file

@ -0,0 +1,4 @@
'use strict';
test('global variable', function() {
assert.ok(MAGIC_GLOBAL_VARIABLE);
});

View 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;

View file

@ -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');
});
});

View file

@ -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);
});
});

View file

@ -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;
});
});

View file

@ -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();
});
});
});

View file

@ -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);
});

View file

@ -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);
});
});
});

View file

@ -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']
);
});
});

View 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');
});
});
});