Wrap Directory.watch on Mac OS for the watcher package.

R=rnystrom@google.com
BUG=14428

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

git-svn-id: https://dart.googlecode.com/svn/branches/bleeding_edge/dart@30168 260f80e4-7a28-3924-810f-c04153c831b5
This commit is contained in:
nweiz@google.com 2013-11-11 19:47:18 +00:00
parent a00c36e401
commit 03337e9e5f
13 changed files with 983 additions and 2 deletions

View file

@ -330,5 +330,11 @@ unittest/test/mock_test: StaticWarning, OK # testing unimplemented members
[ $runtime == vm && ($system == windows || $system == macos) ]
watcher/test/*/linux_test: Skip
[ $runtime == vm && ($system == windows || $system == linux) ]
watcher/test/*/mac_os_test: Skip
[ $runtime == vm && $system == linux ]
watcher/test/*/linux_test: Pass, Slow # Issue 14606
[ $runtime == vm && $system == macos ]
watcher/test/no_subscribers/mac_os_test: Fail # Issue 14793 (see test file for details)

View file

@ -0,0 +1,60 @@
// 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.constructable_file_system_event;
import 'dart:io';
abstract class _ConstructableFileSystemEvent implements FileSystemEvent {
final bool isDirectory;
final String path;
final int type;
_ConstructableFileSystemEvent(this.path, this.isDirectory);
}
class ConstructableFileSystemCreateEvent extends _ConstructableFileSystemEvent
implements FileSystemCreateEvent {
final type = FileSystemEvent.CREATE;
ConstructableFileSystemCreateEvent(String path, bool isDirectory)
: super(path, isDirectory);
String toString() => "FileSystemCreateEvent('$path')";
}
class ConstructableFileSystemDeleteEvent extends _ConstructableFileSystemEvent
implements FileSystemDeleteEvent {
final type = FileSystemEvent.DELETE;
ConstructableFileSystemDeleteEvent(String path, bool isDirectory)
: super(path, isDirectory);
String toString() => "FileSystemDeleteEvent('$path')";
}
class ConstructableFileSystemModifyEvent extends _ConstructableFileSystemEvent
implements FileSystemModifyEvent {
final bool contentChanged;
final type = FileSystemEvent.MODIFY;
ConstructableFileSystemModifyEvent(String path, bool isDirectory,
this.contentChanged)
: super(path, isDirectory);
String toString() =>
"FileSystemModifyEvent('$path', contentChanged=$contentChanged)";
}
class ConstructableFileSystemMoveEvent extends _ConstructableFileSystemEvent
implements FileSystemMoveEvent {
final String destination;
final type = FileSystemEvent.MOVE;
ConstructableFileSystemMoveEvent(String path, bool isDirectory,
this.destination)
: super(path, isDirectory);
String toString() => "FileSystemMoveEvent('$path', '$destination')";
}

View file

@ -9,6 +9,7 @@ import 'dart:io';
import 'watch_event.dart';
import 'directory_watcher/linux.dart';
import 'directory_watcher/mac_os.dart';
import 'directory_watcher/polling.dart';
/// Watches the contents of a directory and emits [WatchEvent]s when something
@ -54,6 +55,7 @@ abstract class DirectoryWatcher {
/// watchers.
factory DirectoryWatcher(String directory, {Duration pollingDelay}) {
if (Platform.isLinux) return new LinuxDirectoryWatcher(directory);
if (Platform.isMacOS) return new MacOSDirectoryWatcher(directory);
return new PollingDirectoryWatcher(directory, pollingDelay: pollingDelay);
}
}

View file

@ -12,8 +12,6 @@ import '../utils.dart';
import '../watch_event.dart';
import 'resubscribable.dart';
import 'package:stack_trace/stack_trace.dart';
/// Uses the inotify subsystem to watch for filesystem events.
///
/// Inotify doesn't suport recursively watching subdirectories, nor does

View file

@ -0,0 +1,355 @@
// 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.directory_watcher.mac_os;
import 'dart:async';
import 'dart:io';
import '../constructable_file_system_event.dart';
import '../path_set.dart';
import '../utils.dart';
import '../watch_event.dart';
import 'resubscribable.dart';
import 'package:path/path.dart' as p;
/// Uses the FSEvents subsystem to watch for filesystem events.
///
/// FSEvents has two main idiosyncrasies that this class works around. First, it
/// will occasionally report events that occurred before the filesystem watch
/// was initiated. Second, if multiple events happen to the same file in close
/// 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].
class MacOSDirectoryWatcher extends ResubscribableDirectoryWatcher {
MacOSDirectoryWatcher(String directory)
: super(directory, () => new _MacOSDirectoryWatcher(directory));
}
class _MacOSDirectoryWatcher implements ManuallyClosedDirectoryWatcher {
final String directory;
Stream<WatchEvent> get events => _eventsController.stream;
final _eventsController = new StreamController<WatchEvent>.broadcast();
bool get isReady => _readyCompleter.isCompleted;
Future get ready => _readyCompleter.future;
final _readyCompleter = new Completer();
/// The number of event batches that have been received from
/// [Directory.watch].
///
/// This is used to determine if the [Directory.watch] stream was falsely
/// closed due to issue 14849. A close caused by events in the past will only
/// happen before or immediately after the first batch of events.
int batches = 0;
/// The set of files that are known to exist recursively within the watched
/// directory.
///
/// The state of files on the filesystem is compared against this to determine
/// the real change that occurred when working around issue 14373. This is
/// also used to emit REMOVE events when subdirectories are moved out of the
/// watched directory.
final PathSet _files;
/// The subscription to the stream returned by [Directory.watch].
///
/// This is separate from [_subscriptions] because this stream occasionally
/// needs to be resubscribed in order to work around issue 14849.
StreamSubscription<FileSystemEvent> _watchSubscription;
/// A set of subscriptions that this watcher subscribes to.
///
/// These are gathered together so that they may all be canceled when the
/// watcher is closed. This does not include [_watchSubscription].
final _subscriptions = new Set<StreamSubscription>();
_MacOSDirectoryWatcher(String directory)
: directory = directory,
_files = new PathSet(directory) {
_startWatch();
_listen(new Directory(directory).list(recursive: true),
(entity) {
if (entity is! Directory) _files.add(entity.path);
},
onError: _emitError,
onDone: _readyCompleter.complete,
cancelOnError: true);
}
void close() {
for (var subscription in _subscriptions) {
subscription.cancel();
}
_subscriptions.clear();
if (_watchSubscription != null) _watchSubscription.cancel();
_watchSubscription = null;
_eventsController.close();
}
/// The callback that's run when [Directory.watch] emits a batch of events.
void _onBatch(List<FileSystemEvent> batch) {
batches++;
_sortEvents(batch).forEach((path, events) {
var canonicalEvent = _canonicalEvent(events);
events = canonicalEvent == null ?
_eventsBasedOnFileSystem(path) : [canonicalEvent];
for (var event in events) {
if (event is FileSystemCreateEvent) {
if (!event.isDirectory) {
_emitEvent(ChangeType.ADD, path);
_files.add(path);
continue;
}
_listen(new Directory(path).list(recursive: true), (entity) {
if (entity is Directory) return;
_emitEvent(ChangeType.ADD, entity.path);
_files.add(entity.path);
}, onError: _emitError, cancelOnError: true);
} else if (event is FileSystemModifyEvent) {
assert(!event.isDirectory);
_emitEvent(ChangeType.MODIFY, path);
} else {
assert(event is FileSystemDeleteEvent);
for (var removedPath in _files.remove(path)) {
_emitEvent(ChangeType.REMOVE, removedPath);
}
}
}
});
}
/// Sort all the events in a batch into sets based on their path.
///
/// A single input event may result in multiple events in the returned map;
/// for example, a MOVE event becomes a DELETE event for the source and a
/// CREATE event for the destination.
///
/// The returned events won't contain any [FileSystemMoveEvent]s, nor will it
/// contain any events relating to [directory].
Map<String, Set<FileSystemEvent>> _sortEvents(List<FileSystemEvent> batch) {
var eventsForPaths = {};
// FSEvents can report past events, including events on the root directory
// such as it being created. We want to ignore these. If the directory is
// really deleted, that's handled by [_onDone].
batch = batch.where((event) => event.path != directory).toList();
// Events within directories that already have events are superfluous; the
// directory's full contents will be examined anyway, so we ignore such
// events. Emitting them could cause useless or out-of-order events.
var directories = unionAll(batch.map((event) {
if (!event.isDirectory) return new Set();
if (event is! FileSystemMoveEvent) return new Set.from([event.path]);
return new Set.from([event.path, event.destination]);
}));
isInModifiedDirectory(path) =>
directories.any((dir) => path != dir && path.startsWith(dir));
addEvent(path, event) {
if (isInModifiedDirectory(path)) return;
var set = eventsForPaths.putIfAbsent(path, () => new Set());
set.add(event);
}
for (var event in batch.where((event) => 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;
}
/// Returns the canonical event from a batch of events on the same path, if
/// one exists.
///
/// If [batch] doesn't contain any contradictory events (e.g. DELETE and
/// CREATE, or events with different values for [isDirectory]), this returns a
/// single event that describes what happened to the path in question.
///
/// If [batch] does contain contradictory events, this returns `null` to
/// indicate that the state of the path on the filesystem should be checked to
/// determine what occurred.
FileSystemEvent _canonicalEvent(Set<FileSystemEvent> batch) {
// An empty batch indicates that we've learned earlier that the batch is
// contradictory (e.g. because of a move).
if (batch.isEmpty) return null;
var type = batch.first.type;
var isDir = batch.first.isDirectory;
for (var event in batch.skip(1)) {
// If one event reports that the file is a directory and another event
// doesn't, that's a contradiction.
if (isDir != event.isDirectory) return null;
// Modify events don't contradict either CREATE or REMOVE events. We can
// 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;
assert(event is FileSystemCreateEvent || event is FileSystemDeleteEvent);
// If we previously thought this was a MODIFY, we now consider it to be a
// CREATE or REMOVE event. This is safe for the same reason as above.
if (type == FileSystemEvent.MODIFY) {
type = event.type;
continue;
}
// A CREATE event contradicts a REMOVE event and vice versa.
assert(type == FileSystemEvent.CREATE || type == FileSystemEvent.DELETE);
if (type != event.type) return null;
}
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;
case FileSystemEvent.DELETE:
return new ConstructableFileSystemDeleteEvent(batch.first.path, isDir);
case FileSystemEvent.MODIFY:
return new ConstructableFileSystemModifyEvent(
batch.first.path, isDir, false);
default: assert(false);
}
}
/// Returns one or more events that describe the change between the last known
/// state of [path] and its current state on the filesystem.
///
/// This returns a list whose order should be reflected in the events emitted
/// to the user, unlike the batched events from [Directory.watch]. The
/// returned list may be empty, indicating that no changes occurred to [path]
/// (probably indicating that it was created and then immediately deleted).
List<FileSystemEvent> _eventsBasedOnFileSystem(String path) {
var fileExisted = _files.contains(path);
var dirExisted = _files.containsDir(path);
var fileExists = new File(path).existsSync();
var dirExists = new Directory(path).existsSync();
var events = [];
if (fileExisted) {
if (fileExists) {
events.add(new ConstructableFileSystemModifyEvent(path, false, false));
} else {
events.add(new ConstructableFileSystemDeleteEvent(path, false));
}
} else if (dirExisted) {
if (dirExists) {
// If we got contradictory events for a directory that used to exist and
// still exists, we need to rescan the whole thing in case it was
// replaced with a different directory.
events.add(new ConstructableFileSystemDeleteEvent(path, true));
events.add(new ConstructableFileSystemCreateEvent(path, true));
} else {
events.add(new ConstructableFileSystemDeleteEvent(path, true));
}
}
if (!fileExisted && fileExists) {
events.add(new ConstructableFileSystemCreateEvent(path, false));
} else if (!dirExisted && dirExists) {
events.add(new ConstructableFileSystemCreateEvent(path, true));
}
return events;
}
/// The callback that's run when the [Directory.watch] stream is closed.
void _onDone() {
_watchSubscription = null;
// If the directory still exists and we haven't seen more than one batch,
// this is probably issue 14849 rather than a real close event. We should
// just restart the watcher.
if (batches < 2 && new Directory(directory).existsSync()) {
_startWatch();
return;
}
// FSEvents can fail to report the contents of the directory being removed
// when the directory itself is removed, so we need to manually mark the as
// removed.
for (var file in _files.toSet()) {
_emitEvent(ChangeType.REMOVE, file);
}
_files.clear();
close();
}
/// Start or restart the underlying [Directory.watch] stream.
void _startWatch() {
// Batch the FSEvent changes together so that we can dedup events.
var innerStream = new Directory(directory).watch(recursive: true).transform(
new BatchedStreamTransformer<FileSystemEvent>());
_watchSubscription = innerStream.listen(_onBatch,
onError: _eventsController.addError,
onDone: _onDone);
}
/// Emit an event with the given [type] and [path].
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));
}
/// Emit an error, then close the watcher.
void _emitError(error, StackTrace stackTrace) {
_eventsController.add(error, stackTrace);
close();
}
/// Like [Stream.listen], but automatically adds the subscription to
/// [_subscriptions] so that it can be canceled when [close] is called.
void _listen(Stream stream, void onData(event), {Function onError,
void onDone(), bool cancelOnError}) {
var subscription;
subscription = stream.listen(onData, onError: onError, onDone: () {
_subscriptions.remove(subscription);
if (onDone != null) onDone();
}, cancelOnError: cancelOnError);
_subscriptions.add(subscription);
}
}

View file

@ -0,0 +1,168 @@
// 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.path_dart;
import 'dart:collection';
import 'package:path/path.dart' as p;
/// A set of paths, organized into a directory hierarchy.
///
/// When a path is [add]ed, it creates an implicit directory structure above
/// that path. Directories can be inspected using [containsDir] and removed
/// using [remove]. If they're removed, their contents are removed as well.
///
/// The paths in the set are normalized so that they all begin with [root].
class PathSet {
/// The root path, which all paths in the set must be under.
final String root;
/// The path set's directory hierarchy.
///
/// Each level of this hierarchy has the same structure: a map from strings to
/// other maps, which are further levels of the hierarchy. A map with no
/// elements indicates a path that was added to the set that has no paths
/// beneath it. Such a path should not be treated as a directory by
/// [containsDir].
final _entries = new Map<String, Map>();
/// The set of paths that were explicitly added to this set.
///
/// This is needed to disambiguate a directory that was explicitly added to
/// the set from a directory that was implicitly added by adding a path
/// beneath it.
final _paths = new Set<String>();
PathSet(this.root);
/// Adds [path] to the set.
void add(String path) {
path = _normalize(path);
_paths.add(path);
var parts = _split(path);
var dir = _entries;
for (var part in parts) {
dir = dir.putIfAbsent(part, () => {});
}
}
/// Removes [path] and any paths beneath it from the set and returns the
/// removed paths.
///
/// Even if [path] itself isn't in the set, if it's a directory containing
/// paths that are in the set those paths will be removed and returned.
///
/// If neither [path] nor any paths beneath it are in the set, returns an
/// empty set.
Set<String> remove(String path) {
path = _normalize(path);
var parts = new Queue.from(_split(path));
// Remove the children of [dir], as well as [dir] itself if necessary.
//
// [partialPath] is the path to [dir], and a prefix of [path]; the remaining
// components of [path] are in [parts].
recurse(dir, partialPath) {
if (parts.length > 1) {
// If there's more than one component left in [path], recurse down to
// the next level.
var part = parts.removeFirst();
var entry = dir[part];
if (entry.isEmpty) return new Set();
partialPath = p.join(partialPath, part);
var paths = recurse(entry, partialPath);
// After removing this entry's children, if it has no more children and
// it's not in the set in its own right, remove it as well.
if (entry.isEmpty && !_paths.contains(partialPath)) dir.remove(part);
return paths;
}
// If there's only one component left in [path], we should remove it.
var entry = dir.remove(parts.first);
if (entry == null) return new Set();
if (entry.isEmpty) {
_paths.remove(path);
return new Set.from([path]);
}
var set = _removePathsIn(entry, path);
if (_paths.contains(path)) {
_paths.remove(path);
set.add(path);
}
return set;
}
return recurse(_entries, root);
}
/// Recursively removes and returns all paths in [dir].
///
/// [root] should be the path to [dir].
Set<String> _removePathsIn(Map dir, String root) {
var removedPaths = new Set();
recurse(dir, path) {
dir.forEach((name, entry) {
var entryPath = p.join(path, name);
if (_paths.remove(entryPath)) removedPaths.add(entryPath);
recurse(entry, entryPath);
});
}
recurse(dir, root);
return removedPaths;
}
/// Returns whether [this] contains [path].
///
/// This only returns true for paths explicitly added to [this].
/// Implicitly-added directories can be inspected using [containsDir].
bool contains(String path) => _paths.contains(_normalize(path));
/// Returns whether [this] contains paths beneath [path].
bool containsDir(String path) {
path = _normalize(path);
var dir = _entries;
for (var part in _split(path)) {
dir = dir[part];
if (dir == null) return false;
}
return !dir.isEmpty;
}
/// Returns a [Set] of all paths in [this].
Set<String> toSet() => _paths.toSet();
/// Removes all paths from [this].
void clear() {
_paths.clear();
_entries.clear();
}
String toString() => _paths.toString();
/// Returns a normalized version of [path].
///
/// This removes any extra ".." or "."s and ensure that the returned path
/// begins with [root]. It's an error if [path] isn't within [root].
String _normalize(String path) {
var relative = p.relative(p.normalize(path), from: root);
var parts = p.split(relative);
// TODO(nweiz): replace this with [p.isWithin] when that exists (issue
// 14980).
if (!p.isRelative(relative) || parts.first == '..' || parts.first == '.') {
throw new ArgumentError('Path "$path" is not inside "$root".');
}
return p.join(root, relative);
}
/// Returns the segments of [path] beneath [root].
List<String> _split(String path) => p.split(p.relative(path, from: root));
}

View file

@ -18,6 +18,10 @@ bool isDirectoryNotFoundException(error) {
return error.osError.errorCode == notFoundCode;
}
/// Returns the union of all elements in each set in [sets].
Set unionAll(Iterable<Set> sets) =>
sets.fold(new Set(), (union, set) => union.union(set));
/// Returns a buffered stream that will emit the same values as the stream
/// returned by [future] once [future] completes.
///

View file

@ -0,0 +1,67 @@
// 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.
import 'package:scheduled_test/scheduled_test.dart';
import 'package:watcher/src/directory_watcher/mac_os.dart';
import 'package:watcher/watcher.dart';
import 'shared.dart';
import '../utils.dart';
main() {
initConfig();
watcherFactory = (dir) => new MacOSDirectoryWatcher(dir);
setUp(createSandbox);
sharedTests();
test('DirectoryWatcher creates a MacOSDirectoryWatcher on Mac OS', () {
expect(new DirectoryWatcher('.'),
new isInstanceOf<MacOSDirectoryWatcher>());
});
test('does not notify about the watched directory being deleted and '
'recreated immediately before watching', () {
createDir("dir");
writeFile("dir/old.txt");
deleteDir("dir");
createDir("dir");
startWatcher(dir: "dir");
writeFile("dir/newer.txt");
expectAddEvent("dir/newer.txt");
});
test('notifies even if the file contents are unchanged', () {
writeFile("a.txt", contents: "same");
writeFile("b.txt", contents: "before");
startWatcher();
writeFile("a.txt", contents: "same");
writeFile("b.txt", contents: "after");
expectModifyEvent("a.txt");
expectModifyEvent("b.txt");
});
test('emits events for many nested files moved out then immediately back in',
() {
withPermutations((i, j, k) =>
writeFile("dir/sub/sub-$i/sub-$j/file-$k.txt"));
startWatcher(dir: "dir");
renameDir("dir/sub", "sub");
renameDir("sub", "dir/sub");
inAnyOrder(() {
withPermutations((i, j, k) =>
expectRemoveEvent("dir/sub/sub-$i/sub-$j/file-$k.txt"));
});
inAnyOrder(() {
withPermutations((i, j, k) =>
expectAddEvent("dir/sub/sub-$i/sub-$j/file-$k.txt"));
});
});
}

View file

@ -6,6 +6,8 @@ import 'package:scheduled_test/scheduled_test.dart';
import '../utils.dart';
import 'dart:async';
sharedTests() {
test('does not notify for files that already exist when started', () {
// Make some pre-existing files.
@ -218,5 +220,21 @@ sharedTests() {
});
});
});
test("emits events for many files added at once in a subdirectory with the "
"same name as a removed file", () {
writeFile("dir/sub");
withPermutations((i, j, k) =>
writeFile("old/sub-$i/sub-$j/file-$k.txt"));
startWatcher(dir: "dir");
deleteFile("dir/sub");
renameDir("old", "dir/sub");
inAnyOrder(() {
expectRemoveEvent("dir/sub");
withPermutations((i, j, k) =>
expectAddEvent("dir/sub/sub-$i/sub-$j/file-$k.txt"));
});
});
});
}

View file

@ -0,0 +1,46 @@
// 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.
import 'package:scheduled_test/scheduled_test.dart';
import 'package:watcher/src/directory_watcher/mac_os.dart';
import 'package:watcher/watcher.dart';
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();
watcherFactory = (dir) => new MacOSDirectoryWatcher(dir);
setUp(createSandbox);
sharedTests();
}

View file

@ -0,0 +1,235 @@
// 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.
import 'package:path/path.dart' as p;
import 'package:unittest/unittest.dart';
import 'package:watcher/src/path_set.dart';
import 'utils.dart';
Matcher containsPath(String path) => predicate((set) =>
set is PathSet && set.contains(path),
'set contains "$path"');
Matcher containsDir(String path) => predicate((set) =>
set is PathSet && set.containsDir(path),
'set contains directory "$path"');
void main() {
initConfig();
var set;
setUp(() => set = new PathSet("root"));
group("adding a path", () {
test("stores the path in the set", () {
set.add("root/path/to/file");
expect(set, containsPath("root/path/to/file"));
});
test("that's a subdir of another path keeps both in the set", () {
set.add("root/path");
set.add("root/path/to/file");
expect(set, containsPath("root/path"));
expect(set, containsPath("root/path/to/file"));
});
test("that's not normalized normalizes the path before storing it", () {
set.add("root/../root/path/to/../to/././file");
expect(set, containsPath("root/path/to/file"));
});
test("that's absolute normalizes the path before storing it", () {
set.add(p.absolute("root/path/to/file"));
expect(set, containsPath("root/path/to/file"));
});
test("that's not beneath the root throws an error", () {
expect(() => set.add("path/to/file"), throwsArgumentError);
});
});
group("removing a path", () {
test("that's in the set removes and returns that path", () {
set.add("root/path/to/file");
expect(set.remove("root/path/to/file"),
unorderedEquals(["root/path/to/file"]));
expect(set, isNot(containsPath("root/path/to/file")));
});
test("that's not in the set returns an empty set", () {
set.add("root/path/to/file");
expect(set.remove("root/path/to/nothing"), isEmpty);
});
test("that's a directory removes and returns all files beneath it", () {
set.add("root/outside");
set.add("root/path/to/one");
set.add("root/path/to/two");
set.add("root/path/to/sub/three");
expect(set.remove("root/path"), unorderedEquals([
"root/path/to/one",
"root/path/to/two",
"root/path/to/sub/three"
]));
expect(set, containsPath("root/outside"));
expect(set, isNot(containsPath("root/path/to/one")));
expect(set, isNot(containsPath("root/path/to/two")));
expect(set, isNot(containsPath("root/path/to/sub/three")));
});
test("that's a directory in the set removes and returns it and all files "
"beneath it", () {
set.add("root/path");
set.add("root/path/to/one");
set.add("root/path/to/two");
set.add("root/path/to/sub/three");
expect(set.remove("root/path"), unorderedEquals([
"root/path",
"root/path/to/one",
"root/path/to/two",
"root/path/to/sub/three"
]));
expect(set, isNot(containsPath("root/path")));
expect(set, isNot(containsPath("root/path/to/one")));
expect(set, isNot(containsPath("root/path/to/two")));
expect(set, isNot(containsPath("root/path/to/sub/three")));
});
test("that's not normalized removes and returns the normalized path", () {
set.add("root/path/to/file");
expect(set.remove("root/../root/path/to/../to/./file"),
unorderedEquals(["root/path/to/file"]));
});
test("that's absolute removes and returns the normalized path", () {
set.add("root/path/to/file");
expect(set.remove(p.absolute("root/path/to/file")),
unorderedEquals(["root/path/to/file"]));
});
test("that's not beneath the root throws an error", () {
expect(() => set.remove("path/to/file"), throwsArgumentError);
});
});
group("containsPath()", () {
test("returns false for a non-existent path", () {
set.add("root/path/to/file");
expect(set, isNot(containsPath("root/path/to/nothing")));
});
test("returns false for a directory that wasn't added explicitly", () {
set.add("root/path/to/file");
expect(set, isNot(containsPath("root/path")));
});
test("returns true for a directory that was added explicitly", () {
set.add("root/path");
set.add("root/path/to/file");
expect(set, containsPath("root/path"));
});
test("with a non-normalized path normalizes the path before looking it up",
() {
set.add("root/path/to/file");
expect(set, containsPath("root/../root/path/to/../to/././file"));
});
test("with an absolute path normalizes the path before looking it up", () {
set.add("root/path/to/file");
expect(set, containsPath(p.absolute("root/path/to/file")));
});
test("with a path that's not beneath the root throws an error", () {
expect(() => set.contains("path/to/file"), throwsArgumentError);
});
});
group("containsDir()", () {
test("returns true for a directory that was added implicitly", () {
set.add("root/path/to/file");
expect(set, containsDir("root/path"));
expect(set, containsDir("root/path/to"));
});
test("returns true for a directory that was added explicitly", () {
set.add("root/path");
set.add("root/path/to/file");
expect(set, containsDir("root/path"));
});
test("returns false for a directory that wasn't added", () {
expect(set, isNot(containsDir("root/nothing")));
});
test("returns false for a non-directory path that was added", () {
set.add("root/path/to/file");
expect(set, isNot(containsDir("root/path/to/file")));
});
test("returns false for a directory that was added implicitly and then "
"removed implicitly", () {
set.add("root/path/to/file");
set.remove("root/path/to/file");
expect(set, isNot(containsDir("root/path")));
});
test("returns false for a directory that was added explicitly whose "
"children were then removed", () {
set.add("root/path");
set.add("root/path/to/file");
set.remove("root/path/to/file");
expect(set, isNot(containsDir("root/path")));
});
test("with a non-normalized path normalizes the path before looking it up",
() {
set.add("root/path/to/file");
expect(set, containsDir("root/../root/path/to/../to/."));
});
test("with an absolute path normalizes the path before looking it up", () {
set.add("root/path/to/file");
expect(set, containsDir(p.absolute("root/path")));
});
});
group("toSet", () {
test("returns paths added to the set", () {
set.add("root/path");
set.add("root/path/to/one");
set.add("root/path/to/two");
expect(set.toSet(), unorderedEquals([
"root/path",
"root/path/to/one",
"root/path/to/two",
]));
});
test("doesn't return paths removed from the set", () {
set.add("root/path/to/one");
set.add("root/path/to/two");
set.remove("root/path/to/two");
expect(set.toSet(), unorderedEquals(["root/path/to/one"]));
});
});
group("clear", () {
test("removes all paths from the set", () {
set.add("root/path");
set.add("root/path/to/one");
set.add("root/path/to/two");
set.clear();
expect(set.toSet(), isEmpty);
});
});
}

View file

@ -0,0 +1,20 @@
// 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.
import 'package:scheduled_test/scheduled_test.dart';
import 'package:watcher/src/directory_watcher/mac_os.dart';
import 'package:watcher/watcher.dart';
import 'shared.dart';
import '../utils.dart';
main() {
initConfig();
watcherFactory = (dir) => new MacOSDirectoryWatcher(dir);
setUp(createSandbox);
sharedTests();
}

View file

@ -137,6 +137,8 @@ void startWatcher({String dir}) {
// people think it might be the root cause.
if (currentSchedule.errors.isEmpty) {
expect(allEvents, hasLength(numEvents));
} else {
currentSchedule.addDebugInfo("Events fired:\n${allEvents.join('\n')}");
}
}, "reset watcher");