[pigeon] Initial integration test setup (#2851)

* [pigeon] Initial integration test setup

This sets up initial proof-of-concept integration tests using the new
shared native test harness:
- Integration tests on the Dart side for void->void and
  Everything->Everything calls.
- macOS implementations in the test plugin on the native side.
- A new test target in the test script to drive them via `flutter test`.
- A minimal change to the example app so that `flutter run`-ing it will
  test that the void->void call is wired up.

Since this simple initial test hit
https://github.com/flutter/flutter/issues/111083, which caused the test
to fail, this includes a fix for that.

Short-term future work (by me):
- Add integration test native setup and script targets for the other
  generators. This includes one just to keep the initial review scope
  smaller.
- Update https://github.com/flutter/packages/pull/2816 to include the
  integration test since it's still blocked until I can address the CI
  issues.

Medium-term future work (not all by me):
- Remove the legacy iOS e2e test scaffold that is currently disabled.
- Add significantly more integration test coverage (likely including
  https://github.com/flutter/flutter/issues/115168 to reduce redundant
  API setup), including Flutter API integration tests rather than just
  host API tests.

Part of https://github.com/flutter/flutter/issues/111505
Fixes https://github.com/flutter/flutter/issues/111083

* Version bump for bugfix

* Check in generated files needed for analysis

* Add the actual integration test file, which was left out

* Address review comments

* Fix incorrect Swift unit test for void call fix

* Analysis ignore

* Autoformat
diff --git a/packages/pigeon/CHANGELOG.md b/packages/pigeon/CHANGELOG.md
index 5cb7827..37b6da9 100644
--- a/packages/pigeon/CHANGELOG.md
+++ b/packages/pigeon/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 4.2.7
+
+* [swift] Fixes a bug when calling methods that return `void`.
+
 ## 4.2.6
 
 * Fixes bug with parsing documentation comments that start with '/'.
diff --git a/packages/pigeon/lib/generator_tools.dart b/packages/pigeon/lib/generator_tools.dart
index 23641d3..7916bda 100644
--- a/packages/pigeon/lib/generator_tools.dart
+++ b/packages/pigeon/lib/generator_tools.dart
@@ -9,7 +9,7 @@
 import 'ast.dart';
 
 /// The current version of pigeon. This must match the version in pubspec.yaml.
-const String pigeonVersion = '4.2.6';
+const String pigeonVersion = '4.2.7';
 
 /// Read all the content from [stdin] to a String.
 String readStdin() {
diff --git a/packages/pigeon/lib/swift_generator.dart b/packages/pigeon/lib/swift_generator.dart
index 758199d..92e3105 100644
--- a/packages/pigeon/lib/swift_generator.dart
+++ b/packages/pigeon/lib/swift_generator.dart
@@ -232,7 +232,7 @@
               indent.write('$call ');
               if (method.returnType.isVoid) {
                 indent.scoped('{', '}', () {
-                  indent.writeln('reply(nil)');
+                  indent.writeln('reply(wrapResult(nil))');
                 });
               } else {
                 indent.scoped('{ result in', '}', () {
@@ -242,7 +242,7 @@
             } else {
               if (method.returnType.isVoid) {
                 indent.writeln(call);
-                indent.writeln('reply(nil)');
+                indent.writeln('reply(wrapResult(nil))');
               } else {
                 indent.writeln('let result = $call');
                 indent.writeln('reply(wrapResult(result))');
diff --git a/packages/pigeon/platform_tests/test_plugin/example/integration_test/test.dart b/packages/pigeon/platform_tests/test_plugin/example/integration_test/test.dart
new file mode 100644
index 0000000..28a3716
--- /dev/null
+++ b/packages/pigeon/platform_tests/test_plugin/example/integration_test/test.dart
@@ -0,0 +1,79 @@
+// Copyright 2013 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.
+
+// TODO(a14n): remove this import once Flutter 3.1 or later reaches stable (including flutter/flutter#104231)
+// ignore: unnecessary_import
+import 'dart:typed_data';
+
+import 'package:flutter/foundation.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:integration_test/integration_test.dart';
+import 'package:test_plugin/all_datatypes.gen.dart';
+import 'package:test_plugin/all_void.gen.dart';
+
+void main() {
+  IntegrationTestWidgetsFlutterBinding.ensureInitialized();
+
+  group('Host API tests', () {
+    testWidgets('voidCallVoidReturn', (WidgetTester _) async {
+      final AllVoidHostApi api = AllVoidHostApi();
+
+      expect(api.doit(), completes);
+    });
+
+    testWidgets('allDataTypesEcho', (WidgetTester _) async {
+      final HostEverything api = HostEverything();
+
+      final Everything sentObject = Everything(
+        aBool: true,
+        anInt: 42,
+        aDouble: 3.14159,
+        aString: 'Hello host!',
+        aByteArray: Uint8List.fromList(<int>[1, 2, 3]),
+        a4ByteArray: Int32List.fromList(<int>[4, 5, 6]),
+        a8ByteArray: Int64List.fromList(<int>[7, 8, 9]),
+        aFloatArray: Float64List.fromList(<double>[2.71828, 3.14159]),
+        aList: <Object?>['Thing 1', 2],
+        aMap: <Object?, Object?>{'a': 1, 'b': 2.0},
+        nestedList: <List<bool>>[
+          <bool>[true, false],
+          <bool>[false, true]
+        ],
+      );
+
+      final Everything echoObject = await api.echo(sentObject);
+      expect(echoObject.aBool, sentObject.aBool);
+      expect(echoObject.anInt, sentObject.anInt);
+      expect(echoObject.aDouble, sentObject.aDouble);
+      expect(echoObject.aString, sentObject.aString);
+      // TODO(stuartmorgan): Enable these once they work for all generators;
+      // currently at least Swift is broken.
+      // See https://github.com/flutter/flutter/issues/115906
+      //expect(echoObject.aByteArray, sentObject.aByteArray);
+      //expect(echoObject.a4ByteArray, sentObject.a4ByteArray);
+      //expect(echoObject.a8ByteArray, sentObject.a8ByteArray);
+      //expect(echoObject.aFloatArray, sentObject.aFloatArray);
+      expect(listEquals(echoObject.aList, sentObject.aList), true);
+      expect(mapEquals(echoObject.aMap, sentObject.aMap), true);
+      expect(echoObject.nestedList?.length, sentObject.nestedList?.length);
+      // TODO(stuartmorgan): Enable this once the Dart types are fixed; see
+      // https://github.com/flutter/flutter/issues/116117
+      //for (int i = 0; i < echoObject.nestedList!.length; i++) {
+      //  expect(listEquals(echoObject.nestedList![i], sentObject.nestedList![i]),
+      //      true);
+      //}
+      expect(
+          mapEquals(
+              echoObject.mapWithAnnotations, sentObject.mapWithAnnotations),
+          true);
+      expect(
+          mapEquals(echoObject.mapWithObject, sentObject.mapWithObject), true);
+    });
+  });
+
+  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.
+  });
+}
diff --git a/packages/pigeon/platform_tests/test_plugin/example/ios/RunnerTests/AsyncHandlersTest.swift b/packages/pigeon/platform_tests/test_plugin/example/ios/RunnerTests/AsyncHandlersTest.swift
index 434b84c..817e04e 100644
--- a/packages/pigeon/platform_tests/test_plugin/example/ios/RunnerTests/AsyncHandlersTest.swift
+++ b/packages/pigeon/platform_tests/test_plugin/example/ios/RunnerTests/AsyncHandlersTest.swift
@@ -44,7 +44,7 @@
     let expectation = XCTestExpectation(description: "voidvoid callback")
     binaryMessenger.handlers[channelName]?(nil) { data in
       let outputMap = binaryMessenger.codec.decode(data) as? [String: Any]
-      XCTAssertNil(outputMap?["result"])
+      XCTAssertEqual(outputMap?["result"] as! NSNull, NSNull())
       XCTAssertNil(outputMap?["error"])
       expectation.fulfill()
     }
diff --git a/packages/pigeon/platform_tests/test_plugin/example/lib/main.dart b/packages/pigeon/platform_tests/test_plugin/example/lib/main.dart
index 4355287..e063cc9 100644
--- a/packages/pigeon/platform_tests/test_plugin/example/lib/main.dart
+++ b/packages/pigeon/platform_tests/test_plugin/example/lib/main.dart
@@ -5,6 +5,7 @@
 // ignore_for_file: public_member_api_docs
 
 import 'package:flutter/material.dart';
+import 'package:test_plugin/all_void.gen.dart';
 import 'package:test_plugin/test_plugin.dart';
 
 void main() {
@@ -21,6 +22,8 @@
 class _MyAppState extends State<MyApp> {
   // ignore: unused_field
   final TestPlugin _testPlugin = TestPlugin();
+  late final AllVoidHostApi api;
+  String status = 'Calling...';
 
   @override
   void initState() {
@@ -29,9 +32,18 @@
   }
 
   Future<void> initPlatformState() async {
-    // TODO(tarrinneal): Call TestPlugin methods here for manual integration
-    // testing, once they exist. See
-    // https://github.com/flutter/flutter/issues/111505
+    api = AllVoidHostApi();
+    try {
+      await api.doit();
+    } catch (e) {
+      setState(() {
+        status = 'Failed: $e';
+      });
+      return;
+    }
+    setState(() {
+      status = 'Success!';
+    });
   }
 
   @override
@@ -41,9 +53,8 @@
         appBar: AppBar(
           title: const Text('Pigeon integration tests'),
         ),
-        body: const Center(
-          child: Text(
-              'TODO, see https://github.com/flutter/flutter/issues/111505'),
+        body: Center(
+          child: Text(status),
         ),
       ),
     );
diff --git a/packages/pigeon/platform_tests/test_plugin/example/pubspec.yaml b/packages/pigeon/platform_tests/test_plugin/example/pubspec.yaml
index 632cdf9..1311268 100644
--- a/packages/pigeon/platform_tests/test_plugin/example/pubspec.yaml
+++ b/packages/pigeon/platform_tests/test_plugin/example/pubspec.yaml
@@ -14,6 +14,8 @@
 dev_dependencies:
   flutter_test:
     sdk: flutter
+  integration_test:
+    sdk: flutter
 
 flutter:
   uses-material-design: true
diff --git a/packages/pigeon/platform_tests/test_plugin/lib/.gitignore b/packages/pigeon/platform_tests/test_plugin/lib/.gitignore
index b81308f..2f74fbc 100644
--- a/packages/pigeon/platform_tests/test_plugin/lib/.gitignore
+++ b/packages/pigeon/platform_tests/test_plugin/lib/.gitignore
@@ -2,8 +2,7 @@
 # changes on generated files. This will need a way to avoid unnecessary churn,
 # such as a flag to suppress version stamp generation.
 *.gen.dart
-# TODO(stuartmorgan): Add exceptions for specific files that are used in
-# integration tests, as they will need to be checked in to avoid analysis
-# failures. The exclusion of other files is to prevent having multiple
-# copies of all of the Dart output until more tests are restructured to
-# minimize duplication.
+# The following are files that are used in integration tests, which need to be
+# checked in to avoid analysis failures.
+!all_datatypes.gen.dart
+!all_void.gen.dart
diff --git a/packages/pigeon/platform_tests/test_plugin/lib/all_datatypes.gen.dart b/packages/pigeon/platform_tests/test_plugin/lib/all_datatypes.gen.dart
new file mode 100644
index 0000000..dada870
--- /dev/null
+++ b/packages/pigeon/platform_tests/test_plugin/lib/all_datatypes.gen.dart
@@ -0,0 +1,245 @@
+// Copyright 2013 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.
+//
+// Autogenerated from Pigeon (v4.2.7), 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';
+import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List;
+
+import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer;
+import 'package:flutter/services.dart';
+
+class Everything {
+  Everything({
+    this.aBool,
+    this.anInt,
+    this.aDouble,
+    this.aString,
+    this.aByteArray,
+    this.a4ByteArray,
+    this.a8ByteArray,
+    this.aFloatArray,
+    this.aList,
+    this.aMap,
+    this.nestedList,
+    this.mapWithAnnotations,
+    this.mapWithObject,
+  });
+
+  bool? aBool;
+  int? anInt;
+  double? aDouble;
+  String? aString;
+  Uint8List? aByteArray;
+  Int32List? a4ByteArray;
+  Int64List? a8ByteArray;
+  Float64List? aFloatArray;
+  List<Object?>? aList;
+  Map<Object?, Object?>? aMap;
+  List<List<bool?>?>? nestedList;
+  Map<String?, String?>? mapWithAnnotations;
+  Map<String?, Object?>? mapWithObject;
+
+  Object encode() {
+    final Map<Object?, Object?> pigeonMap = <Object?, Object?>{};
+    pigeonMap['aBool'] = aBool;
+    pigeonMap['anInt'] = anInt;
+    pigeonMap['aDouble'] = aDouble;
+    pigeonMap['aString'] = aString;
+    pigeonMap['aByteArray'] = aByteArray;
+    pigeonMap['a4ByteArray'] = a4ByteArray;
+    pigeonMap['a8ByteArray'] = a8ByteArray;
+    pigeonMap['aFloatArray'] = aFloatArray;
+    pigeonMap['aList'] = aList;
+    pigeonMap['aMap'] = aMap;
+    pigeonMap['nestedList'] = nestedList;
+    pigeonMap['mapWithAnnotations'] = mapWithAnnotations;
+    pigeonMap['mapWithObject'] = mapWithObject;
+    return pigeonMap;
+  }
+
+  static Everything decode(Object message) {
+    final Map<Object?, Object?> pigeonMap = message as Map<Object?, Object?>;
+    return Everything(
+      aBool: pigeonMap['aBool'] as bool?,
+      anInt: pigeonMap['anInt'] as int?,
+      aDouble: pigeonMap['aDouble'] as double?,
+      aString: pigeonMap['aString'] as String?,
+      aByteArray: pigeonMap['aByteArray'] as Uint8List?,
+      a4ByteArray: pigeonMap['a4ByteArray'] as Int32List?,
+      a8ByteArray: pigeonMap['a8ByteArray'] as Int64List?,
+      aFloatArray: pigeonMap['aFloatArray'] as Float64List?,
+      aList: pigeonMap['aList'] as List<Object?>?,
+      aMap: pigeonMap['aMap'] as Map<Object?, Object?>?,
+      nestedList:
+          (pigeonMap['nestedList'] as List<Object?>?)?.cast<List<bool?>?>(),
+      mapWithAnnotations:
+          (pigeonMap['mapWithAnnotations'] as Map<Object?, Object?>?)
+              ?.cast<String?, String?>(),
+      mapWithObject: (pigeonMap['mapWithObject'] as Map<Object?, Object?>?)
+          ?.cast<String?, Object?>(),
+    );
+  }
+}
+
+class _HostEverythingCodec extends StandardMessageCodec {
+  const _HostEverythingCodec();
+  @override
+  void writeValue(WriteBuffer buffer, Object? value) {
+    if (value is Everything) {
+      buffer.putUint8(128);
+      writeValue(buffer, value.encode());
+    } else {
+      super.writeValue(buffer, value);
+    }
+  }
+
+  @override
+  Object? readValueOfType(int type, ReadBuffer buffer) {
+    switch (type) {
+      case 128:
+        return Everything.decode(readValue(buffer)!);
+
+      default:
+        return super.readValueOfType(type, buffer);
+    }
+  }
+}
+
+class HostEverything {
+  /// Constructor for [HostEverything].  The [binaryMessenger] named argument is
+  /// available for dependency injection.  If it is left null, the default
+  /// BinaryMessenger will be used which routes to the host platform.
+  HostEverything({BinaryMessenger? binaryMessenger})
+      : _binaryMessenger = binaryMessenger;
+  final BinaryMessenger? _binaryMessenger;
+
+  static const MessageCodec<Object?> codec = _HostEverythingCodec();
+
+  Future<Everything> giveMeEverything() async {
+    final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
+        'dev.flutter.pigeon.HostEverything.giveMeEverything', 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 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 Everything?)!;
+    }
+  }
+
+  Future<Everything> echo(Everything arg_everything) async {
+    final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
+        'dev.flutter.pigeon.HostEverything.echo', codec,
+        binaryMessenger: _binaryMessenger);
+    final Map<Object?, Object?>? replyMap =
+        await channel.send(<Object?>[arg_everything]) 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 Everything?)!;
+    }
+  }
+}
+
+class _FlutterEverythingCodec extends StandardMessageCodec {
+  const _FlutterEverythingCodec();
+  @override
+  void writeValue(WriteBuffer buffer, Object? value) {
+    if (value is Everything) {
+      buffer.putUint8(128);
+      writeValue(buffer, value.encode());
+    } else {
+      super.writeValue(buffer, value);
+    }
+  }
+
+  @override
+  Object? readValueOfType(int type, ReadBuffer buffer) {
+    switch (type) {
+      case 128:
+        return Everything.decode(readValue(buffer)!);
+
+      default:
+        return super.readValueOfType(type, buffer);
+    }
+  }
+}
+
+abstract class FlutterEverything {
+  static const MessageCodec<Object?> codec = _FlutterEverythingCodec();
+
+  Everything giveMeEverything();
+  Everything echo(Everything everything);
+  static void setup(FlutterEverything? api,
+      {BinaryMessenger? binaryMessenger}) {
+    {
+      final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
+          'dev.flutter.pigeon.FlutterEverything.giveMeEverything', codec,
+          binaryMessenger: binaryMessenger);
+      if (api == null) {
+        channel.setMessageHandler(null);
+      } else {
+        channel.setMessageHandler((Object? message) async {
+          // ignore message
+          final Everything output = api.giveMeEverything();
+          return output;
+        });
+      }
+    }
+    {
+      final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
+          'dev.flutter.pigeon.FlutterEverything.echo', codec,
+          binaryMessenger: binaryMessenger);
+      if (api == null) {
+        channel.setMessageHandler(null);
+      } else {
+        channel.setMessageHandler((Object? message) async {
+          assert(message != null,
+              'Argument for dev.flutter.pigeon.FlutterEverything.echo was null.');
+          final List<Object?> args = (message as List<Object?>?)!;
+          final Everything? arg_everything = (args[0] as Everything?);
+          assert(arg_everything != null,
+              'Argument for dev.flutter.pigeon.FlutterEverything.echo was null, expected non-null Everything.');
+          final Everything output = api.echo(arg_everything!);
+          return output;
+        });
+      }
+    }
+  }
+}
diff --git a/packages/pigeon/platform_tests/test_plugin/lib/all_void.gen.dart b/packages/pigeon/platform_tests/test_plugin/lib/all_void.gen.dart
new file mode 100644
index 0000000..aec71fd
--- /dev/null
+++ b/packages/pigeon/platform_tests/test_plugin/lib/all_void.gen.dart
@@ -0,0 +1,70 @@
+// Copyright 2013 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.
+//
+// Autogenerated from Pigeon (v4.2.7), 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';
+import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List;
+
+import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer;
+import 'package:flutter/services.dart';
+
+abstract class AllVoidFlutterApi {
+  static const MessageCodec<Object?> codec = StandardMessageCodec();
+
+  void doit();
+  static void setup(AllVoidFlutterApi? api,
+      {BinaryMessenger? binaryMessenger}) {
+    {
+      final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
+          'dev.flutter.pigeon.AllVoidFlutterApi.doit', codec,
+          binaryMessenger: binaryMessenger);
+      if (api == null) {
+        channel.setMessageHandler(null);
+      } else {
+        channel.setMessageHandler((Object? message) async {
+          // ignore message
+          api.doit();
+          return;
+        });
+      }
+    }
+  }
+}
+
+class AllVoidHostApi {
+  /// Constructor for [AllVoidHostApi].  The [binaryMessenger] named argument is
+  /// available for dependency injection.  If it is left null, the default
+  /// BinaryMessenger will be used which routes to the host platform.
+  AllVoidHostApi({BinaryMessenger? binaryMessenger})
+      : _binaryMessenger = binaryMessenger;
+  final BinaryMessenger? _binaryMessenger;
+
+  static const MessageCodec<Object?> codec = StandardMessageCodec();
+
+  Future<void> doit() async {
+    final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
+        'dev.flutter.pigeon.AllVoidHostApi.doit', 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;
+    }
+  }
+}
diff --git a/packages/pigeon/platform_tests/test_plugin/macos/Classes/TestPlugin.swift b/packages/pigeon/platform_tests/test_plugin/macos/Classes/TestPlugin.swift
index 699a7de..d713b4c 100644
--- a/packages/pigeon/platform_tests/test_plugin/macos/Classes/TestPlugin.swift
+++ b/packages/pigeon/platform_tests/test_plugin/macos/Classes/TestPlugin.swift
@@ -6,14 +6,30 @@
 import FlutterMacOS
 
 /**
- * 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/.
  */
