Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: use IPC for granular test reporting #43

Merged
merged 1 commit into from
Dec 6, 2021
Merged
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
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,18 @@ module.exports = {
'FOO_BAR': 'baz',
},

// optional, whether or not to filter console output
// if unspecified, defaults to false
// if true, filters all output to stdout and stderr, except for the download
// of the VS Code executable, to only show the output of console.log(),
// console.error(), console.warn(), and console.info() calls made by tests.
//
// NOTE: This will not display output if you require() or import the console
// API in your tests, as only the global console object is overridden. It also
// will not work if you are using lower-level APIs such as
// process.stdout.write().
filterOutput: true,

// optional, additional arguments to pass to VS Code
launchArgs: [
'--new-window',
Expand Down
14 changes: 12 additions & 2 deletions e2e/__tests__/__snapshots__/passing-tests.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ Object {
"numFailedTestSuites": 0,
"numFailedTests": 0,
"numPassedTestSuites": 1,
"numPassedTests": 2,
"numPassedTests": 3,
"numPendingTestSuites": 0,
"numPendingTests": 0,
"numRuntimeErrorTestSuites": 0,
"numTodoTests": 0,
"numTotalTestSuites": 1,
"numTotalTests": 2,
"numTotalTests": 3,
"openHandles": Array [],
"snapshot": Object {
"added": 0,
Expand Down Expand Up @@ -54,6 +54,16 @@ Object {
"status": "passed",
"title": "should test async",
},
Object {
"ancestorTitles": Array [
"Describe",
],
"failureMessages": Array [],
"fullName": "Describe should test with console.log",
"location": null,
"status": "passed",
"title": "should test with console.log",
},
],
"endTime": undefined,
"failureMessage": "",
Expand Down
1 change: 1 addition & 0 deletions e2e/__tests__/passing-tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ describe('Passing tests', () => {
console.error('stdout', stdout)
}
expect(json).toMatchSnapshot()
expect(stdout).toContain('This message was logged from the test file')
expect(exitCode).toBe(0)
}, 30000)
})
3 changes: 2 additions & 1 deletion e2e/jest-runner-vscode.config.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
/** @type {import('../src/types').JestVSCodeRunnerOptions} */
/** @type {import('../src/types').RunnerOptions} */
const config = {
version: '1.56.2',
launchArgs: ['--disable-extensions'],
filterOutput: true,
}

module.exports = config
5 changes: 5 additions & 0 deletions e2e/passing-tests/__tests__/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,9 @@ describe('Describe', () => {
it('should test async', async () => {
expect(true).toBe(true)
})

it('should test with console.log', () => {
console.log('This message was logged from the test file')
expect(true).toBe(true)
})
})
44 changes: 10 additions & 34 deletions src/child-process-runner.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import type { RemoteTestOptions, RemoteTestResults } from './types'
import type { RemoteTestOptions } from './types'
import * as jest from '@jest/core'
import type { buildArgv as buildArgvType } from 'jest-cli/build/cli/index'
import vscode from 'vscode'
import path from 'path'
import process from 'process'
import { IPC } from 'node-ipc'
import IPCClient from './ipc-client'

// eslint-disable-next-line @typescript-eslint/no-var-requires
const buildArgv: typeof buildArgvType = require(path.resolve(
Expand All @@ -17,29 +17,15 @@ const vscodeModulePath = require.resolve('./jest-vscode-module')
const moduleNameMapper = JSON.stringify({ '^vscode$': vscodeModulePath })

export async function run(): Promise<void> {
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<void>(resolve =>
ipc.connectTo(IPC_CHANNEL, () => {
ipc.of[IPC_CHANNEL].on('connect', resolve)
})
)
const ipc = new IPCClient('child')

const disconnected = new Promise<void>(resolve =>
ipc.of[IPC_CHANNEL].on('disconnect', resolve)
ipc.on('disconnect', resolve)
)

let response: RemoteTestResults
try {
const { PARENT_JEST_OPTIONS } = process.env

if (!PARENT_JEST_OPTIONS) {
throw new Error('PARENT_JEST_OPTIONS is not defined')
}
Expand All @@ -51,30 +37,20 @@ export async function run(): Promise<void> {
'--runner=jest-runner',
`--env=${vscodeTestEnvPath}`,
`--moduleNameMapper=${moduleNameMapper}`,
`--reporters=${require.resolve('./child-reporter')}`,
...(options.globalConfig.updateSnapshot === 'all' ? ['-u'] : []),
'--runTestsByPath',
...options.testPaths,
])

const { results } =
(await jest.runCLI(jestOptions, [options.globalConfig.rootDir])) ?? {}

response = {
is: 'ok',
results,
}
await jest.runCLI(jestOptions, [options.globalConfig.rootDir])
} catch (error: any) {
const errorObj = JSON.parse(
JSON.stringify(error, Object.getOwnPropertyNames(error))
)
response = {
is: 'error',
error: errorObj,
}
ipc.emit('error', errorObj)
}

ipc.of[IPC_CHANNEL].emit('test-results', response)
ipc.disconnect(IPC_CHANNEL)
await disconnected
await Promise.race([disconnected, ipc.disconnect()])
await vscode.commands.executeCommand('workbench.action.closeWindow')
}
88 changes: 88 additions & 0 deletions src/child-reporter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import type {
AggregatedResult,
TestCaseResult,
TestResult,
} from '@jest/test-result'
import type {
ReporterOnStartOptions,
Test,
Reporter,
Context,
} from '@jest/reporters'
import IPCClient from './ipc-client'
import wrapIO from './wrap-io'

export default class ChildReporter implements Reporter {
#ipc: IPCClient
#onConnected: Promise<void>

constructor() {
this.#ipc = new IPCClient('reporter')
this.#onConnected = this.#ipc.connect()

wrapIO(this.#ipc)
}

getLastError() {
return undefined
}

onTestResult(
test: Test,
testResult: TestResult,
aggregatedResult: AggregatedResult
): void {
this.#ipc.emit('testResult', {
test,
testResult,
aggregatedResult,
})
}

onTestFileResult(
test: Test,
testResult: TestResult,
aggregatedResult: AggregatedResult
): void {
this.#ipc.emit('testFileResult', {
test,
testResult,
aggregatedResult,
})
}

