1
0
Fork 0
toolkit/packages/exec/__tests__/exec.test.ts

1221 lines
36 KiB
TypeScript

import * as exec from '../src/exec'
import * as im from '../src/interfaces'
import * as childProcess from 'child_process'
import * as fs from 'fs'
import * as os from 'os'
import * as path from 'path'
import * as stream from 'stream'
import * as io from '@actions/io'
/* eslint-disable @typescript-eslint/unbound-method */
const IS_WINDOWS = process.platform === 'win32'
let outstream: stream.Writable
let errstream: stream.Writable
describe('@actions/exec', () => {
beforeAll(() => {
io.mkdirP(getTestTemp())
outstream = fs.createWriteStream(path.join(getTestTemp(), 'my.log'))
errstream = fs.createWriteStream(path.join(getTestTemp(), 'myerr.log'))
})
beforeEach(() => {
outstream.write = jest.fn()
errstream.write = jest.fn()
})
it('Runs exec successfully with arguments split out', async () => {
const _testExecOptions = getExecOptions()
let exitCode = 1
let toolpath = ''
if (IS_WINDOWS) {
toolpath = await io.which('cmd', true)
exitCode = await exec.exec(
`"${toolpath}"`,
['/c', 'echo', 'hello'],
_testExecOptions
)
} else {
toolpath = await io.which('ls', true)
exitCode = await exec.exec(
`"${toolpath}"`,
['-l', '-a'],
_testExecOptions
)
}
expect(exitCode).toBe(0)
if (IS_WINDOWS) {
expect(outstream.write).toBeCalledWith(
`[command]${toolpath} /c echo hello${os.EOL}`
)
expect(outstream.write).toBeCalledWith(Buffer.from(`hello${os.EOL}`))
} else {
expect(outstream.write).toBeCalledWith(
`[command]${toolpath} -l -a${os.EOL}`
)
}
})
it('Runs exec successfully with arguments partially split out', async () => {
const _testExecOptions = getExecOptions()
let exitCode = 1
let toolpath = ''
if (IS_WINDOWS) {
toolpath = await io.which('cmd', true)
exitCode = await exec.exec(
`"${toolpath}" /c`,
['echo', 'hello'],
_testExecOptions
)
} else {
toolpath = await io.which('ls', true)
exitCode = await exec.exec(`"${toolpath}" -l`, ['-a'], _testExecOptions)
}
expect(exitCode).toBe(0)
if (IS_WINDOWS) {
expect(outstream.write).toBeCalledWith(
`[command]${toolpath} /c echo hello${os.EOL}`
)
expect(outstream.write).toBeCalledWith(Buffer.from(`hello${os.EOL}`))
} else {
expect(outstream.write).toBeCalledWith(
`[command]${toolpath} -l -a${os.EOL}`
)
}
})
it('Runs exec successfully with arguments as part of command line', async () => {
const _testExecOptions = getExecOptions()
let exitCode = 1
let toolpath = ''
if (IS_WINDOWS) {
toolpath = await io.which('cmd', true)
exitCode = await exec.exec(
`"${toolpath}" /c echo hello`,
[],
_testExecOptions
)
} else {
toolpath = await io.which('ls', true)
exitCode = await exec.exec(`"${toolpath}" -l -a`, [], _testExecOptions)
}
expect(exitCode).toBe(0)
if (IS_WINDOWS) {
expect(outstream.write).toBeCalledWith(
`[command]${toolpath} /c echo hello${os.EOL}`
)
expect(outstream.write).toBeCalledWith(Buffer.from(`hello${os.EOL}`))
} else {
expect(outstream.write).toBeCalledWith(
`[command]${toolpath} -l -a${os.EOL}`
)
}
})
it('Runs exec successfully with command from PATH', async () => {
const execOptions = getExecOptions()
const outStream = new StringStream()
execOptions.outStream = outStream
let output = ''
execOptions.listeners = {
stdout: (data: Buffer) => {
output += data.toString()
}
}
let exitCode = 1
let tool: string
let args: string[]
if (IS_WINDOWS) {
tool = 'cmd'
args = ['/c', 'echo', 'hello']
} else {
tool = 'sh'
args = ['-c', 'echo hello']
}
exitCode = await exec.exec(tool, args, execOptions)
expect(exitCode).toBe(0)
const rootedTool = await io.which(tool, true)
expect(outStream.getContents().split(os.EOL)[0]).toBe(
`[command]${rootedTool} ${args.join(' ')}`
)
expect(output.trim()).toBe(`hello`)
})
it('Exec fails with error on bad call', async () => {
const _testExecOptions = getExecOptions()
let toolpath = ''
let args: string[] = []
if (IS_WINDOWS) {
toolpath = await io.which('cmd', true)
args = ['/c', 'non-existent']
} else {
toolpath = await io.which('ls', true)
args = ['-l', 'non-existent']
}
let failed = false
await exec.exec(`"${toolpath}"`, args, _testExecOptions).catch(err => {
failed = true
expect(err.message).toContain(
`The process '${toolpath}' failed with exit code `
)
})
expect(failed).toBe(true)
if (IS_WINDOWS) {
expect(outstream.write).toBeCalledWith(
`[command]${toolpath} /c non-existent${os.EOL}`
)
} else {
expect(outstream.write).toBeCalledWith(
`[command]${toolpath} -l non-existent${os.EOL}`
)
}
})
it('Succeeds on stderr by default', async () => {
const scriptPath: string = path.join(
__dirname,
'scripts',
'stderroutput.js'
)
const nodePath: string = await io.which('node', true)
const _testExecOptions = getExecOptions()
const exitCode = await exec.exec(
`"${nodePath}"`,
[scriptPath],
_testExecOptions
)
expect(exitCode).toBe(0)
expect(outstream.write).toBeCalledWith(
Buffer.from('this is output to stderr')
)
})
it('Fails on stderr if specified', async () => {
const scriptPath: string = path.join(
__dirname,
'scripts',
'stderroutput.js'
)
const nodePath: string = await io.which('node', true)
const _testExecOptions = getExecOptions()
_testExecOptions.failOnStdErr = true
let failed = false
await exec
.exec(`"${nodePath}"`, [scriptPath], _testExecOptions)
.catch(() => {
failed = true
})
expect(failed).toBe(true)
expect(errstream.write).toBeCalledWith(
Buffer.from('this is output to stderr')
)
})
it('Fails when process fails to launch', async () => {
const nodePath: string = await io.which('node', true)
const _testExecOptions = getExecOptions()
_testExecOptions.cwd = path.join(__dirname, 'nosuchdir')
let failed = false
await exec.exec(`"${nodePath}"`, [], _testExecOptions).catch(() => {
failed = true
})
expect(failed).toBe(true)
})
it('Handles output callbacks', async () => {
const stdErrPath: string = path.join(
__dirname,
'scripts',
'stderroutput.js'
)
const stdOutPath: string = path.join(
__dirname,
'scripts',
'stdoutoutput.js'
)
const nodePath: string = await io.which('node', true)
let stdoutCalled = false
let stderrCalled = false
const _testExecOptions = getExecOptions()
_testExecOptions.listeners = {
stdout: (data: Buffer) => {
expect(data).toEqual(Buffer.from('this is output to stdout'))
stdoutCalled = true
},
stderr: (data: Buffer) => {
expect(data).toEqual(Buffer.from('this is output to stderr'))
stderrCalled = true
}
}
let exitCode = await exec.exec(
`"${nodePath}"`,
[stdOutPath],
_testExecOptions
)
expect(exitCode).toBe(0)
exitCode = await exec.exec(`"${nodePath}"`, [stdErrPath], _testExecOptions)
expect(exitCode).toBe(0)
expect(stdoutCalled).toBeTruthy()
expect(stderrCalled).toBeTruthy()
})
it('Handles large stdline', async () => {
const stdlinePath: string = path.join(
__dirname,
'scripts',
'stdlineoutput.js'
)
const nodePath: string = await io.which('node', true)
const _testExecOptions = getExecOptions()
let largeLine = ''
_testExecOptions.listeners = {
stdline: (line: string) => {
largeLine = line
}
}
const exitCode = await exec.exec(
`"${nodePath}"`,
[stdlinePath],
_testExecOptions
)
expect(exitCode).toBe(0)
expect(Buffer.byteLength(largeLine)).toEqual(2 ** 16 + 1)
})
it('Handles stdin shell', async () => {
let command: string
if (IS_WINDOWS) {
command = 'wait-for-input.cmd'
} else {
command = 'wait-for-input.sh'
}
const waitForInput: string = path.join(__dirname, 'scripts', command)
const _testExecOptions = getExecOptions()
_testExecOptions.listeners = {
stdout: (data: Buffer) => {
expect(data).toEqual(Buffer.from(`this is my input${os.EOL}`))
}
}
_testExecOptions.input = Buffer.from('this is my input')
const exitCode = await exec.exec(`"${waitForInput}"`, [], _testExecOptions)
expect(exitCode).toBe(0)
})
it('Handles stdin js', async () => {
const waitForInput: string = path.join(
__dirname,
'scripts',
'wait-for-input.js'
)
const _testExecOptions = getExecOptions()
_testExecOptions.listeners = {
stdout: (data: Buffer) => {
expect(data).toEqual(Buffer.from(`this is my input`))
}
}
_testExecOptions.input = Buffer.from('this is my input')
const nodePath = await io.which('node', true)
const exitCode = await exec.exec(nodePath, [waitForInput], _testExecOptions)
expect(exitCode).toBe(0)
})
it('Handles child process holding streams open', async function () {
const semaphorePath = path.join(
getTestTemp(),
'child-process-semaphore.txt'
)
fs.writeFileSync(semaphorePath, '')
const nodePath = await io.which('node', true)
const scriptPath = path.join(__dirname, 'scripts', 'wait-for-file.js')
const debugList: string[] = []
const _testExecOptions = getExecOptions()
_testExecOptions.delay = 500
_testExecOptions.windowsVerbatimArguments = true
_testExecOptions.listeners = {
debug: (data: string) => {
debugList.push(data)
}
}
let exitCode: number
if (IS_WINDOWS) {
const toolName: string = await io.which('cmd.exe', true)
const args = [
'/D', // Disable execution of AutoRun commands from registry.
'/E:ON', // Enable command extensions. Note, command extensions are enabled by default, unless disabled via registry.
'/V:OFF', // Disable delayed environment expansion. Note, delayed environment expansion is disabled by default, unless enabled via registry.
'/S', // Will cause first and last quote after /C to be stripped.
'/C',
`"start "" /B "${nodePath}" "${scriptPath}" "file=${semaphorePath}""`
]
exitCode = await exec.exec(`"${toolName}"`, args, _testExecOptions)
} else {
const toolName: string = await io.which('bash', true)
const args = ['-c', `node '${scriptPath}' 'file=${semaphorePath}' &`]
exitCode = await exec.exec(`"${toolName}"`, args, _testExecOptions)
/* eslint-disable-next-line no-console */
console.log(toolName)
/* eslint-disable-next-line no-console */
console.log(exitCode)
/* eslint-disable-next-line no-console */
console.log(debugList)
}
expect(exitCode).toBe(0)
expect(
debugList.filter(x => x.includes('STDIO streams did not close')).length
).toBe(1)
fs.unlinkSync(semaphorePath)
}, 10000) // this was timing out on some slower hosted macOS runs at default 5s
it('Handles child process holding streams open and non-zero exit code', async function () {
const semaphorePath = path.join(
getTestTemp(),
'child-process-semaphore.txt'
)
fs.writeFileSync(semaphorePath, '')
const nodePath = await io.which('node', true)
const scriptPath = path.join(__dirname, 'scripts', 'wait-for-file.js')
const debugList: string[] = []
const _testExecOptions = getExecOptions()
_testExecOptions.delay = 500
_testExecOptions.windowsVerbatimArguments = true
_testExecOptions.listeners = {
debug: (data: string) => {
debugList.push(data)
}
}
let toolName: string
let args: string[]
if (IS_WINDOWS) {
toolName = await io.which('cmd.exe', true)
args = [
'/D', // Disable execution of AutoRun commands from registry.
'/E:ON', // Enable command extensions. Note, command extensions are enabled by default, unless disabled via registry.
'/V:OFF', // Disable delayed environment expansion. Note, delayed environment expansion is disabled by default, unless enabled via registry.
'/S', // Will cause first and last quote after /C to be stripped.
'/C',
`"start "" /B "${nodePath}" "${scriptPath}" "file=${semaphorePath}"" & exit /b 123`
]
} else {
toolName = await io.which('bash', true)
args = ['-c', `node '${scriptPath}' 'file=${semaphorePath}' & exit 123`]
}
await exec
.exec(`"${toolName}"`, args, _testExecOptions)
.then(() => {
throw new Error('Should not have succeeded')
})
.catch(err => {
expect(
err.message.indexOf('failed with exit code 123')
).toBeGreaterThanOrEqual(0)
})
expect(
debugList.filter(x => x.includes('STDIO streams did not close')).length
).toBe(1)
fs.unlinkSync(semaphorePath)
}, 10000) // this was timing out on some slower hosted macOS runs at default 5s
it('Handles child process holding streams open and stderr', async function () {
const semaphorePath = path.join(
getTestTemp(),
'child-process-semaphore.txt'
)
fs.writeFileSync(semaphorePath, '')
const nodePath = await io.which('node', true)
const scriptPath = path.join(__dirname, 'scripts', 'wait-for-file.js')
const debugList: string[] = []
const _testExecOptions = getExecOptions()
_testExecOptions.delay = 500
_testExecOptions.failOnStdErr = true
_testExecOptions.windowsVerbatimArguments = true
_testExecOptions.listeners = {
debug: (data: string) => {
debugList.push(data)
}
}
let toolName: string
let args: string[]
if (IS_WINDOWS) {
toolName = await io.which('cmd.exe', true)
args = [
'/D', // Disable execution of AutoRun commands from registry.
'/E:ON', // Enable command extensions. Note, command extensions are enabled by default, unless disabled via registry.
'/V:OFF', // Disable delayed environment expansion. Note, delayed environment expansion is disabled by default, unless enabled via registry.
'/S', // Will cause first and last quote after /C to be stripped.
'/C',
`"start "" /B "${nodePath}" "${scriptPath}" "file=${semaphorePath}"" & echo hi 1>&2`
]
} else {
toolName = await io.which('bash', true)
args = [
'-c',
`node '${scriptPath}' 'file=${semaphorePath}' & echo hi 1>&2`
]
}
await exec
.exec(`"${toolName}"`, args, _testExecOptions)
.then(() => {
throw new Error('Should not have succeeded')
})
.catch(err => {
expect(
err.message.indexOf(
'failed because one or more lines were written to the STDERR stream'
)
).toBeGreaterThanOrEqual(0)
})
expect(
debugList.filter(x => x.includes('STDIO streams did not close')).length
).toBe(1)
fs.unlinkSync(semaphorePath)
})
it('Exec roots relative tool path using unrooted options.cwd', async () => {
let exitCode: number
let command: string
if (IS_WINDOWS) {
command = './print-args-cmd' // let ToolRunner resolve the extension
} else {
command = './print-args-sh.sh'
}
const execOptions = getExecOptions()
execOptions.cwd = 'scripts'
const outStream = new StringStream()
execOptions.outStream = outStream
let output = ''
execOptions.listeners = {
stdout: (data: Buffer) => {
output += data.toString()
}
}
const originalCwd = process.cwd()
try {
process.chdir(__dirname)
exitCode = await exec.exec(`${command} hello world`, [], execOptions)
} catch (err) {
process.chdir(originalCwd)
throw err
}
expect(exitCode).toBe(0)
const toolPath = path.resolve(
__dirname,
execOptions.cwd,
`${command}${IS_WINDOWS ? '.cmd' : ''}`
)
if (IS_WINDOWS) {
expect(outStream.getContents().split(os.EOL)[0]).toBe(
`[command]${process.env.ComSpec} /D /S /C "${toolPath} hello world"`
)
} else {
expect(outStream.getContents().split(os.EOL)[0]).toBe(
`[command]${toolPath} hello world`
)
}
expect(output.trim()).toBe(`args[0]: "hello"${os.EOL}args[1]: "world"`)
})
it('Exec roots throws friendly error when bad cwd is specified', async () => {
const execOptions = getExecOptions()
execOptions.cwd = 'nonexistent/path'
await expect(exec.exec('ls', ['-all'], execOptions)).rejects.toThrowError(
`The cwd: ${execOptions.cwd} does not exist!`
)
})
it('Exec roots does not throw when valid cwd is provided', async () => {
const execOptions = getExecOptions()
execOptions.cwd = './'
await expect(exec.exec('ls', ['-all'], execOptions)).resolves.toBe(0)
})
it('Exec roots relative tool path using rooted options.cwd', async () => {
let command: string
if (IS_WINDOWS) {
command = './print-args-cmd' // let ToolRunner resolve the extension
} else {
command = './print-args-sh.sh'
}
const execOptions = getExecOptions()
execOptions.cwd = path.join(__dirname, 'scripts')
const outStream = new StringStream()
execOptions.outStream = outStream
let output = ''
execOptions.listeners = {
stdout: (data: Buffer) => {
output += data.toString()
}
}
const exitCode = await exec.exec(`${command} hello world`, [], execOptions)
expect(exitCode).toBe(0)
const toolPath = path.resolve(
__dirname,
execOptions.cwd,
`${command}${IS_WINDOWS ? '.cmd' : ''}`
)
if (IS_WINDOWS) {
expect(outStream.getContents().split(os.EOL)[0]).toBe(
`[command]${process.env.ComSpec} /D /S /C "${toolPath} hello world"`
)
} else {
expect(outStream.getContents().split(os.EOL)[0]).toBe(
`[command]${toolPath} hello world`
)
}
expect(output.trim()).toBe(`args[0]: "hello"${os.EOL}args[1]: "world"`)
})
it('Exec roots relative tool path using process.cwd', async () => {
let exitCode: number
let command: string
if (IS_WINDOWS) {
command = 'scripts/print-args-cmd' // let ToolRunner resolve the extension
} else {
command = 'scripts/print-args-sh.sh'
}
const execOptions = getExecOptions()
const outStream = new StringStream()
execOptions.outStream = outStream
let output = ''
execOptions.listeners = {
stdout: (data: Buffer) => {
output += data.toString()
}
}
const originalCwd = process.cwd()
try {
process.chdir(__dirname)
exitCode = await exec.exec(`${command} hello world`, [], execOptions)
} catch (err) {
process.chdir(originalCwd)
throw err
}
expect(exitCode).toBe(0)
const toolPath = path.resolve(
__dirname,
`${command}${IS_WINDOWS ? '.cmd' : ''}`
)
if (IS_WINDOWS) {
expect(outStream.getContents().split(os.EOL)[0]).toBe(
`[command]${process.env.ComSpec} /D /S /C "${toolPath} hello world"`
)
} else {
expect(outStream.getContents().split(os.EOL)[0]).toBe(
`[command]${toolPath} hello world`
)
}
expect(output.trim()).toBe(`args[0]: "hello"${os.EOL}args[1]: "world"`)
})
it('correctly outputs for getExecOutput', async () => {
const stdErrPath: string = path.join(
__dirname,
'scripts',
'stderroutput.js'
)
const stdOutPath: string = path.join(
__dirname,
'scripts',
'stdoutoutput.js'
)
const nodePath: string = await io.which('node', true)
const {exitCode: exitCodeOut, stdout} = await exec.getExecOutput(
`"${nodePath}"`,
[stdOutPath],
getExecOptions()
)
expect(exitCodeOut).toBe(0)
expect(stdout).toBe('this is output to stdout')
const {exitCode: exitCodeErr, stderr} = await exec.getExecOutput(
`"${nodePath}"`,
[stdErrPath],
getExecOptions()
)
expect(exitCodeErr).toBe(0)
expect(stderr).toBe('this is output to stderr')
})
it('correctly outputs for getExecOutput with additional listeners', async () => {
const stdErrPath: string = path.join(
__dirname,
'scripts',
'stderroutput.js'
)
const stdOutPath: string = path.join(
__dirname,
'scripts',
'stdoutoutput.js'
)
const nodePath: string = await io.which('node', true)
let listenerOut = ''
const {exitCode: exitCodeOut, stdout} = await exec.getExecOutput(
`"${nodePath}"`,
[stdOutPath],
{
...getExecOptions(),
listeners: {
stdout: data => {
listenerOut = data.toString()
}
}
}
)
expect(exitCodeOut).toBe(0)
expect(stdout).toBe('this is output to stdout')
expect(listenerOut).toBe('this is output to stdout')
let listenerErr = ''
const {exitCode: exitCodeErr, stderr} = await exec.getExecOutput(
`"${nodePath}"`,
[stdErrPath],
{
...getExecOptions(),
listeners: {
stderr: data => {
listenerErr = data.toString()
}
}
}
)
expect(exitCodeErr).toBe(0)
expect(stderr).toBe('this is output to stderr')
expect(listenerErr).toBe('this is output to stderr')
})
it('correctly outputs for getExecOutput when total size exceeds buffer size', async () => {
const stdErrPath: string = path.join(
__dirname,
'scripts',
'stderroutput.js'
)
const stdOutPath: string = path.join(
__dirname,
'scripts',
'stdoutoutputlarge.js'
)
const nodePath: string = await io.which('node', true)
let listenerOut = ''
const {exitCode: exitCodeOut, stdout} = await exec.getExecOutput(
`"${nodePath}"`,
[stdOutPath],
{
...getExecOptions(),
listeners: {
stdout: data => {
listenerOut += data.toString()
}
}
}
)
expect(exitCodeOut).toBe(0)
expect(Buffer.byteLength(stdout || '', 'utf8')).toBe(2 ** 25)
expect(Buffer.byteLength(listenerOut, 'utf8')).toBe(2 ** 25)
let listenerErr = ''
const {exitCode: exitCodeErr, stderr} = await exec.getExecOutput(
`"${nodePath}"`,
[stdErrPath],
{
...getExecOptions(),
listeners: {
stderr: data => {
listenerErr = data.toString()
}
}
}
)
expect(exitCodeErr).toBe(0)
expect(stderr).toBe('this is output to stderr')
expect(listenerErr).toBe('this is output to stderr')
})
it('correctly outputs for getExecOutput with multi-byte characters', async () => {
const stdOutPath: string = path.join(
__dirname,
'scripts',
'stdoutputspecial.js'
)
const nodePath: string = await io.which('node', true)
let numStdOutBufferCalls = 0
const {exitCode: exitCodeOut, stdout} = await exec.getExecOutput(
`"${nodePath}"`,
[stdOutPath],
{
...getExecOptions(),
listeners: {
stdout: () => {
numStdOutBufferCalls += 1
}
}
}
)
expect(exitCodeOut).toBe(0)
//one call for each half of the © character, ensuring it was actually split and not sent together
expect(numStdOutBufferCalls).toBe(2)
expect(stdout).toBe('©')
})
if (IS_WINDOWS) {
it('Exec roots relative tool path using process.cwd (Windows path separator)', async () => {
let exitCode: number
const command = 'scripts\\print-args-cmd' // let ToolRunner resolve the extension
const execOptions = getExecOptions()
const outStream = new StringStream()
execOptions.outStream = outStream
let output = ''
execOptions.listeners = {
stdout: (data: Buffer) => {
output += data.toString()
}
}
const originalCwd = process.cwd()
try {
process.chdir(__dirname)
exitCode = await exec.exec(`${command} hello world`, [], execOptions)
} catch (err) {
process.chdir(originalCwd)
throw err
}
expect(exitCode).toBe(0)
const toolPath = path.resolve(__dirname, `${command}.cmd`)
expect(outStream.getContents().split(os.EOL)[0]).toBe(
`[command]${process.env.ComSpec} /D /S /C "${toolPath} hello world"`
)
expect(output.trim()).toBe(`args[0]: "hello"${os.EOL}args[1]: "world"`)
})
// Win specific quoting tests
it('execs .exe with verbatim args (Windows)', async () => {
const exePath = process.env.ComSpec
const args: string[] = ['/c', 'echo', 'helloworld', 'hello:"world again"']
const outStream = new StringStream()
let output = ''
const options = {
outStream: <stream.Writable>outStream,
windowsVerbatimArguments: true,
listeners: {
stdout: (data: Buffer) => {
output += data.toString()
}
}
}
const exitCode = await exec.exec(`"${exePath}"`, args, options)
expect(exitCode).toBe(0)
expect(outStream.getContents().split(os.EOL)[0]).toBe(
`[command]"${exePath}" /c echo helloworld hello:"world again"`
)
expect(output.trim()).toBe('helloworld hello:"world again"')
})
it('execs .exe with arg quoting (Windows)', async () => {
const exePath = process.env.ComSpec
const args: string[] = [
'/c',
'echo',
'helloworld',
'hello world',
'hello:"world again"',
'hello,world'
]
const outStream = new StringStream()
let output = ''
const options = {
outStream: <stream.Writable>outStream,
listeners: {
stdout: (data: Buffer) => {
output += data.toString()
}
}
}
const exitCode = await exec.exec(`"${exePath}"`, args, options)
expect(exitCode).toBe(0)
expect(outStream.getContents().split(os.EOL)[0]).toBe(
`[command]${exePath} /c echo` +
` helloworld` +
` "hello world"` +
` "hello:\\"world again\\""` +
` hello,world`
)
expect(output.trim()).toBe(
'helloworld' +
' "hello world"' +
' "hello:\\"world again\\""' +
' hello,world'
)
})
it('execs .exe with a space and with verbatim args (Windows)', async () => {
// this test validates the quoting that tool runner adds around the tool path
// when using the windowsVerbatimArguments option. otherwise the target process
// interprets the args as starting after the first space in the tool path.
const exePath = compileArgsExe('print args exe with spaces.exe')
const args: string[] = ['myarg1 myarg2']
const outStream = new StringStream()
let output = ''
const options = {
outStream: <stream.Writable>outStream,
windowsVerbatimArguments: true,
listeners: {
stdout: (data: Buffer) => {
output += data.toString()
}
}
}
const exitCode = await exec.exec(`"${exePath}"`, args, options)
expect(exitCode).toBe(0)
expect(outStream.getContents().split(os.EOL)[0]).toBe(
`[command]"${exePath}" myarg1 myarg2`
)
expect(output.trim()).toBe("args[0]: 'myarg1'\r\nargs[1]: 'myarg2'")
}, 20000) // slower windows runs timeout, so upping timeout to 20s (from default of 5s)
it('execs .cmd with a space and with verbatim args (Windows)', async () => {
// this test validates the quoting that tool runner adds around the script path.
// otherwise cmd.exe will not be able to resolve the path to the script.
const cmdPath = path.join(
__dirname,
'scripts',
'print args cmd with spaces.cmd'
)
const args: string[] = ['arg1 arg2', 'arg3']
const outStream = new StringStream()
let output = ''
const options = {
outStream: <stream.Writable>outStream,
windowsVerbatimArguments: true,
listeners: {
stdout: (data: Buffer) => {
output += data.toString()
}
}
}
const exitCode = await exec.exec(`"${cmdPath}"`, args, options)
expect(exitCode).toBe(0)
expect(outStream.getContents().split(os.EOL)[0]).toBe(
`[command]${process.env.ComSpec} /D /S /C ""${cmdPath}" arg1 arg2 arg3"`
)
expect(output.trim()).toBe(
'args[0]: "arg1"\r\nargs[1]: "arg2"\r\nargs[2]: "arg3"'
)
})
it('execs .cmd with a space and with arg with space (Windows)', async () => {
// this test validates the command is wrapped in quotes (i.e. cmd.exe /S /C "<COMMAND>").
// otherwise the leading quote (around the script with space path) would be stripped
// and cmd.exe would not be able to resolve the script path.
const cmdPath = path.join(
__dirname,
'scripts',
'print args cmd with spaces.cmd'
)
const args: string[] = ['my arg 1', 'my arg 2']
const outStream = new StringStream()
let output = ''
const options = {
outStream: <stream.Writable>outStream,
listeners: {
stdout: (data: Buffer) => {
output += data.toString()
}
}
}
const exitCode = await exec.exec(`"${cmdPath}"`, args, options)
expect(exitCode).toBe(0)
expect(outStream.getContents().split(os.EOL)[0]).toBe(
`[command]${process.env.ComSpec} /D /S /C ""${cmdPath}" "my arg 1" "my arg 2""`
)
expect(output.trim()).toBe(
'args[0]: "<quote>my arg 1<quote>"\r\n' +
'args[1]: "<quote>my arg 2<quote>"'
)
})
it('execs .cmd from path (Windows)', async () => {
// this test validates whether a .cmd is resolved from the PATH when the extension is not specified
const cmd = 'print-args-cmd' // note, not print-args-cmd.cmd
const cmdPath = path.join(__dirname, 'scripts', `${cmd}.cmd`)
const args: string[] = ['my arg 1', 'my arg 2']
const outStream = new StringStream()
let output = ''
const options = {
outStream: <stream.Writable>outStream,
listeners: {
stdout: (data: Buffer) => {
output += data.toString()
}
}
}
const originalPath = process.env['Path']
try {
process.env['Path'] = `${originalPath};${path.dirname(cmdPath)}`
const exitCode = await exec.exec(`${cmd}`, args, options)
expect(exitCode).toBe(0)
expect(outStream.getContents().split(os.EOL)[0]).toBe(
`[command]${process.env.ComSpec} /D /S /C "${cmdPath} "my arg 1" "my arg 2""`
)
expect(output.trim()).toBe(
'args[0]: "<quote>my arg 1<quote>"\r\n' +
'args[1]: "<quote>my arg 2<quote>"'
)
} catch (err) {
process.env['Path'] = originalPath
throw err
}
})
it('execs .cmd with arg quoting (Windows)', async () => {
// this test validates .cmd quoting rules are applied, not the default libuv rules
const cmdPath = path.join(
__dirname,
'scripts',
'print args cmd with spaces.cmd'
)
const args: string[] = [
'helloworld',
'hello world',
'hello\tworld',
'hello&world',
'hello(world',
'hello)world',
'hello[world',
'hello]world',
'hello{world',
'hello}world',
'hello^world',
'hello=world',
'hello;world',
'hello!world',
"hello'world",
'hello+world',
'hello,world',
'hello`world',
'hello~world',
'hello|world',
'hello<world',
'hello>world',
'hello:"world again"',
'hello world\\'
]
const outStream = new StringStream()
let output = ''
const options = {
outStream: <stream.Writable>outStream,
listeners: {
stdout: (data: Buffer) => {
output += data.toString()
}
}
}
const exitCode = await exec.exec(`"${cmdPath}"`, args, options)
expect(exitCode).toBe(0)
expect(outStream.getContents().split(os.EOL)[0]).toBe(
`[command]${process.env.ComSpec} /D /S /C ""${cmdPath}"` +
` helloworld` +
` "hello world"` +
` "hello\tworld"` +
` "hello&world"` +
` "hello(world"` +
` "hello)world"` +
` "hello[world"` +
` "hello]world"` +
` "hello{world"` +
` "hello}world"` +
` "hello^world"` +
` "hello=world"` +
` "hello;world"` +
` "hello!world"` +
` "hello'world"` +
` "hello+world"` +
` "hello,world"` +
` "hello\`world"` +
` "hello~world"` +
` "hello|world"` +
` "hello<world"` +
` "hello>world"` +
` "hello:""world again"""` +
` "hello world\\\\"` +
`"`
)
expect(output.trim()).toBe(
'args[0]: "helloworld"\r\n' +
'args[1]: "<quote>hello world<quote>"\r\n' +
'args[2]: "<quote>hello\tworld<quote>"\r\n' +
'args[3]: "<quote>hello&world<quote>"\r\n' +
'args[4]: "<quote>hello(world<quote>"\r\n' +
'args[5]: "<quote>hello)world<quote>"\r\n' +
'args[6]: "<quote>hello[world<quote>"\r\n' +
'args[7]: "<quote>hello]world<quote>"\r\n' +
'args[8]: "<quote>hello{world<quote>"\r\n' +
'args[9]: "<quote>hello}world<quote>"\r\n' +
'args[10]: "<quote>hello^world<quote>"\r\n' +
'args[11]: "<quote>hello=world<quote>"\r\n' +
'args[12]: "<quote>hello;world<quote>"\r\n' +
'args[13]: "<quote>hello!world<quote>"\r\n' +
'args[14]: "<quote>hello\'world<quote>"\r\n' +
'args[15]: "<quote>hello+world<quote>"\r\n' +
'args[16]: "<quote>hello,world<quote>"\r\n' +
'args[17]: "<quote>hello`world<quote>"\r\n' +
'args[18]: "<quote>hello~world<quote>"\r\n' +
'args[19]: "<quote>hello|world<quote>"\r\n' +
'args[20]: "<quote>hello<world<quote>"\r\n' +
'args[21]: "<quote>hello>world<quote>"\r\n' +
'args[22]: "<quote>hello:<quote><quote>world again<quote><quote><quote>"\r\n' +
'args[23]: "<quote>hello world\\\\<quote>"'
)
})
}
})
function getTestTemp(): string {
return path.join(__dirname, '_temp')
}
function getExecOptions(): im.ExecOptions {
return {
cwd: __dirname,
env: {},
silent: false,
failOnStdErr: false,
ignoreReturnCode: false,
outStream: outstream,
errStream: errstream
}
}
export class StringStream extends stream.Writable {
constructor() {
super()
stream.Writable.call(this)
}
private contents = ''
_write(
data: string | Buffer | Uint8Array,
encoding: string,
next: Function
): void {
this.contents += data
next()
}
getContents(): string {
return this.contents
}
}
// function to compile a .NET program on Windows.
const compileExe = (sourceFileName: string, targetFileName: string): string => {
const directory = path.join(getTestTemp(), sourceFileName)
io.mkdirP(directory)
const exePath = path.join(directory, targetFileName)
// short-circuit if already compiled
try {
fs.statSync(exePath)
return exePath
} catch (err) {
if (err.code !== 'ENOENT') {
throw err
}
}
const sourceFile = path.join(__dirname, 'scripts', sourceFileName)
const cscPath = 'C:\\Windows\\Microsoft.NET\\Framework64\\v4.0.30319\\csc.exe'
fs.statSync(cscPath)
childProcess.execFileSync(cscPath, [
'/target:exe',
`/out:${exePath}`,
sourceFile
])
return exePath
}
// function to compile a .NET program that prints the command line args.
// the helper program is used to validate that command line args are passed correctly.
const compileArgsExe = (targetFileName: string): string => {
return compileExe('print-args-exe.cs', targetFileName)
}