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:io'; | 9 import 'dart:io'; |
9 | 10 |
10 import 'package:path/path.dart' as p; | 11 import 'package:path/path.dart' as p; |
11 import 'package:scheduled_test/scheduled_test.dart'; | 12 import 'package:scheduled_test/scheduled_test.dart'; |
12 import 'package:unittest/compact_vm_config.dart'; | 13 import 'package:unittest/compact_vm_config.dart'; |
13 import 'package:watcher/watcher.dart'; | 14 import 'package:watcher/watcher.dart'; |
14 import 'package:watcher/src/stat.dart'; | 15 import 'package:watcher/src/stat.dart'; |
| 16 import 'package:watcher/src/utils.dart'; |
15 | 17 |
16 /// The path to the temporary sandbox created for each test. All file | 18 /// The path to the temporary sandbox created for each test. All file |
17 /// operations are implicitly relative to this directory. | 19 /// operations are implicitly relative to this directory. |
18 String _sandboxDir; | 20 String _sandboxDir; |
19 | 21 |
20 /// The [DirectoryWatcher] being used for the current scheduled test. | 22 /// The [DirectoryWatcher] being used for the current scheduled test. |
21 DirectoryWatcher _watcher; | 23 DirectoryWatcher _watcher; |
22 | 24 |
23 /// The index in [_watcher]'s event stream for the next event. When event | 25 /// The index in [_watcher]'s event stream for the next event. When event |
24 /// expectations are set using [expectEvent] (et. al.), they use this to | 26 /// expectations are set using [expectEvent] (et. al.), they use this to |
25 /// expect a series of events in order. | 27 /// expect a series of events in order. |
26 var _nextEvent = 0; | 28 var _nextEvent = 0; |
27 | 29 |
28 /// The mock modification times (in milliseconds since epoch) for each file. | 30 /// The mock modification times (in milliseconds since epoch) for each file. |
29 /// | 31 /// |
30 /// The actual file system has pretty coarse granularity for file modification | 32 /// The actual file system has pretty coarse granularity for file modification |
31 /// times. This means using the real file system requires us to put delays in | 33 /// times. This means using the real file system requires us to put delays in |
32 /// the tests to ensure we wait long enough between operations for the mod time | 34 /// the tests to ensure we wait long enough between operations for the mod time |
33 /// to be different. | 35 /// to be different. |
34 /// | 36 /// |
35 /// Instead, we'll just mock that out. Each time a file is written, we manually | 37 /// Instead, we'll just mock that out. Each time a file is written, we manually |
36 /// increment the mod time for that file instantly. | 38 /// increment the mod time for that file instantly. |
37 Map<String, int> _mockFileModificationTimes; | 39 Map<String, int> _mockFileModificationTimes; |
38 | 40 |
| 41 typedef DirectoryWatcher WatcherFactory(String directory); |
| 42 |
| 43 /// Sets the function used to create the directory watcher. |
| 44 set watcherFactory(WatcherFactory factory) { |
| 45 _watcherFactory = factory; |
| 46 } |
| 47 WatcherFactory _watcherFactory; |
| 48 |
39 void initConfig() { | 49 void initConfig() { |
40 useCompactVMConfiguration(); | 50 useCompactVMConfiguration(); |
41 filterStacks = true; | 51 filterStacks = true; |
42 } | 52 } |
43 | 53 |
44 /// Creates the sandbox directory the other functions in this library use and | 54 /// Creates the sandbox directory the other functions in this library use and |
45 /// ensures it's deleted when the test ends. | 55 /// ensures it's deleted when the test ends. |
46 /// | 56 /// |
47 /// This should usually be called by [setUp]. | 57 /// This should usually be called by [setUp]. |
48 void createSandbox() { | 58 void createSandbox() { |
49 var dir = Directory.systemTemp.createTempSync('watcher_test_'); | 59 var dir = Directory.systemTemp.createTempSync('watcher_test_'); |
50 _sandboxDir = dir.path; | 60 _sandboxDir = dir.path; |
51 | 61 |
52 _mockFileModificationTimes = new Map<String, int>(); | 62 _mockFileModificationTimes = new Map<String, int>(); |
53 mockGetModificationTime((path) { | 63 mockGetModificationTime((path) { |
54 path = p.normalize(p.relative(path, from: _sandboxDir)); | 64 path = p.normalize(p.relative(path, from: _sandboxDir)); |
55 | 65 |
56 // Make sure we got a path in the sandbox. | 66 // Make sure we got a path in the sandbox. |
57 assert(p.isRelative(path) && !path.startsWith("..")); | 67 assert(p.isRelative(path) && !path.startsWith("..")); |
58 | 68 |
59 return new DateTime.fromMillisecondsSinceEpoch( | 69 var mtime = _mockFileModificationTimes[path]; |
60 _mockFileModificationTimes[path]); | 70 return new DateTime.fromMillisecondsSinceEpoch(mtime == null ? 0 : mtime); |
61 }); | 71 }); |
62 | 72 |
63 // Delete the sandbox when done. | 73 // Delete the sandbox when done. |
64 currentSchedule.onComplete.schedule(() { | 74 currentSchedule.onComplete.schedule(() { |
65 if (_sandboxDir != null) { | 75 if (_sandboxDir != null) { |
66 new Directory(_sandboxDir).deleteSync(recursive: true); | 76 new Directory(_sandboxDir).deleteSync(recursive: true); |
67 _sandboxDir = null; | 77 _sandboxDir = null; |
68 } | 78 } |
69 | 79 |
70 _mockFileModificationTimes = null; | 80 _mockFileModificationTimes = null; |
71 mockGetModificationTime(null); | 81 mockGetModificationTime(null); |
72 }, "delete sandbox"); | 82 }, "delete sandbox"); |
73 } | 83 } |
74 | 84 |
75 /// Creates a new [DirectoryWatcher] that watches a temporary directory. | 85 /// Creates a new [DirectoryWatcher] that watches a temporary directory. |
76 /// | 86 /// |
77 /// Normally, this will pause the schedule until the watcher is done scanning | 87 /// Normally, this will pause the schedule until the watcher is done scanning |
78 /// and is polling for changes. If you pass `false` for [waitForReady], it will | 88 /// and is polling for changes. If you pass `false` for [waitForReady], it will |
79 /// not schedule this delay. | 89 /// not schedule this delay. |
80 /// | 90 /// |
81 /// If [dir] is provided, watches a subdirectory in the sandbox with that name. | 91 /// If [dir] is provided, watches a subdirectory in the sandbox with that name. |
82 DirectoryWatcher createWatcher({String dir, bool waitForReady}) { | 92 DirectoryWatcher createWatcher({String dir, bool waitForReady}) { |
83 if (dir == null) { | 93 if (dir == null) { |
84 dir = _sandboxDir; | 94 dir = _sandboxDir; |
85 } else { | 95 } else { |
86 dir = p.join(_sandboxDir, dir); | 96 dir = p.join(_sandboxDir, dir); |
87 } | 97 } |
88 | 98 |
89 // Use a short delay to make the tests run quickly. | 99 var watcher = _watcherFactory(dir); |
90 _watcher = new DirectoryWatcher(dir, | |
91 pollingDelay: new Duration(milliseconds: 100)); | |
92 | 100 |
93 // Wait until the scan is finished so that we don't miss changes to files | 101 // Wait until the scan is finished so that we don't miss changes to files |
94 // that could occur before the scan completes. | 102 // that could occur before the scan completes. |
95 if (waitForReady != false) { | 103 if (waitForReady != false) { |
96 schedule(() => _watcher.ready, "wait for watcher to be ready"); | 104 schedule(() => watcher.ready, "wait for watcher to be ready"); |
97 } | 105 } |
98 | 106 |
99 currentSchedule.onComplete.schedule(() { | 107 return watcher; |
100 _nextEvent = 0; | |
101 _watcher = null; | |
102 }, "reset watcher"); | |
103 | |
104 return _watcher; | |
105 } | 108 } |
106 | 109 |
107 /// Expects that the next set of events will all be changes of [type] on | 110 /// The stream of events from the watcher started with [startWatcher]. |
108 /// [paths]. | 111 Stream _watcherEvents; |
| 112 |
| 113 /// Creates a new [DirectoryWatcher] that watches a temporary directory and |
| 114 /// starts monitoring it for events. |
109 /// | 115 /// |
110 /// Validates that events are delivered for all paths in [paths], but allows | 116 /// If [dir] is provided, watches a subdirectory in the sandbox with that name. |
111 /// them in any order. | 117 void startWatcher({String dir}) { |
112 void expectEvents(ChangeType type, Iterable<String> paths) { | 118 // We want to wait until we're ready *after* we subscribe to the watcher's |
113 var pathSet = paths | 119 // events. |
114 .map((path) => p.join(_sandboxDir, path)) | 120 _watcher = createWatcher(dir: dir, waitForReady: false); |
115 .map(p.normalize) | |
116 .toSet(); | |
117 | 121 |
118 // Create an expectation for as many paths as we have. | 122 // Schedule [_watcher.events.listen] so that the watcher doesn't start |
119 var futures = []; | 123 // watching [dir] before it exists. Expose [_watcherEvents] immediately so |
| 124 // that it can be accessed synchronously after this. |
| 125 _watcherEvents = futureStream(schedule(() { |
| 126 var allEvents = new Queue(); |
| 127 var subscription = _watcher.events.listen(allEvents.add, |
| 128 onError: currentSchedule.signalError); |
120 | 129 |
121 for (var i = 0; i < paths.length; i++) { | 130 currentSchedule.onComplete.schedule(() { |
122 // Immediately create the futures. This ensures we don't register too | 131 var numEvents = _nextEvent; |
123 // late and drop the event before we receive it. | 132 subscription.cancel(); |
124 var future = _watcher.events.elementAt(_nextEvent++).then((event) { | 133 _nextEvent = 0; |
125 expect(event.type, equals(type)); | 134 _watcher = null; |
126 expect(pathSet, contains(event.path)); | |
127 | 135 |
128 pathSet.remove(event.path); | 136 // If there are already errors, don't add this to the output and make |
129 }); | 137 // people think it might be the root cause. |
| 138 if (currentSchedule.errors.isEmpty) { |
| 139 expect(allEvents, hasLength(numEvents)); |
| 140 } |
| 141 }, "reset watcher"); |
130 | 142 |
131 // Make sure the schedule is watching it in case it fails. | 143 return _watcher.events; |
132 currentSchedule.wrapFuture(future); | 144 }, "create watcher")).asBroadcastStream(); |
133 | 145 |
134 futures.add(future); | 146 schedule(() => _watcher.ready, "wait for watcher to be ready"); |
135 } | |
136 | |
137 // Schedule it so that later file modifications don't occur until after this | |
138 // event is received. | |
139 schedule(() => Future.wait(futures), | |
140 "wait for $type events on ${paths.join(', ')}"); | |
141 } | 147 } |
142 | 148 |
143 void expectAddEvent(String path) => expectEvents(ChangeType.ADD, [path]); | 149 /// A future set by [inAnyOrder] that will complete to the set of events that |
144 void expectModifyEvent(String path) => expectEvents(ChangeType.MODIFY, [path]); | 150 /// occur in the [inAnyOrder] block. |
145 void expectRemoveEvent(String path) => expectEvents(ChangeType.REMOVE, [path]); | 151 Future<Set<WatchEvent>> _unorderedEventFuture; |
146 | 152 |
147 void expectRemoveEvents(Iterable<String> paths) { | 153 /// Runs [block] and allows multiple [expectEvent] calls in that block to match |
148 expectEvents(ChangeType.REMOVE, paths); | 154 /// events in any order. |
| 155 void inAnyOrder(block()) { |
| 156 var oldFuture = _unorderedEventFuture; |
| 157 try { |
| 158 var firstEvent = _nextEvent; |
| 159 var completer = new Completer(); |
| 160 _unorderedEventFuture = completer.future; |
| 161 block(); |
| 162 |
| 163 _watcherEvents.skip(firstEvent).take(_nextEvent - firstEvent).toSet() |
| 164 .then(completer.complete, onError: completer.completeError); |
| 165 currentSchedule.wrapFuture(_unorderedEventFuture, |
| 166 "waiting for ${_nextEvent - firstEvent} events"); |
| 167 } finally { |
| 168 _unorderedEventFuture = oldFuture; |
| 169 } |
149 } | 170 } |
150 | 171 |
| 172 /// Expects that the next set of event will be a change of [type] on [path]. |
| 173 /// |
| 174 /// Multiple calls to [expectEvent] require that the events are received in that |
| 175 /// order unless they're called in an [inAnyOrder] block. |
| 176 void expectEvent(ChangeType type, String path) { |
| 177 var matcher = predicate((e) { |
| 178 return e is WatchEvent && e.type == type && |
| 179 e.path == p.join(_sandboxDir, path); |
| 180 }, "is $type $path"); |
| 181 |
| 182 if (_unorderedEventFuture != null) { |
| 183 // Assign this to a local variable since it will be un-assigned by the time |
| 184 // the scheduled callback runs. |
| 185 var future = _unorderedEventFuture; |
| 186 |
| 187 expect( |
| 188 schedule(() => future, "should fire $type event on $path"), |
| 189 completion(contains(matcher))); |
| 190 } else { |
| 191 var future = currentSchedule.wrapFuture( |
| 192 _watcherEvents.elementAt(_nextEvent), |
| 193 "waiting for $type event on $path"); |
| 194 |
| 195 expect( |
| 196 schedule(() => future, "should fire $type event on $path"), |
| 197 completion(matcher)); |
| 198 } |
| 199 _nextEvent++; |
| 200 } |
| 201 |
| 202 void expectAddEvent(String path) => expectEvent(ChangeType.ADD, path); |
| 203 void expectModifyEvent(String path) => expectEvent(ChangeType.MODIFY, path); |
| 204 void expectRemoveEvent(String path) => expectEvent(ChangeType.REMOVE, path); |
| 205 |
151 /// Schedules writing a file in the sandbox at [path] with [contents]. | 206 /// Schedules writing a file in the sandbox at [path] with [contents]. |
152 /// | 207 /// |
153 /// If [contents] is omitted, creates an empty file. If [updatedModified] is | 208 /// If [contents] is omitted, creates an empty file. If [updatedModified] is |
154 /// `false`, the mock file modification time is not changed. | 209 /// `false`, the mock file modification time is not changed. |
155 void writeFile(String path, {String contents, bool updateModified}) { | 210 void writeFile(String path, {String contents, bool updateModified}) { |
156 if (contents == null) contents = ""; | 211 if (contents == null) contents = ""; |
157 if (updateModified == null) updateModified = true; | 212 if (updateModified == null) updateModified = true; |
158 | 213 |
159 schedule(() { | 214 schedule(() { |
160 var fullPath = p.join(_sandboxDir, path); | 215 var fullPath = p.join(_sandboxDir, path); |
(...skipping 33 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
194 | 249 |
195 // Make sure we always use the same separator on Windows. | 250 // Make sure we always use the same separator on Windows. |
196 to = p.normalize(to); | 251 to = p.normalize(to); |
197 | 252 |
198 // Manually update the mock modification time for the file. | 253 // Manually update the mock modification time for the file. |
199 var milliseconds = _mockFileModificationTimes.putIfAbsent(to, () => 0); | 254 var milliseconds = _mockFileModificationTimes.putIfAbsent(to, () => 0); |
200 _mockFileModificationTimes[to]++; | 255 _mockFileModificationTimes[to]++; |
201 }, "rename file $from to $to"); | 256 }, "rename file $from to $to"); |
202 } | 257 } |
203 | 258 |
| 259 /// Schedules creating a directory in the sandbox at [path]. |
| 260 void createDir(String path) { |
| 261 schedule(() { |
| 262 new Directory(p.join(_sandboxDir, path)).createSync(); |
| 263 }, "create directory $path"); |
| 264 } |
| 265 |
| 266 /// Schedules renaming a directory in the sandbox from [from] to [to]. |
| 267 void renameDir(String from, String to) { |
| 268 schedule(() { |
| 269 new Directory(p.join(_sandboxDir, from)) |
| 270 .renameSync(p.join(_sandboxDir, to)); |
| 271 }, "rename directory $from to $to"); |
| 272 } |
| 273 |
204 /// Schedules deleting a directory in the sandbox at [path]. | 274 /// Schedules deleting a directory in the sandbox at [path]. |
205 void deleteDir(String path) { | 275 void deleteDir(String path) { |
206 schedule(() { | 276 schedule(() { |
207 new Directory(p.join(_sandboxDir, path)).deleteSync(recursive: true); | 277 new Directory(p.join(_sandboxDir, path)).deleteSync(recursive: true); |
208 }, "delete directory $path"); | 278 }, "delete directory $path"); |
209 } | 279 } |
210 | 280 |
211 /// A [Matcher] for [WatchEvent]s. | 281 /// Runs [callback] with every permutation of non-negative [i], [j], and [k] |
212 class _ChangeMatcher extends Matcher { | 282 /// less than [limit]. |
213 /// The expected change. | 283 /// |
214 final ChangeType type; | 284 /// [limit] defaults to 3. |
215 | 285 void withPermutations(callback(int i, int j, int k), {int limit}) { |
216 /// The expected path. | 286 if (limit == null) limit = 3; |
217 final String path; | 287 for (var i = 0; i < limit; i++) { |
218 | 288 for (var j = 0; j < limit; j++) { |
219 _ChangeMatcher(this.type, this.path); | 289 for (var k = 0; k < limit; k++) { |
220 | 290 callback(i, j, k); |
221 Description describe(Description description) { | 291 } |
222 description.add("$type $path"); | 292 } |
223 } | 293 } |
224 | |
225 bool matches(item, Map matchState) => | |
226 item is WatchEvent && | |
227 item.type == type && | |
228 p.normalize(item.path) == p.normalize(path); | |
229 } | 294 } |
OLD | NEW |