Index: pkg/watcher/test/utils.dart |
diff --git a/pkg/watcher/test/utils.dart b/pkg/watcher/test/utils.dart |
index a7bd9b636234ba849a44155b07f6ff62a9f75a6a..f7e35f18af7da0d1e074112180e876b5c28e7a52 100644 |
--- a/pkg/watcher/test/utils.dart |
+++ b/pkg/watcher/test/utils.dart |
@@ -9,6 +9,7 @@ import 'dart:collection'; |
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:unittest/compact_vm_config.dart'; |
import 'package:watcher/watcher.dart'; |
@@ -25,11 +26,6 @@ String _sandboxDir; |
/// The [DirectoryWatcher] being used for the current scheduled test. |
DirectoryWatcher _watcher; |
-/// The index in [_watcher]'s event stream for the next event. When event |
-/// expectations are set using [expectEvent] (et. al.), they use this to |
-/// expect a series of events in order. |
-var _nextEvent = 0; |
- |
/// The mock modification times (in milliseconds since epoch) for each file. |
/// |
/// The actual file system has pretty coarse granularity for file modification |
@@ -111,7 +107,7 @@ DirectoryWatcher createWatcher({String dir, bool waitForReady}) { |
} |
/// The stream of events from the watcher started with [startWatcher]. |
-Stream _watcherEvents; |
+ScheduledStream<WatchEvent> _watcherEvents; |
/// Creates a new [DirectoryWatcher] that watches a temporary directory and |
/// starts monitoring it for events. |
@@ -130,82 +126,102 @@ void startWatcher({String dir}) { |
// Schedule [_watcher.events.listen] so that the watcher doesn't start |
// watching [dir] before it exists. Expose [_watcherEvents] immediately so |
// that it can be accessed synchronously after this. |
- _watcherEvents = futureStream(schedule(() { |
- var allEvents = new Queue(); |
- var subscription = _watcher.events.listen(allEvents.add, |
- onError: currentSchedule.signalError); |
- |
+ _watcherEvents = new ScheduledStream(futureStream(schedule(() { |
currentSchedule.onComplete.schedule(() { |
if (MacOSDirectoryWatcher.logDebugInfo) { |
print("stopping watcher for $testCase"); |
} |
- var numEvents = _nextEvent; |
- subscription.cancel(); |
- _nextEvent = 0; |
_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) { |
- expect(allEvents, hasLength(numEvents)); |
- } else { |
- currentSchedule.addDebugInfo("Events fired:\n${allEvents.join('\n')}"); |
+ _watcherEvents.expect(isDone); |
} |
}, "reset watcher"); |
return _watcher.events; |
- }, "create watcher"), broadcast: true); |
+ }, "create watcher"), broadcast: true)); |
schedule(() => _watcher.ready, "wait for watcher to be ready"); |
} |
-/// A future set by [inAnyOrder] that will complete to the set of events that |
-/// occur in the [inAnyOrder] block. |
-Future<Set<WatchEvent>> _unorderedEventFuture; |
+/// Whether an event to close [_watcherEvents] has been scheduled. |
+bool _closePending = false; |
-/// Runs [block] and allows multiple [expectEvent] calls in that block to match |
-/// events in any order. |
-void inAnyOrder(block()) { |
- var oldFuture = _unorderedEventFuture; |
+/// Schedule closing the directory 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 { |
- var firstEvent = _nextEvent; |
- var completer = new Completer(); |
- _unorderedEventFuture = completer.future; |
block(); |
- |
- _watcherEvents.skip(firstEvent).take(_nextEvent - firstEvent).toSet() |
- .then(completer.complete, onError: completer.completeError); |
- currentSchedule.wrapFuture(_unorderedEventFuture, |
- "waiting for ${_nextEvent - firstEvent} events"); |
+ return inOrder(_collectedStreamMatchers); |
} finally { |
- _unorderedEventFuture = oldFuture; |
+ _collectedStreamMatchers = oldStreamMatchers; |
} |
} |
-/// Expects that the next set of event will be a change of [type] on [path]. |
+/// Either add [streamMatcher] as an expectation to [_watcherEvents], or collect |
+/// it with [_collectStreamMatcher]. |
/// |
-/// Multiple calls to [expectEvent] require that the events are received in that |
-/// order unless they're called in an [inAnyOrder] block. |
-void expectEvent(ChangeType type, String path) { |
- if (_unorderedEventFuture != null) { |
- // Assign this to a local variable since it will be un-assigned by the time |
- // the scheduled callback runs. |
- var future = _unorderedEventFuture; |
- |
- expect( |
- schedule(() => future, "should fire $type event on $path"), |
- completion(contains(isWatchEvent(type, path)))); |
+/// [streamMatcher] can be a [StreamMatcher], a [Matcher], or a value. |
+void _expectOrCollect(streamMatcher) { |
+ if (_collectedStreamMatchers != null) { |
+ _collectedStreamMatchers.add(new StreamMatcher.wrap(streamMatcher)); |
} else { |
- var future = currentSchedule.wrapFuture( |
- _watcherEvents.elementAt(_nextEvent), |
- "waiting for $type event on $path"); |
- |
- expect( |
- schedule(() => future, "should fire $type event on $path"), |
- completion(isWatchEvent(type, path))); |
+ _watcherEvents.expect(streamMatcher); |
} |
- _nextEvent++; |
+} |
+ |
+/// 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 |
@@ -217,9 +233,53 @@ Matcher isWatchEvent(ChangeType type, String path) { |
}, "is $type $path"); |
} |
-void expectAddEvent(String path) => expectEvent(ChangeType.ADD, path); |
-void expectModifyEvent(String path) => expectEvent(ChangeType.MODIFY, path); |
-void expectRemoveEvent(String path) => expectEvent(ChangeType.REMOVE, 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]. |
/// |
@@ -299,14 +359,18 @@ void deleteDir(String 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. |
-void withPermutations(callback(int i, int j, int k), {int limit}) { |
+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++) { |
- callback(i, j, k); |
+ results.add(callback(i, j, k)); |
} |
} |
} |
+ return results; |
} |