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 : import 'dart:convert';
7 :
8 : import 'package:async/async.dart';
9 : import 'package:path/path.dart' as p;
10 : import 'package:stream_channel/stream_channel.dart';
11 :
12 : import '../../test.dart';
13 : import '../backend/invoker.dart';
14 : import '../util/remote_exception.dart';
15 : import '../utils.dart';
16 :
17 : /// A transformer that handles messages from the spawned isolate and ensures
18 : /// that messages sent to it are JSON-encodable.
19 : ///
20 : /// The spawned isolate sends three kinds of messages. Data messages are emitted
21 : /// as data events, error messages are emitted as error events, and print
22 : /// messages are printed using `print()`.
23 : final _transformer = new StreamChannelTransformer<Object, Map>(
24 : new StreamTransformer.fromHandlers(handleData: (message, sink) {
25 : switch (message["type"]) {
26 : case "data":
27 : sink.add(message["data"]);
28 : break;
29 :
30 : case "print":
31 : print(message["line"]);
32 : break;
33 :
34 : case "error":
35 : var error = RemoteException.deserialize(message["error"]);
36 : sink.addError(error.error, error.stackTrace);
37 : break;
38 : }
39 : }), new StreamSinkTransformer.fromHandlers(handleData: (message, sink) {
40 : // This is called synchronously from the user's `Sink.add()` call, so if
41 : // [ensureJsonEncodable] throws here they'll get a helpful stack trace.
42 : ensureJsonEncodable(message);
43 : sink.add(message);
44 : }));
45 :
46 : /// Spawns a VM isolate for the given [uri], which may be a [Uri] or a [String].
47 : ///
48 : /// This allows browser tests to spawn servers with which they can communicate
49 : /// to test client/server interactions. It can also be used by VM tests to
50 : /// easily spawn an isolate.
51 : ///
52 : /// The Dart file at [uri] must define a top-level `hybridMain()` function that
53 : /// takes a `StreamChannel` argument and, optionally, an `Object` argument to
54 : /// which [message] will be passed. Note that [message] must be JSON-encodable.
55 : /// For example:
56 : ///
57 : /// ```dart
58 : /// import "package:stream_channel/stream_channel.dart";
59 : ///
60 : /// hybridMain(StreamChannel channel, Object message) {
61 : /// // ...
62 : /// }
63 : /// ```
64 : ///
65 : /// If [uri] is relative, it will be interpreted relative to the `file:` URL for
66 : /// the test suite being executed. If it's a `package:` URL, it will be resolved
67 : /// using the current package's dependency constellation.
68 : ///
69 : /// Returns a [StreamChannel] that's connected to the channel passed to
70 : /// `hybridMain()`. Only JSON-encodable objects may be sent through this
71 : /// channel. If the channel is closed, the hybrid isolate is killed. If the
72 : /// isolate is killed, the channel's stream will emit a "done" event.
73 : ///
74 : /// Any unhandled errors loading or running the hybrid isolate will be emitted
75 : /// as errors over the channel's stream. Any calls to `print()` in the hybrid
76 : /// isolate will be printed as though they came from the test that created the
77 : /// isolate.
78 : ///
79 : /// Code in the hybrid isolate is not considered to be running in a test
80 : /// context, so it can't access test functions like `expect()` and
81 : /// `expectAsync()`.
82 : ///
83 : /// By default, the hybrid isolate is automatically killed when the test
84 : /// finishes running. If [stayAlive] is `true`, it won't be killed until the
85 : /// entire test suite finishes running.
86 : ///
87 : /// **Note**: If you use this API, be sure to add a dependency on the
88 : /// **`stream_channel` package, since you're using its API as well!
89 : StreamChannel spawnHybridUri(uri, {Object message, bool stayAlive: false}) {
90 : Uri parsedUrl;
91 0 : if (uri is Uri) {
92 : parsedUrl = uri;
93 0 : } else if (uri is String) {
94 0 : parsedUrl = Uri.parse(uri);
95 : } else {
96 0 : throw new ArgumentError.value(uri, "uri", "must be a Uri or a String.");
97 : }
98 :
99 : String absoluteUri;
100 0 : if (parsedUrl.scheme.isEmpty) {
101 : // If we're running in a browser context, the working directory is already
102 : // relative to the test file, whereas on the VM the working directory is the
103 : // root of the package.
104 0 : if (p.style == p.Style.url) {
105 0 : absoluteUri = p.absolute(parsedUrl.toString());
106 : } else {
107 0 : var suitePath = Invoker.current.liveTest.suite.path;
108 0 : absoluteUri = p.url.join(
109 0 : p.url.dirname(p.toUri(p.absolute(suitePath)).toString()),
110 0 : parsedUrl.toString());
111 : }
112 : } else {
113 0 : absoluteUri = uri.toString();
114 : }
115 :
116 0 : return _spawn(absoluteUri, message, stayAlive: stayAlive);
117 : }
118 :
119 : /// Spawns a VM isolate that runs the given [dartCode], which is loaded as the
120 : /// contents of a Dart library.
121 : ///
122 : /// This allows browser tests to spawn servers with which they can communicate
123 : /// to test client/server interactions. It can also be used by VM tests to
124 : /// easily spawn an isolate.
125 : ///
126 : /// The [dartCode] must define a top-level `hybridMain()` function that takes a
127 : /// `StreamChannel` argument and, optionally, an `Object` argument to which
128 : /// [message] will be passed. Note that [message] must be JSON-encodable. For
129 : /// example:
130 : ///
131 : /// ```dart
132 : /// import "package:stream_channel/stream_channel.dart";
133 : ///
134 : /// hybridMain(StreamChannel channel, Object message) {
135 : /// // ...
136 : /// }
137 : /// ```
138 : ///
139 : /// Returns a [StreamChannel] that's connected to the channel passed to
140 : /// `hybridMain()`. Only JSON-encodable objects may be sent through this
141 : /// channel. If the channel is closed, the hybrid isolate is killed. If the
142 : /// isolate is killed, the channel's stream will emit a "done" event.
143 : ///
144 : /// Any unhandled errors loading or running the hybrid isolate will be emitted
145 : /// as errors over the channel's stream. Any calls to `print()` in the hybrid
146 : /// isolate will be printed as though they came from the test that created the
147 : /// isolate.
148 : ///
149 : /// Code in the hybrid isolate is not considered to be running in a test
150 : /// context, so it can't access test functions like `expect()` and
151 : /// `expectAsync()`.
152 : ///
153 : /// By default, the hybrid isolate is automatically killed when the test
154 : /// finishes running. If [stayAlive] is `true`, it won't be killed until the
155 : /// entire test suite finishes running.
156 : ///
157 : /// **Note**: If you use this API, be sure to add a dependency on the
158 : /// **`stream_channel` package, since you're using its API as well!
159 : StreamChannel spawnHybridCode(String dartCode,
160 : {Object message, bool stayAlive: false}) {
161 0 : var uri = new Uri.dataFromString(dartCode,
162 : encoding: UTF8, mimeType: 'application/dart');
163 0 : return _spawn(uri.toString(), message, stayAlive: stayAlive);
164 : }
165 :
166 : /// Like [spawnHybridUri], but doesn't take [Uri] objects and doesn't handle
167 : /// relative URLs.
168 : StreamChannel _spawn(String uri, Object message, {bool stayAlive: false}) {
169 0 : var channel = Zone.current[#test.runner.test_channel] as MultiChannel;
170 : if (channel == null) {
171 : // TODO(nweiz): Link to an issue tracking support when running the test file
172 : // directly.
173 0 : throw new UnsupportedError("Can't connect to the test runner.\n"
174 : 'spawnHybridUri() is currently only supported within "pub run test".');
175 : }
176 :
177 0 : ensureJsonEncodable(message);
178 :
179 0 : var isolateChannel = channel.virtualChannel();
180 0 : channel.sink.add({
181 : "type": "spawn-hybrid-uri",
182 : "url": uri,
183 : "message": message,
184 0 : "channel": isolateChannel.id
185 : });
186 :
187 : if (!stayAlive) {
188 0 : var disconnector = new Disconnector();
189 0 : addTearDown(() => disconnector.disconnect());
190 0 : isolateChannel = isolateChannel.transform(disconnector);
191 : }
192 :
193 0 : return isolateChannel.transform(_transformer);
194 : }
|