Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion .github/workflows/ci-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,20 @@ on:
jobs:
build:
runs-on: ubuntu-latest
if: "! contains(toJSON(github.event.pull_request.labels.*.name), 'ci-skip')"
if: ${{ !contains(toJSON(github.event.pull_request.labels.*.name), 'ci-skip') }}
timeout-minutes: 10

services:
redis:
image: redis:6
ports:
- 6379:6379
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5

steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
Expand All @@ -34,3 +45,5 @@ jobs:
env:
NODE_ENV: test
CI: true
REDIS_HOST: localhost
REDIS_PORT: 6379
167 changes: 167 additions & 0 deletions src/FastCache.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -274,4 +274,171 @@ describe('FastCache', () => {
});
});
});

// Boundary value tests and overflow tests
describe('boundary and overflow tests', () => {
test('should handle empty string keys', async () => {
await cache.set('', 'empty-key-value');
const value = await cache.get('');
expect(value).toBe('empty-key-value');
});

test('should handle very long keys', async () => {
const longKey = 'a'.repeat(10000); // 10K character key
await cache.set(longKey, 'long-key-value');
const value = await cache.get(longKey);
expect(value).toBe('long-key-value');
});

test('should handle very long values', async () => {
const longValue = 'a'.repeat(1000000); // 1M character value
await cache.set('longValue', longValue);
const value = await cache.get('longValue');
expect(value).toBe(longValue);
});

test('should handle setting null and undefined values', async () => {
await cache.set('nullValue', null as any);
const nullValue = await cache.get('nullValue');
expect(nullValue).toBe('');

await cache.set('undefinedValue', undefined as any);
const undefinedValue = await cache.get('undefinedValue');
expect(undefinedValue).toBe('');
});

test('should handle setting and retrieving special characters', async () => {
const specialChars = '!@#$%^&*()_+{}[]|\\:;"\'<>,.?/~`';
await cache.set('specialChars', specialChars);
const value = await cache.get('specialChars');
expect(value).toBe(specialChars);
});

test('should handle setting and retrieving emoji', async () => {
const emoji = '😀🙌👍🎉🔥🚀';
await cache.set('emoji', emoji);
const value = await cache.get('emoji');
expect(value).toBe(emoji);
});

test('should handle JSON serialization errors', async () => {
// Create object with circular reference
const circularObj: any = { key: 'value' };
circularObj.self = circularObj;

// Verify that withCache handles serialization errors gracefully
const result = await cache.withCache('circularObj', async () => {
return 'fallback value';
});

expect(result).toBe('fallback value');
});

test('should handle invalid JSON when deserializing', async () => {
// Directly set invalid JSON
await client.set('invalidJson', '{invalid"json:data}');

// Try to get via cache
const result = await cache.get('invalidJson');

// We expect a string return since it couldn't be parsed
expect(result).toBe('{invalid"json:data}');
});

test('should handle concurrent operations on the same key', async () => {
// Create multiple promises that try to set the same key
const promises: Promise<void>[] = [];
for (let i = 0; i < 10; i++) {
promises.push(cache.set('concurrent', `value-${i}`));
}

// Wait for all promises to resolve
await Promise.all(promises);

// Get the final value
const finalValue = await cache.get('concurrent');
expect(finalValue).toBeDefined();
});

test('should handle extremely large list operations', async () => {
const list = cache.list('largeList');
const large = 10000;

// Add many items
for (let i = 0; i < large; i++) {
await list.push(`item-${i}`);
}

// Check length
const length = await list.length();
expect(length).toBe(large);

// Check some values
const items = await list.getAll(large - 5, large - 1);
expect(items.length).toBe(5);
expect(items[0]).toBe(`item-${large - 5}`);
});

test('should handle extremely large map operations', async () => {
const map = cache.map('largeMap');
const large = 1000;

// Add many key-value pairs
for (let i = 0; i < large; i++) {
await map.set(`key-${i}`, `value-${i}`);
}

// Check length
const length = await map.length();
expect(length).toBe(large);

// Check some values
const fields = Array.from({ length: 5 }, (_, i) => `key-${i}`);
const values = await map.getAll(fields);
expect(values.length).toBe(5);
expect(values[0]).toBe('value-0');
});

test('should handle extremely large set operations', async () => {
const set = cache.setOf('largeSet');
const large = 1000;

// Add many items in batches
const batchSize = 100;
for (let i = 0; i < large; i += batchSize) {
const batch = Array.from({ length: batchSize }, (_, j) => `item-${i + j}`);
await set.add(...batch);
}

// Check length
const length = await set.length();
expect(length).toBe(large);

// Check some values
const containsFirst = await set.contains('item-0');
expect(containsFirst).toBeTruthy();

const containsLast = await set.contains(`item-${large - 1}`);
expect(containsLast).toBeTruthy();
});

test('should handle flush with extremely large number of keys', async () => {
// Create many keys with the same prefix
const keyCount = 1000;
const prefix = 'massive-flush-test:';

for (let i = 0; i < keyCount; i++) {
await cache.set(`${prefix}${i}`, `value-${i}`);
}

// Flush all keys with pattern
await cache.flush(`${prefix}*`);

// Verify keys are gone
for (let i = 0; i < 10; i++) {
const value = await cache.get(`${prefix}${i}`);
expect(value).toBeNull();
}
});
});
});
123 changes: 123 additions & 0 deletions src/InMemoryCache.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,4 +169,127 @@ describe('LocalCache', () => {
expect(data).toBeUndefined();
});
});

