Skip to content

Commit 2e8eaa5

Browse files
authored
Use a wrapper to properly test CTRL-C event on Windows (#359)
1 parent cf30009 commit 2e8eaa5

File tree

3 files changed

+114
-19
lines changed

3 files changed

+114
-19
lines changed

bin/concurrently.spec.ts

+43-19
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { subscribeSpyTo } from '@hirez_io/observer-spy';
22
import { spawn } from 'child_process';
3+
import { sendCtrlC, spawnWithWrapper } from 'ctrlc-wrapper';
34
import { build } from 'esbuild';
45
import fs from 'fs';
56
import { escapeRegExp } from 'lodash';
@@ -11,8 +12,14 @@ import { map } from 'rxjs/operators';
1112
import stringArgv from 'string-argv';
1213

1314
const isWindows = process.platform === 'win32';
14-
const createKillMessage = (prefix: string) =>
15-
new RegExp(escapeRegExp(prefix) + ' exited with code ' + (isWindows ? 1 : '(SIGTERM|143)'));
15+
const createKillMessage = (prefix: string, signal: 'SIGTERM' | 'SIGINT') => {
16+
const map: Record<string, string | number> = {
17+
SIGTERM: isWindows ? 1 : '(SIGTERM|143)',
18+
// Could theoretically be anything (e.g. 0) if process has SIGINT handler
19+
SIGINT: isWindows ? '(3221225786|0)' : '(SIGINT|130|0)',
20+
};
21+
return new RegExp(escapeRegExp(prefix) + ' exited with code ' + map[signal]);
22+
};
1623

1724
let tmpDir: string;
1825

@@ -38,8 +45,9 @@ afterAll(() => {
3845
* Creates a child process running 'concurrently' with the given args.
3946
* Returns observables for its combined stdout + stderr output, close events, pid, and stdin stream.
4047
*/
41-
const run = (args: string) => {
42-
const child = spawn('node', [path.join(tmpDir, 'concurrently.js'), ...stringArgv(args)], {
48+
const run = (args: string, ctrlcWrapper?: boolean) => {
49+
const spawnFn = ctrlcWrapper ? spawnWithWrapper : spawn;
50+
const child = spawnFn('node', [path.join(tmpDir, 'concurrently.js'), ...stringArgv(args)], {
4351
cwd: __dirname,
4452
env: {
4553
...process.env,
@@ -94,6 +102,7 @@ const run = (args: string) => {
94102
};
95103

96104
return {
105+
process: child,
97106
stdin: child.stdin,
98107
pid: child.pid,
99108
log,
@@ -160,23 +169,36 @@ describe('exiting conditions', () => {
160169
});
161170

162171
it('is of success when a SIGINT is sent', async () => {
163-
const child = run('"node fixtures/read-echo.js"');
172+
// Windows doesn't support sending signals like on POSIX platforms.
173+
// However, in a console, processes can be interrupted with CTRL+C (like a SIGINT).
174+
// This is what we simulate here with the help of a wrapper application.
175+
const child = run('"node fixtures/read-echo.js"', isWindows ? true : false);
164176
// Wait for command to have started before sending SIGINT
165177
child.log.subscribe((line) => {
166178
if (/READING/.test(line)) {
167-
process.kill(child.pid, 'SIGINT');
179+
if (isWindows) {
180+
// Instruct the wrapper to send CTRL+C to its child
181+
sendCtrlC(child.process);
182+
} else {
183+
process.kill(child.pid, 'SIGINT');
184+
}
168185
}
169186
});
187+
const lines = await child.getLogLines();
170188
const exit = await child.exit;
171189

172-
// TODO
173-
// Windows doesn't support sending signals like on POSIX platforms.
174-
// In a console, processes can be interrupted with CTRL+C (SIGINT).
175-
// However, there is no easy way to simulate this event.
176-
// Calling 'process.kill' on a process in Windows means it
177-
// is getting killed forcefully and abruptly (similar to SIGKILL),
178-
// which then results in the exit code of '1'.
179-
expect(exit.code).toBe(isWindows ? 1 : 0);
190+
expect(exit.code).toBe(0);
191+
expect(lines).toContainEqual(
192+
expect.stringMatching(
193+
createKillMessage(
194+
isWindows
195+
? // '^C' is echoed by read-echo.js (also happens without the wrapper)
196+
'[0] ^Cnode fixtures/read-echo.js'
197+
: '[0] node fixtures/read-echo.js',
198+
'SIGINT'
199+
)
200+
)
201+
);
180202
});
181203
});
182204

@@ -281,7 +303,9 @@ describe('--kill-others', () => {
281303
expect.stringContaining('Sending SIGTERM to other processes')
282304
);
283305
expect(lines).toContainEqual(
284-
expect.stringMatching(createKillMessage('[0] node fixtures/sleep.mjs 10'))
306+
expect.stringMatching(
307+
createKillMessage('[0] node fixtures/sleep.mjs 10', 'SIGTERM')
308+
)
285309
);
286310
});
287311
});
@@ -294,7 +318,7 @@ describe('--kill-others', () => {
294318
expect(lines).toContainEqual(expect.stringContaining('[1] exit 1 exited with code 1'));
295319
expect(lines).toContainEqual(expect.stringContaining('Sending SIGTERM to other processes'));
296320
expect(lines).toContainEqual(
297-
expect.stringMatching(createKillMessage('[0] node fixtures/sleep.mjs 10'))
321+
expect.stringMatching(createKillMessage('[0] node fixtures/sleep.mjs 10', 'SIGTERM'))
298322
);
299323
});
300324
});
@@ -319,7 +343,7 @@ describe('--kill-others-on-fail', () => {
319343
expect(lines).toContainEqual(expect.stringContaining('[1] exit 1 exited with code 1'));
320344
expect(lines).toContainEqual(expect.stringContaining('Sending SIGTERM to other processes'));
321345
expect(lines).toContainEqual(
322-
expect.stringMatching(createKillMessage('[0] node fixtures/sleep.mjs 10'))
346+
expect.stringMatching(createKillMessage('[0] node fixtures/sleep.mjs 10', 'SIGTERM'))
323347
);
324348
});
325349
});
@@ -359,7 +383,7 @@ describe('--handle-input', () => {
359383
expect(exit.code).toBeGreaterThan(0);
360384
expect(lines).toContainEqual(expect.stringContaining('[1] stop'));
361385
expect(lines).toContainEqual(
362-
expect.stringMatching(createKillMessage('[0] node fixtures/read-echo.js'))
386+
expect.stringMatching(createKillMessage('[0] node fixtures/read-echo.js', 'SIGTERM'))
363387
);
364388
});
365389

@@ -376,7 +400,7 @@ describe('--handle-input', () => {
376400
expect(exit.code).toBeGreaterThan(0);
377401
expect(lines).toContainEqual(expect.stringContaining('[1] stop'));
378402
expect(lines).toContainEqual(
379-
expect.stringMatching(createKillMessage('[0] node fixtures/read-echo.js'))
403+
expect.stringMatching(createKillMessage('[0] node fixtures/read-echo.js', 'SIGTERM'))
380404
);
381405
});
382406
});

package-lock.json

+70
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@
7272
"@typescript-eslint/eslint-plugin": "^5.33.0",
7373
"@typescript-eslint/parser": "^5.33.0",
7474
"coveralls-next": "^4.1.2",
75+
"ctrlc-wrapper": "^0.0.4",
7576
"esbuild": "^0.15.1",
7677
"eslint": "^8.21.0",
7778
"eslint-config-prettier": "^8.5.0",

0 commit comments

Comments
 (0)