diff --git a/packages/flutter_tools/bin/fuchsia_tester.dart b/packages/flutter_tools/bin/fuchsia_tester.dart index 5b4d7d76bd..7e16ab180c 100644 --- a/packages/flutter_tools/bin/fuchsia_tester.dart +++ b/packages/flutter_tools/bin/fuchsia_tester.dart @@ -112,16 +112,13 @@ Future run(List args) async { Directory testDirectory; CoverageCollector collector; if (argResults['coverage'] as bool) { + // If we have a specified coverage directory then accept all libraries by + // setting libraryNames to null. + final Set libraryNames = coverageDirectory != null ? null : + {FlutterProject.current().manifest.appName}; collector = CoverageCollector( packagesPath: globals.fs.path.normalize(globals.fs.path.absolute(argResults[_kOptionPackages] as String)), - libraryPredicate: (String libraryName) { - // If we have a specified coverage directory then accept all libraries. - if (coverageDirectory != null) { - return true; - } - final String projectName = FlutterProject.current().manifest.appName; - return libraryName.contains(projectName); - }); + libraryNames: libraryNames); if (!argResults.options.contains(_kOptionTestDirectory)) { throwToolExit('Use of --coverage requires setting --test-directory'); } diff --git a/packages/flutter_tools/lib/src/commands/test.dart b/packages/flutter_tools/lib/src/commands/test.dart index d36e5f6800..b851e84205 100644 --- a/packages/flutter_tools/lib/src/commands/test.dart +++ b/packages/flutter_tools/lib/src/commands/test.dart @@ -380,7 +380,7 @@ class TestCommand extends FlutterCommand with DeviceBasedDevelopmentArtifacts { final String projectName = flutterProject.manifest.appName; collector = CoverageCollector( verbose: !machine, - libraryPredicate: (String libraryName) => libraryName.contains(projectName), + libraryNames: {projectName}, packagesPath: buildInfo.packagesPath ); } diff --git a/packages/flutter_tools/lib/src/test/coverage_collector.dart b/packages/flutter_tools/lib/src/test/coverage_collector.dart index 46d3a76bbe..b17510e4e3 100644 --- a/packages/flutter_tools/lib/src/test/coverage_collector.dart +++ b/packages/flutter_tools/lib/src/test/coverage_collector.dart @@ -19,7 +19,7 @@ import 'watcher.dart'; /// A class that collects code coverage data during test runs. class CoverageCollector extends TestWatcher { - CoverageCollector({this.libraryPredicate, this.verbose = true, @required this.packagesPath}); + CoverageCollector({this.libraryNames, this.verbose = true, @required this.packagesPath}); /// True when log messages should be emitted. final bool verbose; @@ -31,9 +31,9 @@ class CoverageCollector extends TestWatcher { /// Map of file path to coverage hit map for that file. Map _globalHitmap; - /// Predicate function that returns true if the specified library URI should - /// be included in the computed coverage. - bool Function(String) libraryPredicate; + /// The names of the libraries to gather coverage for. If null, all libraries + /// will be accepted. + Set libraryNames; @override Future handleFinishedTest(TestDevice testDevice) async { @@ -83,7 +83,7 @@ class CoverageCollector extends TestWatcher { Future collectCoverageIsolate(Uri observatoryUri) async { assert(observatoryUri != null); _logMessage('collecting coverage data from $observatoryUri...'); - final Map data = await collect(observatoryUri, libraryPredicate); + final Map data = await collect(observatoryUri, libraryNames); if (data == null) { throw Exception('Failed to collect coverage.'); } @@ -121,7 +121,7 @@ class CoverageCollector extends TestWatcher { final Future collectionComplete = testDevice.observatoryUri .then((Uri observatoryUri) { _logMessage('collecting coverage data from $testDevice at $observatoryUri...'); - return collect(observatoryUri, libraryPredicate) + return collect(observatoryUri, libraryNames) .then((Map result) { if (result == null) { throw Exception('Failed to collect coverage.'); @@ -237,77 +237,94 @@ Future _defaultConnect(Uri serviceUri) { serviceUri, compression: CompressionOptions.compressionOff, logger: globals.logger,); } -Future> collect(Uri serviceUri, bool Function(String) libraryPredicate, { +Future> collect(Uri serviceUri, Set libraryNames, { bool waitPaused = false, String debugName, Future Function(Uri) connector = _defaultConnect, @visibleForTesting bool forceSequential = false, }) async { final FlutterVmService vmService = await connector(serviceUri); - final Map result = await _getAllCoverage(vmService.service, libraryPredicate, forceSequential); + final Map result = await _getAllCoverage(vmService.service, libraryNames, forceSequential); await vmService.dispose(); return result; } Future> _getAllCoverage( vm_service.VmService service, - bool Function(String) libraryPredicate, + Set libraryNames, bool forceSequential, ) async { final vm_service.Version version = await service.getVersion(); - final bool reportLines = (version.major == 3 && version.minor >= 51) || version.major > 3; + final bool libraryFilters = (version.major == 3 && version.minor >= 57) || version.major > 3; final vm_service.VM vm = await service.getVM(); final List> coverage = >[]; + bool libraryPredicate(String libraryName) { + if (libraryNames == null) { + return true; + } + final Uri uri = Uri.parse(libraryName); + if (uri.scheme != 'package') { + return false; + } + final String scope = uri.path.split('/').first; + return libraryNames.contains(scope); + } for (final vm_service.IsolateRef isolateRef in vm.isolates) { if (isolateRef.isSystemIsolate) { continue; } - vm_service.ScriptList scriptList; - try { - scriptList = await service.getScripts(isolateRef.id); - } on vm_service.SentinelException { - continue; - } - - final List> futures = >[]; - final Map scripts = {}; - final Map sourceReports = {}; - // For each ScriptRef loaded into the VM, load the corresponding Script and - // SourceReport object. - - for (final vm_service.ScriptRef script in scriptList.scripts) { - final String libraryUri = script.uri; - if (!libraryPredicate(libraryUri)) { + if (libraryFilters) { + final vm_service.SourceReport sourceReport = await service.getSourceReport( + isolateRef.id, + ['Coverage'], + forceCompile: true, + reportLines: true, + libraryFilters: libraryNames == null ? null : List.from( + libraryNames.map((String name) => 'package:$name/')), + ); + _buildCoverageMap( + {}, + [sourceReport], + coverage, + ); + } else { + vm_service.ScriptList scriptList; + try { + scriptList = await service.getScripts(isolateRef.id); + } on vm_service.SentinelException { continue; } - final String scriptId = script.id; - final Future getSourceReport = service.getSourceReport( - isolateRef.id, - ['Coverage'], - scriptId: scriptId, - forceCompile: true, - reportLines: reportLines ? true : null, - ) - .then((vm_service.SourceReport report) { - sourceReports[scriptId] = report; - }); - if (forceSequential) { - await null; - } - futures.add(getSourceReport); - if (reportLines) { - continue; - } - final Future getObject = service - .getObject(isolateRef.id, scriptId) - .then((vm_service.Obj response) { - final vm_service.Script script = response as vm_service.Script; - scripts[scriptId] = script; + + final List> futures = >[]; + final Map scripts = {}; + final List sourceReports = []; + + // For each ScriptRef loaded into the VM, load the corresponding Script + // and SourceReport object. + for (final vm_service.ScriptRef script in scriptList.scripts) { + final String libraryUri = script.uri; + if (!libraryPredicate(libraryUri)) { + continue; + } + final String scriptId = script.id; + final Future getSourceReport = service.getSourceReport( + isolateRef.id, + ['Coverage'], + scriptId: scriptId, + forceCompile: true, + reportLines: true, + ) + .then((vm_service.SourceReport report) { + sourceReports.add(report); }); - futures.add(getObject); + if (forceSequential) { + await null; + } + futures.add(getSourceReport); + } + await Future.wait(futures); + _buildCoverageMap(scripts, sourceReports, coverage); } - await Future.wait(futures); - _buildCoverageMap(scripts, sourceReports, coverage, reportLines); } return {'type': 'CodeCoverage', 'coverage': coverage}; } @@ -315,13 +332,11 @@ Future> _getAllCoverage( // Build a hitmap of Uri -> Line -> Hit Count for each script object. void _buildCoverageMap( Map scripts, - Map sourceReports, + List sourceReports, List> coverage, - bool reportLines, ) { final Map> hitMaps = >{}; - for (final String scriptId in sourceReports.keys) { - final vm_service.SourceReport sourceReport = sourceReports[scriptId]; + for (final vm_service.SourceReport sourceReport in sourceReports) { for (final vm_service.SourceReportRange range in sourceReport.ranges) { final vm_service.SourceReportCoverage coverage = range.coverage; // Coverage reports may sometimes be null for a Script. @@ -335,24 +350,14 @@ void _buildCoverageMap( final Map hitMap = hitMaps[uri]; final List hits = coverage.hits; final List misses = coverage.misses; - final List tokenPositions = scripts[scriptRef.id]?.tokenPosTable; - // The token positions can be null if the script has no lines that may be - // covered. It will also be null if reportLines is true. - if (tokenPositions == null && !reportLines) { - continue; - } if (hits != null) { - for (final int hit in hits) { - final int line = - reportLines ? hit : _lineAndColumn(hit, tokenPositions)[0]; + for (final int line in hits) { final int current = hitMap[line] ?? 0; hitMap[line] = current + 1; } } if (misses != null) { - for (final int miss in misses) { - final int line = - reportLines ? miss : _lineAndColumn(miss, tokenPositions)[0]; + for (final int line in misses) { hitMap[line] ??= 0; } } @@ -363,29 +368,6 @@ void _buildCoverageMap( }); } -// Binary search the token position table for the line and column which -// corresponds to each token position. -// The format of this table is described in https://github.com/dart-lang/sdk/blob/main/runtime/vm/service/service.md#script -List _lineAndColumn(int position, List tokenPositions) { - int min = 0; - int max = tokenPositions.length; - while (min < max) { - final int mid = min + ((max - min) >> 1); - final List row = (tokenPositions[mid] as List).cast(); - if (row[1] > position) { - max = mid; - } else { - for (int i = 1; i < row.length; i += 2) { - if (row[i] == position) { - return [row.first, row[i + 1]]; - } - } - min = mid + 1; - } - } - throw StateError('Unreachable'); -} - // Returns a JSON hit map backward-compatible with pre-1.16.0 SDKs. Map _toScriptCoverageJson(String scriptUri, Map hitMap) { final Map json = {}; diff --git a/packages/flutter_tools/test/general.shard/coverage_collector_test.dart b/packages/flutter_tools/test/general.shard/coverage_collector_test.dart index df502786f6..e7d06d32a9 100644 --- a/packages/flutter_tools/test/general.shard/coverage_collector_test.dart +++ b/packages/flutter_tools/test/general.shard/coverage_collector_test.dart @@ -16,7 +16,7 @@ void main() { requests: [ FakeVmServiceRequest( method: 'getVersion', - jsonResponse: Version(major: 3, minor: 50).toJson(), + jsonResponse: Version(major: 3, minor: 51).toJson(), ), FakeVmServiceRequest( method: 'getVM', @@ -42,7 +42,7 @@ void main() { final Map result = await collect( null, - (String predicate) => true, + {'foo'}, connector: (Uri uri) async { return fakeVmServiceHost.vmService; }, @@ -53,104 +53,6 @@ void main() { }); testWithoutContext('Coverage collector processes coverage and script data', () async { - final FakeVmServiceHost fakeVmServiceHost = FakeVmServiceHost( - requests: [ - FakeVmServiceRequest( - method: 'getVersion', - jsonResponse: Version(major: 3, minor: 50).toJson(), - ), - FakeVmServiceRequest( - method: 'getVM', - jsonResponse: (VM.parse({}) - ..isolates = [ - IsolateRef.parse({ - 'id': '1', - }), - ] - ).toJson(), - ), - FakeVmServiceRequest( - method: 'getScripts', - args: { - 'isolateId': '1', - }, - jsonResponse: ScriptList(scripts: [ - ScriptRef(uri: 'foo.dart', id: '1'), - ]).toJson(), - ), - FakeVmServiceRequest( - method: 'getSourceReport', - args: { - 'isolateId': '1', - 'reports': ['Coverage'], - 'scriptId': '1', - 'forceCompile': true, - }, - jsonResponse: SourceReport( - ranges: [ - SourceReportRange( - scriptIndex: 0, - startPos: 0, - endPos: 0, - compiled: true, - coverage: SourceReportCoverage( - hits: [], - misses: [], - ), - ), - ], - scripts: [ - ScriptRef( - uri: 'foo.dart', - id: '1', - ), - ], - ).toJson(), - ), - FakeVmServiceRequest( - method: 'getObject', - args: { - 'isolateId': '1', - 'objectId': '1', - }, - jsonResponse: Script( - uri: 'foo.dart', - id: '1', - library: LibraryRef(name: '', id: '1111', uri: 'foo.dart'), - tokenPosTable: >[], - ).toJson(), - ), - ], - ); - - final Map result = await collect( - null, - (String predicate) => true, - connector: (Uri uri) async { - return fakeVmServiceHost.vmService; - }, - ); - - expect(result, { - 'type': 'CodeCoverage', - 'coverage': [ - { - 'source': 'foo.dart', - 'script': { - 'type': '@Script', - 'fixedId': true, - 'id': 'libraries/1/scripts/foo.dart', - 'uri': 'foo.dart', - '_kind': 'library', - }, - 'hits': [], - }, - ], - }); - expect(fakeVmServiceHost.hasRemainingExpectations, false); - }); - - testWithoutContext('Coverage collector skips loading Script objects when reportLines is available', () async { final FakeVmServiceHost fakeVmServiceHost = FakeVmServiceHost( requests: [ FakeVmServiceRequest( @@ -173,7 +75,8 @@ void main() { 'isolateId': '1', }, jsonResponse: ScriptList(scripts: [ - ScriptRef(uri: 'foo.dart', id: '1'), + ScriptRef(uri: 'package:foo/foo.dart', id: '1'), + ScriptRef(uri: 'package:bar/bar.dart', id: '2'), ]).toJson(), ), FakeVmServiceRequest( @@ -200,7 +103,7 @@ void main() { ], scripts: [ ScriptRef( - uri: 'foo.dart', + uri: 'package:foo/foo.dart', id: '1', ), ], @@ -211,7 +114,7 @@ void main() { final Map result = await collect( null, - (String predicate) => true, + {'foo'}, connector: (Uri uri) async { return fakeVmServiceHost.vmService; }, @@ -221,12 +124,293 @@ void main() { 'type': 'CodeCoverage', 'coverage': [ { - 'source': 'foo.dart', + 'source': 'package:foo/foo.dart', 'script': { 'type': '@Script', 'fixedId': true, - 'id': 'libraries/1/scripts/foo.dart', - 'uri': 'foo.dart', + 'id': 'libraries/1/scripts/package%3Afoo%2Ffoo.dart', + 'uri': 'package:foo/foo.dart', + '_kind': 'library', + }, + 'hits': [1, 1, 3, 1, 2, 0], + }, + ], + }); + expect(fakeVmServiceHost.hasRemainingExpectations, false); + }); + + testWithoutContext('Coverage collector with null libraryNames accepts all libraries', () async { + final FakeVmServiceHost fakeVmServiceHost = FakeVmServiceHost( + requests: [ + FakeVmServiceRequest( + method: 'getVersion', + jsonResponse: Version(major: 3, minor: 51).toJson(), + ), + FakeVmServiceRequest( + method: 'getVM', + jsonResponse: (VM.parse({}) + ..isolates = [ + IsolateRef.parse({ + 'id': '1', + }), + ] + ).toJson(), + ), + FakeVmServiceRequest( + method: 'getScripts', + args: { + 'isolateId': '1', + }, + jsonResponse: ScriptList(scripts: [ + ScriptRef(uri: 'package:foo/foo.dart', id: '1'), + ScriptRef(uri: 'package:bar/bar.dart', id: '2'), + ]).toJson(), + ), + FakeVmServiceRequest( + method: 'getSourceReport', + args: { + 'isolateId': '1', + 'reports': ['Coverage'], + 'scriptId': '1', + 'forceCompile': true, + 'reportLines': true, + }, + jsonResponse: SourceReport( + ranges: [ + SourceReportRange( + scriptIndex: 0, + startPos: 0, + endPos: 0, + compiled: true, + coverage: SourceReportCoverage( + hits: [1, 3], + misses: [2], + ), + ), + ], + scripts: [ + ScriptRef( + uri: 'package:foo/foo.dart', + id: '1', + ), + ], + ).toJson(), + ), + FakeVmServiceRequest( + method: 'getSourceReport', + args: { + 'isolateId': '1', + 'reports': ['Coverage'], + 'scriptId': '2', + 'forceCompile': true, + 'reportLines': true, + }, + jsonResponse: SourceReport( + ranges: [ + SourceReportRange( + scriptIndex: 0, + startPos: 0, + endPos: 0, + compiled: true, + coverage: SourceReportCoverage( + hits: [47, 21], + misses: [32, 86], + ), + ), + ], + scripts: [ + ScriptRef( + uri: 'package:bar/bar.dart', + id: '2', + ), + ], + ).toJson(), + ), + ], + ); + + final Map result = await collect( + null, + null, + connector: (Uri uri) async { + return fakeVmServiceHost.vmService; + }, + ); + + expect(result, { + 'type': 'CodeCoverage', + 'coverage': [ + { + 'source': 'package:foo/foo.dart', + 'script': { + 'type': '@Script', + 'fixedId': true, + 'id': 'libraries/1/scripts/package%3Afoo%2Ffoo.dart', + 'uri': 'package:foo/foo.dart', + '_kind': 'library', + }, + 'hits': [1, 1, 3, 1, 2, 0], + }, + { + 'source': 'package:bar/bar.dart', + 'script': { + 'type': '@Script', + 'fixedId': true, + 'id': 'libraries/1/scripts/package%3Abar%2Fbar.dart', + 'uri': 'package:bar/bar.dart', + '_kind': 'library', + }, + 'hits': [47, 1, 21, 1, 32, 0, 86, 0], + }, + ], + }); + expect(fakeVmServiceHost.hasRemainingExpectations, false); + }); + + testWithoutContext('Coverage collector with libraryFilters', () async { + final FakeVmServiceHost fakeVmServiceHost = FakeVmServiceHost( + requests: [ + FakeVmServiceRequest( + method: 'getVersion', + jsonResponse: Version(major: 3, minor: 57).toJson(), + ), + FakeVmServiceRequest( + method: 'getVM', + jsonResponse: (VM.parse({}) + ..isolates = [ + IsolateRef.parse({ + 'id': '1', + }), + ] + ).toJson(), + ), + FakeVmServiceRequest( + method: 'getSourceReport', + args: { + 'isolateId': '1', + 'reports': ['Coverage'], + 'forceCompile': true, + 'reportLines': true, + 'libraryFilters': ['package:foo/'], + }, + jsonResponse: SourceReport( + ranges: [ + SourceReportRange( + scriptIndex: 0, + startPos: 0, + endPos: 0, + compiled: true, + coverage: SourceReportCoverage( + hits: [1, 3], + misses: [2], + ), + ), + ], + scripts: [ + ScriptRef( + uri: 'package:foo/foo.dart', + id: '1', + ), + ], + ).toJson(), + ), + ], + ); + + final Map result = await collect( + null, + {'foo'}, + connector: (Uri uri) async { + return fakeVmServiceHost.vmService; + }, + ); + + expect(result, { + 'type': 'CodeCoverage', + 'coverage': [ + { + 'source': 'package:foo/foo.dart', + 'script': { + 'type': '@Script', + 'fixedId': true, + 'id': 'libraries/1/scripts/package%3Afoo%2Ffoo.dart', + 'uri': 'package:foo/foo.dart', + '_kind': 'library', + }, + 'hits': [1, 1, 3, 1, 2, 0], + }, + ], + }); + expect(fakeVmServiceHost.hasRemainingExpectations, false); + }); + + testWithoutContext('Coverage collector with libraryFilters and null libraryNames', () async { + final FakeVmServiceHost fakeVmServiceHost = FakeVmServiceHost( + requests: [ + FakeVmServiceRequest( + method: 'getVersion', + jsonResponse: Version(major: 3, minor: 57).toJson(), + ), + FakeVmServiceRequest( + method: 'getVM', + jsonResponse: (VM.parse({}) + ..isolates = [ + IsolateRef.parse({ + 'id': '1', + }), + ] + ).toJson(), + ), + FakeVmServiceRequest( + method: 'getSourceReport', + args: { + 'isolateId': '1', + 'reports': ['Coverage'], + 'forceCompile': true, + 'reportLines': true, + }, + jsonResponse: SourceReport( + ranges: [ + SourceReportRange( + scriptIndex: 0, + startPos: 0, + endPos: 0, + compiled: true, + coverage: SourceReportCoverage( + hits: [1, 3], + misses: [2], + ), + ), + ], + scripts: [ + ScriptRef( + uri: 'package:foo/foo.dart', + id: '1', + ), + ], + ).toJson(), + ), + ], + ); + + final Map result = await collect( + null, + null, + connector: (Uri uri) async { + return fakeVmServiceHost.vmService; + }, + ); + + expect(result, { + 'type': 'CodeCoverage', + 'coverage': [ + { + 'source': 'package:foo/foo.dart', + 'script': { + 'type': '@Script', + 'fixedId': true, + 'id': 'libraries/1/scripts/package%3Afoo%2Ffoo.dart', + 'uri': 'package:foo/foo.dart', '_kind': 'library', }, 'hits': [1, 1, 3, 1, 2, 0],