| OLD | NEW |
| 1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file | 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 | 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. | 3 // BSD-style license that can be found in the LICENSE file. |
| 4 | 4 |
| 5 /// Test infrastructure for testing pub. | 5 /// Test infrastructure for testing pub. |
| 6 /// | 6 /// |
| 7 /// Unlike typical unit tests, most pub tests are integration tests that stage | 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 | 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. | 9 /// library provides an API to build tests like that. |
| 10 import 'dart:async'; | 10 import 'dart:async'; |
| 11 import 'dart:convert'; | 11 import 'dart:convert'; |
| 12 import 'dart:io'; | 12 import 'dart:io'; |
| 13 import 'dart:math'; | 13 import 'dart:math'; |
| 14 | 14 |
| 15 import 'package:async/async.dart'; |
| 15 import 'package:http/testing.dart'; | 16 import 'package:http/testing.dart'; |
| 16 import 'package:path/path.dart' as p; | 17 import 'package:path/path.dart' as p; |
| 17 import 'package:pub/src/entrypoint.dart'; | 18 import 'package:pub/src/entrypoint.dart'; |
| 18 import 'package:pub/src/exceptions.dart'; | 19 import 'package:pub/src/exceptions.dart'; |
| 19 import 'package:pub/src/exit_codes.dart' as exit_codes; | 20 import 'package:pub/src/exit_codes.dart' as exit_codes; |
| 20 // TODO(rnystrom): Using "gitlib" as the prefix here is ugly, but "git" collides | 21 // TODO(rnystrom): Using "gitlib" as the prefix here is ugly, but "git" collides |
| 21 // with the git descriptor method. Maybe we should try to clean up the top level | 22 // with the git descriptor method. Maybe we should try to clean up the top level |
| 22 // scope a bit? | 23 // scope a bit? |
| 23 import 'package:pub/src/git.dart' as gitlib; | 24 import 'package:pub/src/git.dart' as gitlib; |
| 24 import 'package:pub/src/http.dart'; | 25 import 'package:pub/src/http.dart'; |
| (...skipping 128 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 153 warning: warning, exitCode: exitCode, environment: environment); | 154 warning: warning, exitCode: exitCode, environment: environment); |
| 154 } | 155 } |
| 155 | 156 |
| 156 /// Schedules starting the "pub [global] run" process and validates the | 157 /// Schedules starting the "pub [global] run" process and validates the |
| 157 /// expected startup output. | 158 /// expected startup output. |
| 158 /// | 159 /// |
| 159 /// If [global] is `true`, this invokes "pub global run", otherwise it does | 160 /// If [global] is `true`, this invokes "pub global run", otherwise it does |
| 160 /// "pub run". | 161 /// "pub run". |
| 161 /// | 162 /// |
| 162 /// Returns the `pub run` process. | 163 /// Returns the `pub run` process. |
| 163 ScheduledProcess pubRun({bool global: false, Iterable<String> args}) { | 164 PubProcess pubRun({bool global: false, Iterable<String> args}) { |
| 164 var pubArgs = global ? ["global", "run"] : ["run"]; | 165 var pubArgs = global ? ["global", "run"] : ["run"]; |
| 165 pubArgs.addAll(args); | 166 pubArgs.addAll(args); |
| 166 var pub = startPub(args: pubArgs); | 167 var pub = startPub(args: pubArgs); |
| 167 | 168 |
| 168 // Loading sources and transformers isn't normally printed, but the pub test | 169 // Loading sources and transformers isn't normally printed, but the pub test |
| 169 // infrastructure runs pub in verbose mode, which enables this. | 170 // infrastructure runs pub in verbose mode, which enables this. |
| 170 pub.stdout.expect(consumeWhile(startsWith("Loading"))); | 171 pub.stdout.expect(consumeWhile(startsWith("Loading"))); |
| 171 | 172 |
| 172 return pub; | 173 return pub; |
| 173 } | 174 } |
| (...skipping 62 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 236 assert(output == null || outputJson == null); | 237 assert(output == null || outputJson == null); |
| 237 | 238 |
| 238 var pub = startPub(args: args, environment: environment); | 239 var pub = startPub(args: args, environment: environment); |
| 239 pub.shouldExit(exitCode); | 240 pub.shouldExit(exitCode); |
| 240 | 241 |
| 241 expect(() async { | 242 expect(() async { |
| 242 var actualOutput = (await pub.stdoutStream().toList()).join("\n"); | 243 var actualOutput = (await pub.stdoutStream().toList()).join("\n"); |
| 243 var actualError = (await pub.stderrStream().toList()).join("\n"); | 244 var actualError = (await pub.stderrStream().toList()).join("\n"); |
| 244 var actualSilent = (await pub.silentStream().toList()).join("\n"); | 245 var actualSilent = (await pub.silentStream().toList()).join("\n"); |
| 245 | 246 |
| 246 var failures = []; | 247 var failures = <String>[]; |
| 247 if (outputJson == null) { | 248 if (outputJson == null) { |
| 248 _validateOutput(failures, 'stdout', output, actualOutput); | 249 _validateOutput(failures, 'stdout', output, actualOutput); |
| 249 } else { | 250 } else { |
| 250 _validateOutputJson( | 251 _validateOutputJson( |
| 251 failures, 'stdout', await awaitObject(outputJson), actualOutput); | 252 failures, 'stdout', await awaitObject(outputJson), actualOutput); |
| 252 } | 253 } |
| 253 | 254 |
| 254 _validateOutput(failures, 'stderr', error, actualError); | 255 _validateOutput(failures, 'stderr', error, actualError); |
| 255 _validateOutput(failures, 'silent', silent, actualSilent); | 256 _validateOutput(failures, 'silent', silent, actualSilent); |
| 256 | 257 |
| 257 if (!failures.isEmpty) throw new TestFailure(failures.join('\n')); | 258 if (!failures.isEmpty) throw new TestFailure(failures.join('\n')); |
| 258 }(), completes); | 259 }(), completes); |
| 259 } | 260 } |
| 260 | 261 |
| 261 /// Like [startPub], but runs `pub lish` in particular with [server] used both | 262 /// Like [startPub], but runs `pub lish` in particular with [server] used both |
| 262 /// as the OAuth2 server (with "/token" as the token endpoint) and as the | 263 /// as the OAuth2 server (with "/token" as the token endpoint) and as the |
| 263 /// package server. | 264 /// package server. |
| 264 /// | 265 /// |
| 265 /// Any futures in [args] will be resolved before the process is started. | 266 /// Any futures in [args] will be resolved before the process is started. |
| 266 ScheduledProcess startPublish(ScheduledServer server, {List args}) { | 267 PubProcess startPublish(ScheduledServer server, {List args}) { |
| 267 var tokenEndpoint = server.url.then((url) => | 268 var tokenEndpoint = server.url.then((url) => |
| 268 url.resolve('/token').toString()); | 269 url.resolve('/token').toString()); |
| 269 if (args == null) args = []; | 270 if (args == null) args = []; |
| 270 args = flatten(['lish', '--server', tokenEndpoint, args]); | 271 args = ['lish', '--server', tokenEndpoint]..addAll(args); |
| 271 return startPub(args: args, tokenEndpoint: tokenEndpoint); | 272 return startPub(args: args, tokenEndpoint: tokenEndpoint); |
| 272 } | 273 } |
| 273 | 274 |
| 274 /// Handles the beginning confirmation process for uploading a packages. | 275 /// Handles the beginning confirmation process for uploading a packages. |
| 275 /// | 276 /// |
| 276 /// Ensures that the right output is shown and then enters "y" to confirm the | 277 /// Ensures that the right output is shown and then enters "y" to confirm the |
| 277 /// upload. | 278 /// upload. |
| 278 void confirmPublish(ScheduledProcess pub) { | 279 void confirmPublish(ScheduledProcess pub) { |
| 279 // TODO(rnystrom): This is overly specific and inflexible regarding different | 280 // TODO(rnystrom): This is overly specific and inflexible regarding different |
| 280 // test packages. Should validate this a little more loosely. | 281 // test packages. Should validate this a little more loosely. |
| (...skipping 23 matching lines...) Expand all Loading... |
| 304 } | 305 } |
| 305 | 306 |
| 306 if (globalServer != null) { | 307 if (globalServer != null) { |
| 307 environment['PUB_HOSTED_URL'] = | 308 environment['PUB_HOSTED_URL'] = |
| 308 "http://localhost:${await globalServer.port}"; | 309 "http://localhost:${await globalServer.port}"; |
| 309 } | 310 } |
| 310 | 311 |
| 311 return environment; | 312 return environment; |
| 312 } | 313 } |
| 313 | 314 |
| 314 /// Starts a Pub process and returns a [ScheduledProcess] that supports | 315 /// Starts a Pub process and returns a [PubProcess] that supports interaction |
| 315 /// interaction with that process. | 316 /// with that process. |
| 316 /// | 317 /// |
| 317 /// Any futures in [args] will be resolved before the process is started. | 318 /// Any futures in [args] will be resolved before the process is started. |
| 318 /// | 319 /// |
| 319 /// If [environment] is given, any keys in it will override the environment | 320 /// If [environment] is given, any keys in it will override the environment |
| 320 /// variables passed to the spawned process. | 321 /// variables passed to the spawned process. |
| 321 ScheduledProcess startPub({List args, Future<String> tokenEndpoint, | 322 PubProcess startPub({List args, Future<String> tokenEndpoint, |
| 322 Map<String, String> environment}) { | 323 Map<String, String> environment}) { |
| 324 args ??= []; |
| 325 |
| 323 schedule(() { | 326 schedule(() { |
| 324 ensureDir(_pathInSandbox(appPath)); | 327 ensureDir(_pathInSandbox(appPath)); |
| 325 }, "ensuring $appPath exists"); | 328 }, "ensuring $appPath exists"); |
| 326 | 329 |
| 327 // Find a Dart executable we can use to spawn. Use the same one that was | 330 // Find a Dart executable we can use to spawn. Use the same one that was |
| 328 // used to run this script itself. | 331 // used to run this script itself. |
| 329 var dartBin = Platform.executable; | 332 var dartBin = Platform.executable; |
| 330 | 333 |
| 331 // If the executable looks like a path, get its full path. That way we | 334 // If the executable looks like a path, get its full path. That way we |
| 332 // can still find it when we spawn it with a different working directory. | 335 // can still find it when we spawn it with a different working directory. |
| 333 if (dartBin.contains(Platform.pathSeparator)) { | 336 if (dartBin.contains(Platform.pathSeparator)) { |
| 334 dartBin = p.absolute(dartBin); | 337 dartBin = p.absolute(dartBin); |
| 335 } | 338 } |
| 336 | 339 |
| 337 // If there's a snapshot available, use it. The user is responsible for | 340 // If there's a snapshot available, use it. The user is responsible for |
| 338 // ensuring this is up-to-date.. | 341 // ensuring this is up-to-date.. |
| 339 // | 342 // |
| 340 // TODO(nweiz): When the test runner supports plugins, create one to | 343 // TODO(nweiz): When the test runner supports plugins, create one to |
| 341 // auto-generate the snapshot before each run. | 344 // auto-generate the snapshot before each run. |
| 342 var pubPath = p.absolute(p.join(pubRoot, 'bin/pub.dart')); | 345 var pubPath = p.absolute(p.join(pubRoot, 'bin/pub.dart')); |
| 343 if (fileExists('$pubPath.snapshot')) pubPath += '.snapshot'; | 346 if (fileExists('$pubPath.snapshot')) pubPath += '.snapshot'; |
| 344 | 347 |
| 345 var dartArgs = [ | 348 var dartArgs = <dynamic>[ |
| 346 '--package-root=${p.toUri(p.absolute(p.fromUri(Platform.packageRoot)))}', | 349 '--package-root=${p.toUri(p.absolute(p.fromUri(Platform.packageRoot)))}', |
| 347 pubPath, | 350 pubPath, |
| 348 '--verbose' | 351 '--verbose' |
| 349 ]..addAll(args); | 352 ]..addAll(args); |
| 350 | 353 |
| 351 if (tokenEndpoint == null) tokenEndpoint = new Future.value(); | 354 if (tokenEndpoint == null) tokenEndpoint = new Future.value(); |
| 352 var environmentFuture = tokenEndpoint | 355 var environmentFuture = tokenEndpoint |
| 353 .then((tokenEndpoint) => getPubTestEnvironment(tokenEndpoint)) | 356 .then((tokenEndpoint) => getPubTestEnvironment(tokenEndpoint)) |
| 354 .then((pubEnvironment) { | 357 .then((pubEnvironment) { |
| 355 if (environment != null) pubEnvironment.addAll(environment); | 358 if (environment != null) pubEnvironment.addAll(environment); |
| (...skipping 18 matching lines...) Expand all Loading... |
| 374 {workingDirectory, environment, String description, | 377 {workingDirectory, environment, String description, |
| 375 Encoding encoding: UTF8}) | 378 Encoding encoding: UTF8}) |
| 376 : super.start(executable, arguments, | 379 : super.start(executable, arguments, |
| 377 workingDirectory: workingDirectory, | 380 workingDirectory: workingDirectory, |
| 378 environment: environment, | 381 environment: environment, |
| 379 description: description, | 382 description: description, |
| 380 encoding: encoding); | 383 encoding: encoding); |
| 381 | 384 |
| 382 Stream<Pair<log.Level, String>> _logStream() { | 385 Stream<Pair<log.Level, String>> _logStream() { |
| 383 if (_log == null) { | 386 if (_log == null) { |
| 384 _log = mergeStreams( | 387 _log = StreamGroup.merge([ |
| 385 _outputToLog(super.stdoutStream(), log.Level.MESSAGE), | 388 _outputToLog(super.stdoutStream(), log.Level.MESSAGE), |
| 386 _outputToLog(super.stderrStream(), log.Level.ERROR)); | 389 _outputToLog(super.stderrStream(), log.Level.ERROR) |
| 390 ]); |
| 387 } | 391 } |
| 388 | 392 |
| 389 var pair = tee(_log); | 393 var logs = StreamSplitter.splitFrom(_log); |
| 390 _log = pair.first; | 394 _log = logs.first; |
| 391 return pair.last; | 395 return logs.last; |
| 392 } | 396 } |
| 393 | 397 |
| 394 final _logLineRegExp = new RegExp(r"^([A-Z ]{4})[:|] (.*)$"); | 398 final _logLineRegExp = new RegExp(r"^([A-Z ]{4})[:|] (.*)$"); |
| 395 final _logLevels = [ | 399 final _logLevels = [ |
| 396 log.Level.ERROR, log.Level.WARNING, log.Level.MESSAGE, log.Level.IO, | 400 log.Level.ERROR, log.Level.WARNING, log.Level.MESSAGE, log.Level.IO, |
| 397 log.Level.SOLVER, log.Level.FINE | 401 log.Level.SOLVER, log.Level.FINE |
| 398 ].fold(<String, log.Level>{}, (levels, level) { | 402 ].fold(<String, log.Level>{}, (levels, level) { |
| 399 levels[level.name] = level; | 403 levels[level.name] = level; |
| 400 return levels; | 404 return levels; |
| 401 }); | 405 }); |
| (...skipping 13 matching lines...) Expand all Loading... |
| 415 } | 419 } |
| 416 | 420 |
| 417 Stream<String> stdoutStream() { | 421 Stream<String> stdoutStream() { |
| 418 if (_stdout == null) { | 422 if (_stdout == null) { |
| 419 _stdout = _logStream().expand((entry) { | 423 _stdout = _logStream().expand((entry) { |
| 420 if (entry.first != log.Level.MESSAGE) return []; | 424 if (entry.first != log.Level.MESSAGE) return []; |
| 421 return [entry.last]; | 425 return [entry.last]; |
| 422 }); | 426 }); |
| 423 } | 427 } |
| 424 | 428 |
| 425 var pair = tee(_stdout); | 429 var stdouts = StreamSplitter.splitFrom(_stdout); |
| 426 _stdout = pair.first; | 430 _stdout = stdouts.first; |
| 427 return pair.last; | 431 return stdouts.last; |
| 428 } | 432 } |
| 429 | 433 |
| 430 Stream<String> stderrStream() { | 434 Stream<String> stderrStream() { |
| 431 if (_stderr == null) { | 435 if (_stderr == null) { |
| 432 _stderr = _logStream().expand((entry) { | 436 _stderr = _logStream().expand((entry) { |
| 433 if (entry.first != log.Level.ERROR && | 437 if (entry.first != log.Level.ERROR && |
| 434 entry.first != log.Level.WARNING) { | 438 entry.first != log.Level.WARNING) { |
| 435 return []; | 439 return []; |
| 436 } | 440 } |
| 437 return [entry.last]; | 441 return [entry.last]; |
| 438 }); | 442 }); |
| 439 } | 443 } |
| 440 | 444 |
| 441 var pair = tee(_stderr); | 445 var stderrs = StreamSplitter.splitFrom(_stderr); |
| 442 _stderr = pair.first; | 446 _stderr = stderrs.first; |
| 443 return pair.last; | 447 return stderrs.last; |
| 444 } | 448 } |
| 445 | 449 |
| 446 /// A stream of log messages that are silent by default. | 450 /// A stream of log messages that are silent by default. |
| 447 Stream<String> silentStream() { | 451 Stream<String> silentStream() { |
| 448 if (_silent == null) { | 452 if (_silent == null) { |
| 449 _silent = _logStream().expand((entry) { | 453 _silent = _logStream().expand((entry) { |
| 450 if (entry.first == log.Level.MESSAGE) return []; | 454 if (entry.first == log.Level.MESSAGE) return []; |
| 451 if (entry.first == log.Level.ERROR) return []; | 455 if (entry.first == log.Level.ERROR) return []; |
| 452 if (entry.first == log.Level.WARNING) return []; | 456 if (entry.first == log.Level.WARNING) return []; |
| 453 return [entry.last]; | 457 return [entry.last]; |
| 454 }); | 458 }); |
| 455 } | 459 } |
| 456 | 460 |
| 457 var pair = tee(_silent); | 461 var silents = StreamSplitter.splitFrom(_silent); |
| 458 _silent = pair.first; | 462 _silent = silents.first; |
| 459 return pair.last; | 463 return silents.last; |
| 460 } | 464 } |
| 461 } | 465 } |
| 462 | 466 |
| 463 /// Fails the current test if Git is not installed. | 467 /// Fails the current test if Git is not installed. |
| 464 /// | 468 /// |
| 465 /// We require machines running these tests to have git installed. This | 469 /// We require machines running these tests to have git installed. This |
| 466 /// validation gives an easier-to-understand error when that requirement isn't | 470 /// validation gives an easier-to-understand error when that requirement isn't |
| 467 /// met than just failing in the middle of a test when pub invokes git. | 471 /// met than just failing in the middle of a test when pub invokes git. |
| 468 /// | 472 /// |
| 469 /// This also increases the [Schedule] timeout to 30 seconds on Windows, | 473 /// This also increases the [Schedule] timeout to 30 seconds on Windows, |
| (...skipping 104 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 574 var oldInnerClient = innerHttpClient; | 578 var oldInnerClient = innerHttpClient; |
| 575 innerHttpClient = client; | 579 innerHttpClient = client; |
| 576 currentSchedule.onComplete.schedule(() { | 580 currentSchedule.onComplete.schedule(() { |
| 577 innerHttpClient = oldInnerClient; | 581 innerHttpClient = oldInnerClient; |
| 578 }, 'de-activating the mock client'); | 582 }, 'de-activating the mock client'); |
| 579 } | 583 } |
| 580 | 584 |
| 581 /// Describes a map representing a library package with the given [name], | 585 /// Describes a map representing a library package with the given [name], |
| 582 /// [version], and [dependencies]. | 586 /// [version], and [dependencies]. |
| 583 Map packageMap(String name, String version, [Map dependencies]) { | 587 Map packageMap(String name, String version, [Map dependencies]) { |
| 584 var package = { | 588 var package = <String, dynamic>{ |
| 585 "name": name, | 589 "name": name, |
| 586 "version": version, | 590 "version": version, |
| 587 "author": "Natalie Weizenbaum <nweiz@google.com>", | 591 "author": "Natalie Weizenbaum <nweiz@google.com>", |
| 588 "homepage": "http://pub.dartlang.org", | 592 "homepage": "http://pub.dartlang.org", |
| 589 "description": "A package, I guess." | 593 "description": "A package, I guess." |
| 590 }; | 594 }; |
| 591 | 595 |
| 592 if (dependencies != null) package["dependencies"] = dependencies; | 596 if (dependencies != null) package["dependencies"] = dependencies; |
| 593 | 597 |
| 594 return package; | 598 return package; |
| (...skipping 61 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 656 var actualLines = actual.split("\n"); | 660 var actualLines = actual.split("\n"); |
| 657 var expectedLines = expected.split("\n"); | 661 var expectedLines = expected.split("\n"); |
| 658 | 662 |
| 659 // Strip off the last line. This lets us have expected multiline strings | 663 // Strip off the last line. This lets us have expected multiline strings |
| 660 // where the closing ''' is on its own line. It also fixes '' expected output | 664 // where the closing ''' is on its own line. It also fixes '' expected output |
| 661 // to expect zero lines of output, not a single empty line. | 665 // to expect zero lines of output, not a single empty line. |
| 662 if (expectedLines.last.trim() == '') { | 666 if (expectedLines.last.trim() == '') { |
| 663 expectedLines.removeLast(); | 667 expectedLines.removeLast(); |
| 664 } | 668 } |
| 665 | 669 |
| 666 var results = []; | 670 var results = <String>[]; |
| 667 var failed = false; | 671 var failed = false; |
| 668 | 672 |
| 669 // Compare them line by line to see which ones match. | 673 // Compare them line by line to see which ones match. |
| 670 var length = max(expectedLines.length, actualLines.length); | 674 var length = max(expectedLines.length, actualLines.length); |
| 671 for (var i = 0; i < length; i++) { | 675 for (var i = 0; i < length; i++) { |
| 672 if (i >= actualLines.length) { | 676 if (i >= actualLines.length) { |
| 673 // Missing output. | 677 // Missing output. |
| 674 failed = true; | 678 failed = true; |
| 675 results.add('? ${expectedLines[i]}'); | 679 results.add('? ${expectedLines[i]}'); |
| 676 } else if (i >= expectedLines.length) { | 680 } else if (i >= expectedLines.length) { |
| (...skipping 44 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 721 | 725 |
| 722 /// A function that creates a [Validator] subclass. | 726 /// A function that creates a [Validator] subclass. |
| 723 typedef Validator ValidatorCreator(Entrypoint entrypoint); | 727 typedef Validator ValidatorCreator(Entrypoint entrypoint); |
| 724 | 728 |
| 725 /// Schedules a single [Validator] to run on the [appPath]. | 729 /// Schedules a single [Validator] to run on the [appPath]. |
| 726 /// | 730 /// |
| 727 /// Returns a scheduled Future that contains the errors and warnings produced | 731 /// Returns a scheduled Future that contains the errors and warnings produced |
| 728 /// by that validator. | 732 /// by that validator. |
| 729 Future<Pair<List<String>, List<String>>> schedulePackageValidation( | 733 Future<Pair<List<String>, List<String>>> schedulePackageValidation( |
| 730 ValidatorCreator fn) { | 734 ValidatorCreator fn) { |
| 731 return schedule(() { | 735 return schedule/*<Future<Pair<List<String>, List<String>>>>*/(() async { |
| 732 var cache = new SystemCache(rootDir: p.join(sandboxDir, cachePath)); | 736 var cache = new SystemCache(rootDir: p.join(sandboxDir, cachePath)); |
| 733 return new Future.sync(() { | 737 var validator = fn(new Entrypoint(p.join(sandboxDir, appPath), cache)); |
| 734 var validator = fn(new Entrypoint(p.join(sandboxDir, appPath), cache)); | 738 await validator.validate(); |
| 735 return validator.validate().then((_) { | 739 return new Pair(validator.errors, validator.warnings); |
| 736 return new Pair(validator.errors, validator.warnings); | |
| 737 }); | |
| 738 }); | |
| 739 }, "validating package"); | 740 }, "validating package"); |
| 740 } | 741 } |
| 741 | 742 |
| 742 /// A matcher that matches a Pair. | 743 /// A matcher that matches a Pair. |
| 743 Matcher pairOf(Matcher firstMatcher, Matcher lastMatcher) => | 744 Matcher pairOf(Matcher firstMatcher, Matcher lastMatcher) => |
| 744 new _PairMatcher(firstMatcher, lastMatcher); | 745 new _PairMatcher(firstMatcher, lastMatcher); |
| 745 | 746 |
| 746 class _PairMatcher extends Matcher { | 747 class _PairMatcher extends Matcher { |
| 747 final Matcher _firstMatcher; | 748 final Matcher _firstMatcher; |
| 748 final Matcher _lastMatcher; | 749 final Matcher _lastMatcher; |
| 749 | 750 |
| 750 _PairMatcher(this._firstMatcher, this._lastMatcher); | 751 _PairMatcher(this._firstMatcher, this._lastMatcher); |
| 751 | 752 |
| 752 bool matches(item, Map matchState) { | 753 bool matches(item, Map matchState) { |
| 753 if (item is! Pair) return false; | 754 if (item is! Pair) return false; |
| 754 return _firstMatcher.matches(item.first, matchState) && | 755 return _firstMatcher.matches(item.first, matchState) && |
| 755 _lastMatcher.matches(item.last, matchState); | 756 _lastMatcher.matches(item.last, matchState); |
| 756 } | 757 } |
| 757 | 758 |
| 758 Description describe(Description description) { | 759 Description describe(Description description) { |
| 759 return description.addAll("(", ", ", ")", [_firstMatcher, _lastMatcher]); | 760 return description.addAll("(", ", ", ")", [_firstMatcher, _lastMatcher]); |
| 760 } | 761 } |
| 761 } | 762 } |
| 762 | 763 |
| 763 /// A [StreamMatcher] that matches multiple lines of output. | 764 /// A [StreamMatcher] that matches multiple lines of output. |
| 764 StreamMatcher emitsLines(String output) => inOrder(output.split("\n")); | 765 StreamMatcher emitsLines(String output) => inOrder(output.split("\n")); |
| OLD | NEW |