Line data Source code
1 : // Copyright (c) 2017, 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 : import 'package:async/async.dart'; 6 : import 'package:matcher/matcher.dart'; 7 : import 'package:test_api/hooks.dart'; 8 : 9 : import 'async_matcher.dart'; 10 : import 'util/pretty_print.dart'; 11 : 12 : /// A matcher that matches events from [Stream]s or [StreamQueue]s. 13 : /// 14 : /// Stream matchers are designed to make it straightforward to create complex 15 : /// expectations for streams, and to interleave expectations with the rest of a 16 : /// test. They can be used on a [Stream] to match all events it emits: 17 : /// 18 : /// ```dart 19 : /// expect(stream, emitsInOrder([ 20 : /// // Values match individual events. 21 : /// "Ready.", 22 : /// 23 : /// // Matchers also run against individual events. 24 : /// startsWith("Loading took"), 25 : /// 26 : /// // Stream matchers can be nested. This asserts that one of two events are 27 : /// // emitted after the "Loading took" line. 28 : /// emitsAnyOf(["Succeeded!", "Failed!"]), 29 : /// 30 : /// // By default, more events are allowed after the matcher finishes 31 : /// // matching. This asserts instead that the stream emits a done event and 32 : /// // nothing else. 33 : /// emitsDone 34 : /// ])); 35 : /// ``` 36 : /// 37 : /// It can also match a [StreamQueue], in which case it consumes the matched 38 : /// events. The call to [expect] returns a [Future] that completes when the 39 : /// matcher is done matching. You can `await` this to consume different events 40 : /// at different times: 41 : /// 42 : /// ```dart 43 : /// var stdout = StreamQueue(stdoutLineStream); 44 : /// 45 : /// // Ignore lines from the process until it's about to emit the URL. 46 : /// await expectLater(stdout, emitsThrough('WebSocket URL:')); 47 : /// 48 : /// // Parse the next line as a URL. 49 : /// var url = Uri.parse(await stdout.next); 50 : /// expect(url.host, equals('localhost')); 51 : /// 52 : /// // You can match against the same StreamQueue multiple times. 53 : /// await expectLater(stdout, emits('Waiting for connection...')); 54 : /// ``` 55 : /// 56 : /// Users can call [new StreamMatcher] to create custom matchers. 57 : abstract class StreamMatcher extends Matcher { 58 : /// The description of this matcher. 59 : /// 60 : /// This is in the subjunctive mood, which means it can be used after the word 61 : /// "should". For example, it might be "emit the right events". 62 : String get description; 63 : 64 : /// Creates a new [StreamMatcher] described by [description] that matches 65 : /// events with [matchQueue]. 66 : /// 67 : /// The [matchQueue] callback is used to implement [StreamMatcher.matchQueue], 68 : /// and should follow all the guarantees of that method. In particular: 69 : /// 70 : /// * If it matches successfully, it should return `null` and possibly consume 71 : /// events. 72 : /// * If it fails to match, consume no events and return a description of the 73 : /// failure. 74 : /// * The description should be in past tense. 75 : /// * The description should be gramatically valid when used after "the 76 : /// stream"—"emitted the wrong events", for example. 77 : /// 78 : /// The [matchQueue] callback may return the empty string to indicate a 79 : /// failure if it has no information to add beyond the description of the 80 : /// failure and the events actually emitted by the stream. 81 : /// 82 : /// The [description] should be in the subjunctive mood. This means that it 83 : /// should be grammatically valid when used after the word "should". For 84 : /// example, it might be "emit the right events". 85 : factory StreamMatcher(Future<String?> Function(StreamQueue) matchQueue, 86 : String description) = _StreamMatcher; 87 : 88 : /// Tries to match events emitted by [queue]. 89 : /// 90 : /// If this matches successfully, it consumes the matching events from [queue] 91 : /// and returns `null`. 92 : /// 93 : /// If this fails to match, it doesn't consume any events and returns a 94 : /// description of the failure. This description is in the past tense, and 95 : /// could grammatically be used after "the stream". For example, it might 96 : /// return "emitted the wrong events". 97 : /// 98 : /// The description string may also be empty, which indicates that the 99 : /// matcher's description and the events actually emitted by the stream are 100 : /// enough to understand the failure. 101 : /// 102 : /// If the queue emits an error, that error is re-thrown unless otherwise 103 : /// indicated by the matcher. 104 : Future<String?> matchQueue(StreamQueue queue); 105 : } 106 : 107 : /// A concrete implementation of [StreamMatcher]. 108 : /// 109 : /// This is separate from the original type to hide the private [AsyncMatcher] 110 : /// interface. 111 : class _StreamMatcher extends AsyncMatcher implements StreamMatcher { 112 : @override 113 : final String description; 114 : 115 : /// The callback used to implement [matchQueue]. 116 : final Future<String?> Function(StreamQueue) _matchQueue; 117 : 118 0 : _StreamMatcher(this._matchQueue, this.description); 119 : 120 0 : @override 121 0 : Future<String?> matchQueue(StreamQueue queue) => _matchQueue(queue); 122 : 123 0 : @override 124 : dynamic /*FutureOr<String>*/ matchAsync(item) { 125 : StreamQueue queue; 126 : var shouldCancelQueue = false; 127 0 : if (item is StreamQueue) { 128 : queue = item; 129 0 : } else if (item is Stream) { 130 0 : queue = StreamQueue(item); 131 : shouldCancelQueue = true; 132 : } else { 133 : return 'was not a Stream or a StreamQueue'; 134 : } 135 : 136 : // Avoid async/await in the outer method so that we synchronously error out 137 : // for an invalid argument type. 138 0 : var transaction = queue.startTransaction(); 139 0 : var copy = transaction.newQueue(); 140 0 : return matchQueue(copy).then((result) async { 141 : // Accept the transaction if the result is null, indicating that the match 142 : // succeeded. 143 : if (result == null) { 144 0 : transaction.commit(copy); 145 : return null; 146 : } 147 : 148 : // Get a list of events emitted by the stream so we can emit them as part 149 : // of the error message. 150 0 : var replay = transaction.newQueue(); 151 0 : var events = <Result?>[]; 152 : var subscription = Result.captureStreamTransformer 153 0 : .bind(replay.rest.cast()) 154 0 : .listen(events.add, onDone: () => events.add(null)); 155 : 156 : // Wait on a timer tick so all buffered events are emitted. 157 0 : await Future.delayed(Duration.zero); 158 0 : _unawaited(subscription.cancel()); 159 : 160 0 : var eventsString = events.map((event) { 161 : if (event == null) { 162 : return 'x Stream closed.'; 163 0 : } else if (event.isValue) { 164 0 : return addBullet(event.asValue!.value.toString()); 165 : } else { 166 0 : var error = event.asError!; 167 0 : var chain = TestHandle.current.formatStackTrace(error.stackTrace); 168 0 : var text = '${error.error}\n$chain'; 169 0 : return indent(text, first: '! '); 170 : } 171 0 : }).join('\n'); 172 0 : if (eventsString.isEmpty) eventsString = 'no events'; 173 : 174 0 : transaction.reject(); 175 : 176 0 : var buffer = StringBuffer(); 177 0 : buffer.writeln(indent(eventsString, first: 'emitted ')); 178 0 : if (result.isNotEmpty) buffer.writeln(indent(result, first: ' which ')); 179 0 : return buffer.toString().trimRight(); 180 0 : }, onError: (Object error) { 181 0 : transaction.reject(); 182 : throw error; 183 0 : }).then((result) { 184 0 : if (shouldCancelQueue) queue.cancel(); 185 : return result; 186 : }); 187 : } 188 : 189 0 : @override 190 : Description describe(Description description) => 191 0 : description.add('should ').add(this.description); 192 : } 193 : 194 0 : void _unawaited(Future<void> f) {}