import test from 'node:test'; import assert from 'node:assert/strict'; import { Cache, parseTrace, simulate } from './cache.mjs'; test('address decomposition and spatial reuse in the same block', () => { const cache = new Cache({ sizeBytes: 128, lineBytes: 16, ways: 1 }); assert.deepEqual(cache.access(0x2f), { address: 47, setIndex: 2, tag: 0, offset: 15, hit: false, evictedTag: null }); assert.equal(cache.access(0x20).hit, true); assert.equal(cache.access(0x30).hit, false); assert.deepEqual(cache.stats(), { accesses: 3, hits: 1, misses: 2, evictions: 0, hitRate: 1 / 3 }); }); test('direct-mapped conflict and two-way associativity', () => { const trace = Array.from({ length: 20 }, (_, i) => i % 2 ? 128 : 0); assert.equal(simulate(trace, { ways: 1 }).hits, 0); assert.equal(simulate(trace, { ways: 2 }).hits, 18); }); test('LRU updates on hits; FIFO does not', () => { const trace = [0, 64, 0, 128, 0]; assert.equal(simulate(trace, { ways: 2, policy: 'LRU' }).hits, 2); assert.equal(simulate(trace, { ways: 2, policy: 'FIFO' }).hits, 1); assert.equal(simulate(trace, { ways: 2, policy: 'FIFO' }).evictions, 2); }); test('miss fills invalid line before evicting valid lines', () => { const cache = new Cache({ sizeBytes: 128, lineBytes: 16, ways: 2 }); assert.equal(cache.access(0).evictedTag, null); assert.equal(cache.access(64).evictedTag, null); assert.equal(cache.access(128).evictedTag, 0); assert.equal(cache.stats().evictions, 1); }); test('empty trace and deterministic second simulation', () => { assert.equal(simulate([]).hitRate, 0); const trace = [0xffffffff, 0xfffffff0, 0x0, 0xffffffff]; assert.deepEqual(simulate(trace), simulate(trace)); }); test('reject invalid cache geometries, policies, and address inputs', () => { for (const options of [{ sizeBytes: 0 }, { lineBytes: 24 }, { ways: 3 }, { sizeBytes: 64, lineBytes: 16, ways: 8 }, { sizeBytes: 2 ** 32 }, { policy: 'RANDOM' }]) { assert.throws(() => new Cache(options), RangeError); } for (const address of [-1, 1.5, NaN, Infinity, 2 ** 32, '32']) { assert.throws(() => new Cache().access(address), RangeError); } }); test('parse documented hexadecimal and decimal traces without silently guessing', () => { assert.deepEqual(parseTrace('0x00 # first\n16\r\n 0Xff \n#comment\n'), [0, 16, 255]); for (const input of ['-3', '12 garbage', '0b101', '0x', '0x100000000']) { assert.throws(() => parseTrace(input)); } }); test('fixed-seed randomized accesses agree with independent array-based reference', () => { // Reference implementation intentionally uses different storage/replacement logic. function reference(trace, { sizeBytes, lineBytes, ways, policy }) { const sets = new Map(); let hits = 0, evictions = 0; const setCount = sizeBytes / lineBytes / ways; for (const address of trace) { const block = Math.floor(address / lineBytes); const index = block % setCount; const tag = Math.floor(block / setCount); const entries = sets.get(index) || []; const position = entries.indexOf(tag); if (position !== -1) { hits++; if (policy === 'LRU') entries.push(...entries.splice(position, 1)); } else { if (entries.length === ways) { entries.shift(); evictions++; } entries.push(tag); } sets.set(index, entries); } return { hits, misses: trace.length - hits, evictions }; } let seed = 0x713e; const next = () => { seed ^= seed << 13; seed ^= seed >>> 17; seed ^= seed << 5; return seed >>> 0; }; const trace = Array.from({ length: 800 }, () => (next() % 48) * 16 + (next() % 16)); for (const ways of [1, 2, 4, 8]) for (const policy of ['LRU', 'FIFO']) { const config = { sizeBytes: 128, lineBytes: 16, ways, policy }; const actual = simulate(trace, config); const expected = reference(trace, config); assert.deepEqual({ hits: actual.hits, misses: actual.misses, evictions: actual.evictions }, expected); } });