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