Skip to content

Commit

Permalink
feat: use IPC for granular test reporting (#43)
Browse files Browse the repository at this point in the history
Resolves #26
  • Loading branch information
adalinesimonian committed Dec 6, 2021
1 parent 7e9d0ea commit 84fda0b
Show file tree
Hide file tree
Showing 15 changed files with 492 additions and 114 deletions.
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

0 comments on commit 84fda0b

Please sign in to comment.