// Copyright 2023 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 #include #include #include "gtest/gtest.h" #include "pw_preprocessor/compiler.h" #include "pw_protobuf/internal/codegen.h" #include "pw_span/span.h" #include "pw_status/status.h" #include "pw_status/status_with_size.h" #include "pw_stream/memory_stream.h" // These header files contain the code generated by the pw_protobuf plugin. // They are re-generated every time the tests are built and are used by the // tests to ensure that the interface remains consistent. // // The purpose of the tests in this file is primarily to verify that the // generated C++ interface is valid rather than the correctness of the // low-level encoder. #include "pw_protobuf_test_protos/full_test.pwpb.h" #include "pw_protobuf_test_protos/importer.pwpb.h" #include "pw_protobuf_test_protos/optional.pwpb.h" #include "pw_protobuf_test_protos/repeated.pwpb.h" namespace pw::protobuf { namespace { using namespace ::pw::protobuf::test::pwpb; PW_MODIFY_DIAGNOSTICS_PUSH(); PW_MODIFY_DIAGNOSTIC(ignored, "-Wmissing-field-initializers"); TEST(CodegenMessage, Equality) { const Pigweed::Message one{ .magic_number = 0x49u, .ziggy = -111, .cycles = 0x40302010fecaaddeu, .ratio = -1.42f, .error_message = "not a typewriter", .pigweed = {.status = Bool::FILE_NOT_FOUND}, .bin = Pigweed::Protobuf::Binary::ZERO, .proto = {.bin = Proto::Binary::OFF, .pigweed_pigweed_bin = Pigweed::Pigweed::Binary::ZERO, .pigweed_protobuf_bin = Pigweed::Protobuf::Binary::ZERO, .meta = { .file_name = "/etc/passwd", .status = Pigweed::Protobuf::Compiler::Status::FUBAR, .protobuf_bin = Pigweed::Protobuf::Binary::ONE, .pigweed_bin = Pigweed::Pigweed::Binary::ONE, }}, .data = {std::byte{0x10}, std::byte{0x20}, std::byte{0x30}, std::byte{0x40}, std::byte{0x50}, std::byte{0x60}, std::byte{0x70}, std::byte{0x80}}, .bungle = -111, }; const Pigweed::Message two{ .magic_number = 0x49u, .ziggy = -111, .cycles = 0x40302010fecaaddeu, .ratio = -1.42f, .error_message = "not a typewriter", .pigweed = {.status = Bool::FILE_NOT_FOUND}, .bin = Pigweed::Protobuf::Binary::ZERO, .proto = {.bin = Proto::Binary::OFF, .pigweed_pigweed_bin = Pigweed::Pigweed::Binary::ZERO, .pigweed_protobuf_bin = Pigweed::Protobuf::Binary::ZERO, .meta = { .file_name = "/etc/passwd", .status = Pigweed::Protobuf::Compiler::Status::FUBAR, .protobuf_bin = Pigweed::Protobuf::Binary::ONE, .pigweed_bin = Pigweed::Pigweed::Binary::ONE, }}, .data = {std::byte{0x10}, std::byte{0x20}, std::byte{0x30}, std::byte{0x40}, std::byte{0x50}, std::byte{0x60}, std::byte{0x70}, std::byte{0x80}}, .bungle = -111, }; EXPECT_TRUE(one == two); } TEST(CodegenMessage, CopyEquality) { Pigweed::Message one{ .magic_number = 0x49u, .ziggy = -111, .cycles = 0x40302010fecaaddeu, .ratio = -1.42f, .error_message = "not a typewriter", .pigweed = {.status = Bool::FILE_NOT_FOUND}, .bin = Pigweed::Protobuf::Binary::ZERO, .proto = {.bin = Proto::Binary::OFF, .pigweed_pigweed_bin = Pigweed::Pigweed::Binary::ZERO, .pigweed_protobuf_bin = Pigweed::Protobuf::Binary::ZERO, .meta = { .file_name = "/etc/passwd", .status = Pigweed::Protobuf::Compiler::Status::FUBAR, .protobuf_bin = Pigweed::Protobuf::Binary::ONE, .pigweed_bin = Pigweed::Pigweed::Binary::ONE, }}, .data = {std::byte{0x10}, std::byte{0x20}, std::byte{0x30}, std::byte{0x40}, std::byte{0x50}, std::byte{0x60}, std::byte{0x70}, std::byte{0x80}}, .bungle = -111, }; Pigweed::Message two = one; EXPECT_TRUE(one == two); } TEST(CodegenMessage, EmptyEquality) { const Pigweed::Message one{}; const Pigweed::Message two{}; EXPECT_TRUE(one == two); } TEST(CodegenMessage, Inequality) { const Pigweed::Message one{ .magic_number = 0x49u, .ziggy = -111, .cycles = 0x40302010fecaaddeu, .ratio = -1.42f, .error_message = "not a typewriter", .pigweed = {.status = Bool::FILE_NOT_FOUND}, .bin = Pigweed::Protobuf::Binary::ZERO, .proto = {.bin = Proto::Binary::OFF, .pigweed_pigweed_bin = Pigweed::Pigweed::Binary::ZERO, .pigweed_protobuf_bin = Pigweed::Protobuf::Binary::ZERO, .meta = { .file_name = "/etc/passwd", .status = Pigweed::Protobuf::Compiler::Status::FUBAR, .protobuf_bin = Pigweed::Protobuf::Binary::ONE, .pigweed_bin = Pigweed::Pigweed::Binary::ONE, }}, .data = {std::byte{0x10}, std::byte{0x20}, std::byte{0x30}, std::byte{0x40}, std::byte{0x50}, std::byte{0x60}, std::byte{0x70}, std::byte{0x80}}, .bungle = -111, }; const Pigweed::Message two{ .magic_number = 0x43u, .ziggy = 128, .ratio = -1.42f, .error_message = "not a typewriter", .pigweed = {.status = Bool::TRUE}, .bin = Pigweed::Protobuf::Binary::ZERO, .proto = {.bin = Proto::Binary::OFF, .pigweed_pigweed_bin = Pigweed::Pigweed::Binary::ZERO, .pigweed_protobuf_bin = Pigweed::Protobuf::Binary::ONE, .meta = { .file_name = "/etc/passwd", .status = Pigweed::Protobuf::Compiler::Status::FUBAR, .protobuf_bin = Pigweed::Protobuf::Binary::ONE, .pigweed_bin = Pigweed::Pigweed::Binary::ONE, }}, .data = {std::byte{0x20}, std::byte{0x30}, std::byte{0x40}, std::byte{0x50}, std::byte{0x60}, std::byte{0x70}, std::byte{0x80}, std::byte{0x90}}, }; EXPECT_FALSE(one == two); } TEST(CodegenMessage, TriviallyComparable) { static_assert(IsTriviallyComparable()); static_assert(IsTriviallyComparable()); static_assert(!IsTriviallyComparable()); } TEST(CodegenMessage, ConstCopyable) { const Pigweed::Message one{ .magic_number = 0x49u, .ziggy = -111, .cycles = 0x40302010fecaaddeu, .ratio = -1.42f, .error_message = "not a typewriter", .pigweed = {.status = Bool::FILE_NOT_FOUND}, .bin = Pigweed::Protobuf::Binary::ZERO, .proto = {.bin = Proto::Binary::OFF, .pigweed_pigweed_bin = Pigweed::Pigweed::Binary::ZERO, .pigweed_protobuf_bin = Pigweed::Protobuf::Binary::ZERO, .meta = { .file_name = "/etc/passwd", .status = Pigweed::Protobuf::Compiler::Status::FUBAR, .protobuf_bin = Pigweed::Protobuf::Binary::ONE, .pigweed_bin = Pigweed::Pigweed::Binary::ONE, }}, .data = {std::byte{0x10}, std::byte{0x20}, std::byte{0x30}, std::byte{0x40}, std::byte{0x50}, std::byte{0x60}, std::byte{0x70}, std::byte{0x80}}, .bungle = -111, }; Pigweed::Message two = one; EXPECT_TRUE(one == two); } TEST(CodegenMessage, FixReservedIdentifiers) { // This test checks that the code was generated as expected, so it will simply // fail to compile if its expectations are not met. // Make sure that the `signed` field was renamed to `signed_`. std::ignore = IntegerMetadata::Message{ .bits = 32, .signed_ = true, .null = false, }; // Make sure that the internal enum describing the struct's fields was // generated as expected: // - `BITS` doesn't need an underscore. // - `SIGNED_` has an underscore to match the corresponding `signed_` field. // - `NULL_` has an underscore to avoid a collision with `NULL` (even though // the field `null` doesn't have or need an underscore). std::ignore = IntegerMetadata::Fields::kBits; std::ignore = IntegerMetadata::Fields::kSigned; std::ignore = IntegerMetadata::Fields::kNull; // Make sure that the `ReservedWord` enum values were renamed as expected. // Specifically, only enum-value names that are reserved in UPPER_SNAKE_CASE // should be modified. Names that are only reserved in lower_snake_case should // be left alone since they'll never appear in that form in the generated // code. std::ignore = ReservedWord::NULL_; // Add underscore since NULL is a macro. std::ignore = ReservedWord::kNull; // No underscore necessary. std::ignore = ReservedWord::INT; // No underscore necessary. std::ignore = ReservedWord::kInt; // No underscore necessary. std::ignore = ReservedWord::RETURN; // No underscore necessary. std::ignore = ReservedWord::kReturn; // No underscore necessary. std::ignore = ReservedWord::BREAK; // No underscore necessary. std::ignore = ReservedWord::kBreak; // No underscore necessary. std::ignore = ReservedWord::FOR; // No underscore necessary. std::ignore = ReservedWord::kFor; // No underscore necessary. std::ignore = ReservedWord::DO; // No underscore necessary. std::ignore = ReservedWord::kDo; // No underscore necessary. // Instantiate an extremely degenerately named set of nested types in order to // make sure that name conflicts with the codegen internals are properly // prevented. std::ignore = Function::Message{ .description = Function::Message_::Message{ .content = "multiplication (mod 5)", }, .domain_field = Function::Fields_::INTEGERS_MOD_5, .codomain_field = Function::Fields_::INTEGERS_MOD_5, }; // Check for expected values of `enum class Function::Fields`: std::ignore = Function::Fields::kDescription; std::ignore = Function::Fields::kDomainField; std::ignore = Function::Fields::kCodomainField; // Check for expected values of `enum class Function::Message_::Fields`: std::ignore = Function::Message_::Fields::kContent; // Check for expected values of `enum class Function::Fields_`: std::ignore = Function::Fields_::NONE; std::ignore = Function::Fields_::kNone; std::ignore = Function::Fields_::COMPLEX_NUMBERS; std::ignore = Function::Fields_::kComplexNumbers; std::ignore = Function::Fields_::INTEGERS_MOD_5; std::ignore = Function::Fields_::kIntegersMod5; std::ignore = Function::Fields_::MEROMORPHIC_FUNCTIONS_ON_COMPLEX_PLANE; std::ignore = Function::Fields_::kMeromorphicFunctionsOnComplexPlane; std::ignore = Function::Fields_::OTHER; std::ignore = Function::Fields_::kOther; } PW_MODIFY_DIAGNOSTICS_POP(); TEST(CodegenMessage, Read) { // clang-format off constexpr uint8_t proto_data[] = { // pigweed.magic_number 0x08, 0x49, // pigweed.ziggy 0x10, 0xdd, 0x01, // pigweed.cycles 0x19, 0xde, 0xad, 0xca, 0xfe, 0x10, 0x20, 0x30, 0x40, // pigweed.ratio 0x25, 0x8f, 0xc2, 0xb5, 0xbf, // pigweed.error_message 0x2a, 0x10, 'n', 'o', 't', ' ', 'a', ' ', 't', 'y', 'p', 'e', 'w', 'r', 'i', 't', 'e', 'r', // pigweed.bin 0x40, 0x01, // pigweed.bungle 0x70, 0x91, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x01, }; // clang-format on stream::MemoryReader reader(as_bytes(span(proto_data))); Pigweed::StreamDecoder pigweed(reader); Pigweed::Message message{}; const auto status = pigweed.Read(message); ASSERT_EQ(status, OkStatus()); constexpr std::string_view kExpectedErrorMessage{"not a typewriter"}; EXPECT_EQ(message.magic_number, 0x49u); EXPECT_EQ(message.ziggy, -111); EXPECT_EQ(message.cycles, 0x40302010fecaaddeu); EXPECT_EQ(message.ratio, -1.42f); EXPECT_EQ(message.error_message.size(), kExpectedErrorMessage.size()); EXPECT_EQ(std::memcmp(message.error_message.data(), kExpectedErrorMessage.data(), kExpectedErrorMessage.size()), 0); EXPECT_EQ(message.bin, Pigweed::Protobuf::Binary::ZERO); EXPECT_EQ(message.bungle, -111); } TEST(CodegenMessage, ReadNonPackedScalar) { // clang-format off constexpr uint8_t proto_data[] = { // uint32s[], v={0, 16, 32, 48} 0x08, 0x00, 0x08, 0x10, 0x08, 0x20, 0x08, 0x30, // fixed32s[]. v={0, 16, 32, 48} 0x35, 0x00, 0x00, 0x00, 0x00, 0x35, 0x10, 0x00, 0x00, 0x00, 0x35, 0x20, 0x00, 0x00, 0x00, 0x35, 0x30, 0x00, 0x00, 0x00, }; // clang-format on stream::MemoryReader reader(as_bytes(span(proto_data))); RepeatedTest::StreamDecoder repeated_test(reader); RepeatedTest::Message message{}; const auto status = repeated_test.Read(message); ASSERT_EQ(status, OkStatus()); ASSERT_EQ(message.uint32s.size(), 4u); for (int i = 0; i < 4; ++i) { EXPECT_EQ(message.uint32s[i], i * 16u); } ASSERT_EQ(message.fixed32s.size(), 4u); for (int i = 0; i < 4; ++i) { EXPECT_EQ(message.fixed32s[i], i * 16u); } } TEST(CodegenMessage, ReadPackedScalar) { // clang-format off constexpr uint8_t proto_data[] = { // uint32s[], v={0, 16, 32, 48} 0x0a, 0x04, 0x00, 0x10, 0x20, 0x30, // fixed32s[]. v={0, 16, 32, 48} 0x32, 0x10, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, }; // clang-format on stream::MemoryReader reader(as_bytes(span(proto_data))); RepeatedTest::StreamDecoder repeated_test(reader); RepeatedTest::Message message{}; const auto status = repeated_test.Read(message); ASSERT_EQ(status, OkStatus()); ASSERT_EQ(message.uint32s.size(), 4u); for (int i = 0; i < 4; ++i) { EXPECT_EQ(message.uint32s[i], i * 16u); } ASSERT_EQ(message.fixed32s.size(), 4u); for (int i = 0; i < 4; ++i) { EXPECT_EQ(message.fixed32s[i], i * 16u); } } TEST(CodegenMessage, ReadPackedScalarRepeated) { // clang-format off constexpr uint8_t proto_data[] = { // uint32s[], v={0, 16, 32, 48} 0x0a, 0x04, 0x00, 0x10, 0x20, 0x30, // uint32s[], v={64, 80, 96, 112} 0x0a, 0x04, 0x40, 0x50, 0x60, 0x70, // fixed32s[]. v={0, 16, 32, 48} 0x32, 0x10, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, // fixed32s[]. v={64, 80, 96, 112} 0x32, 0x10, 0x40, 0x00, 0x00, 0x00, 0x50, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, }; // clang-format on stream::MemoryReader reader(as_bytes(span(proto_data))); RepeatedTest::StreamDecoder repeated_test(reader); RepeatedTest::Message message{}; const auto status = repeated_test.Read(message); ASSERT_EQ(status, OkStatus()); ASSERT_EQ(message.uint32s.size(), 8u); for (int i = 0; i < 8; ++i) { EXPECT_EQ(message.uint32s[i], i * 16u); } ASSERT_EQ(message.fixed32s.size(), 8u); for (int i = 0; i < 8; ++i) { EXPECT_EQ(message.fixed32s[i], i * 16u); } } TEST(CodegenMessage, ReadPackedScalarExhausted) { // clang-format off constexpr uint8_t proto_data[] = { // uint32s[], v={0, 16, 32, 48, 64, 80, 96, 112, 128} 0x0a, 0x09, 0x00, 0x10, 0x20, 0x30, 0x40, 0x50, 0x60, 0x70, 0x80, }; // clang-format on stream::MemoryReader reader(as_bytes(span(proto_data))); RepeatedTest::StreamDecoder repeated_test(reader); // uint32s has max_size=8, so this will exhaust the vector. RepeatedTest::Message message{}; const auto status = repeated_test.Read(message); ASSERT_EQ(status, Status::ResourceExhausted()); } TEST(CodegenMessage, ReadPackedScalarCallback) { // clang-format off constexpr uint8_t proto_data[] = { // sint32s[], v={-25, -1, 0, 1, 25} 0x12, 0x05, 0x31, 0x01, 0x00, 0x02, 0x32, }; // clang-format on stream::MemoryReader reader(as_bytes(span(proto_data))); RepeatedTest::StreamDecoder repeated_test(reader); // sint32s is a repeated field declared without max_count, so requirses a // callback to be decoded. RepeatedTest::Message message{}; message.sint32s.SetDecoder([](RepeatedTest::StreamDecoder& decoder) { EXPECT_EQ(decoder.Field().value(), RepeatedTest::Fields::kSint32s); pw::Vector sint32s{}; const auto status = decoder.ReadSint32s(sint32s); EXPECT_EQ(status, OkStatus()); EXPECT_EQ(sint32s.size(), 5u); EXPECT_EQ(sint32s[0], -25); EXPECT_EQ(sint32s[1], -1); EXPECT_EQ(sint32s[2], 0); EXPECT_EQ(sint32s[3], 1); EXPECT_EQ(sint32s[4], 25); return status; }); const auto status = repeated_test.Read(message); ASSERT_EQ(status, OkStatus()); } TEST(CodegenMessage, ReadPackedScalarFixedLength) { // clang-format off constexpr uint8_t proto_data[] = { // uint64s[], v={1000, 2000, 3000, 4000} 0x42, 0x08, 0xe8, 0x07, 0xd0, 0x0f, 0xb8, 0x17, 0xa0, 0x1f, // doubles[], v={3.14159, 2.71828} 0x22, 0x10, 0x6e, 0x86, 0x1b, 0xf0, 0xf9, 0x21, 0x09, 0x40, 0x90, 0xf7, 0xaa, 0x95, 0x09, 0xbf, 0x05, 0x40, }; // clang-format on stream::MemoryReader reader(as_bytes(span(proto_data))); RepeatedTest::StreamDecoder repeated_test(reader); RepeatedTest::Message message{}; const auto status = repeated_test.Read(message); ASSERT_EQ(status, OkStatus()); EXPECT_EQ(message.uint64s[0], 1000u); EXPECT_EQ(message.uint64s[1], 2000u); EXPECT_EQ(message.uint64s[2], 3000u); EXPECT_EQ(message.uint64s[3], 4000u); EXPECT_EQ(message.doubles[0], 3.14159); EXPECT_EQ(message.doubles[1], 2.71828); } TEST(CodegenMessage, ReadPackedScalarFixedLengthShort) { // clang-format off constexpr uint8_t proto_data[] = { // uint64s[], v={1000, 2000} 0x42, 0x04, 0xe8, 0x07, 0xd0, 0x0f, // doubles[], v={3.14159} 0x22, 0x08, 0x6e, 0x86, 0x1b, 0xf0, 0xf9, 0x21, 0x09, 0x40, }; // clang-format on stream::MemoryReader reader(as_bytes(span(proto_data))); RepeatedTest::StreamDecoder repeated_test(reader); RepeatedTest::Message message{}; const auto status = repeated_test.Read(message); ASSERT_EQ(status, OkStatus()); EXPECT_EQ(message.uint64s[0], 1000u); EXPECT_EQ(message.uint64s[1], 2000u); EXPECT_EQ(message.uint64s[2], 0u); EXPECT_EQ(message.uint64s[3], 0u); EXPECT_EQ(message.doubles[0], 3.14159); EXPECT_EQ(message.doubles[1], 0); } TEST(CodegenMessage, ReadPackedScalarVarintFixedLengthExhausted) { // clang-format off constexpr uint8_t proto_data[] = { // uint64s[], v={0, 1000, 2000, 3000, 4000} 0x42, 0x09, 0x08, 0xe8, 0x07, 0xd0, 0x0f, 0xb8, 0x17, 0xa0, 0x1f, }; // clang-format on stream::MemoryReader reader(as_bytes(span(proto_data))); RepeatedTest::StreamDecoder repeated_test(reader); RepeatedTest::Message message{}; const auto status = repeated_test.Read(message); ASSERT_EQ(status, Status::ResourceExhausted()); } TEST(CodegenMessage, ReadPackedScalarFixedLengthExhausted) { // clang-format off constexpr uint8_t proto_data[] = { // doubles[], v={3.14159, 2.71828, 1.41429, 1.73205} 0x22, 0x20, 0x6e, 0x86, 0x1b, 0xf0, 0xf9, 0x21, 0x09, 0x40, 0x90, 0xf7, 0xaa, 0x95, 0x09, 0xbf, 0x05, 0x40, 0x1b, 0xf5, 0x10, 0x8d, 0xee, 0xa0, 0xf6, 0x3f, 0xbc, 0x96, 0x90, 0x0f, 0x7a, 0xb6, 0xfb, 0x3f, }; // clang-format on stream::MemoryReader reader(as_bytes(span(proto_data))); RepeatedTest::StreamDecoder repeated_test(reader); RepeatedTest::Message message{}; const auto status = repeated_test.Read(message); ASSERT_EQ(status, Status::ResourceExhausted()); } TEST(CodegenMessage, ReadPackedEnum) { // clang-format off constexpr uint8_t proto_data[] = { // enums[], v={RED, GREEN, AMBER, RED} 0x4a, 0x04, 0x00, 0x02, 0x01, 0x00, }; // clang-format on stream::MemoryReader reader(as_bytes(span(proto_data))); RepeatedTest::StreamDecoder repeated_test(reader); RepeatedTest::Message message{}; const auto status = repeated_test.Read(message); ASSERT_EQ(status, OkStatus()); ASSERT_EQ(message.enums.size(), 4u); for (int i = 0; i < 4; ++i) { EXPECT_TRUE(IsValidEnum(message.enums[i])); } EXPECT_EQ(message.enums[0], Enum::RED); EXPECT_EQ(message.enums[1], Enum::GREEN); EXPECT_EQ(message.enums[2], Enum::AMBER); EXPECT_EQ(message.enums[3], Enum::RED); } TEST(CodegenMessage, ReadStringExhausted) { // clang-format off constexpr uint8_t proto_data[] = { // pigweed.error_message 0x2a, 0xd3, 0x01, 'T', 'h', 'i', 's', ' ', 'l', 'a', 'b', 'e', 'l', ' ', 'i', 's', ' ', 't', 'h', 'e', ' ', 't', 'a', 'r', 'g', 'e', 't', ' ', 'o', 'f', ' ', 'a', ' ', 'g', 'o', 't', 'o', ' ', 'f', 'r', 'o', 'm', ' ', 'o', 'u', 't', 's', 'i', 'd', 'e', ' ', 'o', 'f', ' ', 't', 'h', 'e', ' ', 'b', 'l', 'o', 'c', 'k', ' ', 'c', 'o', 'n', 't', 'a', 'i', 'n', 'i', 'n', 'g', ' ', 't', 'h', 'i', 's', ' ', 'l', 'a', 'b', 'e', 'l', ' ', 'A', 'N', 'D', ' ', 't', 'h', 'i', 's', ' ', 'b', 'l', 'o', 'c', 'k', ' ', 'h', 'a', 's', ' ', 'a', 'n', ' ', 'a', 'u', 't', 'o', 'm', 'a', 't', 'i', 'c', ' ', 'v', 'a', 'r', 'i', 'a', 'b', 'l', 'e', ' ', 'w', 'i', 't', 'h', ' ', 'a', 'n', ' ', 'i', 'n', 'i', 't', 'i', 'a', 'l', 'i', 'z', 'e', 'r', ' ', 'A', 'N', 'D', ' ', 'y', 'o', 'u', 'r', ' ', 'w', 'i', 'n', 'd', 'o', 'w', ' ', 'w', 'a', 's', 'n', '\'', 't', ' ', 'w', 'i', 'd', 'e', ' ', 'e', 'n', 'o', 'u', 'g', 'h', ' ', 't', 'o', ' ', 'r', 'e', 'a', 'd', ' ', 't', 'h', 'i', 's', ' ', 'w', 'h', 'o', 'l', 'e', ' ', 'e', 'r', 'r', 'o', 'r', ' ', 'm', 'e', 's', 's', 'a', 'g', 'e' }; // clang-format on stream::MemoryReader reader(as_bytes(span(proto_data))); Pigweed::StreamDecoder pigweed(reader); Pigweed::Message message{}; const auto status = pigweed.Read(message); ASSERT_EQ(status, Status::ResourceExhausted()); } TEST(CodegenMessage, ReadStringCallback) { // clang-format off constexpr uint8_t proto_data[] = { // pigweed.description 0x62, 0x5c, 'a', 'n', ' ', 'o', 'p', 'e', 'n', ' ', 's', 'o', 'u', 'r', 'c', 'e', ' ', 'c', 'o', 'l', 'l', 'e', 'c', 't', 'i', 'o', 'n', ' ', 'o', 'f', ' ', 'e', 'm', 'b', 'e', 'd', 'd', 'e', 'd', '-', 't', 'a', 'r', 'g', 'e', 't', 'e', 'd', ' ', 'l', 'i', 'b', 'r', 'a', 'r', 'i', 'e', 's', '-', 'o', 'r', ' ', 'a', 's', ' ', 'w', 'e', ' ', 'l', 'i', 'k', 'e', ' ', 't', 'o', ' ', 'c', 'a', 'l', 'l', ' ', 't', 'h', 'e', 'm', ',', ' ', 'm', 'o', 'd', 'u', 'l', 'e', 's' }; // clang-format on stream::MemoryReader reader(as_bytes(span(proto_data))); Pigweed::StreamDecoder pigweed(reader); // pigweed.description has no max_size specified so a callback must be // set to read the value if present. Pigweed::Message message{}; message.description.SetDecoder([](Pigweed::StreamDecoder& decoder) { EXPECT_EQ(decoder.Field().value(), Pigweed::Fields::kDescription); constexpr std::string_view kExpectedDescription{ "an open source collection of embedded-targeted libraries-or as we " "like to call them, modules"}; std::array description{}; const auto sws = decoder.ReadDescription(description); EXPECT_EQ(sws.status(), OkStatus()); EXPECT_EQ(sws.size(), kExpectedDescription.size()); EXPECT_EQ(std::memcmp(description.data(), kExpectedDescription.data(), kExpectedDescription.size()), 0); return sws.status(); }); const auto status = pigweed.Read(message); ASSERT_EQ(status, OkStatus()); } TEST(CodegenMessage, ReadMultipleString) { // clang-format off constexpr uint8_t proto_data[] = { // pigweed.error_message 0x2a, 0x10, 'n', 'o', 't', ' ', 'a', ' ', 't', 'y', 'p', 'e', 'w', 'r', 'i', 't', 'e', 'r', // pigweed.error_message 0x02a, 0x07, 'o', 'n', ' ', 'f', 'i', 'r', 'e' }; // clang-format on stream::MemoryReader reader(as_bytes(span(proto_data))); Pigweed::StreamDecoder pigweed(reader); Pigweed::Message message{}; const auto status = pigweed.Read(message); ASSERT_EQ(status, OkStatus()); constexpr std::string_view kExpectedErrorMessage{"on fire"}; EXPECT_EQ(message.error_message.size(), kExpectedErrorMessage.size()); EXPECT_EQ(std::memcmp(message.error_message.data(), kExpectedErrorMessage.data(), kExpectedErrorMessage.size()), 0); } TEST(CodegenMessage, ReadRepeatedStrings) { // clang-format off constexpr uint8_t proto_data[] = { // repeated.strings 0x1a, 0x25, 'i', 'f', ' ', 'm', 'u', 's', 'i', 'c', ' ', 'b', 'e', ' ', 't', 'h', 'e', ' ', 'f', 'o', 'o', 'd', ' ', 'o', 'f', ' ', 'l', 'o', 'v', 'e', ',', ' ', 'p', 'l', 'a', 'y', ' ', 'o', 'n', // repeated.strings 0x1a, 0x26, 'g', 'i', 'v', 'e', ' ', 'm', 'e', ' ', 'e', 'x', 'c', 'e', 's', 's', ' ', 'o', 'f', ' ', 'i', 't', ',', ' ', 't', 'h', 'a', 't', ',', ' ', 's', 'u', 'r', 'f', 'e', 'i', 't', 'i', 'n', 'g', // repeated.strings 0x1a, 0x23, 't', 'h', 'e', ' ', 'a', 'p', 'p', 'e', 't', 'i', 't', 'e', ' ', 'm', 'a', 'y', ' ', 's', 'i', 'c', 'k', 'e', 'n', ',', ' ', 'a', 'n', 'd', ' ', 's', 'o', ' ', 'd', 'i', 'e', }; // clang-format on stream::MemoryReader reader(as_bytes(span(proto_data))); RepeatedTest::StreamDecoder repeated_test(reader); // Repeated strings require a callback to avoid forcing multi-dimensional // arrays upon the caller. RepeatedTest::Message message{}; int i = 0; message.strings.SetDecoder([&i](RepeatedTest::StreamDecoder& decoder) { EXPECT_EQ(decoder.Field().value(), RepeatedTest::Fields::kStrings); constexpr std::string_view kExpectedStrings[] = { {"if music be the food of love, play on"}, {"give me excess of it, that, surfeiting"}, {"the appetite may sicken, and so die"}}; std::array strings{}; const StatusWithSize sws = decoder.ReadStrings(strings); EXPECT_EQ(sws.status(), OkStatus()); EXPECT_EQ(sws.size(), kExpectedStrings[i].size()); EXPECT_EQ(std::memcmp(strings.data(), kExpectedStrings[i].data(), kExpectedStrings[i].size()), 0); ++i; return sws.status(); }); const auto status = repeated_test.Read(message); ASSERT_EQ(status, OkStatus()); } TEST(CodegenMessage, ReadForcedCallback) { // clang-format off constexpr uint8_t proto_data[] = { // pigweed.special_property 0x68, 0x2a, }; // clang-format on stream::MemoryReader reader(as_bytes(span(proto_data))); Pigweed::StreamDecoder pigweed(reader); // pigweed.special_property has use_callback=true to force the use of a // callback even though it's a simple scalar. Pigweed::Message message{}; message.special_property.SetDecoder([](Pigweed::StreamDecoder& decoder) { EXPECT_EQ(decoder.Field().value(), Pigweed::Fields::kSpecialProperty); pw::Result result = decoder.ReadSpecialProperty(); EXPECT_EQ(result.status(), OkStatus()); EXPECT_EQ(result.value(), 42u); return result.status(); }); const auto status = pigweed.Read(message); ASSERT_EQ(status, OkStatus()); } TEST(CodegenMessage, ReadMissingCallback) { // clang-format off constexpr uint8_t proto_data[] = { // repeated.strings 0x1a, 0x25, 'i', 'f', ' ', 'm', 'u', 's', 'i', 'c', ' ', 'b', 'e', ' ', 't', 'h', 'e', ' ', 'f', 'o', 'o', 'd', ' ', 'o', 'f', ' ', 'l', 'o', 'v', 'e', ',', ' ', 'p', 'l', 'a', 'y', ' ', 'o', 'n', // repeated.strings 0x1a, 0x26, 'g', 'i', 'v', 'e', ' ', 'm', 'e', ' ', 'e', 'x', 'c', 'e', 's', 's', ' ', 'o', 'f', ' ', 'i', 't', ',', ' ', 't', 'h', 'a', 't', ',', ' ', 's', 'u', 'r', 'f', 'e', 'i', 't', 'i', 'n', 'g', // repeated.strings 0x1a, 0x23, 't', 'h', 'e', ' ', 'a', 'p', 'p', 'e', 't', 'i', 't', 'e', ' ', 'm', 'a', 'y', ' ', 's', 'i', 'c', 'k', 'e', 'n', ',', ' ', 'a', 'n', 'd', ' ', 's', 'o', ' ', 'd', 'i', 'e', }; // clang-format on stream::MemoryReader reader(as_bytes(span(proto_data))); RepeatedTest::StreamDecoder repeated_test(reader); // Failing to set a callback will give a DataLoss error if that field is // present in the decoded data. RepeatedTest::Message message{}; const auto status = repeated_test.Read(message); ASSERT_EQ(status, Status::DataLoss()); } TEST(CodegenMessage, ReadFixedLength) { // clang-format off constexpr uint8_t proto_data[] = { // pigweed.data 0x5a, 0x08, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08 }; // clang-format on stream::MemoryReader reader(as_bytes(span(proto_data))); Pigweed::StreamDecoder pigweed(reader); Pigweed::Message message{}; const auto status = pigweed.Read(message); ASSERT_EQ(status, OkStatus()); EXPECT_EQ(message.data[0], std::byte{0x01}); EXPECT_EQ(message.data[1], std::byte{0x02}); EXPECT_EQ(message.data[2], std::byte{0x03}); EXPECT_EQ(message.data[3], std::byte{0x04}); EXPECT_EQ(message.data[4], std::byte{0x05}); EXPECT_EQ(message.data[5], std::byte{0x06}); EXPECT_EQ(message.data[6], std::byte{0x07}); EXPECT_EQ(message.data[7], std::byte{0x08}); } TEST(CodegenMessage, ReadFixedLengthShort) { // clang-format off constexpr uint8_t proto_data[] = { // pigweed.data 0x5a, 0x04, 0x01, 0x02, 0x03, 0x04 }; // clang-format on stream::MemoryReader reader(as_bytes(span(proto_data))); Pigweed::StreamDecoder pigweed(reader); Pigweed::Message message{}; const auto status = pigweed.Read(message); ASSERT_EQ(status, OkStatus()); EXPECT_EQ(message.data[0], std::byte{0x01}); EXPECT_EQ(message.data[1], std::byte{0x02}); EXPECT_EQ(message.data[2], std::byte{0x03}); EXPECT_EQ(message.data[3], std::byte{0x04}); // Remaining bytes are whatever you initialized them to. EXPECT_EQ(message.data[4], std::byte{0x00}); EXPECT_EQ(message.data[5], std::byte{0x00}); EXPECT_EQ(message.data[6], std::byte{0x00}); EXPECT_EQ(message.data[7], std::byte{0x00}); } TEST(CodegenMessage, ReadFixedLengthExhausted) { // clang-format off constexpr uint8_t proto_data[] = { // pigweed.data 0x5a, 0x0c, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c }; // clang-format on stream::MemoryReader reader(as_bytes(span(proto_data))); Pigweed::StreamDecoder pigweed(reader); Pigweed::Message message{}; const auto status = pigweed.Read(message); ASSERT_EQ(status, Status::ResourceExhausted()); } TEST(CodegenMessage, ReadNested) { // clang-format off constexpr uint8_t proto_data[] = { // pigweed.magic_number 0x08, 0x49, // pigweed.pigweed 0x3a, 0x02, // pigweed.pigweed.status 0x08, 0x02, // pigweed.ziggy 0x10, 0xdd, 0x01, }; // clang-format on stream::MemoryReader reader(as_bytes(span(proto_data))); Pigweed::StreamDecoder pigweed(reader); Pigweed::Message message{}; const auto status = pigweed.Read(message); ASSERT_EQ(status, OkStatus()); EXPECT_EQ(message.magic_number, 0x49u); EXPECT_EQ(message.pigweed.status, Bool::FILE_NOT_FOUND); EXPECT_EQ(message.ziggy, -111); } TEST(CodegenMessage, ReadNestedImported) { // clang-format off constexpr uint8_t proto_data[] = { // period.start 0x0a, 0x08, // period.start.seconds v=1517949900 0x08, 0xcc, 0xa7, 0xe8, 0xd3, 0x05, // period.start.nanoseconds v=0 0x10, 0x00, // period.end 0x12, 0x08, // period.end.seconds, v=1517950378 0x08, 0xaa, 0xab, 0xe8, 0xd3, 0x05, // period.end.nanoseconds, v=0 0x10, 0x00, }; // clang-format on stream::MemoryReader reader(as_bytes(span(proto_data))); Period::StreamDecoder period(reader); // Messages imported from another file can be directly embedded in a message. Period::Message message{}; const auto status = period.Read(message); ASSERT_EQ(status, OkStatus()); EXPECT_EQ(message.start.seconds, 1517949900u); EXPECT_EQ(message.start.nanoseconds, 0u); EXPECT_EQ(message.end.seconds, 1517950378u); EXPECT_EQ(message.end.nanoseconds, 0u); } TEST(CodegenMessage, ReadNestedRepeated) { // clang-format off constexpr uint8_t proto_data[] = { // repeated.structs 0x2a, 0x04, // repeated.structs.one v=16 0x08, 0x10, // repeated.structs.two v=32 0x10, 0x20, // repeated.structs 0x2a, 0x04, // repeated.structs.one v=48 0x08, 0x30, // repeated.structs.two v=64 0x10, 0x40, }; // clang-format on stream::MemoryReader reader(as_bytes(span(proto_data))); RepeatedTest::StreamDecoder repeated_test(reader); // Repeated nested messages require a callback since there would otherwise be // no way to set callbacks on the nested message. RepeatedTest::Message message{}; int i = 0; message.structs.SetDecoder([&i](RepeatedTest::StreamDecoder& decoder) { EXPECT_EQ(decoder.Field().value(), RepeatedTest::Fields::kStructs); Struct::Message structs_message{}; auto structs_decoder = decoder.GetStructsDecoder(); const auto status = structs_decoder.Read(structs_message); EXPECT_EQ(status, OkStatus()); EXPECT_LT(i, 2); EXPECT_EQ(structs_message.one, i * 32 + 16u); EXPECT_EQ(structs_message.two, i * 32 + 32u); ++i; return status; }); const auto status = repeated_test.Read(message); ASSERT_EQ(status, OkStatus()); } TEST(CodegenMessage, ReadNestedForcedCallback) { // clang-format off constexpr uint8_t proto_data[] = { // pigweed.device_info 0x32, 0x0e, // pigweed.device_info.device_name 0x0a, 0x05, 'p', 'i', 'x', 'e', 'l', // pigweed.device_info.device_id 0x15, 0x08, 0x08, 0x08, 0x08, // pigweed.device_info.status 0x18, 0x00, }; // clang-format on stream::MemoryReader reader(as_bytes(span(proto_data))); Pigweed::StreamDecoder pigweed(reader); // pigweed.device_info has use_callback=true to force the use of a callback. Pigweed::Message message{}; message.device_info.SetDecoder([](Pigweed::StreamDecoder& decoder) { EXPECT_EQ(decoder.Field().value(), Pigweed::Fields::kDeviceInfo); DeviceInfo::Message device_info{}; DeviceInfo::StreamDecoder device_info_decoder = decoder.GetDeviceInfoDecoder(); const auto status = device_info_decoder.Read(device_info); EXPECT_EQ(status, OkStatus()); constexpr std::string_view kExpectedDeviceName{"pixel"}; EXPECT_EQ(device_info.device_name.size(), kExpectedDeviceName.size()); EXPECT_EQ(std::memcmp(device_info.device_name.data(), kExpectedDeviceName.data(), kExpectedDeviceName.size()), 0); EXPECT_EQ(device_info.device_id, 0x08080808u); EXPECT_EQ(device_info.status, DeviceInfo::DeviceStatus::OK); return status; }); const auto status = pigweed.Read(message); ASSERT_EQ(status, OkStatus()); } TEST(CodegenMessage, ReadOptionalPresent) { // clang-format off constexpr uint8_t proto_data[] = { // optional.sometimes_present_fixed 0x0d, 0x2a, 0x00, 0x00, 0x00, // optional.sometimes_present_varint 0x10, 0x2a, // optional.explicitly_present_fixed 0x1d, 0x45, 0x00, 0x00, 0x00, // optional.explicitly_present_varint 0x20, 0x45, // optional.sometimes_empty_fixed 0x2a, 0x04, 0x63, 0x00, 0x00, 0x00, // optional.sometimes_empty_varint 0x32, 0x01, 0x63, }; // clang-format on stream::MemoryReader reader(as_bytes(span(proto_data))); OptionalTest::StreamDecoder optional_test(reader); OptionalTest::Message message{}; const auto status = optional_test.Read(message); ASSERT_EQ(status, OkStatus()); EXPECT_EQ(message.sometimes_present_fixed, 0x2a); EXPECT_EQ(message.sometimes_present_varint, 0x2a); EXPECT_TRUE(message.explicitly_present_fixed); EXPECT_EQ(*message.explicitly_present_fixed, 0x45); EXPECT_TRUE(message.explicitly_present_varint); EXPECT_EQ(*message.explicitly_present_varint, 0x45); EXPECT_FALSE(message.sometimes_empty_fixed.empty()); EXPECT_EQ(message.sometimes_empty_fixed.size(), 1u); EXPECT_EQ(message.sometimes_empty_fixed[0], 0x63); EXPECT_FALSE(message.sometimes_empty_varint.empty()); EXPECT_EQ(message.sometimes_empty_varint.size(), 1u); EXPECT_EQ(message.sometimes_empty_varint[0], 0x63); } TEST(CodegenMessage, ReadOptionalNotPresent) { constexpr std::array proto_data{}; stream::MemoryReader reader(proto_data); OptionalTest::StreamDecoder optional_test(reader); OptionalTest::Message message{}; const auto status = optional_test.Read(message); ASSERT_EQ(status, OkStatus()); // Non-optional fields have their default value. EXPECT_EQ(message.sometimes_present_fixed, 0); EXPECT_EQ(message.sometimes_present_varint, 0); EXPECT_TRUE(message.sometimes_empty_fixed.empty()); EXPECT_TRUE(message.sometimes_empty_varint.empty()); // Optional fields are explicitly not present. EXPECT_FALSE(message.explicitly_present_fixed); EXPECT_FALSE(message.explicitly_present_varint); } TEST(CodegenMessage, ReadOptionalPresentDefaults) { // clang-format off constexpr uint8_t proto_data[] = { // optional.sometimes_present_fixed 0x0d, 0x00, 0x00, 0x00, 0x00, // optional.sometimes_present_varint 0x10, 0x00, // optional.explicitly_present_fixed 0x1d, 0x00, 0x00, 0x00, 0x00, // optional.explicitly_present_varint 0x20, 0x00, // optional.sometimes_empty_fixed 0x2a, 0x04, 0x00, 0x00, 0x00, 0x00, // optional.sometimes_empty_varint 0x32, 0x01, 0x00, }; // clang-format on stream::MemoryReader reader(as_bytes(span(proto_data))); OptionalTest::StreamDecoder optional_test(reader); OptionalTest::Message message{}; const auto status = optional_test.Read(message); ASSERT_EQ(status, OkStatus()); // Non-optional fields have their default value and aren't meaningfully // different from missing. EXPECT_EQ(message.sometimes_present_fixed, 0x00); EXPECT_EQ(message.sometimes_present_varint, 0x00); // Optional fields are explicitly present with a default value. EXPECT_TRUE(message.explicitly_present_fixed); EXPECT_EQ(*message.explicitly_present_fixed, 0x00); EXPECT_TRUE(message.explicitly_present_varint); EXPECT_EQ(*message.explicitly_present_varint, 0x00); // Repeated fields with a default value are meaningfully non-empty. EXPECT_FALSE(message.sometimes_empty_fixed.empty()); EXPECT_EQ(message.sometimes_empty_fixed.size(), 1u); EXPECT_EQ(message.sometimes_empty_fixed[0], 0x00); EXPECT_FALSE(message.sometimes_empty_varint.empty()); EXPECT_EQ(message.sometimes_empty_varint.size(), 1u); EXPECT_EQ(message.sometimes_empty_varint[0], 0x00); } TEST(CodegenMessage, ReadImportedOptions) { // clang-format off constexpr uint8_t proto_data[] = { // notice 0x0a, 0x0f, // notice.message 0x0a, 0x0d, 'P', 'r', 'e', 's', 's', ' ', 'a', 'n', 'y', ' ', 'k', 'e', 'y' }; // clang-format on stream::MemoryReader reader(as_bytes(span(proto_data))); TestMessage::StreamDecoder test_message(reader); // The options file for the imported proto is applied, making the string // field a vector rather than requiring a callback. TestMessage::Message message{}; const auto status = test_message.Read(message); ASSERT_EQ(status, OkStatus()); constexpr std::string_view kExpectedMessage{"Press any key"}; EXPECT_EQ(message.notice.message.size(), kExpectedMessage.size()); EXPECT_EQ(std::memcmp(message.notice.message.data(), kExpectedMessage.data(), kExpectedMessage.size()), 0); } TEST(CodegenMessage, ReadImportedFromDepsOptions) { // clang-format off constexpr uint8_t proto_data[] = { // debug 0x12, 0x0f, // debug.message 0x0a, 0x0d, 'P', 'r', 'e', 's', 's', ' ', 'a', 'n', 'y', ' ', 'k', 'e', 'y' }; // clang-format on stream::MemoryReader reader(as_bytes(span(proto_data))); TestMessage::StreamDecoder test_message(reader); // The options file for the imported proto is applied, making the string // field a vector rather than requiring a callback. TestMessage::Message message{}; const auto status = test_message.Read(message); ASSERT_EQ(status, OkStatus()); constexpr std::string_view kExpectedMessage{"Press any key"}; EXPECT_EQ(message.debug.message.size(), kExpectedMessage.size()); EXPECT_EQ(std::memcmp(message.debug.message.data(), kExpectedMessage.data(), kExpectedMessage.size()), 0); } class BreakableDecoder : public KeyValuePair::StreamDecoder { public: constexpr BreakableDecoder(stream::Reader& reader) : StreamDecoder(reader) {} Status Read(KeyValuePair::Message& message, span table) { return ::pw::protobuf::StreamDecoder::Read( as_writable_bytes(span(&message, 1)), table); } }; TEST(CodegenMessage, DISABLED_ReadDoesNotOverrun) { // Deliberately construct a message table that attempts to violate the bounds // of the structure. We're not testing that a developer can't do this, rather // that the protobuf decoder can't be exploited in this way. constexpr internal::MessageField kMessageFields[] = { {1, WireType::kDelimited, sizeof(std::byte), static_cast(0), false, false, false, false, false, 0, sizeof(KeyValuePair::Message) * 2, {}}, }; // clang-format off constexpr uint8_t proto_data[] = { // id=1, len=9, 0x0a, 0x08, 'd', 'o', 'n', 't', 'e', 'a', 't', 'm', 'e', }; // clang-format on stream::MemoryReader reader(as_bytes(span(proto_data))); BreakableDecoder decoder(reader); KeyValuePair::Message message{}; // ASSERT_CRASH std::ignore = decoder.Read(message, kMessageFields); } TEST(CodegenMessage, Write) { constexpr uint8_t pigweed_data[] = { 0x10, 0x20, 0x30, 0x40, 0x50, 0x60, 0x70, 0x80}; Pigweed::Message message{}; message.magic_number = 0x49u; message.ziggy = -111; message.cycles = 0x40302010fecaaddeu; message.ratio = -1.42f; message.error_message = "not a typewriter"; message.pigweed.status = Bool::FILE_NOT_FOUND; message.bin = Pigweed::Protobuf::Binary::ZERO; message.bungle = -111; message.proto.bin = Proto::Binary::OFF; message.proto.pigweed_pigweed_bin = Pigweed::Pigweed::Binary::ZERO; message.proto.pigweed_protobuf_bin = Pigweed::Protobuf::Binary::ZERO; message.proto.meta.file_name = "/etc/passwd"; message.proto.meta.status = Pigweed::Protobuf::Compiler::Status::FUBAR; message.proto.meta.protobuf_bin = Pigweed::Protobuf::Binary::ONE; message.proto.meta.pigweed_bin = Pigweed::Pigweed::Binary::ONE; std::memcpy(message.data.data(), pigweed_data, sizeof(pigweed_data)); std::byte encode_buffer[Pigweed::kMaxEncodedSizeBytes]; std::byte temp_buffer[Pigweed::kScratchBufferSizeBytes]; stream::MemoryWriter writer(encode_buffer); Pigweed::StreamEncoder pigweed(writer, temp_buffer); const auto status = pigweed.Write(message); ASSERT_EQ(status, OkStatus()); // clang-format off constexpr uint8_t expected_proto[] = { // pigweed.magic_number 0x08, 0x49, // pigweed.ziggy 0x10, 0xdd, 0x01, // pigweed.cycles 0x19, 0xde, 0xad, 0xca, 0xfe, 0x10, 0x20, 0x30, 0x40, // pigweed.ratio 0x25, 0x8f, 0xc2, 0xb5, 0xbf, // pigweed.error_message 0x2a, 0x10, 'n', 'o', 't', ' ', 'a', ' ', 't', 'y', 'p', 'e', 'w', 'r', 'i', 't', 'e', 'r', // pigweed.pigweed 0x3a, 0x02, // pigweed.pigweed.status 0x08, 0x02, // pigweed.bin 0x40, 0x01, // pigweed.proto 0x4a, 0x15, // pigweed.proto.pigweed_protobuf_bin 0x20, 0x01, // pigweed.proto.meta 0x2a, 0x11, // pigweed.proto.meta.file_name 0x0a, 0x0b, '/', 'e', 't', 'c', '/', 'p', 'a', 's', 's', 'w', 'd', // pigweed.proto.meta.status 0x10, 0x02, // pigweed.proto.meta.pigweed_bin 0x20, 0x01, // pigweed.bytes 0x5a, 0x08, 0x10, 0x20, 0x30, 0x40, 0x50, 0x60, 0x70, 0x80, // pigweed.bungle 0x70, 0x91, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x01, }; // clang-format on ConstByteSpan result = writer.WrittenData(); EXPECT_EQ(result.size(), sizeof(expected_proto)); EXPECT_EQ(std::memcmp(result.data(), expected_proto, sizeof(expected_proto)), 0); } TEST(CodegenMessage, WriteDefaults) { Pigweed::Message message{}; std::byte encode_buffer[Pigweed::kMaxEncodedSizeBytes]; std::byte temp_buffer[Pigweed::kScratchBufferSizeBytes]; stream::MemoryWriter writer(encode_buffer); Pigweed::StreamEncoder pigweed(writer, temp_buffer); const auto status = pigweed.Write(message); ASSERT_EQ(status, OkStatus()); // Since all fields are at their default, the output should be zero sized. ConstByteSpan result = writer.WrittenData(); EXPECT_EQ(result.size(), 0u); } TEST(CodegenMessage, WritePackedScalar) { RepeatedTest::Message message{}; for (int i = 0; i < 4; ++i) { message.uint32s.push_back(i * 16u); message.fixed32s.push_back(i * 16u); } std::byte encode_buffer[RepeatedTest::kMaxEncodedSizeBytes]; stream::MemoryWriter writer(encode_buffer); RepeatedTest::StreamEncoder repeated_test(writer, ByteSpan()); const auto status = repeated_test.Write(message); ASSERT_EQ(status, OkStatus()); // clang-format off constexpr uint8_t expected_proto[] = { // uint32s[], v={0, 16, 32, 48} 0x0a, 0x04, 0x00, 0x10, 0x20, 0x30, // fixed32s[]. v={0, 16, 32, 48} 0x32, 0x10, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, }; // clang-format on ConstByteSpan result = writer.WrittenData(); EXPECT_EQ(result.size(), sizeof(expected_proto)); EXPECT_EQ(std::memcmp(result.data(), expected_proto, sizeof(expected_proto)), 0); } TEST(CodegenMessage, WritePackedScalarFixedLength) { RepeatedTest::Message message{}; for (int i = 0; i < 4; ++i) { message.uint64s[i] = (i + 1) * 1000u; } message.doubles[0] = 3.14159; message.doubles[1] = 2.71828; std::byte encode_buffer[RepeatedTest::kMaxEncodedSizeBytes]; stream::MemoryWriter writer(encode_buffer); RepeatedTest::StreamEncoder repeated_test(writer, ByteSpan()); const auto status = repeated_test.Write(message); ASSERT_EQ(status, OkStatus()); // clang-format off constexpr uint8_t expected_proto[] = { // doubles[], v={3.14159, 2.71828} 0x22, 0x10, 0x6e, 0x86, 0x1b, 0xf0, 0xf9, 0x21, 0x09, 0x40, 0x90, 0xf7, 0xaa, 0x95, 0x09, 0xbf, 0x05, 0x40, // uint64s[], v={1000, 2000, 3000, 4000} 0x42, 0x08, 0xe8, 0x07, 0xd0, 0x0f, 0xb8, 0x17, 0xa0, 0x1f, }; // clang-format on ConstByteSpan result = writer.WrittenData(); EXPECT_EQ(result.size(), sizeof(expected_proto)); EXPECT_EQ(std::memcmp(result.data(), expected_proto, sizeof(expected_proto)), 0); } TEST(CodegenMessage, WritePackedScalarCallback) { RepeatedTest::Message message{}; message.sint32s.SetEncoder([](RepeatedTest::StreamEncoder& encoder) { constexpr int32_t sint32s[] = {-25, -1, 0, 1, 25}; return encoder.WriteSint32s(sint32s); }); std::byte encode_buffer[RepeatedTest::kMaxEncodedSizeBytes + varint::kMaxVarint32SizeBytes * 5]; stream::MemoryWriter writer(encode_buffer); RepeatedTest::StreamEncoder repeated_test(writer, ByteSpan()); const auto status = repeated_test.Write(message); ASSERT_EQ(status, OkStatus()); // clang-format off constexpr uint8_t expected_proto[] = { // sint32s[], v={-25, -1, 0, 1, 25} 0x12, 0x05, 0x31, 0x01, 0x00, 0x02, 0x32, }; // clang-format on ConstByteSpan result = writer.WrittenData(); EXPECT_EQ(result.size(), sizeof(expected_proto)); EXPECT_EQ(std::memcmp(result.data(), expected_proto, sizeof(expected_proto)), 0); } TEST(CodegenMessage, WritePackedEnum) { RepeatedTest::Message message{}; message.enums.push_back(Enum::RED); message.enums.push_back(Enum::GREEN); message.enums.push_back(Enum::AMBER); message.enums.push_back(Enum::RED); std::byte encode_buffer[RepeatedTest::kMaxEncodedSizeBytes]; stream::MemoryWriter writer(encode_buffer); RepeatedTest::StreamEncoder repeated_test(writer, ByteSpan()); const auto status = repeated_test.Write(message); ASSERT_EQ(status, OkStatus()); // clang-format off constexpr uint8_t expected_proto[] = { // enums[], v={RED, GREEN, AMBER, RED} 0x4a, 0x04, 0x00, 0x02, 0x01, 0x00, }; // clang-format on ConstByteSpan result = writer.WrittenData(); EXPECT_EQ(result.size(), sizeof(expected_proto)); EXPECT_EQ(std::memcmp(result.data(), expected_proto, sizeof(expected_proto)), 0); } TEST(CodegenMessage, WriteStringCallback) { Pigweed::Message message{}; // pigweed.description has no max_size specified so a callback must be // set to write the value. message.description.SetEncoder([](Pigweed::StreamEncoder& encoder) { return encoder.WriteDescription( "an open source collection of embedded-targeted " "libraries-or as we like to call them, modules"); }); std::byte encode_buffer[Pigweed::kMaxEncodedSizeBytes + 92]; std::byte temp_buffer[Pigweed::kScratchBufferSizeBytes]; stream::MemoryWriter writer(encode_buffer); Pigweed::StreamEncoder pigweed(writer, temp_buffer); const auto status = pigweed.Write(message); ASSERT_EQ(status, OkStatus()); // clang-format off constexpr uint8_t expected_proto[] = { // pigweed.description 0x62, 0x5c, 'a', 'n', ' ', 'o', 'p', 'e', 'n', ' ', 's', 'o', 'u', 'r', 'c', 'e', ' ', 'c', 'o', 'l', 'l', 'e', 'c', 't', 'i', 'o', 'n', ' ', 'o', 'f', ' ', 'e', 'm', 'b', 'e', 'd', 'd', 'e', 'd', '-', 't', 'a', 'r', 'g', 'e', 't', 'e', 'd', ' ', 'l', 'i', 'b', 'r', 'a', 'r', 'i', 'e', 's', '-', 'o', 'r', ' ', 'a', 's', ' ', 'w', 'e', ' ', 'l', 'i', 'k', 'e', ' ', 't', 'o', ' ', 'c', 'a', 'l', 'l', ' ', 't', 'h', 'e', 'm', ',', ' ', 'm', 'o', 'd', 'u', 'l', 'e', 's', }; // clang-format on ConstByteSpan result = writer.WrittenData(); EXPECT_EQ(result.size(), sizeof(expected_proto)); EXPECT_EQ(std::memcmp(result.data(), expected_proto, sizeof(expected_proto)), 0); } TEST(CodegenMessage, WriteForcedCallback) { Pigweed::Message message{}; // pigweed.special_property has use_callback=true to force the use of a // callback even though it's a simple scalar. message.special_property.SetEncoder([](Pigweed::StreamEncoder& encoder) { return encoder.WriteSpecialProperty(42u); }); std::byte encode_buffer[Pigweed::kMaxEncodedSizeBytes]; std::byte temp_buffer[Pigweed::kScratchBufferSizeBytes]; stream::MemoryWriter writer(encode_buffer); Pigweed::StreamEncoder pigweed(writer, temp_buffer); const auto status = pigweed.Write(message); ASSERT_EQ(status, OkStatus()); // clang-format off constexpr uint8_t expected_proto[] = { // pigweed.special_property 0x68, 0x2a, }; // clang-format on ConstByteSpan result = writer.WrittenData(); EXPECT_EQ(result.size(), sizeof(expected_proto)); EXPECT_EQ(std::memcmp(result.data(), expected_proto, sizeof(expected_proto)), 0); } TEST(CodegenMessage, WriteNestedImported) { Period::Message message{}; message.start.seconds = 1517949900u; message.end.seconds = 1517950378u; std::byte encode_buffer[Period::kMaxEncodedSizeBytes]; std::byte temp_buffer[Period::kScratchBufferSizeBytes]; stream::MemoryWriter writer(encode_buffer); Period::StreamEncoder period(writer, temp_buffer); const auto status = period.Write(message); ASSERT_EQ(status, OkStatus()); // clang-format off constexpr uint8_t expected_proto[] = { // period.start 0x0a, 0x06, // period.start.seconds v=1517949900 0x08, 0xcc, 0xa7, 0xe8, 0xd3, 0x05, // period.end 0x12, 0x06, // period.end.seconds, v=1517950378 0x08, 0xaa, 0xab, 0xe8, 0xd3, 0x05, }; // clang-format on ConstByteSpan result = writer.WrittenData(); EXPECT_EQ(result.size(), sizeof(expected_proto)); EXPECT_EQ(std::memcmp(result.data(), expected_proto, sizeof(expected_proto)), 0); } TEST(CodegenMessage, WriteNestedRepeated) { RepeatedTest::Message message{}; // Repeated nested messages require a callback since there would otherwise be // no way to set callbacks on the nested message. message.structs.SetEncoder([](RepeatedTest::StreamEncoder& encoder) { for (int i = 0; i < 2; ++i) { Struct::Message struct_message{}; struct_message.one = i * 32 + 16u; struct_message.two = i * 32 + 32u; const auto status = encoder.GetStructsEncoder().Write(struct_message); EXPECT_EQ(status, OkStatus()); } return OkStatus(); }); std::byte encode_buffer[RepeatedTest::kMaxEncodedSizeBytes + Struct::kMaxEncodedSizeBytes * 2]; std::byte temp_buffer[RepeatedTest::kScratchBufferSizeBytes + Struct::kMaxEncodedSizeBytes]; stream::MemoryWriter writer(encode_buffer); RepeatedTest::StreamEncoder repeated_test(writer, temp_buffer); const auto status = repeated_test.Write(message); ASSERT_EQ(status, OkStatus()); // clang-format off constexpr uint8_t expected_proto[] = { // repeated.structs 0x2a, 0x04, // repeated.structs.one v=16 0x08, 0x10, // repeated.structs.two v=32 0x10, 0x20, // repeated.structs 0x2a, 0x04, // repeated.structs.one v=48 0x08, 0x30, // repeated.structs.two v=64 0x10, 0x40, }; // clang-format on ConstByteSpan result = writer.WrittenData(); EXPECT_EQ(result.size(), sizeof(expected_proto)); EXPECT_EQ(std::memcmp(result.data(), expected_proto, sizeof(expected_proto)), 0); } TEST(CodegenMessage, WriteNestedForcedCallback) { Pigweed::Message message{}; // pigweed.device_info has use_callback=true to force the use of a callback. message.device_info.SetEncoder([](Pigweed::StreamEncoder& encoder) { DeviceInfo::Message device_info{}; device_info.device_name = "pixel"; device_info.device_id = 0x08080808u; device_info.status = DeviceInfo::DeviceStatus::OK; // Use the callback to set nested callbacks. device_info.attributes.SetEncoder( [](DeviceInfo::StreamEncoder& device_info_encoder) { KeyValuePair::Message attribute{}; attribute.key = "version"; attribute.value = "5.3.1"; PW_TRY(device_info_encoder.GetAttributesEncoder().Write(attribute)); attribute.key = "chip"; attribute.value = "left-soc"; PW_TRY(device_info_encoder.GetAttributesEncoder().Write(attribute)); return OkStatus(); }); return encoder.GetDeviceInfoEncoder().Write(device_info); }); std::byte encode_buffer[Pigweed::kMaxEncodedSizeBytes + DeviceInfo::kMaxEncodedSizeBytes]; std::byte temp_buffer[Pigweed::kScratchBufferSizeBytes + DeviceInfo::kMaxEncodedSizeBytes]; stream::MemoryWriter writer(encode_buffer); Pigweed::StreamEncoder pigweed(writer, temp_buffer); const auto status = pigweed.Write(message); ASSERT_EQ(status, OkStatus()); // clang-format off constexpr uint8_t expected_proto[] = { // pigweed.device_info 0x32, 0x30, // pigweed.device_info.device_name 0x0a, 0x05, 'p', 'i', 'x', 'e', 'l', // pigweed.device_info.device_id 0x15, 0x08, 0x08, 0x08, 0x08, // pigweed.device_info.attributes[0] 0x22, 0x10, // pigweed.device_info.attributes[0].key 0x0a, 0x07, 'v', 'e', 'r', 's', 'i', 'o', 'n', // pigweed.device_info.attributes[0].value 0x12, 0x05, '5', '.', '3', '.', '1', // pigweed.device_info.attributes[1] 0x22, 0x10, // pigweed.device_info.attributes[1].key 0x0a, 0x04, 'c', 'h', 'i', 'p', // pigweed.device_info.attributes[1].value 0x12, 0x08, 'l', 'e', 'f', 't', '-', 's', 'o', 'c', }; // clang-format on ConstByteSpan result = writer.WrittenData(); EXPECT_EQ(result.size(), sizeof(expected_proto)); EXPECT_EQ(std::memcmp(result.data(), expected_proto, sizeof(expected_proto)), 0); } TEST(CodegenMessage, EnumAliases) { // Unprefixed enum. EXPECT_EQ(Bool::kTrue, Bool::TRUE); EXPECT_EQ(Bool::kFalse, Bool::FALSE); EXPECT_EQ(Bool::kFileNotFound, Bool::FILE_NOT_FOUND); // Prefixed enum has the prefix removed. EXPECT_EQ(Error::kNone, Error::ERROR_NONE); EXPECT_EQ(Error::kNotFound, Error::ERROR_NOT_FOUND); EXPECT_EQ(Error::kUnknown, Error::ERROR_UNKNOWN); // Single-value enum. EXPECT_EQ(AlwaysBlue::kBlue, AlwaysBlue::BLUE); } TEST(CodegenMessage, WriteOptionalPresent) { OptionalTest::Message message{}; message.sometimes_present_fixed = 0x2a; message.sometimes_present_varint = 0x2a; message.explicitly_present_fixed = 0x45; message.explicitly_present_varint = 0x45; message.sometimes_empty_fixed.push_back(0x63); message.sometimes_empty_varint.push_back(0x63); std::byte encode_buffer[512]; stream::MemoryWriter writer(encode_buffer); OptionalTest::StreamEncoder optional_test(writer, ByteSpan()); const auto status = optional_test.Write(message); ASSERT_EQ(status, OkStatus()); // clang-format off constexpr uint8_t expected_proto[] = { // optional.sometimes_present_fixed 0x0d, 0x2a, 0x00, 0x00, 0x00, // optional.sometimes_present_varint 0x10, 0x2a, // optional.explicitly_present_fixed 0x1d, 0x45, 0x00, 0x00, 0x00, // optional.explicitly_present_varint 0x20, 0x45, // optional.sometimes_empty_fixed 0x2a, 0x04, 0x63, 0x00, 0x00, 0x00, // optional.sometimes_empty_varint 0x32, 0x01, 0x63, }; // clang-format on ConstByteSpan result = writer.WrittenData(); EXPECT_EQ(result.size(), sizeof(expected_proto)); EXPECT_EQ(std::memcmp(result.data(), expected_proto, sizeof(expected_proto)), 0); } TEST(CodegenMessage, WriteOptionalNotPresent) { OptionalTest::Message message{}; std::byte encode_buffer[512]; stream::MemoryWriter writer(encode_buffer); OptionalTest::StreamEncoder optional_test(writer, ByteSpan()); const auto status = optional_test.Write(message); ASSERT_EQ(status, OkStatus()); // The expected proto is empty; no bytes should be written. ConstByteSpan result = writer.WrittenData(); EXPECT_TRUE(result.empty()); } TEST(CodegenMessage, WriteOptionalPresentDefaults) { OptionalTest::Message message{}; // Non-optional fields with a default value are not explicitly encoded, so // aren't meaningfully different from one that's just ommitted. message.sometimes_present_fixed = 0x00; message.sometimes_present_varint = 0x00; // Optional fields, even with a default value, are explicitly encoded. message.explicitly_present_fixed = 0x00; message.explicitly_present_varint = 0x00; // Repeated fields with a default value are meaningfully non-empty. message.sometimes_empty_fixed.push_back(0x00); message.sometimes_empty_varint.push_back(0x00); std::byte encode_buffer[512]; stream::MemoryWriter writer(encode_buffer); OptionalTest::StreamEncoder optional_test(writer, ByteSpan()); const auto status = optional_test.Write(message); ASSERT_EQ(status, OkStatus()); // clang-format off constexpr uint8_t expected_proto[] = { // optional.explicitly_present_fixed 0x1d, 0x00, 0x00, 0x00, 0x00, // optional.explicitly_present_varint 0x20, 0x00, // optional.sometimes_empty_fixed 0x2a, 0x04, 0x00, 0x00, 0x00, 0x00, // optional.sometimes_empty_varint 0x32, 0x01, 0x00, }; // clang-format on ConstByteSpan result = writer.WrittenData(); EXPECT_EQ(result.size(), sizeof(expected_proto)); EXPECT_EQ(std::memcmp(result.data(), expected_proto, sizeof(expected_proto)), 0); } class BreakableEncoder : public KeyValuePair::MemoryEncoder { public: constexpr BreakableEncoder(ByteSpan buffer) : KeyValuePair::MemoryEncoder(buffer) {} Status Write(const KeyValuePair::Message& message, span table) { return ::pw::protobuf::StreamEncoder::Write(as_bytes(span(&message, 1)), table); } }; TEST(CodegenMessage, DISABLED_WriteDoesNotOverrun) { // Deliberately construct a message table that attempts to violate the bounds // of the structure. We're not testing that a developer can't do this, rather // that the protobuf encoder can't be exploited in this way. constexpr internal::MessageField kMessageFields[] = { {1, WireType::kDelimited, sizeof(std::byte), static_cast(0), false, false, false, false, false, 0, sizeof(KeyValuePair::Message) * 2, {}}, }; std::byte encode_buffer[64]; BreakableEncoder encoder(encode_buffer); KeyValuePair::Message message{}; // ASSERT_CRASH std::ignore = encoder.Write(message, kMessageFields); } // The following tests cover using the codegen struct Message and callbacks in // different ways. // Check that the callback function object is large enough to implement a // "call a function on this" lambda. class StringChecker { public: StringChecker() = default; ~StringChecker() = default; Status Check(RepeatedTest::StreamDecoder& repeated_test) { RepeatedTest::Message message{}; message.strings.SetDecoder([this](RepeatedTest::StreamDecoder& decoder) { return this->CheckOne(decoder); }); return repeated_test.Read(message); } private: Status CheckOne(RepeatedTest::StreamDecoder& decoder) { EXPECT_EQ(decoder.Field().value(), RepeatedTest::Fields::kStrings); std::array strings{}; const StatusWithSize sws = decoder.ReadStrings(strings); EXPECT_EQ(sws.status(), OkStatus()); EXPECT_EQ(sws.size(), kExpectedStrings[i_].size()); EXPECT_EQ(std::memcmp(strings.data(), kExpectedStrings[i_].data(), kExpectedStrings[i_].size()), 0); ++i_; return sws.status(); } int i_ = 0; constexpr static std::string_view kExpectedStrings[] = { {"if music be the food of love, play on"}, {"give me excess of it, that, surfeiting"}, {"the appetite may sicken, and so die"}}; }; TEST(CodegenMessage, CallbackInClass) { // clang-format off constexpr uint8_t proto_data[] = { // repeated.strings 0x1a, 0x25, 'i', 'f', ' ', 'm', 'u', 's', 'i', 'c', ' ', 'b', 'e', ' ', 't', 'h', 'e', ' ', 'f', 'o', 'o', 'd', ' ', 'o', 'f', ' ', 'l', 'o', 'v', 'e', ',', ' ', 'p', 'l', 'a', 'y', ' ', 'o', 'n', // repeated.strings 0x1a, 0x26, 'g', 'i', 'v', 'e', ' ', 'm', 'e', ' ', 'e', 'x', 'c', 'e', 's', 's', ' ', 'o', 'f', ' ', 'i', 't', ',', ' ', 't', 'h', 'a', 't', ',', ' ', 's', 'u', 'r', 'f', 'e', 'i', 't', 'i', 'n', 'g', // repeated.strings 0x1a, 0x23, 't', 'h', 'e', ' ', 'a', 'p', 'p', 'e', 't', 'i', 't', 'e', ' ', 'm', 'a', 'y', ' ', 's', 'i', 'c', 'k', 'e', 'n', ',', ' ', 'a', 'n', 'd', ' ', 's', 'o', ' ', 'd', 'i', 'e', }; // clang-format on stream::MemoryReader reader(as_bytes(span(proto_data))); RepeatedTest::StreamDecoder repeated_test(reader); StringChecker checker{}; const auto status = checker.Check(repeated_test); ASSERT_EQ(status, OkStatus()); } // Check that we can create a custom subclass of the message struct that sets // its own callbacks to member functions that populate fields added in the // subclass. struct CustomMessage : RepeatedTest::Message { CustomMessage() : RepeatedTest::Message() { strings.SetDecoder([this](RepeatedTest::StreamDecoder& decoder) { return this->ParseStrings(decoder); }); } pw::Vector, 8> all_strings{}; private: Status ParseStrings(RepeatedTest::StreamDecoder& decoder) { PW_ASSERT(decoder.Field().value() == RepeatedTest::Fields::kStrings); std::array one_strings{}; const auto sws = decoder.ReadStrings(one_strings); if (!sws.ok()) { return sws.status(); } one_strings[sws.size()] = '\0'; all_strings.push_back(one_strings); return OkStatus(); } }; TEST(CodegenMessage, CallbackInSubclass) { // clang-format off constexpr uint8_t proto_data[] = { // repeated.strings 0x1a, 0x25, 'i', 'f', ' ', 'm', 'u', 's', 'i', 'c', ' ', 'b', 'e', ' ', 't', 'h', 'e', ' ', 'f', 'o', 'o', 'd', ' ', 'o', 'f', ' ', 'l', 'o', 'v', 'e', ',', ' ', 'p', 'l', 'a', 'y', ' ', 'o', 'n', // repeated.strings 0x1a, 0x26, 'g', 'i', 'v', 'e', ' ', 'm', 'e', ' ', 'e', 'x', 'c', 'e', 's', 's', ' ', 'o', 'f', ' ', 'i', 't', ',', ' ', 't', 'h', 'a', 't', ',', ' ', 's', 'u', 'r', 'f', 'e', 'i', 't', 'i', 'n', 'g', // repeated.strings 0x1a, 0x23, 't', 'h', 'e', ' ', 'a', 'p', 'p', 'e', 't', 'i', 't', 'e', ' ', 'm', 'a', 'y', ' ', 's', 'i', 'c', 'k', 'e', 'n', ',', ' ', 'a', 'n', 'd', ' ', 's', 'o', ' ', 'd', 'i', 'e', }; // clang-format on stream::MemoryReader reader(as_bytes(span(proto_data))); RepeatedTest::StreamDecoder repeated_test(reader); CustomMessage message{}; const auto status = repeated_test.Read(message); ASSERT_EQ(status, OkStatus()); constexpr static std::string_view kExpectedStrings[] = { {"if music be the food of love, play on"}, {"give me excess of it, that, surfeiting"}, {"the appetite may sicken, and so die"}}; EXPECT_EQ(message.all_strings.size(), 3u); for (int i = 0; i < 3; ++i) { EXPECT_EQ(std::memcmp(message.all_strings[i].data(), kExpectedStrings[i].data(), kExpectedStrings[i].size()), 0); EXPECT_EQ(message.all_strings[i].data()[kExpectedStrings[i].size()], '\0'); } } } // namespace } // namespace pw::protobuf