Skip to content

Commit bcb8977

Browse files
authored
Fix PollingFileWatcher.ready for files that don't exist (dart-archive/watcher#157)
There were a few issues here: - FileWatcher.ready never fired for files that don't exist because of logic inside FileWatcher existing early if the modification time was `null` - The test I recently added trying to catch this was incorrectly passing because the mock timestamp code was set so that files that had not been created would return a 0-mtime whereas in the real implementation they return `null` So this change a) updates the mock to return `null` for uncreated files (to match the real implementation) which breaks a bunch of tests b) fixes those tests by updating FileWatcher._poll() to handle `null` mtime separately from being the first poll.
1 parent e826e96 commit bcb8977

File tree

4 files changed

+46
-16
lines changed

4 files changed

+46
-16
lines changed

pkgs/watcher/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
## 1.1.1-wip
22

3+
- Ensure `PollingFileWatcher.ready` completes for files that do not exist.
4+
35
## 1.1.0
46

57
- Require Dart SDK >= 3.0.0

pkgs/watcher/lib/src/file_watcher/polling.dart

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,7 @@ class _PollingFileWatcher implements FileWatcher, ManuallyClosedWatcher {
3939

4040
/// The previous modification time of the file.
4141
///
42-
/// Used to tell when the file was modified. This is `null` before the file's
43-
/// mtime has first been checked.
42+
/// `null` indicates the file does not (or did not on the last poll) exist.
4443
DateTime? _lastModified;
4544

4645
_PollingFileWatcher(this.path, Duration pollingDelay) {
@@ -50,13 +49,14 @@ class _PollingFileWatcher implements FileWatcher, ManuallyClosedWatcher {
5049

5150
/// Checks the mtime of the file and whether it's been removed.
5251
Future<void> _poll() async {
53-
// We don't mark the file as removed if this is the first poll (indicated by
54-
// [_lastModified] being null). Instead, below we forward the dart:io error
55-
// that comes from trying to read the mtime below.
52+
// We don't mark the file as removed if this is the first poll. Instead,
53+
// below we forward the dart:io error that comes from trying to read the
54+
// mtime below.
5655
var pathExists = await File(path).exists();
5756
if (_eventsController.isClosed) return;
5857

5958
if (_lastModified != null && !pathExists) {
59+
_flagReady();
6060
_eventsController.add(WatchEvent(ChangeType.REMOVE, path));
6161
unawaited(close());
6262
return;
@@ -67,22 +67,34 @@ class _PollingFileWatcher implements FileWatcher, ManuallyClosedWatcher {
6767
modified = await modificationTime(path);
6868
} on FileSystemException catch (error, stackTrace) {
6969
if (!_eventsController.isClosed) {
70+
_flagReady();
7071
_eventsController.addError(error, stackTrace);
7172
await close();
7273
}
7374
}
74-
if (_eventsController.isClosed) return;
75-
76-
if (_lastModified == modified) return;
75+
if (_eventsController.isClosed) {
76+
_flagReady();
77+
return;
78+
}
7779

78-
if (_lastModified == null) {
80+
if (!isReady) {
7981
// If this is the first poll, don't emit an event, just set the last mtime
8082
// and complete the completer.
8183
_lastModified = modified;
84+
_flagReady();
85+
return;
86+
}
87+
88+
if (_lastModified == modified) return;
89+
90+
_lastModified = modified;
91+
_eventsController.add(WatchEvent(ChangeType.MODIFY, path));
92+
}
93+
94+
/// Flags this watcher as ready if it has not already been done.
95+
void _flagReady() {
96+
if (!isReady) {
8297
_readyCompleter.complete();
83-
} else {
84-
_lastModified = modified;
85-
_eventsController.add(WatchEvent(ChangeType.MODIFY, path));
8698
}
8799
}
88100

pkgs/watcher/lib/src/stat.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import 'dart:io';
66

77
/// A function that takes a file path and returns the last modified time for
88
/// the file at that path.
9-
typedef MockTimeCallback = DateTime Function(String path);
9+
typedef MockTimeCallback = DateTime? Function(String path);
1010

1111
MockTimeCallback? _mockTimeCallback;
1212

pkgs/watcher/test/utils.dart

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ Future<void> startWatcher({String? path}) async {
6767
'Path is not in the sandbox: $path not in ${d.sandbox}');
6868

6969
var mtime = _mockFileModificationTimes[normalized];
70-
return DateTime.fromMillisecondsSinceEpoch(mtime ?? 0);
70+
return mtime != null ? DateTime.fromMillisecondsSinceEpoch(mtime) : null;
7171
});
7272

7373
// We want to wait until we're ready *after* we subscribe to the watcher's
@@ -195,6 +195,11 @@ Future expectRemoveEvent(String path) =>
195195
Future allowModifyEvent(String path) =>
196196
_expectOrCollect(mayEmit(isWatchEvent(ChangeType.MODIFY, path)));
197197

198+
/// Track a fake timestamp to be used when writing files. This always increases
199+
/// so that files that are deleted and re-created do not have their timestamp
200+
/// set back to a previously used value.
201+
int _nextTimestamp = 1;
202+
198203
/// Schedules writing a file in the sandbox at [path] with [contents].
199204
///
200205
/// If [contents] is omitted, creates an empty file. If [updateModified] is
@@ -216,14 +221,15 @@ void writeFile(String path, {String? contents, bool? updateModified}) {
216221
if (updateModified) {
217222
path = p.normalize(path);
218223

219-
_mockFileModificationTimes.update(path, (value) => value + 1,
220-
ifAbsent: () => 1);
224+
_mockFileModificationTimes[path] = _nextTimestamp++;
221225
}
222226
}
223227

224228
/// Schedules deleting a file in the sandbox at [path].
225229
void deleteFile(String path) {
226230
File(p.join(d.sandbox, path)).deleteSync();
231+
232+
_mockFileModificationTimes.remove(path);
227233
}
228234

229235
/// Schedules renaming a file in the sandbox from [from] to [to].
@@ -245,6 +251,16 @@ void createDir(String path) {
245251
/// Schedules renaming a directory in the sandbox from [from] to [to].
246252
void renameDir(String from, String to) {
247253
Directory(p.join(d.sandbox, from)).renameSync(p.join(d.sandbox, to));
254+
255+
// Migrate timestamps for any files in this folder.
256+
final knownFilePaths = _mockFileModificationTimes.keys.toList();
257+
for (final filePath in knownFilePaths) {
258+
if (p.isWithin(from, filePath)) {
259+
_mockFileModificationTimes[filePath.replaceAll(from, to)] =
260+
_mockFileModificationTimes[filePath]!;
261+
_mockFileModificationTimes.remove(filePath);
262+
}
263+
}
248264
}
249265

250266
/// Schedules deleting a directory in the sandbox at [path].

0 commit comments

Comments
 (0)