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
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;
|
||||
}
|
||||
Loading…
Reference in a new issue