Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(290)

Side by Side Diff: pkg/watcher/test/utils.dart

Issue 46843003: Wrap Directory.watch on linux for the watcher package. (Closed) Base URL: https://dart.googlecode.com/svn/branches/bleeding_edge/dart
Patch Set: Created 7 years, 1 month ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
OLDNEW
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
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 }
OLDNEW
« pkg/watcher/test/directory_watcher/shared.dart ('K') | « pkg/watcher/test/ready_test.dart ('k') | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698