| // 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:developer'; |
| import 'dart:io'; |
| import 'dart:isolate'; |
| |
| import 'package:coverage/coverage.dart'; |
| import 'package:path/path.dart' as p; |
| import 'package:stream_channel/isolate_channel.dart'; |
| import 'package:stream_channel/stream_channel.dart'; |
| import 'package:test_api/src/backend/runtime.dart'; // ignore: implementation_imports |
| import 'package:test_api/src/backend/suite_platform.dart'; // ignore: implementation_imports |
| import 'package:vm_service/vm_service.dart' hide Isolate; |
| import 'package:vm_service/vm_service_io.dart'; |
| |
| import '../../runner/configuration.dart'; |
| import '../../runner/environment.dart'; |
| import '../../runner/load_exception.dart'; |
| import '../../runner/platform.dart'; |
| import '../../runner/plugin/platform_helpers.dart'; |
| import '../../runner/runner_suite.dart'; |
| import '../../runner/suite.dart'; |
| import '../../util/dart.dart' as dart; |
| import 'environment.dart'; |
| |
| /// A platform that loads tests in isolates spawned within this Dart process. |
| class VMPlatform extends PlatformPlugin { |
| /// The test runner configuration. |
| final _config = Configuration.current; |
| |
| VMPlatform(); |
| |
| @override |
| StreamChannel loadChannel(String path, SuitePlatform platform) => |
| throw UnimplementedError(); |
| |
| @override |
| Future<RunnerSuite> load(String path, SuitePlatform platform, |
| SuiteConfiguration suiteConfig, Object message) async { |
| assert(platform.runtime == Runtime.vm); |
| |
| var receivePort = ReceivePort(); |
| Isolate isolate; |
| try { |
| isolate = await _spawnIsolate(path, receivePort.sendPort); |
| } catch (error) { |
| receivePort.close(); |
| rethrow; |
| } |
| |
| VmService client; |
| StreamSubscription<Event> eventSub; |
| var channel = IsolateChannel.connectReceive(receivePort) |
| .transformStream(StreamTransformer.fromHandlers(handleDone: (sink) { |
| isolate.kill(); |
| eventSub?.cancel(); |
| client?.dispose(); |
| sink.close(); |
| })); |
| |
| Environment environment; |
| IsolateRef isolateRef; |
| if (_config.debug) { |
| // Print an empty line because the VM prints an "Observatory listening on" |
| // line and we don't want that to end up on the same line as the reporter |
| // info. |
| if (_config.reporter == 'compact') stdout.writeln(); |
| |
| var info = await Service.controlWebServer(enable: true); |
| var isolateID = Service.getIsolateID(isolate); |
| |
| var libraryPath = p.toUri(p.absolute(path)).toString(); |
| client = await vmServiceConnectUri(_wsUriFor(info.serverUri.toString())); |
| var isolateNumber = int.parse(isolateID.split('/').last); |
| isolateRef = (await client.getVM()) |
| .isolates |
| .firstWhere((isolate) => isolate.number == isolateNumber.toString()); |
| await client.setName(isolateRef.id, path); |
| var libraryRef = (await client.getIsolate(isolateRef.id)) |
| .libraries |
| .firstWhere((library) => library.uri == libraryPath) as LibraryRef; |
| var url = _observatoryUrlFor( |
| info.serverUri.toString(), isolateRef.id, libraryRef.id); |
| environment = VMEnvironment(url, isolateRef, client); |
| } |
| |
| environment ??= PluginEnvironment(); |
| |
| var controller = deserializeSuite( |
| path, platform, suiteConfig, environment, channel, message, |
| gatherCoverage: () => _gatherCoverage(environment)); |
| |
| if (isolateRef != null) { |
| await client.streamListen('Debug'); |
| eventSub = client.onDebugEvent.listen((event) { |
| if (event.kind == EventKind.kResume) { |
| controller.setDebugging(false); |
| } else if (event.kind == EventKind.kPauseInterrupted || |
| event.kind == EventKind.kPauseBreakpoint || |
| event.kind == EventKind.kPauseException) { |
| controller.setDebugging(true); |
| } |
| }); |
| } |
| |
| return await controller.suite; |
| } |
| |
| /// Spawns an isolate and passes it [message]. |
| /// |
| /// This isolate connects an [IsolateChannel] to [message] and sends the |
| /// serialized tests over that channel. |
| Future<Isolate> _spawnIsolate(String path, SendPort message) async { |
| if (_config.suiteDefaults.precompiledPath != null) { |
| return _spawnPrecompiledIsolate( |
| path, message, _config.suiteDefaults.precompiledPath); |
| } else if (_config.pubServeUrl != null) { |
| return _spawnPubServeIsolate(path, message, _config.pubServeUrl); |
| } else { |
| return _spawnDataIsolate(path, message); |
| } |
| } |
| } |
| |
| Future<Isolate> _spawnDataIsolate(String path, SendPort message) async { |
| return await dart.runInIsolate(''' |
| import "dart:isolate"; |
| |
| import "package:stream_channel/isolate_channel.dart"; |
| |
| import "package:test_core/src/runner/plugin/remote_platform_helpers.dart"; |
| |
| import "${p.toUri(p.absolute(path))}" as test; |
| |
| void main(_, SendPort message) { |
| var channel = serializeSuite(() => test.main); |
| IsolateChannel.connectSend(message).pipe(channel); |
| } |
| ''', message, checked: true); |
| } |
| |
| Future<Isolate> _spawnPrecompiledIsolate( |
| String testPath, SendPort message, String precompiledPath) async { |
| testPath = p.absolute(p.join(precompiledPath, testPath) + '.vm_test.dart'); |
| var dillTestpath = |
| testPath.substring(0, testPath.length - '.dart'.length) + '.vm.app.dill'; |
| if (await File(dillTestpath).exists()) { |
| testPath = dillTestpath; |
| } |
| return await Isolate.spawnUri(p.toUri(testPath), [], message, |
| packageConfig: p.toUri(p.join(precompiledPath, '.packages')), |
| checked: true); |
| } |
| |
| Future<Map<String, dynamic>> _gatherCoverage(Environment environment) async { |
| final isolateId = Uri.parse(environment.observatoryUrl.fragment) |
| .queryParameters['isolateId']; |
| return await collect(environment.observatoryUrl, false, false, false, {}, |
| isolateIds: {isolateId}); |
| } |
| |
| Future<Isolate> _spawnPubServeIsolate( |
| String testPath, SendPort message, Uri pubServeUrl) async { |
| var url = pubServeUrl.resolveUri( |
| p.toUri(p.relative(testPath, from: 'test') + '.vm_test.dart')); |
| |
| try { |
| return await Isolate.spawnUri(url, [], message, checked: true); |
| } on IsolateSpawnException catch (error) { |
| if (error.message.contains('OS Error: Connection refused') || |
| error.message.contains('The remote computer refused')) { |
| throw LoadException( |
| testPath, |
| 'Error getting $url: Connection refused\n' |
| 'Make sure "pub serve" is running.'); |
| } else if (error.message.contains('404 Not Found')) { |
| throw LoadException( |
| testPath, |
| 'Error getting $url: 404 Not Found\n' |
| 'Make sure "pub serve" is serving the test/ directory.'); |
| } |
| |
| throw LoadException(testPath, error); |
| } |
| } |
| |
| String _wsUriFor(String observatoryUrl) => |
| "ws:${observatoryUrl.split(':').sublist(1).join(':')}ws"; |
| |
| Uri _observatoryUrlFor(String base, String isolateId, String id) => |
| Uri.parse('$base#/inspect?isolateId=${Uri.encodeQueryComponent(isolateId)}&' |
| 'objectId=${Uri.encodeQueryComponent(id)}'); |