mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
(Please be generous for some typos and grammar error in sentences below. English is not my mother tongue) TRDR : This patch fixes flutter run(and might be flutter build too) failure on iOS flutter apps with companion watchOS app in case the WKCompanionAppBundleIdentifier value is defined by xcode project configuration and uses different app bundle id by scheme(which happens when you do something like com.myapp.app1.dev things for scheme bundle id) This change checks default scheme (the scheme with corresponding name for the debug/release mode and selected build flavor.) in the build settings check step of containsWatchCompanion function. current code of containsWatchCompanion function works like below 1. check all default Info.plist file's content of all targets to determine if the project has watchOS companion apps. (this doesn't work well in mordern xcode settings when they use multi plist files which are selected on build time by build configuration. Sometimes, the default Info.plist file doesn't even exist in the project.) 2. check if "WKCompanionAppBundleIdentifier" is included in the xcode project info file(the ios/Runner.xcodeproj/project.pbxproj file in case of iOS project generated by flutter) 3. If "WKCompanionAppBundleIdentifier" has found in the project info file, check build configuration variables of every single scheme but the current target scheme if they have config value with key "INFOPLIST_KEY_WKCompanionAppBundleIdentifier" identical to current build configuration's bundle identifier returned by productBundleIdentifier@xcode_project.dart function. I believe The third step causes many problems reported in some issue and pr, saying iOS app with companion watchOS app fails on build or run. https://github.com/flutter/flutter/issues/160622 https://github.com/flutter/flutter/pull/172436 In my case, My iOS project had multiple schemes which are linked to different build configurations, but using single Runner setting per app. because we exclude current scheme for the check, we get all the INFOPLIST_KEY_WKCompanionAppBundleIdentifier values except the one which should be same with the identifier returned by productBundleIdentifier@xcode_project.dart function. that is because companion watch apps doesn't have to have same WKCompanionAppBundleIdentifier with other scheme's main app. We have to check the default scheme to see watchOS companion app`s WKCompanionAppBundleIdentifier, not other scheme. (other schemes can have slightly different or totally different bundle id. that was to support different environment systems like dev, prod in my case) as a result, flutter run gave me WatchOS app built for device target iOS/iPad, not Watch. which caused problem during installation of iOS app to a simulator because the embeded Watch App was in invalid format. I found this by inspecting Info.plist of generated watchOS app. below is part of the flutter run verbose log with failure caused by old code. [+1154 ms] An error was encountered processing the command (domain=IXUserPresentableErrorDomain, code=1): App installation failed: ‘Bora Debug-dev’을(를) 설치할 수 없음 (which says "cannot install" in korean) 나중에 다시 시도하십시오. (which says "try again later" in korean) Found WatchKit 2.0 app at /Users/javalia/Library/Developer/CoreSimulator/Devices/071691B3-7901-47E5-9B38-4D5B799F3530/data/Library/Caches/com.apple.mobile.installd.staging/temp.8HZIEG/extracted/Payload/Runner.app/Watch/bora Watch App.app but it does not have a WKWatchKitApp or WKApplication key set to true in its Info.plist Underlying error (domain=IXUserPresentableErrorDomain, code=1): ‘Bora Debug-dev’을(를) 설치할 수 없음 (which says "cannot install" in korean) 나중에 다시 시도하십시오. (which says "try it later" in korean) I suggest this change with two reasons : 1. I think forcing watchOS app makers split their runner configurations to support watch companion app is something not recommendable/not good to force as a convention. (Which is implied in the comment which is removed by this pr, and could be one of the valid detouring. some people in this issue https://github.com/flutter/flutter/issues/160622 said that a detouring is adding of dummy WKCompanionAppBundleIdentifier configuration in iOS app, which should be in only watchOS apps in normal.) 2. The function should work as it's name implies, so I guess it should not omit default scheme during check. Matching function's behavior with it's name will be likely to reduce potential errors caused by this change while fixing problems. ## Pre-launch Checklist - [ X ] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [ X ] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [ X ] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [ X ] I signed the [CLA]. - [ X ] I listed at least one issue that this PR fixes in the description above. - [ ] I updated/added relevant documentation (doc comments with `///`). - [ ] I added new tests to check the change I am making, or this PR is [test-exempt]. - [ ] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [ ] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. <!-- Links --> [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --------- Co-authored-by: hellohuanlin <41930132+hellohuanlin@users.noreply.github.com>
2567 lines
98 KiB
Dart
2567 lines
98 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.
|
|
|
|
/// @docImport 'package:yaml_edit/yaml_edit.dart';
|
|
library;
|
|
|
|
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/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/dart/pub.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';
|
|
import '../src/package_config.dart';
|
|
import '../src/throwing_pub.dart';
|
|
|
|
void main() {
|
|
// TODO(zanderso): remove once FlutterProject is fully refactored.
|
|
// this is safe since no tests have expectations on the test logger.
|
|
final logger = BufferLogger.test();
|
|
|
|
group('Project', () {
|
|
group('construction', () {
|
|
testWithoutContext('invalid utf8 throws a tool exit', () {
|
|
final FileSystem fileSystem = MemoryFileSystem.test();
|
|
final 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 project = FlutterProject(
|
|
globals.fs.directory('not_created'),
|
|
FlutterManifest.empty(logger: logger),
|
|
FlutterManifest.empty(logger: logger),
|
|
);
|
|
await project.regeneratePlatformSpecificTooling(releaseMode: false);
|
|
expectNotExists(project.directory);
|
|
});
|
|
_testInMemory('does nothing in plugin or package root project', () async {
|
|
final FlutterProject project = await aPluginProject();
|
|
await project.regeneratePlatformSpecificTooling(releaseMode: false);
|
|
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(releaseMode: false);
|
|
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(releaseMode: false);
|
|
expectExists(
|
|
project.ios.hostAppRoot.childDirectory('Runner').childFile('GeneratedPluginRegistrant.h'),
|
|
);
|
|
});
|
|
_testInMemory('generates Xcode configuration for iOS', () async {
|
|
final FlutterProject project = await someProject();
|
|
await project.regeneratePlatformSpecificTooling(releaseMode: false);
|
|
expectExists(
|
|
project.ios.hostAppRoot.childDirectory('Flutter').childFile('Generated.xcconfig'),
|
|
);
|
|
});
|
|
_testInMemory('injects plugins for Android', () async {
|
|
final FlutterProject project = await someProject();
|
|
await project.regeneratePlatformSpecificTooling(releaseMode: false);
|
|
expectExists(
|
|
androidPluginRegistrant(project.android.hostAppGradleRoot.childDirectory('app')),
|
|
);
|
|
});
|
|
_testInMemory('updates local properties for Android', () async {
|
|
final FlutterProject project = await someProject();
|
|
await project.regeneratePlatformSpecificTooling(releaseMode: false);
|
|
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 invalidManifest = '<manifest></application>';
|
|
final FlutterProject project = await someProject(androidManifestOverride: invalidManifest);
|
|
|
|
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();
|
|
// 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(includePubspec: false);
|
|
// 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(releaseMode: false);
|
|
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(releaseMode: false);
|
|
expectExists(project.android.hostAppGradleRoot.childFile('local.properties'));
|
|
});
|
|
|
|
testUsingContext(
|
|
'determines dev dependencies',
|
|
() async {
|
|
// Create a plugin.
|
|
await aPluginProject(legacy: false);
|
|
// Create a project that depends on that plugin.
|
|
final FlutterProject project = await projectWithPluginDependency();
|
|
// Don't bother with Android, we just want the manifest.
|
|
project.directory.childDirectory('android').deleteSync(recursive: true);
|
|
|
|
await project.regeneratePlatformSpecificTooling(releaseMode: false);
|
|
expect(
|
|
project.flutterPluginsDependenciesFile.readAsStringSync(),
|
|
contains('"dev_dependency":true'),
|
|
);
|
|
},
|
|
overrides: <Type, Generator>{
|
|
FileSystem: () => MemoryFileSystem.test(),
|
|
ProcessManager: () => FakeProcessManager.any(),
|
|
Pub: ThrowingPub.new,
|
|
FlutterProjectFactory: () =>
|
|
FlutterProjectFactory(logger: logger, fileSystem: globals.fs),
|
|
},
|
|
);
|
|
|
|
testUsingContext(
|
|
'releaseMode: false retains dev plugins',
|
|
() async {
|
|
// Create a plugin.
|
|
await aPluginProject(includeAndroidMain: true, legacy: false);
|
|
// Create a project that depends on that plugin.
|
|
final FlutterProject project = await projectWithPluginDependency();
|
|
|
|
await project.regeneratePlatformSpecificTooling(releaseMode: false);
|
|
expect(
|
|
project.android.generatedPluginRegistrantFile.readAsStringSync(),
|
|
contains('MyPlugin'),
|
|
);
|
|
},
|
|
overrides: <Type, Generator>{
|
|
FileSystem: () => MemoryFileSystem.test(),
|
|
ProcessManager: () => FakeProcessManager.any(),
|
|
Pub: ThrowingPub.new,
|
|
FlutterProjectFactory: () =>
|
|
FlutterProjectFactory(logger: logger, fileSystem: globals.fs),
|
|
},
|
|
);
|
|
|
|
testUsingContext(
|
|
'releaseMode: true omits dev plugins',
|
|
() async {
|
|
// Create a plugin.
|
|
await aPluginProject(includeAndroidMain: true, legacy: false);
|
|
// Create a project that depends on that plugin.
|
|
final FlutterProject project = await projectWithPluginDependency();
|
|
|
|
await project.regeneratePlatformSpecificTooling(releaseMode: true);
|
|
expect(
|
|
project.android.generatedPluginRegistrantFile.readAsStringSync(),
|
|
isNot(contains('MyPlugin')),
|
|
);
|
|
},
|
|
overrides: <Type, Generator>{
|
|
FileSystem: () => MemoryFileSystem.test(),
|
|
ProcessManager: () => FakeProcessManager.any(),
|
|
Pub: ThrowingPub.new,
|
|
FlutterProjectFactory: () =>
|
|
FlutterProjectFactory(logger: logger, fileSystem: globals.fs),
|
|
},
|
|
);
|
|
|
|
testUsingContext(
|
|
'injects plugins for macOS',
|
|
() async {
|
|
final FlutterProject project = await someProject();
|
|
project.macos.managedDirectory.createSync(recursive: true);
|
|
await project.regeneratePlatformSpecificTooling(releaseMode: false);
|
|
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(releaseMode: false);
|
|
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(releaseMode: false);
|
|
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(releaseMode: false);
|
|
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(releaseMode: false);
|
|
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(releaseMode: false);
|
|
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 fileSystem = MemoryFileSystem.test();
|
|
final FlutterManifest manifest = FlutterManifest.createFromString('''
|
|
name: test
|
|
version: 1.0.0+3
|
|
''', logger: BufferLogger.test())!;
|
|
final project = FlutterProject(fileSystem.systemTempDirectory, manifest, manifest);
|
|
final 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('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.list(<FakeCommand>[createKgpVersionCommand('1.9.20')]);
|
|
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.list(<FakeCommand>[createKgpVersionCommand('1.7.20')]);
|
|
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.list(<FakeCommand>[createKgpVersionCommand('1.9.1')]);
|
|
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 javaV = '17.0.2';
|
|
const gradleV = '6.7.3';
|
|
const agpV = '7.2.0';
|
|
const kgpV = '2.1.0';
|
|
|
|
final FakeProcessManager processManager;
|
|
final Java java;
|
|
final AndroidStudio androidStudio;
|
|
final FakeAndroidSdkWithDir androidSdk;
|
|
final FileSystem fileSystem = getFileSystemForPlatform();
|
|
processManager = FakeProcessManager.list(<FakeCommand>[createKgpVersionCommand(kgpV)]);
|
|
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.validJavaGradleAgpKgpString))),
|
|
);
|
|
// 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)));
|
|
// On kgp/gradle eror print help url and kgp versions
|
|
expect(value.description, contains(RegExp(kgpV)));
|
|
expect(value.description, contains(RegExp('KGP/Gradle')));
|
|
expect(value.description, contains(RegExp(AndroidProject.kgpCompatUrl)));
|
|
// On agp/kgp error print help url and agp and kgp versions
|
|
expect(value.description, contains(RegExp(agpV)));
|
|
expect(value.description, contains(RegExp(kgpV)));
|
|
expect(value.description, contains(RegExp('AGP/KGP')));
|
|
expect(value.description, contains(RegExp(AndroidProject.kgpCompatUrl)));
|
|
},
|
|
java: java,
|
|
androidStudio: androidStudio,
|
|
processManager: processManager,
|
|
androidSdk: androidSdk,
|
|
);
|
|
});
|
|
group('_', () {
|
|
const javaV = '17.0.2';
|
|
const gradleV = '6.7.3';
|
|
const agpV = '4.2.0';
|
|
const kgpV = '1.7.22';
|
|
|
|
final FakeProcessManager processManager;
|
|
final Java java;
|
|
final AndroidStudio androidStudio;
|
|
final FakeAndroidSdkWithDir androidSdk;
|
|
final FileSystem fileSystem = getFileSystemForPlatform();
|
|
processManager = FakeProcessManager.list(<FakeCommand>[createKgpVersionCommand(kgpV)]);
|
|
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.validJavaGradleAgpKgpString))),
|
|
);
|
|
// 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.any();
|
|
androidStudio = FakeAndroidStudio();
|
|
androidSdk = FakeAndroidSdkWithDir(fileSystem.currentDirectory);
|
|
fileSystem.currentDirectory.childDirectory(androidStudio.javaPath!).createSync();
|
|
_testInMemory(
|
|
'incompatible gradle/agp only',
|
|
() async {
|
|
const gradleV = '7.0.3';
|
|
const 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.validJavaGradleAgpKgpString))),
|
|
);
|
|
// 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('_', () {
|
|
const gradleV = '8.11';
|
|
const agpV = '8.7.2';
|
|
const kgpV = '2.1.10';
|
|
|
|
final FakeProcessManager processManager;
|
|
final Java java;
|
|
final AndroidStudio androidStudio;
|
|
final FakeAndroidSdkWithDir androidSdk;
|
|
final FileSystem fileSystem = getFileSystemForPlatform();
|
|
processManager = FakeProcessManager.list(<FakeCommand>[createKgpVersionCommand(kgpV)]);
|
|
java = FakeJava(version: Version(17, 0, 2));
|
|
androidStudio = FakeAndroidStudio();
|
|
androidSdk = FakeAndroidSdkWithDir(fileSystem.currentDirectory);
|
|
fileSystem.currentDirectory.childDirectory(androidStudio.javaPath!).createSync();
|
|
_testInMemory(
|
|
'incompatible kgp/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.validJavaGradleAgpKgpString))),
|
|
);
|
|
// On gradle/agp error print help url and java and gradle versions.
|
|
expect(value.description, contains(RegExp(AndroidProject.kgpCompatUrl)));
|
|
expect(value.description, contains(RegExp(kgpV)));
|
|
expect(value.description, contains(RegExp(gradleV)));
|
|
},
|
|
java: java,
|
|
androidStudio: androidStudio,
|
|
processManager: processManager,
|
|
androidSdk: androidSdk,
|
|
);
|
|
});
|
|
group('_', () {
|
|
const gradleV = '8.9';
|
|
const agpV = '8.7.2';
|
|
const kgpV = '2.0.20';
|
|
|
|
final FakeProcessManager processManager;
|
|
final Java java;
|
|
final AndroidStudio androidStudio;
|
|
final FakeAndroidSdkWithDir androidSdk;
|
|
final FileSystem fileSystem = getFileSystemForPlatform();
|
|
processManager = FakeProcessManager.list(<FakeCommand>[createKgpVersionCommand(kgpV)]);
|
|
java = FakeJava(version: Version(17, 0, 2));
|
|
androidStudio = FakeAndroidStudio();
|
|
androidSdk = FakeAndroidSdkWithDir(fileSystem.currentDirectory);
|
|
fileSystem.currentDirectory.childDirectory(androidStudio.javaPath!).createSync();
|
|
_testInMemory(
|
|
'incompatible agp/kgp 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.validJavaGradleAgpKgpString))),
|
|
);
|
|
// On gradle/agp error print help url and java and gradle versions.
|
|
expect(value.description, contains(RegExp(kgpV)));
|
|
expect(value.description, contains(RegExp(agpV)));
|
|
expect(value.description, contains(RegExp('AGP/KGP')));
|
|
expect(value.description, contains(RegExp(AndroidProject.kgpCompatUrl)));
|
|
},
|
|
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.any();
|
|
androidStudio = FakeAndroidStudio();
|
|
androidSdk = FakeAndroidSdkWithDir(fileSystem.currentDirectory);
|
|
fileSystem.currentDirectory.childDirectory(androidStudio.javaPath!).createSync();
|
|
_testInMemory(
|
|
'null agp only',
|
|
() async {
|
|
const 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.validJavaGradleAgpKgpString))),
|
|
);
|
|
// 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 entitlementFilePath = 'myEntitlement.Entitlement';
|
|
project.ios.hostAppRoot.childFile(entitlementFilePath).createSync(recursive: true);
|
|
|
|
const 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',
|
|
'applinks:example3.com?mode=developer',
|
|
]);
|
|
final String outputFilePath = await project.ios.outputsUniversalLinkSettings(
|
|
target: 'Runner',
|
|
configuration: 'config',
|
|
);
|
|
final File outputFile = fs.file(outputFilePath);
|
|
final json = jsonDecode(outputFile.readAsStringSync()) as Map<String, Object?>;
|
|
|
|
expect(
|
|
json['associatedDomains'],
|
|
unorderedEquals(<String>['example.com', 'example2.com', 'example3.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 entitlementFilePath = 'nested/somewhere/myEntitlement.Entitlement';
|
|
project.ios.hostAppRoot.childFile(entitlementFilePath).createSync(recursive: true);
|
|
|
|
const 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 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 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 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 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 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 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 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(
|
|
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(
|
|
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 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('manifest', () {
|
|
_testInMemory('can be replaced', () async {
|
|
final FlutterProject project = await someProject();
|
|
final String originalPubspecContents = project.pubspecFile.readAsStringSync();
|
|
final FlutterManifest updated = FlutterManifest.createFromString(
|
|
validPubspecWithDependencies,
|
|
logger: logger,
|
|
)!;
|
|
// Verifies the pubspec.yaml from [project] is overwritten with the pubspec
|
|
// defined by [updated], both in the [FlutterProject] and on disk.
|
|
expect(project.manifest, isNot(equals(updated)));
|
|
project.replacePubspec(updated);
|
|
expect(project.manifest, equals(updated));
|
|
final String updatedPubspecContents = project.pubspecFile.readAsStringSync();
|
|
expect(updatedPubspecContents, isNot(equals(originalPubspecContents)));
|
|
expect(updatedPubspecContents, validPubspecWithDependenciesAndNullValues);
|
|
});
|
|
});
|
|
|
|
group('Android project file getters', () {
|
|
_testInMemory(
|
|
'Project.android.gradleWrapperPropertiesFile resolves to gradle/wrapper/gradle-wrapper.properties',
|
|
() async {
|
|
final Directory tempDir = globals.fs.systemTempDirectory.createTempSync(
|
|
'flutter_project_test',
|
|
);
|
|
final Directory androidDir = tempDir.childDirectory('android')
|
|
..createSync(recursive: true);
|
|
|
|
// Create gradle/wrapper/gradle-wrapper.properties inside the fake android dir
|
|
final File expected =
|
|
androidDir
|
|
.childDirectory('gradle')
|
|
.childDirectory('wrapper')
|
|
.childFile('gradle-wrapper.properties')
|
|
..createSync(recursive: true);
|
|
|
|
final FlutterProject project = FlutterProject.fromDirectory(tempDir);
|
|
|
|
expect(project.android.gradleWrapperPropertiesFile.path, expected.path);
|
|
},
|
|
);
|
|
_testInMemory('Project.android.appGradleFile resolves to app/build.gradle', () async {
|
|
final Directory tempDir = globals.fs.systemTempDirectory.createTempSync(
|
|
'flutter_project_build_files',
|
|
);
|
|
final Directory androidDir = tempDir.childDirectory('android').childDirectory('app')
|
|
..createSync(recursive: true);
|
|
|
|
final File expected = androidDir.childFile('build.gradle')..createSync(recursive: true);
|
|
|
|
final FlutterProject project = FlutterProject.fromDirectory(tempDir);
|
|
|
|
expect(project.android.appGradleFile.path, expected.path);
|
|
});
|
|
_testInMemory('Project.android.appGradleFile resolves to app/build.gradle.kts', () async {
|
|
final Directory tempDir = globals.fs.systemTempDirectory.createTempSync(
|
|
'flutter_project_build_files',
|
|
);
|
|
final Directory androidDir = tempDir.childDirectory('android').childDirectory('app')
|
|
..createSync(recursive: true);
|
|
|
|
final File expected = androidDir.childFile('build.gradle.kts')..createSync(recursive: true);
|
|
|
|
final FlutterProject project = FlutterProject.fromDirectory(tempDir);
|
|
|
|
expect(project.android.appGradleFile.path, expected.path);
|
|
});
|
|
_testInMemory(
|
|
'Project.android.appGradleFile prefers app/build.gradle over app/build.gradle.kts',
|
|
() async {
|
|
final Directory tempDir = globals.fs.systemTempDirectory.createTempSync(
|
|
'flutter_project_build_files',
|
|
);
|
|
final Directory androidDir = tempDir.childDirectory('android').childDirectory('app')
|
|
..createSync(recursive: true);
|
|
androidDir.childFile('build.gradle.kts').createSync(recursive: true);
|
|
final File expected = androidDir.childFile('build.gradle')..createSync(recursive: true);
|
|
|
|
final FlutterProject project = FlutterProject.fromDirectory(tempDir);
|
|
|
|
expect(project.android.appGradleFile.path, expected.path);
|
|
},
|
|
);
|
|
|
|
_testInMemory(
|
|
'Project.android.hostAppGradleFile resolves to android/build.gradle ',
|
|
() async {
|
|
final Directory tempDir = globals.fs.systemTempDirectory.createTempSync(
|
|
'flutter_project_build_files',
|
|
);
|
|
final Directory androidDir = tempDir.childDirectory('android')
|
|
..createSync(recursive: true);
|
|
|
|
final File expected = androidDir.childFile('build.gradle')..createSync(recursive: true);
|
|
|
|
final FlutterProject project = FlutterProject.fromDirectory(tempDir);
|
|
|
|
expect(project.android.hostAppGradleFile.path, expected.path);
|
|
},
|
|
);
|
|
_testInMemory(
|
|
'Project.android.hostAppGradleFile resolves to android/build.gradle.kts',
|
|
() async {
|
|
final Directory tempDir = globals.fs.systemTempDirectory.createTempSync(
|
|
'flutter_project_build_files',
|
|
);
|
|
final Directory androidDir = tempDir.childDirectory('android')
|
|
..createSync(recursive: true);
|
|
|
|
final File expected = androidDir.childFile('build.gradle.kts')
|
|
..createSync(recursive: true);
|
|
|
|
final FlutterProject project = FlutterProject.fromDirectory(tempDir);
|
|
|
|
expect(project.android.hostAppGradleFile.path, expected.path);
|
|
},
|
|
);
|
|
_testInMemory(
|
|
'Project.android.hostAppGradleFile prefers android/build.gradle over android/build.gradle.kts',
|
|
() async {
|
|
final Directory tempDir = globals.fs.systemTempDirectory.createTempSync(
|
|
'flutter_project_build_files',
|
|
);
|
|
final Directory androidDir = tempDir.childDirectory('android')
|
|
..createSync(recursive: true);
|
|
androidDir.childFile('build.gradle.kts').createSync(recursive: true);
|
|
final File expected = androidDir.childFile('build.gradle')..createSync(recursive: true);
|
|
|
|
final FlutterProject project = FlutterProject.fromDirectory(tempDir);
|
|
|
|
expect(project.android.hostAppGradleFile.path, expected.path);
|
|
},
|
|
);
|
|
_testInMemory(
|
|
'Project.android.settingsGradleFile resolves to android/settings.gradle',
|
|
() async {
|
|
final Directory tempDir = globals.fs.systemTempDirectory.createTempSync(
|
|
'flutter_project_build_files',
|
|
);
|
|
final Directory androidDir = tempDir.childDirectory('android')
|
|
..createSync(recursive: true);
|
|
|
|
final File expected = androidDir.childFile('settings.gradle')
|
|
..createSync(recursive: true);
|
|
|
|
final FlutterProject project = FlutterProject.fromDirectory(tempDir);
|
|
|
|
expect(project.android.settingsGradleFile.path, expected.path);
|
|
},
|
|
);
|
|
_testInMemory(
|
|
'Project.android.settingsGradleFile resolves to android/settings.gradle.kts',
|
|
() async {
|
|
final Directory tempDir = globals.fs.systemTempDirectory.createTempSync(
|
|
'flutter_project_build_files',
|
|
);
|
|
final Directory androidDir = tempDir.childDirectory('android')
|
|
..createSync(recursive: true);
|
|
|
|
final File expected = androidDir.childFile('settings.gradle.kts')
|
|
..createSync(recursive: true);
|
|
|
|
final FlutterProject project = FlutterProject.fromDirectory(tempDir);
|
|
|
|
expect(project.android.settingsGradleFile.path, expected.path);
|
|
},
|
|
);
|
|
_testInMemory(
|
|
'Project.android.settingsGradleFile prefers android/settings.gradle over android/settings.gradle.kts',
|
|
() async {
|
|
final Directory tempDir = globals.fs.systemTempDirectory.createTempSync(
|
|
'flutter_project_build_files',
|
|
);
|
|
final Directory androidDir = tempDir.childDirectory('android')
|
|
..createSync(recursive: true);
|
|
androidDir.childFile('settings.gradle.kts').createSync(recursive: true);
|
|
final File expected = androidDir.childFile('settings.gradle')
|
|
..createSync(recursive: true);
|
|
|
|
final FlutterProject project = FlutterProject.fromDirectory(tempDir);
|
|
|
|
expect(project.android.settingsGradleFile.path, expected.path);
|
|
},
|
|
);
|
|
_testInMemory(
|
|
'Project.android.appManifestFile resolves to android/app/src/main/AndroidManifest.xml when build.gradle exists',
|
|
() async {
|
|
final Directory tempDir = globals.fs.systemTempDirectory.createTempSync(
|
|
'flutter_project_test',
|
|
);
|
|
final Directory androidDir = tempDir.childDirectory('android')
|
|
..createSync(recursive: true);
|
|
|
|
androidDir.childFile('build.gradle').createSync();
|
|
|
|
final File expected =
|
|
androidDir
|
|
.childDirectory('app')
|
|
.childDirectory('src')
|
|
.childDirectory('main')
|
|
.childFile('AndroidManifest.xml')
|
|
..createSync(recursive: true);
|
|
|
|
final FlutterProject project = FlutterProject.fromDirectory(tempDir);
|
|
|
|
expect(project.android.appManifestFile.path, expected.path);
|
|
},
|
|
);
|
|
_testInMemory(
|
|
'Project.android.appManifestFile resolves to android/app/src/main/AndroidManifest.xml when build.gradle.kts exists',
|
|
() async {
|
|
final Directory tempDir = globals.fs.systemTempDirectory.createTempSync(
|
|
'flutter_project_test',
|
|
);
|
|
final Directory androidDir = tempDir.childDirectory('android')
|
|
..createSync(recursive: true);
|
|
|
|
androidDir.childFile('build.gradle.kts').createSync();
|
|
|
|
final File expected =
|
|
androidDir
|
|
.childDirectory('app')
|
|
.childDirectory('src')
|
|
.childDirectory('main')
|
|
.childFile('AndroidManifest.xml')
|
|
..createSync(recursive: true);
|
|
|
|
final FlutterProject project = FlutterProject.fromDirectory(tempDir);
|
|
|
|
expect(project.android.appManifestFile.path, expected.path);
|
|
},
|
|
);
|
|
_testInMemory(
|
|
'Project.android.appManifestFile resolves to android/app/src/main/AndroidManifest.xml when both build.gradle and build.gradle.kts exists',
|
|
() async {
|
|
final Directory tempDir = globals.fs.systemTempDirectory.createTempSync(
|
|
'flutter_project_test',
|
|
);
|
|
final Directory androidDir = tempDir.childDirectory('android')
|
|
..createSync(recursive: true);
|
|
|
|
androidDir.childFile('build.gradle').createSync();
|
|
androidDir.childFile('build.gradle.kts').createSync();
|
|
|
|
final File expected =
|
|
androidDir
|
|
.childDirectory('app')
|
|
.childDirectory('src')
|
|
.childDirectory('main')
|
|
.childFile('AndroidManifest.xml')
|
|
..createSync(recursive: true);
|
|
|
|
final FlutterProject project = FlutterProject.fromDirectory(tempDir);
|
|
|
|
expect(project.android.appManifestFile.path, expected.path);
|
|
},
|
|
);
|
|
_testInMemory(
|
|
'Project.android.appManifestFile resolves to android/AndroidManifest.xml when not using Gradle',
|
|
() async {
|
|
final Directory tempDir = globals.fs.systemTempDirectory.createTempSync(
|
|
'flutter_project_test',
|
|
);
|
|
final Directory androidDir = tempDir.childDirectory('android')
|
|
..createSync(recursive: true);
|
|
|
|
final File expected = androidDir.childFile('AndroidManifest.xml')
|
|
..createSync(recursive: true);
|
|
|
|
final FlutterProject project = FlutterProject.fromDirectory(tempDir);
|
|
|
|
expect(project.android.appManifestFile.path, expected.path);
|
|
},
|
|
);
|
|
_testInMemory(
|
|
'Project.android.localPropertiesFile resolves to android/local.properties',
|
|
() async {
|
|
final Directory tempDir = globals.fs.systemTempDirectory.createTempSync(
|
|
'flutter_project_test',
|
|
);
|
|
final Directory androidDir = tempDir.childDirectory('android')
|
|
..createSync(recursive: true);
|
|
|
|
final File expected = androidDir.childFile('local.properties')
|
|
..createSync(recursive: true);
|
|
|
|
final FlutterProject project = FlutterProject.fromDirectory(tempDir);
|
|
|
|
expect(project.android.localPropertiesFile.path, expected.path);
|
|
},
|
|
);
|
|
});
|
|
|
|
group('workspaces', () {
|
|
_testInMemory('fails on invalid pubspec.yaml', () async {
|
|
final Directory directory = globals.fs.directory('myproject');
|
|
directory.childFile('pubspec.yaml')
|
|
..createSync(recursive: true)
|
|
..writeAsStringSync('''
|
|
name: parent
|
|
flutter:
|
|
workspace:
|
|
- child1
|
|
- child2
|
|
- child2/example
|
|
''');
|
|
directory.childDirectory('child1').childFile('pubspec.yaml')
|
|
..createSync(recursive: true)
|
|
..writeAsStringSync('''
|
|
name: child1
|
|
flutter:
|
|
resolution: workspace
|
|
''');
|
|
directory.childDirectory('child2').childFile('pubspec.yaml')
|
|
..createSync(recursive: true)
|
|
..writeAsStringSync('''
|
|
name: child2
|
|
flutter:
|
|
resolution: workspace
|
|
''');
|
|
directory.childDirectory('child2').childDirectory('example').childFile('pubspec.yaml')
|
|
..createSync(recursive: true)
|
|
..writeAsStringSync('''
|
|
name: child2_example
|
|
flutter:
|
|
resolution: workspace
|
|
''');
|
|
|
|
expect(
|
|
FlutterProject.fromDirectory(directory).workspaceProjects
|
|
.map((FlutterProject subproject) => subproject.manifest.appName)
|
|
.toList(),
|
|
<String>['child1', 'child2', 'child2_example'],
|
|
);
|
|
});
|
|
});
|
|
});
|
|
|
|
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);
|
|
const buildContext = XcodeProjectBuildContext(scheme: 'Runner');
|
|
mockXcodeProjectInterpreter.buildSettingsByBuildContext[buildContext] = <String, String>{
|
|
IosProject.kProductBundleIdKey: 'io.flutter.someProject',
|
|
};
|
|
});
|
|
|
|
testUsingContext(
|
|
'cannot find bundle identifier',
|
|
() async {
|
|
final FlutterProject project = await someProject();
|
|
final 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 and multiple schemes', () {
|
|
setUp(() {
|
|
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 buildContext = XcodeProjectBuildContext(
|
|
scheme: 'Runner',
|
|
deviceId: '123',
|
|
); // for substituteXcodeVariables call of plist parsing
|
|
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 watchBuildContext = XcodeProjectBuildContext(
|
|
scheme: 'WatchScheme',
|
|
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 watchBuildContext = XcodeProjectBuildContext(
|
|
scheme: 'WatchScheme',
|
|
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 = true,
|
|
}) async {
|
|
final Directory directory = globals.fs.directory('some_project');
|
|
writePackageConfigFiles(directory: globals.fs.currentDirectory, mainLibName: 'hello');
|
|
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> projectWithPluginDependency() async {
|
|
final Directory directory = globals.fs.directory('some_project');
|
|
writePackageConfigFiles(
|
|
directory: directory,
|
|
mainLibName: 'app_name',
|
|
packages: <String, String>{'my_plugin': '/plugin_project'},
|
|
devDependencies: <String>['my_plugin'],
|
|
);
|
|
directory.childFile('pubspec.yaml')
|
|
..createSync(recursive: true)
|
|
..writeAsStringSync('''
|
|
name: app_name
|
|
flutter:
|
|
|
|
dev_dependencies:
|
|
my_plugin:
|
|
sdk: flutter
|
|
''');
|
|
directory.childDirectory('ios').createSync(recursive: true);
|
|
final Directory androidDirectory = directory.childDirectory('android')
|
|
..createSync(recursive: true);
|
|
androidDirectory.childFile('AndroidManifest.xml').writeAsStringSync('<manifest></manifest>');
|
|
return FlutterProject.fromDirectory(directory);
|
|
}
|
|
|
|
Future<FlutterProject> aPluginProject({bool legacy = true, bool includeAndroidMain = false}) 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);
|
|
if (includeAndroidMain) {
|
|
directory
|
|
.childDirectory('android')
|
|
.childFile(globals.fs.path.join('src', 'main', 'java', 'com', 'example', 'MyPlugin.java'))
|
|
..createSync(recursive: true)
|
|
..writeAsStringSync('''
|
|
import io.flutter.embedding.engine.plugins.FlutterPlugin;
|
|
class MyPlugin extends FluttPlugin { /* ... */ }
|
|
''');
|
|
}
|
|
return FlutterProject.fromDirectory(directory);
|
|
}
|
|
|
|
Future<FlutterProject> aModuleProject() async {
|
|
final Directory directory = globals.fs.directory('module_project');
|
|
writePackageConfigFiles(mainLibName: 'my_module', directory: directory);
|
|
directory.childFile('pubspec.yaml').writeAsStringSync('''
|
|
name: my_module
|
|
flutter:
|
|
module:
|
|
androidPackage: com.example
|
|
''');
|
|
return FlutterProject.fromDirectory(directory);
|
|
}
|
|
|
|
FakeCommand createKgpVersionCommand(String kgpV) {
|
|
return FakeCommand(
|
|
command: const <String>['./gradlew', 'kgpVersion', '-q'],
|
|
stdout:
|
|
'''
|
|
KGP Version: $kgpV
|
|
''',
|
|
);
|
|
}
|
|
|
|
/// Returns a fake of the `cache/artifacts/gradle_wrapper` directory.
|
|
///
|
|
/// Otherwise this hermeric (general.shard) test needs to download files from
|
|
/// the internet to run.
|
|
///
|
|
/// See https://github.com/flutter/flutter/issues/83275 for details.
|
|
void _insertFakeGradleArtifactDir({required Directory flutterRoot}) {
|
|
final Directory artifactDir = flutterRoot
|
|
.childDirectory('bin')
|
|
.childDirectory('cache')
|
|
.childDirectory('artifacts')
|
|
.childDirectory('gradle_wrapper');
|
|
|
|
artifactDir
|
|
..childFile('gradlew').createSync(recursive: true)
|
|
..childFile('gradlew.bat').createSync(recursive: true)
|
|
..childDirectory('wrapper').childFile('gradle-wrapper.jar').createSync(recursive: true);
|
|
}
|
|
|
|
/// 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();
|
|
|
|
final Directory fakeFlutterRoot = testFileSystem.directory(Cache.flutterRoot);
|
|
_insertFakeGradleArtifactDir(flutterRoot: fakeFlutterRoot);
|
|
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 Directory dummyTemplateImagesDirectory = fakeFlutterRoot.parent;
|
|
dummyTemplateImagesDirectory.createSync(recursive: true);
|
|
writePackageConfigFiles(
|
|
directory: testFileSystem
|
|
.directory(Cache.flutterRoot)
|
|
.childDirectory('packages')
|
|
.childDirectory('flutter_tools'),
|
|
mainLibName: 'app_name',
|
|
packages: <String, String>{
|
|
'flutter_template_images': dummyTemplateImagesDirectory.uri.toString(),
|
|
},
|
|
);
|
|
|
|
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),
|
|
Pub: ThrowingPub.new,
|
|
},
|
|
);
|
|
}
|
|
|
|
/// 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:
|
|
''';
|
|
|
|
/// This is the equivalent to [validPubspecWithDependencies] after it's been
|
|
/// passed through [YamlEditor], which explicitly populates the null values
|
|
/// even if they were specified implicitly.
|
|
String get validPubspecWithDependenciesAndNullValues => '''
|
|
name: hello
|
|
flutter: null
|
|
dependencies:
|
|
plugin_a: null
|
|
plugin_b: null''';
|
|
|
|
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 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;
|
|
}
|