From a84f82bd4415f3d8844d698c674acea6d4ce741a Mon Sep 17 00:00:00 2001 From: kranthi Date: Fri, 14 Aug 2026 18:35:50 +0100 Subject: [PATCH] test(common): add unit test suites for LRUCache and MinHeap --- common/src/util/__tests__/lru-cache.test.ts | 135 ++++++++++++++++++++ common/src/util/__tests__/min-heap.test.ts | 82 ++++++++++++ 2 files changed, 217 insertions(+) create mode 100644 common/src/util/__tests__/lru-cache.test.ts create mode 100644 common/src/util/__tests__/min-heap.test.ts diff --git a/common/src/util/__tests__/lru-cache.test.ts b/common/src/util/__tests__/lru-cache.test.ts new file mode 100644 index 0000000000..3015d1892a --- /dev/null +++ b/common/src/util/__tests__/lru-cache.test.ts @@ -0,0 +1,135 @@ +import { describe, expect, it } from 'bun:test' + +import { LRUCache } from '../lru-cache' + +describe('LRUCache', () => { + describe('constructor', () => { + it('initializes with a valid maxSize', () => { + const cache = new LRUCache(5) + expect(cache.size).toBe(0) + }) + + it('throws error when maxSize is zero', () => { + expect(() => new LRUCache(0)).toThrow( + 'LRUCache maxSize must be a positive number.', + ) + }) + + it('throws error when maxSize is negative', () => { + expect(() => new LRUCache(-5)).toThrow( + 'LRUCache maxSize must be a positive number.', + ) + }) + }) + + describe('basic get and set', () => { + it('returns undefined for non-existent keys', () => { + const cache = new LRUCache(3) + expect(cache.get('unknown')).toBeUndefined() + }) + + it('stores and retrieves values by key', () => { + const cache = new LRUCache(3) + cache.set('a', 'alpha') + cache.set('b', 'beta') + + expect(cache.get('a')).toBe('alpha') + expect(cache.get('b')).toBe('beta') + expect(cache.size).toBe(2) + }) + + it('updates existing keys without increasing size', () => { + const cache = new LRUCache(3) + cache.set('count', 1) + expect(cache.size).toBe(1) + expect(cache.get('count')).toBe(1) + + cache.set('count', 2) + expect(cache.size).toBe(1) + expect(cache.get('count')).toBe(2) + }) + + it('clears all items and resets size to 0', () => { + const cache = new LRUCache(3) + cache.set('x', 10) + cache.set('y', 20) + expect(cache.size).toBe(2) + + cache.clear() + expect(cache.size).toBe(0) + expect(cache.get('x')).toBeUndefined() + expect(cache.get('y')).toBeUndefined() + }) + }) + + describe('eviction and recency order', () => { + it('evicts the least recently added item when capacity is exceeded', () => { + const cache = new LRUCache(2) + cache.set('a', 1) + cache.set('b', 2) + cache.set('c', 3) // Evicts 'a' + + expect(cache.get('a')).toBeUndefined() + expect(cache.get('b')).toBe(2) + expect(cache.get('c')).toBe(3) + expect(cache.size).toBe(2) + }) + + it('marks an item as recently used on get() so it avoids eviction', () => { + const cache = new LRUCache(2) + cache.set('a', 1) + cache.set('b', 2) + + // Access 'a', making 'b' the least recently used + expect(cache.get('a')).toBe(1) + + // Add 'c' - should evict 'b' instead of 'a' + cache.set('c', 3) + + expect(cache.get('a')).toBe(1) + expect(cache.get('b')).toBeUndefined() + expect(cache.get('c')).toBe(3) + }) + + it('marks an item as recently used on set() update so it avoids eviction', () => { + const cache = new LRUCache(2) + cache.set('a', 1) + cache.set('b', 2) + + // Update 'a', making 'b' the least recently used + cache.set('a', 10) + + // Add 'c' - should evict 'b' instead of 'a' + cache.set('c', 30) + + expect(cache.get('a')).toBe(10) + expect(cache.get('b')).toBeUndefined() + expect(cache.get('c')).toBe(30) + }) + + it('handles multiple sequential evictions with capacity 1', () => { + const cache = new LRUCache(1) + cache.set('first', '1') + expect(cache.get('first')).toBe('1') + + cache.set('second', '2') + expect(cache.get('first')).toBeUndefined() + expect(cache.get('second')).toBe('2') + expect(cache.size).toBe(1) + }) + }) + + describe('support for different key/value types', () => { + it('supports numeric keys and object values', () => { + const cache = new LRUCache(2) + const obj1 = { name: 'Item 1' } + const obj2 = { name: 'Item 2' } + + cache.set(100, obj1) + cache.set(200, obj2) + + expect(cache.get(100)).toEqual({ name: 'Item 1' }) + expect(cache.get(200)).toEqual({ name: 'Item 2' }) + }) + }) +}) diff --git a/common/src/util/__tests__/min-heap.test.ts b/common/src/util/__tests__/min-heap.test.ts new file mode 100644 index 0000000000..e258089d2c --- /dev/null +++ b/common/src/util/__tests__/min-heap.test.ts @@ -0,0 +1,82 @@ +import { describe, expect, it } from 'bun:test' + +import { MinHeap } from '../min-heap' + +describe('MinHeap', () => { + it('initializes with size 0', () => { + const heap = new MinHeap() + expect(heap.size).toBe(0) + }) + + it('returns undefined when extracting from an empty heap', () => { + const heap = new MinHeap() + expect(heap.extractMin()).toBeUndefined() + }) + + it('inserts and extracts a single element', () => { + const heap = new MinHeap() + heap.insert('item1', 10) + + expect(heap.size).toBe(1) + expect(heap.extractMin()).toBe('item1') + expect(heap.size).toBe(0) + expect(heap.extractMin()).toBeUndefined() + }) + + it('extracts elements in ascending order of score', () => { + const heap = new MinHeap() + heap.insert('c', 30) + heap.insert('a', 10) + heap.insert('b', 20) + heap.insert('e', 50) + heap.insert('d', 40) + + expect(heap.size).toBe(5) + expect(heap.extractMin()).toBe('a') + expect(heap.extractMin()).toBe('b') + expect(heap.extractMin()).toBe('c') + expect(heap.extractMin()).toBe('d') + expect(heap.extractMin()).toBe('e') + expect(heap.size).toBe(0) + }) + + it('handles negative and floating-point scores', () => { + const heap = new MinHeap() + heap.insert('zero', 0) + heap.insert('neg', -15.5) + heap.insert('pos', 3.14) + heap.insert('more-neg', -100) + + expect(heap.extractMin()).toBe('more-neg') + expect(heap.extractMin()).toBe('neg') + expect(heap.extractMin()).toBe('zero') + expect(heap.extractMin()).toBe('pos') + }) + + it('handles items with duplicate scores', () => { + const heap = new MinHeap() + heap.insert('task1', 5) + heap.insert('task2', 5) + heap.insert('task3', 1) + + expect(heap.extractMin()).toBe('task3') + const next1 = heap.extractMin() + const next2 = heap.extractMin() + expect([next1, next2].sort()).toEqual(['task1', 'task2']) + expect(heap.size).toBe(0) + }) + + it('supports interleaved insertions and extractions', () => { + const heap = new MinHeap() + heap.insert(100, 100) + heap.insert(50, 50) + expect(heap.extractMin()).toBe(50) + + heap.insert(20, 20) + heap.insert(80, 80) + expect(heap.extractMin()).toBe(20) + expect(heap.extractMin()).toBe(80) + expect(heap.extractMin()).toBe(100) + expect(heap.extractMin()).toBeUndefined() + }) +})