271 lines
7.0 KiB
JavaScript
271 lines
7.0 KiB
JavaScript
|
/* eslint-env mocha */
|
||
|
|
||
|
var times = require('lowscore/times')
|
||
|
var range = require('lowscore/range')
|
||
|
var limiter = require('..')
|
||
|
var chai = require('chai')
|
||
|
var expect = chai.expect
|
||
|
chai.use(require('chai-as-promised'))
|
||
|
var max = require('./max')
|
||
|
|
||
|
describe('promise-limit', function () {
|
||
|
var output
|
||
|
|
||
|
beforeEach(function () {
|
||
|
output = []
|
||
|
})
|
||
|
|
||
|
function wait (text, n) {
|
||
|
output.push('starting', text)
|
||
|
|
||
|
return new Promise(function (resolve) {
|
||
|
setTimeout(resolve, n)
|
||
|
}).then(function () {
|
||
|
output.push('finished', text)
|
||
|
return text
|
||
|
})
|
||
|
}
|
||
|
|
||
|
function expectMaxOutstanding (n) {
|
||
|
var outstanding = 0
|
||
|
|
||
|
var outstandingOverTime = output.map(function (line) {
|
||
|
if (line.match(/starting/)) {
|
||
|
outstanding++
|
||
|
} else if (line.match(/finished/)) {
|
||
|
outstanding--
|
||
|
}
|
||
|
|
||
|
return outstanding
|
||
|
})
|
||
|
|
||
|
var maxOutstanding = max(outstandingOverTime)
|
||
|
expect(maxOutstanding).to.equal(n)
|
||
|
}
|
||
|
|
||
|
it('limits the number of outstanding calls to a function', function () {
|
||
|
var limit = limiter(5)
|
||
|
|
||
|
return Promise.all(times(9, function (i) {
|
||
|
return limit(function () {
|
||
|
return wait('job ' + (i + 1), 100)
|
||
|
})
|
||
|
})).then(function () {
|
||
|
expectMaxOutstanding(5)
|
||
|
})
|
||
|
})
|
||
|
|
||
|
it("doesn't limit if the number outstanding is the limit", function () {
|
||
|
var limit = limiter(5)
|
||
|
|
||
|
return Promise.all(times(5, function (i) {
|
||
|
return limit(function () {
|
||
|
return wait('job ' + (i + 1), 100)
|
||
|
})
|
||
|
})).then(function () {
|
||
|
expectMaxOutstanding(5)
|
||
|
})
|
||
|
})
|
||
|
|
||
|
it("doesn't limit if the number outstanding less than the limit", function () {
|
||
|
var limit = limiter(5)
|
||
|
|
||
|
return Promise.all(times(4, function (i) {
|
||
|
return limit(function () {
|
||
|
return wait('job ' + (i + 1), 100)
|
||
|
})
|
||
|
})).then(function () {
|
||
|
expectMaxOutstanding(4)
|
||
|
})
|
||
|
})
|
||
|
|
||
|
it('returns the results from each job', function () {
|
||
|
var limit = limiter(5)
|
||
|
|
||
|
return Promise.all(times(9, function (i) {
|
||
|
return limit(function () {
|
||
|
return wait('job ' + (i + 1), 100)
|
||
|
})
|
||
|
})).then(function (results) {
|
||
|
expect(results).to.eql(times(9, function (i) {
|
||
|
return 'job ' + (i + 1)
|
||
|
}))
|
||
|
})
|
||
|
})
|
||
|
|
||
|
it('returns a rejected promise if the function throws an error', function () {
|
||
|
var limit = limiter(5)
|
||
|
|
||
|
var promise = limit(function () {
|
||
|
throw new Error('uh oh')
|
||
|
})
|
||
|
expect(promise).to.be.a('promise')
|
||
|
return promise.then(function () {
|
||
|
throw new Error('the promise resolved, instead of rejecting')
|
||
|
}).catch(function (err) {
|
||
|
expect(String(err)).to.equal('Error: uh oh')
|
||
|
})
|
||
|
})
|
||
|
|
||
|
it('should fulfil or reject when the function fulfils or rejects', function () {
|
||
|
var limit = limiter(2)
|
||
|
|
||
|
var numbers = [1, 2, 3, 4, 5, 6]
|
||
|
|
||
|
function rejectOdd (n) {
|
||
|
return new Promise(function (resolve, reject) {
|
||
|
if (n % 2 === 0) {
|
||
|
resolve(n + ' is even')
|
||
|
} else {
|
||
|
reject(new Error(n + ' is odd'))
|
||
|
}
|
||
|
})
|
||
|
}
|
||
|
|
||
|
return Promise.all(numbers.map(function (i) {
|
||
|
return limit(function () {
|
||
|
return rejectOdd(i)
|
||
|
}).then(function (r) {
|
||
|
return 'pass: ' + r
|
||
|
}, function (e) {
|
||
|
return 'fail: ' + e.message
|
||
|
})
|
||
|
})).then(function (results) {
|
||
|
expect(results).to.eql([
|
||
|
'fail: 1 is odd',
|
||
|
'pass: 2 is even',
|
||
|
'fail: 3 is odd',
|
||
|
'pass: 4 is even',
|
||
|
'fail: 5 is odd',
|
||
|
'pass: 6 is even'
|
||
|
])
|
||
|
})
|
||
|
})
|
||
|
|
||
|
describe('no limit', function () {
|
||
|
function expectNoLimit (limit) {
|
||
|
return Promise.all(times(9, function (i) {
|
||
|
return limit(function () { return wait('job ' + (i + 1), 100) })
|
||
|
})).then(function () {
|
||
|
expectMaxOutstanding(9)
|
||
|
}).then(function () {
|
||
|
return expectNoMapLimit(limit)
|
||
|
})
|
||
|
}
|
||
|
|
||
|
function expectNoMapLimit (limit) {
|
||
|
return limit.map(range(0, 9), function (i) {
|
||
|
return wait('job ' + (i + 1), 100)
|
||
|
}).then(function () {
|
||
|
expectMaxOutstanding(9)
|
||
|
})
|
||
|
}
|
||
|
|
||
|
it("doesn't limit if the limit is 0", function () {
|
||
|
var limit = limiter(0)
|
||
|
|
||
|
return expectNoLimit(limit)
|
||
|
})
|
||
|
|
||
|
it("doesn't limit if the limit is undefined", function () {
|
||
|
var limit = limiter()
|
||
|
|
||
|
return expectNoLimit(limit)
|
||
|
})
|
||
|
})
|
||
|
|
||
|
describe('map', function () {
|
||
|
function failsAt1 (num) {
|
||
|
if (num === 1) return Promise.reject(new Error('rejecting number ' + num))
|
||
|
else return Promise.resolve('accepting number ' + num)
|
||
|
}
|
||
|
|
||
|
function resolvesAll (num) {
|
||
|
return Promise.resolve('accepting number ' + num)
|
||
|
}
|
||
|
|
||
|
it('returns all results when all are resolved', function () {
|
||
|
var limit = limiter(2)
|
||
|
|
||
|
return limit.map([0, 1, 2, 3], resolvesAll).then(function (results) {
|
||
|
expect(results).to.eql([
|
||
|
'accepting number 0',
|
||
|
'accepting number 1',
|
||
|
'accepting number 2',
|
||
|
'accepting number 3'
|
||
|
])
|
||
|
})
|
||
|
})
|
||
|
|
||
|
it('returns first failure when one fails', function () {
|
||
|
var limit = limiter(2)
|
||
|
|
||
|
return expect(limit.map([0, 1, 2, 3], failsAt1)).to.be.rejectedWith('rejecting number 1')
|
||
|
})
|
||
|
|
||
|
it('limiter can still be used after map with failure', function () {
|
||
|
var limit = limiter(2)
|
||
|
|
||
|
return expect(limit.map([0, 1, 2, 3], failsAt1)).to.be.rejectedWith('rejecting number 1').then(function () {
|
||
|
return limit.map([0, 1, 2, 3], resolvesAll).then(function (results) {
|
||
|
expect(results).to.eql([
|
||
|
'accepting number 0',
|
||
|
'accepting number 1',
|
||
|
'accepting number 2',
|
||
|
'accepting number 3'
|
||
|
])
|
||
|
})
|
||
|
})
|
||
|
})
|
||
|
})
|
||
|
|
||
|
describe('large numbers of tasks failing', function () {
|
||
|
context('when error thrown', function () {
|
||
|
function alwaysFails (n) {
|
||
|
throw new Error('argh: ' + n)
|
||
|
}
|
||
|
|
||
|
it("doesn't exceed stack size", function () {
|
||
|
var limit = limiter(2)
|
||
|
|
||
|
return expect(limit.map(range(0, 1000), alwaysFails)).to.be.rejectedWith('argh: 0')
|
||
|
})
|
||
|
})
|
||
|
|
||
|
context('when error rejected', function () {
|
||
|
function alwaysFails (n) {
|
||
|
return Promise.reject(new Error('argh: ' + n))
|
||
|
}
|
||
|
|
||
|
it("doesn't exceed stack size", function () {
|
||
|
var limit = limiter(2)
|
||
|
|
||
|
return expect(limit.map(range(0, 1000), alwaysFails)).to.be.rejectedWith('argh: 0')
|
||
|
})
|
||
|
})
|
||
|
})
|
||
|
|
||
|
describe('queue length', function () {
|
||
|
it('updates the queue length when there are more jobs than there is concurrency', function () {
|
||
|
var limit = limiter(2)
|
||
|
|
||
|
var one = limit(function () { return wait('one', 10) })
|
||
|
expect(limit.queue).to.equal(0)
|
||
|
var two = limit(function () { return wait('two', 20) })
|
||
|
expect(limit.queue).to.equal(0)
|
||
|
limit(function () { return wait('three', 100) })
|
||
|
expect(limit.queue).to.equal(1)
|
||
|
limit(function () { return wait('four', 100) })
|
||
|
expect(limit.queue).to.equal(2)
|
||
|
|
||
|
return one.then(function () {
|
||
|
expect(limit.queue).to.equal(1)
|
||
|
|
||
|
return two.then(function () {
|
||
|
expect(limit.queue).to.equal(0)
|
||
|
})
|
||
|
})
|
||
|
})
|
||
|
})
|
||
|
})
|