| 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.scheduled_stream; |
| 6 |
| 7 import 'dart:async'; |
| 8 import 'dart:collection'; |
| 9 |
| 10 import 'package:stack_trace/stack_trace.dart'; |
| 11 |
| 12 import 'scheduled_test.dart'; |
| 13 import 'src/stream_matcher.dart'; |
| 14 import 'src/utils.dart'; |
| 15 |
| 16 export 'src/stream_matcher.dart'; |
| 17 |
| 18 /// A wrapper for streams that supports a pull-based model of retrieving values |
| 19 /// as well as a set of [StreamMatcher]s for testing the values emitted by the |
| 20 /// stream. |
| 21 /// |
| 22 /// The only method on [ScheduledStream] that's actually scheduled is [expect], |
| 23 /// which is the method that users testing streaming code are most likely to |
| 24 /// want to use. |
| 25 class ScheduledStream<T> { |
| 26 /// The underlying stream. |
| 27 final Stream<T> _stream; |
| 28 |
| 29 /// The subscription to [_stream]. |
| 30 StreamSubscription<T> _subscription; |
| 31 |
| 32 /// The completer for emitting a value requested by [next]. |
| 33 /// |
| 34 /// If this is non-null, [_pendingValues] will always be empty, since any |
| 35 /// value coming in will be passed to this completer. |
| 36 Completer<T> _nextCompleter; |
| 37 |
| 38 /// The completer for emitting a value requested by [hasNext]. |
| 39 Completer<bool> _hasNextCompleter; |
| 40 |
| 41 /// The set of all streams forked from this one. |
| 42 final _forks = new Set<ScheduledStream<T>>(); |
| 43 |
| 44 /// The queue of values emitted by [_stream] but not yet emitted through |
| 45 /// [next]. |
| 46 final _pendingValues = new Queue<Fallible<T>>(); |
| 47 |
| 48 /// All values emitted by this stream so far. |
| 49 /// |
| 50 /// This does not include values emitted by the underlying stream but not yet |
| 51 /// emitted through [next]. |
| 52 List<T> get emittedValues => new UnmodifiableListView(_emittedValues); |
| 53 final _emittedValues = new List<T>(); |
| 54 |
| 55 /// All values emitted by the underlying stream. |
| 56 /// |
| 57 /// This is intended primarily for providing debugging information. |
| 58 List<T> get allValues { |
| 59 var list = new List<T>.from(_emittedValues); |
| 60 list.addAll(_pendingValues.where((value) => value.hasValue) |
| 61 .map((value) => value.value)); |
| 62 return new UnmodifiableListView(list); |
| 63 } |
| 64 |
| 65 /// Whether the wrapped stream has been closed. |
| 66 bool _isDone = false; |
| 67 |
| 68 /// Whether [next] has been called but has not yet returned. |
| 69 /// |
| 70 /// This is distinct from `_nextCompleter != null` when [next] is called while |
| 71 /// there are pending values available, until the future it returns completes. |
| 72 bool _isNextPending = false; |
| 73 |
| 74 /// Creates a new scheduled stream wrapping [stream]. |
| 75 ScheduledStream(Stream<T> stream) |
| 76 : _stream = stream.asBroadcastStream() { |
| 77 _subscription = _stream.listen((value) { |
| 78 if (_hasNextCompleter != null) { |
| 79 _hasNextCompleter.complete(true); |
| 80 _hasNextCompleter = null; |
| 81 } |
| 82 |
| 83 if (_nextCompleter != null) { |
| 84 _nextCompleter.complete(value); |
| 85 _emittedValues.add(value); |
| 86 _nextCompleter = null; |
| 87 _isNextPending = false; |
| 88 } else { |
| 89 _pendingValues.add(new Fallible.withValue(value)); |
| 90 } |
| 91 }, onError: (error, stackTrace) { |
| 92 if (_hasNextCompleter != null) { |
| 93 _hasNextCompleter.completeError(error, stackTrace); |
| 94 _hasNextCompleter = null; |
| 95 } |
| 96 |
| 97 if (_nextCompleter != null) { |
| 98 _nextCompleter.completeError(error, stackTrace); |
| 99 _nextCompleter = null; |
| 100 } else { |
| 101 _pendingValues.add(new Fallible.withError(error, stackTrace)); |
| 102 } |
| 103 }, onDone: _onDone); |
| 104 } |
| 105 |
| 106 /// Enqueue an expectation that [streamMatcher] will match the value(s) |
| 107 /// emitted by the stream at this point in the schedule. |
| 108 /// |
| 109 /// If [streamMatcher] is a [StreamMatcher], it will match the stream as a |
| 110 /// whole. If it's a [Matcher] or another object, it will match the next value |
| 111 /// emitted by the stream (as though it were a [nextValue] matcher). |
| 112 /// |
| 113 /// This call is scheduled; the expectation won't be added until the schedule |
| 114 /// reaches this point, and the schedule won't continue until the matcher has |
| 115 /// matched the stream. |
| 116 void expect(streamMatcher) { |
| 117 streamMatcher = new StreamMatcher.wrap(streamMatcher); |
| 118 var description = 'stream emits $streamMatcher'; |
| 119 schedule(() { |
| 120 return streamMatcher.tryMatch(this).then((description) { |
| 121 if (description == null) return; |
| 122 |
| 123 var expected = prefixLines(streamMatcher.toString(), |
| 124 firstPrefix: 'Expected: ', |
| 125 prefix: ' | '); |
| 126 |
| 127 var actual = prefixLines(allValues.map((value) { |
| 128 return prefixLines(value.toString(), firstPrefix: '* '); |
| 129 }).join('\n'), |
| 130 firstPrefix: ' Emitted: ', |
| 131 prefix: ' '); |
| 132 |
| 133 var which = ''; |
| 134 if (description.length > 0) { |
| 135 which = '\n' + prefixLines(description.toString(), |
| 136 firstPrefix: ' Which: ', |
| 137 prefix: ' | '); |
| 138 } |
| 139 |
| 140 fail("$expected\n$actual$which"); |
| 141 }); |
| 142 }, description); |
| 143 } |
| 144 |
| 145 /// Returns a Future that completes to the next value emitted by this stream. |
| 146 /// |
| 147 /// It's a [StateError] to call [next] when another call's Future has not yet |
| 148 /// completed, or when the stream has no more values. The latter can be |
| 149 /// checked using [hasNext]. |
| 150 Future<T> next() { |
| 151 if (_isNextPending) { |
| 152 return new Future.error( |
| 153 new StateError("There's already a pending call to " |
| 154 "ScheduledStream.next."), |
| 155 new Chain.current()); |
| 156 } |
| 157 |
| 158 if (_pendingValues.isNotEmpty) { |
| 159 _isNextPending = true; |
| 160 |
| 161 var valueOrError = _pendingValues.removeFirst(); |
| 162 if (valueOrError.hasValue) { |
| 163 _emittedValues.add(valueOrError.value); |
| 164 } |
| 165 return valueOrError.toFuture().whenComplete(() { |
| 166 _isNextPending = false; |
| 167 }); |
| 168 } else if (_isDone) { |
| 169 return new Future.error( |
| 170 new StateError("ScheduledStream has no more elements."), |
| 171 new Chain.current()); |
| 172 } |
| 173 |
| 174 _isNextPending = true; |
| 175 _nextCompleter = new Completer(); |
| 176 return _nextCompleter.future; |
| 177 } |
| 178 |
| 179 /// Returns a Future that completes to a boolean indicating whether the stream |
| 180 /// has additional values or not. |
| 181 Future<bool> get hasNext { |
| 182 if (_hasNextCompleter != null) return _hasNextCompleter.future; |
| 183 |
| 184 if (_pendingValues.isNotEmpty) { |
| 185 return _pendingValues.first.toFuture().then((_) => true); |
| 186 } else if (_isDone) { |
| 187 return new Future.value(false); |
| 188 } |
| 189 |
| 190 _hasNextCompleter = new Completer(); |
| 191 return _hasNextCompleter.future; |
| 192 } |
| 193 |
| 194 /// Returns a fork of this stream. |
| 195 /// |
| 196 /// The fork begins at the same point [this] is at. Values can be read from it |
| 197 /// without consuming values in [this]. If [this] is closed, the fork will be |
| 198 /// closed at whatever point it's currently at. |
| 199 ScheduledStream<T> fork() { |
| 200 var controller = new StreamController<T>(); |
| 201 for (var valueOrError in _pendingValues) { |
| 202 if (valueOrError.hasValue) { |
| 203 controller.add(valueOrError.value); |
| 204 } else { |
| 205 controller.addError(valueOrError.error, valueOrError.stackTrace); |
| 206 } |
| 207 } |
| 208 |
| 209 if (_isDone) { |
| 210 controller.close(); |
| 211 } else { |
| 212 _stream.pipe(controller); |
| 213 } |
| 214 |
| 215 var fork = new ScheduledStream<T>(controller.stream); |
| 216 _forks.add(fork); |
| 217 return fork; |
| 218 } |
| 219 |
| 220 /// Closes this stream. |
| 221 /// |
| 222 /// This cancels the subscription to the underlying stream and acts as though |
| 223 /// [this] was closed immediately after the current position, regardless of |
| 224 /// whether the underlying stream has emitted additional events. |
| 225 void close() { |
| 226 _subscription.cancel(); |
| 227 _pendingValues.clear(); |
| 228 |
| 229 for (var fork in _forks) { |
| 230 fork.close(); |
| 231 } |
| 232 _forks.clear(); |
| 233 |
| 234 if (!_isDone) _onDone(); |
| 235 } |
| 236 |
| 237 /// Handles a "done" event from the underlying stream, as well as [this] being |
| 238 /// closed. |
| 239 void _onDone() { |
| 240 if (_hasNextCompleter != null) { |
| 241 _hasNextCompleter.complete(false); |
| 242 _hasNextCompleter = null; |
| 243 } |
| 244 |
| 245 if (_nextCompleter != null) { |
| 246 _nextCompleter.completeError( |
| 247 new StateError("ScheduledStream has no more elements."), |
| 248 new Chain.current()); |
| 249 _nextCompleter = null; |
| 250 _isNextPending = false; |
| 251 } |
| 252 |
| 253 _isDone = true; |
| 254 } |
| 255 } |
| OLD | NEW |