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 'async_matcher.dart';
12 : import 'stream_matcher.dart';
13 : import 'throws_matcher.dart';
14 :
15 : /// Returns a [StreamMatcher] that asserts that the stream emits a "done" event.
16 : final emitsDone = new StreamMatcher(
17 : (queue) async => (await queue.hasNext) ? "" : null, "be done");
18 :
19 : /// Returns a [StreamMatcher] for [matcher].
20 : ///
21 : /// If [matcher] is already a [StreamMatcher], it's returned as-is. If it's any
22 : /// other [Matcher], this matches a single event that matches that matcher. If
23 : /// it's any other Object, this matches a single event that's equal to that
24 : /// object.
25 : ///
26 : /// This functions like [wrapMatcher] for [StreamMatcher]s: it can convert any
27 : /// matcher-like value into a proper [StreamMatcher].
28 : StreamMatcher emits(matcher) {
29 0 : if (matcher is StreamMatcher) return matcher;
30 0 : var wrapped = wrapMatcher(matcher);
31 :
32 0 : var matcherDescription = wrapped.describe(new StringDescription());
33 :
34 0 : return new StreamMatcher((queue) async {
35 0 : if (!await queue.hasNext) return "";
36 :
37 0 : var matchState = {};
38 0 : var actual = await queue.next;
39 0 : if (wrapped.matches(actual, matchState)) return null;
40 :
41 0 : var mismatchDescription = new StringDescription();
42 0 : wrapped.describeMismatch(actual, mismatchDescription, matchState, false);
43 :
44 0 : if (mismatchDescription.length == 0) return "";
45 0 : return "emitted an event that $mismatchDescription";
46 0 : },
47 : // TODO(nweiz): add "should" once matcher#42 is fixed.
48 0 : "emit an event that $matcherDescription");
49 : }
50 :
51 : /// Returns a [StreamMatcher] that matches a single error event that matches
52 : /// [matcher].
53 : StreamMatcher emitsError(matcher) {
54 0 : var wrapped = wrapMatcher(matcher);
55 0 : var matcherDescription = wrapped.describe(new StringDescription());
56 0 : var throwsMatcher = throwsA(wrapped) as AsyncMatcher;
57 :
58 0 : return new StreamMatcher(
59 0 : (queue) => throwsMatcher.matchAsync(queue.next),
60 : // TODO(nweiz): add "should" once matcher#42 is fixed.
61 0 : "emit an error that $matcherDescription");
62 : }
63 :
64 : /// Returns a [StreamMatcher] that allows (but doesn't require) [matcher] to
65 : /// match the stream.
66 : ///
67 : /// This matcher always succeeds; if [matcher] doesn't match, this just consumes
68 : /// no events.
69 : StreamMatcher mayEmit(matcher) {
70 0 : var streamMatcher = emits(matcher);
71 0 : return new StreamMatcher((queue) async {
72 0 : await queue.withTransaction(
73 0 : (copy) async => (await streamMatcher.matchQueue(copy)) == null);
74 : return null;
75 0 : }, "maybe ${streamMatcher.description}");
76 : }
77 :
78 : /// Returns a [StreamMatcher] that matches the stream if at least one of
79 : /// [matchers] matches.
80 : ///
81 : /// If multiple matchers match the stream, this chooses the matcher that
82 : /// consumes as many events as possible.
83 : ///
84 : /// If any matchers match the stream, no errors from other matchers are thrown.
85 : /// If no matchers match and multiple matchers threw errors, the first error is
86 : /// re-thrown.
87 : StreamMatcher emitsAnyOf(Iterable matchers) {
88 0 : var streamMatchers = matchers.map(emits).toList();
89 0 : if (streamMatchers.isEmpty) {
90 0 : throw new ArgumentError("matcher may not be empty");
91 : }
92 :
93 0 : if (streamMatchers.length == 1) return streamMatchers.first;
94 0 : var description = "do one of the following:\n" +
95 0 : bullet(streamMatchers.map((matcher) => matcher.description));
96 :
97 0 : return new StreamMatcher((queue) async {
98 0 : var transaction = queue.startTransaction();
99 :
100 : // Allocate the failures list ahead of time so that its order matches the
101 : // order of [matchers], and thus the order the matchers will be listed in
102 : // the description.
103 0 : var failures = new List<String>(matchers.length);
104 :
105 : // The first error thrown. If no matchers match and this exists, we rethrow
106 : // it.
107 : Object firstError;
108 : StackTrace firstStackTrace;
109 :
110 0 : var futures = <Future>[];
111 : StreamQueue consumedMost;
112 0 : for (var i = 0; i < matchers.length; i++) {
113 0 : futures.add(() async {
114 0 : var copy = transaction.newQueue();
115 :
116 : String result;
117 : try {
118 0 : result = await streamMatchers[i].matchQueue(copy);
119 : } catch (error, stackTrace) {
120 : if (firstError == null) {
121 : firstError = error;
122 : firstStackTrace = stackTrace;
123 : }
124 : return;
125 : }
126 :
127 : if (result != null) {
128 0 : failures[i] = result;
129 : } else if (consumedMost == null ||
130 0 : consumedMost.eventsDispatched < copy.eventsDispatched) {
131 : consumedMost = copy;
132 : }
133 0 : }());
134 : }
135 :
136 0 : await Future.wait(futures);
137 :
138 : if (consumedMost == null) {
139 0 : transaction.reject();
140 : if (firstError != null) {
141 0 : await new Future.error(firstError, firstStackTrace);
142 : }
143 :
144 0 : var failureMessages = <String>[];
145 0 : for (var i = 0; i < matchers.length; i++) {
146 0 : var message = "failed to ${streamMatchers[i].description}";
147 0 : if (failures[i].isNotEmpty) {
148 0 : message += message.contains("\n") ? "\n" : " ";
149 0 : message += "because it ${failures[i]}";
150 : }
151 :
152 0 : failureMessages.add(message);
153 : }
154 :
155 0 : return "failed all options:\n${bullet(failureMessages)}";
156 : } else {
157 0 : transaction.commit(consumedMost);
158 : return null;
159 : }
160 0 : }, description);
161 : }
162 :
163 : /// Returns a [StreamMatcher] that matches the stream if each matcher in
164 : /// [matchers] matches, one after another.
165 : ///
166 : /// If any matcher fails to match, this fails and consumes no events.
167 : StreamMatcher emitsInOrder(Iterable matchers) {
168 0 : var streamMatchers = matchers.map(emits).toList();
169 0 : if (streamMatchers.length == 1) return streamMatchers.first;
170 :
171 0 : var description = "do the following in order:\n" +
172 0 : bullet(streamMatchers.map((matcher) => matcher.description));
173 :
174 0 : return new StreamMatcher((queue) async {
175 0 : for (var i = 0; i < streamMatchers.length; i++) {
176 0 : var matcher = streamMatchers[i];
177 0 : var result = await matcher.matchQueue(queue);
178 : if (result == null) continue;
179 :
180 0 : var newResult = "didn't ${matcher.description}";
181 0 : if (result.isNotEmpty) {
182 0 : newResult += newResult.contains("\n") ? "\n" : " ";
183 0 : newResult += "because it $result";
184 : }
185 : return newResult;
186 : }
187 0 : }, description);
188 : }
189 :
190 : /// Returns a [StreamMatcher] that matches any number of events followed by
191 : /// events that match [matcher].
192 : ///
193 : /// This consumes all events matched by [matcher], as well as all events before.
194 : /// If the stream emits a done event without matching [matcher], this fails and
195 : /// consumes no events.
196 : StreamMatcher emitsThrough(matcher) {
197 0 : var streamMatcher = emits(matcher);
198 0 : return new StreamMatcher((queue) async {
199 0 : var failures = <String>[];
200 :
201 0 : tryHere() => queue.withTransaction((copy) async {
202 0 : var result = await streamMatcher.matchQueue(copy);
203 : if (result == null) return true;
204 0 : failures.add(result);
205 : return false;
206 0 : });
207 :
208 0 : while (await queue.hasNext) {
209 0 : if (await tryHere()) return null;
210 0 : await queue.next;
211 : }
212 :
213 : // Try after the queue is done in case the matcher can match an empty
214 : // stream.
215 0 : if (await tryHere()) return null;
216 :
217 0 : var result = "never did ${streamMatcher.description}";
218 :
219 : var failureMessages =
220 0 : bullet(failures.where((failure) => failure.isNotEmpty));
221 0 : if (failureMessages.isNotEmpty) {
222 0 : result += result.contains("\n") ? "\n" : " ";
223 0 : result += "because it:\n$failureMessages";
224 : }
225 :
226 : return result;
227 0 : }, "eventually ${streamMatcher.description}");
228 : }
229 :
230 : /// Returns a [StreamMatcher] that matches any number of events that match
231 : /// [matcher].
232 : ///
233 : /// This consumes events until [matcher] no longer matches. It always succeeds;
234 : /// if [matcher] doesn't match, this just consumes no events. It never rethrows
235 : /// errors.
236 : StreamMatcher mayEmitMultiple(matcher) {
237 0 : var streamMatcher = emits(matcher);
238 :
239 0 : var description = streamMatcher.description;
240 0 : description += description.contains("\n") ? "\n" : " ";
241 0 : description += "zero or more times";
242 :
243 0 : return new StreamMatcher((queue) async {
244 0 : while (await _tryMatch(queue, streamMatcher)) {
245 : // Do nothing; the matcher presumably already consumed events.
246 : }
247 : return null;
248 0 : }, description);
249 : }
250 :
251 : /// Returns a [StreamMatcher] that matches a stream that never matches
252 : /// [matcher].
253 : ///
254 : /// This doesn't complete until the stream emits a done event. It never consumes
255 : /// any events. It never re-throws errors.
256 : StreamMatcher neverEmits(matcher) {
257 0 : var streamMatcher = emits(matcher);
258 0 : return new StreamMatcher((queue) async {
259 : var events = 0;
260 : var matched = false;
261 0 : await queue.withTransaction((copy) async {
262 0 : while (await copy.hasNext) {
263 0 : matched = await _tryMatch(copy, streamMatcher);
264 : if (matched) return false;
265 :
266 0 : events++;
267 :
268 : try {
269 0 : await copy.next;
270 : } catch (_) {
271 : // Ignore errors events.
272 : }
273 : }
274 :
275 0 : matched = await _tryMatch(copy, streamMatcher);
276 : return false;
277 0 : });
278 :
279 : if (!matched) return null;
280 0 : return "after $events ${pluralize('event', events)} did "
281 0 : "${streamMatcher.description}";
282 0 : }, "never ${streamMatcher.description}");
283 : }
284 :
285 : /// Returns whether [matcher] matches [queue] at its current position.
286 : ///
287 : /// This treats errors as failures to match.
288 : Future<bool> _tryMatch(StreamQueue queue, StreamMatcher matcher) {
289 0 : return queue.withTransaction((copy) async {
290 : try {
291 0 : return (await matcher.matchQueue(copy)) == null;
292 : } catch (_) {
293 : return false;
294 : }
295 0 : });
296 : }
297 :
298 : /// Returns a [StreamMatcher] that matches the stream if each matcher in
299 : /// [matchers] matches, in any order.
300 : ///
301 : /// If any matcher fails to match, this fails and consumes no events. If the
302 : /// matchers match in multiple different possible orders, this chooses the order
303 : /// that consumes as many events as possible.
304 : ///
305 : /// If any sequence of matchers matches the stream, no errors from other
306 : /// sequences are thrown. If no sequences match and multiple sequences throw
307 : /// errors, the first error is re-thrown.
308 : ///
309 : /// Note that checking every ordering of [matchers] is O(n!) in the worst case,
310 : /// so this should only be called when there are very few [matchers].
311 : StreamMatcher emitsInAnyOrder(Iterable matchers) {
312 0 : var streamMatchers = matchers.map(emits).toSet();
313 0 : if (streamMatchers.length == 1) return streamMatchers.first;
314 0 : var description = "do the following in any order:\n" +
315 0 : bullet(streamMatchers.map((matcher) => matcher.description));
316 :
317 0 : return new StreamMatcher(
318 0 : (queue) async => await _tryInAnyOrder(queue, streamMatchers) ? null : "",
319 : description);
320 : }
321 :
322 : /// Returns whether [queue] matches [matchers] in any order.
323 : Future<bool> _tryInAnyOrder(
324 : StreamQueue queue, Set<StreamMatcher> matchers) async {
325 0 : if (matchers.length == 1)
326 0 : return await matchers.first.matchQueue(queue) == null;
327 :
328 0 : var transaction = queue.startTransaction();
329 : StreamQueue consumedMost;
330 :
331 : // The first error thrown. If no matchers match and this exists, we rethrow
332 : // it.
333 : Object firstError;
334 : StackTrace firstStackTrace;
335 :
336 0 : await Future.wait(matchers.map((matcher) async {
337 0 : var copy = transaction.newQueue();
338 : try {
339 0 : if (await matcher.matchQueue(copy) != null) return;
340 : } catch (error, stackTrace) {
341 : if (firstError == null) {
342 : firstError = error;
343 : firstStackTrace = stackTrace;
344 : }
345 : return;
346 : }
347 :
348 0 : var rest = new Set<StreamMatcher>.from(matchers);
349 0 : rest.remove(matcher);
350 :
351 : try {
352 0 : if (!await _tryInAnyOrder(copy, rest)) return;
353 : } catch (error, stackTrace) {
354 : if (firstError == null) {
355 : firstError = error;
356 : firstStackTrace = stackTrace;
357 : }
358 : return;
359 : }
360 :
361 : if (consumedMost == null ||
362 0 : consumedMost.eventsDispatched < copy.eventsDispatched) {
363 : consumedMost = copy;
364 : }
365 0 : }));
366 :
367 : if (consumedMost == null) {
368 0 : transaction.reject();
369 0 : if (firstError != null) await new Future.error(firstError, firstStackTrace);
370 : return false;
371 : } else {
372 0 : transaction.commit(consumedMost);
373 : return true;
374 : }
375 0 : }
|