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

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

Issue 14297021: Move pub into sdk/lib/_internal. (Closed) Base URL: https://dart.googlecode.com/svn/branches/bleeding_edge/dart
Patch Set: Disallow package: imports of pub. Created 7 years, 8 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
(Empty)
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
3 // BSD-style license that can be found in the LICENSE file.
4
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
7 /// pub, and then validate the results. This library provides an API to build
8 /// tests like that.
9 library test_pub;
10
11 import 'dart:async';
12 import 'dart:collection' show Queue;
13 import 'dart:io' hide sleep;
14 import 'dart:json' as json;
15 import 'dart:math';
16 import 'dart:uri';
17 import 'dart:utf';
18
19 import 'package:http/testing.dart';
20 import 'package:oauth2/oauth2.dart' as oauth2;
21 import 'package:pathos/path.dart' as path;
22 import 'package:scheduled_test/scheduled_process.dart';
23 import 'package:scheduled_test/scheduled_server.dart';
24 import 'package:scheduled_test/scheduled_test.dart';
25 import 'package:yaml/yaml.dart';
26
27 import '../../pub/entrypoint.dart';
28 // TODO(rnystrom): Using "gitlib" as the prefix here is ugly, but "git" collides
29 // with the git descriptor method. Maybe we should try to clean up the top level
30 // scope a bit?
31 import '../../pub/git.dart' as gitlib;
32 import '../../pub/git_source.dart';
33 import '../../pub/hosted_source.dart';
34 import '../../pub/http.dart';
35 import '../../pub/io.dart';
36 import '../../pub/path_source.dart';
37 import '../../pub/safe_http_server.dart';
38 import '../../pub/system_cache.dart';
39 import '../../pub/utils.dart';
40 import '../../pub/validator.dart';
41 import 'command_line_config.dart';
42 import 'descriptor.dart' as d;
43
44 /// This should be called at the top of a test file to set up an appropriate
45 /// test configuration for the machine running the tests.
46 initConfig() {
47 // If we aren't running on the bots, use the human-friendly config.
48 if (!runningOnBuildbot) {
49 unittestConfiguration = new CommandLineConfiguration();
50 }
51 }
52
53 /// Returns whether we're running on a Dart build bot.
54 bool get runningOnBuildbot =>
55 Platform.environment.containsKey('BUILDBOT_BUILDERNAME');
56
57 /// The current [HttpServer] created using [serve].
58 var _server;
59
60 /// The list of paths that have been requested from the server since the last
61 /// call to [getRequestedPaths].
62 final _requestedPaths = <String>[];
63
64 /// The cached value for [_portCompleter].
65 Completer<int> _portCompleterCache;
66
67 /// The completer for [port].
68 Completer<int> get _portCompleter {
69 if (_portCompleterCache != null) return _portCompleterCache;
70 _portCompleterCache = new Completer<int>();
71 currentSchedule.onComplete.schedule(() {
72 _portCompleterCache = null;
73 }, 'clearing the port completer');
74 return _portCompleterCache;
75 }
76
77 /// A future that will complete to the port used for the current server.
78 Future<int> get port => _portCompleter.future;
79
80 /// Gets the list of paths that have been requested from the server since the
81 /// last time this was called (or since the server was first spun up).
82 Future<List<String>> getRequestedPaths() {
83 return schedule(() {
84 var paths = _requestedPaths.toList();
85 _requestedPaths.clear();
86 return paths;
87 });
88 }
89
90 /// Creates an HTTP server to serve [contents] as static files. This server will
91 /// exist only for the duration of the pub run.
92 ///
93 /// Subsequent calls to [serve] will replace the previous server.
94 void serve([List<d.Descriptor> contents]) {
95 var baseDir = d.dir("serve-dir", contents);
96
97 schedule(() {
98 return _closeServer().then((_) {
99 return SafeHttpServer.bind("127.0.0.1", 0).then((server) {
100 _server = server;
101 server.listen((request) {
102 var response = request.response;
103 try {
104 var path = request.uri.path.replaceFirst("/", "");
105
106 if (_requestedPaths == null) _requestedPaths = <String>[];
107 _requestedPaths.add(path);
108
109 response.persistentConnection = false;
110 var stream = baseDir.load(path);
111
112 new ByteStream(stream).toBytes().then((data) {
113 response.statusCode = 200;
114 response.contentLength = data.length;
115 response.add(data);
116 response.close();
117 }).catchError((e) {
118 response.statusCode = 404;
119 response.contentLength = 0;
120 response.close();
121 });
122 } catch (e) {
123 currentSchedule.signalError(e);
124 response.statusCode = 500;
125 response.close();
126 return;
127 }
128 });
129 _portCompleter.complete(_server.port);
130 currentSchedule.onComplete.schedule(_closeServer);
131 return null;
132 });
133 });
134 }, 'starting a server serving:\n${baseDir.describe()}');
135 }
136
137 /// Closes [_server]. Returns a [Future] that will complete after the [_server]
138 /// is closed.
139 Future _closeServer() {
140 if (_server == null) return new Future.value();
141 _server.close();
142 _server = null;
143 _portCompleterCache = null;
144 // TODO(nweiz): Remove this once issue 4155 is fixed. Pumping the event loop
145 // *seems* to be enough to ensure that the server is actually closed, but I'm
146 // putting this at 10ms to be safe.
147 return sleep(10);
148 }
149
150 /// The [d.DirectoryDescriptor] describing the server layout of packages that
151 /// are being served via [servePackages]. This is `null` if [servePackages] has
152 /// not yet been called for this test.
153 d.DirectoryDescriptor _servedPackageDir;
154
155 /// A map from package names to version numbers to YAML-serialized pubspecs for
156 /// those packages. This represents the packages currently being served by
157 /// [servePackages], and is `null` if [servePackages] has not yet been called
158 /// for this test.
159 Map<String, Map<String, String>> _servedPackages;
160
161 /// Creates an HTTP server that replicates the structure of pub.dartlang.org.
162 /// [pubspecs] is a list of unserialized pubspecs representing the packages to
163 /// serve.
164 ///
165 /// Subsequent calls to [servePackages] will add to the set of packages that
166 /// are being served. Previous packages will continue to be served.
167 void servePackages(List<Map> pubspecs) {
168 if (_servedPackages == null || _servedPackageDir == null) {
169 _servedPackages = <String, Map<String, String>>{};
170 _servedPackageDir = d.dir('packages', []);
171 serve([_servedPackageDir]);
172
173 currentSchedule.onComplete.schedule(() {
174 _servedPackages = null;
175 _servedPackageDir = null;
176 }, 'cleaning up served packages');
177 }
178
179 schedule(() {
180 return awaitObject(pubspecs).then((resolvedPubspecs) {
181 for (var spec in resolvedPubspecs) {
182 var name = spec['name'];
183 var version = spec['version'];
184 var versions = _servedPackages.putIfAbsent(
185 name, () => <String, String>{});
186 versions[version] = yaml(spec);
187 }
188
189 _servedPackageDir.contents.clear();
190 for (var name in _servedPackages.keys) {
191 var versions = _servedPackages[name].keys.toList();
192 _servedPackageDir.contents.addAll([
193 d.file('$name.json', json.stringify({'versions': versions})),
194 d.dir(name, [
195 d.dir('versions', flatten(versions.map((version) {
196 return [
197 d.file('$version.yaml', _servedPackages[name][version]),
198 d.tar('$version.tar.gz', [
199 d.file('pubspec.yaml', _servedPackages[name][version]),
200 d.libDir(name, '$name $version')
201 ])
202 ];
203 })))
204 ])
205 ]);
206 }
207 });
208 }, 'initializing the package server');
209 }
210
211 /// Converts [value] into a YAML string.
212 String yaml(value) => json.stringify(value);
213
214 /// The full path to the created sandbox directory for an integration test.
215 String get sandboxDir => _sandboxDir;
216 String _sandboxDir;
217
218 /// The path of the package cache directory used for tests. Relative to the
219 /// sandbox directory.
220 final String cachePath = "cache";
221
222 /// The path of the mock SDK directory used for tests. Relative to the sandbox
223 /// directory.
224 final String sdkPath = "sdk";
225
226 /// The path of the mock app directory used for tests. Relative to the sandbox
227 /// directory.
228 final String appPath = "myapp";
229
230 /// The path of the packages directory in the mock app used for tests. Relative
231 /// to the sandbox directory.
232 final String packagesPath = "$appPath/packages";
233
234 /// Set to true when the current batch of scheduled events should be aborted.
235 bool _abortScheduled = false;
236
237 /// Defines an integration test. The [body] should schedule a series of
238 /// operations which will be run asynchronously.
239 void integration(String description, void body()) =>
240 _integration(description, body, test);
241
242 /// Like [integration], but causes only this test to run.
243 void solo_integration(String description, void body()) =>
244 _integration(description, body, solo_test);
245
246 void _integration(String description, void body(), [Function testFn]) {
247 testFn(description, () {
248 // The windows bots are very slow, so we increase the default timeout.
249 if (Platform.operatingSystem == "windows") {
250 currentSchedule.timeout = new Duration(seconds: 10);
251 }
252
253 // By default, don't capture stack traces since they slow the tests way
254 // down. To debug failing tests, comment this out.
255 currentSchedule.captureStackTraces =
256 new Options().arguments.contains('--trace');
257
258 // Ensure the SDK version is always available.
259 d.dir(sdkPath, [
260 d.file('version', '0.1.2.3')
261 ]).create();
262
263 _sandboxDir = createTempDir();
264 d.defaultRoot = sandboxDir;
265 currentSchedule.onComplete.schedule(() => deleteEntry(_sandboxDir),
266 'deleting the sandbox directory');
267
268 // Schedule the test.
269 body();
270 });
271 }
272
273 /// Get the path to the root "util/test/pub" directory containing the pub
274 /// tests.
275 String get testDirectory {
276 var dir = new Options().script;
277 while (path.basename(dir) != 'pub') dir = path.dirname(dir);
278
279 return path.absolute(dir);
280 }
281
282 /// Schedules renaming (moving) the directory at [from] to [to], both of which
283 /// are assumed to be relative to [sandboxDir].
284 void scheduleRename(String from, String to) {
285 schedule(
286 () => renameDir(
287 path.join(sandboxDir, from),
288 path.join(sandboxDir, to)),
289 'renaming $from to $to');
290 }
291
292 /// Schedules creating a symlink at path [symlink] that points to [target],
293 /// both of which are assumed to be relative to [sandboxDir].
294 void scheduleSymlink(String target, String symlink) {
295 schedule(
296 () => createSymlink(
297 path.join(sandboxDir, target),
298 path.join(sandboxDir, symlink)),
299 'symlinking $target to $symlink');
300 }
301
302 /// Schedules a call to the Pub command-line utility. Runs Pub with [args] and
303 /// validates that its results match [output], [error], and [exitCode].
304 void schedulePub({List args, Pattern output, Pattern error,
305 Future<Uri> tokenEndpoint, int exitCode: 0}) {
306 var pub = startPub(args: args, tokenEndpoint: tokenEndpoint);
307 pub.shouldExit(exitCode);
308
309 expect(Future.wait([
310 pub.remainingStdout(),
311 pub.remainingStderr()
312 ]).then((results) {
313 var failures = [];
314 _validateOutput(failures, 'stdout', output, results[0].split('\n'));
315 _validateOutput(failures, 'stderr', error, results[1].split('\n'));
316 if (!failures.isEmpty) throw new TestFailure(failures.join('\n'));
317 }), completes);
318 }
319
320 /// Like [startPub], but runs `pub lish` in particular with [server] used both
321 /// as the OAuth2 server (with "/token" as the token endpoint) and as the
322 /// package server.
323 ///
324 /// Any futures in [args] will be resolved before the process is started.
325 ScheduledProcess startPublish(ScheduledServer server, {List args}) {
326 var tokenEndpoint = server.url.then((url) =>
327 url.resolve('/token').toString());
328 if (args == null) args = [];
329 args = flatten(['lish', '--server', tokenEndpoint, args]);
330 return startPub(args: args, tokenEndpoint: tokenEndpoint);
331 }
332
333 /// Handles the beginning confirmation process for uploading a packages.
334 /// Ensures that the right output is shown and then enters "y" to confirm the
335 /// upload.
336 void confirmPublish(ScheduledProcess pub) {
337 // TODO(rnystrom): This is overly specific and inflexible regarding different
338 // test packages. Should validate this a little more loosely.
339 expect(pub.nextLine(), completion(equals('Publishing "test_pkg" 1.0.0:')));
340 expect(pub.nextLine(), completion(equals("|-- LICENSE")));
341 expect(pub.nextLine(), completion(equals("|-- lib")));
342 expect(pub.nextLine(), completion(equals("| '-- test_pkg.dart")));
343 expect(pub.nextLine(), completion(equals("'-- pubspec.yaml")));
344 expect(pub.nextLine(), completion(equals("")));
345
346 pub.writeLine("y");
347 }
348
349 /// Starts a Pub process and returns a [ScheduledProcess] that supports
350 /// interaction with that process.
351 ///
352 /// Any futures in [args] will be resolved before the process is started.
353 ScheduledProcess startPub({List args, Future<Uri> tokenEndpoint}) {
354 String pathInSandbox(String relPath) {
355 return path.join(path.absolute(sandboxDir), relPath);
356 }
357
358 ensureDir(pathInSandbox(appPath));
359
360 // Find a Dart executable we can use to spawn. Use the same one that was
361 // used to run this script itself.
362 var dartBin = new Options().executable;
363
364 // If the executable looks like a path, get its full path. That way we
365 // can still find it when we spawn it with a different working directory.
366 if (dartBin.contains(Platform.pathSeparator)) {
367 dartBin = new File(dartBin).fullPathSync();
368 }
369
370 // Find the main pub entrypoint.
371 var pubPath = path.join(testDirectory, '..', '..', 'pub', 'pub.dart');
372
373 var dartArgs = ['--package-root=$_packageRoot/', '--checked', pubPath,
374 '--trace'];
375 dartArgs.addAll(args);
376
377 if (tokenEndpoint == null) tokenEndpoint = new Future.value();
378 var optionsFuture = tokenEndpoint.then((tokenEndpoint) {
379 var options = new ProcessOptions();
380 options.workingDirectory = pathInSandbox(appPath);
381 // TODO(nweiz): remove this when issue 9294 is fixed.
382 options.environment = new Map.from(Platform.environment);
383 options.environment['PUB_CACHE'] = pathInSandbox(cachePath);
384 options.environment['DART_SDK'] = pathInSandbox(sdkPath);
385 if (tokenEndpoint != null) {
386 options.environment['_PUB_TEST_TOKEN_ENDPOINT'] =
387 tokenEndpoint.toString();
388 }
389 return options;
390 });
391
392 return new ScheduledProcess.start(dartBin, dartArgs, options: optionsFuture,
393 description: args.isEmpty ? 'pub' : 'pub ${args.first}');
394 }
395
396 /// Whether pub is running from within the Dart SDK, as opposed to from the Dart
397 /// source repository.
398 bool get _runningFromSdk => path.dirname(relativeToPub('..')) == 'util';
399
400 // TODO(nweiz): use the built-in mechanism for accessing this once it exists
401 // (issue 9119).
402 /// The path to the `packages` directory from which pub loads its dependencies.
403 String get _packageRoot {
404 if (_runningFromSdk) {
405 return path.absolute(relativeToPub(path.join('..', '..', 'packages')));
406 } else {
407 return path.absolute(path.join(
408 path.dirname(new Options().executable), '..', '..', 'packages'));
409 }
410 }
411
412 /// Skips the current test if Git is not installed. This validates that the
413 /// current test is running on a buildbot in which case we expect git to be
414 /// installed. If we are not running on the buildbot, we will instead see if
415 /// git is installed and skip the test if not. This way, users don't need to
416 /// have git installed to run the tests locally (unless they actually care
417 /// about the pub git tests).
418 ///
419 /// This will also increase the [Schedule] timeout to 30 seconds on Windows,
420 /// where Git runs really slowly.
421 void ensureGit() {
422 if (Platform.operatingSystem == "windows") {
423 currentSchedule.timeout = new Duration(seconds: 30);
424 }
425
426 schedule(() {
427 return gitlib.isInstalled.then((installed) {
428 if (installed) return;
429 if (runningOnBuildbot) return;
430 currentSchedule.abort();
431 });
432 }, 'ensuring that Git is installed');
433 }
434
435 /// Use [client] as the mock HTTP client for this test.
436 ///
437 /// Note that this will only affect HTTP requests made via http.dart in the
438 /// parent process.
439 void useMockClient(MockClient client) {
440 var oldInnerClient = httpClient.inner;
441 httpClient.inner = client;
442 currentSchedule.onComplete.schedule(() {
443 httpClient.inner = oldInnerClient;
444 }, 'de-activating the mock client');
445 }
446
447 /// Describes a map representing a library package with the given [name],
448 /// [version], and [dependencies].
449 Map packageMap(String name, String version, [List dependencies]) {
450 var package = {
451 "name": name,
452 "version": version,
453 "author": "Nathan Weizenbaum <nweiz@google.com>",
454 "homepage": "http://pub.dartlang.org",
455 "description": "A package, I guess."
456 };
457 if (dependencies != null) {
458 package["dependencies"] = dependencyListToMap(dependencies);
459 }
460 return package;
461 }
462
463 /// Describes a map representing a dependency on a package in the package
464 /// repository.
465 Map dependencyMap(String name, [String versionConstraint]) {
466 var url = port.then((p) => "http://localhost:$p");
467 var dependency = {"hosted": {"name": name, "url": url}};
468 if (versionConstraint != null) dependency["version"] = versionConstraint;
469 return dependency;
470 }
471
472 /// Converts a list of dependencies as passed to [package] into a hash as used
473 /// in a pubspec.
474 Future<Map> dependencyListToMap(List<Map> dependencies) {
475 return awaitObject(dependencies).then((resolvedDependencies) {
476 var result = <String, Map>{};
477 for (var dependency in resolvedDependencies) {
478 var keys = dependency.keys.where((key) => key != "version");
479 var sourceName = only(keys);
480 var source;
481 switch (sourceName) {
482 case "git":
483 source = new GitSource();
484 break;
485 case "hosted":
486 source = new HostedSource();
487 break;
488 case "path":
489 source = new PathSource();
490 break;
491 default:
492 throw new Exception('Unknown source "$sourceName"');
493 }
494
495 result[_packageName(sourceName, dependency[sourceName])] = dependency;
496 }
497 return result;
498 });
499 }
500
501 /// Return the name for the package described by [description] and from
502 /// [sourceName].
503 String _packageName(String sourceName, description) {
504 switch (sourceName) {
505 case "git":
506 var url = description is String ? description : description['url'];
507 // TODO(rnystrom): Using path.basename on a URL is hacky. If we add URL
508 // support to pkg/pathos, should use an explicit builder for that.
509 return path.basename(url.replaceFirst(new RegExp(r"(\.git)?/?$"), ""));
510 case "hosted":
511 if (description is String) return description;
512 return description['name'];
513 case "path":
514 return path.basename(description);
515 case "sdk":
516 return description;
517 default:
518 return description;
519 }
520 }
521
522 /// Compares the [actual] output from running pub with [expected]. For [String]
523 /// patterns, ignores leading and trailing whitespace differences and tries to
524 /// report the offending difference in a nice way. For other [Pattern]s, just
525 /// reports whether the output contained the pattern.
526 void _validateOutput(List<String> failures, String pipe, Pattern expected,
527 List<String> actual) {
528 if (expected == null) return;
529
530 if (expected is RegExp) {
531 _validateOutputRegex(failures, pipe, expected, actual);
532 } else {
533 _validateOutputString(failures, pipe, expected, actual);
534 }
535 }
536
537 void _validateOutputRegex(List<String> failures, String pipe,
538 RegExp expected, List<String> actual) {
539 var actualText = actual.join('\n');
540 if (actualText.contains(expected)) return;
541
542 if (actual.length == 0) {
543 failures.add('Expected $pipe to match "${expected.pattern}" but got none.');
544 } else {
545 failures.add('Expected $pipe to match "${expected.pattern}" but got:');
546 failures.addAll(actual.map((line) => '| $line'));
547 }
548 }
549
550 void _validateOutputString(List<String> failures, String pipe,
551 String expectedText, List<String> actual) {
552 final expected = expectedText.split('\n');
553
554 // Strip off the last line. This lets us have expected multiline strings
555 // where the closing ''' is on its own line. It also fixes '' expected output
556 // to expect zero lines of output, not a single empty line.
557 if (expected.last.trim() == '') {
558 expected.removeLast();
559 }
560
561 var results = [];
562 var failed = false;
563
564 // Compare them line by line to see which ones match.
565 var length = max(expected.length, actual.length);
566 for (var i = 0; i < length; i++) {
567 if (i >= actual.length) {
568 // Missing output.
569 failed = true;
570 results.add('? ${expected[i]}');
571 } else if (i >= expected.length) {
572 // Unexpected extra output.
573 failed = true;
574 results.add('X ${actual[i]}');
575 } else {
576 var expectedLine = expected[i].trim();
577 var actualLine = actual[i].trim();
578
579 if (expectedLine != actualLine) {
580 // Mismatched lines.
581 failed = true;
582 results.add('X ${actual[i]}');
583 } else {
584 // Output is OK, but include it in case other lines are wrong.
585 results.add('| ${actual[i]}');
586 }
587 }
588 }
589
590 // If any lines mismatched, show the expected and actual.
591 if (failed) {
592 failures.add('Expected $pipe:');
593 failures.addAll(expected.map((line) => '| $line'));
594 failures.add('Got:');
595 failures.addAll(results);
596 }
597 }
598
599 /// A function that creates a [Validator] subclass.
600 typedef Validator ValidatorCreator(Entrypoint entrypoint);
601
602 /// Schedules a single [Validator] to run on the [appPath]. Returns a scheduled
603 /// Future that contains the errors and warnings produced by that validator.
604 Future<Pair<List<String>, List<String>>> schedulePackageValidation(
605 ValidatorCreator fn) {
606 return schedule(() {
607 var cache = new SystemCache.withSources(path.join(sandboxDir, cachePath));
608
609 return new Future.sync(() {
610 var validator = fn(new Entrypoint(path.join(sandboxDir, appPath), cache));
611 return validator.validate().then((_) {
612 return new Pair(validator.errors, validator.warnings);
613 });
614 });
615 }, "validating package");
616 }
617
618 /// A matcher that matches a Pair.
619 Matcher pairOf(Matcher firstMatcher, Matcher lastMatcher) =>
620 new _PairMatcher(firstMatcher, lastMatcher);
621
622 class _PairMatcher extends BaseMatcher {
623 final Matcher _firstMatcher;
624 final Matcher _lastMatcher;
625
626 _PairMatcher(this._firstMatcher, this._lastMatcher);
627
628 bool matches(item, MatchState matchState) {
629 if (item is! Pair) return false;
630 return _firstMatcher.matches(item.first, matchState) &&
631 _lastMatcher.matches(item.last, matchState);
632 }
633
634 Description describe(Description description) {
635 description.addAll("(", ", ", ")", [_firstMatcher, _lastMatcher]);
636 }
637 }
OLDNEW
« no previous file with comments | « utils/tests/pub/real_version_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