| // 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. |
| import 'dart:async'; |
| import 'dart:convert'; |
| import 'dart:io'; |
| import 'dart:isolate'; |
| import 'dart:math'; |
| |
| import 'package:async/async.dart'; |
| import 'package:http/testing.dart'; |
| import 'package:path/path.dart' as p; |
| import 'package:pub_semver/pub_semver.dart'; |
| import 'package:test/test.dart' hide fail; |
| import 'package:test/test.dart' as test show fail; |
| import 'package:test_process/test_process.dart'; |
| |
| import 'package:pub/src/entrypoint.dart'; |
| import 'package:pub/src/exit_codes.dart' as exit_codes; |
| // 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 'package:pub/src/git.dart' as gitlib; |
| import 'package:pub/src/http.dart'; |
| import 'package:pub/src/io.dart'; |
| import 'package:pub/src/lock_file.dart'; |
| import 'package:pub/src/log.dart' as log; |
| import 'package:pub/src/sdk.dart'; |
| import 'package:pub/src/source_registry.dart'; |
| import 'package:pub/src/system_cache.dart'; |
| import 'package:pub/src/utils.dart'; |
| import 'package:pub/src/validator.dart'; |
| |
| import 'descriptor.dart' as d; |
| import 'descriptor_server.dart'; |
| import 'package_server.dart'; |
| |
| export 'descriptor_server.dart'; |
| export 'package_server.dart'; |
| |
| /// A [Matcher] that matches JavaScript generated by dart2js with minification |
| /// enabled. |
| Matcher isMinifiedDart2JSOutput = |
| isNot(contains('// The code supports the following hooks')); |
| |
| /// A [Matcher] that matches JavaScript generated by dart2js with minification |
| /// disabled. |
| Matcher isUnminifiedDart2JSOutput = |
| contains('// The code supports the following hooks'); |
| |
| /// Converts [value] into a YAML string. |
| String yaml(value) => jsonEncode(value); |
| |
| /// The path of the package cache directory used for tests, relative to the |
| /// sandbox directory. |
| const String cachePath = 'cache'; |
| |
| /// The path of the mock app directory used for tests, relative to the sandbox |
| /// directory. |
| const String appPath = 'myapp'; |
| |
| /// The path of the ".packages" file in the mock app used for tests, relative |
| /// to the sandbox directory. |
| const String packagesFilePath = '$appPath/.packages'; |
| |
| /// The line from the `.packages` file for [packageName]. |
| String packageSpecLine(String packageName) => File(d.path(packagesFilePath)) |
| .readAsLinesSync() |
| .firstWhere((l) => l.startsWith('$packageName:')); |
| |
| /// The suffix appended to a precompiled snapshot. |
| final versionSuffix = testVersion ?? sdk.version; |
| |
| /// Enum identifying a pub command that can be run with a well-defined success |
| /// output. |
| class RunCommand { |
| static final add = RunCommand( |
| 'add', RegExp(r'Got dependencies!|Changed \d+ dependenc(y|ies)!')); |
| static final get = RunCommand( |
| 'get', RegExp(r'Got dependencies!|Changed \d+ dependenc(y|ies)!')); |
| static final upgrade = RunCommand('upgrade', RegExp(r''' |
| (No dependencies changed\.|Changed \d+ dependenc(y|ies)!)($| |
| \d+ packages? (has|have) newer versions incompatible with dependency constraints. |
| Try `pub outdated` for more information.$)''')); |
| static final downgrade = RunCommand('downgrade', |
| RegExp(r'(No dependencies changed\.|Changed \d+ dependenc(y|ies)!)$')); |
| static final remove = RunCommand( |
| 'remove', RegExp(r'Got dependencies!|Changed \d+ dependenc(y|ies)!')); |
| |
| final String name; |
| final RegExp success; |
| RunCommand(this.name, this.success); |
| } |
| |
| /// Runs the tests defined within [callback] using both pub get and pub upgrade. |
| /// |
| /// Many tests validate behavior that is the same between pub get and |
| /// upgrade have the same behavior. Instead of duplicating those tests, this |
| /// takes a callback that defines get/upgrade agnostic tests and runs them |
| /// with both commands. |
| void forBothPubGetAndUpgrade(void Function(RunCommand) callback) { |
| group(RunCommand.get.name, () => callback(RunCommand.get)); |
| group(RunCommand.upgrade.name, () => callback(RunCommand.upgrade)); |
| } |
| |
| /// Invokes the pub [command] and validates that it completes in an expected |
| /// way. |
| /// |
| /// By default, this validates that the command completes successfully and |
| /// understands the normal output of a successful pub command. If [warning] is |
| /// given, it expects the command to complete successfully *and* print [warning] |
| /// to stderr. If [error] is given, it expects the command to *only* print |
| /// [error] to stderr. [output], [error], [silent], and [warning] may be |
| /// strings, [RegExp]s, or [Matcher]s. |
| /// |
| /// If [exitCode] is given, expects the command to exit with that code. |
| // TODO(rnystrom): Clean up other tests to call this when possible. |
| Future pubCommand(RunCommand command, |
| {Iterable<String> args, |
| output, |
| error, |
| silent, |
| warning, |
| int exitCode, |
| Map<String, String> environment}) async { |
| if (error != null && warning != null) { |
| throw ArgumentError("Cannot pass both 'error' and 'warning'."); |
| } |
| |
| var allArgs = [command.name]; |
| if (args != null) allArgs.addAll(args); |
| |
| output ??= command.success; |
| |
| if (error != null && exitCode == null) exitCode = 1; |
| |
| // No success output on an error. |
| if (error != null) output = null; |
| if (warning != null) error = warning; |
| |
| await runPub( |
| args: allArgs, |
| output: output, |
| error: error, |
| silent: silent, |
| exitCode: exitCode, |
| environment: environment); |
| } |
| |
| Future pubAdd( |
| {Iterable<String> args, |
| output, |
| error, |
| warning, |
| int exitCode, |
| Map<String, String> environment}) => |
| pubCommand(RunCommand.add, |
| args: args, |
| output: output, |
| error: error, |
| warning: warning, |
| exitCode: exitCode, |
| environment: environment); |
| |
| Future pubGet( |
| {Iterable<String> args, |
| output, |
| error, |
| warning, |
| int exitCode, |
| Map<String, String> environment}) => |
| pubCommand(RunCommand.get, |
| args: args, |
| output: output, |
| error: error, |
| warning: warning, |
| exitCode: exitCode, |
| environment: environment); |
| |
| Future pubUpgrade( |
| {Iterable<String> args, |
| output, |
| error, |
| warning, |
| int exitCode, |
| Map<String, String> environment}) => |
| pubCommand(RunCommand.upgrade, |
| args: args, |
| output: output, |
| error: error, |
| warning: warning, |
| exitCode: exitCode, |
| environment: environment); |
| |
| Future pubDowngrade( |
| {Iterable<String> args, |
| output, |
| error, |
| warning, |
| int exitCode, |
| Map<String, String> environment}) => |
| pubCommand(RunCommand.downgrade, |
| args: args, |
| output: output, |
| error: error, |
| warning: warning, |
| exitCode: exitCode, |
| environment: environment); |
| |
| Future pubRemove( |
| {Iterable<String> args, |
| output, |
| error, |
| warning, |
| int exitCode, |
| Map<String, String> environment}) => |
| pubCommand(RunCommand.remove, |
| args: args, |
| output: output, |
| error: error, |
| warning: warning, |
| exitCode: exitCode, |
| environment: environment); |
| |
| /// Schedules starting the "pub [global] run" process and validates the |
| /// expected startup output. |
| /// |
| /// If [global] is `true`, this invokes "pub global run", otherwise it does |
| /// "pub run". |
| /// |
| /// Returns the `pub run` process. |
| Future<PubProcess> pubRun( |
| {bool global = false, |
| Iterable<String> args, |
| Map<String, String> environment, |
| bool verbose = true}) async { |
| var pubArgs = global ? ['global', 'run'] : ['run']; |
| pubArgs.addAll(args); |
| var pub = await startPub( |
| args: pubArgs, |
| environment: environment, |
| verbose: verbose, |
| ); |
| |
| // Loading sources and transformers isn't normally printed, but the pub test |
| // infrastructure runs pub in verbose mode, which enables this. |
| expect(pub.stdout, mayEmitMultiple(startsWith('Loading'))); |
| |
| return pub; |
| } |
| |
| /// Schedules starting the "pub run --v2" process and validates the |
| /// expected startup output. |
| /// |
| /// Returns the `pub run` process. |
| Future<PubProcess> pubRunFromDartDev({Iterable<String> args}) async { |
| final pub = await startPub(args: ['run', '--dart-dev-run', ...args]); |
| |
| // Loading sources and transformers isn't normally printed, but the pub test |
| // infrastructure runs pub in verbose mode, which enables this. |
| expect(pub.stdout, mayEmitMultiple(startsWith('Loading'))); |
| |
| return pub; |
| } |
| |
| /// Schedules renaming (moving) the directory at [from] to [to], both of which |
| /// are assumed to be relative to [d.sandbox]. |
| void renameInSandbox(String from, String to) { |
| renameDir(_pathInSandbox(from), _pathInSandbox(to)); |
| } |
| |
| /// Schedules creating a symlink at path [symlink] that points to [target], |
| /// both of which are assumed to be relative to [d.sandbox]. |
| void symlinkInSandbox(String target, String symlink) { |
| createSymlink(_pathInSandbox(target), _pathInSandbox(symlink)); |
| } |
| |
| /// Runs Pub with [args] and validates that its results match [output] (or |
| /// [outputJson]), [error], [silent] (for logs that are silent by default), and |
| /// [exitCode]. |
| /// |
| /// [output], [error], and [silent] can be [String]s, [RegExp]s, or [Matcher]s. |
| /// |
| /// If [outputJson] is given, validates that pub outputs stringified JSON |
| /// matching that object, which can be a literal JSON object or any other |
| /// [Matcher]. |
| /// |
| /// If [environment] is given, any keys in it will override the environment |
| /// variables passed to the spawned process. |
| Future runPub( |
| {List<String> args, |
| output, |
| error, |
| outputJson, |
| silent, |
| int exitCode = exit_codes.SUCCESS, |
| String workingDirectory, |
| Map<String, String> environment}) async { |
| // Cannot pass both output and outputJson. |
| assert(output == null || outputJson == null); |
| |
| var pub = await startPub( |
| args: args, workingDirectory: workingDirectory, environment: environment); |
| await pub.shouldExit(exitCode); |
| |
| var actualOutput = (await pub.stdoutStream().toList()).join('\n'); |
| var actualError = (await pub.stderrStream().toList()).join('\n'); |
| var actualSilent = (await pub.silentStream().toList()).join('\n'); |
| |
| var failures = <String>[]; |
| if (outputJson == null) { |
| _validateOutput(failures, 'stdout', output, actualOutput); |
| } else { |
| _validateOutputJson(failures, 'stdout', outputJson, actualOutput); |
| } |
| |
| _validateOutput(failures, 'stderr', error, actualError); |
| _validateOutput(failures, 'silent', silent, actualSilent); |
| |
| if (failures.isNotEmpty) { |
| test.fail(failures.join('\n')); |
| } |
| } |
| |
| /// 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. |
| Future<PubProcess> startPublish(PackageServer server, |
| {List<String> args}) async { |
| var tokenEndpoint = Uri.parse(server.url).resolve('/token').toString(); |
| args = ['lish', ...?args]; |
| return await startPub( |
| args: args, |
| tokenEndpoint: tokenEndpoint, |
| environment: {'PUB_HOSTED_URL': server.url}); |
| } |
| |
| /// Handles the beginning confirmation process for uploading a packages. |
| /// |
| /// Ensures that the right output is shown and then enters "y" to confirm the |
| /// upload. |
| Future confirmPublish(TestProcess pub) async { |
| // TODO(rnystrom): This is overly specific and inflexible regarding different |
| // test packages. Should validate this a little more loosely. |
| await expectLater( |
| pub.stdout, emits(startsWith('Publishing test_pkg 1.0.0 to '))); |
| await expectLater( |
| pub.stdout, |
| emitsThrough(matches( |
| r'^Do you want to publish [^ ]+ [^ ]+ (y/N)?', |
| ))); |
| pub.stdin.writeln('y'); |
| } |
| |
| /// Resolves [path] relative to the package cache in the sandbox. |
| String pathInCache(String path) => p.join(d.sandbox, cachePath, path); |
| |
| /// Gets the absolute path to [relPath], which is a relative path in the test |
| /// sandbox. |
| String _pathInSandbox(String relPath) => p.join(d.sandbox, relPath); |
| |
| String testVersion = '0.1.2+3'; |
| |
| /// Gets the environment variables used to run pub in a test context. |
| Map<String, String> getPubTestEnvironment([String tokenEndpoint]) { |
| var environment = { |
| '_PUB_TESTING': 'true', |
| 'PUB_CACHE': _pathInSandbox(cachePath), |
| 'PUB_ENVIRONMENT': 'test-environment', |
| |
| // Ensure a known SDK version is set for the tests that rely on that. |
| '_PUB_TEST_SDK_VERSION': testVersion |
| }; |
| |
| if (tokenEndpoint != null) { |
| environment['_PUB_TEST_TOKEN_ENDPOINT'] = tokenEndpoint; |
| } |
| |
| if (globalServer != null) { |
| environment['PUB_HOSTED_URL'] = 'http://localhost:${globalServer.port}'; |
| } |
| |
| return environment; |
| } |
| |
| /// The test runner starts all tests from a `data:` URI. |
| final bool _runningAsTestRunner = Platform.script.scheme == 'data'; |
| |
| /// The path to the root of pub's sources in the pub repo. |
| final String _pubRoot = (() { |
| // The test runner always runs from the repo directory. |
| if (_runningAsTestRunner) return p.current; |
| |
| // Running from "test/../some_test.dart". |
| var script = p.fromUri(Platform.script); |
| |
| var components = p.split(script); |
| var testIndex = components.indexOf('test'); |
| if (testIndex == -1) throw StateError("Can't find pub's root."); |
| return p.joinAll(components.take(testIndex)); |
| })(); |
| |
| /// Starts a Pub process and returns a [PubProcess] that supports interaction |
| /// with that process. |
| /// |
| /// Any futures in [args] will be resolved before the process is started. |
| /// |
| /// If [environment] is given, any keys in it will override the environment |
| /// variables passed to the spawned process. |
| Future<PubProcess> startPub( |
| {Iterable<String> args, |
| String tokenEndpoint, |
| String workingDirectory, |
| Map<String, String> environment, |
| bool verbose = true}) async { |
| args ??= []; |
| |
| 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 = Platform.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 = p.absolute(dartBin); |
| } |
| |
| // If there's a snapshot for "pub" available we use it. If the snapshot is |
| // out-of-date local source the tests will be useless, therefore it is |
| // recommended to use a temporary file with a unique name for each test run. |
| // Note: running tests without a snapshot is significantly slower, use |
| // tool/test.dart to generate the snapshot. |
| var pubPath = Platform.environment['_PUB_TEST_SNAPSHOT'] ?? ''; |
| if (pubPath.isEmpty || !fileExists(pubPath)) { |
| pubPath = p.absolute(p.join(_pubRoot, 'bin/pub.dart')); |
| } |
| |
| final dotPackagesPath = (await Isolate.packageConfig).toString(); |
| |
| var dartArgs = ['--packages=$dotPackagesPath']; |
| dartArgs..addAll([pubPath, if (verbose) '--verbose'])..addAll(args); |
| |
| return await PubProcess.start(dartBin, dartArgs, |
| environment: getPubTestEnvironment(tokenEndpoint) |
| ..addAll(environment ?? {}), |
| workingDirectory: workingDirectory ?? _pathInSandbox(appPath), |
| description: args.isEmpty ? 'pub' : 'pub ${args.first}'); |
| } |
| |
| /// A subclass of [TestProcess] that parses pub's verbose logging output and |
| /// makes [stdout] and [stderr] work as though pub weren't running in verbose |
| /// mode. |
| class PubProcess extends TestProcess { |
| StreamSplitter<Pair<log.Level, String>> get _logSplitter { |
| __logSplitter ??= StreamSplitter(StreamGroup.merge([ |
| _outputToLog(super.stdoutStream(), log.Level.MESSAGE), |
| _outputToLog(super.stderrStream(), log.Level.ERROR) |
| ])); |
| return __logSplitter; |
| } |
| |
| StreamSplitter<Pair<log.Level, String>> __logSplitter; |
| |
| static Future<PubProcess> start(String executable, Iterable<String> arguments, |
| {String workingDirectory, |
| Map<String, String> environment, |
| bool includeParentEnvironment = true, |
| bool runInShell = false, |
| String description, |
| Encoding encoding, |
| bool forwardStdio = false}) async { |
| var process = await Process.start(executable, arguments.toList(), |
| workingDirectory: workingDirectory, |
| environment: environment, |
| includeParentEnvironment: includeParentEnvironment, |
| runInShell: runInShell); |
| |
| if (description == null) { |
| var humanExecutable = p.isWithin(p.current, executable) |
| ? p.relative(executable) |
| : executable; |
| description = '$humanExecutable ${arguments.join(' ')}'; |
| } |
| |
| encoding ??= utf8; |
| return PubProcess(process, description, |
| encoding: encoding, forwardStdio: forwardStdio); |
| } |
| |
| /// This is protected. |
| PubProcess(process, description, |
| {Encoding encoding, bool forwardStdio = false}) |
| : super(process, description, |
| encoding: encoding, forwardStdio: forwardStdio); |
| |
| final _logLineRegExp = RegExp(r'^([A-Z ]{4})[:|] (.*)$'); |
| final Map<String, log.Level> _logLevels = [ |
| log.Level.ERROR, |
| log.Level.WARNING, |
| log.Level.MESSAGE, |
| log.Level.IO, |
| log.Level.SOLVER, |
| log.Level.FINE |
| ].fold({}, (levels, level) { |
| levels[level.name] = level; |
| return levels; |
| }); |
| |
| Stream<Pair<log.Level, String>> _outputToLog( |
| Stream<String> stream, log.Level defaultLevel) { |
| log.Level lastLevel; |
| return stream.map((line) { |
| var match = _logLineRegExp.firstMatch(line); |
| if (match == null) return Pair<log.Level, String>(defaultLevel, line); |
| |
| var level = _logLevels[match[1]] ?? lastLevel; |
| lastLevel = level; |
| return Pair<log.Level, String>(level, match[2]); |
| }); |
| } |
| |
| @override |
| Stream<String> stdoutStream() { |
| return _logSplitter.split().expand((entry) { |
| if (entry.first != log.Level.MESSAGE) return []; |
| return [entry.last]; |
| }); |
| } |
| |
| @override |
| Stream<String> stderrStream() { |
| return _logSplitter.split().expand((entry) { |
| if (entry.first != log.Level.ERROR && entry.first != log.Level.WARNING) { |
| return []; |
| } |
| return [entry.last]; |
| }); |
| } |
| |
| /// A stream of log messages that are silent by default. |
| Stream<String> silentStream() { |
| return _logSplitter.split().expand((entry) { |
| if (entry.first == log.Level.MESSAGE) return []; |
| if (entry.first == log.Level.ERROR) return []; |
| if (entry.first == log.Level.WARNING) return []; |
| return [entry.last]; |
| }); |
| } |
| } |
| |
| /// Fails the current test if Git is not installed. |
| /// |
| /// We require machines running these tests to have git installed. This |
| /// validation gives an easier-to-understand error when that requirement isn't |
| /// met than just failing in the middle of a test when pub invokes git. |
| void ensureGit() { |
| if (!gitlib.isInstalled) fail('Git must be installed to run this test.'); |
| } |
| |
| /// Creates a lock file for [package] without running `pub get`. |
| /// |
| /// [sandbox] is a list of path dependencies to be found in the sandbox |
| /// directory. |
| /// |
| /// [hosted] is a list of package names to version strings for dependencies on |
| /// hosted packages. |
| Future createLockFile(String package, |
| {Iterable<String> sandbox, Map<String, String> hosted}) async { |
| var cache = SystemCache(rootDir: _pathInSandbox(cachePath)); |
| |
| var lockFile = |
| _createLockFile(cache.sources, sandbox: sandbox, hosted: hosted); |
| |
| await d.dir(package, [ |
| d.file('pubspec.lock', lockFile.serialize(null)), |
| d.file('.packages', lockFile.packagesFile(cache, package)) |
| ]).create(); |
| } |
| |
| /// Like [createLockFile], but creates only a `.packages` file without a |
| /// lockfile. |
| Future createPackagesFile(String package, |
| {Iterable<String> sandbox, Map<String, String> hosted}) async { |
| var cache = SystemCache(rootDir: _pathInSandbox(cachePath)); |
| var lockFile = |
| _createLockFile(cache.sources, sandbox: sandbox, hosted: hosted); |
| |
| await d.dir(package, |
| [d.file('.packages', lockFile.packagesFile(cache, package))]).create(); |
| } |
| |
| /// Creates a lock file for [sources] without running `pub get`. |
| /// |
| /// [sandbox] is a list of path dependencies to be found in the sandbox |
| /// directory. |
| /// |
| /// [hosted] is a list of package names to version strings for dependencies on |
| /// hosted packages. |
| LockFile _createLockFile(SourceRegistry sources, |
| {Iterable<String> sandbox, Map<String, String> hosted}) { |
| var dependencies = {}; |
| |
| if (sandbox != null) { |
| for (var package in sandbox) { |
| dependencies[package] = '../$package'; |
| } |
| } |
| |
| var packages = dependencies.keys.map((name) { |
| var dependencyPath = dependencies[name]; |
| return sources.path.idFor(name, Version(0, 0, 0), dependencyPath); |
| }).toList(); |
| |
| if (hosted != null) { |
| hosted.forEach((name, version) { |
| var id = sources.hosted.idFor(name, Version.parse(version)); |
| packages.add(id); |
| }); |
| } |
| |
| return LockFile(packages); |
| } |
| |
| /// Uses [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 = innerHttpClient; |
| innerHttpClient = client; |
| addTearDown(() { |
| innerHttpClient = oldInnerClient; |
| }); |
| } |
| |
| /// Describes a map representing a library package with the given [name], |
| /// [version], and [dependencies]. |
| Map packageMap( |
| String name, |
| String version, [ |
| Map dependencies, |
| Map devDependencies, |
| Map environment, |
| ]) { |
| var package = <String, dynamic>{ |
| 'name': name, |
| 'version': version, |
| 'homepage': 'http://pub.dartlang.org', |
| 'description': 'A package, I guess.' |
| }; |
| |
| if (dependencies != null) package['dependencies'] = dependencies; |
| if (devDependencies != null) package['dev_dependencies'] = devDependencies; |
| if (environment != null) package['environment'] = environment; |
| return package; |
| } |
| |
| /// Returns a Map in the format used by the pub.dartlang.org API to represent a |
| /// package version. |
| /// |
| /// [pubspec] is the parsed pubspec of the package version. If [full] is true, |
| /// this returns the complete map, including metadata that's only included when |
| /// requesting the package version directly. |
| Map packageVersionApiMap(String hostedUrl, Map pubspec, {bool full = false}) { |
| var name = pubspec['name']; |
| var version = pubspec['version']; |
| var map = { |
| 'pubspec': pubspec, |
| 'version': version, |
| 'archive_url': '$hostedUrl/packages/$name/versions/$version.tar.gz', |
| }; |
| |
| if (full) { |
| map.addAll({ |
| 'downloads': 0, |
| 'created': '2012-09-25T18:38:28.685260', |
| 'libraries': ['$name.dart'], |
| 'uploader': ['nweiz@google.com'] |
| }); |
| } |
| |
| return map; |
| } |
| |
| /// Returns the name of the shell script for a binstub named [name]. |
| /// |
| /// Adds a ".bat" extension on Windows. |
| String binStubName(String name) => Platform.isWindows ? '$name.bat' : name; |
| |
| /// Compares the [actual] output from running pub with [expected]. |
| /// |
| /// If [expected] is a [String], ignores leading and trailing whitespace |
| /// differences and tries to report the offending difference in a nice way. |
| /// |
| /// If it's a [RegExp] or [Matcher], just reports whether the output matches. |
| void _validateOutput( |
| List<String> failures, String pipe, expected, String actual) { |
| if (expected == null) return; |
| |
| if (expected is String) { |
| _validateOutputString(failures, pipe, expected, actual); |
| } else { |
| if (expected is RegExp) expected = matches(expected); |
| expect(actual, expected); |
| } |
| } |
| |
| void _validateOutputString( |
| List<String> failures, String pipe, String expected, String actual) { |
| var actualLines = actual.split('\n'); |
| var expectedLines = expected.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 (expectedLines.last.trim() == '') { |
| expectedLines.removeLast(); |
| } |
| |
| var results = <String>[]; |
| var failed = false; |
| |
| // Compare them line by line to see which ones match. |
| var length = max(expectedLines.length, actualLines.length); |
| for (var i = 0; i < length; i++) { |
| if (i >= actualLines.length) { |
| // Missing output. |
| failed = true; |
| results.add('? ${expectedLines[i]}'); |
| } else if (i >= expectedLines.length) { |
| // Unexpected extra output. |
| failed = true; |
| results.add('X ${actualLines[i]}'); |
| } else { |
| var expectedLine = expectedLines[i].trim(); |
| var actualLine = actualLines[i].trim(); |
| |
| if (expectedLine != actualLine) { |
| // Mismatched lines. |
| failed = true; |
| results.add('X ${actualLines[i]}'); |
| } else { |
| // Output is OK, but include it in case other lines are wrong. |
| results.add('| ${actualLines[i]}'); |
| } |
| } |
| } |
| |
| // If any lines mismatched, show the expected and actual. |
| if (failed) { |
| failures.add('Expected $pipe:'); |
| failures.addAll(expectedLines.map((line) => '| $line')); |
| failures.add('Got:'); |
| failures.addAll(results); |
| } |
| } |
| |
| /// Validates that [actualText] is a string of JSON that matches [expected], |
| /// which may be a literal JSON object, or any other [Matcher]. |
| void _validateOutputJson( |
| List<String> failures, String pipe, expected, String actualText) { |
| Map actual; |
| try { |
| actual = jsonDecode(actualText); |
| } on FormatException { |
| failures.add('Expected $pipe JSON:'); |
| failures.add(expected); |
| failures.add('Got invalid JSON:'); |
| failures.add(actualText); |
| } |
| |
| // Remove dart2js's timing logs, which would otherwise cause tests to fail |
| // flakily when compilation takes a long time. |
| actual['log']?.removeWhere((entry) => |
| entry['level'] == 'Fine' && |
| entry['message'].startsWith('Not yet complete after')); |
| |
| // Match against the expectation. |
| expect(actual, expected); |
| } |
| |
| /// A function that creates a [Validator] subclass. |
| typedef ValidatorCreator = Validator Function(Entrypoint entrypoint); |
| |
| /// Schedules a single [Validator] to run on the [appPath]. |
| /// |
| /// Returns a scheduled Future that contains the validator after validation. |
| Future<Validator> validatePackage(ValidatorCreator fn) async { |
| var cache = SystemCache(rootDir: _pathInSandbox(cachePath)); |
| var validator = fn(Entrypoint(_pathInSandbox(appPath), cache)); |
| await validator.validate(); |
| return validator; |
| } |
| |
| /// A matcher that matches a Pair. |
| Matcher pairOf(firstMatcher, lastMatcher) => |
| _PairMatcher(wrapMatcher(firstMatcher), wrapMatcher(lastMatcher)); |
| |
| class _PairMatcher extends Matcher { |
| final Matcher _firstMatcher; |
| final Matcher _lastMatcher; |
| |
| _PairMatcher(this._firstMatcher, this._lastMatcher); |
| |
| @override |
| bool matches(item, Map matchState) { |
| if (item is! Pair) return false; |
| return _firstMatcher.matches(item.first, matchState) && |
| _lastMatcher.matches(item.last, matchState); |
| } |
| |
| @override |
| Description describe(Description description) { |
| return description.addAll('(', ', ', ')', [_firstMatcher, _lastMatcher]); |
| } |
| } |
| |
| /// Returns a matcher that asserts that a string contains [times] distinct |
| /// occurrences of [pattern], which must be a regular expression pattern. |
| Matcher matchesMultiple(String pattern, int times) { |
| var buffer = StringBuffer(pattern); |
| for (var i = 1; i < times; i++) { |
| buffer.write(r'(.|\n)*'); |
| buffer.write(pattern); |
| } |
| return matches(buffer.toString()); |
| } |
| |
| /// A [StreamMatcher] that matches multiple lines of output. |
| StreamMatcher emitsLines(String output) => emitsInOrder(output.split('\n')); |