mirror of
				https://github.com/SikongJueluo/cc-utils.git
				synced 2025-11-04 19:27:50 +08:00 
			
		
		
		
	feature: add sortedarray semaphore
This commit is contained in:
		
							
								
								
									
										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();
 | 
			
		||||
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
 | 
			
		||||
function testTimeBasedRotation() {
 | 
			
		||||
@@ -9,25 +9,16 @@ function testTimeBasedRotation() {
 | 
			
		||||
  logger1.info("This is a test message with default interval (1 day)");
 | 
			
		||||
 | 
			
		||||
  // 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");
 | 
			
		||||
 | 
			
		||||
  // 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");
 | 
			
		||||
 | 
			
		||||
  // 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();
 | 
			
		||||
  logger2.close();
 | 
			
		||||
  logger3.close();
 | 
			
		||||
  logger4.close();
 | 
			
		||||
 | 
			
		||||
  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