[pigeon] Add initial FlutterApi integration tests (#2901)

* Add trivial async test as a baseline for the scaffolding

* Kotlin generator async void fix

* Add initial round-trip tests

* Windows fixes

* Cleanup

* Version bump for bug fix

* Fix analysis

* Version bump in generator
This commit is contained in:
stuartmorgan
2022-12-06 14:25:11 -05:00
committed by GitHub
parent a8809d7700
commit 8ed832731c
14 changed files with 429 additions and 31 deletions

View File

@ -1,3 +1,7 @@
## 4.2.9
* [kotlin] Fixes a bug with some methods that return `void`.
## 4.2.8
* Adds the ability to use `runWithOptions` entrypoint to allow external libraries to use the pigeon easier.

View File

@ -9,7 +9,7 @@ import 'dart:mirrors';
import 'ast.dart';
/// The current version of pigeon. This must match the version in pubspec.yaml.
const String pigeonVersion = '4.2.8';
const String pigeonVersion = '4.2.9';
/// Read all the content from [stdin] to a String.
String readStdin() {

View File

@ -233,15 +233,11 @@ void _writeHostApi(Indent indent, Api api, Root root) {
'api.${method.name}(${methodArgument.join(', ')})';
if (method.isAsynchronous) {
indent.write('$call ');
if (method.returnType.isVoid) {
indent.scoped('{', '}', () {
indent.writeln('reply.reply(null)');
});
} else {
indent.scoped('{', '}', () {
indent.writeln('reply.reply(wrapResult(it))');
});
}
final String resultValue =
method.returnType.isVoid ? 'null' : 'it';
indent.scoped('{', '}', () {
indent.writeln('reply.reply(wrapResult($resultValue))');
});
} else if (method.returnType.isVoid) {
indent.writeln(call);
indent.writeln('wrapped["${Keys.result}"] = null');

View File

@ -33,6 +33,8 @@ class AllTypesWrapper {
/// platform_test integration tests.
@HostApi()
abstract class HostIntegrationCoreApi {
// ========== Syncronous method tests ==========
/// A no-op function taking no arguments and returning no value, to sanity
/// test basic calling.
void noop();
@ -54,13 +56,31 @@ abstract class HostIntegrationCoreApi {
@ObjCSelector('createNestedObjectWithString:')
AllTypesWrapper createNestedString(String string);
// TODO(stuartmorgan): Add wrapper methods to trigger calls back into
// FlutterIntegrationCore methods, to allow Dart-driven integration testing
// of host->Dart calls. Each wrapper would be implemented by calling the
// corresponding FlutterIntegrationCore method, passing arguments and return
// values along unchanged. Since these will need to be async, we also need
// async host API tests here, so that failures in Dart->host async calling
// don't only show up here.
// ========== Asyncronous method tests ==========
/// A no-op function taking no arguments and returning no value, to sanity
/// test basic asynchronous calling.
@async
void noopAsync();
/// Returns the passed string asynchronously.
@async
@ObjCSelector('echoAsyncString:')
String echoAsyncString(String aString);
// ========== Flutter API test wrappers ==========
@async
void callFlutterNoop();
@async
@ObjCSelector('callFlutterEchoString:')
String callFlutterEchoString(String aString);
// TODO(stuartmorgan): Add callFlutterEchoString and the associated test once
// either https://github.com/flutter/flutter/issues/116117 is fixed, or the
// problematic type is moved out of AllTypes and into its own test, since
// the type mismatch breaks the second `encode` round.
}
/// The core interface that the Dart platform_test code implements for host
@ -74,6 +94,10 @@ abstract class FlutterIntegrationCoreApi {
/// Returns the passed object, to test serialization and deserialization.
@ObjCSelector('echoAllTypes:')
AllTypes echoAllTypes(AllTypes everything);
/// Returns the passed string, to test serialization and deserialization.
@ObjCSelector('echoString:')
String echoString(String aString);
}
/// An API that can be implemented for minimal, compile-only tests.

View File

@ -8,14 +8,19 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.example.alternate_language_test_plugin.CoreTests.AllTypes;
import com.example.alternate_language_test_plugin.CoreTests.AllTypesWrapper;
import com.example.alternate_language_test_plugin.CoreTests.FlutterIntegrationCoreApi;
import com.example.alternate_language_test_plugin.CoreTests.HostIntegrationCoreApi;
import com.example.alternate_language_test_plugin.CoreTests.Result;
import io.flutter.embedding.engine.plugins.FlutterPlugin;
/** This plugin handles the native side of the integration tests in example/integration_test/. */
public class AlternateLanguageTestPlugin implements FlutterPlugin, HostIntegrationCoreApi {
@Nullable FlutterIntegrationCoreApi flutterApi = null;
@Override
public void onAttachedToEngine(@NonNull FlutterPluginBinding binding) {
HostIntegrationCoreApi.setup(binding.getBinaryMessenger(), this);
flutterApi = new FlutterIntegrationCoreApi(binding.getBinaryMessenger());
}
@Override
@ -46,4 +51,35 @@ public class AlternateLanguageTestPlugin implements FlutterPlugin, HostIntegrati
AllTypes innerObject = new AllTypes.Builder().setAString(string).build();
return new AllTypesWrapper.Builder().setValues(innerObject).build();
}
@Override
public void noopAsync(Result<Void> result) {
result.success(null);
}
@Override
public void echoAsyncString(@NonNull String aString, Result<String> result) {
result.success(aString);
}
@Override
public void callFlutterNoop(Result<Void> result) {
flutterApi.noop(
new FlutterIntegrationCoreApi.Reply<Void>() {
public void reply(Void value) {
result.success(value);
}
});
}
@Override
public void callFlutterEchoString(@NonNull String aString, Result<String> result) {
flutterApi.echoString(
aString,
new FlutterIntegrationCoreApi.Reply<String>() {
public void reply(String value) {
result.success(value);
}
});
}
}

View File

@ -6,14 +6,19 @@
#import "CoreTests.gen.h"
@interface AlternateLanguageTestPlugin ()
@property(nonatomic) FlutterIntegrationCoreApi *flutterAPI;
@end
/**
* This plugin is currently a no-op since only unit tests have been set up.
* In the future, this will register Pigeon APIs used in integration tests.
* This plugin handles the native side of the integration tests in example/integration_test/.
*/
@implementation AlternateLanguageTestPlugin
+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar> *)registrar {
AlternateLanguageTestPlugin *plugin = [[AlternateLanguageTestPlugin alloc] init];
HostIntegrationCoreApiSetup(registrar.messenger, plugin);
HostIntegrationCoreApiSetup([registrar messenger], plugin);
plugin.flutterAPI =
[[FlutterIntegrationCoreApi alloc] initWithBinaryMessenger:[registrar messenger]];
}
#pragma mark HostIntegrationCoreApi implementation
@ -43,4 +48,27 @@
return [AllTypesWrapper makeWithValues:innerObject];
}
- (void)noopAsyncWithCompletion:(void (^)(FlutterError *_Nullable))completion {
completion(nil);
}
- (void)echoAsyncString:(NSString *)aString
completion:(void (^)(NSString *_Nullable, FlutterError *_Nullable))completion {
completion(aString, nil);
}
- (void)callFlutterNoopWithCompletion:(void (^)(FlutterError *_Nullable))completion {
[self.flutterAPI noopWithCompletion:^(NSError *error) {
completion(error);
}];
}
- (void)callFlutterEchoString:(NSString *)aString
completion:(void (^)(NSString *_Nullable, FlutterError *_Nullable))completion {
[self.flutterAPI echoString:aString
completion:^(NSString *value, NSError *error) {
completion(value, error);
}];
}
@end

View File

@ -35,7 +35,7 @@ enum TargetGenerator {
void runPigeonIntegrationTests(TargetGenerator targetGenerator) {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('Host API tests', () {
group('Host sync API tests', () {
testWidgets('basic void->void call works', (WidgetTester _) async {
final HostIntegrationCoreApi api = HostIntegrationCoreApi();
@ -125,8 +125,65 @@ void runPigeonIntegrationTests(TargetGenerator targetGenerator) {
});
});
group('Flutter API tests', () {
// TODO(stuartmorgan): Add Flutter API tests, driven by wrapper host APIs
// that forward the arguments and return values in the opposite direction.
group('Host async API tests', () {
testWidgets('basic void->void call works', (WidgetTester _) async {
final HostIntegrationCoreApi api = HostIntegrationCoreApi();
expect(api.noopAsync(), completes);
});
testWidgets('strings serialize and deserialize correctly',
(WidgetTester _) async {
final HostIntegrationCoreApi api = HostIntegrationCoreApi();
const String sentObject = 'Hello, asyncronously!';
final String echoObject = await api.echoAsyncString(sentObject);
expect(echoObject, sentObject);
});
});
// These tests rely on the ansync Dart->host calls to work correctly, since
// the host->Dart call is wrapped in a driving Dart->host call, so any test
// added to this group should have coverage of the relevant arguments and
// return value in the "Host async API tests" group.
group('Flutter API tests', () {
setUp(() {
FlutterIntegrationCoreApi.setup(_FlutterApiTestImplementation());
});
testWidgets('basic void->void call works', (WidgetTester _) async {
final HostIntegrationCoreApi api = HostIntegrationCoreApi();
expect(api.callFlutterNoop(), completes);
});
testWidgets('strings serialize and deserialize correctly',
(WidgetTester _) async {
final HostIntegrationCoreApi api = HostIntegrationCoreApi();
const String sentObject = 'Hello Dart!';
final String echoObject = await api.callFlutterEchoString(sentObject);
expect(echoObject, sentObject);
});
},
// TODO(stuartmorgan): Enable when FlutterApi generation is fixed for
// C++. See https://github.com/flutter/flutter/issues/108682.
skip: targetGenerator == TargetGenerator.cpp);
}
class _FlutterApiTestImplementation implements FlutterIntegrationCoreApi {
@override
AllTypes echoAllTypes(AllTypes everything) {
return everything;
}
@override
String echoString(String aString) {
return aString;
}
@override
void noop() {}
}

View File

@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
//
// Autogenerated from Pigeon (v4.2.8), do not edit directly.
// Autogenerated from Pigeon (v4.2.9), do not edit directly.
// See also: https://pub.dev/packages/pigeon
// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import
import 'dart:async';
@ -284,6 +284,116 @@ class HostIntegrationCoreApi {
return (replyMap['result'] as AllTypesWrapper?)!;
}
}
/// A no-op function taking no arguments and returning no value, to sanity
/// test basic asynchronous calling.
Future<void> noopAsync() async {
final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
'dev.flutter.pigeon.HostIntegrationCoreApi.noopAsync', codec,
binaryMessenger: _binaryMessenger);
final Map<Object?, Object?>? replyMap =
await channel.send(null) as Map<Object?, Object?>?;
if (replyMap == null) {
throw PlatformException(
code: 'channel-error',
message: 'Unable to establish connection on channel.',
);
} else if (replyMap['error'] != null) {
final Map<Object?, Object?> error =
(replyMap['error'] as Map<Object?, Object?>?)!;
throw PlatformException(
code: (error['code'] as String?)!,
message: error['message'] as String?,
details: error['details'],
);
} else {
return;
}
}
/// Returns the passed string asynchronously.
Future<String> echoAsyncString(String arg_aString) async {
final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
'dev.flutter.pigeon.HostIntegrationCoreApi.echoAsyncString', codec,
binaryMessenger: _binaryMessenger);
final Map<Object?, Object?>? replyMap =
await channel.send(<Object?>[arg_aString]) as Map<Object?, Object?>?;
if (replyMap == null) {
throw PlatformException(
code: 'channel-error',
message: 'Unable to establish connection on channel.',
);
} else if (replyMap['error'] != null) {
final Map<Object?, Object?> error =
(replyMap['error'] as Map<Object?, Object?>?)!;
throw PlatformException(
code: (error['code'] as String?)!,
message: error['message'] as String?,
details: error['details'],
);
} else if (replyMap['result'] == null) {
throw PlatformException(
code: 'null-error',
message: 'Host platform returned null value for non-null return value.',
);
} else {
return (replyMap['result'] as String?)!;
}
}
Future<void> callFlutterNoop() async {
final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
'dev.flutter.pigeon.HostIntegrationCoreApi.callFlutterNoop', codec,
binaryMessenger: _binaryMessenger);
final Map<Object?, Object?>? replyMap =
await channel.send(null) as Map<Object?, Object?>?;
if (replyMap == null) {
throw PlatformException(
code: 'channel-error',
message: 'Unable to establish connection on channel.',
);
} else if (replyMap['error'] != null) {
final Map<Object?, Object?> error =
(replyMap['error'] as Map<Object?, Object?>?)!;
throw PlatformException(
code: (error['code'] as String?)!,
message: error['message'] as String?,
details: error['details'],
);
} else {
return;
}
}
Future<String> callFlutterEchoString(String arg_aString) async {
final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
'dev.flutter.pigeon.HostIntegrationCoreApi.callFlutterEchoString',
codec,
binaryMessenger: _binaryMessenger);
final Map<Object?, Object?>? replyMap =
await channel.send(<Object?>[arg_aString]) as Map<Object?, Object?>?;
if (replyMap == null) {
throw PlatformException(
code: 'channel-error',
message: 'Unable to establish connection on channel.',
);
} else if (replyMap['error'] != null) {
final Map<Object?, Object?> error =
(replyMap['error'] as Map<Object?, Object?>?)!;
throw PlatformException(
code: (error['code'] as String?)!,
message: error['message'] as String?,
details: error['details'],
);
} else if (replyMap['result'] == null) {
throw PlatformException(
code: 'null-error',
message: 'Host platform returned null value for non-null return value.',
);
} else {
return (replyMap['result'] as String?)!;
}
}
}
class _FlutterIntegrationCoreApiCodec extends StandardMessageCodec {
@ -321,6 +431,9 @@ abstract class FlutterIntegrationCoreApi {
/// Returns the passed object, to test serialization and deserialization.
AllTypes echoAllTypes(AllTypes everything);
/// Returns the passed string, to test serialization and deserialization.
String echoString(String aString);
static void setup(FlutterIntegrationCoreApi? api,
{BinaryMessenger? binaryMessenger}) {
{
@ -356,6 +469,25 @@ abstract class FlutterIntegrationCoreApi {
});
}
}
{
final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
'dev.flutter.pigeon.FlutterIntegrationCoreApi.echoString', codec,
binaryMessenger: binaryMessenger);
if (api == null) {
channel.setMessageHandler(null);
} else {
channel.setMessageHandler((Object? message) async {
assert(message != null,
'Argument for dev.flutter.pigeon.FlutterIntegrationCoreApi.echoString was null.');
final List<Object?> args = (message as List<Object?>?)!;
final String? arg_aString = (args[0] as String?);
assert(arg_aString != null,
'Argument for dev.flutter.pigeon.FlutterIntegrationCoreApi.echoString was null, expected non-null String.');
final String output = api.echoString(arg_aString!);
return output;
});
}
}
}
}

View File

@ -17,8 +17,11 @@ import io.flutter.plugin.common.MethodChannel.Result
* example/integration_test/.
*/
class TestPlugin: FlutterPlugin, HostIntegrationCoreApi {
var flutterApi: FlutterIntegrationCoreApi? = null
override fun onAttachedToEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
HostIntegrationCoreApi.setUp(binding.getBinaryMessenger(), this)
flutterApi = FlutterIntegrationCoreApi(binding.getBinaryMessenger())
}
override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
@ -44,4 +47,20 @@ class TestPlugin: FlutterPlugin, HostIntegrationCoreApi {
override fun createNestedString(string: String): AllTypesWrapper {
return AllTypesWrapper(AllTypes(aString = string))
}
override fun noopAsync(callback: () -> Unit) {
callback()
}
override fun echoAsyncString(aString: String, callback: (String) -> Unit) {
callback(aString)
}
override fun callFlutterNoop(callback: () -> Unit) {
flutterApi!!.noop() { callback() }
}
override fun callFlutterEchoString(aString: String, callback: (String) -> Unit) {
flutterApi!!.echoString(aString) { flutterString -> callback(flutterString) }
}
}

View File

@ -10,11 +10,17 @@ import UIKit
* example/integration_test/.
*/
public class TestPlugin: NSObject, FlutterPlugin, HostIntegrationCoreApi {
var flutterAPI: FlutterIntegrationCoreApi
public static func register(with registrar: FlutterPluginRegistrar) {
let plugin = TestPlugin()
let plugin = TestPlugin(binaryMessenger: registrar.messenger())
HostIntegrationCoreApiSetup.setUp(binaryMessenger: registrar.messenger(), api: plugin)
}
init(binaryMessenger: FlutterBinaryMessenger) {
flutterAPI = FlutterIntegrationCoreApi(binaryMessenger: binaryMessenger)
}
// MARK: HostIntegrationCoreApi implementation
func noop() {
@ -36,4 +42,24 @@ public class TestPlugin: NSObject, FlutterPlugin, HostIntegrationCoreApi {
func createNestedString(string: String) -> AllTypesWrapper {
return AllTypesWrapper(values: AllTypes(aString: string))
}
func noopAsync(completion: @escaping () -> Void) {
completion()
}
func echoAsyncString(aString: String, completion: @escaping (String) -> Void) {
completion(aString)
}
func callFlutterNoop(completion: @escaping () -> Void) {
flutterAPI.noop() {
completion()
}
}
func callFlutterEchoString(aString: String, completion: @escaping (String) -> Void) {
flutterAPI.echoString(aString: aString) { flutterString in
completion(flutterString)
}
}
}

View File

@ -10,11 +10,17 @@ import FlutterMacOS
* example/integration_test/.
*/
public class TestPlugin: NSObject, FlutterPlugin, HostIntegrationCoreApi {
var flutterAPI: FlutterIntegrationCoreApi
public static func register(with registrar: FlutterPluginRegistrar) {
let plugin = TestPlugin()
let plugin = TestPlugin(binaryMessenger: registrar.messenger)
HostIntegrationCoreApiSetup.setUp(binaryMessenger: registrar.messenger, api: plugin)
}
init(binaryMessenger: FlutterBinaryMessenger) {
flutterAPI = FlutterIntegrationCoreApi(binaryMessenger: binaryMessenger)
}
// MARK: HostIntegrationCoreApi implementation
func noop() {
@ -36,4 +42,24 @@ public class TestPlugin: NSObject, FlutterPlugin, HostIntegrationCoreApi {
func createNestedString(string: String) -> AllTypesWrapper {
return AllTypesWrapper(values: AllTypes(aString: string))
}
func noopAsync(completion: @escaping () -> Void) {
completion()
}
func echoAsyncString(aString: String, completion: @escaping (String) -> Void) {
completion(aString)
}
func callFlutterNoop(completion: @escaping () -> Void) {
flutterAPI.noop() {
completion()
}
}
func callFlutterEchoString(aString: String, completion: @escaping (String) -> Void) {
flutterAPI.echoString(aString: aString) { flutterString in
completion(flutterString)
}
}
}

View File

@ -9,6 +9,8 @@
#include <windows.h>
#include <memory>
#include <optional>
#include <string>
#include "pigeon/core_tests.gen.h"
@ -18,19 +20,22 @@ using core_tests_pigeontest::AllTypes;
using core_tests_pigeontest::AllTypesWrapper;
using core_tests_pigeontest::ErrorOr;
using core_tests_pigeontest::FlutterError;
using core_tests_pigeontest::FlutterIntegrationCoreApi;
using core_tests_pigeontest::HostIntegrationCoreApi;
// static
void TestPlugin::RegisterWithRegistrar(
flutter::PluginRegistrarWindows* registrar) {
auto plugin = std::make_unique<TestPlugin>();
auto plugin = std::make_unique<TestPlugin>(registrar->messenger());
HostIntegrationCoreApi::SetUp(registrar->messenger(), plugin.get());
registrar->AddPlugin(std::move(plugin));
}
TestPlugin::TestPlugin() {}
TestPlugin::TestPlugin(flutter::BinaryMessenger* binary_messenger)
: flutter_api_(
std::make_unique<FlutterIntegrationCoreApi>(binary_messenger)) {}
TestPlugin::~TestPlugin() {}
@ -60,4 +65,28 @@ ErrorOr<AllTypesWrapper> TestPlugin::CreateNestedString(
return wrapper;
}
void TestPlugin::NoopAsync(
std::function<void(std::optional<FlutterError> reply)> result) {
result(std::nullopt);
}
void TestPlugin::EchoAsyncString(
const std::string& a_string,
std::function<void(ErrorOr<std::string> reply)> result) {
result(a_string);
}
void TestPlugin::CallFlutterNoop(
std::function<void(std::optional<FlutterError> reply)> result) {
flutter_api_->noop([result]() { result(std::nullopt); });
}
void TestPlugin::CallFlutterEchoString(
const std::string& a_string,
std::function<void(ErrorOr<std::string> reply)> result) {
flutter_api_->echoString(
a_string,
[result](const std::string& flutter_string) { result(flutter_string); });
}
} // namespace test_plugin

View File

@ -9,6 +9,8 @@
#include <flutter/plugin_registrar_windows.h>
#include <memory>
#include <optional>
#include <string>
#include "pigeon/core_tests.gen.h"
@ -21,7 +23,7 @@ class TestPlugin : public flutter::Plugin,
public:
static void RegisterWithRegistrar(flutter::PluginRegistrarWindows* registrar);
TestPlugin();
TestPlugin(flutter::BinaryMessenger* binary_messenger);
virtual ~TestPlugin();
@ -39,6 +41,25 @@ class TestPlugin : public flutter::Plugin,
const core_tests_pigeontest::AllTypesWrapper& wrapper) override;
core_tests_pigeontest::ErrorOr<core_tests_pigeontest::AllTypesWrapper>
CreateNestedString(const std::string& string) override;
void NoopAsync(std::function<
void(std::optional<core_tests_pigeontest::FlutterError> reply)>
result) override;
void EchoAsyncString(
const std::string& a_string,
std::function<void(core_tests_pigeontest::ErrorOr<std::string> reply)>
result) override;
void CallFlutterNoop(
std::function<
void(std::optional<core_tests_pigeontest::FlutterError> reply)>
result) override;
void CallFlutterEchoString(
const std::string& a_string,
std::function<void(core_tests_pigeontest::ErrorOr<std::string> reply)>
result) override;
private:
std::unique_ptr<core_tests_pigeontest::FlutterIntegrationCoreApi>
flutter_api_;
};
} // namespace test_plugin

View File

@ -2,7 +2,7 @@ name: pigeon
description: Code generator tool to make communication between Flutter and the host platform type-safe and easier.
repository: https://github.com/flutter/packages/tree/main/packages/pigeon
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3Apigeon
version: 4.2.8 # This must match the version in lib/generator_tools.dart
version: 4.2.9 # This must match the version in lib/generator_tools.dart
environment:
sdk: ">=2.12.0 <3.0.0"