pw_rpc: Generic test for Method and MethodTraits implementations

Change-Id: Iaef3a3b15cbd1c9f6fb7e2f897011f02592466cb
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/25642
Commit-Queue: Wyatt Hepler <hepler@google.com>
Reviewed-by: Keir Mierle <keir@google.com>
diff --git a/pw_rpc/BUILD b/pw_rpc/BUILD
index 260f2d3..fe34d2d 100644
--- a/pw_rpc/BUILD
+++ b/pw_rpc/BUILD
@@ -87,6 +87,7 @@
     hdrs = [
         "public/pw_rpc/internal/test_method.h",
         "pw_rpc_private/internal_test_utils.h",
+        "pw_rpc_private/method_impl_tester.h",
     ],
     visibility = ["//visibility:private"],
     deps = [
diff --git a/pw_rpc/BUILD.gn b/pw_rpc/BUILD.gn
index a04c2e6..1dee6ca 100644
--- a/pw_rpc/BUILD.gn
+++ b/pw_rpc/BUILD.gn
@@ -90,6 +90,7 @@
   public = [
     "public/pw_rpc/internal/test_method.h",
     "pw_rpc_private/internal_test_utils.h",
+    "pw_rpc_private/method_impl_tester.h",
   ]
   public_configs = [ ":private_includes" ]
   public_deps = [
diff --git a/pw_rpc/nanopb/nanopb_method_test.cc b/pw_rpc/nanopb/nanopb_method_test.cc
index 6c0b736..98e3d54 100644
--- a/pw_rpc/nanopb/nanopb_method_test.cc
+++ b/pw_rpc/nanopb/nanopb_method_test.cc
@@ -22,6 +22,7 @@
 #include "pw_rpc/service.h"
 #include "pw_rpc_nanopb_private/internal_test_utils.h"
 #include "pw_rpc_private/internal_test_utils.h"
+#include "pw_rpc_private/method_impl_tester.h"
 #include "pw_rpc_test_protos/test.pb.h"
 
 namespace pw::rpc::internal {
@@ -29,6 +30,43 @@
 
 using std::byte;
 
+struct FakePb {};
+
+// Create a fake service for use with the MethodImplTester.
+class TestNanopbService final : public Service {
+ public:
+  Status Unary(ServerContext&, const FakePb&, FakePb&) { return Status(); }
+
+  static Status StaticUnary(ServerContext&, const FakePb&, FakePb&) {
+    return Status();
+  }
+
+  void ServerStreaming(ServerContext&, const FakePb&, ServerWriter<FakePb>&) {}
+
+  static void StaticServerStreaming(ServerContext&,
+                                    const FakePb&,
+                                    ServerWriter<FakePb>&) {}
+
+  Status UnaryWrongArg(ServerContext&, FakePb&, FakePb&) { return Status(); }
+
+  static void StaticUnaryVoidReturn(ServerContext&, const FakePb&, FakePb&) {}
+
+  int ServerStreamingBadReturn(ServerContext&,
+                               const FakePb&,
+                               ServerWriter<FakePb>&) {
+    return 5;
+  }
+
+  static void StaticServerStreamingMissingArg(const FakePb&,
+                                              ServerWriter<FakePb>&) {}
+};
+
+TEST(MethodImplTester, NanopbMethod) {
+  constexpr MethodImplTester<NanopbMethod, TestNanopbService, nullptr, nullptr>
+      method_tester;
+  EXPECT_TRUE(method_tester.MethodImplIsValid());
+}
+
 pw_rpc_test_TestRequest last_request;
 ServerWriter<pw_rpc_test_TestResponse> last_writer;
 
diff --git a/pw_rpc/pw_rpc_private/method_impl_tester.h b/pw_rpc/pw_rpc_private/method_impl_tester.h
new file mode 100644
index 0000000..13ef880
--- /dev/null
+++ b/pw_rpc/pw_rpc_private/method_impl_tester.h
@@ -0,0 +1,106 @@
+// Copyright 2020 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#pragma once
+
+#include <tuple>
+#include <type_traits>
+
+#include "gtest/gtest.h"
+#include "pw_rpc/internal/packet.h"
+#include "pw_rpc/internal/raw_method.h"
+#include "pw_rpc/server_context.h"
+
+namespace pw::rpc::internal {
+namespace {
+
+// This class tests Method implementation classes and MethodTraits
+// specializations. It verifies that they provide the expected functions and
+// that they correctly identify and construct the various method types.
+//
+// The TestService class must inherit from Service and provide the following
+// methods with valid signatures for RPCs:
+//
+//   - Unary: a valid unary RPC member function
+//   - StaticUnary: valid unary RPC static member function
+//   - ServerStreaming: valid server streaming RPC member function
+//   - StaticServerStreaming: valid server streaming static RPC member function
+//
+// The class must also provide methods with errors as described in their names:
+//
+//   - UnaryWrongArg
+//   - StaticUnaryVoidReturn
+//   - ServerStreamingBadReturn
+//   - StaticServerStreamingMissingArg
+//
+template <typename MethodImpl, typename TestService, auto... extra_method_args>
+struct MethodImplTester {
+  // Test that the matches() function matches valid signatures.
+  static_assert(MethodImpl::template matches<&TestService::Unary>());
+  static_assert(MethodImpl::template matches<&TestService::ServerStreaming>());
+  static_assert(MethodImpl::template matches<&TestService::StaticUnary>());
+  static_assert(
+      MethodImpl::template matches<&TestService::StaticServerStreaming>());
+
+  // Test that the matches() function doesn't match invalid signatures.
+  static_assert(!MethodImpl::template matches<&TestService::UnaryWrongArg>());
+  static_assert(
+      !MethodImpl::template matches<&TestService::StaticUnaryVoidReturn>());
+  static_assert(
+      !MethodImpl::template matches<&TestService::ServerStreamingBadReturn>());
+  static_assert(!MethodImpl::template matches<
+                &TestService::StaticServerStreamingMissingArg>());
+
+  // Test the MethodTraits::kType member.
+  static_assert(MethodTraits<decltype(&TestService::Unary)>::kType ==
+                MethodType::kUnary);
+  static_assert(MethodTraits<decltype(&TestService::StaticUnary)>::kType ==
+                MethodType::kUnary);
+  static_assert(MethodTraits<decltype(&TestService::ServerStreaming)>::kType ==
+                MethodType::kServerStreaming);
+  static_assert(
+      MethodTraits<decltype(&TestService::StaticServerStreaming)>::kType ==
+      MethodType::kServerStreaming);
+
+  // Test method creation.
+  static constexpr MethodImpl kUnaryMethod =
+      MethodImpl::template Unary<&TestService::Unary>(1, extra_method_args...);
+  static_assert(kUnaryMethod.id() == 1);
+
+  static constexpr MethodImpl kStaticUnaryMethod =
+      MethodImpl::template Unary<&TestService::StaticUnary>(
+          2, extra_method_args...);
+  static_assert(kStaticUnaryMethod.id() == 2);
+
+  static constexpr MethodImpl kServerStreamingMethod =
+      MethodImpl::template ServerStreaming<&TestService::ServerStreaming>(
+          3, extra_method_args...);
+  static_assert(kServerStreamingMethod.id() == 3);
+
+  static constexpr MethodImpl kStaticServerStreamingMethod =
+      MethodImpl::template ServerStreaming<&TestService::StaticServerStreaming>(
+          4, extra_method_args...);
+  static_assert(kStaticServerStreamingMethod.id() == 4);
+
+  // Test that there is an Invalid method creation function.
+  static constexpr MethodImpl kInvalidMethod = MethodImpl::Invalid();
+  static_assert(kInvalidMethod.id() == 0);
+
+  // Provide a method that tests can call to ensure this class is instantiated.
+  bool MethodImplIsValid() const {
+    return true;  // If this class compiles, the MethodImpl passes this test.
+  }
+};
+
+}  // namespace
+}  // namespace pw::rpc::internal
diff --git a/pw_rpc/raw/raw_method_test.cc b/pw_rpc/raw/raw_method_test.cc
index c2dfb3e..0008222 100644
--- a/pw_rpc/raw/raw_method_test.cc
+++ b/pw_rpc/raw/raw_method_test.cc
@@ -24,11 +24,50 @@
 #include "pw_rpc/server_context.h"
 #include "pw_rpc/service.h"
 #include "pw_rpc_private/internal_test_utils.h"
+#include "pw_rpc_private/method_impl_tester.h"
 #include "pw_rpc_test_protos/test.pwpb.h"
 
 namespace pw::rpc::internal {
 namespace {
 
+// Create a fake service for use with the MethodImplTester.
+class TestRawService final : public Service {
+ public:
+  StatusWithSize Unary(ServerContext&, ConstByteSpan, ByteSpan) {
+    return StatusWithSize(0);
+  }
+
+  static StatusWithSize StaticUnary(ServerContext&, ConstByteSpan, ByteSpan) {
+    return StatusWithSize(0);
+  }
+
+  void ServerStreaming(ServerContext&, ConstByteSpan, RawServerWriter&) {}
+
+  static void StaticServerStreaming(ServerContext&,
+                                    ConstByteSpan,
+                                    RawServerWriter&) {}
+
+  StatusWithSize UnaryWrongArg(ServerContext&, ConstByteSpan, ConstByteSpan) {
+    return StatusWithSize(0);
+  }
+
+  static void StaticUnaryVoidReturn(ServerContext&, ConstByteSpan, ByteSpan) {}
+
+  Status ServerStreamingBadReturn(ServerContext&,
+                                  ConstByteSpan,
+                                  RawServerWriter&) {
+    return Status();
+  }
+
+  static void StaticServerStreamingMissingArg(ConstByteSpan, RawServerWriter&) {
+  }
+};
+
+TEST(MethodImplTester, RawMethod) {
+  constexpr MethodImplTester<RawMethod, TestRawService> method_tester;
+  EXPECT_TRUE(method_tester.MethodImplIsValid());
+}
+
 struct {
   int64_t integer;
   uint32_t status_code;