diff --git a/lib/src/commands/dart/commands/dart_test_command.dart b/lib/src/commands/dart/commands/dart_test_command.dart index 6e9b25efc..124dd2487 100644 --- a/lib/src/commands/dart/commands/dart_test_command.dart +++ b/lib/src/commands/dart/commands/dart_test_command.dart @@ -28,6 +28,7 @@ class DartTestOptions { required this.reportOn, required this.runSkipped, required this.checkIgnore, + required this.fileReporter, }); /// Parses [ArgResults] into a [DartTestOptions] instance. @@ -61,6 +62,7 @@ class DartTestOptions { .toList(); final runSkipped = argResults['run-skipped'] as bool; final checkIgnore = argResults['check-ignore'] as bool; + final fileReporter = argResults['file-reporter'] as String?; final rest = argResults.rest; return DartTestOptions._( @@ -80,6 +82,7 @@ class DartTestOptions { reportOn: reportOn, runSkipped: runSkipped, checkIgnore: checkIgnore, + fileReporter: fileReporter, rest: rest, ); } @@ -133,6 +136,10 @@ class DartTestOptions { /// 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; + /// The remaining arguments passed to the `dart test` command. final List rest; } @@ -281,6 +288,13 @@ class DartTestCommand extends Command { help: 'Whether to check for and respect coverage ignore comments ' '(e.g. // coverage:ignore-line).', + ) + ..addOption( + 'file-reporter', + help: + 'Enable an additional reporter writing test results to a file. ' + 'Should be in the form : (e.g. "json:reports/tests.json").', + valueHelp: 'name:path', ); } @@ -347,6 +361,8 @@ This command should be run from the root of your Dart project.'''); if (options.runSkipped) '--run-skipped', if (options.platform != null) ...['--platform', options.platform!], if (options.platform == null) ...['-j', options.concurrency], + if (options.fileReporter != null) + '--file-reporter=${options.fileReporter}', ...options.rest, ], reportOn: options.reportOn.isEmpty ? null : options.reportOn, diff --git a/lib/src/commands/test/test.dart b/lib/src/commands/test/test.dart index cfef17a63..13350bf68 100644 --- a/lib/src/commands/test/test.dart +++ b/lib/src/commands/test/test.dart @@ -31,6 +31,7 @@ class FlutterTestOptions { required this.runSkipped, required this.flavor, required this.timeout, + required this.fileReporter, required this.rest, }); @@ -69,12 +70,11 @@ class FlutterTestOptions { .toList(); final runSkipped = argResults['run-skipped'] as bool; final flavor = argResults['flavor'] as String?; - final timeoutSeconds = int.tryParse( - argResults['timeout'] as String? ?? '', - ); + final timeoutSeconds = int.tryParse(argResults['timeout'] as String? ?? ''); final timeout = timeoutSeconds != null ? Duration(seconds: timeoutSeconds) : null; + final fileReporter = argResults['file-reporter'] as String?; final rest = argResults.rest; return FlutterTestOptions._( @@ -98,6 +98,7 @@ class FlutterTestOptions { runSkipped: runSkipped, flavor: flavor, timeout: timeout, + fileReporter: fileReporter, rest: rest, ); } @@ -164,6 +165,10 @@ class FlutterTestOptions { /// Maximum time to let tests run before killing the process. final Duration? timeout; + /// Additional reporter that writes test results to a file, expressed as + /// `:` (e.g. `json:reports/tests.json`). + final String? fileReporter; + /// The remaining arguments passed to the test command. final List rest; } @@ -348,6 +353,13 @@ class TestCommand extends Command { 'Maximum seconds to let tests run before killing the process. ' 'Useful when tests hang due to an unbounded pumpAndSettle() call.', valueHelp: 'seconds', + ) + ..addOption( + 'file-reporter', + help: + 'Enable an additional reporter writing test results to a file. ' + 'Should be in the form : (e.g. "json:reports/tests.json").', + valueHelp: 'name:path', ); } @@ -428,6 +440,8 @@ This command should be run from the root of your Flutter project.'''); '--no-pub', if (options.timeout != null) '--timeout=${options.timeout!.inSeconds}s', + if (options.fileReporter != null) + '--file-reporter=${options.fileReporter}', ...options.rest, ], ); diff --git a/site/docs/commands/test.md b/site/docs/commands/test.md index 80a90e897..8be96f533 100644 --- a/site/docs/commands/test.md +++ b/site/docs/commands/test.md @@ -48,10 +48,25 @@ very_good test [arguments] --fail-fast Stop running tests after the first failure. --timeout= Maximum seconds to let tests run before killing the process. Useful when tests hang due to an unbounded pumpAndSettle() call. + --file-reporter= Enable an additional reporter writing test results to a file. + Should be in the form : (e.g. "json:reports/tests.json"). Run "very_good help" to see global options. ``` +### Machine-readable test reports + +If you need a machine-readable summary of your test results (for example to feed a CI tool or convert to JUnit XML), use `--file-reporter`. It maps directly to the `--file-reporter` flag from `flutter test` and `dart test`, and writes the report to a file instead of stdout — so it stays compatible with `very_good test`'s optimization and progress rendering. + +```sh +# Emits reports/tests.json alongside the normal test output. +very_good test --coverage --file-reporter json:reports/tests.json +``` + +:::info +`--reporter` and `--machine` are intentionally not exposed: they replace stdout with a machine-readable stream, which prevents `very_good test` from rendering progress and picking up failures. `--file-reporter` gives you the same machine-readable content without that trade-off. +::: + :::tip For **Dart** projects, use **`very_good dart test`** instead. diff --git a/test/src/commands/dart/commands/dart_test_test.dart b/test/src/commands/dart/commands/dart_test_test.dart index e1d2b45d5..9de61ba60 100644 --- a/test/src/commands/dart/commands/dart_test_test.dart +++ b/test/src/commands/dart/commands/dart_test_test.dart @@ -48,6 +48,7 @@ const expectedTestUsage = [ ' --run-skipped Run skipped tests instead of skipping them.\n' ' --[no-]check-ignore Whether to check for and respect coverage ignore comments (e.g. // coverage:ignore-line).\n' ' (defaults to on)\n' + ' --file-reporter= Enable an additional reporter writing test results to a file. Should be in the form : (e.g. "json:reports/tests.json").\n' '\n' 'Run "very_good help" to see global options.', ]; @@ -135,6 +136,7 @@ void main() { () => argResults['collect-coverage-from'], ).thenReturn('imports'); when(() => argResults['report-on']).thenReturn([]); + when(() => argResults['file-reporter']).thenReturn(null); when(() => argResults.rest).thenReturn([]); }); @@ -807,5 +809,25 @@ void main() { ), ).called(1); }); + + test('completes normally --file-reporter json:test-report.json', () async { + when( + () => argResults['file-reporter'], + ).thenReturn('json:test-report.json'); + final result = await testCommand.run(); + expect(result, equals(ExitCode.success.code)); + verify( + () => dartTest( + optimizePerformance: true, + arguments: [ + ...defaultArguments, + '--file-reporter=json:test-report.json', + ], + logger: logger, + stdout: logger.write, + stderr: logger.err, + ), + ).called(1); + }); }); } diff --git a/test/src/commands/test/test_test.dart b/test/src/commands/test/test_test.dart index 5e6e466ea..e4503a11b 100644 --- a/test/src/commands/test/test_test.dart +++ b/test/src/commands/test/test_test.dart @@ -51,6 +51,7 @@ const expectedTestUsage = [ ' --run-skipped Run skipped tests instead of skipping them.\n' ' --flavor Build a custom app flavor as defined by platform-specific build setup. Supports the use of product flavors in Android Gradle scripts, and the use of custom Xcode schemes.\n' ' --timeout= Maximum seconds to let tests run before killing the process. Useful when tests hang due to an unbounded pumpAndSettle() call.\n' + ' --file-reporter= Enable an additional reporter writing test results to a file. Should be in the form : (e.g. "json:reports/tests.json").\n' '\n' 'Run "very_good help" to see global options.', ]; @@ -135,6 +136,7 @@ void main() { when(() => argResults['run-skipped']).thenReturn(false); when(() => argResults['flavor']).thenReturn(null); when(() => argResults['timeout']).thenReturn(null); + when(() => argResults['file-reporter']).thenReturn(null); when( () => argResults['collect-coverage-from'], ).thenReturn('imports'); @@ -600,6 +602,29 @@ void main() { ), ).called(1); }); + + test( + 'completes normally --file-reporter json:test-report.json', + () async { + when( + () => argResults['file-reporter'], + ).thenReturn('json:test-report.json'); + final result = await testCommand.run(); + expect(result, equals(ExitCode.success.code)); + verify( + () => flutterTest( + optimizePerformance: true, + arguments: [ + ...defaultArguments, + '--file-reporter=json:test-report.json', + ], + logger: logger, + stdout: logger.write, + stderr: logger.err, + ), + ).called(1); + }, + ); }); group('coverage', () {