| // 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:async'; |
| 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'; |
| 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 [DirectoryWatcher] being used for the current scheduled test. |
| DirectoryWatcher _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 DirectoryWatcher WatcherFactory(String directory); |
| |
| /// Sets the function used to create the directory watcher. |
| set watcherFactory(WatcherFactory factory) { |
| _watcherFactory = factory; |
| } |
| WatcherFactory _watcherFactory; |
| |
| void initConfig() { |
| useCompactVMConfiguration(); |
| filterStacks = true; |
| } |
| |
| /// 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) { |
| new Directory(_sandboxDir).deleteSync(recursive: true); |
| _sandboxDir = null; |
| } |
| |
| _mockFileModificationTimes = null; |
| mockGetModificationTime(null); |
| }, "delete sandbox"); |
| } |
| |
| /// Creates a new [DirectoryWatcher] that watches a temporary 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 [dir] is provided, watches a subdirectory in the sandbox with that name. |
| DirectoryWatcher createWatcher({String dir, bool waitForReady}) { |
| if (dir == null) { |
| dir = _sandboxDir; |
| } else { |
| dir = p.join(_sandboxDir, dir); |
| } |
| |
| var watcher = _watcherFactory(dir); |
| |
| // 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 [DirectoryWatcher] that watches a temporary directory and |
| /// starts monitoring it for events. |
| /// |
| /// If [dir] is provided, watches a subdirectory in the sandbox with that name. |
| void startWatcher({String dir}) { |
| var testCase = currentTestCase.description; |
| if (MacOSDirectoryWatcher.logDebugInfo) { |
| print("starting watcher for $testCase (${new DateTime.now()})"); |
| } |
| |
| // We want to wait until we're ready *after* we subscribe to the watcher's |
| // events. |
| _watcher = createWatcher(dir: dir, waitForReady: false); |
| |
| // 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 = new ScheduledStream(futureStream(schedule(() { |
| currentSchedule.onComplete.schedule(() { |
| if (MacOSDirectoryWatcher.logDebugInfo) { |
| print("stopping watcher for $testCase (${new DateTime.now()})"); |
| } |
| |
| _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 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 { |
| 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); |
| } |
| |
| if (MacOSDirectoryWatcher.logDebugInfo) { |
| print("[test] writing file $path"); |
| } |
| 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(() { |
| if (MacOSDirectoryWatcher.logDebugInfo) { |
| print("[test] deleting file $path"); |
| } |
| 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(() { |
| if (MacOSDirectoryWatcher.logDebugInfo) { |
| print("[test] renaming file $from to $to"); |
| } |
| |
| 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(() { |
| if (MacOSDirectoryWatcher.logDebugInfo) { |
| print("[test] creating directory $path"); |
| } |
| 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(() { |
| if (MacOSDirectoryWatcher.logDebugInfo) { |
| print("[test] renaming directory $from to $to"); |
| } |
| 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(() { |
| if (MacOSDirectoryWatcher.logDebugInfo) { |
| print("[test] deleting directory $path"); |
| } |
| 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; |
| } |