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