-
Notifications
You must be signed in to change notification settings - Fork 294
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
Clean up jest process management #215
Conversation
82ac478
to
beed961
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@seanpoulter I'll leave merging to you, looks great for me
src/JestExt.ts
Outdated
}) | ||
.onJestEditorSupportEvent('terminalError', (error: string) => { | ||
this.channel.appendLine('\nException raised: ' + error) | ||
}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
very elegant 👍
src/JestExt.ts
Outdated
this.startProcess() | ||
vscode.window.showInformationMessage('Updated Snapshots and restarted Jest.') | ||
} else { | ||
vscode.window.showInformationMessage('Updated Snapshots. It will show in your next test run.') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Lovely touch
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
src/JestExt.ts
Outdated
private shouldIgnoreOutput(_: string): boolean { | ||
// this fails when snapshots change - to be revised - returning always false for now | ||
// return text.includes('Watch Usage') | ||
return false |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
want to do anything about this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes. I work continuously with snapshots so I will keep eye on it. I have to look a bit closer on how output is being processed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any progress on this one @marcinczenko? I'd prefer if we kept the existing behavior that's hiding the "Watch Usage" prompt and open a new issue to fix the problem when the snapshots change. Want me to see if we can create a test case for it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, no really progress on it. I was a bit idle waiting for this PR to be finished and merged. It takes very long time and it is a bit demotivating so I started to put my attention somewhere else for a moment. I also wasn't sure what will be the effect of some other investigations on how to communicate with Jest Process that was mentioned in some other discussions before. For this reason I did not rush up. I am less concerned with the console output, which I found buggy anyway - it seems to swallow lots of the contents because of this "Watch Usage" filtering? I was more concerned that Snapshots work. So maybe we keep snapshots working and we open the issue on filtering the output, which I think requires review anyway.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It takes very long time and it is a bit demotivating
Yea, sorry for the delay. Life got busy, and when I did find some time it wasn't enough to get through a PR of this size. It's even slower when you aren't familiar with the new or old code, and we don't have integration tests to make sure we've preserved behavior.
I am less concerned with the console output, which I found buggy anyway - it seems to swallow lots of the contents because of this "Watch Usage" filtering?
That's fair. I've reverted it and opened #228 for the filtering. We filter out the Watch Usage prompt since it's extra noise we don't need to see. It shouldn't swallow anything else.
expect(onExit).toHaveBeenCalledTimes(2) | ||
}) | ||
}) | ||
}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
very impressive
expect(jestProcessMock.mock.calls[0][0]).toHaveProperty('watchMode', true) | ||
}) | ||
}) | ||
}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
also very impressive
👍
src/JestExt.ts
Outdated
this.jestProcessManager.startJestProcess({ | ||
watch: true, | ||
keepAlive: true, | ||
exitCallback: (_, jestProcessInWatchMode) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe I am missing something, this looks to me that all the jest output are only processed upon process exit... but during watch mode, the process will not exit isn't it? does the output get processed upon change?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is good one. Actually it is other way around. I have assigned the handlers correctly for the watch mode (the exit handler for the first process - the one running all the tests first - provides two arguments: one for the process that just exited, second for the jest process for the watch mode that just started), but I did not assigned them for the first process that runs all the tests first.
I have just fixed it - please see the most recent commit.
public watchMode: boolean | ||
|
||
private startRunner() { | ||
this.exited = false |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you might encounter a race condition here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
startRunner
is a private method. It is only called in two circumstances:
- When the
JestProcess
instance is created (startRunner
is called in the constructor). - From method
restart
, which is also private and called only after the process already exited. Note thatrestart
removes all the handlers from the underlyingRunner
before callingstartRunner
.
So, no race condition as far as I can see, but please elaborate if you thought about something else.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if this.exited
is only used by the closure below, just make it a local variable, it will be captured by the closure.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is related to the other comment below. Making it part of the closure would actually indicate that we are protecting it from something at that would be misleading.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the code is cleaner when there is no state, or at least we should keep state as tight as possible, definitely avoid unnecessary state beyond its context.
Suggested change:
private startRunner() {
let exited = false // here
this.runner = new Runner(this.projectWorkspace)
this.runner.start(this.watchMode)
this.runner.on('debuggerProcessExit', () => {
if (!exited) { //here
exited = true // here
this.onExitCallback(this)
if (this.keepAlive) {
this.restart()
}
}
})
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, the argument about keeping the state to the minimum is speaking to me. I will add that.
private restart() { | ||
this.runner.removeAllListeners() | ||
this.startRunner() | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should restart() stop the running process before starting?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, I suppose. restart
is a private method and it is called only internally when process already exited.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see, if it is only used by startRunner()
and depends on that context, maybe better off making this method an inner function within startRunner()
, it will prevent accidental misuse and confusion for future maintenance.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I do not know... Isn't private
not communicating that it is...private? What other people think? Could you please give an example of how you would like it to be?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it is indeed private, but not for the context of the JestProcess class, but for the startRunner()
method, because it only makes sense to be invoked after the process is exited as you indicated. Similar to the comment above, we need to be tight about scope/context to avoid confusion and future maintenance overhead.
Suggested change:
private startRunner() {
// move the function here or inline will be fine too
function restart() {
this.runner.removeAllListeners()
this.startRunner()
}
this.exited = false
this.runner = new Runner(this.projectWorkspace)
this.runner.start(this.watchMode)
this.runner.on('debuggerProcessExit', () => {
if (!this.exited) {
this.exited = true
this.onExitCallback(this)
if (this.keepAlive) {
restart() //here
}
}
})
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
one more question, does the keepAlive has a boundary? I have seen sometimes jest can't start, without the upper bound, will the process just keep spawning jest process until it exhausted all system resources?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- In-line functions is not me, but I do not have time to argue about it. I will add your change (and close my eyes when typing :)).
- Adding a boundary is a good thing. I will add that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In-line functions is not me
I don't like nested function either, but I dislike out-of-context/running-away-state OOP more ;-)... good news is we don't need a nested function here at all, giving it is only used in 1 place and is only 2 lines, you can further simplify it:
private startRunner() {
let exited = false
this.runner = new Runner(this.projectWorkspace)
this.runner.start(this.watchMode)
this.runner.on('debuggerProcessExit', () => {
if (!exited) {
exited = true
this.onExitCallback(this)
if (this.keepAlive) {
// here, even simplier...
this.runner.removeAllListeners()
this.startRunner()
}
}
})
}
hopefully that will keep your eyes open when you type... ;-)
Is there anything that is still blocking this from being merged? Shall I squash and make a ready to merge commit? |
If anyone else wants to take on the merge, feel free. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for all the work that went into this @marcinczenko and your patience for me to find time to step through the details. It's looking pretty good, but there are a few changes I think we should address before we merge. The highest priority to me is catching when Jest has restarted too many times and getting feedback to the user. There have been quite a few issues opened from folks who can't tell what's happened.
src/JestExt.ts
Outdated
private shouldIgnoreOutput(_: string): boolean { | ||
// this fails when snapshots change - to be revised - returning always false for now | ||
// return text.includes('Watch Usage') | ||
return false |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any progress on this one @marcinczenko? I'd prefer if we kept the existing behavior that's hiding the "Watch Usage" prompt and open a new issue to fix the problem when the snapshots change. Want me to see if we can create a test case for it?
"jest.restartJestOnSnapshotUpdate": { | ||
"description": "Restart Jest runner after updating the snapshots", | ||
"type": "boolean", | ||
"default": false |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
src/JestExt.ts
Outdated
this.startProcess() | ||
vscode.window.showInformationMessage('Updated Snapshots and restarted Jest.') | ||
} else { | ||
vscode.window.showInformationMessage('Updated Snapshots. It will show in your next test run.') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
private runner: Runner | ||
private projectWorkspace: ProjectWorkspace | ||
public keepAliveCounter: number | ||
public onExitCallback: Function |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we switch this to private
so we don't get it confused with the onExit(callback)
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should be possible :).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
|
||
private startJestProcessInWatchMode(exitCallback, keepAlive) { | ||
return this.handleNonWatchMode(true, exitCallback, keepAlive) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm confused by the contradicting method names here with "watch mode" and "non-watch mode" together. What's your reasoning behind them @marcinczenko?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, nothing special - just the effect of refactoring and apparently I did not realised that the naming is not really to the point. I will give it a better name.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I refactored it a bit. Do you think it is better now?
} | ||
|
||
private onJestProcessExit(jestProcess, exitCallback, keepAlive) { | ||
const jestProcessInWatchMode = this.startJestProcessInWatchMode(exitCallback, keepAlive) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we call handleNonWatchMode
inline here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, after I refactor the naming it might be better to inline it.
this.projectWorkspace = projectWorkspace | ||
this.keepAliveCounter = keepAlive ? JestProcess.keepAliveLimit : 1 | ||
|
||
this.startRunner() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How do you feel about moving this out of the constructor to separate concerns?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This one I would prefer to keep in the constructor. From my perspective it simplifies the design by removing some ugly corner cases against which I would have to perform additional checks. I like the fact that thanks to this API is very narrow and safe.
If this really feels not right for you, let me know. Then I will try to reorganise things a bit. On the other hand I am not convinced if this is right investment at the moment. There are other things I would prefer to do, and this feels really good enough. JestProcess
will most probably undergo significant changes anyway when we find a better way to talk to jest.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This one I would prefer to keep in the constructor. ... There are other things I would prefer to do, and this feels really good enough.
Sure, let's leave it for now.
if (maxRestart-- <= 0) { | ||
console.warn('jest has been restarted too many times, please check your system') | ||
status.stopped('(too many restarts)') | ||
return |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, done.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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.
src/JestExt.ts
Outdated
} | ||
this.forcedClose = true | ||
this.jestProcess.closeProcess() | ||
this.assignHandlers(this.jestProcess) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When watch mode fails, this output is only assigned once.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You might be right. Let me investigate this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You are right @seanpoulter. So I want to avoid adding more callbacks and taken that we already have separate JestProcess
to deal with this kind of things, I will record registered callbacks and restore them when process is restarted inside JestProcess
class. Feels like a right place to handle that. I will have it tomorrow.
Let me know if I can help with any of those comments @marcinczenko. I'm happy to dig in and send you a PR. |
watch: watch, | ||
keepAlive: keepAlive, | ||
exitCallback: exitCallback, | ||
}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @marcinczenko. That's a lot easier to read.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@seanpoulter Sorry for small delay on my side this time. The remaining changes are on the way!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't be sorry @marcinczenko. It's a big change! After I wrap up some integration tests for jest-editor-support
, I'll try and write some integration tests for the status bar and output channel.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@seanpoulter Just added restoring jest editor support events. These are not really big changes, but to write tests always requires a bit more focus. The last thing to add is to notify the user that the allowed number of Jest process restarts has been reached. I hope to have it done by the end of Monday. Then I think we will be good to go ;).
…t of the exit handler for the jest process running all the tests
…f the jestProcess when it exits without being explicitly stopped
c8f0324
to
67d1309
Compare
b44f8f4
to
6714a90
Compare
private startRunner() { | ||
this.stopRequested = false | ||
let exited = false | ||
this.runner = new Runner(this.projectWorkspace) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be useful to create the Runner with the shell option enabled (requires jest-editor-support 22.1.3) at least on Windows in order to fix #98.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi @stephtr. Thanks for pointing this out. I will take a look. Hopefully, we all got some slack very soon to start review.
@seanpoulter think you can get time to review this? Otherwise I'll take a serious look at it tomorrow 👍 |
You might get to it first @orta. Just wrapping up my first sprint at a new job and have been spending my spare time getting up to speed with it. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry this took so long to get through @marcinczenko - you did a great job explaining your changes, and the code definitely reads much better
Great job!
Hurrah! I'll second that, thanks @marcinczenko for the thorough PR. 🎉 |
Wow! I am very glad it went through 🍿. Big thanks to @connectdotz, @orta, and @seanpoulter for taking your time reviewing it and giving feedback. Now I can start dealing with other issues - especially those related to snapshots. We have a bit busy time at work at the moment, but very soon I should be able to do some work. |
I've invited you to the org because this is a great big contribution, you're welcome to contribute as little or as much as you like - we'll be here to support you 👍 |
…-process-management Clean up jest process management
I added two basic abstractions to make jest process management easier to managed:
JestProcess
andJestProcessManager
.JestProcess
job is to isolate the rest of the extension from the underlyingjest-editor-support
. It takes care for starting and stoping of the underlyingRunner
fromjest-editor-support
. It also handlesrestarting
if the underlying process exits unintentionally (i.e. without being explicitly stopped) and filters out redundant events fordebuggerProcessExit
(only first event is handled, the remaining are ignored).JestProcessManager
manages multiple instances ofJestProcess
. In our current setup we will always have one process running at a time, butJestProcessManager
will handle multiple process as well.JestProcessManager
also takes care for the watch mode whenrunAllTestsFirst
plugin setting istrue
. In such a case, it will first start aJestProcess
to run all the tests, and when it finishes, it will automatically create anotherJestProcess
in the watch mode.Both abstractions have accompanying tests which should also document the actual usage.
This pull-request, also adds possibility to restart Jest when snapshots get updated. This is controlled by the plugin setting:
restartJestOnSnapshotUpdate
.