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 |