| // 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'; |
| import 'stream_matcher.dart'; |
| import 'throws_matcher.dart'; |
| |
| /// Returns a [StreamMatcher] that asserts that the stream emits a "done" event. |
| final emitsDone = StreamMatcher( |
| (queue) async => (await queue.hasNext) ? '' : null, 'be done'); |
| |
| /// Returns a [StreamMatcher] for [matcher]. |
| /// |
| /// If [matcher] is already a [StreamMatcher], it's returned as-is. If it's any |
| /// other [Matcher], this matches a single event that matches that matcher. If |
| /// it's any other Object, this matches a single event that's equal to that |
| /// object. |
| /// |
| /// This functions like [wrapMatcher] for [StreamMatcher]s: it can convert any |
| /// matcher-like value into a proper [StreamMatcher]. |
| StreamMatcher emits(matcher) { |
| if (matcher is StreamMatcher) return matcher; |
| var wrapped = wrapMatcher(matcher); |
| |
| var matcherDescription = wrapped.describe(StringDescription()); |
| |
| return StreamMatcher((queue) async { |
| if (!await queue.hasNext) return ''; |
| |
| var matchState = {}; |
| var actual = await queue.next; |
| if (wrapped.matches(actual, matchState)) return null; |
| |
| var mismatchDescription = StringDescription(); |
| wrapped.describeMismatch(actual, mismatchDescription, matchState, false); |
| |
| if (mismatchDescription.length == 0) return ''; |
| return 'emitted an event that $mismatchDescription'; |
| }, |
| // TODO(nweiz): add "should" once matcher#42 is fixed. |
| 'emit an event that $matcherDescription'); |
| } |
| |
| /// Returns a [StreamMatcher] that matches a single error event that matches |
| /// [matcher]. |
| StreamMatcher emitsError(matcher) { |
| var wrapped = wrapMatcher(matcher); |
| var matcherDescription = wrapped.describe(StringDescription()); |
| var throwsMatcher = throwsA(wrapped) as AsyncMatcher; |
| |
| return StreamMatcher( |
| (queue) => throwsMatcher.matchAsync(queue.next) as Future<String>, |
| // TODO(nweiz): add "should" once matcher#42 is fixed. |
| 'emit an error that $matcherDescription'); |
| } |
| |
| /// Returns a [StreamMatcher] that allows (but doesn't require) [matcher] to |
| /// match the stream. |
| /// |
| /// This matcher always succeeds; if [matcher] doesn't match, this just consumes |
| /// no events. |
| StreamMatcher mayEmit(matcher) { |
| var streamMatcher = emits(matcher); |
| return StreamMatcher((queue) async { |
| await queue.withTransaction( |
| (copy) async => (await streamMatcher.matchQueue(copy)) == null); |
| return null; |
| }, 'maybe ${streamMatcher.description}'); |
| } |
| |
| /// Returns a [StreamMatcher] that matches the stream if at least one of |
| /// [matchers] matches. |
| /// |
| /// If multiple matchers match the stream, this chooses the matcher that |
| /// consumes as many events as possible. |
| /// |
| /// If any matchers match the stream, no errors from other matchers are thrown. |
| /// If no matchers match and multiple matchers threw errors, the first error is |
| /// re-thrown. |
| StreamMatcher emitsAnyOf(Iterable matchers) { |
| var streamMatchers = matchers.map(emits).toList(); |
| if (streamMatchers.isEmpty) { |
| throw ArgumentError('matcher may not be empty'); |
| } |
| |
| if (streamMatchers.length == 1) return streamMatchers.first; |
| var description = 'do one of the following:\n' + |
| bullet(streamMatchers.map((matcher) => matcher.description)); |
| |
| return StreamMatcher((queue) async { |
| var transaction = queue.startTransaction(); |
| |
| // Allocate the failures list ahead of time so that its order matches the |
| // order of [matchers], and thus the order the matchers will be listed in |
| // the description. |
| var failures = List<String>(matchers.length); |
| |
| // The first error thrown. If no matchers match and this exists, we rethrow |
| // it. |
| Object firstError; |
| StackTrace firstStackTrace; |
| |
| var futures = <Future>[]; |
| StreamQueue consumedMost; |
| for (var i = 0; i < matchers.length; i++) { |
| futures.add(() async { |
| var copy = transaction.newQueue(); |
| |
| String result; |
| try { |
| result = await streamMatchers[i].matchQueue(copy); |
| } catch (error, stackTrace) { |
| if (firstError == null) { |
| firstError = error; |
| firstStackTrace = stackTrace; |
| } |
| return; |
| } |
| |
| if (result != null) { |
| failures[i] = result; |
| } else if (consumedMost == null || |
| consumedMost.eventsDispatched < copy.eventsDispatched) { |
| consumedMost = copy; |
| } |
| }()); |
| } |
| |
| await Future.wait(futures); |
| |
| if (consumedMost == null) { |
| transaction.reject(); |
| if (firstError != null) { |
| await Future.error(firstError, firstStackTrace); |
| } |
| |
| var failureMessages = <String>[]; |
| for (var i = 0; i < matchers.length; i++) { |
| var message = 'failed to ${streamMatchers[i].description}'; |
| if (failures[i].isNotEmpty) { |
| message += message.contains('\n') ? '\n' : ' '; |
| message += 'because it ${failures[i]}'; |
| } |
| |
| failureMessages.add(message); |
| } |
| |
| return 'failed all options:\n${bullet(failureMessages)}'; |
| } else { |
| transaction.commit(consumedMost); |
| return null; |
| } |
| }, description); |
| } |
| |
| /// Returns a [StreamMatcher] that matches the stream if each matcher in |
| /// [matchers] matches, one after another. |
| /// |
| /// If any matcher fails to match, this fails and consumes no events. |
| StreamMatcher emitsInOrder(Iterable matchers) { |
| var streamMatchers = matchers.map(emits).toList(); |
| if (streamMatchers.length == 1) return streamMatchers.first; |
| |
| var description = 'do the following in order:\n' + |
| bullet(streamMatchers.map((matcher) => matcher.description)); |
| |
| return StreamMatcher((queue) async { |
| for (var i = 0; i < streamMatchers.length; i++) { |
| var matcher = streamMatchers[i]; |
| var result = await matcher.matchQueue(queue); |
| if (result == null) continue; |
| |
| var newResult = "didn't ${matcher.description}"; |
| if (result.isNotEmpty) { |
| newResult += newResult.contains('\n') ? '\n' : ' '; |
| newResult += 'because it $result'; |
| } |
| return newResult; |
| } |
| return null; |
| }, description); |
| } |
| |
| /// Returns a [StreamMatcher] that matches any number of events followed by |
| /// events that match [matcher]. |
| /// |
| /// This consumes all events matched by [matcher], as well as all events before. |
| /// If the stream emits a done event without matching [matcher], this fails and |
| /// consumes no events. |
| StreamMatcher emitsThrough(matcher) { |
| var streamMatcher = emits(matcher); |
| return StreamMatcher((queue) async { |
| var failures = <String>[]; |
| |
| Future<bool> tryHere() => queue.withTransaction((copy) async { |
| var result = await streamMatcher.matchQueue(copy); |
| if (result == null) return true; |
| failures.add(result); |
| return false; |
| }); |
| |
| while (await queue.hasNext) { |
| if (await tryHere()) return null; |
| await queue.next; |
| } |
| |
| // Try after the queue is done in case the matcher can match an empty |
| // stream. |
| if (await tryHere()) return null; |
| |
| var result = 'never did ${streamMatcher.description}'; |
| |
| var failureMessages = |
| bullet(failures.where((failure) => failure.isNotEmpty)); |
| if (failureMessages.isNotEmpty) { |
| result += result.contains('\n') ? '\n' : ' '; |
| result += 'because it:\n$failureMessages'; |
| } |
| |
| return result; |
| }, 'eventually ${streamMatcher.description}'); |
| } |
| |
| /// Returns a [StreamMatcher] that matches any number of events that match |
| /// [matcher]. |
| /// |
| /// This consumes events until [matcher] no longer matches. It always succeeds; |
| /// if [matcher] doesn't match, this just consumes no events. It never rethrows |
| /// errors. |
| StreamMatcher mayEmitMultiple(matcher) { |
| var streamMatcher = emits(matcher); |
| |
| var description = streamMatcher.description; |
| description += description.contains('\n') ? '\n' : ' '; |
| description += 'zero or more times'; |
| |
| return StreamMatcher((queue) async { |
| while (await _tryMatch(queue, streamMatcher)) { |
| // Do nothing; the matcher presumably already consumed events. |
| } |
| return null; |
| }, description); |
| } |
| |
| /// Returns a [StreamMatcher] that matches a stream that never matches |
| /// [matcher]. |
| /// |
| /// This doesn't complete until the stream emits a done event. It never consumes |
| /// any events. It never re-throws errors. |
| StreamMatcher neverEmits(matcher) { |
| var streamMatcher = emits(matcher); |
| return StreamMatcher((queue) async { |
| var events = 0; |
| var matched = false; |
| await queue.withTransaction((copy) async { |
| while (await copy.hasNext) { |
| matched = await _tryMatch(copy, streamMatcher); |
| if (matched) return false; |
| |
| events++; |
| |
| try { |
| await copy.next; |
| } catch (_) { |
| // Ignore errors events. |
| } |
| } |
| |
| matched = await _tryMatch(copy, streamMatcher); |
| return false; |
| }); |
| |
| if (!matched) return null; |
| return "after $events ${pluralize('event', events)} did " |
| '${streamMatcher.description}'; |
| }, 'never ${streamMatcher.description}'); |
| } |
| |
| /// Returns whether [matcher] matches [queue] at its current position. |
| /// |
| /// This treats errors as failures to match. |
| Future<bool> _tryMatch(StreamQueue queue, StreamMatcher matcher) { |
| return queue.withTransaction((copy) async { |
| try { |
| return (await matcher.matchQueue(copy)) == null; |
| } catch (_) { |
| return false; |
| } |
| }); |
| } |
| |
| /// Returns a [StreamMatcher] that matches the stream if each matcher in |
| /// [matchers] matches, in any order. |
| /// |
| /// If any matcher fails to match, this fails and consumes no events. If the |
| /// matchers match in multiple different possible orders, this chooses the order |
| /// that consumes as many events as possible. |
| /// |
| /// If any sequence of matchers matches the stream, no errors from other |
| /// sequences are thrown. If no sequences match and multiple sequences throw |
| /// errors, the first error is re-thrown. |
| /// |
| /// Note that checking every ordering of [matchers] is O(n!) in the worst case, |
| /// so this should only be called when there are very few [matchers]. |
| StreamMatcher emitsInAnyOrder(Iterable matchers) { |
| var streamMatchers = matchers.map(emits).toSet(); |
| if (streamMatchers.length == 1) return streamMatchers.first; |
| var description = 'do the following in any order:\n' + |
| bullet(streamMatchers.map((matcher) => matcher.description)); |
| |
| return StreamMatcher( |
| (queue) async => await _tryInAnyOrder(queue, streamMatchers) ? null : '', |
| description); |
| } |
| |
| /// Returns whether [queue] matches [matchers] in any order. |
| Future<bool> _tryInAnyOrder( |
| StreamQueue queue, Set<StreamMatcher> matchers) async { |
| if (matchers.length == 1) { |
| return await matchers.first.matchQueue(queue) == null; |
| } |
| |
| var transaction = queue.startTransaction(); |
| StreamQueue consumedMost; |
| |
| // The first error thrown. If no matchers match and this exists, we rethrow |
| // it. |
| Object firstError; |
| StackTrace firstStackTrace; |
| |
| await Future.wait(matchers.map((matcher) async { |
| var copy = transaction.newQueue(); |
| try { |
| if (await matcher.matchQueue(copy) != null) return; |
| } catch (error, stackTrace) { |
| if (firstError == null) { |
| firstError = error; |
| firstStackTrace = stackTrace; |
| } |
| return; |
| } |
| |
| var rest = Set<StreamMatcher>.from(matchers); |
| rest.remove(matcher); |
| |
| try { |
| if (!await _tryInAnyOrder(copy, rest)) return; |
| } catch (error, stackTrace) { |
| if (firstError == null) { |
| firstError = error; |
| firstStackTrace = stackTrace; |
| } |
| return; |
| } |
| |
| if (consumedMost == null || |
| consumedMost.eventsDispatched < copy.eventsDispatched) { |
| consumedMost = copy; |
| } |
| })); |
| |
| if (consumedMost == null) { |
| transaction.reject(); |
| if (firstError != null) await Future.error(firstError, firstStackTrace); |
| return false; |
| } else { |
| transaction.commit(consumedMost); |
| return true; |
| } |
| } |