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