[tool] Add initial file-based command skipping (#8928)

Adds initial file-based filtering. This does not attempt to be comprehensive, just to get some low-hanging fruit, and to create a blueprint for anyone to follow in the future when adding more filtering. I expect that once this is in place, what will happen is that as we notice cases where PRs are hitting slow or flaky tests that they clearly don't need to, we can incrementally improve the filtering on demand.

Fixes https://github.com/flutter/flutter/issues/136394
This commit is contained in:
stuartmorgan-g
2025-04-18 07:19:25 -07:00
committed by GitHub
parent 4988af58c1
commit fdc1ec7c1c
19 changed files with 792 additions and 28 deletions

View File

@ -6,6 +6,7 @@ import 'dart:io' as io;
import 'package:file/file.dart'; import 'package:file/file.dart';
import 'common/file_filters.dart';
import 'common/output_utils.dart'; import 'common/output_utils.dart';
import 'common/package_looping_command.dart'; import 'common/package_looping_command.dart';
import 'common/repository_package.dart'; import 'common/repository_package.dart';
@ -92,6 +93,13 @@ class AnalyzeCommand extends PackageLoopingCommand {
return false; return false;
} }
@override
bool shouldIgnoreFile(String path) {
return isRepoLevelNonCodeImpactingFile(path) ||
isNativeCodeFile(path) ||
isPackageSupportFile(path);
}
@override @override
Future<void> initializeRun() async { Future<void> initializeRun() async {
_allowedCustomAnalysisDirectories = getYamlListArg(_customAnalysisFlag); _allowedCustomAnalysisDirectories = getYamlListArg(_customAnalysisFlag);

View File

@ -6,6 +6,7 @@ import 'package:file/file.dart';
import 'package:yaml/yaml.dart'; import 'package:yaml/yaml.dart';
import 'common/core.dart'; import 'common/core.dart';
import 'common/file_filters.dart';
import 'common/output_utils.dart'; import 'common/output_utils.dart';
import 'common/package_looping_command.dart'; import 'common/package_looping_command.dart';
import 'common/plugin_utils.dart'; import 'common/plugin_utils.dart';
@ -134,6 +135,11 @@ class BuildExamplesCommand extends PackageLoopingCommand {
return getNullableBoolArg(_swiftPackageManagerFlag); return getNullableBoolArg(_swiftPackageManagerFlag);
} }
@override
bool shouldIgnoreFile(String path) {
return isRepoLevelNonCodeImpactingFile(path) || isPackageSupportFile(path);
}
@override @override
Future<void> initializeRun() async { Future<void> initializeRun() async {
final List<String> platformFlags = _platforms.keys.toList(); final List<String> platformFlags = _platforms.keys.toList();

View File

@ -0,0 +1,53 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/// Returns true for repository-level paths of files that do not affect *any*
/// code-related commands (example builds, Dart analysis, native code analysis,
/// native tests, Dart tests, etc.) for use in command-ignored-files lists for
/// commands that are only affected by package code.
bool isRepoLevelNonCodeImpactingFile(String path) {
return <String>[
'AUTHORS',
'CODEOWNERS',
'CONTRIBUTING.md',
'LICENSE',
'README.md',
// This deliberate lists specific files rather than excluding the whole
// .github directory since it's better to have false negatives than to
// accidentally skip tests if something is later added to the directory
// that could affect packages.
'.github/PULL_REQUEST_TEMPLATE.md',
'.github/dependabot.yml',
'.github/labeler.yml',
'.github/post_merge_labeler.yml',
'.github/workflows/pull_request_label.yml',
].contains(path);
}
/// Returns true for native (non-Dart) code files, for use in command-ignored-
/// files lists for commands that aren't affected by native code (e.g., Dart
/// analysis and unit tests).
bool isNativeCodeFile(String path) {
return path.endsWith('.c') ||
path.endsWith('.cc') ||
path.endsWith('.cpp') ||
path.endsWith('.h') ||
path.endsWith('.m') ||
path.endsWith('.swift') ||
path.endsWith('.java') ||
path.endsWith('.kt');
}
/// Returns true for package-level human-focused support files, for use in
/// command-ignored-files lists for commands that aren't affected by files that
/// aren't used in any builds.
///
/// This must *not* include metadata files that do affect builds, such as
/// pubspec.yaml.
bool isPackageSupportFile(String path) {
return path.endsWith('/AUTHORS') ||
path.endsWith('/CHANGELOG.md') ||
path.endsWith('/CONTRIBUTING.md') ||
path.endsWith('/README.md');
}

View File

@ -109,6 +109,20 @@ abstract class PackageLoopingCommand extends PackageCommand {
/// The package currently being run by [runForPackage]. /// The package currently being run by [runForPackage].
PackageEnumerationEntry? _currentPackageEntry; PackageEnumerationEntry? _currentPackageEntry;
/// When running against a merge base, this is called before [initializeRun]
/// for every changed file, to see if that file is a file that is guaranteed
/// *not* to require running this command.
///
/// If every changed file returns true, then the command will be skipped.
/// Because this causes tests not to run, subclasses should be very
/// consevative about what returns true; for anything borderline it is much
/// better to err on the side of running tests unnecessarily than to risk
/// losing test coverage.
///
/// [path] is a POSIX-style path regardless of the host platforrm, and is
/// relative to the git repo root.
bool shouldIgnoreFile(String path) => false;
/// Called during [run] before any calls to [runForPackage]. This provides an /// Called during [run] before any calls to [runForPackage]. This provides an
/// opportunity to fail early if the command can't be run (e.g., because the /// opportunity to fail early if the command can't be run (e.g., because the
/// arguments are invalid), and to set up any run-level state. /// arguments are invalid), and to set up any run-level state.
@ -281,6 +295,14 @@ abstract class PackageLoopingCommand extends PackageCommand {
baseSha = await gitVersionFinder.getBaseSha(); baseSha = await gitVersionFinder.getBaseSha();
changedFiles = await gitVersionFinder.getChangedFiles(); changedFiles = await gitVersionFinder.getChangedFiles();
// Check whether the command needs to run.
if (changedFiles.isNotEmpty && changedFiles.every(shouldIgnoreFile)) {
_printColorized(
'SKIPPING ALL PACKAGES: No changed files affect this command',
Styles.DARK_GRAY);
return true;
}
await initializeRun(); await initializeRun();
final List<PackageEnumerationEntry> targetPackages = final List<PackageEnumerationEntry> targetPackages =

View File

@ -5,6 +5,7 @@
import 'package:file/file.dart'; import 'package:file/file.dart';
import 'common/core.dart'; import 'common/core.dart';
import 'common/file_filters.dart';
import 'common/output_utils.dart'; import 'common/output_utils.dart';
import 'common/package_looping_command.dart'; import 'common/package_looping_command.dart';
import 'common/plugin_utils.dart'; import 'common/plugin_utils.dart';
@ -65,6 +66,13 @@ class DartTestCommand extends PackageLoopingCommand {
PackageLoopingType get packageLoopingType => PackageLoopingType get packageLoopingType =>
PackageLoopingType.includeAllSubpackages; PackageLoopingType.includeAllSubpackages;
@override
bool shouldIgnoreFile(String path) {
return isRepoLevelNonCodeImpactingFile(path) ||
isNativeCodeFile(path) ||
isPackageSupportFile(path);
}
@override @override
Future<PackageResult> runForPackage(RepositoryPackage package) async { Future<PackageResult> runForPackage(RepositoryPackage package) async {
if (!package.testDirectory.existsSync()) { if (!package.testDirectory.existsSync()) {

View File

@ -9,6 +9,7 @@ import 'dart:io';
import 'package:file/file.dart'; import 'package:file/file.dart';
import 'common/core.dart'; import 'common/core.dart';
import 'common/file_filters.dart';
import 'common/output_utils.dart'; import 'common/output_utils.dart';
import 'common/package_looping_command.dart'; import 'common/package_looping_command.dart';
import 'common/plugin_utils.dart'; import 'common/plugin_utils.dart';
@ -68,6 +69,11 @@ class DriveExamplesCommand extends PackageLoopingCommand {
Map<String, List<String>> _targetDeviceFlags = const <String, List<String>>{}; Map<String, List<String>> _targetDeviceFlags = const <String, List<String>>{};
@override
bool shouldIgnoreFile(String path) {
return isRepoLevelNonCodeImpactingFile(path) || isPackageSupportFile(path);
}
@override @override
Future<void> initializeRun() async { Future<void> initializeRun() async {
final List<String> platformSwitches = <String>[ final List<String> platformSwitches = <String>[

View File

@ -8,6 +8,7 @@ import 'package:file/file.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
import 'common/core.dart'; import 'common/core.dart';
import 'common/file_filters.dart';
import 'common/flutter_command_utils.dart'; import 'common/flutter_command_utils.dart';
import 'common/gradle.dart'; import 'common/gradle.dart';
import 'common/output_utils.dart'; import 'common/output_utils.dart';
@ -122,6 +123,11 @@ class FirebaseTestLabCommand extends PackageLoopingCommand {
_firebaseProjectConfigured = true; _firebaseProjectConfigured = true;
} }
@override
bool shouldIgnoreFile(String path) {
return isRepoLevelNonCodeImpactingFile(path) || isPackageSupportFile(path);
}
@override @override
Future<PackageResult> runForPackage(RepositoryPackage package) async { Future<PackageResult> runForPackage(RepositoryPackage package) async {
final List<PackageResult> results = <PackageResult>[]; final List<PackageResult> results = <PackageResult>[];

View File

@ -3,6 +3,7 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'common/core.dart'; import 'common/core.dart';
import 'common/file_filters.dart';
import 'common/flutter_command_utils.dart'; import 'common/flutter_command_utils.dart';
import 'common/gradle.dart'; import 'common/gradle.dart';
import 'common/output_utils.dart'; import 'common/output_utils.dart';
@ -29,6 +30,15 @@ class LintAndroidCommand extends PackageLoopingCommand {
final String description = 'Runs "gradlew lint" on Android plugins.\n\n' final String description = 'Runs "gradlew lint" on Android plugins.\n\n'
'Requires the examples to have been build at least once before running.'; 'Requires the examples to have been build at least once before running.';
@override
bool shouldIgnoreFile(String path) {
return isRepoLevelNonCodeImpactingFile(path) ||
isPackageSupportFile(path) ||
// These are part of the build, but don't affect native code analysis.
path.endsWith('/pubspec.yaml') ||
path.endsWith('.dart');
}
@override @override
Future<PackageResult> runForPackage(RepositoryPackage package) async { Future<PackageResult> runForPackage(RepositoryPackage package) async {
if (!pluginSupportsPlatform(platformAndroid, package, if (!pluginSupportsPlatform(platformAndroid, package,

View File

@ -9,6 +9,7 @@ import 'package:meta/meta.dart';
import 'common/cmake.dart'; import 'common/cmake.dart';
import 'common/core.dart'; import 'common/core.dart';
import 'common/file_filters.dart';
import 'common/flutter_command_utils.dart'; import 'common/flutter_command_utils.dart';
import 'common/gradle.dart'; import 'common/gradle.dart';
import 'common/output_utils.dart'; import 'common/output_utils.dart';
@ -114,6 +115,13 @@ this command.
Set<String> _xcodeWarningsExceptions = <String>{}; Set<String> _xcodeWarningsExceptions = <String>{};
@override
bool shouldIgnoreFile(String path) {
return isRepoLevelNonCodeImpactingFile(path) || isPackageSupportFile(path);
// It may seem tempting to filter out *.dart, but that would skip critical
// testing since native integration tests run the full compiled application.
}
@override @override
Future<void> initializeRun() async { Future<void> initializeRun() async {
_platforms = <String, _PlatformDetails>{ _platforms = <String, _PlatformDetails>{

View File

@ -3,6 +3,7 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'common/core.dart'; import 'common/core.dart';
import 'common/file_filters.dart';
import 'common/flutter_command_utils.dart'; import 'common/flutter_command_utils.dart';
import 'common/output_utils.dart'; import 'common/output_utils.dart';
import 'common/package_looping_command.dart'; import 'common/package_looping_command.dart';
@ -47,6 +48,15 @@ class XcodeAnalyzeCommand extends PackageLoopingCommand {
final String description = final String description =
'Runs Xcode analysis on the iOS and/or macOS example apps.'; 'Runs Xcode analysis on the iOS and/or macOS example apps.';
@override
bool shouldIgnoreFile(String path) {
return isRepoLevelNonCodeImpactingFile(path) ||
isPackageSupportFile(path) ||
// These are part of the build, but don't affect native code analysis.
path.endsWith('/pubspec.yaml') ||
path.endsWith('.dart');
}
@override @override
Future<void> initializeRun() async { Future<void> initializeRun() async {
if (!(getBoolArg(platformIOS) || getBoolArg(platformMacOS))) { if (!(getBoolArg(platformIOS) || getBoolArg(platformMacOS))) {

View File

@ -16,12 +16,13 @@ void main() {
late MockPlatform mockPlatform; late MockPlatform mockPlatform;
late Directory packagesDir; late Directory packagesDir;
late RecordingProcessRunner processRunner; late RecordingProcessRunner processRunner;
late RecordingProcessRunner gitProcessRunner;
late CommandRunner<void> runner; late CommandRunner<void> runner;
setUp(() { setUp(() {
mockPlatform = MockPlatform(); mockPlatform = MockPlatform();
final GitDir gitDir; final GitDir gitDir;
(:packagesDir, :processRunner, gitProcessRunner: _, :gitDir) = (:packagesDir, :processRunner, :gitProcessRunner, :gitDir) =
configureBaseCommandMocks(platform: mockPlatform); configureBaseCommandMocks(platform: mockPlatform);
final AnalyzeCommand analyzeCommand = AnalyzeCommand( final AnalyzeCommand analyzeCommand = AnalyzeCommand(
packagesDir, packagesDir,
@ -470,4 +471,90 @@ void main() {
]), ]),
); );
}); });
group('file filtering', () {
test('runs command for changes to Dart source', () async {
createFakePackage('package_a', packagesDir);
gitProcessRunner.mockProcessesForExecutable['git-diff'] =
<FakeProcessInfo>[
FakeProcessInfo(MockProcess(stdout: '''
packages/package_a/foo.dart
''')),
];
final List<String> output =
await runCapturingPrint(runner, <String>['analyze']);
expect(
output,
containsAllInOrder(<Matcher>[
contains('Running for package_a'),
]));
});
const List<String> files = <String>[
'foo.java',
'foo.kt',
'foo.m',
'foo.swift',
'foo.c',
'foo.cc',
'foo.cpp',
'foo.h',
];
for (final String file in files) {
test('skips command for changes to non-Dart source $file', () async {
createFakePackage('package_a', packagesDir);
gitProcessRunner.mockProcessesForExecutable['git-diff'] =
<FakeProcessInfo>[
FakeProcessInfo(MockProcess(stdout: '''
packages/package_a/$file
''')),
];
final List<String> output =
await runCapturingPrint(runner, <String>['analyze']);
expect(
output,
isNot(containsAllInOrder(<Matcher>[
contains('Running for package_a'),
])));
expect(
output,
containsAllInOrder(<Matcher>[
contains('SKIPPING ALL PACKAGES'),
]));
});
}
test('skips commands if all files should be ignored', () async {
createFakePackage('package_a', packagesDir);
gitProcessRunner.mockProcessesForExecutable['git-diff'] =
<FakeProcessInfo>[
FakeProcessInfo(MockProcess(stdout: '''
README.md
CODEOWNERS
packages/package_a/CHANGELOG.md
''')),
];
final List<String> output =
await runCapturingPrint(runner, <String>['analyze']);
expect(
output,
isNot(containsAllInOrder(<Matcher>[
contains('Running for package_a'),
])));
expect(
output,
containsAllInOrder(<Matcher>[
contains('SKIPPING ALL PACKAGES'),
]));
});
});
} }

View File

@ -19,11 +19,12 @@ void main() {
late Directory packagesDir; late Directory packagesDir;
late CommandRunner<void> runner; late CommandRunner<void> runner;
late RecordingProcessRunner processRunner; late RecordingProcessRunner processRunner;
late RecordingProcessRunner gitProcessRunner;
setUp(() { setUp(() {
mockPlatform = MockPlatform(); mockPlatform = MockPlatform();
final GitDir gitDir; final GitDir gitDir;
(:packagesDir, :processRunner, gitProcessRunner: _, :gitDir) = (:packagesDir, :processRunner, :gitProcessRunner, :gitDir) =
configureBaseCommandMocks(platform: mockPlatform); configureBaseCommandMocks(platform: mockPlatform);
final BuildExamplesCommand command = BuildExamplesCommand( final BuildExamplesCommand command = BuildExamplesCommand(
packagesDir, packagesDir,
@ -998,5 +999,72 @@ void main() {
pluginExampleDirectory.path), pluginExampleDirectory.path),
])); ]));
}); });
group('file filtering', () {
const List<String> files = <String>[
'pubspec.yaml',
'foo.dart',
'foo.java',
'foo.kt',
'foo.m',
'foo.swift',
'foo.cc',
'foo.cpp',
'foo.h',
];
for (final String file in files) {
test('runs command for changes to $file', () async {
createFakePackage('package_a', packagesDir);
gitProcessRunner.mockProcessesForExecutable['git-diff'] =
<FakeProcessInfo>[
FakeProcessInfo(MockProcess(stdout: '''
packages/package_a/$file
''')),
];
// The target platform is irrelevant here; because this repo's
// packages are fully federated, there's no need to distinguish
// the ignore list by target (e.g., skipping iOS tests if only Java or
// Kotlin files change), because package-level filering will already
// accomplish the same goal.
final List<String> output = await runCapturingPrint(
runner, <String>['build-examples', '--web']);
expect(
output,
containsAllInOrder(<Matcher>[
contains('Running for package_a'),
]));
});
}
test('skips commands if all files should be ignored', () async {
createFakePackage('package_a', packagesDir);
gitProcessRunner.mockProcessesForExecutable['git-diff'] =
<FakeProcessInfo>[
FakeProcessInfo(MockProcess(stdout: '''
README.md
CODEOWNERS
packages/package_a/CHANGELOG.md
''')),
];
final List<String> output =
await runCapturingPrint(runner, <String>['build-examples']);
expect(
output,
isNot(containsAllInOrder(<Matcher>[
contains('Running for package_a'),
])));
expect(
output,
containsAllInOrder(<Matcher>[
contains('SKIPPING ALL PACKAGES'),
]));
});
});
}); });
} }

View File

@ -10,12 +10,11 @@ import 'package:file/file.dart';
import 'package:flutter_plugin_tools/src/common/core.dart'; import 'package:flutter_plugin_tools/src/common/core.dart';
import 'package:flutter_plugin_tools/src/common/output_utils.dart'; import 'package:flutter_plugin_tools/src/common/output_utils.dart';
import 'package:flutter_plugin_tools/src/common/package_looping_command.dart'; import 'package:flutter_plugin_tools/src/common/package_looping_command.dart';
import 'package:mockito/mockito.dart'; import 'package:git/git.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
import '../mocks.dart'; import '../mocks.dart';
import '../util.dart'; import '../util.dart';
import 'package_command_test.mocks.dart';
// Constants for colorized output start and end. // Constants for colorized output start and end.
const String _startElapsedTimeColor = '\x1B[90m'; const String _startElapsedTimeColor = '\x1B[90m';
@ -79,10 +78,12 @@ void main() {
late MockPlatform mockPlatform; late MockPlatform mockPlatform;
late Directory packagesDir; late Directory packagesDir;
late Directory thirdPartyPackagesDir; late Directory thirdPartyPackagesDir;
late GitDir gitDir;
late RecordingProcessRunner gitProcessRunner;
setUp(() { setUp(() {
mockPlatform = MockPlatform(); mockPlatform = MockPlatform();
(:packagesDir, processRunner: _, gitProcessRunner: _, gitDir: _) = (:packagesDir, processRunner: _, :gitProcessRunner, :gitDir) =
configureBaseCommandMocks(platform: mockPlatform); configureBaseCommandMocks(platform: mockPlatform);
// Correct color handling is part of the behavior being tested here. // Correct color handling is part of the behavior being tested here.
useColorForOutput = true; useColorForOutput = true;
@ -96,10 +97,8 @@ void main() {
useColorForOutput = io.stdout.supportsAnsiEscapes; useColorForOutput = io.stdout.supportsAnsiEscapes;
}); });
/// Creates a TestPackageLoopingCommand instance that uses [gitDiffResponse] /// Creates a TestPackageLoopingCommand with the given configuration.
/// for git diffs, and logs output to [printOutput].
TestPackageLoopingCommand createTestCommand({ TestPackageLoopingCommand createTestCommand({
String gitDiffResponse = '',
bool hasLongOutput = true, bool hasLongOutput = true,
PackageLoopingType packageLoopingType = PackageLoopingType.topLevelOnly, PackageLoopingType packageLoopingType = PackageLoopingType.topLevelOnly,
bool failsDuringInit = false, bool failsDuringInit = false,
@ -108,20 +107,6 @@ void main() {
String? customFailureListHeader, String? customFailureListHeader,
String? customFailureListFooter, String? customFailureListFooter,
}) { }) {
// Set up the git diff response.
final MockGitDir gitDir = MockGitDir();
when(gitDir.runCommand(any, throwOnError: anyNamed('throwOnError')))
.thenAnswer((Invocation invocation) {
final List<String> arguments =
invocation.positionalArguments[0]! as List<String>;
String? gitStdOut;
if (arguments[0] == 'diff') {
gitStdOut = gitDiffResponse;
}
return Future<io.ProcessResult>.value(
io.ProcessResult(0, 0, gitStdOut ?? '', ''));
});
return TestPackageLoopingCommand( return TestPackageLoopingCommand(
packagesDir, packagesDir,
platform: mockPlatform, platform: mockPlatform,
@ -212,6 +197,77 @@ void main() {
}); });
}); });
group('file filtering', () {
test('runs command if the changed files list is empty', () async {
createFakePackage('package_a', packagesDir);
gitProcessRunner.mockProcessesForExecutable['git-diff'] =
<FakeProcessInfo>[
FakeProcessInfo(MockProcess(stdout: '')),
];
final TestPackageLoopingCommand command =
createTestCommand(hasLongOutput: false);
final List<String> output = await runCommand(command);
expect(
output,
containsAllInOrder(<String>[
'${_startHeadingColor}Running for package_a...$_endColor',
]));
});
test('runs command if any files are not ignored', () async {
createFakePackage('package_a', packagesDir);
gitProcessRunner.mockProcessesForExecutable['git-diff'] =
<FakeProcessInfo>[
FakeProcessInfo(MockProcess(stdout: '''
skip/a
other
skip/b
''')),
];
final TestPackageLoopingCommand command =
createTestCommand(hasLongOutput: false);
final List<String> output = await runCommand(command);
expect(
output,
containsAllInOrder(<String>[
'${_startHeadingColor}Running for package_a...$_endColor',
]));
});
test('skips commands if all files should be ignored', () async {
createFakePackage('package_a', packagesDir);
gitProcessRunner.mockProcessesForExecutable['git-diff'] =
<FakeProcessInfo>[
FakeProcessInfo(MockProcess(stdout: '''
skip/a
skip/b
''')),
];
final TestPackageLoopingCommand command =
createTestCommand(hasLongOutput: false);
final List<String> output = await runCommand(command);
expect(
output,
isNot(containsAllInOrder(<Matcher>[
contains('Running for package_a'),
])));
expect(
output,
containsAllInOrder(<String>[
'${_startSkipColor}SKIPPING ALL PACKAGES: No changed files affect this command$_endColor',
]));
});
});
group('package iteration', () { group('package iteration', () {
test('includes plugins and packages', () async { test('includes plugins and packages', () async {
final RepositoryPackage plugin = final RepositoryPackage plugin =
@ -898,6 +954,11 @@ class TestPackageLoopingCommand extends PackageLoopingCommand {
@override @override
final String description = 'sample package looping command'; final String description = 'sample package looping command';
@override
bool shouldIgnoreFile(String path) {
return path.startsWith('skip/');
}
@override @override
Future<void> initializeRun() async { Future<void> initializeRun() async {
if (warnsDuringInit) { if (warnsDuringInit) {

View File

@ -20,11 +20,12 @@ void main() {
late Directory packagesDir; late Directory packagesDir;
late CommandRunner<void> runner; late CommandRunner<void> runner;
late RecordingProcessRunner processRunner; late RecordingProcessRunner processRunner;
late RecordingProcessRunner gitProcessRunner;
setUp(() { setUp(() {
mockPlatform = MockPlatform(); mockPlatform = MockPlatform();
final GitDir gitDir; final GitDir gitDir;
(:packagesDir, :processRunner, gitProcessRunner: _, :gitDir) = (:packagesDir, :processRunner, :gitProcessRunner, :gitDir) =
configureBaseCommandMocks(platform: mockPlatform); configureBaseCommandMocks(platform: mockPlatform);
final DartTestCommand command = DartTestCommand( final DartTestCommand command = DartTestCommand(
packagesDir, packagesDir,
@ -713,5 +714,91 @@ test_on: !vm && firefox
]), ]),
); );
}); });
group('file filtering', () {
test('runs command for changes to Dart source', () async {
createFakePackage('package_a', packagesDir);
gitProcessRunner.mockProcessesForExecutable['git-diff'] =
<FakeProcessInfo>[
FakeProcessInfo(MockProcess(stdout: '''
packages/package_a/foo.dart
''')),
];
final List<String> output =
await runCapturingPrint(runner, <String>['test']);
expect(
output,
containsAllInOrder(<Matcher>[
contains('Running for package_a'),
]));
});
const List<String> files = <String>[
'foo.java',
'foo.kt',
'foo.m',
'foo.swift',
'foo.c',
'foo.cc',
'foo.cpp',
'foo.h',
];
for (final String file in files) {
test('skips command for changes to non-Dart source $file', () async {
createFakePackage('package_a', packagesDir);
gitProcessRunner.mockProcessesForExecutable['git-diff'] =
<FakeProcessInfo>[
FakeProcessInfo(MockProcess(stdout: '''
packages/package_a/$file
''')),
];
final List<String> output =
await runCapturingPrint(runner, <String>['test']);
expect(
output,
isNot(containsAllInOrder(<Matcher>[
contains('Running for package_a'),
])));
expect(
output,
containsAllInOrder(<Matcher>[
contains('SKIPPING ALL PACKAGES'),
]));
});
}
test('skips commands if all files should be ignored', () async {
createFakePackage('package_a', packagesDir);
gitProcessRunner.mockProcessesForExecutable['git-diff'] =
<FakeProcessInfo>[
FakeProcessInfo(MockProcess(stdout: '''
README.md
CODEOWNERS
packages/package_a/CHANGELOG.md
''')),
];
final List<String> output =
await runCapturingPrint(runner, <String>['test']);
expect(
output,
isNot(containsAllInOrder(<Matcher>[
contains('Running for package_a'),
])));
expect(
output,
containsAllInOrder(<Matcher>[
contains('SKIPPING ALL PACKAGES'),
]));
});
});
}); });
} }

View File

@ -28,11 +28,12 @@ void main() {
late Directory packagesDir; late Directory packagesDir;
late CommandRunner<void> runner; late CommandRunner<void> runner;
late RecordingProcessRunner processRunner; late RecordingProcessRunner processRunner;
late RecordingProcessRunner gitProcessRunner;
setUp(() { setUp(() {
mockPlatform = MockPlatform(); mockPlatform = MockPlatform();
final GitDir gitDir; final GitDir gitDir;
(:packagesDir, :processRunner, gitProcessRunner: _, :gitDir) = (:packagesDir, :processRunner, :gitProcessRunner, :gitDir) =
configureBaseCommandMocks(platform: mockPlatform); configureBaseCommandMocks(platform: mockPlatform);
final DriveExamplesCommand command = DriveExamplesCommand( final DriveExamplesCommand command = DriveExamplesCommand(
packagesDir, packagesDir,
@ -1714,6 +1715,73 @@ void main() {
expect(processRunner.recordedCalls.isEmpty, true); expect(processRunner.recordedCalls.isEmpty, true);
}); });
}); });
group('file filtering', () {
const List<String> files = <String>[
'pubspec.yaml',
'foo.dart',
'foo.java',
'foo.kt',
'foo.m',
'foo.swift',
'foo.cc',
'foo.cpp',
'foo.h',
];
for (final String file in files) {
test('runs command for changes to $file', () async {
createFakePackage('package_a', packagesDir);
gitProcessRunner.mockProcessesForExecutable['git-diff'] =
<FakeProcessInfo>[
FakeProcessInfo(MockProcess(stdout: '''
packages/package_a/$file
''')),
];
// The target platform is irrelevant here; because this repo's
// packages are fully federated, there's no need to distinguish
// the ignore list by target (e.g., skipping iOS tests if only Java or
// Kotlin files change), because package-level filering will already
// accomplish the same goal.
final List<String> output = await runCapturingPrint(
runner, <String>['drive-examples', '--web']);
expect(
output,
containsAllInOrder(<Matcher>[
contains('Running for package_a'),
]));
});
}
test('skips commands if all files should be ignored', () async {
createFakePackage('package_a', packagesDir);
gitProcessRunner.mockProcessesForExecutable['git-diff'] =
<FakeProcessInfo>[
FakeProcessInfo(MockProcess(stdout: '''
README.md
CODEOWNERS
packages/package_a/CHANGELOG.md
''')),
];
final List<String> output =
await runCapturingPrint(runner, <String>['drive-examples']);
expect(
output,
isNot(containsAllInOrder(<Matcher>[
contains('Running for package_a'),
])));
expect(
output,
containsAllInOrder(<Matcher>[
contains('SKIPPING ALL PACKAGES'),
]));
});
});
}); });
} }

View File

@ -20,11 +20,12 @@ void main() {
late Directory packagesDir; late Directory packagesDir;
late CommandRunner<void> runner; late CommandRunner<void> runner;
late RecordingProcessRunner processRunner; late RecordingProcessRunner processRunner;
late RecordingProcessRunner gitProcessRunner;
setUp(() { setUp(() {
mockPlatform = MockPlatform(); mockPlatform = MockPlatform();
final GitDir gitDir; final GitDir gitDir;
(:packagesDir, :processRunner, gitProcessRunner: _, :gitDir) = (:packagesDir, :processRunner, :gitProcessRunner, :gitDir) =
configureBaseCommandMocks(platform: mockPlatform); configureBaseCommandMocks(platform: mockPlatform);
final FirebaseTestLabCommand command = FirebaseTestLabCommand( final FirebaseTestLabCommand command = FirebaseTestLabCommand(
packagesDir, packagesDir,
@ -881,5 +882,75 @@ class MainActivityTest {
]), ]),
); );
}); });
group('file filtering', () {
const List<String> files = <String>[
'pubspec.yaml',
'foo.dart',
'foo.java',
'foo.kt',
'foo.m',
'foo.swift',
'foo.cc',
'foo.cpp',
'foo.h',
];
for (final String file in files) {
test('runs command for changes to $file', () async {
createFakePackage('package_a', packagesDir);
gitProcessRunner.mockProcessesForExecutable['git-diff'] =
<FakeProcessInfo>[
FakeProcessInfo(MockProcess(stdout: '''
packages/package_a/$file
''')),
];
final List<String> output = await runCapturingPrint(runner, <String>[
'firebase-test-lab',
'--results-bucket=a_bucket',
'--device',
'model=redfin,version=30',
]);
expect(
output,
containsAllInOrder(<Matcher>[
contains('Running for package_a'),
]));
});
}
test('skips commands if all files should be ignored', () async {
createFakePackage('package_a', packagesDir);
gitProcessRunner.mockProcessesForExecutable['git-diff'] =
<FakeProcessInfo>[
FakeProcessInfo(MockProcess(stdout: '''
README.md
CODEOWNERS
packages/package_a/CHANGELOG.md
''')),
];
final List<String> output = await runCapturingPrint(runner, <String>[
'firebase-test-lab',
'--results-bucket=a_bucket',
'--device',
'model=redfin,version=30',
]);
expect(
output,
isNot(containsAllInOrder(<Matcher>[
contains('Running for package_a'),
])));
expect(
output,
containsAllInOrder(<Matcher>[
contains('SKIPPING ALL PACKAGES'),
]));
});
});
}); });
} }

View File

@ -19,11 +19,12 @@ void main() {
late CommandRunner<void> runner; late CommandRunner<void> runner;
late MockPlatform mockPlatform; late MockPlatform mockPlatform;
late RecordingProcessRunner processRunner; late RecordingProcessRunner processRunner;
late RecordingProcessRunner gitProcessRunner;
setUp(() { setUp(() {
mockPlatform = MockPlatform(); mockPlatform = MockPlatform();
final GitDir gitDir; final GitDir gitDir;
(:packagesDir, :processRunner, gitProcessRunner: _, :gitDir) = (:packagesDir, :processRunner, :gitProcessRunner, :gitDir) =
configureBaseCommandMocks(platform: mockPlatform); configureBaseCommandMocks(platform: mockPlatform);
final LintAndroidCommand command = LintAndroidCommand( final LintAndroidCommand command = LintAndroidCommand(
packagesDir, packagesDir,
@ -241,5 +242,61 @@ void main() {
], ],
)); ));
}); });
group('file filtering', () {
const List<String> files = <String>[
'foo.java',
'foo.kt',
];
for (final String file in files) {
test('runs command for changes to $file', () async {
createFakePackage('package_a', packagesDir);
gitProcessRunner.mockProcessesForExecutable['git-diff'] =
<FakeProcessInfo>[
FakeProcessInfo(MockProcess(stdout: '''
packages/package_a/$file
''')),
];
final List<String> output =
await runCapturingPrint(runner, <String>['lint-android']);
expect(
output,
containsAllInOrder(<Matcher>[
contains('Running for package_a'),
]));
});
}
test('skips commands if all files should be ignored', () async {
createFakePackage('package_a', packagesDir);
gitProcessRunner.mockProcessesForExecutable['git-diff'] =
<FakeProcessInfo>[
FakeProcessInfo(MockProcess(stdout: '''
README.md
CODEOWNERS
packages/package_a/CHANGELOG.md
packages/package_a/lib/foo.dart
''')),
];
final List<String> output =
await runCapturingPrint(runner, <String>['lint-android']);
expect(
output,
isNot(containsAllInOrder(<Matcher>[
contains('Running for package_a'),
])));
expect(
output,
containsAllInOrder(<Matcher>[
contains('SKIPPING ALL PACKAGES'),
]));
});
});
}); });
} }

