diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index 584ea571f0..267019fe73 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -1,5 +1,9 @@ ## NEXT +- Added Android native integration test support to `native-test`. + +## 0.5.0 + - `--exclude` and `--custom-analysis` now accept paths to YAML files that contain lists of packages to exclude, in addition to just package names, so that exclude lists can be maintained separately from scripts and CI diff --git a/script/tool/lib/src/native_test_command.dart b/script/tool/lib/src/native_test_command.dart index 36b12741f2..9fc6a2912c 100644 --- a/script/tool/lib/src/native_test_command.dart +++ b/script/tool/lib/src/native_test_command.dart @@ -96,11 +96,6 @@ this command. throw ToolExit(exitInvalidArguments); } - if (getBoolArg(kPlatformAndroid) && getBoolArg(_integrationTestFlag)) { - logWarning('This command currently only supports unit tests for Android. ' - 'See https://github.com/flutter/flutter/issues/86490.'); - } - // iOS-specific run-level state. if (_requestedPlatforms.contains('ios')) { String destination = getStringArg(_iosDestinationFlag); @@ -178,12 +173,8 @@ this command. } Future<_PlatformResult> _testAndroid(Directory plugin, _TestMode mode) async { - final List examplesWithTests = []; - for (final Directory example in getExamplesForPlugin(plugin)) { - if (!isFlutterPackage(example)) { - continue; - } - if (example + bool exampleHasUnitTests(Directory example) { + return example .childDirectory('android') .childDirectory('app') .childDirectory('src') @@ -193,20 +184,62 @@ this command. .childDirectory('android') .childDirectory('src') .childDirectory('test') - .existsSync()) { - examplesWithTests.add(example); - } else { - _printNoExampleTestsMessage(example, 'Android'); - } + .existsSync(); } - if (examplesWithTests.isEmpty) { - return _PlatformResult(RunState.skipped); + bool exampleHasNativeIntegrationTests(Directory example) { + final Directory integrationTestDirectory = example + .childDirectory('android') + .childDirectory('app') + .childDirectory('src') + .childDirectory('androidTest'); + // 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 + // 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. + // + // If those are the only files, then there are no tests to run here. + return integrationTestDirectory.existsSync() && + integrationTestDirectory + .listSync(recursive: true) + .whereType() + .any((File file) { + final String basename = file.basename; + return !basename.endsWith('ActivityTest.java') && + basename != 'DartIntegrationTest.java'; + }); } + final Iterable examples = getExamplesForPlugin(plugin); + + bool ranTests = false; bool failed = false; bool hasMissingBuild = false; - for (final Directory example in examplesWithTests) { + for (final Directory example in examples) { + final bool hasUnitTests = exampleHasUnitTests(example); + final bool hasIntegrationTests = + exampleHasNativeIntegrationTests(example); + + if (mode.unit && !hasUnitTests) { + _printNoExampleTestsMessage(example, 'Android unit'); + } + if (mode.integration && !hasIntegrationTests) { + _printNoExampleTestsMessage(example, 'Android integration'); + } + + final bool runUnitTests = mode.unit && hasUnitTests; + final bool runIntegrationTests = mode.integration && hasIntegrationTests; + if (!runUnitTests && !runIntegrationTests) { + continue; + } + final String exampleName = getPackageDescription(example); _printRunningExampleTestsMessage(example, 'Android'); @@ -221,17 +254,52 @@ this command. continue; } - final int exitCode = await processRunner.runAndStream( - gradleFile.path, ['testDebugUnitTest'], - workingDir: androidDirectory); - if (exitCode != 0) { - printError('$exampleName tests failed.'); - failed = true; + if (runUnitTests) { + print('Running unit tests...'); + final int exitCode = await processRunner.runAndStream( + gradleFile.path, ['testDebugUnitTest'], + workingDir: androidDirectory); + if (exitCode != 0) { + printError('$exampleName unit tests failed.'); + failed = true; + } + ranTests = true; + } + + 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 + // filtering them out. + const String filter = + 'notAnnotation=io.flutter.plugins.DartIntegrationTest'; + + print('Running integration tests...'); + final int exitCode = await processRunner.runAndStream( + gradleFile.path, + [ + 'app:connectedAndroidTest', + '-Pandroid.testInstrumentationRunnerArguments.$filter', + ], + workingDir: androidDirectory); + if (exitCode != 0) { + printError('$exampleName integration tests failed.'); + failed = true; + } + ranTests = true; } } - return _PlatformResult(failed ? RunState.failed : RunState.succeeded, - error: - hasMissingBuild ? 'Examples must be built before testing.' : null); + + if (failed) { + return _PlatformResult(RunState.failed, + error: hasMissingBuild + ? 'Examples must be built before testing.' + : null); + } + if (!ranTests) { + return _PlatformResult(RunState.skipped); + } + return _PlatformResult(RunState.succeeded); } Future<_PlatformResult> _testIos(Directory plugin, _TestMode mode) { diff --git a/script/tool/pubspec.yaml b/script/tool/pubspec.yaml index 7b2cdd4f41..02b3ca624b 100644 --- a/script/tool/pubspec.yaml +++ b/script/tool/pubspec.yaml @@ -1,7 +1,7 @@ name: flutter_plugin_tools description: Productivity utils for flutter/plugins and flutter/packages repository: https://github.com/flutter/plugins/tree/master/script/tool -version: 0.4.1 +version: 0.5.0 dependencies: args: ^2.1.0 diff --git a/script/tool/test/native_test_command_test.dart b/script/tool/test/native_test_command_test.dart index e656e2f237..59ca17b25c 100644 --- a/script/tool/test/native_test_command_test.dart +++ b/script/tool/test/native_test_command_test.dart @@ -16,6 +16,10 @@ import 'package:test/test.dart'; import 'mocks.dart'; import 'util.dart'; +const String _androidIntegrationTestFilter = + '-Pandroid.testInstrumentationRunnerArguments.' + 'notAnnotation=io.flutter.plugins.DartIntegrationTest'; + final Map _kDeviceListMap = { 'runtimes': >[ { @@ -353,7 +357,7 @@ void main() { }); group('Android', () { - test('runs Java tests in Android implementation folder', () async { + test('runs Java unit tests in Android implementation folder', () async { final Directory plugin = createFakePlugin( 'plugin', packagesDir, @@ -383,7 +387,7 @@ void main() { ); }); - test('runs Java tests in example folder', () async { + test('runs Java unit tests in example folder', () async { final Directory plugin = createFakePlugin( 'plugin', packagesDir, @@ -413,6 +417,172 @@ void main() { ); }); + test('runs Java integration tests', () async { + final Directory plugin = createFakePlugin( + 'plugin', + packagesDir, + platformSupport: { + kPlatformAndroid: PlatformSupport.inline + }, + extraFiles: [ + 'example/android/gradlew', + 'example/android/app/src/androidTest/IntegrationTest.java', + ], + ); + + await runCapturingPrint(runner, ['native-test', '--android']); + + final Directory androidFolder = + plugin.childDirectory('example').childDirectory('android'); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + androidFolder.childFile('gradlew').path, + const [ + 'app:connectedAndroidTest', + _androidIntegrationTestFilter, + ], + androidFolder.path, + ), + ]), + ); + }); + + test( + 'ignores Java integration test files associated with integration_test', + () async { + createFakePlugin( + 'plugin', + packagesDir, + platformSupport: { + kPlatformAndroid: PlatformSupport.inline + }, + extraFiles: [ + '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', + ], + ); + + await runCapturingPrint(runner, ['native-test', '--android']); + + // Nothing should run since those files are all + // integration_test-specific. + expect( + processRunner.recordedCalls, + orderedEquals([]), + ); + }); + + test('runs all tests when present', () async { + final Directory plugin = createFakePlugin( + 'plugin', + packagesDir, + platformSupport: { + kPlatformAndroid: PlatformSupport.inline + }, + extraFiles: [ + 'android/src/test/example_test.java', + 'example/android/gradlew', + 'example/android/app/src/androidTest/IntegrationTest.java', + ], + ); + + await runCapturingPrint(runner, ['native-test', '--android']); + + final Directory androidFolder = + plugin.childDirectory('example').childDirectory('android'); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + androidFolder.childFile('gradlew').path, + const ['testDebugUnitTest'], + androidFolder.path, + ), + ProcessCall( + androidFolder.childFile('gradlew').path, + const [ + 'app:connectedAndroidTest', + _androidIntegrationTestFilter, + ], + androidFolder.path, + ), + ]), + ); + }); + + test('honors --no-unit', () async { + final Directory plugin = createFakePlugin( + 'plugin', + packagesDir, + platformSupport: { + kPlatformAndroid: PlatformSupport.inline + }, + extraFiles: [ + 'android/src/test/example_test.java', + 'example/android/gradlew', + 'example/android/app/src/androidTest/IntegrationTest.java', + ], + ); + + await runCapturingPrint( + runner, ['native-test', '--android', '--no-unit']); + + final Directory androidFolder = + plugin.childDirectory('example').childDirectory('android'); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + androidFolder.childFile('gradlew').path, + const [ + 'app:connectedAndroidTest', + _androidIntegrationTestFilter, + ], + androidFolder.path, + ), + ]), + ); + }); + + test('honors --no-integration', () async { + final Directory plugin = createFakePlugin( + 'plugin', + packagesDir, + platformSupport: { + kPlatformAndroid: PlatformSupport.inline + }, + extraFiles: [ + 'android/src/test/example_test.java', + 'example/android/gradlew', + 'example/android/app/src/androidTest/IntegrationTest.java', + ], + ); + + await runCapturingPrint( + runner, ['native-test', '--android', '--no-integration']); + + final Directory androidFolder = + plugin.childDirectory('example').childDirectory('android'); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + androidFolder.childFile('gradlew').path, + const ['testDebugUnitTest'], + androidFolder.path, + ), + ]), + ); + }); + test('fails when the app needs to be built', () async { createFakePlugin( 'plugin', @@ -444,6 +614,46 @@ void main() { ); }); + test('logs missing test types', () async { + // No unit tests. + createFakePlugin( + 'plugin1', + packagesDir, + platformSupport: { + kPlatformAndroid: PlatformSupport.inline + }, + extraFiles: [ + 'example/android/gradlew', + 'example/android/app/src/androidTest/IntegrationTest.java', + ], + ); + // No integration tests. + createFakePlugin( + 'plugin2', + packagesDir, + platformSupport: { + kPlatformAndroid: PlatformSupport.inline + }, + extraFiles: [ + 'android/src/test/example_test.java', + 'example/android/gradlew', + ], + ); + + final List output = await runCapturingPrint( + runner, ['native-test', '--android']); + + expect( + output, + containsAllInOrder([ + contains('No Android unit tests found for plugin1/example'), + contains('Running integration tests...'), + contains( + 'No Android integration tests found for plugin2/example'), + contains('Running unit tests...'), + ])); + }); + test('fails when a test fails', () async { final Directory pluginDir = createFakePlugin( 'plugin', @@ -478,7 +688,7 @@ void main() { expect( output, containsAllInOrder([ - contains('plugin/example tests failed.'), + contains('plugin/example unit tests failed.'), contains('The following packages had errors:'), contains('plugin') ]), @@ -518,7 +728,8 @@ void main() { expect( output, containsAllInOrder([ - contains('No Android tests found for plugin/example'), + contains('No Android unit tests found for plugin/example'), + contains('No Android integration tests found for plugin/example'), contains('SKIPPING: No tests found.'), ]), ); @@ -810,10 +1021,8 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ - ProcessCall( - androidFolder.childFile('gradlew').path, - const ['testDebugUnitTest'], - androidFolder.path), + ProcessCall(androidFolder.childFile('gradlew').path, + const ['testDebugUnitTest'], androidFolder.path), ProcessCall( 'xcrun', const [ @@ -1003,7 +1212,7 @@ void main() { output, containsAllInOrder([ contains('Running tests for Android...'), - contains('plugin/example tests failed.'), + contains('plugin/example unit tests failed.'), contains('Running tests for iOS...'), contains('Successfully ran iOS xctest for plugin/example'), contains('The following packages had errors:'),