From c2f5bf99f1d8997cd4ef5e402047d88bdfd36c1c Mon Sep 17 00:00:00 2001 From: Greg Spencer Date: Wed, 15 Mar 2023 18:01:03 -0700 Subject: [PATCH] Add macos project auto migration code for FlutterApplication (#122336) Add macos project auto migration code for FlutterApplication --- .../complex_layout/macos/Runner/Info.plist | 2 +- .../macrobenchmarks/macos/Runner/Info.plist | 2 +- .../channels/macos/Runner/Info.plist | 2 +- .../flavors/macos/Runner/Info.plist | 2 +- .../flutter_gallery/macos/Runner/Info.plist | 2 +- .../ui/macos/Runner/Info.plist | 2 +- dev/manual_tests/macos/Runner/Info.plist | 2 +- examples/api/macos/Runner/Info.plist | 2 +- examples/flutter_view/macos/Runner/Info.plist | 2 +- examples/hello_world/macos/Runner/Info.plist | 2 +- examples/image_list/macos/Runner/Info.plist | 2 +- examples/layers/macos/Runner/Info.plist | 2 +- .../platform_view/macos/Runner/Info.plist | 2 +- .../lib/src/ios/plist_parser.dart | 43 +++++++- .../lib/src/macos/build_macos.dart | 2 + .../flutter_application_migration.dart | 48 +++++++++ .../app_shared/macos.tmpl/Runner/Info.plist | 2 +- .../macos/macos_project_migration_test.dart | 101 +++++++++++++++++- .../integration.shard/plist_parser_test.dart | 82 ++++++++++++-- packages/flutter_tools/test/src/fakes.dart | 10 ++ 20 files changed, 285 insertions(+), 29 deletions(-) create mode 100644 packages/flutter_tools/lib/src/macos/migrations/flutter_application_migration.dart diff --git a/dev/benchmarks/complex_layout/macos/Runner/Info.plist b/dev/benchmarks/complex_layout/macos/Runner/Info.plist index 4789daa6a44..55c6534ef34 100644 --- a/dev/benchmarks/complex_layout/macos/Runner/Info.plist +++ b/dev/benchmarks/complex_layout/macos/Runner/Info.plist @@ -27,6 +27,6 @@ NSMainNibFile MainMenu NSPrincipalClass - NSApplication + FlutterApplication diff --git a/dev/benchmarks/macrobenchmarks/macos/Runner/Info.plist b/dev/benchmarks/macrobenchmarks/macos/Runner/Info.plist index 4789daa6a44..55c6534ef34 100644 --- a/dev/benchmarks/macrobenchmarks/macos/Runner/Info.plist +++ b/dev/benchmarks/macrobenchmarks/macos/Runner/Info.plist @@ -27,6 +27,6 @@ NSMainNibFile MainMenu NSPrincipalClass - NSApplication + FlutterApplication diff --git a/dev/integration_tests/channels/macos/Runner/Info.plist b/dev/integration_tests/channels/macos/Runner/Info.plist index 4789daa6a44..55c6534ef34 100644 --- a/dev/integration_tests/channels/macos/Runner/Info.plist +++ b/dev/integration_tests/channels/macos/Runner/Info.plist @@ -27,6 +27,6 @@ NSMainNibFile MainMenu NSPrincipalClass - NSApplication + FlutterApplication diff --git a/dev/integration_tests/flavors/macos/Runner/Info.plist b/dev/integration_tests/flavors/macos/Runner/Info.plist index e6e728b5521..eba8f244a53 100644 --- a/dev/integration_tests/flavors/macos/Runner/Info.plist +++ b/dev/integration_tests/flavors/macos/Runner/Info.plist @@ -29,6 +29,6 @@ NSMainNibFile MainMenu NSPrincipalClass - NSApplication + FlutterApplication diff --git a/dev/integration_tests/flutter_gallery/macos/Runner/Info.plist b/dev/integration_tests/flutter_gallery/macos/Runner/Info.plist index 4789daa6a44..55c6534ef34 100644 --- a/dev/integration_tests/flutter_gallery/macos/Runner/Info.plist +++ b/dev/integration_tests/flutter_gallery/macos/Runner/Info.plist @@ -27,6 +27,6 @@ NSMainNibFile MainMenu NSPrincipalClass - NSApplication + FlutterApplication diff --git a/dev/integration_tests/ui/macos/Runner/Info.plist b/dev/integration_tests/ui/macos/Runner/Info.plist index 4789daa6a44..55c6534ef34 100644 --- a/dev/integration_tests/ui/macos/Runner/Info.plist +++ b/dev/integration_tests/ui/macos/Runner/Info.plist @@ -27,6 +27,6 @@ NSMainNibFile MainMenu NSPrincipalClass - NSApplication + FlutterApplication diff --git a/dev/manual_tests/macos/Runner/Info.plist b/dev/manual_tests/macos/Runner/Info.plist index 4789daa6a44..55c6534ef34 100644 --- a/dev/manual_tests/macos/Runner/Info.plist +++ b/dev/manual_tests/macos/Runner/Info.plist @@ -27,6 +27,6 @@ NSMainNibFile MainMenu NSPrincipalClass - NSApplication + FlutterApplication diff --git a/examples/api/macos/Runner/Info.plist b/examples/api/macos/Runner/Info.plist index 4789daa6a44..55c6534ef34 100644 --- a/examples/api/macos/Runner/Info.plist +++ b/examples/api/macos/Runner/Info.plist @@ -27,6 +27,6 @@ NSMainNibFile MainMenu NSPrincipalClass - NSApplication + FlutterApplication diff --git a/examples/flutter_view/macos/Runner/Info.plist b/examples/flutter_view/macos/Runner/Info.plist index 4789daa6a44..55c6534ef34 100644 --- a/examples/flutter_view/macos/Runner/Info.plist +++ b/examples/flutter_view/macos/Runner/Info.plist @@ -27,6 +27,6 @@ NSMainNibFile MainMenu NSPrincipalClass - NSApplication + FlutterApplication diff --git a/examples/hello_world/macos/Runner/Info.plist b/examples/hello_world/macos/Runner/Info.plist index 4789daa6a44..55c6534ef34 100644 --- a/examples/hello_world/macos/Runner/Info.plist +++ b/examples/hello_world/macos/Runner/Info.plist @@ -27,6 +27,6 @@ NSMainNibFile MainMenu NSPrincipalClass - NSApplication + FlutterApplication diff --git a/examples/image_list/macos/Runner/Info.plist b/examples/image_list/macos/Runner/Info.plist index 4789daa6a44..55c6534ef34 100644 --- a/examples/image_list/macos/Runner/Info.plist +++ b/examples/image_list/macos/Runner/Info.plist @@ -27,6 +27,6 @@ NSMainNibFile MainMenu NSPrincipalClass - NSApplication + FlutterApplication diff --git a/examples/layers/macos/Runner/Info.plist b/examples/layers/macos/Runner/Info.plist index 4789daa6a44..55c6534ef34 100644 --- a/examples/layers/macos/Runner/Info.plist +++ b/examples/layers/macos/Runner/Info.plist @@ -27,6 +27,6 @@ NSMainNibFile MainMenu NSPrincipalClass - NSApplication + FlutterApplication diff --git a/examples/platform_view/macos/Runner/Info.plist b/examples/platform_view/macos/Runner/Info.plist index 4789daa6a44..55c6534ef34 100644 --- a/examples/platform_view/macos/Runner/Info.plist +++ b/examples/platform_view/macos/Runner/Info.plist @@ -27,6 +27,6 @@ NSMainNibFile MainMenu NSPrincipalClass - NSApplication + FlutterApplication diff --git a/packages/flutter_tools/lib/src/ios/plist_parser.dart b/packages/flutter_tools/lib/src/ios/plist_parser.dart index f10aa4554c1..c12de5e3ea7 100644 --- a/packages/flutter_tools/lib/src/ios/plist_parser.dart +++ b/packages/flutter_tools/lib/src/ios/plist_parser.dart @@ -30,6 +30,9 @@ class PlistParser { static const String kCFBundleVersionKey = 'CFBundleVersion'; static const String kCFBundleDisplayNameKey = 'CFBundleDisplayName'; static const String kMinimumOSVersionKey = 'MinimumOSVersion'; + static const String kNSPrincipalClassKey = 'NSPrincipalClass'; + + static const String _plutilExecutable = '/usr/bin/plutil'; /// Returns the content, converted to XML, of the plist file located at /// [plistFilePath]. @@ -39,12 +42,11 @@ class PlistParser { /// /// The [plistFilePath] argument must not be null. String? plistXmlContent(String plistFilePath) { - const String executable = '/usr/bin/plutil'; - if (!_fileSystem.isFileSync(executable)) { - throw const FileNotFoundException(executable); + if (!_fileSystem.isFileSync(_plutilExecutable)) { + throw const FileNotFoundException(_plutilExecutable); } final List args = [ - executable, '-convert', 'xml1', '-o', '-', plistFilePath, + _plutilExecutable, '-convert', 'xml1', '-o', '-', plistFilePath, ]; try { final String xmlContent = _processUtils.runSync( @@ -53,11 +55,42 @@ class PlistParser { ).stdout.trim(); return xmlContent; } on ProcessException catch (error) { - _logger.printTrace('$error'); + _logger.printError('$error'); return null; } } + /// Replaces the string key in the given plist file with the given value. + /// + /// If the value is null, then the key will be removed. + /// + /// Returns true if successful. + bool replaceKey(String plistFilePath, {required String key, String? value }) { + if (!_fileSystem.isFileSync(_plutilExecutable)) { + throw const FileNotFoundException(_plutilExecutable); + } + final List args; + if (value == null) { + args = [ + _plutilExecutable, '-remove', key, plistFilePath, + ]; + } else { + args = [ + _plutilExecutable, '-replace', key, '-string', value, plistFilePath, + ]; + } + try { + _processUtils.runSync( + args, + throwOnError: true, + ); + } on ProcessException catch (error) { + _logger.printError('$error'); + return false; + } + return true; + } + /// Parses the plist file located at [plistFilePath] and returns the /// associated map of key/value property list pairs. /// diff --git a/packages/flutter_tools/lib/src/macos/build_macos.dart b/packages/flutter_tools/lib/src/macos/build_macos.dart index 6b9c39eb862..85bf8369d50 100644 --- a/packages/flutter_tools/lib/src/macos/build_macos.dart +++ b/packages/flutter_tools/lib/src/macos/build_macos.dart @@ -17,6 +17,7 @@ import '../migrations/xcode_script_build_phase_migration.dart'; import '../migrations/xcode_thin_binary_build_phase_input_paths_migration.dart'; import '../project.dart'; import 'cocoapod_utils.dart'; +import 'migrations/flutter_application_migration.dart'; import 'migrations/macos_deployment_target_migration.dart'; import 'migrations/remove_macos_framework_link_and_embedding_migration.dart'; @@ -57,6 +58,7 @@ Future buildMacOS({ XcodeProjectObjectVersionMigration(flutterProject.macos, globals.logger), XcodeScriptBuildPhaseMigration(flutterProject.macos, globals.logger), XcodeThinBinaryBuildPhaseInputPathsMigration(flutterProject.macos, globals.logger), + FlutterApplicationMigration(flutterProject.macos, globals.logger), ]; final ProjectMigration migration = ProjectMigration(migrators); diff --git a/packages/flutter_tools/lib/src/macos/migrations/flutter_application_migration.dart b/packages/flutter_tools/lib/src/macos/migrations/flutter_application_migration.dart new file mode 100644 index 00000000000..14ea4d64ffe --- /dev/null +++ b/packages/flutter_tools/lib/src/macos/migrations/flutter_application_migration.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 '../../base/file_system.dart'; +import '../../base/project_migrator.dart'; +import '../../globals.dart' as globals; +import '../../ios/plist_parser.dart'; +import '../../xcode_project.dart'; + +/// Update the minimum macOS deployment version to the minimum allowed by Xcode without causing a warning. +class FlutterApplicationMigration extends ProjectMigrator { + FlutterApplicationMigration( + MacOSProject project, + super.logger, + ) : _infoPlistFile = project.defaultHostInfoPlist; + + final File _infoPlistFile; + + @override + void migrate() { + if (_infoPlistFile.existsSync()) { + final String? principleClass = + globals.plistParser.getStringValueFromFile(_infoPlistFile.path, PlistParser.kNSPrincipalClassKey); + if (principleClass == null || principleClass == 'FlutterApplication') { + // No NSPrincipalClass defined, or already converted, so no migration + // needed. + return; + } + if (principleClass != 'NSApplication') { + // Only replace NSApplication values, since we don't know why they might + // have substituted something else. + logger.printTrace('${_infoPlistFile.basename} has an ' + '${PlistParser.kNSPrincipalClassKey} of $principleClass, not ' + 'NSApplication, skipping FlutterApplication migration.\nYou will need ' + 'to modify your application class to derive from FlutterApplication.'); + return; + } + logger.printStatus('Updating ${_infoPlistFile.basename} to use FlutterApplication instead of NSApplication.'); + final bool success = globals.plistParser.replaceKey(_infoPlistFile.path, key: PlistParser.kNSPrincipalClassKey, value: 'FlutterApplication'); + if (!success) { + logger.printError('Updating ${_infoPlistFile.basename} failed.'); + } + } else { + logger.printTrace('${_infoPlistFile.basename} not found, skipping FlutterApplication migration.'); + } + } +} diff --git a/packages/flutter_tools/templates/app_shared/macos.tmpl/Runner/Info.plist b/packages/flutter_tools/templates/app_shared/macos.tmpl/Runner/Info.plist index 4789daa6a44..55c6534ef34 100644 --- a/packages/flutter_tools/templates/app_shared/macos.tmpl/Runner/Info.plist +++ b/packages/flutter_tools/templates/app_shared/macos.tmpl/Runner/Info.plist @@ -27,6 +27,6 @@ NSMainNibFile MainMenu NSPrincipalClass - NSApplication + FlutterApplication diff --git a/packages/flutter_tools/test/general.shard/macos/macos_project_migration_test.dart b/packages/flutter_tools/test/general.shard/macos/macos_project_migration_test.dart index 2b8491d751d..cfcd32e623c 100644 --- a/packages/flutter_tools/test/general.shard/macos/macos_project_migration_test.dart +++ b/packages/flutter_tools/test/general.shard/macos/macos_project_migration_test.dart @@ -5,13 +5,17 @@ import 'package:file/file.dart'; import 'package:file/memory.dart'; import 'package:flutter_tools/src/base/logger.dart'; +import 'package:flutter_tools/src/ios/plist_parser.dart'; +import 'package:flutter_tools/src/macos/migrations/flutter_application_migration.dart'; import 'package:flutter_tools/src/macos/migrations/macos_deployment_target_migration.dart'; import 'package:flutter_tools/src/macos/migrations/remove_macos_framework_link_and_embedding_migration.dart'; +import 'package:flutter_tools/src/project.dart'; import 'package:flutter_tools/src/reporting/reporting.dart'; -import 'package:flutter_tools/src/xcode_project.dart'; import 'package:test/fake.dart'; import '../../src/common.dart'; +import '../../src/context.dart'; +import '../../src/fakes.dart'; void main() { group('remove link and embed migration', () { @@ -275,12 +279,107 @@ platform :osx, '10.14' expect('Updating minimum macOS deployment target to 10.14'.allMatches(testLogger.statusText).length, 1); }); }); + + group('update NSPrincipalClass to FlutterApplication', () { + late MemoryFileSystem memoryFileSystem; + late BufferLogger testLogger; + late FakeMacOSProject project; + late File infoPlistFile; + late FakePlistParser fakePlistParser; + late FlutterProjectFactory flutterProjectFactory; + + setUp(() { + memoryFileSystem = MemoryFileSystem(); + fakePlistParser = FakePlistParser(); + testLogger = BufferLogger.test(); + project = FakeMacOSProject(); + infoPlistFile = memoryFileSystem.file('Info.plist'); + project.defaultHostInfoPlist = infoPlistFile; + flutterProjectFactory = FlutterProjectFactory( + fileSystem: memoryFileSystem, + logger: testLogger, + ); + }); + + void testWithMocks(String description, Future Function() testMethod) { + testUsingContext(description, testMethod, overrides: { + FileSystem: () => memoryFileSystem, + ProcessManager: () => FakeProcessManager.any(), + PlistParser: () => fakePlistParser, + FlutterProjectFactory: () => flutterProjectFactory, + }); + } + + testWithMocks('skipped if files are missing', () async { + final FlutterApplicationMigration macOSProjectMigration = FlutterApplicationMigration( + project, + testLogger, + ); + macOSProjectMigration.migrate(); + expect(infoPlistFile.existsSync(), isFalse); + + expect(testLogger.traceText, contains('${infoPlistFile.basename} not found, skipping FlutterApplication migration.')); + expect(testLogger.statusText, isEmpty); + }); + + testWithMocks('skipped if no NSPrincipalClass key exists to upgrade', () async { + final FlutterApplicationMigration macOSProjectMigration = FlutterApplicationMigration( + project, + testLogger, + ); + infoPlistFile.writeAsStringSync('contents'); // Just so it exists: parser is a fake. + macOSProjectMigration.migrate(); + expect(fakePlistParser.getStringValueFromFile(infoPlistFile.path, PlistParser.kNSPrincipalClassKey), isNull); + expect(testLogger.statusText, isEmpty); + }); + + testWithMocks('skipped if already upgraded', () async { + fakePlistParser.setProperty(PlistParser.kNSPrincipalClassKey, 'FlutterApplication'); + final FlutterApplicationMigration macOSProjectMigration = FlutterApplicationMigration( + project, + testLogger, + ); + infoPlistFile.writeAsStringSync('contents'); // Just so it exists: parser is a fake. + macOSProjectMigration.migrate(); + expect(fakePlistParser.getStringValueFromFile(infoPlistFile.path, PlistParser.kNSPrincipalClassKey), 'FlutterApplication'); + expect(testLogger.statusText, isEmpty); + }); + + testWithMocks('Info.plist migrated to use FlutterApplication', () async { + fakePlistParser.setProperty(PlistParser.kNSPrincipalClassKey, 'NSApplication'); + final FlutterApplicationMigration macOSProjectMigration = FlutterApplicationMigration( + project, + testLogger, + ); + infoPlistFile.writeAsStringSync('contents'); // Just so it exists: parser is a fake. + macOSProjectMigration.migrate(); + expect(fakePlistParser.getStringValueFromFile(infoPlistFile.path, PlistParser.kNSPrincipalClassKey), 'FlutterApplication'); + // Only print once. + expect('Updating ${infoPlistFile.basename} to use FlutterApplication instead of NSApplication.'.allMatches(testLogger.statusText).length, 1); + }); + + testWithMocks('Skip if NSPrincipalClass is not NSApplication', () async { + const String differentApp = 'DIFFERENTApplication'; + fakePlistParser.setProperty(PlistParser.kNSPrincipalClassKey, differentApp); + final FlutterApplicationMigration macOSProjectMigration = FlutterApplicationMigration( + project, + testLogger, + ); + infoPlistFile.writeAsStringSync('contents'); // Just so it exists: parser is a fake. + macOSProjectMigration.migrate(); + expect(fakePlistParser.getStringValueFromFile(infoPlistFile.path, PlistParser.kNSPrincipalClassKey), differentApp); + expect(testLogger.traceText, contains('${infoPlistFile.basename} has an ${PlistParser.kNSPrincipalClassKey} of $differentApp, not NSApplication, skipping FlutterApplication migration')); + }); + }); } class FakeMacOSProject extends Fake implements MacOSProject { @override File xcodeProjectInfoFile = MemoryFileSystem.test().file('xcodeProjectInfoFile'); + @override + File defaultHostInfoPlist = MemoryFileSystem.test().file('InfoplistFile'); + @override File podfile = MemoryFileSystem.test().file('Podfile'); } diff --git a/packages/flutter_tools/test/integration.shard/plist_parser_test.dart b/packages/flutter_tools/test/integration.shard/plist_parser_test.dart index dc8d2e8fa56..9f176dc0117 100644 --- a/packages/flutter_tools/test/integration.shard/plist_parser_test.dart +++ b/packages/flutter_tools/test/integration.shard/plist_parser_test.dart @@ -74,7 +74,7 @@ void main() { file.deleteSync(); }); - testWithoutContext('PlistParser.getStringValueFromFile works with xml file', () { + testWithoutContext('PlistParser.getStringValueFromFile works with an XML file', () { file.writeAsBytesSync(base64.decode(base64PlistXml)); expect(parser.getStringValueFromFile(file.path, 'CFBundleIdentifier'), 'io.flutter.flutter.app'); @@ -83,7 +83,7 @@ void main() { expect(logger.errorText, isEmpty); }, skip: !platform.isMacOS); // [intended] requires macos tool chain. - testWithoutContext('PlistParser.getStringValueFromFile works with binary file', () { + testWithoutContext('PlistParser.getStringValueFromFile works with a binary file', () { file.writeAsBytesSync(base64.decode(base64PlistBinary)); expect(parser.getStringValueFromFile(file.path, 'CFBundleIdentifier'), 'io.flutter.flutter.app'); @@ -92,7 +92,7 @@ void main() { expect(logger.errorText, isEmpty); }, skip: !platform.isMacOS); // [intended] requires macos tool chain. - testWithoutContext('PlistParser.getStringValueFromFile works with json file', () { + testWithoutContext('PlistParser.getStringValueFromFile works with a JSON file', () { file.writeAsBytesSync(base64.decode(base64PlistJson)); expect(parser.getStringValueFromFile(file.path, 'CFBundleIdentifier'), 'io.flutter.flutter.app'); @@ -101,13 +101,13 @@ void main() { expect(logger.errorText, isEmpty); }, skip: !platform.isMacOS); // [intended] requires macos tool chain. - testWithoutContext('PlistParser.getStringValueFromFile returns null for non-existent plist file', () { + testWithoutContext('PlistParser.getStringValueFromFile returns null for a non-existent plist file', () { expect(parser.getStringValueFromFile('missing.plist', 'CFBundleIdentifier'), null); expect(logger.statusText, isEmpty); expect(logger.errorText, isEmpty); }, skip: !platform.isMacOS); // [intended] requires macos tool chain. - testWithoutContext('PlistParser.getStringValueFromFile returns null for non-existent key within plist', () { + testWithoutContext('PlistParser.getStringValueFromFile returns null for a non-existent key within a plist', () { file.writeAsBytesSync(base64.decode(base64PlistXml)); expect(parser.getStringValueFromFile(file.path, 'BadKey'), null); @@ -116,12 +116,15 @@ void main() { expect(logger.errorText, isEmpty); }, skip: !platform.isMacOS); // [intended] requires macos tool chain. - testWithoutContext('PlistParser.getStringValueFromFile returns null for malformed plist file', () { + testWithoutContext('PlistParser.getStringValueFromFile returns null for a malformed plist file', () { file.writeAsBytesSync(const [1, 2, 3, 4, 5, 6]); expect(parser.getStringValueFromFile(file.path, 'CFBundleIdentifier'), null); - expect(logger.statusText, isNotEmpty); - expect(logger.errorText, isEmpty); + expect(logger.statusText, contains('Property List error: Unexpected character \x01 at line 1 / ' + 'JSON error: JSON text did not start with array or object and option to allow fragments not ' + 'set. around line 1, column 0.\n')); + expect(logger.errorText, 'ProcessException: The command failed\n' + ' Command: /usr/bin/plutil -convert xml1 -o - ${file.absolute.path}\n'); }, skip: !platform.isMacOS); // [intended] requires macos tool chain. testWithoutContext('PlistParser.getStringValueFromFile throws when /usr/bin/plutil is not found', () async { @@ -133,7 +136,68 @@ void main() { ); expect(logger.statusText, isEmpty); expect(logger.errorText, isEmpty); - }, skip: platform.isMacOS); // [intended] requires macos tool chain. + }, skip: platform.isMacOS); // [intended] requires absence of macos tool chain. + + testWithoutContext('PlistParser.replaceKey can replace a key', () async { + file.writeAsBytesSync(base64.decode(base64PlistXml)); + + expect(parser.getStringValueFromFile(file.path, 'CFBundleIdentifier'), 'io.flutter.flutter.app'); + expect(parser.replaceKey(file.path, key: 'CFBundleIdentifier', value: 'dev.flutter.fake'), isTrue); + expect(logger.statusText, isEmpty); + expect(logger.errorText, isEmpty); + expect(parser.getStringValueFromFile(file.path, 'CFBundleIdentifier'), equals('dev.flutter.fake')); + }, skip: !platform.isMacOS); // [intended] requires macos tool chain. + + testWithoutContext('PlistParser.replaceKey can create a new key', () async { + file.writeAsBytesSync(base64.decode(base64PlistXml)); + + expect(parser.getStringValueFromFile(file.path, 'CFNewKey'), isNull); + expect(parser.replaceKey(file.path, key: 'CFNewKey', value: 'dev.flutter.fake'), isTrue); + expect(logger.statusText, isEmpty); + expect(logger.errorText, isEmpty); + expect(parser.getStringValueFromFile(file.path, 'CFNewKey'), equals('dev.flutter.fake')); + }, skip: !platform.isMacOS); // [intended] requires macos tool chain. + + testWithoutContext('PlistParser.replaceKey can delete a key', () async { + file.writeAsBytesSync(base64.decode(base64PlistXml)); + + expect(parser.replaceKey(file.path, key: 'CFBundleIdentifier'), isTrue); + expect(logger.statusText, isEmpty); + expect(logger.errorText, isEmpty); + expect(parser.getStringValueFromFile(file.path, 'CFBundleIdentifier'), isNull); + }, skip: !platform.isMacOS); // [intended] requires macos tool chain. + + testWithoutContext('PlistParser.replaceKey throws when /usr/bin/plutil is not found', () async { + file.writeAsBytesSync(base64.decode(base64PlistXml)); + + expect( + () => parser.replaceKey(file.path, key: 'CFBundleIdentifier', value: 'dev.flutter.fake'), + throwsA(isA()), + ); + expect(logger.statusText, isEmpty); + expect(logger.errorText, isEmpty); + }, skip: platform.isMacOS); // [intended] requires absence of macos tool chain. + + testWithoutContext('PlistParser.replaceKey returns false for a malformed plist file', () { + file.writeAsBytesSync(const [1, 2, 3, 4, 5, 6]); + + expect(parser.replaceKey(file.path, key: 'CFBundleIdentifier', value: 'dev.flutter.fake'), isFalse); + expect(logger.statusText, contains('foo.plist: Property List error: Unexpected character \x01 ' + 'at line 1 / JSON error: JSON text did not start with array or object and option to allow ' + 'fragments not set. around line 1, column 0.\n')); + expect(logger.errorText, equals('ProcessException: The command failed\n' + ' Command: /usr/bin/plutil -replace CFBundleIdentifier -string dev.flutter.fake foo.plist\n')); + }, skip: !platform.isMacOS); // [intended] requires macos tool chain. + + testWithoutContext('PlistParser.replaceKey works with a JSON file', () { + file.writeAsBytesSync(base64.decode(base64PlistJson)); + + expect(parser.getStringValueFromFile(file.absolute.path, 'CFBundleIdentifier'), 'io.flutter.flutter.app'); + expect(parser.replaceKey(file.path, key:'CFBundleIdentifier', value: 'dev.flutter.fake'), isTrue); + expect(parser.getStringValueFromFile(file.absolute.path, 'CFBundleIdentifier'), 'dev.flutter.fake'); + expect(logger.statusText, isEmpty); + expect(logger.errorText, isEmpty); + }, skip: !platform.isMacOS); // [intended] requires macos tool chain. testWithoutContext('PlistParser.parseFile can handle different datatypes', () async { file.writeAsBytesSync(base64.decode(base64PlistXmlWithComplexDatatypes)); diff --git a/packages/flutter_tools/test/src/fakes.dart b/packages/flutter_tools/test/src/fakes.dart index 4bb5733e751..808d5c6bf88 100644 --- a/packages/flutter_tools/test/src/fakes.dart +++ b/packages/flutter_tools/test/src/fakes.dart @@ -298,6 +298,16 @@ class FakePlistParser implements PlistParser { String? getStringValueFromFile(String plistFilePath, String key) { return _underlyingValues[key] as String?; } + + @override + bool replaceKey(String plistFilePath, {required String key, String? value}) { + if (value == null) { + _underlyingValues.remove(key); + return true; + } + setProperty(key, value); + return true; + } } class FakeBotDetector implements BotDetector {