blob: 6b6de3cf41d7223b0822621e3f42de42a7d17c36 [file] [log] [blame]
// Copyright (c) 2022, 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:path/path.dart' as p;
import 'package:stream_channel/stream_channel.dart';
import 'package:test/test.dart';
void main() {
group('spawnHybridUri():', () {
test('loads a file in a separate isolate connected via StreamChannel',
() async {
expect(spawnHybridUri('util/emits_numbers.dart').stream.toList(),
completion(equals([1, 2, 3])));
});
test('resolves root-relative URIs relative to the package root', () async {
expect(spawnHybridUri('/test/util/emits_numbers.dart').stream.toList(),
completion(equals([1, 2, 3])));
});
test('supports Uri objects', () async {
expect(
spawnHybridUri(Uri.parse('util/emits_numbers.dart')).stream.toList(),
completion(equals([1, 2, 3])));
});
test('supports package: uris referencing the root package', () async {
expect(
spawnHybridUri(Uri.parse('package:spawn_hybrid/emits_numbers.dart'))
.stream
.toList(),
completion(equals([1, 2, 3])));
});
test('supports package: uris referencing dependency packages', () async {
expect(
spawnHybridUri(Uri.parse('package:other_package/emits_numbers.dart'))
.stream
.toList(),
completion(equals([1, 2, 3])));
});
test('rejects non-String, non-Uri objects', () {
expect(() => spawnHybridUri(123), throwsArgumentError);
});
test('passes a message to the hybrid isolate', () async {
expect(
spawnHybridUri('util/echos_message.dart', message: 123).stream.first,
completion(equals(123)));
expect(
spawnHybridUri('util/echos_message.dart', message: 'wow')
.stream
.first,
completion(equals('wow')));
});
test('emits an error from the stream channel if the isolate fails to load',
() {
expect(spawnHybridUri('non existent file').stream.first,
throwsA(TypeMatcher<Exception>()));
});
});
group('spawnHybridCode()', () {
test('loads the code in a separate isolate connected via StreamChannel',
() {
expect(spawnHybridCode('''
import "package:stream_channel/stream_channel.dart";
void hybridMain(StreamChannel channel) {
channel.sink..add(1)..add(2)..add(3)..close();
}
''').stream.toList(), completion(equals([1, 2, 3])));
});
test('allows a first parameter with type StreamChannel<Object?>', () {
expect(spawnHybridCode('''
import "package:stream_channel/stream_channel.dart";
void hybridMain(StreamChannel<Object?> channel) {
channel.sink..add(1)..add(2)..add(null)..close();
}
''').stream.toList(), completion(equals([1, 2, null])));
});
test('gives a good error when the StreamChannel type is not supported', () {
expect(
spawnHybridCode('''
import "package:stream_channel/stream_channel.dart";
void hybridMain(StreamChannel<Object> channel) {
channel.sink..add(1)..add(2)..add(3)..close();
}
''').stream,
emitsError(isA<Exception>().having(
(e) => e.toString(),
'toString',
contains(
'The first parameter to the top-level hybridMain() must be a '
'StreamChannel<dynamic> or StreamChannel<Object?>. More specific '
'types such as StreamChannel<Object> are not supported.'))));
});
test('can use dart:io even when run from a browser', () async {
var path = p.join('test', 'hybrid_test.dart');
expect(spawnHybridCode("""
import 'dart:io';
import 'package:stream_channel/stream_channel.dart';
void hybridMain(StreamChannel channel) {
channel.sink
..add(File(r"$path").readAsStringSync())
..close();
}
""").stream.first, completion(contains('hybrid emits numbers')));
}, testOn: 'browser');
test('forwards data from the test to the hybrid isolate', () async {
var channel = spawnHybridCode('''
import "package:stream_channel/stream_channel.dart";
void hybridMain(StreamChannel channel) {
channel.stream.listen((num) {
channel.sink.add(num + 1);
});
}
''');
channel.sink
..add(1)
..add(2)
..add(3);
expect(channel.stream.take(3).toList(), completion(equals([2, 3, 4])));
});
test('passes an initial message to the hybrid isolate', () {
var code = '''
import "package:stream_channel/stream_channel.dart";
void hybridMain(StreamChannel channel, Object message) {
channel.sink..add(message)..close();
}
''';
expect(spawnHybridCode(code, message: [1, 2, 3]).stream.first,
completion(equals([1, 2, 3])));
expect(spawnHybridCode(code, message: {'a': 'b'}).stream.first,
completion(equals({'a': 'b'})));
});
test('allows the hybrid isolate to send errors across the stream channel',
() {
var channel = spawnHybridCode('''
import "package:stack_trace/stack_trace.dart";
import "package:stream_channel/stream_channel.dart";
void hybridMain(StreamChannel channel) {
channel.sink.addError("oh no!", Trace.current());
}
''');
channel.stream.listen(null, onError: expectAsync2((error, stackTrace) {
expect(error.toString(), equals('oh no!'));
expect(stackTrace.toString(), contains('hybridMain'));
}));
});
test('sends an unhandled synchronous error across the stream channel', () {
var channel = spawnHybridCode('''
import "package:stream_channel/stream_channel.dart";
void hybridMain(StreamChannel channel) {
throw "oh no!";
}
''');
channel.stream.listen(null, onError: expectAsync2((error, stackTrace) {
expect(error.toString(), equals('oh no!'));
expect(stackTrace.toString(), contains('hybridMain'));
}));
});
test('sends an unhandled asynchronous error across the stream channel', () {
var channel = spawnHybridCode('''
import 'dart:async';
import "package:stream_channel/stream_channel.dart";
void hybridMain(StreamChannel channel) {
scheduleMicrotask(() {
throw "oh no!";
});
}
''');
channel.stream.listen(null, onError: expectAsync2((error, stackTrace) {
expect(error.toString(), equals('oh no!'));
expect(stackTrace.toString(), contains('hybridMain'));
}));
});
test('deserializes TestFailures as TestFailures', () {
var channel = spawnHybridCode('''
import "package:stream_channel/stream_channel.dart";
import "package:test/test.dart";
void hybridMain(StreamChannel channel) {
throw TestFailure("oh no!");
}
''');
expect(channel.stream.first, throwsA(TypeMatcher<TestFailure>()));
});
test('gracefully handles an unserializable message in the VM', () {
var channel = spawnHybridCode('''
import "package:stream_channel/stream_channel.dart";
void hybridMain(StreamChannel channel) {}
''');
expect(() => channel.sink.add([].iterator), throwsArgumentError);
});
test('gracefully handles an unserializable message in the browser',
() async {
var channel = spawnHybridCode('''
import 'package:stream_channel/stream_channel.dart';
void hybridMain(StreamChannel channel) {}
''');
expect(() => channel.sink.add([].iterator), throwsArgumentError);
}, testOn: 'browser');
test('gracefully handles an unserializable message in the hybrid isolate',
() {
var channel = spawnHybridCode('''
import "package:stream_channel/stream_channel.dart";
void hybridMain(StreamChannel channel) {
channel.sink.add([].iterator);
}
''');
channel.stream.listen(null, onError: expectAsync1((error) {
expect(error.toString(), contains("can't be JSON-encoded."));
}));
});
test('forwards prints from the hybrid isolate', () {
expect(() async {
var channel = spawnHybridCode('''
import "package:stream_channel/stream_channel.dart";
void hybridMain(StreamChannel channel) {
print("hi!");
channel.sink.add(null);
}
''');
await channel.stream.first;
}, prints('hi!\n'));
});
// This takes special handling, since the code is packed into a data: URI
// that's imported, URIs don't escape $ by default, and $ isn't allowed in
// imports.
test('supports a dollar character in the hybrid code', () {
expect(spawnHybridCode(r'''
import "package:stream_channel/stream_channel.dart";
void hybridMain(StreamChannel channel) {
var value = "bar";
channel.sink.add("foo${value}baz");
}
''').stream.first, completion('foobarbaz'));
});
test('closes the channel when the hybrid isolate exits', () {
var channel = spawnHybridCode('''
import "dart:isolate";
hybridMain(_) {
Isolate.current.kill();
}
''');
expect(channel.stream.toList(), completion(isEmpty));
});
group('closes the channel when the test finishes by default', () {
late StreamChannel channel;
test('test 1', () {
channel = spawnHybridCode('''
import 'package:stream_channel/stream_channel.dart';
void hybridMain(StreamChannel channel) {}
''');
});
test('test 2', () async {
var isDone = false;
channel.stream.listen(null, onDone: () => isDone = true);
await pumpEventQueue();
expect(isDone, isTrue);
});
});
group('persists across multiple tests with stayAlive: true', () {
late StreamQueue queue;
late StreamSink sink;
setUpAll(() {
var channel = spawnHybridCode('''
import "package:stream_channel/stream_channel.dart";
void hybridMain(StreamChannel channel) {
channel.stream.listen((message) {
channel.sink.add(message);
});
}
''', stayAlive: true);
queue = StreamQueue(channel.stream);
sink = channel.sink;
});
test('echoes a number', () {
expect(queue.next, completion(equals(123)));
sink.add(123);
});
test('echoes a string', () {
expect(queue.next, completion(equals('wow')));
sink.add('wow');
});
});
test('can opt out of null safety', () async {
expect(spawnHybridCode('''
// @dart=2.9
import "package:stream_channel/stream_channel.dart";
// Would cause an error in null safety mode.
int x;
void hybridMain(StreamChannel channel) {
channel.sink..add(1)..add(2)..add(3)..close();
}
''').stream.toList(), completion(equals([1, 2, 3])));
});
test('opts in to null safety by default', () async {
expect(spawnHybridCode('''
import "package:stream_channel/stream_channel.dart";
// Use some null safety syntax
int? x;
void hybridMain(StreamChannel channel) {
channel.sink..add(1)..add(2)..add(3)..close();
}
''').stream.toList(), completion(equals([1, 2, 3])));
});
});
}