| // Copyright 2014 The Flutter Authors. All rights reserved. | |
| // Use of this source code is governed by a BSD-style license that can be | |
| // found in the LICENSE file. | |
| import 'dart:convert'; | |
| import 'dart:io'; | |
| import 'dart:typed_data'; | |
| import 'package:file/file.dart'; | |
| import 'package:file/memory.dart'; | |
| import 'package:file_testing/file_testing.dart'; | |
| import 'package:flutter_tools/src/base/error_handling_io.dart'; | |
| import 'package:flutter_tools/src/base/logger.dart'; | |
| import 'package:flutter_tools/src/base/os.dart'; | |
| import 'package:flutter_tools/src/base/platform.dart'; | |
| import 'package:flutter_tools/src/base/time.dart'; | |
| import 'package:flutter_tools/src/base/utils.dart'; | |
| import 'package:flutter_tools/src/dart/pub.dart'; | |
| import 'package:flutter_tools/src/darwin/darwin.dart'; | |
| import 'package:flutter_tools/src/features.dart'; | |
| import 'package:flutter_tools/src/flutter_manifest.dart'; | |
| import 'package:flutter_tools/src/flutter_plugins.dart'; | |
| import 'package:flutter_tools/src/globals.dart' as globals; | |
| import 'package:flutter_tools/src/ios/xcodeproj.dart'; | |
| import 'package:flutter_tools/src/macos/cocoapods.dart'; | |
| import 'package:flutter_tools/src/macos/darwin_dependency_management.dart'; | |
| import 'package:flutter_tools/src/platform_plugins.dart'; | |
| import 'package:flutter_tools/src/plugins.dart'; | |
| import 'package:flutter_tools/src/project.dart'; | |
| import 'package:flutter_tools/src/version.dart'; | |
| import 'package:package_config/package_config.dart'; | |
| import 'package:test/fake.dart'; | |
| import 'package:yaml/yaml.dart'; | |
| import '../src/common.dart'; | |
| import '../src/context.dart'; | |
| import '../src/fakes.dart' hide FakeOperatingSystemUtils; | |
| import '../src/package_config.dart'; | |
| import '../src/pubspec_schema.dart'; | |
| import '../src/throwing_pub.dart'; | |
| /// Information for a platform entry in the 'platforms' section of a plugin's | |
| /// pubspec.yaml. | |
| class _PluginPlatformInfo { | |
| const _PluginPlatformInfo({ | |
| this.pluginClass, | |
| this.dartPluginClass, | |
| this.androidPackage, | |
| this.sharedDarwinSource = false, | |
| this.fileName, | |
| }) : assert(pluginClass != null || dartPluginClass != null), | |
| assert(androidPackage == null || pluginClass != null); | |
| /// The pluginClass entry, if any. | |
| final String? pluginClass; | |
| /// The dartPluginClass entry, if any. | |
| final String? dartPluginClass; | |
| /// The package entry for an Android plugin implementation using pluginClass. | |
| final String? androidPackage; | |
| final bool sharedDarwinSource; | |
| /// The fileName entry for a web plugin implementation. | |
| final String? fileName; | |
| /// Returns the body of a platform section for a plugin's pubspec, properly | |
| /// indented. | |
| String get indentedPubspecSection { | |
| const indentation = ' '; | |
| return <String>[ | |
| if (pluginClass != null) '${indentation}pluginClass: $pluginClass', | |
| if (dartPluginClass != null) '${indentation}dartPluginClass: $dartPluginClass', | |
| if (androidPackage != null) '${indentation}package: $androidPackage', | |
| if (sharedDarwinSource) '${indentation}sharedDarwinSource: true', | |
| if (fileName != null) '${indentation}fileName: $fileName', | |
| ].join('\n'); | |
| } | |
| } | |
| void main() { | |
| group('plugins', () { | |
| late FileSystem fs; | |
| late FakeFlutterProject flutterProject; | |
| late FakeFlutterManifest flutterManifest; | |
| late FakeIosProject iosProject; | |
| late FakeMacOSProject macosProject; | |
| late FakeAndroidProject androidProject; | |
| late FakeWebProject webProject; | |
| late FakeWindowsProject windowsProject; | |
| late FakeLinuxProject linuxProject; | |
| late FakeSystemClock systemClock; | |
| late FlutterVersion flutterVersion; | |
| late FakeCocoaPodsCapturesInvalidate cocoaPods; | |
| // A Windows-style filesystem. This is not populated by default, so tests | |
| // using it instead of fs must re-run any necessary setup (e.g., | |
| // setUpProject). | |
| late FileSystem fsWindows; | |
| const pubCachePath = '/path/to/.pub-cache/hosted/pub.dartlang.org/foo-1.2.3'; | |
| const ephemeralPackagePath = '/path/to/app/linux/flutter/ephemeral/foo-1.2.3'; | |
| // Adds basic properties to the flutterProject and its subprojects. | |
| void setUpProject(FileSystem fileSystem) { | |
| flutterProject = FakeFlutterProject(); | |
| flutterManifest = FakeFlutterManifest(); | |
| flutterManifest.appName = 'my_app'; | |
| flutterProject | |
| ..manifest = flutterManifest | |
| ..directory = fileSystem.systemTempDirectory.childDirectory('app') | |
| ..flutterPluginsDependenciesFile = flutterProject.directory.childFile( | |
| '.flutter-plugins-dependencies', | |
| ); | |
| iosProject = FakeIosProject(); | |
| flutterProject.ios = iosProject; | |
| final Directory iosDirectory = flutterProject.directory.childDirectory('ios'); | |
| iosProject | |
| ..pluginRegistrantHost = flutterProject.directory.childDirectory('Runner') | |
| ..podfile = iosDirectory.childFile('Podfile') | |
| ..podManifestLock = iosDirectory.childFile('Podfile.lock') | |
| ..testExists = false; | |
| macosProject = FakeMacOSProject(); | |
| flutterProject.macos = macosProject; | |
| final Directory macosDirectory = flutterProject.directory.childDirectory('macos'); | |
| final Directory macosManagedDirectory = macosDirectory.childDirectory('Flutter'); | |
| macosProject | |
| ..podfile = macosDirectory.childFile('Podfile') | |
| ..podManifestLock = macosDirectory.childFile('Podfile.lock') | |
| ..managedDirectory = macosManagedDirectory | |
| ..exists = false; | |
| androidProject = FakeAndroidProject(); | |
| flutterProject.android = androidProject; | |
| final Directory androidDirectory = flutterProject.directory.childDirectory('android'); | |
| androidProject | |
| ..pluginRegistrantHost = androidDirectory.childDirectory('app') | |
| ..hostAppGradleRoot = androidDirectory | |
| ..exists = false | |
| ..embeddingVersion = AndroidEmbeddingVersion.v2; | |
| webProject = FakeWebProject(); | |
| flutterProject.web = webProject; | |
| webProject | |
| ..libDirectory = flutterProject.directory.childDirectory('lib') | |
| ..exists = false; | |
| windowsProject = FakeWindowsProject(); | |
| flutterProject.windows = windowsProject; | |
| final Directory windowsManagedDirectory = flutterProject.directory | |
| .childDirectory('windows') | |
| .childDirectory('flutter'); | |
| windowsProject | |
| ..managedDirectory = windowsManagedDirectory | |
| ..cmakeFile = windowsManagedDirectory.parent.childFile('CMakeLists.txt') | |
| ..generatedPluginCmakeFile = windowsManagedDirectory.childFile('generated_plugins.mk') | |
| ..pluginSymlinkDirectory = windowsManagedDirectory | |
| .childDirectory('ephemeral') | |
| .childDirectory('.plugin_symlinks') | |
| ..exists = false; | |
| linuxProject = FakeLinuxProject(); | |
| flutterProject.linux = linuxProject; | |
| final Directory linuxManagedDirectory = flutterProject.directory | |
| .childDirectory('linux') | |
| .childDirectory('flutter'); | |
| final Directory linuxEphemeralDirectory = linuxManagedDirectory.childDirectory('ephemeral'); | |
| linuxProject | |
| ..managedDirectory = linuxManagedDirectory | |
| ..ephemeralDirectory = linuxEphemeralDirectory | |
| ..pluginSymlinkDirectory = linuxEphemeralDirectory.childDirectory('.plugin_symlinks') | |
| ..cmakeFile = linuxManagedDirectory.parent.childFile('CMakeLists.txt') | |
| ..generatedPluginCmakeFile = linuxManagedDirectory.childFile('generated_plugins.mk') | |
| ..exists = false; | |
| } | |
| setUp(() async { | |
| fs = MemoryFileSystem.test(); | |
| fsWindows = MemoryFileSystem(style: FileSystemStyle.windows); | |
| systemClock = FakeSystemClock()..currentTime = DateTime(1970); | |
| flutterVersion = FakeFlutterVersion(frameworkVersion: '1.0.0'); | |
| cocoaPods = FakeCocoaPodsCapturesInvalidate(); | |
| // Add basic properties to the Flutter project and subprojects | |
| setUpProject(fs); | |
| writePackageConfigFiles(directory: flutterProject.directory, mainLibName: 'my_app'); | |
| }); | |
| void addToPackageConfig(String name, Directory packageDir, {bool isDevDependency = false}) { | |
| final File packageConfigFile = flutterProject.packageConfig; | |
| final packageConfig = | |
| jsonDecode(packageConfigFile.readAsStringSync()) as Map<String, Object?>; | |
| (packageConfig['packages']! as List<Object?>).add(<String, Object?>{ | |
| 'name': name, | |
| 'rootUri': packageDir.uri.toString(), | |
| 'packageUri': 'lib/', | |
| }); | |
| packageConfigFile.writeAsStringSync(jsonEncode(packageConfig)); | |
| final File packageGraphFile = flutterProject.packageConfig.parent.childFile( | |
| 'package_graph.json', | |
| ); | |
| final packageGraph = jsonDecode(packageGraphFile.readAsStringSync()) as Map<String, Object?>; | |
| final packages = packageGraph['packages']! as List<Object?>; | |
| packages.add(<String, Object?>{'name': name, 'dependencies': <Object?>[]}); | |
| final mainPackage = | |
| packages.firstWhere( | |
| (Object? p) => | |
| (p! as Map<String, Object?>)['name'] == flutterProject.manifest.appName, | |
| )! | |
| as Map<String, Object?>; | |
| final dependencyList = | |
| (mainPackage[isDevDependency ? 'devDependencies' : 'dependencies'] ??= <Object?>[]) | |
| as List<Object?>; | |
| dependencyList.add(name); | |
| packageGraphFile.writeAsStringSync(jsonEncode(packageGraph)); | |
| } | |
| // Makes fake plugin packages for each plugin, adds them to flutterProject, | |
| // and returns their directories. | |
| // | |
| // If an entry contains a path separator, it will be treated as a path for | |
| // the location of the package, with the name being the last component. | |
| // Otherwise it will be treated as a name, and put in a default location | |
| // (a fake pub cache). | |
| List<Directory> createFakePlugins(FileSystem fileSystem, List<String> pluginNamesOrPaths) { | |
| const pluginYamlTemplate = ''' | |
| flutter: | |
| plugin: | |
| platforms: | |
| ios: | |
| pluginClass: PLUGIN_CLASS | |
| macos: | |
| pluginClass: PLUGIN_CLASS | |
| windows: | |
| pluginClass: PLUGIN_CLASS | |
| linux: | |
| pluginClass: PLUGIN_CLASS | |
| web: | |
| pluginClass: PLUGIN_CLASS | |
| fileName: lib/PLUGIN_CLASS.dart | |
| android: | |
| pluginClass: PLUGIN_CLASS | |
| package: AndroidPackage | |
| '''; | |
| final directories = <Directory>[]; | |
| final Directory fakePubCache = fileSystem.systemTempDirectory.childDirectory('cache'); | |
| writePackageConfigFiles(directory: flutterProject.directory, mainLibName: 'my_app'); | |
| for (final nameOrPath in pluginNamesOrPaths) { | |
| final String name = fileSystem.path.basename(nameOrPath); | |
| final Directory pluginDirectory = (nameOrPath == name) | |
| ? fakePubCache.childDirectory(name) | |
| : fileSystem.directory(nameOrPath); | |
| addToPackageConfig(name, pluginDirectory); | |
| pluginDirectory.childFile('pubspec.yaml') | |
| ..createSync(recursive: true) | |
| ..writeAsStringSync( | |
| pluginYamlTemplate.replaceAll('PLUGIN_CLASS', sentenceCase(camelCase(name))), | |
| ); | |
| directories.add(pluginDirectory); | |
| } | |
| return directories; | |
| } | |
| // Makes a fake plugin package, adds it to flutterProject, and returns its directory. | |
| Directory createFakePlugin(FileSystem fileSystem) { | |
| return createFakePlugins(fileSystem, <String>['some_plugin'])[0]; | |
| } | |
| void createNewJavaPlugin1() { | |
| final Directory pluginUsingJavaAndNewEmbeddingDir = fs.systemTempDirectory.createTempSync( | |
| 'flutter_plugin_using_java_and_new_embedding_dir.', | |
| ); | |
| pluginUsingJavaAndNewEmbeddingDir.childFile('pubspec.yaml').writeAsStringSync(''' | |
| flutter: | |
| plugin: | |
| androidPackage: plugin1 | |
| pluginClass: UseNewEmbedding | |
| '''); | |
| pluginUsingJavaAndNewEmbeddingDir | |
| .childDirectory('android') | |
| .childDirectory('src') | |
| .childDirectory('main') | |
| .childDirectory('java') | |
| .childDirectory('plugin1') | |
| .childFile('UseNewEmbedding.java') | |
| ..createSync(recursive: true) | |
| ..writeAsStringSync('import io.flutter.embedding.engine.plugins.FlutterPlugin;'); | |
| addToPackageConfig('plugin1', pluginUsingJavaAndNewEmbeddingDir); | |
| } | |
| Directory createPluginWithInvalidAndroidPackage() { | |
| final Directory pluginUsingJavaAndNewEmbeddingDir = fs.systemTempDirectory.createTempSync( | |
| 'flutter_plugin_invalid_package.', | |
| ); | |
| pluginUsingJavaAndNewEmbeddingDir.childFile('pubspec.yaml').writeAsStringSync(''' | |
| flutter: | |
| plugin: | |
| androidPackage: plugin1.invalid | |
| pluginClass: UseNewEmbedding | |
| '''); | |
| pluginUsingJavaAndNewEmbeddingDir | |
| .childDirectory('android') | |
| .childDirectory('src') | |
| .childDirectory('main') | |
| .childDirectory('java') | |
| .childDirectory('plugin1') | |
| .childDirectory('correct') | |
| .childFile('UseNewEmbedding.java') | |
| ..createSync(recursive: true) | |
| ..writeAsStringSync('import io.flutter.embedding.engine.plugins.FlutterPlugin;'); | |
| addToPackageConfig('plugin1', pluginUsingJavaAndNewEmbeddingDir); | |
| return pluginUsingJavaAndNewEmbeddingDir; | |
| } | |
| void createDualSupportJavaPlugin4() { | |
| final Directory pluginUsingJavaAndNewEmbeddingDir = fs.systemTempDirectory.createTempSync( | |
| 'flutter_plugin_using_java_and_new_embedding_dir.', | |
| ); | |
| pluginUsingJavaAndNewEmbeddingDir.childFile('pubspec.yaml').writeAsStringSync(''' | |
| flutter: | |
| plugin: | |
| androidPackage: plugin4 | |
| pluginClass: UseBothEmbedding | |
| '''); | |
| pluginUsingJavaAndNewEmbeddingDir | |
| .childDirectory('android') | |
| .childDirectory('src') | |
| .childDirectory('main') | |
| .childDirectory('java') | |
| .childDirectory('plugin4') | |
| .childFile('UseBothEmbedding.java') | |
| ..createSync(recursive: true) | |
| ..writeAsStringSync( | |
| 'import io.flutter.embedding.engine.plugins.FlutterPlugin;\n' | |
| 'PluginRegistry\n' | |
| 'registerWith(Irrelevant registrar)\n', | |
| ); | |
| addToPackageConfig('plugin4', pluginUsingJavaAndNewEmbeddingDir); | |
| } | |
| Directory createLegacyPluginWithDependencies({ | |
| required String name, | |
| required List<String> dependencies, | |
| }) { | |
| final Directory pluginDirectory = fs.systemTempDirectory.createTempSync('flutter_plugin.'); | |
| pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' | |
| name: $name | |
| flutter: | |
| plugin: | |
| androidPackage: plugin2 | |
| pluginClass: UseNewEmbedding | |
| dependencies: | |
| '''); | |
| for (final dependency in dependencies) { | |
| pluginDirectory | |
| .childFile('pubspec.yaml') | |
| .writeAsStringSync(' $dependency:\n', mode: FileMode.append); | |
| } | |
| addToPackageConfig(name, pluginDirectory); | |
| return pluginDirectory; | |
| } | |
| Directory createPlugin({ | |
| required String name, | |
| required Map<String, _PluginPlatformInfo> platforms, | |
| List<String> dependencies = const <String>[], | |
| bool isDevDependency = false, | |
| }) { | |
| final Iterable<String> platformSections = platforms.entries.map( | |
| (MapEntry<String, _PluginPlatformInfo> entry) => | |
| ''' | |
| ${entry.key}: | |
| ${entry.value.indentedPubspecSection} | |
| ''', | |
| ); | |
| final Directory pluginDirectory = fs.systemTempDirectory.createTempSync('flutter_plugin.'); | |
| pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' | |
| name: $name | |
| flutter: | |
| plugin: | |
| platforms: | |
| ${platformSections.join('\n')} | |
| dependencies: | |
| '''); | |
| for (final dependency in dependencies) { | |
| pluginDirectory | |
| .childFile('pubspec.yaml') | |
| .writeAsStringSync(' $dependency:\n', mode: FileMode.append); | |
| } | |
| addToPackageConfig(name, pluginDirectory, isDevDependency: isDevDependency); | |
| return pluginDirectory; | |
| } | |
| // Creates the files that would indicate that pod install has run for the | |
| // given project. | |
| void simulatePodInstallRun(XcodeBasedProject project) { | |
| project.podManifestLock.createSync(recursive: true); | |
| } | |
| group('refreshPlugins', () { | |
| testUsingContext( | |
| 'Refreshing the plugin list is a no-op when the plugins list stays empty', | |
| () async { | |
| await refreshPluginsList(flutterProject); | |
| expect(flutterProject.flutterPluginsDependenciesFile, isNot(exists)); | |
| }, | |
| overrides: <Type, Generator>{ | |
| FileSystem: () => fs, | |
| ProcessManager: () => FakeProcessManager.any(), | |
| Pub: ThrowingPub.new, | |
| }, | |
| ); | |
| testUsingContext( | |
| 'Refreshing the plugin list deletes the plugin file when there were plugins but no longer are', | |
| () async { | |
| flutterProject.flutterPluginsDependenciesFile.createSync(); | |
| await refreshPluginsList(flutterProject); | |
| expect(flutterProject.flutterPluginsDependenciesFile, isNot(exists)); | |
| }, | |
| overrides: <Type, Generator>{ | |
| FileSystem: () => fs, | |
| ProcessManager: () => FakeProcessManager.any(), | |
| Pub: ThrowingPub.new, | |
| }, | |
| ); | |
| testUsingContext( | |
| 'Refreshing the plugin list creates a sorted plugin directory when there are plugins', | |
| () async { | |
| createFakePlugins(fs, <String>[ | |
| 'plugin_d', | |
| 'plugin_a', | |
| '/local_plugins/plugin_c', | |
| '/local_plugins/plugin_b', | |
| ]); | |
| iosProject.testExists = true; | |
| await refreshPluginsList(flutterProject); | |
| expect(flutterProject.flutterPluginsDependenciesFile, exists); | |
| final String pluginsFileContents = flutterProject.flutterPluginsDependenciesFile | |
| .readAsStringSync(); | |
| expect( | |
| pluginsFileContents.indexOf('plugin_a'), | |
| lessThan(pluginsFileContents.indexOf('plugin_b')), | |
| ); | |
| expect( | |
| pluginsFileContents.indexOf('plugin_b'), | |
| lessThan(pluginsFileContents.indexOf('plugin_c')), | |
| ); | |
| expect( | |
| pluginsFileContents.indexOf('plugin_c'), | |
| lessThan(pluginsFileContents.indexOf('plugin_d')), | |
| ); | |
| }, | |
| overrides: <Type, Generator>{ | |
| FileSystem: () => fs, | |
| ProcessManager: () => FakeProcessManager.any(), | |
| Pub: ThrowingPub.new, | |
| }, | |
| ); | |
| testUsingContext( | |
| 'does not crash when a plugin pubspec.yaml is not valid UTF-8', | |
| () async { | |
| // Regression test for https://github.com/flutter/flutter/issues/188970. | |
| final List<Directory> pluginDirs = createFakePlugins(fs, <String>[ | |
| 'good_plugin', | |
| 'bad_plugin', | |
| ]); | |
| // Write bytes that are not valid UTF-8, so readAsString throws a FileSystemException. | |
| pluginDirs[1] | |
| .childFile('pubspec.yaml') | |
| .writeAsBytesSync(Uint8List.fromList(<int>[0xff, 0xfe, 0xfd])); | |
| // The tool must not crash when a plugin's pubspec.yaml cannot be read. | |
| final Future<List<Plugin>> pluginsFuture = findPlugins(flutterProject); | |
| await expectLater(pluginsFuture, completes); | |
| // The unreadable plugin is skipped, but the readable one is still found. | |
| final List<Plugin> plugins = await pluginsFuture; | |
| final pluginNames = <String>[for (final Plugin plugin in plugins) plugin.name]; | |
| expect(pluginNames, contains('good_plugin')); | |
| expect(pluginNames, isNot(contains('bad_plugin'))); | |
| }, | |
| overrides: <Type, Generator>{ | |
| FileSystem: () => fs, | |
| ProcessManager: () => FakeProcessManager.any(), | |
| Pub: ThrowingPub.new, | |
| }, | |
| ); | |
| testUsingContext( | |
| 'Refreshing the plugin list updates .flutter-plugins-dependencies if the plugins changed', | |
| () async { | |
| // Refresh the plugin list (we have no plugins). | |
| await refreshPluginsList(flutterProject); | |
| expect(flutterProject.flutterPluginsDependenciesFile, isNot(exists)); | |
| // Create an initial plugin (we previously had none). | |
| createLegacyPluginWithDependencies(name: 'plugin-a', dependencies: <String>[]); | |
| await refreshPluginsList(flutterProject, iosPlatform: true, macOSPlatform: true); | |
| expect(flutterProject.flutterPluginsDependenciesFile, exists); | |
| final FileStat stat1 = flutterProject.flutterPluginsDependenciesFile.statSync(); | |
| // Add a new plugin. | |
| createLegacyPluginWithDependencies(name: 'plugin-b', dependencies: <String>[]); | |
| await refreshPluginsList(flutterProject, iosPlatform: true, macOSPlatform: true); | |
| expect(flutterProject.flutterPluginsDependenciesFile, exists); | |
| final FileStat stat2 = flutterProject.flutterPluginsDependenciesFile.statSync(); | |
| expect( | |
| stat2.modified.isAfter(stat1.modified), | |
| isTrue, | |
| reason: 'A new plugin was added, .flutter-plugins-dependencies file should be updated.', | |
| ); | |
| // Do not add new plugins. | |
| await refreshPluginsList(flutterProject, iosPlatform: true, macOSPlatform: true); | |
| expect(flutterProject.flutterPluginsDependenciesFile, exists); | |
| final FileStat stat3 = flutterProject.flutterPluginsDependenciesFile.statSync(); | |
| expect( | |
| stat3.modified, | |
| stat2.modified, | |
| reason: 'No plugins changed, .flutter-plugins-dependencies should not be changed', | |
| ); | |
| }, | |
| overrides: <Type, Generator>{ | |
| FileSystem: () => fs, | |
| ProcessManager: () => FakeProcessManager.any(), | |
| SystemClock: () => systemClock, | |
| FlutterVersion: () => flutterVersion, | |
| Pub: ThrowingPub.new, | |
| // TODO(matanlurey): Remove as part of https://github.com/flutter/flutter/issues/160257. | |
| // Not necessary, you can observe this bug by calling `generateLegacyPlugins: false`, | |
| // but since this flag is about to be enabled, and enabling it implicitly sets that | |
| // argument to false, this is a more "honest" test. | |
| }, | |
| ); | |
| testUsingContext( | |
| 'Refreshing the plugin list for iOS/macOS projects invokes invalidatePodInstallOutput if the plugins changed', | |
| () async { | |
| // Refresh the plugin list (we have no plugins). | |
| await refreshPluginsList(flutterProject, iosPlatform: true, macOSPlatform: true); | |
| expect( | |
| cocoaPods.capturedInvocations, | |
| isEmpty, | |
| reason: 'No plugins exist, so no invalidatePodInstallOutput calls expected.', | |
| ); | |
| // Create an initial plugin (we previously had none). | |
| createPlugin( | |
| name: 'plugin-a', | |
| platforms: const <String, _PluginPlatformInfo>{ | |
| 'ios': _PluginPlatformInfo( | |
| pluginClass: 'Foo', | |
| dartPluginClass: 'Bar', | |
| sharedDarwinSource: true, | |
| ), | |
| }, | |
| ); | |
| await refreshPluginsList(flutterProject, iosPlatform: true, macOSPlatform: true); | |
| expect( | |
| cocoaPods.capturedInvocations, | |
| containsAll(<Matcher>[isA<IosProject>(), isA<MacOSProject>()]), | |
| reason: 'A new plugin was added, so it should cause invalidatePodInstallOutput calls.', | |
| ); | |
| cocoaPods.capturedInvocations.clear(); | |
| // Add a new plugin. | |
| createPlugin( | |
| name: 'plugin-b', | |
| platforms: const <String, _PluginPlatformInfo>{ | |
| 'ios': _PluginPlatformInfo( | |
| pluginClass: 'Foo', | |
| dartPluginClass: 'Bar', | |
| sharedDarwinSource: true, | |
| ), | |
| }, | |
| ); | |
| await refreshPluginsList(flutterProject, iosPlatform: true, macOSPlatform: true); | |
| expect( | |
| cocoaPods.capturedInvocations, | |
| containsAll(<Matcher>[isA<IosProject>(), isA<MacOSProject>()]), | |
| reason: 'A new plugin was added, so it should cause invalidatePodInstallOutput calls.', | |
| ); | |
| cocoaPods.capturedInvocations.clear(); | |
| // Do not add new plugins. | |
| await refreshPluginsList(flutterProject, iosPlatform: true, macOSPlatform: true); | |
| expect( | |
| cocoaPods.capturedInvocations, | |
| isEmpty, | |
| reason: 'No plugins changed, so no updates expected', | |
| ); | |
| }, | |
| overrides: <Type, Generator>{ | |
| CocoaPods: () => cocoaPods, | |
| FileSystem: () => fs, | |
| ProcessManager: () => FakeProcessManager.any(), | |
| SystemClock: () => systemClock, | |
| FlutterVersion: () => flutterVersion, | |
| Pub: ThrowingPub.new, | |
| // TODO(matanlurey): Remove as part of https://github.com/flutter/flutter/issues/160257. | |
| // Not necessary, you can observe this bug by calling `generateLegacyPlugins: false`, | |
| // but since this flag is about to be enabled, and enabling it implicitly sets that | |
| // argument to false, this is a more "honest" test. | |
| }, | |
| ); | |
| testUsingContext( | |
| '.flutter-plugins-dependencies contains plugin platform info', | |
| () async { | |
| createPlugin( | |
| name: 'plugin-a', | |
| platforms: const <String, _PluginPlatformInfo>{ | |
| // Native-only; should include native build. | |
| 'android': _PluginPlatformInfo(pluginClass: 'Foo', androidPackage: 'bar.foo'), | |
| // Hybrid native and Dart; should include native build. | |
| 'ios': _PluginPlatformInfo( | |
| pluginClass: 'Foo', | |
| dartPluginClass: 'Bar', | |
| sharedDarwinSource: true, | |
| ), | |
| // Web; should not have the native build key at all since it doesn't apply. | |
| 'web': _PluginPlatformInfo(pluginClass: 'Foo', fileName: 'lib/foo.dart'), | |
| // Dart-only; should not include native build. | |
| 'windows': _PluginPlatformInfo(dartPluginClass: 'Foo'), | |
| }, | |
| ); | |
| iosProject.testExists = true; | |
| final dateCreated = DateTime(1970); | |
| systemClock.currentTime = dateCreated; | |
| await refreshPluginsList(flutterProject); | |
| expect(flutterProject.flutterPluginsDependenciesFile, exists); | |
| final String pluginsString = flutterProject.flutterPluginsDependenciesFile | |
| .readAsStringSync(); | |
| final jsonContent = json.decode(pluginsString) as Map<String, dynamic>; | |
| final actualPlugins = jsonContent['plugins'] as Map<String, dynamic>?; | |
| final expectedPlugins = <String, Object>{ | |
| 'ios': <Map<String, Object>>[ | |
| <String, Object>{ | |
| 'name': 'plugin-a', | |
| 'path': '/.tmp_rand0/flutter_plugin.rand0/', | |
| 'shared_darwin_source': true, | |
| 'native_build': true, | |
| 'dependencies': <String>[], | |
| 'dev_dependency': false, | |
| }, | |
| ], | |
| 'android': <Map<String, Object>>[ | |
| <String, Object>{ | |
| 'name': 'plugin-a', | |
| 'path': '/.tmp_rand0/flutter_plugin.rand0/', | |
| 'native_build': true, | |
| 'dependencies': <String>[], | |
| 'dev_dependency': false, | |
| }, | |
| ], | |
| 'macos': <Map<String, Object>>[], | |
| 'linux': <Map<String, Object>>[], | |
| 'windows': <Map<String, Object>>[ | |
| <String, Object>{ | |
| 'name': 'plugin-a', | |
| 'path': '/.tmp_rand0/flutter_plugin.rand0/', | |
| 'native_build': false, | |
| 'dependencies': <String>[], | |
| 'dev_dependency': false, | |
| }, | |
| ], | |
| 'web': <Map<String, Object>>[ | |
| <String, Object>{ | |
| 'name': 'plugin-a', | |
| 'path': '/.tmp_rand0/flutter_plugin.rand0/', | |
| 'dependencies': <String>[], | |
| 'dev_dependency': false, | |
| }, | |
| ], | |
| }; | |
| expect(actualPlugins, expectedPlugins); | |
| }, | |
| overrides: <Type, Generator>{ | |
| FileSystem: () => fs, | |
| ProcessManager: () => FakeProcessManager.any(), | |
| SystemClock: () => systemClock, | |
| FlutterVersion: () => flutterVersion, | |
| Pub: ThrowingPub.new, | |
| }, | |
| ); | |
| testUsingContext( | |
| '.flutter-plugins-dependencies contains swift_package_manager_enabled true when project is using Swift Package Manager', | |
| () async { | |
| createPlugin( | |
| name: 'plugin-a', | |
| platforms: const <String, _PluginPlatformInfo>{ | |
| // Native-only; should include native build. | |
| 'android': _PluginPlatformInfo(pluginClass: 'Foo', androidPackage: 'bar.foo'), | |
| // Hybrid native and Dart; should include native build. | |
| 'ios': _PluginPlatformInfo( | |
| pluginClass: 'Foo', | |
| dartPluginClass: 'Bar', | |
| sharedDarwinSource: true, | |
| ), | |
| // Web; should not have the native build key at all since it doesn't apply. | |
| 'web': _PluginPlatformInfo(pluginClass: 'Foo', fileName: 'lib/foo.dart'), | |
| // Dart-only; should not include native build. | |
| 'windows': _PluginPlatformInfo(dartPluginClass: 'Foo'), | |
| }, | |
| ); | |
| iosProject.testExists = true; | |
| final dateCreated = DateTime(1970); | |
| systemClock.currentTime = dateCreated; | |
| iosProject.usesSwiftPackageManager = true; | |
| macosProject.usesSwiftPackageManager = true; | |
| await refreshPluginsList(flutterProject, iosPlatform: true, macOSPlatform: true); | |
| expect(flutterProject.flutterPluginsDependenciesFile, exists); | |
| final String pluginsString = flutterProject.flutterPluginsDependenciesFile | |
| .readAsStringSync(); | |
| final jsonContent = json.decode(pluginsString) as Map<String, dynamic>; | |
| final expectedSwiftPackageManagerEnabled = <String, dynamic>{'ios': true, 'macos': true}; | |
| expect(jsonContent['swift_package_manager_enabled'], expectedSwiftPackageManagerEnabled); | |
| }, | |
| overrides: <Type, Generator>{ | |
| FileSystem: () => fs, | |
| ProcessManager: () => FakeProcessManager.any(), | |
| SystemClock: () => systemClock, | |
| FlutterVersion: () => flutterVersion, | |
| Pub: ThrowingPub.new, | |
| }, | |
| ); | |
| testUsingContext( | |
| '.flutter-plugins-dependencies contains swift_package_manager_enabled false when project is using Swift Package Manager but forceCocoaPodsOnly is true', | |
| () async { | |
| createPlugin( | |
| name: 'plugin-a', | |
| platforms: const <String, _PluginPlatformInfo>{ | |
| // Native-only; should include native build. | |
| 'android': _PluginPlatformInfo(pluginClass: 'Foo', androidPackage: 'bar.foo'), | |
| // Hybrid native and Dart; should include native build. | |
| 'ios': _PluginPlatformInfo( | |
| pluginClass: 'Foo', | |
| dartPluginClass: 'Bar', | |
| sharedDarwinSource: true, | |
| ), | |
| // Web; should not have the native build key at all since it doesn't apply. | |
| 'web': _PluginPlatformInfo(pluginClass: 'Foo', fileName: 'lib/foo.dart'), | |
| // Dart-only; should not include native build. | |
| 'windows': _PluginPlatformInfo(dartPluginClass: 'Foo'), | |
| }, | |
| ); | |
| iosProject.testExists = true; | |
| final dateCreated = DateTime(1970); | |
| systemClock.currentTime = dateCreated; | |
| iosProject.usesSwiftPackageManager = true; | |
| macosProject.usesSwiftPackageManager = true; | |
| await refreshPluginsList(flutterProject, forceCocoaPodsOnly: true); | |
| expect(flutterProject.flutterPluginsDependenciesFile, exists); | |
| final String pluginsString = flutterProject.flutterPluginsDependenciesFile | |
| .readAsStringSync(); | |
| final jsonContent = json.decode(pluginsString) as Map<String, dynamic>; | |
| final expectedSwiftPackageManagerEnabled = <String, dynamic>{ | |
| 'ios': false, | |
| 'macos': false, | |
| }; | |
| expect(jsonContent['swift_package_manager_enabled'], expectedSwiftPackageManagerEnabled); | |
| }, | |
| overrides: <Type, Generator>{ | |
| FileSystem: () => fs, | |
| ProcessManager: () => FakeProcessManager.any(), | |
| SystemClock: () => systemClock, | |
| FlutterVersion: () => flutterVersion, | |
| Pub: ThrowingPub.new, | |
| }, | |
| ); | |
| testUsingContext( | |
| '.flutter-plugins-dependencies forces swift_package_manager_enabled if forceSwiftPM is true', | |
| () async { | |
| createPlugin( | |
| name: 'plugin-a', | |
| platforms: const <String, _PluginPlatformInfo>{ | |
| 'ios': _PluginPlatformInfo( | |
| pluginClass: 'Foo', | |
| dartPluginClass: 'Bar', | |
| sharedDarwinSource: true, | |
| ), | |
| }, | |
| ); | |
| iosProject.testExists = true; | |
| final dateCreated = DateTime(1970); | |
| systemClock.currentTime = dateCreated; | |
| iosProject.usesSwiftPackageManager = false; | |
| macosProject.usesSwiftPackageManager = false; | |
| await refreshPluginsList(flutterProject, forceSwiftPM: true); | |
| expect(flutterProject.flutterPluginsDependenciesFile, exists); | |
| final String pluginsString = flutterProject.flutterPluginsDependenciesFile | |
| .readAsStringSync(); | |
| final jsonContent = json.decode(pluginsString) as Map<String, dynamic>; | |
| final expectedSwiftPackageManagerEnabled = <String, dynamic>{'ios': true, 'macos': true}; | |
| expect(jsonContent['swift_package_manager_enabled'], expectedSwiftPackageManagerEnabled); | |
| }, | |
| overrides: <Type, Generator>{ | |
| FileSystem: () => fs, | |
| ProcessManager: () => FakeProcessManager.any(), | |
| SystemClock: () => systemClock, | |
| FlutterVersion: () => flutterVersion, | |
| Pub: ThrowingPub.new, | |
| }, | |
| ); | |
| testUsingContext( | |
| '.flutter-plugins-dependencies can have different swift_package_manager_enabled values for iOS and macoS', | |
| () async { | |
| createPlugin( | |
| name: 'plugin-a', | |
| platforms: const <String, _PluginPlatformInfo>{ | |
| // Native-only; should include native build. | |
| 'android': _PluginPlatformInfo(pluginClass: 'Foo', androidPackage: 'bar.foo'), | |
| // Hybrid native and Dart; should include native build. | |
| 'ios': _PluginPlatformInfo( | |
| pluginClass: 'Foo', | |
| dartPluginClass: 'Bar', | |
| sharedDarwinSource: true, | |
| ), | |
| // Web; should not have the native build key at all since it doesn't apply. | |
| 'web': _PluginPlatformInfo(pluginClass: 'Foo', fileName: 'lib/foo.dart'), | |
| // Dart-only; should not include native build. | |
| 'windows': _PluginPlatformInfo(dartPluginClass: 'Foo'), | |
| }, | |
| ); | |
| iosProject.testExists = true; | |
| final dateCreated = DateTime(1970); | |
| systemClock.currentTime = dateCreated; | |
| iosProject.usesSwiftPackageManager = true; | |
| macosProject.usesSwiftPackageManager = false; | |
| await refreshPluginsList(flutterProject, iosPlatform: true, macOSPlatform: true); | |
| expect(flutterProject.flutterPluginsDependenciesFile, exists); | |
| final String pluginsString = flutterProject.flutterPluginsDependenciesFile | |
| .readAsStringSync(); | |
| final jsonContent = json.decode(pluginsString) as Map<String, dynamic>; | |
| final expectedSwiftPackageManagerEnabled = <String, dynamic>{'ios': true, 'macos': false}; | |
| expect(jsonContent['swift_package_manager_enabled'], expectedSwiftPackageManagerEnabled); | |
| }, | |
| overrides: <Type, Generator>{ | |
| FileSystem: () => fs, | |
| ProcessManager: () => FakeProcessManager.any(), | |
| SystemClock: () => systemClock, | |
| FlutterVersion: () => flutterVersion, | |
| Pub: ThrowingPub.new, | |
| }, | |
| ); | |
| testUsingContext( | |
| 'Changes to the plugin list invalidates the Cocoapod lockfiles', | |
| () async { | |
| simulatePodInstallRun(iosProject); | |
| simulatePodInstallRun(macosProject); | |
| createFakePlugin(fs); | |
| iosProject.testExists = true; | |
| macosProject.exists = true; | |
| await refreshPluginsList(flutterProject, iosPlatform: true, macOSPlatform: true); | |
| expect(iosProject.podManifestLock, isNot(exists)); | |
| expect(macosProject.podManifestLock, isNot(exists)); | |
| }, | |
| overrides: <Type, Generator>{ | |
| FileSystem: () => fs, | |
| ProcessManager: () => FakeProcessManager.any(), | |
| SystemClock: () => systemClock, | |
| FlutterVersion: () => flutterVersion, | |
| Pub: ThrowingPub.new, | |
| }, | |
| ); | |
| testUsingContext( | |
| 'No changes to the plugin list does not invalidate the Cocoapod lockfiles', | |
| () async { | |
| createFakePlugin(fs); | |
| iosProject.testExists = true; | |
| macosProject.exists = true; | |
| // First call will create the .flutter-plugins-dependencies and the legacy .flutter-plugins file. | |
| // Since there was no plugins list, the lock files will be invalidated. | |
| // The second call is where the plugins list is compared to the existing one, and if there is no change, | |
| // the podfiles shouldn't be invalidated. | |
| await refreshPluginsList(flutterProject, iosPlatform: true, macOSPlatform: true); | |
| simulatePodInstallRun(iosProject); | |
| simulatePodInstallRun(macosProject); | |
| await refreshPluginsList(flutterProject); | |
| expect(iosProject.podManifestLock, exists); | |
| expect(macosProject.podManifestLock, exists); | |
| }, | |
| overrides: <Type, Generator>{ | |
| FileSystem: () => fs, | |
| ProcessManager: () => FakeProcessManager.any(), | |
| SystemClock: () => systemClock, | |
| FlutterVersion: () => flutterVersion, | |
| Pub: ThrowingPub.new, | |
| }, | |
| ); | |
| }); | |
| group('injectPlugins', () { | |
| FakeXcodeProjectInterpreter? xcodeProjectInterpreter; | |
| setUp(() { | |
| xcodeProjectInterpreter = FakeXcodeProjectInterpreter(); | |
| }); | |
| testUsingContext( | |
| 'Registrant uses new embedding if app uses new embedding', | |
| () async { | |
| androidProject.embeddingVersion = AndroidEmbeddingVersion.v2; | |
| await injectPlugins(flutterProject, androidPlatform: true, releaseMode: false); | |
| final File registrant = flutterProject.directory | |
| .childDirectory( | |
| fs.path.join('android', 'app', 'src', 'main', 'java', 'io', 'flutter', 'plugins'), | |
| ) | |
| .childFile('GeneratedPluginRegistrant.java'); | |
| expect(registrant, exists); | |
| expect(registrant.readAsStringSync(), contains('package io.flutter.plugins')); | |
| expect(registrant.readAsStringSync(), contains('class GeneratedPluginRegistrant')); | |
| expect( | |
| registrant.readAsStringSync(), | |
| contains('public static void registerWith(@NonNull FlutterEngine flutterEngine)'), | |
| ); | |
| }, | |
| overrides: <Type, Generator>{ | |
| FileSystem: () => fs, | |
| ProcessManager: () => FakeProcessManager.any(), | |
| Pub: ThrowingPub.new, | |
| }, | |
| ); | |
| // Issue: https://github.com/flutter/flutter/issues/47803 | |
| testUsingContext( | |
| 'exits the tool if a plugin sets an invalid android package in pubspec.yaml', | |
| () async { | |
| androidProject.embeddingVersion = AndroidEmbeddingVersion.v1; | |
| final Directory pluginDir = createPluginWithInvalidAndroidPackage(); | |
| await expectLater( | |
| () async { | |
| await injectPlugins(flutterProject, androidPlatform: true, releaseMode: false); | |
| }, | |
| throwsToolExit( | |
| message: | |
| "The plugin `plugin1` doesn't have a main class defined in " | |
| '${pluginDir.path}/android/src/main/java/plugin1/invalid/UseNewEmbedding.java or ' | |
| '${pluginDir.path}/android/src/main/kotlin/plugin1/invalid/UseNewEmbedding.kt. ' | |
| "This is likely to due to an incorrect `androidPackage: plugin1.invalid` or `mainClass` entry in the plugin's pubspec.yaml.\n" | |
| 'If you are the author of this plugin, fix the `androidPackage` entry or move the main class to any of locations used above. ' | |
| 'Otherwise, please contact the author of this plugin and consider using a different plugin in the meanwhile.', | |
| ), | |
| ); | |
| }, | |
| overrides: <Type, Generator>{ | |
| FileSystem: () => fs, | |
| ProcessManager: () => FakeProcessManager.any(), | |
| XcodeProjectInterpreter: () => xcodeProjectInterpreter, | |
| Pub: ThrowingPub.new, | |
| }, | |
| ); | |
| testUsingContext( | |
| 'new embedding app uses a plugin that supports v1 and v2 embedding', | |
| () async { | |
| androidProject.embeddingVersion = AndroidEmbeddingVersion.v2; | |
| createDualSupportJavaPlugin4(); | |
| await injectPlugins(flutterProject, androidPlatform: true, releaseMode: false); | |
| final File registrant = flutterProject.directory | |
| .childDirectory( | |
| fs.path.join('android', 'app', 'src', 'main', 'java', 'io', 'flutter', 'plugins'), | |
| ) | |
| .childFile('GeneratedPluginRegistrant.java'); | |
| expect(registrant, exists); | |
| expect(registrant.readAsStringSync(), contains('package io.flutter.plugins')); | |
| expect(registrant.readAsStringSync(), contains('class GeneratedPluginRegistrant')); | |
| expect( | |
| registrant.readAsStringSync(), | |
| contains('flutterEngine.getPlugins().add(new plugin4.UseBothEmbedding());'), | |
| ); | |
| }, | |
| overrides: <Type, Generator>{ | |
| FileSystem: () => fs, | |
| ProcessManager: () => FakeProcessManager.any(), | |
| XcodeProjectInterpreter: () => xcodeProjectInterpreter, | |
| Pub: ThrowingPub.new, | |
| }, | |
| ); | |
| testUsingContext( | |
| 'Modules use new embedding', | |
| () async { | |
| flutterProject.isModule = true; | |
| androidProject.embeddingVersion = AndroidEmbeddingVersion.v2; | |
| await injectPlugins(flutterProject, androidPlatform: true, releaseMode: false); | |
| final File registrant = flutterProject.directory | |
| .childDirectory( | |
| fs.path.join('android', 'app', 'src', 'main', 'java', 'io', 'flutter', 'plugins'), | |
| ) | |
| .childFile('GeneratedPluginRegistrant.java'); | |
| expect(registrant, exists); | |
| expect(registrant.readAsStringSync(), contains('package io.flutter.plugins')); | |
| expect(registrant.readAsStringSync(), contains('class GeneratedPluginRegistrant')); | |
| expect( | |
| registrant.readAsStringSync(), | |
| contains('public static void registerWith(@NonNull FlutterEngine flutterEngine)'), | |
| ); | |
| }, | |
| overrides: <Type, Generator>{ | |
| FileSystem: () => fs, | |
| ProcessManager: () => FakeProcessManager.any(), | |
| Pub: ThrowingPub.new, | |
| }, | |
| ); | |
| testUsingContext( | |
| 'Module using new plugin shows no warnings', | |
| () async { | |
| flutterProject.isModule = true; | |
| androidProject.embeddingVersion = AndroidEmbeddingVersion.v2; | |
| createNewJavaPlugin1(); | |
| await injectPlugins(flutterProject, androidPlatform: true, releaseMode: false); | |
| final File registrant = flutterProject.directory | |
| .childDirectory( | |
| fs.path.join('android', 'app', 'src', 'main', 'java', 'io', 'flutter', 'plugins'), | |
| ) | |
| .childFile('GeneratedPluginRegistrant.java'); | |
| expect( | |
| registrant.readAsStringSync(), | |
| contains('flutterEngine.getPlugins().add(new plugin1.UseNewEmbedding());'), | |
| ); | |
| expect(testLogger.errorText, isNot(contains('go/android-plugin-migration'))); | |
| }, | |
| overrides: <Type, Generator>{ | |
| FileSystem: () => fs, | |
| ProcessManager: () => FakeProcessManager.any(), | |
| XcodeProjectInterpreter: () => xcodeProjectInterpreter, | |
| Pub: ThrowingPub.new, | |
| }, | |
| ); | |
| testUsingContext( | |
| 'Module using plugin with v1 and v2 support shows no warning', | |
| () async { | |
| flutterProject.isModule = true; | |
| androidProject.embeddingVersion = AndroidEmbeddingVersion.v2; | |
| createDualSupportJavaPlugin4(); | |
| await injectPlugins(flutterProject, androidPlatform: true, releaseMode: false); | |
| final File registrant = flutterProject.directory | |
| .childDirectory( | |
| fs.path.join('android', 'app', 'src', 'main', 'java', 'io', 'flutter', 'plugins'), | |
| ) | |
| .childFile('GeneratedPluginRegistrant.java'); | |
| expect( | |
| registrant.readAsStringSync(), | |
| contains('flutterEngine.getPlugins().add(new plugin4.UseBothEmbedding());'), | |
| ); | |
| expect(testLogger.errorText, isNot(contains('go/android-plugin-migration'))); | |
| }, | |
| overrides: <Type, Generator>{ | |
| FileSystem: () => fs, | |
| ProcessManager: () => FakeProcessManager.any(), | |
| XcodeProjectInterpreter: () => xcodeProjectInterpreter, | |
| Pub: ThrowingPub.new, | |
| }, | |
| ); | |
| testUsingContext( | |
| 'App using plugin with v1 and v2 support shows no warning', | |
| () async { | |
| flutterProject.isModule = false; | |
| androidProject.embeddingVersion = AndroidEmbeddingVersion.v2; | |
| createDualSupportJavaPlugin4(); | |
| await injectPlugins(flutterProject, androidPlatform: true, releaseMode: false); | |
| final File registrant = flutterProject.directory | |
| .childDirectory( | |
| fs.path.join('android', 'app', 'src', 'main', 'java', 'io', 'flutter', 'plugins'), | |
| ) | |
| .childFile('GeneratedPluginRegistrant.java'); | |
| expect( | |
| registrant.readAsStringSync(), | |
| contains('flutterEngine.getPlugins().add(new plugin4.UseBothEmbedding());'), | |
| ); | |
| expect(testLogger.errorText, isNot(contains('go/android-plugin-migration'))); | |
| }, | |
| overrides: <Type, Generator>{ | |
| FileSystem: () => fs, | |
| ProcessManager: () => FakeProcessManager.any(), | |
| XcodeProjectInterpreter: () => xcodeProjectInterpreter, | |
| Pub: ThrowingPub.new, | |
| }, | |
| ); | |
| testUsingContext( | |
| 'Does not throw when AndroidManifest.xml is not found', | |
| () async { | |
| final File manifest = fs.file('AndroidManifest.xml'); | |
| androidProject.appManifestFile = manifest; | |
| await injectPlugins(flutterProject, androidPlatform: true, releaseMode: false); | |
| }, | |
| overrides: <Type, Generator>{ | |
| FileSystem: () => fs, | |
| ProcessManager: () => FakeProcessManager.any(), | |
| Pub: ThrowingPub.new, | |
| }, | |
| ); | |
| group('Build time plugin injection', () { | |
| testUsingContext( | |
| "Registrant for web doesn't escape slashes in imports", | |
| () async { | |
| flutterProject.isModule = true; | |
| final Directory webPluginWithNestedFile = fs.systemTempDirectory.createTempSync( | |
| 'flutter_web_plugin_with_nested.', | |
| ); | |
| webPluginWithNestedFile.childFile('pubspec.yaml').writeAsStringSync(''' | |
| flutter: | |
| plugin: | |
| platforms: | |
| web: | |
| pluginClass: WebPlugin | |
| fileName: src/web_plugin.dart | |
| '''); | |
| webPluginWithNestedFile | |
| .childDirectory('lib') | |
| .childDirectory('src') | |
| .childFile('web_plugin.dart') | |
| .createSync(recursive: true); | |
| addToPackageConfig('web_plugin_with_nested', webPluginWithNestedFile); | |
| final Directory destination = flutterProject.directory.childDirectory('lib'); | |
| await injectBuildTimePluginFilesForWebPlatform( | |
| flutterProject, | |
| destination: destination, | |
| ); | |
| final File registrant = flutterProject.directory | |
| .childDirectory('lib') | |
| .childFile('web_plugin_registrant.dart'); | |
| expect(registrant, exists); | |
| expect( | |
| registrant.readAsStringSync(), | |
| contains("import 'package:web_plugin_with_nested/src/web_plugin.dart';"), | |
| ); | |
| }, | |
| overrides: <Type, Generator>{ | |
| FileSystem: () => fs, | |
| ProcessManager: () => FakeProcessManager.any(), | |
| Pub: ThrowingPub.new, | |
| }, | |
| ); | |
| testUsingContext( | |
| 'user-selected implementation overrides inline implementation on web', | |
| () async { | |
| final List<Directory> directories = createFakePlugins(fs, <String>[ | |
| 'user_selected_url_launcher_implementation', | |
| 'url_launcher', | |
| ]); | |
| // Add inline web implementation to `user_selected_url_launcher_implementation` | |
| directories[0].childFile('pubspec.yaml').writeAsStringSync(''' | |
| flutter: | |
| plugin: | |
| implements: url_launcher | |
| platforms: | |
| web: | |
| pluginClass: UserSelectedUrlLauncherWeb | |
| fileName: src/web_plugin.dart | |
| '''); | |
| // Add inline native implementation to `url_launcher` | |
| directories[1].childFile('pubspec.yaml').writeAsStringSync(''' | |
| flutter: | |
| plugin: | |
| platforms: | |
| web: | |
| pluginClass: InlineUrlLauncherWeb | |
| fileName: src/web_plugin.dart | |
| '''); | |
| final FlutterManifest manifest = FlutterManifest.createFromString(''' | |
| name: my_app | |
| version: 1.0.0 | |
| dependencies: | |
| url_launcher: ^1.0.0 | |
| user_selected_url_launcher_implementation: ^1.0.0 | |
| ''', logger: BufferLogger.test())!; | |
| flutterProject.manifest = manifest; | |
| flutterProject.isModule = true; | |
| final Directory destination = flutterProject.directory.childDirectory('lib'); | |
| await injectBuildTimePluginFilesForWebPlatform( | |
| flutterProject, | |
| destination: destination, | |
| ); | |
| final File registrant = flutterProject.directory | |
| .childDirectory('lib') | |
| .childFile('web_plugin_registrant.dart'); | |
| expect(registrant, exists); | |
| expect( | |
| registrant.readAsStringSync(), | |
| contains( | |
| "import 'package:user_selected_url_launcher_implementation/src/web_plugin.dart';", | |
| ), | |
| ); | |
| expect( | |
| registrant.readAsStringSync(), | |
| isNot(contains("import 'package:url_launcher/src/web_plugin.dart';")), | |
| ); | |
| }, | |
| overrides: <Type, Generator>{ | |
| FileSystem: () => fs, | |
| ProcessManager: () => FakeProcessManager.any(), | |
| Pub: ThrowingPub.new, | |
| }, | |
| ); | |
| }); | |
| testUsingContext( | |
| 'Injecting creates generated Android registrant, but does not include Dart-only plugins', | |
| () async { | |
| // Create a plugin without a pluginClass. | |
| final Directory pluginDirectory = createFakePlugin(fs); | |
| pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' | |
| flutter: | |
| plugin: | |
| platforms: | |
| android: | |
| dartPluginClass: SomePlugin | |
| '''); | |
| await injectPlugins(flutterProject, androidPlatform: true, releaseMode: false); | |
| final File registrantFile = androidProject.pluginRegistrantHost | |
| .childDirectory(fs.path.join('src', 'main', 'java', 'io', 'flutter', 'plugins')) | |
| .childFile('GeneratedPluginRegistrant.java'); | |
| expect(registrantFile, exists); | |
| expect(registrantFile.readAsStringSync(), isNot(contains('SomePlugin'))); | |
| }, | |
| overrides: <Type, Generator>{ | |
| FileSystem: () => fs, | |
| ProcessManager: () => FakeProcessManager.any(), | |
| Pub: ThrowingPub.new, | |
| }, | |
| ); | |
| testUsingContext( | |
| 'Injecting creates generated iOS registrant, but does not include Dart-only plugins', | |
| () async { | |
| flutterProject.isModule = true; | |
| // Create a plugin without a pluginClass. | |
| final Directory pluginDirectory = createFakePlugin(fs); | |
| pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' | |
| flutter: | |
| plugin: | |
| platforms: | |
| ios: | |
| dartPluginClass: SomePlugin | |
| '''); | |
| final dependencyManagement = FakeDarwinDependencyManagement(); | |
| await injectPlugins( | |
| flutterProject, | |
| releaseMode: false, | |
| iosPlatform: true, | |
| darwinDependencyManagement: dependencyManagement, | |
| ); | |
| final File registrantFile = iosProject.pluginRegistrantImplementation; | |
| expect(registrantFile, exists); | |
| expect(registrantFile.readAsStringSync(), isNot(contains('SomePlugin'))); | |
| }, | |
| overrides: <Type, Generator>{ | |
| FileSystem: () => fs, | |
| ProcessManager: () => FakeProcessManager.any(), | |
| Pub: ThrowingPub.new, | |
| }, | |
| ); | |
| testUsingContext( | |
| 'Injecting does not overwrite unchanged registrant files', | |
| () async { | |
| createFakePlugin(fs); | |
| await injectPlugins(flutterProject, releaseMode: false, linuxPlatform: true); | |
| final File registrantHeader = linuxProject.managedDirectory.childFile( | |
| 'generated_plugin_registrant.h', | |
| ); | |
| final DateTime headerLastModified = registrantHeader.lastModifiedSync(); | |
| await injectPlugins(flutterProject, releaseMode: false, linuxPlatform: true); | |
| // Check that the last modified date is the same. | |
| expect(registrantHeader.lastModifiedSync(), headerLastModified); | |
| }, | |
| overrides: <Type, Generator>{ | |
| FileSystem: () => fs, | |
| ProcessManager: () => FakeProcessManager.any(), | |
| Pub: ThrowingPub.new, | |
| }, | |
| ); | |
| testUsingContext( | |
| 'Injecting creates generated macos registrant, but does not include Dart-only plugins', | |
| () async { | |
| flutterProject.isModule = true; | |
| // Create a plugin without a pluginClass. | |
| final Directory pluginDirectory = createFakePlugin(fs); | |
| pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' | |
| flutter: | |
| plugin: | |
| platforms: | |
| macos: | |
| dartPluginClass: SomePlugin | |
| '''); | |
| final dependencyManagement = FakeDarwinDependencyManagement(); | |
| await injectPlugins( | |
| flutterProject, | |
| releaseMode: false, | |
| macOSPlatform: true, | |
| darwinDependencyManagement: dependencyManagement, | |
| ); | |
| final File registrantFile = macosProject.managedDirectory.childFile( | |
| 'GeneratedPluginRegistrant.swift', | |
| ); | |
| expect(registrantFile, exists); | |
| expect(registrantFile.readAsStringSync(), isNot(contains('SomePlugin'))); | |
| }, | |
| overrides: <Type, Generator>{ | |
| FileSystem: () => fs, | |
| ProcessManager: () => FakeProcessManager.any(), | |
| Pub: ThrowingPub.new, | |
| }, | |
| ); | |
| testUsingContext( | |
| 'Invalid yaml does not crash plugin lookup.', | |
| () async { | |
| flutterProject.isModule = true; | |
| // Create a plugin without a pluginClass. | |
| final Directory pluginDirectory = createFakePlugin(fs); | |
| pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(r''' | |
| "aws ... \"Branch\": $BITBUCKET_BRANCH, \"Date\": $(date +"%m-%d-%y"), \"Time\": $(date +"%T")}\" | |
| '''); | |
| final dependencyManagement = FakeDarwinDependencyManagement(); | |
| await injectPlugins( | |
| flutterProject, | |
| releaseMode: false, | |
| macOSPlatform: true, | |
| darwinDependencyManagement: dependencyManagement, | |
| ); | |
| final File registrantFile = macosProject.managedDirectory.childFile( | |
| 'GeneratedPluginRegistrant.swift', | |
| ); | |
| expect(registrantFile, exists); | |
| }, | |
| overrides: <Type, Generator>{ | |
| FileSystem: () => fs, | |
| ProcessManager: () => FakeProcessManager.any(), | |
| Pub: ThrowingPub.new, | |
| }, | |
| ); | |
| testUsingContext( | |
| 'Injecting creates generated Linux registrant', | |
| () async { | |
| createFakePlugin(fs); | |
| await injectPlugins(flutterProject, releaseMode: false, linuxPlatform: true); | |
| final File registrantHeader = linuxProject.managedDirectory.childFile( | |
| 'generated_plugin_registrant.h', | |
| ); | |
| final File registrantImpl = linuxProject.managedDirectory.childFile( | |
| 'generated_plugin_registrant.cc', | |
| ); | |
| expect(registrantHeader, exists); | |
| expect(registrantImpl, exists); | |
| expect( | |
| registrantImpl.readAsStringSync(), | |
| contains('some_plugin_register_with_registrar'), | |
| ); | |
| }, | |
| overrides: <Type, Generator>{ | |
| FileSystem: () => fs, | |
| ProcessManager: () => FakeProcessManager.any(), | |
| Pub: ThrowingPub.new, | |
| }, | |
| ); | |
| testUsingContext( | |
| 'user-selected implementation overrides inline implementation', | |
| () async { | |
| final List<Directory> directories = createFakePlugins(fs, <String>[ | |
| 'user_selected_url_launcher_implementation', | |
| 'url_launcher', | |
| ]); | |
| // Add inline native implementation to `user_selected_url_launcher_implementation` | |
| directories[0].childFile('pubspec.yaml').writeAsStringSync(''' | |
| flutter: | |
| plugin: | |
| implements: url_launcher | |
| platforms: | |
| linux: | |
| pluginClass: UserSelectedUrlLauncherLinux | |
| '''); | |
| // Add inline native implementation to `url_launcher` | |
| directories[1].childFile('pubspec.yaml').writeAsStringSync(''' | |
| flutter: | |
| plugin: | |
| platforms: | |
| linux: | |
| pluginClass: InlineUrlLauncherLinux | |
| '''); | |
| final FlutterManifest manifest = FlutterManifest.createFromString(''' | |
| name: my_app | |
| version: 1.0.0 | |
| dependencies: | |
| url_launcher: ^1.0.0 | |
| user_selected_url_launcher_implementation: ^1.0.0 | |
| ''', logger: BufferLogger.test())!; | |
| flutterProject.manifest = manifest; | |
| await injectPlugins(flutterProject, releaseMode: false, linuxPlatform: true); | |
| final File registrantImpl = linuxProject.managedDirectory.childFile( | |
| 'generated_plugin_registrant.cc', | |
| ); | |
| expect(registrantImpl, exists); | |
| expect( | |
| registrantImpl.readAsStringSync(), | |
| contains('user_selected_url_launcher_linux_register_with_registrar'), | |
| ); | |
| expect( | |
| registrantImpl.readAsStringSync(), | |
| isNot(contains('inline_url_launcher_linux_register_with_registrar')), | |
| ); | |
| }, | |
| overrides: <Type, Generator>{ | |
| FileSystem: () => fs, | |
| ProcessManager: () => FakeProcessManager.any(), | |
| Pub: ThrowingPub.new, | |
| }, | |
| ); | |
| testUsingContext( | |
| 'user-selected implementation overrides default implementation', | |
| () async { | |
| final List<Directory> directories = createFakePlugins(fs, <String>[ | |
| 'user_selected_url_launcher_implementation', | |
| 'url_launcher', | |
| 'url_launcher_linux', | |
| ]); | |
| // Add inline native implementation to `user_selected_url_launcher_implementation` | |
| directories[0].childFile('pubspec.yaml').writeAsStringSync(''' | |
| flutter: | |
| plugin: | |
| implements: url_launcher | |
| platforms: | |
| linux: | |
| pluginClass: UserSelectedUrlLauncherLinux | |
| '''); | |
| // Add default native implementation to `url_launcher` | |
| directories[1].childFile('pubspec.yaml').writeAsStringSync(''' | |
| flutter: | |
| plugin: | |
| platforms: | |
| linux: | |
| default_package: url_launcher_linux | |
| '''); | |
| // Add inline native implementation to `url_launcher_linux` | |
| directories[1].childFile('pubspec.yaml').writeAsStringSync(''' | |
| flutter: | |
| plugin: | |
| platforms: | |
| linux: | |
| pluginClass: InlineUrlLauncherLinux | |
| '''); | |
| final FlutterManifest manifest = FlutterManifest.createFromString(''' | |
| name: my_app | |
| version: 1.0.0 | |
| dependencies: | |
| url_launcher: ^1.0.0 | |
| user_selected_url_launcher_implementation: ^1.0.0 | |
| ''', logger: BufferLogger.test())!; | |
| flutterProject.manifest = manifest; | |
| await injectPlugins(flutterProject, releaseMode: false, linuxPlatform: true); | |
| final File registrantImpl = linuxProject.managedDirectory.childFile( | |
| 'generated_plugin_registrant.cc', | |
| ); | |
| expect(registrantImpl, exists); | |
| expect( | |
| registrantImpl.readAsStringSync(), | |
| contains('user_selected_url_launcher_linux_register_with_registrar'), | |
| ); | |
| expect( | |
| registrantImpl.readAsStringSync(), | |
| isNot(contains('inline_url_launcher_linux_register_with_registrar')), | |
| ); | |
| }, | |
| overrides: <Type, Generator>{ | |
| FileSystem: () => fs, | |
| ProcessManager: () => FakeProcessManager.any(), | |
| Pub: ThrowingPub.new, | |
| }, | |
| ); | |
| testUsingContext( | |
| 'Injecting creates generated Linux registrant, but does not include Dart-only plugins', | |
| () async { | |
| // Create a plugin without a pluginClass. | |
| final Directory pluginDirectory = createFakePlugin(fs); | |
| pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' | |
| flutter: | |
| plugin: | |
| platforms: | |
| linux: | |
| dartPluginClass: SomePlugin | |
| '''); | |
| await injectPlugins(flutterProject, releaseMode: false, linuxPlatform: true); | |
| final File registrantImpl = linuxProject.managedDirectory.childFile( | |
| 'generated_plugin_registrant.cc', | |
| ); | |
| expect(registrantImpl, exists); | |
| final String contents = registrantImpl.readAsStringSync(); | |
| expect(contents, isNot(contains('SomePlugin'))); | |
| expect(contents, isNot(contains('some_plugin'))); | |
| }, | |
| overrides: <Type, Generator>{ | |
| FileSystem: () => fs, | |
| ProcessManager: () => FakeProcessManager.any(), | |
| Pub: ThrowingPub.new, | |
| }, | |
| ); | |
| testUsingContext( | |
| 'Injecting creates generated Linux plugin Cmake file', | |
| () async { | |
| createFakePlugin(fs); | |
| await injectPlugins(flutterProject, releaseMode: false, linuxPlatform: true); | |
| final File pluginMakefile = linuxProject.generatedPluginCmakeFile; | |
| expect(pluginMakefile, exists); | |
| final String contents = pluginMakefile.readAsStringSync(); | |
| expect(contents, contains('some_plugin')); | |
| expect( | |
| contents, | |
| contains(r'target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin)'), | |
| ); | |
| expect( | |
| contents, | |
| contains(r'list(APPEND PLUGIN_BUNDLED_LIBRARIES $<TARGET_FILE:${plugin}_plugin>)'), | |
| ); | |
| expect( | |
| contents, | |
| contains(r'list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries})'), | |
| ); | |
| }, | |
| overrides: <Type, Generator>{ | |
| FileSystem: () => fs, | |
| ProcessManager: () => FakeProcessManager.any(), | |
| Pub: ThrowingPub.new, | |
| }, | |
| ); | |
| testUsingContext( | |
| 'Generated Linux plugin files sorts by plugin name', | |
| () async { | |
| createFakePlugins(fs, <String>[ | |
| 'plugin_d', | |
| 'plugin_a', | |
| '/local_plugins/plugin_c', | |
| '/local_plugins/plugin_b', | |
| ]); | |
| await injectPlugins(flutterProject, releaseMode: false, linuxPlatform: true); | |
| final File pluginCmakeFile = linuxProject.generatedPluginCmakeFile; | |
| final File pluginRegistrant = linuxProject.managedDirectory.childFile( | |
| 'generated_plugin_registrant.cc', | |
| ); | |
| for (final file in <File>[pluginCmakeFile, pluginRegistrant]) { | |
| final String contents = file.readAsStringSync(); | |
| expect(contents.indexOf('plugin_a'), lessThan(contents.indexOf('plugin_b'))); | |
| expect(contents.indexOf('plugin_b'), lessThan(contents.indexOf('plugin_c'))); | |
| expect(contents.indexOf('plugin_c'), lessThan(contents.indexOf('plugin_d'))); | |
| } | |
| }, | |
| overrides: <Type, Generator>{ | |
| FileSystem: () => fs, | |
| ProcessManager: () => FakeProcessManager.any(), | |
| Pub: ThrowingPub.new, | |
| }, | |
| ); | |
| testUsingContext( | |
| 'Injecting creates generated Windows registrant', | |
| () async { | |
| createFakePlugin(fs); | |
| await injectPlugins(flutterProject, releaseMode: false, windowsPlatform: true); | |
| final File registrantHeader = windowsProject.managedDirectory.childFile( | |
| 'generated_plugin_registrant.h', | |
| ); | |
| final File registrantImpl = windowsProject.managedDirectory.childFile( | |
| 'generated_plugin_registrant.cc', | |
| ); | |
| expect(registrantHeader, exists); | |
| expect(registrantImpl, exists); | |
| expect(registrantImpl.readAsStringSync(), contains('SomePluginRegisterWithRegistrar')); | |
| }, | |
| overrides: <Type, Generator>{ | |
| FileSystem: () => fs, | |
| ProcessManager: () => FakeProcessManager.any(), | |
| Pub: ThrowingPub.new, | |
| }, | |
| ); | |
| testUsingContext( | |
| 'Injecting creates generated Windows registrant, but does not include Dart-only plugins', | |
| () async { | |
| // Create a plugin without a pluginClass. | |
| final Directory pluginDirectory = createFakePlugin(fs); | |
| pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' | |
| flutter: | |
| plugin: | |
| platforms: | |
| windows: | |
| dartPluginClass: SomePlugin | |
| '''); | |
| await injectPlugins(flutterProject, releaseMode: false, windowsPlatform: true); | |
| final File registrantImpl = windowsProject.managedDirectory.childFile( | |
| 'generated_plugin_registrant.cc', | |
| ); | |
| expect(registrantImpl, exists); | |
| expect(registrantImpl.readAsStringSync(), isNot(contains('SomePlugin'))); | |
| }, | |
| overrides: <Type, Generator>{ | |
| FileSystem: () => fs, | |
| ProcessManager: () => FakeProcessManager.any(), | |
| Pub: ThrowingPub.new, | |
| }, | |
| ); | |
| testUsingContext( | |
| 'Generated Windows plugin files sorts by plugin name', | |
| () async { | |
| createFakePlugins(fs, <String>[ | |
| 'plugin_d', | |
| 'plugin_a', | |
| '/local_plugins/plugin_c', | |
| '/local_plugins/plugin_b', | |
| ]); | |
| await injectPlugins(flutterProject, releaseMode: false, windowsPlatform: true); | |
| final File pluginCmakeFile = windowsProject.generatedPluginCmakeFile; | |
| final File pluginRegistrant = windowsProject.managedDirectory.childFile( | |
| 'generated_plugin_registrant.cc', | |
| ); | |
| for (final file in <File>[pluginCmakeFile, pluginRegistrant]) { | |
| final String contents = file.readAsStringSync(); | |
| expect(contents.indexOf('plugin_a'), lessThan(contents.indexOf('plugin_b'))); | |
| expect(contents.indexOf('plugin_b'), lessThan(contents.indexOf('plugin_c'))); | |
| expect(contents.indexOf('plugin_c'), lessThan(contents.indexOf('plugin_d'))); | |
| } | |
| }, | |
| overrides: <Type, Generator>{ | |
| FileSystem: () => fs, | |
| ProcessManager: () => FakeProcessManager.any(), | |
| Pub: ThrowingPub.new, | |
| }, | |
| ); | |
| testUsingContext( | |
| 'Generated plugin CMake files always use posix-style paths', | |
| () async { | |
| // Re-run the setup using the Windows filesystem. | |
| setUpProject(fsWindows); | |
| createFakePlugin(fsWindows); | |
| await injectPlugins( | |
| flutterProject, | |
| releaseMode: false, | |
| linuxPlatform: true, | |
| windowsPlatform: true, | |
| ); | |
| for (final project in <CmakeBasedProject?>[linuxProject, windowsProject]) { | |
| final File pluginCmakefile = project!.generatedPluginCmakeFile; | |
| expect(pluginCmakefile, exists); | |
| final String contents = pluginCmakefile.readAsStringSync(); | |
| expect(contents, contains('add_subdirectory(flutter/ephemeral/.plugin_symlinks')); | |
| } | |
| }, | |
| overrides: <Type, Generator>{ | |
| FileSystem: () => fsWindows, | |
| ProcessManager: () => FakeProcessManager.any(), | |
| Pub: ThrowingPub.new, | |
| }, | |
| ); | |
| testUsingContext( | |
| 'iOS and macOS project setup up Darwin Dependency Management', | |
| () async { | |
| final dependencyManagement = FakeDarwinDependencyManagement(); | |
| await injectPlugins( | |
| flutterProject, | |
| releaseMode: false, | |
| iosPlatform: true, | |
| macOSPlatform: true, | |
| darwinDependencyManagement: dependencyManagement, | |
| ); | |
| expect(dependencyManagement.setupPlatforms, <FlutterDarwinPlatform>[ | |
| FlutterDarwinPlatform.ios, | |
| FlutterDarwinPlatform.macos, | |
| ]); | |
| }, | |
| overrides: <Type, Generator>{ | |
| FileSystem: () => fs, | |
| ProcessManager: () => FakeProcessManager.any(), | |
| Pub: ThrowingPub.new, | |
| }, | |
| ); | |
| testUsingContext( | |
| 'non-iOS or macOS project does not setup up Darwin Dependency Management', | |
| () async { | |
| final dependencyManagement = FakeDarwinDependencyManagement(); | |
| await injectPlugins( | |
| flutterProject, | |
| releaseMode: false, | |
| darwinDependencyManagement: dependencyManagement, | |
| ); | |
| expect(dependencyManagement.setupPlatforms, <FlutterDarwinPlatform>[]); | |
| }, | |
| overrides: <Type, Generator>{ | |
| FileSystem: () => fs, | |
| ProcessManager: () => FakeProcessManager.any(), | |
| Pub: ThrowingPub.new, | |
| }, | |
| ); | |
| }); | |
| group('createPluginSymlinks', () { | |
| FeatureFlags? featureFlags; | |
| setUp(() { | |
| featureFlags = TestFeatureFlags(isLinuxEnabled: true, isWindowsEnabled: true); | |
| }); | |
| testUsingContext( | |
| 'Symlinks are created for Linux plugins', | |
| () async { | |
| linuxProject.exists = true; | |
| createFakePlugin(fs); | |
| // refreshPluginsList should call createPluginSymlinks. | |
| await refreshPluginsList(flutterProject); | |
| expect(linuxProject.pluginSymlinkDirectory.childLink('some_plugin'), exists); | |
| }, | |
| overrides: <Type, Generator>{ | |
| FileSystem: () => fs, | |
| ProcessManager: () => FakeProcessManager.any(), | |
| FeatureFlags: () => featureFlags, | |
| }, | |
| ); | |
| testUsingContext( | |
| 'Symlinks are created for Windows plugins', | |
| () async { | |
| windowsProject.exists = true; | |
| createFakePlugin(fs); | |
| // refreshPluginsList should call createPluginSymlinks. | |
| await refreshPluginsList(flutterProject); | |
| expect(windowsProject.pluginSymlinkDirectory.childLink('some_plugin'), exists); | |
| }, | |
| overrides: <Type, Generator>{ | |
| FileSystem: () => fs, | |
| ProcessManager: () => FakeProcessManager.any(), | |
| FeatureFlags: () => featureFlags, | |
| }, | |
| ); | |
| testUsingContext( | |
| 'Existing symlinks are removed when no longer in use with force', | |
| () { | |
| linuxProject.exists = true; | |
| windowsProject.exists = true; | |
| final dummyFiles = <File>[ | |
| flutterProject.linux.pluginSymlinkDirectory.childFile('dummy'), | |
| flutterProject.windows.pluginSymlinkDirectory.childFile('dummy'), | |
| ]; | |
| for (final file in dummyFiles) { | |
| file.createSync(recursive: true); | |
| } | |
| createPluginSymlinks(flutterProject, force: true); | |
| for (final file in dummyFiles) { | |
| expect(file, isNot(exists)); | |
| } | |
| }, | |
| overrides: <Type, Generator>{ | |
| FileSystem: () => fs, | |
| ProcessManager: () => FakeProcessManager.any(), | |
| FeatureFlags: () => featureFlags, | |
| }, | |
| ); | |
| testUsingContext( | |
| 'Existing symlinks are removed automatically on refresh when no longer in use', | |
| () async { | |
| linuxProject.exists = true; | |
| windowsProject.exists = true; | |
| final dummyFiles = <File>[ | |
| flutterProject.linux.pluginSymlinkDirectory.childFile('dummy'), | |
| flutterProject.windows.pluginSymlinkDirectory.childFile('dummy'), | |
| ]; | |
| for (final file in dummyFiles) { | |
| file.createSync(recursive: true); | |
| } | |
| // refreshPluginsList should remove existing links and recreate on changes. | |
| createFakePlugin(fs); | |
| await refreshPluginsList(flutterProject); | |
| for (final file in dummyFiles) { | |
| expect(file, isNot(exists)); | |
| } | |
| }, | |
| overrides: <Type, Generator>{ | |
| FileSystem: () => fs, | |
| ProcessManager: () => FakeProcessManager.any(), | |
| FeatureFlags: () => featureFlags, | |
| }, | |
| ); | |
| testUsingContext( | |
| 'createPluginSymlinks is a no-op without force when up to date', | |
| () { | |
| linuxProject.exists = true; | |
| windowsProject.exists = true; | |
| final dummyFiles = <File>[ | |
| flutterProject.linux.pluginSymlinkDirectory.childFile('dummy'), | |
| flutterProject.windows.pluginSymlinkDirectory.childFile('dummy'), | |
| ]; | |
| for (final file in dummyFiles) { | |
| file.createSync(recursive: true); | |
| } | |
| // Without force, this should do nothing to existing files. | |
| createPluginSymlinks(flutterProject); | |
| for (final file in dummyFiles) { | |
| expect(file, exists); | |
| } | |
| }, | |
| overrides: <Type, Generator>{ | |
| FileSystem: () => fs, | |
| ProcessManager: () => FakeProcessManager.any(), | |
| FeatureFlags: () => featureFlags, | |
| }, | |
| ); | |
| testUsingContext( | |
| 'createPluginSymlinks repairs missing links', | |
| () async { | |
| linuxProject.exists = true; | |
| windowsProject.exists = true; | |
| createFakePlugin(fs); | |
| await refreshPluginsList(flutterProject); | |
| final links = <Link>[ | |
| linuxProject.pluginSymlinkDirectory.childLink('some_plugin'), | |
| windowsProject.pluginSymlinkDirectory.childLink('some_plugin'), | |
| ]; | |
| for (final link in links) { | |
| link.deleteSync(); | |
| } | |
| createPluginSymlinks(flutterProject); | |
| for (final link in links) { | |
| expect(link, exists); | |
| } | |
| }, | |
| overrides: <Type, Generator>{ | |
| FileSystem: () => fs, | |
| ProcessManager: () => FakeProcessManager.any(), | |
| FeatureFlags: () => featureFlags, | |
| }, | |
| ); | |
| testUsingContext( | |
| 'createPluginSymlinks repairs broken symlinks without failing', | |
| () async { | |
| linuxProject.exists = true; | |
| windowsProject.exists = true; | |
| final Directory pluginDir = createFakePlugin(fs); | |
| await refreshPluginsList(flutterProject); | |
| final links = <Link>[ | |
| linuxProject.pluginSymlinkDirectory.childLink('some_plugin'), | |
| windowsProject.pluginSymlinkDirectory.childLink('some_plugin'), | |
| ]; | |
| for (final link in links) { | |
| link.deleteSync(); | |
| link.createSync('/non_existent_target_path'); | |
| } | |
| createPluginSymlinks(flutterProject); | |
| for (final link in links) { | |
| expect(link, exists); | |
| expect(fs.path.normalize(link.targetSync()), fs.path.normalize(pluginDir.path)); | |
| } | |
| }, | |
| overrides: <Type, Generator>{ | |
| FileSystem: () => fs, | |
| ProcessManager: () => FakeProcessManager.any(), | |
| FeatureFlags: () => featureFlags, | |
| }, | |
| ); | |
| testUsingContext( | |
| 'createPluginSymlinks replaces existing files with symlinks without failing', | |
| () async { | |
| linuxProject.exists = true; | |
| windowsProject.exists = true; | |
| final Directory pluginDir = createFakePlugin(fs); | |
| await refreshPluginsList(flutterProject); | |
| final files = <File>[ | |
| linuxProject.pluginSymlinkDirectory.childFile('some_plugin'), | |
| windowsProject.pluginSymlinkDirectory.childFile('some_plugin'), | |
| ]; | |
| for (final file in files) { | |
| ErrorHandlingFileSystem.deleteIfExists(file, recursive: true); | |
| file.createSync(recursive: true); | |
| file.writeAsStringSync('stale content'); | |
| } | |
| createPluginSymlinks(flutterProject); | |
| final links = <Link>[ | |
| linuxProject.pluginSymlinkDirectory.childLink('some_plugin'), | |
| windowsProject.pluginSymlinkDirectory.childLink('some_plugin'), | |
| ]; | |
| for (final link in links) { | |
| expect(link, exists); | |
| expect(fs.path.normalize(link.targetSync()), fs.path.normalize(pluginDir.path)); | |
| } | |
| }, | |
| overrides: <Type, Generator>{ | |
| FileSystem: () => fs, | |
| ProcessManager: () => FakeProcessManager.any(), | |
| FeatureFlags: () => featureFlags, | |
| }, | |
| ); | |
| }); | |
| group('pubspec', () { | |
| late Directory projectDir; | |
| late Directory tempDir; | |
| setUp(() { | |
| tempDir = globals.fs.systemTempDirectory.createTempSync('flutter_plugin_test.'); | |
| projectDir = tempDir.childDirectory('flutter_project'); | |
| }); | |
| tearDown(() { | |
| tryToDelete(tempDir); | |
| }); | |
| void createPubspecFile(String yamlString) { | |
| projectDir.childFile('pubspec.yaml') | |
| ..createSync(recursive: true) | |
| ..writeAsStringSync(yamlString); | |
| } | |
| testUsingContext('validatePubspecForPlugin works', () async { | |
| const pluginYaml = ''' | |
| flutter: | |
| plugin: | |
| platforms: | |
| ios: | |
| pluginClass: SomePlugin | |
| macos: | |
| pluginClass: SomePlugin | |
| windows: | |
| pluginClass: SomePlugin | |
| linux: | |
| pluginClass: SomePlugin | |
| web: | |
| pluginClass: SomePlugin | |
| fileName: lib/SomeFile.dart | |
| android: | |
| pluginClass: SomePlugin | |
| package: AndroidPackage | |
| '''; | |
| createPubspecFile(pluginYaml); | |
| validatePubspecForPlugin( | |
| projectDir: projectDir.absolute.path, | |
| pluginClass: 'SomePlugin', | |
| expectedPlatforms: <String>['ios', 'macos', 'windows', 'linux', 'android', 'web'], | |
| androidIdentifier: 'AndroidPackage', | |
| webFileName: 'lib/SomeFile.dart', | |
| ); | |
| }); | |
| testUsingContext( | |
| 'Plugin.fromYaml rejects a plugin class with code-injection characters', | |
| () async { | |
| // A (possibly transitive) dependency must not be able to smuggle | |
| // arbitrary source into the generated GeneratedPluginRegistrant by | |
| // declaring a pluginClass that is not a plain identifier. | |
| const maliciousYaml = ''' | |
| platforms: | |
| macos: | |
| pluginClass: "SomePlugin(); evilInjectedCall(); if (false) { SomePlugin" | |
| '''; | |
| expect( | |
| () => Plugin.fromYaml( | |
| 'evil_plugin', | |
| '', | |
| loadYaml(maliciousYaml) as YamlMap, | |
| null, | |
| const <String>[], | |
| fileSystem: globals.fs, | |
| isDevDependency: false, | |
| ), | |
| throwsToolExit(message: 'Invalid plugin specification evil_plugin'), | |
| ); | |
| }, | |
| ); | |
| testUsingContext( | |
| 'Plugin.fromYaml rejects a web plugin whose pluginClass/fileName contain injection', | |
| () async { | |
| const maliciousYaml = ''' | |
| platforms: | |
| web: | |
| pluginClass: "P; void pwn() {} //" | |
| fileName: some_file.dart | |
| '''; | |
| expect( | |
| () => Plugin.fromYaml( | |
| 'evil_web_plugin', | |
| '', | |
| loadYaml(maliciousYaml) as YamlMap, | |
| null, | |
| const <String>[], | |
| fileSystem: globals.fs, | |
| isDevDependency: false, | |
| ), | |
| throwsToolExit(), | |
| ); | |
| }, | |
| ); | |
| testUsingContext( | |
| 'Plugin.fromYaml rejects a dartPluginClass with code-injection characters', | |
| () async { | |
| // A dart plugin class is also interpolated into generated registrant | |
| // source, so it must be a plain identifier like the native one. | |
| const maliciousYaml = ''' | |
| platforms: | |
| android: | |
| dartPluginClass: "Evil(); evilInjectedCall(); class Evil" | |
| '''; | |
| expect( | |
| () => Plugin.fromYaml( | |
| 'evil_dart_plugin', | |
| '', | |
| loadYaml(maliciousYaml) as YamlMap, | |
| null, | |
| const <String>[], | |
| fileSystem: globals.fs, | |
| isDevDependency: false, | |
| ), | |
| throwsToolExit(message: 'Invalid plugin specification evil_dart_plugin'), | |
| ); | |
| }, | |
| ); | |
| testUsingContext( | |
| 'Plugin.fromYaml rejects legacy-format identifiers with code-injection characters', | |
| () async { | |
| // The legacy plugin format is parsed by Plugin._fromLegacyYaml, which | |
| // builds AndroidPlugin/IOSPlugin through their plain constructors and | |
| // so never reaches the platform `fromYaml` validation. Its fields are | |
| // interpolated into the generated registrant just the same. | |
| const maliciousYaml = ''' | |
| androidPackage: com.example.evil | |
| pluginClass: "Evil(); evilInjectedCall(); //" | |
| '''; | |
| expect( | |
| () => Plugin.fromYaml( | |
| 'evil_legacy_plugin', | |
| '', | |
| loadYaml(maliciousYaml) as YamlMap, | |
| null, | |
| const <String>[], | |
| fileSystem: globals.fs, | |
| isDevDependency: false, | |
| ), | |
| throwsToolExit(message: 'The "pluginClass" must be a valid identifier'), | |
| ); | |
| }, | |
| ); | |
| testUsingContext( | |
| 'Plugin.fromYaml rejects a legacy-format androidPackage injection', | |
| () async { | |
| const maliciousYaml = ''' | |
| androidPackage: "com.example.evil.Payload.run(); //" | |
| pluginClass: EvilPlugin | |
| '''; | |
| expect( | |
| () => Plugin.fromYaml( | |
| 'evil_legacy_plugin', | |
| '', | |
| loadYaml(maliciousYaml) as YamlMap, | |
| null, | |
| const <String>[], | |
| fileSystem: globals.fs, | |
| isDevDependency: false, | |
| ), | |
| throwsToolExit(message: 'The "androidPackage" must be a valid identifier'), | |
| ); | |
| }, | |
| ); | |
| testUsingContext('Plugin.fromYaml rejects a legacy-format iosPrefix injection', () async { | |
| const maliciousYaml = ''' | |
| androidPackage: com.example.evil | |
| pluginClass: EvilPlugin | |
| iosPrefix: "FLT; evilInjectedCall(); //" | |
| '''; | |
| expect( | |
| () => Plugin.fromYaml( | |
| 'evil_legacy_plugin', | |
| '', | |
| loadYaml(maliciousYaml) as YamlMap, | |
| null, | |
| const <String>[], | |
| fileSystem: globals.fs, | |
| isDevDependency: false, | |
| ), | |
| throwsToolExit(message: 'The "iosPrefix" must be a valid identifier'), | |
| ); | |
| }); | |
| testUsingContext('Plugin.fromYaml reports every invalid legacy-format field at once', () async { | |
| const maliciousYaml = ''' | |
| androidPackage: "com.example.evil.Payload.run(); //" | |
| pluginClass: "Evil(); evilInjectedCall(); //" | |
| iosPrefix: "FLT; evilInjectedCall(); //" | |
| '''; | |
| expect( | |
| () => Plugin.fromYaml( | |
| 'evil_legacy_plugin', | |
| '', | |
| loadYaml(maliciousYaml) as YamlMap, | |
| null, | |
| const <String>[], | |
| fileSystem: globals.fs, | |
| isDevDependency: false, | |
| ), | |
| throwsToolExit( | |
| message: | |
| 'Invalid plugin specification evil_legacy_plugin.\n' | |
| 'The "androidPackage" must be a valid identifier, optionally with dot-separated segments.\n' | |
| 'The "iosPrefix" must be a valid identifier, optionally with dot-separated segments.\n' | |
| 'The "pluginClass" must be a valid identifier, optionally with dot-separated segments.', | |
| ), | |
| ); | |
| }); | |
| testUsingContext('Plugin.fromYaml accepts a legacy-format plugin declaration', () async { | |
| const legacyYaml = ''' | |
| androidPackage: com.example.sample | |
| pluginClass: SamplePlugin | |
| iosPrefix: FLT | |
| '''; | |
| final plugin = Plugin.fromYaml( | |
| 'sample_plugin', | |
| '', | |
| loadYaml(legacyYaml) as YamlMap, | |
| null, | |
| const <String>[], | |
| fileSystem: globals.fs, | |
| isDevDependency: false, | |
| ); | |
| expect(plugin.platforms, contains(AndroidPlugin.kConfigKey)); | |
| expect(plugin.platforms, contains(IOSPlugin.kConfigKey)); | |
| }); | |
| testUsingContext('createPlatformsYamlMap should create the correct map', () async { | |
| final YamlMap map = Plugin.createPlatformsYamlMap( | |
| <String>['ios', 'android', 'linux'], | |
| 'PluginClass', | |
| 'some.android.package', | |
| ); | |
| expect(map['ios'], <String, String>{'pluginClass': 'PluginClass'}); | |
| expect(map['android'], <String, String>{ | |
| 'pluginClass': 'PluginClass', | |
| 'package': 'some.android.package', | |
| }); | |
| expect(map['linux'], <String, String>{'pluginClass': 'PluginClass'}); | |
| }); | |
| testUsingContext('createPlatformsYamlMap should create empty map', () async { | |
| final YamlMap map = Plugin.createPlatformsYamlMap(<String>[], 'foo', 'bar'); | |
| expect(map.isEmpty, true); | |
| }); | |
| testUsingContext('Platform plugin fromYaml factories perform validation', () async { | |
| expect( | |
| () => AndroidPlugin.fromYaml('foo', YamlMap.wrap(<String, dynamic>{}), '', globals.fs), | |
| throwsToolExit(message: 'Invalid "android" plugin specification for plugin "foo".'), | |
| ); | |
| expect( | |
| () => IOSPlugin.fromYaml('foo', YamlMap.wrap(<String, dynamic>{})), | |
| throwsToolExit(message: 'Invalid "ios" plugin specification for plugin "foo".'), | |
| ); | |
| expect( | |
| () => MacOSPlugin.fromYaml('foo', YamlMap.wrap(<String, dynamic>{})), | |
| throwsToolExit(message: 'Invalid "macos" plugin specification for plugin "foo".'), | |
| ); | |
| expect( | |
| () => WindowsPlugin.fromYaml('foo', YamlMap.wrap(<String, dynamic>{})), | |
| throwsToolExit(message: 'Invalid "windows" plugin specification for plugin "foo".'), | |
| ); | |
| expect( | |
| () => LinuxPlugin.fromYaml('foo', YamlMap.wrap(<String, dynamic>{})), | |
| throwsToolExit(message: 'Invalid "linux" plugin specification for plugin "foo".'), | |
| ); | |
| }); | |
| }); | |
| group('Plugin files', () { | |
| testWithoutContext('for SwiftPM and podspec paths for iOS and macOS plugins', () async { | |
| final fs = MemoryFileSystem.test(); | |
| final plugin = Plugin( | |
| name: 'test', | |
| path: '/path/to/test/', | |
| defaultPackagePlatforms: const <String, String>{}, | |
| pluginDartClassPlatforms: const <String, DartPluginClassAndFilePair>{}, | |
| platforms: const <String, PluginPlatform>{ | |
| IOSPlugin.kConfigKey: IOSPlugin(name: 'test', classPrefix: ''), | |
| MacOSPlugin.kConfigKey: MacOSPlugin(name: 'test'), | |
| }, | |
| dependencies: <String>[], | |
| isDirectDependency: true, | |
| isDevDependency: false, | |
| ); | |
| expect(plugin.pluginSwiftPackagePath(fs, IOSPlugin.kConfigKey), '/path/to/test/ios/test'); | |
| expect( | |
| plugin.pluginSwiftPackagePath(fs, MacOSPlugin.kConfigKey), | |
| '/path/to/test/macos/test', | |
| ); | |
| expect( | |
| plugin.pluginSwiftPackagePath(fs, IOSPlugin.kConfigKey, overridePath: '/override'), | |
| '/override/ios/test', | |
| ); | |
| expect( | |
| plugin.pluginSwiftPackagePath(fs, MacOSPlugin.kConfigKey, overridePath: '/override'), | |
| '/override/macos/test', | |
| ); | |
| expect( | |
| plugin.pluginSwiftPackageManifestPath(fs, IOSPlugin.kConfigKey), | |
| '/path/to/test/ios/test/Package.swift', | |
| ); | |
| expect( | |
| plugin.pluginSwiftPackageManifestPath(fs, MacOSPlugin.kConfigKey), | |
| '/path/to/test/macos/test/Package.swift', | |
| ); | |
| expect( | |
| plugin.pluginPodspecPath(fs, IOSPlugin.kConfigKey), | |
| '/path/to/test/ios/test.podspec', | |
| ); | |
| expect( | |
| plugin.pluginPodspecPath(fs, MacOSPlugin.kConfigKey), | |
| '/path/to/test/macos/test.podspec', | |
| ); | |
| }); | |
| testWithoutContext('for SwiftPM and podspec paths for darwin plugins', () async { | |
| final fs = MemoryFileSystem.test(); | |
| final plugin = Plugin( | |
| name: 'test', | |
| path: '/path/to/test/', | |
| defaultPackagePlatforms: const <String, String>{}, | |
| pluginDartClassPlatforms: const <String, DartPluginClassAndFilePair>{}, | |
| platforms: const <String, PluginPlatform>{ | |
| IOSPlugin.kConfigKey: IOSPlugin( | |
| name: 'test', | |
| classPrefix: '', | |
| sharedDarwinSource: true, | |
| ), | |
| MacOSPlugin.kConfigKey: MacOSPlugin(name: 'test', sharedDarwinSource: true), | |
| }, | |
| dependencies: <String>[], | |
| isDirectDependency: true, | |
| isDevDependency: false, | |
| ); | |
| expect( | |
| plugin.pluginSwiftPackagePath(fs, IOSPlugin.kConfigKey), | |
| '/path/to/test/darwin/test', | |
| ); | |
| expect( | |
| plugin.pluginSwiftPackagePath(fs, MacOSPlugin.kConfigKey), | |
| '/path/to/test/darwin/test', | |
| ); | |
| expect( | |
| plugin.pluginSwiftPackagePath(fs, IOSPlugin.kConfigKey, overridePath: '/override'), | |
| '/override/darwin/test', | |
| ); | |
| expect( | |
| plugin.pluginSwiftPackagePath(fs, MacOSPlugin.kConfigKey, overridePath: '/override'), | |
| '/override/darwin/test', | |
| ); | |
| expect( | |
| plugin.pluginSwiftPackageManifestPath(fs, IOSPlugin.kConfigKey), | |
| '/path/to/test/darwin/test/Package.swift', | |
| ); | |
| expect( | |
| plugin.pluginSwiftPackageManifestPath(fs, MacOSPlugin.kConfigKey), | |
| '/path/to/test/darwin/test/Package.swift', | |
| ); | |
| expect( | |
| plugin.pluginPodspecPath(fs, IOSPlugin.kConfigKey), | |
| '/path/to/test/darwin/test.podspec', | |
| ); | |
| expect( | |
| plugin.pluginPodspecPath(fs, MacOSPlugin.kConfigKey), | |
| '/path/to/test/darwin/test.podspec', | |
| ); | |
| }); | |
| testWithoutContext('for SwiftPM and podspec paths for non darwin plugins', () async { | |
| final fs = MemoryFileSystem.test(); | |
| final plugin = Plugin( | |
| name: 'test', | |
| path: '/path/to/test/', | |
| defaultPackagePlatforms: const <String, String>{}, | |
| pluginDartClassPlatforms: const <String, DartPluginClassAndFilePair>{}, | |
| platforms: const <String, PluginPlatform>{ | |
| WindowsPlugin.kConfigKey: WindowsPlugin(name: 'test', pluginClass: ''), | |
| }, | |
| dependencies: <String>[], | |
| isDirectDependency: true, | |
| isDevDependency: false, | |
| ); | |
| expect(plugin.pluginSwiftPackagePath(fs, IOSPlugin.kConfigKey), isNull); | |
| expect(plugin.pluginSwiftPackagePath(fs, MacOSPlugin.kConfigKey), isNull); | |
| expect(plugin.pluginSwiftPackagePath(fs, WindowsPlugin.kConfigKey), isNull); | |
| expect(plugin.pluginSwiftPackageManifestPath(fs, IOSPlugin.kConfigKey), isNull); | |
| expect(plugin.pluginSwiftPackageManifestPath(fs, MacOSPlugin.kConfigKey), isNull); | |
| expect(plugin.pluginSwiftPackageManifestPath(fs, WindowsPlugin.kConfigKey), isNull); | |
| expect(plugin.pluginPodspecPath(fs, IOSPlugin.kConfigKey), isNull); | |
| expect(plugin.pluginPodspecPath(fs, MacOSPlugin.kConfigKey), isNull); | |
| expect(plugin.pluginPodspecPath(fs, WindowsPlugin.kConfigKey), isNull); | |
| }); | |
| testWithoutContext('supportSwiftPackageManagerForPlatform if manifest exists', () { | |
| final fs = MemoryFileSystem.test(); | |
| final plugin = Plugin( | |
| name: 'test', | |
| path: '/path/to/test/', | |
| defaultPackagePlatforms: const <String, String>{}, | |
| pluginDartClassPlatforms: const <String, DartPluginClassAndFilePair>{}, | |
| platforms: const <String, PluginPlatform>{ | |
| IOSPlugin.kConfigKey: IOSPlugin(name: 'test', classPrefix: ''), | |
| MacOSPlugin.kConfigKey: MacOSPlugin(name: 'test'), | |
| }, | |
| dependencies: <String>[], | |
| isDirectDependency: true, | |
| isDevDependency: false, | |
| ); | |
| expect(plugin.supportSwiftPackageManagerForPlatform(fs, IOSPlugin.kConfigKey), isFalse); | |
| expect(plugin.supportSwiftPackageManagerForPlatform(fs, MacOSPlugin.kConfigKey), isFalse); | |
| fs | |
| .file(fs.path.join(plugin.path, 'ios', plugin.name, 'Package.swift')) | |
| .createSync(recursive: true); | |
| expect(plugin.supportSwiftPackageManagerForPlatform(fs, IOSPlugin.kConfigKey), isTrue); | |
| expect(plugin.supportSwiftPackageManagerForPlatform(fs, MacOSPlugin.kConfigKey), isFalse); | |
| fs | |
| .file(fs.path.join(plugin.path, 'macos', plugin.name, 'Package.swift')) | |
| .createSync(recursive: true); | |
| expect(plugin.supportSwiftPackageManagerForPlatform(fs, IOSPlugin.kConfigKey), isTrue); | |
| expect(plugin.supportSwiftPackageManagerForPlatform(fs, MacOSPlugin.kConfigKey), isTrue); | |
| final darwinPlugin = Plugin( | |
| name: 'test', | |
| path: '/path/to/test/', | |
| defaultPackagePlatforms: const <String, String>{}, | |
| pluginDartClassPlatforms: const <String, DartPluginClassAndFilePair>{}, | |
| platforms: const <String, PluginPlatform>{ | |
| IOSPlugin.kConfigKey: IOSPlugin( | |
| name: 'test', | |
| classPrefix: '', | |
| sharedDarwinSource: true, | |
| ), | |
| MacOSPlugin.kConfigKey: MacOSPlugin(name: 'test', sharedDarwinSource: true), | |
| }, | |
| dependencies: <String>[], | |
| isDirectDependency: true, | |
| isDevDependency: false, | |
| ); | |
| expect( | |
| darwinPlugin.supportSwiftPackageManagerForPlatform(fs, IOSPlugin.kConfigKey), | |
| isFalse, | |
| ); | |
| expect( | |
| darwinPlugin.supportSwiftPackageManagerForPlatform(fs, MacOSPlugin.kConfigKey), | |
| isFalse, | |
| ); | |
| fs | |
| .file(fs.path.join(darwinPlugin.path, 'darwin', darwinPlugin.name, 'Package.swift')) | |
| .createSync(recursive: true); | |
| expect( | |
| darwinPlugin.supportSwiftPackageManagerForPlatform(fs, IOSPlugin.kConfigKey), | |
| isTrue, | |
| ); | |
| expect( | |
| darwinPlugin.supportSwiftPackageManagerForPlatform(fs, MacOSPlugin.kConfigKey), | |
| isTrue, | |
| ); | |
| }); | |
| }); | |
| testWithoutContext( | |
| 'Symlink failures give developer mode instructions on recent versions of Windows', | |
| () async { | |
| final Platform platform = FakePlatform(operatingSystem: 'windows'); | |
| final os = FakeOperatingSystemUtils('Microsoft Windows [Version 10.0.14972.1]'); | |
| const e = FileSystemException('', '', OSError('', 1314)); | |
| expect( | |
| () => handleSymlinkException( | |
| e, | |
| platform: platform, | |
| os: os, | |
| source: pubCachePath, | |
| destination: ephemeralPackagePath, | |
| ), | |
| throwsToolExit(message: 'start ms-settings:developers'), | |
| ); | |
| }, | |
| ); | |
| testUsingContext( | |
| 'Symlink ERROR_ACCESS_DENIED failures show developers paths that were used', | |
| () async { | |
| final flutterProject = FakeFlutterProject() | |
| ..directory = globals.fs.currentDirectory.childDirectory('app'); | |
| final Directory windowsManagedDirectory = flutterProject.directory | |
| .childDirectory('windows') | |
| .childDirectory('flutter'); | |
| final windowsProject = FakeWindowsProject() | |
| ..managedDirectory = windowsManagedDirectory | |
| ..pluginSymlinkDirectory = windowsManagedDirectory | |
| .childDirectory('ephemeral') | |
| .childDirectory('.plugin_symlinks') | |
| ..exists = true; | |
| final File dependenciesFile = flutterProject.directory.childFile( | |
| '.flutter-plugins-dependencies', | |
| ); | |
| flutterProject | |
| ..flutterPluginsDependenciesFile = dependenciesFile | |
| ..windows = windowsProject; | |
| writePackageConfigFiles(directory: flutterProject.directory, mainLibName: 'my_app'); | |
| const dependenciesFileContents = r''' | |
| { | |
| "plugins": { | |
| "windows": [ | |
| { | |
| "name": "some_plugin", | |
| "path": "C:\\some_plugin" | |
| } | |
| ] | |
| } | |
| } | |
| '''; | |
| dependenciesFile.writeAsStringSync(dependenciesFileContents); | |
| const expectedMessage = | |
| 'ERROR_ACCESS_DENIED file system exception thrown while trying to ' | |
| r'create a symlink from C:\some_plugin to ' | |
| r'C:\app\windows\flutter\ephemeral\.plugin_symlinks\some_plugin'; | |
| expect( | |
| () => createPluginSymlinks( | |
| flutterProject, | |
| featureFlagsOverride: TestFeatureFlags(isWindowsEnabled: true), | |
| ), | |
| throwsToolExit(message: expectedMessage), | |
| ); | |
| }, | |
| overrides: <Type, Generator>{ | |
| FileSystem: () { | |
| final handle = FileExceptionHandler(); | |
| final fileSystem = ErrorHandlingFileSystem( | |
| platform: FakePlatform(), | |
| delegate: MemoryFileSystem.test( | |
| style: FileSystemStyle.windows, | |
| opHandle: handle.opHandle, | |
| ), | |
| ); | |
| const pluginSymlinkPath = | |
| r'C:\app\windows\flutter\ephemeral\.plugin_symlinks\some_plugin'; | |
| handle.addError( | |
| fileSystem.link(pluginSymlinkPath), | |
| FileSystemOp.create, | |
| const FileSystemException('', '', OSError('', 5)), | |
| ); | |
| return fileSystem; | |
| }, | |
| Platform: () => FakePlatform(operatingSystem: 'windows'), | |
| ProcessManager: () => FakeProcessManager.empty(), | |
| }, | |
| ); | |
| testWithoutContext( | |
| 'Symlink failures instruct developers to run as administrator on older versions of Windows', | |
| () async { | |
| final Platform platform = FakePlatform(operatingSystem: 'windows'); | |
| final os = FakeOperatingSystemUtils('Microsoft Windows [Version 10.0.14393]'); | |
| const e = FileSystemException('', '', OSError('', 1314)); | |
| expect( | |
| () => handleSymlinkException( | |
| e, | |
| platform: platform, | |
| os: os, | |
| source: pubCachePath, | |
| destination: ephemeralPackagePath, | |
| ), | |
| throwsToolExit(message: 'administrator'), | |
| ); | |
| }, | |
| ); | |
| testWithoutContext( | |
| 'Symlink failures instruct developers to have their project on the same drive as their SDK', | |
| () async { | |
| final Platform platform = FakePlatform(operatingSystem: 'windows'); | |
| final os = FakeOperatingSystemUtils('Microsoft Windows [Version 10.0.14972]'); | |
| const e = FileSystemException('', '', OSError('', 1)); | |
| expect( | |
| () => handleSymlinkException( | |
| e, | |
| platform: platform, | |
| os: os, | |
| source: pubCachePath, | |
| destination: ephemeralPackagePath, | |
| ), | |
| throwsToolExit( | |
| message: 'Try moving your Flutter project to the same drive as your Flutter SDK', | |
| ), | |
| ); | |
| }, | |
| ); | |
| testWithoutContext('Symlink failures only give instructions for specific errors', () async { | |
| final Platform platform = FakePlatform(operatingSystem: 'windows'); | |
| final os = FakeOperatingSystemUtils('Microsoft Windows [Version 10.0.14393]'); | |
| const e = FileSystemException('', '', OSError('', 999)); | |
| expect( | |
| () => handleSymlinkException( | |
| e, | |
| platform: platform, | |
| os: os, | |
| source: pubCachePath, | |
| destination: ephemeralPackagePath, | |
| ), | |
| returnsNormally, | |
| ); | |
| }); | |
| group('injectPlugins in release mode', () { | |
| const testPluginName = 'test_plugin'; | |
| // Fake pub to override dev dependencies of flutterProject. | |
| testUsingContext( | |
| 'excludes dev dependencies from Android plugin registrant', | |
| () async { | |
| final Directory pluginDir = createPlugin( | |
| name: testPluginName, | |
| platforms: const <String, _PluginPlatformInfo>{ | |
| 'android': _PluginPlatformInfo(pluginClass: 'Foo', androidPackage: 'bar.foo'), | |
| }, | |
| isDevDependency: true, | |
| ); | |
| // injectPlugins will fail if main native class not found in expected spot, so add | |
| // it first. | |
| pluginDir | |
| .childDirectory('android') | |
| .childDirectory('src') | |
| .childDirectory('main') | |
| .childDirectory('java') | |
| .childDirectory('bar') | |
| .childDirectory('foo') | |
| .childFile('Foo.java') | |
| ..createSync(recursive: true) | |
| ..writeAsStringSync('import io.flutter.embedding.engine.plugins.FlutterPlugin;'); | |
| // Test non-release mode. | |
| await injectPlugins(flutterProject, androidPlatform: true, releaseMode: false); | |
| final File generatedPluginRegistrant = | |
| flutterProject.android.generatedPluginRegistrantFile; | |
| expect(generatedPluginRegistrant, exists); | |
| expect(generatedPluginRegistrant.readAsStringSync(), contains('bar.foo.Foo')); | |
| // Test release mode. | |
| await injectPlugins(flutterProject, androidPlatform: true, releaseMode: true); | |
| expect(generatedPluginRegistrant, exists); | |
| expect(generatedPluginRegistrant.readAsStringSync(), isNot(contains('bar.foo.Foo'))); | |
| }, | |
| overrides: <Type, Generator>{ | |
| FileSystem: () => fs, | |
| ProcessManager: () => FakeProcessManager.any(), | |
| Pub: () => const ThrowingPub(), | |
| }, | |
| ); | |
| testUsingContext( | |
| 'includes dev dependencies from iOS plugin registrant', | |
| () async { | |
| createPlugin( | |
| name: testPluginName, | |
| platforms: const <String, _PluginPlatformInfo>{ | |
| 'ios': _PluginPlatformInfo(pluginClass: 'Foo'), | |
| }, | |
| isDevDependency: true, | |
| ); | |
| final dependencyManagement = FakeDarwinDependencyManagement(); | |
| const devDepImport = '#import <$testPluginName/Foo.h>'; | |
| // Test non-release mode. | |
| await injectPlugins( | |
| flutterProject, | |
| iosPlatform: true, | |
| darwinDependencyManagement: dependencyManagement, | |
| releaseMode: false, | |
| ); | |
| final File generatedPluginRegistrantImpl = | |
| flutterProject.ios.pluginRegistrantImplementation; | |
| expect(generatedPluginRegistrantImpl, exists); | |
| expect(generatedPluginRegistrantImpl.readAsStringSync(), contains(devDepImport)); | |
| // Test release mode. | |
| await injectPlugins( | |
| flutterProject, | |
| iosPlatform: true, | |
| darwinDependencyManagement: dependencyManagement, | |
| releaseMode: true, | |
| ); | |
| expect(generatedPluginRegistrantImpl, exists); | |
| expect(generatedPluginRegistrantImpl.readAsStringSync(), contains(devDepImport)); | |
| }, | |
| overrides: <Type, Generator>{ | |
| FileSystem: () => fs, | |
| ProcessManager: () => FakeProcessManager.any(), | |
| Pub: () => const ThrowingPub(), | |
| }, | |
| ); | |
| testUsingContext( | |
| 'excludes dev dependencies from Linux plugin registrant', | |
| () async { | |
| createPlugin( | |
| name: testPluginName, | |
| platforms: const <String, _PluginPlatformInfo>{ | |
| 'linux': _PluginPlatformInfo(pluginClass: 'Foo'), | |
| }, | |
| isDevDependency: true, | |
| ); | |
| const expectedDevDepImport = '#include <$testPluginName/foo.h>'; | |
| // Test non-release mode. | |
| await injectPlugins(flutterProject, linuxPlatform: true, releaseMode: false); | |
| final File generatedPluginRegistrant = flutterProject.linux.managedDirectory.childFile( | |
| 'generated_plugin_registrant.cc', | |
| ); | |
| expect(generatedPluginRegistrant, exists); | |
| expect(generatedPluginRegistrant.readAsStringSync(), contains(expectedDevDepImport)); | |
| // Test release mode. | |
| await injectPlugins(flutterProject, linuxPlatform: true, releaseMode: true); | |
| expect(generatedPluginRegistrant, exists); | |
| expect( | |
| generatedPluginRegistrant.readAsStringSync(), | |
| isNot(contains(expectedDevDepImport)), | |
| ); | |
| }, | |
| overrides: <Type, Generator>{ | |
| FileSystem: () => fs, | |
| ProcessManager: () => FakeProcessManager.any(), | |
| Pub: () => const ThrowingPub(), | |
| }, | |
| ); | |
| testUsingContext( | |
| 'includes dev dependencies from MacOS plugin registrant', | |
| () async { | |
| createPlugin( | |
| name: testPluginName, | |
| platforms: const <String, _PluginPlatformInfo>{ | |
| 'macos': _PluginPlatformInfo(pluginClass: 'Foo'), | |
| }, | |
| isDevDependency: true, | |
| ); | |
| final dependencyManagement = FakeDarwinDependencyManagement(); | |
| const expectedDevDepRegistration = 'Foo.register'; | |
| // Test non-release mode. | |
| await injectPlugins( | |
| flutterProject, | |
| macOSPlatform: true, | |
| darwinDependencyManagement: dependencyManagement, | |
| releaseMode: false, | |
| ); | |
| final File generatedPluginRegistrant = flutterProject.macos.managedDirectory.childFile( | |
| 'GeneratedPluginRegistrant.swift', | |
| ); | |
| expect(generatedPluginRegistrant, exists); | |
| expect( | |
| generatedPluginRegistrant.readAsStringSync(), | |
| contains(expectedDevDepRegistration), | |
| ); | |
| // Test release mode. | |
| await injectPlugins( | |
| flutterProject, | |
| macOSPlatform: true, | |
| darwinDependencyManagement: dependencyManagement, | |
| releaseMode: true, | |
| ); | |
| expect(generatedPluginRegistrant, exists); | |
| expect( | |
| generatedPluginRegistrant.readAsStringSync(), | |
| contains(expectedDevDepRegistration), | |
| ); | |
| }, | |
| overrides: <Type, Generator>{ | |
| FileSystem: () => fs, | |
| ProcessManager: () => FakeProcessManager.any(), | |
| Pub: () => const ThrowingPub(), | |
| }, | |
| ); | |
| testUsingContext( | |
| 'excludes dev dependencies from Windows plugin registrant', | |
| () async { | |
| final Directory pluginDir = createPlugin( | |
| name: testPluginName, | |
| platforms: const <String, _PluginPlatformInfo>{ | |
| 'windows': _PluginPlatformInfo(pluginClass: 'Foo'), | |
| }, | |
| isDevDependency: true, | |
| ); | |
| const expectedDevDepRegistration = '#include <$testPluginName/foo.h>'; | |
| writePackageConfigFiles( | |
| directory: flutterProject.directory, | |
| mainLibName: 'my_app', | |
| packages: <String, String>{testPluginName: pluginDir.path}, | |
| devDependencies: <String>[testPluginName], | |
| ); | |
| // Test non-release mode. | |
| await injectPlugins(flutterProject, windowsPlatform: true, releaseMode: false); | |
| final File generatedPluginRegistrantImpl = flutterProject.windows.managedDirectory | |
| .childFile('generated_plugin_registrant.cc'); | |
| expect(generatedPluginRegistrantImpl, exists); | |
| expect( | |
| generatedPluginRegistrantImpl.readAsStringSync(), | |
| contains(expectedDevDepRegistration), | |
| ); | |
| // Test release mode. | |
| await injectPlugins(flutterProject, windowsPlatform: true, releaseMode: true); | |
| expect(generatedPluginRegistrantImpl, exists); | |
| expect( | |
| generatedPluginRegistrantImpl.readAsStringSync(), | |
| isNot(contains(expectedDevDepRegistration)), | |
| ); | |
| }, | |
| overrides: <Type, Generator>{ | |
| FileSystem: () => fs, | |
| ProcessManager: () => FakeProcessManager.any(), | |
| Pub: () => const ThrowingPub(), | |
| }, | |
| ); | |
| }); | |
| }); | |
| testUsingContext( | |
| 'exits tool when deleting .plugin_symlinks fails', | |
| () async { | |
| final flutterProject = FakeFlutterProject() | |
| ..directory = globals.fs.currentDirectory.childDirectory('app'); | |
| final flutterManifest = FakeFlutterManifest(); | |
| final Directory windowsManagedDirectory = flutterProject.directory | |
| .childDirectory('windows') | |
| .childDirectory('flutter'); | |
| final windowsProject = FakeWindowsProject() | |
| ..managedDirectory = windowsManagedDirectory | |
| ..cmakeFile = windowsManagedDirectory.parent.childFile('CMakeLists.txt') | |
| ..generatedPluginCmakeFile = windowsManagedDirectory.childFile('generated_plugins.mk') | |
| ..pluginSymlinkDirectory = windowsManagedDirectory | |
| .childDirectory('ephemeral') | |
| .childDirectory('.plugin_symlinks') | |
| ..exists = true; | |
| flutterProject | |
| ..manifest = flutterManifest | |
| ..flutterPluginsDependenciesFile = flutterProject.directory.childFile( | |
| '.flutter-plugins-dependencies', | |
| ) | |
| ..windows = windowsProject; | |
| writePackageConfigFiles(directory: flutterProject.directory, mainLibName: 'my_app'); | |
| createPluginSymlinks( | |
| flutterProject, | |
| force: true, | |
| featureFlagsOverride: TestFeatureFlags(isWindowsEnabled: true), | |
| ); | |
| expect( | |
| () => createPluginSymlinks( | |
| flutterProject, | |
| force: true, | |
| featureFlagsOverride: TestFeatureFlags(isWindowsEnabled: true), | |
| ), | |
| throwsToolExit( | |
| message: RegExp( | |
| 'Unable to delete file or directory at ' | |
| r'"C:\\app\\windows\\flutter\\ephemeral\\\.plugin_symlinks"', | |
| ), | |
| ), | |
| ); | |
| }, | |
| overrides: <Type, Generator>{ | |
| FileSystem: () { | |
| final handle = FileExceptionHandler(); | |
| final fileSystem = ErrorHandlingFileSystem( | |
| platform: FakePlatform(), | |
| delegate: MemoryFileSystem.test( | |
| style: FileSystemStyle.windows, | |
| opHandle: handle.opHandle, | |
| ), | |
| ); | |
| const symlinkDirectoryPath = r'C:\app\windows\flutter\ephemeral\.plugin_symlinks'; | |
| handle.addError( | |
| fileSystem.directory(symlinkDirectoryPath), | |
| FileSystemOp.delete, | |
| const PathNotFoundException( | |
| symlinkDirectoryPath, | |
| OSError('The system cannot find the path specified.', 3), | |
| ), | |
| ); | |
| return fileSystem; | |
| }, | |
| ProcessManager: () => FakeProcessManager.empty(), | |
| }, | |
| ); | |
| group('resolvePluginImplementationsForPlatform', () { | |
| testWithoutContext('filters plugins based on platformKey', () { | |
| final iosPlugin = Plugin( | |
| name: 'ios_plugin', | |
| path: '', | |
| defaultPackagePlatforms: const <String, String>{}, | |
| pluginDartClassPlatforms: const <String, DartPluginClassAndFilePair>{}, | |
| platforms: const <String, PluginPlatform>{ | |
| IOSPlugin.kConfigKey: IOSPlugin(name: 'ios_plugin', classPrefix: ''), | |
| }, | |
| dependencies: const <String>[], | |
| isDirectDependency: true, | |
| isDevDependency: false, | |
| ); | |
| final macosPlugin = Plugin( | |
| name: 'macos_plugin', | |
| path: '', | |
| defaultPackagePlatforms: const <String, String>{}, | |
| pluginDartClassPlatforms: const <String, DartPluginClassAndFilePair>{}, | |
| platforms: const <String, PluginPlatform>{ | |
| MacOSPlugin.kConfigKey: MacOSPlugin(name: 'macos_plugin'), | |
| }, | |
| dependencies: const <String>[], | |
| isDirectDependency: true, | |
| isDevDependency: false, | |
| ); | |
| final plugins = <Plugin>[iosPlugin, macosPlugin]; | |
| final List<Plugin> iosResolved = resolvePluginImplementationsForPlatform( | |
| plugins, | |
| IOSPlugin.kConfigKey, | |
| ); | |
| expect(iosResolved, contains(iosPlugin)); | |
| expect(iosResolved, isNot(contains(macosPlugin))); | |
| final List<Plugin> macosResolved = resolvePluginImplementationsForPlatform( | |
| plugins, | |
| MacOSPlugin.kConfigKey, | |
| ); | |
| expect(macosResolved, contains(macosPlugin)); | |
| expect(macosResolved, isNot(contains(iosPlugin))); | |
| }); | |
| testWithoutContext('ensures the correct federated implementation is selected', () { | |
| final fs = MemoryFileSystem.test(); | |
| final appFacingPlugin = Plugin( | |
| name: 'foo', | |
| path: '', | |
| defaultPackagePlatforms: const <String, String>{IOSPlugin.kConfigKey: 'foo_ios'}, | |
| pluginDartClassPlatforms: const <String, DartPluginClassAndFilePair>{}, | |
| platforms: const <String, PluginPlatform>{}, | |
| dependencies: const <String>[], | |
| isDirectDependency: true, | |
| isDevDependency: false, | |
| ); | |
| final iosPlatformPlugin = Plugin( | |
| name: 'foo_ios', | |
| path: '', | |
| implementsPackage: 'foo', | |
| defaultPackagePlatforms: const <String, String>{}, | |
| pluginDartClassPlatforms: const <String, DartPluginClassAndFilePair>{}, | |
| platforms: const <String, PluginPlatform>{ | |
| IOSPlugin.kConfigKey: IOSPlugin(name: 'foo_ios', classPrefix: ''), | |
| }, | |
| dependencies: const <String>[], | |
| isDirectDependency: false, | |
| isDevDependency: false, | |
| ); | |
| final overrideIosPlatformPlugin = Plugin( | |
| name: 'foo_ios_override', | |
| path: '', | |
| implementsPackage: 'foo', | |
| defaultPackagePlatforms: const <String, String>{}, | |
| pluginDartClassPlatforms: const <String, DartPluginClassAndFilePair>{}, | |
| platforms: const <String, PluginPlatform>{ | |
| IOSPlugin.kConfigKey: IOSPlugin(name: 'foo_ios_override', classPrefix: ''), | |
| }, | |
| dependencies: const <String>[], | |
| isDirectDependency: true, | |
| isDevDependency: false, | |
| ); | |
| final androidPlatformPlugin = Plugin( | |
| name: 'foo_android', | |
| path: '', | |
| implementsPackage: 'foo', | |
| defaultPackagePlatforms: const <String, String>{}, | |
| pluginDartClassPlatforms: const <String, DartPluginClassAndFilePair>{}, | |
| platforms: <String, PluginPlatform>{ | |
| AndroidPlugin.kConfigKey: AndroidPlugin( | |
| name: 'foo_android', | |
| package: 'com.example', | |
| pluginPath: '', | |
| fileSystem: fs, | |
| ), | |
| }, | |
| dependencies: const <String>[], | |
| isDirectDependency: false, | |
| isDevDependency: false, | |
| ); | |
| final plugins = <Plugin>[ | |
| appFacingPlugin, | |
| iosPlatformPlugin, | |
| androidPlatformPlugin, | |
| overrideIosPlatformPlugin, | |
| ]; | |
| final List<Plugin> iosResolved = resolvePluginImplementationsForPlatform( | |
| plugins, | |
| IOSPlugin.kConfigKey, | |
| ); | |
| expect(iosResolved, contains(overrideIosPlatformPlugin)); | |
| expect(iosResolved, isNot(contains(iosPlatformPlugin))); | |
| expect(iosResolved, isNot(contains(appFacingPlugin))); | |
| expect(iosResolved, isNot(contains(androidPlatformPlugin))); | |
| }); | |
| testUsingContext('respects quiet parameter', () { | |
| final appFacingPluginNoDefaultPackage = Plugin( | |
| name: 'foo', | |
| path: '', | |
| defaultPackagePlatforms: const <String, String>{IOSPlugin.kConfigKey: 'foo_ios'}, | |
| pluginDartClassPlatforms: const <String, DartPluginClassAndFilePair>{}, | |
| platforms: const <String, PluginPlatform>{}, | |
| dependencies: const <String>[], | |
| isDirectDependency: true, | |
| isDevDependency: false, | |
| ); | |
| final appFacingPluginWithNoInlineImplDefault = Plugin( | |
| name: 'bar', | |
| path: '', | |
| defaultPackagePlatforms: const <String, String>{IOSPlugin.kConfigKey: 'bar_ios'}, | |
| pluginDartClassPlatforms: const <String, DartPluginClassAndFilePair>{}, | |
| platforms: const <String, PluginPlatform>{}, | |
| dependencies: const <String>[], | |
| isDirectDependency: true, | |
| isDevDependency: false, | |
| ); | |
| final barIosWithNoInlineImpl = Plugin( | |
| name: 'bar_ios', | |
| path: '', | |
| defaultPackagePlatforms: const <String, String>{}, | |
| pluginDartClassPlatforms: const <String, DartPluginClassAndFilePair>{}, | |
| platforms: const <String, PluginPlatform>{}, | |
| dependencies: const <String>[], | |
| isDirectDependency: false, | |
| isDevDependency: false, | |
| ); | |
| final invalidPlugin = Plugin( | |
| name: 'invalid_plugin', | |
| path: '', | |
| implementsPackage: 'some_package', | |
| defaultPackagePlatforms: const <String, String>{IOSPlugin.kConfigKey: 'some_default'}, | |
| pluginDartClassPlatforms: const <String, DartPluginClassAndFilePair>{}, | |
| platforms: const <String, PluginPlatform>{}, | |
| dependencies: const <String>[], | |
| isDirectDependency: true, | |
| isDevDependency: false, | |
| ); | |
| final directImpl1 = Plugin( | |
| name: 'impl1', | |
| path: '', | |
| implementsPackage: 'conflict_foo', | |
| defaultPackagePlatforms: const <String, String>{}, | |
| pluginDartClassPlatforms: const <String, DartPluginClassAndFilePair>{}, | |
| platforms: const <String, PluginPlatform>{ | |
| IOSPlugin.kConfigKey: IOSPlugin(name: 'impl1', classPrefix: ''), | |
| }, | |
| dependencies: const <String>[], | |
| isDirectDependency: true, | |
| isDevDependency: false, | |
| ); | |
| final directImpl2 = Plugin( | |
| name: 'impl2', | |
| path: '', | |
| implementsPackage: 'conflict_foo', | |
| defaultPackagePlatforms: const <String, String>{}, | |
| pluginDartClassPlatforms: const <String, DartPluginClassAndFilePair>{}, | |
| platforms: const <String, PluginPlatform>{ | |
| IOSPlugin.kConfigKey: IOSPlugin(name: 'impl2', classPrefix: ''), | |
| }, | |
| dependencies: const <String>[], | |
| isDirectDependency: true, | |
| isDevDependency: false, | |
| ); | |
| final plugins = <Plugin>[ | |
| appFacingPluginNoDefaultPackage, | |
| appFacingPluginWithNoInlineImplDefault, | |
| barIosWithNoInlineImpl, | |
| invalidPlugin, | |
| directImpl1, | |
| directImpl2, | |
| ]; | |
| expect( | |
| () => resolvePluginImplementationsForPlatform(plugins, IOSPlugin.kConfigKey, quiet: true), | |
| throwsToolExit(), | |
| ); | |
| expect(testLogger.warningText, isEmpty); | |
| expect(testLogger.errorText, isEmpty); | |
| expect( | |
| () => resolvePluginImplementationsForPlatform(plugins, IOSPlugin.kConfigKey), | |
| throwsToolExit(), | |
| ); | |
| expect( | |
| testLogger.warningText, | |
| contains('references foo_ios:ios as the default plugin, but the package does not exist'), | |
| ); | |
| expect( | |
| testLogger.warningText, | |
| contains( | |
| 'references bar_ios:ios as the default plugin, but it does not provide an inline implementation', | |
| ), | |
| ); | |
| expect( | |
| testLogger.errorText, | |
| contains( | |
| 'invalid_plugin:ios provides an implementation for some_package and also references a default implementation', | |
| ), | |
| ); | |
| expect( | |
| testLogger.errorText, | |
| contains('conflict_foo:ios has conflicting direct dependency implementations'), | |
| ); | |
| }); | |
| }); | |
| group('buildPubspecCache', () { | |
| late MemoryFileSystem fs; | |
| setUp(() { | |
| fs = MemoryFileSystem.test(); | |
| }); | |
| PackageConfig makePackageConfig(List<String> names) { | |
| return PackageConfig( | |
| names | |
| .map( | |
| (String name) => Package( | |
| name, | |
| Uri.parse('file:///pkgs/$name/'), | |
| packageUriRoot: Uri.parse('file:///pkgs/$name/lib/'), | |
| ), | |
| ) | |
| .toList(), | |
| ); | |
| } | |
| test('populates an entry for every package in PackageConfig', () async { | |
| final PackageConfig config = makePackageConfig(<String>['pkg_a', 'pkg_b', 'pkg_c']); | |
| for (final Package package in config.packages) { | |
| fs.file(package.root.resolve('pubspec.yaml')) | |
| ..createSync(recursive: true) | |
| ..writeAsStringSync('name: ${package.name}\n'); | |
| } | |
| final PubspecCache cache = await buildPubspecCache(config, fileSystem: fs); | |
| expect(cache.length, 3); | |
| for (final Package package in config.packages) { | |
| expect(cache.containsKey(package.root.toString()), isTrue); | |
| } | |
| }); | |
| test('parses pubspec YAML content correctly', () async { | |
| final PackageConfig config = makePackageConfig(<String>['my_plugin']); | |
| final Package package = config.packages.first; | |
| fs.file(package.root.resolve('pubspec.yaml')) | |
| ..createSync(recursive: true) | |
| ..writeAsStringSync(''' | |
| name: my_plugin | |
| flutter: | |
| plugin: | |
| platforms: | |
| android: | |
| package: com.example | |
| pluginClass: MyPlugin | |
| '''); | |
| final PubspecCache cache = await buildPubspecCache(config, fileSystem: fs); | |
| final YamlMap? yaml = cache[package.root.toString()]; | |
| expect(yaml, isNotNull); | |
| expect(yaml!['name'], 'my_plugin'); | |
| expect(yaml['flutter'], isNotNull); | |
| }); | |
| test('stores null for packages with no pubspec.yaml', () async { | |
| final PackageConfig config = makePackageConfig(<String>['has_pubspec', 'no_pubspec']); | |
| final Package withPubspec = config.packages.first; | |
| fs.file(withPubspec.root.resolve('pubspec.yaml')) | |
| ..createSync(recursive: true) | |
| ..writeAsStringSync('name: has_pubspec\n'); | |
| final PubspecCache cache = await buildPubspecCache(config, fileSystem: fs); | |
| expect(cache[withPubspec.root.toString()], isNotNull); | |
| final Package withoutPubspec = config.packages.last; | |
| expect(cache.containsKey(withoutPubspec.root.toString()), isTrue); | |
| expect(cache[withoutPubspec.root.toString()], isNull); | |
| }); | |
| test('cached data survives deletion of source files', () async { | |
| final PackageConfig config = makePackageConfig(<String>['pkg_a', 'pkg_b']); | |
| for (final Package package in config.packages) { | |
| fs.file(package.root.resolve('pubspec.yaml')) | |
| ..createSync(recursive: true) | |
| ..writeAsStringSync('name: ${package.name}\n'); | |
| } | |
| final PubspecCache cache = await buildPubspecCache(config, fileSystem: fs); | |
| for (final Package package in config.packages) { | |
| fs.file(package.root.resolve('pubspec.yaml')).deleteSync(); | |
| } | |
| for (final Package package in config.packages) { | |
| expect(cache[package.root.toString()], isNotNull); | |
| } | |
| final PubspecCache freshCache = await buildPubspecCache(config, fileSystem: fs); | |
| for (final Package package in config.packages) { | |
| expect(freshCache[package.root.toString()], isNull); | |
| } | |
| }); | |
| test( | |
| 'packages absent from PackageConfig have no entry, distinguishing them from packages with missing pubspec.yaml', | |
| () async { | |
| final PackageConfig config = makePackageConfig(<String>['in_resolution']); | |
| fs.file(config.packages.first.root.resolve('pubspec.yaml')) | |
| ..createSync(recursive: true) | |
| ..writeAsStringSync('name: in_resolution\n'); | |
| final PubspecCache cache = await buildPubspecCache(config, fileSystem: fs); | |
| // Package in resolution with pubspec | |
| // this means that the key is present and the value is non-null. | |
| expect(cache.containsKey('file:///pkgs/in_resolution/'), isTrue); | |
| expect(cache['file:///pkgs/in_resolution/'], isNotNull); | |
| // Package NOT in resolution (e.g. example-only dep) | |
| // this means that the key is absent entirely. | |
| // containsKey must return false so callers can fall back to disk reads. | |
| expect(cache.containsKey('file:///pkgs/example_only_plugin/'), isFalse); | |
| }, | |
| ); | |
| }); | |
| } | |
| class FakeFlutterManifest extends Fake implements FlutterManifest { | |
| @override | |
| late Set<String> dependencies = <String>{}; | |
| @override | |
| late String appName; | |
| @override | |
| YamlMap toYaml() => YamlMap.wrap(<String, String>{}); | |
| } | |
| class FakeXcodeProjectInterpreter extends Fake implements XcodeProjectInterpreter { | |
| @override | |
| bool get isInstalled => false; | |
| } | |
| class FakeFlutterProject extends Fake implements FlutterProject { | |
| @override | |
| bool isModule = false; | |
| @override | |
| late FlutterManifest manifest; | |
| @override | |
| late Directory directory; | |
| @override | |
| late File flutterPluginsDependenciesFile; | |
| @override | |
| late IosProject ios; | |
| @override | |
| late AndroidProject android; | |
| @override | |
| late WebProject web; | |
| @override | |
| late MacOSProject macos; | |
| @override | |
| late LinuxProject linux; | |
| @override | |
| late WindowsProject windows; | |
| @override | |
| File get packageConfig => directory.childDirectory('.dart_tool').childFile('package_config.json'); | |
| } | |
| class FakeMacOSProject extends Fake implements MacOSProject { | |
| @override | |
| String pluginConfigKey = 'macos'; | |
| bool exists = false; | |
| @override | |
| late File podfile; | |
| @override | |
| late File podManifestLock; | |
| @override | |
| bool usesSwiftPackageManager = false; | |
| @override | |
| late Directory managedDirectory; | |
| @override | |
| File get pluginRegistrantImplementation => | |
| managedDirectory.childFile('GeneratedPluginRegistrant.swift'); | |
| @override | |
| bool existsSync() => exists; | |
| } | |
| class FakeIosProject extends Fake implements IosProject { | |
| @override | |
| String pluginConfigKey = 'ios'; | |
| bool testExists = false; | |
| @override | |
| bool existsSync() => testExists; | |
| @override | |
| bool get exists => testExists; | |
| @override | |
| late Directory pluginRegistrantHost; | |
| @override | |
| File get pluginRegistrantHeader => pluginRegistrantHost.childFile('GeneratedPluginRegistrant.h'); | |
| @override | |
| File get pluginRegistrantImplementation => | |
| pluginRegistrantHost.childFile('GeneratedPluginRegistrant.m'); | |
| @override | |
| late File podfile; | |
| @override | |
| late File podManifestLock; | |
| @override | |
| bool usesSwiftPackageManager = false; | |
| } | |
| class FakeAndroidProject extends Fake implements AndroidProject { | |
| @override | |
| String pluginConfigKey = 'android'; | |
| bool exists = false; | |
| @override | |
| late Directory pluginRegistrantHost; | |
| @override | |
| late Directory hostAppGradleRoot; | |
| @override | |
| late File appManifestFile; | |
| late AndroidEmbeddingVersion embeddingVersion; | |
| @override | |
| bool existsSync() => exists; | |
| @override | |
| AndroidEmbeddingVersion getEmbeddingVersion() { | |
| return embeddingVersion; | |
| } | |
| @override | |
| AndroidEmbeddingVersionResult computeEmbeddingVersion() { | |
| return AndroidEmbeddingVersionResult(embeddingVersion, 'reasons for version'); | |
| } | |
| @override | |
| File get generatedPluginRegistrantFile => hostAppGradleRoot | |
| .childDirectory('app') | |
| .childDirectory('src') | |
| .childDirectory('main') | |
| .childDirectory('java') | |
| .childDirectory('io') | |
| .childDirectory('flutter') | |
| .childDirectory('plugins') | |
| .childFile('GeneratedPluginRegistrant.java'); | |
| } | |
| class FakeWebProject extends Fake implements WebProject { | |
| @override | |
| String pluginConfigKey = 'web'; | |
| @override | |
| late Directory libDirectory; | |
| bool exists = false; | |
| @override | |
| bool existsSync() => exists; | |
| } | |
| class FakeWindowsProject extends Fake implements WindowsProject { | |
| @override | |
| String pluginConfigKey = 'windows'; | |
| @override | |
| late Directory managedDirectory; | |
| @override | |
| late Directory ephemeralDirectory; | |
| @override | |
| late Directory pluginSymlinkDirectory; | |
| @override | |
| late File cmakeFile; | |
| @override | |
| late File generatedPluginCmakeFile; | |
| bool exists = false; | |
| @override | |
| bool existsSync() => exists; | |
| } | |
| class FakeLinuxProject extends Fake implements LinuxProject { | |
| @override | |
| String pluginConfigKey = 'linux'; | |
| @override | |
| late Directory managedDirectory; | |
| @override | |
| late Directory ephemeralDirectory; | |
| @override | |
| late Directory pluginSymlinkDirectory; | |
| @override | |
| late File cmakeFile; | |
| @override | |
| late File generatedPluginCmakeFile; | |
| bool exists = false; | |
| @override | |
| bool existsSync() => exists; | |
| } | |
| class FakeOperatingSystemUtils extends Fake implements OperatingSystemUtils { | |
| FakeOperatingSystemUtils(this.name); | |
| @override | |
| final String name; | |
| } | |
| class FakeSystemClock extends Fake implements SystemClock { | |
| late DateTime currentTime; | |
| @override | |
| DateTime now() { | |
| return currentTime; | |
| } | |
| } | |
| class FakeDarwinDependencyManagement extends Fake implements DarwinDependencyManagement { | |
| List<FlutterDarwinPlatform> setupPlatforms = <FlutterDarwinPlatform>[]; | |
| @override | |
| Future<void> setUp({ | |
| required FlutterDarwinPlatform platform, | |
| required List<Plugin> plugins, | |
| }) async { | |
| setupPlatforms.add(platform); | |
| } | |
| } | |
| /// A fake of [CocoaPods] that writes calls to [invalidatePodInstallOutput] to [capturedInvocations]. | |
| final class FakeCocoaPodsCapturesInvalidate extends Fake implements CocoaPods { | |
| final capturedInvocations = <XcodeBasedProject>[]; | |
| @override | |
| void invalidatePodInstallOutput(XcodeBasedProject xcodeProject) { | |
| capturedInvocations.add(xcodeProject); | |
| } | |
| } |