| OLD | NEW |
| 1 // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file | 1 // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file |
| 2 // for details. All rights reserved. Use of this source code is governed by a | 2 // for details. All rights reserved. Use of this source code is governed by a |
| 3 // BSD-style license that can be found in the LICENSE file. | 3 // BSD-style license that can be found in the LICENSE file. |
| 4 | 4 |
| 5 library watcher.test.utils; | 5 library watcher.test.utils; |
| 6 | 6 |
| 7 import 'dart:async'; | 7 import 'dart:async'; |
| 8 import 'dart:collection'; | 8 import 'dart:collection'; |
| 9 import 'dart:io'; | 9 import 'dart:io'; |
| 10 | 10 |
| 11 import 'package:path/path.dart' as p; | 11 import 'package:path/path.dart' as p; |
| 12 import 'package:scheduled_test/scheduled_stream.dart'; |
| 12 import 'package:scheduled_test/scheduled_test.dart'; | 13 import 'package:scheduled_test/scheduled_test.dart'; |
| 13 import 'package:unittest/compact_vm_config.dart'; | 14 import 'package:unittest/compact_vm_config.dart'; |
| 14 import 'package:watcher/watcher.dart'; | 15 import 'package:watcher/watcher.dart'; |
| 15 import 'package:watcher/src/stat.dart'; | 16 import 'package:watcher/src/stat.dart'; |
| 16 import 'package:watcher/src/utils.dart'; | 17 import 'package:watcher/src/utils.dart'; |
| 17 | 18 |
| 18 // TODO(nweiz): remove this when issue 15042 is fixed. | 19 // TODO(nweiz): remove this when issue 15042 is fixed. |
| 19 import 'package:watcher/src/directory_watcher/mac_os.dart'; | 20 import 'package:watcher/src/directory_watcher/mac_os.dart'; |
| 20 | 21 |
| 21 /// The path to the temporary sandbox created for each test. All file | 22 /// The path to the temporary sandbox created for each test. All file |
| 22 /// operations are implicitly relative to this directory. | 23 /// operations are implicitly relative to this directory. |
| 23 String _sandboxDir; | 24 String _sandboxDir; |
| 24 | 25 |
| 25 /// The [DirectoryWatcher] being used for the current scheduled test. | 26 /// The [DirectoryWatcher] being used for the current scheduled test. |
| 26 DirectoryWatcher _watcher; | 27 DirectoryWatcher _watcher; |
| 27 | 28 |
| 28 /// The index in [_watcher]'s event stream for the next event. When event | |
| 29 /// expectations are set using [expectEvent] (et. al.), they use this to | |
| 30 /// expect a series of events in order. | |
| 31 var _nextEvent = 0; | |
| 32 | |
| 33 /// The mock modification times (in milliseconds since epoch) for each file. | 29 /// The mock modification times (in milliseconds since epoch) for each file. |
| 34 /// | 30 /// |
| 35 /// The actual file system has pretty coarse granularity for file modification | 31 /// The actual file system has pretty coarse granularity for file modification |
| 36 /// times. This means using the real file system requires us to put delays in | 32 /// times. This means using the real file system requires us to put delays in |
| 37 /// the tests to ensure we wait long enough between operations for the mod time | 33 /// the tests to ensure we wait long enough between operations for the mod time |
| 38 /// to be different. | 34 /// to be different. |
| 39 /// | 35 /// |
| 40 /// Instead, we'll just mock that out. Each time a file is written, we manually | 36 /// Instead, we'll just mock that out. Each time a file is written, we manually |
| 41 /// increment the mod time for that file instantly. | 37 /// increment the mod time for that file instantly. |
| 42 Map<String, int> _mockFileModificationTimes; | 38 Map<String, int> _mockFileModificationTimes; |
| (...skipping 61 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 104 // Wait until the scan is finished so that we don't miss changes to files | 100 // Wait until the scan is finished so that we don't miss changes to files |
| 105 // that could occur before the scan completes. | 101 // that could occur before the scan completes. |
| 106 if (waitForReady != false) { | 102 if (waitForReady != false) { |
| 107 schedule(() => watcher.ready, "wait for watcher to be ready"); | 103 schedule(() => watcher.ready, "wait for watcher to be ready"); |
| 108 } | 104 } |
| 109 | 105 |
| 110 return watcher; | 106 return watcher; |
| 111 } | 107 } |
| 112 | 108 |
| 113 /// The stream of events from the watcher started with [startWatcher]. | 109 /// The stream of events from the watcher started with [startWatcher]. |
| 114 Stream _watcherEvents; | 110 ScheduledStream<WatchEvent> _watcherEvents; |
| 115 | 111 |
| 116 /// Creates a new [DirectoryWatcher] that watches a temporary directory and | 112 /// Creates a new [DirectoryWatcher] that watches a temporary directory and |
| 117 /// starts monitoring it for events. | 113 /// starts monitoring it for events. |
| 118 /// | 114 /// |
| 119 /// If [dir] is provided, watches a subdirectory in the sandbox with that name. | 115 /// If [dir] is provided, watches a subdirectory in the sandbox with that name. |
| 120 void startWatcher({String dir}) { | 116 void startWatcher({String dir}) { |
| 121 var testCase = currentTestCase.description; | 117 var testCase = currentTestCase.description; |
| 122 if (MacOSDirectoryWatcher.logDebugInfo) { | 118 if (MacOSDirectoryWatcher.logDebugInfo) { |
| 123 print("starting watcher for $testCase"); | 119 print("starting watcher for $testCase"); |
| 124 } | 120 } |
| 125 | 121 |
| 126 // We want to wait until we're ready *after* we subscribe to the watcher's | 122 // We want to wait until we're ready *after* we subscribe to the watcher's |
| 127 // events. | 123 // events. |
| 128 _watcher = createWatcher(dir: dir, waitForReady: false); | 124 _watcher = createWatcher(dir: dir, waitForReady: false); |
| 129 | 125 |
| 130 // Schedule [_watcher.events.listen] so that the watcher doesn't start | 126 // Schedule [_watcher.events.listen] so that the watcher doesn't start |
| 131 // watching [dir] before it exists. Expose [_watcherEvents] immediately so | 127 // watching [dir] before it exists. Expose [_watcherEvents] immediately so |
| 132 // that it can be accessed synchronously after this. | 128 // that it can be accessed synchronously after this. |
| 133 _watcherEvents = futureStream(schedule(() { | 129 _watcherEvents = new ScheduledStream(futureStream(schedule(() { |
| 134 var allEvents = new Queue(); | |
| 135 var subscription = _watcher.events.listen(allEvents.add, | |
| 136 onError: currentSchedule.signalError); | |
| 137 | |
| 138 currentSchedule.onComplete.schedule(() { | 130 currentSchedule.onComplete.schedule(() { |
| 139 if (MacOSDirectoryWatcher.logDebugInfo) { | 131 if (MacOSDirectoryWatcher.logDebugInfo) { |
| 140 print("stopping watcher for $testCase"); | 132 print("stopping watcher for $testCase"); |
| 141 } | 133 } |
| 142 | 134 |
| 143 var numEvents = _nextEvent; | |
| 144 subscription.cancel(); | |
| 145 _nextEvent = 0; | |
| 146 _watcher = null; | 135 _watcher = null; |
| 136 if (!_closePending) _watcherEvents.close(); |
| 147 | 137 |
| 148 // If there are already errors, don't add this to the output and make | 138 // If there are already errors, don't add this to the output and make |
| 149 // people think it might be the root cause. | 139 // people think it might be the root cause. |
| 150 if (currentSchedule.errors.isEmpty) { | 140 if (currentSchedule.errors.isEmpty) { |
| 151 expect(allEvents, hasLength(numEvents)); | 141 _watcherEvents.expect(isDone); |
| 152 } else { | |
| 153 currentSchedule.addDebugInfo("Events fired:\n${allEvents.join('\n')}"); | |
| 154 } | 142 } |
| 155 }, "reset watcher"); | 143 }, "reset watcher"); |
| 156 | 144 |
| 157 return _watcher.events; | 145 return _watcher.events; |
| 158 }, "create watcher"), broadcast: true); | 146 }, "create watcher"), broadcast: true)); |
| 159 | 147 |
| 160 schedule(() => _watcher.ready, "wait for watcher to be ready"); | 148 schedule(() => _watcher.ready, "wait for watcher to be ready"); |
| 161 } | 149 } |
| 162 | 150 |
| 163 /// A future set by [inAnyOrder] that will complete to the set of events that | 151 /// Whether an event to close [_watcherEvents] has been scheduled. |
| 164 /// occur in the [inAnyOrder] block. | 152 bool _closePending = false; |
| 165 Future<Set<WatchEvent>> _unorderedEventFuture; | |
| 166 | 153 |
| 167 /// Runs [block] and allows multiple [expectEvent] calls in that block to match | 154 /// Schedule closing the directory watcher stream after the event queue has been |
| 168 /// events in any order. | 155 /// pumped. |
| 169 void inAnyOrder(block()) { | 156 /// |
| 170 var oldFuture = _unorderedEventFuture; | 157 /// This is necessary when events are allowed to occur, but don't have to occur, |
| 158 /// at the end of a test. Otherwise, if they don't occur, the test will wait |
| 159 /// indefinitely because they might in the future and because the watcher is |
| 160 /// normally only closed after the test completes. |
| 161 void startClosingEventStream() { |
| 162 schedule(() { |
| 163 _closePending = true; |
| 164 pumpEventQueue().then((_) => _watcherEvents.close()).whenComplete(() { |
| 165 _closePending = false; |
| 166 }); |
| 167 }, 'start closing event stream'); |
| 168 } |
| 169 |
| 170 /// A list of [StreamMatcher]s that have been collected using |
| 171 /// [_collectStreamMatcher]. |
| 172 List<StreamMatcher> _collectedStreamMatchers; |
| 173 |
| 174 /// Collects all stream matchers that are registered within [block] into a |
| 175 /// single stream matcher. |
| 176 /// |
| 177 /// The returned matcher will match each of the collected matchers in order. |
| 178 StreamMatcher _collectStreamMatcher(block()) { |
| 179 var oldStreamMatchers = _collectedStreamMatchers; |
| 180 _collectedStreamMatchers = new List<StreamMatcher>(); |
| 171 try { | 181 try { |
| 172 var firstEvent = _nextEvent; | |
| 173 var completer = new Completer(); | |
| 174 _unorderedEventFuture = completer.future; | |
| 175 block(); | 182 block(); |
| 176 | 183 return inOrder(_collectedStreamMatchers); |
| 177 _watcherEvents.skip(firstEvent).take(_nextEvent - firstEvent).toSet() | |
| 178 .then(completer.complete, onError: completer.completeError); | |
| 179 currentSchedule.wrapFuture(_unorderedEventFuture, | |
| 180 "waiting for ${_nextEvent - firstEvent} events"); | |
| 181 } finally { | 184 } finally { |
| 182 _unorderedEventFuture = oldFuture; | 185 _collectedStreamMatchers = oldStreamMatchers; |
| 183 } | 186 } |
| 184 } | 187 } |
| 185 | 188 |
| 186 /// Expects that the next set of event will be a change of [type] on [path]. | 189 /// Either add [streamMatcher] as an expectation to [_watcherEvents], or collect |
| 190 /// it with [_collectStreamMatcher]. |
| 187 /// | 191 /// |
| 188 /// Multiple calls to [expectEvent] require that the events are received in that | 192 /// [streamMatcher] can be a [StreamMatcher], a [Matcher], or a value. |
| 189 /// order unless they're called in an [inAnyOrder] block. | 193 void _expectOrCollect(streamMatcher) { |
| 190 void expectEvent(ChangeType type, String path) { | 194 if (_collectedStreamMatchers != null) { |
| 191 if (_unorderedEventFuture != null) { | 195 _collectedStreamMatchers.add(new StreamMatcher.wrap(streamMatcher)); |
| 192 // Assign this to a local variable since it will be un-assigned by the time | 196 } else { |
| 193 // the scheduled callback runs. | 197 _watcherEvents.expect(streamMatcher); |
| 194 var future = _unorderedEventFuture; | 198 } |
| 199 } |
| 195 | 200 |
| 196 expect( | 201 /// Expects that [matchers] will match emitted events in any order. |
| 197 schedule(() => future, "should fire $type event on $path"), | 202 /// |
| 198 completion(contains(isWatchEvent(type, path)))); | 203 /// [matchers] may be [Matcher]s or values, but not [StreamMatcher]s. |
| 199 } else { | 204 void inAnyOrder(Iterable matchers) { |
| 200 var future = currentSchedule.wrapFuture( | 205 matchers = matchers.toSet(); |
| 201 _watcherEvents.elementAt(_nextEvent), | 206 _expectOrCollect(nextValues(matchers.length, unorderedMatches(matchers))); |
| 202 "waiting for $type event on $path"); | 207 } |
| 203 | 208 |
| 204 expect( | 209 /// Expects that the expectations established in either [block1] or [block2] |
| 205 schedule(() => future, "should fire $type event on $path"), | 210 /// will match the emitted events. |
| 206 completion(isWatchEvent(type, path))); | 211 /// |
| 207 } | 212 /// If both blocks match, the one that consumed more events will be used. |
| 208 _nextEvent++; | 213 void allowEither(block1(), block2()) { |
| 214 _expectOrCollect(either( |
| 215 _collectStreamMatcher(block1), _collectStreamMatcher(block2))); |
| 216 } |
| 217 |
| 218 /// Allows the expectations established in [block] to match the emitted events. |
| 219 /// |
| 220 /// If the expectations in [block] don't match, no error will be raised and no |
| 221 /// events will be consumed. If this is used at the end of a test, |
| 222 /// [startClosingEventStream] should be called before it. |
| 223 void allowEvents(block()) { |
| 224 _expectOrCollect(allow(_collectStreamMatcher(block))); |
| 209 } | 225 } |
| 210 | 226 |
| 211 /// Returns a matcher that matches a [WatchEvent] with the given [type] and | 227 /// Returns a matcher that matches a [WatchEvent] with the given [type] and |
| 212 /// [path]. | 228 /// [path]. |
| 213 Matcher isWatchEvent(ChangeType type, String path) { | 229 Matcher isWatchEvent(ChangeType type, String path) { |
| 214 return predicate((e) { | 230 return predicate((e) { |
| 215 return e is WatchEvent && e.type == type && | 231 return e is WatchEvent && e.type == type && |
| 216 e.path == p.join(_sandboxDir, p.normalize(path)); | 232 e.path == p.join(_sandboxDir, p.normalize(path)); |
| 217 }, "is $type $path"); | 233 }, "is $type $path"); |
| 218 } | 234 } |
| 219 | 235 |
| 220 void expectAddEvent(String path) => expectEvent(ChangeType.ADD, path); | 236 /// Returns a [Matcher] that matches a [WatchEvent] for an add event for [path]. |
| 221 void expectModifyEvent(String path) => expectEvent(ChangeType.MODIFY, path); | 237 Matcher isAddEvent(String path) => isWatchEvent(ChangeType.ADD, path); |
| 222 void expectRemoveEvent(String path) => expectEvent(ChangeType.REMOVE, path); | 238 |
| 239 /// Returns a [Matcher] that matches a [WatchEvent] for a modification event for |
| 240 /// [path]. |
| 241 Matcher isModifyEvent(String path) => isWatchEvent(ChangeType.MODIFY, path); |
| 242 |
| 243 /// Returns a [Matcher] that matches a [WatchEvent] for a removal event for |
| 244 /// [path]. |
| 245 Matcher isRemoveEvent(String path) => isWatchEvent(ChangeType.REMOVE, path); |
| 246 |
| 247 /// Expects that the next event emitted will be for an add event for [path]. |
| 248 void expectAddEvent(String path) => |
| 249 _expectOrCollect(isWatchEvent(ChangeType.ADD, path)); |
| 250 |
| 251 /// Expects that the next event emitted will be for a modification event for |
| 252 /// [path]. |
| 253 void expectModifyEvent(String path) => |
| 254 _expectOrCollect(isWatchEvent(ChangeType.MODIFY, path)); |
| 255 |
| 256 /// Expects that the next event emitted will be for a removal event for [path]. |
| 257 void expectRemoveEvent(String path) => |
| 258 _expectOrCollect(isWatchEvent(ChangeType.REMOVE, path)); |
| 259 |
| 260 /// Consumes an add event for [path] if one is emitted at this point in the |
| 261 /// schedule, but doesn't throw an error if it isn't. |
| 262 /// |
| 263 /// If this is used at the end of a test, [startClosingEventStream] should be |
| 264 /// called before it. |
| 265 void allowAddEvent(String path) => |
| 266 _expectOrCollect(allow(isWatchEvent(ChangeType.ADD, path))); |
| 267 |
| 268 /// Consumes a modification event for [path] if one is emitted at this point in |
| 269 /// the schedule, but doesn't throw an error if it isn't. |
| 270 /// |
| 271 /// If this is used at the end of a test, [startClosingEventStream] should be |
| 272 /// called before it. |
| 273 void allowModifyEvent(String path) => |
| 274 _expectOrCollect(allow(isWatchEvent(ChangeType.MODIFY, path))); |
| 275 |
| 276 /// Consumes a removal event for [path] if one is emitted at this point in the |
| 277 /// schedule, but doesn't throw an error if it isn't. |
| 278 /// |
| 279 /// If this is used at the end of a test, [startClosingEventStream] should be |
| 280 /// called before it. |
| 281 void allowRemoveEvent(String path) => |
| 282 _expectOrCollect(allow(isWatchEvent(ChangeType.REMOVE, path))); |
| 223 | 283 |
| 224 /// Schedules writing a file in the sandbox at [path] with [contents]. | 284 /// Schedules writing a file in the sandbox at [path] with [contents]. |
| 225 /// | 285 /// |
| 226 /// If [contents] is omitted, creates an empty file. If [updatedModified] is | 286 /// If [contents] is omitted, creates an empty file. If [updatedModified] is |
| 227 /// `false`, the mock file modification time is not changed. | 287 /// `false`, the mock file modification time is not changed. |
| 228 void writeFile(String path, {String contents, bool updateModified}) { | 288 void writeFile(String path, {String contents, bool updateModified}) { |
| 229 if (contents == null) contents = ""; | 289 if (contents == null) contents = ""; |
| 230 if (updateModified == null) updateModified = true; | 290 if (updateModified == null) updateModified = true; |
| 231 | 291 |
| 232 schedule(() { | 292 schedule(() { |
| (...skipping 59 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 292 /// Schedules deleting a directory in the sandbox at [path]. | 352 /// Schedules deleting a directory in the sandbox at [path]. |
| 293 void deleteDir(String path) { | 353 void deleteDir(String path) { |
| 294 schedule(() { | 354 schedule(() { |
| 295 new Directory(p.join(_sandboxDir, path)).deleteSync(recursive: true); | 355 new Directory(p.join(_sandboxDir, path)).deleteSync(recursive: true); |
| 296 }, "delete directory $path"); | 356 }, "delete directory $path"); |
| 297 } | 357 } |
| 298 | 358 |
| 299 /// Runs [callback] with every permutation of non-negative [i], [j], and [k] | 359 /// Runs [callback] with every permutation of non-negative [i], [j], and [k] |
| 300 /// less than [limit]. | 360 /// less than [limit]. |
| 301 /// | 361 /// |
| 362 /// Returns a set of all values returns by [callback]. |
| 363 /// |
| 302 /// [limit] defaults to 3. | 364 /// [limit] defaults to 3. |
| 303 void withPermutations(callback(int i, int j, int k), {int limit}) { | 365 Set withPermutations(callback(int i, int j, int k), {int limit}) { |
| 304 if (limit == null) limit = 3; | 366 if (limit == null) limit = 3; |
| 367 var results = new Set(); |
| 305 for (var i = 0; i < limit; i++) { | 368 for (var i = 0; i < limit; i++) { |
| 306 for (var j = 0; j < limit; j++) { | 369 for (var j = 0; j < limit; j++) { |
| 307 for (var k = 0; k < limit; k++) { | 370 for (var k = 0; k < limit; k++) { |
| 308 callback(i, j, k); | 371 results.add(callback(i, j, k)); |
| 309 } | 372 } |
| 310 } | 373 } |
| 311 } | 374 } |
| 375 return results; |
| 312 } | 376 } |
| OLD | NEW |