Chromium Code Reviews| 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<Either<T, Pair<dynamic, StackTrace>>>(); | |
|
Bob Nystrom
2013/12/21 00:11:34
Instead of Either<Pair>, how about a concrete type
nweiz
2014/01/07 03:16:39
Good idea. Done.
| |
| 47 | |
| 48 /// All values emitted by this stream so far. | |
|
Bob Nystrom
2013/12/21 00:11:34
This is confusing. "Emitted" implies a push model,
nweiz
2014/01/07 03:16:39
"Consumed" is also weird because it sounds like it
| |
| 49 /// | |
| 50 /// This does not include values emitted by the underlying stream but not yet | |
| 51 /// emitted through [next]. | |
| 52 List<T> get values => new UnmodifiableListView(_values); | |
| 53 final _values = 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 { | |
|
Bob Nystrom
2013/12/21 00:11:34
The distinction in names here is a bit ambiguous.
nweiz
2014/01/07 03:16:39
Done.
| |
| 59 var list = new List<T>.from(_values); | |
| 60 list.addAll(_pendingValues.where((value) => value.isFirst) | |
| 61 .map((value) => value.first)); | |
| 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 bool _nextPending = false; | |
|
Bob Nystrom
2013/12/21 00:11:34
How about _isNextPending? "_nextPending" sound to
nweiz
2014/01/07 03:16:39
Done.
| |
| 70 | |
| 71 /// Creates a new scheduled stream wrapping [stream]. | |
| 72 ScheduledStream(Stream<T> stream) | |
| 73 : _stream = stream.asBroadcastStream() { | |
| 74 _subscription = _stream.listen((value) { | |
| 75 if (_hasNextCompleter != null) { | |
| 76 _hasNextCompleter.complete(true); | |
| 77 _hasNextCompleter = null; | |
| 78 } | |
| 79 | |
| 80 if (_nextCompleter != null) { | |
| 81 _nextCompleter.complete(value); | |
| 82 _values.add(value); | |
| 83 _nextCompleter = null; | |
| 84 _nextPending = false; | |
| 85 } else { | |
| 86 _pendingValues.add(new Either.withFirst(value)); | |
| 87 } | |
| 88 }, onError: (error, stackTrace) { | |
| 89 if (_hasNextCompleter != null) { | |
| 90 _hasNextCompleter.completeError(error, stackTrace); | |
| 91 _hasNextCompleter = null; | |
| 92 } | |
| 93 | |
| 94 if (_nextCompleter != null) { | |
| 95 _nextCompleter.completeError(error, stackTrace); | |
| 96 _nextCompleter = null; | |
| 97 } else { | |
| 98 _pendingValues.add(new Either.withSecond(new Pair(error, stackTrace))); | |
| 99 } | |
| 100 }, onDone: _onDone); | |
| 101 } | |
| 102 | |
| 103 /// Enqueue an expectation that [streamMatcher] will match the value(s) | |
| 104 /// emitted by the stream at this point in the schedule. | |
| 105 /// | |
| 106 /// If [streamMatcher] is a [StreamMatcher], it will match the stream as a | |
| 107 /// whole. If it's a [Matcher] or another object, it will match the next value | |
| 108 /// emitted by the stream (as though it were a [nextValue] matcher). | |
| 109 /// | |
| 110 /// This call is scheduled; the expectation won't be added until the schedule | |
| 111 /// reaches this point, and the schedule won't continue until the matcher has | |
| 112 /// matched the stream. | |
| 113 void expect(streamMatcher) { | |
| 114 streamMatcher = new StreamMatcher.wrap(streamMatcher); | |
| 115 var description = 'stream emits $streamMatcher'; | |
| 116 schedule(() { | |
| 117 return streamMatcher.tryMatch(stream).then((description) { | |
| 118 if (description == null) return; | |
| 119 | |
| 120 var expected = prefixLines(streamMatcher.toString(), | |
| 121 firstPrefix: 'Expected: ', | |
| 122 prefix: ' | '); | |
| 123 | |
| 124 var actual = prefixLines(stream.allValues.map((value) { | |
| 125 return prefixLines(value.toString(), firstPrefix: '* '); | |
| 126 }).join('\n'), | |
| 127 firstPrefix: ' Emitted: ', | |
| 128 prefix: ' '); | |
| 129 | |
| 130 var which = ''; | |
| 131 if (description.length > 0) { | |
| 132 which = '\n' + prefixLines(description.toString(), | |
| 133 firstPrefix: ' Which: ', | |
| 134 prefix: ' | '); | |
| 135 } | |
| 136 | |
| 137 fail("$expected\n$actual$which"); | |
| 138 }); | |
| 139 }, description); | |
| 140 } | |
| 141 | |
| 142 /// Returns a Future that completes to the next value emitted by this stream. | |
| 143 /// | |
| 144 /// It's a [StateError] to call [next] when another call's Future has not yet | |
| 145 /// completed, or when the stream has no more values. The latter can be | |
| 146 /// checked using [hasNext]. | |
| 147 Future<T> next() { | |
| 148 if (_nextPending) { | |
| 149 return new Future.error( | |
| 150 new StateError("There's already a pending call to " | |
| 151 "ScheduledStream.next."), | |
| 152 new Chain.current()); | |
| 153 } | |
| 154 | |
| 155 if (_pendingValues.isNotEmpty) { | |
| 156 _nextPending = true; | |
| 157 | |
| 158 return _pendingValues.removeFirst().match((value) { | |
| 159 _values.add(value); | |
| 160 return new Future.value(value); | |
| 161 }, (pair) => Chain.track(new Future.error(pair.first, pair.last))) | |
| 162 .whenComplete(() { | |
|
Bob Nystrom
2013/12/21 00:11:34
It took me a long time to parse this and realize t
nweiz
2014/01/07 03:16:39
I changed this as part of replacing Either with Fa
| |
| 163 _nextPending = false; | |
| 164 }); | |
| 165 } else if (_isDone) { | |
| 166 return new Future.error( | |
| 167 new StateError("ScheduledStream has no more elements."), | |
| 168 new Chain.current()); | |
| 169 } | |
| 170 | |
| 171 _nextPending = true; | |
| 172 _nextCompleter = new Completer(); | |
| 173 return _nextCompleter.future; | |
| 174 } | |
| 175 | |
| 176 /// Returns a Future that completes to a boolean indicating whether the stream | |
| 177 /// has additional values or not. | |
| 178 Future<bool> get hasNext { | |
| 179 if (_hasNextCompleter != null) return _hasNextCompleter.future; | |
| 180 | |
| 181 if (_pendingValues.isNotEmpty) { | |
| 182 return _pendingValues.first.match( | |
| 183 (value) => new Future.value(true), | |
| 184 (pair) => Chain.track(new Future.error(pair.first, pair.last))); | |
| 185 } else if (_isDone) { | |
| 186 return new Future.value(false); | |
| 187 } | |
| 188 | |
| 189 _hasNextCompleter = new Completer(); | |
| 190 return _hasNextCompleter.future; | |
| 191 } | |
| 192 | |
| 193 /// Returns a fork of this stream. | |
| 194 /// | |
| 195 /// The fork begins at the same point [this] is at. Values can be read from it | |
| 196 /// without consuming values in [this]. If [this] is closed, the fork will be | |
| 197 /// closed at whatever point it's currently at. | |
| 198 ScheduledStream<T> fork() { | |
| 199 var controller = new StreamController<T>(); | |
| 200 for (var value in _pendingValues) { | |
| 201 value.match(controller.add, | |
| 202 (pair) => controller.addError(pair.first, pair.last)); | |
| 203 } | |
| 204 | |
| 205 if (_isDone) { | |
| 206 controller.close(); | |
| 207 } else { | |
| 208 _stream.pipe(controller); | |
| 209 } | |
| 210 | |
| 211 var fork = new ScheduledStream<T>(controller.stream); | |
| 212 _forks.add(fork); | |
| 213 return fork; | |
| 214 } | |
| 215 | |
| 216 /// Closes this stream. | |
| 217 /// | |
| 218 /// This cancels the subscription to the underlying stream and acts as though | |
| 219 /// [this] was closed immediately after the current position, regardless of | |
| 220 /// whether the underlying stream has emitted additional events. | |
| 221 void close() { | |
| 222 _subscription.cancel(); | |
| 223 _pendingValues.clear(); | |
| 224 | |
| 225 for (var fork in _forks) { | |
| 226 fork.close(); | |
| 227 } | |
|
Bob Nystrom
2013/12/21 00:11:34
Just to be nice to the GC, may as well clear _fork
nweiz
2014/01/07 03:16:39
Done.
| |
| 228 | |
| 229 if (!_isDone) _onDone(); | |
| 230 } | |
| 231 | |
| 232 /// Handles a "done" event from the underlying stream, as well as [this] being | |
| 233 /// closed. | |
| 234 void _onDone() { | |
| 235 if (_hasNextCompleter != null) { | |
| 236 _hasNextCompleter.complete(false); | |
| 237 _hasNextCompleter = null; | |
| 238 } | |
| 239 | |
| 240 if (_nextCompleter != null) { | |
| 241 _nextCompleter.completeError( | |
| 242 new StateError("ScheduledStream has no more elements."), | |
| 243 new Chain.current()); | |
| 244 _nextCompleter = null; | |
| 245 } | |
|
Bob Nystrom
2013/12/21 00:11:34
Probably want to clear _nextPending here too.
nweiz
2014/01/07 03:16:39
Done.
| |
| 246 | |
| 247 _isDone = true; | |
| 248 } | |
| 249 } | |
| OLD | NEW |