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

Clean up jest process management #215

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
c138da2
Updated jest process management to make it more comprehendible
marcinczenko Dec 11, 2017
b75f7a5
Added restarting Jest on snapshot update
marcinczenko Dec 11, 2017
6a591d1
Added basic wrapper around jest-editor-support Runner
marcinczenko Dec 22, 2017
d567591
Removed a bit of duplication from JestProcess tests
marcinczenko Dec 22, 2017
56cd93f
Added watchMode argument to the JestProcess constructor
marcinczenko Dec 22, 2017
0ce0768
Added JestProcessManager that supports non-watch mode only
marcinczenko Jan 2, 2018
9551f6a
Added support for watchMode
marcinczenko Jan 2, 2018
6d1a973
Passing jestProcess representing the watch mode as the second argumen…
marcinczenko Jan 2, 2018
c6d2a70
JestProcessManager stores jest processes in array
marcinczenko Jan 3, 2018
07a6e16
Added stop method to JestProcess that calls closeProcess on the under…
marcinczenko Jan 3, 2018
5bb442e
Updated jest in devDependencies
marcinczenko Jan 3, 2018
dc9b3f7
Jest Process manager allows stopping the most recent jest process
marcinczenko Jan 3, 2018
8f7e228
Removing references to jest processes that exited on its own
marcinczenko Jan 3, 2018
ef183b6
Added keepAlive flag to jestProcess to control automatic restarting o…
marcinczenko Jan 4, 2018
ebacfcb
Added forwarding call to runJestWithUpdateForSnapshots.
marcinczenko Jan 4, 2018
11fa686
Updated JestExt tests
marcinczenko Jan 4, 2018
b04be74
New process management integrated with JestExt.
marcinczenko Jan 4, 2018
ed8a5b1
shouldIgnoreOutput brought back, but always returns false for now. Ne…
marcinczenko Jan 4, 2018
5e5bca1
Checked with linter
marcinczenko Jan 4, 2018
17fe2de
1) Added support plugin setting runAllTestsFirst. 2) Revised API rega…
marcinczenko Jan 4, 2018
adcced5
Updated CHANGELOG.
marcinczenko Jan 4, 2018
af0d4c1
Fix: handlers are now also assigned to the process running in non-wat…
marcinczenko Jan 7, 2018
e94bbe7
Refactored JestProcesses: exited moved to the closure to keep the sta…
marcinczenko Jan 8, 2018
b6de8cc
Added limit to keepAlive
marcinczenko Jan 8, 2018
265240a
Revert change to shouldIgnoreOutput()
seanpoulter Jan 18, 2018
4e7da5a
onExitCaalback member made private to remove confusion with onExit pu…
marcinczenko Jan 18, 2018
8a59c89
Refactored to make naming more precise
marcinczenko Jan 19, 2018
9c8ecfe
Added restoring jest editor support events when keepAlive is true
marcinczenko Jan 29, 2018
67d1309
Updated how user is notified when process in watch mode restarts too …
marcinczenko Jan 30, 2018
4256eae
Added stopRequested flag to distinguish between failure and explicitl…
marcinczenko Jan 30, 2018
dd97170
restoring jest events before starting runner
marcinczenko Jan 30, 2018
6714a90
Calling testsHaveStartedRunning from one place only
marcinczenko Jan 30, 2018
5727128
JestProcess.stop returns promise to support more sequential flow
marcinczenko Jan 30, 2018
8479a21
Merge branch 'master' into clean-up-jest-process-management
orta Feb 20, 2018
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ Bug-fixes within the same version aren't needed

## Master

* Added `JestProcess` and `JestProcessManager` abstractions to simplify Jest process management - marcinczenko

### 2.6.1

