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; |
+ }; |
+} |