refactor: simplify split-cache handling
Restore cache entries sequentially, keep saves parallel with per-entry error handling, and simplify the cache test setup. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
1 parent
aa2cac01fd
commit
57097ea729
9 files changed
+74
-421
No files matched your search
@@ -2,7 +2,6 @@
|
||||
|
||||
import {jest, describe, it, expect, beforeEach, afterEach} from '@jest/globals';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
jest.unstable_mockModule('@actions/cache', () => ({
|
||||
saveCache: jest.fn(),
|
||||
@@ -88,10 +87,6 @@ describe('restoreCache', () => {
|
||||
];
|
||||
const originalEnv = {...process.env};
|
||||
const originalArch = Object.getOwnPropertyDescriptor(process, 'arch')!;
|
||||
const originalPlatform = Object.getOwnPropertyDescriptor(
|
||||
process,
|
||||
'platform'
|
||||
)!;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
@@ -102,7 +97,6 @@ describe('restoreCache', () => {
|
||||
ImageOS: 'ubuntu24'
|
||||
};
|
||||
Object.defineProperty(process, 'arch', {value: 'x64'});
|
||||
Object.defineProperty(process, 'platform', {value: 'linux'});
|
||||
hashFilesSpy.mockResolvedValue('file_hash');
|
||||
getCacheDirectoryPathSpy.mockResolvedValue(directories);
|
||||
restoreCacheSpy.mockResolvedValue(undefined);
|
||||
@@ -112,7 +106,6 @@ describe('restoreCache', () => {
|
||||
afterEach(() => {
|
||||
process.env = {...originalEnv};
|
||||
Object.defineProperty(process, 'arch', originalArch);
|
||||
Object.defineProperty(process, 'platform', originalPlatform);
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
@@ -153,15 +146,9 @@ describe('restoreCache', () => {
|
||||
expect(infoSpy).toHaveBeenCalledWith('build cache is not found');
|
||||
});
|
||||
|
||||
it.each([
|
||||
{platform: 'linux', firstFails: false},
|
||||
{platform: 'linux', firstFails: true},
|
||||
{platform: 'win32', firstFails: false},
|
||||
{platform: 'win32', firstFails: true}
|
||||
])(
|
||||
'starts both restores concurrently on $platform and waits for both (first fails=$firstFails)',
|
||||
async ({platform, firstFails}) => {
|
||||
Object.defineProperty(process, 'platform', {value: platform});
|
||||
it.each([false, true])(
|
||||
'restores entries sequentially and waits for both (first fails=%s)',
|
||||
async firstFails => {
|
||||
const started = [deferred(), deferred()];
|
||||
const release = [deferred(), deferred()];
|
||||
let completed = false;
|
||||
@@ -179,10 +166,14 @@ describe('restoreCache', () => {
|
||||
completed = true;
|
||||
});
|
||||
try {
|
||||
await Promise.all(started.map(signal => signal.promise));
|
||||
await started[0].promise;
|
||||
expect(restoreCacheSpy).toHaveBeenCalledTimes(1);
|
||||
expect(core.saveState).not.toHaveBeenCalled();
|
||||
expect(setOutputSpy).not.toHaveBeenCalledWith('cache-hit', true);
|
||||
|
||||
release[0].resolve();
|
||||
// Let the first completion propagate while the second stays blocked.
|
||||
await new Promise<void>(resolve => setImmediate(resolve));
|
||||
await started[1].promise;
|
||||
expect(restoreCacheSpy).toHaveBeenCalledTimes(2);
|
||||
expect(completed).toBe(false);
|
||||
expect(core.saveState).not.toHaveBeenCalled();
|
||||
expect(setOutputSpy).not.toHaveBeenCalledWith('cache-hit', true);
|
||||
@@ -196,38 +187,6 @@ describe('restoreCache', () => {
|
||||
}
|
||||
);
|
||||
|
||||
it.each(['linux', 'win32'])(
|
||||
'restores overlapping directories sequentially on %s',
|
||||
async platform => {
|
||||
Object.defineProperty(process, 'platform', {value: platform});
|
||||
getCacheDirectoryPathSpy.mockResolvedValue([
|
||||
directories[0],
|
||||
{kind: 'build', path: path.join(modulePath, 'build')}
|
||||
]);
|
||||
const firstStarted = deferred();
|
||||
const releaseFirst = deferred();
|
||||
restoreCacheSpy.mockImplementation(async (paths, key) => {
|
||||
if (paths[0] === modulePath) {
|
||||
firstStarted.resolve();
|
||||
await releaseFirst.promise;
|
||||
}
|
||||
return key;
|
||||
});
|
||||
|
||||
const pending = restore();
|
||||
try {
|
||||
await firstStarted.promise;
|
||||
expect(restoreCacheSpy).toHaveBeenCalledTimes(1);
|
||||
} finally {
|
||||
releaseFirst.resolve();
|
||||
await pending;
|
||||
}
|
||||
|
||||
expect(restoreCacheSpy).toHaveBeenCalledTimes(2);
|
||||
expect(setOutputSpy).toHaveBeenLastCalledWith('cache-hit', true);
|
||||
}
|
||||
);
|
||||
|
||||
it.each([
|
||||
{modules: false, build: false},
|
||||
{modules: true, build: false},
|
||||
|
||||
@@ -41,16 +41,17 @@ function deferred() {
|
||||
describe('cache-save', () => {
|
||||
let entries: CacheEntry[];
|
||||
|
||||
let getBooleanInputSpy: jest.Mock<typeof core.getBooleanInput>;
|
||||
let getStateSpy: jest.Mock<typeof core.getState>;
|
||||
let infoSpy: jest.Mock<typeof core.info>;
|
||||
let warningSpy: jest.Mock<typeof core.warning>;
|
||||
let debugSpy: jest.Mock<typeof core.debug>;
|
||||
let setFailedSpy: jest.Mock<typeof core.setFailed>;
|
||||
let saveCacheSpy: jest.Mock<typeof cache.saveCache>;
|
||||
let existsSpy: jest.Mock<typeof fs.existsSync>;
|
||||
const getBooleanInputSpy = jest.mocked(core.getBooleanInput);
|
||||
const getStateSpy = jest.mocked(core.getState);
|
||||
const infoSpy = jest.mocked(core.info);
|
||||
const warningSpy = jest.mocked(core.warning);
|
||||
const debugSpy = jest.mocked(core.debug);
|
||||
const setFailedSpy = jest.mocked(core.setFailed);
|
||||
const saveCacheSpy = jest.mocked(cache.saveCache);
|
||||
const existsSpy = jest.mocked(fs.existsSync);
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
entries = [
|
||||
{
|
||||
kind: 'modules',
|
||||
@@ -64,36 +65,14 @@ describe('cache-save', () => {
|
||||
}
|
||||
];
|
||||
|
||||
getBooleanInputSpy = core.getBooleanInput as jest.Mock<
|
||||
typeof core.getBooleanInput
|
||||
>;
|
||||
getBooleanInputSpy.mockReturnValue(true);
|
||||
|
||||
getStateSpy = core.getState as jest.Mock<typeof core.getState>;
|
||||
getStateSpy.mockImplementation(() => JSON.stringify(entries));
|
||||
|
||||
infoSpy = core.info as jest.Mock<typeof core.info>;
|
||||
infoSpy.mockImplementation(() => undefined);
|
||||
|
||||
warningSpy = core.warning as jest.Mock<typeof core.warning>;
|
||||
warningSpy.mockImplementation(() => undefined);
|
||||
|
||||
debugSpy = core.debug as jest.Mock<typeof core.debug>;
|
||||
debugSpy.mockImplementation(() => undefined);
|
||||
|
||||
setFailedSpy = core.setFailed as jest.Mock<typeof core.setFailed>;
|
||||
setFailedSpy.mockImplementation(() => undefined);
|
||||
|
||||
saveCacheSpy = cache.saveCache as jest.Mock<typeof cache.saveCache>;
|
||||
saveCacheSpy.mockImplementation(() => Promise.resolve(0));
|
||||
|
||||
existsSpy = fs.existsSync as jest.Mock<typeof fs.existsSync>;
|
||||
existsSpy.mockImplementation(() => true);
|
||||
saveCacheSpy.mockResolvedValue(0);
|
||||
existsSpy.mockReturnValue(true);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('does not save cache when the cache input is false', async () => {
|
||||
@@ -146,7 +125,7 @@ describe('cache-save', () => {
|
||||
);
|
||||
|
||||
it('does not save cache when there are no cache folders on the disk', async () => {
|
||||
existsSpy.mockImplementation(() => false);
|
||||
existsSpy.mockReturnValue(false);
|
||||
|
||||
await run();
|
||||
|
||||
|
||||
@@ -1,14 +1,4 @@
|
||||
import {
|
||||
jest,
|
||||
describe,
|
||||
it,
|
||||
expect,
|
||||
beforeEach,
|
||||
afterEach,
|
||||
afterAll
|
||||
} from '@jest/globals';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import {jest, describe, it, expect, beforeEach, afterAll} from '@jest/globals';
|
||||
|
||||
jest.unstable_mockModule('@actions/exec', () => ({
|
||||
exec: jest.fn(),
|
||||
@@ -212,139 +202,6 @@ describe('getCacheDirectoryPath', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('canRestoreCachesInParallel', () => {
|
||||
const originalPlatform = Object.getOwnPropertyDescriptor(
|
||||
process,
|
||||
'platform'
|
||||
)!;
|
||||
const directoryStat = fs.lstatSync(process.cwd());
|
||||
|
||||
beforeEach(() => {
|
||||
Object.defineProperty(process, 'platform', {value: 'linux'});
|
||||
jest.spyOn(fs, 'lstatSync').mockReturnValue(directoryStat);
|
||||
jest
|
||||
.spyOn(fs, 'realpathSync')
|
||||
.mockImplementation(directory => path.resolve(directory.toString()));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
Object.defineProperty(process, 'platform', originalPlatform);
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it.each([
|
||||
{modules: 'cache/modules', build: 'cache/build', parallel: true},
|
||||
{modules: 'cache', build: 'cache-build', parallel: true},
|
||||
{modules: 'cache', build: 'cache', parallel: false},
|
||||
{modules: 'cache', build: 'cache/build', parallel: false},
|
||||
{modules: 'cache/modules', build: 'cache', parallel: false},
|
||||
{modules: 'cache', build: 'cache/../cache', parallel: false}
|
||||
])(
|
||||
'checks overlapping paths: $modules and $build',
|
||||
({modules, build, parallel}) => {
|
||||
expect(
|
||||
cacheUtils.canRestoreCachesInParallel([
|
||||
{kind: 'modules', path: modules},
|
||||
{kind: 'build', path: build}
|
||||
])
|
||||
).toBe(parallel);
|
||||
}
|
||||
);
|
||||
|
||||
it('allows concurrent Windows restores with disjoint paths', () => {
|
||||
Object.defineProperty(process, 'platform', {value: 'win32'});
|
||||
|
||||
expect(
|
||||
cacheUtils.canRestoreCachesInParallel([
|
||||
{kind: 'modules', path: 'cache/modules'},
|
||||
{kind: 'build', path: 'cache/build'}
|
||||
])
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it.each(['darwin', 'win32'])(
|
||||
'handles case-insensitive paths conservatively on %s',
|
||||
platform => {
|
||||
Object.defineProperty(process, 'platform', {value: platform});
|
||||
|
||||
expect(
|
||||
cacheUtils.canRestoreCachesInParallel([
|
||||
{kind: 'modules', path: 'cache'},
|
||||
{kind: 'build', path: 'CACHE/build'}
|
||||
])
|
||||
).toBe(false);
|
||||
}
|
||||
);
|
||||
|
||||
it.each([true, false])(
|
||||
'serializes symlink aliases with missing cache directories (overlap=%s)',
|
||||
overlap => {
|
||||
const alias = path.resolve('alias');
|
||||
const target = path.resolve('target');
|
||||
jest.spyOn(fs, 'lstatSync').mockImplementation(directory => {
|
||||
if (directory !== alias && directory !== target) {
|
||||
throw Object.assign(new Error('Missing path'), {code: 'ENOENT'});
|
||||
}
|
||||
return directoryStat;
|
||||
});
|
||||
jest.spyOn(fs, 'realpathSync').mockReturnValue(target);
|
||||
|
||||
expect(
|
||||
cacheUtils.canRestoreCachesInParallel([
|
||||
{kind: 'modules', path: path.join(alias, 'modules')},
|
||||
{
|
||||
kind: 'build',
|
||||
path: path.join(target, overlap ? 'modules/build' : 'build')
|
||||
}
|
||||
])
|
||||
).toBe(false);
|
||||
}
|
||||
);
|
||||
|
||||
it.each(['nested alias', 'alias through module cache'])(
|
||||
'serializes restores when the %s resolves outside the module cache',
|
||||
scenario => {
|
||||
const modules = path.resolve('modules');
|
||||
const target = path.resolve('external-build');
|
||||
const alias = path.join(modules, 'build-link');
|
||||
const build =
|
||||
scenario === 'nested alias'
|
||||
? path.join(alias, 'cache')
|
||||
: path.resolve('build-alias', 'cache');
|
||||
jest.spyOn(fs, 'realpathSync').mockImplementation(directory => {
|
||||
const absolute = path.resolve(directory.toString());
|
||||
if (absolute === build) {
|
||||
return path.join(target, 'cache');
|
||||
}
|
||||
if (absolute === path.dirname(build)) {
|
||||
return scenario === 'nested alias' ? target : alias;
|
||||
}
|
||||
return absolute;
|
||||
});
|
||||
|
||||
expect(
|
||||
cacheUtils.canRestoreCachesInParallel([
|
||||
{kind: 'modules', path: modules},
|
||||
{kind: 'build', path: build}
|
||||
])
|
||||
).toBe(false);
|
||||
}
|
||||
);
|
||||
|
||||
it('falls back to sequential restores when path resolution fails', () => {
|
||||
jest.spyOn(fs, 'realpathSync').mockImplementation(() => {
|
||||
throw new Error('Cannot resolve path');
|
||||
});
|
||||
|
||||
expect(
|
||||
cacheUtils.canRestoreCachesInParallel([
|
||||
{kind: 'modules', path: 'cache/modules'},
|
||||
{kind: 'build', path: 'cache/build'}
|
||||
])
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isCacheFeatureAvailable', () => {
|
||||
//Arrange
|
||||
const isFeatureAvailableSpy = cache.isFeatureAvailable as jest.Mock<
|
||||
|
||||
Reference in new issue
Block a user