diff --git a/lib/src/cli/cli.dart b/lib/src/cli/cli.dart index bf1b750e7..eeae77f9b 100644 --- a/lib/src/cli/cli.dart +++ b/lib/src/cli/cli.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:math'; import 'package:collection/collection.dart'; +import 'package:coverage/coverage.dart' as coverage; import 'package:glob/glob.dart'; import 'package:lcov_parser/lcov_parser.dart'; import 'package:mason/mason.dart'; diff --git a/lib/src/cli/test_cli_runner.dart b/lib/src/cli/test_cli_runner.dart index d6437f1c5..42123716a 100644 --- a/lib/src/cli/test_cli_runner.dart +++ b/lib/src/cli/test_cli_runner.dart @@ -142,6 +142,7 @@ class TestCLIRunner { cwd: cwd, collectCoverage: collectCoverage, testRunner: testRunner, + testType: testType, arguments: [ ...?arguments, if (randomSeed != null) ...[ @@ -158,6 +159,38 @@ class TestCLIRunner { await _cleanupOptimizerFile(cwd); } + // Dart don't directly generate lcov files, so we need + // to read the json that is generates and convert it to lcov. + if (testType == TestRunType.dart && collectCoverage) { + final files = _dartCoverageFilesToProcess( + p.join(cwd, 'coverage'), + ); + + final packagesPath = p.join( + '.dart_tool', + 'package_config.json', + ); + final hitmap = await coverage.HitMap.parseFiles( + files, + packagePath: packagesPath, + ); + + final resolver = await coverage.Resolver.create( + packagesPath: packagesPath, + packagePath: packagesPath, + ); + + final output = hitmap.formatLcov( + resolver, + reportOn: ['lib'], + basePath: cwd, + ); + + // Write the lcov output to the file. + await lcovFile.create(recursive: true); + await lcovFile.writeAsString(output); + } + if (collectCoverage) { assert( lcovFile.existsSync(), @@ -214,12 +247,21 @@ class TestCLIRunner { '''Expected coverage >= ${minCoverage.toStringAsFixed(decimalPlaces)}% but actual is ${e.coverage.toStringAsFixed(decimalPlaces)}%.''', ); } + + static List _dartCoverageFilesToProcess(String absPath) { + return Directory(absPath) + .listSync(recursive: true) + .whereType() + .where((e) => e.path.endsWith('.json')) + .toList(); + } } Future _testCommand({ required void Function(String) stdout, required void Function(String) stderr, required VeryGoodTestRunner testRunner, + required TestRunType testType, String cwd = '.', bool collectCoverage = false, List? arguments, @@ -273,7 +315,13 @@ Future _testCommand({ subscription = testRunner( workingDirectory: cwd, - arguments: [if (collectCoverage) '--coverage', ...?arguments], + arguments: [ + if (collectCoverage) + testType == TestRunType.flutter + ? '--coverage' + : '--coverage=coverage', + ...?arguments, + ], runInShell: true, ).listen( (event) { diff --git a/pubspec.yaml b/pubspec.yaml index 6141371b6..20519d370 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -14,6 +14,7 @@ dependencies: args: ^2.6.0 cli_completion: ^0.5.1 collection: ^1.19.0 + coverage: ^1.15.0 equatable: ^2.0.5 glob: ^2.1.2 lcov_parser: ^0.1.2 diff --git a/test/src/cli/test_runner_cli_test.dart b/test/src/cli/test_runner_cli_test.dart index 8384157c2..cf2c13319 100644 --- a/test/src/cli/test_runner_cli_test.dart +++ b/test/src/cli/test_runner_cli_test.dart @@ -684,7 +684,7 @@ void main() { ); }); - test('runs tests w/coverage', () async { + test('runs flutter tests w/coverage', () async { final tempDirectory = Directory.systemTemp.createTempSync(); addTearDown(() => tempDirectory.deleteSync(recursive: true)); @@ -728,6 +728,50 @@ void main() { expect(testRunnerArgs, equals(['--coverage'])); }); + test('runs flutter tests w/coverage', () async { + final tempDirectory = Directory.systemTemp.createTempSync(); + addTearDown(() => tempDirectory.deleteSync(recursive: true)); + + final lcovFile = File( + p.join(tempDirectory.path, 'coverage', 'lcov.info'), + ); + File(p.join(tempDirectory.path, 'pubspec.yaml')).createSync(); + Directory(p.join(tempDirectory.path, 'test')).createSync(); + lcovFile.createSync(recursive: true); + + await expectLater( + TestCLIRunner.test( + testType: TestRunType.dart, + cwd: tempDirectory.path, + collectCoverage: true, + stdout: stdoutLogs.add, + stderr: stderrLogs.add, + overrideTestRunner: testRunner( + Stream.fromIterable( + [ + const DoneTestEvent(success: true, time: 0), + const ExitTestEvent(exitCode: 0, time: 0), + ], + ), + onStart: () { + expect(lcovFile.existsSync(), isFalse); + lcovFile.createSync(recursive: true); + }, + ), + logger: logger, + ), + completion(equals([ExitCode.success.code])), + ); + expect( + stdoutLogs, + equals([ + 'Running "dart test" in . ...\n', + contains('All tests passed!'), + ]), + ); + expect(testRunnerArgs, equals(['--coverage=coverage'])); + }); + test('runs tests w/coverage + min-coverage 100 (pass)', () async { final tempDirectory = Directory.systemTemp.createTempSync(); addTearDown(() => tempDirectory.deleteSync(recursive: true));