diff --git a/index.js b/index.js index d0ad9d4127..05494a7aa5 100644 --- a/index.js +++ b/index.js @@ -1,6 +1,5 @@ 'use strict'; const path = require('path'); -const os = require('os'); const childProcess = require('child_process'); const crossSpawn = require('cross-spawn'); const stripFinalNewline = require('strip-final-newline'); @@ -9,12 +8,11 @@ const isStream = require('is-stream'); const getStream = require('get-stream'); const mergeStream = require('merge-stream'); const pFinally = require('p-finally'); -const onExit = require('signal-exit'); const makeError = require('./lib/error'); const normalizeStdio = require('./lib/stdio'); +const {spawnedKill, spawnedCancel, setupTimeout, setExitHandler, cleanup} = require('./lib/kill'); const DEFAULT_MAX_BUFFER = 1000 * 1000 * 100; -const DEFAULT_FORCE_KILL_TIMEOUT = 1000 * 5; const SPACES_REGEXP = / +/g; @@ -190,52 +188,6 @@ const mergePromise = (spawned, getPromise) => { return spawned; }; -const spawnedKill = (kill, signal = 'SIGTERM', options = {}) => { - const killResult = kill(signal); - setKillTimeout(kill, signal, options, killResult); - return killResult; -}; - -const setKillTimeout = (kill, signal, options, killResult) => { - if (!shouldForceKill(signal, options, killResult)) { - return; - } - - const timeout = getForceKillAfterTimeout(options); - setTimeout(() => { - kill('SIGKILL'); - }, timeout).unref(); -}; - -const shouldForceKill = (signal, {forceKillAfterTimeout}, killResult) => { - return isSigterm(signal) && forceKillAfterTimeout !== false && killResult; -}; - -const isSigterm = signal => { - return signal === os.constants.signals.SIGTERM || - (typeof signal === 'string' && signal.toUpperCase() === 'SIGTERM'); -}; - -const getForceKillAfterTimeout = ({forceKillAfterTimeout = true}) => { - if (forceKillAfterTimeout === true) { - return DEFAULT_FORCE_KILL_TIMEOUT; - } - - if (!Number.isInteger(forceKillAfterTimeout) || forceKillAfterTimeout < 0) { - throw new TypeError(`Expected the \`forceKillAfterTimeout\` option to be a non-negative integer, got \`${forceKillAfterTimeout}\` (${typeof forceKillAfterTimeout})`); - } - - return forceKillAfterTimeout; -}; - -const spawnedCancel = (spawned, context) => { - const killResult = spawned.kill(); - - if (killResult) { - context.isCanceled = true; - } -}; - const handleSpawned = (spawned, context) => { return new Promise((resolve, reject) => { spawned.on('exit', (code, signal) => { @@ -259,36 +211,6 @@ const handleSpawned = (spawned, context) => { }); }; -const setupTimeout = (spawned, {timeout, killSignal}, context) => { - if (timeout > 0) { - return setTimeout(() => { - context.timedOut = true; - spawned.kill(killSignal); - }, timeout); - } -}; - -// #115 -const setExitHandler = (spawned, {cleanup, detached}) => { - if (!cleanup || detached) { - return; - } - - return onExit(() => { - spawned.kill(); - }); -}; - -const cleanup = (timeoutId, removeExitHandler) => { - if (timeoutId !== undefined) { - clearTimeout(timeoutId); - } - - if (removeExitHandler !== undefined) { - removeExitHandler(); - } -}; - const execa = (file, args, options) => { const parsed = handleArgs(file, args, options); const command = joinCommand(file, args); diff --git a/lib/kill.js b/lib/kill.js new file mode 100644 index 0000000000..4f0eed1716 --- /dev/null +++ b/lib/kill.js @@ -0,0 +1,92 @@ +'use strict'; +const os = require('os'); +const onExit = require('signal-exit'); + +const DEFAULT_FORCE_KILL_TIMEOUT = 1000 * 5; + +// Monkey-patches `childProcess.kill()` to add `forceKillAfterTimeout` behavior +const spawnedKill = (kill, signal = 'SIGTERM', options = {}) => { + const killResult = kill(signal); + setKillTimeout(kill, signal, options, killResult); + return killResult; +}; + +const setKillTimeout = (kill, signal, options, killResult) => { + if (!shouldForceKill(signal, options, killResult)) { + return; + } + + const timeout = getForceKillAfterTimeout(options); + setTimeout(() => { + kill('SIGKILL'); + }, timeout).unref(); +}; + +const shouldForceKill = (signal, {forceKillAfterTimeout}, killResult) => { + return isSigterm(signal) && forceKillAfterTimeout !== false && killResult; +}; + +const isSigterm = signal => { + return signal === os.constants.signals.SIGTERM || + (typeof signal === 'string' && signal.toUpperCase() === 'SIGTERM'); +}; + +const getForceKillAfterTimeout = ({forceKillAfterTimeout = true}) => { + if (forceKillAfterTimeout === true) { + return DEFAULT_FORCE_KILL_TIMEOUT; + } + + if (!Number.isInteger(forceKillAfterTimeout) || forceKillAfterTimeout < 0) { + throw new TypeError(`Expected the \`forceKillAfterTimeout\` option to be a non-negative integer, got \`${forceKillAfterTimeout}\` (${typeof forceKillAfterTimeout})`); + } + + return forceKillAfterTimeout; +}; + +// `childProcess.cancel()` +const spawnedCancel = (spawned, context) => { + const killResult = spawned.kill(); + + if (killResult) { + context.isCanceled = true; + } +}; + +// `timeout` option handling +const setupTimeout = (spawned, {timeout, killSignal}, context) => { + if (timeout > 0) { + return setTimeout(() => { + context.timedOut = true; + spawned.kill(killSignal); + }, timeout); + } +}; + +// `cleanup` option handling +const setExitHandler = (spawned, {cleanup, detached}) => { + if (!cleanup || detached) { + return; + } + + return onExit(() => { + spawned.kill(); + }); +}; + +const cleanup = (timeoutId, removeExitHandler) => { + if (timeoutId !== undefined) { + clearTimeout(timeoutId); + } + + if (removeExitHandler !== undefined) { + removeExitHandler(); + } +}; + +module.exports = { + spawnedKill, + spawnedCancel, + setupTimeout, + setExitHandler, + cleanup +}; diff --git a/test/kill.js b/test/kill.js new file mode 100644 index 0000000000..67e726ab19 --- /dev/null +++ b/test/kill.js @@ -0,0 +1,243 @@ +import path from 'path'; +import test from 'ava'; +import pEvent from 'p-event'; +import isRunning from 'is-running'; +import execa from '..'; + +process.env.PATH = path.join(__dirname, 'fixtures') + path.delimiter + process.env.PATH; + +const TIMEOUT_REGEXP = /timed out after/; + +test('kill("SIGKILL") should terminate cleanly', async t => { + const subprocess = execa('node', ['./test/fixtures/no-killable'], {stdio: ['ipc']}); + await pEvent(subprocess, 'message'); + + subprocess.kill('SIGKILL'); + + const {signal} = await t.throwsAsync(subprocess); + t.is(signal, 'SIGKILL'); +}); + +// `SIGTERM` cannot be caught on Windows, and it always aborts the process (like `SIGKILL` on Unix). +// Therefore, this feature and those tests do not make sense on Windows. +if (process.platform !== 'win32') { + test('`forceKillAfterTimeout: false` should not kill after a timeout', async t => { + const subprocess = execa('node', ['./test/fixtures/no-killable'], {stdio: ['ipc']}); + await pEvent(subprocess, 'message'); + + subprocess.kill('SIGTERM', {forceKillAfterTimeout: false}); + + t.true(isRunning(subprocess.pid)); + subprocess.kill('SIGKILL'); + }); + + test('`forceKillAfterTimeout: number` should kill after a timeout', async t => { + const subprocess = execa('node', ['./test/fixtures/no-killable'], {stdio: ['ipc']}); + await pEvent(subprocess, 'message'); + + subprocess.kill('SIGTERM', {forceKillAfterTimeout: 50}); + + const {signal} = await t.throwsAsync(subprocess); + t.is(signal, 'SIGKILL'); + }); + + test('`forceKillAfterTimeout: true` should kill after a timeout', async t => { + const subprocess = execa('node', ['./test/fixtures/no-killable'], {stdio: ['ipc']}); + await pEvent(subprocess, 'message'); + + subprocess.kill('SIGTERM', {forceKillAfterTimeout: true}); + + const {signal} = await t.throwsAsync(subprocess); + t.is(signal, 'SIGKILL'); + }); + + test('kill() with no arguments should kill after a timeout', async t => { + const subprocess = execa('node', ['./test/fixtures/no-killable'], {stdio: ['ipc']}); + await pEvent(subprocess, 'message'); + + subprocess.kill(); + + const {signal} = await t.throwsAsync(subprocess); + t.is(signal, 'SIGKILL'); + }); + + test('`forceKillAfterTimeout` should not be a float', t => { + t.throws(() => { + execa('noop').kill('SIGTERM', {forceKillAfterTimeout: 0.5}); + }, {instanceOf: TypeError, message: /non-negative integer/}); + }); + + test('`forceKillAfterTimeout` should not be negative', t => { + t.throws(() => { + execa('noop').kill('SIGTERM', {forceKillAfterTimeout: -1}); + }, {instanceOf: TypeError, message: /non-negative integer/}); + }); +} + +test('execa() returns a promise with kill()', t => { + const {kill} = execa('noop', ['foo']); + t.is(typeof kill, 'function'); +}); + +test('timeout kills the process if it times out', async t => { + const {killed, timedOut} = await t.throwsAsync(execa('forever', {timeout: 1, message: TIMEOUT_REGEXP})); + t.false(killed); + t.true(timedOut); +}); + +test('timeout kills the process if it times out, in sync mode', async t => { + const {killed, timedOut} = await t.throws(() => { + execa.sync('forever', {timeout: 1, message: TIMEOUT_REGEXP}); + }); + t.false(killed); + t.true(timedOut); +}); + +test('timeout does not kill the process if it does not time out', async t => { + const {timedOut} = await execa('delay', ['500'], {timeout: 1e8}); + t.false(timedOut); +}); + +test('timedOut is false if no timeout was set', async t => { + const {timedOut} = await execa('noop'); + t.false(timedOut); +}); + +test('timedOut will be false if no timeout was set and zero exit code in sync mode', t => { + const {timedOut} = execa.sync('noop'); + t.false(timedOut); +}); + +// When child process exits before parent process +const spawnAndExit = async (t, cleanup, detached) => { + await t.notThrowsAsync(execa('sub-process-exit', [cleanup, detached])); +}; + +test('spawnAndExit', spawnAndExit, false, false); +test('spawnAndExit cleanup', spawnAndExit, true, false); +test('spawnAndExit detached', spawnAndExit, false, true); +test('spawnAndExit cleanup detached', spawnAndExit, true, true); + +// When parent process exits before child process +const spawnAndKill = async (t, signal, cleanup, detached, isKilled) => { + const subprocess = execa('sub-process', [cleanup, detached], {stdio: ['ignore', 'ignore', 'ignore', 'ipc']}); + + const pid = await pEvent(subprocess, 'message'); + t.true(Number.isInteger(pid)); + t.true(isRunning(pid)); + + process.kill(subprocess.pid, signal); + + await t.throwsAsync(subprocess); + + t.false(isRunning(subprocess.pid)); + t.is(isRunning(pid), !isKilled); + + if (!isKilled) { + process.kill(pid, 'SIGKILL'); + } +}; + +// Without `options.cleanup`: +// - on Windows subprocesses are killed if `options.detached: false`, but not +// if `options.detached: true` +// - on Linux subprocesses are never killed regardless of `options.detached` +// With `options.cleanup`, subprocesses are always killed +// - `options.cleanup` with SIGKILL is a noop, since it cannot be handled +const exitIfWindows = process.platform === 'win32'; +test('spawnAndKill SIGTERM', spawnAndKill, 'SIGTERM', false, false, exitIfWindows); +test('spawnAndKill SIGKILL', spawnAndKill, 'SIGKILL', false, false, exitIfWindows); +test('spawnAndKill cleanup SIGTERM', spawnAndKill, 'SIGTERM', true, false, true); +test('spawnAndKill cleanup SIGKILL', spawnAndKill, 'SIGKILL', true, false, exitIfWindows); +test('spawnAndKill detached SIGTERM', spawnAndKill, 'SIGTERM', false, true, false); +test('spawnAndKill detached SIGKILL', spawnAndKill, 'SIGKILL', false, true, false); +test('spawnAndKill cleanup detached SIGTERM', spawnAndKill, 'SIGTERM', true, true, false); +test('spawnAndKill cleanup detached SIGKILL', spawnAndKill, 'SIGKILL', true, true, false); + +// See #128 +test('removes exit handler on exit', async t => { + // FIXME: This relies on `signal-exit` internals + const ee = process.__signal_exit_emitter__; + + const child = execa('noop'); + const listener = ee.listeners('exit').pop(); + + await new Promise((resolve, reject) => { + child.on('error', reject); + child.on('exit', resolve); + }); + + const included = ee.listeners('exit').includes(listener); + t.false(included); +}); + +test('cancel method kills the subprocess', t => { + const subprocess = execa('node'); + subprocess.cancel(); + t.true(subprocess.killed); +}); + +test('result.isCanceled is false when spawned.cancel() isn\'t called (success)', async t => { + const {isCanceled} = await execa('noop'); + t.false(isCanceled); +}); + +test('result.isCanceled is false when spawned.cancel() isn\'t called (failure)', async t => { + const {isCanceled} = await t.throwsAsync(execa('fail')); + t.false(isCanceled); +}); + +test('result.isCanceled is false when spawned.cancel() isn\'t called in sync mode (success)', t => { + const {isCanceled} = execa.sync('noop'); + t.false(isCanceled); +}); + +test('result.isCanceled is false when spawned.cancel() isn\'t called in sync mode (failure)', t => { + const {isCanceled} = t.throws(() => { + execa.sync('fail'); + }); + t.false(isCanceled); +}); + +test('calling cancel method throws an error with message "Command was canceled"', async t => { + const subprocess = execa('noop'); + subprocess.cancel(); + await t.throwsAsync(subprocess, {message: /Command was canceled/}); +}); + +test('error.isCanceled is true when cancel method is used', async t => { + const subprocess = execa('noop'); + subprocess.cancel(); + const {isCanceled} = await t.throwsAsync(subprocess); + t.true(isCanceled); +}); + +test('error.isCanceled is false when kill method is used', async t => { + const subprocess = execa('noop'); + subprocess.kill(); + const {isCanceled} = await t.throwsAsync(subprocess); + t.false(isCanceled); +}); + +test('calling cancel method twice should show the same behaviour as calling it once', async t => { + const subprocess = execa('noop'); + subprocess.cancel(); + subprocess.cancel(); + const {isCanceled} = await t.throwsAsync(subprocess); + t.true(isCanceled); + t.true(subprocess.killed); +}); + +test('calling cancel method on a successfully completed process does not make result.isCanceled true', async t => { + const subprocess = execa('noop'); + const {isCanceled} = await subprocess; + subprocess.cancel(); + t.false(isCanceled); +}); + +test('calling cancel method on a process which has been killed does not make error.isCanceled true', async t => { + const subprocess = execa('noop'); + subprocess.kill(); + const {isCanceled} = await t.throwsAsync(subprocess); + t.false(isCanceled); +}); diff --git a/test/test.js b/test/test.js index 1225978227..2310757d86 100644 --- a/test/test.js +++ b/test/test.js @@ -5,13 +5,11 @@ import test from 'ava'; import getStream from 'get-stream'; import isRunning from 'is-running'; import tempfile from 'tempfile'; -import pEvent from 'p-event'; import execa from '..'; process.env.PATH = path.join(__dirname, 'fixtures') + path.delimiter + process.env.PATH; process.env.FOO = 'foo'; -const TIMEOUT_REGEXP = /timed out after/; const ENOENT_REGEXP = process.platform === 'win32' ? /failed with exit code 1/ : /spawn.* ENOENT/; test('execa()', async t => { @@ -89,72 +87,6 @@ test('skip throwing when using reject option in sync mode', t => { t.is(exitCode, 2); }); -test('kill("SIGKILL") should terminate cleanly', async t => { - const subprocess = execa('node', ['./test/fixtures/no-killable'], {stdio: ['ipc']}); - await pEvent(subprocess, 'message'); - - subprocess.kill('SIGKILL'); - - const {signal} = await t.throwsAsync(subprocess); - t.is(signal, 'SIGKILL'); -}); - -// `SIGTERM` cannot be caught on Windows, and it always aborts the process (like `SIGKILL` on Unix). -// Therefore, this feature and those tests do not make sense on Windows. -if (process.platform !== 'win32') { - test('`forceKillAfterTimeout: false` should not kill after a timeout', async t => { - const subprocess = execa('node', ['./test/fixtures/no-killable'], {stdio: ['ipc']}); - await pEvent(subprocess, 'message'); - - subprocess.kill('SIGTERM', {forceKillAfterTimeout: false}); - - t.true(isRunning(subprocess.pid)); - subprocess.kill('SIGKILL'); - }); - - test('`forceKillAfterTimeout: number` should kill after a timeout', async t => { - const subprocess = execa('node', ['./test/fixtures/no-killable'], {stdio: ['ipc']}); - await pEvent(subprocess, 'message'); - - subprocess.kill('SIGTERM', {forceKillAfterTimeout: 50}); - - const {signal} = await t.throwsAsync(subprocess); - t.is(signal, 'SIGKILL'); - }); - - test('`forceKillAfterTimeout: true` should kill after a timeout', async t => { - const subprocess = execa('node', ['./test/fixtures/no-killable'], {stdio: ['ipc']}); - await pEvent(subprocess, 'message'); - - subprocess.kill('SIGTERM', {forceKillAfterTimeout: true}); - - const {signal} = await t.throwsAsync(subprocess); - t.is(signal, 'SIGKILL'); - }); - - test('kill() with no arguments should kill after a timeout', async t => { - const subprocess = execa('node', ['./test/fixtures/no-killable'], {stdio: ['ipc']}); - await pEvent(subprocess, 'message'); - - subprocess.kill(); - - const {signal} = await t.throwsAsync(subprocess); - t.is(signal, 'SIGKILL'); - }); - - test('`forceKillAfterTimeout` should not be a float', t => { - t.throws(() => { - execa('noop').kill('SIGTERM', {forceKillAfterTimeout: 0.5}); - }, {instanceOf: TypeError, message: /non-negative integer/}); - }); - - test('`forceKillAfterTimeout` should not be negative', t => { - t.throws(() => { - execa('noop').kill('SIGTERM', {forceKillAfterTimeout: -1}); - }, {instanceOf: TypeError, message: /non-negative integer/}); - }); -} - test('stripFinalNewline: true', async t => { const {stdout} = await execa('noop', ['foo']); t.is(stdout, 'foo'); @@ -272,9 +204,8 @@ test('child process errors rejects promise right away', async t => { await t.throwsAsync(child, /test/); }); -test('execa() returns a promise with kill() and pid', t => { - const {kill, pid} = execa('noop', ['foo']); - t.is(typeof kill, 'function'); +test('execa() returns a promise with pid', t => { + const {pid} = execa('noop', ['foo']); t.is(typeof pid, 'number'); }); @@ -354,35 +285,6 @@ if (process.platform !== 'win32') { }); } -test('timeout kills the process if it times out', async t => { - const {killed, timedOut} = await t.throwsAsync(execa('forever', {timeout: 1, message: TIMEOUT_REGEXP})); - t.false(killed); - t.true(timedOut); -}); - -test('timeout kills the process if it times out, in sync mode', async t => { - const {killed, timedOut} = await t.throws(() => { - execa.sync('forever', {timeout: 1, message: TIMEOUT_REGEXP}); - }); - t.false(killed); - t.true(timedOut); -}); - -test('timeout does not kill the process if it does not time out', async t => { - const {timedOut} = await execa('delay', ['500'], {timeout: 1e8}); - t.false(timedOut); -}); - -test('timedOut is false if no timeout was set', async t => { - const {timedOut} = await execa('noop'); - t.false(timedOut); -}); - -test('timedOut will be false if no timeout was set and zero exit code in sync mode', t => { - const {timedOut} = execa.sync('noop'); - t.false(timedOut); -}); - const command = async (t, expected, ...args) => { const {command: failCommand} = await t.throwsAsync(execa('fail', args)); t.is(failCommand, `fail${expected}`); @@ -397,52 +299,6 @@ test(command, ' foo bar', 'foo', 'bar'); test(command, ' baz quz', 'baz', 'quz'); test(command, ''); -// When child process exits before parent process -const spawnAndExit = async (t, cleanup, detached) => { - await t.notThrowsAsync(execa('sub-process-exit', [cleanup, detached])); -}; - -test('spawnAndExit', spawnAndExit, false, false); -test('spawnAndExit cleanup', spawnAndExit, true, false); -test('spawnAndExit detached', spawnAndExit, false, true); -test('spawnAndExit cleanup detached', spawnAndExit, true, true); - -// When parent process exits before child process -const spawnAndKill = async (t, signal, cleanup, detached, isKilled) => { - const subprocess = execa('sub-process', [cleanup, detached], {stdio: ['ignore', 'ignore', 'ignore', 'ipc']}); - - const pid = await pEvent(subprocess, 'message'); - t.true(Number.isInteger(pid)); - t.true(isRunning(pid)); - - process.kill(subprocess.pid, signal); - - await t.throwsAsync(subprocess); - - t.false(isRunning(subprocess.pid)); - t.is(isRunning(pid), !isKilled); - - if (!isKilled) { - process.kill(pid, 'SIGKILL'); - } -}; - -// Without `options.cleanup`: -// - on Windows subprocesses are killed if `options.detached: false`, but not -// if `options.detached: true` -// - on Linux subprocesses are never killed regardless of `options.detached` -// With `options.cleanup`, subprocesses are always killed -// - `options.cleanup` with SIGKILL is a noop, since it cannot be handled -const exitIfWindows = process.platform === 'win32'; -test('spawnAndKill SIGTERM', spawnAndKill, 'SIGTERM', false, false, exitIfWindows); -test('spawnAndKill SIGKILL', spawnAndKill, 'SIGKILL', false, false, exitIfWindows); -test('spawnAndKill cleanup SIGTERM', spawnAndKill, 'SIGTERM', true, false, true); -test('spawnAndKill cleanup SIGKILL', spawnAndKill, 'SIGKILL', true, false, exitIfWindows); -test('spawnAndKill detached SIGTERM', spawnAndKill, 'SIGTERM', false, true, false); -test('spawnAndKill detached SIGKILL', spawnAndKill, 'SIGKILL', false, true, false); -test('spawnAndKill cleanup detached SIGTERM', spawnAndKill, 'SIGTERM', true, true, false); -test('spawnAndKill cleanup detached SIGKILL', spawnAndKill, 'SIGKILL', true, true, false); - if (process.platform !== 'win32') { test('write to fast-exit process', async t => { // Try-catch here is necessary, because this test is not 100% accurate @@ -505,23 +361,6 @@ test('detach child process', async t => { process.kill(pid, 'SIGKILL'); }); -// See #128 -test('removes exit handler on exit', async t => { - // FIXME: This relies on `signal-exit` internals - const ee = process.__signal_exit_emitter__; - - const child = execa('noop'); - const listener = ee.listeners('exit').pop(); - - await new Promise((resolve, reject) => { - child.on('error', reject); - child.on('exit', resolve); - }); - - const included = ee.listeners('exit').includes(listener); - t.false(included); -}); - test('promise methods are not enumerable', t => { const descriptors = Object.getOwnPropertyDescriptors(execa('noop')); // eslint-disable-next-line promise/prefer-await-to-then @@ -569,77 +408,6 @@ if (Promise.prototype.finally) { }); } -test('cancel method kills the subprocess', t => { - const subprocess = execa('node'); - subprocess.cancel(); - t.true(subprocess.killed); -}); - -test('result.isCanceled is false when spawned.cancel() isn\'t called (success)', async t => { - const {isCanceled} = await execa('noop'); - t.false(isCanceled); -}); - -test('result.isCanceled is false when spawned.cancel() isn\'t called (failure)', async t => { - const {isCanceled} = await t.throwsAsync(execa('fail')); - t.false(isCanceled); -}); - -test('result.isCanceled is false when spawned.cancel() isn\'t called in sync mode (success)', t => { - const {isCanceled} = execa.sync('noop'); - t.false(isCanceled); -}); - -test('result.isCanceled is false when spawned.cancel() isn\'t called in sync mode (failure)', t => { - const {isCanceled} = t.throws(() => { - execa.sync('fail'); - }); - t.false(isCanceled); -}); - -test('calling cancel method throws an error with message "Command was canceled"', async t => { - const subprocess = execa('noop'); - subprocess.cancel(); - await t.throwsAsync(subprocess, {message: /Command was canceled/}); -}); - -test('error.isCanceled is true when cancel method is used', async t => { - const subprocess = execa('noop'); - subprocess.cancel(); - const {isCanceled} = await t.throwsAsync(subprocess); - t.true(isCanceled); -}); - -test('error.isCanceled is false when kill method is used', async t => { - const subprocess = execa('noop'); - subprocess.kill(); - const {isCanceled} = await t.throwsAsync(subprocess); - t.false(isCanceled); -}); - -test('calling cancel method twice should show the same behaviour as calling it once', async t => { - const subprocess = execa('noop'); - subprocess.cancel(); - subprocess.cancel(); - const {isCanceled} = await t.throwsAsync(subprocess); - t.true(isCanceled); - t.true(subprocess.killed); -}); - -test('calling cancel method on a successfully completed process does not make result.isCanceled true', async t => { - const subprocess = execa('noop'); - const {isCanceled} = await subprocess; - subprocess.cancel(); - t.false(isCanceled); -}); - -test('calling cancel method on a process which has been killed does not make error.isCanceled true', async t => { - const subprocess = execa('noop'); - subprocess.kill(); - const {isCanceled} = await t.throwsAsync(subprocess); - t.false(isCanceled); -}); - test('allow commands with spaces and no array arguments', async t => { const {stdout} = await execa('command with space'); t.is(stdout, '');