|
| 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 | + |
| 5 | +import 'dart:async'; |
| 6 | +import 'dart:convert'; |
| 7 | +import 'dart:io'; |
| 8 | + |
| 9 | +import 'package:args/args.dart'; |
| 10 | +import 'package:flutter_tools/src/context_runner.dart'; |
| 11 | +import 'package:flutter_tools/src/project.dart'; |
| 12 | +import 'package:flutter_tools/src/test/coverage_collector.dart'; |
| 13 | +import 'package:pool/pool.dart'; |
| 14 | +import 'package:path/path.dart' as path; |
| 15 | + |
| 16 | +final ArgParser argParser = ArgParser() |
| 17 | + ..addOption('output-html', |
| 18 | + defaultsTo: 'coverage/report.html', |
| 19 | + help: 'The output path for the genhtml report.' |
| 20 | + ) |
| 21 | + ..addOption('output-lcov', |
| 22 | + defaultsTo: 'coverage/lcov.info', |
| 23 | + help: 'The output path for the lcov data.' |
| 24 | + ) |
| 25 | + ..addOption('test-directory', |
| 26 | + defaultsTo: 'test/', |
| 27 | + help: 'The path to the test directory.' |
| 28 | + ) |
| 29 | + ..addOption('packages', |
| 30 | + defaultsTo: '.packages', |
| 31 | + help: 'The path to the .packages file.' |
| 32 | + ) |
| 33 | + ..addOption('genhtml', |
| 34 | + defaultsTo: 'genhtml', |
| 35 | + help: 'The genhtml executable.'); |
| 36 | + |
| 37 | + |
| 38 | +/// Generates an html coverage report for the flutter_tool. |
| 39 | +/// |
| 40 | +/// Example invocation: |
| 41 | +/// |
| 42 | +/// dart tool/tool_coverage.dart --packages=.packages --test-directory=test |
| 43 | +Future<void> main(List<String> arguments) async { |
| 44 | + final ArgResults argResults = argParser.parse(arguments); |
| 45 | + await runInContext(() async { |
| 46 | + final CoverageCollector coverageCollector = CoverageCollector( |
| 47 | + flutterProject: await FlutterProject.current(), |
| 48 | + ); |
| 49 | + /// A temp directory to create synthetic test files in. |
| 50 | + final Directory tempDirectory = Directory.systemTemp.createTempSync('_flutter_coverage') |
| 51 | + ..createSync(); |
| 52 | + final String flutterRoot = File(Platform.script.toFilePath()).parent.parent.parent.parent.path; |
| 53 | + await ToolCoverageRunner(tempDirectory, coverageCollector, flutterRoot, argResults).collectCoverage(); |
| 54 | + }); |
| 55 | +} |
| 56 | + |
| 57 | +class ToolCoverageRunner { |
| 58 | + ToolCoverageRunner( |
| 59 | + this.tempDirectory, |
| 60 | + this.coverageCollector, |
| 61 | + this.flutterRoot, |
| 62 | + this.argResults, |
| 63 | + ); |
| 64 | + |
| 65 | + final ArgResults argResults; |
| 66 | + final Pool pool = Pool(Platform.numberOfProcessors); |
| 67 | + final Directory tempDirectory; |
| 68 | + final CoverageCollector coverageCollector; |
| 69 | + final String flutterRoot; |
| 70 | + |
| 71 | + Future<void> collectCoverage() async { |
| 72 | + final List<Future<void>> pending = <Future<void>>[]; |
| 73 | + |
| 74 | + final Directory testDirectory = Directory(argResults['test-directory']); |
| 75 | + final List<FileSystemEntity> fileSystemEntities = testDirectory.listSync(recursive: true); |
| 76 | + for (FileSystemEntity fileSystemEntity in fileSystemEntities) { |
| 77 | + if (!fileSystemEntity.path.endsWith('_test.dart')) { |
| 78 | + continue; |
| 79 | + } |
| 80 | + pending.add(_runTest(fileSystemEntity)); |
| 81 | + } |
| 82 | + await Future.wait(pending); |
| 83 | + |
| 84 | + final String lcovData = await coverageCollector.finalizeCoverage(); |
| 85 | + final String outputLcovPath = argResults['output-lcov']; |
| 86 | + final String outputHtmlPath = argResults['output-html']; |
| 87 | + final String genHtmlExecutable = argResults['genhtml']; |
| 88 | + File(outputLcovPath) |
| 89 | + ..createSync(recursive: true) |
| 90 | + ..writeAsStringSync(lcovData); |
| 91 | + await Process.run(genHtmlExecutable, <String>[outputLcovPath, '-o', outputHtmlPath], runInShell: true); |
| 92 | + } |
| 93 | + |
| 94 | + // Creates a synthetic test file to wrap the test main in a group invocation. |
| 95 | + // This will set up several fields used by the test methods on the context. Normally |
| 96 | + // this would be handled automatically by the test runner, but since we're executing |
| 97 | + // the files directly with dart we need to handle it manually. |
| 98 | + String _createTest(File testFile) { |
| 99 | + final File fakeTest = File(path.join(tempDirectory.path, testFile.path)) |
| 100 | + ..createSync(recursive: true) |
| 101 | + ..writeAsStringSync(''' |
| 102 | +import "package:test/test.dart"; |
| 103 | +import "${path.absolute(testFile.path)}" as entrypoint; |
| 104 | +
|
| 105 | +void main() { |
| 106 | + group('', entrypoint.main); |
| 107 | +} |
| 108 | +'''); |
| 109 | + return fakeTest.path; |
| 110 | + } |
| 111 | + |
| 112 | + Future<void> _runTest(File testFile) async { |
| 113 | + final PoolResource resource = await pool.request(); |
| 114 | + final String testPath = _createTest(testFile); |
| 115 | + final int port = await _findPort(); |
| 116 | + final Uri coverageUri = Uri.parse('http://127.0.0.1:$port'); |
| 117 | + final Completer<void> completer = Completer<void>(); |
| 118 | + final String packagesPath = argResults['packages']; |
| 119 | + final Process testProcess = await Process.start( |
| 120 | + Platform.resolvedExecutable, |
| 121 | + <String>[ |
| 122 | + '--packages=$packagesPath', |
| 123 | + '--pause-isolates-on-exit', |
| 124 | + '--enable-asserts', |
| 125 | + '--enable-vm-service=${coverageUri.port}', |
| 126 | + testPath, |
| 127 | + ], |
| 128 | + runInShell: true, |
| 129 | + environment: <String, String>{ |
| 130 | + 'FLUTTER_ROOT': flutterRoot, |
| 131 | + }).timeout(const Duration(seconds: 30)); |
| 132 | + testProcess.stdout |
| 133 | + .transform(utf8.decoder) |
| 134 | + .transform(const LineSplitter()) |
| 135 | + .listen((String line) { |
| 136 | + print(line); |
| 137 | + if (line.contains('All tests passed') || line.contains('Some tests failed')) { |
| 138 | + completer.complete(null); |
| 139 | + } |
| 140 | + }); |
| 141 | + try { |
| 142 | + await completer.future; |
| 143 | + await coverageCollector.collectCoverage(testProcess, coverageUri).timeout(const Duration(seconds: 30)); |
| 144 | + testProcess?.kill(); |
| 145 | + } on TimeoutException { |
| 146 | + print('Failed to collect coverage for ${testFile.path} after 30 seconds'); |
| 147 | + } finally { |
| 148 | + resource.release(); |
| 149 | + } |
| 150 | + } |
| 151 | + |
| 152 | + Future<int> _findPort() async { |
| 153 | + int port = 0; |
| 154 | + ServerSocket serverSocket; |
| 155 | + try { |
| 156 | + serverSocket = await ServerSocket.bind(InternetAddress.loopbackIPv4.address, 0); |
| 157 | + port = serverSocket.port; |
| 158 | + } catch (e) { |
| 159 | + // Failures are signaled by a return value of 0 from this function. |
| 160 | + print('_findPort failed: $e'); |
| 161 | + } |
| 162 | + if (serverSocket != null) { |
| 163 | + await serverSocket.close(); |
| 164 | + } |
| 165 | + return port; |
| 166 | + } |
| 167 | +} |
0 commit comments