| // 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 'dart:convert'; | |
| import 'package:async/async.dart'; | |
| import 'package:stream_channel/stream_channel.dart'; | |
| import '../backend/remote_exception.dart'; | |
| import '../utils.dart'; | |
| import 'test_structure.dart' show addTearDown; | |
| /// A transformer that handles messages from the spawned isolate and ensures | |
| /// that messages sent to it are JSON-encodable. | |
| /// | |
| /// The spawned isolate sends three kinds of messages. Data messages are emitted | |
| /// as data events, error messages are emitted as error events, and print | |
| /// messages are printed using `print()`. | |
| // package:test will only send a `Map` across this channel, but users of | |
| // `hybridMain` can send any json encodeable type. | |
| final _transformer = StreamChannelTransformer<dynamic, dynamic>( | |
| StreamTransformer.fromHandlers( | |
| handleData: (message, sink) { | |
| switch (message['type'] as String) { | |
| case 'data': | |
| sink.add(message['data']); | |
| break; | |
| case 'print': | |
| print(message['line']); | |
| break; | |
| case 'error': | |
| var error = RemoteException.deserialize(message['error'] as Map); | |
| sink.addError(error.error, error.stackTrace); | |
| break; | |
| } | |
| }, | |
| ), | |
| StreamSinkTransformer.fromHandlers( | |
| handleData: (message, sink) { | |
| // This is called synchronously from the user's `Sink.add()` call, so if | |
| // [ensureJsonEncodable] throws here they'll get a helpful stack trace. | |
| ensureJsonEncodable(message); | |
| sink.add(message); | |
| }, | |
| ), | |
| ); | |
| /// Spawns a VM isolate for the given [uri], which may be a [Uri] or a [String]. | |
| /// | |
| /// This allows browser tests to spawn servers with which they can communicate | |
| /// to test client/server interactions. It can also be used by VM tests to | |
| /// easily spawn an isolate. | |
| /// | |
| /// The Dart file at [uri] must define a top-level `hybridMain()` function that | |
| /// takes a `StreamChannel` argument and, optionally, an `Object` argument to | |
| /// which [message] will be passed. Note that [message] must be JSON-encodable. | |
| /// For example: | |
| /// | |
| /// ```dart | |
| /// import "package:stream_channel/stream_channel.dart"; | |
| /// | |
| /// hybridMain(StreamChannel channel, Object message) { | |
| /// // ... | |
| /// } | |
| /// ``` | |
| /// | |
| /// If [uri] is relative, it will be interpreted relative to the `file:` URL for | |
| /// the test suite being executed. If it's root-relative (that is, if it begins | |
| /// with `/`) it will be interpreted relative to the root of the package (the | |
| /// directory that contains `pubspec.yaml`, *not* the `test/` directory). If | |
| /// it's a `package:` URL, it will be resolved using the current package's | |
| /// dependency constellation. | |
| /// | |
| /// Returns a [StreamChannel] that's connected to the channel passed to | |
| /// `hybridMain()`. Only JSON-encodable objects may be sent through this | |
| /// channel. If the channel is closed, the hybrid isolate is killed. If the | |
| /// isolate is killed, the channel's stream will emit a "done" event. | |
| /// | |
| /// Any unhandled errors loading or running the hybrid isolate will be emitted | |
| /// as errors over the channel's stream. Any calls to `print()` in the hybrid | |
| /// isolate will be printed as though they came from the test that created the | |
| /// isolate. | |
| /// | |
| /// Code in the hybrid isolate is not considered to be running in a test | |
| /// context, so it can't access test functions like `expect()` and | |
| /// `expectAsync()`. | |
| /// | |
| /// By default, the hybrid isolate is automatically killed when the test | |
| /// finishes running. If [stayAlive] is `true`, it won't be killed until the | |
| /// entire test suite finishes running. | |
| /// | |
| /// **Note**: If you use this API, be sure to add a dependency on the | |
| /// **`stream_channel` package, since you're using its API as well! | |
| StreamChannel spawnHybridUri( | |
| Object uri, { | |
| Object? message, | |
| bool stayAlive = false, | |
| }) { | |
| if (uri is String) { | |
| // Ensure that it can be parsed as a uri. | |
| Uri.parse(uri); | |
| } else if (uri is! Uri) { | |
| throw ArgumentError.value(uri, 'uri', 'must be a Uri or a String.'); | |
| } | |
| return _spawn(uri.toString(), message, stayAlive: stayAlive); | |
| } | |
| /// Spawns a VM isolate that runs the given [dartCode], which is loaded as the | |
| /// contents of a Dart library. | |
| /// | |
| /// This allows browser tests to spawn servers with which they can communicate | |
| /// to test client/server interactions. It can also be used by VM tests to | |
| /// easily spawn an isolate. | |
| /// | |
| /// The [dartCode] must define a top-level `hybridMain()` function that takes a | |
| /// `StreamChannel` argument and, optionally, an `Object` argument to which | |
| /// [message] will be passed. Note that [message] must be JSON-encodable. For | |
| /// example: | |
| /// | |
| /// ```dart | |
| /// import "package:stream_channel/stream_channel.dart"; | |
| /// | |
| /// hybridMain(StreamChannel channel, Object message) { | |
| /// // ... | |
| /// } | |
| /// ``` | |
| /// | |
| /// Returns a [StreamChannel] that's connected to the channel passed to | |
| /// `hybridMain()`. Only JSON-encodable objects may be sent through this | |
| /// channel. If the channel is closed, the hybrid isolate is killed. If the | |
| /// isolate is killed, the channel's stream will emit a "done" event. | |
| /// | |
| /// Any unhandled errors loading or running the hybrid isolate will be emitted | |
| /// as errors over the channel's stream. Any calls to `print()` in the hybrid | |
| /// isolate will be printed as though they came from the test that created the | |
| /// isolate. | |
| /// | |
| /// Code in the hybrid isolate is not considered to be running in a test | |
| /// context, so it can't access test functions like `expect()` and | |
| /// `expectAsync()`. | |
| /// | |
| /// By default, the hybrid isolate is automatically killed when the test | |
| /// finishes running. If [stayAlive] is `true`, it won't be killed until the | |
| /// entire test suite finishes running. | |
| /// | |
| /// **Note**: If you use this API, be sure to add a dependency on the | |
| /// **`stream_channel` package, since you're using its API as well! | |
| StreamChannel spawnHybridCode( | |
| String dartCode, { | |
| Object? message, | |
| bool stayAlive = false, | |
| }) { | |
| var uri = Uri.dataFromString( | |
| dartCode, | |
| encoding: utf8, | |
| mimeType: 'application/dart', | |
| ); | |
| return _spawn(uri.toString(), message, stayAlive: stayAlive); | |
| } | |
| /// Like [spawnHybridUri], but doesn't take [Uri] objects. | |
| StreamChannel _spawn(String uri, Object? message, {bool stayAlive = false}) { | |
| var channel = Zone.current[#test.runner.test_channel] as MultiChannel?; | |
| if (channel == null) { | |
| throw UnsupportedError( | |
| "Can't connect to the test runner.\n" | |
| 'spawnHybridUri() is currently only supported within "dart test".', | |
| ); | |
| } | |
| ensureJsonEncodable(message); | |
| var virtualChannel = channel.virtualChannel(); | |
| StreamChannel isolateChannel = virtualChannel; | |
| channel.sink.add({ | |
| 'type': 'spawn-hybrid-uri', | |
| 'url': uri, | |
| 'message': message, | |
| 'channel': virtualChannel.id, | |
| }); | |
| if (!stayAlive) { | |
| var disconnector = Disconnector<void>(); | |
| addTearDown(() => disconnector.disconnect()); | |
| isolateChannel = isolateChannel.transform(disconnector); | |
| } | |
| return isolateChannel.transform(_transformer); | |
| } |