Skip to content
This repository was archived by the owner on Feb 26, 2024. It is now read-only.

Commit 211997d

Browse files
committed
Split off from main protractor project
0 parents  commit 211997d

32 files changed

+2047
-0
lines changed

.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
node_modules/
2+
npm-debug.log

.npmignore

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
testapp/
2+
spec/
3+
test.js
4+
test_util.js
5+
.npmignore
6+
npm-debug.log

LICENSE

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
The MIT License
2+
3+
Copyright (c) 2010-2015 Google, Inc.
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in
13+
all copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21+
THE SOFTWARE.
22+

README.md

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
Timeline Plugin
2+
===============
3+
4+
This plugin gathers test timeline information from the protractor test process, the selenium
5+
client logs (if available), and sauce labs (if available), and presents the output visually.
6+
This improves understanding of where latency issues are in tests.
7+
8+
To enable the Timeline plugin, set it up in your config file:
9+
```js
10+
exports.config = {
11+
plugins: [{
12+
path: 'node_modules/protractor/plugins/timeline/index.js',
13+
14+
// Output json and html will go in this folder.
15+
outdir: 'timelines',
16+
17+
// Optional - if sauceUser and sauceKey are specified, logs from
18+
// SauceLabs will also be parsed after test invocation.
19+
sauceUser: 'Jane',
20+
sauceKey: 'abcdefg'
21+
}],
22+
// other configuration settings
23+
};
24+
```

index.js