-public class TestPlugin: NSObject, FlutterPlugin {
+public class TestPlugin: NSObject, FlutterPlugin, AllVoidHostApi, HostEverything {
   public static func register(with registrar: FlutterPluginRegistrar) {
+    let plugin = TestPlugin()
+    AllVoidHostApiSetup.setUp(binaryMessenger: registrar.messenger, api: plugin)
+    HostEverythingSetup.setUp(binaryMessenger: registrar.messenger, api: plugin)
   }
 
-  public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
-    result(FlutterMethodNotImplemented)
+  // MARK: AllVoidHostApi implementation
+
+  func doit() {
+    // No-op
+  }
+
+  // MARK: HostEverything implementation
+
+  func giveMeEverything() -> Everything {
+    // Currently unused in integration tests, so just return an empty object.
+    return Everything()
+  }
+
+  func echo(everything: Everything) -> Everything {
+    return everything
   }
 }
diff --git a/packages/pigeon/pubspec.yaml b/packages/pigeon/pubspec.yaml
index 639d3e1..baa21d2 100644
--- a/packages/pigeon/pubspec.yaml
+++ b/packages/pigeon/pubspec.yaml
@@ -2,7 +2,7 @@
 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.6 # This must match the version in lib/generator_tools.dart
+version: 4.2.7 # This must match the version in lib/generator_tools.dart
 
 environment:
   sdk: ">=2.12.0 <3.0.0"
diff --git a/packages/pigeon/run_tests.sh b/packages/pigeon/run_tests.sh
index 3089a45..b294e30 100755
--- a/packages/pigeon/run_tests.sh
+++ b/packages/pigeon/run_tests.sh
@@ -166,7 +166,11 @@
 }
 
 run_macos_swift_unittests() {
-  dart run tool/run_tests.dart -t mac_swift_unittests --skip-generation
+  dart run tool/run_tests.dart -t macos_swift_unittests --skip-generation
+}
+
+run_macos_swift_e2e_tests() {
+  dart run tool/run_tests.dart -t macos_swift_integration_tests --skip-generation
 }
 
 run_android_kotlin_unittests() {
@@ -255,6 +259,7 @@
 should_run_ios_swift_unittests=true
 should_run_mock_handler_tests=true
 should_run_macos_swift_unittests=true
+should_run_macos_swift_e2e_tests=true
 should_run_android_kotlin_unittests=true
 while getopts "t:l?h" opt; do
   case $opt in
@@ -268,6 +273,7 @@
     should_run_ios_swift_unittests=false
     should_run_mock_handler_tests=false
     should_run_macos_swift_unittests=false
+    should_run_macos_swift_e2e_tests=false
     should_run_android_kotlin_unittests=false
     case $OPTARG in
     # TODO(stuartmorgan): Rename to include "java".
@@ -281,6 +287,7 @@
     ios_swift_unittests) should_run_ios_swift_unittests=true ;;
     mock_handler_tests) should_run_mock_handler_tests=true ;;
     macos_swift_unittests) should_run_macos_swift_unittests=true ;;