* Strips testNames so they can be used as regex - BLamy
Expand Down
7 changes: 6 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,11 @@
"description": "Whether snapshot previews should show",
"type": "boolean",
"default": true
},
"jest.restartJestOnSnapshotUpdate": {
"description": "Restart Jest runner after updating the snapshots",
"type": "boolean",
"default": false
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be the default behavior @marcinczenko? Based on your description in #179 it seems like we'll always want to restart Jest and don't need the option.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wanted this change to be not invasive so that current users do not get something they do not expect. I am open to making it a default if there is agreement.

}
}
},
Expand Down Expand Up @@ -145,7 +150,7 @@
"danger": "^2",
"glob": "^7.1.1",
"husky": "^0.14.3",
"jest": "^21.2.0",
"jest": "^22.0.4",
"lint-staged": "^4.2.3",
"prettier": "^1.7.3",
"rimraf": "^2.6.2",
Expand Down
1 change: 1 addition & 0 deletions src/IPluginSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ export interface IPluginSettings {
rootPath?: string
runAllTestsFirst?: boolean
showCoverageOnLoad: boolean
restartJestOnSnapshotUpdate?: boolean
}
202 changes: 96 additions & 106 deletions src/JestExt.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as vscode from 'vscode'
import * as path from 'path'
import * as fs from 'fs'
import { Runner, Settings, ProjectWorkspace, JestTotalResults } from 'jest-editor-support'
import { Settings, ProjectWorkspace, JestTotalResults } from 'jest-editor-support'
import { matcher } from 'micromatch'

import * as decorations from './decorations'
Expand All @@ -21,10 +21,10 @@ import { DebugCodeLensProvider } from './DebugCodeLens'
import { DecorationOptions } from './types'
import { hasDocument, isOpenInMultipleEditors } from './editor'
import { CoverageOverlay } from './Coverage/CoverageOverlay'
import { JestProcess, JestProcessManager } from './JestProcessManagement'

