blob: 827611a5d53020162df234f39bfc72669d642c09 [file] [log] [blame]
// 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.
@TestOn('vm')
import 'dart:io';
import 'dart:isolate';
import 'package:package_config/package_config.dart';
import 'package:path/path.dart' as p;
import 'package:test_descriptor/test_descriptor.dart' as d;
import 'package:test/test.dart';
import '../io.dart';
void main() {
String packageRoot;
setUpAll(() async {
packageRoot = p.absolute(p.dirname(p
.fromUri(await Isolate.resolvePackageUri(Uri.parse('package:test/')))));
});
group('spawnHybridUri():', () {
group('in the VM', () {
_spawnHybridUriTests();
});
group('in the browser', () {
_spawnHybridUriTests(['-p', 'chrome']);
}, tags: 'chrome');
group('in Node.js', () {
_spawnHybridUriTests(['-p', 'node']);
}, tags: 'node');
});
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('can use dart:io even when run from a browser', () async {
var path = p.join(d.sandbox, 'test.dart');
await d.file('test.dart', '''
import "package:test/test.dart";
void main() {
test("hybrid loads dart:io", () {
expect(spawnHybridCode("""
import 'dart:io';
import 'package:stream_channel/stream_channel.dart';
void hybridMain(StreamChannel channel) {
channel.sink
..add(File("$path").readAsStringSync())
..close();
}
""").stream.first, completion(contains("hybrid emits numbers")));
});
}
''').create();
var test = await runTest(['-p', 'chrome', 'test.dart']);
expect(
test.stdout,
containsInOrder(
['+0: hybrid loads dart:io', '+1: All tests passed!']));
await test.shouldExit(0);
}, tags: ['chrome']);
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 {
await d.file('test.dart', '''
import "package:test/test.dart";
void main() {
test("invalid message to hybrid", () {
var channel = spawnHybridCode("""
import "package:stream_channel/stream_channel.dart";
void hybridMain(StreamChannel channel) {}
""");
expect(() => channel.sink.add([].iterator), throwsArgumentError);
});
}
''').create();
var test = await runTest(['-p', 'chrome', 'test.dart']);
expect(
test.stdout,
containsInOrder(
['+0: invalid message to hybrid', '+1: All tests passed!']));
await test.shouldExit(0);
}, tags: ['chrome']);
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('kills the isolate when the test closes the channel', () async {
var channel = spawnHybridCode('''
import "dart:async";
import "dart:io";
import "package:shelf/shelf.dart" as shelf;
import "package:shelf/shelf_io.dart" as io;
import "package:stream_channel/stream_channel.dart";
hybridMain(StreamChannel channel) async {
var server = await ServerSocket.bind("localhost", 0);
server.listen(null);
channel.sink.add(server.port);
}
''');
// Expect that the socket disconnects at some point (presumably when the
// isolate closes).
var port = await channel.stream.first as int;
var socket = await Socket.connect('localhost', port);
expect(socket.listen(null).asFuture(), completes);
await channel.sink.close();
}, skip: 'Enable when sdk#28081 is fixed.');
test('kills the isolate when the hybrid isolate closes the channel',
() async {
var channel = spawnHybridCode('''
import "dart:async";
import "dart:io";
import "package:stream_channel/stream_channel.dart";
hybridMain(StreamChannel channel) async {
var server = await ServerSocket.bind("localhost", 0);
server.listen(null);
channel.sink.add(server.port);
await channel.stream.first;
channel.sink.close();
}
''');
// Expect that the socket disconnects at some point (presumably when the
// isolate closes).
var port = await channel.stream.first as int;
var socket = await Socket.connect('localhost', port);
expect(socket.listen(null).asFuture(), completes);
channel.sink.add(null);
}, skip: 'Enable when sdk#28081 is fixed.');
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));
});
test('closes the channel when the test finishes by default', () async {
await d.file('test.dart', '''
import "package:stream_channel/stream_channel.dart";
import "package:test/test.dart";
import "${p.toUri(packageRoot)}/test/utils.dart";
void main() {
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);
});
}
''').create();
var test = await runTest(['test.dart']);
expect(
test.stdout,
containsInOrder(
['+0: test 1', '+1: test 2', '+2: All tests passed!']));
await test.shouldExit(0);
});
test('persists across multiple tests with stayAlive: true', () async {
await d.file('test.dart', '''
import "dart:async";
import "package:async/async.dart";
import "package:stream_channel/stream_channel.dart";
import "package:test/test.dart";
void main() {
StreamQueue queue;
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");
});
}
''').create();
var test = await runTest(['test.dart']);
expect(
test.stdout,
containsInOrder([
'+0: echoes a number',
'+1: echoes a string',
'+2: All tests passed!'
]));
await test.shouldExit(0);
});
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])));
});
});
}
/// Defines tests for `spawnHybridUri()`.
///
/// If [arguments] is given, it's passed on to the invocation of the test
/// runner.
void _spawnHybridUriTests([Iterable<String> arguments]) {
arguments ??= [];
test('loads a file in a separate isolate connected via StreamChannel',
() async {
await d.file('test.dart', '''
import "package:test/test.dart";
void main() {
test("hybrid emits numbers", () {
expect(spawnHybridUri("hybrid.dart").stream.toList(),
completion(equals([1, 2, 3])));
});
}
''').create();
await d.file('hybrid.dart', '''
import "package:stream_channel/stream_channel.dart";
void hybridMain(StreamChannel channel) {
channel.sink..add(1)..add(2)..add(3)..close();
}
''').create();
var test = await runTest(['test.dart', ...arguments]);
expect(test.stdout,
containsInOrder(['+0: hybrid emits numbers', '+1: All tests passed!']));
await test.shouldExit(0);
});
test('resolves URIs relative to the test file', () async {
await d.dir('test/dir/subdir', [
d.file('test.dart', '''
import "package:test/test.dart";
void main() {
test("hybrid emits numbers", () {
expect(spawnHybridUri("hybrid.dart").stream.toList(),
completion(equals([1, 2, 3])));
});
}
'''),
d.file('hybrid.dart', '''
import "package:stream_channel/stream_channel.dart";
void hybridMain(StreamChannel channel) {
channel.sink..add(1)..add(2)..add(3)..close();
}
'''),
]).create();
var test = await runTest(['test/dir/subdir/test.dart', ...arguments]);
expect(test.stdout,
containsInOrder(['+0: hybrid emits numbers', '+1: All tests passed!']));
await test.shouldExit(0);
});
test('resolves root-relative URIs relative to the package root', () async {
await d.dir('test/dir/subdir', [
d.file('test.dart', '''
import "package:test/test.dart";
void main() {
test("hybrid emits numbers", () {
expect(
spawnHybridUri("/test/dir/subdir/hybrid.dart")
.stream.toList(),
completion(equals([1, 2, 3])));
});
}
'''),
d.file('hybrid.dart', '''
import "package:stream_channel/stream_channel.dart";
void hybridMain(StreamChannel channel) {
channel.sink..add(1)..add(2)..add(3)..close();
}
'''),
]).create();
var test = await runTest(['test/dir/subdir/test.dart', ...arguments]);
expect(test.stdout,
containsInOrder(['+0: hybrid emits numbers', '+1: All tests passed!']));
await test.shouldExit(0);
});
test('supports absolute file: URIs', () async {
var url = p.toUri(p.absolute(p.join(d.sandbox, 'hybrid.dart')));
await d.file('test.dart', '''
import "package:test/test.dart";
void main() {
test("hybrid emits numbers", () {
expect(spawnHybridUri("$url").stream.toList(),
completion(equals([1, 2, 3])));
});
}
''').create();
await d.file('hybrid.dart', '''
import "package:stream_channel/stream_channel.dart";
void hybridMain(StreamChannel channel) {
channel.sink..add(1)..add(2)..add(3)..close();
}
''').create();
var test = await runTest(['test.dart', ...arguments]);
expect(test.stdout,
containsInOrder(['+0: hybrid emits numbers', '+1: All tests passed!']));
await test.shouldExit(0);
});
test('supports Uri objects', () async {
await d.file('test.dart', '''
import "package:test/test.dart";
void main() {
test("hybrid emits numbers", () {
expect(spawnHybridUri(Uri.parse("hybrid.dart")).stream.toList(),
completion(equals([1, 2, 3])));
});
}
''').create();
await d.file('hybrid.dart', '''
import "package:stream_channel/stream_channel.dart";
void hybridMain(StreamChannel channel) {
channel.sink..add(1)..add(2)..add(3)..close();
}
''').create();
var test = await runTest(['test.dart', ...arguments]);
expect(test.stdout,
containsInOrder(['+0: hybrid emits numbers', '+1: All tests passed!']));
await test.shouldExit(0);
});
test('rejects non-String, non-Uri objects', () {
expect(() => spawnHybridUri(123), throwsArgumentError);
});
test('passes a message to the hybrid isolate', () async {
await d.file('test.dart', '''
import "package:test/test.dart";
void main() {
test("hybrid echoes message", () {
expect(
spawnHybridUri(Uri.parse("hybrid.dart"), message: 123)
.stream.first,
completion(equals(123)));
expect(
spawnHybridUri(Uri.parse("hybrid.dart"), message: "wow")
.stream.first,
completion(equals("wow")));
});
}
''').create();
await d.file('hybrid.dart', '''
import "package:stream_channel/stream_channel.dart";
void hybridMain(StreamChannel channel, Object message) {
channel.sink..add(message)..close();
}
''').create();
var test = await runTest(['test.dart', ...arguments]);
expect(
test.stdout,
containsInOrder(
['+0: hybrid echoes message', '+1: All tests passed!']));
await test.shouldExit(0);
});
test('emits an error from the stream channel if the isolate fails to load',
() {
expect(spawnHybridUri('non existent file').stream.first,
throwsA(TypeMatcher<Exception>()));
});
test('can opt out of nnbd via language version comments', () async {
await d.file('test.dart', '''
import "package:test/test.dart";
void main() {
test("hybrid emits numbers", () {
expect(spawnHybridUri("hybrid.dart").stream.toList(),
completion(equals([1, 2, 3])));
});
}
''').create();
await d.file('hybrid.dart', '''
// @dart=2.9
import "package:stream_channel/stream_channel.dart";
// Would fail if null safety were enabled.
int x;
void hybridMain(StreamChannel channel) {
channel.sink..add(1)..add(2)..add(3)..close();
}
''').create();
var test = await runTest(['test.dart', ...arguments]);
expect(test.stdout,
containsInOrder(['+0: hybrid emits numbers', '+1: All tests passed!']));
await test.shouldExit(0);
});
test('can opt in to nnbd via language version comments', () async {
await d.file('test.dart', '''
import "package:test/test.dart";
void main() {
test("hybrid emits numbers", () {
expect(spawnHybridUri("hybrid.dart").stream.toList(),
completion(equals([1, 2, 3])));
});
}
''').create();
await d.file('hybrid.dart', '''
// @dart=2.12
import "package:stream_channel/stream_channel.dart";
// Use some null safety syntax to confirm we are opted in.
int? x;
void hybridMain(StreamChannel channel) {
channel.sink..add(1)..add(2)..add(3)..close();
}
''').create();
var test = await runTest(['test.dart', ...arguments]);
expect(test.stdout,
containsInOrder(['+0: hybrid emits numbers', '+1: All tests passed!']));
await test.shouldExit(0);
});
test('the default language version is used for hybrid code', () async {
await d.file('test.dart', '''
import "package:test/test.dart";
void main() {
test("hybrid emits numbers", () {
expect(spawnHybridUri("hybrid.dart").stream.toList(),
completion(equals([1, 2, 3])));
});
}
''').create();
await d.file('hybrid.dart', '''
import "package:stream_channel/stream_channel.dart";
// Would fail if null safety were enabled.
int x;
void hybridMain(StreamChannel channel) {
channel.sink..add(1)..add(2)..add(3)..close();
}
''').create();
// Adds the sandbox dir as a new package to the existing config,
// opting it out.
var originalPackageConfig =
await loadPackageConfigUri(await Isolate.packageConfig);
var extraPackage = Package('_test', Uri.file('${d.sandbox}/'),
languageVersion: LanguageVersion(2, 9));
var newConfig = PackageConfig([
...originalPackageConfig.packages,
extraPackage,
], extraData: originalPackageConfig.extraData);
await d.dir('.dart_tool').create();
await savePackageConfig(newConfig, Directory(d.sandbox));
var test = await runTest(['test.dart', ...arguments],
packageConfig: p.join(d.sandbox, '.dart_tool', 'package_config.json'));
expect(test.stdout,
containsInOrder(['+0: hybrid emits numbers', '+1: All tests passed!']));
await test.shouldExit(0);
});
}