Repository: sindresorhus/execa
Pull request #294 (2019-06-17 14:18:19)

Refactor `timeout` option
This refactors the `timeout` option handling. This does not change behavior.

With this PR and the upcoming ones, I am trying to split up the main `execa()` function into smaller functions. The main function partially relies on closure state, so I had to introduce a `context` object passed around instead.

ehmicky: Fixed.

Patch:
diff --git a/fixtures/detach b/fixtures/detach
deleted file mode 100755
index 7da1f8f..0000000
--- a/fixtures/detach
+++ /dev/null
@@ -1,8 +0,0 @@
-#!/usr/bin/env node
-'use strict';
-
-const execa = require('..');
-
-const subprocess = execa('node', ['./fixtures/forever'], {detached: true});
-console.log(subprocess.pid);
-process.exit();
diff --git a/fixtures/echo b/fixtures/echo
deleted file mode 100755
index cdfa972..0000000
--- a/fixtures/echo
+++ /dev/null
@@ -1,3 +0,0 @@
-#!/usr/bin/env node
-'use strict';
-console.log(process.argv.slice(2).join('\n'))
diff --git a/index.d.ts b/index.d.ts
index cb25030..9a21846 100644
--- a/index.d.ts
+++ b/index.d.ts
@@ -187,14 +187,29 @@ declare namespace execa {
 		readonly input?: string | Buffer | ReadableStream;
 	}
 
-	interface SyncOptions<EncodingType = string>
-		extends CommonOptions<EncodingType> {
+	interface SyncOptions<EncodingType = string> extends CommonOptions<EncodingType> {
 		/**
 		Write some input to the `stdin` of your binary.
 		*/
 		readonly input?: string | Buffer;
 	}
 
+	interface NodeOptions<EncodingType = string> extends Options<EncodingType> {
+		/**
+		The Node.js executable to use.
+
+		@default process.execPath
+		*/
+		readonly nodePath?: string;
+
+		/**
+		List of string arguments passed to the Node.js executable.
+
+		@default process.execArgv
+		*/
+		readonly nodeArguments?: string[];
+	}
+
 	interface ExecaReturnBase<StdoutStderrType> {
 		/**
 		The file and arguments that were run.
@@ -417,6 +432,26 @@ declare const execa: {
 	*/
 	commandSync(command: string, options?: execa.SyncOptions): execa.ExecaSyncReturnValue;
 	commandSync(command: string, options?: execa.SyncOptions<null>): execa.ExecaSyncReturnValue<Buffer>;
+
+	/**
+	Execute a Node.js script as a child process.
+
+	@param scriptPath - Node.js script to execute.
+	@param arguments - Arguments to pass to `scriptPath` on execution.
+	@returns A [`child_process` instance](https://nodejs.org/api/child_process.html#child_process_class_childprocess), which is enhanced to also be a `Promise` for a result `Object` with `stdout` and `stderr` properties.
+	*/
+	node(
+		scriptPath: string,
+		arguments?: readonly string[],
+		options?: execa.NodeOptions
+	): execa.ExecaChildProcess;
+	node(
+		file: string,
+		arguments?: readonly string[],
+		options?: execa.Options<null>
+	): execa.ExecaChildProcess<Buffer>;
+	node(file: string, options?: execa.Options): execa.ExecaChildProcess;
+	node(file: string, options?: execa.Options<null>): execa.ExecaChildProcess<Buffer>;
 };
 
 export = execa;
diff --git a/index.js b/index.js
index 4c28cc0..8af6259 100644
--- a/index.js
+++ b/index.js
@@ -325,6 +325,15 @@ function handleSpawned(spawned, context) {
 	});
 }
 
+const setupTimeout = (spawned, {timeout, killSignal}, context) => {
+	if (timeout > 0) {
+		context.timeoutId = setTimeout(() => {
+			Object.assign(context, {timeoutId: undefined, timedOut: true});
+			spawned.kill(killSignal);
+		}, timeout);
+	}
+};
+
 const execa = (file, args, options) => {
 	const parsed = handleArgs(file, args, options);
 	const command = joinCommand(file, args);
@@ -355,14 +364,13 @@ const execa = (file, args, options) => {
 		});
 	}
 
