// Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'package:file/file.dart'; import 'package:file/memory.dart'; import 'package:flutter_tools/src/dart/package_map.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/plugins.dart'; import 'package:flutter_tools/src/project.dart'; import 'package:package_config/package_config.dart'; import 'package:pub_semver/pub_semver.dart'; import 'package:test/fake.dart'; import 'package:yaml/yaml.dart'; import '../src/common.dart'; import '../src/context.dart'; void main() { group('Dart plugin registrant', () { late FileSystem fs; late FakeFlutterProject flutterProject; late FakeFlutterManifest flutterManifest; setUp(() async { fs = MemoryFileSystem.test(); final Directory directory = fs.currentDirectory.childDirectory('app'); flutterManifest = FakeFlutterManifest(); flutterProject = FakeFlutterProject() ..manifest = flutterManifest ..directory = directory ..flutterPluginsFile = directory.childFile('.flutter-plugins') ..flutterPluginsDependenciesFile = directory.childFile('.flutter-plugins-dependencies') ..dartPluginRegistrant = directory.childFile('dart_plugin_registrant.dart'); flutterProject.directory.childFile('.packages').createSync(recursive: true); }); group('resolvePlatformImplementation', () { testWithoutContext('selects implementation from direct dependency', () async { final Set directDependencies = { 'url_launcher_linux', 'url_launcher_macos', }; final List resolutions = resolvePlatformImplementation([ Plugin.fromYaml( 'url_launcher_linux', '', YamlMap.wrap({ 'implements': 'url_launcher', 'platforms': { 'linux': { 'dartPluginClass': 'UrlLauncherPluginLinux', }, }, }), null, [], fileSystem: fs, appDependencies: directDependencies, ), Plugin.fromYaml( 'url_launcher_macos', '', YamlMap.wrap({ 'implements': 'url_launcher', 'platforms': { 'macos': { 'dartPluginClass': 'UrlLauncherPluginMacOS', }, }, }), null, [], fileSystem: fs, appDependencies: directDependencies, ), Plugin.fromYaml( 'undirect_dependency_plugin', '', YamlMap.wrap({ 'implements': 'url_launcher', 'platforms': { 'windows': { 'dartPluginClass': 'UrlLauncherPluginWindows', }, }, }), null, [], fileSystem: fs, appDependencies: directDependencies, ), ]); resolvePlatformImplementation([ Plugin.fromYaml( 'url_launcher_macos', '', YamlMap.wrap({ 'implements': 'url_launcher', 'platforms': { 'macos': { 'dartPluginClass': 'UrlLauncherPluginMacOS', }, }, }), null, [], fileSystem: fs, appDependencies: directDependencies, ), ]); expect(resolutions.length, equals(2)); expect(resolutions[0].toMap(), equals( { 'pluginName': 'url_launcher_linux', 'dartClass': 'UrlLauncherPluginLinux', 'platform': 'linux', }) ); expect(resolutions[1].toMap(), equals( { 'pluginName': 'url_launcher_macos', 'dartClass': 'UrlLauncherPluginMacOS', 'platform': 'macos', }) ); }); testWithoutContext('selects inline implementation on mobile', () async { final Set directDependencies = {}; final List resolutions = resolvePlatformImplementation([ Plugin.fromYaml( 'url_launcher', '', YamlMap.wrap({ 'platforms': { 'android': { 'dartPluginClass': 'UrlLauncherAndroid', }, 'ios': { 'dartPluginClass': 'UrlLauncherIos', }, }, }), null, [], fileSystem: fs, appDependencies: directDependencies, ), ]); expect(resolutions.length, equals(2)); expect(resolutions[0].toMap(), equals( { 'pluginName': 'url_launcher', 'dartClass': 'UrlLauncherAndroid', 'platform': 'android', }) ); expect(resolutions[1].toMap(), equals( { 'pluginName': 'url_launcher', 'dartClass': 'UrlLauncherIos', 'platform': 'ios', }) ); }); // See https://github.com/flutter/flutter/issues/87862 for details. testWithoutContext('does not select inline implementation on desktop for ' 'missing min Flutter SDK constraint', () async { final Set directDependencies = {}; final List resolutions = resolvePlatformImplementation([ Plugin.fromYaml( 'url_launcher', '', YamlMap.wrap({ 'platforms': { 'linux': { 'dartPluginClass': 'UrlLauncherLinux', }, 'macos': { 'dartPluginClass': 'UrlLauncherMacOS', }, 'windows': { 'dartPluginClass': 'UrlLauncherWindows', }, }, }), null, [], fileSystem: fs, appDependencies: directDependencies, ), ]); expect(resolutions.length, equals(0)); }); // See https://github.com/flutter/flutter/issues/87862 for details. testWithoutContext('does not select inline implementation on desktop for ' 'min Flutter SDK constraint < 2.11', () async { final Set directDependencies = {}; final List resolutions = resolvePlatformImplementation([ Plugin.fromYaml( 'url_launcher', '', YamlMap.wrap({ 'platforms': { 'linux': { 'dartPluginClass': 'UrlLauncherLinux', }, 'macos': { 'dartPluginClass': 'UrlLauncherMacOS', }, 'windows': { 'dartPluginClass': 'UrlLauncherWindows', }, }, }), VersionConstraint.parse('>=2.10.0'), [], fileSystem: fs, appDependencies: directDependencies, ), ]); expect(resolutions.length, equals(0)); }); testWithoutContext('selects inline implementation on desktop for ' 'min Flutter SDK requirement of at least 2.11', () async { final Set directDependencies = {}; final List resolutions = resolvePlatformImplementation([ Plugin.fromYaml( 'url_launcher', '', YamlMap.wrap({ 'platforms': { 'linux': { 'dartPluginClass': 'UrlLauncherLinux', }, 'macos': { 'dartPluginClass': 'UrlLauncherMacOS', }, 'windows': { 'dartPluginClass': 'UrlLauncherWindows', }, }, }), VersionConstraint.parse('>=2.11.0'), [], fileSystem: fs, appDependencies: directDependencies, ), ]); expect(resolutions.length, equals(3)); expect( resolutions.map((PluginInterfaceResolution resolution) => resolution.toMap()), containsAll(>[ { 'pluginName': 'url_launcher', 'dartClass': 'UrlLauncherLinux', 'platform': 'linux', }, { 'pluginName': 'url_launcher', 'dartClass': 'UrlLauncherMacOS', 'platform': 'macos', }, { 'pluginName': 'url_launcher', 'dartClass': 'UrlLauncherWindows', 'platform': 'windows', }, ]) ); }); testWithoutContext('selects default implementation', () async { final Set directDependencies = {}; final List resolutions = resolvePlatformImplementation([ Plugin.fromYaml( 'url_launcher', '', YamlMap.wrap({ 'platforms': { 'linux': { 'default_package': 'url_launcher_linux', }, }, }), null, [], fileSystem: fs, appDependencies: directDependencies, ), Plugin.fromYaml( 'url_launcher_linux', '', YamlMap.wrap({ 'implements': 'url_launcher', 'platforms': { 'linux': { 'dartPluginClass': 'UrlLauncherPluginLinux', }, }, }), null, [], fileSystem: fs, appDependencies: directDependencies, ), ]); expect(resolutions.length, equals(1)); expect(resolutions[0].toMap(), equals( { 'pluginName': 'url_launcher_linux', 'dartClass': 'UrlLauncherPluginLinux', 'platform': 'linux', }) ); }); testWithoutContext('selects default implementation if interface is direct dependency', () async { final Set directDependencies = {'url_launcher'}; final List resolutions = resolvePlatformImplementation([ Plugin.fromYaml( 'url_launcher', '', YamlMap.wrap({ 'platforms': { 'linux': { 'default_package': 'url_launcher_linux', }, }, }), null, [], fileSystem: fs, appDependencies: directDependencies, ), Plugin.fromYaml( 'url_launcher_linux', '', YamlMap.wrap({ 'implements': 'url_launcher', 'platforms': { 'linux': { 'dartPluginClass': 'UrlLauncherPluginLinux', }, }, }), null, [], fileSystem: fs, appDependencies: directDependencies, ), ]); expect(resolutions.length, equals(1)); expect(resolutions[0].toMap(), equals( { 'pluginName': 'url_launcher_linux', 'dartClass': 'UrlLauncherPluginLinux', 'platform': 'linux', }) ); }); testWithoutContext('selects user selected implementation despites default implementation', () async { final Set directDependencies = { 'user_selected_url_launcher_implementation', 'url_launcher', }; final List resolutions = resolvePlatformImplementation([ Plugin.fromYaml( 'url_launcher', '', YamlMap.wrap({ 'platforms': { 'linux': { 'default_package': 'url_launcher_linux', }, }, }), null, [], fileSystem: fs, appDependencies: directDependencies, ), Plugin.fromYaml( 'url_launcher_linux', '', YamlMap.wrap({ 'implements': 'url_launcher', 'platforms': { 'linux': { 'dartPluginClass': 'UrlLauncherPluginLinux', }, }, }), null, [], fileSystem: fs, appDependencies: directDependencies, ), Plugin.fromYaml( 'user_selected_url_launcher_implementation', '', YamlMap.wrap({ 'implements': 'url_launcher', 'platforms': { 'linux': { 'dartPluginClass': 'UrlLauncherPluginLinux', }, }, }), null, [], fileSystem: fs, appDependencies: directDependencies, ), ]); expect(resolutions.length, equals(1)); expect(resolutions[0].toMap(), equals( { 'pluginName': 'user_selected_url_launcher_implementation', 'dartClass': 'UrlLauncherPluginLinux', 'platform': 'linux', }) ); }); testWithoutContext('selects user selected implementation despites default implementation', () async { final Set directDependencies = { 'user_selected_url_launcher_implementation', 'url_launcher', }; final List resolutions = resolvePlatformImplementation([ Plugin.fromYaml( 'url_launcher', '', YamlMap.wrap({ 'platforms': { 'linux': { 'default_package': 'url_launcher_linux', }, }, }), null, [], fileSystem: fs, appDependencies: directDependencies, ), Plugin.fromYaml( 'url_launcher_linux', '', YamlMap.wrap({ 'implements': 'url_launcher', 'platforms': { 'linux': { 'dartPluginClass': 'UrlLauncherPluginLinux', }, }, }), null, [], fileSystem: fs, appDependencies: directDependencies, ), Plugin.fromYaml( 'user_selected_url_launcher_implementation', '', YamlMap.wrap({ 'implements': 'url_launcher', 'platforms': { 'linux': { 'dartPluginClass': 'UrlLauncherPluginLinux', }, }, }), null, [], fileSystem: fs, appDependencies: directDependencies, ), ]); expect(resolutions.length, equals(1)); expect(resolutions[0].toMap(), equals( { 'pluginName': 'user_selected_url_launcher_implementation', 'dartClass': 'UrlLauncherPluginLinux', 'platform': 'linux', }) ); }); testUsingContext('provides error when user selected multiple implementations', () async { final Set directDependencies = { 'url_launcher_linux_1', 'url_launcher_linux_2', }; expect(() { resolvePlatformImplementation([ Plugin.fromYaml( 'url_launcher_linux_1', '', YamlMap.wrap({ 'implements': 'url_launcher', 'platforms': { 'linux': { 'dartPluginClass': 'UrlLauncherPluginLinux', }, }, }), null, [], fileSystem: fs, appDependencies: directDependencies, ), Plugin.fromYaml( 'url_launcher_linux_2', '', YamlMap.wrap({ 'implements': 'url_launcher', 'platforms': { 'linux': { 'dartPluginClass': 'UrlLauncherPluginLinux', }, }, }), null, [], fileSystem: fs, appDependencies: directDependencies, ), ]); expect( testLogger.errorText, 'Plugin `url_launcher_linux_2` implements an interface for `linux`, which was already implemented by plugin `url_launcher_linux_1`.\n' 'To fix this issue, remove either dependency from pubspec.yaml.' '\n\n' ); }, throwsToolExit( message: 'Please resolve the errors', )); }); testUsingContext('provides all errors when user selected multiple implementations', () async { final Set directDependencies = { 'url_launcher_linux_1', 'url_launcher_linux_2', }; expect(() { resolvePlatformImplementation([ Plugin.fromYaml( 'url_launcher_linux_1', '', YamlMap.wrap({ 'implements': 'url_launcher', 'platforms': { 'linux': { 'dartPluginClass': 'UrlLauncherPluginLinux', }, }, }), null, [], fileSystem: fs, appDependencies: directDependencies, ), Plugin.fromYaml( 'url_launcher_linux_2', '', YamlMap.wrap({ 'implements': 'url_launcher', 'platforms': { 'linux': { 'dartPluginClass': 'UrlLauncherPluginLinux', }, }, }), null, [], fileSystem: fs, appDependencies: directDependencies, ), ]); expect( testLogger.errorText, 'Plugin `url_launcher_linux_2` implements an interface for `linux`, which was already implemented by plugin `url_launcher_linux_1`.\n' 'To fix this issue, remove either dependency from pubspec.yaml.' '\n\n' ); }, throwsToolExit( message: 'Please resolve the errors', )); }); }); group('generateMainDartWithPluginRegistrant', () { testUsingContext('Generates new entrypoint', () async { flutterProject.isModule = true; createFakeDartPlugins( flutterProject, flutterManifest, fs, { 'url_launcher_android': ''' flutter: plugin: implements: url_launcher platforms: android: dartPluginClass: AndroidPlugin ''', 'url_launcher_ios': ''' flutter: plugin: implements: url_launcher platforms: ios: dartPluginClass: IosPlugin ''', 'url_launcher_macos': ''' flutter: plugin: implements: url_launcher platforms: macos: dartPluginClass: MacOSPlugin ''', 'url_launcher_linux': ''' flutter: plugin: implements: url_launcher platforms: linux: dartPluginClass: LinuxPlugin ''', 'url_launcher_windows': ''' flutter: plugin: implements: url_launcher platforms: windows: dartPluginClass: WindowsPlugin ''', 'awesome_macos': ''' flutter: plugin: implements: awesome platforms: macos: dartPluginClass: AwesomeMacOS ''', }); final Directory libDir = flutterProject.directory.childDirectory('lib'); libDir.createSync(recursive: true); final File mainFile = libDir.childFile('main.dart'); mainFile.writeAsStringSync(''' // @dart = 2.8 void main() { } '''); final PackageConfig packageConfig = await loadPackageConfigWithLogging( flutterProject.directory.childDirectory('.dart_tool').childFile('package_config.json'), logger: globals.logger, throwOnError: false, ); await generateMainDartWithPluginRegistrant( flutterProject, packageConfig, 'package:app/main.dart', mainFile, throwOnPluginPubspecError: true, ); expect(flutterProject.dartPluginRegistrant.readAsStringSync(), '//\n' '// Generated file. Do not edit.\n' '// This file is generated from template in file `flutter_tools/lib/src/flutter_plugins.dart`.\n' '//\n' '\n' '// @dart = 2.8\n' '\n' "import 'dart:io'; // flutter_ignore: dart_io_import.\n" "import 'package:url_launcher_android/url_launcher_android.dart';\n" "import 'package:url_launcher_ios/url_launcher_ios.dart';\n" "import 'package:url_launcher_linux/url_launcher_linux.dart';\n" "import 'package:awesome_macos/awesome_macos.dart';\n" "import 'package:url_launcher_macos/url_launcher_macos.dart';\n" "import 'package:url_launcher_windows/url_launcher_windows.dart';\n" '\n' "@pragma('vm:entry-point')\n" 'class _PluginRegistrant {\n' '\n' " @pragma('vm:entry-point')\n" ' static void register() {\n' ' if (Platform.isAndroid) {\n' ' try {\n' ' AndroidPlugin.registerWith();\n' ' } catch (err) {\n' ' print(\n' " '`url_launcher_android` threw an error: \$err. '\n" " 'The app may not function as expected until you remove this plugin from pubspec.yaml'\n" ' );\n' ' rethrow;\n' ' }\n' '\n' ' } else if (Platform.isIOS) {\n' ' try {\n' ' IosPlugin.registerWith();\n' ' } catch (err) {\n' ' print(\n' " '`url_launcher_ios` threw an error: \$err. '\n" " 'The app may not function as expected until you remove this plugin from pubspec.yaml'\n" ' );\n' ' rethrow;\n' ' }\n' '\n' ' } else if (Platform.isLinux) {\n' ' try {\n' ' LinuxPlugin.registerWith();\n' ' } catch (err) {\n' ' print(\n' " '`url_launcher_linux` threw an error: \$err. '\n" " 'The app may not function as expected until you remove this plugin from pubspec.yaml'\n" ' );\n' ' rethrow;\n' ' }\n' '\n' ' } else if (Platform.isMacOS) {\n' ' try {\n' ' AwesomeMacOS.registerWith();\n' ' } catch (err) {\n' ' print(\n' " '`awesome_macos` threw an error: \$err. '\n" " 'The app may not function as expected until you remove this plugin from pubspec.yaml'\n" ' );\n' ' rethrow;\n' ' }\n' '\n' ' try {\n' ' MacOSPlugin.registerWith();\n' ' } catch (err) {\n' ' print(\n' " '`url_launcher_macos` threw an error: \$err. '\n" " 'The app may not function as expected until you remove this plugin from pubspec.yaml'\n" ' );\n' ' rethrow;\n' ' }\n' '\n' ' } else if (Platform.isWindows) {\n' ' try {\n' ' WindowsPlugin.registerWith();\n' ' } catch (err) {\n' ' print(\n' " '`url_launcher_windows` threw an error: \$err. '\n" " 'The app may not function as expected until you remove this plugin from pubspec.yaml'\n" ' );\n' ' rethrow;\n' ' }\n' '\n' ' }\n' ' }\n' '}\n' ); }, overrides: { FileSystem: () => fs, ProcessManager: () => FakeProcessManager.any(), }); testUsingContext('Plugin without platform support throws tool exit', () async { flutterProject.isModule = false; createFakeDartPlugins( flutterProject, flutterManifest, fs, { 'url_launcher_macos': ''' flutter: plugin: implements: url_launcher platforms: macos: invalid: ''', }); final Directory libDir = flutterProject.directory.childDirectory('lib'); libDir.createSync(recursive: true); final File mainFile = libDir.childFile('main.dart')..writeAsStringSync(''); final PackageConfig packageConfig = await loadPackageConfigWithLogging( flutterProject.directory.childDirectory('.dart_tool').childFile('package_config.json'), logger: globals.logger, throwOnError: false, ); await expectLater( generateMainDartWithPluginRegistrant( flutterProject, packageConfig, 'package:app/main.dart', mainFile, throwOnPluginPubspecError: true, ), throwsToolExit(message: 'Invalid plugin specification url_launcher_macos.\n' 'Invalid "macos" plugin specification.' ), ); }, overrides: { FileSystem: () => fs, ProcessManager: () => FakeProcessManager.any(), }); testUsingContext('Plugin with platform support without dart plugin class throws tool exit', () async { flutterProject.isModule = false; createFakeDartPlugins( flutterProject, flutterManifest, fs, { 'url_launcher_macos': ''' flutter: plugin: implements: url_launcher ''', }); final Directory libDir = flutterProject.directory.childDirectory('lib'); libDir.createSync(recursive: true); final File mainFile = libDir.childFile('main.dart')..writeAsStringSync(''); final PackageConfig packageConfig = await loadPackageConfigWithLogging( flutterProject.directory.childDirectory('.dart_tool').childFile('package_config.json'), logger: globals.logger, throwOnError: false, ); await expectLater( generateMainDartWithPluginRegistrant( flutterProject, packageConfig, 'package:app/main.dart', mainFile, throwOnPluginPubspecError: true, ), throwsToolExit(message: 'Invalid plugin specification url_launcher_macos.\n' 'Cannot find the `flutter.plugin.platforms` key in the `pubspec.yaml` file. ' 'An instruction to format the `pubspec.yaml` can be found here: ' 'https://flutter.dev/docs/development/packages-and-plugins/developing-packages#plugin-platforms' ), ); }, overrides: { FileSystem: () => fs, ProcessManager: () => FakeProcessManager.any(), }); testUsingContext('Does not show error messages if throwOnPluginPubspecError is false', () async { final Set directDependencies = { 'url_launcher_windows', }; resolvePlatformImplementation([ Plugin.fromYaml( 'url_launcher_windows', '', YamlMap.wrap({ 'platforms': { 'windows': { 'dartPluginClass': 'UrlLauncherPluginWindows', }, }, }), null, [], fileSystem: fs, appDependencies: directDependencies, ), ], throwOnPluginPubspecError: false, ); expect(testLogger.errorText, ''); }, overrides: { FileSystem: () => fs, ProcessManager: () => FakeProcessManager.any(), }); testUsingContext('Does not create new entrypoint if there are no platform resolutions', () async { flutterProject.isModule = false; final Directory libDir = flutterProject.directory.childDirectory('lib'); libDir.createSync(recursive: true); final File mainFile = libDir.childFile('main.dart')..writeAsStringSync(''); final PackageConfig packageConfig = await loadPackageConfigWithLogging( flutterProject.directory.childDirectory('.dart_tool').childFile('package_config.json'), logger: globals.logger, throwOnError: false, ); await generateMainDartWithPluginRegistrant( flutterProject, packageConfig, 'package:app/main.dart', mainFile, throwOnPluginPubspecError: true, ); expect(flutterProject.dartPluginRegistrant.existsSync(), isFalse); }, overrides: { FileSystem: () => fs, ProcessManager: () => FakeProcessManager.any(), }); testUsingContext('Deletes new entrypoint if there are no platform resolutions', () async { flutterProject.isModule = false; createFakeDartPlugins( flutterProject, flutterManifest, fs, { 'url_launcher_macos': ''' flutter: plugin: implements: url_launcher platforms: macos: dartPluginClass: MacOSPlugin ''', }); final Directory libDir = flutterProject.directory.childDirectory('lib'); libDir.createSync(recursive: true); final File mainFile = libDir.childFile('main.dart')..writeAsStringSync(''); final PackageConfig packageConfig = await loadPackageConfigWithLogging( flutterProject.directory.childDirectory('.dart_tool').childFile('package_config.json'), logger: globals.logger, throwOnError: false, ); await generateMainDartWithPluginRegistrant( flutterProject, packageConfig, 'package:app/main.dart', mainFile, throwOnPluginPubspecError: true, ); expect(flutterProject.dartPluginRegistrant.existsSync(), isTrue); // No plugins. createFakeDartPlugins( flutterProject, flutterManifest, fs, {}); await generateMainDartWithPluginRegistrant( flutterProject, packageConfig, 'package:app/main.dart', mainFile, throwOnPluginPubspecError: true, ); expect(flutterProject.dartPluginRegistrant.existsSync(), isFalse); }, overrides: { FileSystem: () => fs, ProcessManager: () => FakeProcessManager.any(), }); }); }); } void createFakeDartPlugins( FakeFlutterProject flutterProject, FakeFlutterManifest flutterManifest, FileSystem fs, Map plugins, ) { final Directory fakePubCache = fs.systemTempDirectory.childDirectory('cache'); final File packagesFile = flutterProject.directory .childFile('.packages') ..createSync(recursive: true); for (final MapEntry entry in plugins.entries) { final String name = fs.path.basename(entry.key); final Directory pluginDirectory = fakePubCache.childDirectory(name); packagesFile.writeAsStringSync( '$name:file://${pluginDirectory.childFile('lib').uri}\n', mode: FileMode.writeOnlyAppend, ); pluginDirectory.childFile('pubspec.yaml') ..createSync(recursive: true) ..writeAsStringSync(entry.value); } flutterManifest.dependencies = plugins.keys.toSet(); } class FakeFlutterManifest extends Fake implements FlutterManifest { @override Set dependencies = {}; } class FakeFlutterProject extends Fake implements FlutterProject { @override bool isModule = false; @override late FlutterManifest manifest; @override late Directory directory; @override late File flutterPluginsFile; @override late File flutterPluginsDependenciesFile; @override late File dartPluginRegistrant; @override late IosProject ios; @override late AndroidProject android; @override late WebProject web; @override late MacOSProject macos; @override late LinuxProject linux; @override late WindowsProject windows; }