Skip to content

Commit 06a520c

Browse files
tonyliaosscopybara-github
authored andcommitted
Add wire format parsing unit tests for implicit presence fields.
This gist of the new test case coverage can be summarized as: - The wire does not distinguish between explicit and implicit fields. - For implicit presence fields, zeros on the wire can overwrite nonzero values (i.e. they are treated as a 'clear' operation). It's TBD whether we want to accept this behaviour going forward. Right now we are leaning towards keeping this behaviour, because: - If we receive zeros on the wire for implicit-presence fields, the protobuf wire format is "wrong" anyway. Well-behaved code should never generate zeros on the wire for implicit presence fields or serialize the same field multiple times. - There might be some value to enforce that "anything on the wire is accepted". This can make handling of wire format simpler. There are some drawbacks with this approach: - It might be somewhat surprising for users that zeros on the wire are always read, even for implicit-presence fields. - It might make the transition from implicit-presence to explicit-presence harder (or more unsafe) if user wants to migrate. - It leads to an inconsistency between what it means to "Merge". - Merging from a constructed object, with implicit presence and with field set to zero, will not overwrite. - Merging from the wire, with implicit presence and with zero explicitly present on the wire, WILL overwrite. I still need to add conformance tests to ensure that this is a consistent behavior across all languages, but for now let's at least add some coverage in C++ to ensure that this is a tested behaviour. PiperOrigin-RevId: 657724599
1 parent 6ab302d commit 06a520c

File tree

3 files changed

+82
-1
lines changed

3 files changed

+82
-1
lines changed

