forked from mozilla-b2g/gaia
/
runner.js
249 lines (208 loc) · 7.03 KB
/
runner.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
'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 = 60030;
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 })
.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;