-	let timeoutId;
 	const context = {timedOut: false};
 	let isCanceled = false;
 
 	const cleanup = () => {
-		if (timeoutId !== undefined) {
-			clearTimeout(timeoutId);
-			timeoutId = undefined;
+		if (context.timeoutId !== undefined) {
+			clearTimeout(context.timeoutId);
+			context.timeoutId = undefined;
 		}
 
 		if (removeExitHandler) {
@@ -370,13 +378,7 @@ const execa = (file, args, options) => {
 		}
 	};
 
-	if (parsed.options.timeout > 0) {
-		timeoutId = setTimeout(() => {
-			timeoutId = undefined;
-			context.timedOut = true;
-			spawned.kill(parsed.options.killSignal);
-		}, parsed.options.timeout);
-	}
+	setupTimeout(spawned, parsed.options, context);
 
 	// TODO: Use native "finally" syntax when targeting Node.js 10
 	const processDone = pFinally(handleSpawned(spawned, context), cleanup);
@@ -520,3 +522,30 @@ module.exports.commandSync = (command, options) => {
 	const [file, ...args] = parseCommand(command);
 	return execa.sync(file, args, options);
 };
+
+module.exports.node = (scriptPath, args, options) => {
+	if (args && !Array.isArray(args) && typeof args === 'object') {
+		options = args;
+		args = [];
+	}
+
+	const stdioOption = stdio.node(options);
+	options = options || {};
+
+	return execa(
+		options.nodePath || process.execPath,
+		[
+			...(options.nodeArguments || process.execArgv),
+			scriptPath,
+			...(Array.isArray(args) ? args : [])
+		],
+		{
+			...options,
+			stdin: undefined,
+			stdout: undefined,
+			stderr: undefined,
+			stdio: stdioOption,
+			shell: false
+		}
+	);
+};
diff --git a/lib/stdio.js b/lib/stdio.js
index 5b4cb9e..d9f5458 100644
--- a/lib/stdio.js
+++ b/lib/stdio.js
@@ -3,7 +3,7 @@ const alias = ['stdin', 'stdout', 'stderr'];
 
 const hasAlias = opts => alias.some(x => Boolean(opts[x]));
 
-module.exports = opts => {
+const stdio = opts => {
 	if (!opts) {
 		return;
 	}
@@ -39,3 +39,23 @@ module.exports = opts => {
 
 	return result;
 };
+
+module.exports = stdio;
+
+module.exports.node = opts => {
+	const defaultOption = 'pipe';
+
+	let stdioOption = stdio(opts || {stdio: defaultOption});
+
+	if (typeof stdioOption === 'string') {
+		stdioOption = [...new Array(3)].fill(stdioOption);
+	} else if (Array.isArray(stdioOption)) {
+		stdioOption = stdioOption.map((channel = defaultOption) => channel);
+	}
+
+	if (!stdioOption.includes('ipc')) {
+		stdioOption.push('ipc');
+	}
+
+	return stdioOption;
+};
diff --git a/readme.md b/readme.md
index ffd525e..e0e8571 100644
--- a/readme.md
+++ b/readme.md
@@ -6,6 +6,13 @@
 > Process execution for humans
 
 
+---
+
+One of the maintainers [@ehmicky](https://github.com/ehmicky) is looking for a remote full-time position as a Node.js back-end lead. Feel free to [contact him](https://www.mickael-hebert.com)!
+
+---
+
+
 ## Why
 
 This package improves [`child_process`](https://nodejs.org/api/child_process.html) methods with:
@@ -177,6 +184,15 @@ Same as [`execa.command()`](#execacommand-command-options) but synchronous.
 
 Returns or throws a [`childProcessResult`](#childProcessResult).
 
+### execa.node(scriptPath, [arguments], [options])
+
+Execute a Node.js script as a child process.
+
+Same as `execa('node', [file, ...arguments], options)` except (like [`child_process#fork()`](https://nodejs.org/api/child_process.html#child_process_child_process_fork_modulepath_args_options)):
+  - the [`nodePath`](#nodepath-for-node-only) and [`nodeArguments`](#nodearguments-for-node-only) options can be used
+  - the [`shell`](#shell) option cannot be used 
+  - an extra channel [`ipc`](https://nodejs.org/api/child_process.html#child_process_options_stdio) is passed to [`stdio`](#stdio)
+
 ### childProcessResult
 
 Type: `object`
@@ -431,6 +447,19 @@ Default: `false`
 
 If `true`, no quoting or escaping of arguments is done on Windows. Ignored on other platforms. This is set to `true` automatically when the `shell` option is `true`.
 
+#### nodePath *(for `.node()` only)*
+
+Type: `string`<br>
+Default: [`process.execPath`](https://nodejs.org/api/process.html#process_process_execpath)
+
+Node.js executable used to create the child process.
+
+#### nodeArguments *(for `.node()` only)*
+
+Type: `string[]`<br>
+Default: [`process.execArgv`](https://nodejs.org/api/process.html#process_process_execargv)
+
+List of string arguments passed to the Node.js executable.
 
 ## Tips
 
diff --git a/fixtures/stdin b/test/fixtures/stdin
similarity index 100%
rename from fixtures/stdin
rename to test/fixtures/stdin
diff --git a/fixtures/sub-process b/test/fixtures/sub-process
similarity index 57%
rename from fixtures/sub-process
rename to test/fixtures/sub-process
index 727aa00..6920e79 100755
--- a/fixtures/sub-process
+++ b/test/fixtures/sub-process
@@ -1,8 +1,8 @@
 #!/usr/bin/env node
 'use strict';
-const execa = require('..');
+const execa = require('../..');
 
 const cleanup = process.argv[2] === 'true';
 const detached = process.argv[3] === 'true';
-const subprocess = execa('node', ['./fixtures/forever'], {cleanup, detached});
+const subprocess = execa('node', ['./test/fixtures/forever'], {cleanup, detached});
 process.send(subprocess.pid);
diff --git a/fixtures/sub-process-exit b/test/fixtures/sub-process-exit
similarity index 56%
rename from fixtures/sub-process-exit
rename to test/fixtures/sub-process-exit
index 3ea8173..2b350a2 100755
--- a/fixtures/sub-process-exit
+++ b/test/fixtures/sub-process-exit
@@ -1,7 +1,7 @@
 #!/usr/bin/env node
 'use strict';
-const execa = require('..');
+const execa = require('../..');
 
 const cleanup = process.argv[2] === 'true';
 const detached = process.argv[3] === 'true';
-execa('node', ['./fixtures/noop'], {cleanup, detached});
+execa('node', ['./test/fixtures/noop'], {cleanup, detached});


Test patch:
diff --git a/index.test-d.ts b/index.test-d.ts
index a435ef8..591ad5e 100644
--- a/index.test-d.ts
+++ b/index.test-d.ts
@@ -166,3 +166,16 @@ expectType<ExecaSyncReturnValue<string>>(execa.commandSync('unicorns', {encoding
 expectType<ExecaSyncReturnValue<Buffer>>(execa.commandSync('unicorns', {encoding: null}));
 expectType<ExecaSyncReturnValue<string>>(execa.commandSync('unicorns foo', {encoding: 'utf8'}));
 expectType<ExecaSyncReturnValue<Buffer>>(execa.commandSync('unicorns foo', {encoding: null}));
+
+expectType<ExecaChildProcess<string>>(execa.node('unicorns'));
+expectType<ExecaReturnValue<string>>(await execa.node('unicorns'));
+expectType<ExecaReturnValue<string>>(
+	await execa.node('unicorns', {encoding: 'utf8'})
+);
+expectType<ExecaReturnValue<Buffer>>(await execa.node('unicorns', {encoding: null}));
+expectType<ExecaReturnValue<string>>(
+	await execa.node('unicorns', ['foo'], {encoding: 'utf8'})
+);
+expectType<ExecaReturnValue<Buffer>>(
+	await execa.node('unicorns', ['foo'], {encoding: null})
+);
diff --git a/fixtures/command with space b/test/fixtures/command with space
similarity index 100%
rename from fixtures/command with space
rename to test/fixtures/command with space
diff --git a/fixtures/delay b/test/fixtures/delay
similarity index 100%
rename from fixtures/delay
rename to test/fixtures/delay
diff --git a/test/fixtures/detach b/test/fixtures/detach
new file mode 100755
index 0000000..80f8980
--- /dev/null
+++ b/test/fixtures/detach
@@ -0,0 +1,8 @@
+#!/usr/bin/env node
+'use strict';
+
+const execa = require('../..');
+
+const subprocess = execa('node', ['./test/fixtures/forever'], {detached: true});
+console.log(subprocess.pid);
+process.exit();
diff --git a/test/fixtures/echo b/test/fixtures/echo
new file mode 100755
index 0000000..e9baf7e
--- /dev/null
+++ b/test/fixtures/echo
@@ -0,0 +1,3 @@
+#!/usr/bin/env node
+'use strict';
+console.log(process.argv.slice(2).join('\n'));
diff --git a/fixtures/environment b/test/fixtures/environment
similarity index 100%
rename from fixtures/environment
rename to test/fixtures/environment
diff --git a/fixtures/exit b/test/fixtures/exit
similarity index 100%
rename from fixtures/exit
rename to test/fixtures/exit
diff --git a/fixtures/fail b/test/fixtures/fail
similarity index 100%
rename from fixtures/fail
rename to test/fixtures/fail
diff --git a/fixtures/fast-exit-darwin b/test/fixtures/fast-exit-darwin
similarity index 100%
rename from fixtures/fast-exit-darwin
rename to test/fixtures/fast-exit-darwin
diff --git a/fixtures/fast-exit-linux b/test/fixtures/fast-exit-linux
similarity index 100%
rename from fixtures/fast-exit-linux
rename to test/fixtures/fast-exit-linux
diff --git a/fixtures/forever b/test/fixtures/forever
similarity index 100%
rename from fixtures/forever
rename to test/fixtures/forever
diff --git a/fixtures/hello.cmd b/test/fixtures/hello.cmd
old mode 100644
new mode 100755
similarity index 100%
rename from fixtures/hello.cmd
rename to test/fixtures/hello.cmd
diff --git a/test/fixtures/hello.sh b/test/fixtures/hello.sh
new file mode 100755
index 0000000..ff0e12e
--- /dev/null
+++ b/test/fixtures/hello.sh
@@ -0,0 +1,1 @@
+echo Hello World
diff --git a/fixtures/max-buffer b/test/fixtures/max-buffer
similarity index 100%
rename from fixtures/max-buffer
rename to test/fixtures/max-buffer
diff --git a/fixtures/no-killable b/test/fixtures/no-killable
old mode 100644
new mode 100755
similarity index 100%
rename from fixtures/no-killable
rename to test/fixtures/no-killable
diff --git a/fixtures/non-executable b/test/fixtures/non-executable
similarity index 100%
rename from fixtures/non-executable
rename to test/fixtures/non-executable
diff --git a/fixtures/noop b/test/fixtures/noop
similarity index 100%
rename from fixtures/noop
rename to test/fixtures/noop
diff --git a/fixtures/noop-132 b/test/fixtures/noop-132
similarity index 100%
rename from fixtures/noop-132
rename to test/fixtures/noop-132
diff --git a/fixtures/noop-err b/test/fixtures/noop-err
similarity index 100%
rename from fixtures/noop-err
rename to test/fixtures/noop-err
diff --git a/fixtures/noop-throw b/test/fixtures/noop-throw
similarity index 100%
rename from fixtures/noop-throw
rename to test/fixtures/noop-throw
diff --git a/test/fixtures/send b/test/fixtures/send
new file mode 100755
index 0000000..c215df6
--- /dev/null
+++ b/test/fixtures/send
@@ -0,0 +1,9 @@
+#!/usr/bin/env node
+'use strict';
+process.on('message', message => {
+	if (message === 'ping') {
+		process.send('pong');
+	} else {
+		throw new Error('Receive wrong message');
+	}
+});
diff --git a/test/node.js b/test/node.js
new file mode 100644
index 0000000..c6ff30d
--- /dev/null
+++ b/test/node.js
@@ -0,0 +1,46 @@
+import test from 'ava';
+import pEvent from 'p-event';
+import path from 'path';
+import execa from '..';
+
+process.env.PATH = path.join(__dirname, 'fixtures') + path.delimiter + process.env.PATH;
+
+test('node()', async t => {
+	const {exitCode} = await execa.node('test/fixtures/noop');
+	t.is(exitCode, 0);
+});
+
+test('node pipe stdout', async t => {
+	const {stdout} = await execa.node('test/fixtures/noop', ['foo'], {
+		stdout: 'pipe'
+	});
+
+	t.is(stdout, 'foo');
+});
+
+test('node correctly use nodePath', async t => {
+	const {stdout} = await execa.node(process.platform === 'win32' ? 'hello.cmd' : 'hello.sh', {
+		stdout: 'pipe',
+		nodePath: process.platform === 'win32' ? 'cmd.exe' : 'bash',
+		nodeArguments: process.platform === 'win32' ? ['/c'] : []
+	});
+
+	t.is(stdout, 'Hello World');
+});
+
+test('node pass on nodeArguments', async t => {
+	const {stdout} = await execa.node('console.log("foo")', {
+		stdout: 'pipe',
+		nodeArguments: ['-e']
+	});
+
+	t.is(stdout, 'foo');
+});
+
+test('node\'s forked script has a communication channel', async t => {
+	const subprocess = execa.node('test/fixtures/send');
+	subprocess.send('ping');
+
+	const message = await pEvent(subprocess, 'message');
+	t.is(message, 'pong');
+});
diff --git a/test/stdio.js b/test/stdio.js
index e02a329..a9ccb03 100644
--- a/test/stdio.js
+++ b/test/stdio.js
@@ -4,46 +4,69 @@ import stdio from '../lib/stdio';
 
 util.inspect.styles.name = 'magenta';
 
-function macro(t, input, expected) {
-	if (expected instanceof Error) {
-		t.throws(() => stdio(input), expected.message);
-		return;
-	}
-
-	const result = stdio(input);
-
-	if (typeof expected === 'object' && expected !== null) {
-		t.deepEqual(result, expected);
-	} else {
-		t.is(result, expected);
-	}
+function createMacro(func) {
+	const macro = (t, input, expected) => {
+		if (expected instanceof Error) {
+			t.throws(() => {
+				stdio(input);
+			}, expected.message);
+			return;
+		}
+
+		const result = func(input);
+
+		if (typeof expected === 'object' && expected !== null) {
+			t.deepEqual(result, expected);
+		} else {
+			t.is(result, expected);
+		}
+	};
+
+	macro.title = (providedTitle, input) => `${func.name} ${(providedTitle || util.inspect(input, {colors: true}))}`;
+
+	return macro;
 }
 
-macro.title = (providedTitle, input) => providedTitle || util.inspect(input, {colors: true});
-
-test(macro, undefined, undefined);
-
-test(macro, {stdio: 'inherit'}, 'inherit');
-test(macro, {stdio: 'pipe'}, 'pipe');
-test(macro, {stdio: 'ignore'}, 'ignore');
-test(macro, {stdio: [0, 1, 2]}, [0, 1, 2]);
-
-test(macro, {}, [undefined, undefined, undefined]);
-test(macro, {stdio: []}, [undefined, undefined, undefined]);
-test(macro, {stdin: 'pipe'}, ['pipe', undefined, undefined]);
-test(macro, {stdout: 'ignore'}, [undefined, 'ignore', undefined]);
-test(macro, {stderr: 'inherit'}, [undefined, undefined, 'inherit']);
-test(macro, {stdin: 'pipe', stdout: 'ignore', stderr: 'inherit'}, ['pipe', 'ignore', 'inherit']);
-test(macro, {stdin: 'pipe', stdout: 'ignore'}, ['pipe', 'ignore', undefined]);
-test(macro, {stdin: 'pipe', stderr: 'inherit'}, ['pipe', undefined, 'inherit']);
-test(macro, {stdout: 'ignore', stderr: 'inherit'}, [undefined, 'ignore', 'inherit']);
-test(macro, {stdin: 0, stdout: 1, stderr: 2}, [0, 1, 2]);
-test(macro, {stdin: 0, stdout: 1}, [0, 1, undefined]);
-test(macro, {stdin: 0, stderr: 2}, [0, undefined, 2]);
-test(macro, {stdout: 1, stderr: 2}, [undefined, 1, 2]);
-
-test(macro, {stdio: {foo: 'bar'}}, new TypeError('Expected `stdio` to be of type `string` or `Array`, got `object`'));
-
-test(macro, {stdin: 'inherit', stdio: 'pipe'}, new Error('It\'s not possible to provide `stdio` in combination with one of `stdin`, `stdout`, `stderr`'));
-test(macro, {stdin: 'inherit', stdio: ['pipe']}, new Error('It\'s not possible to provide `stdio` in combination with one of `stdin`, `stdout`, `stderr`'));
-test(macro, {stdin: 'inherit', stdio: [undefined, 'pipe']}, new Error('It\'s not possible to provide `stdio` in combination with one of `stdin`, `stdout`, `stderr`'));
+const stdioMacro = createMacro(stdio);
+
+test(stdioMacro, undefined, undefined);
+test(stdioMacro, null, undefined);
+
+test(stdioMacro, {stdio: 'inherit'}, 'inherit');
+test(stdioMacro, {stdio: 'pipe'}, 'pipe');
+test(stdioMacro, {stdio: 'ignore'}, 'ignore');
+test(stdioMacro, {stdio: [0, 1, 2]}, [0, 1, 2]);
+
+test(stdioMacro, {}, [undefined, undefined, undefined]);
+test(stdioMacro, {stdio: []}, [undefined, undefined, undefined]);
+test(stdioMacro, {stdin: 'pipe'}, ['pipe', undefined, undefined]);
+test(stdioMacro, {stdout: 'ignore'}, [undefined, 'ignore', undefined]);
+test(stdioMacro, {stderr: 'inherit'}, [undefined, undefined, 'inherit']);
+test(stdioMacro, {stdin: 'pipe', stdout: 'ignore', stderr: 'inherit'}, ['pipe', 'ignore', 'inherit']);
+test(stdioMacro, {stdin: 'pipe', stdout: 'ignore'}, ['pipe', 'ignore', undefined]);
+test(stdioMacro, {stdin: 'pipe', stderr: 'inherit'}, ['pipe', undefined, 'inherit']);
+test(stdioMacro, {stdout: 'ignore', stderr: 'inherit'}, [undefined, 'ignore', 'inherit']);
+test(stdioMacro, {stdin: 0, stdout: 1, stderr: 2}, [0, 1, 2]);
+test(stdioMacro, {stdin: 0, stdout: 1}, [0, 1, undefined]);
+test(stdioMacro, {stdin: 0, stderr: 2}, [0, undefined, 2]);
+test(stdioMacro, {stdout: 1, stderr: 2}, [undefined, 1, 2]);
+
+test(stdioMacro, {stdio: {foo: 'bar'}}, new TypeError('Expected `stdio` to be of type `string` or `Array`, got `object`'));
+
+test(stdioMacro, {stdin: 'inherit', stdio: 'pipe'}, new Error('It\'s not possible to provide `stdio` in combination with one of `stdin`, `stdout`, `stderr`'));
+test(stdioMacro, {stdin: 'inherit', stdio: ['pipe']}, new Error('It\'s not possible to provide `stdio` in combination with one of `stdin`, `stdout`, `stderr`'));
+test(stdioMacro, {stdin: 'inherit', stdio: [undefined, 'pipe']}, new Error('It\'s not possible to provide `stdio` in combination with one of `stdin`, `stdout`, `stderr`'));
+
+const forkMacro = createMacro(stdio.node);
+
+test(forkMacro, undefined, ['pipe', 'pipe', 'pipe', 'ipc']);
+test(forkMacro, {stdio: 'ignore'}, ['ignore', 'ignore', 'ignore', 'ipc']);
+test(forkMacro, {stdio: [0, 1, 2]}, [0, 1, 2, 'ipc']);
+test(forkMacro, {stdio: [0, 1, 2, 3]}, [0, 1, 2, 3, 'ipc']);
+test(forkMacro, {stdio: [0, 1, 2, 'ipc']}, [0, 1, 2, 'ipc']);
+
+test(forkMacro, {stdout: 'ignore'}, ['pipe', 'ignore', 'pipe', 'ipc']);
+test(forkMacro, {stdout: 'ignore', stderr: 'ignore'}, ['pipe', 'ignore', 'ignore', 'ipc']);
+
+test(forkMacro, {stdio: {foo: 'bar'}}, new TypeError('Expected `stdio` to be of type `string` or `Array`, got `object`'));
+test(forkMacro, {stdin: 'inherit', stdio: 'pipe'}, new Error('It\'s not possible to provide `stdio` in combination with one of `stdin`, `stdout`, `stderr`'));
diff --git a/test.js b/test/test.js
similarity index 93%
rename from test.js
rename to test/test.js
index 18bee8c..613ddec 100644
--- a/test.js
+++ b/test/test.js
@@ -7,7 +7,7 @@ import getStream from 'get-stream';
 import isRunning from 'is-running';
 import tempfile from 'tempfile';
 import pEvent from 'p-event';
-import execa from '.';
+import execa from '..';
 
 process.env.PATH = path.join(__dirname, 'fixtures') + path.delimiter + process.env.PATH;
 process.env.FOO = 'foo';
@@ -87,13 +87,13 @@ test('stdout/stderr/all on process errors, in sync mode', t => {
 
 test('pass `stdout` to a file descriptor', async t => {
 	const file = tempfile('.txt');
-	await execa('fixtures/noop', ['foo bar'], {stdout: fs.openSync(file, 'w')});
+	await execa('test/fixtures/noop', ['foo bar'], {stdout: fs.openSync(file, 'w')});
 	t.is(fs.readFileSync(file, 'utf8'), 'foo bar\n');
 });
 
 test('pass `stderr` to a file descriptor', async t => {
 	const file = tempfile('.txt');
-	await execa('fixtures/noop-err', ['foo bar'], {stderr: fs.openSync(file, 'w')});
+	await execa('test/fixtures/noop-err', ['foo bar'], {stderr: fs.openSync(file, 'w')});
 	t.is(fs.readFileSync(file, 'utf8'), 'foo bar\n');
 });
 
@@ -119,7 +119,7 @@ test('skip throwing when using reject option in sync mode', t => {
 });
 
 test('kill("SIGKILL") should terminate cleanly', async t => {
-	const subprocess = execa('node', ['fixtures/no-killable'], {stdio: ['ipc']});
+	const subprocess = execa('node', ['./test/fixtures/no-killable'], {stdio: ['ipc']});
 	await pEvent(subprocess, 'message');
 
 	subprocess.kill('SIGKILL');
@@ -132,7 +132,7 @@ test('kill("SIGKILL") should terminate cleanly', async t => {
 // 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', ['fixtures/no-killable'], {stdio: ['ipc']});
+		const subprocess = execa('node', ['./test/fixtures/no-killable'], {stdio: ['ipc']});
 		await pEvent(subprocess, 'message');
 
 		subprocess.kill('SIGTERM', {forceKillAfterTimeout: false});
@@ -142,7 +142,7 @@ if (process.platform !== 'win32') {
 	});
 
 	test('`forceKillAfterTimeout: number` should kill after a timeout', async t => {
-		const subprocess = execa('node', ['fixtures/no-killable'], {stdio: ['ipc']});
+		const subprocess = execa('node', ['./test/fixtures/no-killable'], {stdio: ['ipc']});
 		await pEvent(subprocess, 'message');
 
 		subprocess.kill('SIGTERM', {forceKillAfterTimeout: 50});
@@ -152,7 +152,7 @@ if (process.platform !== 'win32') {
 	});
 
 	test('`forceKillAfterTimeout: true` should kill after a timeout', async t => {
-		const subprocess = execa('node', ['fixtures/no-killable'], {stdio: ['ipc']});
+		const subprocess = execa('node', ['./test/fixtures/no-killable'], {stdio: ['ipc']});
 		await pEvent(subprocess, 'message');
 
 		subprocess.kill('SIGTERM', {forceKillAfterTimeout: true});
@@ -162,7 +162,7 @@ if (process.platform !== 'win32') {
 	});
 
 	test('kill() with no arguments should kill after a timeout', async t => {
-		const subprocess = execa('node', ['fixtures/no-killable'], {stdio: ['ipc']});
+		const subprocess = execa('node', ['./test/fixtures/no-killable'], {stdio: ['ipc']});
 		await pEvent(subprocess, 'message');
 
 		subprocess.kill();
@@ -382,7 +382,7 @@ test('execa() returns code and failed properties', async t => {
 });
 
 test('use relative path with \'..\' chars', async t => {
-	const pathViaParentDir = path.join('..', path.basename(__dirname), 'fixtures', 'noop');
+	const pathViaParentDir = path.join('..', path.basename(path.dirname(__dirname)), 'test', 'fixtures', 'noop');
 	const {stdout} = await execa(pathViaParentDir, ['foo']);
 	t.is(stdout, 'foo');
 });
@@ -623,13 +623,13 @@ test('do not extend environment with `extendEnv: false`', async t => {
 });
 
 test('can use `options.shell: true`', async t => {
-	const {stdout} = await execa('node fixtures/noop foo', {shell: true});
+	const {stdout} = await execa('node test/fixtures/noop foo', {shell: true});
 	t.is(stdout, 'foo');
 });
 
 test('can use `options.shell: string`', async t => {
 	const shell = process.platform === 'win32' ? 'cmd.exe' : '/bin/bash';
-	const {stdout} = await execa('node fixtures/noop foo', {shell});
+	const {stdout} = await execa('node test/fixtures/noop foo', {shell});
 	t.is(stdout, 'foo');
 });
 
@@ -777,7 +777,7 @@ test('calling cancel method twice should show the same behaviour as calling it o
 	t.true(subprocess.killed);
 });
 
-test('calling cancel method on a successfuly completed process does not make result.isCanceled true', async t => {
+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();
@@ -792,46 +792,46 @@ test('calling cancel method on a process which has been killed does not make err
 });
 
 test('allow commands with spaces and no array arguments', async t => {
-	const {stdout} = await execa('./fixtures/command with space');
+	const {stdout} = await execa('command with space');
 	t.is(stdout, '');
 });
 
 test('allow commands with spaces and array arguments', async t => {
-	const {stdout} = await execa('./fixtures/command with space', ['foo', 'bar']);
+	const {stdout} = await execa('command with space', ['foo', 'bar']);
 	t.is(stdout, 'foo\nbar');
 });
 
 test('execa.command()', async t => {
-	const {stdout} = await execa.command('node fixtures/echo foo bar');
+	const {stdout} = await execa.command('node test/fixtures/echo foo bar');
 	t.is(stdout, 'foo\nbar');
 });
 
 test('execa.command() ignores consecutive spaces', async t => {
-	const {stdout} = await execa.command('node fixtures/echo foo    bar');
+	const {stdout} = await execa.command('node test/fixtures/echo foo    bar');
 	t.is(stdout, 'foo\nbar');
 });
 
 test('execa.command() allows escaping spaces in commands', async t => {
-	const {stdout} = await execa.command('./fixtures/command\\ with\\ space foo bar');
+	const {stdout} = await execa.command('command\\ with\\ space foo bar');
 	t.is(stdout, 'foo\nbar');
 });
 
 test('execa.command() allows escaping spaces in arguments', async t => {
-	const {stdout} = await execa.command('node fixtures/echo foo\\ bar');
+	const {stdout} = await execa.command('node test/fixtures/echo foo\\ bar');
 	t.is(stdout, 'foo bar');
 });
 
 test('execa.command() escapes other whitespaces', async t => {
-	const {stdout} = await execa.command('node fixtures/echo foo\tbar');
+	const {stdout} = await execa.command('node test/fixtures/echo foo\tbar');
 	t.is(stdout, 'foo\tbar');
 });
 
 test('execa.command() trims', async t => {
-	const {stdout} = await execa.command('  node fixtures/echo foo bar  ');
+	const {stdout} = await execa.command('  node test/fixtures/echo foo bar  ');
 	t.is(stdout, 'foo\nbar');
 });
 
 test('execa.command.sync()', t => {
-	const {stdout} = execa.commandSync('node fixtures/echo foo bar');
+	const {stdout} = execa.commandSync('node test/fixtures/echo foo bar');
 	t.is(stdout, 'foo\nbar');
 });