mirror of
https://github.com/renovatebot/renovate.git
synced 2026-10-27 15:58:48 +00:00
05e1271376
Enables the vitest/prefer-expect-resolves oxlint rule and applies its autofix across the spec suite.
573 lines
14 KiB
TypeScript
573 lines
14 KiB
TypeScript
import { dir as tmpDir } from 'tmp-promise';
|
|
import type { MockInstance } from 'vitest';
|
|
import { GlobalConfig } from '../../../config/global.ts';
|
|
import * as memCache from '../memory/index.ts';
|
|
import * as packageCache from './index.ts';
|
|
import { withCache } from './with-cache.ts';
|
|
|
|
describe('util/cache/package/with-cache', () => {
|
|
let setCache: MockInstance<typeof packageCache.setWithRawTtl>;
|
|
let dirResult: Awaited<ReturnType<typeof tmpDir>>;
|
|
const getValue = vi.fn();
|
|
let count = 1;
|
|
|
|
beforeEach(async () => {
|
|
vi.useRealTimers();
|
|
GlobalConfig.reset();
|
|
memCache.init();
|
|
dirResult = await tmpDir({ unsafeCleanup: true });
|
|
setCache = vi.spyOn(packageCache, 'setWithRawTtl');
|
|
await packageCache.init({ cacheDir: dirResult.path });
|
|
count = 1;
|
|
getValue.mockImplementation(() => {
|
|
const res = String(100 * count + 10 * count + count);
|
|
count += 1;
|
|
return Promise.resolve(res);
|
|
});
|
|
});
|
|
|
|
afterEach(async () => {
|
|
setCache.mockRestore();
|
|
await packageCache.cleanup({});
|
|
await dirResult.cleanup();
|
|
});
|
|
|
|
it('caches string result', async () => {
|
|
function fn() {
|
|
return getValue();
|
|
}
|
|
|
|
await expect(
|
|
withCache({ namespace: '_test-namespace', key: 'some-key' }, fn),
|
|
).resolves.toBe('111');
|
|
await expect(
|
|
withCache({ namespace: '_test-namespace', key: 'some-key' }, fn),
|
|
).resolves.toBe('111');
|
|
await expect(
|
|
withCache({ namespace: '_test-namespace', key: 'some-key' }, fn),
|
|
).resolves.toBe('111');
|
|
|
|
expect(getValue).toHaveBeenCalledTimes(1);
|
|
expect(setCache).toHaveBeenCalledExactlyOnceWith(
|
|
'_test-namespace',
|
|
'cache-decorator:some-key',
|
|
{ cachedAt: expect.any(String), value: '111' },
|
|
30,
|
|
);
|
|
});
|
|
|
|
it('disables cache if cacheable is false', async () => {
|
|
function fn() {
|
|
return getValue();
|
|
}
|
|
|
|
await expect(
|
|
withCache(
|
|
{ namespace: '_test-namespace', key: 'key', cacheable: false },
|
|
fn,
|
|
),
|
|
).resolves.toBe('111');
|
|
await expect(
|
|
withCache(
|
|
{ namespace: '_test-namespace', key: 'key', cacheable: false },
|
|
fn,
|
|
),
|
|
).resolves.toBe('222');
|
|
await expect(
|
|
withCache(
|
|
{ namespace: '_test-namespace', key: 'key', cacheable: false },
|
|
fn,
|
|
),
|
|
).resolves.toBe('333');
|
|
|
|
expect(getValue).toHaveBeenCalledTimes(3);
|
|
expect(setCache).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('forces cache if cachePrivatePackages=true', async () => {
|
|
GlobalConfig.set({ cachePrivatePackages: true });
|
|
function fn() {
|
|
return getValue();
|
|
}
|
|
|
|
await expect(
|
|
withCache(
|
|
{ namespace: '_test-namespace', key: 'key', cacheable: false },
|
|
fn,
|
|
),
|
|
).resolves.toBe('111');
|
|
await expect(
|
|
withCache(
|
|
{ namespace: '_test-namespace', key: 'key', cacheable: false },
|
|
fn,
|
|
),
|
|
).resolves.toBe('111');
|
|
await expect(
|
|
withCache(
|
|
{ namespace: '_test-namespace', key: 'key', cacheable: false },
|
|
fn,
|
|
),
|
|
).resolves.toBe('111');
|
|
|
|
expect(getValue).toHaveBeenCalledTimes(1);
|
|
expect(setCache).toHaveBeenCalledExactlyOnceWith(
|
|
'_test-namespace',
|
|
'cache-decorator:key',
|
|
{ cachedAt: expect.any(String), value: '111' },
|
|
30,
|
|
);
|
|
});
|
|
|
|
it('caches null values', async () => {
|
|
async function fn(): Promise<string | null> {
|
|
await getValue();
|
|
return null;
|
|
}
|
|
|
|
await expect(
|
|
withCache({ namespace: '_test-namespace', key: 'key' }, fn),
|
|
).resolves.toBeNull();
|
|
await expect(
|
|
withCache({ namespace: '_test-namespace', key: 'key' }, fn),
|
|
).resolves.toBeNull();
|
|
await expect(
|
|
withCache({ namespace: '_test-namespace', key: 'key' }, fn),
|
|
).resolves.toBeNull();
|
|
|
|
expect(getValue).toHaveBeenCalledTimes(1);
|
|
expect(setCache).toHaveBeenCalledExactlyOnceWith(
|
|
'_test-namespace',
|
|
'cache-decorator:key',
|
|
{ cachedAt: expect.any(String), value: null },
|
|
30,
|
|
);
|
|
});
|
|
|
|
it('does not cache values rejected by cacheResult predicate', async () => {
|
|
async function fn(): Promise<string | null> {
|
|
await getValue();
|
|
return null;
|
|
}
|
|
function shouldCacheResult(value: unknown): boolean {
|
|
return value !== null;
|
|
}
|
|
|
|
await expect(
|
|
withCache(
|
|
{ namespace: '_test-namespace', key: 'key', shouldCacheResult },
|
|
fn,
|
|
),
|
|
).resolves.toBeNull();
|
|
await expect(
|
|
withCache(
|
|
{ namespace: '_test-namespace', key: 'key', shouldCacheResult },
|
|
fn,
|
|
),
|
|
).resolves.toBeNull();
|
|
await expect(
|
|
withCache(
|
|
{ namespace: '_test-namespace', key: 'key', shouldCacheResult },
|
|
fn,
|
|
),
|
|
).resolves.toBeNull();
|
|
|
|
expect(getValue).toHaveBeenCalledTimes(3);
|
|
expect(setCache).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('ignores cached values rejected by cacheResult predicate', async () => {
|
|
async function nullFn(): Promise<string | null> {
|
|
await getValue();
|
|
return null;
|
|
}
|
|
function fn() {
|
|
return getValue();
|
|
}
|
|
function cacheResult(value: unknown): boolean {
|
|
return value !== null;
|
|
}
|
|
|
|
await expect(
|
|
withCache({ namespace: '_test-namespace', key: 'key' }, nullFn),
|
|
).resolves.toBeNull();
|
|
await expect(
|
|
withCache(
|
|
{
|
|
namespace: '_test-namespace',
|
|
key: 'key',
|
|
shouldCacheResult: cacheResult,
|
|
},
|
|
fn,
|
|
),
|
|
).resolves.toBe('222');
|
|
await expect(
|
|
withCache(
|
|
{
|
|
namespace: '_test-namespace',
|
|
key: 'key',
|
|
shouldCacheResult: cacheResult,
|
|
},
|
|
fn,
|
|
),
|
|
).resolves.toBe('222');
|
|
|
|
expect(getValue).toHaveBeenCalledTimes(2);
|
|
expect(setCache).toHaveBeenCalledTimes(2);
|
|
expect(setCache).toHaveBeenLastCalledWith(
|
|
'_test-namespace',
|
|
'cache-decorator:key',
|
|
{ cachedAt: expect.any(String), value: '222' },
|
|
30,
|
|
);
|
|
});
|
|
|
|
it('does not cache undefined', async () => {
|
|
async function fn(): Promise<string | undefined> {
|
|
await getValue();
|
|
return undefined;
|
|
}
|
|
|
|
await expect(
|
|
withCache({ namespace: '_test-namespace', key: 'key' }, fn),
|
|
).resolves.toBeUndefined();
|
|
await expect(
|
|
withCache({ namespace: '_test-namespace', key: 'key' }, fn),
|
|
).resolves.toBeUndefined();
|
|
await expect(
|
|
withCache({ namespace: '_test-namespace', key: 'key' }, fn),
|
|
).resolves.toBeUndefined();
|
|
|
|
expect(getValue).toHaveBeenCalledTimes(3);
|
|
expect(setCache).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('uses custom ttlMinutes', async () => {
|
|
function fn() {
|
|
return getValue();
|
|
}
|
|
|
|
await expect(
|
|
withCache(
|
|
{ namespace: '_test-namespace', key: 'key', ttlMinutes: 60 },
|
|
fn,
|
|
),
|
|
).resolves.toBe('111');
|
|
|
|
expect(setCache).toHaveBeenCalledExactlyOnceWith(
|
|
'_test-namespace',
|
|
'cache-decorator:key',
|
|
{ cachedAt: expect.any(String), value: '111' },
|
|
60,
|
|
);
|
|
});
|
|
|
|
describe('fallback with hard TTL', () => {
|
|
beforeEach(() => {
|
|
vi.useFakeTimers();
|
|
GlobalConfig.set({ cacheHardTtlMinutes: 2 });
|
|
});
|
|
|
|
it('updates cached result after soft TTL expires', async () => {
|
|
function fn() {
|
|
return getValue();
|
|
}
|
|
|
|
await expect(
|
|
withCache(
|
|
{
|
|
namespace: '_test-namespace',
|
|
key: 'key',
|
|
ttlMinutes: 1,
|
|
fallback: true,
|
|
},
|
|
fn,
|
|
),
|
|
).resolves.toBe('111');
|
|
expect(getValue).toHaveBeenCalledTimes(1);
|
|
|
|
vi.advanceTimersByTime(60 * 1000 - 1);
|
|
await expect(
|
|
withCache(
|
|
{
|
|
namespace: '_test-namespace',
|
|
key: 'key',
|
|
ttlMinutes: 1,
|
|
fallback: true,
|
|
},
|
|
fn,
|
|
),
|
|
).resolves.toBe('111');
|
|
expect(getValue).toHaveBeenCalledTimes(1);
|
|
expect(setCache).toHaveBeenLastCalledWith(
|
|
'_test-namespace',
|
|
'cache-decorator:key',
|
|
{ cachedAt: expect.any(String), value: '111' },
|
|
2,
|
|
);
|
|
|
|
vi.advanceTimersByTime(1);
|
|
await expect(
|
|
withCache(
|
|
{
|
|
namespace: '_test-namespace',
|
|
key: 'key',
|
|
ttlMinutes: 1,
|
|
fallback: true,
|
|
},
|
|
fn,
|
|
),
|
|
).resolves.toBe('222');
|
|
expect(getValue).toHaveBeenCalledTimes(2);
|
|
expect(setCache).toHaveBeenLastCalledWith(
|
|
'_test-namespace',
|
|
'cache-decorator:key',
|
|
{ cachedAt: expect.any(String), value: '222' },
|
|
2,
|
|
);
|
|
});
|
|
|
|
it('overrides soft ttl and updates result', async () => {
|
|
GlobalConfig.set({
|
|
cacheTtlOverride: { '_test-namespace': 2 },
|
|
cacheHardTtlMinutes: 3,
|
|
});
|
|
function fn() {
|
|
return getValue();
|
|
}
|
|
|
|
await expect(
|
|
withCache(
|
|
{
|
|
namespace: '_test-namespace',
|
|
key: 'key',
|
|
ttlMinutes: 1,
|
|
fallback: true,
|
|
},
|
|
fn,
|
|
),
|
|
).resolves.toBe('111');
|
|
expect(getValue).toHaveBeenCalledTimes(1);
|
|
expect(setCache).toHaveBeenLastCalledWith(
|
|
'_test-namespace',
|
|
'cache-decorator:key',
|
|
{ cachedAt: expect.any(String), value: '111' },
|
|
3,
|
|
);
|
|
|
|
vi.advanceTimersByTime(120 * 1000 - 1);
|
|
await expect(
|
|
withCache(
|
|
{
|
|
namespace: '_test-namespace',
|
|
key: 'key',
|
|
ttlMinutes: 1,
|
|
fallback: true,
|
|
},
|
|
fn,
|
|
),
|
|
).resolves.toBe('111');
|
|
expect(getValue).toHaveBeenCalledTimes(1);
|
|
expect(setCache).toHaveBeenCalledTimes(1);
|
|
|
|
vi.advanceTimersByTime(1);
|
|
await expect(
|
|
withCache(
|
|
{
|
|
namespace: '_test-namespace',
|
|
key: 'key',
|
|
ttlMinutes: 1,
|
|
fallback: true,
|
|
},
|
|
fn,
|
|
),
|
|
).resolves.toBe('222');
|
|
expect(getValue).toHaveBeenCalledTimes(2);
|
|
expect(setCache).toHaveBeenLastCalledWith(
|
|
'_test-namespace',
|
|
'cache-decorator:key',
|
|
{ cachedAt: expect.any(String), value: '222' },
|
|
3,
|
|
);
|
|
});
|
|
|
|
it('returns stale result on error', async () => {
|
|
function fn() {
|
|
return getValue();
|
|
}
|
|
|
|
await expect(
|
|
withCache(
|
|
{
|
|
namespace: '_test-namespace',
|
|
key: 'key',
|
|
ttlMinutes: 1,
|
|
fallback: true,
|
|
},
|
|
fn,
|
|
),
|
|
).resolves.toBe('111');
|
|
expect(getValue).toHaveBeenCalledTimes(1);
|
|
expect(setCache).toHaveBeenLastCalledWith(
|
|
'_test-namespace',
|
|
'cache-decorator:key',
|
|
{ cachedAt: expect.any(String), value: '111' },
|
|
2,
|
|
);
|
|
|
|
vi.advanceTimersByTime(60 * 1000);
|
|
getValue.mockRejectedValueOnce(new Error('test'));
|
|
await expect(
|
|
withCache(
|
|
{
|
|
namespace: '_test-namespace',
|
|
key: 'key',
|
|
ttlMinutes: 1,
|
|
fallback: true,
|
|
},
|
|
fn,
|
|
),
|
|
).resolves.toBe('111');
|
|
expect(getValue).toHaveBeenCalledTimes(2);
|
|
expect(setCache).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('does not return stale values rejected by cacheResult predicate', async () => {
|
|
async function nullFn(): Promise<string | null> {
|
|
await getValue();
|
|
return null;
|
|
}
|
|
function fn() {
|
|
return getValue();
|
|
}
|
|
function shouldCacheResult(value: unknown): boolean {
|
|
return value !== null;
|
|
}
|
|
|
|
await expect(
|
|
withCache(
|
|
{
|
|
namespace: '_test-namespace',
|
|
key: 'key',
|
|
ttlMinutes: 1,
|
|
fallback: true,
|
|
},
|
|
nullFn,
|
|
),
|
|
).resolves.toBeNull();
|
|
expect(getValue).toHaveBeenCalledTimes(1);
|
|
expect(setCache).toHaveBeenCalledTimes(1);
|
|
|
|
vi.advanceTimersByTime(60 * 1000);
|
|
getValue.mockRejectedValueOnce(new Error('test'));
|
|
await expect(
|
|
withCache(
|
|
{
|
|
namespace: '_test-namespace',
|
|
key: 'key',
|
|
ttlMinutes: 1,
|
|
fallback: true,
|
|
shouldCacheResult,
|
|
},
|
|
fn,
|
|
),
|
|
).rejects.toThrow('test');
|
|
expect(getValue).toHaveBeenCalledTimes(2);
|
|
expect(setCache).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('drops stale value after hard TTL expires', async () => {
|
|
function fn() {
|
|
return getValue();
|
|
}
|
|
|
|
await expect(
|
|
withCache(
|
|
{
|
|
namespace: '_test-namespace',
|
|
key: 'key',
|
|
ttlMinutes: 1,
|
|
fallback: true,
|
|
},
|
|
fn,
|
|
),
|
|
).resolves.toBe('111');
|
|
expect(getValue).toHaveBeenCalledTimes(1);
|
|
expect(setCache).toHaveBeenLastCalledWith(
|
|
'_test-namespace',
|
|
'cache-decorator:key',
|
|
{ cachedAt: expect.any(String), value: '111' },
|
|
2,
|
|
);
|
|
|
|
vi.advanceTimersByTime(2 * 60 * 1000 - 1);
|
|
getValue.mockRejectedValueOnce(new Error('test'));
|
|
await expect(
|
|
withCache(
|
|
{
|
|
namespace: '_test-namespace',
|
|
key: 'key',
|
|
ttlMinutes: 1,
|
|
fallback: true,
|
|
},
|
|
fn,
|
|
),
|
|
).resolves.toBe('111');
|
|
|
|
vi.advanceTimersByTime(1);
|
|
getValue.mockRejectedValueOnce(new Error('test'));
|
|
await expect(
|
|
withCache(
|
|
{
|
|
namespace: '_test-namespace',
|
|
key: 'key',
|
|
ttlMinutes: 1,
|
|
fallback: true,
|
|
},
|
|
fn,
|
|
),
|
|
).rejects.toThrow('test');
|
|
});
|
|
|
|
it('does not use fallback when fallback=false', async () => {
|
|
function fn() {
|
|
return getValue();
|
|
}
|
|
|
|
await expect(
|
|
withCache(
|
|
{
|
|
namespace: '_test-namespace',
|
|
key: 'key',
|
|
ttlMinutes: 1,
|
|
fallback: false,
|
|
},
|
|
fn,
|
|
),
|
|
).resolves.toBe('111');
|
|
expect(getValue).toHaveBeenCalledTimes(1);
|
|
// Without fallback, hard TTL equals soft TTL
|
|
expect(setCache).toHaveBeenLastCalledWith(
|
|
'_test-namespace',
|
|
'cache-decorator:key',
|
|
{ cachedAt: expect.any(String), value: '111' },
|
|
1,
|
|
);
|
|
|
|
vi.advanceTimersByTime(60 * 1000);
|
|
getValue.mockRejectedValueOnce(new Error('test'));
|
|
// Error should propagate since fallback is disabled
|
|
await expect(
|
|
withCache(
|
|
{
|
|
namespace: '_test-namespace',
|
|
key: 'key',
|
|
ttlMinutes: 1,
|
|
fallback: false,
|
|
},
|
|
fn,
|
|
),
|
|
).rejects.toThrow('test');
|
|
});
|
|
});
|
|
});
|