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
Extract watch mode #2362
Extract watch mode #2362
Conversation
Current coverage is 62.92% (diff: 21.46%)@@ master #2362 diff @@
==========================================
Files 116 123 +7
Lines 4802 4826 +24
Methods 0 0
Messages 0 0
Branches 0 0
==========================================
+ Hits 2990 3037 +47
+ Misses 1812 1789 -23
Partials 0 0
|
@@ -212,6 +215,7 @@ class Runtime { | |||
retainAllFiles: false, | |||
roots: config.testPathDirs, | |||
useWatchman: config.watchman, | |||
watch: (options && options.watch), |
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 you remove the curly braces here? They don't add anything. We are only using it above for maxWorkers because we also use ||
but in the console
case we don't add it.
let testWatcher; | ||
let displayHelp = true; | ||
|
||
jestHasteMap.on('change', ({eventsQueue, hasteFS, moduleMap}) => { |
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 call this hasteMap
? It's called with jest
prefixed in react-native mainly because the codebase there still has a legacy haste map ;)
return null; | ||
} | ||
createDirectory(config.cacheDirectory); | ||
const jestHasteMap = Runtime.createHasteMap(config, { |
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.
nit: hasteMap
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 seems to me like we can get rid of Runtime.createHasteContext
entirely with your PR applied, right?
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.
packages/jest-cli/src/__tests__/SearchSource-test.js
is using it, so I didn't remove it. If you prefer, I can also just move it inside of SearchSource-test.js
as a helper function only for those tests.
testWatcher = new TestWatcher({isWatchMode: true}); | ||
pipe.write(CLEAR); | ||
return jestHasteMap.build().then( | ||
hasteMap => ({ |
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 could potentially move createHasteContext
into a utility function inside of jest-cli and do this:
const createHasteContext = (config, {hasteFS, moduleMap}) => {
hasteFS,
resolver: Runtime.createResolver(config, moduleMap),
});
this way we can reuse this "map" operation of the code in two places. I think it makes sense, this may change in the future and I'd like to reuse code if possible :)
}, | ||
) | ||
.then(hasteContext => { | ||
if (argv.debug) { |
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 move this before creating the haste map again? I don't think we need to create the haste map before logging this. Could also factor this if out into a logDebugMessages
.
* @emails oncall+jsinfra | ||
*/ | ||
|
||
const getMaxWorkers = require('../getMaxWorkers'); |
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.
omg tests. So good!
|
||
const realFs = require('fs'); | ||
const fs = require('graceful-fs'); | ||
fs.gracefulify(realFs); |
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 don't think we need this here, it already happens in jest.js
which should be the entry point for 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.
We can simply use const fs = require('graceful-fs');
right?
|
||
const realFs = require('fs'); | ||
const fs = require('graceful-fs'); | ||
fs.gracefulify(realFs); |
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.
same here, we don't need this
const TestWatcher = require('./TestWatcher'); | ||
const runJest = require('./runJest'); | ||
const setWatchMode = require('./lib/setWatchMode'); | ||
const HasteMap = require('jest-haste-map'); |
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 you sort these alphabetically?
@@ -35,7 +35,7 @@ export type InternalHasteMap = {| | |||
export type HasteMap = {| | |||
hasteFS: HasteFS, | |||
moduleMap: ModuleMap, | |||
__hasteMapForTest: ?InternalHasteMap, | |||
__hasteMapForTest?: ?InternalHasteMap, |
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.
@cpojer this should be optional right?
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.
yeah I guess this is fine.
@@ -86,7 +86,9 @@ const watch = ( | |||
results => { | |||
isRunning = false; | |||
hasSnapshotFailure = !!results.snapshot.failure; | |||
testWatcher.setState({interrupted: false}); | |||
if (config.bail) { |
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 think the test interruption issue was introduced here #2312 and it is happening in master...
@cpojer Is this the right fix? I think that we should only set {interrupted: false}
when using bail
... I decided to fix it as part of this PR but it's fine if you prefer me to do it as a separate one that just fixes the test interruption bug.
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, seems like my fix wasn't super thorough. I need to think about this, for now using config.bail
as a flag seems reasonable. Do you mind opening another issue linking to this piece in the code? I'll take care of it at a later time.
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.
Filed it here: #2385
f2b637b
to
3063f80
Compare
With this change we don't store the haste map on disk before starting to run tests. This means if Jest would like to run tests in parallel, we need to persist the haste map or pass it through to the workers, otherwise new files that get added can't be run and will fail. I spent some time running some benchmarks on passing data to and back from the worker, see https://gist.github.com/cpojer/488c85f2cf86cacc3f4cc378be3f68eb This version takes about 6s on my mbp and about 1.3s when only sending data to the worker only (like we'd do in Jest). This is still a lot of overhead. One issue with persisting the state is also that the watchman clocks aren't updated through the file watcher – this means that storing the haste map as is will result in a recrawl of the file system on the next startup. There are a number of ways to address this:
I'm inclined to go with the first solution and benchmarking it on our internal react-native codebase, which is relatively big, to see what the performance will look like. Either way, I need to find a way to write tests for this as it is easy to break. I'll push directly to this PR and merge it once done. @rogeliog nice work on making this all happen! |
This reverts commit ba4baec.
hasteMap.on('change', ({eventsQueue, hasteFS, moduleMap}) => { | ||
if (eventsQueue.find(({type}) => type !== 'change')) { | ||
hasteContext = createHasteContext(config, {hasteFS, moduleMap}); | ||
} |
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 always need a new instance of the hasteContext not just when they get added/deleted. At Facebook, if a module's "@providesModule" header changes, the module receives a new name for our "haste" module system. cc @rogeliog
if (watcher.isInterrupted()) { | ||
return Promise.reject(); | ||
} | ||
this._dispatcher.onTestStart(config, path); | ||
return promisify(farm)({config, path}); | ||
return worker({config, path}); |
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.
just nits here, not really fixes.
Ok I tested this and it works now. @rogeliog mind checking my code before I merge this? Here is what this solves:
HasteModule.js
Without my last commit, even after creating the "HasteModule.js" file, the test would fail and be unable to locate the new haste module (haste is Facebook's module resolution system) because we don't persist the haste map to disk so when we read it in the worker thread, it is still missing. My change makes it so that in watch mode, the module map is always passed to the workers and it is only read from disk in the regular mode. One thing I haven't had time to do is write tests. It would be good to create some tests with some of the env mocked, like a |
@cpojer Code looks good, I've been working on adding the tests that you mentioned and doing some good progress there... |
|
||
const runner = new TestRunner(hasteContext, config, {maxWorkers: 2}); | ||
|
||
return runner._createParallelTestRun( |
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 decided to tests _createParallelTestRun
instead of runTests
even though it is a private method.
I did this because the current implementation of runTests
is a harder to test, mainly because of all of the extra logic that the inner callbacks have. If you prefer I can also spend some time refactoring some of it to make runTests
easier to test :)
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.
Yeah I was thinking that as well. We could do that as a follow-up. TestRunner is one of the oldest parts of Jest and has been through a lot. I'm open to any ideas on how to improve the code to make it more testable. Feel free to create an issues to discuss or a PR to fix.
One thing that is untested as well is the performance test cache and that Jest runs failed tests first on a re-run. This all may break at any time. I'm really grateful you are adding such extensive unit tests for the watch mode, as it was entirely untested previously.
@@ -93,7 +75,7 @@ const watch = ( | |||
// and prevent test runs from the previous run. | |||
testWatcher = new TestWatcher({isWatchMode: true}); | |||
if (displayHelp) { | |||
console.log(usage(argv, hasSnapshotFailure)); | |||
pipe.write(usage(argv, hasSnapshotFailure)); |
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.
@cpojer I changed this and 7909aeb#diff-d8a3d0a3a8b42ab87b18835ad8f7cb46L177 to use pipe.write
instead of console.log
for testability purposes, but I'm unsure if this has any impact in on how things are printed out that I'm unaware of... I'll keep investigating tomorrow
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 fine. Unsure why we used console.log here in the first place.
* Add watch option to HastMapOptions * wip * Extract runJest and other utility functions * Start to extract watch to its own module * Fix flow and lint errors * Add tests to getMaxWorkers and setWatchMode * Use createHasteContext instead of custom method * Keep haste context up to date FS add/remove This reverts commit ba4baec. * Fix flow and eslint errors * Fix caps in require * Extract createHasteContext and logDebugMessages * Alphabetize requires and remove extra graceful-fs requires * Fix test interrupution issue with --bail * Fixes for new watch mode. * Pass the raw module map from the test runner to the test worker in watch mode. * Add test for injecting rawModuleMap to workers * Add tests for watch mode and extract KEYS to contstants module
* Add watch option to HastMapOptions * wip * Extract runJest and other utility functions * Start to extract watch to its own module * Fix flow and lint errors * Add tests to getMaxWorkers and setWatchMode * Use createHasteContext instead of custom method * Keep haste context up to date FS add/remove This reverts commit ba4baec. * Fix flow and eslint errors * Fix caps in require * Extract createHasteContext and logDebugMessages * Alphabetize requires and remove extra graceful-fs requires * Fix test interrupution issue with --bail * Fixes for new watch mode. * Pass the raw module map from the test runner to the test worker in watch mode. * Add test for injecting rawModuleMap to workers * Add tests for watch mode and extract KEYS to contstants module
This pull request has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs. |
Summary
This extracts the watch mode to its own module, which was suggested by @cpojer as a prerequisite for #2324.
This should make watch mode faster and removes some duplicated code from
jest.js
.Now we only need to reconcile with the file system only when a file is added/removed, but not otherwise
More info #2324 (comment) I hope that this will also allow us to add more tests in the future.
Known issues:
Test interruption is not working and I'm not sure why. It seems that I'm setting the correct state in theUPDATE: I first thought that the issue was introduced in this PR, but it seems that it is also happening in master. Based on my understanding it was introduced in #2312TestWatcher
:(