Unverified Commit b00f1c45 authored by chunhtai's avatar chunhtai Committed by GitHub

Adding vmservice to get iOS app settings (#123156)

fixes https://github.com/flutter/flutter/issues/120405
parent 529b919f
......@@ -376,11 +376,11 @@ class BuildIOSArchiveCommand extends _BuildIOSSubCommand {
final Map<String, String?> xcodeProjectSettingsMap = <String, String?>{};
xcodeProjectSettingsMap['Version Number'] = globals.plistParser.getStringValueFromFile(plistPath, PlistParser.kCFBundleShortVersionStringKey);
xcodeProjectSettingsMap['Build Number'] = globals.plistParser.getStringValueFromFile(plistPath, PlistParser.kCFBundleVersionKey);
xcodeProjectSettingsMap['Display Name'] = globals.plistParser.getStringValueFromFile(plistPath, PlistParser.kCFBundleDisplayNameKey);
xcodeProjectSettingsMap['Deployment Target'] = globals.plistParser.getStringValueFromFile(plistPath, PlistParser.kMinimumOSVersionKey);
xcodeProjectSettingsMap['Bundle Identifier'] = globals.plistParser.getStringValueFromFile(plistPath, PlistParser.kCFBundleIdentifierKey);
xcodeProjectSettingsMap['Version Number'] = globals.plistParser.getValueFromFile<String>(plistPath, PlistParser.kCFBundleShortVersionStringKey);
xcodeProjectSettingsMap['Build Number'] = globals.plistParser.getValueFromFile<String>(plistPath, PlistParser.kCFBundleVersionKey);
xcodeProjectSettingsMap['Display Name'] = globals.plistParser.getValueFromFile<String>(plistPath, PlistParser.kCFBundleDisplayNameKey);
xcodeProjectSettingsMap['Deployment Target'] = globals.plistParser.getValueFromFile<String>(plistPath, PlistParser.kMinimumOSVersionKey);
xcodeProjectSettingsMap['Bundle Identifier'] = globals.plistParser.getValueFromFile<String>(plistPath, PlistParser.kCFBundleIdentifierKey);
final List<ValidationMessage> validationMessages = xcodeProjectSettingsMap.entries.map((MapEntry<String, String?> entry) {
final String title = entry.key;
......
......@@ -494,7 +494,7 @@ class IntelliJValidatorOnMac extends IntelliJValidator {
@override
String get version {
return _version ??= _plistParser.getStringValueFromFile(
return _version ??= _plistParser.getValueFromFile<String>(
plistFile,
PlistParser.kCFBundleShortVersionStringKey,
) ?? 'unknown';
......@@ -508,7 +508,7 @@ class IntelliJValidatorOnMac extends IntelliJValidator {
}
final String? altLocation = _plistParser
.getStringValueFromFile(plistFile, 'JetBrainsToolboxApp');
.getValueFromFile<String>(plistFile, 'JetBrainsToolboxApp');
if (altLocation != null) {
_pluginsPath = '$altLocation.plugins';
......
......@@ -58,7 +58,7 @@ abstract class IOSApp extends ApplicationPackage {
globals.printError('Invalid prebuilt iOS app. Does not contain Info.plist.');
return null;
}
final String? id = globals.plistParser.getStringValueFromFile(
final String? id = globals.plistParser.getValueFromFile<String>(
plistPath,
PlistParser.kCFBundleIdentifierKey,
);
......
......@@ -451,7 +451,7 @@ class IOSDevice extends Device {
_logger.printError('');
return LaunchResult.failed();
}
packageId = buildResult.xcodeBuildExecution?.buildSettings['PRODUCT_BUNDLE_IDENTIFIER'];
packageId = buildResult.xcodeBuildExecution?.buildSettings[IosProject.kProductBundleIdKey];
}
packageId ??= package.id;
......
......@@ -24,6 +24,7 @@ class PlistParser {
final Logger _logger;
final ProcessUtils _processUtils;
// info.pList keys
static const String kCFBundleIdentifierKey = 'CFBundleIdentifier';
static const String kCFBundleShortVersionStringKey = 'CFBundleShortVersionString';
static const String kCFBundleExecutableKey = 'CFBundleExecutable';
......@@ -32,6 +33,9 @@ class PlistParser {
static const String kMinimumOSVersionKey = 'MinimumOSVersion';
static const String kNSPrincipalClassKey = 'NSPrincipalClass';
// entitlement file keys
static const String kAssociatedDomainsKey = 'com.apple.developer.associated-domains';
static const String _plutilExecutable = '/usr/bin/plutil';
/// Returns the content, converted to XML, of the plist file located at
......@@ -164,8 +168,8 @@ class PlistParser {
return null;
}
/// Parses the Plist file located at [plistFilePath] and returns the string
/// value that's associated with the specified [key] within the property list.
/// Parses the Plist file located at [plistFilePath] and returns the value
/// that's associated with the specified [key] within the property list.
///
/// If [plistFilePath] points to a non-existent file or a file that's not a
/// valid property list file, this will return null.
......@@ -173,8 +177,8 @@ class PlistParser {
/// If [key] is not found in the property list, this will return null.
///
/// The [plistFilePath] and [key] arguments must not be null.
String? getStringValueFromFile(String plistFilePath, String key) {
T? getValueFromFile<T>(String plistFilePath, String key) {
final Map<String, dynamic> parsed = parseFile(plistFilePath);
return parsed[key] as String?;
return parsed[key] as T?;
}
}
......@@ -468,7 +468,7 @@ class IOSSimulator extends Device {
// parsing the xcodeproj or configuration files.
// See https://github.com/flutter/flutter/issues/31037 for more information.
final String plistPath = globals.fs.path.join(package.simulatorBundlePath, 'Info.plist');
final String? bundleIdentifier = globals.plistParser.getStringValueFromFile(plistPath, PlistParser.kCFBundleIdentifierKey);
final String? bundleIdentifier = globals.plistParser.getValueFromFile<String>(plistPath, PlistParser.kCFBundleIdentifierKey);
if (bundleIdentifier == null) {
globals.printError('Invalid prebuilt iOS app. Info.plist does not contain bundle identifier');
return LaunchResult.failed();
......
......@@ -185,6 +185,7 @@ class XcodeProjectInterpreter {
final Status status = _logger.startSpinner();
final String? scheme = buildContext.scheme;
final String? configuration = buildContext.configuration;
final String? target = buildContext.target;
final String? deviceId = buildContext.deviceId;
final List<String> showBuildSettingsCommand = <String>[
...xcrunCommand(),
......@@ -195,6 +196,8 @@ class XcodeProjectInterpreter {
...<String>['-scheme', scheme],
if (configuration != null)
...<String>['-configuration', configuration],
if (target != null)
...<String>['-target', target],
if (buildContext.environmentType == EnvironmentType.simulator)
...<String>['-sdk', 'iphonesimulator'],
'-destination',
......@@ -380,6 +383,7 @@ class XcodeProjectBuildContext {
this.configuration,
this.environmentType = EnvironmentType.physical,
this.deviceId,
this.target,
this.isWatch = false,
});
......@@ -387,10 +391,11 @@ class XcodeProjectBuildContext {
final String? configuration;
final EnvironmentType environmentType;
final String? deviceId;
final String? target;
final bool isWatch;
@override
int get hashCode => Object.hash(scheme, configuration, environmentType, deviceId);
int get hashCode => Object.hash(scheme, configuration, environmentType, deviceId, target);
@override
bool operator ==(Object other) {
......@@ -402,10 +407,26 @@ class XcodeProjectBuildContext {
other.configuration == configuration &&
other.deviceId == deviceId &&
other.environmentType == environmentType &&
other.isWatch == isWatch;
other.isWatch == isWatch &&
other.target == target;
}
}
/// The settings that are relevant for setting up universal links
@immutable
class XcodeUniversalLinkSettings {
const XcodeUniversalLinkSettings({
this.bundleIdentifier,
this.teamIdentifier,
this.associatedDomains = const <String>[],
});
final String? bundleIdentifier;
final String? teamIdentifier;
final List<String> associatedDomains;
}
/// Information about an Xcode project.
///
/// Represents the output of `xcodebuild -list`.
......
......@@ -27,7 +27,7 @@ class FlutterApplicationMigration extends ProjectMigrator {
void migrate() {
if (_infoPlistFile.existsSync()) {
final String? principalClass =
globals.plistParser.getStringValueFromFile(_infoPlistFile.path, PlistParser.kNSPrincipalClassKey);
globals.plistParser.getValueFromFile<String>(_infoPlistFile.path, PlistParser.kNSPrincipalClassKey);
if (principalClass == null || principalClass == 'NSApplication') {
// No NSPrincipalClass defined, or already converted. No migration
// needed.
......
......@@ -41,6 +41,7 @@ const String kFlutterMemoryInfoServiceName = 'flutterMemoryInfo';
const String kFlutterGetSkSLServiceName = 'flutterGetSkSL';
const String kFlutterGetIOSBuildOptionsServiceName = 'flutterGetIOSBuildOptions';
const String kFlutterGetAndroidBuildVariantsServiceName = 'flutterGetAndroidBuildVariants';
const String kFlutterGetIOSDeeplinkSettingsServiceName = 'flutterGetIOSDeeplinkSettings';
/// The error response code from an unrecoverable compilation failure.
const int kIsolateReloadBarred = 1005;
......@@ -337,6 +338,25 @@ Future<vm_service.VmService> setUpVmService({
registrationRequests.add(
vmService.registerService(kFlutterGetAndroidBuildVariantsServiceName, kFlutterToolAlias),
);
vmService.registerServiceCallback(kFlutterGetIOSDeeplinkSettingsServiceName, (Map<String, Object?> params) async {
final XcodeUniversalLinkSettings settings = await flutterProject.ios.universalLinkSettings(
configuration: params['configuration']! as String,
scheme: params['scheme']! as String,
target: params['target']! as String,
);
return <String, Object>{
'result': <String, Object>{
kResultType: kResultTypeSuccess,
'bundleIdentifier': settings.bundleIdentifier ?? '',
'teamIdentifier': settings.teamIdentifier ?? '',
'associatedDomains': settings.associatedDomains,
},
};
});
registrationRequests.add(
vmService.registerService(kFlutterGetIOSDeeplinkSettingsServiceName, 'Flutter Tools'),
);
}
if (printStructuredErrorLogMethod != null) {
......
......@@ -124,8 +124,16 @@ class IosProject extends XcodeBasedProject {
@override
String get pluginConfigKey => IOSPlugin.kConfigKey;
static final RegExp _productBundleIdPattern = RegExp(r'''^\s*PRODUCT_BUNDLE_IDENTIFIER\s*=\s*(["']?)(.*?)\1;\s*$''');
static const String _productBundleIdVariable = r'$(PRODUCT_BUNDLE_IDENTIFIER)';
// build setting keys
static const String kProductBundleIdKey = 'PRODUCT_BUNDLE_IDENTIFIER';
static const String kTeamIdKey = 'DEVELOPMENT_TEAM';
static const String kEntitlementFilePathKey = 'CODE_SIGN_ENTITLEMENTS';
static const String kHostAppBundleNameKey = 'FULL_PRODUCT_NAME';
static final RegExp _productBundleIdPattern = RegExp('^\\s*$kProductBundleIdKey\\s*=\\s*(["\']?)(.*?)\\1;\\s*\$');
static const String _kProductBundleIdVariable = '\$($kProductBundleIdKey)';
static final RegExp _associatedDomainPattern = RegExp(r'^applinks:(.*)');
Directory get ephemeralModuleDirectory => parent.directory.childDirectory('.ios');
Directory get _editableDirectory => parent.directory.childDirectory('ios');
......@@ -203,24 +211,71 @@ class IosProject extends XcodeBasedProject {
return parent.isModule || _editableDirectory.existsSync();
}
Future<XcodeUniversalLinkSettings> universalLinkSettings({
required String configuration,
required String scheme,
required String target,
}) async {
final XcodeProjectBuildContext context = XcodeProjectBuildContext(
configuration: configuration,
scheme: scheme,
target: target,
);
return XcodeUniversalLinkSettings(
bundleIdentifier: await _productBundleIdentifierWithBuildContext(context),
teamIdentifier: await _getTeamIdentifier(context),
associatedDomains: await _getAssociatedDomains(context),
);
}
/// The product bundle identifier of the host app, or null if not set or if
/// iOS tooling needed to read it is not installed.
Future<String?> productBundleIdentifier(BuildInfo? buildInfo) async {
if (!existsSync()) {
return null;
}
return _productBundleIdentifier ??= await _parseProductBundleIdentifier(buildInfo);
XcodeProjectBuildContext? buildContext;
final XcodeProjectInfo? info = await projectInfo();
if (info != null) {
final String? scheme = info.schemeFor(buildInfo);
if (scheme == null) {
info.reportFlavorNotFoundAndExit();
}
final String? configuration = info.buildConfigurationFor(
buildInfo,
scheme,
);
buildContext = XcodeProjectBuildContext(
configuration: configuration,
scheme: scheme,
);
}
return _productBundleIdentifierWithBuildContext(buildContext);
}
Future<String?> _productBundleIdentifierWithBuildContext(XcodeProjectBuildContext? buildContext) async {
if (!existsSync()) {
return null;
}
if (_productBundleIdentifiers.containsKey(buildContext)) {
return _productBundleIdentifiers[buildContext];
}
return _productBundleIdentifiers[buildContext] = await _parseProductBundleIdentifier(buildContext);
}
String? _productBundleIdentifier;
Future<String?> _parseProductBundleIdentifier(BuildInfo? buildInfo) async {
final Map<XcodeProjectBuildContext?, String?> _productBundleIdentifiers = <XcodeProjectBuildContext?, String?>{};
Future<String?> _parseProductBundleIdentifier(XcodeProjectBuildContext? buildContext) async {
String? fromPlist;
final File defaultInfoPlist = defaultHostInfoPlist;
// Users can change the location of the Info.plist.
// Try parsing the default, first.
if (defaultInfoPlist.existsSync()) {
try {
fromPlist = globals.plistParser.getStringValueFromFile(
fromPlist = globals.plistParser.getValueFromFile<String>(
defaultHostInfoPlist.path,
PlistParser.kCFBundleIdentifierKey,
);
......@@ -232,13 +287,18 @@ class IosProject extends XcodeBasedProject {
return fromPlist;
}
}
final Map<String, String>? allBuildSettings = await buildSettingsForBuildInfo(buildInfo);
if (buildContext == null) {
// Getting build settings to evaluate info.Plist requires a context.
return null;
}
final Map<String, String>? allBuildSettings = await _buildSettingsForXcodeProjectBuildContext(buildContext);
if (allBuildSettings != null) {
if (fromPlist != null) {
// Perform variable substitution using build settings.
return substituteXcodeVariables(fromPlist, allBuildSettings);
}
return allBuildSettings['PRODUCT_BUNDLE_IDENTIFIER'];
return allBuildSettings[kProductBundleIdKey];
}
// On non-macOS platforms, parse the first PRODUCT_BUNDLE_IDENTIFIER from
......@@ -248,13 +308,48 @@ class IosProject extends XcodeBasedProject {
// only used for display purposes and to regenerate organization names, so
// best-effort is probably fine.
final String? fromPbxproj = firstMatchInFile(xcodeProjectInfoFile, _productBundleIdPattern)?.group(2);
if (fromPbxproj != null && (fromPlist == null || fromPlist == _productBundleIdVariable)) {
if (fromPbxproj != null && (fromPlist == null || fromPlist == _kProductBundleIdVariable)) {
return fromPbxproj;
}
return null;
}
Future<String?> _getTeamIdentifier(XcodeProjectBuildContext buildContext) async {
final Map<String, String>? buildSettings = await _buildSettingsForXcodeProjectBuildContext(buildContext);
if (buildSettings != null) {
return buildSettings[kTeamIdKey];
}
return null;
}
Future<List<String>> _getAssociatedDomains(XcodeProjectBuildContext buildContext) async {
final Map<String, String>? buildSettings = await _buildSettingsForXcodeProjectBuildContext(buildContext);
if (buildSettings != null) {
final String? entitlementPath = buildSettings[kEntitlementFilePathKey];
if (entitlementPath != null) {
final File entitlement = hostAppRoot.childFile(entitlementPath);
if (entitlement.existsSync()) {
final List<String>? domains = globals.plistParser.getValueFromFile<List<Object>>(
entitlement.path,
PlistParser.kAssociatedDomainsKey,
)?.cast<String>();
if (domains != null) {
final List<String> result = <String>[];
for (final String domain in domains) {
final RegExpMatch? match = _associatedDomainPattern.firstMatch(domain);
if (match != null) {
result.add(match.group(1)!);
}
}
return result;
}
}
}
}
return const <String>[];
}
/// The bundle name of the host app, `My App.app`.
Future<String?> hostAppBundleName(BuildInfo? buildInfo) async {
if (!existsSync()) {
......@@ -273,11 +368,11 @@ class IosProject extends XcodeBasedProject {
if (globals.xcodeProjectInterpreter?.isInstalled ?? false) {
final Map<String, String>? xcodeBuildSettings = await buildSettingsForBuildInfo(buildInfo);
if (xcodeBuildSettings != null) {
productName = xcodeBuildSettings['FULL_PRODUCT_NAME'];
productName = xcodeBuildSettings[kHostAppBundleNameKey];
}
}
if (productName == null) {
globals.printTrace('FULL_PRODUCT_NAME not present, defaulting to $hostAppProjectName');
globals.printTrace('$kHostAppBundleNameKey not present, defaulting to $hostAppProjectName');
}
return productName ?? '${XcodeBasedProject._defaultHostAppName}.app';
}
......@@ -287,9 +382,11 @@ class IosProject extends XcodeBasedProject {
/// Returns null, if iOS tooling is unavailable.
Future<Map<String, String>?> buildSettingsForBuildInfo(
BuildInfo? buildInfo, {
String? scheme,
String? configuration,
String? target,
EnvironmentType environmentType = EnvironmentType.physical,
String? deviceId,
String? scheme,
bool isWatch = false,
}) async {
if (!existsSync()) {
......@@ -300,24 +397,31 @@ class IosProject extends XcodeBasedProject {
return null;
}
scheme ??= info.schemeFor(buildInfo);
if (scheme == null) {
scheme = info.schemeFor(buildInfo);
if (scheme == null) {
info.reportFlavorNotFoundAndExit();
}
info.reportFlavorNotFoundAndExit();
}
final String? configuration = (await projectInfo())?.buildConfigurationFor(
configuration ??= (await projectInfo())?.buildConfigurationFor(
buildInfo,
scheme,
);
final XcodeProjectBuildContext buildContext = XcodeProjectBuildContext(
environmentType: environmentType,
scheme: scheme,
configuration: configuration,
deviceId: deviceId,
isWatch: isWatch,
return _buildSettingsForXcodeProjectBuildContext(
XcodeProjectBuildContext(
environmentType: environmentType,
scheme: scheme,
configuration: configuration,
target: target,
deviceId: deviceId,
isWatch: isWatch,
),
);
}
Future<Map<String, String>?> _buildSettingsForXcodeProjectBuildContext(XcodeProjectBuildContext buildContext) async {
if (!existsSync()) {
return null;
}
final Map<String, String>? currentBuildSettings = _buildSettingsByBuildContext[buildContext];
if (currentBuildSettings == null) {
final Map<String, String>? calculatedBuildSettings = await _xcodeProjectBuildSettings(buildContext);
......@@ -381,7 +485,7 @@ class IosProject extends XcodeBasedProject {
// In older versions of Xcode, if the target was a watchOS companion app,
// the Info.plist file of the target contained the key WKCompanionAppBundleIdentifier.
if (infoFile.existsSync()) {
final String? fromPlist = globals.plistParser.getStringValueFromFile(infoFile.path, 'WKCompanionAppBundleIdentifier');
final String? fromPlist = globals.plistParser.getValueFromFile<String>(infoFile.path, 'WKCompanionAppBundleIdentifier');
if (bundleIdentifier == fromPlist) {
return true;
}
......
......@@ -62,8 +62,8 @@ class FakePlistUtils extends Fake implements PlistParser {
final Map<String, Map<String, Object>> fileContents = <String, Map<String, Object>>{};
@override
String? getStringValueFromFile(String plistFilePath, String key) {
return fileContents[plistFilePath]![key] as String?;
T? getValueFromFile<T>(String plistFilePath, String key) {
return fileContents[plistFilePath]![key] as T?;
}
}
......
......@@ -329,7 +329,7 @@ platform :osx, '10.14'
);
infoPlistFile.writeAsStringSync('contents'); // Just so it exists: parser is a fake.
macOSProjectMigration.migrate();
expect(fakePlistParser.getStringValueFromFile(infoPlistFile.path, PlistParser.kNSPrincipalClassKey), isNull);
expect(fakePlistParser.getValueFromFile<String>(infoPlistFile.path, PlistParser.kNSPrincipalClassKey), isNull);
expect(testLogger.statusText, isEmpty);
});
......@@ -341,7 +341,7 @@ platform :osx, '10.14'
);
infoPlistFile.writeAsStringSync('contents'); // Just so it exists: parser is a fake.
macOSProjectMigration.migrate();
expect(fakePlistParser.getStringValueFromFile(infoPlistFile.path, PlistParser.kNSPrincipalClassKey), 'NSApplication');
expect(fakePlistParser.getValueFromFile<String>(infoPlistFile.path, PlistParser.kNSPrincipalClassKey), 'NSApplication');
expect(testLogger.statusText, isEmpty);
});
......@@ -353,7 +353,7 @@ platform :osx, '10.14'
);
infoPlistFile.writeAsStringSync('contents'); // Just so it exists: parser is a fake.
macOSProjectMigration.migrate();
expect(fakePlistParser.getStringValueFromFile(infoPlistFile.path, PlistParser.kNSPrincipalClassKey), 'NSApplication');
expect(fakePlistParser.getValueFromFile<String>(infoPlistFile.path, PlistParser.kNSPrincipalClassKey), 'NSApplication');
// Only print once.
expect('Updating ${infoPlistFile.basename} to use NSApplication instead of FlutterApplication.'.allMatches(testLogger.statusText).length, 1);
});
......@@ -367,7 +367,7 @@ platform :osx, '10.14'
);
infoPlistFile.writeAsStringSync('contents'); // Just so it exists: parser is a fake.
macOSProjectMigration.migrate();
expect(fakePlistParser.getStringValueFromFile(infoPlistFile.path, PlistParser.kNSPrincipalClassKey), differentApp);
expect(fakePlistParser.getValueFromFile<String>(infoPlistFile.path, PlistParser.kNSPrincipalClassKey), differentApp);
expect(testLogger.traceText, isEmpty);
});
});
......
......@@ -693,7 +693,7 @@ apply plugin: 'kotlin-android'
});
});
group('product bundle identifier', () {
group('With mocked context', () {
late MemoryFileSystem fs;
late FakePlistParser testPlistUtils;
late FakeXcodeProjectInterpreter xcodeProjectInterpreter;
......@@ -718,140 +718,260 @@ apply plugin: 'kotlin-android'
});
}
testWithMocks('null, if no build settings or plist entries', () async {
final FlutterProject project = await someProject();
expect(await project.ios.productBundleIdentifier(null), isNull);
});
group('universal link', () {
testWithMocks('build with flavor', () async {
final FlutterProject project = await someProject();
project.ios.xcodeProject.createSync();
project.ios.defaultHostInfoPlist.createSync(recursive: true);
const String entitlementFilePath = 'myEntitlement.Entitlement';
project.ios.hostAppRoot.childFile(entitlementFilePath).createSync(recursive: true);
const XcodeProjectBuildContext buildContext = XcodeProjectBuildContext(
target: 'Runner',
scheme: 'Debug',
configuration: 'config',
);
xcodeProjectInterpreter.buildSettingsByBuildContext[buildContext] = <String, String>{
IosProject.kProductBundleIdKey: 'io.flutter.someProject',
IosProject.kTeamIdKey: 'ABC',
IosProject.kEntitlementFilePathKey: entitlementFilePath,
'SUFFIX': 'suffix',
};
xcodeProjectInterpreter.xcodeProjectInfo = XcodeProjectInfo(<String>[], <String>[], <String>['Runner'], logger);
testPlistUtils.setProperty(PlistParser.kCFBundleIdentifierKey, r'$(PRODUCT_BUNDLE_IDENTIFIER).$(SUFFIX)');
testPlistUtils.setProperty(
PlistParser.kAssociatedDomainsKey,
<String>[
'applinks:example.com',
'applinks:example2.com',
],
);
final XcodeUniversalLinkSettings settings = await project.ios.universalLinkSettings(
target: 'Runner',
scheme: 'Debug',
configuration: 'config',
);
expect(
settings.associatedDomains,
unorderedEquals(
<String>[
'example.com',
'example2.com',
],
),
);
expect(settings.teamIdentifier, 'ABC');
expect(settings.bundleIdentifier, 'io.flutter.someProject.suffix');
});
testWithMocks('from build settings, if no plist', () async {
final FlutterProject project = await someProject();
project.ios.xcodeProject.createSync();
const XcodeProjectBuildContext buildContext = XcodeProjectBuildContext(scheme: 'Runner');
xcodeProjectInterpreter.buildSettingsByBuildContext[buildContext] = <String, String>{
'PRODUCT_BUNDLE_IDENTIFIER': 'io.flutter.someProject',
};
xcodeProjectInterpreter.xcodeProjectInfo = XcodeProjectInfo(<String>[], <String>[], <String>['Runner'], logger);
testWithMocks('can handle entitlement file in nested directory structure.', () async {
final FlutterProject project = await someProject();
project.ios.xcodeProject.createSync();
project.ios.defaultHostInfoPlist.createSync(recursive: true);
const String entitlementFilePath = 'nested/somewhere/myEntitlement.Entitlement';
project.ios.hostAppRoot.childFile(entitlementFilePath).createSync(recursive: true);
const XcodeProjectBuildContext buildContext = XcodeProjectBuildContext(
target: 'Runner',
scheme: 'Debug',
configuration: 'config',
);
xcodeProjectInterpreter.buildSettingsByBuildContext[buildContext] = <String, String>{
IosProject.kProductBundleIdKey: 'io.flutter.someProject',
IosProject.kTeamIdKey: 'ABC',
IosProject.kEntitlementFilePathKey: entitlementFilePath,
'SUFFIX': 'suffix',
};
xcodeProjectInterpreter.xcodeProjectInfo = XcodeProjectInfo(<String>[], <String>[], <String>['Runner'], logger);
testPlistUtils.setProperty(PlistParser.kCFBundleIdentifierKey, r'$(PRODUCT_BUNDLE_IDENTIFIER).$(SUFFIX)');
testPlistUtils.setProperty(
PlistParser.kAssociatedDomainsKey,
<String>[
'applinks:example.com',
'applinks:example2.com',
],
);
final XcodeUniversalLinkSettings settings = await project.ios.universalLinkSettings(
target: 'Runner',
scheme: 'Debug',
configuration: 'config',
);
expect(
settings.associatedDomains,
unorderedEquals(
<String>[
'example.com',
'example2.com',
],
),
);
expect(settings.teamIdentifier, 'ABC');
expect(settings.bundleIdentifier, 'io.flutter.someProject.suffix');
});
expect(await project.ios.productBundleIdentifier(null), 'io.flutter.someProject');
testWithMocks('return empty when no entitlement', () async {
final FlutterProject project = await someProject();
project.ios.xcodeProject.createSync();
project.ios.defaultHostInfoPlist.createSync(recursive: true);
const XcodeProjectBuildContext buildContext = XcodeProjectBuildContext(
target: 'Runner',
scheme: 'Debug',
configuration: 'config',
);
xcodeProjectInterpreter.buildSettingsByBuildContext[buildContext] = <String, String>{
IosProject.kProductBundleIdKey: 'io.flutter.someProject',
IosProject.kTeamIdKey: 'ABC',
};
xcodeProjectInterpreter.xcodeProjectInfo = XcodeProjectInfo(<String>[], <String>[], <String>['Runner'], logger);
testPlistUtils.setProperty(PlistParser.kCFBundleIdentifierKey, r'$(PRODUCT_BUNDLE_IDENTIFIER)');
final XcodeUniversalLinkSettings settings = await project.ios.universalLinkSettings(
target: 'Runner',
scheme: 'Debug',
configuration: 'config',
);
expect(settings.teamIdentifier, 'ABC');
expect(settings.bundleIdentifier, 'io.flutter.someProject');
});
});
testWithMocks('from project file, if no plist or build settings', () async {
final FlutterProject project = await someProject();
xcodeProjectInterpreter.xcodeProjectInfo = XcodeProjectInfo(<String>[], <String>[], <String>['Runner'], logger);
addIosProjectFile(project.directory, projectFileContent: () {
return projectFileWithBundleId('io.flutter.someProject');
group('product bundle identifier', () {
testWithMocks('null, if no build settings or plist entries', () async {
final FlutterProject project = await someProject();
expect(await project.ios.productBundleIdentifier(null), isNull);
});
expect(await project.ios.productBundleIdentifier(null), 'io.flutter.someProject');
});
testWithMocks('from plist, if no variables', () async {
final FlutterProject project = await someProject();
project.ios.defaultHostInfoPlist.createSync(recursive: true);
testPlistUtils.setProperty('CFBundleIdentifier', 'io.flutter.someProject');
expect(await project.ios.productBundleIdentifier(null), 'io.flutter.someProject');
});
testWithMocks('from build settings, if no plist', () async {
final FlutterProject project = await someProject();
project.ios.xcodeProject.createSync();
const XcodeProjectBuildContext buildContext = XcodeProjectBuildContext(scheme: 'Runner');
xcodeProjectInterpreter.buildSettingsByBuildContext[buildContext] =
<String, String>{
IosProject.kProductBundleIdKey: 'io.flutter.someProject',
};
xcodeProjectInterpreter.xcodeProjectInfo = XcodeProjectInfo(<String>[], <String>[], <String>['Runner'], logger);
expect(await project.ios.productBundleIdentifier(null), 'io.flutter.someProject');
});
testWithMocks('from build settings and plist, if default variable', () async {
final FlutterProject project = await someProject();
project.ios.xcodeProject.createSync();
const XcodeProjectBuildContext buildContext = XcodeProjectBuildContext(scheme: 'Runner');
xcodeProjectInterpreter.buildSettingsByBuildContext[buildContext] = <String, String>{
'PRODUCT_BUNDLE_IDENTIFIER': 'io.flutter.someProject',
};
xcodeProjectInterpreter.xcodeProjectInfo = XcodeProjectInfo(<String>[], <String>[], <String>['Runner'], logger);
testPlistUtils.setProperty('CFBundleIdentifier', r'$(PRODUCT_BUNDLE_IDENTIFIER)');
testWithMocks('from project file, if no plist or build settings', () async {
final FlutterProject project = await someProject();
xcodeProjectInterpreter.xcodeProjectInfo = XcodeProjectInfo(<String>[], <String>[], <String>['Runner'], logger);
expect(await project.ios.productBundleIdentifier(null), 'io.flutter.someProject');
});
addIosProjectFile(project.directory, projectFileContent: () {
return projectFileWithBundleId('io.flutter.someProject');
});
expect(await project.ios.productBundleIdentifier(null), 'io.flutter.someProject');
});
testWithMocks('from build settings and plist, by substitution', () async {
final FlutterProject project = await someProject();
project.ios.xcodeProject.createSync();
project.ios.defaultHostInfoPlist.createSync(recursive: true);
const XcodeProjectBuildContext buildContext = XcodeProjectBuildContext(scheme: 'Runner');
xcodeProjectInterpreter.buildSettingsByBuildContext[buildContext] = <String, String>{
'PRODUCT_BUNDLE_IDENTIFIER': 'io.flutter.someProject',
'SUFFIX': 'suffix',
};
xcodeProjectInterpreter.xcodeProjectInfo = XcodeProjectInfo(<String>[], <String>[], <String>['Runner'], logger);
testPlistUtils.setProperty('CFBundleIdentifier', r'$(PRODUCT_BUNDLE_IDENTIFIER).$(SUFFIX)');
testWithMocks('from plist, if no variables', () async {
final FlutterProject project = await someProject();
project.ios.defaultHostInfoPlist.createSync(recursive: true);
testPlistUtils.setProperty('CFBundleIdentifier', 'io.flutter.someProject');
expect(await project.ios.productBundleIdentifier(null), 'io.flutter.someProject');
});
expect(await project.ios.productBundleIdentifier(null), 'io.flutter.someProject.suffix');
});
testWithMocks('from build settings and plist, if default variable', () async {
final FlutterProject project = await someProject();
project.ios.xcodeProject.createSync();
const XcodeProjectBuildContext buildContext = XcodeProjectBuildContext(scheme: 'Runner');
xcodeProjectInterpreter.buildSettingsByBuildContext[buildContext] = <String, String>{
IosProject.kProductBundleIdKey: 'io.flutter.someProject',
};
xcodeProjectInterpreter.xcodeProjectInfo = XcodeProjectInfo(<String>[], <String>[], <String>['Runner'], logger);
testPlistUtils.setProperty('CFBundleIdentifier', r'$(PRODUCT_BUNDLE_IDENTIFIER)');
expect(await project.ios.productBundleIdentifier(null), 'io.flutter.someProject');
});
testWithMocks('Always pass parsing org on ios project with flavors', () async {
final FlutterProject project = await someProject();
addIosProjectFile(project.directory, projectFileContent: () {
return projectFileWithBundleId('io.flutter.someProject', qualifier: "'");
testWithMocks('from build settings and plist, by substitution', () async {
final FlutterProject project = await someProject();
project.ios.xcodeProject.createSync();
project.ios.defaultHostInfoPlist.createSync(recursive: true);
const XcodeProjectBuildContext buildContext = XcodeProjectBuildContext(scheme: 'Runner');
xcodeProjectInterpreter.buildSettingsByBuildContext[buildContext] = <String, String>{
IosProject.kProductBundleIdKey: 'io.flutter.someProject',
'SUFFIX': 'suffix',
};
xcodeProjectInterpreter.xcodeProjectInfo = XcodeProjectInfo(<String>[], <String>[], <String>['Runner'], logger);
testPlistUtils.setProperty('CFBundleIdentifier', r'$(PRODUCT_BUNDLE_IDENTIFIER).$(SUFFIX)');
expect(await project.ios.productBundleIdentifier(null), 'io.flutter.someProject.suffix');
});
project.ios.xcodeProject.createSync();
xcodeProjectInterpreter.xcodeProjectInfo = XcodeProjectInfo(<String>[], <String>[], <String>['free', 'paid'], logger);
expect(await project.organizationNames, <String>[]);
});
testWithMocks('Always pass parsing org on ios project with flavors', () async {
final FlutterProject project = await someProject();
addIosProjectFile(project.directory, projectFileContent: () {
return projectFileWithBundleId('io.flutter.someProject', qualifier: "'");
});
project.ios.xcodeProject.createSync();
xcodeProjectInterpreter.xcodeProjectInfo = XcodeProjectInfo(<String>[], <String>[], <String>['free', 'paid'], logger);
testWithMocks('fails with no flavor and defined schemes', () async {
final FlutterProject project = await someProject();
project.ios.xcodeProject.createSync();
xcodeProjectInterpreter.xcodeProjectInfo = XcodeProjectInfo(<String>[], <String>[], <String>['free', 'paid'], logger);
expect(await project.organizationNames, <String>[]);
});
await expectToolExitLater(
project.ios.productBundleIdentifier(null),
contains('You must specify a --flavor option to select one of the available schemes.')
);
});
testWithMocks('fails with no flavor and defined schemes', () async {
final FlutterProject project = await someProject();
project.ios.xcodeProject.createSync();
xcodeProjectInterpreter.xcodeProjectInfo = XcodeProjectInfo(<String>[], <String>[], <String>['free', 'paid'], logger);
testWithMocks('handles case insensitive flavor', () async {
final FlutterProject project = await someProject();
project.ios.xcodeProject.createSync();
const XcodeProjectBuildContext buildContext = XcodeProjectBuildContext(scheme: 'Free');
xcodeProjectInterpreter.buildSettingsByBuildContext[buildContext] = <String, String>{
'PRODUCT_BUNDLE_IDENTIFIER': 'io.flutter.someProject',
};
xcodeProjectInterpreter.xcodeProjectInfo =XcodeProjectInfo(<String>[], <String>[], <String>['Free'], logger);
const BuildInfo buildInfo = BuildInfo(BuildMode.debug, 'free', treeShakeIcons: false);
await expectToolExitLater(
project.ios.productBundleIdentifier(null),
contains('You must specify a --flavor option to select one of the available schemes.'),
);
});
expect(await project.ios.productBundleIdentifier(buildInfo), 'io.flutter.someProject');
});
testWithMocks('handles case insensitive flavor', () async {
final FlutterProject project = await someProject();
project.ios.xcodeProject.createSync();
const XcodeProjectBuildContext buildContext = XcodeProjectBuildContext(scheme: 'Free');
xcodeProjectInterpreter.buildSettingsByBuildContext[buildContext] = <String, String>{
IosProject.kProductBundleIdKey: 'io.flutter.someProject',
};
xcodeProjectInterpreter.xcodeProjectInfo = XcodeProjectInfo(<String>[], <String>[], <String>['Free'], logger);
const BuildInfo buildInfo = BuildInfo(BuildMode.debug, 'free', treeShakeIcons: false);
expect(await project.ios.productBundleIdentifier(buildInfo), 'io.flutter.someProject');
});
testWithMocks('fails with flavor and default schemes', () async {
final FlutterProject project = await someProject();
project.ios.xcodeProject.createSync();
xcodeProjectInterpreter.xcodeProjectInfo = XcodeProjectInfo(<String>[], <String>[], <String>['Runner'], logger);
const BuildInfo buildInfo = BuildInfo(BuildMode.debug, 'free', treeShakeIcons: false);
testWithMocks('fails with flavor and default schemes', () async {
final FlutterProject project = await someProject();
project.ios.xcodeProject.createSync();
xcodeProjectInterpreter.xcodeProjectInfo = XcodeProjectInfo(<String>[], <String>[], <String>['Runner'], logger);
const BuildInfo buildInfo = BuildInfo(BuildMode.debug, 'free', treeShakeIcons: false);
await expectToolExitLater(
project.ios.productBundleIdentifier(buildInfo),
contains('The Xcode project does not define custom schemes. You cannot use the --flavor option.')
);
});
await expectToolExitLater(
project.ios.productBundleIdentifier(buildInfo),
contains('The Xcode project does not define custom schemes. You cannot use the --flavor option.'),
);
});
testWithMocks('empty surrounded by quotes', () async {
final FlutterProject project = await someProject();
xcodeProjectInterpreter.xcodeProjectInfo = XcodeProjectInfo(<String>[], <String>[], <String>['Runner'], logger);
addIosProjectFile(project.directory, projectFileContent: () {
return projectFileWithBundleId('', qualifier: '"');
testWithMocks('empty surrounded by quotes', () async {
final FlutterProject project = await someProject();
xcodeProjectInterpreter.xcodeProjectInfo = XcodeProjectInfo(<String>[], <String>[], <String>['Runner'], logger);
addIosProjectFile(project.directory, projectFileContent: () {
return projectFileWithBundleId('', qualifier: '"');
});
expect(await project.ios.productBundleIdentifier(null), '');
});
expect(await project.ios.productBundleIdentifier(null), '');
});
testWithMocks('surrounded by double quotes', () async {
final FlutterProject project = await someProject();
xcodeProjectInterpreter.xcodeProjectInfo = XcodeProjectInfo(<String>[], <String>[], <String>['Runner'], logger);
addIosProjectFile(project.directory, projectFileContent: () {
return projectFileWithBundleId('io.flutter.someProject', qualifier: '"');
testWithMocks('surrounded by double quotes', () async {
final FlutterProject project = await someProject();
xcodeProjectInterpreter.xcodeProjectInfo = XcodeProjectInfo(<String>[], <String>[], <String>['Runner'], logger);
addIosProjectFile(project.directory, projectFileContent: () {
return projectFileWithBundleId('io.flutter.someProject', qualifier: '"');
});
expect(await project.ios.productBundleIdentifier(null), 'io.flutter.someProject');
});
expect(await project.ios.productBundleIdentifier(null), 'io.flutter.someProject');
});
testWithMocks('surrounded by single quotes', () async {
final FlutterProject project = await someProject();
xcodeProjectInterpreter.xcodeProjectInfo = XcodeProjectInfo(<String>[], <String>[], <String>['Runner'], logger);
addIosProjectFile(project.directory, projectFileContent: () {
return projectFileWithBundleId('io.flutter.someProject', qualifier: "'");
testWithMocks('surrounded by single quotes', () async {
final FlutterProject project = await someProject();
xcodeProjectInterpreter.xcodeProjectInfo = XcodeProjectInfo(<String>[], <String>[], <String>['Runner'], logger);
addIosProjectFile(project.directory, projectFileContent: () {
return projectFileWithBundleId('io.flutter.someProject', qualifier: "'");
});
expect(await project.ios.productBundleIdentifier(null), 'io.flutter.someProject');
});
expect(await project.ios.productBundleIdentifier(null), 'io.flutter.someProject');
});
});
......@@ -999,7 +1119,7 @@ apply plugin: 'kotlin-android'
setUp(() {
const XcodeProjectBuildContext buildContext = XcodeProjectBuildContext(scheme: 'Runner');
mockXcodeProjectInterpreter.buildSettingsByBuildContext[buildContext] = <String, String>{
'PRODUCT_BUNDLE_IDENTIFIER': 'io.flutter.someProject',
IosProject.kProductBundleIdKey: 'io.flutter.someProject',
};
mockXcodeProjectInterpreter.xcodeProjectInfo = XcodeProjectInfo(<String>['Runner', 'WatchTarget'], <String>[], <String>['Runner', 'WatchScheme'], logger);
});
......@@ -1093,7 +1213,7 @@ apply plugin: 'kotlin-android'
deviceId: '123',
);
mockXcodeProjectInterpreter.buildSettingsByBuildContext[buildContext] = <String, String>{
'PRODUCT_BUNDLE_IDENTIFIER': 'io.flutter.someProject',
IosProject.kProductBundleIdKey: 'io.flutter.someProject',
};
project.ios.hostAppRoot.childDirectory('WatchTarget').childFile('Info.plist').createSync(recursive: true);
testPlistParser.setProperty('WKCompanionAppBundleIdentifier', r'$(PRODUCT_BUNDLE_IDENTIFIER)');
......@@ -1127,7 +1247,7 @@ apply plugin: 'kotlin-android'
deviceId: '123',
);
mockXcodeProjectInterpreter.buildSettingsByBuildContext[buildContext] = <String, String>{
'PRODUCT_BUNDLE_IDENTIFIER': 'io.flutter.someProject',
IosProject.kProductBundleIdKey: 'io.flutter.someProject',
};
const XcodeProjectBuildContext watchBuildContext = XcodeProjectBuildContext(
......@@ -1167,7 +1287,7 @@ apply plugin: 'kotlin-android'
deviceId: '123'
);
mockXcodeProjectInterpreter.buildSettingsByBuildContext[buildContext] = <String, String>{
'PRODUCT_BUNDLE_IDENTIFIER': 'io.flutter.someProject',
IosProject.kProductBundleIdKey: 'io.flutter.someProject',
};
const XcodeProjectBuildContext watchBuildContext = XcodeProjectBuildContext(
......@@ -1176,7 +1296,7 @@ apply plugin: 'kotlin-android'
isWatch: true,
);
mockXcodeProjectInterpreter.buildSettingsByBuildContext[watchBuildContext] = <String, String>{
'PRODUCT_BUNDLE_IDENTIFIER': 'io.flutter.someProject',
IosProject.kProductBundleIdKey: 'io.flutter.someProject',
'INFOPLIST_KEY_WKCompanionAppBundleIdentifier': r'$(PRODUCT_BUNDLE_IDENTIFIER)',
};
......
......@@ -82,6 +82,10 @@ const List<VmServiceExpectation> kAttachIsolateExpectations =
'service': kFlutterGetAndroidBuildVariantsServiceName,
'alias': kFlutterToolAlias,
}),
FakeVmServiceRequest(method: 'registerService', args: <String, Object>{
'service': kFlutterGetIOSDeeplinkSettingsServiceName,
'alias': 'Flutter Tools',
}),
FakeVmServiceRequest(
method: 'streamListen',
args: <String, Object>{
......
......@@ -74,52 +74,52 @@ void main() {
file.deleteSync();
});
testWithoutContext('PlistParser.getStringValueFromFile works with an XML file', () {
testWithoutContext('PlistParser.getValueFromFile<String> works with an XML file', () {
file.writeAsBytesSync(base64.decode(base64PlistXml));
expect(parser.getStringValueFromFile(file.path, 'CFBundleIdentifier'), 'io.flutter.flutter.app');
expect(parser.getStringValueFromFile(file.absolute.path, 'CFBundleIdentifier'), 'io.flutter.flutter.app');
expect(parser.getValueFromFile<String>(file.path, 'CFBundleIdentifier'), 'io.flutter.flutter.app');
expect(parser.getValueFromFile<String>(file.absolute.path, 'CFBundleIdentifier'), 'io.flutter.flutter.app');
expect(logger.statusText, isEmpty);
expect(logger.errorText, isEmpty);
}, skip: !platform.isMacOS); // [intended] requires macos tool chain.
testWithoutContext('PlistParser.getStringValueFromFile works with a binary file', () {
testWithoutContext('PlistParser.getValueFromFile<String> works with a binary file', () {
file.writeAsBytesSync(base64.decode(base64PlistBinary));
expect(parser.getStringValueFromFile(file.path, 'CFBundleIdentifier'), 'io.flutter.flutter.app');
expect(parser.getStringValueFromFile(file.absolute.path, 'CFBundleIdentifier'), 'io.flutter.flutter.app');
expect(parser.getValueFromFile<String>(file.path, 'CFBundleIdentifier'), 'io.flutter.flutter.app');
expect(parser.getValueFromFile<String>(file.absolute.path, 'CFBundleIdentifier'), 'io.flutter.flutter.app');
expect(logger.statusText, isEmpty);
expect(logger.errorText, isEmpty);
}, skip: !platform.isMacOS); // [intended] requires macos tool chain.
testWithoutContext('PlistParser.getStringValueFromFile works with a JSON file', () {
testWithoutContext('PlistParser.getValueFromFile<String> works with a JSON file', () {
file.writeAsBytesSync(base64.decode(base64PlistJson));
expect(parser.getStringValueFromFile(file.path, 'CFBundleIdentifier'), 'io.flutter.flutter.app');
expect(parser.getStringValueFromFile(file.absolute.path, 'CFBundleIdentifier'), 'io.flutter.flutter.app');
expect(parser.getValueFromFile<String>(file.path, 'CFBundleIdentifier'), 'io.flutter.flutter.app');
expect(parser.getValueFromFile<String>(file.absolute.path, 'CFBundleIdentifier'), 'io.flutter.flutter.app');
expect(logger.statusText, isEmpty);
expect(logger.errorText, isEmpty);
}, skip: !platform.isMacOS); // [intended] requires macos tool chain.
testWithoutContext('PlistParser.getStringValueFromFile returns null for a non-existent plist file', () {
expect(parser.getStringValueFromFile('missing.plist', 'CFBundleIdentifier'), null);
testWithoutContext('PlistParser.getValueFromFile<String> returns null for a non-existent plist file', () {
expect(parser.getValueFromFile<String>('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 a non-existent key within a plist', () {
testWithoutContext('PlistParser.getValueFromFile<String> returns null for a non-existent key within a plist', () {
file.writeAsBytesSync(base64.decode(base64PlistXml));
expect(parser.getStringValueFromFile(file.path, 'BadKey'), null);
expect(parser.getStringValueFromFile(file.absolute.path, 'BadKey'), null);
expect(parser.getValueFromFile<String>(file.path, 'BadKey'), null);
expect(parser.getValueFromFile<String>(file.absolute.path, 'BadKey'), null);
expect(logger.statusText, isEmpty);
expect(logger.errorText, isEmpty);
}, skip: !platform.isMacOS); // [intended] requires macos tool chain.
testWithoutContext('PlistParser.getStringValueFromFile returns null for a malformed plist file', () {
testWithoutContext('PlistParser.getValueFromFile<String> returns null for a malformed plist file', () {
file.writeAsBytesSync(const <int>[1, 2, 3, 4, 5, 6]);
expect(parser.getStringValueFromFile(file.path, 'CFBundleIdentifier'), null);
expect(parser.getValueFromFile<String>(file.path, 'CFBundleIdentifier'), null);
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'));
......@@ -127,11 +127,11 @@ void main() {
' 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 {
testWithoutContext('PlistParser.getValueFromFile<String> throws when /usr/bin/plutil is not found', () async {
file.writeAsBytesSync(base64.decode(base64PlistXml));
expect(
() => parser.getStringValueFromFile(file.path, 'unused'),
() => parser.getValueFromFile<String>(file.path, 'unused'),
throwsA(isA<FileNotFoundException>()),
);
expect(logger.statusText, isEmpty);
......@@ -141,21 +141,21 @@ void main() {
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.getValueFromFile<String>(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'));
expect(parser.getValueFromFile<String>(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.getValueFromFile<String>(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'));
expect(parser.getValueFromFile<String>(file.path, 'CFNewKey'), equals('dev.flutter.fake'));
}, skip: !platform.isMacOS); // [intended] requires macos tool chain.
testWithoutContext('PlistParser.replaceKey can delete a key', () async {
......@@ -164,7 +164,7 @@ void main() {
expect(parser.replaceKey(file.path, key: 'CFBundleIdentifier'), isTrue);
expect(logger.statusText, isEmpty);
expect(logger.errorText, isEmpty);
expect(parser.getStringValueFromFile(file.path, 'CFBundleIdentifier'), isNull);
expect(parser.getValueFromFile<String>(file.path, 'CFBundleIdentifier'), isNull);
}, skip: !platform.isMacOS); // [intended] requires macos tool chain.
testWithoutContext('PlistParser.replaceKey throws when /usr/bin/plutil is not found', () async {
......@@ -192,9 +192,9 @@ void main() {
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.getValueFromFile<String>(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(parser.getValueFromFile<String>(file.absolute.path, 'CFBundleIdentifier'), 'dev.flutter.fake');
expect(logger.statusText, isEmpty);
expect(logger.errorText, isEmpty);
}, skip: !platform.isMacOS); // [intended] requires macos tool chain.
......
......@@ -296,8 +296,8 @@ class FakePlistParser implements PlistParser {
}
@override
String? getStringValueFromFile(String plistFilePath, String key) {
return _underlyingValues[key] as String?;
T? getValueFromFile<T>(String plistFilePath, String key) {
return _underlyingValues[key] as T?;
}
@override
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment