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

Side by Side Diff: utils/tests/pub/test_pub.dart

Issue 12437022: Use scheduled_test for Pub tests. (Closed) Base URL: https://dart.googlecode.com/svn/branches/bleeding_edge/dart
Patch Set: re-upload Created 7 years, 9 months 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) 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
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 }
OLDNEW
« no previous file with comments | « utils/tests/pub/sdk_constraint_test.dart ('k') | utils/tests/pub/update/git/do_not_update_if_unneeded_test.dart » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698