// Copyright 2019 The Chromium Authors. 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:convert'; import 'dart:io'; import 'package:args/args.dart'; import 'package:flutter_tools/src/context_runner.dart'; import 'package:flutter_tools/src/project.dart'; import 'package:flutter_tools/src/test/coverage_collector.dart'; import 'package:pool/pool.dart'; import 'package:path/path.dart' as path; final ArgParser argParser = ArgParser() ..addOption('output-html', defaultsTo: 'coverage/report.html', help: 'The output path for the genhtml report.' ) ..addOption('output-lcov', defaultsTo: 'coverage/lcov.info', help: 'The output path for the lcov data.' ) ..addOption('test-directory', defaultsTo: 'test/', help: 'The path to the test directory.' ) ..addOption('packages', defaultsTo: '.packages', help: 'The path to the .packages file.' ) ..addOption('genhtml', defaultsTo: 'genhtml', help: 'The genhtml executable.'); /// Generates an html coverage report for the flutter_tool. /// /// Example invocation: /// /// dart tool/tool_coverage.dart --packages=.packages --test-directory=test Future<void> main(List<String> arguments) async { final ArgResults argResults = argParser.parse(arguments); await runInContext(() async { final CoverageCollector coverageCollector = CoverageCollector( flutterProject: FlutterProject.current(), ); /// A temp directory to create synthetic test files in. final Directory tempDirectory = Directory.systemTemp.createTempSync('_flutter_coverage') ..createSync(); final String flutterRoot = File(Platform.script.toFilePath()).parent.parent.parent.parent.path; await ToolCoverageRunner(tempDirectory, coverageCollector, flutterRoot, argResults).collectCoverage(); }); } class ToolCoverageRunner { ToolCoverageRunner( this.tempDirectory, this.coverageCollector, this.flutterRoot, this.argResults, ); final ArgResults argResults; final Pool pool = Pool(Platform.numberOfProcessors); final Directory tempDirectory; final CoverageCollector coverageCollector; final String flutterRoot; Future<void> collectCoverage() async { final List<Future<void>> pending = <Future<void>>[]; final Directory testDirectory = Directory(argResults['test-directory']); final List<FileSystemEntity> fileSystemEntities = testDirectory.listSync(recursive: true); for (FileSystemEntity fileSystemEntity in fileSystemEntities) { if (!fileSystemEntity.path.endsWith('_test.dart')) { continue; } pending.add(_runTest(fileSystemEntity)); } await Future.wait(pending); final String lcovData = await coverageCollector.finalizeCoverage(); final String outputLcovPath = argResults['output-lcov']; final String outputHtmlPath = argResults['output-html']; final String genHtmlExecutable = argResults['genhtml']; File(outputLcovPath) ..createSync(recursive: true) ..writeAsStringSync(lcovData); await Process.run(genHtmlExecutable, <String>[outputLcovPath, '-o', outputHtmlPath], runInShell: true); } // Creates a synthetic test file to wrap the test main in a group invocation. // This will set up several fields used by the test methods on the context. Normally // this would be handled automatically by the test runner, but since we're executing // the files directly with dart we need to handle it manually. String _createTest(File testFile) { final File fakeTest = File(path.join(tempDirectory.path, testFile.path)) ..createSync(recursive: true) ..writeAsStringSync(''' import "package:test/test.dart"; import "${path.absolute(testFile.path)}" as entrypoint; void main() { group('', entrypoint.main); } '''); return fakeTest.path; } Future<void> _runTest(File testFile) async { final PoolResource resource = await pool.request(); final String testPath = _createTest(testFile); final int port = await _findPort(); final Uri coverageUri = Uri.parse('http://127.0.0.1:$port'); final Completer<void> completer = Completer<void>(); final String packagesPath = argResults['packages']; final Process testProcess = await Process.start( Platform.resolvedExecutable, <String>[ '--packages=$packagesPath', '--pause-isolates-on-exit', '--enable-asserts', '--enable-vm-service=${coverageUri.port}', testPath, ], runInShell: true, environment: <String, String>{ 'FLUTTER_ROOT': flutterRoot, }).timeout(const Duration(seconds: 30)); testProcess.stdout .transform(utf8.decoder) .transform(const LineSplitter()) .listen((String line) { print(line); if (line.contains('All tests passed') || line.contains('Some tests failed')) { completer.complete(null); } }); try { await completer.future; await coverageCollector.collectCoverage(testProcess, coverageUri).timeout(const Duration(seconds: 30)); testProcess?.kill(); } on TimeoutException { print('Failed to collect coverage for ${testFile.path} after 30 seconds'); } finally { resource.release(); } } Future<int> _findPort() async { int port = 0; ServerSocket serverSocket; try { serverSocket = await ServerSocket.bind(InternetAddress.loopbackIPv4.address, 0); port = serverSocket.port; } catch (e) { // Failures are signaled by a return value of 0 from this function. print('_findPort failed: $e'); } if (serverSocket != null) { await serverSocket.close(); } return port; } }