|
| 1 | +import Queue from '@shell/utils/queue'; |
| 2 | +import { |
| 3 | + allHash, |
| 4 | + allHashSettled, |
| 5 | + deferred, |
| 6 | + eachLimit, |
| 7 | + setPromiseResult, |
| 8 | +} from '@shell/utils/promise'; |
| 9 | + |
| 10 | +describe('queue', () => { |
| 11 | + describe('getLength', () => { |
| 12 | + it.each([ |
| 13 | + { |
| 14 | + desc: '0 for an empty queue', enqueueCount: 0, dequeueCount: 0, expected: 0 |
| 15 | + }, |
| 16 | + { |
| 17 | + desc: '1 after one enqueue', enqueueCount: 1, dequeueCount: 0, expected: 1 |
| 18 | + }, |
| 19 | + { |
| 20 | + desc: 'decrements after dequeue', enqueueCount: 2, dequeueCount: 1, expected: 1 |
| 21 | + }, |
| 22 | + ])('returns $desc', ({ enqueueCount, dequeueCount, expected }) => { |
| 23 | + const q = new Queue(); |
| 24 | + |
| 25 | + for (let i = 0; i < enqueueCount; i++) { |
| 26 | + q.enqueue(`item-${ i }`); |
| 27 | + } |
| 28 | + for (let i = 0; i < dequeueCount; i++) { |
| 29 | + q.dequeue(); |
| 30 | + } |
| 31 | + |
| 32 | + expect(q.getLength()).toStrictEqual(expected); |
| 33 | + }); |
| 34 | + }); |
| 35 | + |
| 36 | + describe('isEmpty', () => { |
| 37 | + it.each([ |
| 38 | + { |
| 39 | + desc: 'true when queue is empty', enqueueCount: 0, dequeueCount: 0, expected: true |
| 40 | + }, |
| 41 | + { |
| 42 | + desc: 'false after enqueueing an item', enqueueCount: 1, dequeueCount: 0, expected: false |
| 43 | + }, |
| 44 | + { |
| 45 | + desc: 'true after all items are dequeued', enqueueCount: 1, dequeueCount: 1, expected: true |
| 46 | + }, |
| 47 | + ])('returns $desc', ({ enqueueCount, dequeueCount, expected }) => { |
| 48 | + const q = new Queue(); |
| 49 | + |
| 50 | + for (let i = 0; i < enqueueCount; i++) { |
| 51 | + q.enqueue('x'); |
| 52 | + } |
| 53 | + for (let i = 0; i < dequeueCount; i++) { |
| 54 | + q.dequeue(); |
| 55 | + } |
| 56 | + |
| 57 | + expect(q.isEmpty()).toStrictEqual(expected); |
| 58 | + }); |
| 59 | + }); |
| 60 | + |
| 61 | + describe('enqueue / dequeue', () => { |
| 62 | + it('dequeues items in FIFO order', () => { |
| 63 | + const q = new Queue(); |
| 64 | + |
| 65 | + q.enqueue('first'); |
| 66 | + q.enqueue('second'); |
| 67 | + q.enqueue('third'); |
| 68 | + |
| 69 | + expect(q.dequeue()).toStrictEqual('first'); |
| 70 | + expect(q.dequeue()).toStrictEqual('second'); |
| 71 | + expect(q.dequeue()).toStrictEqual('third'); |
| 72 | + }); |
| 73 | + |
| 74 | + it('returns undefined when dequeuing from an empty queue', () => { |
| 75 | + const q = new Queue(); |
| 76 | + |
| 77 | + expect(q.dequeue()).toBeUndefined(); |
| 78 | + }); |
| 79 | + |
| 80 | + it('works with objects as items', () => { |
| 81 | + const q = new Queue(); |
| 82 | + const item = { key: 'value' }; |
| 83 | + |
| 84 | + q.enqueue(item); |
| 85 | + expect(q.dequeue()).toStrictEqual(item); |
| 86 | + }); |
| 87 | + }); |
| 88 | + |
| 89 | + describe('peek', () => { |
| 90 | + it.each([ |
| 91 | + { |
| 92 | + desc: 'the front item without removing it', items: ['a', 'b'], expected: 'a' |
| 93 | + }, |
| 94 | + { |
| 95 | + desc: 'undefined when queue is empty', items: [] as string[], expected: undefined |
| 96 | + }, |
| 97 | + ])('returns $desc', ({ items, expected }) => { |
| 98 | + const q = new Queue(); |
| 99 | + |
| 100 | + items.forEach((item) => q.enqueue(item)); |
| 101 | + |
| 102 | + expect(q.peek()).toStrictEqual(expected); |
| 103 | + expect(q.getLength()).toStrictEqual(items.length); |
| 104 | + }); |
| 105 | + }); |
| 106 | + |
| 107 | + describe('clear', () => { |
| 108 | + it('empties the queue', () => { |
| 109 | + const q = new Queue(); |
| 110 | + |
| 111 | + q.enqueue('a'); |
| 112 | + q.enqueue('b'); |
| 113 | + q.clear(); |
| 114 | + |
| 115 | + expect(q.isEmpty()).toStrictEqual(true); |
| 116 | + expect(q.getLength()).toStrictEqual(0); |
| 117 | + }); |
| 118 | + |
| 119 | + it('allows re-use after clear', () => { |
| 120 | + const q = new Queue(); |
| 121 | + |
| 122 | + q.enqueue('a'); |
| 123 | + q.clear(); |
| 124 | + q.enqueue('b'); |
| 125 | + |
| 126 | + expect(q.dequeue()).toStrictEqual('b'); |
| 127 | + }); |
| 128 | + }); |
| 129 | +}); |
| 130 | + |
| 131 | +describe('allHash', () => { |
| 132 | + it('resolves a hash of resolved promises', async() => { |
| 133 | + const result = await allHash({ |
| 134 | + a: Promise.resolve(1), |
| 135 | + b: Promise.resolve(2), |
| 136 | + c: Promise.resolve(3), |
| 137 | + }); |
| 138 | + |
| 139 | + expect(result).toStrictEqual({ |
| 140 | + a: 1, |
| 141 | + b: 2, |
| 142 | + c: 3, |
| 143 | + }); |
| 144 | + }); |
| 145 | + |
| 146 | + it('rejects if any promise in the hash rejects', async() => { |
| 147 | + await expect( |
| 148 | + allHash({ |
| 149 | + a: Promise.resolve(1), |
| 150 | + b: Promise.reject(new Error('fail')), |
| 151 | + }) |
| 152 | + ).rejects.toThrow('fail'); |
| 153 | + }); |
| 154 | + |
| 155 | + it('returns an empty object for an empty hash', async() => { |
| 156 | + const result = await allHash({}); |
| 157 | + |
| 158 | + expect(result).toStrictEqual({}); |
| 159 | + }); |
| 160 | + |
| 161 | + it('preserves key ordering from the input hash', async() => { |
| 162 | + const result = await allHash({ |
| 163 | + z: Promise.resolve('z-val'), |
| 164 | + a: Promise.resolve('a-val'), |
| 165 | + }); |
| 166 | + |
| 167 | + expect(Object.keys(result)).toStrictEqual(['z', 'a']); |
| 168 | + expect(result).toStrictEqual({ z: 'z-val', a: 'a-val' }); |
| 169 | + }); |
| 170 | + |
| 171 | + it('resolves non-promise values in the hash', async() => { |
| 172 | + const result = await allHash({ x: 42 }); |
| 173 | + |
| 174 | + expect(result).toStrictEqual({ x: 42 }); |
| 175 | + }); |
| 176 | +}); |
| 177 | + |
| 178 | +describe('allHashSettled', () => { |
| 179 | + it('resolves with fulfilled/rejected statuses for each key', async() => { |
| 180 | + const result = await allHashSettled({ |
| 181 | + ok: Promise.resolve('success'), |
| 182 | + err: Promise.reject(new Error('oops')), |
| 183 | + }); |
| 184 | + |
| 185 | + expect(result.ok).toStrictEqual({ status: 'fulfilled', value: 'success' }); |
| 186 | + expect(result.err.status).toStrictEqual('rejected'); |
| 187 | + expect((result.err as PromiseRejectedResult).reason.message).toStrictEqual('oops'); |
| 188 | + }); |
| 189 | + |
| 190 | + it('returns an empty object for an empty hash', async() => { |
| 191 | + const result = await allHashSettled({}); |
| 192 | + |
| 193 | + expect(result).toStrictEqual({}); |
| 194 | + }); |
| 195 | + |
| 196 | + it('never rejects — always resolves even when all promises fail', async() => { |
| 197 | + await expect( |
| 198 | + allHashSettled({ |
| 199 | + a: Promise.reject(new Error('a-fail')), |
| 200 | + b: Promise.reject(new Error('b-fail')), |
| 201 | + }) |
| 202 | + ).resolves.toBeDefined(); |
| 203 | + }); |
| 204 | +}); |
| 205 | + |
| 206 | +describe('eachLimit', () => { |
| 207 | + it('processes all items and returns results in index order', async() => { |
| 208 | + const items = [1, 2, 3, 4, 5]; |
| 209 | + const result = await eachLimit(items, 2, (item: number) => Promise.resolve(item * 10)); |
| 210 | + |
| 211 | + expect(result).toStrictEqual([10, 20, 30, 40, 50]); |
| 212 | + }); |
| 213 | + |
| 214 | + it('returns an empty array for empty input', async() => { |
| 215 | + const result = await eachLimit([], 3, () => Promise.resolve('x')); |
| 216 | + |
| 217 | + expect(result).toStrictEqual([]); |
| 218 | + }); |
| 219 | + |
| 220 | + it('rejects if any iterator rejects', async() => { |
| 221 | + await expect( |
| 222 | + eachLimit([1, 2, 3], 2, (item: number) => { |
| 223 | + if (item === 2) { |
| 224 | + return Promise.reject(new Error('item-2-failed')); |
| 225 | + } |
| 226 | + |
| 227 | + return Promise.resolve(item); |
| 228 | + }) |
| 229 | + ).rejects.toThrow('item-2-failed'); |
| 230 | + }); |
| 231 | + |
| 232 | + it('handles limit larger than the number of items', async() => { |
| 233 | + const items = ['a', 'b']; |
| 234 | + const result = await eachLimit(items, 100, (item: string) => Promise.resolve(`done-${ item }`)); |
| 235 | + |
| 236 | + expect(result).toStrictEqual(['done-a', 'done-b']); |
| 237 | + }); |
| 238 | + |
| 239 | + it('handles limit of 1 (sequential processing)', async() => { |
| 240 | + const order: number[] = []; |
| 241 | + const items = [1, 2, 3]; |
| 242 | + |
| 243 | + await eachLimit(items, 1, (item: number) => { |
| 244 | + order.push(item); |
| 245 | + |
| 246 | + return Promise.resolve(item); |
| 247 | + }); |
| 248 | + |
| 249 | + expect(order).toStrictEqual([1, 2, 3]); |
| 250 | + }); |
| 251 | + |
| 252 | + it('respects the concurrency limit', async() => { |
| 253 | + let concurrent = 0; |
| 254 | + let maxConcurrent = 0; |
| 255 | + const limit = 2; |
| 256 | + const items = [1, 2, 3, 4, 5]; |
| 257 | + |
| 258 | + await eachLimit(items, limit, (item: number) => { |
| 259 | + concurrent++; |
| 260 | + if (concurrent > maxConcurrent) { |
| 261 | + maxConcurrent = concurrent; |
| 262 | + } |
| 263 | + |
| 264 | + return new Promise<number>((resolve) => { |
| 265 | + setTimeout(() => { |
| 266 | + concurrent--; |
| 267 | + resolve(item); |
| 268 | + }, 5); |
| 269 | + }); |
| 270 | + }); |
| 271 | + |
| 272 | + expect(maxConcurrent).toStrictEqual(limit); |
| 273 | + }); |
| 274 | + |
| 275 | + it('passes the item index as the second argument to the iterator', async() => { |
| 276 | + const indices: number[] = []; |
| 277 | + |
| 278 | + await eachLimit(['x', 'y', 'z'], 3, (_item: string, idx: number) => { |
| 279 | + indices.push(idx); |
| 280 | + |
| 281 | + return Promise.resolve(idx); |
| 282 | + }); |
| 283 | + |
| 284 | + expect(indices).toStrictEqual([0, 1, 2]); |
| 285 | + }); |
| 286 | +}); |
| 287 | + |
| 288 | +describe('deferred', () => { |
| 289 | + it('returns an object with promise, resolve, and reject', () => { |
| 290 | + const d = deferred('test'); |
| 291 | + |
| 292 | + expect(d.promise).toBeInstanceOf(Promise); |
| 293 | + expect(typeof d.resolve).toStrictEqual('function'); |
| 294 | + expect(typeof d.reject).toStrictEqual('function'); |
| 295 | + }); |
| 296 | + |
| 297 | + it('resolves the promise when resolve is called', async() => { |
| 298 | + const d = deferred('resolve-test'); |
| 299 | + |
| 300 | + d.resolve('resolved-value'); |
| 301 | + |
| 302 | + await expect(d.promise).resolves.toStrictEqual('resolved-value'); |
| 303 | + }); |
| 304 | + |
| 305 | + it('rejects the promise when reject is called', async() => { |
| 306 | + const d = deferred('reject-test'); |
| 307 | + |
| 308 | + d.reject(new Error('deferred-error')); |
| 309 | + |
| 310 | + await expect(d.promise).rejects.toThrow('deferred-error'); |
| 311 | + }); |
| 312 | +}); |
| 313 | + |
| 314 | +describe('setPromiseResult', () => { |
| 315 | + it('sets the resolved value on the target object property', async() => { |
| 316 | + const obj: { result?: string } = {}; |
| 317 | + |
| 318 | + setPromiseResult(Promise.resolve('hello'), obj, 'result', 'test label'); |
| 319 | + await new Promise((resolve) => setTimeout(resolve, 0)); |
| 320 | + |
| 321 | + expect(obj.result).toStrictEqual('hello'); |
| 322 | + }); |
| 323 | + |
| 324 | + it('does not throw when the promise rejects — logs a warning instead', async() => { |
| 325 | + const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {}); |
| 326 | + const obj: { value?: string } = {}; |
| 327 | + |
| 328 | + setPromiseResult(Promise.reject(new Error('boom')), obj, 'value', 'failing label'); |
| 329 | + await new Promise((resolve) => setTimeout(resolve, 0)); |
| 330 | + |
| 331 | + expect(obj.value).toBeUndefined(); |
| 332 | + expect(warnSpy).toHaveBeenCalledWith('Failed to: ', 'failing label', expect.any(Error)); |
| 333 | + warnSpy.mockRestore(); |
| 334 | + }); |
| 335 | + |
| 336 | + it('leaves the object property unchanged when the promise rejects', async() => { |
| 337 | + jest.spyOn(console, 'warn').mockImplementation(() => {}); |
| 338 | + const obj: { data: string } = { data: 'original' }; |
| 339 | + |
| 340 | + setPromiseResult(Promise.reject(new Error('err')), obj, 'data', 'test'); |
| 341 | + await new Promise((resolve) => setTimeout(resolve, 0)); |
| 342 | + |
| 343 | + expect(obj.data).toStrictEqual('original'); |
| 344 | + jest.restoreAllMocks(); |
| 345 | + }); |
| 346 | +}); |
0 commit comments