Skip to content

Commit

Permalink
Refactor tests
Browse files Browse the repository at this point in the history
  • Loading branch information
ehmicky committed Dec 17, 2023
1 parent 248349c commit 806b3dd
Show file tree
Hide file tree
Showing 9 changed files with 484 additions and 570 deletions.
10 changes: 10 additions & 0 deletions test/helpers/stdio.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export const getStdinOption = stdioOption => ({stdin: stdioOption});
export const getStdoutOption = stdioOption => ({stdout: stdioOption});
export const getStderrOption = stdioOption => ({stderr: stdioOption});
export const getPlainStdioOption = stdioOption => ({stdio: stdioOption});
export const getInputOption = input => ({input});
export const getInputFileOption = inputFile => ({inputFile});

export const getScriptSync = $ => $.sync;

export const identity = value => value;
31 changes: 31 additions & 0 deletions test/stdio/file-descriptor.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import {readFile, open} from 'node:fs/promises';
import test from 'ava';
import tempfile from 'tempfile';
import {execa, execaSync} from '../../index.js';
import {setFixtureDir} from '../helpers/fixtures-dir.js';
import {getStdinOption, getStdoutOption, getStderrOption} from '../helpers/stdio.js';

setFixtureDir();

const getStdinProp = ({stdin}) => stdin;

const testFileDescriptorOption = async (t, fixtureName, getOptions, execaMethod) => {
const filePath = tempfile();
const fileDescriptor = await open(filePath, 'w');
await execaMethod(fixtureName, ['foobar'], getOptions(fileDescriptor));
t.is(await readFile(filePath, 'utf8'), 'foobar\n');
};

test('pass `stdout` to a file descriptor', testFileDescriptorOption, 'noop.js', getStdoutOption, execa);
test('pass `stderr` to a file descriptor', testFileDescriptorOption, 'noop-err.js', getStderrOption, execa);
test('pass `stdout` to a file descriptor - sync', testFileDescriptorOption, 'noop.js', getStdoutOption, execaSync);
test('pass `stderr` to a file descriptor - sync', testFileDescriptorOption, 'noop-err.js', getStderrOption, execaSync);

const testStdinWrite = async (t, getStreamProp, fixtureName, getOptions) => {
const subprocess = execa(fixtureName, getOptions('pipe'));
getStreamProp(subprocess).end('unicorns');
const {stdout} = await subprocess;
t.is(stdout, 'unicorns');
};

test('you can write to child.stdin', testStdinWrite, getStdinProp, 'stdin.js', getStdinOption);
156 changes: 156 additions & 0 deletions test/stdio/file-path.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import {readFile, writeFile} from 'node:fs/promises';
import {relative, dirname, basename} from 'node:path';
import process from 'node:process';
import {pathToFileURL} from 'node:url';
import test from 'ava';
import tempfile from 'tempfile';
import {execa, execaSync, $} from '../../index.js';
import {setFixtureDir} from '../helpers/fixtures-dir.js';
import {getStdinOption, getStdoutOption, getStderrOption, getInputFileOption, getScriptSync, identity} from '../helpers/stdio.js';

setFixtureDir();

const nonFileUrl = new URL('https://example.com');

const getRelativePath = filePath => relative('.', filePath);

const testStdinFile = async (t, mapFilePath, getOptions, execaMethod) => {
const inputPath = tempfile();
await writeFile(inputPath, 'foobar');
const {stdout} = await execaMethod('stdin.js', getOptions(mapFilePath(inputPath)));
t.is(stdout, 'foobar');
};

test('inputFile can be a file URL', testStdinFile, pathToFileURL, getInputFileOption, execa);
test('stdin can be a file URL', testStdinFile, pathToFileURL, getStdinOption, execa);
test('inputFile can be an absolute file path', testStdinFile, identity, getInputFileOption, execa);
test('stdin can be an absolute file path', testStdinFile, identity, getStdinOption, execa);
test('inputFile can be a relative file path', testStdinFile, getRelativePath, getInputFileOption, execa);
test('stdin can be a relative file path', testStdinFile, getRelativePath, getStdinOption, execa);
test('inputFile can be a file URL - sync', testStdinFile, pathToFileURL, getInputFileOption, execaSync);
test('stdin can be a file URL - sync', testStdinFile, pathToFileURL, getStdinOption, execaSync);
test('inputFile can be an absolute file path - sync', testStdinFile, identity, getInputFileOption, execaSync);
test('stdin can be an absolute file path - sync', testStdinFile, identity, getStdinOption, execaSync);
test('inputFile can be a relative file path - sync', testStdinFile, getRelativePath, getInputFileOption, execaSync);
test('stdin can be a relative file path - sync', testStdinFile, getRelativePath, getStdinOption, execaSync);

