Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions lib/src/commands/dart/commands/dart_test_command.dart
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ class DartTestOptions {
required this.reportOn,
required this.runSkipped,
required this.checkIgnore,
required this.fileReporter,
});

/// Parses [ArgResults] into a [DartTestOptions] instance.
Expand Down Expand Up @@ -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._(
Expand All @@ -80,6 +82,7 @@ class DartTestOptions {
reportOn: reportOn,
runSkipped: runSkipped,
checkIgnore: checkIgnore,
fileReporter: fileReporter,
rest: rest,
);
}
Expand Down Expand Up @@ -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
/// `<name>:<path>` (e.g. `json:reports/tests.json`).
final String? fileReporter;

/// The remaining arguments passed to the `dart test` command.
final List<String> rest;
}
Expand Down Expand Up @@ -281,6 +288,13 @@ class DartTestCommand extends Command<int> {
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 <name>:<path> (e.g. "json:reports/tests.json").',
valueHelp: 'name:path',
);
}

Expand Down Expand Up @@ -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,
Expand Down
20 changes: 17 additions & 3 deletions lib/src/commands/test/test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ class FlutterTestOptions {
required this.runSkipped,
required this.flavor,
required this.timeout,
required this.fileReporter,
required this.rest,
});

Expand Down Expand Up @@ -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._(
Expand All @@ -98,6 +98,7 @@ class FlutterTestOptions {
runSkipped: runSkipped,
flavor: flavor,
timeout: timeout,
fileReporter: fileReporter,
rest: rest,
);
}
Expand Down Expand Up @@ -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
/// `<name>:<path>` (e.g. `json:reports/tests.json`).
final String? fileReporter;

/// The remaining arguments passed to the test command.
final List<String> rest;
}
Expand Down Expand Up @@ -348,6 +353,13 @@ class TestCommand extends Command<int> {
'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 <name>:<path> (e.g. "json:reports/tests.json").',
valueHelp: 'name:path',
);
}

Expand Down Expand Up @@ -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,
],
);
Expand Down
15 changes: 15 additions & 0 deletions site/docs/commands/test.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,25 @@ very_good test [arguments]
--fail-fast Stop running tests after the first failure.
--timeout=<seconds> Maximum seconds to let tests run before killing the process.
Useful when tests hang due to an unbounded pumpAndSettle() call.
--file-reporter=<name:path> Enable an additional reporter writing test results to a file.
Should be in the form <name>:<path> (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.

Expand Down
22 changes: 22 additions & 0 deletions test/src/commands/dart/commands/dart_test_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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=<name:path> Enable an additional reporter writing test results to a file. Should be in the form <name>:<path> (e.g. "json:reports/tests.json").\n'
'\n'
'Run "very_good help" to see global options.',
];
Expand Down Expand Up @@ -135,6 +136,7 @@ void main() {
() => argResults['collect-coverage-from'],
).thenReturn('imports');
when<dynamic>(() => argResults['report-on']).thenReturn(<String>[]);
when<dynamic>(() => argResults['file-reporter']).thenReturn(null);
when(() => argResults.rest).thenReturn([]);
});

Expand Down Expand Up @@ -807,5 +809,25 @@ void main() {
),
).called(1);
});

test('completes normally --file-reporter json:test-report.json', () async {
when<dynamic>(
() => 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);
});
});
}
25 changes: 25 additions & 0 deletions test/src/commands/test/test_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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=<seconds> Maximum seconds to let tests run before killing the process. Useful when tests hang due to an unbounded pumpAndSettle() call.\n'
' --file-reporter=<name:path> Enable an additional reporter writing test results to a file. Should be in the form <name>:<path> (e.g. "json:reports/tests.json").\n'
'\n'
'Run "very_good help" to see global options.',
];
Expand Down Expand Up @@ -135,6 +136,7 @@ void main() {
when<dynamic>(() => argResults['run-skipped']).thenReturn(false);
when<dynamic>(() => argResults['flavor']).thenReturn(null);
when<dynamic>(() => argResults['timeout']).thenReturn(null);
when<dynamic>(() => argResults['file-reporter']).thenReturn(null);
when<dynamic>(
() => argResults['collect-coverage-from'],
).thenReturn('imports');
Expand Down Expand Up @@ -600,6 +602,29 @@ void main() {
),
).called(1);
});

test(
'completes normally --file-reporter json:test-report.json',
() async {
when<dynamic>(
() => 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', () {
Expand Down
Loading