Skip to content

Commit 3625dd8

Browse files
authored
Chrome coverage support (#1155)
Support coverage collection for the Chrome platform. Coverage information is output to `.chrome.json` in a format suitable for consumption by `package:coverage`. Closes #36
1 parent 1fb4609 commit 3625dd8

File tree

6 files changed

+142
-31
lines changed

6 files changed

+142
-31
lines changed

pkgs/test/CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1-
## 1.11.2-dev
1+
## 1.12.0-dev
22

33
* Bump minimum SDK to `2.4.0` for safer usage of for-loop elements.
44
* Deprecate `PhantomJS` and provide warning when used. Support for `PhantomJS`
55
will be removed in version `2.0.0`.
6+
* Support coverage collection for the Chrome platform. See `README.md` for usage
7+
details.
68

79
## 1.11.1
810

pkgs/test/README.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@ specifying it at all, meaning the test order will remain as-is.
186186

187187
### Collecting Code Coverage
188188
To collect code coverage, you can run tests with the `--coverage <directory>`
189-
argument. The directory specified can be an absolute or relative path.
189+
argument. The directory specified can be an absolute or relative path.
190190
If a directory does not exist at the path specified, a directory will be
191191
created. If a directory does exist, files may be overwritten with the latest
192192
coverage data, if they conflict.
@@ -196,7 +196,8 @@ and the resulting coverage files will be outputted in the directory specified.
196196
The files can then be formatted using the `package:coverage`
197197
`format_coverage` executable.
198198

199-
Coverage gathering is currently only implemented for tests run in the Dart VM.
199+
Coverage gathering is currently only implemented for tests run on the Dart VM or
200+
Chrome.
200201

201202
### Restricting Tests to Certain Platforms
202203

@@ -765,7 +766,7 @@ A configuration file can do much more than just set global defaults. See
765766

766767
Tests can be debugged interactively using platforms' built-in development tools.
767768
Tests running on browsers can use those browsers' development consoles to inspect
768-
the document, set breakpoints, and step through code. Those running on the Dart
769+
the document, set breakpoints, and step through code. Those running on the Dart
769770
VM use [the Dart Observatory][observatory]'s .
770771

771772
[observatory]: https://dart-lang.github.io/observatory/

pkgs/test/lib/src/runner/browser/browser_manager.dart

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,15 @@ import 'dart:convert';
88
import 'package:async/async.dart';
99
import 'package:pool/pool.dart';
1010
import 'package:stream_channel/stream_channel.dart';
11-
import 'package:web_socket_channel/web_socket_channel.dart';
12-
1311
import 'package:test_api/src/backend/runtime.dart'; // ignore: implementation_imports
1412
import 'package:test_api/src/util/stack_trace_mapper.dart'; // ignore: implementation_imports
15-
import 'package:test_core/src/runner/runner_suite.dart'; // ignore: implementation_imports
16-
import 'package:test_core/src/runner/environment.dart'; // ignore: implementation_imports
17-
import 'package:test_core/src/runner/suite.dart'; // ignore: implementation_imports
18-
1913
import 'package:test_core/src/runner/application_exception.dart'; // ignore: implementation_imports
14+
import 'package:test_core/src/runner/environment.dart'; // ignore: implementation_imports
2015
import 'package:test_core/src/runner/plugin/platform_helpers.dart'; // ignore: implementation_imports
16+
import 'package:test_core/src/runner/runner_suite.dart'; // ignore: implementation_imports
17+
import 'package:test_core/src/runner/suite.dart'; // ignore: implementation_imports
2118
import 'package:test_core/src/util/io.dart'; // ignore: implementation_imports
19+
import 'package:web_socket_channel/web_socket_channel.dart';
2220

2321
import '../executable_settings.dart';
2422
import 'browser.dart';
@@ -238,8 +236,17 @@ class BrowserManager {
238236
});
239237

