Commit 96ec5316 authored by Adam Barth's avatar Adam Barth

Switch from hack_load_vm_file_hook to PlatformPlugin

This patch switches us from using our previous hacking approach to integrating
with package:test to using the new PlatformPlugin interface.
parent 13e7c017
......@@ -14,7 +14,7 @@ dependencies:
# See the comment in flutter_tools' pubspec.yaml. We have to pin it
# here also because sky_services depends on mojo_sdk which depends
# on test.
test: 0.12.6+1
test: 0.12.11+1
# We have to pin analyzer to 0.27.1 because the flx package depends
# on pointycastle which depends on reflectable which depends on
......
name: flutter_test
dependencies:
test: 0.12.6+1
test: 0.12.11+1
quiver: ^0.21.4
flutter:
path: ../flutter
......
......@@ -12,7 +12,7 @@ import '../artifacts.dart';
import '../build_configuration.dart';
import '../globals.dart';
import '../runner/flutter_command.dart';
import '../test/loader.dart' as loader;
import '../test/flutter_platform.dart' as loader;
class TestCommand extends FlutterCommand {
String get name => 'test';
......
// 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 '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';
import 'package:test/src/runner/plugin/platform.dart';
import 'package:test/src/runner/plugin/hack_register_platform.dart' as hack;
import '../artifacts.dart';
final String _kSkyShell = Platform.environment['SKY_SHELL'];
const String _kHost = '127.0.0.1';
const String _kPath = '/runner';
String shellPath;
void installHook() {
hack.registerPlatformPlugin([TestPlatform.vm], () => new FlutterPlatform());
}
class _ServerInfo {
final String url;
final Future<WebSocket> socket;
final HttpServer server;
_ServerInfo(this.server, this.url, 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 == _kPath)
socket.complete(WebSocketTransformer.upgrade(request));
});
return new _ServerInfo(server, 'ws://$_kHost:${server.port}$_kPath', socket.future);
}
Future<Process> _startProcess(String mainPath, { String packageRoot }) {
assert(shellPath != null || _kSkyShell != null); // Please provide the path to the shell in the SKY_SHELL environment variable.
return Process.start(shellPath ?? _kSkyShell, [
'--enable-checked-mode',
'--non-interactive',
'--package-root=$packageRoot',
mainPath,
]);
}
class FlutterPlatform extends PlatformPlugin {
StreamChannel loadChannel(String mainPath, TestPlatform platform) {
return StreamChannelCompleter.fromFuture(_startTest(mainPath));
}
Future<StreamChannel> _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));
});
}
''');
Process process = await _startProcess(
listenerFile.path,
packageRoot: path.absolute(ArtifactStore.packageRoot)
);
void finalize() {
if (process != null) {
Process processToKill = process;
process = null;
processToKill.kill();
}
if (tempDir != null) {
Directory dirToDelete = tempDir;
tempDir = null;
dirToDelete.deleteSync(recursive: true);
}
}
try {
WebSocket socket = await info.socket;
StreamChannel channel = new StreamChannel(socket.map(JSON.decode), socket);
return channel.transformStream(
new StreamTransformer.fromHandlers(
handleDone: (sink) {
finalize();
sink.close();
}
)
).transformSink(new StreamSinkTransformer.fromHandlers(
handleData: (data, StreamSink sink) {
sink.add(JSON.encode(data));
},
handleDone: (sink) {
finalize();
sink.close();
}
));
} catch(e) {
finalize();
rethrow;
}
}
}
// 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';
class JSONSocket {
JSONSocket(WebSocket socket, this.unusualTermination)
: _socket = socket, stream = socket.map(JSON.decode).asBroadcastStream();
final WebSocket _socket;
final Stream stream;
final Future<String> unusualTermination;
void send(dynamic data) {
_socket.add(JSON.encode(data));
}
}
// 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 'package:path/path.dart' as path;
import 'package:stack_trace/stack_trace.dart';
import 'package:test/src/backend/group.dart';
import 'package:test/src/backend/metadata.dart';
import 'package:test/src/backend/test_platform.dart';
import 'package:test/src/runner/configuration.dart';
import 'package:test/src/runner/hack_load_vm_file_hook.dart' as hack;
import 'package:test/src/runner/load_exception.dart';
import 'package:test/src/runner/runner_suite.dart';
import 'package:test/src/runner/vm/environment.dart';
import 'package:test/src/util/io.dart';
import 'package:test/src/util/remote_exception.dart';
import 'json_socket.dart';
import 'remote_test.dart';
void installHook() {
hack.loadVMFileHook = _loadVMFile;
}
final String _kSkyShell = Platform.environment['SKY_SHELL'];
const String _kHost = '127.0.0.1';
const String _kPath = '/runner';
String shellPath;
// Right now a bunch of our tests crash or assert after the tests have finished running.
// Mostly this is just because the test puts the framework in an inconsistent state with
// a scheduled microtask that verifies that state. Eventually we should fix all these
// problems but for now we'll just paper over them.
const bool kExpectAllTestsToCloseCleanly = false;
class _ServerInfo {
final String url;
final Future<WebSocket> socket;
final HttpServer server;
_ServerInfo(this.server, this.url, this.socket);
}
Future<_ServerInfo> _createServer() async {
HttpServer server = await HttpServer.bind(_kHost, 0);
Completer<WebSocket> socket = new Completer<WebSocket>();
server.listen((HttpRequest request) {
if (request.uri.path == _kPath)
socket.complete(WebSocketTransformer.upgrade(request));
});
return new _ServerInfo(server, 'ws://$_kHost:${server.port}$_kPath', socket.future);
}
Future<Process> _startProcess(String mainPath, { String packageRoot }) {
assert(shellPath != null || _kSkyShell != null); // Please provide the path to the shell in the SKY_SHELL environment variable.
return Process.start(shellPath ?? _kSkyShell, [
'--enable-checked-mode',
'--non-interactive',
'--package-root=$packageRoot',
mainPath,
]);
}
Future<RunnerSuite> _loadVMFile(String mainPath,
Metadata metadata,
Configuration config) async {
String encodedMetadata = Uri.encodeComponent(JSON.encode(
metadata.serialize()));
_ServerInfo info = await _createServer();
Directory tempDir = await Directory.systemTemp.createTemp(
'dart_test_listener');
File listenerFile = new File('${tempDir.path}/listener.dart');
await listenerFile.create();
await listenerFile.writeAsString('''
import 'dart:convert';
import 'package:test/src/backend/metadata.dart';
import 'package:flutter_tools/src/test/remote_listener.dart';
import '${path.toUri(path.absolute(mainPath))}' as test;
void main() {
String server = Uri.decodeComponent('${Uri.encodeComponent(info.url)}');
Metadata metadata = new Metadata.deserialize(
JSON.decode(Uri.decodeComponent('$encodedMetadata')));
RemoteListener.start(server, metadata, () => test.main);
}
''');
Completer<Iterable<RemoteTest>> completer = new Completer<Iterable<RemoteTest>>();
Completer<String> deathCompleter = new Completer();
Process process = await _startProcess(
listenerFile.path,
packageRoot: path.absolute(config.packageRoot)
);
Future cleanupTempDirectory() async {
if (tempDir == null)
return;
Directory dirToDelete = tempDir;
tempDir = null;
await dirToDelete.delete(recursive: true);
}
process.exitCode.then((int exitCode) async {
try {
info.server.close(force: true);
await cleanupTempDirectory();
String output = '';
if (exitCode < 0) {
// Abnormal termination (high bit of signed 8-bit exitCode is set)
switch (exitCode) {
case -0x0f: // ProcessSignal.SIGTERM
break; // we probably killed it ourselves
case -0x0b: // ProcessSignal.SIGSEGV
output += 'Segmentation fault in subprocess for: $mainPath\n';
break;
case -0x06: // ProcessSignal.SIGABRT
output += 'Aborted while running: $mainPath\n';
break;
default:
output += 'Unexpected exit code $exitCode from subprocess for: $mainPath\n';
}
}
String stdout = await process.stdout.transform(UTF8.decoder).join('\n');
String stderr = await process.stderr.transform(UTF8.decoder).join('\n');
if (stdout != '')
output += '\nstdout:\n$stdout';
if (stderr != '')
output += '\nstderr:\n$stderr';
if (!completer.isCompleted) {
if (output == '')
output = 'No output.';
completer.completeError(
new LoadException(mainPath, output),
new Trace.current()
);
} else {
if (kExpectAllTestsToCloseCleanly && output != '')
print('Unexpected failure after test claimed to pass:\n$output');
}
deathCompleter.complete(output);
} catch (e) {
// Throwing inside this block causes all kinds of hard-to-debug issues
// like stack overflows and hangs. So catch everything just in case.
print("exception while handling subprocess termination: $e");
}
});
JSONSocket socket = new JSONSocket(await info.socket, deathCompleter.future);
await cleanupTempDirectory();
StreamSubscription subscription;
subscription = socket.stream.listen((response) {
if (response["type"] == "print") {
print(response["line"]);
} else if (response["type"] == "loadException") {
process.kill(ProcessSignal.SIGTERM);
completer.completeError(
new LoadException(mainPath, response["message"]),
new Trace.current());
} else if (response["type"] == "error") {
process.kill(ProcessSignal.SIGTERM);
AsyncError asyncError = RemoteException.deserialize(response["error"]);
completer.completeError(
new LoadException(mainPath, asyncError.error),
asyncError.stackTrace);
} else {
assert(response["type"] == "success");
subscription.cancel();
completer.complete(response["tests"].map((test) {
var testMetadata = new Metadata.deserialize(test['metadata']);
return new RemoteTest(test['name'], testMetadata, socket, test['index']);
}));
}
});
Iterable<RemoteTest> entries = await completer.future;
return new RunnerSuite(
const VMEnvironment(),
new Group.root(entries, metadata: metadata),
path: mainPath,
platform: TestPlatform.vm,
os: currentOS,
onClose: () { process.kill(ProcessSignal.SIGTERM); }
);
}
// 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:collection';
import 'dart:convert';
import 'dart:io';
import 'dart:isolate';
import 'package:stack_trace/stack_trace.dart';
import 'package:test/src/backend/declarer.dart';
import 'package:test/src/backend/live_test.dart';
import 'package:test/src/backend/metadata.dart';
import 'package:test/src/backend/operating_system.dart';
import 'package:test/src/backend/suite.dart';
import 'package:test/src/backend/test.dart';
import 'package:test/src/backend/test_platform.dart';
import 'package:test/src/util/remote_exception.dart';
final OperatingSystem currentOS = (() {
var name = Platform.operatingSystem;
var os = OperatingSystem.findByIoName(name);
if (os != null) return os;
throw new UnsupportedError('Unsupported operating system "$name".');
})();
typedef AsyncFunction();
class RemoteListener {
RemoteListener._(this._suite, this._socket);
final Suite _suite;
final WebSocket _socket;
final Set<LiveTest> _liveTests = new HashSet<LiveTest>();
static Future start(String server, Metadata metadata, Function getMain()) async {
WebSocket socket = await WebSocket.connect(server);
// Capture any top-level errors (mostly lazy syntax errors, since other are
// caught below) and report them to the parent isolate. We set errors
// non-fatal because otherwise they'll be double-printed.
var errorPort = new ReceivePort();
Isolate.current.setErrorsFatal(false);
Isolate.current.addErrorListener(errorPort.sendPort);
errorPort.listen((message) {
// Masquerade as an IsolateSpawnException because that's what this would
// be if the error had been detected statically.
var error = new IsolateSpawnException(message[0]);
var stackTrace =
message[1] == null ? new Trace([]) : new Trace.parse(message[1]);
socket.add(JSON.encode({
"type": "error",
"error": RemoteException.serialize(error, stackTrace)
}));
});
var main;
try {
main = getMain();
} on NoSuchMethodError catch (_) {
_sendLoadException(socket, "No top-level main() function defined.");
return;
}
if (main is! Function) {
_sendLoadException(socket, "Top-level main getter is not a function.");
return;
} else if (main is! AsyncFunction) {
_sendLoadException(
socket, "Top-level main() function takes arguments.");
return;
}
Declarer declarer = new Declarer(metadata);
try {
await runZoned(() => new Future.sync(main), zoneValues: {
#test.declarer: declarer
}, zoneSpecification: new ZoneSpecification(print: (_, __, ___, line) {
socket.add(JSON.encode({"type": "print", "line": line}));
}));
} catch (error, stackTrace) {
socket.add(JSON.encode({
"type": "error",
"error": RemoteException.serialize(error, stackTrace)
}));
return;
}
Suite suite = new Suite(declarer.build(),
platform: TestPlatform.vm, os: currentOS);
new RemoteListener._(suite, socket)._listen();
}
static void _sendLoadException(WebSocket socket, String message) {
socket.add(JSON.encode({"type": "loadException", "message": message}));
}
void _send(data) {
_socket.add(JSON.encode(data));
}
void _listen() {
List tests = [];
for (var i = 0; i < _suite.group.entries.length; i++) {
// TODO(ianh): entries[] might return a Group instead of a Test. We don't
// currently support nested groups.
Test test = _suite.group.entries[i];
tests.add({
"name": test.name,
"metadata": test.metadata.serialize(),
"index": i,
});
}
_send({"type": "success", "tests": tests});
_socket.listen(_handleCommand);
}
void _handleCommand(String data) {
var message = JSON.decode(data);
if (message['command'] == 'run') {
// TODO(ianh): entries[] might return a Group instead of a Test. We don't
// currently support nested groups.
Test test = _suite.group.entries[message['index']];
LiveTest liveTest = test.load(_suite);
_liveTests.add(liveTest);
liveTest.onStateChange.listen((state) {
_send({
"type": "state-change",
"status": state.status.name,
"result": state.result.name
});
});
liveTest.onError.listen((asyncError) {
_send({
"type": "error",
"error": RemoteException.serialize(
asyncError.error,
asyncError.stackTrace
)
});
});
liveTest.onPrint.listen((line) {
_send({"type": "print", "line": line});
});
liveTest.run().then((_) {
_send({"type": "complete"});
_liveTests.remove(liveTest);
});
} else if (message['command'] == 'close') {
if (_liveTests.isNotEmpty)
print('closing with ${_liveTests.length} live tests');
for (LiveTest liveTest in _liveTests)
liveTest.close();
_liveTests.clear();
} else {
print('remote_listener.dart: ignoring command "${message["command"]}" from test harness');
}
}
}
// 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 'package:stack_trace/stack_trace.dart';
import 'package:test/src/backend/group.dart';
import 'package:test/src/backend/live_test.dart';
import 'package:test/src/backend/live_test_controller.dart';
import 'package:test/src/backend/metadata.dart';
import 'package:test/src/backend/operating_system.dart';
import 'package:test/src/backend/state.dart';
import 'package:test/src/backend/suite.dart';
import 'package:test/src/backend/test.dart';
import 'package:test/src/backend/test_platform.dart';
import 'package:test/src/util/remote_exception.dart';
import 'json_socket.dart';
class RemoteTest extends Test {
RemoteTest(this.name, this.metadata, this._socket, this._index);
final String name;
final Metadata metadata;
final JSONSocket _socket;
final int _index;
LiveTest load(Suite suite, { Iterable<Group> groups }) {
LiveTestController controller;
StreamSubscription subscription;
controller = new LiveTestController(suite, this, () async {
controller.setState(const State(Status.running, Result.success));
_socket.send({'command': 'run', 'index': _index});
subscription = _socket.stream.listen((message) {
if (message['type'] == 'error') {
AsyncError asyncError = RemoteException.deserialize(message['error']);
controller.addError(asyncError.error, asyncError.stackTrace);
} else if (message['type'] == 'state-change') {
controller.setState(
new State(
new Status.parse(message['status']),
new Result.parse(message['result'])));
} else if (message['type'] == 'print') {
controller.print(message['line']);
} else {
assert(message['type'] == 'complete');
subscription.cancel();
subscription = null;
controller.completer.complete();
}
});
_socket.unusualTermination.then((String message) {
if (subscription != null) {
controller.print('Unexpected subprocess termination: $message');
controller.addError(new Exception('Unexpected subprocess termination.'), new Trace.current());
controller.setState(new State(Status.complete, Result.error));
subscription.cancel();
subscription = null;
controller.completer.complete();
}
});
}, () async {
_socket.send({'command': 'close'});
if (subscription != null) {
subscription.cancel();
subscription = null;
}
}, groups: groups);
return controller.liveTest;
}
Test change({String name, Metadata metadata}) {
if (name == name && metadata == this.metadata) return this;
if (name == null) name = this.name;
if (metadata == null) metadata = this.metadata;
return new RemoteTest(name, metadata, _socket, _index);
}
// TODO(ianh): Implement this if we need it.
Test forPlatform(TestPlatform platform, {OperatingSystem os}) {
if (!metadata.testOn.evaluate(platform, os: os))
return null;
return new RemoteTest(
name,
metadata.forPlatform(platform, os: os),
_socket,
_index
);
}
}
......@@ -18,7 +18,7 @@ dependencies:
path: ^1.3.0
pub_semver: ^1.0.0
stack_trace: ^1.4.0
test: 0.12.6+1 # see note below
test: 0.12.11+1 # see note below
yaml: ^2.1.3
xml: ^2.4.1
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment