diff --git a/lib/src/commands/dart/commands/dart_test_command.dart b/lib/src/commands/dart/commands/dart_test_command.dart index 124dd2487..4cea82b6c 100644 --- a/lib/src/commands/dart/commands/dart_test_command.dart +++ b/lib/src/commands/dart/commands/dart_test_command.dart @@ -7,6 +7,7 @@ import 'package:mason/mason.dart'; import 'package:meta/meta.dart'; import 'package:path/path.dart' as path; import 'package:very_good_cli/src/cli/cli.dart'; +import 'package:very_good_cli/src/very_good_config/very_good_config.dart'; /// Options for configuring the Dart test command. class DartTestOptions { @@ -32,18 +33,43 @@ class DartTestOptions { }); /// Parses [ArgResults] into a [DartTestOptions] instance. - factory DartTestOptions.parse(ArgResults argResults) { - final concurrency = argResults['concurrency'] as String; - final collectCoverage = argResults['coverage'] as bool; - final minCoverage = double.tryParse( - argResults['min-coverage'] as String? ?? '', + /// + /// When [config] is provided, its values are used as defaults for any + /// option that was not explicitly parsed on the command line. + factory DartTestOptions.parse( + ArgResults argResults, { + VeryGoodConfig config = VeryGoodConfig.empty, + }) { + final testConfig = config.dart.test; + + final concurrency = argResults.resolve( + 'concurrency', + testConfig.concurrency, + ); + final collectCoverage = argResults.resolve('coverage', testConfig.coverage); + final minCoverageString = argResults.resolve( + 'min-coverage', + testConfig.minCoverage, + ); + final minCoverage = double.tryParse(minCoverageString ?? ''); + final showUncovered = argResults.resolve( + 'show-uncovered', + testConfig.showUncovered, + ); + final excludeTags = argResults.resolve( + 'exclude-tags', + testConfig.excludeTags, + ); + final tags = argResults.resolve('tags', testConfig.tags); + final excludeFromCoverage = argResults.resolve( + 'exclude-coverage', + testConfig.excludeCoverage, + ); + final collectCoverageFromString = argResults.resolve( + 'collect-coverage-from', + testConfig.collectCoverageFrom, + fallbackValue: 'imports', ); - final showUncovered = argResults['show-uncovered'] as bool; - final excludeTags = argResults['exclude-tags'] as String?; - final tags = argResults['tags'] as String?; - final excludeFromCoverage = argResults['exclude-coverage'] as String?; - final collectCoverageFromString = - argResults['collect-coverage-from'] as String? ?? 'imports'; final collectCoverageFrom = CoverageCollectionMode.fromString( collectCoverageFromString, ); @@ -52,17 +78,33 @@ class DartTestOptions { final randomSeed = randomOrderingSeed == 'random' ? Random().nextInt(4294967295).toString() : randomOrderingSeed; - final optimizePerformance = argResults['optimization'] as bool; - final failFast = argResults['fail-fast'] as bool; + final optimizePerformance = argResults.resolve( + 'optimization', + testConfig.optimization, + ); + final failFast = argResults.resolve('fail-fast', testConfig.failFast); final forceAnsi = argResults['force-ansi'] as bool?; - final platform = argResults['platform'] as String?; - final reportOn = (argResults['report-on'] as List) + final platform = argResults.resolve( + 'platform', + testConfig.platform, + ); + final reportOn = argResults + .resolve>('report-on', testConfig.reportOn) .expand((e) => e.split(RegExp(r'[,\s]+'))) .where((e) => e.isNotEmpty) .toList(); - final runSkipped = argResults['run-skipped'] as bool; - final checkIgnore = argResults['check-ignore'] as bool; - final fileReporter = argResults['file-reporter'] as String?; + final runSkipped = argResults.resolve( + 'run-skipped', + testConfig.runSkipped, + ); + final checkIgnore = argResults.resolve( + 'check-ignore', + testConfig.checkIgnore, + ); + final fileReporter = argResults.resolve( + 'file-reporter', + testConfig.fileReporter, + ); final rest = argResults.rest; return DartTestOptions._( @@ -327,9 +369,20 @@ This command should be run from the root of your Dart project.'''); return ExitCode.noInput.code; } + final VeryGoodConfig config; + try { + config = VeryGoodConfig.loadFromClosestAncestor(Directory(targetPath)); + } on VeryGoodConfigParseException catch (e) { + _logger.err( + 'Could not read `$veryGoodConfigFileName`.\n' + '${e.message}', + ); + return ExitCode.config.code; + } + final isDartInstalled = await _dartInstalled(logger: _logger); - final options = DartTestOptions.parse(_argResults); + final options = DartTestOptions.parse(_argResults, config: config); if (isDartInstalled) { try { diff --git a/lib/src/commands/packages/commands/check/commands/licenses.dart b/lib/src/commands/packages/commands/check/commands/licenses.dart index cd6ce999d..0c4cdbf1d 100644 --- a/lib/src/commands/packages/commands/check/commands/licenses.dart +++ b/lib/src/commands/packages/commands/check/commands/licenses.dart @@ -87,33 +87,27 @@ class PackagesCheckLicensesOptions { }) { final licensesConfig = config.packages.check.licenses; - final ignoreRetrievalFailures = resolveArg( - argResults, + final ignoreRetrievalFailures = argResults.resolve( 'ignore-retrieval-failures', licensesConfig.ignoreRetrievalFailures, ); - final dependencyTypes = resolveArg>( - argResults, + final dependencyTypes = argResults.resolve>( 'dependency-type', licensesConfig.dependencyType, ); - final allowedLicenses = resolveArg>( - argResults, + final allowedLicenses = argResults.resolve>( 'allowed', licensesConfig.allowed, ); - final forbiddenLicenses = resolveArg>( - argResults, + final forbiddenLicenses = argResults.resolve>( 'forbidden', licensesConfig.forbidden, ); - final skippedPackages = resolveArg>( - argResults, + final skippedPackages = argResults.resolve>( 'skip-packages', licensesConfig.skipPackages, ); - final reporter = resolveArg( - argResults, + final reporter = argResults.resolve( 'reporter', licensesConfig.reporter, ); diff --git a/lib/src/commands/packages/commands/get.dart b/lib/src/commands/packages/commands/get.dart index 29abf81f5..f85acf4dc 100644 --- a/lib/src/commands/packages/commands/get.dart +++ b/lib/src/commands/packages/commands/get.dart @@ -21,13 +21,11 @@ class PackagesGetOptions { }) { final getConfig = config.packages.get; - final recursive = resolveArg( - argResults, + final recursive = argResults.resolve( 'recursive', getConfig.recursive, ); - final ignore = resolveArg>( - argResults, + final ignore = argResults.resolve>( 'ignore', getConfig.ignore, ); diff --git a/lib/src/commands/test/test.dart b/lib/src/commands/test/test.dart index 2a0827005..cfe0ce8ac 100644 --- a/lib/src/commands/test/test.dart +++ b/lib/src/commands/test/test.dart @@ -46,40 +46,33 @@ class FlutterTestOptions { }) { final testConfig = config.test; - final concurrency = resolveArg( - argResults, + final concurrency = argResults.resolve( 'concurrency', testConfig.concurrency, ); - final collectCoverage = resolveArg( - argResults, + final collectCoverage = argResults.resolve( 'coverage', testConfig.coverage, ); - final minCoverage = resolveArg( - argResults, + final minCoverage = argResults.resolve( 'min-coverage', testConfig.minCoverage, ); final effectiveMinCoverage = double.tryParse(minCoverage ?? ''); - final showUncovered = resolveArg( - argResults, + final showUncovered = argResults.resolve( 'show-uncovered', testConfig.showUncovered, ); - final excludeTags = resolveArg( - argResults, + final excludeTags = argResults.resolve( 'exclude-tags', testConfig.excludeTags, ); - final tags = resolveArg(argResults, 'tags', testConfig.tags); - final excludeFromCoverage = resolveArg( - argResults, + final tags = argResults.resolve('tags', testConfig.tags); + final excludeFromCoverage = argResults.resolve( 'exclude-coverage', testConfig.excludeCoverage, ); - final collectCoverageFrom = resolveArg( - argResults, + final collectCoverageFrom = argResults.resolve( 'collect-coverage-from', testConfig.collectCoverageFrom, fallbackValue: 'imports', @@ -92,39 +85,32 @@ class FlutterTestOptions { final randomSeed = randomOrderingSeed == 'random' ? Random().nextInt(4294967295).toString() : randomOrderingSeed; - final optimizePerformance = resolveArg( - argResults, + final optimizePerformance = argResults.resolve( 'optimization', testConfig.optimization, ); - final updateGoldens = resolveArg( - argResults, + final updateGoldens = argResults.resolve( 'update-goldens', testConfig.updateGoldens, ); - final failFast = resolveArg( - argResults, + final failFast = argResults.resolve( 'fail-fast', testConfig.failFast, ); final forceAnsi = argResults['force-ansi'] as bool?; - final dartDefine = resolveArg?>( - argResults, + final dartDefine = argResults.resolve?>( 'dart-define', testConfig.dartDefine, ); - final dartDefineFromFile = resolveArg?>( - argResults, + final dartDefineFromFile = argResults.resolve?>( 'dart-define-from-file', testConfig.dartDefineFromFile, ); - final platform = resolveArg( - argResults, + final platform = argResults.resolve( 'platform', testConfig.platform, ); - final reportOn = resolveArg>( - argResults, + final reportOn = argResults.resolve>( 'report-on', testConfig.reportOn, ); @@ -133,18 +119,15 @@ class FlutterTestOptions { .where((e) => e.isNotEmpty) .toList(); - final runSkipped = resolveArg( - argResults, + final runSkipped = argResults.resolve( 'run-skipped', testConfig.runSkipped, ); - final flavor = resolveArg( - argResults, + final flavor = argResults.resolve( 'flavor', testConfig.flavor, ); - final timeout = resolveArg( - argResults, + final timeout = argResults.resolve( 'timeout', testConfig.timeout, ); @@ -152,8 +135,7 @@ class FlutterTestOptions { final effectiveTimeout = timeoutSeconds != null ? Duration(seconds: timeoutSeconds) : null; - final fileReporter = resolveArg( - argResults, + final fileReporter = argResults.resolve( 'file-reporter', testConfig.fileReporter, ); diff --git a/lib/src/very_good_config/very_good_config.dart b/lib/src/very_good_config/very_good_config.dart index f3aff1c07..cd5973a7e 100644 --- a/lib/src/very_good_config/very_good_config.dart +++ b/lib/src/very_good_config/very_good_config.dart @@ -18,25 +18,25 @@ part 'very_good_config.g.dart'; /// The default name of the Very Good CLI configuration file. const veryGoodConfigFileName = 'very_good.yaml'; -/// Resolves the value for the argument named [name] against a `very_good.yaml` -/// configuration value. -/// -/// Resolution follows a fixed precedence, from highest to lowest: -/// -/// 1. A command line argument that was explicitly parsed. -/// 2. [configValue], the corresponding value from the configuration file. -/// 3. [fallbackValue], used when neither the command line nor the configuration -/// provide a value (typically the argument's command line default). -T resolveArg( - ArgResults argResults, - String name, - T? configValue, { - T? fallbackValue, -}) { - final value = configValue != null && !argResults.wasParsed(name) - ? configValue - : argResults[name] as T?; - return (value ?? fallbackValue) as T; +/// Extension that resolves argument values against `very_good.yaml` +/// configuration values. +extension ArgResultsResolver on ArgResults { + /// Resolves the value for the argument named [name] against a + /// `very_good.yaml` configuration value. + /// + /// Resolution follows a fixed precedence, from highest to lowest: + /// + /// 1. A command line argument that was explicitly parsed. + /// 2. [configValue], the corresponding value from the configuration file. + /// 3. [fallbackValue], used when neither the command line nor the + /// configuration provide a value (typically the argument's command line + /// default). + T resolve(String name, T? configValue, {T? fallbackValue}) { + final value = configValue != null && !wasParsed(name) + ? configValue + : this[name] as T?; + return (value ?? fallbackValue) as T; + } } /// {@template very_good_config_parse_exception} @@ -71,6 +71,7 @@ class VeryGoodConfig extends Equatable { /// {@macro very_good_config} const VeryGoodConfig({ this.test = const VeryGoodTestConfig(), + this.dart = const VeryGoodDartConfig(), this.packages = const VeryGoodPackagesConfig(), }); @@ -145,11 +146,14 @@ class VeryGoodConfig extends Equatable { /// Configuration values for the `very_good test` command. final VeryGoodTestConfig test; + /// Configuration values for the `very_good dart test` command. + final VeryGoodDartConfig dart; + /// Configuration values for the `very_good packages` command. final VeryGoodPackagesConfig packages; @override - List get props => [test, packages]; + List get props => [test, dart, packages]; } /// {@template very_good_test_config} @@ -283,6 +287,143 @@ class VeryGoodTestConfig extends Equatable { ]; } +/// {@template very_good_dart_config} +/// Configuration values that customize the defaults of the +/// `very_good dart test` command. +/// +/// Any field that is left as `null` retains its CLI default. +/// {@endtemplate} +@JsonSerializable( + anyMap: true, + checked: true, + createToJson: false, + disallowUnrecognizedKeys: true, + fieldRename: FieldRename.snake, +) +class VeryGoodDartConfig extends Equatable { + /// {@macro very_good_dart_config} + const VeryGoodDartConfig({this.test = const VeryGoodDartTestConfig()}); + + /// Creates a [VeryGoodDartConfig] from a decoded YAML/JSON [json] map. + factory VeryGoodDartConfig.fromJson(Map json) { + return _$VeryGoodDartConfigFromJson(json); + } + + /// Configuration values for the `very_good dart test` subcommand. + final VeryGoodDartTestConfig test; + + @override + List get props => [test]; +} + +/// {@template very_good_dart_test_config} +/// Configuration values that customize the defaults of the +/// `very_good dart test` command. +/// +/// Any field that is left as `null` retains its CLI default. +/// {@endtemplate} +@JsonSerializable( + anyMap: true, + checked: true, + createToJson: false, + disallowUnrecognizedKeys: true, + fieldRename: FieldRename.snake, +) +class VeryGoodDartTestConfig extends Equatable { + /// {@macro very_good_dart_test_config} + const VeryGoodDartTestConfig({ + this.coverage, + this.optimization, + this.concurrency, + this.tags, + this.excludeCoverage, + this.excludeTags, + this.minCoverage, + this.showUncovered, + this.collectCoverageFrom, + this.failFast, + this.platform, + this.reportOn, + this.runSkipped, + this.checkIgnore, + this.fileReporter, + }); + + /// Creates a [VeryGoodDartTestConfig] from a decoded YAML/JSON [json] map. + factory VeryGoodDartTestConfig.fromJson(Map json) { + return _$VeryGoodDartTestConfigFromJson(json); + } + + /// Whether to collect coverage information. + final bool? coverage; + + /// Whether to apply optimizations for test performance. + final bool? optimization; + + /// The number of concurrent test suites run. + @JsonKey(fromJson: _concurrency) + final String? concurrency; + + /// Run only tests associated with the specified tags. + final String? tags; + + /// A glob which will be used to exclude files that match from the coverage. + final String? excludeCoverage; + + /// Run only tests that do not have the specified tags. + final String? excludeTags; + + /// The minimum coverage percentage enforced. + @JsonKey(fromJson: _minCoverage) + final String? minCoverage; + + /// Whether to show uncovered lines when coverage is below 100%. + final bool? showUncovered; + + /// Whether to collect coverage from imported files only or all files. + @JsonKey(fromJson: _collectCoverageFrom) + final String? collectCoverageFrom; + + /// Whether to stop running tests after the first failure. + final bool? failFast; + + /// The platform to run tests on (e.g. `chrome`, `vm`). + final String? platform; + + /// Optional file paths to report coverage information to. + @JsonKey(fromJson: _stringList) + final List? reportOn; + + /// Whether to run skipped tests instead of skipping them. + final bool? runSkipped; + + /// Whether to check for and respect coverage ignore comments. + final bool? checkIgnore; + + /// Additional reporter that writes test results to a file, expressed as + /// `:` (e.g. `json:reports/tests.json`). + final String? fileReporter; + + @override + List get props => [ + coverage, + optimization, + concurrency, + tags, + excludeCoverage, + excludeTags, + minCoverage, + showUncovered, + collectCoverageFrom, + failFast, + platform, + reportOn, + runSkipped, + checkIgnore, + fileReporter, + ]; +} + /// {@template very_good_packages_config} /// Configuration values that customize the defaults of the /// `very_good packages` command. diff --git a/lib/src/very_good_config/very_good_config.g.dart b/lib/src/very_good_config/very_good_config.g.dart index 0334b407a..f551f6e21 100644 --- a/lib/src/very_good_config/very_good_config.g.dart +++ b/lib/src/very_good_config/very_good_config.g.dart @@ -8,7 +8,7 @@ part of 'very_good_config.dart'; VeryGoodConfig _$VeryGoodConfigFromJson(Map json) => $checkedCreate('VeryGoodConfig', json, ($checkedConvert) { - $checkKeys(json, allowedKeys: const ['test', 'packages']); + $checkKeys(json, allowedKeys: const ['test', 'dart', 'packages']); final val = VeryGoodConfig( test: $checkedConvert( 'test', @@ -16,6 +16,12 @@ VeryGoodConfig _$VeryGoodConfigFromJson(Map json) => ? const VeryGoodTestConfig() : VeryGoodTestConfig.fromJson(v as Map), ), + dart: $checkedConvert( + 'dart', + (v) => v == null + ? const VeryGoodDartConfig() + : VeryGoodDartConfig.fromJson(v as Map), + ), packages: $checkedConvert( 'packages', (v) => v == null @@ -99,6 +105,84 @@ VeryGoodTestConfig _$VeryGoodTestConfigFromJson(Map json) => $checkedCreate( }, ); +VeryGoodDartConfig _$VeryGoodDartConfigFromJson(Map json) => + $checkedCreate('VeryGoodDartConfig', json, ($checkedConvert) { + $checkKeys(json, allowedKeys: const ['test']); + final val = VeryGoodDartConfig( + test: $checkedConvert( + 'test', + (v) => v == null + ? const VeryGoodDartTestConfig() + : VeryGoodDartTestConfig.fromJson(v as Map), + ), + ); + return val; + }); + +VeryGoodDartTestConfig _$VeryGoodDartTestConfigFromJson(Map json) => + $checkedCreate( + 'VeryGoodDartTestConfig', + json, + ($checkedConvert) { + $checkKeys( + json, + allowedKeys: const [ + 'coverage', + 'optimization', + 'concurrency', + 'tags', + 'exclude_coverage', + 'exclude_tags', + 'min_coverage', + 'show_uncovered', + 'collect_coverage_from', + 'fail_fast', + 'platform', + 'report_on', + 'run_skipped', + 'check_ignore', + 'file_reporter', + ], + ); + final val = VeryGoodDartTestConfig( + coverage: $checkedConvert('coverage', (v) => v as bool?), + optimization: $checkedConvert('optimization', (v) => v as bool?), + concurrency: $checkedConvert('concurrency', (v) => _concurrency(v)), + tags: $checkedConvert('tags', (v) => v as String?), + excludeCoverage: $checkedConvert( + 'exclude_coverage', + (v) => v as String?, + ), + excludeTags: $checkedConvert('exclude_tags', (v) => v as String?), + minCoverage: $checkedConvert('min_coverage', (v) => _minCoverage(v)), + showUncovered: $checkedConvert('show_uncovered', (v) => v as bool?), + collectCoverageFrom: $checkedConvert( + 'collect_coverage_from', + (v) => _collectCoverageFrom(v), + ), + failFast: $checkedConvert('fail_fast', (v) => v as bool?), + platform: $checkedConvert('platform', (v) => v as String?), + reportOn: $checkedConvert('report_on', (v) => _stringList(v)), + runSkipped: $checkedConvert('run_skipped', (v) => v as bool?), + checkIgnore: $checkedConvert('check_ignore', (v) => v as bool?), + fileReporter: $checkedConvert('file_reporter', (v) => v as String?), + ); + return val; + }, + fieldKeyMap: const { + 'excludeCoverage': 'exclude_coverage', + 'excludeTags': 'exclude_tags', + 'minCoverage': 'min_coverage', + 'showUncovered': 'show_uncovered', + 'collectCoverageFrom': 'collect_coverage_from', + 'failFast': 'fail_fast', + 'reportOn': 'report_on', + 'runSkipped': 'run_skipped', + 'checkIgnore': 'check_ignore', + 'fileReporter': 'file_reporter', + }, + ); + VeryGoodPackagesConfig _$VeryGoodPackagesConfigFromJson(Map json) => $checkedCreate('VeryGoodPackagesConfig', json, ($checkedConvert) { $checkKeys(json, allowedKeys: const ['get', 'check']); diff --git a/site/docs/commands/test.md b/site/docs/commands/test.md index b76fc14ae..09b688441 100644 --- a/site/docs/commands/test.md +++ b/site/docs/commands/test.md @@ -126,4 +126,17 @@ test: With the file above, running `very_good test` behaves the same as running `very_good test --min-coverage 100 --exclude-coverage '**/*.g.dart' --report-on lib/ --dart-define=FLAVOR=development`. You can still override any of these values on the command line, for example `very_good test --min-coverage 90` to lower the coverage threshold for a single run. +To customize the defaults for `very_good dart test`, use the `dart.test` section. It accepts the same options as `test` (minus the Flutter-only flags such as `update_goldens`, `flavor`, `timeout`, `dart_define`, and `dart_define_from_file`) plus `check_ignore`. + +```yaml +# very_good.yaml +dart: + test: + min_coverage: 100 + exclude_coverage: '**/*.g.dart' + report_on: + - lib/ + file_reporter: json:reports/tests.json +``` + The `very_good.yaml` file is looked up starting from the directory where the command runs and walking up through its ancestors. The closest file wins; configuration from ancestor directories is not merged. This lets a single `very_good.yaml` at the repository root apply to commands run from any nested package. diff --git a/test/src/commands/dart/commands/dart_test_test.dart b/test/src/commands/dart/commands/dart_test_test.dart index 9de61ba60..d157d012b 100644 --- a/test/src/commands/dart/commands/dart_test_test.dart +++ b/test/src/commands/dart/commands/dart_test_test.dart @@ -11,6 +11,7 @@ import 'package:path/path.dart' as path; import 'package:test/test.dart'; import 'package:very_good_cli/src/cli/cli.dart'; import 'package:very_good_cli/src/commands/dart/commands/commands.dart'; +import 'package:very_good_cli/src/very_good_config/very_good_config.dart'; import '../../../../helpers/helpers.dart'; @@ -829,5 +830,100 @@ void main() { ), ).called(1); }); + + group('very_good.yaml configuration', () { + test( + 'fails with exit code ${ExitCode.config.code} ' + 'when very_good.yaml is malformed', + withRunner((commandRunner, logger, pubUpdater, printLogs) async { + final tempDirectory = Directory.systemTemp.createTempSync(); + addTearDown(() { + Directory.current = cwd; + tempDirectory.deleteSync(recursive: true); + }); + + Directory.current = tempDirectory.path; + File(path.join(tempDirectory.path, 'pubspec.yaml')).createSync(); + File( + path.join(tempDirectory.path, 'very_good.yaml'), + ).writeAsStringSync('- not\n- a\n- map'); + + final result = await commandRunner.run(['dart', 'test']); + expect(result, equals(ExitCode.config.code)); + verify( + () => logger.err( + any(that: contains('Could not read `very_good.yaml`')), + ), + ).called(1); + }), + ); + + test('applies config value when arg was not parsed', () { + when(() => argResults.wasParsed(any())).thenReturn(false); + final options = DartTestOptions.parse( + argResults, + config: const VeryGoodConfig( + dart: VeryGoodDartConfig( + test: VeryGoodDartTestConfig( + minCoverage: '90', + excludeCoverage: '**/*.g.dart', + reportOn: ['lib/'], + fileReporter: 'json:reports/tests.json', + ), + ), + ), + ); + expect(options.minCoverage, equals(90)); + expect(options.excludeFromCoverage, equals('**/*.g.dart')); + expect(options.reportOn, equals(['lib/'])); + expect(options.fileReporter, equals('json:reports/tests.json')); + }); + + test('CLI argument takes precedence over config value', () { + when(() => argResults.wasParsed(any())).thenReturn(false); + when(() => argResults.wasParsed('min-coverage')).thenReturn(true); + when(() => argResults['min-coverage']).thenReturn('50'); + + final options = DartTestOptions.parse( + argResults, + config: const VeryGoodConfig( + dart: VeryGoodDartConfig( + test: VeryGoodDartTestConfig(minCoverage: '90'), + ), + ), + ); + expect(options.minCoverage, equals(50)); + }); + + test('CLI --file-reporter takes precedence over config value', () { + when(() => argResults.wasParsed(any())).thenReturn(false); + when(() => argResults.wasParsed('file-reporter')).thenReturn(true); + when( + () => argResults['file-reporter'], + ).thenReturn('json:cli.json'); + + final options = DartTestOptions.parse( + argResults, + config: const VeryGoodConfig( + dart: VeryGoodDartConfig( + test: VeryGoodDartTestConfig(fileReporter: 'json:config.json'), + ), + ), + ); + expect(options.fileReporter, equals('json:cli.json')); + }); + + test('falls back to the CLI default when the parsed arg is null ' + 'and the config is unset', () { + when(() => argResults.wasParsed(any())).thenReturn(true); + when( + () => argResults['collect-coverage-from'], + ).thenReturn(null); + + final options = DartTestOptions.parse(argResults); + + expect(options.collectCoverageFrom, CoverageCollectionMode.imports); + }); + }); }); } diff --git a/test/src/very_good_config/fixtures/all_dart_test_options.yaml b/test/src/very_good_config/fixtures/all_dart_test_options.yaml new file mode 100644 index 000000000..9a6468d65 --- /dev/null +++ b/test/src/very_good_config/fixtures/all_dart_test_options.yaml @@ -0,0 +1,19 @@ +dart: + test: + coverage: true + optimization: false + concurrency: 8 + tags: my-tag + exclude_coverage: "**/*.g.dart" + exclude_tags: skip + min_coverage: 95 + show_uncovered: true + collect_coverage_from: all + fail_fast: true + platform: chrome + report_on: + - lib/ + - packages/foo/lib/ + run_skipped: true + check_ignore: false + file_reporter: json:reports/tests.json diff --git a/test/src/very_good_config/very_good_config_test.dart b/test/src/very_good_config/very_good_config_test.dart index a9e53d2be..60a48bdc5 100644 --- a/test/src/very_good_config/very_good_config_test.dart +++ b/test/src/very_good_config/very_good_config_test.dart @@ -64,10 +64,7 @@ void main() { expect(config.test.runSkipped, isTrue); expect(config.test.flavor, equals('staging')); expect(config.test.timeout, equals('30')); - expect( - config.test.fileReporter, - equals('json:reports/tests.json'), - ); + expect(config.test.fileReporter, equals('json:reports/tests.json')); }); test('parses min-coverage as decimal string', () { @@ -141,9 +138,8 @@ test: test('throws when collect-coverage-from has invalid value', () { expect( - () => VeryGoodConfig.fromString( - 'test:\n collect_coverage_from: bad', - ), + () => + VeryGoodConfig.fromString('test:\n collect_coverage_from: bad'), throwsA(isA()), ); }); @@ -162,6 +158,69 @@ test: ); }); + test('parses all supported dart test options', () { + final fixture = File( + p.join( + 'test', + 'src', + 'very_good_config', + 'fixtures', + 'all_dart_test_options.yaml', + ), + ); + final config = VeryGoodConfig.fromString(fixture.readAsStringSync()); + + expect(config.dart.test.coverage, isTrue); + expect(config.dart.test.optimization, isFalse); + expect(config.dart.test.concurrency, equals('8')); + expect(config.dart.test.tags, equals('my-tag')); + expect(config.dart.test.excludeCoverage, equals('**/*.g.dart')); + expect(config.dart.test.excludeTags, equals('skip')); + expect(config.dart.test.minCoverage, equals('95')); + expect(config.dart.test.showUncovered, isTrue); + expect(config.dart.test.collectCoverageFrom, equals('all')); + expect(config.dart.test.failFast, isTrue); + expect(config.dart.test.platform, equals('chrome')); + expect( + config.dart.test.reportOn, + equals(['lib/', 'packages/foo/lib/']), + ); + expect(config.dart.test.runSkipped, isTrue); + expect(config.dart.test.checkIgnore, isFalse); + expect( + config.dart.test.fileReporter, + equals('json:reports/tests.json'), + ); + }); + + test('throws when dart section is not a map', () { + expect( + () => VeryGoodConfig.fromString('dart: foo'), + throwsA(isA()), + ); + }); + + test('throws when dart.test section is not a map', () { + expect( + () => VeryGoodConfig.fromString('dart:\n test: foo'), + throwsA(isA()), + ); + }); + + test('throws when an unrecognized dart key is present', () { + expect( + () => VeryGoodConfig.fromString('dart:\n unknown: true'), + throwsA(isA()), + ); + }); + + test('throws when an unrecognized dart.test key is present', () { + expect( + () => VeryGoodConfig.fromString('dart:\n test:\n unknown: true'), + throwsA(isA()), + ); + }); + test('parses all supported packages options', () { final fixture = File( p.join( @@ -182,10 +241,7 @@ test: final licenses = config.packages.check.licenses; expect(licenses.ignoreRetrievalFailures, isTrue); - expect( - licenses.dependencyType, - equals(['direct-main', 'direct-dev']), - ); + expect(licenses.dependencyType, equals(['direct-main', 'direct-dev'])); expect(licenses.allowed, equals(['MIT', 'BSD-3-Clause'])); expect(licenses.forbidden, isNull); expect(licenses.skipPackages, equals(['very_good_analysis'])); @@ -399,9 +455,8 @@ packages: setUp(() { tempDir = Directory.systemTemp.createTempSync('very_good_config_'); - nestedDir = Directory( - p.join(tempDir.path, 'packages', 'foo'), - )..createSync(recursive: true); + nestedDir = Directory(p.join(tempDir.path, 'packages', 'foo')) + ..createSync(recursive: true); }); tearDown(() { @@ -475,6 +530,36 @@ test: equals(VeryGoodConfig(test: VeryGoodTestConfig(coverage: false))), ), ); + expect( + VeryGoodConfig( + dart: VeryGoodDartConfig( + test: VeryGoodDartTestConfig(coverage: true), + ), + ), + equals( + VeryGoodConfig( + dart: VeryGoodDartConfig( + test: VeryGoodDartTestConfig(coverage: true), + ), + ), + ), + ); + expect( + VeryGoodConfig( + dart: VeryGoodDartConfig( + test: VeryGoodDartTestConfig(coverage: true), + ), + ), + isNot( + equals( + VeryGoodConfig( + dart: VeryGoodDartConfig( + test: VeryGoodDartTestConfig(coverage: false), + ), + ), + ), + ), + ); expect( VeryGoodConfig( packages: VeryGoodPackagesConfig( @@ -521,16 +606,44 @@ test: }); }); - group(VeryGoodPackagesConfig, () { + group(VeryGoodDartConfig, () { test('supports value equality', () { + expect(VeryGoodDartConfig(), equals(VeryGoodDartConfig())); expect( - VeryGoodPackagesConfig(), - equals(VeryGoodPackagesConfig()), + VeryGoodDartConfig(test: VeryGoodDartTestConfig(coverage: true)), + equals( + VeryGoodDartConfig(test: VeryGoodDartTestConfig(coverage: true)), + ), ); expect( - VeryGoodPackagesConfig( - get: VeryGoodPackagesGetConfig(recursive: true), + VeryGoodDartConfig(test: VeryGoodDartTestConfig(coverage: true)), + isNot( + equals( + VeryGoodDartConfig(test: VeryGoodDartTestConfig(coverage: false)), + ), ), + ); + }); + }); + + group(VeryGoodDartTestConfig, () { + test('supports value equality', () { + expect( + VeryGoodDartTestConfig(coverage: true, minCoverage: '95'), + equals(VeryGoodDartTestConfig(coverage: true, minCoverage: '95')), + ); + expect( + VeryGoodDartTestConfig(coverage: true), + isNot(equals(VeryGoodDartTestConfig(coverage: false))), + ); + }); + }); + + group(VeryGoodPackagesConfig, () { + test('supports value equality', () { + expect(VeryGoodPackagesConfig(), equals(VeryGoodPackagesConfig())); + expect( + VeryGoodPackagesConfig(get: VeryGoodPackagesGetConfig(recursive: true)), equals( VeryGoodPackagesConfig( get: VeryGoodPackagesGetConfig(recursive: true), @@ -538,9 +651,7 @@ test: ), ); expect( - VeryGoodPackagesConfig( - get: VeryGoodPackagesGetConfig(recursive: true), - ), + VeryGoodPackagesConfig(get: VeryGoodPackagesGetConfig(recursive: true)), isNot( equals( VeryGoodPackagesConfig(