Line data Source code
1 : // Copyright (c) 2015, 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:core' as core; 8 : import 'dart:core'; 9 : import 'dart:io'; 10 : 11 : import 'package:async/async.dart'; 12 : import 'package:path/path.dart' as p; 13 : import 'package:test_api/src/backend/operating_system.dart'; // ignore: implementation_imports 14 : import 'package:test_api/src/backend/runtime.dart'; // ignore: implementation_imports 15 : import 'package:test_api/src/backend/suite_platform.dart'; // ignore: implementation_imports 16 : 17 : import 'pretty_print.dart'; 18 : 19 : /// The default line length for output when there isn't a terminal attached to 20 : /// stdout. 21 : const _defaultLineLength = 200; 22 : 23 : /// Whether the test runner is running on Google-internal infrastructure. 24 0 : final bool inGoogle = Platform.version.contains('(google3)'); 25 : 26 : /// The maximum line length for output. 27 0 : final int lineLength = () { 28 : try { 29 : return stdout.terminalColumns; 30 : } on UnsupportedError { 31 : // This can throw an [UnsupportedError] if we're running in a JS context 32 : // where `dart:io` is unavaiable. 33 : return _defaultLineLength; 34 : } on StdoutException { 35 : return _defaultLineLength; 36 : } 37 : }(); 38 : 39 : /// The root directory of the Dart SDK. 40 0 : final String sdkDir = p.dirname(p.dirname(Platform.resolvedExecutable)); 41 : 42 : /// The current operating system. 43 0 : final currentOS = OperatingSystem.findByIoName(Platform.operatingSystem); 44 : 45 : /// Returns a [SuitePlatform] with the given [runtime], and with [os] and 46 : /// [inGoogle] determined automatically. 47 : /// 48 : /// If [runtime] is a browser, this will set [os] to [OperatingSystem.none]. 49 0 : SuitePlatform currentPlatform(Runtime runtime) => SuitePlatform(runtime, 50 0 : os: runtime.isBrowser ? OperatingSystem.none : currentOS, 51 0 : inGoogle: inGoogle); 52 : 53 : /// A transformer that decodes bytes using UTF-8 and splits them on newlines. 54 0 : final lineSplitter = StreamTransformer<List<int>, String>( 55 : (stream, cancelOnError) => utf8.decoder 56 : .bind(stream) 57 : .transform(const LineSplitter()) 58 : .listen(null, cancelOnError: cancelOnError)); 59 : 60 : /// A queue of lines of standard input. 61 : /// 62 : /// Also returns an empty stream for Fuchsia since Fuchsia components can't 63 : /// access stdin. 64 0 : StreamQueue<String> get stdinLines => _stdinLines ??= StreamQueue( 65 0 : Platform.isFuchsia ? Stream<String>.empty() : lineSplitter.bind(stdin)); 66 : 67 : StreamQueue<String>? _stdinLines; 68 : 69 : /// Call cancel on [stdinLines], but only if it's been accessed previously. 70 0 : void cancelStdinLines() => _stdinLines?.cancel(immediate: true); 71 : 72 : /// Whether this is being run as a subprocess in the test package's own tests. 73 0 : bool inTestTests = Platform.environment['_DART_TEST_TESTING'] == 'true'; 74 : 75 : /// The root directory below which to nest temporary directories created by the 76 : /// test runner. 77 : /// 78 : /// This is configurable so that the test code can validate that the runner 79 : /// cleans up after itself fully. 80 0 : final _tempDir = Platform.environment.containsKey('_UNITTEST_TEMP_DIR') 81 : ? Platform.environment['_UNITTEST_TEMP_DIR']! 82 : : Directory.systemTemp.path; 83 : 84 : /// Whether or not the current terminal supports ansi escape codes. 85 : /// 86 : /// Otherwise only printable ASCII characters should be used. 87 0 : bool get canUseSpecialChars => 88 0 : (!Platform.isWindows || stdout.supportsAnsiEscapes) && !inTestTests; 89 : 90 : /// Creates a temporary directory and returns its path. 91 0 : String createTempDir() => 92 0 : Directory(_tempDir).createTempSync('dart_test_').resolveSymbolicLinksSync(); 93 : 94 : /// Creates a temporary directory and passes its path to [fn]. 95 : /// 96 : /// Once the [Future] returned by [fn] completes, the temporary directory and 97 : /// all its contents are deleted. [fn] can also return `null`, in which case 98 : /// the temporary directory is deleted immediately afterwards. 99 : /// 100 : /// Returns a future that completes to the value that the future returned from 101 : /// [fn] completes to. 102 0 : Future withTempDir(Future Function(String) fn) { 103 0 : return Future.sync(() { 104 0 : var tempDir = createTempDir(); 105 0 : return Future.sync(() => fn(tempDir)) 106 0 : .whenComplete(() => Directory(tempDir).deleteSync(recursive: true)); 107 : }); 108 : } 109 : 110 : /// Wraps [text] so that it fits within [lineLength]. 111 : /// 112 : /// This preserves existing newlines and doesn't consider terminal color escapes 113 : /// part of a word's length. It only splits words on spaces, not on other sorts 114 : /// of whitespace. 115 0 : String wordWrap(String text) { 116 0 : return text.split('\n').map((originalLine) { 117 0 : var buffer = StringBuffer(); 118 : var lengthSoFar = 0; 119 0 : for (var word in originalLine.split(' ')) { 120 0 : var wordLength = withoutColors(word).length; 121 0 : if (wordLength > lineLength) { 122 0 : if (lengthSoFar != 0) buffer.writeln(); 123 0 : buffer.writeln(word); 124 0 : } else if (lengthSoFar == 0) { 125 0 : buffer.write(word); 126 : lengthSoFar = wordLength; 127 0 : } else if (lengthSoFar + 1 + wordLength > lineLength) { 128 0 : buffer.writeln(); 129 0 : buffer.write(word); 130 : lengthSoFar = wordLength; 131 : } else { 132 0 : buffer.write(' $word'); 133 0 : lengthSoFar += 1 + wordLength; 134 : } 135 : } 136 0 : return buffer.toString(); 137 0 : }).join('\n'); 138 : } 139 : 140 : /// Print a warning containing [message]. 141 : /// 142 : /// This automatically wraps lines if they get too long. If [color] is passed, 143 : /// it controls whether the warning header is color; otherwise, it defaults to 144 : /// [canUseSpecialChars]. 145 : /// 146 : /// If [print] is `true`, this prints the message using [print] to associate it 147 : /// with the current test. Otherwise, it prints it using [stderr]. 148 0 : void warn(String message, {bool? color, bool print = false}) { 149 0 : color ??= canUseSpecialChars; 150 : var header = color ? '\u001b[33mWarning:\u001b[0m' : 'Warning:'; 151 0 : (print ? core.print : stderr.writeln)(wordWrap('$header $message\n')); 152 : } 153 : 154 : /// Repeatedly finds a probably-unused port on localhost and passes it to 155 : /// [tryPort] until it binds successfully. 156 : /// 157 : /// [tryPort] should return a non-`null` value or a Future completing to a 158 : /// non-`null` value once it binds successfully. This value will be returned 159 : /// by [getUnusedPort] in turn. 160 : /// 161 : /// This is necessary for ensuring that our port binding isn't flaky for 162 : /// applications that don't print out the bound port. 163 0 : Future<T> getUnusedPort<T>(FutureOr<T> Function(int port) tryPort) async { 164 : T? value; 165 0 : await Future.doWhile(() async { 166 0 : value = await tryPort(await getUnsafeUnusedPort()); 167 : return value == null; 168 : }); 169 : return value!; 170 : } 171 : 172 : /// Whether this computer supports binding to IPv6 addresses. 173 : var _maySupportIPv6 = true; 174 : 175 : /// Returns a port that is probably, but not definitely, not in use. 176 : /// 177 : /// This has a built-in race condition: another process may bind this port at 178 : /// any time after this call has returned. If at all possible, callers should 179 : /// use [getUnusedPort] instead. 180 0 : Future<int> getUnsafeUnusedPort() async { 181 : late int port; 182 : if (_maySupportIPv6) { 183 : try { 184 0 : final socket = await ServerSocket.bind(InternetAddress.loopbackIPv6, 0, 185 : v6Only: true); 186 0 : port = socket.port; 187 0 : await socket.close(); 188 0 : } on SocketException { 189 : _maySupportIPv6 = false; 190 : } 191 : } 192 : if (!_maySupportIPv6) { 193 0 : final socket = await RawServerSocket.bind(InternetAddress.loopbackIPv4, 0); 194 0 : port = socket.port; 195 0 : await socket.close(); 196 : } 197 0 : return port; 198 : } 199 : 200 : /// Returns the full URL of the Chrome remote debugger for the main page. 201 : /// 202 : /// This takes the [base] remote debugger URL (which points to a browser-wide 203 : /// page) and uses its JSON API to find the resolved URL for debugging the host 204 : /// page. 205 0 : Future<Uri> getRemoteDebuggerUrl(Uri base) async { 206 : try { 207 0 : var client = HttpClient(); 208 0 : var request = await client.getUrl(base.resolve('/json/list')); 209 0 : var response = await request.close(); 210 : var jsonObject = 211 0 : await json.fuse(utf8).decoder.bind(response).single as List; 212 0 : return base.resolve(jsonObject.first['devtoolsFrontendUrl'] as String); 213 : } catch (_) { 214 : // If we fail to talk to the remote debugger protocol, give up and return 215 : // the raw URL rather than crashing. 216 : return base; 217 : } 218 : }