Index: packages/watcher/test/utils.dart |
diff --git a/packages/watcher/test/utils.dart b/packages/watcher/test/utils.dart |
new file mode 100644 |
index 0000000000000000000000000000000000000000..7dd833231603d1bb2485151ebe608af3fe8ab571 |
--- /dev/null |
+++ b/packages/watcher/test/utils.dart |
@@ -0,0 +1,363 @@ |
+// Copyright (c) 2012, 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.test.utils; |
+ |
+import 'dart:io'; |
+ |
+import 'package:path/path.dart' as p; |
+import 'package:scheduled_test/scheduled_stream.dart'; |
+import 'package:scheduled_test/scheduled_test.dart'; |
+import 'package:watcher/watcher.dart'; |
+import 'package:watcher/src/stat.dart'; |
+import 'package:watcher/src/utils.dart'; |
+ |
+// TODO(nweiz): remove this when issue 15042 is fixed. |
+import 'package:watcher/src/directory_watcher/mac_os.dart'; |
+ |
+/// The path to the temporary sandbox created for each test. All file |
+/// operations are implicitly relative to this directory. |
+String _sandboxDir; |
+ |
+/// The [Watcher] being used for the current scheduled test. |
+Watcher _watcher; |
+ |
+/// The mock modification times (in milliseconds since epoch) for each file. |
+/// |
+/// The actual file system has pretty coarse granularity for file modification |
+/// times. This means using the real file system requires us to put delays in |
+/// the tests to ensure we wait long enough between operations for the mod time |
+/// to be different. |
+/// |
+/// Instead, we'll just mock that out. Each time a file is written, we manually |
+/// increment the mod time for that file instantly. |
+Map<String, int> _mockFileModificationTimes; |
+ |
+typedef Watcher WatcherFactory(String directory); |
+ |
+/// Sets the function used to create the watcher. |
+set watcherFactory(WatcherFactory factory) { |
+ _watcherFactory = factory; |
+} |
+WatcherFactory _watcherFactory; |
+ |
+/// Creates the sandbox directory the other functions in this library use and |
+/// ensures it's deleted when the test ends. |
+/// |
+/// This should usually be called by [setUp]. |
+void createSandbox() { |
+ var dir = Directory.systemTemp.createTempSync('watcher_test_'); |
+ _sandboxDir = dir.path; |
+ |
+ _mockFileModificationTimes = new Map<String, int>(); |
+ mockGetModificationTime((path) { |
+ path = p.normalize(p.relative(path, from: _sandboxDir)); |
+ |
+ // Make sure we got a path in the sandbox. |
+ assert(p.isRelative(path) && !path.startsWith("..")); |
+ |
+ var mtime = _mockFileModificationTimes[path]; |
+ return new DateTime.fromMillisecondsSinceEpoch(mtime == null ? 0 : mtime); |
+ }); |
+ |
+ // Delete the sandbox when done. |
+ currentSchedule.onComplete.schedule(() { |
+ if (_sandboxDir != null) { |
+ // TODO(rnystrom): Issue 19155. The watcher should already be closed when |
+ // we clean up the sandbox. |
+ if (_watcherEvents != null) { |
+ _watcherEvents.close(); |
+ } |
+ new Directory(_sandboxDir).deleteSync(recursive: true); |
+ _sandboxDir = null; |
+ } |
+ |
+ _mockFileModificationTimes = null; |
+ mockGetModificationTime(null); |
+ }, "delete sandbox"); |
+} |
+ |
+/// Creates a new [Watcher] that watches a temporary file or directory. |
+/// |
+/// 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. |
+/// |
+/// If [path] is provided, watches a subdirectory in the sandbox with that name. |
+Watcher createWatcher({String path, bool waitForReady}) { |
+ if (path == null) { |
+ path = _sandboxDir; |
+ } else { |
+ path = p.join(_sandboxDir, path); |
+ } |
+ |
+ var watcher = _watcherFactory(path); |
+ |
+ // Wait until the scan is finished so that we don't miss changes to files |
+ // that could occur before the scan completes. |
+ if (waitForReady != false) { |
+ schedule(() => watcher.ready, "wait for watcher to be ready"); |
+ } |
+ |
+ return watcher; |
+} |
+ |
+/// The stream of events from the watcher started with [startWatcher]. |
+ScheduledStream<WatchEvent> _watcherEvents; |
+ |
+/// Creates a new [Watcher] that watches a temporary file or directory and |
+/// starts monitoring it for events. |
+/// |
+/// If [path] is provided, watches a path in the sandbox with that name. |
+void startWatcher({String path}) { |
+ // We want to wait until we're ready *after* we subscribe to the watcher's |
+ // events. |
+ _watcher = createWatcher(path: path, waitForReady: false); |
+ |
+ // Schedule [_watcher.events.listen] so that the watcher doesn't start |
+ // watching [path] before it exists. Expose [_watcherEvents] immediately so |
+ // that it can be accessed synchronously after this. |
+ _watcherEvents = new ScheduledStream(futureStream(schedule(() { |
+ currentSchedule.onComplete.schedule(() { |
+ _watcher = null; |
+ if (!_closePending) _watcherEvents.close(); |
+ |
+ // If there are already errors, don't add this to the output and make |
+ // people think it might be the root cause. |
+ if (currentSchedule.errors.isEmpty) { |
+ _watcherEvents.expect(isDone); |
+ } |
+ }, "reset watcher"); |
+ |
+ return _watcher.events; |
+ }, "create watcher"), broadcast: true)); |
+ |
+ schedule(() => _watcher.ready, "wait for watcher to be ready"); |
+} |
+ |
+/// Whether an event to close [_watcherEvents] has been scheduled. |
+bool _closePending = false; |
+ |
+/// Schedule closing the watcher stream after the event queue has been pumped. |
+/// |
+/// This is necessary when events are allowed to occur, but don't have to occur, |
+/// at the end of a test. Otherwise, if they don't occur, the test will wait |
+/// indefinitely because they might in the future and because the watcher is |
+/// normally only closed after the test completes. |
+void startClosingEventStream() { |
+ schedule(() { |
+ _closePending = true; |
+ pumpEventQueue().then((_) => _watcherEvents.close()).whenComplete(() { |
+ _closePending = false; |
+ }); |
+ }, 'start closing event stream'); |
+} |
+ |
+/// A list of [StreamMatcher]s that have been collected using |
+/// [_collectStreamMatcher]. |
+List<StreamMatcher> _collectedStreamMatchers; |
+ |
+/// Collects all stream matchers that are registered within [block] into a |
+/// single stream matcher. |
+/// |
+/// The returned matcher will match each of the collected matchers in order. |
+StreamMatcher _collectStreamMatcher(block()) { |
+ var oldStreamMatchers = _collectedStreamMatchers; |
+ _collectedStreamMatchers = new List<StreamMatcher>(); |
+ try { |
+ block(); |
+ return inOrder(_collectedStreamMatchers); |
+ } finally { |
+ _collectedStreamMatchers = oldStreamMatchers; |
+ } |
+} |
+ |
+/// Either add [streamMatcher] as an expectation to [_watcherEvents], or collect |
+/// it with [_collectStreamMatcher]. |
+/// |
+/// [streamMatcher] can be a [StreamMatcher], a [Matcher], or a value. |
+void _expectOrCollect(streamMatcher) { |
+ if (_collectedStreamMatchers != null) { |
+ _collectedStreamMatchers.add(new StreamMatcher.wrap(streamMatcher)); |
+ } else { |
+ _watcherEvents.expect(streamMatcher); |
+ } |
+} |
+ |
+/// Expects that [matchers] will match emitted events in any order. |
+/// |
+/// [matchers] may be [Matcher]s or values, but not [StreamMatcher]s. |
+void inAnyOrder(Iterable matchers) { |
+ matchers = matchers.toSet(); |
+ _expectOrCollect(nextValues(matchers.length, unorderedMatches(matchers))); |
+} |
+ |
+/// Expects that the expectations established in either [block1] or [block2] |
+/// will match the emitted events. |
+/// |
+/// If both blocks match, the one that consumed more events will be used. |
+void allowEither(block1(), block2()) { |
+ _expectOrCollect(either( |
+ _collectStreamMatcher(block1), _collectStreamMatcher(block2))); |
+} |
+ |
+/// Allows the expectations established in [block] to match the emitted events. |
+/// |
+/// If the expectations in [block] don't match, no error will be raised and no |
+/// events will be consumed. If this is used at the end of a test, |
+/// [startClosingEventStream] should be called before it. |
+void allowEvents(block()) { |
+ _expectOrCollect(allow(_collectStreamMatcher(block))); |
+} |
+ |
+/// Returns a matcher that matches a [WatchEvent] with the given [type] and |
+/// [path]. |
+Matcher isWatchEvent(ChangeType type, String path) { |
+ return predicate((e) { |
+ return e is WatchEvent && e.type == type && |
+ e.path == p.join(_sandboxDir, p.normalize(path)); |
+ }, "is $type $path"); |
+} |
+ |
+/// Returns a [Matcher] that matches a [WatchEvent] for an add event for [path]. |
+Matcher isAddEvent(String path) => isWatchEvent(ChangeType.ADD, path); |
+ |
+/// Returns a [Matcher] that matches a [WatchEvent] for a modification event for |
+/// [path]. |
+Matcher isModifyEvent(String path) => isWatchEvent(ChangeType.MODIFY, path); |
+ |
+/// Returns a [Matcher] that matches a [WatchEvent] for a removal event for |
+/// [path]. |
+Matcher isRemoveEvent(String path) => isWatchEvent(ChangeType.REMOVE, path); |
+ |
+/// Expects that the next event emitted will be for an add event for [path]. |
+void expectAddEvent(String path) => |
+ _expectOrCollect(isWatchEvent(ChangeType.ADD, path)); |
+ |
+/// Expects that the next event emitted will be for a modification event for |
+/// [path]. |
+void expectModifyEvent(String path) => |
+ _expectOrCollect(isWatchEvent(ChangeType.MODIFY, path)); |
+ |
+/// Expects that the next event emitted will be for a removal event for [path]. |
+void expectRemoveEvent(String path) => |
+ _expectOrCollect(isWatchEvent(ChangeType.REMOVE, path)); |
+ |
+/// Consumes an add event for [path] if one is emitted at this point in the |
+/// schedule, but doesn't throw an error if it isn't. |
+/// |
+/// If this is used at the end of a test, [startClosingEventStream] should be |
+/// called before it. |
+void allowAddEvent(String path) => |
+ _expectOrCollect(allow(isWatchEvent(ChangeType.ADD, path))); |
+ |
+/// Consumes a modification event for [path] if one is emitted at this point in |
+/// the schedule, but doesn't throw an error if it isn't. |
+/// |
+/// If this is used at the end of a test, [startClosingEventStream] should be |
+/// called before it. |
+void allowModifyEvent(String path) => |
+ _expectOrCollect(allow(isWatchEvent(ChangeType.MODIFY, path))); |
+ |
+/// Consumes a removal event for [path] if one is emitted at this point in the |
+/// schedule, but doesn't throw an error if it isn't. |
+/// |
+/// If this is used at the end of a test, [startClosingEventStream] should be |
+/// called before it. |
+void allowRemoveEvent(String path) => |
+ _expectOrCollect(allow(isWatchEvent(ChangeType.REMOVE, path))); |
+ |
+/// Schedules writing a file in the sandbox at [path] with [contents]. |
+/// |
+/// If [contents] is omitted, creates an empty file. If [updatedModified] is |
+/// `false`, the mock file modification time is not changed. |
+void writeFile(String path, {String contents, bool updateModified}) { |
+ if (contents == null) contents = ""; |
+ if (updateModified == null) updateModified = true; |
+ |
+ schedule(() { |
+ var fullPath = p.join(_sandboxDir, path); |
+ |
+ // Create any needed subdirectories. |
+ var dir = new Directory(p.dirname(fullPath)); |
+ if (!dir.existsSync()) { |
+ dir.createSync(recursive: true); |
+ } |
+ |
+ new File(fullPath).writeAsStringSync(contents); |
+ |
+ // Manually update the mock modification time for the file. |
+ if (updateModified) { |
+ // Make sure we always use the same separator on Windows. |
+ path = p.normalize(path); |
+ |
+ var milliseconds = _mockFileModificationTimes.putIfAbsent(path, () => 0); |
+ _mockFileModificationTimes[path]++; |
+ } |
+ }, "write file $path"); |
+} |
+ |
+/// Schedules deleting a file in the sandbox at [path]. |
+void deleteFile(String path) { |
+ schedule(() { |
+ new File(p.join(_sandboxDir, path)).deleteSync(); |
+ }, "delete file $path"); |
+} |
+ |
+/// Schedules renaming a file in the sandbox from [from] to [to]. |
+/// |
+/// If [contents] is omitted, creates an empty file. |
+void renameFile(String from, String to) { |
+ schedule(() { |
+ new File(p.join(_sandboxDir, from)).renameSync(p.join(_sandboxDir, to)); |
+ |
+ // Make sure we always use the same separator on Windows. |
+ to = p.normalize(to); |
+ |
+ // Manually update the mock modification time for the file. |
+ var milliseconds = _mockFileModificationTimes.putIfAbsent(to, () => 0); |
+ _mockFileModificationTimes[to]++; |
+ }, "rename file $from to $to"); |
+} |
+ |
+/// Schedules creating a directory in the sandbox at [path]. |
+void createDir(String path) { |
+ schedule(() { |
+ new Directory(p.join(_sandboxDir, path)).createSync(); |
+ }, "create directory $path"); |
+} |
+ |
+/// Schedules renaming a directory in the sandbox from [from] to [to]. |
+void renameDir(String from, String to) { |
+ schedule(() { |
+ new Directory(p.join(_sandboxDir, from)) |
+ .renameSync(p.join(_sandboxDir, to)); |
+ }, "rename directory $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"); |
+} |
+ |
+/// Runs [callback] with every permutation of non-negative [i], [j], and [k] |
+/// less than [limit]. |
+/// |
+/// Returns a set of all values returns by [callback]. |
+/// |
+/// [limit] defaults to 3. |
+Set withPermutations(callback(int i, int j, int k), {int limit}) { |
+ if (limit == null) limit = 3; |
+ var results = new Set(); |
+ for (var i = 0; i < limit; i++) { |
+ for (var j = 0; j < limit; j++) { |
+ for (var k = 0; k < limit; k++) { |
+ results.add(callback(i, j, k)); |
+ } |
+ } |
+ } |
+ return results; |
+} |