-
Notifications
You must be signed in to change notification settings - Fork 626
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add Watcher tests for behaviour on pre-existing files
Summary: Refactor watcher integration tests to extract a lot of supporting code for clarity, and add some fixtures *before* creating the Watcher in order to test behaviour when pre-existing files are modified or deleted. Use those fixtures for two additional tests. (Motivation: Similar tests for symlinks have inconsistent results, fixed in the next diff) Changelog: [Internal] Reviewed By: jacdebug Differential Revision: D42110285 fbshipit-source-id: 0721cbc5a41ce3eddef7ff4c4aff0b7ffff34156
- Loading branch information
1 parent
339794e
commit 33ead26
Showing
2 changed files
with
266 additions
and
148 deletions.
There are no files selected for viewing
185 changes: 185 additions & 0 deletions
185
packages/metro-file-map/src/watchers/__tests__/helpers.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,185 @@ | ||
/** | ||
* Copyright (c) Meta Platforms, Inc. and affiliates. | ||
* | ||
* This source code is licensed under the MIT license found in the | ||
* LICENSE file in the root directory of this source tree. | ||
* | ||
* @flow strict-local | ||
* @format | ||
* @oncall react_native | ||
*/ | ||
|
||
import type {WatcherOptions} from '../common'; | ||
|
||
import NodeWatcher from '../NodeWatcher'; | ||
import FSEventsWatcher from '../FSEventsWatcher'; | ||
import WatchmanWatcher from '../WatchmanWatcher'; | ||
import type {ChangeEventMetadata} from '../../flow-types'; | ||
import {promises as fsPromises} from 'fs'; | ||
import {execSync} from 'child_process'; | ||
import {join} from 'path'; | ||
import os from 'os'; | ||
import invariant from 'invariant'; | ||
|
||
jest.useRealTimers(); | ||
|
||
const {mkdtemp, writeFile} = fsPromises; | ||
|
||
// At runtime we use a more sophisticated + robust Watchman capability check, | ||
// but this simple heuristic is fast to check, synchronous (we can't | ||
// asynchronously skip tests: https://github.com/facebook/jest/issues/8604), | ||
// and will tend to exercise our Watchman tests whenever possible. | ||
const isWatchmanOnPath = () => { | ||
try { | ||
execSync(os.platform() === 'windows' ? 'where watchman' : 'which watchman'); | ||
return true; | ||
} catch { | ||
return false; | ||
} | ||
}; | ||
|
||
// `null` Watchers will be marked as skipped tests. | ||
export const WATCHERS: $ReadOnly<{ | ||
[key: string]: | ||
| Class<NodeWatcher> | ||
| Class<FSEventsWatcher> | ||
| Class<WatchmanWatcher> | ||
| null, | ||
}> = { | ||
Node: NodeWatcher, | ||
Watchman: isWatchmanOnPath() ? WatchmanWatcher : null, | ||
FSEvents: FSEventsWatcher.isSupported() ? FSEventsWatcher : null, | ||
}; | ||
|
||
export type EventHelpers = { | ||
nextEvent: (afterFn: () => Promise<void>) => Promise<{ | ||
eventType: string, | ||
path: string, | ||
metadata?: ChangeEventMetadata, | ||
}>, | ||
untilEvent: ( | ||
afterFn: () => Promise<void>, | ||
expectedPath: string, | ||
expectedEvent: 'add' | 'delete' | 'change', | ||
) => Promise<void>, | ||
allEvents: ( | ||
afterFn: () => Promise<void>, | ||
events: $ReadOnlyArray<[string, 'add' | 'delete' | 'change']>, | ||
opts?: {rejectUnexpected: boolean}, | ||
) => Promise<void>, | ||
}; | ||
|
||
export const createTempWatchRoot = async ( | ||
watcherName: string, | ||
watchmanConfig: {[key: string]: mixed} | false = {}, | ||
): Promise<string> => { | ||
const tmpDir = await mkdtemp( | ||
join(os.tmpdir(), `metro-watcher-${watcherName}-test-`), | ||
); | ||
|
||
// os.tmpdir() on macOS gives us a symlink /var/foo -> /private/var/foo, | ||
// we normalise it with realpath so that watchers report predictable | ||
// root-relative paths for change events. | ||
const watchRoot = await fsPromises.realpath(tmpDir); | ||
if (watchmanConfig) { | ||
await writeFile( | ||
join(watchRoot, '.watchmanconfig'), | ||
JSON.stringify(watchmanConfig), | ||
); | ||
} | ||
|
||
return watchRoot; | ||
}; | ||
|
||
export const startWatching = async ( | ||
watcherName: string, | ||
watchRoot: string, | ||
opts: WatcherOptions, | ||
): (Promise<{ | ||
eventHelpers: EventHelpers, | ||
stopWatching: () => Promise<void>, | ||
}>) => { | ||
const Watcher = WATCHERS[watcherName]; | ||
invariant(Watcher != null, `Watcher ${watcherName} is not supported`); | ||
const watcherInstance = new Watcher(watchRoot, opts); | ||
|
||
await new Promise(resolve => { | ||
watcherInstance.once('ready', resolve); | ||
}); | ||
|
||
const eventHelpers: EventHelpers = { | ||
nextEvent: afterFn => | ||
Promise.all([ | ||
new Promise<{ | ||
eventType: string, | ||
metadata?: ChangeEventMetadata, | ||
path: string, | ||
}>((resolve, reject) => { | ||
const listener = ( | ||
eventType: string, | ||
path: string, | ||
root: string, | ||
metadata?: ChangeEventMetadata, | ||
) => { | ||
if (path === '') { | ||
// FIXME: FSEventsWatcher sometimes reports 'change' events to | ||
// the watch root. | ||
return; | ||
} | ||
watcherInstance.removeListener('all', listener); | ||
if (root !== watchRoot) { | ||
reject(new Error(`Expected root ${watchRoot}, got ${root}`)); | ||
} | ||
|
||
resolve({eventType, path, metadata}); | ||
}; | ||
watcherInstance.on('all', listener); | ||
}), | ||
afterFn(), | ||
]).then(([event]) => event), | ||
|
||
untilEvent: (afterFn, expectedPath, expectedEventType) => | ||
eventHelpers.allEvents(afterFn, [[expectedPath, expectedEventType]], { | ||
rejectUnexpected: false, | ||
}), | ||
|
||
// $FlowFixMe[incompatible-use] | ||
allEvents: (afterFn, expectedEvents, {rejectUnexpected = true} = {}) => | ||
Promise.all([ | ||
new Promise((resolve, reject) => { | ||
const tupleToKey = (tuple: $ReadOnlyArray<string>) => | ||
tuple.join('\0'); | ||
const allEventKeys = new Set( | ||
expectedEvents.map(tuple => tupleToKey(tuple)), | ||
); | ||
const listener = (eventType: string, path: string) => { | ||
if (path === '') { | ||
// FIXME: FSEventsWatcher sometimes reports 'change' events to | ||
// the watch root. | ||
return; | ||
} | ||
const receivedKey = tupleToKey([path, eventType]); | ||
if (allEventKeys.has(receivedKey)) { | ||
allEventKeys.delete(receivedKey); | ||
if (allEventKeys.size === 0) { | ||
watcherInstance.removeListener('all', listener); | ||
resolve(); | ||
} | ||
} else if (rejectUnexpected) { | ||
watcherInstance.removeListener('all', listener); | ||
reject(new Error(`Unexpected event: ${eventType} ${path}.`)); | ||
} | ||
}; | ||
watcherInstance.on('all', listener); | ||
}), | ||
afterFn(), | ||
]).then(() => {}), | ||
}; | ||
|
||
return { | ||
eventHelpers, | ||
stopWatching: async () => { | ||
await watcherInstance.close(); | ||
}, | ||
}; | ||
}; |
Oops, something went wrong.