1// Copyright 2019 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5import 'dart:async'; 6import 'dart:developer'; 7import 'dart:io'; 8import 'dart:isolate'; 9 10import 'package:async/async.dart'; 11import 'package:coverage/coverage.dart'; 12import 'package:flutter_tools/src/context_runner.dart'; 13import 'package:path/path.dart' as path; 14import 'package:pedantic/pedantic.dart'; 15import 'package:stream_channel/isolate_channel.dart'; 16import 'package:stream_channel/stream_channel.dart'; 17import 'package:test_core/src/runner/hack_register_platform.dart' as hack; // ignore: implementation_imports 18import 'package:test_core/src/executable.dart' as test; // ignore: implementation_imports 19import 'package:vm_service_client/vm_service_client.dart'; // ignore: deprecated_member_use 20import 'package:test_api/src/backend/runtime.dart'; // ignore: implementation_imports 21import 'package:test_api/src/backend/suite_platform.dart'; // ignore: implementation_imports 22import 'package:test_core/src/runner/platform.dart'; // ignore: implementation_imports 23import 'package:test_core/src/runner/runner_suite.dart'; // ignore: implementation_imports 24import 'package:test_core/src/runner/suite.dart'; // ignore: implementation_imports 25import 'package:test_core/src/runner/plugin/platform_helpers.dart'; // ignore: implementation_imports 26import 'package:test_core/src/runner/environment.dart'; // ignore: implementation_imports 27import 'package:flutter_tools/src/project.dart'; 28import 'package:flutter_tools/src/test/coverage_collector.dart'; 29 30/// Generates an lcov report for the flutter tool unit tests. 31/// 32/// Example invocation: 33/// 34/// dart tool/tool_coverage.dart 35Future<void> main(List<String> arguments) async { 36 return runInContext(() async { 37 final VMPlatform vmPlatform = VMPlatform(); 38 hack.registerPlatformPlugin( 39 <Runtime>[Runtime.vm], 40 () => vmPlatform, 41 ); 42 await test.main(<String>['-x', 'no_coverage', '--no-color', '-r', 'compact', '-j', '1', ...arguments]); 43 exit(exitCode); 44 }); 45} 46 47/// A platform that loads tests in isolates spawned within this Dart process. 48class VMPlatform extends PlatformPlugin { 49 final CoverageCollector coverageCollector = CoverageCollector( 50 libraryPredicate: (String libraryName) => libraryName.contains(FlutterProject.current().manifest.appName), 51 ); 52 final Map<String, Future<void>> _pending = <String, Future<void>>{}; 53 final String precompiledPath = path.join('.dart_tool', 'build', 'generated', 'flutter_tools'); 54 55 @override 56 StreamChannel<void> loadChannel(String codePath, SuitePlatform platform) => 57 throw UnimplementedError(); 58 59 @override 60 Future<RunnerSuite> load(String codePath, SuitePlatform platform, 61 SuiteConfiguration suiteConfig, Object message) async { 62 final ReceivePort receivePort = ReceivePort(); 63 Isolate isolate; 64 try { 65 isolate = await _spawnIsolate(codePath, receivePort.sendPort); 66 } catch (error) { 67 receivePort.close(); 68 rethrow; 69 } 70 final Completer<void> completer = Completer<void>(); 71 // When this is completed we remove it from the map of pending so we can 72 // log the futures that get "stuck". 73 unawaited(completer.future.whenComplete(() { 74 _pending.remove(codePath); 75 })); 76 final ServiceProtocolInfo info = await Service.controlWebServer(enable: true); 77 final dynamic channel = IsolateChannel<Object>.connectReceive(receivePort) 78 .transformStream(StreamTransformer<Object, Object>.fromHandlers(handleDone: (EventSink<Object> sink) async { 79 try { 80 // this will throw if collection fails. 81 await coverageCollector.collectCoverageIsolate(info.serverUri); 82 } finally { 83 isolate.kill(priority: Isolate.immediate); 84 isolate = null; 85 sink.close(); 86 completer.complete(); 87 } 88 }, handleError: (dynamic error, StackTrace stackTrace, EventSink<Object> sink) { 89 isolate.kill(priority: Isolate.immediate); 90 isolate = null; 91 sink.close(); 92 completer.complete(); 93 })); 94 95 VMEnvironment environment; 96 final RunnerSuiteController controller = deserializeSuite( 97 codePath, 98 platform, 99 suiteConfig, 100 environment, 101 channel, 102 message, 103 ); 104 _pending[codePath] = completer.future; 105 return await controller.suite; 106 } 107 108 /// Spawns an isolate and passes it [message]. 109 /// 110 /// This isolate connects an [IsolateChannel] to [message] and sends the 111 /// serialized tests over that channel. 112 Future<Isolate> _spawnIsolate(String codePath, SendPort message) async { 113 String testPath = path.absolute(path.join(precompiledPath, codePath) + '.vm_test.dart'); 114 testPath = testPath.substring(0, testPath.length - '.dart'.length) + '.vm.app.dill'; 115 return await Isolate.spawnUri(path.toUri(testPath), <String>[], message, 116 packageConfig: path.toUri('.packages'), 117 checked: true, 118 ); 119 } 120 121 @override 122 Future<void> close() async { 123 try { 124 await Future.wait(_pending.values).timeout(const Duration(minutes: 1)); 125 } on TimeoutException { 126 // TODO(jonahwilliams): resolve whether there are any specific tests that 127 // get stuck or if it is a general infra issue with how we are collecting 128 // coverage. 129 // Log tests that are "Stuck" waiting for coverage. 130 print('The following tests timed out waiting for coverage:'); 131 print(_pending.keys.join(', ')); 132 } 133 final String packagePath = Directory.current.path; 134 final Resolver resolver = Resolver(packagesPath: '.packages'); 135 final Formatter formatter = LcovFormatter(resolver, reportOn: <String>[ 136 'lib', 137 ], basePath: packagePath); 138 final String result = await coverageCollector.finalizeCoverage( 139 formatter: formatter, 140 ); 141 final String prefix = Platform.environment['SUBSHARD'] ?? ''; 142 final String outputLcovPath = path.join('coverage', '$prefix.lcov.info'); 143 File(outputLcovPath) 144 ..createSync(recursive: true) 145 ..writeAsStringSync(result); 146 } 147} 148 149class VMEnvironment implements Environment { 150 VMEnvironment(this.observatoryUrl, this._isolate); 151 152 @override 153 final bool supportsDebugging = false; 154 155 @override 156 final Uri observatoryUrl; 157 158 /// The VM service isolate object used to control this isolate. 159 final VMIsolateRef _isolate; 160 161 @override 162 Uri get remoteDebuggerUrl => null; 163 164 @override 165 Stream<void> get onRestart => StreamController<dynamic>.broadcast().stream; 166 167 @override 168 CancelableOperation<void> displayPause() { 169 final CancelableCompleter<dynamic> completer = CancelableCompleter<dynamic>(onCancel: () => _isolate.resume()); 170 171 completer.complete(_isolate.pause().then((dynamic _) => _isolate.onPauseOrResume 172 .firstWhere((VMPauseEvent event) => event is VMResumeEvent))); 173 174 return completer.operation; 175 } 176} 177