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.