Handle watching a non-existent directory. BUG= R=nweiz@google.com Review URL: https://codereview.chromium.org//22999008 git-svn-id: https://dart.googlecode.com/svn/branches/bleeding_edge/dart/pkg/watcher@26153 260f80e4-7a28-3924-810f-c04153c831b5
diff --git a/pkgs/watcher/lib/src/directory_watcher.dart b/pkgs/watcher/lib/src/directory_watcher.dart index c5dd9db..eb948f5 100644 --- a/pkgs/watcher/lib/src/directory_watcher.dart +++ b/pkgs/watcher/lib/src/directory_watcher.dart
@@ -11,6 +11,7 @@ import 'async_queue.dart'; import 'stat.dart'; +import 'utils.dart'; import 'watch_event.dart'; /// Watches the contents of a directory and emits [WatchEvent]s when something @@ -118,19 +119,33 @@ _filesToProcess.clear(); _polledFiles.clear(); + endListing() { + assert(_state != _WatchState.UNSUBSCRIBED); + _listSubscription = null; + + // Null tells the queue consumer that we're done listing. + _filesToProcess.add(null); + } + var stream = new Directory(directory).list(recursive: true); _listSubscription = stream.listen((entity) { assert(_state != _WatchState.UNSUBSCRIBED); if (entity is! File) return; _filesToProcess.add(entity.path); - }, onDone: () { - assert(_state != _WatchState.UNSUBSCRIBED); - _listSubscription = null; + }, onError: (error) { + if (isDirectoryNotFoundException(error)) { + // If the directory doesn't exist, we end the listing normally, which + // has the desired effect of marking all files that were in the + // directory as being removed. + endListing(); + return; + } - // Null tells the queue consumer that we're done listing. - _filesToProcess.add(null); - }); + // It's some unknown error. Pipe it over to the event stream so we don't + // take down the whole isolate. + _events.addError(error); + }, onDone: endListing); } /// Processes [file] to determine if it has been modified since the last
diff --git a/pkgs/watcher/lib/src/utils.dart b/pkgs/watcher/lib/src/utils.dart new file mode 100644 index 0000000..319835e --- /dev/null +++ b/pkgs/watcher/lib/src/utils.dart
@@ -0,0 +1,16 @@ +// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +library watcher.utils; + +import 'dart:io'; + +/// Returns `true` if [error] is a [DirectoryException] for a missing directory. +bool isDirectoryNotFoundException(error) { + if (error is! DirectoryException) return false; + + // See dartbug.com/12461 and tests/standalone/io/directory_error_test.dart. + var notFoundCode = Platform.operatingSystem == "windows" ? 3 : 2; + return error.osError.errorCode == notFoundCode; +}
diff --git a/pkgs/watcher/test/directory_watcher_test.dart b/pkgs/watcher/test/directory_watcher_test.dart index 9070179..841dd08 100644 --- a/pkgs/watcher/test/directory_watcher_test.dart +++ b/pkgs/watcher/test/directory_watcher_test.dart
@@ -86,4 +86,23 @@ writeFile("a/b/c/d/file.txt"); expectAddEvent("a/b/c/d/file.txt"); }); + + test('watches a directory created after the watcher', () { + // Watch a subdirectory that doesn't exist yet. + createWatcher(dir: "a"); + + // This implicity creates it. + writeFile("a/b/c/d/file.txt"); + expectAddEvent("a/b/c/d/file.txt"); + }); + + test('when the watched directory is deleted, removes all files', () { + writeFile("dir/a.txt"); + writeFile("dir/b.txt"); + + createWatcher(dir: "dir"); + + deleteDir("dir"); + expectRemoveEvents(["dir/a.txt", "dir/b.txt"]); + }); }
diff --git a/pkgs/watcher/test/utils.dart b/pkgs/watcher/test/utils.dart index 65d4719..18bec97 100644 --- a/pkgs/watcher/test/utils.dart +++ b/pkgs/watcher/test/utils.dart
@@ -76,9 +76,17 @@ /// Normally, this will pause the schedule until the watcher is done scanning /// and is polling for changes. If you pass `false` for [waitForReady], it will /// not schedule this delay. -DirectoryWatcher createWatcher({bool waitForReady}) { +/// +/// If [dir] is provided, watches a subdirectory in the sandbox with that name. +DirectoryWatcher createWatcher({String dir, bool waitForReady}) { + if (dir == null) { + dir = _sandboxDir; + } else { + dir = p.join(_sandboxDir, dir); + } + // Use a short delay to make the tests run quickly. - _watcher = new DirectoryWatcher(_sandboxDir, + _watcher = new DirectoryWatcher(dir, pollingDelay: new Duration(milliseconds: 100)); // Wait until the scan is finished so that we don't miss changes to files @@ -95,31 +103,45 @@ return _watcher; } -void expectEvent(ChangeType type, String path) { - // Immediately create the future. This ensures we don't register too late and - // drop the event before we receive it. - var future = _watcher.events.elementAt(_nextEvent++).then((event) { - expect(event, new _ChangeMatcher(type, path)); - }); +/// Expects that the next set of events will all be changes of [type] on +/// [paths]. +/// +/// Validates that events are delivered for all paths in [paths], but allows +/// them in any order. +void expectEvents(ChangeType type, Iterable<String> paths) { + var pathSet = paths.map((path) => p.join(_sandboxDir, path)).toSet(); - // Make sure the schedule is watching it in case it fails. - currentSchedule.wrapFuture(future); + // Create an expectation for as many paths as we have. + var futures = []; + + for (var i = 0; i < paths.length; i++) { + // Immediately create the futures. This ensures we don't register too + // late and drop the event before we receive it. + var future = _watcher.events.elementAt(_nextEvent++).then((event) { + expect(event.type, equals(type)); + expect(pathSet, contains(event.path)); + + pathSet.remove(event.path); + }); + + // Make sure the schedule is watching it in case it fails. + currentSchedule.wrapFuture(future); + + futures.add(future); + } // Schedule it so that later file modifications don't occur until after this // event is received. - schedule(() => future, "wait for $type event"); + schedule(() => Future.wait(futures), + "wait for $type events on ${paths.join(', ')}"); } -void expectAddEvent(String path) { - expectEvent(ChangeType.ADD, p.join(_sandboxDir, path)); -} +void expectAddEvent(String path) => expectEvents(ChangeType.ADD, [path]); +void expectModifyEvent(String path) => expectEvents(ChangeType.MODIFY, [path]); +void expectRemoveEvent(String path) => expectEvents(ChangeType.REMOVE, [path]); -void expectModifyEvent(String path) { - expectEvent(ChangeType.MODIFY, p.join(_sandboxDir, path)); -} - -void expectRemoveEvent(String path) { - expectEvent(ChangeType.REMOVE, p.join(_sandboxDir, path)); +void expectRemoveEvents(Iterable<String> paths) { + expectEvents(ChangeType.REMOVE, paths); } /// Schedules writing a file in the sandbox at [path] with [contents]. @@ -175,6 +197,13 @@ }, "rename file $from to $to"); } +/// Schedules deleting a directory in the sandbox at [path]. +void deleteDir(String path) { + schedule(() { + new Directory(p.join(_sandboxDir, path)).deleteSync(recursive: true); + }, "delete directory $path"); +} + /// A [Matcher] for [WatchEvent]s. class _ChangeMatcher extends Matcher { /// The expected change.