spec.js 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270
  1. /* eslint-env mocha */
  2. var times = require('lowscore/times')
  3. var range = require('lowscore/range')
  4. var limiter = require('..')
  5. var chai = require('chai')
  6. var expect = chai.expect
  7. chai.use(require('chai-as-promised'))
  8. var max = require('./max')
  9. describe('promise-limit', function () {
  10. var output
  11. beforeEach(function () {
  12. output = []
  13. })
  14. function wait (text, n) {
  15. output.push('starting', text)
  16. return new Promise(function (resolve) {
  17. setTimeout(resolve, n)
  18. }).then(function () {
  19. output.push('finished', text)
  20. return text
  21. })
  22. }
  23. function expectMaxOutstanding (n) {
  24. var outstanding = 0
  25. var outstandingOverTime = output.map(function (line) {
  26. if (line.match(/starting/)) {
  27. outstanding++
  28. } else if (line.match(/finished/)) {
  29. outstanding--
  30. }
  31. return outstanding
  32. })
  33. var maxOutstanding = max(outstandingOverTime)
  34. expect(maxOutstanding).to.equal(n)
  35. }
  36. it('limits the number of outstanding calls to a function', function () {
  37. var limit = limiter(5)
  38. return Promise.all(times(9, function (i) {
  39. return limit(function () {
  40. return wait('job ' + (i + 1), 100)
  41. })
  42. })).then(function () {
  43. expectMaxOutstanding(5)
  44. })
  45. })
  46. it("doesn't limit if the number outstanding is the limit", function () {
  47. var limit = limiter(5)
  48. return Promise.all(times(5, function (i) {
  49. return limit(function () {
  50. return wait('job ' + (i + 1), 100)
  51. })
  52. })).then(function () {
  53. expectMaxOutstanding(5)
  54. })
  55. })
  56. it("doesn't limit if the number outstanding less than the limit", function () {
  57. var limit = limiter(5)
  58. return Promise.all(times(4, function (i) {
  59. return limit(function () {
  60. return wait('job ' + (i + 1), 100)
  61. })
  62. })).then(function () {
  63. expectMaxOutstanding(4)
  64. })
  65. })
  66. it('returns the results from each job', function () {
  67. var limit = limiter(5)
  68. return Promise.all(times(9, function (i) {
  69. return limit(function () {
  70. return wait('job ' + (i + 1), 100)
  71. })
  72. })).then(function (results) {
  73. expect(results).to.eql(times(9, function (i) {
  74. return 'job ' + (i + 1)
  75. }))
  76. })
  77. })
  78. it('returns a rejected promise if the function throws an error', function () {
  79. var limit = limiter(5)
  80. var promise = limit(function () {
  81. throw new Error('uh oh')
  82. })
  83. expect(promise).to.be.a('promise')
  84. return promise.then(function () {
  85. throw new Error('the promise resolved, instead of rejecting')
  86. }).catch(function (err) {
  87. expect(String(err)).to.equal('Error: uh oh')
  88. })
  89. })
  90. it('should fulfil or reject when the function fulfils or rejects', function () {
  91. var limit = limiter(2)
  92. var numbers = [1, 2, 3, 4, 5, 6]
  93. function rejectOdd (n) {
  94. return new Promise(function (resolve, reject) {
  95. if (n % 2 === 0) {
  96. resolve(n + ' is even')
  97. } else {
  98. reject(new Error(n + ' is odd'))
  99. }
  100. })
  101. }
  102. return Promise.all(numbers.map(function (i) {
  103. return limit(function () {
  104. return rejectOdd(i)
  105. }).then(function (r) {
  106. return 'pass: ' + r
  107. }, function (e) {
  108. return 'fail: ' + e.message
  109. })
  110. })).then(function (results) {
  111. expect(results).to.eql([
  112. 'fail: 1 is odd',
  113. 'pass: 2 is even',
  114. 'fail: 3 is odd',
  115. 'pass: 4 is even',
  116. 'fail: 5 is odd',
  117. 'pass: 6 is even'
  118. ])
  119. })
  120. })
  121. describe('no limit', function () {
  122. function expectNoLimit (limit) {
  123. return Promise.all(times(9, function (i) {
  124. return limit(function () { return wait('job ' + (i + 1), 100) })
  125. })).then(function () {
  126. expectMaxOutstanding(9)
  127. }).then(function () {
  128. return expectNoMapLimit(limit)
  129. })
  130. }
  131. function expectNoMapLimit (limit) {
  132. return limit.map(range(0, 9), function (i) {
  133. return wait('job ' + (i + 1), 100)
  134. }).then(function () {
  135. expectMaxOutstanding(9)
  136. })
  137. }
  138. it("doesn't limit if the limit is 0", function () {
  139. var limit = limiter(0)
  140. return expectNoLimit(limit)
  141. })
  142. it("doesn't limit if the limit is undefined", function () {
  143. var limit = limiter()
  144. return expectNoLimit(limit)
  145. })
  146. })
  147. describe('map', function () {
  148. function failsAt1 (num) {
  149. if (num === 1) return Promise.reject(new Error('rejecting number ' + num))
  150. else return Promise.resolve('accepting number ' + num)
  151. }
  152. function resolvesAll (num) {
  153. return Promise.resolve('accepting number ' + num)
  154. }
  155. it('returns all results when all are resolved', function () {
  156. var limit = limiter(2)
  157. return limit.map([0, 1, 2, 3], resolvesAll).then(function (results) {
  158. expect(results).to.eql([
  159. 'accepting number 0',
  160. 'accepting number 1',
  161. 'accepting number 2',
  162. 'accepting number 3'
  163. ])
  164. })
  165. })
  166. it('returns first failure when one fails', function () {
  167. var limit = limiter(2)
  168. return expect(limit.map([0, 1, 2, 3], failsAt1)).to.be.rejectedWith('rejecting number 1')
  169. })
  170. it('limiter can still be used after map with failure', function () {
  171. var limit = limiter(2)
  172. return expect(limit.map([0, 1, 2, 3], failsAt1)).to.be.rejectedWith('rejecting number 1').then(function () {
  173. return limit.map([0, 1, 2, 3], resolvesAll).then(function (results) {
  174. expect(results).to.eql([
  175. 'accepting number 0',
  176. 'accepting number 1',
  177. 'accepting number 2',
  178. 'accepting number 3'
  179. ])
  180. })
  181. })
  182. })
  183. })
  184. describe('large numbers of tasks failing', function () {
  185. context('when error thrown', function () {
  186. function alwaysFails (n) {
  187. throw new Error('argh: ' + n)
  188. }
  189. it("doesn't exceed stack size", function () {
  190. var limit = limiter(2)
  191. return expect(limit.map(range(0, 1000), alwaysFails)).to.be.rejectedWith('argh: 0')
  192. })
  193. })
  194. context('when error rejected', function () {
  195. function alwaysFails (n) {
  196. return Promise.reject(new Error('argh: ' + n))
  197. }
  198. it("doesn't exceed stack size", function () {
  199. var limit = limiter(2)
  200. return expect(limit.map(range(0, 1000), alwaysFails)).to.be.rejectedWith('argh: 0')
  201. })
  202. })
  203. })
  204. describe('queue length', function () {
  205. it('updates the queue length when there are more jobs than there is concurrency', function () {
  206. var limit = limiter(2)
  207. var one = limit(function () { return wait('one', 10) })
  208. expect(limit.queue).to.equal(0)
  209. var two = limit(function () { return wait('two', 20) })
  210. expect(limit.queue).to.equal(0)
  211. limit(function () { return wait('three', 100) })
  212. expect(limit.queue).to.equal(1)
  213. limit(function () { return wait('four', 100) })
  214. expect(limit.queue).to.equal(2)
  215. return one.then(function () {
  216. expect(limit.queue).to.equal(1)
  217. return two.then(function () {
  218. expect(limit.queue).to.equal(0)
  219. })
  220. })
  221. })
  222. })
  223. })