Skip to content

Commit 65f4599

Browse files
initial work on coverage generating script for tool (flutter#29494)
1 parent f5672b9 commit 65f4599

File tree

4 files changed

+178
-3
lines changed

4 files changed

+178
-3
lines changed

.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,9 @@ unlinked_spec.ds
8585
**/ios/ServiceDefinitions.json
8686
**/ios/Runner/GeneratedPluginRegistrant.*
8787

88+
# Coverage
89+
coverage/
90+
8891
# Exceptions to above rules.
8992
!**/ios/**/default.mode1v3
9093
!**/ios/**/default.mode2v3

packages/flutter_tools/lib/src/test/coverage_collector.dart

+5-3
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,11 @@ class CoverageCollector extends TestWatcher {
3434
}
3535

3636
void _addHitmap(Map<String, dynamic> hitmap) {
37-
if (_globalHitmap == null)
37+
if (_globalHitmap == null) {
3838
_globalHitmap = hitmap;
39-
else
39+
} else {
4040
coverage.mergeHitmaps(hitmap, _globalHitmap);
41+
}
4142
}
4243

4344
/// Collects coverage for the given [Process] using the given `port`.
@@ -91,8 +92,9 @@ class CoverageCollector extends TestWatcher {
9192
Directory coverageDirectory,
9293
}) async {
9394
printTrace('formating coverage data');
94-
if (_globalHitmap == null)
95+
if (_globalHitmap == null) {
9596
return null;
97+
}
9698
if (formatter == null) {
9799
final coverage.Resolver resolver = coverage.Resolver(packagesPath: PackageMap.globalPackagesPath);
98100
final String packagePath = fs.currentDirectory.path;

packages/flutter_tools/test/integration/test_driver.dart

+3
Original file line numberDiff line numberDiff line change
@@ -511,6 +511,9 @@ class FlutterRunTestDriver extends FlutterTestDriver {
511511
}
512512

513513
Future<int> detach() async {
514+
if (_process == null) {
515+
return 0;
516+
}
514517
if (_vmService != null) {
515518
_debugPrint('Closing VM service...');
516519
_vmService.dispose();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
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

Comments
 (0)