export class JestExt {
private workspace: ProjectWorkspace
private jestProcess: Runner
private jestSettings: Settings
private pluginSettings: IPluginSettings

Expand All @@ -50,8 +50,10 @@ export class JestExt {
// We have to keep track of our inline assert fails to remove later
failingAssertionDecorators: { [fileName: string]: vscode.TextEditorDecorationType[] }

private jestProcessManager: JestProcessManager
private jestProcess: JestProcess

private clearOnNextInput: boolean
private forcedClose = false

constructor(workspace: ProjectWorkspace, outputChannel: vscode.OutputChannel, pluginSettings: IPluginSettings) {
this.workspace = workspace
Expand All @@ -68,119 +70,111 @@ export class JestExt {
this.testResultProvider = new TestResultProvider()
this.debugCodeLensProvider = new DebugCodeLensProvider(this.testResultProvider, pluginSettings.enableCodeLens)

this.jestProcessManager = new JestProcessManager({
projectWorkspace: workspace,
runAllTestsFirstInWatchMode: this.pluginSettings.runAllTestsFirst,
})

this.getSettings()
}
// The theme stuff
this.setupDecorators()
// The bottom bar thing
this.setupStatusBar()
//reset the jest diagnostics
resetDiagnostics(this.failDiagnostics)

public startProcess() {
// The Runner is an event emitter that handles taking the Jest
// output and converting it into different types of data that
// we can handle here differently.
if (this.jestProcess) {
this.jestProcess.closeProcess()
delete this.jestProcess
// If we should start the process by default, do so
if (this.pluginSettings.autoEnable) {
this.startProcess()
} else {
this.channel.appendLine('Skipping initial Jest runner process start.')
}
}

let maxRestart = 4
this.jestProcess = new Runner(this.workspace)
private handleStdErr(error: Buffer) {
const message = error.toString()

this.jestProcess
.on('debuggerProcessExit', () => {
this.channel.appendLine('Closed Jest')
if (this.shouldIgnoreOutput(message)) {
return
}

if (this.forcedClose) {
this.forcedClose = false
return
}
// The "tests are done" message comes through stdErr
// We want to use this as a marker that the console should
// be cleared, as the next input will be from a new test run.
if (this.clearOnNextInput) {
this.clearOnNextInput = false
this.parsingTestFile = false
this.channel.clear()
}
// thanks Qix, http://stackoverflow.com/questions/25245716/remove-all-ansi-colors-styles-from-strings
const noANSI = message.replace(/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, '')
if (noANSI.includes('snapshot test failed')) {
this.detectedSnapshotErrors()
}

if (maxRestart-- <= 0) {
console.warn('jest has been restarted too many times, please check your system')
status.stopped('(too many restarts)')
return
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're missing this behavior in the PR. When the Jest process in watch mode fails:

  • the status bar spinner will keep spinning with "Starting watch mode" shown
  • there's no annunciation on the output channel

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is good point. The user should be notified. I will add this behaviour.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Working on it - when the restart threshold has been reached the callback will be called so that we can update the channel and the status bar appropriately.

Copy link
Member Author

@marcinczenko marcinczenko Jan 30, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, so I suggest the following. For processes in watch mode I call exit callback only if they failed for a number of times (JestProcesses.keepAliveLimit). In the response to this, I update the channel with text "Starting Jest in Watch mode failed too many times and has been stopped. Please check your system configuration." and I set the status bar to status.stopped(). This way the user will see that jest is no longer running and should start jest again. We also do not have additional callback and just use one. I will still rebase everything (if needed), and give it a spin locally today to see if nothing unexpected happens.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm...there is still one issue with this. When stopping Jest explicitly, we will get a misleading message in the channel. I suppose I will need to add an argument to the stop handler to mark that it is the result of explicit user action.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, maybe there is a better way to accomplish that without adding any extra argument to the handlers. Let me check.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, done.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One more small thing. Just to see how it feels, on a separate branch I created a version where JestProcess.stop returns a promise. I tested it out when updating snapshots and it feels quite nice. I remember that @connectdotz proposed more synchronous flow. If you think it is better like this, I will probably add a test for this. Let me know. Here is the link to the commit on a separate branch: marcinczenko@5727128

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@seanpoulter I am running it now for two days using the version where JestProcess.stop returns a promise and it feels quite good. Never had any surprise. I will merge the "promise" version to the PR and I hope we will be able to merge it quickly (I would like to avoid another rebasing...). I could then proceed to issue #215 as this effectively prevents snapshots from working (I am testing snapshots by modifying the shouldIgnoreOutput() function).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good. I'll wrap up my own dev efforts then focus on the review. I'm pretty short on spare time these days, so sorry in advance for it taking a while. If anyone else has the bandwidth, feel free to take the lead.

}
this.channel.appendLine(noANSI)
}

this.closeJest()
this.startWatchMode()
})
.on('executableJSON', (data: JestTotalResults) => {
private assignHandlers(jestProcess) {
jestProcess
.onJestEditorSupportEvent('executableJSON', (data: JestTotalResults) => {
this.updateWithData(data)
})
.on('executableOutput', (output: string) => {
.onJestEditorSupportEvent('executableOutput', (output: string) => {
if (!this.shouldIgnoreOutput(output)) {
this.channel.appendLine(output)
}
})
.on('executableStdErr', (error: Buffer) => {
const message = error.toString()

if (this.shouldIgnoreOutput(message)) {
return
}

// The "tests are done" message comes through stdErr
// We want to use this as a marker that the console should
// be cleared, as the next input will be from a new test run.
if (this.clearOnNextInput) {
this.clearOnNextInput = false
this.parsingTestFile = false
this.testsHaveStartedRunning()
}
// thanks Qix, http://stackoverflow.com/questions/25245716/remove-all-ansi-colors-styles-from-strings
const noANSI = message.replace(
/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g,
''
)
if (/snapshot tests? failed/i.test(noANSI)) {
this.detectedSnapshotErrors()
}

this.channel.appendLine(noANSI)
})
.on('nonTerminalError', (error: string) => {
.onJestEditorSupportEvent('executableStdErr', (error: Buffer) => this.handleStdErr(error))
.onJestEditorSupportEvent('nonTerminalError', (error: string) => {
this.channel.appendLine(`Received an error from Jest Runner: ${error.toString()}`)
})
.on('exception', result => {
.onJestEditorSupportEvent('exception', result => {
this.channel.appendLine(`\nException raised: [${result.type}]: ${result.message}\n`)
})
.on('terminalError', (error: string) => {
.onJestEditorSupportEvent('terminalError', (error: string) => {
this.channel.appendLine('\nException raised: ' + error)
})
}

// The theme stuff
this.setupDecorators()
// The bottom bar thing
this.setupStatusBar()
//reset the jest diagnostics
resetDiagnostics(this.failDiagnostics)
public startProcess() {
if (this.jestProcessManager.numberOfProcesses > 0) {
return
}

this.forcedClose = false
// Go!
if (this.pluginSettings.runAllTestsFirst) {
this.jestProcess.start(false)
} else {
this.startWatchMode()
this.testsHaveStartedRunning()
}
}

public stopProcess() {
this.channel.appendLine('Closing Jest jest_runner.')
this.closeJest()
delete this.jestProcess
status.stopped()
}
this.jestProcess = this.jestProcessManager.startJestProcess({
watch: true,
keepAlive: true,
exitCallback: (jestProcess, jestProcessInWatchMode) => {
if (jestProcessInWatchMode) {
this.jestProcess = jestProcessInWatchMode

private closeJest() {
if (!this.jestProcess) {
return
}
this.forcedClose = true
this.jestProcess.closeProcess()
this.channel.appendLine('Finished running all tests. Starting watch mode.')
status.running('Starting watch mode')

this.assignHandlers(this.jestProcess)
} else {
status.stopped()
if (!jestProcess.stopRequested) {
this.channel.appendLine(
'Starting Jest in Watch mode failed too many times and has been stopped. Please check your system configuration.'
)
}
}
},
})

this.assignHandlers(this.jestProcess)
}

private startWatchMode() {
const msg = this.jestProcess.watchMode ? 'Jest exited unexpectedly, restarting watch mode' : 'Starting watch mode'
this.channel.appendLine(msg)
this.jestProcess.start(true)
status.running(msg)
public stopProcess() {
this.channel.appendLine('Closing Jest')
this.jestProcessManager.stopAll()
status.stopped()
}

private getSettings() {
Expand All @@ -191,13 +185,6 @@ export class JestExt {
)
}
this.workspace.localJestMajorVersion = jestVersionMajor

// If we should start the process by default, do so
if (this.pluginSettings.autoEnable) {
this.startProcess()
} else {
this.channel.appendLine('Skipping initial Jest runner process start.')
}
})

// Do nothing for the minute, the above ^ can come back once
Expand All @@ -219,7 +206,14 @@ export class JestExt {
// No response == cancel
if (response) {
this.jestProcess.runJestWithUpdateForSnapshots(() => {
vscode.window.showInformationMessage('Updated Snapshots. It will show in your next test run.')
if (this.pluginSettings.restartJestOnSnapshotUpdate) {
this.jestProcessManager.stopJestProcess(this.jestProcess).then(() => {
this.startProcess()
})
vscode.window.showInformationMessage('Updated Snapshots and restarted Jest.')
} else {
vscode.window.showInformationMessage('Updated Snapshots. It will show in your next test run.')
}
})
}
})
Expand Down Expand Up @@ -345,11 +339,7 @@ export class JestExt {
}

private setupStatusBar() {
if (this.pluginSettings.autoEnable) {
this.testsHaveStartedRunning()
} else {
status.initial()
}
status.initial()
}

private setupDecorators() {
Expand All @@ -360,13 +350,13 @@ export class JestExt {
}

private shouldIgnoreOutput(text: string): boolean {
// this fails when snapshots change - to be revised - returning always false for now
return text.includes('Watch Usage')
}

private testsHaveStartedRunning() {
this.channel.clear()
const details = this.jestProcess && this.jestProcess.watchMode ? 'testing changes' : 'initial full test run'
status.running(details)
status.running('initial full test run')
}

private updateWithData(data: JestTotalResults) {
Expand Down Expand Up @@ -418,7 +408,7 @@ export class JestExt {
}

public deactivate() {
this.jestProcess.closeProcess()
this.jestProcessManager.stopAll()
}

private getJestVersion(version: (v: number) => void) {
Expand Down Expand Up @@ -514,8 +504,8 @@ export class JestExt {
}

public runTest = (fileName: string, identifier: string) => {
const restart = this.jestProcess !== undefined
this.closeJest()
const restart = this.jestProcessManager.numberOfProcesses > 0
this.jestProcessManager.stopAll()
const program = this.resolvePathToJestBin()
if (!program) {
console.log("Could not find Jest's CLI path")
Expand Down
Loading