| 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 StreamMatcher(); | |
| 27 | |
| 28 /// Tries to match [this] against [stream]. | |
| 29 /// | |
| 30 /// If the match succeeds, this returns `null`. If it fails, this returns a | |
| 31 /// [Description] describing the failure. | |
| 32 Future<Description> tryMatch(ScheduledStream stream); | |
| 33 | |
| 34 /// Returns whether [this] would match [stream] without actually consuming | |
| 35 /// values from [stream]. | |
| 36 /// | |
| 37 /// If the match succeeds, this returns `null`. If it fails, this returns a | |
| 38 /// [Description] describing the failure. | |
| 39 Future<Description> hasMatch(ScheduledStream stream) { | |
| 40 var fork = stream.fork(); | |
| 41 return tryMatch(fork).whenComplete(fork.close); | |
| 42 } | |
| 43 | |
| 44 String toString(); | |
| 45 } | |
| 46 | |
| 47 /// A matcher that consumes and matches a single value. | |
| 48 /// | |
| 49 /// [matcher] can be a [Matcher] or an [Object], but not a [StreamMatcher]. | |
| 50 StreamMatcher nextValue(matcher) => new _NextValueMatcher(matcher); | |
| 51 | |
| 52 /// A matcher that consumes [n] values and matches a list containing those | |
| 53 /// objects against [matcher]. | |
| 54 /// | |
| 55 /// [matcher] can be a [Matcher] or an [Object], but not a [StreamMatcher]. | |
| 56 StreamMatcher nextValues(int n, matcher) => new _NextValuesMatcher(n, matcher); | |
| 57 | |
| 58 /// A matcher that matches several sub-matchers in sequence. | |
| 59 /// | |
| 60 /// Each element of [streamMatchers] can be a [StreamMatcher], a [Matcher], or | |
| 61 /// an [Object]. | |
| 62 StreamMatcher inOrder(Iterable streamMatchers) { | |
| 63 streamMatchers = streamMatchers.toList(); | |
| 64 if (streamMatchers.length == 1) { | |
| 65 return new StreamMatcher.wrap(streamMatchers.first); | |
| 66 } else { | |
| 67 return new _InOrderMatcher(streamMatchers); | |
| 68 } | |
| 69 } | |
| 70 | |
| 71 /// A matcher that consumes values emitted by a stream until one matching | |
| 72 /// [streamMatcher] is emitted. | |
| 73 /// | |
| 74 /// This will fail if the stream never emits a value that matches | |
| 75 /// [streamMatcher]. | |
| 76 /// | |
| 77 /// [streamMatcher] can be a [StreamMatcher], a [Matcher] or an [Object]. | |
| 78 StreamMatcher consumeThrough(streamMatcher) => | |
| 79 new _ConsumeThroughMatcher(streamMatcher); | |
| 80 | |
| 81 /// A matcher that consumes values emitted by a stream as long as they match | |
| 82 /// [streamMatcher]. | |
| 83 /// | |
| 84 /// This matcher will always match a stream. It exists to consume values. If | |
| 85 /// [streamMatcher] consumes more than one value, the next match will begin | |
| 86 /// after all the consumed values. | |
| 87 /// | |
| 88 /// [streamMatcher] can be a [StreamMatcher], a [Matcher] or an [Object]. | |
| 89 StreamMatcher consumeWhile(streamMatcher) => | |
| 90 new _ConsumeWhileMatcher(streamMatcher); | |
| 91 | |
| 92 /// A matcher that matches either [streamMatcher1], [streamMatcher2], or both. | |
| 93 /// | |
| 94 /// If both matchers match the stream, the one that consumed more values will be | |
| 95 /// used. | |
| 96 /// | |
| 97 /// Both [streamMatcher1] and [streamMatcher2] can be a [StreamMatcher], a | |
| 98 /// [Matcher], or an [Object]. | |
| 99 StreamMatcher either(streamMatcher1, streamMatcher2) => | |
| 100 new _EitherMatcher(streamMatcher1, streamMatcher2); | |
| 101 | |
| 102 /// A matcher that consumes [streamMatcher] if it matches, or nothing otherwise. | |
| 103 /// | |
| 104 /// This matcher will always match a stream. It exists to consume values that | |
| 105 /// may or may not be emitted by a stream. | |
| 106 /// | |
| 107 /// [streamMatcher] can be a [StreamMatcher], a [Matcher], or an [Object]. | |
| 108 StreamMatcher allow(streamMatcher) => new _AllowMatcher(streamMatcher); | |
| 109 | |
| 110 /// A matcher that asserts that a stream never emits values matching | |
| 111 /// [streamMatcher]. | |
| 112 /// | |
| 113 /// This will consume the remainder of a stream. | |
| 114 /// | |
| 115 /// [streamMatcher] can be a [StreamMatcher], a [Matcher], or an [Object]. | |
| 116 StreamMatcher never(streamMatcher) => new _NeverMatcher(streamMatcher); | |
| 117 | |
| 118 /// A matcher that matches a stream that emits no more values. | |
| 119 StreamMatcher get isDone => new _IsDoneMatcher(); | |
| 120 | |
| 121 /// See [nextValue]. | |
| 122 class _NextValueMatcher extends StreamMatcher { | |
| 123 final Matcher _matcher; | |
| 124 | |
| 125 _NextValueMatcher(matcher) | |
| 126 : _matcher = wrapMatcher(matcher); | |
| 127 | |
| 128 Future<Description> tryMatch(ScheduledStream stream) { | |
| 129 return stream.hasNext.then((hasNext) { | |
| 130 if (!hasNext) { | |
| 131 return new StringDescription("unexpected end of stream"); | |
| 132 } | |
| 133 return stream.next().then((value) { | |
| 134 var matchState = {}; | |
| 135 if (_matcher.matches(value, matchState)) return null; | |
| 136 return _matcher.describeMismatch(value, new StringDescription(), | |
| 137 matchState, false); | |
| 138 }); | |
| 139 }); | |
| 140 } | |
| 141 | |
| 142 String toString() => _matcher.describe(new StringDescription()).toString(); | |
| 143 } | |
| 144 | |
| 145 /// See [nextValues]. | |
| 146 class _NextValuesMatcher extends StreamMatcher { | |
| 147 final int _n; | |
| 148 final Matcher _matcher; | |
| 149 | |
| 150 _NextValuesMatcher(this._n, matcher) | |
| 151 : _matcher = wrapMatcher(matcher); | |
| 152 | |
| 153 Future<Description> tryMatch(ScheduledStream stream) { | |
| 154 var collectedValues = []; | |
| 155 collectValues(count) { | |
| 156 if (count == 0) return null; | |
| 157 | |
| 158 return stream.hasNext.then((hasNext) { | |
| 159 if (!hasNext) return new StringDescription('unexpected end of stream'); | |
| 160 | |
| 161 return stream.next().then((value) { | |
| 162 collectedValues.add(value); | |
| 163 return collectValues(count - 1); | |
| 164 }); | |
| 165 }); | |
| 166 } | |
| 167 | |
| 168 return collectValues(_n).then((failure) { | |
| 169 if (failure != null) return failure; | |
| 170 var matchState = {}; | |
| 171 if (_matcher.matches(collectedValues, matchState)) return null; | |
| 172 return _matcher.describeMismatch(collectedValues, new StringDescription(), | |
| 173 matchState, false); | |
| 174 }); | |
| 175 } | |
| 176 | |
| 177 String toString() { | |
| 178 return new StringDescription('$_n values that ') | |
| 179 .addDescriptionOf(_matcher) | |
| 180 .toString(); | |
| 181 } | |
| 182 } | |
| 183 | |
| 184 /// See [inOrder]. | |
| 185 class _InOrderMatcher extends StreamMatcher { | |
| 186 final List<StreamMatcher> _matchers; | |
| 187 | |
| 188 _InOrderMatcher(Iterable streamMatchers) | |
| 189 : _matchers = streamMatchers.map((matcher) => | |
| 190 new StreamMatcher.wrap(matcher)).toList(); | |
| 191 | |
| 192 Future<Description> tryMatch(ScheduledStream stream) { | |
| 193 var matchers = new Queue.from(_matchers); | |
| 194 | |
| 195 matchNext() { | |
| 196 if (matchers.isEmpty) return new Future.value(); | |
| 197 var matcher = matchers.removeFirst(); | |
| 198 return matcher.tryMatch(stream).then((failure) { | |
| 199 if (failure == null) return matchNext(); | |
| 200 var newFailure = new StringDescription( | |
| 201 'matcher #${_matchers.length - matchers.length} failed'); | |
| 202 if (failure.length != 0) newFailure.add(':\n$failure'); | |
| 203 return newFailure; | |
| 204 }); | |
| 205 } | |
| 206 | |
| 207 return matchNext(); | |
| 208 } | |
| 209 | |
| 210 String toString() => _matchers | |
| 211 .map((matcher) => prefixLines(matcher.toString(), firstPrefix: '* ')) | |
| 212 .join('\n'); | |
| 213 } | |
| 214 | |
| 215 /// See [consumeThrough]. | |
| 216 class _ConsumeThroughMatcher extends StreamMatcher { | |
| 217 final StreamMatcher _matcher; | |
| 218 | |
| 219 _ConsumeThroughMatcher(streamMatcher) | |
| 220 : _matcher = new StreamMatcher.wrap(streamMatcher); | |
| 221 | |
| 222 Future<Description> tryMatch(ScheduledStream stream) { | |
| 223 consumeNext() { | |
| 224 return stream.hasNext.then((hasNext) { | |
| 225 if (!hasNext) return new StringDescription("unexpected end of stream"); | |
| 226 | |
| 227 return _matcher.hasMatch(stream).then((failure) { | |
| 228 if (failure != null) return stream.next().then((_) => consumeNext()); | |
| 229 return _matcher.tryMatch(stream); | |
| 230 }); | |
| 231 }); | |
| 232 } | |
| 233 | |
| 234 return consumeNext(); | |
| 235 } | |
| 236 | |
| 237 String toString() { | |
| 238 var matcherString = _matcher.toString(); | |
| 239 if (matcherString.contains("\n")) { | |
| 240 return 'values followed by:\n' + prefixLines(matcherString, prefix: ' '); | |
| 241 } else { | |
| 242 return 'values followed by $matcherString'; | |
| 243 } | |
| 244 } | |
| 245 } | |
| 246 | |
| 247 /// See [consumeWhile]. | |
| 248 class _ConsumeWhileMatcher extends StreamMatcher { | |
| 249 final StreamMatcher _matcher; | |
| 250 | |
| 251 _ConsumeWhileMatcher(matcher) | |
| 252 : _matcher = new StreamMatcher.wrap(matcher); | |
| 253 | |
| 254 Future<Description> tryMatch(ScheduledStream stream) { | |
| 255 consumeNext() { | |
| 256 return stream.hasNext.then((hasNext) { | |
| 257 if (!hasNext) return new Future.value(); | |
| 258 | |
| 259 return _matcher.hasMatch(stream).then((failure) { | |
| 260 if (failure != null) return new Future.value(); | |
| 261 return _matcher.tryMatch(stream).then((_) => consumeNext()); | |
| 262 }); | |
| 263 }); | |
| 264 } | |
| 265 | |
| 266 return consumeNext(); | |
| 267 } | |
| 268 | |
| 269 String toString() { | |
| 270 var matcherString = _matcher.toString(); | |
| 271 if (matcherString.contains("\n")) { | |
| 272 return 'any number of\n' + prefixLines(matcherString, prefix: ' '); | |
| 273 } else { | |
| 274 return 'any number of $matcherString'; | |
| 275 } | |
| 276 } | |
| 277 } | |
| 278 | |
| 279 /// See [either]. | |
| 280 class _EitherMatcher extends StreamMatcher { | |
| 281 final StreamMatcher _matcher1; | |
| 282 final StreamMatcher _matcher2; | |
| 283 | |
| 284 _EitherMatcher(streamMatcher1, streamMatcher2) | |
| 285 : _matcher1 = new StreamMatcher.wrap(streamMatcher1), | |
| 286 _matcher2 = new StreamMatcher.wrap(streamMatcher2); | |
| 287 | |
| 288 Future<Description> tryMatch(ScheduledStream stream) { | |
| 289 var stream1 = stream.fork(); | |
| 290 var stream2 = stream.fork(); | |
| 291 | |
| 292 return Future.wait([ | |
| 293 _matcher1.tryMatch(stream1).whenComplete(stream1.close), | |
| 294 _matcher2.tryMatch(stream2).whenComplete(stream2.close) | |
| 295 ]).then((failures) { | |
| 296 var failure1 = failures.first; | |
| 297 var failure2 = failures.last; | |
| 298 | |
| 299 // If both matchers matched, use the one that consumed more of the stream. | |
| 300 if (failure1 == null && failure2 == null) { | |
| 301 if (stream1.emittedValues.length >= stream2.emittedValues.length) { | |
| 302 return _matcher1.tryMatch(stream); | |
| 303 } else { | |
| 304 return _matcher2.tryMatch(stream); | |
| 305 } | |
| 306 } else if (failure1 == null) { | |
| 307 return _matcher1.tryMatch(stream); | |
| 308 } else if (failure2 == null) { | |
| 309 return _matcher2.tryMatch(stream); | |
| 310 } else { | |
| 311 return new StringDescription('both\n') | |
| 312 .add(prefixLines(failure1.toString(), prefix: ' ')) | |
| 313 .add('\nand\n') | |
| 314 .add(prefixLines(failure2.toString(), prefix: ' ')) | |
| 315 .toString(); | |
| 316 } | |
| 317 }); | |
| 318 } | |
| 319 | |
| 320 String toString() { | |
| 321 return new StringDescription('either\n') | |
| 322 .add(prefixLines(_matcher1.toString(), prefix: ' ')) | |
| 323 .add('\nor\n') | |
| 324 .add(prefixLines(_matcher2.toString(), prefix: ' ')) | |
| 325 .toString(); | |
| 326 } | |
| 327 } | |
| 328 | |
| 329 /// See [allow]. | |
| 330 class _AllowMatcher extends StreamMatcher { | |
| 331 final StreamMatcher _matcher; | |
| 332 | |
| 333 _AllowMatcher(streamMatcher) | |
| 334 : _matcher = new StreamMatcher.wrap(streamMatcher); | |
| 335 | |
| 336 Future<Description> tryMatch(ScheduledStream stream) { | |
| 337 return _matcher.hasMatch(stream).then((failure) { | |
| 338 if (failure != null) return null; | |
| 339 return _matcher.tryMatch(stream); | |
| 340 }); | |
| 341 } | |
| 342 | |
| 343 String toString() { | |
| 344 return new StringDescription('allow\n') | |
| 345 .add(prefixLines(_matcher.toString())) | |
| 346 .toString(); | |
| 347 } | |
| 348 } | |
| 349 | |
| 350 /// See [never]. | |
| 351 class _NeverMatcher extends StreamMatcher { | |
| 352 final StreamMatcher _matcher; | |
| 353 | |
| 354 _NeverMatcher(streamMatcher) | |
| 355 : _matcher = new StreamMatcher.wrap(streamMatcher); | |
| 356 | |
| 357 Future<Description> tryMatch(ScheduledStream stream) { | |
| 358 consumeNext() { | |
| 359 return stream.hasNext.then((hasNext) { | |
| 360 if (!hasNext) return new Future.value(); | |
| 361 | |
| 362 return _matcher.hasMatch(stream).then((failure) { | |
| 363 if (failure != null) { | |
| 364 return stream.next().then((_) => consumeNext()); | |
| 365 } | |
| 366 | |
| 367 return new StringDescription("matched\n") | |
| 368 .add(prefixLines(_matcher.toString(), prefix: ' ')); | |
| 369 }); | |
| 370 }); | |
| 371 } | |
| 372 | |
| 373 return consumeNext(); | |
| 374 } | |
| 375 | |
| 376 String toString() => | |
| 377 'never\n${prefixLines(_matcher.toString(), prefix: ' ')}'; | |
| 378 } | |
| 379 | |
| 380 /// See [isDone]. | |
| 381 class _IsDoneMatcher extends StreamMatcher { | |
| 382 _IsDoneMatcher(); | |
| 383 | |
| 384 Future<Description> tryMatch(ScheduledStream stream) { | |
| 385 return stream.hasNext.then((hasNext) { | |
| 386 if (!hasNext) return null; | |
| 387 return new StringDescription("stream wasn't finished"); | |
| 388 }); | |
| 389 } | |
| 390 | |
| 391 String toString() => 'is done'; | |
| 392 } | |
| 393 | |
| 394 /// Returns a [Future] that completes to the next value emitted by [stream] | |
| 395 /// without actually consuming that value. | |
| 396 Future _peek(ScheduledStream stream) { | |
| 397 var fork = stream.fork(); | |
| 398 return fork.next().whenComplete(fork.close); | |
| 399 } | |
| OLD | NEW |