diff --git a/package.json b/package.json index 04f3005..5757e44 100644 --- a/package.json +++ b/package.json @@ -38,12 +38,14 @@ "@vscode/test-electron": "^1.6.2", "cosmiconfig": "^7.0.1", "jest-cli": "^27.2.1", - "jest-environment-node": "^27.2.0" + "jest-environment-node": "^27.2.0", + "node-ipc": "^9.0.0" }, "devDependencies": { "@types/fs-extra": "9.0.13", "@types/jest": "27.0.3", "@types/node": "14.14.45", + "@types/node-ipc": "^9.2.0", "@types/vscode": "1.56.0", "@typescript-eslint/eslint-plugin": "5.5.0", "@typescript-eslint/parser": "5.5.0", diff --git a/renovate.json b/renovate.json index 7aeb309..45f9657 100644 --- a/renovate.json +++ b/renovate.json @@ -8,6 +8,10 @@ { "matchPackageNames": ["@types/vscode"], "allowedVersions": "~1.56.0" + }, + { + "matchPackageNames": ["node-ipc", "@types/node-ipc"], + "allowedVersions": "^9.0.0" } ] } diff --git a/src/child-process-runner.ts b/src/child-process-runner.ts index 7af4fb2..440be63 100644 --- a/src/child-process-runner.ts +++ b/src/child-process-runner.ts @@ -1,9 +1,11 @@ import type { RemoteTestOptions, RemoteTestResults } from './types' import * as jest from '@jest/core' -import console from 'console' +import type { buildArgv as buildArgvType } from 'jest-cli/build/cli/index' import vscode from 'vscode' import path from 'path' -import type { buildArgv as buildArgvType } from 'jest-cli/build/cli/index' +import process from 'process' +import { IPC } from 'node-ipc' +import console from 'console' // eslint-disable-next-line @typescript-eslint/no-var-requires const buildArgv: typeof buildArgvType = require(path.resolve( @@ -16,14 +18,36 @@ const vscodeModulePath = require.resolve('./jest-vscode-module') const moduleNameMapper = JSON.stringify({ '^vscode$': vscodeModulePath }) export async function run(): Promise { + const { IPC_CHANNEL, PARENT_JEST_OPTIONS } = process.env + + if (!IPC_CHANNEL) { + throw new Error('IPC_CHANNEL is not defined') + } + + const ipc = new IPC() + + ipc.config.silent = true + ipc.config.id = `jest-runner-vscode-client-${process.pid}` + + await new Promise(resolve => + ipc.connectTo(IPC_CHANNEL, () => { + ipc.of[IPC_CHANNEL].on('connect', () => { + console.log(`Connected to ${IPC_CHANNEL}`) + resolve() + }) + }) + ) + + const disconnected = new Promise(resolve => + ipc.of[IPC_CHANNEL].on('disconnect', resolve) + ) + let response: RemoteTestResults try { - if (!process.env.PARENT_JEST_OPTIONS) { + if (!PARENT_JEST_OPTIONS) { throw new Error('PARENT_JEST_OPTIONS is not defined') } - const options: RemoteTestOptions = JSON.parse( - process.env.PARENT_JEST_OPTIONS - ) + const options: RemoteTestOptions = JSON.parse(PARENT_JEST_OPTIONS) const jestOptions = buildArgv([ '-i', @@ -53,7 +77,8 @@ export async function run(): Promise { } } - console.log(`[jest-runner-vscode] ${JSON.stringify(response)}\n`) - + ipc.of[IPC_CHANNEL].emit('test-results', response) + ipc.disconnect(IPC_CHANNEL) + await disconnected await vscode.commands.executeCommand('workbench.action.closeWindow') } diff --git a/src/run-vscode.ts b/src/run-vscode.ts index 2618d51..5ad895e 100644 --- a/src/run-vscode.ts +++ b/src/run-vscode.ts @@ -1,15 +1,16 @@ import type { RemoteTestOptions, RemoteTestResults } from './types' import cp from 'child_process' import console from 'console' -import readline from 'readline' +import type { IPC } from 'node-ipc' -export default function runVSCode( +export default async function runVSCode( vscodePath: string, args: string[], env: Record | undefined, - options: RemoteTestOptions + options: RemoteTestOptions, + ipc: InstanceType ): Promise { - return new Promise(resolve => { + return await new Promise(resolve => { let results: RemoteTestResults | undefined = undefined const useStdErr = @@ -21,24 +22,19 @@ export default function runVSCode( ...process.env, ...env, PARENT_JEST_OPTIONS: JSON.stringify(options), + IPC_CHANNEL: ipc.config.id, } - const vscode = cp.spawn(vscodePath, args, { env: environment }) + const onTestResults = (response: RemoteTestResults) => { + results = response + } - const stdoutReader = readline.createInterface({ - input: vscode.stdout, - terminal: true, - }) + ipc.server.on('test-results', onTestResults) - stdoutReader.on('line', line => { - const output = line.replace(/^\[jest-runner-vscode\] .+$/, match => { - results = JSON.parse(match.slice(21)) - return '' - }) - silent || log(output) - }) + const vscode = cp.spawn(vscodePath, args, { env: environment }) if (!silent) { + vscode.stdout.pipe(process.stdout) vscode.stderr.pipe(process.stderr) } @@ -47,23 +43,12 @@ export default function runVSCode( }) let exited = false - let streamClosed = false - - stdoutReader.on('close', () => { - streamClosed = true - if (exited) { - resolve(results) - } - }) - const onExit = ( + const onExit = async ( code: number | null, signal: NodeJS.Signals | null - ): void => { + ): Promise => { if (exited) { - if (streamClosed) { - resolve(results) - } return } exited = true @@ -81,6 +66,10 @@ export default function runVSCode( } else { silent || log(message) } + + ipc.server.off('test-results', onTestResults) + + resolve(results) } vscode.on('exit', onExit) diff --git a/src/runner.ts b/src/runner.ts index b3eb403..b487973 100644 --- a/src/runner.ts +++ b/src/runner.ts @@ -2,8 +2,10 @@ import type { Config } from '@jest/types' import type * as JestRunner from 'jest-runner' import type { TestResult as JestTestResult } from '@jest/types' import type { TestResult } from '@jest/test-result' +import { IPC } from 'node-ipc' import type { RunnerOptions } from './types' import path from 'path' +import process from 'process' import { cosmiconfig } from 'cosmiconfig' import downloadVSCode from './download-vscode' import runVSCode from './run-vscode' @@ -64,9 +66,21 @@ export default class VSCodeTestRunner { } } + // Start IPC server. + const ipc = new IPC() + + ipc.config.silent = true + ipc.config.id = `jest-runner-vscode-server-${process.pid}` + + await new Promise(resolve => { + ipc.serve(resolve) + ipc.server.start() + }) + // Run each group of tests in its own VS Code process. for (const [testDir, testGroup] of testsByDir.entries()) { if (watcher.isInterrupted()) { + ipc.server.stop() throw Object.assign(new Error(), { name: 'CancelRun' }) } @@ -113,7 +127,8 @@ export default class VSCodeTestRunner { workspacePath: this._globalConfig.rootDir, options, testPaths: testGroup.map(test => test.path), - } + }, + ipc ) if (!testResults) { @@ -164,5 +179,7 @@ export default class VSCodeTestRunner { } } } + + ipc.server.stop() } } diff --git a/yarn.lock b/yarn.lock index 339f141..2544e4b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1057,6 +1057,15 @@ __metadata: languageName: node linkType: hard +"@types/node-ipc@npm:^9.2.0": + version: 9.2.0 + resolution: "@types/node-ipc@npm:9.2.0" + dependencies: + "@types/node": "*" + checksum: 05954414de9e3c1e5022338774b02ffd91d2be391157c0d2d9a3de5ffd5cbe0e2d1b9e0f1979232331e7338b2e089a7ea6b6dd2a188b9796614829f7b27cedcc + languageName: node + linkType: hard + "@types/node@npm:*": version: 16.9.1 resolution: "@types/node@npm:16.9.1" @@ -2122,6 +2131,13 @@ __metadata: languageName: node linkType: hard +"easy-stack@npm:^1.0.1": + version: 1.0.1 + resolution: "easy-stack@npm:1.0.1" + checksum: 161a99e497b3857b0be4ec9e1ebbe90b241ea9d84702f9881b8e5b3f6822065b8c4e33436996935103e191bffba3607de70712a792f4d406a050def48c6bc381 + languageName: node + linkType: hard + "electron-to-chromium@npm:^1.3.830": version: 1.3.840 resolution: "electron-to-chromium@npm:1.3.840" @@ -2439,6 +2455,13 @@ __metadata: languageName: node linkType: hard +"event-pubsub@npm:4.3.0": + version: 4.3.0 + resolution: "event-pubsub@npm:4.3.0" + checksum: 6940f57790c01a967b7c637f1c9fd000ee968a1d5894186ffb3356ffbe174c70e22e62adbbcfcee3f305482d99b6abe7613c1c27c909b07adc9127dc16c8cf73 + languageName: node + linkType: hard + "execa@npm:5.1.1, execa@npm:^5.0.0": version: 5.1.1 resolution: "execa@npm:5.1.1" @@ -3925,6 +3948,7 @@ fsevents@^2.3.2: "@types/fs-extra": 9.0.13 "@types/jest": 27.0.3 "@types/node": 14.14.45 + "@types/node-ipc": ^9.2.0 "@types/vscode": 1.56.0 "@typescript-eslint/eslint-plugin": 5.5.0 "@typescript-eslint/parser": 5.5.0 @@ -3938,6 +3962,7 @@ fsevents@^2.3.2: jest: 27.4.3 jest-cli: ^27.2.1 jest-environment-node: ^27.2.0 + node-ipc: ^9.0.0 npm-run-all: 4.1.5 prettier: 2.5.1 ts-jest: 27.0.7 @@ -4284,6 +4309,22 @@ fsevents@^2.3.2: languageName: node linkType: hard +"js-message@npm:1.0.7": + version: 1.0.7 + resolution: "js-message@npm:1.0.7" + checksum: 18dcc4d80356e8b5be978ca7838d96d4e350a1cb8adc5741c229dec6df09f53bfed7c75c1f360171d2d791a14e2f077d6c2b1013ba899ded7a27d7dfcd4f3784 + languageName: node + linkType: hard + +"js-queue@npm:2.0.2": + version: 2.0.2 + resolution: "js-queue@npm:2.0.2" + dependencies: + easy-stack: ^1.0.1 + checksum: 5049c3f648315ed13e46755704ff5453df70f7e8e1812acf1f98d6700efbec32421f76294a0e63fd2a9f8aabaf124233bbb308f9a2caec9d9f3d833ab5a73079 + languageName: node + linkType: hard + "js-tokens@npm:^4.0.0": version: 4.0.0 resolution: "js-tokens@npm:4.0.0" @@ -4777,6 +4818,17 @@ fsevents@^2.3.2: languageName: node linkType: hard +"node-ipc@npm:^9.0.0": + version: 9.2.1 + resolution: "node-ipc@npm:9.2.1" + dependencies: + event-pubsub: 4.3.0 + js-message: 1.0.7 + js-queue: 2.0.2 + checksum: a38aa4c8ca4317b293e0ce21f0a3a4941fc51c054800b35e263fcfe3e0feeb60e7d2c497f015054b28783316c6e7d9cc3837af9d9958bcbd8c577d0cdf6964b7 + languageName: node + linkType: hard + "node-modules-regexp@npm:^1.0.0": version: 1.0.0 resolution: "node-modules-regexp@npm:1.0.0"