Skip to content
This repository was archived by the owner on Aug 28, 2024. It is now read-only.

Commit ac28541

Browse files
authored
parseChromeCoverage (#281)
Towards dart-lang/test#36 Add `parseChromeCoverage` method to be used by `package:test` and internal tools. This method returns a hit-map based Dart report suitable for consumption by `package:coverage` to produce LCOV reports.
1 parent 5880488 commit ac28541

11 files changed

+304
-23
lines changed

CHANGELOG.md

+5
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
## 0.13.4 - 2020-01-23
2+
3+
* Add `parseChromeCoverage` for creating a Dart based coverage report from a
4+
Chrome coverage report.
5+
16
## 0.13.3+3 - 2019-12-03
27

38
* Re-loosen the dependency on the `vm_service` package from `>=1.0.0 < 2.1.2`

lib/coverage.dart

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// for details. All rights reserved. Use of this source code is governed by a
33
// BSD-style license that can be found in the LICENSE file.
44

5+
export 'src/chrome.dart';
56
export 'src/collect.dart';
67
export 'src/formatter.dart';
78
export 'src/hitmap.dart';

lib/src/chrome.dart

+155
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'package:coverage/src/util.dart';
6+
import 'package:source_maps/parser.dart';
7+
8+
/// Returns a Dart based hit-map containing coverage report for the provided
9+
/// Chrome [preciseCoverage].
10+
///
11+
/// [sourceProvider] returns the source content for the Chrome scriptId, or null
12+
/// if not available.
13+
///
14+
/// [sourceMapProvider] returns the associated source map content for the Chrome
15+
/// scriptId, or null if not available.
16+
///
17+
/// [sourceUriProvider] returns the uri for the provided sourceUrl and
18+
/// associated scriptId.
19+
///
20+
/// Chrome coverage information for which the corresponding source map or source
21+
/// content is null will be ignored.
22+
Future<Map<String, dynamic>> parseChromeCoverage(
23+
List<Map<String, dynamic>> preciseCoverage,
24+
Future<String> Function(String scriptId) sourceProvider,
25+
Future<String> Function(String scriptId) sourceMapProvider,
26+
Future<Uri> Function(String sourceUrl, String scriptId) sourceUriProvider,
27+
) async {
28+
final coverageReport = <Uri, Map<int, bool>>{};
29+
for (Map<String, dynamic> entry in preciseCoverage) {
30+
final String scriptId = entry['scriptId'];
31+
32+
final mapResponse = await sourceMapProvider(scriptId);
33+
if (mapResponse == null) continue;
34+
35+
final SingleMapping mapping = parse(mapResponse);
36+
37+
final compiledSource = await sourceProvider(scriptId);
38+
if (compiledSource == null) continue;
39+
40+
final coverageInfo = _coverageInfoFor(entry);
41+
final offsetCoverage = _offsetCoverage(coverageInfo, compiledSource.length);
42+
final coveredPositions = _coveredPositions(compiledSource, offsetCoverage);
43+
44+
for (var lineEntry in mapping.lines) {
45+
for (var columnEntry in lineEntry.entries) {
46+
if (columnEntry.sourceUrlId == null) continue;
47+
final sourceUrl = mapping.urls[columnEntry.sourceUrlId];
48+
49+
// Ignore coverage information for the SDK.
50+
if (sourceUrl.startsWith('org-dartlang-sdk:')) continue;
51+
52+
final uri = await sourceUriProvider(sourceUrl, scriptId);
53+
final coverage = coverageReport.putIfAbsent(uri, () => <int, bool>{});
54+
55+
coverage[columnEntry.sourceLine + 1] = coveredPositions
56+
.contains(_Position(lineEntry.line + 1, columnEntry.column + 1));
57+
}
58+
}
59+
}
60+
61+
final coverageHitMaps = <Uri, Map<int, int>>{};
62+
coverageReport.forEach((uri, coverage) {
63+
final hitMap = <int, int>{};
64+
for (var line in coverage.keys.toList()..sort()) {
65+
hitMap[line] = coverage[line] ? 1 : 0;
66+
}
67+
coverageHitMaps[uri] = hitMap;
68+
});
69+
70+
final allCoverage = <Map<String, dynamic>>[];
71+
coverageHitMaps.forEach((uri, hitMap) {
72+
allCoverage.add(toScriptCoverageJson(uri, hitMap));
73+
});
74+
return <String, dynamic>{'type': 'CodeCoverage', 'coverage': allCoverage};
75+
}
76+
77+
/// Returns all covered positions in a provided source.
78+
Set<_Position> _coveredPositions(
79+
String compiledSource, List<bool> offsetCoverage) {
80+
final positions = Set<_Position>();
81+
// Line is 1 based.
82+
var line = 1;
83+
// Column is 1 based.
84+
var column = 0;
85+
for (var offset = 0; offset < compiledSource.length; offset++) {
86+
if (compiledSource[offset] == '\n') {
87+
line++;
88+
column = 0;
89+
} else {
90+
column++;
91+
}
92+
if (offsetCoverage[offset]) positions.add(_Position(line, column));
93+
}
94+
return positions;
95+
}
96+
97+
/// Returns coverage information for a Chrome entry.
98+
List<_CoverageInfo> _coverageInfoFor(Map<String, dynamic> entry) {
99+
final result = <_CoverageInfo>[];
100+
for (Map<String, dynamic> functions in entry['functions']) {
101+
for (Map<String, dynamic> range in functions['ranges']) {
102+
result.add(_CoverageInfo(
103+
range['startOffset'],
104+
range['endOffset'],
105+
range['count'] > 0,
106+
));
107+
}
108+
}
109+
return result;
110+
}
111+
112+
/// Returns the coverage information for each offset.
113+
List<bool> _offsetCoverage(List<_CoverageInfo> coverageInfo, int sourceLength) {
114+
final offsetCoverage = List.filled(sourceLength, false);
115+
116+
// Sort coverage information by their size.
117+
// Coverage information takes granularity as precedence.
118+
coverageInfo.sort((a, b) =>
119+
(b.endOffset - b.startOffset).compareTo(a.endOffset - a.startOffset));
120+
121+
for (var range in coverageInfo) {
122+
for (var i = range.startOffset; i < range.endOffset; i++) {
123+
offsetCoverage[i] = range.isCovered;
124+
}
125+
}
126+
127+
return offsetCoverage;
128+
}
129+
130+
class _CoverageInfo {
131+
_CoverageInfo(this.startOffset, this.endOffset, this.isCovered);
132+
133+
/// 0 based byte offset.
134+
final int startOffset;
135+
136+
/// 0 based byte offset.
137+
final int endOffset;
138+
139+
final bool isCovered;
140+
}
141+
142+
/// A covered position in a source file where [line] and [column] are 1 based.
143+
class _Position {
144+
_Position(this.line, this.column);
145+
146+
final int line;
147+
final int column;
148+
149+
@override
150+
int get hashCode => hash2(line, column);
151+
152+
@override
153+
bool operator ==(dynamic o) =>
154+
o is _Position && o.line == line && o.column == column;
155+
}

lib/src/collect.dart

+1-22
Original file line numberDiff line numberDiff line change
@@ -217,32 +217,11 @@ Future<List<Map<String, dynamic>>> _getCoverageJson(VmService service,
217217
// Output JSON
218218
final coverage = <Map<String, dynamic>>[];
219219
hitMaps.forEach((uri, hitMap) {
220-
coverage.add(_toScriptCoverageJson(uri, hitMap));
220+
coverage.add(toScriptCoverageJson(uri, hitMap));
221221
});
222222
return coverage;
223223
}
224224

225-
/// Returns a JSON hit map backward-compatible with pre-1.16.0 SDKs.
226-
Map<String, dynamic> _toScriptCoverageJson(
227-
Uri scriptUri, Map<int, int> hitMap) {
228-
final json = <String, dynamic>{};
229-
final hits = <int>[];
230-
hitMap.forEach((line, hitCount) {
231-
hits.add(line);
232-
hits.add(hitCount);
233-
});
234-
json['source'] = '$scriptUri';
235-
json['script'] = {
236-
'type': '@Script',
237-
'fixedId': true,
238-
'id': 'libraries/1/scripts/${Uri.encodeComponent(scriptUri.toString())}',
239-
'uri': '$scriptUri',
240-
'_kind': 'library',
241-
};
242-
json['hits'] = hits;
243-
return json;
244-
}
245-
246225
class StdoutLog extends Log {
247226
@override
248227
void warning(String message) => print(message);

lib/src/util.dart

+36
Original file line numberDiff line numberDiff line change
@@ -72,3 +72,39 @@ Future<int> getOpenPort() async {
7272
await socket.close();
7373
}
7474
}
75+
76+
/// Returns a JSON hit map backward-compatible with pre-1.16.0 SDKs.
77+
Map<String, dynamic> toScriptCoverageJson(Uri scriptUri, Map<int, int> hitMap) {
78+
final json = <String, dynamic>{};
79+
final hits = <int>[];
80+
hitMap.forEach((line, hitCount) {
81+
hits.add(line);
82+
hits.add(hitCount);
83+
});
84+
json['source'] = '$scriptUri';
85+
json['script'] = {
86+
'type': '@Script',
87+
'fixedId': true,
88+
'id': 'libraries/1/scripts/${Uri.encodeComponent(scriptUri.toString())}',
89+
'uri': '$scriptUri',
90+
'_kind': 'library',
91+
};
92+
json['hits'] = hits;
93+
return json;
94+
}
95+
96+
/// Generates a hash code for two objects.
97+
int hash2(dynamic a, dynamic b) =>
98+
_finish(_combine(_combine(0, a.hashCode), b.hashCode));
99+
100+
int _combine(int hash, int value) {
101+
hash = 0x1fffffff & (hash + value);
102+
hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
103+
return hash ^ (hash >> 6);
104+
}
105+
106+
int _finish(int hash) {
107+
hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
108+
hash = hash ^ (hash >> 11);
109+
return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
110+
}

pubspec.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name: coverage
2-
version: 0.13.3+3
2+
version: 0.13.4
33
author: Dart Team <[email protected]>
44
description: Coverage data manipulation and formatting
55
homepage: https://github.com/dart-lang/coverage

test/chrome_test.dart

+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'dart:convert';
6+
import 'dart:io';
7+
8+
import 'package:coverage/coverage.dart';
9+
import 'package:test/test.dart';
10+
11+
Future<String> sourceMapProvider(String scriptId) async {
12+
// The scriptId for the main_test.ddc.js in the sample report is 37.
13+
if (scriptId != '37') return null;
14+
return File('test/test_files/main_test.ddc.js.map').readAsString();
15+
}
16+
17+
Future<String> sourceProvider(String scriptId) async {
18+
// The scriptId for the main_test.ddc.js in the sample report is 37.
19+
if (scriptId != '37') return null;
20+
return File('test/test_files/main_test.ddc.js').readAsString();
21+
}
22+
23+
Future<Uri> sourceUriProvider(String sourceUrl, String scriptId) async =>
24+
Uri.parse(sourceUrl);
25+
26+
void main() {
27+
test('reports correctly', () async {
28+
final preciseCoverage = json.decode(
29+
await File('test/test_files/chrome_precise_report.txt').readAsString());
30+
31+
final report = await parseChromeCoverage(
32+
// ignore: avoid_as
33+
(preciseCoverage as List).cast(),
34+
sourceProvider,
35+
sourceMapProvider,
36+
sourceUriProvider,
37+
);
38+
39+
final coverage = report['coverage'];
40+
expect(coverage.length, equals(1));
41+
42+
final sourceReport = coverage.first;
43+
expect(sourceReport['source'], equals('main_test.dart'));
44+
45+
final Map<int, int> expectedHits = {
46+
5: 1,
47+
6: 1,
48+
7: 1,
49+
8: 0,
50+
10: 1,
51+
11: 1,
52+
13: 1,
53+
14: 1,
54+
15: 1,
55+
};
56+
57+
final List<int> hitMap = sourceReport['hits'];
58+
expect(hitMap.length, equals(expectedHits.keys.length * 2));
59+
for (var i = 0; i < hitMap.length; i += 2) {
60+
expect(expectedHits[hitMap[i]], equals(hitMap[i + 1]));
61+
}
62+
});
63+
}

test/test_all.dart

+2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import 'package:test/test.dart';
66

7+
import 'chrome_test.dart' as chrome;
78
import 'collect_coverage_api_test.dart' as collect_coverage_api;
89
import 'collect_coverage_test.dart' as collect_coverage;
910
import 'lcov_test.dart' as lcov;
@@ -18,4 +19,5 @@ void main() {
1819
group('resolver', resolver.main);
1920
group('run_and_collect', run_and_collect.main);
2021
group('util', util.main);
22+
group('chrome', chrome.main);
2123
}

test/test_files/chrome_precise_report.txt

+1
Large diffs are not rendered by default.

test/test_files/main_test.ddc.js

+38
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

test/test_files/main_test.ddc.js.map

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)