flutter/packages/flutter_tools/test/general.shard/project_test.dart
Loïc Sharma 40843e3e61
Update minimum macOS version as needed in Swift package (#152347)
If Swift Package Manager is enabled, the tool generates a Swift package at `<ios/macos>/Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage/`. This Swift package is how the tool adds plugins to the Flutter project.

SwiftPM is strictly enforces platform versions: you cannot depend on a Swift package if its supported version is higher than your own.

On iOS, we use the project's minimum deployment version for the generated Swift package. If a plugin has a higher requirement, you'll need to update your project's minimum deployment version. The generated Swift package is automatically updated the next time you run the tool.

This updates macOS to do the same thing.

Fixes https://github.com/flutter/flutter/issues/146204
2024-07-26 22:02:08 +00:00

1910 lines
79 KiB
Dart

// 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/memory.dart';
import 'package:flutter_tools/src/android/android_sdk.dart';
import 'package:flutter_tools/src/android/android_studio.dart';
import 'package:flutter_tools/src/android/gradle_utils.dart' as gradle_utils;
import 'package:flutter_tools/src/android/java.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/base/os.dart';
import 'package:flutter_tools/src/base/version.dart';
import 'package:flutter_tools/src/build_info.dart';
import 'package:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/convert.dart';
import 'package:flutter_tools/src/features.dart';
import 'package:flutter_tools/src/flutter_manifest.dart';
import 'package:flutter_tools/src/globals.dart' as globals;
import 'package:flutter_tools/src/ios/plist_parser.dart';
import 'package:flutter_tools/src/ios/xcodeproj.dart';
import 'package:flutter_tools/src/project.dart';
import 'package:meta/meta.dart';
import 'package:test/fake.dart';
import '../src/common.dart';
import '../src/context.dart';
import '../src/fakes.dart';
void main() {
// TODO(zanderso): remove once FlutterProject is fully refactored.
// this is safe since no tests have expectations on the test logger.
final BufferLogger logger = BufferLogger.test();
group('Project', () {
group('construction', () {
testWithoutContext('invalid utf8 throws a tool exit', () {
final FileSystem fileSystem = MemoryFileSystem.test();
final FlutterProjectFactory projectFactory = FlutterProjectFactory(
fileSystem: fileSystem,
logger: BufferLogger.test(),
);
fileSystem.file('pubspec.yaml').writeAsBytesSync(<int>[0xFFFE]);
/// Technically this should throw a FileSystemException but this is
/// currently a bug in package:file.
expect(
() => projectFactory.fromDirectory(fileSystem.currentDirectory),
throwsToolExit(),
);
});
_testInMemory('fails on invalid pubspec.yaml', () async {
final Directory directory = globals.fs.directory('myproject');
directory.childFile('pubspec.yaml')
..createSync(recursive: true)
..writeAsStringSync(invalidPubspec);
expect(
() => FlutterProject.fromDirectory(directory),
throwsToolExit(),
);
});
_testInMemory('fails on pubspec.yaml parse failure', () async {
final Directory directory = globals.fs.directory('myproject');
directory.childFile('pubspec.yaml')
..createSync(recursive: true)
..writeAsStringSync(parseErrorPubspec);
expect(
() => FlutterProject.fromDirectory(directory),
throwsToolExit(),
);
});
_testInMemory('fails on invalid example/pubspec.yaml', () async {
final Directory directory = globals.fs.directory('myproject');
directory.childDirectory('example').childFile('pubspec.yaml')
..createSync(recursive: true)
..writeAsStringSync(invalidPubspec);
expect(
() => FlutterProject.fromDirectory(directory),
throwsToolExit(),
);
});
_testInMemory('treats missing pubspec.yaml as empty', () async {
final Directory directory = globals.fs.directory('myproject')
..createSync(recursive: true);
expect(FlutterProject.fromDirectory(directory).manifest.isEmpty,
true,
);
});
_testInMemory('reads valid pubspec.yaml', () async {
final Directory directory = globals.fs.directory('myproject');
directory.childFile('pubspec.yaml')
..createSync(recursive: true)
..writeAsStringSync(validPubspec);
expect(
FlutterProject.fromDirectory(directory).manifest.appName,
'hello',
);
});
_testInMemory('reads dependencies from pubspec.yaml', () async {
final Directory directory = globals.fs.directory('myproject');
directory.childFile('pubspec.yaml')
..createSync(recursive: true)
..writeAsStringSync(validPubspecWithDependencies);
expect(
FlutterProject.fromDirectory(directory).manifest.dependencies,
<String>{'plugin_a', 'plugin_b'},
);
});
_testInMemory('sets up location', () async {
final Directory directory = globals.fs.directory('myproject');
expect(
FlutterProject.fromDirectory(directory).directory.absolute.path,
directory.absolute.path,
);
expect(
FlutterProject.fromDirectoryTest(directory).directory.absolute.path,
directory.absolute.path,
);
expect(
FlutterProject.current().directory.absolute.path,
globals.fs.currentDirectory.absolute.path,
);
});
});
group('ensure ready for platform-specific tooling', () {
_testInMemory('does nothing, if project is not created', () async {
final FlutterProject project = FlutterProject(
globals.fs.directory('not_created'),
FlutterManifest.empty(logger: logger),
FlutterManifest.empty(logger: logger),
);
await project.regeneratePlatformSpecificTooling();
expectNotExists(project.directory);
});
_testInMemory('does nothing in plugin or package root project', () async {
final FlutterProject project = await aPluginProject();
await project.regeneratePlatformSpecificTooling();
expectNotExists(project.ios.hostAppRoot.childDirectory('Runner').childFile('GeneratedPluginRegistrant.h'));
expectNotExists(androidPluginRegistrant(project.android.hostAppGradleRoot.childDirectory('app')));
expectNotExists(project.ios.hostAppRoot.childDirectory('Flutter').childFile('Generated.xcconfig'));
expectNotExists(project.android.hostAppGradleRoot.childFile('local.properties'));
});
_testInMemory('works if there is an "example" folder', () async {
final FlutterProject project = await someProject();
// The presence of an "example" folder used to be used as an indicator
// that a project was a plugin, but shouldn't be as this creates false
// positives.
project.directory.childDirectory('example').createSync();
await project.regeneratePlatformSpecificTooling();
expectExists(project.ios.hostAppRoot.childDirectory('Runner').childFile('GeneratedPluginRegistrant.h'));
expectExists(androidPluginRegistrant(project.android.hostAppGradleRoot.childDirectory('app')));
expectExists(project.ios.hostAppRoot.childDirectory('Flutter').childFile('Generated.xcconfig'));
expectExists(project.android.hostAppGradleRoot.childFile('local.properties'));
});
_testInMemory('injects plugins for iOS', () async {
final FlutterProject project = await someProject();
await project.regeneratePlatformSpecificTooling();
expectExists(project.ios.hostAppRoot.childDirectory('Runner').childFile('GeneratedPluginRegistrant.h'));
});
_testInMemory('generates Xcode configuration for iOS', () async {
final FlutterProject project = await someProject();
await project.regeneratePlatformSpecificTooling();
expectExists(project.ios.hostAppRoot.childDirectory('Flutter').childFile('Generated.xcconfig'));
});
_testInMemory('injects plugins for Android', () async {
final FlutterProject project = await someProject();
await project.regeneratePlatformSpecificTooling();
expectExists(androidPluginRegistrant(project.android.hostAppGradleRoot.childDirectory('app')));
});
_testInMemory('updates local properties for Android', () async {
final FlutterProject project = await someProject();
await project.regeneratePlatformSpecificTooling();
expectExists(project.android.hostAppGradleRoot.childFile('local.properties'));
});
_testInMemory('checkForDeprecation fails on invalid android app manifest file', () async {
// This is not a valid Xml document
const String invalidManifest = '<manifest></application>';
final FlutterProject project = await someProject(androidManifestOverride: invalidManifest, includePubspec: true);
expect(
() => project.checkForDeprecation(deprecationBehavior: DeprecationBehavior.ignore),
throwsToolExit(message: 'Please ensure that the android manifest is a valid XML document and try again.'),
);
});
_testInMemory('Project not on v2 embedding does not warn if deprecation status is irrelevant', () async {
final FlutterProject project = await someProject(includePubspec: true);
// The default someProject with an empty <manifest> already indicates
// v1 embedding, as opposed to having <meta-data
// android:name="flutterEmbedding" android:value="2" />.
// Default is "DeprecationBehavior.none"
project.checkForDeprecation();
expect(testLogger.statusText, isEmpty);
});
_testInMemory('Android project no pubspec continues', () async {
final FlutterProject project = await someProject();
// The default someProject with an empty <manifest> already indicates
// v1 embedding, as opposed to having <meta-data
// android:name="flutterEmbedding" android:value="2" />.
project.checkForDeprecation(deprecationBehavior: DeprecationBehavior.ignore);
expect(testLogger.statusText, isNot(contains('https://github.com/flutter/flutter/blob/main/docs/platforms/android/Upgrading-pre-1.12-Android-projects.md')));
});
_testInMemory('Android plugin project does not throw v1 embedding deprecation warning', () async {
final FlutterProject project = await aPluginProject();
project.checkForDeprecation(deprecationBehavior: DeprecationBehavior.exit);
expect(testLogger.statusText, isNot(contains('https://github.com/flutter/flutter/blob/main/docs/platforms/android/Upgrading-pre-1.12-Android-projects.md')));
expect(testLogger.statusText, isNot(contains('No `<meta-data android:name="flutterEmbedding" android:value="2"/>` in ')));
});
_testInMemory('Android plugin without example app does not show a warning', () async {
final FlutterProject project = await aPluginProject();
project.example.directory.deleteSync();
await project.regeneratePlatformSpecificTooling();
expect(testLogger.statusText, isNot(contains('https://github.com/flutter/flutter/blob/main/docs/platforms/android/Upgrading-pre-1.12-Android-projects.md')));
});
_testInMemory('updates local properties for Android', () async {
final FlutterProject project = await someProject();
await project.regeneratePlatformSpecificTooling();
expectExists(project.android.hostAppGradleRoot.childFile('local.properties'));
});
testUsingContext('injects plugins for macOS', () async {
final FlutterProject project = await someProject();
project.macos.managedDirectory.createSync(recursive: true);
await project.regeneratePlatformSpecificTooling();
expectExists(project.macos.pluginRegistrantImplementation);
}, overrides: <Type, Generator>{
FileSystem: () => MemoryFileSystem.test(),
ProcessManager: () => FakeProcessManager.any(),
FeatureFlags: () => TestFeatureFlags(isMacOSEnabled: true),
FlutterProjectFactory: () => FlutterProjectFactory(
logger: logger,
fileSystem: globals.fs,
),
});
testUsingContext('generates Xcode configuration for macOS', () async {
final FlutterProject project = await someProject();
project.macos.managedDirectory.createSync(recursive: true);
await project.regeneratePlatformSpecificTooling();
expectExists(project.macos.generatedXcodePropertiesFile);
}, overrides: <Type, Generator>{
FileSystem: () => MemoryFileSystem.test(),
ProcessManager: () => FakeProcessManager.any(),
FeatureFlags: () => TestFeatureFlags(isMacOSEnabled: true),
FlutterProjectFactory: () => FlutterProjectFactory(
logger: logger,
fileSystem: globals.fs,
),
});
testUsingContext('injects plugins for Linux', () async {
final FlutterProject project = await someProject();
project.linux.cmakeFile.createSync(recursive: true);
await project.regeneratePlatformSpecificTooling();
expectExists(project.linux.managedDirectory.childFile('generated_plugin_registrant.h'));
expectExists(project.linux.managedDirectory.childFile('generated_plugin_registrant.cc'));
}, overrides: <Type, Generator>{
FileSystem: () => MemoryFileSystem.test(),
ProcessManager: () => FakeProcessManager.any(),
FeatureFlags: () => TestFeatureFlags(isLinuxEnabled: true),
FlutterProjectFactory: () => FlutterProjectFactory(
logger: logger,
fileSystem: globals.fs,
),
});
testUsingContext('injects plugins for Windows', () async {
final FlutterProject project = await someProject();
project.windows.cmakeFile.createSync(recursive: true);
await project.regeneratePlatformSpecificTooling();
expectExists(project.windows.managedDirectory.childFile('generated_plugin_registrant.h'));
expectExists(project.windows.managedDirectory.childFile('generated_plugin_registrant.cc'));
}, overrides: <Type, Generator>{
FileSystem: () => MemoryFileSystem.test(),
ProcessManager: () => FakeProcessManager.any(),
FeatureFlags: () => TestFeatureFlags(isWindowsEnabled: true),
FlutterProjectFactory: () => FlutterProjectFactory(
logger: logger,
fileSystem: globals.fs,
),
});
_testInMemory('creates Android library in module', () async {
final FlutterProject project = await aModuleProject();
await project.regeneratePlatformSpecificTooling();
expectExists(project.android.hostAppGradleRoot.childFile('settings.gradle'));
expectExists(project.android.hostAppGradleRoot.childFile('local.properties'));
expectExists(androidPluginRegistrant(project.android.hostAppGradleRoot.childDirectory('Flutter')));
});
_testInMemory('creates iOS pod in module', () async {
final FlutterProject project = await aModuleProject();
await project.regeneratePlatformSpecificTooling();
final Directory flutter = project.ios.hostAppRoot.childDirectory('Flutter');
expectExists(flutter.childFile('podhelper.rb'));
expectExists(flutter.childFile('flutter_export_environment.sh'));
expectExists(flutter.childFile('Generated.xcconfig'));
final Directory pluginRegistrantClasses = flutter
.childDirectory('FlutterPluginRegistrant')
.childDirectory('Classes');
expectExists(pluginRegistrantClasses.childFile('GeneratedPluginRegistrant.h'));
expectExists(pluginRegistrantClasses.childFile('GeneratedPluginRegistrant.m'));
});
testUsingContext('Version.json info is correct', () {
final MemoryFileSystem fileSystem = MemoryFileSystem.test();
final FlutterManifest manifest = FlutterManifest.createFromString('''
name: test
version: 1.0.0+3
''', logger: BufferLogger.test())!;
final FlutterProject project = FlutterProject(fileSystem.systemTempDirectory, manifest, manifest);
final Map<String, dynamic> versionInfo = jsonDecode(project.getVersionInfo()) as Map<String, dynamic>;
expect(versionInfo['app_name'],'test');
expect(versionInfo['version'],'1.0.0');
expect(versionInfo['build_number'],'3');
expect(versionInfo['package_name'],'test');
});
_testInMemory('gets xcworkspace directory', () async {
final FlutterProject project = await someProject();
project.ios.xcodeProject.createSync();
project.ios.hostAppRoot.childFile('._Runner.xcworkspace').createSync(recursive: true);
project.ios.hostAppRoot.childFile('Runner.xcworkspace').createSync(recursive: true);
expect(project.ios.xcodeWorkspace?.basename, 'Runner.xcworkspace');
});
_testInMemory('no xcworkspace directory found', () async {
final FlutterProject project = await someProject();
project.ios.xcodeProject.createSync();
expect(project.ios.xcodeWorkspace?.basename, null);
});
});
group('module status', () {
_testInMemory('is known for module', () async {
final FlutterProject project = await aModuleProject();
expect(project.isModule, isTrue);
expect(project.android.isModule, isTrue);
expect(project.ios.isModule, isTrue);
expect(project.android.hostAppGradleRoot.basename, '.android');
expect(project.ios.hostAppRoot.basename, '.ios');
});
_testInMemory('is known for non-module', () async {
final FlutterProject project = await someProject();
expect(project.isModule, isFalse);
expect(project.android.isModule, isFalse);
expect(project.ios.isModule, isFalse);
expect(project.android.hostAppGradleRoot.basename, 'android');
expect(project.ios.hostAppRoot.basename, 'ios');
});
});
group('example', () {
_testInMemory('exists for plugin in legacy format', () async {
final FlutterProject project = await aPluginProject();
expect(project.isPlugin, isTrue);
expect(project.hasExampleApp, isTrue);
});
_testInMemory('exists for plugin in multi-platform format', () async {
final FlutterProject project = await aPluginProject(legacy: false);
expect(project.hasExampleApp, isTrue);
});
_testInMemory('does not exist for non-plugin', () async {
final FlutterProject project = await someProject();
expect(project.isPlugin, isFalse);
expect(project.hasExampleApp, isFalse);
});
});
group('usesSwiftPackageManager', () {
testUsingContext('is true when iOS project exists', () async {
final MemoryFileSystem fs = MemoryFileSystem.test();
final Directory projectDirectory = fs.directory('path');
projectDirectory.childDirectory('ios').createSync(recursive: true);
final FlutterManifest manifest = FakeFlutterManifest();
final FlutterProject project = FlutterProject(projectDirectory, manifest, manifest);
expect(project.usesSwiftPackageManager, isTrue);
}, overrides: <Type, Generator>{
FeatureFlags: () => TestFeatureFlags(isSwiftPackageManagerEnabled: true),
XcodeProjectInterpreter: () => FakeXcodeProjectInterpreter(version: Version(15, 0, 0)),
});
testUsingContext('is true when macOS project exists', () async {
final MemoryFileSystem fs = MemoryFileSystem.test();
final Directory projectDirectory = fs.directory('path');
projectDirectory.childDirectory('macos').createSync(recursive: true);
final FlutterManifest manifest = FakeFlutterManifest();
final FlutterProject project = FlutterProject(projectDirectory, manifest, manifest);
expect(project.usesSwiftPackageManager, isTrue);
}, overrides: <Type, Generator>{
FeatureFlags: () => TestFeatureFlags(isSwiftPackageManagerEnabled: true),
XcodeProjectInterpreter: () => FakeXcodeProjectInterpreter(version: Version(15, 0, 0)),
});
testUsingContext('is false when disabled via manifest', () async {
final MemoryFileSystem fs = MemoryFileSystem.test();
final Directory projectDirectory = fs.directory('path');
projectDirectory.childDirectory('ios').createSync(recursive: true);
final FlutterManifest manifest = FakeFlutterManifest(disabledSwiftPackageManager: true);
final FlutterProject project = FlutterProject(projectDirectory, manifest, manifest);
expect(project.usesSwiftPackageManager, isFalse);
}, overrides: <Type, Generator>{
FeatureFlags: () => TestFeatureFlags(isSwiftPackageManagerEnabled: true),
XcodeProjectInterpreter: () => FakeXcodeProjectInterpreter(version: Version(15, 0, 0)),
});
testUsingContext("is false when iOS and macOS project don't exist", () async {
final MemoryFileSystem fs = MemoryFileSystem.test();
final Directory projectDirectory = fs.directory('path');
final FlutterManifest manifest = FakeFlutterManifest();
final FlutterProject project = FlutterProject(projectDirectory, manifest, manifest);
expect(project.usesSwiftPackageManager, isFalse);
}, overrides: <Type, Generator>{
FeatureFlags: () => TestFeatureFlags(isSwiftPackageManagerEnabled: true),
XcodeProjectInterpreter: () => FakeXcodeProjectInterpreter(version: Version(15, 0, 0)),
});
testUsingContext('is false when Xcode is less than 15', () async {
final MemoryFileSystem fs = MemoryFileSystem.test();
final Directory projectDirectory = fs.directory('path');
projectDirectory.childDirectory('ios').createSync(recursive: true);
final FlutterManifest manifest = FakeFlutterManifest();
final FlutterProject project = FlutterProject(projectDirectory, manifest, manifest);
expect(project.usesSwiftPackageManager, isFalse);
}, overrides: <Type, Generator>{
FeatureFlags: () => TestFeatureFlags(isSwiftPackageManagerEnabled: true),
XcodeProjectInterpreter: () => FakeXcodeProjectInterpreter(version: Version(14, 0, 0)),
});
testUsingContext('is false when Swift Package Manager feature is not enabled', () async {
final MemoryFileSystem fs = MemoryFileSystem.test();
final Directory projectDirectory = fs.directory('path');
projectDirectory.childDirectory('ios').createSync(recursive: true);
final FlutterManifest manifest = FakeFlutterManifest();
final FlutterProject project = FlutterProject(projectDirectory, manifest, manifest);
expect(project.usesSwiftPackageManager, isFalse);
}, overrides: <Type, Generator>{
FeatureFlags: () => TestFeatureFlags(),
XcodeProjectInterpreter: () => FakeXcodeProjectInterpreter(version: Version(15, 0, 0)),
});
testUsingContext('is false when project is a module', () async {
final MemoryFileSystem fs = MemoryFileSystem.test();
final Directory projectDirectory = fs.directory('path');
projectDirectory.childDirectory('ios').createSync(recursive: true);
final FlutterManifest manifest = FakeFlutterManifest(isModule: true);
final FlutterProject project = FlutterProject(projectDirectory, manifest, manifest);
expect(project.usesSwiftPackageManager, isFalse);
}, overrides: <Type, Generator>{
FeatureFlags: () => TestFeatureFlags(isSwiftPackageManagerEnabled: true),
XcodeProjectInterpreter: () => FakeXcodeProjectInterpreter(version: Version(15, 0, 0)),
});
});
group('java gradle agp compatibility', () {
Future<FlutterProject?> configureGradleAgpForTest({
required String gradleV,
required String agpV,
}) async {
final FlutterProject project = await someProject();
addRootGradleFile(project.directory, gradleFileContent: () {
return '''
dependencies {
classpath 'com.android.tools.build:gradle:$agpV'
}
''';
});
addGradleWrapperFile(project.directory, gradleV);
return project;
}
// Tests in this group that use overrides and _testInMemory should
// be placed in their own group to avoid test pollution. This is
// especially important for filesystem.
group('_', () {
final FakeProcessManager processManager;
final Java java;
final AndroidStudio androidStudio;
final FakeAndroidSdkWithDir androidSdk;
final FileSystem fileSystem = getFileSystemForPlatform();
java = FakeJava(version: Version(17, 0, 2));
processManager = FakeProcessManager.empty();
androidStudio = FakeAndroidStudio();
androidSdk =
FakeAndroidSdkWithDir(fileSystem.currentDirectory);
fileSystem.currentDirectory
.childDirectory(androidStudio.javaPath!)
.createSync();
_testInMemory(
'flamingo values are compatible',
() async {
final FlutterProject? project = await configureGradleAgpForTest(
gradleV: '8.0',
agpV: '7.4.2',
);
final CompatibilityResult value =
await project!.android.hasValidJavaGradleAgpVersions();
expect(value.success, isTrue);
},
java: java,
androidStudio: androidStudio,
processManager: processManager,
androidSdk: androidSdk,
);
});
group('_', () {
final FakeProcessManager processManager;
final Java java;
final AndroidStudio androidStudio;
final FakeAndroidSdkWithDir androidSdk;
final FileSystem fileSystem = getFileSystemForPlatform();
java = FakeJava(version: const Version.withText(1, 8, 0, '1.8.0_242'));
processManager = FakeProcessManager.empty();
androidStudio = FakeAndroidStudio();
androidSdk =
FakeAndroidSdkWithDir(fileSystem.currentDirectory);
fileSystem.currentDirectory
.childDirectory(androidStudio.javaPath!)
.createSync();
_testInMemory(
'java 8 era values are compatible',
() async {
final FlutterProject? project = await configureGradleAgpForTest(
gradleV: '6.7.1',
agpV: '4.2.0',
);
final CompatibilityResult value =
await project!.android.hasValidJavaGradleAgpVersions();
expect(value.success, isTrue);
},
java: java,
androidStudio: androidStudio,
processManager: processManager,
androidSdk: androidSdk,
);
});
group('_', () {
final FakeProcessManager processManager;
final Java java;
final AndroidStudio androidStudio;
final FakeAndroidSdkWithDir androidSdk;
final FileSystem fileSystem = getFileSystemForPlatform();
processManager = FakeProcessManager.empty();
java = FakeJava(version: Version(11, 0, 14));
androidStudio = FakeAndroidStudio();
androidSdk =
FakeAndroidSdkWithDir(fileSystem.currentDirectory);
fileSystem.currentDirectory
.childDirectory(androidStudio.javaPath!)
.createSync();
_testInMemory(
'electric eel era values are compatible',
() async {
final FlutterProject? project = await configureGradleAgpForTest(
gradleV: '7.3.3',
agpV: '7.2.0',
);
final CompatibilityResult value =
await project!.android.hasValidJavaGradleAgpVersions();
expect(value.success, isTrue);
},
java: java,
androidStudio: androidStudio,
processManager: processManager,
androidSdk: androidSdk,
);
});
group('_', () {
const String javaV = '17.0.2';
const String gradleV = '6.7.3';
const String agpV = '7.2.0';
final FakeProcessManager processManager;
final Java java;
final AndroidStudio androidStudio;
final FakeAndroidSdkWithDir androidSdk;
final FileSystem fileSystem = getFileSystemForPlatform();
processManager = FakeProcessManager.empty();
java = FakeJava(version: Version.parse(javaV));
androidStudio = FakeAndroidStudio();
androidSdk =
FakeAndroidSdkWithDir(fileSystem.currentDirectory);
fileSystem.currentDirectory
.childDirectory(androidStudio.javaPath!)
.createSync();
_testInMemory(
'incompatible everything',
() async {
final FlutterProject? project = await configureGradleAgpForTest(
gradleV: gradleV,
agpV: agpV,
);
final CompatibilityResult value =
await project!.android.hasValidJavaGradleAgpVersions();
expect(value.success, isFalse);
// Should not have the valid string
expect(
value.description,
isNot(
contains(RegExp(AndroidProject.validJavaGradleAgpString))));
// On gradle/agp error print help url and gradle and agp versions.
expect(value.description,
contains(RegExp(AndroidProject.gradleAgpCompatUrl)));
expect(value.description, contains(RegExp(gradleV)));
expect(value.description, contains(RegExp(agpV)));
// On gradle/agp error print help url and java and gradle versions.
expect(value.description,
contains(RegExp(AndroidProject.javaGradleCompatUrl)));
expect(value.description, contains(RegExp(javaV)));
expect(value.description, contains(RegExp(gradleV)));
},
java: java,
androidStudio: androidStudio,
processManager: processManager,
androidSdk: androidSdk,
);
});
group('_', () {
const String javaV = '17.0.2';
const String gradleV = '6.7.3';
const String agpV = '4.2.0';
final FakeProcessManager processManager;
final Java java;
final AndroidStudio androidStudio;
final FakeAndroidSdkWithDir androidSdk;
final FileSystem fileSystem = getFileSystemForPlatform();
processManager = FakeProcessManager.empty();
java = FakeJava(version: Version(17, 0, 2));
androidStudio = FakeAndroidStudio();
androidSdk =
FakeAndroidSdkWithDir(fileSystem.currentDirectory);
fileSystem.currentDirectory
.childDirectory(androidStudio.javaPath!)
.createSync();
_testInMemory(
'incompatible java/gradle only',
() async {
final FlutterProject? project = await configureGradleAgpForTest(
gradleV: gradleV,
agpV: agpV,
);
final CompatibilityResult value =
await project!.android.hasValidJavaGradleAgpVersions();
expect(value.success, isFalse);
// Should not have the valid string.
expect(
value.description,
isNot(
contains(RegExp(AndroidProject.validJavaGradleAgpString))));
// On gradle/agp error print help url and java and gradle versions.
expect(value.description,
contains(RegExp(AndroidProject.javaGradleCompatUrl)));
expect(value.description, contains(RegExp(javaV)));
expect(value.description, contains(RegExp(gradleV)));
},
java: java,
androidStudio: androidStudio,
processManager: processManager,
androidSdk: androidSdk,
);
});
group('_', () {
final FakeProcessManager processManager;
final Java java;
final AndroidStudio androidStudio;
final FakeAndroidSdkWithDir androidSdk;
final FileSystem fileSystem = getFileSystemForPlatform();
java = FakeJava(version: Version(11, 0, 2));
processManager = FakeProcessManager.empty();
androidStudio = FakeAndroidStudio();
androidSdk =
FakeAndroidSdkWithDir(fileSystem.currentDirectory);
fileSystem.currentDirectory
.childDirectory(androidStudio.javaPath!)
.createSync();
_testInMemory(
'incompatible gradle/agp only',
() async {
const String gradleV = '7.0.3';
const String agpV = '7.1.0';
final FlutterProject? project = await configureGradleAgpForTest(
gradleV: gradleV,
agpV: agpV,
);
final CompatibilityResult value =
await project!.android.hasValidJavaGradleAgpVersions();
expect(value.success, isFalse);
// Should not have the valid string.
expect(
value.description,
isNot(
contains(RegExp(AndroidProject.validJavaGradleAgpString))));
// On gradle/agp error print help url and gradle and agp versions.
expect(value.description,
contains(RegExp(AndroidProject.gradleAgpCompatUrl)));
expect(value.description, contains(RegExp(gradleV)));
expect(value.description, contains(RegExp(agpV)));
},
java: java,
androidStudio: androidStudio,
processManager: processManager,
androidSdk: androidSdk,
);
});
group('_', () {
final FakeProcessManager processManager;
final Java java;
final AndroidStudio androidStudio;
final FakeAndroidSdkWithDir androidSdk;
final FileSystem fileSystem = getFileSystemForPlatform();
java = FakeJava(version: Version(11, 0, 2));
processManager = FakeProcessManager.empty();
androidStudio = FakeAndroidStudio();
androidSdk =
FakeAndroidSdkWithDir(fileSystem.currentDirectory);
fileSystem.currentDirectory
.childDirectory(androidStudio.javaPath!)
.createSync();
_testInMemory(
'null agp only',
() async {
const String gradleV = '7.0.3';
final FlutterProject? project = await configureGradleAgpForTest(
gradleV: gradleV,
agpV: '',
);
final CompatibilityResult value =
await project!.android.hasValidJavaGradleAgpVersions();
expect(value.success, isFalse);
// Should not have the valid string.
expect(
value.description,
isNot(
contains(RegExp(AndroidProject.validJavaGradleAgpString))));
// On gradle/agp error print help url null value for agp.
expect(value.description,
contains(RegExp(AndroidProject.gradleAgpCompatUrl)));
expect(value.description, contains(RegExp(gradleV)));
expect(value.description, contains(RegExp('null')));
},
java: java,
androidStudio: androidStudio,
processManager: processManager,
androidSdk: androidSdk,
);
});
});
group('language', () {
late XcodeProjectInterpreter xcodeProjectInterpreter;
late MemoryFileSystem fs;
late FlutterProjectFactory flutterProjectFactory;
setUp(() {
fs = MemoryFileSystem.test();
xcodeProjectInterpreter = XcodeProjectInterpreter.test(processManager: FakeProcessManager.any());
flutterProjectFactory = FlutterProjectFactory(
logger: logger,
fileSystem: fs,
);
});
_testInMemory('default host app language', () async {
final FlutterProject project = await someProject();
expect(project.android.isKotlin, isFalse);
});
testUsingContext('kotlin host app language', () async {
final FlutterProject project = await someProject();
addAndroidGradleFile(project.directory,
gradleFileContent: () {
return '''
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
''';
});
expect(project.android.isKotlin, isTrue);
}, overrides: <Type, Generator>{
FileSystem: () => fs,
ProcessManager: () => FakeProcessManager.any(),
XcodeProjectInterpreter: () => xcodeProjectInterpreter,
FlutterProjectFactory: () => flutterProjectFactory,
});
testUsingContext('kotlin host app language with Gradle Kotlin DSL', () async {
final FlutterProject project = await someProject();
addAndroidGradleFile(project.directory,
kotlinDsl: true,
gradleFileContent: () {
return '''
plugins {
id "com.android.application"
id "kotlin-android"
id "dev.flutter.flutter-gradle-plugin"
}
''';
});
expect(project.android.isKotlin, isTrue);
}, overrides: <Type, Generator>{
FileSystem: () => fs,
ProcessManager: () => FakeProcessManager.any(),
XcodeProjectInterpreter: () => xcodeProjectInterpreter,
FlutterProjectFactory: () => flutterProjectFactory,
});
testUsingContext('kotlin host app language with Gradle Kotlin DSL and typesafe plugin id', () async {
final FlutterProject project = await someProject();
addAndroidGradleFile(project.directory,
kotlinDsl: true,
gradleFileContent: () {
return '''
plugins {
id "com.android.application"
id "kotlin-android"
dev.flutter.`flutter-gradle-plugin`
}
''';
});
expect(project.android.isKotlin, isTrue);
}, overrides: <Type, Generator>{
FileSystem: () => fs,
ProcessManager: () => FakeProcessManager.any(),
XcodeProjectInterpreter: () => xcodeProjectInterpreter,
FlutterProjectFactory: () => flutterProjectFactory,
});
testUsingContext('Gradle Groovy files are preferred to Gradle Kotlin files', () async {
final FlutterProject project = await someProject();
addAndroidGradleFile(project.directory,
gradleFileContent: () {
return '''
plugins {
id "com.android.application"
id "dev.flutter.flutter-gradle-plugin"
}
''';
});
addAndroidGradleFile(project.directory,
kotlinDsl: true,
gradleFileContent: () {
return '''
plugins {
id("com.android.application")
id("kotlin-android")
id("dev.flutter.flutter-gradle-plugin")
}
''';
});
expect(project.android.isKotlin, isFalse);
}, overrides: <Type, Generator>{
FileSystem: () => fs,
ProcessManager: () => FakeProcessManager.any(),
XcodeProjectInterpreter: () => xcodeProjectInterpreter,
FlutterProjectFactory: () => flutterProjectFactory,
});
});
group('With mocked context', () {
late MemoryFileSystem fs;
late FakePlistParser testPlistUtils;
late FakeXcodeProjectInterpreter xcodeProjectInterpreter;
late FlutterProjectFactory flutterProjectFactory;
setUp(() {
fs = MemoryFileSystem.test();
testPlistUtils = FakePlistParser();
xcodeProjectInterpreter = FakeXcodeProjectInterpreter();
flutterProjectFactory = FlutterProjectFactory(
fileSystem: fs,
logger: logger,
);
});
void testWithMocks(String description, Future<void> Function() testMethod) {
testUsingContext(description, testMethod, overrides: <Type, Generator>{
FileSystem: () => fs,
ProcessManager: () => FakeProcessManager.any(),
PlistParser: () => testPlistUtils,
XcodeProjectInterpreter: () => xcodeProjectInterpreter,
FlutterProjectFactory: () => flutterProjectFactory,
});
}
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',
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 String outputFilePath = await project.ios.outputsUniversalLinkSettings(
target: 'Runner',
configuration: 'config',
);
final File outputFile = fs.file(outputFilePath);
final Map<String, Object?> json = jsonDecode(outputFile.readAsStringSync()) as Map<String, Object?>;
expect(
json['associatedDomains'],
unorderedEquals(
<String>[
'example.com',
'example2.com',
],
),
);
expect(json['teamIdentifier'], 'ABC');
expect(json['bundleIdentifier'], 'io.flutter.someProject.suffix');
});
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',
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 String outputFilePath = await project.ios.outputsUniversalLinkSettings(
target: 'Runner',
configuration: 'config',
);
final File outputFile = fs.file(outputFilePath);
final Map<String, Object?> json = jsonDecode(outputFile.readAsStringSync()) as Map<String, Object?>;
expect(
json['associatedDomains'],
unorderedEquals(
<String>[
'example.com',
'example2.com',
],
),
);
expect(json['teamIdentifier'], 'ABC');
expect(json['bundleIdentifier'], 'io.flutter.someProject.suffix');
});
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',
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 String outputFilePath = await project.ios.outputsUniversalLinkSettings(
target: 'Runner',
configuration: 'config',
);
final File outputFile = fs.file(outputFilePath);
final Map<String, Object?> json = jsonDecode(outputFile.readAsStringSync()) as Map<String, Object?>;
expect(json['teamIdentifier'], 'ABC');
expect(json['bundleIdentifier'], 'io.flutter.someProject');
expect(json['associatedDomains'], unorderedEquals(<String>[]));
});
});
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);
});
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 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');
});
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 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('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');
});
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);
expect(await project.organizationNames, <String>[]);
});
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);
await expectToolExitLater(
project.ios.productBundleIdentifier(null),
contains('You must specify a --flavor option to select one of the available schemes.'),
);
});
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, packageConfigPath: '.dart_tool/package_config.json');
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, packageConfigPath: '.dart_tool/package_config.json');
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: '"');
});
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: '"');
});
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: "'");
});
expect(await project.ios.productBundleIdentifier(null), 'io.flutter.someProject');
});
});
group('flutterSwiftPackageInProjectSettings', () {
testWithMocks('is false if pbxproj missing', () async {
final FlutterProject project = await someProject();
expect(project.ios.xcodeProjectInfoFile.existsSync(), isFalse);
expect(project.ios.flutterPluginSwiftPackageInProjectSettings, isFalse);
});
testWithMocks('is false if pbxproj does not contain FlutterGeneratedPluginSwiftPackage in build process', () async {
final FlutterProject project = await someProject();
project.ios.xcodeProjectInfoFile.createSync(recursive: true);
expect(project.ios.xcodeProjectInfoFile.existsSync(), isTrue);
expect(project.ios.flutterPluginSwiftPackageInProjectSettings, isFalse);
});
testWithMocks('is true if pbxproj does contain FlutterGeneratedPluginSwiftPackage in build process', () async {
final FlutterProject project = await someProject();
project.ios.xcodeProjectInfoFile.createSync(recursive: true);
project.ios.xcodeProjectInfoFile.writeAsStringSync('''
' 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; };';
''');
expect(project.ios.xcodeProjectInfoFile.existsSync(), isTrue);
expect(project.ios.flutterPluginSwiftPackageInProjectSettings, isTrue);
});
});
});
group('application bundle name', () {
late MemoryFileSystem fs;
late FakeXcodeProjectInterpreter mockXcodeProjectInterpreter;
setUp(() {
fs = MemoryFileSystem.test();
mockXcodeProjectInterpreter = FakeXcodeProjectInterpreter();
});
testUsingContext('app product name defaults to Runner', () async {
final FlutterProject project = await someProject();
expect(await project.ios.productName(null), 'Runner');
}, overrides: <Type, Generator>{
FileSystem: () => fs,
ProcessManager: () => FakeProcessManager.any(),
XcodeProjectInterpreter: () => mockXcodeProjectInterpreter,
});
testUsingContext('app product name xcodebuild settings', () async {
final FlutterProject project = await someProject();
project.ios.xcodeProject.createSync();
const XcodeProjectBuildContext buildContext = XcodeProjectBuildContext(scheme: 'Runner');
mockXcodeProjectInterpreter.buildSettingsByBuildContext[buildContext] = <String, String>{
'PRODUCT_NAME': 'My App',
};
mockXcodeProjectInterpreter.xcodeProjectInfo = XcodeProjectInfo(<String>[], <String>[], <String>['Runner'], logger);
expect(await project.ios.productName(null), 'My App');
}, overrides: <Type, Generator>{
FileSystem: () => fs,
ProcessManager: () => FakeProcessManager.any(),
XcodeProjectInterpreter: () => mockXcodeProjectInterpreter,
});
});
group('organization names set', () {
_testInMemory('is empty, if project not created', () async {
final FlutterProject project = await someProject();
expect(await project.organizationNames, isEmpty);
});
_testInMemory('is empty, if no platform folders exist', () async {
final FlutterProject project = await someProject();
project.directory.createSync();
expect(await project.organizationNames, isEmpty);
});
_testInMemory('is populated from iOS bundle identifier', () async {
final FlutterProject project = await someProject();
addIosProjectFile(project.directory, projectFileContent: () {
return projectFileWithBundleId('io.flutter.someProject', qualifier: "'");
});
expect(await project.organizationNames, <String>['io.flutter']);
});
_testInMemory('is populated from Android application ID', () async {
final FlutterProject project = await someProject();
addAndroidGradleFile(project.directory,
gradleFileContent: () {
return gradleFileWithApplicationId('io.flutter.someproject');
});
expect(await project.organizationNames, <String>['io.flutter']);
});
_testInMemory('is populated from iOS bundle identifier in plugin example', () async {
final FlutterProject project = await someProject();
addIosProjectFile(project.example.directory, projectFileContent: () {
return projectFileWithBundleId('io.flutter.someProject', qualifier: "'");
});
expect(await project.organizationNames, <String>['io.flutter']);
});
_testInMemory('is populated from Android application ID in plugin example', () async {
final FlutterProject project = await someProject();
addAndroidGradleFile(project.example.directory,
gradleFileContent: () {
return gradleFileWithApplicationId('io.flutter.someproject');
});
expect(await project.organizationNames, <String>['io.flutter']);
});
_testInMemory('is populated from Android group in plugin', () async {
final FlutterProject project = await someProject();
addAndroidWithGroup(project.directory, 'io.flutter.someproject');
expect(await project.organizationNames, <String>['io.flutter']);
});
_testInMemory('is singleton, if sources agree', () async {
final FlutterProject project = await someProject();
addIosProjectFile(project.directory, projectFileContent: () {
return projectFileWithBundleId('io.flutter.someProject');
});
addAndroidGradleFile(project.directory,
gradleFileContent: () {
return gradleFileWithApplicationId('io.flutter.someproject');
});
expect(await project.organizationNames, <String>['io.flutter']);
});
_testInMemory('is non-singleton, if sources disagree', () async {
final FlutterProject project = await someProject();
addIosProjectFile(project.directory, projectFileContent: () {
return projectFileWithBundleId('io.flutter.someProject');
});
addAndroidGradleFile(project.directory,
gradleFileContent: () {
return gradleFileWithApplicationId('io.clutter.someproject');
});
expect(
await project.organizationNames,
<String>['io.flutter', 'io.clutter'],
);
});
});
});
group('watch companion', () {
late MemoryFileSystem fs;
late FakePlistParser testPlistParser;
late FakeXcodeProjectInterpreter mockXcodeProjectInterpreter;
late FlutterProjectFactory flutterProjectFactory;
setUp(() {
fs = MemoryFileSystem.test();
testPlistParser = FakePlistParser();
mockXcodeProjectInterpreter = FakeXcodeProjectInterpreter();
flutterProjectFactory = FlutterProjectFactory(
fileSystem: fs,
logger: logger,
);
});
testUsingContext('cannot find bundle identifier', () async {
final FlutterProject project = await someProject();
final XcodeProjectInfo projectInfo = XcodeProjectInfo(<String>['WatchTarget'], <String>[], <String>[], logger);
expect(
await project.ios.containsWatchCompanion(
projectInfo: projectInfo,
buildInfo: BuildInfo.debug,
deviceId: '123',
),
isFalse,
);
}, overrides: <Type, Generator>{
FileSystem: () => fs,
ProcessManager: () => FakeProcessManager.any(),
PlistParser: () => testPlistParser,
XcodeProjectInterpreter: () => mockXcodeProjectInterpreter,
FlutterProjectFactory: () => flutterProjectFactory,
});
group('with bundle identifier', () {
setUp(() {
const XcodeProjectBuildContext buildContext = XcodeProjectBuildContext(scheme: 'Runner');
mockXcodeProjectInterpreter.buildSettingsByBuildContext[buildContext] = <String, String>{
IosProject.kProductBundleIdKey: 'io.flutter.someProject',
};
mockXcodeProjectInterpreter.xcodeProjectInfo = XcodeProjectInfo(<String>['Runner', 'WatchTarget'], <String>[], <String>['Runner', 'WatchScheme'], logger);
});
testUsingContext('no Info.plist in target', () async {
final FlutterProject project = await someProject();
expect(
await project.ios.containsWatchCompanion(
projectInfo: mockXcodeProjectInterpreter.xcodeProjectInfo,
buildInfo: BuildInfo.debug,
deviceId: '123',
),
isFalse,
);
}, overrides: <Type, Generator>{
FileSystem: () => fs,
ProcessManager: () => FakeProcessManager.any(),
PlistParser: () => testPlistParser,
XcodeProjectInterpreter: () => mockXcodeProjectInterpreter,
FlutterProjectFactory: () => flutterProjectFactory,
});
testUsingContext('Info.plist in target does not contain WKCompanionAppBundleIdentifier', () async {
final FlutterProject project = await someProject();
project.ios.hostAppRoot.childDirectory('WatchTarget').childFile('Info.plist').createSync(recursive: true);
expect(
await project.ios.containsWatchCompanion(
projectInfo: mockXcodeProjectInterpreter.xcodeProjectInfo,
buildInfo: BuildInfo.debug,
deviceId: '123',
),
isFalse,
);
}, overrides: <Type, Generator>{
FileSystem: () => fs,
ProcessManager: () => FakeProcessManager.any(),
PlistParser: () => testPlistParser,
XcodeProjectInterpreter: () => mockXcodeProjectInterpreter,
FlutterProjectFactory: () => flutterProjectFactory,
});
testUsingContext('target WKCompanionAppBundleIdentifier is not project bundle identifier', () async {
final FlutterProject project = await someProject();
project.ios.hostAppRoot.childDirectory('WatchTarget').childFile('Info.plist').createSync(recursive: true);
testPlistParser.setProperty('WKCompanionAppBundleIdentifier', 'io.flutter.someOTHERproject');
expect(
await project.ios.containsWatchCompanion(
projectInfo: mockXcodeProjectInterpreter.xcodeProjectInfo,
buildInfo: BuildInfo.debug,
deviceId: '123',
),
isFalse,
);
}, overrides: <Type, Generator>{
FileSystem: () => fs,
ProcessManager: () => FakeProcessManager.any(),
PlistParser: () => testPlistParser,
XcodeProjectInterpreter: () => mockXcodeProjectInterpreter,
FlutterProjectFactory: () => flutterProjectFactory,
});
testUsingContext('has watch companion in plist', () async {
final FlutterProject project = await someProject();
project.ios.xcodeProject.createSync();
project.ios.hostAppRoot.childDirectory('WatchTarget').childFile('Info.plist').createSync(recursive: true);
testPlistParser.setProperty('WKCompanionAppBundleIdentifier', 'io.flutter.someProject');
expect(
await project.ios.containsWatchCompanion(
projectInfo: mockXcodeProjectInterpreter.xcodeProjectInfo,
buildInfo: BuildInfo.debug,
deviceId: '123',
),
isTrue,
);
}, overrides: <Type, Generator>{
FileSystem: () => fs,
ProcessManager: () => FakeProcessManager.any(),
PlistParser: () => testPlistParser,
XcodeProjectInterpreter: () => mockXcodeProjectInterpreter,
FlutterProjectFactory: () => flutterProjectFactory,
});
testUsingContext('has watch companion in plist with xcode variable', () async {
final FlutterProject project = await someProject();
project.ios.xcodeProject.createSync();
const XcodeProjectBuildContext buildContext = XcodeProjectBuildContext(
scheme: 'Runner',
deviceId: '123',
);
mockXcodeProjectInterpreter.buildSettingsByBuildContext[buildContext] = <String, String>{
IosProject.kProductBundleIdKey: 'io.flutter.someProject',
};
project.ios.hostAppRoot.childDirectory('WatchTarget').childFile('Info.plist').createSync(recursive: true);
testPlistParser.setProperty('WKCompanionAppBundleIdentifier', r'$(PRODUCT_BUNDLE_IDENTIFIER)');
expect(
await project.ios.containsWatchCompanion(
projectInfo: mockXcodeProjectInterpreter.xcodeProjectInfo,
buildInfo: BuildInfo.debug,
deviceId: '123',
),
isTrue,
);
}, overrides: <Type, Generator>{
FileSystem: () => fs,
ProcessManager: () => FakeProcessManager.any(),
PlistParser: () => testPlistParser,
XcodeProjectInterpreter: () => mockXcodeProjectInterpreter,
FlutterProjectFactory: () => flutterProjectFactory,
});
testUsingContext('has watch companion in other scheme build settings', () async {
final FlutterProject project = await someProject();
project.ios.xcodeProject.createSync();
project.ios.xcodeProjectInfoFile.writeAsStringSync('''
Build settings for action build and target "WatchTarget":
INFOPLIST_KEY_WKCompanionAppBundleIdentifier = io.flutter.someProject
''');
const XcodeProjectBuildContext buildContext = XcodeProjectBuildContext(
scheme: 'Runner',
deviceId: '123',
);
mockXcodeProjectInterpreter.buildSettingsByBuildContext[buildContext] = <String, String>{
IosProject.kProductBundleIdKey: 'io.flutter.someProject',
};
const XcodeProjectBuildContext watchBuildContext = XcodeProjectBuildContext(
scheme: 'WatchScheme',
deviceId: '123',
sdk: XcodeSdk.WatchOS,
);
mockXcodeProjectInterpreter.buildSettingsByBuildContext[watchBuildContext] = <String, String>{
'INFOPLIST_KEY_WKCompanionAppBundleIdentifier': 'io.flutter.someProject',
};
expect(
await project.ios.containsWatchCompanion(
projectInfo: mockXcodeProjectInterpreter.xcodeProjectInfo,
buildInfo: BuildInfo.debug,
deviceId: '123',
),
isTrue,
);
}, overrides: <Type, Generator>{
FileSystem: () => fs,
ProcessManager: () => FakeProcessManager.any(),
PlistParser: () => testPlistParser,
XcodeProjectInterpreter: () => mockXcodeProjectInterpreter,
FlutterProjectFactory: () => flutterProjectFactory,
});
testUsingContext('has watch companion in other scheme build settings with xcode variable', () async {
final FlutterProject project = await someProject();
project.ios.xcodeProject.createSync();
project.ios.xcodeProjectInfoFile.writeAsStringSync(r'''
Build settings for action build and target "WatchTarget":
INFOPLIST_KEY_WKCompanionAppBundleIdentifier = $(PRODUCT_BUNDLE_IDENTIFIER)
''');
const XcodeProjectBuildContext buildContext = XcodeProjectBuildContext(
scheme: 'Runner',
deviceId: '123'
);
mockXcodeProjectInterpreter.buildSettingsByBuildContext[buildContext] = <String, String>{
IosProject.kProductBundleIdKey: 'io.flutter.someProject',
};
const XcodeProjectBuildContext watchBuildContext = XcodeProjectBuildContext(
scheme: 'WatchScheme',
deviceId: '123',
sdk: XcodeSdk.WatchOS,
);
mockXcodeProjectInterpreter.buildSettingsByBuildContext[watchBuildContext] = <String, String>{
IosProject.kProductBundleIdKey: 'io.flutter.someProject',
'INFOPLIST_KEY_WKCompanionAppBundleIdentifier': r'$(PRODUCT_BUNDLE_IDENTIFIER)',
};
expect(
await project.ios.containsWatchCompanion(
projectInfo: mockXcodeProjectInterpreter.xcodeProjectInfo,
buildInfo: BuildInfo.debug,
deviceId: '123',
),
isTrue,
);
}, overrides: <Type, Generator>{
FileSystem: () => fs,
ProcessManager: () => FakeProcessManager.any(),
PlistParser: () => testPlistParser,
XcodeProjectInterpreter: () => mockXcodeProjectInterpreter,
FlutterProjectFactory: () => flutterProjectFactory,
});
});
});
}
Future<FlutterProject> someProject({
String? androidManifestOverride,
bool includePubspec = false,
}) async {
final Directory directory = globals.fs.directory('some_project');
directory.childDirectory('.dart_tool')
.childFile('package_config.json')
..createSync(recursive: true)
..writeAsStringSync('{"configVersion":2,"packages":[]}');
if (includePubspec) {
directory.childFile('pubspec.yaml')
..createSync(recursive: true)
..writeAsStringSync(validPubspec);
}
directory.childDirectory('ios').createSync(recursive: true);
final Directory androidDirectory = directory
.childDirectory('android')
..createSync(recursive: true);
androidDirectory
.childFile('AndroidManifest.xml')
.writeAsStringSync(androidManifestOverride ?? '<manifest></manifest>');
return FlutterProject.fromDirectory(directory);
}
Future<FlutterProject> aPluginProject({bool legacy = true}) async {
final Directory directory = globals.fs.directory('plugin_project');
directory.childDirectory('ios').createSync(recursive: true);
directory.childDirectory('android').createSync(recursive: true);
directory.childDirectory('example').createSync(recursive: true);
String pluginPubSpec;
if (legacy) {
pluginPubSpec = '''
name: my_plugin
flutter:
plugin:
androidPackage: com.example
pluginClass: MyPlugin
iosPrefix: FLT
''';
} else {
pluginPubSpec = '''
name: my_plugin
flutter:
plugin:
platforms:
android:
package: com.example
pluginClass: MyPlugin
ios:
pluginClass: MyPlugin
linux:
pluginClass: MyPlugin
macos:
pluginClass: MyPlugin
windows:
pluginClass: MyPlugin
''';
}
directory.childFile('pubspec.yaml').writeAsStringSync(pluginPubSpec);
return FlutterProject.fromDirectory(directory);
}
Future<FlutterProject> aModuleProject() async {
final Directory directory = globals.fs.directory('module_project');
directory
.childDirectory('.dart_tool')
.childFile('package_config.json')
..createSync(recursive: true)
..writeAsStringSync('{"configVersion":2,"packages":[]}');
directory.childFile('pubspec.yaml').writeAsStringSync('''
name: my_module
flutter:
module:
androidPackage: com.example
''');
return FlutterProject.fromDirectory(directory);
}
/// Executes the [testMethod] in a context where the file system
/// is in memory.
@isTest
void _testInMemory(
String description,
Future<void> Function() testMethod, {
FileSystem? fileSystem,
Java? java,
AndroidStudio? androidStudio,
ProcessManager? processManager,
AndroidSdk? androidSdk,
}) {
Cache.flutterRoot = getFlutterRoot();
final FileSystem testFileSystem = fileSystem ?? getFileSystemForPlatform();
testFileSystem.directory('.dart_tool').childFile('package_config.json')
..createSync(recursive: true)
..writeAsStringSync('{"configVersion":2,"packages":[]}');
// Transfer needed parts of the Flutter installation folder
// to the in-memory file system used during testing.
final Logger logger = BufferLogger.test();
transfer(
Cache(
fileSystem: globals.fs,
logger: logger,
artifacts: <ArtifactSet>[],
osUtils: OperatingSystemUtils(
fileSystem: globals.fs,
logger: logger,
platform: globals.platform,
processManager: globals.processManager,
),
platform: globals.platform,
).getArtifactDirectory('gradle_wrapper'),
testFileSystem);
transfer(
globals.fs
.directory(Cache.flutterRoot)
.childDirectory('packages')
.childDirectory('flutter_tools')
.childDirectory('templates'),
testFileSystem);
// Set up enough of the packages to satisfy the templating code.
final File packagesFile = testFileSystem
.directory(Cache.flutterRoot)
.childDirectory('packages')
.childDirectory('flutter_tools')
.childDirectory('.dart_tool')
.childFile('package_config.json');
final Directory dummyTemplateImagesDirectory =
testFileSystem.directory(Cache.flutterRoot).parent;
dummyTemplateImagesDirectory.createSync(recursive: true);
packagesFile.createSync(recursive: true);
packagesFile.writeAsStringSync(json.encode(<String, Object>{
'configVersion': 2,
'packages': <Object>[
<String, Object>{
'name': 'flutter_template_images',
'rootUri': dummyTemplateImagesDirectory.uri.toString(),
'packageUri': 'lib/',
'languageVersion': '2.6',
},
],
}));
testUsingContext(
description,
testMethod,
overrides: <Type, Generator>{
FileSystem: () => testFileSystem,
ProcessManager: () => processManager ?? FakeProcessManager.any(),
Java : () => java,
AndroidStudio: () => androidStudio ?? FakeAndroidStudio(),
// Intentionally null if not set. Some ios tests fail if this is a fake.
AndroidSdk: () => androidSdk,
Cache: () => Cache(
logger: globals.logger,
fileSystem: testFileSystem,
osUtils: globals.os,
platform: globals.platform,
artifacts: <ArtifactSet>[],
),
FlutterProjectFactory: () => FlutterProjectFactory(
fileSystem: testFileSystem,
logger: globals.logger,
),
},
);
}
/// Transfers files and folders from the local file system's Flutter
/// installation to an (in-memory) file system used for testing.
void transfer(FileSystemEntity entity, FileSystem target) {
if (entity is Directory) {
target.directory(entity.absolute.path).createSync(recursive: true);
for (final FileSystemEntity child in entity.listSync()) {
transfer(child, target);
}
} else if (entity is File) {
target.file(entity.absolute.path).writeAsBytesSync(entity.readAsBytesSync(), flush: true);
} else {
throw Exception('Unsupported FileSystemEntity ${entity.runtimeType}');
}
}
void expectExists(FileSystemEntity entity) {
expect(entity.existsSync(), isTrue);
}
void expectNotExists(FileSystemEntity entity) {
expect(entity.existsSync(), isFalse);
}
void addIosProjectFile(Directory directory, {required String Function() projectFileContent}) {
directory
.childDirectory('ios')
.childDirectory('Runner.xcodeproj')
.childFile('project.pbxproj')
..createSync(recursive: true)
..writeAsStringSync(projectFileContent());
}
/// Adds app-level Gradle Groovy build file (build.gradle) to [directory].
///
/// If [kotlinDsl] is true, then build.gradle.kts is created instead of
/// build.gradle. It's the caller's responsibility to make sure that
/// [gradleFileContent] is consistent with the value of the [kotlinDsl] flag.
void addAndroidGradleFile(Directory directory, {
required String Function() gradleFileContent, bool kotlinDsl = false,
}) {
directory
.childDirectory('android')
.childDirectory('app')
.childFile(kotlinDsl ? 'build.gradle.kts' : 'build.gradle')
..createSync(recursive: true)
..writeAsStringSync(gradleFileContent());
}
void addRootGradleFile(Directory directory,
{required String Function() gradleFileContent}) {
directory.childDirectory('android').childFile('build.gradle')
..createSync(recursive: true)
..writeAsStringSync(gradleFileContent());
}
void addGradleWrapperFile(Directory directory, String gradleVersion) {
directory
.childDirectory('android')
.childDirectory(gradle_utils.gradleDirectoryName)
.childDirectory(gradle_utils.gradleWrapperDirectoryName)
.childFile(gradle_utils.gradleWrapperPropertiesFilename)
..createSync(recursive: true)
..writeAsStringSync('''
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https://services.gradle.org/distributions/gradle-$gradleVersion-all.zip
''');
}
FileSystem getFileSystemForPlatform() {
return MemoryFileSystem(
style: globals.platform.isWindows
? FileSystemStyle.windows
: FileSystemStyle.posix,
);
}
void addAndroidWithGroup(Directory directory, String id, {bool kotlinDsl = false}) {
directory.childDirectory('android').childFile(kotlinDsl ? 'build.gradle.kts' : 'build.gradle')
..createSync(recursive: true)
..writeAsStringSync(gradleFileWithGroupId(id));
}
String get validPubspec => '''
name: hello
flutter:
''';
String get validPubspecWithDependencies => '''
name: hello
flutter:
dependencies:
plugin_a:
plugin_b:
''';
String get invalidPubspec => '''
name: hello
flutter:
invalid:
''';
String get parseErrorPubspec => '''
name: hello
# Whitespace is important.
flutter:
something:
something_else:
''';
String projectFileWithBundleId(String id, {String? qualifier}) {
return '''
97C147061CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = {
PRODUCT_BUNDLE_IDENTIFIER = ${qualifier ?? ''}$id${qualifier ?? ''};
PRODUCT_NAME = "\$(TARGET_NAME)";
};
name = Debug;
};
''';
}
String gradleFileWithApplicationId(String id) {
return '''
apply plugin: 'com.android.application'
android {
compileSdk 34
defaultConfig {
applicationId '$id'
}
}
''';
}
String gradleFileWithGroupId(String id) {
return '''
group '$id'
version '1.0-SNAPSHOT'
apply plugin: 'com.android.library'
android {
compileSdk 34
}
''';
}
File androidPluginRegistrant(Directory parent) {
return parent.childDirectory('src')
.childDirectory('main')
.childDirectory('java')
.childDirectory('io')
.childDirectory('flutter')
.childDirectory('plugins')
.childFile('GeneratedPluginRegistrant.java');
}
class FakeXcodeProjectInterpreter extends Fake implements XcodeProjectInterpreter {
FakeXcodeProjectInterpreter({
this.version,
});
final Map<XcodeProjectBuildContext, Map<String, String>> buildSettingsByBuildContext = <XcodeProjectBuildContext, Map<String, String>>{};
late XcodeProjectInfo xcodeProjectInfo;
@override
Future<Map<String, String>> getBuildSettings(String projectPath, {
XcodeProjectBuildContext? buildContext,
Duration timeout = const Duration(minutes: 1),
}) async {
if (buildSettingsByBuildContext[buildContext] == null) {
return <String, String>{};
}
return buildSettingsByBuildContext[buildContext]!;
}
@override
Future<XcodeProjectInfo> getInfo(String projectPath, {String? projectFilename}) async {
return xcodeProjectInfo;
}
@override
bool get isInstalled => true;
@override
Version? version;
}
class FakeAndroidSdkWithDir extends Fake implements AndroidSdk {
FakeAndroidSdkWithDir(this._directory);
final Directory _directory;
@override
Directory get directory => _directory;
}
class FakeFlutterManifest extends Fake implements FlutterManifest {
FakeFlutterManifest({
this.disabledSwiftPackageManager = false,
this.isModule = false,
});
@override
bool disabledSwiftPackageManager;
@override
bool isModule;
}