Skip to content

Commit 08e76c0

Browse files
[tool] Check Java integration test configuration (flutter#3499)
[tool] Check Java integration test configuration
1 parent b073b74 commit 08e76c0

File tree

2 files changed

+168
-38
lines changed

2 files changed

+168
-38
lines changed

script/tool/lib/src/native_test_command.dart

Lines changed: 101 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// found in the LICENSE file.
44

55
import 'package:file/file.dart';
6+
import 'package:meta/meta.dart';
67
import 'package:platform/platform.dart';
78

89
import 'common/cmake.dart';
@@ -21,6 +22,16 @@ const String _iOSDestinationFlag = 'ios-destination';
2122

2223
const int _exitNoIOSSimulators = 3;
2324

25+
/// The error message logged when a FlutterTestRunner test is not annotated with
26+
/// @DartIntegrationTest.
27+
@visibleForTesting
28+
const String misconfiguredJavaIntegrationTestErrorExplanation =
29+
'The following files use @RunWith(FlutterTestRunner.class) '
30+
'but not @DartIntegrationTest, which will cause hangs when run with '
31+
'this command. See '
32+
'https://github.com/flutter/flutter/wiki/Plugin-Tests#enabling-android-ui-tests '
33+
'for instructions.';
34+
2435
/// The command to run native tests for plugins:
2536
/// - iOS and macOS: XCTests (XCUnitTest and XCUITest)
2637
/// - Android: JUnit tests
@@ -211,7 +222,17 @@ this command.
211222
.existsSync();
212223
}
213224

