Skip to content

Commit

Permalink
Add retries to the crawler.
Browse files Browse the repository at this point in the history
  • Loading branch information
cpojer committed Apr 28, 2016
1 parent 13abace commit 46f1a10
Show file tree
Hide file tree
Showing 7 changed files with 140 additions and 42 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
## master

## jest-cli 12.0.3

* Added a fallback to the watchman crawler.

## jest-cli 12.0.2

* Bug fixes when running a single test file and for scoped package names.
Expand Down
16 changes: 0 additions & 16 deletions docs/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@ Jest uses Jasmine 2 by default. An introduction to Jasmine 2 can be found

- [`automock` [boolean]](#automock-boolean)
- [`bail` [boolean]](#bail-boolean)
- [`cache` [boolean]](#cache-boolean)
- [`cacheDirectory` [string]](#cachedirectory-string)
- [`coverageDirectory` [string]](#coveragedirectory-string)
- [`collectCoverage` [boolean]](#collectcoverage-boolean)
Expand All @@ -73,7 +72,6 @@ Jest uses Jasmine 2 by default. An introduction to Jasmine 2 can be found
- [`testRunner` [string]](#testrunner-string)
- [`unmockedModulePathPatterns` [array<string>]](#unmockedmodulepathpatterns-array-string)
- [`verbose` [boolean]](#verbose-boolean)
- [`watchman` [boolean]](#watchman-boolean)

#### Globally injected variables

Expand Down Expand Up @@ -375,13 +373,6 @@ mock modules using `jest.mock(moduleName)`.

By default, Jest runs all tests and produces all errors into the console upon completion. The bail config option can be used here to have Jest stop running tests after the first failure.

### `cache` [boolean]
(default: true)

By default, Jest caches heavily to speed up subsequent test runs. Sometimes this
may not be desirable and can be turned off. *Note: it is generally better
not to disable this feature, but rather run Jest with `--no-cache` once.*

### `cacheDirectory` [string]
(default: 'jest-cli/.haste_cache')

Expand Down Expand Up @@ -598,10 +589,3 @@ It is possible to override this setting in individual tests by explicitly callin
(default: `false`)

Indicates whether each individual test should be reported during the run. All errors will also still be shown on the bottom after execution.

### `watchman` [boolean]
(default: `true`)

[Watchman](https://facebook.github.io/watchman/) monitors the file system for
changes and is used by Jest for crawling for files. Disable this if you cannot
use watchman or use the `--no-watchman` flag.
6 changes: 1 addition & 5 deletions packages/jest-haste-map/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,11 @@
"license": "BSD-3-Clause",
"main": "src/index.js",
"dependencies": {
"denodeify": "^1.2.1",
"fb-watchman": "^1.9.0",
"graceful-fs": "^4.1.3"
},
"jest": {
"testEnvironment": "node",
"unmockedModulePathPatterns": [
"denodeify"
]
"testEnvironment": "node"
},
"scripts": {
"test": "../../bin/jest.js"
Expand Down
80 changes: 79 additions & 1 deletion packages/jest-haste-map/src/__tests__/index-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -289,7 +289,7 @@ describe('HasteMap', () => {
.toEqual('/fruits/strawberry.js');

expect(console.warn).toBeCalledWith([
'@providesModule naming collision:',
'jest-haste-map: @providesModule naming collision:',
' Duplicate module name: Strawberry',
' Paths: /fruits/raspberry.js collides with /fruits/strawberry.js',
'',
Expand Down Expand Up @@ -477,4 +477,82 @@ describe('HasteMap', () => {
});
});

pit('tries to crawl using node as a fallback', () => {
const watchman = require('../crawlers/watchman');
const node = require('../crawlers/node');

watchman.mockImplementation(() => {
throw new Error('watchman error');
});
node.mockImplementation((roots, extensions, ignore, data) => {
data.files = object({
'/fruits/banana.js': ['', 32, 0, []],
});
return Promise.resolve(data);
});

return new HasteMap(defaultConfig).build().then(data => {
expect(watchman).toBeCalled();
expect(node).toBeCalled();

expect(data.files).toEqual({
'/fruits/banana.js': ['Banana', 32, 1, ['Strawberry']],
});

expect(console.warn).toBeCalledWith(
'jest-haste-map: Watchman crawl failed. Retrying once with node ' +
'crawler.\n Error: watchman error'
);
});
});

pit('tries to crawl using node as a fallback when promise fails once', () => {
const watchman = require('../crawlers/watchman');
const node = require('../crawlers/node');

watchman.mockImplementation(() => {
return Promise.reject(new Error('watchman error'));
});
node.mockImplementation((roots, extensions, ignore, data) => {
data.files = object({
'/fruits/banana.js': ['', 32, 0, []],
});
return Promise.resolve(data);
});

return new HasteMap(defaultConfig).build().then(data => {
expect(watchman).toBeCalled();
expect(node).toBeCalled();

expect(data.files).toEqual({
'/fruits/banana.js': ['Banana', 32, 1, ['Strawberry']],
});
});
});

pit('stops crawling when both crawlers fail', () => {
const watchman = require('../crawlers/watchman');
const node = require('../crawlers/node');

watchman.mockImplementation(() => {
return Promise.reject(new Error('watchman error'));
});

node.mockImplementation((roots, extensions, ignore, data) => {
return Promise.reject(new Error('node error'));
});

return new HasteMap(defaultConfig).build()
.then(
() => expect(() => {}).toThrow(),
error => {
expect(error.message).toEqual(
'Crawler retry failed:\n' +
' Original error: watchman error\n' +
' Retry error: node error\n'
);
}
);
});

});
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ jest.mock('fb-watchman', () => {
}
jest.runAllTimers();
});
Client.prototype.on = jest.fn();
Client.prototype.end = jest.fn();
return {Client};
});
Expand Down Expand Up @@ -100,6 +101,9 @@ describe('watchman watch', () => {
const client = watchman.Client.mock.instances[0];
const calls = client.command.mock.calls;

expect(client.on).toBeCalled();
expect(client.on).toBeCalledWith('error', jasmine.any(Function));

// Call 0 and 1 are for ['watch-project']
expect(calls[0][0][0]).toEqual('watch-project');
expect(calls[1][0][0]).toEqual('watch-project');
Expand Down
13 changes: 11 additions & 2 deletions packages/jest-haste-map/src/crawlers/watchman.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@

const H = require('../constants');

const denodeify = require('denodeify');
const path = require('../fastpath');
const watchman = require('fb-watchman');

Expand All @@ -30,7 +29,17 @@ function WatchmanError(error) {

module.exports = function watchmanCrawl(roots, extensions, ignore, data) {
const client = new watchman.Client();
const cmd = denodeify(client.command.bind(client));
const cmd = args => new Promise((resolve, reject) => {
client.on('error', error => reject(error));
client.command(args, (error, result) => {
if (error) {
reject(error);
} else {
resolve(result);
}
});
});

const clocks = data.clocks;
let files = data.files;

Expand Down
59 changes: 41 additions & 18 deletions packages/jest-haste-map/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
const H = require('./constants');

const crypto = require('crypto');
const denodeify = require('denodeify');
const execSync = require('child_process').execSync;
const fs = require('graceful-fs');
const getPlatformExtension = require('./lib/getPlatformExtension');
Expand Down Expand Up @@ -216,7 +215,7 @@ class HasteMap {
const existingModule = moduleMap[platform];
if (existingModule && existingModule[H.PATH] !== module[H.PATH]) {
console.warn(
`@providesModule naming collision:\n` +
`jest-haste-map: @providesModule naming collision:\n` +
` Duplicate module name: ${id}\n` +
` Paths: ${module[H.PATH]} collides with ` +
`${existingModule[H.PATH]}\n\n` +
Expand Down Expand Up @@ -293,25 +292,28 @@ class HasteMap {
*/
_getWorker() {
if (!this._workerPromise) {
let workerFn;
if (this._options.maxWorkers === 1) {
this._workerPromise = message => new Promise(
(resolve, reject) => worker(message, (error, metadata) => {
if (error) {
reject(error);
} else {
resolve(metadata);
}
})
);
workerFn = worker;
} else {
this._workerFarm = workerFarm(
{
maxConcurrentWorkers: this._options.maxWorkers,
},
require.resolve('./worker')
);
this._workerPromise = denodeify(this._workerFarm);
workerFn = this._workerFarm;
}

this._workerPromise = message => new Promise(
(resolve, reject) => workerFn(message, (error, metadata) => {
if (error) {
reject(error);
} else {
resolve(metadata);
}
})
);
}

return this._workerPromise;
Expand All @@ -326,15 +328,36 @@ class HasteMap {
}

_crawl(hasteMap) {
const options = this._options;
const ignore = this._ignore.bind(this);
const crawl =
(canUseWatchman && this._options.useWatchman) ? watchmanCrawl : nodeCrawl;

return crawl(
this._options.roots,
this._options.extensions,
this._ignore.bind(this),
hasteMap
);
const retry = error => {
if (crawl === watchmanCrawl) {
console.warn(
`jest-haste-map: Watchman crawl failed. Retrying once with node ` +
`crawler.\n ${error}`
);
return nodeCrawl(options.roots, options.extensions, ignore, hasteMap)
.catch(e => {
throw new Error(
`Crawler retry failed:\n` +
` Original error: ${error.message}\n` +
` Retry error: ${e.message}\n`
);
});
}

throw error;
};

try {
return crawl(options.roots, options.extensions, ignore, hasteMap)
.catch(retry);
} catch (error) {
return retry(error);
}
}

_ignore(filePath) {
Expand Down

0 comments on commit 46f1a10

Please sign in to comment.