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

fix(jest-haste-map): Make watchman existence check lazy+async #12675

Merged
merged 5 commits into from Apr 16, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -74,6 +74,7 @@
- `[jest-environment-node]` Add `structuredClone` to globals ([#12631](https://github.com/facebook/jest/pull/12631))
- `[@jest/expect-utils]` [**BREAKING**] Fix false positives when looking for `undefined` prop ([#8923](https://github.com/facebook/jest/pull/8923))
- `[jest-haste-map]` Don't use partial results if file crawl errors ([#12420](https://github.com/facebook/jest/pull/12420))
- `[jest-haste-map]` Make watchman existence check lazy+async ([#12675](https://github.com/facebook/jest/pull/12675))
- `[jest-jasmine2, jest-types]` [**BREAKING**] Move all `jasmine` specific types from `@jest/types` to its own package ([#12125](https://github.com/facebook/jest/pull/12125))
- `[jest-jasmine2]` Do not set `duration` to `0` for skipped tests ([#12518](https://github.com/facebook/jest/pull/12518))
- `[jest-matcher-utils]` Pass maxWidth to `pretty-format` to avoid printing every element in arrays by default ([#12402](https://github.com/facebook/jest/pull/12402))
Expand Down
14 changes: 11 additions & 3 deletions packages/jest-haste-map/src/__tests__/index.test.js
Expand Up @@ -13,9 +13,11 @@ function mockHashContents(contents) {
return crypto.createHash('sha1').update(contents).digest('hex');
}

jest.mock('child_process', () => ({
// If this does not throw, we'll use the (mocked) watchman crawler
execSync() {},
const mockIsWatchmanInstalled = jest.fn().mockResolvedValue(true);

jest.mock('../lib/isWatchmanInstalled', () => ({
__esModule: true,
default: mockIsWatchmanInstalled,
}));

jest.mock('jest-worker', () => ({
Expand Down Expand Up @@ -612,6 +614,8 @@ describe('HasteMap', () => {
});
});

mockIsWatchmanInstalled.mockClear();

const hasteMap = await HasteMap.create({
...defaultConfig,
computeSha1: true,
Expand All @@ -621,6 +625,10 @@ describe('HasteMap', () => {

const data = (await hasteMap.build()).__hasteMapForTest;

expect(mockIsWatchmanInstalled).toHaveBeenCalledTimes(
useWatchman ? 1 : 0,
);

expect(data.files).toEqual(
createMap({
[path.join('fruits', 'Banana.js')]: [
Expand Down
39 changes: 20 additions & 19 deletions packages/jest-haste-map/src/index.ts
Expand Up @@ -7,7 +7,6 @@

/* eslint-disable local/ban-types-eventually */

import {execSync} from 'child_process';
import {createHash} from 'crypto';
import {EventEmitter} from 'events';
import {tmpdir} from 'os';
Expand All @@ -26,6 +25,7 @@ import {watchmanCrawl} from './crawlers/watchman';
import getMockName from './getMockName';
import * as fastPath from './lib/fast_path';
import getPlatformExtension from './lib/getPlatformExtension';
import isWatchmanInstalled from './lib/isWatchmanInstalled';
import normalizePathSep from './lib/normalizePathSep';
import type {
ChangeEvent,
Expand Down Expand Up @@ -124,14 +124,6 @@ const VCS_DIRECTORIES = ['.git', '.hg']
.map(vcs => escapePathForRegex(path.sep + vcs + path.sep))
.join('|');

const canUseWatchman = ((): boolean => {
try {
execSync('watchman --version', {stdio: ['ignore']});
return true;
} catch {}
return false;
})();

function invariant(condition: unknown, message?: string): asserts condition {
if (!condition) {
throw new Error(message);
Expand Down Expand Up @@ -221,6 +213,7 @@ export default class HasteMap extends EventEmitter {
private _cachePath: string;
private _changeInterval?: ReturnType<typeof setInterval>;
private _console: Console;
private _isWatchmanInstalledPromise: Promise<boolean> | null = null;
private _options: InternalOptions;
private _watchers: Array<Watcher>;
private _worker: WorkerInterface | null;
Expand Down Expand Up @@ -763,11 +756,10 @@ export default class HasteMap extends EventEmitter {
return this._worker;
}

private _crawl(hasteMap: InternalHasteMap) {
private async _crawl(hasteMap: InternalHasteMap) {
const options = this._options;
const ignore = this._ignore.bind(this);
const crawl =
canUseWatchman && this._options.useWatchman ? watchmanCrawl : nodeCrawl;
const crawl = (await this._shouldUseWatchman()) ? watchmanCrawl : nodeCrawl;
const crawlerOptions: CrawlerOptions = {
computeSha1: options.computeSha1,
data: hasteMap,
Expand Down Expand Up @@ -811,7 +803,7 @@ export default class HasteMap extends EventEmitter {
/**
* Watch mode
*/
private _watch(hasteMap: InternalHasteMap): Promise<void> {
private async _watch(hasteMap: InternalHasteMap): Promise<void> {
if (!this._options.watch) {
return Promise.resolve();
}
Expand All @@ -822,12 +814,11 @@ export default class HasteMap extends EventEmitter {
this._options.retainAllFiles = true;

// WatchmanWatcher > FSEventsWatcher > sane.NodeWatcher
const Watcher =
canUseWatchman && this._options.useWatchman
? WatchmanWatcher
: FSEventsWatcher.isSupported()
? FSEventsWatcher
: NodeWatcher;
const Watcher = (await this._shouldUseWatchman())
? WatchmanWatcher
: FSEventsWatcher.isSupported()
? FSEventsWatcher
: NodeWatcher;

const extensions = this._options.extensions;
const ignorePattern = this._options.ignorePattern;
Expand Down Expand Up @@ -1110,6 +1101,16 @@ export default class HasteMap extends EventEmitter {
);
}

private async _shouldUseWatchman(): Promise<boolean> {
if (!this._options.useWatchman) {
return false;
}
if (!this._isWatchmanInstalledPromise) {
this._isWatchmanInstalledPromise = isWatchmanInstalled();
}
return this._isWatchmanInstalledPromise;
}

private _createEmptyMap(): InternalHasteMap {
return {
clocks: new Map(),
Expand Down
@@ -0,0 +1,37 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

import {execFile} from 'child_process';
import isWatchmanInstalled from '../isWatchmanInstalled';

jest.mock('child_process');

describe('isWatchmanInstalled', () => {
beforeEach(() => jest.clearAllMocks());

it('executes watchman --version and returns true on success', async () => {
execFile.mockImplementation((file, args, cb) => {
expect(file).toBe('watchman');
expect(args).toStrictEqual(['--version']);
cb(null, {stdout: 'v123'});
});
expect(await isWatchmanInstalled()).toBe(true);
expect(execFile).toHaveBeenCalledWith(
'watchman',
['--version'],
expect.any(Function),
);
});

it('returns false when execFile fails', async () => {
execFile.mockImplementation((file, args, cb) => {
cb(new Error());
});
expect(await isWatchmanInstalled()).toBe(false);
expect(execFile).toHaveBeenCalled();
});
});
18 changes: 18 additions & 0 deletions packages/jest-haste-map/src/lib/isWatchmanInstalled.ts
@@ -0,0 +1,18 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

import {execFile} from 'child_process';
import {promisify} from 'util';

export default async function isWatchmanInstalled(): Promise<boolean> {
try {
await promisify(execFile)('watchman', ['--version']);
return true;
} catch {
return false;
}
}