407 lines
13 KiB
JavaScript
407 lines
13 KiB
JavaScript
|
/*
|
||
|
* Copyright (c) 2012 Mathieu Turcotte
|
||
|
* Licensed under the MIT license.
|
||
|
*/
|
||
|
|
||
|
var assert = require('assert');
|
||
|
var events = require('events');
|
||
|
var sinon = require('sinon');
|
||
|
var util = require('util');
|
||
|
|
||
|
var FunctionCall = require('../lib/function_call');
|
||
|
|
||
|
function MockBackoff() {
|
||
|
events.EventEmitter.call(this);
|
||
|
|
||
|
this.reset = sinon.spy();
|
||
|
this.backoff = sinon.spy();
|
||
|
this.failAfter = sinon.spy();
|
||
|
}
|
||
|
util.inherits(MockBackoff, events.EventEmitter);
|
||
|
|
||
|
exports["FunctionCall"] = {
|
||
|
setUp: function(callback) {
|
||
|
this.wrappedFn = sinon.stub();
|
||
|
this.callback = sinon.stub();
|
||
|
this.backoff = new MockBackoff();
|
||
|
this.backoffFactory = sinon.stub();
|
||
|
this.backoffFactory.returns(this.backoff);
|
||
|
callback();
|
||
|
},
|
||
|
|
||
|
tearDown: function(callback) {
|
||
|
callback();
|
||
|
},
|
||
|
|
||
|
"constructor's first argument should be a function": function(test) {
|
||
|
test.throws(function() {
|
||
|
new FunctionCall(1, [], function() {});
|
||
|
}, /Expected fn to be a function./);
|
||
|
test.done();
|
||
|
},
|
||
|
|
||
|
"constructor's last argument should be a function": function(test) {
|
||
|
test.throws(function() {
|
||
|
new FunctionCall(function() {}, [], 3);
|
||
|
}, /Expected callback to be a function./);
|
||
|
test.done();
|
||
|
},
|
||
|
|
||
|
"isPending should return false once the call is started": function(test) {
|
||
|
this.wrappedFn.
|
||
|
onFirstCall().yields(new Error()).
|
||
|
onSecondCall().yields(null, 'Success!');
|
||
|
var call = new FunctionCall(this.wrappedFn, [], this.callback);
|
||
|
|
||
|
test.ok(call.isPending());
|
||
|
|
||
|
call.start(this.backoffFactory);
|
||
|
test.ok(!call.isPending());
|
||
|
|
||
|
this.backoff.emit('ready');
|
||
|
test.ok(!call.isPending());
|
||
|
|
||
|
test.done();
|
||
|
},
|
||
|
|
||
|
"isRunning should return true when call is in progress": function(test) {
|
||
|
this.wrappedFn.
|
||
|
onFirstCall().yields(new Error()).
|
||
|
onSecondCall().yields(null, 'Success!');
|
||
|
var call = new FunctionCall(this.wrappedFn, [], this.callback);
|
||
|
|
||
|
test.ok(!call.isRunning());
|
||
|
|
||
|
call.start(this.backoffFactory);
|
||
|
test.ok(call.isRunning());
|
||
|
|
||
|
this.backoff.emit('ready');
|
||
|
test.ok(!call.isRunning());
|
||
|
|
||
|
test.done();
|
||
|
},
|
||
|
|
||
|
"isCompleted should return true once the call completes": function(test) {
|
||
|
this.wrappedFn.
|
||
|
onFirstCall().yields(new Error()).
|
||
|
onSecondCall().yields(null, 'Success!');
|
||
|
var call = new FunctionCall(this.wrappedFn, [], this.callback);
|
||
|
|
||
|
test.ok(!call.isCompleted());
|
||
|
|
||
|
call.start(this.backoffFactory);
|
||
|
test.ok(!call.isCompleted());
|
||
|
|
||
|
this.backoff.emit('ready');
|
||
|
test.ok(call.isCompleted());
|
||
|
|
||
|
test.done();
|
||
|
},
|
||
|
|
||
|
"isAborted should return true once the call is aborted": function(test) {
|
||
|
this.wrappedFn.
|
||
|
onFirstCall().yields(new Error()).
|
||
|
onSecondCall().yields(null, 'Success!');
|
||
|
var call = new FunctionCall(this.wrappedFn, [], this.callback);
|
||
|
|
||
|
test.ok(!call.isAborted());
|
||
|
call.abort();
|
||
|
test.ok(call.isAborted());
|
||
|
|
||
|
test.done();
|
||
|
},
|
||
|
|
||
|
"setStrategy should overwrite the default strategy": function(test) {
|
||
|
var replacementStrategy = {};
|
||
|
var call = new FunctionCall(this.wrappedFn, [], this.callback);
|
||
|
call.setStrategy(replacementStrategy);
|
||
|
call.start(this.backoffFactory);
|
||
|
test.ok(this.backoffFactory.calledWith(replacementStrategy),
|
||
|
'User defined strategy should be used to instantiate ' +
|
||
|
'the backoff instance.');
|
||
|
test.done();
|
||
|
},
|
||
|
|
||
|
"setStrategy should throw if the call is in progress": function(test) {
|
||
|
var call = new FunctionCall(this.wrappedFn, [], this.callback);
|
||
|
call.start(this.backoffFactory);
|
||
|
test.throws(function() {
|
||
|
call.setStrategy({});
|
||
|
}, /in progress/);
|
||
|
test.done();
|
||
|
},
|
||
|
|
||
|
"failAfter should not be set by default": function(test) {
|
||
|
var call = new FunctionCall(this.wrappedFn, [], this.callback);
|
||
|
call.start(this.backoffFactory);
|
||
|
test.equal(0, this.backoff.failAfter.callCount);
|
||
|
test.done();
|
||
|
},
|
||
|
|
||
|
"failAfter should be used as the maximum number of backoffs": function(test) {
|
||
|
var failAfterValue = 99;
|
||
|
var call = new FunctionCall(this.wrappedFn, [], this.callback);
|
||
|
call.failAfter(failAfterValue);
|
||
|
call.start(this.backoffFactory);
|
||
|
test.ok(this.backoff.failAfter.calledWith(failAfterValue),
|
||
|
'User defined maximum number of backoffs shoud be ' +
|
||
|
'used to configure the backoff instance.');
|
||
|
test.done();
|
||
|
},
|
||
|
|
||
|
"failAfter should throw if the call is in progress": function(test) {
|
||
|
var call = new FunctionCall(this.wrappedFn, [], this.callback);
|
||
|
call.start(this.backoffFactory);
|
||
|
test.throws(function() {
|
||
|
call.failAfter(1234);
|
||
|
}, /in progress/);
|
||
|
test.done();
|
||
|
},
|
||
|
|
||
|
"start shouldn't allow overlapping invocation": function(test) {
|
||
|
var call = new FunctionCall(this.wrappedFn, [], this.callback);
|
||
|
var backoffFactory = this.backoffFactory;
|
||
|
|
||
|
call.start(backoffFactory);
|
||
|
test.throws(function() {
|
||
|
call.start(backoffFactory);
|
||
|
}, /already started/);
|
||
|
test.done();
|
||
|
},
|
||
|
|
||
|
"start shouldn't allow invocation of aborted call": function(test) {
|
||
|
var call = new FunctionCall(this.wrappedFn, [], this.callback);
|
||
|
var backoffFactory = this.backoffFactory;
|
||
|
|
||
|
call.abort();
|
||
|
test.throws(function() {
|
||
|
call.start(backoffFactory);
|
||
|
}, /aborted/);
|
||
|
test.done();
|
||
|
},
|
||
|
|
||
|
"call should forward its arguments to the wrapped function": function(test) {
|
||
|
var call = new FunctionCall(this.wrappedFn, [1, 2, 3], this.callback);
|
||
|
call.start(this.backoffFactory);
|
||
|
test.ok(this.wrappedFn.calledWith(1, 2, 3));
|
||
|
test.done();
|
||
|
},
|
||
|
|
||
|
"call should complete when the wrapped function succeeds": function(test) {
|
||
|
var call = new FunctionCall(this.wrappedFn, [1, 2, 3], this.callback);
|
||
|
this.wrappedFn.
|
||
|
onCall(0).yields(new Error()).
|
||
|
onCall(1).yields(new Error()).
|
||
|
onCall(2).yields(new Error()).
|
||
|
onCall(3).yields(null, 'Success!');
|
||
|
|
||
|
call.start(this.backoffFactory);
|
||
|
|
||
|
for (var i = 0; i < 2; i++) {
|
||
|
this.backoff.emit('ready');
|
||
|
}
|
||
|
|
||
|
test.equals(this.callback.callCount, 0);
|
||
|
this.backoff.emit('ready');
|
||
|
|
||
|
test.ok(this.callback.calledWith(null, 'Success!'));
|
||
|
test.ok(this.wrappedFn.alwaysCalledWith(1, 2, 3));
|
||
|
test.done();
|
||
|
},
|
||
|
|
||
|
"call should fail when the backoff limit is reached": function(test) {
|
||
|
var call = new FunctionCall(this.wrappedFn, [1, 2, 3], this.callback);
|
||
|
var error = new Error();
|
||
|
this.wrappedFn.yields(error);
|
||
|
call.start(this.backoffFactory);
|
||
|
|
||
|
for (var i = 0; i < 3; i++) {
|
||
|
this.backoff.emit('ready');
|
||
|
}
|
||
|
|
||
|
test.equals(this.callback.callCount, 0);
|
||
|
|
||
|
this.backoff.emit('fail');
|
||
|
|
||
|
test.ok(this.callback.calledWith(error));
|
||
|
test.ok(this.wrappedFn.alwaysCalledWith(1, 2, 3));
|
||
|
test.done();
|
||
|
},
|
||
|
|
||
|
"call should fail when the retry predicate returns false": function(test) {
|
||
|
var call = new FunctionCall(this.wrappedFn, [1, 2, 3], this.callback);
|
||
|
call.retryIf(function(err) { return err.retriable; });
|
||
|
|
||
|
var retriableError = new Error();
|
||
|
retriableError.retriable = true;
|
||
|
|
||
|
var fatalError = new Error();
|
||
|
fatalError.retriable = false;
|
||
|
|
||
|
this.wrappedFn.
|
||
|
onCall(0).yields(retriableError).
|
||
|
onCall(1).yields(retriableError).
|
||
|
onCall(2).yields(fatalError);
|
||
|
|
||
|
call.start(this.backoffFactory);
|
||
|
|
||
|
for (var i = 0; i < 2; i++) {
|
||
|
this.backoff.emit('ready');
|
||
|
}
|
||
|
|
||
|
test.equals(this.callback.callCount, 1);
|
||
|
test.ok(this.callback.calledWith(fatalError));
|
||
|
test.ok(this.wrappedFn.alwaysCalledWith(1, 2, 3));
|
||
|
test.done();
|
||
|
},
|
||
|
|
||
|
"wrapped function's callback shouldn't be called after abort": function(test) {
|
||
|
var call = new FunctionCall(function(callback) {
|
||
|
call.abort(); // Abort in middle of wrapped function's execution.
|
||
|
callback(null, 'ok');
|
||
|
}, [], this.callback);
|
||
|
|
||
|
call.start(this.backoffFactory);
|
||
|
|
||
|
test.equals(this.callback.callCount, 1,
|
||
|
'Wrapped function\'s callback shouldn\'t be called after abort.');
|
||
|
test.ok(this.callback.calledWithMatch(sinon.match(function (err) {
|
||
|
return !!err.message.match(/Backoff aborted/);
|
||
|
}, "abort error")));
|
||
|
test.done();
|
||
|
},
|
||
|
|
||
|
"abort event is emitted once when abort is called": function(test) {
|
||
|
var call = new FunctionCall(this.wrappedFn, [], this.callback);
|
||
|
this.wrappedFn.yields(new Error());
|
||
|
var callEventSpy = sinon.spy();
|
||
|
|
||
|
call.on('abort', callEventSpy);
|
||
|
call.start(this.backoffFactory);
|
||
|
|
||
|
call.abort();
|
||
|
call.abort();
|
||
|
call.abort();
|
||
|
|
||
|
test.equals(callEventSpy.callCount, 1);
|
||
|
test.done();
|
||
|
},
|
||
|
|
||
|
"getLastResult should return the last intermediary result": function(test) {
|
||
|
var call = new FunctionCall(this.wrappedFn, [], this.callback);
|
||
|
this.wrappedFn.yields(1);
|
||
|
call.start(this.backoffFactory);
|
||
|
|
||
|
for (var i = 2; i < 5; i++) {
|
||
|
this.wrappedFn.yields(i);
|
||
|
this.backoff.emit('ready');
|
||
|
test.deepEqual([i], call.getLastResult());
|
||
|
}
|
||
|
|
||
|
this.wrappedFn.yields(null);
|
||
|
this.backoff.emit('ready');
|
||
|
test.deepEqual([null], call.getLastResult());
|
||
|
|
||
|
test.done();
|
||
|
},
|
||
|
|
||
|
"getNumRetries should return the number of retries": function(test) {
|
||
|
var call = new FunctionCall(this.wrappedFn, [], this.callback);
|
||
|
|
||
|
this.wrappedFn.yields(1);
|
||
|
call.start(this.backoffFactory);
|
||
|
// The inital call doesn't count as a retry.
|
||
|
test.equals(0, call.getNumRetries());
|
||
|
|
||
|
for (var i = 2; i < 5; i++) {
|
||
|
this.wrappedFn.yields(i);
|
||
|
this.backoff.emit('ready');
|
||
|
test.equals(i - 1, call.getNumRetries());
|
||
|
}
|
||
|
|
||
|
this.wrappedFn.yields(null);
|
||
|
this.backoff.emit('ready');
|
||
|
test.equals(4, call.getNumRetries());
|
||
|
|
||
|
test.done();
|
||
|
},
|
||
|
|
||
|
"wrapped function's errors should be propagated": function(test) {
|
||
|
var call = new FunctionCall(this.wrappedFn, [1, 2, 3], this.callback);
|
||
|
this.wrappedFn.throws(new Error());
|
||
|
test.throws(function() {
|
||
|
call.start(this.backoffFactory);
|
||
|
}, Error);
|
||
|
test.done();
|
||
|
},
|
||
|
|
||
|
"wrapped callback's errors should be propagated": function(test) {
|
||
|
var call = new FunctionCall(this.wrappedFn, [1, 2, 3], this.callback);
|
||
|
this.wrappedFn.yields(null, 'Success!');
|
||
|
this.callback.throws(new Error());
|
||
|
test.throws(function() {
|
||
|
call.start(this.backoffFactory);
|
||
|
}, Error);
|
||
|
test.done();
|
||
|
},
|
||
|
|
||
|
"call event should be emitted when wrapped function gets called": function(test) {
|
||
|
this.wrappedFn.yields(1);
|
||
|
var callEventSpy = sinon.spy();
|
||
|
|
||
|
var call = new FunctionCall(this.wrappedFn, [1, 'two'], this.callback);
|
||
|
call.on('call', callEventSpy);
|
||
|
call.start(this.backoffFactory);
|
||
|
|
||
|
for (var i = 1; i < 5; i++) {
|
||
|
this.backoff.emit('ready');
|
||
|
}
|
||
|
|
||
|
test.equal(5, callEventSpy.callCount,
|
||
|
'The call event should have been emitted 5 times.');
|
||
|
test.deepEqual([1, 'two'], callEventSpy.getCall(0).args,
|
||
|
'The call event should carry function\'s args.');
|
||
|
test.done();
|
||
|
},
|
||
|
|
||
|
"callback event should be emitted when callback is called": function(test) {
|
||
|
var call = new FunctionCall(this.wrappedFn, [1, 'two'], this.callback);
|
||
|
var callbackSpy = sinon.spy();
|
||
|
call.on('callback', callbackSpy);
|
||
|
|
||
|
this.wrappedFn.yields('error');
|
||
|
call.start(this.backoffFactory);
|
||
|
|
||
|
this.wrappedFn.yields(null, 'done');
|
||
|
this.backoff.emit('ready');
|
||
|
|
||
|
test.equal(2, callbackSpy.callCount,
|
||
|
'Callback event should have been emitted 2 times.');
|
||
|
test.deepEqual(['error'], callbackSpy.firstCall.args,
|
||
|
'First callback event should carry first call\'s results.');
|
||
|
test.deepEqual([null, 'done'], callbackSpy.secondCall.args,
|
||
|
'Second callback event should carry second call\'s results.');
|
||
|
test.done();
|
||
|
},
|
||
|
|
||
|
"backoff event should be emitted on backoff start": function(test) {
|
||
|
var err = new Error('backoff event error');
|
||
|
var call = new FunctionCall(this.wrappedFn, [1, 'two'], this.callback);
|
||
|
var backoffSpy = sinon.spy();
|
||
|
|
||
|
call.on('backoff', backoffSpy);
|
||
|
|
||
|
this.wrappedFn.yields(err);
|
||
|
call.start(this.backoffFactory);
|
||
|
this.backoff.emit('backoff', 3, 1234, err);
|
||
|
|
||
|
test.ok(this.backoff.backoff.calledWith(err),
|
||
|
'The backoff instance should have been called with the error.');
|
||
|
test.equal(1, backoffSpy.callCount,
|
||
|
'Backoff event should have been emitted 1 time.');
|
||
|
test.deepEqual([3, 1234, err], backoffSpy.firstCall.args,
|
||
|
'Backoff event should carry the backoff number, delay and error.');
|
||
|
test.done();
|
||
|
}
|
||
|
};
|