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