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

Extract watch mode #2362

Merged
merged 17 commits into from Jan 8, 2017
Merged

Extract watch mode #2362

merged 17 commits into from Jan 8, 2017

Conversation

rogeliog
Copy link
Contributor

@rogeliog rogeliog commented Dec 19, 2016

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 the TestWatcher :( UPDATE: 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 #2312

interrupt

@codecov-io
Copy link

codecov-io commented Dec 19, 2016

Current coverage is 62.92% (diff: 21.46%)

Merging #2362 into master will increase coverage by 0.66%

@@             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          

Powered by Codecov. Last update 87cb544...16b2780

@@ -212,6 +215,7 @@ class Runtime {
retainAllFiles: false,
roots: config.testPathDirs,
useWatchman: config.watchman,
watch: (options && options.watch),
Copy link
Member

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}) => {
Copy link
Member

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, {
Copy link
Member

Choose a reason for hiding this comment

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

nit: hasteMap

Copy link
Member

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?

Copy link
Contributor Author

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 => ({
Copy link
Member

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) {
Copy link
Member

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');
Copy link
Member

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);
Copy link
Member

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.

Copy link
Contributor Author

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);
Copy link
Member

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');
Copy link
Member

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,
Copy link
Contributor Author

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?

Copy link
Member

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) {
Copy link
Contributor Author

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.

Copy link
Member

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.

Copy link
Member

Choose a reason for hiding this comment

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

Filed it here: #2385

@cpojer
Copy link
Member

cpojer commented Jan 5, 2017

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:

  • Ignore the overhead this incurs. Fast tests will run in the same process and slow tests (3+ seconds or so) run in parallel. The added overhead isn't that big of a deal on an already slow test run. This could be optimized by only sending the data of the module map (about half the amount of data).
  • Split up the module map and haste fs data into separate cache files and only write the module map one. This should allow to properly reconcile the cached data with information from watchman on the next startup.

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!

hasteMap.on('change', ({eventsQueue, hasteFS, moduleMap}) => {
if (eventsQueue.find(({type}) => type !== 'change')) {
hasteContext = createHasteContext(config, {hasteFS, moduleMap});
}
Copy link
Member

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});
Copy link
Member

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.

@cpojer
Copy link
Member

cpojer commented Jan 7, 2017

Ok I tested this and it works now. @rogeliog mind checking my code before I merge this?

Here is what this solves:

HasteModule.js

/**
 * @providesModule HasteModule
 */
module.exports = () => 'banana';

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 worker-farm mock (similar to the tests in jest-haste-map) and a test that ensures that when TestWatcher's state is set to watch mode, it passes the raw module map and otherwise it doesn't. @rogeliog is this something you could help out with to get this PR over the finish line?

@rogeliog
Copy link
Contributor Author

rogeliog commented Jan 8, 2017

@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(
Copy link
Contributor Author

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 :)

Copy link
Member

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));
Copy link
Contributor Author

@rogeliog rogeliog Jan 8, 2017

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

Copy link
Member

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.

@cpojer cpojer merged commit b2b2039 into jestjs:master Jan 8, 2017
@rogeliog rogeliog deleted the extract-watch branch January 9, 2017 06:59
skovhus pushed a commit to skovhus/jest that referenced this pull request Apr 29, 2017
* 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
tushardhole pushed a commit to tushardhole/jest that referenced this pull request Aug 21, 2017
* 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
@github-actions
Copy link

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.
Please note this issue tracker is not a help forum. We recommend using StackOverflow or our discord channel for questions.

@github-actions github-actions bot locked as resolved and limited conversation to collaborators May 14, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants