diff --git a/dev/devicelab/bin/tasks/android_engine_flags_debug_test.dart b/dev/devicelab/bin/tasks/android_engine_flags_debug_test.dart new file mode 100644 index 00000000000..925c1a7cd87 --- /dev/null +++ b/dev/devicelab/bin/tasks/android_engine_flags_debug_test.dart @@ -0,0 +1,10 @@ +// 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:flutter_devicelab/framework/framework.dart'; +import 'package:flutter_devicelab/tasks/android_engine_flags_test.dart'; + +Future main() async { + await task(androidEngineFlagsTest('debug')); +} diff --git a/dev/devicelab/bin/tasks/android_engine_flags_release_test.dart b/dev/devicelab/bin/tasks/android_engine_flags_release_test.dart new file mode 100644 index 00000000000..6b44782fa23 --- /dev/null +++ b/dev/devicelab/bin/tasks/android_engine_flags_release_test.dart @@ -0,0 +1,10 @@ +// 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:flutter_devicelab/framework/framework.dart'; +import 'package:flutter_devicelab/tasks/android_engine_flags_test.dart'; + +Future main() async { + await task(androidEngineFlagsTest('release')); +} diff --git a/dev/devicelab/lib/framework/android_utils.dart b/dev/devicelab/lib/framework/android_utils.dart new file mode 100644 index 00000000000..e876bfd3044 --- /dev/null +++ b/dev/devicelab/lib/framework/android_utils.dart @@ -0,0 +1,48 @@ +// 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 'dart:io'; + +import 'package:path/path.dart' as path; +import 'package:xml/xml.dart'; + +void addMetadataToManifest(String testDirectory, List<(String, String?)> keyPairs) { + final String manifestPath = path.join( + testDirectory, + 'android', + 'app', + 'src', + 'main', + 'AndroidManifest.xml', + ); + final file = File(manifestPath); + + if (!file.existsSync()) { + throw Exception('AndroidManifest.xml not found at $manifestPath'); + } + + final String xmlStr = file.readAsStringSync(); + final xmlDoc = XmlDocument.parse(xmlStr); + final XmlElement applicationNode = xmlDoc.findAllElements('application').first; + + // Check if the meta-data node already exists. + for (final (String key, String? value) in keyPairs) { + final Iterable existingMetaData = applicationNode + .findAllElements('meta-data') + .where((XmlElement node) => node.getAttribute('android:name') == key); + + if (existingMetaData.isNotEmpty) { + final XmlElement existingEntry = existingMetaData.first; + existingEntry.setAttribute('android:value', value); + } else { + final metaData = XmlElement(XmlName('meta-data'), [ + XmlAttribute(XmlName('android:name'), key), + if (value != null) XmlAttribute(XmlName('android:value'), value), + ]); + applicationNode.children.add(metaData); + } + } + + file.writeAsStringSync(xmlDoc.toXmlString(pretty: true, indent: ' ')); +} diff --git a/dev/devicelab/lib/tasks/android_engine_flags_test.dart b/dev/devicelab/lib/tasks/android_engine_flags_test.dart new file mode 100644 index 00000000000..6fb9815b31d --- /dev/null +++ b/dev/devicelab/lib/tasks/android_engine_flags_test.dart @@ -0,0 +1,257 @@ +// 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 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:path/path.dart' as path; + +import '../framework/android_utils.dart'; +import '../framework/framework.dart'; +import '../framework/task_result.dart'; +import '../framework/utils.dart'; + +typedef TestFunction = Future Function(); + +TaskFunction androidEngineFlagsTest(String buildMode) { + final isReleaseMode = buildMode == 'release'; + final List tests = [ + _testInvalidFlag(buildMode), + if (isReleaseMode) _testIllegalFlagInReleaseMode(), + if (!isReleaseMode) _testCommandLineFlagPrecedence(), + ]; + + return () async { + final List results = []; + for (final test in tests) { + final TaskResult result = await test(); + results.add(result); + if (result.failed) { + return result; + } + } + + return TaskResult.success(null); + }; +} + +TaskFunction _testInvalidFlag(String buildMode) { + return () async { + section('Create new Flutter Android app'); + final Directory tempDir = Directory.systemTemp.createTempSync( + 'android_flutter_shell_args_test.', + ); + const projectName = 'androidfluttershellargstest'; + + try { + await inDirectory(tempDir, () async { + await flutter( + 'create', + options: ['--platforms', 'android', '--org', 'io.flutter.devicelab', projectName], + ); + }); + + section('Insert metadata with valid and invalid flags into AndroidManifest.xml'); + final metadataKeyPairs = <(String, String)>[ + ( + 'io.flutter.embedding.android.AOTSharedLibraryName', + 'something/completely/and/totally/invalid.so', + ), + ('io.flutter.embedding.android.ImpellerLazyShaderInitialization', 'true'), + ]; + addMetadataToManifest(path.join(tempDir.path, projectName), metadataKeyPairs); + + section('Run Flutter Android app with modified manifest'); + final foundInvalidAotLibraryLog = Completer(); + late Process run; + + await inDirectory(path.join(tempDir.path, projectName), () async { + run = await startFlutter('run', options: ['--$buildMode', '--verbose']); + }); + + final StreamSubscription stdout = run.stdout + .transform(utf8.decoder) + .transform(const LineSplitter()) + .listen((String line) { + print('CAMILLE :$line'); + if (line.contains( + "Skipping unsafe AOT shared library name flag: something/completely/and/totally/invalid.so. Please ensure that the library is vetted and placed in your application's internal storage.", + )) { + foundInvalidAotLibraryLog.complete(true); + } + }); + + section('Check that warning log for invalid AOT shared library name is in STDOUT'); + final Object result = await Future.any(>[ + foundInvalidAotLibraryLog.future, + run.exitCode, + ]); + + if (result is int) { + throw Exception('flutter run failed, exitCode=$result'); + } + + section('Stop listening to STDOUT'); + await stdout.cancel(); + run.kill(); + + return TaskResult.success(null); + } on TaskResult catch (taskResult) { + return taskResult; + } catch (e, stackTrace) { + print('Task exception stack trace:\n$stackTrace'); + return TaskResult.failure(e.toString()); + } finally { + rmTree(tempDir); + } + }; +} + +TaskFunction _testIllegalFlagInReleaseMode() { + return () async { + section('Create new Flutter Android app'); + final Directory tempDir = Directory.systemTemp.createTempSync( + 'android_flutter_shell_args_test.', + ); + const projectName = 'androidfluttershellargstest'; + + try { + await inDirectory(tempDir, () async { + await flutter( + 'create', + options: ['--platforms', 'android', '--org', 'io.flutter.devicelab', projectName], + ); + }); + + section('Insert metadata only allowed in release mode for testing into AndroidManifest.xml'); + final metadataKeyPairs = <(String, String)>[ + ('io.flutter.embedding.android.UseTestFonts', 'true'), + ]; + addMetadataToManifest(path.join(tempDir.path, projectName), metadataKeyPairs); + + section('Run Flutter Android app with modified manifest'); + final foundUseTestFontsLog = Completer(); + late Process run; + + await inDirectory(path.join(tempDir.path, projectName), () async { + run = await startFlutter('run', options: ['--release', '--verbose']); + }); + + final StreamSubscription stdout = run.stdout + .transform(utf8.decoder) + .transform(const LineSplitter()) + .listen((String line) { + if (line.contains( + 'Flag with metadata key io.flutter.embedding.android.UseTestFonts is not allowed in release builds and will be ignored if specified in the application manifest or via the command line.', + )) { + foundUseTestFontsLog.complete(true); + } + }); + + section('Check that warning log for disallowed UseTestFonts flag is in STDOUT'); + final Object result = await Future.any(>[ + foundUseTestFontsLog.future, + run.exitCode, + ]); + + if (result is int) { + throw Exception('flutter run failed, exitCode=$result'); + } + + section('Stop listening to STDOUT'); + await stdout.cancel(); + run.kill(); + + return TaskResult.success(null); + } on TaskResult catch (taskResult) { + return taskResult; + } catch (e, stackTrace) { + print('Task exception stack trace:\n$stackTrace'); + return TaskResult.failure(e.toString()); + } finally { + rmTree(tempDir); + } + }; +} + +TaskFunction _testCommandLineFlagPrecedence() { + return () async { + section('Create new Flutter Android app'); + final Directory tempDir = Directory.systemTemp.createTempSync( + 'android_flutter_shell_args_test.', + ); + const projectName = 'androidfluttershellargstest'; + + try { + await inDirectory(tempDir, () async { + await flutter( + 'create', + options: ['--platforms', 'android', '--org', 'io.flutter.devicelab', projectName], + ); + }); + + section('Insert metadata for test flag into the manifest'); + final metadataKeyPairs = <(String, String?)>[('io.flutter.embedding.android.TestFlag', null)]; + + addMetadataToManifest(path.join(tempDir.path, projectName), metadataKeyPairs); + + section('Run Flutter Android app with modified manifest and --test-flag'); + final metadataLineFoundFirst = Completer(); + late Process run; + + await inDirectory(path.join(tempDir.path, projectName), () async { + run = await startFlutter('run', options: ['--test-flag', '--verbose']); + }); + + final StreamSubscription stdoutSubscription = run.stdout + .transform(utf8.decoder) + .transform(const LineSplitter()) + .listen((String line) { + if (metadataLineFoundFirst.isCompleted) { + return; + } else if (line.contains( + 'For testing purposes only: test flag specified on the command line was loaded by the FlutterLoader.', + )) { + metadataLineFoundFirst.complete(false); + } else if (line.contains( + 'For testing purposes only: test flag specified in the manifest was loaded by the FlutterLoader.', + )) { + metadataLineFoundFirst.complete(true); + } + }); + + section('Check that the test flag logs are found in the expected order in STDOUT'); + final Future assetsContentFoundFuture = metadataLineFoundFirst.future; + final Object result = await Future.any(>[ + assetsContentFoundFuture, + run.exitCode, + ]); + + if (result is int) { + throw Exception('flutter run failed, exitCode=$result'); + } else if (result is bool) { + if (!result) { + throw Exception( + 'Test flags specified in the manifest unexpectedly took precedence over that specified on the command line.', + ); + } + } + + section('Kill the app'); + await stdoutSubscription.cancel(); + run.kill(); + await run.exitCode; + + return TaskResult.success(null); + } on TaskResult catch (taskResult) { + return taskResult; + } catch (e, stackTrace) { + print('Task exception stack trace:\n$stackTrace'); + return TaskResult.failure(e.toString()); + } finally { + rmTree(tempDir); + } + }; +} diff --git a/dev/devicelab/lib/tasks/perf_tests.dart b/dev/devicelab/lib/tasks/perf_tests.dart index bed49b17deb..ae3a55dea48 100644 --- a/dev/devicelab/lib/tasks/perf_tests.dart +++ b/dev/devicelab/lib/tasks/perf_tests.dart @@ -13,6 +13,7 @@ import 'package:meta/meta.dart'; import 'package:path/path.dart' as path; import 'package:xml/xml.dart'; +import '../framework/android_utils.dart'; import '../framework/devices.dart'; import '../framework/framework.dart'; import '../framework/host_agent.dart'; @@ -820,58 +821,18 @@ Future _resetPlist(String testDirectory) async { await exec('git', ['checkout', file.path]); } -void _addMetadataToManifest(String testDirectory, List<(String, String)> keyPairs) { - final String manifestPath = path.join( - testDirectory, - 'android', - 'app', - 'src', - 'main', - 'AndroidManifest.xml', - ); - final file = File(manifestPath); - - if (!file.existsSync()) { - throw Exception('AndroidManifest.xml not found at $manifestPath'); - } - - final String xmlStr = file.readAsStringSync(); - final xmlDoc = XmlDocument.parse(xmlStr); - final XmlElement applicationNode = xmlDoc.findAllElements('application').first; - - // Check if the meta-data node already exists. - for (final (String key, String value) in keyPairs) { - final Iterable existingMetaData = applicationNode - .findAllElements('meta-data') - .where((XmlElement node) => node.getAttribute('android:name') == key); - - if (existingMetaData.isNotEmpty) { - final XmlElement existingEntry = existingMetaData.first; - existingEntry.setAttribute('android:value', value); - } else { - final metaData = XmlElement(XmlName('meta-data'), [ - XmlAttribute(XmlName('android:name'), key), - XmlAttribute(XmlName('android:value'), value), - ]); - applicationNode.children.add(metaData); - } - } - - file.writeAsStringSync(xmlDoc.toXmlString(pretty: true, indent: ' ')); -} - void _addSurfaceControlSupportToManifest(String testDirectory) { final keyPairs = <(String, String)>[ ('io.flutter.embedding.android.EnableSurfaceControl', 'true'), ]; - _addMetadataToManifest(testDirectory, keyPairs); + addMetadataToManifest(testDirectory, keyPairs); } void _addMergedPlatformThreadSupportToManifest(String testDirectory) { final keyPairs = <(String, String)>[ ('io.flutter.embedding.android.EnableMergedPlatformUIThread', 'true'), ]; - _addMetadataToManifest(testDirectory, keyPairs); + addMetadataToManifest(testDirectory, keyPairs); } /// Opens the file at testDirectory + 'android/app/src/main/AndroidManifest.xml' @@ -882,7 +843,7 @@ void _addVulkanGPUTracingToManifest(String testDirectory) { final keyPairs = <(String, String)>[ ('io.flutter.embedding.android.EnableVulkanGPUTracing', 'true'), ]; - _addMetadataToManifest(testDirectory, keyPairs); + addMetadataToManifest(testDirectory, keyPairs); } /// Opens the file at testDirectory + 'android/app/src/main/AndroidManifest.xml' @@ -893,7 +854,7 @@ void _addLazyShaderMode(String testDirectory) { final keyPairs = <(String, String)>[ ('io.flutter.embedding.android.ImpellerLazyShaderInitialization', 'true'), ]; - _addMetadataToManifest(testDirectory, keyPairs); + addMetadataToManifest(testDirectory, keyPairs); } /// Opens the file at testDirectory + 'android/app/src/main/AndroidManifest.xml' @@ -909,7 +870,7 @@ void _addOpenGLESToManifest(String testDirectory) { ('io.flutter.embedding.android.ImpellerBackend', 'opengles'), ('io.flutter.embedding.android.EnableOpenGLGPUTracing', 'true'), ]; - _addMetadataToManifest(testDirectory, keyPairs); + addMetadataToManifest(testDirectory, keyPairs); } Future _resetManifest(String testDirectory) async {