Skip to content

Commit 9757ee4

Browse files
authored
test(js/internal): add unit tests for Dequeue (#16189)
1 parent 833b718 commit 9757ee4

File tree

5 files changed

+285
-24
lines changed

5 files changed

+285
-24
lines changed

Diff for: src/js/builtins.d.ts

+19-1
Original file line numberDiff line numberDiff line change
@@ -161,8 +161,26 @@ declare function $toPropertyKey(x: any): PropertyKey;
161161
* `$toObject(this, "Class.prototype.method requires that |this| not be null or undefined");`
162162
*/
163163
declare function $toObject(object: any, errorMessage?: string): object;
164+
/**
165+
* ## References
166+
* - [WebKit - `emit_intrinsic_newArrayWithSize`](https://github.com/oven-sh/WebKit/blob/e1a802a2287edfe7f4046a9dd8307c8b59f5d816/Source/JavaScriptCore/bytecompiler/NodesCodegen.cpp#L2317)
167+
*/
164168
declare function $newArrayWithSize<T>(size: number): T[];
165-
declare function $newArrayWithSpecies(): TODO;
169+
/**
170+
* Optimized path for creating a new array storing objects with the same homogenous Structure
171+
* as {@link array}.
172+
*
173+
* @param size the initial size of the new array
174+
* @param array the array whose shape we want to copy
175+
*
176+
* @returns a new array
177+
*
178+
* ## References
179+
* - [WebKit - `emit_intrinsic_newArrayWithSpecies`](https://github.com/oven-sh/WebKit/blob/e1a802a2287edfe7f4046a9dd8307c8b59f5d816/Source/JavaScriptCore/bytecompiler/NodesCodegen.cpp#L2328)
180+
* - [WebKit - #4909](https://github.com/WebKit/WebKit/pull/4909)
181+
* - [WebKit Bugzilla - Related Issue/Ticket](https://bugs.webkit.org/show_bug.cgi?id=245797)
182+
*/
183+
declare function $newArrayWithSpecies<T>(size: number, array: T[]): T[];
166184
declare function $newPromise(): TODO;
167185
declare function $createPromise(): TODO;
168186
declare const $iterationKindKey: TODO;

Diff for: src/js/builtins/StreamInternals.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -89,8 +89,8 @@ export function validateAndNormalizeQueuingStrategy(size, highWaterMark) {
8989

9090
$linkTimeConstant;
9191
export function createFIFO() {
92-
const Denqueue = require("internal/fifo");
93-
return new Denqueue();
92+
const Dequeue = require("internal/fifo");
93+
return new Dequeue();
9494
}
9595

9696
export function newQueue() {

Diff for: src/js/internal-for-testing.ts

+1
Original file line numberDiff line numberDiff line change
@@ -151,3 +151,4 @@ export const bindgen = $zig("bindgen_test.zig", "getBindgenTestFunctions") as {
151151
};
152152

153153
export const noOpForTesting = $cpp("NoOpForTesting.cpp", "createNoOpForTesting");
154+
export const Dequeue = require("internal/fifo");

Diff for: src/js/internal/fifo.ts

+18-21
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
var slice = Array.prototype.slice;
2-
class Denqueue {
2+
class Dequeue<T> {
3+
_head: number;
4+
_tail: number;
5+
_capacityMask: number;
6+
_list: (T | undefined)[];
7+
38
constructor() {
49
this._head = 0;
510
this._tail = 0;
@@ -8,26 +13,21 @@ class Denqueue {
813
this._list = $newArrayWithSize(4);
914
}
1015

11-
_head;
12-
_tail;
13-
_capacityMask;
14-
_list;
15-
16-
size() {
16+
size(): number {
1717
if (this._head === this._tail) return 0;
1818
if (this._head < this._tail) return this._tail - this._head;
1919
else return this._capacityMask + 1 - (this._head - this._tail);
2020
}
2121

22-
isEmpty() {
22+
isEmpty(): boolean {
2323
return this.size() == 0;
2424
}
2525

26-
isNotEmpty() {
26+
isNotEmpty(): boolean {
2727
return this.size() > 0;
2828
}
2929

30-
shift() {
30+
shift(): T | undefined {
3131
var { _head: head, _tail, _list, _capacityMask } = this;
3232
if (head === _tail) return undefined;
3333
var item = _list[head];
@@ -37,24 +37,21 @@ class Denqueue {
3737
return item;
3838
}
3939

40-
peek() {
40+
peek(): T | undefined {
4141
if (this._head === this._tail) return undefined;
4242
return this._list[this._head];
4343
}
4444

45-
push(item) {
45+
push(item: T): void {
4646
var tail = this._tail;
4747
$putByValDirect(this._list, tail, item);
4848
this._tail = (tail + 1) & this._capacityMask;
4949
if (this._tail === this._head) {
5050
this._growArray();
5151
}
52-
// if (this._capacity && this.size() > this._capacity) {
53-
// this.shift();
54-
// }
5552
}
5653

57-
toArray(fullCopy) {
54+
toArray(fullCopy: boolean): T[] {
5855
var list = this._list;
5956
var len = $toLength(list.length);
6057

@@ -66,19 +63,19 @@ class Denqueue {
6663
var j = 0;
6764
for (var i = _head; i < len; i++) $putByValDirect(array, j++, list[i]);
6865
for (var i = 0; i < _tail; i++) $putByValDirect(array, j++, list[i]);
69-
return array;
66+
return array as T[];
7067
} else {
7168
return slice.$call(list, this._head, this._tail);
7269
}
7370
}
7471

75-
clear() {
72+
clear(): void {
7673
this._head = 0;
7774
this._tail = 0;
7875
this._list.fill(undefined);
7976
}
8077

81-
_growArray() {
78+
private _growArray(): void {
8279
if (this._head) {
8380
// copy existing data, head to end, then beginning to tail.
8481
this._list = this.toArray(true);
@@ -92,10 +89,10 @@ class Denqueue {
9289
this._capacityMask = (this._capacityMask << 1) | 1;
9390
}
9491

95-
_shrinkArray() {
92+
private _shrinkArray(): void {
9693
this._list.length >>>= 1;
9794
this._capacityMask >>>= 1;
9895
}
9996
}
10097

101-
export default Denqueue;
98+
export default Dequeue;

Diff for: test/internal/fifo.test.ts

+245
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
1+
import { Dequeue } from "bun:internal-for-testing";
2+
import { describe, expect, test, it, beforeAll, beforeEach } from "bun:test";
3+
4+
/**
5+
* Implements the same API as {@link Dequeue} but uses a simple list as the
6+
* backing store.
7+
*
8+
* Used to check expected behavior.
9+
*/
10+
class DequeueList<T> {
11+
private _list: T[];
12+
13+
constructor() {
14+
this._list = [];
15+
}
16+
17+
size(): number {
18+
return this._list.length;
19+
}
20+
21+
isEmpty(): boolean {
22+
return this.size() == 0;
23+
}
24+
25+
isNotEmpty(): boolean {
26+
return this.size() > 0;
27+
}
28+
29+
shift(): T | undefined {
30+
return this._list.shift();
31+
}
32+
33+
peek(): T | undefined {
34+
return this._list[0];
35+
}
36+
37+
push(item: T): void {
38+
this._list.push(item);
39+
}
40+
41+
toArray(fullCopy: boolean): T[] {
42+
return fullCopy ? this._list.slice() : this._list;
43+
}
44+
45+
clear(): void {
46+
this._list = [];
47+
}
48+
}
49+
50+
describe("Given an empty queue", () => {
51+
let queue: Dequeue<number>;
52+
53+
beforeEach(() => {
54+
queue = new Dequeue();
55+
});
56+
57+
it("has a size of 0", () => {
58+
expect(queue.size()).toBe(0);
59+
});
60+
61+
it("is empty", () => {
62+
expect(queue.isEmpty()).toBe(true);
63+
expect(queue.isNotEmpty()).toBe(false);
64+
});
65+
66+
it("shift() returns undefined", () => {
67+
expect(queue.shift()).toBe(undefined);
68+
expect(queue.size()).toBe(0);
69+
});
70+
71+
it("has an initial capacity of 4", () => {
72+
expect(queue._list.length).toBe(4);
73+
expect(queue._capacityMask).toBe(3);
74+
});
75+
76+
it("toArray() returns an empty array", () => {
77+
expect(queue.toArray()).toEqual([]);
78+
});
79+
80+
describe("When an element is pushed", () => {
81+
beforeEach(() => {
82+
queue.push(42);
83+
});
84+
85+
it("has a size of 1", () => {
86+
expect(queue.size()).toBe(1);
87+
});
88+
89+
it("can be peeked without removing it", () => {
90+
expect(queue.peek()).toBe(42);
91+
expect(queue.size()).toBe(1);
92+
});
93+
94+
it("is not empty", () => {
95+
expect(queue.isEmpty()).toBe(false);
96+
expect(queue.isNotEmpty()).toBe(true);
97+
});
98+
99+
it("can be shifted out", () => {
100+
const el = queue.shift();
101+
expect(el).toBe(42);
102+
expect(queue.size()).toBe(0);
103+
expect(queue.isEmpty()).toBe(true);
104+
});
105+
}); // </When an element is pushed>
106+
}); // </Given an empty queue>
107+
108+
describe("grow boundary conditions", () => {
109+
describe.each([3, 4, 16])("when %d items are pushed", n => {
110+
let queue: Dequeue<number>;
111+
112+
beforeEach(() => {
113+
queue = new Dequeue();
114+
for (let i = 0; i < n; i++) {
115+
queue.push(i);
116+
}
117+
});
118+
119+
it(`has a size of ${n}`, () => {
120+
expect(queue.size()).toBe(n);
121+
});
122+
123+
it("is not empty", () => {
124+
expect(queue.isEmpty()).toBe(false);
125+
expect(queue.isNotEmpty()).toBe(true);
126+
});
127+
128+
it(`can shift() ${n} times`, () => {
129+
for (let i = 0; i < n; i++) {
130+
expect(queue.peek()).toBe(i);
131+
expect(queue.shift()).toBe(i);
132+
}
133+
expect(queue.size()).toBe(0);
134+
expect(queue.shift()).toBe(undefined);
135+
});
136+
137+
it("toArray() returns [0..n-1]", () => {
138+
// same as repeated push() but only allocates once
139+
var expected = new Array<number>(n);
140+
for (let i = 0; i < n; i++) {
141+
expected[i] = i;
142+
}
143+
expect(queue.toArray()).toEqual(expected);
144+
});
145+
});
146+
}); // </grow boundary conditions>
147+
148+
describe("adding and removing items", () => {
149+
let queue: Dequeue<number>;
150+
let expected: DequeueList<number>;
151+
152+
describe("when 10k items are pushed", () => {
153+
beforeEach(() => {
154+
queue = new Dequeue();
155+
expected = new DequeueList();
156+
157+
for (let i = 0; i < 10_000; i++) {
158+
queue.push(i);
159+
expected.push(i);
160+
}
161+
});
162+
163+
it("has a size of 10000", () => {
164+
expect(queue.size()).toBe(10_000);
165+
expect(expected.size()).toBe(10_000);
166+
});
167+
168+
describe("when 10 items are shifted", () => {
169+
beforeEach(() => {
170+
for (let i = 0; i < 10; i++) {
171+
expect(queue.shift()).toBe(expected.shift());
172+
}
173+
});
174+
175+
it("has a size of 9990", () => {
176+
expect(queue.size()).toBe(9990);
177+
expect(expected.size()).toBe(9990);
178+
});
179+
});
180+
}); // </when 10k items are pushed>
181+
182+
describe("when 1k items are pushed, then removed", () => {
183+
beforeEach(() => {
184+
queue = new Dequeue();
185+
expected = new DequeueList();
186+
187+
for (let i = 0; i < 1_000; i++) {
188+
queue.push(i);
189+
expected.push(i);
190+
}
191+
expect(queue.size()).toBe(1_000);
192+
193+
while (queue.isNotEmpty()) {
194+
expect(queue.shift()).toBe(expected.shift());
195+
}
196+
});
197+
198+
it("is now empty", () => {
199+
expect(queue.size()).toBe(0);
200+
expect(queue.isEmpty()).toBeTrue();
201+
expect(queue.isNotEmpty()).toBeFalse();
202+
});
203+
204+
it("when new items are added, the backing list is resized", () => {
205+
for (let i = 0; i < 10_000; i++) {
206+
queue.push(i);
207+
expected.push(i);
208+
expect(queue.size()).toBe(expected.size());
209+
expect(queue.peek()).toBe(expected.peek());
210+
expect(queue.isEmpty()).toBeFalse();
211+
expect(queue.isNotEmpty()).toBeTrue();
212+
}
213+
});
214+
}); // </when 1k items are pushed, then removed>
215+
216+
it("pushing and shifting a lot of items affects the size and backing list correctly", () => {
217+
queue = new Dequeue();
218+
expected = new DequeueList();
219+
220+
for (let i = 0; i < 15_000; i++) {
221+
queue.push(i);
222+
expected.push(i);
223+
expect(queue.size()).toBe(expected.size());
224+
expect(queue.peek()).toBe(expected.peek());
225+
expect(queue.isEmpty()).toBeFalse();
226+
expect(queue.isNotEmpty()).toBeTrue();
227+
}
228+
229+
// shift() shrinks the backing array when tail > 10,000 and the list is
230+
// shrunk too far (tail <= list.length >>> 2)
231+
for (let i = 0; i < 10_000; i++) {
232+
expect(queue.shift()).toBe(expected.shift());
233+
expect(queue.size()).toBe(expected.size());
234+
}
235+
236+
for (let i = 0; i < 5_000; i++) {
237+
queue.push(i);
238+
expected.push(i);
239+
expect(queue.size()).toBe(expected.size());
240+
expect(queue.peek()).toBe(expected.peek());
241+
expect(queue.isEmpty()).toBeFalse();
242+
expect(queue.isNotEmpty()).toBeTrue();
243+
}
244+
}); // </pushing a lot of items affects the size and backing list correctly>
245+
}); // </adding and removing items>

0 commit comments

Comments
 (0)