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:stream_channel/stream_channel.dart'; 10 : 11 : import '../backend/remote_exception.dart'; 12 : import '../utils.dart'; 13 : import 'test_structure.dart' show addTearDown; 14 : 15 : /// A transformer that handles messages from the spawned isolate and ensures 16 : /// that messages sent to it are JSON-encodable. 17 : /// 18 : /// The spawned isolate sends three kinds of messages. Data messages are emitted 19 : /// as data events, error messages are emitted as error events, and print 20 : /// messages are printed using `print()`. 21 : // package:test will only send a `Map` across this channel, but users of 22 : // `hybridMain` can send any json encodeable type. 23 0 : final _transformer = StreamChannelTransformer<dynamic, dynamic>( 24 : StreamTransformer.fromHandlers(handleData: (message, sink) { 25 : switch (message['type'] as String) { 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 : }), 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 root-relative (that is, if it begins 67 : /// with `/`) it will be interpreted relative to the root of the package (the 68 : /// directory that contains `pubspec.yaml`, *not* the `test/` directory). If 69 : /// it's a `package:` URL, it will be resolved using the current package's 70 : /// dependency constellation. 71 : /// 72 : /// Returns a [StreamChannel] that's connected to the channel passed to 73 : /// `hybridMain()`. Only JSON-encodable objects may be sent through this 74 : /// channel. If the channel is closed, the hybrid isolate is killed. If the 75 : /// isolate is killed, the channel's stream will emit a "done" event. 76 : /// 77 : /// Any unhandled errors loading or running the hybrid isolate will be emitted 78 : /// as errors over the channel's stream. Any calls to `print()` in the hybrid 79 : /// isolate will be printed as though they came from the test that created the 80 : /// isolate. 81 : /// 82 : /// Code in the hybrid isolate is not considered to be running in a test 83 : /// context, so it can't access test functions like `expect()` and 84 : /// `expectAsync()`. 85 : /// 86 : /// By default, the hybrid isolate is automatically killed when the test 87 : /// finishes running. If [stayAlive] is `true`, it won't be killed until the 88 : /// entire test suite finishes running. 89 : /// 90 : /// **Note**: If you use this API, be sure to add a dependency on the 91 : /// **`stream_channel` package, since you're using its API as well! 92 0 : StreamChannel spawnHybridUri(uri, {Object? message, bool stayAlive = false}) { 93 0 : if (uri is String) { 94 : // Ensure that it can be parsed as a uri. 95 0 : Uri.parse(uri); 96 0 : } else if (uri is! Uri) { 97 0 : throw ArgumentError.value(uri, 'uri', 'must be a Uri or a String.'); 98 : } 99 0 : return _spawn(uri.toString(), message, stayAlive: stayAlive); 100 : } 101 : 102 : /// Spawns a VM isolate that runs the given [dartCode], which is loaded as the 103 : /// contents of a Dart library. 104 : /// 105 : /// This allows browser tests to spawn servers with which they can communicate 106 : /// to test client/server interactions. It can also be used by VM tests to 107 : /// easily spawn an isolate. 108 : /// 109 : /// The [dartCode] must define a top-level `hybridMain()` function that takes a 110 : /// `StreamChannel` argument and, optionally, an `Object` argument to which 111 : /// [message] will be passed. Note that [message] must be JSON-encodable. For 112 : /// example: 113 : /// 114 : /// ```dart 115 : /// import "package:stream_channel/stream_channel.dart"; 116 : /// 117 : /// hybridMain(StreamChannel channel, Object message) { 118 : /// // ... 119 : /// } 120 : /// ``` 121 : /// 122 : /// Returns a [StreamChannel] that's connected to the channel passed to 123 : /// `hybridMain()`. Only JSON-encodable objects may be sent through this 124 : /// channel. If the channel is closed, the hybrid isolate is killed. If the 125 : /// isolate is killed, the channel's stream will emit a "done" event. 126 : /// 127 : /// Any unhandled errors loading or running the hybrid isolate will be emitted 128 : /// as errors over the channel's stream. Any calls to `print()` in the hybrid 129 : /// isolate will be printed as though they came from the test that created the 130 : /// isolate. 131 : /// 132 : /// Code in the hybrid isolate is not considered to be running in a test 133 : /// context, so it can't access test functions like `expect()` and 134 : /// `expectAsync()`. 135 : /// 136 : /// By default, the hybrid isolate is automatically killed when the test 137 : /// finishes running. If [stayAlive] is `true`, it won't be killed until the 138 : /// entire test suite finishes running. 139 : /// 140 : /// **Note**: If you use this API, be sure to add a dependency on the 141 : /// **`stream_channel` package, since you're using its API as well! 142 0 : StreamChannel spawnHybridCode(String dartCode, 143 : {Object? message, bool stayAlive = false}) { 144 0 : var uri = Uri.dataFromString(dartCode, 145 : encoding: utf8, mimeType: 'application/dart'); 146 0 : return _spawn(uri.toString(), message, stayAlive: stayAlive); 147 : } 148 : 149 : /// Like [spawnHybridUri], but doesn't take [Uri] objects. 150 0 : StreamChannel _spawn(String uri, Object? message, {bool stayAlive = false}) { 151 0 : var channel = Zone.current[#test.runner.test_channel] as MultiChannel?; 152 : if (channel == null) { 153 0 : throw UnsupportedError("Can't connect to the test runner.\n" 154 : 'spawnHybridUri() is currently only supported within "dart test".'); 155 : } 156 : 157 0 : ensureJsonEncodable(message); 158 : 159 0 : var virtualChannel = channel.virtualChannel(); 160 : StreamChannel isolateChannel = virtualChannel; 161 0 : channel.sink.add({ 162 : 'type': 'spawn-hybrid-uri', 163 : 'url': uri, 164 : 'message': message, 165 0 : 'channel': virtualChannel.id 166 : }); 167 : 168 : if (!stayAlive) { 169 0 : var disconnector = Disconnector(); 170 0 : addTearDown(() => disconnector.disconnect()); 171 0 : isolateChannel = isolateChannel.transform(disconnector); 172 : } 173 : 174 0 : return isolateChannel.transform(_transformer); 175 : }