Line data Source code
1 : // Copyright (c) 2017, 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 : import 'dart:async';
6 :
7 : import 'package:async/async.dart';
8 : import 'package:matcher/matcher.dart';
9 :
10 : import '../utils.dart';
11 : import 'test_chain.dart';
12 : import 'async_matcher.dart';
13 :
14 : /// The type for [_StreamMatcher._matchQueue].
15 : typedef Future<String> _MatchQueue(StreamQueue queue);
16 :
17 : /// A matcher that matches events from [Stream]s or [StreamQueue]s.
18 : ///
19 : /// Stream matchers are designed to make it straightforward to create complex
20 : /// expectations for streams, and to interleave expectations with the rest of a
21 : /// test. They can be used on a [Stream] to match all events it emits:
22 : ///
23 : /// ```dart
24 : /// expect(stream, emitsInOrder([
25 : /// // Values match individual events.
26 : /// "Ready.",
27 : ///
28 : /// // Matchers also run against individual events.
29 : /// startsWith("Loading took"),
30 : ///
31 : /// // Stream matchers can be nested. This asserts that one of two events are
32 : /// // emitted after the "Loading took" line.
33 : /// emitsAnyOf(["Succeeded!", "Failed!"]),
34 : ///
35 : /// // By default, more events are allowed after the matcher finishes
36 : /// // matching. This asserts instead that the stream emits a done event and
37 : /// // nothing else.
38 : /// emitsDone
39 : /// ]));
40 : /// ```
41 : ///
42 : /// It can also match a [StreamQueue], in which case it consumes the matched
43 : /// events. The call to [expect] returns a [Future] that completes when the
44 : /// matcher is done matching. You can `await` this to consume different events
45 : /// at different times:
46 : ///
47 : /// ```dart
48 : /// var stdout = new StreamQueue(stdoutLineStream);
49 : ///
50 : /// // Ignore lines from the process until it's about to emit the URL.
51 : /// await expect(stdout, emitsThrough("WebSocket URL:"));
52 : ///
53 : /// // Parse the next line as a URL.
54 : /// var url = Uri.parse(await stdout.next);
55 : /// expect(url.host, equals('localhost'));
56 : ///
57 : /// // You can match against the same StreamQueue multiple times.
58 : /// await expect(stdout, emits("Waiting for connection..."));
59 : /// ```
60 : ///
61 : /// Users can call [new StreamMatcher] to create custom matchers.
62 : abstract class StreamMatcher extends Matcher {
63 : /// The description of this matcher.
64 : ///
65 : /// This is in the subjunctive mood, which means it can be used after the word
66 : /// "should". For example, it might be "emit the right events".
67 : String get description;
68 :
69 : /// Creates a new [StreamMatcher] described by [description] that matches
70 : /// events with [matchQueue].
71 : ///
72 : /// The [matchQueue] callback is used to implement [StreamMatcher.matchQueue],
73 : /// and should follow all the guarantees of that method. In particular:
74 : ///
75 : /// * If it matches successfully, it should return `null` and possibly consume
76 : /// events.
77 : /// * If it fails to match, consume no events and return a description of the
78 : /// failure.
79 : /// * The description should be in past tense.
80 : /// * The description should be gramatically valid when used after "the
81 : /// stream"—"emitted the wrong events", for example.
82 : ///
83 : /// The [matchQueue] callback may return the empty string to indicate a
84 : /// failure if it has no information to add beyond the description of the
85 : /// failure and the events actually emitted by the stream.
86 : ///
87 : /// The [description] should be in the subjunctive mood. This means that it
88 : /// should be grammatically valid when used after the word "should". For
89 : /// example, it might be "emit the right events".
90 : factory StreamMatcher(
91 : Future<String> matchQueue(StreamQueue queue), String description) =
92 : _StreamMatcher;
93 :
94 : /// Tries to match events emitted by [queue].
95 : ///
96 : /// If this matches successfully, it consumes the matching events from [queue]
97 : /// and returns `null`.
98 : ///
99 : /// If this fails to match, it doesn't consume any events and returns a
100 : /// description of the failure. This description is in the past tense, and
101 : /// could grammatically be used after "the stream". For example, it might
102 : /// return "emitted the wrong events".
103 : ///
104 : /// The description string may also be empty, which indicates that the
105 : /// matcher's description and the events actually emitted by the stream are
106 : /// enough to understand the failure.
107 : ///
108 : /// If the queue emits an error, that error is re-thrown unless otherwise
109 : /// indicated by the matcher.
110 : Future<String> matchQueue(StreamQueue queue);
111 : }
112 :
113 : /// A concrete implementation of [StreamMatcher].
114 : ///
115 : /// This is separate from the original type to hide the private [AsyncMatcher]
116 : /// interface.
117 : class _StreamMatcher extends AsyncMatcher implements StreamMatcher {
118 : final String description;
119 :
120 : /// The callback used to implement [matchQueue].
121 : final _MatchQueue _matchQueue;
122 :
123 0 : _StreamMatcher(this._matchQueue, this.description);
124 :
125 0 : Future<String> matchQueue(StreamQueue queue) => _matchQueue(queue);
126 :
127 : /*FutureOr<String>*/ matchAsync(item) {
128 : StreamQueue queue;
129 0 : if (item is StreamQueue) {
130 : queue = item;
131 0 : } else if (item is Stream) {
132 0 : queue = new StreamQueue(item);
133 : } else {
134 : return "was not a Stream or a StreamQueue";
135 : }
136 :
137 : // Avoid async/await in the outer method so that we synchronously error out
138 : // for an invalid argument type.
139 0 : var transaction = queue.startTransaction();
140 0 : var copy = transaction.newQueue();
141 0 : return matchQueue(copy).then((result) async {
142 : // Accept the transaction if the result is null, indicating that the match
143 : // succeeded.
144 : if (result == null) {
145 0 : transaction.commit(copy);
146 : return null;
147 : }
148 :
149 : // Get a list of events emitted by the stream so we can emit them as part
150 : // of the error message.
151 0 : var replay = transaction.newQueue();
152 0 : var events = <Result>[];
153 : var subscription = Result.captureStreamTransformer
154 0 : .bind(replay.rest)
155 0 : .listen(events.add, onDone: () => events.add(null));
156 :
157 : // Wait on a timer tick so all buffered events are emitted.
158 0 : await new Future.delayed(Duration.ZERO);
159 0 : subscription.cancel();
160 :
161 0 : var eventsString = events.map((event) {
162 : if (event == null) {
163 : return "x Stream closed.";
164 0 : } else if (event.isValue) {
165 0 : return addBullet(event.asValue.value.toString());
166 : } else {
167 0 : var error = event.asError;
168 0 : var chain = testChain(error.stackTrace);
169 0 : var text = "${error.error}\n$chain";
170 0 : return prefixLines(text, " ", first: "! ");
171 : }
172 0 : }).join("\n");
173 0 : if (eventsString.isEmpty) eventsString = "no events";
174 :
175 0 : transaction.reject();
176 :
177 0 : var buffer = new StringBuffer();
178 0 : buffer.writeln(indent(eventsString, first: "emitted "));
179 0 : if (result.isNotEmpty) buffer.writeln(indent(result, first: " which "));
180 0 : return buffer.toString().trimRight();
181 0 : }, onError: (error) {
182 0 : transaction.reject();
183 : throw error;
184 : });
185 : }
186 :
187 : Description describe(Description description) =>
188 0 : description.add("should ").add(this.description);
189 : }
|