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 var future = _unorderedEventFuture; | |
Bob Nystrom
2013/11/06 19:24:04
Document that this is being stored locally since _
nweiz
2013/11/07 00:46:37
Done.
| |
184 _nextEvent++; | |
Bob Nystrom
2013/11/06 19:24:04
Move this above the if() and remove the ++ below.
nweiz
2013/11/07 00:46:37
Done.
| |
185 | |
186 expect( | |
187 schedule(() => future, "should fire $type event on $path"), | |
188 completion(contains(matcher))); | |
Bob Nystrom
2013/11/06 19:24:04
For inAnyOrder, should we care about duplicate eve
nweiz
2013/11/07 00:46:37
I thought about doing so, but I decided it wasn't
| |
189 } else { | |
190 var future = currentSchedule.wrapFuture( | |
191 _watcherEvents.elementAt(_nextEvent++), | |
192 "waiting for $type event on $path"); | |
193 | |
194 expect( | |
195 schedule(() => future, "should fire $type event on $path"), | |
196 completion(matcher)); | |
197 } | |
198 } | |
199 | |
200 void expectAddEvent(String path) => expectEvent(ChangeType.ADD, path); | |
201 void expectModifyEvent(String path) => expectEvent(ChangeType.MODIFY, path); | |
202 void expectRemoveEvent(String path) => expectEvent(ChangeType.REMOVE, path); | |
203 | |
151 /// Schedules writing a file in the sandbox at [path] with [contents]. | 204 /// Schedules writing a file in the sandbox at [path] with [contents]. |
152 /// | 205 /// |
153 /// If [contents] is omitted, creates an empty file. If [updatedModified] is | 206 /// If [contents] is omitted, creates an empty file. If [updatedModified] is |
154 /// `false`, the mock file modification time is not changed. | 207 /// `false`, the mock file modification time is not changed. |
155 void writeFile(String path, {String contents, bool updateModified}) { | 208 void writeFile(String path, {String contents, bool updateModified}) { |
156 if (contents == null) contents = ""; | 209 if (contents == null) contents = ""; |
157 if (updateModified == null) updateModified = true; | 210 if (updateModified == null) updateModified = true; |
158 | 211 |
159 schedule(() { | 212 schedule(() { |
160 var fullPath = p.join(_sandboxDir, path); | 213 var fullPath = p.join(_sandboxDir, path); |
(...skipping 33 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
194 | 247 |
195 // Make sure we always use the same separator on Windows. | 248 // Make sure we always use the same separator on Windows. |
196 to = p.normalize(to); | 249 to = p.normalize(to); |
197 | 250 |
198 // Manually update the mock modification time for the file. | 251 // Manually update the mock modification time for the file. |
199 var milliseconds = _mockFileModificationTimes.putIfAbsent(to, () => 0); | 252 var milliseconds = _mockFileModificationTimes.putIfAbsent(to, () => 0); |
200 _mockFileModificationTimes[to]++; | 253 _mockFileModificationTimes[to]++; |
201 }, "rename file $from to $to"); | 254 }, "rename file $from to $to"); |
202 } | 255 } |
203 | 256 |
257 /// Schedules creating a directory in the sandbox at [path]. | |
Bob Nystrom
2013/11/06 19:24:04
Can you use the descriptor API instead of adding t
nweiz
2013/11/07 00:46:37
I wanted to keep this consistent with the other me
Bob Nystrom
2013/11/07 18:16:05
SGTM. That was my guess behind your thinking here.
| |
258 void createDir(String path) { | |
259 schedule(() { | |
260 new Directory(p.join(_sandboxDir, path)).createSync(); | |
261 }, "create directory $path"); | |
262 } | |
263 | |
264 /// Schedules renaming a directory in the sandbox from [from] to [to]. | |
265 void renameDir(String from, String to) { | |
Bob Nystrom
2013/11/06 19:24:04
Does scheduled_test have something for this alread
nweiz
2013/11/07 00:46:37
It doesn't. Right now it just exposes the descript
| |
266 schedule(() { | |
267 new Directory(p.join(_sandboxDir, from)) | |
268 .renameSync(p.join(_sandboxDir, to)); | |
269 }, "rename directory $from to $to"); | |
270 } | |
271 | |
204 /// Schedules deleting a directory in the sandbox at [path]. | 272 /// Schedules deleting a directory in the sandbox at [path]. |
205 void deleteDir(String path) { | 273 void deleteDir(String path) { |
Bob Nystrom
2013/11/06 19:24:04
Ditto.
| |
206 schedule(() { | 274 schedule(() { |
207 new Directory(p.join(_sandboxDir, path)).deleteSync(recursive: true); | 275 new Directory(p.join(_sandboxDir, path)).deleteSync(recursive: true); |
208 }, "delete directory $path"); | 276 }, "delete directory $path"); |
209 } | 277 } |
210 | 278 |
211 /// A [Matcher] for [WatchEvent]s. | 279 /// Runs [callback] with every permutation of non-negative [i], [j], and [k] |
212 class _ChangeMatcher extends Matcher { | 280 /// less than [limit]. |
213 /// The expected change. | 281 /// |
214 final ChangeType type; | 282 /// [limit] defaults to 3. |
215 | 283 void withPermutations(callback(int i, int j, int k), {int limit}) { |
216 /// The expected path. | 284 if (limit == null) limit = 3; |
217 final String path; | 285 for (var i = 0; i < limit; i++) { |
218 | 286 for (var j = 0; j < limit; j++) { |
219 _ChangeMatcher(this.type, this.path); | 287 for (var k = 0; k < limit; k++) { |
220 | 288 callback(i, j, k); |
221 Description describe(Description description) { | 289 } |
222 description.add("$type $path"); | 290 } |
223 } | 291 } |
224 | |
225 bool matches(item, Map matchState) => | |
226 item is WatchEvent && | |
227 item.type == type && | |
228 p.normalize(item.path) == p.normalize(path); | |
229 } | 292 } |
OLD | NEW |