Skip to content
Open
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: 4 additions & 1 deletion doc/mcp.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,9 +123,10 @@ Runs tests in a Dart or Flutter project.
"tool": "test",
"arguments": {
"directory": "./my_app",
"paths": ["test/src/foo_test.dart", "test/widgets"],
"dart": false,
"coverage": true,
"recursive": true,
"recursive": false,
"optimization": true,
"concurrency": "4",
"min_coverage": "100",
Expand All @@ -145,6 +146,8 @@ Runs tests in a Dart or Flutter project.

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.

`directory` selects the package to test; `paths` selects test files or directories *within* that package. To run another package's tests, point `directory` at it — putting a package path in `paths` runs the wrong thing, or nothing at all. Passing `paths` disables the test optimization step, and it cannot be combined with `recursive`, since each package in a recursive run gets the same relative path and only one of them can resolve it.

### `packages_get`

Installs or updates Dart/Flutter package dependencies.
Expand Down
13 changes: 9 additions & 4 deletions lib/src/cli/test_cli_runner.dart
Original file line number Diff line number Diff line change
Expand Up @@ -62,14 +62,19 @@ class MinCoverageNotMet implements Exception {
class TestCLIRunner {
/// Determines whether the user is targetting test files or not.
///
/// The user can only target test files by using the `--` option terminator.
/// The additional options after the `--` are passed to the test runner which
/// allows the user to target specific test files or directories.
/// [rest] holds the positional arguments left over after option parsing.
/// Trailing options are allowed, so bare targets work
/// (`very_good test test/foo_test.dart`); the `--` option terminator is only
/// needed for a target that begins with `-`, which would otherwise be read
/// as an option. Either way the parser strips the `--` out of [rest], so
/// what arrives here is the targets alone.
///
/// The heuristics used to determine if the user is not targetting test files
/// are:
/// * No [rest] arguments are passed.
/// * All [rest] arguments are options (i.e. they do not start with `-`).
/// * Every [rest] argument is an option (i.e. it starts with `-`), which
/// only happens when the options were passed through a `--` for the
/// underlying test runner.
///
/// See also:
/// * [What does -- mean in Shell?](https://www.cyberciti.biz/faq/what-does-double-dash-mean-in-ssh-command/)
Expand Down
9 changes: 9 additions & 0 deletions lib/src/commands/dart/commands/dart_test_command.dart
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,15 @@ class DartTestCommand extends Command<int> {
final pubspec = File(path.join(targetPath, 'pubspec.yaml'));
final recursive = _argResults['recursive'] as bool;

if (recursive && TestCLIRunner.isTargettingTestFiles(_argResults.rest)) {
_logger.err('''
Cannot target specific test files together with --recursive.
Test targets are resolved against a single package root, so the same path
cannot apply to every package. Drop --recursive and run from the package
that contains them.''');
return ExitCode.usage.code;
}

if (!recursive && !pubspec.existsSync()) {
_logger.err('''
Could not find a pubspec.yaml in $targetPath.
Expand Down
9 changes: 9 additions & 0 deletions lib/src/commands/test/test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,15 @@ class TestCommand extends Command<int> {
final pubspec = File(path.join(targetPath, 'pubspec.yaml'));
final recursive = _argResults['recursive'] as bool;

if (recursive && TestCLIRunner.isTargettingTestFiles(_argResults.rest)) {
_logger.err('''
Cannot target specific test files together with --recursive.
Test targets are resolved against a single package root, so the same path
cannot apply to every package. Drop --recursive and run from the package
that contains them.''');
return ExitCode.usage.code;
}

if (!recursive && !pubspec.existsSync()) {
_logger.err('''
Could not find a pubspec.yaml in $targetPath.
Expand Down
33 changes: 30 additions & 3 deletions lib/src/mcp/mcp_server.dart
Original file line number Diff line number Diff line change
Expand Up @@ -170,8 +170,21 @@ If is omitted, then core will be selected.
properties: {
'directory': StringSchema(
description:
'Target directory path (defaults to current directory). '
'Can be absolute or relative path to project root.',
'The package to test (defaults to current directory). '
'Can be absolute or relative path to project root. '
'A package path belongs here, not in "paths".',
),
'paths': ListSchema(
description:
'Test files or directories to run, relative to the package '
'selected by "directory" (e.g. '
"['test/src/foo_test.dart', 'test/widgets']). These are "
'targets inside that one package: to test a different '
'package, set "directory" to it rather than putting its '
'path here. When omitted, the whole suite runs. Cannot be '
'combined with "recursive". Note that targeting specific '
'paths disables the test optimization step.',
items: StringSchema(),
),
'dart': BooleanSchema(
description: '''Whether to run Dart tests. If not specified, Flutter tests will be run if a Flutter project is detected.''',
Expand All @@ -180,7 +193,11 @@ If is omitted, then core will be selected.
description: 'Whether to collect coverage information.',
),
'recursive': BooleanSchema(
description: 'Run tests recursively for all nested packages.',
description:
'Run tests recursively for all nested packages. '
'Cannot be combined with "paths", because each package runs '
'in its own working directory and a path is only meaningful '
'within one of them.',
),
'optimization': BooleanSchema(
description: '''
Expand Down Expand Up @@ -448,6 +465,16 @@ Only one value can be selected.
]);
}

// Positional test targets go last, after every option, so that they are
// parsed as `rest` rather than as a value for the preceding option. The
// `--` terminator keeps a target that begins with `-` from being read as
// an option; the parser strips it back out of `rest`, so the test command
// sees the paths and nothing else.
final paths = args['paths'] as List<Object?>?;
if (paths != null && paths.isNotEmpty) {
cliArgs.addAll(['--', ...paths.cast<String>()]);
}

return cliArgs;
}

Expand Down
37 changes: 37 additions & 0 deletions test/src/commands/dart/commands/dart_test_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -760,6 +760,43 @@ void main() {
},
);

test(
'exits with usage code when targeting test files with --recursive',
() async {
when<dynamic>(() => argResults['recursive']).thenReturn(true);
when(() => argResults.rest).thenReturn(['test/my_test.dart']);

final result = await testCommand.run();

expect(result, equals(ExitCode.usage.code));
verify(
() => logger.err(any(that: contains('--recursive'))),
).called(1);
},
);

test(
'allows --recursive when rest arguements are all options',
() async {
when<dynamic>(() => argResults['recursive']).thenReturn(true);
when(() => argResults.rest).thenReturn(['--track-wdiget-creation']);

final result = await testCommand.run();

expect(result, equals(ExitCode.success.code));
verify(
() => dartTest(
recursive: true,
optimizePerformance: true,
arguments: [...defaultArguments, '--track-wdiget-creation'],
logger: logger,
stdout: logger.write,
stderr: logger.err,
),
).called(1);
},
);

test(
'enables optimizePerformance when rest arguement is an option',
() async {
Expand Down
37 changes: 37 additions & 0 deletions test/src/commands/test/test_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -539,6 +539,43 @@ void main() {
},
);

test(
'exits with usage code when targeting test files with --recursive',
() async {
when<dynamic>(() => argResults['recursive']).thenReturn(true);
when(() => argResults.rest).thenReturn(['test/my_test.dart']);

final result = await testCommand.run();

expect(result, equals(ExitCode.usage.code));
verify(
() => logger.err(any(that: contains('--recursive'))),
).called(1);
},
);

test(
'allows --recursive when rest arguements are all options',
() async {
when<dynamic>(() => argResults['recursive']).thenReturn(true);
when(() => argResults.rest).thenReturn(['--track-wdiget-creation']);

final result = await testCommand.run();

expect(result, equals(ExitCode.success.code));
verify(
() => flutterTest(
recursive: true,
optimizePerformance: true,
arguments: [...defaultArguments, '--track-wdiget-creation'],
logger: logger,
stdout: logger.write,
stderr: logger.err,
),
).called(1);
},
);

test(
'enables optimizePerformance when rest arguement is an option',
() async {
Expand Down
82 changes: 82 additions & 0 deletions test/src/mcp/mcp_server_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -502,6 +502,88 @@ void main() {
as List<String>;
expect(capturedArgs, equals(['test', '--timeout', '120']));
});

test('passes paths as positional test targets behind `--`', () async {
await sendRequest(
CallToolRequest.methodName,
_params(
CallToolRequest(
name: 'test',
arguments: {
'paths': ['test/src/foo_test.dart', 'test/widgets'],
},
),
),
);

final capturedArgs =
verify(() => mockCommandRunner.run(captureAny())).captured.first
as List<String>;
expect(
capturedArgs,
equals(['test', '--', 'test/src/foo_test.dart', 'test/widgets']),
);
});

test('passes paths after options so they are parsed as rest', () async {
await sendRequest(
CallToolRequest.methodName,
_params(
CallToolRequest(
name: 'test',
arguments: {
'dart': true,
'concurrency': '8',
'paths': ['test/src/foo_test.dart'],
},
),
),
);

final capturedArgs =
verify(() => mockCommandRunner.run(captureAny())).captured.first
as List<String>;
expect(
capturedArgs,
equals(['dart', 'test', '-j', '8', '--', 'test/src/foo_test.dart']),
);
});

test('passes a path beginning with `-` through unparsed', () async {
await sendRequest(
CallToolRequest.methodName,
_params(
CallToolRequest(
name: 'test',
arguments: {
'paths': ['-weird_test.dart'],
},
),
),
);

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

test('adds no positional targets when paths is empty', () async {
await sendRequest(
CallToolRequest.methodName,
_params(
CallToolRequest(
name: 'test',
arguments: {'paths': <String>[]},
),
),
);

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

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