| OLD | NEW |
| (Empty) |
| 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 | |
| 3 // BSD-style license that can be found in the LICENSE file. | |
| 4 | |
| 5 library watcher.test.utils; | |
| 6 | |
| 7 import 'dart:io'; | |
| 8 | |
| 9 import 'package:path/path.dart' as p; | |
| 10 import 'package:scheduled_test/scheduled_stream.dart'; | |
| 11 import 'package:scheduled_test/scheduled_test.dart'; | |
| 12 import 'package:unittest/compact_vm_config.dart'; | |
| 13 import 'package:watcher/watcher.dart'; | |
| 14 import 'package:watcher/src/stat.dart'; | |
| 15 import 'package:watcher/src/utils.dart'; | |
| 16 | |
| 17 // TODO(nweiz): remove this when issue 15042 is fixed. | |
| 18 import 'package:watcher/src/directory_watcher/mac_os.dart'; | |
| 19 | |
| 20 /// The path to the temporary sandbox created for each test. All file | |
| 21 /// operations are implicitly relative to this directory. | |
| 22 String _sandboxDir; | |
| 23 | |
| 24 /// The [DirectoryWatcher] being used for the current scheduled test. | |
| 25 DirectoryWatcher _watcher; | |
| 26 | |
| 27 /// The mock modification times (in milliseconds since epoch) for each file. | |
| 28 /// | |
| 29 /// The actual file system has pretty coarse granularity for file modification | |
| 30 /// times. This means using the real file system requires us to put delays in | |
| 31 /// the tests to ensure we wait long enough between operations for the mod time | |
| 32 /// to be different. | |
| 33 /// | |
| 34 /// Instead, we'll just mock that out. Each time a file is written, we manually | |
| 35 /// increment the mod time for that file instantly. | |
| 36 Map<String, int> _mockFileModificationTimes; | |
| 37 | |
| 38 typedef DirectoryWatcher WatcherFactory(String directory); | |
| 39 | |
| 40 /// Sets the function used to create the directory watcher. | |
| 41 set watcherFactory(WatcherFactory factory) { | |
| 42 _watcherFactory = factory; | |
| 43 } | |
| 44 WatcherFactory _watcherFactory; | |
| 45 | |
| 46 void initConfig() { | |
| 47 useCompactVMConfiguration(); | |
| 48 filterStacks = true; | |
| 49 } | |
| 50 | |
| 51 /// Creates the sandbox directory the other functions in this library use and | |
| 52 /// ensures it's deleted when the test ends. | |
| 53 /// | |
| 54 /// This should usually be called by [setUp]. | |
| 55 void createSandbox() { | |
| 56 var dir = Directory.systemTemp.createTempSync('watcher_test_'); | |
| 57 _sandboxDir = dir.path; | |
| 58 | |
| 59 _mockFileModificationTimes = new Map<String, int>(); | |
| 60 mockGetModificationTime((path) { | |
| 61 path = p.normalize(p.relative(path, from: _sandboxDir)); | |
| 62 | |
| 63 // Make sure we got a path in the sandbox. | |
| 64 assert(p.isRelative(path) && !path.startsWith("..")); | |
| 65 | |
| 66 var mtime = _mockFileModificationTimes[path]; | |
| 67 return new DateTime.fromMillisecondsSinceEpoch(mtime == null ? 0 : mtime); | |
| 68 }); | |
| 69 | |
| 70 // Delete the sandbox when done. | |
| 71 currentSchedule.onComplete.schedule(() { | |
| 72 if (_sandboxDir != null) { | |
| 73 // TODO(rnystrom): Issue 19155. The watcher should already be closed when | |
| 74 // we clean up the sandbox. | |
| 75 if (_watcherEvents != null) { | |
| 76 _watcherEvents.close(); | |
| 77 } | |
| 78 new Directory(_sandboxDir).deleteSync(recursive: true); | |
| 79 _sandboxDir = null; | |
| 80 } | |
| 81 | |
| 82 _mockFileModificationTimes = null; | |
| 83 mockGetModificationTime(null); | |
| 84 }, "delete sandbox"); | |
| 85 } | |
| 86 | |
| 87 /// Creates a new [DirectoryWatcher] that watches a temporary directory. | |
| 88 /// | |
| 89 /// Normally, this will pause the schedule until the watcher is done scanning | |
| 90 /// and is polling for changes. If you pass `false` for [waitForReady], it will | |
| 91 /// not schedule this delay. | |
| 92 /// | |
| 93 /// If [dir] is provided, watches a subdirectory in the sandbox with that name. | |
| 94 DirectoryWatcher createWatcher({String dir, bool waitForReady}) { | |
| 95 if (dir == null) { | |
| 96 dir = _sandboxDir; | |
| 97 } else { | |
| 98 dir = p.join(_sandboxDir, dir); | |
| 99 } | |
| 100 | |
| 101 var watcher = _watcherFactory(dir); | |
| 102 | |
| 103 // Wait until the scan is finished so that we don't miss changes to files | |
| 104 // that could occur before the scan completes. | |
| 105 if (waitForReady != false) { | |
| 106 schedule(() => watcher.ready, "wait for watcher to be ready"); | |
| 107 } | |
| 108 | |
| 109 return watcher; | |
| 110 } | |
| 111 | |
| 112 /// The stream of events from the watcher started with [startWatcher]. | |
| 113 ScheduledStream<WatchEvent> _watcherEvents; | |
| 114 | |
| 115 /// Creates a new [DirectoryWatcher] that watches a temporary directory and | |
| 116 /// starts monitoring it for events. | |
| 117 /// | |
| 118 /// If [dir] is provided, watches a subdirectory in the sandbox with that name. | |
| 119 void startWatcher({String dir}) { | |
| 120 var testCase = currentTestCase.description; | |
| 121 if (MacOSDirectoryWatcher.logDebugInfo) { | |
| 122 print("starting watcher for $testCase (${new DateTime.now()})"); | |
| 123 } | |
| 124 | |
| 125 // We want to wait until we're ready *after* we subscribe to the watcher's | |
| 126 // events. | |
| 127 _watcher = createWatcher(dir: dir, waitForReady: false); | |
| 128 | |
| 129 // Schedule [_watcher.events.listen] so that the watcher doesn't start | |
| 130 // watching [dir] before it exists. Expose [_watcherEvents] immediately so | |
| 131 // that it can be accessed synchronously after this. | |
| 132 _watcherEvents = new ScheduledStream(futureStream(schedule(() { | |
| 133 currentSchedule.onComplete.schedule(() { | |
| 134 if (MacOSDirectoryWatcher.logDebugInfo) { | |
| 135 print("stopping watcher for $testCase (${new DateTime.now()})"); | |
| 136 } | |
| 137 | |
| 138 _watcher = null; | |
| 139 if (!_closePending) _watcherEvents.close(); | |
| 140 | |
| 141 // If there are already errors, don't add this to the output and make | |
| 142 // people think it might be the root cause. | |
| 143 if (currentSchedule.errors.isEmpty) { | |
| 144 _watcherEvents.expect(isDone); | |
| 145 } | |
| 146 }, "reset watcher"); | |
| 147 | |
| 148 return _watcher.events; | |
| 149 }, "create watcher"), broadcast: true)); | |
| 150 | |
| 151 schedule(() => _watcher.ready, "wait for watcher to be ready"); | |
| 152 } | |
| 153 | |
| 154 /// Whether an event to close [_watcherEvents] has been scheduled. | |
| 155 bool _closePending = false; | |
| 156 | |
| 157 /// Schedule closing the directory watcher stream after the event queue has been | |
| 158 /// pumped. | |
| 159 /// | |
| 160 /// This is necessary when events are allowed to occur, but don't have to occur, | |
| 161 /// at the end of a test. Otherwise, if they don't occur, the test will wait | |
| 162 /// indefinitely because they might in the future and because the watcher is | |
| 163 /// normally only closed after the test completes. | |
| 164 void startClosingEventStream() { | |
| 165 schedule(() { | |
| 166 _closePending = true; | |
| 167 pumpEventQueue().then((_) => _watcherEvents.close()).whenComplete(() { | |
| 168 _closePending = false; | |
| 169 }); | |
| 170 }, 'start closing event stream'); | |
| 171 } | |
| 172 | |
| 173 /// A list of [StreamMatcher]s that have been collected using | |
| 174 /// [_collectStreamMatcher]. | |
| 175 List<StreamMatcher> _collectedStreamMatchers; | |
| 176 | |
| 177 /// Collects all stream matchers that are registered within [block] into a | |
| 178 /// single stream matcher. | |
| 179 /// | |
| 180 /// The returned matcher will match each of the collected matchers in order. | |
| 181 StreamMatcher _collectStreamMatcher(block()) { | |
| 182 var oldStreamMatchers = _collectedStreamMatchers; | |
| 183 _collectedStreamMatchers = new List<StreamMatcher>(); | |
| 184 try { | |
| 185 block(); | |
| 186 return inOrder(_collectedStreamMatchers); | |
| 187 } finally { | |
| 188 _collectedStreamMatchers = oldStreamMatchers; | |
| 189 } | |
| 190 } | |
| 191 | |
| 192 /// Either add [streamMatcher] as an expectation to [_watcherEvents], or collect | |
| 193 /// it with [_collectStreamMatcher]. | |
| 194 /// | |
| 195 /// [streamMatcher] can be a [StreamMatcher], a [Matcher], or a value. | |
| 196 void _expectOrCollect(streamMatcher) { | |
| 197 if (_collectedStreamMatchers != null) { | |
| 198 _collectedStreamMatchers.add(new StreamMatcher.wrap(streamMatcher)); | |
| 199 } else { | |
| 200 _watcherEvents.expect(streamMatcher); | |
| 201 } | |
| 202 } | |
| 203 | |
| 204 /// Expects that [matchers] will match emitted events in any order. | |
| 205 /// | |
| 206 /// [matchers] may be [Matcher]s or values, but not [StreamMatcher]s. | |
| 207 void inAnyOrder(Iterable matchers) { | |
| 208 matchers = matchers.toSet(); | |
| 209 _expectOrCollect(nextValues(matchers.length, unorderedMatches(matchers))); | |
| 210 } | |
| 211 | |
| 212 /// Expects that the expectations established in either [block1] or [block2] | |
| 213 /// will match the emitted events. | |
| 214 /// | |
| 215 /// If both blocks match, the one that consumed more events will be used. | |
| 216 void allowEither(block1(), block2()) { | |
| 217 _expectOrCollect(either( | |
| 218 _collectStreamMatcher(block1), _collectStreamMatcher(block2))); | |
| 219 } | |
| 220 | |
| 221 /// Allows the expectations established in [block] to match the emitted events. | |
| 222 /// | |
| 223 /// If the expectations in [block] don't match, no error will be raised and no | |
| 224 /// events will be consumed. If this is used at the end of a test, | |
| 225 /// [startClosingEventStream] should be called before it. | |
| 226 void allowEvents(block()) { | |
| 227 _expectOrCollect(allow(_collectStreamMatcher(block))); | |
| 228 } | |
| 229 | |
| 230 /// Returns a matcher that matches a [WatchEvent] with the given [type] and | |
| 231 /// [path]. | |
| 232 Matcher isWatchEvent(ChangeType type, String path) { | |
| 233 return predicate((e) { | |
| 234 return e is WatchEvent && e.type == type && | |
| 235 e.path == p.join(_sandboxDir, p.normalize(path)); | |
| 236 }, "is $type $path"); | |
| 237 } | |
| 238 | |
| 239 /// Returns a [Matcher] that matches a [WatchEvent] for an add event for [path]. | |
| 240 Matcher isAddEvent(String path) => isWatchEvent(ChangeType.ADD, path); | |
| 241 | |
| 242 /// Returns a [Matcher] that matches a [WatchEvent] for a modification event for | |
| 243 /// [path]. | |
| 244 Matcher isModifyEvent(String path) => isWatchEvent(ChangeType.MODIFY, path); | |
| 245 | |
| 246 /// Returns a [Matcher] that matches a [WatchEvent] for a removal event for | |
| 247 /// [path]. | |
| 248 Matcher isRemoveEvent(String path) => isWatchEvent(ChangeType.REMOVE, path); | |
| 249 | |
| 250 /// Expects that the next event emitted will be for an add event for [path]. | |
| 251 void expectAddEvent(String path) => | |
| 252 _expectOrCollect(isWatchEvent(ChangeType.ADD, path)); | |
| 253 | |
| 254 /// Expects that the next event emitted will be for a modification event for | |
| 255 /// [path]. | |
| 256 void expectModifyEvent(String path) => | |
| 257 _expectOrCollect(isWatchEvent(ChangeType.MODIFY, path)); | |
| 258 | |
| 259 /// Expects that the next event emitted will be for a removal event for [path]. | |
| 260 void expectRemoveEvent(String path) => | |
| 261 _expectOrCollect(isWatchEvent(ChangeType.REMOVE, path)); | |
| 262 | |
| 263 /// Consumes an add event for [path] if one is emitted at this point in the | |
| 264 /// schedule, but doesn't throw an error if it isn't. | |
| 265 /// | |
| 266 /// If this is used at the end of a test, [startClosingEventStream] should be | |
| 267 /// called before it. | |
| 268 void allowAddEvent(String path) => | |
| 269 _expectOrCollect(allow(isWatchEvent(ChangeType.ADD, path))); | |
| 270 | |
| 271 /// Consumes a modification event for [path] if one is emitted at this point in | |
| 272 /// the schedule, but doesn't throw an error if it isn't. | |
| 273 /// | |
| 274 /// If this is used at the end of a test, [startClosingEventStream] should be | |
| 275 /// called before it. | |
| 276 void allowModifyEvent(String path) => | |
| 277 _expectOrCollect(allow(isWatchEvent(ChangeType.MODIFY, path))); | |
| 278 | |
| 279 /// Consumes a removal event for [path] if one is emitted at this point in the | |
| 280 /// schedule, but doesn't throw an error if it isn't. | |
| 281 /// | |
| 282 /// If this is used at the end of a test, [startClosingEventStream] should be | |
| 283 /// called before it. | |
| 284 void allowRemoveEvent(String path) => | |
| 285 _expectOrCollect(allow(isWatchEvent(ChangeType.REMOVE, path))); | |
| 286 | |
| 287 /// Schedules writing a file in the sandbox at [path] with [contents]. | |
| 288 /// | |
| 289 /// If [contents] is omitted, creates an empty file. If [updatedModified] is | |
| 290 /// `false`, the mock file modification time is not changed. | |
| 291 void writeFile(String path, {String contents, bool updateModified}) { | |
| 292 if (contents == null) contents = ""; | |
| 293 if (updateModified == null) updateModified = true; | |
| 294 | |
| 295 schedule(() { | |
| 296 var fullPath = p.join(_sandboxDir, path); | |
| 297 | |
| 298 // Create any needed subdirectories. | |
| 299 var dir = new Directory(p.dirname(fullPath)); | |
| 300 if (!dir.existsSync()) { | |
| 301 dir.createSync(recursive: true); | |
| 302 } | |
| 303 | |
| 304 if (MacOSDirectoryWatcher.logDebugInfo) { | |
| 305 print("[test] writing file $path"); | |
| 306 } | |
| 307 new File(fullPath).writeAsStringSync(contents); | |
| 308 | |
| 309 // Manually update the mock modification time for the file. | |
| 310 if (updateModified) { | |
| 311 // Make sure we always use the same separator on Windows. | |
| 312 path = p.normalize(path); | |
| 313 | |
| 314 var milliseconds = _mockFileModificationTimes.putIfAbsent(path, () => 0); | |
| 315 _mockFileModificationTimes[path]++; | |
| 316 } | |
| 317 }, "write file $path"); | |
| 318 } | |
| 319 | |
| 320 /// Schedules deleting a file in the sandbox at [path]. | |
| 321 void deleteFile(String path) { | |
| 322 schedule(() { | |
| 323 if (MacOSDirectoryWatcher.logDebugInfo) { | |
| 324 print("[test] deleting file $path"); | |
| 325 } | |
| 326 new File(p.join(_sandboxDir, path)).deleteSync(); | |
| 327 }, "delete file $path"); | |
| 328 } | |
| 329 | |
| 330 /// Schedules renaming a file in the sandbox from [from] to [to]. | |
| 331 /// | |
| 332 /// If [contents] is omitted, creates an empty file. | |
| 333 void renameFile(String from, String to) { | |
| 334 schedule(() { | |
| 335 if (MacOSDirectoryWatcher.logDebugInfo) { | |
| 336 print("[test] renaming file $from to $to"); | |
| 337 } | |
| 338 | |
| 339 new File(p.join(_sandboxDir, from)).renameSync(p.join(_sandboxDir, to)); | |
| 340 | |
| 341 // Make sure we always use the same separator on Windows. | |
| 342 to = p.normalize(to); | |
| 343 | |
| 344 // Manually update the mock modification time for the file. | |
| 345 var milliseconds = _mockFileModificationTimes.putIfAbsent(to, () => 0); | |
| 346 _mockFileModificationTimes[to]++; | |
| 347 }, "rename file $from to $to"); | |
| 348 } | |
| 349 | |
| 350 /// Schedules creating a directory in the sandbox at [path]. | |
| 351 void createDir(String path) { | |
| 352 schedule(() { | |
| 353 if (MacOSDirectoryWatcher.logDebugInfo) { | |
| 354 print("[test] creating directory $path"); | |
| 355 } | |
| 356 new Directory(p.join(_sandboxDir, path)).createSync(); | |
| 357 }, "create directory $path"); | |
| 358 } | |
| 359 | |
| 360 /// Schedules renaming a directory in the sandbox from [from] to [to]. | |
| 361 void renameDir(String from, String to) { | |
| 362 schedule(() { | |
| 363 if (MacOSDirectoryWatcher.logDebugInfo) { | |
| 364 print("[test] renaming directory $from to $to"); | |
| 365 } | |
| 366 new Directory(p.join(_sandboxDir, from)) | |
| 367 .renameSync(p.join(_sandboxDir, to)); | |
| 368 }, "rename directory $from to $to"); | |
| 369 } | |
| 370 | |
| 371 /// Schedules deleting a directory in the sandbox at [path]. | |
| 372 void deleteDir(String path) { | |
| 373 schedule(() { | |
| 374 if (MacOSDirectoryWatcher.logDebugInfo) { | |
| 375 print("[test] deleting directory $path"); | |
| 376 } | |
| 377 new Directory(p.join(_sandboxDir, path)).deleteSync(recursive: true); | |
| 378 }, "delete directory $path"); | |
| 379 } | |
| 380 | |
| 381 /// Runs [callback] with every permutation of non-negative [i], [j], and [k] | |
| 382 /// less than [limit]. | |
| 383 /// | |
| 384 /// Returns a set of all values returns by [callback]. | |
| 385 /// | |
| 386 /// [limit] defaults to 3. | |
| 387 Set withPermutations(callback(int i, int j, int k), {int limit}) { | |
| 388 if (limit == null) limit = 3; | |
| 389 var results = new Set(); | |
| 390 for (var i = 0; i < limit; i++) { | |
| 391 for (var j = 0; j < limit; j++) { | |
| 392 for (var k = 0; k < limit; k++) { | |
| 393 results.add(callback(i, j, k)); | |
| 394 } | |
| 395 } | |
| 396 } | |
| 397 return results; | |
| 398 } | |
| OLD | NEW |