| // 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:async/async.dart'; |
| import 'package:stream_channel/stream_channel.dart'; |
| import 'package:term_glyph/term_glyph.dart' as glyph; |
| |
| import 'package:test_api/src/backend/declarer.dart'; // ignore: implementation_imports |
| import 'package:test_api/src/backend/group.dart'; // ignore: implementation_imports |
| import 'package:test_api/src/backend/invoker.dart'; // ignore: implementation_imports |
| import 'package:test_api/src/backend/live_test.dart'; // ignore: implementation_imports |
| import 'package:test_api/src/backend/metadata.dart'; // ignore: implementation_imports |
| import 'package:test_api/src/backend/stack_trace_formatter.dart'; // ignore: implementation_imports |
| import 'package:test_api/src/backend/suite.dart'; // ignore: implementation_imports |
| import 'package:test_api/src/backend/suite_platform.dart'; // ignore: implementation_imports |
| import 'package:test_api/src/backend/test.dart'; // ignore: implementation_imports |
| import 'package:test_api/src/util/remote_exception.dart'; // ignore: implementation_imports |
| |
| import 'suite_channel_manager.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. |
| /// |
| /// If [beforeLoad] is passed, it's called before the tests have been declared |
| /// for this worker. |
| static StreamChannel start(Function Function() getMain, |
| {bool hidePrints = true, Future Function() beforeLoad}) { |
| // 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 = |
| StreamChannelController<Object>(allowForeignErrors: false, sync: true); |
| var channel = MultiChannel(controller.local); |
| |
| var verboseChain = true; |
| |
| var printZone = hidePrints ? null : Zone.current; |
| var spec = ZoneSpecification(print: (_, __, ___, line) { |
| if (printZone != null) printZone.print(line); |
| channel.sink.add({'type': 'print', 'line': line}); |
| }); |
| |
| // Work-around for https://github.com/dart-lang/sdk/issues/32556. Remove |
| // once fixed. |
| Stream.fromIterable([]).listen((_) {}).cancel(); |
| |
| SuiteChannelManager().asCurrent(() { |
| StackTraceFormatter().asCurrent(() { |
| runZoned(() async { |
| dynamic main; |
| try { |
| main = getMain(); |
| } on NoSuchMethodError catch (_) { |
| _sendLoadException( |
| channel, 'No top-level main() function defined.'); |
| return; |
| } catch (error, stackTrace) { |
| _sendError(channel, error, stackTrace, verboseChain); |
| return; |
| } |
| |
| if (main is! Function) { |
| _sendLoadException( |
| channel, 'Top-level main getter is not a function.'); |
| return; |
| } else if (main is! Function()) { |
| _sendLoadException( |
| channel, 'Top-level main() function takes arguments.'); |
| return; |
| } |
| |
| var queue = StreamQueue(channel.stream); |
| var message = await queue.next; |
| assert(message['type'] == 'initial'); |
| |
| queue.rest.listen((message) { |
| if (message['type'] == 'close') { |
| controller.local.sink.close(); |
| return; |
| } |
| |
| assert(message['type'] == 'suiteChannel'); |
| SuiteChannelManager.current.connectIn(message['name'] as String, |
| channel.virtualChannel(message['id'] as int)); |
| }); |
| |
| if ((message['asciiGlyphs'] as bool) ?? false) glyph.ascii = true; |
| var metadata = Metadata.deserialize(message['metadata']); |
| verboseChain = metadata.verboseTrace; |
| var declarer = Declarer( |
| metadata: metadata, |
| platformVariables: |
| Set.from(message['platformVariables'] as Iterable), |
| collectTraces: message['collectTraces'] as bool, |
| noRetry: message['noRetry'] as bool); |
| |
| StackTraceFormatter.current.configure( |
| except: _deserializeSet(message['foldTraceExcept'] as List), |
| only: _deserializeSet(message['foldTraceOnly'] as List)); |
| |
| if (beforeLoad != null) await beforeLoad(); |
| |
| // TODO - is this await necessary? |
| await (declarer.declare(main as Function()) as dynamic); |
| |
| var suite = Suite( |
| declarer.build(), SuitePlatform.deserialize(message['platform']), |
| path: message['path'] as String); |
| |
| runZoned(() { |
| Invoker.guard( |
| () => RemoteListener._(suite, printZone)._listen(channel)); |
| }, |
| // Make the declarer visible to running tests so that they'll throw |
| // useful errors when calling `test()` and `group()` within a test, |
| // and so they can add to the declarer's `tearDownAll()` list. |
| zoneValues: {#test.declarer: declarer}); |
| }, onError: (error, StackTrace stackTrace) { |
| _sendError(channel, error, stackTrace, verboseChain); |
| }, zoneSpecification: spec); |
| }); |
| }); |
| |
| return controller.foreign; |
| } |
| |
| /// Returns a [Set] from a JSON serialized list of strings. |
| static Set<String> _deserializeSet(List list) { |
| if (list == null) return null; |
| if (list.isEmpty) return null; |
| return Set.from(list); |
| } |
| |
| /// 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, bool verboseChain) { |
| channel.sink.add({ |
| 'type': 'error', |
| 'error': RemoteException.serialize( |
| error, |
| StackTraceFormatter.current |
| .formatStackTrace(stackTrace, verbose: verboseChain)) |
| }); |
| } |
| |
| 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(), |
| 'trace': group.trace?.toString(), |
| '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 as Test, 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'] as int)); |
| }); |
| |
| return { |
| 'type': 'test', |
| 'name': test.name, |
| 'metadata': test.metadata.serialize(), |
| 'trace': test.trace?.toString(), |
| '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, |
| StackTraceFormatter.current.formatStackTrace(asyncError.stackTrace, |
| verbose: liveTest.test.metadata.verboseTrace)) |
| }); |
| }); |
| |
| liveTest.onMessage.listen((message) { |
| if (_printZone != null) _printZone.print(message.text); |
| channel.sink.add({ |
| 'type': 'message', |
| 'message-type': message.type.name, |
| 'text': message.text |
| }); |
| }); |
| |
| runZoned(() { |
| liveTest.run().then((_) => channel.sink.add({'type': 'complete'})); |
| }, zoneValues: {#test.runner.test_channel: channel}); |
| } |
| } |