diff --git a/__tests__/utils/run-command.spec.js b/__tests__/utils/run-command.spec.js index 9adf818..df2b31d 100644 --- a/__tests__/utils/run-command.spec.js +++ b/__tests__/utils/run-command.spec.js @@ -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'); @@ -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; @@ -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'); + }); + }); }); diff --git a/src/utils/run-command.js b/src/utils/run-command.js index e1eb2e7..68b4a74 100644 --- a/src/utils/run-command.js +++ b/src/utils/run-command.js @@ -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;