mirror of
https://github.com/SikongJueluo/cc-utils.git
synced 2025-11-04 19:27:50 +08:00
Compare commits
2 Commits
9d9dcade7b
...
4e71fbffc3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4e71fbffc3 | ||
| a3479865c8 |
94
src/lib/Queue.ts
Normal file
94
src/lib/Queue.ts
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
export class Node<T> {
|
||||||
|
public value: T;
|
||||||
|
public next?: Node<T>
|
||||||
|
public prev?: Node<T>
|
||||||
|
|
||||||
|
constructor(value: T, next?: Node<T>, prev?: Node<T>) {
|
||||||
|
this.value = value;
|
||||||
|
this.next = next;
|
||||||
|
this.prev = prev;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Queue<T> {
|
||||||
|
private _head?: Node<T>;
|
||||||
|
private _tail?: Node<T>;
|
||||||
|
private _size: number;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this._head = undefined;
|
||||||
|
this._tail = undefined;
|
||||||
|
this._size = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public enqueue(data: T): void {
|
||||||
|
const node = new Node(data);
|
||||||
|
|
||||||
|
if (this._head === undefined) {
|
||||||
|
this._head = node;
|
||||||
|
this._tail = node;
|
||||||
|
} else {
|
||||||
|
this._tail!.next = node;
|
||||||
|
node.prev = this._tail;
|
||||||
|
this._tail = node;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._size++;
|
||||||
|
}
|
||||||
|
|
||||||
|
public dequeue(): T | undefined {
|
||||||
|
const node = this._head;
|
||||||
|
if (node === undefined) return undefined;
|
||||||
|
|
||||||
|
this._head = this._head!.next;
|
||||||
|
this._head!.prev = undefined;
|
||||||
|
this._size--;
|
||||||
|
|
||||||
|
return node.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public clear(): void {
|
||||||
|
this._head = undefined;
|
||||||
|
this._tail = undefined;
|
||||||
|
this._size = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public peek(): T | undefined {
|
||||||
|
if (this._head === undefined) return undefined;
|
||||||
|
return this._head.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public size(): number {
|
||||||
|
return this._size;
|
||||||
|
}
|
||||||
|
|
||||||
|
public toArray(): T[] | undefined {
|
||||||
|
if (this._size === 0) return undefined;
|
||||||
|
|
||||||
|
const array: T[] = [];
|
||||||
|
let currentNode: Node<T> = this._head!;
|
||||||
|
for (let i = 0; i < this._size; i++) {
|
||||||
|
if (currentNode.value !== undefined)
|
||||||
|
array.push(currentNode.value);
|
||||||
|
|
||||||
|
currentNode = currentNode.next!;
|
||||||
|
}
|
||||||
|
return array;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Symbol.iterator](): Iterator<T> {
|
||||||
|
let currentNode = this._head;
|
||||||
|
|
||||||
|
return {
|
||||||
|
next(): IteratorResult<T> {
|
||||||
|
if (currentNode === undefined) {
|
||||||
|
return { value: undefined, done: true }
|
||||||
|
} else {
|
||||||
|
const data = currentNode.value;
|
||||||
|
currentNode = currentNode.next;
|
||||||
|
return { value: data, done: false }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
170
src/lib/Semaphore.ts
Normal file
170
src/lib/Semaphore.ts
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
import { SortedArray } from "./SortedArray";
|
||||||
|
|
||||||
|
const E_CANCELED = new Error("Request canceled");
|
||||||
|
// const E_INSUFFICIENT_RESOURCES = new Error("Insufficient resources");
|
||||||
|
|
||||||
|
interface QueueEntry {
|
||||||
|
resolve(result: [number, () => void]): void;
|
||||||
|
reject(error: unknown): void;
|
||||||
|
weight: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Waiter {
|
||||||
|
resolve(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Semaphore {
|
||||||
|
private _value: number;
|
||||||
|
private _cancelError: Error;
|
||||||
|
private _queue = new SortedArray<QueueEntry>();
|
||||||
|
private _waiters = new SortedArray<Waiter>();
|
||||||
|
|
||||||
|
constructor(value: number, cancelError: Error = E_CANCELED) {
|
||||||
|
if (value < 0) {
|
||||||
|
throw new Error("Semaphore value must be non-negative");
|
||||||
|
}
|
||||||
|
this._value = value;
|
||||||
|
this._cancelError = cancelError;
|
||||||
|
}
|
||||||
|
|
||||||
|
acquire(weight = 1, priority = 0): Promise<[number, () => void]> {
|
||||||
|
if (weight <= 0) {
|
||||||
|
throw new Error(`invalid weight ${weight}: must be positive`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const entry: QueueEntry = { resolve, reject, weight };
|
||||||
|
|
||||||
|
if (this._queue.toArray().length === 0 && weight <= this._value) {
|
||||||
|
this._dispatchItem(entry);
|
||||||
|
} else {
|
||||||
|
this._queue.push({ priority, data: entry });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
tryAcquire(weight = 1): (() => void) | null {
|
||||||
|
if (weight <= 0) {
|
||||||
|
throw new Error(`invalid weight ${weight}: must be positive`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (weight > this._value || this._queue.toArray().length > 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._value -= weight;
|
||||||
|
return this._newReleaser(weight);
|
||||||
|
}
|
||||||
|
|
||||||
|
async runExclusive<T>(
|
||||||
|
callback: (value: number) => T | Promise<T>,
|
||||||
|
weight = 1,
|
||||||
|
priority = 0,
|
||||||
|
): Promise<T> {
|
||||||
|
const [value, release] = await this.acquire(weight, priority);
|
||||||
|
try {
|
||||||
|
return await callback(value);
|
||||||
|
} finally {
|
||||||
|
release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
waitForUnlock(weight = 1, priority = 0): Promise<void> {
|
||||||
|
if (weight <= 0) {
|
||||||
|
throw new Error(`invalid weight ${weight}: must be positive`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._couldLockImmediately(weight)) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise<void>((resolve) => {
|
||||||
|
const waiter: Waiter = { resolve };
|
||||||
|
this._waiters.push({ priority, data: waiter });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
isLocked(): boolean {
|
||||||
|
return this._value <= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
getValue(): number {
|
||||||
|
return this._value;
|
||||||
|
}
|
||||||
|
|
||||||
|
setValue(value: number): void {
|
||||||
|
if (value < 0) {
|
||||||
|
throw new Error("Semaphore value must be non-negative");
|
||||||
|
}
|
||||||
|
this._value = value;
|
||||||
|
this._dispatchQueue();
|
||||||
|
}
|
||||||
|
|
||||||
|
release(weight = 1): void {
|
||||||
|
if (weight <= 0) {
|
||||||
|
throw new Error(`invalid weight ${weight}: must be positive`);
|
||||||
|
}
|
||||||
|
this._value += weight;
|
||||||
|
this._dispatchQueue();
|
||||||
|
}
|
||||||
|
|
||||||
|
cancel(): void {
|
||||||
|
const queueItems = this._queue.toArray();
|
||||||
|
queueItems.forEach((entry) => entry.reject(this._cancelError));
|
||||||
|
this._queue.clear();
|
||||||
|
|
||||||
|
const waiters = this._waiters.toArray();
|
||||||
|
waiters.forEach((waiter) => waiter.resolve());
|
||||||
|
this._waiters.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
private _dispatchQueue(): void {
|
||||||
|
this._drainWaiters();
|
||||||
|
|
||||||
|
let next = this._peek();
|
||||||
|
while (next && next.weight <= this._value) {
|
||||||
|
const item = this._queue.shift();
|
||||||
|
if (item) {
|
||||||
|
this._dispatchItem(item);
|
||||||
|
}
|
||||||
|
this._drainWaiters();
|
||||||
|
next = this._peek();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _dispatchItem(item: QueueEntry): void {
|
||||||
|
const previousValue = this._value;
|
||||||
|
this._value -= item.weight;
|
||||||
|
item.resolve([previousValue, this._newReleaser(item.weight)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _peek(): QueueEntry | undefined {
|
||||||
|
const array = this._queue.toArray();
|
||||||
|
return array.length > 0 ? array[0] : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _newReleaser(weight: number): () => void {
|
||||||
|
let called = false;
|
||||||
|
return () => {
|
||||||
|
if (called) return;
|
||||||
|
called = true;
|
||||||
|
this.release(weight);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private _drainWaiters(): void {
|
||||||
|
const waiters = this._waiters.toArray();
|
||||||
|
if (waiters.length === 0) return;
|
||||||
|
|
||||||
|
// If no queue or resources available, resolve all waiters
|
||||||
|
const hasQueue = this._queue.toArray().length > 0;
|
||||||
|
if (!hasQueue || this._value > 0) {
|
||||||
|
waiters.forEach((waiter) => waiter.resolve());
|
||||||
|
this._waiters.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _couldLockImmediately(weight: number): boolean {
|
||||||
|
return this._queue.toArray().length === 0 && weight <= this._value;
|
||||||
|
}
|
||||||
|
}
|
||||||
82
src/lib/SortedArray.ts
Normal file
82
src/lib/SortedArray.ts
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
interface Priority<T> {
|
||||||
|
priority: number;
|
||||||
|
data: T;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SortedArray<T> {
|
||||||
|
private _data: Priority<T>[];
|
||||||
|
|
||||||
|
constructor(data?: Priority<T>[]) {
|
||||||
|
this._data = data ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
private findIndex(priority: number): number {
|
||||||
|
const target = priority + 1;
|
||||||
|
let left = 0;
|
||||||
|
let right = this._data.length - 1;
|
||||||
|
|
||||||
|
while (left <= right) {
|
||||||
|
const mid = Math.floor((left + right) / 2);
|
||||||
|
if (this._data[mid].priority < target) {
|
||||||
|
left = mid + 1;
|
||||||
|
} else if (this._data[mid].priority > target) {
|
||||||
|
right = mid - 1;
|
||||||
|
} else {
|
||||||
|
right = mid - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return left - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public push(value: Priority<T>): void {
|
||||||
|
if (this._data.length === 0) {
|
||||||
|
this._data.push(value);
|
||||||
|
return;
|
||||||
|
} else if (this._data.length === 1) {
|
||||||
|
if (this._data[0].priority <= value.priority) this._data.push(value);
|
||||||
|
else this._data = [value, ...this._data];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const index = this.findIndex(value.priority);
|
||||||
|
|
||||||
|
if (index === this._data.length - 1) {
|
||||||
|
if (this._data[index].priority <= value.priority) {
|
||||||
|
this._data.push(value);
|
||||||
|
} else {
|
||||||
|
this._data = [
|
||||||
|
...this._data.slice(0, index),
|
||||||
|
value,
|
||||||
|
...this._data.slice(index),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const endIndex = index + 1;
|
||||||
|
this._data = [
|
||||||
|
...this._data.slice(0, endIndex),
|
||||||
|
value,
|
||||||
|
...this._data.slice(endIndex),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public shift(): T | undefined {
|
||||||
|
const value = this._data.shift();
|
||||||
|
return value?.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public pop(): T | undefined {
|
||||||
|
const value = this._data.pop();
|
||||||
|
return value?.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public toArray(): T[] {
|
||||||
|
return this._data.map(({ data }) => data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public clear() {
|
||||||
|
this._data = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,7 @@
|
|||||||
import { testTimeBasedRotation } from "./testCcLog";
|
import { testTimeBasedRotation } from "./testCCLog";
|
||||||
|
import { testSortedArray } from "./testSortedArray";
|
||||||
|
import { testSemaphore } from "./testSemaphore";
|
||||||
|
|
||||||
testTimeBasedRotation();
|
testTimeBasedRotation();
|
||||||
|
testSortedArray();
|
||||||
|
testSemaphore();
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { CCLog, MINUTE, HOUR, SECOND } from "@/lib/ccLog";
|
import { CCLog, MINUTE, HOUR } from "@/lib/ccLog";
|
||||||
|
|
||||||
// Test the new time-based rotation functionality
|
// Test the new time-based rotation functionality
|
||||||
function testTimeBasedRotation() {
|
function testTimeBasedRotation() {
|
||||||
@@ -9,25 +9,16 @@ function testTimeBasedRotation() {
|
|||||||
logger1.info("This is a test message with default interval (1 day)");
|
logger1.info("This is a test message with default interval (1 day)");
|
||||||
|
|
||||||
// Test with custom interval (1 hour)
|
// Test with custom interval (1 hour)
|
||||||
const logger2 = new CCLog("test_log_hourly.txt", HOUR);
|
const logger2 = new CCLog("test_log_hourly.txt", { logInterval: HOUR });
|
||||||
logger2.info("This is a test message with 1-hour interval");
|
logger2.info("This is a test message with 1-hour interval");
|
||||||
|
|
||||||
// Test with custom interval (30 minutes)
|
// Test with custom interval (30 minutes)
|
||||||
const logger3 = new CCLog("test_log_30min.txt", 30 * MINUTE);
|
const logger3 = new CCLog("test_log_30min.txt", { logInterval: 30 * MINUTE });
|
||||||
logger3.info("This is a test message with 30-minute interval");
|
logger3.info("This is a test message with 30-minute interval");
|
||||||
|
|
||||||
// Test with custom interval (5 seconds)
|
|
||||||
const logger4 = new CCLog("test_log_5sec.txt", 5 * SECOND);
|
|
||||||
logger4.info("This is a test message with 5-second interval");
|
|
||||||
for (let i = 0; i < 10; i++) {
|
|
||||||
logger4.info(`This is a test message with 5-second interval ${i}`);
|
|
||||||
sleep(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
logger1.close();
|
logger1.close();
|
||||||
logger2.close();
|
logger2.close();
|
||||||
logger3.close();
|
logger3.close();
|
||||||
logger4.close();
|
|
||||||
|
|
||||||
print("Test completed successfully!");
|
print("Test completed successfully!");
|
||||||
}
|
}
|
||||||
|
|||||||
199
src/test/testSemaphore.ts
Normal file
199
src/test/testSemaphore.ts
Normal file
@@ -0,0 +1,199 @@
|
|||||||
|
import { Semaphore } from "@/lib/Semaphore";
|
||||||
|
|
||||||
|
function assert(condition: boolean, message: string) {
|
||||||
|
if (!condition) {
|
||||||
|
error(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testBasicAcquireRelease() {
|
||||||
|
print(" Running test: testBasicAcquireRelease");
|
||||||
|
const s = new Semaphore(1);
|
||||||
|
assert(s.getValue() === 1, "Initial value should be 1");
|
||||||
|
|
||||||
|
const [, release] = await s.acquire();
|
||||||
|
assert(s.getValue() === 0, "Value after acquire should be 0");
|
||||||
|
|
||||||
|
release();
|
||||||
|
assert(s.getValue() === 1, "Value after release should be 1");
|
||||||
|
print(" Test passed: testBasicAcquireRelease");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testRunExclusive() {
|
||||||
|
print(" Running test: testRunExclusive");
|
||||||
|
const s = new Semaphore(1);
|
||||||
|
let inside = false;
|
||||||
|
await s.runExclusive(() => {
|
||||||
|
inside = true;
|
||||||
|
assert(s.isLocked(), "Should be locked inside runExclusive");
|
||||||
|
assert(s.getValue() === 0, "Value should be 0 inside runExclusive");
|
||||||
|
});
|
||||||
|
assert(inside, "Callback should have been executed");
|
||||||
|
assert(!s.isLocked(), "Should be unlocked after runExclusive");
|
||||||
|
assert(s.getValue() === 1, "Value should be 1 after runExclusive");
|
||||||
|
print(" Test passed: testRunExclusive");
|
||||||
|
}
|
||||||
|
|
||||||
|
function testTryAcquire() {
|
||||||
|
print(" Running test: testTryAcquire");
|
||||||
|
const s = new Semaphore(1);
|
||||||
|
const release1 = s.tryAcquire();
|
||||||
|
assert(release1 !== null, "tryAcquire should succeed");
|
||||||
|
assert(s.getValue() === 0, "Value should be 0 after tryAcquire");
|
||||||
|
|
||||||
|
const release2 = s.tryAcquire();
|
||||||
|
assert(release2 === null, "tryAcquire should fail when locked");
|
||||||
|
|
||||||
|
release1!();
|
||||||
|
assert(s.getValue() === 1, "Value should be 1 after release");
|
||||||
|
|
||||||
|
const release3 = s.tryAcquire();
|
||||||
|
assert(release3 !== null, "tryAcquire should succeed again");
|
||||||
|
release3!();
|
||||||
|
print(" Test passed: testTryAcquire");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testQueueing() {
|
||||||
|
print(" Running test: testQueueing");
|
||||||
|
const s = new Semaphore(1);
|
||||||
|
const events: string[] = [];
|
||||||
|
|
||||||
|
// Take the lock
|
||||||
|
const [, release1] = await s.acquire();
|
||||||
|
events.push("acquired1");
|
||||||
|
|
||||||
|
// These two will be queued
|
||||||
|
await s.acquire().then(([, release]) => {
|
||||||
|
events.push("acquired2");
|
||||||
|
sleep(0.1);
|
||||||
|
release();
|
||||||
|
events.push("released2");
|
||||||
|
});
|
||||||
|
|
||||||
|
await s.acquire().then(([, release]) => {
|
||||||
|
events.push("acquired3");
|
||||||
|
release();
|
||||||
|
events.push("released3");
|
||||||
|
});
|
||||||
|
|
||||||
|
// Give some time for promises to queue
|
||||||
|
sleep(0.1);
|
||||||
|
assert(events.length === 1, "Only first acquire should have completed");
|
||||||
|
|
||||||
|
// Release the first lock, allowing the queue to proceed
|
||||||
|
release1();
|
||||||
|
events.push("released1");
|
||||||
|
|
||||||
|
// Wait for all promises to finish
|
||||||
|
sleep(0.5);
|
||||||
|
|
||||||
|
const expected = [
|
||||||
|
"acquired1",
|
||||||
|
"released1",
|
||||||
|
"acquired2",
|
||||||
|
"released2",
|
||||||
|
"acquired3",
|
||||||
|
"released3",
|
||||||
|
];
|
||||||
|
assert(
|
||||||
|
textutils.serialiseJSON(events) === textutils.serialiseJSON(expected),
|
||||||
|
`Event order incorrect: got ${textutils.serialiseJSON(events)}`,
|
||||||
|
);
|
||||||
|
print(" Test passed: testQueueing");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testPriority() {
|
||||||
|
print(" Running test: testPriority");
|
||||||
|
const s = new Semaphore(1);
|
||||||
|
const events: string[] = [];
|
||||||
|
|
||||||
|
const [, release1] = await s.acquire();
|
||||||
|
events.push("acquired_main");
|
||||||
|
|
||||||
|
// Queue with low priority
|
||||||
|
await s.acquire(1, 10).then(([, release]) => {
|
||||||
|
events.push("acquired_low_prio");
|
||||||
|
release();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Queue with high priority
|
||||||
|
await s.acquire(1, 1).then(([, release]) => {
|
||||||
|
events.push("acquired_high_prio");
|
||||||
|
release();
|
||||||
|
});
|
||||||
|
|
||||||
|
sleep(0.1);
|
||||||
|
release1();
|
||||||
|
sleep(0.1);
|
||||||
|
|
||||||
|
const expected = ["acquired_main", "acquired_high_prio", "acquired_low_prio"];
|
||||||
|
assert(
|
||||||
|
textutils.serialiseJSON(events) === textutils.serialiseJSON(expected),
|
||||||
|
`Priority order incorrect: got ${textutils.serialiseJSON(events)}`,
|
||||||
|
);
|
||||||
|
print(" Test passed: testPriority");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testWaitForUnlock() {
|
||||||
|
print(" Running test: testWaitForUnlock");
|
||||||
|
const s = new Semaphore(1);
|
||||||
|
let waited = false;
|
||||||
|
|
||||||
|
const [, release] = await s.acquire();
|
||||||
|
assert(s.isLocked(), "Semaphore should be locked");
|
||||||
|
|
||||||
|
await s.waitForUnlock().then(() => {
|
||||||
|
waited = true;
|
||||||
|
assert(!s.isLocked(), "Should be unlocked when wait is over");
|
||||||
|
});
|
||||||
|
|
||||||
|
sleep(0.1);
|
||||||
|
assert(!waited, "waitForUnlock should not resolve yet");
|
||||||
|
|
||||||
|
release();
|
||||||
|
sleep(0.1);
|
||||||
|
assert(waited, "waitForUnlock should have resolved");
|
||||||
|
print(" Test passed: testWaitForUnlock");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testCancel() {
|
||||||
|
print(" Running test: testCancel");
|
||||||
|
const cancelError = new Error("Canceled for test");
|
||||||
|
const s = new Semaphore(1, cancelError);
|
||||||
|
let rejected = false;
|
||||||
|
|
||||||
|
const [, release] = await s.acquire();
|
||||||
|
|
||||||
|
s.acquire().then(
|
||||||
|
() => {
|
||||||
|
assert(false, "acquire should have been rejected");
|
||||||
|
},
|
||||||
|
(err) => {
|
||||||
|
assert(err === cancelError, "acquire rejected with wrong error");
|
||||||
|
rejected = true;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
sleep(0.1);
|
||||||
|
s.cancel();
|
||||||
|
sleep(0.1);
|
||||||
|
|
||||||
|
assert(rejected, "pending acquire should have been rejected");
|
||||||
|
assert(s.getValue() === 0, "cancel should not affect current lock");
|
||||||
|
|
||||||
|
release();
|
||||||
|
assert(s.getValue() === 1, "release should still work");
|
||||||
|
print(" Test passed: testCancel");
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function testSemaphore() {
|
||||||
|
print("Testing Semaphore...");
|
||||||
|
await testBasicAcquireRelease();
|
||||||
|
await testRunExclusive();
|
||||||
|
testTryAcquire();
|
||||||
|
await testQueueing();
|
||||||
|
await testPriority();
|
||||||
|
await testWaitForUnlock();
|
||||||
|
await testCancel();
|
||||||
|
print("Semaphore tests passed!");
|
||||||
|
}
|
||||||
62
src/test/testSortedArray.ts
Normal file
62
src/test/testSortedArray.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import { SortedArray } from "@/lib/SortedArray";
|
||||||
|
|
||||||
|
function assert(condition: boolean, message: string) {
|
||||||
|
if (!condition) {
|
||||||
|
error(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertDeepEquals(actual: object, expect: object, message: string) {
|
||||||
|
const jsonExpect = textutils.serialiseJSON(expect, {
|
||||||
|
allow_repetitions: true,
|
||||||
|
});
|
||||||
|
const jsonActual = textutils.serialiseJSON(actual, {
|
||||||
|
allow_repetitions: true,
|
||||||
|
});
|
||||||
|
if (jsonExpect !== jsonActual) {
|
||||||
|
error(`${message}: expected ${jsonExpect}, got ${jsonActual}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function testSortedArray() {
|
||||||
|
print("Testing SortedArray...");
|
||||||
|
|
||||||
|
// Test constructor
|
||||||
|
const sortedArray = new SortedArray<string>();
|
||||||
|
assert(
|
||||||
|
sortedArray.toArray().length === 0,
|
||||||
|
"Constructor: initial length should be 0",
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test push (FIFO)
|
||||||
|
const fifoArray = new SortedArray<string>([]);
|
||||||
|
fifoArray.push({ priority: 2, data: "b" });
|
||||||
|
fifoArray.push({ priority: 1, data: "a" });
|
||||||
|
fifoArray.push({ priority: 3, data: "c" });
|
||||||
|
fifoArray.push({ priority: 2, data: "b2" });
|
||||||
|
assertDeepEquals(fifoArray.toArray(), ["a", "b", "b2", "c"], "Push (FIFO)");
|
||||||
|
|
||||||
|
// Test shift
|
||||||
|
const shiftedValue = fifoArray.shift();
|
||||||
|
assert(shiftedValue === "a", "Shift: should return the first element");
|
||||||
|
assertDeepEquals(
|
||||||
|
fifoArray.toArray(),
|
||||||
|
["b", "b2", "c"],
|
||||||
|
"Shift: array should be modified",
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test pop
|
||||||
|
const poppedValue = fifoArray.pop();
|
||||||
|
assert(poppedValue === "c", "Pop: should return the last element");
|
||||||
|
assertDeepEquals(
|
||||||
|
fifoArray.toArray(),
|
||||||
|
["b", "b2"],
|
||||||
|
"Pop: array should be modified",
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test clear
|
||||||
|
fifoArray.clear();
|
||||||
|
assert(fifoArray.toArray().length === 0, "Clear: array should be empty");
|
||||||
|
|
||||||
|
print("SortedArray tests passed!");
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user