diff --git a/packages/flutter/pubspec.yaml b/packages/flutter/pubspec.yaml index 15773e65b3..0aac449ab1 100644 --- a/packages/flutter/pubspec.yaml +++ b/packages/flutter/pubspec.yaml @@ -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 diff --git a/packages/flutter_test/pubspec.yaml b/packages/flutter_test/pubspec.yaml index 1db1803802..d5c71b7464 100644 --- a/packages/flutter_test/pubspec.yaml +++ b/packages/flutter_test/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_test dependencies: - test: 0.12.6+1 + test: 0.12.11+1 quiver: ^0.21.4 flutter: path: ../flutter diff --git a/packages/flutter_tools/lib/src/commands/test.dart b/packages/flutter_tools/lib/src/commands/test.dart index c900d29682..6aef766a9a 100644 --- a/packages/flutter_tools/lib/src/commands/test.dart +++ b/packages/flutter_tools/lib/src/commands/test.dart @@ -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'; diff --git a/packages/flutter_tools/lib/src/test/flutter_platform.dart b/packages/flutter_tools/lib/src/test/flutter_platform.dart new file mode 100644 index 0000000000..188dc673aa --- /dev/null +++ b/packages/flutter_tools/lib/src/test/flutter_platform.dart @@ -0,0 +1,133 @@ +// 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 socket; + final HttpServer server; + + _ServerInfo(this.server, this.url, this.socket); +} + +Future<_ServerInfo> _startServer() async { + HttpServer server = await HttpServer.bind(_kHost, 0); + Completer socket = new Completer(); + 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 _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 _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; + } + } +} diff --git a/packages/flutter_tools/lib/src/test/json_socket.dart b/packages/flutter_tools/lib/src/test/json_socket.dart deleted file mode 100644 index 2edc71db8c..0000000000 --- a/packages/flutter_tools/lib/src/test/json_socket.dart +++ /dev/null @@ -1,20 +0,0 @@ -// 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 unusualTermination; - - void send(dynamic data) { - _socket.add(JSON.encode(data)); - } -} diff --git a/packages/flutter_tools/lib/src/test/loader.dart b/packages/flutter_tools/lib/src/test/loader.dart deleted file mode 100644 index ca90c2fcad..0000000000 --- a/packages/flutter_tools/lib/src/test/loader.dart +++ /dev/null @@ -1,195 +0,0 @@ -// 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 socket; - final HttpServer server; - - _ServerInfo(this.server, this.url, this.socket); -} - -Future<_ServerInfo> _createServer() async { - HttpServer server = await HttpServer.bind(_kHost, 0); - Completer socket = new Completer(); - 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 _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 _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> completer = new Completer>(); - Completer 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 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); } - ); -} diff --git a/packages/flutter_tools/lib/src/test/remote_listener.dart b/packages/flutter_tools/lib/src/test/remote_listener.dart deleted file mode 100644 index d3a3f992f8..0000000000 --- a/packages/flutter_tools/lib/src/test/remote_listener.dart +++ /dev/null @@ -1,165 +0,0 @@ -// 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 _liveTests = new HashSet(); - - 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'); - } - } -} diff --git a/packages/flutter_tools/lib/src/test/remote_test.dart b/packages/flutter_tools/lib/src/test/remote_test.dart deleted file mode 100644 index 5a34fc5f88..0000000000 --- a/packages/flutter_tools/lib/src/test/remote_test.dart +++ /dev/null @@ -1,96 +0,0 @@ -// 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 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 - ); - } -} diff --git a/packages/flutter_tools/pubspec.yaml b/packages/flutter_tools/pubspec.yaml index e4ef1cdae2..d095be5564 100644 --- a/packages/flutter_tools/pubspec.yaml +++ b/packages/flutter_tools/pubspec.yaml @@ -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