Index: utils/tests/pub/test_pub.dart |
diff --git a/utils/tests/pub/test_pub.dart b/utils/tests/pub/test_pub.dart |
deleted file mode 100644 |
index f5cb82c3841bebf3b36463419229bd949daa74ac..0000000000000000000000000000000000000000 |
--- a/utils/tests/pub/test_pub.dart |
+++ /dev/null |
@@ -1,637 +0,0 @@ |
-// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file |
-// for details. All rights reserved. Use of this source code is governed by a |
-// BSD-style license that can be found in the LICENSE file. |
- |
-/// Test infrastructure for testing pub. Unlike typical unit tests, most pub |
-/// tests are integration tests that stage some stuff on the file system, run |
-/// pub, and then validate the results. This library provides an API to build |
-/// tests like that. |
-library test_pub; |
- |
-import 'dart:async'; |
-import 'dart:collection' show Queue; |
-import 'dart:io' hide sleep; |
-import 'dart:json' as json; |
-import 'dart:math'; |
-import 'dart:uri'; |
-import 'dart:utf'; |
- |
-import 'package:http/testing.dart'; |
-import 'package:oauth2/oauth2.dart' as oauth2; |
-import 'package:pathos/path.dart' as path; |
-import 'package:scheduled_test/scheduled_process.dart'; |
-import 'package:scheduled_test/scheduled_server.dart'; |
-import 'package:scheduled_test/scheduled_test.dart'; |
-import 'package:yaml/yaml.dart'; |
- |
-import '../../pub/entrypoint.dart'; |
-// TODO(rnystrom): Using "gitlib" as the prefix here is ugly, but "git" collides |
-// with the git descriptor method. Maybe we should try to clean up the top level |
-// scope a bit? |
-import '../../pub/git.dart' as gitlib; |
-import '../../pub/git_source.dart'; |
-import '../../pub/hosted_source.dart'; |
-import '../../pub/http.dart'; |
-import '../../pub/io.dart'; |
-import '../../pub/path_source.dart'; |
-import '../../pub/safe_http_server.dart'; |
-import '../../pub/system_cache.dart'; |
-import '../../pub/utils.dart'; |
-import '../../pub/validator.dart'; |
-import 'command_line_config.dart'; |
-import 'descriptor.dart' as d; |
- |
-/// This should be called at the top of a test file to set up an appropriate |
-/// test configuration for the machine running the tests. |
-initConfig() { |
- // If we aren't running on the bots, use the human-friendly config. |
- if (!runningOnBuildbot) { |
- unittestConfiguration = new CommandLineConfiguration(); |
- } |
-} |
- |
-/// Returns whether we're running on a Dart build bot. |
-bool get runningOnBuildbot => |
- Platform.environment.containsKey('BUILDBOT_BUILDERNAME'); |
- |
-/// The current [HttpServer] created using [serve]. |
-var _server; |
- |
-/// The list of paths that have been requested from the server since the last |
-/// call to [getRequestedPaths]. |
-final _requestedPaths = <String>[]; |
- |
-/// The cached value for [_portCompleter]. |
-Completer<int> _portCompleterCache; |
- |
-/// The completer for [port]. |
-Completer<int> get _portCompleter { |
- if (_portCompleterCache != null) return _portCompleterCache; |
- _portCompleterCache = new Completer<int>(); |
- currentSchedule.onComplete.schedule(() { |
- _portCompleterCache = null; |
- }, 'clearing the port completer'); |
- return _portCompleterCache; |
-} |
- |
-/// A future that will complete to the port used for the current server. |
-Future<int> get port => _portCompleter.future; |
- |
-/// Gets the list of paths that have been requested from the server since the |
-/// last time this was called (or since the server was first spun up). |
-Future<List<String>> getRequestedPaths() { |
- return schedule(() { |
- var paths = _requestedPaths.toList(); |
- _requestedPaths.clear(); |
- return paths; |
- }); |
-} |
- |
-/// Creates an HTTP server to serve [contents] as static files. This server will |
-/// exist only for the duration of the pub run. |
-/// |
-/// Subsequent calls to [serve] will replace the previous server. |
-void serve([List<d.Descriptor> contents]) { |
- var baseDir = d.dir("serve-dir", contents); |
- |
- schedule(() { |
- return _closeServer().then((_) { |
- return SafeHttpServer.bind("127.0.0.1", 0).then((server) { |
- _server = server; |
- server.listen((request) { |
- var response = request.response; |
- try { |
- var path = request.uri.path.replaceFirst("/", ""); |
- |
- if (_requestedPaths == null) _requestedPaths = <String>[]; |
- _requestedPaths.add(path); |
- |
- response.persistentConnection = false; |
- var stream = baseDir.load(path); |
- |
- new ByteStream(stream).toBytes().then((data) { |
- response.statusCode = 200; |
- response.contentLength = data.length; |
- response.add(data); |
- response.close(); |
- }).catchError((e) { |
- response.statusCode = 404; |
- response.contentLength = 0; |
- response.close(); |
- }); |
- } catch (e) { |
- currentSchedule.signalError(e); |
- response.statusCode = 500; |
- response.close(); |
- return; |
- } |
- }); |
- _portCompleter.complete(_server.port); |
- currentSchedule.onComplete.schedule(_closeServer); |
- return null; |
- }); |
- }); |
- }, 'starting a server serving:\n${baseDir.describe()}'); |
-} |
- |
-/// Closes [_server]. Returns a [Future] that will complete after the [_server] |
-/// is closed. |
-Future _closeServer() { |
- if (_server == null) return new Future.value(); |
- _server.close(); |
- _server = null; |
- _portCompleterCache = null; |
- // TODO(nweiz): Remove this once issue 4155 is fixed. Pumping the event loop |
- // *seems* to be enough to ensure that the server is actually closed, but I'm |
- // putting this at 10ms to be safe. |
- return sleep(10); |
-} |
- |
-/// The [d.DirectoryDescriptor] describing the server layout of packages that |
-/// are being served via [servePackages]. This is `null` if [servePackages] has |
-/// not yet been called for this test. |
-d.DirectoryDescriptor _servedPackageDir; |
- |
-/// A map from package names to version numbers to YAML-serialized pubspecs for |
-/// those packages. This represents the packages currently being served by |
-/// [servePackages], and is `null` if [servePackages] has not yet been called |
-/// for this test. |
-Map<String, Map<String, String>> _servedPackages; |
- |
-/// Creates an HTTP server that replicates the structure of pub.dartlang.org. |
-/// [pubspecs] is a list of unserialized pubspecs representing the packages to |
-/// serve. |
-/// |
-/// Subsequent calls to [servePackages] will add to the set of packages that |
-/// are being served. Previous packages will continue to be served. |
-void servePackages(List<Map> pubspecs) { |
- if (_servedPackages == null || _servedPackageDir == null) { |
- _servedPackages = <String, Map<String, String>>{}; |
- _servedPackageDir = d.dir('packages', []); |
- serve([_servedPackageDir]); |
- |
- currentSchedule.onComplete.schedule(() { |
- _servedPackages = null; |
- _servedPackageDir = null; |
- }, 'cleaning up served packages'); |
- } |
- |
- schedule(() { |
- return awaitObject(pubspecs).then((resolvedPubspecs) { |
- for (var spec in resolvedPubspecs) { |
- var name = spec['name']; |
- var version = spec['version']; |
- var versions = _servedPackages.putIfAbsent( |
- name, () => <String, String>{}); |
- versions[version] = yaml(spec); |
- } |
- |
- _servedPackageDir.contents.clear(); |
- for (var name in _servedPackages.keys) { |
- var versions = _servedPackages[name].keys.toList(); |
- _servedPackageDir.contents.addAll([ |
- d.file('$name.json', json.stringify({'versions': versions})), |
- d.dir(name, [ |
- d.dir('versions', flatten(versions.map((version) { |
- return [ |
- d.file('$version.yaml', _servedPackages[name][version]), |
- d.tar('$version.tar.gz', [ |
- d.file('pubspec.yaml', _servedPackages[name][version]), |
- d.libDir(name, '$name $version') |
- ]) |
- ]; |
- }))) |
- ]) |
- ]); |
- } |
- }); |
- }, 'initializing the package server'); |
-} |
- |
-/// Converts [value] into a YAML string. |
-String yaml(value) => json.stringify(value); |
- |
-/// The full path to the created sandbox directory for an integration test. |
-String get sandboxDir => _sandboxDir; |
-String _sandboxDir; |
- |
-/// The path of the package cache directory used for tests. Relative to the |
-/// sandbox directory. |
-final String cachePath = "cache"; |
- |
-/// The path of the mock SDK directory used for tests. Relative to the sandbox |
-/// directory. |
-final String sdkPath = "sdk"; |
- |
-/// The path of the mock app directory used for tests. Relative to the sandbox |
-/// directory. |
-final String appPath = "myapp"; |
- |
-/// The path of the packages directory in the mock app used for tests. Relative |
-/// to the sandbox directory. |
-final String packagesPath = "$appPath/packages"; |
- |
-/// Set to true when the current batch of scheduled events should be aborted. |
-bool _abortScheduled = false; |
- |
-/// Defines an integration test. The [body] should schedule a series of |
-/// operations which will be run asynchronously. |
-void integration(String description, void body()) => |
- _integration(description, body, test); |
- |
-/// Like [integration], but causes only this test to run. |
-void solo_integration(String description, void body()) => |
- _integration(description, body, solo_test); |
- |
-void _integration(String description, void body(), [Function testFn]) { |
- testFn(description, () { |
- // The windows bots are very slow, so we increase the default timeout. |
- if (Platform.operatingSystem == "windows") { |
- currentSchedule.timeout = new Duration(seconds: 10); |
- } |
- |
- // By default, don't capture stack traces since they slow the tests way |
- // down. To debug failing tests, comment this out. |
- currentSchedule.captureStackTraces = |
- new Options().arguments.contains('--trace'); |
- |
- // Ensure the SDK version is always available. |
- d.dir(sdkPath, [ |
- d.file('version', '0.1.2.3') |
- ]).create(); |
- |
- _sandboxDir = createTempDir(); |
- d.defaultRoot = sandboxDir; |
- currentSchedule.onComplete.schedule(() => deleteEntry(_sandboxDir), |
- 'deleting the sandbox directory'); |
- |
- // Schedule the test. |
- body(); |
- }); |
-} |
- |
-/// Get the path to the root "util/test/pub" directory containing the pub |
-/// tests. |
-String get testDirectory { |
- var dir = new Options().script; |
- while (path.basename(dir) != 'pub') dir = path.dirname(dir); |
- |
- return path.absolute(dir); |
-} |
- |
-/// Schedules renaming (moving) the directory at [from] to [to], both of which |
-/// are assumed to be relative to [sandboxDir]. |
-void scheduleRename(String from, String to) { |
- schedule( |
- () => renameDir( |
- path.join(sandboxDir, from), |
- path.join(sandboxDir, to)), |
- 'renaming $from to $to'); |
-} |
- |
-/// Schedules creating a symlink at path [symlink] that points to [target], |
-/// both of which are assumed to be relative to [sandboxDir]. |
-void scheduleSymlink(String target, String symlink) { |
- schedule( |
- () => createSymlink( |
- path.join(sandboxDir, target), |
- path.join(sandboxDir, symlink)), |
- 'symlinking $target to $symlink'); |
-} |
- |
-/// Schedules a call to the Pub command-line utility. Runs Pub with [args] and |
-/// validates that its results match [output], [error], and [exitCode]. |
-void schedulePub({List args, Pattern output, Pattern error, |
- Future<Uri> tokenEndpoint, int exitCode: 0}) { |
- var pub = startPub(args: args, tokenEndpoint: tokenEndpoint); |
- pub.shouldExit(exitCode); |
- |
- expect(Future.wait([ |
- pub.remainingStdout(), |
- pub.remainingStderr() |
- ]).then((results) { |
- var failures = []; |
- _validateOutput(failures, 'stdout', output, results[0].split('\n')); |
- _validateOutput(failures, 'stderr', error, results[1].split('\n')); |
- if (!failures.isEmpty) throw new TestFailure(failures.join('\n')); |
- }), completes); |
-} |
- |
-/// Like [startPub], but runs `pub lish` in particular with [server] used both |
-/// as the OAuth2 server (with "/token" as the token endpoint) and as the |
-/// package server. |
-/// |
-/// Any futures in [args] will be resolved before the process is started. |
-ScheduledProcess startPublish(ScheduledServer server, {List args}) { |
- var tokenEndpoint = server.url.then((url) => |
- url.resolve('/token').toString()); |
- if (args == null) args = []; |
- args = flatten(['lish', '--server', tokenEndpoint, args]); |
- return startPub(args: args, tokenEndpoint: tokenEndpoint); |
-} |
- |
-/// Handles the beginning confirmation process for uploading a packages. |
-/// Ensures that the right output is shown and then enters "y" to confirm the |
-/// upload. |
-void confirmPublish(ScheduledProcess pub) { |
- // TODO(rnystrom): This is overly specific and inflexible regarding different |
- // test packages. Should validate this a little more loosely. |
- expect(pub.nextLine(), completion(equals('Publishing "test_pkg" 1.0.0:'))); |
- expect(pub.nextLine(), completion(equals("|-- LICENSE"))); |
- expect(pub.nextLine(), completion(equals("|-- lib"))); |
- expect(pub.nextLine(), completion(equals("| '-- test_pkg.dart"))); |
- expect(pub.nextLine(), completion(equals("'-- pubspec.yaml"))); |
- expect(pub.nextLine(), completion(equals(""))); |
- |
- pub.writeLine("y"); |
-} |
- |
-/// Starts a Pub process and returns a [ScheduledProcess] that supports |
-/// interaction with that process. |
-/// |
-/// Any futures in [args] will be resolved before the process is started. |
-ScheduledProcess startPub({List args, Future<Uri> tokenEndpoint}) { |
- String pathInSandbox(String relPath) { |
- return path.join(path.absolute(sandboxDir), relPath); |
- } |
- |
- ensureDir(pathInSandbox(appPath)); |
- |
- // Find a Dart executable we can use to spawn. Use the same one that was |
- // used to run this script itself. |
- var dartBin = new Options().executable; |
- |
- // If the executable looks like a path, get its full path. That way we |
- // can still find it when we spawn it with a different working directory. |
- if (dartBin.contains(Platform.pathSeparator)) { |
- dartBin = new File(dartBin).fullPathSync(); |
- } |
- |
- // Find the main pub entrypoint. |
- var pubPath = path.join(testDirectory, '..', '..', 'pub', 'pub.dart'); |
- |
- var dartArgs = ['--package-root=$_packageRoot/', '--checked', pubPath, |
- '--trace']; |
- dartArgs.addAll(args); |
- |
- if (tokenEndpoint == null) tokenEndpoint = new Future.value(); |
- var optionsFuture = tokenEndpoint.then((tokenEndpoint) { |
- var options = new ProcessOptions(); |
- options.workingDirectory = pathInSandbox(appPath); |
- // TODO(nweiz): remove this when issue 9294 is fixed. |
- options.environment = new Map.from(Platform.environment); |
- options.environment['PUB_CACHE'] = pathInSandbox(cachePath); |
- options.environment['DART_SDK'] = pathInSandbox(sdkPath); |
- if (tokenEndpoint != null) { |
- options.environment['_PUB_TEST_TOKEN_ENDPOINT'] = |
- tokenEndpoint.toString(); |
- } |
- return options; |
- }); |
- |
- return new ScheduledProcess.start(dartBin, dartArgs, options: optionsFuture, |
- description: args.isEmpty ? 'pub' : 'pub ${args.first}'); |
-} |
- |
-/// Whether pub is running from within the Dart SDK, as opposed to from the Dart |
-/// source repository. |
-bool get _runningFromSdk => path.dirname(relativeToPub('..')) == 'util'; |
- |
-// TODO(nweiz): use the built-in mechanism for accessing this once it exists |
-// (issue 9119). |
-/// The path to the `packages` directory from which pub loads its dependencies. |
-String get _packageRoot { |
- if (_runningFromSdk) { |
- return path.absolute(relativeToPub(path.join('..', '..', 'packages'))); |
- } else { |
- return path.absolute(path.join( |
- path.dirname(new Options().executable), '..', '..', 'packages')); |
- } |
-} |
- |
-/// Skips the current test if Git is not installed. This validates that the |
-/// current test is running on a buildbot in which case we expect git to be |
-/// installed. If we are not running on the buildbot, we will instead see if |
-/// git is installed and skip the test if not. This way, users don't need to |
-/// have git installed to run the tests locally (unless they actually care |
-/// about the pub git tests). |
-/// |
-/// This will also increase the [Schedule] timeout to 30 seconds on Windows, |
-/// where Git runs really slowly. |
-void ensureGit() { |
- if (Platform.operatingSystem == "windows") { |
- currentSchedule.timeout = new Duration(seconds: 30); |
- } |
- |
- schedule(() { |
- return gitlib.isInstalled.then((installed) { |
- if (installed) return; |
- if (runningOnBuildbot) return; |
- currentSchedule.abort(); |
- }); |
- }, 'ensuring that Git is installed'); |
-} |
- |
-/// Use [client] as the mock HTTP client for this test. |
-/// |
-/// Note that this will only affect HTTP requests made via http.dart in the |
-/// parent process. |
-void useMockClient(MockClient client) { |
- var oldInnerClient = httpClient.inner; |
- httpClient.inner = client; |
- currentSchedule.onComplete.schedule(() { |
- httpClient.inner = oldInnerClient; |
- }, 'de-activating the mock client'); |
-} |
- |
-/// Describes a map representing a library package with the given [name], |
-/// [version], and [dependencies]. |
-Map packageMap(String name, String version, [List dependencies]) { |
- var package = { |
- "name": name, |
- "version": version, |
- "author": "Nathan Weizenbaum <nweiz@google.com>", |
- "homepage": "http://pub.dartlang.org", |
- "description": "A package, I guess." |
- }; |
- if (dependencies != null) { |
- package["dependencies"] = dependencyListToMap(dependencies); |
- } |
- return package; |
-} |
- |
-/// Describes a map representing a dependency on a package in the package |
-/// repository. |
-Map dependencyMap(String name, [String versionConstraint]) { |
- var url = port.then((p) => "http://localhost:$p"); |
- var dependency = {"hosted": {"name": name, "url": url}}; |
- if (versionConstraint != null) dependency["version"] = versionConstraint; |
- return dependency; |
-} |
- |
-/// Converts a list of dependencies as passed to [package] into a hash as used |
-/// in a pubspec. |
-Future<Map> dependencyListToMap(List<Map> dependencies) { |
- return awaitObject(dependencies).then((resolvedDependencies) { |
- var result = <String, Map>{}; |
- for (var dependency in resolvedDependencies) { |
- var keys = dependency.keys.where((key) => key != "version"); |
- var sourceName = only(keys); |
- var source; |
- switch (sourceName) { |
- case "git": |
- source = new GitSource(); |
- break; |
- case "hosted": |
- source = new HostedSource(); |
- break; |
- case "path": |
- source = new PathSource(); |
- break; |
- default: |
- throw new Exception('Unknown source "$sourceName"'); |
- } |
- |
- result[_packageName(sourceName, dependency[sourceName])] = dependency; |
- } |
- return result; |
- }); |
-} |
- |
-/// Return the name for the package described by [description] and from |
-/// [sourceName]. |
-String _packageName(String sourceName, description) { |
- switch (sourceName) { |
- case "git": |
- var url = description is String ? description : description['url']; |
- // TODO(rnystrom): Using path.basename on a URL is hacky. If we add URL |
- // support to pkg/pathos, should use an explicit builder for that. |
- return path.basename(url.replaceFirst(new RegExp(r"(\.git)?/?$"), "")); |
- case "hosted": |
- if (description is String) return description; |
- return description['name']; |
- case "path": |
- return path.basename(description); |
- case "sdk": |
- return description; |
- default: |
- return description; |
- } |
-} |
- |
-/// Compares the [actual] output from running pub with [expected]. For [String] |
-/// patterns, ignores leading and trailing whitespace differences and tries to |
-/// report the offending difference in a nice way. For other [Pattern]s, just |
-/// reports whether the output contained the pattern. |
-void _validateOutput(List<String> failures, String pipe, Pattern expected, |
- List<String> actual) { |
- if (expected == null) return; |
- |
- if (expected is RegExp) { |
- _validateOutputRegex(failures, pipe, expected, actual); |
- } else { |
- _validateOutputString(failures, pipe, expected, actual); |
- } |
-} |
- |
-void _validateOutputRegex(List<String> failures, String pipe, |
- RegExp expected, List<String> actual) { |
- var actualText = actual.join('\n'); |
- if (actualText.contains(expected)) return; |
- |
- if (actual.length == 0) { |
- failures.add('Expected $pipe to match "${expected.pattern}" but got none.'); |
- } else { |
- failures.add('Expected $pipe to match "${expected.pattern}" but got:'); |
- failures.addAll(actual.map((line) => '| $line')); |
- } |
-} |
- |
-void _validateOutputString(List<String> failures, String pipe, |
- String expectedText, List<String> actual) { |
- final expected = expectedText.split('\n'); |
- |
- // Strip off the last line. This lets us have expected multiline strings |
- // where the closing ''' is on its own line. It also fixes '' expected output |
- // to expect zero lines of output, not a single empty line. |
- if (expected.last.trim() == '') { |
- expected.removeLast(); |
- } |
- |
- var results = []; |
- var failed = false; |
- |
- // Compare them line by line to see which ones match. |
- var length = max(expected.length, actual.length); |
- for (var i = 0; i < length; i++) { |
- if (i >= actual.length) { |
- // Missing output. |
- failed = true; |
- results.add('? ${expected[i]}'); |
- } else if (i >= expected.length) { |
- // Unexpected extra output. |
- failed = true; |
- results.add('X ${actual[i]}'); |
- } else { |
- var expectedLine = expected[i].trim(); |
- var actualLine = actual[i].trim(); |
- |
- if (expectedLine != actualLine) { |
- // Mismatched lines. |
- failed = true; |
- results.add('X ${actual[i]}'); |
- } else { |
- // Output is OK, but include it in case other lines are wrong. |
- results.add('| ${actual[i]}'); |
- } |
- } |
- } |
- |
- // If any lines mismatched, show the expected and actual. |
- if (failed) { |
- failures.add('Expected $pipe:'); |
- failures.addAll(expected.map((line) => '| $line')); |
- failures.add('Got:'); |
- failures.addAll(results); |
- } |
-} |
- |
-/// A function that creates a [Validator] subclass. |
-typedef Validator ValidatorCreator(Entrypoint entrypoint); |
- |
-/// Schedules a single [Validator] to run on the [appPath]. Returns a scheduled |
-/// Future that contains the errors and warnings produced by that validator. |
-Future<Pair<List<String>, List<String>>> schedulePackageValidation( |
- ValidatorCreator fn) { |
- return schedule(() { |
- var cache = new SystemCache.withSources(path.join(sandboxDir, cachePath)); |
- |
- return new Future.sync(() { |
- var validator = fn(new Entrypoint(path.join(sandboxDir, appPath), cache)); |
- return validator.validate().then((_) { |
- return new Pair(validator.errors, validator.warnings); |
- }); |
- }); |
- }, "validating package"); |
-} |
- |
-/// A matcher that matches a Pair. |
-Matcher pairOf(Matcher firstMatcher, Matcher lastMatcher) => |
- new _PairMatcher(firstMatcher, lastMatcher); |
- |
-class _PairMatcher extends BaseMatcher { |
- final Matcher _firstMatcher; |
- final Matcher _lastMatcher; |
- |
- _PairMatcher(this._firstMatcher, this._lastMatcher); |
- |
- bool matches(item, MatchState matchState) { |
- if (item is! Pair) return false; |
- return _firstMatcher.matches(item.first, matchState) && |
- _lastMatcher.matches(item.last, matchState); |
- } |
- |
- Description describe(Description description) { |
- description.addAll("(", ", ", ")", [_firstMatcher, _lastMatcher]); |
- } |
-} |