lib/webdriver/stacktrace.js

1// Licensed to the Software Freedom Conservancy (SFC) under one
2// or more contributor license agreements. See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership. The SFC licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License. You may obtain a copy of the License at
8//
9// http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied. See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18/**
19 * @fileoverview Tools for parsing and pretty printing error stack traces. This
20 * file is based on goog.testing.stacktrace.
21 */
22
23goog.provide('webdriver.stacktrace');
24goog.provide('webdriver.stacktrace.Snapshot');
25
26goog.require('goog.array');
27goog.require('goog.string');
28goog.require('goog.userAgent');
29
30
31
32/**
33 * Stores a snapshot of the stack trace at the time this instance was created.
34 * The stack trace will always be adjusted to exclude this function call.
35 * @param {number=} opt_slice The number of frames to remove from the top of
36 * the generated stack trace.
37 * @constructor
38 */
39webdriver.stacktrace.Snapshot = function(opt_slice) {
40
41 /** @private {number} */
42 this.slice_ = opt_slice || 0;
43
44 var error;
45 if (webdriver.stacktrace.CAN_CAPTURE_STACK_TRACE_) {
46 error = Error();
47 Error.captureStackTrace(error, webdriver.stacktrace.Snapshot);
48 } else {
49 // Remove 1 extra frame for the call to this constructor.
50 this.slice_ += 1;
51 // IE will only create a stack trace when the Error is thrown.
52 // We use null.x() to throw an exception instead of throw this.error_
53 // because the closure compiler may optimize throws to a function call
54 // in an attempt to minimize the binary size which in turn has the side
55 // effect of adding an unwanted stack frame.
56 try {
57 null.x();
58 } catch (e) {
59 error = e;
60 }
61 }
62
63 /**
64 * The error's stacktrace.
65 * @private {string}
66 */
67 this.stack_ = webdriver.stacktrace.getStack(error);
68};
69
70
71/**
72 * Whether the current environment supports the Error.captureStackTrace
73 * function (as of 10/17/2012, only V8).
74 * @private {boolean}
75 * @const
76 */
77webdriver.stacktrace.CAN_CAPTURE_STACK_TRACE_ =
78 goog.isFunction(Error.captureStackTrace);
79
80
81/**
82 * Whether the current browser supports stack traces.
83 *
84 * @type {boolean}
85 * @const
86 */
87webdriver.stacktrace.BROWSER_SUPPORTED =
88 webdriver.stacktrace.CAN_CAPTURE_STACK_TRACE_ || (function() {
89 try {
90 throw Error();
91 } catch (e) {
92 return !!e.stack;
93 }
94 })();
95
96
97/**
98 * The parsed stack trace. This list is lazily generated the first time it is
99 * accessed.
100 * @private {Array.<!webdriver.stacktrace.Frame>}
101 */
102webdriver.stacktrace.Snapshot.prototype.parsedStack_ = null;
103
104
105/**
106 * @return {!Array.<!webdriver.stacktrace.Frame>} The parsed stack trace.
107 */
108webdriver.stacktrace.Snapshot.prototype.getStacktrace = function() {
109 if (goog.isNull(this.parsedStack_)) {
110 this.parsedStack_ = webdriver.stacktrace.parse_(this.stack_);
111 if (this.slice_) {
112 this.parsedStack_ = goog.array.slice(this.parsedStack_, this.slice_);
113 }
114 delete this.slice_;
115 delete this.stack_;
116 }
117 return this.parsedStack_;
118};
119
120
121
122/**
123 * Class representing one stack frame.
124 * @param {(string|undefined)} context Context object, empty in case of global
125 * functions or if the browser doesn't provide this information.
126 * @param {(string|undefined)} name Function name, empty in case of anonymous
127 * functions.
128 * @param {(string|undefined)} alias Alias of the function if available. For
129 * example the function name will be 'c' and the alias will be 'b' if the
130 * function is defined as <code>a.b = function c() {};</code>.
131 * @param {(string|undefined)} path File path or URL including line number and
132 * optionally column number separated by colons.
133 * @constructor
134 */
135webdriver.stacktrace.Frame = function(context, name, alias, path) {
136
137 /** @private {string} */
138 this.context_ = context || '';
139
140 /** @private {string} */
141 this.name_ = name || '';
142
143 /** @private {string} */
144 this.alias_ = alias || '';
145
146 /** @private {string} */
147 this.path_ = path || '';
148
149 /** @private {string} */
150 this.url_ = this.path_;
151
152 /** @private {number} */
153 this.line_ = -1;
154
155 /** @private {number} */
156 this.column_ = -1;
157
158 if (path) {
159 var match = /:(\d+)(?::(\d+))?$/.exec(path);
160 if (match) {
161 this.line_ = Number(match[1]);
162 this.column = Number(match[2] || -1);
163 this.url_ = path.substr(0, match.index);
164 }
165 }
166};
167
168
169/**
170 * Constant for an anonymous frame.
171 * @private {!webdriver.stacktrace.Frame}
172 * @const
173 */
174webdriver.stacktrace.ANONYMOUS_FRAME_ =
175 new webdriver.stacktrace.Frame('', '', '', '');
176
177
178/**
179 * @return {string} The function name or empty string if the function is
180 * anonymous and the object field which it's assigned to is unknown.
181 */
182webdriver.stacktrace.Frame.prototype.getName = function() {
183 return this.name_;
184};
185
186
187/**
188 * @return {string} The url or empty string if it is unknown.
189 */
190webdriver.stacktrace.Frame.prototype.getUrl = function() {
191 return this.url_;
192};
193
194
195/**
196 * @return {number} The line number if known or -1 if it is unknown.
197 */
198webdriver.stacktrace.Frame.prototype.getLine = function() {
199 return this.line_;
200};
201
202
203/**
204 * @return {number} The column number if known and -1 if it is unknown.
205 */
206webdriver.stacktrace.Frame.prototype.getColumn = function() {
207 return this.column_;
208};
209
210
211/**
212 * @return {boolean} Whether the stack frame contains an anonymous function.
213 */
214webdriver.stacktrace.Frame.prototype.isAnonymous = function() {
215 return !this.name_ || this.context_ == '[object Object]';
216};
217
218
219/**
220 * Converts this frame to its string representation using V8's stack trace
221 * format: http://code.google.com/p/v8/wiki/JavaScriptStackTraceApi
222 * @return {string} The string representation of this frame.
223 * @override
224 */
225webdriver.stacktrace.Frame.prototype.toString = function() {
226 var context = this.context_;
227 if (context && context !== 'new ') {
228 context += '.';
229 }
230 context += this.name_;
231 context += this.alias_ ? ' [as ' + this.alias_ + ']' : '';
232
233 var path = this.path_ || '<anonymous>';
234 return ' at ' + (context ? context + ' (' + path + ')' : path);
235};
236
237
238/**
239 * Maximum length of a string that can be matched with a RegExp on
240 * Firefox 3x. Exceeding this approximate length will cause string.match
241 * to exceed Firefox's stack quota. This situation can be encountered
242 * when goog.globalEval is invoked with a long argument; such as
243 * when loading a module.
244 * @private {number}
245 * @const
246 */
247webdriver.stacktrace.MAX_FIREFOX_FRAMESTRING_LENGTH_ = 500000;
248
249
250/**
251 * RegExp pattern for JavaScript identifiers. We don't support Unicode
252 * identifiers defined in ECMAScript v3.
253 * @private {string}
254 * @const
255 */
256webdriver.stacktrace.IDENTIFIER_PATTERN_ = '[a-zA-Z_$][\\w$]*';
257
258
259/**
260 * Pattern for a matching the type on a fully-qualified name. Forms an
261 * optional sub-match on the type. For example, in "foo.bar.baz", will match on
262 * "foo.bar".
263 * @private {string}
264 * @const
265 */
266webdriver.stacktrace.CONTEXT_PATTERN_ =
267 '(' + webdriver.stacktrace.IDENTIFIER_PATTERN_ +
268 '(?:\\.' + webdriver.stacktrace.IDENTIFIER_PATTERN_ + ')*)\\.';
269
270
271/**
272 * Pattern for matching a fully qualified name. Will create two sub-matches:
273 * the type (optional), and the name. For example, in "foo.bar.baz", will
274 * match on ["foo.bar", "baz"].
275 * @private {string}
276 * @const
277 */
278webdriver.stacktrace.QUALIFIED_NAME_PATTERN_ =
279 '(?:' + webdriver.stacktrace.CONTEXT_PATTERN_ + ')?' +
280 '(' + webdriver.stacktrace.IDENTIFIER_PATTERN_ + ')';
281
282
283/**
284 * RegExp pattern for function name alias in the V8 stack trace.
285 * @private {string}
286 * @const
287 */
288webdriver.stacktrace.V8_ALIAS_PATTERN_ =
289 '(?: \\[as (' + webdriver.stacktrace.IDENTIFIER_PATTERN_ + ')\\])?';
290
291
292/**
293 * RegExp pattern for function names and constructor calls in the V8 stack
294 * trace.
295 * @private {string}
296 * @const
297 */
298webdriver.stacktrace.V8_FUNCTION_NAME_PATTERN_ =
299 '(?:' + webdriver.stacktrace.IDENTIFIER_PATTERN_ + '|<anonymous>)';
300
301
302/**
303 * RegExp pattern for the context of a function call in V8. Creates two
304 * submatches, only one of which will ever match: either the namespace
305 * identifier (with optional "new" keyword in the case of a constructor call),
306 * or just the "new " phrase for a top level constructor call.
307 * @private {string}
308 * @const
309 */
310webdriver.stacktrace.V8_CONTEXT_PATTERN_ =
311 '(?:((?:new )?(?:\\[object Object\\]|' +
312 webdriver.stacktrace.IDENTIFIER_PATTERN_ +
313 '(?:\\.' + webdriver.stacktrace.IDENTIFIER_PATTERN_ + ')*)' +
314 ')\\.|(new ))';
315
316
317/**
318 * RegExp pattern for function call in the V8 stack trace.
319 * Creates 3 submatches with context object (optional), function name and
320 * function alias (optional).
321 * @private {string}
322 * @const
323 */
324webdriver.stacktrace.V8_FUNCTION_CALL_PATTERN_ =
325 ' (?:' + webdriver.stacktrace.V8_CONTEXT_PATTERN_ + ')?' +
326 '(' + webdriver.stacktrace.V8_FUNCTION_NAME_PATTERN_ + ')' +
327 webdriver.stacktrace.V8_ALIAS_PATTERN_;
328
329
330/**
331 * RegExp pattern for an URL + position inside the file.
332 * @private {string}
333 * @const
334 */
335webdriver.stacktrace.URL_PATTERN_ =
336 '((?:http|https|file)://[^\\s]+|javascript:.*)';
337
338
339/**
340 * RegExp pattern for a location string in a V8 stack frame. Creates two
341 * submatches for the location, one for enclosed in parentheticals and on
342 * where the location appears alone (which will only occur if the location is
343 * the only information in the frame).
344 * @private {string}
345 * @const
346 * @see http://code.google.com/p/v8/wiki/JavaScriptStackTraceApi
347 */
348webdriver.stacktrace.V8_LOCATION_PATTERN_ = ' (?:\\((.*)\\)|(.*))';
349
350
351/**
352 * Regular expression for parsing one stack frame in V8.
353 * @private {!RegExp}
354 * @const
355 */
356webdriver.stacktrace.V8_STACK_FRAME_REGEXP_ = new RegExp('^\\s+at' +
357 // Prevent intersections with IE10 stack frame regex.
358 '(?! (?:Anonymous function|Global code|eval code) )' +
359 '(?:' + webdriver.stacktrace.V8_FUNCTION_CALL_PATTERN_ + ')?' +
360 webdriver.stacktrace.V8_LOCATION_PATTERN_ + '$');
361
362
363/**
364 * RegExp pattern for function names in the Firefox stack trace.
365 * Firefox has extended identifiers to deal with inner functions and anonymous
366 * functions: https://bugzilla.mozilla.org/show_bug.cgi?id=433529#c9
367 * @private {string}
368 * @const
369 */
370webdriver.stacktrace.FIREFOX_FUNCTION_NAME_PATTERN_ =
371 webdriver.stacktrace.IDENTIFIER_PATTERN_ + '[\\w./<$]*';
372
373
374/**
375 * RegExp pattern for function call in the Firefox stack trace.
376 * Creates a submatch for the function name.
377 * @private {string}
378 * @const
379 */
380webdriver.stacktrace.FIREFOX_FUNCTION_CALL_PATTERN_ =
381 '(' + webdriver.stacktrace.FIREFOX_FUNCTION_NAME_PATTERN_ + ')?' +
382 '(?:\\(.*\\))?@';
383
384
385/**
386 * Regular expression for parsing one stack frame in Firefox.
387 * @private {!RegExp}
388 * @const
389 */
390webdriver.stacktrace.FIREFOX_STACK_FRAME_REGEXP_ = new RegExp('^' +
391 webdriver.stacktrace.FIREFOX_FUNCTION_CALL_PATTERN_ +
392 '(?::0|' + webdriver.stacktrace.URL_PATTERN_ + ')$');
393
394
395/**
396 * RegExp pattern for function call in a Chakra (IE) stack trace. This
397 * expression creates 2 submatches on the (optional) context and function name,
398 * matching identifiers like 'foo.Bar.prototype.baz', 'Anonymous function',
399 * 'eval code', and 'Global code'.
400 * @private {string}
401 * @const
402 */
403webdriver.stacktrace.CHAKRA_FUNCTION_CALL_PATTERN_ =
404 '(?:(' + webdriver.stacktrace.IDENTIFIER_PATTERN_ +
405 '(?:\\.' + webdriver.stacktrace.IDENTIFIER_PATTERN_ + ')*)\\.)?' +
406 '(' + webdriver.stacktrace.IDENTIFIER_PATTERN_ + '(?:\\s+\\w+)*)';
407
408
409/**
410 * Regular expression for parsing on stack frame in Chakra (IE).
411 * @private {!RegExp}
412 * @const
413 */
414webdriver.stacktrace.CHAKRA_STACK_FRAME_REGEXP_ = new RegExp('^ at ' +
415 webdriver.stacktrace.CHAKRA_FUNCTION_CALL_PATTERN_ +
416 '\\s*(?:\\((.*)\\))$');
417
418
419/**
420 * Placeholder for an unparsable frame in a stack trace generated by
421 * {@link goog.testing.stacktrace}.
422 * @private {string}
423 * @const
424 */
425webdriver.stacktrace.UNKNOWN_CLOSURE_FRAME_ = '> (unknown)';
426
427
428/**
429 * Representation of an anonymous frame in a stack trace generated by
430 * {@link goog.testing.stacktrace}.
431 * @private {string}
432 * @const
433 */
434webdriver.stacktrace.ANONYMOUS_CLOSURE_FRAME_ = '> anonymous';
435
436
437/**
438 * Pattern for a function call in a Closure stack trace. Creates three optional
439 * submatches: the context, function name, and alias.
440 * @private {string}
441 * @const
442 */
443webdriver.stacktrace.CLOSURE_FUNCTION_CALL_PATTERN_ =
444 webdriver.stacktrace.QUALIFIED_NAME_PATTERN_ +
445 '(?:\\(.*\\))?' + // Ignore arguments if present.
446 webdriver.stacktrace.V8_ALIAS_PATTERN_;
447
448
449/**
450 * Regular expression for parsing a stack frame generated by Closure's
451 * {@link goog.testing.stacktrace}.
452 * @private {!RegExp}
453 * @const
454 */
455webdriver.stacktrace.CLOSURE_STACK_FRAME_REGEXP_ = new RegExp('^> ' +
456 '(?:' + webdriver.stacktrace.CLOSURE_FUNCTION_CALL_PATTERN_ +
457 '(?: at )?)?' +
458 '(?:(.*:\\d+:\\d+)|' + webdriver.stacktrace.URL_PATTERN_ + ')?$');
459
460
461/**
462 * Parses one stack frame.
463 * @param {string} frameStr The stack frame as string.
464 * @return {webdriver.stacktrace.Frame} Stack frame object or null if the
465 * parsing failed.
466 * @private
467 */
468webdriver.stacktrace.parseStackFrame_ = function(frameStr) {
469 var m = frameStr.match(webdriver.stacktrace.V8_STACK_FRAME_REGEXP_);
470 if (m) {
471 return new webdriver.stacktrace.Frame(
472 m[1] || m[2], m[3], m[4], m[5] || m[6]);
473 }
474
475 if (frameStr.length >
476 webdriver.stacktrace.MAX_FIREFOX_FRAMESTRING_LENGTH_) {
477 return webdriver.stacktrace.parseLongFirefoxFrame_(frameStr);
478 }
479
480 m = frameStr.match(webdriver.stacktrace.FIREFOX_STACK_FRAME_REGEXP_);
481 if (m) {
482 return new webdriver.stacktrace.Frame('', m[1], '', m[2]);
483 }
484
485 m = frameStr.match(webdriver.stacktrace.CHAKRA_STACK_FRAME_REGEXP_);
486 if (m) {
487 return new webdriver.stacktrace.Frame(m[1], m[2], '', m[3]);
488 }
489
490 if (frameStr == webdriver.stacktrace.UNKNOWN_CLOSURE_FRAME_ ||
491 frameStr == webdriver.stacktrace.ANONYMOUS_CLOSURE_FRAME_) {
492 return webdriver.stacktrace.ANONYMOUS_FRAME_;
493 }
494
495 m = frameStr.match(webdriver.stacktrace.CLOSURE_STACK_FRAME_REGEXP_);
496 if (m) {
497 return new webdriver.stacktrace.Frame(m[1], m[2], m[3], m[4] || m[5]);
498 }
499
500 return null;
501};
502
503
504/**
505 * Parses a long firefox stack frame.
506 * @param {string} frameStr The stack frame as string.
507 * @return {!webdriver.stacktrace.Frame} Stack frame object.
508 * @private
509 */
510webdriver.stacktrace.parseLongFirefoxFrame_ = function(frameStr) {
511 var firstParen = frameStr.indexOf('(');
512 var lastAmpersand = frameStr.lastIndexOf('@');
513 var lastColon = frameStr.lastIndexOf(':');
514 var functionName = '';
515 if ((firstParen >= 0) && (firstParen < lastAmpersand)) {
516 functionName = frameStr.substring(0, firstParen);
517 }
518 var loc = '';
519 if ((lastAmpersand >= 0) && (lastAmpersand + 1 < lastColon)) {
520 loc = frameStr.substring(lastAmpersand + 1);
521 }
522 return new webdriver.stacktrace.Frame('', functionName, '', loc);
523};
524
525
526/**
527 * Get an error's stack trace with the error string trimmed.
528 * V8 prepends the string representation of an error to its stack trace.
529 * This function trims the string so that the stack trace can be parsed
530 * consistently with the other JS engines.
531 * @param {(Error|goog.testing.JsUnitException)} error The error.
532 * @return {string} The stack trace string.
533 */
534webdriver.stacktrace.getStack = function(error) {
535 if (!error) {
536 return '';
537 }
538 var stack = error.stack || error.stackTrace || '';
539 var errorStr = error + '\n';
540 if (goog.string.startsWith(stack, errorStr)) {
541 stack = stack.substring(errorStr.length);
542 }
543 return stack;
544};
545
546
547/**
548 * Formats an error's stack trace.
549 * @param {!(Error|goog.testing.JsUnitException)} error The error to format.
550 * @return {!(Error|goog.testing.JsUnitException)} The formatted error.
551 */
552webdriver.stacktrace.format = function(error) {
553 var stack = webdriver.stacktrace.getStack(error);
554 var frames = webdriver.stacktrace.parse_(stack);
555
556 // If the original stack is in an unexpected format, our formatted stack
557 // trace will be a bunch of " at <anonymous>" lines. If this is the case,
558 // just return the error unmodified to avoid losing information. This is
559 // necessary since the user may have defined a custom stack formatter in
560 // V8 via Error.prepareStackTrace. See issue 7994.
561 var isAnonymousFrame = function(frame) {
562 return frame.toString() === ' at <anonymous>';
563 };
564 if (frames.length && goog.array.every(frames, isAnonymousFrame)) {
565 return error;
566 }
567
568 // Older versions of IE simply return [object Error] for toString(), so
569 // only use that as a last resort.
570 var errorStr = '';
571 if (error.message) {
572 errorStr = (error.name ? error.name + ': ' : '') + error.message;
573 } else {
574 errorStr = error.toString();
575 }
576
577 // Ensure the error is in the V8 style with the error's string representation
578 // prepended to the stack.
579 error.stack = errorStr + '\n' + frames.join('\n');
580 return error;
581};
582
583
584/**
585 * Parses an Error object's stack trace.
586 * @param {string} stack The stack trace.
587 * @return {!Array.<!webdriver.stacktrace.Frame>} Stack frames. The
588 * unrecognized frames will be nulled out.
589 * @private
590 */
591webdriver.stacktrace.parse_ = function(stack) {
592 if (!stack) {
593 return [];
594 }
595
596 var lines = stack.
597 replace(/\s*$/, '').
598 split('\n');
599 var frames = [];
600 for (var i = 0; i < lines.length; i++) {
601 var frame = webdriver.stacktrace.parseStackFrame_(lines[i]);
602 // The first two frames will be:
603 // webdriver.stacktrace.Snapshot()
604 // webdriver.stacktrace.get()
605 frames.push(frame || webdriver.stacktrace.ANONYMOUS_FRAME_);
606 }
607 return frames;
608};
609
610
611/**
612 * Gets the native stack trace if available otherwise follows the call chain.
613 * The generated trace will exclude all frames up to and including the call to
614 * this function.
615 * @return {!Array.<!webdriver.stacktrace.Frame>} The frames of the stack trace.
616 */
617webdriver.stacktrace.get = function() {
618 return new webdriver.stacktrace.Snapshot(1).getStacktrace();
619};