mirror of https://github.com/actions/setup-go.git
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
454 lines
13 KiB
TypeScript
454 lines
13 KiB
TypeScript
import {
|
|
jest,
|
|
describe,
|
|
it,
|
|
expect,
|
|
beforeEach,
|
|
afterEach,
|
|
afterAll
|
|
} from '@jest/globals';
|
|
import fs from 'fs';
|
|
import path from 'path';
|
|
|
|
jest.unstable_mockModule('@actions/exec', () => ({
|
|
exec: jest.fn(),
|
|
getExecOutput: jest.fn()
|
|
}));
|
|
|
|
jest.unstable_mockModule('@actions/cache', () => ({
|
|
saveCache: jest.fn(),
|
|
restoreCache: jest.fn(),
|
|
isFeatureAvailable: jest.fn()
|
|
}));
|
|
|
|
jest.unstable_mockModule('@actions/core', () => ({
|
|
info: jest.fn(),
|
|
warning: jest.fn(),
|
|
debug: jest.fn(),
|
|
error: jest.fn(),
|
|
notice: jest.fn(),
|
|
setFailed: jest.fn(),
|
|
setOutput: jest.fn(),
|
|
getInput: jest.fn(),
|
|
getBooleanInput: jest.fn(),
|
|
getMultilineInput: jest.fn(),
|
|
addPath: jest.fn(),
|
|
exportVariable: jest.fn(),
|
|
saveState: jest.fn(),
|
|
getState: jest.fn(),
|
|
setSecret: jest.fn(),
|
|
isDebug: jest.fn(() => false),
|
|
startGroup: jest.fn(),
|
|
endGroup: jest.fn(),
|
|
group: jest.fn((_name: string, fn: () => Promise<unknown>) => fn()),
|
|
toPlatformPath: jest.fn((p: string) => p),
|
|
toWin32Path: jest.fn((p: string) => p),
|
|
toPosixPath: jest.fn((p: string) => p)
|
|
}));
|
|
|
|
const exec = await import('@actions/exec');
|
|
const cache = await import('@actions/cache');
|
|
const core = await import('@actions/core');
|
|
const cacheUtils = await import('../src/cache-utils.js');
|
|
import type {PackageManagerInfo} from '../src/package-managers.js';
|
|
|
|
describe('getCommandOutput', () => {
|
|
//Arrange
|
|
const getExecOutputSpy = exec.getExecOutput as jest.Mock<
|
|
typeof exec.getExecOutput
|
|
>;
|
|
|
|
it('should return trimmed stdout in case of successful exit code', async () => {
|
|
//Arrange
|
|
const stdoutResult = ' stdout ';
|
|
const trimmedStdout = stdoutResult.trim();
|
|
|
|
getExecOutputSpy.mockImplementation(async (commandLine: string) => {
|
|
return {exitCode: 0, stdout: stdoutResult, stderr: ''};
|
|
});
|
|
|
|
//Act + Assert
|
|
return cacheUtils
|
|
.getCommandOutput('command')
|
|
.then(data => expect(data).toBe(trimmedStdout));
|
|
});
|
|
|
|
it('should return error in case of unsuccessful exit code', async () => {
|
|
//Arrange
|
|
const stderrResult = 'error message';
|
|
|
|
getExecOutputSpy.mockImplementation(async (commandLine: string) => {
|
|
return {exitCode: 10, stdout: '', stderr: stderrResult};
|
|
});
|
|
|
|
//Act + Assert
|
|
await expect(async () => {
|
|
await cacheUtils.getCommandOutput('command');
|
|
}).rejects.toThrow();
|
|
});
|
|
});
|
|
|
|
describe('getPackageManagerInfo', () => {
|
|
it('should return package manager info in case of valid package manager name', async () => {
|
|
//Arrange
|
|
const packageManagerName = 'default';
|
|
const expectedResult = {
|
|
dependencyFilePattern: 'go.mod',
|
|
cacheFolderCommandList: [
|
|
{kind: 'modules', command: 'go env GOMODCACHE'},
|
|
{kind: 'build', command: 'go env GOCACHE'}
|
|
]
|
|
};
|
|
|
|
//Act + Assert
|
|
return cacheUtils
|
|
.getPackageManagerInfo(packageManagerName)
|
|
.then(data => expect(data).toEqual(expectedResult));
|
|
});
|
|
|
|
it('should throw the error in case of invalid package manager name', async () => {
|
|
//Arrange
|
|
const packageManagerName = 'invalidName';
|
|
|
|
//Act + Assert
|
|
await expect(async () => {
|
|
await cacheUtils.getPackageManagerInfo(packageManagerName);
|
|
}).rejects.toThrow();
|
|
});
|
|
});
|
|
|
|
describe('getCacheDirectoryPath', () => {
|
|
//Arrange
|
|
const getExecOutputSpy = exec.getExecOutput as jest.Mock<
|
|
typeof exec.getExecOutput
|
|
>;
|
|
|
|
const validPackageManager: PackageManagerInfo = {
|
|
dependencyFilePattern: 'go.mod',
|
|
cacheFolderCommandList: [
|
|
{kind: 'modules', command: 'go env GOMODCACHE'},
|
|
{kind: 'build', command: 'go env GOCACHE'}
|
|
]
|
|
};
|
|
|
|
it('should return path to the cache folders which specified package manager uses', async () => {
|
|
//Arrange
|
|
getExecOutputSpy.mockImplementation(async (commandLine: string) => {
|
|
return {
|
|
exitCode: 0,
|
|
stdout:
|
|
commandLine === 'go env GOMODCACHE'
|
|
? 'path/to/modules'
|
|
: 'path/to/build',
|
|
stderr: ''
|
|
};
|
|
});
|
|
|
|
const expectedResult = [
|
|
{kind: 'modules', path: 'path/to/modules'},
|
|
{kind: 'build', path: 'path/to/build'}
|
|
];
|
|
|
|
//Act + Assert
|
|
return cacheUtils
|
|
.getCacheDirectoryPath(validPackageManager)
|
|
.then(data => expect(data).toEqual(expectedResult));
|
|
});
|
|
|
|
it.each(validPackageManager.cacheFolderCommandList)(
|
|
'preserves the $kind identity when the other command returns an empty path',
|
|
async ({kind, command}) => {
|
|
getExecOutputSpy.mockImplementation(async commandLine => ({
|
|
exitCode: 0,
|
|
stdout: commandLine === command ? 'path/to/cache/folder' : '',
|
|
stderr: ''
|
|
}));
|
|
|
|
await expect(
|
|
cacheUtils.getCacheDirectoryPath(validPackageManager)
|
|
).resolves.toEqual([{kind, path: 'path/to/cache/folder'}]);
|
|
}
|
|
);
|
|
|
|
it.each(validPackageManager.cacheFolderCommandList)(
|
|
'preserves the $kind identity when the other command fails',
|
|
async ({kind, command}) => {
|
|
getExecOutputSpy.mockImplementation(async commandLine => {
|
|
if (commandLine !== command) {
|
|
throw new Error('Could not run go env');
|
|
}
|
|
return {exitCode: 0, stdout: 'path/to/cache/folder', stderr: ''};
|
|
});
|
|
|
|
await expect(
|
|
cacheUtils.getCacheDirectoryPath(validPackageManager)
|
|
).resolves.toEqual([{kind, path: 'path/to/cache/folder'}]);
|
|
expect(core.info).toHaveBeenCalledWith(
|
|
expect.stringContaining('Could not run go env')
|
|
);
|
|
}
|
|
);
|
|
|
|
it('should throw if the both commands return empty str', async () => {
|
|
getExecOutputSpy.mockImplementation(async (commandLine: string) => {
|
|
return {exitCode: 0, stdout: '', stderr: ''};
|
|
});
|
|
|
|
//Act + Assert
|
|
await expect(async () => {
|
|
await cacheUtils.getCacheDirectoryPath(validPackageManager);
|
|
}).rejects.toThrow();
|
|
});
|
|
|
|
it('should throw if both commands fail', async () => {
|
|
getExecOutputSpy.mockImplementation(async (commandLine: string) => {
|
|
return {exitCode: 10, stdout: '', stderr: 'Error message'};
|
|
});
|
|
|
|
//Act + Assert
|
|
await expect(async () => {
|
|
await cacheUtils.getCacheDirectoryPath(validPackageManager);
|
|
}).rejects.toThrow();
|
|
});
|
|
});
|
|
|
|
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<
|
|
typeof cache.isFeatureAvailable
|
|
>;
|
|
const warningSpy = core.warning as jest.Mock<typeof core.warning>;
|
|
|
|
it('should return true when cache feature is available', () => {
|
|
//Arrange
|
|
isFeatureAvailableSpy.mockImplementation(() => {
|
|
return true;
|
|
});
|
|
|
|
//Act
|
|
const functionResult = cacheUtils.isCacheFeatureAvailable();
|
|
|
|
//Assert
|
|
expect(functionResult).toBeTruthy();
|
|
});
|
|
|
|
it('should warn when cache feature is unavailable and GHES is not used', () => {
|
|
//Arrange
|
|
isFeatureAvailableSpy.mockImplementation(() => {
|
|
return false;
|
|
});
|
|
|
|
process.env['GITHUB_SERVER_URL'] = 'https://github.com';
|
|
|
|
const warningMessage =
|
|
'The runner was not able to contact the cache service. Caching will be skipped';
|
|
|
|
//Act
|
|
cacheUtils.isCacheFeatureAvailable();
|
|
|
|
//Assert
|
|
expect(warningSpy).toHaveBeenCalledWith(warningMessage);
|
|
});
|
|
|
|
it('should return false when cache feature is unavailable', () => {
|
|
//Arrange
|
|
isFeatureAvailableSpy.mockImplementation(() => {
|
|
return false;
|
|
});
|
|
|
|
process.env['GITHUB_SERVER_URL'] = 'https://github.com';
|
|
|
|
//Act
|
|
const functionResult = cacheUtils.isCacheFeatureAvailable();
|
|
|
|
//Assert
|
|
expect(functionResult).toBeFalsy();
|
|
});
|
|
|
|
it('should warn when cache feature is unavailable and GHES is used', () => {
|
|
//Arrange
|
|
isFeatureAvailableSpy.mockImplementation(() => {
|
|
return false;
|
|
});
|
|
|
|
process.env['GITHUB_SERVER_URL'] = 'https://nongithub.com';
|
|
|
|
const warningMessage =
|
|
'Cache action is only supported on GHES version >= 3.5. If you are on version >=3.5 Please check with GHES admin if Actions cache service is enabled or not.';
|
|
|
|
//Act + Assert
|
|
expect(cacheUtils.isCacheFeatureAvailable()).toBeFalsy();
|
|
expect(warningSpy).toHaveBeenCalledWith(warningMessage);
|
|
});
|
|
});
|
|
|
|
describe('isGhes', () => {
|
|
const pristineEnv = {...process.env};
|
|
|
|
beforeEach(() => {
|
|
process.env = {...pristineEnv};
|
|
});
|
|
|
|
afterAll(() => {
|
|
process.env = pristineEnv;
|
|
});
|
|
|
|
it('returns false when the GITHUB_SERVER_URL environment variable is not defined', async () => {
|
|
delete process.env['GITHUB_SERVER_URL'];
|
|
expect(cacheUtils.isGhes()).toBeFalsy();
|
|
});
|
|
|
|
it('returns false when the GITHUB_SERVER_URL environment variable is set to github.com', async () => {
|
|
process.env['GITHUB_SERVER_URL'] = 'https://github.com';
|
|
expect(cacheUtils.isGhes()).toBeFalsy();
|
|
});
|
|
|
|
it('returns false when the GITHUB_SERVER_URL environment variable is set to a GitHub Enterprise Cloud-style URL', async () => {
|
|
process.env['GITHUB_SERVER_URL'] = 'https://contoso.ghe.com';
|
|
expect(cacheUtils.isGhes()).toBeFalsy();
|
|
});
|
|
|
|
it('returns false when the GITHUB_SERVER_URL environment variable has a .localhost suffix', async () => {
|
|
process.env['GITHUB_SERVER_URL'] = 'https://mock-github.localhost';
|
|
expect(cacheUtils.isGhes()).toBeFalsy();
|
|
});
|
|
|
|
it('returns true when the GITHUB_SERVER_URL environment variable is set to some other URL', async () => {
|
|
process.env['GITHUB_SERVER_URL'] = 'https://src.onpremise.fabrikam.com';
|
|
expect(cacheUtils.isGhes()).toBeTruthy();
|
|
});
|
|
});
|