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

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

Issue 10874039: Use ephemeral ports for serving stuff during the pub tests. (Closed) Base URL: https://dart.googlecode.com/svn/branches/bleeding_edge/dart
Patch Set: Code review changes Created 8 years, 3 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
« no previous file with comments | « utils/tests/pub/pub_test.dart ('k') | no next file » | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
1 // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file 1 // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file
2 // for details. All rights reserved. Use of this source code is governed by a 2 // for details. All rights reserved. Use of this source code is governed by a
3 // BSD-style license that can be found in the LICENSE file. 3 // BSD-style license that can be found in the LICENSE file.
4 4
5 /** 5 /**
6 * Test infrastructure for testing pub. Unlike typical unit tests, most pub 6 * Test infrastructure for testing pub. Unlike typical unit tests, most pub
7 * tests are integration tests that stage some stuff on the file system, run 7 * tests are integration tests that stage some stuff on the file system, run
8 * pub, and then validate the results. This library provides an API to build 8 * pub, and then validate the results. This library provides an API to build
9 * tests like that. 9 * tests like that.
10 */ 10 */
11 #library('test_pub'); 11 #library('test_pub');
12 12
13 #import('dart:io'); 13 #import('dart:io');
14 #import('dart:isolate'); 14 #import('dart:isolate');
15 #import('dart:json'); 15 #import('dart:json');
16 #import('dart:math'); 16 #import('dart:math');
17 #import('dart:uri'); 17 #import('dart:uri');
18 18
19 #import('../../../pkg/unittest/unittest.dart'); 19 #import('../../../pkg/unittest/unittest.dart');
20 #import('../../lib/file_system.dart', prefix: 'fs'); 20 #import('../../lib/file_system.dart', prefix: 'fs');
21 #import('../../pub/git_source.dart');
21 #import('../../pub/io.dart'); 22 #import('../../pub/io.dart');
23 #import('../../pub/repo_source.dart');
24 #import('../../pub/sdk_source.dart');
22 #import('../../pub/utils.dart'); 25 #import('../../pub/utils.dart');
23 #import('../../pub/yaml/yaml.dart'); 26 #import('../../pub/yaml/yaml.dart');
24 27
25 /** 28 /**
26 * Creates a new [FileDescriptor] with [name] and [contents]. 29 * Creates a new [FileDescriptor] with [name] and [contents].
27 */ 30 */
28 FileDescriptor file(Pattern name, String contents) => 31 FileDescriptor file(Pattern name, String contents) =>
29 new FileDescriptor(name, contents); 32 new FileDescriptor(name, contents);
30 33
31 /** 34 /**
(...skipping 18 matching lines...) Expand all
50 * Creates a new [TarFileDescriptor] with [name] and [contents]. 53 * Creates a new [TarFileDescriptor] with [name] and [contents].
51 */ 54 */
52 TarFileDescriptor tar(Pattern name, [List<Descriptor> contents]) => 55 TarFileDescriptor tar(Pattern name, [List<Descriptor> contents]) =>
53 new TarFileDescriptor(name, contents); 56 new TarFileDescriptor(name, contents);
54 57
55 /** 58 /**
56 * The current [HttpServer] created using [serve]. 59 * The current [HttpServer] created using [serve].
57 */ 60 */
58 var _server; 61 var _server;
59 62
63 /** The cached value for [_portCompleter]. */
64 Completer<int> _portCompleterCache;
65
66 /** The completer for [port]. */
67 Completer<int> get _portCompleter {
68 if (_portCompleterCache != null) return _portCompleterCache;
69 _portCompleterCache = new Completer<int>();
70 _scheduleCleanup((_) {
71 _portCompleterCache = null;
72 });
73 return _portCompleterCache;
74 }
75
76 /**
77 * A future that will complete to the port used for the current server.
78 */
79 Future<int> get port => _portCompleter.future;
80
60 /** 81 /**
61 * Creates an HTTP server to serve [contents] as static files. This server will 82 * Creates an HTTP server to serve [contents] as static files. This server will
62 * exist only for the duration of the pub run. 83 * exist only for the duration of the pub run.
63 * 84 *
64 * Subsequent calls to [serve] will replace the previous server. 85 * Subsequent calls to [serve] will replace the previous server.
65 */ 86 */
66 void serve(String host, int port, [List<Descriptor> contents]) { 87 void serve([List<Descriptor> contents]) {
67 var baseDir = dir("serve-dir", contents); 88 var baseDir = dir("serve-dir", contents);
68 if (host == 'localhost') {
69 host = '127.0.0.1';
70 }
71 89
72 _schedule((_) { 90 _schedule((_) {
73 return _closeServer().transform((_) { 91 return _closeServer().transform((_) {
74 _server = new HttpServer(); 92 _server = new HttpServer();
75 _server.defaultRequestHandler = (request, response) { 93 _server.defaultRequestHandler = (request, response) {
76 var path = request.uri.replaceFirst("/", "").split("/"); 94 var path = request.uri.replaceFirst("/", "").split("/");
77 response.persistentConnection = false; 95 response.persistentConnection = false;
78 var stream; 96 var stream;
79 try { 97 try {
80 stream = baseDir.load(path); 98 stream = baseDir.load(path);
(...skipping 12 matching lines...) Expand all
93 response.outputStream.close(); 111 response.outputStream.close();
94 }); 112 });
95 113
96 future.handleException((e) { 114 future.handleException((e) {
97 print("Exception while handling ${request.uri}: $e"); 115 print("Exception while handling ${request.uri}: $e");
98 response.statusCode = 500; 116 response.statusCode = 500;
99 response.reasonPhrase = e.message; 117 response.reasonPhrase = e.message;
100 response.outputStream.close(); 118 response.outputStream.close();
101 }); 119 });
102 }; 120 };
103 _server.listen(host, port); 121 _server.listen("127.0.0.1", 0);
122 _portCompleter.complete(_server.port);
104 _scheduleCleanup((_) => _closeServer()); 123 _scheduleCleanup((_) => _closeServer());
105 return null; 124 return null;
106 }); 125 });
107 }); 126 });
108 } 127 }
109 128
110 /** 129 /**
111 * Closes [_server]. Returns a [Future] that will complete after the [_server] 130 * Closes [_server]. Returns a [Future] that will complete after the [_server]
112 * is closed. 131 * is closed.
113 */ 132 */
114 Future _closeServer() { 133 Future _closeServer() {
115 if (_server == null) return new Future.immediate(null); 134 if (_server == null) return new Future.immediate(null);
116 _server.close(); 135 _server.close();
117 _server = null; 136 _server = null;
137 _portCompleterCache = null;
118 // TODO(nweiz): Remove this once issue 4155 is fixed. Pumping the event loop 138 // TODO(nweiz): Remove this once issue 4155 is fixed. Pumping the event loop
119 // *seems* to be enough to ensure that the server is actually closed, but I'm 139 // *seems* to be enough to ensure that the server is actually closed, but I'm
120 // putting this at 10ms to be safe. 140 // putting this at 10ms to be safe.
121 return sleep(10); 141 return sleep(10);
122 } 142 }
123 143
124 /** 144 /**
145 * The [DirectoryDescriptor] describing the server layout of packages that are
146 * being served via [servePackages]. This is `null` if [servePackages] has not
147 * yet been called for this test.
148 */
149 DirectoryDescriptor _servedPackageDir;
150
151 /**
152 * A map from package names to version numbers to YAML-serialized pubspecs for
153 * those packages. This represents the packages currently being served by
154 * [servePackages], and is `null` if [servePackages] has not yet been called for
155 * this test.
156 */
157 Map<String, Map<String, String>> _servedPackages;
158
159 /**
125 * Creates an HTTP server that replicates the structure of pub.dartlang.org. 160 * Creates an HTTP server that replicates the structure of pub.dartlang.org.
126 * [pubspecs] is a list of YAML-format pubspecs representing the packages to 161 * [pubspecs] is a list of unserialized pubspecs representing the packages to
127 * serve. 162 * serve.
128 */ 163 *
129 void servePackages(String host, int port, List<String> pubspecs) { 164 * Subsequent calls to [servePackages] will add to the set of packages that are
130 var packages = <String, Map<String, String>>{}; 165 * being served. Previous packages will continue to be served.
131 pubspecs.forEach((spec) { 166 */
132 var parsed = loadYaml(spec); 167 void servePackages(List<Map> pubspecs) {
133 var name = parsed['name']; 168 if (_servedPackages == null || _servedPackageDir) {
134 var version = parsed['version']; 169 _servedPackages = <String, Map<String, String>>{};
135 packages.putIfAbsent(name, () => <String, String>{})[version] = spec; 170 _servedPackageDir = dir('packages', []);
136 }); 171 serve([_servedPackageDir]);
137 172
138 serve(host, port, [ 173 _scheduleCleanup((_) {
139 dir('packages', flatten(packages.getKeys().map((name) { 174 _servedPackages = null;
140 return [ 175 _servedPackageDir = null;
141 file('$name.json', 176 });
142 JSON.stringify({'versions': packages[name].getKeys()})), 177 }
143 dir(name, [ 178
144 dir('versions', flatten(packages[name].getKeys().map((version) { 179 _schedule((_) {
145 return [ 180 return _awaitObject(pubspecs).transform((resolvedPubspecs) {
146 file('$version.yaml', packages[name][version]), 181 for (var spec in resolvedPubspecs) {
147 tar('$version.tar.gz', [ 182 var name = spec['name'];
148 file('pubspec.yaml', packages[name][version]), 183 var version = spec['version'];
149 file('$name.dart', 'main() => print("$name $version");') 184 var versions = _servedPackages.putIfAbsent(
150 ]) 185 name, () => <String, String>{});
151 ]; 186 versions[version] = yaml(spec);
152 }))) 187 }
153 ]) 188
154 ]; 189 _servedPackageDir.contents.clear();
155 }))) 190 for (var name in _servedPackages.getKeys()) {
156 ]); 191 var versions = _servedPackages[name].getKeys();
192 _servedPackageDir.contents.addAll([
193 file('$name.json',
194 JSON.stringify({'versions': versions})),
195 dir(name, [
196 dir('versions', flatten(versions.map((version) {
197 return [
198 file('$version.yaml', _servedPackages[name][version]),
199 tar('$version.tar.gz', [
200 file('pubspec.yaml', _servedPackages[name][version]),
201 file('$name.dart', 'main() => print("$name $version");')
202 ])
203 ];
204 })))
205 ])
206 ]);
207 }
208 });
209 });
210 }
211
212 /** Converts [value] into a YAML string. */
213 String yaml(value) => JSON.stringify(value);
214
215 /**
216 * Describes a file named `pubspec.yaml` with the given YAML-serialized
217 * [contents], which should be a serializable object.
218 *
219 * [contents] may contain [Future]s that resolve to serializable objects, which
220 * may in turn contain [Future]s recursively.
221 */
222 Descriptor pubspec(Map contents) {
223 return async(_awaitObject(contents).transform((resolvedContents) =>
224 file("pubspec.yaml", yaml(resolvedContents))));
225 }
226
227 /**
228 * Describes a file named `pubspec.yaml` for an application package with the
229 * given [dependencies].
230 */
231 Descriptor appPubspec(List dependencies) =>
232 pubspec({"dependencies": _dependencyListToMap(dependencies)});
233
234 /**
235 * Describes a file named `pubspec.yaml` for a library package with the given
236 * [name], [version], and [dependencies].
237 */
238 Descriptor libPubspec(String name, String version, [List dependencies]) =>
239 pubspec(package(name, version, dependencies));
240
241 /**
242 * Describes a map representing a library package with the given [name],
243 * [version], and [dependencies].
244 */
245 Map package(String name, String version, [List dependencies]) {
246 var package = {"name": name, "version": version};
247 if (dependencies != null) {
248 package["dependencies"] = _dependencyListToMap(dependencies);
249 }
250 return package;
251 }
252
253 /**
254 * Describes a map representing a dependency on a package in the package
255 * repository.
256 */
257 Map dependency(String name, [String versionConstraint]) {
258 var url = port.transform((p) => "http://localhost:$p");
259 var dependency = {"repo": {"name": name, "url": url}};
260 if (versionConstraint != null) dependency["version"] = versionConstraint;
261 return dependency;
262 }
263
264 /**
265 * Describes a directory for a package installed from the mock package repo.
266 * This directory is of the form found in the `packages/` directory.
267 */
268 DirectoryDescriptor packageDir(String name, String version) {
269 return dir(name, [
270 file("$name.dart", 'main() => print("$name $version");')
271 ]);
272 }
273
274 /**
275 * Describes a directory for a package installed from the mock package server.
276 * This directory is of the form found in the global package cache.
277 */
278 DirectoryDescriptor packageCacheDir(String name, String version) {
279 return dir("$name-$version", [
280 file("$name.dart", 'main() => print("$name $version");')
281 ]);
282 }
283
284 /**
285 * Describes a directory for a Git package. This directory is of the form found
286 * in the global package cache.
287 */
288 DirectoryDescriptor gitPackageCacheDir(String name, [int modifier]) {
289 var value = name;
290 if (modifier != null) value = "$name $modifier";
291 return dir(new RegExp("$name${@'-[a-f0-9]+'}"), [
292 file('$name.dart', 'main() => "$value";')
293 ]);
294 }
295
296 /**
297 * Describes the `packages/` directory containing all the given [packages],
298 * which should be name/version pairs. The packages will be validated against
299 * the format produced by the mock package server.
300 */
301 DirectoryDescriptor packagesDir(Map<String, String> packages) {
302 var contents = <Map>[];
303 packages.forEach((name, version) {
304 contents.add(packageDir(name, version));
305 });
306 return dir(packagesPath, contents);
307 }
308
309 /**
310 * Describes the global package cache directory containing all the given
311 * [packages], which should be name/version pairs. The packages will be
312 * validated against the format produced by the mock package server.
313 *
314 * A package's value may also be a list of versions, in which case all versions
315 * are expected to be installed.
316 */
317 DirectoryDescriptor cacheDir(Map packages) {
318 var contents = <Map>[];
319 packages.forEach((name, versions) {
320 if (versions is! List) versions = [versions];
321 for (var version in versions) {
322 contents.add(packageCacheDir(name, version));
323 }
324 });
325 return dir(cachePath, [
326 dir('repo', [
327 async(port.transform((p) => dir('localhost%58$p', contents)))
328 ])
329 ]);
330 }
331
332 /**
333 * Describes the application directory, containing only a pubspec specifying the
334 * given [dependencies].
335 */
336 DirectoryDescriptor appDir(List dependencies) =>
337 dir(appPath, [appPubspec(dependencies)]);
338
339 /**
340 * Converts a list of dependencies as passed to [package] into a hash as used in
341 * a pubspec.
342 */
343 Map _dependencyListToMap(List<Map> dependencies) {
344 var result = <String, Map>{};
345 dependencies.map((dependency) {
346 var sourceName = only(dependency.getKeys());
347 var source;
348 switch (sourceName) {
349 case "git":
350 source = new GitSource();
351 break;
352 case "repo":
353 source = new RepoSource();
354 break;
355 case "sdk":
356 source = new SdkSource('');
357 break;
358 default:
359 throw 'Unknown source "$sourceName"';
360 }
361
362 result[source.packageName(dependency[sourceName])] = dependency;
363 });
364 return result;
157 } 365 }
158 366
159 /** 367 /**
160 * The path of the package cache directory used for tests. Relative to the 368 * The path of the package cache directory used for tests. Relative to the
161 * sandbox directory. 369 * sandbox directory.
162 */ 370 */
163 final String cachePath = "cache"; 371 final String cachePath = "cache";
164 372
165 /** 373 /**
166 * The path of the mock SDK directory used for tests. Relative to the sandbox 374 * The path of the mock SDK directory used for tests. Relative to the sandbox
(...skipping 310 matching lines...) Expand 10 before | Expand all | Expand 10 after
477 } 685 }
478 686
479 return listDir(dir).chain((files) { 687 return listDir(dir).chain((files) {
480 var matches = files.filter((file) => endsWithPattern(file, name)); 688 var matches = files.filter((file) => endsWithPattern(file, name));
481 if (matches.length == 0) { 689 if (matches.length == 0) {
482 Expect.fail('No files in $dir match pattern $name.'); 690 Expect.fail('No files in $dir match pattern $name.');
483 } 691 }
484 if (matches.length == 1) return validate(matches[0]); 692 if (matches.length == 1) return validate(matches[0]);
485 693
486 var failures = []; 694 var failures = [];
695 var successes = 0;
487 var completer = new Completer(); 696 var completer = new Completer();
697 checkComplete() {
698 if (failures.length + successes != matches.length) return;
699 if (successes > 0) {
700 completer.complete(null);
701 return;
702 }
703
704 var error = new StringBuffer();
705 error.add("No files named $name in $dir were valid:\n");
706 for (var failure in failures) {
707 error.add(" ").add(failure).add("\n");
708 }
709 completer.completeException(new ExpectException(error.toString()));
710 }
711
488 for (var match in matches) { 712 for (var match in matches) {
489 var future = validate(match); 713 var future = validate(match);
490 714
491 future.handleException((e) { 715 future.handleException((e) {
492 failures.add(e); 716 failures.add(e);
493 if (failures.length != matches.length) return true; 717 checkComplete();
494
495 var error = new StringBuffer();
496 error.add("No files named $name in $dir were valid:\n");
497 for (var failure in failures) {
498 error.add(" ").add(failure).add("\n");
499 }
500 completer.completeException(new ExpectException(error.toString()));
501 return true; 718 return true;
502 }); 719 });
503 720
504 future.then(completer.complete); 721 future.then((_) {
722 successes++;
723 checkComplete();
724 });
505 } 725 }
506 return completer.future; 726 return completer.future;
507 }); 727 });
508 } 728 }
509 } 729 }
510 730
511 /** 731 /**
512 * Describes a file. These are used both for setting up an expected directory 732 * Describes a file. These are used both for setting up an expected directory
513 * tree before running a test, and for validating that the file system matches 733 * tree before running a test, and for validating that the file system matches
514 * some expectations after running it. 734 * some expectations after running it.
(...skipping 63 matching lines...) Expand 10 before | Expand all | Expand 10 after
578 */ 798 */
579 final List<Descriptor> contents; 799 final List<Descriptor> contents;
580 800
581 DirectoryDescriptor(Pattern name, this.contents) : super(name); 801 DirectoryDescriptor(Pattern name, this.contents) : super(name);
582 802
583 /** 803 /**
584 * Creates the file within [dir]. Returns a [Future] that is completed after 804 * Creates the file within [dir]. Returns a [Future] that is completed after
585 * the creation is done. 805 * the creation is done.
586 */ 806 */
587 Future<Directory> create(parentDir) { 807 Future<Directory> create(parentDir) {
588 final completer = new Completer<Directory>(); 808 // Create the directory.
809 return ensureDir(join(parentDir, _stringName)).chain((dir) {
810 if (contents == null) return new Future<Directory>.immediate(dir);
589 811
590 // Create the directory. 812 // Recursively create all of its children.
591 ensureDir(join(parentDir, _stringName)).then((dir) { 813 final childFutures = contents.map((child) => child.create(dir));
592 if (contents == null) { 814 // Only complete once all of the children have been created too.
593 completer.complete(dir); 815 return Futures.wait(childFutures).transform((_) => dir);
594 } else {
595 // Recursively create all of its children.
596 final childFutures = contents.map((child) => child.create(dir));
597 Futures.wait(childFutures).then((_) {
598 // Only complete once all of the children have been created too.
599 completer.complete(dir);
600 });
601 }
602 }); 816 });
603
604 return completer.future;
605 } 817 }
606 818
607 /** 819 /**
608 * Deletes the directory within [dir]. Returns a [Future] that is completed 820 * Deletes the directory within [dir]. Returns a [Future] that is completed
609 * after the deletion is done. 821 * after the deletion is done.
610 */ 822 */
611 Future delete(dir) { 823 Future delete(dir) {
612 return deleteDir(join(dir, _stringName)); 824 return deleteDir(join(dir, _stringName));
613 } 825 }
614 826
(...skipping 182 matching lines...) Expand 10 before | Expand all | Expand 10 after
797 }).then((tar) { 1009 }).then((tar) {
798 var sourceStream = tar.openInputStream(); 1010 var sourceStream = tar.openInputStream();
799 pipeInputToInput( 1011 pipeInputToInput(
800 sourceStream, sinkStream, onClosed: tempDir.deleteRecursively); 1012 sourceStream, sinkStream, onClosed: tempDir.deleteRecursively);
801 }); 1013 });
802 return sinkStream; 1014 return sinkStream;
803 } 1015 }
804 } 1016 }
805 1017
806 /** 1018 /**
1019 * Takes a simple data structure (composed of [Map]s, [List]s, scalar objects,
1020 * and [Future]s) and recursively resolves all the [Future]s contained within.
1021 * Completes with the fully resolved structure.
1022 */
1023 Future _awaitObject(object) {
1024 // Unroll nested futures.
1025 if (object is Future) return object.chain(_awaitObject);
1026 if (object is Collection) return Futures.wait(object.map(_awaitObject));
1027 if (object is! Map) return new Future.immediate(object);
1028
1029 var pairs = <Future<Pair>>[];
1030 object.forEach((key, value) {
1031 pairs.add(_awaitObject(value)
1032 .transform((resolved) => new Pair(key, resolved)));
1033 });
1034 return Futures.wait(pairs).transform((resolvedPairs) {
1035 var map = {};
1036 for (var pair in resolvedPairs) {
1037 map[pair.first] = pair.last;
1038 }
1039 return map;
1040 });
1041 }
1042
1043 /**
807 * Schedules a callback to be called as part of the test case. 1044 * Schedules a callback to be called as part of the test case.
808 */ 1045 */
809 void _schedule(_ScheduledEvent event) { 1046 void _schedule(_ScheduledEvent event) {
810 if (_scheduled == null) _scheduled = []; 1047 if (_scheduled == null) _scheduled = [];
811 _scheduled.add(event); 1048 _scheduled.add(event);
812 } 1049 }
813 1050
814 /** 1051 /**
815 * Schedules a callback to be called after Pub is run with [runPub], even if it 1052 * Schedules a callback to be called after Pub is run with [runPub], even if it
816 * fails. 1053 * fails.
817 */ 1054 */
818 void _scheduleCleanup(_ScheduledEvent event) { 1055 void _scheduleCleanup(_ScheduledEvent event) {
819 if (_scheduledCleanup == null) _scheduledCleanup = []; 1056 if (_scheduledCleanup == null) _scheduledCleanup = [];
820 _scheduledCleanup.add(event); 1057 _scheduledCleanup.add(event);
821 } 1058 }
OLDNEW
« no previous file with comments | « utils/tests/pub/pub_test.dart ('k') | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698