From 5eb394e08492fadd66581c4c0af45ec657d4495a Mon Sep 17 00:00:00 2001 From: Francisco Magdaleno Date: Thu, 16 Jan 2020 12:42:05 -0800 Subject: [PATCH] [flutter_tools] Removes the need of a no-op plugin implementations (#48614) --- .../bin/tasks/plugin_dependencies_test.dart | 49 +++-- packages/flutter_tools/lib/src/plugins.dart | 179 ++++++++++++++---- packages/flutter_tools/lib/src/project.dart | 45 ++++- packages/flutter_tools/lib/src/version.dart | 2 + .../test/general.shard/plugins_test.dart | 169 ++++++++++++++--- 5 files changed, 354 insertions(+), 90 deletions(-) diff --git a/dev/devicelab/bin/tasks/plugin_dependencies_test.dart b/dev/devicelab/bin/tasks/plugin_dependencies_test.dart index f3714f2669..6e32c2d65e 100644 --- a/dev/devicelab/bin/tasks/plugin_dependencies_test.dart +++ b/dev/devicelab/bin/tasks/plugin_dependencies_test.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'dart:async'; +import 'dart:convert'; import 'dart:io'; import 'package:flutter_devicelab/framework/framework.dart'; @@ -74,10 +75,6 @@ Future main() async { ); }); - // https://github.com/flutter/flutter/issues/46898 - // https://github.com/flutter/flutter/issues/39657 - File(path.join(pluginCDirectory.path, 'android', 'build.gradle')).deleteSync(); - final File pluginCpubspec = File(path.join(pluginCDirectory.path, 'pubspec.yaml')); await pluginCpubspec.writeAsString(''' name: plugin_c @@ -177,30 +174,32 @@ public class DummyPluginAClass { } final String flutterPluginsDependenciesFileContent = flutterPluginsDependenciesFile.readAsStringSync(); - const String kExpectedPluginsDependenciesContent = - '{' - '\"_info\":\"// This is a generated file; do not edit or check into version control.\",' - '\"dependencyGraph\":[' - '{' - '\"name\":\"plugin_a\",' - '\"dependencies\":[\"plugin_b\",\"plugin_c\"]' - '},' - '{' - '\"name\":\"plugin_b\",' - '\"dependencies\":[]' - '},' - '{' - '\"name\":\"plugin_c\",' - '\"dependencies\":[]' - '}' - ']' - '}'; - if (flutterPluginsDependenciesFileContent != kExpectedPluginsDependenciesContent) { + final Map jsonContent = json.decode(flutterPluginsDependenciesFileContent) as Map; + + // Verify the dependencyGraph object is valid. The rest of the contents of this file are not relevant to the + // dependency graph and are tested by unit tests. + final List dependencyGraph = jsonContent['dependencyGraph'] as List; + const String kExpectedPluginsDependenciesContent = + '[' + '{' + '\"name\":\"plugin_a\",' + '\"dependencies\":[\"plugin_b\",\"plugin_c\"]' + '},' + '{' + '\"name\":\"plugin_b\",' + '\"dependencies\":[]' + '},' + '{' + '\"name\":\"plugin_c\",' + '\"dependencies\":[]' + '}' + ']'; + final String graphString = json.encode(dependencyGraph); + if (graphString != kExpectedPluginsDependenciesContent) { return TaskResult.failure( 'Unexpected file content in ${flutterPluginsDependenciesFile.path}: ' - 'Found "$flutterPluginsDependenciesFileContent" instead of ' - '"$kExpectedPluginsDependenciesContent"' + 'Found "$graphString" instead of "$kExpectedPluginsDependenciesContent"' ); } diff --git a/packages/flutter_tools/lib/src/plugins.dart b/packages/flutter_tools/lib/src/plugins.dart index efee39fe55..628058f347 100644 --- a/packages/flutter_tools/lib/src/plugins.dart +++ b/packages/flutter_tools/lib/src/plugins.dart @@ -11,6 +11,7 @@ import 'package:yaml/yaml.dart'; import 'android/gradle.dart'; import 'base/common.dart'; import 'base/file_system.dart'; +import 'base/time.dart'; import 'convert.dart'; import 'dart/package_map.dart'; import 'features.dart'; @@ -18,6 +19,7 @@ import 'globals.dart' as globals; import 'macos/cocoapods.dart'; import 'platform_plugins.dart'; import 'project.dart'; +import 'version.dart'; void _renderTemplateToFile(String template, dynamic context, String filePath) { final String renderedTemplate = @@ -263,7 +265,7 @@ class Plugin { final Map platforms; } -Plugin _pluginFromPubspec(String name, Uri packageRoot) { +Plugin _pluginFromPackage(String name, Uri packageRoot) { final String pubspecPath = globals.fs.path.fromUri(packageRoot.resolve('pubspec.yaml')); if (!globals.fs.isFileSync(pubspecPath)) { return null; @@ -302,7 +304,7 @@ List findPlugins(FlutterProject project) { } packages.forEach((String name, Uri uri) { final Uri packageRoot = uri.resolve('..'); - final Plugin plugin = _pluginFromPubspec(name, packageRoot); + final Plugin plugin = _pluginFromPackage(name, packageRoot); if (plugin != null) { plugins.add(plugin); } @@ -310,55 +312,159 @@ List findPlugins(FlutterProject project) { return plugins; } -/// Writes the .flutter-plugins and .flutter-plugins-dependencies files based on the list of plugins. -/// If there aren't any plugins, then the files aren't written to disk. + /// Filters [plugins] to those supported by [platformKey]. + List> _filterPluginsByPlatform(Listplugins, String platformKey) { + final Iterable platformPlugins = plugins.where((Plugin p) { + return p.platforms.containsKey(platformKey); + }); + + final Set pluginNames = platformPlugins.map((Plugin plugin) => plugin.name).toSet(); + final List> list = >[]; + for (final Plugin plugin in platformPlugins) { + list.add({ + 'name': plugin.name, + 'path': fsUtils.escapePath(plugin.path), + 'dependencies': [...plugin.dependencies.where(pluginNames.contains)], + }); + } + return list; + } + +/// Writes the .flutter-plugins-dependencies file based on the list of plugins. +/// If there aren't any plugins, then the files aren't written to disk. The resulting +/// file looks something like this (order of keys is not guaranteed): +/// { +/// "info": "This is a generated file; do not edit or check into version control.", +/// "plugins": { +/// "ios": [ +/// { +/// "name": "test", +/// "path": "test_path", +/// "dependencies": [ +/// "plugin-a", +/// "plugin-b" +/// ] +/// } +/// ], +/// "android": [], +/// "macos": [], +/// "linux": [], +/// "windows": [], +/// "web": [] +/// }, +/// "dependencyGraph": [ +/// { +/// "name": "plugin-a", +/// "dependencies": [ +/// "plugin-b", +/// "plugin-c" +/// ] +/// }, +/// { +/// "name": "plugin-b", +/// "dependencies": [ +/// "plugin-c" +/// ] +/// }, +/// { +/// "name": "plugin-c", +/// "dependencies": [] +/// } +/// ], +/// "date_created": "1970-01-01 00:00:00.000", +/// "version": "0.0.0-unknown" +/// } /// -/// Finally, returns [true] if .flutter-plugins or .flutter-plugins-dependencies have changed, +/// +/// Finally, returns [true] if .flutter-plugins-dependencies has changed, /// otherwise returns [false]. bool _writeFlutterPluginsList(FlutterProject project, List plugins) { - final List directAppDependencies = []; - const String info = 'This is a generated file; do not edit or check into version control.'; - final StringBuffer flutterPluginsBuffer = StringBuffer('# $info\n'); - - final Set pluginNames = {}; - for (final Plugin plugin in plugins) { - pluginNames.add(plugin.name); + final File pluginsFile = project.flutterPluginsDependenciesFile; + if (plugins.isEmpty) { + if (pluginsFile.existsSync()) { + pluginsFile.deleteSync(); + return true; + } + return false; } + + final String iosKey = project.ios.pluginConfigKey; + final String androidKey = project.android.pluginConfigKey; + final String macosKey = project.macos.pluginConfigKey; + final String linuxKey = project.linux.pluginConfigKey; + final String windowsKey = project.windows.pluginConfigKey; + final String webKey = project.web.pluginConfigKey; + + final Map pluginsMap = {}; + pluginsMap[iosKey] = _filterPluginsByPlatform(plugins, iosKey); + pluginsMap[androidKey] = _filterPluginsByPlatform(plugins, androidKey); + pluginsMap[macosKey] = _filterPluginsByPlatform(plugins, macosKey); + pluginsMap[linuxKey] = _filterPluginsByPlatform(plugins, linuxKey); + pluginsMap[windowsKey] = _filterPluginsByPlatform(plugins, windowsKey); + pluginsMap[webKey] = _filterPluginsByPlatform(plugins, webKey); + + final Map result = {}; + + result['info'] = 'This is a generated file; do not edit or check into version control.'; + result['plugins'] = pluginsMap; + /// The dependencyGraph object is kept for backwards compatibility, but + /// should be removed once migration is complete. + /// https://github.com/flutter/flutter/issues/48918 + result['dependencyGraph'] = _createPluginLegacyDependencyGraph(plugins); + result['date_created'] = systemClock.now().toString(); + result['version'] = flutterVersion.frameworkVersion; + + final String oldPluginFileContent = _readFileContent(pluginsFile); + final String pluginFileContent = json.encode(result); + pluginsFile.writeAsStringSync(pluginFileContent, flush: true); + + return oldPluginFileContent != pluginFileContent; +} + +List _createPluginLegacyDependencyGraph(List plugins) { + final List directAppDependencies = []; + + final Set pluginNames = plugins.map((Plugin plugin) => plugin.name).toSet(); for (final Plugin plugin in plugins) { - flutterPluginsBuffer.write('${plugin.name}=${fsUtils.escapePath(plugin.path)}\n'); directAppDependencies.add({ 'name': plugin.name, // Extract the plugin dependencies which happen to be plugins. 'dependencies': [...plugin.dependencies.where(pluginNames.contains)], }); } + return directAppDependencies; +} + +// The .flutter-plugins file will be DEPRECATED in favor of .flutter-plugins-dependencies. +// TODO(franciscojma): Remove this method once deprecated. +// https://github.com/flutter/flutter/issues/48918 +// +/// Writes the .flutter-plugins files based on the list of plugins. +/// If there aren't any plugins, then the files aren't written to disk. +/// +/// Finally, returns [true] if .flutter-plugins has changed, otherwise returns [false]. +bool _writeFlutterPluginsListLegacy(FlutterProject project, List plugins) { + final File pluginsFile = project.flutterPluginsFile; - final String oldPluginFileContent = _readFileContent(pluginsFile); - final String pluginFileContent = flutterPluginsBuffer.toString(); - if (pluginNames.isNotEmpty) { - pluginsFile.writeAsStringSync(pluginFileContent, flush: true); - } else { + if (plugins.isEmpty) { if (pluginsFile.existsSync()) { pluginsFile.deleteSync(); + return true; } + return false; } - final File dependenciesFile = project.flutterPluginsDependenciesFile; - final String oldDependenciesFileContent = _readFileContent(dependenciesFile); - final String dependenciesFileContent = json.encode({ - '_info': '// $info', - 'dependencyGraph': directAppDependencies, - }); - if (pluginNames.isNotEmpty) { - dependenciesFile.writeAsStringSync(dependenciesFileContent, flush: true); - } else { - if (dependenciesFile.existsSync()) { - dependenciesFile.deleteSync(); - } - } + const String info = 'This is a generated file; do not edit or check into version control.'; + final StringBuffer flutterPluginsBuffer = StringBuffer('# $info\n'); - return oldPluginFileContent != _readFileContent(pluginsFile) - || oldDependenciesFileContent != _readFileContent(dependenciesFile); + for (final Plugin plugin in plugins) { + flutterPluginsBuffer.write('${plugin.name}=${fsUtils.escapePath(plugin.path)}\n'); + } + final String oldPluginFileContent = _readFileContent(pluginsFile); + final String pluginFileContent = flutterPluginsBuffer.toString(); + pluginsFile.writeAsStringSync(pluginFileContent, flush: true); + + return oldPluginFileContent != _readFileContent(pluginsFile); } /// Returns the contents of [File] or [null] if that file does not exist. @@ -782,8 +888,13 @@ Future _writeWebPluginRegistrant(FlutterProject project, List plug /// Assumes `pub get` has been executed since last change to `pubspec.yaml`. void refreshPluginsList(FlutterProject project, {bool checkProjects = false}) { final List plugins = findPlugins(project); + + // TODO(franciscojma): Remove once migration is complete. + // Write the legacy plugin files to avoid breaking existing apps. + final bool legacyChanged = _writeFlutterPluginsListLegacy(project, plugins); + final bool changed = _writeFlutterPluginsList(project, plugins); - if (changed) { + if (changed || legacyChanged) { if (!checkProjects || project.ios.existsSync()) { cocoaPods.invalidatePodInstallOutput(project.ios); } diff --git a/packages/flutter_tools/lib/src/project.dart b/packages/flutter_tools/lib/src/project.dart index fd8a48054f..f397a332b1 100644 --- a/packages/flutter_tools/lib/src/project.dart +++ b/packages/flutter_tools/lib/src/project.dart @@ -20,6 +20,7 @@ import 'flutter_manifest.dart'; import 'globals.dart' as globals; import 'ios/plist_parser.dart'; import 'ios/xcodeproj.dart' as xcode; +import 'platform_plugins.dart'; import 'plugins.dart'; import 'template.dart'; @@ -251,6 +252,16 @@ class FlutterProject { } } +/// Base class for projects per platform. +abstract class FlutterProjectPlatform { + + /// Plugin's platform config key, e.g., "macos", "ios". + String get pluginConfigKey; + + /// Whether the platform exists in the project. + bool existsSync(); +} + /// Represents an Xcode-based sub-project. /// /// This defines interfaces common to iOS and macOS projects. @@ -300,12 +311,15 @@ abstract class XcodeBasedProject { /// /// Instances will reflect the contents of the `ios/` sub-folder of /// Flutter applications and the `.ios/` sub-folder of Flutter module projects. -class IosProject implements XcodeBasedProject { +class IosProject extends FlutterProjectPlatform implements XcodeBasedProject { IosProject.fromFlutter(this.parent); @override final FlutterProject parent; + @override + String get pluginConfigKey => IOSPlugin.kConfigKey; + static final RegExp _productBundleIdPattern = RegExp(r'''^\s*PRODUCT_BUNDLE_IDENTIFIER\s*=\s*(["']?)(.*?)\1;\s*$'''); static const String _productBundleIdVariable = r'$(PRODUCT_BUNDLE_IDENTIFIER)'; static const String _hostAppBundleName = 'Runner'; @@ -574,12 +588,15 @@ class IosProject implements XcodeBasedProject { /// /// Instances will reflect the contents of the `android/` sub-folder of /// Flutter applications and the `.android/` sub-folder of Flutter module projects. -class AndroidProject { +class AndroidProject extends FlutterProjectPlatform { AndroidProject._(this.parent); /// The parent of this project. final FlutterProject parent; + @override + String get pluginConfigKey => AndroidPlugin.kConfigKey; + static final RegExp _applicationIdPattern = RegExp('^\\s*applicationId\\s+[\'\"](.*)[\'\"]\\s*\$'); static final RegExp _kotlinPluginPattern = RegExp('^\\s*apply plugin\:\\s+[\'\"]kotlin-android[\'\"]\\s*\$'); static final RegExp _groupPattern = RegExp('^\\s*group\\s+[\'\"](.*)[\'\"]\\s*\$'); @@ -627,6 +644,7 @@ class AndroidProject { } /// Whether the current flutter project has an Android sub-project. + @override bool existsSync() { return parent.isModule || _editableHostAppDirectory.existsSync(); } @@ -760,12 +778,16 @@ enum AndroidEmbeddingVersion { } /// Represents the web sub-project of a Flutter project. -class WebProject { +class WebProject extends FlutterProjectPlatform { WebProject._(this.parent); final FlutterProject parent; + @override + String get pluginConfigKey => WebPlugin.kConfigKey; + /// Whether this flutter project has a web sub-project. + @override bool existsSync() { return parent.directory.childDirectory('web').existsSync() && indexFile.existsSync(); @@ -810,12 +832,15 @@ Match _firstMatchInFile(File file, RegExp regExp) { } /// The macOS sub project. -class MacOSProject implements XcodeBasedProject { +class MacOSProject extends FlutterProjectPlatform implements XcodeBasedProject { MacOSProject._(this.parent); @override final FlutterProject parent; + @override + String get pluginConfigKey => MacOSPlugin.kConfigKey; + static const String _hostAppBundleName = 'Runner'; @override @@ -895,11 +920,15 @@ class MacOSProject implements XcodeBasedProject { } /// The Windows sub project -class WindowsProject { +class WindowsProject extends FlutterProjectPlatform { WindowsProject._(this.project); final FlutterProject project; + @override + String get pluginConfigKey => WindowsPlugin.kConfigKey; + + @override bool existsSync() => _editableDirectory.existsSync(); Directory get _editableDirectory => project.directory.childDirectory('windows'); @@ -933,11 +962,14 @@ class WindowsProject { } /// The Linux sub project. -class LinuxProject { +class LinuxProject extends FlutterProjectPlatform { LinuxProject._(this.project); final FlutterProject project; + @override + String get pluginConfigKey => LinuxPlugin.kConfigKey; + Directory get _editableDirectory => project.directory.childDirectory('linux'); /// The directory in the project that is managed by Flutter. As much as @@ -950,6 +982,7 @@ class LinuxProject { /// checked in should live here. Directory get ephemeralDirectory => managedDirectory.childDirectory('ephemeral'); + @override bool existsSync() => _editableDirectory.existsSync(); /// The Linux project makefile. diff --git a/packages/flutter_tools/lib/src/version.dart b/packages/flutter_tools/lib/src/version.dart index d6dc8c859d..470f4eda63 100644 --- a/packages/flutter_tools/lib/src/version.dart +++ b/packages/flutter_tools/lib/src/version.dart @@ -16,6 +16,8 @@ import 'cache.dart'; import 'convert.dart'; import 'globals.dart' as globals; +FlutterVersion get flutterVersion => context.get(); + class FlutterVersion { FlutterVersion([this._clock = const SystemClock()]) { _frameworkRevision = _runGit(gitLog(['-n', '1', '--pretty=format:%H']).join(' ')); diff --git a/packages/flutter_tools/test/general.shard/plugins_test.dart b/packages/flutter_tools/test/general.shard/plugins_test.dart index 635d0e92fd..1e58e34b96 100644 --- a/packages/flutter_tools/test/general.shard/plugins_test.dart +++ b/packages/flutter_tools/test/general.shard/plugins_test.dart @@ -2,13 +2,17 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:convert'; + import 'package:file/file.dart'; import 'package:file/memory.dart'; +import 'package:flutter_tools/src/base/time.dart'; import 'package:flutter_tools/src/dart/package_map.dart'; import 'package:flutter_tools/src/features.dart'; import 'package:flutter_tools/src/ios/xcodeproj.dart'; import 'package:flutter_tools/src/plugins.dart'; import 'package:flutter_tools/src/project.dart'; +import 'package:flutter_tools/src/version.dart'; import 'package:meta/meta.dart'; import 'package:mockito/mockito.dart'; @@ -23,15 +27,22 @@ void main() { MockMacOSProject macosProject; MockAndroidProject androidProject; MockWebProject webProject; + MockWindowsProject windowsProject; + MockLinuxProject linuxProject; File packagesFile; Directory dummyPackageDirectory; + SystemClock mockClock; + FlutterVersion mockVersion; setUp(() async { fs = MemoryFileSystem(); + mockClock = MockClock(); + mockVersion = MockFlutterVersion(); // Add basic properties to the Flutter project and subprojects flutterProject = MockFlutterProject(); when(flutterProject.directory).thenReturn(fs.directory('/')); + // TODO(franciscojma): Remove logic for .flutter-plugins it's deprecated. when(flutterProject.flutterPluginsFile).thenReturn(flutterProject.directory.childFile('.flutter-plugins')); when(flutterProject.flutterPluginsDependenciesFile).thenReturn(flutterProject.directory.childFile('.flutter-plugins-dependencies')); iosProject = MockIosProject(); @@ -39,18 +50,34 @@ void main() { when(iosProject.pluginRegistrantHost).thenReturn(flutterProject.directory.childDirectory('Runner')); when(iosProject.podfile).thenReturn(flutterProject.directory.childDirectory('ios').childFile('Podfile')); when(iosProject.podManifestLock).thenReturn(flutterProject.directory.childDirectory('ios').childFile('Podfile.lock')); + when(iosProject.pluginConfigKey).thenReturn('ios'); + when(iosProject.existsSync()).thenReturn(false); macosProject = MockMacOSProject(); when(flutterProject.macos).thenReturn(macosProject); when(macosProject.podfile).thenReturn(flutterProject.directory.childDirectory('macos').childFile('Podfile')); when(macosProject.podManifestLock).thenReturn(flutterProject.directory.childDirectory('macos').childFile('Podfile.lock')); + when(macosProject.pluginConfigKey).thenReturn('macos'); + when(macosProject.existsSync()).thenReturn(false); androidProject = MockAndroidProject(); when(flutterProject.android).thenReturn(androidProject); when(androidProject.pluginRegistrantHost).thenReturn(flutterProject.directory.childDirectory('android').childDirectory('app')); when(androidProject.hostAppGradleRoot).thenReturn(flutterProject.directory.childDirectory('android')); + when(androidProject.pluginConfigKey).thenReturn('android'); + when(androidProject.existsSync()).thenReturn(false); webProject = MockWebProject(); when(flutterProject.web).thenReturn(webProject); when(webProject.libDirectory).thenReturn(flutterProject.directory.childDirectory('lib')); when(webProject.existsSync()).thenReturn(true); + when(webProject.pluginConfigKey).thenReturn('web'); + when(webProject.existsSync()).thenReturn(false); + windowsProject = MockWindowsProject(); + when(flutterProject.windows).thenReturn(windowsProject); + when(windowsProject.pluginConfigKey).thenReturn('windows'); + when(windowsProject.existsSync()).thenReturn(false); + linuxProject = MockLinuxProject(); + when(flutterProject.linux).thenReturn(linuxProject); + when(linuxProject.pluginConfigKey).thenReturn('linux'); + when(linuxProject.existsSync()).thenReturn(false); // Set up a simple .packages file for all the tests to use, pointing to one package. dummyPackageDirectory = fs.directory('/pubcache/apackage/lib/'); @@ -67,6 +94,18 @@ void main() { platforms: ios: pluginClass: FLESomePlugin + macos: + pluginClass: FLESomePlugin + windows: + pluginClass: FLESomePlugin + linux: + pluginClass: FLESomePlugin + web: + pluginClass: SomePlugin + fileName: lib/SomeFile.dart + android: + pluginClass: SomePlugin + package: AndroidPackage '''); } @@ -239,8 +278,8 @@ dependencies: testUsingContext('Refreshing the plugin list deletes the plugin file when there were plugins but no longer are', () { flutterProject.flutterPluginsFile.createSync(); - when(iosProject.existsSync()).thenReturn(false); - when(macosProject.existsSync()).thenReturn(false); + flutterProject.flutterPluginsDependenciesFile.createSync(); + refreshPluginsList(flutterProject); expect(flutterProject.flutterPluginsFile.existsSync(), false); expect(flutterProject.flutterPluginsDependenciesFile.existsSync(), false); @@ -251,8 +290,8 @@ dependencies: testUsingContext('Refreshing the plugin list creates a plugin directory when there are plugins', () { configureDummyPackageAsPlugin(); - when(iosProject.existsSync()).thenReturn(false); - when(macosProject.existsSync()).thenReturn(false); + when(iosProject.existsSync()).thenReturn(true); + refreshPluginsList(flutterProject); expect(flutterProject.flutterPluginsFile.existsSync(), true); expect(flutterProject.flutterPluginsDependenciesFile.existsSync(), true); @@ -265,11 +304,20 @@ dependencies: createPluginWithDependencies(name: 'plugin-a', dependencies: const ['plugin-b', 'plugin-c', 'random-package']); createPluginWithDependencies(name: 'plugin-b', dependencies: const ['plugin-c']); createPluginWithDependencies(name: 'plugin-c', dependencies: const []); - when(iosProject.existsSync()).thenReturn(false); - when(macosProject.existsSync()).thenReturn(false); + when(iosProject.existsSync()).thenReturn(true); + + final DateTime dateCreated = DateTime(1970, 1, 1); + when(mockClock.now()).thenAnswer( + (Invocation _) => dateCreated + ); + const String version = '1.0.0'; + when(mockVersion.frameworkVersion).thenAnswer( + (Invocation _) => version + ); refreshPluginsList(flutterProject); + // Verify .flutter-plugins-dependencies is configured correctly. expect(flutterProject.flutterPluginsFile.existsSync(), true); expect(flutterProject.flutterPluginsDependenciesFile.existsSync(), true); expect(flutterProject.flutterPluginsFile.readAsStringSync(), @@ -279,28 +327,79 @@ dependencies: 'plugin-c=/.tmp_rand0/plugin.rand2/\n' '' ); - expect(flutterProject.flutterPluginsDependenciesFile.readAsStringSync(), - '{' - '"_info":"// This is a generated file; do not edit or check into version control.",' - '"dependencyGraph":[' - '{' - '"name":"plugin-a",' - '"dependencies":["plugin-b","plugin-c"]' - '},' - '{' - '"name":"plugin-b",' - '"dependencies":["plugin-c"]' - '},' - '{' - '"name":"plugin-c",' - '"dependencies":[]' - '}' - ']' - '}' - ); + + final String pluginsString = flutterProject.flutterPluginsDependenciesFile.readAsStringSync(); + final Map jsonContent = json.decode(pluginsString) as Map; + expect(jsonContent['info'], 'This is a generated file; do not edit or check into version control.'); + + final Map plugins = jsonContent['plugins'] as Map; + final List expectedPlugins = [ + { + 'name': 'plugin-a', + 'path': '/.tmp_rand0/plugin.rand0/', + 'dependencies': [ + 'plugin-b', + 'plugin-c' + ] + }, + { + 'name': 'plugin-b', + 'path': '/.tmp_rand0/plugin.rand1/', + 'dependencies': [ + 'plugin-c' + ] + }, + { + 'name': 'plugin-c', + 'path': '/.tmp_rand0/plugin.rand2/', + 'dependencies': [] + }, + ]; + expect(plugins['ios'], expectedPlugins); + expect(plugins['android'], expectedPlugins); + expect(plugins['macos'], []); + expect(plugins['windows'], []); + expect(plugins['linux'], []); + expect(plugins['web'], []); + + final List expectedDependencyGraph = [ + { + 'name': 'plugin-a', + 'dependencies': [ + 'plugin-b', + 'plugin-c' + ] + }, + { + 'name': 'plugin-b', + 'dependencies': [ + 'plugin-c' + ] + }, + { + 'name': 'plugin-c', + 'dependencies': [] + }, + ]; + + expect(jsonContent['dependencyGraph'], expectedDependencyGraph); + expect(jsonContent['date_created'], dateCreated.toString()); + expect(jsonContent['version'], version); + + // Make sure tests are updated if a new object is added/removed. + final List expectedKeys = [ + 'info', + 'plugins', + 'dependencyGraph', + 'date_created', + 'version', + ]; + expect(jsonContent.keys, expectedKeys); }, overrides: { FileSystem: () => fs, ProcessManager: () => FakeProcessManager.any(), + SystemClock: () => mockClock, + FlutterVersion: () => mockVersion }); testUsingContext('Changes to the plugin list invalidates the Cocoapod lockfiles', () { @@ -316,8 +415,25 @@ dependencies: FileSystem: () => fs, ProcessManager: () => FakeProcessManager.any(), }); + + testUsingContext('Changes to the plugin json list invalidates the Cocoapod lockfiles', () { + simulatePodInstallRun(iosProject); + simulatePodInstallRun(macosProject); + configureDummyPackageAsPlugin(); + + when(iosProject.existsSync()).thenReturn(true); + when(macosProject.existsSync()).thenReturn(true); + + refreshPluginsList(flutterProject); + expect(iosProject.podManifestLock.existsSync(), false); + expect(macosProject.podManifestLock.existsSync(), false); + }, overrides: { + FileSystem: () => fs, + ProcessManager: () => FakeProcessManager.any(), + }); }); + group('injectPlugins', () { MockFeatureFlags featureFlags; MockXcodeProjectInterpreter xcodeProjectInterpreter; @@ -600,6 +716,7 @@ dependencies: testUsingContext('Registrant for web doesn\'t escape slashes in imports', () async { when(flutterProject.isModule).thenReturn(true); when(featureFlags.isWebEnabled).thenReturn(true); + when(webProject.existsSync()).thenReturn(true); final Directory webPluginWithNestedFile = fs.systemTempDirectory.createTempSync('web_plugin_with_nested'); @@ -648,3 +765,5 @@ class MockIosProject extends Mock implements IosProject {} class MockMacOSProject extends Mock implements MacOSProject {} class MockXcodeProjectInterpreter extends Mock implements XcodeProjectInterpreter {} class MockWebProject extends Mock implements WebProject {} +class MockWindowsProject extends Mock implements WindowsProject {} +class MockLinuxProject extends Mock implements LinuxProject {}