forked from mozilla-b2g/gaia
-
Notifications
You must be signed in to change notification settings - Fork 1
/
recorder_client_helper.js
330 lines (287 loc) · 10.4 KB
/
recorder_client_helper.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
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
/* jshint node: true, browser: true */
/* global marionette, suiteSetup, setup, teardown, suiteTeardown */
'use strict';
/**
* When running b2g-desktop and if available, wrap its invocation so that we are
* able to record its execution to a .webm file.
*
**/
var fs = require('fs');
var path = require('path');
var mkdirp = require('mkdirp');
var xRecorder = require('x-recorder');
var EventEmitter = require('events').EventEmitter;
// XXX until this is in gaia/shared/test/integration we need this here.
marionette.plugin('logger', require('marionette-js-logger'));
/**
* Return true if our target is (local) b2g-desktop and we have xvfb and ffmpeg
* available.
*/
function canRunRecorded() {
// XXX IMPLEMENT AFTER WORKING AT ALL WITH THOSE THINGS INSTALLED
return true;
}
/**
* Given an absolute test file path, convert it to a nice small path hierarchy.
*/
function normalizeTestFilePath(file) {
var parts = file.split(path.sep);
var useRelPath = '';
// assume the path looks something like:
// /blah/blah/some-gaia-name/apps/APPNAME/test/marionette/FILENAME
// We care about the APPNAME, and the 'apps' dir helps us find it. We
// don't want to assume too much about the apps' internal structure for
// reasons of potential brittleness.
var appDirIndex = null;
// Don't search things that can't be the app indicator, specifically, ignore
// the last 3 since we want at least: apps/APPNAME/sometestindicator/FILENAME
parts.slice(0, -3).some(function(part, index) {
// There are a bunch of app directory roots; try and match the expected
// pattern.
if (/^(?:.+[-_])?apps$/.test(part)) {
appDirIndex = index;
return true;
}
return false;
});
if (appDirIndex !== null) {
useRelPath += parts[appDirIndex + 1] + path.sep;
}
// Just use the file-name sans-extension (to avoid confusion) and
// spaces-coverted-to-underscores (since spaces can be annoying in command
// lines)
useRelPath += path.basename(file, path.extname(file))
.replace(/ /g, '_');
return useRelPath;
}
/**
* Create a reasonable filename given a test title.
*/
function normalizeTestTitleToFile(title) {
return title.replace(/\W+/g, '_').substring(0, 24).toLowerCase();
}
/**
* The system app shows startup logos or videos or what not, depending on
* configuration. Although this is not a bad idea, as of writing this, the
* logo's fadeout styling in initlogo.css takes 2 seconds to complete, which
* is not helpful when unit testing. Until we have the time dilation factor
* available to us for animations, it's best to just clobber it to death.
*
* The code lives in apps/system/js/init_logo_handler.js.
*/
function nukeAnnoyingOsLogo(client) {
// The system app is the top-level frame as far as Marionette is concerned.
// (shell.html is the true top-level, but hey.)
client.switchToFrame(null);
var removedAtTS = client.findElement('#os-logo').scriptWith(function(elem) {
elem.parentNode.removeChild(elem);
return Date.now();
});
return removedAtTS;
}
exports.recordedMarionetteClient = function() {
// Note: all settings are biased towards the e-mail app's use-case right now.
//
// Also, we will mutate this after we spin up the xvfb instance; this entire
// dictionary is held by reference and not remoted until the remote
// instance is created.
var profileSettings = {
prefs: {
// Do not require the B2G-desktop app window to have focus (as per the
// system window manager) in order for it to do focus-related things.
'focusmanager.testmode': true,
},
settings: {
// Explicitly disable the FTU (first-run) series of cards
'ftu.manifestURL': '',
// lock-screen stuff:
// 'screen.timeout'
'lockscreen.enabled': false
// 'lockscreen.locked'
}
// no 'apps' needed
// 'hostOptions' gets filled in below
};
// Check if we are actually in a configuration where we can do our fancy
// stuff. If not, just bail and use the standard marionette client logic.
if (!canRunRecorded) {
return marionette.client(profileSettings);
}
console.log('IN RECORDER LAND');
// Use the dominant/smallest resolution we have for tests by default. We
// will probably need to actually run with permutations in the future.
var dimWidth = 320;
var dimHeight = 480;
// It's quite possible there will be annoying window borders or something.
var windowPaddingX = 10;
var windowPaddingY = 10;
var vidWidth = dimWidth + windowPaddingX;
var vidHeight = dimHeight + windowPaddingY;
var xvfbDimensions = vidWidth + 'x' + vidHeight;
var testArtifactsDir;
var xvfb, xcapture, logStream;
// We want our suite setup to run prior to the host creation so we can set-up
// the xvfb instance.
suiteSetup(function(done) {
console.log('creating xvfb');
xvfb = new xRecorder.Xvfb({
dimensions: xvfbDimensions + 'x24',
});
xvfb.start(function() {
profileSettings.hostOptions = {
envOverrides: {
DISPLAY: ':' + xvfb.display
}
};
console.log('xvfb started. super happy!');
done();
});
});
var videoPath;
// Likewise, we want to start recording prior to each test case.
setup(function(done) {
console.log('creating log');
var curTest = this.currentTest;
testArtifactsDir = 'artifacts' + path.sep +
normalizeTestFilePath(curTest.title) + path.sep;
// Everything else we're doing is inherently dependent on creating the dir,
// so just do it synchronously (for now).
if (!fs.existsSync(testArtifactsDir)) {
mkdirp.sync(testArtifactsDir);
}
var basenamePath = testArtifactsDir +
normalizeTestTitleToFile(curTest.title);
videoPath = basenamePath + '.webm';
// We write one JSON string per line since consumers should absolutely
// process this as a stream rather than trying to load it as a single big
// array. While it might be friendly to try and let them do that load if
// they wanted, there's a fair chance we might have to deal with crashes,
// in which case we may fail to close out the file and the string may end
// up only partially written.
var jsonLogPath = basenamePath + '.jsons';
logStream = fs.createWriteStream(
jsonLogPath,
{ flags: 'w', encoding: 'utf8', mode: /* 0o666 */ 438 });
// We cannot access client.logger until the plugin is spun up at the
// 'startSession' hook, to defer registering for messages until after that
// time. (See below.)
console.log('creating xcapture, save target of', videoPath);
xcapture = new xRecorder.XCapture({
display: xvfb.display,
output: videoPath,
tool: 'avconv',
codec: 'libvpx',
dimensions: xvfbDimensions
});
xcapture.start(function(err, proc, startTimeSecs) {
// This start time will be accurate, but if you are doing something
// like encoding video to webm on the fly, frames may be dropped and
// sadness will result because the timestamps will become useless because
// in the case it's likely ffmpeg/avconv was not actually aware frames
// were being dropped and the timestamps will be lies.
client.recorderHelper.logObj({
source: 'test',
type: 'video-start',
path: videoPath,
startTS: Math.floor(startTimeSecs * 1000),
width: vidWidth,
height: vidHeight
});
done();
});
});
// Fetch the logs before we shut down the host.
teardown(function() {
console.log('grabbing log messages at shutdown');
client.logger.grabLogMessages();
});
// Investigate failures and log state.
teardown(function() {
console.log('failcheck', this.currentTest.state);
if (this.currentTest.state !== 'failed') {
return;
}
console.log('FAILURE FAILURE FAILURE, filling in details');
var failureDetails = {};
client.recorderHelper.emit('fill-in-failure-details', failureDetails);
var failureLog = {
source: 'test',
type: 'failureLog',
timeStamp: Date.now(),
details: failureDetails
};
client.recorderHelper.logObj(failureLog);
});
var client = marionette.client(profileSettings);
// Disable the default script timeout which logs data-URI screenshots.
client.onScriptTimeout = null;
client.recorderHelper = new EventEmitter();
// helper method to help us/others be able to explicitly log something to the
// log file.
client.recorderHelper.logObj = function(obj) {
if (logStream) {
logStream.write(JSON.stringify(obj) + '\n');
}
};
// - Things that require the plugins to exist.
// Plugins are created during the setup() phase of marionette-js-runner's
// client-creating logic. That stage also creates the session as a blocking
// asynchronous process. Thus we are ensured that client.logger exists by
// the time our function is called since we are calling setup() after
// marionette.client. However, when we become a plugin we will want to wait
// for startSession since that ensures all plugins have been initialized
// without requiring an explicit plugin ordering.
setup(function() {
// client.addHook('startSession', function() {
client.recorderHelper.logObj({
source: 'test',
type: 'start',
startTS: Date.now()
});
console.log('listening for logger messages');
client.logger.on('message', function(msg) {
var logObj = {
source: 'client',
type: 'log',
msg: msg
};
logStream.write(JSON.stringify(logObj) + '\n');
});
// To synchronize with the video, we tell it when we removed the splash
// screen and let the
var killedAt = nukeAnnoyingOsLogo(client);
client.recorderHelper.logObj({
source: 'test',
type: 'splashkilled',
timeStamp: killedAt
});
// });
});
// But we want to stop recording after the host gets torn down.
teardown(function(done) {
client.recorderHelper.logObj({
source: 'test',
type: 'video-stop',
timeStamp: Date.now()
});
console.log('stopping xcapture');
xcapture.stop(function(err) {
console.log(' stopped. err?', err);
logStream.end(function() {
logStream = null;
done();
});
});
xcapture = null;
});
// And to kill the xvfb instance after the host gets
suiteTeardown(function(done) {
console.log('stopping xvfb');
xvfb.stop(function(err) {
console.log(' stopped. err?', err);
done();
});
xvfb = null;
});
return client;
};