Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
169 changes: 169 additions & 0 deletions packages/cache/__tests__/getCacheEntry.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
import * as core from '@actions/core'
import {getCacheEntry} from '../src/cache'
import * as cacheHttpClient from '../src/internal/cacheHttpClient'
import {CompressionMethod} from '../src/internal/constants'
import {ArtifactCacheEntry} from '../src/internal/contracts'

jest.mock('../src/internal/cacheHttpClient')
jest.mock('../src/internal/cacheUtils')

beforeAll(() => {
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(() => {})
})

test('getCacheEntry with no path should fail', async () => {
const paths: string[] = []
const key = 'node-test'
await expect(getCacheEntry(paths, key)).rejects.toThrowError(
`Path Validation Error: At least one directory or file path is required`
)
})

test('getCacheEntry with too many keys should fail', async () => {
const paths = ['node_modules']
const key = 'node-test'
const restoreKeys = [...Array(20).keys()].map(x => x.toString())
await expect(
getCacheEntry(paths, key, restoreKeys, CompressionMethod.Zstd)
).rejects.toThrowError(
`Key Validation Error: Keys are limited to a maximum of 10.`
)
})

test('getCacheEntry with large key should fail', async () => {
const paths = ['node_modules']
const key = 'foo'.repeat(512) // Over the 512 character limit
await expect(getCacheEntry(paths, key)).rejects.toThrowError(
`Key Validation Error: ${key} cannot be larger than 512 characters.`
)
})

test('getCacheEntry with invalid key should fail', async () => {
const paths = ['node_modules']
const key = 'comma,comma'
await expect(getCacheEntry(paths, key)).rejects.toThrowError(
`Key Validation Error: ${key} cannot contain commas.`
)
})

test('getCacheEntry with no cache found', async () => {
const paths = ['node_modules']
const key = 'node-test'

jest.spyOn(cacheHttpClient, 'getCacheEntry').mockImplementation(async () => {
return Promise.resolve(null)
})

const cacheEntry = await getCacheEntry(paths, key)

expect(cacheEntry).toBe(null)
})

test('getCacheEntry with server error should fail', async () => {
const paths = ['node_modules']
const key = 'node-test'

jest.spyOn(cacheHttpClient, 'getCacheEntry').mockImplementation(() => {
throw new Error('HTTP Error Occurred')
})

await expect(getCacheEntry(paths, key)).rejects.toThrowError(
'HTTP Error Occurred'
)
})

test('getCacheEntry with restore keys and no cache found', async () => {
const paths = ['node_modules']
const key = 'node-test'
const restoreKey = 'node-'

jest.spyOn(cacheHttpClient, 'getCacheEntry').mockImplementation(async () => {
return Promise.resolve(null)
})

const cacheEntry = await getCacheEntry(
paths,
key,
[restoreKey],
CompressionMethod.Zstd
)

expect(cacheEntry).toBe(null)
})

test('getCacheEntry with gzip compressed cache found', async () => {
const paths = ['node_modules']
const key = 'node-test'

const cacheEntry: ArtifactCacheEntry = {
cacheKey: key,
scope: 'refs/heads/main',
archiveLocation: 'www.actionscache.test/download'
}
const getCacheMock = jest.spyOn(cacheHttpClient, 'getCacheEntry')
getCacheMock.mockImplementation(async () => {
return Promise.resolve(cacheEntry)
})

const compression = CompressionMethod.Gzip

const result = await getCacheEntry(paths, key, undefined, compression)

expect(result).toBe(cacheEntry)
expect(getCacheMock).toHaveBeenCalledWith([key], paths, {
compressionMethod: compression
})
})

test('getCacheEntry with zstd compressed cache found', async () => {
const paths = ['node_modules']
const key = 'node-test'

const cacheEntry: ArtifactCacheEntry = {
cacheKey: key,
scope: 'refs/heads/main',
archiveLocation: 'www.actionscache.test/download'
}
const getCacheMock = jest.spyOn(cacheHttpClient, 'getCacheEntry')
getCacheMock.mockImplementation(async () => {
return Promise.resolve(cacheEntry)
})

const compression = CompressionMethod.Zstd

const result = await getCacheEntry(paths, key, undefined, compression)

expect(result).toBe(cacheEntry)
expect(getCacheMock).toHaveBeenCalledWith([key], paths, {
compressionMethod: compression
})
})

test('getCacheEntry with cache found for restore key', async () => {
const paths = ['node_modules']
const key = 'node-test'
const restoreKey = 'node-'

const cacheEntry: ArtifactCacheEntry = {
cacheKey: restoreKey,
scope: 'refs/heads/main',
archiveLocation: 'www.actionscache.test/download'
}
const getCacheMock = jest.spyOn(cacheHttpClient, 'getCacheEntry')
getCacheMock.mockImplementation(async () => {
return Promise.resolve(cacheEntry)
})

const compression = CompressionMethod.Zstd

const result = await getCacheEntry(paths, key, [restoreKey], compression)

expect(result).toBe(cacheEntry)
expect(getCacheMock).toHaveBeenCalledWith([key, restoreKey], paths, {
compressionMethod: compression
})
})
47 changes: 45 additions & 2 deletions packages/cache/src/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import * as core from '@actions/core'
import * as path from 'path'
import * as utils from './internal/cacheUtils'
import * as cacheHttpClient from './internal/cacheHttpClient'
import {CompressionMethod} from './internal/constants'
import {ArtifactCacheEntry} from './internal/contracts'
import {createTar, extractTar, listTar} from './internal/tar'
import {DownloadOptions, UploadOptions} from './options'

Expand Down Expand Up @@ -53,6 +55,44 @@ export function isFeatureAvailable(): boolean {
return !!process.env['ACTIONS_CACHE_URL']
}

/**
* Get the cache entry from keys
*
* @param paths a list of file paths to restore from the cache
* @param primaryKey an explicit key for restoring the cache
* @param restoreKeys an optional ordered list of keys to use for restoring the cache if no cache hit occurred for key
* @param compressionMethod cache compression method
* @returns string returns the cache entry for the cache hit, otherwise returns null
*/
export async function getCacheEntry(
paths: string[],
primaryKey: string,
restoreKeys?: string[],
compressionMethod?: CompressionMethod
): Promise<ArtifactCacheEntry | null> {
checkPaths(paths)

restoreKeys = restoreKeys || []
const keys = [primaryKey, ...restoreKeys]

core.debug('Resolved Keys:')
core.debug(JSON.stringify(keys))

if (keys.length > 10) {
throw new ValidationError(
`Key Validation Error: Keys are limited to a maximum of 10.`
)
}
for (const key of keys) {
checkKey(key)
}

// path are needed to compute version
return await cacheHttpClient.getCacheEntry(keys, paths, {
compressionMethod: compressionMethod ?? (await utils.getCompressionMethod())
})
}

/**
* Restores cache from keys
*
Expand Down Expand Up @@ -89,9 +129,12 @@ export async function restoreCache(
let archivePath = ''
try {
// path are needed to compute version
const cacheEntry = await cacheHttpClient.getCacheEntry(keys, paths, {
const cacheEntry = await getCacheEntry(
paths,
primaryKey,
restoreKeys,
compressionMethod
})
)

if (!cacheEntry?.archiveLocation) {
// Cache not found
Expand Down