Update the Mac OS watcher in pkg/watcher.

The behavior of Directory.watch has changed, and this updates the
watcher to match the new behavior.

R=rnystrom@google.com
BUG=

Review URL: https://codereview.chromium.org//100823005

git-svn-id: https://dart.googlecode.com/svn/branches/bleeding_edge/dart/pkg/watcher@30915 260f80e4-7a28-3924-810f-c04153c831b5
diff --git a/lib/src/directory_watcher/mac_os.dart b/lib/src/directory_watcher/mac_os.dart
index c3e03b8..3e68cd9 100644
--- a/lib/src/directory_watcher/mac_os.dart
+++ b/lib/src/directory_watcher/mac_os.dart
@@ -23,8 +23,8 @@
 /// succession, it won't report them in the order they occurred. See issue
 /// 14373.
 ///
-/// This also works around issues 14793, 14806, and 14849 in the implementation
-/// of [Directory.watch].
+/// This also works around issues 15458 and 14849 in the implementation of
+/// [Directory.watch].
 class MacOSDirectoryWatcher extends ResubscribableDirectoryWatcher {
   MacOSDirectoryWatcher(String directory)
       : super(directory, () => new _MacOSDirectoryWatcher(directory));
@@ -106,11 +106,18 @@
       for (var event in events) {
         if (event is FileSystemCreateEvent) {
           if (!event.isDirectory) {
+            // Don't emit ADD events for files or directories that we already
+            // know about. Such an event comes from FSEvents reporting an add
+            // that happened prior to the watch beginning.
+            if (_files.contains(path)) continue;
+
             _emitEvent(ChangeType.ADD, path);
             _files.add(path);
             continue;
           }
 
+          if (_files.containsDir(path)) continue;
+
           _listen(Chain.track(new Directory(path).list(recursive: true)),
               (entity) {
             if (entity is Directory) return;
@@ -164,34 +171,12 @@
       set.add(event);
     }
 
-    for (var event in batch.where((event) => event is! FileSystemMoveEvent)) {
+    for (var event in batch) {
+      // The Mac OS watcher doesn't emit move events. See issue 14806.
+      assert(event is! FileSystemMoveEvent);
       addEvent(event.path, event);
     }
 
-    // Issue 14806 means that move events can be misleading if they're in the
-    // same batch as another modification of a related file. If they are, we
-    // make the event set empty to ensure we check the state of the filesystem.
-    // Otherwise, treat them as a DELETE followed by an ADD.
-    for (var event in batch.where((event) => event is FileSystemMoveEvent)) {
-      if (eventsForPaths.containsKey(event.path) ||
-          eventsForPaths.containsKey(event.destination)) {
-
-        if (!isInModifiedDirectory(event.path)) {
-          eventsForPaths[event.path] = new Set();
-        }
-        if (!isInModifiedDirectory(event.destination)) {
-          eventsForPaths[event.destination] = new Set();
-        }
-
-        continue;
-      }
-
-      addEvent(event.path, new ConstructableFileSystemDeleteEvent(
-          event.path, event.isDirectory));
-      addEvent(event.destination, new ConstructableFileSystemCreateEvent(
-          event.path, event.isDirectory));
-    }
-
     return eventsForPaths;
   }
 
@@ -212,6 +197,7 @@
 
     var type = batch.first.type;
     var isDir = batch.first.isDirectory;
+    var hadModifyEvent = false;
 
     for (var event in batch.skip(1)) {
       // If one event reports that the file is a directory and another event
@@ -222,7 +208,10 @@
       // safely assume the file was modified after a CREATE or before the
       // REMOVE; otherwise there will also be a REMOVE or CREATE event
       // (respectively) that will be contradictory.
-      if (event is FileSystemModifyEvent) continue;
+      if (event is FileSystemModifyEvent) {
+        hadModifyEvent = true;
+        continue;
+      }
       assert(event is FileSystemCreateEvent || event is FileSystemDeleteEvent);
 
       // If we previously thought this was a MODIFY, we now consider it to be a
@@ -237,13 +226,23 @@
       if (type != event.type) return null;
     }
 
+    // If we got a CREATE event for a file we already knew about, that comes
+    // from FSEvents reporting an add that happened prior to the watch
+    // beginning. If we also received a MODIFY event, we want to report that,
+    // but not the CREATE.
+    if (type == FileSystemEvent.CREATE && hadModifyEvent &&
+        _files.contains(batch.first.path)) {
+      type = FileSystemEvent.MODIFY;
+    }
+
     switch (type) {
       case FileSystemEvent.CREATE:
-        // Issue 14793 means that CREATE events can actually mean DELETE, so we
-        // should always check the filesystem for them.
-        return null;
+        return new ConstructableFileSystemCreateEvent(batch.first.path, isDir);
       case FileSystemEvent.DELETE:
-        return new ConstructableFileSystemDeleteEvent(batch.first.path, isDir);
+        // Issue 15458 means that DELETE events for directories can actually
+        // mean CREATE, so we always check the filesystem for them.
+        if (isDir) return null;
+        return new ConstructableFileSystemCreateEvent(batch.first.path, false);
       case FileSystemEvent.MODIFY:
         return new ConstructableFileSystemModifyEvent(
             batch.first.path, isDir, false);
@@ -329,11 +328,6 @@
   void _emitEvent(ChangeType type, String path) {
     if (!isReady) return;
 
-    // Don't emit ADD events for files that we already know about. Such an event
-    // probably comes from FSEvents reporting an add that happened prior to the
-    // watch beginning.
-    if (type == ChangeType.ADD && _files.contains(path)) return;
-
     _eventsController.add(new WatchEvent(type, path));
   }
 
diff --git a/test/directory_watcher/mac_os_test.dart b/test/directory_watcher/mac_os_test.dart
index 7d31221..b71e145 100644
--- a/test/directory_watcher/mac_os_test.dart
+++ b/test/directory_watcher/mac_os_test.dart
@@ -2,8 +2,6 @@
 // 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.
 
-import 'dart:async';
-
 import 'package:scheduled_test/scheduled_test.dart';
 import 'package:watcher/src/directory_watcher/mac_os.dart';
 import 'package:watcher/watcher.dart';
@@ -52,28 +50,6 @@
     withPermutations((i, j, k) =>
         writeFile("dir/sub/sub-$i/sub-$j/file-$k.txt"));
 
-    // We sleep here because a narrow edge case caused by two interacting bugs
-    // can produce events that aren't expected if we start the watcher too
-    // soon after creating the files above. Here's what happens:
-    //
-    // * We create "dir/sub" and its contents above.
-    //
-    // * We initialize the watcher watching "dir".
-    //
-    // * Due to issue 14373, the watcher can receive native events describing
-    //   the creation of "dir/sub" and its contents despite the fact that they
-    //   occurred before the watcher was started.
-    //
-    // * Usually the above events will occur while the watcher is doing its
-    //   initial scan of "dir/sub" and be ignored, but occasionally they will
-    //   occur afterwards.
-    //
-    // * When processing the bogus CREATE events, the watcher has to assume that
-    //   they could mean something other than CREATE (issue 14793). Thus it
-    //   assumes that the files or directories in question could have changed
-    //   and emits CHANGE events or additional REMOVE/CREATE pairs for them.
-    schedule(() => new Future.delayed(new Duration(seconds: 2)));
-
     startWatcher(dir: "dir");
 
     renameDir("dir/sub", "sub");
diff --git a/test/no_subscription/mac_os_test.dart b/test/no_subscription/mac_os_test.dart
index 3862134..e0275c4 100644
--- a/test/no_subscription/mac_os_test.dart
+++ b/test/no_subscription/mac_os_test.dart
@@ -9,32 +9,6 @@
 import 'shared.dart';
 import '../utils.dart';
 
-// This is currently failing due to issue 14793. The reason is fairly complex:
-//
-// 1. As part of the test, an "unwatched.txt" file is created while there are no
-//    active watchers on the containing directory.
-//
-// 2. A watcher is then added.
-//
-// 3. The watcher lists the contents of the directory and notices that
-//    "unwatched.txt" already exists.
-//
-// 4. Since FSEvents reports past changes (issue 14373), the IO event stream
-//    emits a CREATED event for "unwatched.txt".
-//
-// 5. Due to issue 14793, the watcher cannot trust that this is really a CREATED
-//    event and checks the status of "unwatched.txt" on the filesystem against
-//    its internal state.
-//
-// 6. "unwatched.txt" exists on the filesystem and the watcher knows about it
-//    internally as well. It assumes this means that the file was modified.
-//
-// 7. The watcher emits an unexpected MODIFIED event for "unwatched.txt",
-//    causing the test to fail.
-//
-// Once issue 14793 is fixed, this will no longer be the case and the test will
-// work again.
-
 main() {
   initConfig();
 
diff --git a/test/no_subscription/shared.dart b/test/no_subscription/shared.dart
index de625d5..99172a2 100644
--- a/test/no_subscription/shared.dart
+++ b/test/no_subscription/shared.dart
@@ -41,16 +41,6 @@
     schedule(() {
       completer = new Completer();
       subscription = watcher.events.listen(wrapAsync((event) {
-        // TODO(nweiz): Remove this when either issue 14373 or 14793 is fixed.
-        // Issue 14373 means that the new [Directory.watch] will emit an event
-        // for "unwatched.txt" being created, and issue 14793 means we have to
-        // check the filesystem, which leads us to assume that the file has been
-        // modified.
-        if (Platform.isMacOS && event.path.endsWith("unwatched.txt")) {
-          expect(event, isWatchEvent(ChangeType.MODIFY, "unwatched.txt"));
-          return;
-        }
-
         // We should get an event for the third file, not the one added while
         // we weren't subscribed.
         expect(event, isWatchEvent(ChangeType.ADD, "added.txt"));