Attempt at normalizing error stacks across browsers.

Failed expectations now have a `stack` property, remove `trace.stack`
This commit is contained in:
Dan Hansen and Davis W. Frank
2013-02-27 16:37:31 -08:00
parent dc4563d45c
commit d6da13a8dd
12 changed files with 237 additions and 132 deletions
+1 -2
View File
@@ -79,9 +79,8 @@ jasmine.HtmlReporter = function(options) {
for (var i = 0; i < result.failedExpectations.length; i++) { for (var i = 0; i < result.failedExpectations.length; i++) {
var expectation = result.failedExpectations[i]; var expectation = result.failedExpectations[i];
var stack = (expectation.trace && expectation.trace.stack) || "";
messages.appendChild(createDom("div", {className: "result-message"}, expectation.message)); messages.appendChild(createDom("div", {className: "result-message"}, expectation.message));
messages.appendChild(createDom("div", {className: "stack-trace"}, stack)); messages.appendChild(createDom("div", {className: "stack-trace"}, expectation.stack));
} }
failures.push(failure); failures.push(failure);
+70 -32
View File
@@ -439,29 +439,66 @@ jasmine.util.argsToArray = function(args) {
var arrayOfArgs = []; var arrayOfArgs = [];
for (var i = 0; i < args.length; i++) arrayOfArgs.push(args[i]); for (var i = 0; i < args.length; i++) arrayOfArgs.push(args[i]);
return arrayOfArgs; return arrayOfArgs;
};jasmine.exceptionFormatter = function(e) { };jasmine.ExceptionFormatter = function() {
var message = e.name this.message = function(error) {
var message = error.name
+ ': ' + ': '
+ e.message + error.message;
+ ' in '
+ (e.fileName || e.sourceURL || '') if (error.fileName || error.sourceURL) {
+ ' (line ' message += " in " + (error.fileName || error.sourceURL);
+ (e.line || e.lineNumber || '') }
+ ')';
if (error.line || error.lineNumber) {
message += " (line " + (error.line || error.lineNumber) + ")"
}
return message; return message;
}; };
//TODO: expectation result may make more sense as a presentation of an expectation.
jasmine.buildExpectationResult = function(params) { this.stack = function(error) {
return error ? error.stack : null;
}
};//TODO: expectation result may make more sense as a presentation of an expectation.
jasmine.buildExpectationResult = function(options) {
var messageFormatter = options.messageFormatter || function() {},
stackFormatter = options.stackFormatter || function() {};
return { return {
type: 'expect', matcherName: options.matcherName,
matcherName: params.matcherName, expected: options.expected,
expected: params.expected, actual: options.actual,
actual: params.actual, message: message(),
message: params.passed ? 'Passed.' : params.message, stack: stack(),
trace: params.passed ? '' : (params.trace || new Error(this.message)), passed: options.passed
passed: params.passed
}; };
function message() {
if (options.passed) {
return "Passed."
} else if (options.message) {
return options.message
} else if (options.error) {
return messageFormatter(options.error);
}
return ""
}
function stack() {
if (options.passed) {
return "";
}
var error = options.error;
if (!error) {
try {
throw new Error(message());
} catch (e) {
error = e;
}
}
return stackFormatter(error);
}
}; };
(function() { (function() {
jasmine.Env = function(options) { jasmine.Env = function(options) {
@@ -536,16 +573,19 @@ jasmine.buildExpectationResult = function(params) {
} }
}; };
var exceptionFormatter = jasmine.exceptionFormatter;
var specConstructor = jasmine.Spec; var specConstructor = jasmine.Spec;
var getSpecName = function(spec, currentSuite) { var getSpecName = function(spec, currentSuite) {
return currentSuite.getFullName() + ' ' + spec.description + '.'; return currentSuite.getFullName() + ' ' + spec.description + '.';
}; };
var buildExpectationResult = jasmine.buildExpectationResult; // TODO: we may just be able to pass in the fn instead of wrapping here
var expectationResultFactory = function(attrs) { var buildExpectationResult = jasmine.buildExpectationResult,
exceptionFormatter = new jasmine.ExceptionFormatter(),
expectationResultFactory = function(attrs) {
attrs.messageFormatter = exceptionFormatter.message;
attrs.stackFormatter = exceptionFormatter.stack;
return buildExpectationResult(attrs); return buildExpectationResult(attrs);
}; };
@@ -1043,21 +1083,19 @@ jasmine.Matchers.matcherFn_ = function(matcherName, matcherFunction) {
message += "."; message += ".";
} }
} }
var expectationResult = jasmine.buildExpectationResult({
this.spec.addExpectationResult(result, {
matcherName: matcherName, matcherName: matcherName,
passed: result, passed: result,
expected: matcherArgs.length > 1 ? matcherArgs : matcherArgs[0], expected: matcherArgs.length > 1 ? matcherArgs : matcherArgs[0],
actual: this.actual, actual: this.actual,
message: message message: message
}); });
this.spec.addExpectationResult(result, expectationResult);
return jasmine.undefined; return jasmine.undefined;
}; };
}; };
/** /**
* toBe: compares the actual to the expected using === * toBe: compares the actual to the expected using ===
* @param expected * @param expected
@@ -1583,9 +1621,10 @@ jasmine.Spec = function(attrs) {
jasmine.Spec.prototype.addExpectationResult = function(passed, data) { jasmine.Spec.prototype.addExpectationResult = function(passed, data) {
this.encounteredExpectations = true; this.encounteredExpectations = true;
if (!passed) { if (passed) {
this.result.failedExpectations.push(data); return;
} }
this.result.failedExpectations.push(this.expectationResultFactory(data));
}; };
jasmine.Spec.prototype.expect = function(actual) { jasmine.Spec.prototype.expect = function(actual) {
@@ -1608,14 +1647,13 @@ jasmine.Spec.prototype.execute = function(onComplete) {
this.queueRunner({ this.queueRunner({
fns: allFns, fns: allFns,
onException: function(e) { onException: function(e) {
self.addExpectationResult(false, self.expectationResultFactory({ self.addExpectationResult(false, {
matcherName: "", matcherName: "",
passed: false, passed: false,
expected: "", expected: "",
actual: "", actual: "",
message: self.exceptionFormatter(e), error: e
trace: e });
}));
}, },
onComplete: complete onComplete: complete
}); });
@@ -1652,7 +1690,7 @@ jasmine.Spec.prototype.status = function() {
jasmine.Spec.prototype.getFullName = function() { jasmine.Spec.prototype.getFullName = function() {
return this.getSpecName(this); return this.getSpecName(this);
} };
jasmine.Suite = function(attrs) { jasmine.Suite = function(attrs) {
this.env = attrs.env; this.env = attrs.env;
this.id = attrs.id; this.id = attrs.id;
+31 -5
View File
@@ -1,13 +1,14 @@
describe("ExceptionFormatter", function() { describe("ExceptionFormatter", function() {
describe("#message", function() {
it('formats Firefox exception messages', function() { it('formats Firefox exception messages', function() {
var sampleFirefoxException = { var sampleFirefoxException = {
fileName: 'foo.js', fileName: 'foo.js',
line: '1978', lineNumber: '1978',
message: 'you got your foo in my bar', message: 'you got your foo in my bar',
name: 'A Classic Mistake' name: 'A Classic Mistake'
}, },
message = jasmine.exceptionMessageFor(sampleFirefoxException); exceptionFormatter = new jasmine.ExceptionFormatter(),
message = exceptionFormatter.message(sampleFirefoxException);
expect(message).toEqual('A Classic Mistake: you got your foo in my bar in foo.js (line 1978)'); expect(message).toEqual('A Classic Mistake: you got your foo in my bar in foo.js (line 1978)');
}); });
@@ -15,12 +16,37 @@ describe("ExceptionFormatter", function() {
it('formats Webkit exception messages', function() { it('formats Webkit exception messages', function() {
var sampleWebkitException = { var sampleWebkitException = {
sourceURL: 'foo.js', sourceURL: 'foo.js',
lineNumber: '1978', line: '1978',
message: 'you got your foo in my bar', message: 'you got your foo in my bar',
name: 'A Classic Mistake' name: 'A Classic Mistake'
}, },
message = jasmine.exceptionMessageFor(sampleWebkitException); exceptionFormatter = new jasmine.ExceptionFormatter(),
message = exceptionFormatter.message(sampleWebkitException);
expect(message).toEqual('A Classic Mistake: you got your foo in my bar in foo.js (line 1978)'); expect(message).toEqual('A Classic Mistake: you got your foo in my bar in foo.js (line 1978)');
}); });
it('formats V8 exception messages', function() {
var sampleV8 = {
message: 'you got your foo in my bar',
name: 'A Classic Mistake'
},
exceptionFormatter = new jasmine.ExceptionFormatter(),
message = exceptionFormatter.message(sampleV8);
expect(message).toEqual('A Classic Mistake: you got your foo in my bar');
});
});
describe("#stack", function() {
it("formats stack traces from Webkit, Firefox or node.js", function() {
var error = new Error("an error");
expect(new jasmine.ExceptionFormatter().stack(error)).toMatch(/ExceptionFormatterSpec\.js.*\d+/)
});
it("returns null if no Error provided", function() {
expect(new jasmine.ExceptionFormatter().stack()).toBeNull();
});
});
}); });
+27 -14
View File
@@ -1,33 +1,47 @@
describe("buildExpectationResult", function() { describe("buildExpectationResult", function() {
it("defaults to passed", function() { it("defaults to passed", function() {
var result = jasmine.buildExpectationResult({passed: 'some-value'}); var result = jasmine.buildExpectationResult({passed: 'some-value'});
expect(result.passed).toBe('some-value'); expect(result.passed).toBe('some-value');
}); });
it("has a type of expect", function() {
var result = jasmine.buildExpectationResult({});
expect(result.type).toBe('expect');
});
it("message defaults to Passed for passing specs", function() { it("message defaults to Passed for passing specs", function() {
var result = jasmine.buildExpectationResult({passed: true, message: 'some-value'}); var result = jasmine.buildExpectationResult({passed: true, message: 'some-value'});
expect(result.message).toBe('Passed.'); expect(result.message).toBe('Passed.');
}); });
it("message returns the message for failing specs", function() { it("message returns the message for failing expecations", function() {
var result = jasmine.buildExpectationResult({passed: false, message: 'some-value'}); var result = jasmine.buildExpectationResult({passed: false, message: 'some-value'});
expect(result.message).toBe('some-value'); expect(result.message).toBe('some-value');
}); });
it("trace passes trace if exists", function() { it("delegates message formatting to the provided formatter if there was an Error", function() {
var result = jasmine.buildExpectationResult({trace: 'some-value'}); var fakeError = {message: 'foo'},
expect(result.trace).toBe('some-value'); messageFormatter = jasmine.createSpy("exception message formatter").andReturn(fakeError.message);
var result = jasmine.buildExpectationResult(
{
passed: false,
error: fakeError,
messageFormatter: messageFormatter
}); });
it("trace returns a new error if trace is falsy", function() { expect(messageFormatter).toHaveBeenCalledWith(fakeError);
var result = jasmine.buildExpectationResult({trace: false}); expect(result.message).toEqual('foo');
expect(result.trace).toEqual(jasmine.any(Error)); });
it("delegates stack formatting to the provided formatter if there was an Error", function() {
var fakeError = {stack: 'foo'},
stackFormatter = jasmine.createSpy("stack formatter").andReturn(fakeError.stack);
var result = jasmine.buildExpectationResult(
{
passed: false,
error: fakeError,
stackFormatter: stackFormatter
});
expect(stackFormatter).toHaveBeenCalledWith(fakeError);
expect(result.stack).toEqual('foo');
}); });
it("matcherName returns passed matcherName", function() { it("matcherName returns passed matcherName", function() {
@@ -44,5 +58,4 @@ describe("buildExpectationResult", function() {
var result = jasmine.buildExpectationResult({actual: 'some-value'}); var result = jasmine.buildExpectationResult({actual: 'some-value'});
expect(result.actual).toBe('some-value'); expect(result.actual).toBe('some-value');
}); });
}); });
-7
View File
@@ -660,9 +660,6 @@ describe("jasmine.Matchers", function() {
}); });
it("should provide an inverted default message", function() { it("should provide an inverted default message", function() {
match(37).not.toBeGreaterThan(42);
expect(lastResult().message).toEqual("Passed.");
match(42).not.toBeGreaterThan(37); match(42).not.toBeGreaterThan(37);
expect(lastResult().message).toEqual("Expected 42 not to be greater than 37."); expect(lastResult().message).toEqual("Expected 42 not to be greater than 37.");
}); });
@@ -677,14 +674,10 @@ describe("jasmine.Matchers", function() {
} }
}); });
match(true).custom();
expect(lastResult().message).toEqual("Passed.");
match(false).custom(); match(false).custom();
expect(lastResult().message).toEqual("Expected it was called."); expect(lastResult().message).toEqual("Expected it was called.");
match(true).not.custom(); match(true).not.custom();
expect(lastResult().message).toEqual("Expected it wasn't called."); expect(lastResult().message).toEqual("Expected it wasn't called.");
match(false).not.custom();
expect(lastResult().message).toEqual("Passed.");
}); });
}); });
-2
View File
@@ -369,10 +369,8 @@ describe("New HtmlReporter", function() {
failedExpectations: [ failedExpectations: [
{ {
message: "a failure message", message: "a failure message",
trace: {
stack: "a stack trace" stack: "a stack trace"
} }
}
] ]
}; };
reporter.specStarted(failingResult); reporter.specStarted(failingResult);
+7 -4
View File
@@ -71,16 +71,19 @@
} }
}; };
var exceptionFormatter = jasmine.exceptionFormatter;
var specConstructor = jasmine.Spec; var specConstructor = jasmine.Spec;
var getSpecName = function(spec, currentSuite) { var getSpecName = function(spec, currentSuite) {
return currentSuite.getFullName() + ' ' + spec.description + '.'; return currentSuite.getFullName() + ' ' + spec.description + '.';
}; };
var buildExpectationResult = jasmine.buildExpectationResult; // TODO: we may just be able to pass in the fn instead of wrapping here
var expectationResultFactory = function(attrs) { var buildExpectationResult = jasmine.buildExpectationResult,
exceptionFormatter = new jasmine.ExceptionFormatter(),
expectationResultFactory = function(attrs) {
attrs.messageFormatter = exceptionFormatter.message;
attrs.stackFormatter = exceptionFormatter.stack;
return buildExpectationResult(attrs); return buildExpectationResult(attrs);
}; };
+17 -8
View File
@@ -1,12 +1,21 @@
jasmine.exceptionMessageFor = function(e) { jasmine.ExceptionFormatter = function() {
var message = e.name this.message = function(error) {
var message = error.name
+ ': ' + ': '
+ e.message + error.message;
+ ' in '
+ (e.fileName || e.sourceURL || '') if (error.fileName || error.sourceURL) {
+ ' (line ' message += " in " + (error.fileName || error.sourceURL);
+ (e.line || e.lineNumber || '') }
+ ')';
if (error.line || error.lineNumber) {
message += " (line " + (error.line || error.lineNumber) + ")"
}
return message; return message;
}; };
this.stack = function(error) {
return error ? error.stack : null;
}
};
+37 -8
View File
@@ -1,12 +1,41 @@
//TODO: expectation result may make more sense as a presentation of an expectation. //TODO: expectation result may make more sense as a presentation of an expectation.
jasmine.buildExpectationResult = function(params) { jasmine.buildExpectationResult = function(options) {
var messageFormatter = options.messageFormatter || function() {},
stackFormatter = options.stackFormatter || function() {};
return { return {
type: 'expect', matcherName: options.matcherName,
matcherName: params.matcherName, expected: options.expected,
expected: params.expected, actual: options.actual,
actual: params.actual, message: message(),
message: params.passed ? 'Passed.' : params.message, stack: stack(),
trace: params.passed ? '' : (params.trace || new Error(this.message)), passed: options.passed
passed: params.passed
}; };
function message() {
if (options.passed) {
return "Passed."
} else if (options.message) {
return options.message
} else if (options.error) {
return messageFormatter(options.error);
}
return ""
}
function stack() {
if (options.passed) {
return "";
}
var error = options.error;
if (!error) {
try {
throw new Error(message());
} catch (e) {
error = e;
}
}
return stackFormatter(error);
}
}; };
+2 -4
View File
@@ -53,21 +53,19 @@ jasmine.Matchers.matcherFn_ = function(matcherName, matcherFunction) {
message += "."; message += ".";
} }
} }
var expectationResult = jasmine.buildExpectationResult({
this.spec.addExpectationResult(result, {
matcherName: matcherName, matcherName: matcherName,
passed: result, passed: result,
expected: matcherArgs.length > 1 ? matcherArgs : matcherArgs[0], expected: matcherArgs.length > 1 ? matcherArgs : matcherArgs[0],
actual: this.actual, actual: this.actual,
message: message message: message
}); });
this.spec.addExpectationResult(result, expectationResult);
return jasmine.undefined; return jasmine.undefined;
}; };
}; };
/** /**
* toBe: compares the actual to the expected using === * toBe: compares the actual to the expected using ===
* @param expected * @param expected
+7 -7
View File
@@ -26,9 +26,10 @@ jasmine.Spec = function(attrs) {
jasmine.Spec.prototype.addExpectationResult = function(passed, data) { jasmine.Spec.prototype.addExpectationResult = function(passed, data) {
this.encounteredExpectations = true; this.encounteredExpectations = true;
if (!passed) { if (passed) {
this.result.failedExpectations.push(data); return;
} }
this.result.failedExpectations.push(this.expectationResultFactory(data));
}; };
jasmine.Spec.prototype.expect = function(actual) { jasmine.Spec.prototype.expect = function(actual) {
@@ -51,14 +52,13 @@ jasmine.Spec.prototype.execute = function(onComplete) {
this.queueRunner({ this.queueRunner({
fns: allFns, fns: allFns,
onException: function(e) { onException: function(e) {
self.addExpectationResult(false, self.expectationResultFactory({ self.addExpectationResult(false, {
matcherName: "", matcherName: "",
passed: false, passed: false,
expected: "", expected: "",
actual: "", actual: "",
message: self.exceptionFormatter(e), error: e
trace: e });
}));
}, },
onComplete: complete onComplete: complete
}); });
@@ -95,4 +95,4 @@ jasmine.Spec.prototype.status = function() {
jasmine.Spec.prototype.getFullName = function() { jasmine.Spec.prototype.getFullName = function() {
return this.getSpecName(this); return this.getSpecName(this);
} };
+1 -2
View File
@@ -79,9 +79,8 @@ jasmine.HtmlReporter = function(options) {
for (var i = 0; i < result.failedExpectations.length; i++) { for (var i = 0; i < result.failedExpectations.length; i++) {
var expectation = result.failedExpectations[i]; var expectation = result.failedExpectations[i];
var stack = (expectation.trace && expectation.trace.stack) || "";
messages.appendChild(createDom("div", {className: "result-message"}, expectation.message)); messages.appendChild(createDom("div", {className: "result-message"}, expectation.message));
messages.appendChild(createDom("div", {className: "stack-trace"}, stack)); messages.appendChild(createDom("div", {className: "stack-trace"}, expectation.stack));
} }
failures.push(failure); failures.push(failure);