• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright 2016 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';
6
7import 'package:coverage/coverage.dart' as coverage;
8
9import '../base/file_system.dart';
10import '../base/io.dart';
11import '../base/logger.dart';
12import '../base/os.dart';
13import '../base/platform.dart';
14import '../base/process_manager.dart';
15import '../dart/package_map.dart';
16import '../globals.dart';
17import '../vmservice.dart';
18
19import 'watcher.dart';
20
21/// A class that's used to collect coverage data during tests.
22class CoverageCollector extends TestWatcher {
23  CoverageCollector({this.libraryPredicate});
24
25  Map<String, dynamic> _globalHitmap;
26  bool Function(String) libraryPredicate;
27
28  @override
29  Future<void> handleFinishedTest(ProcessEvent event) async {
30    printTrace('test ${event.childIndex}: collecting coverage');
31    await collectCoverage(event.process, event.observatoryUri);
32  }
33
34  void _addHitmap(Map<String, dynamic> hitmap) {
35    if (_globalHitmap == null) {
36      _globalHitmap = hitmap;
37    } else {
38      coverage.mergeHitmaps(hitmap, _globalHitmap);
39    }
40  }
41
42  /// Collects coverage for an isolate using the given `port`.
43  ///
44  /// This should be called when the code whose coverage data is being collected
45  /// has been run to completion so that all coverage data has been recorded.
46  ///
47  /// The returned [Future] completes when the coverage is collected.
48  Future<void> collectCoverageIsolate(Uri observatoryUri) async {
49    assert(observatoryUri != null);
50    print('collecting coverage data from $observatoryUri...');
51    final Map<String, dynamic> data = await collect(observatoryUri, libraryPredicate);
52    if (data == null) {
53      throw Exception('Failed to collect coverage.');
54    }
55    assert(data != null);
56
57    print('($observatoryUri): collected coverage data; merging...');
58    _addHitmap(coverage.createHitmap(data['coverage']));
59    print('($observatoryUri): done merging coverage data into global coverage map.');
60  }
61
62  /// Collects coverage for the given [Process] using the given `port`.
63  ///
64  /// This should be called when the code whose coverage data is being collected
65  /// has been run to completion so that all coverage data has been recorded.
66  ///
67  /// The returned [Future] completes when the coverage is collected.
68  Future<void> collectCoverage(Process process, Uri observatoryUri) async {
69    assert(process != null);
70    assert(observatoryUri != null);
71    final int pid = process.pid;
72    printTrace('pid $pid: collecting coverage data from $observatoryUri...');
73
74    Map<String, dynamic> data;
75    final Future<void> processComplete = process.exitCode
76      .then<void>((int code) {
77        throw Exception('Failed to collect coverage, process terminated prematurely with exit code $code.');
78      });
79    final Future<void> collectionComplete = collect(observatoryUri, libraryPredicate)
80      .then<void>((Map<String, dynamic> result) {
81        if (result == null)
82          throw Exception('Failed to collect coverage.');
83        data = result;
84      });
85    await Future.any<void>(<Future<void>>[ processComplete, collectionComplete ]);
86    assert(data != null);
87
88    printTrace('pid $pid ($observatoryUri): collected coverage data; merging...');
89    _addHitmap(coverage.createHitmap(data['coverage']));
90    printTrace('pid $pid ($observatoryUri): done merging coverage data into global coverage map.');
91  }
92
93  /// Returns a future that will complete with the formatted coverage data
94  /// (using [formatter]) once all coverage data has been collected.
95  ///
96  /// This will not start any collection tasks. It us up to the caller of to
97  /// call [collectCoverage] for each process first.
98  Future<String> finalizeCoverage({
99    coverage.Formatter formatter,
100    Directory coverageDirectory,
101  }) async {
102    if (_globalHitmap == null) {
103      return null;
104    }
105    if (formatter == null) {
106      final coverage.Resolver resolver = coverage.Resolver(packagesPath: PackageMap.globalPackagesPath);
107      final String packagePath = fs.currentDirectory.path;
108      final List<String> reportOn = coverageDirectory == null
109        ? <String>[fs.path.join(packagePath, 'lib')]
110        : <String>[coverageDirectory.path];
111      formatter = coverage.LcovFormatter(resolver, reportOn: reportOn, basePath: packagePath);
112    }
113    final String result = await formatter.format(_globalHitmap);
114    _globalHitmap = null;
115    return result;
116  }
117
118  Future<bool> collectCoverageData(String coveragePath, { bool mergeCoverageData = false, Directory coverageDirectory }) async {
119    final Status status = logger.startProgress('Collecting coverage information...', timeout: timeoutConfiguration.fastOperation);
120    final String coverageData = await finalizeCoverage(
121      coverageDirectory: coverageDirectory,
122    );
123    status.stop();
124    printTrace('coverage information collection complete');
125    if (coverageData == null)
126      return false;
127
128    final File coverageFile = fs.file(coveragePath)
129      ..createSync(recursive: true)
130      ..writeAsStringSync(coverageData, flush: true);
131    printTrace('wrote coverage data to $coveragePath (size=${coverageData.length})');
132
133    const String baseCoverageData = 'coverage/lcov.base.info';
134    if (mergeCoverageData) {
135      if (!fs.isFileSync(baseCoverageData)) {
136        printError('Missing "$baseCoverageData". Unable to merge coverage data.');
137        return false;
138      }
139
140      if (os.which('lcov') == null) {
141        String installMessage = 'Please install lcov.';
142        if (platform.isLinux)
143          installMessage = 'Consider running "sudo apt-get install lcov".';
144        else if (platform.isMacOS)
145          installMessage = 'Consider running "brew install lcov".';
146        printError('Missing "lcov" tool. Unable to merge coverage data.\n$installMessage');
147        return false;
148      }
149
150      final Directory tempDir = fs.systemTempDirectory.createTempSync('flutter_tools_test_coverage.');
151      try {
152        final File sourceFile = coverageFile.copySync(fs.path.join(tempDir.path, 'lcov.source.info'));
153        final ProcessResult result = processManager.runSync(<String>[
154          'lcov',
155          '--add-tracefile', baseCoverageData,
156          '--add-tracefile', sourceFile.path,
157          '--output-file', coverageFile.path,
158        ]);
159        if (result.exitCode != 0)
160          return false;
161      } finally {
162        tempDir.deleteSync(recursive: true);
163      }
164    }
165    return true;
166  }
167}
168
169Future<VMService> _defaultConnect(Uri serviceUri) {
170  return VMService.connect(
171      serviceUri, compression: CompressionOptions.compressionOff);
172}
173
174Future<Map<String, dynamic>> collect(Uri serviceUri, bool Function(String) libraryPredicate, {
175  bool waitPaused = false,
176  String debugName,
177  Future<VMService> Function(Uri) connector = _defaultConnect,
178}) async {
179  final VMService vmService = await connector(serviceUri);
180  await vmService.getVM();
181  return _getAllCoverage(vmService, libraryPredicate);
182}
183
184Future<Map<String, dynamic>> _getAllCoverage(VMService service, bool Function(String) libraryPredicate) async {
185  await service.getVM();
186  final List<Map<String, dynamic>> coverage = <Map<String, dynamic>>[];
187  for (Isolate isolateRef in service.vm.isolates) {
188    await isolateRef.load();
189    final Map<String, dynamic> scriptList = await isolateRef.invokeRpcRaw('getScripts', params: <String, dynamic>{'isolateId': isolateRef.id});
190    final List<Future<void>> futures = <Future<void>>[];
191
192    final Map<String, Map<String, dynamic>> scripts = <String, Map<String, dynamic>>{};
193    final Map<String, Map<String, dynamic>> sourceReports = <String, Map<String, dynamic>>{};
194    // For each ScriptRef loaded into the VM, load the corresponding Script and
195    // SourceReport object.
196
197    // We may receive such objects as
198    // {type: Sentinel, kind: Collected, valueAsString: <collected>}
199    // that need to be skipped.
200    if (scriptList['scripts'] == null) {
201      continue;
202    }
203    for (Map<String, dynamic> script in scriptList['scripts']) {
204      if (!libraryPredicate(script['uri'])) {
205        continue;
206      }
207      final String scriptId = script['id'];
208      futures.add(
209        isolateRef.invokeRpcRaw('getSourceReport', params: <String, dynamic>{
210          'forceCompile': true,
211          'scriptId': scriptId,
212          'isolateId': isolateRef.id,
213          'reports': <String>['Coverage'],
214        })
215        .then((Map<String, dynamic> report) {
216          sourceReports[scriptId] = report;
217        })
218      );
219      futures.add(
220        isolateRef.invokeRpcRaw('getObject', params: <String, dynamic>{
221          'isolateId': isolateRef.id,
222          'objectId': scriptId,
223        })
224        .then((Map<String, dynamic> script) {
225          scripts[scriptId] = script;
226        })
227      );
228    }
229    await Future.wait(futures);
230    _buildCoverageMap(scripts, sourceReports, coverage);
231  }
232  return <String, dynamic>{'type': 'CodeCoverage', 'coverage': coverage};
233}
234
235// Build a hitmap of Uri -> Line -> Hit Count for each script object.
236void _buildCoverageMap(
237  Map<String, Map<String, dynamic>> scripts,
238  Map<String, Map<String, dynamic>> sourceReports,
239  List<Map<String, dynamic>> coverage,
240) {
241  final Map<String, Map<int, int>> hitMaps = <String, Map<int, int>>{};
242  for (String scriptId in scripts.keys) {
243    final Map<String, dynamic> sourceReport = sourceReports[scriptId];
244    for (Map<String, dynamic> range in sourceReport['ranges']) {
245      final Map<String, dynamic> coverage = range['coverage'];
246      // Coverage reports may sometimes be null for a Script.
247      if (coverage == null) {
248        continue;
249      }
250      final Map<String, dynamic> scriptRef = sourceReport['scripts'][range['scriptIndex']];
251      final String uri = scriptRef['uri'];
252
253      hitMaps[uri] ??= <int, int>{};
254      final Map<int, int> hitMap = hitMaps[uri];
255      final List<dynamic> hits = coverage['hits'];
256      final List<dynamic> misses = coverage['misses'];
257      final List<dynamic> tokenPositions = scripts[scriptRef['id']]['tokenPosTable'];
258      // The token positions can be null if the script has no coverable lines.
259      if (tokenPositions == null) {
260        continue;
261      }
262      if (hits != null) {
263        for (int hit in hits) {
264          final int line = _lineAndColumn(hit, tokenPositions)[0];
265          final int current = hitMap[line] ?? 0;
266          hitMap[line] = current + 1;
267        }
268      }
269      if (misses != null) {
270        for (int miss in misses) {
271          final int line = _lineAndColumn(miss, tokenPositions)[0];
272          hitMap[line] ??= 0;
273        }
274      }
275    }
276  }
277  hitMaps.forEach((String uri, Map<int, int> hitMap) {
278    coverage.add(_toScriptCoverageJson(uri, hitMap));
279  });
280}
281
282// Binary search the token position table for the line and column which
283// corresponds to each token position.
284// The format of this table is described in https://github.com/dart-lang/sdk/blob/master/runtime/vm/service/service.md#script
285List<int> _lineAndColumn(int position, List<dynamic> tokenPositions) {
286  int min = 0;
287  int max = tokenPositions.length;
288  while (min < max) {
289    final int mid = min + ((max - min) >> 1);
290    final List<dynamic> row = tokenPositions[mid];
291    if (row[1] > position) {
292      max = mid;
293    } else {
294      for (int i = 1; i < row.length; i += 2) {
295        if (row[i] == position) {
296          return <int>[row.first, row[i + 1]];
297        }
298      }
299      min = mid + 1;
300    }
301  }
302  throw StateError('Unreachable');
303}
304
305// Returns a JSON hit map backward-compatible with pre-1.16.0 SDKs.
306Map<String, dynamic> _toScriptCoverageJson(String scriptUri, Map<int, int> hitMap) {
307  final Map<String, dynamic> json = <String, dynamic>{};
308  final List<int> hits = <int>[];
309  hitMap.forEach((int line, int hitCount) {
310    hits.add(line);
311    hits.add(hitCount);
312  });
313  json['source'] = scriptUri;
314  json['script'] = <String, dynamic>{
315    'type': '@Script',
316    'fixedId': true,
317    'id': 'libraries/1/scripts/${Uri.encodeComponent(scriptUri)}',
318    'uri': scriptUri,
319    '_kind': 'library',
320  };
321  json['hits'] = hits;
322  return json;
323}
324