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..2da70f6800e6d359b4707da9c8c70b5114ad9c82 |
--- /dev/null |
+++ b/lib/src/runner/remote_listener.dart |
@@ -0,0 +1,193 @@ |
+// 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 'dart:developer'; |
kevmoo
2016/02/11 19:57:50
unused import
nweiz
2016/02/11 21:29:18
Done.
|
+ |
+import 'package:async/async.dart'; |
kevmoo
2016/02/11 19:57:50
unused import
nweiz
2016/02/11 21:29:18
Done.
|
+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"})); |
+ } |
+} |