emitsAnyOf function

StreamMatcher emitsAnyOf(
  1. Iterable matchers
)

Returns a StreamMatcher that matches the stream if at least one of matchers matches.

If multiple matchers match the stream, this chooses the matcher that consumes as many events as possible.

If any matchers match the stream, no errors from other matchers are thrown. If no matchers match and multiple matchers threw errors, the first error is re-thrown.

Implementation

StreamMatcher emitsAnyOf(Iterable matchers) {
  var streamMatchers = matchers.map(emits).toList();
  if (streamMatchers.isEmpty) {
    throw ArgumentError('matcher may not be empty');
  }

  if (streamMatchers.length == 1) return streamMatchers.first;
  var description = 'do one of the following:\n'
      '${bullet(streamMatchers.map((matcher) => matcher.description))}';

  return StreamMatcher((queue) async {
    var transaction = queue.startTransaction();

    // Allocate the failures list ahead of time so that its order matches the
    // order of [matchers], and thus the order the matchers will be listed in
    // the description.
    var failures = List<String?>.filled(matchers.length, null);

    // The first error thrown. If no matchers match and this exists, we rethrow
    // it.
    Object? firstError;
    StackTrace? firstStackTrace;

    var futures = <Future>[];
    StreamQueue? consumedMost;
    for (var i = 0; i < matchers.length; i++) {
      futures.add(() async {
        var copy = transaction.newQueue();

        String? result;
        try {
          result = await streamMatchers[i].matchQueue(copy);
        } catch (error, stackTrace) {
          if (firstError == null) {
            firstError = error;
            firstStackTrace = stackTrace;
          }
          return;
        }

        if (result != null) {
          failures[i] = result;
        } else if (consumedMost == null ||
            consumedMost!.eventsDispatched < copy.eventsDispatched) {
          consumedMost = copy;
        }
      }());
    }

    await Future.wait(futures);

    if (consumedMost == null) {
      transaction.reject();
      if (firstError != null) {
        await Future.error(firstError!, firstStackTrace);
      }

      var failureMessages = <String>[];
      for (var i = 0; i < matchers.length; i++) {
        var message = 'failed to ${streamMatchers[i].description}';
        if (failures[i]!.isNotEmpty) {
          message += message.contains('\n') ? '\n' : ' ';
          message += 'because it ${failures[i]}';
        }

        failureMessages.add(message);
      }

      return 'failed all options:\n${bullet(failureMessages)}';
    } else {
      transaction.commit(consumedMost!);
      return null;
    }
  }, description);
}