blob: d0bf9440f8cf15c8bfa293cc19acc51bb93729eb [file]
// Copyright 2014 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.
// To run this, from the root of the Flutter repository:
// bin/cache/dart-sdk/bin/dart --enable-asserts dev/bots/check_tests_cross_imports.dart
import 'dart:io';
import 'package:args/args.dart';
import 'package:file/file.dart';
import 'package:file/local.dart';
import 'package:path/path.dart' as path;
import 'utils.dart';
final String _scriptLocation = path.fromUri(Platform.script);
final String _flutterRoot = path.dirname(path.dirname(path.dirname(_scriptLocation)));
final String _testDirectoryPath = path.join(_flutterRoot, 'packages', 'flutter', 'test');
void main(List<String> args) {
final argParser = ArgParser();
argParser.addFlag('help', negatable: false, help: 'Print help for this command.');
argParser.addOption(
'test',
valueHelp: 'path',
defaultsTo: _testDirectoryPath,
help: 'A location where the tests are found.',
);
argParser.addOption(
'flutter-root',
valueHelp: 'path',
defaultsTo: _flutterRoot,
help: 'The path to the root of the Flutter repo.',
);
final ArgResults parsedArgs;
void usage() {
print('dart --enable-asserts ${path.basename(_scriptLocation)} [options]');
print(argParser.usage);
}
try {
parsedArgs = argParser.parse(args);
} on FormatException catch (e) {
print(e.message);
usage();
exit(1);
}
if (parsedArgs['help'] as bool) {
usage();
exit(0);
}
const FileSystem filesystem = LocalFileSystem();
final Directory flutterSlashTestDirectory = filesystem.directory(parsedArgs['test']! as String);
final Directory flutterRoot = filesystem.directory(parsedArgs['flutter-root']! as String);
final Directory flutterTestLibraryDirectory = flutterRoot
.childDirectory('packages')
.childDirectory('flutter_test');
final checker = TestsCrossImportChecker(
flutterSlashTestDirectory: flutterSlashTestDirectory,
flutterTestLibraryDirectory: flutterTestLibraryDirectory,
flutterRoot: flutterRoot,
);
if (!checker.check()) {
reportErrorsAndExit('Some errors were found in the framework test imports.');
}
reportSuccessAndExit('No errors were detected with test cross imports.');
}
/// Checks the tests in the `flutter/test/**` libraries for cross imports.
///
/// Excludes known tests that contain cross imports, i.e.
/// [TestsCrossImportChecker.knownWidgetsCrossImports] and
/// [TestsCrossImportChecker.knownCupertinoCrossImports].
///
/// In short, the Material library should contain tests that verify behaviors
/// involving multiple libraries, such as platform adaptivity. Otherwise, these
/// libraries should not import each other in tests.
///
/// The guiding principles behind this organization of our tests are as follows:
///
/// - Cupertino should test its widgets under a full-Cupertino scenario. The
/// Cupertino library and tests should never import Material.
/// - The Material library should test its widgets in a full-Material scenario.
/// - Design languages are responsible for testing their own interoperability
/// with the Widgets library.
/// - Tests that cover interoperability between Material and Cupertino should
/// go in Material.
/// - The Widgets library and tests should never import Cupertino or Material.
/// - Libraries that do not have anything to do with Cupertino or Material should never import them.
class TestsCrossImportChecker {
TestsCrossImportChecker({
required this.flutterSlashTestDirectory,
required this.flutterTestLibraryDirectory,
required this.flutterRoot,
this.filesystem = const LocalFileSystem(),
});
final Directory flutterSlashTestDirectory;
final Directory flutterTestLibraryDirectory;
final Directory flutterRoot;
final FileSystem filesystem;
/// These Widgets tests are known to have cross imports. These cross imports
/// should all eventually be resolved, but until they are we allow them, so
/// that we can catch any new cross imports that are added.
///
/// See also:
///
/// * [knownCupertinoCrossImports], which is like this list, but for
/// Cupertino tests importing Material.
// TODO(justinmc): Fix all of these tests so there are no cross imports.
// See https://github.com/flutter/flutter/issues/177028.
static final Set<String> knownWidgetsCrossImports = <String>{
'packages/flutter/test/widgets/page_transitions_test.dart',
'packages/flutter/test/widgets/routes_test.dart',
'packages/flutter/test/widgets/app_test.dart',
'packages/flutter/test/widgets/routes_transition_test.dart',
'packages/flutter/test/widgets/editable_text_test.dart',
'packages/flutter/test/widgets/scrollbar_test.dart',
'packages/flutter/test/widgets/inherited_test.dart',
'packages/flutter/test/widgets/heroes_test.dart',
'packages/flutter/test/widgets/drawer_test.dart',
'packages/flutter/test/widgets/nested_scroll_view_test.dart',
'packages/flutter/test/widgets/scrollable_selection_test.dart',
'packages/flutter/test/widgets/page_transitions_builder_test.dart',
'packages/flutter/test/widgets/navigator_test.dart',
'packages/flutter/test/widgets/navigator_restoration_test.dart',
'packages/flutter/test/widgets/form_test.dart',
'packages/flutter/test/widgets/text_selection_toolbar_utils.dart',
'packages/flutter/test/widgets/live_text_utils.dart',
};
/// These tests are known to have cross imports. These cross imports
/// should all eventually be resolved, but until they are we allow them, so
/// that we can catch any new cross imports that are added.
///
/// Each set corresponds to a subdirectory under `flutter/test`,
/// for example `knownWidgetsCrossImports` corresponds to `flutter/test/widgets`
/// and `knownSchedulerCrossImports` corresponds to `flutter/test/scheduler`.
// TODO(justinmc): Fix all of these tests so there are no cross imports.
// See https://github.com/flutter/flutter/issues/177028.
static final Set<String> knownAnimationCrossImports = <String>{};
static final Set<String> knownCupertinoCrossImports = <String>{};
static final Set<String> knownDartCrossImports = <String>{};
static final Set<String> knownExamplesCrossImports = <String>{};
static final Set<String> knownFoundationCrossImports = <String>{};
static final Set<String> knownGesturesCrossImports = <String>{};
static final Set<String> knownHarnessCrossImports = <String>{};
static final Set<String> knownPaintingCrossImports = <String>{
'packages/flutter/test/painting/system_fonts_test.dart',
'packages/flutter/test/painting/colors_test.dart',
};
static final Set<String> knownPhysicsCrossImports = <String>{};
static final Set<String> knownRenderingCrossImports = <String>{
'packages/flutter/test/rendering/aligning_shifted_box_baseline_test.dart',
'packages/flutter/test/rendering/localized_fonts_test.dart',
};
static final Set<String> knownSchedulerCrossImports = <String>{};
static final Set<String> knownSemanticsCrossImports = <String>{};
static final Set<String> knownServicesCrossImports = <String>{};
/// These tests are known to have cross imports. These cross imports
/// should all eventually be resolved, but until they are we allow them, so
/// that we can catch any new cross imports that are added.
///
/// This set corresponds to violations in `packages/flutter/test` itself,
/// for the lists for the subdirectories of `packages/flutter/test`,
/// see [knownWidgetsCrossImports] and related lists.
// TODO(justinmc): Fix all of these tests so there are no cross imports.
// See https://github.com/flutter/flutter/issues/177028.
static final Set<String> knownFlutterSlashTestCrossImports = <String>{};
/// These tests are known to have cross imports. These cross imports
/// should all eventually be resolved, but until they are we allow them, so
/// that we can catch any new cross imports that are added.
///
/// The files in this set belong to `packages/flutter_test`, or one of its subdirectories.
// TODO(justinmc): Fix all of these tests so there are no cross imports.
// See https://github.com/flutter/flutter/issues/177028.
static final Set<String> knownFlutterTestLibraryCrossImports = <String>{
'packages/flutter_test/lib/src/widget_tester.dart',
'packages/flutter_test/lib/src/finders.dart',
'packages/flutter_test/lib/src/matchers.dart',
'packages/flutter_test/test_fixes/flutter_test/animation_sheet_builder.dart',
'packages/flutter_test/test_fixes/flutter_test/matchers.dart',
'packages/flutter_test/test/navigator_test.dart',
'packages/flutter_test/test/mock_canvas_test.dart',
'packages/flutter_test/test/semantics_finder_test.dart',
'packages/flutter_test/test/accessibility_window_test.dart',
'packages/flutter_test/test/widget_tester_live_device_test.dart',
'packages/flutter_test/test/all_elements_test.dart',
'packages/flutter_test/test/utils/memory_leak_tests.dart',
'packages/flutter_test/test/widget_tester_test.dart',
'packages/flutter_test/test/live_widget_controller_test.dart',
'packages/flutter_test/test/accessibility_test.dart',
'packages/flutter_test/test/finders_test.dart',
'packages/flutter_test/test/controller_test.dart',
'packages/flutter_test/test/recording_canvas_test.dart',
};
static final Set<String> _knownCrossImports = {
...knownFlutterTestLibraryCrossImports,
...knownFlutterSlashTestCrossImports,
...knownWidgetsCrossImports,
...knownAnimationCrossImports,
...knownCupertinoCrossImports,
...knownDartCrossImports,
...knownExamplesCrossImports,
...knownFoundationCrossImports,
...knownGesturesCrossImports,
...knownHarnessCrossImports,
...knownPaintingCrossImports,
...knownPhysicsCrossImports,
...knownRenderingCrossImports,
...knownSchedulerCrossImports,
...knownSemanticsCrossImports,
...knownServicesCrossImports,
};
// This matches both `packages/flutter/test` and `packages/flutter_test`.
static final RegExp _flutterTestPrefix = RegExp(r'packages[/\\]flutter[/\\_]test');
/// Returns the [Set] of paths in [knownPaths] that are not in [files].
static Set<String> _differencePaths(Set<String> knownPaths, Set<File> files) {
final Set<String> testPaths = files.map((File file) {
final int index = file.absolute.path.indexOf(_flutterTestPrefix);
if (index < 0) {
throw ArgumentError('All files must include $_flutterTestPrefix in their path.', 'files');
}
return file.absolute.path.substring(index).replaceAll(Platform.pathSeparator, '/');
}).toSet();
return knownPaths.difference(testPaths);
}
/// Get the [Map] of files, per [_Library], of the files that have cross imports on Material and Cupertino.
static Map<_Library, _CrossImportingFiles> _getCrossImports(Map<_Library, Set<File>> libraries) {
final Map<_Library, _CrossImportingFiles> crossImports = {};
for (final MapEntry<_Library, Set<File>> entry in libraries.entries) {
final Set<File> cupertinoImports = {};
final Set<File> materialImports = {};
for (final File file in entry.value) {
final String contents = file.readAsStringSync();
if (!entry.key.canImport(_LibraryImportStatement.cupertino) &&
contents.contains(_LibraryImportStatement.cupertino.importString)) {
cupertinoImports.add(file);
}
if (!entry.key.canImport(_LibraryImportStatement.material) &&
contents.contains(_LibraryImportStatement.material.importString)) {
materialImports.add(file);
}
}
crossImports[entry.key] = (
cupertinoImports: cupertinoImports,
materialImports: materialImports,
);
}
return crossImports;
}
/// Returns the [Set] of files that are not in [knownPaths].
static Set<File> _getUnknowns(Set<String> knownPaths, Set<File> files) {
return files.where((File file) {
final int index = file.absolute.path.indexOf(_flutterTestPrefix);
if (index < 0) {
throw ArgumentError('All files must include $_flutterTestPrefix in their path.', 'files');
}
final String comparablePath = file.absolute.path
.substring(index)
.replaceAll(Platform.pathSeparator, '/');
return !knownPaths.contains(comparablePath);
}).toSet();
}
/// Returns the error message for the given [fixedPaths] that no longer have a
/// cross import.
///
/// The [library] must not be [_MaterialLibrary], because Material is allowed to
/// cross-import.
static String _getFixedImportError(Set<String> fixedPaths, _Library library) {
assert(fixedPaths.isNotEmpty);
final buffer = StringBuffer(
'Huzzah! The following tests in ${library.name} no longer contain cross imports!\n',
);
for (final path in fixedPaths) {
buffer.writeln(' $path');
}
buffer.writeln('However, they now need to be removed from the');
buffer.write(
'${library.crossImportsListSymbolName} list in the script /dev/bots/check_tests_cross_imports.dart.',
);
return buffer.toString().trimRight();
}
/// Returns the [file]'s relative path, relative to [flutterRoot].
String _getRelativePath(File file) {
return path.relative(file.absolute.path, from: flutterRoot.absolute.path);
}
/// Get a list of all the filenames that end in ".dart", grouped by library.
Map<_Library, Set<File>> _getTestFiles() {
final dartFilePattern = RegExp(r'\.dart$');
const _Library flutterSlashTest = _OtherLibrary('packages/flutter/test');
const _Library flutterTestLibrary = _OtherLibrary('packages/flutter_test');
final Map<_Library, Set<File>> mapping = {flutterSlashTest: {}, flutterTestLibrary: {}};
// List the files directly under `packages/flutter/test` and then walk the subdirectories.
for (final FileSystemEntity fileOrDirectory in flutterSlashTestDirectory.listSync()) {
if (fileOrDirectory is File && fileOrDirectory.absolute.path.contains(dartFilePattern)) {
mapping[flutterSlashTest]?.add(fileOrDirectory);
continue;
}
if (fileOrDirectory is Directory) {
final library = _Library.fromDirectory(fileOrDirectory, flutterRoot: flutterRoot);
mapping[library] = {
for (final File file in fileOrDirectory.listSync(recursive: true).whereType<File>())
if (file.absolute.path.contains(dartFilePattern)) file,
};
}
}
// List the files directly under `packages/flutter_test` and then walk the subdirectories.
// Since packages/flutter_test is a library in itself,
// exclude generated directories like `packages/flutter_test/build` and `packages/flutter_test/.dart_tool`.
for (final FileSystemEntity fileSystemEntity in flutterTestLibraryDirectory.listSync()) {
if (fileSystemEntity is File && fileSystemEntity.absolute.path.contains(dartFilePattern)) {
mapping[flutterTestLibrary]?.add(fileSystemEntity);
continue;
}
if (fileSystemEntity is Directory) {
final String directoryName = path.basename(fileSystemEntity.absolute.path);
if (directoryName == 'build' || directoryName == '.dart_tool') {
continue;
}
for (final File file in fileSystemEntity.listSync(recursive: true).whereType<File>()) {
if (file.absolute.path.contains(dartFilePattern)) {
mapping[flutterTestLibrary]?.add(file);
}
}
}
}
return mapping;
}
/// Returns the import error for the [files] in [testLibrary] which contain the given [importStatement].
///
/// Import errors only occur when:
/// - any library that is not Material or Cupertino, imports Material or Cupertino
/// - Cupertino imports Material
String _getImportError({
required Set<File> files,
required _Library testLibrary,
required _LibraryImportStatement importStatement,
}) {
assert(
!testLibrary.canImport(importStatement),
'any library that is not Material or Cupertino, imports Material or Cupertino, '
'and when Cupertino imports Material.',
);
final String importedLibraryName = importStatement.readableName;
final buffer = StringBuffer(
files.length < 2
? 'The following test in ${testLibrary.name} has a disallowed import of $importedLibraryName. Refactor it or move it to $importedLibraryName.\n'
: 'The following ${files.length} tests in ${testLibrary.name} have a disallowed import of $importedLibraryName. Refactor them or move them to $importedLibraryName.\n',
);
for (final file in files) {
buffer.writeln(' ${_getRelativePath(file).replaceAll(Platform.pathSeparator, '/')}');
}
return buffer.toString().trimRight();
}
/// Returns true if there are no errors, false otherwise.
bool check() {
filesystem.currentDirectory = flutterRoot;
final Map<_Library, Set<File>> filesByLibrary = _getTestFiles();
// Find all cross imports.
final Map<_Library, _CrossImportingFiles> crossImportsPerLibrary = _getCrossImports(
filesByLibrary,
);
var valid = true;
// Find any cross imports that are not in the known list.
for (final MapEntry<_Library, _CrossImportingFiles> entry in crossImportsPerLibrary.entries) {
final Set<File> unknownCupertinoImports = _getUnknowns(
_knownCrossImports,
entry.value.cupertinoImports,
);
final Set<File> unknownMaterialImports = _getUnknowns(
_knownCrossImports,
entry.value.materialImports,
);
if (unknownMaterialImports.isNotEmpty) {
valid = false;
foundError(
_getImportError(
files: unknownMaterialImports,
testLibrary: entry.key,
importStatement: _LibraryImportStatement.material,
).split('\n'),
);
}
if (unknownCupertinoImports.isNotEmpty) {
valid = false;
foundError(
_getImportError(
files: unknownCupertinoImports,
testLibrary: entry.key,
importStatement: _LibraryImportStatement.cupertino,
).split('\n'),
);
}
}
// Find any known cross imports that weren't found, and are therefore fixed.
// TODO(justinmc): Remove this after all known cross imports have been
// fixed.
// See https://github.com/flutter/flutter/issues/177028.
for (final MapEntry<_Library, _CrossImportingFiles> entry in crossImportsPerLibrary.entries) {
final Set<File> crossImportsForLibrary = entry.value.cupertinoImports.union(
entry.value.materialImports,
);
final Set<String> knownCrossImportsForLibrary = entry.key.knownCrossImports;
final Set<String> fixedCrossImports = _differencePaths(
knownCrossImportsForLibrary,
crossImportsForLibrary,
);
if (fixedCrossImports.isNotEmpty) {
valid = false;
foundError(_getFixedImportError(fixedCrossImports, entry.key).split('\n'));
}
}
return valid;
}
}
/// The set of files that import Cupertino and Material for a given [_Library].
typedef _CrossImportingFiles = ({Set<File> cupertinoImports, Set<File> materialImports});
enum _LibraryImportStatement {
material('Material', "import 'package:flutter/material.dart'"),
cupertino('Cupertino', "import 'package:flutter/cupertino.dart'");
const _LibraryImportStatement(this.readableName, this.importString);
final String readableName;
final String importString;
}
/// The libraries that we are concerned with cross importing.
sealed class _Library {
const _Library(this.name);
/// Construct a [_Library] from a given [directory].
///
/// The [directory] must be inside the [flutterRoot].
factory _Library.fromDirectory(Directory directory, {required Directory flutterRoot}) {
if (!directory.absolute.path.startsWith(flutterRoot.absolute.path)) {
throw ArgumentError('Directory must be inside ${flutterRoot.absolute.path}.', 'directory');
}
final String relativePath = path
.relative(directory.absolute.path, from: flutterRoot.absolute.path)
.replaceAll(Platform.pathSeparator, '/');
return switch (relativePath) {
'packages/flutter/test/material' => const _MaterialLibrary(),
'packages/flutter/test/cupertino' => const _CupertinoLibrary(),
_ => _OtherLibrary(relativePath),
};
}
/// The short name of the library, for example `packages/flutter/test/widgets`.
final String name;
/// The name of the variable in [TestsCrossImportChecker]
/// that contains the list of known cross imports for this library.
///
/// This is used for reporting mismatched cross imports.
String get crossImportsListSymbolName {
return switch (name) {
'packages/flutter_test' => 'knownFlutterTestLibraryCrossImports',
'packages/flutter/test' => 'knownFlutterSlashTestCrossImports',
'packages/flutter/test/animation' => 'knownAnimationCrossImports',
'packages/flutter/test/cupertino' => 'knownCupertinoCrossImports',
'packages/flutter/test/dart' => 'knownDartCrossImports',
'packages/flutter/test/examples' => 'knownExamplesCrossImports',
'packages/flutter/test/foundation' => 'knownFoundationCrossImports',
'packages/flutter/test/gestures' => 'knownGesturesCrossImports',
'packages/flutter/test/harness' => 'knownHarnessCrossImports',
'packages/flutter/test/material' => throw UnsupportedError(
'Material is responsible for testing its interactions with Cupertino, so it is allowed to cross-import.',
),
'packages/flutter/test/painting' => 'knownPaintingCrossImports',
'packages/flutter/test/physics' => 'knownPhysicsCrossImports',
'packages/flutter/test/rendering' => 'knownRenderingCrossImports',
'packages/flutter/test/scheduler' => 'knownSchedulerCrossImports',
'packages/flutter/test/semantics' => 'knownSemanticsCrossImports',
'packages/flutter/test/services' => 'knownServicesCrossImports',
'packages/flutter/test/widgets' => 'knownWidgetsCrossImports',
_ => throw UnimplementedError('Unknown library: $name'),
};
}
/// Get the list of known cross imports for this [_Library].
Set<String> get knownCrossImports {
// Material is allowed to cross import.
if (this is _MaterialLibrary) {
return const <String>{};
}
return switch (crossImportsListSymbolName) {
'knownFlutterTestLibraryCrossImports' =>
TestsCrossImportChecker.knownFlutterTestLibraryCrossImports,
'knownFlutterSlashTestCrossImports' =>
TestsCrossImportChecker.knownFlutterSlashTestCrossImports,
'knownAnimationCrossImports' => TestsCrossImportChecker.knownAnimationCrossImports,
'knownCupertinoCrossImports' => TestsCrossImportChecker.knownCupertinoCrossImports,
'knownDartCrossImports' => TestsCrossImportChecker.knownDartCrossImports,
'knownExamplesCrossImports' => TestsCrossImportChecker.knownExamplesCrossImports,
'knownFoundationCrossImports' => TestsCrossImportChecker.knownFoundationCrossImports,
'knownGesturesCrossImports' => TestsCrossImportChecker.knownGesturesCrossImports,
'knownHarnessCrossImports' => TestsCrossImportChecker.knownHarnessCrossImports,
'knownPaintingCrossImports' => TestsCrossImportChecker.knownPaintingCrossImports,
'knownPhysicsCrossImports' => TestsCrossImportChecker.knownPhysicsCrossImports,
'knownRenderingCrossImports' => TestsCrossImportChecker.knownRenderingCrossImports,
'knownSchedulerCrossImports' => TestsCrossImportChecker.knownSchedulerCrossImports,
'knownSemanticsCrossImports' => TestsCrossImportChecker.knownSemanticsCrossImports,
'knownServicesCrossImports' => TestsCrossImportChecker.knownServicesCrossImports,
'knownWidgetsCrossImports' => TestsCrossImportChecker.knownWidgetsCrossImports,
_ => throw UnimplementedError('Unknown library: $name'),
};
}
/// Returns whether this library can contain the given [import].
bool canImport(_LibraryImportStatement import) {
return switch (this) {
_MaterialLibrary() =>
import == _LibraryImportStatement.material || import == _LibraryImportStatement.cupertino,
_CupertinoLibrary() => import == _LibraryImportStatement.cupertino,
_OtherLibrary() => false,
};
}
}
/// The Material library, also known as `packages/flutter/test/material`.
final class _MaterialLibrary extends _Library {
const _MaterialLibrary() : super('packages/flutter/test/material');
}
/// The Cupertino library, also known as `packages/flutter/test/cupertino`.
final class _CupertinoLibrary extends _Library {
const _CupertinoLibrary() : super('packages/flutter/test/cupertino');
}
/// Any library that is not [_MaterialLibrary] or [_CupertinoLibrary],
/// such as `packages/flutter/test/widgets` or `packages/flutter/test/services`.
final class _OtherLibrary extends _Library {
const _OtherLibrary(super.name);
}