// Boundary value tests and overflow tests
describe('boundary and overflow tests', () => {
it('should handle zero TTL value', async () => {
const zeroTtlCache = new InMemoryCache({ ttlInSec: 0 });
zeroTtlCache.setCache('foo', { foo: 123 });

// Data should be immediately invalidated with zero TTL
await setTimeout(10);

const data = zeroTtlCache.getCache('foo');
expect(data).toBeUndefined();
});

it('should handle negative TTL value by treating it as zero', async () => {
const negativeTtlCache = new InMemoryCache({ ttlInSec: -1 });
negativeTtlCache.setCache('foo', { foo: 123 });

// Data should be immediately invalidated with negative TTL
await setTimeout(10);

const data = negativeTtlCache.getCache('foo');
expect(data).toBeUndefined();
});

it('should handle extremely large TTL value', async () => {
// 실제 MAX_SAFE_INTEGER는 너무 커서 테스트하기 어려움
// 대신 10초 정도로 충분히 긴 TTL을 사용
const largeTtlCache = new InMemoryCache({ ttlInSec: 10 });
largeTtlCache.setCache('foo', { foo: 123 });

// 여기서는 10초보다 훨씬 짧은 시간 후에 확인
await setTimeout(10);

const data = largeTtlCache.getCache('foo');
expect(data).toEqual({ foo: 123 });
});

it('should correctly handle hit counter overflow', async () => {
const hitOverflowCache = new InMemoryCache({ ttlInSec: 10 });
hitOverflowCache.setCache('foo', { foo: 123 });

// 로직 검증: InMemoryCache.ts는 Number.MAX_VALUE와 비교
hitOverflowCache.totalHit = Number.MAX_VALUE - 1;

// Get cache to increment hit counter
hitOverflowCache.getCache('foo');

// 비동기 호출이 완료되도록 약간의 지연
await setTimeout(10);

// 실제 구현에서는 totalHit이 0으로 리셋되고 hitCarry가 1 증가
expect(hitOverflowCache.totalHit).toBe(0);
expect(hitOverflowCache.hitCarry).toBe(1);
});

it('should handle very long keys', async () => {
const longKey = 'a'.repeat(1000000); // 1 million chars
localCache.setCache(longKey, { value: 'test' });

await setTimeout(10);

const data = localCache.getCache(longKey);
expect(data).toEqual({ value: 'test' });
});

it('should handle storing very large objects', async () => {
// Create large object with deep nesting
const generateLargeObject = (depth: number, breadth: number): any => {
if (depth <= 0) {
return 'leaf';
}

const obj: Record<string, any> = {};
for (let i = 0; i < breadth; i++) {
obj[`key${i}`] = generateLargeObject(depth - 1, breadth);
}
return obj;
};

const largeObject = generateLargeObject(10, 5);
localCache.setCache('largeObj', largeObject);

await setTimeout(10);

const data = localCache.getCache('largeObj');
expect(data).toEqual(largeObject);
});

it('should handle circular references gracefully', async () => {
const circularObj: any = { value: 1 };
circularObj.self = circularObj; // Create circular reference

expect(() => {
localCache.setCache('circularObj', circularObj);
}).not.toThrow();
});

it('should handle multiple rapid cache operations', async () => {
// Perform lots of operations in quick succession
for (let i = 0; i < 1000; i++) {
localCache.setCache(`key${i}`, { value: i });
}

// Validate some random values
for (let i = 100; i < 110; i++) {
expect(localCache.getCache(`key${i}`)).toEqual({ value: i });
}

// Validate cache size
expect(InMemoryCache.snip(localCache).itemCount).toBe(1000);
});

it('should handle function that throws exception', async () => {
const throwingFn = () => {
throw new Error('Expected function error');
};

expect(() => {
localCache.setCache('throwingFn', throwingFn);
}).toThrow('Expected function error');
});
});
});
Loading