LCOV - code coverage report
Current view: top level - src/commands/test - test.dart (source / functions) Hit Total Coverage
Test: lcov.info Lines: 66 66 100.0 %
Date: 2024-03-25 10:36:11 Functions: 0 0 -

          Line data    Source code
       1             : import 'dart:math';
       2             : 
       3             : import 'package:args/args.dart';
       4             : import 'package:args/command_runner.dart';
       5             : import 'package:mason/mason.dart';
       6             : import 'package:meta/meta.dart';
       7             : import 'package:path/path.dart' as path;
       8             : import 'package:universal_io/io.dart';
       9             : import 'package:very_good_cli/src/cli/cli.dart';
      10             : 
      11             : /// Signature for the [Flutter.installed] method.
      12             : typedef FlutterInstalledCommand = Future<bool> Function({
      13             :   required Logger logger,
      14             : });
      15             : 
      16             : /// Signature for the [Flutter.test] method.
      17             : typedef FlutterTestCommand = Future<List<int>> Function({
      18             :   required Logger logger,
      19             :   String cwd,
      20             :   bool recursive,
      21             :   bool collectCoverage,
      22             :   bool optimizePerformance,
      23             :   double? minCoverage,
      24             :   String? excludeFromCoverage,
      25             :   String? randomSeed,
      26             :   bool? forceAnsi,
      27             :   List<String>? arguments,
      28             :   void Function(String)? stdout,
      29             :   void Function(String)? stderr,
      30             : });
      31             : 
      32             : /// {@template test_command}
      33             : /// `very_good test` command for running tests.
      34             : /// {@endtemplate}
      35             : class TestCommand extends Command<int> {
      36             :   /// {@macro test_command}
      37           1 :   TestCommand({
      38             :     required Logger logger,
      39             :     @visibleForTesting FlutterInstalledCommand? flutterInstalled,
      40             :     @visibleForTesting FlutterTestCommand? flutterTest,
      41             :   })  : _logger = logger,
      42             :         _flutterInstalled = flutterInstalled ?? Flutter.installed,
      43             :         _flutterTest = flutterTest ?? Flutter.test {
      44           1 :     argParser
      45           1 :       ..addFlag(
      46             :         'coverage',
      47             :         help: 'Whether to collect coverage information.',
      48             :         negatable: false,
      49             :       )
      50           1 :       ..addFlag(
      51             :         'recursive',
      52             :         abbr: 'r',
      53             :         help: 'Run tests recursively for all nested packages.',
      54             :         negatable: false,
      55             :       )
      56           1 :       ..addFlag(
      57             :         'optimization',
      58             :         defaultsTo: true,
      59             :         help: 'Whether to apply optimizations for test performance.',
      60             :       )
      61           1 :       ..addOption(
      62             :         'concurrency',
      63             :         abbr: 'j',
      64             :         defaultsTo: '4',
      65             :         help: 'The number of concurrent test suites run.',
      66             :       )
      67           1 :       ..addOption(
      68             :         'tags',
      69             :         abbr: 't',
      70             :         help: 'Run only tests associated with the specified tags.',
      71             :       )
      72           1 :       ..addOption(
      73             :         'exclude-coverage',
      74             :         help: 'A glob which will be used to exclude files that match from the '
      75             :             'coverage.',
      76             :       )
      77           1 :       ..addOption(
      78             :         'exclude-tags',
      79             :         abbr: 'x',
      80             :         help: 'Run only tests that do not have the specified tags.',
      81             :       )
      82           1 :       ..addOption(
      83             :         'min-coverage',
      84             :         help: 'Whether to enforce a minimum coverage percentage.',
      85             :       )
      86           1 :       ..addOption(
      87             :         'test-randomize-ordering-seed',
      88             :         help: 'The seed to randomize the execution order of test cases '
      89             :             'within test files.',
      90             :       )
      91           1 :       ..addFlag(
      92             :         'update-goldens',
      93             :         help: 'Whether "matchesGoldenFile()" calls within your test methods '
      94             :             'should update the golden files.',
      95             :         negatable: false,
      96             :       )
      97           1 :       ..addFlag(
      98             :         'force-ansi',
      99             :         defaultsTo: null,
     100             :         help: 'Whether to force ansi output. If not specified, '
     101             :             'it will maintain the default behavior based on stdout and stderr.',
     102             :         negatable: false,
     103             :       )
     104           1 :       ..addMultiOption(
     105             :         'dart-define',
     106             :         help: 'Additional key-value pairs that will be available as constants '
     107             :             'from the String.fromEnvironment, bool.fromEnvironment, '
     108             :             'int.fromEnvironment, and double.fromEnvironment constructors. '
     109             :             'Multiple defines can be passed by repeating '
     110             :             '"--dart-define" multiple times.',
     111             :         valueHelp: 'foo=bar',
     112             :       );
     113             :   }
     114             : 
     115             :   final Logger _logger;
     116             :   final FlutterInstalledCommand _flutterInstalled;
     117             :   final FlutterTestCommand _flutterTest;
     118             : 
     119           1 :   @override
     120             :   String get description => 'Run tests in a Dart or Flutter project.';
     121             : 
     122           1 :   @override
     123             :   String get name => 'test';
     124             : 
     125             :   /// [ArgResults] which can be overridden for testing.
     126             :   @visibleForTesting
     127             :   ArgResults? argResultOverrides;
     128             : 
     129           3 :   ArgResults get _argResults => argResultOverrides ?? argResults!;
     130             : 
     131           1 :   @override
     132             :   Future<int> run() async {
     133           4 :     final targetPath = path.normalize(Directory.current.absolute.path);
     134           2 :     final pubspec = File(path.join(targetPath, 'pubspec.yaml'));
     135           2 :     final recursive = _argResults['recursive'] as bool;
     136             : 
     137           1 :     if (!recursive && !pubspec.existsSync()) {
     138           2 :       _logger.err(
     139             :         '''
     140             : Could not find a pubspec.yaml in $targetPath.
     141           1 : This command should be run from the root of your Flutter project.''',
     142             :       );
     143           1 :       return ExitCode.noInput.code;
     144             :     }
     145             : 
     146           2 :     final concurrency = _argResults['concurrency'] as String;
     147           2 :     final collectCoverage = _argResults['coverage'] as bool;
     148           1 :     final minCoverage = double.tryParse(
     149           2 :       _argResults['min-coverage'] as String? ?? '',
     150             :     );
     151           2 :     final excludeTags = _argResults['exclude-tags'] as String?;
     152           2 :     final tags = _argResults['tags'] as String?;
     153           3 :     final isFlutterInstalled = await _flutterInstalled(logger: _logger);
     154           2 :     final excludeFromCoverage = _argResults['exclude-coverage'] as String?;
     155             :     final randomOrderingSeed =
     156           2 :         _argResults['test-randomize-ordering-seed'] as String?;
     157           1 :     final randomSeed = randomOrderingSeed == 'random'
     158           3 :         ? Random().nextInt(4294967295).toString()
     159             :         : randomOrderingSeed;
     160           2 :     final optimizePerformance = _argResults['optimization'] as bool;
     161           2 :     final updateGoldens = _argResults['update-goldens'] as bool;
     162           2 :     final forceAnsi = _argResults['force-ansi'] as bool?;
     163           2 :     final dartDefine = _argResults['dart-define'] as List<String>?;
     164           2 :     final rest = _argResults.rest;
     165             : 
     166             :     if (isFlutterInstalled) {
     167             :       try {
     168           2 :         final results = await _flutterTest(
     169             :           optimizePerformance: optimizePerformance &&
     170           1 :               !_isTargettingTestFiles(rest) &&
     171             :               !updateGoldens,
     172             :           recursive: recursive,
     173           1 :           logger: _logger,
     174           2 :           stdout: _logger.write,
     175           2 :           stderr: _logger.err,
     176             :           collectCoverage: collectCoverage || minCoverage != null,
     177             :           minCoverage: minCoverage,
     178             :           excludeFromCoverage: excludeFromCoverage,
     179             :           randomSeed: randomSeed,
     180             :           forceAnsi: forceAnsi,
     181           1 :           arguments: [
     182           1 :             if (excludeTags != null) ...['-x', excludeTags],
     183           1 :             if (tags != null) ...['-t', tags],
     184           1 :             if (updateGoldens) '--update-goldens',
     185             :             if (dartDefine != null)
     186           2 :               for (final value in dartDefine) '--dart-define=$value',
     187           1 :             ...['-j', concurrency],
     188           1 :             '--no-pub',
     189           1 :             ...rest,
     190             :           ],
     191             :         );
     192           4 :         if (results.any((code) => code != ExitCode.success.code)) {
     193           1 :           return ExitCode.unavailable.code;
     194             :         }
     195           1 :       } on MinCoverageNotMet catch (e) {
     196           2 :         _logger.err(
     197           4 :           '''Expected coverage >= ${minCoverage!.toStringAsFixed(2)}% but actual is ${e.coverage.toStringAsFixed(2)}%.''',
     198             :         );
     199           1 :         return ExitCode.unavailable.code;
     200             :       } catch (error) {
     201           3 :         _logger.err('$error');
     202           1 :         return ExitCode.unavailable.code;
     203             :       }
     204             :     }
     205           1 :     return ExitCode.success.code;
     206             :   }
     207             : }
     208             : 
     209             : /// Determines whether the user is targetting test files or not.
     210             : ///
     211             : /// The user can only target test files by using the `--` option terminator.
     212             : /// The additional options after the `--` are passed to the test runner which
     213             : /// allows the user to target specific test files or directories.
     214             : ///
     215             : /// The heuristics used to determine if the user is not targetting test files
     216             : /// are:
     217             : /// * No [rest] arguments are passed.
     218             : /// * All [rest] arguments are options (i.e. they do not start with `-`).
     219             : ///
     220             : /// See also:
     221             : /// * [What does -- mean in Shell?](https://www.cyberciti.biz/faq/what-does-double-dash-mean-in-ssh-command/)
     222           1 : bool _isTargettingTestFiles(List<String> rest) {
     223           1 :   if (rest.isEmpty) {
     224             :     return false;
     225             :   }
     226             : 
     227           4 :   return rest.where((arg) => !arg.startsWith('-')).isNotEmpty;
     228             : }

Generated by: LCOV version 1.15