2024-11-24 18:44:39 +00:00
|
|
|
import * as core from '@actions/core'
|
|
|
|
import * as path from 'path'
|
2024-11-28 11:53:34 +00:00
|
|
|
import {saveCache} from '../src/cache'
|
2024-11-24 18:44:39 +00:00
|
|
|
import * as cacheUtils from '../src/internal/cacheUtils'
|
2024-11-28 11:53:34 +00:00
|
|
|
import {CacheFilename, CompressionMethod} from '../src/internal/constants'
|
2024-11-24 18:44:39 +00:00
|
|
|
import * as config from '../src/internal/config'
|
|
|
|
import * as tar from '../src/internal/tar'
|
2024-11-28 11:53:34 +00:00
|
|
|
import {CacheServiceClientJSON} from '../src/generated/results/api/v1/cache.twirp'
|
2024-11-28 11:52:09 +00:00
|
|
|
import * as uploadCacheModule from '../src/internal/uploadUtils'
|
2024-11-28 11:53:34 +00:00
|
|
|
import {BlobUploadCommonResponse} from '@azure/storage-blob'
|
|
|
|
import {InvalidResponseError} from '../src/internal/shared/errors'
|
2024-11-24 18:44:39 +00:00
|
|
|
|
|
|
|
let logDebugMock: jest.SpyInstance
|
|
|
|
|
|
|
|
jest.mock('../src/internal/tar')
|
|
|
|
|
2024-11-27 01:45:46 +00:00
|
|
|
let uploadFileMock = jest.fn()
|
|
|
|
const blockBlobClientMock = jest.fn().mockImplementation(() => ({
|
|
|
|
uploadFile: uploadFileMock
|
|
|
|
}))
|
|
|
|
jest.mock('@azure/storage-blob', () => ({
|
|
|
|
BlobClient: jest.fn().mockImplementation(() => {
|
|
|
|
return {
|
|
|
|
getBlockBlobClient: blockBlobClientMock
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}))
|
|
|
|
|
2024-11-24 18:44:39 +00:00
|
|
|
beforeAll(() => {
|
|
|
|
process.env['ACTIONS_RUNTIME_TOKEN'] = 'token'
|
2024-11-28 11:53:34 +00:00
|
|
|
jest.spyOn(console, 'log').mockImplementation(() => {})
|
|
|
|
jest.spyOn(core, 'debug').mockImplementation(() => {})
|
|
|
|
jest.spyOn(core, 'info').mockImplementation(() => {})
|
|
|
|
jest.spyOn(core, 'warning').mockImplementation(() => {})
|
|
|
|
jest.spyOn(core, 'error').mockImplementation(() => {})
|
2024-11-24 18:44:39 +00:00
|
|
|
jest.spyOn(cacheUtils, 'resolvePaths').mockImplementation(async filePaths => {
|
|
|
|
return filePaths.map(x => path.resolve(x))
|
|
|
|
})
|
|
|
|
jest.spyOn(cacheUtils, 'createTempDirectory').mockImplementation(async () => {
|
|
|
|
return Promise.resolve('/foo/bar')
|
|
|
|
})
|
|
|
|
|
|
|
|
// Ensure that we're using v2 for these tests
|
|
|
|
jest.spyOn(config, 'getCacheServiceVersion').mockReturnValue('v2')
|
|
|
|
|
|
|
|
logDebugMock = jest.spyOn(core, 'debug')
|
|
|
|
})
|
|
|
|
|
|
|
|
afterEach(() => {
|
|
|
|
expect(logDebugMock).toHaveBeenCalledWith('Cache service version: v2')
|
|
|
|
jest.clearAllMocks()
|
|
|
|
})
|
|
|
|
|
|
|
|
test('save with missing input should fail', async () => {
|
|
|
|
const paths: string[] = []
|
2024-11-26 16:36:12 +00:00
|
|
|
const key = 'Linux-node-bb828da54c148048dd17899ba9fda624811cfb43'
|
2024-11-24 18:44:39 +00:00
|
|
|
|
2024-11-26 16:36:12 +00:00
|
|
|
await expect(saveCache(paths, key)).rejects.toThrowError(
|
2024-11-24 18:44:39 +00:00
|
|
|
`Path Validation Error: At least one directory or file path is required`
|
|
|
|
)
|
|
|
|
})
|
|
|
|
|
2024-11-26 00:39:01 +00:00
|
|
|
test('save with large cache outputs should fail using', async () => {
|
2024-11-26 16:36:12 +00:00
|
|
|
const paths = 'node_modules'
|
|
|
|
const key = 'Linux-node-bb828da54c148048dd17899ba9fda624811cfb43'
|
|
|
|
const cachePaths = [path.resolve(paths)]
|
2024-11-24 18:44:39 +00:00
|
|
|
|
|
|
|
const createTarMock = jest.spyOn(tar, 'createTar')
|
|
|
|
const logWarningMock = jest.spyOn(core, 'warning')
|
|
|
|
|
|
|
|
const cacheSize = 11 * 1024 * 1024 * 1024 //~11GB, over the 10GB limit
|
|
|
|
jest
|
|
|
|
.spyOn(cacheUtils, 'getArchiveFileSizeInBytes')
|
|
|
|
.mockReturnValueOnce(cacheSize)
|
|
|
|
const compression = CompressionMethod.Gzip
|
|
|
|
const getCompressionMock = jest
|
|
|
|
.spyOn(cacheUtils, 'getCompressionMethod')
|
|
|
|
.mockReturnValueOnce(Promise.resolve(compression))
|
|
|
|
|
2024-11-26 16:36:12 +00:00
|
|
|
const cacheId = await saveCache([paths], key)
|
2024-11-24 18:44:39 +00:00
|
|
|
expect(cacheId).toBe(-1)
|
|
|
|
expect(logWarningMock).toHaveBeenCalledWith(
|
|
|
|
'Failed to save: Cache size of ~11264 MB (11811160064 B) is over the 10GB limit, not saving cache.'
|
|
|
|
)
|
|
|
|
|
|
|
|
const archiveFolder = '/foo/bar'
|
|
|
|
|
|
|
|
expect(createTarMock).toHaveBeenCalledWith(
|
|
|
|
archiveFolder,
|
|
|
|
cachePaths,
|
|
|
|
compression
|
|
|
|
)
|
|
|
|
expect(getCompressionMock).toHaveBeenCalledTimes(1)
|
|
|
|
})
|
|
|
|
|
|
|
|
test('create cache entry failure', async () => {
|
|
|
|
const paths = ['node_modules']
|
2024-11-26 16:36:12 +00:00
|
|
|
const key = 'Linux-node-bb828da54c148048dd17899ba9fda624811cfb43'
|
2024-11-24 18:44:39 +00:00
|
|
|
const infoLogMock = jest.spyOn(core, 'info')
|
|
|
|
|
|
|
|
const createCacheEntryMock = jest
|
|
|
|
.spyOn(CacheServiceClientJSON.prototype, 'CreateCacheEntry')
|
2024-11-28 11:53:34 +00:00
|
|
|
.mockReturnValue(Promise.resolve({ok: false, signedUploadUrl: ''}))
|
2024-11-24 18:44:39 +00:00
|
|
|
|
|
|
|
const createTarMock = jest.spyOn(tar, 'createTar')
|
|
|
|
const finalizeCacheEntryMock = jest.spyOn(
|
|
|
|
CacheServiceClientJSON.prototype,
|
|
|
|
'FinalizeCacheEntryUpload'
|
|
|
|
)
|
|
|
|
const compression = CompressionMethod.Zstd
|
|
|
|
const getCompressionMock = jest
|
|
|
|
.spyOn(cacheUtils, 'getCompressionMethod')
|
|
|
|
.mockReturnValueOnce(Promise.resolve(compression))
|
2024-11-26 23:05:11 +00:00
|
|
|
const archiveFileSize = 1024
|
|
|
|
jest
|
|
|
|
.spyOn(cacheUtils, 'getArchiveFileSizeInBytes')
|
|
|
|
.mockReturnValueOnce(archiveFileSize)
|
2024-11-24 18:44:39 +00:00
|
|
|
const cacheVersion = cacheUtils.getCacheVersion(paths, compression)
|
2024-11-28 11:52:09 +00:00
|
|
|
const uploadCacheArchiveSDKMock = jest
|
|
|
|
.spyOn(uploadCacheModule, 'uploadCacheArchiveSDK')
|
2024-11-27 01:45:46 +00:00
|
|
|
.mockReturnValueOnce(
|
2024-11-24 18:44:39 +00:00
|
|
|
Promise.resolve({
|
|
|
|
_response: {
|
|
|
|
status: 200
|
|
|
|
}
|
|
|
|
} as BlobUploadCommonResponse)
|
|
|
|
)
|
|
|
|
|
2024-11-26 16:36:12 +00:00
|
|
|
const cacheId = await saveCache(paths, key)
|
2024-11-24 18:44:39 +00:00
|
|
|
expect(cacheId).toBe(-1)
|
|
|
|
expect(infoLogMock).toHaveBeenCalledWith(
|
2024-11-26 16:36:12 +00:00
|
|
|
`Failed to save: Unable to reserve cache with key ${key}, another job may be creating this cache.`
|
2024-11-24 18:44:39 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
expect(createCacheEntryMock).toHaveBeenCalledWith({
|
2024-11-26 16:36:12 +00:00
|
|
|
key,
|
2024-11-24 18:44:39 +00:00
|
|
|
version: cacheVersion
|
|
|
|
})
|
|
|
|
expect(createTarMock).toHaveBeenCalledTimes(1)
|
|
|
|
expect(getCompressionMock).toHaveBeenCalledTimes(1)
|
|
|
|
expect(finalizeCacheEntryMock).toHaveBeenCalledTimes(0)
|
2024-11-28 11:52:09 +00:00
|
|
|
expect(uploadCacheArchiveSDKMock).toHaveBeenCalledTimes(0)
|
2024-11-24 18:44:39 +00:00
|
|
|
})
|
|
|
|
|
|
|
|
test('finalize save cache failure', async () => {
|
2024-11-26 16:36:12 +00:00
|
|
|
const paths = 'node_modules'
|
|
|
|
const key = 'Linux-node-bb828da54c148048dd17899ba9fda624811cfb43'
|
|
|
|
const cachePaths = [path.resolve(paths)]
|
2024-11-24 18:44:39 +00:00
|
|
|
const logWarningMock = jest.spyOn(core, 'warning')
|
|
|
|
const signedUploadURL = 'https://blob-storage.local?signed=true'
|
|
|
|
|
|
|
|
const createCacheEntryMock = jest
|
|
|
|
.spyOn(CacheServiceClientJSON.prototype, 'CreateCacheEntry')
|
|
|
|
.mockReturnValue(
|
2024-11-28 11:53:34 +00:00
|
|
|
Promise.resolve({ok: true, signedUploadUrl: signedUploadURL})
|
2024-11-24 18:44:39 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
const createTarMock = jest.spyOn(tar, 'createTar')
|
|
|
|
|
2024-11-28 11:52:09 +00:00
|
|
|
const uploadCacheMock = jest.spyOn(uploadCacheModule, 'uploadCacheArchiveSDK')
|
2024-11-27 01:45:46 +00:00
|
|
|
uploadCacheMock.mockReturnValueOnce(
|
2024-11-24 18:44:39 +00:00
|
|
|
Promise.resolve({
|
|
|
|
_response: {
|
|
|
|
status: 200
|
|
|
|
}
|
|
|
|
} as BlobUploadCommonResponse)
|
|
|
|
)
|
|
|
|
|
|
|
|
const compression = CompressionMethod.Zstd
|
|
|
|
const getCompressionMock = jest
|
|
|
|
.spyOn(cacheUtils, 'getCompressionMethod')
|
|
|
|
.mockReturnValueOnce(Promise.resolve(compression))
|
|
|
|
|
2024-11-26 16:36:12 +00:00
|
|
|
const cacheVersion = cacheUtils.getCacheVersion([paths], compression)
|
2024-11-24 18:44:39 +00:00
|
|
|
const archiveFileSize = 1024
|
|
|
|
jest
|
|
|
|
.spyOn(cacheUtils, 'getArchiveFileSizeInBytes')
|
|
|
|
.mockReturnValueOnce(archiveFileSize)
|
|
|
|
|
|
|
|
const finalizeCacheEntryMock = jest
|
|
|
|
.spyOn(CacheServiceClientJSON.prototype, 'FinalizeCacheEntryUpload')
|
2024-11-28 11:53:34 +00:00
|
|
|
.mockReturnValue(Promise.resolve({ok: false, entryId: ''}))
|
2024-11-24 18:44:39 +00:00
|
|
|
|
2024-11-26 16:36:12 +00:00
|
|
|
const cacheId = await saveCache([paths], key)
|
2024-11-24 18:44:39 +00:00
|
|
|
|
|
|
|
expect(createCacheEntryMock).toHaveBeenCalledWith({
|
2024-11-26 16:36:12 +00:00
|
|
|
key,
|
2024-11-24 18:44:39 +00:00
|
|
|
version: cacheVersion
|
|
|
|
})
|
|
|
|
|
|
|
|
const archiveFolder = '/foo/bar'
|
|
|
|
const archiveFile = path.join(archiveFolder, CacheFilename.Zstd)
|
|
|
|
expect(createTarMock).toHaveBeenCalledWith(
|
|
|
|
archiveFolder,
|
|
|
|
cachePaths,
|
|
|
|
compression
|
|
|
|
)
|
|
|
|
|
|
|
|
expect(uploadCacheMock).toHaveBeenCalledWith(signedUploadURL, archiveFile)
|
|
|
|
expect(getCompressionMock).toHaveBeenCalledTimes(1)
|
|
|
|
|
|
|
|
expect(finalizeCacheEntryMock).toHaveBeenCalledWith({
|
2024-11-26 16:36:12 +00:00
|
|
|
key,
|
2024-11-24 18:44:39 +00:00
|
|
|
version: cacheVersion,
|
|
|
|
sizeBytes: archiveFileSize.toString()
|
|
|
|
})
|
|
|
|
|
|
|
|
expect(cacheId).toBe(-1)
|
|
|
|
expect(logWarningMock).toHaveBeenCalledWith(
|
2024-11-26 16:36:12 +00:00
|
|
|
`Failed to save: Unable to finalize cache with key ${key}, another job may be finalizing this cache.`
|
2024-11-24 18:44:39 +00:00
|
|
|
)
|
|
|
|
})
|
|
|
|
|
|
|
|
test('save with uploadCache Server error will fail', async () => {
|
2024-11-26 16:36:12 +00:00
|
|
|
const paths = 'node_modules'
|
|
|
|
const key = 'Linux-node-bb828da54c148048dd17899ba9fda624811cfb43'
|
|
|
|
const signedUploadURL = 'https://blob-storage.local?signed=true'
|
2024-11-24 18:44:39 +00:00
|
|
|
jest
|
|
|
|
.spyOn(CacheServiceClientJSON.prototype, 'CreateCacheEntry')
|
|
|
|
.mockReturnValue(
|
2024-11-28 11:53:34 +00:00
|
|
|
Promise.resolve({ok: true, signedUploadUrl: signedUploadURL})
|
2024-11-24 18:44:39 +00:00
|
|
|
)
|
|
|
|
|
2024-11-26 23:05:11 +00:00
|
|
|
const archiveFileSize = 1024
|
|
|
|
jest
|
|
|
|
.spyOn(cacheUtils, 'getArchiveFileSizeInBytes')
|
|
|
|
.mockReturnValueOnce(archiveFileSize)
|
2024-11-24 18:44:39 +00:00
|
|
|
jest
|
2024-11-28 11:52:09 +00:00
|
|
|
.spyOn(uploadCacheModule, 'uploadCacheArchiveSDK')
|
2024-11-26 23:05:11 +00:00
|
|
|
.mockRejectedValueOnce(new InvalidResponseError('boom'))
|
2024-11-24 18:44:39 +00:00
|
|
|
|
2024-11-26 16:36:12 +00:00
|
|
|
const cacheId = await saveCache([paths], key)
|
2024-11-24 18:44:39 +00:00
|
|
|
expect(cacheId).toBe(-1)
|
|
|
|
})
|
|
|
|
|
2024-11-27 01:45:46 +00:00
|
|
|
test('uploadFile returns 500', async () => {
|
|
|
|
const paths = 'node_modules'
|
|
|
|
const key = 'Linux-node-bb828da54c148048dd17899ba9fda624811cfb43'
|
|
|
|
const signedUploadURL = 'https://blob-storage.local?signed=true'
|
|
|
|
const logWarningMock = jest.spyOn(core, 'warning')
|
|
|
|
jest
|
|
|
|
.spyOn(CacheServiceClientJSON.prototype, 'CreateCacheEntry')
|
|
|
|
.mockReturnValue(
|
2024-11-28 11:53:34 +00:00
|
|
|
Promise.resolve({ok: true, signedUploadUrl: signedUploadURL})
|
2024-11-27 01:45:46 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
const archiveFileSize = 1024
|
|
|
|
jest
|
|
|
|
.spyOn(cacheUtils, 'getArchiveFileSizeInBytes')
|
|
|
|
.mockReturnValueOnce(archiveFileSize)
|
2024-11-28 11:52:09 +00:00
|
|
|
jest.spyOn(uploadCacheModule, 'uploadCacheArchiveSDK').mockRestore()
|
2024-11-27 01:45:46 +00:00
|
|
|
|
|
|
|
uploadFileMock = jest.fn().mockResolvedValueOnce({
|
|
|
|
_response: {
|
|
|
|
status: 500
|
|
|
|
}
|
|
|
|
})
|
|
|
|
const cacheId = await saveCache([paths], key)
|
|
|
|
|
|
|
|
expect(logWarningMock).toHaveBeenCalledWith(
|
|
|
|
'Failed to save: Upload failed with status code 500'
|
|
|
|
)
|
|
|
|
expect(cacheId).toBe(-1)
|
|
|
|
})
|
|
|
|
|
2024-11-24 18:44:39 +00:00
|
|
|
test('save with valid inputs uploads a cache', async () => {
|
2024-11-26 16:36:12 +00:00
|
|
|
const paths = 'node_modules'
|
|
|
|
const key = 'Linux-node-bb828da54c148048dd17899ba9fda624811cfb43'
|
|
|
|
const cachePaths = [path.resolve(paths)]
|
2024-11-24 18:44:39 +00:00
|
|
|
const signedUploadURL = 'https://blob-storage.local?signed=true'
|
|
|
|
const createTarMock = jest.spyOn(tar, 'createTar')
|
|
|
|
|
|
|
|
const archiveFileSize = 1024
|
|
|
|
jest
|
|
|
|
.spyOn(cacheUtils, 'getArchiveFileSizeInBytes')
|
|
|
|
.mockReturnValueOnce(archiveFileSize)
|
|
|
|
|
|
|
|
const cacheId = 4
|
|
|
|
jest
|
|
|
|
.spyOn(CacheServiceClientJSON.prototype, 'CreateCacheEntry')
|
|
|
|
.mockReturnValue(
|
2024-11-28 11:53:34 +00:00
|
|
|
Promise.resolve({ok: true, signedUploadUrl: signedUploadURL})
|
2024-11-24 18:44:39 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
const uploadCacheMock = jest
|
2024-11-28 11:52:09 +00:00
|
|
|
.spyOn(uploadCacheModule, 'uploadCacheArchiveSDK')
|
2024-11-27 01:45:46 +00:00
|
|
|
.mockReturnValueOnce(
|
2024-11-24 18:44:39 +00:00
|
|
|
Promise.resolve({
|
|
|
|
_response: {
|
|
|
|
status: 200
|
|
|
|
}
|
|
|
|
} as BlobUploadCommonResponse)
|
|
|
|
)
|
|
|
|
|
|
|
|
const compression = CompressionMethod.Zstd
|
|
|
|
const getCompressionMock = jest
|
|
|
|
.spyOn(cacheUtils, 'getCompressionMethod')
|
|
|
|
.mockReturnValue(Promise.resolve(compression))
|
2024-11-26 16:36:12 +00:00
|
|
|
const cacheVersion = cacheUtils.getCacheVersion([paths], compression)
|
2024-11-24 18:44:39 +00:00
|
|
|
|
|
|
|
const finalizeCacheEntryMock = jest
|
|
|
|
.spyOn(CacheServiceClientJSON.prototype, 'FinalizeCacheEntryUpload')
|
2024-11-28 11:53:34 +00:00
|
|
|
.mockReturnValue(Promise.resolve({ok: true, entryId: cacheId.toString()}))
|
2024-11-24 18:44:39 +00:00
|
|
|
|
2024-11-26 16:36:12 +00:00
|
|
|
const expectedCacheId = await saveCache([paths], key)
|
2024-11-24 18:44:39 +00:00
|
|
|
|
|
|
|
const archiveFolder = '/foo/bar'
|
|
|
|
const archiveFile = path.join(archiveFolder, CacheFilename.Zstd)
|
|
|
|
expect(uploadCacheMock).toHaveBeenCalledWith(signedUploadURL, archiveFile)
|
|
|
|
expect(createTarMock).toHaveBeenCalledWith(
|
|
|
|
archiveFolder,
|
|
|
|
cachePaths,
|
|
|
|
compression
|
|
|
|
)
|
|
|
|
|
|
|
|
expect(finalizeCacheEntryMock).toHaveBeenCalledWith({
|
2024-11-26 16:36:12 +00:00
|
|
|
key,
|
2024-11-24 18:44:39 +00:00
|
|
|
version: cacheVersion,
|
|
|
|
sizeBytes: archiveFileSize.toString()
|
|
|
|
})
|
|
|
|
|
|
|
|
expect(getCompressionMock).toHaveBeenCalledTimes(1)
|
|
|
|
expect(expectedCacheId).toBe(cacheId)
|
|
|
|
})
|
|
|
|
|
|
|
|
test('save with non existing path should not save cache using v2 saveCache', async () => {
|
|
|
|
const path = 'node_modules'
|
2024-11-26 16:36:12 +00:00
|
|
|
const key = 'Linux-node-bb828da54c148048dd17899ba9fda624811cfb43'
|
2024-11-24 18:44:39 +00:00
|
|
|
jest.spyOn(cacheUtils, 'resolvePaths').mockImplementation(async () => {
|
|
|
|
return []
|
|
|
|
})
|
2024-11-26 16:36:12 +00:00
|
|
|
await expect(saveCache([path], key)).rejects.toThrowError(
|
2024-11-24 18:44:39 +00:00
|
|
|
`Path Validation Error: Path(s) specified in the action for caching do(es) not exist, hence no cache is being saved.`
|
|
|
|
)
|
|
|
|
})
|