// 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. #include "pw_rpc/internal/nanopb_method.h" #include #include "gtest/gtest.h" #include "pw_rpc/internal/nanopb_method_union.h" #include "pw_rpc/server_context.h" #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 { namespace { 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&) {} static void StaticServerStreaming(ServerContext&, const FakePb&, ServerWriter&) {} Status UnaryWrongArg(ServerContext&, FakePb&, FakePb&) { return Status(); } static void StaticUnaryVoidReturn(ServerContext&, const FakePb&, FakePb&) {} int ServerStreamingBadReturn(ServerContext&, const FakePb&, ServerWriter&) { return 5; } static void StaticServerStreamingMissingArg(const FakePb&, ServerWriter&) {} }; // Test that the matches() function matches valid signatures. static_assert(NanopbMethod::template matches<&TestNanopbService::Unary, FakePb, FakePb>()); static_assert( NanopbMethod::template matches<&TestNanopbService::ServerStreaming, FakePb, FakePb>()); static_assert(NanopbMethod::template matches<&TestNanopbService::StaticUnary, FakePb, FakePb>()); static_assert( NanopbMethod::template matches<&TestNanopbService::StaticServerStreaming, FakePb, FakePb>()); // Test that the matches() function does not match the wrong method type. static_assert(!NanopbMethod::template matches<&TestNanopbService::UnaryWrongArg, FakePb, FakePb>()); static_assert( !NanopbMethod::template matches<&TestNanopbService::StaticUnaryVoidReturn, FakePb, FakePb>()); static_assert(!NanopbMethod::template matches< &TestNanopbService::ServerStreamingBadReturn, FakePb, FakePb>()); static_assert(!NanopbMethod::template matches< &TestNanopbService::StaticServerStreamingMissingArg, FakePb, FakePb>()); struct WrongPb; // Test matches() rejects incorrect request/response types. static_assert(!NanopbMethod::template matches<&TestNanopbService::Unary, WrongPb, FakePb>()); static_assert(!NanopbMethod::template matches<&TestNanopbService::Unary, FakePb, WrongPb>()); static_assert(!NanopbMethod::template matches<&TestNanopbService::Unary, WrongPb, WrongPb>()); static_assert( !NanopbMethod::template matches<&TestNanopbService::ServerStreaming, WrongPb, FakePb>()); static_assert(!NanopbMethod::template matches<&TestNanopbService::StaticUnary, FakePb, WrongPb>()); static_assert( !NanopbMethod::template matches<&TestNanopbService::StaticServerStreaming, FakePb, WrongPb>()); TEST(MethodImplTester, NanopbMethod) { constexpr MethodImplTester method_tester; EXPECT_TRUE(method_tester.MethodImplIsValid()); } pw_rpc_test_TestRequest last_request; ServerWriter last_writer; Status AddFive(ServerContext&, const pw_rpc_test_TestRequest& request, pw_rpc_test_TestResponse& response) { last_request = request; response.value = request.integer + 5; return Status::Unauthenticated(); } Status DoNothing(ServerContext&, const pw_rpc_test_Empty&, pw_rpc_test_Empty&) { return Status::Unknown(); } void StartStream(ServerContext&, const pw_rpc_test_TestRequest& request, ServerWriter& writer) { last_request = request; last_writer = std::move(writer); } class FakeService : public Service { public: FakeService(uint32_t id) : Service(id, kMethods) {} static constexpr std::array kMethods = { NanopbMethod::Unary( 10u, pw_rpc_test_Empty_fields, pw_rpc_test_Empty_fields), NanopbMethod::Unary( 11u, pw_rpc_test_TestRequest_fields, pw_rpc_test_TestResponse_fields), NanopbMethod::ServerStreaming( 12u, pw_rpc_test_TestRequest_fields, pw_rpc_test_TestResponse_fields), }; }; TEST(NanopbMethod, UnaryRpc_SendsResponse) { PW_ENCODE_PB( pw_rpc_test_TestRequest, request, .integer = 123, .status_code = 0); const NanopbMethod& method = std::get<1>(FakeService::kMethods).nanopb_method(); ServerContextForTest context(method); method.Invoke(context.get(), context.packet(request)); const Packet& response = context.output().sent_packet(); EXPECT_EQ(response.status(), Status::Unauthenticated()); // Field 1 (encoded as 1 << 3) with 128 as the value. constexpr std::byte expected[]{ std::byte{0x08}, std::byte{0x80}, std::byte{0x01}}; EXPECT_EQ(sizeof(expected), response.payload().size()); EXPECT_EQ(0, std::memcmp(expected, response.payload().data(), sizeof(expected))); EXPECT_EQ(123, last_request.integer); } TEST(NanopbMethod, UnaryRpc_InvalidPayload_SendsError) { std::array bad_payload{byte{0xFF}, byte{0xAA}, byte{0xDD}}; const NanopbMethod& method = std::get<0>(FakeService::kMethods).nanopb_method(); ServerContextForTest context(method); method.Invoke(context.get(), context.packet(bad_payload)); const Packet& packet = context.output().sent_packet(); EXPECT_EQ(PacketType::SERVER_ERROR, packet.type()); EXPECT_EQ(Status::DataLoss(), packet.status()); EXPECT_EQ(context.service_id(), packet.service_id()); EXPECT_EQ(method.id(), packet.method_id()); } TEST(NanopbMethod, UnaryRpc_BufferTooSmallForResponse_SendsInternalError) { constexpr int64_t value = 0x7FFFFFFF'FFFFFF00ll; PW_ENCODE_PB( pw_rpc_test_TestRequest, request, .integer = value, .status_code = 0); const NanopbMethod& method = std::get<1>(FakeService::kMethods).nanopb_method(); // Output buffer is too small for the response, but can fit an error packet. ServerContextForTest context(method); ASSERT_LT(context.output().buffer_size(), context.packet(request).MinEncodedSizeBytes() + request.size() + 1); method.Invoke(context.get(), context.packet(request)); const Packet& packet = context.output().sent_packet(); EXPECT_EQ(PacketType::SERVER_ERROR, packet.type()); EXPECT_EQ(Status::Internal(), packet.status()); EXPECT_EQ(context.service_id(), packet.service_id()); EXPECT_EQ(method.id(), packet.method_id()); EXPECT_EQ(value, last_request.integer); } TEST(NanopbMethod, ServerStreamingRpc_SendsNothingWhenInitiallyCalled) { PW_ENCODE_PB( pw_rpc_test_TestRequest, request, .integer = 555, .status_code = 0); const NanopbMethod& method = std::get<2>(FakeService::kMethods).nanopb_method(); ServerContextForTest context(method); method.Invoke(context.get(), context.packet(request)); EXPECT_EQ(0u, context.output().packet_count()); EXPECT_EQ(555, last_request.integer); } TEST(NanopbMethod, ServerWriter_SendsResponse) { const NanopbMethod& method = std::get<2>(FakeService::kMethods).nanopb_method(); ServerContextForTest context(method); method.Invoke(context.get(), context.packet({})); EXPECT_EQ(OkStatus(), last_writer.Write({.value = 100})); PW_ENCODE_PB(pw_rpc_test_TestResponse, payload, .value = 100); std::array encoded_response = {}; auto encoded = context.packet(payload).Encode(encoded_response); ASSERT_EQ(OkStatus(), encoded.status()); ASSERT_EQ(encoded.value().size(), context.output().sent_data().size()); EXPECT_EQ(0, std::memcmp(encoded.value().data(), context.output().sent_data().data(), encoded.value().size())); } TEST(NanopbMethod, ServerWriter_WriteWhenClosed_ReturnsFailedPrecondition) { const NanopbMethod& method = std::get<2>(FakeService::kMethods).nanopb_method(); ServerContextForTest context(method); method.Invoke(context.get(), context.packet({})); EXPECT_EQ(OkStatus(), last_writer.Finish()); EXPECT_TRUE(last_writer.Write({.value = 100}).IsFailedPrecondition()); } TEST(NanopbMethod, ServerWriter_WriteAfterMoved_ReturnsFailedPrecondition) { const NanopbMethod& method = std::get<2>(FakeService::kMethods).nanopb_method(); ServerContextForTest context(method); method.Invoke(context.get(), context.packet({})); ServerWriter new_writer = std::move(last_writer); EXPECT_EQ(OkStatus(), new_writer.Write({.value = 100})); EXPECT_EQ(Status::FailedPrecondition(), last_writer.Write({.value = 100})); EXPECT_EQ(Status::FailedPrecondition(), last_writer.Finish()); EXPECT_EQ(OkStatus(), new_writer.Finish()); } TEST(NanopbMethod, ServerStreamingRpc_ServerWriterBufferTooSmall_InternalError) { const NanopbMethod& method = std::get<2>(FakeService::kMethods).nanopb_method(); constexpr size_t kNoPayloadPacketSize = 2 /* type */ + 2 /* channel */ + 5 /* service */ + 5 /* method */ + 2 /* payload */ + 2 /* status */; // Make the buffer barely fit a packet with no payload. ServerContextForTest context(method); // Verify that the encoded size of a packet with an empty payload is correct. std::array encoded_response = {}; auto encoded = context.packet({}).Encode(encoded_response); ASSERT_EQ(OkStatus(), encoded.status()); ASSERT_EQ(kNoPayloadPacketSize, encoded.value().size()); method.Invoke(context.get(), context.packet({})); EXPECT_EQ(OkStatus(), last_writer.Write({})); // Barely fits EXPECT_EQ(Status::Internal(), last_writer.Write({.value = 1})); // Too big } } // namespace } // namespace pw::rpc::internal