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
5 changes: 3 additions & 2 deletions doc/mcp.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,12 +137,13 @@ Runs tests in a Dart or Flutter project.
"platform": "chrome | vm | android | ios",
"dart-define": "foo=bar",
"dart-define-from-file": "config.json",
"test_randomize_ordering_seed": "random"
"test_randomize_ordering_seed": "random",
"timeout_seconds": 120
}
}
```

All parameters are optional. When `optimization` is not specified, `--no-optimization` is applied by default.
All parameters are optional. When `optimization` is not specified, `--no-optimization` is applied by default. When `timeout_seconds` is not specified, no timeout is applied.

### `packages_get`

Expand Down
20 changes: 20 additions & 0 deletions lib/src/commands/test/test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ class FlutterTestOptions {
required this.reportOn,
required this.runSkipped,
required this.flavor,
required this.timeout,
required this.rest,
});

Expand Down Expand Up @@ -68,6 +69,12 @@ 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 timeout = timeoutSeconds != null
? Duration(seconds: timeoutSeconds)
: null;
final rest = argResults.rest;

return FlutterTestOptions._(
Expand All @@ -90,6 +97,7 @@ class FlutterTestOptions {
reportOn: reportOn,
runSkipped: runSkipped,
flavor: flavor,
timeout: timeout,
rest: rest,
);
}
Expand Down Expand Up @@ -153,6 +161,9 @@ class FlutterTestOptions {
/// The flavor to build for testing.
final String? flavor;

/// Maximum time to let tests run before killing the process.
final Duration? timeout;

/// The remaining arguments passed to the test command.
final List<String> rest;
}
Expand Down Expand Up @@ -331,6 +342,13 @@ class TestCommand extends Command<int> {
'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.',
)
..addOption(
'timeout',
help:
'Maximum seconds to let tests run before killing the process. '
'Useful when tests hang due to an unbounded pumpAndSettle() call.',
valueHelp: 'seconds',
);
}

Expand Down Expand Up @@ -409,6 +427,8 @@ This command should be run from the root of your Flutter project.''');
'--dart-define-from-file=$value',
if (options.platform == null) ...['-j', options.concurrency],
'--no-pub',
if (options.timeout != null)
'--timeout=${options.timeout!.inSeconds}s',
...options.rest,
],
);
Expand Down
13 changes: 13 additions & 0 deletions lib/src/mcp/mcp_server.dart
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,13 @@ Only one value can be selected.
'(e.g. // coverage:ignore-line). '
'Only applies to Dart tests (dart: true).',
),
'timeout_seconds': IntegerSchema(
description:
'Maximum seconds to wait for the test run before killing '
'the Flutter test process. Flutter tests can hang '
'indefinitely when pumpAndSettle() is called without a '
'timeout argument. When omitted, no timeout is applied.',
),
},
),
),
Expand Down Expand Up @@ -384,6 +391,12 @@ Only one value can be selected.
if (args['check_ignore'] == true) {
cliArgs.add('--check-ignore');
}
if (args['timeout_seconds'] != null) {
cliArgs.addAll([
'--timeout',
(args['timeout_seconds']! as num).toInt().toString(),
]);
}

return cliArgs;
}
Expand Down
2 changes: 2 additions & 0 deletions site/docs/commands/test.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ very_good test [arguments]
--run-skipped Run skipped tests instead of skipping them.
--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.
--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.

Run "very_good help" to see global options.
```
Expand Down
17 changes: 17 additions & 0 deletions test/src/commands/test/test_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ const expectedTestUsage = [
' --report-on=<lib/> Optional file paths to report coverage information to. This should be paths relative to the current working directory. Can be passed multiple times.\n'
' --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'
'\n'
'Run "very_good help" to see global options.',
];
Expand Down Expand Up @@ -133,6 +134,7 @@ void main() {
when<dynamic>(() => argResults['report-on']).thenReturn(<String>[]);
when<dynamic>(() => argResults['run-skipped']).thenReturn(false);
when<dynamic>(() => argResults['flavor']).thenReturn(null);
when<dynamic>(() => argResults['timeout']).thenReturn(null);
when<dynamic>(
() => argResults['collect-coverage-from'],
).thenReturn('imports');
Expand Down Expand Up @@ -583,6 +585,21 @@ void main() {
),
).called(1);
});

test('completes normally --timeout 30', () async {
when<dynamic>(() => argResults['timeout']).thenReturn('30');
final result = await testCommand.run();
expect(result, equals(ExitCode.success.code));
verify(
() => flutterTest(
optimizePerformance: true,
arguments: [...defaultArguments, '--timeout=30s'],
logger: logger,
stdout: logger.write,
stderr: logger.err,
),
).called(1);
});
});

group('coverage', () {
Expand Down
20 changes: 20 additions & 0 deletions test/src/mcp/mcp_server_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,7 @@ void main() {
'platform': 'chrome',
'run_skipped': true,
'check_ignore': true,
'timeout_seconds': 60,
},
),
),
Expand Down Expand Up @@ -367,6 +368,8 @@ void main() {
'chrome',
'--run-skipped',
'--check-ignore',
'--timeout',
'60',
]);
});

Expand Down Expand Up @@ -420,6 +423,23 @@ void main() {
contains('"test" failed with exit code'),
);
});

test('passes --timeout when timeout_seconds is provided', () async {
await sendRequest(
CallToolRequest.methodName,
_params(
CallToolRequest(
name: 'test',
arguments: {'timeout_seconds': 120},
),
),
);

final capturedArgs =
verify(() => mockCommandRunner.run(captureAny())).captured.first
as List<String>;
expect(capturedArgs, ['test', '--timeout', '120']);
});
});

group('Tool: packages_get', () {
Expand Down
Loading