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 creates 2 submatches on the (optional) context and function name,
430 * matching identifiers like 'foo.Bar.prototype.baz', 'Anonymous function',
431 * 'eval code', and 'Global code'.
432 * @private {string}
433 * @const
434 */
435webdriver.stacktrace.CHAKRA_FUNCTION_CALL_PATTERN_ =
436 '(?:(' + webdriver.stacktrace.IDENTIFIER_PATTERN_ +
437 '(?:\\.' + webdriver.stacktrace.IDENTIFIER_PATTERN_ + ')*)\\.)?' +
438 '(' + webdriver.stacktrace.IDENTIFIER_PATTERN_ + '(?:\\s+\\w+)*)';
439
440
441/**
442 * Regular expression for parsing on stack frame in Chakra (IE).
443 * @private {!RegExp}
444 * @const
445 */
446webdriver.stacktrace.CHAKRA_STACK_FRAME_REGEXP_ = new RegExp('^ at ' +
447 webdriver.stacktrace.CHAKRA_FUNCTION_CALL_PATTERN_ +
448 '\\s*(?:\\((.*)\\))$');
449
450
451/**
452 * Placeholder for an unparsable frame in a stack trace generated by
453 * {@link goog.testing.stacktrace}.
454 * @private {string}
455 * @const
456 */
457webdriver.stacktrace.UNKNOWN_CLOSURE_FRAME_ = '> (unknown)';
458
459
460/**
461 * Representation of an anonymous frame in a stack trace generated by
462 * {@link goog.testing.stacktrace}.
463 * @private {string}
464 * @const
465 */
466webdriver.stacktrace.ANONYMOUS_CLOSURE_FRAME_ = '> anonymous';
467
468
469/**
470 * Pattern for a function call in a Closure stack trace. Creates three optional
471 * submatches: the context, function name, and alias.
472 * @private {string}
473 * @const
474 */
475webdriver.stacktrace.CLOSURE_FUNCTION_CALL_PATTERN_ =
476 webdriver.stacktrace.QUALIFIED_NAME_PATTERN_ +
477 '(?:\\(.*\\))?' + // Ignore arguments if present.
478 webdriver.stacktrace.V8_ALIAS_PATTERN_;
479
480
481/**
482 * Regular expression for parsing a stack frame generated by Closure's
483 * {@link goog.testing.stacktrace}.
484 * @private {!RegExp}
485 * @const
486 */
487webdriver.stacktrace.CLOSURE_STACK_FRAME_REGEXP_ = new RegExp('^> ' +
488 '(?:' + webdriver.stacktrace.CLOSURE_FUNCTION_CALL_PATTERN_ +
489 '(?: at )?)?' +
490 '(?:(.*:\\d+:\\d+)|' + webdriver.stacktrace.URL_PATTERN_ + ')?$');
491
492
493/**
494 * Parses one stack frame.
495 * @param {string} frameStr The stack frame as string.
496 * @return {webdriver.stacktrace.Frame} Stack frame object or null if the
497 * parsing failed.
498 * @private
499 */
500webdriver.stacktrace.parseStackFrame_ = function(frameStr) {
501 var m = frameStr.match(webdriver.stacktrace.V8_STACK_FRAME_REGEXP_);
502 if (m) {
503 return new webdriver.stacktrace.Frame(
504 m[1] || m[2], m[3], m[4], m[5] || m[6]);
505 }
506
507 if (frameStr.length >
508 webdriver.stacktrace.MAX_FIREFOX_FRAMESTRING_LENGTH_) {
509 return webdriver.stacktrace.parseLongFirefoxFrame_(frameStr);
510 }
511
512 m = frameStr.match(webdriver.stacktrace.FIREFOX_STACK_FRAME_REGEXP_);
513 if (m) {
514 return new webdriver.stacktrace.Frame('', m[1], '', m[2]);
515 }
516
517 m = frameStr.match(webdriver.stacktrace.OPERA_STACK_FRAME_REGEXP_);
518 if (m) {
519 return new webdriver.stacktrace.Frame(m[2], m[1] || m[3], '', m[4]);
520 }
521
522 m = frameStr.match(webdriver.stacktrace.CHAKRA_STACK_FRAME_REGEXP_);
523 if (m) {
524 return new webdriver.stacktrace.Frame(m[1], m[2], '', m[3]);
525 }
526
527 if (frameStr == webdriver.stacktrace.UNKNOWN_CLOSURE_FRAME_ ||
528 frameStr == webdriver.stacktrace.ANONYMOUS_CLOSURE_FRAME_) {
529 return webdriver.stacktrace.ANONYMOUS_FRAME_;
530 }
531
532 m = frameStr.match(webdriver.stacktrace.CLOSURE_STACK_FRAME_REGEXP_);
533 if (m) {
534 return new webdriver.stacktrace.Frame(m[1], m[2], m[3], m[4] || m[5]);
535 }
536
537 return null;
538};
539
540
541/**
542 * Parses a long firefox stack frame.
543 * @param {string} frameStr The stack frame as string.
544 * @return {!webdriver.stacktrace.Frame} Stack frame object.
545 * @private
546 */
547webdriver.stacktrace.parseLongFirefoxFrame_ = function(frameStr) {
548 var firstParen = frameStr.indexOf('(');
549 var lastAmpersand = frameStr.lastIndexOf('@');
550 var lastColon = frameStr.lastIndexOf(':');
551 var functionName = '';
552 if ((firstParen >= 0) && (firstParen < lastAmpersand)) {
553 functionName = frameStr.substring(0, firstParen);
554 }
555 var loc = '';
556 if ((lastAmpersand >= 0) && (lastAmpersand + 1 < lastColon)) {
557 loc = frameStr.substring(lastAmpersand + 1);
558 }
559 return new webdriver.stacktrace.Frame('', functionName, '', loc);
560};
561
562
563/**
564 * Get an error's stack trace with the error string trimmed.
565 * V8 prepends the string representation of an error to its stack trace.
566 * This function trims the string so that the stack trace can be parsed
567 * consistently with the other JS engines.
568 * @param {(Error|goog.testing.JsUnitException)} error The error.
569 * @return {string} The stack trace string.
570 * @private
571 */
572webdriver.stacktrace.getStack_ = function(error) {
573 if (!error) {
574 return '';
575 }
576 var stack = error.stack || error.stackTrace || '';
577 var errorStr = error + '\n';
578 if (goog.string.startsWith(stack, errorStr)) {
579 stack = stack.substring(errorStr.length);
580 }
581 return stack;
582};
583
584
585/**
586 * Formats an error's stack trace.
587 * @param {!(Error|goog.testing.JsUnitException)} error The error to format.
588 * @return {!(Error|goog.testing.JsUnitException)} The formatted error.
589 */
590webdriver.stacktrace.format = function(error) {
591 var stack = webdriver.stacktrace.getStack_(error);
592 var frames = webdriver.stacktrace.parse_(stack);
593
594 // Older versions of IE simply return [object Error] for toString(), so
595 // only use that as a last resort.
596 var errorStr = '';
597 if (error.message) {
598 errorStr = (error.name ? error.name + ': ' : '') + error.message;
599 } else {
600 errorStr = error.toString();
601 }
602
603 // Ensure the error is in the V8 style with the error's string representation
604 // prepended to the stack.
605 error.stack = errorStr + '\n' + frames.join('\n');
606 return error;
607};
608
609
610/**
611 * Parses an Error object's stack trace.
612 * @param {string} stack The stack trace.
613 * @return {!Array.<!webdriver.stacktrace.Frame>} Stack frames. The
614 * unrecognized frames will be nulled out.
615 * @private
616 */
617webdriver.stacktrace.parse_ = function(stack) {
618 if (!stack) {
619 return [];
620 }
621
622 var lines = stack.
623 replace(/\s*$/, '').
624 split('\n');
625 var frames = [];
626 for (var i = 0; i < lines.length; i++) {
627 var frame = webdriver.stacktrace.parseStackFrame_(lines[i]);
628 // The first two frames will be:
629 // webdriver.stacktrace.Snapshot()
630 // webdriver.stacktrace.get()
631 // In the case of Opera, sometimes an extra frame is injected in the next
632 // frame with a reported line number of zero. The next line detects that
633 // case and skips that frame.
634 if (!(goog.userAgent.OPERA && i == 2 && frame.getLine() == 0)) {
635 frames.push(frame || webdriver.stacktrace.ANONYMOUS_FRAME_);
636 }
637 }
638 return frames;
639};
640
641
642/**
643 * Gets the native stack trace if available otherwise follows the call chain.
644 * The generated trace will exclude all frames up to and including the call to
645 * this function.
646 * @return {!Array.<!webdriver.stacktrace.Frame>} The frames of the stack trace.
647 */
648webdriver.stacktrace.get = function() {
649 return new webdriver.stacktrace.Snapshot(1).getStacktrace();
650};