+    macos_swift_e2e_tests) should_run_macos_swift_e2e_tests=true ;;
     android_kotlin_unittests) should_run_android_kotlin_unittests=true ;;
     *)
       echo "unrecognized test: $OPTARG"
@@ -300,6 +307,7 @@
   ios_swift_unittests      - Unit tests on generated Swift code.
   mock_handler_tests       - Unit tests on generated Dart mock handler code.
   macos_swift_unittests    - Unit tests on generated Swift code on macOS.
+  macos_swift_e2e_tests    - Integration tests on generated Swift code on macOS.
   "
     exit 1
     ;;
@@ -356,6 +364,9 @@
 if [ "$should_run_macos_swift_unittests" = true ]; then
   run_macos_swift_unittests
 fi
+if [ "$should_run_macos_swift_e2e_tests" = true ]; then
+  run_macos_swift_e2e_tests
+fi
 if [ "$should_run_android_kotlin_unittests" = true ]; then
   run_android_kotlin_unittests
 fi
diff --git a/packages/pigeon/tool/run_tests.dart b/packages/pigeon/tool/run_tests.dart
index b354ac1..afa0b98 100644
--- a/packages/pigeon/tool/run_tests.dart
+++ b/packages/pigeon/tool/run_tests.dart
@@ -24,7 +24,8 @@
 const String _listFlag = 'list';
 const String _skipGenerationFlag = 'skip-generation';
 
