When stop on failure is enabled, skip subsequent it() and beforeEach().

Note: afterEach() functions are still run, because skipping them is
highly likely to pollute specs that run after the failure.

[Finishes #92252330]

- Fixes #577
- Fixes #807
This commit is contained in:
Steve Gravrock
2017-06-15 17:30:00 -07:00
parent de862f8133
commit 585287b9d6
7 changed files with 368 additions and 56 deletions

View File

@@ -473,10 +473,12 @@ getJasmineRequireObj().Spec = function(j$) {
} }
var fns = this.beforeAndAfterFns(); var fns = this.beforeAndAfterFns();
var allFns = fns.befores.concat(this.queueableFn).concat(fns.afters); var regularFns = fns.befores.concat(this.queueableFn);
this.queueRunnerFactory({ this.queueRunnerFactory({
queueableFns: allFns, isLeaf: true,
queueableFns: regularFns,
cleanupFns: fns.afters,
onException: function() { self.onException.apply(self, arguments); }, onException: function() { self.onException.apply(self, arguments); },
onComplete: complete, onComplete: complete,
userContext: this.userContext() userContext: this.userContext()
@@ -817,6 +819,7 @@ getJasmineRequireObj().Env = function(j$) {
options.timeout = {setTimeout: realSetTimeout, clearTimeout: realClearTimeout}; options.timeout = {setTimeout: realSetTimeout, clearTimeout: realClearTimeout};
options.fail = self.fail; options.fail = self.fail;
options.globalErrors = globalErrors; options.globalErrors = globalErrors;
options.completeOnFirstError = throwOnExpectationFailure && options.isLeaf;
new j$.QueueRunner(options).execute(); new j$.QueueRunner(options).execute();
}; };
@@ -1141,6 +1144,7 @@ getJasmineRequireObj().Env = function(j$) {
this.afterEach = function(afterEachFunction, timeout) { this.afterEach = function(afterEachFunction, timeout) {
ensureIsFunctionOrAsync(afterEachFunction, 'afterEach'); ensureIsFunctionOrAsync(afterEachFunction, 'afterEach');
afterEachFunction.isCleanup = true;
currentDeclarationSuite.afterEach({ currentDeclarationSuite.afterEach({
fn: afterEachFunction, fn: afterEachFunction,
timeout: function() { return timeout || j$.DEFAULT_TIMEOUT_INTERVAL; } timeout: function() { return timeout || j$.DEFAULT_TIMEOUT_INTERVAL; }
@@ -1189,6 +1193,10 @@ getJasmineRequireObj().Env = function(j$) {
message: message, message: message,
error: error && error.message ? error : null error: error && error.message ? error : null
}); });
if (self.throwingExpectationFailures()) {
throw new Error(message);
}
}; };
} }
@@ -3934,7 +3942,9 @@ getJasmineRequireObj().QueueRunner = function(j$) {
} }
function QueueRunner(attrs) { function QueueRunner(attrs) {
this.queueableFns = attrs.queueableFns || []; var queueableFns = attrs.queueableFns || [];
this.queueableFns = queueableFns.concat(attrs.cleanupFns || []);
this.firstCleanupIx = queueableFns.length;
this.onComplete = attrs.onComplete || function() {}; this.onComplete = attrs.onComplete || function() {};
this.clearStack = attrs.clearStack || function(fn) {fn();}; this.clearStack = attrs.clearStack || function(fn) {fn();};
this.onException = attrs.onException || function() {}; this.onException = attrs.onException || function() {};
@@ -3943,6 +3953,7 @@ getJasmineRequireObj().QueueRunner = function(j$) {
this.timeout = attrs.timeout || {setTimeout: setTimeout, clearTimeout: clearTimeout}; this.timeout = attrs.timeout || {setTimeout: setTimeout, clearTimeout: clearTimeout};
this.fail = attrs.fail || function() {}; this.fail = attrs.fail || function() {};
this.globalErrors = attrs.globalErrors || { pushListener: function() {}, popListener: function() {} }; this.globalErrors = attrs.globalErrors || { pushListener: function() {}, popListener: function() {} };
this.completeOnFirstError = !!attrs.completeOnFirstError;
} }
QueueRunner.prototype.execute = function() { QueueRunner.prototype.execute = function() {
@@ -3951,20 +3962,32 @@ getJasmineRequireObj().QueueRunner = function(j$) {
self.onException(error); self.onException(error);
}; };
this.globalErrors.pushListener(this.handleFinalError); this.globalErrors.pushListener(this.handleFinalError);
this.run(this.queueableFns, 0); this.run(0);
}; };
QueueRunner.prototype.run = function(queueableFns, recursiveIndex) { QueueRunner.prototype.skipToCleanup = function(lastRanIndex) {
var length = queueableFns.length, if (lastRanIndex < this.firstCleanupIx) {
this.run(this.firstCleanupIx);
} else {
this.run(lastRanIndex + 1);
}
};
QueueRunner.prototype.run = function(recursiveIndex) {
var length = this.queueableFns.length,
self = this, self = this,
iterativeIndex; iterativeIndex;
for(iterativeIndex = recursiveIndex; iterativeIndex < length; iterativeIndex++) { for(iterativeIndex = recursiveIndex; iterativeIndex < length; iterativeIndex++) {
var queueableFn = queueableFns[iterativeIndex]; var result = attempt(iterativeIndex);
var completedSynchronously = attempt(queueableFn);
if (!completedSynchronously) { if (!result.completedSynchronously) {
return;
}
if (this.completeOnFirstError && result.errored) {
this.skipToCleanup(iterativeIndex);
return; return;
} }
} }
@@ -3974,7 +3997,7 @@ getJasmineRequireObj().QueueRunner = function(j$) {
self.onComplete(); self.onComplete();
}); });
function attempt(queueableFn) { function attempt() {
var clearTimeout = function () { var clearTimeout = function () {
Function.prototype.apply.apply(self.timeout.clearTimeout, [j$.getGlobal(), [timeoutId]]); Function.prototype.apply.apply(self.timeout.clearTimeout, [j$.getGlobal(), [timeoutId]]);
}, },
@@ -3992,18 +4015,28 @@ getJasmineRequireObj().QueueRunner = function(j$) {
}), }),
next = once(function () { next = once(function () {
cleanup(); cleanup();
function runNext() {
if (self.completeOnFirstError && errored) {
self.skipToCleanup(iterativeIndex);
} else {
self.run(iterativeIndex + 1);
}
}
if (completedSynchronously) { if (completedSynchronously) {
setTimeout(function() { setTimeout(runNext);
self.run(queueableFns, iterativeIndex + 1);
});
} else { } else {
self.run(queueableFns, iterativeIndex + 1); runNext();
} }
}), }),
errored = false,
queueableFn = self.queueableFns[iterativeIndex],
timeoutId; timeoutId;
next.fail = function() { next.fail = function() {
self.fail.apply(null, arguments); self.fail.apply(null, arguments);
errored = true;
next(); next();
}; };
@@ -4024,31 +4057,33 @@ getJasmineRequireObj().QueueRunner = function(j$) {
if (maybeThenable && j$.isFunction_(maybeThenable.then)) { if (maybeThenable && j$.isFunction_(maybeThenable.then)) {
maybeThenable.then(next, next.fail); maybeThenable.then(next, next.fail);
completedSynchronously = false; completedSynchronously = false;
return false; return { completedSynchronously: false };
} }
} else { } else {
queueableFn.fn.call(self.userContext, next); queueableFn.fn.call(self.userContext, next);
completedSynchronously = false; completedSynchronously = false;
return false; return { completedSynchronously: false };
} }
} catch (e) { } catch (e) {
handleException(e, queueableFn); handleException(e, queueableFn);
errored = true;
} }
cleanup(); cleanup();
return true; return { completedSynchronously: true, errored: errored };
}
function onException(e) { function onException(e) {
self.onException(e); self.onException(e);
} errored = true;
}
function handleException(e, queueableFn) { function handleException(e, queueableFn) {
onException(e); onException(e);
if (!self.catchException(e)) { if (!self.catchException(e)) {
//TODO: set a var when we catch an exception and //TODO: set a var when we catch an exception and
//use a finally block to close the loop in a nice way.. //use a finally block to close the loop in a nice way..
throw e; throw e;
}
} }
} }
}; };

