// Copyright 2015 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'dart:math' as math; import 'package:async/async.dart'; import 'package:path/path.dart' as path; import 'package:stream_channel/stream_channel.dart'; import 'package:test/src/backend/test_platform.dart'; // ignore: implementation_imports import 'package:test/src/runner/plugin/platform.dart'; // ignore: implementation_imports import 'package:test/src/runner/plugin/hack_register_platform.dart' as hack; // ignore: implementation_imports import '../dart/package_map.dart'; import '../globals.dart'; import 'coverage_collector.dart'; final String _kSkyShell = Platform.environment['SKY_SHELL']; const String _kHost = '127.0.0.1'; const String _kRunnerPath = '/runner'; const String _kShutdownPath = '/shutdown'; String shellPath; List<String> fontDirectories = <String>[cache.getCacheArtifacts().path]; void installHook() { hack.registerPlatformPlugin(<TestPlatform>[TestPlatform.vm], () => new FlutterPlatform()); } class _ServerInfo { final String url; final String shutdownUrl; final Future<WebSocket> socket; final HttpServer server; _ServerInfo(this.server, this.url, this.shutdownUrl, this.socket); } Future<_ServerInfo> _startServer() async { HttpServer server = await HttpServer.bind(_kHost, 0); Completer<WebSocket> socket = new Completer<WebSocket>(); server.listen((HttpRequest request) { if (request.uri.path == _kRunnerPath) socket.complete(WebSocketTransformer.upgrade(request)); else if (!socket.isCompleted && request.uri.path == _kShutdownPath) socket.completeError('Failed to start test'); }); return new _ServerInfo(server, 'ws://$_kHost:${server.port}$_kRunnerPath', 'ws://$_kHost:${server.port}$_kShutdownPath', socket.future); } Future<Process> _startProcess(String mainPath, { String packages, int observatoryPort }) { assert(shellPath != null || _kSkyShell != null); // Please provide the path to the shell in the SKY_SHELL environment variable. String executable = shellPath ?? _kSkyShell; List<String> arguments = <String>[]; if (observatoryPort != null) { arguments.add('--observatory-port=$observatoryPort'); } else { arguments.add('--disable-observatory'); } arguments.addAll(<String>[ '--non-interactive', '--enable-checked-mode', '--packages=$packages', mainPath ]); printTrace('$executable ${arguments.join(' ')}'); Map<String, String> environment = <String, String>{ 'FLUTTER_TEST': 'true', 'FONTCONFIG_FILE': _fontConfigFile.path, }; return Process.start(executable, arguments, environment: environment); } void _attachStandardStreams(Process process) { for (Stream<List<int>> stream in <Stream<List<int>>>[process.stderr, process.stdout]) { stream.transform(UTF8.decoder) .transform(const LineSplitter()) .listen((String line) { if (line != null) print('Shell: $line'); }); } } File _cachedFontConfig; /// Returns a Fontconfig config file that limits font fallback to directories /// specified in [fontDirectories]. File get _fontConfigFile { if (_cachedFontConfig != null) return _cachedFontConfig; Directory fontsDir = Directory.systemTemp.createTempSync('flutter_fonts'); StringBuffer sb = new StringBuffer(); sb.writeln('<fontconfig>'); for (String fontDir in fontDirectories) { sb.writeln(' <dir>$fontDir</dir>'); } sb.writeln(' <cachedir>/var/cache/fontconfig</cachedir>'); sb.writeln('</fontconfig>'); _cachedFontConfig = new File('${fontsDir.path}/fonts.conf'); _cachedFontConfig.createSync(); _cachedFontConfig.writeAsStringSync(sb.toString()); return _cachedFontConfig; } class FlutterPlatform extends PlatformPlugin { @override StreamChannel<dynamic> loadChannel(String mainPath, TestPlatform platform) { return StreamChannelCompleter.fromFuture(_startTest(mainPath)); } Future<StreamChannel<dynamic>> _startTest(String mainPath) async { _ServerInfo info = await _startServer(); Directory tempDir = Directory.systemTemp.createTempSync( 'dart_test_listener'); File listenerFile = new File('${tempDir.path}/listener.dart'); listenerFile.createSync(); listenerFile.writeAsStringSync(''' import 'dart:convert'; import 'dart:io'; import 'package:stream_channel/stream_channel.dart'; import 'package:test/src/runner/plugin/remote_platform_helpers.dart'; import 'package:test/src/runner/vm/catch_isolate_errors.dart'; import '${path.toUri(path.absolute(mainPath))}' as test; void main() { String server = Uri.decodeComponent('${Uri.encodeComponent(info.url)}'); StreamChannel channel = serializeSuite(() { catchIsolateErrors(); return test.main; }); WebSocket.connect(server).then((WebSocket socket) { socket.map(JSON.decode).pipe(channel.sink); socket.addStream(channel.stream.map(JSON.encode)); }); } '''); int observatoryPort; if (CoverageCollector.instance.enabled) { observatoryPort = CoverageCollector.instance.observatoryPort ?? new math.Random().nextInt(30000) + 2000; await CoverageCollector.instance.finishPendingJobs(); } Process process = await _startProcess( listenerFile.path, packages: PackageMap.globalPackagesPath, observatoryPort: observatoryPort ); _attachStandardStreams(process); void finalize() { if (process != null) { Process processToKill = process; process = null; CoverageCollector.instance.collectCoverage( host: _kHost, port: observatoryPort, processToKill: processToKill ); } if (tempDir != null) { Directory dirToDelete = tempDir; tempDir = null; dirToDelete.deleteSync(recursive: true); } } process.exitCode.then((_) { WebSocket.connect(info.shutdownUrl); }); try { WebSocket socket = await info.socket; StreamChannel<dynamic> channel = new StreamChannel<dynamic>(socket.map(JSON.decode), socket); return channel.transformStream( new StreamTransformer<dynamic, dynamic>.fromHandlers( handleDone: (EventSink<dynamic> sink) { finalize(); sink.close(); } ) ).transformSink(new StreamSinkTransformer<dynamic, String>.fromHandlers( handleData: (dynamic data, StreamSink<String> sink) { sink.add(JSON.encode(data)); }, handleDone: (EventSink<String> sink) { finalize(); sink.close(); } )); } catch(e) { finalize(); rethrow; } } }