Skip to content

Commit 9b16680

Browse files
feat(dart): add dart config to very_good.yaml
Closes #1655 Co-authored-by: marcossevilla <marcossevilla@users.noreply.github.com>
1 parent 2266566 commit 9b16680

7 files changed

Lines changed: 603 additions & 23 deletions

File tree

lib/src/commands/dart/commands/dart_test_command.dart

Lines changed: 117 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import 'package:mason/mason.dart';
77
import 'package:meta/meta.dart';
88
import 'package:path/path.dart' as path;
99
import 'package:very_good_cli/src/cli/cli.dart';
10+
import 'package:very_good_cli/src/very_good_config/very_good_config.dart';
1011

1112
/// Options for configuring the Dart test command.
1213
class DartTestOptions {
@@ -32,18 +33,53 @@ class DartTestOptions {
3233
});
3334

3435
/// Parses [ArgResults] into a [DartTestOptions] instance.
35-
factory DartTestOptions.parse(ArgResults argResults) {
36-
final concurrency = argResults['concurrency'] as String;
37-
final collectCoverage = argResults['coverage'] as bool;
38-
final minCoverage = double.tryParse(
39-
argResults['min-coverage'] as String? ?? '',
36+
///
37+
/// When [config] is provided, its values are used as defaults for any
38+
/// option that was not explicitly parsed on the command line.
39+
factory DartTestOptions.parse(
40+
ArgResults argResults, {
41+
VeryGoodConfig config = VeryGoodConfig.empty,
42+
}) {
43+
final testConfig = config.dart.test;
44+
45+
final concurrency = _resolveArg(
46+
argResults,
47+
'concurrency',
48+
testConfig.concurrency,
49+
);
50+
final collectCoverage = _resolveArg(
51+
argResults,
52+
'coverage',
53+
testConfig.coverage,
54+
);
55+
final minCoverageString = _resolveArg<String?>(
56+
argResults,
57+
'min-coverage',
58+
testConfig.minCoverage,
59+
);
60+
final minCoverage = double.tryParse(minCoverageString ?? '');
61+
final showUncovered = _resolveArg(
62+
argResults,
63+
'show-uncovered',
64+
testConfig.showUncovered,
65+
);
66+
final excludeTags = _resolveArg<String?>(
67+
argResults,
68+
'exclude-tags',
69+
testConfig.excludeTags,
70+
);
71+
final tags = _resolveArg<String?>(argResults, 'tags', testConfig.tags);
72+
final excludeFromCoverage = _resolveArg<String?>(
73+
argResults,
74+
'exclude-coverage',
75+
testConfig.excludeCoverage,
76+
);
77+
final collectCoverageFromString = _resolveArg<String>(
78+
argResults,
79+
'collect-coverage-from',
80+
testConfig.collectCoverageFrom,
81+
fallbackValue: 'imports',
4082
);
41-
final showUncovered = argResults['show-uncovered'] as bool;
42-
final excludeTags = argResults['exclude-tags'] as String?;
43-
final tags = argResults['tags'] as String?;
44-
final excludeFromCoverage = argResults['exclude-coverage'] as String?;
45-
final collectCoverageFromString =
46-
argResults['collect-coverage-from'] as String? ?? 'imports';
4783
final collectCoverageFrom = CoverageCollectionMode.fromString(
4884
collectCoverageFromString,
4985
);
@@ -52,17 +88,46 @@ class DartTestOptions {
5288
final randomSeed = randomOrderingSeed == 'random'
5389
? Random().nextInt(4294967295).toString()
5490
: randomOrderingSeed;
55-
final optimizePerformance = argResults['optimization'] as bool;
56-
final failFast = argResults['fail-fast'] as bool;
91+
final optimizePerformance = _resolveArg(
92+
argResults,
93+
'optimization',
94+
testConfig.optimization,
95+
);
96+
final failFast = _resolveArg(
97+
argResults,
98+
'fail-fast',
99+
testConfig.failFast,
100+
);
57101
final forceAnsi = argResults['force-ansi'] as bool?;
58-
final platform = argResults['platform'] as String?;
59-
final reportOn = (argResults['report-on'] as List<String>)
102+
final platform = _resolveArg<String?>(
103+
argResults,
104+
'platform',
105+
testConfig.platform,
106+
);
107+
final reportOn = _resolveArg<List<String>>(
108+
argResults,
109+
'report-on',
110+
testConfig.reportOn,
111+
);
112+
final effectiveReportOn = reportOn
60113
.expand((e) => e.split(RegExp(r'[,\s]+')))
61114
.where((e) => e.isNotEmpty)
62115
.toList();
63-
final runSkipped = argResults['run-skipped'] as bool;
64-
final checkIgnore = argResults['check-ignore'] as bool;
65-
final fileReporter = argResults['file-reporter'] as String?;
116+
final runSkipped = _resolveArg(
117+
argResults,
118+
'run-skipped',
119+
testConfig.runSkipped,
120+
);
121+
final checkIgnore = _resolveArg(
122+
argResults,
123+
'check-ignore',
124+
testConfig.checkIgnore,
125+
);
126+
final fileReporter = _resolveArg<String?>(
127+
argResults,
128+
'file-reporter',
129+
testConfig.fileReporter,
130+
);
66131
final rest = argResults.rest;
67132

68133
return DartTestOptions._(
@@ -79,7 +144,7 @@ class DartTestOptions {
79144
failFast: failFast,
80145
forceAnsi: forceAnsi,
81146
platform: platform,
82-
reportOn: reportOn,
147+
reportOn: effectiveReportOn,
83148
runSkipped: runSkipped,
84149
checkIgnore: checkIgnore,
85150
fileReporter: fileReporter,
@@ -144,6 +209,27 @@ class DartTestOptions {
144209
final List<String> rest;
145210
}
146211

212+
/// Resolves the value for the argument named [name] against a `very_good.yaml`
213+
/// configuration value.
214+
///
215+
/// Resolution follows a fixed precedence, from highest to lowest:
216+
///
217+
/// 1. A command line argument that was explicitly parsed.
218+
/// 2. [configValue], the corresponding value from the configuration file.
219+
/// 3. [fallbackValue], used when neither the command line nor the configuration
220+
/// provide a value (typically the argument's command line default).
221+
T _resolveArg<T>(
222+
ArgResults argResults,
223+
String name,
224+
T? configValue, {
225+
T? fallbackValue,
226+
}) {
227+
final value = configValue != null && !argResults.wasParsed(name)
228+
? configValue
229+
: argResults[name] as T?;
230+
return (value ?? fallbackValue) as T;
231+
}
232+
147233
/// Signature for the [Dart.installed] method.
148234
typedef DartInstalledCommand = Future<bool> Function({required Logger logger});
149235

@@ -327,9 +413,20 @@ This command should be run from the root of your Dart project.''');
327413
return ExitCode.noInput.code;
328414
}
329415

416+
final VeryGoodConfig config;
417+
try {
418+
config = VeryGoodConfig.loadFromClosestAncestor(Directory(targetPath));
419+
} on VeryGoodConfigParseException catch (e) {
420+
_logger.err(
421+
'Could not read `$veryGoodConfigFileName`.\n'
422+
'${e.message}',
423+
);
424+
return ExitCode.config.code;
425+
}
426+
330427
final isDartInstalled = await _dartInstalled(logger: _logger);
331428

332-
final options = DartTestOptions.parse(_argResults);
429+
final options = DartTestOptions.parse(_argResults, config: config);
333430

334431
if (isDartInstalled) {
335432
try {

lib/src/very_good_config/very_good_config.dart

Lines changed: 147 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,10 @@ class VeryGoodConfigParseException implements Exception {
4848
)
4949
class VeryGoodConfig extends Equatable {
5050
/// {@macro very_good_config}
51-
const VeryGoodConfig({this.test = const VeryGoodTestConfig()});
51+
const VeryGoodConfig({
52+
this.test = const VeryGoodTestConfig(),
53+
this.dart = const VeryGoodDartConfig(),
54+
});
5255

5356
/// Creates a [VeryGoodConfig] from a decoded YAML/JSON [json] map.
5457
factory VeryGoodConfig.fromJson(Map<dynamic, dynamic> json) {
@@ -121,8 +124,11 @@ class VeryGoodConfig extends Equatable {
121124
/// Configuration values for the `very_good test` command.
122125
final VeryGoodTestConfig test;
123126

127+
/// Configuration values for the `very_good dart test` command.
128+
final VeryGoodDartConfig dart;
129+
124130
@override
125-
List<Object?> get props => [test];
131+
List<Object?> get props => [test, dart];
126132
}
127133

128134
/// {@template very_good_test_config}
@@ -256,6 +262,145 @@ class VeryGoodTestConfig extends Equatable {
256262
];
257263
}
258264

265+
/// {@template very_good_dart_config}
266+
/// Configuration values that customize the defaults of the
267+
/// `very_good dart test` command.
268+
///
269+
/// Any field that is left as `null` retains its CLI default.
270+
/// {@endtemplate}
271+
@JsonSerializable(
272+
anyMap: true,
273+
checked: true,
274+
createToJson: false,
275+
disallowUnrecognizedKeys: true,
276+
fieldRename: FieldRename.snake,
277+
)
278+
class VeryGoodDartConfig extends Equatable {
279+
/// {@macro very_good_dart_config}
280+
const VeryGoodDartConfig({
281+
this.test = const VeryGoodDartTestConfig(),
282+
});
283+
284+
/// Creates a [VeryGoodDartConfig] from a decoded YAML/JSON [json] map.
285+
factory VeryGoodDartConfig.fromJson(Map<dynamic, dynamic> json) {
286+
return _$VeryGoodDartConfigFromJson(json);
287+
}
288+
289+
/// Configuration values for the `very_good dart test` subcommand.
290+
final VeryGoodDartTestConfig test;
291+
292+
@override
293+
List<Object?> get props => [test];
294+
}
295+
296+
/// {@template very_good_dart_test_config}
297+
/// Configuration values that customize the defaults of the
298+
/// `very_good dart test` command.
299+
///
300+
/// Any field that is left as `null` retains its CLI default.
301+
/// {@endtemplate}
302+
@JsonSerializable(
303+
anyMap: true,
304+
checked: true,
305+
createToJson: false,
306+
disallowUnrecognizedKeys: true,
307+
fieldRename: FieldRename.snake,
308+
)
309+
class VeryGoodDartTestConfig extends Equatable {
310+
/// {@macro very_good_dart_test_config}
311+
const VeryGoodDartTestConfig({
312+
this.coverage,
313+
this.optimization,
314+
this.concurrency,
315+
this.tags,
316+
this.excludeCoverage,
317+
this.excludeTags,
318+
this.minCoverage,
319+
this.showUncovered,
320+
this.collectCoverageFrom,
321+
this.failFast,
322+
this.platform,
323+
this.reportOn,
324+
this.runSkipped,
325+
this.checkIgnore,
326+
this.fileReporter,
327+
});
328+
329+
/// Creates a [VeryGoodDartTestConfig] from a decoded YAML/JSON [json] map.
330+
factory VeryGoodDartTestConfig.fromJson(Map<dynamic, dynamic> json) {
331+
return _$VeryGoodDartTestConfigFromJson(json);
332+
}
333+
334+
/// Whether to collect coverage information.
335+
final bool? coverage;
336+
337+
/// Whether to apply optimizations for test performance.
338+
final bool? optimization;
339+
340+
/// The number of concurrent test suites run.
341+
@JsonKey(fromJson: _concurrency)
342+
final String? concurrency;
343+
344+
/// Run only tests associated with the specified tags.
345+
final String? tags;
346+
347+
/// A glob which will be used to exclude files that match from the coverage.
348+
final String? excludeCoverage;
349+
350+
/// Run only tests that do not have the specified tags.
351+
final String? excludeTags;
352+
353+
/// The minimum coverage percentage enforced.
354+
@JsonKey(fromJson: _minCoverage)
355+
final String? minCoverage;
356+
357+
/// Whether to show uncovered lines when coverage is below 100%.
358+
final bool? showUncovered;
359+
360+
/// Whether to collect coverage from imported files only or all files.
361+
@JsonKey(fromJson: _collectCoverageFrom)
362+
final String? collectCoverageFrom;
363+
364+
/// Whether to stop running tests after the first failure.
365+
final bool? failFast;
366+
367+
/// The platform to run tests on (e.g. `chrome`, `vm`).
368+
final String? platform;
369+
370+
/// Optional file paths to report coverage information to.
371+
@JsonKey(fromJson: _stringList)
372+
final List<String>? reportOn;
373+
374+
/// Whether to run skipped tests instead of skipping them.
375+
final bool? runSkipped;
376+
377+
/// Whether to check for and respect coverage ignore comments.
378+
final bool? checkIgnore;
379+
380+
/// Additional reporter that writes test results to a file, expressed as
381+
/// `<name>:<path>` (e.g. `json:reports/tests.json`).
382+
final String? fileReporter;
383+
384+
@override
385+
List<Object?> get props => [
386+
coverage,
387+
optimization,
388+
concurrency,
389+
tags,
390+
excludeCoverage,
391+
excludeTags,
392+
minCoverage,
393+
showUncovered,
394+
collectCoverageFrom,
395+
failFast,
396+
platform,
397+
reportOn,
398+
runSkipped,
399+
checkIgnore,
400+
fileReporter,
401+
];
402+
}
403+
259404
// The coercers below intentionally validate more strictly than the CLI flag
260405
// parser. A value such as `min_coverage: 150` is rejected here at config load
261406
// time even though `--min-coverage 150` is accepted by the flag parser, so

0 commit comments

Comments
 (0)