# 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 |
23 | goog.provide('webdriver.stacktrace');
24 | goog.provide('webdriver.stacktrace.Snapshot');
25 |
26 | goog.require('goog.array');
27 | goog.require('goog.string');
28 | goog.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 | */
39 | webdriver.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 | */
77 | webdriver.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 | */
87 | webdriver.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 | */
102 | webdriver.stacktrace.Snapshot.prototype.parsedStack_ = null;
103 |
104 |
105 | /**
106 | * @return {!Array.<!webdriver.stacktrace.Frame>} The parsed stack trace.
107 | */
108 | webdriver.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 | */
135 | webdriver.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 | */
174 | webdriver.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 | */
182 | webdriver.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 | */
190 | webdriver.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 | */
198 | webdriver.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 | */
206 | webdriver.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 | */
214 | webdriver.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 | */
225 | webdriver.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 | */
247 | webdriver.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 | */
256 | webdriver.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 | */
266 | webdriver.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 | */
278 | webdriver.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 | */
288 | webdriver.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 | */
298 | webdriver.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 | */
310 | webdriver.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 | */
324 | webdriver.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 | */
335 | webdriver.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 | */
348 | webdriver.stacktrace.V8_LOCATION_PATTERN_ = ' (?:\\((.*)\\)|(.*))';
349 |
350 |
351 | /**
352 | * Regular expression for parsing one stack frame in V8.
353 | * @private {!RegExp}
354 | * @const
355 | */
356 | webdriver.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 | */
370 | webdriver.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 | */
380 | webdriver.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 | */
390 | webdriver.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 | */
403 | webdriver.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 | */
414 | webdriver.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 | */
425 | webdriver.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 | */
434 | webdriver.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 | */
443 | webdriver.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 | */
455 | webdriver.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 | */
468 | webdriver.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 | */
510 | webdriver.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 | */
534 | webdriver.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 | */
552 | webdriver.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 | */
591 | webdriver.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 | */
617 | webdriver.stacktrace.get = function() {
618 | return new webdriver.stacktrace.Snapshot(1).getStacktrace();
619 | };