| 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] = p.join( | |
| 146 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(baseDir.load(path)) | |
| 200 .then((stream) => new shelf.Response.ok(stream)) | |
| 201 .catchError((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('get', new RegExp( | |
| 254 r'Got dependencies!|Changed \d+ dependenc(y|ies)!')); | |
| 255 static final upgrade = new RunCommand('upgrade', new RegExp( | |
| 256 r'(No dependencies changed\.|Changed \d+ dependenc(y|ies)!)$')); | |
| 257 static final downgrade = new RunCommand('downgrade', new RegExp( | |
| 258 r'(No dependencies changed\.|Changed \d+ dependenc(y|ies)!)$')); | |
| 259 | |
| 260 final String name; | |
| 261 final RegExp success; | |
| 262 RunCommand(this.name, this.success); | |
| 263 } | |
| 264 | |
| 265 /// Runs the tests defined within [callback] using both pub get and pub upgrade. | |
| 266 /// | |
| 267 /// Many tests validate behavior that is the same between pub get and | |
| 268 /// upgrade have the same behavior. Instead of duplicating those tests, this | |
| 269 /// takes a callback that defines get/upgrade agnostic tests and runs them | |
| 270 /// with both commands. | |
| 271 void forBothPubGetAndUpgrade(void callback(RunCommand command)) { | |
| 272 group(RunCommand.get.name, () => callback(RunCommand.get)); | |
| 273 group(RunCommand.upgrade.name, () => callback(RunCommand.upgrade)); | |
| 274 } | |
| 275 | |
| 276 /// Schedules an invocation of pub [command] and validates that it completes | |
| 277 /// in an expected way. | |
| 278 /// | |
| 279 /// By default, this validates that the command completes successfully and | |
| 280 /// understands the normal output of a successful pub command. If [warning] is | |
| 281 /// given, it expects the command to complete successfully *and* print | |
| 282 /// [warning] to stderr. If [error] is given, it expects the command to *only* | |
| 283 /// print [error] to stderr. [output], [error], and [warning] may be strings, | |
| 284 /// [RegExp]s, or [Matcher]s. | |
| 285 /// | |
| 286 /// If [exitCode] is given, expects the command to exit with that code. | |
| 287 // TODO(rnystrom): Clean up other tests to call this when possible. | |
| 288 void pubCommand(RunCommand command, | |
| 289 {Iterable<String> args, output, error, warning, int exitCode}) { | |
| 290 if (error != null && warning != null) { | |
| 291 throw new ArgumentError("Cannot pass both 'error' and 'warning'."); | |
| 292 } | |
| 293 | |
| 294 var allArgs = [command.name]; | |
| 295 if (args != null) allArgs.addAll(args); | |
| 296 | |
| 297 if (output == null) output = command.success; | |
| 298 | |
| 299 if (error != null && exitCode == null) exitCode = 1; | |
| 300 | |
| 301 // No success output on an error. | |
| 302 if (error != null) output = null; | |
| 303 if (warning != null) error = warning; | |
| 304 | |
| 305 schedulePub(args: allArgs, output: output, error: error, exitCode: exitCode); | |
| 306 } | |
| 307 | |
| 308 void pubGet({Iterable<String> args, output, error, warning, int exitCode}) { | |
| 309 pubCommand(RunCommand.get, args: args, output: output, error: error, | |
| 310 warning: warning, exitCode: exitCode); | |
| 311 } | |
| 312 | |
| 313 void pubUpgrade({Iterable<String> args, output, error, warning, int exitCode}) { | |
| 314 pubCommand(RunCommand.upgrade, args: args, output: output, error: error, | |
| 315 warning: warning, exitCode: exitCode); | |
| 316 } | |
| 317 | |
| 318 void pubDowngrade({Iterable<String> args, output, error, warning, | |
| 319 int exitCode}) { | |
| 320 pubCommand(RunCommand.downgrade, args: args, output: output, error: error, | |
| 321 warning: warning, exitCode: exitCode); | |
| 322 } | |
| 323 | |
| 324 /// Schedules starting the "pub [global] run" process and validates the | |
| 325 /// expected startup output. | |
| 326 /// | |
| 327 /// If [global] is `true`, this invokes "pub global run", otherwise it does | |
| 328 /// "pub run". | |
| 329 /// | |
| 330 /// Returns the `pub run` process. | |
| 331 ScheduledProcess pubRun({bool global: false, Iterable<String> args}) { | |
| 332 var pubArgs = global ? ["global", "run"] : ["run"]; | |
| 333 pubArgs.addAll(args); | |
| 334 var pub = startPub(args: pubArgs); | |
| 335 | |
| 336 // Loading sources and transformers isn't normally printed, but the pub test | |
| 337 // infrastructure runs pub in verbose mode, which enables this. | |
| 338 pub.stdout.expect(consumeWhile(startsWith("Loading"))); | |
| 339 | |
| 340 return pub; | |
| 341 } | |
| 342 | |
| 343 /// Defines an integration test. | |
| 344 /// | |
| 345 /// The [body] should schedule a series of operations which will be run | |
| 346 /// asynchronously. | |
| 347 void integration(String description, void body()) => | |
| 348 _integration(description, body, test); | |
| 349 | |
| 350 /// Like [integration], but causes only this test to run. | |
| 351 void solo_integration(String description, void body()) => | |
| 352 _integration(description, body, solo_test); | |
| 353 | |
| 354 void _integration(String description, void body(), [Function testFn]) { | |
| 355 testFn(description, () { | |
| 356 // TODO(nweiz): remove this when issue 15362 is fixed. | |
| 357 currentSchedule.timeout *= 2; | |
| 358 | |
| 359 // The windows bots are very slow, so we increase the default timeout. | |
| 360 if (Platform.operatingSystem == "windows") { | |
| 361 currentSchedule.timeout *= 2; | |
| 362 } | |
| 363 | |
| 364 _sandboxDir = createSystemTempDir(); | |
| 365 d.defaultRoot = sandboxDir; | |
| 366 currentSchedule.onComplete.schedule(() => deleteEntry(_sandboxDir), | |
| 367 'deleting the sandbox directory'); | |
| 368 | |
| 369 // Schedule the test. | |
| 370 body(); | |
| 371 }); | |
| 372 } | |
| 373 | |
| 374 /// Get the path to the root "pub/test" directory containing the pub | |
| 375 /// tests. | |
| 376 String get testDirectory => | |
| 377 p.absolute(p.dirname(libraryPath('test_pub'))); | |
| 378 | |
| 379 /// Schedules renaming (moving) the directory at [from] to [to], both of which | |
| 380 /// are assumed to be relative to [sandboxDir]. | |
| 381 void scheduleRename(String from, String to) { | |
| 382 schedule( | |
| 383 () => renameDir( | |
| 384 p.join(sandboxDir, from), | |
| 385 p.join(sandboxDir, to)), | |
| 386 'renaming $from to $to'); | |
| 387 } | |
| 388 | |
| 389 /// Schedules creating a symlink at path [symlink] that points to [target], | |
| 390 /// both of which are assumed to be relative to [sandboxDir]. | |
| 391 void scheduleSymlink(String target, String symlink) { | |
| 392 schedule( | |
| 393 () => createSymlink( | |
| 394 p.join(sandboxDir, target), | |
| 395 p.join(sandboxDir, symlink)), | |
| 396 'symlinking $target to $symlink'); | |
| 397 } | |
| 398 | |
| 399 /// Schedules a call to the Pub command-line utility. | |
| 400 /// | |
| 401 /// Runs Pub with [args] and validates that its results match [output] (or | |
| 402 /// [outputJson]), [error], and [exitCode]. | |
| 403 /// | |
| 404 /// [output] and [error] can be [String]s, [RegExp]s, or [Matcher]s. | |
| 405 /// | |
| 406 /// If [outputJson] is given, validates that pub outputs stringified JSON | |
| 407 /// matching that object, which can be a literal JSON object or any other | |
| 408 /// [Matcher]. | |
| 409 /// | |
| 410 /// If [environment] is given, any keys in it will override the environment | |
| 411 /// variables passed to the spawned process. | |
| 412 void schedulePub({List args, output, error, outputJson, | |
| 413 int exitCode: exit_codes.SUCCESS, Map<String, String> environment}) { | |
| 414 // Cannot pass both output and outputJson. | |
| 415 assert(output == null || outputJson == null); | |
| 416 | |
| 417 var pub = startPub(args: args, environment: environment); | |
| 418 pub.shouldExit(exitCode); | |
| 419 | |
| 420 var failures = []; | |
| 421 var stderr; | |
| 422 | |
| 423 expect(Future.wait([ | |
| 424 pub.stdoutStream().toList(), | |
| 425 pub.stderrStream().toList() | |
| 426 ]).then((results) { | |
| 427 var stdout = results[0].join("\n"); | |
| 428 stderr = results[1].join("\n"); | |
| 429 | |
| 430 if (outputJson == null) { | |
| 431 _validateOutput(failures, 'stdout', output, stdout); | |
| 432 return null; | |
| 433 } | |
| 434 | |
| 435 // Allow the expected JSON to contain futures. | |
| 436 return awaitObject(outputJson).then((resolved) { | |
| 437 _validateOutputJson(failures, 'stdout', resolved, stdout); | |
| 438 }); | |
| 439 }).then((_) { | |
| 440 _validateOutput(failures, 'stderr', error, stderr); | |
| 441 | |
| 442 if (!failures.isEmpty) throw new TestFailure(failures.join('\n')); | |
| 443 }), completes); | |
| 444 } | |
| 445 | |
| 446 /// Like [startPub], but runs `pub lish` in particular with [server] used both | |
| 447 /// as the OAuth2 server (with "/token" as the token endpoint) and as the | |
| 448 /// package server. | |
| 449 /// | |
| 450 /// Any futures in [args] will be resolved before the process is started. | |
| 451 ScheduledProcess startPublish(ScheduledServer server, {List args}) { | |
| 452 var tokenEndpoint = server.url.then((url) => | |
| 453 url.resolve('/token').toString()); | |
| 454 if (args == null) args = []; | |
| 455 args = flatten(['lish', '--server', tokenEndpoint, args]); | |
| 456 return startPub(args: args, tokenEndpoint: tokenEndpoint); | |
| 457 } | |
| 458 | |
| 459 /// Handles the beginning confirmation process for uploading a packages. | |
| 460 /// | |
| 461 /// Ensures that the right output is shown and then enters "y" to confirm the | |
| 462 /// upload. | |
| 463 void confirmPublish(ScheduledProcess pub) { | |
| 464 // TODO(rnystrom): This is overly specific and inflexible regarding different | |
| 465 // test packages. Should validate this a little more loosely. | |
| 466 pub.stdout.expect(startsWith('Publishing test_pkg 1.0.0 to ')); | |
| 467 pub.stdout.expect(consumeThrough( | |
| 468 "Looks great! Are you ready to upload your package (y/n)?")); | |
| 469 pub.writeLine("y"); | |
| 470 } | |
| 471 | |
| 472 /// Gets the absolute path to [relPath], which is a relative path in the test | |
| 473 /// sandbox. | |
| 474 String _pathInSandbox(String relPath) { | |
| 475 return p.join(p.absolute(sandboxDir), relPath); | |
| 476 } | |
| 477 | |
| 478 /// Gets the environment variables used to run pub in a test context. | |
| 479 Future<Map> getPubTestEnvironment([String tokenEndpoint]) async { | |
| 480 var environment = {}; | |
| 481 environment['_PUB_TESTING'] = 'true'; | |
| 482 environment['PUB_CACHE'] = _pathInSandbox(cachePath); | |
| 483 | |
| 484 // Ensure a known SDK version is set for the tests that rely on that. | |
| 485 environment['_PUB_TEST_SDK_VERSION'] = "0.1.2+3"; | |
| 486 | |
| 487 if (tokenEndpoint != null) { | |
| 488 environment['_PUB_TEST_TOKEN_ENDPOINT'] = tokenEndpoint.toString(); | |
| 489 } | |
| 490 | |
| 491 if (_hasServer) { | |
| 492 return port.then((p) { | |
| 493 environment['PUB_HOSTED_URL'] = "http://localhost:$p"; | |
| 494 return environment; | |
| 495 }); | |
| 496 } | |
| 497 | |
| 498 return environment; | |
| 499 } | |
| 500 | |
| 501 /// Starts a Pub process and returns a [ScheduledProcess] that supports | |
| 502 /// interaction with that process. | |
| 503 /// | |
| 504 /// Any futures in [args] will be resolved before the process is started. | |
| 505 /// | |
| 506 /// If [environment] is given, any keys in it will override the environment | |
| 507 /// variables passed to the spawned process. | |
| 508 ScheduledProcess startPub({List args, Future<String> tokenEndpoint, | |
| 509 Map<String, String> environment}) { | |
| 510 ensureDir(_pathInSandbox(appPath)); | |
| 511 | |
| 512 // Find a Dart executable we can use to spawn. Use the same one that was | |
| 513 // used to run this script itself. | |
| 514 var dartBin = Platform.executable; | |
| 515 | |
| 516 // If the executable looks like a path, get its full path. That way we | |
| 517 // can still find it when we spawn it with a different working directory. | |
| 518 if (dartBin.contains(Platform.pathSeparator)) { | |
| 519 dartBin = p.absolute(dartBin); | |
| 520 } | |
| 521 | |
| 522 // Always run pub from a snapshot. Since we require the SDK to be built, the | |
| 523 // snapshot should be there. Note that this *does* mean that the snapshot has | |
| 524 // to be manually updated when changing code before running the tests. | |
| 525 // Otherwise, you will test against stale data. | |
| 526 // | |
| 527 // Using the snapshot makes running the tests much faster, which is why we | |
| 528 // make this trade-off. | |
| 529 var pubPath = p.join(p.dirname(dartBin), 'snapshots/pub.dart.snapshot'); | |
| 530 var dartArgs = [pubPath, '--verbose']; | |
| 531 dartArgs.addAll(args); | |
| 532 | |
| 533 if (tokenEndpoint == null) tokenEndpoint = new Future.value(); | |
| 534 var environmentFuture = tokenEndpoint | |
| 535 .then((tokenEndpoint) => getPubTestEnvironment(tokenEndpoint)) | |
| 536 .then((pubEnvironment) { | |
| 537 if (environment != null) pubEnvironment.addAll(environment); | |
| 538 return pubEnvironment; | |
| 539 }); | |
| 540 | |
| 541 return new PubProcess.start(dartBin, dartArgs, environment: environmentFuture, | |
| 542 workingDirectory: _pathInSandbox(appPath), | |
| 543 description: args.isEmpty ? 'pub' : 'pub ${args.first}'); | |
| 544 } | |
| 545 | |
| 546 /// A subclass of [ScheduledProcess] that parses pub's verbose logging output | |
| 547 /// and makes [stdout] and [stderr] work as though pub weren't running in | |
| 548 /// verbose mode. | |
| 549 class PubProcess extends ScheduledProcess { | |
| 550 Stream<Pair<log.Level, String>> _log; | |
| 551 Stream<String> _stdout; | |
| 552 Stream<String> _stderr; | |
| 553 | |
| 554 PubProcess.start(executable, arguments, | |
| 555 {workingDirectory, environment, String description, | |
| 556 Encoding encoding: UTF8}) | |
| 557 : super.start(executable, arguments, | |
| 558 workingDirectory: workingDirectory, | |
| 559 environment: environment, | |
| 560 description: description, | |
| 561 encoding: encoding); | |
| 562 | |
| 563 Stream<Pair<log.Level, String>> _logStream() { | |
| 564 if (_log == null) { | |
| 565 _log = mergeStreams( | |
| 566 _outputToLog(super.stdoutStream(), log.Level.MESSAGE), | |
| 567 _outputToLog(super.stderrStream(), log.Level.ERROR)); | |
| 568 } | |
| 569 | |
| 570 var pair = tee(_log); | |
| 571 _log = pair.first; | |
| 572 return pair.last; | |
| 573 } | |
| 574 | |
| 575 final _logLineRegExp = new RegExp(r"^([A-Z ]{4})[:|] (.*)$"); | |
| 576 final _logLevels = [ | |
| 577 log.Level.ERROR, log.Level.WARNING, log.Level.MESSAGE, log.Level.IO, | |
| 578 log.Level.SOLVER, log.Level.FINE | |
| 579 ].fold(<String, log.Level>{}, (levels, level) { | |
| 580 levels[level.name] = level; | |
| 581 return levels; | |
| 582 }); | |
| 583 | |
| 584 Stream<Pair<log.Level, String>> _outputToLog(Stream<String> stream, | |
| 585 log.Level defaultLevel) { | |
| 586 var lastLevel; | |
| 587 return stream.map((line) { | |
| 588 var match = _logLineRegExp.firstMatch(line); | |
| 589 if (match == null) return new Pair<log.Level, String>(defaultLevel, line); | |
| 590 | |
| 591 var level = _logLevels[match[1]]; | |
| 592 if (level == null) level = lastLevel; | |
| 593 lastLevel = level; | |
| 594 return new Pair<log.Level, String>(level, match[2]); | |
| 595 }); | |
| 596 } | |
| 597 | |
| 598 Stream<String> stdoutStream() { | |
| 599 if (_stdout == null) { | |
| 600 _stdout = _logStream().expand((entry) { | |
| 601 if (entry.first != log.Level.MESSAGE) return []; | |
| 602 return [entry.last]; | |
| 603 }); | |
| 604 } | |
| 605 | |
| 606 var pair = tee(_stdout); | |
| 607 _stdout = pair.first; | |
| 608 return pair.last; | |
| 609 } | |
| 610 | |
| 611 Stream<String> stderrStream() { | |
| 612 if (_stderr == null) { | |
| 613 _stderr = _logStream().expand((entry) { | |
| 614 if (entry.first != log.Level.ERROR && | |
| 615 entry.first != log.Level.WARNING) { | |
| 616 return []; | |
| 617 } | |
| 618 return [entry.last]; | |
| 619 }); | |
| 620 } | |
| 621 | |
| 622 var pair = tee(_stderr); | |
| 623 _stderr = pair.first; | |
| 624 return pair.last; | |
| 625 } | |
| 626 } | |
| 627 | |
| 628 /// The path to the `packages` directory from which pub loads its dependencies. | |
| 629 String get _packageRoot => p.absolute(Platform.packageRoot); | |
| 630 | |
| 631 /// Fails the current test if Git is not installed. | |
| 632 /// | |
| 633 /// We require machines running these tests to have git installed. This | |
| 634 /// validation gives an easier-to-understand error when that requirement isn't | |
| 635 /// met than just failing in the middle of a test when pub invokes git. | |
| 636 /// | |
| 637 /// This also increases the [Schedule] timeout to 30 seconds on Windows, | |
| 638 /// where Git runs really slowly. | |
| 639 void ensureGit() { | |
| 640 if (Platform.operatingSystem == "windows") { | |
| 641 currentSchedule.timeout = new Duration(seconds: 30); | |
| 642 } | |
| 643 | |
| 644 if (!gitlib.isInstalled) { | |
| 645 throw new Exception("Git must be installed to run this test."); | |
| 646 } | |
| 647 } | |
| 648 | |
| 649 /// Schedules activating a global package [package] without running | |
| 650 /// "pub global activate". | |
| 651 /// | |
| 652 /// This is useful because global packages must be hosted, but the test hosted | |
| 653 /// server doesn't serve barback. The other parameters here follow | |
| 654 /// [createLockFile]. | |
| 655 void makeGlobalPackage(String package, String version, | |
| 656 Iterable<d.Descriptor> contents, {Iterable<String> pkg, | |
| 657 Map<String, String> hosted}) { | |
| 658 // Start the server so we know what port to use in the cache directory name. | |
| 659 serveNoPackages(); | |
| 660 | |
| 661 // Create the package in the hosted cache. | |
| 662 d.hostedCache([ | |
| 663 d.dir("$package-$version", contents) | |
| 664 ]).create(); | |
| 665 | |
| 666 var lockFile = _createLockFile(pkg: pkg, hosted: hosted); | |
| 667 | |
| 668 // Add the root package to the lockfile. | |
| 669 var id = new PackageId(package, "hosted", new Version.parse(version), | |
| 670 package); | |
| 671 lockFile.packages[package] = id; | |
| 672 | |
| 673 // Write the lockfile to the global cache. | |
| 674 var sources = new SourceRegistry(); | |
| 675 sources.register(new HostedSource()); | |
| 676 sources.register(new PathSource()); | |
| 677 | |
| 678 d.dir(cachePath, [ | |
| 679 d.dir("global_packages", [ | |
| 680 d.file("$package.lock", lockFile.serialize(null, sources)) | |
| 681 ]) | |
| 682 ]).create(); | |
| 683 } | |
| 684 | |
| 685 /// Creates a lock file for [package] without running `pub get`. | |
| 686 /// | |
| 687 /// [sandbox] is a list of path dependencies to be found in the sandbox | |
| 688 /// directory. [pkg] is a list of packages in the Dart repo's "pkg" directory; | |
| 689 /// each package listed here and all its dependencies will be linked to the | |
| 690 /// version in the Dart repo. | |
| 691 /// | |
| 692 /// [hosted] is a list of package names to version strings for dependencies on | |
| 693 /// hosted packages. | |
| 694 void createLockFile(String package, {Iterable<String> sandbox, | |
| 695 Iterable<String> pkg, Map<String, String> hosted}) { | |
| 696 var lockFile = _createLockFile(sandbox: sandbox, pkg: pkg, hosted: hosted); | |
| 697 | |
| 698 var sources = new SourceRegistry(); | |
| 699 sources.register(new HostedSource()); | |
| 700 sources.register(new PathSource()); | |
| 701 | |
| 702 d.file(p.join(package, 'pubspec.lock'), | |
| 703 lockFile.serialize(null, sources)).create(); | |
| 704 } | |
| 705 | |
| 706 /// Creates a lock file for [package] without running `pub get`. | |
| 707 /// | |
| 708 /// [sandbox] is a list of path dependencies to be found in the sandbox | |
| 709 /// directory. [pkg] is a list of packages in the Dart repo's "pkg" directory; | |
| 710 /// each package listed here and all its dependencies will be linked to the | |
| 711 /// version in the Dart repo. | |
| 712 /// | |
| 713 /// [hosted] is a list of package names to version strings for dependencies on | |
| 714 /// hosted packages. | |
| 715 LockFile _createLockFile({Iterable<String> sandbox, | |
| 716 Iterable<String> pkg, Map<String, String> hosted}) { | |
| 717 var dependencies = {}; | |
| 718 | |
| 719 if (sandbox != null) { | |
| 720 for (var package in sandbox) { | |
| 721 dependencies[package] = '../$package'; | |
| 722 } | |
| 723 } | |
| 724 | |
| 725 if (pkg != null) { | |
| 726 _addPackage(String package) { | |
| 727 if (dependencies.containsKey(package)) return; | |
| 728 | |
| 729 var path; | |
| 730 if (package == 'barback' && _packageOverrides == null) { | |
| 731 throw new StateError("createLockFile() can only create a lock file " | |
| 732 "with a barback dependency within a withBarbackVersions() " | |
| 733 "block."); | |
| 734 } | |
| 735 | |
| 736 if (_packageOverrides.containsKey(package)) { | |
| 737 path = _packageOverrides[package]; | |
| 738 } else { | |
| 739 path = packagePath(package); | |
| 740 } | |
| 741 | |
| 742 dependencies[package] = path; | |
| 743 var pubspec = loadYaml( | |
| 744 readTextFile(p.join(path, 'pubspec.yaml'))); | |
| 745 var packageDeps = pubspec['dependencies']; | |
| 746 if (packageDeps == null) return; | |
| 747 packageDeps.keys.forEach(_addPackage); | |
| 748 } | |
| 749 | |
| 750 pkg.forEach(_addPackage); | |
| 751 } | |
| 752 | |
| 753 var lockFile = new LockFile.empty(); | |
| 754 dependencies.forEach((name, dependencyPath) { | |
| 755 var id = new PackageId(name, 'path', new Version(0, 0, 0), { | |
| 756 'path': dependencyPath, | |
| 757 'relative': p.isRelative(dependencyPath) | |
| 758 }); | |
| 759 lockFile.packages[name] = id; | |
| 760 }); | |
| 761 | |
| 762 if (hosted != null) { | |
| 763 hosted.forEach((name, version) { | |
| 764 var id = new PackageId(name, 'hosted', new Version.parse(version), name); | |
| 765 lockFile.packages[name] = id; | |
| 766 }); | |
| 767 } | |
| 768 | |
| 769 return lockFile; | |
| 770 } | |
| 771 | |
| 772 /// Returns the path to [package] within the repo. | |
| 773 String packagePath(String package) => | |
| 774 dirExists(p.join(repoRoot, 'pkg', package)) ? | |
| 775 p.join(repoRoot, 'pkg', package) : | |
| 776 p.join(repoRoot, 'third_party', 'pkg', package); | |
| 777 | |
| 778 /// Uses [client] as the mock HTTP client for this test. | |
| 779 /// | |
| 780 /// Note that this will only affect HTTP requests made via http.dart in the | |
| 781 /// parent process. | |
| 782 void useMockClient(MockClient client) { | |
| 783 var oldInnerClient = innerHttpClient; | |
| 784 innerHttpClient = client; | |
| 785 currentSchedule.onComplete.schedule(() { | |
| 786 innerHttpClient = oldInnerClient; | |
| 787 }, 'de-activating the mock client'); | |
| 788 } | |
| 789 | |
| 790 /// Describes a map representing a library package with the given [name], | |
| 791 /// [version], and [dependencies]. | |
| 792 Map packageMap(String name, String version, [Map dependencies]) { | |
| 793 var package = { | |
| 794 "name": name, | |
| 795 "version": version, | |
| 796 "author": "Natalie Weizenbaum <nweiz@google.com>", | |
| 797 "homepage": "http://pub.dartlang.org", | |
| 798 "description": "A package, I guess." | |
| 799 }; | |
| 800 | |
| 801 if (dependencies != null) package["dependencies"] = dependencies; | |
| 802 | |
| 803 return package; | |
| 804 } | |
| 805 | |
| 806 /// Resolves [target] relative to the path to pub's `test/asset` directory. | |
| 807 String testAssetPath(String target) => | |
| 808 p.join(p.dirname(libraryPath('test_pub')), 'asset', target); | |
| 809 | |
| 810 /// Returns a Map in the format used by the pub.dartlang.org API to represent a | |
| 811 /// package version. | |
| 812 /// | |
| 813 /// [pubspec] is the parsed pubspec of the package version. If [full] is true, | |
| 814 /// this returns the complete map, including metadata that's only included when | |
| 815 /// requesting the package version directly. | |
| 816 Map packageVersionApiMap(Map pubspec, {bool full: false}) { | |
| 817 var name = pubspec['name']; | |
| 818 var version = pubspec['version']; | |
| 819 var map = { | |
| 820 'pubspec': pubspec, | |
| 821 'version': version, | |
| 822 'url': '/api/packages/$name/versions/$version', | |
| 823 'archive_url': '/packages/$name/versions/$version.tar.gz', | |
| 824 'new_dartdoc_url': '/api/packages/$name/versions/$version' | |
| 825 '/new_dartdoc', | |
| 826 'package_url': '/api/packages/$name' | |
| 827 }; | |
| 828 | |
| 829 if (full) { | |
| 830 map.addAll({ | |
| 831 'downloads': 0, | |
| 832 'created': '2012-09-25T18:38:28.685260', | |
| 833 'libraries': ['$name.dart'], | |
| 834 'uploader': ['nweiz@google.com'] | |
| 835 }); | |
| 836 } | |
| 837 | |
| 838 return map; | |
| 839 } | |
| 840 | |
| 841 /// Returns the name of the shell script for a binstub named [name]. | |
| 842 /// | |
| 843 /// Adds a ".bat" extension on Windows. | |
| 844 String binStubName(String name) => Platform.isWindows ? '$name.bat' : name; | |
| 845 | |
| 846 /// Compares the [actual] output from running pub with [expected]. | |
| 847 /// | |
| 848 /// If [expected] is a [String], ignores leading and trailing whitespace | |
| 849 /// differences and tries to report the offending difference in a nice way. | |
| 850 /// | |
| 851 /// If it's a [RegExp] or [Matcher], just reports whether the output matches. | |
| 852 void _validateOutput(List<String> failures, String pipe, expected, | |
| 853 String actual) { | |
| 854 if (expected == null) return; | |
| 855 | |
| 856 if (expected is String) { | |
| 857 _validateOutputString(failures, pipe, expected, actual); | |
| 858 } else { | |
| 859 if (expected is RegExp) expected = matches(expected); | |
| 860 expect(actual, expected); | |
| 861 } | |
| 862 } | |
| 863 | |
| 864 void _validateOutputString(List<String> failures, String pipe, | |
| 865 String expected, String actual) { | |
| 866 var actualLines = actual.split("\n"); | |
| 867 var expectedLines = expected.split("\n"); | |
| 868 | |
| 869 // Strip off the last line. This lets us have expected multiline strings | |
| 870 // where the closing ''' is on its own line. It also fixes '' expected output | |
| 871 // to expect zero lines of output, not a single empty line. | |
| 872 if (expectedLines.last.trim() == '') { | |
| 873 expectedLines.removeLast(); | |
| 874 } | |
| 875 | |
| 876 var results = []; | |
| 877 var failed = false; | |
| 878 | |
| 879 // Compare them line by line to see which ones match. | |
| 880 var length = max(expectedLines.length, actualLines.length); | |
| 881 for (var i = 0; i < length; i++) { | |
| 882 if (i >= actualLines.length) { | |
| 883 // Missing output. | |
| 884 failed = true; | |
| 885 results.add('? ${expectedLines[i]}'); | |
| 886 } else if (i >= expectedLines.length) { | |
| 887 // Unexpected extra output. | |
| 888 failed = true; | |
| 889 results.add('X ${actualLines[i]}'); | |
| 890 } else { | |
| 891 var expectedLine = expectedLines[i].trim(); | |
| 892 var actualLine = actualLines[i].trim(); | |
| 893 | |
| 894 if (expectedLine != actualLine) { | |
| 895 // Mismatched lines. | |
| 896 failed = true; | |
| 897 results.add('X ${actualLines[i]}'); | |
| 898 } else { | |
| 899 // Output is OK, but include it in case other lines are wrong. | |
| 900 results.add('| ${actualLines[i]}'); | |
| 901 } | |
| 902 } | |
| 903 } | |
| 904 | |
| 905 // If any lines mismatched, show the expected and actual. | |
| 906 if (failed) { | |
| 907 failures.add('Expected $pipe:'); | |
| 908 failures.addAll(expectedLines.map((line) => '| $line')); | |
| 909 failures.add('Got:'); | |
| 910 failures.addAll(results); | |
| 911 } | |
| 912 } | |
| 913 | |
| 914 /// Validates that [actualText] is a string of JSON that matches [expected], | |
| 915 /// which may be a literal JSON object, or any other [Matcher]. | |
| 916 void _validateOutputJson(List<String> failures, String pipe, | |
| 917 expected, String actualText) { | |
| 918 var actual; | |
| 919 try { | |
| 920 actual = JSON.decode(actualText); | |
| 921 } on FormatException catch(error) { | |
| 922 failures.add('Expected $pipe JSON:'); | |
| 923 failures.add(expected); | |
| 924 failures.add('Got invalid JSON:'); | |
| 925 failures.add(actualText); | |
| 926 } | |
| 927 | |
| 928 // Match against the expectation. | |
| 929 expect(actual, expected); | |
| 930 } | |
| 931 | |
| 932 /// A function that creates a [Validator] subclass. | |
| 933 typedef Validator ValidatorCreator(Entrypoint entrypoint); | |
| 934 | |
| 935 /// Schedules a single [Validator] to run on the [appPath]. | |
| 936 /// | |
| 937 /// Returns a scheduled Future that contains the errors and warnings produced | |
| 938 /// by that validator. | |
| 939 Future<Pair<List<String>, List<String>>> schedulePackageValidation( | |
| 940 ValidatorCreator fn) { | |
| 941 return schedule(() { | |
| 942 var cache = new SystemCache.withSources( | |
| 943 rootDir: p.join(sandboxDir, cachePath)); | |
| 944 | |
| 945 return new Future.sync(() { | |
| 946 var validator = fn(new Entrypoint(p.join(sandboxDir, appPath), cache)); | |
| 947 return validator.validate().then((_) { | |
| 948 return new Pair(validator.errors, validator.warnings); | |
| 949 }); | |
| 950 }); | |
| 951 }, "validating package"); | |
| 952 } | |
| 953 | |
| 954 /// A matcher that matches a Pair. | |
| 955 Matcher pairOf(Matcher firstMatcher, Matcher lastMatcher) => | |
| 956 new _PairMatcher(firstMatcher, lastMatcher); | |
| 957 | |
| 958 class _PairMatcher extends Matcher { | |
| 959 final Matcher _firstMatcher; | |
| 960 final Matcher _lastMatcher; | |
| 961 | |
| 962 _PairMatcher(this._firstMatcher, this._lastMatcher); | |
| 963 | |
| 964 bool matches(item, Map matchState) { | |
| 965 if (item is! Pair) return false; | |
| 966 return _firstMatcher.matches(item.first, matchState) && | |
| 967 _lastMatcher.matches(item.last, matchState); | |
| 968 } | |
| 969 | |
| 970 Description describe(Description description) { | |
| 971 return description.addAll("(", ", ", ")", [_firstMatcher, _lastMatcher]); | |
| 972 } | |
| 973 } | |
| 974 | |
| 975 /// A [StreamMatcher] that matches multiple lines of output. | |
| 976 StreamMatcher emitsLines(String output) => inOrder(output.split("\n")); | |
| OLD | NEW |