lib/goog/testing/stacktrace.js

1// Copyright 2009 The Closure Library Authors. All Rights Reserved.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS-IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15/**
16 * @fileoverview Tools for parsing and pretty printing error stack traces.
17 *
18 */
19
20goog.provide('goog.testing.stacktrace');
21goog.provide('goog.testing.stacktrace.Frame');
22
23
24
25/**
26 * Class representing one stack frame.
27 * @param {string} context Context object, empty in case of global functions or
28 * if the browser doesn't provide this information.
29 * @param {string} name Function name, empty in case of anonymous functions.
30 * @param {string} alias Alias of the function if available. For example the
31 * function name will be 'c' and the alias will be 'b' if the function is
32 * defined as <code>a.b = function c() {};</code>.
33 * @param {string} args Arguments of the function in parentheses if available.
34 * @param {string} path File path or URL including line number and optionally
35 * column number separated by colons.
36 * @constructor
37 * @final
38 */
39goog.testing.stacktrace.Frame = function(context, name, alias, args, path) {
40 this.context_ = context;
41 this.name_ = name;
42 this.alias_ = alias;
43 this.args_ = args;
44 this.path_ = path;
45};
46
47
48/**
49 * @return {string} The function name or empty string if the function is
50 * anonymous and the object field which it's assigned to is unknown.
51 */
52goog.testing.stacktrace.Frame.prototype.getName = function() {
53 return this.name_;
54};
55
56
57/**
58 * @return {boolean} Whether the stack frame contains an anonymous function.
59 */
60goog.testing.stacktrace.Frame.prototype.isAnonymous = function() {
61 return !this.name_ || this.context_ == '[object Object]';
62};
63
64
65/**
66 * Brings one frame of the stack trace into a common format across browsers.
67 * @return {string} Pretty printed stack frame.
68 */
69goog.testing.stacktrace.Frame.prototype.toCanonicalString = function() {
70 var htmlEscape = goog.testing.stacktrace.htmlEscape_;
71 var deobfuscate = goog.testing.stacktrace.maybeDeobfuscateFunctionName_;
72
73 var canonical = [
74 this.context_ ? htmlEscape(this.context_) + '.' : '',
75 this.name_ ? htmlEscape(deobfuscate(this.name_)) : 'anonymous',
76 htmlEscape(this.args_),
77 this.alias_ ? ' [as ' + htmlEscape(deobfuscate(this.alias_)) + ']' : ''
78 ];
79
80 if (this.path_) {
81 canonical.push(' at ');
82 canonical.push(htmlEscape(this.path_));
83 }
84 return canonical.join('');
85};
86
87
88/**
89 * Maximum number of steps while the call chain is followed.
90 * @private {number}
91 * @const
92 */
93goog.testing.stacktrace.MAX_DEPTH_ = 20;
94
95
96/**
97 * Maximum length of a string that can be matched with a RegExp on
98 * Firefox 3x. Exceeding this approximate length will cause string.match
99 * to exceed Firefox's stack quota. This situation can be encountered
100 * when goog.globalEval is invoked with a long argument; such as
101 * when loading a module.
102 * @private {number}
103 * @const
104 */
105goog.testing.stacktrace.MAX_FIREFOX_FRAMESTRING_LENGTH_ = 500000;
106
107
108/**
109 * RegExp pattern for JavaScript identifiers. We don't support Unicode
110 * identifiers defined in ECMAScript v3.
111 * @private {string}
112 * @const
113 */
114goog.testing.stacktrace.IDENTIFIER_PATTERN_ = '[a-zA-Z_$][\\w$]*';
115
116
117/**
118 * RegExp pattern for function name alias in the V8 stack trace.
119 * @private {string}
120 * @const
121 */
122goog.testing.stacktrace.V8_ALIAS_PATTERN_ =
123 '(?: \\[as (' + goog.testing.stacktrace.IDENTIFIER_PATTERN_ + ')\\])?';
124
125
126/**
127 * RegExp pattern for the context of a function call in a V8 stack trace.
128 * Creates an optional submatch for the namespace identifier including the
129 * "new" keyword for constructor calls (e.g. "new foo.Bar").
130 * @private {string}
131 * @const
132 */
133goog.testing.stacktrace.V8_CONTEXT_PATTERN_ =
134 '(?:((?:new )?(?:\\[object Object\\]|' +
135 goog.testing.stacktrace.IDENTIFIER_PATTERN_ +
136 '(?:\\.' + goog.testing.stacktrace.IDENTIFIER_PATTERN_ + ')*))\\.)?';
137
138
139/**
140 * RegExp pattern for function names and constructor calls in the V8 stack
141 * trace.
142 * @private {string}
143 * @const
144 */
145goog.testing.stacktrace.V8_FUNCTION_NAME_PATTERN_ =
146 '(?:new )?(?:' + goog.testing.stacktrace.IDENTIFIER_PATTERN_ +
147 '|<anonymous>)';
148
149
150/**
151 * RegExp pattern for function call in the V8 stack trace. Creates 3 submatches
152 * with context object (optional), function name and function alias (optional).
153 * @private {string}
154 * @const
155 */
156goog.testing.stacktrace.V8_FUNCTION_CALL_PATTERN_ =
157 ' ' + goog.testing.stacktrace.V8_CONTEXT_PATTERN_ +
158 '(' + goog.testing.stacktrace.V8_FUNCTION_NAME_PATTERN_ + ')' +
159 goog.testing.stacktrace.V8_ALIAS_PATTERN_;
160
161
162/**
163 * RegExp pattern for an URL + position inside the file.
164 * @private {string}
165 * @const
166 */
167goog.testing.stacktrace.URL_PATTERN_ =
168 '((?:http|https|file)://[^\\s)]+|javascript:.*)';
169
170
171/**
172 * RegExp pattern for an URL + line number + column number in V8.
173 * The URL is either in submatch 1 or submatch 2.
174 * @private {string}
175 * @const
176 */
177goog.testing.stacktrace.CHROME_URL_PATTERN_ = ' (?:' +
178 '\\(unknown source\\)' + '|' +
179 '\\(native\\)' + '|' +
180 '\\((.+)\\)|(.+))';
181
182
183/**
184 * Regular expression for parsing one stack frame in V8. For more information
185 * on V8 stack frame formats, see
186 * https://code.google.com/p/v8/wiki/JavaScriptStackTraceApi.
187 * @private {!RegExp}
188 * @const
189 */
190goog.testing.stacktrace.V8_STACK_FRAME_REGEXP_ = new RegExp('^ at' +
191 '(?:' + goog.testing.stacktrace.V8_FUNCTION_CALL_PATTERN_ + ')?' +
192 goog.testing.stacktrace.CHROME_URL_PATTERN_ + '$');
193
194
195/**
196 * RegExp pattern for function call in the Firefox stack trace.
197 * Creates 2 submatches with function name (optional) and arguments.
198 * @private {string}
199 * @const
200 */
201goog.testing.stacktrace.FIREFOX_FUNCTION_CALL_PATTERN_ =
202 '(' + goog.testing.stacktrace.IDENTIFIER_PATTERN_ + ')?' +
203 '(\\(.*\\))?@';
204
205
206/**
207 * Regular expression for parsing one stack frame in Firefox.
208 * @private {!RegExp}
209 * @const
210 */
211goog.testing.stacktrace.FIREFOX_STACK_FRAME_REGEXP_ = new RegExp('^' +
212 goog.testing.stacktrace.FIREFOX_FUNCTION_CALL_PATTERN_ +
213 '(?::0|' + goog.testing.stacktrace.URL_PATTERN_ + ')$');
214
215
216/**
217 * RegExp pattern for an anonymous function call in an Opera stack frame.
218 * Creates 2 (optional) submatches: the context object and function name.
219 * @private {string}
220 * @const
221 */
222goog.testing.stacktrace.OPERA_ANONYMOUS_FUNCTION_NAME_PATTERN_ =
223 '<anonymous function(?:\\: ' +
224 '(?:(' + goog.testing.stacktrace.IDENTIFIER_PATTERN_ +
225 '(?:\\.' + goog.testing.stacktrace.IDENTIFIER_PATTERN_ + ')*)\\.)?' +
226 '(' + goog.testing.stacktrace.IDENTIFIER_PATTERN_ + '))?>';
227
228
229/**
230 * RegExp pattern for a function call in an Opera stack frame.
231 * Creates 4 (optional) submatches: the function name (if not anonymous),
232 * the aliased context object and function name (if anonymous), and the
233 * function call arguments.
234 * @private {string}
235 * @const
236 */
237goog.testing.stacktrace.OPERA_FUNCTION_CALL_PATTERN_ =
238 '(?:(?:(' + goog.testing.stacktrace.IDENTIFIER_PATTERN_ + ')|' +
239 goog.testing.stacktrace.OPERA_ANONYMOUS_FUNCTION_NAME_PATTERN_ +
240 ')(\\(.*\\)))?@';
241
242
243/**
244 * Regular expression for parsing on stack frame in Opera 11.68 - 12.17.
245 * Newer versions of Opera use V8 and stack frames should match against
246 * goog.testing.stacktrace.V8_STACK_FRAME_REGEXP_.
247 * @private {!RegExp}
248 * @const
249 */
250goog.testing.stacktrace.OPERA_STACK_FRAME_REGEXP_ = new RegExp('^' +
251 goog.testing.stacktrace.OPERA_FUNCTION_CALL_PATTERN_ +
252 goog.testing.stacktrace.URL_PATTERN_ + '?$');
253
254
255/**
256 * Regular expression for finding the function name in its source.
257 * @private {!RegExp}
258 * @const
259 */
260goog.testing.stacktrace.FUNCTION_SOURCE_REGEXP_ = new RegExp(
261 '^function (' + goog.testing.stacktrace.IDENTIFIER_PATTERN_ + ')');
262
263
264/**
265 * RegExp pattern for function call in a IE stack trace. This expression allows
266 * for identifiers like 'Anonymous function', 'eval code', and 'Global code'.
267 * @private {string}
268 * @const
269 */
270goog.testing.stacktrace.IE_FUNCTION_CALL_PATTERN_ = '(' +
271 goog.testing.stacktrace.IDENTIFIER_PATTERN_ + '(?:\\s+\\w+)*)';
272
273
274/**
275 * Regular expression for parsing a stack frame in IE.
276 * @private {!RegExp}
277 * @const
278 */
279goog.testing.stacktrace.IE_STACK_FRAME_REGEXP_ = new RegExp('^ at ' +
280 goog.testing.stacktrace.IE_FUNCTION_CALL_PATTERN_ +
281 '\\s*\\((eval code:[^)]*|' + goog.testing.stacktrace.URL_PATTERN_ +
282 ')\\)?$');
283
284
285/**
286 * Creates a stack trace by following the call chain. Based on
287 * {@link goog.debug.getStacktrace}.
288 * @return {!Array<!goog.testing.stacktrace.Frame>} Stack frames.
289 * @private
290 * @suppress {es5Strict}
291 */
292goog.testing.stacktrace.followCallChain_ = function() {
293 var frames = [];
294 var fn = arguments.callee.caller;
295 var depth = 0;
296
297 while (fn && depth < goog.testing.stacktrace.MAX_DEPTH_) {
298 var fnString = Function.prototype.toString.call(fn);
299 var match = fnString.match(goog.testing.stacktrace.FUNCTION_SOURCE_REGEXP_);
300 var functionName = match ? match[1] : '';
301
302 var argsBuilder = ['('];
303 if (fn.arguments) {
304 for (var i = 0; i < fn.arguments.length; i++) {
305 var arg = fn.arguments[i];
306 if (i > 0) {
307 argsBuilder.push(', ');
308 }
309 if (goog.isString(arg)) {
310 argsBuilder.push('"', arg, '"');
311 } else {
312 // Some args are mocks, and we don't want to fail from them not having
313 // expected a call to toString, so instead insert a static string.
314 if (arg && arg['$replay']) {
315 argsBuilder.push('goog.testing.Mock');
316 } else {
317 argsBuilder.push(String(arg));
318 }
319 }
320 }
321 } else {
322 // Opera 10 doesn't know the arguments of native functions.
323 argsBuilder.push('unknown');
324 }
325 argsBuilder.push(')');
326 var args = argsBuilder.join('');
327
328 frames.push(new goog.testing.stacktrace.Frame('', functionName, '', args,
329 ''));
330
331 /** @preserveTry */
332 try {
333 fn = fn.caller;
334 } catch (e) {
335 break;
336 }
337 depth++;
338 }
339
340 return frames;
341};
342
343
344/**
345 * Parses one stack frame.
346 * @param {string} frameStr The stack frame as string.
347 * @return {goog.testing.stacktrace.Frame} Stack frame object or null if the
348 * parsing failed.
349 * @private
350 */
351goog.testing.stacktrace.parseStackFrame_ = function(frameStr) {
352 // This match includes newer versions of Opera (15+).
353 var m = frameStr.match(goog.testing.stacktrace.V8_STACK_FRAME_REGEXP_);
354 if (m) {
355 return new goog.testing.stacktrace.Frame(m[1] || '', m[2] || '', m[3] || '',
356 '', m[4] || m[5] || m[6] || '');
357 }
358
359 if (frameStr.length >
360 goog.testing.stacktrace.MAX_FIREFOX_FRAMESTRING_LENGTH_) {
361 return goog.testing.stacktrace.parseLongFirefoxFrame_(frameStr);
362 }
363
364 m = frameStr.match(goog.testing.stacktrace.FIREFOX_STACK_FRAME_REGEXP_);
365 if (m) {
366 return new goog.testing.stacktrace.Frame('', m[1] || '', '', m[2] || '',
367 m[3] || '');
368 }
369
370 // Match against Presto Opera 11.68 - 12.17.
371 m = frameStr.match(goog.testing.stacktrace.OPERA_STACK_FRAME_REGEXP_);
372 if (m) {
373 return new goog.testing.stacktrace.Frame(m[2] || '', m[1] || m[3] || '',
374 '', m[4] || '', m[5] || '');
375 }
376
377 m = frameStr.match(goog.testing.stacktrace.IE_STACK_FRAME_REGEXP_);
378 if (m) {
379 return new goog.testing.stacktrace.Frame('', m[1] || '', '', '',
380 m[2] || '');
381 }
382
383 return null;
384};
385
386
387/**
388 * Parses a long firefox stack frame.
389 * @param {string} frameStr The stack frame as string.
390 * @return {!goog.testing.stacktrace.Frame} Stack frame object.
391 * @private
392 */
393goog.testing.stacktrace.parseLongFirefoxFrame_ = function(frameStr) {
394 var firstParen = frameStr.indexOf('(');
395 var lastAmpersand = frameStr.lastIndexOf('@');
396 var lastColon = frameStr.lastIndexOf(':');
397 var functionName = '';
398 if ((firstParen >= 0) && (firstParen < lastAmpersand)) {
399 functionName = frameStr.substring(0, firstParen);
400 }
401 var loc = '';
402 if ((lastAmpersand >= 0) && (lastAmpersand + 1 < lastColon)) {
403 loc = frameStr.substring(lastAmpersand + 1);
404 }
405 var args = '';
406 if ((firstParen >= 0 && lastAmpersand > 0) &&
407 (firstParen < lastAmpersand)) {
408 args = frameStr.substring(firstParen, lastAmpersand);
409 }
410 return new goog.testing.stacktrace.Frame('', functionName, '', args, loc);
411};
412
413
414/**
415 * Function to deobfuscate function names.
416 * @type {function(string): string}
417 * @private
418 */
419goog.testing.stacktrace.deobfuscateFunctionName_;
420
421
422/**
423 * Sets function to deobfuscate function names.
424 * @param {function(string): string} fn function to deobfuscate function names.
425 */
426goog.testing.stacktrace.setDeobfuscateFunctionName = function(fn) {
427 goog.testing.stacktrace.deobfuscateFunctionName_ = fn;
428};
429
430
431/**
432 * Deobfuscates a compiled function name with the function passed to
433 * {@link #setDeobfuscateFunctionName}. Returns the original function name if
434 * the deobfuscator hasn't been set.
435 * @param {string} name The function name to deobfuscate.
436 * @return {string} The deobfuscated function name.
437 * @private
438 */
439goog.testing.stacktrace.maybeDeobfuscateFunctionName_ = function(name) {
440 return goog.testing.stacktrace.deobfuscateFunctionName_ ?
441 goog.testing.stacktrace.deobfuscateFunctionName_(name) : name;
442};
443
444
445/**
446 * Escapes the special character in HTML.
447 * @param {string} text Plain text.
448 * @return {string} Escaped text.
449 * @private
450 */
451goog.testing.stacktrace.htmlEscape_ = function(text) {
452 return text.replace(/&/g, '&amp;').
453 replace(/</g, '&lt;').
454 replace(/>/g, '&gt;').
455 replace(/"/g, '&quot;');
456};
457
458
459/**
460 * Converts the stack frames into canonical format. Chops the beginning and the
461 * end of it which come from the testing environment, not from the test itself.
462 * @param {!Array<goog.testing.stacktrace.Frame>} frames The frames.
463 * @return {string} Canonical, pretty printed stack trace.
464 * @private
465 */
466goog.testing.stacktrace.framesToString_ = function(frames) {
467 // Removes the anonymous calls from the end of the stack trace (they come
468 // from testrunner.js, testcase.js and asserts.js), so the stack trace will
469 // end with the test... method.
470 var lastIndex = frames.length - 1;
471 while (frames[lastIndex] && frames[lastIndex].isAnonymous()) {
472 lastIndex--;
473 }
474
475 // Removes the beginning of the stack trace until the call of the private
476 // _assert function (inclusive), so the stack trace will begin with a public
477 // asserter. Does nothing if _assert is not present in the stack trace.
478 var privateAssertIndex = -1;
479 for (var i = 0; i < frames.length; i++) {
480 if (frames[i] && frames[i].getName() == '_assert') {
481 privateAssertIndex = i;
482 break;
483 }
484 }
485
486 var canonical = [];
487 for (var i = privateAssertIndex + 1; i <= lastIndex; i++) {
488 canonical.push('> ');
489 if (frames[i]) {
490 canonical.push(frames[i].toCanonicalString());
491 } else {
492 canonical.push('(unknown)');
493 }
494 canonical.push('\n');
495 }
496 return canonical.join('');
497};
498
499
500/**
501 * Parses the browser's native stack trace.
502 * @param {string} stack Stack trace.
503 * @return {!Array<goog.testing.stacktrace.Frame>} Stack frames. The
504 * unrecognized frames will be nulled out.
505 * @private
506 */
507goog.testing.stacktrace.parse_ = function(stack) {
508 var lines = stack.replace(/\s*$/, '').split('\n');
509 var frames = [];
510 for (var i = 0; i < lines.length; i++) {
511 frames.push(goog.testing.stacktrace.parseStackFrame_(lines[i]));
512 }
513 return frames;
514};
515
516
517/**
518 * Brings the stack trace into a common format across browsers.
519 * @param {string} stack Browser-specific stack trace.
520 * @return {string} Same stack trace in common format.
521 */
522goog.testing.stacktrace.canonicalize = function(stack) {
523 var frames = goog.testing.stacktrace.parse_(stack);
524 return goog.testing.stacktrace.framesToString_(frames);
525};
526
527
528/**
529 * Returns the native stack trace.
530 * @return {string|!Array<!CallSite>}
531 * @private
532 */
533goog.testing.stacktrace.getNativeStack_ = function() {
534 var tmpError = new Error();
535 if (tmpError.stack) {
536 return tmpError.stack;
537 }
538
539 // IE10 will only create a stack trace when the Error is thrown.
540 // We use null.x() to throw an exception because the closure compiler may
541 // replace "throw" with a function call in an attempt to minimize the binary
542 // size, which in turn has the side effect of adding an unwanted stack frame.
543 try {
544 null.x();
545 } catch (e) {
546 return e.stack;
547 }
548 return '';
549};
550
551
552/**
553 * Gets the native stack trace if available otherwise follows the call chain.
554 * @return {string} The stack trace in canonical format.
555 */
556goog.testing.stacktrace.get = function() {
557 var stack = goog.testing.stacktrace.getNativeStack_();
558 var frames;
559 if (!stack) {
560 frames = goog.testing.stacktrace.followCallChain_();
561 } else if (goog.isArray(stack)) {
562 frames = goog.testing.stacktrace.callSitesToFrames_(stack);
563 } else {
564 frames = goog.testing.stacktrace.parse_(stack);
565 }
566 return goog.testing.stacktrace.framesToString_(frames);
567};
568
569
570/**
571 * Converts an array of CallSite (elements of a stack trace in V8) to an array
572 * of Frames.
573 * @param {!Array<!CallSite>} stack The stack as an array of CallSites.
574 * @return {!Array<!goog.testing.stacktrace.Frame>} The stack as an array of
575 * Frames.
576 * @private
577 */
578goog.testing.stacktrace.callSitesToFrames_ = function(stack) {
579 var frames = [];
580 for (var i = 0; i < stack.length; i++) {
581 var callSite = stack[i];
582 var functionName = callSite.getFunctionName() || 'unknown';
583 var fileName = callSite.getFileName();
584 var path = fileName ? fileName + ':' + callSite.getLineNumber() + ':' +
585 callSite.getColumnNumber() : 'unknown';
586 frames.push(
587 new goog.testing.stacktrace.Frame('', functionName, '', '', path));
588 }
589 return frames;
590};
591
592
593goog.exportSymbol('setDeobfuscateFunctionName',
594 goog.testing.stacktrace.setDeobfuscateFunctionName);