lib/webdriver/stacktrace.js

1// Copyright 2009 The Closure Library Authors. All Rights Reserved.
2// Copyright 2012 Selenium comitters
3// Copyright 2012 Software Freedom Conservancy
4//
5// Licensed under the Apache License, Version 2.0 (the "License");
6// you may not use this file except in compliance with the License.
7// 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, software
12// distributed under the License is distributed on an "AS-IS" BASIS,
13// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14// See the License for the specific language governing permissions and
15// limitations under the License.
16
17/**
18 * @fileoverview Tools for parsing and pretty printing error stack traces. This
19 * file is based on goog.testing.stacktrace.
20 */
21
22goog.provide('webdriver.stacktrace');
23goog.provide('webdriver.stacktrace.Snapshot');
24
25goog.require('goog.array');
26goog.require('goog.string');
27goog.require('goog.userAgent');
28
29
30
31/**
32 * Stores a snapshot of the stack trace at the time this instance was created.
33 * The stack trace will always be adjusted to exclude this function call.
34 * @param {number=} opt_slice The number of frames to remove from the top of
35 * the generated stack trace.
36 * @constructor
37 */
38webdriver.stacktrace.Snapshot = function(opt_slice) {
39
40 /** @private {number} */
41 this.slice_ = opt_slice || 0;
42
43 var error;
44 if (webdriver.stacktrace.CAN_CAPTURE_STACK_TRACE_) {
45 error = Error();
46 Error.captureStackTrace(error, webdriver.stacktrace.Snapshot);
47 } else {
48 // Remove 1 extra frame for the call to this constructor.
49 this.slice_ += 1;
50 // IE will only create a stack trace when the Error is thrown.
51 // We use null.x() to throw an exception instead of throw this.error_
52 // because the closure compiler may optimize throws to a function call
53 // in an attempt to minimize the binary size which in turn has the side
54 // effect of adding an unwanted stack frame.
55 try {
56 null.x();
57 } catch (e) {
58 error = e;
59 }
60 }
61
62 /**
63 * The error's stacktrace. This must be accessed immediately to ensure Opera
64 * computes the context correctly.
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('^ at' +
357 '(?:' + webdriver.stacktrace.V8_FUNCTION_CALL_PATTERN_ + ')?' +
358 webdriver.stacktrace.V8_LOCATION_PATTERN_ + '$');
359
360
361/**
362 * RegExp pattern for function names in the Firefox stack trace.
363 * Firefox has extended identifiers to deal with inner functions and anonymous
364 * functions: https://bugzilla.mozilla.org/show_bug.cgi?id=433529#c9
365 * @private {string}
366 * @const
367 */
368webdriver.stacktrace.FIREFOX_FUNCTION_NAME_PATTERN_ =
369 webdriver.stacktrace.IDENTIFIER_PATTERN_ + '[\\w./<$]*';
370
371
372/**
373 * RegExp pattern for function call in the Firefox stack trace.
374 * Creates a submatch for the function name.
375 * @private {string}
376 * @const
377 */
378webdriver.stacktrace.FIREFOX_FUNCTION_CALL_PATTERN_ =
379 '(' + webdriver.stacktrace.FIREFOX_FUNCTION_NAME_PATTERN_ + ')?' +
380 '(?:\\(.*\\))?@';
381
382
383/**
384 * Regular expression for parsing one stack frame in Firefox.
385 * @private {!RegExp}
386 * @const
387 */
388webdriver.stacktrace.FIREFOX_STACK_FRAME_REGEXP_ = new RegExp('^' +
389 webdriver.stacktrace.FIREFOX_FUNCTION_CALL_PATTERN_ +
390 '(?::0|' + webdriver.stacktrace.URL_PATTERN_ + ')$');
391
392
393/**
394 * RegExp pattern for an anonymous function call in an Opera stack frame.
395 * Creates 2 (optional) submatches: the context object and function name.
396 * @private {string}
397 * @const
398 */
399webdriver.stacktrace.OPERA_ANONYMOUS_FUNCTION_NAME_PATTERN_ =
400 '<anonymous function(?:\\: ' +
401 webdriver.stacktrace.QUALIFIED_NAME_PATTERN_ + ')?>';
402
403
404/**
405 * RegExp pattern for a function call in an Opera stack frame.
406 * Creates 3 (optional) submatches: the function name (if not anonymous),
407 * the aliased context object and the function name (if anonymous).
408 * @private {string}
409 * @const
410 */
411webdriver.stacktrace.OPERA_FUNCTION_CALL_PATTERN_ =
412 '(?:(?:(' + webdriver.stacktrace.IDENTIFIER_PATTERN_ + ')|' +
413 webdriver.stacktrace.OPERA_ANONYMOUS_FUNCTION_NAME_PATTERN_ +
414 ')(?:\\(.*\\)))?@';
415
416
417/**
418 * Regular expression for parsing on stack frame in Opera 11.68+
419 * @private {!RegExp}
420 * @const
421 */
422webdriver.stacktrace.OPERA_STACK_FRAME_REGEXP_ = new RegExp('^' +
423 webdriver.stacktrace.OPERA_FUNCTION_CALL_PATTERN_ +
424 webdriver.stacktrace.URL_PATTERN_ + '?$');
425
426
427/**
428 * RegExp pattern for function call in a Chakra (IE) stack trace. This
429 * expression allows for identifiers like 'Anonymous function', 'eval code',
430 * and 'Global code'.
431 * @private {string}
432 * @const
433 */
434webdriver.stacktrace.CHAKRA_FUNCTION_CALL_PATTERN_ = '(' +
435 webdriver.stacktrace.IDENTIFIER_PATTERN_ + '(?:\\s+\\w+)*)';
436
437
438/**
439 * Regular expression for parsing on stack frame in Chakra (IE).
440 * @private {!RegExp}
441 * @const
442 */
443webdriver.stacktrace.CHAKRA_STACK_FRAME_REGEXP_ = new RegExp('^ at ' +
444 webdriver.stacktrace.CHAKRA_FUNCTION_CALL_PATTERN_ +
445 '\\s*(?:\\((.*)\\))$');
446
447
448/**
449 * Placeholder for an unparsable frame in a stack trace generated by
450 * {@link goog.testing.stacktrace}.
451 * @private {string}
452 * @const
453 */
454webdriver.stacktrace.UNKNOWN_CLOSURE_FRAME_ = '> (unknown)';
455
456
457/**
458 * Representation of an anonymous frame in a stack trace generated by
459 * {@link goog.testing.stacktrace}.
460 * @private {string}
461 * @const
462 */
463webdriver.stacktrace.ANONYMOUS_CLOSURE_FRAME_ = '> anonymous';
464
465
466/**
467 * Pattern for a function call in a Closure stack trace. Creates three optional
468 * submatches: the context, function name, and alias.
469 * @private {string}
470 * @const
471 */
472webdriver.stacktrace.CLOSURE_FUNCTION_CALL_PATTERN_ =
473 webdriver.stacktrace.QUALIFIED_NAME_PATTERN_ +
474 '(?:\\(.*\\))?' + // Ignore arguments if present.
475 webdriver.stacktrace.V8_ALIAS_PATTERN_;
476
477
478/**
479 * Regular expression for parsing a stack frame generated by Closure's
480 * {@link goog.testing.stacktrace}.
481 * @private {!RegExp}
482 * @const
483 */
484webdriver.stacktrace.CLOSURE_STACK_FRAME_REGEXP_ = new RegExp('^> ' +
485 '(?:' + webdriver.stacktrace.CLOSURE_FUNCTION_CALL_PATTERN_ +
486 '(?: at )?)?' +
487 '(?:(.*:\\d+:\\d+)|' + webdriver.stacktrace.URL_PATTERN_ + ')?$');
488
489
490/**
491 * Parses one stack frame.
492 * @param {string} frameStr The stack frame as string.
493 * @return {webdriver.stacktrace.Frame} Stack frame object or null if the
494 * parsing failed.
495 * @private
496 */
497webdriver.stacktrace.parseStackFrame_ = function(frameStr) {
498 var m = frameStr.match(webdriver.stacktrace.V8_STACK_FRAME_REGEXP_);
499 if (m) {
500 return new webdriver.stacktrace.Frame(
501 m[1] || m[2], m[3], m[4], m[5] || m[6]);
502 }
503
504 if (frameStr.length >
505 webdriver.stacktrace.MAX_FIREFOX_FRAMESTRING_LENGTH_) {
506 return webdriver.stacktrace.parseLongFirefoxFrame_(frameStr);
507 }
508
509 m = frameStr.match(webdriver.stacktrace.FIREFOX_STACK_FRAME_REGEXP_);
510 if (m) {
511 return new webdriver.stacktrace.Frame('', m[1], '', m[2]);
512 }
513
514 m = frameStr.match(webdriver.stacktrace.OPERA_STACK_FRAME_REGEXP_);
515 if (m) {
516 return new webdriver.stacktrace.Frame(m[2], m[1] || m[3], '', m[4]);
517 }
518
519 m = frameStr.match(webdriver.stacktrace.CHAKRA_STACK_FRAME_REGEXP_);
520 if (m) {
521 return new webdriver.stacktrace.Frame('', m[1], '', m[2]);
522 }
523
524 if (frameStr == webdriver.stacktrace.UNKNOWN_CLOSURE_FRAME_ ||
525 frameStr == webdriver.stacktrace.ANONYMOUS_CLOSURE_FRAME_) {
526 return webdriver.stacktrace.ANONYMOUS_FRAME_;
527 }
528
529 m = frameStr.match(webdriver.stacktrace.CLOSURE_STACK_FRAME_REGEXP_);
530 if (m) {
531 return new webdriver.stacktrace.Frame(m[1], m[2], m[3], m[4] || m[5]);
532 }
533
534 return null;
535};
536
537
538/**
539 * Parses a long firefox stack frame.
540 * @param {string} frameStr The stack frame as string.
541 * @return {!webdriver.stacktrace.Frame} Stack frame object.
542 * @private
543 */
544webdriver.stacktrace.parseLongFirefoxFrame_ = function(frameStr) {
545 var firstParen = frameStr.indexOf('(');
546 var lastAmpersand = frameStr.lastIndexOf('@');
547 var lastColon = frameStr.lastIndexOf(':');
548 var functionName = '';
549 if ((firstParen >= 0) && (firstParen < lastAmpersand)) {
550 functionName = frameStr.substring(0, firstParen);
551 }
552 var loc = '';
553 if ((lastAmpersand >= 0) && (lastAmpersand + 1 < lastColon)) {
554 loc = frameStr.substring(lastAmpersand + 1);
555 }
556 return new webdriver.stacktrace.Frame('', functionName, '', loc);
557};
558
559
560/**
561 * Get an error's stack trace with the error string trimmed.
562 * V8 prepends the string representation of an error to its stack trace.
563 * This function trims the string so that the stack trace can be parsed
564 * consistently with the other JS engines.
565 * @param {(Error|goog.testing.JsUnitException)} error The error.
566 * @return {string} The stack trace string.
567 * @private
568 */
569webdriver.stacktrace.getStack_ = function(error) {
570 if (!error) {
571 return '';
572 }
573 var stack = error.stack || error.stackTrace || '';
574 var errorStr = error + '\n';
575 if (goog.string.startsWith(stack, errorStr)) {
576 stack = stack.substring(errorStr.length);
577 }
578 return stack;
579};
580
581
582/**
583 * Formats an error's stack trace.
584 * @param {!(Error|goog.testing.JsUnitException)} error The error to format.
585 * @return {!(Error|goog.testing.JsUnitException)} The formatted error.
586 */
587webdriver.stacktrace.format = function(error) {
588 var stack = webdriver.stacktrace.getStack_(error);
589 var frames = webdriver.stacktrace.parse_(stack);
590
591 // Older versions of IE simply return [object Error] for toString(), so
592 // only use that as a last resort.
593 var errorStr = '';
594 if (error.message) {
595 errorStr = (error.name ? error.name + ': ' : '') + error.message;
596 } else {
597 errorStr = error.toString();
598 }
599
600 // Ensure the error is in the V8 style with the error's string representation
601 // prepended to the stack.
602 error.stack = errorStr + '\n' + frames.join('\n');
603 return error;
604};
605
606
607/**
608 * Parses an Error object's stack trace.
609 * @param {string} stack The stack trace.
610 * @return {!Array.<!webdriver.stacktrace.Frame>} Stack frames. The
611 * unrecognized frames will be nulled out.
612 * @private
613 */
614webdriver.stacktrace.parse_ = function(stack) {
615 if (!stack) {
616 return [];
617 }
618
619 var lines = stack.
620 replace(/\s*$/, '').
621 split('\n');
622 var frames = [];
623 for (var i = 0; i < lines.length; i++) {
624 var frame = webdriver.stacktrace.parseStackFrame_(lines[i]);
625 // The first two frames will be:
626 // webdriver.stacktrace.Snapshot()
627 // webdriver.stacktrace.get()
628 // In the case of Opera, sometimes an extra frame is injected in the next
629 // frame with a reported line number of zero. The next line detects that
630 // case and skips that frame.
631 if (!(goog.userAgent.OPERA && i == 2 && frame.getLine() == 0)) {
632 frames.push(frame || webdriver.stacktrace.ANONYMOUS_FRAME_);
633 }
634 }
635 return frames;
636};
637
638
639/**
640 * Gets the native stack trace if available otherwise follows the call chain.
641 * The generated trace will exclude all frames up to and including the call to
642 * this function.
643 * @return {!Array.<!webdriver.stacktrace.Frame>} The frames of the stack trace.
644 */
645webdriver.stacktrace.get = function() {
646 return new webdriver.stacktrace.Snapshot(1).getStacktrace();
647};