Skip to content

Commit d9b4f0c

Browse files
BYKcpojer
authored andcommitted
fix(watchman): Fix watchman checks on Windows (#5553)
1 parent b69ac08 commit d9b4f0c

2 files changed

Lines changed: 102 additions & 88 deletions

File tree

packages/jest-haste-map/src/crawlers/__tests__/watchman.test.js

Lines changed: 60 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,18 @@
88

99
'use strict';
1010

11-
const SkipOnWindows = require('../../../../../scripts/SkipOnWindows');
11+
const path = require('path');
1212

1313
jest.mock('fb-watchman', () => {
14+
const normalizePathSep = require('../../lib/normalize_path_sep').default;
1415
const Client = jest.fn();
1516
Client.prototype.command = jest.fn((args, callback) => {
1617
if (args[0] === 'watch-project') {
17-
setImmediate(() => callback(null, {watch: args[1]}));
18+
setImmediate(() => callback(null, {watch: args[1].replace(/\\/g, '/')}));
1819
} else if (args[0] === 'query') {
19-
setImmediate(() => callback(null, mockResponse[args[1]]));
20+
setImmediate(() =>
21+
callback(null, mockResponse[normalizePathSep(args[1])]),
22+
);
2023
}
2124
});
2225
Client.prototype.on = jest.fn();
@@ -30,14 +33,21 @@ let watchmanCrawl;
3033
let mockResponse;
3134
let mockFiles;
3235

33-
describe('watchman watch', () => {
34-
SkipOnWindows.suite();
36+
const FRUITS = path.sep + 'fruits';
37+
const VEGETABLES = path.sep + 'vegetables';
38+
const ROOTS = [FRUITS, VEGETABLES];
39+
const BANANA = path.join(FRUITS, 'banana.js');
40+
const STRAWBERRY = path.join(FRUITS, 'strawberry.js');
41+
const KIWI = path.join(FRUITS, 'kiwi.js');
42+
const TOMATO = path.join(FRUITS, 'tomato.js');
43+
const MELON = path.join(VEGETABLES, 'melon.json');
3544

45+
describe('watchman watch', () => {
3646
beforeEach(() => {
3747
watchmanCrawl = require('../watchman');
3848

3949
mockResponse = {
40-
'/fruits': {
50+
[FRUITS]: {
4151
clock: 'c:fake-clock:1',
4252
files: [
4353
{
@@ -59,7 +69,7 @@ describe('watchman watch', () => {
5969
is_fresh_instance: true,
6070
version: '4.5.0',
6171
},
62-
'/vegetables': {
72+
[VEGETABLES]: {
6373
clock: 'c:fake-clock:2',
6474
files: [
6575
{
@@ -74,18 +84,19 @@ describe('watchman watch', () => {
7484
};
7585

7686
mockFiles = Object.assign(Object.create(null), {
77-
'/fruits/strawberry.js': ['', 30, 0, []],
78-
'/fruits/tomato.js': ['', 31, 0, []],
79-
'/vegetables/melon.json': ['', 33, 0, []],
87+
[MELON]: ['', 33, 0, []],
88+
[STRAWBERRY]: ['', 30, 0, []],
89+
[TOMATO]: ['', 31, 0, []],
8090
});
8191
});
8292

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

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

90101
return watchmanCrawl({
91102
data: {
@@ -94,7 +105,7 @@ describe('watchman watch', () => {
94105
},
95106
extensions: ['js', 'json'],
96107
ignore: pearMatcher,
97-
roots: ['/fruits', '/vegetables'],
108+
roots: ROOTS,
98109
}).then(data => {
99110
const client = watchman.Client.mock.instances[0];
100111
const calls = client.command.mock.calls;
@@ -116,13 +127,13 @@ describe('watchman watch', () => {
116127
'allof',
117128
['type', 'f'],
118129
['anyof', ['suffix', 'js'], ['suffix', 'json']],
119-
['anyof', ['dirname', '/root-mock/fruits']],
130+
['anyof', ['dirname', ROOT_MOCK + FRUITS]],
120131
]);
121132
expect(query2[2].expression).toEqual([
122133
'allof',
123134
['type', 'f'],
124135
['anyof', ['suffix', 'js'], ['suffix', 'json']],
125-
['anyof', ['dirname', '/root-mock/vegetables']],
136+
['anyof', ['dirname', ROOT_MOCK + VEGETABLES]],
126137
]);
127138

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

134145
expect(data.clocks).toEqual({
135-
'/fruits': 'c:fake-clock:1',
136-
'/vegetables': 'c:fake-clock:2',
146+
[FRUITS]: 'c:fake-clock:1',
147+
[VEGETABLES]: 'c:fake-clock:2',
137148
});
138149

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

147158
it('updates the file object when the clock is given', () => {
148159
mockResponse = {
149-
'/fruits': {
160+
[FRUITS]: {
150161
clock: 'c:fake-clock:3',
151162
files: [
152163
{
@@ -163,16 +174,16 @@ describe('watchman watch', () => {
163174
is_fresh_instance: false,
164175
version: '4.5.0',
165176
},
166-
'/vegetables': {
177+
[VEGETABLES]: {
167178
clock: 'c:fake-clock:4',
168179
files: [],
169180
version: '4.5.0',
170181
},
171182
};
172183

173184
const clocks = Object.assign(Object.create(null), {
174-
'/fruits': 'c:fake-clock:1',
175-
'/vegetables': 'c:fake-clock:2',
185+
[FRUITS]: 'c:fake-clock:1',
186+
[VEGETABLES]: 'c:fake-clock:2',
176187
});
177188

178189
return watchmanCrawl({
@@ -182,27 +193,27 @@ describe('watchman watch', () => {
182193
},
183194
extensions: ['js', 'json'],
184195
ignore: pearMatcher,
185-
roots: ['/fruits', '/vegetables'],
196+
roots: ROOTS,
186197
}).then(data => {
187198
// The object was reused.
188199
expect(data.files).toBe(mockFiles);
189200

190201
expect(data.clocks).toEqual({
191-
'/fruits': 'c:fake-clock:3',
192-
'/vegetables': 'c:fake-clock:4',
202+
[FRUITS]: 'c:fake-clock:3',
203+
[VEGETABLES]: 'c:fake-clock:4',
193204
});
194205

195206
expect(data.files).toEqual({
196-
'/fruits/kiwi.js': ['', 42, 0, []],
197-
'/fruits/strawberry.js': ['', 30, 0, []],
198-
'/vegetables/melon.json': ['', 33, 0, []],
207+
[KIWI]: ['', 42, 0, []],
208+
[MELON]: ['', 33, 0, []],
209+
[STRAWBERRY]: ['', 30, 0, []],
199210
});
200211
});
201212
});
202213

203214
it('resets the file object when watchman is restarted', () => {
204215
mockResponse = {
205-
'/fruits': {
216+
[FRUITS]: {
206217
clock: 'c:fake-clock:5',
207218
files: [
208219
{
@@ -224,7 +235,7 @@ describe('watchman watch', () => {
224235
is_fresh_instance: true,
225236
version: '4.5.0',
226237
},
227-
'/vegetables': {
238+
[VEGETABLES]: {
228239
clock: 'c:fake-clock:6',
229240
files: [],
230241
is_fresh_instance: true,
@@ -233,11 +244,11 @@ describe('watchman watch', () => {
233244
};
234245

235246
const mockMetadata = ['Banana', 41, 1, ['Raspberry']];
236-
mockFiles['/fruits/banana.js'] = mockMetadata;
247+
mockFiles[BANANA] = mockMetadata;
237248

238249
const clocks = Object.assign(Object.create(null), {
239-
'/fruits': 'c:fake-clock:1',
240-
'/vegetables': 'c:fake-clock:2',
250+
[FRUITS]: 'c:fake-clock:1',
251+
[VEGETABLES]: 'c:fake-clock:2',
241252
});
242253

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

255266
expect(data.clocks).toEqual({
256-
'/fruits': 'c:fake-clock:5',
257-
'/vegetables': 'c:fake-clock:6',
267+
[FRUITS]: 'c:fake-clock:5',
268+
[VEGETABLES]: 'c:fake-clock:6',
258269
});
259270

260271
// /fruits/strawberry.js was removed from the file list.
261272
expect(data.files).toEqual({
262-
'/fruits/banana.js': mockMetadata,
263-
'/fruits/kiwi.js': ['', 42, 0, []],
264-
'/fruits/tomato.js': mockFiles['/fruits/tomato.js'],
273+
[BANANA]: mockMetadata,
274+
[KIWI]: ['', 42, 0, []],
275+
[TOMATO]: mockFiles[TOMATO],
265276
});
266277

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

271-
expect(data.files['/fruits/tomato.js']).toBe(
272-
mockFiles['/fruits/tomato.js'],
273-
);
282+
expect(data.files[TOMATO]).toBe(mockFiles[TOMATO]);
274283
});
275284
});
276285

277286
it('properly resets the file map when only one watcher is reset', () => {
278287
mockResponse = {
279-
'/fruits': {
288+
[FRUITS]: {
280289
clock: 'c:fake-clock:3',
281290
files: [
282291
{
@@ -288,7 +297,7 @@ describe('watchman watch', () => {
288297
is_fresh_instance: false,
289298
version: '4.5.0',
290299
},
291-
'/vegetables': {
300+
[VEGETABLES]: {
292301
clock: 'c:fake-clock:4',
293302
files: [
294303
{
@@ -303,8 +312,8 @@ describe('watchman watch', () => {
303312
};
304313

305314
const clocks = Object.assign(Object.create(null), {
306-
'/fruits': 'c:fake-clock:1',
307-
'/vegetables': 'c:fake-clock:2',
315+
[FRUITS]: 'c:fake-clock:1',
316+
[VEGETABLES]: 'c:fake-clock:2',
308317
});
309318

310319
return watchmanCrawl({
@@ -314,16 +323,16 @@ describe('watchman watch', () => {
314323
},
315324
extensions: ['js', 'json'],
316325
ignore: pearMatcher,
317-
roots: ['/fruits', '/vegetables'],
326+
roots: ROOTS,
318327
}).then(data => {
319328
expect(data.clocks).toEqual({
320-
'/fruits': 'c:fake-clock:3',
321-
'/vegetables': 'c:fake-clock:4',
329+
[FRUITS]: 'c:fake-clock:3',
330+
[VEGETABLES]: 'c:fake-clock:4',
322331
});
323332

324333
expect(data.files).toEqual({
325-
'/fruits/kiwi.js': ['', 42, 0, []],
326-
'/vegetables/melon.json': ['', 33, 0, []],
334+
[KIWI]: ['', 42, 0, []],
335+
[MELON]: ['', 33, 0, []],
327336
});
328337
});
329338
});

packages/jest-haste-map/src/crawlers/watchman.js

Lines changed: 42 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,18 @@ module.exports = function watchmanCrawl(
3333
options: CrawlerOptions,
3434
): Promise<InternalHasteMap> {
3535
const {data, extensions, ignore, roots} = options;
36+
// Watchman always returns POSIX style paths so use posixRoots
37+
// instead of roots to avoid on-the-fly checks inside the loop.
38+
const posixRoots =
39+
path.sep === '/'
40+
? Array.from(roots)
41+
: roots.map(root => root.replace(/\\/g, '/'));
3642

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

41-
const cmd = args =>
47+
const cmd = (...args) =>
4248
new Promise((resolve, reject) => {
4349
client.command(args, (error, result) => {
4450
if (error) {
@@ -52,42 +58,41 @@ module.exports = function watchmanCrawl(
5258
const clocks = data.clocks;
5359
let files = data.files;
5460

55-
return Promise.all(roots.map(root => cmd(['watch-project', root])))
56-
.then(responses => {
57-
const watchmanRoots = Array.from(
58-
new Set(responses.map(response => response.watch)),
59-
);
60-
return Promise.all(
61-
watchmanRoots.map(root => {
62-
// Build an expression to filter the output by the relevant roots.
63-
const dirExpr = (['anyof']: Array<string | Array<string>>);
64-
roots.forEach(subRoot => {
65-
if (isDescendant(root, subRoot)) {
66-
dirExpr.push(['dirname', path.relative(root, subRoot)]);
61+
return Promise.all(roots.map(root => cmd('watch-project', root)))
62+
.then(responses =>
63+
Promise.all(
64+
Array.from(new Set(responses.map(response => response.watch))).map(
65+
root => {
66+
// Build an expression to filter the output by the relevant roots.
67+
const dirExpr = (['anyof']: Array<string | Array<string>>);
68+
posixRoots.forEach(subRoot => {
69+
if (isDescendant(root, subRoot)) {
70+
dirExpr.push(['dirname', path.relative(root, subRoot)]);
71+
}
72+
});
73+
const expression = [
74+
'allof',
75+
['type', 'f'],
76+
['anyof'].concat(
77+
extensions.map(extension => ['suffix', extension]),
78+
),
79+
];
80+
if (dirExpr.length > 1) {
81+
expression.push(dirExpr);
6782
}
68-
});
69-
const expression = [
70-
'allof',
71-
['type', 'f'],
72-
['anyof'].concat(
73-
extensions.map(extension => ['suffix', extension]),
74-
),
75-
];
76-
if (dirExpr.length > 1) {
77-
expression.push(dirExpr);
78-
}
79-
const fields = ['name', 'exists', 'mtime_ms'];
83+
const fields = ['name', 'exists', 'mtime_ms'];
8084

81-
const query = clocks[root]
82-
? // Use the `since` generator if we have a clock available
83-
{expression, fields, since: clocks[root]}
84-
: // Otherwise use the `suffix` generator
85-
{expression, fields, suffix: extensions};
86-
return cmd(['query', root, query]).then(response => ({
87-
response,
88-
root,
89-
}));
90-
}),
85+
const query = clocks[root]
86+
? // Use the `since` generator if we have a clock available
87+
{expression, fields, since: clocks[root]}
88+
: // Otherwise use the `suffix` generator
89+
{expression, fields, suffix: extensions};
90+
return cmd('query', root, query).then(response => ({
91+
response,
92+
root,
93+
}));
94+
},
95+
),
9196
).then(pairs => {
9297
// Reset the file map if watchman was restarted and sends us a list of
9398
// files.
@@ -123,8 +128,8 @@ module.exports = function watchmanCrawl(
123128
}
124129
});
125130
});
126-
});
127-
})
131+
}),
132+
)
128133
.then(() => {
129134
client.end();
130135
data.files = files;

0 commit comments

Comments
 (0)