Line data Source code
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 : import 'dart:async';
6 : import 'dart:convert';
7 : import 'dart:math' as math;
8 : import 'dart:typed_data';
9 :
10 : import 'package:async/async.dart';
11 : import 'package:matcher/matcher.dart';
12 : import 'package:path/path.dart' as p;
13 : import 'package:stream_channel/stream_channel.dart';
14 : import 'package:term_glyph/term_glyph.dart' as glyph;
15 :
16 : import 'backend/invoker.dart';
17 : import 'backend/operating_system.dart';
18 :
19 : /// The maximum console line length.
20 : const _lineLength = 100;
21 :
22 : /// A typedef for a possibly-asynchronous function.
23 : ///
24 : /// The return type should only ever by [Future] or void.
25 : typedef AsyncFunction();
26 :
27 : /// A typedef for a zero-argument callback function.
28 : typedef void Callback();
29 :
30 : /// A transformer that decodes bytes using UTF-8 and splits them on newlines.
31 : final lineSplitter = new StreamTransformer<List<int>, String>(
32 : (stream, cancelOnError) => stream
33 : .transform(UTF8.decoder)
34 : .transform(const LineSplitter())
35 : .listen(null, cancelOnError: cancelOnError));
36 :
37 : /// A [StreamChannelTransformer] that converts a chunked string channel to a
38 : /// line-by-line channel. Note that this is only safe for channels whose
39 : /// messages are guaranteed not to contain newlines.
40 : final chunksToLines = new StreamChannelTransformer(
41 : const LineSplitter(),
42 : new StreamSinkTransformer.fromHandlers(
43 : handleData: (data, sink) => sink.add("$data\n")));
44 :
45 : /// A regular expression to match the exception prefix that some exceptions'
46 : /// [Object.toString] values contain.
47 : final _exceptionPrefix = new RegExp(r'^([A-Z][a-zA-Z]*)?(Exception|Error): ');
48 :
49 : /// A regular expression matching a single vowel.
50 : final _vowel = new RegExp('[aeiou]');
51 :
52 : /// Directories that are specific to OS X.
53 : ///
54 : /// This is used to try to distinguish OS X and Linux in [currentOSGuess].
55 : final _macOSDirectories = new Set<String>.from(
56 : ["/Applications", "/Library", "/Network", "/System", "/Users"]);
57 :
58 : /// Returns the best guess for the current operating system without using
59 : /// `dart:io`.
60 : ///
61 : /// This is useful for running test files directly and skipping tests as
62 : /// appropriate. The only OS-specific information we have is the current path,
63 : /// which we try to use to figure out the OS.
64 : final OperatingSystem currentOSGuess = (() {
65 15 : if (p.style == p.Style.url) return OperatingSystem.none;
66 15 : if (p.style == p.Style.windows) return OperatingSystem.windows;
67 20 : if (_macOSDirectories.any(p.current.startsWith)) return OperatingSystem.macOS;
68 : return OperatingSystem.linux;
69 : })();
70 :
71 : /// A regular expression matching a hyphenated identifier.
72 : ///
73 : /// This is like a standard Dart identifier, except that it can also contain
74 : /// hyphens.
75 : final hyphenatedIdentifier = new RegExp(r"[a-zA-Z_-][a-zA-Z0-9_-]*");
76 :
77 : /// Like [hyphenatedIdentifier], but anchored so that it must match the entire
78 : /// string.
79 : final anchoredHyphenatedIdentifier =
80 : new RegExp("^${hyphenatedIdentifier.pattern}\$");
81 :
82 : /// A pair of values.
83 : class Pair<E, F> {
84 : E first;
85 : F last;
86 :
87 0 : Pair(this.first, this.last);
88 :
89 0 : String toString() => '($first, $last)';
90 :
91 : bool operator ==(other) {
92 0 : if (other is! Pair) return false;
93 0 : return other.first == first && other.last == last;
94 : }
95 :
96 0 : int get hashCode => first.hashCode ^ last.hashCode;
97 : }
98 :
99 : /// Get a string description of an exception.
100 : ///
101 : /// Many exceptions include the exception class name at the beginning of their
102 : /// [toString], so we remove that if it exists.
103 : String getErrorMessage(error) =>
104 0 : error.toString().replaceFirst(_exceptionPrefix, '');
105 :
106 : /// Indent each line in [string] by [size] spaces.
107 : ///
108 : /// If [first] is passed, it's used in place of the first line's indentation and
109 : /// [size] defaults to `first.length`. Otherwise, [size] defaults to 2.
110 : String indent(String string, {int size, String first}) {
111 0 : size ??= first == null ? 2 : first.length;
112 0 : return prefixLines(string, " " * size, first: first);
113 : }
114 :
115 : /// Returns a sentence fragment listing the elements of [iter].
116 : ///
117 : /// This converts each element of [iter] to a string and separates them with
118 : /// commas and/or [conjunction] where appropriate. The [conjunction] defaults to
119 : /// "and".
120 : String toSentence(Iterable iter, {String conjunction}) {
121 0 : if (iter.length == 1) return iter.first.toString();
122 :
123 0 : var result = iter.take(iter.length - 1).join(", ");
124 0 : if (iter.length > 2) result += ",";
125 0 : return "$result ${conjunction ?? 'and'} ${iter.last}";
126 : }
127 :
128 : /// Returns [name] if [number] is 1, or the plural of [name] otherwise.
129 : ///
130 : /// By default, this just adds "s" to the end of [name] to get the plural. If
131 : /// [plural] is passed, that's used instead.
132 : String pluralize(String name, int number, {String plural}) {
133 0 : if (number == 1) return name;
134 : if (plural != null) return plural;
135 0 : return '${name}s';
136 : }
137 :
138 : /// Returns [noun] with an indefinite article ("a" or "an") added, based on
139 : /// whether its first letter is a vowel.
140 0 : String a(String noun) => noun.startsWith(_vowel) ? "an $noun" : "a $noun";
141 :
142 : /// Wraps [text] so that it fits within [lineLength], which defaults to 100
143 : /// characters.
144 : ///
145 : /// This preserves existing newlines and doesn't consider terminal color escapes
146 : /// part of a word's length.
147 : String wordWrap(String text, {int lineLength}) {
148 : if (lineLength == null) lineLength = _lineLength;
149 0 : return text.split("\n").map((originalLine) {
150 0 : var buffer = new StringBuffer();
151 : var lengthSoFar = 0;
152 0 : for (var word in originalLine.split(" ")) {
153 0 : var wordLength = withoutColors(word).length;
154 0 : if (wordLength > lineLength) {
155 0 : if (lengthSoFar != 0) buffer.writeln();
156 0 : buffer.writeln(word);
157 0 : } else if (lengthSoFar == 0) {
158 0 : buffer.write(word);
159 : lengthSoFar = wordLength;
160 0 : } else if (lengthSoFar + 1 + wordLength > lineLength) {
161 0 : buffer.writeln();
162 0 : buffer.write(word);
163 : lengthSoFar = wordLength;
164 : } else {
165 0 : buffer.write(" $word");
166 0 : lengthSoFar += 1 + wordLength;
167 : }
168 : }
169 0 : return buffer.toString();
170 0 : }).join("\n");
171 : }
172 :
173 : /// A regular expression matching terminal color codes.
174 : final _colorCode = new RegExp('\u001b\\[[0-9;]+m');
175 :
176 : /// Returns [str] without any color codes.
177 0 : String withoutColors(String str) => str.replaceAll(_colorCode, '');
178 :
179 : /// Flattens nested [Iterable]s inside an [Iterable] into a single [List]
180 : /// containing only non-[Iterable] elements.
181 : List flatten(Iterable nested) {
182 0 : var result = [];
183 : helper(iter) {
184 0 : for (var element in iter) {
185 0 : if (element is Iterable) {
186 0 : helper(element);
187 : } else {
188 0 : result.add(element);
189 : }
190 : }
191 : }
192 :
193 0 : helper(nested);
194 : return result;
195 : }
196 :
197 : /// Truncates [text] to fit within [maxLength].
198 : ///
199 : /// This will try to truncate along word boundaries and preserve words both at
200 : /// the beginning and the end of [text].
201 : String truncate(String text, int maxLength) {
202 : // Return the full message if it fits.
203 0 : if (text.length <= maxLength) return text;
204 :
205 : // If we can fit the first and last three words, do so.
206 0 : var words = text.split(' ');
207 0 : if (words.length > 1) {
208 0 : var i = words.length;
209 0 : var length = words.first.length + 4;
210 : do {
211 0 : i--;
212 0 : length += 1 + words[i].length;
213 0 : } while (length <= maxLength && i > 0);
214 0 : if (length > maxLength || i == 0) i++;
215 0 : if (i < words.length - 4) {
216 : // Require at least 3 words at the end.
217 0 : var buffer = new StringBuffer();
218 0 : buffer.write(words.first);
219 0 : buffer.write(' ...');
220 0 : for (; i < words.length; i++) {
221 0 : buffer.write(' ');
222 0 : buffer.write(words[i]);
223 : }
224 0 : return buffer.toString();
225 : }
226 : }
227 :
228 : // Otherwise truncate to return the trailing text, but attempt to start at
229 : // the beginning of a word.
230 0 : var result = text.substring(text.length - maxLength + 4);
231 0 : var firstSpace = result.indexOf(' ');
232 0 : if (firstSpace > 0) {
233 0 : result = result.substring(firstSpace);
234 : }
235 0 : return '...$result';
236 : }
237 :
238 : /// Returns a human-friendly representation of [duration].
239 : String niceDuration(Duration duration) {
240 0 : var minutes = duration.inMinutes;
241 0 : var seconds = duration.inSeconds % 60;
242 0 : var decaseconds = (duration.inMilliseconds % 1000) ~/ 100;
243 :
244 0 : var buffer = new StringBuffer();
245 0 : if (minutes != 0) buffer.write("$minutes minutes");
246 :
247 0 : if (minutes == 0 || seconds != 0) {
248 0 : if (minutes != 0) buffer.write(", ");
249 0 : buffer.write(seconds);
250 0 : if (decaseconds != 0) buffer.write(".$decaseconds");
251 0 : buffer.write(" seconds");
252 : }
253 :
254 0 : return buffer.toString();
255 : }
256 :
257 : /// Returns the first value [stream] emits, or `null` if [stream] closes before
258 : /// emitting a value.
259 : Future maybeFirst(Stream stream) {
260 0 : var completer = new Completer();
261 :
262 : var subscription;
263 0 : subscription = stream.listen((data) {
264 0 : completer.complete(data);
265 0 : subscription.cancel();
266 : }, onError: (error, stackTrace) {
267 0 : completer.completeError(error, stackTrace);
268 0 : subscription.cancel();
269 : }, onDone: () {
270 0 : completer.complete();
271 : });
272 :
273 0 : return completer.future;
274 : }
275 :
276 : /// Returns a single-subscription stream that emits the results of [operations]
277 : /// in the order they complete.
278 : ///
279 : /// If the subscription is canceled, any pending operations are canceled as
280 : /// well.
281 : Stream<T> inCompletionOrder<T>(Iterable<CancelableOperation<T>> operations) {
282 0 : var operationSet = operations.toSet();
283 0 : var controller = new StreamController<T>(
284 : sync: true,
285 : onCancel: () {
286 0 : return Future.wait(operationSet.map((operation) => operation.cancel()));
287 : });
288 :
289 0 : for (var operation in operationSet) {
290 0 : operation.value
291 0 : .then((value) => controller.add(value))
292 0 : .catchError(controller.addError)
293 0 : .whenComplete(() {
294 0 : operationSet.remove(operation);
295 0 : if (operationSet.isEmpty) controller.close();
296 : });
297 : }
298 :
299 0 : return controller.stream;
300 : }
301 :
302 : /// Returns a stream that emits [error] and [stackTrace], then closes.
303 : ///
304 : /// This is useful for adding errors to streams defined via `async*`.
305 : Stream errorStream(error, StackTrace stackTrace) {
306 0 : var controller = new StreamController();
307 0 : controller.addError(error, stackTrace);
308 0 : controller.close();
309 0 : return controller.stream;
310 : }
311 :
312 : /// Runs [fn] and discards its return value.
313 : ///
314 : /// This is useful for making a block of code async without forcing the
315 : /// containing method to return a future.
316 : void invoke(fn()) {
317 0 : fn();
318 : }
319 :
320 : /// Runs [body] with special error-handling behavior.
321 : ///
322 : /// Errors emitted [body] will still cause the current test to fail, but they
323 : /// won't cause it to *stop*. In particular, they won't remove any outstanding
324 : /// callbacks registered outside of [body].
325 : ///
326 : /// This may only be called within a test.
327 : Future errorsDontStopTest(body()) {
328 0 : var completer = new Completer();
329 :
330 0 : Invoker.current.addOutstandingCallback();
331 0 : Invoker.current.waitForOutstandingCallbacks(() {
332 0 : new Future.sync(body).whenComplete(completer.complete);
333 0 : }).then((_) => Invoker.current.removeOutstandingCallback());
334 :
335 0 : return completer.future;
336 : }
337 :
338 : /// Returns a random base64 string containing [bytes] bytes of data.
339 : ///
340 : /// [seed] is passed to [math.Random].
341 : String randomBase64(int bytes, {int seed}) {
342 0 : var random = new math.Random(seed);
343 0 : var data = new Uint8List(bytes);
344 0 : for (var i = 0; i < bytes; i++) {
345 0 : data[i] = random.nextInt(256);
346 : }
347 0 : return BASE64.encode(data);
348 : }
349 :
350 : /// Throws an [ArgumentError] if [message] isn't recursively JSON-safe.
351 : void ensureJsonEncodable(Object message) {
352 : if (message == null ||
353 0 : message is String ||
354 0 : message is num ||
355 0 : message is bool) {
356 : // JSON-encodable, hooray!
357 0 : } else if (message is List) {
358 0 : for (var element in message) {
359 0 : ensureJsonEncodable(element);
360 : }
361 0 : } else if (message is Map) {
362 0 : message.forEach((key, value) {
363 0 : if (key is! String) {
364 0 : throw new ArgumentError("$message can't be JSON-encoded.");
365 : }
366 :
367 0 : ensureJsonEncodable(value);
368 : });
369 : } else {
370 0 : throw new ArgumentError.value("$message can't be JSON-encoded.");
371 : }
372 : }
373 :
374 : /// Prepends a vertical bar to [text].
375 0 : String addBar(String text) => prefixLines(text, "${glyph.verticalLine} ",
376 0 : first: "${glyph.downEnd} ", last: "${glyph.upEnd} ", single: "| ");
377 :
378 : /// Indents [text], and adds a bullet at the beginning.
379 : String addBullet(String text) =>
380 0 : prefixLines(text, " ", first: "${glyph.bullet} ");
381 :
382 : /// Converts [strings] to a bulleted list.
383 0 : String bullet(Iterable<String> strings) => strings.map(addBullet).join("\n");
384 :
385 : /// Prepends each line in [text] with [prefix].
386 : ///
387 : /// If [first] or [last] is passed, the first and last lines, respectively, are
388 : /// prefixed with those instead. If [single] is passed, it's used if there's
389 : /// only a single line; otherwise, [first], [last], or [prefix] is used, in that
390 : /// order of precedence.
391 : String prefixLines(String text, String prefix,
392 : {String first, String last, String single}) {
393 : first ??= prefix;
394 : last ??= prefix;
395 : single ??= first ?? last ?? prefix;
396 :
397 0 : var lines = text.split('\n');
398 0 : if (lines.length == 1) return "$single$text";
399 :
400 0 : var buffer = new StringBuffer("$first${lines.first}\n");
401 :
402 : // Write out all but the first and last lines with [prefix].
403 0 : for (var line in lines.skip(1).take(lines.length - 2)) {
404 0 : buffer.writeln("$prefix$line");
405 : }
406 0 : buffer.write("$last${lines.last}");
407 0 : return buffer.toString();
408 : }
409 :
410 : /// Returns a pretty-printed representation of [value].
411 : ///
412 : /// The matcher package doesn't expose its pretty-print function directly, but
413 : /// we can use it through StringDescription.
414 : String prettyPrint(value) =>
415 0 : new StringDescription().addDescriptionOf(value).toString();
|