# 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 |
20 | goog.provide('goog.testing.stacktrace');
21 | goog.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 | */
39 | goog.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 | */
52 | goog.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 | */
60 | goog.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 | */
69 | goog.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 | */
93 | goog.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 | */
105 | goog.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 | */
114 | goog.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 | */
122 | goog.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 | */
133 | goog.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 | */
145 | goog.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 | */
156 | goog.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 | */
167 | goog.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 | */
177 | goog.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 | */
190 | goog.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 | */
201 | goog.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 | */
211 | goog.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 | */
222 | goog.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 | */
237 | goog.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 | */
250 | goog.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 | */
260 | goog.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 | */
270 | goog.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 | */
279 | goog.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 | */
292 | goog.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 | */
351 | goog.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 | */
393 | goog.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 | */
419 | goog.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 | */
426 | goog.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 | */
439 | goog.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 | */
451 | goog.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 | */
466 | goog.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 | */
507 | goog.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 | */
522 | goog.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 | */
533 | goog.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 | */
556 | goog.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 | */
578 | goog.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 |
593 | goog.exportSymbol('setDeobfuscateFunctionName',
594 | goog.testing.stacktrace.setDeobfuscateFunctionName);