[tool] Check Java integration test configuration (#3499)
[tool] Check Java integration test configuration
diff --git a/script/tool/lib/src/native_test_command.dart b/script/tool/lib/src/native_test_command.dart
index af5f4df..f9967ca 100644
--- a/script/tool/lib/src/native_test_command.dart
+++ b/script/tool/lib/src/native_test_command.dart
@@ -3,6 +3,7 @@
// found in the LICENSE file.
import 'package:file/file.dart';
+import 'package:meta/meta.dart';
import 'package:platform/platform.dart';
import 'common/cmake.dart';
@@ -21,6 +22,16 @@
const int _exitNoIOSSimulators = 3;
+/// The error message logged when a FlutterTestRunner test is not annotated with
+/// @DartIntegrationTest.
+@visibleForTesting
+const String misconfiguredJavaIntegrationTestErrorExplanation =
+ 'The following files use @RunWith(FlutterTestRunner.class) '
+ 'but not @DartIntegrationTest, which will cause hangs when run with '
+ 'this command. See '
+ 'https://github.com/flutter/flutter/wiki/Plugin-Tests#enabling-android-ui-tests '
+ 'for instructions.';
+
/// The command to run native tests for plugins:
/// - iOS and macOS: XCTests (XCUnitTest and XCUITest)
/// - Android: JUnit tests
@@ -211,7 +222,17 @@
.existsSync();
}
- bool exampleHasNativeIntegrationTests(RepositoryPackage example) {
+ _JavaTestInfo getJavaTestInfo(File testFile) {
+ final List<String> contents = testFile.readAsLinesSync();
+ return _JavaTestInfo(
+ usesFlutterTestRunner: contents.any((String line) =>
+ line.trimLeft().startsWith('@RunWith(FlutterTestRunner.class)')),
+ hasDartIntegrationTestAnnotation: contents.any((String line) =>
+ line.trimLeft().startsWith('@DartIntegrationTest')));
+ }
+
+ Map<File, _JavaTestInfo> findIntegrationTestFiles(
+ RepositoryPackage example) {
final Directory integrationTestDirectory = example
.platformDirectory(FlutterPlatform.android)
.childDirectory('app')
@@ -220,25 +241,30 @@
// There are two types of integration tests that can be in the androidTest
// directory:
// - FlutterTestRunner.class tests, which bridge to Dart integration tests
- // - Purely native tests
+ // - Purely native integration tests
// Only the latter is supported by this command; the former will hang if
// run here because they will wait for a Dart call that will never come.
//
- // This repository uses a convention of putting the former in a
- // *ActivityTest.java file, so ignore that file when checking for tests.
- // Also ignore DartIntegrationTest.java, which defines the annotation used
- // below for filtering the former out when running tests.
+ // Find all test files, and determine which kind of test they are based on
+ // the annotations they use.
//
- // If those are the only files, then there are no tests to run here.
- return integrationTestDirectory.existsSync() &&
- integrationTestDirectory
- .listSync(recursive: true)
- .whereType<File>()
- .any((File file) {
- final String basename = file.basename;
- return !basename.endsWith('ActivityTest.java') &&
- basename != 'DartIntegrationTest.java';
- });
+ // Ignore DartIntegrationTest.java, which defines the annotation used
+ // below for filtering the former out when running tests.
+ if (!integrationTestDirectory.existsSync()) {
+ return <File, _JavaTestInfo>{};
+ }
+ final Iterable<File> integrationTestFiles = integrationTestDirectory
+ .listSync(recursive: true)
+ .whereType<File>()
+ .where((File file) {
+ final String basename = file.basename;
+ return basename != 'DartIntegrationTest.java' &&
+ basename != 'DartIntegrationTest.kt';
+ });
+ return <File, _JavaTestInfo>{
+ for (final File file in integrationTestFiles)
+ file: getJavaTestInfo(file)
+ };
}
final Iterable<RepositoryPackage> examples = plugin.getExamples();
@@ -247,10 +273,17 @@
bool ranAnyTests = false;
bool failed = false;
bool hasMissingBuild = false;
+ bool hasMisconfiguredIntegrationTest = false;
+ // Iterate through all examples (in the rare case that there is more than
+ // one example); running any tests found for each one. Requirements on what
+ // tests are present are enforced at the overall package level, not a per-
+ // example level. E.g., it's fine for only one example to have unit tests.
for (final RepositoryPackage example in examples) {
final bool hasUnitTests = exampleHasUnitTests(example);
- final bool hasIntegrationTests =
- exampleHasNativeIntegrationTests(example);
+ final Map<File, _JavaTestInfo> candidateIntegrationTestFiles =
+ findIntegrationTestFiles(example);
+ final bool hasIntegrationTests = candidateIntegrationTestFiles.values
+ .any((_JavaTestInfo info) => !info.hasDartIntegrationTestAnnotation);
if (mode.unit && !hasUnitTests) {
_printNoExampleTestsMessage(example, 'Android unit');
@@ -295,24 +328,41 @@
if (runIntegrationTests) {
// FlutterTestRunner-based tests will hang forever if run in a normal
- // app build, since they wait for a Dart call from integration_test that
- // will never come. Those tests have an extra annotation to allow
+ // app build, since they wait for a Dart call from integration_test
+ // that will never come. Those tests have an extra annotation to allow
// filtering them out.
- const String filter =
- 'notAnnotation=io.flutter.plugins.DartIntegrationTest';
+ final List<File> misconfiguredTestFiles = candidateIntegrationTestFiles
+ .entries
+ .where((MapEntry<File, _JavaTestInfo> entry) =>
+ entry.value.usesFlutterTestRunner &&
+ !entry.value.hasDartIntegrationTestAnnotation)
+ .map((MapEntry<File, _JavaTestInfo> entry) => entry.key)
+ .toList();
+ if (misconfiguredTestFiles.isEmpty) {
+ // Ideally we would filter out @RunWith(FlutterTestRunner.class)
+ // tests directly, but there doesn't seem to be a way to filter based
+ // on annotation contents, so the DartIntegrationTest annotation was
+ // created as a proxy for that.
+ const String filter =
+ 'notAnnotation=io.flutter.plugins.DartIntegrationTest';
- print('Running integration tests...');
- final int exitCode = await project.runCommand(
- 'app:connectedAndroidTest',
- arguments: <String>[
- '-Pandroid.testInstrumentationRunnerArguments.$filter',
- ],
- );
- if (exitCode != 0) {
- printError('$exampleName integration tests failed.');
- failed = true;
+ print('Running integration tests...');
+ final int exitCode = await project.runCommand(
+ 'app:connectedAndroidTest',
+ arguments: <String>[
+ '-Pandroid.testInstrumentationRunnerArguments.$filter',
+ ],
+ );
+ if (exitCode != 0) {
+ printError('$exampleName integration tests failed.');
+ failed = true;
+ }
+ ranAnyTests = true;
+ } else {
+ hasMisconfiguredIntegrationTest = true;
+ printError('$misconfiguredJavaIntegrationTestErrorExplanation\n'
+ '${misconfiguredTestFiles.map((File f) => ' ${f.path}').join('\n')}');
}
- ranAnyTests = true;
}
}
@@ -322,6 +372,10 @@
? 'Examples must be built before testing.'
: null);
}
+ if (hasMisconfiguredIntegrationTest) {
+ return _PlatformResult(RunState.failed,
+ error: 'Misconfigured integration test.');
+ }
if (!mode.integrationOnly && !ranUnitTests) {
printError('No unit tests ran. Plugins are required to have unit tests.');
return _PlatformResult(RunState.failed,
@@ -622,3 +676,16 @@
/// Ignored unless [state] is `failed`.
final String? error;
}
+
+/// The state of a .java test file.
+class _JavaTestInfo {
+ const _JavaTestInfo(
+ {required this.usesFlutterTestRunner,
+ required this.hasDartIntegrationTestAnnotation});
+
+ /// Whether the test class uses the FlutterTestRunner.
+ final bool usesFlutterTestRunner;
+
+ /// Whether the test class has the @DartIntegrationTest annotation.
+ final bool hasDartIntegrationTestAnnotation;
+}
diff --git a/script/tool/test/native_test_command_test.dart b/script/tool/test/native_test_command_test.dart
index f24d014..ddbf6a5 100644
--- a/script/tool/test/native_test_command_test.dart
+++ b/script/tool/test/native_test_command_test.dart
@@ -13,6 +13,7 @@
import 'package:flutter_plugin_tools/src/common/file_utils.dart';
import 'package:flutter_plugin_tools/src/common/plugin_utils.dart';
import 'package:flutter_plugin_tools/src/native_test_command.dart';
+import 'package:path/path.dart' as p;
import 'package:platform/platform.dart';
import 'package:test/test.dart';
@@ -511,9 +512,11 @@
});
test(
- 'ignores Java integration test files associated with integration_test',
+ 'ignores Java integration test files using (or defining) DartIntegrationTest',
() async {
- createFakePlugin(
+ const String dartTestDriverRelativePath =
+ 'android/app/src/androidTest/java/io/flutter/plugins/plugin/FlutterActivityTest.java';
+ final RepositoryPackage plugin = createFakePlugin(
'plugin',
packagesDir,
platformSupport: <String, PlatformDetails>{
@@ -522,11 +525,24 @@
extraFiles: <String>[
'example/android/gradlew',
'example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java',
- 'example/android/app/src/androidTest/java/io/flutter/plugins/plugin/FlutterActivityTest.java',
- 'example/android/app/src/androidTest/java/io/flutter/plugins/plugin/MainActivityTest.java',
+ 'example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.kt',
+ 'example/$dartTestDriverRelativePath',
],
);
+ final File dartTestDriverFile = childFileWithSubcomponents(
+ plugin.getExamples().first.directory,
+ p.posix.split(dartTestDriverRelativePath));
+ dartTestDriverFile.writeAsStringSync('''
+import io.flutter.plugins.DartIntegrationTest;
+import org.junit.runner.RunWith;
+
+@DartIntegrationTest
+@RunWith(FlutterTestRunner.class)
+public class FlutterActivityTest {
+}
+''');
+
await runCapturingPrint(
runner, <String>['native-test', '--android', '--no-unit']);
@@ -538,6 +554,53 @@
);
});
+ test(
+ 'fails for Java integration tests Using FlutterTestRunner without @DartIntegrationTest',
+ () async {
+ const String dartTestDriverRelativePath =
+ 'android/app/src/androidTest/java/io/flutter/plugins/plugin/FlutterActivityTest.java';
+ final RepositoryPackage plugin = createFakePlugin(
+ 'plugin',
+ packagesDir,
+ platformSupport: <String, PlatformDetails>{
+ platformAndroid: const PlatformDetails(PlatformSupport.inline)
+ },
+ extraFiles: <String>[
+ 'example/android/gradlew',
+ 'example/$dartTestDriverRelativePath',
+ ],
+ );
+
+ final File dartTestDriverFile = childFileWithSubcomponents(
+ plugin.getExamples().first.directory,
+ p.posix.split(dartTestDriverRelativePath));
+ dartTestDriverFile.writeAsStringSync('''
+import io.flutter.plugins.DartIntegrationTest;
+import org.junit.runner.RunWith;
+
+@RunWith(FlutterTestRunner.class)
+public class FlutterActivityTest {
+}
+''');
+
+ Error? commandError;
+ final List<String> output = await runCapturingPrint(
+ runner, <String>['native-test', '--android', '--no-unit'],
+ errorHandler: (Error e) {
+ commandError = e;
+ });
+
+ expect(commandError, isA<ToolExit>());
+ expect(
+ output,
+ contains(
+ contains(misconfiguredJavaIntegrationTestErrorExplanation)));
+ expect(
+ output,
+ contains(contains(
+ 'example/android/app/src/androidTest/java/io/flutter/plugins/plugin/FlutterActivityTest.java')));
+ });
+
test('runs all tests when present', () async {
final RepositoryPackage plugin = createFakePlugin(
'plugin',