// eslint-disable-next-line max-params
const testOutputFile = async (t, mapFile, fixtureName, getOptions, execaMethod) => {
const outputFile = tempfile();
await execaMethod(fixtureName, ['foobar'], getOptions(mapFile(outputFile)));
t.is(await readFile(outputFile, 'utf8'), 'foobar\n');
};

test('stdout can be a file URL', testOutputFile, pathToFileURL, 'noop.js', getStdoutOption, execa);
test('stderr can be a file URL', testOutputFile, pathToFileURL, 'noop-err.js', getStderrOption, execa);
test('stdout can be an absolute file path', testOutputFile, identity, 'noop.js', getStdoutOption, execa);
test('stderr can be an absolute file path', testOutputFile, identity, 'noop-err.js', getStderrOption, execa);
test('stdout can be a relative file path', testOutputFile, getRelativePath, 'noop.js', getStdoutOption, execa);
test('stderr can be a relative file path', testOutputFile, getRelativePath, 'noop-err.js', getStderrOption, execa);
test('stdout can be a file URL - sync', testOutputFile, pathToFileURL, 'noop.js', getStdoutOption, execaSync);
test('stderr can be a file URL - sync', testOutputFile, pathToFileURL, 'noop-err.js', getStderrOption, execaSync);
test('stdout can be an absolute file path - sync', testOutputFile, identity, 'noop.js', getStdoutOption, execaSync);
test('stderr can be an absolute file path - sync', testOutputFile, identity, 'noop-err.js', getStderrOption, execaSync);
test('stdout can be a relative file path - sync', testOutputFile, getRelativePath, 'noop.js', getStdoutOption, execaSync);
test('stderr can be a relative file path - sync', testOutputFile, getRelativePath, 'noop-err.js', getStderrOption, execaSync);

const testStdioNonFileUrl = (t, getOptions, execaMethod) => {
t.throws(() => {
execaMethod('noop.js', getOptions(nonFileUrl));
}, {message: /pathToFileURL/});
};

test('inputFile cannot be a non-file URL', testStdioNonFileUrl, getInputFileOption, execa);
test('stdin cannot be a non-file URL', testStdioNonFileUrl, getStdinOption, execa);
test('stdout cannot be a non-file URL', testStdioNonFileUrl, getStdoutOption, execa);
test('stderr cannot be a non-file URL', testStdioNonFileUrl, getStderrOption, execa);
test('inputFile cannot be a non-file URL - sync', testStdioNonFileUrl, getInputFileOption, execaSync);
test('stdin cannot be a non-file URL - sync', testStdioNonFileUrl, getStdinOption, execaSync);
test('stdout cannot be a non-file URL - sync', testStdioNonFileUrl, getStdoutOption, execaSync);
test('stderr cannot be a non-file URL - sync', testStdioNonFileUrl, getStderrOption, execaSync);

const testInputFileValidUrl = async (t, execaMethod) => {
const inputPath = tempfile();
await writeFile(inputPath, 'foobar');
const currentCwd = process.cwd();
process.chdir(dirname(inputPath));
try {
const {stdout} = await execaMethod('stdin.js', {inputFile: basename(inputPath)});
t.is(stdout, 'foobar');
} finally {
process.chdir(currentCwd);
}
};

test('inputFile does not need to start with . when being a relative file path', testInputFileValidUrl, execa);
test('inputFile does not need to start with . when being a relative file path - sync', testInputFileValidUrl, execaSync);

const testStdioValidUrl = (t, getOptions, execaMethod) => {
t.throws(() => {
execaMethod('noop.js', getOptions('foobar'));
}, {message: /absolute file path/});
};

test('stdin must start with . when being a relative file path', testStdioValidUrl, getStdinOption, execa);
test('stdout must start with . when being a relative file path', testStdioValidUrl, getStdoutOption, execa);
test('stderr must start with . when being a relative file path', testStdioValidUrl, getStderrOption, execa);
test('stdin must start with . when being a relative file path - sync', testStdioValidUrl, getStdinOption, execaSync);
test('stdout must start with . when being a relative file path - sync', testStdioValidUrl, getStdoutOption, execaSync);
test('stderr must start with . when being a relative file path - sync', testStdioValidUrl, getStderrOption, execaSync);

const testFileError = async (t, mapFile, getOptions) => {
await t.throwsAsync(
execa('noop.js', getOptions(mapFile('./unknown/file'))),
{code: 'ENOENT'},
);
};

