native-watcher-registry-spec.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389
  1. /** @babel */
  2. import path from 'path';
  3. import { Emitter } from 'event-kit';
  4. import { NativeWatcherRegistry } from '../src/native-watcher-registry';
  5. function findRootDirectory() {
  6. let current = process.cwd();
  7. while (true) {
  8. let next = path.resolve(current, '..');
  9. if (next === current) {
  10. return next;
  11. } else {
  12. current = next;
  13. }
  14. }
  15. }
  16. const ROOT = findRootDirectory();
  17. function absolute(...parts) {
  18. const candidate = path.join(...parts);
  19. return path.isAbsolute(candidate) ? candidate : path.join(ROOT, candidate);
  20. }
  21. function parts(fullPath) {
  22. return fullPath.split(path.sep).filter(part => part.length > 0);
  23. }
  24. class MockWatcher {
  25. constructor(normalizedPath) {
  26. this.normalizedPath = normalizedPath;
  27. this.native = null;
  28. }
  29. getNormalizedPathPromise() {
  30. return Promise.resolve(this.normalizedPath);
  31. }
  32. attachToNative(native, nativePath) {
  33. if (this.normalizedPath.startsWith(nativePath)) {
  34. if (this.native) {
  35. this.native.attached = this.native.attached.filter(
  36. each => each !== this
  37. );
  38. }
  39. this.native = native;
  40. this.native.attached.push(this);
  41. }
  42. }
  43. }
  44. class MockNative {
  45. constructor(name) {
  46. this.name = name;
  47. this.attached = [];
  48. this.disposed = false;
  49. this.stopped = false;
  50. this.emitter = new Emitter();
  51. }
  52. reattachTo(newNative, nativePath) {
  53. for (const watcher of this.attached) {
  54. watcher.attachToNative(newNative, nativePath);
  55. }
  56. }
  57. onWillStop(callback) {
  58. return this.emitter.on('will-stop', callback);
  59. }
  60. dispose() {
  61. this.disposed = true;
  62. }
  63. stop() {
  64. this.stopped = true;
  65. this.emitter.emit('will-stop');
  66. }
  67. }
  68. describe('NativeWatcherRegistry', function() {
  69. let createNative, registry;
  70. beforeEach(function() {
  71. registry = new NativeWatcherRegistry(normalizedPath =>
  72. createNative(normalizedPath)
  73. );
  74. });
  75. it('attaches a Watcher to a newly created NativeWatcher for a new directory', async function() {
  76. const watcher = new MockWatcher(absolute('some', 'path'));
  77. const NATIVE = new MockNative('created');
  78. createNative = () => NATIVE;
  79. await registry.attach(watcher);
  80. expect(watcher.native).toBe(NATIVE);
  81. });
  82. it('reuses an existing NativeWatcher on the same directory', async function() {
  83. this.RETRY_FLAKY_TEST_AND_SLOW_DOWN_THE_BUILD();
  84. const EXISTING = new MockNative('existing');
  85. const existingPath = absolute('existing', 'path');
  86. let firstTime = true;
  87. createNative = () => {
  88. if (firstTime) {
  89. firstTime = false;
  90. return EXISTING;
  91. }
  92. return new MockNative('nope');
  93. };
  94. await registry.attach(new MockWatcher(existingPath));
  95. const watcher = new MockWatcher(existingPath);
  96. await registry.attach(watcher);
  97. expect(watcher.native).toBe(EXISTING);
  98. });
  99. it('attaches to an existing NativeWatcher on a parent directory', async function() {
  100. const EXISTING = new MockNative('existing');
  101. const parentDir = absolute('existing', 'path');
  102. const subDir = path.join(parentDir, 'sub', 'directory');
  103. let firstTime = true;
  104. createNative = () => {
  105. if (firstTime) {
  106. firstTime = false;
  107. return EXISTING;
  108. }
  109. return new MockNative('nope');
  110. };
  111. await registry.attach(new MockWatcher(parentDir));
  112. const watcher = new MockWatcher(subDir);
  113. await registry.attach(watcher);
  114. expect(watcher.native).toBe(EXISTING);
  115. });
  116. it('adopts Watchers from NativeWatchers on child directories', async function() {
  117. const parentDir = absolute('existing', 'path');
  118. const childDir0 = path.join(parentDir, 'child', 'directory', 'zero');
  119. const childDir1 = path.join(parentDir, 'child', 'directory', 'one');
  120. const otherDir = absolute('another', 'path');
  121. const CHILD0 = new MockNative('existing0');
  122. const CHILD1 = new MockNative('existing1');
  123. const OTHER = new MockNative('existing2');
  124. const PARENT = new MockNative('parent');
  125. createNative = dir => {
  126. if (dir === childDir0) {
  127. return CHILD0;
  128. } else if (dir === childDir1) {
  129. return CHILD1;
  130. } else if (dir === otherDir) {
  131. return OTHER;
  132. } else if (dir === parentDir) {
  133. return PARENT;
  134. } else {
  135. throw new Error(`Unexpected path: ${dir}`);
  136. }
  137. };
  138. const watcher0 = new MockWatcher(childDir0);
  139. await registry.attach(watcher0);
  140. const watcher1 = new MockWatcher(childDir1);
  141. await registry.attach(watcher1);
  142. const watcher2 = new MockWatcher(otherDir);
  143. await registry.attach(watcher2);
  144. expect(watcher0.native).toBe(CHILD0);
  145. expect(watcher1.native).toBe(CHILD1);
  146. expect(watcher2.native).toBe(OTHER);
  147. // Consolidate all three watchers beneath the same native watcher on the parent directory
  148. const watcher = new MockWatcher(parentDir);
  149. await registry.attach(watcher);
  150. expect(watcher.native).toBe(PARENT);
  151. expect(watcher0.native).toBe(PARENT);
  152. expect(CHILD0.stopped).toBe(true);
  153. expect(CHILD0.disposed).toBe(true);
  154. expect(watcher1.native).toBe(PARENT);
  155. expect(CHILD1.stopped).toBe(true);
  156. expect(CHILD1.disposed).toBe(true);
  157. expect(watcher2.native).toBe(OTHER);
  158. expect(OTHER.stopped).toBe(false);
  159. expect(OTHER.disposed).toBe(false);
  160. });
  161. describe('removing NativeWatchers', function() {
  162. it('happens when they stop', async function() {
  163. const STOPPED = new MockNative('stopped');
  164. const RUNNING = new MockNative('running');
  165. const stoppedPath = absolute('watcher', 'that', 'will', 'be', 'stopped');
  166. const stoppedPathParts = stoppedPath
  167. .split(path.sep)
  168. .filter(part => part.length > 0);
  169. const runningPath = absolute(
  170. 'watcher',
  171. 'that',
  172. 'will',
  173. 'continue',
  174. 'to',
  175. 'exist'
  176. );
  177. const runningPathParts = runningPath
  178. .split(path.sep)
  179. .filter(part => part.length > 0);
  180. createNative = dir => {
  181. if (dir === stoppedPath) {
  182. return STOPPED;
  183. } else if (dir === runningPath) {
  184. return RUNNING;
  185. } else {
  186. throw new Error(`Unexpected path: ${dir}`);
  187. }
  188. };
  189. const stoppedWatcher = new MockWatcher(stoppedPath);
  190. await registry.attach(stoppedWatcher);
  191. const runningWatcher = new MockWatcher(runningPath);
  192. await registry.attach(runningWatcher);
  193. STOPPED.stop();
  194. const runningNode = registry.tree.root.lookup(runningPathParts).when({
  195. parent: node => node,
  196. missing: () => false,
  197. children: () => false
  198. });
  199. expect(runningNode).toBeTruthy();
  200. expect(runningNode.getNativeWatcher()).toBe(RUNNING);
  201. const stoppedNode = registry.tree.root.lookup(stoppedPathParts).when({
  202. parent: () => false,
  203. missing: () => true,
  204. children: () => false
  205. });
  206. expect(stoppedNode).toBe(true);
  207. });
  208. it('reassigns new child watchers when a parent watcher is stopped', async function() {
  209. const CHILD0 = new MockNative('child0');
  210. const CHILD1 = new MockNative('child1');
  211. const PARENT = new MockNative('parent');
  212. const parentDir = absolute('parent');
  213. const childDir0 = path.join(parentDir, 'child0');
  214. const childDir1 = path.join(parentDir, 'child1');
  215. createNative = dir => {
  216. if (dir === parentDir) {
  217. return PARENT;
  218. } else if (dir === childDir0) {
  219. return CHILD0;
  220. } else if (dir === childDir1) {
  221. return CHILD1;
  222. } else {
  223. throw new Error(`Unexpected directory ${dir}`);
  224. }
  225. };
  226. const parentWatcher = new MockWatcher(parentDir);
  227. const childWatcher0 = new MockWatcher(childDir0);
  228. const childWatcher1 = new MockWatcher(childDir1);
  229. await registry.attach(parentWatcher);
  230. await Promise.all([
  231. registry.attach(childWatcher0),
  232. registry.attach(childWatcher1)
  233. ]);
  234. // All three watchers should share the parent watcher's native watcher.
  235. expect(parentWatcher.native).toBe(PARENT);
  236. expect(childWatcher0.native).toBe(PARENT);
  237. expect(childWatcher1.native).toBe(PARENT);
  238. // Stopping the parent should detach and recreate the child watchers.
  239. PARENT.stop();
  240. expect(childWatcher0.native).toBe(CHILD0);
  241. expect(childWatcher1.native).toBe(CHILD1);
  242. expect(
  243. registry.tree.root.lookup(parts(parentDir)).when({
  244. parent: () => false,
  245. missing: () => false,
  246. children: () => true
  247. })
  248. ).toBe(true);
  249. expect(
  250. registry.tree.root.lookup(parts(childDir0)).when({
  251. parent: () => true,
  252. missing: () => false,
  253. children: () => false
  254. })
  255. ).toBe(true);
  256. expect(
  257. registry.tree.root.lookup(parts(childDir1)).when({
  258. parent: () => true,
  259. missing: () => false,
  260. children: () => false
  261. })
  262. ).toBe(true);
  263. });
  264. it('consolidates children when splitting a parent watcher', async function() {
  265. const CHILD0 = new MockNative('child0');
  266. const PARENT = new MockNative('parent');
  267. const parentDir = absolute('parent');
  268. const childDir0 = path.join(parentDir, 'child0');
  269. const childDir1 = path.join(parentDir, 'child0', 'child1');
  270. createNative = dir => {
  271. if (dir === parentDir) {
  272. return PARENT;
  273. } else if (dir === childDir0) {
  274. return CHILD0;
  275. } else {
  276. throw new Error(`Unexpected directory ${dir}`);
  277. }
  278. };
  279. const parentWatcher = new MockWatcher(parentDir);
  280. const childWatcher0 = new MockWatcher(childDir0);
  281. const childWatcher1 = new MockWatcher(childDir1);
  282. await registry.attach(parentWatcher);
  283. await Promise.all([
  284. registry.attach(childWatcher0),
  285. registry.attach(childWatcher1)
  286. ]);
  287. // All three watchers should share the parent watcher's native watcher.
  288. expect(parentWatcher.native).toBe(PARENT);
  289. expect(childWatcher0.native).toBe(PARENT);
  290. expect(childWatcher1.native).toBe(PARENT);
  291. // Stopping the parent should detach and create the child watchers. Both child watchers should
  292. // share the same native watcher.
  293. PARENT.stop();
  294. expect(childWatcher0.native).toBe(CHILD0);
  295. expect(childWatcher1.native).toBe(CHILD0);
  296. expect(
  297. registry.tree.root.lookup(parts(parentDir)).when({
  298. parent: () => false,
  299. missing: () => false,
  300. children: () => true
  301. })
  302. ).toBe(true);
  303. expect(
  304. registry.tree.root.lookup(parts(childDir0)).when({
  305. parent: () => true,
  306. missing: () => false,
  307. children: () => false
  308. })
  309. ).toBe(true);
  310. expect(
  311. registry.tree.root.lookup(parts(childDir1)).when({
  312. parent: () => true,
  313. missing: () => false,
  314. children: () => false
  315. })
  316. ).toBe(true);
  317. });
  318. });
  319. });