| OLD | NEW |
| (Empty) | |
| 1 // Copyright (c) 2013, 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 library scheduled_test.stream_matcher; |
| 6 |
| 7 import 'dart:async'; |
| 8 import 'dart:collection'; |
| 9 |
| 10 import '../scheduled_stream.dart'; |
| 11 import '../scheduled_test.dart'; |
| 12 import 'utils.dart'; |
| 13 |
| 14 /// An abstract superclass for matchers that validate and consume zero or more |
| 15 /// values emitted by a [ScheduledStream]. |
| 16 /// |
| 17 /// [StreamMatcher]s are most commonly used by passing them to |
| 18 /// [ScheduledStream.expect]. |
| 19 abstract class StreamMatcher { |
| 20 /// Wrap a [Matcher], [StreamMatcher] or [Object] in a [StreamMatcher]. |
| 21 /// |
| 22 /// If this isn't a [StreamMatcher], a [nextValue] matcher is used. |
| 23 factory StreamMatcher.wrap(matcher) => |
| 24 matcher is StreamMatcher ? matcher : nextValue(matcher); |
| 25 |
| 26 /// Tries to match [this] against [stream]. |
| 27 /// |
| 28 /// If the match succeeds, this returns `null`. If it fails, this returns a |
| 29 /// [Description] describing the failure. |
| 30 Future<Description> tryMatch(ScheduledStream stream); |
| 31 |
| 32 String toString(); |
| 33 } |
| 34 |
| 35 /// A matcher that consumes and matches a single value. |
| 36 /// |
| 37 /// [matcher] can be a [Matcher] or an [Object], but not a [StreamMatcher]. |
| 38 StreamMatcher nextValue(matcher) => new _NextValueMatcher(matcher); |
| 39 |
| 40 /// A matcher that consumes [n] values and matches a list containing those |
| 41 /// objects against [matcher]. |
| 42 /// |
| 43 /// [matcher] can be a [Matcher] or an [Object], but not a [StreamMatcher]. |
| 44 StreamMatcher nextValues(int n, matcher) => new _NextValuesMatcher(n, matcher); |
| 45 |
| 46 /// A matcher that matches several sub-matchers in sequence. |
| 47 /// |
| 48 /// Each element of [streamMatchers] can be a [StreamMatcher], a [Matcher], or |
| 49 /// an [Object]. |
| 50 StreamMatcher inOrder(Iterable streamMatchers) { |
| 51 streamMatchers = streamMatchers.toList(); |
| 52 if (streamMatchers.length == 1) { |
| 53 return new StreamMatcher.wrap(streamMatchers.first); |
| 54 } else { |
| 55 return new _InOrderMatcher(streamMatchers); |
| 56 } |
| 57 } |
| 58 |
| 59 /// A matcher that consumes values emitted by a stream until one matching |
| 60 /// [matcher] is emitted. |
| 61 /// |
| 62 /// This will fail if the stream never emits a value that matches [matcher]. |
| 63 /// |
| 64 /// [matcher] can be a [Matcher] or an [Object], but not a [StreamMatcher]. |
| 65 StreamMatcher consumeThrough(matcher) => new _ConsumeThroughMatcher(matcher); |
| 66 |
| 67 /// A matcher that matches either [streamMatcher1], [streamMatcher2], or both. |
| 68 /// |
| 69 /// If both matchers match the stream, the one that consumed more values will be |
| 70 /// used. |
| 71 /// |
| 72 /// Both [streamMatcher1] and [streamMatcher2] can be a [StreamMatcher], a |
| 73 /// [Matcher], or an [Object]. |
| 74 StreamMatcher either(streamMatcher1, streamMatcher2) => |
| 75 new _EitherMatcher(streamMatcher1, streamMatcher2); |
| 76 |
| 77 /// A matcher that consumes [streamMatcher] if it matches, or nothing otherwise. |
| 78 /// |
| 79 /// This matcher will always match a stream. It exists to consume values that |
| 80 /// may or may not be emitted by a stream. |
| 81 /// |
| 82 /// [streamMatcher] can be a [StreamMatcher], a [Matcher], or an [Object]. |
| 83 StreamMatcher allow(streamMatcher) => new _AllowMatcher(streamMatcher); |
| 84 |
| 85 /// A matcher that matches a stream that emits no more values. |
| 86 StreamMatcher get isDone => new _IsDoneMatcher(); |
| 87 |
| 88 /// See [nextValue]. |
| 89 class _NextValueMatcher implements StreamMatcher { |
| 90 final Matcher _matcher; |
| 91 |
| 92 _NextValueMatcher(matcher) |
| 93 : _matcher = wrapMatcher(matcher); |
| 94 |
| 95 Future<Description> tryMatch(ScheduledStream stream) { |
| 96 return stream.hasNext.then((hasNext) { |
| 97 if (!hasNext) { |
| 98 return new StringDescription("unexpected end of stream"); |
| 99 } |
| 100 return stream.next().then((value) { |
| 101 var matchState = {}; |
| 102 if (_matcher.matches(value, matchState)) return null; |
| 103 return _matcher.describeMismatch(value, new StringDescription(), |
| 104 matchState, false); |
| 105 }); |
| 106 }); |
| 107 } |
| 108 |
| 109 String toString() => _matcher.describe(new StringDescription()).toString(); |
| 110 } |
| 111 |
| 112 /// See [nextValues]. |
| 113 class _NextValuesMatcher implements StreamMatcher { |
| 114 final int _n; |
| 115 final Matcher _matcher; |
| 116 |
| 117 _NextValuesMatcher(this._n, matcher) |
| 118 : _matcher = wrapMatcher(matcher); |
| 119 |
| 120 Future<Description> tryMatch(ScheduledStream stream) { |
| 121 var collectedValues = []; |
| 122 collectValues(count) { |
| 123 if (count == 0) return null; |
| 124 |
| 125 return stream.hasNext.then((hasNext) { |
| 126 if (!hasNext) return new StringDescription('unexpected end of stream'); |
| 127 |
| 128 return stream.next().then((value) { |
| 129 collectedValues.add(value); |
| 130 return collectValues(count - 1); |
| 131 }); |
| 132 }); |
| 133 } |
| 134 |
| 135 return collectValues(_n).then((description) { |
| 136 if (description != null) return description; |
| 137 var matchState = {}; |
| 138 if (_matcher.matches(collectedValues, matchState)) return null; |
| 139 return _matcher.describeMismatch(collectedValues, new StringDescription(), |
| 140 matchState, false); |
| 141 }); |
| 142 } |
| 143 |
| 144 String toString() { |
| 145 return new StringDescription('$_n values that ') |
| 146 .addDescriptionOf(_matcher) |
| 147 .toString(); |
| 148 } |
| 149 } |
| 150 |
| 151 /// See [inOrder]. |
| 152 class _InOrderMatcher implements StreamMatcher { |
| 153 final List<StreamMatcher> _matchers; |
| 154 |
| 155 _InOrderMatcher(Iterable streamMatchers) |
| 156 : _matchers = streamMatchers.map((matcher) => |
| 157 new StreamMatcher.wrap(matcher)).toList(); |
| 158 |
| 159 Future<Description> tryMatch(ScheduledStream stream) { |
| 160 var matchers = new Queue.from(_matchers); |
| 161 |
| 162 matchNext() { |
| 163 if (matchers.isEmpty) return new Future.value(); |
| 164 var matcher = matchers.removeFirst(); |
| 165 return matcher.tryMatch(stream).then((description) { |
| 166 if (description == null) return matchNext(); |
| 167 var newDescription = new StringDescription( |
| 168 'matcher #${_matchers.length - matchers.length} failed'); |
| 169 if (description.length != 0) newDescription.add(':\n$description'); |
| 170 return newDescription; |
| 171 }); |
| 172 } |
| 173 |
| 174 return matchNext(); |
| 175 } |
| 176 |
| 177 String toString() => _matchers |
| 178 .map((matcher) => prefixLines(matcher.toString(), firstPrefix: '* ')) |
| 179 .join('\n'); |
| 180 } |
| 181 |
| 182 /// See [consumeThrough]. |
| 183 class _ConsumeThroughMatcher implements StreamMatcher { |
| 184 final Matcher _matcher; |
| 185 |
| 186 _ConsumeThroughMatcher(matcher) |
| 187 : _matcher = wrapMatcher(matcher); |
| 188 |
| 189 Future<Description> tryMatch(ScheduledStream stream) { |
| 190 consumeNext() { |
| 191 return stream.hasNext.then((hasNext) { |
| 192 if (!hasNext) return new StringDescription("unexpected end of stream"); |
| 193 |
| 194 return stream.next().then((value) { |
| 195 if (_matcher.matches(value, {})) return null; |
| 196 return consumeNext(); |
| 197 }); |
| 198 }); |
| 199 } |
| 200 |
| 201 return consumeNext(); |
| 202 } |
| 203 |
| 204 String toString() { |
| 205 return new StringDescription('values followed by ') |
| 206 .addDescriptionOf(_matcher).toString(); |
| 207 } |
| 208 } |
| 209 |
| 210 /// See [either]. |
| 211 class _EitherMatcher implements StreamMatcher { |
| 212 final StreamMatcher _matcher1; |
| 213 final StreamMatcher _matcher2; |
| 214 |
| 215 _EitherMatcher(streamMatcher1, streamMatcher2) |
| 216 : _matcher1 = new StreamMatcher.wrap(streamMatcher1), |
| 217 _matcher2 = new StreamMatcher.wrap(streamMatcher2); |
| 218 |
| 219 Future<Description> tryMatch(ScheduledStream stream) { |
| 220 var stream1 = stream.fork(); |
| 221 var stream2 = stream.fork(); |
| 222 |
| 223 return Future.wait([ |
| 224 _matcher1.tryMatch(stream1).whenComplete(stream1.close), |
| 225 _matcher2.tryMatch(stream2).whenComplete(stream2.close) |
| 226 ]).then((descriptions) { |
| 227 var description1 = descriptions.first; |
| 228 var description2 = descriptions.last; |
| 229 |
| 230 // If both matchers matched, use the one that consumed more of the stream. |
| 231 if (description1 == null && description2 == null) { |
| 232 if (stream1.emittedValues.length >= stream2.emittedValues.length) { |
| 233 return _matcher1.tryMatch(stream); |
| 234 } else { |
| 235 return _matcher2.tryMatch(stream); |
| 236 } |
| 237 } else if (description1 == null) { |
| 238 return _matcher1.tryMatch(stream); |
| 239 } else if (description2 == null) { |
| 240 return _matcher2.tryMatch(stream); |
| 241 } else { |
| 242 return new StringDescription('both\n') |
| 243 .add(prefixLines(description1.toString(), prefix: ' ')) |
| 244 .add('\nand\n') |
| 245 .add(prefixLines(description2.toString(), prefix: ' ')) |
| 246 .toString(); |
| 247 } |
| 248 }); |
| 249 } |
| 250 |
| 251 String toString() { |
| 252 return new StringDescription('either\n') |
| 253 .add(prefixLines(_matcher1.toString(), prefix: ' ')) |
| 254 .add('\nor\n') |
| 255 .add(prefixLines(_matcher2.toString(), prefix: ' ')) |
| 256 .toString(); |
| 257 } |
| 258 } |
| 259 |
| 260 /// See [allow]. |
| 261 class _AllowMatcher implements StreamMatcher { |
| 262 final StreamMatcher _matcher; |
| 263 |
| 264 _AllowMatcher(streamMatcher) |
| 265 : _matcher = new StreamMatcher.wrap(streamMatcher); |
| 266 |
| 267 Future<Description> tryMatch(ScheduledStream stream) { |
| 268 var fork = stream.fork(); |
| 269 return _matcher.tryMatch(fork).whenComplete(fork.close).then((description) { |
| 270 if (description != null) return null; |
| 271 return _matcher.tryMatch(stream); |
| 272 }); |
| 273 } |
| 274 |
| 275 String toString() { |
| 276 return new StringDescription('allow\n') |
| 277 .add(prefixLines(_matcher.toString())) |
| 278 .toString(); |
| 279 } |
| 280 } |
| 281 |
| 282 /// See [isDone]. |
| 283 class _IsDoneMatcher implements StreamMatcher { |
| 284 _IsDoneMatcher(); |
| 285 |
| 286 Future<Description> tryMatch(ScheduledStream stream) { |
| 287 return stream.hasNext.then((hasNext) { |
| 288 if (!hasNext) return null; |
| 289 return new StringDescription("stream wasn't finished"); |
| 290 }); |
| 291 } |
| 292 |
| 293 String toString() => 'is done'; |
| 294 } |
| OLD | NEW |