|
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406 |
- /*
- * 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();
- }
- };
|