test('inputFile file URL errors should be handled', testFileError, pathToFileURL, getInputFileOption);
test('stdin file URL errors should be handled', testFileError, pathToFileURL, getStdinOption);
test('stdout file URL errors should be handled', testFileError, pathToFileURL, getStdoutOption);
test('stderr file URL errors should be handled', testFileError, pathToFileURL, getStderrOption);
test('inputFile file path errors should be handled', testFileError, identity, getInputFileOption);
test('stdin file path errors should be handled', testFileError, identity, getStdinOption);
test('stdout file path errors should be handled', testFileError, identity, getStdoutOption);
test('stderr file path errors should be handled', testFileError, identity, getStderrOption);

const testFileErrorSync = (t, mapFile, getOptions) => {
t.throws(() => {
execaSync('noop.js', getOptions(mapFile('./unknown/file')));
}, {code: 'ENOENT'});
};

test('inputFile file URL errors should be handled - sync', testFileErrorSync, pathToFileURL, getInputFileOption);
test('stdin file URL errors should be handled - sync', testFileErrorSync, pathToFileURL, getStdinOption);
test('stdout file URL errors should be handled - sync', testFileErrorSync, pathToFileURL, getStdoutOption);
test('stderr file URL errors should be handled - sync', testFileErrorSync, pathToFileURL, getStderrOption);
test('inputFile file path errors should be handled - sync', testFileErrorSync, identity, getInputFileOption);
test('stdin file path errors should be handled - sync', testFileErrorSync, identity, getStdinOption);
test('stdout file path errors should be handled - sync', testFileErrorSync, identity, getStdoutOption);
test('stderr file path errors should be handled - sync', testFileErrorSync, identity, getStderrOption);

const testInputFile = async (t, execaMethod) => {
const inputFile = tempfile();
await writeFile(inputFile, 'foobar');
const {stdout} = await execaMethod('stdin.js', {inputFile});
t.is(stdout, 'foobar');
};

test('inputFile can be set', testInputFile, execa);
test('inputFile can be set - sync', testInputFile, execa);

const testInputFileScript = async (t, getExecaMethod) => {
const inputFile = tempfile();
await writeFile(inputFile, 'foobar');
const {stdout} = await getExecaMethod($({inputFile}))`stdin.js`;
t.is(stdout, 'foobar');
};

test('inputFile can be set with $', testInputFileScript, identity);
test('inputFile can be set with $.sync', testInputFileScript, getScriptSync);

test('inputFile option cannot be set when stdin is set', t => {
t.throws(() => {
execa('stdin.js', {inputFile: '', stdin: 'ignore'});
}, {message: /`inputFile` and `stdin` options/});
});
64 changes: 64 additions & 0 deletions test/stdio/input.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import {Buffer} from 'node:buffer';
import {Readable} from 'node:stream';
import {pathToFileURL} from 'node:url';
import test from 'ava';
import {execa, execaSync, $} from '../../index.js';
import {setFixtureDir} from '../helpers/fixtures-dir.js';
import {getStdinOption, getPlainStdioOption, getScriptSync, identity} from '../helpers/stdio.js';

setFixtureDir();

const testInputOptionError = (t, stdin, inputName) => {
t.throws(() => {
execa('stdin.js', {stdin, [inputName]: 'foobar'});
}, {message: new RegExp(`\`${inputName}\` and \`stdin\` options`)});
};

test('stdin option cannot be an iterable when "input" is used', testInputOptionError, ['foo', 'bar'], 'input');
test('stdin option cannot be an iterable when "inputFile" is used', testInputOptionError, ['foo', 'bar'], 'inputFile');
test('stdin option cannot be a file URL when "input" is used', testInputOptionError, pathToFileURL('unknown'), 'input');
test('stdin option cannot be a file URL when "inputFile" is used', testInputOptionError, pathToFileURL('unknown'), 'inputFile');
test('stdin option cannot be a file path when "input" is used', testInputOptionError, './unknown', 'input');
test('stdin option cannot be a file path when "inputFile" is used', testInputOptionError, './unknown', 'inputFile');
test('stdin option cannot be a ReadableStream when "input" is used', testInputOptionError, new ReadableStream(), 'input');
test('stdin option cannot be a ReadableStream when "inputFile" is used', testInputOptionError, new ReadableStream(), 'inputFile');

const testInput = async (t, input, execaMethod) => {
const {stdout} = await execaMethod('stdin.js', {input});
t.is(stdout, 'foobar');
};

test('input option can be a String', testInput, 'foobar', execa);
test('input option can be a String - sync', testInput, 'foobar', execaSync);
test('input option can be a Buffer', testInput, Buffer.from('foobar'), execa);
test('input option can be a Buffer - sync', testInput, Buffer.from('foobar'), execaSync);