View File

@ -84,6 +84,7 @@ void main() {
late Directory packagesDir; late Directory packagesDir;
late CommandRunner<void> runner; late CommandRunner<void> runner;
late RecordingProcessRunner processRunner; late RecordingProcessRunner processRunner;
late RecordingProcessRunner gitProcessRunner;
setUp(() { setUp(() {
// iOS and macOS tests expect macOS, Linux tests expect Linux; nothing // iOS and macOS tests expect macOS, Linux tests expect Linux; nothing
@ -91,7 +92,7 @@ void main() {
// allow them to share a setup group. // allow them to share a setup group.
mockPlatform = MockPlatform(isMacOS: true, isLinux: true); mockPlatform = MockPlatform(isMacOS: true, isLinux: true);
final GitDir gitDir; final GitDir gitDir;
(:packagesDir, :processRunner, gitProcessRunner: _, :gitDir) = (:packagesDir, :processRunner, :gitProcessRunner, :gitDir) =
configureBaseCommandMocks(platform: mockPlatform); configureBaseCommandMocks(platform: mockPlatform);
final NativeTestCommand command = NativeTestCommand( final NativeTestCommand command = NativeTestCommand(
packagesDir, packagesDir,
@ -379,6 +380,73 @@ void main() {
destination: 'id=$_simulatorDeviceId'), destination: 'id=$_simulatorDeviceId'),
])); ]));
}); });
group('file filtering', () {
const List<String> files = <String>[
'pubspec.yaml',
'foo.dart',
'foo.java',
'foo.kt',
'foo.m',
'foo.swift',
'foo.cc',
'foo.cpp',
'foo.h',
];
for (final String file in files) {
test('runs command for changes to $file', () async {
createFakePackage('package_a', packagesDir);
gitProcessRunner.mockProcessesForExecutable['git-diff'] =
<FakeProcessInfo>[
FakeProcessInfo(MockProcess(stdout: '''
packages/package_a/$file
''')),
];
// The target platform is irrelevant here; because this repo's
// packages are fully federated, there's no need to distinguish
// the ignore list by target (e.g., skipping iOS tests if only Java or
// Kotlin files change), because package-level filering will already
// accomplish the same goal.
final List<String> output = await runCapturingPrint(
runner, <String>['native-test', '--android']);
expect(
output,
containsAllInOrder(<Matcher>[
contains('Running for package_a'),
]));
});
}
test('skips commands if all files should be ignored', () async {
createFakePackage('package_a', packagesDir);
gitProcessRunner.mockProcessesForExecutable['git-diff'] =
<FakeProcessInfo>[
FakeProcessInfo(MockProcess(stdout: '''
README.md
CODEOWNERS
packages/package_a/CHANGELOG.md
''')),
];
final List<String> output = await runCapturingPrint(
runner, <String>['native-test', 'android']);
expect(
output,
isNot(containsAllInOrder(<Matcher>[
contains('Running for package_a'),
])));
expect(
output,
containsAllInOrder(<Matcher>[
contains('SKIPPING ALL PACKAGES'),
]));
});
});
}); });
group('macOS', () { group('macOS', () {

View File

@ -21,11 +21,12 @@ void main() {
late Directory packagesDir; late Directory packagesDir;
late CommandRunner<void> runner; late CommandRunner<void> runner;
late RecordingProcessRunner processRunner; late RecordingProcessRunner processRunner;
late RecordingProcessRunner gitProcessRunner;
setUp(() { setUp(() {
mockPlatform = MockPlatform(isMacOS: true); mockPlatform = MockPlatform(isMacOS: true);
final GitDir gitDir; final GitDir gitDir;
(:packagesDir, :processRunner, gitProcessRunner: _, :gitDir) = (:packagesDir, :processRunner, :gitProcessRunner, :gitDir) =
configureBaseCommandMocks(platform: mockPlatform); configureBaseCommandMocks(platform: mockPlatform);
final XcodeAnalyzeCommand command = XcodeAnalyzeCommand( final XcodeAnalyzeCommand command = XcodeAnalyzeCommand(
packagesDir, packagesDir,
@ -561,5 +562,64 @@ void main() {
expect(processRunner.recordedCalls, orderedEquals(<ProcessCall>[])); expect(processRunner.recordedCalls, orderedEquals(<ProcessCall>[]));
}); });
}); });
group('file filtering', () {
const List<String> files = <String>[
'foo.m',
'foo.swift',
'foo.cc',
'foo.cpp',
'foo.h',
];
for (final String file in files) {
test('runs command for changes to $file', () async {
createFakePackage('package_a', packagesDir);
gitProcessRunner.mockProcessesForExecutable['git-diff'] =
<FakeProcessInfo>[
FakeProcessInfo(MockProcess(stdout: '''
packages/package_a/$file
''')),
];
final List<String> output = await runCapturingPrint(
runner, <String>['xcode-analyze', '--ios']);
expect(
output,
containsAllInOrder(<Matcher>[
contains('Running for package_a'),
]));
});
}
test('skips commands if all files should be ignored', () async {
createFakePackage('package_a', packagesDir);
gitProcessRunner.mockProcessesForExecutable['git-diff'] =
<FakeProcessInfo>[
FakeProcessInfo(MockProcess(stdout: '''
README.md
CODEOWNERS
packages/package_a/CHANGELOG.md
packages/package_a/lib/foo.dart
''')),
];
final List<String> output =
await runCapturingPrint(runner, <String>['xcode-analyze', '--ios']);
expect(
output,
isNot(containsAllInOrder(<Matcher>[
contains('Running for package_a'),
])));
expect(
output,
containsAllInOrder(<Matcher>[
contains('SKIPPING ALL PACKAGES'),
]));
});
});
}); });
} }