View File

@@ -19,6 +19,26 @@ describe("QueueRunner", function() {
expect(calls).toEqual(['fn1', 'fn2']); expect(calls).toEqual(['fn1', 'fn2']);
}); });
it("runs cleanup functions after the others", function() {
var calls = [],
queueableFn1 = { fn: jasmine.createSpy('fn1') },
queueableFn2 = { fn: jasmine.createSpy('fn2') },
queueRunner = new jasmineUnderTest.QueueRunner({
queueableFns: [queueableFn1],
cleanupFns: [queueableFn2]
});
queueableFn1.fn.and.callFake(function() {
calls.push('fn1');
});
queueableFn2.fn.and.callFake(function() {
calls.push('fn2');
});
queueRunner.execute();
expect(calls).toEqual(['fn1', 'fn2']);
});
it("calls each function with a consistent 'this'-- an empty object", function() { it("calls each function with a consistent 'this'-- an empty object", function() {
var queueableFn1 = { fn: jasmine.createSpy('fn1') }, var queueableFn1 = { fn: jasmine.createSpy('fn1') },
queueableFn2 = { fn: jasmine.createSpy('fn2') }, queueableFn2 = { fn: jasmine.createSpy('fn2') },
@@ -421,6 +441,87 @@ describe("QueueRunner", function() {
expect(nextQueueableFn.fn).toHaveBeenCalled(); expect(nextQueueableFn.fn).toHaveBeenCalled();
}); });
describe("When configured to complete on first error", function() {
it("skips to cleanup functions on the first exception", function() {
var queueableFn = { fn: function() { throw new Error("error"); } },
nextQueueableFn = { fn: jasmine.createSpy("nextFunction") },
cleanupFn = { fn: jasmine.createSpy("cleanup") },
queueRunner = new jasmineUnderTest.QueueRunner({
queueableFns: [queueableFn, nextQueueableFn],
cleanupFns: [cleanupFn],
completeOnFirstError: true
});
queueRunner.execute();
expect(nextQueueableFn.fn).not.toHaveBeenCalled();
expect(cleanupFn.fn).toHaveBeenCalled();
});
it("does not skip when a cleanup function throws", function() {
var queueableFn = { fn: function() { } },
cleanupFn1 = { fn: function() { throw new Error("error"); } },
cleanupFn2 = { fn: jasmine.createSpy("cleanupFn2") },
queueRunner = new jasmineUnderTest.QueueRunner({
queueableFns: [queueableFn],
cleanupFns: [cleanupFn1, cleanupFn2],
completeOnFirstError: true
});
queueRunner.execute();
expect(cleanupFn2.fn).toHaveBeenCalled();
});
describe("with an asynchronous function", function() {
beforeEach(function() {
jasmine.clock().install();
});
afterEach(function() {
jasmine.clock().uninstall();
});
it("skips to cleanup functions on the first exception", function() {
var errorListeners = [],
queueableFn = { fn: function(done) {} },
nextQueueableFn = { fn: jasmine.createSpy('nextFunction') },
cleanupFn = { fn: jasmine.createSpy('cleanup') },
queueRunner = new jasmineUnderTest.QueueRunner({
globalErrors: {
pushListener: function(f) { errorListeners.push(f); },
popListener: function() { errorListeners.pop(); },
},
queueableFns: [queueableFn, nextQueueableFn],
cleanupFns: [cleanupFn],
completeOnFirstError: true,
});
queueRunner.execute();
errorListeners[errorListeners.length - 1](new Error('error'));
expect(nextQueueableFn.fn).not.toHaveBeenCalled();
expect(cleanupFn.fn).toHaveBeenCalled();
});
it("skips to cleanup functions when next.fail is called", function() {
var queueableFn = { fn: function(done) {
done.fail('nope');
} },
nextQueueableFn = { fn: jasmine.createSpy('nextFunction') },
cleanupFn = { fn: jasmine.createSpy('cleanup') },
queueRunner = new jasmineUnderTest.QueueRunner({
queueableFns: [queueableFn, nextQueueableFn],
cleanupFns: [cleanupFn],
completeOnFirstError: true,
});
queueRunner.execute();
jasmine.clock().tick();
expect(nextQueueableFn.fn).not.toHaveBeenCalled();
expect(cleanupFn.fn).toHaveBeenCalled();
});
});
});
it("calls a provided complete callback when done", function() { it("calls a provided complete callback when done", function() {
var queueableFn = { fn: jasmine.createSpy('fn') }, var queueableFn = { fn: jasmine.createSpy('fn') },
completeCallback = jasmine.createSpy('completeCallback'), completeCallback = jasmine.createSpy('completeCallback'),

View File

@@ -103,8 +103,26 @@ describe("Spec", function() {
spec.execute(); spec.execute();
var allSpecFns = fakeQueueRunner.calls.mostRecent().args[0].queueableFns; var options = fakeQueueRunner.calls.mostRecent().args[0];
expect(allSpecFns).toEqual([before, queueableFn, after]); expect(options.queueableFns).toEqual([before, queueableFn]);
expect(options.cleanupFns).toEqual([after]);
});
it("tells the queue runner that it's a leaf node", function() {
var fakeQueueRunner = jasmine.createSpy('fakeQueueRunner'),
spec = new jasmineUnderTest.Spec({
queueableFn: { fn: function() {} },
beforeAndAfterFns: function() {
return {befores: [], afters: []}
},
queueRunnerFactory: fakeQueueRunner
});
spec.execute();
expect(fakeQueueRunner).toHaveBeenCalledWith(jasmine.objectContaining({
isLeaf: true
}));
}); });
it("is marked pending if created without a function body", function() { it("is marked pending if created without a function body", function() {

View File

@@ -836,4 +836,127 @@ describe("jasmine spec running", function () {
env.execute(); env.execute();
}); });
describe("When throwOnExpectationFailure is set", function() {
it("skips to cleanup functions after an error", function(done) {
var actions = [];
env.describe('Something', function() {
env.beforeEach(function() {
actions.push('outer beforeEach');
throw new Error("error");
});
env.afterEach(function() {
actions.push('outer afterEach');
});
env.describe('Inner', function() {
env.beforeEach(function() {
actions.push('inner beforeEach');
});
env.afterEach(function() {
actions.push('inner afterEach');
});
env.it('does it' , function() {
actions.push('inner it');
});
});
});
env.throwOnExpectationFailure(true);
var assertions = function() {
expect(actions).toEqual([
'outer beforeEach',
'inner afterEach',
'outer afterEach'
]);
done();
};
env.addReporter({jasmineDone: assertions});
env.execute();
});
it("skips to cleanup functions after done.fail is called", function(done) {
var actions = [];
env.describe('Something', function() {
env.beforeEach(function(done) {
actions.push('beforeEach');
done.fail('error');
actions.push('after done.fail');
});
env.afterEach(function() {
actions.push('afterEach');
});
env.it('does it' , function() {
actions.push('it');
});
});
env.throwOnExpectationFailure(true);
var assertions = function() {
expect(actions).toEqual([
'beforeEach',
'afterEach'
]);
done();
};
env.addReporter({jasmineDone: assertions});
env.execute();
});
describe("When an async function times out", function() {
beforeEach(function() {
jasmine.clock().install();
env = new jasmineUnderTest.Env();
});
afterEach(function() {
jasmine.clock().uninstall();
});
it("skips to cleanup functions", function(done) {
var actions = [];
env.describe('Something', function() {
env.beforeEach(function(done) {
actions.push('beforeEach');
});
env.afterEach(function() {
actions.push('afterEach');
});
env.it('does it' , function() {
actions.push('it');
});
});
env.throwOnExpectationFailure(true);
var assertions = function() {
expect(actions).toEqual([
'beforeEach',
'afterEach'
]);
done();
};
env.addReporter({jasmineDone: assertions});
env.execute();
jasmine.clock().tick(jasmineUnderTest.DEFAULT_TIMEOUT_INTERVAL);
});
});
});
}); });

View File

@@ -196,6 +196,7 @@ getJasmineRequireObj().Env = function(j$) {
options.timeout = {setTimeout: realSetTimeout, clearTimeout: realClearTimeout}; options.timeout = {setTimeout: realSetTimeout, clearTimeout: realClearTimeout};
options.fail = self.fail; options.fail = self.fail;
options.globalErrors = globalErrors; options.globalErrors = globalErrors;
options.completeOnFirstError = throwOnExpectationFailure && options.isLeaf;
new j$.QueueRunner(options).execute(); new j$.QueueRunner(options).execute();
}; };
@@ -520,6 +521,7 @@ getJasmineRequireObj().Env = function(j$) {
this.afterEach = function(afterEachFunction, timeout) { this.afterEach = function(afterEachFunction, timeout) {
ensureIsFunctionOrAsync(afterEachFunction, 'afterEach'); ensureIsFunctionOrAsync(afterEachFunction, 'afterEach');
afterEachFunction.isCleanup = true;
currentDeclarationSuite.afterEach({ currentDeclarationSuite.afterEach({
fn: afterEachFunction, fn: afterEachFunction,
timeout: function() { return timeout || j$.DEFAULT_TIMEOUT_INTERVAL; } timeout: function() { return timeout || j$.DEFAULT_TIMEOUT_INTERVAL; }
@@ -568,6 +570,10 @@ getJasmineRequireObj().Env = function(j$) {
message: message, message: message,
error: error && error.message ? error : null error: error && error.message ? error : null
}); });
if (self.throwingExpectationFailures()) {
throw new Error(message);
}
}; };
} }

View File

@@ -12,7 +12,9 @@ getJasmineRequireObj().QueueRunner = function(j$) {
} }
function QueueRunner(attrs) { function QueueRunner(attrs) {
this.queueableFns = attrs.queueableFns || []; var queueableFns = attrs.queueableFns || [];
this.queueableFns = queueableFns.concat(attrs.cleanupFns || []);
this.firstCleanupIx = queueableFns.length;
this.onComplete = attrs.onComplete || function() {}; this.onComplete = attrs.onComplete || function() {};
this.clearStack = attrs.clearStack || function(fn) {fn();}; this.clearStack = attrs.clearStack || function(fn) {fn();};
this.onException = attrs.onException || function() {}; this.onException = attrs.onException || function() {};
@@ -21,6 +23,7 @@ getJasmineRequireObj().QueueRunner = function(j$) {
this.timeout = attrs.timeout || {setTimeout: setTimeout, clearTimeout: clearTimeout}; this.timeout = attrs.timeout || {setTimeout: setTimeout, clearTimeout: clearTimeout};
this.fail = attrs.fail || function() {}; this.fail = attrs.fail || function() {};
this.globalErrors = attrs.globalErrors || { pushListener: function() {}, popListener: function() {} }; this.globalErrors = attrs.globalErrors || { pushListener: function() {}, popListener: function() {} };
this.completeOnFirstError = !!attrs.completeOnFirstError;
} }
QueueRunner.prototype.execute = function() { QueueRunner.prototype.execute = function() {
@@ -29,20 +32,32 @@ getJasmineRequireObj().QueueRunner = function(j$) {
self.onException(error); self.onException(error);
}; };
this.globalErrors.pushListener(this.handleFinalError); this.globalErrors.pushListener(this.handleFinalError);
this.run(this.queueableFns, 0); this.run(0);
}; };
QueueRunner.prototype.run = function(queueableFns, recursiveIndex) { QueueRunner.prototype.skipToCleanup = function(lastRanIndex) {
var length = queueableFns.length, if (lastRanIndex < this.firstCleanupIx) {
this.run(this.firstCleanupIx);
} else {
this.run(lastRanIndex + 1);
}
};
QueueRunner.prototype.run = function(recursiveIndex) {
var length = this.queueableFns.length,
self = this, self = this,
iterativeIndex; iterativeIndex;
for(iterativeIndex = recursiveIndex; iterativeIndex < length; iterativeIndex++) { for(iterativeIndex = recursiveIndex; iterativeIndex < length; iterativeIndex++) {
var queueableFn = queueableFns[iterativeIndex]; var result = attempt(iterativeIndex);
var completedSynchronously = attempt(queueableFn);
if (!completedSynchronously) { if (!result.completedSynchronously) {
return;
}
if (this.completeOnFirstError && result.errored) {
this.skipToCleanup(iterativeIndex);
return; return;
} }
} }
@@ -52,7 +67,7 @@ getJasmineRequireObj().QueueRunner = function(j$) {
self.onComplete(); self.onComplete();
}); });
function attempt(queueableFn) { function attempt() {
var clearTimeout = function () { var clearTimeout = function () {
Function.prototype.apply.apply(self.timeout.clearTimeout, [j$.getGlobal(), [timeoutId]]); Function.prototype.apply.apply(self.timeout.clearTimeout, [j$.getGlobal(), [timeoutId]]);
}, },
@@ -70,18 +85,28 @@ getJasmineRequireObj().QueueRunner = function(j$) {
}), }),
next = once(function () { next = once(function () {
cleanup(); cleanup();
function runNext() {
if (self.completeOnFirstError && errored) {
self.skipToCleanup(iterativeIndex);
} else {
self.run(iterativeIndex + 1);
}
}
if (completedSynchronously) { if (completedSynchronously) {
setTimeout(function() { setTimeout(runNext);
self.run(queueableFns, iterativeIndex + 1);
});
} else { } else {
self.run(queueableFns, iterativeIndex + 1); runNext();
} }
}), }),
errored = false,
queueableFn = self.queueableFns[iterativeIndex],
timeoutId; timeoutId;
next.fail = function() { next.fail = function() {
self.fail.apply(null, arguments); self.fail.apply(null, arguments);
errored = true;
next(); next();
}; };
@@ -102,31 +127,33 @@ getJasmineRequireObj().QueueRunner = function(j$) {
if (maybeThenable && j$.isFunction_(maybeThenable.then)) { if (maybeThenable && j$.isFunction_(maybeThenable.then)) {
maybeThenable.then(next, next.fail); maybeThenable.then(next, next.fail);
completedSynchronously = false; completedSynchronously = false;
return false; return { completedSynchronously: false };
} }
} else { } else {
queueableFn.fn.call(self.userContext, next); queueableFn.fn.call(self.userContext, next);
completedSynchronously = false; completedSynchronously = false;
return false; return { completedSynchronously: false };
} }
} catch (e) { } catch (e) {
handleException(e, queueableFn); handleException(e, queueableFn);
errored = true;
} }
cleanup(); cleanup();
return true; return { completedSynchronously: true, errored: errored };
}
function onException(e) { function onException(e) {
self.onException(e); self.onException(e);
} errored = true;
}
function handleException(e, queueableFn) { function handleException(e, queueableFn) {
onException(e); onException(e);
if (!self.catchException(e)) { if (!self.catchException(e)) {
//TODO: set a var when we catch an exception and //TODO: set a var when we catch an exception and
//use a finally block to close the loop in a nice way.. //use a finally block to close the loop in a nice way..
throw e; throw e;
}
} }
} }
}; };

View File

@@ -56,10 +56,12 @@ getJasmineRequireObj().Spec = function(j$) {
} }
var fns = this.beforeAndAfterFns(); var fns = this.beforeAndAfterFns();
var allFns = fns.befores.concat(this.queueableFn).concat(fns.afters); var regularFns = fns.befores.concat(this.queueableFn);
this.queueRunnerFactory({ this.queueRunnerFactory({
queueableFns: allFns, isLeaf: true,
queueableFns: regularFns,
cleanupFns: fns.afters,
onException: function() { self.onException.apply(self, arguments); }, onException: function() { self.onException.apply(self, arguments); },
onComplete: complete, onComplete: complete,
userContext: this.userContext() userContext: this.userContext()