feat(exception_handler): print originalException and originalStack for all exceptions

This commit is contained in:
vsavkin 2015-07-23 18:00:19 -07:00
parent 0a8b3816f7
commit e744409cb9
18 changed files with 259 additions and 120 deletions

View File

@ -64,7 +64,6 @@ import {
} from 'angular2/src/render/dom/dom_renderer';
import {DefaultDomCompiler} from 'angular2/src/render/dom/compiler/compiler';
import {internalView} from 'angular2/src/core/compiler/view_ref';
import {appComponentRefPromiseToken, appComponentTypeToken} from './application_tokens';
var _rootInjector: Injector;
@ -129,7 +128,7 @@ function _injectorBindings(appComponentType): List<Type | Binding | List<any>> {
DirectiveResolver,
Parser,
Lexer,
ExceptionHandler,
bind(ExceptionHandler).toFactory(() => new ExceptionHandler(DOM)),
bind(XHR).toValue(new XHRImpl()),
ComponentUrlMapper,
UrlResolver,
@ -282,8 +281,7 @@ export function commonBootstrap(
componentInjectableBindings: List<Type | Binding | List<any>> = null): Promise<ApplicationRef> {
BrowserDomAdapter.makeCurrent();
var bootstrapProcess = PromiseWrapper.completer();
var zone = createNgZone(new ExceptionHandler());
var zone = createNgZone(new ExceptionHandler(DOM));
zone.run(() => {
// TODO(rado): prepopulate template cache, so applications with only
// index.html and main.js are possible.

View File

@ -1,7 +1,13 @@
import {Injectable} from 'angular2/di';
import {isPresent, print, BaseException} from 'angular2/src/facade/lang';
import {isPresent, isBlank, print, BaseException} from 'angular2/src/facade/lang';
import {ListWrapper, isListLikeIterable} from 'angular2/src/facade/collection';
import {DOM} from 'angular2/src/dom/dom_adapter';
class _ArrayLogger {
res: any[] = [];
log(s: any): void { this.res.push(s); }
logGroup(s: any): void { this.res.push(s); }
logGroupEnd(){};
}
/**
* Provides a hook for centralized exception handling.
@ -26,31 +32,91 @@ import {DOM} from 'angular2/src/dom/dom_adapter';
*/
@Injectable()
export class ExceptionHandler {
logError: Function = DOM.logError;
constructor(private logger: any, private rethrowException: boolean = true) {}
call(exception: Object, stackTrace: any = null, reason: string = null) {
var longStackTrace = isListLikeIterable(stackTrace) ?
(<any>stackTrace).join("\n\n-----async gap-----\n") :
stackTrace;
static exceptionToString(exception: any, stackTrace: any = null, reason: string = null): string {
var l = new _ArrayLogger();
var e = new ExceptionHandler(l, false);
e.call(exception, stackTrace, reason);
return l.res.join("\n");
}
this.logError(`${exception}\n\n${longStackTrace}`);
call(exception: any, stackTrace: any = null, reason: string = null): void {
var originalException = this._findOriginalException(exception);
var originalStack = this._findOriginalStack(exception);
var context = this._findContext(exception);
this.logger.logGroup(`EXCEPTION: ${exception}`);
if (isPresent(stackTrace) && isBlank(originalStack)) {
this.logger.log("STACKTRACE:");
this.logger.log(this._longStackTrace(stackTrace))
}
if (isPresent(reason)) {
this.logError(`Reason: ${reason}`);
this.logger.log(`REASON: ${reason}`);
}
if (isPresent(originalException)) {
this.logger.log(`ORIGINAL EXCEPTION: ${originalException}`);
}
if (isPresent(originalStack)) {
this.logger.log("ORIGINAL STACKTRACE:");
this.logger.log(this._longStackTrace(originalStack));
}
var context = this._findContext(exception);
if (isPresent(context)) {
this.logError("Error Context:");
this.logError(context);
this.logger.log("ERROR CONTEXT:");
this.logger.log(context);
}
throw exception;
this.logger.logGroupEnd();
// We rethrow exceptions, so operations like 'bootstrap' will result in an error
// when an exception happens. If we do not rethrow, bootstrap will always succeed.
if (this.rethrowException) throw exception;
}
_longStackTrace(stackTrace: any): any {
return isListLikeIterable(stackTrace) ? (<any>stackTrace).join("\n\n-----async gap-----\n") :
stackTrace;
}
_findContext(exception: any): any {
try {
if (!(exception instanceof BaseException)) return null;
return isPresent(exception.context) ? exception.context :
this._findContext(exception.originalException);
} catch (e) {
// exception.context can throw an exception. if it happens, we ignore the context.
return null;
}
}
_findOriginalException(exception: any): any {
if (!(exception instanceof BaseException)) return null;
var e = exception.originalException;
while (e instanceof BaseException && isPresent(e.originalException)) {
e = e.originalException;
}
return e;
}
_findOriginalStack(exception: any): any {
if (!(exception instanceof BaseException)) return null;
var e = exception;
var stack = exception.originalStack;
while (e instanceof BaseException && isPresent(e.originalException)) {
e = e.originalException;
if (e instanceof BaseException && isPresent(e.originalException)) {
stack = e.originalStack;
}
}
return stack;
}
}

View File

@ -142,6 +142,18 @@ class BrowserDomAdapter extends GenericBrowserDomAdapter {
window.console.error(error);
}
log(error) {
window.console.log(error);
}
logGroup(error) {
window.console.group(error);
}
logGroupEnd() {
window.console.groupEnd();
}
@override
Map<String, String> get attrToPropMap => const <String, String>{
'innerHtml': 'innerHTML',

View File

@ -61,6 +61,22 @@ export class BrowserDomAdapter extends GenericBrowserDomAdapter {
// TODO(tbosch): move this into a separate environment class once we have it
logError(error) { window.console.error(error); }
log(error) { window.console.log(error); }
logGroup(error) {
if (window.console.group) {
window.console.group(error);
} else {
window.console.log(error);
}
}
logGroupEnd() {
if (window.console.groupEnd) {
window.console.groupEnd();
}
}
get attrToPropMap(): any { return _attrToPropMap; }
query(selector: string): any { return document.querySelector(selector); }

View File

@ -23,6 +23,9 @@ export class DomAdapter {
invoke(el: Element, methodName: string, args: List<any>): any { throw _abstract(); }
logError(error) { throw _abstract(); }
log(error) { throw _abstract(); }
logGroup(error) { throw _abstract(); }
logGroupEnd() { throw _abstract(); }
/**
* Maps attribute names to their corresponding property names for cases

View File

@ -27,6 +27,11 @@ class Html5LibDomAdapter implements DomAdapter {
stderr.writeln('${error}');
}
log(error) { stdout.writeln('${error}'); }
logGroup(error) { stdout.writeln('${error}'); }
logGroupEnd() { }
@override
final attrToPropMap = const {
'innerHtml': 'innerHTML',

View File

@ -47,6 +47,12 @@ export class Parse5DomAdapter extends DomAdapter {
logError(error) { console.error(error); }
log(error) { console.log(error); }
logGroup(error) { console.log(error); }
logGroupEnd() {}
get attrToPropMap() { return _attrToPropMap; }
query(selector) { throw _notImplemented('query'); }

View File

@ -119,7 +119,7 @@ function _getAppBindings() {
DirectiveResolver,
Parser,
Lexer,
ExceptionHandler,
bind(ExceptionHandler).toValue(new ExceptionHandler(DOM)),
bind(LocationStrategy).toClass(MockLocationStrategy),
bind(XHR).toClass(MockXHR),
ComponentUrlMapper,

View File

@ -22,6 +22,7 @@ import 'package:angular2/src/reflection/reflection_capabilities.dart';
import 'package:angular2/src/di/binding.dart' show bind;
import 'package:angular2/src/di/injector.dart' show Injector;
import 'package:angular2/src/core/exception_handler.dart' show ExceptionHandler;
import 'package:angular2/src/facade/collection.dart' show StringMapWrapper;
import 'test_injector.dart';
@ -77,13 +78,27 @@ Expect expect(actual, [matcher]) {
const _u = const Object();
expectErrorMessage(actual, expectedMessage) {
expect(ExceptionHandler.exceptionToString(actual)).toContain(expectedMessage);
}
expectException(Function actual, expectedMessage) {
try {
actual();
} catch (e, s) {
expectErrorMessage(e, expectedMessage);
}
}
class Expect extends gns.Expect {
Expect(actual) : super(actual);
NotExpect get not => new NotExpect(actual);
void toEqual(expected) => toHaveSameProps(expected);
void toContainError(message) => expectErrorMessage(this.actual, message);
void toThrowError([message = ""]) => toThrowWith(message: message);
void toThrowErrorWith(message) => expectException(this.actual, message);
void toBePromise() => gns.guinness.matchers.toBeTrue(actual is Future);
void toImplement(expected) => toBeA(expected);
void toBeNaN() => gns.guinness.matchers.toBeTrue(double.NAN.compareTo(actual) == 0);

View File

@ -6,6 +6,7 @@ import {global} from 'angular2/src/facade/lang';
import {NgZoneZone} from 'angular2/src/core/zone/ng_zone';
import {bind} from 'angular2/di';
import {ExceptionHandler} from 'angular2/src/core/exception_handler';
import {createTestInjector, FunctionWithParamTokens, inject} from './test_injector';
@ -24,6 +25,8 @@ export interface NgMatchers extends jasmine.Matchers {
toBeAnInstanceOf(expected: any): boolean;
toHaveText(expected: any): boolean;
toImplement(expected: any): boolean;
toContainError(expected: any): boolean;
toThrowErrorWith(expectedMessage: any): boolean;
not: NgMatchers;
}
@ -240,6 +243,38 @@ _global.beforeEach(function() {
};
},
toContainError: function() {
return {
compare: function(actual, expectedText) {
var errorMessage = ExceptionHandler.exceptionToString(actual);
return {
pass: errorMessage.indexOf(expectedText) > -1,
get message() { return 'Expected ' + errorMessage + ' to contain ' + expectedText; }
};
}
};
},
toThrowErrorWith: function() {
return {
compare: function(actual, expectedText) {
try {
actual();
return {
pass: false,
get message() { return "Was expected to throw, but did not throw"; }
};
} catch (e) {
var errorMessage = ExceptionHandler.exceptionToString(e);
return {
pass: errorMessage.indexOf(expectedText) > -1,
get message() { return 'Expected ' + errorMessage + ' to contain ' + expectedText; }
};
}
}
};
},
toImplement: function() {
return {
compare: function(actualObject, expectedInterface) {

View File

@ -31,6 +31,7 @@ import {AnchorBasedAppRootUrl} from 'angular2/src/services/anchor_based_app_root
import {ExceptionHandler} from 'angular2/src/core/exception_handler';
import {Injectable} from 'angular2/di';
import {BrowserDomAdapter} from 'angular2/src/dom/browser_adapter';
import {DOM} from 'angular2/src/dom/dom_adapter';
/**
* Creates a zone, sets up the DI bindings
@ -38,7 +39,7 @@ import {BrowserDomAdapter} from 'angular2/src/dom/browser_adapter';
*/
export function bootstrapUICommon(bus: MessageBus) {
BrowserDomAdapter.makeCurrent();
var zone = createNgZone(new ExceptionHandler());
var zone = createNgZone(new ExceptionHandler(DOM));
zone.run(() => {
var injector = createInjector(zone);
var webWorkerMain = injector.get(WebWorkerMain);

View File

@ -62,13 +62,18 @@ import {RenderProtoViewRefStore} from 'angular2/src/web-workers/shared/render_pr
import {
RenderViewWithFragmentsStore
} from 'angular2/src/web-workers/shared/render_view_with_fragments_store';
import {WorkerExceptionHandler} from 'angular2/src/web-workers/worker/exception_handler';
var _rootInjector: Injector;
// Contains everything that is safe to share between applications.
var _rootBindings = [bind(Reflector).toValue(reflector)];
class PrintLogger {
log = print;
logGroup = print;
logGroupEnd() {}
}
function _injectorBindings(appComponentType, bus: WorkerMessageBus,
initData: StringMap<string, any>): List<Type | Binding | List<any>> {
var bestChangeDetection: Type = DynamicChangeDetection;
@ -118,8 +123,7 @@ function _injectorBindings(appComponentType, bus: WorkerMessageBus,
DirectiveResolver,
Parser,
Lexer,
WorkerExceptionHandler,
bind(ExceptionHandler).toAlias(WorkerExceptionHandler),
bind(ExceptionHandler).toFactory(() => {new ExceptionHandler(new PrintLogger())}),
bind(XHR).toValue(new XHRImpl()),
ComponentUrlMapper,
UrlResolver,
@ -135,7 +139,7 @@ export function bootstrapWebworkerCommon(
componentInjectableBindings: List<Type | Binding | List<any>> = null): Promise<ApplicationRef> {
var bootstrapProcess = PromiseWrapper.completer();
var zone = createNgZone(new WorkerExceptionHandler());
var zone = createNgZone(new ExceptionHandler(new PrintLogger()));
zone.run(() => {
// TODO(rado): prepopulate template cache, so applications with only
// index.html and main.js are possible.

View File

@ -1,35 +0,0 @@
import {isPresent, print, BaseException} from 'angular2/src/facade/lang';
import {ListWrapper, isListLikeIterable} from 'angular2/src/facade/collection';
import {ExceptionHandler} from 'angular2/src/core/exception_handler';
import {Injectable} from 'angular2/di';
@Injectable()
export class WorkerExceptionHandler implements ExceptionHandler {
logError: Function = print;
call(exception: Object, stackTrace: any = null, reason: string = null) {
var longStackTrace = isListLikeIterable(stackTrace) ?
(<any>stackTrace).join("\n\n-----async gap-----\n") :
stackTrace;
this.logError(`${exception}\n\n${longStackTrace}`);
if (isPresent(reason)) {
this.logError(`Reason: ${reason}`);
}
var context = this._findContext(exception);
if (isPresent(context)) {
this.logError("Error Context:");
this.logError(context);
}
throw exception;
}
_findContext(exception: any): any {
if (!(exception instanceof BaseException)) return null;
return isPresent(exception.context) ? exception.context :
this._findContext(exception.originalException);
}
}

View File

@ -132,7 +132,7 @@ export function main() {
Injector.resolveAndCreate([Pipes.extend({'async': [secondPipeFactory]})]);
expect(() => injector.get(Pipes))
.toThrowError(/Cannot extend Pipes without a parent injector/g);
.toThrowErrorWith("Cannot extend Pipes without a parent injector");
});
it('should extend di-inherited pipes', () => {

View File

@ -18,6 +18,7 @@ import {DOM} from 'angular2/src/dom/dom_adapter';
import {PromiseWrapper} from 'angular2/src/facade/async';
import {bind, Inject, Injector} from 'angular2/di';
import {LifeCycle} from 'angular2/src/core/life_cycle/life_cycle';
import {ExceptionHandler} from 'angular2/src/core/exception_handler';
import {Testability, TestabilityRegistry} from 'angular2/src/core/testability/testability';
import {DOCUMENT_TOKEN} from 'angular2/src/render/dom/dom_renderer';
@ -65,6 +66,12 @@ class HelloRootMissingTemplate {
class HelloRootDirectiveIsNotCmp {
}
class _NilLogger {
log(s: any): void {}
logGroup(s: any): void {}
logGroupEnd(){};
}
export function main() {
var fakeDoc, el, el2, testBindings, lightDom;
@ -85,9 +92,8 @@ export function main() {
inject([AsyncTestCompleter], (async) => {
var refPromise = bootstrap(HelloRootDirectiveIsNotCmp, testBindings);
PromiseWrapper.then(refPromise, null, (reason) => {
expect(reason.message)
.toContain(
PromiseWrapper.then(refPromise, null, (exception) => {
expect(exception).toContainError(
`Could not load '${stringify(HelloRootDirectiveIsNotCmp)}' because it is not a component.`);
async.done();
return null;
@ -95,7 +101,10 @@ export function main() {
}));
it('should throw if no element is found', inject([AsyncTestCompleter], (async) => {
var refPromise = bootstrap(HelloRootCmp, []);
// do not print errors to the console
var e = new ExceptionHandler(new _NilLogger());
var refPromise = bootstrap(HelloRootCmp, [bind(ExceptionHandler).toValue(e)]);
PromiseWrapper.then(refPromise, null, (reason) => {
expect(reason.message).toContain('The selector "hello-app" did not match any elements');
async.done();

View File

@ -67,8 +67,8 @@ main() {
directives: [ThrowingComponent]))
.createAsync(Dummy).catchError((e, stack) {
expect(e.message).toContain("MockException");
expect(e.message).toContain("functionThatThrows");
expect(e).toContainError("MockException");
expect(e).toContainError("functionThatThrows");
async.done();
});
}));
@ -82,8 +82,8 @@ main() {
directives: [ThrowingComponent2]))
.createAsync(Dummy).catchError((e, stack) {
expect(e.message).toContain("NonError");
expect(e.message).toContain("functionThatThrows");
expect(e).toContainError("NonError");
expect(e).toContainError("functionThatThrows");
async.done();
});
}));

View File

@ -17,74 +17,78 @@ import {ExceptionHandler} from 'angular2/src/core/exception_handler';
class _CustomException {
context = "some context";
toString(): string { return "custom"; }
}
export function main() {
describe('ExceptionHandler', () => {
var log, handler;
beforeEach(() => {
log = new Log();
handler = new ExceptionHandler();
handler.logError = (e) => log.add(e);
});
it("should output exception", () => {
try {
handler.call(new BaseException("message!"));
} catch (e) {
}
expect(log.result()).toContain("message!");
var e = ExceptionHandler.exceptionToString(new BaseException("message!"));
expect(e).toContain("message!");
});
it("should output stackTrace", () => {
try {
handler.call(new BaseException("message!"), "stack!");
} catch (e) {
}
expect(log.result()).toContain("stack!");
var e = ExceptionHandler.exceptionToString(new BaseException("message!"), "stack!");
expect(e).toContain("stack!");
});
it("should join a long stackTrace", () => {
try {
handler.call(new BaseException("message!"), ["stack1", "stack2"]);
} catch (e) {
}
expect(log.result()).toContain("stack1");
expect(log.result()).toContain("stack2");
var e =
ExceptionHandler.exceptionToString(new BaseException("message!"), ["stack1", "stack2"]);
expect(e).toContain("stack1");
expect(e).toContain("stack2");
});
it("should output reason when present", () => {
try {
handler.call(new BaseException("message!"), null, "reason!");
} catch (e) {
}
expect(log.result()).toContain("reason!");
var e = ExceptionHandler.exceptionToString(new BaseException("message!"), null, "reason!");
expect(e).toContain("reason!");
});
describe("context", () => {
it("should print context", () => {
try {
handler.call(new BaseException("message!", null, null, "context!"));
} catch (e) {
}
expect(log.result()).toContain("context!");
var e = ExceptionHandler.exceptionToString(
new BaseException("message!", null, null, "context!"));
expect(e).toContain("context!");
});
it("should print nested context", () => {
try {
var original = new BaseException("message!", null, null, "context!");
handler.call(new BaseException("message", original));
} catch (e) {
}
expect(log.result()).toContain("context!");
var e = ExceptionHandler.exceptionToString(new BaseException("message", original));
expect(e).toContain("context!");
});
it("should not print context when the passed-in exception is not a BaseException", () => {
try {
handler.call(new _CustomException());
} catch (e) {
}
expect(log.result()).not.toContain("context");
var e = ExceptionHandler.exceptionToString(new _CustomException());
expect(e).not.toContain("context");
});
});
describe('original exception', () => {
it("should print original exception message if available (original is BaseException)", () => {
var realOriginal = new BaseException("inner");
var original = new BaseException("wrapped", realOriginal);
var e = ExceptionHandler.exceptionToString(new BaseException("wrappedwrapped", original));
expect(e).toContain("inner");
});
it("should print original exception message if available (original is not BaseException)",
() => {
var realOriginal = new _CustomException();
var original = new BaseException("wrapped", realOriginal);
var e =
ExceptionHandler.exceptionToString(new BaseException("wrappedwrapped", original));
expect(e).toContain("custom");
});
});
describe('original stack', () => {
it("should print original stack if available", () => {
var realOriginal = new BaseException("inner");
var original = new BaseException("wrapped", realOriginal, "originalStack");
var e = ExceptionHandler.exceptionToString(
new BaseException("wrappedwrapped", original, "wrappedStack"));
expect(e).toContain("originalStack");
});
});
});
}

View File

@ -74,7 +74,7 @@ export function main() {
var router = rootTC.componentInstance.router;
PromiseWrapper.catchError(router.navigate('/cause-error'), (error) => {
expect(rootTC.nativeElement).toHaveText('outer { oh no }');
expect(error.message).toContain('oops!');
expect(error).toContainError('oops!');
async.done();
});
});