flutter/packages/flutter_tools/lib/src/test/flutter_platform.dart
Todd Volkert 60b19b2035 Create abstraction layer for dart:io's Process commands (#7100)
With this change, they're run via instance methods on an object
obtained through the context. This will allow us to substitute
that object in tests with replay/record versions to allow us to
mock out the os-layer in tests.
2016-11-30 08:42:42 -08:00

211 lines
6.7 KiB
Dart

// 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 '../base/process_manager.dart';
import '../dart/package_map.dart';
import '../globals.dart';
import 'coverage_collector.dart';
final String _kSkyShell = Platform.environment['SKY_SHELL'];
final InternetAddress _kHost = InternetAddress.LOOPBACK_IP_V4;
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.address}:${server.port}$_kRunnerPath',
'ws://${_kHost.address}:${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 processManager.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.address,
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;
}
}
}