Merge remote-tracking branch 'upstream/master' into tool-cache-xar

# Conflicts:
#	packages/tool-cache/__tests__/tool-cache.test.ts
This commit is contained in:
Frederik Wallner
2020-05-03 19:37:24 +02:00
119 changed files with 23561 additions and 4209 deletions
Binary file not shown.
@@ -0,0 +1,125 @@
import * as core from '@actions/core'
import {RetryHelper} from '../src/retry-helper'
let info: string[]
let retryHelper: RetryHelper
describe('retry-helper tests', () => {
beforeAll(() => {
// Mock @actions/core info()
jest.spyOn(core, 'info').mockImplementation((message: string) => {
info.push(message)
})
retryHelper = new RetryHelper(3, 0, 0)
})
beforeEach(() => {
// Reset info
info = []
})
afterAll(() => {
// Restore
jest.restoreAllMocks()
})
it('first attempt succeeds', async () => {
const actual = await retryHelper.execute(async () => {
return 'some result'
})
expect(actual).toBe('some result')
expect(info).toHaveLength(0)
})
it('second attempt succeeds', async () => {
let attempts = 0
const actual = await retryHelper.execute(async () => {
if (++attempts === 1) {
throw new Error('some error')
}
return Promise.resolve('some result')
})
expect(attempts).toBe(2)
expect(actual).toBe('some result')
expect(info).toHaveLength(2)
expect(info[0]).toBe('some error')
expect(info[1]).toMatch(/Waiting .+ seconds before trying again/)
})
it('third attempt succeeds', async () => {
let attempts = 0
const actual = await retryHelper.execute(async () => {
if (++attempts < 3) {
throw new Error(`some error ${attempts}`)
}
return Promise.resolve('some result')
})
expect(attempts).toBe(3)
expect(actual).toBe('some result')
expect(info).toHaveLength(4)
expect(info[0]).toBe('some error 1')
expect(info[1]).toMatch(/Waiting .+ seconds before trying again/)
expect(info[2]).toBe('some error 2')
expect(info[3]).toMatch(/Waiting .+ seconds before trying again/)
})
it('all attempts fail', async () => {
let attempts = 0
let error: Error = (null as unknown) as Error
try {
await retryHelper.execute(() => {
throw new Error(`some error ${++attempts}`)
})
} catch (err) {
error = err
}
expect(error.message).toBe('some error 3')
expect(attempts).toBe(3)
expect(info).toHaveLength(4)
expect(info[0]).toBe('some error 1')
expect(info[1]).toMatch(/Waiting .+ seconds before trying again/)
expect(info[2]).toBe('some error 2')
expect(info[3]).toMatch(/Waiting .+ seconds before trying again/)
})
it('checks retryable after first attempt', async () => {
let attempts = 0
let error: Error = (null as unknown) as Error
try {
await retryHelper.execute(
async () => {
throw new Error(`some error ${++attempts}`)
},
() => false
)
} catch (err) {
error = err
}
expect(error.message).toBe('some error 1')
expect(attempts).toBe(1)
expect(info).toHaveLength(0)
})
it('checks retryable after second attempt', async () => {
let attempts = 0
let error: Error = (null as unknown) as Error
try {
await retryHelper.execute(
async () => {
throw new Error(`some error ${++attempts}`)
},
(e: Error) => e.message === 'some error 1'
)
} catch (err) {
error = err
}
expect(error.message).toBe('some error 2')
expect(attempts).toBe(2)
expect(info).toHaveLength(2)
expect(info[0]).toBe('some error 1')
expect(info[1]).toMatch(/Waiting .+ seconds before trying again/)
})
})
+354 -71
View File
@@ -1,8 +1,9 @@
import * as fs from 'fs'
import * as nock from 'nock'
import * as path from 'path'
import * as io from '@actions/io'
import * as exec from '@actions/exec'
import * as stream from 'stream'
import nock from 'nock'
const cachePath = path.join(__dirname, 'CACHE')
const tempPath = path.join(__dirname, 'TEMP')
@@ -25,6 +26,8 @@ describe('@actions/tool-cache', function() {
username: 'abc',
password: 'def'
})
setGlobal('TEST_DOWNLOAD_TOOL_RETRY_MIN_SECONDS', 0)
setGlobal('TEST_DOWNLOAD_TOOL_RETRY_MAX_SECONDS', 0)
})
beforeEach(async function() {
@@ -34,9 +37,15 @@ describe('@actions/tool-cache', function() {
await io.mkdirP(tempPath)
})
afterEach(function() {
setResponseMessageFactory(undefined)
})
afterAll(async function() {
await io.rmRF(tempPath)
await io.rmRF(cachePath)
setGlobal('TEST_DOWNLOAD_TOOL_RETRY_MIN_SECONDS', undefined)
setGlobal('TEST_DOWNLOAD_TOOL_RETRY_MAX_SECONDS', undefined)
})
it('downloads a 35 byte file', async () => {
@@ -48,8 +57,50 @@ describe('@actions/tool-cache', function() {
expect(fs.statSync(downPath).size).toBe(35)
})
it('downloads a 35 byte file (dest)', async () => {
const dest = 'test-download-file'
try {
const downPath: string = await tc.downloadTool(
'http://example.com/bytes/35',
dest
)
expect(downPath).toEqual(dest)
expect(fs.existsSync(downPath)).toBeTruthy()
expect(fs.statSync(downPath).size).toBe(35)
} finally {
try {
await fs.promises.unlink(dest)
} catch {
// intentionally empty
}
}
})
it('downloads a 35 byte file (dest requires mkdirp)', async () => {
const dest = 'test-download-dir/test-download-file'
try {
const downPath: string = await tc.downloadTool(
'http://example.com/bytes/35',
dest
)
expect(downPath).toEqual(dest)
expect(fs.existsSync(downPath)).toBeTruthy()
expect(fs.statSync(downPath).size).toBe(35)
} finally {
try {
await fs.promises.unlink(dest)
await fs.promises.rmdir(path.dirname(dest))
} catch {
// intentionally empty
}
}
})
it('downloads a 35 byte file after a redirect', async () => {
nock('http://example.com')
.persist()
.get('/redirect-to')
.reply(303, undefined, {
location: 'http://example.com/bytes/35'
@@ -63,8 +114,75 @@ describe('@actions/tool-cache', function() {
expect(fs.statSync(downPath).size).toBe(35)
})
it('handles error from response message stream', async () => {
nock('http://example.com')
.persist()
.get('/error-from-response-message-stream')
.reply(200, {})
setResponseMessageFactory(() => {
const readStream = new stream.Readable()
/* eslint-disable @typescript-eslint/unbound-method */
readStream._read = () => {
readStream.destroy(new Error('uh oh'))
}
/* eslint-enable @typescript-eslint/unbound-method */
return readStream
})
let error = new Error('unexpected')
try {
await tc.downloadTool(
'http://example.com/error-from-response-message-stream'
)
} catch (err) {
error = err
}
expect(error).not.toBeUndefined()
expect(error.message).toBe('uh oh')
})
it('retries error from response message stream', async () => {
nock('http://example.com')
.persist()
.get('/retries-error-from-response-message-stream')
.reply(200, {})
/* eslint-disable @typescript-eslint/unbound-method */
let attempt = 1
setResponseMessageFactory(() => {
const readStream = new stream.Readable()
let failsafe = 0
readStream._read = () => {
// First attempt fails
if (attempt++ === 1) {
readStream.destroy(new Error('uh oh'))
return
}
// Write some data
if (failsafe++ === 0) {
readStream.push(''.padEnd(35))
readStream.push(null) // no more data
}
}
return readStream
})
/* eslint-enable @typescript-eslint/unbound-method */
const downPath = await tc.downloadTool(
'http://example.com/retries-error-from-response-message-stream'
)
expect(fs.existsSync(downPath)).toBeTruthy()
expect(fs.statSync(downPath).size).toBe(35)
})
it('has status code in exception dictionary for HTTP error code responses', async () => {
nock('http://example.com')
.persist()
.get('/bytes/bad')
.reply(400, {
username: 'bad',
@@ -84,6 +202,7 @@ describe('@actions/tool-cache', function() {
it('works with redirect code 302', async function() {
nock('http://example.com')
.persist()
.get('/redirect-to')
.reply(302, undefined, {
location: 'http://example.com/bytes/35'
@@ -143,6 +262,36 @@ describe('@actions/tool-cache', function() {
}
})
it('extracts a 7z to a directory that does not exist', async () => {
const tempDir = path.join(__dirname, 'test-install-7z')
const destDir = path.join(tempDir, 'not-exist')
try {
await io.mkdirP(tempDir)
// copy the 7z file to the test dir
const _7zFile: string = path.join(tempDir, 'test.7z')
await io.cp(path.join(__dirname, 'data', 'test.7z'), _7zFile)
// extract/cache
const extPath: string = await tc.extract7z(_7zFile, destDir)
await tc.cacheDir(extPath, 'my-7z-contents', '1.1.0')
const toolPath: string = tc.find('my-7z-contents', '1.1.0')
expect(extPath).toContain('not-exist')
expect(fs.existsSync(toolPath)).toBeTruthy()
expect(fs.existsSync(`${toolPath}.complete`)).toBeTruthy()
expect(fs.existsSync(path.join(toolPath, 'file.txt'))).toBeTruthy()
expect(
fs.existsSync(path.join(toolPath, 'file-with-ç-character.txt'))
).toBeTruthy()
expect(
fs.existsSync(path.join(toolPath, 'folder', 'nested-file.txt'))
).toBeTruthy()
} finally {
await io.rmRF(tempDir)
}
})
it('extract 7z using custom 7z tool', async function() {
const tempDir = path.join(
__dirname,
@@ -186,7 +335,7 @@ describe('@actions/tool-cache', function() {
.readFileSync(path.join(tempDir, 'mock7zr-args.txt'))
.toString()
.trim()
).toBe(`x -bb1 -bd -sccUTF-8 ${_7zFile}`)
).toBe(`x -bb0 -bd -sccUTF-8 ${_7zFile}`)
expect(fs.existsSync(path.join(extPath, 'file.txt'))).toBeTruthy()
expect(
fs.existsSync(path.join(extPath, 'file-with-ç-character.txt'))
@@ -198,53 +347,20 @@ describe('@actions/tool-cache', function() {
await io.rmRF(tempDir)
}
})
} else {
if (IS_MAC) {
it('extract .xar', async () => {
const tempDir = path.join(tempPath, 'test-install.xar')
await io.mkdirP(tempDir)
// copy the .xar file to the test dir
const _xarFile: string = path.join(tempDir, 'test.xar')
await io.cp(path.join(__dirname, 'data', 'test.xar'), _xarFile)
// extract/cache
const extPath: string = await tc.extractXar(_xarFile, undefined, '-x')
await tc.cacheDir(extPath, 'my-xar-contents', '1.1.0')
const toolPath: string = tc.find('my-xar-contents', '1.1.0')
expect(fs.existsSync(toolPath)).toBeTruthy()
expect(fs.existsSync(`${toolPath}.complete`)).toBeTruthy()
expect(fs.existsSync(path.join(toolPath, 'file.txt'))).toBeTruthy()
expect(
fs.existsSync(path.join(toolPath, 'file-with-ç-character.txt'))
).toBeTruthy()
expect(
fs.existsSync(path.join(toolPath, 'folder', 'nested-file.txt'))
).toBeTruthy()
expect(
fs.readFileSync(
path.join(toolPath, 'folder', 'nested-file.txt'),
'utf8'
)
).toBe('folder/nested-file.txt contents')
})
}
it('extract .tar.gz', async () => {
const tempDir = path.join(tempPath, 'test-install-tar.gz')
} else if (IS_MAC) {
it('extract .xar', async () => {
const tempDir = path.join(tempPath, 'test-install.xar')
await io.mkdirP(tempDir)
// copy the .tar.gz file to the test dir
const _tgzFile: string = path.join(tempDir, 'test.tar.gz')
await io.cp(path.join(__dirname, 'data', 'test.tar.gz'), _tgzFile)
// copy the .xar file to the test dir
const _xarFile: string = path.join(tempDir, 'test.xar')
await io.cp(path.join(__dirname, 'data', 'test.xar'), _xarFile)
// extract/cache
const extPath: string = await tc.extractTar(_tgzFile)
await tc.cacheDir(extPath, 'my-tgz-contents', '1.1.0')
const toolPath: string = tc.find('my-tgz-contents', '1.1.0')
const extPath: string = await tc.extractXar(_xarFile, undefined, '-x')
await tc.cacheDir(extPath, 'my-xar-contents', '1.1.0')
const toolPath: string = tc.find('my-xar-contents', '1.1.0')
expect(fs.existsSync(toolPath)).toBeTruthy()
expect(fs.existsSync(`${toolPath}.complete`)).toBeTruthy()
@@ -262,33 +378,89 @@ describe('@actions/tool-cache', function() {
)
).toBe('folder/nested-file.txt contents')
})
it('extract .tar.xz', async () => {
const tempDir = path.join(tempPath, 'test-install-tar.xz')
await io.mkdirP(tempDir)
// copy the .tar.gz file to the test dir
const _txzFile: string = path.join(tempDir, 'test.tar.xz')
await io.cp(path.join(__dirname, 'data', 'test.tar.xz'), _txzFile)
// extract/cache
const extPath: string = await tc.extractTar(_txzFile, undefined, 'x')
await tc.cacheDir(extPath, 'my-txz-contents', '1.1.0')
const toolPath: string = tc.find('my-txz-contents', '1.1.0')
expect(fs.existsSync(toolPath)).toBeTruthy()
expect(fs.existsSync(`${toolPath}.complete`)).toBeTruthy()
expect(fs.existsSync(path.join(toolPath, 'bar.txt'))).toBeTruthy()
expect(
fs.existsSync(path.join(toolPath, 'foo', 'hello.txt'))
).toBeTruthy()
expect(
fs.readFileSync(path.join(toolPath, 'foo', 'hello.txt'), 'utf8')
).toBe('foo/hello: world')
})
}
it('extract .tar.gz', async () => {
const tempDir = path.join(tempPath, 'test-install-tar.gz')
await io.mkdirP(tempDir)
// copy the .tar.gz file to the test dir
const _tgzFile: string = path.join(tempDir, 'test.tar.gz')
await io.cp(path.join(__dirname, 'data', 'test.tar.gz'), _tgzFile)
// extract/cache
const extPath: string = await tc.extractTar(_tgzFile)
await tc.cacheDir(extPath, 'my-tgz-contents', '1.1.0')
const toolPath: string = tc.find('my-tgz-contents', '1.1.0')
expect(fs.existsSync(toolPath)).toBeTruthy()
expect(fs.existsSync(`${toolPath}.complete`)).toBeTruthy()
expect(fs.existsSync(path.join(toolPath, 'file.txt'))).toBeTruthy()
expect(
fs.existsSync(path.join(toolPath, 'file-with-ç-character.txt'))
).toBeTruthy()
expect(
fs.existsSync(path.join(toolPath, 'folder', 'nested-file.txt'))
).toBeTruthy()
expect(
fs.readFileSync(path.join(toolPath, 'folder', 'nested-file.txt'), 'utf8')
).toBe('folder/nested-file.txt contents')
})
it('extract .tar.gz to a directory that does not exist', async () => {
const tempDir = path.join(tempPath, 'test-install-tar.gz')
const destDir = path.join(tempDir, 'not-exist')
await io.mkdirP(tempDir)
// copy the .tar.gz file to the test dir
const _tgzFile: string = path.join(tempDir, 'test.tar.gz')
await io.cp(path.join(__dirname, 'data', 'test.tar.gz'), _tgzFile)
// extract/cache
const extPath: string = await tc.extractTar(_tgzFile, destDir)
await tc.cacheDir(extPath, 'my-tgz-contents', '1.1.0')
const toolPath: string = tc.find('my-tgz-contents', '1.1.0')
expect(extPath).toContain('not-exist')
expect(fs.existsSync(toolPath)).toBeTruthy()
expect(fs.existsSync(`${toolPath}.complete`)).toBeTruthy()
expect(fs.existsSync(path.join(toolPath, 'file.txt'))).toBeTruthy()
expect(
fs.existsSync(path.join(toolPath, 'file-with-ç-character.txt'))
).toBeTruthy()
expect(
fs.existsSync(path.join(toolPath, 'folder', 'nested-file.txt'))
).toBeTruthy()
expect(
fs.readFileSync(path.join(toolPath, 'folder', 'nested-file.txt'), 'utf8')
).toBe('folder/nested-file.txt contents')
})
it('extract .tar.xz', async () => {
const tempDir = path.join(tempPath, 'test-install-tar.xz')
await io.mkdirP(tempDir)
// copy the .tar.gz file to the test dir
const _txzFile: string = path.join(tempDir, 'test.tar.xz')
await io.cp(path.join(__dirname, 'data', 'test.tar.xz'), _txzFile)
// extract/cache
const extPath: string = await tc.extractTar(_txzFile, undefined, 'x')
await tc.cacheDir(extPath, 'my-txz-contents', '1.1.0')
const toolPath: string = tc.find('my-txz-contents', '1.1.0')
expect(fs.existsSync(toolPath)).toBeTruthy()
expect(fs.existsSync(`${toolPath}.complete`)).toBeTruthy()
expect(fs.existsSync(path.join(toolPath, 'bar.txt'))).toBeTruthy()
expect(fs.existsSync(path.join(toolPath, 'foo', 'hello.txt'))).toBeTruthy()
expect(
fs.readFileSync(path.join(toolPath, 'foo', 'hello.txt'), 'utf8')
).toBe('foo/hello: world')
})
it('installs a zip and finds it', async () => {
const tempDir = path.join(__dirname, 'test-install-zip')
try {
@@ -321,7 +493,7 @@ describe('@actions/tool-cache', function() {
]
await exec.exec(`"${powershellPath}"`, args)
} else {
const zipPath: string = await io.which('zip')
const zipPath: string = await io.which('zip', true)
await exec.exec(`"${zipPath}`, [zipFile, '-r', '.'], {cwd: stagingDir})
}
@@ -372,7 +544,7 @@ describe('@actions/tool-cache', function() {
]
await exec.exec(`"${powershellPath}"`, args)
} else {
const zipPath: string = await io.which('zip')
const zipPath: string = await io.which('zip', true)
await exec.exec(zipPath, [zipFile, '-r', '.'], {cwd: stagingDir})
}
@@ -397,6 +569,60 @@ describe('@actions/tool-cache', function() {
}
})
it('extract zip to a directory that does not exist', async function() {
const tempDir = path.join(__dirname, 'test-install-zip')
try {
await io.mkdirP(tempDir)
// stage the layout for a zip file:
// file.txt
// folder/nested-file.txt
const stagingDir = path.join(tempDir, 'zip-staging')
await io.mkdirP(path.join(stagingDir, 'folder'))
fs.writeFileSync(path.join(stagingDir, 'file.txt'), '')
fs.writeFileSync(path.join(stagingDir, 'folder', 'nested-file.txt'), '')
// create the zip
const zipFile = path.join(tempDir, 'test.zip')
await io.rmRF(zipFile)
if (IS_WINDOWS) {
const escapedStagingPath = stagingDir.replace(/'/g, "''") // double-up single quotes
const escapedZipFile = zipFile.replace(/'/g, "''")
const powershellPath = await io.which('powershell', true)
const args = [
'-NoLogo',
'-Sta',
'-NoProfile',
'-NonInteractive',
'-ExecutionPolicy',
'Unrestricted',
'-Command',
`$ErrorActionPreference = 'Stop' ; Add-Type -AssemblyName System.IO.Compression.FileSystem ; [System.IO.Compression.ZipFile]::CreateFromDirectory('${escapedStagingPath}', '${escapedZipFile}')`
]
await exec.exec(`"${powershellPath}"`, args)
} else {
const zipPath: string = await io.which('zip', true)
await exec.exec(zipPath, [zipFile, '-r', '.'], {cwd: stagingDir})
}
const destDir = path.join(tempDir, 'not-exist')
const extPath: string = await tc.extractZip(zipFile, destDir)
await tc.cacheDir(extPath, 'foo', '1.1.0')
const toolPath: string = tc.find('foo', '1.1.0')
expect(extPath).toContain('not-exist')
expect(fs.existsSync(toolPath)).toBeTruthy()
expect(fs.existsSync(`${toolPath}.complete`)).toBeTruthy()
expect(fs.existsSync(path.join(toolPath, 'file.txt'))).toBeTruthy()
expect(
fs.existsSync(path.join(toolPath, 'folder', 'nested-file.txt'))
).toBeTruthy()
} finally {
await io.rmRF(tempDir)
}
})
it('works with a 502 temporary failure', async function() {
nock('http://example.com')
.get('/temp502')
@@ -425,4 +651,61 @@ describe('@actions/tool-cache', function() {
expect(err.toString()).toContain('502')
}
})
it('retries 429s', async function() {
nock('http://example.com')
.get('/too-many-requests-429')
.times(2)
.reply(429, undefined)
nock('http://example.com')
.get('/too-many-requests-429')
.reply(500, undefined)
try {
const statusCodeUrl = 'http://example.com/too-many-requests-429'
await tc.downloadTool(statusCodeUrl)
} catch (err) {
expect(err.toString()).toContain('500')
}
})
it("doesn't retry 404", async function() {
nock('http://example.com')
.get('/not-found-404')
.reply(404, undefined)
nock('http://example.com')
.get('/not-found-404')
.reply(500, undefined)
try {
const statusCodeUrl = 'http://example.com/not-found-404'
await tc.downloadTool(statusCodeUrl)
} catch (err) {
expect(err.toString()).toContain('404')
}
})
})
/**
* Sets up a mock response body for downloadTool. This function works around a limitation with
* nock when the response stream emits an error.
*/
function setResponseMessageFactory(
factory: (() => stream.Readable) | undefined
): void {
setGlobal('TEST_DOWNLOAD_TOOL_RESPONSE_MESSAGE_FACTORY', factory)
}
/**
* Sets a global variable
*/
function setGlobal<T>(key: string, value: T | undefined): void {
/* eslint-disable @typescript-eslint/no-explicit-any */
const g = global as any
/* eslint-enable @typescript-eslint/no-explicit-any */
if (value === undefined) {
delete g[key]
} else {
g[key] = value
}
}