onTestCaseResult(test: Test, testCaseResult: TestCaseResult): void {
this.#ipc.emit('testCaseResult', {
test,
testCaseResult,
})
}

onRunStart(
aggregatedResult: AggregatedResult,
options: ReporterOnStartOptions
): void {
this.#ipc.emit('runStart', {
aggregatedResult,
options,
})
}

onTestStart(test: Test): void {
this.#ipc.emit('testStart', { test })
}

onTestFileStart(test: Test): void {
this.#ipc.emit('testFileStart', { test })
}

async onRunComplete(
contexts: Set<Context>,
results: AggregatedResult
): Promise<void> {
this.#ipc.emit('runComplete', { contexts, results })

await this.#onConnected
await this.#ipc.disconnect()
}
}
117 changes: 117 additions & 0 deletions src/ipc-client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import console from 'console'
import EventEmitter from 'events'
import { IPC } from 'node-ipc'
import MessageWriter from './message-writer'

export default class IPCClient {
#ipc: InstanceType<typeof IPC>
#ipcChannel: string
#messageQueue: Array<[string, unknown]> = []
#writer: MessageWriter
#connected = false
#promises: Set<Promise<void>> = new Set()
#emitter: EventEmitter = new EventEmitter()

get on(): EventEmitter['on'] {
return this.#emitter.on.bind(this.#emitter)
}

get once(): EventEmitter['once'] {
return this.#emitter.once.bind(this.#emitter)
}

get off(): EventEmitter['off'] {
return this.#emitter.off.bind(this.#emitter)
}

get removeListener(): EventEmitter['removeListener'] {
return this.#emitter.removeListener.bind(this.#emitter)
}

get removeAllListeners(): EventEmitter['removeAllListeners'] {
return this.#emitter.removeAllListeners.bind(this.#emitter)
}

constructor(id: string) {
const { IPC_CHANNEL, DEBUG_VSCODE_IPC } = process.env

if (!IPC_CHANNEL) {
throw new Error('IPC_CHANNEL is not defined')
}

this.#ipcChannel = IPC_CHANNEL

this.#ipc = new IPC()
this.#ipc.config.silent = !DEBUG_VSCODE_IPC
this.#ipc.config.id = `jest-runner-vscode-${id}-${process.pid}`
this.#ipc.config.logger = (message: string) => {
// keep message no longer than 500 characters
const truncatedMessage =
message.length > 500 ? `${message.slice(0, 500)}...\u001b[0m` : message

console.log(truncatedMessage)
}

this.#writer = new MessageWriter(this.#ipc, this.#ipcChannel)
}

async #flush(): Promise<void> {
while (this.#messageQueue.length) {
const message = this.#messageQueue.shift()

if (message) {
const [type, data] = message

await this.#writer.write(type, data)

this.#emitter.emit(type, data)
}
}
}

async connect(): Promise<void> {
return new Promise<void>(resolve => {
this.#ipc.connectTo(this.#ipcChannel, async () => {
this.#connected = true

await this.#flush()

this.#emitter.emit('connect')

this.#ipc.of[this.#ipcChannel].on('disconnect', () => {
this.#connected = false
this.#emitter.emit('disconnect')
})

resolve()
})
})
}

async disconnect(): Promise<void> {
if (!this.#connected) {
return
}

await Promise.all(this.#promises)

const disconnected = new Promise<void>(resolve => {
this.#emitter.once('disconnect', resolve)
})

this.#ipc.disconnect(this.#ipcChannel)

return disconnected
}

emit(type: string, data: unknown): void {
if (!this.#connected) {
this.#messageQueue.push([type, data])
} else {
const promise = this.#writer.write(type, data)

this.#promises.add(promise)
promise.then(() => this.#promises.delete(promise))
}
}
}
7 changes: 7 additions & 0 deletions src/jest-vscode-env.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
import NodeEnvironment from 'jest-environment-node'
import vscode from 'vscode'
import IPCClient from './ipc-client'
import wrapIO from './wrap-io'

const ipc = new IPCClient('env')

class VSCodeEnvironment extends NodeEnvironment {
async setup() {
await super.setup()
this.global.vscode = vscode
await ipc.connect()
await wrapIO(ipc, this.global)
}

async teardown() {
this.global.vscode = {}
await ipc.disconnect()
await super.teardown()
}
}
Expand Down
6 changes: 6 additions & 0 deletions src/js-message.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
declare module 'js-message' {
export default class Message {
type: string
data: unknown
}
}
Loading