+340
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,340 @@
1+
var q = require('q'),
2+
fs = require('fs'),
3+
path = require('path'),
4+
SauceLabs = require('saucelabs'),
5+
https = require('https');
6+
7+
var SAUCE_LOGS_WAIT = 5000;
8+
9+
/**
10+
* Outputs information about where your Protractor test is spending its time
11+
* to the specified folder. A JSON data file and small index.html to view
12+
* it will be created. The page uses Google Charts to show the timeline.
13+
*
14+
* You enable this plugin in your config file:
15+
*
16+
* exports.config = {
17+
* plugins: [{
18+
* path: 'node_modules/protractor/plugins/timeline',
19+
*
20+
* // Output json and html will go in this folder. Relative
21+
* // to current working directory of the process.
22+
* // TODO - it would make more sense for this to be relative
23+
* // to the config file - reconsider this setup
24+
* outdir: 'timelines',
25+
*
26+
* // Optional - if sauceUser and sauceKey are specified, logs from
27+
* // SauceLabs will also be parsed after test invocation.
28+
* sauceUser: 'Jane',
29+
* sauceKey: 'abcdefg'
30+
* }]
31+
* };
32+
*
33+
* The plugin will create timeline entries from
34+
* - The Protractor test process itself.
35+
* - The WebDriver Selenium Server (these logs are unavailable for Internet
36+
* Explorer and for Chrome test run over Sauce Labs).
37+
* - Sauce Labs job logs, if sauceUser and sauceKey are specified.
38+
*
39+
* @constructor
40+
*/
41+
var TimelinePlugin = function() {
42+
// Timelines are of the format:
43+
// Array<{
44+
// source: string,
45+
// id: number,
46+
// command: string,
47+
// start: number,
48+
// end: number
49+
// }>
50+
this.timeline = [];
51+
52+
this.clientLogAvailable = false;
53+
this.outdir;
54+
this.sessionId;
55+
this.testProcessSetTimeoutTimestamp = 0;
56+
};
57+
58+
/**
59+
* Parse a selenium log in array form. For example, the logs returned
60+
* from the selenium standalone server are returned as arrays.
61+
*
62+
* @param {Array<Object>} logArr The selenium server logs.
63+
* @param {string} sourceName Descripton of source.
64+
* @param {number} referenceStart Date in millis.
65+
*/
66+
TimelinePlugin.parseArrayLog = function(logArr, sourceName, referenceStart) {
67+
return TimelinePlugin.parseLog(logArr, sourceName, {
68+
isEventStart: function(event) {
69+
return /Executing:/.test(event.message);
70+
},
71+
isEventEnd: function(event) {
72+
return /Done:/.test(event.message);
73+
},
74+
extractCommand: function(event) {
75+
// Messages from the Selenium Standalone server are of the form
76+
// org...DriverServlet Executing: [command: details [params]] at URL /url/
77+
return /Executing: \[([^:^\]]*)/.exec(event.message)[1];
78+
},
79+
extractTimestamp: function(event) {
80+
return event.timestamp;
81+
}
82+
}, referenceStart);
83+
};
84+
85+
/**
86+
* Parse a selenium log from a string. For example, the logs returned from
87+
* Sauce Labs are available only as plain text.
88+
*
89+
* @param {string} text The text logs.
90+
* @param {string} sourceName Descripton of source.
91+
* @param {number} referenceStart Date in millis.
92+
*/
93+
TimelinePlugin.parseTextLog = function(text, sourceName, referenceStart) {
94+
var logLines = text.split('\n');
95+
var actions;
96+
97+
// Look for 'standalone server' in the first couple lines of the log.
98+
if (/standalone server/.test(logLines.slice(0, 3).join(' '))) {
99+
// This is a Selenium Standalone Server log.
100+
actions = {
101+
isEventStart: function(event) {
102+
return /INFO - Executing:/.test(event);
103+
},
104+
isEventEnd: function(event) {
105+
return /INFO - Done:/.test(event);
106+
},
107+
extractCommand: function(event) {
108+
// Messages are of the form
109+
// timestamp INFO - Executing: [command: details; [params]]
110+
return /Executing: \[([^:^\]]*)/.exec(event)[1];
111+
},
112+
extractTimestamp: function(event) {
113+
// Timestamps begin the line and are formatted as
114+
// HH:MM:SS.SSS
115+
// We don't care about the date so just set it to 0.
116+
return Date.parse('01 Jan 1970 ' + event.slice(0, 12));
117+
}
118+
};
119+
} else {
120+
// This is a ChromeDriver log.
121+
actions = {
122+
isEventStart: function(event) {
123+
return /: COMMAND/.test(event);
124+
},
125+
isEventEnd: function(event) {
126+
return /: RESPONSE/.test(event);
127+
},
128+
extractCommand: function(event) {
129+
return /: COMMAND ([^\s]*)/.exec(event)[1];
130+
},
131+
extractTimestamp: function(event) {
132+
return parseFloat(/^\[?([^\]]*)/.exec(event)[1]) * 1000;
133+
}
134+
};
135+
}
136+
137+
return TimelinePlugin.parseLog(logLines, sourceName, actions, referenceStart);
138+
};
139+
140+
141+
/**
142+
* Parse a selenium log.
143+
*
144+
* @param {Array<Object>} entries The list of entries.
145+
* @param {string} sourceName Descripton of source.
146+
* @param {isEventStart: function,
147+
isEventEnd: function,
148+
extractCommand: function,
149+
extractTimestamp: function} actions Methods to interpret entries.
150+
* @param {number} referenceStart Date in millis.
151+
*/
152+
TimelinePlugin.parseLog =
153+
function(entries, sourceName, actions, referenceStart) {
154+
var parsedTimeline = [];
155+
var currentEvent = {};
156+
var index = 0;
157+
var relativeStartTime = 0;
158+
for (var j = 0; j < entries.length; ++j) {
159+
var event = entries[j];
160+
if (actions.isEventStart(event)) {
161+
currentEvent = {
162+
source: sourceName,
163+
id: index++,
164+
command: actions.extractCommand(event),
165+
start: actions.extractTimestamp(event)
166+
};
167+
if (!relativeStartTime &&
168+
currentEvent.command.toString() == 'setScriptTimeout' ||
169+
currentEvent.command.toString() == 'set script timeout' ||
170+
// [sic], the timeoutt typo is present in the logs
171+
currentEvent.command.toString() == 'set script timeoutt' ||
172+
currentEvent.command.toString() == 'SetScriptTimeout') {
173+
relativeStartTime = currentEvent.start;
174+
}
175+
} else if (actions.isEventEnd(event)) {
176+
currentEvent.end = actions.extractTimestamp(event);
177+
currentEvent.duration = currentEvent.end - currentEvent.start;
178+
parsedTimeline.push(currentEvent);
179+
}
180+
}
181+
182+
// Make all the times relative to the first time log types is fetched.
183+
for (var k = 0; k < parsedTimeline.length; ++k) {
184+
parsedTimeline[k].start += (referenceStart - relativeStartTime);
185+
parsedTimeline[k].end += (referenceStart - relativeStartTime);
186+
}
187+
188+
return parsedTimeline;
189+
};
190+
191+
TimelinePlugin.prototype.outputResults = function(done) {
192+
try {
193+
fs.mkdirSync(this.outdir);
194+
} catch (e) {
195+
if (e.code != 'EEXIST') throw e;
196+
}
197+
var stream = fs.createReadStream(
198+
path.join(__dirname, 'indextemplate.html'));
199+
var outfile = path.join(this.outdir, 'timeline.json');
200+
fs.writeFileSync(outfile, JSON.stringify(this.timeline));
201+
stream.pipe(fs.createWriteStream(path.join(this.outdir, 'index.html')));
202+
stream.on('end', done);
203+
};
204+
205+
/**
206+
* @see docs/plugins.md
207+
*/
208+
TimelinePlugin.prototype.setup = function() {
209+
var self = this;
210+
var deferred = q.defer();
211+
self.outdir = path.resolve(process.cwd(), this.config.outdir);
212+
var counter = 0;
213+
214+
// Override executor so that we get information about commands starting
215+
// and stopping.
216+
var originalExecute = browser.driver.executor_.execute;
217+
browser.driver.executor_.execute = function(command, callback) {
218+
var timelineEvent = {
219+
source: 'Test Process',
220+
id: counter++,
221+
command: command,
222+
start: new Date().getTime(),
223+
end: null
224+
};
225+
if (!self.testProcessSetTimeoutTimestamp &&
226+
timelineEvent.command.name_ == 'setScriptTimeout') {
227+
self.testProcessSetTimeoutTimestamp = timelineEvent.start;
228+
}
229+
self.timeline.push(timelineEvent);
230+
var wrappedCallback = function(var_args) {
231+
timelineEvent.end = new Date().getTime();
232+
callback.apply(this, arguments);
233+
};
234+
originalExecute.apply(browser.driver.executor_, [command, wrappedCallback]);
235+
};
236+
237+
// Clear the logs here.
238+
browser.manage().logs().getAvailableLogTypes().then(function(result) {
239+
// The Selenium standalone server stores its logs in the 'client' channel.
240+
if (result.indexOf('client') !== -1) {
241+
self.clientLogAvailable = true;
242+
deferred.resolve();
243+
// browser.manage().logs().get('client').then(function() {
244+
// deferred.resolve();
245+
// });
246+
} else {
247+
deferred.resolve();
248+
}
249+
}, function(error) {
250+
// No logs are available - this will happen for Internet Explorer, which
251+
// does not implement webdriver logs. See
252+
// https://code.google.com/p/selenium/issues/detail?id=4925
253+
deferred.resolve();
254+
});
255+
return deferred.promise;
256+
};
257+
258+
/**
259+
* @see docs/plugins.md
260+
*/
261+
TimelinePlugin.prototype.teardown = function() {
262+
var self = this;
263+
var deferred = q.defer();
264+
// This will be needed later for grabbing data from Sauce Labs.
265+
browser.getSession().then(function(session) {
266+
self.sessionId = session.getId();
267+
});
268+
269+
// If running with a Selenium Standalone server, get the client logs.
270+
if (self.clientLogAvailable) {
271+
browser.manage().logs().get('client').then(function(result) {
272+
var serverTimeline = TimelinePlugin.parseArrayLog(
273+
result, 'Selenium Client', self.testProcessSetTimeoutTimestamp);
274+
self.timeline = self.timeline.concat(serverTimeline);
275+
deferred.resolve();
276+
});
277+
} else {
278+
deferred.resolve();
279+
}
280+
return deferred.promise;
281+
};
282+
283+
/**
284+
* @see docs/plugins.md
285+
*/
286+
TimelinePlugin.prototype.postResults = function() {
287+
var self = this;
288+
var deferred = q.defer();
289+
// We can't get Chrome or IE logs from Sauce Labs via the webdriver logs API
290+
// because it does not expose them.
291+
// TODO - if the feature request at
292+
// https://support.saucelabs.com/entries/60070884-Enable-grabbing-server-logs-from-the-wire-protocol
293+
// gets implemented, remove this hack.
294+
if (this.config.sauceUser && this.config.sauceKey) {
295+
// WARNING, HACK: we have a timeout to deal with the fact that there's a
296+
// delay before Sauce Labs updates logs.
297+
setTimeout(function() {
298+
var sauceServer = new SauceLabs({
299+
username: this.config.sauceUser,
300+
password: this.config.sauceKey
301+
});
302+
303+
sauceServer.showJob(self.sessionId, function(err, job) {
304+
var sauceLog = '';
305+
if (!job.log_url) {
306+
console.log('WARNING - no Sauce Labs log url found');
307+
deferred.resolve();
308+
return;
309+
}
310+
https.get(job.log_url, function(res) {
311+
res.on('data', function(data) {
312+
sauceLog += data;
313+
});
314+
315+
res.on('end', function() {
316+
var sauceTimeline =
317+
TimelinePlugin.parseTextLog(
318+
sauceLog,
319+
'SauceLabs Server',
320+
self.testProcessSetTimeoutTimestamp);
321+
self.timeline = self.timeline.concat(sauceTimeline);
322+
self.outputResults(deferred.resolve);
323+
});
324+
325+
}).on('error', function(e) {
326+
console.error(e);
327+
});
328+
});
329+
}, SAUCE_LOGS_WAIT);
330+
} else {
331+
self.outputResults(deferred.resolve);
332+
}
333+
return deferred.promise;
334+
};
335+
336+
337+
// Export
338+
339+
module.exports = new TimelinePlugin();
340+
module.exports.TimelinePlugin = TimelinePlugin;

0 commit comments

Comments
 (0)