diff --git a/packages/flutter_tools/lib/src/commands/test.dart b/packages/flutter_tools/lib/src/commands/test.dart index 6e01be15d891e..b2c5a335d166f 100644 --- a/packages/flutter_tools/lib/src/commands/test.dart +++ b/packages/flutter_tools/lib/src/commands/test.dart @@ -121,6 +121,11 @@ class TestCommand extends FlutterCommand with DeviceBasedDevelopmentArtifacts { help: 'Whether to merge coverage data with "coverage/lcov.base.info".\n' 'Implies collecting coverage data. (Requires lcov.)', ) + ..addFlag('branch-coverage', + negatable: false, + help: 'Whether to collect branch coverage information. ' + 'Implies collecting coverage data.', + ) ..addFlag('ipv6', negatable: false, hide: !verboseHelp, @@ -378,7 +383,8 @@ class TestCommand extends FlutterCommand with DeviceBasedDevelopmentArtifacts { final bool machine = boolArgDeprecated('machine'); CoverageCollector? collector; - if (boolArgDeprecated('coverage') || boolArgDeprecated('merge-coverage')) { + if (boolArgDeprecated('coverage') || boolArgDeprecated('merge-coverage') || + boolArgDeprecated('branch-coverage')) { final String projectName = flutterProject.manifest.appName; collector = CoverageCollector( verbose: !machine, @@ -386,6 +392,7 @@ class TestCommand extends FlutterCommand with DeviceBasedDevelopmentArtifacts { packagesPath: buildInfo.packagesPath, resolver: await CoverageCollector.getResolver(buildInfo.packagesPath), testTimeRecorder: testTimeRecorder, + branchCoverage: boolArgDeprecated('branch-coverage'), ); } diff --git a/packages/flutter_tools/lib/src/test/coverage_collector.dart b/packages/flutter_tools/lib/src/test/coverage_collector.dart index 0380557fb0cf9..1a96cdb213473 100644 --- a/packages/flutter_tools/lib/src/test/coverage_collector.dart +++ b/packages/flutter_tools/lib/src/test/coverage_collector.dart @@ -17,7 +17,9 @@ import 'watcher.dart'; /// A class that collects code coverage data during test runs. class CoverageCollector extends TestWatcher { - CoverageCollector({this.libraryNames, this.verbose = true, required this.packagesPath, this.resolver, this.testTimeRecorder}); + CoverageCollector({ + this.libraryNames, this.verbose = true, required this.packagesPath, + this.resolver, this.testTimeRecorder, this.branchCoverage = false}); /// True when log messages should be emitted. final bool verbose; @@ -38,6 +40,9 @@ class CoverageCollector extends TestWatcher { final TestTimeRecorder? testTimeRecorder; + /// Whether to collect branch coverage information. + bool branchCoverage; + static Future getResolver(String? packagesPath) async { try { return await coverage.Resolver.create(packagesPath: packagesPath); @@ -97,7 +102,8 @@ class CoverageCollector extends TestWatcher { /// The returned [Future] completes when the coverage is collected. Future collectCoverageIsolate(Uri observatoryUri) async { _logMessage('collecting coverage data from $observatoryUri...'); - final Map data = await collect(observatoryUri, libraryNames); + final Map data = await collect( + observatoryUri, libraryNames, branchCoverage: branchCoverage); if (data == null) { throw Exception('Failed to collect coverage.'); } @@ -136,7 +142,9 @@ class CoverageCollector extends TestWatcher { final Future collectionComplete = testDevice.observatoryUri .then((Uri? observatoryUri) { _logMessage('collecting coverage data from $testDevice at $observatoryUri...'); - return collect(observatoryUri!, libraryNames, serviceOverride: serviceOverride) + return collect( + observatoryUri!, libraryNames, serviceOverride: serviceOverride, + branchCoverage: branchCoverage) .then((Map result) { if (result == null) { throw Exception('Failed to collect coverage.'); @@ -254,6 +262,10 @@ Future> collect(Uri serviceUri, Set? libraryNames, String? debugName, @visibleForTesting bool forceSequential = false, @visibleForTesting FlutterVmService? serviceOverride, + bool branchCoverage = false, }) { - return coverage.collect(serviceUri, false, false, false, libraryNames, serviceOverrideForTesting: serviceOverride?.service); + return coverage.collect( + serviceUri, false, false, false, libraryNames, + serviceOverrideForTesting: serviceOverride?.service, + branchCoverage: branchCoverage); } 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 9f330d7ff8756..23bd2523871b5 100644 --- a/packages/flutter_tools/test/general.shard/coverage_collector_test.dart +++ b/packages/flutter_tools/test/general.shard/coverage_collector_test.dart @@ -2,8 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -@Timeout.factor(10) - import 'dart:convert' show jsonEncode; import 'dart:io' show Directory, File; @@ -331,6 +329,97 @@ void main() { expect(fakeVmServiceHost.hasRemainingExpectations, false); }); + testWithoutContext('Coverage collector with branch coverage', () async { + final FakeVmServiceHost fakeVmServiceHost = FakeVmServiceHost( + requests: [ + FakeVmServiceRequest( + method: 'getVM', + jsonResponse: (VM.parse({})! + ..isolates = [ + IsolateRef.parse({ + 'id': '1', + })!, + ] + ).toJson(), + ), + FakeVmServiceRequest( + method: 'getVersion', + jsonResponse: Version(major: 3, minor: 56).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', 'BranchCoverage'], + '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], + ), + branchCoverage: SourceReportCoverage( + hits: [4, 6], + misses: [5], + ), + ), + ], + scripts: [ + ScriptRef( + uri: 'package:foo/foo.dart', + id: '1', + ), + ], + ).toJson(), + ), + ], + ); + + final Map result = await collect( + Uri(), + {'foo'}, + serviceOverride: fakeVmServiceHost.vmService, + branchCoverage: true, + ); + + 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], + 'branchHits': [4, 1, 6, 1, 5, 0], + }, + ], + }); + expect(fakeVmServiceHost.hasRemainingExpectations, false); + }); + testWithoutContext('Coverage collector caches read files', () async { Directory? tempDir; try {