blob: d56a65b73d4af107dc3be2aece26e7972e73c888 [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.
import 'package:file/file.dart';
import 'package:file/local.dart';
import 'package:glob/glob.dart';
import 'package:glob/list_local_fs.dart';
import 'package:path/path.dart' as path;
import '../run_command.dart';
import '../utils.dart';
/// The root of the `android_engine_test` integration test project.
final String _androidEngineTestPath = path.join('dev', 'integration_tests', 'android_engine_test');
/// To run this test locally:
///
/// 1. Connect an Android device or emulator.
/// 2. Run `dart pub get` in dev/bots
/// 3. Run the following command from the root of the Flutter repository:
///
/// ```sh
/// # Generate a baseline of local golden files.
/// SHARD=android_engine_vulkan_tests UPDATE_GOLDENS=1 bin/cache/dart-sdk/bin/dart dev/bots/test.dart
/// # Or for HCPP tests:
/// SHARD=android_engine_hcpp_tests UPDATE_GOLDENS=1 bin/cache/dart-sdk/bin/dart dev/bots/test.dart
/// ```
///
/// 4. Then, re-run the command against the baseline images:
///
/// ```sh
/// SHARD=android_engine_vulkan_tests bin/cache/dart-sdk/bin/dart dev/bots/test.dart
/// # Or for HCPP tests:
/// SHARD=android_engine_hcpp_tests bin/cache/dart-sdk/bin/dart dev/bots/test.dart
/// ```
///
/// If you are trying to debug a commit, you will want to run step (3) first,
/// then apply the commit (or flag), and then run step (4). If you are trying
/// to determine flakiness in the *same* state, or want better debugging, see
/// `dev/integration_tests/android_engine_test/README.md`.
///
/// This runs every test *except* the HCPP tests, which are their own shard; see
/// [runAndroidEngineHcppTests].
Future<void> runAndroidEngineTests({required ImpellerBackend impellerBackend}) async {
print('Running Flutter Driver Android tests (backend=$impellerBackend)');
await _withPatchedManifest(impellerBackend, (List<FileSystemEntity> mains, File _) async {
for (final file in mains) {
if (file.path.contains('hcpp')) {
continue;
}
await _runTest(file, impellerBackend: impellerBackend);
}
});
}
/// Runs the HCPP (Hybrid Composition++) subset of the Android engine tests.
///
/// These live in `dev/integration_tests/android_engine_test/lib/hcpp` and only
/// run on Vulkan, as HCPP requires `SurfaceControl`. They are a separate shard
/// (`android_engine_hcpp_tests`) rather than part of [runAndroidEngineTests] so
/// that flakiness in the other Android engine tests cannot suppress the
/// dashboard signal for HCPP regressions.
///
/// Unlike [runAndroidEngineTests], this toggles the `EnableHcpp` manifest
/// metadata part-way through the run:
///
/// 1. `upgrade_legacy_pv_types` runs with `--enable-hcpp` while the manifest
/// still has HCPP disabled, verifying the flag alone turns HCPP on.
/// 2. The manifest is flipped to enable HCPP and the same test runs again with
/// `--no-enable-hcpp`, verifying the flag overrides the manifest.
/// 3. The remaining HCPP tests run with the manifest enabled and no flag.
///
/// See [runAndroidEngineTests] for how to run these locally.
Future<void> runAndroidEngineHcppTests() async {
const ImpellerBackend impellerBackend = ImpellerBackend.vulkan;
print('Running Flutter Driver Android HCPP tests (backend=$impellerBackend)');
await _withPatchedManifest(impellerBackend, (
List<FileSystemEntity> mains,
File androidManifestXml,
) async {
final runFirstTests = <String>[
// Run upgrade_legacy_pv_types first, as it is testing the flag and not the manifest
'upgrade_legacy_pv_types',
];
for (final testName in runFirstTests) {
final FileSystemEntity testFile = mains.firstWhere(
(FileSystemEntity file) => file.path.contains(testName),
orElse: () => throw StateError('Could not find test file matching "$testName"'),
);
await _runTest(testFile, impellerBackend: impellerBackend, useHCPPFlag: true);
}
androidManifestXml.writeAsStringSync(
androidManifestXml.readAsStringSync().replaceFirst(
kHcppMetadataDisabled,
kHcppMetadataEnabled,
),
);
// Verify that --no-enable-hcpp disables HCPP even when the manifest enables it.
for (final testName in runFirstTests) {
final FileSystemEntity testFile = mains.firstWhere(
(FileSystemEntity file) => file.path.contains(testName),
orElse: () => throw StateError('Could not find test file matching "$testName"'),
);
await _runTest(
testFile,
impellerBackend: impellerBackend,
useHCPPFlag: false,
additionalEnvironment: const <String, String>{'EXPECT_HCPP': 'false'},
);
}
for (final file in mains) {
// This statement catches all tests inside of the
// dev/integration_tests/android_engine_test/lib/hcpp
// directory, except for upgrade_legacy_pv_types which we already ran.
if (!file.path.contains('hcpp') ||
runFirstTests.any((String name) => file.path.contains(name))) {
continue;
}
await _runTest(file, impellerBackend: impellerBackend);
}
});
}
/// Points the test app's `AndroidManifest.xml` at [impellerBackend], invokes
/// [body], and restores the original manifest afterwards.
///
/// [body] is passed the list of `*_main.dart` entrypoints discovered under
/// `lib/`, and the manifest file itself so that callers can make further edits
/// (such as toggling `EnableHcpp`) that are also reverted on completion.
Future<void> _withPatchedManifest(
ImpellerBackend impellerBackend,
Future<void> Function(List<FileSystemEntity> mains, File androidManifestXml) body,
) async {
final List<FileSystemEntity> mains = Glob('$_androidEngineTestPath/lib/**_main.dart').listSync();
final File androidManifestXml = const LocalFileSystem().file(
path.join(_androidEngineTestPath, 'android', 'app', 'src', 'main', 'AndroidManifest.xml'),
);
final String androidManifestContents = androidManifestXml.readAsStringSync();
try {
// Replace whatever the current backend is with the specified backend.
final impellerBackendMetadata = RegExp(_impellerBackendMetadata(value: '.*'));
androidManifestXml.writeAsStringSync(
androidManifestContents.replaceFirst(
impellerBackendMetadata,
_impellerBackendMetadata(value: impellerBackend.name),
),
);
await body(mains, androidManifestXml);
} finally {
// Restore original contents.
androidManifestXml.writeAsStringSync(androidManifestContents);
}
}
Future<void> _runTest(
FileSystemEntity file, {
required ImpellerBackend impellerBackend,
bool? useHCPPFlag,
Map<String, String>? additionalEnvironment,
}) async {
final CommandResult result = await runCommand(
'flutter',
<String>[
'drive',
path.relative(file.path, from: _androidEngineTestPath),
// There are no reason to enable development flags for this test.
// Disable them to work around flakiness issues, and in general just
// make less things start up unnecessarily.
'--no-dds',
'--no-enable-dart-profiling',
if (useHCPPFlag == true) '--enable-hcpp',
if (useHCPPFlag == false) '--no-enable-hcpp',
'--test-arguments=test',
'--test-arguments=--reporter=expanded',
],
workingDirectory: _androidEngineTestPath,
environment: <String, String>{
'ANDROID_ENGINE_TEST_GOLDEN_VARIANT': impellerBackend.name,
...?additionalEnvironment,
},
);
// TODO(matanlurey): Also assert that the backend reported on stdout matches
// [impellerBackend]. Stdout produces "Using the Impeller rendering backend
// (<backend>)", but this cannot be checked until `flutter drive` retains
// error logs. https://github.com/flutter/flutter/issues/162087
if (result.flattenedStdout == null) {
foundError(<String>['No stdout produced.']);
}
}
const String kHcppMetadataDisabled =
'<meta-data android:name="io.flutter.embedding.android.EnableHcpp" android:value="false" />';
const String kHcppMetadataEnabled =
'<meta-data android:name="io.flutter.embedding.android.EnableHcpp" android:value="true" />';
String _impellerBackendMetadata({required String value}) {
return '<meta-data android:name="io.flutter.embedding.android.ImpellerBackend" android:value="$value" />';
}
enum ImpellerBackend { vulkan, opengles }