const testInputScript = async (t, getExecaMethod) => {
const {stdout} = await getExecaMethod($({input: 'foobar'}))`stdin.js`;
t.is(stdout, 'foobar');
};

test('input option can be used with $', testInputScript, identity);
test('input option can be used with $.sync', testInputScript, getScriptSync);

const testInputWithStdinError = (t, input, getOptions, execaMethod) => {
t.throws(() => {
execaMethod('stdin.js', {input, ...getOptions('ignore')});
}, {message: /`input` and `stdin` options/});
};

test('input option cannot be a String when stdin is set', testInputWithStdinError, 'foobar', getStdinOption, execa);
test('input option cannot be a String when stdio is set', testInputWithStdinError, 'foobar', getPlainStdioOption, execa);
test('input option cannot be a String when stdin is set - sync', testInputWithStdinError, 'foobar', getStdinOption, execaSync);
test('input option cannot be a String when stdio is set - sync', testInputWithStdinError, 'foobar', getPlainStdioOption, execaSync);
test('input option cannot be a Node.js Readable when stdin is set', testInputWithStdinError, new Readable(), getStdinOption, execa);
test('input option cannot be a Node.js Readable when stdio is set', testInputWithStdinError, new Readable(), getPlainStdioOption, execa);

const testInputAndInputFile = async (t, execaMethod) => {
t.throws(() => execaMethod('stdin.js', {inputFile: '', input: ''}), {
message: /cannot be both set/,
});
};

test('inputFile and input cannot be both set', testInputAndInputFile, execa);
test('inputFile and input cannot be both set - sync', testInputAndInputFile, execaSync);
60 changes: 60 additions & 0 deletions test/stdio/iterable.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import test from 'ava';
import {execa, execaSync} from '../../index.js';
import {setFixtureDir} from '../helpers/fixtures-dir.js';
import {getStdinOption, getStdoutOption, getStderrOption} from '../helpers/stdio.js';

setFixtureDir();

const textEncoder = new TextEncoder();
const binaryFoo = textEncoder.encode('foo');
const binaryBar = textEncoder.encode('bar');

const stringGenerator = function * () {
yield * ['foo', 'bar'];
};

const binaryGenerator = function * () {
yield * [binaryFoo, binaryBar];
};

// eslint-disable-next-line require-yield
const throwingGenerator = function * () {
throw new Error('generator error');
};

const testIterable = async (t, stdioOption, fixtureName, getOptions) => {
const {stdout} = await execa(fixtureName, getOptions(stdioOption));
t.is(stdout, 'foobar');
};

test('stdin option can be a sync iterable of strings', testIterable, ['foo', 'bar'], 'stdin.js', getStdinOption);
test('stdin option can be a sync iterable of Uint8Arrays', testIterable, [binaryFoo, binaryBar], 'stdin.js', getStdinOption);
test('stdin option can be an sync iterable of strings', testIterable, stringGenerator(), 'stdin.js', getStdinOption);
test('stdin option can be an sync iterable of Uint8Arrays', testIterable, binaryGenerator(), 'stdin.js', getStdinOption);

const testIterableSync = (t, stdioOption, fixtureName, getOptions) => {
t.throws(() => {
execaSync(fixtureName, getOptions(stdioOption));
}, {message: /an iterable in sync mode/});
};

test('stdin option cannot be a sync iterable - sync', testIterableSync, ['foo', 'bar'], 'stdin.js', getStdinOption);
test('stdin option cannot be an async iterable - sync', testIterableSync, stringGenerator(), 'stdin.js', getStdinOption);

const testIterableError = async (t, fixtureName, getOptions) => {
const {originalMessage} = await t.throwsAsync(execa(fixtureName, getOptions(throwingGenerator())));
t.is(originalMessage, 'generator error');
};

test('stdin option handles errors in iterables', testIterableError, 'stdin.js', getStdinOption);

const testNoIterableOutput = (t, getOptions, execaMethod) => {
t.throws(() => {
execaMethod('noop.js', getOptions(['foo', 'bar']));
}, {message: /cannot be an iterable/});
};

test('stdout option cannot be an iterable', testNoIterableOutput, getStdoutOption, execa);
test('stderr option cannot be an iterable', testNoIterableOutput, getStderrOption, execa);
test('stdout option cannot be an iterable - sync', testNoIterableOutput, getStdoutOption, execaSync);
test('stderr option cannot be an iterable - sync', testNoIterableOutput, getStderrOption, execaSync);

0 comments on commit 806b3dd

Please sign in to comment.