214-
bool exampleHasNativeIntegrationTests(RepositoryPackage example) {
225+
_JavaTestInfo getJavaTestInfo(File testFile) {
226+
final List<String> contents = testFile.readAsLinesSync();
227+
return _JavaTestInfo(
228+
usesFlutterTestRunner: contents.any((String line) =>
229+
line.trimLeft().startsWith('@RunWith(FlutterTestRunner.class)')),
230+
hasDartIntegrationTestAnnotation: contents.any((String line) =>
231+
line.trimLeft().startsWith('@DartIntegrationTest')));
232+
}
233+
234+
Map<File, _JavaTestInfo> findIntegrationTestFiles(
235+
RepositoryPackage example) {
215236
final Directory integrationTestDirectory = example
216237
.platformDirectory(FlutterPlatform.android)
217238
.childDirectory('app')
@@ -220,25 +241,30 @@ this command.
220241
// There are two types of integration tests that can be in the androidTest
221242
// directory:
222243
// - FlutterTestRunner.class tests, which bridge to Dart integration tests
223-
// - Purely native tests
244+
// - Purely native integration tests
224245
// Only the latter is supported by this command; the former will hang if
225246
// run here because they will wait for a Dart call that will never come.
226247
//
227-
// This repository uses a convention of putting the former in a
228-
// *ActivityTest.java file, so ignore that file when checking for tests.
229-
// Also ignore DartIntegrationTest.java, which defines the annotation used
230-
// below for filtering the former out when running tests.
248+
// Find all test files, and determine which kind of test they are based on
249+
// the annotations they use.
231250
//
232-
// If those are the only files, then there are no tests to run here.
233-
return integrationTestDirectory.existsSync() &&
234-
integrationTestDirectory
235-
.listSync(recursive: true)
236-
.whereType<File>()
237-
.any((File file) {
238-
final String basename = file.basename;
239-
return !basename.endsWith('ActivityTest.java') &&
240-
basename != 'DartIntegrationTest.java';
241-
});
251+
// Ignore DartIntegrationTest.java, which defines the annotation used
252+
// below for filtering the former out when running tests.
253+
if (!integrationTestDirectory.existsSync()) {
254+
return <File, _JavaTestInfo>{};
255+
}
256+
final Iterable<File> integrationTestFiles = integrationTestDirectory
257+
.listSync(recursive: true)
258+
.whereType<File>()
259+
.where((File file) {
260+
final String basename = file.basename;
261+
return basename != 'DartIntegrationTest.java' &&
262+
basename != 'DartIntegrationTest.kt';
263+
});
264+
return <File, _JavaTestInfo>{
265+
for (final File file in integrationTestFiles)
266+
file: getJavaTestInfo(file)
267+
};
242268
}
243269

244270
final Iterable<RepositoryPackage> examples = plugin.getExamples();
@@ -247,10 +273,17 @@ this command.
247273
bool ranAnyTests = false;
248274
bool failed = false;
249275
bool hasMissingBuild = false;
276+
bool hasMisconfiguredIntegrationTest = false;
277+
// Iterate through all examples (in the rare case that there is more than
278+
// one example); running any tests found for each one. Requirements on what
279+
// tests are present are enforced at the overall package level, not a per-
280+
// example level. E.g., it's fine for only one example to have unit tests.
250281
for (final RepositoryPackage example in examples) {
251282
final bool hasUnitTests = exampleHasUnitTests(example);
252-
final bool hasIntegrationTests =
253-
exampleHasNativeIntegrationTests(example);
283+
final Map<File, _JavaTestInfo> candidateIntegrationTestFiles =
284+
findIntegrationTestFiles(example);
285+
final bool hasIntegrationTests = candidateIntegrationTestFiles.values
286+
.any((_JavaTestInfo info) => !info.hasDartIntegrationTestAnnotation);
254287

255288
if (mode.unit && !hasUnitTests) {
256289
_printNoExampleTestsMessage(example, 'Android unit');
@@ -295,24 +328,41 @@ this command.
295328

296329
if (runIntegrationTests) {
297330
// FlutterTestRunner-based tests will hang forever if run in a normal
298-
// app build, since they wait for a Dart call from integration_test that
299-
// will never come. Those tests have an extra annotation to allow
331+
// app build, since they wait for a Dart call from integration_test
332+
// that will never come. Those tests have an extra annotation to allow
300333
// filtering them out.
301-
const String filter =
302-
'notAnnotation=io.flutter.plugins.DartIntegrationTest';
303-
304-
print('Running integration tests...');
305-
final int exitCode = await project.runCommand(
306-
'app:connectedAndroidTest',
307-
arguments: <String>[
308-
'-Pandroid.testInstrumentationRunnerArguments.$filter',
309-
],
310-
);
311-
if (exitCode != 0) {
312-
printError('$exampleName integration tests failed.');
313-
failed = true;
334+
final List<File> misconfiguredTestFiles = candidateIntegrationTestFiles
335+
.entries
336+
.where((MapEntry<File, _JavaTestInfo> entry) =>
337+
entry.value.usesFlutterTestRunner &&
338+
!entry.value.hasDartIntegrationTestAnnotation)
339+
.map((MapEntry<File, _JavaTestInfo> entry) => entry.key)
340+
.toList();
341+
if (misconfiguredTestFiles.isEmpty) {
342+
// Ideally we would filter out @RunWith(FlutterTestRunner.class)
343+
// tests directly, but there doesn't seem to be a way to filter based
344+
// on annotation contents, so the DartIntegrationTest annotation was
345+
// created as a proxy for that.
346+
const String filter =
347+
'notAnnotation=io.flutter.plugins.DartIntegrationTest';
348+
349+
print('Running integration tests...');
350+
final int exitCode = await project.runCommand(
351+
'app:connectedAndroidTest',
352+
arguments: <String>[
353+
'-Pandroid.testInstrumentationRunnerArguments.$filter',
354+
],
355+
);
356+
if (exitCode != 0) {
357+
printError('$exampleName integration tests failed.');
358+
failed = true;
359+
}
360+
ranAnyTests = true;
361+
} else {
362+
hasMisconfiguredIntegrationTest = true;
363+
printError('$misconfiguredJavaIntegrationTestErrorExplanation\n'
364+
'${misconfiguredTestFiles.map((File f) => ' ${f.path}').join('\n')}');
314365
}
315-
ranAnyTests = true;
316366
}
317367
}
318368

@@ -322,6 +372,10 @@ this command.
322372
? 'Examples must be built before testing.'
323373
: null);
324374
}
375+
if (hasMisconfiguredIntegrationTest) {
376+
return _PlatformResult(RunState.failed,
377+
error: 'Misconfigured integration test.');
378+
}
325379
if (!mode.integrationOnly && !ranUnitTests) {
326380
printError('No unit tests ran. Plugins are required to have unit tests.');
327381
return _PlatformResult(RunState.failed,
@@ -622,3 +676,16 @@ class _PlatformResult {
622676
/// Ignored unless [state] is `failed`.
623677
final String? error;
624678
}
679+
680+
/// The state of a .java test file.
681+
class _JavaTestInfo {
682+
const _JavaTestInfo(
683+
{required this.usesFlutterTestRunner,
684+
required this.hasDartIntegrationTestAnnotation});
685+
686+
/// Whether the test class uses the FlutterTestRunner.
687+
final bool usesFlutterTestRunner;
688+
689+
/// Whether the test class has the @DartIntegrationTest annotation.
690+
final bool hasDartIntegrationTestAnnotation;
691+
}

script/tool/test/native_test_command_test.dart

Lines changed: 67 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import 'package:flutter_plugin_tools/src/common/core.dart';
1313
import 'package:flutter_plugin_tools/src/common/file_utils.dart';
1414
import 'package:flutter_plugin_tools/src/common/plugin_utils.dart';
1515
import 'package:flutter_plugin_tools/src/native_test_command.dart';
16+
import 'package:path/path.dart' as p;
1617
import 'package:platform/platform.dart';
1718
import 'package:test/test.dart';
1819

@@ -511,9 +512,11 @@ void main() {
511512
});
512513

513514
test(
514-
'ignores Java integration test files associated with integration_test',
515+
'ignores Java integration test files using (or defining) DartIntegrationTest',
515516
() async {
516-
createFakePlugin(
517+
const String dartTestDriverRelativePath =
518+
'android/app/src/androidTest/java/io/flutter/plugins/plugin/FlutterActivityTest.java';
519+
final RepositoryPackage plugin = createFakePlugin(
517520
'plugin',
518521
packagesDir,
519522
platformSupport: <String, PlatformDetails>{
@@ -522,11 +525,24 @@ void main() {
522525
extraFiles: <String>[
523526
'example/android/gradlew',
524527
'example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java',
525-
'example/android/app/src/androidTest/java/io/flutter/plugins/plugin/FlutterActivityTest.java',
526-
'example/android/app/src/androidTest/java/io/flutter/plugins/plugin/MainActivityTest.java',
528+
'example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.kt',
529+
'example/$dartTestDriverRelativePath',
527530
],
528531
);
529532

533+
final File dartTestDriverFile = childFileWithSubcomponents(
534+
plugin.getExamples().first.directory,
535+
p.posix.split(dartTestDriverRelativePath));
536+
dartTestDriverFile.writeAsStringSync('''
537+
import io.flutter.plugins.DartIntegrationTest;
538+
import org.junit.runner.RunWith;
539+
540+
@DartIntegrationTest
541+
@RunWith(FlutterTestRunner.class)
542+
public class FlutterActivityTest {
543+
}
544+
''');
545+
530546
await runCapturingPrint(
531547
runner, <String>['native-test', '--android', '--no-unit']);
532548

@@ -538,6 +554,53 @@ void main() {
538554
);
539555
});
540556

557+
test(
558+
'fails for Java integration tests Using FlutterTestRunner without @DartIntegrationTest',
559+
() async {
560+
const String dartTestDriverRelativePath =
561+
'android/app/src/androidTest/java/io/flutter/plugins/plugin/FlutterActivityTest.java';
562+
final RepositoryPackage plugin = createFakePlugin(
563+
'plugin',
564+
packagesDir,
565+
platformSupport: <String, PlatformDetails>{
566+
platformAndroid: const PlatformDetails(PlatformSupport.inline)
567+
},
568+
extraFiles: <String>[
569+
'example/android/gradlew',
570+
'example/$dartTestDriverRelativePath',
571+
],
572+
);
573+
574+
final File dartTestDriverFile = childFileWithSubcomponents(
575+
plugin.getExamples().first.directory,
576+
p.posix.split(dartTestDriverRelativePath));
577+
dartTestDriverFile.writeAsStringSync('''
578+
import io.flutter.plugins.DartIntegrationTest;
579+
import org.junit.runner.RunWith;
580+
581+
@RunWith(FlutterTestRunner.class)
582+
public class FlutterActivityTest {
583+
}
584+
''');
585+
586+
Error? commandError;
587+
final List<String> output = await runCapturingPrint(
588+
runner, <String>['native-test', '--android', '--no-unit'],
589+
errorHandler: (Error e) {
590+
commandError = e;
591+
});
592+
593+
expect(commandError, isA<ToolExit>());
594+
expect(
595+
output,
596+
contains(
597+
contains(misconfiguredJavaIntegrationTestErrorExplanation)));
598+
expect(
599+
output,
600+
contains(contains(
601+
'example/android/app/src/androidTest/java/io/flutter/plugins/plugin/FlutterActivityTest.java')));
602+
});
603+
541604
test('runs all tests when present', () async {
542605
final RepositoryPackage plugin = createFakePlugin(
543606
'plugin',

0 commit comments

Comments
 (0)