This repository was archived by the owner on Feb 26, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 13
/
Copy pathindex.js
354 lines (330 loc) · 11.2 KB
/
index.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
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
var q = require('q'),
fs = require('fs'),
path = require('path'),
SauceLabs = require('saucelabs'),
https = require('https');
var SAUCE_LOGS_WAIT = 5000;
/**
* Outputs information about where your Protractor test is spending its time
* to the specified folder. A JSON data file and small index.html to view
* it will be created. The page uses Google Charts to show the timeline.
*
* You enable this plugin in your config file:
*
* exports.config = {
* plugins: [{
* path: 'node_modules/protractor/plugins/timeline',
*
* // Output json and html will go in this folder. Relative
* // to current working directory of the process.
* // TODO - it would make more sense for this to be relative
* // to the config file - reconsider this setup
* outdir: 'timelines',
*
* // Optional - if sauceUser and sauceKey are specified, logs from
* // SauceLabs will also be parsed after test invocation.
* sauceUser: 'Jane',
* sauceKey: 'abcdefg'
* }]
* };
*
* The plugin will create timeline entries from
* - The Protractor test process itself.
* - The WebDriver Selenium Server (these logs are unavailable for Internet
* Explorer and for Chrome test run over Sauce Labs).
* - Sauce Labs job logs, if sauceUser and sauceKey are specified.
*
* @constructor
*/
var TimelinePlugin = function() {
// Timelines are of the format:
// Array<{
// source: string,
// id: number,
// command: string,
// start: number,
// end: number
// }>
this.timeline = [];
this.clientLogAvailable = false;
this.outdir;
this.sessionId;
this.testProcessSetTimeoutTimestamp = 0;
};
/**
* Parse a selenium log in array form. For example, the logs returned
* from the selenium standalone server are returned as arrays.
*
* @param {Array<Object>} logArr The selenium server logs.
* @param {string} sourceName Descripton of source.
* @param {number} referenceStart Date in millis.
*/
TimelinePlugin.parseArrayLog = function(logArr, sourceName, referenceStart) {
return TimelinePlugin.parseLog(logArr, sourceName, {
isEventStart: function(event) {
return /Executing:/.test(event.message);
},
isEventEnd: function(event) {
return /Done:/.test(event.message);
},
extractCommand: function(event) {
// Messages from the Selenium Standalone server are of the form
// org...DriverServlet Executing: [command: details [params]] at URL /url/
return /Executing: \[([^:^\]]*)/.exec(event.message)[1];
},
extractTimestamp: function(event) {
return event.timestamp;
}
}, referenceStart);
};
/**
* Parse a selenium log from a string. For example, the logs returned from
* Sauce Labs are available only as plain text.
*
* @param {string} text The text logs.
* @param {string} sourceName Descripton of source.
* @param {number} referenceStart Date in millis.
*/
TimelinePlugin.parseTextLog = function(text, sourceName, referenceStart) {
var logLines = text.split('\n');
var actions;
// Look for 'standalone server' in the first couple lines of the log.
if (/standalone server/.test(logLines.slice(0, 3).join(' '))) {
// This is a Selenium Standalone Server log.
actions = {
isEventStart: function(event) {
return /INFO - Executing:/.test(event);
},
isEventEnd: function(event) {
return /INFO - Done:/.test(event);
},
extractCommand: function(event) {
// Messages are of the form
// timestamp INFO - Executing: [command: details; [params]]
return /Executing: \[([^:^\]]*)/.exec(event)[1];
},
extractTimestamp: function(event) {
// Timestamps begin the line and are formatted as
// HH:MM:SS.SSS
// We don't care about the date so just set it to 0.
return Date.parse('01 Jan 1970 ' + event.slice(0, 12));
}
};
} else {
// This is a ChromeDriver log.
actions = {
isEventStart: function(event) {
return /: COMMAND/.test(event);
},
isEventEnd: function(event) {
return /: RESPONSE/.test(event);
},
extractCommand: function(event) {
return /: COMMAND ([^\s]*)/.exec(event)[1];
},
extractTimestamp: function(event) {
return parseFloat(/^\[?([^\]]*)/.exec(event)[1]) * 1000;
}
};
}
return TimelinePlugin.parseLog(logLines, sourceName, actions, referenceStart);
};
/**
* Parse a selenium log.
*
* @param {Array<Object>} entries The list of entries.
* @param {string} sourceName Descripton of source.
* @param {isEventStart: function,
isEventEnd: function,
extractCommand: function,
extractTimestamp: function} actions Methods to interpret entries.
* @param {number} referenceStart Date in millis.
*/
TimelinePlugin.parseLog =
function(entries, sourceName, actions, referenceStart) {
var parsedTimeline = [];
var currentEvent = {};
var index = 0;
var relativeStartTime = 0;
for (var j = 0; j < entries.length; ++j) {
var event = entries[j];
if (actions.isEventStart(event)) {
currentEvent = {
source: sourceName,
id: index++,
command: actions.extractCommand(event),
start: actions.extractTimestamp(event)
};
if (!relativeStartTime &&
currentEvent.command.toString() == 'setScriptTimeout' ||
currentEvent.command.toString() == 'set script timeout' ||
// [sic], the timeoutt typo is present in the logs
currentEvent.command.toString() == 'set script timeoutt' ||
currentEvent.command.toString() == 'SetScriptTimeout') {
relativeStartTime = currentEvent.start;
}
} else if (actions.isEventEnd(event)) {
currentEvent.end = actions.extractTimestamp(event);
currentEvent.duration = currentEvent.end - currentEvent.start;
parsedTimeline.push(currentEvent);
}
}
// Make all the times relative to the first time log types is fetched.
for (var k = 0; k < parsedTimeline.length; ++k) {
parsedTimeline[k].start += (referenceStart - relativeStartTime);
parsedTimeline[k].end += (referenceStart - relativeStartTime);
}
return parsedTimeline;
};
TimelinePlugin.prototype.outputResults = function(done) {
try {
fs.mkdirSync(this.outdir);
} catch (e) {
if (e.code != 'EEXIST') throw e;
}
var stream = fs.createReadStream(
path.resolve(__dirname, 'indextemplate.html'));
var outfile = path.resolve(this.outdir, 'timeline.json');
fs.writeFileSync(outfile, JSON.stringify(this.timeline));
stream.pipe(fs.createWriteStream(path.resolve(this.outdir, this.config.outputHtmlFileName || 'index.html')));
stream.on('end', done);
};
/**
* @see docs/plugins.md
*/
TimelinePlugin.prototype.setup = function() {
var self = this;
var deferred = q.defer();
self.outdir = path.resolve(process.cwd(), this.config.outdir);
var counter = 0;
// Override executor so that we get information about commands starting
// and stopping.
var originalExecute = browser.driver.executor_.execute;
browser.driver.executor_.execute = function(command, callback) {
var timelineEvent = {
source: 'Test Process',
id: counter++,
command: command,
start: new Date().getTime(),
end: null
};
if (!self.testProcessSetTimeoutTimestamp &&
timelineEvent.command.name_ == 'setScriptTimeout') {
self.testProcessSetTimeoutTimestamp = timelineEvent.start;
}
self.timeline.push(timelineEvent);
var usesPromises = false; // Unsure if .execute() returns a promise
var wrappedCallback = function(var_args) {
};
var ret = originalExecute.call(browser.driver.executor_, command,
function() {
if (!usesPromises) {
timelineEvent.end = new Date().getTime();
callback.apply(this, arguments);
}
}
);
if (typeof ret.then == 'function') {
usesPromises = true;
ret = ret.then((val) => {
timelineEvent.end = new Date().getTime();
return val;
});
}
return ret;
};
// Clear the logs here.
browser.manage().logs().getAvailableLogTypes().then(function(result) {
// The Selenium standalone server stores its logs in the 'client' channel.
if (result.indexOf('client') !== -1) {
self.clientLogAvailable = true;
deferred.resolve();
// browser.manage().logs().get('client').then(function() {
// deferred.resolve();
// });
} else {
deferred.resolve();
}
}, function(error) {
// No logs are available - this will happen for Internet Explorer, which
// does not implement webdriver logs. See
// https://code.google.com/p/selenium/issues/detail?id=4925
deferred.resolve();
});
return deferred.promise;
};
/**
* @see docs/plugins.md
*/
TimelinePlugin.prototype.teardown = function() {
var self = this;
var deferred = q.defer();
// This will be needed later for grabbing data from Sauce Labs.
browser.getSession().then(function(session) {
self.sessionId = session.getId();
});
// If running with a Selenium Standalone server, get the client logs.
if (self.clientLogAvailable) {
browser.manage().logs().get('client').then(function(result) {
var serverTimeline = TimelinePlugin.parseArrayLog(
result, 'Selenium Client', self.testProcessSetTimeoutTimestamp);
self.timeline = self.timeline.concat(serverTimeline);
deferred.resolve();
});
} else {
deferred.resolve();
}
return deferred.promise;
};
/**
* @see docs/plugins.md
*/
TimelinePlugin.prototype.postResults = function() {
var self = this;
var deferred = q.defer();
// We can't get Chrome or IE logs from Sauce Labs via the webdriver logs API
// because it does not expose them.
// TODO - if the feature request at
// https://support.saucelabs.com/entries/60070884-Enable-grabbing-server-logs-from-the-wire-protocol
// gets implemented, remove this hack.
if (this.config.sauceUser && this.config.sauceKey) {
// WARNING, HACK: we have a timeout to deal with the fact that there's a
// delay before Sauce Labs updates logs.
setTimeout(function() {
var sauceServer = new SauceLabs({
username: self.config.sauceUser,
password: self.config.sauceKey
});
sauceServer.showJob(self.sessionId, function(err, job) {
var sauceLog = '';
if (!job.log_url) {
console.log('WARNING - no Sauce Labs log url found');
deferred.resolve();
return;
}
https.get(job.log_url, function(res) {
res.on('data', function(data) {
sauceLog += data;
});
res.on('end', function() {
var sauceTimeline =
TimelinePlugin.parseTextLog(
sauceLog,
'SauceLabs Server',
self.testProcessSetTimeoutTimestamp);
self.timeline = self.timeline.concat(sauceTimeline);
self.outputResults(deferred.resolve);
});
}).on('error', function(e) {
console.error(e);
});
});
}, SAUCE_LOGS_WAIT);
} else {
self.outputResults(deferred.resolve);
}
return deferred.promise;
};
// Export
module.exports = new TimelinePlugin();
module.exports.TimelinePlugin = TimelinePlugin;