src/google/protobuf/BUILD.bazel

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1657,6 +1657,8 @@ cc_test(
16571657
deps = [
16581658
":cc_test_protos",
16591659
":protobuf",
1660+
"@com_google_absl//absl/log:absl_check",
1661+
"@com_google_absl//absl/strings:string_view",
16601662
"@com_google_googletest//:gtest",
16611663
"@com_google_googletest//:gtest_main",
16621664
],

src/google/protobuf/no_field_presence_test.cc

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@
88
#include <string>
99

1010
#include "google/protobuf/descriptor.pb.h"
11+
#include <gmock/gmock.h>
1112
#include <gtest/gtest.h>
13+
#include "absl/log/absl_check.h"
14+
#include "absl/strings/string_view.h"
1215
#include "google/protobuf/descriptor.h"
1316
#include "google/protobuf/unittest.pb.h"
1417
#include "google/protobuf/unittest_no_field_presence.pb.h"
@@ -17,6 +20,8 @@ namespace google {
1720
namespace protobuf {
1821
namespace {
1922

23+
using ::testing::StrEq;
24+
2025
// Helper: checks that all fields have default (zero/empty) values.
2126
void CheckDefaultValues(
2227
const proto2_nofieldpresence_unittest::TestAllTypes& m) {
@@ -457,6 +462,74 @@ TEST(NoFieldPresenceTest, MergeFromIfNonzeroTest) {
457462
EXPECT_EQ("test2", dest.optional_string());
458463
}
459464

465+
TEST(NoFieldPresenceTest, ExtraZeroesInWireParseTest) {
466+
// check extra serialized zeroes on the wire are parsed into the object.
467+
proto2_nofieldpresence_unittest::ForeignMessage dest;
468+
dest.set_c(42);
469+
ASSERT_EQ(42, dest.c());
470+
471+
// ExplicitForeignMessage has the same fields as ForeignMessage, but with
472+
// explicit presence instead of implicit presence.
473+
proto2_nofieldpresence_unittest::ExplicitForeignMessage source;
474+
source.set_c(0);
475+
std::string wire = source.SerializeAsString();
476+
ASSERT_THAT(wire, StrEq(absl::string_view{"\x08\x00", 2}));
477+
478+
// The "parse" operation clears all fields before merging from wire.
479+
ASSERT_TRUE(dest.ParseFromString(wire));
480+
EXPECT_EQ(0, dest.c());
481+
std::string dest_data;
482+
EXPECT_TRUE(dest.SerializeToString(&dest_data));
483+
EXPECT_TRUE(dest_data.empty());
484+
}
485+
486+
TEST(NoFieldPresenceTest, ExtraZeroesInWireMergeTest) {
487+
// check explicit zeros on the wire are merged into an implicit one.
488+
proto2_nofieldpresence_unittest::ForeignMessage dest;
489+
dest.set_c(42);
490+
ASSERT_EQ(42, dest.c());
491+
492+
// ExplicitForeignMessage has the same fields as ForeignMessage, but with
493+
// explicit presence instead of implicit presence.
494+
proto2_nofieldpresence_unittest::ExplicitForeignMessage source;
495+
source.set_c(0);
496+
std::string wire = source.SerializeAsString();
497+
ASSERT_THAT(wire, StrEq(absl::string_view{"\x08\x00", 2}));
498+
499+
// TODO: b/356132170 -- Add conformance tests to ensure this behaviour is
500+
// well-defined.
501+
// As implemented, the C++ "merge" operation does not distinguish between
502+
// implicit and explicit fields when reading from the wire.
503+
ASSERT_TRUE(dest.MergeFromString(wire));
504+
// If zero is present on the wire, the original value is overwritten, even
505+
// though this is specified as an "implicit presence" field.
506+
EXPECT_EQ(0, dest.c());
507+
std::string dest_data;
508+
EXPECT_TRUE(dest.SerializeToString(&dest_data));
509+
EXPECT_TRUE(dest_data.empty());
510+
}
511+
512+
TEST(NoFieldPresenceTest, ExtraZeroesInWireLastWins) {
513+
// check that, when the same field is present multiple times on the wire, we
514+
// always take the last one -- even if it is a zero.
515+
516+
absl::string_view wire{"\x08\x01\x08\x00", /*len=*/4}; // note the null-byte.
517+
proto2_nofieldpresence_unittest::ForeignMessage dest;
518+
519+
// TODO: b/356132170 -- Add conformance tests to ensure this behaviour is
520+
// well-defined.
521+
// As implemented, the C++ "merge" operation does not distinguish between
522+
// implicit and explicit fields when reading from the wire.
523+
ASSERT_TRUE(dest.MergeFromString(wire));
524+
// If the same field is present multiple times on the wire, "last one wins".
525+
// i.e. -- the last seen field content will always overwrite, even if it's
526+
// zero and the field is implicit presence.
527+
EXPECT_EQ(0, dest.c());
528+
std::string dest_data;
529+
EXPECT_TRUE(dest.SerializeToString(&dest_data));
530+
EXPECT_TRUE(dest_data.empty());
531+
}
532+
460533
TEST(NoFieldPresenceTest, IsInitializedTest) {
461534
// Check that IsInitialized works properly.
462535
proto2_nofieldpresence_unittest::TestProto2Required message;

src/google/protobuf/unittest_no_field_presence.proto

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ message TestAllTypes {
9292
repeated string repeated_string_piece = 54 [ctype = STRING_PIECE];
9393
repeated string repeated_cord = 55 [ctype = CORD];
9494

95-
repeated NestedMessage repeated_lazy_message = 57 ;
95+
repeated NestedMessage repeated_lazy_message = 57;
9696

9797
oneof oneof_field {
9898
uint32 oneof_uint32 = 111;
@@ -112,6 +112,12 @@ message ForeignMessage {
112112
int32 c = 1;
113113
}
114114

115+
// Same as ForeignMessage, but all fields have explicit presence.
116+
// It can be useful for testing explicit-implicit presence interop behaviour.
117+
message ExplicitForeignMessage {
118+
int32 c = 1 [features.field_presence = EXPLICIT];
119+
}
120+
115121
enum ForeignEnum {
116122
FOREIGN_FOO = 0;
117123
FOREIGN_BAR = 1;

0 commit comments

Comments
 (0)