| 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. Unlike typical unit tests, most pub | |
| 6 /// tests are integration tests that stage some stuff on the file system, run | |
| 7 /// pub, and then validate the results. This library provides an API to build | |
| 8 /// tests like that. | |
| 9 library test_pub; | |
| 10 | |
| 11 import 'dart:async'; | |
| 12 import 'dart:collection' show Queue; | |
| 13 import 'dart:io' hide sleep; | |
| 14 import 'dart:json' as json; | |
| 15 import 'dart:math'; | |
| 16 import 'dart:uri'; | |
| 17 import 'dart:utf'; | |
| 18 | |
| 19 import 'package:http/testing.dart'; | |
| 20 import 'package:oauth2/oauth2.dart' as oauth2; | |
| 21 import 'package:pathos/path.dart' as path; | |
| 22 import 'package:scheduled_test/scheduled_process.dart'; | |
| 23 import 'package:scheduled_test/scheduled_server.dart'; | |
| 24 import 'package:scheduled_test/scheduled_test.dart'; | |
| 25 import 'package:yaml/yaml.dart'; | |
| 26 | |
| 27 import '../../pub/entrypoint.dart'; | |
| 28 // TODO(rnystrom): Using "gitlib" as the prefix here is ugly, but "git" collides | |
| 29 // with the git descriptor method. Maybe we should try to clean up the top level | |
| 30 // scope a bit? | |
| 31 import '../../pub/git.dart' as gitlib; | |
| 32 import '../../pub/git_source.dart'; | |
| 33 import '../../pub/hosted_source.dart'; | |
| 34 import '../../pub/http.dart'; | |
| 35 import '../../pub/io.dart'; | |
| 36 import '../../pub/path_source.dart'; | |
| 37 import '../../pub/safe_http_server.dart'; | |
| 38 import '../../pub/system_cache.dart'; | |
| 39 import '../../pub/utils.dart'; | |
| 40 import '../../pub/validator.dart'; | |
| 41 import 'command_line_config.dart'; | |
| 42 import 'descriptor.dart' as d; | |
| 43 | |
| 44 /// This should be called at the top of a test file to set up an appropriate | |
| 45 /// test configuration for the machine running the tests. | |
| 46 initConfig() { | |
| 47 // If we aren't running on the bots, use the human-friendly config. | |
| 48 if (!runningOnBuildbot) { | |
| 49 unittestConfiguration = new CommandLineConfiguration(); | |
| 50 } | |
| 51 } | |
| 52 | |
| 53 /// Returns whether we're running on a Dart build bot. | |
| 54 bool get runningOnBuildbot => | |
| 55 Platform.environment.containsKey('BUILDBOT_BUILDERNAME'); | |
| 56 | |
| 57 /// The current [HttpServer] created using [serve]. | |
| 58 var _server; | |
| 59 | |
| 60 /// The list of paths that have been requested from the server since the last | |
| 61 /// call to [getRequestedPaths]. | |
| 62 final _requestedPaths = <String>[]; | |
| 63 | |
| 64 /// The cached value for [_portCompleter]. | |
| 65 Completer<int> _portCompleterCache; | |
| 66 | |
| 67 /// The completer for [port]. | |
| 68 Completer<int> get _portCompleter { | |
| 69 if (_portCompleterCache != null) return _portCompleterCache; | |
| 70 _portCompleterCache = new Completer<int>(); | |
| 71 currentSchedule.onComplete.schedule(() { | |
| 72 _portCompleterCache = null; | |
| 73 }, 'clearing the port completer'); | |
| 74 return _portCompleterCache; | |
| 75 } | |
| 76 | |
| 77 /// A future that will complete to the port used for the current server. | |
| 78 Future<int> get port => _portCompleter.future; | |
| 79 | |
| 80 /// Gets the list of paths that have been requested from the server since the | |
| 81 /// last time this was called (or since the server was first spun up). | |
| 82 Future<List<String>> getRequestedPaths() { | |
| 83 return schedule(() { | |
| 84 var paths = _requestedPaths.toList(); | |
| 85 _requestedPaths.clear(); | |
| 86 return paths; | |
| 87 }); | |
| 88 } | |
| 89 | |
| 90 /// Creates an HTTP server to serve [contents] as static files. This server will | |
| 91 /// exist only for the duration of the pub run. | |
| 92 /// | |
| 93 /// Subsequent calls to [serve] will replace the previous server. | |
| 94 void serve([List<d.Descriptor> contents]) { | |
| 95 var baseDir = d.dir("serve-dir", contents); | |
| 96 | |
| 97 schedule(() { | |
| 98 return _closeServer().then((_) { | |
| 99 return SafeHttpServer.bind("127.0.0.1", 0).then((server) { | |
| 100 _server = server; | |
| 101 server.listen((request) { | |
| 102 var response = request.response; | |
| 103 try { | |
| 104 var path = request.uri.path.replaceFirst("/", ""); | |
| 105 | |
| 106 if (_requestedPaths == null) _requestedPaths = <String>[]; | |
| 107 _requestedPaths.add(path); | |
| 108 | |
| 109 response.persistentConnection = false; | |
| 110 var stream = baseDir.load(path); | |
| 111 | |
| 112 new ByteStream(stream).toBytes().then((data) { | |
| 113 response.statusCode = 200; | |
| 114 response.contentLength = data.length; | |
| 115 response.add(data); | |
| 116 response.close(); | |
| 117 }).catchError((e) { | |
| 118 response.statusCode = 404; | |
| 119 response.contentLength = 0; | |
| 120 response.close(); | |
| 121 }); | |
| 122 } catch (e) { | |
| 123 currentSchedule.signalError(e); | |
| 124 response.statusCode = 500; | |
| 125 response.close(); | |
| 126 return; | |
| 127 } | |
| 128 }); | |
| 129 _portCompleter.complete(_server.port); | |
| 130 currentSchedule.onComplete.schedule(_closeServer); | |
| 131 return null; | |
| 132 }); | |
| 133 }); | |
| 134 }, 'starting a server serving:\n${baseDir.describe()}'); | |
| 135 } | |
| 136 | |
| 137 /// Closes [_server]. Returns a [Future] that will complete after the [_server] | |
| 138 /// is closed. | |
| 139 Future _closeServer() { | |
| 140 if (_server == null) return new Future.value(); | |
| 141 _server.close(); | |
| 142 _server = null; | |
| 143 _portCompleterCache = null; | |
| 144 // TODO(nweiz): Remove this once issue 4155 is fixed. Pumping the event loop | |
| 145 // *seems* to be enough to ensure that the server is actually closed, but I'm | |
| 146 // putting this at 10ms to be safe. | |
| 147 return sleep(10); | |
| 148 } | |
| 149 | |
| 150 /// The [d.DirectoryDescriptor] describing the server layout of packages that | |
| 151 /// are being served via [servePackages]. This is `null` if [servePackages] has | |
| 152 /// not yet been called for this test. | |
| 153 d.DirectoryDescriptor _servedPackageDir; | |
| 154 | |
| 155 /// A map from package names to version numbers to YAML-serialized pubspecs for | |
| 156 /// those packages. This represents the packages currently being served by | |
| 157 /// [servePackages], and is `null` if [servePackages] has not yet been called | |
| 158 /// for this test. | |
| 159 Map<String, Map<String, String>> _servedPackages; | |
| 160 | |
| 161 /// Creates an HTTP server that replicates the structure of pub.dartlang.org. | |
| 162 /// [pubspecs] is a list of unserialized pubspecs representing the packages to | |
| 163 /// serve. | |
| 164 /// | |
| 165 /// Subsequent calls to [servePackages] will add to the set of packages that | |
| 166 /// are being served. Previous packages will continue to be served. | |
| 167 void servePackages(List<Map> pubspecs) { | |
| 168 if (_servedPackages == null || _servedPackageDir == null) { | |
| 169 _servedPackages = <String, Map<String, String>>{}; | |
| 170 _servedPackageDir = d.dir('packages', []); | |
| 171 serve([_servedPackageDir]); | |
| 172 | |
| 173 currentSchedule.onComplete.schedule(() { | |
| 174 _servedPackages = null; | |
| 175 _servedPackageDir = null; | |
| 176 }, 'cleaning up served packages'); | |
| 177 } | |
| 178 | |
| 179 schedule(() { | |
| 180 return awaitObject(pubspecs).then((resolvedPubspecs) { | |
| 181 for (var spec in resolvedPubspecs) { | |
| 182 var name = spec['name']; | |
| 183 var version = spec['version']; | |
| 184 var versions = _servedPackages.putIfAbsent( | |
| 185 name, () => <String, String>{}); | |
| 186 versions[version] = yaml(spec); | |
| 187 } | |
| 188 | |
| 189 _servedPackageDir.contents.clear(); | |
| 190 for (var name in _servedPackages.keys) { | |
| 191 var versions = _servedPackages[name].keys.toList(); | |
| 192 _servedPackageDir.contents.addAll([ | |
| 193 d.file('$name.json', json.stringify({'versions': versions})), | |
| 194 d.dir(name, [ | |
| 195 d.dir('versions', flatten(versions.map((version) { | |
| 196 return [ | |
| 197 d.file('$version.yaml', _servedPackages[name][version]), | |
| 198 d.tar('$version.tar.gz', [ | |
| 199 d.file('pubspec.yaml', _servedPackages[name][version]), | |
| 200 d.libDir(name, '$name $version') | |
| 201 ]) | |
| 202 ]; | |
| 203 }))) | |
| 204 ]) | |
| 205 ]); | |
| 206 } | |
| 207 }); | |
| 208 }, 'initializing the package server'); | |
| 209 } | |
| 210 | |
| 211 /// Converts [value] into a YAML string. | |
| 212 String yaml(value) => json.stringify(value); | |
| 213 | |
| 214 /// The full path to the created sandbox directory for an integration test. | |
| 215 String get sandboxDir => _sandboxDir; | |
| 216 String _sandboxDir; | |
| 217 | |
| 218 /// The path of the package cache directory used for tests. Relative to the | |
| 219 /// sandbox directory. | |
| 220 final String cachePath = "cache"; | |
| 221 | |
| 222 /// The path of the mock SDK directory used for tests. Relative to the sandbox | |
| 223 /// directory. | |
| 224 final String sdkPath = "sdk"; | |
| 225 | |
| 226 /// The path of the mock app directory used for tests. Relative to the sandbox | |
| 227 /// directory. | |
| 228 final String appPath = "myapp"; | |
| 229 | |
| 230 /// The path of the packages directory in the mock app used for tests. Relative | |
| 231 /// to the sandbox directory. | |
| 232 final String packagesPath = "$appPath/packages"; | |
| 233 | |
| 234 /// Set to true when the current batch of scheduled events should be aborted. | |
| 235 bool _abortScheduled = false; | |
| 236 | |
| 237 /// Defines an integration test. The [body] should schedule a series of | |
| 238 /// operations which will be run asynchronously. | |
| 239 void integration(String description, void body()) => | |
| 240 _integration(description, body, test); | |
| 241 | |
| 242 /// Like [integration], but causes only this test to run. | |
| 243 void solo_integration(String description, void body()) => | |
| 244 _integration(description, body, solo_test); | |
| 245 | |
| 246 void _integration(String description, void body(), [Function testFn]) { | |
| 247 testFn(description, () { | |
| 248 // The windows bots are very slow, so we increase the default timeout. | |
| 249 if (Platform.operatingSystem == "windows") { | |
| 250 currentSchedule.timeout = new Duration(seconds: 10); | |
| 251 } | |
| 252 | |
| 253 // By default, don't capture stack traces since they slow the tests way | |
| 254 // down. To debug failing tests, comment this out. | |
| 255 currentSchedule.captureStackTraces = | |
| 256 new Options().arguments.contains('--trace'); | |
| 257 | |
| 258 // Ensure the SDK version is always available. | |
| 259 d.dir(sdkPath, [ | |
| 260 d.file('version', '0.1.2.3') | |
| 261 ]).create(); | |
| 262 | |
| 263 _sandboxDir = createTempDir(); | |
| 264 d.defaultRoot = sandboxDir; | |
| 265 currentSchedule.onComplete.schedule(() => deleteEntry(_sandboxDir), | |
| 266 'deleting the sandbox directory'); | |
| 267 | |
| 268 // Schedule the test. | |
| 269 body(); | |
| 270 }); | |
| 271 } | |
| 272 | |
| 273 /// Get the path to the root "util/test/pub" directory containing the pub | |
| 274 /// tests. | |
| 275 String get testDirectory { | |
| 276 var dir = new Options().script; | |
| 277 while (path.basename(dir) != 'pub') dir = path.dirname(dir); | |
| 278 | |
| 279 return path.absolute(dir); | |
| 280 } | |
| 281 | |
| 282 /// Schedules renaming (moving) the directory at [from] to [to], both of which | |
| 283 /// are assumed to be relative to [sandboxDir]. | |
| 284 void scheduleRename(String from, String to) { | |
| 285 schedule( | |
| 286 () => renameDir( | |
| 287 path.join(sandboxDir, from), | |
| 288 path.join(sandboxDir, to)), | |
| 289 'renaming $from to $to'); | |
| 290 } | |
| 291 | |
| 292 /// Schedules creating a symlink at path [symlink] that points to [target], | |
| 293 /// both of which are assumed to be relative to [sandboxDir]. | |
| 294 void scheduleSymlink(String target, String symlink) { | |
| 295 schedule( | |
| 296 () => createSymlink( | |
| 297 path.join(sandboxDir, target), | |
| 298 path.join(sandboxDir, symlink)), | |
| 299 'symlinking $target to $symlink'); | |
| 300 } | |
| 301 | |
| 302 /// Schedules a call to the Pub command-line utility. Runs Pub with [args] and | |
| 303 /// validates that its results match [output], [error], and [exitCode]. | |
| 304 void schedulePub({List args, Pattern output, Pattern error, | |
| 305 Future<Uri> tokenEndpoint, int exitCode: 0}) { | |
| 306 var pub = startPub(args: args, tokenEndpoint: tokenEndpoint); | |
| 307 pub.shouldExit(exitCode); | |
| 308 | |
| 309 expect(Future.wait([ | |
| 310 pub.remainingStdout(), | |
| 311 pub.remainingStderr() | |
| 312 ]).then((results) { | |
| 313 var failures = []; | |
| 314 _validateOutput(failures, 'stdout', output, results[0].split('\n')); | |
| 315 _validateOutput(failures, 'stderr', error, results[1].split('\n')); | |
| 316 if (!failures.isEmpty) throw new TestFailure(failures.join('\n')); | |
| 317 }), completes); | |
| 318 } | |
| 319 | |
| 320 /// Like [startPub], but runs `pub lish` in particular with [server] used both | |
| 321 /// as the OAuth2 server (with "/token" as the token endpoint) and as the | |
| 322 /// package server. | |
| 323 /// | |
| 324 /// Any futures in [args] will be resolved before the process is started. | |
| 325 ScheduledProcess startPublish(ScheduledServer server, {List args}) { | |
| 326 var tokenEndpoint = server.url.then((url) => | |
| 327 url.resolve('/token').toString()); | |
| 328 if (args == null) args = []; | |
| 329 args = flatten(['lish', '--server', tokenEndpoint, args]); | |
| 330 return startPub(args: args, tokenEndpoint: tokenEndpoint); | |
| 331 } | |
| 332 | |
| 333 /// Handles the beginning confirmation process for uploading a packages. | |
| 334 /// Ensures that the right output is shown and then enters "y" to confirm the | |
| 335 /// upload. | |
| 336 void confirmPublish(ScheduledProcess pub) { | |
| 337 // TODO(rnystrom): This is overly specific and inflexible regarding different | |
| 338 // test packages. Should validate this a little more loosely. | |
| 339 expect(pub.nextLine(), completion(equals('Publishing "test_pkg" 1.0.0:'))); | |
| 340 expect(pub.nextLine(), completion(equals("|-- LICENSE"))); | |
| 341 expect(pub.nextLine(), completion(equals("|-- lib"))); | |
| 342 expect(pub.nextLine(), completion(equals("| '-- test_pkg.dart"))); | |
| 343 expect(pub.nextLine(), completion(equals("'-- pubspec.yaml"))); | |
| 344 expect(pub.nextLine(), completion(equals(""))); | |
| 345 | |
| 346 pub.writeLine("y"); | |
| 347 } | |
| 348 | |
| 349 /// Starts a Pub process and returns a [ScheduledProcess] that supports | |
| 350 /// interaction with that process. | |
| 351 /// | |
| 352 /// Any futures in [args] will be resolved before the process is started. | |
| 353 ScheduledProcess startPub({List args, Future<Uri> tokenEndpoint}) { | |
| 354 String pathInSandbox(String relPath) { | |
| 355 return path.join(path.absolute(sandboxDir), relPath); | |
| 356 } | |
| 357 | |
| 358 ensureDir(pathInSandbox(appPath)); | |
| 359 | |
| 360 // Find a Dart executable we can use to spawn. Use the same one that was | |
| 361 // used to run this script itself. | |
| 362 var dartBin = new Options().executable; | |
| 363 | |
| 364 // If the executable looks like a path, get its full path. That way we | |
| 365 // can still find it when we spawn it with a different working directory. | |
| 366 if (dartBin.contains(Platform.pathSeparator)) { | |
| 367 dartBin = new File(dartBin).fullPathSync(); | |
| 368 } | |
| 369 | |
| 370 // Find the main pub entrypoint. | |
| 371 var pubPath = path.join(testDirectory, '..', '..', 'pub', 'pub.dart'); | |
| 372 | |
| 373 var dartArgs = ['--package-root=$_packageRoot/', '--checked', pubPath, | |
| 374 '--trace']; | |
| 375 dartArgs.addAll(args); | |
| 376 | |
| 377 if (tokenEndpoint == null) tokenEndpoint = new Future.value(); | |
| 378 var optionsFuture = tokenEndpoint.then((tokenEndpoint) { | |
| 379 var options = new ProcessOptions(); | |
| 380 options.workingDirectory = pathInSandbox(appPath); | |
| 381 // TODO(nweiz): remove this when issue 9294 is fixed. | |
| 382 options.environment = new Map.from(Platform.environment); | |
| 383 options.environment['PUB_CACHE'] = pathInSandbox(cachePath); | |
| 384 options.environment['DART_SDK'] = pathInSandbox(sdkPath); | |
| 385 if (tokenEndpoint != null) { | |
| 386 options.environment['_PUB_TEST_TOKEN_ENDPOINT'] = | |
| 387 tokenEndpoint.toString(); | |
| 388 } | |
| 389 return options; | |
| 390 }); | |
| 391 | |
| 392 return new ScheduledProcess.start(dartBin, dartArgs, options: optionsFuture, | |
| 393 description: args.isEmpty ? 'pub' : 'pub ${args.first}'); | |
| 394 } | |
| 395 | |
| 396 /// Whether pub is running from within the Dart SDK, as opposed to from the Dart | |
| 397 /// source repository. | |
| 398 bool get _runningFromSdk => path.dirname(relativeToPub('..')) == 'util'; | |
| 399 | |
| 400 // TODO(nweiz): use the built-in mechanism for accessing this once it exists | |
| 401 // (issue 9119). | |
| 402 /// The path to the `packages` directory from which pub loads its dependencies. | |
| 403 String get _packageRoot { | |
| 404 if (_runningFromSdk) { | |
| 405 return path.absolute(relativeToPub(path.join('..', '..', 'packages'))); | |
| 406 } else { | |
| 407 return path.absolute(path.join( | |
| 408 path.dirname(new Options().executable), '..', '..', 'packages')); | |
| 409 } | |
| 410 } | |
| 411 | |
| 412 /// Skips the current test if Git is not installed. This validates that the | |
| 413 /// current test is running on a buildbot in which case we expect git to be | |
| 414 /// installed. If we are not running on the buildbot, we will instead see if | |
| 415 /// git is installed and skip the test if not. This way, users don't need to | |
| 416 /// have git installed to run the tests locally (unless they actually care | |
| 417 /// about the pub git tests). | |
| 418 /// | |
| 419 /// This will also increase the [Schedule] timeout to 30 seconds on Windows, | |
| 420 /// where Git runs really slowly. | |
| 421 void ensureGit() { | |
| 422 if (Platform.operatingSystem == "windows") { | |
| 423 currentSchedule.timeout = new Duration(seconds: 30); | |
| 424 } | |
| 425 | |
| 426 schedule(() { | |
| 427 return gitlib.isInstalled.then((installed) { | |
| 428 if (installed) return; | |
| 429 if (runningOnBuildbot) return; | |
| 430 currentSchedule.abort(); | |
| 431 }); | |
| 432 }, 'ensuring that Git is installed'); | |
| 433 } | |
| 434 | |
| 435 /// Use [client] as the mock HTTP client for this test. | |
| 436 /// | |
| 437 /// Note that this will only affect HTTP requests made via http.dart in the | |
| 438 /// parent process. | |
| 439 void useMockClient(MockClient client) { | |
| 440 var oldInnerClient = httpClient.inner; | |
| 441 httpClient.inner = client; | |
| 442 currentSchedule.onComplete.schedule(() { | |
| 443 httpClient.inner = oldInnerClient; | |
| 444 }, 'de-activating the mock client'); | |
| 445 } | |
| 446 | |
| 447 /// Describes a map representing a library package with the given [name], | |
| 448 /// [version], and [dependencies]. | |
| 449 Map packageMap(String name, String version, [List dependencies]) { | |
| 450 var package = { | |
| 451 "name": name, | |
| 452 "version": version, | |
| 453 "author": "Nathan Weizenbaum <nweiz@google.com>", | |
| 454 "homepage": "http://pub.dartlang.org", | |
| 455 "description": "A package, I guess." | |
| 456 }; | |
| 457 if (dependencies != null) { | |
| 458 package["dependencies"] = dependencyListToMap(dependencies); | |
| 459 } | |
| 460 return package; | |
| 461 } | |
| 462 | |
| 463 /// Describes a map representing a dependency on a package in the package | |
| 464 /// repository. | |
| 465 Map dependencyMap(String name, [String versionConstraint]) { | |
| 466 var url = port.then((p) => "http://localhost:$p"); | |
| 467 var dependency = {"hosted": {"name": name, "url": url}}; | |
| 468 if (versionConstraint != null) dependency["version"] = versionConstraint; | |
| 469 return dependency; | |
| 470 } | |
| 471 | |
| 472 /// Converts a list of dependencies as passed to [package] into a hash as used | |
| 473 /// in a pubspec. | |
| 474 Future<Map> dependencyListToMap(List<Map> dependencies) { | |
| 475 return awaitObject(dependencies).then((resolvedDependencies) { | |
| 476 var result = <String, Map>{}; | |
| 477 for (var dependency in resolvedDependencies) { | |
| 478 var keys = dependency.keys.where((key) => key != "version"); | |
| 479 var sourceName = only(keys); | |
| 480 var source; | |
| 481 switch (sourceName) { | |
| 482 case "git": | |
| 483 source = new GitSource(); | |
| 484 break; | |
| 485 case "hosted": | |
| 486 source = new HostedSource(); | |
| 487 break; | |
| 488 case "path": | |
| 489 source = new PathSource(); | |
| 490 break; | |
| 491 default: | |
| 492 throw new Exception('Unknown source "$sourceName"'); | |
| 493 } | |
| 494 | |
| 495 result[_packageName(sourceName, dependency[sourceName])] = dependency; | |
| 496 } | |
| 497 return result; | |
| 498 }); | |
| 499 } | |
| 500 | |
| 501 /// Return the name for the package described by [description] and from | |
| 502 /// [sourceName]. | |
| 503 String _packageName(String sourceName, description) { | |
| 504 switch (sourceName) { | |
| 505 case "git": | |
| 506 var url = description is String ? description : description['url']; | |
| 507 // TODO(rnystrom): Using path.basename on a URL is hacky. If we add URL | |
| 508 // support to pkg/pathos, should use an explicit builder for that. | |
| 509 return path.basename(url.replaceFirst(new RegExp(r"(\.git)?/?$"), "")); | |
| 510 case "hosted": | |
| 511 if (description is String) return description; | |
| 512 return description['name']; | |
| 513 case "path": | |
| 514 return path.basename(description); | |
| 515 case "sdk": | |
| 516 return description; | |
| 517 default: | |
| 518 return description; | |
| 519 } | |
| 520 } | |
| 521 | |
| 522 /// Compares the [actual] output from running pub with [expected]. For [String] | |
| 523 /// patterns, ignores leading and trailing whitespace differences and tries to | |
| 524 /// report the offending difference in a nice way. For other [Pattern]s, just | |
| 525 /// reports whether the output contained the pattern. | |
| 526 void _validateOutput(List<String> failures, String pipe, Pattern expected, | |
| 527 List<String> actual) { | |
| 528 if (expected == null) return; | |
| 529 | |
| 530 if (expected is RegExp) { | |
| 531 _validateOutputRegex(failures, pipe, expected, actual); | |
| 532 } else { | |
| 533 _validateOutputString(failures, pipe, expected, actual); | |
| 534 } | |
| 535 } | |
| 536 | |
| 537 void _validateOutputRegex(List<String> failures, String pipe, | |
| 538 RegExp expected, List<String> actual) { | |
| 539 var actualText = actual.join('\n'); | |
| 540 if (actualText.contains(expected)) return; | |
| 541 | |
| 542 if (actual.length == 0) { | |
| 543 failures.add('Expected $pipe to match "${expected.pattern}" but got none.'); | |
| 544 } else { | |
| 545 failures.add('Expected $pipe to match "${expected.pattern}" but got:'); | |
| 546 failures.addAll(actual.map((line) => '| $line')); | |
| 547 } | |
| 548 } | |
| 549 | |
| 550 void _validateOutputString(List<String> failures, String pipe, | |
| 551 String expectedText, List<String> actual) { | |
| 552 final expected = expectedText.split('\n'); | |
| 553 | |
| 554 // Strip off the last line. This lets us have expected multiline strings | |
| 555 // where the closing ''' is on its own line. It also fixes '' expected output | |
| 556 // to expect zero lines of output, not a single empty line. | |
| 557 if (expected.last.trim() == '') { | |
| 558 expected.removeLast(); | |
| 559 } | |
| 560 | |
| 561 var results = []; | |
| 562 var failed = false; | |
| 563 | |
| 564 // Compare them line by line to see which ones match. | |
| 565 var length = max(expected.length, actual.length); | |
| 566 for (var i = 0; i < length; i++) { | |
| 567 if (i >= actual.length) { | |
| 568 // Missing output. | |
| 569 failed = true; | |
| 570 results.add('? ${expected[i]}'); | |
| 571 } else if (i >= expected.length) { | |
| 572 // Unexpected extra output. | |
| 573 failed = true; | |
| 574 results.add('X ${actual[i]}'); | |
| 575 } else { | |
| 576 var expectedLine = expected[i].trim(); | |
| 577 var actualLine = actual[i].trim(); | |
| 578 | |
| 579 if (expectedLine != actualLine) { | |
| 580 // Mismatched lines. | |
| 581 failed = true; | |
| 582 results.add('X ${actual[i]}'); | |
| 583 } else { | |
| 584 // Output is OK, but include it in case other lines are wrong. | |
| 585 results.add('| ${actual[i]}'); | |
| 586 } | |
| 587 } | |
| 588 } | |
| 589 | |
| 590 // If any lines mismatched, show the expected and actual. | |
| 591 if (failed) { | |
| 592 failures.add('Expected $pipe:'); | |
| 593 failures.addAll(expected.map((line) => '| $line')); | |
| 594 failures.add('Got:'); | |
| 595 failures.addAll(results); | |
| 596 } | |
| 597 } | |
| 598 | |
| 599 /// A function that creates a [Validator] subclass. | |
| 600 typedef Validator ValidatorCreator(Entrypoint entrypoint); | |
| 601 | |
| 602 /// Schedules a single [Validator] to run on the [appPath]. Returns a scheduled | |
| 603 /// Future that contains the errors and warnings produced by that validator. | |
| 604 Future<Pair<List<String>, List<String>>> schedulePackageValidation( | |
| 605 ValidatorCreator fn) { | |
| 606 return schedule(() { | |
| 607 var cache = new SystemCache.withSources(path.join(sandboxDir, cachePath)); | |
| 608 | |
| 609 return new Future.sync(() { | |
| 610 var validator = fn(new Entrypoint(path.join(sandboxDir, appPath), cache)); | |
| 611 return validator.validate().then((_) { | |
| 612 return new Pair(validator.errors, validator.warnings); | |
| 613 }); | |
| 614 }); | |
| 615 }, "validating package"); | |
| 616 } | |
| 617 | |
| 618 /// A matcher that matches a Pair. | |
| 619 Matcher pairOf(Matcher firstMatcher, Matcher lastMatcher) => | |
| 620 new _PairMatcher(firstMatcher, lastMatcher); | |
| 621 | |
| 622 class _PairMatcher extends BaseMatcher { | |
| 623 final Matcher _firstMatcher; | |
| 624 final Matcher _lastMatcher; | |
| 625 | |
| 626 _PairMatcher(this._firstMatcher, this._lastMatcher); | |
| 627 | |
| 628 bool matches(item, MatchState matchState) { | |
| 629 if (item is! Pair) return false; | |
| 630 return _firstMatcher.matches(item.first, matchState) && | |
| 631 _lastMatcher.matches(item.last, matchState); | |
| 632 } | |
| 633 | |
| 634 Description describe(Description description) { | |
| 635 description.addAll("(", ", ", ")", [_firstMatcher, _lastMatcher]); | |
| 636 } | |
| 637 } | |
| OLD | NEW |