2021-11-17 10:31:22 +00:00
|
|
|
import * as core from '@actions/core';
|
|
|
|
import * as cache from '@actions/cache';
|
|
|
|
import * as exec from '@actions/exec';
|
|
|
|
import {run} from '../src/cache-save';
|
|
|
|
import {State} from '../src/cache-distributions/cache-distributor';
|
|
|
|
|
|
|
|
describe('run', () => {
|
|
|
|
const pipFileLockHash =
|
2022-04-04 13:12:24 +00:00
|
|
|
'd1dd6218299d8a6db5fc2001d988b34a8b31f1e9d0bb4534d377dde7c19f64b3';
|
2021-11-17 10:31:22 +00:00
|
|
|
const requirementsHash =
|
|
|
|
'd8110e0006d7fb5ee76365d565eef9d37df1d11598b912d3eb66d398d57a1121';
|
|
|
|
const requirementsLinuxHash =
|
|
|
|
'2d0ff7f46b0e120e3d3294db65768b474934242637b9899b873e6283dfd16d7c';
|
2022-03-07 02:30:49 +00:00
|
|
|
const poetryLockHash =
|
|
|
|
'571bf984f8d210e6a97f854e479fdd4a2b5af67b5fdac109ec337a0ea16e7836';
|
2024-02-19 11:30:09 +00:00
|
|
|
const uvLockHash = 'TODO'; // TODO: what should be the correct value?
|
2021-11-17 10:31:22 +00:00
|
|
|
|
|
|
|
// core spy
|
|
|
|
let infoSpy: jest.SpyInstance;
|
|
|
|
let warningSpy: jest.SpyInstance;
|
|
|
|
let debugSpy: jest.SpyInstance;
|
Use correct Poetry config when collecting Poetry projects (#447)
* Use correct Poetry config when collecting Poetry projects
When collecting Poetry projects for caching, a '**/poetry.lock' glob is
used. However, in order to process the Poetry configuration, the
"poetry" command is run from the repo's root directory; this causes
Poetry to return an invalid configuration when there is a Poetry project
inside an inner directory.
Instead of running a single Poetry command, glob for the same pattern,
and run a Poetry command for every discovered project.
* Fix typo: saveSatetSpy -> saveStateSpy
* poetry: Support same virtualenv appearing in multiple projects
* Add nested Poetry projects test
* poetry: Set up environment for each project individually
* tests/cache-restore: Do not look for dependency files outside `data`
When the default dependency path is used for cache distributors, they
are looking for the dependency file in the project's root (including the
source code), which leads to tests taking a significant amount of time,
especially on Windows runners. We thus hit sporadic test failures.
Change the test cases such that dependency files are always searched for
inside of `__tests__/data`, ignoring the rest of the project.
* poetry: Simplify `virtualenvs.in-project` boolean check
* README: Explain that poetry might create multiple caches
* poetry: Run `poetry env use` only after cache is loaded
The virtualenv cache might contain invalid entries, such as virtualenvs
built in previous, buggy versions of this action. The `poetry env use`
command will recreate virtualenvs in case they are invalid, but it has
to be run only *after* the cache is loaded.
Refactor `CacheDistributor` a bit such that the validation (and possible
recreation) of virtualenvs happens only after the cache is loaded.
* poetry: Bump cache primary key
2023-01-03 16:13:00 +00:00
|
|
|
let saveStateSpy: jest.SpyInstance;
|
2021-11-17 10:31:22 +00:00
|
|
|
let getStateSpy: jest.SpyInstance;
|
|
|
|
let getInputSpy: jest.SpyInstance;
|
|
|
|
let setFailedSpy: jest.SpyInstance;
|
|
|
|
|
|
|
|
// cache spy
|
|
|
|
let saveCacheSpy: jest.SpyInstance;
|
|
|
|
|
|
|
|
// exec spy
|
|
|
|
let getExecOutputSpy: jest.SpyInstance;
|
|
|
|
|
|
|
|
let inputs = {} as any;
|
|
|
|
|
|
|
|
beforeEach(() => {
|
|
|
|
process.env['RUNNER_OS'] = process.env['RUNNER_OS'] ?? 'linux';
|
|
|
|
|
|
|
|
infoSpy = jest.spyOn(core, 'info');
|
|
|
|
infoSpy.mockImplementation(input => undefined);
|
|
|
|
|
|
|
|
warningSpy = jest.spyOn(core, 'warning');
|
|
|
|
warningSpy.mockImplementation(input => undefined);
|
|
|
|
|
|
|
|
debugSpy = jest.spyOn(core, 'debug');
|
|
|
|
debugSpy.mockImplementation(input => undefined);
|
|
|
|
|
Use correct Poetry config when collecting Poetry projects (#447)
* Use correct Poetry config when collecting Poetry projects
When collecting Poetry projects for caching, a '**/poetry.lock' glob is
used. However, in order to process the Poetry configuration, the
"poetry" command is run from the repo's root directory; this causes
Poetry to return an invalid configuration when there is a Poetry project
inside an inner directory.
Instead of running a single Poetry command, glob for the same pattern,
and run a Poetry command for every discovered project.
* Fix typo: saveSatetSpy -> saveStateSpy
* poetry: Support same virtualenv appearing in multiple projects
* Add nested Poetry projects test
* poetry: Set up environment for each project individually
* tests/cache-restore: Do not look for dependency files outside `data`
When the default dependency path is used for cache distributors, they
are looking for the dependency file in the project's root (including the
source code), which leads to tests taking a significant amount of time,
especially on Windows runners. We thus hit sporadic test failures.
Change the test cases such that dependency files are always searched for
inside of `__tests__/data`, ignoring the rest of the project.
* poetry: Simplify `virtualenvs.in-project` boolean check
* README: Explain that poetry might create multiple caches
* poetry: Run `poetry env use` only after cache is loaded
The virtualenv cache might contain invalid entries, such as virtualenvs
built in previous, buggy versions of this action. The `poetry env use`
command will recreate virtualenvs in case they are invalid, but it has
to be run only *after* the cache is loaded.
Refactor `CacheDistributor` a bit such that the validation (and possible
recreation) of virtualenvs happens only after the cache is loaded.
* poetry: Bump cache primary key
2023-01-03 16:13:00 +00:00
|
|
|
saveStateSpy = jest.spyOn(core, 'saveState');
|
|
|
|
saveStateSpy.mockImplementation(input => undefined);
|
2021-11-17 10:31:22 +00:00
|
|
|
|
|
|
|
getStateSpy = jest.spyOn(core, 'getState');
|
|
|
|
getStateSpy.mockImplementation(input => {
|
|
|
|
if (input === State.CACHE_PATHS) {
|
|
|
|
return JSON.stringify([__dirname]);
|
|
|
|
}
|
|
|
|
return requirementsHash;
|
|
|
|
});
|
|
|
|
|
|
|
|
setFailedSpy = jest.spyOn(core, 'setFailed');
|
|
|
|
|
|
|
|
getInputSpy = jest.spyOn(core, 'getInput');
|
|
|
|
getInputSpy.mockImplementation(input => inputs[input]);
|
|
|
|
|
|
|
|
getExecOutputSpy = jest.spyOn(exec, 'getExecOutput');
|
|
|
|
getExecOutputSpy.mockImplementation((input: string) => {
|
|
|
|
if (input.includes('pip')) {
|
|
|
|
return {stdout: 'pip', stderr: '', exitCode: 0};
|
|
|
|
}
|
|
|
|
|
|
|
|
return {stdout: '', stderr: 'Error occured', exitCode: 2};
|
|
|
|
});
|
|
|
|
|
|
|
|
saveCacheSpy = jest.spyOn(cache, 'saveCache');
|
|
|
|
saveCacheSpy.mockImplementation(() => undefined);
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('Package manager validation', () => {
|
|
|
|
it('Package manager is not provided, skip caching', async () => {
|
|
|
|
inputs['cache'] = '';
|
|
|
|
await run();
|
|
|
|
|
|
|
|
expect(getInputSpy).toHaveBeenCalled();
|
|
|
|
expect(infoSpy).not.toHaveBeenCalled();
|
|
|
|
expect(saveCacheSpy).not.toHaveBeenCalled();
|
|
|
|
expect(setFailedSpy).not.toHaveBeenCalled();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('Validate unchanged cache is not saved', () => {
|
|
|
|
it('should not save cache for pip', async () => {
|
|
|
|
inputs['cache'] = 'pip';
|
2023-04-06 10:02:34 +00:00
|
|
|
inputs['python-version'] = '3.10.0';
|
2021-11-17 10:31:22 +00:00
|
|
|
|
|
|
|
await run();
|
|
|
|
|
|
|
|
expect(getInputSpy).toHaveBeenCalled();
|
|
|
|
expect(debugSpy).toHaveBeenCalledWith(
|
|
|
|
`paths for caching are ${__dirname}`
|
|
|
|
);
|
|
|
|
expect(getStateSpy).toHaveBeenCalledTimes(3);
|
|
|
|
expect(infoSpy).toHaveBeenCalledWith(
|
|
|
|
`Cache hit occurred on the primary key ${requirementsHash}, not saving cache.`
|
|
|
|
);
|
|
|
|
expect(setFailedSpy).not.toHaveBeenCalled();
|
|
|
|
});
|
2021-11-24 19:58:34 +00:00
|
|
|
|
|
|
|
it('should not save cache for pipenv', async () => {
|
|
|
|
inputs['cache'] = 'pipenv';
|
2023-04-06 10:02:34 +00:00
|
|
|
inputs['python-version'] = '3.10.0';
|
2021-11-24 19:58:34 +00:00
|
|
|
|
|
|
|
await run();
|
|
|
|
|
|
|
|
expect(getInputSpy).toHaveBeenCalled();
|
|
|
|
expect(debugSpy).toHaveBeenCalledWith(
|
|
|
|
`paths for caching are ${__dirname}`
|
|
|
|
);
|
|
|
|
expect(getStateSpy).toHaveBeenCalledTimes(3);
|
|
|
|
expect(infoSpy).toHaveBeenCalledWith(
|
|
|
|
`Cache hit occurred on the primary key ${requirementsHash}, not saving cache.`
|
|
|
|
);
|
|
|
|
expect(setFailedSpy).not.toHaveBeenCalled();
|
|
|
|
});
|
2021-11-17 10:31:22 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
describe('action saves the cache', () => {
|
|
|
|
it('saves cache from pip', async () => {
|
|
|
|
inputs['cache'] = 'pip';
|
2023-04-06 10:02:34 +00:00
|
|
|
inputs['python-version'] = '3.10.0';
|
2021-11-17 10:31:22 +00:00
|
|
|
getStateSpy.mockImplementation((name: string) => {
|
|
|
|
if (name === State.CACHE_MATCHED_KEY) {
|
|
|
|
return requirementsHash;
|
|
|
|
} else if (name === State.CACHE_PATHS) {
|
|
|
|
return JSON.stringify([__dirname]);
|
|
|
|
} else {
|
|
|
|
return pipFileLockHash;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
await run();
|
|
|
|
|
|
|
|
expect(getInputSpy).toHaveBeenCalled();
|
|
|
|
expect(getStateSpy).toHaveBeenCalledTimes(3);
|
|
|
|
expect(infoSpy).not.toHaveBeenCalledWith(
|
|
|
|
`Cache hit occurred on the primary key ${requirementsHash}, not saving cache.`
|
|
|
|
);
|
|
|
|
expect(saveCacheSpy).toHaveBeenCalled();
|
|
|
|
expect(infoSpy).toHaveBeenLastCalledWith(
|
|
|
|
`Cache saved with the key: ${pipFileLockHash}`
|
|
|
|
);
|
|
|
|
expect(setFailedSpy).not.toHaveBeenCalled();
|
|
|
|
});
|
|
|
|
|
|
|
|
it('saves cache from pipenv', async () => {
|
|
|
|
inputs['cache'] = 'pipenv';
|
2023-04-06 10:02:34 +00:00
|
|
|
inputs['python-version'] = '3.10.0';
|
2021-11-17 10:31:22 +00:00
|
|
|
getStateSpy.mockImplementation((name: string) => {
|
|
|
|
if (name === State.CACHE_MATCHED_KEY) {
|
|
|
|
return pipFileLockHash;
|
|
|
|
} else if (name === State.CACHE_PATHS) {
|
|
|
|
return JSON.stringify([__dirname]);
|
|
|
|
} else {
|
|
|
|
return requirementsHash;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
await run();
|
|
|
|
|
|
|
|
expect(getInputSpy).toHaveBeenCalled();
|
|
|
|
expect(getStateSpy).toHaveBeenCalledTimes(3);
|
|
|
|
expect(infoSpy).not.toHaveBeenCalledWith(
|
|
|
|
`Cache hit occurred on the primary key ${pipFileLockHash}, not saving cache.`
|
|
|
|
);
|
|
|
|
expect(saveCacheSpy).toHaveBeenCalled();
|
|
|
|
expect(infoSpy).toHaveBeenLastCalledWith(
|
|
|
|
`Cache saved with the key: ${requirementsHash}`
|
|
|
|
);
|
|
|
|
expect(setFailedSpy).not.toHaveBeenCalled();
|
|
|
|
});
|
2021-11-24 19:58:34 +00:00
|
|
|
|
|
|
|
it('saves cache from poetry', async () => {
|
|
|
|
inputs['cache'] = 'poetry';
|
2023-04-06 10:02:34 +00:00
|
|
|
inputs['python-version'] = '3.10.0';
|
2021-11-24 19:58:34 +00:00
|
|
|
getStateSpy.mockImplementation((name: string) => {
|
|
|
|
if (name === State.CACHE_MATCHED_KEY) {
|
|
|
|
return poetryLockHash;
|
|
|
|
} else if (name === State.CACHE_PATHS) {
|
|
|
|
return JSON.stringify([__dirname]);
|
|
|
|
} else {
|
|
|
|
return requirementsHash;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
await run();
|
|
|
|
|
|
|
|
expect(getInputSpy).toHaveBeenCalled();
|
|
|
|
expect(getStateSpy).toHaveBeenCalledTimes(3);
|
|
|
|
expect(infoSpy).not.toHaveBeenCalledWith(
|
|
|
|
`Cache hit occurred on the primary key ${poetryLockHash}, not saving cache.`
|
|
|
|
);
|
|
|
|
expect(saveCacheSpy).toHaveBeenCalled();
|
|
|
|
expect(infoSpy).toHaveBeenLastCalledWith(
|
2024-02-19 11:30:09 +00:00
|
|
|
`Cache saved with the key: ${requirementsHash}`
|
|
|
|
);
|
|
|
|
expect(setFailedSpy).not.toHaveBeenCalled();
|
|
|
|
});
|
|
|
|
|
|
|
|
it('saves cache from uv', async () => {
|
|
|
|
inputs['cache'] = 'uv';
|
|
|
|
inputs['python-version'] = '3.12.0';
|
|
|
|
getStateSpy.mockImplementation((name: string) => {
|
|
|
|
if (name === State.CACHE_MATCHED_KEY) {
|
|
|
|
console.log(name);
|
|
|
|
return uvLockHash;
|
|
|
|
} else if (name === State.CACHE_PATHS) {
|
|
|
|
return JSON.stringify([__dirname]);
|
|
|
|
} else {
|
|
|
|
return requirementsHash;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
await run();
|
|
|
|
|
|
|
|
expect(getInputSpy).toHaveBeenCalled();
|
|
|
|
expect(getStateSpy).toHaveBeenCalledTimes(3);
|
|
|
|
expect(infoSpy).not.toHaveBeenCalledWith(
|
|
|
|
`Cache hit occurred on the primary key ${uvLockHash}, not saving cache.`
|
|
|
|
);
|
|
|
|
expect(saveCacheSpy).toHaveBeenCalled();
|
|
|
|
expect(infoSpy).toHaveBeenLastCalledWith(
|
2021-11-24 19:58:34 +00:00
|
|
|
`Cache saved with the key: ${requirementsHash}`
|
|
|
|
);
|
|
|
|
expect(setFailedSpy).not.toHaveBeenCalled();
|
|
|
|
});
|
2022-06-28 13:17:50 +00:00
|
|
|
|
|
|
|
it('saves with -1 cacheId , should not fail workflow', async () => {
|
|
|
|
inputs['cache'] = 'poetry';
|
2023-04-06 10:02:34 +00:00
|
|
|
inputs['python-version'] = '3.10.0';
|
2022-06-28 13:17:50 +00:00
|
|
|
getStateSpy.mockImplementation((name: string) => {
|
|
|
|
if (name === State.STATE_CACHE_PRIMARY_KEY) {
|
|
|
|
return poetryLockHash;
|
|
|
|
} else if (name === State.CACHE_PATHS) {
|
|
|
|
return JSON.stringify([__dirname]);
|
|
|
|
} else {
|
|
|
|
return requirementsHash;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
saveCacheSpy.mockImplementation(() => {
|
|
|
|
return -1;
|
|
|
|
});
|
|
|
|
|
|
|
|
await run();
|
|
|
|
|
|
|
|
expect(getInputSpy).toHaveBeenCalled();
|
|
|
|
expect(getStateSpy).toHaveBeenCalledTimes(3);
|
|
|
|
expect(infoSpy).not.toHaveBeenCalled();
|
|
|
|
expect(saveCacheSpy).toHaveBeenCalled();
|
|
|
|
expect(infoSpy).not.toHaveBeenLastCalledWith(
|
|
|
|
`Cache saved with the key: ${poetryLockHash}`
|
|
|
|
);
|
|
|
|
expect(setFailedSpy).not.toHaveBeenCalled();
|
|
|
|
});
|
|
|
|
|
2023-03-10 11:15:18 +00:00
|
|
|
it('saves with error from toolkit, should not fail the workflow', async () => {
|
2022-06-28 13:17:50 +00:00
|
|
|
inputs['cache'] = 'npm';
|
2023-04-06 10:02:34 +00:00
|
|
|
inputs['python-version'] = '3.10.0';
|
2022-06-28 13:17:50 +00:00
|
|
|
getStateSpy.mockImplementation((name: string) => {
|
|
|
|
if (name === State.STATE_CACHE_PRIMARY_KEY) {
|
|
|
|
return poetryLockHash;
|
|
|
|
} else if (name === State.CACHE_PATHS) {
|
|
|
|
return JSON.stringify([__dirname]);
|
|
|
|
} else {
|
|
|
|
return requirementsHash;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
saveCacheSpy.mockImplementation(() => {
|
|
|
|
throw new cache.ValidationError('Validation failed');
|
|
|
|
});
|
|
|
|
|
|
|
|
await run();
|
|
|
|
|
|
|
|
expect(getInputSpy).toHaveBeenCalled();
|
|
|
|
expect(getStateSpy).toHaveBeenCalledTimes(3);
|
|
|
|
expect(infoSpy).not.toHaveBeenCalledWith();
|
|
|
|
expect(saveCacheSpy).toHaveBeenCalled();
|
2023-03-10 11:15:18 +00:00
|
|
|
expect(setFailedSpy).not.toHaveBeenCalled();
|
2022-06-28 13:17:50 +00:00
|
|
|
});
|
2021-11-17 10:31:22 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
afterEach(() => {
|
|
|
|
jest.resetAllMocks();
|
|
|
|
jest.clearAllMocks();
|
|
|
|
inputs = {};
|
|
|
|
});
|
|
|
|
});
|