| // 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: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: "browser"); |
| }); |
| |
| 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(new File("$path").readAsStringSync()) |
| ..close(); |
| } |
| ''').stream.first, completion(contains("hybrid emits numbers"))); |
| }); |
| } |
| """).create(); |
| |
| var test = await runTest(["-p", "content-shell", "test.dart"]); |
| expect( |
| test.stdout, |
| containsInOrder( |
| ["+0: hybrid loads dart:io", "+1: All tests passed!"])); |
| await test.shouldExit(0); |
| }, tags: ["content-shell"]); |
| |
| 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!", new 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 new TestFailure("oh no!"); |
| } |
| """); |
| |
| expect(channel.stream.first, throwsA(new isInstanceOf<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", "content-shell", "test.dart"]); |
| expect( |
| test.stdout, |
| containsInOrder( |
| ["+0: invalid message to hybrid", "+1: All tests passed!"])); |
| await test.shouldExit(0); |
| }, tags: ['content-shell']); |
| |
| 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; |
| 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; |
| 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 = new 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); |
| }); |
| }); |
| } |
| |
| /// 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"]..addAll(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"]..addAll(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"]..addAll(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"]..addAll(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"]..addAll(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(new isInstanceOf<IsolateSpawnException>())); |
| }); |
| } |