| // 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 'declarer.dart'; |
| import 'group.dart'; |
| import 'invoker.dart'; |
| import 'live_test.dart'; |
| import 'metadata.dart'; |
| import 'remote_exception.dart'; |
| import 'stack_trace_formatter.dart'; |
| import 'suite.dart'; |
| import 'suite_channel_manager.dart'; |
| import 'suite_platform.dart'; |
| import 'test.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<Object?> start(Function Function() getMain, |
| {bool hidePrints = true, |
| Future Function( |
| StreamChannel<Object?> Function(String name) suiteChannel)? |
| beforeLoad}) { |
| // Synchronous in order to allow `print` output to show up immediately, even |
| // if they are followed by long running synchronous work. |
| var controller = |
| StreamChannelController<Object?>(allowForeignErrors: false, sync: true); |
| var channel = MultiChannel<Object?>(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}); |
| }); |
| |
| final suiteChannelManager = SuiteChannelManager(); |
| StackTraceFormatter().asCurrent(() { |
| runZonedGuarded(() async { |
| Function? 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() function takes arguments.'); |
| return; |
| } |
| |
| var queue = StreamQueue(channel.stream); |
| var message = await queue.next as Map; |
| assert(message['type'] == 'initial'); |
| |
| queue.rest.cast<Map>().listen((message) { |
| if (message['type'] == 'close') { |
| controller.local.sink.close(); |
| return; |
| } |
| |
| assert(message['type'] == 'suiteChannel'); |
| suiteChannelManager.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, |
| // TODO: Change to non-nullable https://github.com/dart-lang/test/issues/1591 |
| allowDuplicateTestNames: |
| message['allowDuplicateTestNames'] as bool? ?? true, |
| ); |
| StackTraceFormatter.current!.configure( |
| except: _deserializeSet(message['foldTraceExcept'] as List), |
| only: _deserializeSet(message['foldTraceOnly'] as List)); |
| |
| if (beforeLoad != null) { |
| await beforeLoad(suiteChannelManager.connectOut); |
| } |
| |
| await declarer.declare(main); |
| |
| var suite = Suite(declarer.build(), |
| SuitePlatform.deserialize(message['platform'] as Object), |
| 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}); |
| }, (error, stackTrace) { |
| _sendError(channel, error, stackTrace, verboseChain); |
| }, zoneSpecification: spec); |
| }); |
| |
| return controller.foreign; |
| } |
| |
| /// Returns a [Set] from a JSON serialized list of strings, or `null` if the |
| /// list is empty or `null`. |
| 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, Object 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}); |
| } |
| } |