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(watchman): Fix watchman checks on Windows #5553

Merged
merged 1 commit into from
Feb 13, 2018
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 60 additions & 51 deletions packages/jest-haste-map/src/crawlers/__tests__/watchman.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,18 @@

'use strict';

const SkipOnWindows = require('../../../../../scripts/SkipOnWindows');
const path = require('path');

jest.mock('fb-watchman', () => {
const normalizePathSep = require('../../lib/normalize_path_sep').default;
const Client = jest.fn();
Client.prototype.command = jest.fn((args, callback) => {
if (args[0] === 'watch-project') {
setImmediate(() => callback(null, {watch: args[1]}));
setImmediate(() => callback(null, {watch: args[1].replace(/\\/g, '/')}));
} else if (args[0] === 'query') {
setImmediate(() => callback(null, mockResponse[args[1]]));
setImmediate(() =>
callback(null, mockResponse[normalizePathSep(args[1])]),
);
}
});
Client.prototype.on = jest.fn();
Expand All @@ -30,14 +33,21 @@ let watchmanCrawl;
let mockResponse;
let mockFiles;

describe('watchman watch', () => {
SkipOnWindows.suite();
const FRUITS = path.sep + 'fruits';
const VEGETABLES = path.sep + 'vegetables';
const ROOTS = [FRUITS, VEGETABLES];
const BANANA = path.join(FRUITS, 'banana.js');
const STRAWBERRY = path.join(FRUITS, 'strawberry.js');
const KIWI = path.join(FRUITS, 'kiwi.js');
const TOMATO = path.join(FRUITS, 'tomato.js');
const MELON = path.join(VEGETABLES, 'melon.json');

describe('watchman watch', () => {
beforeEach(() => {
watchmanCrawl = require('../watchman');

mockResponse = {
'/fruits': {
[FRUITS]: {
clock: 'c:fake-clock:1',
files: [
{
Expand All @@ -59,7 +69,7 @@ describe('watchman watch', () => {
is_fresh_instance: true,
version: '4.5.0',
},
'/vegetables': {
[VEGETABLES]: {
clock: 'c:fake-clock:2',
files: [
{
Expand All @@ -74,18 +84,19 @@ describe('watchman watch', () => {
};

mockFiles = Object.assign(Object.create(null), {
'/fruits/strawberry.js': ['', 30, 0, []],
'/fruits/tomato.js': ['', 31, 0, []],
'/vegetables/melon.json': ['', 33, 0, []],
[MELON]: ['', 33, 0, []],
[STRAWBERRY]: ['', 30, 0, []],
[TOMATO]: ['', 31, 0, []],
});
});

it('returns a list of all files when there are no clocks', () => {
const watchman = require('fb-watchman');
const normalizePathSep = require('../../lib/normalize_path_sep').default;

const path = require('path');
const originalPathRelative = path.relative;
path.relative = jest.fn(from => '/root-mock' + from);
const ROOT_MOCK = path.sep === '/' ? '/root-mock' : 'M:\\root-mock';
path.relative = jest.fn(from => normalizePathSep(ROOT_MOCK + from));

return watchmanCrawl({
data: {
Expand All @@ -94,7 +105,7 @@ describe('watchman watch', () => {
},
extensions: ['js', 'json'],
ignore: pearMatcher,
roots: ['/fruits', '/vegetables'],
roots: ROOTS,
}).then(data => {
const client = watchman.Client.mock.instances[0];
const calls = client.command.mock.calls;
Expand All @@ -116,13 +127,13 @@ describe('watchman watch', () => {
'allof',
['type', 'f'],
['anyof', ['suffix', 'js'], ['suffix', 'json']],
['anyof', ['dirname', '/root-mock/fruits']],
['anyof', ['dirname', ROOT_MOCK + FRUITS]],
]);
expect(query2[2].expression).toEqual([
'allof',
['type', 'f'],
['anyof', ['suffix', 'js'], ['suffix', 'json']],
['anyof', ['dirname', '/root-mock/vegetables']],
['anyof', ['dirname', ROOT_MOCK + VEGETABLES]],
]);

expect(query1[2].fields).toEqual(['name', 'exists', 'mtime_ms']);
Expand All @@ -132,8 +143,8 @@ describe('watchman watch', () => {
expect(query2[2].suffix).toEqual(['js', 'json']);

expect(data.clocks).toEqual({
'/fruits': 'c:fake-clock:1',
'/vegetables': 'c:fake-clock:2',
[FRUITS]: 'c:fake-clock:1',
[VEGETABLES]: 'c:fake-clock:2',
});

expect(data.files).toEqual(mockFiles);
Expand All @@ -146,7 +157,7 @@ describe('watchman watch', () => {

it('updates the file object when the clock is given', () => {
mockResponse = {
'/fruits': {
[FRUITS]: {
clock: 'c:fake-clock:3',
files: [
{
Expand All @@ -163,16 +174,16 @@ describe('watchman watch', () => {
is_fresh_instance: false,
version: '4.5.0',
},
'/vegetables': {
[VEGETABLES]: {
clock: 'c:fake-clock:4',
files: [],
version: '4.5.0',
},
};

const clocks = Object.assign(Object.create(null), {
'/fruits': 'c:fake-clock:1',
'/vegetables': 'c:fake-clock:2',
[FRUITS]: 'c:fake-clock:1',
[VEGETABLES]: 'c:fake-clock:2',
});

return watchmanCrawl({
Expand All @@ -182,27 +193,27 @@ describe('watchman watch', () => {
},
extensions: ['js', 'json'],
ignore: pearMatcher,
roots: ['/fruits', '/vegetables'],
roots: ROOTS,
}).then(data => {
// The object was reused.
expect(data.files).toBe(mockFiles);

expect(data.clocks).toEqual({
'/fruits': 'c:fake-clock:3',
'/vegetables': 'c:fake-clock:4',
[FRUITS]: 'c:fake-clock:3',
[VEGETABLES]: 'c:fake-clock:4',
});

expect(data.files).toEqual({
'/fruits/kiwi.js': ['', 42, 0, []],
'/fruits/strawberry.js': ['', 30, 0, []],
'/vegetables/melon.json': ['', 33, 0, []],
[KIWI]: ['', 42, 0, []],
[MELON]: ['', 33, 0, []],
[STRAWBERRY]: ['', 30, 0, []],
});
});
});

it('resets the file object when watchman is restarted', () => {
mockResponse = {
'/fruits': {
[FRUITS]: {
clock: 'c:fake-clock:5',
files: [
{
Expand All @@ -224,7 +235,7 @@ describe('watchman watch', () => {
is_fresh_instance: true,
version: '4.5.0',
},
'/vegetables': {
[VEGETABLES]: {
clock: 'c:fake-clock:6',
files: [],
is_fresh_instance: true,
Expand All @@ -233,11 +244,11 @@ describe('watchman watch', () => {
};

const mockMetadata = ['Banana', 41, 1, ['Raspberry']];
mockFiles['/fruits/banana.js'] = mockMetadata;
mockFiles[BANANA] = mockMetadata;

const clocks = Object.assign(Object.create(null), {
'/fruits': 'c:fake-clock:1',
'/vegetables': 'c:fake-clock:2',
[FRUITS]: 'c:fake-clock:1',
[VEGETABLES]: 'c:fake-clock:2',
});

return watchmanCrawl({
Expand All @@ -247,36 +258,34 @@ describe('watchman watch', () => {
},
extensions: ['js', 'json'],
ignore: pearMatcher,
roots: ['/fruits', '/vegetables'],
roots: ROOTS,
}).then(data => {
// The file object was *not* reused.
expect(data.files).not.toBe(mockFiles);

expect(data.clocks).toEqual({
'/fruits': 'c:fake-clock:5',
'/vegetables': 'c:fake-clock:6',
[FRUITS]: 'c:fake-clock:5',
[VEGETABLES]: 'c:fake-clock:6',
});

// /fruits/strawberry.js was removed from the file list.
expect(data.files).toEqual({
'/fruits/banana.js': mockMetadata,
'/fruits/kiwi.js': ['', 42, 0, []],
'/fruits/tomato.js': mockFiles['/fruits/tomato.js'],
[BANANA]: mockMetadata,
[KIWI]: ['', 42, 0, []],
[TOMATO]: mockFiles[TOMATO],
});

// Even though the file list was reset, old file objects are still reused
// if no changes have been made.
expect(data.files['/fruits/banana.js']).toBe(mockMetadata);
expect(data.files[BANANA]).toBe(mockMetadata);

expect(data.files['/fruits/tomato.js']).toBe(
mockFiles['/fruits/tomato.js'],
);
expect(data.files[TOMATO]).toBe(mockFiles[TOMATO]);
});
});

it('properly resets the file map when only one watcher is reset', () => {
mockResponse = {
'/fruits': {
[FRUITS]: {
clock: 'c:fake-clock:3',
files: [
{
Expand All @@ -288,7 +297,7 @@ describe('watchman watch', () => {
is_fresh_instance: false,
version: '4.5.0',
},
'/vegetables': {
[VEGETABLES]: {
clock: 'c:fake-clock:4',
files: [
{
Expand All @@ -303,8 +312,8 @@ describe('watchman watch', () => {
};

const clocks = Object.assign(Object.create(null), {
'/fruits': 'c:fake-clock:1',
'/vegetables': 'c:fake-clock:2',
[FRUITS]: 'c:fake-clock:1',
[VEGETABLES]: 'c:fake-clock:2',
});

return watchmanCrawl({
Expand All @@ -314,16 +323,16 @@ describe('watchman watch', () => {
},
extensions: ['js', 'json'],
ignore: pearMatcher,
roots: ['/fruits', '/vegetables'],
roots: ROOTS,
}).then(data => {
expect(data.clocks).toEqual({
'/fruits': 'c:fake-clock:3',
'/vegetables': 'c:fake-clock:4',
[FRUITS]: 'c:fake-clock:3',
[VEGETABLES]: 'c:fake-clock:4',
});

expect(data.files).toEqual({
'/fruits/kiwi.js': ['', 42, 0, []],
'/vegetables/melon.json': ['', 33, 0, []],
[KIWI]: ['', 42, 0, []],
[MELON]: ['', 33, 0, []],
});
});
});
Expand Down
79 changes: 42 additions & 37 deletions packages/jest-haste-map/src/crawlers/watchman.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,18 @@ module.exports = function watchmanCrawl(
options: CrawlerOptions,
): Promise<InternalHasteMap> {
const {data, extensions, ignore, roots} = options;
// Watchman always returns POSIX style paths so use posixRoots
// instead of roots to avoid on-the-fly checks inside the loop.
const posixRoots =
path.sep === '/'
? Array.from(roots)
: roots.map(root => root.replace(/\\/g, '/'));

return new Promise((resolve, reject) => {
const client = new watchman.Client();
client.on('error', error => reject(error));

const cmd = args =>
const cmd = (...args) =>
new Promise((resolve, reject) => {
client.command(args, (error, result) => {
if (error) {
Expand All @@ -52,42 +58,41 @@ module.exports = function watchmanCrawl(
const clocks = data.clocks;
let files = data.files;

return Promise.all(roots.map(root => cmd(['watch-project', root])))
.then(responses => {
const watchmanRoots = Array.from(
new Set(responses.map(response => response.watch)),
);
return Promise.all(
watchmanRoots.map(root => {
// Build an expression to filter the output by the relevant roots.
const dirExpr = (['anyof']: Array<string | Array<string>>);
roots.forEach(subRoot => {
if (isDescendant(root, subRoot)) {
dirExpr.push(['dirname', path.relative(root, subRoot)]);
return Promise.all(roots.map(root => cmd('watch-project', root)))
.then(responses =>
Promise.all(
Array.from(new Set(responses.map(response => response.watch))).map(
root => {
// Build an expression to filter the output by the relevant roots.
const dirExpr = (['anyof']: Array<string | Array<string>>);
posixRoots.forEach(subRoot => {
if (isDescendant(root, subRoot)) {
dirExpr.push(['dirname', path.relative(root, subRoot)]);
}
});
const expression = [
'allof',
['type', 'f'],
['anyof'].concat(
extensions.map(extension => ['suffix', extension]),
),
];
if (dirExpr.length > 1) {
expression.push(dirExpr);
}
});
const expression = [
'allof',
['type', 'f'],
['anyof'].concat(
extensions.map(extension => ['suffix', extension]),
),
];
if (dirExpr.length > 1) {
expression.push(dirExpr);
}
const fields = ['name', 'exists', 'mtime_ms'];
const fields = ['name', 'exists', 'mtime_ms'];

const query = clocks[root]
? // Use the `since` generator if we have a clock available
{expression, fields, since: clocks[root]}
: // Otherwise use the `suffix` generator
{expression, fields, suffix: extensions};
return cmd(['query', root, query]).then(response => ({
response,
root,
}));
}),
const query = clocks[root]
? // Use the `since` generator if we have a clock available
{expression, fields, since: clocks[root]}
: // Otherwise use the `suffix` generator
{expression, fields, suffix: extensions};
return cmd('query', root, query).then(response => ({
response,
root,
}));
},
),
).then(pairs => {
// Reset the file map if watchman was restarted and sends us a list of
// files.
Expand Down Expand Up @@ -123,8 +128,8 @@ module.exports = function watchmanCrawl(
}
});
});
});
})
}),
)
.then(() => {
client.end();
data.files = files;
Expand Down