| Index: lib/src/runner/remote_listener.dart
|
| diff --git a/lib/src/runner/remote_listener.dart b/lib/src/runner/remote_listener.dart
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..a041ccd35eb668d4c45f48eec3785d085746b496
|
| --- /dev/null
|
| +++ b/lib/src/runner/remote_listener.dart
|
| @@ -0,0 +1,191 @@
|
| +// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file
|
| +// for details. 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:stream_channel/stream_channel.dart';
|
| +
|
| +import '../backend/declarer.dart';
|
| +import '../backend/group.dart';
|
| +import '../backend/live_test.dart';
|
| +import '../backend/metadata.dart';
|
| +import '../backend/operating_system.dart';
|
| +import '../backend/suite.dart';
|
| +import '../backend/test.dart';
|
| +import '../backend/test_platform.dart';
|
| +import '../util/remote_exception.dart';
|
| +import '../utils.dart';
|
| +
|
| +class RemoteListener {
|
| + /// The test suite to run.
|
| + final Suite _suite;
|
| +
|
| + /// The zone to forward prints to, or `null` if prints shouldn't be forwarded.
|
| + final Zone _printZone;
|
| +
|
| + /// Extracts metadata about all the tests in the function returned by
|
| + /// [getMain] and returns a channel that will send information about them.
|
| + ///
|
| + /// The main function is wrapped in a closure so that we can handle it being
|
| + /// undefined here rather than in the generated code.
|
| + ///
|
| + /// Once that's done, this starts listening for commands about which tests to
|
| + /// run.
|
| + ///
|
| + /// If [hidePrints] is `true` (the default), calls to `print()` within this
|
| + /// suite will not be forwarded to the parent zone's print handler. However,
|
| + /// the caller may want them to be forwarded in (for example) a browser
|
| + /// context where they'll be visible in the development console.
|
| + static StreamChannel start(AsyncFunction getMain(), {bool hidePrints: true}) {
|
| + // This has to be synchronous to work around sdk#25745. Otherwise, there'll
|
| + // be an asynchronous pause before a syntax error notification is sent,
|
| + // which will cause the send to fail entirely.
|
| + var controller = new StreamChannelController(
|
| + allowForeignErrors: false, sync: true);
|
| + var channel = new MultiChannel(controller.local);
|
| +
|
| + var printZone = hidePrints ? null : Zone.current;
|
| + runZoned(() async {
|
| + var main;
|
| + try {
|
| + main = getMain();
|
| + } on NoSuchMethodError catch (_) {
|
| + _sendLoadException(channel, "No top-level main() function defined.");
|
| + return;
|
| + } catch (error, stackTrace) {
|
| + _sendError(channel, error, stackTrace);
|
| + return;
|
| + }
|
| +
|
| + if (main is! Function) {
|
| + _sendLoadException(channel, "Top-level main getter is not a function.");
|
| + return;
|
| + } else if (main is! AsyncFunction) {
|
| + _sendLoadException(
|
| + channel, "Top-level main() function takes arguments.");
|
| + return;
|
| + }
|
| +
|
| + var message = await channel.stream.first;
|
| + var metadata = new Metadata.deserialize(message['metadata']);
|
| + var declarer = new Declarer(metadata);
|
| + await declarer.declare(main);
|
| +
|
| + var os = message['os'] == null
|
| + ? null
|
| + : OperatingSystem.find(message['os']);
|
| + var platform = TestPlatform.find(message['platform']);
|
| + var suite = new Suite(declarer.build(), platform: platform, os: os);
|
| + new RemoteListener._(suite, printZone)._listen(channel);
|
| + }, onError: (error, stackTrace) {
|
| + _sendError(channel, error, stackTrace);
|
| + }, zoneSpecification: new ZoneSpecification(print: (_, __, ___, line) {
|
| + if (printZone != null) printZone.print(line);
|
| + channel.sink.add({"type": "print", "line": line});
|
| + }));
|
| +
|
| + return controller.foreign;
|
| + }
|
| +
|
| +
|
| + /// Sends a message over [channel] indicating that the tests failed to load.
|
| + ///
|
| + /// [message] should describe the failure.
|
| + static void _sendLoadException(StreamChannel channel, String message) {
|
| + channel.sink.add({"type": "loadException", "message": message});
|
| + }
|
| +
|
| + /// Sends a message over [channel] indicating an error from user code.
|
| + static void _sendError(StreamChannel channel, error, StackTrace stackTrace) {
|
| + channel.sink.add({
|
| + "type": "error",
|
| + "error": RemoteException.serialize(error, stackTrace)
|
| + });
|
| + }
|
| +
|
| + RemoteListener._(this._suite, this._printZone);
|
| +
|
| + /// Send information about [_suite] across [channel] and start listening for
|
| + /// commands to run the tests.
|
| + void _listen(MultiChannel channel) {
|
| + channel.sink.add({
|
| + "type": "success",
|
| + "root": _serializeGroup(channel, _suite.group, [])
|
| + });
|
| + }
|
| +
|
| + /// Serializes [group] into a JSON-safe map.
|
| + ///
|
| + /// [parents] lists the groups that contain [group].
|
| + Map _serializeGroup(MultiChannel channel, Group group,
|
| + Iterable<Group> parents) {
|
| + parents = parents.toList()..add(group);
|
| + return {
|
| + "type": "group",
|
| + "name": group.name,
|
| + "metadata": group.metadata.serialize(),
|
| + "setUpAll": _serializeTest(channel, group.setUpAll, parents),
|
| + "tearDownAll": _serializeTest(channel, group.tearDownAll, parents),
|
| + "entries": group.entries.map((entry) {
|
| + return entry is Group
|
| + ? _serializeGroup(channel, entry, parents)
|
| + : _serializeTest(channel, entry, parents);
|
| + }).toList()
|
| + };
|
| + }
|
| +
|
| + /// Serializes [test] into a JSON-safe map.
|
| + ///
|
| + /// [groups] lists the groups that contain [test]. Returns `null` if [test]
|
| + /// is `null`.
|
| + Map _serializeTest(MultiChannel channel, Test test, Iterable<Group> groups) {
|
| + if (test == null) return null;
|
| +
|
| + var testChannel = channel.virtualChannel();
|
| + testChannel.stream.listen((message) {
|
| + assert(message['command'] == 'run');
|
| + _runLiveTest(
|
| + test.load(_suite, groups: groups),
|
| + channel.virtualChannel(message['channel']));
|
| + });
|
| +
|
| + return {
|
| + "type": "test",
|
| + "name": test.name,
|
| + "metadata": test.metadata.serialize(),
|
| + "channel": testChannel.id
|
| + };
|
| + }
|
| +
|
| + /// Runs [liveTest] and sends the results across [channel].
|
| + void _runLiveTest(LiveTest liveTest, MultiChannel channel) {
|
| + channel.stream.listen((message) {
|
| + assert(message['command'] == 'close');
|
| + liveTest.close();
|
| + });
|
| +
|
| + liveTest.onStateChange.listen((state) {
|
| + channel.sink.add({
|
| + "type": "state-change",
|
| + "status": state.status.name,
|
| + "result": state.result.name
|
| + });
|
| + });
|
| +
|
| + liveTest.onError.listen((asyncError) {
|
| + channel.sink.add({
|
| + "type": "error",
|
| + "error": RemoteException.serialize(
|
| + asyncError.error, asyncError.stackTrace)
|
| + });
|
| + });
|
| +
|
| + liveTest.onPrint.listen((line) {
|
| + if (_printZone != null) _printZone.print(line);
|
| + channel.sink.add({"type": "print", "line": line});
|
| + });
|
| +
|
| + liveTest.run().then((_) => channel.sink.add({"type": "complete"}));
|
| + }
|
| +}
|
|
|