@theia/filesystem 1.77.0-next.6 → 1.77.0-next.63
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/browser/download/file-download-service.d.ts.map +1 -1
- package/lib/browser/download/file-download-service.js +0 -1
- package/lib/browser/download/file-download-service.js.map +1 -1
- package/lib/browser/file-dialog/file-dialog.js +1 -1
- package/lib/browser/file-dialog/file-dialog.js.map +1 -1
- package/lib/browser/file-resource.spec.js +2 -1
- package/lib/browser/file-resource.spec.js.map +1 -1
- package/lib/browser/file-service-watcher.spec.js +2 -1
- package/lib/browser/file-service-watcher.spec.js.map +1 -1
- package/lib/browser/file-service.d.ts +1 -1
- package/lib/browser/file-service.js +1 -1
- package/lib/browser/file-service.js.map +1 -1
- package/lib/browser/file-tree/file-tree-decorator-adapter.js +1 -1
- package/lib/browser/file-tree/file-tree-decorator-adapter.js.map +1 -1
- package/lib/browser/file-tree/file-tree-decorator-adapter.spec.js +2 -1
- package/lib/browser/file-tree/file-tree-decorator-adapter.spec.js.map +1 -1
- package/lib/browser/file-tree/file-tree-model.js +1 -1
- package/lib/browser/file-tree/file-tree-model.js.map +1 -1
- package/lib/browser/file-tree/file-tree-widget.js +1 -1
- package/lib/browser/file-tree/file-tree-widget.js.map +1 -1
- package/lib/browser/filesystem-frontend-contribution.js +1 -1
- package/lib/browser/filesystem-frontend-contribution.js.map +1 -1
- package/lib/browser/location/location-renderer.js +1 -1
- package/lib/browser/location/location-renderer.js.map +1 -1
- package/lib/browser/location/location-renderer.spec.js +2 -1
- package/lib/browser/location/location-renderer.spec.js.map +1 -1
- package/lib/browser/upload/file-upload-service-impl.js +1 -1
- package/lib/browser/upload/file-upload-service-impl.js.map +1 -1
- package/lib/browser-only/download/file-download-service.js +1 -1
- package/lib/browser-only/download/file-download-service.js.map +1 -1
- package/lib/browser-only/file-search.js +2 -1
- package/lib/browser-only/file-search.js.map +1 -1
- package/lib/browser-only/upload/file-upload-service-impl.js +1 -1
- package/lib/browser-only/upload/file-upload-service-impl.js.map +1 -1
- package/lib/common/files.js +2 -1
- package/lib/common/files.js.map +1 -1
- package/lib/common/files.spec.js +2 -1
- package/lib/common/files.spec.js.map +1 -1
- package/lib/common/filesystem-watcher-protocol.d.ts +4 -0
- package/lib/common/filesystem-watcher-protocol.d.ts.map +1 -1
- package/lib/common/remote-file-system-provider.d.ts +4 -4
- package/lib/common/remote-file-system-provider.d.ts.map +1 -1
- package/lib/common/remote-file-system-provider.js +1 -1
- package/lib/common/remote-file-system-provider.js.map +1 -1
- package/lib/node/disk-file-system-provider.d.ts.map +1 -1
- package/lib/node/disk-file-system-provider.js +8 -5
- package/lib/node/disk-file-system-provider.js.map +1 -1
- package/lib/node/disk-file-system-provider.spec.js +28 -2
- package/lib/node/disk-file-system-provider.spec.js.map +1 -1
- package/lib/node/download/directory-archiver.js +2 -2
- package/lib/node/download/directory-archiver.js.map +1 -1
- package/lib/node/download/directory-archiver.spec.js +5 -4
- package/lib/node/download/directory-archiver.spec.js.map +1 -1
- package/lib/node/download/file-download-handler.d.ts.map +1 -1
- package/lib/node/download/file-download-handler.js +4 -5
- package/lib/node/download/file-download-handler.js.map +1 -1
- package/lib/node/file-change-collection.spec.js +2 -1
- package/lib/node/file-change-collection.spec.js.map +1 -1
- package/lib/node/filesystem-backend-module.d.ts +2 -2
- package/lib/node/filesystem-backend-module.d.ts.map +1 -1
- package/lib/node/filesystem-backend-module.js +7 -5
- package/lib/node/filesystem-backend-module.js.map +1 -1
- package/lib/node/filesystem-watcher-service-impl.d.ts +30 -0
- package/lib/node/filesystem-watcher-service-impl.d.ts.map +1 -0
- package/lib/node/filesystem-watcher-service-impl.js +92 -0
- package/lib/node/filesystem-watcher-service-impl.js.map +1 -0
- package/lib/node/filesystem-watcher-service-impl.spec.d.ts +2 -0
- package/lib/node/filesystem-watcher-service-impl.spec.d.ts.map +1 -0
- package/lib/node/filesystem-watcher-service-impl.spec.js +202 -0
- package/lib/node/filesystem-watcher-service-impl.spec.js.map +1 -0
- package/lib/node/filesystem-watcher.d.ts +93 -0
- package/lib/node/filesystem-watcher.d.ts.map +1 -0
- package/lib/node/filesystem-watcher.js +134 -0
- package/lib/node/filesystem-watcher.js.map +1 -0
- package/lib/node/nodejs-watcher/node-directory-watcher.d.ts +175 -0
- package/lib/node/nodejs-watcher/node-directory-watcher.d.ts.map +1 -0
- package/lib/node/nodejs-watcher/node-directory-watcher.js +580 -0
- package/lib/node/nodejs-watcher/node-directory-watcher.js.map +1 -0
- package/lib/node/nodejs-watcher/node-directory-watcher.spec.d.ts +2 -0
- package/lib/node/nodejs-watcher/node-directory-watcher.spec.d.ts.map +1 -0
- package/lib/node/nodejs-watcher/node-directory-watcher.spec.js +1284 -0
- package/lib/node/nodejs-watcher/node-directory-watcher.spec.js.map +1 -0
- package/lib/node/nodejs-watcher/watch-request-router.d.ts +63 -0
- package/lib/node/nodejs-watcher/watch-request-router.d.ts.map +1 -0
- package/lib/node/nodejs-watcher/watch-request-router.js +183 -0
- package/lib/node/nodejs-watcher/watch-request-router.js.map +1 -0
- package/lib/node/nodejs-watcher/watcher-host.d.ts +50 -0
- package/lib/node/nodejs-watcher/watcher-host.d.ts.map +1 -0
- package/lib/node/nodejs-watcher/watcher-host.js +99 -0
- package/lib/node/nodejs-watcher/watcher-host.js.map +1 -0
- package/lib/node/parcel-watcher/index.js +4 -3
- package/lib/node/parcel-watcher/index.js.map +1 -1
- package/lib/node/parcel-watcher/parcel-filesystem-service.d.ts +14 -194
- package/lib/node/parcel-watcher/parcel-filesystem-service.d.ts.map +1 -1
- package/lib/node/parcel-watcher/parcel-filesystem-service.js +7 -424
- package/lib/node/parcel-watcher/parcel-filesystem-service.js.map +1 -1
- package/lib/node/parcel-watcher/parcel-filesystem-watcher.spec.js +8 -7
- package/lib/node/parcel-watcher/parcel-filesystem-watcher.spec.js.map +1 -1
- package/lib/node/parcel-watcher/parcel-watcher-exclude.spec.js +8 -7
- package/lib/node/parcel-watcher/parcel-watcher-exclude.spec.js.map +1 -1
- package/lib/node/parcel-watcher/parcel-watcher-retry.spec.js +7 -6
- package/lib/node/parcel-watcher/parcel-watcher-retry.spec.js.map +1 -1
- package/lib/node/parcel-watcher/parcel-watcher.d.ts +165 -0
- package/lib/node/parcel-watcher/parcel-watcher.d.ts.map +1 -0
- package/lib/node/parcel-watcher/parcel-watcher.js +370 -0
- package/lib/node/parcel-watcher/parcel-watcher.js.map +1 -0
- package/lib/node/test/watcher-test-helper.d.ts +21 -0
- package/lib/node/test/watcher-test-helper.d.ts.map +1 -0
- package/lib/node/test/watcher-test-helper.js +56 -0
- package/lib/node/test/watcher-test-helper.js.map +1 -0
- package/lib/node/upload/node-file-upload-service.spec.js +4 -3
- package/lib/node/upload/node-file-upload-service.spec.js.map +1 -1
- package/package.json +7 -7
- package/src/browser/download/file-download-service.ts +0 -1
- package/src/browser/file-service.ts +3 -3
- package/src/common/filesystem-watcher-protocol.ts +4 -0
- package/src/node/disk-file-system-provider.spec.ts +29 -0
- package/src/node/disk-file-system-provider.ts +5 -2
- package/src/node/download/file-download-handler.ts +0 -1
- package/src/node/filesystem-backend-module.ts +5 -4
- package/src/node/filesystem-watcher-service-impl.spec.ts +272 -0
- package/src/node/filesystem-watcher-service-impl.ts +109 -0
- package/src/node/filesystem-watcher.ts +197 -0
- package/src/node/nodejs-watcher/node-directory-watcher.spec.ts +1594 -0
- package/src/node/nodejs-watcher/node-directory-watcher.ts +664 -0
- package/src/node/nodejs-watcher/watch-request-router.ts +224 -0
- package/src/node/nodejs-watcher/watcher-host.ts +126 -0
- package/src/node/parcel-watcher/index.ts +2 -2
- package/src/node/parcel-watcher/parcel-filesystem-service.ts +14 -500
- package/src/node/parcel-watcher/parcel-filesystem-watcher.spec.ts +4 -4
- package/src/node/parcel-watcher/parcel-watcher-exclude.spec.ts +4 -4
- package/src/node/parcel-watcher/parcel-watcher-retry.spec.ts +3 -3
- package/src/node/parcel-watcher/parcel-watcher.ts +432 -0
- package/src/node/test/watcher-test-helper.ts +58 -0
|
@@ -0,0 +1,1594 @@
|
|
|
1
|
+
// *****************************************************************************
|
|
2
|
+
// Copyright (C) 2026 Ehab Younes and others.
|
|
3
|
+
//
|
|
4
|
+
// This program and the accompanying materials are made available under the
|
|
5
|
+
// terms of the Eclipse Public License v. 2.0 which is available at
|
|
6
|
+
// http://www.eclipse.org/legal/epl-2.0.
|
|
7
|
+
//
|
|
8
|
+
// This Source Code may also be made available under the following Secondary
|
|
9
|
+
// Licenses when the conditions for such availability set forth in the Eclipse
|
|
10
|
+
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
|
11
|
+
// with the GNU Classpath Exception which is available at
|
|
12
|
+
// https://www.gnu.org/software/classpath/license.html.
|
|
13
|
+
//
|
|
14
|
+
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
15
|
+
// *****************************************************************************
|
|
16
|
+
|
|
17
|
+
import * as assert from 'assert';
|
|
18
|
+
import * as path from 'path';
|
|
19
|
+
import * as temp from 'temp';
|
|
20
|
+
import * as fs from '@theia/core/shared/fs-extra';
|
|
21
|
+
import { EventEmitter } from 'events';
|
|
22
|
+
import { FSWatcher } from 'fs';
|
|
23
|
+
import { Deferred, wait } from '@theia/core/lib/common/promise-util';
|
|
24
|
+
import { isWindows } from '@theia/core';
|
|
25
|
+
import { FileUri } from '@theia/core/lib/node';
|
|
26
|
+
import { DidFilesChangedParams, FileSystemWatcherServiceClient } from '../../common/filesystem-watcher-protocol';
|
|
27
|
+
import { NO_LOGGING, TempDir, WATCHER_TIMINGS as TIMINGS } from '../test/watcher-test-helper';
|
|
28
|
+
import { DirectoryWatcherProvider, NodeDirectoryWatcher } from './node-directory-watcher';
|
|
29
|
+
import { DirectoryWatchRequest } from './watch-request-router';
|
|
30
|
+
import { DirectoryIdentity, WatcherHost, WatchEventListener } from './watcher-host';
|
|
31
|
+
|
|
32
|
+
const track = temp.track();
|
|
33
|
+
|
|
34
|
+
/** How long to keep listening after the expected changes arrived, to catch any that should not have. */
|
|
35
|
+
const SETTLE_DELAY = TIMINGS.deleteDelay * 3;
|
|
36
|
+
|
|
37
|
+
/** Indexed by `FileChangeType`. */
|
|
38
|
+
const CHANGE_NAMES = ['updated', 'added', 'deleted'];
|
|
39
|
+
|
|
40
|
+
/** Replaces the whole platform boundary, so behaviour no host can reproduce is driven directly. */
|
|
41
|
+
class TestHost extends WatcherHost {
|
|
42
|
+
|
|
43
|
+
/** Set to pretend the watched directory was replaced, which no real file system will do on cue. */
|
|
44
|
+
fakeIdentity: DirectoryIdentity | undefined;
|
|
45
|
+
/** Set to exercise the macOS and Windows name handling on any host. */
|
|
46
|
+
decomposes = false;
|
|
47
|
+
/** Defaults to what this host really is, so Windows and macOS behave like themselves. */
|
|
48
|
+
caseInsensitive = super.caseInsensitiveFileNames;
|
|
49
|
+
/** Set to make `fs.watch` refuse, as on EACCES or an exhausted handle budget. */
|
|
50
|
+
refuseToOpen = false;
|
|
51
|
+
|
|
52
|
+
protected listener: WatchEventListener | undefined;
|
|
53
|
+
protected handleEmitter: EventEmitter | undefined;
|
|
54
|
+
protected readonly missing = new Deferred<void>();
|
|
55
|
+
protected missingTarget: string | undefined;
|
|
56
|
+
|
|
57
|
+
/** Resolves once `expectMissing` was told a path and that path was found missing. */
|
|
58
|
+
readonly whenMissing = this.missing.promise;
|
|
59
|
+
|
|
60
|
+
expectMissing(target: string): void {
|
|
61
|
+
this.missingTarget = target;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Runs once during the next directory read, to drive an event while a start is mid-flight. */
|
|
65
|
+
duringNextRead: (() => void | Promise<void>) | undefined;
|
|
66
|
+
|
|
67
|
+
/** How many times a directory was read. */
|
|
68
|
+
reads = 0;
|
|
69
|
+
|
|
70
|
+
override async readChildren(directory: string): Promise<Set<string>> {
|
|
71
|
+
this.reads++;
|
|
72
|
+
const during = this.duringNextRead;
|
|
73
|
+
this.duringNextRead = undefined;
|
|
74
|
+
await during?.();
|
|
75
|
+
return super.readChildren(directory);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Feeds the watcher an event as the platform would. */
|
|
79
|
+
fire(eventType: string, fileName: string | null): void {
|
|
80
|
+
assert.ok(this.listener, 'the watcher has not opened a handle');
|
|
81
|
+
this.listener(eventType, fileName);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Fails the open handle, as Windows does when the watched directory goes away. */
|
|
85
|
+
fail(error: Error): void {
|
|
86
|
+
this.handleEmitter?.emit('error', error);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
override get caseInsensitiveFileNames(): boolean {
|
|
90
|
+
return this.caseInsensitive;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Applies the network share check on any host, keyed on the target rather than on `/Volumes`. */
|
|
94
|
+
override isUnsupportedTarget(fsPath: string): boolean {
|
|
95
|
+
return fsPath.includes('network-share');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
override normalizeFileName(fileName: string): string {
|
|
99
|
+
return this.decomposes ? fileName.normalize('NFC') : fileName;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
override async readIdentity(directory: string): Promise<DirectoryIdentity | undefined> {
|
|
103
|
+
return this.fakeIdentity ?? super.readIdentity(directory);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
override async exists(fsPath: string): Promise<boolean> {
|
|
107
|
+
const result = await super.exists(fsPath);
|
|
108
|
+
if (!result && fsPath === this.missingTarget) {
|
|
109
|
+
this.missing.resolve();
|
|
110
|
+
}
|
|
111
|
+
return result;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
override watch(directory: string, listener: WatchEventListener): FSWatcher {
|
|
115
|
+
if (this.refuseToOpen) {
|
|
116
|
+
throw new Error('EACCES');
|
|
117
|
+
}
|
|
118
|
+
this.listener = listener;
|
|
119
|
+
this.handleEmitter = new EventEmitter();
|
|
120
|
+
return Object.assign(this.handleEmitter, { close: () => { this.listener = undefined; } }) as unknown as FSWatcher;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** A temporary directory, the watchers on it, and the changes their clients were told about. */
|
|
125
|
+
class Sandbox extends TempDir implements FileSystemWatcherServiceClient {
|
|
126
|
+
|
|
127
|
+
/** Errors logged by the watchers of this sandbox. */
|
|
128
|
+
readonly errors: unknown[] = [];
|
|
129
|
+
|
|
130
|
+
protected readonly logging = { ...NO_LOGGING, error: (message: string) => this.errors.push(message) };
|
|
131
|
+
protected readonly reports: DidFilesChangedParams[] = [];
|
|
132
|
+
/** The platform boundary every watcher of this sandbox shares. */
|
|
133
|
+
readonly host = new TestHost(this.logging);
|
|
134
|
+
|
|
135
|
+
protected readonly watchers: NodeDirectoryWatcher[] = [];
|
|
136
|
+
protected notify: (() => void) | undefined;
|
|
137
|
+
|
|
138
|
+
constructor() {
|
|
139
|
+
super(fs.realpathSync.native(temp.mkdirSync('node-directory-watcher')));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
onDidFilesChanged(report: DidFilesChangedParams): void {
|
|
143
|
+
this.reports.push(report);
|
|
144
|
+
this.notify?.();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
onError(): void { }
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* A request for every direct child of a directory. `realPath` is what the provider would have resolved
|
|
151
|
+
* the path to, which only differs when a symlink is involved.
|
|
152
|
+
*/
|
|
153
|
+
directory(directoryPath = this.root, clientId = 1, ignored: string[] = [], realPath = directoryPath): DirectoryWatchRequest {
|
|
154
|
+
// Whether the path exists is read when the request is made, as the provider does.
|
|
155
|
+
return { clientId, path: directoryPath, ignored, realPath, exists: fs.existsSync(directoryPath) };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** A request for a single file, which resolves to the file rather than to the directory holding it. */
|
|
159
|
+
file(filePath: string, clientId = 1, ignored: string[] = []): DirectoryWatchRequest {
|
|
160
|
+
return { ...this.directory(filePath, clientId, ignored), realPath: filePath };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** The request as a re-key moves it: made while its path was missing, it waits for it. */
|
|
164
|
+
waiting(request: DirectoryWatchRequest): DirectoryWatchRequest {
|
|
165
|
+
return { ...request, exists: false };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** A watcher whose events the test feeds in, already started. */
|
|
169
|
+
async watching(target = this.root, ...requests: DirectoryWatchRequest[]): Promise<NodeDirectoryWatcher> {
|
|
170
|
+
const watcher = this.starting(target, ...requests);
|
|
171
|
+
await watcher.whenStarted;
|
|
172
|
+
return watcher;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** A watcher whose events the test feeds in, not yet started. */
|
|
176
|
+
starting(target = this.root, ...requests: DirectoryWatchRequest[]): NodeDirectoryWatcher {
|
|
177
|
+
return this.track(new NodeDirectoryWatcher(target, this.logging, this, TIMINGS, this.host), requests);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Feeds the watchers of this sandbox an event as the platform would. */
|
|
181
|
+
fire(eventType: string, fileName: string | null): void {
|
|
182
|
+
this.host.fire(eventType, fileName);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** A watcher driven by a real `fs.watch` handle, already started. */
|
|
186
|
+
async watchingForReal(target = this.root, ...requests: DirectoryWatchRequest[]): Promise<NodeDirectoryWatcher> {
|
|
187
|
+
const watcher = this.track(new NodeDirectoryWatcher(target, this.logging, this, TIMINGS, new WatcherHost(this.logging)), requests);
|
|
188
|
+
await watcher.whenStarted;
|
|
189
|
+
await this.warmUp(watcher, target);
|
|
190
|
+
return watcher;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* macOS FSEvents starts asynchronously after `fs.watch` returns and drops what happens meanwhile, so keep
|
|
195
|
+
* touching a probe file until a change is delivered, proving the handle live before the test acts.
|
|
196
|
+
*/
|
|
197
|
+
protected async warmUp(watcher: NodeDirectoryWatcher, target: string): Promise<void> {
|
|
198
|
+
const directory = fs.statSync(target).isDirectory() ? target : path.dirname(target);
|
|
199
|
+
watcher.addRequest(9999, this.directory(directory, 99));
|
|
200
|
+
const probe = path.join(directory, '.warmup');
|
|
201
|
+
const deadline = Date.now() + 5000;
|
|
202
|
+
while (!this.reported(99).some(entry => entry.includes('.warmup')) && Date.now() < deadline) {
|
|
203
|
+
fs.writeFileSync(probe, 'ping');
|
|
204
|
+
await wait(100);
|
|
205
|
+
}
|
|
206
|
+
fs.removeSync(probe);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** What a client was told, as `'<change> <path relative to the root>'`, the root itself being `'.'`. */
|
|
210
|
+
reported(clientId = 1): string[] {
|
|
211
|
+
return this.reports
|
|
212
|
+
.filter(report => report.clients?.includes(clientId))
|
|
213
|
+
.flatMap(report => report.changes)
|
|
214
|
+
.map(change => {
|
|
215
|
+
const relative = path.relative(this.root, FileUri.fsPath(change.uri)).split(path.sep).join('/');
|
|
216
|
+
return `${CHANGE_NAMES[change.type]} ${relative || '.'}`;
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** Waits for the client to have been told exactly this, and for a moment longer to catch anything extra. */
|
|
221
|
+
async expect(clientId: number, ...expected: string[]): Promise<void> {
|
|
222
|
+
await this.settle(() => this.reported(clientId).length >= expected.length);
|
|
223
|
+
assert.deepStrictEqual(this.reported(clientId), expected);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** Waits for the client to have been told at least this, tolerating whatever else the platform reports. */
|
|
227
|
+
async expectAmong(clientId: number, ...expected: string[]): Promise<void> {
|
|
228
|
+
await this.settle(() => expected.every(entry => this.reported(clientId).includes(entry)));
|
|
229
|
+
expected.forEach(entry => assert.ok(this.reported(clientId).includes(entry),
|
|
230
|
+
`expected "${entry}" among ${JSON.stringify(this.reported(clientId))}`));
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
dispose(): void {
|
|
234
|
+
this.watchers.splice(0).forEach(watcher => watcher.dispose());
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
protected track<T extends NodeDirectoryWatcher>(watcher: T, requests: DirectoryWatchRequest[]): T {
|
|
238
|
+
this.watchers.push(watcher);
|
|
239
|
+
requests.forEach((request, index) => watcher.addRequest(index, request));
|
|
240
|
+
return watcher;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
protected async settle(reached: () => boolean): Promise<void> {
|
|
244
|
+
const deadline = Date.now() + 5000;
|
|
245
|
+
while (!reached() && Date.now() < deadline) {
|
|
246
|
+
await new Promise<void>(resolve => {
|
|
247
|
+
this.notify = resolve;
|
|
248
|
+
setTimeout(resolve, 5);
|
|
249
|
+
});
|
|
250
|
+
this.notify = undefined;
|
|
251
|
+
}
|
|
252
|
+
await wait(SETTLE_DELAY);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
describe('node-directory-watcher', function (): void {
|
|
257
|
+
|
|
258
|
+
this.timeout(20000);
|
|
259
|
+
|
|
260
|
+
let box: Sandbox;
|
|
261
|
+
|
|
262
|
+
beforeEach(() => {
|
|
263
|
+
box = new Sandbox();
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
afterEach(() => {
|
|
267
|
+
box.dispose();
|
|
268
|
+
track.cleanupSync();
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
describe('resolving changes', () => {
|
|
272
|
+
|
|
273
|
+
it('reports a new direct child as added, and a known one as updated', async () => {
|
|
274
|
+
await box.watching(box.root, box.directory());
|
|
275
|
+
|
|
276
|
+
box.write('a.txt');
|
|
277
|
+
box.fire('rename', 'a.txt');
|
|
278
|
+
await box.expect(1, 'added a.txt');
|
|
279
|
+
|
|
280
|
+
box.fire('rename', 'a.txt');
|
|
281
|
+
await box.expect(1, 'added a.txt', 'updated a.txt');
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it('reports a modification as updated', async () => {
|
|
285
|
+
box.write('a.txt');
|
|
286
|
+
await box.watching(box.root, box.directory());
|
|
287
|
+
|
|
288
|
+
box.fire('change', 'a.txt');
|
|
289
|
+
|
|
290
|
+
await box.expect(1, 'updated a.txt');
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it('reports a modification during the first read as updated, even once its batch was flushed', async () => {
|
|
294
|
+
box.write('a.txt');
|
|
295
|
+
box.host.duringNextRead = async () => {
|
|
296
|
+
box.fire('change', 'a.txt');
|
|
297
|
+
await wait(TIMINGS.changeDelay * 4);
|
|
298
|
+
};
|
|
299
|
+
await box.watching(box.root, box.directory());
|
|
300
|
+
|
|
301
|
+
await box.expect(1, 'updated a.txt');
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it('reports a creation during the first read as added, even once its batch was flushed', async () => {
|
|
305
|
+
box.host.duringNextRead = async () => {
|
|
306
|
+
box.write('a.txt');
|
|
307
|
+
box.fire('rename', 'a.txt');
|
|
308
|
+
await wait(TIMINGS.changeDelay * 4);
|
|
309
|
+
};
|
|
310
|
+
await box.watching(box.root, box.directory());
|
|
311
|
+
|
|
312
|
+
await box.expect(1, 'added a.txt');
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it('reports a deletion only once the grace period passed', async () => {
|
|
316
|
+
box.write('a.txt');
|
|
317
|
+
await box.watching(box.root, box.directory());
|
|
318
|
+
|
|
319
|
+
box.remove('a.txt');
|
|
320
|
+
box.fire('rename', 'a.txt');
|
|
321
|
+
assert.deepStrictEqual(box.reported(1), [], 'nothing is reported before the grace period');
|
|
322
|
+
|
|
323
|
+
await box.expect(1, 'deleted a.txt');
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
it('reports an atomic save as an update rather than a deletion', async () => {
|
|
327
|
+
box.write('a.txt');
|
|
328
|
+
await box.watching(box.root, box.directory());
|
|
329
|
+
|
|
330
|
+
box.remove('a.txt');
|
|
331
|
+
box.fire('rename', 'a.txt');
|
|
332
|
+
box.write('a.txt');
|
|
333
|
+
|
|
334
|
+
await box.expect(1, 'updated a.txt');
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
it('still confirms a deletion when another one on its timer is called off', async () => {
|
|
338
|
+
box.write('a.txt');
|
|
339
|
+
box.write('b.txt');
|
|
340
|
+
// A long grace period, so the deletions wait for the second batch on a slow host.
|
|
341
|
+
const watcher = new NodeDirectoryWatcher(box.root, NO_LOGGING, box, { ...TIMINGS, deleteDelay: 1000 }, box.host);
|
|
342
|
+
watcher.addRequest(0, box.directory());
|
|
343
|
+
await watcher.whenStarted;
|
|
344
|
+
const lookedUp = new Deferred<void>();
|
|
345
|
+
const childLookup = box.host.childLookup.bind(box.host);
|
|
346
|
+
box.host.childLookup = directory => {
|
|
347
|
+
const storedName = childLookup(directory);
|
|
348
|
+
return async fileName => {
|
|
349
|
+
const stored = await storedName(fileName);
|
|
350
|
+
if (fileName === 'b.txt') {
|
|
351
|
+
lookedUp.resolve();
|
|
352
|
+
}
|
|
353
|
+
return stored;
|
|
354
|
+
};
|
|
355
|
+
};
|
|
356
|
+
try {
|
|
357
|
+
box.remove('a.txt');
|
|
358
|
+
box.remove('b.txt');
|
|
359
|
+
box.fire('rename', 'a.txt');
|
|
360
|
+
box.fire('rename', 'b.txt');
|
|
361
|
+
// The batch looked up both names, so it found a.txt missing.
|
|
362
|
+
await lookedUp.promise;
|
|
363
|
+
box.write('a.txt');
|
|
364
|
+
box.fire('rename', 'a.txt');
|
|
365
|
+
|
|
366
|
+
await box.expect(1, 'updated a.txt', 'deleted b.txt');
|
|
367
|
+
} finally {
|
|
368
|
+
watcher.dispose();
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
it('reports a file that appears and vanishes within the grace period as both', async () => {
|
|
373
|
+
await box.watching(box.root, box.directory());
|
|
374
|
+
|
|
375
|
+
box.fire('rename', 'ghost.txt');
|
|
376
|
+
|
|
377
|
+
await box.expect(1, 'added ghost.txt', 'deleted ghost.txt');
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
it('rescans when the platform reports a change without a file name', async () => {
|
|
381
|
+
box.write('gone.txt');
|
|
382
|
+
await box.watching(box.root, box.directory());
|
|
383
|
+
|
|
384
|
+
box.write('new.txt');
|
|
385
|
+
box.remove('gone.txt');
|
|
386
|
+
// eslint-disable-next-line no-null/no-null
|
|
387
|
+
box.fire('change', null);
|
|
388
|
+
|
|
389
|
+
await box.expect(1, 'deleted gone.txt', 'added new.txt');
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
it('reports a deletion once when its rename follows a rescan in the same batch', async () => {
|
|
393
|
+
box.write('a.txt');
|
|
394
|
+
await box.watching(box.root, box.directory());
|
|
395
|
+
|
|
396
|
+
box.remove('a.txt');
|
|
397
|
+
// eslint-disable-next-line no-null/no-null
|
|
398
|
+
box.fire('change', null);
|
|
399
|
+
box.fire('rename', 'a.txt');
|
|
400
|
+
|
|
401
|
+
await box.expect(1, 'deleted a.txt');
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
it('reports a deletion once when a rescan settles it before the grace period', async () => {
|
|
405
|
+
box.write('a.txt');
|
|
406
|
+
await box.watching(box.root, box.directory());
|
|
407
|
+
|
|
408
|
+
box.remove('a.txt');
|
|
409
|
+
box.fire('rename', 'a.txt');
|
|
410
|
+
// eslint-disable-next-line no-null/no-null
|
|
411
|
+
box.fire('change', null);
|
|
412
|
+
|
|
413
|
+
await box.expect(1, 'deleted a.txt');
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
it('reports a change to an unknown child as added, and keeps it known', async () => {
|
|
417
|
+
await box.watching(box.root, box.directory());
|
|
418
|
+
|
|
419
|
+
// A file the snapshot never saw, as happens when it changes while the directory is being read.
|
|
420
|
+
box.write('late.txt');
|
|
421
|
+
box.fire('change', 'late.txt');
|
|
422
|
+
await box.expect(1, 'added late.txt');
|
|
423
|
+
|
|
424
|
+
// Now known, so losing it is a plain deletion rather than an appearance out of nowhere.
|
|
425
|
+
box.remove('late.txt');
|
|
426
|
+
box.fire('rename', 'late.txt');
|
|
427
|
+
await box.expect(1, 'added late.txt', 'deleted late.txt');
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
it('ignores an event naming a path below the watched directory', async () => {
|
|
431
|
+
box.mkdir('nested');
|
|
432
|
+
box.write('nested', 'deep.txt');
|
|
433
|
+
box.write('sentinel.txt');
|
|
434
|
+
await box.watching(box.root, box.directory());
|
|
435
|
+
|
|
436
|
+
box.fire('rename', path.join('nested', 'deep.txt'));
|
|
437
|
+
box.fire('rename', 'sentinel.txt');
|
|
438
|
+
|
|
439
|
+
await box.expect(1, 'updated sentinel.txt');
|
|
440
|
+
});
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
describe('routing', () => {
|
|
444
|
+
|
|
445
|
+
it('checks file requests against the children without a pass per child on every batch', async () => {
|
|
446
|
+
for (let i = 0; i < 1000; i++) {
|
|
447
|
+
box.write(`file-${i}.txt`);
|
|
448
|
+
}
|
|
449
|
+
const requests = Array.from({ length: 10 }, (_, i) => box.file(box.path(`file-${i}.txt`)));
|
|
450
|
+
await box.watching(box.root, ...requests);
|
|
451
|
+
let comparisons = 0;
|
|
452
|
+
const samePath = box.host.samePath.bind(box.host);
|
|
453
|
+
box.host.samePath = (expected, actual) => {
|
|
454
|
+
comparisons++;
|
|
455
|
+
return samePath(expected, actual);
|
|
456
|
+
};
|
|
457
|
+
|
|
458
|
+
box.fire('change', 'file-0.txt');
|
|
459
|
+
await box.expect(1, 'updated file-0.txt');
|
|
460
|
+
assert.ok(comparisons < 1000, `${comparisons} path comparisons for one change`);
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
it('routes a batch to file requests without a comparison per change', async () => {
|
|
464
|
+
for (let i = 0; i < 1000; i++) {
|
|
465
|
+
box.write(`file-${i}.txt`);
|
|
466
|
+
}
|
|
467
|
+
const requests = Array.from({ length: 10 }, (_, i) => box.file(box.path(`file-${i}.txt`)));
|
|
468
|
+
await box.watching(box.root, ...requests);
|
|
469
|
+
let comparisons = 0;
|
|
470
|
+
const samePath = box.host.samePath.bind(box.host);
|
|
471
|
+
box.host.samePath = (expected, actual) => {
|
|
472
|
+
comparisons++;
|
|
473
|
+
return samePath(expected, actual);
|
|
474
|
+
};
|
|
475
|
+
|
|
476
|
+
for (let i = 0; i < 1000; i++) {
|
|
477
|
+
box.fire('change', `file-${i}.txt`);
|
|
478
|
+
}
|
|
479
|
+
await box.expect(1, ...requests.map((_, i) => `updated file-${i}.txt`));
|
|
480
|
+
// One per event tells it from the watched directory itself, but none may pair a request with a change.
|
|
481
|
+
assert.ok(comparisons < requests.length * 1000, `${comparisons} path comparisons for one batch`);
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
it('tells a file request about its own file only', async () => {
|
|
485
|
+
await box.watching(box.root, box.file(box.path('wanted.txt')));
|
|
486
|
+
|
|
487
|
+
box.write('other.txt');
|
|
488
|
+
box.write('wanted.txt');
|
|
489
|
+
box.fire('rename', 'other.txt');
|
|
490
|
+
box.fire('rename', 'wanted.txt');
|
|
491
|
+
|
|
492
|
+
await box.expect(1, 'added wanted.txt');
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
it('applies the excludes of each request separately', async () => {
|
|
496
|
+
await box.watching(box.root, box.directory(box.root, 1, ['**/node_modules']), box.directory(box.root, 2));
|
|
497
|
+
|
|
498
|
+
box.mkdir('node_modules');
|
|
499
|
+
box.fire('rename', 'node_modules');
|
|
500
|
+
|
|
501
|
+
await box.expect(2, 'added node_modules');
|
|
502
|
+
await box.expect(1);
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
it('does not apply the excludes of a request to the path it asked to watch', async () => {
|
|
506
|
+
const file = box.write('a.txt');
|
|
507
|
+
await box.watching(box.root, box.file(file, 1, ['**/a.txt']));
|
|
508
|
+
|
|
509
|
+
box.fire('change', 'a.txt');
|
|
510
|
+
|
|
511
|
+
await box.expect(1, 'updated a.txt');
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
it('tells a client holding overlapping requests once, and other clients independently', async () => {
|
|
515
|
+
const file = box.write('a.txt');
|
|
516
|
+
await box.watching(box.root, box.directory(), box.file(file), box.directory(box.root, 2));
|
|
517
|
+
|
|
518
|
+
box.fire('change', 'a.txt');
|
|
519
|
+
|
|
520
|
+
await box.expect(1, 'updated a.txt');
|
|
521
|
+
await box.expect(2, 'updated a.txt');
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
it('reports changes under the path each request asked for', async () => {
|
|
525
|
+
const real = box.mkdir('real');
|
|
526
|
+
fs.symlinkSync(real, box.path('link'), isWindows ? 'junction' : 'dir');
|
|
527
|
+
await box.watching(real, box.directory(real), box.directory(box.path('link'), 2, [], real));
|
|
528
|
+
|
|
529
|
+
box.write('real', 'a.txt');
|
|
530
|
+
box.fire('rename', 'a.txt');
|
|
531
|
+
|
|
532
|
+
await box.expect(1, 'added real/a.txt');
|
|
533
|
+
await box.expect(2, 'added link/a.txt');
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
it('matches a decomposed file name against the composed path a request asked for', async () => {
|
|
537
|
+
const composed = 'café.txt'.normalize('NFC');
|
|
538
|
+
await box.watching(box.root, box.file(box.path(composed)));
|
|
539
|
+
box.host.decomposes = true;
|
|
540
|
+
|
|
541
|
+
box.write(composed);
|
|
542
|
+
box.fire('rename', 'café.txt'.normalize('NFD'));
|
|
543
|
+
|
|
544
|
+
await box.expect(1, `added ${composed}`);
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
it('matches a composed file name against the decomposed path a request resolved to', async () => {
|
|
548
|
+
const decomposed = 'café.txt'.normalize('NFD');
|
|
549
|
+
const file = box.write(decomposed);
|
|
550
|
+
box.host.decomposes = true;
|
|
551
|
+
// `realpath` returns the name as HFS+ stores it, decomposed.
|
|
552
|
+
await box.watching(box.root, box.file(file));
|
|
553
|
+
|
|
554
|
+
box.fire('change', decomposed);
|
|
555
|
+
|
|
556
|
+
await box.expect(1, `updated ${decomposed}`);
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
it('matches a file name irrespective of case where the platform does', async () => {
|
|
560
|
+
await box.watching(box.root, box.file(box.path('Wanted.txt')));
|
|
561
|
+
box.host.caseInsensitive = true;
|
|
562
|
+
|
|
563
|
+
box.write('wanted.txt');
|
|
564
|
+
box.fire('rename', 'wanted.txt');
|
|
565
|
+
|
|
566
|
+
await box.expect(1, 'added Wanted.txt');
|
|
567
|
+
});
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
describe('the watched directory', () => {
|
|
571
|
+
|
|
572
|
+
it('is reported and watched once it appears', async () => {
|
|
573
|
+
const target = box.path('later');
|
|
574
|
+
box.host.expectMissing(target);
|
|
575
|
+
const watcher = box.starting(target, box.directory(target));
|
|
576
|
+
|
|
577
|
+
await box.host.whenMissing;
|
|
578
|
+
fs.mkdirSync(target);
|
|
579
|
+
await watcher.whenStarted;
|
|
580
|
+
box.fire('rename', path.basename(box.write('later', 'a.txt')));
|
|
581
|
+
|
|
582
|
+
await box.expect(1, 'added later', 'added later/a.txt');
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
it('is reported once it appears, even when the start that found it is superseded', async () => {
|
|
586
|
+
const target = box.path('later');
|
|
587
|
+
box.host.expectMissing(target);
|
|
588
|
+
box.starting(target, box.directory(target));
|
|
589
|
+
|
|
590
|
+
await box.host.whenMissing;
|
|
591
|
+
box.host.duringNextRead = () => box.host.fail(new Error('EPERM'));
|
|
592
|
+
fs.mkdirSync(target);
|
|
593
|
+
|
|
594
|
+
await box.expect(1, 'added later');
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
it('is the parent when the target turns out to be a file', async () => {
|
|
598
|
+
const target = box.path('later.txt');
|
|
599
|
+
box.host.expectMissing(target);
|
|
600
|
+
const watcher = box.starting(target, box.directory(target));
|
|
601
|
+
|
|
602
|
+
await box.host.whenMissing;
|
|
603
|
+
fs.writeFileSync(target, 'content');
|
|
604
|
+
await watcher.whenStarted;
|
|
605
|
+
box.write('sibling.txt');
|
|
606
|
+
box.fire('rename', 'sibling.txt');
|
|
607
|
+
box.fire('change', 'later.txt');
|
|
608
|
+
|
|
609
|
+
await box.expect(1, 'added later.txt', 'updated later.txt');
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
it('is followed through a symbolic link above it once it appears', async () => {
|
|
613
|
+
const real = box.mkdir('real');
|
|
614
|
+
fs.symlinkSync(real, box.path('link'), isWindows ? 'junction' : 'dir');
|
|
615
|
+
const target = box.path('link', 'later');
|
|
616
|
+
box.host.expectMissing(target);
|
|
617
|
+
// Missing, so the provider could not resolve the link and kept the path as it was asked for.
|
|
618
|
+
const watcher = box.starting(target, box.directory(target));
|
|
619
|
+
|
|
620
|
+
await box.host.whenMissing;
|
|
621
|
+
fs.mkdirSync(path.join(real, 'later'));
|
|
622
|
+
await watcher.whenStarted;
|
|
623
|
+
box.write('real', 'later', 'a.txt');
|
|
624
|
+
box.fire('rename', 'a.txt');
|
|
625
|
+
|
|
626
|
+
await box.expect(1, 'added link/later', 'added link/later/a.txt');
|
|
627
|
+
});
|
|
628
|
+
|
|
629
|
+
it('stays on the parent after the file it was created for is gone', async () => {
|
|
630
|
+
const target = box.path('later.txt');
|
|
631
|
+
box.host.expectMissing(target);
|
|
632
|
+
const watcher = box.starting(target, box.directory(target));
|
|
633
|
+
|
|
634
|
+
await box.host.whenMissing;
|
|
635
|
+
fs.writeFileSync(target, 'content');
|
|
636
|
+
await watcher.whenStarted;
|
|
637
|
+
// Stands for a request a re-key merged in once the file appeared.
|
|
638
|
+
watcher.addRequest(1, box.directory(box.root, 2));
|
|
639
|
+
box.remove('later.txt');
|
|
640
|
+
box.host.fail(new Error('EPERM'));
|
|
641
|
+
await wait(TIMINGS.existencePollDelay * 2);
|
|
642
|
+
|
|
643
|
+
box.write('after.txt');
|
|
644
|
+
box.fire('rename', 'after.txt');
|
|
645
|
+
await box.expectAmong(2, 'added after.txt');
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
it('is reported as deleted, and its recovery reports what changed meanwhile', async () => {
|
|
649
|
+
const target = box.mkdir('workspace');
|
|
650
|
+
box.write('workspace', 'before.txt');
|
|
651
|
+
await box.watching(target, box.directory(target));
|
|
652
|
+
|
|
653
|
+
box.remove('workspace');
|
|
654
|
+
box.fire('rename', 'workspace');
|
|
655
|
+
await box.expect(1, 'deleted workspace');
|
|
656
|
+
|
|
657
|
+
box.mkdir('workspace');
|
|
658
|
+
box.write('workspace', 'after.txt');
|
|
659
|
+
|
|
660
|
+
await box.expect(1, 'deleted workspace', 'added workspace', 'deleted workspace/before.txt', 'added workspace/after.txt');
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
it('is reported added once when it turns into a file', async () => {
|
|
664
|
+
const target = box.mkdir('later');
|
|
665
|
+
box.write('later', 'a.txt');
|
|
666
|
+
await box.watching(target, box.directory(target));
|
|
667
|
+
|
|
668
|
+
box.remove('later');
|
|
669
|
+
box.write('later');
|
|
670
|
+
box.fire('rename', 'later');
|
|
671
|
+
|
|
672
|
+
await box.expect(1, 'deleted later', 'added later');
|
|
673
|
+
assert.deepStrictEqual(box.errors, []);
|
|
674
|
+
});
|
|
675
|
+
|
|
676
|
+
it('keeps a file request waiting while it is a file', async () => {
|
|
677
|
+
const directory = box.mkdir('dir');
|
|
678
|
+
const file = box.write('dir', 'f.txt');
|
|
679
|
+
await box.watching(directory, box.file(file));
|
|
680
|
+
|
|
681
|
+
box.remove('dir');
|
|
682
|
+
box.write('dir');
|
|
683
|
+
box.fire('rename', 'dir');
|
|
684
|
+
await box.expect(1, 'deleted dir/f.txt');
|
|
685
|
+
|
|
686
|
+
box.remove('dir');
|
|
687
|
+
box.mkdir('dir');
|
|
688
|
+
box.write('dir', 'f.txt');
|
|
689
|
+
await box.expect(1, 'deleted dir/f.txt', 'added dir/f.txt');
|
|
690
|
+
});
|
|
691
|
+
|
|
692
|
+
it('keeps the file request it was created for waiting while it is a file', async () => {
|
|
693
|
+
box.mkdir('dir');
|
|
694
|
+
const file = box.path('dir', 'f.txt');
|
|
695
|
+
box.host.expectMissing(file);
|
|
696
|
+
const watcher = box.starting(file, box.file(file));
|
|
697
|
+
await box.host.whenMissing;
|
|
698
|
+
box.write('dir', 'f.txt');
|
|
699
|
+
await watcher.whenStarted;
|
|
700
|
+
|
|
701
|
+
box.remove('dir');
|
|
702
|
+
box.write('dir');
|
|
703
|
+
box.fire('rename', 'dir');
|
|
704
|
+
await box.expect(1, 'added dir/f.txt', 'deleted dir/f.txt');
|
|
705
|
+
|
|
706
|
+
box.remove('dir');
|
|
707
|
+
box.mkdir('dir');
|
|
708
|
+
box.write('dir', 'f.txt');
|
|
709
|
+
await box.expect(1, 'added dir/f.txt', 'deleted dir/f.txt', 'added dir/f.txt');
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
it('tells a file request once that its file went with the directory', async () => {
|
|
713
|
+
const target = box.mkdir('workspace');
|
|
714
|
+
const file = box.write('workspace', 'a.txt');
|
|
715
|
+
await box.watching(target, box.directory(target), box.file(file, 2));
|
|
716
|
+
|
|
717
|
+
box.remove('workspace');
|
|
718
|
+
box.fire('rename', 'workspace');
|
|
719
|
+
await box.expect(1, 'deleted workspace');
|
|
720
|
+
box.mkdir('workspace');
|
|
721
|
+
|
|
722
|
+
await box.expect(1, 'deleted workspace', 'added workspace', 'deleted workspace/a.txt');
|
|
723
|
+
await box.expect(2, 'deleted workspace/a.txt');
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
it('tells a file request once about its file across a loss and recovery of the directory', async () => {
|
|
727
|
+
const target = box.mkdir('workspace');
|
|
728
|
+
const file = box.write('workspace', 'a.txt');
|
|
729
|
+
await box.watching(target, box.directory(target), box.file(file, 2));
|
|
730
|
+
|
|
731
|
+
box.remove('workspace', 'a.txt');
|
|
732
|
+
box.fire('rename', 'a.txt');
|
|
733
|
+
await box.expectAmong(2, 'deleted workspace/a.txt');
|
|
734
|
+
|
|
735
|
+
box.remove('workspace');
|
|
736
|
+
box.fire('rename', 'workspace');
|
|
737
|
+
await box.expectAmong(1, 'deleted workspace');
|
|
738
|
+
|
|
739
|
+
box.mkdir('workspace');
|
|
740
|
+
box.write('workspace', 'a.txt');
|
|
741
|
+
await box.expectAmong(1, 'added workspace');
|
|
742
|
+
await box.expect(2, 'deleted workspace/a.txt', 'added workspace/a.txt');
|
|
743
|
+
});
|
|
744
|
+
|
|
745
|
+
it('tells a file request once about its file when it is created during the recovery read', async () => {
|
|
746
|
+
const target = box.mkdir('ws');
|
|
747
|
+
const file = box.write('ws', 'a.txt');
|
|
748
|
+
await box.watching(target, box.directory(target), box.file(file, 2));
|
|
749
|
+
|
|
750
|
+
box.remove('ws', 'a.txt');
|
|
751
|
+
box.fire('rename', 'a.txt');
|
|
752
|
+
await box.expect(2, 'deleted ws/a.txt');
|
|
753
|
+
box.remove('ws');
|
|
754
|
+
box.fire('rename', 'ws');
|
|
755
|
+
await box.expectAmong(1, 'deleted ws');
|
|
756
|
+
|
|
757
|
+
box.host.duringNextRead = () => {
|
|
758
|
+
box.write('ws', 'a.txt');
|
|
759
|
+
box.fire('rename', 'a.txt');
|
|
760
|
+
};
|
|
761
|
+
box.mkdir('ws');
|
|
762
|
+
|
|
763
|
+
await box.expect(1, 'deleted ws/a.txt', 'deleted ws', 'added ws', 'added ws/a.txt');
|
|
764
|
+
await box.expect(2, 'deleted ws/a.txt', 'added ws/a.txt');
|
|
765
|
+
});
|
|
766
|
+
|
|
767
|
+
it('is reported deleted once when it goes again while its recovery reads it', async () => {
|
|
768
|
+
const target = box.mkdir('ws');
|
|
769
|
+
box.write('ws', 'a.txt');
|
|
770
|
+
await box.watching(target, box.directory(target));
|
|
771
|
+
box.remove('ws');
|
|
772
|
+
box.fire('rename', 'ws');
|
|
773
|
+
await box.expect(1, 'deleted ws');
|
|
774
|
+
|
|
775
|
+
box.host.duringNextRead = () => {
|
|
776
|
+
box.remove('ws');
|
|
777
|
+
box.fire('rename', 'ws');
|
|
778
|
+
};
|
|
779
|
+
box.mkdir('ws');
|
|
780
|
+
box.write('ws', 'a.txt');
|
|
781
|
+
await wait(TIMINGS.existencePollDelay * 4);
|
|
782
|
+
box.mkdir('ws');
|
|
783
|
+
box.write('ws', 'a.txt');
|
|
784
|
+
|
|
785
|
+
await box.expect(1, 'deleted ws', 'added ws');
|
|
786
|
+
});
|
|
787
|
+
|
|
788
|
+
it('tells a file request its file went when the first start finds the directory gone after its read', async () => {
|
|
789
|
+
const target = box.mkdir('ws');
|
|
790
|
+
const file = box.write('ws', 'a.txt');
|
|
791
|
+
box.host.duringNextRead = () => box.remove('ws');
|
|
792
|
+
await box.watching(target, box.directory(target), box.file(file, 2));
|
|
793
|
+
|
|
794
|
+
await box.expect(1, 'deleted ws');
|
|
795
|
+
await box.expect(2, 'deleted ws/a.txt');
|
|
796
|
+
});
|
|
797
|
+
|
|
798
|
+
it('reports nothing when the first start finds the directory briefly moved away', async () => {
|
|
799
|
+
const target = box.mkdir('ws');
|
|
800
|
+
box.write('ws', 'a.txt');
|
|
801
|
+
const aside = box.path('ws-aside');
|
|
802
|
+
const readIdentity = box.host.readIdentity.bind(box.host);
|
|
803
|
+
box.host.duringNextRead = () => fs.renameSync(target, aside);
|
|
804
|
+
box.host.readIdentity = async directory => {
|
|
805
|
+
const identity = await readIdentity(directory);
|
|
806
|
+
if (!identity && fs.existsSync(aside)) {
|
|
807
|
+
// Moved back: the same inode and birth time.
|
|
808
|
+
fs.renameSync(aside, target);
|
|
809
|
+
}
|
|
810
|
+
return identity;
|
|
811
|
+
};
|
|
812
|
+
await box.watching(target, box.directory(target));
|
|
813
|
+
|
|
814
|
+
await box.expect(1);
|
|
815
|
+
});
|
|
816
|
+
|
|
817
|
+
it('does not report an existing file added when the first read is found moved away', async () => {
|
|
818
|
+
const target = box.mkdir('ws');
|
|
819
|
+
box.write('ws', 'a.txt');
|
|
820
|
+
const readIdentity = box.host.readIdentity.bind(box.host);
|
|
821
|
+
let identityReads = 0;
|
|
822
|
+
box.host.readIdentity = async directory => {
|
|
823
|
+
if (++identityReads !== 2) {
|
|
824
|
+
return readIdentity(directory);
|
|
825
|
+
}
|
|
826
|
+
// The check after the first read: moved away and back.
|
|
827
|
+
fs.renameSync(target, box.path('ws-aside'));
|
|
828
|
+
const identity = await readIdentity(directory);
|
|
829
|
+
fs.renameSync(box.path('ws-aside'), target);
|
|
830
|
+
return identity;
|
|
831
|
+
};
|
|
832
|
+
box.host.duringNextRead = async () => {
|
|
833
|
+
box.write('ws', 'a.txt');
|
|
834
|
+
box.fire('change', 'a.txt');
|
|
835
|
+
await wait(TIMINGS.changeDelay * 4);
|
|
836
|
+
};
|
|
837
|
+
await box.watching(target, box.directory(target));
|
|
838
|
+
|
|
839
|
+
await box.expect(1);
|
|
840
|
+
});
|
|
841
|
+
|
|
842
|
+
it('tells an adopted request nothing while the start recovering the directory finds it gone again', async () => {
|
|
843
|
+
const target = box.mkdir('cfg');
|
|
844
|
+
const file = box.write('cfg', 'settings.json');
|
|
845
|
+
const watcher = await box.watching(target, box.directory(target));
|
|
846
|
+
box.remove('cfg');
|
|
847
|
+
box.fire('rename', 'cfg');
|
|
848
|
+
await box.expect(1, 'deleted cfg');
|
|
849
|
+
|
|
850
|
+
box.host.duringNextRead = () => {
|
|
851
|
+
watcher.adoptRequests(new Map([[7, box.waiting(box.file(file, 2))]]));
|
|
852
|
+
box.remove('cfg');
|
|
853
|
+
};
|
|
854
|
+
box.mkdir('cfg');
|
|
855
|
+
box.write('cfg', 'settings.json');
|
|
856
|
+
await wait(TIMINGS.existencePollDelay * 6);
|
|
857
|
+
|
|
858
|
+
await box.expect(1, 'deleted cfg');
|
|
859
|
+
await box.expect(2);
|
|
860
|
+
});
|
|
861
|
+
|
|
862
|
+
it('reports a child once when the start recovering the directory finds it replaced after a batch was queued', async () => {
|
|
863
|
+
const target = box.mkdir('ws');
|
|
864
|
+
await box.watching(target, box.directory(target));
|
|
865
|
+
box.remove('ws');
|
|
866
|
+
box.fire('rename', 'ws');
|
|
867
|
+
await box.expect(1, 'deleted ws');
|
|
868
|
+
|
|
869
|
+
box.host.duringNextRead = async () => {
|
|
870
|
+
box.write('ws', 'a.txt');
|
|
871
|
+
box.fire('rename', 'a.txt');
|
|
872
|
+
await wait(TIMINGS.changeDelay * 4);
|
|
873
|
+
fs.renameSync(target, box.path('ws-old'));
|
|
874
|
+
box.mkdir('ws');
|
|
875
|
+
box.write('ws', 'a.txt');
|
|
876
|
+
};
|
|
877
|
+
box.mkdir('ws');
|
|
878
|
+
|
|
879
|
+
await box.expect(1, 'deleted ws', 'added ws', 'added ws/a.txt');
|
|
880
|
+
});
|
|
881
|
+
|
|
882
|
+
it('opens no handle on its path once that is a file', async () => {
|
|
883
|
+
const target = box.mkdir('later');
|
|
884
|
+
const watcher = await box.watching(target, box.directory(target));
|
|
885
|
+
const resolveTarget = box.host.resolveTarget.bind(box.host);
|
|
886
|
+
box.host.resolveTarget = async fsPath => {
|
|
887
|
+
const resolved = await resolveTarget(fsPath);
|
|
888
|
+
box.host.resolveTarget = resolveTarget;
|
|
889
|
+
// Turns into a file right after it resolved as a directory.
|
|
890
|
+
box.remove('later');
|
|
891
|
+
box.write('later');
|
|
892
|
+
return resolved;
|
|
893
|
+
};
|
|
894
|
+
box.host.fail(new Error('EPERM'));
|
|
895
|
+
|
|
896
|
+
await box.expect(1, 'deleted later', 'added later');
|
|
897
|
+
assert.strictEqual(watcher.directory, box.root);
|
|
898
|
+
});
|
|
899
|
+
|
|
900
|
+
it('tells a directory request it became a file when a restart finds that out', async () => {
|
|
901
|
+
const target = box.mkdir('later');
|
|
902
|
+
box.write('later', 'a.txt');
|
|
903
|
+
const watcher = await box.watching(target, box.directory(target));
|
|
904
|
+
|
|
905
|
+
// The restart's new handle is refused, as on EMFILE, while the directory turns into a file.
|
|
906
|
+
const watch = box.host.watch.bind(box.host);
|
|
907
|
+
box.host.watch = () => {
|
|
908
|
+
box.host.watch = watch;
|
|
909
|
+
box.remove('later');
|
|
910
|
+
box.write('later');
|
|
911
|
+
throw new Error('EMFILE');
|
|
912
|
+
};
|
|
913
|
+
box.host.fail(new Error('EPERM'));
|
|
914
|
+
|
|
915
|
+
await box.expect(1, 'deleted later', 'added later');
|
|
916
|
+
assert.strictEqual(watcher.directory, box.root);
|
|
917
|
+
});
|
|
918
|
+
|
|
919
|
+
it('reports the changes of the batch that finds it replaced', async () => {
|
|
920
|
+
const target = box.mkdir('ws');
|
|
921
|
+
await box.watching(target, box.directory(target));
|
|
922
|
+
|
|
923
|
+
// What inotify reports for: touch ws/a.txt; mv ws ws-old; mkdir ws; touch ws/a.txt
|
|
924
|
+
box.write('ws', 'a.txt');
|
|
925
|
+
fs.renameSync(target, box.path('ws-old'));
|
|
926
|
+
box.mkdir('ws');
|
|
927
|
+
box.write('ws', 'a.txt');
|
|
928
|
+
box.fire('rename', 'a.txt');
|
|
929
|
+
box.fire('rename', 'ws');
|
|
930
|
+
|
|
931
|
+
await box.expect(1, 'added ws/a.txt', 'deleted ws', 'added ws');
|
|
932
|
+
});
|
|
933
|
+
|
|
934
|
+
it('is noticed as replaced while a start reads it', async () => {
|
|
935
|
+
const target = box.mkdir('ws');
|
|
936
|
+
box.host.duringNextRead = () => {
|
|
937
|
+
// After the handle opened on the old directory. Faked, as a file system can reuse the inode.
|
|
938
|
+
box.host.fakeIdentity = { dev: 1, ino: 2, birthtimeMs: 3 };
|
|
939
|
+
box.fire('rename', 'ws');
|
|
940
|
+
};
|
|
941
|
+
await box.watching(target, box.directory(target));
|
|
942
|
+
await box.expect(1, 'deleted ws', 'added ws');
|
|
943
|
+
|
|
944
|
+
box.write('ws', 'a.txt');
|
|
945
|
+
box.fire('rename', 'a.txt');
|
|
946
|
+
await box.expect(1, 'deleted ws', 'added ws', 'added ws/a.txt');
|
|
947
|
+
});
|
|
948
|
+
|
|
949
|
+
it('tells a request that joined during a start it was added again once the start saw it replaced', async () => {
|
|
950
|
+
const watcher = box.starting(box.root, box.directory());
|
|
951
|
+
box.host.duringNextRead = () => {
|
|
952
|
+
// Joins once the path was found, then the start finds the directory replaced.
|
|
953
|
+
watcher.addRequest(1, box.directory(box.root, 2));
|
|
954
|
+
box.host.fakeIdentity = { dev: 1, ino: 2, birthtimeMs: 3 };
|
|
955
|
+
};
|
|
956
|
+
|
|
957
|
+
await box.expect(1, 'deleted .', 'added .');
|
|
958
|
+
await box.expect(2, 'deleted .', 'added .');
|
|
959
|
+
});
|
|
960
|
+
|
|
961
|
+
it('tells a request that joined once it appeared that it went again before the start finished', async () => {
|
|
962
|
+
const target = box.path('later');
|
|
963
|
+
box.host.expectMissing(target);
|
|
964
|
+
const watcher = box.starting(target, box.directory(target));
|
|
965
|
+
await box.host.whenMissing;
|
|
966
|
+
box.host.duringNextRead = () => {
|
|
967
|
+
// Joins once the path was found, then the path goes before the start reports it.
|
|
968
|
+
watcher.addRequest(1, box.directory(target, 2));
|
|
969
|
+
box.remove('later');
|
|
970
|
+
};
|
|
971
|
+
fs.mkdirSync(target);
|
|
972
|
+
await box.expect(2, 'deleted later');
|
|
973
|
+
|
|
974
|
+
fs.mkdirSync(target);
|
|
975
|
+
|
|
976
|
+
await box.expect(1, 'added later');
|
|
977
|
+
await box.expect(2, 'deleted later', 'added later');
|
|
978
|
+
});
|
|
979
|
+
|
|
980
|
+
it('tells a request it went when it goes before the first handle opens', async () => {
|
|
981
|
+
const target = box.mkdir('ws');
|
|
982
|
+
box.host.refuseToOpen = true;
|
|
983
|
+
box.host.expectMissing(target);
|
|
984
|
+
box.starting(target, box.directory(target));
|
|
985
|
+
await wait(TIMINGS.existencePollDelay * 2);
|
|
986
|
+
box.remove('ws');
|
|
987
|
+
await box.host.whenMissing;
|
|
988
|
+
await box.expect(1, 'deleted ws');
|
|
989
|
+
|
|
990
|
+
box.host.refuseToOpen = false;
|
|
991
|
+
box.mkdir('ws');
|
|
992
|
+
|
|
993
|
+
await box.expect(1, 'deleted ws', 'added ws');
|
|
994
|
+
});
|
|
995
|
+
|
|
996
|
+
it('tells a request it appeared when it appears before the first start finds it', async () => {
|
|
997
|
+
const target = box.path('ws');
|
|
998
|
+
const request = box.directory(target);
|
|
999
|
+
box.mkdir('ws');
|
|
1000
|
+
await box.watching(target, request);
|
|
1001
|
+
|
|
1002
|
+
await box.expect(1, 'added ws');
|
|
1003
|
+
});
|
|
1004
|
+
|
|
1005
|
+
it('tells a file request its file went when it goes before the first read', async () => {
|
|
1006
|
+
const file = box.write('a.txt');
|
|
1007
|
+
box.host.duringNextRead = () => box.remove('a.txt');
|
|
1008
|
+
await box.watching(box.root, box.file(file));
|
|
1009
|
+
|
|
1010
|
+
await box.expect(1, 'deleted a.txt');
|
|
1011
|
+
});
|
|
1012
|
+
|
|
1013
|
+
it('tells a request that joined while it was briefly moved away that it is back, before a change inside it', async () => {
|
|
1014
|
+
const target = box.mkdir('ws');
|
|
1015
|
+
const watcher = await box.watching(target, box.directory(target));
|
|
1016
|
+
fs.renameSync(target, box.path('ws-aside'));
|
|
1017
|
+
watcher.addRequest(1, box.directory(target, 2));
|
|
1018
|
+
fs.renameSync(box.path('ws-aside'), target);
|
|
1019
|
+
box.write('ws', 'a.txt');
|
|
1020
|
+
box.fire('rename', 'a.txt');
|
|
1021
|
+
|
|
1022
|
+
await box.expect(1, 'added ws/a.txt');
|
|
1023
|
+
await box.expect(2, 'added ws', 'added ws/a.txt');
|
|
1024
|
+
});
|
|
1025
|
+
|
|
1026
|
+
it('tells a request that joined while it was taken for missing that it went again', async () => {
|
|
1027
|
+
const target = box.mkdir('ws');
|
|
1028
|
+
const watcher = await box.watching(target, box.directory(target));
|
|
1029
|
+
box.remove('ws');
|
|
1030
|
+
box.fire('rename', 'ws');
|
|
1031
|
+
await box.expect(1, 'deleted ws');
|
|
1032
|
+
|
|
1033
|
+
// Joins while it is back for a moment, before the watcher looks again.
|
|
1034
|
+
box.mkdir('ws');
|
|
1035
|
+
watcher.addRequest(1, box.directory(target, 2));
|
|
1036
|
+
box.remove('ws');
|
|
1037
|
+
await box.expect(2, 'deleted ws');
|
|
1038
|
+
|
|
1039
|
+
box.mkdir('ws');
|
|
1040
|
+
|
|
1041
|
+
await box.expect(1, 'deleted ws', 'added ws');
|
|
1042
|
+
await box.expect(2, 'deleted ws', 'added ws');
|
|
1043
|
+
});
|
|
1044
|
+
|
|
1045
|
+
it('is not lost when an event names it, as macOS reports any change inside it', async () => {
|
|
1046
|
+
box.write('a.txt');
|
|
1047
|
+
await box.watching(box.root, box.directory());
|
|
1048
|
+
|
|
1049
|
+
// libuv names an event on the directory after the directory itself.
|
|
1050
|
+
box.fire('rename', path.basename(box.root));
|
|
1051
|
+
box.fire('change', 'a.txt');
|
|
1052
|
+
|
|
1053
|
+
await box.expect(1, 'updated a.txt');
|
|
1054
|
+
});
|
|
1055
|
+
|
|
1056
|
+
it('ignores a change event naming it, as for a metadata change on it', async () => {
|
|
1057
|
+
box.write('a.txt');
|
|
1058
|
+
await box.watching(box.root, box.directory());
|
|
1059
|
+
|
|
1060
|
+
box.fire('change', path.basename(box.root));
|
|
1061
|
+
box.fire('change', 'a.txt');
|
|
1062
|
+
|
|
1063
|
+
await box.expect(1, 'updated a.txt');
|
|
1064
|
+
});
|
|
1065
|
+
|
|
1066
|
+
it('keeps working after being replaced while its inode number was reused', async () => {
|
|
1067
|
+
await box.watching(box.root, box.directory());
|
|
1068
|
+
|
|
1069
|
+
box.host.fakeIdentity = { dev: 1, ino: 2, birthtimeMs: 3 };
|
|
1070
|
+
box.fire('rename', 'a.txt');
|
|
1071
|
+
await box.expect(1, 'deleted .', 'added .');
|
|
1072
|
+
|
|
1073
|
+
box.write('a.txt');
|
|
1074
|
+
box.fire('rename', 'a.txt');
|
|
1075
|
+
await box.expect(1, 'deleted .', 'added .', 'added a.txt');
|
|
1076
|
+
});
|
|
1077
|
+
|
|
1078
|
+
it('reports a handle that will not open once, then recovers when it does', async () => {
|
|
1079
|
+
const watcher = box.starting(box.root);
|
|
1080
|
+
box.host.refuseToOpen = true;
|
|
1081
|
+
|
|
1082
|
+
// Several poll rounds, one report.
|
|
1083
|
+
await wait(TIMINGS.existencePollDelay * 4);
|
|
1084
|
+
assert.strictEqual(box.errors.length, 1, `expected one report, got ${JSON.stringify(box.errors)}`);
|
|
1085
|
+
|
|
1086
|
+
box.host.refuseToOpen = false;
|
|
1087
|
+
watcher.addRequest(0, box.directory());
|
|
1088
|
+
await watcher.whenStarted;
|
|
1089
|
+
box.write('a.txt');
|
|
1090
|
+
box.fire('rename', 'a.txt');
|
|
1091
|
+
|
|
1092
|
+
await box.expect(1, 'added a.txt');
|
|
1093
|
+
});
|
|
1094
|
+
|
|
1095
|
+
it('recovers when the handle fails again while it is still restarting', async () => {
|
|
1096
|
+
await box.watching(box.root, box.directory());
|
|
1097
|
+
|
|
1098
|
+
// The second failure lands inside the restart's snapshot. A flag guarding the restart would
|
|
1099
|
+
// decline this one and leave the watcher holding a dead handle for good.
|
|
1100
|
+
box.host.duringNextRead = () => box.host.fail(new Error('EPERM'));
|
|
1101
|
+
box.host.fail(new Error('EPERM'));
|
|
1102
|
+
await wait(TIMINGS.existencePollDelay * 5);
|
|
1103
|
+
|
|
1104
|
+
box.write('a.txt');
|
|
1105
|
+
box.fire('rename', 'a.txt');
|
|
1106
|
+
await box.expectAmong(1, 'added a.txt');
|
|
1107
|
+
});
|
|
1108
|
+
|
|
1109
|
+
it('reports what changed when a restart is superseded before it could report', async () => {
|
|
1110
|
+
await box.watching(box.root, box.directory());
|
|
1111
|
+
|
|
1112
|
+
box.host.duringNextRead = () => {
|
|
1113
|
+
box.write('a.txt');
|
|
1114
|
+
box.host.fail(new Error('EPERM'));
|
|
1115
|
+
};
|
|
1116
|
+
box.host.fail(new Error('EPERM'));
|
|
1117
|
+
|
|
1118
|
+
await box.expect(1, 'added a.txt');
|
|
1119
|
+
});
|
|
1120
|
+
|
|
1121
|
+
it('reports itself added again when the restart that reported it deleted is superseded', async () => {
|
|
1122
|
+
await box.watching(box.root, box.directory());
|
|
1123
|
+
|
|
1124
|
+
// The superseded restart stores the new identity, so the next one finds nothing replaced.
|
|
1125
|
+
box.host.fakeIdentity = { dev: 1, ino: 2, birthtimeMs: 3 };
|
|
1126
|
+
box.host.duringNextRead = () => box.host.fail(new Error('EPERM'));
|
|
1127
|
+
box.host.fail(new Error('EPERM'));
|
|
1128
|
+
|
|
1129
|
+
await box.expect(1, 'deleted .', 'added .');
|
|
1130
|
+
});
|
|
1131
|
+
|
|
1132
|
+
it('reports a creation once when the restart that read it is superseded after its batch was flushed', async () => {
|
|
1133
|
+
await box.watching(box.root, box.directory());
|
|
1134
|
+
|
|
1135
|
+
box.host.duringNextRead = async () => {
|
|
1136
|
+
box.write('a.txt');
|
|
1137
|
+
box.fire('rename', 'a.txt');
|
|
1138
|
+
// Long enough for the batch to be queued behind the restart before the handle fails again.
|
|
1139
|
+
await wait(TIMINGS.changeDelay * 4);
|
|
1140
|
+
box.host.fail(new Error('EPERM'));
|
|
1141
|
+
};
|
|
1142
|
+
box.host.fail(new Error('EPERM'));
|
|
1143
|
+
|
|
1144
|
+
await box.expect(1, 'added a.txt');
|
|
1145
|
+
});
|
|
1146
|
+
|
|
1147
|
+
it('reports a child after itself when the restart that reported it deleted is superseded', async () => {
|
|
1148
|
+
await box.watching(box.root, box.directory());
|
|
1149
|
+
|
|
1150
|
+
box.host.fakeIdentity = { dev: 1, ino: 2, birthtimeMs: 3 };
|
|
1151
|
+
box.host.duringNextRead = async () => {
|
|
1152
|
+
box.write('a.txt');
|
|
1153
|
+
box.fire('rename', 'a.txt');
|
|
1154
|
+
await wait(TIMINGS.changeDelay * 4);
|
|
1155
|
+
box.host.fail(new Error('EPERM'));
|
|
1156
|
+
};
|
|
1157
|
+
box.host.fail(new Error('EPERM'));
|
|
1158
|
+
|
|
1159
|
+
await box.expect(1, 'deleted .', 'added .', 'added a.txt');
|
|
1160
|
+
});
|
|
1161
|
+
|
|
1162
|
+
it('reports a creation, a deletion, and a replacement as an update once each during a restart read', async () => {
|
|
1163
|
+
box.write('b.txt');
|
|
1164
|
+
box.write('c.txt');
|
|
1165
|
+
await box.watching(box.root, box.directory());
|
|
1166
|
+
|
|
1167
|
+
box.host.duringNextRead = () => {
|
|
1168
|
+
box.write('a.txt');
|
|
1169
|
+
box.remove('b.txt');
|
|
1170
|
+
box.write('b.txt');
|
|
1171
|
+
box.remove('c.txt');
|
|
1172
|
+
['a.txt', 'b.txt', 'c.txt'].forEach(fileName => box.fire('rename', fileName));
|
|
1173
|
+
};
|
|
1174
|
+
box.host.fail(new Error('EPERM'));
|
|
1175
|
+
|
|
1176
|
+
await box.expect(1, 'added a.txt', 'updated b.txt', 'deleted c.txt');
|
|
1177
|
+
});
|
|
1178
|
+
|
|
1179
|
+
it('reports a deletion once when the handle fails while its batch is resolved', async () => {
|
|
1180
|
+
box.write('a.txt');
|
|
1181
|
+
await box.watching(box.root, box.directory());
|
|
1182
|
+
// Where names ignore case the batch reads the directory, which lets the failure land mid-batch.
|
|
1183
|
+
box.host.caseInsensitive = true;
|
|
1184
|
+
|
|
1185
|
+
box.host.duringNextRead = () => box.host.fail(new Error('EPERM'));
|
|
1186
|
+
box.remove('a.txt');
|
|
1187
|
+
box.fire('rename', 'a.txt');
|
|
1188
|
+
|
|
1189
|
+
await box.expect(1, 'deleted a.txt');
|
|
1190
|
+
});
|
|
1191
|
+
|
|
1192
|
+
it('is re-keyed when a superseded restart already moved it to the parent', async () => {
|
|
1193
|
+
const target = box.mkdir('later');
|
|
1194
|
+
const watcher = await box.watching(target, box.directory(target));
|
|
1195
|
+
let resolved = 0;
|
|
1196
|
+
watcher.onDidResolveDirectory(() => resolved++);
|
|
1197
|
+
|
|
1198
|
+
box.remove('later');
|
|
1199
|
+
box.write('later');
|
|
1200
|
+
box.host.duringNextRead = () => box.host.fail(new Error('EPERM'));
|
|
1201
|
+
box.host.fail(new Error('EPERM'));
|
|
1202
|
+
await wait(TIMINGS.existencePollDelay * 5);
|
|
1203
|
+
|
|
1204
|
+
assert.strictEqual(watcher.directory, box.root);
|
|
1205
|
+
assert.strictEqual(resolved, 1);
|
|
1206
|
+
});
|
|
1207
|
+
|
|
1208
|
+
it('reports no change when the handle fails during the first read', async () => {
|
|
1209
|
+
box.write('a.txt');
|
|
1210
|
+
box.host.duringNextRead = () => box.host.fail(new Error('EPERM'));
|
|
1211
|
+
await box.watching(box.root, box.directory());
|
|
1212
|
+
await wait(TIMINGS.existencePollDelay * 2);
|
|
1213
|
+
|
|
1214
|
+
box.write('b.txt');
|
|
1215
|
+
box.fire('rename', 'b.txt');
|
|
1216
|
+
await box.expect(1, 'added b.txt');
|
|
1217
|
+
});
|
|
1218
|
+
|
|
1219
|
+
it('keeps working after a handle failure, without reporting a change', async () => {
|
|
1220
|
+
await box.watching(box.root, box.directory());
|
|
1221
|
+
|
|
1222
|
+
box.host.fail(new Error('EPERM'));
|
|
1223
|
+
await box.expect(1);
|
|
1224
|
+
|
|
1225
|
+
box.write('a.txt');
|
|
1226
|
+
box.fire('rename', 'a.txt');
|
|
1227
|
+
await box.expect(1, 'added a.txt');
|
|
1228
|
+
});
|
|
1229
|
+
});
|
|
1230
|
+
|
|
1231
|
+
describe('platform behavior', () => {
|
|
1232
|
+
|
|
1233
|
+
it('reports a case-only rename as a deletion and an addition, or as an update to a request for the file', async () => {
|
|
1234
|
+
const file = box.write('foo.txt');
|
|
1235
|
+
await box.watching(box.root, box.directory(), box.file(file, 2));
|
|
1236
|
+
box.host.caseInsensitive = true;
|
|
1237
|
+
|
|
1238
|
+
fs.renameSync(box.path('foo.txt'), box.path('Foo.txt'));
|
|
1239
|
+
box.fire('rename', 'foo.txt');
|
|
1240
|
+
box.fire('rename', 'Foo.txt');
|
|
1241
|
+
|
|
1242
|
+
await box.expect(1, 'deleted foo.txt', 'added Foo.txt');
|
|
1243
|
+
await box.expect(2, 'updated foo.txt');
|
|
1244
|
+
});
|
|
1245
|
+
|
|
1246
|
+
it('reports a case-only rename found by a rescan as an update to a request for the file', async () => {
|
|
1247
|
+
const file = box.write('foo.txt');
|
|
1248
|
+
await box.watching(box.root, box.file(file));
|
|
1249
|
+
box.host.caseInsensitive = true;
|
|
1250
|
+
|
|
1251
|
+
fs.renameSync(box.path('foo.txt'), box.path('Foo.txt'));
|
|
1252
|
+
// eslint-disable-next-line no-null/no-null
|
|
1253
|
+
box.fire('change', null);
|
|
1254
|
+
|
|
1255
|
+
await box.expect(1, 'updated foo.txt');
|
|
1256
|
+
});
|
|
1257
|
+
|
|
1258
|
+
it('leaves a request for a file with its file when its case changes over two batches', async () => {
|
|
1259
|
+
const file = box.write('foo.txt');
|
|
1260
|
+
box.host.caseInsensitive = true;
|
|
1261
|
+
// A long grace period, so the deletion of the old name still waits after the first batch on a slow host.
|
|
1262
|
+
const watcher = new NodeDirectoryWatcher(box.root, NO_LOGGING, box, { ...TIMINGS, deleteDelay: 1000 }, box.host);
|
|
1263
|
+
watcher.addRequest(0, box.file(file));
|
|
1264
|
+
watcher.addRequest(1, box.directory(box.root, 2));
|
|
1265
|
+
await watcher.whenStarted;
|
|
1266
|
+
try {
|
|
1267
|
+
fs.renameSync(box.path('foo.txt'), box.path('tmp'));
|
|
1268
|
+
box.fire('rename', 'foo.txt');
|
|
1269
|
+
box.fire('rename', 'tmp');
|
|
1270
|
+
await box.expectAmong(2, 'added tmp');
|
|
1271
|
+
fs.renameSync(box.path('tmp'), box.path('Foo.txt'));
|
|
1272
|
+
box.fire('rename', 'tmp');
|
|
1273
|
+
box.fire('rename', 'Foo.txt');
|
|
1274
|
+
|
|
1275
|
+
await box.expect(1, 'updated foo.txt');
|
|
1276
|
+
} finally {
|
|
1277
|
+
watcher.dispose();
|
|
1278
|
+
}
|
|
1279
|
+
});
|
|
1280
|
+
|
|
1281
|
+
it('leaves a request for a file with its file when a rescan finds its case changed', async () => {
|
|
1282
|
+
const file = box.write('foo.txt');
|
|
1283
|
+
await box.watching(box.root, box.file(file));
|
|
1284
|
+
box.host.caseInsensitive = true;
|
|
1285
|
+
|
|
1286
|
+
// The overflow lost the rename; the write that followed it was delivered.
|
|
1287
|
+
fs.renameSync(box.path('foo.txt'), box.path('Foo.txt'));
|
|
1288
|
+
// eslint-disable-next-line no-null/no-null
|
|
1289
|
+
box.fire('change', null);
|
|
1290
|
+
box.fire('change', 'Foo.txt');
|
|
1291
|
+
|
|
1292
|
+
await box.expect(1, 'updated foo.txt');
|
|
1293
|
+
});
|
|
1294
|
+
|
|
1295
|
+
it('tells names that differ only in case apart where a directory does', async function (): Promise<void> {
|
|
1296
|
+
box.write('foo.txt');
|
|
1297
|
+
if (fs.existsSync(box.path('FOO.TXT'))) {
|
|
1298
|
+
// This file system ignores case, so it cannot hold both names.
|
|
1299
|
+
this.skip();
|
|
1300
|
+
}
|
|
1301
|
+
box.write('Foo.txt');
|
|
1302
|
+
box.host.caseInsensitive = true;
|
|
1303
|
+
await box.watching(box.root, box.directory());
|
|
1304
|
+
|
|
1305
|
+
box.write('foo.txt');
|
|
1306
|
+
box.fire('rename', 'foo.txt');
|
|
1307
|
+
box.remove('Foo.txt');
|
|
1308
|
+
box.fire('rename', 'Foo.txt');
|
|
1309
|
+
|
|
1310
|
+
await box.expect(1, 'updated foo.txt', 'deleted Foo.txt');
|
|
1311
|
+
});
|
|
1312
|
+
|
|
1313
|
+
it('reads the directory once for a batch of renames where names ignore case', async () => {
|
|
1314
|
+
box.host.caseInsensitive = true;
|
|
1315
|
+
await box.watching(box.root, box.directory());
|
|
1316
|
+
const readsBefore = box.host.reads;
|
|
1317
|
+
|
|
1318
|
+
['a.txt', 'b.txt', 'c.txt'].forEach(fileName => {
|
|
1319
|
+
box.write(fileName);
|
|
1320
|
+
box.fire('rename', fileName);
|
|
1321
|
+
});
|
|
1322
|
+
|
|
1323
|
+
await box.expect(1, 'added a.txt', 'added b.txt', 'added c.txt');
|
|
1324
|
+
assert.strictEqual(box.host.reads - readsBefore, 1);
|
|
1325
|
+
});
|
|
1326
|
+
|
|
1327
|
+
it('confirms the deletions of a batch with one directory read where names ignore case', async () => {
|
|
1328
|
+
box.host.caseInsensitive = true;
|
|
1329
|
+
const fileNames = ['a.txt', 'b.txt', 'c.txt'];
|
|
1330
|
+
fileNames.forEach(fileName => box.write(fileName));
|
|
1331
|
+
await box.watching(box.root, box.directory());
|
|
1332
|
+
const readsBefore = box.host.reads;
|
|
1333
|
+
|
|
1334
|
+
fileNames.forEach(fileName => {
|
|
1335
|
+
box.remove(fileName);
|
|
1336
|
+
box.fire('rename', fileName);
|
|
1337
|
+
});
|
|
1338
|
+
|
|
1339
|
+
await box.expect(1, 'deleted a.txt', 'deleted b.txt', 'deleted c.txt');
|
|
1340
|
+
// One read resolves the batch, one confirms its deletions.
|
|
1341
|
+
assert.strictEqual(box.host.reads - readsBefore, 2);
|
|
1342
|
+
});
|
|
1343
|
+
|
|
1344
|
+
it('rejects a timing that would silently become 1ms', () => {
|
|
1345
|
+
assert.throws(() => new NodeDirectoryWatcher(box.root, NO_LOGGING, box, { ...TIMINGS, deleteDelay: 0 }), /positive number/);
|
|
1346
|
+
assert.throws(() => new NodeDirectoryWatcher(box.root, NO_LOGGING, box, { ...TIMINGS, changeDelay: -1 }), /positive number/);
|
|
1347
|
+
});
|
|
1348
|
+
|
|
1349
|
+
it('refuses to watch a network share, which crashes macOS', async () => {
|
|
1350
|
+
const target = box.mkdir('network-share');
|
|
1351
|
+
const watcher = box.starting(target, box.directory(target));
|
|
1352
|
+
|
|
1353
|
+
await watcher.whenStarted;
|
|
1354
|
+
|
|
1355
|
+
assert.strictEqual(box.errors.length, 1, `expected a report, got ${JSON.stringify(box.errors)}`);
|
|
1356
|
+
assert.throws(() => box.fire('change', 'a.txt'), /has not opened a handle/);
|
|
1357
|
+
});
|
|
1358
|
+
});
|
|
1359
|
+
|
|
1360
|
+
describe('adopting requests', () => {
|
|
1361
|
+
|
|
1362
|
+
it('tells them about their file once a batch queued before them reported it', async () => {
|
|
1363
|
+
const watcher = await box.watching(box.root, box.directory());
|
|
1364
|
+
|
|
1365
|
+
box.write('x.txt');
|
|
1366
|
+
box.fire('rename', 'x.txt');
|
|
1367
|
+
watcher.adoptRequests(new Map([[1, box.waiting(box.file(box.path('x.txt'), 2))]]));
|
|
1368
|
+
|
|
1369
|
+
await box.expect(1, 'added x.txt');
|
|
1370
|
+
await box.expect(2, 'added x.txt');
|
|
1371
|
+
});
|
|
1372
|
+
|
|
1373
|
+
it('tells only those whose file exists that it was added', async () => {
|
|
1374
|
+
const file = box.write('x.txt');
|
|
1375
|
+
const watcher = await box.watching(box.root, box.directory());
|
|
1376
|
+
|
|
1377
|
+
watcher.adoptRequests(new Map([[1, box.waiting(box.file(file, 2))], [2, box.waiting(box.file(box.path('missing.txt'), 3))]]));
|
|
1378
|
+
|
|
1379
|
+
await box.expect(2, 'added x.txt');
|
|
1380
|
+
await box.expect(3);
|
|
1381
|
+
await box.expect(1);
|
|
1382
|
+
});
|
|
1383
|
+
|
|
1384
|
+
it('leaves a scheduled disposal alone when there are none', async () => {
|
|
1385
|
+
const watcher = await box.watching(box.root, box.directory());
|
|
1386
|
+
|
|
1387
|
+
watcher.removeRequest(0);
|
|
1388
|
+
watcher.adoptRequests(new Map());
|
|
1389
|
+
|
|
1390
|
+
await watcher.whenDisposed;
|
|
1391
|
+
});
|
|
1392
|
+
|
|
1393
|
+
it('tells a request moved onto a watcher still waiting for its directory about its file once', async () => {
|
|
1394
|
+
const provider = new DirectoryWatcherProvider(NO_LOGGING, box, { ...TIMINGS, existencePollDelay: 400 }, box.host);
|
|
1395
|
+
const directory = box.path('cfg');
|
|
1396
|
+
const file = box.path('cfg', 'settings.json');
|
|
1397
|
+
try {
|
|
1398
|
+
await provider.watch(0, box.directory(directory));
|
|
1399
|
+
await wait(200);
|
|
1400
|
+
await provider.watch(1, box.file(file, 2));
|
|
1401
|
+
// Both appear right after the directory watcher polled, so the file watcher finds them first.
|
|
1402
|
+
const exists = box.host.exists.bind(box.host);
|
|
1403
|
+
box.host.exists = async fsPath => {
|
|
1404
|
+
const result = await exists(fsPath);
|
|
1405
|
+
if (!result && fsPath === directory) {
|
|
1406
|
+
box.host.exists = exists;
|
|
1407
|
+
fs.mkdirSync(directory);
|
|
1408
|
+
fs.writeFileSync(file, '{}');
|
|
1409
|
+
}
|
|
1410
|
+
return result;
|
|
1411
|
+
};
|
|
1412
|
+
|
|
1413
|
+
await box.expect(1, 'added cfg');
|
|
1414
|
+
await box.expect(2, 'added cfg/settings.json');
|
|
1415
|
+
} finally {
|
|
1416
|
+
provider.unwatch(0);
|
|
1417
|
+
provider.unwatch(1);
|
|
1418
|
+
}
|
|
1419
|
+
});
|
|
1420
|
+
|
|
1421
|
+
it('opens no second handle on a directory when a file request for it is merged', async () => {
|
|
1422
|
+
const provider = new DirectoryWatcherProvider(NO_LOGGING, box, TIMINGS, box.host);
|
|
1423
|
+
const opened: string[] = [];
|
|
1424
|
+
const watch = box.host.watch.bind(box.host);
|
|
1425
|
+
box.host.watch = (directory, listener) => {
|
|
1426
|
+
opened.push(directory);
|
|
1427
|
+
return watch(directory, listener);
|
|
1428
|
+
};
|
|
1429
|
+
try {
|
|
1430
|
+
await provider.watch(0, box.directory());
|
|
1431
|
+
// Started before the file appears, so only its event reports the file.
|
|
1432
|
+
await (provider['watcherFor'](0) as NodeDirectoryWatcher).whenStarted;
|
|
1433
|
+
await provider.watch(1, box.file(box.path('later.txt'), 2));
|
|
1434
|
+
box.write('later.txt');
|
|
1435
|
+
box.fire('rename', 'later.txt');
|
|
1436
|
+
|
|
1437
|
+
await box.expect(2, 'added later.txt');
|
|
1438
|
+
assert.deepStrictEqual(opened.filter(directory => directory === box.root).length, 1);
|
|
1439
|
+
} finally {
|
|
1440
|
+
provider.unwatch(0);
|
|
1441
|
+
provider.unwatch(1);
|
|
1442
|
+
}
|
|
1443
|
+
});
|
|
1444
|
+
|
|
1445
|
+
it('does not tell a directory request joining a re-keyed file watcher that its directory was added', async () => {
|
|
1446
|
+
const provider = new DirectoryWatcherProvider(NO_LOGGING, box, TIMINGS, box.host);
|
|
1447
|
+
try {
|
|
1448
|
+
await provider.watch(0, box.file(box.path('later.txt')));
|
|
1449
|
+
// Joins after the re-key to the directory, while the file watcher reads its children.
|
|
1450
|
+
box.host.duringNextRead = () => provider.watch(1, box.directory(box.root, 2));
|
|
1451
|
+
box.write('later.txt');
|
|
1452
|
+
|
|
1453
|
+
await box.expect(1, 'added later.txt');
|
|
1454
|
+
await box.expect(2);
|
|
1455
|
+
} finally {
|
|
1456
|
+
provider.unwatch(0);
|
|
1457
|
+
provider.unwatch(1);
|
|
1458
|
+
}
|
|
1459
|
+
});
|
|
1460
|
+
});
|
|
1461
|
+
|
|
1462
|
+
describe('disposal', () => {
|
|
1463
|
+
|
|
1464
|
+
it('happens once the last request is released', async () => {
|
|
1465
|
+
const watcher = await box.watching(box.root, box.directory(), box.directory(box.root, 2));
|
|
1466
|
+
|
|
1467
|
+
watcher.removeRequest(0);
|
|
1468
|
+
await wait(TIMINGS.deferredDisposalTimeout * 2);
|
|
1469
|
+
assert.strictEqual(watcher.isDisposed, false, 'a watcher with a request left must stay alive');
|
|
1470
|
+
|
|
1471
|
+
watcher.removeRequest(1);
|
|
1472
|
+
await watcher.whenDisposed;
|
|
1473
|
+
});
|
|
1474
|
+
|
|
1475
|
+
it('is called off by a request arriving before the deferred timeout', async () => {
|
|
1476
|
+
const watcher = await box.watching(box.root, box.directory());
|
|
1477
|
+
|
|
1478
|
+
watcher.removeRequest(0);
|
|
1479
|
+
watcher.addRequest(1, box.directory(box.root, 2));
|
|
1480
|
+
await wait(TIMINGS.deferredDisposalTimeout * 2);
|
|
1481
|
+
|
|
1482
|
+
assert.strictEqual(watcher.isDisposed, false);
|
|
1483
|
+
});
|
|
1484
|
+
|
|
1485
|
+
it('silences a deletion that was still pending', async () => {
|
|
1486
|
+
box.write('a.txt');
|
|
1487
|
+
const watcher = await box.watching(box.root, box.directory());
|
|
1488
|
+
|
|
1489
|
+
box.remove('a.txt');
|
|
1490
|
+
box.fire('rename', 'a.txt');
|
|
1491
|
+
watcher.dispose();
|
|
1492
|
+
|
|
1493
|
+
await box.expect(1);
|
|
1494
|
+
});
|
|
1495
|
+
|
|
1496
|
+
it('keeps its state in sync while it briefly has no requests', async () => {
|
|
1497
|
+
box.write('a.txt');
|
|
1498
|
+
const watcher = new NodeDirectoryWatcher(box.root, NO_LOGGING, box, { ...TIMINGS, deferredDisposalTimeout: 1000 }, box.host);
|
|
1499
|
+
watcher.addRequest(0, box.directory());
|
|
1500
|
+
await watcher.whenStarted;
|
|
1501
|
+
try {
|
|
1502
|
+
// The file is deleted just as the last request leaves; the grace period keeps the watcher alive.
|
|
1503
|
+
box.remove('a.txt');
|
|
1504
|
+
box.fire('rename', 'a.txt');
|
|
1505
|
+
watcher.removeRequest(0);
|
|
1506
|
+
await wait(TIMINGS.deleteDelay * 3);
|
|
1507
|
+
|
|
1508
|
+
// The deletion was settled meanwhile, so the recreated file is an addition, not an update.
|
|
1509
|
+
watcher.addRequest(1, box.directory(box.root, 2));
|
|
1510
|
+
box.write('a.txt');
|
|
1511
|
+
box.fire('rename', 'a.txt');
|
|
1512
|
+
await box.expect(2, 'added a.txt');
|
|
1513
|
+
|
|
1514
|
+
box.remove('a.txt');
|
|
1515
|
+
box.fire('rename', 'a.txt');
|
|
1516
|
+
await box.expect(2, 'added a.txt', 'deleted a.txt');
|
|
1517
|
+
} finally {
|
|
1518
|
+
watcher.dispose();
|
|
1519
|
+
}
|
|
1520
|
+
});
|
|
1521
|
+
});
|
|
1522
|
+
|
|
1523
|
+
describe('with a real fs.watch handle', () => {
|
|
1524
|
+
|
|
1525
|
+
it('reports direct children and nothing below them', async () => {
|
|
1526
|
+
box.mkdir('nested');
|
|
1527
|
+
await box.watchingForReal(box.root, box.directory());
|
|
1528
|
+
|
|
1529
|
+
box.write('nested', 'deep.txt');
|
|
1530
|
+
box.write('direct.txt');
|
|
1531
|
+
|
|
1532
|
+
await box.expectAmong(1, 'added direct.txt');
|
|
1533
|
+
assert.ok(!box.reported(1).includes('added nested/deep.txt'), 'a nested change must not be reported');
|
|
1534
|
+
});
|
|
1535
|
+
|
|
1536
|
+
it('reports a child named like the watched directory', async () => {
|
|
1537
|
+
const sameName = path.basename(box.root);
|
|
1538
|
+
await box.watchingForReal(box.root, box.directory());
|
|
1539
|
+
|
|
1540
|
+
box.mkdir(sameName);
|
|
1541
|
+
await box.expectAmong(1, `added ${sameName}`);
|
|
1542
|
+
|
|
1543
|
+
box.remove(sameName);
|
|
1544
|
+
await box.expectAmong(1, `deleted ${sameName}`);
|
|
1545
|
+
});
|
|
1546
|
+
|
|
1547
|
+
it('reports an update and a deletion of a direct child', async () => {
|
|
1548
|
+
box.write('a.txt');
|
|
1549
|
+
await box.watchingForReal(box.root, box.directory());
|
|
1550
|
+
|
|
1551
|
+
box.write('a.txt');
|
|
1552
|
+
await box.expectAmong(1, 'updated a.txt');
|
|
1553
|
+
|
|
1554
|
+
box.remove('a.txt');
|
|
1555
|
+
await box.expectAmong(1, 'deleted a.txt');
|
|
1556
|
+
});
|
|
1557
|
+
|
|
1558
|
+
it('resolves a case-only rename against the real file system', async () => {
|
|
1559
|
+
box.write('foo.txt');
|
|
1560
|
+
await box.watchingForReal(box.root, box.directory());
|
|
1561
|
+
|
|
1562
|
+
// A case-insensitive host would report an update of the old name if `stat` were trusted.
|
|
1563
|
+
fs.renameSync(box.path('foo.txt'), box.path('Foo.txt'));
|
|
1564
|
+
|
|
1565
|
+
await box.expectAmong(1, 'added Foo.txt', 'deleted foo.txt');
|
|
1566
|
+
});
|
|
1567
|
+
|
|
1568
|
+
it('reports the watched directory being lost and coming back', async () => {
|
|
1569
|
+
const target = box.mkdir('workspace');
|
|
1570
|
+
box.write('workspace', 'before.txt');
|
|
1571
|
+
await box.watchingForReal(target, box.directory(target));
|
|
1572
|
+
|
|
1573
|
+
// Reported as a named event, an event on the directory, or a handle error, depending on the host.
|
|
1574
|
+
box.remove('workspace');
|
|
1575
|
+
await box.expectAmong(1, 'deleted workspace');
|
|
1576
|
+
|
|
1577
|
+
box.mkdir('workspace');
|
|
1578
|
+
box.write('workspace', 'after.txt');
|
|
1579
|
+
|
|
1580
|
+
await box.expectAmong(1, 'added workspace', 'added workspace/after.txt');
|
|
1581
|
+
});
|
|
1582
|
+
|
|
1583
|
+
it('reports a single file through its parent directory', async () => {
|
|
1584
|
+
const file = box.write('a.txt');
|
|
1585
|
+
await box.watchingForReal(file, box.file(file));
|
|
1586
|
+
|
|
1587
|
+
box.write('other.txt');
|
|
1588
|
+
box.write('a.txt');
|
|
1589
|
+
|
|
1590
|
+
await box.expectAmong(1, 'updated a.txt');
|
|
1591
|
+
assert.ok(!box.reported(1).includes('added other.txt'), 'a sibling must not be reported');
|
|
1592
|
+
});
|
|
1593
|
+
});
|
|
1594
|
+
});
|