mirror of
https://github.com/oven-sh/bun
synced 2026-02-09 10:28:47 +00:00
test(js/internal): add unit tests for Dequeue (#16189)
This commit is contained in:
20
src/js/builtins.d.ts
vendored
20
src/js/builtins.d.ts
vendored
@@ -161,8 +161,26 @@ declare function $toPropertyKey(x: any): PropertyKey;
|
||||
* `$toObject(this, "Class.prototype.method requires that |this| not be null or undefined");`
|
||||
*/
|
||||
declare function $toObject(object: any, errorMessage?: string): object;
|
||||
/**
|
||||
* ## References
|
||||
* - [WebKit - `emit_intrinsic_newArrayWithSize`](https://github.com/oven-sh/WebKit/blob/e1a802a2287edfe7f4046a9dd8307c8b59f5d816/Source/JavaScriptCore/bytecompiler/NodesCodegen.cpp#L2317)
|
||||
*/
|
||||
declare function $newArrayWithSize<T>(size: number): T[];
|
||||
declare function $newArrayWithSpecies(): TODO;
|
||||
/**
|
||||
* Optimized path for creating a new array storing objects with the same homogenous Structure
|
||||
* as {@link array}.
|
||||
*
|
||||
* @param size the initial size of the new array
|
||||
* @param array the array whose shape we want to copy
|
||||
*
|
||||
* @returns a new array
|
||||
*
|
||||
* ## References
|
||||
* - [WebKit - `emit_intrinsic_newArrayWithSpecies`](https://github.com/oven-sh/WebKit/blob/e1a802a2287edfe7f4046a9dd8307c8b59f5d816/Source/JavaScriptCore/bytecompiler/NodesCodegen.cpp#L2328)
|
||||
* - [WebKit - #4909](https://github.com/WebKit/WebKit/pull/4909)
|
||||
* - [WebKit Bugzilla - Related Issue/Ticket](https://bugs.webkit.org/show_bug.cgi?id=245797)
|
||||
*/
|
||||
declare function $newArrayWithSpecies<T>(size: number, array: T[]): T[];
|
||||
declare function $newPromise(): TODO;
|
||||
declare function $createPromise(): TODO;
|
||||
declare const $iterationKindKey: TODO;
|
||||
|
||||
@@ -89,8 +89,8 @@ export function validateAndNormalizeQueuingStrategy(size, highWaterMark) {
|
||||
|
||||
$linkTimeConstant;
|
||||
export function createFIFO() {
|
||||
const Denqueue = require("internal/fifo");
|
||||
return new Denqueue();
|
||||
const Dequeue = require("internal/fifo");
|
||||
return new Dequeue();
|
||||
}
|
||||
|
||||
export function newQueue() {
|
||||
|
||||
@@ -151,3 +151,4 @@ export const bindgen = $zig("bindgen_test.zig", "getBindgenTestFunctions") as {
|
||||
};
|
||||
|
||||
export const noOpForTesting = $cpp("NoOpForTesting.cpp", "createNoOpForTesting");
|
||||
export const Dequeue = require("internal/fifo");
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
var slice = Array.prototype.slice;
|
||||
class Denqueue {
|
||||
class Dequeue<T> {
|
||||
_head: number;
|
||||
_tail: number;
|
||||
_capacityMask: number;
|
||||
_list: (T | undefined)[];
|
||||
|
||||
constructor() {
|
||||
this._head = 0;
|
||||
this._tail = 0;
|
||||
@@ -8,26 +13,21 @@ class Denqueue {
|
||||
this._list = $newArrayWithSize(4);
|
||||
}
|
||||
|
||||
_head;
|
||||
_tail;
|
||||
_capacityMask;
|
||||
_list;
|
||||
|
||||
size() {
|
||||
size(): number {
|
||||
if (this._head === this._tail) return 0;
|
||||
if (this._head < this._tail) return this._tail - this._head;
|
||||
else return this._capacityMask + 1 - (this._head - this._tail);
|
||||
}
|
||||
|
||||
isEmpty() {
|
||||
isEmpty(): boolean {
|
||||
return this.size() == 0;
|
||||
}
|
||||
|
||||
isNotEmpty() {
|
||||
isNotEmpty(): boolean {
|
||||
return this.size() > 0;
|
||||
}
|
||||
|
||||
shift() {
|
||||
shift(): T | undefined {
|
||||
var { _head: head, _tail, _list, _capacityMask } = this;
|
||||
if (head === _tail) return undefined;
|
||||
var item = _list[head];
|
||||
@@ -37,24 +37,21 @@ class Denqueue {
|
||||
return item;
|
||||
}
|
||||
|
||||
peek() {
|
||||
peek(): T | undefined {
|
||||
if (this._head === this._tail) return undefined;
|
||||
return this._list[this._head];
|
||||
}
|
||||
|
||||
push(item) {
|
||||
push(item: T): void {
|
||||
var tail = this._tail;
|
||||
$putByValDirect(this._list, tail, item);
|
||||
this._tail = (tail + 1) & this._capacityMask;
|
||||
if (this._tail === this._head) {
|
||||
this._growArray();
|
||||
}
|
||||
// if (this._capacity && this.size() > this._capacity) {
|
||||
// this.shift();
|
||||
// }
|
||||
}
|
||||
|
||||
toArray(fullCopy) {
|
||||
toArray(fullCopy: boolean): T[] {
|
||||
var list = this._list;
|
||||
var len = $toLength(list.length);
|
||||
|
||||
@@ -66,19 +63,19 @@ class Denqueue {
|
||||
var j = 0;
|
||||
for (var i = _head; i < len; i++) $putByValDirect(array, j++, list[i]);
|
||||
for (var i = 0; i < _tail; i++) $putByValDirect(array, j++, list[i]);
|
||||
return array;
|
||||
return array as T[];
|
||||
} else {
|
||||
return slice.$call(list, this._head, this._tail);
|
||||
}
|
||||
}
|
||||
|
||||
clear() {
|
||||
clear(): void {
|
||||
this._head = 0;
|
||||
this._tail = 0;
|
||||
this._list.fill(undefined);
|
||||
}
|
||||
|
||||
_growArray() {
|
||||
private _growArray(): void {
|
||||
if (this._head) {
|
||||
// copy existing data, head to end, then beginning to tail.
|
||||
this._list = this.toArray(true);
|
||||
@@ -92,10 +89,10 @@ class Denqueue {
|
||||
this._capacityMask = (this._capacityMask << 1) | 1;
|
||||
}
|
||||
|
||||
_shrinkArray() {
|
||||
private _shrinkArray(): void {
|
||||
this._list.length >>>= 1;
|
||||
this._capacityMask >>>= 1;
|
||||
}
|
||||
}
|
||||
|
||||
export default Denqueue;
|
||||
export default Dequeue;
|
||||
|
||||
245
test/internal/fifo.test.ts
Normal file
245
test/internal/fifo.test.ts
Normal file
@@ -0,0 +1,245 @@
|
||||
import { Dequeue } from "bun:internal-for-testing";
|
||||
import { describe, expect, test, it, beforeAll, beforeEach } from "bun:test";
|
||||
|
||||
/**
|
||||
* Implements the same API as {@link Dequeue} but uses a simple list as the
|
||||
* backing store.
|
||||
*
|
||||
* Used to check expected behavior.
|
||||
*/
|
||||
class DequeueList<T> {
|
||||
private _list: T[];
|
||||
|
||||
constructor() {
|
||||
this._list = [];
|
||||
}
|
||||
|
||||
size(): number {
|
||||
return this._list.length;
|
||||
}
|
||||
|
||||
isEmpty(): boolean {
|
||||
return this.size() == 0;
|
||||
}
|
||||
|
||||
isNotEmpty(): boolean {
|
||||
return this.size() > 0;
|
||||
}
|
||||
|
||||
shift(): T | undefined {
|
||||
return this._list.shift();
|
||||
}
|
||||
|
||||
peek(): T | undefined {
|
||||
return this._list[0];
|
||||
}
|
||||
|
||||
push(item: T): void {
|
||||
this._list.push(item);
|
||||
}
|
||||
|
||||
toArray(fullCopy: boolean): T[] {
|
||||
return fullCopy ? this._list.slice() : this._list;
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this._list = [];
|
||||
}
|
||||
}
|
||||
|
||||
describe("Given an empty queue", () => {
|
||||
let queue: Dequeue<number>;
|
||||
|
||||
beforeEach(() => {
|
||||
queue = new Dequeue();
|
||||
});
|
||||
|
||||
it("has a size of 0", () => {
|
||||
expect(queue.size()).toBe(0);
|
||||
});
|
||||
|
||||
it("is empty", () => {
|
||||
expect(queue.isEmpty()).toBe(true);
|
||||
expect(queue.isNotEmpty()).toBe(false);
|
||||
});
|
||||
|
||||
it("shift() returns undefined", () => {
|
||||
expect(queue.shift()).toBe(undefined);
|
||||
expect(queue.size()).toBe(0);
|
||||
});
|
||||
|
||||
it("has an initial capacity of 4", () => {
|
||||
expect(queue._list.length).toBe(4);
|
||||
expect(queue._capacityMask).toBe(3);
|
||||
});
|
||||
|
||||
it("toArray() returns an empty array", () => {
|
||||
expect(queue.toArray()).toEqual([]);
|
||||
});
|
||||
|
||||
describe("When an element is pushed", () => {
|
||||
beforeEach(() => {
|
||||
queue.push(42);
|
||||
});
|
||||
|
||||
it("has a size of 1", () => {
|
||||
expect(queue.size()).toBe(1);
|
||||
});
|
||||
|
||||
it("can be peeked without removing it", () => {
|
||||
expect(queue.peek()).toBe(42);
|
||||
expect(queue.size()).toBe(1);
|
||||
});
|
||||
|
||||
it("is not empty", () => {
|
||||
expect(queue.isEmpty()).toBe(false);
|
||||
expect(queue.isNotEmpty()).toBe(true);
|
||||
});
|
||||
|
||||
it("can be shifted out", () => {
|
||||
const el = queue.shift();
|
||||
expect(el).toBe(42);
|
||||
expect(queue.size()).toBe(0);
|
||||
expect(queue.isEmpty()).toBe(true);
|
||||
});
|
||||
}); // </When an element is pushed>
|
||||
}); // </Given an empty queue>
|
||||
|
||||
describe("grow boundary conditions", () => {
|
||||
describe.each([3, 4, 16])("when %d items are pushed", n => {
|
||||
let queue: Dequeue<number>;
|
||||
|
||||
beforeEach(() => {
|
||||
queue = new Dequeue();
|
||||
for (let i = 0; i < n; i++) {
|
||||
queue.push(i);
|
||||
}
|
||||
});
|
||||
|
||||
it(`has a size of ${n}`, () => {
|
||||
expect(queue.size()).toBe(n);
|
||||
});
|
||||
|
||||
it("is not empty", () => {
|
||||
expect(queue.isEmpty()).toBe(false);
|
||||
expect(queue.isNotEmpty()).toBe(true);
|
||||
});
|
||||
|
||||
it(`can shift() ${n} times`, () => {
|
||||
for (let i = 0; i < n; i++) {
|
||||
expect(queue.peek()).toBe(i);
|
||||
expect(queue.shift()).toBe(i);
|
||||
}
|
||||
expect(queue.size()).toBe(0);
|
||||
expect(queue.shift()).toBe(undefined);
|
||||
});
|
||||
|
||||
it("toArray() returns [0..n-1]", () => {
|
||||
// same as repeated push() but only allocates once
|
||||
var expected = new Array<number>(n);
|
||||
for (let i = 0; i < n; i++) {
|
||||
expected[i] = i;
|
||||
}
|
||||
expect(queue.toArray()).toEqual(expected);
|
||||
});
|
||||
});
|
||||
}); // </grow boundary conditions>
|
||||
|
||||
describe("adding and removing items", () => {
|
||||
let queue: Dequeue<number>;
|
||||
let expected: DequeueList<number>;
|
||||
|
||||
describe("when 10k items are pushed", () => {
|
||||
beforeEach(() => {
|
||||
queue = new Dequeue();
|
||||
expected = new DequeueList();
|
||||
|
||||
for (let i = 0; i < 10_000; i++) {
|
||||
queue.push(i);
|
||||
expected.push(i);
|
||||
}
|
||||
});
|
||||
|
||||
it("has a size of 10000", () => {
|
||||
expect(queue.size()).toBe(10_000);
|
||||
expect(expected.size()).toBe(10_000);
|
||||
});
|
||||
|
||||
describe("when 10 items are shifted", () => {
|
||||
beforeEach(() => {
|
||||
for (let i = 0; i < 10; i++) {
|
||||
expect(queue.shift()).toBe(expected.shift());
|
||||
}
|
||||
});
|
||||
|
||||
it("has a size of 9990", () => {
|
||||
expect(queue.size()).toBe(9990);
|
||||
expect(expected.size()).toBe(9990);
|
||||
});
|
||||
});
|
||||
}); // </when 10k items are pushed>
|
||||
|
||||
describe("when 1k items are pushed, then removed", () => {
|
||||
beforeEach(() => {
|
||||
queue = new Dequeue();
|
||||
expected = new DequeueList();
|
||||
|
||||
for (let i = 0; i < 1_000; i++) {
|
||||
queue.push(i);
|
||||
expected.push(i);
|
||||
}
|
||||
expect(queue.size()).toBe(1_000);
|
||||
|
||||
while (queue.isNotEmpty()) {
|
||||
expect(queue.shift()).toBe(expected.shift());
|
||||
}
|
||||
});
|
||||
|
||||
it("is now empty", () => {
|
||||
expect(queue.size()).toBe(0);
|
||||
expect(queue.isEmpty()).toBeTrue();
|
||||
expect(queue.isNotEmpty()).toBeFalse();
|
||||
});
|
||||
|
||||
it("when new items are added, the backing list is resized", () => {
|
||||
for (let i = 0; i < 10_000; i++) {
|
||||
queue.push(i);
|
||||
expected.push(i);
|
||||
expect(queue.size()).toBe(expected.size());
|
||||
expect(queue.peek()).toBe(expected.peek());
|
||||
expect(queue.isEmpty()).toBeFalse();
|
||||
expect(queue.isNotEmpty()).toBeTrue();
|
||||
}
|
||||
});
|
||||
}); // </when 1k items are pushed, then removed>
|
||||
|
||||
it("pushing and shifting a lot of items affects the size and backing list correctly", () => {
|
||||
queue = new Dequeue();
|
||||
expected = new DequeueList();
|
||||
|
||||
for (let i = 0; i < 15_000; i++) {
|
||||
queue.push(i);
|
||||
expected.push(i);
|
||||
expect(queue.size()).toBe(expected.size());
|
||||
expect(queue.peek()).toBe(expected.peek());
|
||||
expect(queue.isEmpty()).toBeFalse();
|
||||
expect(queue.isNotEmpty()).toBeTrue();
|
||||
}
|
||||
|
||||
// shift() shrinks the backing array when tail > 10,000 and the list is
|
||||
// shrunk too far (tail <= list.length >>> 2)
|
||||
for (let i = 0; i < 10_000; i++) {
|
||||
expect(queue.shift()).toBe(expected.shift());
|
||||
expect(queue.size()).toBe(expected.size());
|
||||
}
|
||||
|
||||
for (let i = 0; i < 5_000; i++) {
|
||||
queue.push(i);
|
||||
expected.push(i);
|
||||
expect(queue.size()).toBe(expected.size());
|
||||
expect(queue.peek()).toBe(expected.peek());
|
||||
expect(queue.isEmpty()).toBeFalse();
|
||||
expect(queue.isNotEmpty()).toBeTrue();
|
||||
}
|
||||
}); // </pushing a lot of items affects the size and backing list correctly>
|
||||
}); // </adding and removing items>
|
||||
Reference in New Issue
Block a user