-
Notifications
You must be signed in to change notification settings - Fork 209
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
9 changed files
with
1,065 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,187 @@ | ||
// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file | ||
// for details. All rights reserved. Use of this source code is governed by a | ||
// BSD-style license that can be found in the LICENSE file. | ||
|
||
import 'dart:async'; | ||
|
||
import 'package:async/async.dart'; | ||
import 'package:matcher/matcher.dart'; | ||
|
||
import '../utils.dart'; | ||
import 'async_matcher.dart'; | ||
|
||
/// The type for [_StreamMatcher._matchQueue]. | ||
typedef Future<String> _MatchQueue(StreamQueue queue); | ||
|
||
/// A matcher that matches events from [Stream]s or [StreamQueue]s. | ||
/// | ||
/// Stream matchers are designed to make it straightforward to create complex | ||
/// expectations for streams, and to interleave expectations with the rest of a | ||
/// test. They can be used on a [Stream] to match all events it emits: | ||
/// | ||
/// ```dart | ||
/// expect(stream, emitsInOrder([ | ||
/// // Values match individual events. | ||
/// "Ready.", | ||
/// | ||
/// // Matchers also run against individual events. | ||
/// startsWith("Loading took"), | ||
/// | ||
/// // Stream matchers can be nested. This asserts that one of two events are | ||
/// // emitted after the "Loading took" line. | ||
/// emitsAnyOf(["Succeeded!", "Failed!"]), | ||
/// | ||
/// // By default, more events are allowed after the matcher finishes | ||
/// // matching. This asserts instead that the stream emits a done event and | ||
/// // nothing else. | ||
/// emitsDone | ||
/// ])); | ||
/// ``` | ||
/// | ||
/// It can also match a [StreamQueue], in which case it consumes the matched | ||
/// events. The call to [expect] returns a [Future] that completes when the | ||
/// matcher is done matching. You can `await` this to consume different events | ||
/// at different times: | ||
/// | ||
/// ```dart | ||
/// var stdout = new StreamQueue(stdoutLineStream); | ||
/// | ||
/// // Ignore lines from the process until it's about to emit the URL. | ||
/// await expect(stdout, emitsThrough("WebSocket URL:")); | ||
/// | ||
/// // Parse the next line as a URL. | ||
/// var url = Uri.parse(await stdout.next); | ||
/// expect(url.host, equals('localhost')); | ||
/// | ||
/// // You can match against the same StreamQueue multiple times. | ||
/// await expect(stdout, emits("Waiting for connection...")); | ||
/// ``` | ||
/// | ||
/// Users can call [new StreamMatcher] to create custom matchers. | ||
abstract class StreamMatcher extends Matcher { | ||
/// The description of this matcher. | ||
/// | ||
/// This is in the subjunctive mood, which means it can be used after the word | ||
/// "should". For example, it might be "emit the right events". | ||
String get description; | ||
|
||
/// Creates a new [StreamMatcher] described by [description] that matches | ||
/// events with [matchQueue]. | ||
/// | ||
/// The [matchQueue] callback is used to implement [StreamMatcher.matchQueue], | ||
/// and should follow all the guarantees of that method. In particular: | ||
/// | ||
/// * If it matches successfully, it should return `null` and possibly consume | ||
/// events. | ||
/// * If it fails to match, consume no events and return a description of the | ||
/// failure. | ||
/// * The description should be in past tense. | ||
/// * The description should be gramatically valid when used after "the | ||
/// stream"—"emitted the wrong events", for example. | ||
/// | ||
/// The [matchQueue] callback may return the empty string to indicate a | ||
/// failure if it has no information to add beyond the description of the | ||
/// failure and the events actually emitted by the stream. | ||
/// | ||
/// The [description] should be in the subjunctive mood. This means that it | ||
/// should be grammatically valid when used after the word "should". For | ||
/// example, it might be "emit the right events". | ||
factory StreamMatcher( | ||
Future<String> matchQueue(StreamQueue queue), | ||
String description) | ||
= _StreamMatcher; | ||
|
||
/// Tries to match events emitted by [queue]. | ||
/// | ||
/// If this matches successfully, it consumes the matching events from [queue] | ||
/// and returns `null`. | ||
/// | ||
/// If this fails to match, it doesn't consume any events and returns a | ||
/// description of the failure. This description is in the past tense, and | ||
/// could grammatically be used after "the stream". For example, it might | ||
/// return "emitted the wrong events". | ||
/// | ||
/// The description string may also be empty, which indicates that the | ||
/// matcher's description and the events actually emitted by the stream are | ||
/// enough to understand the failure. | ||
/// | ||
/// If the queue emits an error, that error is re-thrown unless otherwise | ||
/// indicated by the matcher. | ||
Future<String> matchQueue(StreamQueue queue); | ||
} | ||
|
||
/// A concrete implementation of [StreamMatcher]. | ||
/// | ||
/// This is separate from the original type to hide the private [AsyncMatcher] | ||
/// interface. | ||
class _StreamMatcher extends AsyncMatcher implements StreamMatcher { | ||
final String description; | ||
|
||
/// The callback used to implement [matchQueue]. | ||
final _MatchQueue _matchQueue; | ||
|
||
_StreamMatcher(this._matchQueue, this.description); | ||
|
||
Future<String> matchQueue(StreamQueue queue) => _matchQueue(queue); | ||
|
||
/*FutureOr<String>*/ matchAsync(item) { | ||
StreamQueue queue; | ||
if (item is StreamQueue) { | ||
queue = item; | ||
} else if (item is Stream) { | ||
queue = new StreamQueue(item); | ||
} else { | ||
return "was not a Stream or a StreamQueue"; | ||
} | ||
|
||
// Avoid async/await in the outer method so that we synchronously error out | ||
// for an invalid argument type. | ||
var transaction = queue.startTransaction(); | ||
var copy = transaction.newQueue(); | ||
return matchQueue(copy).then((result) async { | ||
// Accept the transaction if the result is null, indicating that the match | ||
// succeeded. | ||
if (result == null) { | ||
transaction.commit(copy); | ||
return null; | ||
} | ||
|
||
// Get a list of events emitted by the stream so we can emit them as part | ||
// of the error message. | ||
var replay = transaction.newQueue(); | ||
var events = <Result>[]; | ||
var subscription = Result.captureStreamTransformer.bind(replay.rest) | ||
.listen(events.add, onDone: () => events.add(null)); | ||
|
||
// Wait on a timer tick so all buffered events are emitted. | ||
await new Future.delayed(Duration.ZERO); | ||
subscription.cancel(); | ||
|
||
var eventsString = events.map((event) { | ||
if (event == null) { | ||
return "x Stream closed."; | ||
} else if (event.isValue) { | ||
return addBullet(event.asValue.value.toString()); | ||
} else { | ||
var error = event.asError; | ||
var text = "${error.error}\n${testChain(error.stackTrace)}"; | ||
return prefixLines(text, " ", first: "! "); | ||
} | ||
}).join("\n"); | ||
if (eventsString.isEmpty) eventsString = "no events"; | ||
|
||
transaction.reject(); | ||
|
||
var buffer = new StringBuffer(); | ||
buffer.writeln(indent(eventsString, first: "emitted ")); | ||
if (result.isNotEmpty) buffer.writeln(indent(result, first: " which ")); | ||
return buffer.toString().trimRight(); | ||
}, onError: (error) { | ||
transaction.reject(); | ||
throw error; | ||
}); | ||
} | ||
|
||
Description describe(Description description) => | ||
description.add("should ").add(this.description); | ||
} |
Oops, something went wrong.
459f80b
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ohh, this is great! Makes very happy, thanks @nex3