Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 49 additions & 1 deletion __tests__/utils/run-command.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,27 @@ const runCommand = require('../../src/utils/run-command');

jest.mock('cross-spawn', () => jest.fn(() => ({
on: jest.fn(),
kill: jest.fn(),
})));

describe('runCommand', () => {
let subProcessMock;
let processOnSpy;
let processRemoveListenerSpy;
beforeEach(() => {
jest.clearAllMocks();
subProcessMock = {
on: jest.fn(),
kill: jest.fn(),
};
spawn.mockImplementation(() => subProcessMock);
processOnSpy = jest.spyOn(process, 'on');
processRemoveListenerSpy = jest.spyOn(process, 'removeListener');
});

afterEach(() => {
processOnSpy.mockRestore();
processRemoveListenerSpy.mockRestore();
});
it('should spawn a subProcess with the proper parameters', async () => {
const promise = runCommand('commandMock', ['arg1', 'arg2'], 'workingDirectoryMock');
Expand Down Expand Up @@ -69,12 +80,38 @@ describe('runCommand', () => {
subProcessMock.on.mock.calls[0][1](0);
await promise;

expect(subProcessMock.on).toHaveBeenCalledTimes(1);
expect(subProcessMock.on).toHaveBeenCalledTimes(2);
// expecting a function to have been called with something,
// and looking that thing up in that functions mocked parameters is not really testing anything
// in this test we are testing that the first parameter is correct,
// the passed function is tested in another test
expect(subProcessMock.on).toHaveBeenNthCalledWith(1, 'close', subProcessMock.on.mock.calls[0][1]);
expect(subProcessMock.on).toHaveBeenNthCalledWith(2, 'error', subProcessMock.on.mock.calls[1][1]);
});

it('should register a SIGINT listener that forwards to the sub process', async () => {
const promise = runCommand('commandMock', ['arg1', 'arg2'], 'workingDirectoryMock');

expect(processOnSpy).toHaveBeenCalledWith('SIGINT', expect.any(Function));

const sigintHandler = processOnSpy.mock.calls.find(([event]) => event === 'SIGINT')[1];
sigintHandler();

expect(subProcessMock.kill).toHaveBeenCalledTimes(1);
expect(subProcessMock.kill).toHaveBeenCalledWith('SIGINT');

subProcessMock.on.mock.calls[0][1](0);
await promise;
});

it('should remove the SIGINT listener when the sub process closes', async () => {
const promise = runCommand('commandMock', ['arg1', 'arg2'], 'workingDirectoryMock');
const sigintHandler = processOnSpy.mock.calls.find(([event]) => event === 'SIGINT')[1];

subProcessMock.on.mock.calls[0][1](0);
await promise;

expect(processRemoveListenerSpy).toHaveBeenCalledWith('SIGINT', sigintHandler);
});
describe('the registered on close handler', () => {
let promise;
Expand All @@ -99,4 +136,15 @@ describe('runCommand', () => {
await expect(promise).rejects.toThrow('Failed to execute: commandMock arg1 arg2');
});
});

describe('the registered on error handler', () => {
it('should reject the promise with the subprocess error', async () => {
const promise = runCommand('commandMock', ['arg1', 'arg2'], 'workingDirectoryMock');
const processError = new Error('failed to start process');

subProcessMock.on.mock.calls[1][1](processError);

await expect(promise).rejects.toThrow('failed to start process');
});
});
});
16 changes: 16 additions & 0 deletions src/utils/run-command.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,29 @@ const spawn = require('cross-spawn');

const runCommand = (command, args = [], workingDirectory = './') => new Promise((resolve, reject) => {
const subProcess = spawn(command, args, { stdio: 'inherit', cwd: workingDirectory });
const forwardSigintToChild = () => {
subProcess.kill('SIGINT');
};

const cleanup = () => {
process.removeListener('SIGINT', forwardSigintToChild);
};

process.on('SIGINT', forwardSigintToChild);

subProcess.on('close', (code) => {
cleanup();
if (code !== 0) {
reject(new Error(`Failed to execute: ${command} ${args.join(' ')}`));
return;
}
resolve();
});

subProcess.on('error', (err) => {
cleanup();
reject(err);
});
});

module.exports = runCommand;
Loading