blob: 8392d96de12edb5f35cda50183b875be55e88eaf [file] [log] [blame]
// 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 'test_chain.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 chain = testChain(error.stackTrace);
var text = "${error.error}\n$chain";
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);
}