| // Copyright 2013 The Flutter Authors | |
| // Use of this source code is governed by a BSD-style license that can be | |
| // found in the LICENSE file. | |
| import 'package:args/command_runner.dart'; | |
| import 'package:file/file.dart'; | |
| import 'package:flutter_plugin_tools/src/common/core.dart'; | |
| import 'package:flutter_plugin_tools/src/coverage_check_command.dart'; | |
| import 'package:git/git.dart'; | |
| import 'package:platform/platform.dart'; | |
| import 'package:test/test.dart'; | |
| import 'mocks.dart'; | |
| import 'util.dart'; | |
| void main() { | |
| group('CoverageCheckCommand', () { | |
| late NativePlatform mockPlatform; | |
| late Directory packagesDir; | |
| late CommandRunner<void> runner; | |
| late RecordingProcessRunner processRunner; | |
| late CoverageCheckCommand command; | |
| setUp(() { | |
| mockPlatform = createMockPlatform(); | |
| late GitDir gitDir; | |
| (:packagesDir, :processRunner, gitProcessRunner: _, :gitDir) = configureBaseCommandMocks( | |
| platform: mockPlatform, | |
| ); | |
| command = CoverageCheckCommand( | |
| packagesDir, | |
| processRunner: processRunner, | |
| platform: mockPlatform, | |
| gitDir: gitDir, | |
| ); | |
| runner = CommandRunner<void>('coverage_test', 'Test for CoverageCheckCommand'); | |
| runner.addCommand(command); | |
| final File minimumsFile = packagesDir.parent | |
| .childDirectory('script') | |
| .childDirectory('configs') | |
| .childFile('custom_coverage_minimums.yaml'); | |
| minimumsFile.createSync(recursive: true); | |
| minimumsFile.writeAsStringSync(''' | |
| custom_coverage_minimums: | |
| plugin1: 50.0 | |
| plugin2: 100.0 | |
| '''); | |
| }); | |
| test('fails if custom_coverage_minimums.yaml is missing', () async { | |
| final File minimumsFile = packagesDir.parent | |
| .childDirectory('script') | |
| .childDirectory('configs') | |
| .childFile('custom_coverage_minimums.yaml'); | |
| minimumsFile.deleteSync(); | |
| Error? commandError; | |
| final List<String> output = await runCapturingPrint( | |
| runner, | |
| <String>['coverage-check'], | |
| errorHandler: (Error e) { | |
| commandError = e; | |
| }, | |
| ); | |
| expect(commandError, isA<ToolExit>()); | |
| expect( | |
| output, | |
| containsAllInOrder(<Matcher>[ | |
| contains('The custom_coverage_minimums.yaml file is missing.'), | |
| ]), | |
| ); | |
| }); | |
| test('skips packages not in custom minimums', () async { | |
| createFakePlugin( | |
| 'unlisted_plugin', | |
| packagesDir, | |
| extraFiles: <String>['test/empty_test.dart'], | |
| ); | |
| final List<String> output = await runCapturingPrint(runner, <String>['coverage-check']); | |
| expect( | |
| output, | |
| containsAllInOrder(<Matcher>[ | |
| contains('SKIPPING: Package not opted into coverage checks.'), | |
| ]), | |
| ); | |
| expect(processRunner.recordedCalls, isEmpty); | |
| }); | |
| test('skips third-party packages', () async { | |
| createFakePlugin( | |
| 'plugin1', | |
| packagesDir.parent.childDirectory('third_party').childDirectory('packages'), | |
| extraFiles: <String>['test/empty_test.dart'], | |
| ); | |
| final List<String> output = await runCapturingPrint(runner, <String>['coverage-check']); | |
| expect( | |
| output, | |
| containsAllInOrder(<Matcher>[contains('SKIPPING: Not a first-party package.')]), | |
| ); | |
| expect(processRunner.recordedCalls, isEmpty); | |
| }); | |
| test('passes when coverage meets minimum', () async { | |
| final RepositoryPackage plugin = createFakePlugin( | |
| 'plugin1', | |
| packagesDir, | |
| extraFiles: <String>['test/empty_test.dart'], | |
| ); | |
| final Directory coverageDir = plugin.directory.childDirectory('coverage'); | |
| coverageDir.createSync(); | |
| coverageDir.childFile('lcov.info').writeAsStringSync(''' | |
| SF:lib/plugin1.dart | |
| DA:1,1 | |
| DA:2,1 | |
| LF:2 | |
| LH:2 | |
| end_of_record | |
| '''); | |
| final List<String> output = await runCapturingPrint(runner, <String>['coverage-check']); | |
| expect( | |
| processRunner.recordedCalls, | |
| orderedEquals(<ProcessCall>[ | |
| ProcessCall('flutter', const <String>['test', '--coverage'], plugin.path), | |
| ]), | |
| ); | |
| expect(output, contains(contains('Ran for 1 package(s)'))); | |
| expect(coverageDir.existsSync(), isFalse); | |
| }); | |
| test('fails when coverage is below minimum', () async { | |
| final RepositoryPackage plugin = createFakePlugin( | |
| 'plugin1', | |
| packagesDir, | |
| extraFiles: <String>['test/empty_test.dart'], | |
| ); | |
| final Directory coverageDir = plugin.directory.childDirectory('coverage'); | |
| coverageDir.createSync(); | |
| coverageDir.childFile('lcov.info').writeAsStringSync(''' | |
| SF:lib/plugin1.dart | |
| DA:1,1 | |
| DA:2,0 | |
| DA:3,0 | |
| DA:4,0 | |
| LF:4 | |
| LH:1 | |
| end_of_record | |
| '''); | |
| Error? commandError; | |
| final List<String> output = await runCapturingPrint( | |
| runner, | |
| <String>['coverage-check'], | |
| errorHandler: (Error e) { | |
| commandError = e; | |
| }, | |
| ); | |
| expect(commandError, isA<ToolExit>()); | |
| expect( | |
| output, | |
| containsAllInOrder(<Matcher>[ | |
| contains('Code coverage for plugin1 is 25.0%, which is below the required 50.0%.'), | |
| ]), | |
| ); | |
| expect(coverageDir.existsSync(), isFalse); | |
| }); | |
| test('ignores generated files when calculating coverage', () async { | |
| final RepositoryPackage plugin = createFakePlugin( | |
| 'plugin1', | |
| packagesDir, | |
| extraFiles: <String>['test/empty_test.dart'], | |
| ); | |
| final Directory coverageDir = plugin.directory.childDirectory('coverage'); | |
| coverageDir.createSync(); | |
| coverageDir.childFile('lcov.info').writeAsStringSync(''' | |
| SF:lib/plugin1.dart | |
| DA:1,1 | |
| DA:2,1 | |
| LF:2 | |
| LH:2 | |
| end_of_record | |
| SF:lib/plugin1.g.dart | |
| DA:1,0 | |
| DA:2,0 | |
| DA:3,0 | |
| DA:4,0 | |
| LF:4 | |
| LH:0 | |
| end_of_record | |
| '''); | |
| final List<String> output = await runCapturingPrint(runner, <String>['coverage-check']); | |
| expect(output, contains(contains('Ran for 1 package(s)'))); | |
| }); | |
| test('fails with clear message when no instrumented lines are found', () async { | |
| final RepositoryPackage plugin = createFakePlugin( | |
| 'plugin1', | |
| packagesDir, | |
| extraFiles: <String>['test/empty_test.dart'], | |
| ); | |
| final Directory coverageDir = plugin.directory.childDirectory('coverage'); | |
| coverageDir.createSync(); | |
| coverageDir.childFile('lcov.info').writeAsStringSync(''' | |
| SF:lib/plugin1.g.dart | |
| DA:1,0 | |
| LF:1 | |
| LH:0 | |
| end_of_record | |
| '''); | |
| Error? commandError; | |
| final List<String> output = await runCapturingPrint( | |
| runner, | |
| <String>['coverage-check'], | |
| errorHandler: (Error e) { | |
| commandError = e; | |
| }, | |
| ); | |
| expect(commandError, isA<ToolExit>()); | |
| expect( | |
| output, | |
| contains( | |
| contains('No instrumented Dart lines were found in the coverage report for plugin1.'), | |
| ), | |
| ); | |
| expect( | |
| output, | |
| contains(contains('If this package contains no code to cover, please remove it from')), | |
| ); | |
| }); | |
| test('calculates coverage correctly across multiple files', () async { | |
| final RepositoryPackage plugin = createFakePlugin( | |
| 'plugin1', | |
| packagesDir, | |
| extraFiles: <String>['test/empty_test.dart'], | |
| ); | |
| final Directory coverageDir = plugin.directory.childDirectory('coverage'); | |
| coverageDir.createSync(); | |
| coverageDir.childFile('lcov.info').writeAsStringSync(''' | |
| SF:lib/file1.dart | |
| DA:1,1 | |
| DA:2,0 | |
| LF:2 | |
| LH:1 | |
| end_of_record | |
| SF:lib/file2.dart | |
| DA:1,1 | |
| DA:2,1 | |
| DA:3,1 | |
| LF:3 | |
| LH:3 | |
| end_of_record | |
| '''); // Total LF: 5, LH: 4 => 80.0% coverage. 80.0 >= 50.0 so it should pass. | |
| final List<String> output = await runCapturingPrint(runner, <String>['coverage-check']); | |
| expect(output, contains(contains('Ran for 1 package(s)'))); | |
| }); | |
| test('fails when test command fails', () async { | |
| createFakePlugin('plugin1', packagesDir, extraFiles: <String>['test/empty_test.dart']); | |
| processRunner.mockProcessesForExecutable['flutter'] = <FakeProcessInfo>[ | |
| FakeProcessInfo(MockProcess(exitCode: 1), <String>['test', '--coverage']), | |
| ]; | |
| Error? commandError; | |
| final List<String> output = await runCapturingPrint( | |
| runner, | |
| <String>['coverage-check'], | |
| errorHandler: (Error e) { | |
| commandError = e; | |
| }, | |
| ); | |
| expect(commandError, isA<ToolExit>()); | |
| expect( | |
| output, | |
| containsAllInOrder(<Matcher>[contains('Failed to run tests or parse coverage')]), | |
| ); | |
| }); | |
| test('fails when lcov.info is missing', () async { | |
| createFakePlugin('plugin1', packagesDir, extraFiles: <String>['test/empty_test.dart']); | |
| Error? commandError; | |
| final List<String> output = await runCapturingPrint( | |
| runner, | |
| <String>['coverage-check'], | |
| errorHandler: (Error e) { | |
| commandError = e; | |
| }, | |
| ); | |
| expect(commandError, isA<ToolExit>()); | |
| expect( | |
| output, | |
| containsAllInOrder(<Matcher>[ | |
| contains( | |
| 'Coverage file not found at ${packagesDir.childDirectory('plugin1').childDirectory('coverage').childFile('lcov.info').path}.', | |
| ), | |
| contains('Failed to run tests or parse coverage'), | |
| ]), | |
| ); | |
| }); | |
| group('calculateCoverage', () { | |
| late File tempLcovFile; | |
| setUp(() { | |
| tempLcovFile = packagesDir.parent.childFile('temp_lcov.info'); | |
| }); | |
| tearDown(() { | |
| if (tempLcovFile.existsSync()) { | |
| tempLcovFile.deleteSync(); | |
| } | |
| }); | |
| test('calculates 100% coverage correctly', () { | |
| tempLcovFile.writeAsStringSync(''' | |
| SF:lib/foo.dart | |
| DA:1,1 | |
| DA:2,1 | |
| LF:2 | |
| LH:2 | |
| end_of_record | |
| '''); | |
| expect(command.calculateCoverage(tempLcovFile), 100.0); | |
| }); | |
| test('calculates partial coverage correctly', () { | |
| tempLcovFile.writeAsStringSync(''' | |
| SF:lib/foo.dart | |
| DA:1,1 | |
| DA:2,0 | |
| LF:2 | |
| LH:1 | |
| end_of_record | |
| '''); | |
| expect(command.calculateCoverage(tempLcovFile), 50.0); | |
| }); | |
| test('calculates partial coverage across multiple files correctly', () { | |
| tempLcovFile.writeAsStringSync(''' | |
| SF:lib/foo.dart | |
| DA:1,1 | |
| DA:2,0 | |
| LF:2 | |
| LH:1 | |
| end_of_record | |
| SF:lib/bar.dart | |
| DA:1,1 | |
| DA:2,1 | |
| DA:3,0 | |
| LF:3 | |
| LH:2 | |
| end_of_record | |
| '''); | |
| expect(command.calculateCoverage(tempLcovFile), 60.0); | |
| }); | |
| test('throws NoLinesFoundException when no lines are found', () { | |
| tempLcovFile.writeAsStringSync(''' | |
| SF:lib/foo.g.dart | |
| DA:1,0 | |
| LF:1 | |
| LH:0 | |
| end_of_record | |
| '''); | |
| expect( | |
| () => command.calculateCoverage(tempLcovFile), | |
| throwsA(isA<NoLinesFoundException>()), | |
| ); | |
| }); | |
| test('ignores multiple types of generated files', () { | |
| tempLcovFile.writeAsStringSync(''' | |
| SF:lib/foo.dart | |
| DA:1,1 | |
| LF:1 | |
| LH:1 | |
| end_of_record | |
| SF:lib/foo.g.dart | |
| DA:1,0 | |
| LF:1 | |
| LH:0 | |
| end_of_record | |
| SF:lib/foo.mocks.dart | |
| DA:1,0 | |
| LF:1 | |
| LH:0 | |
| end_of_record | |
| SF:lib/foo.pb.dart | |
| DA:1,0 | |
| LF:1 | |
| LH:0 | |
| end_of_record | |
| '''); | |
| expect(command.calculateCoverage(tempLcovFile), 100.0); | |
| }); | |
| }); | |
| }); | |
| } |