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 |