| OLD | NEW |
| 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. |
| 6 /// |
| 7 /// Unlike typical unit tests, most pub tests are integration tests that stage |
| 8 /// some stuff on the file system, run pub, and then validate the results. This |
| 9 /// library provides an API to build tests like that. |
| 1 library test_pub; | 10 library test_pub; |
| 11 |
| 2 import 'dart:async'; | 12 import 'dart:async'; |
| 3 import 'dart:convert'; | 13 import 'dart:convert'; |
| 4 import 'dart:io'; | 14 import 'dart:io'; |
| 5 import 'dart:math'; | 15 import 'dart:math'; |
| 16 |
| 6 import 'package:http/testing.dart'; | 17 import 'package:http/testing.dart'; |
| 7 import 'package:path/path.dart' as p; | 18 import 'package:path/path.dart' as p; |
| 8 import 'package:pub_semver/pub_semver.dart'; | 19 import 'package:pub_semver/pub_semver.dart'; |
| 9 import 'package:scheduled_test/scheduled_process.dart'; | 20 import 'package:scheduled_test/scheduled_process.dart'; |
| 10 import 'package:scheduled_test/scheduled_server.dart'; | 21 import 'package:scheduled_test/scheduled_server.dart'; |
| 11 import 'package:scheduled_test/scheduled_stream.dart'; | 22 import 'package:scheduled_test/scheduled_stream.dart'; |
| 12 import 'package:scheduled_test/scheduled_test.dart' hide fail; | 23 import 'package:scheduled_test/scheduled_test.dart' hide fail; |
| 13 import 'package:shelf/shelf.dart' as shelf; | 24 import 'package:shelf/shelf.dart' as shelf; |
| 14 import 'package:shelf/shelf_io.dart' as shelf_io; | 25 import 'package:shelf/shelf_io.dart' as shelf_io; |
| 15 import 'package:unittest/compact_vm_config.dart'; | 26 import 'package:unittest/compact_vm_config.dart'; |
| 16 import 'package:yaml/yaml.dart'; | 27 import 'package:yaml/yaml.dart'; |
| 28 |
| 17 import '../lib/src/entrypoint.dart'; | 29 import '../lib/src/entrypoint.dart'; |
| 18 import '../lib/src/exit_codes.dart' as exit_codes; | 30 import '../lib/src/exit_codes.dart' as exit_codes; |
| 31 // TODO(rnystrom): Using "gitlib" as the prefix here is ugly, but "git" collides |
| 32 // with the git descriptor method. Maybe we should try to clean up the top level |
| 33 // scope a bit? |
| 19 import '../lib/src/git.dart' as gitlib; | 34 import '../lib/src/git.dart' as gitlib; |
| 20 import '../lib/src/http.dart'; | 35 import '../lib/src/http.dart'; |
| 21 import '../lib/src/io.dart'; | 36 import '../lib/src/io.dart'; |
| 22 import '../lib/src/lock_file.dart'; | 37 import '../lib/src/lock_file.dart'; |
| 23 import '../lib/src/log.dart' as log; | 38 import '../lib/src/log.dart' as log; |
| 24 import '../lib/src/package.dart'; | 39 import '../lib/src/package.dart'; |
| 25 import '../lib/src/pubspec.dart'; | 40 import '../lib/src/pubspec.dart'; |
| 26 import '../lib/src/source/hosted.dart'; | 41 import '../lib/src/source/hosted.dart'; |
| 27 import '../lib/src/source/path.dart'; | 42 import '../lib/src/source/path.dart'; |
| 28 import '../lib/src/source_registry.dart'; | 43 import '../lib/src/source_registry.dart'; |
| 29 import '../lib/src/system_cache.dart'; | 44 import '../lib/src/system_cache.dart'; |
| 30 import '../lib/src/utils.dart'; | 45 import '../lib/src/utils.dart'; |
| 31 import '../lib/src/validator.dart'; | 46 import '../lib/src/validator.dart'; |
| 32 import 'descriptor.dart' as d; | 47 import 'descriptor.dart' as d; |
| 33 import 'serve_packages.dart'; | 48 import 'serve_packages.dart'; |
| 49 |
| 34 export 'serve_packages.dart'; | 50 export 'serve_packages.dart'; |
| 51 |
| 52 /// This should be called at the top of a test file to set up an appropriate |
| 53 /// test configuration for the machine running the tests. |
| 35 initConfig() { | 54 initConfig() { |
| 36 useCompactVMConfiguration(); | 55 useCompactVMConfiguration(); |
| 37 filterStacks = true; | 56 filterStacks = true; |
| 38 unittestConfiguration.timeout = null; | 57 unittestConfiguration.timeout = null; |
| 39 } | 58 } |
| 59 |
| 60 /// The current [HttpServer] created using [serve]. |
| 40 var _server; | 61 var _server; |
| 62 |
| 63 /// The list of paths that have been requested from the server since the last |
| 64 /// call to [getRequestedPaths]. |
| 41 final _requestedPaths = <String>[]; | 65 final _requestedPaths = <String>[]; |
| 66 |
| 67 /// The cached value for [_portCompleter]. |
| 42 Completer<int> _portCompleterCache; | 68 Completer<int> _portCompleterCache; |
| 69 |
| 70 /// A [Matcher] that matches JavaScript generated by dart2js with minification |
| 71 /// enabled. |
| 43 Matcher isMinifiedDart2JSOutput = | 72 Matcher isMinifiedDart2JSOutput = |
| 44 isNot(contains("// The code supports the following hooks")); | 73 isNot(contains("// The code supports the following hooks")); |
| 74 |
| 75 /// A [Matcher] that matches JavaScript generated by dart2js with minification |
| 76 /// disabled. |
| 45 Matcher isUnminifiedDart2JSOutput = | 77 Matcher isUnminifiedDart2JSOutput = |
| 46 contains("// The code supports the following hooks"); | 78 contains("// The code supports the following hooks"); |
| 79 |
| 80 /// A map from package names to paths from which those packages should be loaded |
| 81 /// for [createLockFile]. |
| 82 /// |
| 83 /// This allows older versions of dependencies than those that exist in the repo |
| 84 /// to be used when testing pub. |
| 47 Map<String, String> _packageOverrides; | 85 Map<String, String> _packageOverrides; |
| 86 |
| 87 /// A map from barback versions to the paths of directories in the repo |
| 88 /// containing them. |
| 89 /// |
| 90 /// This includes the latest version of barback from pkg as well as all old |
| 91 /// versions of barback in third_party. |
| 48 final _barbackVersions = _findBarbackVersions(); | 92 final _barbackVersions = _findBarbackVersions(); |
| 93 |
| 94 /// Some older barback versions require older versions of barback's dependencies |
| 95 /// than those that are in the repo. |
| 96 /// |
| 97 /// This is a map from barback version ranges to the dependencies for those |
| 98 /// barback versions. Each dependency version listed here should be included in |
| 99 /// third_party/pkg. |
| 49 final _barbackDeps = { | 100 final _barbackDeps = { |
| 50 new VersionConstraint.parse("<0.15.0"): { | 101 new VersionConstraint.parse("<0.15.0"): { |
| 51 "source_maps": "0.9.4" | 102 "source_maps": "0.9.4" |
| 52 } | 103 } |
| 53 }; | 104 }; |
| 105 |
| 106 /// Populates [_barbackVersions]. |
| 54 Map<Version, String> _findBarbackVersions() { | 107 Map<Version, String> _findBarbackVersions() { |
| 55 var versions = {}; | 108 var versions = {}; |
| 56 var currentBarback = p.join(repoRoot, 'pkg', 'barback'); | 109 var currentBarback = p.join(repoRoot, 'pkg', 'barback'); |
| 57 versions[new Pubspec.load(currentBarback, new SourceRegistry()).version] = | 110 versions[new Pubspec.load(currentBarback, new SourceRegistry()).version] = |
| 58 currentBarback; | 111 currentBarback; |
| 112 |
| 59 for (var dir in listDir(p.join(repoRoot, 'third_party', 'pkg'))) { | 113 for (var dir in listDir(p.join(repoRoot, 'third_party', 'pkg'))) { |
| 60 var basename = p.basename(dir); | 114 var basename = p.basename(dir); |
| 61 if (!basename.startsWith('barback')) continue; | 115 if (!basename.startsWith('barback')) continue; |
| 62 versions[new Version.parse(split1(basename, '-').last)] = dir; | 116 versions[new Version.parse(split1(basename, '-').last)] = dir; |
| 63 } | 117 } |
| 118 |
| 64 return versions; | 119 return versions; |
| 65 } | 120 } |
| 121 |
| 122 /// Runs the tests in [callback] against all versions of barback in the repo |
| 123 /// that match [versionConstraint]. |
| 124 /// |
| 125 /// This is used to test that pub doesn't accidentally break older versions of |
| 126 /// barback that it's committed to supporting. Only versions `0.13.0` and later |
| 127 /// will be tested. |
| 66 void withBarbackVersions(String versionConstraint, void callback()) { | 128 void withBarbackVersions(String versionConstraint, void callback()) { |
| 67 var constraint = new VersionConstraint.parse(versionConstraint); | 129 var constraint = new VersionConstraint.parse(versionConstraint); |
| 130 |
| 68 var validVersions = _barbackVersions.keys.where(constraint.allows); | 131 var validVersions = _barbackVersions.keys.where(constraint.allows); |
| 69 if (validVersions.isEmpty) { | 132 if (validVersions.isEmpty) { |
| 70 throw new ArgumentError( | 133 throw new ArgumentError( |
| 71 'No available barback version matches "$versionConstraint".'); | 134 'No available barback version matches "$versionConstraint".'); |
| 72 } | 135 } |
| 136 |
| 73 for (var version in validVersions) { | 137 for (var version in validVersions) { |
| 74 group("with barback $version", () { | 138 group("with barback $version", () { |
| 75 setUp(() { | 139 setUp(() { |
| 76 _packageOverrides = {}; | 140 _packageOverrides = {}; |
| 77 _packageOverrides['barback'] = _barbackVersions[version]; | 141 _packageOverrides['barback'] = _barbackVersions[version]; |
| 78 _barbackDeps.forEach((constraint, deps) { | 142 _barbackDeps.forEach((constraint, deps) { |
| 79 if (!constraint.allows(version)) return; | 143 if (!constraint.allows(version)) return; |
| 80 deps.forEach((packageName, version) { | 144 deps.forEach((packageName, version) { |
| 81 _packageOverrides[packageName] = | 145 _packageOverrides[packageName] = |
| 82 p.join(repoRoot, 'third_party', 'pkg', '$packageName-$version'); | 146 p.join(repoRoot, 'third_party', 'pkg', '$packageName-$version'); |
| 83 }); | 147 }); |
| 84 }); | 148 }); |
| 149 |
| 85 currentSchedule.onComplete.schedule(() { | 150 currentSchedule.onComplete.schedule(() { |
| 86 _packageOverrides = null; | 151 _packageOverrides = null; |
| 87 }); | 152 }); |
| 88 }); | 153 }); |
| 154 |
| 89 callback(); | 155 callback(); |
| 90 }); | 156 }); |
| 91 } | 157 } |
| 92 } | 158 } |
| 159 |
| 160 /// The completer for [port]. |
| 93 Completer<int> get _portCompleter { | 161 Completer<int> get _portCompleter { |
| 94 if (_portCompleterCache != null) return _portCompleterCache; | 162 if (_portCompleterCache != null) return _portCompleterCache; |
| 95 _portCompleterCache = new Completer<int>(); | 163 _portCompleterCache = new Completer<int>(); |
| 96 currentSchedule.onComplete.schedule(() { | 164 currentSchedule.onComplete.schedule(() { |
| 97 _portCompleterCache = null; | 165 _portCompleterCache = null; |
| 98 }, 'clearing the port completer'); | 166 }, 'clearing the port completer'); |
| 99 return _portCompleterCache; | 167 return _portCompleterCache; |
| 100 } | 168 } |
| 169 |
| 170 /// A future that will complete to the port used for the current server. |
| 101 Future<int> get port => _portCompleter.future; | 171 Future<int> get port => _portCompleter.future; |
| 172 |
| 173 /// Gets the list of paths that have been requested from the server since the |
| 174 /// last time this was called (or since the server was first spun up). |
| 102 Future<List<String>> getRequestedPaths() { | 175 Future<List<String>> getRequestedPaths() { |
| 103 return schedule(() { | 176 return schedule(() { |
| 104 var paths = _requestedPaths.toList(); | 177 var paths = _requestedPaths.toList(); |
| 105 _requestedPaths.clear(); | 178 _requestedPaths.clear(); |
| 106 return paths; | 179 return paths; |
| 107 }, "get previous network requests"); | 180 }, "get previous network requests"); |
| 108 } | 181 } |
| 182 |
| 183 /// Creates an HTTP server to serve [contents] as static files. |
| 184 /// |
| 185 /// This server will exist only for the duration of the pub run. Subsequent |
| 186 /// calls to [serve] replace the previous server. |
| 109 void serve([List<d.Descriptor> contents]) { | 187 void serve([List<d.Descriptor> contents]) { |
| 110 var baseDir = d.dir("serve-dir", contents); | 188 var baseDir = d.dir("serve-dir", contents); |
| 189 |
| 111 _hasServer = true; | 190 _hasServer = true; |
| 191 |
| 112 schedule(() { | 192 schedule(() { |
| 113 return _closeServer().then((_) { | 193 return _closeServer().then((_) { |
| 114 return shelf_io.serve((request) { | 194 return shelf_io.serve((request) { |
| 115 currentSchedule.heartbeat(); | 195 currentSchedule.heartbeat(); |
| 116 var path = p.posix.fromUri(request.url.path.replaceFirst("/", "")); | 196 var path = p.posix.fromUri(request.url.path.replaceFirst("/", "")); |
| 117 _requestedPaths.add(path); | 197 _requestedPaths.add(path); |
| 198 |
| 118 return validateStream( | 199 return validateStream( |
| 119 baseDir.load( | 200 baseDir.load( |
| 120 path)).then((stream) => new shelf.Response.ok(stream)).catchErro
r((error) { | 201 path)).then((stream) => new shelf.Response.ok(stream)).catchErro
r((error) { |
| 121 return new shelf.Response.notFound('File "$path" not found.'); | 202 return new shelf.Response.notFound('File "$path" not found.'); |
| 122 }); | 203 }); |
| 123 }, 'localhost', 0).then((server) { | 204 }, 'localhost', 0).then((server) { |
| 124 _server = server; | 205 _server = server; |
| 125 _portCompleter.complete(_server.port); | 206 _portCompleter.complete(_server.port); |
| 126 currentSchedule.onComplete.schedule(_closeServer); | 207 currentSchedule.onComplete.schedule(_closeServer); |
| 127 }); | 208 }); |
| 128 }); | 209 }); |
| 129 }, 'starting a server serving:\n${baseDir.describe()}'); | 210 }, 'starting a server serving:\n${baseDir.describe()}'); |
| 130 } | 211 } |
| 212 |
| 213 /// Closes [_server]. |
| 214 /// |
| 215 /// Returns a [Future] that completes after the [_server] is closed. |
| 131 Future _closeServer() { | 216 Future _closeServer() { |
| 132 if (_server == null) return new Future.value(); | 217 if (_server == null) return new Future.value(); |
| 133 var future = _server.close(); | 218 var future = _server.close(); |
| 134 _server = null; | 219 _server = null; |
| 135 _hasServer = false; | 220 _hasServer = false; |
| 136 _portCompleterCache = null; | 221 _portCompleterCache = null; |
| 137 return future; | 222 return future; |
| 138 } | 223 } |
| 224 |
| 225 /// `true` if the current test spins up an HTTP server. |
| 139 bool _hasServer = false; | 226 bool _hasServer = false; |
| 227 |
| 228 /// Converts [value] into a YAML string. |
| 140 String yaml(value) => JSON.encode(value); | 229 String yaml(value) => JSON.encode(value); |
| 230 |
| 231 /// The full path to the created sandbox directory for an integration test. |
| 141 String get sandboxDir => _sandboxDir; | 232 String get sandboxDir => _sandboxDir; |
| 142 String _sandboxDir; | 233 String _sandboxDir; |
| 234 |
| 235 /// The path to the Dart repo's packages. |
| 143 final String pkgPath = | 236 final String pkgPath = |
| 144 p.absolute(p.join(p.dirname(Platform.executable), '../../../../pkg')); | 237 p.absolute(p.join(p.dirname(Platform.executable), '../../../../pkg')); |
| 238 |
| 239 /// The path of the package cache directory used for tests, relative to the |
| 240 /// sandbox directory. |
| 145 final String cachePath = "cache"; | 241 final String cachePath = "cache"; |
| 242 |
| 243 /// The path of the mock app directory used for tests, relative to the sandbox |
| 244 /// directory. |
| 146 final String appPath = "myapp"; | 245 final String appPath = "myapp"; |
| 246 |
| 247 /// The path of the packages directory in the mock app used for tests, relative |
| 248 /// to the sandbox directory. |
| 147 final String packagesPath = "$appPath/packages"; | 249 final String packagesPath = "$appPath/packages"; |
| 250 |
| 251 /// Set to true when the current batch of scheduled events should be aborted. |
| 148 bool _abortScheduled = false; | 252 bool _abortScheduled = false; |
| 253 |
| 254 /// Enum identifying a pub command that can be run with a well-defined success |
| 255 /// output. |
| 149 class RunCommand { | 256 class RunCommand { |
| 150 static final get = new RunCommand( | 257 static final get = new RunCommand( |
| 151 'get', | 258 'get', |
| 152 new RegExp(r'Got dependencies!|Changed \d+ dependenc(y|ies)!')); | 259 new RegExp(r'Got dependencies!|Changed \d+ dependenc(y|ies)!')); |
| 153 static final upgrade = new RunCommand( | 260 static final upgrade = new RunCommand( |
| 154 'upgrade', | 261 'upgrade', |
| 155 new RegExp(r'(No dependencies changed\.|Changed \d+ dependenc(y|ies)!)$'))
; | 262 new RegExp(r'(No dependencies changed\.|Changed \d+ dependenc(y|ies)!)$'))
; |
| 156 static final downgrade = new RunCommand( | 263 static final downgrade = new RunCommand( |
| 157 'downgrade', | 264 'downgrade', |
| 158 new RegExp(r'(No dependencies changed\.|Changed \d+ dependenc(y|ies)!)$'))
; | 265 new RegExp(r'(No dependencies changed\.|Changed \d+ dependenc(y|ies)!)$'))
; |
| 266 |
| 159 final String name; | 267 final String name; |
| 160 final RegExp success; | 268 final RegExp success; |
| 161 RunCommand(this.name, this.success); | 269 RunCommand(this.name, this.success); |
| 162 } | 270 } |
| 271 |
| 272 /// Runs the tests defined within [callback] using both pub get and pub upgrade. |
| 273 /// |
| 274 /// Many tests validate behavior that is the same between pub get and |
| 275 /// upgrade have the same behavior. Instead of duplicating those tests, this |
| 276 /// takes a callback that defines get/upgrade agnostic tests and runs them |
| 277 /// with both commands. |
| 163 void forBothPubGetAndUpgrade(void callback(RunCommand command)) { | 278 void forBothPubGetAndUpgrade(void callback(RunCommand command)) { |
| 164 group(RunCommand.get.name, () => callback(RunCommand.get)); | 279 group(RunCommand.get.name, () => callback(RunCommand.get)); |
| 165 group(RunCommand.upgrade.name, () => callback(RunCommand.upgrade)); | 280 group(RunCommand.upgrade.name, () => callback(RunCommand.upgrade)); |
| 166 } | 281 } |
| 282 |
| 283 /// Schedules an invocation of pub [command] and validates that it completes |
| 284 /// in an expected way. |
| 285 /// |
| 286 /// By default, this validates that the command completes successfully and |
| 287 /// understands the normal output of a successful pub command. If [warning] is |
| 288 /// given, it expects the command to complete successfully *and* print |
| 289 /// [warning] to stderr. If [error] is given, it expects the command to *only* |
| 290 /// print [error] to stderr. [output], [error], and [warning] may be strings, |
| 291 /// [RegExp]s, or [Matcher]s. |
| 292 /// |
| 293 /// If [exitCode] is given, expects the command to exit with that code. |
| 294 // TODO(rnystrom): Clean up other tests to call this when possible. |
| 167 void pubCommand(RunCommand command, {Iterable<String> args, output, error, | 295 void pubCommand(RunCommand command, {Iterable<String> args, output, error, |
| 168 warning, int exitCode}) { | 296 warning, int exitCode}) { |
| 169 if (error != null && warning != null) { | 297 if (error != null && warning != null) { |
| 170 throw new ArgumentError("Cannot pass both 'error' and 'warning'."); | 298 throw new ArgumentError("Cannot pass both 'error' and 'warning'."); |
| 171 } | 299 } |
| 300 |
| 172 var allArgs = [command.name]; | 301 var allArgs = [command.name]; |
| 173 if (args != null) allArgs.addAll(args); | 302 if (args != null) allArgs.addAll(args); |
| 303 |
| 174 if (output == null) output = command.success; | 304 if (output == null) output = command.success; |
| 305 |
| 175 if (error != null && exitCode == null) exitCode = 1; | 306 if (error != null && exitCode == null) exitCode = 1; |
| 307 |
| 308 // No success output on an error. |
| 176 if (error != null) output = null; | 309 if (error != null) output = null; |
| 177 if (warning != null) error = warning; | 310 if (warning != null) error = warning; |
| 311 |
| 178 schedulePub(args: allArgs, output: output, error: error, exitCode: exitCode); | 312 schedulePub(args: allArgs, output: output, error: error, exitCode: exitCode); |
| 179 } | 313 } |
| 314 |
| 180 void pubGet({Iterable<String> args, output, error, warning, int exitCode}) { | 315 void pubGet({Iterable<String> args, output, error, warning, int exitCode}) { |
| 181 pubCommand( | 316 pubCommand( |
| 182 RunCommand.get, | 317 RunCommand.get, |
| 183 args: args, | 318 args: args, |
| 184 output: output, | 319 output: output, |
| 185 error: error, | 320 error: error, |
| 186 warning: warning, | 321 warning: warning, |
| 187 exitCode: exitCode); | 322 exitCode: exitCode); |
| 188 } | 323 } |
| 324 |
| 189 void pubUpgrade({Iterable<String> args, output, error, warning, int exitCode}) { | 325 void pubUpgrade({Iterable<String> args, output, error, warning, int exitCode}) { |
| 190 pubCommand( | 326 pubCommand( |
| 191 RunCommand.upgrade, | 327 RunCommand.upgrade, |
| 192 args: args, | 328 args: args, |
| 193 output: output, | 329 output: output, |
| 194 error: error, | 330 error: error, |
| 195 warning: warning, | 331 warning: warning, |
| 196 exitCode: exitCode); | 332 exitCode: exitCode); |
| 197 } | 333 } |
| 334 |
| 198 void pubDowngrade({Iterable<String> args, output, error, warning, int exitCode}) | 335 void pubDowngrade({Iterable<String> args, output, error, warning, int exitCode}) |
| 199 { | 336 { |
| 200 pubCommand( | 337 pubCommand( |
| 201 RunCommand.downgrade, | 338 RunCommand.downgrade, |
| 202 args: args, | 339 args: args, |
| 203 output: output, | 340 output: output, |
| 204 error: error, | 341 error: error, |
| 205 warning: warning, | 342 warning: warning, |
| 206 exitCode: exitCode); | 343 exitCode: exitCode); |
| 207 } | 344 } |
| 345 |
| 346 /// Schedules starting the "pub [global] run" process and validates the |
| 347 /// expected startup output. |
| 348 /// |
| 349 /// If [global] is `true`, this invokes "pub global run", otherwise it does |
| 350 /// "pub run". |
| 351 /// |
| 352 /// Returns the `pub run` process. |
| 208 ScheduledProcess pubRun({bool global: false, Iterable<String> args}) { | 353 ScheduledProcess pubRun({bool global: false, Iterable<String> args}) { |
| 209 var pubArgs = global ? ["global", "run"] : ["run"]; | 354 var pubArgs = global ? ["global", "run"] : ["run"]; |
| 210 pubArgs.addAll(args); | 355 pubArgs.addAll(args); |
| 211 var pub = startPub(args: pubArgs); | 356 var pub = startPub(args: pubArgs); |
| 357 |
| 358 // Loading sources and transformers isn't normally printed, but the pub test |
| 359 // infrastructure runs pub in verbose mode, which enables this. |
| 212 pub.stdout.expect(consumeWhile(startsWith("Loading"))); | 360 pub.stdout.expect(consumeWhile(startsWith("Loading"))); |
| 361 |
| 213 return pub; | 362 return pub; |
| 214 } | 363 } |
| 364 |
| 365 /// Defines an integration test. |
| 366 /// |
| 367 /// The [body] should schedule a series of operations which will be run |
| 368 /// asynchronously. |
| 215 void integration(String description, void body()) => | 369 void integration(String description, void body()) => |
| 216 _integration(description, body, test); | 370 _integration(description, body, test); |
| 371 |
| 372 /// Like [integration], but causes only this test to run. |
| 217 void solo_integration(String description, void body()) => | 373 void solo_integration(String description, void body()) => |
| 218 _integration(description, body, solo_test); | 374 _integration(description, body, solo_test); |
| 375 |
| 219 void _integration(String description, void body(), [Function testFn]) { | 376 void _integration(String description, void body(), [Function testFn]) { |
| 220 testFn(description, () { | 377 testFn(description, () { |
| 378 // TODO(nweiz): remove this when issue 15362 is fixed. |
| 221 currentSchedule.timeout *= 2; | 379 currentSchedule.timeout *= 2; |
| 380 |
| 381 // The windows bots are very slow, so we increase the default timeout. |
| 222 if (Platform.operatingSystem == "windows") { | 382 if (Platform.operatingSystem == "windows") { |
| 223 currentSchedule.timeout *= 2; | 383 currentSchedule.timeout *= 2; |
| 224 } | 384 } |
| 385 |
| 225 _sandboxDir = createSystemTempDir(); | 386 _sandboxDir = createSystemTempDir(); |
| 226 d.defaultRoot = sandboxDir; | 387 d.defaultRoot = sandboxDir; |
| 227 currentSchedule.onComplete.schedule( | 388 currentSchedule.onComplete.schedule( |
| 228 () => deleteEntry(_sandboxDir), | 389 () => deleteEntry(_sandboxDir), |
| 229 'deleting the sandbox directory'); | 390 'deleting the sandbox directory'); |
| 391 |
| 392 // Schedule the test. |
| 230 body(); | 393 body(); |
| 231 }); | 394 }); |
| 232 } | 395 } |
| 396 |
| 397 /// Get the path to the root "pub/test" directory containing the pub |
| 398 /// tests. |
| 233 String get testDirectory => p.absolute(p.dirname(libraryPath('test_pub'))); | 399 String get testDirectory => p.absolute(p.dirname(libraryPath('test_pub'))); |
| 400 |
| 401 /// Schedules renaming (moving) the directory at [from] to [to], both of which |
| 402 /// are assumed to be relative to [sandboxDir]. |
| 234 void scheduleRename(String from, String to) { | 403 void scheduleRename(String from, String to) { |
| 235 schedule( | 404 schedule( |
| 236 () => renameDir(p.join(sandboxDir, from), p.join(sandboxDir, to)), | 405 () => renameDir(p.join(sandboxDir, from), p.join(sandboxDir, to)), |
| 237 'renaming $from to $to'); | 406 'renaming $from to $to'); |
| 238 } | 407 } |
| 408 |
| 409 /// Schedules creating a symlink at path [symlink] that points to [target], |
| 410 /// both of which are assumed to be relative to [sandboxDir]. |
| 239 void scheduleSymlink(String target, String symlink) { | 411 void scheduleSymlink(String target, String symlink) { |
| 240 schedule( | 412 schedule( |
| 241 () => createSymlink(p.join(sandboxDir, target), p.join(sandboxDir, symlink
)), | 413 () => createSymlink(p.join(sandboxDir, target), p.join(sandboxDir, symlink
)), |
| 242 'symlinking $target to $symlink'); | 414 'symlinking $target to $symlink'); |
| 243 } | 415 } |
| 416 |
| 417 /// Schedules a call to the Pub command-line utility. |
| 418 /// |
| 419 /// Runs Pub with [args] and validates that its results match [output] (or |
| 420 /// [outputJson]), [error], and [exitCode]. |
| 421 /// |
| 422 /// [output] and [error] can be [String]s, [RegExp]s, or [Matcher]s. |
| 423 /// |
| 424 /// If [outputJson] is given, validates that pub outputs stringified JSON |
| 425 /// matching that object, which can be a literal JSON object or any other |
| 426 /// [Matcher]. |
| 427 /// |
| 428 /// If [environment] is given, any keys in it will override the environment |
| 429 /// variables passed to the spawned process. |
| 244 void schedulePub({List args, output, error, outputJson, int exitCode: | 430 void schedulePub({List args, output, error, outputJson, int exitCode: |
| 245 exit_codes.SUCCESS, Map<String, String> environment}) { | 431 exit_codes.SUCCESS, Map<String, String> environment}) { |
| 432 // Cannot pass both output and outputJson. |
| 246 assert(output == null || outputJson == null); | 433 assert(output == null || outputJson == null); |
| 434 |
| 247 var pub = startPub(args: args, environment: environment); | 435 var pub = startPub(args: args, environment: environment); |
| 248 pub.shouldExit(exitCode); | 436 pub.shouldExit(exitCode); |
| 437 |
| 249 var failures = []; | 438 var failures = []; |
| 250 var stderr; | 439 var stderr; |
| 440 |
| 251 expect( | 441 expect( |
| 252 Future.wait( | 442 Future.wait( |
| 253 [pub.stdoutStream().toList(), pub.stderrStream().toList()]).then((resu
lts) { | 443 [pub.stdoutStream().toList(), pub.stderrStream().toList()]).then((resu
lts) { |
| 254 var stdout = results[0].join("\n"); | 444 var stdout = results[0].join("\n"); |
| 255 stderr = results[1].join("\n"); | 445 stderr = results[1].join("\n"); |
| 446 |
| 256 if (outputJson == null) { | 447 if (outputJson == null) { |
| 257 _validateOutput(failures, 'stdout', output, stdout); | 448 _validateOutput(failures, 'stdout', output, stdout); |
| 258 return null; | 449 return null; |
| 259 } | 450 } |
| 451 |
| 452 // Allow the expected JSON to contain futures. |
| 260 return awaitObject(outputJson).then((resolved) { | 453 return awaitObject(outputJson).then((resolved) { |
| 261 _validateOutputJson(failures, 'stdout', resolved, stdout); | 454 _validateOutputJson(failures, 'stdout', resolved, stdout); |
| 262 }); | 455 }); |
| 263 }).then((_) { | 456 }).then((_) { |
| 264 _validateOutput(failures, 'stderr', error, stderr); | 457 _validateOutput(failures, 'stderr', error, stderr); |
| 458 |
| 265 if (!failures.isEmpty) throw new TestFailure(failures.join('\n')); | 459 if (!failures.isEmpty) throw new TestFailure(failures.join('\n')); |
| 266 }), completes); | 460 }), completes); |
| 267 } | 461 } |
| 462 |
| 463 /// Like [startPub], but runs `pub lish` in particular with [server] used both |
| 464 /// as the OAuth2 server (with "/token" as the token endpoint) and as the |
| 465 /// package server. |
| 466 /// |
| 467 /// Any futures in [args] will be resolved before the process is started. |
| 268 ScheduledProcess startPublish(ScheduledServer server, {List args}) { | 468 ScheduledProcess startPublish(ScheduledServer server, {List args}) { |
| 269 var tokenEndpoint = | 469 var tokenEndpoint = |
| 270 server.url.then((url) => url.resolve('/token').toString()); | 470 server.url.then((url) => url.resolve('/token').toString()); |
| 271 if (args == null) args = []; | 471 if (args == null) args = []; |
| 272 args = flatten(['lish', '--server', tokenEndpoint, args]); | 472 args = flatten(['lish', '--server', tokenEndpoint, args]); |
| 273 return startPub(args: args, tokenEndpoint: tokenEndpoint); | 473 return startPub(args: args, tokenEndpoint: tokenEndpoint); |
| 274 } | 474 } |
| 475 |
| 476 /// Handles the beginning confirmation process for uploading a packages. |
| 477 /// |
| 478 /// Ensures that the right output is shown and then enters "y" to confirm the |
| 479 /// upload. |
| 275 void confirmPublish(ScheduledProcess pub) { | 480 void confirmPublish(ScheduledProcess pub) { |
| 481 // TODO(rnystrom): This is overly specific and inflexible regarding different |
| 482 // test packages. Should validate this a little more loosely. |
| 276 pub.stdout.expect(startsWith('Publishing test_pkg 1.0.0 to ')); | 483 pub.stdout.expect(startsWith('Publishing test_pkg 1.0.0 to ')); |
| 277 pub.stdout.expect( | 484 pub.stdout.expect( |
| 278 emitsLines( | 485 emitsLines( |
| 279 "|-- LICENSE\n" "|-- lib\n" "| '-- test_pkg.dart\n" "'-- pubspec.yam
l\n" "\n" | 486 "|-- LICENSE\n" "|-- lib\n" "| '-- test_pkg.dart\n" "'-- pubspec.yam
l\n" "\n" |
| 280 "Looks great! Are you ready to upload your package (y/n)?")); | 487 "Looks great! Are you ready to upload your package (y/n)?")); |
| 281 pub.writeLine("y"); | 488 pub.writeLine("y"); |
| 282 } | 489 } |
| 490 |
| 491 /// Gets the absolute path to [relPath], which is a relative path in the test |
| 492 /// sandbox. |
| 283 String _pathInSandbox(String relPath) { | 493 String _pathInSandbox(String relPath) { |
| 284 return p.join(p.absolute(sandboxDir), relPath); | 494 return p.join(p.absolute(sandboxDir), relPath); |
| 285 } | 495 } |
| 496 |
| 497 /// Gets the environment variables used to run pub in a test context. |
| 286 Map getPubTestEnvironment([String tokenEndpoint]) { | 498 Map getPubTestEnvironment([String tokenEndpoint]) { |
| 287 var environment = {}; | 499 var environment = {}; |
| 288 environment['_PUB_TESTING'] = 'true'; | 500 environment['_PUB_TESTING'] = 'true'; |
| 289 environment['PUB_CACHE'] = _pathInSandbox(cachePath); | 501 environment['PUB_CACHE'] = _pathInSandbox(cachePath); |
| 502 |
| 503 // Ensure a known SDK version is set for the tests that rely on that. |
| 290 environment['_PUB_TEST_SDK_VERSION'] = "0.1.2+3"; | 504 environment['_PUB_TEST_SDK_VERSION'] = "0.1.2+3"; |
| 505 |
| 291 if (tokenEndpoint != null) { | 506 if (tokenEndpoint != null) { |
| 292 environment['_PUB_TEST_TOKEN_ENDPOINT'] = tokenEndpoint.toString(); | 507 environment['_PUB_TEST_TOKEN_ENDPOINT'] = tokenEndpoint.toString(); |
| 293 } | 508 } |
| 509 |
| 294 return environment; | 510 return environment; |
| 295 } | 511 } |
| 512 |
| 513 /// Starts a Pub process and returns a [ScheduledProcess] that supports |
| 514 /// interaction with that process. |
| 515 /// |
| 516 /// Any futures in [args] will be resolved before the process is started. |
| 517 /// |
| 518 /// If [environment] is given, any keys in it will override the environment |
| 519 /// variables passed to the spawned process. |
| 296 ScheduledProcess startPub({List args, Future<String> tokenEndpoint, Map<String, | 520 ScheduledProcess startPub({List args, Future<String> tokenEndpoint, Map<String, |
| 297 String> environment}) { | 521 String> environment}) { |
| 298 ensureDir(_pathInSandbox(appPath)); | 522 ensureDir(_pathInSandbox(appPath)); |
| 523 |
| 524 // Find a Dart executable we can use to spawn. Use the same one that was |
| 525 // used to run this script itself. |
| 299 var dartBin = Platform.executable; | 526 var dartBin = Platform.executable; |
| 527 |
| 528 // If the executable looks like a path, get its full path. That way we |
| 529 // can still find it when we spawn it with a different working directory. |
| 300 if (dartBin.contains(Platform.pathSeparator)) { | 530 if (dartBin.contains(Platform.pathSeparator)) { |
| 301 dartBin = p.absolute(dartBin); | 531 dartBin = p.absolute(dartBin); |
| 302 } | 532 } |
| 533 |
| 534 // Always run pub from a snapshot. Since we require the SDK to be built, the |
| 535 // snapshot should be there. Note that this *does* mean that the snapshot has |
| 536 // to be manually updated when changing code before running the tests. |
| 537 // Otherwise, you will test against stale data. |
| 538 // |
| 539 // Using the snapshot makes running the tests much faster, which is why we |
| 540 // make this trade-off. |
| 303 var pubPath = p.join(p.dirname(dartBin), 'snapshots/pub.dart.snapshot'); | 541 var pubPath = p.join(p.dirname(dartBin), 'snapshots/pub.dart.snapshot'); |
| 304 var dartArgs = [pubPath, '--verbose']; | 542 var dartArgs = [pubPath, '--verbose']; |
| 305 dartArgs.addAll(args); | 543 dartArgs.addAll(args); |
| 544 |
| 306 if (tokenEndpoint == null) tokenEndpoint = new Future.value(); | 545 if (tokenEndpoint == null) tokenEndpoint = new Future.value(); |
| 307 var environmentFuture = tokenEndpoint.then((tokenEndpoint) { | 546 var environmentFuture = tokenEndpoint.then((tokenEndpoint) { |
| 308 var pubEnvironment = getPubTestEnvironment(tokenEndpoint); | 547 var pubEnvironment = getPubTestEnvironment(tokenEndpoint); |
| 548 |
| 549 // If there is a server running, tell pub what its URL is so hosted |
| 550 // dependencies will look there. |
| 309 if (_hasServer) { | 551 if (_hasServer) { |
| 310 return port.then((p) { | 552 return port.then((p) { |
| 311 pubEnvironment['PUB_HOSTED_URL'] = "http://localhost:$p"; | 553 pubEnvironment['PUB_HOSTED_URL'] = "http://localhost:$p"; |
| 312 return pubEnvironment; | 554 return pubEnvironment; |
| 313 }); | 555 }); |
| 314 } | 556 } |
| 557 |
| 315 return pubEnvironment; | 558 return pubEnvironment; |
| 316 }).then((pubEnvironment) { | 559 }).then((pubEnvironment) { |
| 317 if (environment != null) pubEnvironment.addAll(environment); | 560 if (environment != null) pubEnvironment.addAll(environment); |
| 318 return pubEnvironment; | 561 return pubEnvironment; |
| 319 }); | 562 }); |
| 563 |
| 320 return new PubProcess.start( | 564 return new PubProcess.start( |
| 321 dartBin, | 565 dartBin, |
| 322 dartArgs, | 566 dartArgs, |
| 323 environment: environmentFuture, | 567 environment: environmentFuture, |
| 324 workingDirectory: _pathInSandbox(appPath), | 568 workingDirectory: _pathInSandbox(appPath), |
| 325 description: args.isEmpty ? 'pub' : 'pub ${args.first}'); | 569 description: args.isEmpty ? 'pub' : 'pub ${args.first}'); |
| 326 } | 570 } |
| 571 |
| 572 /// A subclass of [ScheduledProcess] that parses pub's verbose logging output |
| 573 /// and makes [stdout] and [stderr] work as though pub weren't running in |
| 574 /// verbose mode. |
| 327 class PubProcess extends ScheduledProcess { | 575 class PubProcess extends ScheduledProcess { |
| 328 Stream<Pair<log.Level, String>> _log; | 576 Stream<Pair<log.Level, String>> _log; |
| 329 Stream<String> _stdout; | 577 Stream<String> _stdout; |
| 330 Stream<String> _stderr; | 578 Stream<String> _stderr; |
| 579 |
| 331 PubProcess.start(executable, arguments, {workingDirectory, environment, | 580 PubProcess.start(executable, arguments, {workingDirectory, environment, |
| 332 String description, Encoding encoding: UTF8}) | 581 String description, Encoding encoding: UTF8}) |
| 333 : super.start( | 582 : super.start( |
| 334 executable, | 583 executable, |
| 335 arguments, | 584 arguments, |
| 336 workingDirectory: workingDirectory, | 585 workingDirectory: workingDirectory, |
| 337 environment: environment, | 586 environment: environment, |
| 338 description: description, | 587 description: description, |
| 339 encoding: encoding); | 588 encoding: encoding); |
| 589 |
| 340 Stream<Pair<log.Level, String>> _logStream() { | 590 Stream<Pair<log.Level, String>> _logStream() { |
| 341 if (_log == null) { | 591 if (_log == null) { |
| 342 _log = mergeStreams( | 592 _log = mergeStreams( |
| 343 _outputToLog(super.stdoutStream(), log.Level.MESSAGE), | 593 _outputToLog(super.stdoutStream(), log.Level.MESSAGE), |
| 344 _outputToLog(super.stderrStream(), log.Level.ERROR)); | 594 _outputToLog(super.stderrStream(), log.Level.ERROR)); |
| 345 } | 595 } |
| 596 |
| 346 var pair = tee(_log); | 597 var pair = tee(_log); |
| 347 _log = pair.first; | 598 _log = pair.first; |
| 348 return pair.last; | 599 return pair.last; |
| 349 } | 600 } |
| 601 |
| 350 final _logLineRegExp = new RegExp(r"^([A-Z ]{4})[:|] (.*)$"); | 602 final _logLineRegExp = new RegExp(r"^([A-Z ]{4})[:|] (.*)$"); |
| 351 final _logLevels = [ | 603 final _logLevels = [ |
| 352 log.Level.ERROR, | 604 log.Level.ERROR, |
| 353 log.Level.WARNING, | 605 log.Level.WARNING, |
| 354 log.Level.MESSAGE, | 606 log.Level.MESSAGE, |
| 355 log.Level.IO, | 607 log.Level.IO, |
| 356 log.Level.SOLVER, | 608 log.Level.SOLVER, |
| 357 log.Level.FINE].fold(<String, log.Level>{}, (levels, level) { | 609 log.Level.FINE].fold(<String, log.Level>{}, (levels, level) { |
| 358 levels[level.name] = level; | 610 levels[level.name] = level; |
| 359 return levels; | 611 return levels; |
| 360 }); | 612 }); |
| 613 |
| 361 Stream<Pair<log.Level, String>> _outputToLog(Stream<String> stream, | 614 Stream<Pair<log.Level, String>> _outputToLog(Stream<String> stream, |
| 362 log.Level defaultLevel) { | 615 log.Level defaultLevel) { |
| 363 var lastLevel; | 616 var lastLevel; |
| 364 return stream.map((line) { | 617 return stream.map((line) { |
| 365 var match = _logLineRegExp.firstMatch(line); | 618 var match = _logLineRegExp.firstMatch(line); |
| 366 if (match == null) return new Pair<log.Level, String>(defaultLevel, line); | 619 if (match == null) return new Pair<log.Level, String>(defaultLevel, line); |
| 620 |
| 367 var level = _logLevels[match[1]]; | 621 var level = _logLevels[match[1]]; |
| 368 if (level == null) level = lastLevel; | 622 if (level == null) level = lastLevel; |
| 369 lastLevel = level; | 623 lastLevel = level; |
| 370 return new Pair<log.Level, String>(level, match[2]); | 624 return new Pair<log.Level, String>(level, match[2]); |
| 371 }); | 625 }); |
| 372 } | 626 } |
| 627 |
| 373 Stream<String> stdoutStream() { | 628 Stream<String> stdoutStream() { |
| 374 if (_stdout == null) { | 629 if (_stdout == null) { |
| 375 _stdout = _logStream().expand((entry) { | 630 _stdout = _logStream().expand((entry) { |
| 376 if (entry.first != log.Level.MESSAGE) return []; | 631 if (entry.first != log.Level.MESSAGE) return []; |
| 377 return [entry.last]; | 632 return [entry.last]; |
| 378 }); | 633 }); |
| 379 } | 634 } |
| 635 |
| 380 var pair = tee(_stdout); | 636 var pair = tee(_stdout); |
| 381 _stdout = pair.first; | 637 _stdout = pair.first; |
| 382 return pair.last; | 638 return pair.last; |
| 383 } | 639 } |
| 640 |
| 384 Stream<String> stderrStream() { | 641 Stream<String> stderrStream() { |
| 385 if (_stderr == null) { | 642 if (_stderr == null) { |
| 386 _stderr = _logStream().expand((entry) { | 643 _stderr = _logStream().expand((entry) { |
| 387 if (entry.first != log.Level.ERROR && | 644 if (entry.first != log.Level.ERROR && |
| 388 entry.first != log.Level.WARNING) { | 645 entry.first != log.Level.WARNING) { |
| 389 return []; | 646 return []; |
| 390 } | 647 } |
| 391 return [entry.last]; | 648 return [entry.last]; |
| 392 }); | 649 }); |
| 393 } | 650 } |
| 651 |
| 394 var pair = tee(_stderr); | 652 var pair = tee(_stderr); |
| 395 _stderr = pair.first; | 653 _stderr = pair.first; |
| 396 return pair.last; | 654 return pair.last; |
| 397 } | 655 } |
| 398 } | 656 } |
| 657 |
| 658 /// The path to the `packages` directory from which pub loads its dependencies. |
| 399 String get _packageRoot => p.absolute(Platform.packageRoot); | 659 String get _packageRoot => p.absolute(Platform.packageRoot); |
| 660 |
| 661 /// Fails the current test if Git is not installed. |
| 662 /// |
| 663 /// We require machines running these tests to have git installed. This |
| 664 /// validation gives an easier-to-understand error when that requirement isn't |
| 665 /// met than just failing in the middle of a test when pub invokes git. |
| 666 /// |
| 667 /// This also increases the [Schedule] timeout to 30 seconds on Windows, |
| 668 /// where Git runs really slowly. |
| 400 void ensureGit() { | 669 void ensureGit() { |
| 401 if (Platform.operatingSystem == "windows") { | 670 if (Platform.operatingSystem == "windows") { |
| 402 currentSchedule.timeout = new Duration(seconds: 30); | 671 currentSchedule.timeout = new Duration(seconds: 30); |
| 403 } | 672 } |
| 673 |
| 404 if (!gitlib.isInstalled) { | 674 if (!gitlib.isInstalled) { |
| 405 throw new Exception("Git must be installed to run this test."); | 675 throw new Exception("Git must be installed to run this test."); |
| 406 } | 676 } |
| 407 } | 677 } |
| 678 |
| 679 /// Schedules activating a global package [package] without running |
| 680 /// "pub global activate". |
| 681 /// |
| 682 /// This is useful because global packages must be hosted, but the test hosted |
| 683 /// server doesn't serve barback. The other parameters here follow |
| 684 /// [createLockFile]. |
| 408 void makeGlobalPackage(String package, String version, | 685 void makeGlobalPackage(String package, String version, |
| 409 Iterable<d.Descriptor> contents, {Iterable<String> pkg, Map<String, | 686 Iterable<d.Descriptor> contents, {Iterable<String> pkg, Map<String, |
| 410 String> hosted}) { | 687 String> hosted}) { |
| 688 // Start the server so we know what port to use in the cache directory name. |
| 411 serveNoPackages(); | 689 serveNoPackages(); |
| 690 |
| 691 // Create the package in the hosted cache. |
| 412 d.hostedCache([d.dir("$package-$version", contents)]).create(); | 692 d.hostedCache([d.dir("$package-$version", contents)]).create(); |
| 693 |
| 413 var lockFile = _createLockFile(pkg: pkg, hosted: hosted); | 694 var lockFile = _createLockFile(pkg: pkg, hosted: hosted); |
| 695 |
| 696 // Add the root package to the lockfile. |
| 414 var id = | 697 var id = |
| 415 new PackageId(package, "hosted", new Version.parse(version), package); | 698 new PackageId(package, "hosted", new Version.parse(version), package); |
| 416 lockFile.packages[package] = id; | 699 lockFile.packages[package] = id; |
| 700 |
| 701 // Write the lockfile to the global cache. |
| 417 var sources = new SourceRegistry(); | 702 var sources = new SourceRegistry(); |
| 418 sources.register(new HostedSource()); | 703 sources.register(new HostedSource()); |
| 419 sources.register(new PathSource()); | 704 sources.register(new PathSource()); |
| 705 |
| 420 d.dir( | 706 d.dir( |
| 421 cachePath, | 707 cachePath, |
| 422 [ | 708 [ |
| 423 d.dir( | 709 d.dir( |
| 424 "global_packages", | 710 "global_packages", |
| 425 [d.file("$package.lock", lockFile.serialize(null, sources))])]).cr
eate(); | 711 [d.file("$package.lock", lockFile.serialize(null, sources))])]).cr
eate(); |
| 426 } | 712 } |
| 713 |
| 714 /// Creates a lock file for [package] without running `pub get`. |
| 715 /// |
| 716 /// [sandbox] is a list of path dependencies to be found in the sandbox |
| 717 /// directory. [pkg] is a list of packages in the Dart repo's "pkg" directory; |
| 718 /// each package listed here and all its dependencies will be linked to the |
| 719 /// version in the Dart repo. |
| 720 /// |
| 721 /// [hosted] is a list of package names to version strings for dependencies on |
| 722 /// hosted packages. |
| 427 void createLockFile(String package, {Iterable<String> sandbox, | 723 void createLockFile(String package, {Iterable<String> sandbox, |
| 428 Iterable<String> pkg, Map<String, String> hosted}) { | 724 Iterable<String> pkg, Map<String, String> hosted}) { |
| 429 var lockFile = _createLockFile(sandbox: sandbox, pkg: pkg, hosted: hosted); | 725 var lockFile = _createLockFile(sandbox: sandbox, pkg: pkg, hosted: hosted); |
| 726 |
| 430 var sources = new SourceRegistry(); | 727 var sources = new SourceRegistry(); |
| 431 sources.register(new HostedSource()); | 728 sources.register(new HostedSource()); |
| 432 sources.register(new PathSource()); | 729 sources.register(new PathSource()); |
| 730 |
| 433 d.file( | 731 d.file( |
| 434 p.join(package, 'pubspec.lock'), | 732 p.join(package, 'pubspec.lock'), |
| 435 lockFile.serialize(null, sources)).create(); | 733 lockFile.serialize(null, sources)).create(); |
| 436 } | 734 } |
| 735 |
| 736 /// Creates a lock file for [package] without running `pub get`. |
| 737 /// |
| 738 /// [sandbox] is a list of path dependencies to be found in the sandbox |
| 739 /// directory. [pkg] is a list of packages in the Dart repo's "pkg" directory; |
| 740 /// each package listed here and all its dependencies will be linked to the |
| 741 /// version in the Dart repo. |
| 742 /// |
| 743 /// [hosted] is a list of package names to version strings for dependencies on |
| 744 /// hosted packages. |
| 437 LockFile _createLockFile({Iterable<String> sandbox, Iterable<String> pkg, | 745 LockFile _createLockFile({Iterable<String> sandbox, Iterable<String> pkg, |
| 438 Map<String, String> hosted}) { | 746 Map<String, String> hosted}) { |
| 439 var dependencies = {}; | 747 var dependencies = {}; |
| 748 |
| 440 if (sandbox != null) { | 749 if (sandbox != null) { |
| 441 for (var package in sandbox) { | 750 for (var package in sandbox) { |
| 442 dependencies[package] = '../$package'; | 751 dependencies[package] = '../$package'; |
| 443 } | 752 } |
| 444 } | 753 } |
| 754 |
| 445 if (pkg != null) { | 755 if (pkg != null) { |
| 446 _addPackage(String package) { | 756 _addPackage(String package) { |
| 447 if (dependencies.containsKey(package)) return; | 757 if (dependencies.containsKey(package)) return; |
| 758 |
| 448 var packagePath; | 759 var packagePath; |
| 449 if (package == 'barback' && _packageOverrides == null) { | 760 if (package == 'barback' && _packageOverrides == null) { |
| 450 throw new StateError( | 761 throw new StateError( |
| 451 "createLockFile() can only create a lock file " | 762 "createLockFile() can only create a lock file " |
| 452 "with a barback dependency within a withBarbackVersions() " "blo
ck."); | 763 "with a barback dependency within a withBarbackVersions() " "blo
ck."); |
| 453 } | 764 } |
| 765 |
| 454 if (_packageOverrides.containsKey(package)) { | 766 if (_packageOverrides.containsKey(package)) { |
| 455 packagePath = _packageOverrides[package]; | 767 packagePath = _packageOverrides[package]; |
| 456 } else { | 768 } else { |
| 457 packagePath = p.join(pkgPath, package); | 769 packagePath = p.join(pkgPath, package); |
| 458 } | 770 } |
| 771 |
| 459 dependencies[package] = packagePath; | 772 dependencies[package] = packagePath; |
| 460 var pubspec = loadYaml(readTextFile(p.join(packagePath, 'pubspec.yaml'))); | 773 var pubspec = loadYaml(readTextFile(p.join(packagePath, 'pubspec.yaml'))); |
| 461 var packageDeps = pubspec['dependencies']; | 774 var packageDeps = pubspec['dependencies']; |
| 462 if (packageDeps == null) return; | 775 if (packageDeps == null) return; |
| 463 packageDeps.keys.forEach(_addPackage); | 776 packageDeps.keys.forEach(_addPackage); |
| 464 } | 777 } |
| 778 |
| 465 pkg.forEach(_addPackage); | 779 pkg.forEach(_addPackage); |
| 466 } | 780 } |
| 781 |
| 467 var lockFile = new LockFile.empty(); | 782 var lockFile = new LockFile.empty(); |
| 468 dependencies.forEach((name, dependencyPath) { | 783 dependencies.forEach((name, dependencyPath) { |
| 469 var id = new PackageId(name, 'path', new Version(0, 0, 0), { | 784 var id = new PackageId(name, 'path', new Version(0, 0, 0), { |
| 470 'path': dependencyPath, | 785 'path': dependencyPath, |
| 471 'relative': p.isRelative(dependencyPath) | 786 'relative': p.isRelative(dependencyPath) |
| 472 }); | 787 }); |
| 473 lockFile.packages[name] = id; | 788 lockFile.packages[name] = id; |
| 474 }); | 789 }); |
| 790 |
| 475 if (hosted != null) { | 791 if (hosted != null) { |
| 476 hosted.forEach((name, version) { | 792 hosted.forEach((name, version) { |
| 477 var id = new PackageId(name, 'hosted', new Version.parse(version), name); | 793 var id = new PackageId(name, 'hosted', new Version.parse(version), name); |
| 478 lockFile.packages[name] = id; | 794 lockFile.packages[name] = id; |
| 479 }); | 795 }); |
| 480 } | 796 } |
| 797 |
| 481 return lockFile; | 798 return lockFile; |
| 482 } | 799 } |
| 800 |
| 801 /// Uses [client] as the mock HTTP client for this test. |
| 802 /// |
| 803 /// Note that this will only affect HTTP requests made via http.dart in the |
| 804 /// parent process. |
| 483 void useMockClient(MockClient client) { | 805 void useMockClient(MockClient client) { |
| 484 var oldInnerClient = innerHttpClient; | 806 var oldInnerClient = innerHttpClient; |
| 485 innerHttpClient = client; | 807 innerHttpClient = client; |
| 486 currentSchedule.onComplete.schedule(() { | 808 currentSchedule.onComplete.schedule(() { |
| 487 innerHttpClient = oldInnerClient; | 809 innerHttpClient = oldInnerClient; |
| 488 }, 'de-activating the mock client'); | 810 }, 'de-activating the mock client'); |
| 489 } | 811 } |
| 812 |
| 813 /// Describes a map representing a library package with the given [name], |
| 814 /// [version], and [dependencies]. |
| 490 Map packageMap(String name, String version, [Map dependencies]) { | 815 Map packageMap(String name, String version, [Map dependencies]) { |
| 491 var package = { | 816 var package = { |
| 492 "name": name, | 817 "name": name, |
| 493 "version": version, | 818 "version": version, |
| 494 "author": "Natalie Weizenbaum <nweiz@google.com>", | 819 "author": "Natalie Weizenbaum <nweiz@google.com>", |
| 495 "homepage": "http://pub.dartlang.org", | 820 "homepage": "http://pub.dartlang.org", |
| 496 "description": "A package, I guess." | 821 "description": "A package, I guess." |
| 497 }; | 822 }; |
| 823 |
| 498 if (dependencies != null) package["dependencies"] = dependencies; | 824 if (dependencies != null) package["dependencies"] = dependencies; |
| 825 |
| 499 return package; | 826 return package; |
| 500 } | 827 } |
| 828 |
| 829 /// Resolves [target] relative to the path to pub's `test/asset` directory. |
| 501 String testAssetPath(String target) { | 830 String testAssetPath(String target) { |
| 502 var libPath = libraryPath('test_pub'); | 831 var libPath = libraryPath('test_pub'); |
| 832 |
| 833 // We are running from the generated directory, but non-dart assets are only |
| 834 // in the canonical directory. |
| 835 // TODO(rnystrom): Remove this when #104 is fixed. |
| 503 libPath = libPath.replaceAll('pub_generated', 'pub'); | 836 libPath = libPath.replaceAll('pub_generated', 'pub'); |
| 837 |
| 504 return p.join(p.dirname(libPath), 'asset', target); | 838 return p.join(p.dirname(libPath), 'asset', target); |
| 505 } | 839 } |
| 840 |
| 841 /// Returns a Map in the format used by the pub.dartlang.org API to represent a |
| 842 /// package version. |
| 843 /// |
| 844 /// [pubspec] is the parsed pubspec of the package version. If [full] is true, |
| 845 /// this returns the complete map, including metadata that's only included when |
| 846 /// requesting the package version directly. |
| 506 Map packageVersionApiMap(Map pubspec, {bool full: false}) { | 847 Map packageVersionApiMap(Map pubspec, {bool full: false}) { |
| 507 var name = pubspec['name']; | 848 var name = pubspec['name']; |
| 508 var version = pubspec['version']; | 849 var version = pubspec['version']; |
| 509 var map = { | 850 var map = { |
| 510 'pubspec': pubspec, | 851 'pubspec': pubspec, |
| 511 'version': version, | 852 'version': version, |
| 512 'url': '/api/packages/$name/versions/$version', | 853 'url': '/api/packages/$name/versions/$version', |
| 513 'archive_url': '/packages/$name/versions/$version.tar.gz', | 854 'archive_url': '/packages/$name/versions/$version.tar.gz', |
| 514 'new_dartdoc_url': '/api/packages/$name/versions/$version' '/new_dartdoc', | 855 'new_dartdoc_url': '/api/packages/$name/versions/$version' '/new_dartdoc', |
| 515 'package_url': '/api/packages/$name' | 856 'package_url': '/api/packages/$name' |
| 516 }; | 857 }; |
| 858 |
| 517 if (full) { | 859 if (full) { |
| 518 map.addAll({ | 860 map.addAll({ |
| 519 'downloads': 0, | 861 'downloads': 0, |
| 520 'created': '2012-09-25T18:38:28.685260', | 862 'created': '2012-09-25T18:38:28.685260', |
| 521 'libraries': ['$name.dart'], | 863 'libraries': ['$name.dart'], |
| 522 'uploader': ['nweiz@google.com'] | 864 'uploader': ['nweiz@google.com'] |
| 523 }); | 865 }); |
| 524 } | 866 } |
| 867 |
| 525 return map; | 868 return map; |
| 526 } | 869 } |
| 870 |
| 871 /// Compares the [actual] output from running pub with [expected]. |
| 872 /// |
| 873 /// If [expected] is a [String], ignores leading and trailing whitespace |
| 874 /// differences and tries to report the offending difference in a nice way. |
| 875 /// |
| 876 /// If it's a [RegExp] or [Matcher], just reports whether the output matches. |
| 527 void _validateOutput(List<String> failures, String pipe, expected, | 877 void _validateOutput(List<String> failures, String pipe, expected, |
| 528 String actual) { | 878 String actual) { |
| 529 if (expected == null) return; | 879 if (expected == null) return; |
| 880 |
| 530 if (expected is String) { | 881 if (expected is String) { |
| 531 _validateOutputString(failures, pipe, expected, actual); | 882 _validateOutputString(failures, pipe, expected, actual); |
| 532 } else { | 883 } else { |
| 533 if (expected is RegExp) expected = matches(expected); | 884 if (expected is RegExp) expected = matches(expected); |
| 534 expect(actual, expected); | 885 expect(actual, expected); |
| 535 } | 886 } |
| 536 } | 887 } |
| 888 |
| 537 void _validateOutputString(List<String> failures, String pipe, String expected, | 889 void _validateOutputString(List<String> failures, String pipe, String expected, |
| 538 String actual) { | 890 String actual) { |
| 539 var actualLines = actual.split("\n"); | 891 var actualLines = actual.split("\n"); |
| 540 var expectedLines = expected.split("\n"); | 892 var expectedLines = expected.split("\n"); |
| 893 |
| 894 // Strip off the last line. This lets us have expected multiline strings |
| 895 // where the closing ''' is on its own line. It also fixes '' expected output |
| 896 // to expect zero lines of output, not a single empty line. |
| 541 if (expectedLines.last.trim() == '') { | 897 if (expectedLines.last.trim() == '') { |
| 542 expectedLines.removeLast(); | 898 expectedLines.removeLast(); |
| 543 } | 899 } |
| 900 |
| 544 var results = []; | 901 var results = []; |
| 545 var failed = false; | 902 var failed = false; |
| 903 |
| 904 // Compare them line by line to see which ones match. |
| 546 var length = max(expectedLines.length, actualLines.length); | 905 var length = max(expectedLines.length, actualLines.length); |
| 547 for (var i = 0; i < length; i++) { | 906 for (var i = 0; i < length; i++) { |
| 548 if (i >= actualLines.length) { | 907 if (i >= actualLines.length) { |
| 908 // Missing output. |
| 549 failed = true; | 909 failed = true; |
| 550 results.add('? ${expectedLines[i]}'); | 910 results.add('? ${expectedLines[i]}'); |
| 551 } else if (i >= expectedLines.length) { | 911 } else if (i >= expectedLines.length) { |
| 912 // Unexpected extra output. |
| 552 failed = true; | 913 failed = true; |
| 553 results.add('X ${actualLines[i]}'); | 914 results.add('X ${actualLines[i]}'); |
| 554 } else { | 915 } else { |
| 555 var expectedLine = expectedLines[i].trim(); | 916 var expectedLine = expectedLines[i].trim(); |
| 556 var actualLine = actualLines[i].trim(); | 917 var actualLine = actualLines[i].trim(); |
| 918 |
| 557 if (expectedLine != actualLine) { | 919 if (expectedLine != actualLine) { |
| 920 // Mismatched lines. |
| 558 failed = true; | 921 failed = true; |
| 559 results.add('X ${actualLines[i]}'); | 922 results.add('X ${actualLines[i]}'); |
| 560 } else { | 923 } else { |
| 924 // Output is OK, but include it in case other lines are wrong. |
| 561 results.add('| ${actualLines[i]}'); | 925 results.add('| ${actualLines[i]}'); |
| 562 } | 926 } |
| 563 } | 927 } |
| 564 } | 928 } |
| 929 |
| 930 // If any lines mismatched, show the expected and actual. |
| 565 if (failed) { | 931 if (failed) { |
| 566 failures.add('Expected $pipe:'); | 932 failures.add('Expected $pipe:'); |
| 567 failures.addAll(expectedLines.map((line) => '| $line')); | 933 failures.addAll(expectedLines.map((line) => '| $line')); |
| 568 failures.add('Got:'); | 934 failures.add('Got:'); |
| 569 failures.addAll(results); | 935 failures.addAll(results); |
| 570 } | 936 } |
| 571 } | 937 } |
| 938 |
| 939 /// Validates that [actualText] is a string of JSON that matches [expected], |
| 940 /// which may be a literal JSON object, or any other [Matcher]. |
| 572 void _validateOutputJson(List<String> failures, String pipe, expected, | 941 void _validateOutputJson(List<String> failures, String pipe, expected, |
| 573 String actualText) { | 942 String actualText) { |
| 574 var actual; | 943 var actual; |
| 575 try { | 944 try { |
| 576 actual = JSON.decode(actualText); | 945 actual = JSON.decode(actualText); |
| 577 } on FormatException catch (error) { | 946 } on FormatException catch (error) { |
| 578 failures.add('Expected $pipe JSON:'); | 947 failures.add('Expected $pipe JSON:'); |
| 579 failures.add(expected); | 948 failures.add(expected); |
| 580 failures.add('Got invalid JSON:'); | 949 failures.add('Got invalid JSON:'); |
| 581 failures.add(actualText); | 950 failures.add(actualText); |
| 582 } | 951 } |
| 952 |
| 953 // Match against the expectation. |
| 583 expect(actual, expected); | 954 expect(actual, expected); |
| 584 } | 955 } |
| 956 |
| 957 /// A function that creates a [Validator] subclass. |
| 585 typedef Validator ValidatorCreator(Entrypoint entrypoint); | 958 typedef Validator ValidatorCreator(Entrypoint entrypoint); |
| 959 |
| 960 /// Schedules a single [Validator] to run on the [appPath]. |
| 961 /// |
| 962 /// Returns a scheduled Future that contains the errors and warnings produced |
| 963 /// by that validator. |
| 586 Future<Pair<List<String>, List<String>>> | 964 Future<Pair<List<String>, List<String>>> |
| 587 schedulePackageValidation(ValidatorCreator fn) { | 965 schedulePackageValidation(ValidatorCreator fn) { |
| 588 return schedule(() { | 966 return schedule(() { |
| 589 var cache = new SystemCache.withSources(p.join(sandboxDir, cachePath)); | 967 var cache = new SystemCache.withSources(p.join(sandboxDir, cachePath)); |
| 968 |
| 590 return new Future.sync(() { | 969 return new Future.sync(() { |
| 591 var validator = fn(new Entrypoint(p.join(sandboxDir, appPath), cache)); | 970 var validator = fn(new Entrypoint(p.join(sandboxDir, appPath), cache)); |
| 592 return validator.validate().then((_) { | 971 return validator.validate().then((_) { |
| 593 return new Pair(validator.errors, validator.warnings); | 972 return new Pair(validator.errors, validator.warnings); |
| 594 }); | 973 }); |
| 595 }); | 974 }); |
| 596 }, "validating package"); | 975 }, "validating package"); |
| 597 } | 976 } |
| 977 |
| 978 /// A matcher that matches a Pair. |
| 598 Matcher pairOf(Matcher firstMatcher, Matcher lastMatcher) => | 979 Matcher pairOf(Matcher firstMatcher, Matcher lastMatcher) => |
| 599 new _PairMatcher(firstMatcher, lastMatcher); | 980 new _PairMatcher(firstMatcher, lastMatcher); |
| 981 |
| 600 class _PairMatcher extends Matcher { | 982 class _PairMatcher extends Matcher { |
| 601 final Matcher _firstMatcher; | 983 final Matcher _firstMatcher; |
| 602 final Matcher _lastMatcher; | 984 final Matcher _lastMatcher; |
| 985 |
| 603 _PairMatcher(this._firstMatcher, this._lastMatcher); | 986 _PairMatcher(this._firstMatcher, this._lastMatcher); |
| 987 |
| 604 bool matches(item, Map matchState) { | 988 bool matches(item, Map matchState) { |
| 605 if (item is! Pair) return false; | 989 if (item is! Pair) return false; |
| 606 return _firstMatcher.matches(item.first, matchState) && | 990 return _firstMatcher.matches(item.first, matchState) && |
| 607 _lastMatcher.matches(item.last, matchState); | 991 _lastMatcher.matches(item.last, matchState); |
| 608 } | 992 } |
| 993 |
| 609 Description describe(Description description) { | 994 Description describe(Description description) { |
| 610 return description.addAll("(", ", ", ")", [_firstMatcher, _lastMatcher]); | 995 return description.addAll("(", ", ", ")", [_firstMatcher, _lastMatcher]); |
| 611 } | 996 } |
| 612 } | 997 } |
| 998 |
| 999 /// A [StreamMatcher] that matches multiple lines of output. |
| 613 StreamMatcher emitsLines(String output) => inOrder(output.split("\n")); | 1000 StreamMatcher emitsLines(String output) => inOrder(output.split("\n")); |
| OLD | NEW |