-const String testPluginRelativePath = 'platform_tests/test_plugin';
+const String _testPluginRelativePath = 'platform_tests/test_plugin';
+const String _integrationTestFileRelativePath = 'integration_test/test.dart';
 
 @immutable
 class _TestInfo {
@@ -52,18 +53,18 @@
   'flutter_unittests': _TestInfo(
       function: _runFlutterUnitTests,
       description: 'Unit tests on generated Dart code.'),
-  'ios_e2e_tests': _TestInfo(
-      function: _runIosE2eTests,
-      description: 'End-to-end Objective-C tests run on iOS Simulator'),
   'ios_unittests': _TestInfo(
       function: _runIosUnitTests,
       description: 'Unit tests on generated Objective-C code.'),
   'ios_swift_unittests': _TestInfo(
       function: _runIosSwiftUnitTests,
       description: 'Unit tests on generated Swift code.'),
-  'mac_swift_unittests': _TestInfo(
+  'macos_swift_unittests': _TestInfo(
       function: _runMacOSSwiftUnitTests,
       description: 'Unit tests on generated Swift code on macOS.'),
+  'macos_swift_integration_tests': _TestInfo(
+      function: _runMacOSSwiftIntegrationTests,
+      description: 'Integration tests on generated Swift code on macOS.'),
   'mock_handler_tests': _TestInfo(
       function: _runMockHandlerTests,
       description: 'Unit tests on generated Dart mock handler code.'),
@@ -74,7 +75,7 @@
 }
 
 Future<int> _runAndroidKotlinUnitTests() async {
-  const String examplePath = './$testPluginRelativePath/example';
+  const String examplePath = './$_testPluginRelativePath/example';
   const String androidProjectPath = '$examplePath/android';
   final File gradleFile = File(p.join(androidProjectPath, 'gradlew'));
   if (!gradleFile.existsSync()) {
@@ -179,16 +180,12 @@
   return 0;
 }
 
-Future<int> _runIosE2eTests() async {
-  throw UnimplementedError('See run_tests.sh.');
-}
-
 Future<int> _runIosUnitTests() async {
   throw UnimplementedError('See run_tests.sh.');
 }
 
 Future<int> _runMacOSSwiftUnitTests() async {
-  const String examplePath = './$testPluginRelativePath/example';
+  const String examplePath = './$_testPluginRelativePath/example';
   final int compileCode = await runFlutterBuild(examplePath, 'macos');
   if (compileCode != 0) {
     return compileCode;
@@ -200,8 +197,17 @@
   );
 }
 
+Future<int> _runMacOSSwiftIntegrationTests() async {
+  const String examplePath = './$_testPluginRelativePath/example';
+  return runFlutterCommand(
+    examplePath,
+    'test',
+    <String>[_integrationTestFileRelativePath, '-d', 'macos'],
+  );
+}
+
 Future<int> _runIosSwiftUnitTests() async {
-  const String examplePath = './$testPluginRelativePath/example';
+  const String examplePath = './$_testPluginRelativePath/example';
   final int compileCode = await runFlutterBuild(
     examplePath,
     'ios',
@@ -238,7 +244,7 @@
 }
 
 Future<int> _runWindowsUnitTests() async {
-  const String examplePath = './$testPluginRelativePath/example';
+  const String examplePath = './$_testPluginRelativePath/example';
   final int compileCode = await runFlutterBuild(examplePath, 'windows');
   if (compileCode != 0) {
     return compileCode;