Skip to content

Commit

Permalink
fix(watchman): Fix watchman checks on Windows (#5553)
Browse files Browse the repository at this point in the history
  • Loading branch information
BYK authored and cpojer committed Feb 13, 2018
1 parent b69ac08 commit d9b4f0c
Show file tree
Hide file tree
Showing 2 changed files with 102 additions and 88 deletions.
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

0 comments on commit d9b4f0c

Please sign in to comment.