Line data Source code
1 : // Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file
2 : // for details. All rights reserved. Use of this source code is governed by a
3 : // BSD-style license that can be found in the LICENSE file.
4 :
5 : import 'dart:async';
6 :
7 : import 'package:async/async.dart';
8 : import 'package:stream_channel/stream_channel.dart';
9 : import 'package:term_glyph/term_glyph.dart' as glyph;
10 :
11 : import 'declarer.dart';
12 : import 'group.dart';
13 : import 'invoker.dart';
14 : import 'live_test.dart';
15 : import 'metadata.dart';
16 : import 'remote_exception.dart';
17 : import 'stack_trace_formatter.dart';
18 : import 'suite.dart';
19 : import 'suite_channel_manager.dart';
20 : import 'suite_platform.dart';
21 : import 'test.dart';
22 :
23 : class RemoteListener {
24 : /// The test suite to run.
25 : final Suite _suite;
26 :
27 : /// The zone to forward prints to, or `null` if prints shouldn't be forwarded.
28 : final Zone? _printZone;
29 :
30 : /// Extracts metadata about all the tests in the function returned by
31 : /// [getMain] and returns a channel that will send information about them.
32 : ///
33 : /// The main function is wrapped in a closure so that we can handle it being
34 : /// undefined here rather than in the generated code.
35 : ///
36 : /// Once that's done, this starts listening for commands about which tests to
37 : /// run.
38 : ///
39 : /// If [hidePrints] is `true` (the default), calls to `print()` within this
40 : /// suite will not be forwarded to the parent zone's print handler. However,
41 : /// the caller may want them to be forwarded in (for example) a browser
42 : /// context where they'll be visible in the development console.
43 : ///
44 : /// If [beforeLoad] is passed, it's called before the tests have been declared
45 : /// for this worker.
46 11 : static StreamChannel<Object?> start(Function Function() getMain,
47 : {bool hidePrints = true,
48 : Future Function(
49 : StreamChannel<Object?> Function(String name) suiteChannel)?
50 : beforeLoad}) {
51 : // Synchronous in order to allow `print` output to show up immediately, even
52 : // if they are followed by long running synchronous work.
53 : var controller =
54 11 : StreamChannelController<Object?>(allowForeignErrors: false, sync: true);
55 22 : var channel = MultiChannel<Object?>(controller.local);
56 :
57 : var verboseChain = true;
58 :
59 0 : var printZone = hidePrints ? null : Zone.current;
60 11 : var spec = ZoneSpecification(print: (_, __, ___, line) {
61 0 : if (printZone != null) printZone.print(line);
62 0 : channel.sink.add({'type': 'print', 'line': line});
63 : });
64 :
65 11 : final suiteChannelManager = SuiteChannelManager();
66 33 : StackTraceFormatter().asCurrent(() {
67 22 : runZonedGuarded(() async {
68 : Function? main;
69 : try {
70 : main = getMain();
71 0 : } on NoSuchMethodError catch (_) {
72 0 : _sendLoadException(channel, 'No top-level main() function defined.');
73 : return;
74 : } catch (error, stackTrace) {
75 0 : _sendError(channel, error, stackTrace, verboseChain);
76 : return;
77 : }
78 :
79 11 : if (main is! Function()) {
80 0 : _sendLoadException(
81 : channel, 'Top-level main() function takes arguments.');
82 : return;
83 : }
84 :
85 22 : var queue = StreamQueue(channel.stream);
86 22 : var message = await queue.next as Map;
87 22 : assert(message['type'] == 'initial');
88 :
89 33 : queue.rest.cast<Map>().listen((message) {
90 0 : if (message['type'] == 'close') {
91 0 : controller.local.sink.close();
92 : return;
93 : }
94 :
95 0 : assert(message['type'] == 'suiteChannel');
96 0 : suiteChannelManager.connectIn(message['name'] as String,
97 0 : channel.virtualChannel(message['id'] as int));
98 : });
99 :
100 11 : if ((message['asciiGlyphs'] as bool?) ?? false) glyph.ascii = true;
101 22 : var metadata = Metadata.deserialize(message['metadata']);
102 11 : verboseChain = metadata.verboseTrace;
103 11 : var declarer = Declarer(
104 : metadata: metadata,
105 22 : platformVariables: Set.from(message['platformVariables'] as Iterable),
106 11 : collectTraces: message['collectTraces'] as bool,
107 11 : noRetry: message['noRetry'] as bool,
108 : // TODO: Change to non-nullable https://github.com/dart-lang/test/issues/1591
109 : allowDuplicateTestNames:
110 11 : message['allowDuplicateTestNames'] as bool? ?? true,
111 : );
112 22 : StackTraceFormatter.current!.configure(
113 22 : except: _deserializeSet(message['foldTraceExcept'] as List),
114 22 : only: _deserializeSet(message['foldTraceOnly'] as List));
115 :
116 : if (beforeLoad != null) {
117 0 : await beforeLoad(suiteChannelManager.connectOut);
118 : }
119 :
120 22 : await declarer.declare(main);
121 :
122 22 : var suite = Suite(declarer.build(),
123 22 : SuitePlatform.deserialize(message['platform'] as Object),
124 11 : path: message['path'] as String);
125 :
126 22 : runZoned(() {
127 11 : Invoker.guard(
128 33 : () => RemoteListener._(suite, printZone)._listen(channel));
129 : },
130 : // Make the declarer visible to running tests so that they'll throw
131 : // useful errors when calling `test()` and `group()` within a test,
132 : // and so they can add to the declarer's `tearDownAll()` list.
133 11 : zoneValues: {#test.declarer: declarer});
134 0 : }, (error, stackTrace) {
135 0 : _sendError(channel, error, stackTrace, verboseChain);
136 : }, zoneSpecification: spec);
137 : });
138 :
139 11 : return controller.foreign;
140 : }
141 :
142 : /// Returns a [Set] from a JSON serialized list of strings, or `null` if the
143 : /// list is empty or `null`.
144 11 : static Set<String>? _deserializeSet(List? list) {
145 : if (list == null) return null;
146 11 : if (list.isEmpty) return null;
147 0 : return Set.from(list);
148 : }
149 :
150 : /// Sends a message over [channel] indicating that the tests failed to load.
151 : ///
152 : /// [message] should describe the failure.
153 0 : static void _sendLoadException(StreamChannel channel, String message) {
154 0 : channel.sink.add({'type': 'loadException', 'message': message});
155 : }
156 :
157 : /// Sends a message over [channel] indicating an error from user code.
158 0 : static void _sendError(StreamChannel channel, Object error,
159 : StackTrace stackTrace, bool verboseChain) {
160 0 : channel.sink.add({
161 : 'type': 'error',
162 0 : 'error': RemoteException.serialize(
163 : error,
164 0 : StackTraceFormatter.current!
165 0 : .formatStackTrace(stackTrace, verbose: verboseChain))
166 : });
167 : }
168 :
169 11 : RemoteListener._(this._suite, this._printZone);
170 :
171 : /// Send information about [_suite] across [channel] and start listening for
172 : /// commands to run the tests.
173 11 : void _listen(MultiChannel channel) {
174 33 : channel.sink.add({
175 : 'type': 'success',
176 44 : 'root': _serializeGroup(channel, _suite.group, [])
177 : });
178 : }
179 :
180 : /// Serializes [group] into a JSON-safe map.
181 : ///
182 : /// [parents] lists the groups that contain [group].
183 11 : Map _serializeGroup(
184 : MultiChannel channel, Group group, Iterable<Group> parents) {
185 22 : parents = parents.toList()..add(group);
186 11 : return {
187 : 'type': 'group',
188 11 : 'name': group.name,
189 22 : 'metadata': group.metadata.serialize(),
190 11 : 'trace': group.trace == null
191 : ? null
192 2 : : StackTraceFormatter.current
193 4 : ?.formatStackTrace(group.trace!)
194 2 : .toString() ??
195 0 : group.trace?.toString(),
196 22 : 'setUpAll': _serializeTest(channel, group.setUpAll, parents),
197 22 : 'tearDownAll': _serializeTest(channel, group.tearDownAll, parents),
198 33 : 'entries': group.entries.map((entry) {
199 11 : return entry is Group
200 2 : ? _serializeGroup(channel, entry, parents)
201 11 : : _serializeTest(channel, entry as Test, parents);
202 11 : }).toList()
203 : };
204 : }
205 :
206 : /// Serializes [test] into a JSON-safe map.
207 : ///
208 : /// [groups] lists the groups that contain [test]. Returns `null` if [test]
209 : /// is `null`.
210 11 : Map? _serializeTest(
211 : MultiChannel channel, Test? test, Iterable<Group>? groups) {
212 : if (test == null) return null;
213 :
214 11 : var testChannel = channel.virtualChannel();
215 33 : testChannel.stream.listen((message) {
216 22 : assert(message['command'] == 'run');
217 33 : _runLiveTest(test.load(_suite, groups: groups),
218 22 : channel.virtualChannel(message['channel'] as int));
219 : });
220 :
221 11 : return {
222 : 'type': 'test',
223 11 : 'name': test.name,
224 22 : 'metadata': test.metadata.serialize(),
225 11 : 'trace': test.trace == null
226 : ? null
227 11 : : StackTraceFormatter.current
228 22 : ?.formatStackTrace(test.trace!)
229 11 : .toString() ??
230 0 : test.trace?.toString(),
231 11 : 'channel': testChannel.id
232 : };
233 : }
234 :
235 : /// Runs [liveTest] and sends the results across [channel].
236 11 : void _runLiveTest(LiveTest liveTest, MultiChannel channel) {
237 22 : channel.stream.listen((message) {
238 0 : assert(message['command'] == 'close');
239 0 : liveTest.close();
240 : });
241 :
242 33 : liveTest.onStateChange.listen((state) {
243 33 : channel.sink.add({
244 : 'type': 'state-change',
245 22 : 'status': state.status.name,
246 22 : 'result': state.result.name
247 : });
248 : });
249 :
250 22 : liveTest.onError.listen((asyncError) {
251 0 : channel.sink.add({
252 : 'type': 'error',
253 0 : 'error': RemoteException.serialize(
254 0 : asyncError.error,
255 0 : StackTraceFormatter.current!.formatStackTrace(asyncError.stackTrace,
256 0 : verbose: liveTest.test.metadata.verboseTrace))
257 : });
258 : });
259 :
260 22 : liveTest.onMessage.listen((message) {
261 0 : if (_printZone != null) _printZone!.print(message.text);
262 0 : channel.sink.add({
263 : 'type': 'message',
264 0 : 'message-type': message.type.name,
265 0 : 'text': message.text
266 : });
267 : });
268 :
269 22 : runZoned(() {
270 66 : liveTest.run().then((_) => channel.sink.add({'type': 'complete'}));
271 11 : }, zoneValues: {#test.runner.test_channel: channel});
272 : }
273 : }
|