OLD | NEW |
1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file | 1 // Copyright (c) 2013, 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 /// Test infrastructure for testing pub. Unlike typical unit tests, most pub | 5 /// Test infrastructure for testing pub. Unlike typical unit tests, most pub |
6 /// tests are integration tests that stage some stuff on the file system, run | 6 /// tests are integration tests that stage some stuff on the file system, run |
7 /// pub, and then validate the results. This library provides an API to build | 7 /// pub, and then validate the results. This library provides an API to build |
8 /// tests like that. | 8 /// tests like that. |
9 library test_pub; | 9 library test_pub; |
10 | 10 |
11 import 'dart:async'; | 11 import 'dart:async'; |
12 import 'dart:collection' show Queue; | 12 import 'dart:collection' show Queue; |
13 import 'dart:io'; | 13 import 'dart:io'; |
14 import 'dart:json' as json; | 14 import 'dart:json' as json; |
15 import 'dart:math'; | 15 import 'dart:math'; |
16 import 'dart:uri'; | 16 import 'dart:uri'; |
17 import 'dart:utf'; | 17 import 'dart:utf'; |
18 | 18 |
19 import '../../../pkg/http/lib/testing.dart'; | 19 import '../../../pkg/http/lib/testing.dart'; |
20 import '../../../pkg/oauth2/lib/oauth2.dart' as oauth2; | 20 import '../../../pkg/oauth2/lib/oauth2.dart' as oauth2; |
21 import '../../../pkg/pathos/lib/path.dart' as path; | 21 import '../../../pkg/pathos/lib/path.dart' as path; |
22 import '../../../pkg/unittest/lib/unittest.dart'; | 22 import '../../../pkg/scheduled_test/lib/scheduled_process.dart'; |
| 23 import '../../../pkg/scheduled_test/lib/scheduled_server.dart'; |
| 24 import '../../../pkg/scheduled_test/lib/scheduled_test.dart'; |
23 import '../../../pkg/yaml/lib/yaml.dart'; | 25 import '../../../pkg/yaml/lib/yaml.dart'; |
| 26 |
24 import '../../lib/file_system.dart' as fs; | 27 import '../../lib/file_system.dart' as fs; |
25 import '../../pub/entrypoint.dart'; | 28 import '../../pub/entrypoint.dart'; |
26 // TODO(rnystrom): Using "gitlib" as the prefix here is ugly, but "git" collides | 29 // TODO(rnystrom): Using "gitlib" as the prefix here is ugly, but "git" collides |
27 // with the git descriptor method. Maybe we should try to clean up the top level | 30 // with the git descriptor method. Maybe we should try to clean up the top level |
28 // scope a bit? | 31 // scope a bit? |
29 import '../../pub/git.dart' as gitlib; | 32 import '../../pub/git.dart' as gitlib; |
30 import '../../pub/git_source.dart'; | 33 import '../../pub/git_source.dart'; |
31 import '../../pub/hosted_source.dart'; | 34 import '../../pub/hosted_source.dart'; |
32 import '../../pub/http.dart'; | 35 import '../../pub/http.dart'; |
33 import '../../pub/io.dart'; | 36 import '../../pub/io.dart'; |
34 import '../../pub/path_source.dart'; | 37 import '../../pub/path_source.dart'; |
35 import '../../pub/safe_http_server.dart'; | 38 import '../../pub/safe_http_server.dart'; |
36 import '../../pub/system_cache.dart'; | 39 import '../../pub/system_cache.dart'; |
37 import '../../pub/utils.dart'; | 40 import '../../pub/utils.dart'; |
38 import '../../pub/validator.dart'; | 41 import '../../pub/validator.dart'; |
39 import 'command_line_config.dart'; | 42 import 'command_line_config.dart'; |
| 43 import 'descriptor.dart' as d; |
40 | 44 |
41 /// This should be called at the top of a test file to set up an appropriate | 45 /// This should be called at the top of a test file to set up an appropriate |
42 /// test configuration for the machine running the tests. | 46 /// test configuration for the machine running the tests. |
43 initConfig() { | 47 initConfig() { |
44 // If we aren't running on the bots, use the human-friendly config. | 48 // If we aren't running on the bots, use the human-friendly config. |
45 if (new Options().arguments.contains('--human')) { | 49 if (new Options().arguments.contains('--human')) { |
46 configure(new CommandLineConfiguration()); | 50 configure(new CommandLineConfiguration()); |
47 } | 51 } |
48 } | 52 } |
49 | 53 |
50 /// Creates a new [FileDescriptor] with [name] and [contents]. | |
51 FileDescriptor file(Pattern name, String contents) => | |
52 new FileDescriptor(name, contents); | |
53 | |
54 /// Creates a new [FileDescriptor] with [name] and [contents]. | |
55 FileDescriptor binaryFile(Pattern name, List<int> contents) => | |
56 new FileDescriptor.bytes(name, contents); | |
57 | |
58 /// Creates a new [DirectoryDescriptor] with [name] and [contents]. | |
59 DirectoryDescriptor dir(Pattern name, [List<Descriptor> contents]) => | |
60 new DirectoryDescriptor(name, contents); | |
61 | |
62 /// Creates a new [FutureDescriptor] wrapping [future]. | |
63 FutureDescriptor async(Future<Descriptor> future) => | |
64 new FutureDescriptor(future); | |
65 | |
66 /// Creates a new [GitRepoDescriptor] with [name] and [contents]. | |
67 GitRepoDescriptor git(Pattern name, [List<Descriptor> contents]) => | |
68 new GitRepoDescriptor(name, contents); | |
69 | |
70 /// Creates a new [TarFileDescriptor] with [name] and [contents]. | |
71 TarFileDescriptor tar(Pattern name, [List<Descriptor> contents]) => | |
72 new TarFileDescriptor(name, contents); | |
73 | |
74 /// Creates a new [NothingDescriptor] with [name]. | |
75 NothingDescriptor nothing(String name) => new NothingDescriptor(name); | |
76 | |
77 /// The current [HttpServer] created using [serve]. | 54 /// The current [HttpServer] created using [serve]. |
78 var _server; | 55 var _server; |
79 | 56 |
80 /// The cached value for [_portCompleter]. | 57 /// The cached value for [_portCompleter]. |
81 Completer<int> _portCompleterCache; | 58 Completer<int> _portCompleterCache; |
82 | 59 |
83 /// The completer for [port]. | 60 /// The completer for [port]. |
84 Completer<int> get _portCompleter { | 61 Completer<int> get _portCompleter { |
85 if (_portCompleterCache != null) return _portCompleterCache; | 62 if (_portCompleterCache != null) return _portCompleterCache; |
86 _portCompleterCache = new Completer<int>(); | 63 _portCompleterCache = new Completer<int>(); |
87 _scheduleCleanup((_) { | 64 currentSchedule.onComplete.schedule(() { |
88 _portCompleterCache = null; | 65 _portCompleterCache = null; |
89 }); | 66 }, 'clearing the port completer'); |
90 return _portCompleterCache; | 67 return _portCompleterCache; |
91 } | 68 } |
92 | 69 |
93 /// A future that will complete to the port used for the current server. | 70 /// A future that will complete to the port used for the current server. |
94 Future<int> get port => _portCompleter.future; | 71 Future<int> get port => _portCompleter.future; |
95 | 72 |
96 /// Creates an HTTP server to serve [contents] as static files. This server will | 73 /// Creates an HTTP server to serve [contents] as static files. This server will |
97 /// exist only for the duration of the pub run. | 74 /// exist only for the duration of the pub run. |
98 /// | 75 /// |
99 /// Subsequent calls to [serve] will replace the previous server. | 76 /// Subsequent calls to [serve] will replace the previous server. |
100 void serve([List<Descriptor> contents]) { | 77 void serve([List<d.Descriptor> contents]) { |
101 var baseDir = dir("serve-dir", contents); | 78 var baseDir = d.dir("serve-dir", contents); |
102 | 79 |
103 _schedule((_) { | 80 schedule(() { |
104 return _closeServer().then((_) { | 81 return _closeServer().then((_) { |
105 return SafeHttpServer.bind("127.0.0.1", 0).then((server) { | 82 return SafeHttpServer.bind("127.0.0.1", 0).then((server) { |
106 _server = server; | 83 _server = server; |
107 server.listen((request) { | 84 server.listen((request) { |
108 var response = request.response; | 85 var response = request.response; |
109 var path = request.uri.path.replaceFirst("/", "").split("/"); | |
110 response.persistentConnection = false; | |
111 var stream; | |
112 try { | 86 try { |
113 stream = baseDir.load(path); | 87 var path = request.uri.path.replaceFirst("/", ""); |
| 88 response.persistentConnection = false; |
| 89 var stream = baseDir.load(path); |
| 90 |
| 91 new ByteStream(stream).toBytes().then((data) { |
| 92 response.statusCode = 200; |
| 93 response.contentLength = data.length; |
| 94 response.writeBytes(data); |
| 95 response.close(); |
| 96 }).catchError((e) { |
| 97 response.statusCode = 404; |
| 98 response.contentLength = 0; |
| 99 response.close(); |
| 100 }); |
114 } catch (e) { | 101 } catch (e) { |
115 response.statusCode = 404; | 102 currentSchedule.signalError(e); |
116 response.contentLength = 0; | 103 response.statusCode = 500; |
117 response.close(); | 104 response.close(); |
118 return; | 105 return; |
119 } | 106 } |
120 | |
121 stream.toBytes().then((data) { | |
122 response.statusCode = 200; | |
123 response.contentLength = data.length; | |
124 response.writeBytes(data); | |
125 response.close(); | |
126 }).catchError((e) { | |
127 print("Exception while handling ${request.uri}: $e"); | |
128 response.statusCode = 500; | |
129 response.reasonPhrase = e.message; | |
130 response.close(); | |
131 }); | |
132 }); | 107 }); |
133 _portCompleter.complete(_server.port); | 108 _portCompleter.complete(_server.port); |
134 _scheduleCleanup((_) => _closeServer()); | 109 currentSchedule.onComplete.schedule(_closeServer); |
135 return null; | 110 return null; |
136 }); | 111 }); |
137 }); | 112 }); |
138 }); | 113 }, 'starting a server serving:\n${baseDir.describe()}'); |
139 } | 114 } |
140 | 115 |
141 /// Closes [_server]. Returns a [Future] that will complete after the [_server] | 116 /// Closes [_server]. Returns a [Future] that will complete after the [_server] |
142 /// is closed. | 117 /// is closed. |
143 Future _closeServer() { | 118 Future _closeServer() { |
144 if (_server == null) return new Future.immediate(null); | 119 if (_server == null) return new Future.immediate(null); |
145 _server.close(); | 120 _server.close(); |
146 _server = null; | 121 _server = null; |
147 _portCompleterCache = null; | 122 _portCompleterCache = null; |
148 // TODO(nweiz): Remove this once issue 4155 is fixed. Pumping the event loop | 123 // TODO(nweiz): Remove this once issue 4155 is fixed. Pumping the event loop |
149 // *seems* to be enough to ensure that the server is actually closed, but I'm | 124 // *seems* to be enough to ensure that the server is actually closed, but I'm |
150 // putting this at 10ms to be safe. | 125 // putting this at 10ms to be safe. |
151 return sleep(10); | 126 return sleep(10); |
152 } | 127 } |
153 | 128 |
154 /// The [DirectoryDescriptor] describing the server layout of packages that are | 129 /// The [d.DirectoryDescriptor] describing the server layout of packages that |
155 /// being served via [servePackages]. This is `null` if [servePackages] has not | 130 /// are being served via [servePackages]. This is `null` if [servePackages] has |
156 /// yet been called for this test. | 131 /// not yet been called for this test. |
157 DirectoryDescriptor _servedPackageDir; | 132 d.DirectoryDescriptor _servedPackageDir; |
158 | 133 |
159 /// A map from package names to version numbers to YAML-serialized pubspecs for | 134 /// A map from package names to version numbers to YAML-serialized pubspecs for |
160 /// those packages. This represents the packages currently being served by | 135 /// those packages. This represents the packages currently being served by |
161 /// [servePackages], and is `null` if [servePackages] has not yet been called | 136 /// [servePackages], and is `null` if [servePackages] has not yet been called |
162 /// for this test. | 137 /// for this test. |
163 Map<String, Map<String, String>> _servedPackages; | 138 Map<String, Map<String, String>> _servedPackages; |
164 | 139 |
165 /// Creates an HTTP server that replicates the structure of pub.dartlang.org. | 140 /// Creates an HTTP server that replicates the structure of pub.dartlang.org. |
166 /// [pubspecs] is a list of unserialized pubspecs representing the packages to | 141 /// [pubspecs] is a list of unserialized pubspecs representing the packages to |
167 /// serve. | 142 /// serve. |
168 /// | 143 /// |
169 /// Subsequent calls to [servePackages] will add to the set of packages that | 144 /// Subsequent calls to [servePackages] will add to the set of packages that |
170 /// are being served. Previous packages will continue to be served. | 145 /// are being served. Previous packages will continue to be served. |
171 void servePackages(List<Map> pubspecs) { | 146 void servePackages(List<Map> pubspecs) { |
172 if (_servedPackages == null || _servedPackageDir == null) { | 147 if (_servedPackages == null || _servedPackageDir == null) { |
173 _servedPackages = <String, Map<String, String>>{}; | 148 _servedPackages = <String, Map<String, String>>{}; |
174 _servedPackageDir = dir('packages', []); | 149 _servedPackageDir = d.dir('packages', []); |
175 serve([_servedPackageDir]); | 150 serve([_servedPackageDir]); |
176 | 151 |
177 _scheduleCleanup((_) { | 152 currentSchedule.onComplete.schedule(() { |
178 _servedPackages = null; | 153 _servedPackages = null; |
179 _servedPackageDir = null; | 154 _servedPackageDir = null; |
180 }); | 155 }, 'cleaning up served packages'); |
181 } | 156 } |
182 | 157 |
183 _schedule((_) { | 158 schedule(() { |
184 return _awaitObject(pubspecs).then((resolvedPubspecs) { | 159 return awaitObject(pubspecs).then((resolvedPubspecs) { |
185 for (var spec in resolvedPubspecs) { | 160 for (var spec in resolvedPubspecs) { |
186 var name = spec['name']; | 161 var name = spec['name']; |
187 var version = spec['version']; | 162 var version = spec['version']; |
188 var versions = _servedPackages.putIfAbsent( | 163 var versions = _servedPackages.putIfAbsent( |
189 name, () => <String, String>{}); | 164 name, () => <String, String>{}); |
190 versions[version] = yaml(spec); | 165 versions[version] = yaml(spec); |
191 } | 166 } |
192 | 167 |
193 _servedPackageDir.contents.clear(); | 168 _servedPackageDir.contents.clear(); |
194 for (var name in _servedPackages.keys) { | 169 for (var name in _servedPackages.keys) { |
195 var versions = _servedPackages[name].keys.toList(); | 170 var versions = _servedPackages[name].keys.toList(); |
196 _servedPackageDir.contents.addAll([ | 171 _servedPackageDir.contents.addAll([ |
197 file('$name.json', | 172 d.file('$name.json', json.stringify({'versions': versions})), |
198 json.stringify({'versions': versions})), | 173 d.dir(name, [ |
199 dir(name, [ | 174 d.dir('versions', flatten(versions.map((version) { |
200 dir('versions', flatten(versions.map((version) { | |
201 return [ | 175 return [ |
202 file('$version.yaml', _servedPackages[name][version]), | 176 d.file('$version.yaml', _servedPackages[name][version]), |
203 tar('$version.tar.gz', [ | 177 d.tar('$version.tar.gz', [ |
204 file('pubspec.yaml', _servedPackages[name][version]), | 178 d.file('pubspec.yaml', _servedPackages[name][version]), |
205 libDir(name, '$name $version') | 179 d.libDir(name, '$name $version') |
206 ]) | 180 ]) |
207 ]; | 181 ]; |
208 }))) | 182 }))) |
209 ]) | 183 ]) |
210 ]); | 184 ]); |
211 } | 185 } |
212 }); | 186 }); |
213 }); | 187 }, 'initializing the package server'); |
214 } | 188 } |
215 | 189 |
216 /// Converts [value] into a YAML string. | 190 /// Converts [value] into a YAML string. |
217 String yaml(value) => json.stringify(value); | 191 String yaml(value) => json.stringify(value); |
218 | 192 |
219 /// Describes a package that passes all validation. | |
220 Descriptor get normalPackage => dir(appPath, [ | |
221 libPubspec("test_pkg", "1.0.0"), | |
222 file("LICENSE", "Eh, do what you want."), | |
223 dir("lib", [ | |
224 file("test_pkg.dart", "int i = 1;") | |
225 ]) | |
226 ]); | |
227 | |
228 /// Describes a file named `pubspec.yaml` with the given YAML-serialized | |
229 /// [contents], which should be a serializable object. | |
230 /// | |
231 /// [contents] may contain [Future]s that resolve to serializable objects, | |
232 /// which may in turn contain [Future]s recursively. | |
233 Descriptor pubspec(Map contents) { | |
234 return async(_awaitObject(contents).then((resolvedContents) => | |
235 file("pubspec.yaml", yaml(resolvedContents)))); | |
236 } | |
237 | |
238 /// Describes a file named `pubspec.yaml` for an application package with the | |
239 /// given [dependencies]. | |
240 Descriptor appPubspec(List dependencies) { | |
241 return pubspec({ | |
242 "name": "myapp", | |
243 "dependencies": _dependencyListToMap(dependencies) | |
244 }); | |
245 } | |
246 | |
247 /// Describes a file named `pubspec.yaml` for a library package with the given | |
248 /// [name], [version], and [deps]. If "sdk" is given, then it adds an SDK | |
249 /// constraint on that version. | |
250 Descriptor libPubspec(String name, String version, {List deps, String sdk}) { | |
251 var map = package(name, version, deps); | |
252 | |
253 if (sdk != null) { | |
254 map["environment"] = { | |
255 "sdk": sdk | |
256 }; | |
257 } | |
258 | |
259 return pubspec(map); | |
260 } | |
261 | |
262 /// Describes a directory named `lib` containing a single dart file named | |
263 /// `<name>.dart` that contains a line of Dart code. | |
264 Descriptor libDir(String name, [String code]) { | |
265 // Default to printing the name if no other code was given. | |
266 if (code == null) { | |
267 code = name; | |
268 } | |
269 | |
270 return dir("lib", [ | |
271 file("$name.dart", 'main() => "$code";') | |
272 ]); | |
273 } | |
274 | |
275 /// Describes a map representing a library package with the given [name], | |
276 /// [version], and [dependencies]. | |
277 Map package(String name, String version, [List dependencies]) { | |
278 var package = { | |
279 "name": name, | |
280 "version": version, | |
281 "author": "Nathan Weizenbaum <nweiz@google.com>", | |
282 "homepage": "http://pub.dartlang.org", | |
283 "description": "A package, I guess." | |
284 }; | |
285 if (dependencies != null) { | |
286 package["dependencies"] = _dependencyListToMap(dependencies); | |
287 } | |
288 return package; | |
289 } | |
290 | |
291 /// Describes a map representing a dependency on a package in the package | |
292 /// repository. | |
293 Map dependency(String name, [String versionConstraint]) { | |
294 var url = port.then((p) => "http://localhost:$p"); | |
295 var dependency = {"hosted": {"name": name, "url": url}}; | |
296 if (versionConstraint != null) dependency["version"] = versionConstraint; | |
297 return dependency; | |
298 } | |
299 | |
300 /// Describes a directory for a package installed from the mock package server. | |
301 /// This directory is of the form found in the global package cache. | |
302 DirectoryDescriptor packageCacheDir(String name, String version) { | |
303 return dir("$name-$version", [ | |
304 libDir(name, '$name $version') | |
305 ]); | |
306 } | |
307 | |
308 /// Describes a directory for a Git package. This directory is of the form | |
309 /// found in the revision cache of the global package cache. | |
310 DirectoryDescriptor gitPackageRevisionCacheDir(String name, [int modifier]) { | |
311 var value = name; | |
312 if (modifier != null) value = "$name $modifier"; | |
313 return dir(new RegExp("$name${r'-[a-f0-9]+'}"), [ | |
314 libDir(name, value) | |
315 ]); | |
316 } | |
317 | |
318 /// Describes a directory for a Git package. This directory is of the form | |
319 /// found in the repo cache of the global package cache. | |
320 DirectoryDescriptor gitPackageRepoCacheDir(String name) { | |
321 return dir(new RegExp("$name${r'-[a-f0-9]+'}"), [ | |
322 dir('hooks'), | |
323 dir('info'), | |
324 dir('objects'), | |
325 dir('refs') | |
326 ]); | |
327 } | |
328 | |
329 /// Describes the `packages/` directory containing all the given [packages], | |
330 /// which should be name/version pairs. The packages will be validated against | |
331 /// the format produced by the mock package server. | |
332 /// | |
333 /// A package with a null version should not be installed. | |
334 DirectoryDescriptor packagesDir(Map<String, String> packages) { | |
335 var contents = <Descriptor>[]; | |
336 packages.forEach((name, version) { | |
337 if (version == null) { | |
338 contents.add(nothing(name)); | |
339 } else { | |
340 contents.add(dir(name, [ | |
341 file("$name.dart", 'main() => "$name $version";') | |
342 ])); | |
343 } | |
344 }); | |
345 return dir(packagesPath, contents); | |
346 } | |
347 | |
348 /// Describes the global package cache directory containing all the given | |
349 /// [packages], which should be name/version pairs. The packages will be | |
350 /// validated against the format produced by the mock package server. | |
351 /// | |
352 /// A package's value may also be a list of versions, in which case all | |
353 /// versions are expected to be installed. | |
354 DirectoryDescriptor cacheDir(Map packages) { | |
355 var contents = <Descriptor>[]; | |
356 packages.forEach((name, versions) { | |
357 if (versions is! List) versions = [versions]; | |
358 for (var version in versions) { | |
359 contents.add(packageCacheDir(name, version)); | |
360 } | |
361 }); | |
362 return dir(cachePath, [ | |
363 dir('hosted', [ | |
364 async(port.then((p) => dir('localhost%58$p', contents))) | |
365 ]) | |
366 ]); | |
367 } | |
368 | |
369 /// Describes the file in the system cache that contains the client's OAuth2 | |
370 /// credentials. The URL "/token" on [server] will be used as the token | |
371 /// endpoint for refreshing the access token. | |
372 Descriptor credentialsFile( | |
373 ScheduledServer server, | |
374 String accessToken, | |
375 {String refreshToken, | |
376 DateTime expiration}) { | |
377 return async(server.url.then((url) { | |
378 return dir(cachePath, [ | |
379 file('credentials.json', new oauth2.Credentials( | |
380 accessToken, | |
381 refreshToken, | |
382 url.resolve('/token'), | |
383 ['https://www.googleapis.com/auth/userinfo.email'], | |
384 expiration).toJson()) | |
385 ]); | |
386 })); | |
387 } | |
388 | |
389 /// Describes the application directory, containing only a pubspec specifying | |
390 /// the given [dependencies]. | |
391 DirectoryDescriptor appDir(List dependencies) => | |
392 dir(appPath, [appPubspec(dependencies)]); | |
393 | |
394 /// Converts a list of dependencies as passed to [package] into a hash as used | |
395 /// in a pubspec. | |
396 Future<Map> _dependencyListToMap(List<Map> dependencies) { | |
397 return _awaitObject(dependencies).then((resolvedDependencies) { | |
398 var result = <String, Map>{}; | |
399 for (var dependency in resolvedDependencies) { | |
400 var keys = dependency.keys.where((key) => key != "version"); | |
401 var sourceName = only(keys); | |
402 var source; | |
403 switch (sourceName) { | |
404 case "git": | |
405 source = new GitSource(); | |
406 break; | |
407 case "hosted": | |
408 source = new HostedSource(); | |
409 break; | |
410 case "path": | |
411 source = new PathSource(); | |
412 break; | |
413 default: | |
414 throw 'Unknown source "$sourceName"'; | |
415 } | |
416 | |
417 result[_packageName(sourceName, dependency[sourceName])] = dependency; | |
418 } | |
419 return result; | |
420 }); | |
421 } | |
422 | |
423 /// Return the name for the package described by [description] and from | |
424 /// [sourceName]. | |
425 String _packageName(String sourceName, description) { | |
426 switch (sourceName) { | |
427 case "git": | |
428 var url = description is String ? description : description['url']; | |
429 // TODO(rnystrom): Using path.basename on a URL is hacky. If we add URL | |
430 // support to pkg/pathos, should use an explicit builder for that. | |
431 return path.basename(url.replaceFirst(new RegExp(r"(\.git)?/?$"), "")); | |
432 case "hosted": | |
433 if (description is String) return description; | |
434 return description['name']; | |
435 case "path": | |
436 return path.basename(description); | |
437 case "sdk": | |
438 return description; | |
439 default: | |
440 return description; | |
441 } | |
442 } | |
443 | |
444 /// The full path to the created sandbox directory for an integration test. | 193 /// The full path to the created sandbox directory for an integration test. |
445 String get sandboxDir => _sandboxDir; | 194 String get sandboxDir => _sandboxDir; |
446 String _sandboxDir; | 195 String _sandboxDir; |
447 | 196 |
448 /// The path of the package cache directory used for tests. Relative to the | 197 /// The path of the package cache directory used for tests. Relative to the |
449 /// sandbox directory. | 198 /// sandbox directory. |
450 final String cachePath = "cache"; | 199 final String cachePath = "cache"; |
451 | 200 |
452 /// The path of the mock SDK directory used for tests. Relative to the sandbox | 201 /// The path of the mock SDK directory used for tests. Relative to the sandbox |
453 /// directory. | 202 /// directory. |
454 final String sdkPath = "sdk"; | 203 final String sdkPath = "sdk"; |
455 | 204 |
456 /// The path of the mock app directory used for tests. Relative to the sandbox | 205 /// The path of the mock app directory used for tests. Relative to the sandbox |
457 /// directory. | 206 /// directory. |
458 final String appPath = "myapp"; | 207 final String appPath = "myapp"; |
459 | 208 |
460 /// The path of the packages directory in the mock app used for tests. Relative | 209 /// The path of the packages directory in the mock app used for tests. Relative |
461 /// to the sandbox directory. | 210 /// to the sandbox directory. |
462 final String packagesPath = "$appPath/packages"; | 211 final String packagesPath = "$appPath/packages"; |
463 | 212 |
464 /// The type for callbacks that will be fired during [schedulePub]. Takes the | |
465 /// sandbox directory as a parameter. | |
466 typedef Future _ScheduledEvent(String parentDir); | |
467 | |
468 /// The list of events that are scheduled to run as part of the test case. | |
469 Queue<_ScheduledEvent> _scheduled; | |
470 | |
471 /// The list of events that are scheduled to run after the test case, even if | |
472 /// it failed. | |
473 Queue<_ScheduledEvent> _scheduledCleanup; | |
474 | |
475 /// The list of events that are scheduled to run after the test case only if it | |
476 /// failed. | |
477 Queue<_ScheduledEvent> _scheduledOnException; | |
478 | |
479 /// Set to true when the current batch of scheduled events should be aborted. | 213 /// Set to true when the current batch of scheduled events should be aborted. |
480 bool _abortScheduled = false; | 214 bool _abortScheduled = false; |
481 | 215 |
482 /// The time (in milliseconds) to wait for the entire scheduled test to | 216 /// The time (in milliseconds) to wait for the entire scheduled test to |
483 /// complete. | 217 /// complete. |
484 final _TIMEOUT = 30000; | 218 final _TIMEOUT = 30000; |
485 | 219 |
486 /// Defines an integration test. The [body] should schedule a series of | 220 /// Defines an integration test. The [body] should schedule a series of |
487 /// operations which will be run asynchronously. | 221 /// operations which will be run asynchronously. |
488 void integration(String description, void body()) => | 222 void integration(String description, void body()) => |
489 _integration(description, body, test); | 223 _integration(description, body, test); |
490 | 224 |
491 /// Like [integration], but causes only this test to run. | 225 /// Like [integration], but causes only this test to run. |
492 void solo_integration(String description, void body()) => | 226 void solo_integration(String description, void body()) => |
493 _integration(description, body, solo_test); | 227 _integration(description, body, solo_test); |
494 | 228 |
495 void _integration(String description, void body(), [Function testFn]) { | 229 void _integration(String description, void body(), [Function testFn]) { |
496 testFn(description, () { | 230 testFn(description, () { |
497 // Ensure the SDK version is always available. | 231 // Ensure the SDK version is always available. |
498 dir(sdkPath, [ | 232 d.dir(sdkPath, [ |
499 file('version', '0.1.2.3') | 233 d.file('version', '0.1.2.3') |
500 ]).scheduleCreate(); | 234 ]).create(); |
501 | 235 |
502 _sandboxDir = createTempDir(); | 236 _sandboxDir = createTempDir(); |
| 237 d.defaultRoot = sandboxDir; |
| 238 currentSchedule.onComplete.schedule(() => deleteDir(_sandboxDir), |
| 239 'deleting the sandbox directory'); |
503 | 240 |
504 // Schedule the test. | 241 // Schedule the test. |
505 body(); | 242 body(); |
506 | |
507 // Run all of the scheduled tasks. If an error occurs, it will propagate | |
508 // through the futures back up to here where we can hand it off to unittest. | |
509 var asyncDone = expectAsync0(() {}); | |
510 return timeout(_runScheduled(_scheduled), | |
511 _TIMEOUT, 'waiting for a test to complete').catchError((e) { | |
512 return _runScheduled(_scheduledOnException).then((_) { | |
513 // Rethrow the original error so it keeps propagating. | |
514 throw e; | |
515 }); | |
516 }).whenComplete(() { | |
517 // Clean up after ourselves. Do this first before reporting back to | |
518 // unittest because it will advance to the next test immediately. | |
519 return _runScheduled(_scheduledCleanup).then((_) { | |
520 _scheduled = null; | |
521 _scheduledCleanup = null; | |
522 _scheduledOnException = null; | |
523 if (_sandboxDir != null) { | |
524 var dir = _sandboxDir; | |
525 _sandboxDir = null; | |
526 return deleteDir(dir); | |
527 } | |
528 }); | |
529 }).then((_) { | |
530 // If we got here, the test completed successfully so tell unittest so. | |
531 asyncDone(); | |
532 }).catchError((e) { | |
533 // If we got here, an error occurred. We will register it with unittest | |
534 // directly so that the error message isn't wrapped in any matcher stuff. | |
535 // We do this call last because it will cause unittest to *synchronously* | |
536 // advance to the next test and run it. | |
537 registerException(e.error, e.stackTrace); | |
538 }); | |
539 }); | 243 }); |
540 } | 244 } |
541 | 245 |
542 /// Get the path to the root "util/test/pub" directory containing the pub | 246 /// Get the path to the root "util/test/pub" directory containing the pub |
543 /// tests. | 247 /// tests. |
544 String get testDirectory { | 248 String get testDirectory { |
545 var dir = new Options().script; | 249 var dir = new Options().script; |
546 while (path.basename(dir) != 'pub') dir = path.dirname(dir); | 250 while (path.basename(dir) != 'pub') dir = path.dirname(dir); |
547 | 251 |
548 return path.absolute(dir); | 252 return path.absolute(dir); |
549 } | 253 } |
550 | 254 |
551 /// Schedules renaming (moving) the directory at [from] to [to], both of which | 255 /// Schedules renaming (moving) the directory at [from] to [to], both of which |
552 /// are assumed to be relative to [sandboxDir]. | 256 /// are assumed to be relative to [sandboxDir]. |
553 void scheduleRename(String from, String to) { | 257 void scheduleRename(String from, String to) { |
554 _schedule((sandboxDir) { | 258 schedule( |
555 return renameDir(path.join(sandboxDir, from), path.join(sandboxDir, to)); | 259 () => renameDir( |
556 }); | 260 path.join(sandboxDir, from), |
| 261 path.join(sandboxDir, to)), |
| 262 'renaming $from to $to'); |
557 } | 263 } |
558 | 264 |
559 | |
560 /// Schedules creating a symlink at path [symlink] that points to [target], | 265 /// Schedules creating a symlink at path [symlink] that points to [target], |
561 /// both of which are assumed to be relative to [sandboxDir]. | 266 /// both of which are assumed to be relative to [sandboxDir]. |
562 void scheduleSymlink(String target, String symlink) { | 267 void scheduleSymlink(String target, String symlink) { |
563 _schedule((sandboxDir) { | 268 schedule( |
564 return createSymlink(path.join(sandboxDir, target), | 269 () => createSymlink( |
565 path.join(sandboxDir, symlink)); | 270 path.join(sandboxDir, target), |
566 }); | 271 path.join(sandboxDir, symlink)), |
| 272 'symlinking $target to $symlink'); |
567 } | 273 } |
568 | 274 |
569 /// Schedules a call to the Pub command-line utility. Runs Pub with [args] and | 275 /// Schedules a call to the Pub command-line utility. Runs Pub with [args] and |
570 /// validates that its results match [output], [error], and [exitCode]. | 276 /// validates that its results match [output], [error], and [exitCode]. |
571 void schedulePub({List args, Pattern output, Pattern error, | 277 void schedulePub({List args, Pattern output, Pattern error, |
572 Future<Uri> tokenEndpoint, int exitCode: 0}) { | 278 Future<Uri> tokenEndpoint, int exitCode: 0}) { |
573 _schedule((sandboxDir) { | 279 var pub = startPub(args: args, tokenEndpoint: tokenEndpoint); |
574 return _doPub(runProcess, sandboxDir, args, tokenEndpoint).then((result) { | 280 pub.shouldExit(exitCode); |
575 var failures = []; | |
576 | 281 |
577 _validateOutput(failures, 'stdout', output, result.stdout); | 282 expect(Future.wait([ |
578 _validateOutput(failures, 'stderr', error, result.stderr); | 283 pub.remainingStdout(), |
579 | 284 pub.remainingStderr() |
580 if (result.exitCode != exitCode) { | 285 ]).then((results) { |
581 failures.add( | 286 var failures = []; |
582 'Pub returned exit code ${result.exitCode}, expected $exitCode.'); | 287 _validateOutput(failures, 'stdout', output, results[0].split('\n')); |
583 } | 288 _validateOutput(failures, 'stderr', error, results[1].split('\n')); |
584 | 289 if (!failures.isEmpty) throw new TestFailure(failures.join('\n')); |
585 if (failures.length > 0) { | 290 }), completes); |
586 if (error == null) { | |
587 // If we aren't validating the error, still show it on failure. | |
588 failures.add('Pub stderr:'); | |
589 failures.addAll(result.stderr.map((line) => '| $line')); | |
590 } | |
591 | |
592 throw new TestFailure(failures.join('\n')); | |
593 } | |
594 | |
595 return null; | |
596 }); | |
597 }); | |
598 } | |
599 | |
600 /// Starts a Pub process and returns a [ScheduledProcess] that supports | |
601 /// interaction with that process. | |
602 /// | |
603 /// Any futures in [args] will be resolved before the process is started. | |
604 ScheduledProcess startPub({List args, Future<Uri> tokenEndpoint}) { | |
605 var process = _scheduleValue((sandboxDir) => | |
606 _doPub(startProcess, sandboxDir, args, tokenEndpoint)); | |
607 return new ScheduledProcess("pub", process); | |
608 } | 291 } |
609 | 292 |
610 /// Like [startPub], but runs `pub lish` in particular with [server] used both | 293 /// Like [startPub], but runs `pub lish` in particular with [server] used both |
611 /// as the OAuth2 server (with "/token" as the token endpoint) and as the | 294 /// as the OAuth2 server (with "/token" as the token endpoint) and as the |
612 /// package server. | 295 /// package server. |
613 /// | 296 /// |
614 /// Any futures in [args] will be resolved before the process is started. | 297 /// Any futures in [args] will be resolved before the process is started. |
615 ScheduledProcess startPubLish(ScheduledServer server, {List args}) { | 298 ScheduledProcess startPublish(ScheduledServer server, {List args}) { |
616 var tokenEndpoint = server.url.then((url) => | 299 var tokenEndpoint = server.url.then((url) => |
617 url.resolve('/token').toString()); | 300 url.resolve('/token').toString()); |
618 if (args == null) args = []; | 301 if (args == null) args = []; |
619 args = flatten(['lish', '--server', tokenEndpoint, args]); | 302 args = flatten(['lish', '--server', tokenEndpoint, args]); |
620 return startPub(args: args, tokenEndpoint: tokenEndpoint); | 303 return startPub(args: args, tokenEndpoint: tokenEndpoint); |
621 } | 304 } |
622 | 305 |
623 /// Handles the beginning confirmation process for uploading a packages. | 306 /// Handles the beginning confirmation process for uploading a packages. |
624 /// Ensures that the right output is shown and then enters "y" to confirm the | 307 /// Ensures that the right output is shown and then enters "y" to confirm the |
625 /// upload. | 308 /// upload. |
626 void confirmPublish(ScheduledProcess pub) { | 309 void confirmPublish(ScheduledProcess pub) { |
627 // TODO(rnystrom): This is overly specific and inflexible regarding different | 310 // TODO(rnystrom): This is overly specific and inflexible regarding different |
628 // test packages. Should validate this a little more loosely. | 311 // test packages. Should validate this a little more loosely. |
629 expectLater(pub.nextLine(), equals('Publishing "test_pkg" 1.0.0:')); | 312 expect(pub.nextLine(), completion(equals('Publishing "test_pkg" 1.0.0:'))); |
630 expectLater(pub.nextLine(), equals("|-- LICENSE")); | 313 expect(pub.nextLine(), completion(equals("|-- LICENSE"))); |
631 expectLater(pub.nextLine(), equals("|-- lib")); | 314 expect(pub.nextLine(), completion(equals("|-- lib"))); |
632 expectLater(pub.nextLine(), equals("| '-- test_pkg.dart")); | 315 expect(pub.nextLine(), completion(equals("| '-- test_pkg.dart"))); |
633 expectLater(pub.nextLine(), equals("'-- pubspec.yaml")); | 316 expect(pub.nextLine(), completion(equals("'-- pubspec.yaml"))); |
634 expectLater(pub.nextLine(), equals("")); | 317 expect(pub.nextLine(), completion(equals(""))); |
635 | 318 |
636 pub.writeLine("y"); | 319 pub.writeLine("y"); |
637 } | 320 } |
638 | 321 |
639 /// Calls [fn] with appropriately modified arguments to run a pub process. [fn] | 322 /// Starts a Pub process and returns a [ScheduledProcess] that supports |
640 /// should have the same signature as [startProcess], except that the returned | 323 /// interaction with that process. |
641 /// [Future] may have a type other than [Process]. | 324 /// |
642 Future _doPub(Function fn, sandboxDir, List args, Future<Uri> tokenEndpoint) { | 325 /// Any futures in [args] will be resolved before the process is started. |
| 326 ScheduledProcess startPub({List args, Future<Uri> tokenEndpoint}) { |
643 String pathInSandbox(String relPath) { | 327 String pathInSandbox(String relPath) { |
644 return path.join(path.absolute(sandboxDir), relPath); | 328 return path.join(path.absolute(sandboxDir), relPath); |
645 } | 329 } |
646 | 330 |
647 return defer(() { | 331 ensureDir(pathInSandbox(appPath)); |
648 ensureDir(pathInSandbox(appPath)); | |
649 return Future.wait([ | |
650 _awaitObject(args), | |
651 tokenEndpoint == null ? new Future.immediate(null) : tokenEndpoint | |
652 ]); | |
653 }).then((results) { | |
654 var args = results[0]; | |
655 var tokenEndpoint = results[1]; | |
656 // Find a Dart executable we can use to spawn. Use the same one that was | |
657 // used to run this script itself. | |
658 var dartBin = new Options().executable; | |
659 | 332 |
660 // If the executable looks like a path, get its full path. That way we | 333 // Find a Dart executable we can use to spawn. Use the same one that was |
661 // can still find it when we spawn it with a different working directory. | 334 // used to run this script itself. |
662 if (dartBin.contains(Platform.pathSeparator)) { | 335 var dartBin = new Options().executable; |
663 dartBin = new File(dartBin).fullPathSync(); | 336 |
| 337 // If the executable looks like a path, get its full path. That way we |
| 338 // can still find it when we spawn it with a different working directory. |
| 339 if (dartBin.contains(Platform.pathSeparator)) { |
| 340 dartBin = new File(dartBin).fullPathSync(); |
| 341 } |
| 342 |
| 343 // Find the main pub entrypoint. |
| 344 var pubPath = fs.joinPaths(testDirectory, '../../pub/pub.dart'); |
| 345 |
| 346 var dartArgs = ['--checked', pubPath, '--trace']; |
| 347 dartArgs.addAll(args); |
| 348 |
| 349 if (tokenEndpoint == null) tokenEndpoint = new Future.immediate(null); |
| 350 var optionsFuture = tokenEndpoint.then((tokenEndpoint) { |
| 351 var options = new ProcessOptions(); |
| 352 options.workingDirectory = pathInSandbox(appPath); |
| 353 // TODO(nweiz): remove this when issue 9294 is fixed. |
| 354 options.environment = new Map(Platform.environment); |
| 355 options.environment['PUB_CACHE'] = pathInSandbox(cachePath); |
| 356 options.environment['DART_SDK'] = pathInSandbox(sdkPath); |
| 357 if (tokenEndpoint != null) { |
| 358 options.environment['_PUB_TEST_TOKEN_ENDPOINT'] = |
| 359 tokenEndpoint.toString(); |
664 } | 360 } |
| 361 return options; |
| 362 }); |
665 | 363 |
666 // Find the main pub entrypoint. | 364 return new ScheduledProcess.start(dartBin, dartArgs, options: optionsFuture, |
667 var pubPath = fs.joinPaths(testDirectory, '../../pub/pub.dart'); | 365 description: args.isEmpty ? 'pub' : 'pub ${args.first}'); |
668 | |
669 var dartArgs = ['--checked', pubPath, '--trace']; | |
670 dartArgs.addAll(args); | |
671 | |
672 var environment = { | |
673 'PUB_CACHE': pathInSandbox(cachePath), | |
674 'DART_SDK': pathInSandbox(sdkPath) | |
675 }; | |
676 | |
677 if (tokenEndpoint != null) { | |
678 environment['_PUB_TEST_TOKEN_ENDPOINT'] = tokenEndpoint.toString(); | |
679 } | |
680 | |
681 return fn(dartBin, dartArgs, workingDir: pathInSandbox(appPath), | |
682 environment: environment); | |
683 }); | |
684 } | 366 } |
685 | 367 |
686 /// Skips the current test if Git is not installed. This validates that the | 368 /// Skips the current test if Git is not installed. This validates that the |
687 /// current test is running on a buildbot in which case we expect git to be | 369 /// current test is running on a buildbot in which case we expect git to be |
688 /// installed. If we are not running on the buildbot, we will instead see if | 370 /// installed. If we are not running on the buildbot, we will instead see if |
689 /// git is installed and skip the test if not. This way, users don't need to | 371 /// git is installed and skip the test if not. This way, users don't need to |
690 /// have git installed to run the tests locally (unless they actually care | 372 /// have git installed to run the tests locally (unless they actually care |
691 /// about the pub git tests). | 373 /// about the pub git tests). |
692 void ensureGit() { | 374 void ensureGit() { |
693 _schedule((_) { | 375 schedule(() { |
694 return gitlib.isInstalled.then((installed) { | 376 return gitlib.isInstalled.then((installed) { |
695 if (!installed && | 377 if (installed) return; |
696 !Platform.environment.containsKey('BUILDBOT_BUILDERNAME')) { | 378 if (Platform.environment.containsKey('BUILDBOT_BUILDERNAME')) return; |
697 _abortScheduled = true; | 379 currentSchedule.abort(); |
698 } | |
699 return null; | |
700 }); | 380 }); |
701 }); | 381 }, 'ensuring that Git is installed'); |
702 } | 382 } |
703 | 383 |
704 /// Use [client] as the mock HTTP client for this test. | 384 /// Use [client] as the mock HTTP client for this test. |
705 /// | 385 /// |
706 /// Note that this will only affect HTTP requests made via http.dart in the | 386 /// Note that this will only affect HTTP requests made via http.dart in the |
707 /// parent process. | 387 /// parent process. |
708 void useMockClient(MockClient client) { | 388 void useMockClient(MockClient client) { |
709 var oldInnerClient = httpClient.inner; | 389 var oldInnerClient = httpClient.inner; |
710 httpClient.inner = client; | 390 httpClient.inner = client; |
711 _scheduleCleanup((_) { | 391 currentSchedule.onComplete.schedule(() { |
712 httpClient.inner = oldInnerClient; | 392 httpClient.inner = oldInnerClient; |
| 393 }, 'de-activating the mock client'); |
| 394 } |
| 395 |
| 396 /// Describes a map representing a library package with the given [name], |
| 397 /// [version], and [dependencies]. |
| 398 Map packageMap(String name, String version, [List dependencies]) { |
| 399 var package = { |
| 400 "name": name, |
| 401 "version": version, |
| 402 "author": "Nathan Weizenbaum <nweiz@google.com>", |
| 403 "homepage": "http://pub.dartlang.org", |
| 404 "description": "A package, I guess." |
| 405 }; |
| 406 if (dependencies != null) { |
| 407 package["dependencies"] = dependencyListToMap(dependencies); |
| 408 } |
| 409 return package; |
| 410 } |
| 411 |
| 412 /// Describes a map representing a dependency on a package in the package |
| 413 /// repository. |
| 414 Map dependencyMap(String name, [String versionConstraint]) { |
| 415 var url = port.then((p) => "http://localhost:$p"); |
| 416 var dependency = {"hosted": {"name": name, "url": url}}; |
| 417 if (versionConstraint != null) dependency["version"] = versionConstraint; |
| 418 return dependency; |
| 419 } |
| 420 |
| 421 /// Converts a list of dependencies as passed to [package] into a hash as used |
| 422 /// in a pubspec. |
| 423 Future<Map> dependencyListToMap(List<Map> dependencies) { |
| 424 return awaitObject(dependencies).then((resolvedDependencies) { |
| 425 var result = <String, Map>{}; |
| 426 for (var dependency in resolvedDependencies) { |
| 427 var keys = dependency.keys.where((key) => key != "version"); |
| 428 var sourceName = only(keys); |
| 429 var source; |
| 430 switch (sourceName) { |
| 431 case "git": |
| 432 source = new GitSource(); |
| 433 break; |
| 434 case "hosted": |
| 435 source = new HostedSource(); |
| 436 break; |
| 437 case "path": |
| 438 source = new PathSource(); |
| 439 break; |
| 440 default: |
| 441 throw 'Unknown source "$sourceName"'; |
| 442 } |
| 443 |
| 444 result[_packageName(sourceName, dependency[sourceName])] = dependency; |
| 445 } |
| 446 return result; |
713 }); | 447 }); |
714 } | 448 } |
715 | 449 |
716 Future _runScheduled(Queue<_ScheduledEvent> scheduled) { | 450 /// Return the name for the package described by [description] and from |
717 if (scheduled == null) return new Future.immediate(null); | 451 /// [sourceName]. |
718 | 452 String _packageName(String sourceName, description) { |
719 Future runNextEvent(_) { | 453 switch (sourceName) { |
720 if (_abortScheduled || scheduled.isEmpty) { | 454 case "git": |
721 _abortScheduled = false; | 455 var url = description is String ? description : description['url']; |
722 return new Future.immediate(null); | 456 // TODO(rnystrom): Using path.basename on a URL is hacky. If we add URL |
723 } | 457 // support to pkg/pathos, should use an explicit builder for that. |
724 | 458 return path.basename(url.replaceFirst(new RegExp(r"(\.git)?/?$"), "")); |
725 var future = scheduled.removeFirst()(_sandboxDir); | 459 case "hosted": |
726 if (future != null) { | 460 if (description is String) return description; |
727 return future.then(runNextEvent); | 461 return description['name']; |
728 } else { | 462 case "path": |
729 return runNextEvent(null); | 463 return path.basename(description); |
730 } | 464 case "sdk": |
| 465 return description; |
| 466 default: |
| 467 return description; |
731 } | 468 } |
732 | |
733 return runNextEvent(null); | |
734 } | 469 } |
735 | 470 |
736 /// Compares the [actual] output from running pub with [expected]. For [String] | 471 /// Compares the [actual] output from running pub with [expected]. For [String] |
737 /// patterns, ignores leading and trailing whitespace differences and tries to | 472 /// patterns, ignores leading and trailing whitespace differences and tries to |
738 /// report the offending difference in a nice way. For other [Pattern]s, just | 473 /// report the offending difference in a nice way. For other [Pattern]s, just |
739 /// reports whether the output contained the pattern. | 474 /// reports whether the output contained the pattern. |
740 void _validateOutput(List<String> failures, String pipe, Pattern expected, | 475 void _validateOutput(List<String> failures, String pipe, Pattern expected, |
741 List<String> actual) { | 476 List<String> actual) { |
742 if (expected == null) return; | 477 if (expected == null) return; |
743 | 478 |
(...skipping 59 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
803 | 538 |
804 // If any lines mismatched, show the expected and actual. | 539 // If any lines mismatched, show the expected and actual. |
805 if (failed) { | 540 if (failed) { |
806 failures.add('Expected $pipe:'); | 541 failures.add('Expected $pipe:'); |
807 failures.addAll(expected.map((line) => '| $line')); | 542 failures.addAll(expected.map((line) => '| $line')); |
808 failures.add('Got:'); | 543 failures.add('Got:'); |
809 failures.addAll(results); | 544 failures.addAll(results); |
810 } | 545 } |
811 } | 546 } |
812 | 547 |
813 /// Base class for [FileDescriptor] and [DirectoryDescriptor] so that a | |
814 /// directory can contain a heterogeneous collection of files and | |
815 /// subdirectories. | |
816 abstract class Descriptor { | |
817 /// The name of this file or directory. This must be a [String] if the file | |
818 /// or directory is going to be created. | |
819 final Pattern name; | |
820 | |
821 Descriptor(this.name); | |
822 | |
823 /// Creates the file or directory within [dir]. Returns a [Future] that is | |
824 /// completed after the creation is done. | |
825 Future create(dir); | |
826 | |
827 /// Validates that this descriptor correctly matches the corresponding file | |
828 /// system entry within [dir]. Returns a [Future] that completes to `null` if | |
829 /// the entry is valid, or throws an error if it failed. | |
830 Future validate(String dir); | |
831 | |
832 /// Deletes the file or directory within [dir]. Returns a [Future] that is | |
833 /// completed after the deletion is done. | |
834 Future delete(String dir); | |
835 | |
836 /// Loads the file at [path] from within this descriptor. If [path] is empty, | |
837 /// loads the contents of the descriptor itself. | |
838 ByteStream load(List<String> path); | |
839 | |
840 /// Schedules the directory to be created before Pub is run with | |
841 /// [schedulePub]. The directory will be created relative to the sandbox | |
842 /// directory. | |
843 // TODO(nweiz): Use implicit closurization once issue 2984 is fixed. | |
844 void scheduleCreate() => _schedule((dir) => this.create(dir)); | |
845 | |
846 /// Schedules the file or directory to be deleted recursively. | |
847 void scheduleDelete() => _schedule((dir) => this.delete(dir)); | |
848 | |
849 /// Schedules the directory to be validated after Pub is run with | |
850 /// [schedulePub]. The directory will be validated relative to the sandbox | |
851 /// directory. | |
852 void scheduleValidate() => _schedule((parentDir) => validate(parentDir)); | |
853 | |
854 /// Asserts that the name of the descriptor is a [String] and returns it. | |
855 String get _stringName { | |
856 if (name is String) return name; | |
857 throw 'Pattern $name must be a string.'; | |
858 } | |
859 | |
860 /// Validates that at least one file in [dir] matching [name] is valid | |
861 /// according to [validate]. [validate] should throw or complete to an | |
862 /// exception if the input path is invalid. | |
863 Future _validateOneMatch(String dir, Future validate(String entry)) { | |
864 // Special-case strings to support multi-level names like "myapp/packages". | |
865 if (name is String) { | |
866 var entry = path.join(dir, name); | |
867 return defer(() { | |
868 if (!entryExists(entry)) { | |
869 throw new TestFailure('Entry $entry not found.'); | |
870 } | |
871 return validate(entry); | |
872 }); | |
873 } | |
874 | |
875 // TODO(nweiz): remove this when issue 4061 is fixed. | |
876 var stackTrace; | |
877 try { | |
878 throw ""; | |
879 } catch (_, localStackTrace) { | |
880 stackTrace = localStackTrace; | |
881 } | |
882 | |
883 return listDir(dir).then((files) { | |
884 var matches = files.where((file) => endsWithPattern(file, name)).toList(); | |
885 if (matches.isEmpty) { | |
886 throw new TestFailure('No files in $dir match pattern $name.'); | |
887 } | |
888 if (matches.length == 1) return validate(matches[0]); | |
889 | |
890 var failures = []; | |
891 var successes = 0; | |
892 var completer = new Completer(); | |
893 checkComplete() { | |
894 if (failures.length + successes != matches.length) return; | |
895 if (successes > 0) { | |
896 completer.complete(); | |
897 return; | |
898 } | |
899 | |
900 var error = new StringBuffer(); | |
901 error.write("No files named $name in $dir were valid:\n"); | |
902 for (var failure in failures) { | |
903 error.write(" $failure\n"); | |
904 } | |
905 completer.completeError( | |
906 new TestFailure(error.toString()), stackTrace); | |
907 } | |
908 | |
909 for (var match in matches) { | |
910 var future = validate(match).then((_) { | |
911 successes++; | |
912 checkComplete(); | |
913 }).catchError((e) { | |
914 failures.add(e); | |
915 checkComplete(); | |
916 }); | |
917 } | |
918 return completer.future; | |
919 }); | |
920 } | |
921 } | |
922 | |
923 /// Describes a file. These are used both for setting up an expected directory | |
924 /// tree before running a test, and for validating that the file system matches | |
925 /// some expectations after running it. | |
926 class FileDescriptor extends Descriptor { | |
927 /// The contents of the file, in bytes. | |
928 final List<int> contents; | |
929 | |
930 String get textContents => new String.fromCharCodes(contents); | |
931 | |
932 FileDescriptor.bytes(Pattern name, this.contents) : super(name); | |
933 | |
934 FileDescriptor(Pattern name, String contents) : | |
935 this.bytes(name, encodeUtf8(contents)); | |
936 | |
937 /// Creates the file within [dir]. Returns a [Future] that is completed after | |
938 /// the creation is done. | |
939 Future<String> create(dir) => | |
940 defer(() => writeBinaryFile(path.join(dir, _stringName), contents)); | |
941 | |
942 /// Deletes the file within [dir]. Returns a [Future] that is completed after | |
943 /// the deletion is done. | |
944 Future delete(dir) => | |
945 defer(() => deleteFile(path.join(dir, _stringName))); | |
946 | |
947 /// Validates that this file correctly matches the actual file at [path]. | |
948 Future validate(String path) { | |
949 return _validateOneMatch(path, (file) { | |
950 var text = readTextFile(file); | |
951 if (text == textContents) return null; | |
952 | |
953 throw new TestFailure( | |
954 'File $file should contain:\n\n$textContents\n\n' | |
955 'but contained:\n\n$text'); | |
956 }); | |
957 } | |
958 | |
959 /// Loads the contents of the file. | |
960 ByteStream load(List<String> path) { | |
961 if (!path.isEmpty) { | |
962 throw "Can't load ${path.join('/')} from within $name: not a directory."; | |
963 } | |
964 | |
965 return new ByteStream.fromBytes(contents); | |
966 } | |
967 } | |
968 | |
969 /// Describes a directory and its contents. These are used both for setting up | |
970 /// an expected directory tree before running a test, and for validating that | |
971 /// the file system matches some expectations after running it. | |
972 class DirectoryDescriptor extends Descriptor { | |
973 /// The files and directories contained in this directory. | |
974 final List<Descriptor> contents; | |
975 | |
976 DirectoryDescriptor(Pattern name, List<Descriptor> contents) | |
977 : this.contents = contents == null ? <Descriptor>[] : contents, | |
978 super(name); | |
979 | |
980 /// Creates the file within [dir]. Returns a [Future] that is completed after | |
981 /// the creation is done. | |
982 Future<String> create(parentDir) { | |
983 return defer(() { | |
984 // Create the directory. | |
985 var dir = ensureDir(path.join(parentDir, _stringName)); | |
986 if (contents == null) return dir; | |
987 | |
988 // Recursively create all of its children. | |
989 var childFutures = contents.map((child) => child.create(dir)).toList(); | |
990 // Only complete once all of the children have been created too. | |
991 return Future.wait(childFutures).then((_) => dir); | |
992 }); | |
993 } | |
994 | |
995 /// Deletes the directory within [dir]. Returns a [Future] that is completed | |
996 /// after the deletion is done. | |
997 Future delete(dir) { | |
998 return deleteDir(path.join(dir, _stringName)); | |
999 } | |
1000 | |
1001 /// Validates that the directory at [path] contains all of the expected | |
1002 /// contents in this descriptor. Note that this does *not* check that the | |
1003 /// directory doesn't contain other unexpected stuff, just that it *does* | |
1004 /// contain the stuff we do expect. | |
1005 Future validate(String path) { | |
1006 return _validateOneMatch(path, (dir) { | |
1007 // Validate each of the items in this directory. | |
1008 final entryFutures = | |
1009 contents.map((entry) => entry.validate(dir)).toList(); | |
1010 | |
1011 // If they are all valid, the directory is valid. | |
1012 return Future.wait(entryFutures).then((entries) => null); | |
1013 }); | |
1014 } | |
1015 | |
1016 /// Loads [path] from within this directory. | |
1017 ByteStream load(List<String> path) { | |
1018 if (path.isEmpty) { | |
1019 throw "Can't load the contents of $name: is a directory."; | |
1020 } | |
1021 | |
1022 for (var descriptor in contents) { | |
1023 if (descriptor.name == path[0]) { | |
1024 return descriptor.load(path.sublist(1)); | |
1025 } | |
1026 } | |
1027 | |
1028 throw "Directory $name doesn't contain ${path.join('/')}."; | |
1029 } | |
1030 } | |
1031 | |
1032 /// Wraps a [Future] that will complete to a [Descriptor] and makes it behave | |
1033 /// like a concrete [Descriptor]. This is necessary when the contents of the | |
1034 /// descriptor depends on information that's not available until part of the | |
1035 /// test run is completed. | |
1036 class FutureDescriptor extends Descriptor { | |
1037 Future<Descriptor> _future; | |
1038 | |
1039 FutureDescriptor(this._future) : super('<unknown>'); | |
1040 | |
1041 Future create(dir) => _future.then((desc) => desc.create(dir)); | |
1042 | |
1043 Future validate(dir) => _future.then((desc) => desc.validate(dir)); | |
1044 | |
1045 Future delete(dir) => _future.then((desc) => desc.delete(dir)); | |
1046 | |
1047 ByteStream load(List<String> path) { | |
1048 var controller = new StreamController<List<int>>(); | |
1049 _future.then((desc) => store(desc.load(path), controller)); | |
1050 return new ByteStream(controller.stream); | |
1051 } | |
1052 } | |
1053 | |
1054 /// Describes a Git repository and its contents. | |
1055 class GitRepoDescriptor extends DirectoryDescriptor { | |
1056 GitRepoDescriptor(Pattern name, List<Descriptor> contents) | |
1057 : super(name, contents); | |
1058 | |
1059 /// Creates the Git repository and commits the contents. | |
1060 Future create(parentDir) { | |
1061 return _runGitCommands(parentDir, [ | |
1062 ['init'], | |
1063 ['add', '.'], | |
1064 ['commit', '-m', 'initial commit'] | |
1065 ]); | |
1066 } | |
1067 | |
1068 /// Commits any changes to the Git repository. | |
1069 Future commit(parentDir) { | |
1070 return _runGitCommands(parentDir, [ | |
1071 ['add', '.'], | |
1072 ['commit', '-m', 'update'] | |
1073 ]); | |
1074 } | |
1075 | |
1076 /// Schedules changes to be committed to the Git repository. | |
1077 void scheduleCommit() => _schedule((dir) => this.commit(dir)); | |
1078 | |
1079 /// Return a Future that completes to the commit in the git repository | |
1080 /// referred to by [ref] at the current point in the scheduled test run. | |
1081 Future<String> revParse(String ref) { | |
1082 return _scheduleValue((parentDir) { | |
1083 return super.create(parentDir).then((rootDir) { | |
1084 return _runGit(['rev-parse', ref], rootDir); | |
1085 }).then((output) => output[0]); | |
1086 }); | |
1087 } | |
1088 | |
1089 /// Schedule a Git command to run in this repository. | |
1090 void scheduleGit(List<String> args) { | |
1091 _schedule((parentDir) => _runGit(args, path.join(parentDir, name))); | |
1092 } | |
1093 | |
1094 Future _runGitCommands(parentDir, List<List<String>> commands) { | |
1095 var workingDir; | |
1096 | |
1097 Future runGitStep(_) { | |
1098 if (commands.isEmpty) return new Future.immediate(workingDir); | |
1099 var command = commands.removeAt(0); | |
1100 return _runGit(command, workingDir).then(runGitStep); | |
1101 } | |
1102 | |
1103 return super.create(parentDir).then((rootDir) { | |
1104 workingDir = rootDir; | |
1105 return runGitStep(null); | |
1106 }); | |
1107 } | |
1108 | |
1109 Future<List<String>> _runGit(List<String> args, String workingDir) { | |
1110 // Explicitly specify the committer information. Git needs this to commit | |
1111 // and we don't want to rely on the buildbots having this already set up. | |
1112 var environment = { | |
1113 'GIT_AUTHOR_NAME': 'Pub Test', | |
1114 'GIT_AUTHOR_EMAIL': 'pub@dartlang.org', | |
1115 'GIT_COMMITTER_NAME': 'Pub Test', | |
1116 'GIT_COMMITTER_EMAIL': 'pub@dartlang.org' | |
1117 }; | |
1118 | |
1119 return gitlib.run(args, workingDir: workingDir, environment: environment); | |
1120 } | |
1121 } | |
1122 | |
1123 /// Describes a gzipped tar file and its contents. | |
1124 class TarFileDescriptor extends Descriptor { | |
1125 final List<Descriptor> contents; | |
1126 | |
1127 TarFileDescriptor(Pattern name, this.contents) | |
1128 : super(name); | |
1129 | |
1130 /// Creates the files and directories within this tar file, then archives | |
1131 /// them, compresses them, and saves the result to [parentDir]. | |
1132 Future<String> create(parentDir) { | |
1133 return withTempDir((tempDir) { | |
1134 return Future.wait(contents.map((child) => child.create(tempDir))) | |
1135 .then((createdContents) { | |
1136 return createTarGz(createdContents, baseDir: tempDir).toBytes(); | |
1137 }).then((bytes) { | |
1138 var file = path.join(parentDir, _stringName); | |
1139 writeBinaryFile(file, bytes); | |
1140 return file; | |
1141 }); | |
1142 }); | |
1143 } | |
1144 | |
1145 /// Validates that the `.tar.gz` file at [path] contains the expected | |
1146 /// contents. | |
1147 Future validate(String path) { | |
1148 throw "TODO(nweiz): implement this"; | |
1149 } | |
1150 | |
1151 Future delete(dir) { | |
1152 throw new UnsupportedError(''); | |
1153 } | |
1154 | |
1155 /// Loads the contents of this tar file. | |
1156 ByteStream load(List<String> path) { | |
1157 if (!path.isEmpty) { | |
1158 throw "Can't load ${path.join('/')} from within $name: not a directory."; | |
1159 } | |
1160 | |
1161 var controller = new StreamController<List<int>>(); | |
1162 // TODO(nweiz): propagate any errors to the return value. See issue 3657. | |
1163 withTempDir((tempDir) { | |
1164 return create(tempDir).then((tar) { | |
1165 var sourceStream = new File(tar).openRead(); | |
1166 return store(sourceStream, controller); | |
1167 }); | |
1168 }); | |
1169 return new ByteStream(controller.stream); | |
1170 } | |
1171 } | |
1172 | |
1173 /// A descriptor that validates that no file or directory exists with the given | |
1174 /// name. | |
1175 class NothingDescriptor extends Descriptor { | |
1176 NothingDescriptor(String name) : super(name); | |
1177 | |
1178 Future create(dir) => new Future.immediate(null); | |
1179 Future delete(dir) => new Future.immediate(null); | |
1180 | |
1181 Future validate(String dir) { | |
1182 return defer(() { | |
1183 if (entryExists(path.join(dir, name))) { | |
1184 throw new TestFailure('Entry $name in $dir should not exist.'); | |
1185 } | |
1186 }); | |
1187 } | |
1188 | |
1189 ByteStream load(List<String> path) { | |
1190 if (path.isEmpty) { | |
1191 throw "Can't load the contents of $name: it doesn't exist."; | |
1192 } else { | |
1193 throw "Can't load ${path.join('/')} from within $name: $name doesn't " | |
1194 "exist."; | |
1195 } | |
1196 } | |
1197 } | |
1198 | |
1199 /// A function that creates a [Validator] subclass. | 548 /// A function that creates a [Validator] subclass. |
1200 typedef Validator ValidatorCreator(Entrypoint entrypoint); | 549 typedef Validator ValidatorCreator(Entrypoint entrypoint); |
1201 | 550 |
1202 /// Schedules a single [Validator] to run on the [appPath]. Returns a scheduled | 551 /// Schedules a single [Validator] to run on the [appPath]. Returns a scheduled |
1203 /// Future that contains the errors and warnings produced by that validator. | 552 /// Future that contains the errors and warnings produced by that validator. |
1204 Future<Pair<List<String>, List<String>>> schedulePackageValidation( | 553 Future<Pair<List<String>, List<String>>> schedulePackageValidation( |
1205 ValidatorCreator fn) { | 554 ValidatorCreator fn) { |
1206 return _scheduleValue((sandboxDir) { | 555 return schedule(() { |
1207 var cache = new SystemCache.withSources(path.join(sandboxDir, cachePath)); | 556 var cache = new SystemCache.withSources(path.join(sandboxDir, cachePath)); |
1208 | 557 |
1209 return defer(() { | 558 return defer(() { |
1210 var validator = fn(new Entrypoint(path.join(sandboxDir, appPath), cache)); | 559 var validator = fn(new Entrypoint(path.join(sandboxDir, appPath), cache)); |
1211 return validator.validate().then((_) { | 560 return validator.validate().then((_) { |
1212 return new Pair(validator.errors, validator.warnings); | 561 return new Pair(validator.errors, validator.warnings); |
1213 }); | 562 }); |
1214 }); | 563 }); |
1215 }); | 564 }, "validating package"); |
1216 } | 565 } |
1217 | 566 |
1218 /// A matcher that matches a Pair. | 567 /// A matcher that matches a Pair. |
1219 Matcher pairOf(Matcher firstMatcher, Matcher lastMatcher) => | 568 Matcher pairOf(Matcher firstMatcher, Matcher lastMatcher) => |
1220 new _PairMatcher(firstMatcher, lastMatcher); | 569 new _PairMatcher(firstMatcher, lastMatcher); |
1221 | 570 |
1222 class _PairMatcher extends BaseMatcher { | 571 class _PairMatcher extends BaseMatcher { |
1223 final Matcher _firstMatcher; | 572 final Matcher _firstMatcher; |
1224 final Matcher _lastMatcher; | 573 final Matcher _lastMatcher; |
1225 | 574 |
1226 _PairMatcher(this._firstMatcher, this._lastMatcher); | 575 _PairMatcher(this._firstMatcher, this._lastMatcher); |
1227 | 576 |
1228 bool matches(item, MatchState matchState) { | 577 bool matches(item, MatchState matchState) { |
1229 if (item is! Pair) return false; | 578 if (item is! Pair) return false; |
1230 return _firstMatcher.matches(item.first, matchState) && | 579 return _firstMatcher.matches(item.first, matchState) && |
1231 _lastMatcher.matches(item.last, matchState); | 580 _lastMatcher.matches(item.last, matchState); |
1232 } | 581 } |
1233 | 582 |
1234 Description describe(Description description) { | 583 Description describe(Description description) { |
1235 description.addAll("(", ", ", ")", [_firstMatcher, _lastMatcher]); | 584 description.addAll("(", ", ", ")", [_firstMatcher, _lastMatcher]); |
1236 } | 585 } |
1237 } | 586 } |
1238 | |
1239 /// The time (in milliseconds) to wait for scheduled events that could run | |
1240 /// forever. | |
1241 const _SCHEDULE_TIMEOUT = 10000; | |
1242 | |
1243 /// A class representing a [Process] that is scheduled to run in the course of | |
1244 /// the test. This class allows actions on the process to be scheduled | |
1245 /// synchronously. All operations on this class are scheduled. | |
1246 /// | |
1247 /// Before running the test, either [shouldExit] or [kill] must be called on | |
1248 /// this to ensure that the process terminates when expected. | |
1249 /// | |
1250 /// If the test fails, this will automatically print out any remaining stdout | |
1251 /// and stderr from the process to aid debugging. | |
1252 class ScheduledProcess { | |
1253 /// The name of the process. Used for error reporting. | |
1254 final String name; | |
1255 | |
1256 /// The process future that's scheduled to run. | |
1257 Future<PubProcess> _processFuture; | |
1258 | |
1259 /// The process that's scheduled to run. It may be null. | |
1260 PubProcess _process; | |
1261 | |
1262 /// The exit code of the scheduled program. It may be null. | |
1263 int _exitCode; | |
1264 | |
1265 /// A future that will complete to a list of all the lines emitted on the | |
1266 /// process's standard output stream. This is independent of what data is read | |
1267 /// from [_stdout]. | |
1268 Future<List<String>> _stdoutLines; | |
1269 | |
1270 /// A [Stream] of stdout lines emitted by the process that's scheduled to run. | |
1271 /// It may be null. | |
1272 Stream<String> _stdout; | |
1273 | |
1274 /// A [Future] that will resolve to [_stdout] once it's available. | |
1275 Future get _stdoutFuture => _processFuture.then((_) => _stdout); | |
1276 | |
1277 /// A [StreamSubscription] that controls [_stdout]. | |
1278 StreamSubscription _stdoutSubscription; | |
1279 | |
1280 /// A future that will complete to a list of all the lines emitted on the | |
1281 /// process's standard error stream. This is independent of what data is read | |
1282 /// from [_stderr]. | |
1283 Future<List<String>> _stderrLines; | |
1284 | |
1285 /// A [Stream] of stderr lines emitted by the process that's scheduled to run. | |
1286 /// It may be null. | |
1287 Stream<String> _stderr; | |
1288 | |
1289 /// A [Future] that will resolve to [_stderr] once it's available. | |
1290 Future get _stderrFuture => _processFuture.then((_) => _stderr); | |
1291 | |
1292 /// A [StreamSubscription] that controls [_stderr]. | |
1293 StreamSubscription _stderrSubscription; | |
1294 | |
1295 /// The exit code of the process that's scheduled to run. This will naturally | |
1296 /// only complete once the process has terminated. | |
1297 Future<int> get _exitCodeFuture => _exitCodeCompleter.future; | |
1298 | |
1299 /// The completer for [_exitCode]. | |
1300 final Completer<int> _exitCodeCompleter = new Completer(); | |
1301 | |
1302 /// Whether the user has scheduled the end of this process by calling either | |
1303 /// [shouldExit] or [kill]. | |
1304 bool _endScheduled = false; | |
1305 | |
1306 /// Whether the process is expected to terminate at this point. | |
1307 bool _endExpected = false; | |
1308 | |
1309 /// Wraps a [Process] [Future] in a scheduled process. | |
1310 ScheduledProcess(this.name, Future<PubProcess> process) | |
1311 : _processFuture = process { | |
1312 var pairFuture = process.then((p) { | |
1313 _process = p; | |
1314 | |
1315 byteStreamToLines(stream) { | |
1316 return streamToLines(new ByteStream(stream.handleError((e) { | |
1317 registerException(e.error, e.stackTrace); | |
1318 })).toStringStream()); | |
1319 } | |
1320 | |
1321 var stdoutTee = tee(byteStreamToLines(p.stdout)); | |
1322 var stdoutPair = streamWithSubscription(stdoutTee.last); | |
1323 _stdout = stdoutPair.first; | |
1324 _stdoutSubscription = stdoutPair.last; | |
1325 | |
1326 var stderrTee = tee(byteStreamToLines(p.stderr)); | |
1327 var stderrPair = streamWithSubscription(stderrTee.last); | |
1328 _stderr = stderrPair.first; | |
1329 _stderrSubscription = stderrPair.last; | |
1330 | |
1331 return new Pair(stdoutTee.first, stderrTee.first); | |
1332 }); | |
1333 | |
1334 _stdoutLines = pairFuture.then((pair) => pair.first.toList()); | |
1335 _stderrLines = pairFuture.then((pair) => pair.last.toList()); | |
1336 | |
1337 _schedule((_) { | |
1338 if (!_endScheduled) { | |
1339 throw new StateError("Scheduled process $name must have shouldExit() " | |
1340 "or kill() called before the test is run."); | |
1341 } | |
1342 | |
1343 process.then((p) => p.exitCode).then((exitCode) { | |
1344 if (_endExpected) { | |
1345 _exitCode = exitCode; | |
1346 _exitCodeCompleter.complete(exitCode); | |
1347 return; | |
1348 } | |
1349 | |
1350 // Sleep for half a second in case _endExpected is set in the next | |
1351 // scheduled event. | |
1352 return sleep(500).then((_) { | |
1353 if (_endExpected) { | |
1354 _exitCodeCompleter.complete(exitCode); | |
1355 return; | |
1356 } | |
1357 | |
1358 return _printStreams(); | |
1359 }).then((_) { | |
1360 registerException(new TestFailure("Process $name ended " | |
1361 "earlier than scheduled with exit code $exitCode")); | |
1362 }); | |
1363 }).catchError((e) => registerException(e.error, e.stackTrace)); | |
1364 }); | |
1365 | |
1366 _scheduleOnException((_) { | |
1367 if (_process == null) return; | |
1368 | |
1369 if (_exitCode == null) { | |
1370 print("\nKilling process $name prematurely."); | |
1371 _endExpected = true; | |
1372 _process.kill(); | |
1373 } | |
1374 | |
1375 return _printStreams(); | |
1376 }); | |
1377 | |
1378 _scheduleCleanup((_) { | |
1379 if (_process == null) return; | |
1380 // Ensure that the process is dead and we aren't waiting on any IO. | |
1381 _process.kill(); | |
1382 _stdoutSubscription.cancel(); | |
1383 _stderrSubscription.cancel(); | |
1384 }); | |
1385 } | |
1386 | |
1387 /// Reads the next line of stdout from the process. | |
1388 Future<String> nextLine() { | |
1389 return _scheduleValue((_) { | |
1390 return timeout(_stdoutFuture.then((stream) => streamFirst(stream)), | |
1391 _SCHEDULE_TIMEOUT, | |
1392 "waiting for the next stdout line from process $name"); | |
1393 }); | |
1394 } | |
1395 | |
1396 /// Reads the next line of stderr from the process. | |
1397 Future<String> nextErrLine() { | |
1398 return _scheduleValue((_) { | |
1399 return timeout(_stderrFuture.then((stream) => streamFirst(stream)), | |
1400 _SCHEDULE_TIMEOUT, | |
1401 "waiting for the next stderr line from process $name"); | |
1402 }); | |
1403 } | |
1404 | |
1405 /// Reads the remaining stdout from the process. This should only be called | |
1406 /// after kill() or shouldExit(). | |
1407 Future<String> remainingStdout() { | |
1408 if (!_endScheduled) { | |
1409 throw new StateError("remainingStdout() should only be called after " | |
1410 "kill() or shouldExit()."); | |
1411 } | |
1412 | |
1413 return _scheduleValue((_) { | |
1414 return timeout(_stdoutFuture.then((stream) => stream.toList()) | |
1415 .then((lines) => lines.join("\n")), | |
1416 _SCHEDULE_TIMEOUT, | |
1417 "waiting for the last stdout line from process $name"); | |
1418 }); | |
1419 } | |
1420 | |
1421 /// Reads the remaining stderr from the process. This should only be called | |
1422 /// after kill() or shouldExit(). | |
1423 Future<String> remainingStderr() { | |
1424 if (!_endScheduled) { | |
1425 throw new StateError("remainingStderr() should only be called after " | |
1426 "kill() or shouldExit()."); | |
1427 } | |
1428 | |
1429 return _scheduleValue((_) { | |
1430 return timeout(_stderrFuture.then((stream) => stream.toList()) | |
1431 .then((lines) => lines.join("\n")), | |
1432 _SCHEDULE_TIMEOUT, | |
1433 "waiting for the last stderr line from process $name"); | |
1434 }); | |
1435 } | |
1436 | |
1437 /// Writes [line] to the process as stdin. | |
1438 void writeLine(String line) { | |
1439 _schedule((_) => _processFuture.then( | |
1440 (p) => p.stdin.add(encodeUtf8('$line\n')))); | |
1441 } | |
1442 | |
1443 /// Kills the process, and waits until it's dead. | |
1444 void kill() { | |
1445 _endScheduled = true; | |
1446 _schedule((_) { | |
1447 _endExpected = true; | |
1448 _process.kill(); | |
1449 timeout(_exitCodeFuture, _SCHEDULE_TIMEOUT, | |
1450 "waiting for process $name to die"); | |
1451 }); | |
1452 } | |
1453 | |
1454 /// Waits for the process to exit, and verifies that the exit code matches | |
1455 /// [expectedExitCode] (if given). | |
1456 void shouldExit([int expectedExitCode]) { | |
1457 _endScheduled = true; | |
1458 _schedule((_) { | |
1459 _endExpected = true; | |
1460 return timeout(_exitCodeFuture, _SCHEDULE_TIMEOUT, | |
1461 "waiting for process $name to exit").then((exitCode) { | |
1462 if (expectedExitCode != null) { | |
1463 expect(exitCode, equals(expectedExitCode)); | |
1464 } | |
1465 }); | |
1466 }); | |
1467 } | |
1468 | |
1469 /// Prints the remaining data in the process's stdout and stderr streams. | |
1470 /// Prints nothing if the streams are empty. | |
1471 Future _printStreams() { | |
1472 void printStream(String streamName, List<String> lines) { | |
1473 if (lines.isEmpty) return; | |
1474 | |
1475 print('\nProcess $name $streamName:'); | |
1476 for (var line in lines) { | |
1477 print('| $line'); | |
1478 } | |
1479 } | |
1480 | |
1481 return _stdoutLines.then((stdoutLines) { | |
1482 printStream('stdout', stdoutLines); | |
1483 return _stderrLines.then((stderrLines) { | |
1484 printStream('stderr', stderrLines); | |
1485 }); | |
1486 }); | |
1487 } | |
1488 } | |
1489 | |
1490 /// A class representing an [HttpServer] that's scheduled to run in the course | |
1491 /// of the test. This class allows the server's request handling to be | |
1492 /// scheduled synchronously. All operations on this class are scheduled. | |
1493 class ScheduledServer { | |
1494 /// The wrapped server. | |
1495 final Future<HttpServer> _server; | |
1496 | |
1497 /// The queue of handlers to run for upcoming requests. | |
1498 final _handlers = new Queue<Future>(); | |
1499 | |
1500 /// The requests to be ignored. | |
1501 final _ignored = new Set<Pair<String, String>>(); | |
1502 | |
1503 ScheduledServer._(this._server); | |
1504 | |
1505 /// Creates a new server listening on an automatically-allocated port on | |
1506 /// localhost. | |
1507 factory ScheduledServer() { | |
1508 var scheduledServer; | |
1509 scheduledServer = new ScheduledServer._(_scheduleValue((_) { | |
1510 return SafeHttpServer.bind("127.0.0.1", 0).then((server) { | |
1511 server.listen(scheduledServer._awaitHandle); | |
1512 _scheduleCleanup((_) => server.close()); | |
1513 return server; | |
1514 }); | |
1515 })); | |
1516 return scheduledServer; | |
1517 } | |
1518 | |
1519 /// The port on which the server is listening. | |
1520 Future<int> get port => _server.then((s) => s.port); | |
1521 | |
1522 /// The base URL of the server, including its port. | |
1523 Future<Uri> get url => | |
1524 port.then((p) => Uri.parse("http://localhost:$p")); | |
1525 | |
1526 /// Assert that the next request has the given [method] and [path], and pass | |
1527 /// it to [handler] to handle. If [handler] returns a [Future], wait until | |
1528 /// it's completed to continue the schedule. | |
1529 void handle(String method, String path, | |
1530 Future handler(HttpRequest request, HttpResponse response)) { | |
1531 var handlerCompleter = new Completer<Function>(); | |
1532 _scheduleValue((_) { | |
1533 var requestCompleteCompleter = new Completer(); | |
1534 handlerCompleter.complete((request, response) { | |
1535 expect(request.method, equals(method)); | |
1536 expect(request.uri.path, equals(path)); | |
1537 | |
1538 var future = handler(request, response); | |
1539 if (future == null) future = new Future.immediate(null); | |
1540 chainToCompleter(future, requestCompleteCompleter); | |
1541 }); | |
1542 return timeout(requestCompleteCompleter.future, | |
1543 _SCHEDULE_TIMEOUT, "waiting for $method $path"); | |
1544 }); | |
1545 _handlers.add(handlerCompleter.future); | |
1546 } | |
1547 | |
1548 /// Ignore all requests with the given [method] and [path]. If one is | |
1549 /// received, don't respond to it. | |
1550 void ignore(String method, String path) => | |
1551 _ignored.add(new Pair(method, path)); | |
1552 | |
1553 /// Raises an error complaining of an unexpected request. | |
1554 void _awaitHandle(HttpRequest request) { | |
1555 HttpResponse response = request.response; | |
1556 if (_ignored.contains(new Pair(request.method, request.uri.path))) return; | |
1557 var future = timeout(defer(() { | |
1558 if (_handlers.isEmpty) { | |
1559 fail('Unexpected ${request.method} request to ${request.uri.path}.'); | |
1560 } | |
1561 return _handlers.removeFirst(); | |
1562 }).then((handler) { | |
1563 handler(request, response); | |
1564 }), _SCHEDULE_TIMEOUT, "waiting for a handler for ${request.method} " | |
1565 "${request.uri.path}"); | |
1566 expect(future, completes); | |
1567 } | |
1568 } | |
1569 | |
1570 /// Takes a simple data structure (composed of [Map]s, [List]s, scalar objects, | |
1571 /// and [Future]s) and recursively resolves all the [Future]s contained within. | |
1572 /// Completes with the fully resolved structure. | |
1573 Future _awaitObject(object) { | |
1574 // Unroll nested futures. | |
1575 if (object is Future) return object.then(_awaitObject); | |
1576 if (object is Collection) { | |
1577 return Future.wait(object.map(_awaitObject).toList()); | |
1578 } | |
1579 if (object is! Map) return new Future.immediate(object); | |
1580 | |
1581 var pairs = <Future<Pair>>[]; | |
1582 object.forEach((key, value) { | |
1583 pairs.add(_awaitObject(value) | |
1584 .then((resolved) => new Pair(key, resolved))); | |
1585 }); | |
1586 return Future.wait(pairs).then((resolvedPairs) { | |
1587 var map = {}; | |
1588 for (var pair in resolvedPairs) { | |
1589 map[pair.first] = pair.last; | |
1590 } | |
1591 return map; | |
1592 }); | |
1593 } | |
1594 | |
1595 /// Schedules a callback to be called as part of the test case. | |
1596 void _schedule(_ScheduledEvent event) { | |
1597 if (_scheduled == null) _scheduled = new Queue(); | |
1598 _scheduled.addLast(event); | |
1599 } | |
1600 | |
1601 /// Like [_schedule], but pipes the return value of [event] to a returned | |
1602 /// [Future]. | |
1603 Future _scheduleValue(_ScheduledEvent event) { | |
1604 var completer = new Completer(); | |
1605 _schedule((parentDir) { | |
1606 chainToCompleter(event(parentDir), completer); | |
1607 return completer.future; | |
1608 }); | |
1609 return completer.future; | |
1610 } | |
1611 | |
1612 /// Schedules a callback to be called after the test case has completed, even | |
1613 /// if it failed. | |
1614 void _scheduleCleanup(_ScheduledEvent event) { | |
1615 if (_scheduledCleanup == null) _scheduledCleanup = new Queue(); | |
1616 _scheduledCleanup.addLast(event); | |
1617 } | |
1618 | |
1619 /// Schedules a callback to be called after the test case has completed, but | |
1620 /// only if it failed. | |
1621 void _scheduleOnException(_ScheduledEvent event) { | |
1622 if (_scheduledOnException == null) _scheduledOnException = new Queue(); | |
1623 _scheduledOnException.addLast(event); | |
1624 } | |
1625 | |
1626 /// Like [expect], but for [Future]s that complete as part of the scheduled | |
1627 /// test. This is necessary to ensure that the exception thrown by the | |
1628 /// expectation failing is handled by the scheduler. | |
1629 /// | |
1630 /// Note that [matcher] matches against the completed value of [actual], so | |
1631 /// calling [completion] is unnecessary. | |
1632 void expectLater(Future actual, matcher, {String reason, | |
1633 FailureHandler failureHandler, bool verbose: false}) { | |
1634 _schedule((_) { | |
1635 return actual.then((value) { | |
1636 expect(value, matcher, reason: reason, failureHandler: failureHandler, | |
1637 verbose: false); | |
1638 }); | |
1639 }); | |
1640 } | |
OLD | NEW |