From af93500e8cf424a3da6346440828963db35be709 Mon Sep 17 00:00:00 2001 From: Amir Hardon Date: Wed, 4 Dec 2019 14:23:33 -0800 Subject: [PATCH 1/2] Add a plugin_platform_interface package --- packages/plugin_platform_interface/.gitignore | 75 ++++++++++++++ packages/plugin_platform_interface/.metadata | 10 ++ .../plugin_platform_interface/CHANGELOG.md | 5 + packages/plugin_platform_interface/LICENSE | 27 +++++ packages/plugin_platform_interface/README.md | 51 ++++++++++ .../lib/plugin_platform_interface.dart | 98 +++++++++++++++++++ .../plugin_platform_interface/pubspec.yaml | 27 +++++ .../test/plugin_platform_interface_test.dart | 47 +++++++++ 8 files changed, 340 insertions(+) create mode 100644 packages/plugin_platform_interface/.gitignore create mode 100644 packages/plugin_platform_interface/.metadata create mode 100644 packages/plugin_platform_interface/CHANGELOG.md create mode 100644 packages/plugin_platform_interface/LICENSE create mode 100644 packages/plugin_platform_interface/README.md create mode 100644 packages/plugin_platform_interface/lib/plugin_platform_interface.dart create mode 100644 packages/plugin_platform_interface/pubspec.yaml create mode 100644 packages/plugin_platform_interface/test/plugin_platform_interface_test.dart diff --git a/packages/plugin_platform_interface/.gitignore b/packages/plugin_platform_interface/.gitignore new file mode 100644 index 000000000000..bb431f0d5b47 --- /dev/null +++ b/packages/plugin_platform_interface/.gitignore @@ -0,0 +1,75 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +build/ + +# Android related +**/android/**/gradle-wrapper.jar +**/android/.gradle +**/android/captures/ +**/android/gradlew +**/android/gradlew.bat +**/android/local.properties +**/android/**/GeneratedPluginRegistrant.java + +# iOS/XCode related +**/ios/**/*.mode1v3 +**/ios/**/*.mode2v3 +**/ios/**/*.moved-aside +**/ios/**/*.pbxuser +**/ios/**/*.perspectivev3 +**/ios/**/*sync/ +**/ios/**/.sconsign.dblite +**/ios/**/.tags* +**/ios/**/.vagrant/ +**/ios/**/DerivedData/ +**/ios/**/Icon? +**/ios/**/Pods/ +**/ios/**/.symlinks/ +**/ios/**/profile +**/ios/**/xcuserdata +**/ios/.generated/ +**/ios/Flutter/App.framework +**/ios/Flutter/Flutter.framework +**/ios/Flutter/Flutter.podspec +**/ios/Flutter/Generated.xcconfig +**/ios/Flutter/app.flx +**/ios/Flutter/app.zip +**/ios/Flutter/flutter_assets/ +**/ios/Flutter/flutter_export_environment.sh +**/ios/ServiceDefinitions.json +**/ios/Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!**/ios/**/default.mode1v3 +!**/ios/**/default.mode2v3 +!**/ios/**/default.pbxuser +!**/ios/**/default.perspectivev3 +!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages diff --git a/packages/plugin_platform_interface/.metadata b/packages/plugin_platform_interface/.metadata new file mode 100644 index 000000000000..30f7030eb120 --- /dev/null +++ b/packages/plugin_platform_interface/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: f213f92587b3f6f3b079a9cf356b2c5fcf00d20b + channel: master + +project_type: package diff --git a/packages/plugin_platform_interface/CHANGELOG.md b/packages/plugin_platform_interface/CHANGELOG.md new file mode 100644 index 000000000000..29f2ef9c1b7f --- /dev/null +++ b/packages/plugin_platform_interface/CHANGELOG.md @@ -0,0 +1,5 @@ +## 1.0.0 - Initial release. + +* Provides `PlatformInterface` with common mechanism for enforcing that a platform interface + is not implemented with `implements`. +* Provides test only `MockPlatformInterface` to enable using Mockito to mock platform interfaces. diff --git a/packages/plugin_platform_interface/LICENSE b/packages/plugin_platform_interface/LICENSE new file mode 100644 index 000000000000..03118dc2b39b --- /dev/null +++ b/packages/plugin_platform_interface/LICENSE @@ -0,0 +1,27 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/packages/plugin_platform_interface/README.md b/packages/plugin_platform_interface/README.md new file mode 100644 index 000000000000..f213bc777d76 --- /dev/null +++ b/packages/plugin_platform_interface/README.md @@ -0,0 +1,51 @@ +# plugin_platform_interface + +This package provides a base class for platform interfaces of [federated flutter plugins](https://fluter.dev/go/federated-plugins). + +Platform implementations should extend their platform interface classes rather than implement it as +newly added methods to platform interfaces are not considered as breaking changes. Extending a platform +interface ensures that subclasses will get the default implementations from the base class, while +platform implementations that `implements` their platform interface will be broken by newly added methods. + +This class package provides common functionality for platform interface to enforce that they are extended +and not implemented. + +## Sample usage: + +```dart +abstract class UrlLauncherPlatform extends PlatformInterface { + UrlLauncherPlatform() : super(token: _token); + + static UrlLauncherPlatform _instance = MethodChannelUrlLauncher(); + + static const Object _token = Object(); + + static UrlLauncherPlatform get instance => _instance; + + /// Platform-specific plugins should set this with their own platform-specific + /// class that extends [UrlLauncherPlatform] when they register themselves. + static set instance(UrlLauncherPlatform instance) { + PlatformInterface.verifyToken(instance, _token); + _instance = instance; + } + + } +``` + +This guarantees that UrlLauncherPlatform.instance cannot be set to an object that `implements` +UrlLauncherPlatform (it can only be set to an object that `extends` UrlLauncherPlatform). + +## Mocking platform interfaces with Mockito + + +Mockito mocks of platform interfaces will fail the verification done by `verifyToken`. +This package provides a `MockPlatformInterfaceMixin` which can be used in test code only to disable +the `extends` enforcement. + +A Mockito mock of a platform interface can be created with: + +```dart +class UrlLauncherPlatformMock extends Mock + with MockPlatformInterfaceMixin + implements UrlLauncherPlatform {} +``` diff --git a/packages/plugin_platform_interface/lib/plugin_platform_interface.dart b/packages/plugin_platform_interface/lib/plugin_platform_interface.dart new file mode 100644 index 000000000000..e914d2894065 --- /dev/null +++ b/packages/plugin_platform_interface/lib/plugin_platform_interface.dart @@ -0,0 +1,98 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +library plugin_platform_interface; + +import 'package:meta/meta.dart'; + +/// Base class for platform interfaces. +/// +/// Provides a static helper method for ensuring that platform interfaces are +/// implemented using `extends` instead of `implements`. +/// +/// Platform interface classes are expected to have a private static token object which will be +/// be passed to [verifyToken] along with a platform interface object for verification. +/// +/// Sample usage: +/// +/// ```dart +/// abstract class UrlLauncherPlatform extends PlatformInterface { +/// UrlLauncherPlatform() : super(token: _token); +/// +/// static UrlLauncherPlatform _instance = MethodChannelUrlLauncher(); +/// +/// static const Object _token = Object(); +/// +/// static UrlLauncherPlatform get instance => _instance; +/// +/// /// Platform-specific plugins should set this with their own platform-specific +/// /// class that extends [UrlLauncherPlatform] when they register themselves. +/// static set instance(UrlLauncherPlatform instance) { +/// PlatformInterface.verifyToken(instance, _token); +/// _instance = instance; +/// } +/// +/// } +/// ``` +/// +/// Mockito mocks of platform interfaces will fail the verification, in test code only it is possible +/// to include the [MockPlatformInterfaceMixin] for the verification to be temporarily disabled. See +/// [MockPlatformInterfaceMixin] for a sample of using Mockito to mock a platform interface. +abstract class PlatformInterface { + /// Pass a private, class-specific `const Object()` as the `token`. + PlatformInterface({@required Object token}) : _instanceToken = token; + + final Object _instanceToken; + + /// Ensures that the platform instance has a token that matches the + /// provided token and throws [AssertionError] if not. + /// + /// This is used to ensure that implementers are using `extends` rather than + /// `implements`. + /// + /// Subclasses of [MockPlatformInterfaceMixin] are assumed to be valid in debug + /// builds. + /// + /// This is implemented as a static method so that it cannot be overridden + /// with `noSuchMethod`. + static void verifyToken(PlatformInterface instance, Object token) { + if (identical(instance._instanceToken, MockPlatformInterfaceMixin._token)) { + bool assertionsEnabled = false; + assert(() { + assertionsEnabled = true; + return true; + }()); + if (!assertionsEnabled) { + throw AssertionError( + '`MockPlatformInterfaceMixin` is not intended for use in release builds.'); + } + } + if (!identical(token, instance._instanceToken)) { + throw AssertionError( + 'Platform interfaces must not be implemented with `implements`'); + } + } +} + +/// A [PlatformInterface] mixin that can be combined with mockito's `Mock`. +/// +/// It passes the [PlatformInterface.verifyToken] check even though it isn't +/// using `extends`. +/// +/// This class is intended for use in tests only. +/// +/// Sample usage (assuming UrlLauncherPlatform extends [PlatformInterface]: +/// +/// ```dart +/// class UrlLauncherPlatformMock extends Mock +/// with MockPlatformInterfaceMixin +/// implements UrlLauncherPlatform {} +/// ``` +@visibleForTesting +abstract class MockPlatformInterfaceMixin implements PlatformInterface { + static const Object _token = Object(); + + @override + Object get _instanceToken => _token; +} \ No newline at end of file diff --git a/packages/plugin_platform_interface/pubspec.yaml b/packages/plugin_platform_interface/pubspec.yaml new file mode 100644 index 000000000000..29d08e14ba9a --- /dev/null +++ b/packages/plugin_platform_interface/pubspec.yaml @@ -0,0 +1,27 @@ +name: plugin_platform_interface +description: Reusable base class for Flutter plugin platform interfaces. + +# DO NOT MAKE A BREAKING CHANGE TO THIS PACKAGE +# DO NOT INCREASE THE MAJOR VERSION OF THIS PACKAGE +# +# This package is used as a second level dependency for many plugins, a major version bump here +# is guaranteed to lead the ecosystem to a version lock (the first plugin that upgrades to version +# 2 of this package cannot be used with any other plugin that have not yet migrated). +# +# Please consider carefully before bumping the major version of this package, ideally it should only +# be done when absolutely necessary and after the ecosystem has already migrated to 1.X.Y version +# that is forward compatible with 2.0.0 (ideally the ecosystem have migrated to depend on: +# `plugin_platform_interface: >=1.X.Y <3.0.0`). +version: 1.0.0 + +homepage: https://github.com/flutter/plugins/plugin_platform_interface + +environment: + sdk: ">=2.1.0 <3.0.0" + +dependencies: + meta: ^1.0.0 + +dev_dependencies: + mockito: ^4.1.1 + test: ^1.9.4 diff --git a/packages/plugin_platform_interface/test/plugin_platform_interface_test.dart b/packages/plugin_platform_interface/test/plugin_platform_interface_test.dart new file mode 100644 index 000000000000..a33dcf4626dd --- /dev/null +++ b/packages/plugin_platform_interface/test/plugin_platform_interface_test.dart @@ -0,0 +1,47 @@ +// Copyright 2019 The Chromium 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:mockito/mockito.dart'; +import 'package:test/test.dart'; + +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; + +class SamplePluginPlatform extends PlatformInterface { + SamplePluginPlatform(): super(token: _token); + + static const Object _token = Object(); + + + static set instance(SamplePluginPlatform instance) { + PlatformInterface.verifyToken(instance, _token); + // A real implementation would set a static instance field here. + } +} + +class ImplementsSamplePluginPlatform extends Mock + implements SamplePluginPlatform {} + +class ImplementsSamplePluginPlatformUsingMockPlatformInterfaceMixin extends Mock + with MockPlatformInterfaceMixin + implements SamplePluginPlatform {} + +class ExtendsSamplePluginPlatform extends SamplePluginPlatform {} + +void main() { + test('Cannot be implemented with `implements`', () { + expect(() { + SamplePluginPlatform.instance = ImplementsSamplePluginPlatform(); + }, throwsA(isA())); + }); + + test('Can be mocked with `implements`', () { + final SamplePluginPlatform mock = + ImplementsSamplePluginPlatformUsingMockPlatformInterfaceMixin(); + SamplePluginPlatform.instance = mock; + }); + + test('Can be extended', () { + SamplePluginPlatform.instance = ExtendsSamplePluginPlatform(); + }); +} From d40c255e15fa93469ef5b116ca23b1e0059dc721 Mon Sep 17 00:00:00 2001 From: Amir Hardon Date: Tue, 10 Dec 2019 11:49:30 -0800 Subject: [PATCH 2/2] format --- .../lib/plugin_platform_interface.dart | 2 +- .../test/plugin_platform_interface_test.dart | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/plugin_platform_interface/lib/plugin_platform_interface.dart b/packages/plugin_platform_interface/lib/plugin_platform_interface.dart index e914d2894065..6f78768c1ebe 100644 --- a/packages/plugin_platform_interface/lib/plugin_platform_interface.dart +++ b/packages/plugin_platform_interface/lib/plugin_platform_interface.dart @@ -95,4 +95,4 @@ abstract class MockPlatformInterfaceMixin implements PlatformInterface { @override Object get _instanceToken => _token; -} \ No newline at end of file +} diff --git a/packages/plugin_platform_interface/test/plugin_platform_interface_test.dart b/packages/plugin_platform_interface/test/plugin_platform_interface_test.dart index a33dcf4626dd..b8fc4ffae5dc 100644 --- a/packages/plugin_platform_interface/test/plugin_platform_interface_test.dart +++ b/packages/plugin_platform_interface/test/plugin_platform_interface_test.dart @@ -8,11 +8,10 @@ import 'package:test/test.dart'; import 'package:plugin_platform_interface/plugin_platform_interface.dart'; class SamplePluginPlatform extends PlatformInterface { - SamplePluginPlatform(): super(token: _token); + SamplePluginPlatform() : super(token: _token); static const Object _token = Object(); - static set instance(SamplePluginPlatform instance) { PlatformInterface.verifyToken(instance, _token); // A real implementation would set a static instance field here. @@ -37,7 +36,7 @@ void main() { test('Can be mocked with `implements`', () { final SamplePluginPlatform mock = - ImplementsSamplePluginPlatformUsingMockPlatformInterfaceMixin(); + ImplementsSamplePluginPlatformUsingMockPlatformInterfaceMixin(); SamplePluginPlatform.instance = mock; });