240238
try {
241-
controller = deserializeSuite(path, currentPlatform(_runtime),
242-
suiteConfig, await _environment, suiteChannel, message);
239+
controller = deserializeSuite(
240+
path,
241+
currentPlatform(_runtime),
242+
suiteConfig,
243+
await _environment,
244+
suiteChannel,
245+
message, gatherCoverage: () async {
246+
var browser = _browser;
247+
if (browser is Chrome) return browser.gatherCoverage();
248+
return {};
249+
});
243250

244251
controller.channel('test.browser.mapper').sink.add(mapper?.serialize());
245252

pkgs/test/lib/src/runner/browser/chrome.dart

Lines changed: 88 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,21 @@
33
// BSD-style license that can be found in the LICENSE file.
44

55
import 'dart:async';
6+
import 'dart:convert';
67
import 'dart:io';
78

9+
import 'package:coverage/coverage.dart';
10+
import 'package:http/http.dart' as http;
11+
import 'package:path/path.dart' as p;
812
import 'package:pedantic/pedantic.dart';
913
import 'package:test_api/src/backend/runtime.dart'; // ignore: implementation_imports
1014
import 'package:test_core/src/util/io.dart'; // ignore: implementation_imports
15+
import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart';
1116

1217
import '../executable_settings.dart';
1318
import 'browser.dart';
1419
import 'default_settings.dart';
1520

16-
// TODO(nweiz): move this into its own package?
1721
/// A class for running an instance of Chrome.
1822
///
1923
/// Most of the communication with the browser is expected to happen via HTTP,
@@ -28,11 +32,16 @@ class Chrome extends Browser {
2832
@override
2933
final Future<Uri> remoteDebuggerUrl;
3034

35+
final Future<WipConnection> _tabConnection;
36+
final Map<String, String> _idToUrl;
37+
3138
/// Starts a new instance of Chrome open to the given [url], which may be a
3239
/// [Uri] or a [String].
3340
factory Chrome(Uri url, {ExecutableSettings settings, bool debug = false}) {
3441
settings ??= defaultSettings[Runtime.chrome];
3542
var remoteDebuggerCompleter = Completer<Uri>.sync();
43+
var connectionCompleter = Completer<WipConnection>();
44+
var idToUrl = <String, String>{};
3645
return Chrome._(() async {
3746
var tryPort = ([int port]) async {
3847
var dir = createTempDir();
@@ -73,6 +82,8 @@ class Chrome extends Browser {
7382
if (port != null) {
7483
remoteDebuggerCompleter.complete(
7584
getRemoteDebuggerUrl(Uri.parse('http://localhost:$port')));
85+
86+
connectionCompleter.complete(_connect(process, port, idToUrl));
7687
} else {
7788
remoteDebuggerCompleter.complete(null);
7889
}
@@ -85,9 +96,83 @@ class Chrome extends Browser {
8596

8697
if (!debug) return tryPort();
8798
return getUnusedPort<Process>(tryPort);
88-
}, remoteDebuggerCompleter.future);
99+
}, remoteDebuggerCompleter.future, connectionCompleter.future, idToUrl);
89100
}
90101

91-
Chrome._(Future<Process> Function() startBrowser, this.remoteDebuggerUrl)
102+
/// Returns a Dart based hit-map containing coverage report, suitable for use
103+
/// with `package:coverage`.
104+
Future<Map<String, dynamic>> gatherCoverage() async {
105+
var tabConnection = await _tabConnection;
106+
var response = await tabConnection.debugger.connection
107+
.sendCommand('Profiler.takePreciseCoverage', {});
108+
var result = response.result['result'];
109+
var coverage = await parseChromeCoverage(
110+
(result as List).cast(),
111+
_sourceProvider,
112+
_sourceMapProvider,
113+
_sourceUriProvider,
114+
);
115+
return coverage;
116+
}
117+
118+
Chrome._(Future<Process> Function() startBrowser, this.remoteDebuggerUrl,
119+
this._tabConnection, this._idToUrl)
92120
: super(startBrowser);
121+
122+
Future<Uri> _sourceUriProvider(String sourceUrl, String scriptId) async {
123+
var script = _idToUrl[scriptId];
124+
if (script == null) return null;
125+
var uri = Uri.parse(script);
126+
var path = p.join(
127+
p.joinAll(uri.pathSegments.sublist(1, uri.pathSegments.length - 1)),
128+
sourceUrl);
129+
return path.contains('/packages/')
130+
? Uri(scheme: 'package', path: path.split('/packages/').last)
131+
: Uri.file(p.absolute(path));
132+
}
133+
134+
Future<String> _sourceMapProvider(String scriptId) async {
135+
var script = _idToUrl[scriptId];
136+
if (script == null) return null;
137+
var mapResponse = await http.get('$script.map');
138+
if (mapResponse.statusCode != HttpStatus.ok) return null;
139+
return mapResponse.body;
140+
}
141+
142+
Future<String> _sourceProvider(String scriptId) async {
143+
var script = _idToUrl[scriptId];
144+
if (script == null) return null;
145+
var scriptResponse = await http.get(script);
146+
if (scriptResponse.statusCode != HttpStatus.ok) return null;
147+
return scriptResponse.body;
148+
}
149+
}
150+
151+
Future<WipConnection> _connect(
152+
Process process, int port, Map<String, String> idToUrl) async {
153+
// Wait for Chrome to be in a ready state.
154+
await process.stderr
155+
.transform(utf8.decoder)
156+
.transform(LineSplitter())
157+
.firstWhere((line) => line.startsWith('DevTools listening'));
158+
159+
var chromeConnection = ChromeConnection('localhost', port);
160+
var tab = (await chromeConnection.getTabs()).first;
161+
var tabConnection = await tab.connect();
162+
163+
// Enable debugging.
164+
await tabConnection.debugger.enable();
165+
166+
// Coverage reports are in terms of scriptIds so keep note of URLs.
167+
tabConnection.debugger.onScriptParsed.listen((data) {
168+
var script = data.script;
169+
if (script.url.isNotEmpty) idToUrl[script.scriptId] = script.url;
170+
});
171+
172+
// Enable coverage collection.
173+
await tabConnection.debugger.connection.sendCommand('Profiler.enable', {});
174+
await tabConnection.debugger.connection.sendCommand(
175+
'Profiler.startPreciseCoverage', {'detailed': true, 'callCount': false});
176+
177+
return tabConnection;
93178
}

pkgs/test/pubspec.yaml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name: test
2-
version: 1.11.2-dev
2+
version: 1.12.0-dev
33
description: A full featured library for writing and running Dart tests.
44
homepage: https://github.com/dart-lang/test/blob/master/pkgs/test
55

@@ -10,6 +10,7 @@ dependencies:
1010
analyzer: ">=0.36.0 <0.40.0"
1111
async: ^2.0.0
1212
boolean_selector: ">=1.0.0 <3.0.0"
13+
coverage: ^0.13.4
1314
http_multi_server: ^2.0.0
1415
io: ^0.3.0
1516
js: ^0.6.0
@@ -28,6 +29,7 @@ dependencies:
2829
stream_channel: ">=1.7.0 <3.0.0"
2930
typed_data: ^1.0.0
3031
web_socket_channel: ^1.0.0
32+
webkit_inspection_protocol: ^0.5.0
3133
yaml: ^2.0.0
3234
# Use an exact version until the test_api and test_core package are stable.
3335
test_api: 0.2.14

pkgs/test/test/runner/coverage_test.dart

Lines changed: 29 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,26 @@ import 'package:test_descriptor/test_descriptor.dart' as d;
1111

1212
import 'package:path/path.dart' as p;
1313
import 'package:test/test.dart';
14+
import 'package:test_process/test_process.dart';
1415

1516
import '../io.dart';
1617

1718
void main() {
1819
group('with the --coverage flag,', () {
19-
test('gathers coverage for VM tests', () async {
20+
Directory coverageDirectory;
21+
22+
Future<void> _validateCoverage(
23+
TestProcess test, String coveragePath) async {
24+
expect(test.stdout, emitsThrough(contains('+1: All tests passed!')));
25+
await test.shouldExit(0);
26+
27+
final coverageFile = File(p.join(coverageDirectory.path, coveragePath));
28+
final coverage = await coverageFile.readAsString();
29+
final jsonCoverage = json.decode(coverage);
30+
expect(jsonCoverage['coverage'], isNotEmpty);
31+
}
32+
33+
setUp(() async {
2034
await d.file('test.dart', '''
2135
import 'package:test/test.dart';
2236
@@ -27,24 +41,24 @@ void main() {
2741
}
2842
''').create();
2943

30-
final coverageDirectory =
31-
Directory(p.join(Directory.current.path, 'test_coverage'));
32-
expect(await coverageDirectory.exists(), isFalse,
33-
reason:
34-
'Coverage directory exists, cannot safely run coverage tests. Delete the ${coverageDirectory.path} directory to fix.');
44+
coverageDirectory =
45+
await Directory.systemTemp.createTemp('test_coverage');
46+
});
47+
48+
tearDown(() async {
49+
await coverageDirectory.delete(recursive: true);
50+
});
3551

52+
test('gathers coverage for VM tests', () async {
3653
var test =
3754
await runTest(['--coverage', coverageDirectory.path, 'test.dart']);
38-
expect(test.stdout, emitsThrough(contains('+1: All tests passed!')));
39-
await test.shouldExit(0);
40-
41-
final coverageFile =
42-
File(p.join(coverageDirectory.path, 'test.dart.vm.json'));
43-
final coverage = await coverageFile.readAsString();
44-
final jsonCoverage = json.decode(coverage);
45-
expect(jsonCoverage['coverage'], isNotEmpty);
55+
await _validateCoverage(test, 'test.dart.vm.json');
56+
});
4657

47-
await coverageDirectory.delete(recursive: true);
58+
test('gathers coverage for Chrome tests', () async {
59+
var test = await runTest(
60+
['--coverage', coverageDirectory.path, 'test.dart', '-p', 'chrome']);
61+
await _validateCoverage(test, 'test.dart.chrome.json');
4862
});
4963
});
5064
}

0 commit comments

Comments
 (0)