Skip to content

Commit 477a227

Browse files
HugoMendes98Hugo Mendes
authored and
Hugo Mendes
committed
test(producer-consumer): add test for ProducerConsumer
#3
1 parent 3bd0207 commit 477a227

File tree

4 files changed

+401
-15
lines changed

4 files changed

+401
-15
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,375 @@
1+
import { ProducerConsumerInvalidReadParameterException } from "./exceptions";
2+
import { ProducerConsumer } from "./producer-consumer";
3+
import { sleep } from "../../support/sleep";
4+
import { timeFunction } from "../../support/time-function";
5+
import {
6+
ConcurrencyExceedTimeoutException,
7+
ConcurrencyInterruptedException,
8+
ConcurrencyInvalidTimeoutException
9+
} from "../exceptions";
10+
11+
describe("ProducerConsumer", () => {
12+
const delay = 60;
13+
const offsetLow = 5;
14+
const offset = 15;
15+
16+
describe("Input validation", () => {
17+
const prodCons = new ProducerConsumer();
18+
19+
it("should throw a read parameter exception when reading a negative number of items", async () => {
20+
for (const nItem of [-1, -10, -100]) {
21+
await expect(() => prodCons.read(nItem)).rejects.toThrow(
22+
ProducerConsumerInvalidReadParameterException
23+
);
24+
}
25+
});
26+
27+
it("should throw a timeout exception when try-reading with negative timeout", async () => {
28+
for (const timeout of [-1, -10, -100]) {
29+
await expect(() => prodCons.tryRead(timeout, 1)).rejects.toThrow(
30+
ConcurrencyInvalidTimeoutException
31+
);
32+
}
33+
});
34+
35+
it("should throw a read parameter exception when try-reading a negative number of items", async () => {
36+
for (const nItem of [-1, -10, -100]) {
37+
await expect(() => prodCons.tryRead(1000, nItem)).rejects.toThrow(
38+
ProducerConsumerInvalidReadParameterException
39+
);
40+
}
41+
});
42+
43+
it("should throw a timeout exception when try-reading-one with negative timeout", async () => {
44+
for (const timeout of [-1, -10, -100]) {
45+
await expect(() => prodCons.tryReadOne(timeout)).rejects.toThrow(
46+
ConcurrencyInvalidTimeoutException
47+
);
48+
}
49+
});
50+
});
51+
52+
describe("`read` usage", () => {
53+
it("should read immediately with initial items", async () => {
54+
const items = [1, "A string", { msg: "An object" }, ["An Array", 1]] as const;
55+
56+
for (const [i, item] of items.entries()) {
57+
const n = i + 1;
58+
59+
const itemsInitial = Array(n).fill(item);
60+
61+
const prodCons = new ProducerConsumer(itemsInitial);
62+
expect(prodCons.itemsAvailable).toBe(n);
63+
64+
const itemsRead = await prodCons.read(n);
65+
expect(prodCons.itemsAvailable).toBe(0);
66+
67+
expect(itemsRead).toHaveLength(n);
68+
expect(itemsRead).toStrictEqual(itemsInitial);
69+
}
70+
});
71+
72+
it("should wait until items are available", async () => {
73+
const prodCons = new ProducerConsumer<number>();
74+
const valueWrite = [123, 456];
75+
76+
setTimeout(() => {
77+
expect(prodCons.itemsRequired).toBe(2);
78+
expect(prodCons.queueLength).toBe(1);
79+
prodCons.write(...valueWrite);
80+
}, delay);
81+
82+
const [elapsed, valueRead] = await timeFunction(() => prodCons.read(2));
83+
84+
expect(prodCons.itemsRequired).toBe(0);
85+
expect(prodCons.queueLength).toBe(0);
86+
expect(valueRead).toStrictEqual(valueWrite);
87+
88+
expect(elapsed).toBeGreaterThanOrEqual(delay - offsetLow);
89+
expect(elapsed).toBeLessThanOrEqual(delay + offset);
90+
});
91+
});
92+
93+
describe("`tryRead` usage", () => {
94+
it("should work normally", async () => {
95+
const prodCons = new ProducerConsumer();
96+
const [w1, w2] = [1, 2];
97+
98+
setTimeout(() => {
99+
expect(prodCons.itemsRequired).toBe(2);
100+
expect(prodCons.queueLength).toBe(1);
101+
prodCons.write(w1);
102+
103+
expect(prodCons.itemsRequired).toBe(1);
104+
expect(prodCons.queueLength).toBe(1);
105+
}, delay / 2);
106+
107+
setTimeout(() => {
108+
expect(prodCons.itemsRequired).toBe(1);
109+
expect(prodCons.queueLength).toBe(1);
110+
prodCons.write(w2);
111+
112+
expect(prodCons.itemsRequired).toBe(0);
113+
expect(prodCons.queueLength).toBe(0);
114+
}, delay);
115+
116+
const [r1, r2] = await prodCons.tryRead(delay * 2, 2);
117+
118+
expect(r1).toBe(w1);
119+
expect(r2).toBe(w2);
120+
});
121+
122+
it("should immediately read when there is enough items", async () => {
123+
const values = [1, 2, 3, 4, 5];
124+
const [w1, w2, w3, w4, w5] = values;
125+
const prodCons = new ProducerConsumer(values);
126+
127+
const [elapsed1, [r1, r2]] = await timeFunction(() => prodCons.tryRead(1, 2));
128+
const [elapsed2, [r3, r4, r5]] = await timeFunction(() => prodCons.tryRead(1, 3));
129+
130+
expect(r1).toBe(w1);
131+
expect(r2).toBe(w2);
132+
expect(r3).toBe(w3);
133+
expect(r4).toBe(w4);
134+
expect(r5).toBe(w5);
135+
136+
expect(elapsed1).toBeLessThanOrEqual(offset);
137+
expect(elapsed2).toBeLessThanOrEqual(offset);
138+
});
139+
140+
it("should throw an error when the time exceeds", async () => {
141+
const prodCons = new ProducerConsumer(["a"]);
142+
143+
setTimeout(() => expect(prodCons.queueLength).toBe(1), delay / 2);
144+
await expect(() => prodCons.tryRead(delay, 2)).rejects.toThrow(
145+
ConcurrencyExceedTimeoutException
146+
);
147+
148+
// The queue is reset, since the `tryRead` failed
149+
expect(prodCons.queueLength).toBe(0);
150+
expect(prodCons.itemsAvailable).toBe(1);
151+
});
152+
153+
it('should not "reset" the state after a successful `tryRead`', async () => {
154+
const prodCons = new ProducerConsumer([1]);
155+
156+
// "Regular" use
157+
setTimeout(() => prodCons.write(1), delay / 2);
158+
await prodCons.tryRead(delay, 2);
159+
160+
// Ok
161+
expect(prodCons.itemsAvailable).toBe(0);
162+
expect(prodCons.queueLength).toBe(0);
163+
164+
// After the timeout
165+
await sleep(delay);
166+
expect(prodCons.itemsAvailable).toBe(0);
167+
expect(prodCons.queueLength).toBe(0);
168+
});
169+
170+
it("should write to other `read` or `tryRead` a when a `tryRead` fails", async () => {
171+
const prodCons = new ProducerConsumer([1]);
172+
173+
setTimeout(() => {
174+
expect(prodCons.itemsAvailable).toBe(0);
175+
expect(prodCons.itemsRequired).toBe(5);
176+
expect(prodCons.queueLength).toBe(2);
177+
178+
prodCons.write(2);
179+
expect(prodCons.itemsRequired).toBe(4);
180+
expect(prodCons.queueLength).toBe(2);
181+
}, delay);
182+
183+
const [[v1, v2, v3]] = await Promise.all([
184+
// `read` after the `tryRead`
185+
sleep(delay / 2).then(() => prodCons.read(3)),
186+
187+
prodCons
188+
.tryRead(delay * 2, 3)
189+
.catch((error: unknown) => {
190+
if (error instanceof ConcurrencyExceedTimeoutException) {
191+
return [];
192+
}
193+
194+
throw error;
195+
})
196+
.finally(() => {
197+
// The `write` in the `setTimeout` reduced the required items
198+
// of the second read to 1, so still 0 available
199+
expect(prodCons.itemsAvailable).toBe(0);
200+
expect(prodCons.itemsRequired).toBe(1);
201+
expect(prodCons.queueLength).toBe(1);
202+
203+
setTimeout(() => prodCons.write(3, 4), delay);
204+
})
205+
]);
206+
207+
// The state is reset: 3 items written for a single successful read (+ the initial item)
208+
expect(prodCons.itemsAvailable).toBe(1);
209+
expect(prodCons.itemsRequired).toBe(0);
210+
expect(prodCons.queueLength).toBe(0);
211+
212+
expect(v1).toBe(1);
213+
expect(v2).toBe(2);
214+
expect(v3).toBe(3);
215+
216+
expect(await prodCons.readOne()).toBe(4);
217+
});
218+
});
219+
220+
describe("`readOne` usage", () => {
221+
it("should read immediately with initial items", async () => {
222+
const items = [1, "A string", { msg: "An object" }, ["An Array", 1]] as const;
223+
224+
for (const item of items) {
225+
const prodCons = new ProducerConsumer([item]);
226+
expect(prodCons.itemsAvailable).toBe(1);
227+
228+
const itemRead = await prodCons.readOne();
229+
expect(prodCons.itemsAvailable).toBe(0);
230+
231+
expect(itemRead).toStrictEqual(item);
232+
}
233+
});
234+
235+
it("should wait until an item is available", async () => {
236+
const prodCons = new ProducerConsumer<number>();
237+
const valueWrite = 123;
238+
239+
setTimeout(() => {
240+
expect(prodCons.itemsRequired).toBe(1);
241+
expect(prodCons.queueLength).toBe(1);
242+
prodCons.write(valueWrite);
243+
}, delay);
244+
245+
const [elapsed, valueRead] = await timeFunction(() => prodCons.readOne());
246+
247+
expect(prodCons.itemsRequired).toBe(0);
248+
expect(prodCons.queueLength).toBe(0);
249+
expect(valueRead).toStrictEqual(valueWrite);
250+
251+
expect(elapsed).toBeGreaterThanOrEqual(delay - offsetLow);
252+
expect(elapsed).toBeLessThanOrEqual(delay + offset);
253+
});
254+
});
255+
256+
describe("`tryReadOne` usage", () => {
257+
it("should work normally", async () => {
258+
const prodCons = new ProducerConsumer();
259+
const [w1, w2] = [1, 2];
260+
261+
setTimeout(() => {
262+
expect(prodCons.itemsRequired).toBe(2);
263+
expect(prodCons.queueLength).toBe(2);
264+
prodCons.write(w1);
265+
266+
expect(prodCons.itemsRequired).toBe(1);
267+
expect(prodCons.queueLength).toBe(1);
268+
}, delay / 2);
269+
270+
setTimeout(() => {
271+
expect(prodCons.itemsRequired).toBe(1);
272+
expect(prodCons.queueLength).toBe(1);
273+
prodCons.write(w2);
274+
275+
expect(prodCons.itemsRequired).toBe(0);
276+
expect(prodCons.queueLength).toBe(0);
277+
}, delay);
278+
279+
const [r1, r2] = await Promise.all([
280+
prodCons.tryReadOne(delay * 2),
281+
prodCons.tryReadOne(delay * 2)
282+
]);
283+
284+
expect(r1).toBe(w1);
285+
expect(r2).toBe(w2);
286+
});
287+
288+
it("should immediately read when there is enough items", async () => {
289+
const values = [1, 2];
290+
const [w1, w2] = values;
291+
const prodCons = new ProducerConsumer(values);
292+
293+
const [elapsed1, r1] = await timeFunction(() => prodCons.tryReadOne(1));
294+
const [elapsed2, r2] = await timeFunction(() => prodCons.tryReadOne(1));
295+
296+
expect(r1).toBe(w1);
297+
expect(r2).toBe(w2);
298+
299+
expect(elapsed1).toBeLessThanOrEqual(offset);
300+
expect(elapsed2).toBeLessThanOrEqual(offset);
301+
});
302+
303+
it("should throw an error when the time exceeds", async () => {
304+
const prodCons = new ProducerConsumer();
305+
306+
setTimeout(() => {
307+
expect(prodCons.itemsAvailable).toBe(0);
308+
expect(prodCons.itemsRequired).toBe(1);
309+
expect(prodCons.queueLength).toBe(1);
310+
}, delay / 2);
311+
await expect(() => prodCons.tryReadOne(delay)).rejects.toThrow(
312+
ConcurrencyExceedTimeoutException
313+
);
314+
315+
// The queue is reset, since the `tryReadOne` failed
316+
expect(prodCons.itemsAvailable).toBe(0);
317+
expect(prodCons.itemsRequired).toBe(0);
318+
expect(prodCons.queueLength).toBe(0);
319+
});
320+
321+
it('should not "reset" the state after a successful `tryRead`', async () => {
322+
const prodCons = new ProducerConsumer<number>();
323+
324+
// "Regular" use
325+
setTimeout(() => prodCons.write(1), delay / 2);
326+
await prodCons.tryReadOne(delay);
327+
328+
// Ok
329+
expect(prodCons.itemsAvailable).toBe(0);
330+
expect(prodCons.queueLength).toBe(0);
331+
332+
// After the timeout
333+
await sleep(delay);
334+
expect(prodCons.itemsAvailable).toBe(0);
335+
expect(prodCons.queueLength).toBe(0);
336+
});
337+
});
338+
339+
it("should interrupt all", async () => {
340+
const delay = 80;
341+
const prodCons = new ProducerConsumer<number>();
342+
const reason = "test";
343+
344+
setTimeout(() => {
345+
expect(prodCons.itemsAvailable).toBe(0);
346+
expect(prodCons.queueLength).toBe(4);
347+
prodCons.interrupt(reason, [1, 2, 3]);
348+
}, delay);
349+
350+
const errors = await Promise.all([
351+
prodCons.readOne().catch((err: unknown) => err),
352+
prodCons.read(2).catch((err: unknown) => err),
353+
prodCons.tryReadOne(delay * 10).catch((err: unknown) => err),
354+
prodCons.tryRead(delay * 10, 6).catch((err: unknown) => err)
355+
]);
356+
357+
for (const error of errors) {
358+
expect(error).toBeInstanceOf(ConcurrencyInterruptedException);
359+
expect((error as ConcurrencyInterruptedException).getReason()).toBe(reason);
360+
}
361+
362+
expect(prodCons.itemsAvailable).toBe(3);
363+
expect(prodCons.queueLength).toBe(0);
364+
365+
// Same but without items
366+
setTimeout(() => {
367+
prodCons.interrupt(reason);
368+
}, delay);
369+
370+
await Promise.all([prodCons.read(5), prodCons.read(5)]).catch(() => void 0);
371+
372+
expect(prodCons.itemsAvailable).toBe(0);
373+
expect(prodCons.queueLength).toBe(0);
374+
});
375+
});

0 commit comments

Comments
 (0)