| Index: mojo/public/dart/third_party/test/lib/src/utils.dart
|
| diff --git a/mojo/public/dart/third_party/test/lib/src/utils.dart b/mojo/public/dart/third_party/test/lib/src/utils.dart
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..7793d3084deaa94f9493ecd1f7b64039ea32b181
|
| --- /dev/null
|
| +++ b/mojo/public/dart/third_party/test/lib/src/utils.dart
|
| @@ -0,0 +1,405 @@
|
| +// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
|
| +// for details. All rights reserved. Use of this source code is governed by a
|
| +// BSD-style license that can be found in the LICENSE file.
|
| +
|
| +library test.utils;
|
| +
|
| +import 'dart:async';
|
| +import 'dart:convert';
|
| +import 'dart:math' as math;
|
| +
|
| +import 'package:crypto/crypto.dart';
|
| +import 'package:path/path.dart' as p;
|
| +import 'package:shelf/shelf.dart' as shelf;
|
| +import 'package:stack_trace/stack_trace.dart';
|
| +
|
| +import 'backend/operating_system.dart';
|
| +import 'util/cancelable_future.dart';
|
| +import 'util/path_handler.dart';
|
| +import 'util/stream_queue.dart';
|
| +
|
| +/// The maximum console line length.
|
| +const _lineLength = 100;
|
| +
|
| +/// A typedef for a possibly-asynchronous function.
|
| +///
|
| +/// The return type should only ever by [Future] or void.
|
| +typedef AsyncFunction();
|
| +
|
| +/// A typedef for a zero-argument callback function.
|
| +typedef void Callback();
|
| +
|
| +/// A converter that decodes bytes using UTF-8 and splits them on newlines.
|
| +final lineSplitter = UTF8.decoder.fuse(const LineSplitter());
|
| +
|
| +/// A regular expression to match the exception prefix that some exceptions'
|
| +/// [Object.toString] values contain.
|
| +final _exceptionPrefix = new RegExp(r'^([A-Z][a-zA-Z]*)?(Exception|Error): ');
|
| +
|
| +/// Directories that are specific to OS X.
|
| +///
|
| +/// This is used to try to distinguish OS X and Linux in [currentOSGuess].
|
| +final _macOSDirectories = new Set<String>.from([
|
| + "/Applications",
|
| + "/Library",
|
| + "/Network",
|
| + "/System",
|
| + "/Users"
|
| +]);
|
| +
|
| +/// Returns the best guess for the current operating system without using
|
| +/// `dart:io`.
|
| +///
|
| +/// This is useful for running test files directly and skipping tests as
|
| +/// appropriate. The only OS-specific information we have is the current path,
|
| +/// which we try to use to figure out the OS.
|
| +final OperatingSystem currentOSGuess = (() {
|
| + if (p.style == p.Style.url) return OperatingSystem.none;
|
| + if (p.style == p.Style.windows) return OperatingSystem.windows;
|
| + if (_macOSDirectories.any(p.current.startsWith)) return OperatingSystem.macOS;
|
| + return OperatingSystem.linux;
|
| +})();
|
| +
|
| +/// A pair of values.
|
| +class Pair<E, F> {
|
| + E first;
|
| + F last;
|
| +
|
| + Pair(this.first, this.last);
|
| +
|
| + String toString() => '($first, $last)';
|
| +
|
| + bool operator==(other) {
|
| + if (other is! Pair) return false;
|
| + return other.first == first && other.last == last;
|
| + }
|
| +
|
| + int get hashCode => first.hashCode ^ last.hashCode;
|
| +}
|
| +
|
| +/// Get a string description of an exception.
|
| +///
|
| +/// Many exceptions include the exception class name at the beginning of their
|
| +/// [toString], so we remove that if it exists.
|
| +String getErrorMessage(error) =>
|
| + error.toString().replaceFirst(_exceptionPrefix, '');
|
| +
|
| +/// Indent each line in [str] by two spaces.
|
| +String indent(String str) =>
|
| + str.replaceAll(new RegExp("^", multiLine: true), " ");
|
| +
|
| +/// Returns a sentence fragment listing the elements of [iter].
|
| +///
|
| +/// This converts each element of [iter] to a string and separates them with
|
| +/// commas and/or "and" where appropriate.
|
| +String toSentence(Iterable iter) {
|
| + if (iter.length == 1) return iter.first.toString();
|
| + var result = iter.take(iter.length - 1).join(", ");
|
| + if (iter.length > 2) result += ",";
|
| + return "$result and ${iter.last}";
|
| +}
|
| +
|
| +/// Wraps [text] so that it fits within [lineLength], which defaults to 100
|
| +/// characters.
|
| +///
|
| +/// This preserves existing newlines and doesn't consider terminal color escapes
|
| +/// part of a word's length.
|
| +String wordWrap(String text, {int lineLength}) {
|
| + if (lineLength == null) lineLength = _lineLength;
|
| + return text.split("\n").map((originalLine) {
|
| + var buffer = new StringBuffer();
|
| + var lengthSoFar = 0;
|
| + for (var word in originalLine.split(" ")) {
|
| + var wordLength = withoutColors(word).length;
|
| + if (wordLength > lineLength) {
|
| + if (lengthSoFar != 0) buffer.writeln();
|
| + buffer.writeln(word);
|
| + } else if (lengthSoFar == 0) {
|
| + buffer.write(word);
|
| + lengthSoFar = wordLength;
|
| + } else if (lengthSoFar + 1 + wordLength > lineLength) {
|
| + buffer.writeln();
|
| + buffer.write(word);
|
| + lengthSoFar = wordLength;
|
| + } else {
|
| + buffer.write(" $word");
|
| + lengthSoFar += 1 + wordLength;
|
| + }
|
| + }
|
| + return buffer.toString();
|
| + }).join("\n");
|
| +}
|
| +
|
| +/// A regular expression matching terminal color codes.
|
| +final _colorCode = new RegExp('\u001b\\[[0-9;]+m');
|
| +
|
| +/// Returns [str] without any color codes.
|
| +String withoutColors(String str) => str.replaceAll(_colorCode, '');
|
| +
|
| +/// A regular expression matching the path to a temporary file used to start an
|
| +/// isolate.
|
| +///
|
| +/// These paths aren't relevant and are removed from stack traces.
|
| +final _isolatePath =
|
| + new RegExp(r"/test_[A-Za-z0-9]{6}/runInIsolate\.dart$");
|
| +
|
| +/// Returns [stackTrace] converted to a [Chain] with all irrelevant frames
|
| +/// folded together.
|
| +///
|
| +/// If [verbose] is `true`, returns the chain for [stackTrace] unmodified.
|
| +Chain terseChain(StackTrace stackTrace, {bool verbose: false}) {
|
| + if (verbose) return new Chain.forTrace(stackTrace);
|
| + return new Chain.forTrace(stackTrace).foldFrames((frame) {
|
| + if (frame.package == 'test') return true;
|
| +
|
| + // Filter out frames from our isolate bootstrap as well.
|
| + if (frame.uri.scheme != 'file') return false;
|
| + return frame.uri.path.contains(_isolatePath);
|
| + }, terse: true);
|
| +}
|
| +
|
| +/// Flattens nested [Iterable]s inside an [Iterable] into a single [List]
|
| +/// containing only non-[Iterable] elements.
|
| +List flatten(Iterable nested) {
|
| + var result = [];
|
| + helper(iter) {
|
| + for (var element in iter) {
|
| + if (element is Iterable) {
|
| + helper(element);
|
| + } else {
|
| + result.add(element);
|
| + }
|
| + }
|
| + }
|
| + helper(nested);
|
| + return result;
|
| +}
|
| +
|
| +/// Returns a new map with all values in both [map1] and [map2].
|
| +///
|
| +/// If there are conflicting keys, [map2]'s value wins.
|
| +Map mergeMaps(Map map1, Map map2) {
|
| + var result = {};
|
| + map1.forEach((key, value) {
|
| + result[key] = value;
|
| + });
|
| + map2.forEach((key, value) {
|
| + result[key] = value;
|
| + });
|
| + return result;
|
| +}
|
| +
|
| +/// Returns a sink that maps events sent to [original] using [fn].
|
| +StreamSink mapSink(StreamSink original, fn(event)) {
|
| + var controller = new StreamController(sync: true);
|
| + controller.stream.listen(
|
| + (event) => original.add(fn(event)),
|
| + onError: (error, stackTrace) => original.addError(error, stackTrace),
|
| + onDone: () => original.close());
|
| + return controller.sink;
|
| +}
|
| +
|
| +/// Like [runZoned], but [zoneValues] are set for the callbacks in
|
| +/// [zoneSpecification] and [onError].
|
| +runZonedWithValues(body(), {Map zoneValues,
|
| + ZoneSpecification zoneSpecification, Function onError}) {
|
| + return runZoned(() {
|
| + return runZoned(body,
|
| + zoneSpecification: zoneSpecification, onError: onError);
|
| + }, zoneValues: zoneValues);
|
| +}
|
| +
|
| +/// Truncates [text] to fit within [maxLength].
|
| +///
|
| +/// This will try to truncate along word boundaries and preserve words both at
|
| +/// the beginning and the end of [text].
|
| +String truncate(String text, int maxLength) {
|
| + // Return the full message if it fits.
|
| + if (text.length <= maxLength) return text;
|
| +
|
| + // If we can fit the first and last three words, do so.
|
| + var words = text.split(' ');
|
| + if (words.length > 1) {
|
| + var i = words.length;
|
| + var length = words.first.length + 4;
|
| + do {
|
| + i--;
|
| + length += 1 + words[i].length;
|
| + } while (length <= maxLength && i > 0);
|
| + if (length > maxLength || i == 0) i++;
|
| + if (i < words.length - 4) {
|
| + // Require at least 3 words at the end.
|
| + var buffer = new StringBuffer();
|
| + buffer.write(words.first);
|
| + buffer.write(' ...');
|
| + for ( ; i < words.length; i++) {
|
| + buffer.write(' ');
|
| + buffer.write(words[i]);
|
| + }
|
| + return buffer.toString();
|
| + }
|
| + }
|
| +
|
| + // Otherwise truncate to return the trailing text, but attempt to start at
|
| + // the beginning of a word.
|
| + var result = text.substring(text.length - maxLength + 4);
|
| + var firstSpace = result.indexOf(' ');
|
| + if (firstSpace > 0) {
|
| + result = result.substring(firstSpace);
|
| + }
|
| + return '...$result';
|
| +}
|
| +
|
| +/// Returns a human-friendly representation of [duration].
|
| +String niceDuration(Duration duration) {
|
| + var minutes = duration.inMinutes;
|
| + var seconds = duration.inSeconds % 59;
|
| + var decaseconds = (duration.inMilliseconds % 1000) ~/ 100;
|
| +
|
| + var buffer = new StringBuffer();
|
| + if (minutes != 0) buffer.write("$minutes minutes");
|
| +
|
| + if (minutes == 0 || seconds != 0) {
|
| + if (minutes != 0) buffer.write(", ");
|
| + buffer.write(seconds);
|
| + if (decaseconds != 0) buffer.write(".$decaseconds");
|
| + buffer.write(" seconds");
|
| + }
|
| +
|
| + return buffer.toString();
|
| +}
|
| +
|
| +/// Merges [streams] into a single stream that emits events from all sources.
|
| +Stream mergeStreams(Iterable<Stream> streamIter) {
|
| + var streams = streamIter.toList();
|
| +
|
| + var subscriptions = new Set();
|
| + var controller;
|
| + controller = new StreamController(sync: true, onListen: () {
|
| + for (var stream in streams) {
|
| + var subscription;
|
| + subscription = stream.listen(
|
| + controller.add,
|
| + onError: controller.addError,
|
| + onDone: () {
|
| + subscriptions.remove(subscription);
|
| + if (subscriptions.isEmpty) controller.close();
|
| + });
|
| + subscriptions.add(subscription);
|
| + }
|
| + }, onPause: () {
|
| + for (var subscription in subscriptions) {
|
| + subscription.pause();
|
| + }
|
| + }, onResume: () {
|
| + for (var subscription in subscriptions) {
|
| + subscription.resume();
|
| + }
|
| + }, onCancel: () {
|
| + for (var subscription in subscriptions) {
|
| + subscription.cancel();
|
| + }
|
| + });
|
| +
|
| + return controller.stream;
|
| +}
|
| +
|
| +/// Returns the first value [stream] emits, or `null` if [stream] closes before
|
| +/// emitting a value.
|
| +Future maybeFirst(Stream stream) {
|
| + var completer = new Completer();
|
| +
|
| + var subscription;
|
| + subscription = stream.listen((data) {
|
| + completer.complete(data);
|
| + subscription.cancel();
|
| + }, onError: (error, stackTrace) {
|
| + completer.completeError(error, stackTrace);
|
| + subscription.cancel();
|
| + }, onDone: () {
|
| + completer.complete();
|
| + });
|
| +
|
| + return completer.future;
|
| +}
|
| +
|
| +/// Returns a [CancelableFuture] that returns the next value of [queue] unless
|
| +/// it's canceled.
|
| +///
|
| +/// If the future is canceled, [queue] is not moved forward at all. Note that
|
| +/// it's not safe to call further methods on [queue] until this future has
|
| +/// either completed or been canceled.
|
| +CancelableFuture cancelableNext(StreamQueue queue) {
|
| + var fork = queue.fork();
|
| + var completer = new CancelableCompleter(() => fork.cancel(immediate: true));
|
| + completer.complete(fork.next.then((_) {
|
| + fork.cancel();
|
| + return queue.next;
|
| + }));
|
| + return completer.future;
|
| +}
|
| +
|
| +/// Returns a single-subscription stream that emits the results of [futures] in
|
| +/// the order they complete.
|
| +///
|
| +/// If any futures in [futures] are [CancelableFuture]s, this will cancel them
|
| +/// if the subscription is canceled.
|
| +Stream inCompletionOrder(Iterable<Future> futures) {
|
| + var futureSet = futures.toSet();
|
| + var controller = new StreamController(sync: true, onCancel: () {
|
| + return Future.wait(futureSet.map((future) {
|
| + return future is CancelableFuture ? future.cancel() : null;
|
| + }).where((future) => future != null));
|
| + });
|
| +
|
| + for (var future in futureSet) {
|
| + future.then(controller.add).catchError(controller.addError)
|
| + .whenComplete(() {
|
| + futureSet.remove(future);
|
| + if (futureSet.isEmpty) controller.close();
|
| + });
|
| + }
|
| +
|
| + return controller.stream;
|
| +}
|
| +
|
| +/// Returns a stream that emits [error] and [stackTrace], then closes.
|
| +///
|
| +/// This is useful for adding errors to streams defined via `async*`.
|
| +Stream errorStream(error, StackTrace stackTrace) {
|
| + var controller = new StreamController();
|
| + controller.addError(error, stackTrace);
|
| + controller.close();
|
| + return controller.stream;
|
| +}
|
| +
|
| +/// Runs [fn] and discards its return value.
|
| +///
|
| +/// This is useful for making a block of code async without forcing the
|
| +/// containing method to return a future.
|
| +void invoke(fn()) {
|
| + fn();
|
| +}
|
| +
|
| +/// Returns a random base64 string containing [bytes] bytes of data.
|
| +///
|
| +/// [seed] is passed to [math.Random]; [urlSafe] and [addLineSeparator] are
|
| +/// passed to [CryptoUtils.bytesToBase64].
|
| +String randomBase64(int bytes, {int seed, bool urlSafe: false,
|
| + bool addLineSeparator: false}) {
|
| + var random = new math.Random(seed);
|
| + var data = [];
|
| + for (var i = 0; i < bytes; i++) {
|
| + data.add(random.nextInt(256));
|
| + }
|
| + return CryptoUtils.bytesToBase64(data,
|
| + urlSafe: urlSafe, addLineSeparator: addLineSeparator);
|
| +}
|
| +
|
| +/// Returns middleware that nests all requests beneath the URL prefix [beneath].
|
| +shelf.Middleware nestingMiddleware(String beneath) {
|
| + return (handler) {
|
| + var pathHandler = new PathHandler()..add(beneath, handler);
|
| + return pathHandler.handler;
|
| + };
|
| +}
|
|
|