-
Notifications
You must be signed in to change notification settings - Fork 218
Chrome coverage support #1155
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Chrome coverage support #1155
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,17 +8,15 @@ import 'dart:convert'; | |
import 'package:async/async.dart'; | ||
import 'package:pool/pool.dart'; | ||
import 'package:stream_channel/stream_channel.dart'; | ||
import 'package:web_socket_channel/web_socket_channel.dart'; | ||
|
||
import 'package:test_api/src/backend/runtime.dart'; // ignore: implementation_imports | ||
import 'package:test_api/src/util/stack_trace_mapper.dart'; // ignore: implementation_imports | ||
import 'package:test_core/src/runner/runner_suite.dart'; // ignore: implementation_imports | ||
import 'package:test_core/src/runner/environment.dart'; // ignore: implementation_imports | ||
import 'package:test_core/src/runner/suite.dart'; // ignore: implementation_imports | ||
|
||
import 'package:test_core/src/runner/application_exception.dart'; // ignore: implementation_imports | ||
import 'package:test_core/src/runner/environment.dart'; // ignore: implementation_imports | ||
import 'package:test_core/src/runner/plugin/platform_helpers.dart'; // ignore: implementation_imports | ||
import 'package:test_core/src/runner/runner_suite.dart'; // ignore: implementation_imports | ||
import 'package:test_core/src/runner/suite.dart'; // ignore: implementation_imports | ||
import 'package:test_core/src/util/io.dart'; // ignore: implementation_imports | ||
import 'package:web_socket_channel/web_socket_channel.dart'; | ||
|
||
import '../executable_settings.dart'; | ||
import 'browser.dart'; | ||
|
@@ -238,8 +236,17 @@ class BrowserManager { | |
}); | ||
|
||
try { | ||
controller = deserializeSuite(path, currentPlatform(_runtime), | ||
suiteConfig, await _environment, suiteChannel, message); | ||
controller = deserializeSuite( | ||
path, | ||
currentPlatform(_runtime), | ||
suiteConfig, | ||
await _environment, | ||
suiteChannel, | ||
message, gatherCoverage: () async { | ||
var browser = _browser; | ||
if (browser is Chrome) return browser.gatherCoverage(); | ||
return {}; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should we warn here or anything? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This will produce an empty result which is consistent with the current behavior. I might follow up with a PR that warns at the time of arg parsing. |
||
}); | ||
|
||
controller.channel('test.browser.mapper').sink.add(mapper?.serialize()); | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,17 +3,21 @@ | |
// BSD-style license that can be found in the LICENSE file. | ||
|
||
import 'dart:async'; | ||
import 'dart:convert'; | ||
import 'dart:io'; | ||
|
||
import 'package:coverage/coverage.dart'; | ||
import 'package:http/http.dart' as http; | ||
import 'package:path/path.dart' as p; | ||
import 'package:pedantic/pedantic.dart'; | ||
import 'package:test_api/src/backend/runtime.dart'; // ignore: implementation_imports | ||
import 'package:test_core/src/util/io.dart'; // ignore: implementation_imports | ||
import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart'; | ||
|
||
import '../executable_settings.dart'; | ||
import 'browser.dart'; | ||
import 'default_settings.dart'; | ||
|
||
// TODO(nweiz): move this into its own package? | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are we using it? Should we file an issue to migrate to that package? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
/// A class for running an instance of Chrome. | ||
/// | ||
/// Most of the communication with the browser is expected to happen via HTTP, | ||
|
@@ -28,11 +32,16 @@ class Chrome extends Browser { | |
@override | ||
final Future<Uri> remoteDebuggerUrl; | ||
|
||
final Future<WipConnection> _tabConnection; | ||
final Map<String, String> _idToUrl; | ||
|
||
/// Starts a new instance of Chrome open to the given [url], which may be a | ||
/// [Uri] or a [String]. | ||
factory Chrome(Uri url, {ExecutableSettings settings, bool debug = false}) { | ||
settings ??= defaultSettings[Runtime.chrome]; | ||
var remoteDebuggerCompleter = Completer<Uri>.sync(); | ||
var connectionCompleter = Completer<WipConnection>(); | ||
var idToUrl = <String, String>{}; | ||
return Chrome._(() async { | ||
var tryPort = ([int port]) async { | ||
var dir = createTempDir(); | ||
|
@@ -73,6 +82,8 @@ class Chrome extends Browser { | |
if (port != null) { | ||
remoteDebuggerCompleter.complete( | ||
getRemoteDebuggerUrl(Uri.parse('http://localhost:$port'))); | ||
|
||
connectionCompleter.complete(_connect(process, port, idToUrl)); | ||
} else { | ||
remoteDebuggerCompleter.complete(null); | ||
} | ||
|
@@ -85,9 +96,83 @@ class Chrome extends Browser { | |
|
||
if (!debug) return tryPort(); | ||
return getUnusedPort<Process>(tryPort); | ||
}, remoteDebuggerCompleter.future); | ||
}, remoteDebuggerCompleter.future, connectionCompleter.future, idToUrl); | ||
} | ||
|
||
Chrome._(Future<Process> Function() startBrowser, this.remoteDebuggerUrl) | ||
/// Returns a Dart based hit-map containing coverage report, suitable for use | ||
/// with `package:coverage`. | ||
Future<Map<String, dynamic>> gatherCoverage() async { | ||
var tabConnection = await _tabConnection; | ||
var response = await tabConnection.debugger.connection | ||
.sendCommand('Profiler.takePreciseCoverage', {}); | ||
var result = response.result['result']; | ||
var coverage = await parseChromeCoverage( | ||
(result as List).cast(), | ||
_sourceProvider, | ||
_sourceMapProvider, | ||
_sourceUriProvider, | ||
); | ||
return coverage; | ||
} | ||
|
||
Chrome._(Future<Process> Function() startBrowser, this.remoteDebuggerUrl, | ||
this._tabConnection, this._idToUrl) | ||
: super(startBrowser); | ||
|
||
Future<Uri> _sourceUriProvider(String sourceUrl, String scriptId) async { | ||
var script = _idToUrl[scriptId]; | ||
if (script == null) return null; | ||
var uri = Uri.parse(script); | ||
var path = p.join( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: you could do var path = p.joinAll([
...uri.pathSegments.sublist(1, uri.pathSegments.length - 1),
sourceUrl,
]); |
||
p.joinAll(uri.pathSegments.sublist(1, uri.pathSegments.length - 1)), | ||
sourceUrl); | ||
return path.contains('/packages/') | ||
? Uri(scheme: 'package', path: path.split('/packages/').last) | ||
: Uri.file(p.absolute(path)); | ||
} | ||
|
||
Future<String> _sourceMapProvider(String scriptId) async { | ||
var script = _idToUrl[scriptId]; | ||
if (script == null) return null; | ||
var mapResponse = await http.get('$script.map'); | ||
if (mapResponse.statusCode != HttpStatus.ok) return null; | ||
return mapResponse.body; | ||
} | ||
|
||
Future<String> _sourceProvider(String scriptId) async { | ||
var script = _idToUrl[scriptId]; | ||
if (script == null) return null; | ||
var scriptResponse = await http.get(script); | ||
if (scriptResponse.statusCode != HttpStatus.ok) return null; | ||
return scriptResponse.body; | ||
} | ||
} | ||
|
||
Future<WipConnection> _connect( | ||
Process process, int port, Map<String, String> idToUrl) async { | ||
// Wait for Chrome to be in a ready state. | ||
await process.stderr | ||
.transform(utf8.decoder) | ||
.transform(LineSplitter()) | ||
.firstWhere((line) => line.startsWith('DevTools listening')); | ||
|
||
var chromeConnection = ChromeConnection('localhost', port); | ||
var tab = (await chromeConnection.getTabs()).first; | ||
var tabConnection = await tab.connect(); | ||
|
||
// Enable debugging. | ||
await tabConnection.debugger.enable(); | ||
|
||
// Coverage reports are in terms of scriptIds so keep note of URLs. | ||
tabConnection.debugger.onScriptParsed.listen((data) { | ||
var script = data.script; | ||
if (script.url.isNotEmpty) idToUrl[script.scriptId] = script.url; | ||
}); | ||
|
||
// Enable coverage collection. | ||
await tabConnection.debugger.connection.sendCommand('Profiler.enable', {}); | ||
await tabConnection.debugger.connection.sendCommand( | ||
'Profiler.startPreciseCoverage', {'detailed': true, 'callCount': false}); | ||
|
||
return tabConnection; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
1.12.0 since this is a new feature 🎉
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.