Migrate the Go conformance test to the conformance_test() macro. The go_test ran conformance_test_runner against its own binary, which acts as the testee under RUN_AS_CONFORMANCE_PLUGIN=1. It becomes a manual testee target that only provides the binary, and the golang conformance_test() drives the suites against it with that variable set. The old failure lists are split per suite, unchanged, and stay google3-only. PiperOrigin-RevId: 984357922
diff --git a/cmake/conformance.cmake b/cmake/conformance.cmake index 7eedc94..24bd2e2 100644 --- a/cmake/conformance.cmake +++ b/cmake/conformance.cmake
@@ -141,11 +141,33 @@ target_include_directories(conformance_test_runner PRIVATE ${ABSL_ROOT_DIR}) target_include_directories(conformance_cpp PRIVATE ${ABSL_ROOT_DIR}) +# The runner hosts the gtest-based conformance suites (see +# conformance/conformance_test_main.cc), so it needs googletest even when +# protobuf_BUILD_TESTS is off. gtest.cmake is a no-op if GTest::gmock already +# exists, and otherwise finds or fetches googletest the same way the unit tests +# do. +include(${protobuf_SOURCE_DIR}/cmake/gtest.cmake) + target_link_libraries(conformance_test_runner libconformance_common ${protobuf_LIB_PROTOBUF} ${protobuf_ABSL_USED_TARGETS} + GTest::gmock ) +# The runner reads and sets gtest's flags (GTEST_FLAG_GET/GTEST_FLAG_SET), which +# are exported data symbols of the gtest library. When googletest is built as +# a shared library, consumers must compile with GTEST_LINKED_AS_SHARED_LIBRARY +# so that those symbols are declared dllimport on Windows; googletest only +# attaches that definition to its installed (find_package) targets, not to the +# ones FetchContent builds in-tree, so set it here for either case. +if(TARGET GTest::gtest) + get_target_property(_gtest_target_type GTest::gtest TYPE) + if(_gtest_target_type STREQUAL "SHARED_LIBRARY") + target_compile_definitions(conformance_test_runner + PRIVATE GTEST_LINKED_AS_SHARED_LIBRARY=1) + endif() +endif() + target_link_libraries(conformance_cpp libconformance_common ${protobuf_LIB_PROTOBUF}
diff --git a/conformance/BUILD b/conformance/BUILD index 11b0156..d1b764b 100644 --- a/conformance/BUILD +++ b/conformance/BUILD
@@ -7,6 +7,7 @@ # Protobuf conformance tests. +load("@bazel_skylib//:bzl_library.bzl", "bzl_library") load("@rules_cc//cc:cc_test.bzl", "cc_test") load("@rules_cc//cc:defs.bzl", "cc_binary", "cc_library", "objc_library") load("@rules_java//java:defs.bzl", "java_binary") @@ -23,6 +24,7 @@ load("//bazel:java_proto_library.bzl", "java_proto_library") load("//bazel:proto_library.bzl", "proto_library") load("//build_defs:internal_shell.bzl", "inline_sh_binary") +load("//conformance:conformance.bzl", "conformance_suite", "conformance_test", "failure_lists") exports_files([ "bazel_conformance_test_runner.sh", @@ -49,45 +51,78 @@ "text_format_failure_list_python.txt", ]) +# The command line and the report format of conformance_test_runner; see +# conformance_test_runner.h. cc_library( - name = "conformance_test_lib", + name = "conformance_test_runner_lib", testonly = 1, - srcs = [ - "conformance_test.cc", - "conformance_test_runner.cc", - ], - hdrs = [ - "conformance_test.h", - ], + srcs = ["conformance_test_runner.cc"], + hdrs = ["conformance_test_runner.h"], deps = [ - ":conformance_cc_proto", - ":failure_list_trie_node", - ":fork_pipe_runner", - ":recording_test_runner", - ":test_runner", "//src/google/protobuf", - "//src/google/protobuf:descriptor_legacy", - "//src/google/protobuf:endian", - "//src/google/protobuf/util:differencer", - "@abseil-cpp//absl/container:btree", "@abseil-cpp//absl/container:flat_hash_set", - "@abseil-cpp//absl/log:absl_check", - "@abseil-cpp//absl/log:absl_log", "@abseil-cpp//absl/status", + "@abseil-cpp//absl/status:statusor", "@abseil-cpp//absl/strings", "@abseil-cpp//absl/strings:str_format", - "@abseil-cpp//absl/types:optional", + "@abseil-cpp//absl/strings:string_view", + "@abseil-cpp//absl/types:span", ], ) +# The main of conformance_test_runner: runs the gtest-based conformance suites +# linked into the same binary behind the conformance_test_runner command line; +# see conformance_test_main.cc. cc_library( name = "conformance_test_main", testonly = 1, srcs = ["conformance_test_main.cc"], deps = [ - ":binary_json_conformance_suite", - ":conformance_test_lib", - ":text_format_conformance_suite", + # The gtest-based conformance suites, linked in as they are migrated. + # The performance suite is linked from the start, so that the runner's + # --performance selects its tests (see PerformanceGtestFilter()) as + # soon as there are any. + ":binary_conformance_tests", # buildcleaner: keep + ":json_conformance_tests", # buildcleaner: keep + ":performance_conformance_tests", # buildcleaner: keep + ":conformance_test_runner_lib", + ":filtering_test_runner", + ":fork_pipe_runner", + ":recording_test_runner", + ":test_environment", + ":result_ledger", + ":test_runner", + ":testee", + ":text_conformance_tests", # buildcleaner: keep + "//src/google/protobuf", + "@googletest//:gtest", + "@abseil-cpp//absl/base:nullability", + "@abseil-cpp//absl/container:flat_hash_map", + "@abseil-cpp//absl/container:flat_hash_set", + "@abseil-cpp//absl/status", + "@abseil-cpp//absl/status:statusor", + "@abseil-cpp//absl/strings", + "@abseil-cpp//absl/strings:str_format", + "@abseil-cpp//absl/strings:string_view", + "@abseil-cpp//absl/types:optional", + "@abseil-cpp//absl/types:span", + ], +) + +cc_test( + name = "conformance_test_runner_test", + srcs = ["conformance_test_runner_test.cc"], + deps = [ + ":conformance_test_runner_lib", + "//src/google/protobuf", + "@abseil-cpp//absl/log:absl_check", + "@abseil-cpp//absl/status", + "@abseil-cpp//absl/status:status_matchers", + "@abseil-cpp//absl/status:statusor", + "@abseil-cpp//absl/strings", + "@abseil-cpp//absl/strings:string_view", + "@googletest//:gtest", + "@googletest//:gtest_main", ], ) @@ -123,58 +158,57 @@ tests = [ "binary_test_util_test", "binary_wireformat_test", + "conformance_cpp_harness_test", + "conformance_test_runner_test", + "cpp_in_process_test_runner_test", + "cpp_in_process_testee_test", "failure_list_trie_node_test", + "filtering_test_runner_test", + "fork_pipe_runner_test", + "forked_testee_test", + "in_process_test_runner_test", + "in_process_testee_test", + "json_test_util_test", "matchers_test", + "message_type_fixtures_test", "naming_test", "recording_test_runner_test", + "recursion_limit_payloads_test", + "result_ledger_test", + "result_listener_test", + "result_record_test", + "sample_conformance_test", + "test_environment_flags_test", + "test_environment_integration_test", "test_environment_test", "test_fixture_test", - "test_manager_test", "testee_test", + "//conformance/bzl_tests:conformance_bzl_test", ], ) -cc_library( - name = "binary_json_conformance_suite", - testonly = 1, - srcs = ["binary_json_conformance_suite.cc"], - hdrs = ["binary_json_conformance_suite.h"], - deps = [ - ":binary_wireformat", - ":conformance_cc_proto", - ":conformance_test_lib", - "//:test_messages_proto2_cc_proto", - "//:test_messages_proto3_cc_proto", - "//conformance/test_protos:test_messages_edition2023_cc_proto", - "//conformance/test_protos:test_messages_edition2026_cc_proto", - "//conformance/test_protos:test_messages_edition_unstable_cc_proto", - "//editions:test_messages_proto2_editions_cc_proto", - "//editions:test_messages_proto3_editions_cc_proto", - "//src/google/protobuf", - "//src/google/protobuf:protobuf_lite", - "//src/google/protobuf/json", - "//src/google/protobuf/util:differencer", - "//src/google/protobuf/util:type_resolver", - "@abseil-cpp//absl/log:absl_check", - "@abseil-cpp//absl/log:absl_log", - "@abseil-cpp//absl/log:die_if_null", - "@abseil-cpp//absl/status", - "@abseil-cpp//absl/strings", - "@abseil-cpp//absl/strings:str_format", - "@abseil-cpp//absl/strings:string_view", - "@jsoncpp", - ], -) - -# Helpers shared by the gtest-based binary conformance tests (the field-type -# tables the tests are parameterized over). +# Helpers shared by the gtest-based binary conformance tests (field lookups, +# wire types, test-message parameter sets). cc_library( name = "binary_test_util", testonly = 1, srcs = ["binary_test_util.cc"], hdrs = ["binary_test_util.h"], deps = [ + ":binary_wireformat", + ":naming", + "//:test_messages_proto2_cc_proto", + "//:test_messages_proto3_cc_proto", + "//editions:test_messages_proto2_editions_cc_proto", + "//editions:test_messages_proto3_editions_cc_proto", "//src/google/protobuf", + "//src/google/protobuf:port", + "//src/google/protobuf:protobuf_lite", + "@abseil-cpp//absl/base:nullability", + "@abseil-cpp//absl/log:absl_check", + "@abseil-cpp//absl/log:absl_log", + "@abseil-cpp//absl/strings", + "@abseil-cpp//absl/strings:string_view", "@abseil-cpp//absl/types:span", ], ) @@ -184,38 +218,286 @@ srcs = ["binary_test_util_test.cc"], deps = [ ":binary_test_util", + ":binary_wireformat", + "//:test_messages_proto2_cc_proto", + "//:test_messages_proto3_cc_proto", + "//conformance/test_protos:test_messages_edition2023_cc_proto", + "//conformance/test_protos:test_messages_edition_unstable_cc_proto", + "//editions:test_messages_proto2_editions_cc_proto", + "//editions:test_messages_proto3_editions_cc_proto", "//src/google/protobuf", + "//src/google/protobuf/util:differencer", + "@abseil-cpp//absl/strings", "@googletest//:gtest", "@googletest//:gtest_main", ], ) cc_library( - name = "text_format_conformance_suite", + name = "recursion_limit_payloads", testonly = 1, - srcs = ["text_format_conformance_suite.cc"], - hdrs = ["text_format_conformance_suite.h"], + srcs = ["recursion_limit_payloads.cc"], + hdrs = ["recursion_limit_payloads.h"], deps = [ - ":conformance_test_lib", + ":binary_wireformat", + "@abseil-cpp//absl/log:absl_check", + "@abseil-cpp//absl/strings", + ], +) + +cc_test( + name = "recursion_limit_payloads_test", + srcs = ["recursion_limit_payloads_test.cc"], + deps = [ + ":binary_wireformat", + ":recursion_limit_payloads", + "//:test_messages_proto2_cc_proto", + "//conformance/test_protos:test_messages_edition2023_cc_proto", + "@googletest//:gtest", + "@googletest//:gtest_main", + ], +) + +cc_library( + name = "message_type_fixtures", + testonly = 1, + srcs = ["message_type_fixtures.cc"], + hdrs = ["message_type_fixtures.h"], + deps = [ + ":binary_test_util", + ":test_fixture", + "//conformance/test_protos:test_messages_edition2023_cc_proto", + "//src/google/protobuf", + "@abseil-cpp//absl/strings", + "@googletest//:gtest", + ], +) + +cc_test( + name = "message_type_fixtures_test", + srcs = ["message_type_fixtures_test.cc"], + deps = [ + ":binary_test_util", + ":message_type_fixtures", + ":mock_test_runner", + ":test_environment", + ":test_environment_testing", "//:test_messages_proto2_cc_proto", "//:test_messages_proto3_cc_proto", - "//conformance/test_protos:test_messages_edition2023_cc_proto", - "//conformance/test_protos:test_messages_edition2026_cc_proto", - "//conformance/test_protos:test_messages_edition_unstable_cc_proto", "//editions:test_messages_proto2_editions_cc_proto", - "//editions:test_messages_proto3_editions_cc_proto", "//src/google/protobuf", - "@abseil-cpp//absl/log:absl_log", - "@abseil-cpp//absl/log:die_if_null", "@abseil-cpp//absl/strings", - "@abseil-cpp//absl/strings:str_format", + "@abseil-cpp//absl/strings:string_view", + "@abseil-cpp//absl/types:optional", + "@googletest//:gtest", + "@googletest//:gtest_main", + ], +) + +# The packages using conformance_test() (conformance.bzl), which links the +# suite libraries below, :test_environment_main and :forked_testee into each +# test. Add a package here when it moves its conformance test to the macro +# (google3-only packages are marked to be stripped from the open-source +# export; upb's is exported along with its tests). +package_group( + name = "conformance_test_users", + packages = [ + "//conformance/...", + "//upb/conformance", + ], +) + +# The gtest-based binary conformance suite (:binary_conformance_tests), one +# file per migrated group. A single alwayslink library so that linking it into +# conformance_test_runner (or any binary using test_environment_main) registers +# every test; each newly migrated group just adds its file here. +conformance_suite( + name = "binary", + srcs = [ + "binary_delimited_field_test.cc", + "binary_illegal_lengths_test.cc", + "binary_illegal_tags_test.cc", + "binary_merge_test.cc", + "binary_message_set_test.cc", + "binary_oneof_test.cc", + "binary_oneof_zero_test.cc", + "binary_premature_eof_test.cc", + "binary_unknown_fields_test.cc", + "binary_unknown_wire_type_test.cc", + "binary_unmatched_group_test.cc", + "binary_unstable_edition_test.cc", + "binary_utf8_test.cc", + "binary_valid_data_map_test.cc", + "binary_valid_data_repeated_test.cc", + "binary_valid_data_scalar_test.cc", + ], + # conformance_test() links :binary_conformance_tests into every test it + # creates, so it must be visible wherever that macro is used (see + # :conformance_test_users; a package moving its conformance test to the + # macro is added there). + visibility = [":conformance_test_users"], + deps = [ + ":binary_test_util", + ":binary_wireformat", + ":matchers", + ":message_type_fixtures", + ":test_fixture", + ":testee", + "//:test_messages_proto2_cc_proto", + "//conformance/test_protos:test_messages_edition_unstable_cc_proto", + "//src/google/protobuf", + "//src/google/protobuf:port", + "//src/google/protobuf:protobuf_lite", + "@abseil-cpp//absl/log:absl_log", + "@abseil-cpp//absl/strings", + "@abseil-cpp//absl/strings:string_view", + "@googletest//:gtest", + ], +) + +# The gtest-based performance conformance suite +# (:performance_conformance_tests): the tests that stress the testee with +# deeply nested and very large payloads, for every wire format that has them. +# Every fixture in it must be named `*PerformanceTest`; see conformance.bzl. +conformance_suite( + name = "performance", + srcs = [ + "binary_performance_test.cc", + "binary_recursion_limit_test.cc", + "text_performance_test.cc", + ], + visibility = [":conformance_test_users"], + deps = [ + ":binary_test_util", + ":binary_wireformat", + ":matchers", + ":message_type_fixtures", + ":recursion_limit_payloads", + ":test_fixture", + ":testee", + "//:test_messages_proto2_cc_proto", + "//conformance/test_protos:test_messages_edition2023_cc_proto", + "//src/google/protobuf", + "@abseil-cpp//absl/strings", + "@abseil-cpp//absl/strings:string_view", + "@googletest//:gtest", + ], +) + +# The gtest-based text-format conformance suite (:text_conformance_tests); see +# the binary suite above. +conformance_suite( + name = "text", + srcs = [ + "text_any_test.cc", + "text_enum_test.cc", + "text_float_test.cc", + "text_group_test.cc", + "text_hello_world_test.cc", + "text_integer_test.cc", + "text_map_test.cc", + "text_reserved_field_test.cc", + "text_string_test.cc", + "text_syntax_test.cc", + "text_unknown_field_test.cc", + ], + # conformance_test() links :text_conformance_tests into every test it + # creates, so it must be visible wherever that macro is used (see the + # binary suite above). + visibility = [":conformance_test_users"], + deps = [ + ":binary_test_util", + ":binary_wireformat", + ":conformance_cc_proto", + ":matchers", + ":message_type_fixtures", + ":test_fixture", + ":testee", + "//:test_messages_proto2_cc_proto", + "//src/google/protobuf", + "@abseil-cpp//absl/strings", + "@abseil-cpp//absl/strings:string_view", + "@googletest//:gtest", + ], +) + +# Matchers and pure values shared by the JSON conformance tests; see +# :binary_test_util. +cc_library( + name = "json_test_util", + testonly = 1, + hdrs = ["json_test_util.h"], + deps = [ + "@abseil-cpp//absl/strings", + "@abseil-cpp//absl/strings:string_view", + "@googletest//:gtest", + "@jsoncpp", + ], +) + +cc_test( + name = "json_test_util_test", + srcs = ["json_test_util_test.cc"], + deps = [ + ":json_test_util", + "@googletest//:gtest", + "@googletest//:gtest_main", + "@jsoncpp", + ], +) + +# The gtest-based JSON conformance suite (:json_conformance_tests); see the +# binary suite above. +conformance_suite( + name = "json", + srcs = [ + "json_any_test.cc", + "json_bool_test.cc", + "json_duration_test.cc", + "json_enum_test.cc", + "json_field_mask_test.cc", + "json_field_name_test.cc", + "json_float_test.cc", + "json_hello_world_test.cc", + "json_integer_test.cc", + "json_map_test.cc", + "json_message_test.cc", + "json_null_test.cc", + "json_oneof_test.cc", + "json_repeated_test.cc", + "json_string_test.cc", + "json_struct_test.cc", + "json_timestamp_test.cc", + "json_unknown_enum_test.cc", + "json_value_test.cc", + "json_wrapper_test.cc", + ], + # conformance_test() links :json_conformance_tests into every test it + # creates, so it must be visible wherever that macro is used (see the + # binary suite above). + visibility = [":conformance_test_users"], + deps = [ + ":binary_test_util", + ":binary_wireformat", + ":conformance_cc_proto", + ":json_test_util", + ":matchers", + ":message_type_fixtures", + ":test_fixture", + ":testee", + "//src/google/protobuf", + "@abseil-cpp//absl/strings", + "@abseil-cpp//absl/strings:string_view", + "@googletest//:gtest", ], ) cc_library( name = "test_runner", hdrs = ["test_runner.h"], - visibility = ["//src/google/protobuf:__pkg__"], + # An in-process testee library (testee_runner.h) subclasses + # ConformanceTestRunner, so packages that define one need this directly. + visibility = [":conformance_test_users"], deps = ["@abseil-cpp//absl/strings:string_view"], ) @@ -303,6 +585,153 @@ ) cc_library( + name = "filtering_test_runner", + srcs = ["filtering_test_runner.cc"], + hdrs = ["filtering_test_runner.h"], + deps = [ + ":conformance_cc_proto", + ":test_runner", + "@abseil-cpp//absl/algorithm:container", + "@abseil-cpp//absl/base:nullability", + "@abseil-cpp//absl/container:flat_hash_set", + "@abseil-cpp//absl/strings:string_view", + ], +) + +cc_test( + name = "filtering_test_runner_test", + srcs = ["filtering_test_runner_test.cc"], + deps = [ + ":conformance_cc_proto", + ":filtering_test_runner", + ":mock_test_runner", + "@googletest//:gtest", + "@googletest//:gtest_main", + ], +) + +cc_library( + name = "in_process_test_runner", + testonly = 1, + srcs = ["in_process_test_runner.cc"], + hdrs = ["in_process_test_runner.h"], + deps = [ + ":conformance_cc_proto", + ":test_runner", + "@abseil-cpp//absl/functional:any_invocable", + "@abseil-cpp//absl/status:statusor", + "@abseil-cpp//absl/strings", + "@abseil-cpp//absl/strings:string_view", + ], +) + +cc_test( + name = "in_process_test_runner_test", + srcs = ["in_process_test_runner_test.cc"], + deps = [ + ":binary_wireformat", + ":conformance_cc_proto", + ":in_process_test_runner", + "//src/google/protobuf:test_textproto", + "@abseil-cpp//absl/log:absl_check", + "@abseil-cpp//absl/status", + "@abseil-cpp//absl/status:statusor", + "@abseil-cpp//absl/strings:string_view", + "@googletest//:gtest", + "@googletest//:gtest_main", + ], +) + +# The request-handling logic of the C++ conformance testee, shared by the +# conformance_cpp binary and by in-process conformance tests. +cc_library( + name = "conformance_cpp_harness", + testonly = 1, + srcs = ["conformance_cpp_harness.cc"], + hdrs = ["conformance_cpp_harness.h"], + deps = [ + ":conformance_cc_proto", + "//:any_cc_proto", + "//:api_cc_proto", + "//:duration_cc_proto", + "//:empty_cc_proto", + "//:field_mask_cc_proto", + "//:struct_cc_proto", + "//:test_messages_proto2_cc_proto", + "//:test_messages_proto3_cc_proto", + "//:timestamp_cc_proto", + "//:type_cc_proto", + "//:wrappers_cc_proto", + "//conformance/test_protos:test_messages_edition2023_cc_proto", + "//conformance/test_protos:test_messages_edition2026_cc_proto", + "//conformance/test_protos:test_messages_edition_unstable_cc_proto", + "//editions:test_messages_proto2_editions_cc_proto", + "//editions:test_messages_proto3_editions_cc_proto", + "//src/google/protobuf", + "//src/google/protobuf/json", + "@abseil-cpp//absl/log:absl_check", + "@abseil-cpp//absl/status", + "@abseil-cpp//absl/status:statusor", + "@abseil-cpp//absl/strings", + ], +) + +cc_test( + name = "conformance_cpp_harness_test", + srcs = ["conformance_cpp_harness_test.cc"], + deps = [ + ":binary_wireformat", + ":conformance_cc_proto", + ":conformance_cpp_harness", + "//:test_messages_proto3_cc_proto", + "//src/google/protobuf:test_textproto", + "@abseil-cpp//absl/status", + "@abseil-cpp//absl/status:status_matchers", + "@abseil-cpp//absl/status:statusor", + "@abseil-cpp//absl/strings", + "@abseil-cpp//absl/strings:string_view", + "@googletest//:gtest", + "@googletest//:gtest_main", + ], +) + +# Glue between the C++ testee (conformance_cpp_harness) and the tester side +# (in_process_test_runner). Kept separate so conformance_cpp links no runner +# code. +cc_library( + name = "cpp_in_process_test_runner", + testonly = 1, + srcs = ["cpp_in_process_test_runner.cc"], + hdrs = ["cpp_in_process_test_runner.h"], + deps = [ + ":conformance_cc_proto", + ":conformance_cpp_harness", + ":in_process_test_runner", + ":test_runner", + ], +) + +cc_test( + name = "cpp_in_process_test_runner_test", + srcs = ["cpp_in_process_test_runner_test.cc"], + deps = [ + ":conformance_cc_proto", + ":conformance_cpp_harness", + ":cpp_in_process_test_runner", + ":test_runner", + "//:test_messages_proto3_cc_proto", + "//src/google/protobuf:test_textproto", + "//src/google/protobuf/util:differencer", + "@abseil-cpp//absl/log:absl_check", + "@abseil-cpp//absl/status:status_matchers", + "@abseil-cpp//absl/status:statusor", + "@abseil-cpp//absl/strings:string_view", + "@googletest//:gtest", + "@googletest//:gtest_main", + ], +) + +cc_library( name = "binary_wireformat", testonly = 1, srcs = ["binary_wireformat.cc"], @@ -316,14 +745,63 @@ ) cc_library( - name = "test_manager", + name = "failure_list", testonly = True, - srcs = ["test_manager.cc"], - hdrs = ["test_manager.h"], + srcs = ["failure_list.cc"], + hdrs = ["failure_list.h"], deps = [ - ":conformance_cc_proto", ":failure_list_trie_node", ":testee", + "@abseil-cpp//absl/container:flat_hash_map", + "@abseil-cpp//absl/log:absl_check", + "@abseil-cpp//absl/status", + "@abseil-cpp//absl/strings", + "@abseil-cpp//absl/strings:string_view", + "@abseil-cpp//absl/types:optional", + ], +) + +cc_library( + name = "result_record", + testonly = True, + srcs = ["result_record.cc"], + hdrs = ["result_record.h"], + deps = [ + ":testee", + "@abseil-cpp//absl/algorithm:container", + "@abseil-cpp//absl/status", + "@abseil-cpp//absl/status:statusor", + "@abseil-cpp//absl/strings", + "@abseil-cpp//absl/strings:string_view", + "@abseil-cpp//absl/types:optional", + ], +) + +cc_test( + name = "result_record_test", + srcs = ["result_record_test.cc"], + deps = [ + ":result_record", + ":testee", + "@abseil-cpp//absl/status", + "@abseil-cpp//absl/status:status_matchers", + "@abseil-cpp//absl/strings", + "@abseil-cpp//absl/strings:string_view", + "@abseil-cpp//absl/types:optional", + "@googletest//:gtest", + "@googletest//:gtest_main", + ], +) + +cc_library( + name = "result_ledger", + testonly = True, + srcs = ["result_ledger.cc"], + hdrs = ["result_ledger.h"], + deps = [ + ":failure_list", + ":result_record", + ":testee", "@abseil-cpp//absl/algorithm:container", "@abseil-cpp//absl/container:btree", "@abseil-cpp//absl/container:flat_hash_map", @@ -338,6 +816,21 @@ ) cc_library( + name = "result_listener", + testonly = True, + srcs = ["result_listener.cc"], + hdrs = ["result_listener.h"], + deps = [ + ":result_ledger", + ":result_record", + "@abseil-cpp//absl/base:nullability", + "@abseil-cpp//absl/status", + "@abseil-cpp//absl/status:statusor", + "@googletest//:gtest", + ], +) + +cc_library( name = "naming", hdrs = ["naming.h"], deps = [ @@ -382,6 +875,8 @@ "@abseil-cpp//absl/log:absl_log", "@abseil-cpp//absl/strings", "@abseil-cpp//absl/strings:string_view", + "@abseil-cpp//absl/types:optional", + "@abseil-cpp//absl/types:span", ], ) @@ -406,10 +901,12 @@ ) cc_test( - name = "test_manager_test", - srcs = ["test_manager_test.cc"], + name = "result_ledger_test", + srcs = ["result_ledger_test.cc"], deps = [ - ":test_manager", + ":failure_list", + ":result_ledger", + ":result_record", ":testee", "//src/google/protobuf/testing", "//src/google/protobuf/testing:file", @@ -423,16 +920,31 @@ ], ) +cc_test( + name = "result_listener_test", + srcs = ["result_listener_test.cc"], + deps = [ + ":result_ledger", + ":result_listener", + ":result_record", + ":testee", + "@abseil-cpp//absl/strings", + "@abseil-cpp//absl/strings:string_view", + "@googletest//:gtest", + "@googletest//:gtest_main", + ], +) + cc_library( name = "global_test_environment", testonly = 1, hdrs = ["global_test_environment.h"], - deps = [":test_manager"], + deps = [":failure_list"], ) # gMock matchers for conformance TestResults. # -# NOTE: Consumers must link a definition of GetGlobalTestManager(), declared in +# NOTE: Consumers must link a definition of GetGlobalFailureList(), declared in # global_test_environment.h. The conformance test environment provides it in # the real test binary; matchers_test provides an in-memory stand-in. cc_library( @@ -443,22 +955,43 @@ deps = [ ":binary_wireformat", ":conformance_cc_proto", + ":failure_list", + ":filtering_test_runner", ":global_test_environment", - ":test_manager", + ":result_record", ":testee", "//src/google/protobuf", "//src/google/protobuf:port", + "//src/google/protobuf:protobuf_lite", + "//src/google/protobuf/json", "//src/google/protobuf/util:differencer", - "@abseil-cpp//absl/base:core_headers", "@abseil-cpp//absl/base:nullability", "@abseil-cpp//absl/log:absl_check", "@abseil-cpp//absl/log:absl_log", "@abseil-cpp//absl/memory", "@abseil-cpp//absl/status", + "@abseil-cpp//absl/status:statusor", "@abseil-cpp//absl/strings", "@abseil-cpp//absl/strings:str_format", "@abseil-cpp//absl/strings:string_view", - "@abseil-cpp//absl/synchronization", + "@abseil-cpp//absl/types:optional", + "@googletest//:gtest", + "@jsoncpp", + ], +) + +# Helpers for unit tests of code that checks results with Yields(). +cc_library( + name = "matchers_testing", + testonly = 1, + srcs = ["matchers_testing.cc"], + hdrs = ["matchers_testing.h"], + deps = [ + ":result_record", + "@abseil-cpp//absl/container:flat_hash_set", + "@abseil-cpp//absl/status:statusor", + "@abseil-cpp//absl/strings", + "@abseil-cpp//absl/strings:string_view", "@abseil-cpp//absl/types:optional", "@googletest//:gtest", ], @@ -470,13 +1003,18 @@ deps = [ ":binary_wireformat", ":conformance_cc_proto", + ":failure_list", ":global_test_environment", + ":json_test_util", ":matchers", + ":matchers_testing", ":mock_test_runner", - ":test_manager", + ":result_ledger", + ":result_record", ":testee", "//:test_messages_proto2_cc_proto", "//src/google/protobuf", + "//src/google/protobuf:protobuf_lite", "@abseil-cpp//absl/base:log_severity", "@abseil-cpp//absl/log:absl_check", "@abseil-cpp//absl/log:scoped_mock_log", @@ -486,6 +1024,7 @@ "@abseil-cpp//absl/types:optional", "@googletest//:gtest", "@googletest//:gtest_main", + "@jsoncpp", ], ) @@ -507,9 +1046,11 @@ srcs = ["test_environment.cc"], hdrs = ["test_environment.h"], deps = [ + ":failure_list", ":fork_pipe_runner", ":global_test_environment", - ":test_manager", + ":result_ledger", + ":result_listener", ":test_runner", ":testee", "//src/google/protobuf", @@ -520,6 +1061,7 @@ "@abseil-cpp//absl/strings", "@abseil-cpp//absl/strings:string_view", "@abseil-cpp//absl/types:optional", + "@abseil-cpp//absl/types:span", "@googletest//:gtest", ], ) @@ -544,10 +1086,13 @@ ":conformance_cc_proto", ":global_test_environment", ":matchers", + ":matchers_testing", ":mock_test_runner", + ":result_ledger", + ":result_listener", + ":result_record", ":test_environment", ":test_environment_testing", - ":test_manager", ":testee", "//:test_messages_proto3_cc_proto", "//src/google/protobuf", @@ -574,7 +1119,6 @@ "//src/google/protobuf", "//src/google/protobuf:descriptor_legacy", "@abseil-cpp//absl/base:nullability", - "@abseil-cpp//absl/container:flat_hash_map", "@abseil-cpp//absl/log:absl_check", "@abseil-cpp//absl/strings:string_view", "@googletest//:gtest", @@ -606,6 +1150,246 @@ ], ) +# Defines the global --testee_binary, --failure_list, --fix, ... flags of a +# conformance test binary. Only link it into binaries with a +# test_environment_main-style main (never into conformance_test_runner, whose +# command line parses flags of the same names itself). +cc_library( + name = "test_environment_flags", + testonly = 1, + srcs = ["test_environment_flags.cc"], + hdrs = ["test_environment_flags.h"], + deps = [ + ":test_environment", + ":testee", + "//src/google/protobuf", + "@abseil-cpp//absl/flags:flag", + "@abseil-cpp//absl/types:span", + ], +) + +cc_test( + name = "test_environment_flags_test", + srcs = ["test_environment_flags_test.cc"], + deps = [ + ":test_environment", + ":test_environment_flags", + ":testee", + "//src/google/protobuf", + "@abseil-cpp//absl/flags:flag", + "@abseil-cpp//absl/flags:parse", + "@abseil-cpp//absl/flags:reflection", + "@abseil-cpp//absl/strings:string_view", + "@googletest//:gtest", + "@googletest//:gtest_main", + ], +) + +# The main() for gtest-based conformance suites. Depend on this instead of a +# generic gtest main, together with exactly one library defining +# MakeTesteeRunner() (see :testee_runner): :forked_testee, or an in-process +# testee. conformance_test() (conformance.bzl) adds them as implicit deps, so +# they must be visible wherever that macro is used (see +# :conformance_test_users). +cc_library( + name = "test_environment_main", + testonly = 1, + srcs = ["test_environment_main.cc"], + visibility = [":conformance_test_users"], + deps = [ + ":test_environment", + ":test_environment_flags", + ":testee_runner", + "@abseil-cpp//absl/flags:parse", # buildcleaner: keep + "@abseil-cpp//absl/log:initialize", # buildcleaner: keep + "@abseil-cpp//absl/types:span", + "@googletest//:gtest", + ], + alwayslink = 1, +) + +# The link-time seam through which test_environment_main gets its testee: the +# declaration of MakeTesteeRunner(), which an in-process testee library defines. +cc_library( + name = "testee_runner", + testonly = 1, + hdrs = ["testee_runner.h"], + visibility = [":conformance_test_users"], + deps = [ + ":test_runner", + "@abseil-cpp//absl/strings:string_view", + "@abseil-cpp//absl/types:span", + ], +) + +# The default testee: --testee_binary, spawned as a child process. Like every +# library defining MakeTesteeRunner() it is alwayslink, so that the definition +# is linked in wherever the library appears on the link line. +cc_library( + name = "forked_testee", + testonly = 1, + srcs = ["forked_testee.cc"], + visibility = [":conformance_test_users"], + deps = [ + ":fork_pipe_runner", + ":test_runner", + ":testee_runner", + "@abseil-cpp//absl/log:absl_check", + "@abseil-cpp//absl/strings:string_view", + "@abseil-cpp//absl/types:span", + ], + alwayslink = 1, +) + +# The C++ runtime's in-process testee: defines MakeTesteeRunner() on top of +# cpp_in_process_test_runner. conformance_test(in_process = True) links it in +# place of :forked_testee; see conformance.bzl for the one-per-test rule. +cc_library( + name = "cpp_in_process_testee", + testonly = 1, + srcs = ["cpp_in_process_testee.cc"], + deps = [ + ":cpp_in_process_test_runner", + ":test_runner", + ":testee_runner", + "@abseil-cpp//absl/log:absl_check", + "@abseil-cpp//absl/strings", + "@abseil-cpp//absl/strings:string_view", + "@abseil-cpp//absl/types:span", + ], + alwayslink = 1, +) + +cc_test( + name = "cpp_in_process_testee_test", + srcs = ["cpp_in_process_testee_test.cc"], + deps = [ + ":conformance_cc_proto", + ":cpp_in_process_testee", + ":test_runner", + ":testee_runner", + "//:test_messages_proto3_cc_proto", + "@googletest//:gtest", + "@googletest//:gtest_main", + ], +) + +cc_test( + name = "forked_testee_test", + srcs = ["forked_testee_test.cc"], + deps = [ + ":forked_testee", + ":test_runner", + ":testee_runner", + "@googletest//:gtest", + "@googletest//:gtest_main", + ], +) + +# A conformance test binary whose testee is linked in (the test file defines +# MakeTesteeRunner() itself), run without --testee_binary: checks that +# test_environment_main hosts an in-process testee end to end. +cc_test( + name = "in_process_testee_test", + srcs = ["in_process_testee_test.cc"], + deps = [ + ":binary_wireformat", + ":conformance_cc_proto", + ":matchers", + ":test_environment_main", + ":test_fixture", + ":test_runner", + ":testee_runner", + "//:test_messages_proto3_cc_proto", + "@abseil-cpp//absl/log:absl_check", + "@abseil-cpp//absl/strings:string_view", + "@abseil-cpp//absl/types:span", + "@googletest//:gtest", + ], +) + +bzl_library( + name = "conformance_bzl", + srcs = ["conformance.bzl"], + visibility = ["//conformance/bzl_tests:__pkg__"], + deps = [ + "@rules_cc//cc:core_rules", + ], +) + +# The expected-failure lists of the conformance_test()s below, one file per +# test and suite (e.g. failure_lists/cpp_binary.txt). Update them with +# `blaze run :<test>_<suite>_test -- --fix`; see conformance.bzl. +failure_lists(name = "failure_lists") + +# The gtest-based conformance tests of the C++ testee: cpp_binary_test, +# cpp_json_test, cpp_performance_test and cpp_text_test, grouped under the :cpp +# test_suite. They host the testee in-process (:cpp_in_process_testee) rather +# than forking :conformance_cpp, so a testee crash or log shows up in the test +# itself. +conformance_test( + name = "cpp", + failure_lists = ":failure_lists", + in_process = True, + maximum_edition = "2023", + testee = ":cpp_in_process_testee", +) + +# A tiny conformance suite exercising the framework end to end. It +# deliberately contains failing tests, which the accompanying failure lists +# account for. +cc_test( + name = "sample_conformance_test", + srcs = ["sample_conformance_test.cc"], + args = [ + "--testee_binary=$(rootpath :conformance_cpp)", + "--failure_list=$(rootpath sample_failure_list_ok.txt)", + ], + data = [ + "sample_failure_list_ok.txt", + ":conformance_cpp", + ], + deps = [ + ":binary_wireformat", + ":forked_testee", + ":matchers", + ":test_environment_main", + ":test_fixture", + ":testee", + "//:test_messages_proto3_cc_proto", + "@googletest//:gtest", + ], +) + +# Runs sample_conformance_test as a subprocess. Only what needs a real test +# process is checked here; see test_environment_test for the rest. +cc_test( + name = "test_environment_integration_test", + srcs = ["test_environment_integration_test.cc"], + data = [ + "sample_failure_list_empty.txt", + "sample_failure_list_ok.txt", + "sample_failure_list_unseen.txt", + ":conformance_cpp", + ":sample_conformance_test", + ], + env = { + "SAMPLE_TEST": "$(rlocationpath :sample_conformance_test)", + "TESTEE": "$(rlocationpath :conformance_cpp)", + }, + deps = [ + "//src/google/protobuf/testing", + "//src/google/protobuf/testing:file", + "@abseil-cpp//absl/log:absl_check", + "@abseil-cpp//absl/strings", + "@abseil-cpp//absl/strings:string_view", + "@abseil-cpp//absl/types:span", + "@bazel_tools//tools/cpp/runfiles", + "@googletest//:gtest", + "@googletest//:gtest_main", + ], +) + cc_binary( name = "conformance_test_runner", testonly = 1, @@ -712,35 +1496,15 @@ ], deps = [ ":conformance_cc_proto", - "//:any_cc_proto", - "//:api_cc_proto", - "//:duration_cc_proto", - "//:empty_cc_proto", - "//:field_mask_cc_proto", + ":conformance_cpp_harness", "//:protobuf", - "//:struct_cc_proto", - "//:test_messages_proto2_cc_proto", - "//:test_messages_proto3_cc_proto", - "//:timestamp_cc_proto", - "//:type_cc_proto", - "//:wrappers_cc_proto", - "//conformance/test_protos:test_messages_edition2023_cc_proto", - "//conformance/test_protos:test_messages_edition2026_cc_proto", - "//conformance/test_protos:test_messages_edition_unstable_cc_proto", - "//editions:test_messages_proto2_editions_cc_proto", - "//editions:test_messages_proto3_editions_cc_proto", "//src/google/protobuf", - "//src/google/protobuf:port", - "//src/google/protobuf:protobuf_lite", - "//src/google/protobuf/json", + "//src/google/protobuf:endian", "//src/google/protobuf/stubs", - "//src/google/protobuf/util:json_util", - "//src/google/protobuf/util:type_resolver", "@abseil-cpp//absl/log:absl_check", "@abseil-cpp//absl/log:absl_log", "@abseil-cpp//absl/status", "@abseil-cpp//absl/status:statusor", - "@abseil-cpp//absl/strings", ], )
diff --git a/conformance/binary_delimited_field_test.cc b/conformance/binary_delimited_field_test.cc new file mode 100644 index 0000000..52b6324 --- /dev/null +++ b/conformance/binary_delimited_field_test.cc
@@ -0,0 +1,188 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2026 Google LLC. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +// Binary conformance tests for TestAllTypesEdition2023: length-prefixed and +// delimited (group-encoded) message fields, maps, and delimited extensions +// under editions. This holds both the binary-output and the JSON-output leg +// of the legacy suite-level +// BinaryAndJsonConformanceSuite::RunDelimitedFieldTests(); the test names and +// the requests sent to the testee are identical to the legacy ones. + +#include <gmock/gmock.h> +#include <gtest/gtest.h> +#include "conformance/binary_wireformat.h" +#include "conformance/matchers.h" +#include "conformance/message_type_fixtures.h" +#include "conformance/testee.h" + +namespace google { +namespace protobuf { +namespace conformance { +namespace { + +// Every test here parses TestAllTypesEdition2023 (message()), so the fixtures +// skip themselves when --maximum_edition doesn't cover editions. +using DelimitedFieldTest = Edition2023ConformanceTest; + +// Field 1 is optional_int32. +TEST_F(DelimitedFieldTest, ValidNonMessage) { + EXPECT_THAT(Testee("ValidNonMessage") + .ParseBinary(message(), VarintField(1, 99)) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(R"pb(optional_int32: 99)pb")))); +} + +TEST_F(DelimitedFieldTest, ValidNonMessageJson) { + EXPECT_THAT(Testee("ValidNonMessage") + .ParseBinary(message(), VarintField(1, 99)) + .SerializeJson(), + Yields(WhenParsed(EqualsTextProto(R"pb(optional_int32: 99)pb")))); +} + +// Field 18 is optional_nested_message, whose field 1 is a. +TEST_F(DelimitedFieldTest, ValidLengthPrefixedField) { + EXPECT_THAT( + Testee("ValidLengthPrefixedField") + .ParseBinary(message(), LengthPrefixedField(18, VarintField(1, 99))) + .SerializeBinary(), + Yields(WhenParsed( + EqualsTextProto(R"pb(optional_nested_message { a: 99 })pb")))); +} + +TEST_F(DelimitedFieldTest, ValidLengthPrefixedFieldJson) { + EXPECT_THAT( + Testee("ValidLengthPrefixedField") + .ParseBinary(message(), LengthPrefixedField(18, VarintField(1, 99))) + .SerializeJson(), + Yields(WhenParsed( + EqualsTextProto(R"pb(optional_nested_message { a: 99 })pb")))); +} + +// Field 56 is map_int32_int32; map entries have key = 1 and value = 2. +TEST_F(DelimitedFieldTest, ValidMapInteger) { + EXPECT_THAT( + Testee("ValidMap.Integer") + .ParseBinary(message(), + LengthPrefixedField( + 56, Wire(VarintField(1, 99), VarintField(2, 87)))) + .SerializeBinary(), + Yields(WhenParsed( + EqualsTextProto(R"pb(map_int32_int32 { key: 99 value: 87 })pb")))); +} + +TEST_F(DelimitedFieldTest, ValidMapIntegerJson) { + EXPECT_THAT( + Testee("ValidMap.Integer") + .ParseBinary(message(), + LengthPrefixedField( + 56, Wire(VarintField(1, 99), VarintField(2, 87)))) + .SerializeJson(), + Yields(WhenParsed( + EqualsTextProto(R"pb(map_int32_int32 { key: 99 value: 87 })pb")))); +} + +// Field 71 is map_string_nested_message. +TEST_F(DelimitedFieldTest, ValidMapLengthPrefixed) { + EXPECT_THAT(Testee("ValidMap.LengthPrefixed") + .ParseBinary( + message(), + LengthPrefixedField( + 71, Wire(LengthPrefixedField(1, "a"), + LengthPrefixedField(2, VarintField(1, 87))))) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(R"pb(map_string_nested_message { + key: "a" + value: { a: 87 } + })pb")))); +} + +TEST_F(DelimitedFieldTest, ValidMapLengthPrefixedJson) { + EXPECT_THAT(Testee("ValidMap.LengthPrefixed") + .ParseBinary( + message(), + LengthPrefixedField( + 71, Wire(LengthPrefixedField(1, "a"), + LengthPrefixedField(2, VarintField(1, 87))))) + .SerializeJson(), + Yields(WhenParsed(EqualsTextProto(R"pb(map_string_nested_message { + key: "a" + value: { a: 87 } + })pb")))); +} + +// Field 201 is groupliketype (whose name matches its type, as a proto2 group +// would) and field 202 is delimited_field (whose name doesn't). Both are +// GroupLikeType, whose field 202 is group_int32. +TEST_F(DelimitedFieldTest, GroupLike) { + EXPECT_THAT( + Testee("ValidDelimitedField.GroupLike") + .ParseBinary(message(), DelimitedField(201, VarintField(202, 99))) + .SerializeBinary(), + Yields(WhenParsed( + EqualsTextProto(R"pb(groupliketype { group_int32: 99 })pb")))); +} + +TEST_F(DelimitedFieldTest, GroupLikeJson) { + EXPECT_THAT( + Testee("ValidDelimitedField.GroupLike") + .ParseBinary(message(), DelimitedField(201, VarintField(202, 99))) + .SerializeJson(), + Yields(WhenParsed( + EqualsTextProto(R"pb(groupliketype { group_int32: 99 })pb")))); +} + +TEST_F(DelimitedFieldTest, NotGroupLike) { + EXPECT_THAT( + Testee("ValidDelimitedField.NotGroupLike") + .ParseBinary(message(), DelimitedField(202, VarintField(202, 99))) + .SerializeBinary(), + Yields(WhenParsed( + EqualsTextProto(R"pb(delimited_field { group_int32: 99 })pb")))); +} + +TEST_F(DelimitedFieldTest, NotGroupLikeJson) { + EXPECT_THAT( + Testee("ValidDelimitedField.NotGroupLike") + .ParseBinary(message(), DelimitedField(202, VarintField(202, 99))) + .SerializeJson(), + Yields(WhenParsed( + EqualsTextProto(R"pb(delimited_field { group_int32: 99 })pb")))); +} + +// Only TestAllTypesEdition2023 declares delimited extensions: field 121 is +// the groupliketype extension (whose name matches its type, as a proto2 group +// would) and field 122 is delimited_ext (whose name doesn't). Both are +// GroupLikeType, whose field 1 is c. Like the legacy suite, these have no +// JSON-output leg. +using DelimitedExtensionTest = Edition2023ConformanceTest; + +TEST_F(DelimitedExtensionTest, GroupLike) { + EXPECT_THAT( + Testee("ValidDelimitedExtension.GroupLike") + .ParseBinary(message(), DelimitedField(121, VarintField(1, 99))) + .SerializeBinary(), + Yields(WhenParsed( + EqualsTextProto(R"pb([protobuf_test_messages.editions.groupliketype] { + c: 99 + })pb")))); +} + +TEST_F(DelimitedExtensionTest, NotGroupLike) { + EXPECT_THAT( + Testee("ValidDelimitedExtension.NotGroupLike") + .ParseBinary(message(), DelimitedField(122, VarintField(1, 99))) + .SerializeBinary(), + Yields(WhenParsed( + EqualsTextProto(R"pb([protobuf_test_messages.editions.delimited_ext] { + c: 99 + })pb")))); +} + +} // namespace +} // namespace conformance +} // namespace protobuf +} // namespace google
diff --git a/conformance/binary_illegal_lengths_test.cc b/conformance/binary_illegal_lengths_test.cc new file mode 100644 index 0000000..023f86f --- /dev/null +++ b/conformance/binary_illegal_lengths_test.cc
@@ -0,0 +1,51 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2026 Google LLC. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +// Binary conformance tests checking that illegal length prefixes are +// rejected. This replaces the legacy +// BinaryAndJsonConformanceSuiteImpl<M>::TestIllegalLengths(); the test names +// and the requests sent to the testee are identical to the legacy ones. + +#include <gmock/gmock.h> +#include <gtest/gtest.h> +#include "conformance/binary_test_util.h" +#include "conformance/binary_wireformat.h" +#include "conformance/matchers.h" +#include "conformance/message_type_fixtures.h" +#include "conformance/testee.h" +#include "google/protobuf/descriptor.h" + +namespace google { +namespace protobuf { +namespace conformance { +namespace { + +using ::testing::ValuesIn; + +using IllegalLengthsTest = MessageTypeConformanceTest; + +// A 5-byte varint length whose bit 32 is set (0x10 in the last byte), so that +// it wraps to 0 in 32-bit arithmetic. Parsers must not overflow and must +// reject the length. +TEST_P(IllegalLengthsTest, BadLengthVarint32BitOverflow) { + const FieldDescriptor& string_field = *GetFieldForType( + *message(), FieldDescriptor::TYPE_STRING, /*repeated=*/false); + EXPECT_THAT(Testee("BadLength_Varint32BitOverflow") + .ParseBinary(message(), Wire(Tag(FieldNumber(string_field), + WireType::kLengthPrefixed), + "\x80\x80\x80\x80\x10")) + .ParseOnly(), + Yields(IsParseError())); +} + +INSTANTIATE_TEST_SUITE_P(All, IllegalLengthsTest, + ValuesIn(AllTestMessageTypes()), MessageTypeParamName); + +} // namespace +} // namespace conformance +} // namespace protobuf +} // namespace google
diff --git a/conformance/binary_illegal_tags_test.cc b/conformance/binary_illegal_tags_test.cc new file mode 100644 index 0000000..08b23dc --- /dev/null +++ b/conformance/binary_illegal_tags_test.cc
@@ -0,0 +1,123 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2026 Google LLC. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +// Binary conformance tests checking that illegal field tags are rejected: a +// field number of zero, and tag varints whose field number is out of range or +// that are overlong. This replaces the legacy +// BinaryAndJsonConformanceSuiteImpl<M>::TestIllegalTags(); the test names and +// the requests sent to the testee are identical to the legacy ones. + +#include <gmock/gmock.h> +#include <gtest/gtest.h> +#include "absl/strings/string_view.h" +#include "conformance/binary_test_util.h" +#include "conformance/binary_wireformat.h" +#include "conformance/matchers.h" +#include "conformance/message_type_fixtures.h" +#include "conformance/testee.h" + +namespace google { +namespace protobuf { +namespace conformance { +namespace { + +using ::testing::ValuesIn; + +// The one-byte tag of field number 1 with wire type varint (0x08) with its +// continuation bit (0x80) set, i.e. the first byte of a longer tag varint. The +// BadTag tests append further bytes to it. +constexpr absl::string_view kTagWithContinuationBit = "\x88"; + +// The last byte of an overlong varint: neither continuation bit nor value +// bits. Spelled with an explicit length since it is a NUL. +constexpr absl::string_view kVarintTerminator("\0", 1); + +using IllegalTagsTest = MessageTypeConformanceTest; + +// Field number 0 is illegal, whatever the wire type. (The leading "\1" etc. +// are octal escapes: the tag bytes 0x01, 0x02, 0x03 and 0x05.) +TEST_P(IllegalTagsTest, IllegalZeroFieldNumCase0) { + EXPECT_THAT(Testee("IllegalZeroFieldNum_Case_0") + .ParseBinary(message(), Wire("\1DEADBEEF")) + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(IllegalTagsTest, IllegalZeroFieldNumCase1) { + EXPECT_THAT(Testee("IllegalZeroFieldNum_Case_1") + .ParseBinary(message(), Wire("\2\1\1")) + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(IllegalTagsTest, IllegalZeroFieldNumCase2) { + EXPECT_THAT(Testee("IllegalZeroFieldNum_Case_2") + .ParseBinary(message(), Wire("\3\4")) + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(IllegalTagsTest, IllegalZeroFieldNumCase3) { + EXPECT_THAT(Testee("IllegalZeroFieldNum_Case_3") + .ParseBinary(message(), Wire("\5DEAD")) + .ParseOnly(), + Yields(IsParseError())); +} + +// A tag varint whose field number is far out of range of the maximum legal +// field number. The lower 5 bytes of the varint do look like a well-formed +// tag for field number 1. +TEST_P(IllegalTagsTest, BadTagFieldNumberTooHigh) { + EXPECT_THAT(Testee("BadTag_FieldNumberTooHigh") + .ParseBinary(message(), + Wire(kTagWithContinuationBit, + "\x80\x80\x80\x80\x80\x0F", Varint(1234))) + .ParseOnly(), + Yields(IsParseError())); +} + +// A 5-byte tag varint whose value is above UINT32_MAX (bit 35 is set). +TEST_P(IllegalTagsTest, BadTagFieldNumberSlightlyTooHigh) { + EXPECT_THAT( + Testee("BadTag_FieldNumberSlightlyTooHigh") + .ParseBinary(message(), Wire(kTagWithContinuationBit, + "\x80\x80\x80\x40", Varint(1234))) + .ParseOnly(), + Yields(IsParseError())); +} + +// A tag varint that is more than 5 bytes only because it is overlong (seven +// empty continuation bytes), so the decoded value is still below UINT32_MAX. +TEST_P(IllegalTagsTest, BadTagOverlongVarint) { + EXPECT_THAT(Testee("BadTag_OverlongVarint") + .ParseBinary(message(), Wire(kTagWithContinuationBit, + "\x80\x80\x80\x80\x80\x80\x80", + kVarintTerminator, Varint(1234))) + .ParseOnly(), + Yields(IsParseError())); +} + +// An overlong tag varint (eleven empty continuation bytes) that is even more +// than 10 bytes long. +TEST_P(IllegalTagsTest, BadTagVarintMoreThanTenBytes) { + EXPECT_THAT( + Testee("BadTag_VarintMoreThanTenBytes") + .ParseBinary(message(), + Wire(kTagWithContinuationBit, + "\x80\x80\x80\x80\x80\x80\x80\x80\x80\x80\x80", + kVarintTerminator, Varint(1234))) + .ParseOnly(), + Yields(IsParseError())); +} + +INSTANTIATE_TEST_SUITE_P(All, IllegalTagsTest, ValuesIn(AllTestMessageTypes()), + MessageTypeParamName); + +} // namespace +} // namespace conformance +} // namespace protobuf +} // namespace google
diff --git a/conformance/binary_json_conformance_suite.cc b/conformance/binary_json_conformance_suite.cc deleted file mode 100644 index 6d3c285..0000000 --- a/conformance/binary_json_conformance_suite.cc +++ /dev/null
@@ -1,4509 +0,0 @@ -// Protocol Buffers - Google's data interchange format -// Copyright 2008 Google Inc. All rights reserved. -// -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file or at -// https://developers.google.com/open-source/licenses/bsd - -#include "conformance/binary_json_conformance_suite.h" - -#include <cassert> -#include <cctype> -#include <cstddef> -#include <cstdint> -#include <cstring> -#include <memory> -#include <string> -#include <type_traits> -#include <utility> -#include <vector> - -#include "absl/log/absl_check.h" -#include "absl/log/absl_log.h" -#include "absl/log/die_if_null.h" -#include "absl/status/status.h" -#include "absl/strings/str_cat.h" -#include "absl/strings/str_format.h" -#include "absl/strings/string_view.h" -#include "absl/strings/substitute.h" -#include "json/config.h" -#include "json/reader.h" -#include "json/value.h" -#include "conformance/binary_wireformat.h" -#include "conformance/conformance.pb.h" -#include "conformance/conformance_test.h" -#include "conformance/test_protos/test_messages_edition2023.pb.h" -#include "conformance/test_protos/test_messages_edition2026.pb.h" -#include "conformance/test_protos/test_messages_edition_unstable.pb.h" -#include "editions/golden/test_messages_proto2_editions.pb.h" -#include "editions/golden/test_messages_proto3_editions.pb.h" -#include "google/protobuf/json/json.h" -#include "google/protobuf/test_messages_proto2.pb.h" -#include "google/protobuf/test_messages_proto3.pb.h" -#include "google/protobuf/text_format.h" -#include "google/protobuf/unknown_field_set.h" -#include "google/protobuf/util/message_differencer.h" -#include "google/protobuf/util/type_resolver_util.h" -#include "google/protobuf/wire_format_lite.h" - -using ::conformance::ConformanceRequest; -using ::conformance::ConformanceResponse; -using ::conformance::TestStatus; -using ::conformance::WireFormat; -using google::protobuf::Descriptor; -using google::protobuf::FieldDescriptor; -using google::protobuf::internal::WireFormatLite; -using google::protobuf::util::NewTypeResolverForDescriptorPool; -using protobuf_test_messages::edition_unstable::TestAllTypesEditionUnstable; -using protobuf_test_messages::editions::TestAllTypesEdition2023; -using protobuf_test_messages::editions::TestAllTypesEdition2026; -using protobuf_test_messages::proto2::TestAllTypesProto2; -using protobuf_test_messages::proto3::TestAllTypesProto3; -using TestAllTypesProto2Editions = - protobuf_test_messages::editions::proto2::TestAllTypesProto2; -using TestAllTypesProto3Editions = - protobuf_test_messages::editions::proto3::TestAllTypesProto3; - -namespace { - -constexpr absl::string_view kTypeUrlPrefix = "type.googleapis.com"; - -// The number of repetitions to use for performance tests. -// Corresponds approx to 500KB wireformat bytes. -const size_t kPerformanceRepeatCount = 50000; - -std::string GetTypeUrl(const Descriptor* message) { - return absl::StrCat(kTypeUrlPrefix, "/", message->full_name()); -} - -std::string str_repeat(absl::string_view s, int count) { - std::string result; - for (int i = 0; i < count; ++i) { - absl::StrAppend(&result, s); - } - return result; -} - -/* Routines for building arbitrary protos *************************************/ - -// We would use CodedOutputStream except that we want more freedom to build -// arbitrary protos (even invalid ones). - -std::string varint(uint64_t x) { return google::protobuf::conformance::Varint(x).str(); } -std::string longvarint(uint64_t x, int extra) { - return google::protobuf::conformance::LongVarint(x, extra).str(); -} -std::string delim(const std::string& buf) { - return google::protobuf::conformance::LengthPrefixed(buf).str(); -} -std::string u32(uint32_t u32) { - return google::protobuf::conformance::Fixed32(u32).str(); -} -std::string u64(uint64_t u64) { - return google::protobuf::conformance::Fixed64(u64).str(); -} -std::string flt(float f) { return google::protobuf::conformance::Float(f).str(); } -std::string dbl(double d) { return google::protobuf::conformance::Double(d).str(); } -std::string zz32(int32_t x) { return google::protobuf::conformance::SInt32(x).str(); } -std::string zz64(int64_t x) { return google::protobuf::conformance::SInt64(x).str(); } - -std::string tag(uint32_t fieldnum, char wire_type) { - return google::protobuf::conformance::Tag( - fieldnum, static_cast<google::protobuf::conformance::WireType>(wire_type)) - .str(); -} -std::string tag(int fieldnum, char wire_type) { - return tag(static_cast<uint32_t>(fieldnum), wire_type); -} - -std::string field(uint32_t fieldnum, char wire_type, std::string content) { - return absl::StrCat(tag(fieldnum, wire_type), content); -} - -std::string group(uint32_t fieldnum, absl::string_view content) { - return google::protobuf::conformance::DelimitedField(fieldnum, - google::protobuf::conformance::Wire(content)) - .str(); -} - -std::string len(uint32_t fieldnum, std::string content) { - return google::protobuf::conformance::LengthPrefixedField(fieldnum, std::move(content)) - .str(); -} - -std::string GetDefaultValue(FieldDescriptor::Type type) { - switch (type) { - case FieldDescriptor::TYPE_INT32: - case FieldDescriptor::TYPE_INT64: - case FieldDescriptor::TYPE_UINT32: - case FieldDescriptor::TYPE_UINT64: - case FieldDescriptor::TYPE_ENUM: - case FieldDescriptor::TYPE_BOOL: - return varint(0); - case FieldDescriptor::TYPE_SINT32: - return zz32(0); - case FieldDescriptor::TYPE_SINT64: - return zz64(0); - case FieldDescriptor::TYPE_FIXED32: - case FieldDescriptor::TYPE_SFIXED32: - return u32(0); - case FieldDescriptor::TYPE_FIXED64: - case FieldDescriptor::TYPE_SFIXED64: - return u64(0); - case FieldDescriptor::TYPE_FLOAT: - return flt(0); - case FieldDescriptor::TYPE_DOUBLE: - return dbl(0); - case FieldDescriptor::TYPE_STRING: - case FieldDescriptor::TYPE_BYTES: - case FieldDescriptor::TYPE_MESSAGE: - return delim(""); - default: - return ""; - } - return ""; -} - -std::string GetNonDefaultValue(FieldDescriptor::Type type) { - switch (type) { - case FieldDescriptor::TYPE_INT32: - case FieldDescriptor::TYPE_INT64: - case FieldDescriptor::TYPE_UINT32: - case FieldDescriptor::TYPE_UINT64: - case FieldDescriptor::TYPE_ENUM: - case FieldDescriptor::TYPE_BOOL: - return varint(1); - case FieldDescriptor::TYPE_SINT32: - return zz32(1); - case FieldDescriptor::TYPE_SINT64: - return zz64(1); - case FieldDescriptor::TYPE_FIXED32: - case FieldDescriptor::TYPE_SFIXED32: - return u32(1); - case FieldDescriptor::TYPE_FIXED64: - case FieldDescriptor::TYPE_SFIXED64: - return u64(1); - case FieldDescriptor::TYPE_FLOAT: - return flt(1); - case FieldDescriptor::TYPE_DOUBLE: - return dbl(1); - case FieldDescriptor::TYPE_STRING: - case FieldDescriptor::TYPE_BYTES: - return delim("a"); - case FieldDescriptor::TYPE_MESSAGE: - return delim( - absl::StrCat(tag(1, WireFormatLite::WIRETYPE_VARINT), varint(1234))); - default: - return ""; - } - return ""; -} - -#define UNKNOWN_FIELD 666 - -std::string UpperCase(std::string str) { - for (size_t i = 0; i < str.size(); i++) { - str[i] = toupper(str[i]); - } - return str; -} - -std::string MismatchedWireTypeField( - int field_number, WireFormatLite::WireType correct_wire_type) { - if (correct_wire_type == WireFormatLite::WIRETYPE_LENGTH_DELIMITED) { - return absl::StrCat(tag(field_number, WireFormatLite::WIRETYPE_VARINT), - varint(1)); - } - return absl::StrCat( - tag(field_number, WireFormatLite::WIRETYPE_LENGTH_DELIMITED), delim("a")); -} - -bool IsProto3Default(FieldDescriptor::Type type, - const std::string& binary_data) { - switch (type) { - case FieldDescriptor::TYPE_DOUBLE: - return binary_data == dbl(0); - case FieldDescriptor::TYPE_FLOAT: - return binary_data == flt(0); - case FieldDescriptor::TYPE_BOOL: - case FieldDescriptor::TYPE_INT64: - case FieldDescriptor::TYPE_UINT64: - case FieldDescriptor::TYPE_INT32: - case FieldDescriptor::TYPE_UINT32: - case FieldDescriptor::TYPE_SINT32: - case FieldDescriptor::TYPE_SINT64: - case FieldDescriptor::TYPE_ENUM: - return binary_data == varint(0); - case FieldDescriptor::TYPE_FIXED64: - case FieldDescriptor::TYPE_SFIXED64: - return binary_data == u64(0); - case FieldDescriptor::TYPE_FIXED32: - case FieldDescriptor::TYPE_SFIXED32: - return binary_data == u32(0); - case FieldDescriptor::TYPE_STRING: - case FieldDescriptor::TYPE_BYTES: - return binary_data == delim(""); - default: - return false; - } -} - -} // anonymous namespace - -namespace google { -namespace protobuf { -namespace conformance { - -bool BinaryAndJsonConformanceSuite::ParseJsonResponse( - const ConformanceResponse& response, Message* test_message) { - json::ParseOptions options; - options.allow_legacy_nonconformant_behavior = false; - absl::Status status = - json::JsonStringToMessage(response.json_payload(), test_message); - if (!status.ok()) { - ABSL_LOG(ERROR) << status; - return false; - } - return true; -} - -bool BinaryAndJsonConformanceSuite::ParseResponse( - const ConformanceResponse& response, - const ConformanceRequestSetting& setting, Message* test_message) { - const ConformanceRequest& request = setting.GetRequest(); - WireFormat requested_output = request.requested_output_format(); - const std::string& test_name = setting.GetTestName(); - ConformanceLevel level = setting.GetLevel(); - - TestStatus test; - test.set_name(test_name); - switch (response.result_case()) { - case ConformanceResponse::kProtobufPayload: { - if (requested_output != ::conformance::PROTOBUF) { - test.set_failure_message(absl::StrCat( - "Test was asked for ", WireFormatToString(requested_output), - " output but provided PROTOBUF instead.")); - ReportFailure(test, level, request, response); - return false; - } - - if (!test_message->ParseFromString(response.protobuf_payload())) { - test.set_failure_message( - "Protobuf output we received from test was unparseable."); - ReportFailure(test, level, request, response); - return false; - } - - break; - } - - case ConformanceResponse::kJsonPayload: { - if (requested_output != ::conformance::JSON) { - test.set_failure_message(absl::StrCat( - "Test was asked for ", WireFormatToString(requested_output), - " output but provided JSON instead.")); - ReportFailure(test, level, request, response); - return false; - } - - if (!ParseJsonResponse(response, test_message)) { - test.set_failure_message( - "JSON output we received from test was unparseable."); - ReportFailure(test, level, request, response); - return false; - } - - break; - } - - default: - ABSL_LOG(FATAL) << test_name - << ": unknown payload type: " << response.result_case() - << ", response: " << response; - } - - return true; -} - -void BinaryAndJsonConformanceSuite::RunSuiteImpl() { - type_resolver_.reset(NewTypeResolverForDescriptorPool( - kTypeUrlPrefix, DescriptorPool::generated_pool())); - - BinaryAndJsonConformanceSuiteImpl<TestAllTypesProto3>( - this, /*run_proto3_tests=*/true); - BinaryAndJsonConformanceSuiteImpl<TestAllTypesProto2>( - this, /*run_proto3_tests=*/false); - if (!this->performance_) { - RunMessageSetTests(); - } else { - RunRecursionLimitTests(); - } - if (maximum_edition_ >= Edition::EDITION_2023) { - BinaryAndJsonConformanceSuiteImpl<TestAllTypesProto3Editions>( - this, /*run_proto3_tests=*/true); - BinaryAndJsonConformanceSuiteImpl<TestAllTypesProto2Editions>( - this, /*run_proto3_tests=*/false); - if (!this->performance_) { - RunDelimitedFieldTests(); - RunUnstableTests(); - if (maximum_edition_ >= Edition::EDITION_2026) { - RunEdition2026Tests(); - } - RunUtf8ValidationTests(); - } - } -} - -void BinaryAndJsonConformanceSuite::RunDelimitedFieldTests() { - SetTypeUrl(GetTypeUrl(TestAllTypesEdition2023::GetDescriptor())); - - RunValidProtobufTest<TestAllTypesEdition2023>( - absl::StrCat("ValidNonMessage"), REQUIRED, - field(1, WireFormatLite::WIRETYPE_VARINT, varint(99)), - R"pb(optional_int32: 99)pb"); - - RunValidProtobufTest<TestAllTypesEdition2023>( - absl::StrCat("ValidLengthPrefixedField"), REQUIRED, - len(18, field(1, WireFormatLite::WIRETYPE_VARINT, varint(99))), - R"pb(optional_nested_message { a: 99 })pb"); - - RunValidProtobufTest<TestAllTypesEdition2023>( - absl::StrCat("ValidMap.Integer"), REQUIRED, - len(56, - absl::StrCat(field(1, WireFormatLite::WIRETYPE_VARINT, varint(99)), - field(2, WireFormatLite::WIRETYPE_VARINT, varint(87)))), - R"pb(map_int32_int32 { key: 99 value: 87 })pb"); - - RunValidProtobufTest<TestAllTypesEdition2023>( - absl::StrCat("ValidMap.LengthPrefixed"), REQUIRED, - len(71, absl::StrCat(len(1, "a"), - len(2, field(1, WireFormatLite::WIRETYPE_VARINT, - varint(87))))), - R"pb(map_string_nested_message { - key: "a" - value: { a: 87 } - })pb"); - - RunValidProtobufTest<TestAllTypesEdition2023>( - absl::StrCat("ValidDelimitedField.GroupLike"), REQUIRED, - group(201, field(202, WireFormatLite::WIRETYPE_VARINT, varint(99))), - R"pb(groupliketype { group_int32: 99 })pb"); - - RunValidProtobufTest<TestAllTypesEdition2023>( - absl::StrCat("ValidDelimitedField.NotGroupLike"), REQUIRED, - group(202, field(202, WireFormatLite::WIRETYPE_VARINT, varint(99))), - R"pb(delimited_field { group_int32: 99 })pb"); - - // Note: extensions don't work with TypeResolver, which is used by - // binary->JSON tests. - RunValidBinaryProtobufTest<TestAllTypesEdition2023>( - absl::StrCat("ValidDelimitedExtension.GroupLike"), REQUIRED, - group(121, field(1, WireFormatLite::WIRETYPE_VARINT, varint(99))), - R"pb([protobuf_test_messages.editions.groupliketype] { c: 99 })pb"); - - RunValidBinaryProtobufTest<TestAllTypesEdition2023>( - absl::StrCat("ValidDelimitedExtension.NotGroupLike"), REQUIRED, - group(122, field(1, WireFormatLite::WIRETYPE_VARINT, varint(99))), - R"pb([protobuf_test_messages.editions.delimited_ext] { c: 99 })pb"); -} - -void BinaryAndJsonConformanceSuite::RunUnstableTests() { - SetTypeUrl(GetTypeUrl(TestAllTypesEditionUnstable::GetDescriptor())); - - RunValidProtobufTest<TestAllTypesEditionUnstable>( - absl::StrCat("ValidBytes"), REQUIRED, len(13, "foo"), - R"pb(optional_bytes: "foo")pb"); - - RunValidProtobufTest<TestAllTypesEditionUnstable>( - absl::StrCat("ValidMap.Bytes"), REQUIRED, - len(15, absl::StrCat(len(1, "foo"), len(2, "barbaz"))), - R"pb(map_string_bytes { key: "foo" value: "barbaz" })pb"); -} - -void BinaryAndJsonConformanceSuite::RunEdition2026Tests() { - SetTypeUrl(GetTypeUrl(TestAllTypesEdition2026::GetDescriptor())); - - RunValidProtobufTest<TestAllTypesEdition2026>( - absl::StrCat("ValidInt32"), REQUIRED, - field(1, WireFormatLite::WIRETYPE_VARINT, varint(99)), - R"pb(optional_int32: 99)pb"); - - // The (pb.enumvalue.json) option overrides the name used for an enum value - // in JSON. - RunValidJsonTest<TestAllTypesEdition2026>( - "EnumValueCustomJsonName", REQUIRED, - R"({"optionalForeignEnum": "customBar"})", - "optional_foreign_enum: FOREIGN_ENUM_EDITION2026_BAR"); - // Using the original enum value name in JSON is also allowed. - RunValidJsonTest<TestAllTypesEdition2026>( - "EnumValueCustomJsonNameOriginalName", REQUIRED, - R"({"optionalForeignEnum": "FOREIGN_ENUM_EDITION2026_BAR"})", - "optional_foreign_enum: FOREIGN_ENUM_EDITION2026_BAR"); - RunValidJsonTest<TestAllTypesEdition2026>( - "EnumValueCustomJsonNameNumericValue", REQUIRED, - R"({"optionalForeignEnum": 1})", - "optional_foreign_enum: FOREIGN_ENUM_EDITION2026_BAR"); - RunValidJsonTest<TestAllTypesEdition2026>( - "RepeatedEnumValueCustomJsonName", REQUIRED, - R"({"repeatedForeignEnum": ["customBar", "FOREIGN_ENUM_EDITION2026_BAZ", "customBaz"]})", - R"( - repeated_foreign_enum: FOREIGN_ENUM_EDITION2026_BAR - repeated_foreign_enum: FOREIGN_ENUM_EDITION2026_BAZ - repeated_foreign_enum: FOREIGN_ENUM_EDITION2026_BAZ - )"); - RunValidJsonTest<TestAllTypesEdition2026>( - "MapEnumValueCustomJsonName", REQUIRED, - R"({"mapStringForeignEnum": {"a": "customBar", "b": "FOREIGN_ENUM_EDITION2026_BAZ"}})", - R"( - map_string_foreign_enum { key: "a" value: FOREIGN_ENUM_EDITION2026_BAR } - map_string_foreign_enum { key: "b" value: FOREIGN_ENUM_EDITION2026_BAZ } - )"); - // Unknown names that are neither a declared name nor a custom JSON name. - ExpectParseFailureForJson<TestAllTypesEdition2026>( - "EnumValueUnknownCustomJsonName", REQUIRED, - R"({"optionalForeignEnum": "customQux"})"); - ExpectParseFailureForJson<TestAllTypesEdition2026>( - "EnumValueUnknownOriginalName", REQUIRED, - R"({"optionalForeignEnum": "FOREIGN_ENUM_EDITION2026_QUX"})"); - // Empty custom JSON name. - RunValidJsonTest<TestAllTypesEdition2026>( - "EnumValueEmptyCustomJsonName", REQUIRED, - R"({"optionalForeignEnum": ""})", - "optional_foreign_enum: FOREIGN_ENUM_EDITION2026_EMPTY"); - RunValidJsonTestWithValidator<TestAllTypesEdition2026>( - "EnumValueEmptyCustomJsonNameOutput", REQUIRED, - R"({"optionalForeignEnum": "FOREIGN_ENUM_EDITION2026_EMPTY"})", - [](const Json::Value& value) { - return value["optionalForeignEnum"].isString() && - value["optionalForeignEnum"].asString().empty(); - }); - // Custom JSON name with characters that require escaping in JSON. - RunValidJsonTest<TestAllTypesEdition2026>( - "EnumValueEscapedCustomJsonName", REQUIRED, - R"({"optionalForeignEnum": "e\"sc\tap\ne"})", - "optional_foreign_enum: FOREIGN_ENUM_EDITION2026_ESCAPES"); - RunValidJsonTestWithValidator<TestAllTypesEdition2026>( - "EnumValueEscapedCustomJsonNameOutput", REQUIRED, - R"({"optionalForeignEnum": "FOREIGN_ENUM_EDITION2026_ESCAPES"})", - [](const Json::Value& value) { - return value["optionalForeignEnum"].asString() == "e\"sc\tap\ne"; - }); - // Aliased values share a custom JSON name, and their declared names both - // still parse. - RunValidJsonTest<TestAllTypesEdition2026>( - "EnumValueAliasCustomJsonName", REQUIRED, - R"({"optionalForeignEnum": "customAlias"})", - "optional_foreign_enum: FOREIGN_ENUM_EDITION2026_ALIAS"); - RunValidJsonTest<TestAllTypesEdition2026>( - "EnumValueAliasCustomJsonNameOriginalName", REQUIRED, - R"({"optionalForeignEnum": "FOREIGN_ENUM_EDITION2026_ALIAS_TOO"})", - "optional_foreign_enum: FOREIGN_ENUM_EDITION2026_ALIAS"); - RunValidJsonTestWithValidator<TestAllTypesEdition2026>( - "EnumValueAliasCustomJsonNameOutput", REQUIRED, - R"({"optionalForeignEnum": "FOREIGN_ENUM_EDITION2026_ALIAS"})", - [](const Json::Value& value) { - return value["optionalForeignEnum"].asString() == "customAlias"; - }); - // A numeric-looking custom JSON name serializes as a JSON string, and both - // the string and the integer forms parse. - RunValidJsonTest<TestAllTypesEdition2026>( - "EnumValueNumericCustomJsonName", REQUIRED, - R"({"optionalForeignEnum": "6"})", - "optional_foreign_enum: FOREIGN_ENUM_EDITION2026_NUMERIC"); - RunValidJsonTest<TestAllTypesEdition2026>( - "EnumValueNumericCustomJsonNameIntegerValue", REQUIRED, - R"({"optionalForeignEnum": 6})", - "optional_foreign_enum: FOREIGN_ENUM_EDITION2026_NUMERIC"); - RunValidJsonTestWithValidator<TestAllTypesEdition2026>( - "EnumValueNumericCustomJsonNameOutput", REQUIRED, - R"({"optionalForeignEnum": "FOREIGN_ENUM_EDITION2026_NUMERIC"})", - [](const Json::Value& value) { - return value["optionalForeignEnum"].isString() && - value["optionalForeignEnum"].asString() == "6"; - }); - RunValidJsonTestWithValidator<TestAllTypesEdition2026>( - "EnumValueCustomJsonNameOutput", REQUIRED, - R"({"optionalForeignEnum": "FOREIGN_ENUM_EDITION2026_BAR"})", - [](const Json::Value& value) { - return value["optionalForeignEnum"].asString() == "customBar"; - }); - // A value without the option keeps using its declared name in JSON. - RunValidJsonTestWithValidator<TestAllTypesEdition2026>( - "EnumValueWithoutCustomJsonNameOutput", REQUIRED, - R"({"repeatedForeignEnum": ["FOREIGN_ENUM_EDITION2026_FOO"]})", - [](const Json::Value& value) { - return value["repeatedForeignEnum"][0].asString() == - "FOREIGN_ENUM_EDITION2026_FOO"; - }); -} - -void BinaryAndJsonConformanceSuite::RunUtf8ValidationTests() { - ExpectParseFailureForProto<TestAllTypesEdition2023>( - len(133, "\xA0\xB0\xC0\xD0"), "RejectInvalidUtf8.String.Extension", - RECOMMENDED); - RunValidBinaryProtobufTest< - TestAllTypesEdition2023>(absl::StrCat( - "AcceptInvalidUtf8.Bytes.Extension"), - REQUIRED, len(134, "\xA0\xB0\xC0\xD0"), - R"pb([protobuf_test_messages.editions - .extension_bytes]: "\xA0\xB0\xC0\xD0")pb"); -} - -void BinaryAndJsonConformanceSuite::RunMessageSetTests() { - RunValidBinaryProtobufTest<TestAllTypesProto2>( - absl::StrCat("ValidMessageSetEncoding"), REQUIRED, - len(500, - group(1, absl::StrCat(field(2, WireFormatLite::WIRETYPE_VARINT, - varint(4135312)), - len(3, field(9, WireFormatLite::WIRETYPE_VARINT, - varint(99)))))), - // clang-format off - R"pb(message_set_correct: { - [protobuf_test_messages.proto2 - .TestAllTypesProto2.MessageSetCorrectExtension2]: { i: 99 } - })pb" - // clang-format on - ); - RunValidBinaryProtobufTest<TestAllTypesProto2>( - absl::StrCat("ValidMessageSetEncoding.OutOfOrderGroupsEntries"), REQUIRED, - len(500, - group(1, absl::StrCat(len(3, field(9, WireFormatLite::WIRETYPE_VARINT, - varint(99))), - field(2, WireFormatLite::WIRETYPE_VARINT, - varint(4135312))))), - // clang-format off - R"pb(message_set_correct: { - [protobuf_test_messages.proto2 - .TestAllTypesProto2.MessageSetCorrectExtension2]: { i: 99 } - })pb" - // clang-format on - ); - - // Test that an unknown message set extension always goes to unknown fields. - // This is done by poisoning the extension payload with an entry for field 0. - RunValidRoundtripProtobufTest<TestAllTypesProto2>( - "MessageSetEncoding.UnknownExtension", REQUIRED, - len(500, - group(1, absl::StrCat(field(2, WireFormatLite::WIRETYPE_VARINT, - varint(4135300)), - len(3, field(0, WireFormatLite::WIRETYPE_VARINT, - varint(99))))))); - - // If an encoder is unaware of the message_set_wire_format option it will be - // encoded like any other extension submessage. Decoders should be able to - // tolerate this format as well. - RunValidBinaryProtobufTest<TestAllTypesProto2>( - absl::StrCat("ValidMessageSetEncoding.SubmessageEncoding"), RECOMMENDED, - len(500, - len(4135312, field(9, WireFormatLite::WIRETYPE_VARINT, varint(99)))), - // clang-format off - R"pb(message_set_correct: { - [protobuf_test_messages.proto2 - .TestAllTypesProto2.MessageSetCorrectExtension2]: { i: 99 } - })pb" - // clang-format on - ); - - // Test again, but this time we'll try to detect if the implementation put the - // submessage encoded entry into the unknown field set. We'll do this by using - // conflicting oneof entries where order matters when the messages are merged. - // - // In a non-compliant implementation submessage encoded messageset entry will - // be moved to unknown fields and then tacked onto the end of the payload. - // Thus we'll see field b set first, and then field a. - // - // In a compliant implementation we expect the submessage encoded messageset - // to be read first with field a set, and then the normal message set entry - // will be read with field b will be set -- thus field b will win. - RunValidBinaryProtobufTest<TestAllTypesProto2>( - absl::StrCat("ValidMessageSetEncoding.SubmessageEncoding.NotUnknown"), - RECOMMENDED, - len(500, absl::StrCat( - len(123456789, - field(1, WireFormatLite::WIRETYPE_VARINT, varint(42))), - group(1, absl::StrCat( - field(2, WireFormatLite::WIRETYPE_VARINT, - varint(123456789)), - len(3, field(2, WireFormatLite::WIRETYPE_VARINT, - varint(99))))))), - // clang-format off - R"pb(message_set_correct: { - [protobuf_test_messages.proto2 - .TestAllTypesProto2.ExtensionWithOneof]: { b: 99 } - })pb" - // clang-format on - ); - - // [type_id, value, type_id (different)] -> first type_id and value honored. - RunValidBinaryProtobufTest<TestAllTypesProto2>( - absl::StrCat("ValidMessageSetEncoding.DuplicateDifferentTypeId"), - RECOMMENDED, - len(500, - group( - 1, - absl::StrCat( - field(2, WireFormatLite::WIRETYPE_VARINT, varint(4135312)), - len(3, field(9, WireFormatLite::WIRETYPE_VARINT, varint(99))), - field(2, WireFormatLite::WIRETYPE_VARINT, varint(1547769))))), - // clang-format off - R"pb(message_set_correct: { - [protobuf_test_messages.proto2 - .TestAllTypesProto2.MessageSetCorrectExtension2]: { i: 99 } - })pb" - // clang-format on - ); - - // [type_id, value, value] -> first value honored, no merge. - RunValidBinaryProtobufTest<TestAllTypesProto2>( - absl::StrCat("ValidMessageSetEncoding.DuplicateValue"), RECOMMENDED, - len(500, - group( - 1, - absl::StrCat( - field(2, WireFormatLite::WIRETYPE_VARINT, varint(4135312)), - len(3, field(9, WireFormatLite::WIRETYPE_VARINT, varint(99))), - len(3, - field(9, WireFormatLite::WIRETYPE_VARINT, varint(88)))))), - // clang-format off - R"pb(message_set_correct: { - [protobuf_test_messages.proto2 - .TestAllTypesProto2.MessageSetCorrectExtension2]: { i: 99 } - })pb" - // clang-format on - ); - - // [value, type_id, value] -> first value honored, no merge. - RunValidBinaryProtobufTest<TestAllTypesProto2>( - absl::StrCat("ValidMessageSetEncoding.DuplicateValueOutOfOrder"), - RECOMMENDED, - len(500, - group( - 1, - absl::StrCat( - len(3, field(9, WireFormatLite::WIRETYPE_VARINT, varint(99))), - field(2, WireFormatLite::WIRETYPE_VARINT, varint(4135312)), - len(3, - field(9, WireFormatLite::WIRETYPE_VARINT, varint(88)))))), - // clang-format off - R"pb(message_set_correct: { - [protobuf_test_messages.proto2 - .TestAllTypesProto2.MessageSetCorrectExtension2]: { i: 99 } - })pb" - // clang-format on - ); -} - -void BinaryAndJsonConformanceSuite::RunRecursionLimitTests() { - { - TestAllTypesEdition2023 message; - TestAllTypesEdition2023* sub = &message; - // The default recursion limit is 100 for most languages. 10,000 for - // golang. We use a larger number here for test. - for (int i = 0; i < 20000; i++) { - sub = &(*sub->mutable_map_recursive())[0]; - sub->set_optional_int32(123); - } - ExpectParseFailureForProto<TestAllTypesEdition2023>( - message.SerializeAsString(), "EnforceDepthLimit.Map", RECOMMENDED); - } - - { - TestAllTypesEdition2023 message; - TestAllTypesEdition2023* sub = &message; - for (int i = 0; i < 10000; i++) { - sub = - (*sub->mutable_map_string_nested_message())[""].mutable_corecursive(); - sub->set_optional_int32(123); - } - ExpectParseFailureForProto<TestAllTypesEdition2023>( - message.SerializeAsString(), "EnforceDepthLimit.MapStringKey", - RECOMMENDED); - } - - { - TestAllTypesProto2 proto2_msg; - auto sub = proto2_msg.mutable_message_set_correct(); - // The default recursion limit is 100 for most languages. 10,000 for - // golang. We use a larger number here for test. - for (int i = 0; i < 20000; i++) { - sub = sub->MutableExtension( - TestAllTypesProto2::MessageSetCorrectExtension2:: - message_set_extension) - ->mutable_sub_msg(); - } - sub->MutableExtension(TestAllTypesProto2::MessageSetCorrectExtension2:: - message_set_extension) - ->set_i(123); - ExpectParseFailureForProto<TestAllTypesProto2>( - proto2_msg.SerializeAsString(), "EnforceDepthLimit.MessageSetExtension", - RECOMMENDED); - } -} - -template <typename MessageType> -void BinaryAndJsonConformanceSuite::ExpectParseFailureForProto( - const std::string& proto, const std::string& test_name, - ConformanceLevel level) { - MessageType prototype; - // We don't expect output, but if the program erroneously accepts the protobuf - // we let it send its response as this. We must not leave it unspecified. - ConformanceRequestSetting setting( - level, ::conformance::PROTOBUF, ::conformance::PROTOBUF, - ::conformance::BINARY_TEST, prototype, test_name, proto); - - const ConformanceRequest& request = setting.GetRequest(); - ConformanceResponse response; - std::string effective_test_name = - absl::StrCat(setting.ConformanceLevelToString(level), ".", - setting.GetSyntaxIdentifier(), ".ProtobufInput.", test_name); - - if (!RunTest(effective_test_name, request, &response)) { - return; - } - - TestStatus test; - test.set_name(effective_test_name); - if (response.result_case() == ConformanceResponse::kParseError) { - ReportSuccess(test); - } else if (response.result_case() == ConformanceResponse::kSkipped) { - ReportSkip(test, request, response); - } else if (response.result_case() == ConformanceResponse::kRuntimeError) { - test.set_failure_message( - "Should have failed to parse, but raised an error instead."); - ReportFailure(test, level, request, response); - } else { - test.set_failure_message("Should have failed to parse, but didn't."); - ReportFailure(test, level, request, response); - } -} - -template <typename MessageType> -void BinaryAndJsonConformanceSuite::RunValidJsonTest( - const std::string& test_name, ConformanceLevel level, - const std::string& input_json, const std::string& equivalent_text_format) { - MessageType prototype; - RunValidJsonTestWithMessage(test_name, level, input_json, - equivalent_text_format, prototype); -} - -void BinaryAndJsonConformanceSuite::RunValidJsonTestWithMessage( - const std::string& test_name, ConformanceLevel level, - const std::string& input_json, const std::string& equivalent_text_format, - const Message& prototype) { - ConformanceRequestSetting setting1( - level, ::conformance::JSON, ::conformance::PROTOBUF, - ::conformance::JSON_TEST, prototype, test_name, input_json); - RunValidInputTest(setting1, equivalent_text_format); - ConformanceRequestSetting setting2( - level, ::conformance::JSON, ::conformance::JSON, ::conformance::JSON_TEST, - prototype, test_name, input_json); - RunValidInputTest(setting2, equivalent_text_format); -} - -template <typename MessageType> -void BinaryAndJsonConformanceSuite::ExpectParseFailureForJson( - const std::string& test_name, ConformanceLevel level, - const std::string& input_json) { - MessageType prototype; - // We don't expect output, but if the program erroneously accepts the JSON - // we let it send its response as this. We must not leave it unspecified. - ConformanceRequestSetting setting( - level, ::conformance::JSON, ::conformance::JSON, ::conformance::JSON_TEST, - prototype, test_name, input_json); - const ConformanceRequest& request = setting.GetRequest(); - ConformanceResponse response; - std::string effective_test_name = - absl::StrCat(setting.ConformanceLevelToString(level), ".", - setting.GetSyntaxIdentifier(), ".JsonInput.", test_name); - - if (!RunTest(effective_test_name, request, &response)) { - return; - } - - TestStatus test; - test.set_name(effective_test_name); - if (response.result_case() == ConformanceResponse::kParseError) { - ReportSuccess(test); - } else if (response.result_case() == ConformanceResponse::kSkipped) { - ReportSkip(test, request, response); - } else if (response.result_case() == ConformanceResponse::kRuntimeError) { - test.set_failure_message( - "Should have failed to parse, but raised an error instead."); - ReportFailure(test, level, request, response); - } else { - test.set_failure_message("Should have failed to parse, but didn't."); - ReportFailure(test, level, request, response); - } -} - -template <typename MessageType> -void BinaryAndJsonConformanceSuite::RunValidJsonTestWithValidator( - const std::string& test_name, ConformanceLevel level, - const std::string& input_json, const Validator& validator) { - MessageType prototype; - ConformanceRequestSetting setting( - level, ::conformance::JSON, ::conformance::JSON, ::conformance::JSON_TEST, - prototype, test_name, input_json); - const ConformanceRequest& request = setting.GetRequest(); - ConformanceResponse response; - const std::string& effective_test_name = setting.GetTestName(); - - if (!RunTest(effective_test_name, request, &response)) { - return; - } - - TestStatus test; - test.set_name(effective_test_name); - if (response.result_case() == ConformanceResponse::kSkipped) { - ReportSkip(test, request, response); - return; - } - - if (response.result_case() != ConformanceResponse::kJsonPayload) { - test.set_failure_message(absl::StrCat("Expected JSON payload but got type ", - response.result_case())); - ReportFailure(test, level, request, response); - return; - } - Json::CharReaderBuilder builder; - Json::Value value; - Json::String err; - const std::unique_ptr<Json::CharReader> reader(builder.newCharReader()); - if (!reader->parse( - response.json_payload().c_str(), - response.json_payload().c_str() + response.json_payload().length(), - &value, &err)) { - test.set_failure_message( - absl::StrCat("JSON payload cannot be parsed as valid JSON: ", err)); - ReportFailure(test, level, request, response); - return; - } - if (!validator(value)) { - test.set_failure_message("JSON payload validation failed."); - ReportFailure(test, level, request, response); - return; - } - ReportSuccess(test); -} - -template <typename MessageType> -void BinaryAndJsonConformanceSuiteImpl<MessageType>:: - ExpectParseFailureForProtoWithProtoVersion(const std::string& proto, - const std::string& test_name, - ConformanceLevel level) { - suite_.ExpectParseFailureForProto<MessageType>(proto, test_name, level); -} - -// Expect that this precise protobuf will cause a parse error. -template <typename MessageType> -void BinaryAndJsonConformanceSuiteImpl<MessageType>::ExpectParseFailureForProto( - const std::string& proto, const std::string& test_name, - ConformanceLevel level) { - ExpectParseFailureForProtoWithProtoVersion(proto, test_name, level); -} - -// Expect that this protobuf will cause a parse error, even if it is followed -// by valid protobuf data. We can try running this twice: once with this -// data verbatim and once with this data followed by some valid data. -// -// TODO: implement the second of these. -template <typename MessageType> -void BinaryAndJsonConformanceSuiteImpl< - MessageType>::ExpectHardParseFailureForProto(const std::string& proto, - const std::string& test_name, - ConformanceLevel level) { - return ExpectParseFailureForProto(proto, test_name, level); -} - -template <typename MessageType> -void BinaryAndJsonConformanceSuiteImpl<MessageType>::RunValidJsonTest( - const std::string& test_name, ConformanceLevel level, - const std::string& input_json, const std::string& equivalent_text_format) { - MessageType prototype; - RunValidJsonTestWithMessage(test_name, level, input_json, - equivalent_text_format, prototype); -} - -template <typename MessageType> -void BinaryAndJsonConformanceSuiteImpl<MessageType>:: - RunValidJsonTestWithMessage(const std::string& test_name, - ConformanceLevel level, - const std::string& input_json, - const std::string& equivalent_text_format, - const Message& prototype) { - suite_.RunValidJsonTestWithMessage(test_name, level, input_json, - equivalent_text_format, prototype); -} - -template <typename MessageType> -void BinaryAndJsonConformanceSuiteImpl<MessageType>:: - RunValidJsonTestWithProtobufInput( - const std::string& test_name, ConformanceLevel level, - const MessageType& input, const std::string& equivalent_text_format) { - ConformanceRequestSetting setting( - level, ::conformance::PROTOBUF, ::conformance::JSON, - ::conformance::JSON_TEST, input, test_name, input.SerializeAsString()); - suite_.RunValidInputTest(setting, equivalent_text_format); -} - -template <typename MessageType> -void BinaryAndJsonConformanceSuiteImpl<MessageType>:: - RunValidJsonIgnoreUnknownTest(const std::string& test_name, - ConformanceLevel level, - const std::string& input_json, - const std::string& equivalent_text_format) { - MessageType prototype; - ConformanceRequestSetting setting( - level, ::conformance::JSON, ::conformance::PROTOBUF, - ::conformance::JSON_IGNORE_UNKNOWN_PARSING_TEST, prototype, test_name, - input_json); - suite_.RunValidInputTest(setting, equivalent_text_format); -} - -template <typename MessageType> -void BinaryAndJsonConformanceSuite::RunValidBinaryProtobufTest( - const std::string& test_name, ConformanceLevel level, - const std::string& input_protobuf, - const std::string& equivalent_text_format) { - MessageType prototype; - - ConformanceRequestSetting binary_to_binary( - level, ::conformance::PROTOBUF, ::conformance::PROTOBUF, - ::conformance::BINARY_TEST, prototype, test_name, input_protobuf); - RunValidInputTest(binary_to_binary, equivalent_text_format); -} - -template <typename MessageType> -void BinaryAndJsonConformanceSuite::RunValidRoundtripProtobufTest( - const std::string& test_name, ConformanceLevel level, - const std::string& input_protobuf) { - MessageType prototype; - - ConformanceRequestSetting binary_to_binary( - level, ::conformance::PROTOBUF, ::conformance::PROTOBUF, - ::conformance::BINARY_TEST, prototype, test_name, input_protobuf); - RunValidBinaryInputTest(binary_to_binary, input_protobuf); -} - -template <typename MessageType> -void BinaryAndJsonConformanceSuite::RunValidProtobufTest( - const std::string& test_name, ConformanceLevel level, - const std::string& input_protobuf, - const std::string& equivalent_text_format) { - MessageType prototype; - - ConformanceRequestSetting binary_to_binary( - level, ::conformance::PROTOBUF, ::conformance::PROTOBUF, - ::conformance::BINARY_TEST, prototype, test_name, input_protobuf); - RunValidInputTest(binary_to_binary, equivalent_text_format); - - ConformanceRequestSetting binary_to_json( - level, ::conformance::PROTOBUF, ::conformance::JSON, - ::conformance::BINARY_TEST, prototype, test_name, input_protobuf); - RunValidInputTest(binary_to_json, equivalent_text_format); -} - -template <typename MessageType> -void BinaryAndJsonConformanceSuiteImpl<MessageType>::RunValidProtobufTest( - const std::string& test_name, ConformanceLevel level, - const std::string& input_protobuf, - const std::string& equivalent_text_format) { - suite_.RunValidProtobufTest<MessageType>(test_name, level, input_protobuf, - equivalent_text_format); -} - -template <typename MessageType> -void BinaryAndJsonConformanceSuiteImpl<MessageType>::RunValidBinaryProtobufTest( - const std::string& test_name, ConformanceLevel level, - const std::string& input_protobuf) { - RunValidBinaryProtobufTest(test_name, level, input_protobuf, input_protobuf); -} - -template <typename MessageType> -void BinaryAndJsonConformanceSuiteImpl<MessageType>::RunValidBinaryProtobufTest( - const std::string& test_name, ConformanceLevel level, - const std::string& input_protobuf, const std::string& expected_protobuf) { - MessageType prototype; - ConformanceRequestSetting setting( - level, ::conformance::PROTOBUF, ::conformance::PROTOBUF, - ::conformance::BINARY_TEST, prototype, test_name, input_protobuf); - suite_.RunValidBinaryInputTest(setting, expected_protobuf, true); -} - -template <typename MessageType> -void BinaryAndJsonConformanceSuiteImpl<MessageType>:: - RunBinaryPerformanceMergeMessageWithField(const std::string& test_name, - const std::string& field_proto) { - std::string message_tag = tag(27, WireFormatLite::WIRETYPE_LENGTH_DELIMITED); - std::string message_proto = absl::StrCat(message_tag, delim(field_proto)); - - std::string proto; - for (size_t i = 0; i < kPerformanceRepeatCount; i++) { - proto.append(message_proto); - } - - std::string multiple_repeated_field_proto; - for (size_t i = 0; i < kPerformanceRepeatCount; i++) { - multiple_repeated_field_proto.append(field_proto); - } - std::string expected_proto = - absl::StrCat(message_tag, delim(multiple_repeated_field_proto)); - - RunValidBinaryProtobufTest(test_name, RECOMMENDED, proto, expected_proto); -} - -template <typename MessageType> -void BinaryAndJsonConformanceSuiteImpl<MessageType>:: - RunValidProtobufTestWithMessage(const std::string& test_name, - ConformanceLevel level, - const Message* input, - const std::string& equivalent_text_format) { - RunValidProtobufTest(test_name, level, input->SerializeAsString(), - equivalent_text_format); -} - -// According to proto JSON specification, JSON serializers follow more strict -// rules than parsers (e.g., a serializer must serialize int32 values as JSON -// numbers while the parser is allowed to accept them as JSON strings). This -// method allows strict checking on a proto JSON serializer by inspecting -// the JSON output directly. -template <typename MessageType> -void BinaryAndJsonConformanceSuiteImpl< - MessageType>::RunValidJsonTestWithValidator(const std::string& test_name, - ConformanceLevel level, - const std::string& input_json, - const Validator& validator) { - suite_.RunValidJsonTestWithValidator<MessageType>(test_name, level, - input_json, validator); -} - -template <typename MessageType> -void BinaryAndJsonConformanceSuiteImpl<MessageType>::ExpectParseFailureForJson( - const std::string& test_name, ConformanceLevel level, - const std::string& input_json) { - suite_.ExpectParseFailureForJson<MessageType>(test_name, level, input_json); -} - -template <typename MessageType> -void BinaryAndJsonConformanceSuiteImpl<MessageType>:: - RunValidJsonTestOrParseFailure(const std::string& test_name, - ConformanceLevel level, - const std::string& input_json, - const std::string& equivalent_text_format) { - MessageType prototype; - ConformanceRequestSetting setting( - level, ::conformance::JSON, ::conformance::PROTOBUF, - ::conformance::JSON_TEST, prototype, test_name, input_json); - const ConformanceRequest& request = setting.GetRequest(); - ConformanceResponse response; - std::string effective_test_name = - absl::StrCat(setting.ConformanceLevelToString(level), ".", - SyntaxIdentifier(), ".JsonInput.", test_name); - - if (!suite_.RunTest(effective_test_name, request, &response)) { - return; - } - - TestStatus test; - test.set_name(effective_test_name); - if (response.result_case() == ConformanceResponse::kParseError) { - suite_.ReportSuccess(test); - } else if (response.result_case() == ConformanceResponse::kSkipped) { - suite_.ReportSkip(test, request, response); - } else { - std::unique_ptr<Message> reference_message(setting.NewTestMessage()); - ABSL_CHECK(TextFormat::ParseFromString(equivalent_text_format, - reference_message.get())) - << "Failed to parse data for test case: " << setting.GetTestName() - << ", data: " << equivalent_text_format; - std::unique_ptr<Message> test_message(setting.NewTestMessage()); - bool parsed = false; - if (response.result_case() == ConformanceResponse::kProtobufPayload) { - parsed = test_message->ParseFromString(response.protobuf_payload()); - } - if (!parsed) { - test.set_failure_message("Malformed protobuf response"); - suite_.ReportFailure(test, level, request, response); - return; - } - - util::MessageDifferencer differencer; - util::DefaultFieldComparator field_comparator; - field_comparator.set_treat_nan_as_equal(true); - differencer.set_field_comparator(&field_comparator); - std::string differences; - differencer.ReportDifferencesToString(&differences); - if (differencer.Compare(*reference_message, *test_message)) { - suite_.ReportSuccess(test); - } else { - test.set_failure_message( - "Should have failed to parse or matched expected output but did " - "not."); - suite_.ReportFailure(test, level, request, response); - } - } -} - -template <typename MessageType> -void BinaryAndJsonConformanceSuiteImpl<MessageType>:: - ExpectSerializeFailureForJson(const std::string& test_name, - ConformanceLevel level, - const std::string& text_format) { - MessageType payload_message; - ABSL_CHECK(TextFormat::ParseFromString(text_format, &payload_message)) - << "Failed to parse: " << text_format; - - MessageType prototype; - ConformanceRequestSetting setting( - level, ::conformance::PROTOBUF, ::conformance::JSON, - ::conformance::JSON_TEST, prototype, test_name, - payload_message.SerializeAsString()); - const ConformanceRequest& request = setting.GetRequest(); - ConformanceResponse response; - const std::string& effective_test_name = setting.GetTestName(); - - if (!suite_.RunTest(effective_test_name, request, &response)) { - return; - } - - TestStatus test; - test.set_name(effective_test_name); - if (response.result_case() == ConformanceResponse::kSerializeError) { - suite_.ReportSuccess(test); - } else if (response.result_case() == ConformanceResponse::kSkipped) { - suite_.ReportSkip(test, request, response); - } else if (response.result_case() == ConformanceResponse::kRuntimeError) { - test.set_failure_message( - "Should have failed to serialize, but raised an error instead."); - suite_.ReportFailure(test, level, request, response); - } else { - test.set_failure_message("Should have failed to serialize, but didn't."); - suite_.ReportFailure(test, level, request, response); - } -} - -template <typename MessageType> -void BinaryAndJsonConformanceSuiteImpl<MessageType>::TestPrematureEOFForType( - FieldDescriptor::Type type) { - // Incomplete values for each wire type. - static constexpr absl::string_view incompletes[6] = { - "\x80", // VARINT - "abcdefg", // 64BIT - "\x80", // DELIMITED (partial length) - "", // START_GROUP (no value required) - "", // END_GROUP (no value required) - "abc" // 32BIT - }; - - const FieldDescriptor* field = GetFieldForType(type, false); - const FieldDescriptor* rep_field = GetFieldForType(type, true); - WireFormatLite::WireType wire_type = WireFormatLite::WireTypeForFieldType( - static_cast<WireFormatLite::FieldType>(type)); - absl::string_view incomplete = incompletes[wire_type]; - const std::string type_name = - UpperCase(absl::StrCat(".", FieldDescriptor::TypeName(type))); - - ExpectParseFailureForProto( - tag(field->number(), wire_type), - absl::StrCat("PrematureEofBeforeKnownNonRepeatedValue", type_name), - REQUIRED); - - ExpectParseFailureForProto( - tag(rep_field->number(), wire_type), - absl::StrCat("PrematureEofBeforeKnownRepeatedValue", type_name), - REQUIRED); - - ExpectParseFailureForProto( - tag(UNKNOWN_FIELD, wire_type), - absl::StrCat("PrematureEofBeforeUnknownValue", type_name), REQUIRED); - - ExpectParseFailureForProto( - absl::StrCat(tag(field->number(), wire_type), incomplete), - absl::StrCat("PrematureEofInsideKnownNonRepeatedValue", type_name), - REQUIRED); - - ExpectParseFailureForProto( - absl::StrCat(tag(rep_field->number(), wire_type), incomplete), - absl::StrCat("PrematureEofInsideKnownRepeatedValue", type_name), - REQUIRED); - - ExpectParseFailureForProto( - absl::StrCat(tag(UNKNOWN_FIELD, wire_type), incomplete), - absl::StrCat("PrematureEofInsideUnknownValue", type_name), REQUIRED); - - if (wire_type == WireFormatLite::WIRETYPE_LENGTH_DELIMITED) { - ExpectParseFailureForProto( - absl::StrCat(tag(field->number(), wire_type), varint(1)), - absl::StrCat("PrematureEofInDelimitedDataForKnownNonRepeatedValue", - type_name), - REQUIRED); - - ExpectParseFailureForProto( - absl::StrCat(tag(rep_field->number(), wire_type), varint(1)), - absl::StrCat("PrematureEofInDelimitedDataForKnownRepeatedValue", - type_name), - REQUIRED); - - // EOF in the middle of delimited data for unknown value. - ExpectParseFailureForProto( - absl::StrCat(tag(UNKNOWN_FIELD, wire_type), varint(1)), - absl::StrCat("PrematureEofInDelimitedDataForUnknownValue", type_name), - REQUIRED); - - if (type == FieldDescriptor::TYPE_MESSAGE) { - // Submessage ends in the middle of a value. - std::string incomplete_submsg = absl::StrCat( - tag(WireFormatLite::TYPE_INT32, WireFormatLite::WIRETYPE_VARINT), - incompletes[WireFormatLite::WIRETYPE_VARINT]); - ExpectHardParseFailureForProto( - absl::StrCat( - tag(field->number(), WireFormatLite::WIRETYPE_LENGTH_DELIMITED), - varint(incomplete_submsg.size()), incomplete_submsg), - absl::StrCat("PrematureEofInSubmessageValue", type_name), REQUIRED); - } - } else if (type != FieldDescriptor::TYPE_GROUP) { - // Non-delimited, non-group: eligible for packing. - - // Packed region ends in the middle of a value. - ExpectHardParseFailureForProto( - absl::StrCat( - tag(rep_field->number(), WireFormatLite::WIRETYPE_LENGTH_DELIMITED), - varint(incomplete.size()), incomplete), - absl::StrCat("PrematureEofInPackedFieldValue", type_name), REQUIRED); - - // EOF in the middle of packed region. - ExpectParseFailureForProto( - absl::StrCat( - tag(rep_field->number(), WireFormatLite::WIRETYPE_LENGTH_DELIMITED), - varint(1)), - absl::StrCat("PrematureEofInPackedField", type_name), REQUIRED); - } -} - -template <typename MessageType> -void BinaryAndJsonConformanceSuiteImpl<MessageType>::TestValidDataForType( - FieldDescriptor::Type type, - std::vector<std::pair<std::string, std::string>> values) { - const std::string type_name = - UpperCase(absl::StrCat(".", FieldDescriptor::TypeName(type))); - WireFormatLite::WireType wire_type = WireFormatLite::WireTypeForFieldType( - static_cast<WireFormatLite::FieldType>(type)); - const FieldDescriptor* field = GetFieldForType(type, false); - const FieldDescriptor* rep_field = GetFieldForType(type, true); - - // Test singular data for singular fields. - for (size_t i = 0; i < values.size(); i++) { - std::string proto = - absl::StrCat(tag(field->number(), wire_type), values[i].first); - // In proto3, default primitive fields should not be encoded. - std::string expected_proto = - run_proto3_tests_ && IsProto3Default(field->type(), values[i].second) - ? "" - : absl::StrCat(tag(field->number(), wire_type), values[i].second); - MessageType test_message; - ABSL_CHECK(test_message.MergeFromString(expected_proto)); - std::string text; - ABSL_CHECK(TextFormat::PrintToString(test_message, &text)); - - RunValidProtobufTest( - absl::StrCat("ValidDataScalar", type_name, "[", i, "]"), REQUIRED, - proto, text); - RunValidBinaryProtobufTest( - absl::StrCat("ValidDataScalarBinary", type_name, "[", i, "]"), - RECOMMENDED, proto, expected_proto); - } - - // Test repeated data for singular fields. - // For scalar message fields, repeated values are merged, which is tested - // separately. - if (type != FieldDescriptor::TYPE_MESSAGE) { - std::string proto; - for (size_t i = 0; i < values.size(); i++) { - absl::StrAppend(&proto, tag(field->number(), wire_type), values[i].first); - } - std::string expected_proto = - absl::StrCat(tag(field->number(), wire_type), values.back().second); - MessageType test_message; - ABSL_CHECK(test_message.MergeFromString(expected_proto)); - std::string text; - ABSL_CHECK(TextFormat::PrintToString(test_message, &text)); - - RunValidProtobufTest(absl::StrCat("RepeatedScalarSelectsLast", type_name), - REQUIRED, proto, text); - } - - // Test repeated fields. - if (FieldDescriptor::IsTypePackable(type)) { - const FieldDescriptor* packed_field = - GetFieldForType(type, true, Packed::kTrue); - const FieldDescriptor* unpacked_field = - GetFieldForType(type, true, Packed::kFalse); - - std::string default_proto_packed; - std::string default_proto_unpacked; - std::string default_proto_packed_expected; - std::string default_proto_unpacked_expected; - std::string packed_proto_packed; - std::string packed_proto_unpacked; - std::string packed_proto_expected; - std::string unpacked_proto_packed; - std::string unpacked_proto_unpacked; - std::string unpacked_proto_expected; - - for (size_t i = 0; i < values.size(); i++) { - absl::StrAppend(&default_proto_unpacked, - tag(rep_field->number(), wire_type), values[i].first); - absl::StrAppend(&default_proto_unpacked_expected, - tag(rep_field->number(), wire_type), values[i].second); - default_proto_packed += values[i].first; - default_proto_packed_expected += values[i].second; - absl::StrAppend(&packed_proto_unpacked, - tag(packed_field->number(), wire_type), values[i].first); - packed_proto_packed += values[i].first; - packed_proto_expected += values[i].second; - absl::StrAppend(&unpacked_proto_unpacked, - tag(unpacked_field->number(), wire_type), - values[i].first); - unpacked_proto_packed += values[i].first; - absl::StrAppend(&unpacked_proto_expected, - tag(unpacked_field->number(), wire_type), - values[i].second); - } - default_proto_packed = absl::StrCat( - tag(rep_field->number(), WireFormatLite::WIRETYPE_LENGTH_DELIMITED), - delim(default_proto_packed)); - default_proto_packed_expected = absl::StrCat( - tag(rep_field->number(), WireFormatLite::WIRETYPE_LENGTH_DELIMITED), - delim(default_proto_packed_expected)); - packed_proto_packed = absl::StrCat( - tag(packed_field->number(), WireFormatLite::WIRETYPE_LENGTH_DELIMITED), - delim(packed_proto_packed)); - packed_proto_expected = absl::StrCat( - tag(packed_field->number(), WireFormatLite::WIRETYPE_LENGTH_DELIMITED), - delim(packed_proto_expected)); - unpacked_proto_packed = - absl::StrCat(tag(unpacked_field->number(), - WireFormatLite::WIRETYPE_LENGTH_DELIMITED), - delim(unpacked_proto_packed)); - - MessageType test_message; - ABSL_CHECK(test_message.MergeFromString(default_proto_packed_expected)); - std::string text; - ABSL_CHECK(TextFormat::PrintToString(test_message, &text)); - - // Ensures both packed and unpacked data can be parsed. - RunValidProtobufTest( - absl::StrCat("ValidDataRepeated", type_name, ".UnpackedInput"), - REQUIRED, default_proto_unpacked, text); - RunValidProtobufTest( - absl::StrCat("ValidDataRepeated", type_name, ".PackedInput"), REQUIRED, - default_proto_packed, text); - - // proto2 should encode as unpacked by default and proto3 should encode as - // packed by default. - std::string expected_proto = rep_field->is_packed() - ? default_proto_packed_expected - : default_proto_unpacked_expected; - RunValidBinaryProtobufTest(absl::StrCat("ValidDataRepeated", type_name, - ".UnpackedInput.DefaultOutput"), - RECOMMENDED, default_proto_unpacked, - expected_proto); - RunValidBinaryProtobufTest(absl::StrCat("ValidDataRepeated", type_name, - ".PackedInput.DefaultOutput"), - RECOMMENDED, default_proto_packed, - expected_proto); - RunValidBinaryProtobufTest(absl::StrCat("ValidDataRepeated", type_name, - ".UnpackedInput.PackedOutput"), - RECOMMENDED, packed_proto_unpacked, - packed_proto_expected); - RunValidBinaryProtobufTest(absl::StrCat("ValidDataRepeated", type_name, - ".PackedInput.PackedOutput"), - RECOMMENDED, packed_proto_packed, - packed_proto_expected); - RunValidBinaryProtobufTest(absl::StrCat("ValidDataRepeated", type_name, - ".UnpackedInput.UnpackedOutput"), - RECOMMENDED, unpacked_proto_unpacked, - unpacked_proto_expected); - RunValidBinaryProtobufTest(absl::StrCat("ValidDataRepeated", type_name, - ".PackedInput.UnpackedOutput"), - RECOMMENDED, unpacked_proto_packed, - unpacked_proto_expected); - } else { - std::string proto; - std::string expected_proto; - for (size_t i = 0; i < values.size(); i++) { - proto += - absl::StrCat(tag(rep_field->number(), wire_type), values[i].first); - expected_proto += - absl::StrCat(tag(rep_field->number(), wire_type), values[i].second); - } - MessageType test_message; - ABSL_CHECK(test_message.MergeFromString(expected_proto)); - std::string text; - ABSL_CHECK(TextFormat::PrintToString(test_message, &text)); - - RunValidProtobufTest(absl::StrCat("ValidDataRepeated", type_name), REQUIRED, - proto, text); - } -} - -template <typename MessageType> -void BinaryAndJsonConformanceSuiteImpl< - MessageType>::TestValidDataForRepeatedScalarMessage() { - std::vector<std::string> values = { - delim(absl::StrCat( - tag(2, WireFormatLite::WIRETYPE_LENGTH_DELIMITED), - delim(absl::StrCat( - tag(1, WireFormatLite::WIRETYPE_VARINT), varint(1234), - tag(2, WireFormatLite::WIRETYPE_VARINT), varint(1234), - tag(31, WireFormatLite::WIRETYPE_VARINT), varint(1234))))), - delim(absl::StrCat( - tag(2, WireFormatLite::WIRETYPE_LENGTH_DELIMITED), - delim(absl::StrCat( - tag(1, WireFormatLite::WIRETYPE_VARINT), varint(4321), - tag(3, WireFormatLite::WIRETYPE_VARINT), varint(4321), - tag(31, WireFormatLite::WIRETYPE_VARINT), varint(4321))))), - }; - - const std::string expected = - R"({ - corecursive: { - optional_int32: 4321, - optional_int64: 1234, - optional_uint32: 4321, - repeated_int32: [1234, 4321], - } - })"; - - std::string proto; - const FieldDescriptor* field = - GetFieldForType(FieldDescriptor::TYPE_MESSAGE, false); - for (size_t i = 0; i < values.size(); i++) { - absl::StrAppend( - &proto, tag(field->number(), WireFormatLite::WIRETYPE_LENGTH_DELIMITED), - values[i]); - } - - RunValidProtobufTest("RepeatedScalarMessageMerge", REQUIRED, proto, - absl::StrCat(field->name(), ": ", expected)); -} - -template <typename MessageType> -void BinaryAndJsonConformanceSuiteImpl<MessageType>::TestValidDataForMapType( - FieldDescriptor::Type key_type, FieldDescriptor::Type value_type) { - const std::string key_type_name = - UpperCase(absl::StrCat(".", FieldDescriptor::TypeName(key_type))); - const std::string value_type_name = - UpperCase(absl::StrCat(".", FieldDescriptor::TypeName(value_type))); - WireFormatLite::WireType key_wire_type = WireFormatLite::WireTypeForFieldType( - static_cast<WireFormatLite::FieldType>(key_type)); - WireFormatLite::WireType value_wire_type = - WireFormatLite::WireTypeForFieldType( - static_cast<WireFormatLite::FieldType>(value_type)); - - std::string key1_data = - absl::StrCat(tag(1, key_wire_type), GetDefaultValue(key_type)); - std::string value1_data = - absl::StrCat(tag(2, value_wire_type), GetDefaultValue(value_type)); - std::string key2_data = - absl::StrCat(tag(1, key_wire_type), GetNonDefaultValue(key_type)); - std::string value2_data = - absl::StrCat(tag(2, value_wire_type), GetNonDefaultValue(value_type)); - - const FieldDescriptor* field = GetFieldForMapType(key_type, value_type); - - { - // Tests map with default key and value. - std::string proto = absl::StrCat( - tag(field->number(), WireFormatLite::WIRETYPE_LENGTH_DELIMITED), - delim(absl::StrCat(key1_data, value1_data))); - MessageType test_message; - ABSL_CHECK(test_message.MergeFromString(proto)); - std::string text; - ABSL_CHECK(TextFormat::PrintToString(test_message, &text)); - RunValidProtobufTest(absl::StrCat("ValidDataMap", key_type_name, - value_type_name, ".Default"), - REQUIRED, proto, text); - } - - { - // Tests map with missing default key and value. - std::string proto = absl::StrCat( - tag(field->number(), WireFormatLite::WIRETYPE_LENGTH_DELIMITED), - delim("")); - MessageType test_message; - ABSL_CHECK(test_message.MergeFromString(proto)); - std::string text; - ABSL_CHECK(TextFormat::PrintToString(test_message, &text)); - RunValidProtobufTest(absl::StrCat("ValidDataMap", key_type_name, - value_type_name, ".MissingDefault"), - REQUIRED, proto, text); - } - - { - // Tests map with non-default key and value. - std::string proto = absl::StrCat( - tag(field->number(), WireFormatLite::WIRETYPE_LENGTH_DELIMITED), - delim(absl::StrCat(key2_data, value2_data))); - MessageType test_message; - ABSL_CHECK(test_message.MergeFromString(proto)); - std::string text; - ABSL_CHECK(TextFormat::PrintToString(test_message, &text)); - RunValidProtobufTest(absl::StrCat("ValidDataMap", key_type_name, - value_type_name, ".NonDefault"), - REQUIRED, proto, text); - } - - { - // Tests map with unordered key and value. - std::string proto = absl::StrCat( - tag(field->number(), WireFormatLite::WIRETYPE_LENGTH_DELIMITED), - delim(absl::StrCat(value2_data, key2_data))); - MessageType test_message; - ABSL_CHECK(test_message.MergeFromString(proto)); - std::string text; - ABSL_CHECK(TextFormat::PrintToString(test_message, &text)); - RunValidProtobufTest(absl::StrCat("ValidDataMap", key_type_name, - value_type_name, ".Unordered"), - REQUIRED, proto, text); - } - - { - // Tests map with duplicate key. - std::string proto1 = absl::StrCat( - tag(field->number(), WireFormatLite::WIRETYPE_LENGTH_DELIMITED), - delim(absl::StrCat(key2_data, value1_data))); - std::string proto2 = absl::StrCat( - tag(field->number(), WireFormatLite::WIRETYPE_LENGTH_DELIMITED), - delim(absl::StrCat(key2_data, value2_data))); - std::string proto = absl::StrCat(proto1, proto2); - MessageType test_message; - ABSL_CHECK(test_message.MergeFromString(proto2)); - std::string text; - ABSL_CHECK(TextFormat::PrintToString(test_message, &text)); - RunValidProtobufTest(absl::StrCat("ValidDataMap", key_type_name, - value_type_name, ".DuplicateKey"), - REQUIRED, proto, text); - } - - { - // Tests map with duplicate key in map entry. - std::string proto = absl::StrCat( - tag(field->number(), WireFormatLite::WIRETYPE_LENGTH_DELIMITED), - delim(absl::StrCat(key1_data, key2_data, value2_data))); - MessageType test_message; - ABSL_CHECK(test_message.MergeFromString(proto)); - std::string text; - ABSL_CHECK(TextFormat::PrintToString(test_message, &text)); - RunValidProtobufTest( - absl::StrCat("ValidDataMap", key_type_name, value_type_name, - ".DuplicateKeyInMapEntry"), - REQUIRED, proto, text); - } - - { - // Tests map with duplicate value in map entry. - std::string proto = absl::StrCat( - tag(field->number(), WireFormatLite::WIRETYPE_LENGTH_DELIMITED), - delim(absl::StrCat(key2_data, value1_data, value2_data))); - MessageType test_message; - ABSL_CHECK(test_message.MergeFromString(proto)); - std::string text; - ABSL_CHECK(TextFormat::PrintToString(test_message, &text)); - RunValidProtobufTest( - absl::StrCat("ValidDataMap", key_type_name, value_type_name, - ".DuplicateValueInMapEntry"), - REQUIRED, proto, text); - } -} - -template <typename MessageType> -void BinaryAndJsonConformanceSuiteImpl< - MessageType>::RunMapEntryWireTypeMismatchTest(const std::string& test_name, - const std::string& proto) { - MessageType prototype; - ConformanceRequestSetting setting( - RECOMMENDED, ::conformance::PROTOBUF, ::conformance::PROTOBUF, - ::conformance::BINARY_TEST, prototype, test_name, proto); - const ConformanceRequest& request = setting.GetRequest(); - ConformanceResponse response; - if (!suite_.RunTest(setting.GetTestName(), request, &response)) { - return; - } - - TestStatus test; - test.set_name(setting.GetTestName()); - - if (response.result_case() == ConformanceResponse::kSkipped) { - suite_.ReportSkip(test, request, response); - } else if (response.result_case() == ConformanceResponse::kParseError) { - suite_.ReportSuccess(test); - } else if (response.result_case() == ConformanceResponse::kProtobufPayload) { - MessageType reference; - ABSL_CHECK(reference.MergeFromString(proto)); - MessageType response_message; - if (suite_.ParseResponse(response, setting, &response_message)) { - if (google::protobuf::util::MessageDifferencer::Equals(reference, - response_message)) { - suite_.ReportSuccess(test); - } else { - test.set_failure_message( - "Output was not equivalent to reference message; the mismatched " - "field appears to have been decoded as a value."); - suite_.ReportFailure(test, setting.GetLevel(), request, response); - } - } - } else { - test.set_failure_message( - "Should have failed to parse or matched expected output but did " - "not."); - suite_.ReportFailure(test, setting.GetLevel(), request, response); - } -} - -template <typename MessageType> -void BinaryAndJsonConformanceSuiteImpl<MessageType>:: - TestMapEntryWireTypeMismatch(FieldDescriptor::Type key_type, - FieldDescriptor::Type value_type) { - const std::string key_type_name = - UpperCase(absl::StrCat(".", FieldDescriptor::TypeName(key_type))); - const std::string value_type_name = - UpperCase(absl::StrCat(".", FieldDescriptor::TypeName(value_type))); - const WireFormatLite::WireType key_wire_type = - WireFormatLite::WireTypeForFieldType( - static_cast<WireFormatLite::FieldType>(key_type)); - const WireFormatLite::WireType value_wire_type = - WireFormatLite::WireTypeForFieldType( - static_cast<WireFormatLite::FieldType>(value_type)); - - const std::string good_key_data = - absl::StrCat(tag(1, key_wire_type), GetNonDefaultValue(key_type)); - const std::string good_value_data = - absl::StrCat(tag(2, value_wire_type), GetNonDefaultValue(value_type)); - - const FieldDescriptor* field = GetFieldForMapType(key_type, value_type); - - // A map entry whose key is encoded with the wrong wire type; the value is - // still well formed. - RunMapEntryWireTypeMismatchTest( - absl::StrCat("ValidDataMap", key_type_name, value_type_name, - ".KeyWireTypeMismatch"), - absl::StrCat( - tag(field->number(), WireFormatLite::WIRETYPE_LENGTH_DELIMITED), - delim(absl::StrCat(MismatchedWireTypeField(1, key_wire_type), - good_value_data)))); - - // A map entry whose value is encoded with the wrong wire type; the key is - // still well formed. - RunMapEntryWireTypeMismatchTest( - absl::StrCat("ValidDataMap", key_type_name, value_type_name, - ".ValueWireTypeMismatch"), - absl::StrCat( - tag(field->number(), WireFormatLite::WIRETYPE_LENGTH_DELIMITED), - delim(absl::StrCat(good_key_data, - MismatchedWireTypeField(2, value_wire_type))))); -} - -template <typename MessageType> -void BinaryAndJsonConformanceSuiteImpl< - MessageType>::TestOverwriteMessageValueMap() { - std::string key_data = absl::StrCat( - tag(1, WireFormatLite::WIRETYPE_LENGTH_DELIMITED), delim("")); - std::string field1_data = - absl::StrCat(tag(1, WireFormatLite::WIRETYPE_VARINT), varint(1)); - std::string field2_data = - absl::StrCat(tag(2, WireFormatLite::WIRETYPE_VARINT), varint(1)); - std::string field31_data = - absl::StrCat(tag(31, WireFormatLite::WIRETYPE_VARINT), varint(1)); - std::string submsg1_data = delim(absl::StrCat(field1_data, field31_data)); - std::string submsg2_data = delim(absl::StrCat(field2_data, field31_data)); - std::string value1_data = absl::StrCat( - tag(2, WireFormatLite::WIRETYPE_LENGTH_DELIMITED), - delim(absl::StrCat(tag(2, WireFormatLite::WIRETYPE_LENGTH_DELIMITED), - submsg1_data))); - std::string value2_data = absl::StrCat( - tag(2, WireFormatLite::WIRETYPE_LENGTH_DELIMITED), - delim(absl::StrCat(tag(2, WireFormatLite::WIRETYPE_LENGTH_DELIMITED), - submsg2_data))); - - const FieldDescriptor* field = GetFieldForMapType( - FieldDescriptor::TYPE_STRING, FieldDescriptor::TYPE_MESSAGE); - - std::string proto1 = absl::StrCat( - tag(field->number(), WireFormatLite::WIRETYPE_LENGTH_DELIMITED), - delim(absl::StrCat(key_data, value1_data))); - std::string proto2 = absl::StrCat( - tag(field->number(), WireFormatLite::WIRETYPE_LENGTH_DELIMITED), - delim(absl::StrCat(key_data, value2_data))); - std::string proto = absl::StrCat(proto1, proto2); - MessageType test_message; - ABSL_CHECK(test_message.MergeFromString(proto2)); - std::string text; - ABSL_CHECK(TextFormat::PrintToString(test_message, &text)); - RunValidProtobufTest("ValidDataMap.STRING.MESSAGE.MergeValue", REQUIRED, - proto, text); -} - -template <typename MessageType> -void BinaryAndJsonConformanceSuiteImpl<MessageType>::TestValidDataForOneofType( - FieldDescriptor::Type type) { - const std::string type_name = - UpperCase(absl::StrCat(".", FieldDescriptor::TypeName(type))); - WireFormatLite::WireType wire_type = WireFormatLite::WireTypeForFieldType( - static_cast<WireFormatLite::FieldType>(type)); - - const FieldDescriptor* field = GetFieldForOneofType(type); - const std::string default_value = - absl::StrCat(tag(field->number(), wire_type), GetDefaultValue(type)); - const std::string non_default_value = - absl::StrCat(tag(field->number(), wire_type), GetNonDefaultValue(type)); - - { - // Tests oneof with default value. - const std::string& proto = default_value; - MessageType test_message; - ABSL_CHECK(test_message.MergeFromString(proto)); - std::string text; - ABSL_CHECK(TextFormat::PrintToString(test_message, &text)); - - RunValidProtobufTest( - absl::StrCat("ValidDataOneof", type_name, ".DefaultValue"), REQUIRED, - proto, text); - RunValidBinaryProtobufTest( - absl::StrCat("ValidDataOneofBinary", type_name, ".DefaultValue"), - RECOMMENDED, proto, proto); - } - - { - // Tests oneof with non-default value. - const std::string& proto = non_default_value; - MessageType test_message; - ABSL_CHECK(test_message.MergeFromString(proto)); - std::string text; - ABSL_CHECK(TextFormat::PrintToString(test_message, &text)); - - RunValidProtobufTest( - absl::StrCat("ValidDataOneof", type_name, ".NonDefaultValue"), REQUIRED, - proto, text); - RunValidBinaryProtobufTest( - absl::StrCat("ValidDataOneofBinary", type_name, ".NonDefaultValue"), - RECOMMENDED, proto, proto); - } - - { - // Tests oneof with multiple values of the same field. - const std::string proto = absl::StrCat(default_value, non_default_value); - const std::string& expected_proto = non_default_value; - MessageType test_message; - ABSL_CHECK(test_message.MergeFromString(expected_proto)); - std::string text; - ABSL_CHECK(TextFormat::PrintToString(test_message, &text)); - - RunValidProtobufTest(absl::StrCat("ValidDataOneof", type_name, - ".MultipleValuesForSameField"), - REQUIRED, proto, text); - RunValidBinaryProtobufTest(absl::StrCat("ValidDataOneofBinary", type_name, - ".MultipleValuesForSameField"), - RECOMMENDED, proto, expected_proto); - } - - { - // Tests oneof with multiple values of the different fields. - const FieldDescriptor* other_field = GetFieldForOneofType(type, true); - FieldDescriptor::Type other_type = other_field->type(); - WireFormatLite::WireType other_wire_type = - WireFormatLite::WireTypeForFieldType( - static_cast<WireFormatLite::FieldType>(other_type)); - const std::string other_value = - absl::StrCat(tag(other_field->number(), other_wire_type), - GetDefaultValue(other_type)); - - const std::string proto = absl::StrCat(other_value, non_default_value); - const std::string& expected_proto = non_default_value; - MessageType test_message; - ABSL_CHECK(test_message.MergeFromString(expected_proto)); - std::string text; - ABSL_CHECK(TextFormat::PrintToString(test_message, &text)); - - RunValidProtobufTest(absl::StrCat("ValidDataOneof", type_name, - ".MultipleValuesForDifferentField"), - REQUIRED, proto, text); - RunValidBinaryProtobufTest(absl::StrCat("ValidDataOneofBinary", type_name, - ".MultipleValuesForDifferentField"), - RECOMMENDED, proto, expected_proto); - } -} - -template <typename MessageType> -void BinaryAndJsonConformanceSuiteImpl<MessageType>::TestMergeOneofMessage() { - std::string field1_data = - absl::StrCat(tag(1, WireFormatLite::WIRETYPE_VARINT), varint(1)); - std::string field2a_data = - absl::StrCat(tag(2, WireFormatLite::WIRETYPE_VARINT), varint(1)); - std::string field2b_data = - absl::StrCat(tag(2, WireFormatLite::WIRETYPE_VARINT), varint(1)); - std::string field89_data = - absl::StrCat(tag(89, WireFormatLite::WIRETYPE_VARINT), varint(1)); - std::string submsg1_data = absl::StrCat( - tag(2, WireFormatLite::WIRETYPE_LENGTH_DELIMITED), - delim(absl::StrCat(field1_data, field2a_data, field89_data))); - std::string submsg2_data = - absl::StrCat(tag(2, WireFormatLite::WIRETYPE_LENGTH_DELIMITED), - delim(absl::StrCat(field2b_data, field89_data))); - std::string merged_data = - absl::StrCat(tag(2, WireFormatLite::WIRETYPE_LENGTH_DELIMITED), - delim(absl::StrCat(field1_data, field2b_data, field89_data, - field89_data))); - - const FieldDescriptor* field = - GetFieldForOneofType(FieldDescriptor::TYPE_MESSAGE); - - std::string proto1 = absl::StrCat( - tag(field->number(), WireFormatLite::WIRETYPE_LENGTH_DELIMITED), - delim(submsg1_data)); - std::string proto2 = absl::StrCat( - tag(field->number(), WireFormatLite::WIRETYPE_LENGTH_DELIMITED), - delim(submsg2_data)); - std::string proto = absl::StrCat(proto1, proto2); - std::string expected_proto = absl::StrCat( - tag(field->number(), WireFormatLite::WIRETYPE_LENGTH_DELIMITED), - delim(merged_data)); - - MessageType test_message; - ABSL_CHECK(test_message.MergeFromString(expected_proto)); - std::string text; - ABSL_CHECK(TextFormat::PrintToString(test_message, &text)); - RunValidProtobufTest("ValidDataOneof.MESSAGE.Merge", REQUIRED, proto, text); - RunValidBinaryProtobufTest("ValidDataOneofBinary.MESSAGE.Merge", RECOMMENDED, - proto, expected_proto); -} - -template <typename MessageType> -void BinaryAndJsonConformanceSuiteImpl<MessageType>::TestIllegalTags() { - // field num 0 is illegal - std::string nullfield[] = {"\1DEADBEEF", "\2\1\1", "\3\4", "\5DEAD"}; - for (int i = 0; i < 4; i++) { - std::string name = "IllegalZeroFieldNum_Case_0"; - name.back() += i; - ExpectParseFailureForProto(nullfield[i], name, REQUIRED); - } - - // Reused for a few cases: this is a single byte tag for a field number 1 - // varint but where the byte has the continuation bit set, so that we can - // easily construct longer tags. - std::string tag_with_continuation_bit = - tag(1, WireFormatLite::WIRETYPE_VARINT); - assert(tag_with_continuation_bit.size() == 1); - tag_with_continuation_bit[0] |= 0b10000000; - - // A varint where field number is far out of range of - // maximum legal field number. The lower 5 bytes of the varint do look like a - // well-formed tag for field number 1. - ExpectParseFailureForProto( - absl::StrCat(tag_with_continuation_bit, "\x80\x80\x80\x80\x80\x0F", - varint(1234)), - "BadTag_FieldNumberTooHigh", REQUIRED); - - // A 5-byte tag varint where the value is above UINT32_MAX (bit 35 is set) - ExpectParseFailureForProto( - absl::StrCat(tag_with_continuation_bit, "\x80\x80\x80\x40", varint(1234)), - "BadTag_FieldNumberSlightlyTooHigh", REQUIRED); - - // A tag where the varint is more than 5 bytes but only because it is an - // overlong varint, so the decoded value is still below UINT32_MAX. - ExpectParseFailureForProto( - absl::StrCat(tag_with_continuation_bit, "\x80\x80\x80\x80\x80\x80\x80", - std::string("\0", 1), varint(1234)), - "BadTag_OverlongVarint", REQUIRED); - - // An overlong varint that is even more than 10 bytes. - ExpectParseFailureForProto( - absl::StrCat(tag_with_continuation_bit, - "\x80\x80\x80\x80\x80\x80\x80\x80\x80\x80\x80", - std::string("\0", 1), varint(1234)), - "BadTag_VarintMoreThanTenBytes", REQUIRED); -} - -template <typename MessageType> -void BinaryAndJsonConformanceSuiteImpl<MessageType>::TestIllegalLengths() { - const FieldDescriptor* string_field = - GetFieldForType(FieldDescriptor::TYPE_STRING, false); - - // A 5-byte varint length where bits 32-34 are set (e.g. bit 32 = 0x10), which - // overflows 32-bit arithmetic and wraps to 0 modulo 2^32. Parsers must not - // overflow and must reject this invalid wire format. - ExpectParseFailureForProto( - absl::StrCat(tag(string_field->number(), - WireFormatLite::WIRETYPE_LENGTH_DELIMITED), - "\x80\x80\x80\x80\x10"), - "BadLength_Varint32BitOverflow", REQUIRED); -} - -template <typename MessageType> -void BinaryAndJsonConformanceSuiteImpl<MessageType>::TestUnmatchedGroup() { - ExpectParseFailureForProto(tag(201, WireFormatLite::WIRETYPE_END_GROUP), - "UnmatchedEndGroup", REQUIRED); - ExpectParseFailureForProto(tag(1234, WireFormatLite::WIRETYPE_END_GROUP), - "UnmatchedEndGroupUnknown", REQUIRED); - ExpectParseFailureForProto(tag(1, WireFormatLite::WIRETYPE_END_GROUP), - "UnmatchedEndGroupWrongType", REQUIRED); - ExpectParseFailureForProto( - len(18, tag(1234, WireFormatLite::WIRETYPE_END_GROUP)), - "UnmatchedEndGroupNestedLen", REQUIRED); - ExpectParseFailureForProto( - group(201, tag(202, WireFormatLite::WIRETYPE_END_GROUP)), - "UnmatchedEndGroupNested", REQUIRED); - ExpectParseFailureForProto( - absl::StrCat(tag(1, WireFormatLite::WIRETYPE_END_GROUP), - len(2, "hello world")), - "UnmatchedEndGroupWithData", REQUIRED); - - ExpectParseFailureForProto(tag(201, WireFormatLite::WIRETYPE_START_GROUP), - "UnmatchedStartGroup", REQUIRED); - ExpectParseFailureForProto(tag(1234, WireFormatLite::WIRETYPE_START_GROUP), - "UnmatchedStartGroupUnknown", REQUIRED); - ExpectParseFailureForProto(tag(1, WireFormatLite::WIRETYPE_START_GROUP), - "UnmatchedStartGroupWrongType", REQUIRED); - ExpectParseFailureForProto( - len(18, tag(1234, WireFormatLite::WIRETYPE_START_GROUP)), - "UnmatchedStartGroupNestedLen", REQUIRED); - ExpectParseFailureForProto( - group(201, tag(202, WireFormatLite::WIRETYPE_START_GROUP)), - "UnmatchedStartGroupNested", REQUIRED); - ExpectParseFailureForProto( - absl::StrCat(tag(1, WireFormatLite::WIRETYPE_START_GROUP), - len(2, "hello world")), - "UnmatchedStartGroupWithData", REQUIRED); - - ExpectParseFailureForProto( - absl::StrCat(tag(201, WireFormatLite::WIRETYPE_START_GROUP), - len(2, "hello world"), - tag(202, WireFormatLite::WIRETYPE_END_GROUP)), - "MismatchedGroupTags", REQUIRED); - ExpectParseFailureForProto( - group(201, absl::StrCat(tag(202, WireFormatLite::WIRETYPE_START_GROUP), - len(2, "hello world"), - tag(203, WireFormatLite::WIRETYPE_END_GROUP))), - "MismatchedNestedGroupTags", REQUIRED); -} - -template <typename MessageType> -void BinaryAndJsonConformanceSuiteImpl<MessageType>::TestUnknownWireType() { - for (uint8_t type : {0x6, 0x7}) { - for (uint8_t field = 0; field < 4; ++field) { - for (uint8_t value = 0; value < 4; ++value) { - std::string name = absl::StrFormat( - "UnknownWireType%d_Field%d_Version%d", type, field, value); - - char data[2]; - data[0] = (field << 3) | type; // unknown wire type. - data[1] = value; - std::string proto = {data, 2}; - ExpectParseFailureForProto(proto, name, REQUIRED); - } - } - } -} - -template <typename MessageType> -void BinaryAndJsonConformanceSuiteImpl<MessageType>::TestInvalidUtf8String() { - if (run_proto3_tests_) { - ExpectParseFailureForProto(len(14, "\xA0\xB0\xC0\xD0"), - "RejectInvalidUtf8.String.Singular", - RECOMMENDED); - ExpectParseFailureForProto(len(44, "\xA0\xB0\xC0\xD0"), - "RejectInvalidUtf8.String.Repeated", - RECOMMENDED); - ExpectParseFailureForProto(len(113, "\xA0\xB0\xC0\xD0"), - "RejectInvalidUtf8.String.Oneof", RECOMMENDED); - ExpectParseFailureForProto( - len(69, absl::StrCat(len(1, "\xA0\xB0\xC0\xD0"), len(2, "foo"))), - "RejectInvalidUtf8.String.MapKey", RECOMMENDED); - ExpectParseFailureForProto( - len(69, absl::StrCat(len(1, "foo"), len(2, "\xA0\xB0\xC0\xD0"))), - "RejectInvalidUtf8.String.MapValue", RECOMMENDED); - } else { - // TODO - Once conformance tests can express failures that are - // not expected to be fixed, add proto2 coverage here. - } -} - -template <typename MessageType> -void BinaryAndJsonConformanceSuiteImpl<MessageType>::TestOneofMessage() { - MessageType message; - message.set_oneof_uint32(0); - RunValidProtobufTestWithMessage("OneofZeroUint32", RECOMMENDED, &message, - "oneof_uint32: 0"); - message.mutable_oneof_nested_message()->set_a(0); - RunValidProtobufTestWithMessage("OneofZeroMessage", RECOMMENDED, &message, - run_proto3_tests_ - ? "oneof_nested_message: {}" - : "oneof_nested_message: {a: 0}"); - message.mutable_oneof_nested_message()->set_a(1); - RunValidProtobufTestWithMessage("OneofZeroMessageSetTwice", RECOMMENDED, - &message, "oneof_nested_message: {a: 1}"); - message.set_oneof_string(""); - RunValidProtobufTestWithMessage("OneofZeroString", RECOMMENDED, &message, - "oneof_string: \"\""); - message.set_oneof_bytes(""); - RunValidProtobufTestWithMessage("OneofZeroBytes", RECOMMENDED, &message, - "oneof_bytes: \"\""); - message.set_oneof_bool(false); - RunValidProtobufTestWithMessage("OneofZeroBool", RECOMMENDED, &message, - "oneof_bool: false"); - message.set_oneof_uint64(0); - RunValidProtobufTestWithMessage("OneofZeroUint64", RECOMMENDED, &message, - "oneof_uint64: 0"); - message.set_oneof_float(0.0f); - RunValidProtobufTestWithMessage("OneofZeroFloat", RECOMMENDED, &message, - "oneof_float: 0"); - message.set_oneof_double(0.0); - RunValidProtobufTestWithMessage("OneofZeroDouble", RECOMMENDED, &message, - "oneof_double: 0"); - message.set_oneof_enum(MessageType::FOO); - RunValidProtobufTestWithMessage("OneofZeroEnum", RECOMMENDED, &message, - "oneof_enum: FOO"); -} - -template <typename MessageType> -void BinaryAndJsonConformanceSuiteImpl<MessageType>::TestUnknownMessage() { - MessageType message; - ABSL_CHECK(message.ParseFromString("\xA8\x1F\x01")); - RunValidBinaryProtobufTest("UnknownVarint", REQUIRED, - message.SerializeAsString()); -} - -template <typename MessageType> -void BinaryAndJsonConformanceSuiteImpl<MessageType>::TestUnknownOrdering() { - // Implementations must preserve the ordering of different unknown fields for - // the same field number. This is because some field types will accept - // multiple wire types for the same field. For example, repeated primitive - // fields will accept both length-prefixed (packed) and - // varint/fixed32/fixed64 (unpacked) wire types, and reordering these could - // reorder the elements of the repeated field. - MessageType message; - MessageType prototype; - message.mutable_unknown_fields()->AddLengthDelimited(UNKNOWN_FIELD, "abc"); - message.mutable_unknown_fields()->AddVarint(UNKNOWN_FIELD, 123); - message.mutable_unknown_fields()->AddLengthDelimited(UNKNOWN_FIELD, "def"); - message.mutable_unknown_fields()->AddVarint(UNKNOWN_FIELD, 456); - std::string serialized = message.SerializeAsString(); - - ConformanceRequestSetting setting( - REQUIRED, ::conformance::PROTOBUF, ::conformance::PROTOBUF, - ::conformance::BINARY_TEST, prototype, "UnknownOrdering", serialized); - const ConformanceRequest& request = setting.GetRequest(); - ConformanceResponse response; - if (!suite_.RunTest(setting.GetTestName(), request, &response)) { - return; - } - - MessageType response_message; - TestStatus test; - test.set_name(setting.GetTestName()); - if (response.result_case() == ConformanceResponse::kSkipped) { - suite_.ReportSkip(test, request, response); - return; - } - - suite_.ParseResponse(response, setting, &response_message); - - const UnknownFieldSet& ufs = response_message.unknown_fields(); - if (ufs.field_count() != 4 || ufs.field(0).number() != UNKNOWN_FIELD || - ufs.field(1).number() != UNKNOWN_FIELD || - ufs.field(2).number() != UNKNOWN_FIELD || - ufs.field(3).number() != UNKNOWN_FIELD || - ufs.field(0).type() != UnknownField::Type::TYPE_LENGTH_DELIMITED || - ufs.field(1).type() != UnknownField::Type::TYPE_VARINT || - ufs.field(2).type() != UnknownField::Type::TYPE_LENGTH_DELIMITED || - ufs.field(3).type() != UnknownField::Type::TYPE_VARINT || - ufs.field(0).length_delimited() != "abc" || - ufs.field(1).varint() != 123 || - ufs.field(2).length_delimited() != "def" || - ufs.field(3).varint() != 456) { - test.set_failure_message("Unknown field mismatch"); - suite_.ReportFailure(test, setting.GetLevel(), request, response); - } else { - suite_.ReportSuccess(test); - } -} - -template <typename MessageType> -void BinaryAndJsonConformanceSuiteImpl< - MessageType>::TestBinaryPerformanceForAlternatingUnknownFields() { - std::string unknown_field_1 = absl::StrCat( - tag(UNKNOWN_FIELD, WireFormatLite::WIRETYPE_VARINT), varint(1234)); - std::string unknown_field_2 = absl::StrCat( - tag(UNKNOWN_FIELD + 1, WireFormatLite::WIRETYPE_VARINT), varint(5678)); - std::string proto; - for (size_t i = 0; i < kPerformanceRepeatCount; i++) { - proto.append(unknown_field_1); - proto.append(unknown_field_2); - } - - RunValidBinaryProtobufTest("TestBinaryPerformanceForAlternatingUnknownFields", - RECOMMENDED, proto); -} - -template <typename MessageType> -void BinaryAndJsonConformanceSuiteImpl<MessageType>:: - TestBinaryPerformanceMergeMessageWithRepeatedFieldForType( - FieldDescriptor::Type type) { - const std::string type_name = - UpperCase(absl::StrCat(".", FieldDescriptor::TypeName(type))); - int field_number = GetFieldForType(type, true, Packed::kFalse)->number(); - std::string rep_field_proto = absl::StrCat( - tag(field_number, WireFormatLite::WireTypeForFieldType( - static_cast<WireFormatLite::FieldType>(type))), - GetNonDefaultValue(type)); - - RunBinaryPerformanceMergeMessageWithField( - absl::StrCat("TestBinaryPerformanceMergeMessageWithRepeatedFieldForType", - type_name), - rep_field_proto); -} - -template <typename MessageType> -void BinaryAndJsonConformanceSuiteImpl<MessageType>:: - TestBinaryPerformanceMergeMessageWithUnknownFieldForType( - FieldDescriptor::Type type) { - const std::string type_name = - UpperCase(absl::StrCat(".", FieldDescriptor::TypeName(type))); - std::string unknown_field_proto = absl::StrCat( - tag(UNKNOWN_FIELD, WireFormatLite::WireTypeForFieldType( - static_cast<WireFormatLite::FieldType>(type))), - GetNonDefaultValue(type)); - RunBinaryPerformanceMergeMessageWithField( - absl::StrCat("TestBinaryPerformanceMergeMessageWithUnknownFieldForType", - type_name), - unknown_field_proto); -} - -template <typename MessageType> -BinaryAndJsonConformanceSuiteImpl<MessageType>:: - BinaryAndJsonConformanceSuiteImpl(BinaryAndJsonConformanceSuite* suite, - bool run_proto3_tests) - : suite_(*ABSL_DIE_IF_NULL(suite)), run_proto3_tests_(run_proto3_tests) { - suite_.SetTypeUrl(GetTypeUrl(MessageType::GetDescriptor())); - RunAllTests(); -} - -template <typename MessageType> -void BinaryAndJsonConformanceSuiteImpl<MessageType>::RunAllTests() { - if (!suite_.performance_) { - for (int i = 1; i <= FieldDescriptor::MAX_TYPE; i++) { - if (i == FieldDescriptor::TYPE_GROUP) continue; - TestPrematureEOFForType(static_cast<FieldDescriptor::Type>(i)); - } - - TestIllegalTags(); - TestIllegalLengths(); - TestUnmatchedGroup(); - TestUnknownWireType(); - TestInvalidUtf8String(); - - int64_t kInt64Min = -9223372036854775808ULL; - int64_t kInt64Max = 9223372036854775807ULL; - uint64_t kUint64Max = 18446744073709551615ULL; - int32_t kInt32Max = 2147483647; - int32_t kInt32Min = -2147483648; - uint32_t kUint32Max = 4294967295UL; - - TestValidDataForType( - FieldDescriptor::TYPE_DOUBLE, - { - {dbl(0), dbl(0)}, - {dbl(0.1), dbl(0.1)}, - {dbl(1.7976931348623157e+308), dbl(1.7976931348623157e+308)}, - {dbl(2.22507385850720138309e-308), - dbl(2.22507385850720138309e-308)}, - }); - TestValidDataForType( - FieldDescriptor::TYPE_FLOAT, - { - {flt(0), flt(0)}, - {flt(0.1), flt(0.1)}, - {flt(1.00000075e-36), flt(1.00000075e-36)}, - {flt(3.402823e+38), flt(3.402823e+38)}, // 3.40282347e+38 - {flt(1.17549435e-38f), flt(1.17549435e-38)}, - }); - TestValidDataForType(FieldDescriptor::TYPE_INT64, - { - {varint(0), varint(0)}, - {varint(12345), varint(12345)}, - {varint(kInt64Max), varint(kInt64Max)}, - {varint(kInt64Min), varint(kInt64Min)}, - }); - TestValidDataForType(FieldDescriptor::TYPE_UINT64, - { - {varint(0), varint(0)}, - {varint(12345), varint(12345)}, - {varint(kUint64Max), varint(kUint64Max)}, - }); - TestValidDataForType(FieldDescriptor::TYPE_INT32, - { - {varint(0), varint(0)}, - {varint(12345), varint(12345)}, - {longvarint(12345, 2), varint(12345)}, - {longvarint(12345, 7), varint(12345)}, - {varint(kInt32Max), varint(kInt32Max)}, - {varint(kInt32Min), varint(kInt32Min)}, - {varint(1LL << 33), varint(0)}, - {varint((1LL << 33) - 1), varint(-1)}, - {varint(kInt64Max), varint(-1)}, - {varint(kInt64Min + 1), varint(1)}, - }); - TestValidDataForType( - FieldDescriptor::TYPE_UINT32, - { - {varint(0), varint(0)}, - {varint(12345), varint(12345)}, - {longvarint(12345, 2), varint(12345)}, - {longvarint(12345, 7), varint(12345)}, - {varint(kUint32Max), varint(kUint32Max)}, // UINT32_MAX - {varint(1LL << 33), varint(0)}, - {varint((1LL << 33) + 1), varint(1)}, - {varint((1LL << 33) - 1), varint((1LL << 32) - 1)}, - {varint(kInt64Max), varint((1LL << 32) - 1)}, - {varint(kInt64Min + 1), varint(1)}, - }); - TestValidDataForType(FieldDescriptor::TYPE_FIXED64, - { - {u64(0), u64(0)}, - {u64(12345), u64(12345)}, - {u64(kUint64Max), u64(kUint64Max)}, - }); - TestValidDataForType(FieldDescriptor::TYPE_FIXED32, - { - {u32(0), u32(0)}, - {u32(12345), u32(12345)}, - {u32(kUint32Max), u32(kUint32Max)}, // UINT32_MAX - }); - TestValidDataForType(FieldDescriptor::TYPE_SFIXED64, - { - {u64(0), u64(0)}, - {u64(12345), u64(12345)}, - {u64(kInt64Max), u64(kInt64Max)}, - {u64(kInt64Min), u64(kInt64Min)}, - }); - TestValidDataForType(FieldDescriptor::TYPE_SFIXED32, - { - {u32(0), u32(0)}, - {u32(12345), u32(12345)}, - {u32(kInt32Max), u32(kInt32Max)}, - {u32(kInt32Min), u32(kInt32Min)}, - }); - // Bools should be serialized as 0 for false and 1 for true. Parsers should - // also interpret any nonzero value as true. - TestValidDataForType(FieldDescriptor::TYPE_BOOL, - { - {varint(0), varint(0)}, - {varint(1), varint(1)}, - {varint(-1), varint(1)}, - {varint(12345678), varint(1)}, - {varint(1LL << 33), varint(1)}, - {varint(kInt64Max), varint(1)}, - {varint(kInt64Min), varint(1)}, - }); - TestValidDataForType(FieldDescriptor::TYPE_SINT32, - { - {zz32(0), zz32(0)}, - {zz32(12345), zz32(12345)}, - {zz32(kInt32Max), zz32(kInt32Max)}, - {zz32(kInt32Min), zz32(kInt32Min)}, - {zz64(kInt32Max + 2LL), zz32(1)}, - }); - TestValidDataForType(FieldDescriptor::TYPE_SINT64, - { - {zz64(0), zz64(0)}, - {zz64(12345), zz64(12345)}, - {zz64(kInt64Max), zz64(kInt64Max)}, - {zz64(kInt64Min), zz64(kInt64Min)}, - }); - TestValidDataForType( - FieldDescriptor::TYPE_STRING, - { - {delim(""), delim("")}, - {delim("Hello world!"), delim("Hello world!")}, - {delim("\'\"\?\\\a\b\f\n\r\t\v"), - delim("\'\"\?\\\a\b\f\n\r\t\v")}, // escape - // U+8C37 U+6B4C ("Google" in Chinese), as UTF-8. - {delim("\xE8\xB0\xB7\xE6\xAD\x8C"), - delim("\xE8\xB0\xB7\xE6\xAD\x8C")}, - // U+1F601 (grinning face with smiling eyes), as UTF-8. - {delim("\xF0\x9F\x98\x81"), delim("\xF0\x9F\x98\x81")}, - }); - TestValidDataForType(FieldDescriptor::TYPE_BYTES, - { - {delim(""), delim("")}, - {delim("Hello world!"), delim("Hello world!")}, - {delim("\x01\x02"), delim("\x01\x02")}, - {delim("\xfb"), delim("\xfb")}, - }); - TestValidDataForType(FieldDescriptor::TYPE_ENUM, - { - {varint(0), varint(0)}, - {varint(1), varint(1)}, - {varint(2), varint(2)}, - {varint(-1), varint(-1)}, - {varint(kInt64Max), varint(-1)}, - {varint(kInt64Min + 1), varint(1)}, - }); - TestValidDataForRepeatedScalarMessage(); - TestValidDataForType( - FieldDescriptor::TYPE_MESSAGE, - { - {delim(""), delim("")}, - {delim(absl::StrCat(tag(1, WireFormatLite::WIRETYPE_VARINT), - varint(1234))), - delim(absl::StrCat(tag(1, WireFormatLite::WIRETYPE_VARINT), - varint(1234)))}, - }); - - TestValidDataForMapType(FieldDescriptor::TYPE_INT32, - FieldDescriptor::TYPE_INT32); - TestValidDataForMapType(FieldDescriptor::TYPE_INT64, - FieldDescriptor::TYPE_INT64); - TestValidDataForMapType(FieldDescriptor::TYPE_UINT32, - FieldDescriptor::TYPE_UINT32); - TestValidDataForMapType(FieldDescriptor::TYPE_UINT64, - FieldDescriptor::TYPE_UINT64); - TestValidDataForMapType(FieldDescriptor::TYPE_SINT32, - FieldDescriptor::TYPE_SINT32); - TestValidDataForMapType(FieldDescriptor::TYPE_SINT64, - FieldDescriptor::TYPE_SINT64); - TestValidDataForMapType(FieldDescriptor::TYPE_FIXED32, - FieldDescriptor::TYPE_FIXED32); - TestValidDataForMapType(FieldDescriptor::TYPE_FIXED64, - FieldDescriptor::TYPE_FIXED64); - TestValidDataForMapType(FieldDescriptor::TYPE_SFIXED32, - FieldDescriptor::TYPE_SFIXED32); - TestValidDataForMapType(FieldDescriptor::TYPE_SFIXED64, - FieldDescriptor::TYPE_SFIXED64); - TestValidDataForMapType(FieldDescriptor::TYPE_INT32, - FieldDescriptor::TYPE_FLOAT); - TestValidDataForMapType(FieldDescriptor::TYPE_INT32, - FieldDescriptor::TYPE_DOUBLE); - TestValidDataForMapType(FieldDescriptor::TYPE_BOOL, - FieldDescriptor::TYPE_BOOL); - TestValidDataForMapType(FieldDescriptor::TYPE_STRING, - FieldDescriptor::TYPE_STRING); - TestValidDataForMapType(FieldDescriptor::TYPE_STRING, - FieldDescriptor::TYPE_BYTES); - TestValidDataForMapType(FieldDescriptor::TYPE_STRING, - FieldDescriptor::TYPE_ENUM); - TestValidDataForMapType(FieldDescriptor::TYPE_STRING, - FieldDescriptor::TYPE_MESSAGE); - // Additional test to check overwriting message value map. - TestOverwriteMessageValueMap(); - - TestMapEntryWireTypeMismatch(FieldDescriptor::TYPE_INT32, - FieldDescriptor::TYPE_INT32); - TestMapEntryWireTypeMismatch(FieldDescriptor::TYPE_FIXED32, - FieldDescriptor::TYPE_FIXED32); - TestMapEntryWireTypeMismatch(FieldDescriptor::TYPE_BOOL, - FieldDescriptor::TYPE_BOOL); - TestMapEntryWireTypeMismatch(FieldDescriptor::TYPE_STRING, - FieldDescriptor::TYPE_STRING); - TestMapEntryWireTypeMismatch(FieldDescriptor::TYPE_STRING, - FieldDescriptor::TYPE_MESSAGE); - - TestValidDataForOneofType(FieldDescriptor::TYPE_UINT32); - TestValidDataForOneofType(FieldDescriptor::TYPE_BOOL); - TestValidDataForOneofType(FieldDescriptor::TYPE_UINT64); - TestValidDataForOneofType(FieldDescriptor::TYPE_FLOAT); - TestValidDataForOneofType(FieldDescriptor::TYPE_DOUBLE); - TestValidDataForOneofType(FieldDescriptor::TYPE_STRING); - TestValidDataForOneofType(FieldDescriptor::TYPE_BYTES); - TestValidDataForOneofType(FieldDescriptor::TYPE_ENUM); - TestValidDataForOneofType(FieldDescriptor::TYPE_MESSAGE); - // Additional test to check merging oneof message. - TestMergeOneofMessage(); - - // TODO: - // TestValidDataForType(FieldDescriptor::TYPE_GROUP - - // Unknown fields. - // TODO: update this behavior when unknown field's behavior - // changed in open source. Also delete - // Required.Proto3.ProtobufInput.UnknownVarint.ProtobufOutput - // from failure list of python_cpp python java - TestUnknownMessage(); - TestUnknownOrdering(); - TestOneofMessage(); - - RunJsonTests(); - } - // Flag control performance tests to keep them internal and opt-in only - if (suite_.performance_) { - RunBinaryPerformanceTests(); - } -} - -template <typename MessageType> -void BinaryAndJsonConformanceSuiteImpl< - MessageType>::RunBinaryPerformanceTests() { - TestBinaryPerformanceForAlternatingUnknownFields(); - - TestBinaryPerformanceMergeMessageWithRepeatedFieldForType( - FieldDescriptor::TYPE_BOOL); - TestBinaryPerformanceMergeMessageWithRepeatedFieldForType( - FieldDescriptor::TYPE_DOUBLE); - TestBinaryPerformanceMergeMessageWithRepeatedFieldForType( - FieldDescriptor::TYPE_FLOAT); - TestBinaryPerformanceMergeMessageWithRepeatedFieldForType( - FieldDescriptor::TYPE_UINT32); - TestBinaryPerformanceMergeMessageWithRepeatedFieldForType( - FieldDescriptor::TYPE_UINT64); - TestBinaryPerformanceMergeMessageWithRepeatedFieldForType( - FieldDescriptor::TYPE_STRING); - TestBinaryPerformanceMergeMessageWithRepeatedFieldForType( - FieldDescriptor::TYPE_BYTES); - - TestBinaryPerformanceMergeMessageWithUnknownFieldForType( - FieldDescriptor::TYPE_BOOL); - TestBinaryPerformanceMergeMessageWithUnknownFieldForType( - FieldDescriptor::TYPE_DOUBLE); - TestBinaryPerformanceMergeMessageWithUnknownFieldForType( - FieldDescriptor::TYPE_FLOAT); - TestBinaryPerformanceMergeMessageWithUnknownFieldForType( - FieldDescriptor::TYPE_UINT32); - TestBinaryPerformanceMergeMessageWithUnknownFieldForType( - FieldDescriptor::TYPE_UINT64); - TestBinaryPerformanceMergeMessageWithUnknownFieldForType( - FieldDescriptor::TYPE_STRING); - TestBinaryPerformanceMergeMessageWithUnknownFieldForType( - FieldDescriptor::TYPE_BYTES); -} - -template <typename MessageType> -void BinaryAndJsonConformanceSuiteImpl<MessageType>::RunJsonTests() { - RunValidJsonTest("HelloWorld", REQUIRED, - "{\"optionalString\":\"Hello, World!\"}", - "optional_string: 'Hello, World!'"); - - RunJsonTestsForFieldNameConvention(); - RunJsonTestsForNonRepeatedTypes(); - RunJsonTestsForRepeatedTypes(); - RunJsonTestsForNullTypes(); - - if (run_proto3_tests_) { - RunJsonTestsForWrapperTypes(); - RunJsonTestsForFieldMask(); - RunJsonTestsForStruct(); - RunJsonTestsForValue(); - RunJsonTestsForAny(); - } else { - // Currently Proto2 only, but should also be run on Proto3-optional. - RunJsonTestsForStoresDefaultPrimitive(); - } - - RunJsonTestsForUnknownEnumStringValues(); - - RunValidJsonIgnoreUnknownTest("IgnoreUnknownJsonNumber", REQUIRED, - R"({"unknown": 1})", ""); - RunValidJsonIgnoreUnknownTest("IgnoreUnknownJsonString", REQUIRED, - R"({"unknown": "a"})", ""); - RunValidJsonIgnoreUnknownTest("IgnoreUnknownJsonTrue", REQUIRED, - R"({"unknown": true})", ""); - RunValidJsonIgnoreUnknownTest("IgnoreUnknownJsonFalse", REQUIRED, - R"({"unknown": false})", ""); - RunValidJsonIgnoreUnknownTest("IgnoreUnknownJsonNull", REQUIRED, - R"({"unknown": null})", ""); - RunValidJsonIgnoreUnknownTest("IgnoreUnknownJsonObject", REQUIRED, - R"({"unknown": {"a": 1}})", ""); - - ExpectParseFailureForJson("RejectTopLevelNull", REQUIRED, "null"); -} - -template <typename MessageType> -void BinaryAndJsonConformanceSuiteImpl< - MessageType>::RunJsonTestsForStoresDefaultPrimitive() { - RunValidJsonTestWithValidator( - "StoresDefaultPrimitive", REQUIRED, - R"({ - "FieldName13": 0 - })", - [](const Json::Value& value) { return value.isMember("FieldName13"); }); - std::vector<const FieldDescriptor*> extensions; - MessageType::GetDescriptor()->file()->pool()->FindAllExtensions( - MessageType::GetDescriptor(), &extensions); - RunValidJsonTestWithValidator("FieldNameExtension", RECOMMENDED, - absl::Substitute(R"({ - "[$0]": 1 - })", - extensions[0]->full_name()), - [&](const Json::Value& value) { - return value.isMember(absl::StrCat( - "[", extensions[0]->full_name(), "]")); - }); -} - -template <typename MessageType> -void BinaryAndJsonConformanceSuiteImpl< - MessageType>::RunJsonTestsForReservedFields() { - for (const auto& test_case : std::vector<std::pair<std::string, std::string>>{ - {"Boolean", "true"}, - {"Number", "1"}, - {"String", "\"hello\""}, - {"Message", R"json({ "a": 1 })json"}, - }) { - ExpectParseFailureForJson( - absl::StrCat("RejectReservedFieldName.", test_case.first), REQUIRED, - absl::Substitute(R"json({ - "reserved_field": $0 - })json", - test_case.second)); - } -} - -template <typename MessageType> -void BinaryAndJsonConformanceSuiteImpl< - MessageType>::RunJsonTestsForUnknownEnumStringValues() { - // Tests the handling of unknown enum values when encoded as string labels. - // The expected behavior depends on whether unknown fields are ignored: - // * when ignored, the parser should ignore the unknown enum string value. - // * when not ignored, the parser should fail. - struct TestCase { - // Used in the test name. - std::string enum_location; - // JSON input which will contain the unknown field. - std::string input_json; - }; - const std::vector<TestCase> test_cases = { - {"InOptionalField", R"json({ - "optional_nested_enum": "UNKNOWN_ENUM_VALUE" - })json"}, - {"InRepeatedField", R"json({ - "repeated_nested_enum": ["UNKNOWN_ENUM_VALUE"] - })json"}, - {"InMapValue", R"json({ - "map_string_nested_enum": {"key": "UNKNOWN_ENUM_VALUE"} - })json"}, - }; - for (const TestCase& test_case : test_cases) { - // Unknown enum string value is a parse failure when not ignoring unknown - // fields. - ExpectParseFailureForJson( - absl::StrCat("RejectUnknownEnumStringValue", test_case.enum_location), - RECOMMENDED, test_case.input_json); - // Unknown enum string value is ignored when ignoring unknown fields. - RunValidJsonIgnoreUnknownTest( - absl::StrCat("IgnoreUnknownEnumStringValue", test_case.enum_location), - RECOMMENDED, test_case.input_json, ""); - } - - // This test is similar to "InRepeatedField" from above, but it highlights the - // potentially unexpected behavior in an array with mixed known and unknown - // enum string values. - RunValidJsonIgnoreUnknownTest("IgnoreUnknownEnumStringValueInRepeatedPart", - RECOMMENDED, - R"json({ - "repeated_nested_enum": [ - "FOO", - "UNKNOWN_ENUM_VALUE", - "FOO" - ]})json", - R"( - repeated_nested_enum: FOO - repeated_nested_enum: FOO - )"); - - // This test is similar to "InMapValue" from above with mixture of known and - // unknown enum string values in the map. - RunValidJsonIgnoreUnknownTest("IgnoreUnknownEnumStringValueInMapPart", - RECOMMENDED, - R"json({ - "map_string_nested_enum": { - "key1": "FOO", - "key2": "UNKNOWN_ENUM_VALUE" - }})json", - R"( - map_string_nested_enum: { - key: "key1" - value: FOO - } - )"); -} - -template <typename MessageType> -void BinaryAndJsonConformanceSuiteImpl< - MessageType>::RunJsonTestsForFieldNameConvention() { - RunValidJsonTest("FieldNameInSnakeCase", REQUIRED, - R"({ - "fieldname1": 1, - "fieldName2": 2, - "FieldName3": 3, - "fieldName4": 4 - })", - R"( - fieldname1: 1 - field_name2: 2 - _field_name3: 3 - field__name4_: 4 - )"); - RunValidJsonTest("FieldNameWithNumbers", REQUIRED, - R"({ - "field0name5": 5, - "field0Name6": 6 - })", - R"( - field0name5: 5 - field_0_name6: 6 - )"); - RunValidJsonTest("FieldNameWithMixedCases", REQUIRED, - R"({ - "fieldName7": 7, - "FieldName8": 8, - "fieldName9": 9, - "FieldName10": 10, - "FIELDNAME11": 11, - "FIELDName12": 12 - })", - R"( - fieldName7: 7 - FieldName8: 8 - field_Name9: 9 - Field_Name10: 10 - FIELD_NAME11: 11 - FIELD_name12: 12 - )"); - RunValidJsonTest("FieldNameWithDoubleUnderscores", RECOMMENDED, - R"({ - "FieldName13": 13, - "FieldName14": 14, - "fieldName15": 15, - "fieldName16": 16, - "fieldName17": 17, - "FieldName18": 18 - })", - R"( - __field_name13: 13 - __Field_name14: 14 - field__name15: 15 - field__Name16: 16 - field_name17__: 17 - Field_name18__: 18 - )"); - // Using the original proto field name in JSON is also allowed. - RunValidJsonTest("OriginalProtoFieldName", REQUIRED, - R"({ - "fieldname1": 1, - "field_name2": 2, - "_field_name3": 3, - "field__name4_": 4, - "field0name5": 5, - "field_0_name6": 6, - "fieldName7": 7, - "FieldName8": 8, - "field_Name9": 9, - "Field_Name10": 10, - "FIELD_NAME11": 11, - "FIELD_name12": 12, - "__field_name13": 13, - "__Field_name14": 14, - "field__name15": 15, - "field__Name16": 16, - "field_name17__": 17, - "Field_name18__": 18 - })", - R"( - fieldname1: 1 - field_name2: 2 - _field_name3: 3 - field__name4_: 4 - field0name5: 5 - field_0_name6: 6 - fieldName7: 7 - FieldName8: 8 - field_Name9: 9 - Field_Name10: 10 - FIELD_NAME11: 11 - FIELD_name12: 12 - __field_name13: 13 - __Field_name14: 14 - field__name15: 15 - field__Name16: 16 - field_name17__: 17 - Field_name18__: 18 - )"); - // Field names can be escaped. - RunValidJsonTest("FieldNameEscaped", REQUIRED, R"({"fieldn\u0061me1": 1})", - "fieldname1: 1"); - // String ends with escape character. - ExpectParseFailureForJson("StringEndsWithEscapeChar", RECOMMENDED, - "{\"optionalString\": \"abc\\"); - // Field names must be quoted (or it's not valid JSON). - ExpectParseFailureForJson("FieldNameNotQuoted", RECOMMENDED, - "{fieldname1: 1}"); - // Trailing comma is not allowed (not valid JSON). - ExpectParseFailureForJson("TrailingCommaInAnObject", RECOMMENDED, - R"({"fieldname1":1,})"); - ExpectParseFailureForJson("TrailingCommaInAnObjectWithSpace", RECOMMENDED, - R"({"fieldname1":1 ,})"); - ExpectParseFailureForJson("TrailingCommaInAnObjectWithSpaceCommaSpace", - RECOMMENDED, R"({"fieldname1":1 , })"); - ExpectParseFailureForJson("TrailingCommaInAnObjectWithNewlines", RECOMMENDED, - R"({ - "fieldname1":1, - })"); - // JSON doesn't support comments. - ExpectParseFailureForJson("JsonWithComments", RECOMMENDED, - R"({ - // This is a comment. - "fieldname1": 1 - })"); - // JSON spec says whitespace doesn't matter, so try a few spacings to be sure. - RunValidJsonTest("OneLineNoSpaces", RECOMMENDED, - "{\"optionalInt32\":1,\"optionalInt64\":2}", - R"( - optional_int32: 1 - optional_int64: 2 - )"); - RunValidJsonTest("OneLineWithSpaces", RECOMMENDED, - "{ \"optionalInt32\" : 1 , \"optionalInt64\" : 2 }", - R"( - optional_int32: 1 - optional_int64: 2 - )"); - RunValidJsonTest("MultilineNoSpaces", RECOMMENDED, - "{\n\"optionalInt32\"\n:\n1\n,\n\"optionalInt64\"\n:\n2\n}", - R"( - optional_int32: 1 - optional_int64: 2 - )"); - RunValidJsonTest( - "MultilineWithSpaces", RECOMMENDED, - "{\n \"optionalInt32\" : 1\n ,\n \"optionalInt64\" : 2\n}\n", - R"( - optional_int32: 1 - optional_int64: 2 - )"); - // Missing comma between key/value pairs. - ExpectParseFailureForJson("MissingCommaOneLine", RECOMMENDED, - "{ \"optionalInt32\": 1 \"optionalInt64\": 2 }"); - ExpectParseFailureForJson( - "MissingCommaMultiline", RECOMMENDED, - "{\n \"optionalInt32\": 1\n \"optionalInt64\": 2\n}"); - // Duplicated field names have either last-wins or parse failure. - RunValidJsonTestOrParseFailure("FieldNameDuplicate", RECOMMENDED, - R"({ - "optionalNestedMessage": {"a": 1}, - "optionalNestedMessage": {} - })", - "optional_nested_message: {}"); - RunValidJsonTestOrParseFailure("FieldNameDuplicateDifferentCasing1", - RECOMMENDED, - R"({ - "optional_nested_message": {"a": 1}, - "optionalNestedMessage": {} - })", - "optional_nested_message: {}"); - RunValidJsonTestOrParseFailure("FieldNameDuplicateDifferentCasing2", - RECOMMENDED, - R"({ - "optionalNestedMessage": {"a": 1}, - "optional_nested_message": {} - })", - "optional_nested_message: {}"); - // Serializers should use lowerCamelCase by default. - RunValidJsonTestWithValidator("FieldNameInLowerCamelCase", REQUIRED, - R"({ - "fieldname1": 1, - "fieldName2": 2, - "FieldName3": 3, - "fieldName4": 4 - })", - [](const Json::Value& value) { - return value.isMember("fieldname1") && - value.isMember("fieldName2") && - value.isMember("FieldName3") && - value.isMember("fieldName4"); - }); - RunValidJsonTestWithValidator("FieldNameWithNumbersValidator", REQUIRED, - R"({ - "field0name5": 5, - "field0Name6": 6 - })", - [](const Json::Value& value) { - return value.isMember("field0name5") && - value.isMember("field0Name6"); - }); - RunValidJsonTestWithValidator( - "FieldNameWithMixedCasesValidator", REQUIRED, - R"({ - "fieldName7": 7, - "FieldName8": 8, - "fieldName9": 9, - "FieldName10": 10, - "FIELDNAME11": 11, - "FIELDName12": 12 - })", - [](const Json::Value& value) { - return value.isMember("fieldName7") && value.isMember("FieldName8") && - value.isMember("fieldName9") && value.isMember("FieldName10") && - value.isMember("FIELDNAME11") && value.isMember("FIELDName12"); - }); - RunValidJsonTestWithValidator( - "FieldNameWithDoubleUnderscoresValidator", RECOMMENDED, - R"({ - "FieldName13": 13, - "FieldName14": 14, - "fieldName15": 15, - "fieldName16": 16, - "fieldName17": 17, - "FieldName18": 18 - })", - [](const Json::Value& value) { - return value.isMember("FieldName13") && value.isMember("FieldName14") && - value.isMember("fieldName15") && value.isMember("fieldName16") && - value.isMember("fieldName17") && value.isMember("FieldName18"); - }); - - if (run_proto3_tests_) { - RunValidJsonTestWithValidator("SkipsDefaultPrimitive", REQUIRED, - R"({"FieldName13": 0})", - [](const Json::Value& value) { - return !value.isMember("FieldName13"); - }); - } -} - -template <typename MessageType> -void BinaryAndJsonConformanceSuiteImpl< - MessageType>::RunJsonTestsForNonRepeatedTypes() { - // Integer fields. - RunValidJsonTest("Int32FieldMaxValue", REQUIRED, - R"({"optionalInt32": 2147483647})", - "optional_int32: 2147483647"); - RunValidJsonTest("Int32FieldMinValue", REQUIRED, - R"({"optionalInt32": -2147483648})", - "optional_int32: -2147483648"); - RunValidJsonTest("Uint32FieldMaxValue", REQUIRED, - R"({"optionalUint32": 4294967295})", - "optional_uint32: 4294967295"); - RunValidJsonTest("Int64FieldMaxValue", REQUIRED, - R"({"optionalInt64": "9223372036854775807"})", - "optional_int64: 9223372036854775807"); - RunValidJsonTest("Int64FieldMinValue", REQUIRED, - R"({"optionalInt64": "-9223372036854775808"})", - "optional_int64: -9223372036854775808"); - RunValidJsonTest("Uint64FieldMaxValue", REQUIRED, - R"({"optionalUint64": "18446744073709551615"})", - "optional_uint64: 18446744073709551615"); - // While not the largest Int64, this is the largest - // Int64 which can be exactly represented within an - // IEEE-754 64-bit float, which is the expected level - // of interoperability guarantee. Larger values may - // work in some implementations, but should not be - // relied upon. - RunValidJsonTest("Int64FieldMaxValueNotQuoted", REQUIRED, - R"({"optionalInt64": 9223372036854774784})", - "optional_int64: 9223372036854774784"); - RunValidJsonTest("Int64FieldMinValueNotQuoted", REQUIRED, - R"({"optionalInt64": -9223372036854775808})", - "optional_int64: -9223372036854775808"); - // Largest interoperable Uint64; see comment above - // for Int64FieldMaxValueNotQuoted. - RunValidJsonTest("Uint64FieldMaxValueNotQuoted", REQUIRED, - R"({"optionalUint64": 18446744073709549568})", - "optional_uint64: 18446744073709549568"); - // Values can be represented as JSON strings. - RunValidJsonTest("Int32FieldStringValue", REQUIRED, - R"({"optionalInt32": "2147483647"})", - "optional_int32: 2147483647"); - RunValidJsonTest("Int32FieldStringValueEscaped", REQUIRED, - R"({"optionalInt32": "2\u003147483647"})", - "optional_int32: 2147483647"); - RunValidJsonTest("Int32FieldStringValueZero", REQUIRED, - R"({"optionalInt32": "0"})", "optional_int32: 0"); - RunValidJsonTest("Int32FieldQuotedExponentialValue", REQUIRED, - R"({"optionalInt32": "1e5"})", "optional_int32: 100000"); - - // Parsers reject out-of-bound integer values. - ExpectParseFailureForJson("Int32FieldTooLarge", REQUIRED, - R"({"optionalInt32": 2147483648})"); - ExpectParseFailureForJson("Int32FieldTooSmall", REQUIRED, - R"({"optionalInt32": -2147483649})"); - ExpectParseFailureForJson("Uint32FieldTooLarge", REQUIRED, - R"({"optionalUint32": 4294967296})"); - ExpectParseFailureForJson("Int64FieldTooLarge", REQUIRED, - R"({"optionalInt64": "9223372036854775808"})"); - ExpectParseFailureForJson("Int64FieldTooSmall", REQUIRED, - R"({"optionalInt64": "-9223372036854775809"})"); - ExpectParseFailureForJson("Uint64FieldTooLarge", REQUIRED, - R"({"optionalUint64": "18446744073709551616"})"); - ExpectParseFailureForJson("Uint64QuotedExponentFieldTooLarge", REQUIRED, - R"({"optionalUint64": "1e536870000"})"); - - // Parser reject non-integer numeric values. - ExpectParseFailureForJson("Int32FieldNotInteger", REQUIRED, - R"({"optionalInt32": 0.5})"); - ExpectParseFailureForJson("Uint32FieldNotInteger", REQUIRED, - R"({"optionalUint32": 0.5})"); - ExpectParseFailureForJson("Int64FieldNotInteger", REQUIRED, - R"({"optionalInt64": "0.5"})"); - ExpectParseFailureForJson("Uint64FieldNotInteger", REQUIRED, - R"({"optionalUint64": "0.5"})"); - - // Parser reject boolean values for integer fields. - ExpectParseFailureForJson("Int32FieldTrueValue", REQUIRED, - R"({"optionalInt32": true})"); - ExpectParseFailureForJson("Int32FieldFalseValue", REQUIRED, - R"({"optionalInt32": false})"); - - // Parser reject non-numeric string values. - ExpectParseFailureForJson("Int32FieldStringValuePartiallyNumeric", REQUIRED, - R"({"optionalInt32": "12abc"})"); - ExpectParseFailureForJson("Int32FieldStringValuePartiallyNumericSpace", - REQUIRED, R"({"optionalInt32": "12 34"})"); - ExpectParseFailureForJson("Int32FieldStringValuePartiallyNumericComma", - REQUIRED, R"({"optionalInt32": "12,34"})"); - // Not a raw string literal: the payload holds the UTF-8 bytes of U+8C37 - // U+6B4C between the digits. - ExpectParseFailureForJson("Int32FieldStringValuePartiallyNumericUnicode", - REQUIRED, - "{\"optionalInt32\": \"12\xE8\xB0\xB7\xE6\xAD\x8C" - "34\"}"); - ExpectParseFailureForJson("Int32FieldStringValueNonNumeric", REQUIRED, - R"({"optionalInt32": "abc"})"); - - // Only ASCII digits 0-9 are digits; other Unicode decimal digits (Nd) are - // rejected. Not raw string literals: the payloads hold UTF-8 bytes of - // Arabic-Indic digits (U+0660-U+0669) and fullwidth digits (U+FF10-U+FF19). - // "١٢٣" - ExpectParseFailureForJson( - "Int32FieldStringValueArabicIndicDigits", REQUIRED, - "{\"optionalInt32\": \"\xD9\xA1\xD9\xA2\xD9\xA3\"}"); - ExpectParseFailureForJson( - "Int64FieldStringValueFullwidthDigits", REQUIRED, - "{\"optionalInt64\": \"\xEF\xBC\x91\xEF\xBC\x92\xEF\xBC\x93\"}"); - - // Parser reject empty string values. - ExpectParseFailureForJson("Int32FieldEmptyString", REQUIRED, - R"({"optionalInt32": ""})"); - ExpectParseFailureForJson("Uint32FieldEmptyString", REQUIRED, - R"({"optionalUint32": ""})"); - ExpectParseFailureForJson("Int64FieldEmptyString", REQUIRED, - R"({"optionalInt64": ""})"); - ExpectParseFailureForJson("Uint64FieldEmptyString", REQUIRED, - R"({"optionalUint64": ""})"); - - // Integers but represented as float values are accepted. - RunValidJsonTest("Int32FieldFloatTrailingZero", REQUIRED, - R"({"optionalInt32": 100000.000})", - "optional_int32: 100000"); - RunValidJsonTest("Int32FieldExponentialFormat", REQUIRED, - R"({"optionalInt32": 1e5})", "optional_int32: 100000"); - RunValidJsonTest("Int32FieldMaxFloatValue", REQUIRED, - R"({"optionalInt32": 2.147483647e9})", - "optional_int32: 2147483647"); - RunValidJsonTest("Int32FieldMinFloatValue", REQUIRED, - R"({"optionalInt32": -2.147483648e9})", - "optional_int32: -2147483648"); - RunValidJsonTest("Uint32FieldMaxFloatValue", REQUIRED, - R"({"optionalUint32": 4.294967295e9})", - "optional_uint32: 4294967295"); - - // Parser reject non-numeric values. - ExpectParseFailureForJson("Int32FieldNotNumber", REQUIRED, - R"({"optionalInt32": "3x3"})"); - ExpectParseFailureForJson("Uint32FieldNotNumber", REQUIRED, - R"({"optionalUint32": "3x3"})"); - ExpectParseFailureForJson("Int64FieldNotNumber", REQUIRED, - R"({"optionalInt64": "3x3"})"); - ExpectParseFailureForJson("Uint64FieldNotNumber", REQUIRED, - R"({"optionalUint64": "3x3"})"); - // JSON does not allow "+" on numeric values. - ExpectParseFailureForJson("Int32FieldPlusSign", REQUIRED, - R"({"optionalInt32": +1})"); - // JSON doesn't allow leading 0s. - ExpectParseFailureForJson("Int32FieldLeadingZero", REQUIRED, - R"({"optionalInt32": 01})"); - ExpectParseFailureForJson("Int32FieldNegativeWithLeadingZero", REQUIRED, - R"({"optionalInt32": -01})"); - // String values must follow the same syntax rule. Specifically leading - // or trailing spaces are not allowed. - ExpectParseFailureForJson("Int32FieldLeadingSpace", REQUIRED, - R"({"optionalInt32": " 1"})"); - ExpectParseFailureForJson("Int32FieldTrailingSpace", REQUIRED, - R"({"optionalInt32": "1 "})"); - - // 64-bit values are serialized as strings. - RunValidJsonTestWithValidator( - "Int64FieldBeString", RECOMMENDED, R"({"optionalInt64": 1})", - [](const Json::Value& value) { - return value["optionalInt64"].type() == Json::stringValue && - value["optionalInt64"].asString() == "1"; - }); - RunValidJsonTestWithValidator( - "Uint64FieldBeString", RECOMMENDED, R"({"optionalUint64": 1})", - [](const Json::Value& value) { - return value["optionalUint64"].type() == Json::stringValue && - value["optionalUint64"].asString() == "1"; - }); - - // Bool fields. - RunValidJsonTest("BoolFieldTrue", REQUIRED, R"({"optionalBool":true})", - "optional_bool: true"); - RunValidJsonTest("BoolFieldFalse", REQUIRED, R"({"optionalBool":false})", - "optional_bool: false"); - - // Other forms are not allowed. - ExpectParseFailureForJson("BoolFieldIntegerZero", RECOMMENDED, - R"({"optionalBool":0})"); - ExpectParseFailureForJson("BoolFieldIntegerOne", RECOMMENDED, - R"({"optionalBool":1})"); - ExpectParseFailureForJson("BoolFieldCamelCaseTrue", RECOMMENDED, - R"({"optionalBool":True})"); - ExpectParseFailureForJson("BoolFieldCamelCaseFalse", RECOMMENDED, - R"({"optionalBool":False})"); - ExpectParseFailureForJson("BoolFieldAllCapitalTrue", RECOMMENDED, - R"({"optionalBool":TRUE})"); - ExpectParseFailureForJson("BoolFieldAllCapitalFalse", RECOMMENDED, - R"({"optionalBool":FALSE})"); - ExpectParseFailureForJson("BoolFieldDoubleQuotedTrue", RECOMMENDED, - R"({"optionalBool":"true"})"); - ExpectParseFailureForJson("BoolFieldDoubleQuotedFalse", RECOMMENDED, - R"({"optionalBool":"false"})"); - - // Float fields. - RunValidJsonTest("FloatFieldMinPositiveValue", REQUIRED, - R"({"optionalFloat": 1.175494e-38})", - "optional_float: 1.175494e-38"); - RunValidJsonTest("FloatFieldMaxNegativeValue", REQUIRED, - R"({"optionalFloat": -1.175494e-38})", - "optional_float: -1.175494e-38"); - RunValidJsonTest("FloatFieldMaxPositiveValue", REQUIRED, - R"({"optionalFloat": 3.402823e+38})", - "optional_float: 3.402823e+38"); - RunValidJsonTest("FloatFieldMinNegativeValue", REQUIRED, - R"({"optionalFloat": 3.402823e+38})", - "optional_float: 3.402823e+38"); - // Values can be quoted. - RunValidJsonTest("FloatFieldQuotedValue", REQUIRED, - R"({"optionalFloat": "1"})", "optional_float: 1"); - RunValidJsonTest("FloatFieldQuotedExponentialValue", REQUIRED, - R"({"optionalFloat": "1.175494e-38"})", - "optional_float: 1.175494e-38"); - // Special values. - RunValidJsonTest("FloatFieldNan", REQUIRED, R"({"optionalFloat": "NaN"})", - "optional_float: nan"); - RunValidJsonTest("FloatFieldInfinity", REQUIRED, - R"({"optionalFloat": "Infinity"})", "optional_float: inf"); - RunValidJsonTest("FloatFieldNegativeInfinity", REQUIRED, - R"({"optionalFloat": "-Infinity"})", "optional_float: -inf"); - // Non-canonical Nan will be correctly normalized. - { - MessageType message; - // IEEE floating-point standard 32-bit quiet NaN: - // 0111 1111 1xxx xxxx xxxx xxxx xxxx xxxx - message.set_optional_float(WireFormatLite::DecodeFloat(0x7FA12345)); - RunValidJsonTestWithProtobufInput("FloatFieldNormalizeQuietNan", REQUIRED, - message, "optional_float: nan"); - // IEEE floating-point standard 64-bit signaling NaN: - // 1111 1111 1xxx xxxx xxxx xxxx xxxx xxxx - message.set_optional_float(WireFormatLite::DecodeFloat(0xFFB54321)); - RunValidJsonTestWithProtobufInput("FloatFieldNormalizeSignalingNan", - REQUIRED, message, "optional_float: nan"); - } - - // Special values must be quoted. - ExpectParseFailureForJson("FloatFieldNanNotQuoted", RECOMMENDED, - R"({"optionalFloat": NaN})"); - ExpectParseFailureForJson("FloatFieldInfinityNotQuoted", RECOMMENDED, - R"({"optionalFloat": Infinity})"); - ExpectParseFailureForJson("FloatFieldNegativeInfinityNotQuoted", RECOMMENDED, - R"({"optionalFloat": -Infinity})"); - - // Parsers should reject out-of-bound values. - ExpectParseFailureForJson("FloatFieldTooSmall", REQUIRED, - R"({"optionalFloat": -3.502823e+38})"); - ExpectParseFailureForJson("FloatFieldTooLarge", REQUIRED, - R"({"optionalFloat": 3.502823e+38})"); - - // Parsers should reject empty string values. - ExpectParseFailureForJson("FloatFieldEmptyString", REQUIRED, - R"({"optionalFloat": ""})"); - - // Parser reject non-numeric string values. - ExpectParseFailureForJson("FloatFieldStringValuePartiallyNumeric", REQUIRED, - R"({"optionalFloat": "12abc"})"); - ExpectParseFailureForJson("FloatFieldStringValueNonNumeric", REQUIRED, - R"({"optionalFloat": "abc"})"); - ExpectParseFailureForJson("FloatFieldStringValuePartiallyNumericSpace", - REQUIRED, R"({"optionalFloat": "12 34"})"); - ExpectParseFailureForJson("FloatFieldStringValuePartiallyNumericComma", - REQUIRED, R"({"optionalFloat": "12,34"})"); - // Not a raw string literal: the payload holds the UTF-8 bytes of U+8C37 - // U+6B4C between the digits. - ExpectParseFailureForJson("FloatFieldStringValuePartiallyNumericUnicode", - REQUIRED, - "{\"optionalFloat\": \"12\xE8\xB0\xB7\xE6\xAD\x8C" - "34\"}"); - // Non-ASCII Unicode decimal digits: "١٢٣" (Arabic-Indic) and "123" - // (fullwidth). - ExpectParseFailureForJson( - "FloatFieldStringValueArabicIndicDigits", REQUIRED, - "{\"optionalFloat\": \"\xD9\xA1\xD9\xA2\xD9\xA3\"}"); - ExpectParseFailureForJson( - "FloatFieldStringValueFullwidthDigits", REQUIRED, - "{\"optionalFloat\": \"\xEF\xBC\x91\xEF\xBC\x92\xEF\xBC\x93\"}"); - - // Parser reject boolean values for float fields. - ExpectParseFailureForJson("FloatFieldTrueValue", REQUIRED, - R"({"optionalFloat": true})"); - ExpectParseFailureForJson("FloatFieldFalseValue", REQUIRED, - R"({"optionalFloat": false})"); - - // Double fields. - RunValidJsonTest("DoubleFieldMinPositiveValue", REQUIRED, - R"({"optionalDouble": 2.22507e-308})", - "optional_double: 2.22507e-308"); - RunValidJsonTest("DoubleFieldMaxNegativeValue", REQUIRED, - R"({"optionalDouble": -2.22507e-308})", - "optional_double: -2.22507e-308"); - RunValidJsonTest("DoubleFieldMaxPositiveValue", REQUIRED, - R"({"optionalDouble": 1.79769e+308})", - "optional_double: 1.79769e+308"); - RunValidJsonTest("DoubleFieldMinNegativeValue", REQUIRED, - R"({"optionalDouble": -1.79769e+308})", - "optional_double: -1.79769e+308"); - // Values can be quoted. - RunValidJsonTest("DoubleFieldQuotedValue", REQUIRED, - R"({"optionalDouble": "1"})", "optional_double: 1"); - RunValidJsonTest("DoubleFieldQuotedExponentialValue", REQUIRED, - R"({"optionalDouble": "2.22507e-308"})", - "optional_double: 2.22507e-308"); - // Special values. - RunValidJsonTest("DoubleFieldNan", REQUIRED, R"({"optionalDouble": "NaN"})", - "optional_double: nan"); - RunValidJsonTest("DoubleFieldInfinity", REQUIRED, - R"({"optionalDouble": "Infinity"})", "optional_double: inf"); - RunValidJsonTest("DoubleFieldNegativeInfinity", REQUIRED, - R"({"optionalDouble": "-Infinity"})", - "optional_double: -inf"); - // Non-canonical Nan will be correctly normalized. - { - MessageType message; - message.set_optional_double( - WireFormatLite::DecodeDouble(int64_t{0x7FFA123456789ABC})); - RunValidJsonTestWithProtobufInput("DoubleFieldNormalizeQuietNan", REQUIRED, - message, "optional_double: nan"); - message.set_optional_double( - WireFormatLite::DecodeDouble(uint64_t{0xFFFBCBA987654321})); - RunValidJsonTestWithProtobufInput("DoubleFieldNormalizeSignalingNan", - REQUIRED, message, - "optional_double: nan"); - } - - // Special values must be quoted. - ExpectParseFailureForJson("DoubleFieldNanNotQuoted", RECOMMENDED, - R"({"optionalDouble": NaN})"); - ExpectParseFailureForJson("DoubleFieldInfinityNotQuoted", RECOMMENDED, - R"({"optionalDouble": Infinity})"); - ExpectParseFailureForJson("DoubleFieldNegativeInfinityNotQuoted", RECOMMENDED, - R"({"optionalDouble": -Infinity})"); - - // Parsers should reject out-of-bound values. - ExpectParseFailureForJson("DoubleFieldTooSmall", REQUIRED, - R"({"optionalDouble": -1.89769e+308})"); - ExpectParseFailureForJson("DoubleFieldTooLarge", REQUIRED, - R"({"optionalDouble": 1.89769e+308})"); - - // Parsers should reject empty string values. - ExpectParseFailureForJson("DoubleFieldEmptyString", REQUIRED, - R"({"optionalDouble": ""})"); - - // Parser reject non-numeric string values. - ExpectParseFailureForJson("DoubleFieldStringValuePartiallyNumeric", REQUIRED, - R"({"optionalDouble": "12abc"})"); - ExpectParseFailureForJson("DoubleFieldStringValueNonNumeric", REQUIRED, - R"({"optionalDouble": "abc"})"); - // Non-ASCII Unicode decimal digits: "١٢٣" (Arabic-Indic) and "123" - // (fullwidth). - ExpectParseFailureForJson( - "DoubleFieldStringValueArabicIndicDigits", REQUIRED, - "{\"optionalDouble\": \"\xD9\xA1\xD9\xA2\xD9\xA3\"}"); - ExpectParseFailureForJson( - "DoubleFieldStringValueFullwidthDigits", REQUIRED, - "{\"optionalDouble\": \"\xEF\xBC\x91\xEF\xBC\x92\xEF\xBC\x93\"}"); - - // Parser reject boolean values for double fields. - ExpectParseFailureForJson("DoubleFieldTrueValue", REQUIRED, - R"({"optionalDouble": true})"); - ExpectParseFailureForJson("DoubleFieldFalseValue", REQUIRED, - R"({"optionalDouble": false})"); - - // Enum fields. - RunValidJsonTest("EnumField", REQUIRED, R"({"optionalNestedEnum": "FOO"})", - "optional_nested_enum: FOO"); - - // Enum fields with alias - if (run_proto3_tests_) { - RunValidJsonTest("EnumFieldWithAlias", REQUIRED, - R"({"optionalAliasedEnum": "ALIAS_BAZ"})", - "optional_aliased_enum: ALIAS_BAZ"); - RunValidJsonTest("EnumFieldWithAliasUseAlias", REQUIRED, - R"({"optionalAliasedEnum": "MOO"})", - "optional_aliased_enum: ALIAS_BAZ"); - RunValidJsonTest("EnumFieldWithAliasLowerCase", REQUIRED, - R"({"optionalAliasedEnum": "moo"})", - "optional_aliased_enum: ALIAS_BAZ"); - RunValidJsonTest("EnumFieldWithAliasDifferentCase", REQUIRED, - R"({"optionalAliasedEnum": "bAz"})", - "optional_aliased_enum: ALIAS_BAZ"); - } - - // Enum values must be represented as strings. - ExpectParseFailureForJson("EnumFieldNotQuoted", REQUIRED, - R"({"optionalNestedEnum": FOO})"); - // Numeric values are allowed. - RunValidJsonTest("EnumFieldNumericValueZero", REQUIRED, - R"({"optionalNestedEnum": 0})", "optional_nested_enum: FOO"); - RunValidJsonTest("EnumFieldNumericValueNonZero", REQUIRED, - R"({"optionalNestedEnum": 1})", "optional_nested_enum: BAR"); - // Arrays are not allowed for non-repeated enum fields. - ExpectParseFailureForJson("EnumFieldSingleElementArrayEnumName", REQUIRED, - R"({"optionalNestedEnum": ["FOO"]})"); - ExpectParseFailureForJson("EnumFieldSingleElementArrayNumericValue", REQUIRED, - R"({"optionalNestedEnum": [2]})"); - - // Booleans are not allowed for enum fields. - ExpectParseFailureForJson("EnumFieldTrueValue", REQUIRED, - R"({"optionalNestedEnum": true})"); - ExpectParseFailureForJson("EnumFieldFalseValue", REQUIRED, - R"({"optionalNestedEnum": false})"); - - if (run_proto3_tests_) { - // Unknown enum values are represented as numeric values. - RunValidJsonTestWithValidator( - "EnumFieldUnknownValue", REQUIRED, R"({"optionalNestedEnum": 123})", - [](const Json::Value& value) { - return value["optionalNestedEnum"].type() == Json::intValue && - value["optionalNestedEnum"].asInt() == 123; - }); - } - - // String fields. - RunValidJsonTest("StringField", REQUIRED, - R"({"optionalString": "Hello world!"})", - R"(optional_string: "Hello world!")"); - // Non-ASCII characters in the JSON text itself, as opposed to the \uXXXX - // escapes tested below. Note that this is deliberately not a raw string - // literal: the compiler resolves the \x escapes, so the payload holds the - // UTF-8 bytes of U+8C37 U+6B4C ("Google" in Chinese). In the raw string - // literals below, the \u escapes reach the testee's JSON parser verbatim. - RunValidJsonTest("StringFieldUnicode", REQUIRED, - "{\"optionalString\": \"\xE8\xB0\xB7\xE6\xAD\x8C\"}", - R"(optional_string: "\xE8\xB0\xB7\xE6\xAD\x8C")"); - RunValidJsonTest("StringFieldEscape", REQUIRED, - R"({"optionalString": "\"\\\/\b\f\n\r\t"})", - R"(optional_string: "\"\\/\b\f\n\r\t")"); - RunValidJsonTest("StringFieldUnicodeEscape", REQUIRED, - R"({"optionalString": "\u8C37\u6B4C"})", - R"(optional_string: "\xE8\xB0\xB7\xE6\xAD\x8C")"); - RunValidJsonTest("StringFieldSurrogatePair", REQUIRED, - // U+1F601 (grinning face with smiling eyes). - R"({"optionalString": "\uD83D\uDE01"})", - R"(optional_string: "\xF0\x9F\x98\x81")"); - RunValidJsonTest("StringFieldEmbeddedNull", REQUIRED, - R"({"optionalString": "Hello\u0000world!"})", - R"(optional_string: "Hello\000world!")"); - - // Unicode escapes must start with "\u" (lowercase u). - ExpectParseFailureForJson("StringFieldUppercaseEscapeLetter", RECOMMENDED, - R"({"optionalString": "\U8C37\U6b4C"})"); - ExpectParseFailureForJson("StringFieldInvalidEscape", RECOMMENDED, - R"({"optionalString": "\uXXXX\u6B4C"})"); - ExpectParseFailureForJson("StringFieldUnterminatedEscape", RECOMMENDED, - R"({"optionalString": "\u8C3"})"); - ExpectParseFailureForJson("StringFieldUnpairedHighSurrogate", RECOMMENDED, - R"({"optionalString": "\uD800"})"); - ExpectParseFailureForJson("StringFieldUnpairedLowSurrogate", RECOMMENDED, - R"({"optionalString": "\uDC00"})"); - ExpectParseFailureForJson("StringFieldSurrogateInWrongOrder", RECOMMENDED, - R"({"optionalString": "\uDE01\uD83D"})"); - ExpectParseFailureForJson("StringFieldNotAString", REQUIRED, - R"({"optionalString": 12345})"); - - // Bytes fields. - RunValidJsonTest("BytesField", REQUIRED, R"({"optionalBytes": "AQI="})", - R"(optional_bytes: "\x01\x02")"); - RunValidJsonTest("BytesFieldBase64Url", RECOMMENDED, - R"({"optionalBytes": "-_"})", R"(optional_bytes: "\xfb")"); - - // Message fields. - RunValidJsonTest("MessageField", REQUIRED, - R"({"optionalNestedMessage": {"a": 1234}})", - "optional_nested_message: {a: 1234}"); - - // Oneof fields. - RunValidJsonTestOrParseFailure("OneofFieldDuplicate", REQUIRED, - R"({"oneofUint32": 1, "oneofString": "test"})", - "oneof_string: \"test\""); - RunValidJsonTestOrParseFailure("OneofFieldDuplicate2", REQUIRED, - R"({"oneofString": "test", "oneofUint32": 1})", - "oneof_uint32: 1"); - RunValidJsonTest("OneofFieldNullFirst", REQUIRED, - R"({"oneofUint32": null, "oneofString": "test"})", - "oneof_string: \"test\""); - RunValidJsonTest("OneofFieldNullSecond", REQUIRED, - R"({"oneofString": "test", "oneofUint32": null})", - "oneof_string: \"test\""); - RunValidJsonTest("OneofZeroUint32", RECOMMENDED, R"({"oneofUint32": 0})", - "oneof_uint32: 0"); - RunValidJsonTest("OneofZeroMessage", RECOMMENDED, - R"({"oneofNestedMessage": {}})", "oneof_nested_message: {}"); - RunValidJsonTest("OneofZeroString", RECOMMENDED, R"({"oneofString": ""})", - "oneof_string: \"\""); - RunValidJsonTest("OneofZeroBytes", RECOMMENDED, R"({"oneofBytes": ""})", - "oneof_bytes: \"\""); - RunValidJsonTest("OneofZeroBool", RECOMMENDED, R"({"oneofBool": false})", - "oneof_bool: false"); - RunValidJsonTest("OneofZeroUint64", RECOMMENDED, R"({"oneofUint64": 0})", - "oneof_uint64: 0"); - RunValidJsonTest("OneofZeroFloat", RECOMMENDED, R"({"oneofFloat": 0.0})", - "oneof_float: 0"); - RunValidJsonTest("OneofZeroDouble", RECOMMENDED, R"({"oneofDouble": 0.0})", - "oneof_double: 0"); - RunValidJsonTest("OneofZeroEnum", RECOMMENDED, R"({"oneofEnum":"FOO"})", - "oneof_enum: FOO"); - - // Map fields. - RunValidJsonTest("Int32MapField", REQUIRED, - R"({"mapInt32Int32": {"1": 2, "3": 4}})", - "map_int32_int32: {key: 1 value: 2}" - "map_int32_int32: {key: 3 value: 4}"); - ExpectParseFailureForJson("Int32MapFieldKeyNotQuoted", RECOMMENDED, - R"({"mapInt32Int32": {1: 2, 3: 4}})"); - RunValidJsonTest("Uint32MapField", REQUIRED, - R"({"mapUint32Uint32": {"1": 2, "3": 4}})", - "map_uint32_uint32: {key: 1 value: 2}" - "map_uint32_uint32: {key: 3 value: 4}"); - ExpectParseFailureForJson("Uint32MapFieldKeyNotQuoted", RECOMMENDED, - R"({"mapUint32Uint32": {1: 2, 3: 4}})"); - RunValidJsonTest("Int64MapField", REQUIRED, - R"({"mapInt64Int64": {"1": 2, "3": 4}})", - "map_int64_int64: {key: 1 value: 2}" - "map_int64_int64: {key: 3 value: 4}"); - ExpectParseFailureForJson("Int64MapFieldKeyNotQuoted", RECOMMENDED, - R"({"mapInt64Int64": {1: 2, 3: 4}})"); - RunValidJsonTest("Uint64MapField", REQUIRED, - R"({"mapUint64Uint64": {"1": 2, "3": 4}})", - "map_uint64_uint64: {key: 1 value: 2}" - "map_uint64_uint64: {key: 3 value: 4}"); - ExpectParseFailureForJson("Uint64MapFieldKeyNotQuoted", RECOMMENDED, - R"({"mapUint64Uint64": {1: 2, 3: 4}})"); - RunValidJsonTest("BoolMapField", REQUIRED, - R"({"mapBoolBool": {"true": true, "false": false}})", - "map_bool_bool: {key: true value: true}" - "map_bool_bool: {key: false value: false}"); - ExpectParseFailureForJson("BoolMapFieldKeyNotQuoted", RECOMMENDED, - R"({"mapBoolBool": {true: true, false: false}})"); - RunValidJsonTest("MessageMapField", REQUIRED, - R"({ - "mapStringNestedMessage": { - "hello": {"a": 1234}, - "world": {"a": 5678} - } - })", - R"( - map_string_nested_message: { - key: "hello" - value: {a: 1234} - } - map_string_nested_message: { - key: "world" - value: {a: 5678} - } - )"); - // Since Map keys are represented as JSON strings, escaping should be allowed. - RunValidJsonTest("Int32MapEscapedKey", REQUIRED, - R"({"mapInt32Int32": {"\u0031": 2}})", - "map_int32_int32: {key: 1 value: 2}"); - RunValidJsonTest("Int64MapEscapedKey", REQUIRED, - R"({"mapInt64Int64": {"\u0031": 2}})", - "map_int64_int64: {key: 1 value: 2}"); - RunValidJsonTest("BoolMapEscapedKey", REQUIRED, - R"({"mapBoolBool": {"tr\u0075e": true}})", - "map_bool_bool: {key: true value: true}"); - - // http://www.rfc-editor.org/rfc/rfc7159.txt says strings have to use double - // quotes. - ExpectParseFailureForJson("StringFieldSingleQuoteKey", RECOMMENDED, - R"({'optionalString': "Hello world!"})"); - ExpectParseFailureForJson("StringFieldSingleQuoteValue", RECOMMENDED, - R"({"optionalString": 'Hello world!'})"); - ExpectParseFailureForJson("StringFieldSingleQuoteBoth", RECOMMENDED, - R"({'optionalString': 'Hello world!'})"); -} - -template <typename MessageType> -void BinaryAndJsonConformanceSuiteImpl< - MessageType>::RunJsonTestsForRepeatedTypes() { - // Repeated fields. - RunValidJsonTest("PrimitiveRepeatedField", REQUIRED, - R"({"repeatedInt32": [1, 2, 3, 4]})", - "repeated_int32: [1, 2, 3, 4]"); - RunValidJsonTest("EnumRepeatedField", REQUIRED, - R"({"repeatedNestedEnum": ["FOO", "BAR", "BAZ"]})", - "repeated_nested_enum: [FOO, BAR, BAZ]"); - RunValidJsonTest("StringRepeatedField", REQUIRED, - R"({"repeatedString": ["Hello", "world"]})", - R"(repeated_string: ["Hello", "world"])"); - RunValidJsonTest("BytesRepeatedField", REQUIRED, - R"({"repeatedBytes": ["AAEC", "AQI="]})", - R"(repeated_bytes: ["\x00\x01\x02", "\x01\x02"])"); - RunValidJsonTest("MessageRepeatedField", REQUIRED, - R"({"repeatedNestedMessage": [{"a": 1234}, {"a": 5678}]})", - "repeated_nested_message: {a: 1234}" - "repeated_nested_message: {a: 5678}"); - - // Repeated field elements are of incorrect type. - ExpectParseFailureForJson( - "RepeatedFieldWrongElementTypeExpectingIntegersGotBool", REQUIRED, - R"({"repeatedInt32": [1, false, 3, 4]})"); - ExpectParseFailureForJson( - "RepeatedFieldWrongElementTypeExpectingIntegersGotString", REQUIRED, - R"({"repeatedInt32": [1, 2, "name", 4]})"); - ExpectParseFailureForJson( - "RepeatedFieldWrongElementTypeExpectingIntegersGotMessage", REQUIRED, - R"({"repeatedInt32": [1, 2, 3, {"a": 4}]})"); - ExpectParseFailureForJson( - "RepeatedFieldWrongElementTypeExpectingStringsGotInt", REQUIRED, - R"({"repeatedString": ["1", 2, "3", "4"]})"); - ExpectParseFailureForJson( - "RepeatedFieldWrongElementTypeExpectingStringsGotBool", REQUIRED, - R"({"repeatedString": ["1", "2", false, "4"]})"); - ExpectParseFailureForJson( - "RepeatedFieldWrongElementTypeExpectingStringsGotMessage", REQUIRED, - R"({"repeatedString": ["1", 2, "3", {"a": 4}]})"); - ExpectParseFailureForJson( - "RepeatedFieldWrongElementTypeExpectingMessagesGotInt", REQUIRED, - R"({"repeatedNestedMessage": [{"a": 1}, 2]})"); - ExpectParseFailureForJson( - "RepeatedFieldWrongElementTypeExpectingMessagesGotBool", REQUIRED, - R"({"repeatedNestedMessage": [{"a": 1}, false]})"); - ExpectParseFailureForJson( - "RepeatedFieldWrongElementTypeExpectingMessagesGotString", REQUIRED, - R"({"repeatedNestedMessage": [{"a": 1}, "2"]})"); - - // A singular field where a repeated field was expected is not allowed, even - // if it is the right type. - ExpectParseFailureForJson("SingleValueForRepeatedFieldInt32", REQUIRED, - R"({"repeatedInt32": 1})"); - ExpectParseFailureForJson("SingleValueForRepeatedFieldMessage", REQUIRED, - R"({"repeatedNestedMessage": {"a": 1}})"); - - // Trailing comma in the repeated field is not allowed. - ExpectParseFailureForJson("RepeatedFieldTrailingComma", RECOMMENDED, - R"({"repeatedInt32": [1, 2, 3, 4,]})"); - ExpectParseFailureForJson("RepeatedFieldTrailingCommaWithSpace", RECOMMENDED, - "{\"repeatedInt32\": [1, 2, 3, 4 ,]}"); - ExpectParseFailureForJson("RepeatedFieldTrailingCommaWithSpaceCommaSpace", - RECOMMENDED, - "{\"repeatedInt32\": [1, 2, 3, 4 , ]}"); - ExpectParseFailureForJson( - "RepeatedFieldTrailingCommaWithNewlines", RECOMMENDED, - "{\"repeatedInt32\": [\n 1,\n 2,\n 3,\n 4,\n]}"); -} - -template <typename MessageType> -void BinaryAndJsonConformanceSuiteImpl< - MessageType>::RunJsonTestsForNullTypes() { - // "null" is accepted for all fields types. - RunValidJsonTest("AllFieldAcceptNull", REQUIRED, - R"({ - "optionalInt32": null, - "optionalInt64": null, - "optionalUint32": null, - "optionalUint64": null, - "optionalSint32": null, - "optionalSint64": null, - "optionalFixed32": null, - "optionalFixed64": null, - "optionalSfixed32": null, - "optionalSfixed64": null, - "optionalFloat": null, - "optionalDouble": null, - "optionalBool": null, - "optionalString": null, - "optionalBytes": null, - "optionalNestedEnum": null, - "optionalNestedMessage": null, - "repeatedInt32": null, - "repeatedInt64": null, - "repeatedUint32": null, - "repeatedUint64": null, - "repeatedSint32": null, - "repeatedSint64": null, - "repeatedFixed32": null, - "repeatedFixed64": null, - "repeatedSfixed32": null, - "repeatedSfixed64": null, - "repeatedFloat": null, - "repeatedDouble": null, - "repeatedBool": null, - "repeatedString": null, - "repeatedBytes": null, - "repeatedNestedEnum": null, - "repeatedNestedMessage": null, - "mapInt32Int32": null, - "mapBoolBool": null, - "mapStringNestedMessage": null - })", - ""); - - // Repeated field elements cannot be null. - ExpectParseFailureForJson("RepeatedFieldPrimitiveElementIsNull", RECOMMENDED, - R"({"repeatedInt32": [1, null, 2]})"); - ExpectParseFailureForJson( - "RepeatedFieldMessageElementIsNull", RECOMMENDED, - R"({"repeatedNestedMessage": [{"a":1}, null, {"a":2}]})"); - // Map field keys cannot be null. - ExpectParseFailureForJson("MapFieldKeyIsNull", RECOMMENDED, - R"({"mapInt32Int32": {null: 1}})"); - // Map field values cannot be null. - ExpectParseFailureForJson("MapFieldValueIsNull", RECOMMENDED, - R"({"mapInt32Int32": {"0": null}})"); -} - -template <typename MessageType> -void BinaryAndJsonConformanceSuiteImpl< - MessageType>::RunJsonTestsForWrapperTypes() { - RunValidJsonTest("OptionalBoolWrapper", REQUIRED, - R"({"optionalBoolWrapper": false})", - "optional_bool_wrapper: {value: false}"); - RunValidJsonTest("OptionalInt32Wrapper", REQUIRED, - R"({"optionalInt32Wrapper": 0})", - "optional_int32_wrapper: {value: 0}"); - RunValidJsonTest("OptionalUint32Wrapper", REQUIRED, - R"({"optionalUint32Wrapper": 0})", - "optional_uint32_wrapper: {value: 0}"); - RunValidJsonTest("OptionalInt64Wrapper", REQUIRED, - R"({"optionalInt64Wrapper": 0})", - "optional_int64_wrapper: {value: 0}"); - RunValidJsonTest("OptionalUint64Wrapper", REQUIRED, - R"({"optionalUint64Wrapper": 0})", - "optional_uint64_wrapper: {value: 0}"); - RunValidJsonTest("OptionalFloatWrapper", REQUIRED, - R"({"optionalFloatWrapper": 0})", - "optional_float_wrapper: {value: 0}"); - RunValidJsonTest("OptionalDoubleWrapper", REQUIRED, - R"({"optionalDoubleWrapper": 0})", - "optional_double_wrapper: {value: 0}"); - RunValidJsonTest("OptionalStringWrapper", REQUIRED, - R"({"optionalStringWrapper": ""})", - R"(optional_string_wrapper: {value: ""})"); - RunValidJsonTest("OptionalBytesWrapper", REQUIRED, - R"({"optionalBytesWrapper": ""})", - R"(optional_bytes_wrapper: {value: ""})"); - RunValidJsonTest("OptionalWrapperTypesWithNonDefaultValue", REQUIRED, - R"({ - "optionalBoolWrapper": true, - "optionalInt32Wrapper": 1, - "optionalUint32Wrapper": 1, - "optionalInt64Wrapper": "1", - "optionalUint64Wrapper": "1", - "optionalFloatWrapper": 1, - "optionalDoubleWrapper": 1, - "optionalStringWrapper": "1", - "optionalBytesWrapper": "AQI=" - })", - R"( - optional_bool_wrapper: {value: true} - optional_int32_wrapper: {value: 1} - optional_uint32_wrapper: {value: 1} - optional_int64_wrapper: {value: 1} - optional_uint64_wrapper: {value: 1} - optional_float_wrapper: {value: 1} - optional_double_wrapper: {value: 1} - optional_string_wrapper: {value: "1"} - optional_bytes_wrapper: {value: "\x01\x02"} - )"); - RunValidJsonTest("RepeatedBoolWrapper", REQUIRED, - R"({"repeatedBoolWrapper": [true, false]})", - "repeated_bool_wrapper: {value: true}" - "repeated_bool_wrapper: {value: false}"); - RunValidJsonTest("RepeatedInt32Wrapper", REQUIRED, - R"({"repeatedInt32Wrapper": [0, 1]})", - "repeated_int32_wrapper: {value: 0}" - "repeated_int32_wrapper: {value: 1}"); - RunValidJsonTest("RepeatedUint32Wrapper", REQUIRED, - R"({"repeatedUint32Wrapper": [0, 1]})", - "repeated_uint32_wrapper: {value: 0}" - "repeated_uint32_wrapper: {value: 1}"); - RunValidJsonTest("RepeatedInt64Wrapper", REQUIRED, - R"({"repeatedInt64Wrapper": [0, 1]})", - "repeated_int64_wrapper: {value: 0}" - "repeated_int64_wrapper: {value: 1}"); - RunValidJsonTest("RepeatedUint64Wrapper", REQUIRED, - R"({"repeatedUint64Wrapper": [0, 1]})", - "repeated_uint64_wrapper: {value: 0}" - "repeated_uint64_wrapper: {value: 1}"); - RunValidJsonTest("RepeatedFloatWrapper", REQUIRED, - R"({"repeatedFloatWrapper": [0, 1]})", - "repeated_float_wrapper: {value: 0}" - "repeated_float_wrapper: {value: 1}"); - RunValidJsonTest("RepeatedDoubleWrapper", REQUIRED, - R"({"repeatedDoubleWrapper": [0, 1]})", - "repeated_double_wrapper: {value: 0}" - "repeated_double_wrapper: {value: 1}"); - RunValidJsonTest("RepeatedStringWrapper", REQUIRED, - R"({"repeatedStringWrapper": ["", "AQI="]})", - R"( - repeated_string_wrapper: {value: ""} - repeated_string_wrapper: {value: "AQI="} - )"); - RunValidJsonTest("RepeatedBytesWrapper", REQUIRED, - R"({"repeatedBytesWrapper": ["", "AQI="]})", - R"( - repeated_bytes_wrapper: {value: ""} - repeated_bytes_wrapper: {value: "\x01\x02"} - )"); - RunValidJsonTest("WrapperTypesWithNullValue", REQUIRED, - R"({ - "optionalBoolWrapper": null, - "optionalInt32Wrapper": null, - "optionalUint32Wrapper": null, - "optionalInt64Wrapper": null, - "optionalUint64Wrapper": null, - "optionalFloatWrapper": null, - "optionalDoubleWrapper": null, - "optionalStringWrapper": null, - "optionalBytesWrapper": null, - "repeatedBoolWrapper": null, - "repeatedInt32Wrapper": null, - "repeatedUint32Wrapper": null, - "repeatedInt64Wrapper": null, - "repeatedUint64Wrapper": null, - "repeatedFloatWrapper": null, - "repeatedDoubleWrapper": null, - "repeatedStringWrapper": null, - "repeatedBytesWrapper": null - })", - ""); - - // Duration - RunValidJsonTest( - "DurationMinValue", REQUIRED, - R"({"optionalDuration": "-315576000000.999999999s"})", - "optional_duration: {seconds: -315576000000 nanos: -999999999}"); - RunValidJsonTest( - "DurationMaxValue", REQUIRED, - R"({"optionalDuration": "315576000000.999999999s"})", - "optional_duration: {seconds: 315576000000 nanos: 999999999}"); - RunValidJsonTest("DurationRepeatedValue", REQUIRED, - R"({"repeatedDuration": ["1.5s", "-1.5s"]})", - "repeated_duration: {seconds: 1 nanos: 500000000}" - "repeated_duration: {seconds: -1 nanos: -500000000}"); - RunValidJsonTest("DurationNull", REQUIRED, R"({"optionalDuration": null})", - ""); - RunValidJsonTest("DurationNegativeSeconds", REQUIRED, - R"({"optionalDuration": "-5s"})", - "optional_duration: {seconds: -5 nanos: 0}"); - RunValidJsonTest("DurationNegativeNanos", REQUIRED, - R"({"optionalDuration": "-0.5s"})", - "optional_duration: {seconds: 0 nanos: -500000000}"); - - ExpectParseFailureForJson("DurationMissingS", REQUIRED, - R"({"optionalDuration": "1"})"); - ExpectParseFailureForJson( - "DurationJsonInputTooSmall", REQUIRED, - R"({"optionalDuration": "-315576000001.000000000s"})"); - ExpectParseFailureForJson( - "DurationJsonInputTooLarge", REQUIRED, - R"({"optionalDuration": "315576000001.000000000s"})"); - ExpectSerializeFailureForJson( - "DurationProtoInputTooSmall", REQUIRED, - "optional_duration: {seconds: -315576000001 nanos: 0}"); - ExpectSerializeFailureForJson( - "DurationProtoInputTooLarge", REQUIRED, - "optional_duration: {seconds: 315576000001 nanos: 0}"); - - ExpectSerializeFailureForJson("DurationProtoNanosWrongSign", REQUIRED, - "optional_duration: {seconds: 1 nanos: -1}"); - ExpectSerializeFailureForJson("DurationProtoNanosWrongSignNegativeSecs", - REQUIRED, - "optional_duration: {seconds: -1 nanos: 1}"); - ExpectSerializeFailureForJson( - "DurationProtoNanosTooSmall", REQUIRED, - "optional_duration: {seconds: -1 nanos: -1000000000}"); - ExpectSerializeFailureForJson( - "DurationProtoNanosTooLarge", REQUIRED, - "optional_duration: {seconds: 1 nanos: 1000000000}"); - - RunValidJsonTestWithValidator( - "DurationHasZeroFractionalDigit", RECOMMENDED, - R"({"optionalDuration": "1.000000000s"})", [](const Json::Value& value) { - return value["optionalDuration"].asString() == "1s"; - }); - RunValidJsonTestWithValidator( - "DurationHas3FractionalDigits", RECOMMENDED, - R"({"optionalDuration": "1.010000000s"})", [](const Json::Value& value) { - return value["optionalDuration"].asString() == "1.010s"; - }); - RunValidJsonTestWithValidator( - "DurationHas6FractionalDigits", RECOMMENDED, - R"({"optionalDuration": "1.000010000s"})", [](const Json::Value& value) { - return value["optionalDuration"].asString() == "1.000010s"; - }); - RunValidJsonTestWithValidator( - "DurationHas9FractionalDigits", RECOMMENDED, - R"({"optionalDuration": "1.000000010s"})", [](const Json::Value& value) { - return value["optionalDuration"].asString() == "1.000000010s"; - }); - - // Timestamp - RunValidJsonTest("TimestampMinValue", REQUIRED, - R"({"optionalTimestamp": "0001-01-01T00:00:00Z"})", - "optional_timestamp: {seconds: -62135596800}"); - RunValidJsonTest( - "TimestampMaxValue", REQUIRED, - R"({"optionalTimestamp": "9999-12-31T23:59:59.999999999Z"})", - "optional_timestamp: {seconds: 253402300799 nanos: 999999999}"); - RunValidJsonTest( - "TimestampRepeatedValue", REQUIRED, - R"({ - "repeatedTimestamp": [ - "0001-01-01T00:00:00Z", - "9999-12-31T23:59:59.999999999Z" - ] - })", - "repeated_timestamp: {seconds: -62135596800}" - "repeated_timestamp: {seconds: 253402300799 nanos: 999999999}"); - RunValidJsonTest("TimestampEpochValue", REQUIRED, - R"({"optionalTimestamp": "1970-01-01T00:00:00.000Z"})", - "optional_timestamp: {seconds: 0}"); - RunValidJsonTest("TimestampNanoAfterEpochlValue", REQUIRED, - R"({"optionalTimestamp": "1970-01-01T00:00:00.000000001Z"})", - "optional_timestamp: {seconds: 0 nanos: 1}"); - RunValidJsonTest("TimestampNanoBeforeEpochValue", REQUIRED, - R"({"optionalTimestamp": "1969-12-31T23:59:59.999999999Z"})", - "optional_timestamp: {seconds: -1 nanos: 999999999}"); - RunValidJsonTest("TimestampLittleAfterEpochlValue", REQUIRED, - R"({"optionalTimestamp": "1970-01-01T00:00:01.000000001Z"})", - "optional_timestamp: {seconds: 1 nanos: 1}"); - RunValidJsonTest("TimestampLittleBeforeEpochValue", REQUIRED, - R"({"optionalTimestamp": "1969-12-31T23:59:58.999999999Z"})", - "optional_timestamp: {seconds: -2 nanos: 999999999}"); - RunValidJsonTest("TimestampTenAndHalfSecondsAfterEpochValue", REQUIRED, - R"({"optionalTimestamp": "1970-01-01T00:00:10.500Z"})", - "optional_timestamp: {seconds: 10 nanos: 500000000}"); - RunValidJsonTest("TimestampTenAndHalfSecondsBeforeEpochValue", REQUIRED, - R"({"optionalTimestamp": "1969-12-31T23:59:49.500Z"})", - "optional_timestamp: {seconds: -11 nanos: 500000000}"); - RunValidJsonTest("TimestampLeap", REQUIRED, - R"({"optionalTimestamp": "1993-02-10T00:00:00.000Z"})", - "optional_timestamp: {seconds: 729302400}"); - RunValidJsonTest("TimestampWithPositiveOffset", REQUIRED, - R"({"optionalTimestamp": "1970-01-01T08:00:01+08:00"})", - "optional_timestamp: {seconds: 1}"); - RunValidJsonTest("TimestampWithNegativeOffset", REQUIRED, - R"({"optionalTimestamp": "1969-12-31T16:00:01-08:00"})", - "optional_timestamp: {seconds: 1}"); - RunValidJsonTest("TimestampNull", REQUIRED, R"({"optionalTimestamp": null})", - ""); - - ExpectParseFailureForJson("TimestampJsonInputTooSmall", REQUIRED, - R"({"optionalTimestamp": "0000-01-01T00:00:00Z"})"); - ExpectParseFailureForJson( - "TimestampJsonInputTooLarge", REQUIRED, - R"({"optionalTimestamp": "10000-01-01T00:00:00Z"})"); - ExpectParseFailureForJson("TimestampJsonInputMissingZ", REQUIRED, - R"({"optionalTimestamp": "0001-01-01T00:00:00"})"); - ExpectParseFailureForJson("TimestampJsonInputMissingT", REQUIRED, - R"({"optionalTimestamp": "0001-01-01 00:00:00Z"})"); - ExpectParseFailureForJson("TimestampJsonInputLowercaseZ", REQUIRED, - R"({"optionalTimestamp": "0001-01-01T00:00:00z"})"); - ExpectParseFailureForJson("TimestampJsonInputLowercaseT", REQUIRED, - R"({"optionalTimestamp": "0001-01-01t00:00:00Z"})"); - ExpectParseFailureForJson( - "TimestampWithMissingColonInOffset", REQUIRED, - R"({"optionalTimestamp": "1970-01-01T08:00:01+0800"})"); - ExpectSerializeFailureForJson("TimestampProtoInputTooSmall", REQUIRED, - "optional_timestamp: {seconds: -62135596801}"); - ExpectSerializeFailureForJson("TimestampProtoInputTooLarge", REQUIRED, - "optional_timestamp: {seconds: 253402300800}"); - ExpectSerializeFailureForJson( - "TimestampProtoNegativeNanos", REQUIRED, - "optional_timestamp: {seconds: 5000 nanos: -1}"); - ExpectSerializeFailureForJson( - "TimestampProtoNanoTooLarge", REQUIRED, - "optional_timestamp: {seconds: 5000 nanos: 1000000000}"); - RunValidJsonTestWithValidator( - "TimestampZeroNormalized", RECOMMENDED, - R"({"optionalTimestamp": "1969-12-31T16:00:00-08:00"})", - [](const Json::Value& value) { - return value["optionalTimestamp"].asString() == "1970-01-01T00:00:00Z"; - }); - RunValidJsonTestWithValidator( - "TimestampHasZeroFractionalDigit", RECOMMENDED, - R"({"optionalTimestamp": "1970-01-01T00:00:00.000000000Z"})", - [](const Json::Value& value) { - return value["optionalTimestamp"].asString() == "1970-01-01T00:00:00Z"; - }); - RunValidJsonTestWithValidator( - "TimestampHas3FractionalDigits", RECOMMENDED, - R"({"optionalTimestamp": "1970-01-01T00:00:00.010000000Z"})", - [](const Json::Value& value) { - return value["optionalTimestamp"].asString() == - "1970-01-01T00:00:00.010Z"; - }); - RunValidJsonTestWithValidator( - "TimestampHas6FractionalDigits", RECOMMENDED, - R"({"optionalTimestamp": "1970-01-01T00:00:00.000010000Z"})", - [](const Json::Value& value) { - return value["optionalTimestamp"].asString() == - "1970-01-01T00:00:00.000010Z"; - }); - RunValidJsonTestWithValidator( - "TimestampHas9FractionalDigits", RECOMMENDED, - R"({"optionalTimestamp": "1970-01-01T00:00:00.000000010Z"})", - [](const Json::Value& value) { - return value["optionalTimestamp"].asString() == - "1970-01-01T00:00:00.000000010Z"; - }); - - // Out of bounds / invalid components should JSON parse fail - ExpectParseFailureForJson("TimestampJsonInputMonthTooLarge", REQUIRED, - R"({"optionalTimestamp": "1970-13-01T00:00:00Z"})"); - ExpectParseFailureForJson("TimestampJsonInputMonthZero", REQUIRED, - R"({"optionalTimestamp": "1970-00-01T00:00:00Z"})"); - ExpectParseFailureForJson("TimestampJsonInputDayTooLarge", REQUIRED, - R"({"optionalTimestamp": "1970-01-32T00:00:00Z"})"); - ExpectParseFailureForJson("TimestampJsonInputDayZero", REQUIRED, - R"({"optionalTimestamp": "1970-01-00T00:00:00Z"})"); - ExpectParseFailureForJson("TimestampJsonInputHourTooLarge", REQUIRED, - R"({"optionalTimestamp": "1970-01-01T24:00:00Z"})"); - ExpectParseFailureForJson("TimestampJsonInputHourTooLarge25", REQUIRED, - R"({"optionalTimestamp": "1970-01-01T25:00:00Z"})"); - ExpectParseFailureForJson("TimestampJsonInputMinuteTooLarge", REQUIRED, - R"({"optionalTimestamp": "1970-01-01T00:60:00Z"})"); - ExpectParseFailureForJson("TimestampJsonInputSecondTooLarge", REQUIRED, - R"({"optionalTimestamp": "1970-01-01T00:00:60Z"})"); - ExpectParseFailureForJson( - "TimestampJsonInputInvalidOffsetHour", REQUIRED, - R"({"optionalTimestamp": "1970-01-01T00:00:00+24:00"})"); - ExpectParseFailureForJson( - "TimestampJsonInputInvalidOffsetMinute", REQUIRED, - R"({"optionalTimestamp": "1970-01-01T00:00:00+00:60"})"); - ExpectParseFailureForJson( - "TimestampJsonInputOffsetBoundaryUnderflow", REQUIRED, - R"({"optionalTimestamp": "0001-01-01T00:00:00+00:01"})"); - ExpectParseFailureForJson( - "TimestampJsonInputOffsetBoundaryOverflow", REQUIRED, - R"({"optionalTimestamp": "9999-12-31T23:59:59-00:01"})"); - ExpectParseFailureForJson( - "TimestampJsonInputInvalidNanos", REQUIRED, - R"({"optionalTimestamp": "1970-01-01T00:00:00.1234567890Z"})"); - ExpectParseFailureForJson( - "TimestampJsonInputInvalidCharsInNanos", REQUIRED, - R"({"optionalTimestamp": "1970-01-01T00:00:00.123aZ"})"); - ExpectParseFailureForJson("TimestampJsonInputYearTooShort", REQUIRED, - R"({"optionalTimestamp": "999-01-01T00:00:00Z"})"); - ExpectParseFailureForJson( - "TimestampJsonInputYearTooLong", REQUIRED, - R"({"optionalTimestamp": "00001-01-01T00:00:00Z"})"); - ExpectParseFailureForJson("TimestampJsonInputMonthTooShort", REQUIRED, - R"({"optionalTimestamp": "1970-1-01T00:00:00Z"})"); - ExpectParseFailureForJson("TimestampJsonInputDayTooShort", REQUIRED, - R"({"optionalTimestamp": "1970-01-1T00:00:00Z"})"); - ExpectParseFailureForJson("TimestampJsonInputNonLeapFeb29", REQUIRED, - R"({"optionalTimestamp": "2001-02-29T00:00:00Z"})"); - - // Honoring time zones correctly - RunValidJsonTest("TimestampJsonInputLeapFeb29", REQUIRED, - R"({"optionalTimestamp": "2000-02-29T00:00:00Z"})", - "optional_timestamp: {seconds: 951782400}"); - RunValidJsonTest("TimestampWithOffsetShiftsDay", REQUIRED, - - R"({"optionalTimestamp": "1970-01-02T01:00:00+02:00"})", - "optional_timestamp: {seconds: 82800}"); - RunValidJsonTest("TimestampWithOffsetBoundaryInBoundsMin", REQUIRED, - R"({"optionalTimestamp": "0001-01-01T00:00:00-00:01"})", - "optional_timestamp: {seconds: -62135596740}"); - RunValidJsonTest("TimestampWithOffsetBoundaryInBoundsMax", REQUIRED, - R"({"optionalTimestamp": "9999-12-31T23:59:59+00:01"})", - "optional_timestamp: {seconds: 253402300739}"); - RunValidJsonTest("TimestampWithComplexOffset", REQUIRED, - R"({"optionalTimestamp": "1970-01-01T00:00:00-11:30"})", - "optional_timestamp: {seconds: 41400}"); -} - -template <typename MessageType> -void BinaryAndJsonConformanceSuiteImpl< - MessageType>::RunJsonTestsForFieldMask() { - RunValidJsonTest("FieldMask", REQUIRED, - R"({"optionalFieldMask": "foo,barBaz"})", - R"(optional_field_mask: {paths: "foo" paths: "bar_baz"})"); - RunValidJsonTest("EmptyFieldMask", REQUIRED, R"({"optionalFieldMask": ""})", - R"(optional_field_mask: {})"); - ExpectParseFailureForJson("FieldMaskInvalidCharacter", RECOMMENDED, - R"({"optionalFieldMask": "foo,bar_bar"})"); - ExpectSerializeFailureForJson("FieldMaskPathsDontRoundTrip", RECOMMENDED, - R"(optional_field_mask: {paths: "fooBar"})"); - ExpectSerializeFailureForJson("FieldMaskNumbersDontRoundTrip", RECOMMENDED, - R"(optional_field_mask: {paths: "foo_3_bar"})"); - ExpectSerializeFailureForJson("FieldMaskTooManyUnderscore", RECOMMENDED, - R"(optional_field_mask: {paths: "foo__bar"})"); -} - -template <typename MessageType> -void BinaryAndJsonConformanceSuiteImpl<MessageType>::RunJsonTestsForStruct() { - RunValidJsonTest("Struct", REQUIRED, - R"({ - "optionalStruct": { - "nullValue": null, - "intValue": 1234, - "boolValue": true, - "doubleValue": 1234.5678, - "stringValue": "Hello world!", - "listValue": [1234, "5678"], - "objectValue": { - "value": 0 - } - } - })", - R"( - optional_struct: { - fields: { - key: "nullValue" - value: {null_value: NULL_VALUE} - } - fields: { - key: "intValue" - value: {number_value: 1234} - } - fields: { - key: "boolValue" - value: {bool_value: true} - } - fields: { - key: "doubleValue" - value: {number_value: 1234.5678} - } - fields: { - key: "stringValue" - value: {string_value: "Hello world!"} - } - fields: { - key: "listValue" - value: { - list_value: { - values: { - number_value: 1234 - } - values: { - string_value: "5678" - } - } - } - } - fields: { - key: "objectValue" - value: { - struct_value: { - fields: { - key: "value" - value: { - number_value: 0 - } - } - } - } - } - } - )"); - RunValidJsonTest("StructWithEmptyListValue", REQUIRED, - R"({ - "optionalStruct": { - "listValue": [] - } - })", - R"( - optional_struct: { - fields: { - key: "listValue" - value: { - list_value: { - } - } - } - } - )"); - - RunValidJsonTest( - "StructDeepNesting25", RECOMMENDED, - absl::StrCat(R"({"optionalStruct": {)", str_repeat(R"("n": {)", 25), - R"("value": 1)", std::string(25, '}'), "}}"), - absl::StrCat("optional_struct: {\n", - str_repeat(" fields: {\n" - " key: \"n\"\n" - " value: {\n" - " struct_value: {\n", - 25), - " fields: {\n" - " key: \"value\"\n" - " value: { number_value: 1 }\n" - " }\n", - str_repeat(" }\n" - " }\n" - " }\n", - 25), - "}\n")); - - ExpectParseFailureForJson( - "StructDeepNesting200", RECOMMENDED, - absl::StrCat(R"({"optionalStruct": {)", str_repeat(R"("n": {)", 200), - R"("value": 1)", std::string(200, '}'), "}}")); -} - -template <typename MessageType> -void BinaryAndJsonConformanceSuiteImpl<MessageType>::RunJsonTestsForValue() { - RunValidJsonTest("ValueAcceptInteger", REQUIRED, R"({"optionalValue": 1})", - "optional_value: { number_value: 1}"); - RunValidJsonTest("ValueAcceptFloat", REQUIRED, R"({"optionalValue": 1.5})", - "optional_value: { number_value: 1.5}"); - RunValidJsonTest("ValueAcceptBool", REQUIRED, R"({"optionalValue": false})", - "optional_value: { bool_value: false}"); - RunValidJsonTest("ValueAcceptNull", REQUIRED, R"({"optionalValue": null})", - "optional_value: { null_value: NULL_VALUE}"); - RunValidJsonTest("ValueAcceptString", REQUIRED, - R"({"optionalValue": "hello"})", - R"(optional_value: { string_value: "hello"})"); - RunValidJsonTest("ValueAcceptList", REQUIRED, - R"({"optionalValue": [0, "hello"]})", - R"( - optional_value: { - list_value: { - values: { - number_value: 0 - } - values: { - string_value: "hello" - } - } - } - )"); - RunValidJsonTest("ValueAcceptObject", REQUIRED, - R"({"optionalValue": {"value": 1}})", - R"( - optional_value: { - struct_value: { - fields: { - key: "value" - value: { - number_value: 1 - } - } - } - } - )"); - RunValidJsonTest("RepeatedValue", REQUIRED, - R"({ - "repeatedValue": [["a"]] - })", - R"( - repeated_value: [ - { - list_value: { - values: [ - { string_value: "a"} - ] - } - } - ] - )"); - RunValidJsonTest("RepeatedListValue", REQUIRED, - R"({ - "repeatedListValue": [["a"]] - })", - R"( - repeated_list_value: [ - { - values: [ - { string_value: "a"} - ] - } - ] - )"); - RunValidJsonTestWithValidator("NullValueInOtherOneofOldFormat", RECOMMENDED, - R"({"oneofNullValue": "NULL_VALUE"})", - [](const Json::Value& value) { - return (value.isMember("oneofNullValue") && - value["oneofNullValue"].isNull()); - }); - RunValidJsonTestWithValidator("NullValueInOtherOneofNewFormat", RECOMMENDED, - R"({"oneofNullValue": null})", - [](const Json::Value& value) { - return (value.isMember("oneofNullValue") && - value["oneofNullValue"].isNull()); - }); - RunValidJsonTestWithValidator( - "NullValueInNormalMessage", RECOMMENDED, R"({"optionalNullValue": null})", - [](const Json::Value& value) { return value.empty(); }); - ExpectSerializeFailureForJson("ValueRejectNanNumberValue", RECOMMENDED, - "optional_value: { number_value: nan}"); - ExpectSerializeFailureForJson("ValueRejectInfNumberValue", RECOMMENDED, - "optional_value: { number_value: inf}"); - RunValidJsonTest("ListValueDeepNesting25", RECOMMENDED, - absl::StrCat(R"({"optionalValue": )", std::string(25, '['), - "1", std::string(25, ']'), "}"), - absl::StrCat("optional_value: {\n", - str_repeat(" list_value: {\n" - " values: {\n", - 25), - " number_value: 1\n", - str_repeat(" }\n" - " }\n", - 25), - "}\n")); - - ExpectParseFailureForJson( - "ListValueDeepNesting200", RECOMMENDED, - absl::StrCat(R"({"optionalValue": )", std::string(200, '['), "1", - std::string(200, ']'), "}")); - RunValidJsonTest( - "ValueDeepNesting25", RECOMMENDED, - absl::StrCat(R"({"optionalValue": {)", str_repeat(R"("n": {)", 25), - R"("value": 1)", std::string(25, '}'), "}}"), - absl::StrCat("optional_value: {\n", " struct_value: {\n", - str_repeat(" fields: {\n" - " key: \"n\"\n" - " value: {\n" - " struct_value: {\n", - 25), - " fields: {\n" - " key: \"value\"\n" - " value: { number_value: 1 }\n" - " }\n", - str_repeat(" }\n" - " }\n" - " }\n", - 25), - " }\n", "}\n")); - - ExpectParseFailureForJson( - "ValueDeepNesting200", RECOMMENDED, - absl::StrCat(R"({"optionalValue": {)", str_repeat(R"("n": {)", 200), - R"("value": 1)", std::string(200, '}'), "}}")); -} - -template <typename MessageType> -void BinaryAndJsonConformanceSuiteImpl<MessageType>::RunJsonTestsForAny() { - std::string type_url = GetTypeUrl(MessageType::GetDescriptor()); - RunValidJsonTest("Any", REQUIRED, - absl::Substitute(R"({ - "optionalAny": { - "@type": "$0", - "optionalInt32": 12345 - } - })", - GetTypeUrl(MessageType::GetDescriptor())), - absl::Substitute(R"( - optional_any: { - [$0] { - optional_int32: 12345 - } - } - )", - type_url)); - RunValidJsonTest("AnyNested", REQUIRED, - absl::Substitute(R"({ - "optionalAny": { - "@type": "type.googleapis.com/google.protobuf.Any", - "value": { - "@type": "$0", - "optionalInt32": 12345 - } - } - })", - type_url), - absl::Substitute(R"( - optional_any: { - [type.googleapis.com/google.protobuf.Any] { - [$0] { - optional_int32: 12345 - } - } - } - )", - type_url)); - // The special "@type" tag is not required to appear first. - RunValidJsonTest("AnyUnorderedTypeTag", REQUIRED, - absl::Substitute(R"({ - "optionalAny": { - "optionalInt32": 12345, - "@type": "$0" - } - })", - type_url), - absl::Substitute(R"( - optional_any: { - [$0] { - optional_int32: 12345 - } - } - )", - type_url)); - // Well-known types in Any. - RunValidJsonTest("AnyWithInt32ValueWrapper", REQUIRED, - R"({ - "optionalAny": { - "@type": "type.googleapis.com/google.protobuf.Int32Value", - "value": 12345 - } - })", - R"( - optional_any: { - [type.googleapis.com/google.protobuf.Int32Value] { - value: 12345 - } - } - )"); - RunValidJsonTest("AnyWithDuration", REQUIRED, - R"({ - "optionalAny": { - "@type": "type.googleapis.com/google.protobuf.Duration", - "value": "1.5s" - } - })", - R"( - optional_any: { - [type.googleapis.com/google.protobuf.Duration] { - seconds: 1 - nanos: 500000000 - } - } - )"); - RunValidJsonTest("AnyWithTimestamp", REQUIRED, - R"({ - "optionalAny": { - "@type": "type.googleapis.com/google.protobuf.Timestamp", - "value": "1970-01-01T00:00:00Z" - } - })", - R"( - optional_any: { - [type.googleapis.com/google.protobuf.Timestamp] { - seconds: 0 - nanos: 0 - } - } - )"); - RunValidJsonTest("AnyWithFieldMask", REQUIRED, - R"({ - "optionalAny": { - "@type": "type.googleapis.com/google.protobuf.FieldMask", - "value": "foo,barBaz" - } - })", - R"( - optional_any: { - [type.googleapis.com/google.protobuf.FieldMask] { - paths: ["foo", "bar_baz"] - } - } - )"); - RunValidJsonTest("AnyWithStruct", REQUIRED, - R"({ - "optionalAny": { - "@type": "type.googleapis.com/google.protobuf.Struct", - "value": { - "foo": 1 - } - } - })", - R"( - optional_any: { - [type.googleapis.com/google.protobuf.Struct] { - fields: { - key: "foo" - value: { - number_value: 1 - } - } - } - } - )"); - RunValidJsonTest("AnyWithValueForJsonObject", REQUIRED, - R"({ - "optionalAny": { - "@type": "type.googleapis.com/google.protobuf.Value", - "value": { - "foo": 1 - } - } - })", - R"( - optional_any: { - [type.googleapis.com/google.protobuf.Value] { - struct_value: { - fields: { - key: "foo" - value: { - number_value: 1 - } - } - } - } - } - )"); - RunValidJsonTest("AnyWithValueForInteger", REQUIRED, - R"({ - "optionalAny": { - "@type": "type.googleapis.com/google.protobuf.Value", - "value": 1 - } - })", - R"( - optional_any: { - [type.googleapis.com/google.protobuf.Value] { - number_value: 1 - } - } - )"); - // When the Any is in WKT form (with "@type"), the type_url must be present - // and URL shaped, otherwise it should be a parse error (because it can't be - // parsed into the Any schema). - ExpectParseFailureForJson("AnyWktRepresentationWithEmptyTypeAndValue", - REQUIRED, - R"({ - "optionalAny": { - "@type": "", - "value": "" - } - })"); - ExpectParseFailureForJson("AnyWktRepresentationWithBadType", REQUIRED, - R"({ - "optionalAny": { - "@type": "not_a_url", - "value": "" - } - })"); - // When the Any can be parsed as non-WKT form, the type_url could be missing - // or invalid, since that can still be parsed into the Any schema. - RunValidJsonTest("AnyWithNoType", REQUIRED, - R"({ - "optionalAny": {} - })", - R"( - optional_any: {} - )"); - // `null` where an Any exists should just result in the field being unset. - RunValidJsonTest("AnyNull", REQUIRED, - R"({ - "optionalAny": null - })", - R"( - )"); - - // google.protobuf.Empty packed into an Any, implementations must accept it - // without the "value" field set. This also confirms that what they round trip - // does not have `"value":{}` set on it, since the test harness uses the C++ - // JSON parser which will reject it. - RunValidJsonTest("AnyEmpty", REQUIRED, - R"({ - "optionalAny": { - "@type": "type.googleapis.com/google.protobuf.Empty" - } - })", - R"( - optional_any: { - [type.googleapis.com/google.protobuf.Empty] {} - } - )"); -} - -template <typename MessageType> -const FieldDescriptor* -BinaryAndJsonConformanceSuiteImpl<MessageType>::GetFieldForType( - FieldDescriptor::Type type, bool repeated, Packed packed) const { - const Descriptor* d = MessageType::GetDescriptor(); - for (int i = 0; i < d->field_count(); i++) { - const FieldDescriptor* f = d->field(i); - if (f->type() == type && f->is_repeated() == repeated) { - if ((packed == Packed::kTrue && !f->is_packed()) || - (packed == Packed::kFalse && f->is_packed())) { - continue; - } - return f; - } - } - - absl::string_view packed_string = ""; - const absl::string_view repeated_string = - repeated ? "Repeated " : "Singular "; - if (packed == Packed::kTrue) { - packed_string = "Packed "; - } - if (packed == Packed::kFalse) { - packed_string = "Unpacked "; - } - ABSL_LOG(FATAL) << "Couldn't find field with type: " << repeated_string - << packed_string << FieldDescriptor::TypeName(type) << " for " - << d->full_name(); - return nullptr; -} - -template <typename MessageType> -const FieldDescriptor* -BinaryAndJsonConformanceSuiteImpl<MessageType>::GetFieldForMapType( - FieldDescriptor::Type key_type, FieldDescriptor::Type value_type) const { - const Descriptor* d = MessageType::GetDescriptor(); - for (int i = 0; i < d->field_count(); i++) { - const FieldDescriptor* f = d->field(i); - if (f->is_map()) { - const Descriptor* map_entry = f->message_type(); - const FieldDescriptor* key = map_entry->field(0); - const FieldDescriptor* value = map_entry->field(1); - if (key->type() == key_type && value->type() == value_type) { - return f; - } - } - } - - ABSL_LOG(FATAL) << "Couldn't find map field with type: " - << FieldDescriptor::TypeName(key_type) << " and " - << FieldDescriptor::TypeName(key_type) << " for " - << d->full_name(); - return nullptr; -} - -template <typename MessageType> -const FieldDescriptor* -BinaryAndJsonConformanceSuiteImpl<MessageType>::GetFieldForOneofType( - FieldDescriptor::Type type, bool exclusive) const { - const Descriptor* d = MessageType::GetDescriptor(); - for (int i = 0; i < d->field_count(); i++) { - const FieldDescriptor* f = d->field(i); - if (f->containing_oneof() && ((f->type() == type) ^ exclusive)) { - return f; - } - } - - ABSL_LOG(FATAL) << "Couldn't find oneof field with type: " - << FieldDescriptor::TypeName(type) << " for " - << d->full_name(); - return nullptr; -} - -template <typename MessageType> -std::string BinaryAndJsonConformanceSuiteImpl<MessageType>::SyntaxIdentifier() - const { - if (std::is_same_v<MessageType, TestAllTypesProto2>) { - return "Proto2"; - } else if (std::is_same_v<MessageType, TestAllTypesProto3>) { - return "Proto3"; - } else if (std::is_same_v<MessageType, TestAllTypesProto2Editions>) { - return "Editions_Proto2"; - } else { - return "Editions_Proto3"; - } -} - -} // namespace conformance -} // namespace protobuf -} // namespace google
diff --git a/conformance/binary_json_conformance_suite.h b/conformance/binary_json_conformance_suite.h deleted file mode 100644 index 545b23b..0000000 --- a/conformance/binary_json_conformance_suite.h +++ /dev/null
@@ -1,241 +0,0 @@ -// Protocol Buffers - Google's data interchange format -// Copyright 2008 Google Inc. All rights reserved. -// -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file or at -// https://developers.google.com/open-source/licenses/bsd - -#ifndef GOOGLE_PROTOBUF_CONFORMANCE_BINARY_JSON_CONFORMANCE_SUITE_H__ -#define GOOGLE_PROTOBUF_CONFORMANCE_BINARY_JSON_CONFORMANCE_SUITE_H__ - -#include <functional> -#include <memory> -#include <string> -#include <utility> -#include <vector> - -#include "absl/strings/string_view.h" -#include "json/json.h" -#include "conformance/conformance_test.h" -#include "google/protobuf/descriptor.h" -#include "google/protobuf/util/type_resolver.h" - -namespace google { -namespace protobuf { -namespace conformance { - -class BinaryAndJsonConformanceSuite : public ConformanceTestSuite { - public: - BinaryAndJsonConformanceSuite() = default; - - private: - void RunSuiteImpl() override; - bool ParseJsonResponse(const ::conformance::ConformanceResponse& response, - Message* test_message); - bool ParseResponse(const ::conformance::ConformanceResponse& response, - const ConformanceRequestSetting& setting, - Message* test_message) override; - void SetTypeUrl(absl::string_view type_url) { - type_url_ = std::string(type_url); - } - - template <typename MessageType> - void RunValidBinaryProtobufTest(const std::string& test_name, - ConformanceLevel level, - const std::string& input_protobuf, - const std::string& equivalent_text_format); - - template <typename MessageType> - void RunValidRoundtripProtobufTest(const std::string& test_name, - ConformanceLevel level, - const std::string& input_protobuf); - - template <typename MessageType> - void RunValidProtobufTest(const std::string& test_name, - ConformanceLevel level, - const std::string& input_protobuf, - const std::string& equivalent_text_format); - - template <typename MessageType> - void ExpectParseFailureForProto(const std::string& proto, - const std::string& test_name, - ConformanceLevel level); - - template <typename MessageType> - void RunValidJsonTest(const std::string& test_name, ConformanceLevel level, - const std::string& input_json, - const std::string& equivalent_text_format); - - void RunValidJsonTestWithMessage(const std::string& test_name, - ConformanceLevel level, - const std::string& input_json, - const std::string& equivalent_text_format, - const Message& prototype); - - template <typename MessageType> - void ExpectParseFailureForJson(const std::string& test_name, - ConformanceLevel level, - const std::string& input_json); - - typedef std::function<bool(const Json::Value&)> Validator; - template <typename MessageType> - void RunValidJsonTestWithValidator(const std::string& test_name, - ConformanceLevel level, - const std::string& input_json, - const Validator& validator); - - void RunDelimitedFieldTests(); - - void RunUnstableTests(); - - void RunEdition2026Tests(); - - void RunUtf8ValidationTests(); - - void RunMessageSetTests(); - - void RunRecursionLimitTests(); - - template <typename MessageType> - friend class BinaryAndJsonConformanceSuiteImpl; - - std::unique_ptr<google::protobuf::util::TypeResolver> type_resolver_; - std::string type_url_; -}; - -template <typename MessageType> -class BinaryAndJsonConformanceSuiteImpl { - public: - explicit BinaryAndJsonConformanceSuiteImpl( - BinaryAndJsonConformanceSuite* suite, bool run_proto3_tests); - - private: - using ConformanceRequestSetting = - BinaryAndJsonConformanceSuite::ConformanceRequestSetting; - using ConformanceLevel = BinaryAndJsonConformanceSuite::ConformanceLevel; - constexpr static ConformanceLevel RECOMMENDED = ConformanceLevel::RECOMMENDED; - constexpr static ConformanceLevel REQUIRED = ConformanceLevel::REQUIRED; - - void RunAllTests(); - - void RunBinaryPerformanceTests(); - void RunJsonTests(); - void RunJsonTestsForStoresDefaultPrimitive(); - void RunJsonTestsForFieldNameConvention(); - void RunJsonTestsForNonRepeatedTypes(); - void RunJsonTestsForRepeatedTypes(); - void RunJsonTestsForNullTypes(); - void RunJsonTestsForWrapperTypes(); - void RunJsonTestsForFieldMask(); - void RunJsonTestsForStruct(); - void RunJsonTestsForValue(); - void RunJsonTestsForAny(); - void RunJsonTestsForUnknownEnumStringValues(); - void RunJsonTestsForReservedFields(); - void RunValidJsonTest(const std::string& test_name, ConformanceLevel level, - const std::string& input_json, - const std::string& equivalent_text_format); - void RunValidJsonTestWithMessage(const std::string& test_name, - ConformanceLevel level, - const std::string& input_json, - const std::string& equivalent_text_forma, - const Message& prototype); - void RunValidJsonTestWithProtobufInput( - const std::string& test_name, ConformanceLevel level, - const MessageType& input, const std::string& equivalent_text_format); - void RunValidJsonIgnoreUnknownTest(const std::string& test_name, - ConformanceLevel level, - const std::string& input_json, - const std::string& equivalent_text_format); - void RunValidProtobufTest(const std::string& test_name, - ConformanceLevel level, - const std::string& input_protobuf, - const std::string& equivalent_text_format); - void RunValidBinaryProtobufTest(const std::string& test_name, - ConformanceLevel level, - const std::string& input_protobuf); - void RunValidBinaryProtobufTest(const std::string& test_name, - ConformanceLevel level, - const std::string& input_protobuf, - const std::string& expected_protobuf); - void RunBinaryPerformanceMergeMessageWithField( - const std::string& test_name, const std::string& field_proto); - - void RunValidProtobufTestWithMessage( - const std::string& test_name, ConformanceLevel level, - const Message* input, const std::string& equivalent_text_format); - - typedef std::function<bool(const Json::Value&)> Validator; - void RunValidJsonTestWithValidator(const std::string& test_name, - ConformanceLevel level, - const std::string& input_json, - const Validator& validator); - void ExpectParseFailureForJson(const std::string& test_name, - ConformanceLevel level, - const std::string& input_json); - void RunValidJsonTestOrParseFailure( - const std::string& test_name, ConformanceLevel level, - const std::string& input_json, const std::string& equivalent_text_format); - void ExpectSerializeFailureForJson(const std::string& test_name, - ConformanceLevel level, - const std::string& text_format); - void ExpectParseFailureForProtoWithProtoVersion(const std::string& proto, - const std::string& test_name, - ConformanceLevel level); - void ExpectParseFailureForProto(const std::string& proto, - const std::string& test_name, - ConformanceLevel level); - void ExpectHardParseFailureForProto(const std::string& proto, - const std::string& test_name, - ConformanceLevel level); - void TestPrematureEOFForType(google::protobuf::FieldDescriptor::Type type); - void TestIllegalTags(); - void TestIllegalLengths(); - void TestUnmatchedGroup(); - void TestUnknownWireType(); - void TestInvalidUtf8String(); - void TestOneofMessage(); - void TestUnknownMessage(); - void TestUnknownOrdering(); - void TestValidDataForType( - google::protobuf::FieldDescriptor::Type, - std::vector<std::pair<std::string, std::string>> values); - void TestValidDataForRepeatedScalarMessage(); - void TestValidDataForMapType(google::protobuf::FieldDescriptor::Type, - google::protobuf::FieldDescriptor::Type); - void TestMapEntryWireTypeMismatch(google::protobuf::FieldDescriptor::Type, - google::protobuf::FieldDescriptor::Type); - void RunMapEntryWireTypeMismatchTest(const std::string& test_name, - const std::string& proto); - void TestValidDataForOneofType(google::protobuf::FieldDescriptor::Type); - void TestMergeOneofMessage(); - void TestOverwriteMessageValueMap(); - void TestBinaryPerformanceForAlternatingUnknownFields(); - void TestBinaryPerformanceMergeMessageWithRepeatedFieldForType( - google::protobuf::FieldDescriptor::Type); - void TestBinaryPerformanceMergeMessageWithUnknownFieldForType( - google::protobuf::FieldDescriptor::Type); - - enum class Packed { - kUnspecified = 0, - kTrue = 1, - kFalse = 2, - }; - const FieldDescriptor* GetFieldForType( - FieldDescriptor::Type type, bool repeated, - Packed packed = Packed::kUnspecified) const; - const FieldDescriptor* GetFieldForMapType( - FieldDescriptor::Type key_type, FieldDescriptor::Type value_type) const; - const FieldDescriptor* GetFieldForOneofType(FieldDescriptor::Type type, - bool exclusive = false) const; - std::string SyntaxIdentifier() const; - - BinaryAndJsonConformanceSuite& suite_; - bool run_proto3_tests_; -}; - -} // namespace conformance -} // namespace protobuf -} // namespace google - -#endif // GOOGLE_PROTOBUF_CONFORMANCE_BINARY_JSON_CONFORMANCE_SUITE_H__
diff --git a/conformance/binary_merge_test.cc b/conformance/binary_merge_test.cc new file mode 100644 index 0000000..0d8e20c --- /dev/null +++ b/conformance/binary_merge_test.cc
@@ -0,0 +1,232 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2026 Google LLC. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +// Binary conformance tests checking how several occurrences of the same field +// in one payload are combined: repeated submessages are merged, a repeated +// map entry replaces the previous value for its key, and repeated occurrences +// of a oneof message field are merged too. This replaces the legacy +// BinaryAndJsonConformanceSuiteImpl<M>:: +// TestValidDataForRepeatedScalarMessage(), +// TestOverwriteMessageValueMap() and TestMergeOneofMessage(); the test names +// and the requests sent to the testee are identical to the legacy ones. +// +// Like the legacy RunValidProtobufTest(), each input is sent twice: once to be +// serialized as binary ("<name>.ProtobufOutput") and once as JSON +// ("<name>.JsonOutput"), and both outputs must be equivalent to the same +// reference message. + +#include <cstdint> +#include <utility> + +#include <gmock/gmock.h> +#include <gtest/gtest.h> +#include "conformance/binary_test_util.h" +#include "conformance/binary_wireformat.h" +#include "conformance/matchers.h" +#include "conformance/message_type_fixtures.h" +#include "conformance/testee.h" +#include "google/protobuf/descriptor.h" + +namespace google { +namespace protobuf { +namespace conformance { +namespace { + +using ::testing::ValuesIn; + +// In every TestAllTypes message, NestedMessage's fields are a (1) and +// corecursive (2), and the TestAllTypes fields used inside the corecursive +// submessages of the merge-test inputs are optional_int32 (1), +// optional_int64 (2), optional_uint32 (3), repeated_int32 (31) and +// unpacked_int32 (89). +constexpr uint32_t kNestedMessageCorecursiveField = 2; +constexpr uint32_t kOptionalInt32Field = 1; +constexpr uint32_t kOptionalInt64Field = 2; +constexpr uint32_t kOptionalUint32Field = 3; +constexpr uint32_t kRepeatedInt32Field = 31; +constexpr uint32_t kUnpackedInt32Field = 89; + +// A NestedMessage whose corecursive submessage is `corecursive`. +Wire NestedMessageWithCorecursive(const Wire& corecursive) { + return LengthPrefixedField(kNestedMessageCorecursiveField, corecursive); +} + +// The input of a merge test together with the single occurrence of the field +// that the parsed input must be equivalent to (and, for the byte-exact test, +// serialize to). +struct MergeTestData { + Wire input; + Wire expected; +}; + +// RepeatedScalarMessageMerge: `message`'s singular message field +// (optional_nested_message) twice, each holding a `corecursive` submessage +// that sets optional_int32, one of optional_int64 / optional_uint32, and +// repeated_int32. A parser must merge the two submessages rather than +// replace the first with the second. +Wire RepeatedScalarMessageMergeInput(const Descriptor& message) { + const uint32_t field = FieldNumber(*GetFieldForType( + message, FieldDescriptor::TYPE_MESSAGE, /*repeated=*/false)); + return Wire( + LengthPrefixedField(field, NestedMessageWithCorecursive(Wire( + VarintField(kOptionalInt32Field, 1234), + VarintField(kOptionalInt64Field, 1234), + VarintField(kRepeatedInt32Field, 1234)))), + LengthPrefixedField(field, NestedMessageWithCorecursive(Wire( + VarintField(kOptionalInt32Field, 4321), + VarintField(kOptionalUint32Field, 4321), + VarintField(kRepeatedInt32Field, 4321))))); +} + +// ValidDataMap.STRING.MESSAGE.MergeValue: two entries of `message`'s +// map_string_nested_message field with the same (empty) key and different +// message values. A later map entry replaces an earlier one with the same +// key, so `expected` is the second entry alone. +MergeTestData MapMessageValueMergeData(const Descriptor& message) { + // Map entries have key = 1 and value = 2. + constexpr uint32_t kKeyField = 1; + constexpr uint32_t kValueField = 2; + const uint32_t field = FieldNumber(*GetFieldForMapType( + message, FieldDescriptor::TYPE_STRING, FieldDescriptor::TYPE_MESSAGE)); + Wire key = LengthPrefixedField(kKeyField, ""); + Wire first_entry = LengthPrefixedField( + field, + Wire(key, LengthPrefixedField( + kValueField, NestedMessageWithCorecursive(Wire( + VarintField(kOptionalInt32Field, 1), + VarintField(kRepeatedInt32Field, 1)))))); + Wire second_entry = LengthPrefixedField( + field, + Wire(key, LengthPrefixedField( + kValueField, NestedMessageWithCorecursive(Wire( + VarintField(kOptionalInt64Field, 1), + VarintField(kRepeatedInt32Field, 1)))))); + return {/*input=*/Wire(first_entry, second_entry), + /*expected=*/std::move(second_entry)}; +} + +// ValidDataOneof.MESSAGE.Merge: `message`'s oneof message field +// (oneof_nested_message) twice, each holding a `corecursive` submessage. +// Repeated occurrences of a oneof message field are merged like any other +// message field, so `expected` is a single occurrence holding the merged +// submessage. +MergeTestData OneofMessageMergeData(const Descriptor& message) { + const uint32_t field = FieldNumber( + *GetFieldForOneofType(message, FieldDescriptor::TYPE_MESSAGE)); + Wire first = LengthPrefixedField( + field, + NestedMessageWithCorecursive(Wire(VarintField(kOptionalInt32Field, 1), + VarintField(kOptionalInt64Field, 1), + VarintField(kUnpackedInt32Field, 1)))); + Wire second = LengthPrefixedField( + field, + NestedMessageWithCorecursive(Wire(VarintField(kOptionalInt64Field, 1), + VarintField(kUnpackedInt32Field, 1)))); + // The singular fields take the last value seen and the repeated field + // accumulates both. + Wire merged = LengthPrefixedField( + field, + NestedMessageWithCorecursive(Wire(VarintField(kOptionalInt32Field, 1), + VarintField(kOptionalInt64Field, 1), + VarintField(kUnpackedInt32Field, 1), + VarintField(kUnpackedInt32Field, 1)))); + return {/*input=*/Wire(first, second), /*expected=*/std::move(merged)}; +} + +using MergeTest = MessageTypeConformanceTest; + +// Two occurrences of optional_nested_message, each with a corecursive +// submessage, must be merged field by field. optional_nested_message (18) is +// the first singular message field in every TestAllTypes message, which is +// what the legacy test's GetFieldForType(MESSAGE) lookup resolved to. +TEST_P(MergeTest, RepeatedScalarMessageMerge) { + EXPECT_THAT( + Testee("RepeatedScalarMessageMerge") + .ParseBinary(message(), RepeatedScalarMessageMergeInput(*message())) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(R"pb(optional_nested_message: { + corecursive: { + optional_int32: 4321 + optional_int64: 1234 + optional_uint32: 4321 + repeated_int32: [ 1234, 4321 ] + } + })pb")))); +} + +TEST_P(MergeTest, RepeatedScalarMessageMergeJson) { + EXPECT_THAT( + Testee("RepeatedScalarMessageMerge") + .ParseBinary(message(), RepeatedScalarMessageMergeInput(*message())) + .SerializeJson(), + Yields(WhenParsed(EqualsTextProto(R"pb(optional_nested_message: { + corecursive: { + optional_int32: 4321 + optional_int64: 1234 + optional_uint32: 4321 + repeated_int32: [ 1234, 4321 ] + } + })pb")))); +} + +// Two map_string_nested_message entries with the same key: the second entry's +// value replaces the first one's, it isn't merged into it. The legacy test +// computed its expected text by round-tripping the second entry through the +// generated message class and TextFormat; matching the second entry's bytes +// directly is the same equivalence check without the detour. +TEST_P(MergeTest, MapMessageValue) { + MergeTestData data = MapMessageValueMergeData(*message()); + EXPECT_THAT(Testee("ValidDataMap.STRING.MESSAGE.MergeValue") + .ParseBinary(message(), data.input) + .SerializeBinary(), + Yields(WhenParsed(EqualsBinaryProto(data.expected)))); +} + +TEST_P(MergeTest, MapMessageValueJson) { + MergeTestData data = MapMessageValueMergeData(*message()); + EXPECT_THAT(Testee("ValidDataMap.STRING.MESSAGE.MergeValue") + .ParseBinary(message(), data.input) + .SerializeJson(), + Yields(WhenParsed(EqualsBinaryProto(data.expected)))); +} + +// Two occurrences of oneof_nested_message are merged like any other message +// field. The legacy test compared the parsed output against the merged +// message (via a TextFormat round trip); see MapMessageValue above. +TEST_P(MergeTest, OneofMessage) { + MergeTestData data = OneofMessageMergeData(*message()); + EXPECT_THAT(Testee("ValidDataOneof.MESSAGE.Merge") + .ParseBinary(message(), data.input) + .SerializeBinary(), + Yields(WhenParsed(EqualsBinaryProto(data.expected)))); +} + +TEST_P(MergeTest, OneofMessageJson) { + MergeTestData data = OneofMessageMergeData(*message()); + EXPECT_THAT(Testee("ValidDataOneof.MESSAGE.Merge") + .ParseBinary(message(), data.input) + .SerializeJson(), + Yields(WhenParsed(EqualsBinaryProto(data.expected)))); +} + +// The same, but the serialized output must be exactly the merged bytes. The +// legacy suite had no JSON leg for this one. +TEST_P(MergeTest, OneofMessageBinary) { + MergeTestData data = OneofMessageMergeData(*message()); + EXPECT_THAT(Testee(kP1, "ValidDataOneofBinary.MESSAGE.Merge") + .ParseBinary(message(), data.input) + .SerializeBinary(), + Yields(RawPayload(data.expected))); +} + +INSTANTIATE_TEST_SUITE_P(All, MergeTest, ValuesIn(AllTestMessageTypes()), + MessageTypeParamName); + +} // namespace +} // namespace conformance +} // namespace protobuf +} // namespace google
diff --git a/conformance/binary_message_set_test.cc b/conformance/binary_message_set_test.cc new file mode 100644 index 0000000..117509d --- /dev/null +++ b/conformance/binary_message_set_test.cc
@@ -0,0 +1,204 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2026 Google LLC. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +// Binary conformance tests for the MessageSet wire format: well-formed items, +// items in unusual orders, duplicated entries, and the plain-submessage +// encoding of an item. This replaces the legacy suite-level +// BinaryAndJsonConformanceSuite::RunMessageSetTests(); the test names and the +// requests sent to the testee are identical to the legacy ones. +// +// These tests only exist for TestAllTypesProto2, the one test message type +// with a message_set_wire_format field. + +#include <cstdint> + +#include <gmock/gmock.h> +#include <gtest/gtest.h> +#include "absl/strings/string_view.h" +#include "conformance/binary_wireformat.h" +#include "conformance/matchers.h" +#include "conformance/test_fixture.h" +#include "conformance/testee.h" +#include "google/protobuf/test_messages_proto2.pb.h" + +namespace google { +namespace protobuf { +namespace conformance { +namespace { + +using ::protobuf_test_messages::proto2::TestAllTypesProto2; + +// Field 500 of TestAllTypesProto2 is message_set_correct, whose type has +// message_set_wire_format. Such a message is a sequence of items, each a +// group (field 1) holding the extension's field number as its type_id (field +// 2, varint) and the extension message itself (field 3, length-prefixed). +constexpr uint32_t kMessageSetField = 500; +constexpr uint32_t kItemField = 1; +constexpr uint32_t kTypeIdField = 2; +constexpr uint32_t kMessageField = 3; + +// The extension numbers, i.e. type_ids, of the MessageSetCorrect extensions +// declared in test_messages_proto2.proto, and one that isn't declared. +constexpr uint32_t kMessageSetCorrectExtension1TypeId = 1547769; +constexpr uint32_t kMessageSetCorrectExtension2TypeId = 4135312; +constexpr uint32_t kExtensionWithOneofTypeId = 123456789; +constexpr uint32_t kUnknownTypeId = 4135300; + +// Field 9 of MessageSetCorrectExtension2 is i. +constexpr uint32_t kExtension2IField = 9; + +// The expected result of every test whose payload decodes to +// MessageSetCorrectExtension2 { i: 99 }. +constexpr absl::string_view kMessageSetCorrectExtension2Text = + R"pb(message_set_correct: { + [protobuf_test_messages.proto2.TestAllTypesProto2 + .MessageSetCorrectExtension2]: { i: 99 } + })pb"; + +// The message_set_correct field, holding the items `content`. +Wire MessageSet(const Wire& content) { + return LengthPrefixedField(kMessageSetField, content); +} + +// A MessageSet item (group 1) with the entries `content`, in that order. +Wire Item(const Wire& content) { return DelimitedField(kItemField, content); } + +// The type_id entry of a MessageSet item. +Wire TypeId(uint32_t type_id) { return VarintField(kTypeIdField, type_id); } + +// The message entry of a MessageSet item. +Wire ItemMessage(const Wire& message) { + return LengthPrefixedField(kMessageField, message); +} + +// A serialized MessageSetCorrectExtension2 with i set to `i`. +Wire Extension2Message(int i) { return VarintField(kExtension2IField, i); } + +class MessageSetTest : public ConformanceTest { + private: + TestPriority DefaultPriority() const override { return kP1; } +}; + +TEST_F(MessageSetTest, ValidMessageSetEncoding) { + EXPECT_THAT( + Testee(kP0, "ValidMessageSetEncoding") + .ParseBinary( + TestAllTypesProto2::descriptor(), + MessageSet(Item(Wire(TypeId(kMessageSetCorrectExtension2TypeId), + ItemMessage(Extension2Message(99)))))) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kMessageSetCorrectExtension2Text)))); +} + +TEST_F(MessageSetTest, OutOfOrderGroupsEntries) { + EXPECT_THAT( + Testee(kP0, "ValidMessageSetEncoding.OutOfOrderGroupsEntries") + .ParseBinary(TestAllTypesProto2::descriptor(), + MessageSet(Item( + Wire(ItemMessage(Extension2Message(99)), + TypeId(kMessageSetCorrectExtension2TypeId))))) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kMessageSetCorrectExtension2Text)))); +} + +// Test that an unknown message set extension always goes to unknown fields. +// This is done by poisoning the extension payload with an entry for field 0. +TEST_F(MessageSetTest, UnknownExtension) { + Wire input = MessageSet( + Item(Wire(TypeId(kUnknownTypeId), ItemMessage(VarintField(0, 99))))); + EXPECT_THAT(Testee(kP0, "MessageSetEncoding.UnknownExtension") + .ParseBinary(TestAllTypesProto2::descriptor(), input) + .SerializeBinary(), + Yields(WhenParsed(EqualsBinaryProto(input)))); +} + +// If an encoder is unaware of the message_set_wire_format option it will be +// encoded like any other extension submessage. Decoders should be able to +// tolerate this format as well. +TEST_F(MessageSetTest, SubmessageEncoding) { + EXPECT_THAT( + Testee("ValidMessageSetEncoding.SubmessageEncoding") + .ParseBinary( + TestAllTypesProto2::descriptor(), + MessageSet(LengthPrefixedField(kMessageSetCorrectExtension2TypeId, + Extension2Message(99)))) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kMessageSetCorrectExtension2Text)))); +} + +// Test again, but this time we'll try to detect if the implementation put the +// submessage encoded entry into the unknown field set. We'll do this by using +// conflicting oneof entries where order matters when the messages are merged. +// +// In a non-compliant implementation submessage encoded messageset entry will +// be moved to unknown fields and then tacked onto the end of the payload. +// Thus we'll see field b set first, and then field a. +// +// In a compliant implementation we expect the submessage encoded messageset +// to be read first with field a set, and then the normal message set entry +// will be read with field b set, so field b wins. +TEST_F(MessageSetTest, SubmessageEncodingNotUnknown) { + // Fields 1 and 2 of ExtensionWithOneof are a and b. + EXPECT_THAT( + Testee("ValidMessageSetEncoding.SubmessageEncoding.NotUnknown") + .ParseBinary( + TestAllTypesProto2::descriptor(), + MessageSet(Wire(LengthPrefixedField(kExtensionWithOneofTypeId, + VarintField(1, 42)), + Item(Wire(TypeId(kExtensionWithOneofTypeId), + ItemMessage(VarintField(2, 99))))))) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto( + R"pb(message_set_correct: { + [protobuf_test_messages.proto2.TestAllTypesProto2 + .ExtensionWithOneof]: { b: 99 } + })pb")))); +} + +// [type_id, value, type_id (different)] -> first type_id and value honored. +TEST_F(MessageSetTest, DuplicateDifferentTypeId) { + EXPECT_THAT( + Testee("ValidMessageSetEncoding.DuplicateDifferentTypeId") + .ParseBinary(TestAllTypesProto2::descriptor(), + MessageSet(Item( + Wire(TypeId(kMessageSetCorrectExtension2TypeId), + ItemMessage(Extension2Message(99)), + TypeId(kMessageSetCorrectExtension1TypeId))))) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kMessageSetCorrectExtension2Text)))); +} + +// [type_id, value, value] -> first value honored, no merge. +TEST_F(MessageSetTest, DuplicateValue) { + EXPECT_THAT( + Testee("ValidMessageSetEncoding.DuplicateValue") + .ParseBinary( + TestAllTypesProto2::descriptor(), + MessageSet(Item(Wire(TypeId(kMessageSetCorrectExtension2TypeId), + ItemMessage(Extension2Message(99)), + ItemMessage(Extension2Message(88)))))) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kMessageSetCorrectExtension2Text)))); +} + +// [value, type_id, value] -> first value honored, no merge. +TEST_F(MessageSetTest, DuplicateValueOutOfOrder) { + EXPECT_THAT( + Testee("ValidMessageSetEncoding.DuplicateValueOutOfOrder") + .ParseBinary( + TestAllTypesProto2::descriptor(), + MessageSet(Item(Wire(ItemMessage(Extension2Message(99)), + TypeId(kMessageSetCorrectExtension2TypeId), + ItemMessage(Extension2Message(88)))))) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kMessageSetCorrectExtension2Text)))); +} + +} // namespace +} // namespace conformance +} // namespace protobuf +} // namespace google
diff --git a/conformance/binary_oneof_test.cc b/conformance/binary_oneof_test.cc new file mode 100644 index 0000000..fd4d50b --- /dev/null +++ b/conformance/binary_oneof_test.cc
@@ -0,0 +1,208 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2026 Google LLC. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +// Binary conformance tests checking that a oneof member is parsed correctly +// and that later occurrences of a oneof's members replace earlier ones, for +// every member type in ValidDataOneofTypes(): ValidDataOneof.<TYPE>.<Case> +// (REQUIRED, equivalence, with a binary-output and a JSON-output leg) and +// ValidDataOneofBinary.<TYPE>.<Case> (RECOMMENDED, byte-exact) for the cases +// DefaultValue, NonDefaultValue, MultipleValuesForSameField and +// MultipleValuesForDifferentField. +// +// This holds both legs of the legacy +// BinaryAndJsonConformanceSuiteImpl<M>::TestValidDataForOneofType(); the test +// names and the requests sent to the testee are identical to the legacy ones. +// The oneof merge and zero-value tests live in binary_merge_test.cc and +// binary_oneof_zero_test.cc. + +#include <tuple> + +#include <gmock/gmock.h> +#include <gtest/gtest.h> +#include "absl/strings/str_cat.h" +#include "absl/strings/string_view.h" +#include "conformance/binary_test_util.h" +#include "conformance/binary_wireformat.h" +#include "conformance/matchers.h" +#include "conformance/message_type_fixtures.h" +#include "conformance/test_fixture.h" +#include "conformance/testee.h" +#include "google/protobuf/descriptor.h" + +namespace google { +namespace protobuf { +namespace conformance { +namespace { + +using ::testing::Combine; +using ::testing::ValuesIn; + +// Parameterized over (test message type, member type), exercising the +// message's first oneof member of that type. Reporting the message type +// through MessageUnderTest() makes the base SetUp() skip the editions +// instances when --maximum_edition doesn't cover them. +class ValidDataOneofTest + : public ConformanceTest, + public testing::WithParamInterface< + std::tuple<const Descriptor*, FieldDescriptor::Type>> { + protected: + const Descriptor* message() const { return std::get<0>(GetParam()); } + FieldDescriptor::Type type() const { return std::get<1>(GetParam()); } + + // The member holding the default and the non-default sample value of + // type(). These scan the descriptor (and check-fail if there is no such + // member), hence the function-style names. + Wire DefaultMember() const { + return FieldWithValue(Member(), GetDefaultValue(type())); + } + Wire NonDefaultMember() const { + return FieldWithValue(Member(), GetNonDefaultValue(type())); + } + + // Another member of the same oneof, of a different type, holding its + // type's default value. + Wire OtherMemberValue() const { + const FieldDescriptor* other = + GetFieldForOneofType(*message(), type(), OneofMember::kOfOtherType); + return FieldWithValue(other, GetDefaultValue(other->type())); + } + + // The legacy test name: "<prefix>.<TYPE>.<suffix>", e.g. + // "ValidDataOneof.UINT32.DefaultValue". + std::string TestName(absl::string_view prefix, + absl::string_view suffix) const { + return absl::StrCat(prefix, ".", UpperCaseTypeName(type()), ".", suffix); + } + + private: + const FieldDescriptor* Member() const { + return GetFieldForOneofType(*message(), type()); + } + + // `payload` (a value without a tag) as `field`. + static Wire FieldWithValue(const FieldDescriptor* field, + const Wire& payload) { + return Wire(Tag(FieldNumber(*field), WireTypeForFieldType(field->type())), + payload); + } + + const Descriptor* MessageUnderTest() const override { return message(); } +}; + +// A member set to its default value has presence and must round trip. +TEST_P(ValidDataOneofTest, DefaultValue) { + EXPECT_THAT(Testee(TestName("ValidDataOneof", "DefaultValue")) + .ParseBinary(message(), DefaultMember()) + .SerializeBinary(), + Yields(WhenParsed(EqualsBinaryProto(DefaultMember())))); +} + +// ... in JSON as well as in binary. +TEST_P(ValidDataOneofTest, DefaultValueJson) { + EXPECT_THAT(Testee(TestName("ValidDataOneof", "DefaultValue")) + .ParseBinary(message(), DefaultMember()) + .SerializeJson(), + Yields(WhenParsed(EqualsBinaryProto(DefaultMember())))); +} + +// ... and its binary serialization must be exactly the input. +TEST_P(ValidDataOneofTest, DefaultValueBinary) { + EXPECT_THAT(Testee(kP1, TestName("ValidDataOneofBinary", "DefaultValue")) + .ParseBinary(message(), DefaultMember()) + .SerializeBinary(), + Yields(RawPayload(DefaultMember()))); +} + +// So does a non-default value. +TEST_P(ValidDataOneofTest, NonDefaultValue) { + EXPECT_THAT(Testee(TestName("ValidDataOneof", "NonDefaultValue")) + .ParseBinary(message(), NonDefaultMember()) + .SerializeBinary(), + Yields(WhenParsed(EqualsBinaryProto(NonDefaultMember())))); +} + +// ... in JSON as well as in binary. +TEST_P(ValidDataOneofTest, NonDefaultValueJson) { + EXPECT_THAT(Testee(TestName("ValidDataOneof", "NonDefaultValue")) + .ParseBinary(message(), NonDefaultMember()) + .SerializeJson(), + Yields(WhenParsed(EqualsBinaryProto(NonDefaultMember())))); +} + +// ... and its binary serialization must be exactly the input. +TEST_P(ValidDataOneofTest, NonDefaultValueBinary) { + EXPECT_THAT(Testee(kP1, TestName("ValidDataOneofBinary", "NonDefaultValue")) + .ParseBinary(message(), NonDefaultMember()) + .SerializeBinary(), + Yields(RawPayload(NonDefaultMember()))); +} + +// The same member twice: the last value wins. +TEST_P(ValidDataOneofTest, MultipleValuesForSameField) { + EXPECT_THAT( + Testee(TestName("ValidDataOneof", "MultipleValuesForSameField")) + .ParseBinary(message(), Wire(DefaultMember(), NonDefaultMember())) + .SerializeBinary(), + Yields(WhenParsed(EqualsBinaryProto(NonDefaultMember())))); +} + +// ... in JSON as well as in binary. +TEST_P(ValidDataOneofTest, MultipleValuesForSameFieldJson) { + EXPECT_THAT( + Testee(TestName("ValidDataOneof", "MultipleValuesForSameField")) + .ParseBinary(message(), Wire(DefaultMember(), NonDefaultMember())) + .SerializeJson(), + Yields(WhenParsed(EqualsBinaryProto(NonDefaultMember())))); +} + +// ... and the binary serialization must be exactly the last value. +TEST_P(ValidDataOneofTest, MultipleValuesForSameFieldBinary) { + EXPECT_THAT( + Testee(kP1, + TestName("ValidDataOneofBinary", "MultipleValuesForSameField")) + .ParseBinary(message(), Wire(DefaultMember(), NonDefaultMember())) + .SerializeBinary(), + Yields(RawPayload(NonDefaultMember()))); +} + +// Two different members of the oneof: the last one set is the only one kept. +TEST_P(ValidDataOneofTest, MultipleValuesForDifferentField) { + EXPECT_THAT( + Testee(TestName("ValidDataOneof", "MultipleValuesForDifferentField")) + .ParseBinary(message(), Wire(OtherMemberValue(), NonDefaultMember())) + .SerializeBinary(), + Yields(WhenParsed(EqualsBinaryProto(NonDefaultMember())))); +} + +// ... in JSON as well as in binary. +TEST_P(ValidDataOneofTest, MultipleValuesForDifferentFieldJson) { + EXPECT_THAT( + Testee(TestName("ValidDataOneof", "MultipleValuesForDifferentField")) + .ParseBinary(message(), Wire(OtherMemberValue(), NonDefaultMember())) + .SerializeJson(), + Yields(WhenParsed(EqualsBinaryProto(NonDefaultMember())))); +} + +// ... and the binary serialization must be exactly the last member. +TEST_P(ValidDataOneofTest, MultipleValuesForDifferentFieldBinary) { + EXPECT_THAT( + Testee(kP1, TestName("ValidDataOneofBinary", + "MultipleValuesForDifferentField")) + .ParseBinary(message(), Wire(OtherMemberValue(), NonDefaultMember())) + .SerializeBinary(), + Yields(RawPayload(NonDefaultMember()))); +} + +INSTANTIATE_TEST_SUITE_P(All, ValidDataOneofTest, + Combine(ValuesIn(AllTestMessageTypes()), + ValuesIn(ValidDataOneofTypes())), + TupleParamName<ValidDataOneofTest::ParamType>); + +} // namespace +} // namespace conformance +} // namespace protobuf +} // namespace google
diff --git a/conformance/binary_oneof_zero_test.cc b/conformance/binary_oneof_zero_test.cc new file mode 100644 index 0000000..594261a --- /dev/null +++ b/conformance/binary_oneof_zero_test.cc
@@ -0,0 +1,228 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2026 Google LLC. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +// Binary conformance tests checking that a oneof member set to its default +// value (zero, empty, or an empty message) survives a round trip to binary and +// to JSON: unlike a plain proto3 field, a oneof member always has presence. +// This holds both legs of the legacy +// BinaryAndJsonConformanceSuiteImpl<M>::TestOneofMessage(); the test names and +// the requests sent to the testee are identical to the legacy ones. +// +// The legacy test set the members one after another on a single generated +// message and sent its serialization each time. Since all of them belong to +// the same oneof, each step left only the member just set, so every test here +// sends the wire encoding of just that member. The oneof members have the +// same field numbers (111-119) in every TestAllTypes message. + +#include <gmock/gmock.h> +#include <gtest/gtest.h> +#include "conformance/binary_test_util.h" +#include "conformance/binary_wireformat.h" +#include "conformance/matchers.h" +#include "conformance/message_type_fixtures.h" +#include "conformance/testee.h" + +namespace google { +namespace protobuf { +namespace conformance { +namespace { + +using ::testing::ValuesIn; + +class OneofZeroTest : public MessageTypeConformanceTest { + protected: + // Whether NestedMessage.a has explicit presence (proto2) rather than + // implicit presence (proto3). + bool NestedMessageFieldHasPresence() const { + return message() + ->FindFieldByName("oneof_nested_message") + ->message_type() + ->FindFieldByName("a") + ->has_presence(); + } + + private: + TestPriority DefaultPriority() const override { return kP1; } +}; + +// Field 111 = oneof_uint32. +TEST_P(OneofZeroTest, Uint32) { + EXPECT_THAT(Testee("OneofZeroUint32") + .ParseBinary(message(), VarintField(111, 0)) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(R"pb(oneof_uint32: 0)pb")))); +} + +TEST_P(OneofZeroTest, Uint32Json) { + EXPECT_THAT(Testee("OneofZeroUint32") + .ParseBinary(message(), VarintField(111, 0)) + .SerializeJson(), + Yields(WhenParsed(EqualsTextProto(R"pb(oneof_uint32: 0)pb")))); +} + +// Field 112 = oneof_nested_message, whose field 1 is a. The legacy test set a +// to 0: with explicit presence (proto2) a is serialized and must round trip; +// with implicit presence (proto3) it is indistinguishable from unset, so the +// input is the empty message and only that is expected back. +TEST_P(OneofZeroTest, Message) { + const bool has_presence = NestedMessageFieldHasPresence(); + EXPECT_THAT(Testee("OneofZeroMessage") + .ParseBinary(message(), + has_presence + ? LengthPrefixedField(112, VarintField(1, 0)) + : LengthPrefixedField(112, Wire())) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto( + has_presence ? R"pb(oneof_nested_message: { a: 0 })pb" + : R"pb(oneof_nested_message: {})pb")))); +} + +TEST_P(OneofZeroTest, MessageJson) { + const bool has_presence = NestedMessageFieldHasPresence(); + EXPECT_THAT(Testee("OneofZeroMessage") + .ParseBinary(message(), + has_presence + ? LengthPrefixedField(112, VarintField(1, 0)) + : LengthPrefixedField(112, Wire())) + .SerializeJson(), + Yields(WhenParsed(EqualsTextProto( + has_presence ? R"pb(oneof_nested_message: { a: 0 })pb" + : R"pb(oneof_nested_message: {})pb")))); +} + +// Field 112 = oneof_nested_message. The legacy test set a to 0 and then to 1 +// on the same nested message. +TEST_P(OneofZeroTest, MessageSetTwice) { + EXPECT_THAT( + Testee("OneofZeroMessageSetTwice") + .ParseBinary(message(), LengthPrefixedField(112, VarintField(1, 1))) + .SerializeBinary(), + Yields(WhenParsed( + EqualsTextProto(R"pb(oneof_nested_message: { a: 1 })pb")))); +} + +TEST_P(OneofZeroTest, MessageSetTwiceJson) { + EXPECT_THAT( + Testee("OneofZeroMessageSetTwice") + .ParseBinary(message(), LengthPrefixedField(112, VarintField(1, 1))) + .SerializeJson(), + Yields(WhenParsed( + EqualsTextProto(R"pb(oneof_nested_message: { a: 1 })pb")))); +} + +// Field 113 = oneof_string. +TEST_P(OneofZeroTest, String) { + EXPECT_THAT(Testee("OneofZeroString") + .ParseBinary(message(), LengthPrefixedField(113, "")) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(R"pb(oneof_string: "")pb")))); +} + +TEST_P(OneofZeroTest, StringJson) { + EXPECT_THAT(Testee("OneofZeroString") + .ParseBinary(message(), LengthPrefixedField(113, "")) + .SerializeJson(), + Yields(WhenParsed(EqualsTextProto(R"pb(oneof_string: "")pb")))); +} + +// Field 114 = oneof_bytes. +TEST_P(OneofZeroTest, Bytes) { + EXPECT_THAT(Testee("OneofZeroBytes") + .ParseBinary(message(), LengthPrefixedField(114, "")) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(R"pb(oneof_bytes: "")pb")))); +} + +TEST_P(OneofZeroTest, BytesJson) { + EXPECT_THAT(Testee("OneofZeroBytes") + .ParseBinary(message(), LengthPrefixedField(114, "")) + .SerializeJson(), + Yields(WhenParsed(EqualsTextProto(R"pb(oneof_bytes: "")pb")))); +} + +// Field 115 = oneof_bool. +TEST_P(OneofZeroTest, Bool) { + EXPECT_THAT(Testee("OneofZeroBool") + .ParseBinary(message(), VarintField(115, 0)) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(R"pb(oneof_bool: false)pb")))); +} + +TEST_P(OneofZeroTest, BoolJson) { + EXPECT_THAT(Testee("OneofZeroBool") + .ParseBinary(message(), VarintField(115, 0)) + .SerializeJson(), + Yields(WhenParsed(EqualsTextProto(R"pb(oneof_bool: false)pb")))); +} + +// Field 116 = oneof_uint64. +TEST_P(OneofZeroTest, Uint64) { + EXPECT_THAT(Testee("OneofZeroUint64") + .ParseBinary(message(), VarintField(116, 0)) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(R"pb(oneof_uint64: 0)pb")))); +} + +TEST_P(OneofZeroTest, Uint64Json) { + EXPECT_THAT(Testee("OneofZeroUint64") + .ParseBinary(message(), VarintField(116, 0)) + .SerializeJson(), + Yields(WhenParsed(EqualsTextProto(R"pb(oneof_uint64: 0)pb")))); +} + +// Field 117 = oneof_float. +TEST_P(OneofZeroTest, Float) { + EXPECT_THAT(Testee("OneofZeroFloat") + .ParseBinary(message(), FloatField(117, 0)) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(R"pb(oneof_float: 0)pb")))); +} + +TEST_P(OneofZeroTest, FloatJson) { + EXPECT_THAT(Testee("OneofZeroFloat") + .ParseBinary(message(), FloatField(117, 0)) + .SerializeJson(), + Yields(WhenParsed(EqualsTextProto(R"pb(oneof_float: 0)pb")))); +} + +// Field 118 = oneof_double. +TEST_P(OneofZeroTest, Double) { + EXPECT_THAT(Testee("OneofZeroDouble") + .ParseBinary(message(), DoubleField(118, 0)) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(R"pb(oneof_double: 0)pb")))); +} + +TEST_P(OneofZeroTest, DoubleJson) { + EXPECT_THAT(Testee("OneofZeroDouble") + .ParseBinary(message(), DoubleField(118, 0)) + .SerializeJson(), + Yields(WhenParsed(EqualsTextProto(R"pb(oneof_double: 0)pb")))); +} + +// Field 119 = oneof_enum; FOO is the zero value of NestedEnum. +TEST_P(OneofZeroTest, Enum) { + EXPECT_THAT(Testee("OneofZeroEnum") + .ParseBinary(message(), VarintField(119, 0)) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(R"pb(oneof_enum: FOO)pb")))); +} + +TEST_P(OneofZeroTest, EnumJson) { + EXPECT_THAT(Testee("OneofZeroEnum") + .ParseBinary(message(), VarintField(119, 0)) + .SerializeJson(), + Yields(WhenParsed(EqualsTextProto(R"pb(oneof_enum: FOO)pb")))); +} + +INSTANTIATE_TEST_SUITE_P(All, OneofZeroTest, ValuesIn(AllTestMessageTypes()), + MessageTypeParamName); + +} // namespace +} // namespace conformance +} // namespace protobuf +} // namespace google
diff --git a/conformance/binary_performance_test.cc b/conformance/binary_performance_test.cc new file mode 100644 index 0000000..5fb55b9 --- /dev/null +++ b/conformance/binary_performance_test.cc
@@ -0,0 +1,161 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2026 Google LLC. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +// Binary performance conformance tests: payloads with tens of thousands of +// unknown fields, or of submessages that have to be merged into one. Part of +// the performance suite (see conformance.bzl). This replaces the legacy +// BinaryAndJsonConformanceSuiteImpl<M>::RunBinaryPerformanceTests() +// and its helpers; the test names and the requests sent to the testee are +// identical to the legacy ones. + +#include <cstddef> +#include <cstdint> +#include <string> +#include <tuple> + +#include <gmock/gmock.h> +#include <gtest/gtest.h> +#include "absl/strings/str_cat.h" +#include "absl/strings/string_view.h" +#include "conformance/binary_test_util.h" +#include "conformance/binary_wireformat.h" +#include "conformance/matchers.h" +#include "conformance/message_type_fixtures.h" +#include "conformance/test_fixture.h" +#include "conformance/testee.h" +#include "google/protobuf/descriptor.h" + +namespace google { +namespace protobuf { +namespace conformance { +namespace { + +using ::testing::Combine; +using ::testing::ValuesIn; + +// The number of repetitions to use for performance tests. +// Corresponds approx to 500KB wireformat bytes. +constexpr int kPerformanceRepeatCount = 50000; + +// Field numbers no test message defines. +constexpr uint32_t kUnknownFieldNumber = 666; +constexpr uint32_t kOtherUnknownFieldNumber = 667; + +// Field 27 of every test message is recursive_message, a submessage of the +// message's own type. +constexpr uint32_t kRecursiveMessageField = 27; + +// The field types the merge tests are instantiated with, in the legacy order. +constexpr FieldDescriptor::Type kMergeTypes[] = { + FieldDescriptor::TYPE_BOOL, FieldDescriptor::TYPE_DOUBLE, + FieldDescriptor::TYPE_FLOAT, FieldDescriptor::TYPE_UINT32, + FieldDescriptor::TYPE_UINT64, FieldDescriptor::TYPE_STRING, + FieldDescriptor::TYPE_BYTES, +}; + +// `wire` repeated `count` times. +Wire Repeat(const Wire& wire, int count) { + std::string repeated; + repeated.reserve(wire.size() * static_cast<size_t>(count)); + for (int i = 0; i < count; ++i) { + absl::StrAppend(&repeated, wire.data()); + } + return Wire(repeated); +} + +// Parameterized over the test message type only. +class UnknownFieldsPerformanceTest : public MessageTypeConformanceTest { + private: + TestPriority DefaultPriority() const override { return kP1; } +}; + +// Many unknown fields alternating between two field numbers must be preserved +// byte for byte. +TEST_P(UnknownFieldsPerformanceTest, AlternatingUnknownFields) { + Wire input = Repeat(Wire(VarintField(kUnknownFieldNumber, 1234), + VarintField(kOtherUnknownFieldNumber, 5678)), + kPerformanceRepeatCount); + EXPECT_THAT(Testee("TestBinaryPerformanceForAlternatingUnknownFields") + .ParseBinary(message(), input) + .SerializeBinary(), + Yields(RawPayload(input))); +} + +INSTANTIATE_TEST_SUITE_P(All, UnknownFieldsPerformanceTest, + ValuesIn(AllTestMessageTypes()), MessageTypeParamName); + +// Parameterized over (test message type, field type): many occurrences of +// recursive_message, each holding one field, must be merged into a single +// recursive_message holding all of them, in order. +class MergeMessagePerformanceTest + : public ConformanceTest, + public testing::WithParamInterface< + std::tuple<const Descriptor*, FieldDescriptor::Type>> { + protected: + const Descriptor* message() const { return std::get<0>(GetParam()); } + FieldDescriptor::Type type() const { return std::get<1>(GetParam()); } + + // The legacy test name: "<prefix>.<TYPE>", e.g. + // "TestBinaryPerformanceMergeMessageWithRepeatedFieldForType.BOOL". + std::string TestName(absl::string_view prefix) const { + return absl::StrCat(prefix, ".", UpperCaseTypeName(type())); + } + + private: + TestPriority DefaultPriority() const override { return kP1; } + const Descriptor* MessageUnderTest() const override { return message(); } +}; + +// The input of a merge performance test: recursive_message holding `field` (a +// single encoded field, tag and value), kPerformanceRepeatCount times. +Wire RepeatedRecursiveMessageInput(const Wire& field) { + return Repeat(LengthPrefixedField(kRecursiveMessageField, field), + kPerformanceRepeatCount); +} + +// What a parser must merge RepeatedRecursiveMessageInput(field) into: one +// recursive_message holding kPerformanceRepeatCount occurrences of `field`. +Wire MergedRecursiveMessage(const Wire& field) { + return LengthPrefixedField(kRecursiveMessageField, + Repeat(field, kPerformanceRepeatCount)); +} + +// The field is the message's unpacked repeated field of the type. +TEST_P(MergeMessagePerformanceTest, WithRepeatedField) { + const FieldDescriptor* field = GetFieldForType( + *message(), type(), /*repeated=*/true, Packedness::kUnpacked); + Wire entry(Tag(FieldNumber(*field), WireTypeForFieldType(type())), + GetNonDefaultValue(type())); + EXPECT_THAT( + Testee( + TestName("TestBinaryPerformanceMergeMessageWithRepeatedFieldForType")) + .ParseBinary(message(), RepeatedRecursiveMessageInput(entry)) + .SerializeBinary(), + Yields(RawPayload(MergedRecursiveMessage(entry)))); +} + +// The field is unknown, with the wire type of the type. +TEST_P(MergeMessagePerformanceTest, WithUnknownField) { + Wire entry(Tag(kUnknownFieldNumber, WireTypeForFieldType(type())), + GetNonDefaultValue(type())); + EXPECT_THAT( + Testee( + TestName("TestBinaryPerformanceMergeMessageWithUnknownFieldForType")) + .ParseBinary(message(), RepeatedRecursiveMessageInput(entry)) + .SerializeBinary(), + Yields(RawPayload(MergedRecursiveMessage(entry)))); +} + +INSTANTIATE_TEST_SUITE_P( + All, MergeMessagePerformanceTest, + Combine(ValuesIn(AllTestMessageTypes()), ValuesIn(kMergeTypes)), + TupleParamName<MergeMessagePerformanceTest::ParamType>); + +} // namespace +} // namespace conformance +} // namespace protobuf +} // namespace google
diff --git a/conformance/binary_premature_eof_test.cc b/conformance/binary_premature_eof_test.cc new file mode 100644 index 0000000..3a927f9 --- /dev/null +++ b/conformance/binary_premature_eof_test.cc
@@ -0,0 +1,249 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2026 Google LLC. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +// Binary conformance tests checking that a payload truncated before or inside +// a field's value is rejected. This replaces the legacy +// BinaryAndJsonConformanceSuiteImpl<M>::TestPrematureEOFForType(), which +// RunAllTests() called for every field type except groups; the test names and +// the requests sent to the testee are identical to the legacy ones. + +#include <cstdint> +#include <tuple> + +#include <gmock/gmock.h> +#include <gtest/gtest.h> +#include "absl/strings/str_cat.h" +#include "absl/strings/string_view.h" +#include "conformance/binary_test_util.h" +#include "conformance/binary_wireformat.h" +#include "conformance/matchers.h" +#include "conformance/message_type_fixtures.h" +#include "conformance/test_fixture.h" +#include "conformance/testee.h" +#include "google/protobuf/descriptor.h" +#include "google/protobuf/port.h" + +namespace google { +namespace protobuf { +namespace conformance { +namespace { + +using ::testing::Combine; +using ::testing::ValuesIn; + +// A field number no test message defines. +constexpr uint32_t kUnknownFieldNumber = 666; + +// An incomplete value for each wire type, exactly as in the legacy suite. +absl::string_view IncompleteValue(WireType wire_type) { + switch (wire_type) { + case WireType::kVarint: + return "\x80"; + case WireType::kFixed64: + return "abcdefg"; + case WireType::kLengthPrefixed: + return "\x80"; // Partial length. + case WireType::kStartGroup: + case WireType::kEndGroup: + return ""; // No value required. + case WireType::kFixed32: + return "abc"; + case WireType::kInvalid: + break; + } + google::protobuf::internal::Unreachable(); +} + +// Parameterized over (test message type, field type). The three test suites +// below only differ in the field types they are instantiated with, so that no +// test ever has to skip itself as "not applicable". Reporting the message +// type through MessageUnderTest() makes the base SetUp() skip the editions +// instances when --maximum_edition doesn't cover them. +class PrematureEofTest + : public ConformanceTest, + public testing::WithParamInterface< + std::tuple<const Descriptor*, FieldDescriptor::Type>> { + protected: + const Descriptor* message() const { return std::get<0>(GetParam()); } + FieldDescriptor::Type type() const { return std::get<1>(GetParam()); } + WireType wire_type() const { return WireTypeForFieldType(type()); } + absl::string_view incomplete() const { return IncompleteValue(wire_type()); } + + // The message's singular and repeated field of type(). These scan the + // descriptor (and check-fail if there is no such field), hence the + // function-style names. + const FieldDescriptor* Field() const { + return GetFieldForType(*message(), type(), /*repeated=*/false); + } + const FieldDescriptor* RepeatedField() const { + return GetFieldForType(*message(), type(), /*repeated=*/true); + } + + // The legacy test name: "<prefix>.<TYPE>", e.g. + // "PrematureEofBeforeKnownNonRepeatedValue.INT32". + std::string TestName(absl::string_view prefix) const { + return absl::StrCat(prefix, ".", UpperCaseTypeName(type())); + } + + private: + const Descriptor* MessageUnderTest() const override { return message(); } +}; + +TEST_P(PrematureEofTest, BeforeKnownNonRepeatedValue) { + EXPECT_THAT( + Testee(TestName("PrematureEofBeforeKnownNonRepeatedValue")) + .ParseBinary(message(), Tag(FieldNumber(*Field()), wire_type())) + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(PrematureEofTest, BeforeKnownRepeatedValue) { + EXPECT_THAT(Testee(TestName("PrematureEofBeforeKnownRepeatedValue")) + .ParseBinary(message(), + Tag(FieldNumber(*RepeatedField()), wire_type())) + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(PrematureEofTest, BeforeUnknownValue) { + EXPECT_THAT(Testee(TestName("PrematureEofBeforeUnknownValue")) + .ParseBinary(message(), Tag(kUnknownFieldNumber, wire_type())) + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(PrematureEofTest, InsideKnownNonRepeatedValue) { + EXPECT_THAT( + Testee(TestName("PrematureEofInsideKnownNonRepeatedValue")) + .ParseBinary(message(), Wire(Tag(FieldNumber(*Field()), wire_type()), + incomplete())) + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(PrematureEofTest, InsideKnownRepeatedValue) { + EXPECT_THAT( + Testee(TestName("PrematureEofInsideKnownRepeatedValue")) + .ParseBinary(message(), + Wire(Tag(FieldNumber(*RepeatedField()), wire_type()), + incomplete())) + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(PrematureEofTest, InsideUnknownValue) { + EXPECT_THAT( + Testee(TestName("PrematureEofInsideUnknownValue")) + .ParseBinary(message(), Wire(Tag(kUnknownFieldNumber, wire_type()), + incomplete())) + .ParseOnly(), + Yields(IsParseError())); +} + +INSTANTIATE_TEST_SUITE_P(All, PrematureEofTest, + Combine(ValuesIn(AllTestMessageTypes()), + ValuesIn(AllFieldTypesExceptGroup())), + TupleParamName<PrematureEofTest::ParamType>); + +// A length prefix that promises more data than follows. +class PrematureEofInDelimitedDataTest : public PrematureEofTest {}; + +TEST_P(PrematureEofInDelimitedDataTest, ForKnownNonRepeatedValue) { + EXPECT_THAT( + Testee(TestName("PrematureEofInDelimitedDataForKnownNonRepeatedValue")) + .ParseBinary(message(), Wire(Tag(FieldNumber(*Field()), + WireType::kLengthPrefixed), + Varint(1))) + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(PrematureEofInDelimitedDataTest, ForKnownRepeatedValue) { + EXPECT_THAT( + Testee(TestName("PrematureEofInDelimitedDataForKnownRepeatedValue")) + .ParseBinary(message(), Wire(Tag(FieldNumber(*RepeatedField()), + WireType::kLengthPrefixed), + Varint(1))) + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(PrematureEofInDelimitedDataTest, ForUnknownValue) { + EXPECT_THAT(Testee(TestName("PrematureEofInDelimitedDataForUnknownValue")) + .ParseBinary(message(), Wire(Tag(kUnknownFieldNumber, + WireType::kLengthPrefixed), + Varint(1))) + .ParseOnly(), + Yields(IsParseError())); +} + +INSTANTIATE_TEST_SUITE_P( + All, PrematureEofInDelimitedDataTest, + Combine(ValuesIn(AllTestMessageTypes()), + ValuesIn(LengthDelimitedFieldTypes())), + TupleParamName<PrematureEofInDelimitedDataTest::ParamType>); + +// A packed repeated field whose payload is truncated. +class PrematureEofInPackedFieldTest : public PrematureEofTest {}; + +// The packed region ends in the middle of one of its values. +// TODO: also run with valid data appended (legacy TODO). +TEST_P(PrematureEofInPackedFieldTest, PackedFieldValue) { + EXPECT_THAT(Testee(TestName("PrematureEofInPackedFieldValue")) + .ParseBinary(message(), + LengthPrefixedField( + FieldNumber(*RepeatedField()), incomplete())) + .ParseOnly(), + Yields(IsParseError())); +} + +// The payload ends before the packed region does. +TEST_P(PrematureEofInPackedFieldTest, PackedField) { + EXPECT_THAT( + Testee(TestName("PrematureEofInPackedField")) + .ParseBinary(message(), Wire(Tag(FieldNumber(*RepeatedField()), + WireType::kLengthPrefixed), + Varint(1))) + .ParseOnly(), + Yields(IsParseError())); +} + +INSTANTIATE_TEST_SUITE_P( + All, PrematureEofInPackedFieldTest, + Combine(ValuesIn(AllTestMessageTypes()), ValuesIn(PackableFieldTypes())), + TupleParamName<PrematureEofInPackedFieldTest::ParamType>); + +// A submessage that ends in the middle of one of its own values. The legacy +// test only ran this for TYPE_MESSAGE, so the suite is parameterized over the +// message type alone and, as in the other suites here, the gtest name is the +// rest of the conformance name: its legacy ".MESSAGE" suffix. +using PrematureEofInSubmessageValueTest = MessageTypeConformanceTest; + +// TODO: also run with valid data appended (legacy TODO). +TEST_P(PrematureEofInSubmessageValueTest, Message) { + const FieldDescriptor* field = GetFieldForType( + *message(), FieldDescriptor::TYPE_MESSAGE, /*repeated=*/false); + // Field 5 is what the legacy test used: it (accidentally) passed + // WireFormatLite::TYPE_INT32 == 5 as the field number. The bytes are kept + // identical. + Wire incomplete_submessage(Tag(5, WireType::kVarint), + IncompleteValue(WireType::kVarint)); + EXPECT_THAT( + Testee("PrematureEofInSubmessageValue.MESSAGE") + .ParseBinary(message(), LengthPrefixedField(FieldNumber(*field), + incomplete_submessage)) + .ParseOnly(), + Yields(IsParseError())); +} + +INSTANTIATE_TEST_SUITE_P(All, PrematureEofInSubmessageValueTest, + ValuesIn(AllTestMessageTypes()), MessageTypeParamName); + +} // namespace +} // namespace conformance +} // namespace protobuf +} // namespace google
diff --git a/conformance/binary_recursion_limit_test.cc b/conformance/binary_recursion_limit_test.cc new file mode 100644 index 0000000..4d3bd6d --- /dev/null +++ b/conformance/binary_recursion_limit_test.cc
@@ -0,0 +1,85 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2026 Google LLC. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +// Binary performance conformance tests checking that absurdly deeply nested +// messages are rejected rather than parsed. Part of the performance suite +// (see conformance.bzl). This replaces the legacy suite-level +// BinaryAndJsonConformanceSuite::RunRecursionLimitTests(); the test names and +// the requests sent to the testee are identical to the legacy ones. +// +// Unlike the legacy suite, which ran the TestAllTypesEdition2023 tests here +// regardless of --maximum_edition, these are edition-gated like every other +// editions test (through MessageUnderTest(), see message_type_fixtures.h). + +#include <gmock/gmock.h> +#include <gtest/gtest.h> +#include "conformance/matchers.h" +#include "conformance/message_type_fixtures.h" +#include "conformance/recursion_limit_payloads.h" +#include "conformance/test_fixture.h" +#include "conformance/test_protos/test_messages_edition2023.pb.h" +#include "conformance/testee.h" +#include "google/protobuf/test_messages_proto2.pb.h" + +namespace google { +namespace protobuf { +namespace conformance { +namespace { + +using ::protobuf_test_messages::editions::TestAllTypesEdition2023; +using ::protobuf_test_messages::proto2::TestAllTypesProto2; + +// The default recursion limit is 100 for most languages. 10,000 for golang. +// We use a larger number here for test. +constexpr int kMapDepth = 20000; +constexpr int kMapStringKeyDepth = 10000; +constexpr int kMessageSetDepth = 20000; + +// The payloads are built directly on the wire, see recursion_limit_payloads.h. + +// The TestAllTypesProto2 test, which every --maximum_edition covers. +class RecursionLimitPerformanceTest : public ConformanceTest { + private: + TestPriority DefaultPriority() const override { return kP1; } +}; + +// The TestAllTypesEdition2023 tests, which the base SetUp() skips when +// --maximum_edition doesn't cover editions. +class Edition2023RecursionLimitPerformanceTest + : public Edition2023ConformanceTest { + private: + TestPriority DefaultPriority() const override { return kP1; } +}; + +TEST_F(Edition2023RecursionLimitPerformanceTest, Map) { + EXPECT_THAT(Testee("EnforceDepthLimit.Map") + .ParseBinary(TestAllTypesEdition2023::descriptor(), + DeepMapPayload(kMapDepth)) + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_F(Edition2023RecursionLimitPerformanceTest, MapStringKey) { + EXPECT_THAT(Testee("EnforceDepthLimit.MapStringKey") + .ParseBinary(TestAllTypesEdition2023::descriptor(), + DeepMapStringKeyPayload(kMapStringKeyDepth)) + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_F(RecursionLimitPerformanceTest, MessageSetExtension) { + EXPECT_THAT(Testee("EnforceDepthLimit.MessageSetExtension") + .ParseBinary(TestAllTypesProto2::descriptor(), + DeepMessageSetPayload(kMessageSetDepth)) + .ParseOnly(), + Yields(IsParseError())); +} + +} // namespace +} // namespace conformance +} // namespace protobuf +} // namespace google
diff --git a/conformance/binary_test_util.cc b/conformance/binary_test_util.cc index c083a80..8cd7830 100644 --- a/conformance/binary_test_util.cc +++ b/conformance/binary_test_util.cc
@@ -7,12 +7,421 @@ #include "conformance/binary_test_util.h" +#include <cstdint> +#include <limits> +#include <string> +#include <vector> + +#include "absl/base/nullability.h" +#include "absl/log/absl_check.h" +#include "absl/log/absl_log.h" +#include "absl/strings/ascii.h" +#include "absl/strings/str_cat.h" +#include "absl/strings/string_view.h" #include "absl/types/span.h" +#include "conformance/binary_wireformat.h" +#include "conformance/naming.h" #include "google/protobuf/descriptor.h" +#include "editions/golden/test_messages_proto2_editions.pb.h" +#include "editions/golden/test_messages_proto3_editions.pb.h" +#include "google/protobuf/port.h" +#include "google/protobuf/test_messages_proto2.pb.h" +#include "google/protobuf/test_messages_proto3.pb.h" +#include "google/protobuf/wire_format_lite.h" namespace google { namespace protobuf { namespace conformance { +const FieldDescriptor* absl_nonnull GetFieldForType(const Descriptor& message, + FieldDescriptor::Type type, + bool repeated, + Packedness packedness) { + for (int i = 0; i < message.field_count(); i++) { + const FieldDescriptor* field = message.field(i); + if (field->type() == type && field->is_repeated() == repeated) { + if ((packedness == Packedness::kPacked && !field->is_packed()) || + (packedness == Packedness::kUnpacked && field->is_packed())) { + continue; + } + return field; + } + } + + absl::string_view packed_string; + const absl::string_view repeated_string = + repeated ? "Repeated " : "Singular "; + if (packedness == Packedness::kPacked) { + packed_string = "Packed "; + } + if (packedness == Packedness::kUnpacked) { + packed_string = "Unpacked "; + } + ABSL_LOG(FATAL) << "Couldn't find field with type: " << repeated_string + << packed_string << FieldDescriptor::TypeName(type) << " for " + << message.full_name(); +} + +const FieldDescriptor* absl_nonnull GetFieldForMapType( + const Descriptor& message, FieldDescriptor::Type key_type, + FieldDescriptor::Type value_type) { + for (int i = 0; i < message.field_count(); i++) { + const FieldDescriptor* field = message.field(i); + if (!field->is_map()) continue; + const Descriptor* entry = field->message_type(); + if (entry->map_key()->type() == key_type && + entry->map_value()->type() == value_type) { + return field; + } + } + ABSL_LOG(FATAL) << "Couldn't find map field with type: " + << FieldDescriptor::TypeName(key_type) << " and " + << FieldDescriptor::TypeName(value_type) << " for " + << message.full_name(); +} + +const FieldDescriptor* absl_nonnull GetFieldForOneofType( + const Descriptor& message, FieldDescriptor::Type type, OneofMember member) { + const bool of_type = member == OneofMember::kOfType; + for (int i = 0; i < message.field_count(); i++) { + const FieldDescriptor* field = message.field(i); + if (field->real_containing_oneof() != nullptr && + (field->type() == type) == of_type) { + return field; + } + } + ABSL_LOG(FATAL) << "Couldn't find oneof field with type" + << (of_type ? ": " : " other than: ") + << FieldDescriptor::TypeName(type) << " for " + << message.full_name(); +} + +WireType WireTypeForFieldType(FieldDescriptor::Type type) { + // WireType's enumerators are defined as WireFormatLite's, so the cast is + // value-preserving. + return static_cast<WireType>( + ::google::protobuf::internal::WireFormatLite::WireTypeForFieldType( + static_cast<::google::protobuf::internal::WireFormatLite::FieldType>(type))); +} + +std::string UpperCaseTypeName(FieldDescriptor::Type type) { + return absl::AsciiStrToUpper(FieldDescriptor::TypeName(type)); +} + +Wire GetDefaultValue(FieldDescriptor::Type type) { + switch (type) { + case FieldDescriptor::TYPE_INT32: + case FieldDescriptor::TYPE_INT64: + case FieldDescriptor::TYPE_UINT32: + case FieldDescriptor::TYPE_UINT64: + case FieldDescriptor::TYPE_ENUM: + case FieldDescriptor::TYPE_BOOL: + return Varint(0); + case FieldDescriptor::TYPE_SINT32: + return SInt32(0); + case FieldDescriptor::TYPE_SINT64: + return SInt64(0); + case FieldDescriptor::TYPE_FIXED32: + case FieldDescriptor::TYPE_SFIXED32: + return Fixed32(0); + case FieldDescriptor::TYPE_FIXED64: + case FieldDescriptor::TYPE_SFIXED64: + return Fixed64(0); + case FieldDescriptor::TYPE_FLOAT: + return Float(0); + case FieldDescriptor::TYPE_DOUBLE: + return Double(0); + case FieldDescriptor::TYPE_STRING: + case FieldDescriptor::TYPE_BYTES: + case FieldDescriptor::TYPE_MESSAGE: + return LengthPrefixed(""); + case FieldDescriptor::TYPE_GROUP: + return Wire(); + } + google::protobuf::internal::Unreachable(); +} + +Wire GetNonDefaultValue(FieldDescriptor::Type type) { + switch (type) { + case FieldDescriptor::TYPE_INT32: + case FieldDescriptor::TYPE_INT64: + case FieldDescriptor::TYPE_UINT32: + case FieldDescriptor::TYPE_UINT64: + case FieldDescriptor::TYPE_ENUM: + case FieldDescriptor::TYPE_BOOL: + return Varint(1); + case FieldDescriptor::TYPE_SINT32: + return SInt32(1); + case FieldDescriptor::TYPE_SINT64: + return SInt64(1); + case FieldDescriptor::TYPE_FIXED32: + case FieldDescriptor::TYPE_SFIXED32: + return Fixed32(1); + case FieldDescriptor::TYPE_FIXED64: + case FieldDescriptor::TYPE_SFIXED64: + return Fixed64(1); + case FieldDescriptor::TYPE_FLOAT: + return Float(1); + case FieldDescriptor::TYPE_DOUBLE: + return Double(1); + case FieldDescriptor::TYPE_STRING: + case FieldDescriptor::TYPE_BYTES: + return LengthPrefixed("a"); + case FieldDescriptor::TYPE_MESSAGE: + return LengthPrefixed(VarintField(1, 1234)); + case FieldDescriptor::TYPE_GROUP: + return Wire(); + } + google::protobuf::internal::Unreachable(); +} + +bool IsDefaultValue(FieldDescriptor::Type type, const Wire& payload) { + if (type == FieldDescriptor::TYPE_MESSAGE || + type == FieldDescriptor::TYPE_GROUP) { + return false; + } + return payload == GetDefaultValue(type); +} + +bool HasImplicitPresence(const Descriptor& message) { + return !GetFieldForType(message, FieldDescriptor::TYPE_INT32, + /*repeated=*/false) + ->has_presence(); +} + +absl::Span<const ValidDataCase> ValidDataCases(FieldDescriptor::Type type) { + constexpr int64_t kInt64Min = std::numeric_limits<int64_t>::min(); + constexpr int64_t kInt64Max = std::numeric_limits<int64_t>::max(); + constexpr uint64_t kUint64Max = std::numeric_limits<uint64_t>::max(); + constexpr int32_t kInt32Min = std::numeric_limits<int32_t>::min(); + constexpr int32_t kInt32Max = std::numeric_limits<int32_t>::max(); + constexpr uint32_t kUint32Max = std::numeric_limits<uint32_t>::max(); + + // The tables are the legacy suite's, verbatim (including the entries whose + // input isn't canonical: over-long varints, out-of-range varints that must + // be truncated or clamped, and non-zero bools). Their order is part of the + // test names, so entries may only be appended. + switch (type) { + case FieldDescriptor::TYPE_DOUBLE: { + static const auto* const kCases = new std::vector<ValidDataCase>{ + {Double(0), Double(0)}, + {Double(0.1), Double(0.1)}, + {Double(1.7976931348623157e+308), Double(1.7976931348623157e+308)}, + {Double(2.22507385850720138309e-308), + Double(2.22507385850720138309e-308)}, + }; + return *kCases; + } + case FieldDescriptor::TYPE_FLOAT: { + static const auto* const kCases = new std::vector<ValidDataCase>{ + {Float(0), Float(0)}, + {Float(0.1), Float(0.1)}, + {Float(1.00000075e-36), Float(1.00000075e-36)}, + {Float(3.402823e+38), Float(3.402823e+38)}, // 3.40282347e+38 + {Float(1.17549435e-38f), Float(1.17549435e-38)}, + }; + return *kCases; + } + case FieldDescriptor::TYPE_INT64: { + static const auto* const kCases = new std::vector<ValidDataCase>{ + {Varint(0), Varint(0)}, + {Varint(12345), Varint(12345)}, + {Varint(kInt64Max), Varint(kInt64Max)}, + {Varint(kInt64Min), Varint(kInt64Min)}, + }; + return *kCases; + } + case FieldDescriptor::TYPE_UINT64: { + static const auto* const kCases = new std::vector<ValidDataCase>{ + {Varint(0), Varint(0)}, + {Varint(12345), Varint(12345)}, + {Varint(kUint64Max), Varint(kUint64Max)}, + }; + return *kCases; + } + case FieldDescriptor::TYPE_INT32: { + static const auto* const kCases = new std::vector<ValidDataCase>{ + {Varint(0), Varint(0)}, + {Varint(12345), Varint(12345)}, + {LongVarint(12345, 2), Varint(12345)}, + {LongVarint(12345, 7), Varint(12345)}, + {Varint(kInt32Max), Varint(kInt32Max)}, + {Varint(kInt32Min), Varint(kInt32Min)}, + {Varint(1LL << 33), Varint(0)}, + {Varint((1LL << 33) - 1), Varint(-1)}, + {Varint(kInt64Max), Varint(-1)}, + {Varint(kInt64Min + 1), Varint(1)}, + }; + return *kCases; + } + case FieldDescriptor::TYPE_UINT32: { + static const auto* const kCases = new std::vector<ValidDataCase>{ + {Varint(0), Varint(0)}, + {Varint(12345), Varint(12345)}, + {LongVarint(12345, 2), Varint(12345)}, + {LongVarint(12345, 7), Varint(12345)}, + {Varint(kUint32Max), Varint(kUint32Max)}, // UINT32_MAX + {Varint(1LL << 33), Varint(0)}, + {Varint((1LL << 33) + 1), Varint(1)}, + {Varint((1LL << 33) - 1), Varint((1LL << 32) - 1)}, + {Varint(kInt64Max), Varint((1LL << 32) - 1)}, + {Varint(kInt64Min + 1), Varint(1)}, + }; + return *kCases; + } + case FieldDescriptor::TYPE_FIXED64: { + static const auto* const kCases = new std::vector<ValidDataCase>{ + {Fixed64(0), Fixed64(0)}, + {Fixed64(12345), Fixed64(12345)}, + {Fixed64(kUint64Max), Fixed64(kUint64Max)}, + }; + return *kCases; + } + case FieldDescriptor::TYPE_FIXED32: { + static const auto* const kCases = new std::vector<ValidDataCase>{ + {Fixed32(0), Fixed32(0)}, + {Fixed32(12345), Fixed32(12345)}, + {Fixed32(kUint32Max), Fixed32(kUint32Max)}, // UINT32_MAX + }; + return *kCases; + } + case FieldDescriptor::TYPE_SFIXED64: { + static const auto* const kCases = new std::vector<ValidDataCase>{ + {Fixed64(0), Fixed64(0)}, + {Fixed64(12345), Fixed64(12345)}, + {Fixed64(kInt64Max), Fixed64(kInt64Max)}, + {Fixed64(kInt64Min), Fixed64(kInt64Min)}, + }; + return *kCases; + } + case FieldDescriptor::TYPE_SFIXED32: { + static const auto* const kCases = new std::vector<ValidDataCase>{ + {Fixed32(0), Fixed32(0)}, + {Fixed32(12345), Fixed32(12345)}, + {Fixed32(kInt32Max), Fixed32(kInt32Max)}, + {Fixed32(kInt32Min), Fixed32(kInt32Min)}, + }; + return *kCases; + } + case FieldDescriptor::TYPE_BOOL: { + // Bools should be serialized as 0 for false and 1 for true. Parsers + // should also interpret any nonzero value as true. + // clang-format off + static const auto* const kCases = new std::vector<ValidDataCase>{ + {Varint(0), Varint(0)}, + {Varint(1), Varint(1)}, + {Varint(-1), Varint(1)}, + {Varint(12345678), Varint(1)}, + {Varint(1LL << 33), Varint(1)}, + {Varint(kInt64Max), Varint(1)}, + {Varint(kInt64Min), Varint(1)}, + }; + // clang-format on + return *kCases; + } + case FieldDescriptor::TYPE_SINT32: { + static const auto* const kCases = new std::vector<ValidDataCase>{ + {SInt32(0), SInt32(0)}, + {SInt32(12345), SInt32(12345)}, + {SInt32(kInt32Max), SInt32(kInt32Max)}, + {SInt32(kInt32Min), SInt32(kInt32Min)}, + {SInt64(kInt32Max + 2LL), SInt32(1)}, + }; + return *kCases; + } + case FieldDescriptor::TYPE_SINT64: { + static const auto* const kCases = new std::vector<ValidDataCase>{ + {SInt64(0), SInt64(0)}, + {SInt64(12345), SInt64(12345)}, + {SInt64(kInt64Max), SInt64(kInt64Max)}, + {SInt64(kInt64Min), SInt64(kInt64Min)}, + }; + return *kCases; + } + case FieldDescriptor::TYPE_STRING: { + static const auto* const kCases = new std::vector<ValidDataCase>{ + {LengthPrefixed(""), LengthPrefixed("")}, + {LengthPrefixed("Hello world!"), LengthPrefixed("Hello world!")}, + {LengthPrefixed("\'\"\?\\\a\b\f\n\r\t\v"), + LengthPrefixed("\'\"\?\\\a\b\f\n\r\t\v")}, // escape + // U+8C37 U+6B4C ("Google" in Chinese), as UTF-8. + {LengthPrefixed("\xE8\xB0\xB7\xE6\xAD\x8C"), + LengthPrefixed("\xE8\xB0\xB7\xE6\xAD\x8C")}, + // U+1F601 (grinning face with smiling eyes), as UTF-8. + {LengthPrefixed("\xF0\x9F\x98\x81"), + LengthPrefixed("\xF0\x9F\x98\x81")}, + }; + return *kCases; + } + case FieldDescriptor::TYPE_BYTES: { + static const auto* const kCases = new std::vector<ValidDataCase>{ + {LengthPrefixed(""), LengthPrefixed("")}, + {LengthPrefixed("Hello world!"), LengthPrefixed("Hello world!")}, + {LengthPrefixed("\x01\x02"), LengthPrefixed("\x01\x02")}, + {LengthPrefixed("\xfb"), LengthPrefixed("\xfb")}, + }; + return *kCases; + } + case FieldDescriptor::TYPE_ENUM: { + // clang-format off + static const auto* const kCases = new std::vector<ValidDataCase>{ + {Varint(0), Varint(0)}, + {Varint(1), Varint(1)}, + {Varint(2), Varint(2)}, + {Varint(-1), Varint(-1)}, + {Varint(kInt64Max), Varint(-1)}, + {Varint(kInt64Min + 1), Varint(1)}, + }; + // clang-format on + return *kCases; + } + case FieldDescriptor::TYPE_MESSAGE: { + static const auto* const kCases = new std::vector<ValidDataCase>{ + {LengthPrefixed(""), LengthPrefixed("")}, + {LengthPrefixed(VarintField(1, 1234)), + LengthPrefixed(VarintField(1, 1234))}, + }; + return *kCases; + } + case FieldDescriptor::TYPE_GROUP: + break; + } + ABSL_LOG(FATAL) << "No valid-data cases for field type " + << FieldDescriptor::TypeName(type); +} + +absl::Span<const MapType> ValidDataMapTypes() { + static constexpr MapType kTypes[] = { + {FieldDescriptor::TYPE_INT32, FieldDescriptor::TYPE_INT32}, + {FieldDescriptor::TYPE_INT64, FieldDescriptor::TYPE_INT64}, + {FieldDescriptor::TYPE_UINT32, FieldDescriptor::TYPE_UINT32}, + {FieldDescriptor::TYPE_UINT64, FieldDescriptor::TYPE_UINT64}, + {FieldDescriptor::TYPE_SINT32, FieldDescriptor::TYPE_SINT32}, + {FieldDescriptor::TYPE_SINT64, FieldDescriptor::TYPE_SINT64}, + {FieldDescriptor::TYPE_FIXED32, FieldDescriptor::TYPE_FIXED32}, + {FieldDescriptor::TYPE_FIXED64, FieldDescriptor::TYPE_FIXED64}, + {FieldDescriptor::TYPE_SFIXED32, FieldDescriptor::TYPE_SFIXED32}, + {FieldDescriptor::TYPE_SFIXED64, FieldDescriptor::TYPE_SFIXED64}, + {FieldDescriptor::TYPE_INT32, FieldDescriptor::TYPE_FLOAT}, + {FieldDescriptor::TYPE_INT32, FieldDescriptor::TYPE_DOUBLE}, + {FieldDescriptor::TYPE_BOOL, FieldDescriptor::TYPE_BOOL}, + {FieldDescriptor::TYPE_STRING, FieldDescriptor::TYPE_STRING}, + {FieldDescriptor::TYPE_STRING, FieldDescriptor::TYPE_BYTES}, + {FieldDescriptor::TYPE_STRING, FieldDescriptor::TYPE_ENUM}, + {FieldDescriptor::TYPE_STRING, FieldDescriptor::TYPE_MESSAGE}, + }; + return kTypes; +} + +absl::Span<const FieldDescriptor::Type> ValidDataOneofTypes() { + static constexpr FieldDescriptor::Type kTypes[] = { + FieldDescriptor::TYPE_UINT32, FieldDescriptor::TYPE_BOOL, + FieldDescriptor::TYPE_UINT64, FieldDescriptor::TYPE_FLOAT, + FieldDescriptor::TYPE_DOUBLE, FieldDescriptor::TYPE_STRING, + FieldDescriptor::TYPE_BYTES, FieldDescriptor::TYPE_ENUM, + FieldDescriptor::TYPE_MESSAGE, + }; + return kTypes; +} absl::Span<const FieldDescriptor::Type> AllFieldTypesExceptGroup() { static constexpr FieldDescriptor::Type kTypes[] = { @@ -51,6 +460,62 @@ return kTypes; } +std::vector<const Descriptor*> AllTestMessageTypes() { + return { + protobuf_test_messages::proto3::TestAllTypesProto3::descriptor(), + protobuf_test_messages::proto2::TestAllTypesProto2::descriptor(), + protobuf_test_messages::editions::proto3::TestAllTypesProto3:: + descriptor(), + protobuf_test_messages::editions::proto2::TestAllTypesProto2:: + descriptor(), + }; +} + +std::vector<const Descriptor*> Proto3TestMessageTypes() { + return { + protobuf_test_messages::proto3::TestAllTypesProto3::descriptor(), + protobuf_test_messages::editions::proto3::TestAllTypesProto3:: + descriptor(), + }; +} + +std::vector<const Descriptor*> Proto2TestMessageTypes() { + return { + protobuf_test_messages::proto2::TestAllTypesProto2::descriptor(), + protobuf_test_messages::editions::proto2::TestAllTypesProto2:: + descriptor(), + }; +} + +std::string ParamName(absl::string_view text) { + std::string name; + for (char c : text) { + if (absl::ascii_isalnum(c)) name.push_back(c); + } + ABSL_CHECK(!name.empty()) + << "No letters or digits in parameter name: " << text; + return name; +} + +std::string ParamName(const Descriptor* absl_nonnull message) { + // The edition identifier used in test names ("Editions_Proto2") only differs + // from a valid gtest parameter name by its underscore. + return ParamName(GetEditionIdentifier(*message)); +} + +std::string ParamName(FieldDescriptor::Type type) { + return UpperCaseTypeName(type); +} + +std::string ParamName(int value) { + ABSL_CHECK_GE(value, 0) << "Negative parameters have no valid gtest name"; + return absl::StrCat(value); +} + +std::string ParamName(const MapType& type) { + return absl::StrCat(ParamName(type.key), "_", ParamName(type.value)); +} + } // namespace conformance } // namespace protobuf } // namespace google
diff --git a/conformance/binary_test_util.h b/conformance/binary_test_util.h index 744a5a1..64e61a3 100644 --- a/conformance/binary_test_util.h +++ b/conformance/binary_test_util.h
@@ -8,16 +8,150 @@ #ifndef GOOGLE_PROTOBUF_CONFORMANCE_BINARY_TEST_UTIL_H__ #define GOOGLE_PROTOBUF_CONFORMANCE_BINARY_TEST_UTIL_H__ +#include <cstdint> +#include <string> +#include <vector> + +#include "absl/base/nullability.h" +#include "absl/strings/string_view.h" #include "absl/types/span.h" +#include "conformance/binary_wireformat.h" #include "google/protobuf/descriptor.h" -// Helpers shared by the gtest-based binary conformance tests: the field-type -// tables the tests are parameterized over. Deliberately gtest-free. +// Helpers shared by the gtest-based binary conformance tests. They look up +// the test-message fields exercised for a given field type, encode sample +// values, define the parameter sets the tests are instantiated over, and +// stringify those parameters for gtest names. +// +// This header is deliberately gtest-free. The fixtures and +// INSTANTIATE_TEST_SUITE_P name generators built on top of these helpers live +// in message_type_fixtures.h. namespace google { namespace protobuf { namespace conformance { +// Restricts GetFieldForType() to packed or unpacked repeated fields. Named +// Packedness rather than Packed because Packed() is already the wire-format +// builder in binary_wireformat.h. +enum class Packedness { + kUnspecified = 0, + kPacked = 1, + kUnpacked = 2, +}; + +// Returns the first field of `message`, in declaration order, whose type is +// `type` and whose cardinality matches `repeated`. When `packedness` isn't +// kUnspecified, only fields whose is_packed() matches it are considered. +// That is how proto2 and proto3 messages, with their different packing +// defaults, select their [packed = true] and [packed = false] test fields. +// +// const FieldDescriptor* field = GetFieldForType( +// message, FieldDescriptor::TYPE_INT32, /*repeated=*/true, +// Packedness::kPacked); +// +// Check-fails if `message` has no such field. In particular, kPacked with +// `repeated` false is always fatal, since singular fields are never packed. +const FieldDescriptor* absl_nonnull GetFieldForType( + const Descriptor& message, FieldDescriptor::Type type, bool repeated, + Packedness packedness = Packedness::kUnspecified); + +// Returns `field`'s number as the wire-format builders of binary_wireformat.h +// take it. FieldDescriptor::number() is an int, but field numbers are never +// negative. +inline uint32_t FieldNumber(const FieldDescriptor& field) { + return static_cast<uint32_t>(field.number()); +} + +// Returns the first map field of `message`, in declaration order, whose keys +// are of type `key_type` and whose values are of type `value_type`. For +// example, map_int32_int32 (56) for INT32/INT32 or map_string_nested_message +// (71) for STRING/MESSAGE. Check-fails if `message` has no such field. +const FieldDescriptor* absl_nonnull GetFieldForMapType( + const Descriptor& message, FieldDescriptor::Type key_type, + FieldDescriptor::Type value_type); + +// Which member of a oneof GetFieldForOneofType() selects relative to the +// requested field type. +enum class OneofMember { + kOfType, // The first member of the given type. + kOfOtherType, // The first member of any other type. +}; + +// Returns the first field of `message`, in declaration order, that is a member +// of a non-synthetic oneof and whose type is `type`. For example, oneof_uint32 +// (111) for UINT32. With OneofMember::kOfOtherType, returns the first oneof +// member whose type is not `type` instead. The oneof tests use that to find a +// second member of the same oneof to override. Check-fails if there is no +// such field. +const FieldDescriptor* absl_nonnull GetFieldForOneofType( + const Descriptor& message, FieldDescriptor::Type type, + OneofMember member = OneofMember::kOfType); + +// Returns the wire type a non-packed value of `type` is encoded with. +WireType WireTypeForFieldType(FieldDescriptor::Type type); + +// Returns the upper-case name of `type` as it appears in conformance test +// names, e.g. "INT32", "SFIXED64" or "MESSAGE". +std::string UpperCaseTypeName(FieldDescriptor::Type type); + +// Returns the encoded default value of a field of `type`, without a tag. That +// is zero, or an empty string, bytes or message. TYPE_GROUP has no such +// single-value encoding and yields an empty Wire. +Wire GetDefaultValue(FieldDescriptor::Type type); + +// Returns an encoded non-default value of a field of `type`, without a tag. +// That is 1 for numeric types and enums, "a" for strings and bytes, and a +// message whose field 1 is the varint 1234. TYPE_GROUP yields an empty Wire. +Wire GetNonDefaultValue(FieldDescriptor::Type type); + +// Returns whether `payload`, the encoding of a value of `type` without its tag, +// is the encoding of the type's default value. That is zero, false, the empty +// string or empty bytes, which is what a proto3 serializer omits from a +// singular field. Always false for TYPE_MESSAGE and TYPE_GROUP. Only the +// canonical encoding counts. A ten-byte varint 0 doesn't. +bool IsDefaultValue(FieldDescriptor::Type type, const Wire& payload); + +// Returns whether the singular scalar fields of `message` have implicit +// presence, that is whether it follows proto3 semantics. True for +// TestAllTypesProto3 and its editions equivalent. False for the proto2 ones +// and TestAllTypesEdition2023. Decided by the presence of the message's first +// singular INT32 field, optional_int32 in every TestAllTypes. Check-fails for +// a message without one. +bool HasImplicitPresence(const Descriptor& message); + +// One case of the valid-data tests (ValidDataScalar.<TYPE>[i] and friends). +// `input` is the encoding of a value as sent to the testee. `expected` is the +// canonical encoding a conforming parser must be equivalent to after reading +// it. For example, an over-long varint and its minimal form, or a 64-bit +// varint and its truncation to 32 bits. Both are without a tag. +struct ValidDataCase { + Wire input; + Wire expected; +}; + +// Returns the valid-data cases of `type`. Case i is what the tests named +// ValidDataScalar.<TYPE>[i] and ValidDataScalarBinary.<TYPE>[i] send. The +// repeated-field tests send all of them in this order. Both the binary->binary +// and the binary->JSON legs iterate these tables. Check-fails for TYPE_GROUP, +// which has no cases. +absl::Span<const ValidDataCase> ValidDataCases(FieldDescriptor::Type type); + +// The key and value types of a map field, selecting one of the +// map_<key>_<value> fields of the test messages (see GetFieldForMapType()). +struct MapType { + FieldDescriptor::Type key; + FieldDescriptor::Type value; +}; + +// The key/value type combinations of the map valid-data tests +// (ValidDataMap.<KEY>.<VALUE>.*). +absl::Span<const MapType> ValidDataMapTypes(); + +// The field types of the oneof valid-data tests (ValidDataOneof.<TYPE>.* and +// ValidDataOneofBinary.<TYPE>.*). +absl::Span<const FieldDescriptor::Type> ValidDataOneofTypes(); + // Every field type except TYPE_GROUP (the 17 types the test messages have // singular, repeated, packed and unpacked fields of), in the order the legacy // valid-data tests ran them: DOUBLE, FLOAT, INT64, UINT64, INT32, UINT32, @@ -35,6 +169,62 @@ // non-packable ones: STRING, BYTES, MESSAGE, in the same order. absl::Span<const FieldDescriptor::Type> LengthDelimitedFieldTypes(); +// Returns all four TestAllTypes message types the binary conformance tests run +// over, in this order: Proto3, Proto2, Editions_Proto3, Editions_Proto2. +// +// INSTANTIATE_TEST_SUITE_P(All, FooTest, ValuesIn(AllTestMessageTypes()), +// MessageTypeParamName); +// +// The editions variants are skipped at runtime by the ConformanceTest fixture +// when --maximum_edition doesn't cover them (see MessageUnderTest() in +// test_fixture.h). +// +// Calling this from INSTANTIATE_TEST_SUITE_P is fine. gtest evaluates the +// parameter generators during test registration, not during static +// initialization, so the generated messages' descriptor() accessors are safe +// to call there. +std::vector<const Descriptor*> AllTestMessageTypes(); + +// Returns the proto3-style subset of AllTestMessageTypes(), in this order: +// Proto3, Editions_Proto3. For tests whose expectations depend on proto3 +// semantics, for example UTF-8 validation of string fields. Like +// AllTestMessageTypes(), safe to call from INSTANTIATE_TEST_SUITE_P. +std::vector<const Descriptor*> Proto3TestMessageTypes(); + +// Returns the proto2-style subset of AllTestMessageTypes(), in this order: +// Proto2, Editions_Proto2. For tests whose expectations depend on proto2 +// semantics, for example closed enums or required fields. Like +// AllTestMessageTypes(), safe to call from INSTANTIATE_TEST_SUITE_P. +std::vector<const Descriptor*> Proto2TestMessageTypes(); + +// ParamName() renders one test parameter as a component of a gtest parameter +// name. Every overload yields a non-empty string of letters and digits, with +// "_" between the parts of a compound parameter. Components can therefore be +// joined with "_" (see TupleParamName() in message_type_fixtures.h) and the +// result is always a valid gtest name. Add an overload here for each new +// parameter type the conformance tests are instantiated over. + +// Returns `text` with every character that isn't a letter or digit removed. +// For example, "Editions_Proto2" becomes "EditionsProto2". `text` must contain +// at least one letter or digit. +std::string ParamName(absl::string_view text); + +// Returns the edition identifier of `message` as used in conformance test +// names (see GetEditionIdentifier() in naming.h), minus its underscore. That +// is "Proto3", "Proto2", "EditionsProto3", "EditionsProto2" or +// "EditionUnstable". +std::string ParamName(const Descriptor* absl_nonnull message); + +// The upper-case type name, e.g. "INT32"; see UpperCaseTypeName(). +std::string ParamName(FieldDescriptor::Type type); + +// The decimal representation of `value`, which must not be negative. +std::string ParamName(int value); + +// The key and value type names joined by "_", e.g. "INT32_INT32" or +// "STRING_MESSAGE". +std::string ParamName(const MapType& type); + } // namespace conformance } // namespace protobuf } // namespace google
diff --git a/conformance/binary_test_util_test.cc b/conformance/binary_test_util_test.cc index 305c52b..1d980ff 100644 --- a/conformance/binary_test_util_test.cc +++ b/conformance/binary_test_util_test.cc
@@ -7,21 +7,595 @@ #include "conformance/binary_test_util.h" +#include <algorithm> +#include <cstddef> +#include <cstdint> +#include <limits> +#include <map> +#include <memory> #include <set> +#include <string> #include <vector> #include <gmock/gmock.h> #include <gtest/gtest.h> +#include "absl/strings/ascii.h" +#include "absl/strings/str_cat.h" +#include "absl/strings/str_split.h" +#include "conformance/binary_wireformat.h" +#include "conformance/test_protos/test_messages_edition2023.pb.h" +#include "conformance/test_protos/test_messages_edition_unstable.pb.h" #include "google/protobuf/descriptor.h" +#include "editions/golden/test_messages_proto2_editions.pb.h" +#include "editions/golden/test_messages_proto3_editions.pb.h" +#include "google/protobuf/message.h" +#include "google/protobuf/test_messages_proto2.pb.h" +#include "google/protobuf/test_messages_proto3.pb.h" +#include "google/protobuf/util/message_differencer.h" namespace google { namespace protobuf { namespace conformance { namespace { +using ::protobuf_test_messages::edition_unstable::TestAllTypesEditionUnstable; +using ::protobuf_test_messages::editions::TestAllTypesEdition2023; +using ::protobuf_test_messages::proto2::TestAllTypesProto2; +using ::protobuf_test_messages::proto3::TestAllTypesProto3; +using TestAllTypesProto2Editions = + ::protobuf_test_messages::editions::proto2::TestAllTypesProto2; +using TestAllTypesProto3Editions = + ::protobuf_test_messages::editions::proto3::TestAllTypesProto3; +using ::testing::ElementsAre; using ::testing::ElementsAreArray; +using ::testing::FieldsAre; +using ::testing::IsSubsetOf; using ::testing::UnorderedElementsAreArray; +TEST(GetFieldForTypeTest, SingularInt32) { + EXPECT_EQ(GetFieldForType(*TestAllTypesProto3::descriptor(), + FieldDescriptor::TYPE_INT32, /*repeated=*/false) + ->number(), + 1); + EXPECT_EQ(GetFieldForType(*TestAllTypesProto2::descriptor(), + FieldDescriptor::TYPE_INT32, /*repeated=*/false) + ->number(), + 1); +} + +TEST(GetFieldForTypeTest, RepeatedInt32) { + EXPECT_EQ(GetFieldForType(*TestAllTypesProto3::descriptor(), + FieldDescriptor::TYPE_INT32, /*repeated=*/true) + ->number(), + 31); + EXPECT_EQ(GetFieldForType(*TestAllTypesProto2::descriptor(), + FieldDescriptor::TYPE_INT32, /*repeated=*/true) + ->number(), + 31); +} + +// repeated_int32 (31) is packed by default in proto3 but not in proto2, so the +// packed lookup only has to fall through to packed_int32 (75) for proto2. +TEST(GetFieldForTypeTest, PackedInt32) { + EXPECT_EQ(GetFieldForType(*TestAllTypesProto3::descriptor(), + FieldDescriptor::TYPE_INT32, /*repeated=*/true, + Packedness::kPacked) + ->number(), + 31); + EXPECT_EQ(GetFieldForType(*TestAllTypesProto2::descriptor(), + FieldDescriptor::TYPE_INT32, /*repeated=*/true, + Packedness::kPacked) + ->number(), + 75); +} + +// ... and conversely the unpacked lookup only has to fall through to +// unpacked_int32 (89) for proto3. +TEST(GetFieldForTypeTest, UnpackedInt32) { + EXPECT_EQ(GetFieldForType(*TestAllTypesProto3::descriptor(), + FieldDescriptor::TYPE_INT32, /*repeated=*/true, + Packedness::kUnpacked) + ->number(), + 89); + EXPECT_EQ(GetFieldForType(*TestAllTypesProto2::descriptor(), + FieldDescriptor::TYPE_INT32, /*repeated=*/true, + Packedness::kUnpacked) + ->number(), + 31); +} + +TEST(GetFieldForTypeTest, Message) { + EXPECT_EQ(GetFieldForType(*TestAllTypesProto3::descriptor(), + FieldDescriptor::TYPE_MESSAGE, /*repeated=*/false) + ->number(), + 18); + EXPECT_EQ(GetFieldForType(*TestAllTypesProto3::descriptor(), + FieldDescriptor::TYPE_MESSAGE, /*repeated=*/true) + ->number(), + 48); +} + +TEST(GetFieldForTypeTest, Enum) { + EXPECT_EQ(GetFieldForType(*TestAllTypesProto2::descriptor(), + FieldDescriptor::TYPE_ENUM, /*repeated=*/false) + ->number(), + 21); + EXPECT_EQ(GetFieldForType(*TestAllTypesProto2::descriptor(), + FieldDescriptor::TYPE_ENUM, /*repeated=*/true) + ->number(), + 51); +} + +TEST(GetFieldForTypeTest, EditionsMessagesMatchTheirProto2AndProto3Originals) { + for (FieldDescriptor::Type type : + {FieldDescriptor::TYPE_INT32, FieldDescriptor::TYPE_MESSAGE, + FieldDescriptor::TYPE_ENUM, FieldDescriptor::TYPE_STRING}) { + for (bool repeated : {false, true}) { + SCOPED_TRACE(absl::StrCat(repeated ? "repeated " : "singular ", + FieldDescriptor::TypeName(type))); + EXPECT_EQ( + GetFieldForType(*TestAllTypesProto3Editions::descriptor(), type, + repeated) + ->number(), + GetFieldForType(*TestAllTypesProto3::descriptor(), type, repeated) + ->number()); + EXPECT_EQ( + GetFieldForType(*TestAllTypesProto2Editions::descriptor(), type, + repeated) + ->number(), + GetFieldForType(*TestAllTypesProto2::descriptor(), type, repeated) + ->number()); + } + } +} + +TEST(GetFieldForTypeDeathTest, MissingFieldIsFatal) { + // TestAllTypesProto3 has no singular group field. + EXPECT_DEATH(GetFieldForType(*TestAllTypesProto3::descriptor(), + FieldDescriptor::TYPE_GROUP, /*repeated=*/false), + "Couldn't find field with type: Singular group for " + "protobuf_test_messages.proto3.TestAllTypesProto3"); +} + +// Singular fields are never packed, so asking for a packed singular field can +// never succeed, even for a type the message does have a singular field of. +TEST(GetFieldForTypeDeathTest, PackedSingularIsFatal) { + EXPECT_DEATH(GetFieldForType(*TestAllTypesProto3::descriptor(), + FieldDescriptor::TYPE_INT32, /*repeated=*/false, + Packedness::kPacked), + "Couldn't find field with type: Singular Packed int32 for " + "protobuf_test_messages.proto3.TestAllTypesProto3"); +} + +TEST(GetFieldForMapTypeTest, LegacyFieldNumbers) { + for (const Descriptor* message : AllTestMessageTypes()) { + SCOPED_TRACE(message->full_name()); + EXPECT_EQ(GetFieldForMapType(*message, FieldDescriptor::TYPE_INT32, + FieldDescriptor::TYPE_INT32) + ->number(), + 56); + EXPECT_EQ(GetFieldForMapType(*message, FieldDescriptor::TYPE_STRING, + FieldDescriptor::TYPE_STRING) + ->number(), + 69); + EXPECT_EQ(GetFieldForMapType(*message, FieldDescriptor::TYPE_STRING, + FieldDescriptor::TYPE_MESSAGE) + ->number(), + 71); + } +} + +TEST(GetFieldForMapTypeDeathTest, MissingFieldIsFatal) { + // There is no map<bytes, ...> (bytes aren't valid map keys). + EXPECT_DEATH(GetFieldForMapType(*TestAllTypesProto3::descriptor(), + FieldDescriptor::TYPE_BYTES, + FieldDescriptor::TYPE_BYTES), + "Couldn't find map field with type: bytes and bytes for " + "protobuf_test_messages.proto3.TestAllTypesProto3"); +} + +TEST(GetFieldForOneofTypeTest, LegacyFieldNumbers) { + for (const Descriptor* message : AllTestMessageTypes()) { + SCOPED_TRACE(message->full_name()); + EXPECT_EQ( + GetFieldForOneofType(*message, FieldDescriptor::TYPE_UINT32)->number(), + 111); + EXPECT_EQ( + GetFieldForOneofType(*message, FieldDescriptor::TYPE_MESSAGE)->number(), + 112); + EXPECT_EQ( + GetFieldForOneofType(*message, FieldDescriptor::TYPE_ENUM)->number(), + 119); + } +} + +// With kOfOtherType the first oneof member of any *other* type is returned, +// which is oneof_uint32 (111) unless that is the type being excluded. +TEST(GetFieldForOneofTypeTest, OfOtherType) { + const Descriptor& message = *TestAllTypesProto3::descriptor(); + EXPECT_EQ(GetFieldForOneofType(message, FieldDescriptor::TYPE_MESSAGE, + OneofMember::kOfOtherType) + ->number(), + 111); + EXPECT_EQ(GetFieldForOneofType(message, FieldDescriptor::TYPE_UINT32, + OneofMember::kOfOtherType) + ->number(), + 112); +} + +TEST(GetFieldForOneofTypeDeathTest, MissingFieldIsFatal) { + // No oneof member is a group. + EXPECT_DEATH(GetFieldForOneofType(*TestAllTypesProto2::descriptor(), + FieldDescriptor::TYPE_GROUP), + "Couldn't find oneof field with type: group for " + "protobuf_test_messages.proto2.TestAllTypesProto2"); +} + +TEST(WireTypeForFieldTypeTest, AllTypes) { + EXPECT_EQ(WireTypeForFieldType(FieldDescriptor::TYPE_DOUBLE), + WireType::kFixed64); + EXPECT_EQ(WireTypeForFieldType(FieldDescriptor::TYPE_FLOAT), + WireType::kFixed32); + EXPECT_EQ(WireTypeForFieldType(FieldDescriptor::TYPE_INT64), + WireType::kVarint); + EXPECT_EQ(WireTypeForFieldType(FieldDescriptor::TYPE_UINT64), + WireType::kVarint); + EXPECT_EQ(WireTypeForFieldType(FieldDescriptor::TYPE_INT32), + WireType::kVarint); + EXPECT_EQ(WireTypeForFieldType(FieldDescriptor::TYPE_FIXED64), + WireType::kFixed64); + EXPECT_EQ(WireTypeForFieldType(FieldDescriptor::TYPE_FIXED32), + WireType::kFixed32); + EXPECT_EQ(WireTypeForFieldType(FieldDescriptor::TYPE_BOOL), + WireType::kVarint); + EXPECT_EQ(WireTypeForFieldType(FieldDescriptor::TYPE_STRING), + WireType::kLengthPrefixed); + EXPECT_EQ(WireTypeForFieldType(FieldDescriptor::TYPE_GROUP), + WireType::kStartGroup); + EXPECT_EQ(WireTypeForFieldType(FieldDescriptor::TYPE_MESSAGE), + WireType::kLengthPrefixed); + EXPECT_EQ(WireTypeForFieldType(FieldDescriptor::TYPE_BYTES), + WireType::kLengthPrefixed); + EXPECT_EQ(WireTypeForFieldType(FieldDescriptor::TYPE_UINT32), + WireType::kVarint); + EXPECT_EQ(WireTypeForFieldType(FieldDescriptor::TYPE_ENUM), + WireType::kVarint); + EXPECT_EQ(WireTypeForFieldType(FieldDescriptor::TYPE_SFIXED32), + WireType::kFixed32); + EXPECT_EQ(WireTypeForFieldType(FieldDescriptor::TYPE_SFIXED64), + WireType::kFixed64); + EXPECT_EQ(WireTypeForFieldType(FieldDescriptor::TYPE_SINT32), + WireType::kVarint); + EXPECT_EQ(WireTypeForFieldType(FieldDescriptor::TYPE_SINT64), + WireType::kVarint); +} + +TEST(UpperCaseTypeNameTest, MatchesLegacyTestNames) { + EXPECT_EQ(UpperCaseTypeName(FieldDescriptor::TYPE_INT32), "INT32"); + EXPECT_EQ(UpperCaseTypeName(FieldDescriptor::TYPE_SFIXED64), "SFIXED64"); + EXPECT_EQ(UpperCaseTypeName(FieldDescriptor::TYPE_MESSAGE), "MESSAGE"); + EXPECT_EQ(UpperCaseTypeName(FieldDescriptor::TYPE_BYTES), "BYTES"); +} + +TEST(GetDefaultValueTest, Varints) { + for (FieldDescriptor::Type type : + {FieldDescriptor::TYPE_INT32, FieldDescriptor::TYPE_INT64, + FieldDescriptor::TYPE_UINT32, FieldDescriptor::TYPE_UINT64, + FieldDescriptor::TYPE_ENUM, FieldDescriptor::TYPE_BOOL, + FieldDescriptor::TYPE_SINT32, FieldDescriptor::TYPE_SINT64}) { + SCOPED_TRACE(FieldDescriptor::TypeName(type)); + EXPECT_EQ(GetDefaultValue(type), Varint(0)); + } +} + +TEST(GetDefaultValueTest, FixedWidth) { + EXPECT_EQ(GetDefaultValue(FieldDescriptor::TYPE_FIXED32), Fixed32(0)); + EXPECT_EQ(GetDefaultValue(FieldDescriptor::TYPE_SFIXED32), Fixed32(0)); + EXPECT_EQ(GetDefaultValue(FieldDescriptor::TYPE_FLOAT), Float(0)); + EXPECT_EQ(GetDefaultValue(FieldDescriptor::TYPE_FIXED64), Fixed64(0)); + EXPECT_EQ(GetDefaultValue(FieldDescriptor::TYPE_SFIXED64), Fixed64(0)); + EXPECT_EQ(GetDefaultValue(FieldDescriptor::TYPE_DOUBLE), Double(0)); +} + +TEST(GetDefaultValueTest, LengthPrefixed) { + EXPECT_EQ(GetDefaultValue(FieldDescriptor::TYPE_STRING), LengthPrefixed("")); + EXPECT_EQ(GetDefaultValue(FieldDescriptor::TYPE_BYTES), LengthPrefixed("")); + EXPECT_EQ(GetDefaultValue(FieldDescriptor::TYPE_MESSAGE), LengthPrefixed("")); +} + +TEST(GetDefaultValueTest, GroupIsEmpty) { + EXPECT_EQ(GetDefaultValue(FieldDescriptor::TYPE_GROUP), Wire()); +} + +TEST(GetNonDefaultValueTest, Varints) { + for (FieldDescriptor::Type type : + {FieldDescriptor::TYPE_INT32, FieldDescriptor::TYPE_INT64, + FieldDescriptor::TYPE_UINT32, FieldDescriptor::TYPE_UINT64, + FieldDescriptor::TYPE_ENUM, FieldDescriptor::TYPE_BOOL}) { + SCOPED_TRACE(FieldDescriptor::TypeName(type)); + EXPECT_EQ(GetNonDefaultValue(type), Varint(1)); + } + // Zig-zag encoded, so 1 becomes 2. + EXPECT_EQ(GetNonDefaultValue(FieldDescriptor::TYPE_SINT32), Varint(2)); + EXPECT_EQ(GetNonDefaultValue(FieldDescriptor::TYPE_SINT64), Varint(2)); +} + +TEST(GetNonDefaultValueTest, FixedWidth) { + EXPECT_EQ(GetNonDefaultValue(FieldDescriptor::TYPE_FIXED32), Fixed32(1)); + EXPECT_EQ(GetNonDefaultValue(FieldDescriptor::TYPE_SFIXED32), Fixed32(1)); + EXPECT_EQ(GetNonDefaultValue(FieldDescriptor::TYPE_FLOAT), Float(1)); + EXPECT_EQ(GetNonDefaultValue(FieldDescriptor::TYPE_FIXED64), Fixed64(1)); + EXPECT_EQ(GetNonDefaultValue(FieldDescriptor::TYPE_SFIXED64), Fixed64(1)); + EXPECT_EQ(GetNonDefaultValue(FieldDescriptor::TYPE_DOUBLE), Double(1)); +} + +TEST(GetNonDefaultValueTest, LengthPrefixed) { + EXPECT_EQ(GetNonDefaultValue(FieldDescriptor::TYPE_STRING), + LengthPrefixed("a")); + EXPECT_EQ(GetNonDefaultValue(FieldDescriptor::TYPE_BYTES), + LengthPrefixed("a")); + EXPECT_EQ(GetNonDefaultValue(FieldDescriptor::TYPE_MESSAGE), + LengthPrefixed(VarintField(1, 1234))); +} + +TEST(GetNonDefaultValueTest, GroupIsEmpty) { + EXPECT_EQ(GetNonDefaultValue(FieldDescriptor::TYPE_GROUP), Wire()); +} + +TEST(GetNonDefaultValueTest, DiffersFromDefaultValue) { + for (FieldDescriptor::Type type : + {FieldDescriptor::TYPE_INT32, FieldDescriptor::TYPE_SINT64, + FieldDescriptor::TYPE_FIXED32, FieldDescriptor::TYPE_DOUBLE, + FieldDescriptor::TYPE_STRING, FieldDescriptor::TYPE_MESSAGE}) { + SCOPED_TRACE(FieldDescriptor::TypeName(type)); + EXPECT_NE(GetNonDefaultValue(type), GetDefaultValue(type)); + } +} + +// `wire` parsed by the generated class of `message`, or null if it doesn't +// parse. The test inputs are checked against the C++ generated classes, whose +// parsing and merge semantics are the reference the conformance tests hold +// others to. +std::unique_ptr<Message> ParseAs(const Descriptor* message, const Wire& wire) { + std::unique_ptr<Message> parsed( + MessageFactory::generated_factory()->GetPrototype(message)->New()); + if (!parsed->ParseFromString(wire.data())) return nullptr; + return parsed; +} + +// Matches a Message equal to `*expected`. +MATCHER_P(EqualsMessage, expected, "") { + std::string diff; + util::MessageDifferencer differencer; + differencer.ReportDifferencesToString(&diff); + if (differencer.Compare(*expected, arg)) return true; + *result_listener << "which differs: " << diff; + return false; +} + +// Matches a Wire that the generated class of `message` parses into a message +// matching `matcher`. +MATCHER_P2(ParsesAs, message, matcher, "") { + std::unique_ptr<Message> parsed = ParseAs(message, arg); + if (parsed == nullptr) { + *result_listener << "which doesn't parse as " << message->full_name(); + return false; + } + return ExplainMatchResult(matcher, *parsed, result_listener); +} + +// The legacy table sizes; the test names ValidDataScalar.<TYPE>[i] depend on +// them. +TEST(ValidDataCasesTest, LegacyTableSizes) { + const std::map<FieldDescriptor::Type, size_t> kSizes = { + {FieldDescriptor::TYPE_DOUBLE, 4}, {FieldDescriptor::TYPE_FLOAT, 5}, + {FieldDescriptor::TYPE_INT64, 4}, {FieldDescriptor::TYPE_UINT64, 3}, + {FieldDescriptor::TYPE_INT32, 10}, {FieldDescriptor::TYPE_UINT32, 10}, + {FieldDescriptor::TYPE_FIXED64, 3}, {FieldDescriptor::TYPE_FIXED32, 3}, + {FieldDescriptor::TYPE_SFIXED64, 4}, {FieldDescriptor::TYPE_SFIXED32, 4}, + {FieldDescriptor::TYPE_BOOL, 7}, {FieldDescriptor::TYPE_SINT32, 5}, + {FieldDescriptor::TYPE_SINT64, 4}, {FieldDescriptor::TYPE_STRING, 5}, + {FieldDescriptor::TYPE_BYTES, 4}, {FieldDescriptor::TYPE_ENUM, 6}, + {FieldDescriptor::TYPE_MESSAGE, 2}, + }; + ASSERT_EQ(kSizes.size(), AllFieldTypesExceptGroup().size()); + size_t total = 0; + for (const auto& [type, size] : kSizes) { + SCOPED_TRACE(FieldDescriptor::TypeName(type)); + EXPECT_EQ(ValidDataCases(type).size(), size); + total += size; + } + EXPECT_EQ(total, size_t{83}); +} + +// Every table starts with the type's default value, which is what the proto3 +// rule of the singular-field tests hinges on. +TEST(ValidDataCasesTest, FirstCaseIsTheDefaultValue) { + for (FieldDescriptor::Type type : AllFieldTypesExceptGroup()) { + SCOPED_TRACE(FieldDescriptor::TypeName(type)); + EXPECT_EQ(ValidDataCases(type).front().input, GetDefaultValue(type)); + EXPECT_EQ(ValidDataCases(type).front().expected, GetDefaultValue(type)); + } +} + +// The RepeatedScalarSelectsLast tests expect the last case of a table without +// applying the proto3 default rule, which relies on no table ending in the +// type's default value. +TEST(ValidDataCasesTest, LastCaseIsNotDefault) { + for (FieldDescriptor::Type type : AllFieldTypesExceptGroup()) { + if (type == FieldDescriptor::TYPE_MESSAGE) continue; + SCOPED_TRACE(FieldDescriptor::TypeName(type)); + EXPECT_FALSE(IsDefaultValue(type, ValidDataCases(type).back().expected)); + } +} + +// A few entries whose input isn't the canonical encoding, spelled out to pin +// the legacy bytes. +TEST(ValidDataCasesTest, NonCanonicalInputs) { + EXPECT_EQ(ValidDataCases(FieldDescriptor::TYPE_INT32)[2].input, + LongVarint(12345, 2)); + EXPECT_EQ(ValidDataCases(FieldDescriptor::TYPE_INT32)[2].expected, + Varint(12345)); + // A 64-bit varint truncated to int32 -1. + EXPECT_EQ(ValidDataCases(FieldDescriptor::TYPE_INT32)[8].input, + Varint(std::numeric_limits<int64_t>::max())); + EXPECT_EQ(ValidDataCases(FieldDescriptor::TYPE_INT32)[8].expected, + Varint(-1)); + // Any non-zero bool is true. + EXPECT_EQ(ValidDataCases(FieldDescriptor::TYPE_BOOL)[3].input, + Varint(12345678)); + EXPECT_EQ(ValidDataCases(FieldDescriptor::TYPE_BOOL)[3].expected, Varint(1)); + // A non-ASCII string (U+8C37 U+6B4C as UTF-8) round-trips unchanged. + EXPECT_EQ(ValidDataCases(FieldDescriptor::TYPE_STRING)[3].input, + LengthPrefixed("\xE8\xB0\xB7\xE6\xAD\x8C")); + EXPECT_EQ(ValidDataCases(FieldDescriptor::TYPE_STRING)[3].expected, + LengthPrefixed("\xE8\xB0\xB7\xE6\xAD\x8C")); +} + +// The invariant every table encodes: for the singular field of the type, +// the C++ generated class parses `input` into a message equivalent to the one +// it parses `expected` into. +TEST(ValidDataCasesTest, InputParsesEquivalentlyToExpected) { + for (const Descriptor* message : AllTestMessageTypes()) { + for (FieldDescriptor::Type type : AllFieldTypesExceptGroup()) { + const Wire tag = + Tag(FieldNumber(*GetFieldForType(*message, type, /*repeated=*/false)), + WireTypeForFieldType(type)); + int i = 0; + for (const ValidDataCase& value : ValidDataCases(type)) { + SCOPED_TRACE(absl::StrCat(message->full_name(), " ", + FieldDescriptor::TypeName(type), "[", i++, + "]")); + std::unique_ptr<Message> expected = + ParseAs(message, Wire(tag, value.expected)); + ASSERT_NE(expected, nullptr); + EXPECT_THAT(Wire(tag, value.input), + ParsesAs(message, EqualsMessage(expected.get()))); + } + } + } +} + +TEST(ValidDataCasesDeathTest, GroupIsFatal) { + EXPECT_DEATH(ValidDataCases(FieldDescriptor::TYPE_GROUP), + "No valid-data cases for field type group"); +} + +// The legacy table; the test names ValidDataMap.<KEY>.<VALUE>.* depend on it. +TEST(ValidDataMapTypesTest, LegacyTable) { + EXPECT_THAT( + ValidDataMapTypes(), + ElementsAre( + FieldsAre(FieldDescriptor::TYPE_INT32, FieldDescriptor::TYPE_INT32), + FieldsAre(FieldDescriptor::TYPE_INT64, FieldDescriptor::TYPE_INT64), + FieldsAre(FieldDescriptor::TYPE_UINT32, FieldDescriptor::TYPE_UINT32), + FieldsAre(FieldDescriptor::TYPE_UINT64, FieldDescriptor::TYPE_UINT64), + FieldsAre(FieldDescriptor::TYPE_SINT32, FieldDescriptor::TYPE_SINT32), + FieldsAre(FieldDescriptor::TYPE_SINT64, FieldDescriptor::TYPE_SINT64), + FieldsAre(FieldDescriptor::TYPE_FIXED32, + FieldDescriptor::TYPE_FIXED32), + FieldsAre(FieldDescriptor::TYPE_FIXED64, + FieldDescriptor::TYPE_FIXED64), + FieldsAre(FieldDescriptor::TYPE_SFIXED32, + FieldDescriptor::TYPE_SFIXED32), + FieldsAre(FieldDescriptor::TYPE_SFIXED64, + FieldDescriptor::TYPE_SFIXED64), + FieldsAre(FieldDescriptor::TYPE_INT32, FieldDescriptor::TYPE_FLOAT), + FieldsAre(FieldDescriptor::TYPE_INT32, FieldDescriptor::TYPE_DOUBLE), + FieldsAre(FieldDescriptor::TYPE_BOOL, FieldDescriptor::TYPE_BOOL), + FieldsAre(FieldDescriptor::TYPE_STRING, FieldDescriptor::TYPE_STRING), + FieldsAre(FieldDescriptor::TYPE_STRING, FieldDescriptor::TYPE_BYTES), + FieldsAre(FieldDescriptor::TYPE_STRING, FieldDescriptor::TYPE_ENUM), + FieldsAre(FieldDescriptor::TYPE_STRING, + FieldDescriptor::TYPE_MESSAGE))); +} + +// Every test message has a map field for every entry whose key and value are +// of the entry's types (GetFieldForMapType() check-fails if there is none). +TEST(ValidDataMapTypesTest, EveryMessageTypeHasEveryMapField) { + for (const Descriptor* message : AllTestMessageTypes()) { + for (const MapType& type : ValidDataMapTypes()) { + SCOPED_TRACE(absl::StrCat(message->full_name(), " ", ParamName(type))); + const Descriptor* entry = + GetFieldForMapType(*message, type.key, type.value)->message_type(); + EXPECT_EQ(entry->map_key()->type(), type.key); + EXPECT_EQ(entry->map_value()->type(), type.value); + } + } +} + +// The legacy table; the test names ValidDataOneof.<TYPE>.* depend on it. +TEST(ValidDataOneofTypesTest, LegacyTable) { + EXPECT_THAT( + ValidDataOneofTypes(), + ElementsAre(FieldDescriptor::TYPE_UINT32, FieldDescriptor::TYPE_BOOL, + FieldDescriptor::TYPE_UINT64, FieldDescriptor::TYPE_FLOAT, + FieldDescriptor::TYPE_DOUBLE, FieldDescriptor::TYPE_STRING, + FieldDescriptor::TYPE_BYTES, FieldDescriptor::TYPE_ENUM, + FieldDescriptor::TYPE_MESSAGE)); +} + +// Every test message has a oneof member of every entry's type, and another +// member of a different type to override it with (GetFieldForOneofType() +// would check-fail otherwise). +TEST(ValidDataOneofTypesTest, EveryMessageTypeHasEveryOneofMember) { + for (const Descriptor* message : AllTestMessageTypes()) { + for (FieldDescriptor::Type type : ValidDataOneofTypes()) { + SCOPED_TRACE(absl::StrCat(message->full_name(), " ", + FieldDescriptor::TypeName(type))); + const FieldDescriptor* field = GetFieldForOneofType(*message, type); + const FieldDescriptor* other = + GetFieldForOneofType(*message, type, OneofMember::kOfOtherType); + EXPECT_EQ(field->type(), type); + EXPECT_NE(other->type(), type); + EXPECT_EQ(field->real_containing_oneof(), other->real_containing_oneof()); + } + } +} + +TEST(IsDefaultValueTest, ScalarTypes) { + for (FieldDescriptor::Type type : AllFieldTypesExceptGroup()) { + if (type == FieldDescriptor::TYPE_MESSAGE) continue; + SCOPED_TRACE(FieldDescriptor::TypeName(type)); + EXPECT_TRUE(IsDefaultValue(type, GetDefaultValue(type))); + EXPECT_FALSE(IsDefaultValue(type, GetNonDefaultValue(type))); + // Case 1 of every table is a non-default value. + EXPECT_FALSE(IsDefaultValue(type, ValidDataCases(type)[1].expected)); + } +} + +// Only the canonical encoding of the default value counts. +TEST(IsDefaultValueTest, OverlongVarintZeroIsNotDefault) { + EXPECT_FALSE(IsDefaultValue(FieldDescriptor::TYPE_INT32, + Wire(std::string("\x80\x00", 2)))); +} + +// Messages and groups have no default value that a serializer would omit. +TEST(IsDefaultValueTest, MessageAndGroupAreNeverDefault) { + EXPECT_FALSE(IsDefaultValue(FieldDescriptor::TYPE_MESSAGE, + GetDefaultValue(FieldDescriptor::TYPE_MESSAGE))); + EXPECT_FALSE(IsDefaultValue(FieldDescriptor::TYPE_GROUP, + GetDefaultValue(FieldDescriptor::TYPE_GROUP))); +} + +TEST(HasImplicitPresenceTest, Proto3FamilyOnly) { + EXPECT_TRUE(HasImplicitPresence(*TestAllTypesProto3::descriptor())); + EXPECT_FALSE(HasImplicitPresence(*TestAllTypesProto2::descriptor())); + EXPECT_TRUE(HasImplicitPresence(*TestAllTypesProto3Editions::descriptor())); + EXPECT_FALSE(HasImplicitPresence(*TestAllTypesProto2Editions::descriptor())); + EXPECT_FALSE(HasImplicitPresence(*TestAllTypesEdition2023::descriptor())); +} + +// ... which is exactly the Proto3TestMessageTypes() subset. +TEST(HasImplicitPresenceTest, MatchesProto3TestMessageTypes) { + const std::vector<const Descriptor*> proto3 = Proto3TestMessageTypes(); + for (const Descriptor* message : AllTestMessageTypes()) { + SCOPED_TRACE(message->full_name()); + EXPECT_EQ(HasImplicitPresence(*message), + std::find(proto3.begin(), proto3.end(), message) != proto3.end()); + } +} + // Enumerates FieldDescriptor::Type independently of the table. TEST(AllFieldTypesExceptGroupTest, IsEveryTypeButGroup) { std::set<FieldDescriptor::Type> all_but_group; @@ -33,6 +607,29 @@ UnorderedElementsAreArray(all_but_group)); } +// Every test message has a singular and a repeated field of every entry's type +// (GetFieldForType() would check-fail otherwise). +TEST(AllFieldTypesExceptGroupTest, + EveryMessageTypeHasSingularAndRepeatedField) { + for (const Descriptor* message : AllTestMessageTypes()) { + for (FieldDescriptor::Type type : AllFieldTypesExceptGroup()) { + SCOPED_TRACE(absl::StrCat(message->full_name(), " ", + FieldDescriptor::TypeName(type))); + EXPECT_EQ(GetFieldForType(*message, type, /*repeated=*/false)->type(), + type); + EXPECT_EQ(GetFieldForType(*message, type, /*repeated=*/true)->type(), + type); + } + } +} + +TEST(AllFieldTypesExceptGroupTest, EveryTypeHasValidDataCases) { + for (FieldDescriptor::Type type : AllFieldTypesExceptGroup()) { + SCOPED_TRACE(FieldDescriptor::TypeName(type)); + EXPECT_FALSE(ValidDataCases(type).empty()); + } +} + TEST(PackableFieldTypesTest, IsThePackableSubsetInTableOrder) { std::vector<FieldDescriptor::Type> packable; for (FieldDescriptor::Type type : AllFieldTypesExceptGroup()) { @@ -41,6 +638,24 @@ EXPECT_THAT(PackableFieldTypes(), ElementsAreArray(packable)); } +// Every test message has both a [packed = true] and a [packed = false] +// repeated field of every packable type, which is what the packed-field tests +// select with Packedness::kPacked / kUnpacked. +TEST(PackableFieldTypesTest, EveryMessageTypeHasPackedAndUnpackedField) { + for (const Descriptor* message : AllTestMessageTypes()) { + for (FieldDescriptor::Type type : PackableFieldTypes()) { + SCOPED_TRACE(absl::StrCat(message->full_name(), " ", + FieldDescriptor::TypeName(type))); + const FieldDescriptor* packed = GetFieldForType( + *message, type, /*repeated=*/true, Packedness::kPacked); + const FieldDescriptor* unpacked = GetFieldForType( + *message, type, /*repeated=*/true, Packedness::kUnpacked); + EXPECT_TRUE(packed->is_packed()); + EXPECT_FALSE(unpacked->is_packed()); + } + } +} + TEST(LengthDelimitedFieldTypesTest, IsTheNonPackableSubsetInTableOrder) { std::vector<FieldDescriptor::Type> non_packable; for (FieldDescriptor::Type type : AllFieldTypesExceptGroup()) { @@ -49,6 +664,121 @@ EXPECT_THAT(LengthDelimitedFieldTypes(), ElementsAreArray(non_packable)); } +TEST(LengthDelimitedFieldTypesTest, AreEncodedLengthPrefixed) { + for (FieldDescriptor::Type type : LengthDelimitedFieldTypes()) { + SCOPED_TRACE(FieldDescriptor::TypeName(type)); + EXPECT_EQ(WireTypeForFieldType(type), WireType::kLengthPrefixed); + } +} + +TEST(AllTestMessageTypesTest, LegacyOrder) { + EXPECT_THAT(AllTestMessageTypes(), + ElementsAre(TestAllTypesProto3::descriptor(), + TestAllTypesProto2::descriptor(), + TestAllTypesProto3Editions::descriptor(), + TestAllTypesProto2Editions::descriptor())); +} + +TEST(Proto3TestMessageTypesTest, LegacyOrder) { + EXPECT_THAT(Proto3TestMessageTypes(), + ElementsAre(TestAllTypesProto3::descriptor(), + TestAllTypesProto3Editions::descriptor())); +} + +TEST(Proto3TestMessageTypesTest, IsSubsetOfAllTestMessageTypes) { + EXPECT_THAT(Proto3TestMessageTypes(), IsSubsetOf(AllTestMessageTypes())); +} + +TEST(Proto2TestMessageTypesTest, LegacyOrder) { + EXPECT_THAT(Proto2TestMessageTypes(), + ElementsAre(TestAllTypesProto2::descriptor(), + TestAllTypesProto2Editions::descriptor())); +} + +TEST(Proto2TestMessageTypesTest, IsSubsetOfAllTestMessageTypes) { + EXPECT_THAT(Proto2TestMessageTypes(), IsSubsetOf(AllTestMessageTypes())); +} + +TEST(Proto2TestMessageTypesTest, TogetherWithProto3CoversAllTestMessageTypes) { + std::vector<const Descriptor*> both = Proto2TestMessageTypes(); + for (const Descriptor* message : Proto3TestMessageTypes()) { + both.push_back(message); + } + EXPECT_THAT(both, UnorderedElementsAreArray(AllTestMessageTypes())); +} + +TEST(ParamNameTest, StringKeepsOnlyLettersAndDigits) { + EXPECT_EQ(ParamName("Proto3"), "Proto3"); + EXPECT_EQ(ParamName("Editions_Proto2"), "EditionsProto2"); + EXPECT_EQ(ParamName("a-b.c d/e"), "abcde"); + const std::string underscored = "x_1"; + EXPECT_EQ(ParamName(underscored), "x1"); +} + +TEST(ParamNameTest, MessageTypeIsTheEditionIdentifierWithoutUnderscore) { + EXPECT_EQ(ParamName(TestAllTypesProto3::descriptor()), "Proto3"); + EXPECT_EQ(ParamName(TestAllTypesProto2::descriptor()), "Proto2"); + EXPECT_EQ(ParamName(TestAllTypesProto3Editions::descriptor()), + "EditionsProto3"); + EXPECT_EQ(ParamName(TestAllTypesProto2Editions::descriptor()), + "EditionsProto2"); + EXPECT_EQ(ParamName(TestAllTypesEditionUnstable::descriptor()), + "EditionUnstable"); +} + +TEST(ParamNameTest, FieldTypeIsTheUpperCaseTypeName) { + EXPECT_EQ(ParamName(FieldDescriptor::TYPE_INT32), "INT32"); + EXPECT_EQ(ParamName(FieldDescriptor::TYPE_SFIXED64), "SFIXED64"); + EXPECT_EQ(ParamName(FieldDescriptor::TYPE_MESSAGE), "MESSAGE"); +} + +TEST(ParamNameTest, IntIsDecimal) { + EXPECT_EQ(ParamName(0), "0"); + EXPECT_EQ(ParamName(7), "7"); + EXPECT_EQ(ParamName(1234), "1234"); +} + +TEST(ParamNameTest, MapTypeIsKeyAndValueJoinedByUnderscore) { + EXPECT_EQ(ParamName(MapType{FieldDescriptor::TYPE_INT32, + FieldDescriptor::TYPE_INT32}), + "INT32_INT32"); + EXPECT_EQ(ParamName(MapType{FieldDescriptor::TYPE_STRING, + FieldDescriptor::TYPE_MESSAGE}), + "STRING_MESSAGE"); +} + +// Matches a non-empty string of ASCII letters and digits. (Not +// MatchesRegex("[A-Za-z0-9]+"): gtest's own regex engine, which it falls back +// to on Windows, has no bracket expressions.) +MATCHER(IsAlphanumeric, negation ? "isn't alphanumeric" : "is alphanumeric") { + return !arg.empty() && std::all_of(arg.begin(), arg.end(), [](char c) { + return absl::ascii_isalnum(c); + }); +} + +// Every ParamName() result must be usable as (part of) a gtest parameter name. +TEST(ParamNameTest, ResultsAreValidGtestNameComponents) { + for (const Descriptor* message : AllTestMessageTypes()) { + EXPECT_THAT(ParamName(message), IsAlphanumeric()); + } + for (int i = 1; i <= FieldDescriptor::MAX_TYPE; ++i) { + EXPECT_THAT(ParamName(static_cast<FieldDescriptor::Type>(i)), + IsAlphanumeric()); + } + for (const MapType& type : ValidDataMapTypes()) { + std::vector<std::string> parts = absl::StrSplit(ParamName(type), '_'); + EXPECT_THAT(parts, ElementsAre(IsAlphanumeric(), IsAlphanumeric())); + } +} + +TEST(ParamNameDeathTest, StringWithoutLettersOrDigitsIsFatal) { + EXPECT_DEATH(ParamName("_-_"), "No letters or digits in parameter name"); +} + +TEST(ParamNameDeathTest, NegativeIntIsFatal) { + EXPECT_DEATH(ParamName(-1), "Negative parameters have no valid gtest name"); +} + } // namespace } // namespace conformance } // namespace protobuf
diff --git a/conformance/binary_unknown_fields_test.cc b/conformance/binary_unknown_fields_test.cc new file mode 100644 index 0000000..4fb5cc6 --- /dev/null +++ b/conformance/binary_unknown_fields_test.cc
@@ -0,0 +1,76 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2026 Google LLC. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +// Binary conformance tests checking that unknown fields are preserved, and +// preserved in order. This replaces the legacy +// BinaryAndJsonConformanceSuiteImpl<M>::TestUnknownMessage() and +// TestUnknownOrdering(); the test names and the requests sent to the testee +// are identical to the legacy ones. + +#include <cstdint> + +#include <gmock/gmock.h> +#include <gtest/gtest.h> +#include "conformance/binary_test_util.h" +#include "conformance/binary_wireformat.h" +#include "conformance/matchers.h" +#include "conformance/message_type_fixtures.h" +#include "conformance/testee.h" +#include "google/protobuf/unknown_field_set.h" + +namespace google { +namespace protobuf { +namespace conformance { +namespace { + +using ::testing::ValuesIn; + +// A field number no test message defines. +constexpr uint32_t kUnknownFieldNumber = 666; + +using UnknownFieldsTest = MessageTypeConformanceTest; + +// An unknown varint field must be round-tripped byte for byte. Field 501 +// isn't defined by any of the test messages; the legacy test spelled the input +// as the bytes "\xA8\x1F\x01". +TEST_P(UnknownFieldsTest, UnknownVarint) { + Wire input = VarintField(501, 1); + EXPECT_THAT( + Testee("UnknownVarint").ParseBinary(message(), input).SerializeBinary(), + Yields(RawPayload(input))); +} + +// Implementations must preserve the ordering of different unknown fields for +// the same field number. This is because some field types will accept +// multiple wire types for the same field. For example, repeated primitive +// fields will accept both length-prefixed (packed) and +// varint/fixed32/fixed64 (unpacked) wire types, and reordering these could +// reorder the elements of the repeated field. +TEST_P(UnknownFieldsTest, UnknownOrdering) { + UnknownFieldSet expected; + expected.AddLengthDelimited(kUnknownFieldNumber, "abc"); + expected.AddVarint(kUnknownFieldNumber, 123); + expected.AddLengthDelimited(kUnknownFieldNumber, "def"); + expected.AddVarint(kUnknownFieldNumber, 456); + // The legacy test sent the serialization of a message holding exactly + // these unknown fields; these are its bytes. + Wire input(LengthPrefixedField(kUnknownFieldNumber, "abc"), + VarintField(kUnknownFieldNumber, 123), + LengthPrefixedField(kUnknownFieldNumber, "def"), + VarintField(kUnknownFieldNumber, 456)); + EXPECT_THAT( + Testee("UnknownOrdering").ParseBinary(message(), input).SerializeBinary(), + Yields(WhenParsed(HasUnknownFieldsInOrder(expected)))); +} + +INSTANTIATE_TEST_SUITE_P(All, UnknownFieldsTest, + ValuesIn(AllTestMessageTypes()), MessageTypeParamName); + +} // namespace +} // namespace conformance +} // namespace protobuf +} // namespace google
diff --git a/conformance/binary_unknown_wire_type_test.cc b/conformance/binary_unknown_wire_type_test.cc new file mode 100644 index 0000000..49e91fe --- /dev/null +++ b/conformance/binary_unknown_wire_type_test.cc
@@ -0,0 +1,76 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2026 Google LLC. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +// Binary conformance tests checking that a tag with one of the two undefined +// wire types (6 and 7) is rejected. This replaces the legacy +// BinaryAndJsonConformanceSuiteImpl<M>::TestUnknownWireType(); the test names +// and the requests sent to the testee are identical to the legacy ones. + +#include <string> +#include <tuple> + +#include <gmock/gmock.h> +#include <gtest/gtest.h> +#include "absl/strings/str_cat.h" +#include "conformance/binary_test_util.h" +#include "conformance/binary_wireformat.h" +#include "conformance/matchers.h" +#include "conformance/message_type_fixtures.h" +#include "conformance/test_fixture.h" +#include "conformance/testee.h" +#include "google/protobuf/descriptor.h" + +namespace google { +namespace protobuf { +namespace conformance { +namespace { + +using ::testing::Combine; +using ::testing::Range; +using ::testing::Values; +using ::testing::ValuesIn; + +// Parameterized over (test message type, wire type, field number, value byte). +// Reporting the message type through MessageUnderTest() makes the base SetUp() +// skip the editions instances when --maximum_edition doesn't cover them. +class UnknownWireTypeTest : public ConformanceTest, + public testing::WithParamInterface< + std::tuple<const Descriptor*, int, int, int>> { + protected: + const Descriptor* message() const { return std::get<0>(GetParam()); } + int wire_type() const { return std::get<1>(GetParam()); } + int field_number() const { return std::get<2>(GetParam()); } + int value() const { return std::get<3>(GetParam()); } + + private: + const Descriptor* MessageUnderTest() const override { return message(); } +}; + +TEST_P(UnknownWireTypeTest, Rejected) { + // Two bytes: a one-byte tag with the unknown wire type, then one byte of + // "value". Tag() can't encode wire type 7, so the tag byte is assembled by + // hand exactly as the legacy test did. + const std::string payload = { + static_cast<char>((field_number() << 3) | wire_type()), + static_cast<char>(value())}; + EXPECT_THAT(Testee(absl::StrCat("UnknownWireType", wire_type(), "_Field", + field_number(), "_Version", value())) + .ParseBinary(message(), Wire(payload)) + .ParseOnly(), + Yields(IsParseError())); +} + +// Instance names are "<Message>_<wire type>_<field number>_<value>". +INSTANTIATE_TEST_SUITE_P(All, UnknownWireTypeTest, + Combine(ValuesIn(AllTestMessageTypes()), Values(6, 7), + Range(0, 4), Range(0, 4)), + TupleParamName<UnknownWireTypeTest::ParamType>); + +} // namespace +} // namespace conformance +} // namespace protobuf +} // namespace google
diff --git a/conformance/binary_unmatched_group_test.cc b/conformance/binary_unmatched_group_test.cc new file mode 100644 index 0000000..8dc1232 --- /dev/null +++ b/conformance/binary_unmatched_group_test.cc
@@ -0,0 +1,158 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2026 Google LLC. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +// Binary conformance tests checking that group tags without a matching start +// or end tag, or with mismatched field numbers, are rejected. This replaces +// the legacy BinaryAndJsonConformanceSuiteImpl<M>::TestUnmatchedGroup(); the +// test names and the requests sent to the testee are identical to the legacy +// ones. + +#include <gmock/gmock.h> +#include <gtest/gtest.h> +#include "conformance/binary_test_util.h" +#include "conformance/binary_wireformat.h" +#include "conformance/matchers.h" +#include "conformance/message_type_fixtures.h" +#include "conformance/testee.h" + +namespace google { +namespace protobuf { +namespace conformance { +namespace { + +using ::testing::ValuesIn; + +// Field numbers as in the legacy tests: 18 is optional_nested_message, 1 and +// 2 are scalar fields, 1234 is undefined, and 201-203 are the Data group and +// its members in the proto2 messages (wrapper fields in the proto3 ones). +using UnmatchedGroupTest = MessageTypeConformanceTest; + +TEST_P(UnmatchedGroupTest, UnmatchedEndGroup) { + EXPECT_THAT(Testee("UnmatchedEndGroup") + .ParseBinary(message(), Tag(201, WireType::kEndGroup)) + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(UnmatchedGroupTest, UnmatchedEndGroupUnknown) { + EXPECT_THAT(Testee("UnmatchedEndGroupUnknown") + .ParseBinary(message(), Tag(1234, WireType::kEndGroup)) + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(UnmatchedGroupTest, UnmatchedEndGroupWrongType) { + EXPECT_THAT(Testee("UnmatchedEndGroupWrongType") + .ParseBinary(message(), Tag(1, WireType::kEndGroup)) + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(UnmatchedGroupTest, UnmatchedEndGroupNestedLen) { + EXPECT_THAT( + Testee("UnmatchedEndGroupNestedLen") + .ParseBinary(message(), + LengthPrefixedField(18, Tag(1234, WireType::kEndGroup))) + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(UnmatchedGroupTest, UnmatchedEndGroupNested) { + EXPECT_THAT( + Testee("UnmatchedEndGroupNested") + .ParseBinary(message(), + DelimitedField(201, Tag(202, WireType::kEndGroup))) + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(UnmatchedGroupTest, UnmatchedEndGroupWithData) { + EXPECT_THAT( + Testee("UnmatchedEndGroupWithData") + .ParseBinary(message(), Wire(Tag(1, WireType::kEndGroup), + LengthPrefixedField(2, "hello world"))) + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(UnmatchedGroupTest, UnmatchedStartGroup) { + EXPECT_THAT(Testee("UnmatchedStartGroup") + .ParseBinary(message(), Tag(201, WireType::kStartGroup)) + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(UnmatchedGroupTest, UnmatchedStartGroupUnknown) { + EXPECT_THAT(Testee("UnmatchedStartGroupUnknown") + .ParseBinary(message(), Tag(1234, WireType::kStartGroup)) + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(UnmatchedGroupTest, UnmatchedStartGroupWrongType) { + EXPECT_THAT(Testee("UnmatchedStartGroupWrongType") + .ParseBinary(message(), Tag(1, WireType::kStartGroup)) + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(UnmatchedGroupTest, UnmatchedStartGroupNestedLen) { + EXPECT_THAT( + Testee("UnmatchedStartGroupNestedLen") + .ParseBinary(message(), LengthPrefixedField( + 18, Tag(1234, WireType::kStartGroup))) + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(UnmatchedGroupTest, UnmatchedStartGroupNested) { + EXPECT_THAT( + Testee("UnmatchedStartGroupNested") + .ParseBinary(message(), + DelimitedField(201, Tag(202, WireType::kStartGroup))) + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(UnmatchedGroupTest, UnmatchedStartGroupWithData) { + EXPECT_THAT( + Testee("UnmatchedStartGroupWithData") + .ParseBinary(message(), Wire(Tag(1, WireType::kStartGroup), + LengthPrefixedField(2, "hello world"))) + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(UnmatchedGroupTest, MismatchedGroupTags) { + EXPECT_THAT( + Testee("MismatchedGroupTags") + .ParseBinary(message(), Wire(Tag(201, WireType::kStartGroup), + LengthPrefixedField(2, "hello world"), + Tag(202, WireType::kEndGroup))) + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(UnmatchedGroupTest, MismatchedNestedGroupTags) { + EXPECT_THAT( + Testee("MismatchedNestedGroupTags") + .ParseBinary( + message(), + DelimitedField(201, Wire(Tag(202, WireType::kStartGroup), + LengthPrefixedField(2, "hello world"), + Tag(203, WireType::kEndGroup)))) + .ParseOnly(), + Yields(IsParseError())); +} + +INSTANTIATE_TEST_SUITE_P(All, UnmatchedGroupTest, + ValuesIn(AllTestMessageTypes()), MessageTypeParamName); + +} // namespace +} // namespace conformance +} // namespace protobuf +} // namespace google
diff --git a/conformance/binary_unstable_edition_test.cc b/conformance/binary_unstable_edition_test.cc new file mode 100644 index 0000000..4f2d700 --- /dev/null +++ b/conformance/binary_unstable_edition_test.cc
@@ -0,0 +1,89 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2026 Google LLC. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +// Binary conformance tests for TestAllTypesEditionUnstable, the test message +// declared under the unstable edition. This holds both the binary-output and +// the JSON-output leg of the legacy suite-level +// BinaryAndJsonConformanceSuite::RunUnstableTests(); the test names and the +// requests sent to the testee are identical to the legacy ones. +// +// Like the legacy suite, these run whenever --maximum_edition is 2023 or +// newer (see ConformanceTest::MessageUnderTest()), not only when it names +// the unstable edition itself. + +#include <gmock/gmock.h> +#include <gtest/gtest.h> +#include "conformance/binary_wireformat.h" +#include "conformance/matchers.h" +#include "conformance/test_fixture.h" +#include "conformance/test_protos/test_messages_edition_unstable.pb.h" +#include "conformance/testee.h" +#include "google/protobuf/descriptor.h" + +namespace google { +namespace protobuf { +namespace conformance { +namespace { + +using ::protobuf_test_messages::edition_unstable::TestAllTypesEditionUnstable; + +// Reporting TestAllTypesEditionUnstable through MessageUnderTest() makes the +// base SetUp() skip these tests when --maximum_edition doesn't cover it. +class UnstableEditionTest : public ConformanceTest { + protected: + static const Descriptor* message() { + return TestAllTypesEditionUnstable::descriptor(); + } + + private: + const Descriptor* MessageUnderTest() const override { return message(); } +}; + +// Field 13 is optional_bytes. +TEST_F(UnstableEditionTest, ValidBytes) { + EXPECT_THAT( + Testee("ValidBytes") + .ParseBinary(message(), LengthPrefixedField(13, "foo")) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(R"pb(optional_bytes: "foo")pb")))); +} + +TEST_F(UnstableEditionTest, ValidBytesJson) { + EXPECT_THAT( + Testee("ValidBytes") + .ParseBinary(message(), LengthPrefixedField(13, "foo")) + .SerializeJson(), + Yields(WhenParsed(EqualsTextProto(R"pb(optional_bytes: "foo")pb")))); +} + +// Field 15 is map_string_bytes; its entries have key = 1 and value = 2. +TEST_F(UnstableEditionTest, ValidMapBytes) { + EXPECT_THAT(Testee("ValidMap.Bytes") + .ParseBinary(message(), + LengthPrefixedField( + 15, Wire(LengthPrefixedField(1, "foo"), + LengthPrefixedField(2, "barbaz")))) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto( + R"pb(map_string_bytes { key: "foo" value: "barbaz" })pb")))); +} + +TEST_F(UnstableEditionTest, ValidMapBytesJson) { + EXPECT_THAT(Testee("ValidMap.Bytes") + .ParseBinary(message(), + LengthPrefixedField( + 15, Wire(LengthPrefixedField(1, "foo"), + LengthPrefixedField(2, "barbaz")))) + .SerializeJson(), + Yields(WhenParsed(EqualsTextProto( + R"pb(map_string_bytes { key: "foo" value: "barbaz" })pb")))); +} + +} // namespace +} // namespace conformance +} // namespace protobuf +} // namespace google
diff --git a/conformance/binary_utf8_test.cc b/conformance/binary_utf8_test.cc new file mode 100644 index 0000000..24056b8 --- /dev/null +++ b/conformance/binary_utf8_test.cc
@@ -0,0 +1,124 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2026 Google LLC. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +// Binary conformance tests checking that invalid UTF-8 in a string field is +// rejected, and that a bytes field accepts it. This replaces the legacy +// BinaryAndJsonConformanceSuiteImpl<M>::TestInvalidUtf8String() (which only +// ran for the proto3-style message types) and the suite-level +// BinaryAndJsonConformanceSuite::RunUtf8ValidationTests(); the test names and +// the requests sent to the testee are identical to the legacy ones. +// +// TODO: Once conformance tests can express failures that are +// not expected to be fixed, add proto2 coverage here. + +#include <gmock/gmock.h> +#include <gtest/gtest.h> +#include "absl/strings/string_view.h" +#include "conformance/binary_test_util.h" +#include "conformance/binary_wireformat.h" +#include "conformance/matchers.h" +#include "conformance/message_type_fixtures.h" +#include "conformance/testee.h" + +namespace google { +namespace protobuf { +namespace conformance { +namespace { + +using ::testing::ValuesIn; + +// Four bytes that aren't valid UTF-8. +constexpr absl::string_view kInvalidUtf8 = "\xA0\xB0\xC0\xD0"; + +// Instantiated over the proto3-style test message types only: proto3 rejects +// invalid UTF-8 in string fields, proto2 doesn't validate it. +class Utf8StringTest : public MessageTypeConformanceTest { + private: + TestPriority DefaultPriority() const override { return kP1; } +}; + +// Field 14 is optional_string. +TEST_P(Utf8StringTest, Singular) { + EXPECT_THAT(Testee("RejectInvalidUtf8.String.Singular") + .ParseBinary(message(), LengthPrefixedField(14, kInvalidUtf8)) + .ParseOnly(), + Yields(IsParseError())); +} + +// Field 44 is repeated_string. +TEST_P(Utf8StringTest, Repeated) { + EXPECT_THAT(Testee("RejectInvalidUtf8.String.Repeated") + .ParseBinary(message(), LengthPrefixedField(44, kInvalidUtf8)) + .ParseOnly(), + Yields(IsParseError())); +} + +// Field 113 is oneof_string. +TEST_P(Utf8StringTest, Oneof) { + EXPECT_THAT( + Testee("RejectInvalidUtf8.String.Oneof") + .ParseBinary(message(), LengthPrefixedField(113, kInvalidUtf8)) + .ParseOnly(), + Yields(IsParseError())); +} + +// Field 69 is map_string_string; its entries have key = 1 and value = 2. +TEST_P(Utf8StringTest, MapKey) { + EXPECT_THAT( + Testee("RejectInvalidUtf8.String.MapKey") + .ParseBinary( + message(), + LengthPrefixedField(69, Wire(LengthPrefixedField(1, kInvalidUtf8), + LengthPrefixedField(2, "foo")))) + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(Utf8StringTest, MapValue) { + EXPECT_THAT( + Testee("RejectInvalidUtf8.String.MapValue") + .ParseBinary(message(), + LengthPrefixedField( + 69, Wire(LengthPrefixedField(1, "foo"), + LengthPrefixedField(2, kInvalidUtf8)))) + .ParseOnly(), + Yields(IsParseError())); +} + +INSTANTIATE_TEST_SUITE_P(All, Utf8StringTest, + ValuesIn(Proto3TestMessageTypes()), + MessageTypeParamName); + +// The extension counterparts only exist for TestAllTypesEdition2023, where +// field 133 is the extension_string extension and field 134 is the +// extension_bytes extension. +using Utf8ExtensionTest = Edition2023ConformanceTest; + +TEST_F(Utf8ExtensionTest, String) { + EXPECT_THAT( + Testee(kP1, "RejectInvalidUtf8.String.Extension") + .ParseBinary(message(), LengthPrefixedField(133, kInvalidUtf8)) + .ParseOnly(), + Yields(IsParseError())); +} + +// Bytes fields aren't validated, so the same bytes must be accepted. +TEST_F(Utf8ExtensionTest, Bytes) { + constexpr absl::string_view kExpected = R"pb( + [protobuf_test_messages.editions.extension_bytes]: "\xA0\xB0\xC0\xD0" + )pb"; + EXPECT_THAT( + Testee("AcceptInvalidUtf8.Bytes.Extension") + .ParseBinary(message(), LengthPrefixedField(134, kInvalidUtf8)) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +} // namespace +} // namespace conformance +} // namespace protobuf +} // namespace google
diff --git a/conformance/binary_valid_data_map_test.cc b/conformance/binary_valid_data_map_test.cc new file mode 100644 index 0000000..c4f01b4 --- /dev/null +++ b/conformance/binary_valid_data_map_test.cc
@@ -0,0 +1,320 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2026 Google LLC. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +// Binary conformance tests for map fields: ValidDataMap.<KEY>.<VALUE>.<Case> +// (REQUIRED, equivalence) checks that a map entry is parsed correctly whatever +// the order, presence and repetition of its key and value, for every key/value +// type combination in ValidDataMapTypes(); and the RECOMMENDED +// .KeyWireTypeMismatch / .ValueWireTypeMismatch tests check that an entry +// whose key or value is encoded with the wrong wire type is either rejected or +// parsed as if that field were unknown. +// +// This replaces the legacy +// BinaryAndJsonConformanceSuiteImpl<M>::TestValidDataForMapType() and +// TestMapEntryWireTypeMismatch(); the test names and the requests sent to the +// testee are identical to the legacy ones. Like the legacy +// RunValidProtobufTest(), each ValidDataMap input is sent twice: once to be +// serialized as binary ("<name>.ProtobufOutput") and once as JSON +// ("<name>.JsonOutput"), and both outputs must be equivalent to the same +// reference message. The wire-type mismatch tests were binary-only. + +#include <cstdint> +#include <tuple> + +#include <gmock/gmock.h> +#include <gtest/gtest.h> +#include "absl/strings/str_cat.h" +#include "absl/strings/string_view.h" +#include "conformance/binary_test_util.h" +#include "conformance/binary_wireformat.h" +#include "conformance/matchers.h" +#include "conformance/message_type_fixtures.h" +#include "conformance/test_fixture.h" +#include "conformance/testee.h" +#include "google/protobuf/descriptor.h" + +namespace google { +namespace protobuf { +namespace conformance { +namespace { + +using ::testing::AnyOf; +using ::testing::Combine; +using ::testing::ValuesIn; + +// Map entries are messages with key = 1 and value = 2. +constexpr uint32_t kKeyField = 1; +constexpr uint32_t kValueField = 2; + +// The five key/value type combinations the legacy suite ran the wire-type +// mismatch tests over, in its order: varint, fixed32 and bool scalars, string, +// and a message-valued map. +constexpr MapType kMapEntryWireTypeMismatchTypes[] = { + {FieldDescriptor::TYPE_INT32, FieldDescriptor::TYPE_INT32}, + {FieldDescriptor::TYPE_FIXED32, FieldDescriptor::TYPE_FIXED32}, + {FieldDescriptor::TYPE_BOOL, FieldDescriptor::TYPE_BOOL}, + {FieldDescriptor::TYPE_STRING, FieldDescriptor::TYPE_STRING}, + {FieldDescriptor::TYPE_STRING, FieldDescriptor::TYPE_MESSAGE}, +}; + +// Field `field_number` encoded with a wire type other than +// `correct_wire_type`: a one-byte length-delimited "a" for scalar fields, and +// the varint 1 for length-delimited ones. +Wire MismatchedWireTypeField(uint32_t field_number, + WireType correct_wire_type) { + if (correct_wire_type == WireType::kLengthPrefixed) { + return VarintField(field_number, 1); + } + return LengthPrefixedField(field_number, "a"); +} + +// Common part of the fixtures below: parameterized over (test message type, +// map type), exercising the message's map field with those key and value +// types. Reporting the message type through MessageUnderTest() makes the base +// SetUp() skip the editions instances when --maximum_edition doesn't cover +// them. +class MapTestBase : public ConformanceTest, + public testing::WithParamInterface< + std::tuple<const Descriptor*, MapType>> { + protected: + const Descriptor* message() const { return std::get<0>(GetParam()); } + FieldDescriptor::Type key_type() const { return std::get<1>(GetParam()).key; } + FieldDescriptor::Type value_type() const { + return std::get<1>(GetParam()).value; + } + + // The key and value fields of an entry holding the default and the + // non-default sample value of their types (GetDefaultValue() and + // GetNonDefaultValue() with the entry field's tag in front). + Wire DefaultKeyField() const { return Key(GetDefaultValue(key_type())); } + Wire NonDefaultKeyField() const { + return Key(GetNonDefaultValue(key_type())); + } + Wire DefaultValueField() const { + return Value(GetDefaultValue(value_type())); + } + Wire NonDefaultValueField() const { + return Value(GetNonDefaultValue(value_type())); + } + + // The key and value fields of an entry encoded with the wrong wire type. + Wire MismatchedKeyField() const { + return MismatchedWireTypeField(kKeyField, WireTypeForFieldType(key_type())); + } + Wire MismatchedValueField() const { + return MismatchedWireTypeField(kValueField, + WireTypeForFieldType(value_type())); + } + + // `entry` as one entry of the message's map field for (key_type(), + // value_type()). This scans the descriptor (and check-fails if there is no + // such field), hence the function-style name. + Wire MapEntry(const Wire& entry) const { + return LengthPrefixedField( + FieldNumber(*GetFieldForMapType(*message(), key_type(), value_type())), + entry); + } + + // The legacy test name: "ValidDataMap.<KEY>.<VALUE>.<suffix>", e.g. + // "ValidDataMap.INT32.INT32.Default". + std::string TestName(absl::string_view suffix) const { + return absl::StrCat("ValidDataMap.", UpperCaseTypeName(key_type()), ".", + UpperCaseTypeName(value_type()), ".", suffix); + } + + private: + Wire Key(const Wire& payload) const { + return Wire(Tag(kKeyField, WireTypeForFieldType(key_type())), payload); + } + Wire Value(const Wire& payload) const { + return Wire(Tag(kValueField, WireTypeForFieldType(value_type())), payload); + } + + const Descriptor* MessageUnderTest() const override { return message(); } +}; + +// Every entry below must parse to the same map as a conforming parser (the +// C++ implementation) reads from it. +class ValidDataMapTest : public MapTestBase {}; + +// An entry with the default key and value, both present. +TEST_P(ValidDataMapTest, Default) { + const Wire input = MapEntry(Wire(DefaultKeyField(), DefaultValueField())); + EXPECT_THAT(Testee(TestName("Default")) + .ParseBinary(message(), input) + .SerializeBinary(), + Yields(WhenParsed(EqualsBinaryProto(input)))); +} + +TEST_P(ValidDataMapTest, DefaultJson) { + const Wire input = MapEntry(Wire(DefaultKeyField(), DefaultValueField())); + EXPECT_THAT( + Testee(TestName("Default")).ParseBinary(message(), input).SerializeJson(), + Yields(WhenParsed(EqualsBinaryProto(input)))); +} + +// An empty entry: the key and value are both missing and take their defaults. +TEST_P(ValidDataMapTest, MissingDefault) { + const Wire input = MapEntry(Wire()); + EXPECT_THAT(Testee(TestName("MissingDefault")) + .ParseBinary(message(), input) + .SerializeBinary(), + Yields(WhenParsed(EqualsBinaryProto(input)))); +} + +TEST_P(ValidDataMapTest, MissingDefaultJson) { + const Wire input = MapEntry(Wire()); + EXPECT_THAT(Testee(TestName("MissingDefault")) + .ParseBinary(message(), input) + .SerializeJson(), + Yields(WhenParsed(EqualsBinaryProto(input)))); +} + +// An entry with a non-default key and value. +TEST_P(ValidDataMapTest, NonDefault) { + const Wire input = + MapEntry(Wire(NonDefaultKeyField(), NonDefaultValueField())); + EXPECT_THAT(Testee(TestName("NonDefault")) + .ParseBinary(message(), input) + .SerializeBinary(), + Yields(WhenParsed(EqualsBinaryProto(input)))); +} + +TEST_P(ValidDataMapTest, NonDefaultJson) { + const Wire input = + MapEntry(Wire(NonDefaultKeyField(), NonDefaultValueField())); + EXPECT_THAT(Testee(TestName("NonDefault")) + .ParseBinary(message(), input) + .SerializeJson(), + Yields(WhenParsed(EqualsBinaryProto(input)))); +} + +// The value before the key. +TEST_P(ValidDataMapTest, Unordered) { + const Wire input = + MapEntry(Wire(NonDefaultValueField(), NonDefaultKeyField())); + EXPECT_THAT(Testee(TestName("Unordered")) + .ParseBinary(message(), input) + .SerializeBinary(), + Yields(WhenParsed(EqualsBinaryProto(input)))); +} + +TEST_P(ValidDataMapTest, UnorderedJson) { + const Wire input = + MapEntry(Wire(NonDefaultValueField(), NonDefaultKeyField())); + EXPECT_THAT(Testee(TestName("Unordered")) + .ParseBinary(message(), input) + .SerializeJson(), + Yields(WhenParsed(EqualsBinaryProto(input)))); +} + +// Two entries with the same key: the second one replaces the first. +TEST_P(ValidDataMapTest, DuplicateKey) { + const Wire first = MapEntry(Wire(NonDefaultKeyField(), DefaultValueField())); + const Wire second = + MapEntry(Wire(NonDefaultKeyField(), NonDefaultValueField())); + EXPECT_THAT(Testee(TestName("DuplicateKey")) + .ParseBinary(message(), Wire(first, second)) + .SerializeBinary(), + Yields(WhenParsed(EqualsBinaryProto(second)))); +} + +TEST_P(ValidDataMapTest, DuplicateKeyJson) { + const Wire first = MapEntry(Wire(NonDefaultKeyField(), DefaultValueField())); + const Wire second = + MapEntry(Wire(NonDefaultKeyField(), NonDefaultValueField())); + EXPECT_THAT(Testee(TestName("DuplicateKey")) + .ParseBinary(message(), Wire(first, second)) + .SerializeJson(), + Yields(WhenParsed(EqualsBinaryProto(second)))); +} + +// The key twice within one entry: the last one wins. +TEST_P(ValidDataMapTest, DuplicateKeyInMapEntry) { + const Wire input = MapEntry( + Wire(DefaultKeyField(), NonDefaultKeyField(), NonDefaultValueField())); + EXPECT_THAT(Testee(TestName("DuplicateKeyInMapEntry")) + .ParseBinary(message(), input) + .SerializeBinary(), + Yields(WhenParsed(EqualsBinaryProto(input)))); +} + +TEST_P(ValidDataMapTest, DuplicateKeyInMapEntryJson) { + const Wire input = MapEntry( + Wire(DefaultKeyField(), NonDefaultKeyField(), NonDefaultValueField())); + EXPECT_THAT(Testee(TestName("DuplicateKeyInMapEntry")) + .ParseBinary(message(), input) + .SerializeJson(), + Yields(WhenParsed(EqualsBinaryProto(input)))); +} + +// The value twice within one entry: the last one wins (or, for message +// values, they merge: the same result here). +TEST_P(ValidDataMapTest, DuplicateValueInMapEntry) { + const Wire input = MapEntry( + Wire(NonDefaultKeyField(), DefaultValueField(), NonDefaultValueField())); + EXPECT_THAT(Testee(TestName("DuplicateValueInMapEntry")) + .ParseBinary(message(), input) + .SerializeBinary(), + Yields(WhenParsed(EqualsBinaryProto(input)))); +} + +TEST_P(ValidDataMapTest, DuplicateValueInMapEntryJson) { + const Wire input = MapEntry( + Wire(NonDefaultKeyField(), DefaultValueField(), NonDefaultValueField())); + EXPECT_THAT(Testee(TestName("DuplicateValueInMapEntry")) + .ParseBinary(message(), input) + .SerializeJson(), + Yields(WhenParsed(EqualsBinaryProto(input)))); +} + +INSTANTIATE_TEST_SUITE_P(All, ValidDataMapTest, + Combine(ValuesIn(AllTestMessageTypes()), + ValuesIn(ValidDataMapTypes())), + TupleParamName<ValidDataMapTest::ParamType>); + +// An entry whose key or value is encoded with the wrong wire type. A parser +// may reject it or treat the mismatched field as unknown, so that the key or +// value takes its default (the C++ implementation does the latter); what it +// must not do is decode the bytes as a value of the declared type. +class MapEntryWireTypeMismatchTest : public MapTestBase { + private: + TestPriority DefaultPriority() const override { return kP1; } +}; + +// The key has the wrong wire type; the value is still well formed. +TEST_P(MapEntryWireTypeMismatchTest, KeyWireTypeMismatch) { + const Wire input = + MapEntry(Wire(MismatchedKeyField(), NonDefaultValueField())); + EXPECT_THAT( + Testee(TestName("KeyWireTypeMismatch")) + .ParseBinary(message(), input) + .SerializeBinary(), + Yields(AnyOf(IsParseError(), WhenParsed(EqualsBinaryProto(input))))); +} + +// The value has the wrong wire type; the key is still well formed. +TEST_P(MapEntryWireTypeMismatchTest, ValueWireTypeMismatch) { + const Wire input = + MapEntry(Wire(NonDefaultKeyField(), MismatchedValueField())); + EXPECT_THAT( + Testee(TestName("ValueWireTypeMismatch")) + .ParseBinary(message(), input) + .SerializeBinary(), + Yields(AnyOf(IsParseError(), WhenParsed(EqualsBinaryProto(input))))); +} + +INSTANTIATE_TEST_SUITE_P( + All, MapEntryWireTypeMismatchTest, + Combine(ValuesIn(AllTestMessageTypes()), + ValuesIn(kMapEntryWireTypeMismatchTypes)), + TupleParamName<MapEntryWireTypeMismatchTest::ParamType>); + +} // namespace +} // namespace conformance +} // namespace protobuf +} // namespace google
diff --git a/conformance/binary_valid_data_repeated_test.cc b/conformance/binary_valid_data_repeated_test.cc new file mode 100644 index 0000000..ffe3717 --- /dev/null +++ b/conformance/binary_valid_data_repeated_test.cc
@@ -0,0 +1,258 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2026 Google LLC. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +// Binary conformance tests checking that a repeated field accepts every valid +// encoding of its values, packed or unpacked, and that the output uses the +// packing the field declares. For every packable type, ValidDataCases(type) +// is sent as a whole to the message's repeated field, in both encodings: +// ValidDataRepeated.<TYPE>.UnpackedInput / .PackedInput (REQUIRED, +// equivalence) and the byte-exact RECOMMENDED variants +// .<Un>PackedInput.DefaultOutput (the field with the message's default +// packing), .<Un>PackedInput.PackedOutput (the [packed = true] field) and +// .<Un>PackedInput.UnpackedOutput (the [packed = false] field). The +// non-packable types (string, bytes, message) only have the REQUIRED +// ValidDataRepeated.<TYPE>. +// +// This replaces the repeated-field part of the legacy +// BinaryAndJsonConformanceSuiteImpl<M>::TestValidDataForType(); the test names +// and the requests sent to the testee are identical to the legacy ones. +// +// Like the legacy RunValidBinaryProtobufTest() + RunValidProtobufToJsonTest() +// pair, each REQUIRED input is sent twice: once to be serialized as binary +// ("<name>.ProtobufOutput") and once as JSON ("<name>.JsonOutput"), both +// expecting the same canonical message. The byte-exact RECOMMENDED tests +// only have a binary leg, as before. + +#include <string> +#include <tuple> + +#include <gmock/gmock.h> +#include <gtest/gtest.h> +#include "absl/strings/str_cat.h" +#include "absl/strings/string_view.h" +#include "conformance/binary_test_util.h" +#include "conformance/binary_wireformat.h" +#include "conformance/matchers.h" +#include "conformance/message_type_fixtures.h" +#include "conformance/test_fixture.h" +#include "conformance/testee.h" +#include "google/protobuf/descriptor.h" + +namespace google { +namespace protobuf { +namespace conformance { +namespace { + +using ::testing::Combine; +using ::testing::ValuesIn; + +// Parameterized over (test message type, field type). Reporting the message +// type through MessageUnderTest() makes the base SetUp() skip the editions +// instances when --maximum_edition doesn't cover them. +class ValidDataRepeatedTest + : public ConformanceTest, + public testing::WithParamInterface< + std::tuple<const Descriptor*, FieldDescriptor::Type>> { + protected: + const Descriptor* message() const { return std::get<0>(GetParam()); } + FieldDescriptor::Type type() const { return std::get<1>(GetParam()); } + + // The message's repeated fields of type(): its first repeated field of + // that type, with the message's default packing (e.g. repeated_int32, or + // repeated_nested_enum for ENUM), and the ones declared [packed = true] and + // [packed = false]. These scan the descriptor (and check-fail if + // there is no such field), hence the function-style names. + const FieldDescriptor* RepeatedField() const { + return GetFieldForType(*message(), type(), /*repeated=*/true); + } + const FieldDescriptor* PackedField() const { + return GetFieldForType(*message(), type(), /*repeated=*/true, + Packedness::kPacked); + } + const FieldDescriptor* UnpackedField() const { + return GetFieldForType(*message(), type(), /*repeated=*/true, + Packedness::kUnpacked); + } + + // All cases of type() as `field`, one tag per value, as sent to the testee + // (the inputs) or as a conforming implementation encodes them (the canonical + // forms). + Wire UnpackedInput(const FieldDescriptor* field) const { + return UnpackedCases(field, &ValidDataCase::input); + } + Wire UnpackedExpected(const FieldDescriptor* field) const { + return UnpackedCases(field, &ValidDataCase::expected); + } + + // The same values in one packed `field`. + Wire PackedInput(const FieldDescriptor* field) const { + return PackedCases(field, &ValidDataCase::input); + } + Wire PackedExpected(const FieldDescriptor* field) const { + return PackedCases(field, &ValidDataCase::expected); + } + + // The canonical forms encoded as RepeatedField() declares: packed by + // default in proto3, unpacked in proto2. + Wire DefaultExpected() const { + return RepeatedField()->is_packed() ? PackedExpected(RepeatedField()) + : UnpackedExpected(RepeatedField()); + } + + // The legacy test name: "ValidDataRepeated.<TYPE><suffix>", e.g. + // "ValidDataRepeated.INT32.PackedInput.DefaultOutput". + std::string TestName(absl::string_view suffix = "") const { + return absl::StrCat("ValidDataRepeated.", UpperCaseTypeName(type()), + suffix); + } + + private: + // One `column` of every case of type(), each as its own occurrence of + // `field` ... + Wire UnpackedCases(const FieldDescriptor* field, + Wire ValidDataCase::* column) const { + const Wire tag = Tag(FieldNumber(*field), WireTypeForFieldType(type())); + std::string bytes; + for (const ValidDataCase& value : ValidDataCases(type())) { + absl::StrAppend(&bytes, tag.data(), (value.*column).data()); + } + return Wire(bytes); + } + + // ... or concatenated into one packed `field`. + Wire PackedCases(const FieldDescriptor* field, + Wire ValidDataCase::* column) const { + std::string bytes; + for (const ValidDataCase& value : ValidDataCases(type())) { + absl::StrAppend(&bytes, (value.*column).data()); + } + return LengthPrefixedField(FieldNumber(*field), bytes); + } + + TestPriority DefaultPriority() const override { return kP1; } + const Descriptor* MessageUnderTest() const override { return message(); } +}; + +// Both encodings of the default field must be accepted and yield the same +// values, whether serialized as binary or as JSON. +TEST_P(ValidDataRepeatedTest, UnpackedInput) { + EXPECT_THAT( + Testee(kP0, TestName(".UnpackedInput")) + .ParseBinary(message(), UnpackedInput(RepeatedField())) + .SerializeBinary(), + Yields(WhenParsed(EqualsBinaryProto(PackedExpected(RepeatedField()))))); +} + +TEST_P(ValidDataRepeatedTest, UnpackedInputJson) { + EXPECT_THAT( + Testee(kP0, TestName(".UnpackedInput")) + .ParseBinary(message(), UnpackedInput(RepeatedField())) + .SerializeJson(), + Yields(WhenParsed(EqualsBinaryProto(PackedExpected(RepeatedField()))))); +} + +TEST_P(ValidDataRepeatedTest, PackedInput) { + EXPECT_THAT( + Testee(kP0, TestName(".PackedInput")) + .ParseBinary(message(), PackedInput(RepeatedField())) + .SerializeBinary(), + Yields(WhenParsed(EqualsBinaryProto(PackedExpected(RepeatedField()))))); +} + +TEST_P(ValidDataRepeatedTest, PackedInputJson) { + EXPECT_THAT( + Testee(kP0, TestName(".PackedInput")) + .ParseBinary(message(), PackedInput(RepeatedField())) + .SerializeJson(), + Yields(WhenParsed(EqualsBinaryProto(PackedExpected(RepeatedField()))))); +} + +// The default field must be serialized with its declared packing whatever +// the input's encoding was ... +TEST_P(ValidDataRepeatedTest, UnpackedInputDefaultOutput) { + EXPECT_THAT(Testee(TestName(".UnpackedInput.DefaultOutput")) + .ParseBinary(message(), UnpackedInput(RepeatedField())) + .SerializeBinary(), + Yields(RawPayload(DefaultExpected()))); +} + +TEST_P(ValidDataRepeatedTest, PackedInputDefaultOutput) { + EXPECT_THAT(Testee(TestName(".PackedInput.DefaultOutput")) + .ParseBinary(message(), PackedInput(RepeatedField())) + .SerializeBinary(), + Yields(RawPayload(DefaultExpected()))); +} + +// ... and so must the explicitly packed ... +TEST_P(ValidDataRepeatedTest, UnpackedInputPackedOutput) { + EXPECT_THAT(Testee(TestName(".UnpackedInput.PackedOutput")) + .ParseBinary(message(), UnpackedInput(PackedField())) + .SerializeBinary(), + Yields(RawPayload(PackedExpected(PackedField())))); +} + +TEST_P(ValidDataRepeatedTest, PackedInputPackedOutput) { + EXPECT_THAT(Testee(TestName(".PackedInput.PackedOutput")) + .ParseBinary(message(), PackedInput(PackedField())) + .SerializeBinary(), + Yields(RawPayload(PackedExpected(PackedField())))); +} + +// ... and explicitly unpacked fields. +TEST_P(ValidDataRepeatedTest, UnpackedInputUnpackedOutput) { + EXPECT_THAT(Testee(TestName(".UnpackedInput.UnpackedOutput")) + .ParseBinary(message(), UnpackedInput(UnpackedField())) + .SerializeBinary(), + Yields(RawPayload(UnpackedExpected(UnpackedField())))); +} + +TEST_P(ValidDataRepeatedTest, PackedInputUnpackedOutput) { + EXPECT_THAT(Testee(TestName(".PackedInput.UnpackedOutput")) + .ParseBinary(message(), PackedInput(UnpackedField())) + .SerializeBinary(), + Yields(RawPayload(UnpackedExpected(UnpackedField())))); +} + +INSTANTIATE_TEST_SUITE_P(All, ValidDataRepeatedTest, + Combine(ValuesIn(AllTestMessageTypes()), + ValuesIn(PackableFieldTypes())), + TupleParamName<ValidDataRepeatedTest::ParamType>); + +// The non-packable types only have one encoding; the conformance name has no +// suffix. Of the inherited helpers only RepeatedField(), UnpackedInput() and +// UnpackedExpected() apply here; the packed ones would check-fail. +class ValidDataRepeatedNonPackableTest : public ValidDataRepeatedTest { + private: + TestPriority DefaultPriority() const override { return kP0; } +}; + +TEST_P(ValidDataRepeatedNonPackableTest, Repeated) { + EXPECT_THAT( + Testee(TestName()) + .ParseBinary(message(), UnpackedInput(RepeatedField())) + .SerializeBinary(), + Yields(WhenParsed(EqualsBinaryProto(UnpackedExpected(RepeatedField()))))); +} + +TEST_P(ValidDataRepeatedNonPackableTest, RepeatedJson) { + EXPECT_THAT( + Testee(TestName()) + .ParseBinary(message(), UnpackedInput(RepeatedField())) + .SerializeJson(), + Yields(WhenParsed(EqualsBinaryProto(UnpackedExpected(RepeatedField()))))); +} + +INSTANTIATE_TEST_SUITE_P( + All, ValidDataRepeatedNonPackableTest, + Combine(ValuesIn(AllTestMessageTypes()), + ValuesIn(LengthDelimitedFieldTypes())), + TupleParamName<ValidDataRepeatedNonPackableTest::ParamType>); + +} // namespace +} // namespace conformance +} // namespace protobuf +} // namespace google
diff --git a/conformance/binary_valid_data_scalar_test.cc b/conformance/binary_valid_data_scalar_test.cc new file mode 100644 index 0000000..cdbe7f2 --- /dev/null +++ b/conformance/binary_valid_data_scalar_test.cc
@@ -0,0 +1,200 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2026 Google LLC. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +// Binary conformance tests checking that every valid encoding of a singular +// field's value is parsed correctly: ValidDataScalar.<TYPE>[i] (REQUIRED, +// equivalence) and ValidDataScalarBinary.<TYPE>[i] (RECOMMENDED, byte-exact) +// for case i of ValidDataCases(type), and RepeatedScalarSelectsLast.<TYPE>, +// which sends all cases for the singular field at once and expects the last +// one. This replaces the singular-field part of the legacy +// BinaryAndJsonConformanceSuiteImpl<M>::TestValidDataForType(); the test names +// and the requests sent to the testee are identical to the legacy ones. +// +// Like the legacy RunValidBinaryProtobufTest() + RunValidProtobufToJsonTest() +// pair, each REQUIRED input is sent twice: once to be serialized as binary +// ("<name>.ProtobufOutput") and once as JSON ("<name>.JsonOutput"), both +// expecting the same canonical message. + +#include <cstddef> +#include <tuple> +#include <vector> + +#include <gmock/gmock.h> +#include <gtest/gtest.h> +#include "absl/strings/str_cat.h" +#include "absl/strings/string_view.h" +#include "conformance/binary_test_util.h" +#include "conformance/binary_wireformat.h" +#include "conformance/matchers.h" +#include "conformance/message_type_fixtures.h" +#include "conformance/test_fixture.h" +#include "conformance/testee.h" +#include "google/protobuf/descriptor.h" + +namespace google { +namespace protobuf { +namespace conformance { +namespace { + +using ::testing::Combine; +using ::testing::ValuesIn; + +// AllFieldTypesExceptGroup() without TYPE_MESSAGE: repeated occurrences of a +// singular message field are merged, not replaced, which binary_merge_test.cc +// covers. +std::vector<FieldDescriptor::Type> SelectsLastTypes() { + std::vector<FieldDescriptor::Type> types; + for (FieldDescriptor::Type type : AllFieldTypesExceptGroup()) { + if (type != FieldDescriptor::TYPE_MESSAGE) types.push_back(type); + } + return types; +} + +// Common part of the fixtures below: parameterized over a tuple whose first +// two elements are the test message type and the field type, exercising the +// message's singular field of that type. Reporting the message type through +// MessageUnderTest() makes the base SetUp() skip the editions instances when +// --maximum_edition doesn't cover them. +template <typename Param> +class ValidDataTestBase : public ConformanceTest, + public testing::WithParamInterface<Param> { + protected: + const Descriptor* message() const { return std::get<0>(this->GetParam()); } + FieldDescriptor::Type type() const { return std::get<1>(this->GetParam()); } + + // `payload` (a value without a tag) as the message's singular field of + // type(). This scans the descriptor (and check-fails if there is no such + // field), hence the function-style name. + Wire FieldWithValue(const Wire& payload) const { + return Wire(Tag(FieldNumber(*GetFieldForType(*message(), type(), + /*repeated=*/false)), + WireTypeForFieldType(type())), + payload); + } + + // The legacy test name: "<prefix>.<TYPE><suffix>", e.g. + // "ValidDataScalar.INT32[3]". + std::string TestName(absl::string_view prefix, + absl::string_view suffix = "") const { + return absl::StrCat(prefix, ".", UpperCaseTypeName(type()), suffix); + } + + private: + const Descriptor* MessageUnderTest() const override { return message(); } +}; + +// One gtest per (message type, field type, case index), so that every +// conformance test is its own gtest and no test body loops or skips. +class ValidDataScalarTest + : public ValidDataTestBase< + std::tuple<const Descriptor*, FieldDescriptor::Type, int>> { + protected: + int index() const { return std::get<2>(GetParam()); } + const ValidDataCase& value() const { + return ValidDataCases(type())[static_cast<size_t>(index())]; + } + + // The canonical serialization of the parsed input. A proto3 message + // doesn't serialize a singular scalar field holding its default value, so + // the expected output is then empty; this is the legacy suite's proto3 + // rule. + Wire Expected() const { + if (HasImplicitPresence(*message()) && + IsDefaultValue(type(), value().expected)) { + return Wire(); + } + return FieldWithValue(value().expected); + } + + std::string TestName(absl::string_view prefix) const { + return ValidDataTestBase::TestName(prefix, absl::StrCat("[", index(), "]")); + } +}; + +// AllTestMessageTypes() x AllFieldTypesExceptGroup() x the case indices of each +// type. +std::vector<ValidDataScalarTest::ParamType> ValidDataScalarParams() { + std::vector<ValidDataScalarTest::ParamType> params; + for (const Descriptor* message : AllTestMessageTypes()) { + for (FieldDescriptor::Type type : AllFieldTypesExceptGroup()) { + const int count = static_cast<int>(ValidDataCases(type).size()); + for (int i = 0; i < count; ++i) params.emplace_back(message, type, i); + } + } + return params; +} + +// The parsed value must be equivalent to the case's canonical form ... +TEST_P(ValidDataScalarTest, Scalar) { + EXPECT_THAT(Testee(TestName("ValidDataScalar")) + .ParseBinary(message(), FieldWithValue(value().input)) + .SerializeBinary(), + Yields(WhenParsed(EqualsBinaryProto(Expected())))); +} + +// ... also when serialized as JSON ... +TEST_P(ValidDataScalarTest, ScalarJson) { + EXPECT_THAT(Testee(TestName("ValidDataScalar")) + .ParseBinary(message(), FieldWithValue(value().input)) + .SerializeJson(), + Yields(WhenParsed(EqualsBinaryProto(Expected())))); +} + +// ... and its binary serialization must be exactly the canonical form. +TEST_P(ValidDataScalarTest, ScalarBinary) { + EXPECT_THAT(Testee(kP1, TestName("ValidDataScalarBinary")) + .ParseBinary(message(), FieldWithValue(value().input)) + .SerializeBinary(), + Yields(RawPayload(Expected()))); +} + +INSTANTIATE_TEST_SUITE_P(All, ValidDataScalarTest, + ValuesIn(ValidDataScalarParams()), + TupleParamName<ValidDataScalarTest::ParamType>); + +// Several occurrences of a singular scalar field: the last one wins. The +// input is every case of the type in order, so the last case's canonical form +// is expected. Unlike the singular tests above, the legacy test didn't apply +// the proto3 default rule here (no type's last case is a default value +// anyway). +class RepeatedScalarSelectsLastTest + : public ValidDataTestBase< + std::tuple<const Descriptor*, FieldDescriptor::Type>> {}; + +TEST_P(RepeatedScalarSelectsLastTest, SelectsLast) { + Wire input; + for (const ValidDataCase& value : ValidDataCases(type())) { + input = Wire(input, FieldWithValue(value.input)); + } + EXPECT_THAT(Testee(TestName("RepeatedScalarSelectsLast")) + .ParseBinary(message(), input) + .SerializeBinary(), + Yields(WhenParsed(EqualsBinaryProto( + FieldWithValue(ValidDataCases(type()).back().expected))))); +} + +TEST_P(RepeatedScalarSelectsLastTest, SelectsLastJson) { + Wire input; + for (const ValidDataCase& value : ValidDataCases(type())) { + input = Wire(input, FieldWithValue(value.input)); + } + EXPECT_THAT(Testee(TestName("RepeatedScalarSelectsLast")) + .ParseBinary(message(), input) + .SerializeJson(), + Yields(WhenParsed(EqualsBinaryProto( + FieldWithValue(ValidDataCases(type()).back().expected))))); +} + +INSTANTIATE_TEST_SUITE_P( + All, RepeatedScalarSelectsLastTest, + Combine(ValuesIn(AllTestMessageTypes()), ValuesIn(SelectsLastTypes())), + TupleParamName<RepeatedScalarSelectsLastTest::ParamType>); + +} // namespace +} // namespace conformance +} // namespace protobuf +} // namespace google
diff --git a/conformance/bzl_tests/BUILD b/conformance/bzl_tests/BUILD new file mode 100644 index 0000000..f355b12 --- /dev/null +++ b/conformance/bzl_tests/BUILD
@@ -0,0 +1,59 @@ +# Copyright (c) 2026, Google LLC +# All rights reserved. +# +# Use of this source code is governed by a BSD-style +# license that can be found in the LICENSE file or at +# https://developers.google.com/open-source/licenses/bsd + +# Analysis tests for //conformance:conformance.bzl. They +# live in this leaf package, which nothing depends on, because they load +# rules_testing: a dev-only dependency of the open-source build that the +# exported conformance package itself must not require. + +load("@bazel_skylib//:bzl_library.bzl", "bzl_library") +load("@rules_cc//cc:cc_binary.bzl", "cc_binary") +load("@rules_cc//cc:defs.bzl", "cc_library") +load("//conformance:conformance.bzl", "failure_lists") +load(":conformance_bzl_test.bzl", "conformance_bzl_test_suite") + +package(default_applicable_licenses = ["//:license"]) + +# The analysis tests and their subjects (manual conformance_tests that are +# never run). The subjects' failure lists are the fixtures in +# bzl_test_failure_lists/, and their testees are the stubs below. +conformance_bzl_test_suite(name = "conformance_bzl_test") + +failure_lists(name = "bzl_test_failure_lists") + +cc_binary( + name = "stub_testee", + testonly = 1, + srcs = ["stub_testee.cc"], +) + +# The in_process = True counterpart: a library that would define +# MakeTesteeRunner() (see testee_runner.h) if it were ever linked. +cc_library( + name = "stub_in_process_testee", + testonly = 1, + srcs = ["stub_in_process_testee.cc"], + deps = [ + "//conformance:test_runner", + "//conformance:testee_runner", + "@abseil-cpp//absl/strings:string_view", + "@abseil-cpp//absl/types:span", + ], + alwayslink = 1, +) + +bzl_library( + name = "conformance_bzl_test_bzl", + srcs = ["conformance_bzl_test.bzl"], + visibility = ["//visibility:private"], + deps = [ + "//conformance:conformance_bzl", + "@rules_testing//lib:analysis_test_bzl", + "@rules_testing//lib:truth_bzl", + "@rules_testing//lib:util_bzl", + ], +)
diff --git a/conformance/bzl_tests/bzl_test_failure_lists/conformance_bzl_test_with_failure_list_subject_binary.txt b/conformance/bzl_tests/bzl_test_failure_lists/conformance_bzl_test_with_failure_list_subject_binary.txt new file mode 100644 index 0000000..7e3c8a4 --- /dev/null +++ b/conformance/bzl_tests/bzl_test_failure_lists/conformance_bzl_test_with_failure_list_subject_binary.txt
@@ -0,0 +1,2 @@ +# Fixture for conformance_bzl_test (see conformance_bzl_test.bzl); the test +# it belongs to is never run.
diff --git a/conformance/bzl_tests/bzl_test_failure_lists/conformance_bzl_test_with_failure_list_subject_performance.txt b/conformance/bzl_tests/bzl_test_failure_lists/conformance_bzl_test_with_failure_list_subject_performance.txt new file mode 100644 index 0000000..7e3c8a4 --- /dev/null +++ b/conformance/bzl_tests/bzl_test_failure_lists/conformance_bzl_test_with_failure_list_subject_performance.txt
@@ -0,0 +1,2 @@ +# Fixture for conformance_bzl_test (see conformance_bzl_test.bzl); the test +# it belongs to is never run.
diff --git a/conformance/bzl_tests/conformance_bzl_test.bzl b/conformance/bzl_tests/conformance_bzl_test.bzl new file mode 100644 index 0000000..a6cd8b7 --- /dev/null +++ b/conformance/bzl_tests/conformance_bzl_test.bzl
@@ -0,0 +1,275 @@ +# Protocol Buffers - Google's data interchange format +# Copyright 2026 Google LLC. All rights reserved. +# +# Use of this source code is governed by a BSD-style +# license that can be found in the LICENSE file or at +# https://developers.google.com/open-source/licenses/bsd + +"""Analysis tests for the conformance_test() macro in conformance.bzl. + +These check the targets the macro generates and the command lines it passes +to them. The `fail()` paths (unknown suite, misnamed failure list, foreign +failure_lists) happen at loading time and so can't be covered here. + +The tests live in their own leaf package (bzl_tests) because they load +rules_testing, which is only a dev dependency of the open-source build; the +exported conformance package must not depend on it. +""" + +load( + "@rules_testing//lib:analysis_test.bzl", + "analysis_test", +) +load( + "@rules_testing//lib:truth.bzl", + "matching", + "subjects", +) +load("@rules_testing//lib:util.bzl", "util") +load("//conformance:conformance.bzl", "conformance_test") + +# The failure_lists() the subjects use, and the stub testees (a binary and an +# in-process library); see BUILD. +_FAILURE_LISTS = "bzl_test_failure_lists" +_TESTEE = ":stub_testee" +_IN_PROCESS_TESTEE = ":stub_in_process_testee" + +def conformance_bzl_test_suite(name): + """Creates the analysis tests for conformance_test() and a suite of them. + + Every generated target is prefixed with `name`. The `with_failure_list` + test needs the fixtures `<name>_with_failure_list_subject_binary.txt` and + `<name>_with_failure_list_subject_performance.txt` in the package's + failure lists directory. + + Args: + name: The name of the test_suite, and the prefix of its tests. + """ + tests = [] + for suffix, setup in [ + ("with_failure_list", _test_with_failure_list), + ("without_failure_list", _test_without_failure_list), + ("options", _test_options), + ("in_process", _test_in_process), + ]: + test_name = "%s_%s" % (name, suffix) + setup(test_name) + tests.append(":" + test_name) + native.test_suite( + name = name, + tests = tests, + ) + +# The conformance_test() under test can't be named after the analysis test, +# since its test_suite would clash with it. +def _subject(name): + return name + "_subject" + +def _subject_of(target, suite = "binary"): + """The name of the conformance_test() whose `suite` test is `target`.""" + return target.label.name.removesuffix("_%s_test" % suite) + +def _failure_list_path(target, suite = "binary"): + """The workspace-relative path of `target`'s failure list. + + `target` is the `suite` test of a conformance_test(). + """ + return "%s/%s/%s_%s.txt" % ( + target.label.package, + _FAILURE_LISTS, + _subject_of(target, suite), + suite, + ) + +def _label_names(value, *, meta): + """Subject factory for a label list attribute, comparing target names. + + Labels from other repositories are ignored: those are implicit dependencies + added by the rule itself (e.g. @bazel_tools//tools/cpp:link_extra_lib on a + cc_test), not by the macro under test. + """ + return subjects.collection( + [target.label.name for target in value if not target.label.repo_name], + meta = meta, + ) + +def _test_with_failure_list(name): + for suite in ("binary", "performance"): + fixture = "%s/%s_%s.txt" % (_FAILURE_LISTS, _subject(name), suite) + if not native.glob([fixture], allow_empty = True): + fail("%s: the fixture failure list %s doesn't exist." % + (name, fixture)) + util.helper_target( + conformance_test, + name = _subject(name), + testee = _TESTEE, + failure_lists = ":" + _FAILURE_LISTS, + ) + analysis_test( + name = name, + targets = { + "suite": _subject(name), + "test": _subject(name) + "_binary_test", + "text_test": _subject(name) + "_text_test", + "performance_test": _subject(name) + "_performance_test", + "testee": _TESTEE, + }, + impl = _test_with_failure_list_impl, + ) + +def _test_with_failure_list_impl(env, targets): + test = env.expect.that_target(targets.test) + failure_list = _failure_list_path(targets.test) + test.attr("args", factory = subjects.collection).contains_exactly([ + "--testee_binary=$(rootpath %s)" % _TESTEE, + "--failure_list=" + failure_list, + ]).in_order() + test.attr("deps", factory = _label_names).contains_exactly([ + "binary_conformance_tests", + "test_environment_main", + "forked_testee", + ]) + test.tags().contains("conformance") + + # The testee's path is taken from the target rather than spelled out so + # that the check also holds where the binary is stub_testee.exe. + testee = targets.testee[DefaultInfo].files_to_run.executable + test.runfiles().contains_at_least([ + "{workspace}/" + failure_list, + "{workspace}/" + testee.short_path, + ]) + + # The text suite's test links that suite instead; it has no fixture list, + # so --fix is told where to create one (see _test_without_failure_list). + text_test = env.expect.that_target(targets.text_test) + text_failure_list = _failure_list_path(targets.text_test, suite = "text") + text_test.attr("args", factory = subjects.collection).contains_exactly([ + "--testee_binary=$(rootpath %s)" % _TESTEE, + "--fix_output_file=" + text_failure_list, + ]).in_order() + text_test.attr("deps", factory = _label_names).contains_exactly([ + "text_conformance_tests", + "test_environment_main", + "forked_testee", + ]) + + # The performance suite is one of the default suites: it gets a test of + # its own, built like the others from its own library and failure list. + performance_test = env.expect.that_target(targets.performance_test) + performance_failure_list = _failure_list_path( + targets.performance_test, + suite = "performance", + ) + args_subject = performance_test.attr("args", factory = subjects.collection) + args_subject.contains_exactly([ + "--testee_binary=$(rootpath %s)" % _TESTEE, + "--failure_list=" + performance_failure_list, + ]).in_order() + performance_test.attr("deps", factory = _label_names).contains_exactly([ + "performance_conformance_tests", + "test_environment_main", + "forked_testee", + ]) + performance_test.runfiles().contains_at_least([ + "{workspace}/" + performance_failure_list, + ]) + + # No suite is kept off the sanitizers any more: the macro adds no tag but + # "conformance" to the caller's. + performance_test.tags().contains("conformance") + performance_test.tags().not_contains("noasan") + test.tags().not_contains("noasan") + + # One test per known conformance suite (see _SUITES in conformance.bzl). + suite = env.expect.that_target(targets.suite) + suite.attr("tests", factory = _label_names).contains_exactly([ + _subject_of(targets.test) + "_binary_test", + _subject_of(targets.test) + "_text_test", + _subject_of(targets.test) + "_json_test", + _subject_of(targets.test) + "_performance_test", + ]) + + # helper_target tags the tests manual (among others) so that they never run + # outside these analysis tests; the tags must reach the test_suite too. + suite.tags().contains("manual") + +def _test_without_failure_list(name): + util.helper_target( + conformance_test, + name = _subject(name), + testee = _TESTEE, + failure_lists = ":" + _FAILURE_LISTS, + ) + analysis_test( + name = name, + target = _subject(name) + "_binary_test", + impl = _test_without_failure_list_impl, + ) + +def _test_without_failure_list_impl(env, target): + # With no failure list, --fix is told where to create one instead. + test = env.expect.that_target(target) + test.attr("args", factory = subjects.collection).contains_exactly([ + "--testee_binary=$(rootpath %s)" % _TESTEE, + "--fix_output_file=" + _failure_list_path(target), + ]).in_order() + +def _test_options(name): + util.helper_target( + conformance_test, + name = _subject(name), + testee = _TESTEE, + failure_lists = ":" + _FAILURE_LISTS, + suites = ["binary"], + maximum_edition = "2023", + enforcement_level = 0, + tags = ["noasan"], + ) + analysis_test( + name = name, + target = _subject(name) + "_binary_test", + impl = _test_options_impl, + ) + +def _test_options_impl(env, target): + test = env.expect.that_target(target) + test.attr("args", factory = subjects.collection).contains_exactly([ + "--testee_binary=$(rootpath %s)" % _TESTEE, + "--fix_output_file=" + _failure_list_path(target), + "--maximum_edition=EDITION_2023", + "--enforcement_level=0", + ]).in_order() + test.tags().contains_at_least(["conformance", "noasan"]) + +def _test_in_process(name): + util.helper_target( + conformance_test, + name = _subject(name), + testee = _IN_PROCESS_TESTEE, + failure_lists = ":" + _FAILURE_LISTS, + suites = ["binary"], + in_process = True, + ) + analysis_test( + name = name, + target = _subject(name) + "_binary_test", + impl = _test_in_process_impl, + ) + +def _test_in_process_impl(env, target): + # The testee is linked in place of forked_testee and gets no + # --testee_binary; the other flags are unchanged. + test = env.expect.that_target(target) + test.attr("args", factory = subjects.collection).contains_exactly([ + "--fix_output_file=" + _failure_list_path(target), + ]).in_order() + test.attr("deps", factory = _label_names).contains_exactly([ + "binary_conformance_tests", + "test_environment_main", + "stub_in_process_testee", + ]) + test.attr("data", factory = _label_names).contains_exactly([_FAILURE_LISTS]) + test.runfiles().not_contains_predicate( + matching.str_matches("*/stub_testee*"), + ) + test.tags().contains("conformance")
diff --git a/conformance/bzl_tests/stub_in_process_testee.cc b/conformance/bzl_tests/stub_in_process_testee.cc new file mode 100644 index 0000000..e623b7e --- /dev/null +++ b/conformance/bzl_tests/stub_in_process_testee.cc
@@ -0,0 +1,32 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2026 Google LLC. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +// A stand-in in-process testee for the analysis tests of +// conformance_test(in_process = True) (see conformance_bzl_test.bzl). It is +// only ever analyzed, never linked or run. + +#include <memory> +#include <string> + +#include "absl/strings/string_view.h" +#include "absl/types/span.h" +#include "conformance/test_runner.h" +#include "conformance/testee_runner.h" + +namespace google { +namespace protobuf { +namespace conformance { + +std::unique_ptr<ConformanceTestRunner> MakeTesteeRunner( + absl::string_view testee_binary, + absl::Span<const std::string> testee_args) { + return nullptr; +} + +} // namespace conformance +} // namespace protobuf +} // namespace google
diff --git a/conformance/bzl_tests/stub_testee.cc b/conformance/bzl_tests/stub_testee.cc new file mode 100644 index 0000000..eb1be33 --- /dev/null +++ b/conformance/bzl_tests/stub_testee.cc
@@ -0,0 +1,10 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2026 Google LLC. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +// A stand-in testee for the analysis tests of conformance_test() (see +// conformance_bzl_test.bzl). It is only ever analyzed, never run. +int main() { return 0; }
diff --git a/conformance/conformance.bzl b/conformance/conformance.bzl new file mode 100644 index 0000000..e66880b --- /dev/null +++ b/conformance/conformance.bzl
@@ -0,0 +1,308 @@ +# Protocol Buffers - Google's data interchange format +# Copyright 2026 Google LLC. All rights reserved. +# +# Use of this source code is governed by a BSD-style +# license that can be found in the LICENSE file or at +# https://developers.google.com/open-source/licenses/bsd + +"""Starlark macros for the gtest-based Protobuf conformance tests. + +A conformance *suite* (e.g. `binary`) is a `cc_library` of gtest-based tests +that drive a testee through the conformance protocol; see +`conformance_suite()`. A conformance *test* runs every suite against one +testee, one `cc_test` per suite, with that testee's failure lists; see +`conformance_test()` and `failure_lists()`. The testee is either a binary +that the test forks and talks to over a pipe, or (`in_process = True`) a +`cc_library` linked into the test binary itself. + +Typical use, in the package holding the testee: + + failure_lists(name = "failure_lists") + + conformance_test( + name = "cpp", + testee = ":conformance_cpp", + failure_lists = ":failure_lists", + ) + +This creates `:cpp_binary_test`, `:cpp_json_test`, `:cpp_performance_test` +and `:cpp_text_test` (one `:cpp_<suite>_test` per suite), plus a `:cpp` +test_suite running all of them. The expected failures for `:cpp_binary_test` +live in `failure_lists/cpp_binary.txt`; when that file doesn't exist, the +testee is expected to pass everything. Either way + + blaze run :cpp_binary_test -- --fix + +creates or rewrites the file to match the observed results. + +conformance_test() supersedes the macro of the same name in defs.bzl, which +wraps the conformance_test_runner command line (legacy `--failure_list` / +`--text_format_failure_list` flags) and stays until the language runtimes +have migrated to this macro. +TODO: retire defs.bzl once they have. + +PLEASE DO NOT DEPEND ON THE CONTENTS OF THIS FILE, IT IS UNSTABLE. +""" + +load("@rules_cc//cc:cc_test.bzl", "cc_test") +load("@rules_cc//cc:defs.bzl", "cc_library") + +# The conformance suites, i.e. the valid `name`s of conformance_suite(). +# conformance_test() creates one test per suite, and failure lists are named +# after them (see failure_lists()). Every suite but `performance` covers one +# wire format; `performance` holds the tests that stress the testee (deeply +# nested and very large payloads) for every format, which are kept apart +# because they are slow and their expected failures are unrelated to the +# format's. Every gtest fixture in the performance suite is named +# `*PerformanceTest` and no fixture outside it is: that is how +# conformance_test_runner's --performance tells them apart (see +# test_fixture.h). +_SUITES = ["binary", "json", "performance", "text"] + +# The gtest main that turns a suite library into a conformance test binary, and +# the testee it links by default: --testee_binary run as a subprocess (see +# testee_runner.h for how the testee is chosen at link time). +_TEST_ENVIRONMENT_MAIN = Label( + "//conformance:test_environment_main", +) +_FORKED_TESTEE = Label("//conformance:forked_testee") + +def _suite_library(suite): + """The label of the cc_library that conformance_suite(suite) creates.""" + return Label( + "//conformance:%s_conformance_tests" % suite, + ) + +def _check_suite(suite, context): + if suite not in _SUITES: + fail("%s: unknown conformance suite %r; the known suites are %s." % + (context, suite, ", ".join(_SUITES))) + +def conformance_suite(name, srcs, deps = [], **kwargs): + """Defines a conformance suite: a library of gtest-based conformance tests. + + Creates `cc_library(name = name + "_conformance_tests", alwayslink = 1, + testonly = 1, ...)`; no target named `name` itself is created, so + depend on `:<name>_conformance_tests`. Linking it into a binary that uses + `:test_environment_main` (which conformance_test() does) registers every + test in `srcs`; the library itself must not depend on any gtest main. + + Must be used in //conformance, which is where + conformance_test() looks the suite libraries up. + + Args: + name: The suite's name, one of the known suites (`binary`, ...). + srcs: The test sources. Fixtures in the `performance` suite must be + named `*PerformanceTest`; fixtures in any other suite must not be. + deps: Their dependencies, e.g. `:test_fixture` and `:matchers`. + **kwargs: Passed through to the cc_library (e.g. `visibility`). + `testonly` and `alwayslink` are set by the macro and can't be given. + """ + _check_suite(name, "conformance_suite(%r)" % name) + library = _suite_library(name) + if native.package_relative_label(":" + library.name) != library: + fail("conformance_suite(%r) must be used in package //%s, not //%s." % + (name, library.package, native.package_name())) + for attr in ("testonly", "alwayslink"): + if attr in kwargs: + fail(("conformance_suite(%r): %s is set by the macro and can't " + + "be overridden.") % (name, attr)) + cc_library( + name = library.name, + testonly = 1, + srcs = srcs, + deps = deps, + alwayslink = 1, + **kwargs + ) + +def failure_lists(name): + """Declares the directory `<name>/` of this package as its failure lists. + + The directory holds the expected-failure lists of the package's + conformance_test()s, one file per (test, suite) pair named + `<test name>_<suite>.txt`, where `<test name>` is the `name` of a + conformance_test() and `<suite>` one of the conformance suites, e.g. + `cpp_binary.txt` for `conformance_test(name = "cpp")` and the `binary` + suite. A file that doesn't exist means "no expected failures". Anything + else in the directory (a file not named that way, a file for an unknown + suite, any file in a subdirectory) fails the load; a well-formed list that + no conformance_test() uses (e.g. `foo_binary.txt` without a `foo` test, or + a list for a suite the test skips) can't be detected, though, and is simply + never read. The directory must not be a package of its own (the lists + are found with `glob`, which doesn't look into subpackages). + + Nothing in the directory should be edited by hand: `blaze run + :<test name>_<suite>_test -- --fix` creates or rewrites the file for that + test to match the observed results. Only the directory itself has to + exist beforehand; create it (`mkdir`) together with the first list. + + Args: + name: The name of the directory, and of the filegroup created for it. + Pass `":" + name` as the `failure_lists` of the package's + conformance_test()s. + """ + if native.subpackages(include = [name, name + "/**"], allow_empty = True): + fail(("failure_lists(%r): %s/ must not contain a BUILD file (it is " + + "a directory of failure lists, not a package).") % (name, name)) + files = native.glob(["%s/**" % name], allow_empty = True) + for file in files: + # The test name may itself contain underscores (e.g. java_lite), so + # only the suffix can be validated. + stem = file[len(name) + 1:] + if "/" in stem or not stem.endswith(".txt"): + stem = "" + else: + stem = stem[:-len(".txt")] + if not [ + s + for s in _SUITES + if stem.endswith("_" + s) and len(stem) > len(s) + 1 + ]: + fail(("failure_lists(%r): %s doesn't correspond to a " + + "conformance suite: failure lists must be named " + + "<test name>_<suite>.txt, where <suite> is one of %s.") % + (name, file, ", ".join(_SUITES))) + native.filegroup( + name = name, + srcs = files, + ) + +def conformance_test( + name, + testee, + failure_lists, + suites = None, + maximum_edition = None, + enforcement_level = None, + in_process = False, + **kwargs): + """Runs the conformance suites against a testee. + + Creates one `cc_test` per suite, named `<name>_<suite>_test` (e.g. + `cpp_binary_test`, `cpp_performance_test`), and a `test_suite` named + `name` running all of them. + Each test links the suite's library with `:test_environment_main` and runs + it against `testee` with the failure list + `<failure_lists directory>/<name>_<suite>.txt` if that file exists; + otherwise the testee is expected to pass every test in the suite, and + `blaze run ... -- --fix` creates the file (see failure_lists()). + + By default `testee` is a binary: the test links `:forked_testee`, which + spawns it as a subprocess speaking the conformance protocol on its + stdin/stdout, and it is passed as `--testee_binary`. With `in_process`, + `testee` is a cc_library that defines MakeTesteeRunner() (see + testee_runner.h) and is linked into the test instead, so the suites run + the testee in their own process: no fork, one process to debug, and a + testee-side crash or log shows up in the test itself. The flip side is + that there is no per-test isolation: a testee crash or hang takes the + whole test binary down instead of failing one test (the forked form + respawns the testee), so only use it for testees that never crash on + conformance input. Both modes take the same failure lists and flags. + + Args: + name: The test's name; also the prefix of its failure lists. + testee: The testee. Without `in_process`, a binary run as a subprocess. + With `in_process`, a `cc_library` defining `MakeTesteeRunner()`, like + this package's own `:cpp_in_process_testee` (private to it). Such a + library must be `alwayslink = 1` and `testonly = 1`, and exactly one + of them may be linked into a test: none fails to link, two is an ODR + violation (a duplicate-symbol error under static linking, silently + ambiguous under dynamic linking). A macro can't check any of this, + nor the kind of a label, so passing a binary here with `in_process` + fails at analysis time (a cc_test can't depend on a binary), and a + library without it fails at analysis time on `$(rootpath)` or, if + that resolves to its single archive, when the test tries to run it. + failure_lists: The failure_lists() of this package. + suites: The suites to run; default all of them, the performance suite + included. Use this to skip a suite the testee doesn't support at all + (e.g. `text` for a runtime without text format). + maximum_edition: The newest edition to test (e.g. `"2023"`); default + proto2 and proto3 only. Passed as `--maximum_edition=EDITION_2023`. + enforcement_level: The lowest priority whose failures fail the run, as its + number (0: only P0, 1: P0 and P1). By default every priority is + enforced; failures of tests of a lower priority are tolerated unless the + test is in the failure list. Passed as `--enforcement_level`. + in_process: Whether `testee` is a cc_library to link into each test + instead of a binary to spawn. + **kwargs: Passed through to every cc_test (e.g. `size`, `timeout`, + `tags`); `tags` also apply to the test_suite. `args`, `deps` and + `data` are set by the macro and can't be given. + """ + if suites == None: + suites = _SUITES + for suite in suites: + _check_suite(suite, "conformance_test(%r)" % name) + + if enforcement_level != None: + if type(enforcement_level) != "int" or enforcement_level < 0: + fail(("conformance_test(%r): enforcement_level must be None or a " + + "non-negative int, not %r.") % (name, enforcement_level)) + + # The failure lists must be in this package, since only the calling + # package's files can be globbed to find out which lists exist. + lists_label = native.package_relative_label(failure_lists) + if native.package_relative_label(":" + lists_label.name) != lists_label: + fail(("conformance_test(%r): failure_lists (%s) must be a " + + "failure_lists() in the same package, //%s.") % + (name, failure_lists, native.package_name())) + + for attr in ("args", "deps", "data"): + if attr in kwargs: + fail(("conformance_test(%r): %s is set by the macro and can't " + + "be overridden.") % (name, attr)) + tags = kwargs.pop("tags", []) + tests = [] + for suite in suites: + test_name = "%s_%s_test" % (name, suite) + failure_list = "%s/%s_%s.txt" % (lists_label.name, name, suite) + + # The runfiles path of the failure list, which is also its path relative + # to the workspace root so that --fix can find the source file under + # $BUILD_WORKSPACE_DIRECTORY. + failure_list_path = failure_list + if lists_label.package: + failure_list_path = "%s/%s" % (lists_label.package, failure_list) + + if in_process: + # The testee is linked in and would reject --testee_binary (see + # testee_runner.h). + args = [] + data = [failure_lists] + deps = [_suite_library(suite), _TEST_ENVIRONMENT_MAIN, testee] + else: + args = ["--testee_binary=$(rootpath %s)" % testee] + data = [testee, failure_lists] + deps = [ + _suite_library(suite), + _TEST_ENVIRONMENT_MAIN, + _FORKED_TESTEE, + ] + + # (native.glob wouldn't see the file if the failure lists directory ever + # became a package of its own; failure_lists() forbids that.) + if native.glob([failure_list], allow_empty = True): + args.append("--failure_list=%s" % failure_list_path) + else: + # No expected failures; tell --fix where to create the list. + args.append("--fix_output_file=%s" % failure_list_path) + if maximum_edition: + args.append("--maximum_edition=EDITION_%s" % maximum_edition) + if enforcement_level != None: + args.append("--enforcement_level=%d" % enforcement_level) + + cc_test( + name = test_name, + args = args, + data = data, + tags = ["conformance"] + tags, + deps = deps, + **kwargs + ) + tests.append(":" + test_name) + + native.test_suite( + name = name, + tests = tests, + tags = tags, + )
diff --git a/conformance/conformance_cpp.cc b/conformance/conformance_cpp.cc index 4b292dd..edeb7f0 100644 --- a/conformance/conformance_cpp.cc +++ b/conformance/conformance_cpp.cc
@@ -5,14 +5,15 @@ // license that can be found in the LICENSE file or at // https://developers.google.com/open-source/licenses/bsd +// The C++ conformance testee. Speaks the length-prefixed stdin/stdout protocol +// described in fork_pipe_runner.cc and delegates each request to +// CppConformanceHarness. + #include <errno.h> -#include <stdarg.h> -#include <stdlib.h> #include <cstddef> #include <cstdint> #include <cstdio> -#include <memory> #include <string> #ifdef _WIN32 @@ -20,60 +21,26 @@ #include <io.h> #endif -#include "google/protobuf/any.pb.h" -#include "google/protobuf/api.pb.h" -#include "google/protobuf/duration.pb.h" -#include "google/protobuf/empty.pb.h" -#include "google/protobuf/field_mask.pb.h" -#include "google/protobuf/struct.pb.h" -#include "google/protobuf/timestamp.pb.h" -#include "google/protobuf/type.pb.h" -#include "google/protobuf/wrappers.pb.h" #include "absl/log/absl_check.h" #include "absl/log/absl_log.h" #include "absl/status/status.h" #include "absl/status/statusor.h" -#include "absl/strings/str_cat.h" #include "conformance/conformance.pb.h" -#include "conformance/test_protos/test_messages_edition2023.pb.h" -#include "conformance/test_protos/test_messages_edition2026.pb.h" -#include "conformance/test_protos/test_messages_edition_unstable.pb.h" -#include "editions/golden/test_messages_proto2_editions.pb.h" -#include "editions/golden/test_messages_proto3_editions.pb.h" +#include "conformance/conformance_cpp_harness.h" #include "google/protobuf/endian.h" -#include "google/protobuf/json/json.h" #include "google/protobuf/message.h" -#include "google/protobuf/test_messages_proto2.pb.h" -#include "google/protobuf/test_messages_proto3.pb.h" -#include "google/protobuf/text_format.h" -#include "google/protobuf/util/json_util.h" -#include "google/protobuf/util/type_resolver.h" -#include "google/protobuf/util/type_resolver_util.h" #include "google/protobuf/stubs/status_macros.h" -// Must be included last. -#include "google/protobuf/port_def.inc" - namespace google { namespace protobuf { namespace conformance { namespace { + using ::conformance::ConformanceRequest; using ::conformance::ConformanceResponse; -using ::google::protobuf::util::JsonParseOptions; -using ::google::protobuf::util::JsonStringToMessage; -using ::google::protobuf::util::MessageToJsonString; -using ::google::protobuf::util::NewTypeResolverForDescriptorPool; -using ::google::protobuf::util::TypeResolver; -using ::protobuf_test_messages::edition_unstable::TestAllTypesEditionUnstable; -using ::protobuf_test_messages::editions::TestAllTypesEdition2023; -using ::protobuf_test_messages::editions::TestAllTypesEdition2026; -using ::protobuf_test_messages::proto2::TestAllTypesProto2; -using ::protobuf_test_messages::proto3::TestAllTypesProto3; -using TestAllTypesProto2Editions = - ::protobuf_test_messages::editions::proto2::TestAllTypesProto2; -using TestAllTypesProto3Editions = - ::protobuf_test_messages::editions::proto3::TestAllTypesProto3; + +// Set to true to log every request/response pair. +constexpr bool kVerbose = false; absl::Status ReadAll(FILE* file, char* buf, size_t len) { while (len > 0) { @@ -99,143 +66,16 @@ return absl::OkStatus(); } -class Harness { - public: - Harness() { - google::protobuf::LinkMessageReflection<TestAllTypesProto2>(); - google::protobuf::LinkMessageReflection<TestAllTypesProto3>(); - google::protobuf::LinkMessageReflection<TestAllTypesEdition2023>(); - google::protobuf::LinkMessageReflection<TestAllTypesEdition2026>(); - google::protobuf::LinkMessageReflection<TestAllTypesEditionUnstable>(); - google::protobuf::LinkMessageReflection<TestAllTypesProto2Editions>(); - google::protobuf::LinkMessageReflection<TestAllTypesProto3Editions>(); - - // Force link one wkt from each wkt file. - google::protobuf::LinkMessageReflection<google::protobuf::Any>(); - google::protobuf::LinkMessageReflection<google::protobuf::Api>(); - google::protobuf::LinkMessageReflection<google::protobuf::Duration>(); - google::protobuf::LinkMessageReflection<google::protobuf::Empty>(); - google::protobuf::LinkMessageReflection<google::protobuf::FieldMask>(); - google::protobuf::LinkMessageReflection<google::protobuf::Struct>(); - google::protobuf::LinkMessageReflection<google::protobuf::Timestamp>(); - google::protobuf::LinkMessageReflection<google::protobuf::Type>(); - google::protobuf::LinkMessageReflection<google::protobuf::DoubleValue>(); - - resolver_.reset(NewTypeResolverForDescriptorPool( - "type.googleapis.com", DescriptorPool::generated_pool())); - } - - absl::StatusOr<ConformanceResponse> RunTest( - const ConformanceRequest& request); - - // Returns Ok(true) if we're done processing requests. - absl::StatusOr<bool> ServeConformanceRequest(); - - private: - bool verbose_ = false; - std::unique_ptr<TypeResolver> resolver_; -}; - -absl::StatusOr<ConformanceResponse> Harness::RunTest( - const ConformanceRequest& request) { - const Descriptor* descriptor = - DescriptorPool::generated_pool()->FindMessageTypeByName( - request.message_type()); - if (descriptor == nullptr) { - return absl::NotFoundError( - absl::StrCat("No such message type: ", request.message_type())); - } - std::string type_url = - absl::StrCat("type.googleapis.com/", request.message_type()); - - std::unique_ptr<Message> test_message( - MessageFactory::generated_factory()->GetPrototype(descriptor)->New()); - ConformanceResponse response; - - switch (request.payload_case()) { - case ConformanceRequest::kProtobufPayload: { - if (!test_message->ParseFromString(request.protobuf_payload())) { - response.set_parse_error("parse error (no more details available)"); - return response; - } - break; - } - - case ConformanceRequest::kJsonPayload: { - JsonParseOptions options; - options.ignore_unknown_fields = - (request.test_category() == - ::conformance::JSON_IGNORE_UNKNOWN_PARSING_TEST); - absl::Status status = JsonStringToMessage(request.json_payload(), - test_message.get(), options); - if (!status.ok()) { - response.set_parse_error( - absl::StrCat("parse error: ", status.message())); - return response; - } - break; - } - - case ConformanceRequest::kTextPayload: { - if (!TextFormat::ParseFromString(request.text_payload(), - test_message.get())) { - response.set_parse_error("parse error (no more details available)"); - return response; - } - break; - } - - case ConformanceRequest::PAYLOAD_NOT_SET: - return absl::InvalidArgumentError("request didn't have payload"); - - default: - return absl::InvalidArgumentError( - absl::StrCat("unknown payload type", request.payload_case())); - } - - switch (request.requested_output_format()) { - case ::conformance::UNSPECIFIED: - return absl::InvalidArgumentError("unspecified output format"); - - case ::conformance::PROTOBUF: { - ABSL_CHECK( - test_message->SerializeToString(response.mutable_protobuf_payload())); - break; - } - - case ::conformance::JSON: { - absl::Status status = - MessageToJsonString(*test_message, response.mutable_json_payload()); - if (!status.ok()) { - response.set_serialize_error(absl::StrCat( - "failed to serialize JSON output: ", status.message())); - } - break; - } - - case ::conformance::TEXT_FORMAT: { - TextFormat::Printer printer; - printer.SetHideUnknownFields(!request.print_unknown_fields()); - ABSL_CHECK(printer.PrintToString(*test_message, - response.mutable_text_payload())); - break; - } - - default: - return absl::InvalidArgumentError(absl::StrCat( - "unknown output format", request.requested_output_format())); - } - - return response; -} - -absl::StatusOr<bool> Harness::ServeConformanceRequest() { +// Reads one request from stdin, runs it through `harness`, and writes the +// response to stdout. Returns Ok(true) if we're done processing requests. +absl::StatusOr<bool> ServeConformanceRequest( + const CppConformanceHarness& harness) { uint32_t in_len; if (!ReadAll(stdin, reinterpret_cast<char*>(&in_len), sizeof(in_len)).ok()) { // EOF means we're done. return true; } - in_len = ::google::protobuf::internal::little_endian::ToHost(in_len); + in_len = google::protobuf::internal::little_endian::ToHost(in_len); std::string serialized_input; serialized_input.resize(in_len); @@ -244,14 +84,14 @@ ConformanceRequest request; ABSL_CHECK(request.ParseFromString(serialized_input)); - absl::StatusOr<ConformanceResponse> response = RunTest(request); + absl::StatusOr<ConformanceResponse> response = harness.RunTest(request); RETURN_IF_ERROR(response.status()); std::string serialized_output; // TODO: Remove this suppression. (void)response->SerializeToString(&serialized_output); - uint32_t out_len = ::google::protobuf::internal::little_endian::FromHost( + uint32_t out_len = google::protobuf::internal::little_endian::FromHost( static_cast<uint32_t>(serialized_output.size())); RETURN_IF_ERROR(WriteAll(stdout, &out_len, sizeof(out_len))); @@ -261,13 +101,14 @@ return absl::ErrnoToStatus(errno, "error flushing to test runner"); } - if (verbose_) { + if (kVerbose) { ABSL_LOG(INFO) << "conformance-cpp: request=" << google::protobuf::ShortFormat(request) << ", response=" << google::protobuf::ShortFormat(*response); } return false; } + } // namespace } // namespace conformance } // namespace protobuf @@ -278,10 +119,11 @@ _setmode(_fileno(stdin), _O_BINARY); _setmode(_fileno(stdout), _O_BINARY); #endif - google::protobuf::conformance::Harness harness; + google::protobuf::conformance::CppConformanceHarness harness; int total_runs = 0; while (true) { - auto is_done = harness.ServeConformanceRequest(); + absl::StatusOr<bool> is_done = + google::protobuf::conformance::ServeConformanceRequest(harness); if (!is_done.ok()) { ABSL_LOG(FATAL) << is_done.status(); } @@ -293,5 +135,3 @@ ABSL_LOG(INFO) << "conformance-cpp: received EOF from test runner after " << total_runs << " tests"; } - -#include "google/protobuf/port_undef.inc"
diff --git a/conformance/conformance_cpp_harness.cc b/conformance/conformance_cpp_harness.cc new file mode 100644 index 0000000..568d368 --- /dev/null +++ b/conformance/conformance_cpp_harness.cc
@@ -0,0 +1,174 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2026 Google LLC. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +#include "conformance/conformance_cpp_harness.h" + +#include <memory> + +#include "google/protobuf/any.pb.h" +#include "google/protobuf/api.pb.h" +#include "google/protobuf/duration.pb.h" +#include "google/protobuf/empty.pb.h" +#include "google/protobuf/field_mask.pb.h" +#include "google/protobuf/struct.pb.h" +#include "google/protobuf/timestamp.pb.h" +#include "google/protobuf/type.pb.h" +#include "google/protobuf/wrappers.pb.h" +#include "absl/log/absl_check.h" +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "absl/strings/str_cat.h" +#include "conformance/conformance.pb.h" +#include "conformance/test_protos/test_messages_edition2023.pb.h" +#include "conformance/test_protos/test_messages_edition2026.pb.h" +#include "conformance/test_protos/test_messages_edition_unstable.pb.h" +#include "google/protobuf/descriptor.h" +#include "editions/golden/test_messages_proto2_editions.pb.h" +#include "editions/golden/test_messages_proto3_editions.pb.h" +#include "google/protobuf/json/json.h" +#include "google/protobuf/message.h" +#include "google/protobuf/test_messages_proto2.pb.h" +#include "google/protobuf/test_messages_proto3.pb.h" +#include "google/protobuf/text_format.h" + +namespace google { +namespace protobuf { +namespace conformance { +namespace { + +using ::conformance::ConformanceRequest; +using ::conformance::ConformanceResponse; +using ::google::protobuf::json::JsonStringToMessage; +using ::google::protobuf::json::MessageToJsonString; +using ::google::protobuf::json::ParseOptions; +using ::protobuf_test_messages::edition_unstable::TestAllTypesEditionUnstable; +using ::protobuf_test_messages::editions::TestAllTypesEdition2023; +using ::protobuf_test_messages::editions::TestAllTypesEdition2026; +using ::protobuf_test_messages::proto2::TestAllTypesProto2; +using ::protobuf_test_messages::proto3::TestAllTypesProto3; +using TestAllTypesProto2Editions = + ::protobuf_test_messages::editions::proto2::TestAllTypesProto2; +using TestAllTypesProto3Editions = + ::protobuf_test_messages::editions::proto3::TestAllTypesProto3; + +} // namespace + +CppConformanceHarness::CppConformanceHarness() { + google::protobuf::LinkMessageReflection<TestAllTypesProto2>(); + google::protobuf::LinkMessageReflection<TestAllTypesProto3>(); + google::protobuf::LinkMessageReflection<TestAllTypesEdition2023>(); + google::protobuf::LinkMessageReflection<TestAllTypesEdition2026>(); + google::protobuf::LinkMessageReflection<TestAllTypesEditionUnstable>(); + google::protobuf::LinkMessageReflection<TestAllTypesProto2Editions>(); + google::protobuf::LinkMessageReflection<TestAllTypesProto3Editions>(); + + // Force link one wkt from each wkt file. + google::protobuf::LinkMessageReflection<google::protobuf::Any>(); + google::protobuf::LinkMessageReflection<google::protobuf::Api>(); + google::protobuf::LinkMessageReflection<google::protobuf::Duration>(); + google::protobuf::LinkMessageReflection<google::protobuf::Empty>(); + google::protobuf::LinkMessageReflection<google::protobuf::FieldMask>(); + google::protobuf::LinkMessageReflection<google::protobuf::Struct>(); + google::protobuf::LinkMessageReflection<google::protobuf::Timestamp>(); + google::protobuf::LinkMessageReflection<google::protobuf::Type>(); + google::protobuf::LinkMessageReflection<google::protobuf::DoubleValue>(); +} + +absl::StatusOr<ConformanceResponse> CppConformanceHarness::RunTest( + const ConformanceRequest& request) const { + const Descriptor* descriptor = + DescriptorPool::generated_pool()->FindMessageTypeByName( + request.message_type()); + if (descriptor == nullptr) { + return absl::NotFoundError( + absl::StrCat("No such message type: ", request.message_type())); + } + + std::unique_ptr<Message> test_message( + MessageFactory::generated_factory()->GetPrototype(descriptor)->New()); + ConformanceResponse response; + + switch (request.payload_case()) { + case ConformanceRequest::kProtobufPayload: { + if (!test_message->ParseFromString(request.protobuf_payload())) { + response.set_parse_error("parse error (no more details available)"); + return response; + } + break; + } + + case ConformanceRequest::kJsonPayload: { + ParseOptions options; + options.ignore_unknown_fields = + (request.test_category() == + ::conformance::JSON_IGNORE_UNKNOWN_PARSING_TEST); + absl::Status status = JsonStringToMessage(request.json_payload(), + test_message.get(), options); + if (!status.ok()) { + response.set_parse_error( + absl::StrCat("parse error: ", status.message())); + return response; + } + break; + } + + case ConformanceRequest::kTextPayload: { + if (!TextFormat::ParseFromString(request.text_payload(), + test_message.get())) { + response.set_parse_error("parse error (no more details available)"); + return response; + } + break; + } + + case ConformanceRequest::PAYLOAD_NOT_SET: + return absl::InvalidArgumentError("request didn't have payload"); + + default: + return absl::InvalidArgumentError( + absl::StrCat("unknown payload type: ", request.payload_case())); + } + + switch (request.requested_output_format()) { + case ::conformance::UNSPECIFIED: + return absl::InvalidArgumentError("unspecified output format"); + + case ::conformance::PROTOBUF: { + ABSL_CHECK( + test_message->SerializeToString(response.mutable_protobuf_payload())); + break; + } + + case ::conformance::JSON: { + absl::Status status = + MessageToJsonString(*test_message, response.mutable_json_payload()); + if (!status.ok()) { + response.set_serialize_error(absl::StrCat( + "failed to serialize JSON output: ", status.message())); + } + break; + } + + case ::conformance::TEXT_FORMAT: { + TextFormat::Printer printer; + printer.SetHideUnknownFields(!request.print_unknown_fields()); + ABSL_CHECK(printer.PrintToString(*test_message, + response.mutable_text_payload())); + break; + } + + default: + return absl::InvalidArgumentError(absl::StrCat( + "unknown output format: ", request.requested_output_format())); + } + + return response; +} + +} // namespace conformance +} // namespace protobuf +} // namespace google
diff --git a/conformance/conformance_cpp_harness.h b/conformance/conformance_cpp_harness.h new file mode 100644 index 0000000..e9633ff --- /dev/null +++ b/conformance/conformance_cpp_harness.h
@@ -0,0 +1,48 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2026 Google LLC. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +#ifndef GOOGLE_PROTOBUF_CONFORMANCE_CONFORMANCE_CPP_HARNESS_H__ +#define GOOGLE_PROTOBUF_CONFORMANCE_CONFORMANCE_CPP_HARNESS_H__ + +#include "absl/status/statusor.h" +#include "conformance/conformance.pb.h" + +namespace google { +namespace protobuf { +namespace conformance { + +// Implements the C++ conformance testee. Given a ConformanceRequest, it +// parses the payload into the requested test message and serializes it in +// the requested output format. +// +// Shared by the conformance_cpp binary (stdin/stdout protocol) and, via +// cpp_in_process_test_runner.h, by in-process conformance tests. +// +// Instances are stateless and cheap to construct. RunTest is const, so a +// single instance may be shared across threads. +class CppConformanceHarness { + public: + // Force-links the reflection data of every test message and well-known type + // into the binary so that they can be looked up by name in the generated + // descriptor pool. + CppConformanceHarness(); + + // Runs a single conformance test. + // + // Returns a response with `parse_error` or `serialize_error` set when the + // payload cannot be handled. Returns a non-OK status when the request + // itself is malformed: unknown message type, missing payload, or an + // unspecified output format. + absl::StatusOr<::conformance::ConformanceResponse> RunTest( + const ::conformance::ConformanceRequest& request) const; +}; + +} // namespace conformance +} // namespace protobuf +} // namespace google + +#endif // GOOGLE_PROTOBUF_CONFORMANCE_CONFORMANCE_CPP_HARNESS_H__
diff --git a/conformance/conformance_cpp_harness_test.cc b/conformance/conformance_cpp_harness_test.cc new file mode 100644 index 0000000..83b8b0f --- /dev/null +++ b/conformance/conformance_cpp_harness_test.cc
@@ -0,0 +1,285 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2026 Google LLC. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +#include "conformance/conformance_cpp_harness.h" + +#include <string> + +#include <gmock/gmock.h> +#include <gtest/gtest.h> +#include "absl/status/status.h" +#include "absl/status/status_matchers.h" +#include "absl/status/statusor.h" +#include "absl/strings/str_replace.h" +#include "absl/strings/string_view.h" +#include "conformance/binary_wireformat.h" +#include "conformance/conformance.pb.h" +#include "google/protobuf/test_messages_proto3.pb.h" +#include "google/protobuf/test_textproto.h" + +namespace google { +namespace protobuf { +namespace conformance { +namespace { + +using ::absl_testing::IsOk; +using ::absl_testing::IsOkAndHolds; +using ::absl_testing::StatusIs; +using ::conformance::ConformanceRequest; +using ::conformance::ConformanceResponse; +using ::protobuf_test_messages::proto3::TestAllTypesProto3; +using ::testing::HasSubstr; +using ::testing::Property; +using ::testing::StartsWith; +using ::testing::TestParamInfo; +using ::testing::TestWithParam; +using ::testing::Values; + +constexpr absl::string_view kProto3MessageType = + "protobuf_test_messages.proto3.TestAllTypesProto3"; + +// Returns a request for TestAllTypesProto3 with the given output format and no +// payload. Tests add whichever payload they exercise, or leave it unset to +// exercise the missing-payload path. +ConformanceRequest Proto3Request(::conformance::WireFormat output_format) { + ConformanceRequest request; + request.set_message_type(kProto3MessageType); + request.set_requested_output_format(output_format); + return request; +} + +// Serializes the TestAllTypesProto3 described by `textproto` to the wire +// format. +std::string SerializeProto3(absl::string_view textproto) { + TestAllTypesProto3 message = ParseTextOrDie(textproto); + return message.SerializeAsString(); +} + +// Serializes `optional_int32: 42` with an additional unknown varint field +// (number 9999, value 1) so that unknown-field handling can be observed. +std::string Proto3PayloadWithUnknownField() { + TestAllTypesProto3 message; + message.set_optional_int32(42); + message.GetReflection()->MutableUnknownFields(&message)->AddVarint(9999, 1); + return message.SerializeAsString(); +} + +TEST(CppConformanceHarnessTest, ProtobufRoundTrip) { + CppConformanceHarness harness; + ConformanceRequest request = Proto3Request(::conformance::PROTOBUF); + request.set_protobuf_payload( + SerializeProto3(R"pb(optional_int32: 42 optional_string: "hello")pb")); + + absl::StatusOr<ConformanceResponse> response = harness.RunTest(request); + + ASSERT_THAT(response, IsOk()); + ASSERT_EQ(response->result_case(), ConformanceResponse::kProtobufPayload); + TestAllTypesProto3 round_tripped; + ASSERT_TRUE(round_tripped.ParseFromString(response->protobuf_payload())); + EXPECT_THAT(round_tripped, EqualsProto(R"pb(optional_int32: 42 + optional_string: "hello")pb")); +} + +TEST(CppConformanceHarnessTest, ProtobufParseErrorOnMalformedBytes) { + CppConformanceHarness harness; + ConformanceRequest request = Proto3Request(::conformance::PROTOBUF); + // Field 1, length-delimited, claims 5 bytes but only 3 follow. + request.set_protobuf_payload( + Wire(Tag(1, WireType::kLengthPrefixed), Varint(5), "abc").str()); + + EXPECT_THAT(harness.RunTest(request), + IsOkAndHolds(EqualsProto( + R"pb(parse_error: "parse error (no more details available)" + )pb"))); +} + +TEST(CppConformanceHarnessTest, JsonOutput) { + CppConformanceHarness harness; + ConformanceRequest request = Proto3Request(::conformance::JSON); + request.set_protobuf_payload(SerializeProto3(R"pb(optional_int32: 42)pb")); + + // Kept out of the macro arguments: MSVC's traditional preprocessor mis-scans + // raw string literals containing \" inside macro arguments. + constexpr absl::string_view kExpected = + R"pb(json_payload: "{\"optionalInt32\":42}")pb"; + EXPECT_THAT(harness.RunTest(request), IsOkAndHolds(EqualsProto(kExpected))); +} + +TEST(CppConformanceHarnessTest, JsonSerializeErrorOnUnresolvableAny) { + CppConformanceHarness harness; + ConformanceRequest request = Proto3Request(::conformance::JSON); + // The payload parses from the wire format, but JSON printing has to expand + // the Any and cannot resolve its type URL. + request.set_protobuf_payload(SerializeProto3( + R"pb(optional_any { type_url: "type.googleapis.com/no.such.Message" } + )pb")); + + absl::StatusOr<ConformanceResponse> response = harness.RunTest(request); + + ASSERT_THAT(response, IsOk()); + ASSERT_EQ(response->result_case(), ConformanceResponse::kSerializeError); + EXPECT_THAT(response->serialize_error(), + StartsWith("failed to serialize JSON output: ")); +} + +TEST(CppConformanceHarnessTest, JsonUnknownFieldIsParseErrorByDefault) { + CppConformanceHarness harness; + ConformanceRequest request = Proto3Request(::conformance::PROTOBUF); + request.set_json_payload(R"json({"noSuchField": 1})json"); + request.set_test_category(::conformance::JSON_TEST); + + EXPECT_THAT(harness.RunTest(request), + IsOkAndHolds(Property(&ConformanceResponse::parse_error, + StartsWith("parse error: ")))); +} + +TEST(CppConformanceHarnessTest, JsonUnknownFieldIsIgnoredInIgnoreUnknownTest) { + CppConformanceHarness harness; + ConformanceRequest request = Proto3Request(::conformance::PROTOBUF); + request.set_json_payload( + R"json({"noSuchField": 1, "optionalInt32": 42})json"); + request.set_test_category(::conformance::JSON_IGNORE_UNKNOWN_PARSING_TEST); + + absl::StatusOr<ConformanceResponse> response = harness.RunTest(request); + + ASSERT_THAT(response, IsOk()); + ASSERT_EQ(response->result_case(), ConformanceResponse::kProtobufPayload); + TestAllTypesProto3 parsed; + ASSERT_TRUE(parsed.ParseFromString(response->protobuf_payload())); + EXPECT_THAT(parsed, EqualsProto(R"pb(optional_int32: 42)pb")); +} + +TEST(CppConformanceHarnessTest, TextPayloadRoundTrip) { + CppConformanceHarness harness; + ConformanceRequest request = Proto3Request(::conformance::TEXT_FORMAT); + request.set_text_payload( + R"pb(optional_int32: 42 optional_string: 'hello')pb"); + + constexpr absl::string_view kExpected = + R"pb(text_payload: "optional_int32: 42\n" + "optional_string: \"hello\"\n")pb"; + EXPECT_THAT(harness.RunTest(request), IsOkAndHolds(EqualsProto(kExpected))); +} + +TEST(CppConformanceHarnessTest, TextParseErrorOnMalformedInput) { + CppConformanceHarness harness; + ConformanceRequest request = Proto3Request(::conformance::TEXT_FORMAT); + request.set_text_payload(R"pb(optional_int32: not_a_number)pb"); + + EXPECT_THAT(harness.RunTest(request), + IsOkAndHolds(EqualsProto( + R"pb(parse_error: "parse error (no more details available)" + )pb"))); +} + +TEST(CppConformanceHarnessTest, TextFormatOutputHidesUnknownFieldsByDefault) { + CppConformanceHarness harness; + ConformanceRequest request = Proto3Request(::conformance::TEXT_FORMAT); + request.set_protobuf_payload(Proto3PayloadWithUnknownField()); + + EXPECT_THAT( + harness.RunTest(request), + IsOkAndHolds(EqualsProto(R"pb(text_payload: "optional_int32: 42\n")pb"))); +} + +TEST(CppConformanceHarnessTest, TextFormatOutputPrintsUnknownFieldsOnRequest) { + CppConformanceHarness harness; + ConformanceRequest request = Proto3Request(::conformance::TEXT_FORMAT); + request.set_protobuf_payload(Proto3PayloadWithUnknownField()); + request.set_print_unknown_fields(true); + + EXPECT_THAT(harness.RunTest(request), + IsOkAndHolds(EqualsProto( + R"pb(text_payload: "optional_int32: 42\n9999: 1\n")pb"))); +} + +TEST(CppConformanceHarnessTest, UnknownMessageTypeIsNotFound) { + CppConformanceHarness harness; + ConformanceRequest request = Proto3Request(::conformance::PROTOBUF); + request.set_message_type("no.such.Message"); + request.set_protobuf_payload(""); + + EXPECT_THAT(harness.RunTest(request), StatusIs(absl::StatusCode::kNotFound, + HasSubstr("no.such.Message"))); +} + +TEST(CppConformanceHarnessTest, MissingPayloadIsInvalidArgument) { + CppConformanceHarness harness; + + EXPECT_THAT( + harness.RunTest(Proto3Request(::conformance::PROTOBUF)), + StatusIs(absl::StatusCode::kInvalidArgument, HasSubstr("payload"))); +} + +TEST(CppConformanceHarnessTest, JspbPayloadIsInvalidArgument) { + CppConformanceHarness harness; + ConformanceRequest request = Proto3Request(::conformance::PROTOBUF); + request.set_jspb_payload("[]"); + + EXPECT_THAT(harness.RunTest(request), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("unknown payload type"))); +} + +TEST(CppConformanceHarnessTest, UnspecifiedOutputFormatIsInvalidArgument) { + CppConformanceHarness harness; + ConformanceRequest request = Proto3Request(::conformance::UNSPECIFIED); + request.set_protobuf_payload(""); + + EXPECT_THAT( + harness.RunTest(request), + StatusIs(absl::StatusCode::kInvalidArgument, HasSubstr("output format"))); +} + +TEST(CppConformanceHarnessTest, UnknownOutputFormatIsInvalidArgument) { + CppConformanceHarness harness; + ConformanceRequest request = + Proto3Request(static_cast<::conformance::WireFormat>(99)); + request.set_protobuf_payload(""); + + EXPECT_THAT( + harness.RunTest(request), + StatusIs(absl::StatusCode::kInvalidArgument, HasSubstr("output format"))); +} + +// Message types the conformance suites may name in a request, other than +// TestAllTypesProto3 (which this file references directly). The harness +// constructor force-links the reflection data for each of them. This test +// deliberately does not reference the generated C++ types, so it is intended +// to fail if the harness stops pulling one of them in; it is only a weak +// guarantee, though, since the linker may keep the descriptors alive for other +// reasons (e.g. alwayslink or dynamic linking). +class LinkedMessageTypeTest : public TestWithParam<absl::string_view> {}; + +TEST_P(LinkedMessageTypeTest, EmptyPayloadRoundTrips) { + CppConformanceHarness harness; + ConformanceRequest request; + request.set_message_type(GetParam()); + request.set_requested_output_format(::conformance::PROTOBUF); + request.set_protobuf_payload(""); + + EXPECT_THAT(harness.RunTest(request), + IsOkAndHolds(EqualsProto(R"pb(protobuf_payload: "")pb"))); +} + +INSTANTIATE_TEST_SUITE_P( + CppConformanceHarnessTest, LinkedMessageTypeTest, + Values("protobuf_test_messages.proto2.TestAllTypesProto2", + "protobuf_test_messages.editions.TestAllTypesEdition2023", + "protobuf_test_messages.edition_unstable." + "TestAllTypesEditionUnstable", + "protobuf_test_messages.editions.proto2.TestAllTypesProto2", + "protobuf_test_messages.editions.proto3.TestAllTypesProto3", + "google.protobuf.Any", "google.protobuf.Timestamp"), + [](const TestParamInfo<absl::string_view>& info) { + return absl::StrReplaceAll(info.param, {{".", "_"}}); + }); + +} // namespace +} // namespace conformance +} // namespace protobuf +} // namespace google
diff --git a/conformance/conformance_test.cc b/conformance/conformance_test.cc deleted file mode 100644 index 467bcfe..0000000 --- a/conformance/conformance_test.cc +++ /dev/null
@@ -1,798 +0,0 @@ -// Protocol Buffers - Google's data interchange format -// Copyright 2008 Google Inc. All rights reserved. -// -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file or at -// https://developers.google.com/open-source/licenses/bsd - -#include "conformance/conformance_test.h" - -#include <stdarg.h> - -#include <algorithm> -#include <cstddef> -#include <cstdint> -#include <cstdio> -#include <fstream> -#include <memory> -#include <string> -#include <utility> - -#include "absl/container/btree_map.h" -#include "absl/container/flat_hash_set.h" -#include "absl/log/absl_check.h" -#include "absl/log/absl_log.h" -#include "absl/status/status.h" -#include "absl/strings/str_cat.h" -#include "absl/strings/str_format.h" -#include "absl/strings/string_view.h" -#include "conformance/conformance.pb.h" -#include "conformance/failure_list_trie_node.h" -#include "google/protobuf/descriptor_legacy.h" -#include "google/protobuf/endian.h" -#include "google/protobuf/message.h" -#include "google/protobuf/text_format.h" -#include "google/protobuf/util/field_comparator.h" -#include "google/protobuf/util/message_differencer.h" - -using conformance::ConformanceRequest; -using conformance::ConformanceResponse; -using conformance::TestStatus; -using conformance::WireFormat; -using google::protobuf::util::DefaultFieldComparator; -using google::protobuf::util::MessageDifferencer; -using std::string; - -namespace { - -static void ReplaceAll(std::string& input, std::string replace_word, - std::string replace_by) { - size_t pos = input.find(replace_word); - while (pos != std::string::npos) { - input.replace(pos, replace_word.length(), replace_by); - pos = input.find(replace_word, pos + replace_by.length()); - } -} - -static std::string ToOctString(const std::string& binary_string) { - std::string oct_string; - for (size_t i = 0; i < binary_string.size(); i++) { - uint8_t c = binary_string.at(i); - uint8_t high = c / 64; - uint8_t mid = (c % 64) / 8; - uint8_t low = c % 8; - oct_string.push_back('\\'); - oct_string.push_back('0' + high); - oct_string.push_back('0' + mid); - oct_string.push_back('0' + low); - } - return oct_string; -} - -// Returns full filename path of written .txt file if successful -static std::string ProduceOctalSerialized(const std::string& request, - uint32_t len) { - char* len_split_bytes = static_cast<char*>(static_cast<void*>(&len)); - - std::string out; - - std::string hex_repr; - for (int i = 0; i < 4; i++) { - auto conversion = (unsigned int)static_cast<uint8_t>(len_split_bytes[i]); - std::string hex = absl::StrFormat("\\x%x", conversion); - absl::StrAppend(&hex_repr, hex); - } - - absl::StrAppend(&out, hex_repr); - - absl::StrAppend(&out, ToOctString(request)); - - return out; -} - -static std::string WriteToFile(const std::string& octal_serialized, - const std::string& output_dir, - const std::string& test_name) { - std::string test_name_txt = test_name; - ReplaceAll(test_name_txt, ".", "_"); - absl::StrAppend(&test_name_txt, ".txt"); - std::string full_filename; - if (!output_dir.empty()) { - full_filename = output_dir; - if (*output_dir.rbegin() != '/') { - full_filename.push_back('/'); - } - absl::StrAppend(&full_filename, test_name_txt); - } - std::ofstream os{std::string(full_filename)}; - if (os) { - os << octal_serialized; - return full_filename; - } else { - ABSL_LOG(INFO) << "Failed to open file for debugging: " << full_filename - << "\n"; - return ""; - } -} - -// Removes all newlines. -static void Normalize(std::string& input) { - input.erase(std::remove(input.begin(), input.end(), '\n'), input.end()); -} - -// Sets up a failure message properly for our failure lists. -static TestStatus FormatFailureMessage(const TestStatus& input) { - // Make copy just this once, as we need to modify it for our failure lists. - std::string formatted_failure_message = input.failure_message(); - // Remove newlines - Normalize(formatted_failure_message); - // Truncate failure message if needed - if (formatted_failure_message.length() > 128) { - formatted_failure_message = formatted_failure_message.substr(0, 128); - } - TestStatus properly_formatted; - properly_formatted.set_name(input.name()); - properly_formatted.set_failure_message(formatted_failure_message); - return properly_formatted; -} - -bool CheckSetEmpty(const absl::btree_map<std::string, TestStatus>& set_to_check, - absl::string_view write_to_file, absl::string_view msg, - absl::string_view output_dir, std::string* output) { - if (set_to_check.empty()) return true; - - absl::StrAppendFormat(output, "\n"); - absl::StrAppendFormat(output, "%s\n\n", msg); - for (const auto& pair : set_to_check) { - absl::StrAppendFormat(output, " %s # %s\n", pair.first, - pair.second.failure_message()); - } - absl::StrAppendFormat(output, "\n"); - - if (!write_to_file.empty()) { - std::string full_filename; - absl::string_view filename = write_to_file; - if (!output_dir.empty()) { - full_filename = std::string(output_dir); - absl::StrAppend(&full_filename, write_to_file); - filename = full_filename; - } - std::ofstream os{std::string(filename)}; - if (os) { - for (const auto& pair : set_to_check) { - // Additions will not have a 'matched_name' while removals will. - string potential_add_or_removal = pair.second.matched_name().empty() - ? pair.first - : pair.second.matched_name(); - os << potential_add_or_removal << " # " << pair.second.failure_message() - << "\n"; - } - } else { - absl::StrAppendFormat(output, - "Failed to open file: %s\n", - filename); - } - } - - return false; -} - -} // namespace - -namespace google { -namespace protobuf { -namespace conformance { - -constexpr int kMaximumWildcardExpansions = 20; - -ConformanceTestSuite::ConformanceRequestSetting::ConformanceRequestSetting( - ConformanceLevel level, ::conformance::WireFormat input_format, - ::conformance::WireFormat output_format, - ::conformance::TestCategory test_category, const Message& prototype_message, - const std::string& test_name, const std::string& input) - : level_(level), - input_format_(input_format), - output_format_(output_format), - prototype_message_(prototype_message), - prototype_message_for_compare_(prototype_message.New()), - test_name_(test_name) { - switch (input_format) { - case ::conformance::PROTOBUF: { - request_.set_protobuf_payload(input); - break; - } - - case ::conformance::JSON: { - request_.set_json_payload(input); - break; - } - - case ::conformance::JSPB: { - request_.set_jspb_payload(input); - break; - } - - case ::conformance::TEXT_FORMAT: { - request_.set_text_payload(input); - break; - } - - default: - ABSL_LOG(FATAL) << "Unspecified input format"; - } - - request_.set_test_category(test_category); - - request_.set_message_type(prototype_message.GetDescriptor()->full_name()); - request_.set_requested_output_format(output_format); -} - -std::unique_ptr<Message> -ConformanceTestSuite::ConformanceRequestSetting::NewTestMessage() const { - return std::unique_ptr<Message>(prototype_message_for_compare_->New()); -} - -std::string -ConformanceTestSuite::ConformanceRequestSetting::GetSyntaxIdentifier() const { - switch (FileDescriptorLegacy(prototype_message_.GetDescriptor()->file()) - .edition()) { - case Edition::EDITION_PROTO3: - return "Proto3"; - case Edition::EDITION_PROTO2: - return "Proto2"; - case Edition::EDITION_UNSTABLE: - return "EditionUnstable"; - case Edition::EDITION_2026: - return "Edition2026"; - default: { - std::string id = "Editions"; - if (prototype_message_.GetDescriptor()->name() == "TestAllTypesProto2") { - absl::StrAppend(&id, "_Proto2"); - } else if (prototype_message_.GetDescriptor()->name() == - "TestAllTypesProto3") { - absl::StrAppend(&id, "_Proto3"); - } - return id; - } - } -} - -string ConformanceTestSuite::ConformanceRequestSetting::GetTestName() const { - return absl::StrCat(ConformanceLevelToString(level_), ".", - GetSyntaxIdentifier(), ".", - InputFormatString(input_format_), ".", test_name_, ".", - OutputFormatString(output_format_)); -} - -std::string -ConformanceTestSuite::ConformanceRequestSetting::ConformanceLevelToString( - ConformanceLevel level) const { - switch (level) { - case REQUIRED: - return "Required"; - case RECOMMENDED: - return "Recommended"; - } - ABSL_LOG(FATAL) << "Unknown value: " << level; - return ""; -} - -std::string ConformanceTestSuite::ConformanceRequestSetting::InputFormatString( - ::conformance::WireFormat format) const { - switch (format) { - case ::conformance::PROTOBUF: - return "ProtobufInput"; - case ::conformance::JSON: - return "JsonInput"; - case ::conformance::TEXT_FORMAT: - return "TextFormatInput"; - default: - ABSL_LOG(FATAL) << "Unspecified output format"; - } - return ""; -} - -std::string ConformanceTestSuite::ConformanceRequestSetting::OutputFormatString( - ::conformance::WireFormat format) const { - switch (format) { - case ::conformance::PROTOBUF: - return "ProtobufOutput"; - case ::conformance::JSON: - return "JsonOutput"; - case ::conformance::TEXT_FORMAT: - return "TextFormatOutput"; - default: - ABSL_LOG(FATAL) << "Unspecified output format"; - } - return ""; -} - -void ConformanceTestSuite::TruncateDebugPayload(string* payload) { - if (payload != nullptr && payload->size() > 200) { - payload->resize(200); - payload->append("...(truncated)"); - } -} - -ConformanceRequest ConformanceTestSuite::TruncateRequest( - const ConformanceRequest& request) { - ConformanceRequest debug_request(request); - switch (debug_request.payload_case()) { - case ConformanceRequest::kProtobufPayload: - TruncateDebugPayload(debug_request.mutable_protobuf_payload()); - break; - case ConformanceRequest::kJsonPayload: - TruncateDebugPayload(debug_request.mutable_json_payload()); - break; - case ConformanceRequest::kTextPayload: - TruncateDebugPayload(debug_request.mutable_text_payload()); - break; - case ConformanceRequest::kJspbPayload: - TruncateDebugPayload(debug_request.mutable_jspb_payload()); - break; - default: - // Do nothing. - break; - } - return debug_request; -} - -ConformanceResponse ConformanceTestSuite::TruncateResponse( - const ConformanceResponse& response) { - ConformanceResponse debug_response(response); - switch (debug_response.result_case()) { - case ConformanceResponse::kProtobufPayload: - TruncateDebugPayload(debug_response.mutable_protobuf_payload()); - break; - case ConformanceResponse::kJsonPayload: - TruncateDebugPayload(debug_response.mutable_json_payload()); - break; - case ConformanceResponse::kTextPayload: - TruncateDebugPayload(debug_response.mutable_text_payload()); - break; - case ConformanceResponse::kJspbPayload: - TruncateDebugPayload(debug_response.mutable_jspb_payload()); - break; - default: - // Do nothing. - break; - } - return debug_response; -} - -void ConformanceTestSuite::ReportSuccess(const TestStatus& test) { - if (expected_to_fail_.contains(test.name())) { - absl::StrAppendFormat(&output_, - "ERROR: test %s (matched to %s) is in the failure " - "list, but test succeeded. " - "Remove its match from the failure list.\n", - test.name(), - expected_to_fail_[test.name()].matched_name()); - unexpected_succeeding_tests_[test.name()] = expected_to_fail_[test.name()]; - } - expected_to_fail_.erase(test.name()); - successes_++; -} - -void ConformanceTestSuite::ReportFailure(TestStatus& test, - ConformanceLevel level, - const ConformanceRequest& request, - const ConformanceResponse& response) { - if (expected_to_fail_.contains(test.name())) { - // Make copy just this once, as we need to modify them for comparison. - // Failure message from the failure list. - string expected_failure_message = - expected_to_fail_[test.name()].failure_message(); - // Actual failure message from the test run. - std::string actual_failure_message = test.failure_message(); - - Normalize(actual_failure_message); - if (actual_failure_message.rfind(expected_failure_message, 0) == 0) { - // Our failure messages match. - expected_failures_++; - } else { - // We want to add the test to the failure list with its correct failure - // message. - unexpected_failure_messages_[test.name()] = FormatFailureMessage(test); - // We want to remove the test from the failure list. That means passing - // to it the same failure message that was in the list. - TestStatus incorrect_failure_message; - incorrect_failure_message.set_name(test.name()); - incorrect_failure_message.set_failure_message(expected_failure_message); - incorrect_failure_message.set_matched_name( - expected_to_fail_[test.name()].matched_name()); - - expected_failure_messages_[test.name()] = incorrect_failure_message; - } - expected_to_fail_.erase(test.name()); - if (!verbose_) return; - } else if (level == RECOMMENDED && !enforce_recommended_) { - absl::StrAppendFormat(&output_, "WARNING, test=%s: ", test.name()); - } else { - absl::StrAppendFormat(&output_, "ERROR, test=%s: ", test.name()); - - unexpected_failing_tests_[test.name()] = FormatFailureMessage(test); - } - - absl::StrAppendFormat(&output_, "%s, request=%s, response=%s\n", - test.failure_message(), - TruncateRequest(request).ShortDebugString(), - TruncateResponse(response).ShortDebugString()); -} - -void ConformanceTestSuite::ReportSkip(const TestStatus& test, - const ConformanceRequest& request, - const ConformanceResponse& response) { - if (verbose_) { - absl::StrAppendFormat( - &output_, "SKIPPED, test=%s request=%s, response=%s\n", test.name(), - request.ShortDebugString(), response.ShortDebugString()); - } - skipped_[test.name()] = test; -} - -void ConformanceTestSuite::RunValidInputTest( - const ConformanceRequestSetting& setting, - const std::string& equivalent_text_format) { - std::unique_ptr<Message> reference_message(setting.NewTestMessage()); - ABSL_CHECK(TextFormat::ParseFromString(equivalent_text_format, - reference_message.get())) - << "Failed to parse data for test case: " << setting.GetTestName() - << ", data: " << equivalent_text_format; - const std::string equivalent_wire_format = - reference_message->SerializeAsString(); - RunValidBinaryInputTest(setting, equivalent_wire_format); -} - -void ConformanceTestSuite::RunValidBinaryInputTest( - const ConformanceRequestSetting& setting, - const std::string& equivalent_wire_format, bool require_same_wire_format) { - const ConformanceRequest& request = setting.GetRequest(); - ConformanceResponse response; - if (!RunTest(setting.GetTestName(), request, &response)) { - return; - } - - VerifyResponse(setting, equivalent_wire_format, response, true, - require_same_wire_format); -} - -void ConformanceTestSuite::VerifyResponse( - const ConformanceRequestSetting& setting, - const std::string& equivalent_wire_format, - const ConformanceResponse& response, bool need_report_success, - bool require_same_wire_format) { - std::unique_ptr<Message> test_message(setting.NewTestMessage()); - const ConformanceRequest& request = setting.GetRequest(); - const std::string& test_name = setting.GetTestName(); - ConformanceLevel level = setting.GetLevel(); - std::unique_ptr<Message> reference_message = setting.NewTestMessage(); - - ABSL_CHECK(reference_message->ParseFromString(equivalent_wire_format)) - << "Failed to parse wire data for test case: " << test_name; - - TestStatus test; - test.set_name(test_name); - - switch (response.result_case()) { - case ConformanceResponse::RESULT_NOT_SET: - test.set_failure_message( - "Response didn't have any field in the Response."); - ReportFailure(test, level, request, response); - return; - - case ConformanceResponse::kParseError: - case ConformanceResponse::kTimeoutError: - case ConformanceResponse::kRuntimeError: - case ConformanceResponse::kSerializeError: - test.set_failure_message("Failed to parse input or produce output."); - ReportFailure(test, level, request, response); - return; - - case ConformanceResponse::kSkipped: - ReportSkip(test, request, response); - return; - - default: - if (!ParseResponse(response, setting, test_message.get())) return; - } - - MessageDifferencer differencer; - DefaultFieldComparator field_comparator; - field_comparator.set_treat_nan_as_equal(true); - differencer.set_field_comparator(&field_comparator); - std::string differences; - differencer.ReportDifferencesToString(&differences); - - bool check = false; - - if (require_same_wire_format) { - ABSL_DCHECK_EQ(response.result_case(), - ConformanceResponse::kProtobufPayload); - const std::string& protobuf_payload = response.protobuf_payload(); - check = equivalent_wire_format == protobuf_payload; - differences = absl::StrCat("Expect: ", ToOctString(equivalent_wire_format), - ", but got: ", ToOctString(protobuf_payload)); - } else { - check = differencer.Compare(*reference_message, *test_message); - } - if (check) { - if (need_report_success) { - ReportSuccess(test); - } - } else { - test.set_failure_message(absl::StrCat( - "Output was not equivalent to reference message: ", differences)); - ReportFailure(test, level, request, response); - } -} - -bool ConformanceTestSuite::RunTest(const std::string& test_name, - const ConformanceRequest& request, - ConformanceResponse* response) { - if (test_names_ran_.insert(test_name).second == false) { - ABSL_LOG(FATAL) << "Duplicated test name: " << test_name; - } - - // In essence, find what wildcarded test names expand to or direct matches - // (without wildcards). - auto result = failure_list_root_.WalkDownMatch(test_name); - if (result.has_value()) { - string matched_equivalent = result.value(); - unmatched_.erase(matched_equivalent); - TestStatus expansion; - expansion.set_name(test_name); - expansion.set_matched_name(matched_equivalent); - expansion.set_failure_message(saved_failure_messages_[matched_equivalent]); - expected_to_fail_[test_name] = expansion; - - if (number_of_matches_.contains(matched_equivalent)) { - if (number_of_matches_[matched_equivalent] > kMaximumWildcardExpansions && - !exceeded_max_matches_.contains(matched_equivalent)) { - exceeded_max_matches_[matched_equivalent] = expansion; - } - number_of_matches_[matched_equivalent]++; - } else { - number_of_matches_[matched_equivalent] = 1; - } - } - - std::string serialized_request; - ABSL_CHECK(request.SerializeToString(&serialized_request)); - - uint32_t len = ::google::protobuf::internal::little_endian::FromHost( - static_cast<uint32_t>(serialized_request.size())); - - if (isolated_) { - if (names_to_test_.erase(test_name) == - 0) { // Tests were asked to be run in isolated mode, but this test was - // not asked to be run. - expected_to_fail_.erase(test_name); - return false; - } - if (debug_) { - std::string octal = ProduceOctalSerialized(serialized_request, len); - std::string full_filename = WriteToFile(octal, output_dir_, test_name); - if (!full_filename.empty()) { - absl::StrAppendFormat( - &output_, "Produced octal serialized request file for test %s\n", - test_name); - absl::StrAppendFormat( - &output_, - " To pipe the " - "serialized request directly to " - "the " - "testee run from the root of your workspace:\n printf $(" - "<\"%s\") | %s\n\n", - full_filename, testee_); - absl::StrAppendFormat( - &output_, - " To inspect the wire format of the serialized request with " - "protoscope run " - "(Disclaimer: This may not work properly on non-Linux " - "platforms):\n " - " " - "contents=$(<\"%s\"); sub=$(cut -d \\\\ -f 6- <<< " - "$contents) ; printf \"\\\\${sub}\" | protoscope \n\n\n", - full_filename); - } - } - } - - response->set_protobuf_payload(serialized_request); - - std::string serialized_response = - runner_->RunTest(test_name, serialized_request); - - if (!response->ParseFromString(serialized_response)) { - response->Clear(); - response->set_runtime_error("response proto could not be parsed."); - } - - if (verbose_) { - absl::StrAppendFormat( - &output_, "conformance test: name=%s, request=%s, response=%s\n", - test_name, TruncateRequest(request).ShortDebugString(), - TruncateResponse(*response).ShortDebugString()); - } - return true; -} - -std::string ConformanceTestSuite::WireFormatToString(WireFormat wire_format) { - switch (wire_format) { - case ::conformance::PROTOBUF: - return "PROTOBUF"; - case ::conformance::JSON: - return "JSON"; - case ::conformance::JSPB: - return "JSPB"; - case ::conformance::TEXT_FORMAT: - return "TEXT_FORMAT"; - case ::conformance::UNSPECIFIED: - return "UNSPECIFIED"; - default: - ABSL_LOG(FATAL) << "unknown wire type: " << wire_format; - } - return ""; -} - -bool ConformanceTestSuite::AddExpectedFailedTest( - const TestStatus& expected_failure) { - absl::Status attempt = failure_list_root_.Insert(expected_failure.name()); - if (!attempt.ok()) { - absl::StrAppend(&output_, attempt.message(), "\n\n"); - return false; - } - unmatched_[expected_failure.name()] = expected_failure; - saved_failure_messages_[expected_failure.name()] = - expected_failure.failure_message(); - return true; -} - -bool ConformanceTestSuite::RunSuite(ConformanceTestRunner* runner, - std::string* output, - const std::string& filename, - ::conformance::FailureSet* failure_list) { - runner_ = runner; - failure_list_root_ = FailureListTrieNode("root"); - successes_ = 0; - expected_failures_ = 0; - skipped_.clear(); - test_names_ran_.clear(); - unexpected_failing_tests_.clear(); - unexpected_succeeding_tests_.clear(); - - std::string mode = debug_ ? "DEBUG" : "TEST"; - absl::StrAppendFormat( - &output_, "CONFORMANCE %s BEGIN ====================================\n\n", - mode); - - failure_list_filename_ = filename; - expected_to_fail_.clear(); - for (const TestStatus& expected_failure : failure_list->test()) { - if (!AddExpectedFailedTest(expected_failure)) { - output->assign(output_); - return false; - } - } - - RunSuiteImpl(); - - if (*output_dir_.rbegin() != '/') { - output_dir_.push_back('/'); - } - - bool ok = true; - if (!CheckSetEmpty( - unmatched_, "unmatched.txt", - absl::StrCat( - "These test names were listed in the failure list, but they " - "didn't match any actual test name. Remove them from the " - "failure list by running from the root of your workspace:\n" - " bazel run " - "//google/protobuf/conformance:update_failure_list -- ", - failure_list_filename_, " --remove ", output_dir_, - "unmatched.txt"), - output_dir_, &output_)) { - ok = false; - } - - if (!CheckSetEmpty( - expected_failure_messages_, "expected_failure_messages.txt", - absl::StrCat( - "These tests (either expanded from wildcard(s) or direct " - "matches) were listed in the failure list, but their " - "failure messages do not match. Remove their match from the " - "failure list by running from the root of your workspace:\n" - " bazel run ", - "//google/protobuf/conformance:update_failure_list -- ", - failure_list_filename_, " --remove ", output_dir_, - "expected_failure_messages.txt"), - output_dir_, &output_)) { - ok = false; - } - - if (!CheckSetEmpty( - unexpected_succeeding_tests_, "succeeding_tests.txt", - absl::StrCat( - "These tests succeeded, even though they were listed in " - "the failure list (expanded from wildcard(s) or direct matches). " - " Remove their match from the failure list by " - "running from the root of your workspace:\n" - " bazel run " - "//google/protobuf/conformance:update_failure_list -- ", - failure_list_filename_, " --remove ", output_dir_, - "succeeding_tests.txt"), - output_dir_, &output_)) { - ok = false; - } - - if (!CheckSetEmpty( - exceeded_max_matches_, "exceeded_max_matches.txt", - absl::StrFormat( - "These failure list entries served as matches to too many test " - "names exceeding the max amount of %d. " - "Remove them from the failure list by running from the root of " - "your workspace:\n" - " bazel run " - "//google/protobuf/conformance:update_failure_list -- %s " - "--remove %sexceeded_max_matches.txt", - kMaximumWildcardExpansions, failure_list_filename_, output_dir_), - output_dir_, &output_)) { - ok = false; - } - - if (!CheckSetEmpty( - unexpected_failure_messages_, "unexpected_failure_messages.txt", - absl::StrCat( - "These tests (expanded from wildcard(s) or direct matches from " - "the failure list) failed because their failure messages did " - "not match. If they can't be fixed right now, " - "you can add them to the failure list so the overall " - "suite can succeed. Add them to the failure list by " - "running from the root of your workspace:\n" - " bazel run " - "//google/protobuf/conformance:update_failure_list -- ", - failure_list_filename_, " --add ", output_dir_, - "unexpected_failure_messages.txt"), - output_dir_, &output_)) { - ok = false; - } - - if (!CheckSetEmpty( - unexpected_failing_tests_, "failing_tests.txt", - absl::StrCat( - "These tests failed. If they can't be fixed right now, " - "you can add them to the failure list so the overall " - "suite can succeed. Add them to the failure list by " - "running from the root of your workspace:\n" - " bazel run " - "//google/protobuf/conformance:update_failure_list -- ", - failure_list_filename_, " --add ", output_dir_, - "failing_tests.txt"), - output_dir_, &output_)) { - ok = false; - } - - if (verbose_) { - CheckSetEmpty(skipped_, "", - "These tests were skipped (probably because support for some " - "features is not implemented)", - output_dir_, &output_); - } - - absl::StrAppendFormat(&output_, - "CONFORMANCE SUITE %s: %d successes, %zu skipped, " - "%d expected failures, %zu unexpected failures.\n", - ok ? "PASSED" : "FAILED", successes_, skipped_.size(), - expected_failures_, unexpected_failing_tests_.size()); - absl::StrAppendFormat(&output_, "\n"); - - output->assign(output_); - - return ok; -} - -} // namespace conformance -} // namespace protobuf -} // namespace google
diff --git a/conformance/conformance_test.h b/conformance/conformance_test.h deleted file mode 100644 index 991f9b0..0000000 --- a/conformance/conformance_test.h +++ /dev/null
@@ -1,322 +0,0 @@ -// Protocol Buffers - Google's data interchange format -// Copyright 2008 Google Inc. All rights reserved. -// -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file or at -// https://developers.google.com/open-source/licenses/bsd - -// This file defines a protocol for running the conformance test suite -// in-process. In other words, the suite itself will run in the same process as -// the code under test. -// -// For pros and cons of this approach, please see conformance.proto. - -#ifndef GOOGLE_PROTOBUF_CONFORMANCE_CONFORMANCE_TEST_H__ -#define GOOGLE_PROTOBUF_CONFORMANCE_CONFORMANCE_TEST_H__ - -#include <memory> -#include <string> -#include <utility> -#include <vector> - -#include "google/protobuf/descriptor.pb.h" -#include "absl/container/btree_map.h" -#include "absl/container/flat_hash_set.h" -#include "conformance/conformance.pb.h" -#include "conformance/failure_list_trie_node.h" -#include "conformance/test_runner.h" -#include "google/protobuf/descriptor.h" - -namespace conformance { -class ConformanceRequest; -class ConformanceResponse; -} // namespace conformance - -namespace protobuf_test_messages { -namespace proto3 { -class TestAllTypesProto3; -} // namespace proto3 -} // namespace protobuf_test_messages - -namespace google { -namespace protobuf { -namespace conformance { - -class ConformanceTestSuite; - -int RunConformanceTests(int argc, char* argv[], - const std::vector<ConformanceTestSuite*>& suites); - -// Class representing the test suite itself. To run it, implement your own -// class derived from ConformanceTestRunner, class derived from -// ConformanceTestSuite and then write code like: -// -// class MyConformanceTestSuite : public ConformanceTestSuite { -// public: -// void RunSuiteImpl() { -// // INSERT ACTUAL TESTS. -// } -// }; -// -// class MyConformanceTestRunner : public ConformanceTestRunner { -// public: -// static int Run(int argc, char *argv[], -// ConformanceTestSuite* suite); -// -// private: -// virtual void RunTest(...) { -// // INSERT YOUR FRAMEWORK-SPECIFIC CODE HERE. -// } -// }; -// -// int main() { -// MyConformanceTestSuite suite; -// MyConformanceTestRunner::Run(argc, argv, &suite); -// } -// -class ConformanceTestSuite { - public: - ConformanceTestSuite() = default; - virtual ~ConformanceTestSuite() = default; - - void SetPerformance(bool performance) { performance_ = performance; } - void SetVerbose(bool verbose) { verbose_ = verbose; } - - // Whether to require the testee to pass RECOMMENDED tests. By default failing - // a RECOMMENDED test case will not fail the entire suite but will only - // generated a warning. If this flag is set to true, RECOMMENDED tests will - // be treated the same way as REQUIRED tests and failing a RECOMMENDED test - // case will cause the entire test suite to fail as well. An implementation - // can enable this if it wants to be strictly conforming to protobuf spec. - // See the comments about ConformanceLevel below to learn more about the - // difference between REQUIRED and RECOMMENDED test cases. - void SetEnforceRecommended(bool value) { enforce_recommended_ = value; } - - // Sets the maximum edition (inclusive) that should be tests for conformance. - void SetMaximumEdition(Edition edition) { maximum_edition_ = edition; } - - // Gets the flag name to the failure list file. - // By default, this would return --failure_list - std::string GetFailureListFlagName() { return failure_list_flag_name_; } - - void SetFailureListFlagName(const std::string& failure_list_flag_name) { - failure_list_flag_name_ = failure_list_flag_name; - } - - // Sets the path of the output directory. - void SetOutputDir(const std::string& output_dir) { output_dir_ = output_dir; } - - // Sets if we are running the test in debug mode. - void SetDebug(bool debug) { debug_ = debug; } - - // Sets if we are running ONLY the tests provided in the 'names_to_test_' set. - void SetIsolated(bool isolated) { isolated_ = isolated; } - - // Sets the file path of the testee. - void SetTestee(const std::string& testee) { testee_ = testee; } - - // Sets the names of tests to ONLY be run isolated from all the others. - void SetNamesToTest(absl::flat_hash_set<std::string> names_to_test) { - names_to_test_ = std::move(names_to_test); - } - - absl::flat_hash_set<std::string> GetExpectedTestsNotRun() { - return names_to_test_; - } - - // Run all the conformance tests against the given test runner. - // Test output will be stored in "output". - // - // Returns true if the set of failing tests was exactly the same as the - // failure list. - // The filename here is *only* used to create/format useful error messages for - // how to update the failure list. We do NOT read this file at all. - - bool RunSuite(ConformanceTestRunner* runner, std::string* output, - const std::string& filename, - ::conformance::FailureSet* failure_list); - - protected: - // Test cases are classified into a few categories: - // REQUIRED: the test case must be passed for an implementation to be - // interoperable with other implementations. For example, a - // parser implementation must accept both packed and unpacked - // form of repeated primitive fields. - // RECOMMENDED: the test case is not required for the implementation to - // be interoperable with other implementations, but is - // recommended for best performance and compatibility. For - // example, a proto3 serializer should serialize repeated - // primitive fields in packed form, but an implementation - // failing to do so will still be able to communicate with - // other implementations. - enum ConformanceLevel { - REQUIRED = 0, - RECOMMENDED = 1, - }; - - class ConformanceRequestSetting { - public: - ConformanceRequestSetting(ConformanceLevel level, - ::conformance::WireFormat input_format, - ::conformance::WireFormat output_format, - ::conformance::TestCategory test_category, - const Message& prototype_message, - const std::string& test_name, - const std::string& input); - virtual ~ConformanceRequestSetting() = default; - - std::unique_ptr<Message> NewTestMessage() const; - - std::string GetSyntaxIdentifier() const; - - std::string GetTestName() const; - - const ::conformance::ConformanceRequest& GetRequest() const { - return request_; - } - - ConformanceLevel GetLevel() const { return level_; } - - std::string ConformanceLevelToString(ConformanceLevel level) const; - - void SetPrintUnknownFields(bool print_unknown_fields) { - request_.set_print_unknown_fields(true); - } - - void SetPrototypeMessageForCompare(const Message& message) { - prototype_message_for_compare_.reset(message.New()); - } - - protected: - virtual std::string InputFormatString( - ::conformance::WireFormat format) const; - virtual std::string OutputFormatString( - ::conformance::WireFormat format) const; - ::conformance::ConformanceRequest request_; - - private: - ConformanceLevel level_; - ::conformance::WireFormat input_format_; - ::conformance::WireFormat output_format_; - const Message& prototype_message_; - std::unique_ptr<Message> prototype_message_for_compare_; - std::string test_name_; - }; - - std::string WireFormatToString(::conformance::WireFormat wire_format); - - // Parse payload in the response to the given message. Returns true on - // success. - virtual bool ParseResponse(const ::conformance::ConformanceResponse& response, - const ConformanceRequestSetting& setting, - Message* test_message) = 0; - - void VerifyResponse(const ConformanceRequestSetting& setting, - const std::string& equivalent_wire_format, - const ::conformance::ConformanceResponse& response, - bool need_report_success, bool require_same_wire_format); - - void TruncateDebugPayload(std::string* payload); - ::conformance::ConformanceRequest TruncateRequest( - const ::conformance::ConformanceRequest& request); - ::conformance::ConformanceResponse TruncateResponse( - const ::conformance::ConformanceResponse& response); - - void ReportSuccess(const ::conformance::TestStatus& test); - void ReportFailure(::conformance::TestStatus& test, ConformanceLevel level, - const ::conformance::ConformanceRequest& request, - const ::conformance::ConformanceResponse& response); - void ReportSkip(const ::conformance::TestStatus& test, - const ::conformance::ConformanceRequest& request, - const ::conformance::ConformanceResponse& response); - - void RunValidInputTest(const ConformanceRequestSetting& setting, - const std::string& equivalent_text_format); - void RunValidBinaryInputTest(const ConformanceRequestSetting& setting, - const std::string& equivalent_wire_format, - bool require_same_wire_format = false); - - // Returns true if our runner_ ran the test and false if it did not. - bool RunTest(const std::string& test_name, - const ::conformance::ConformanceRequest& request, - ::conformance::ConformanceResponse* response); - - // Will return false if an entry from the failure list was either a - // duplicate of an already added one to the trie or it contained invalid - // wildcards; otherwise, returns true. - bool AddExpectedFailedTest(const ::conformance::TestStatus& failure); - - virtual void RunSuiteImpl() = 0; - - ConformanceTestRunner* runner_; - FailureListTrieNode failure_list_root_; - std::string testee_; - int successes_; - int expected_failures_; - bool verbose_ = false; - bool performance_ = false; - bool enforce_recommended_ = false; - Edition maximum_edition_ = Edition::EDITION_PROTO3; - std::string output_; - std::string output_dir_; - std::string failure_list_flag_name_ = "--failure_list"; - std::string failure_list_filename_; - absl::flat_hash_set<std::string> names_to_test_; - bool debug_ = false; - // If names were given for names_to_test_, only those tests - // will be run and this bool will be set to true. - bool isolated_ = false; - - // The set of test names (expanded from wildcard(s) and non-expanded) that are - // expected to fail in this run, but haven't failed yet. - absl::btree_map<std::string, ::conformance::TestStatus> expected_to_fail_; - - // The set of tests that failed because their failure message did not match - // the actual failure message. These are failure messages that may need to be - // removed from our failure lists. - absl::btree_map<std::string, ::conformance::TestStatus> - expected_failure_messages_; - - // The set of test names that have been run. Used to ensure that there are no - // duplicate names in the suite. - absl::flat_hash_set<std::string> test_names_ran_; - - // The set of tests that failed, but weren't expected to: They weren't - // present in our failure lists. - absl::btree_map<std::string, ::conformance::TestStatus> - unexpected_failing_tests_; - - // The set of tests that succeeded, but weren't expected to: They were present - // in our failure lists, but managed to succeed. - absl::btree_map<std::string, ::conformance::TestStatus> - unexpected_succeeding_tests_; - - // The set of tests that failed because their failure message did not match - // the actual failure message. These are failure messages that may need to be - // added to our failure lists. - absl::btree_map<std::string, ::conformance::TestStatus> - unexpected_failure_messages_; - - // The set of test names (wildcarded or not) from the failure list that did - // not match any actual test name. - absl::btree_map<std::string, ::conformance::TestStatus> unmatched_; - - // The set of tests that the testee opted out of; - absl::btree_map<std::string, ::conformance::TestStatus> skipped_; - - // Allows us to remove from unmatched_. - absl::btree_map<std::string, std::string> saved_failure_messages_; - - // If a failure list entry served as a match for more than 'max_matches_', - // those will be added here for removal. - absl::btree_map<std::string, ::conformance::TestStatus> exceeded_max_matches_; - - // Keeps track of how many tests matched to each failure list entry. - absl::btree_map<std::string, int> number_of_matches_; -}; - -} // namespace conformance -} // namespace protobuf -} // namespace google - -#endif // GOOGLE_PROTOBUF_CONFORMANCE_CONFORMANCE_TEST_H__
diff --git a/conformance/conformance_test_main.cc b/conformance/conformance_test_main.cc index 3e4dd84..cffd864 100644 --- a/conformance/conformance_test_main.cc +++ b/conformance/conformance_test_main.cc
@@ -5,13 +5,746 @@ // license that can be found in the LICENSE file or at // https://developers.google.com/open-source/licenses/bsd -#include "conformance/binary_json_conformance_suite.h" -#include "conformance/conformance_test.h" -#include "conformance/text_format_conformance_suite.h" +// The main of conformance_test_runner. +// +// The runner runs the gtest-based conformance suites (see test_fixture.h) +// linked into this binary, the alwayslink libraries in conformance_test_main's +// BUILD deps (binary_conformance_tests, json_conformance_tests, +// performance_conformance_tests, text_conformance_tests), against the testee +// named on its command line. The command line and the report format are those +// of the legacy runner, which ran hand-written suites instead (see +// conformance_test_runner.h), so nothing changes for its users, the test +// targets and scripts that invoke it with their failure lists and --output_dir: +// +// * gtest flags given before the testee are accepted too. Both spellings +// work everywhere: --gtest_* (the open source one) and --gunit_* (the +// google3 one); a --gtest_ flag is rewritten to the prefix this binary's +// gtest was built with (see RewriteGtestFlagPrefix()), since google3's +// gtest rejects the other. +// * --failure_list and --text_format_failure_list (see kFailureListFlag and +// kTextFormatFailureListFlag in conformance_test_runner.h) are loaded into +// the gtest suites' one ResultLedger. An entry that matches no test is +// reported as unmatched, in the legacy format, and fails the run like it +// failed the legacy suites. +// * --test <name> isolates the named tests (see FilteringTestRunner), and +// names that don't exist are reported as before. +// * --performance selects the performance tests: the test suites of the +// performance conformance suite, which are the ones named *PerformanceTest +// (see test_fixture.h), are selected through the gtest filter, and +// deselected without the flag (see PerformanceGtestFilter()). +// * --record_requests writes every request into one file. +// * --output_dir receives the files update_failure_list consumes: the gtest +// suites write failing_tests.txt (unexpected failures), +// succeeding_tests.txt (unexpected successes), listed_skips.txt and +// unmatched.txt in the legacy format, see ReportTestStatusSet(). +// +// TODO: retire the legacy-compatible parts of this command line +// (--failure_list / --text_format_failure_list and conformance_test_wrapper.sh) +// once the remaining sh_test and script invocations have moved to the +// conformance_test() macro, which runs the gtest suites as ordinary test +// binaries (see test_environment_main.cc). +// +// Behavior notes, i.e. what does differ from the legacy runner: +// +// * gtest flags are accepted before the testee, in their `--flag=value` form +// only (the runner's own parser would take a separate value for the +// testee). Anything else starting with --gtest_ or --gunit_ is still +// rejected, but by whoever sees it first: in google3 InitGoogle() exits +// with its own "Unknown command line flag" error, in open source the +// runner prints its usage text as before. +// * The runner is a self-contained program: the environment variables +// through which bazel and the test bridge configure a gtest binary +// (kInheritedTestEnvironmentVariables: test filter, sharding, fail-fast, +// result and status files, ...) are meant for the enclosing test and are +// cleared before gtest initializes, like the legacy runner ignored them, +// and the gtest flags open source gtest has already defaulted from them by +// then are reset (see ResetInheritedGtestFlags()). Explicit gtest flags +// on the command line are the only way to filter, shard or get an XML/JSON +// report of the run. As a consequence, the testee doesn't see those +// variables either. +// * Unless a gtest flag was given, the run prints gtest's brief output +// (--gtest_brief=1): failures, skips and the summary, not every test. +// * --debug and --verbose are accepted but have no effect: they only ever +// affected the legacy suites (see ConformanceRunnerOptions). gtest's own +// flags take their place (--gtest_brief=0 lists every test's result). +// * testing::InitGoogleTest() runs. In google3 that also runs InitGoogle(): +// absl logging is initialized (ABSL_LOG(INFO) lines, e.g. the testee +// command line logged by ForkPipeRunner, go to the log files instead of +// stderr) and the failure signal handler is installed. +// * --gtest_repeat is not supported (see test_environment.h). +// * --gtest_filter, test sharding and --gtest_fail_fast make the run a +// partial one, which disables the unmatched-entry check: a failure list +// entry that no test matched may simply belong to a test that wasn't +// selected (or run). The same applies when the filter selects no test at +// all. The selection --performance (or its absence) makes is not partial: +// its baseline is the tests of the selected kind (see +// IsPartialGtestPhase()). An explicit --gtest_filter applies within that +// selection. +// * --gtest_output only takes effect when given on the command line itself, +// not through --gtest_flagfile (see GtestOutputRequested()). +// * If writing the --record_requests file fails *and* --test named tests +// that don't exist, the legacy "do not exist" report is printed first and +// the write error last. +// * The gtest suites only report the --output_dir categories above. A test +// that was expected to fail with a different message is an unexpected +// failure to them (the legacy suites reported it separately, in +// unexpected_failure_messages.txt and expected_failure_messages.txt). +// Since the gtest suites load every failure list into one ResultLedger, +// their report comes in one block per failure list flag, each with the +// hint naming that flag's last file: a result matched by a failure list +// entry goes to the flag whose file lists the entry, a test in no list to +// --text_format_failure_list if it is a text-format test (see +// IsTextFormatTest()) and to --failure_list otherwise. Like the legacy +// suites among themselves, a later block overwrites the --output_dir +// files of an earlier one. +// TODO: goes away with the --text_format_failure_list flag; +// conformance_test() passes one failure list per suite instead. -int main(int argc, char *argv[]) { - google::protobuf::conformance::BinaryAndJsonConformanceSuite binary_and_json_suite; - google::protobuf::conformance::TextFormatConformanceTestSuite text_format_suite; - return google::protobuf::conformance::RunConformanceTests( - argc, argv, {&binary_and_json_suite, &text_format_suite}); +#include <algorithm> +#include <cstddef> +#include <cstdio> // IWYU pragma: keep +#include <cstdlib> +#include <fstream> +#include <memory> +#include <ostream> +#include <string> +#include <utility> +#include <vector> + +#include "google/protobuf/descriptor.pb.h" +#include <gtest/gtest.h> +#include "absl/base/nullability.h" +#include "absl/container/flat_hash_map.h" +#include "absl/container/flat_hash_set.h" +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "absl/strings/match.h" +#include "absl/strings/str_cat.h" +#include "absl/strings/str_format.h" +#include "absl/strings/str_join.h" +#include "absl/strings/string_view.h" +#include "absl/types/optional.h" +#include "absl/types/span.h" +#include "conformance/conformance_test_runner.h" +#include "conformance/filtering_test_runner.h" +#include "conformance/fork_pipe_runner.h" +#include "conformance/recording_test_runner.h" +#include "conformance/result_ledger.h" +#include "conformance/test_environment.h" +#include "conformance/test_runner.h" +#include "conformance/testee.h" + +namespace google { +namespace protobuf { +namespace conformance { +namespace { + +// The environment variables through which bazel and the test bridge configure +// the gtest binary they run. Under `bazel test` they describe the *enclosing* +// test (typically an sh_test wrapping this runner), so the gtest phase must not +// act on them: it would run only that test's shard or filter, stop at its +// first failure, or overwrite its result file. Cleared before +// testing::InitGoogleTest(); see the file comment and +// ResetInheritedGtestFlags(). Both the open source (GTEST_) and google3 +// (GUNIT_) spellings are listed. +constexpr const char* kInheritedTestEnvironmentVariables[] = { + // Test selection, sharding and shuffling. + "TESTBRIDGE_TEST_ONLY", + "TEST_TOTAL_SHARDS", + "TEST_SHARD_INDEX", + "TEST_SHARD_STATUS_FILE", + "GTEST_FILTER", + "GUNIT_FILTER", + "GTEST_FAIL_IF_NO_TEST_SELECTED", + "GUNIT_FAIL_IF_NO_TEST_SELECTED", + "GTEST_ALSO_RUN_DISABLED_TESTS", + "GUNIT_ALSO_RUN_DISABLED_TESTS", + "GTEST_SHUFFLE", + "GUNIT_SHUFFLE", + "GTEST_RANDOM_SEED", + "GUNIT_RANDOM_SEED", + // How far the run goes and how it is reported. + "TESTBRIDGE_TEST_RUNNER_FAIL_FAST", + "GTEST_FAIL_FAST", + "GUNIT_FAIL_FAST", + "GTEST_REPEAT", + "GUNIT_REPEAT", + "GTEST_BRIEF", + "GUNIT_BRIEF", + "GTEST_FLAGFILE", + "GUNIT_FLAGFILE", + // Result and status files. + "GTEST_OUTPUT", + "GUNIT_OUTPUT", + "XML_OUTPUT_FILE", + "TEST_PREMATURE_EXIT_FILE", + "TEST_WARNINGS_OUTPUT_FILE", +}; + +// Resets the gtest flags that kInheritedTestEnvironmentVariables would have +// defaulted to their built-in values, and picks the brief output unless +// `gtest_args` (the explicit gtest flags) configure gtest. Must be called +// after the variables are cleared and before testing::InitGoogleTest(). +// +// Clearing the variables is enough in google3, whose gtest flags are absl +// flags that evaluate their environment-derived default lazily, on first +// access; that happens after the variables are gone. Open source gtest +// evaluates them once, at static initialization, before main() runs: by now +// the enclosing test's filter, fail-fast, shuffle, ... settings are already +// baked into the flags, so they are reset here. Explicit gtest flags are +// parsed afterwards, by InitGoogleTest(), and therefore still win. +// +// Not reset here: sharding, whose flags don't exist in every gtest version +// and whose variables gtest only reads in RUN_ALL_TESTS() (after they are +// cleared; a shard that got through anyway makes the run partial, see +// IsPartialGtestPhase()), and the output flag, which is handled after +// InitGoogleTest() (see GtestOutputRequested()). +void ResetInheritedGtestFlags(absl::Span<const std::string> gtest_args) { + GTEST_FLAG_SET(filter, "*"); + GTEST_FLAG_SET(also_run_disabled_tests, false); + GTEST_FLAG_SET(shuffle, false); + GTEST_FLAG_SET(random_seed, 0); + GTEST_FLAG_SET(fail_fast, false); + GTEST_FLAG_SET(repeat, 1); + // Keep the gtest phase's output as short as the legacy reports, unless the + // caller configured gtest themselves. Like the others, this has to happen + // before InitGoogleTest(), which is where gtest picks its result printer + // (in PostFlagParsingInit()). + GTEST_FLAG_SET(brief, gtest_args.empty()); +} + +// The gtest output flag, spelled as this binary's gtest expects it, e.g. +// "--gunit_output=". gtest only accepts the `--flag=value` form for it (and so +// does this runner, see the file comment), so that is the only form matched. +constexpr absl::string_view kGtestOutputFlagPrefix = + "--" GTEST_FLAG_PREFIX_ "output="; + +// Returns true if gtest output was asked for explicitly on the command line. +// `gtest_args` must already have been through RewriteGtestFlagPrefix(). The +// environment variables gtest would otherwise default the flag from are +// deliberately not consulted: they belong to the enclosing test (see +// kInheritedTestEnvironmentVariables). Neither is --gtest_flagfile: an output +// flag given through it isn't seen here and so has no effect. +bool GtestOutputRequested(absl::Span<const std::string> gtest_args) { + for (const std::string& arg : gtest_args) { + if (absl::StartsWith(arg, kGtestOutputFlagPrefix)) return true; + } + return false; +} + +// The names of every gtest test suite registered in this binary. +std::vector<std::string> RegisteredTestSuiteNames() { + const testing::UnitTest& unit_test = *testing::UnitTest::GetInstance(); + const int count = unit_test.total_test_suite_count(); + std::vector<std::string> names; + names.reserve(static_cast<size_t>(count)); + for (int i = 0; i < count; ++i) { + names.push_back(unit_test.GetTestSuite(i)->name()); + } + return names; +} + +// Narrows the gtest filter to the performance test suites if `performance`, +// and to all the others otherwise; see PerformanceGtestFilter(). Must be +// called after testing::InitGoogleTest() (which parses an explicit +// --gtest_filter) and before the tests are listed or run. +void SelectPerformanceTests(bool performance) { + GTEST_FLAG_SET(filter, + PerformanceGtestFilter(GTEST_FLAG_GET(filter), performance, + RegisteredTestSuiteNames())); +} + +// The number of tests a full run runs after SelectPerformanceTests(): +// the tests of the selected kind, minus the DISABLED_ ones (like the baseline +// of the environment's IsPartialRun(), which counts every registered test). +int SelectedTestCount(bool performance) { + const testing::UnitTest& unit_test = *testing::UnitTest::GetInstance(); + int count = 0; + for (int i = 0; i < unit_test.total_test_suite_count(); ++i) { + const testing::TestSuite& test_suite = *unit_test.GetTestSuite(i); + if (IsPerformanceFixture(test_suite.name()) != performance) continue; + count += test_suite.total_test_count() - test_suite.disabled_test_count(); + } + return count; +} + +// Whether only some of the `selected_test_count` tests of the selected kind +// ran, because of an explicit --gtest_filter, sharding or --gtest_fail_fast. +// The runner's counterpart of the environment's IsPartialRun() (see +// test_environment.cc), which would count the tests SelectPerformanceTests() +// deselected as not run, and so call every run partial. +bool IsPartialGtestPhase(int selected_test_count) { + const testing::UnitTest& unit_test = *testing::UnitTest::GetInstance(); + if (GTEST_FLAG_GET(fail_fast) && unit_test.Failed()) return true; + return unit_test.test_to_run_count() < selected_test_count; +} + +internal::ConformanceEnvironmentOptions EnvironmentOptionsFor( + const ConformanceRunnerOptions& options, + ConformanceTestRunner* absl_nonnull runner) { + internal::ConformanceEnvironmentOptions env_options; + env_options.runner = runner; + for (const auto& [flag, file] : options.failure_list_files) { + env_options.failure_list_files.push_back(file); + } + env_options.enforcement_level = + options.enforce_recommended ? kLowestPriority : kP0; + // The legacy runner treats any maximum below PROTO3 (its EDITION_UNKNOWN + // default, or an explicit PROTO2) as "proto2 and proto3 only", which is the + // environment's EDITION_PROTO3. ConformanceEnvironment clamps this itself; + // the std::max here just keeps the intent visible. + env_options.maximum_edition = + std::max(options.maximum_edition, EDITION_PROTO3); + // --performance is applied through the gtest filter instead; see + // SelectPerformanceTests(). + // Unmatched entries are reported per failure list flag below instead. + env_options.check_unseen_expected_failures = false; + // --debug and --verbose have no effect (see the file comment). + return env_options; +} + +// Converts `results` for ReportTestStatusSet(). +std::vector<ReportedTestStatus> ToReportedTestStatuses( + absl::Span<const internal::UnexpectedResult> results) { + std::vector<ReportedTestStatus> statuses; + statuses.reserve(results.size()); + for (const internal::UnexpectedResult& result : results) { + ReportedTestStatus status; + status.test_name = result.test_name; + status.failure_message = result.failure_message; + status.matched_name = result.matched_entry.value_or(""); + statuses.push_back(std::move(status)); + } + return statuses; +} + +// Whether `test_name` is a text-format conformance test, i.e. one the legacy +// text-format suite used to run: a test with text-format input or output. +// Test names read "<Level>.<Edition>.<Format>Input.<name>[.<Format>Output]" +// (see Testee). +bool IsTextFormatTest(absl::string_view test_name) { + return absl::StrContains(test_name, ".TextFormatInput.") || + absl::EndsWith(test_name, ".TextFormatOutput"); +} + +// The failure list flag whose block reports a test that is in no failure +// list, with the hint to add it to that flag's last file (the flags are those +// of conformance_test_runner.h). +absl::string_view FailureListFlagFor(absl::string_view test_name) { + return IsTextFormatTest(test_name) ? kTextFormatFailureListFlag + : kFailureListFlag; +} + +// One block of the gtest suites' report: the results one failure list flag's +// files are responsible for, reported by ReportFailureListResults() with the +// hint naming the flag's last file (like each legacy suite names the last +// file of its flag). +struct FailureListReport { + std::string flag; + std::vector<std::string> files; + std::vector<internal::UnexpectedResult> successes; + std::vector<internal::UnexpectedResult> failures; + // (test name, matched entry); see ResultLedger::ListedSkips(). + std::vector<std::pair<std::string, std::string>> listed_skips; + // The entries no test matched, with their messages. + std::vector<ReportedTestStatus> unmatched; +}; + +// Where a failure list entry (as listed, wildcards included) comes from. +struct FailureListEntry { + size_t report = 0; // An index into the FailureListReports. + std::string message; +}; + +// The index of the report block for `flag`, appended if there is none yet +// (with no files: the caller appends those). +size_t ReportIndexFor(std::vector<FailureListReport>& reports, + absl::string_view flag) { + for (size_t i = 0; i < reports.size(); ++i) { + if (reports[i].flag == flag) return i; + } + reports.push_back({/*flag=*/std::string(flag)}); + return reports.size() - 1; +} + +// Groups the failure lists (`failure_list_files`, (flag, file) pairs in +// command line order) into one report block per flag, in order of the flags' +// first appearance, and indexes their entries in `entries`. The files are +// loaded into a scratch ResultLedger exactly as ConformanceEnvironment will, +// so that a bad failure list is reported once and clearly instead of failing +// every gtest test in SetUp(). The environment loads all the files into one +// ResultLedger, so unlike with the legacy suites (each of which only loaded its +// own flag's files) an entry may only appear in one of them: the same name in +// both --failure_list and --text_format_failure_list, or a wildcard in one +// covering an entry of the other, is an error. +// TODO: goes away with the --text_format_failure_list flag; +// conformance_test() passes one failure list per suite instead. +absl::Status LoadFailureListReports( + absl::Span<const std::pair<std::string, std::string>> failure_list_files, + std::vector<FailureListReport>& reports, + absl::flat_hash_map<std::string, FailureListEntry>& entries) { + internal::ResultLedger ledger; + absl::Status status; + std::vector<std::string> loaded; + for (const auto& [flag, file] : failure_list_files) { + status = ledger.LoadFailureList(file); + if (!status.ok()) { + if (absl::IsAlreadyExists(status)) { + status = absl::InvalidArgumentError(absl::StrCat( + "While loading failure list ", file, + loaded.empty() + ? "" + : absl::StrCat(" after ", absl::StrJoin(loaded, ", ")), + ": ", status.message(), + ". The gtest conformance suites load all the failure lists into " + "one set, so an entry (or a wildcard covering it) may only appear " + "in one of them.")); + } + break; + } + loaded.push_back(file); + const size_t index = ReportIndexFor(reports, flag); + reports[index].files.push_back(file); + // The entries this file added are the ones not indexed yet. (Nothing has + // been reported, so no entry has been matched.) + for (const std::string& entry : ledger.UnmatchedExpectedFailures()) { + if (entries.contains(entry)) continue; + entries.emplace( + entry, FailureListEntry{ + /*report=*/index, + /*message=*/*ledger.failure_list().ExpectedMessage(entry), + }); + } + } + // ResultLedger insists on being finalized before destruction. + ledger.Finalize().IgnoreError(); + return status; +} + +// Reports `report` (the gtest suites' unexpected successes, unexpected +// failures, listed skips, i.e. tests in the failure list that the testee +// skipped, and unmatched entries) to stderr and to succeeding_tests.txt / +// failing_tests.txt / listed_skips.txt / unmatched.txt in `output_dir` (which +// must end in '/' unless empty), exactly as each legacy suite reports its own; +// see ReportTestStatusSet(). The hints tell the user to update the report's +// last file. Returns false if there were unmatched entries (the only category +// the gtest assertions haven't already failed for). +bool ReportFailureListResults(const FailureListReport& report, + absl::string_view output_dir) { + // A block may have no file: a test in no list is routed to its flag's block + // (see FailureListFlagFor()) whether or not that flag was given. + const std::string failure_list = + report.files.empty() ? absl::StrCat("<the ", report.flag, " file>") + : report.files.back(); + std::string output; + ReportTestStatusSet( + ToReportedTestStatuses(report.successes), "succeeding_tests.txt", + absl::StrCat( + "These tests succeeded, even though they were listed in " + "the failure list (expanded from wildcard(s) or direct matches). " + " Remove their match from the failure list by " + "running from the root of your workspace:\n" + " bazel run " + "//third_party/protobuf/conformance:update_failure_list -- ", + failure_list, " --remove ", output_dir, "succeeding_tests.txt"), + output_dir, output); + ReportTestStatusSet( + ToReportedTestStatuses(report.failures), "failing_tests.txt", + absl::StrCat("These tests failed. If they can't be fixed right now, " + "you can add them to the failure list so the overall " + "suite can succeed. Add them to the failure list by " + "running from the root of your workspace:\n" + " bazel run " + "//third_party/protobuf/conformance:update_failure_list -- ", + failure_list, " --add ", output_dir, "failing_tests.txt"), + output_dir, output); + std::vector<ReportedTestStatus> listed_skips; + listed_skips.reserve(report.listed_skips.size()); + for (const auto& [test_name, matched_entry] : report.listed_skips) { + listed_skips.push_back( + {/*test_name=*/test_name, + /*failure_message=*/ + absl::StrCat("skipped by the testee (matched to ", matched_entry, ")"), + /*matched_name=*/matched_entry}); + } + ReportTestStatusSet( + listed_skips, "listed_skips.txt", + absl::StrCat("These tests were skipped by the testee, even though they " + "were listed in the failure list (expanded from wildcard(s) " + "or direct matches). A skipped test can't be an expected " + "failure. Remove their match from the failure list by " + "running from the root of your workspace:\n" + " bazel run " + "//third_party/protobuf/conformance:update_failure_list -- ", + failure_list, " --remove ", output_dir, "listed_skips.txt"), + output_dir, output); + const bool no_unmatched = ReportTestStatusSet( + report.unmatched, "unmatched.txt", + absl::StrCat("These test names were listed in the failure list, but they " + "didn't match any actual test name. Remove them from the " + "failure list by running from the root of your workspace:\n" + " bazel run " + "//third_party/protobuf/conformance:update_failure_list -- ", + failure_list, " --remove ", output_dir, "unmatched.txt"), + output_dir, output); + absl::FPrintF(stderr, "%s", output); + return no_unmatched; +} + +// What running the gtest suites leaves behind for Main(). +struct GtestPhaseResult { + // RUN_ALL_TESTS()'s result, or 1 if a failure list had unmatched entries. + int exit_code = 0; + // The tests --test named that the gtest suites ran. + std::vector<std::string> names_run; +}; + +// Runs the gtest suites linked into this binary against `options.testee` and +// prints their report: one block per failure list flag (see +// ReportFailureListResults()) and a summary line. A failure list entry no +// test matched fails the run, like it failed the legacy suites. Returns an +// error, before anything runs, if the failure lists can't be loaded into one +// ResultLedger (see LoadFailureListReports()). Must be called after +// testing::InitGoogleTest(), and at most once (it installs the +// ConformanceEnvironment and calls RUN_ALL_TESTS()). +absl::StatusOr<GtestPhaseResult> RunGtestPhase( + const ConformanceRunnerOptions& options, + std::ostream* absl_nullable record_requests) { + GtestPhaseResult result; + testing::UnitTest* unit_test = testing::UnitTest::GetInstance(); + + // The ForkPipeRunner (which only spawns the testee on first use) is wrapped + // for recording and for --test isolation. The runners only need to live for + // this call: the environment installed below outlives them, but it doesn't + // own the runner and drops its reference to it in TearDown() (run by gtest, + // or below if gtest selected no test); ~ConformanceEnvironment only + // finalizes its ResultLedger. + ForkPipeRunner fork_pipe_runner(options.testee, options.testee_args); + ConformanceTestRunner* gtest_runner = &fork_pipe_runner; + absl::optional<RecordingTestRunner> recording_runner; + if (record_requests != nullptr) { + recording_runner.emplace(gtest_runner, record_requests); + gtest_runner = &*recording_runner; + } + absl::optional<FilteringTestRunner> filtering_runner; + if (options.isolated) { + filtering_runner.emplace(gtest_runner, options.names_to_test); + gtest_runner = &*filtering_runner; + } + + internal::ConformanceEnvironmentOptions env_options = + EnvironmentOptionsFor(options, gtest_runner); + std::vector<FailureListReport> reports; + absl::flat_hash_map<std::string, FailureListEntry> entries; + if (absl::Status status = + LoadFailureListReports(options.failure_list_files, reports, entries); + !status.ok()) { + return status; + } + + internal::ConformanceEnvironment& environment = + internal::ConformanceEnvironment::Install(std::move(env_options)); + const int selected_test_count = SelectedTestCount(options.performance); + result.exit_code = RUN_ALL_TESTS(); + if (unit_test->test_to_run_count() == 0) { + // gtest only runs an environment's SetUp() and TearDown() if it selected + // some test to run (which is only known now, so the runners above can't + // be created conditionally). Run TearDown() ourselves, so that the + // environment (which outlives this call, see Install()) lets go of the + // runners destroyed at the end of it instead of keeping a dangling + // pointer. There is nothing else for it to do: SetUp() never ran, so no + // failure list was loaded, and no test ran either. + environment.TearDown(); + } + + const internal::ResultLedger& ledger = environment.ledger(); + // The report block a result belongs to: the one of the entry it matched, + // or, for a test in no list, the one FailureListFlagFor() says. + auto report_for_entry = [&](absl::string_view entry) -> FailureListReport& { + return reports[entries.at(entry).report]; + }; + // Only a full run can tell that an entry is stale: if only some of the gtest + // tests of the selected kind ran (--gtest_filter, sharding, + // --gtest_fail_fast), or none at all (in which case gtest didn't even set + // the environment up), an entry they didn't match may well belong to a test + // that wasn't selected, so nothing is reported rather than guessed. Nor is + // anything when the gtest suites have no test of the selected kind at all + // (e.g. --performance was given and no performance test is linked in): a + // full run of nothing, which loaded no list. + if (selected_test_count > 0 && !IsPartialGtestPhase(selected_test_count)) { + for (const std::string& entry : ledger.UnmatchedExpectedFailures()) { + report_for_entry(entry).unmatched.push_back( + {/*test_name=*/entry, /*failure_message=*/entries.at(entry).message}); + } + } + if (filtering_runner.has_value()) { + result.names_run = filtering_runner->NamesRun(); + } + + for (const internal::UnexpectedResult& success : + ledger.UnexpectedSuccesses()) { + report_for_entry(*success.matched_entry).successes.push_back(success); + } + // An unexpected failure has no matched entry (see UnexpectedResult), also + // when it was listed with a different message, so it is routed by name. + // A text-format test listed in a --failure_list file with a stale message + // therefore gets the --text_format_failure_list hint; the list it is in is + // unaffected either way, since SaveFailureList() replaces the entry in + // place. + for (const internal::UnexpectedResult& failure : + ledger.UnexpectedFailures()) { + reports[ReportIndexFor(reports, FailureListFlagFor(failure.test_name))] + .failures.push_back(failure); + } + for (const auto& listed_skip : ledger.ListedSkips()) { + report_for_entry(listed_skip.second).listed_skips.push_back(listed_skip); + } + + // One report block per failure list flag, like the legacy suites' own + // reports. + std::string output_dir = options.output_dir; + if (!output_dir.empty() && output_dir.back() != '/') { + output_dir.push_back('/'); + } + for (const FailureListReport& report : reports) { + if (!ReportFailureListResults(report, output_dir) && + result.exit_code == 0) { + result.exit_code = 1; + } + } + absl::FPrintF(stderr, + "CONFORMANCE GTEST SUITES %s: %d tests: %d successes, %d " + "skipped, %d expected failures, %d unexpected failures, " + "%d unexpected successes, %d listed skips\n\n", + result.exit_code == 0 ? "PASSED" : "FAILED", + unit_test->test_to_run_count(), ledger.expected_successes(), + ledger.skipped(), ledger.expected_failures(), + ledger.unexpected_failures(), ledger.unexpected_successes(), + ledger.listed_skips()); + return result; +} + +int Main(int argc, char* argv[]) { + // The runner's own parser knows which of its flags take a value, so it is + // the one that can tell the gtest flags (which it collects) from the + // testee's arguments (which may well start with --gtest_ too). + absl::StatusOr<ConformanceRunnerOptions> options = + ParseConformanceRunnerArgs(argc, argv); + if (!options.ok()) { + PrintConformanceRunnerUsage(options.status().message()); + return EXIT_FAILURE; + } + for (std::string& arg : options->gtest_args) { + arg = RewriteGtestFlagPrefix(arg, GTEST_FLAG_PREFIX_); + } + + // See the file comment: only the command line configures the gtest phase. + // (In google3, unsetenv() alone does the job because the flags' defaults are + // evaluated lazily; see ResetInheritedGtestFlags() for open source.) + for (const char* variable : kInheritedTestEnvironmentVariables) { +#ifdef _WIN32 + _putenv_s(variable, ""); // the MSVC CRT has no unsetenv() +#else + unsetenv(variable); +#endif + } + ResetInheritedGtestFlags(options->gtest_args); + + // In google3, InitGoogleTest() also runs InitGoogle(), which exits on any + // flag it doesn't know, so it only gets to see the gtest flags. (In open + // source it's plain gtest, which leaves unknown flags in place.) Whatever it + // leaves behind is an unknown option, as it was for the legacy runner. + std::vector<char*> gtest_argv = {argv[0]}; + gtest_argv.reserve(options->gtest_args.size() + 2); + for (std::string& arg : options->gtest_args) { + gtest_argv.push_back(arg.data()); + } + // Like a real argv, the vector must be null-terminated: InitGoogleTest() + // removes the flags it consumed by shifting the rest down and writing + // argv[argc] = nullptr. + gtest_argv.push_back(nullptr); + int gtest_argc = static_cast<int>(gtest_argv.size()) - 1; + testing::InitGoogleTest(>est_argc, gtest_argv.data()); + if (gtest_argc > 1) { + PrintConformanceRunnerUsage( + absl::StrCat("Unknown option: ", gtest_argv[1])); + return EXIT_FAILURE; + } + + SelectPerformanceTests(options->performance); + + if (GTEST_FLAG_GET(list_tests)) return RUN_ALL_TESTS(); + + // The environment variables gtest would default --gtest_output from are + // cleared above, so by now the flag is only set if the command line asked + // for a report. Should gtest have found a default some other way, don't let + // it write anywhere: InitGoogleTest() already installed the output listener + // for the default, so clearing the flag isn't enough, release the listener + // as well. Despite its name, default_xml_generator() is the listener for + // whichever format the flag selected, so this covers the JSON printer too. + if (!GtestOutputRequested(options->gtest_args)) { + GTEST_FLAG_SET(output, ""); + testing::TestEventListeners& listeners = + testing::UnitTest::GetInstance()->listeners(); + std::unique_ptr<testing::TestEventListener> released_listener( + listeners.Release(listeners.default_xml_generator())); + } + + std::ofstream record_requests_file; + if (!options->record_requests_file.empty()) { + record_requests_file.open(options->record_requests_file); + if (!record_requests_file.is_open()) { + absl::FPrintF(stderr, "Couldn't open record_requests file: %s\n", + options->record_requests_file); + return EXIT_FAILURE; + } + } + std::ostream* record_requests = + record_requests_file.is_open() ? &record_requests_file : nullptr; + + absl::StatusOr<GtestPhaseResult> gtest_result = + RunGtestPhase(*options, record_requests); + if (!gtest_result.ok()) { + absl::FPrintF(stderr, "%s\n", gtest_result.status().message()); + return EXIT_FAILURE; + } + + // Like the legacy runner, list the tests --test named that don't exist, + // without failing the run. + absl::flat_hash_set<std::string> names_not_run = options->names_to_test; + for (const std::string& name : gtest_result->names_run) { + names_not_run.erase(name); + } + if (!names_not_run.empty()) { + // Sorted: flat_hash_set iteration order varies from run to run. + std::vector<std::string> sorted(names_not_run.begin(), names_not_run.end()); + std::sort(sorted.begin(), sorted.end()); + absl::FPrintF(stderr, + "These tests were requested to be run isolated, but they do " + "not exist. Revise the test names:\n\n"); + for (const std::string& test_name : sorted) { + absl::FPrintF(stderr, " %s\n", test_name); + } + absl::FPrintF(stderr, "\n\n"); + } + + if (record_requests_file.is_open()) { + record_requests_file.close(); + if (!record_requests_file) { + absl::FPrintF(stderr, "Error writing record_requests file: %s\n", + options->record_requests_file); + return EXIT_FAILURE; + } + } + + return gtest_result->exit_code == 0 ? EXIT_SUCCESS : EXIT_FAILURE; +} + +} // namespace +} // namespace conformance +} // namespace protobuf +} // namespace google + +int main(int argc, char* argv[]) { + return google::protobuf::conformance::Main(argc, argv); }
diff --git a/conformance/conformance_test_runner.cc b/conformance/conformance_test_runner.cc index f4ef9af..a328d4e 100644 --- a/conformance/conformance_test_runner.cc +++ b/conformance/conformance_test_runner.cc
@@ -5,99 +5,75 @@ // license that can be found in the LICENSE file or at // https://developers.google.com/open-source/licenses/bsd -// This file contains a program for running the test suite in a separate -// process. The other alternative is to run the suite in-process. See -// conformance.proto for pros/cons of these two options. -// -// This program will fork the process under test and communicate with it over -// its stdin/stdout: -// -// +--------+ pipe +----------+ -// | tester | <------> | testee | -// | | | | -// | C++ | | any lang | -// +--------+ +----------+ -// -// The tester contains all of the test cases and their expected output. -// The testee is a simple program written in the target language that reads -// each test case and attempts to produce acceptable output for it. -// -// Every test consists of a ConformanceRequest/ConformanceResponse -// request/reply pair. The protocol on the pipe is simply: -// -// 1. tester sends 4-byte length N (little endian) -// 2. tester sends N bytes representing a ConformanceRequest proto -// 3. testee sends 4-byte length M (little endian) -// 4. testee sends M bytes representing a ConformanceResponse proto +// The command line parser and the report writer of conformance_test_runner; +// see conformance_test_runner.h and, for the runner itself, +// conformance_test_main.cc. -#include <stdio.h> +#include "conformance/conformance_test_runner.h" -#include <algorithm> -#include <cctype> #include <cstdio> -#include <cstdlib> #include <cstring> #include <fstream> #include <string> #include <vector> -#include "absl/container/flat_hash_set.h" -#include "absl/strings/ascii.h" +#include "google/protobuf/descriptor.pb.h" +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "absl/strings/match.h" #include "absl/strings/str_cat.h" -#include "absl/types/optional.h" -#include "conformance/conformance.pb.h" -#include "conformance/conformance_test.h" -#include "conformance/fork_pipe_runner.h" -#include "conformance/recording_test_runner.h" -#include "conformance/test_runner.h" +#include "absl/strings/str_format.h" +#include "absl/strings/str_join.h" +#include "absl/strings/str_split.h" +#include "absl/strings/string_view.h" +#include "absl/types/span.h" namespace google { namespace protobuf { namespace conformance { -namespace { -void ParseFailureList(const char* filename, - ::conformance::FailureSet* failure_list) { - std::ifstream infile(filename); - - if (!infile.is_open()) { - fprintf(stderr, "Couldn't open failure list file: %s\n", filename); - exit(1); - } - - for (std::string line; std::getline(infile, line);) { - // Remove comments. - std::string test_name = line.substr(0, line.find('#')); - - test_name.erase( - std::remove_if(test_name.begin(), test_name.end(), ::isspace), - test_name.end()); - - if (test_name.empty()) { // Skip empty lines. - continue; - } - - // If we remove whitespace from the beginning of a line, and what we have - // left at first is a '#', then we have a comment. - if (test_name[0] != '#') { - // Find our failure message if it exists. Will be set to an empty string - // if no message is found. Empty failure messages also pass our tests. - size_t check_message = line.find('#'); - std::string message; - if (check_message != std::string::npos) { - message = line.substr(check_message + 1); // +1 to skip the delimiter - // If we had only whitespace after the delimiter, we will have an empty - // failure message and the test will still pass. - message = std::string(absl::StripAsciiWhitespace(message)); - } - ::conformance::TestStatus* test = failure_list->add_test(); - test->set_name(test_name); - test->set_failure_message(message); - } - } +std::string RewriteGtestFlagPrefix(absl::string_view arg, + absl::string_view flag_prefix) { + constexpr absl::string_view kGtestPrefix = "--gtest_"; + if (!absl::StartsWith(arg, kGtestPrefix)) return std::string(arg); + return absl::StrCat("--", flag_prefix, arg.substr(kGtestPrefix.size())); } -void UsageError() { +bool IsPerformanceFixture(absl::string_view test_suite_name) { + for (absl::string_view component : absl::StrSplit(test_suite_name, '/')) { + if (absl::EndsWith(component, "PerformanceTest")) return true; + } + return false; +} + +std::string PerformanceGtestFilter( + absl::string_view filter, bool performance, + absl::Span<const std::string> test_suite_names) { + std::vector<std::string> excluded; + for (const std::string& test_suite_name : test_suite_names) { + if (IsPerformanceFixture(test_suite_name) != performance) { + excluded.push_back(absl::StrCat(test_suite_name, ".*")); + } + } + if (excluded.empty()) return std::string(filter); + + // As gtest reads a filter: everything before the first '-' is the positive + // patterns (none means "*"), everything after it the negative ones. + absl::string_view positive = filter; + absl::string_view negative; + if (size_t dash = filter.find('-'); dash != absl::string_view::npos) { + positive = filter.substr(0, dash); + negative = filter.substr(dash + 1); + } + if (positive.empty()) positive = "*"; + return absl::StrCat(positive, "-", negative, negative.empty() ? "" : ":", + absl::StrJoin(excluded, ":")); +} + +void PrintConformanceRunnerUsage(absl::string_view error_message) { + if (!error_message.empty()) { + absl::FPrintF(stderr, "%s\n", error_message); + } fprintf(stderr, "Usage: conformance-test-runner [options] <test-program>\n"); fprintf(stderr, "\n"); fprintf(stderr, "Options:\n"); @@ -115,7 +91,7 @@ " of tests that are expected to \n"); fprintf(stderr, " fail in the \n"); fprintf(stderr, - " text_format_conformance_suite. \n"); + " text-format conformance tests. \n"); fprintf(stderr, " File should contain one test name \n"); fprintf(stderr, @@ -144,14 +120,17 @@ " can be specified by repeating the \n" " flag.\n\n"); fprintf(stderr, - " --debug Enable debug mode\n" - " to produce octal serialized\n" - " ConformanceRequest for the tests\n" - " passed to --test (required)\n\n"); + " --debug Accepted for compatibility; has\n" + " no effect. Requires --test, as\n" + " it always did.\n\n"); fprintf(stderr, " --performance Boolean option\n"); fprintf(stderr, " for enabling run of\n"); fprintf(stderr, " performance tests.\n\n"); fprintf(stderr, + " --verbose Accepted for compatibility; has\n" + " no effect (use --gtest_brief=0 to\n" + " list every test's result).\n\n"); + fprintf(stderr, " --record_requests <file> Write one line per request sent\n" " to the testee, of the form\n" " '<test_name> <input_size> " @@ -159,158 +138,121 @@ " CRC32C is of the request's\n" " canonical rendering, for\n" " verifying that refactorings of\n" - " the suites preserve behavior.\n"); - exit(1); + " the suites preserve behavior.\n\n"); + fprintf(stderr, + " --gtest_<flag>=<value> Passed on to the gtest-based\n" + " suites (e.g. --gtest_filter,\n" + " --gtest_output); --gunit_ is\n" + " accepted too. Must precede the\n" + " testee and use the --flag=value\n" + " form.\n"); } -} // namespace - -int RunConformanceTests(int argc, char *argv[], - const std::vector<ConformanceTestSuite *> &suites) { - if (suites.empty()) { - fprintf(stderr, "No test suites found.\n"); - return EXIT_FAILURE; - } - - std::string program; - std::string testee; - std::vector<std::string> program_args; - bool performance = false; +absl::StatusOr<ConformanceRunnerOptions> ParseConformanceRunnerArgs( + int argc, char* argv[]) { + ConformanceRunnerOptions options; bool debug = false; - absl::flat_hash_set<std::string> names_to_test; - bool enforce_recommended = false; - Edition maximum_edition = EDITION_UNKNOWN; - std::string output_dir; - std::string record_requests_filename; - bool verbose = false; - bool isolated = false; + + // The error for a flag at `arg` that takes a value but is the last argument. + auto missing_value = [&](int arg) { + return absl::InvalidArgumentError( + absl::StrCat("Missing value for ", argv[arg])); + }; for (int arg = 1; arg < argc; ++arg) { if (strcmp(argv[arg], "--performance") == 0) { - performance = true; + options.performance = true; } else if (strcmp(argv[arg], "--debug") == 0) { + // No effect (see ConformanceRunnerOptions); only its "requires --test" + // check below survives. debug = true; } else if (strcmp(argv[arg], "--verbose") == 0) { - verbose = true; + // No effect (see ConformanceRunnerOptions). } else if (strcmp(argv[arg], "--enforce_recommended") == 0) { - enforce_recommended = true; + options.enforce_recommended = true; } else if (strcmp(argv[arg], "--maximum_edition") == 0) { - if (++arg == argc) UsageError(); + if (arg + 1 == argc) return missing_value(arg); + ++arg; Edition edition = EDITION_UNKNOWN; if (!Edition_Parse(absl::StrCat("EDITION_", argv[arg]), &edition)) { - fprintf(stderr, "Unknown edition: %s\n", argv[arg]); - UsageError(); + return absl::InvalidArgumentError( + absl::StrCat("Unknown edition: ", argv[arg])); } - maximum_edition = edition; + options.maximum_edition = edition; } else if (strcmp(argv[arg], "--output_dir") == 0) { - if (++arg == argc) UsageError(); - output_dir = argv[arg]; + if (arg + 1 == argc) return missing_value(arg); + options.output_dir = argv[++arg]; } else if (strcmp(argv[arg], "--record_requests") == 0) { - if (++arg == argc) UsageError(); - record_requests_filename = argv[arg]; + if (arg + 1 == argc) return missing_value(arg); + options.record_requests_file = argv[++arg]; } else if (strcmp(argv[arg], "--test") == 0) { - if (++arg == argc) UsageError(); - names_to_test.insert(argv[arg]); + if (arg + 1 == argc) return missing_value(arg); + options.names_to_test.insert(argv[++arg]); + + } else if (argv[arg] == kFailureListFlag || + argv[arg] == kTextFormatFailureListFlag) { + if (arg + 1 == argc) return missing_value(arg); + options.failure_list_files.emplace_back(argv[arg], argv[arg + 1]); + ++arg; + + } else if (absl::StartsWith(argv[arg], "--gtest_") || + absl::StartsWith(argv[arg], "--gunit_")) { + options.gtest_args.push_back(argv[arg]); } else if (argv[arg][0] == '-') { - bool recognized_flag = false; - for (ConformanceTestSuite *suite : suites) { - if (strcmp(argv[arg], suite->GetFailureListFlagName().c_str()) == 0) { - if (++arg == argc) UsageError(); - recognized_flag = true; - } - } - if (!recognized_flag) { - fprintf(stderr, "Unknown option: %s\n", argv[arg]); - UsageError(); - } + return absl::InvalidArgumentError( + absl::StrCat("Unknown option: ", argv[arg])); } else { - program += argv[arg++]; + options.testee = argv[arg++]; while (arg < argc) { - program_args.push_back(argv[arg]); + options.testee_args.push_back(argv[arg]); arg++; } } } - if (debug && names_to_test.empty()) { - UsageError(); + if (debug && options.names_to_test.empty()) { + return absl::InvalidArgumentError("--debug requires --test"); } + options.isolated = !options.names_to_test.empty(); - if (!names_to_test.empty()) { - isolated = true; + return options; +} + +bool ReportTestStatusSet(absl::Span<const ReportedTestStatus> statuses, + absl::string_view file_name, absl::string_view message, + absl::string_view output_dir, std::string& output) { + if (statuses.empty()) return true; + + absl::StrAppend(&output, "\n", message, "\n\n"); + for (const ReportedTestStatus& status : statuses) { + absl::StrAppendFormat(&output, " %s # %s\n", status.test_name, + status.failure_message); } + absl::StrAppend(&output, "\n"); - // Opened once (truncating) so that requests from all suites are appended in - // order to a single file. - std::ofstream record_requests_file; - if (!record_requests_filename.empty()) { - record_requests_file.open(record_requests_filename); - if (!record_requests_file.is_open()) { - fprintf(stderr, "Couldn't open record_requests file: %s\n", - record_requests_filename.c_str()); - return EXIT_FAILURE; - } - } - - bool all_ok = true; - for (ConformanceTestSuite *suite : suites) { - std::string failure_list_filename; - ::conformance::FailureSet failure_list; - for (int arg = 1; arg < argc; ++arg) { - if (strcmp(argv[arg], suite->GetFailureListFlagName().c_str()) == 0) { - if (++arg == argc) UsageError(); - failure_list_filename = argv[arg]; - ParseFailureList(argv[arg], &failure_list); + if (!file_name.empty()) { + const std::string filename = absl::StrCat(output_dir, file_name); + std::ofstream os(filename); + if (os) { + for (const ReportedTestStatus& status : statuses) { + // Additions will not have a 'matched_name' while removals will. + absl::string_view potential_add_or_removal = status.matched_name.empty() + ? status.test_name + : status.matched_name; + os << potential_add_or_removal << " # " << status.failure_message + << "\n"; } - } - suite->SetPerformance(performance); - suite->SetVerbose(verbose); - suite->SetEnforceRecommended(enforce_recommended); - suite->SetMaximumEdition(maximum_edition); - suite->SetOutputDir(output_dir); - suite->SetDebug(debug); - suite->SetNamesToTest(names_to_test); - suite->SetTestee(program); - suite->SetIsolated(isolated); - - ForkPipeRunner fork_pipe_runner(program, program_args); - ConformanceTestRunner* runner = &fork_pipe_runner; - absl::optional<RecordingTestRunner> recording_runner; - if (record_requests_file.is_open()) { - recording_runner.emplace(&fork_pipe_runner, &record_requests_file); - runner = &*recording_runner; - } - - std::string output; - all_ok = all_ok && suite->RunSuite(runner, &output, failure_list_filename, - &failure_list); - - names_to_test = suite->GetExpectedTestsNotRun(); - fwrite(output.c_str(), 1, output.size(), stderr); - } - - if (record_requests_file.is_open()) { - record_requests_file.close(); - if (!record_requests_file) { - fprintf(stderr, "Error writing record_requests file: %s\n", - record_requests_filename.c_str()); - return EXIT_FAILURE; + } else { + absl::StrAppendFormat(&output, + "Failed to open file: %s\n", + filename); } } - if (!names_to_test.empty()) { - fprintf(stderr, - "These tests were requested to be ran isolated, but they do " - "not exist. Revise the test names:\n\n"); - for (const std::string &test_name : names_to_test) { - fprintf(stderr, " %s\n", test_name.c_str()); - } - fprintf(stderr, "\n\n"); - } - return all_ok ? EXIT_SUCCESS : EXIT_FAILURE; + return false; } } // namespace conformance
diff --git a/conformance/conformance_test_runner.h b/conformance/conformance_test_runner.h new file mode 100644 index 0000000..f1ff657 --- /dev/null +++ b/conformance/conformance_test_runner.h
@@ -0,0 +1,160 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2026 Google LLC. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +// The command line of conformance_test_runner and the report format its users +// rely on. Those users are the conformance test targets, their +// update_failure_list tooling and the scripts around them. The runner itself +// is conformance_test_main.cc. It runs the gtest-based conformance suites +// (see test_fixture.h) against the testee named on the command line. See +// PrintConformanceRunnerUsage() for the options. +// +// The command line and the report format are those of the legacy +// conformance_test_runner, which ran hand-written ConformanceTestSuite classes +// instead of gtest suites. They are kept so that nothing changes for its +// users. +// TODO: retire the failure list flags below together with the +// per-flag report blocks of conformance_test_main.cc once the remaining sh_test +// and script invocations have moved to the conformance_test() macro, which runs +// the gtest suites as ordinary test binaries. + +#ifndef GOOGLE_PROTOBUF_CONFORMANCE_CONFORMANCE_TEST_RUNNER_H__ +#define GOOGLE_PROTOBUF_CONFORMANCE_CONFORMANCE_TEST_RUNNER_H__ + +#include <string> +#include <utility> +#include <vector> + +#include "google/protobuf/descriptor.pb.h" +#include "absl/container/flat_hash_set.h" +#include "absl/status/statusor.h" +#include "absl/strings/string_view.h" +#include "absl/types/span.h" + +namespace google { +namespace protobuf { +namespace conformance { + +// The failure list flags the runner accepts. Every file given for either is +// loaded into the gtest suites' one ResultLedger. The files are recorded under +// their flag (see ConformanceRunnerOptions::failure_list_files) so that the +// runner can tell which list an entry came from and report per list. +// --failure_list is the flag of the binary and JSON tests. +// --text_format_failure_list is the one of the text-format tests. The test +// targets and scripts pass both. +inline constexpr absl::string_view kFailureListFlag = "--failure_list"; +inline constexpr absl::string_view kTextFormatFailureListFlag = + "--text_format_failure_list"; + +// The options accepted on the conformance_test_runner command line. +struct ConformanceRunnerOptions { + // The first non-flag argument and everything after it. + std::string testee; + std::vector<std::string> testee_args; + + // Every failure list flag given, as (flag name, file) pairs in command line + // order, e.g. ("--failure_list", "failure_list_cpp.txt"). + std::vector<std::pair<std::string, std::string>> failure_list_files; + + bool performance = false; + bool enforce_recommended = false; + // EDITION_UNKNOWN (the default) runs the proto2 and proto3 tests only. + Edition maximum_edition = EDITION_UNKNOWN; + std::string output_dir; + // Where --record_requests should write, if given. + std::string record_requests_file; + // The tests named with --test, and whether to run only those (set by + // ParseConformanceRunnerArgs() when --test was given). + absl::flat_hash_set<std::string> names_to_test; + bool isolated = false; + + // Arguments starting with --gtest_ or --gunit_ that appeared before the + // testee, in order and as given. conformance_test_main.cc passes them on to + // gtest, after RewriteGtestFlagPrefix(). + std::vector<std::string> gtest_args; + + // --debug and --verbose are accepted too, for the scripts that still pass + // them, but have no effect. They only ever configured the legacy runner's + // suites. gtest's own flags take their place. For example, --gtest_brief=0 + // lists every test's result. +}; + +// Parses the conformance_test_runner command line. On error, returns +// InvalidArgumentError with the message the runner prints before its usage +// text; see PrintConformanceRunnerUsage(). +absl::StatusOr<ConformanceRunnerOptions> ParseConformanceRunnerArgs( + int argc, char* argv[]); + +// Returns `arg` with a leading "--gtest_" replaced by "--" + `flag_prefix`. +// Any other argument, including one already spelled with `flag_prefix`, is +// returned unchanged. conformance_test_main.cc applies this to +// ConformanceRunnerOptions::gtest_args with the prefix its gtest was built +// with, so that the documented --gtest_ spelling works everywhere. That +// prefix is GTEST_FLAG_PREFIX_ from gtest-port.h: "gtest_" in open source and +// "gunit_" in google3, whose gtest rejects the --gtest_ spelling. +// GTEST_FLAG_PREFIX_ is one of gtest's internal macros, but it is what gtest +// itself parses its flags by. merged_runner_test.sh checks end to end that a +// --gtest_ flag reaches gtest. +std::string RewriteGtestFlagPrefix(absl::string_view arg, + absl::string_view flag_prefix); + +// Returns whether the gtest test suite (fixture) named `test_suite_name` +// belongs to the performance conformance suite, that is, whether its fixture +// is named `*PerformanceTest` (the naming rule in test_fixture.h). gtest +// names an instantiated fixture `<instantiation>/<fixture>` for a value +// parameterized one, `<fixture>/<index or type name>` for a typed one, and +// `<instantiation>/<fixture>/<index or type name>` for a type parameterized +// one, so every '/'-separated component is checked. +bool IsPerformanceFixture(absl::string_view test_suite_name); + +// Returns the gtest filter that narrows `filter` to the test suites of the +// kind `performance` selects. The filter has the form +// `positive patterns[-negative patterns]`, each list ':'-separated, see +// gtest's --gtest_filter. `test_suite_names` are the names of every +// registered gtest test suite. With `performance`, the performance fixtures +// among them are selected (see IsPerformanceFixture()), otherwise all the +// others. The suites of the other kind are appended to the negative patterns +// as `<test suite name>.*`, so `filter` still applies within the selected +// kind. `filter` is typically an explicit --gtest_filter, or "*". This is +// how the runner (conformance_test_main.cc) implements --performance. +std::string PerformanceGtestFilter( + absl::string_view filter, bool performance, + absl::Span<const std::string> test_suite_names); + +// Prints `error_message` (if non-empty) followed by the usage text to stderr. +void PrintConformanceRunnerUsage(absl::string_view error_message); + +// One entry of a report written by ReportTestStatusSet(). +struct ReportedTestStatus { + // The full name of the test. + std::string test_name; + // The failure message shown next to the test: the test's own for a failure, + // the failure list entry's for an unexpected success. + std::string failure_message; + // The failure list entry (possibly a wildcard) the test matched, when the + // report is about removing that entry from the list. Empty when it is about + // adding `test_name` to the list. + std::string matched_name; +}; + +// Reports one category of test statuses in the runner's report format. Does +// nothing and returns true if `statuses` is empty. Otherwise appends +// `message` followed by one " <test name> # <failure message>" line per +// status to `output` and returns false. If `file_name` is non-empty, it also +// writes one "<name> # <failure message>" line per status to the file +// `output_dir` + `file_name`, in the form update_failure_list expects. There +// <name> is the status's matched_name if set (an entry to remove from the +// failure list) and its test name otherwise (an entry to add). A file that +// cannot be opened is reported in `output` too. +bool ReportTestStatusSet(absl::Span<const ReportedTestStatus> statuses, + absl::string_view file_name, absl::string_view message, + absl::string_view output_dir, std::string& output); + +} // namespace conformance +} // namespace protobuf +} // namespace google + +#endif // GOOGLE_PROTOBUF_CONFORMANCE_CONFORMANCE_TEST_RUNNER_H__
diff --git a/conformance/conformance_test_runner_test.cc b/conformance/conformance_test_runner_test.cc new file mode 100644 index 0000000..150b70f --- /dev/null +++ b/conformance/conformance_test_runner_test.cc
@@ -0,0 +1,516 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2026 Google LLC. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +// Tests of the conformance_test_runner command line parsing and of the report +// writer in conformance_test_runner.cc (see conformance_test_runner.h). + +#include "conformance/conformance_test_runner.h" + +#ifdef _WIN32 +#include <direct.h> // for _mkdir +#else +#include <sys/stat.h> // for mkdir +#endif + +#include <cerrno> +#include <fstream> +#include <iterator> +#include <string> +#include <vector> + +#include "google/protobuf/descriptor.pb.h" +#include <gmock/gmock.h> +#include <gtest/gtest.h> +#include "absl/log/absl_check.h" +#include "absl/status/status.h" +#include "absl/status/status_matchers.h" +#include "absl/status/statusor.h" +#include "absl/strings/match.h" +#include "absl/strings/str_cat.h" +#include "absl/strings/string_view.h" + +namespace google { +namespace protobuf { +namespace conformance { +namespace { + +using ::absl_testing::IsOk; +using ::absl_testing::StatusIs; +using ::testing::ElementsAre; +using ::testing::HasSubstr; +using ::testing::IsEmpty; +using ::testing::Not; +using ::testing::Pair; +using ::testing::UnorderedElementsAre; + +// Parses `args` (without argv[0], which is added). +absl::StatusOr<ConformanceRunnerOptions> Parse( + const std::vector<std::string>& args) { + std::vector<std::string> owned_args = {"conformance_test_runner"}; + owned_args.insert(owned_args.end(), args.begin(), args.end()); + std::vector<char*> argv; + for (std::string& arg : owned_args) argv.push_back(arg.data()); + argv.push_back(nullptr); + return ParseConformanceRunnerArgs(static_cast<int>(owned_args.size()), + argv.data()); +} + +// mkdir() and its MSVC CRT spelling. +int MakeDirectory(const std::string& dir) { +#ifdef _WIN32 + return _mkdir(dir.c_str()); +#else + return mkdir(dir.c_str(), 0755); +#endif +} + +// Returns a fresh directory for one test's output files, with a trailing '/'. +std::string NewOutputDir() { + const testing::TestInfo& test_info = + *testing::UnitTest::GetInstance()->current_test_info(); + std::string dir = + absl::StrCat(testing::TempDir(), test_info.test_suite_name(), "_", + test_info.name(), "/"); + ABSL_PCHECK(MakeDirectory(dir) == 0 || errno == EEXIST) << dir; + return dir; +} + +std::string ReadFile(absl::string_view path) { + std::ifstream in{std::string(path)}; + ABSL_CHECK(in.is_open()) << path; + return std::string(std::istreambuf_iterator<char>(in), {}); +} + +bool FileExists(absl::string_view path) { + return std::ifstream(std::string(path)).is_open(); +} + +TEST(ParseConformanceRunnerArgsTest, Defaults) { + absl::StatusOr<ConformanceRunnerOptions> options = Parse({"testee"}); + ASSERT_THAT(options, IsOk()); + + EXPECT_EQ(options->testee, "testee"); + EXPECT_THAT(options->testee_args, IsEmpty()); + EXPECT_THAT(options->failure_list_files, IsEmpty()); + EXPECT_FALSE(options->performance); + EXPECT_FALSE(options->enforce_recommended); + EXPECT_EQ(options->maximum_edition, EDITION_UNKNOWN); + EXPECT_THAT(options->output_dir, IsEmpty()); + EXPECT_THAT(options->record_requests_file, IsEmpty()); + EXPECT_THAT(options->names_to_test, IsEmpty()); + EXPECT_FALSE(options->isolated); + EXPECT_THAT(options->gtest_args, IsEmpty()); +} + +TEST(ParseConformanceRunnerArgsTest, NoTesteeIsAccepted) { + // The legacy runner never checked for one; the testee fails to spawn later. + absl::StatusOr<ConformanceRunnerOptions> options = Parse({}); + ASSERT_THAT(options, IsOk()); + EXPECT_THAT(options->testee, IsEmpty()); +} + +TEST(ParseConformanceRunnerArgsTest, ParsesEveryFlag) { + absl::StatusOr<ConformanceRunnerOptions> options = + Parse({"--performance", "--debug", "--verbose", "--enforce_recommended", + "--maximum_edition", "2023", "--output_dir", "out", + "--record_requests", "requests.txt", "--test", "Some.Test", + "--failure_list", "list.txt", "testee"}); + ASSERT_THAT(options, IsOk()); + + EXPECT_TRUE(options->performance); + EXPECT_TRUE(options->enforce_recommended); + EXPECT_EQ(options->maximum_edition, EDITION_2023); + EXPECT_EQ(options->output_dir, "out"); + EXPECT_EQ(options->record_requests_file, "requests.txt"); + EXPECT_THAT(options->names_to_test, ElementsAre("Some.Test")); + EXPECT_THAT(options->failure_list_files, + ElementsAre(Pair("--failure_list", "list.txt"))); + EXPECT_EQ(options->testee, "testee"); +} + +TEST(ParseConformanceRunnerArgsTest, EverythingAfterTesteeGoesToTestee) { + absl::StatusOr<ConformanceRunnerOptions> options = + Parse({"--verbose", "testee", "--verbose", "--failure_list", "x", + "--gtest_filter=*", "plain", "--"}); + ASSERT_THAT(options, IsOk()); + + EXPECT_EQ(options->testee, "testee"); + EXPECT_THAT(options->testee_args, + ElementsAre("--verbose", "--failure_list", "x", + "--gtest_filter=*", "plain", "--")); + EXPECT_THAT(options->gtest_args, IsEmpty()); + EXPECT_THAT(options->failure_list_files, IsEmpty()); +} + +TEST(ParseConformanceRunnerArgsTest, GtestFlagsBeforeTesteeAreCollected) { + absl::StatusOr<ConformanceRunnerOptions> options = + Parse({"--gtest_filter=Foo.*", "--verbose", "--gunit_brief=1", + "--gtest_list_tests", "testee", "--gtest_after"}); + ASSERT_THAT(options, IsOk()); + + EXPECT_THAT(options->gtest_args, + ElementsAre("--gtest_filter=Foo.*", "--gunit_brief=1", + "--gtest_list_tests")); + EXPECT_THAT(options->testee_args, ElementsAre("--gtest_after")); +} + +// --debug and --verbose only ever configured the legacy suites; they are still +// accepted (conformance_test_wrapper.sh passes --debug on) and change nothing. +TEST(ParseConformanceRunnerArgsTest, NoEffectFlagsAreAccepted) { + absl::StatusOr<ConformanceRunnerOptions> with_flags = + Parse({"--verbose", "--debug", "--test", "Some.Test", "testee"}); + absl::StatusOr<ConformanceRunnerOptions> without_flags = + Parse({"--test", "Some.Test", "testee"}); + ASSERT_THAT(with_flags, IsOk()); + ASSERT_THAT(without_flags, IsOk()); + + EXPECT_EQ(with_flags->testee, without_flags->testee); + EXPECT_EQ(with_flags->names_to_test, without_flags->names_to_test); + EXPECT_EQ(with_flags->isolated, without_flags->isolated); + EXPECT_EQ(with_flags->performance, without_flags->performance); + EXPECT_EQ(with_flags->enforce_recommended, + without_flags->enforce_recommended); + EXPECT_EQ(with_flags->gtest_args, without_flags->gtest_args); +} + +TEST(ParseConformanceRunnerArgsTest, DoubleDashIsAnUnknownOption) { + // Pins the legacy behavior: there is no way to end the runner's own flags + // other than naming the testee. + EXPECT_THAT(Parse({"--", "testee"}), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Unknown option: --"))); +} + +TEST(ParseConformanceRunnerArgsTest, UnknownOption) { + EXPECT_THAT(Parse({"--bogus", "testee"}), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Unknown option: --bogus"))); + EXPECT_THAT(Parse({"-v", "testee"}), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Unknown option: -v"))); +} + +class MissingValueTest : public testing::TestWithParam<std::string> {}; + +TEST_P(MissingValueTest, LastArgumentIsAValueFlag) { + EXPECT_THAT(Parse({GetParam()}), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Missing value for " + GetParam()))) + << GetParam(); + EXPECT_THAT(Parse({"--verbose", GetParam()}), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Missing value for " + GetParam()))) + << GetParam(); + // After the testee it is just an argument for the testee. + absl::StatusOr<ConformanceRunnerOptions> options = + Parse({"testee", GetParam()}); + ASSERT_THAT(options, IsOk()) << GetParam(); + EXPECT_THAT(options->testee_args, ElementsAre(GetParam())); +} + +INSTANTIATE_TEST_SUITE_P(ValueFlags, MissingValueTest, + testing::Values("--maximum_edition", "--output_dir", + "--record_requests", "--test", + "--failure_list", + "--text_format_failure_list")); + +TEST(ParseConformanceRunnerArgsTest, DebugRequiresTest) { + EXPECT_THAT(Parse({"--debug", "testee"}), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("--debug requires --test"))); + EXPECT_THAT(Parse({"--debug", "--test", "Some.Test", "testee"}), IsOk()); +} + +TEST(ParseConformanceRunnerArgsTest, MaximumEdition) { + EXPECT_THAT(Parse({"--maximum_edition", "bogus", "testee"}), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Unknown edition: bogus"))); + // The legacy runner prepends "EDITION_", so only the short form works. + EXPECT_THAT(Parse({"--maximum_edition", "EDITION_2023", "testee"}), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Unknown edition: EDITION_2023"))); + + absl::StatusOr<ConformanceRunnerOptions> options = + Parse({"--maximum_edition", "PROTO3", "testee"}); + ASSERT_THAT(options, IsOk()); + EXPECT_EQ(options->maximum_edition, EDITION_PROTO3); +} + +// Both failure list flags are recorded under their own name, in command line +// order. +TEST(ParseConformanceRunnerArgsTest, FailureListFlagsAreRecordedByName) { + absl::StatusOr<ConformanceRunnerOptions> options = + Parse({"--text_format_failure_list", "t.txt", "--failure_list", "f.txt", + "testee"}); + ASSERT_THAT(options, IsOk()); + EXPECT_THAT(options->failure_list_files, + ElementsAre(Pair("--text_format_failure_list", "t.txt"), + Pair("--failure_list", "f.txt"))); +} + +TEST(ParseConformanceRunnerArgsTest, FailureListFlagsKeepCommandLineOrder) { + absl::StatusOr<ConformanceRunnerOptions> options = + Parse({"--text_format_failure_list", "t1.txt", "--failure_list", "b1.txt", + "--text_format_failure_list", "t2.txt", "--failure_list", "b2.txt", + "testee"}); + ASSERT_THAT(options, IsOk()); + + EXPECT_THAT(options->failure_list_files, + ElementsAre(Pair("--text_format_failure_list", "t1.txt"), + Pair("--failure_list", "b1.txt"), + Pair("--text_format_failure_list", "t2.txt"), + Pair("--failure_list", "b2.txt"))); +} + +TEST(ParseConformanceRunnerArgsTest, IsolatedFollowsFromTest) { + absl::StatusOr<ConformanceRunnerOptions> options = Parse( + {"--test", "Test.B", "--test", "Test.A", "--test", "Test.B", "testee"}); + ASSERT_THAT(options, IsOk()); + EXPECT_TRUE(options->isolated); + EXPECT_THAT(options->names_to_test, UnorderedElementsAre("Test.A", "Test.B")); + + options = Parse({"testee"}); + ASSERT_THAT(options, IsOk()); + EXPECT_FALSE(options->isolated); +} + +TEST(RewriteGtestFlagPrefixTest, RewritesLeadingGtestPrefixOnly) { + EXPECT_EQ(RewriteGtestFlagPrefix("--gtest_filter=Foo.*", "gunit_"), + "--gunit_filter=Foo.*"); + EXPECT_EQ(RewriteGtestFlagPrefix("--gtest_list_tests", "gunit_"), + "--gunit_list_tests"); + // A no-op when the prefix already is gtest's own, as in open source. + EXPECT_EQ(RewriteGtestFlagPrefix("--gtest_filter=Foo.*", "gtest_"), + "--gtest_filter=Foo.*"); + // Already spelled with the target prefix, or not a gtest flag at all. + EXPECT_EQ(RewriteGtestFlagPrefix("--gunit_filter=Foo.*", "gunit_"), + "--gunit_filter=Foo.*"); + EXPECT_EQ(RewriteGtestFlagPrefix("--verbose", "gunit_"), "--verbose"); + EXPECT_EQ(RewriteGtestFlagPrefix("gtest_filter=x", "gunit_"), + "gtest_filter=x"); + EXPECT_EQ(RewriteGtestFlagPrefix("--gtest_", "gunit_"), "--gunit_"); + EXPECT_EQ(RewriteGtestFlagPrefix("", "gunit_"), ""); +} + +TEST(IsPerformanceFixtureTest, PlainFixture) { + EXPECT_TRUE(IsPerformanceFixture("BinaryPerformanceTest")); + EXPECT_TRUE(IsPerformanceFixture("PerformanceTest")); + EXPECT_FALSE(IsPerformanceFixture("BinaryTest")); + EXPECT_FALSE(IsPerformanceFixture("PerformanceTestOfSomething")); + EXPECT_FALSE(IsPerformanceFixture("BinaryPerformance")); + EXPECT_FALSE(IsPerformanceFixture("")); +} + +TEST(IsPerformanceFixtureTest, InstantiatedFixtures) { + // Value-parameterized: <instantiation>/<fixture>. + EXPECT_TRUE(IsPerformanceFixture("Proto3/UnknownFieldsPerformanceTest")); + EXPECT_FALSE(IsPerformanceFixture("Proto3/UnknownFieldsTest")); + // Typed: <fixture>/<index or type name>. + EXPECT_TRUE(IsPerformanceFixture("DepthLimitPerformanceTest/0")); + EXPECT_TRUE(IsPerformanceFixture("DepthLimitPerformanceTest/TestAllTypes")); + EXPECT_FALSE(IsPerformanceFixture("DepthLimitTest/0")); + // Type-parameterized: <instantiation>/<fixture>/<index or type name>. + EXPECT_TRUE(IsPerformanceFixture("Editions/DepthLimitPerformanceTest/1")); + EXPECT_FALSE(IsPerformanceFixture("Editions/DepthLimitTest/1")); + // A two-component name does not tell whether it is <instantiation>/<fixture> + // or <fixture>/<type name>, so every component counts: an instantiation or + // type name ending in the suffix marks the suite too (nothing else is + // expected to be named that way), while the suffix inside a component or + // in the test name does not. + EXPECT_TRUE(IsPerformanceFixture("PerformanceTest/DepthLimitTest")); + EXPECT_TRUE(IsPerformanceFixture("Inst/DepthLimitTest/PerformanceTest")); + EXPECT_FALSE(IsPerformanceFixture("Inst/DepthLimitTest/PerformanceTestType")); +} + +// Fixtures of the performance kind in every form gtest registers a fixture +// in, so that the test below can check the names gtest reports for them (the +// input of IsPerformanceFixture() in the merged runner) against real gtest +// rather than against this file's reading of gtest's documentation. +class ProbePerformanceTest : public testing::Test {}; +TEST_F(ProbePerformanceTest, IsRegistered) {} + +class ProbeParameterizedPerformanceTest : public testing::TestWithParam<int> {}; +TEST_P(ProbeParameterizedPerformanceTest, IsRegistered) {} +INSTANTIATE_TEST_SUITE_P(Probe, ProbeParameterizedPerformanceTest, + testing::Values(0)); + +template <typename T> +class ProbeTypedPerformanceTest : public testing::Test {}; +TYPED_TEST_SUITE(ProbeTypedPerformanceTest, testing::Types<int>); +TYPED_TEST(ProbeTypedPerformanceTest, IsRegistered) {} + +template <typename T> +class ProbeTypeParameterizedPerformanceTest : public testing::Test {}; +TYPED_TEST_SUITE_P(ProbeTypeParameterizedPerformanceTest); +TYPED_TEST_P(ProbeTypeParameterizedPerformanceTest, IsRegistered) {} +REGISTER_TYPED_TEST_SUITE_P(ProbeTypeParameterizedPerformanceTest, + IsRegistered); +INSTANTIATE_TYPED_TEST_SUITE_P(Probe, ProbeTypeParameterizedPerformanceTest, + testing::Types<int>); + +TEST(IsPerformanceFixtureTest, RecognizesEveryFormGtestRegisters) { + const testing::UnitTest& unit_test = *testing::UnitTest::GetInstance(); + std::vector<std::string> probes; + for (int i = 0; i < unit_test.total_test_suite_count(); ++i) { + const std::string name = unit_test.GetTestSuite(i)->name(); + if (absl::StrContains(name, "Probe")) probes.push_back(name); + } + EXPECT_THAT(probes, UnorderedElementsAre( + "ProbePerformanceTest", + "Probe/ProbeParameterizedPerformanceTest", + "ProbeTypedPerformanceTest/0", + "Probe/ProbeTypeParameterizedPerformanceTest/0")); + for (const std::string& name : probes) { + EXPECT_TRUE(IsPerformanceFixture(name)) << name; + } + // The fixtures of this file's other tests are of the regular kind. + EXPECT_FALSE(IsPerformanceFixture("IsPerformanceFixtureTest")); + EXPECT_FALSE(IsPerformanceFixture("ValueFlags/MissingValueTest")); +} + +TEST(PerformanceGtestFilterTest, ExcludesTheOtherKind) { + const std::vector<std::string> suites = { + "BinaryTest", "Proto3/UnknownFieldsPerformanceTest", + "DepthLimitPerformanceTest/0", "Editions/DelimitedTest/1"}; + EXPECT_EQ(PerformanceGtestFilter("*", /*performance=*/true, suites), + "*-BinaryTest.*:Editions/DelimitedTest/1.*"); + EXPECT_EQ(PerformanceGtestFilter("*", /*performance=*/false, suites), + "*-Proto3/UnknownFieldsPerformanceTest.*:" + "DepthLimitPerformanceTest/0.*"); +} + +TEST(PerformanceGtestFilterTest, KeepsAnExplicitFilter) { + const std::vector<std::string> suites = {"BinaryTest", + "BinaryPerformanceTest"}; + // Positive patterns only. + EXPECT_EQ( + PerformanceGtestFilter("Binary*.Foo:*.Bar", /*performance=*/true, suites), + "Binary*.Foo:*.Bar-BinaryTest.*"); + // Positive and negative patterns. + EXPECT_EQ(PerformanceGtestFilter("*.Foo-*.Bar:*.Baz", /*performance=*/false, + suites), + "*.Foo-*.Bar:*.Baz:BinaryPerformanceTest.*"); + // Negative patterns only: gtest takes the positive part as "*". + EXPECT_EQ(PerformanceGtestFilter("-*.Bar", /*performance=*/true, suites), + "*-*.Bar:BinaryTest.*"); + EXPECT_EQ(PerformanceGtestFilter("", /*performance=*/true, suites), + "*-BinaryTest.*"); +} + +TEST(PerformanceGtestFilterTest, UnchangedWhenNothingToExclude) { + EXPECT_EQ(PerformanceGtestFilter("*", /*performance=*/true, {}), "*"); + EXPECT_EQ(PerformanceGtestFilter("Foo.*-Bar.*", /*performance=*/false, + {"BinaryTest"}), + "Foo.*-Bar.*"); + EXPECT_EQ(PerformanceGtestFilter("Foo.*", /*performance=*/true, + {"BinaryPerformanceTest"}), + "Foo.*"); +} + +TEST(PrintConformanceRunnerUsageTest, DocumentsEveryOption) { + testing::internal::CaptureStderr(); + PrintConformanceRunnerUsage("Some error"); + const std::string usage = testing::internal::GetCapturedStderr(); + + EXPECT_THAT(usage, HasSubstr("Some error\nUsage: conformance-test-runner")); + for (absl::string_view option : + {"--failure_list", "--text_format_failure_list", "--enforce_recommended", + "--maximum_edition", "--output_dir", "--test", "--debug", + "--performance", "--verbose", "--record_requests", + "--gtest_<flag>=<value>"}) { + EXPECT_THAT(usage, HasSubstr(absl::StrCat("\n ", option, " "))) << option; + } + EXPECT_THAT(usage, HasSubstr("--gunit_")); +} + +TEST(ReportTestStatusSetTest, EmptySetReportsNothing) { + const std::string output_dir = NewOutputDir(); + std::string output = "before"; + EXPECT_TRUE( + ReportTestStatusSet({}, "empty.txt", "Some message", output_dir, output)); + EXPECT_EQ(output, "before"); + EXPECT_FALSE(FileExists(absl::StrCat(output_dir, "empty.txt"))); +} + +TEST(ReportTestStatusSetTest, WritesReportAndFile) { + const std::string output_dir = NewOutputDir(); + ReportedTestStatus added; + added.test_name = "Test.Added"; + added.failure_message = "it failed"; + ReportedTestStatus removed; + removed.test_name = "Test.Removed"; + removed.failure_message = "it passed"; + removed.matched_name = "Test.*"; + const std::vector<ReportedTestStatus> statuses = {added, removed}; + + std::string output = "before\n"; + EXPECT_FALSE(ReportTestStatusSet(statuses, "report.txt", "Some message", + output_dir, output)); + // The report always names the test itself ... + EXPECT_EQ(output, + "before\n" + "\n" + "Some message\n" + "\n" + " Test.Added # it failed\n" + " Test.Removed # it passed\n" + "\n"); + // ... whereas the file for update_failure_list has the failure list entry + // to remove, if there is one, and the test name to add otherwise. + EXPECT_EQ(ReadFile(absl::StrCat(output_dir, "report.txt")), + "Test.Added # it failed\n" + "Test.* # it passed\n"); +} + +TEST(ReportTestStatusSetTest, WritesRelativeToCurrentDirWithoutOutputDir) { + // An empty output_dir means the file name is used as is. Point it into the + // temp dir to keep the test hermetic. + const std::string output_dir = NewOutputDir(); + ReportedTestStatus status; + status.test_name = "Test.A"; + const std::vector<ReportedTestStatus> statuses = {status}; + + std::string output; + EXPECT_FALSE(ReportTestStatusSet(statuses, + absl::StrCat(output_dir, "plain.txt"), + "Some message", "", output)); + EXPECT_EQ(ReadFile(absl::StrCat(output_dir, "plain.txt")), "Test.A # \n"); +} + +TEST(ReportTestStatusSetTest, NoFileNameMeansNoFile) { + const std::string output_dir = NewOutputDir(); + ReportedTestStatus status; + status.test_name = "Test.A"; + status.failure_message = "message"; + const std::vector<ReportedTestStatus> statuses = {status}; + + std::string output; + EXPECT_FALSE( + ReportTestStatusSet(statuses, "", "Some message", output_dir, output)); + EXPECT_THAT(output, HasSubstr(" Test.A # message\n")); + EXPECT_THAT(output, Not(HasSubstr("Failed to open file"))); +} + +TEST(ReportTestStatusSetTest, ReportsUnwritableFile) { + const std::string output_dir = + absl::StrCat(NewOutputDir(), "does_not_exist/"); + ReportedTestStatus status; + status.test_name = "Test.A"; + const std::vector<ReportedTestStatus> statuses = {status}; + + std::string output; + EXPECT_FALSE(ReportTestStatusSet(statuses, "report.txt", "Some message", + output_dir, output)); + EXPECT_THAT(output, HasSubstr(" Test.A # \n")); + EXPECT_THAT(output, HasSubstr(absl::StrCat( + "Failed to open file: ", output_dir, "report.txt"))); +} + +} // namespace +} // namespace conformance +} // namespace protobuf +} // namespace google
diff --git a/conformance/cpp_in_process_test_runner.cc b/conformance/cpp_in_process_test_runner.cc new file mode 100644 index 0000000..e80c826 --- /dev/null +++ b/conformance/cpp_in_process_test_runner.cc
@@ -0,0 +1,31 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2026 Google LLC. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +#include "conformance/cpp_in_process_test_runner.h" + +#include <memory> + +#include "conformance/conformance.pb.h" +#include "conformance/conformance_cpp_harness.h" +#include "conformance/in_process_test_runner.h" +#include "conformance/test_runner.h" + +namespace google { +namespace protobuf { +namespace conformance { + +std::unique_ptr<ConformanceTestRunner> NewCppInProcessTestRunner() { + return std::make_unique<InProcessTestRunner>( + [harness = CppConformanceHarness()]( + const ::conformance::ConformanceRequest& request) { + return harness.RunTest(request); + }); +} + +} // namespace conformance +} // namespace protobuf +} // namespace google
diff --git a/conformance/cpp_in_process_test_runner.h b/conformance/cpp_in_process_test_runner.h new file mode 100644 index 0000000..4adf801 --- /dev/null +++ b/conformance/cpp_in_process_test_runner.h
@@ -0,0 +1,32 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2026 Google LLC. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +#ifndef GOOGLE_PROTOBUF_CONFORMANCE_CPP_IN_PROCESS_TEST_RUNNER_H__ +#define GOOGLE_PROTOBUF_CONFORMANCE_CPP_IN_PROCESS_TEST_RUNNER_H__ + +#include <memory> + +#include "conformance/test_runner.h" + +namespace google { +namespace protobuf { +namespace conformance { + +// Returns a ConformanceTestRunner that runs every request through a +// CppConformanceHarness owned by the runner, i.e. the C++ testee hosted in the +// test process instead of behind a fork/pipe. +// +// This is the glue between the testee side (conformance_cpp_harness) and the +// tester side (in_process_test_runner); it lives in its own library so that +// the conformance_cpp binary does not link any runner code. +std::unique_ptr<ConformanceTestRunner> NewCppInProcessTestRunner(); + +} // namespace conformance +} // namespace protobuf +} // namespace google + +#endif // GOOGLE_PROTOBUF_CONFORMANCE_CPP_IN_PROCESS_TEST_RUNNER_H__
diff --git a/conformance/cpp_in_process_test_runner_test.cc b/conformance/cpp_in_process_test_runner_test.cc new file mode 100644 index 0000000..bcf71af --- /dev/null +++ b/conformance/cpp_in_process_test_runner_test.cc
@@ -0,0 +1,118 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2026 Google LLC. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +#include "conformance/cpp_in_process_test_runner.h" + +#include <memory> +#include <string> + +#include <gmock/gmock.h> +#include <gtest/gtest.h> +#include "absl/log/absl_check.h" +#include "absl/status/status_matchers.h" +#include "absl/status/statusor.h" +#include "absl/strings/string_view.h" +#include "conformance/conformance.pb.h" +#include "conformance/conformance_cpp_harness.h" +#include "conformance/test_runner.h" +#include "google/protobuf/test_messages_proto3.pb.h" +#include "google/protobuf/test_textproto.h" +#include "google/protobuf/util/message_differencer.h" + +namespace google { +namespace protobuf { +namespace conformance { +namespace { + +using ::absl_testing::IsOkAndHolds; +using ::conformance::ConformanceRequest; +using ::conformance::ConformanceResponse; +using ::protobuf_test_messages::proto3::TestAllTypesProto3; +using ::testing::HasSubstr; + +// Returns a request that exercises the harness end to end: a binary +// TestAllTypesProto3 payload to be re-serialized as JSON. +ConformanceRequest Proto3JsonRequest() { + TestAllTypesProto3 message = + ParseTextOrDie(R"pb(optional_int32: 42 optional_string: "hello")pb"); + + ConformanceRequest request; + request.set_message_type("protobuf_test_messages.proto3.TestAllTypesProto3"); + request.set_requested_output_format(::conformance::JSON); + request.set_protobuf_payload(message.SerializeAsString()); + return request; +} + +// Parses the serialized ConformanceResponse returned by a runner, aborting the +// test if the runner produced bytes that are not a valid response. +ConformanceResponse ParseResponse(absl::string_view serialized) { + ConformanceResponse response; + ABSL_CHECK(response.ParseFromString(serialized)) + << "runner returned an unparseable ConformanceResponse"; + return response; +} + +// The response every Proto3JsonRequest() yields. Kept out of the macro +// arguments: MSVC's traditional preprocessor mis-scans raw string literals +// containing \" inside macro arguments. +constexpr absl::string_view kProto3JsonResponse = + R"pb(json_payload: "{\"optionalInt32\":42,\"optionalString\":\"hello\"}")pb"; + +TEST(CppInProcessTestRunnerTest, RunsHarness) { + std::unique_ptr<ConformanceTestRunner> runner = NewCppInProcessTestRunner(); + + ConformanceResponse response = ParseResponse( + runner->RunTest("Suite.Test", Proto3JsonRequest().SerializeAsString())); + + EXPECT_THAT(response, EqualsProto(kProto3JsonResponse)); +} + +TEST(CppInProcessTestRunnerTest, MatchesDirectHarnessResult) { + std::unique_ptr<ConformanceTestRunner> runner = NewCppInProcessTestRunner(); + ConformanceRequest request = Proto3JsonRequest(); + + absl::StatusOr<ConformanceResponse> direct = + CppConformanceHarness().RunTest(request); + ConformanceResponse via_runner = + ParseResponse(runner->RunTest("Suite.Test", request.SerializeAsString())); + + ASSERT_THAT(direct, IsOkAndHolds(EqualsProto(kProto3JsonResponse))); + EXPECT_TRUE(util::MessageDifferencer::Equals(via_runner, *direct)) + << via_runner.ShortDebugString(); +} + +TEST(CppInProcessTestRunnerTest, ReportsHarnessErrorAsRuntimeError) { + std::unique_ptr<ConformanceTestRunner> runner = NewCppInProcessTestRunner(); + ConformanceRequest request = Proto3JsonRequest(); + request.set_message_type("no.such.Message"); + + ConformanceResponse response = + ParseResponse(runner->RunTest("Suite.Test", request.SerializeAsString())); + + ASSERT_EQ(response.result_case(), ConformanceResponse::kRuntimeError); + EXPECT_THAT(response.runtime_error(), HasSubstr("no.such.Message")); + EXPECT_THAT(response.runtime_error(), HasSubstr("Suite.Test")); +} + +TEST(CppInProcessTestRunnerTest, RunnerIsReusableAcrossRequests) { + std::unique_ptr<ConformanceTestRunner> runner = NewCppInProcessTestRunner(); + std::string request = Proto3JsonRequest().SerializeAsString(); + + ConformanceResponse first = + ParseResponse(runner->RunTest("Suite.A", request)); + ConformanceResponse second = + ParseResponse(runner->RunTest("Suite.B", request)); + + EXPECT_TRUE(util::MessageDifferencer::Equals(first, second)) + << second.ShortDebugString(); + EXPECT_EQ(second.result_case(), ConformanceResponse::kJsonPayload); +} + +} // namespace +} // namespace conformance +} // namespace protobuf +} // namespace google
diff --git a/conformance/cpp_in_process_testee.cc b/conformance/cpp_in_process_testee.cc new file mode 100644 index 0000000..deb1d1c --- /dev/null +++ b/conformance/cpp_in_process_testee.cc
@@ -0,0 +1,51 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2026 Google LLC. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +// The C++ runtime's in-process conformance testee: defines MakeTesteeRunner() +// (testee_runner.h) to host CppConformanceHarness in the test process. +// `conformance_test(testee = ":cpp_in_process_testee", in_process = True)` +// links it into the C++ suites' tests in place of :forked_testee, so they run +// the testee without forking conformance_cpp. Other runtimes that can be +// linked into a C++ binary (e.g. upb) provide their own definition in their +// own library. + +#include <memory> +#include <string> + +#include "absl/log/absl_check.h" +#include "absl/strings/str_join.h" +#include "absl/strings/string_view.h" +#include "absl/types/span.h" +#include "conformance/cpp_in_process_test_runner.h" +#include "conformance/test_runner.h" +#include "conformance/testee_runner.h" + +namespace google { +namespace protobuf { +namespace conformance { + +std::unique_ptr<ConformanceTestRunner> MakeTesteeRunner( + absl::string_view testee_binary, + absl::Span<const std::string> testee_args) { + // The flags of a forked testee have no meaning here; refusing them keeps a + // stale command line from silently testing the linked testee instead. + ABSL_QCHECK(testee_binary.empty()) + << "--testee_binary=" << testee_binary + << " was given, but this test links the C++ testee in-process " + "(cpp_in_process_testee) and can't run another binary; drop " + "--testee_binary."; + ABSL_QCHECK(testee_args.empty()) + << "--testee_args / positional arguments [" + << absl::StrJoin(testee_args, " ") + << "] were given, but this test links the C++ testee in-process " + "(cpp_in_process_testee), which takes no arguments; drop them."; + return NewCppInProcessTestRunner(); +} + +} // namespace conformance +} // namespace protobuf +} // namespace google
diff --git a/conformance/cpp_in_process_testee_test.cc b/conformance/cpp_in_process_testee_test.cc new file mode 100644 index 0000000..5d70031 --- /dev/null +++ b/conformance/cpp_in_process_testee_test.cc
@@ -0,0 +1,60 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2026 Google LLC. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +#include <memory> +#include <string> +#include <vector> + +#include <gtest/gtest.h> +#include "conformance/conformance.pb.h" +#include "conformance/test_runner.h" +#include "conformance/testee_runner.h" +#include "google/protobuf/test_messages_proto3.pb.h" + +namespace google { +namespace protobuf { +namespace conformance { +namespace { + +using ::conformance::ConformanceRequest; +using ::conformance::ConformanceResponse; +using ::protobuf_test_messages::proto3::TestAllTypesProto3; + +TEST(CppInProcessTesteeTest, AnswersRequestsWithTheCppTestee) { + std::unique_ptr<ConformanceTestRunner> runner = MakeTesteeRunner("", {}); + ASSERT_NE(runner, nullptr); + + TestAllTypesProto3 message; + message.set_optional_int32(99); + ConformanceRequest request; + request.set_message_type(TestAllTypesProto3::descriptor()->full_name()); + request.set_protobuf_payload(message.SerializeAsString()); + request.set_requested_output_format(::conformance::TEXT_FORMAT); + + ConformanceResponse response; + ASSERT_TRUE(response.ParseFromString( + runner->RunTest("SomeTest", request.SerializeAsString()))); + EXPECT_EQ(response.text_payload(), "optional_int32: 99\n"); +} + +TEST(CppInProcessTesteeDeathTest, RejectsATesteeBinary) { + EXPECT_DEATH(MakeTesteeRunner("/some/testee", {}), + "--testee_binary=/some/testee was given, but this test links " + "the C\\+\\+ testee in-process"); +} + +TEST(CppInProcessTesteeDeathTest, RejectsTesteeArgs) { + const std::vector<std::string> args = {"--foo", "bar"}; + EXPECT_DEATH(MakeTesteeRunner("", args), + "positional arguments \\[--foo bar\\] were given, but this test " + "links the C\\+\\+ testee in-process"); +} + +} // namespace +} // namespace conformance +} // namespace protobuf +} // namespace google
diff --git a/conformance/failure_list.cc b/conformance/failure_list.cc new file mode 100644 index 0000000..ac97276 --- /dev/null +++ b/conformance/failure_list.cc
@@ -0,0 +1,123 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2026 Google LLC. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +#include "conformance/failure_list.h" + +#include <algorithm> +#include <string> + +#include "absl/log/absl_check.h" +#include "absl/status/status.h" +#include "absl/strings/ascii.h" +#include "absl/strings/match.h" +#include "absl/strings/str_cat.h" +#include "absl/strings/string_view.h" +#include "absl/types/optional.h" +#include "conformance/testee.h" + +namespace google { +namespace protobuf { +namespace conformance { +namespace internal { +namespace { + +constexpr int kFailureMessageLengthLimit = 128; + +void Normalize(std::string& input) { + input.erase(std::remove(input.begin(), input.end(), '\n'), input.end()); +} + +} // namespace + +std::string FormatFailureMessage(absl::string_view message) { + std::string result(absl::StripAsciiWhitespace(message)); + // Remove newlines + Normalize(result); + // Truncate failure message if needed + if (result.length() > kFailureMessageLengthLimit) { + result = result.substr(0, kFailureMessageLengthLimit); + } + return result; +} + +absl::Status FailureList::Add(absl::string_view test_name, + absl::string_view failure_message) { + if (absl::Status status = entries_.Insert(test_name); !status.ok()) { + return status; + } + // Normalize the message the same way reported failures are normalized so + // that the two compare equal in VerdictOnFailure(). + ABSL_CHECK(expected_messages_ + .emplace(test_name, FormatFailureMessage(failure_message)) + .second); + return absl::OkStatus(); +} + +absl::Status FailureList::VerdictOnSuccess(absl::string_view test_name) const { + absl::optional<std::string> failure_match = MatchingEntry(test_name); + if (!failure_match.has_value()) { + return absl::OkStatus(); + } + return absl::FailedPreconditionError(absl::StrCat( + "test ", test_name, " (matched to ", *failure_match, + ") is in the failure list, but test succeeded. Remove its match from " + "the failure list.")); +} + +absl::Status FailureList::VerdictOnFailure( + absl::string_view test_name, TestPriority priority, + absl::string_view failure_message) const { + absl::optional<std::string> failure_match = MatchingEntry(test_name); + if (!failure_match.has_value()) { + if (priority > enforcement_level_) { + return absl::OkStatus(); // Tolerated. + } + return absl::FailedPreconditionError( + absl::StrCat("Unexpected failure for test: ", test_name)); + } + + // Like the legacy runner, only require the actual failure message to start + // with the expected one. + std::string formatted_failure_message = FormatFailureMessage(failure_message); + const std::string& expected_failure_message = + expected_messages_.at(*failure_match); + if (!absl::StartsWith(formatted_failure_message, expected_failure_message)) { + return absl::FailedPreconditionError(absl::StrCat( + "Unexpected failure message for test: ", test_name, " expected: ", + expected_failure_message, " actual: ", formatted_failure_message)); + } + return absl::OkStatus(); +} + +absl::Status FailureList::VerdictOnSkip(absl::string_view test_name, + absl::string_view skip_reason) const { + absl::optional<std::string> failure_match = MatchingEntry(test_name); + if (!failure_match.has_value()) { + return absl::OkStatus(); + } + return absl::FailedPreconditionError(absl::StrCat( + "test ", test_name, " (matched to ", *failure_match, + ") is in the failure list but was skipped by the testee: ", skip_reason, + ". Remove its match from the failure list.")); +} + +absl::optional<std::string> FailureList::MatchingEntry( + absl::string_view test_name) const { + return entries_.WalkDownMatch(test_name); +} + +absl::optional<std::string> FailureList::ExpectedMessage( + absl::string_view entry) const { + auto it = expected_messages_.find(entry); + if (it == expected_messages_.end()) return absl::nullopt; + return it->second; +} + +} // namespace internal +} // namespace conformance +} // namespace protobuf +} // namespace google
diff --git a/conformance/failure_list.h b/conformance/failure_list.h new file mode 100644 index 0000000..bcbcc63 --- /dev/null +++ b/conformance/failure_list.h
@@ -0,0 +1,98 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2026 Google LLC. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +#ifndef GOOGLE_PROTOBUF_CONFORMANCE_FAILURE_LIST_H__ +#define GOOGLE_PROTOBUF_CONFORMANCE_FAILURE_LIST_H__ + +#include <string> + +#include "absl/container/flat_hash_map.h" +#include "absl/status/status.h" +#include "absl/strings/string_view.h" +#include "absl/types/optional.h" +#include "conformance/failure_list_trie_node.h" +#include "conformance/testee.h" + +namespace google { +namespace protobuf { +namespace conformance { +namespace internal { + +// The form a failure message takes in a failure list: surrounding whitespace +// and newlines removed, truncated to 128 characters. Expected messages are +// normalized when they are added and actual ones when they are judged, so +// that FailureList::VerdictOnFailure() compares like with like. +std::string FormatFailureMessage(absl::string_view message); + +// The expected failures of a conformance run, as the failure list files list +// them, plus the enforcement level: everything needed to judge one test's +// outcome, and nothing that changes once the tests run. The ResultLedger +// (result_ledger.h) builds it while loading the failure lists and tallies the +// outcomes against it. Yields() (matchers.h) reads the run's list through +// GetGlobalFailureList() (global_test_environment.h) and changes nothing. +class FailureList { + public: + FailureList() : entries_("root") {} + + // The lowest priority whose failures fail the suite (see TestPriority in + // testee.h). VerdictOnFailure() tolerates a failing test of a lower priority + // unless it is listed. Defaults to kLowestPriority, which enforces every + // priority. + void set_enforcement_level(TestPriority level) { enforcement_level_ = level; } + TestPriority enforcement_level() const { return enforcement_level_; } + + // Adds the entry `test_name`, a test name or a wildcard, as expected to fail + // with `failure_message` (see VerdictOnFailure()). Returns an error if the + // entry is already present or contains an invalid wildcard. + absl::Status Add(absl::string_view test_name, + absl::string_view failure_message); + + // The verdict on a test that succeeded: an error naming the entry it + // matched if the test was expected to fail. + absl::Status VerdictOnSuccess(absl::string_view test_name) const; + + // The verdict on a test that failed with `failure_message`. OK if the + // failure doesn't fail the suite. That is the case when the test is in the + // failure list with a matching message (an expected failure), or when it + // isn't listed and `priority` is above the enforcement level (a tolerated + // failure). Otherwise the failure is unexpected and the error says why. A + // listed test is checked whatever its priority, so that the failure list + // can't go stale unnoticed. The normalized actual message only needs to + // start with the expected message, so an empty expected message matches any + // failure. + absl::Status VerdictOnFailure(absl::string_view test_name, + TestPriority priority, + absl::string_view failure_message) const; + + // The verdict on a test the testee skipped for `skip_reason`: an error + // naming the entry it matched if the test is in the failure list, since a + // skipped test can't be an expected failure. Whether that error fails the + // test is the caller's policy; Yields() fails such a test. + absl::Status VerdictOnSkip(absl::string_view test_name, + absl::string_view skip_reason) const; + + // The failure list entry `test_name` matches, possibly a wildcard, if any. + absl::optional<std::string> MatchingEntry(absl::string_view test_name) const; + + // The expected failure message of the entry `entry`, normalized (see + // FormatFailureMessage()). `entry` is an entry exactly as listed, wildcards + // included, not a test name to match. Returns nullopt if there is no such + // entry. + absl::optional<std::string> ExpectedMessage(absl::string_view entry) const; + + private: + FailureListTrieNode entries_; + absl::flat_hash_map<std::string, std::string> expected_messages_; + TestPriority enforcement_level_ = kLowestPriority; +}; + +} // namespace internal +} // namespace conformance +} // namespace protobuf +} // namespace google + +#endif // GOOGLE_PROTOBUF_CONFORMANCE_FAILURE_LIST_H__
diff --git a/conformance/failure_list_cpp.txt b/conformance/failure_list_cpp.txt index 3b8d0e9..70597f1 100644 --- a/conformance/failure_list_cpp.txt +++ b/conformance/failure_list_cpp.txt
@@ -9,9 +9,9 @@ Recommended.*.JsonInput.BoolFieldDoubleQuotedFalse # Should have failed to parse, but didn't. Recommended.*.JsonInput.BoolFieldDoubleQuotedTrue # Should have failed to parse, but didn't. -Recommended.*.JsonInput.FieldNameDuplicate # Should have failed to parse or matched expected output but did not. -Recommended.*.JsonInput.FieldNameDuplicateDifferentCasing1 # Should have failed to parse or matched expected output but did not. -Recommended.*.JsonInput.FieldNameDuplicateDifferentCasing2 # Should have failed to parse or matched expected output but did not. +Recommended.*.JsonInput.FieldNameDuplicate # Should have failed to parse, but didn't., and Output was not equivalent to reference message: added: optional_nested_message.a: +Recommended.*.JsonInput.FieldNameDuplicateDifferentCasing1 # Should have failed to parse, but didn't., and Output was not equivalent to reference message: added: optional_nested_message.a: +Recommended.*.JsonInput.FieldNameDuplicateDifferentCasing2 # Should have failed to parse, but didn't., and Output was not equivalent to reference message: added: optional_nested_message.a: Recommended.*.JsonInput.FieldNameNotQuoted # Should have failed to parse, but didn't. Recommended.*.JsonInput.MapFieldValueIsNull # Should have failed to parse, but didn't. Recommended.*.JsonInput.RepeatedFieldMessageElementIsNull # Should have failed to parse, but didn't.
diff --git a/conformance/failure_list_java.txt b/conformance/failure_list_java.txt index 61872eb..4cfe7d9 100644 --- a/conformance/failure_list_java.txt +++ b/conformance/failure_list_java.txt
@@ -14,7 +14,7 @@ Recommended.*.JsonInput.DoubleFieldInfinityNotQuoted # Should have failed to parse, but didn't. Recommended.*.JsonInput.DoubleFieldNanNotQuoted # Should have failed to parse, but didn't. Recommended.*.JsonInput.DoubleFieldNegativeInfinityNotQuoted # Should have failed to parse, but didn't. -Recommended.*.JsonInput.FieldNameExtension.JsonOutput # Expected JSON payload but got type 1 +Recommended.*.JsonInput.FieldNameExtension.JsonOutput # Failed to parse input or produce output. Recommended.*.JsonInput.FieldNameNotQuoted # Should have failed to parse, but didn't. Recommended.*.JsonInput.FloatFieldInfinityNotQuoted # Should have failed to parse, but didn't. Recommended.*.JsonInput.FloatFieldNanNotQuoted # Should have failed to parse, but didn't.
diff --git a/conformance/failure_list_jruby_ffi.txt b/conformance/failure_list_jruby_ffi.txt index d1d4c33..9606e4d 100644 --- a/conformance/failure_list_jruby_ffi.txt +++ b/conformance/failure_list_jruby_ffi.txt
@@ -1,6 +1,6 @@ -Recommended.*.JsonInput.FieldNameDuplicate # Should have failed to parse or matched expected output but did not. -Recommended.*.JsonInput.FieldNameDuplicateDifferentCasing1 # Should have failed to parse or matched expected output but did not. -Recommended.*.JsonInput.FieldNameDuplicateDifferentCasing2 # Should have failed to parse or matched expected output but did not. +Recommended.*.JsonInput.FieldNameDuplicate # Should have failed to parse, but didn't., and Output was not equivalent to reference message: +Recommended.*.JsonInput.FieldNameDuplicateDifferentCasing1 # Should have failed to parse, but didn't., and Output was not equivalent to reference message: +Recommended.*.JsonInput.FieldNameDuplicateDifferentCasing2 # Should have failed to parse, but didn't., and Output was not equivalent to reference message: Required.*.JsonInput.Int32FieldQuotedExponentialValue.* # Failed to parse input or produce output. Required.*.JsonInput.AnyWithNoType.* # Failed to parse input or produce output. # TODO: Uncomment once conformance tests can express failures that are not expected to be fixed.
diff --git a/conformance/failure_list_php.txt b/conformance/failure_list_php.txt index 88cb0c4..31d9342 100644 --- a/conformance/failure_list_php.txt +++ b/conformance/failure_list_php.txt
@@ -6,10 +6,10 @@ Recommended.Editions_Proto3.JsonInput.BytesFieldBase64Url.ProtobufOutput Recommended.Proto3.JsonInput.BytesFieldBase64Url.ProtobufOutput Recommended.*.JsonInput.FieldMaskInvalidCharacter -Recommended.Editions_Proto3.JsonInput.FieldNameDuplicateDifferentCasing1 # Should have failed to parse or matched expected output but did not. -Recommended.Proto3.JsonInput.FieldNameDuplicateDifferentCasing1 # Should have failed to parse or matched expected output but did not. -Recommended.Editions_Proto3.JsonInput.FieldNameDuplicateDifferentCasing2 # Should have failed to parse or matched expected output but did not. -Recommended.Proto3.JsonInput.FieldNameDuplicateDifferentCasing2 # Should have failed to parse or matched expected output but did not. +Recommended.Editions_Proto3.JsonInput.FieldNameDuplicateDifferentCasing1 # Should have failed to parse, but didn't., and Output was not equivalent to reference message: +Recommended.Proto3.JsonInput.FieldNameDuplicateDifferentCasing1 # Should have failed to parse, but didn't., and Output was not equivalent to reference message: +Recommended.Editions_Proto3.JsonInput.FieldNameDuplicateDifferentCasing2 # Should have failed to parse, but didn't., and Output was not equivalent to reference message: +Recommended.Proto3.JsonInput.FieldNameDuplicateDifferentCasing2 # Should have failed to parse, but didn't., and Output was not equivalent to reference message: Recommended.Editions_Proto3.ProtobufInput.ValidDataOneofBinary.MESSAGE.Merge.ProtobufOutput Recommended.Proto3.ProtobufInput.ValidDataOneofBinary.MESSAGE.Merge.ProtobufOutput Recommended.*.ProtobufInput.ValueRejectInfNumberValue.JsonOutput # Should have failed to serialize, but didn't.
diff --git a/conformance/failure_list_php_c.txt b/conformance/failure_list_php_c.txt index adc306a..dff232c 100644 --- a/conformance/failure_list_php_c.txt +++ b/conformance/failure_list_php_c.txt
@@ -1,9 +1,9 @@ -Recommended.Editions_Proto3.JsonInput.FieldNameDuplicate # Should have failed to parse or matched expected output but did not. -Recommended.Proto3.JsonInput.FieldNameDuplicate # Should have failed to parse or matched expected output but did not. -Recommended.Editions_Proto3.JsonInput.FieldNameDuplicateDifferentCasing1 # Should have failed to parse or matched expected output but did not. -Recommended.Proto3.JsonInput.FieldNameDuplicateDifferentCasing1 # Should have failed to parse or matched expected output but did not. -Recommended.Editions_Proto3.JsonInput.FieldNameDuplicateDifferentCasing2 # Should have failed to parse or matched expected output but did not. -Recommended.Proto3.JsonInput.FieldNameDuplicateDifferentCasing2 # Should have failed to parse or matched expected output but did not. +Recommended.Editions_Proto3.JsonInput.FieldNameDuplicate # Should have failed to parse, but didn't., and Output was not equivalent to reference message: +Recommended.Proto3.JsonInput.FieldNameDuplicate # Should have failed to parse, but didn't., and Output was not equivalent to reference message: +Recommended.Editions_Proto3.JsonInput.FieldNameDuplicateDifferentCasing1 # Should have failed to parse, but didn't., and Output was not equivalent to reference message: +Recommended.Proto3.JsonInput.FieldNameDuplicateDifferentCasing1 # Should have failed to parse, but didn't., and Output was not equivalent to reference message: +Recommended.Editions_Proto3.JsonInput.FieldNameDuplicateDifferentCasing2 # Should have failed to parse, but didn't., and Output was not equivalent to reference message: +Recommended.Proto3.JsonInput.FieldNameDuplicateDifferentCasing2 # Should have failed to parse, but didn't., and Output was not equivalent to reference message: Required.Editions_Proto3.JsonInput.Int32FieldQuotedExponentialValue.* # Failed to parse input or produce output. Required.Proto3.JsonInput.Int32FieldQuotedExponentialValue.* # Failed to parse input or produce output. Required.*.JsonInput.AnyWithNoType.*
diff --git a/conformance/failure_list_python.txt b/conformance/failure_list_python.txt index 873421d..fe59385 100644 --- a/conformance/failure_list_python.txt +++ b/conformance/failure_list_python.txt
@@ -1,5 +1,5 @@ -Recommended.*.JsonInput.FieldNameDuplicateDifferentCasing1 # Should have failed to parse or matched expected output but did not. -Recommended.*.JsonInput.FieldNameDuplicateDifferentCasing2 # Should have failed to parse or matched expected output but did not. +Recommended.*.JsonInput.FieldNameDuplicateDifferentCasing1 # Should have failed to parse, but didn't., and Output was not equivalent to reference message: added: optional_nested_message.a: +Recommended.*.JsonInput.FieldNameDuplicateDifferentCasing2 # Should have failed to parse, but didn't., and Output was not equivalent to reference message: added: optional_nested_message.a: Required.*.JsonInput.DoubleFieldFalseValue # Should have failed to parse, but didn't. Required.*.JsonInput.DoubleFieldStringValueArabicIndicDigits # Should have failed to parse, but didn't. Required.*.JsonInput.DoubleFieldStringValueFullwidthDigits # Should have failed to parse, but didn't.
diff --git a/conformance/failure_list_python_cpp.txt b/conformance/failure_list_python_cpp.txt index f49eaba..c5af70d 100644 --- a/conformance/failure_list_python_cpp.txt +++ b/conformance/failure_list_python_cpp.txt
@@ -7,8 +7,8 @@ # TODO: insert links to corresponding bugs tracking the issue. # Should we use GitHub issues or the Google-internal bug tracker? -Recommended.*.JsonInput.FieldNameDuplicateDifferentCasing1 # Should have failed to parse or matched expected output but did not. -Recommended.*.JsonInput.FieldNameDuplicateDifferentCasing2 # Should have failed to parse or matched expected output but did not. +Recommended.*.JsonInput.FieldNameDuplicateDifferentCasing1 # Should have failed to parse, but didn't., and Output was not equivalent to reference message: added: optional_nested_message.a: +Recommended.*.JsonInput.FieldNameDuplicateDifferentCasing2 # Should have failed to parse, but didn't., and Output was not equivalent to reference message: added: optional_nested_message.a: Required.*.JsonInput.DoubleFieldFalseValue # Should have failed to parse, but didn't. Required.*.JsonInput.DoubleFieldStringValueArabicIndicDigits # Should have failed to parse, but didn't. Required.*.JsonInput.DoubleFieldStringValueFullwidthDigits # Should have failed to parse, but didn't.
diff --git a/conformance/failure_list_python_upb.txt b/conformance/failure_list_python_upb.txt index 72fb8b8..9be5513 100644 --- a/conformance/failure_list_python_upb.txt +++ b/conformance/failure_list_python_upb.txt
@@ -1,5 +1,5 @@ -Recommended.*.JsonInput.FieldNameDuplicateDifferentCasing1 # Should have failed to parse or matched expected output but did not. -Recommended.*.JsonInput.FieldNameDuplicateDifferentCasing2 # Should have failed to parse or matched expected output but did not. +Recommended.*.JsonInput.FieldNameDuplicateDifferentCasing1 # Should have failed to parse, but didn't., and Output was not equivalent to reference message: added: optional_nested_message.a: +Recommended.*.JsonInput.FieldNameDuplicateDifferentCasing2 # Should have failed to parse, but didn't., and Output was not equivalent to reference message: added: optional_nested_message.a: Required.*.JsonInput.DoubleFieldFalseValue # Should have failed to parse, but didn't. Required.*.JsonInput.DoubleFieldStringValueArabicIndicDigits # Should have failed to parse, but didn't. Required.*.JsonInput.DoubleFieldStringValueFullwidthDigits # Should have failed to parse, but didn't.
diff --git a/conformance/failure_list_ruby.txt b/conformance/failure_list_ruby.txt index 4a79ef7..8196d08 100644 --- a/conformance/failure_list_ruby.txt +++ b/conformance/failure_list_ruby.txt
@@ -1,6 +1,6 @@ -Recommended.*.JsonInput.FieldNameDuplicate # Should have failed to parse or matched expected output but did not. -Recommended.*.JsonInput.FieldNameDuplicateDifferentCasing1 # Should have failed to parse or matched expected output but did not. -Recommended.*.JsonInput.FieldNameDuplicateDifferentCasing2 # Should have failed to parse or matched expected output but did not. +Recommended.*.JsonInput.FieldNameDuplicate # Should have failed to parse, but didn't., and Output was not equivalent to reference message: +Recommended.*.JsonInput.FieldNameDuplicateDifferentCasing1 # Should have failed to parse, but didn't., and Output was not equivalent to reference message: +Recommended.*.JsonInput.FieldNameDuplicateDifferentCasing2 # Should have failed to parse, but didn't., and Output was not equivalent to reference message: Required.*.JsonInput.Int32FieldQuotedExponentialValue.* # Failed to parse input or produce output. Required.*.JsonInput.AnyWithNoType.* # TODO: Uncomment once conformance tests can express failures that are not expected to be fixed.
diff --git a/conformance/failure_lists/cpp_binary.txt b/conformance/failure_lists/cpp_binary.txt new file mode 100644 index 0000000..b02b49a --- /dev/null +++ b/conformance/failure_lists/cpp_binary.txt
@@ -0,0 +1,4 @@ +Required.Editions_Proto2.ProtobufInput.BadTag_FieldNumberSlightlyTooHigh # Should have failed to parse, but didn't. +Required.Editions_Proto3.ProtobufInput.BadTag_FieldNumberSlightlyTooHigh # Should have failed to parse, but didn't. +Required.Proto2.ProtobufInput.BadTag_FieldNumberSlightlyTooHigh # Should have failed to parse, but didn't. +Required.Proto3.ProtobufInput.BadTag_FieldNumberSlightlyTooHigh # Should have failed to parse, but didn't.
diff --git a/conformance/failure_lists/cpp_json.txt b/conformance/failure_lists/cpp_json.txt new file mode 100644 index 0000000..c0d4486 --- /dev/null +++ b/conformance/failure_lists/cpp_json.txt
@@ -0,0 +1,122 @@ +Recommended.Editions_Proto2.JsonInput.BoolFieldDoubleQuotedFalse # Should have failed to parse, but didn't. +Recommended.Editions_Proto2.JsonInput.BoolFieldDoubleQuotedTrue # Should have failed to parse, but didn't. +Recommended.Editions_Proto2.JsonInput.FieldNameDuplicate # Should have failed to parse, but didn't., and Output was not equivalent to reference message: added: optional_nested_message.a: +Recommended.Editions_Proto2.JsonInput.FieldNameDuplicateDifferentCasing1 # Should have failed to parse, but didn't., and Output was not equivalent to reference message: added: optional_nested_message.a: +Recommended.Editions_Proto2.JsonInput.FieldNameDuplicateDifferentCasing2 # Should have failed to parse, but didn't., and Output was not equivalent to reference message: added: optional_nested_message.a: +Recommended.Editions_Proto2.JsonInput.FieldNameNotQuoted # Should have failed to parse, but didn't. +Recommended.Editions_Proto2.JsonInput.MapFieldValueIsNull # Should have failed to parse, but didn't. +Recommended.Editions_Proto2.JsonInput.RepeatedFieldMessageElementIsNull # Should have failed to parse, but didn't. +Recommended.Editions_Proto2.JsonInput.RepeatedFieldPrimitiveElementIsNull # Should have failed to parse, but didn't. +Recommended.Editions_Proto2.JsonInput.RepeatedFieldTrailingComma # Should have failed to parse, but didn't. +Recommended.Editions_Proto2.JsonInput.RepeatedFieldTrailingCommaWithNewlines # Should have failed to parse, but didn't. +Recommended.Editions_Proto2.JsonInput.RepeatedFieldTrailingCommaWithSpace # Should have failed to parse, but didn't. +Recommended.Editions_Proto2.JsonInput.RepeatedFieldTrailingCommaWithSpaceCommaSpace # Should have failed to parse, but didn't. +Recommended.Editions_Proto2.JsonInput.StringFieldSingleQuoteBoth # Should have failed to parse, but didn't. +Recommended.Editions_Proto2.JsonInput.StringFieldSingleQuoteKey # Should have failed to parse, but didn't. +Recommended.Editions_Proto2.JsonInput.StringFieldSingleQuoteValue # Should have failed to parse, but didn't. +Recommended.Editions_Proto2.JsonInput.StringFieldUppercaseEscapeLetter # Should have failed to parse, but didn't. +Recommended.Editions_Proto2.JsonInput.TrailingCommaInAnObject # Should have failed to parse, but didn't. +Recommended.Editions_Proto2.JsonInput.TrailingCommaInAnObjectWithNewlines # Should have failed to parse, but didn't. +Recommended.Editions_Proto2.JsonInput.TrailingCommaInAnObjectWithSpace # Should have failed to parse, but didn't. +Recommended.Editions_Proto2.JsonInput.TrailingCommaInAnObjectWithSpaceCommaSpace # Should have failed to parse, but didn't. +Recommended.Editions_Proto3.ProtobufInput.FieldMaskNumbersDontRoundTrip.JsonOutput # Should have failed to serialize, but didn't. +Recommended.Editions_Proto3.ProtobufInput.FieldMaskPathsDontRoundTrip.JsonOutput # Should have failed to serialize, but didn't. +Recommended.Editions_Proto3.ProtobufInput.FieldMaskTooManyUnderscore.JsonOutput # Should have failed to serialize, but didn't. +Recommended.Editions_Proto3.JsonInput.BoolFieldDoubleQuotedFalse # Should have failed to parse, but didn't. +Recommended.Editions_Proto3.JsonInput.BoolFieldDoubleQuotedTrue # Should have failed to parse, but didn't. +Recommended.Editions_Proto3.JsonInput.FieldMaskInvalidCharacter # Should have failed to parse, but didn't. +Recommended.Editions_Proto3.JsonInput.FieldNameDuplicate # Should have failed to parse, but didn't., and Output was not equivalent to reference message: added: optional_nested_message.a: +Recommended.Editions_Proto3.JsonInput.FieldNameDuplicateDifferentCasing1 # Should have failed to parse, but didn't., and Output was not equivalent to reference message: added: optional_nested_message.a: +Recommended.Editions_Proto3.JsonInput.FieldNameDuplicateDifferentCasing2 # Should have failed to parse, but didn't., and Output was not equivalent to reference message: added: optional_nested_message.a: +Recommended.Editions_Proto3.JsonInput.FieldNameNotQuoted # Should have failed to parse, but didn't. +Recommended.Editions_Proto3.JsonInput.MapFieldValueIsNull # Should have failed to parse, but didn't. +Recommended.Editions_Proto3.JsonInput.RepeatedFieldMessageElementIsNull # Should have failed to parse, but didn't. +Recommended.Editions_Proto3.JsonInput.RepeatedFieldPrimitiveElementIsNull # Should have failed to parse, but didn't. +Recommended.Editions_Proto3.JsonInput.RepeatedFieldTrailingComma # Should have failed to parse, but didn't. +Recommended.Editions_Proto3.JsonInput.RepeatedFieldTrailingCommaWithNewlines # Should have failed to parse, but didn't. +Recommended.Editions_Proto3.JsonInput.RepeatedFieldTrailingCommaWithSpace # Should have failed to parse, but didn't. +Recommended.Editions_Proto3.JsonInput.RepeatedFieldTrailingCommaWithSpaceCommaSpace # Should have failed to parse, but didn't. +Recommended.Editions_Proto3.JsonInput.StringFieldSingleQuoteBoth # Should have failed to parse, but didn't. +Recommended.Editions_Proto3.JsonInput.StringFieldSingleQuoteKey # Should have failed to parse, but didn't. +Recommended.Editions_Proto3.JsonInput.StringFieldSingleQuoteValue # Should have failed to parse, but didn't. +Recommended.Editions_Proto3.JsonInput.StringFieldUppercaseEscapeLetter # Should have failed to parse, but didn't. +Recommended.Editions_Proto3.JsonInput.TrailingCommaInAnObject # Should have failed to parse, but didn't. +Recommended.Editions_Proto3.JsonInput.TrailingCommaInAnObjectWithNewlines # Should have failed to parse, but didn't. +Recommended.Editions_Proto3.JsonInput.TrailingCommaInAnObjectWithSpace # Should have failed to parse, but didn't. +Recommended.Editions_Proto3.JsonInput.TrailingCommaInAnObjectWithSpaceCommaSpace # Should have failed to parse, but didn't. +Recommended.Proto2.JsonInput.BoolFieldDoubleQuotedFalse # Should have failed to parse, but didn't. +Recommended.Proto2.JsonInput.BoolFieldDoubleQuotedTrue # Should have failed to parse, but didn't. +Recommended.Proto2.JsonInput.FieldNameDuplicate # Should have failed to parse, but didn't., and Output was not equivalent to reference message: added: optional_nested_message.a: +Recommended.Proto2.JsonInput.FieldNameDuplicateDifferentCasing1 # Should have failed to parse, but didn't., and Output was not equivalent to reference message: added: optional_nested_message.a: +Recommended.Proto2.JsonInput.FieldNameDuplicateDifferentCasing2 # Should have failed to parse, but didn't., and Output was not equivalent to reference message: added: optional_nested_message.a: +Recommended.Proto2.JsonInput.FieldNameNotQuoted # Should have failed to parse, but didn't. +Recommended.Proto2.JsonInput.MapFieldValueIsNull # Should have failed to parse, but didn't. +Recommended.Proto2.JsonInput.RepeatedFieldMessageElementIsNull # Should have failed to parse, but didn't. +Recommended.Proto2.JsonInput.RepeatedFieldPrimitiveElementIsNull # Should have failed to parse, but didn't. +Recommended.Proto2.JsonInput.RepeatedFieldTrailingComma # Should have failed to parse, but didn't. +Recommended.Proto2.JsonInput.RepeatedFieldTrailingCommaWithNewlines # Should have failed to parse, but didn't. +Recommended.Proto2.JsonInput.RepeatedFieldTrailingCommaWithSpace # Should have failed to parse, but didn't. +Recommended.Proto2.JsonInput.RepeatedFieldTrailingCommaWithSpaceCommaSpace # Should have failed to parse, but didn't. +Recommended.Proto2.JsonInput.StringFieldSingleQuoteBoth # Should have failed to parse, but didn't. +Recommended.Proto2.JsonInput.StringFieldSingleQuoteKey # Should have failed to parse, but didn't. +Recommended.Proto2.JsonInput.StringFieldSingleQuoteValue # Should have failed to parse, but didn't. +Recommended.Proto2.JsonInput.StringFieldUppercaseEscapeLetter # Should have failed to parse, but didn't. +Recommended.Proto2.JsonInput.TrailingCommaInAnObject # Should have failed to parse, but didn't. +Recommended.Proto2.JsonInput.TrailingCommaInAnObjectWithNewlines # Should have failed to parse, but didn't. +Recommended.Proto2.JsonInput.TrailingCommaInAnObjectWithSpace # Should have failed to parse, but didn't. +Recommended.Proto2.JsonInput.TrailingCommaInAnObjectWithSpaceCommaSpace # Should have failed to parse, but didn't. +Recommended.Proto3.ProtobufInput.FieldMaskNumbersDontRoundTrip.JsonOutput # Should have failed to serialize, but didn't. +Recommended.Proto3.ProtobufInput.FieldMaskPathsDontRoundTrip.JsonOutput # Should have failed to serialize, but didn't. +Recommended.Proto3.ProtobufInput.FieldMaskTooManyUnderscore.JsonOutput # Should have failed to serialize, but didn't. +Recommended.Proto3.JsonInput.BoolFieldDoubleQuotedFalse # Should have failed to parse, but didn't. +Recommended.Proto3.JsonInput.BoolFieldDoubleQuotedTrue # Should have failed to parse, but didn't. +Recommended.Proto3.JsonInput.FieldMaskInvalidCharacter # Should have failed to parse, but didn't. +Recommended.Proto3.JsonInput.FieldNameDuplicate # Should have failed to parse, but didn't., and Output was not equivalent to reference message: added: optional_nested_message.a: +Recommended.Proto3.JsonInput.FieldNameDuplicateDifferentCasing1 # Should have failed to parse, but didn't., and Output was not equivalent to reference message: added: optional_nested_message.a: +Recommended.Proto3.JsonInput.FieldNameDuplicateDifferentCasing2 # Should have failed to parse, but didn't., and Output was not equivalent to reference message: added: optional_nested_message.a: +Recommended.Proto3.JsonInput.FieldNameNotQuoted # Should have failed to parse, but didn't. +Recommended.Proto3.JsonInput.MapFieldValueIsNull # Should have failed to parse, but didn't. +Recommended.Proto3.JsonInput.RepeatedFieldMessageElementIsNull # Should have failed to parse, but didn't. +Recommended.Proto3.JsonInput.RepeatedFieldPrimitiveElementIsNull # Should have failed to parse, but didn't. +Recommended.Proto3.JsonInput.RepeatedFieldTrailingComma # Should have failed to parse, but didn't. +Recommended.Proto3.JsonInput.RepeatedFieldTrailingCommaWithNewlines # Should have failed to parse, but didn't. +Recommended.Proto3.JsonInput.RepeatedFieldTrailingCommaWithSpace # Should have failed to parse, but didn't. +Recommended.Proto3.JsonInput.RepeatedFieldTrailingCommaWithSpaceCommaSpace # Should have failed to parse, but didn't. +Recommended.Proto3.JsonInput.StringFieldSingleQuoteBoth # Should have failed to parse, but didn't. +Recommended.Proto3.JsonInput.StringFieldSingleQuoteKey # Should have failed to parse, but didn't. +Recommended.Proto3.JsonInput.StringFieldSingleQuoteValue # Should have failed to parse, but didn't. +Recommended.Proto3.JsonInput.StringFieldUppercaseEscapeLetter # Should have failed to parse, but didn't. +Recommended.Proto3.JsonInput.TrailingCommaInAnObject # Should have failed to parse, but didn't. +Recommended.Proto3.JsonInput.TrailingCommaInAnObjectWithNewlines # Should have failed to parse, but didn't. +Recommended.Proto3.JsonInput.TrailingCommaInAnObjectWithSpace # Should have failed to parse, but didn't. +Recommended.Proto3.JsonInput.TrailingCommaInAnObjectWithSpaceCommaSpace # Should have failed to parse, but didn't. +Required.Editions_Proto2.JsonInput.SingleValueForRepeatedFieldInt32 # Should have failed to parse, but didn't. +Required.Editions_Proto2.JsonInput.SingleValueForRepeatedFieldMessage # Should have failed to parse, but didn't. +Required.Editions_Proto3.JsonInput.SingleValueForRepeatedFieldInt32 # Should have failed to parse, but didn't. +Required.Editions_Proto3.JsonInput.SingleValueForRepeatedFieldMessage # Should have failed to parse, but didn't. +Required.Editions_Proto3.JsonInput.TimestampJsonInputDayTooLarge # Should have failed to parse, but didn't. +Required.Editions_Proto3.JsonInput.TimestampJsonInputHourTooLarge # Should have failed to parse, but didn't. +Required.Editions_Proto3.JsonInput.TimestampJsonInputHourTooLarge25 # Should have failed to parse, but didn't. +Required.Editions_Proto3.JsonInput.TimestampJsonInputInvalidOffsetHour # Should have failed to parse, but didn't. +Required.Editions_Proto3.JsonInput.TimestampJsonInputInvalidOffsetMinute # Should have failed to parse, but didn't. +Required.Editions_Proto3.JsonInput.TimestampJsonInputMinuteTooLarge # Should have failed to parse, but didn't. +Required.Editions_Proto3.JsonInput.TimestampJsonInputMonthTooLarge # Should have failed to parse, but didn't. +Required.Editions_Proto3.JsonInput.TimestampJsonInputNonLeapFeb29 # Should have failed to parse, but didn't. +Required.Editions_Proto3.JsonInput.TimestampJsonInputOffsetBoundaryOverflow # Should have failed to parse, but didn't. +Required.Editions_Proto3.JsonInput.TimestampJsonInputOffsetBoundaryUnderflow # Should have failed to parse, but didn't. +Required.Editions_Proto3.JsonInput.TimestampJsonInputSecondTooLarge # Should have failed to parse, but didn't. +Required.Proto2.JsonInput.SingleValueForRepeatedFieldInt32 # Should have failed to parse, but didn't. +Required.Proto2.JsonInput.SingleValueForRepeatedFieldMessage # Should have failed to parse, but didn't. +Required.Proto3.JsonInput.SingleValueForRepeatedFieldInt32 # Should have failed to parse, but didn't. +Required.Proto3.JsonInput.SingleValueForRepeatedFieldMessage # Should have failed to parse, but didn't. +Required.Proto3.JsonInput.TimestampJsonInputDayTooLarge # Should have failed to parse, but didn't. +Required.Proto3.JsonInput.TimestampJsonInputHourTooLarge # Should have failed to parse, but didn't. +Required.Proto3.JsonInput.TimestampJsonInputHourTooLarge25 # Should have failed to parse, but didn't. +Required.Proto3.JsonInput.TimestampJsonInputInvalidOffsetHour # Should have failed to parse, but didn't. +Required.Proto3.JsonInput.TimestampJsonInputInvalidOffsetMinute # Should have failed to parse, but didn't. +Required.Proto3.JsonInput.TimestampJsonInputMinuteTooLarge # Should have failed to parse, but didn't. +Required.Proto3.JsonInput.TimestampJsonInputMonthTooLarge # Should have failed to parse, but didn't. +Required.Proto3.JsonInput.TimestampJsonInputNonLeapFeb29 # Should have failed to parse, but didn't. +Required.Proto3.JsonInput.TimestampJsonInputOffsetBoundaryOverflow # Should have failed to parse, but didn't. +Required.Proto3.JsonInput.TimestampJsonInputOffsetBoundaryUnderflow # Should have failed to parse, but didn't. +Required.Proto3.JsonInput.TimestampJsonInputSecondTooLarge # Should have failed to parse, but didn't.
diff --git a/conformance/failure_lists/cpp_performance.txt b/conformance/failure_lists/cpp_performance.txt new file mode 100644 index 0000000..72f63eb --- /dev/null +++ b/conformance/failure_lists/cpp_performance.txt
@@ -0,0 +1,16 @@ +# This is the list of conformance performance tests that are known to +# fail for the C++ implementation right now. + +# Expected: Non-repeated field "recursive_message" is specified multiple times. +Recommended.*.TextFormatInput.TestTextFormatPerformanceMergeMessageWithRepeatedFieldBool.ProtobufOutput # Failed to parse input or produce output. +Recommended.*.TextFormatInput.TestTextFormatPerformanceMergeMessageWithRepeatedFieldBool.TextFormatOutput # Failed to parse input or produce output. +Recommended.*.TextFormatInput.TestTextFormatPerformanceMergeMessageWithRepeatedFieldBytes.ProtobufOutput # Failed to parse input or produce output. +Recommended.*.TextFormatInput.TestTextFormatPerformanceMergeMessageWithRepeatedFieldBytes.TextFormatOutput # Failed to parse input or produce output. +Recommended.*.TextFormatInput.TestTextFormatPerformanceMergeMessageWithRepeatedFieldDouble.ProtobufOutput # Failed to parse input or produce output. +Recommended.*.TextFormatInput.TestTextFormatPerformanceMergeMessageWithRepeatedFieldDouble.TextFormatOutput # Failed to parse input or produce output. +Recommended.*.TextFormatInput.TestTextFormatPerformanceMergeMessageWithRepeatedFieldInt32.ProtobufOutput # Failed to parse input or produce output. +Recommended.*.TextFormatInput.TestTextFormatPerformanceMergeMessageWithRepeatedFieldInt32.TextFormatOutput # Failed to parse input or produce output. +Recommended.*.TextFormatInput.TestTextFormatPerformanceMergeMessageWithRepeatedFieldInt64.ProtobufOutput # Failed to parse input or produce output. +Recommended.*.TextFormatInput.TestTextFormatPerformanceMergeMessageWithRepeatedFieldInt64.TextFormatOutput # Failed to parse input or produce output. +Recommended.*.TextFormatInput.TestTextFormatPerformanceMergeMessageWithRepeatedFieldString.ProtobufOutput # Failed to parse input or produce output. +Recommended.*.TextFormatInput.TestTextFormatPerformanceMergeMessageWithRepeatedFieldString.TextFormatOutput # Failed to parse input or produce output.
diff --git a/conformance/failure_lists/cpp_text.txt b/conformance/failure_lists/cpp_text.txt new file mode 100644 index 0000000..69895e1 --- /dev/null +++ b/conformance/failure_lists/cpp_text.txt
@@ -0,0 +1,45 @@ +Recommended.Editions_Proto3.TextFormatInput.StringLiteralLongUnicodeEscapeSurrogateFirstOnlyBytes # Should have failed to parse, but didn't. +Recommended.Editions_Proto3.TextFormatInput.StringLiteralLongUnicodeEscapeSurrogateFirstOnlyString # Should have failed to parse, but didn't. +Recommended.Editions_Proto3.TextFormatInput.StringLiteralLongUnicodeEscapeSurrogatePairBytes # Should have failed to parse, but didn't. +Recommended.Editions_Proto3.TextFormatInput.StringLiteralLongUnicodeEscapeSurrogatePairString # Should have failed to parse, but didn't. +Recommended.Editions_Proto3.TextFormatInput.StringLiteralLongUnicodeEscapeSurrogateSecondOnlyBytes # Should have failed to parse, but didn't. +Recommended.Editions_Proto3.TextFormatInput.StringLiteralLongUnicodeEscapeSurrogateSecondOnlyString # Should have failed to parse, but didn't. +Recommended.Editions_Proto3.TextFormatInput.StringLiteralShortUnicodeEscapeSurrogateFirstOnlyBytes # Should have failed to parse, but didn't. +Recommended.Editions_Proto3.TextFormatInput.StringLiteralShortUnicodeEscapeSurrogateFirstOnlyString # Should have failed to parse, but didn't. +Recommended.Editions_Proto3.TextFormatInput.StringLiteralShortUnicodeEscapeSurrogatePairBytes # Should have failed to parse, but didn't. +Recommended.Editions_Proto3.TextFormatInput.StringLiteralShortUnicodeEscapeSurrogatePairString # Should have failed to parse, but didn't. +Recommended.Editions_Proto3.TextFormatInput.StringLiteralShortUnicodeEscapeSurrogateSecondOnlyBytes # Should have failed to parse, but didn't. +Recommended.Editions_Proto3.TextFormatInput.StringLiteralShortUnicodeEscapeSurrogateSecondOnlyString # Should have failed to parse, but didn't. +Recommended.Editions_Proto3.TextFormatInput.StringLiteralUnicodeEscapeSurrogatePairLongShortBytes # Should have failed to parse, but didn't. +Recommended.Editions_Proto3.TextFormatInput.StringLiteralUnicodeEscapeSurrogatePairLongShortString # Should have failed to parse, but didn't. +Recommended.Editions_Proto3.TextFormatInput.StringLiteralUnicodeEscapeSurrogatePairShortLongBytes # Should have failed to parse, but didn't. +Recommended.Editions_Proto3.TextFormatInput.StringLiteralUnicodeEscapeSurrogatePairShortLongString # Should have failed to parse, but didn't. +Recommended.Proto3.TextFormatInput.StringLiteralLongUnicodeEscapeSurrogateFirstOnlyBytes # Should have failed to parse, but didn't. +Recommended.Proto3.TextFormatInput.StringLiteralLongUnicodeEscapeSurrogateFirstOnlyString # Should have failed to parse, but didn't. +Recommended.Proto3.TextFormatInput.StringLiteralLongUnicodeEscapeSurrogatePairBytes # Should have failed to parse, but didn't. +Recommended.Proto3.TextFormatInput.StringLiteralLongUnicodeEscapeSurrogatePairString # Should have failed to parse, but didn't. +Recommended.Proto3.TextFormatInput.StringLiteralLongUnicodeEscapeSurrogateSecondOnlyBytes # Should have failed to parse, but didn't. +Recommended.Proto3.TextFormatInput.StringLiteralLongUnicodeEscapeSurrogateSecondOnlyString # Should have failed to parse, but didn't. +Recommended.Proto3.TextFormatInput.StringLiteralShortUnicodeEscapeSurrogateFirstOnlyBytes # Should have failed to parse, but didn't. +Recommended.Proto3.TextFormatInput.StringLiteralShortUnicodeEscapeSurrogateFirstOnlyString # Should have failed to parse, but didn't. +Recommended.Proto3.TextFormatInput.StringLiteralShortUnicodeEscapeSurrogatePairBytes # Should have failed to parse, but didn't. +Recommended.Proto3.TextFormatInput.StringLiteralShortUnicodeEscapeSurrogatePairString # Should have failed to parse, but didn't. +Recommended.Proto3.TextFormatInput.StringLiteralShortUnicodeEscapeSurrogateSecondOnlyBytes # Should have failed to parse, but didn't. +Recommended.Proto3.TextFormatInput.StringLiteralShortUnicodeEscapeSurrogateSecondOnlyString # Should have failed to parse, but didn't. +Recommended.Proto3.TextFormatInput.StringLiteralUnicodeEscapeSurrogatePairLongShortBytes # Should have failed to parse, but didn't. +Recommended.Proto3.TextFormatInput.StringLiteralUnicodeEscapeSurrogatePairLongShortString # Should have failed to parse, but didn't. +Recommended.Proto3.TextFormatInput.StringLiteralUnicodeEscapeSurrogatePairShortLongBytes # Should have failed to parse, but didn't. +Recommended.Proto3.TextFormatInput.StringLiteralUnicodeEscapeSurrogatePairShortLongString # Should have failed to parse, but didn't. +Required.Editions_Proto3.TextFormatInput.StringFieldBadUTF8Hex # Should have failed to parse, but didn't. +Required.Editions_Proto3.TextFormatInput.StringFieldBadUTF8Octal # Should have failed to parse, but didn't. +Required.Editions_Proto3.TextFormatInput.StringLiteralLongUnicodeEscapeTooLargeBytes # Should have failed to parse, but didn't. +Required.Editions_Proto3.TextFormatInput.StringLiteralLongUnicodeEscapeTooLargeString # Should have failed to parse, but didn't. +Required.Proto3.TextFormatInput.StringFieldBadUTF8Hex # Should have failed to parse, but didn't. +Required.Proto3.TextFormatInput.StringFieldBadUTF8Octal # Should have failed to parse, but didn't. +Required.Proto3.TextFormatInput.StringLiteralLongUnicodeEscapeTooLargeBytes # Should have failed to parse, but didn't. +Required.Proto3.TextFormatInput.StringLiteralLongUnicodeEscapeTooLargeString # Should have failed to parse, but didn't. + +# Hex literals with the high bit set are parsed as negative numbers instead of +# being rejected. (The same entries are in text_format_failure_list_cpp.txt, +# the list merged_runner_test and the open-source build pass to +# conformance_test_runner.)
diff --git a/conformance/filtering_test_runner.cc b/conformance/filtering_test_runner.cc new file mode 100644 index 0000000..d65df2f --- /dev/null +++ b/conformance/filtering_test_runner.cc
@@ -0,0 +1,53 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2026 Google LLC. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +#include "conformance/filtering_test_runner.h" + +#include <string> +#include <utility> +#include <vector> + +#include "absl/algorithm/container.h" +#include "absl/base/nullability.h" +#include "absl/container/flat_hash_set.h" +#include "absl/strings/string_view.h" +#include "conformance/conformance.pb.h" +#include "conformance/test_runner.h" + +namespace google { +namespace protobuf { +namespace conformance { + +FilteringTestRunner::FilteringTestRunner( + ConformanceTestRunner* absl_nonnull delegate, + absl::flat_hash_set<std::string> names_to_run) + : delegate_(delegate), names_to_run_(std::move(names_to_run)) {} + +FilteringTestRunner::~FilteringTestRunner() = default; + +std::string FilteringTestRunner::RunTest(absl::string_view test_name, + absl::string_view input) { + if (!names_to_run_.contains(test_name)) { + // Fully qualified: inside google::protobuf::conformance, a bare `conformance::` + // would name this namespace rather than the protos' one. + ::conformance::ConformanceResponse response; + response.set_skipped(kNotSelectedSkipReason); + return response.SerializeAsString(); + } + names_run_.insert(std::string(test_name)); + return delegate_->RunTest(test_name, input); +} + +std::vector<std::string> FilteringTestRunner::NamesRun() const { + std::vector<std::string> names_run(names_run_.begin(), names_run_.end()); + absl::c_sort(names_run); + return names_run; +} + +} // namespace conformance +} // namespace protobuf +} // namespace google
diff --git a/conformance/filtering_test_runner.h b/conformance/filtering_test_runner.h new file mode 100644 index 0000000..b7a4a1a --- /dev/null +++ b/conformance/filtering_test_runner.h
@@ -0,0 +1,68 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2026 Google LLC. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +#ifndef GOOGLE_PROTOBUF_CONFORMANCE_FILTERING_TEST_RUNNER_H__ +#define GOOGLE_PROTOBUF_CONFORMANCE_FILTERING_TEST_RUNNER_H__ + +#include <string> +#include <vector> + +#include "absl/base/nullability.h" +#include "absl/container/flat_hash_set.h" +#include "absl/strings/string_view.h" +#include "conformance/test_runner.h" + +namespace google { +namespace protobuf { +namespace conformance { + +// Decorates a ConformanceTestRunner so that only the tests named in a set are +// forwarded to the delegate. Any other test gets a ConformanceResponse with +// `skipped` set to kNotSelectedSkipReason, without contacting the delegate. +// So no testee process is ever started if none of the tests is selected. +// +// This implements the `--test <name>` isolation of conformance_test_runner +// for gtest-based conformance suites. The shared reason string is what lets +// the reporting side (Yields(), see matchers.h) tell such a test apart from +// one the testee itself opted out of. NamesRun() tells which of the +// requested names came up, so that the caller can tell which ones are still +// to be looked for elsewhere. +// +// The delegate is not owned and must outlive this object. +class FilteringTestRunner : public ConformanceTestRunner { + public: + // The `skipped` reason answered for every test that was not selected. It + // is not a testee skip: Yields() (matchers.h) passes such a response + // silently and only records that the test exists. A testee must never + // answer with it, or its skip would go unnoticed. + static constexpr absl::string_view kNotSelectedSkipReason = + "Not selected by --test."; + + FilteringTestRunner(ConformanceTestRunner* absl_nonnull delegate, + absl::flat_hash_set<std::string> names_to_run); + ~FilteringTestRunner() override; + + FilteringTestRunner(const FilteringTestRunner&) = delete; + FilteringTestRunner& operator=(const FilteringTestRunner&) = delete; + + std::string RunTest(absl::string_view test_name, + absl::string_view input) override; + + // The names forwarded to the delegate so far, in sorted order. + std::vector<std::string> NamesRun() const; + + private: + ConformanceTestRunner* absl_nonnull delegate_; + absl::flat_hash_set<std::string> names_to_run_; + absl::flat_hash_set<std::string> names_run_; +}; + +} // namespace conformance +} // namespace protobuf +} // namespace google + +#endif // GOOGLE_PROTOBUF_CONFORMANCE_FILTERING_TEST_RUNNER_H__
diff --git a/conformance/filtering_test_runner_test.cc b/conformance/filtering_test_runner_test.cc new file mode 100644 index 0000000..b698898 --- /dev/null +++ b/conformance/filtering_test_runner_test.cc
@@ -0,0 +1,95 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2026 Google LLC. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +#include "conformance/filtering_test_runner.h" + +#include <gmock/gmock.h> +#include <gtest/gtest.h> +#include "conformance/conformance.pb.h" +#include "conformance/mock_test_runner.h" + +namespace google { +namespace protobuf { +namespace conformance { +namespace { + +using ::testing::ElementsAre; +using ::testing::InSequence; +using ::testing::IsEmpty; +using ::testing::Return; +using ::testing::StrictMock; + +// The delegate is a strict mock throughout: a call the filter should not +// have forwarded fails the test. +TEST(FilteringTestRunnerTest, ForwardsSelectedTests) { + StrictMock<MockTestRunner> delegate; + FilteringTestRunner runner(&delegate, {"Test.One", "Test.Two"}); + + InSequence in_sequence; + EXPECT_CALL(delegate, RunTest("Test.One", "abc")).WillOnce(Return("one")); + EXPECT_CALL(delegate, RunTest("Test.Two", "")).WillOnce(Return("two")); + EXPECT_EQ(runner.RunTest("Test.One", "abc"), "one"); + EXPECT_EQ(runner.RunTest("Test.Two", ""), "two"); +} + +TEST(FilteringTestRunnerTest, SkipsUnselectedTestsWithoutDelegating) { + StrictMock<MockTestRunner> delegate; + FilteringTestRunner runner(&delegate, {"Test.One"}); + + ::conformance::ConformanceResponse response; + ASSERT_TRUE(response.ParseFromString(runner.RunTest("Test.Other", "abc"))); + EXPECT_EQ(response.result_case(), + ::conformance::ConformanceResponse::kSkipped); + // The reason Yields() (matchers.h) recognizes. + EXPECT_EQ(response.skipped(), "Not selected by --test."); +} + +TEST(FilteringTestRunnerTest, EmptySelectionSkipsEverything) { + StrictMock<MockTestRunner> delegate; + FilteringTestRunner runner(&delegate, {}); + + ::conformance::ConformanceResponse response; + ASSERT_TRUE(response.ParseFromString(runner.RunTest("Test.One", "abc"))); + EXPECT_EQ(response.result_case(), + ::conformance::ConformanceResponse::kSkipped); + EXPECT_THAT(runner.NamesRun(), IsEmpty()); +} + +TEST(FilteringTestRunnerTest, TracksNamesRunInSortedOrder) { + StrictMock<MockTestRunner> delegate; + FilteringTestRunner runner(&delegate, {"Test.C", "Test.A", "Test.B"}); + + EXPECT_THAT(runner.NamesRun(), IsEmpty()); + + InSequence in_sequence; + EXPECT_CALL(delegate, RunTest("Test.C", "")); + EXPECT_CALL(delegate, RunTest("Test.B", "")); + EXPECT_CALL(delegate, RunTest("Test.C", "")); + runner.RunTest("Test.C", ""); + runner.RunTest("Test.Other", ""); + runner.RunTest("Test.B", ""); + // Running a test twice is fine and counts once. + runner.RunTest("Test.C", ""); + + EXPECT_THAT(runner.NamesRun(), ElementsAre("Test.B", "Test.C")); +} + +TEST(FilteringTestRunnerTest, MatchesExactNamesOnly) { + StrictMock<MockTestRunner> delegate; + FilteringTestRunner runner(&delegate, {"Test.One"}); + + runner.RunTest("Test.On", ""); + runner.RunTest("Test.One.More", ""); + runner.RunTest("test.one", ""); + + EXPECT_THAT(runner.NamesRun(), IsEmpty()); +} + +} // namespace +} // namespace conformance +} // namespace protobuf +} // namespace google
diff --git a/conformance/forked_testee.cc b/conformance/forked_testee.cc new file mode 100644 index 0000000..30a864e --- /dev/null +++ b/conformance/forked_testee.cc
@@ -0,0 +1,35 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2026 Google LLC. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +// The default testee of a conformance test binary: --testee_binary, run as a +// child process (see testee_runner.h). + +#include <memory> +#include <string> + +#include "absl/log/absl_check.h" +#include "absl/strings/string_view.h" +#include "absl/types/span.h" +#include "conformance/fork_pipe_runner.h" +#include "conformance/test_runner.h" +#include "conformance/testee_runner.h" + +namespace google { +namespace protobuf { +namespace conformance { + +std::unique_ptr<ConformanceTestRunner> MakeTesteeRunner( + absl::string_view testee_binary, + absl::Span<const std::string> testee_args) { + ABSL_QCHECK(!testee_binary.empty()) << "--testee_binary is required."; + // Spawns the testee lazily, on the first request. + return std::make_unique<ForkPipeRunner>(testee_binary, testee_args); +} + +} // namespace conformance +} // namespace protobuf +} // namespace google
diff --git a/conformance/forked_testee_test.cc b/conformance/forked_testee_test.cc new file mode 100644 index 0000000..cb870e7 --- /dev/null +++ b/conformance/forked_testee_test.cc
@@ -0,0 +1,50 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2026 Google LLC. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +#include <memory> +#include <string> +#include <vector> + +#include <gtest/gtest.h> +#include "conformance/test_runner.h" +#include "conformance/testee_runner.h" + +namespace google { +namespace protobuf { +namespace conformance { +namespace { + +// TODO: cover Windows too, with a portable stand-in testee. +#ifndef _WIN32 +TEST(ForkedTesteeTest, RunsTheTesteeBinaryWithItsArgs) { + // `cat` echoes the length-prefixed request straight back, so a round trip + // shows that --testee_binary and --testee_args reached a child process. + const std::vector<std::string> args = {"-c", "exec cat"}; + std::unique_ptr<ConformanceTestRunner> runner = + MakeTesteeRunner("/bin/sh", args); + ASSERT_NE(runner, nullptr); + EXPECT_EQ(runner->RunTest("SomeTest", "request bytes"), "request bytes"); +} +#endif // !_WIN32 + +TEST(ForkedTesteeTest, AcceptsANonexistentBinary) { + // MakeTesteeRunner() only builds the runner; ForkPipeRunner spawns the + // testee on the first request, so the binary need not even exist until a + // test runs (--gtest_list_tests and an all-skipped run never do). + std::unique_ptr<ConformanceTestRunner> runner = + MakeTesteeRunner("/no/such/testee", {}); + EXPECT_NE(runner, nullptr); +} + +TEST(ForkedTesteeDeathTest, RequiresATesteeBinary) { + EXPECT_DEATH(MakeTesteeRunner("", {}), "--testee_binary is required"); +} + +} // namespace +} // namespace conformance +} // namespace protobuf +} // namespace google
diff --git a/conformance/global_test_environment.h b/conformance/global_test_environment.h index 8742960..45e658b 100644 --- a/conformance/global_test_environment.h +++ b/conformance/global_test_environment.h
@@ -5,7 +5,7 @@ // license that can be found in the LICENSE file or at // https://developers.google.com/open-source/licenses/bsd -// Declares the process-wide state that the Yields() matcher depends on (see +// Declares the process-wide state that the Yields() matcher reads (see // matchers.h). The definition is deliberately not provided here. It is the // link-time seam between the matchers and the process that hosts them. The // conformance test binary provides it from its global test environment, @@ -15,18 +15,20 @@ #ifndef GOOGLE_PROTOBUF_CONFORMANCE_GLOBAL_TEST_ENVIRONMENT_H__ #define GOOGLE_PROTOBUF_CONFORMANCE_GLOBAL_TEST_ENVIRONMENT_H__ -#include "conformance/test_manager.h" +#include "conformance/failure_list.h" namespace google { namespace protobuf { namespace conformance { namespace internal { -// Returns the process-wide TestManager that records test results against the -// expected-failure list. Yields() reports the outcome of every test here. -// That lets the failure list be validated and regenerated, and the enforcement -// level be applied (see TestManager::set_enforcement_level()). -TestManager& GetGlobalTestManager(); +// Returns the process-wide expected-failure list. Yields() asks it for the +// verdict on every test, which applies the failure list and the enforcement +// level (see FailureList::set_enforcement_level()). The test environment, +// which owns the list as part of its ResultLedger, is what tallies the +// outcomes (see ConformanceEnvironment in test_environment.h); the matchers +// never change anything. +const FailureList& GetGlobalFailureList(); } // namespace internal } // namespace conformance
diff --git a/conformance/in_process_test_runner.cc b/conformance/in_process_test_runner.cc new file mode 100644 index 0000000..342b7d3 --- /dev/null +++ b/conformance/in_process_test_runner.cc
@@ -0,0 +1,57 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2026 Google LLC. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +#include "conformance/in_process_test_runner.h" + +#include <string> +#include <utility> + +#include "absl/status/statusor.h" +#include "absl/strings/str_cat.h" +#include "absl/strings/string_view.h" +#include "conformance/conformance.pb.h" + +namespace google { +namespace protobuf { +namespace conformance { +namespace { + +// Returns a serialized ConformanceResponse whose `runtime_error` is `message`. +std::string RuntimeError(absl::string_view message) { + ::conformance::ConformanceResponse response; + response.set_runtime_error(message); + return response.SerializeAsString(); +} + +} // namespace + +InProcessTestRunner::InProcessTestRunner(RequestHandler handler) + : handler_(std::move(handler)) {} + +InProcessTestRunner::~InProcessTestRunner() = default; + +std::string InProcessTestRunner::RunTest(absl::string_view test_name, + absl::string_view input) { + ::conformance::ConformanceRequest request; + if (!request.ParseFromString(input)) { + return RuntimeError(absl::StrCat( + "failed to parse ConformanceRequest for test ", test_name)); + } + + absl::StatusOr<::conformance::ConformanceResponse> response = + handler_(request); + if (!response.ok()) { + return RuntimeError(absl::StrCat("request handler failed for test ", + test_name, ": ", + response.status().ToString())); + } + return response->SerializeAsString(); +} + +} // namespace conformance +} // namespace protobuf +} // namespace google
diff --git a/conformance/in_process_test_runner.h b/conformance/in_process_test_runner.h new file mode 100644 index 0000000..0a7c084 --- /dev/null +++ b/conformance/in_process_test_runner.h
@@ -0,0 +1,56 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2026 Google LLC. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +#ifndef GOOGLE_PROTOBUF_CONFORMANCE_IN_PROCESS_TEST_RUNNER_H__ +#define GOOGLE_PROTOBUF_CONFORMANCE_IN_PROCESS_TEST_RUNNER_H__ + +#include <string> + +#include "absl/functional/any_invocable.h" +#include "absl/status/statusor.h" +#include "absl/strings/string_view.h" +#include "conformance/conformance.pb.h" +#include "conformance/test_runner.h" + +namespace google { +namespace protobuf { +namespace conformance { + +// A ConformanceTestRunner that dispatches each request to an in-process +// callback instead of a testee subprocess. Parses the serialized request, +// invokes `handler`, and serializes the response. +// +// If the request cannot be parsed, or `handler` returns a non-OK status, the +// returned response has `runtime_error` set describing the failure. +// +// Any state referenced by `handler` (e.g. a captured harness) must outlive the +// runner. Like every other ConformanceTestRunner, this class is meant for +// single-threaded use: RunTest is not thread-safe if `handler` mutates state. +class InProcessTestRunner : public ConformanceTestRunner { + public: + using RequestHandler = + absl::AnyInvocable<absl::StatusOr<::conformance::ConformanceResponse>( + const ::conformance::ConformanceRequest&)>; + + explicit InProcessTestRunner(RequestHandler handler); + ~InProcessTestRunner() override; + + InProcessTestRunner(const InProcessTestRunner&) = delete; + InProcessTestRunner& operator=(const InProcessTestRunner&) = delete; + + std::string RunTest(absl::string_view test_name, + absl::string_view input) override; + + private: + RequestHandler handler_; +}; + +} // namespace conformance +} // namespace protobuf +} // namespace google + +#endif // GOOGLE_PROTOBUF_CONFORMANCE_IN_PROCESS_TEST_RUNNER_H__
diff --git a/conformance/in_process_test_runner_test.cc b/conformance/in_process_test_runner_test.cc new file mode 100644 index 0000000..60e6498 --- /dev/null +++ b/conformance/in_process_test_runner_test.cc
@@ -0,0 +1,132 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2026 Google LLC. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +#include "conformance/in_process_test_runner.h" + +#include <string> + +#include <gmock/gmock.h> +#include <gtest/gtest.h> +#include "absl/log/absl_check.h" +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "absl/strings/string_view.h" +#include "conformance/binary_wireformat.h" +#include "conformance/conformance.pb.h" +#include "google/protobuf/test_textproto.h" + +namespace google { +namespace protobuf { +namespace conformance { +namespace { + +using ::conformance::ConformanceRequest; +using ::conformance::ConformanceResponse; +using ::testing::AllOf; +using ::testing::HasSubstr; + +// Parses the serialized ConformanceResponse returned by a runner, aborting the +// test if the runner produced bytes that are not a valid response. +ConformanceResponse ParseResponse(absl::string_view serialized) { + ConformanceResponse response; + ABSL_CHECK(response.ParseFromString(serialized)) + << "runner returned an unparseable ConformanceResponse"; + return response; +} + +TEST(InProcessTestRunnerTest, HandlerReceivesParsedRequest) { + ConformanceRequest received; + InProcessTestRunner runner([&received](const ConformanceRequest& request) + -> absl::StatusOr<ConformanceResponse> { + received = request; + return ConformanceResponse(); + }); + ConformanceRequest request = ParseTextOrDie(R"pb( + message_type: "some.Message" + requested_output_format: TEXT_FORMAT + protobuf_payload: "payload" + )pb"); + + runner.RunTest("Suite.Test", request.SerializeAsString()); + + EXPECT_THAT(received, EqualsProto(R"pb( + message_type: "some.Message" + requested_output_format: TEXT_FORMAT + protobuf_payload: "payload" + )pb")); +} + +TEST(InProcessTestRunnerTest, HandlerResponseIsSerialized) { + InProcessTestRunner runner( + [](const ConformanceRequest&) -> absl::StatusOr<ConformanceResponse> { + ConformanceResponse response; + response.set_text_payload("optional_int32: 1\n"); + return response; + }); + + std::string serialized = + runner.RunTest("Suite.Test", ConformanceRequest().SerializeAsString()); + + EXPECT_THAT(ParseResponse(serialized), + EqualsProto(R"pb(text_payload: "optional_int32: 1\n")pb")); +} + +TEST(InProcessTestRunnerTest, HandlerErrorBecomesRuntimeError) { + InProcessTestRunner runner( + [](const ConformanceRequest&) -> absl::StatusOr<ConformanceResponse> { + return absl::NotFoundError("no such message type"); + }); + + ConformanceResponse response = ParseResponse( + runner.RunTest("Suite.Test", ConformanceRequest().SerializeAsString())); + + ASSERT_EQ(response.result_case(), ConformanceResponse::kRuntimeError); + EXPECT_THAT( + response.runtime_error(), + AllOf(HasSubstr("Suite.Test"), HasSubstr("no such message type"))); +} + +TEST(InProcessTestRunnerTest, UnparseableRequestBecomesRuntimeError) { + bool handler_called = false; + InProcessTestRunner runner([&handler_called](const ConformanceRequest&) + -> absl::StatusOr<ConformanceResponse> { + handler_called = true; + return ConformanceResponse(); + }); + + // Field 1, length-delimited, claims 5 bytes but only 3 follow. + ConformanceResponse response = ParseResponse(runner.RunTest( + "Suite.Test", + Wire(Tag(1, WireType::kLengthPrefixed), Varint(5), "abc").str())); + + EXPECT_FALSE(handler_called); + ASSERT_EQ(response.result_case(), ConformanceResponse::kRuntimeError); + EXPECT_THAT(response.runtime_error(), + AllOf(HasSubstr("Suite.Test"), + HasSubstr("failed to parse ConformanceRequest"))); +} + +TEST(InProcessTestRunnerTest, HandlerMayBeStateful) { + InProcessTestRunner runner([calls = 0](const ConformanceRequest&) mutable + -> absl::StatusOr<ConformanceResponse> { + ConformanceResponse response; + response.set_text_payload(std::to_string(++calls)); + return response; + }); + std::string request = ConformanceRequest().SerializeAsString(); + + runner.RunTest("Suite.First", request); + ConformanceResponse response = + ParseResponse(runner.RunTest("Suite.Second", request)); + + EXPECT_THAT(response, EqualsProto(R"pb(text_payload: "2")pb")); +} + +} // namespace +} // namespace conformance +} // namespace protobuf +} // namespace google
diff --git a/conformance/in_process_testee_test.cc b/conformance/in_process_testee_test.cc new file mode 100644 index 0000000..70ce1d7 --- /dev/null +++ b/conformance/in_process_testee_test.cc
@@ -0,0 +1,111 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2026 Google LLC. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +// A conformance test binary with an in-process testee: this file defines +// MakeTesteeRunner() (see testee_runner.h) and links :test_environment_main +// without :forked_testee. It is run without --testee_binary (see BUILD), +// which a forked testee would refuse, and checks that the main hands the +// suites the linked testee. + +#include <memory> +#include <string> +#include <vector> + +#include <gmock/gmock.h> +#include <gtest/gtest.h> +#include "absl/log/absl_check.h" +#include "absl/strings/string_view.h" +#include "absl/types/span.h" +#include "conformance/binary_wireformat.h" +#include "conformance/conformance.pb.h" +#include "conformance/matchers.h" +#include "conformance/test_fixture.h" +#include "conformance/test_runner.h" +#include "conformance/testee_runner.h" +#include "google/protobuf/test_messages_proto3.pb.h" + +namespace google { +namespace protobuf { +namespace conformance { +namespace { + +using ::conformance::ConformanceRequest; +using ::conformance::ConformanceResponse; +using ::protobuf_test_messages::proto3::TestAllTypesProto3; +using ::testing::IsEmpty; + +// What test_environment_main passed to MakeTesteeRunner(), and how many +// requests the testee it got back has answered. +struct LinkedTesteeState { + bool created = false; + std::string testee_binary; + std::vector<std::string> testee_args; + int requests_answered = 0; +}; + +LinkedTesteeState& State() { + static auto* const state = new LinkedTesteeState; + return *state; +} + +// An in-process testee that "parses" a binary payload by echoing it back, so +// the suites see a conforming response without any real parsing. +class EchoTestee : public ConformanceTestRunner { + public: + std::string RunTest(absl::string_view test_name, + absl::string_view serialized_request) override { + ConformanceRequest request; + ABSL_CHECK(request.ParseFromString(serialized_request)) << test_name; + ConformanceResponse response; + if (request.has_protobuf_payload() && + request.requested_output_format() == ::conformance::PROTOBUF) { + response.set_protobuf_payload(request.protobuf_payload()); + } else { + response.set_skipped("EchoTestee only echoes binary payloads."); + } + ++State().requests_answered; + return response.SerializeAsString(); + } +}; + +} // namespace + +std::unique_ptr<ConformanceTestRunner> MakeTesteeRunner( + absl::string_view testee_binary, + absl::Span<const std::string> testee_args) { + State().created = true; + State().testee_binary = std::string(testee_binary); + State().testee_args.assign(testee_args.begin(), testee_args.end()); + return std::make_unique<EchoTestee>(); +} + +namespace { + +using InProcessTesteeTest = ConformanceTest; + +TEST_F(InProcessTesteeTest, MainCreatesTheLinkedTesteeFromEmptyFlags) { + // Run without --testee_binary or --testee_args, which is how a test with an + // in-process testee is invoked (see conformance.bzl). + EXPECT_TRUE(State().created); + EXPECT_THAT(State().testee_binary, IsEmpty()); + EXPECT_THAT(State().testee_args, IsEmpty()); +} + +TEST_F(InProcessTesteeTest, SuitesTalkToTheLinkedTestee) { + const int requests_before = State().requests_answered; + EXPECT_THAT( + Testee() + .ParseBinary(TestAllTypesProto3::descriptor(), VarintField(1, 99)) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(R"pb(optional_int32: 99)pb")))); + EXPECT_EQ(State().requests_answered, requests_before + 1); +} + +} // namespace +} // namespace conformance +} // namespace protobuf +} // namespace google
diff --git a/conformance/json_any_test.cc b/conformance/json_any_test.cc new file mode 100644 index 0000000..68616af --- /dev/null +++ b/conformance/json_any_test.cc
@@ -0,0 +1,374 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2026 Google LLC. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +// JSON conformance tests for google.protobuf.Any fields: the "@type" member +// (in any position, or nested in another Any), the well-known types whose +// JSON form is a "value" member, the type URLs that must be rejected, an Any +// without a type, `null`, and an empty packed message. This replaces the +// legacy BinaryAndJsonConformanceSuiteImpl<M>::RunJsonTestsForAny(), which ran +// for the proto3-style message types only; the test names and the requests +// sent to the testee are identical to the legacy ones. +// +// Like the legacy RunValidJsonTest(), each valid input is sent twice: once to +// be serialized as binary ("<name>.ProtobufOutput") and once as JSON +// ("<name>.JsonOutput"). ExpectParseFailureForJson() asked for JSON output +// under a name without an output suffix. The inputs are the legacy raw +// string literals verbatim; the ones packing the test message itself get its +// type URL substituted per message type, as in the legacy suite. + +#include <string> + +#include <gmock/gmock.h> +#include <gtest/gtest.h> +#include "absl/strings/str_cat.h" +#include "absl/strings/string_view.h" +#include "absl/strings/substitute.h" +#include "conformance/binary_test_util.h" +#include "conformance/matchers.h" +#include "conformance/message_type_fixtures.h" +#include "conformance/testee.h" + +namespace google { +namespace protobuf { +namespace conformance { +namespace { + +using ::testing::ValuesIn; + +using JsonAnyTest = MessageTypeConformanceTest; + +TEST_P(JsonAnyTest, Any) { + const std::string type_url = + absl::StrCat("type.googleapis.com/", message()->full_name()); + const std::string input = absl::Substitute(R"({ + "optionalAny": { + "@type": "$0", + "optionalInt32": 12345 + } + })", + type_url); + const std::string expected = absl::Substitute(R"( + optional_any: { + [$0] { + optional_int32: 12345 + } + } + )", + type_url); + EXPECT_THAT(Testee("Any").ParseJson(message(), input).SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(expected)))); + EXPECT_THAT(Testee("Any").ParseJson(message(), input).SerializeJson(), + Yields(WhenParsed(EqualsTextProto(expected)))); +} + +TEST_P(JsonAnyTest, AnyNested) { + const std::string type_url = + absl::StrCat("type.googleapis.com/", message()->full_name()); + const std::string input = absl::Substitute(R"({ + "optionalAny": { + "@type": "type.googleapis.com/google.protobuf.Any", + "value": { + "@type": "$0", + "optionalInt32": 12345 + } + } + })", + type_url); + const std::string expected = absl::Substitute(R"( + optional_any: { + [type.googleapis.com/google.protobuf.Any] { + [$0] { + optional_int32: 12345 + } + } + } + )", + type_url); + EXPECT_THAT(Testee("AnyNested").ParseJson(message(), input).SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(expected)))); + EXPECT_THAT(Testee("AnyNested").ParseJson(message(), input).SerializeJson(), + Yields(WhenParsed(EqualsTextProto(expected)))); +} + +// The special "@type" tag is not required to appear first. +TEST_P(JsonAnyTest, AnyUnorderedTypeTag) { + const std::string type_url = + absl::StrCat("type.googleapis.com/", message()->full_name()); + const std::string input = absl::Substitute(R"({ + "optionalAny": { + "optionalInt32": 12345, + "@type": "$0" + } + })", + type_url); + const std::string expected = absl::Substitute(R"( + optional_any: { + [$0] { + optional_int32: 12345 + } + } + )", + type_url); + EXPECT_THAT(Testee("AnyUnorderedTypeTag") + .ParseJson(message(), input) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(expected)))); + EXPECT_THAT( + Testee("AnyUnorderedTypeTag").ParseJson(message(), input).SerializeJson(), + Yields(WhenParsed(EqualsTextProto(expected)))); +} + +// Well-known types in Any. +TEST_P(JsonAnyTest, AnyWithInt32ValueWrapper) { + constexpr absl::string_view kInput = R"json({ + "optionalAny": { + "@type": "type.googleapis.com/google.protobuf.Int32Value", + "value": 12345 + } + })json"; + constexpr absl::string_view kExpected = R"pb( + optional_any: { + [type.googleapis.com/google.protobuf.Int32Value] { value: 12345 } + } + )pb"; + EXPECT_THAT(Testee("AnyWithInt32ValueWrapper") + .ParseJson(message(), kInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT(Testee("AnyWithInt32ValueWrapper") + .ParseJson(message(), kInput) + .SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +TEST_P(JsonAnyTest, AnyWithDuration) { + constexpr absl::string_view kInput = R"json({ + "optionalAny": { + "@type": "type.googleapis.com/google.protobuf.Duration", + "value": "1.5s" + } + })json"; + constexpr absl::string_view kExpected = R"pb( + optional_any: { + [type.googleapis.com/google.protobuf.Duration] { + seconds: 1 + nanos: 500000000 + } + } + )pb"; + EXPECT_THAT( + Testee("AnyWithDuration").ParseJson(message(), kInput).SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT( + Testee("AnyWithDuration").ParseJson(message(), kInput).SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +TEST_P(JsonAnyTest, AnyWithTimestamp) { + constexpr absl::string_view kInput = R"json({ + "optionalAny": { + "@type": "type.googleapis.com/google.protobuf.Timestamp", + "value": "1970-01-01T00:00:00Z" + } + })json"; + constexpr absl::string_view kExpected = R"pb( + optional_any: { + [type.googleapis.com/google.protobuf.Timestamp] { seconds: 0 nanos: 0 } + } + )pb"; + EXPECT_THAT( + Testee("AnyWithTimestamp").ParseJson(message(), kInput).SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT( + Testee("AnyWithTimestamp").ParseJson(message(), kInput).SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +TEST_P(JsonAnyTest, AnyWithFieldMask) { + constexpr absl::string_view kInput = R"json({ + "optionalAny": { + "@type": "type.googleapis.com/google.protobuf.FieldMask", + "value": "foo,barBaz" + } + })json"; + constexpr absl::string_view kExpected = R"pb( + optional_any: { + [type.googleapis.com/google.protobuf.FieldMask] { + paths: [ "foo", "bar_baz" ] + } + } + )pb"; + EXPECT_THAT( + Testee("AnyWithFieldMask").ParseJson(message(), kInput).SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT( + Testee("AnyWithFieldMask").ParseJson(message(), kInput).SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +TEST_P(JsonAnyTest, AnyWithStruct) { + constexpr absl::string_view kInput = R"json({ + "optionalAny": { + "@type": "type.googleapis.com/google.protobuf.Struct", + "value": { + "foo": 1 + } + } + })json"; + constexpr absl::string_view kExpected = R"pb( + optional_any: { + [type.googleapis.com/google.protobuf.Struct] { + fields: { + key: "foo" + value: { number_value: 1 } + } + } + } + )pb"; + EXPECT_THAT( + Testee("AnyWithStruct").ParseJson(message(), kInput).SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT( + Testee("AnyWithStruct").ParseJson(message(), kInput).SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +TEST_P(JsonAnyTest, AnyWithValueForJsonObject) { + constexpr absl::string_view kInput = R"json({ + "optionalAny": { + "@type": "type.googleapis.com/google.protobuf.Value", + "value": { + "foo": 1 + } + } + })json"; + constexpr absl::string_view kExpected = R"pb( + optional_any: { + [type.googleapis.com/google.protobuf.Value] { + struct_value: { + fields: { + key: "foo" + value: { number_value: 1 } + } + } + } + } + )pb"; + EXPECT_THAT(Testee("AnyWithValueForJsonObject") + .ParseJson(message(), kInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT(Testee("AnyWithValueForJsonObject") + .ParseJson(message(), kInput) + .SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +TEST_P(JsonAnyTest, AnyWithValueForInteger) { + constexpr absl::string_view kInput = R"json({ + "optionalAny": { + "@type": "type.googleapis.com/google.protobuf.Value", + "value": 1 + } + })json"; + constexpr absl::string_view kExpected = R"pb( + optional_any: { + [type.googleapis.com/google.protobuf.Value] { number_value: 1 } + } + )pb"; + EXPECT_THAT(Testee("AnyWithValueForInteger") + .ParseJson(message(), kInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT(Testee("AnyWithValueForInteger") + .ParseJson(message(), kInput) + .SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +// When the Any is in WKT form (with "@type"), the type_url must be present +// and URL shaped, otherwise it should be a parse error (because it can't be +// parsed into the Any schema). +TEST_P(JsonAnyTest, AnyWktRepresentationWithEmptyTypeAndValue) { + EXPECT_THAT(Testee("AnyWktRepresentationWithEmptyTypeAndValue") + .ParseJson(message(), R"json({ + "optionalAny": { + "@type": "", + "value": "" + } + })json") + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(JsonAnyTest, AnyWktRepresentationWithBadType) { + EXPECT_THAT(Testee("AnyWktRepresentationWithBadType") + .ParseJson(message(), R"json({ + "optionalAny": { + "@type": "not_a_url", + "value": "" + } + })json") + .ParseOnly(), + Yields(IsParseError())); +} + +// When the Any can be parsed as non-WKT form, the type_url could be missing +// or invalid, since that can still be parsed into the Any schema. +TEST_P(JsonAnyTest, AnyWithNoType) { + constexpr absl::string_view kInput = R"json({ + "optionalAny": {} + })json"; + constexpr absl::string_view kExpected = R"pb(optional_any: {})pb"; + EXPECT_THAT( + Testee("AnyWithNoType").ParseJson(message(), kInput).SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT( + Testee("AnyWithNoType").ParseJson(message(), kInput).SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +// `null` where an Any exists should just result in the field being unset. +TEST_P(JsonAnyTest, AnyNull) { + constexpr absl::string_view kInput = R"json({ + "optionalAny": null + })json"; + constexpr absl::string_view kExpected = ""; + EXPECT_THAT(Testee("AnyNull").ParseJson(message(), kInput).SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT(Testee("AnyNull").ParseJson(message(), kInput).SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +// google.protobuf.Empty packed into an Any, implementations must accept it +// without the "value" field set. This also confirms that what they round trip +// does not have `"value":{}` set on it, since the test harness uses the C++ +// JSON parser which will reject it. +TEST_P(JsonAnyTest, AnyEmpty) { + constexpr absl::string_view kInput = R"json({ + "optionalAny": { + "@type": "type.googleapis.com/google.protobuf.Empty" + } + })json"; + constexpr absl::string_view kExpected = R"pb( + optional_any: { + [type.googleapis.com/google.protobuf.Empty] {} + } + )pb"; + EXPECT_THAT(Testee("AnyEmpty").ParseJson(message(), kInput).SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT(Testee("AnyEmpty").ParseJson(message(), kInput).SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +INSTANTIATE_TEST_SUITE_P(All, JsonAnyTest, ValuesIn(Proto3TestMessageTypes()), + MessageTypeParamName); + +} // namespace +} // namespace conformance +} // namespace protobuf +} // namespace google
diff --git a/conformance/json_bool_test.cc b/conformance/json_bool_test.cc new file mode 100644 index 0000000..a8b1afc --- /dev/null +++ b/conformance/json_bool_test.cc
@@ -0,0 +1,131 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2026 Google LLC. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +// JSON conformance tests for bool fields: `true` / `false` round-trip, and +// every other spelling (0, 1, True, TRUE, "true", ...) is rejected. This +// replaces the "Bool fields" block of the legacy +// BinaryAndJsonConformanceSuiteImpl<M>::RunJsonTestsForNonRepeatedTypes(); the +// test names and the requests sent to the testee are identical to the legacy +// ones. +// +// Like the legacy RunValidJsonTest(), each valid input is sent twice: once to +// be serialized as binary ("<name>.ProtobufOutput") and once as JSON +// ("<name>.JsonOutput"). ExpectParseFailureForJson() asked for JSON output +// under a name without an output suffix. + +#include <gmock/gmock.h> +#include <gtest/gtest.h> +#include "absl/strings/string_view.h" +#include "conformance/binary_test_util.h" +#include "conformance/matchers.h" +#include "conformance/message_type_fixtures.h" +#include "conformance/testee.h" + +namespace google { +namespace protobuf { +namespace conformance { +namespace { + +using ::testing::ValuesIn; + +class JsonBoolTest : public MessageTypeConformanceTest { + private: + TestPriority DefaultPriority() const override { return kP1; } +}; + +TEST_P(JsonBoolTest, BoolFieldTrue) { + constexpr absl::string_view kInput = R"json({"optionalBool":true})json"; + constexpr absl::string_view kExpected = R"pb(optional_bool: true)pb"; + EXPECT_THAT(Testee(kP0, "BoolFieldTrue") + .ParseJson(message(), kInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT( + Testee(kP0, "BoolFieldTrue").ParseJson(message(), kInput).SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +TEST_P(JsonBoolTest, BoolFieldFalse) { + constexpr absl::string_view kInput = R"json({"optionalBool":false})json"; + constexpr absl::string_view kExpected = R"pb(optional_bool: false)pb"; + EXPECT_THAT(Testee(kP0, "BoolFieldFalse") + .ParseJson(message(), kInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT(Testee(kP0, "BoolFieldFalse") + .ParseJson(message(), kInput) + .SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +// --------------------------------------------------------------------------- +// Other forms are not allowed. +// --------------------------------------------------------------------------- + +TEST_P(JsonBoolTest, BoolFieldIntegerZero) { + EXPECT_THAT(Testee("BoolFieldIntegerZero") + .ParseJson(message(), R"json({"optionalBool":0})json") + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(JsonBoolTest, BoolFieldIntegerOne) { + EXPECT_THAT(Testee("BoolFieldIntegerOne") + .ParseJson(message(), R"json({"optionalBool":1})json") + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(JsonBoolTest, BoolFieldCamelCaseTrue) { + EXPECT_THAT(Testee("BoolFieldCamelCaseTrue") + .ParseJson(message(), R"json({"optionalBool":True})json") + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(JsonBoolTest, BoolFieldCamelCaseFalse) { + EXPECT_THAT(Testee("BoolFieldCamelCaseFalse") + .ParseJson(message(), R"json({"optionalBool":False})json") + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(JsonBoolTest, BoolFieldAllCapitalTrue) { + EXPECT_THAT(Testee("BoolFieldAllCapitalTrue") + .ParseJson(message(), R"json({"optionalBool":TRUE})json") + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(JsonBoolTest, BoolFieldAllCapitalFalse) { + EXPECT_THAT(Testee("BoolFieldAllCapitalFalse") + .ParseJson(message(), R"json({"optionalBool":FALSE})json") + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(JsonBoolTest, BoolFieldDoubleQuotedTrue) { + EXPECT_THAT(Testee("BoolFieldDoubleQuotedTrue") + .ParseJson(message(), R"json({"optionalBool":"true"})json") + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(JsonBoolTest, BoolFieldDoubleQuotedFalse) { + EXPECT_THAT(Testee("BoolFieldDoubleQuotedFalse") + .ParseJson(message(), R"json({"optionalBool":"false"})json") + .ParseOnly(), + Yields(IsParseError())); +} + +INSTANTIATE_TEST_SUITE_P(All, JsonBoolTest, ValuesIn(AllTestMessageTypes()), + MessageTypeParamName); + +} // namespace +} // namespace conformance +} // namespace protobuf +} // namespace google
diff --git a/conformance/json_duration_test.cc b/conformance/json_duration_test.cc new file mode 100644 index 0000000..ae9db0a --- /dev/null +++ b/conformance/json_duration_test.cc
@@ -0,0 +1,276 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2026 Google LLC. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +// JSON conformance tests for google.protobuf.Duration: its "<seconds>s" +// string form with up to nine fractional digits, its range, the rejection of +// out-of-range or malformed values in either direction, and the number of +// fractional digits a serializer emits. This replaces the Duration part of +// the legacy +// BinaryAndJsonConformanceSuiteImpl<M>::RunJsonTestsForWrapperTypes(), which +// ran for the proto3-style message types only; the test names and the requests +// sent to the testee are identical to the legacy ones, except that the +// binary-input tests below are BINARY_TEST requests (see below). +// +// Like the legacy RunValidJsonTest(), each valid input is sent twice: once to +// be serialized as binary ("<name>.ProtobufOutput") and once as JSON +// ("<name>.JsonOutput"). ExpectParseFailureForJson() asked for JSON output +// under a name without an output suffix; ExpectSerializeFailureForJson() sent +// a binary message (built here from its wire format) to be serialized as +// JSON ("ProtobufInput.<name>.JsonOutput"), categorized as JSON_TEST; here +// ParseBinary() categorizes them as BINARY_TEST, which no testee tells apart +// from JSON_TEST on a binary payload (b/563657722). And the +// legacy "validators" (RunValidJsonTestWithValidator()) looked at the +// serialized JSON text itself. + +#include <cstdint> + +#include <gmock/gmock.h> +#include <gtest/gtest.h> +#include "absl/strings/string_view.h" +#include "conformance/binary_test_util.h" +#include "conformance/binary_wireformat.h" +#include "conformance/json_test_util.h" +#include "conformance/matchers.h" +#include "conformance/message_type_fixtures.h" +#include "conformance/testee.h" + +namespace google { +namespace protobuf { +namespace conformance { +namespace { + +using ::testing::ValuesIn; + +using JsonDurationTest = MessageTypeConformanceTest; + +// optional_duration is field 301 of the test message types; Duration's +// seconds is field 1 and nanos field 2. +constexpr uint32_t kOptionalDuration = 301; +constexpr uint32_t kSeconds = 1; +constexpr uint32_t kNanos = 2; + +TEST_P(JsonDurationTest, DurationMinValue) { + constexpr absl::string_view kInput = + R"json({"optionalDuration": "-315576000000.999999999s"})json"; + constexpr absl::string_view kExpected = + R"pb(optional_duration: { seconds: -315576000000 nanos: -999999999 })pb"; + EXPECT_THAT( + Testee("DurationMinValue").ParseJson(message(), kInput).SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT( + Testee("DurationMinValue").ParseJson(message(), kInput).SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +TEST_P(JsonDurationTest, DurationMaxValue) { + constexpr absl::string_view kInput = + R"json({"optionalDuration": "315576000000.999999999s"})json"; + constexpr absl::string_view kExpected = + R"pb(optional_duration: { seconds: 315576000000 nanos: 999999999 })pb"; + EXPECT_THAT( + Testee("DurationMaxValue").ParseJson(message(), kInput).SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT( + Testee("DurationMaxValue").ParseJson(message(), kInput).SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +TEST_P(JsonDurationTest, DurationRepeatedValue) { + constexpr absl::string_view kInput = + R"json({"repeatedDuration": ["1.5s", "-1.5s"]})json"; + constexpr absl::string_view kExpected = R"pb( + repeated_duration: { seconds: 1 nanos: 500000000 } + repeated_duration: { seconds: -1 nanos: -500000000 } + )pb"; + EXPECT_THAT(Testee("DurationRepeatedValue") + .ParseJson(message(), kInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT(Testee("DurationRepeatedValue") + .ParseJson(message(), kInput) + .SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +TEST_P(JsonDurationTest, DurationNull) { + constexpr absl::string_view kInput = R"json({"optionalDuration": null})json"; + constexpr absl::string_view kExpected = ""; + EXPECT_THAT( + Testee("DurationNull").ParseJson(message(), kInput).SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT( + Testee("DurationNull").ParseJson(message(), kInput).SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +TEST_P(JsonDurationTest, DurationNegativeSeconds) { + constexpr absl::string_view kInput = R"json({"optionalDuration": "-5s"})json"; + constexpr absl::string_view kExpected = + R"pb(optional_duration: { seconds: -5 nanos: 0 })pb"; + EXPECT_THAT(Testee("DurationNegativeSeconds") + .ParseJson(message(), kInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT(Testee("DurationNegativeSeconds") + .ParseJson(message(), kInput) + .SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +TEST_P(JsonDurationTest, DurationNegativeNanos) { + constexpr absl::string_view kInput = + R"json({"optionalDuration": "-0.5s"})json"; + constexpr absl::string_view kExpected = + R"pb(optional_duration: { seconds: 0 nanos: -500000000 })pb"; + EXPECT_THAT(Testee("DurationNegativeNanos") + .ParseJson(message(), kInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT(Testee("DurationNegativeNanos") + .ParseJson(message(), kInput) + .SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +// Malformed and out-of-range JSON values are rejected. +TEST_P(JsonDurationTest, DurationMissingS) { + EXPECT_THAT(Testee("DurationMissingS") + .ParseJson(message(), R"json({"optionalDuration": "1"})json") + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(JsonDurationTest, DurationJsonInputTooSmall) { + EXPECT_THAT( + Testee("DurationJsonInputTooSmall") + .ParseJson( + message(), + R"json({"optionalDuration": "-315576000001.000000000s"})json") + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(JsonDurationTest, DurationJsonInputTooLarge) { + EXPECT_THAT( + Testee("DurationJsonInputTooLarge") + .ParseJson( + message(), + R"json({"optionalDuration": "315576000001.000000000s"})json") + .ParseOnly(), + Yields(IsParseError())); +} + +// Out-of-range or inconsistent messages can't be serialized. The inputs +// are the legacy text-format messages on the wire (zero fields omitted). +TEST_P(JsonDurationTest, DurationProtoInputTooSmall) { + EXPECT_THAT( + Testee("DurationProtoInputTooSmall") + .ParseBinary(message(), LengthPrefixedField( + kOptionalDuration, + VarintField(kSeconds, -315576000001))) + .SerializeJson(), + Yields(IsSerializeError())); +} + +TEST_P(JsonDurationTest, DurationProtoInputTooLarge) { + EXPECT_THAT( + Testee("DurationProtoInputTooLarge") + .ParseBinary(message(), + LengthPrefixedField(kOptionalDuration, + VarintField(kSeconds, 315576000001))) + .SerializeJson(), + Yields(IsSerializeError())); +} + +TEST_P(JsonDurationTest, DurationProtoNanosWrongSign) { + EXPECT_THAT(Testee("DurationProtoNanosWrongSign") + .ParseBinary(message(), LengthPrefixedField( + kOptionalDuration, + Wire(VarintField(kSeconds, 1), + VarintField(kNanos, -1)))) + .SerializeJson(), + Yields(IsSerializeError())); +} + +TEST_P(JsonDurationTest, DurationProtoNanosWrongSignNegativeSecs) { + EXPECT_THAT(Testee("DurationProtoNanosWrongSignNegativeSecs") + .ParseBinary(message(), LengthPrefixedField( + kOptionalDuration, + Wire(VarintField(kSeconds, -1), + VarintField(kNanos, 1)))) + .SerializeJson(), + Yields(IsSerializeError())); +} + +TEST_P(JsonDurationTest, DurationProtoNanosTooSmall) { + EXPECT_THAT( + Testee("DurationProtoNanosTooSmall") + .ParseBinary(message(), LengthPrefixedField( + kOptionalDuration, + Wire(VarintField(kSeconds, -1), + VarintField(kNanos, -1000000000)))) + .SerializeJson(), + Yields(IsSerializeError())); +} + +TEST_P(JsonDurationTest, DurationProtoNanosTooLarge) { + EXPECT_THAT( + Testee("DurationProtoNanosTooLarge") + .ParseBinary(message(), LengthPrefixedField( + kOptionalDuration, + Wire(VarintField(kSeconds, 1), + VarintField(kNanos, 1000000000)))) + .SerializeJson(), + Yields(IsSerializeError())); +} + +// Serializers emit 0, 3, 6 or 9 fractional digits, whichever is the fewest +// that represent the value exactly. +TEST_P(JsonDurationTest, DurationHasZeroFractionalDigit) { + EXPECT_THAT(Testee(kP1, "DurationHasZeroFractionalDigit") + .ParseJson(message(), + R"json({"optionalDuration": "1.000000000s"})json") + .SerializeJson(), + Yields(JsonPayload( + HasJsonMemberThat("optionalDuration", IsJsonString("1s"))))); +} + +TEST_P(JsonDurationTest, DurationHas3FractionalDigits) { + EXPECT_THAT(Testee(kP1, "DurationHas3FractionalDigits") + .ParseJson(message(), + R"json({"optionalDuration": "1.010000000s"})json") + .SerializeJson(), + Yields(JsonPayload(HasJsonMemberThat("optionalDuration", + IsJsonString("1.010s"))))); +} + +TEST_P(JsonDurationTest, DurationHas6FractionalDigits) { + EXPECT_THAT(Testee(kP1, "DurationHas6FractionalDigits") + .ParseJson(message(), + R"json({"optionalDuration": "1.000010000s"})json") + .SerializeJson(), + Yields(JsonPayload(HasJsonMemberThat( + "optionalDuration", IsJsonString("1.000010s"))))); +} + +TEST_P(JsonDurationTest, DurationHas9FractionalDigits) { + EXPECT_THAT(Testee(kP1, "DurationHas9FractionalDigits") + .ParseJson(message(), + R"json({"optionalDuration": "1.000000010s"})json") + .SerializeJson(), + Yields(JsonPayload(HasJsonMemberThat( + "optionalDuration", IsJsonString("1.000000010s"))))); +} + +INSTANTIATE_TEST_SUITE_P(All, JsonDurationTest, + ValuesIn(Proto3TestMessageTypes()), + MessageTypeParamName); + +} // namespace +} // namespace conformance +} // namespace protobuf +} // namespace google
diff --git a/conformance/json_enum_test.cc b/conformance/json_enum_test.cc new file mode 100644 index 0000000..519fdc8 --- /dev/null +++ b/conformance/json_enum_test.cc
@@ -0,0 +1,211 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2026 Google LLC. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +// JSON conformance tests for enum fields: values by name and by number, +// rejected unquoted names, arrays and booleans, and, for proto3 message +// types only, like the legacy suite, enum aliases and unknown values being +// serialized as numbers. This replaces the "Enum fields" block of the legacy +// BinaryAndJsonConformanceSuiteImpl<M>::RunJsonTestsForNonRepeatedTypes(); the +// test names and the requests sent to the testee are identical to the legacy +// ones. +// +// Like the legacy RunValidJsonTest(), each valid input is sent twice: once to +// be serialized as binary ("<name>.ProtobufOutput") and once as JSON +// ("<name>.JsonOutput"). ExpectParseFailureForJson() asked for JSON output +// under a name without an output suffix, and the legacy "validators" +// (RunValidJsonTestWithValidator()) looked at the serialized JSON text itself. + +#include <gmock/gmock.h> +#include <gtest/gtest.h> +#include "absl/strings/string_view.h" +#include "conformance/binary_test_util.h" +#include "conformance/json_test_util.h" +#include "conformance/matchers.h" +#include "conformance/message_type_fixtures.h" +#include "conformance/testee.h" + +namespace google { +namespace protobuf { +namespace conformance { +namespace { + +using ::testing::ValuesIn; + +// --------------------------------------------------------------------------- +// Every message type. +// --------------------------------------------------------------------------- + +using JsonEnumTest = MessageTypeConformanceTest; + +TEST_P(JsonEnumTest, EnumField) { + constexpr absl::string_view kInput = + R"json({"optionalNestedEnum": "FOO"})json"; + constexpr absl::string_view kExpected = R"pb(optional_nested_enum: FOO)pb"; + EXPECT_THAT( + Testee("EnumField").ParseJson(message(), kInput).SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT(Testee("EnumField").ParseJson(message(), kInput).SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +// Enum values must be represented as strings. +TEST_P(JsonEnumTest, EnumFieldNotQuoted) { + EXPECT_THAT( + Testee("EnumFieldNotQuoted") + .ParseJson(message(), R"json({"optionalNestedEnum": FOO})json") + .ParseOnly(), + Yields(IsParseError())); +} + +// Numeric values are allowed. +TEST_P(JsonEnumTest, EnumFieldNumericValueZero) { + constexpr absl::string_view kInput = R"json({"optionalNestedEnum": 0})json"; + constexpr absl::string_view kExpected = R"pb(optional_nested_enum: FOO)pb"; + EXPECT_THAT(Testee("EnumFieldNumericValueZero") + .ParseJson(message(), kInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT(Testee("EnumFieldNumericValueZero") + .ParseJson(message(), kInput) + .SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +TEST_P(JsonEnumTest, EnumFieldNumericValueNonZero) { + constexpr absl::string_view kInput = R"json({"optionalNestedEnum": 1})json"; + constexpr absl::string_view kExpected = R"pb(optional_nested_enum: BAR)pb"; + EXPECT_THAT(Testee("EnumFieldNumericValueNonZero") + .ParseJson(message(), kInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT(Testee("EnumFieldNumericValueNonZero") + .ParseJson(message(), kInput) + .SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +// Arrays are not allowed for non-repeated enum fields. +TEST_P(JsonEnumTest, EnumFieldSingleElementArrayEnumName) { + EXPECT_THAT( + Testee("EnumFieldSingleElementArrayEnumName") + .ParseJson(message(), R"json({"optionalNestedEnum": ["FOO"]})json") + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(JsonEnumTest, EnumFieldSingleElementArrayNumericValue) { + EXPECT_THAT( + Testee("EnumFieldSingleElementArrayNumericValue") + .ParseJson(message(), R"json({"optionalNestedEnum": [2]})json") + .ParseOnly(), + Yields(IsParseError())); +} + +// Booleans are not allowed for enum fields. +TEST_P(JsonEnumTest, EnumFieldTrueValue) { + EXPECT_THAT( + Testee("EnumFieldTrueValue") + .ParseJson(message(), R"json({"optionalNestedEnum": true})json") + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(JsonEnumTest, EnumFieldFalseValue) { + EXPECT_THAT( + Testee("EnumFieldFalseValue") + .ParseJson(message(), R"json({"optionalNestedEnum": false})json") + .ParseOnly(), + Yields(IsParseError())); +} + +INSTANTIATE_TEST_SUITE_P(All, JsonEnumTest, ValuesIn(AllTestMessageTypes()), + MessageTypeParamName); + +// --------------------------------------------------------------------------- +// Proto3 message types only: enum aliases and unknown (open enum) values. +// --------------------------------------------------------------------------- + +using JsonProto3EnumTest = MessageTypeConformanceTest; + +// Enum fields with alias. +TEST_P(JsonProto3EnumTest, EnumFieldWithAlias) { + constexpr absl::string_view kInput = + R"json({"optionalAliasedEnum": "ALIAS_BAZ"})json"; + constexpr absl::string_view kExpected = + R"pb(optional_aliased_enum: ALIAS_BAZ)pb"; + EXPECT_THAT(Testee("EnumFieldWithAlias") + .ParseJson(message(), kInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT( + Testee("EnumFieldWithAlias").ParseJson(message(), kInput).SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +TEST_P(JsonProto3EnumTest, EnumFieldWithAliasUseAlias) { + constexpr absl::string_view kInput = + R"json({"optionalAliasedEnum": "MOO"})json"; + constexpr absl::string_view kExpected = + R"pb(optional_aliased_enum: ALIAS_BAZ)pb"; + EXPECT_THAT(Testee("EnumFieldWithAliasUseAlias") + .ParseJson(message(), kInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT(Testee("EnumFieldWithAliasUseAlias") + .ParseJson(message(), kInput) + .SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +TEST_P(JsonProto3EnumTest, EnumFieldWithAliasLowerCase) { + constexpr absl::string_view kInput = + R"json({"optionalAliasedEnum": "moo"})json"; + constexpr absl::string_view kExpected = + R"pb(optional_aliased_enum: ALIAS_BAZ)pb"; + EXPECT_THAT(Testee("EnumFieldWithAliasLowerCase") + .ParseJson(message(), kInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT(Testee("EnumFieldWithAliasLowerCase") + .ParseJson(message(), kInput) + .SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +TEST_P(JsonProto3EnumTest, EnumFieldWithAliasDifferentCase) { + constexpr absl::string_view kInput = + R"json({"optionalAliasedEnum": "bAz"})json"; + constexpr absl::string_view kExpected = + R"pb(optional_aliased_enum: ALIAS_BAZ)pb"; + EXPECT_THAT(Testee("EnumFieldWithAliasDifferentCase") + .ParseJson(message(), kInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT(Testee("EnumFieldWithAliasDifferentCase") + .ParseJson(message(), kInput) + .SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +// Unknown enum values are represented as numeric values. +TEST_P(JsonProto3EnumTest, EnumFieldUnknownValue) { + EXPECT_THAT( + Testee("EnumFieldUnknownValue") + .ParseJson(message(), R"json({"optionalNestedEnum": 123})json") + .SerializeJson(), + Yields(JsonPayload( + HasJsonMemberThat("optionalNestedEnum", IsJsonInt(123))))); +} + +INSTANTIATE_TEST_SUITE_P(All, JsonProto3EnumTest, + ValuesIn(Proto3TestMessageTypes()), + MessageTypeParamName); + +} // namespace +} // namespace conformance +} // namespace protobuf +} // namespace google
diff --git a/conformance/json_field_mask_test.cc b/conformance/json_field_mask_test.cc new file mode 100644 index 0000000..be19ce6 --- /dev/null +++ b/conformance/json_field_mask_test.cc
@@ -0,0 +1,127 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2026 Google LLC. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +// JSON conformance tests for google.protobuf.FieldMask fields: the +// comma-separated lowerCamelCase form, the empty mask, a path character the +// JSON form cannot express, and the paths that don't round-trip through +// lowerCamelCase and so must be rejected by the serializer. This replaces the +// legacy BinaryAndJsonConformanceSuiteImpl<M>::RunJsonTestsForFieldMask(), +// which ran for the proto3-style message types only; the test names and the +// requests sent to the testee are identical to the legacy ones, except that +// the binary-input tests below are BINARY_TEST requests (see below). +// +// Like the legacy RunValidJsonTest(), each valid input is sent twice: once to +// be serialized as binary ("<name>.ProtobufOutput") and once as JSON +// ("<name>.JsonOutput"). ExpectParseFailureForJson() asked for JSON output +// under a name without an output suffix. ExpectSerializeFailureForJson() sent +// the serialized text-format message as binary input, categorized as a JSON +// test, under "ProtobufInput.<name>.JsonOutput"; the same bytes are built here +// with the wire-format builders, and ParseBinary() categorizes them as +// BINARY_TEST, which no testee tells apart from JSON_TEST on a binary payload +// (b/563657722). + +#include <cstdint> + +#include <gmock/gmock.h> +#include <gtest/gtest.h> +#include "absl/strings/string_view.h" +#include "conformance/binary_test_util.h" +#include "conformance/binary_wireformat.h" +#include "conformance/matchers.h" +#include "conformance/message_type_fixtures.h" +#include "conformance/testee.h" + +namespace google { +namespace protobuf { +namespace conformance { +namespace { + +using ::testing::ValuesIn; + +using JsonFieldMaskTest = MessageTypeConformanceTest; + +// Field numbers of the binary inputs of the serialize-failure tests: the +// test message's optional_field_mask and google.protobuf.FieldMask.paths. +constexpr uint32_t kOptionalFieldMask = 303; +constexpr uint32_t kFieldMaskPaths = 1; + +TEST_P(JsonFieldMaskTest, FieldMask) { + constexpr absl::string_view kInput = + R"json({"optionalFieldMask": "foo,barBaz"})json"; + constexpr absl::string_view kExpected = + R"pb(optional_field_mask: { paths: "foo" paths: "bar_baz" })pb"; + EXPECT_THAT( + Testee("FieldMask").ParseJson(message(), kInput).SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT(Testee("FieldMask").ParseJson(message(), kInput).SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +TEST_P(JsonFieldMaskTest, EmptyFieldMask) { + constexpr absl::string_view kInput = R"json({"optionalFieldMask": ""})json"; + constexpr absl::string_view kExpected = R"pb(optional_field_mask: {})pb"; + EXPECT_THAT( + Testee("EmptyFieldMask").ParseJson(message(), kInput).SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT( + Testee("EmptyFieldMask").ParseJson(message(), kInput).SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +TEST_P(JsonFieldMaskTest, FieldMaskInvalidCharacter) { + EXPECT_THAT(Testee(kP1, "FieldMaskInvalidCharacter") + .ParseJson(message(), + R"json({"optionalFieldMask": "foo,bar_bar"})json") + .ParseOnly(), + Yields(IsParseError())); +} + +// The binary form of the legacy input +// `optional_field_mask: {paths: "fooBar"}`. +TEST_P(JsonFieldMaskTest, FieldMaskPathsDontRoundTrip) { + EXPECT_THAT(Testee(kP1, "FieldMaskPathsDontRoundTrip") + .ParseBinary(message(), LengthPrefixedField( + kOptionalFieldMask, + LengthPrefixedField( + kFieldMaskPaths, "fooBar"))) + .SerializeJson(), + Yields(IsSerializeError())); +} + +// The binary form of the legacy input `optional_field_mask: {paths: +// "foo_3_bar"}`. +TEST_P(JsonFieldMaskTest, FieldMaskNumbersDontRoundTrip) { + EXPECT_THAT( + Testee(kP1, "FieldMaskNumbersDontRoundTrip") + .ParseBinary(message(), + LengthPrefixedField( + kOptionalFieldMask, + LengthPrefixedField(kFieldMaskPaths, "foo_3_bar"))) + .SerializeJson(), + Yields(IsSerializeError())); +} + +// The binary form of the legacy input `optional_field_mask: {paths: +// "foo__bar"}`. +TEST_P(JsonFieldMaskTest, FieldMaskTooManyUnderscore) { + EXPECT_THAT(Testee(kP1, "FieldMaskTooManyUnderscore") + .ParseBinary(message(), LengthPrefixedField( + kOptionalFieldMask, + LengthPrefixedField( + kFieldMaskPaths, "foo__bar"))) + .SerializeJson(), + Yields(IsSerializeError())); +} + +INSTANTIATE_TEST_SUITE_P(All, JsonFieldMaskTest, + ValuesIn(Proto3TestMessageTypes()), + MessageTypeParamName); + +} // namespace +} // namespace conformance +} // namespace protobuf +} // namespace google
diff --git a/conformance/json_field_name_test.cc b/conformance/json_field_name_test.cc new file mode 100644 index 0000000..83dfc5a --- /dev/null +++ b/conformance/json_field_name_test.cc
@@ -0,0 +1,534 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2026 Google LLC. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +// JSON conformance tests about field names and object syntax: the +// lowerCamelCase / original-name conventions, escaped and unquoted names, +// trailing commas, comments, whitespace, duplicate members, and whether +// default values of proto2 and proto3 fields are serialized. This replaces +// the legacy BinaryAndJsonConformanceSuiteImpl<M>:: +// RunJsonTestsForFieldNameConvention() and +// RunJsonTestsForStoresDefaultPrimitive(); the test names and the requests +// sent to the testee are identical to the legacy ones. +// +// Like the legacy RunValidJsonTest(), each valid input is sent twice: once to +// be serialized as binary ("<name>.ProtobufOutput") and once as JSON +// ("<name>.JsonOutput"). The legacy "validators" +// (RunValidJsonTestWithValidator()) looked at the serialized JSON text itself +// and are the tests that only serialize as JSON (FieldNameInLowerCamelCase, +// FieldNameWith*Validator, StoresDefaultPrimitive, ...). +// RunValidJsonTestOrParseFailure() asked for binary output under a name +// without an output suffix. + +#include <vector> + +#include <gmock/gmock.h> +#include <gtest/gtest.h> +#include "absl/strings/str_cat.h" +#include "absl/strings/string_view.h" +#include "absl/strings/substitute.h" +#include "conformance/binary_test_util.h" +#include "conformance/conformance.pb.h" +#include "conformance/json_test_util.h" +#include "conformance/matchers.h" +#include "conformance/message_type_fixtures.h" +#include "conformance/testee.h" +#include "google/protobuf/descriptor.h" + +namespace google { +namespace protobuf { +namespace conformance { +namespace { + +using ::testing::AllOf; +using ::testing::AnyOf; +using ::testing::Not; +using ::testing::ValuesIn; + +// --------------------------------------------------------------------------- +// Parsing field names in either convention. +// --------------------------------------------------------------------------- + +using JsonFieldNameTest = MessageTypeConformanceTest; + +TEST_P(JsonFieldNameTest, FieldNameInSnakeCase) { + constexpr absl::string_view kInput = R"json({ + "fieldname1": 1, + "fieldName2": 2, + "FieldName3": 3, + "fieldName4": 4 + })json"; + constexpr absl::string_view kExpected = + R"pb(fieldname1: 1 field_name2: 2 _field_name3: 3 field__name4_: 4)pb"; + EXPECT_THAT(Testee("FieldNameInSnakeCase") + .ParseJson(message(), kInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT(Testee("FieldNameInSnakeCase") + .ParseJson(message(), kInput) + .SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +TEST_P(JsonFieldNameTest, FieldNameWithNumbers) { + constexpr absl::string_view kInput = R"json({ + "field0name5": 5, + "field0Name6": 6 + })json"; + constexpr absl::string_view kExpected = + R"pb(field0name5: 5 field_0_name6: 6)pb"; + EXPECT_THAT(Testee("FieldNameWithNumbers") + .ParseJson(message(), kInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT(Testee("FieldNameWithNumbers") + .ParseJson(message(), kInput) + .SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +TEST_P(JsonFieldNameTest, FieldNameWithMixedCases) { + constexpr absl::string_view kInput = R"json({ + "fieldName7": 7, + "FieldName8": 8, + "fieldName9": 9, + "FieldName10": 10, + "FIELDNAME11": 11, + "FIELDName12": 12 + })json"; + constexpr absl::string_view kExpected = R"pb( + fieldName7: 7 + FieldName8: 8 + field_Name9: 9 + Field_Name10: 10 + FIELD_NAME11: 11 + FIELD_name12: 12 + )pb"; + EXPECT_THAT(Testee("FieldNameWithMixedCases") + .ParseJson(message(), kInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT(Testee("FieldNameWithMixedCases") + .ParseJson(message(), kInput) + .SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +TEST_P(JsonFieldNameTest, FieldNameWithDoubleUnderscores) { + constexpr absl::string_view kInput = R"json({ + "FieldName13": 13, + "FieldName14": 14, + "fieldName15": 15, + "fieldName16": 16, + "fieldName17": 17, + "FieldName18": 18 + })json"; + constexpr absl::string_view kExpected = R"pb( + __field_name13: 13 + __Field_name14: 14 + field__name15: 15 + field__Name16: 16 + field_name17__: 17 + Field_name18__: 18 + )pb"; + EXPECT_THAT(Testee(kP1, "FieldNameWithDoubleUnderscores") + .ParseJson(message(), kInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT(Testee(kP1, "FieldNameWithDoubleUnderscores") + .ParseJson(message(), kInput) + .SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +// Using the original proto field name in JSON is also allowed. +TEST_P(JsonFieldNameTest, OriginalProtoFieldName) { + constexpr absl::string_view kInput = R"json({ + "fieldname1": 1, + "field_name2": 2, + "_field_name3": 3, + "field__name4_": 4, + "field0name5": 5, + "field_0_name6": 6, + "fieldName7": 7, + "FieldName8": 8, + "field_Name9": 9, + "Field_Name10": 10, + "FIELD_NAME11": 11, + "FIELD_name12": 12, + "__field_name13": 13, + "__Field_name14": 14, + "field__name15": 15, + "field__Name16": 16, + "field_name17__": 17, + "Field_name18__": 18 + })json"; + constexpr absl::string_view kExpected = R"pb( + fieldname1: 1 + field_name2: 2 + _field_name3: 3 + field__name4_: 4 + field0name5: 5 + field_0_name6: 6 + fieldName7: 7 + FieldName8: 8 + field_Name9: 9 + Field_Name10: 10 + FIELD_NAME11: 11 + FIELD_name12: 12 + __field_name13: 13 + __Field_name14: 14 + field__name15: 15 + field__Name16: 16 + field_name17__: 17 + Field_name18__: 18 + )pb"; + EXPECT_THAT(Testee("OriginalProtoFieldName") + .ParseJson(message(), kInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT(Testee("OriginalProtoFieldName") + .ParseJson(message(), kInput) + .SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +// Field names can be escaped. +TEST_P(JsonFieldNameTest, FieldNameEscaped) { + constexpr absl::string_view kInput = R"json({"fieldn\u0061me1": 1})json"; + EXPECT_THAT( + Testee("FieldNameEscaped").ParseJson(message(), kInput).SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(R"pb(fieldname1: 1)pb")))); + EXPECT_THAT( + Testee("FieldNameEscaped").ParseJson(message(), kInput).SerializeJson(), + Yields(WhenParsed(EqualsTextProto(R"pb(fieldname1: 1)pb")))); +} + +// String ends with escape character. +TEST_P(JsonFieldNameTest, StringEndsWithEscapeChar) { + EXPECT_THAT(Testee(kP1, "StringEndsWithEscapeChar") + .ParseJson(message(), "{\"optionalString\": \"abc\\") + .ParseOnly(), + Yields(IsParseError())); +} + +// Field names must be quoted (or it's not valid JSON). +TEST_P(JsonFieldNameTest, FieldNameNotQuoted) { + EXPECT_THAT(Testee(kP1, "FieldNameNotQuoted") + .ParseJson(message(), "{fieldname1: 1}") + .ParseOnly(), + Yields(IsParseError())); +} + +INSTANTIATE_TEST_SUITE_P(All, JsonFieldNameTest, + ValuesIn(AllTestMessageTypes()), MessageTypeParamName); + +// --------------------------------------------------------------------------- +// Object syntax: trailing commas, comments, whitespace, missing commas. +// --------------------------------------------------------------------------- + +class JsonObjectSyntaxTest : public MessageTypeConformanceTest { + private: + TestPriority DefaultPriority() const override { return kP1; } +}; + +// Trailing comma is not allowed (not valid JSON). +TEST_P(JsonObjectSyntaxTest, TrailingCommaInAnObject) { + EXPECT_THAT(Testee("TrailingCommaInAnObject") + .ParseJson(message(), R"json({"fieldname1":1,})json") + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(JsonObjectSyntaxTest, TrailingCommaInAnObjectWithSpace) { + EXPECT_THAT(Testee("TrailingCommaInAnObjectWithSpace") + .ParseJson(message(), R"json({"fieldname1":1 ,})json") + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(JsonObjectSyntaxTest, TrailingCommaInAnObjectWithSpaceCommaSpace) { + EXPECT_THAT(Testee("TrailingCommaInAnObjectWithSpaceCommaSpace") + .ParseJson(message(), R"json({"fieldname1":1 , })json") + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(JsonObjectSyntaxTest, TrailingCommaInAnObjectWithNewlines) { + EXPECT_THAT(Testee("TrailingCommaInAnObjectWithNewlines") + .ParseJson(message(), R"json({ + "fieldname1":1, + })json") + .ParseOnly(), + Yields(IsParseError())); +} + +// JSON doesn't support comments. +TEST_P(JsonObjectSyntaxTest, JsonWithComments) { + EXPECT_THAT(Testee("JsonWithComments") + .ParseJson(message(), R"json({ + // This is a comment. + "fieldname1": 1 + })json") + .ParseOnly(), + Yields(IsParseError())); +} + +// JSON spec says whitespace doesn't matter, so try a few spacings to be sure. +constexpr absl::string_view kTwoInt32sExpected = R"pb( + optional_int32: 1 + optional_int64: 2 +)pb"; + +TEST_P(JsonObjectSyntaxTest, OneLineNoSpaces) { + constexpr absl::string_view kInput = + R"json({"optionalInt32":1,"optionalInt64":2})json"; + EXPECT_THAT( + Testee("OneLineNoSpaces").ParseJson(message(), kInput).SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kTwoInt32sExpected)))); + EXPECT_THAT( + Testee("OneLineNoSpaces").ParseJson(message(), kInput).SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kTwoInt32sExpected)))); +} + +TEST_P(JsonObjectSyntaxTest, OneLineWithSpaces) { + constexpr absl::string_view kInput = + R"json({ "optionalInt32" : 1 , "optionalInt64" : 2 })json"; + EXPECT_THAT(Testee("OneLineWithSpaces") + .ParseJson(message(), kInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kTwoInt32sExpected)))); + EXPECT_THAT( + Testee("OneLineWithSpaces").ParseJson(message(), kInput).SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kTwoInt32sExpected)))); +} + +TEST_P(JsonObjectSyntaxTest, MultilineNoSpaces) { + constexpr absl::string_view kInput = + "{\n\"optionalInt32\"\n:\n1\n,\n\"optionalInt64\"\n:\n2\n}"; + EXPECT_THAT(Testee("MultilineNoSpaces") + .ParseJson(message(), kInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kTwoInt32sExpected)))); + EXPECT_THAT( + Testee("MultilineNoSpaces").ParseJson(message(), kInput).SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kTwoInt32sExpected)))); +} + +TEST_P(JsonObjectSyntaxTest, MultilineWithSpaces) { + constexpr absl::string_view kInput = + "{\n \"optionalInt32\" : 1\n ,\n \"optionalInt64\" : 2\n}\n"; + EXPECT_THAT(Testee("MultilineWithSpaces") + .ParseJson(message(), kInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kTwoInt32sExpected)))); + EXPECT_THAT(Testee("MultilineWithSpaces") + .ParseJson(message(), kInput) + .SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kTwoInt32sExpected)))); +} + +// Missing comma between key/value pairs. +TEST_P(JsonObjectSyntaxTest, MissingCommaOneLine) { + EXPECT_THAT( + Testee("MissingCommaOneLine") + .ParseJson(message(), + R"json({ "optionalInt32": 1 "optionalInt64": 2 })json") + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(JsonObjectSyntaxTest, MissingCommaMultiline) { + EXPECT_THAT( + Testee("MissingCommaMultiline") + .ParseJson(message(), + "{\n \"optionalInt32\": 1\n \"optionalInt64\": 2\n}") + .ParseOnly(), + Yields(IsParseError())); +} + +INSTANTIATE_TEST_SUITE_P(All, JsonObjectSyntaxTest, + ValuesIn(AllTestMessageTypes()), MessageTypeParamName); + +// --------------------------------------------------------------------------- +// Duplicated field names have either last-wins or parse failure. Like the +// legacy RunValidJsonTestOrParseFailure(), binary output is requested under a +// name without an output suffix. +// --------------------------------------------------------------------------- + +class JsonDuplicateFieldNameTest : public MessageTypeConformanceTest { + private: + TestPriority DefaultPriority() const override { return kP1; } +}; + +TEST_P(JsonDuplicateFieldNameTest, FieldNameDuplicate) { + EXPECT_THAT(Testee("FieldNameDuplicate") + .ParseJson(message(), R"json({ + "optionalNestedMessage": {"a": 1}, + "optionalNestedMessage": {} + })json") + .ParseOnly({/*output_format=*/::conformance::PROTOBUF}), + Yields(AnyOf(IsParseError(), WhenParsed(EqualsTextProto(R"pb( + optional_nested_message: {} + )pb"))))); +} + +TEST_P(JsonDuplicateFieldNameTest, FieldNameDuplicateDifferentCasing1) { + EXPECT_THAT(Testee("FieldNameDuplicateDifferentCasing1") + .ParseJson(message(), R"json({ + "optional_nested_message": {"a": 1}, + "optionalNestedMessage": {} + })json") + .ParseOnly({/*output_format=*/::conformance::PROTOBUF}), + Yields(AnyOf(IsParseError(), WhenParsed(EqualsTextProto(R"pb( + optional_nested_message: {} + )pb"))))); +} + +TEST_P(JsonDuplicateFieldNameTest, FieldNameDuplicateDifferentCasing2) { + EXPECT_THAT(Testee("FieldNameDuplicateDifferentCasing2") + .ParseJson(message(), R"json({ + "optionalNestedMessage": {"a": 1}, + "optional_nested_message": {} + })json") + .ParseOnly({/*output_format=*/::conformance::PROTOBUF}), + Yields(AnyOf(IsParseError(), WhenParsed(EqualsTextProto(R"pb( + optional_nested_message: {} + )pb"))))); +} + +INSTANTIATE_TEST_SUITE_P(All, JsonDuplicateFieldNameTest, + ValuesIn(AllTestMessageTypes()), MessageTypeParamName); + +// --------------------------------------------------------------------------- +// Serializers should use lowerCamelCase by default. These look at the JSON +// text the testee produces, not at the message it decodes to. +// --------------------------------------------------------------------------- + +using JsonFieldNameSerializationTest = MessageTypeConformanceTest; + +TEST_P(JsonFieldNameSerializationTest, FieldNameInLowerCamelCase) { + EXPECT_THAT(Testee("FieldNameInLowerCamelCase") + .ParseJson(message(), R"json({ + "fieldname1": 1, + "fieldName2": 2, + "FieldName3": 3, + "fieldName4": 4 + })json") + .SerializeJson(), + Yields(JsonPayload(AllOf( + HasJsonMember("fieldname1"), HasJsonMember("fieldName2"), + HasJsonMember("FieldName3"), HasJsonMember("fieldName4"))))); +} + +TEST_P(JsonFieldNameSerializationTest, FieldNameWithNumbersValidator) { + EXPECT_THAT(Testee("FieldNameWithNumbersValidator") + .ParseJson(message(), R"json({ + "field0name5": 5, + "field0Name6": 6 + })json") + .SerializeJson(), + Yields(JsonPayload(AllOf(HasJsonMember("field0name5"), + HasJsonMember("field0Name6"))))); +} + +TEST_P(JsonFieldNameSerializationTest, FieldNameWithMixedCasesValidator) { + EXPECT_THAT( + Testee("FieldNameWithMixedCasesValidator") + .ParseJson(message(), R"json({ + "fieldName7": 7, + "FieldName8": 8, + "fieldName9": 9, + "FieldName10": 10, + "FIELDNAME11": 11, + "FIELDName12": 12 + })json") + .SerializeJson(), + Yields(JsonPayload( + AllOf(HasJsonMember("fieldName7"), HasJsonMember("FieldName8"), + HasJsonMember("fieldName9"), HasJsonMember("FieldName10"), + HasJsonMember("FIELDNAME11"), HasJsonMember("FIELDName12"))))); +} + +TEST_P(JsonFieldNameSerializationTest, + FieldNameWithDoubleUnderscoresValidator) { + EXPECT_THAT( + Testee(kP1, "FieldNameWithDoubleUnderscoresValidator") + .ParseJson(message(), R"json({ + "FieldName13": 13, + "FieldName14": 14, + "fieldName15": 15, + "fieldName16": 16, + "fieldName17": 17, + "FieldName18": 18 + })json") + .SerializeJson(), + Yields(JsonPayload( + AllOf(HasJsonMember("FieldName13"), HasJsonMember("FieldName14"), + HasJsonMember("fieldName15"), HasJsonMember("fieldName16"), + HasJsonMember("fieldName17"), HasJsonMember("FieldName18"))))); +} + +INSTANTIATE_TEST_SUITE_P(All, JsonFieldNameSerializationTest, + ValuesIn(AllTestMessageTypes()), MessageTypeParamName); + +// --------------------------------------------------------------------------- +// Default values: proto3 (implicit presence) skips them, proto2 (explicit +// presence) stores them. Extensions are serialized under their bracketed +// full name. +// --------------------------------------------------------------------------- + +using JsonSkipsDefaultTest = MessageTypeConformanceTest; + +TEST_P(JsonSkipsDefaultTest, SkipsDefaultPrimitive) { + EXPECT_THAT(Testee("SkipsDefaultPrimitive") + .ParseJson(message(), R"json({"FieldName13": 0})json") + .SerializeJson(), + Yields(JsonPayload(Not(HasJsonMember("FieldName13"))))); +} + +INSTANTIATE_TEST_SUITE_P(All, JsonSkipsDefaultTest, + ValuesIn(Proto3TestMessageTypes()), + MessageTypeParamName); + +// Only run on proto2 message types for now; proto3 fields with explicit +// presence would be a candidate as well. +using JsonStoresDefaultTest = MessageTypeConformanceTest; + +TEST_P(JsonStoresDefaultTest, StoresDefaultPrimitive) { + EXPECT_THAT(Testee("StoresDefaultPrimitive") + .ParseJson(message(), R"json({ + "FieldName13": 0 + })json") + .SerializeJson(), + Yields(JsonPayload(HasJsonMember("FieldName13")))); +} + +TEST_P(JsonStoresDefaultTest, FieldNameExtension) { + // Like the legacy suite, the first extension of the message type in its + // pool is used, whichever it is. + std::vector<const FieldDescriptor*> extensions; + message()->file()->pool()->FindAllExtensions(message(), &extensions); + ASSERT_FALSE(extensions.empty()); + const absl::string_view extension_name = extensions[0]->full_name(); + EXPECT_THAT(Testee(kP1, "FieldNameExtension") + .ParseJson(message(), absl::Substitute(R"({ + "[$0]": 1 + })", + extension_name)) + .SerializeJson(), + Yields(JsonPayload( + HasJsonMember(absl::StrCat("[", extension_name, "]"))))); +} + +INSTANTIATE_TEST_SUITE_P(All, JsonStoresDefaultTest, + ValuesIn(Proto2TestMessageTypes()), + MessageTypeParamName); + +} // namespace +} // namespace conformance +} // namespace protobuf +} // namespace google
diff --git a/conformance/json_float_test.cc b/conformance/json_float_test.cc new file mode 100644 index 0000000..8b70281 --- /dev/null +++ b/conformance/json_float_test.cc
@@ -0,0 +1,594 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2026 Google LLC. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +// JSON conformance tests for float and double fields: the value ranges, +// quoted values, the special values NaN / Infinity / -Infinity (which must be +// quoted), the normalization of non-canonical NaN bit patterns when +// serializing to JSON, and rejected out-of-range, empty, non-numeric and +// boolean values. This replaces the "Float fields" and "Double fields" blocks +// of the legacy BinaryAndJsonConformanceSuiteImpl<M>:: +// RunJsonTestsForNonRepeatedTypes(); the test names and the requests sent to +// the testee are identical to the legacy ones, except that the NaN +// normalization tests are BINARY_TEST requests (see below). +// +// Like the legacy RunValidJsonTest(), each valid input is sent twice: once to +// be serialized as binary ("<name>.ProtobufOutput") and once as JSON +// ("<name>.JsonOutput"). ExpectParseFailureForJson() asked for JSON output +// under a name without an output suffix. The NaN normalization tests +// (RunValidJsonTestWithProtobufInput()) send a *binary* payload and ask for +// JSON output ("<S>.<E>.ProtobufInput.<name>.JsonOutput"); the legacy suite +// filed them under the JSON_TEST category, here ParseBinary() categorizes them +// as BINARY_TEST, which no testee tells apart from JSON_TEST on a binary +// payload (b/563657722). + +#include <cstdint> + +#include <gmock/gmock.h> +#include <gtest/gtest.h> +#include "absl/strings/string_view.h" +#include "conformance/binary_test_util.h" +#include "conformance/binary_wireformat.h" +#include "conformance/matchers.h" +#include "conformance/message_type_fixtures.h" +#include "conformance/testee.h" + +namespace google { +namespace protobuf { +namespace conformance { +namespace { + +using ::testing::ValuesIn; + +// The field numbers of optional_float and optional_double, the same in every +// test message type. +constexpr uint32_t kOptionalFloatFieldNumber = 11; +constexpr uint32_t kOptionalDoubleFieldNumber = 12; + +// --------------------------------------------------------------------------- +// Float fields. +// --------------------------------------------------------------------------- + +using JsonFloatTest = MessageTypeConformanceTest; + +TEST_P(JsonFloatTest, FloatFieldMinPositiveValue) { + constexpr absl::string_view kInput = + R"json({"optionalFloat": 1.175494e-38})json"; + constexpr absl::string_view kExpected = R"pb(optional_float: 1.175494e-38)pb"; + EXPECT_THAT(Testee("FloatFieldMinPositiveValue") + .ParseJson(message(), kInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT(Testee("FloatFieldMinPositiveValue") + .ParseJson(message(), kInput) + .SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +TEST_P(JsonFloatTest, FloatFieldMaxNegativeValue) { + constexpr absl::string_view kInput = + R"json({"optionalFloat": -1.175494e-38})json"; + constexpr absl::string_view kExpected = + R"pb(optional_float: -1.175494e-38)pb"; + EXPECT_THAT(Testee("FloatFieldMaxNegativeValue") + .ParseJson(message(), kInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT(Testee("FloatFieldMaxNegativeValue") + .ParseJson(message(), kInput) + .SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +TEST_P(JsonFloatTest, FloatFieldMaxPositiveValue) { + constexpr absl::string_view kInput = + R"json({"optionalFloat": 3.402823e+38})json"; + constexpr absl::string_view kExpected = R"pb(optional_float: 3.402823e+38)pb"; + EXPECT_THAT(Testee("FloatFieldMaxPositiveValue") + .ParseJson(message(), kInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT(Testee("FloatFieldMaxPositiveValue") + .ParseJson(message(), kInput) + .SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +// Sends the same (positive) value as FloatFieldMaxPositiveValue, like the +// legacy suite did. +TEST_P(JsonFloatTest, FloatFieldMinNegativeValue) { + constexpr absl::string_view kInput = + R"json({"optionalFloat": 3.402823e+38})json"; + constexpr absl::string_view kExpected = R"pb(optional_float: 3.402823e+38)pb"; + EXPECT_THAT(Testee("FloatFieldMinNegativeValue") + .ParseJson(message(), kInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT(Testee("FloatFieldMinNegativeValue") + .ParseJson(message(), kInput) + .SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +// Values can be quoted. +TEST_P(JsonFloatTest, FloatFieldQuotedValue) { + constexpr absl::string_view kInput = R"json({"optionalFloat": "1"})json"; + constexpr absl::string_view kExpected = R"pb(optional_float: 1)pb"; + EXPECT_THAT(Testee("FloatFieldQuotedValue") + .ParseJson(message(), kInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT(Testee("FloatFieldQuotedValue") + .ParseJson(message(), kInput) + .SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +TEST_P(JsonFloatTest, FloatFieldQuotedExponentialValue) { + constexpr absl::string_view kInput = + R"json({"optionalFloat": "1.175494e-38"})json"; + constexpr absl::string_view kExpected = R"pb(optional_float: 1.175494e-38)pb"; + EXPECT_THAT(Testee("FloatFieldQuotedExponentialValue") + .ParseJson(message(), kInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT(Testee("FloatFieldQuotedExponentialValue") + .ParseJson(message(), kInput) + .SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +// Special values. +TEST_P(JsonFloatTest, FloatFieldNan) { + constexpr absl::string_view kInput = R"json({"optionalFloat": "NaN"})json"; + constexpr absl::string_view kExpected = R"pb(optional_float: nan)pb"; + EXPECT_THAT( + Testee("FloatFieldNan").ParseJson(message(), kInput).SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT( + Testee("FloatFieldNan").ParseJson(message(), kInput).SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +TEST_P(JsonFloatTest, FloatFieldInfinity) { + constexpr absl::string_view kInput = + R"json({"optionalFloat": "Infinity"})json"; + constexpr absl::string_view kExpected = R"pb(optional_float: inf)pb"; + EXPECT_THAT(Testee("FloatFieldInfinity") + .ParseJson(message(), kInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT( + Testee("FloatFieldInfinity").ParseJson(message(), kInput).SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +TEST_P(JsonFloatTest, FloatFieldNegativeInfinity) { + constexpr absl::string_view kInput = + R"json({"optionalFloat": "-Infinity"})json"; + constexpr absl::string_view kExpected = R"pb(optional_float: -inf)pb"; + EXPECT_THAT(Testee("FloatFieldNegativeInfinity") + .ParseJson(message(), kInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT(Testee("FloatFieldNegativeInfinity") + .ParseJson(message(), kInput) + .SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +// Non-canonical NaNs are normalized: the binary payload carries a NaN with +// a non-canonical payload (0x7FA12345; the legacy names notwithstanding, +// bit 22 is clear in both patterns, so both are signaling NaNs), ... +TEST_P(JsonFloatTest, FloatFieldNormalizeQuietNan) { + EXPECT_THAT( + Testee("FloatFieldNormalizeQuietNan") + .ParseBinary(message(), + Fixed32Field(kOptionalFloatFieldNumber, 0x7FA12345)) + .SerializeJson(), + Yields(WhenParsed(EqualsTextProto(R"pb(optional_float: nan)pb")))); +} + +// ... or a NaN with the sign bit set and another payload (0xFFB54321). +TEST_P(JsonFloatTest, FloatFieldNormalizeSignalingNan) { + EXPECT_THAT( + Testee("FloatFieldNormalizeSignalingNan") + .ParseBinary(message(), + Fixed32Field(kOptionalFloatFieldNumber, 0xFFB54321)) + .SerializeJson(), + Yields(WhenParsed(EqualsTextProto(R"pb(optional_float: nan)pb")))); +} + +// Special values must be quoted. +TEST_P(JsonFloatTest, FloatFieldNanNotQuoted) { + EXPECT_THAT(Testee(kP1, "FloatFieldNanNotQuoted") + .ParseJson(message(), R"json({"optionalFloat": NaN})json") + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(JsonFloatTest, FloatFieldInfinityNotQuoted) { + EXPECT_THAT( + Testee(kP1, "FloatFieldInfinityNotQuoted") + .ParseJson(message(), R"json({"optionalFloat": Infinity})json") + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(JsonFloatTest, FloatFieldNegativeInfinityNotQuoted) { + EXPECT_THAT( + Testee(kP1, "FloatFieldNegativeInfinityNotQuoted") + .ParseJson(message(), R"json({"optionalFloat": -Infinity})json") + .ParseOnly(), + Yields(IsParseError())); +} + +// Parsers should reject out-of-bound values. +TEST_P(JsonFloatTest, FloatFieldTooSmall) { + EXPECT_THAT( + Testee("FloatFieldTooSmall") + .ParseJson(message(), R"json({"optionalFloat": -3.502823e+38})json") + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(JsonFloatTest, FloatFieldTooLarge) { + EXPECT_THAT( + Testee("FloatFieldTooLarge") + .ParseJson(message(), R"json({"optionalFloat": 3.502823e+38})json") + .ParseOnly(), + Yields(IsParseError())); +} + +// Parsers should reject empty string values. +TEST_P(JsonFloatTest, FloatFieldEmptyString) { + EXPECT_THAT(Testee("FloatFieldEmptyString") + .ParseJson(message(), R"json({"optionalFloat": ""})json") + .ParseOnly(), + Yields(IsParseError())); +} + +// Parsers reject non-numeric string values. +TEST_P(JsonFloatTest, FloatFieldStringValuePartiallyNumeric) { + EXPECT_THAT(Testee("FloatFieldStringValuePartiallyNumeric") + .ParseJson(message(), R"json({"optionalFloat": "12abc"})json") + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(JsonFloatTest, FloatFieldStringValueNonNumeric) { + EXPECT_THAT(Testee("FloatFieldStringValueNonNumeric") + .ParseJson(message(), R"json({"optionalFloat": "abc"})json") + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(JsonFloatTest, FloatFieldStringValuePartiallyNumericSpace) { + EXPECT_THAT(Testee("FloatFieldStringValuePartiallyNumericSpace") + .ParseJson(message(), R"json({"optionalFloat": "12 34"})json") + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(JsonFloatTest, FloatFieldStringValuePartiallyNumericComma) { + EXPECT_THAT(Testee("FloatFieldStringValuePartiallyNumericComma") + .ParseJson(message(), R"json({"optionalFloat": "12,34"})json") + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(JsonFloatTest, FloatFieldStringValuePartiallyNumericUnicode) { + // Not a raw string literal: the payload holds the UTF-8 bytes of U+8C37 + // U+6B4C between the digits. + EXPECT_THAT(Testee("FloatFieldStringValuePartiallyNumericUnicode") + .ParseJson(message(), + "{\"optionalFloat\": \"12\xE8\xB0\xB7\xE6\xAD\x8C" + "34\"}") + .ParseOnly(), + Yields(IsParseError())); +} + +// Only ASCII digits are accepted in quoted numbers. Not raw string +// literals: the payloads hold the UTF-8 bytes of the Arabic-Indic digits +// U+0661 U+0662 U+0663 and of the fullwidth digits U+FF11 U+FF12 U+FF13. +TEST_P(JsonFloatTest, FloatFieldStringValueArabicIndicDigits) { + constexpr absl::string_view kInput = + "{\"optionalFloat\": \"\xD9\xA1\xD9\xA2\xD9\xA3\"}"; + EXPECT_THAT(Testee("FloatFieldStringValueArabicIndicDigits") + .ParseJson(message(), kInput) + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(JsonFloatTest, FloatFieldStringValueFullwidthDigits) { + constexpr absl::string_view kInput = + "{\"optionalFloat\": \"\xEF\xBC\x91\xEF\xBC\x92\xEF\xBC\x93\"}"; + EXPECT_THAT(Testee("FloatFieldStringValueFullwidthDigits") + .ParseJson(message(), kInput) + .ParseOnly(), + Yields(IsParseError())); +} + +// Parsers reject boolean values for float fields. +TEST_P(JsonFloatTest, FloatFieldTrueValue) { + EXPECT_THAT(Testee("FloatFieldTrueValue") + .ParseJson(message(), R"json({"optionalFloat": true})json") + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(JsonFloatTest, FloatFieldFalseValue) { + EXPECT_THAT(Testee("FloatFieldFalseValue") + .ParseJson(message(), R"json({"optionalFloat": false})json") + .ParseOnly(), + Yields(IsParseError())); +} + +INSTANTIATE_TEST_SUITE_P(All, JsonFloatTest, ValuesIn(AllTestMessageTypes()), + MessageTypeParamName); + +// --------------------------------------------------------------------------- +// Double fields. +// --------------------------------------------------------------------------- + +using JsonDoubleTest = MessageTypeConformanceTest; + +TEST_P(JsonDoubleTest, DoubleFieldMinPositiveValue) { + constexpr absl::string_view kInput = + R"json({"optionalDouble": 2.22507e-308})json"; + constexpr absl::string_view kExpected = + R"pb(optional_double: 2.22507e-308)pb"; + EXPECT_THAT(Testee("DoubleFieldMinPositiveValue") + .ParseJson(message(), kInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT(Testee("DoubleFieldMinPositiveValue") + .ParseJson(message(), kInput) + .SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +TEST_P(JsonDoubleTest, DoubleFieldMaxNegativeValue) { + constexpr absl::string_view kInput = + R"json({"optionalDouble": -2.22507e-308})json"; + constexpr absl::string_view kExpected = + R"pb(optional_double: -2.22507e-308)pb"; + EXPECT_THAT(Testee("DoubleFieldMaxNegativeValue") + .ParseJson(message(), kInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT(Testee("DoubleFieldMaxNegativeValue") + .ParseJson(message(), kInput) + .SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +TEST_P(JsonDoubleTest, DoubleFieldMaxPositiveValue) { + constexpr absl::string_view kInput = + R"json({"optionalDouble": 1.79769e+308})json"; + constexpr absl::string_view kExpected = + R"pb(optional_double: 1.79769e+308)pb"; + EXPECT_THAT(Testee("DoubleFieldMaxPositiveValue") + .ParseJson(message(), kInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT(Testee("DoubleFieldMaxPositiveValue") + .ParseJson(message(), kInput) + .SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +TEST_P(JsonDoubleTest, DoubleFieldMinNegativeValue) { + constexpr absl::string_view kInput = + R"json({"optionalDouble": -1.79769e+308})json"; + constexpr absl::string_view kExpected = + R"pb(optional_double: -1.79769e+308)pb"; + EXPECT_THAT(Testee("DoubleFieldMinNegativeValue") + .ParseJson(message(), kInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT(Testee("DoubleFieldMinNegativeValue") + .ParseJson(message(), kInput) + .SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +// Values can be quoted. +TEST_P(JsonDoubleTest, DoubleFieldQuotedValue) { + constexpr absl::string_view kInput = R"json({"optionalDouble": "1"})json"; + constexpr absl::string_view kExpected = R"pb(optional_double: 1)pb"; + EXPECT_THAT(Testee("DoubleFieldQuotedValue") + .ParseJson(message(), kInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT(Testee("DoubleFieldQuotedValue") + .ParseJson(message(), kInput) + .SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +TEST_P(JsonDoubleTest, DoubleFieldQuotedExponentialValue) { + constexpr absl::string_view kInput = + R"json({"optionalDouble": "2.22507e-308"})json"; + constexpr absl::string_view kExpected = + R"pb(optional_double: 2.22507e-308)pb"; + EXPECT_THAT(Testee("DoubleFieldQuotedExponentialValue") + .ParseJson(message(), kInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT(Testee("DoubleFieldQuotedExponentialValue") + .ParseJson(message(), kInput) + .SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +// Special values. +TEST_P(JsonDoubleTest, DoubleFieldNan) { + constexpr absl::string_view kInput = R"json({"optionalDouble": "NaN"})json"; + constexpr absl::string_view kExpected = R"pb(optional_double: nan)pb"; + EXPECT_THAT( + Testee("DoubleFieldNan").ParseJson(message(), kInput).SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT( + Testee("DoubleFieldNan").ParseJson(message(), kInput).SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +TEST_P(JsonDoubleTest, DoubleFieldInfinity) { + constexpr absl::string_view kInput = + R"json({"optionalDouble": "Infinity"})json"; + constexpr absl::string_view kExpected = R"pb(optional_double: inf)pb"; + EXPECT_THAT(Testee("DoubleFieldInfinity") + .ParseJson(message(), kInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT(Testee("DoubleFieldInfinity") + .ParseJson(message(), kInput) + .SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +TEST_P(JsonDoubleTest, DoubleFieldNegativeInfinity) { + constexpr absl::string_view kInput = + R"json({"optionalDouble": "-Infinity"})json"; + constexpr absl::string_view kExpected = R"pb(optional_double: -inf)pb"; + EXPECT_THAT(Testee("DoubleFieldNegativeInfinity") + .ParseJson(message(), kInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT(Testee("DoubleFieldNegativeInfinity") + .ParseJson(message(), kInput) + .SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +// Non-canonical NaNs are normalized, as for floats above. +TEST_P(JsonDoubleTest, DoubleFieldNormalizeQuietNan) { + EXPECT_THAT( + Testee("DoubleFieldNormalizeQuietNan") + .ParseBinary(message(), Fixed64Field(kOptionalDoubleFieldNumber, + 0x7FFA123456789ABC)) + .SerializeJson(), + Yields(WhenParsed(EqualsTextProto(R"pb(optional_double: nan)pb")))); +} + +TEST_P(JsonDoubleTest, DoubleFieldNormalizeSignalingNan) { + EXPECT_THAT( + Testee("DoubleFieldNormalizeSignalingNan") + .ParseBinary(message(), Fixed64Field(kOptionalDoubleFieldNumber, + 0xFFFBCBA987654321)) + .SerializeJson(), + Yields(WhenParsed(EqualsTextProto(R"pb(optional_double: nan)pb")))); +} + +// Special values must be quoted. +TEST_P(JsonDoubleTest, DoubleFieldNanNotQuoted) { + EXPECT_THAT(Testee(kP1, "DoubleFieldNanNotQuoted") + .ParseJson(message(), R"json({"optionalDouble": NaN})json") + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(JsonDoubleTest, DoubleFieldInfinityNotQuoted) { + EXPECT_THAT( + Testee(kP1, "DoubleFieldInfinityNotQuoted") + .ParseJson(message(), R"json({"optionalDouble": Infinity})json") + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(JsonDoubleTest, DoubleFieldNegativeInfinityNotQuoted) { + EXPECT_THAT( + Testee(kP1, "DoubleFieldNegativeInfinityNotQuoted") + .ParseJson(message(), R"json({"optionalDouble": -Infinity})json") + .ParseOnly(), + Yields(IsParseError())); +} + +// Parsers should reject out-of-bound values. +TEST_P(JsonDoubleTest, DoubleFieldTooSmall) { + EXPECT_THAT( + Testee("DoubleFieldTooSmall") + .ParseJson(message(), R"json({"optionalDouble": -1.89769e+308})json") + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(JsonDoubleTest, DoubleFieldTooLarge) { + EXPECT_THAT( + Testee("DoubleFieldTooLarge") + .ParseJson(message(), R"json({"optionalDouble": 1.89769e+308})json") + .ParseOnly(), + Yields(IsParseError())); +} + +// Parsers should reject empty string values. +TEST_P(JsonDoubleTest, DoubleFieldEmptyString) { + EXPECT_THAT(Testee("DoubleFieldEmptyString") + .ParseJson(message(), R"json({"optionalDouble": ""})json") + .ParseOnly(), + Yields(IsParseError())); +} + +// Parsers reject non-numeric string values. +TEST_P(JsonDoubleTest, DoubleFieldStringValuePartiallyNumeric) { + EXPECT_THAT( + Testee("DoubleFieldStringValuePartiallyNumeric") + .ParseJson(message(), R"json({"optionalDouble": "12abc"})json") + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(JsonDoubleTest, DoubleFieldStringValueNonNumeric) { + EXPECT_THAT(Testee("DoubleFieldStringValueNonNumeric") + .ParseJson(message(), R"json({"optionalDouble": "abc"})json") + .ParseOnly(), + Yields(IsParseError())); +} + +// Only ASCII digits are accepted in quoted numbers. Not raw string +// literals: the payloads hold the UTF-8 bytes of the Arabic-Indic digits +// U+0661 U+0662 U+0663 and of the fullwidth digits U+FF11 U+FF12 U+FF13. +TEST_P(JsonDoubleTest, DoubleFieldStringValueArabicIndicDigits) { + constexpr absl::string_view kInput = + "{\"optionalDouble\": \"\xD9\xA1\xD9\xA2\xD9\xA3\"}"; + EXPECT_THAT(Testee("DoubleFieldStringValueArabicIndicDigits") + .ParseJson(message(), kInput) + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(JsonDoubleTest, DoubleFieldStringValueFullwidthDigits) { + constexpr absl::string_view kInput = + "{\"optionalDouble\": \"\xEF\xBC\x91\xEF\xBC\x92\xEF\xBC\x93\"}"; + EXPECT_THAT(Testee("DoubleFieldStringValueFullwidthDigits") + .ParseJson(message(), kInput) + .ParseOnly(), + Yields(IsParseError())); +} + +// Parsers reject boolean values for double fields. +TEST_P(JsonDoubleTest, DoubleFieldTrueValue) { + EXPECT_THAT(Testee("DoubleFieldTrueValue") + .ParseJson(message(), R"json({"optionalDouble": true})json") + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(JsonDoubleTest, DoubleFieldFalseValue) { + EXPECT_THAT(Testee("DoubleFieldFalseValue") + .ParseJson(message(), R"json({"optionalDouble": false})json") + .ParseOnly(), + Yields(IsParseError())); +} + +INSTANTIATE_TEST_SUITE_P(All, JsonDoubleTest, ValuesIn(AllTestMessageTypes()), + MessageTypeParamName); + +} // namespace +} // namespace conformance +} // namespace protobuf +} // namespace google
diff --git a/conformance/json_hello_world_test.cc b/conformance/json_hello_world_test.cc new file mode 100644 index 0000000..4554f77 --- /dev/null +++ b/conformance/json_hello_world_test.cc
@@ -0,0 +1,112 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2026 Google LLC. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +// The smoke tests of the JSON conformance suite: a single string field +// round-trips through the testee, unknown members of every JSON kind are +// ignored when asked to, and a top-level `null` is rejected. This replaces +// the tests the legacy BinaryAndJsonConformanceSuiteImpl<M>::RunJsonTests() +// ran directly (rather than through one of its RunJsonTestsFor*() groups), +// for every test message type; the test names and the requests sent to the +// testee are identical to the legacy ones. +// +// Like the legacy RunValidJsonTest(), each valid input is sent twice: once to +// be serialized as binary ("<name>.ProtobufOutput") and once as JSON +// ("<name>.JsonOutput"). The legacy RunValidJsonIgnoreUnknownTest() only had +// the binary leg, and ExpectParseFailureForJson() asked for JSON output under +// a name without an output suffix. + +#include <gmock/gmock.h> +#include <gtest/gtest.h> +#include "absl/strings/string_view.h" +#include "conformance/binary_test_util.h" +#include "conformance/matchers.h" +#include "conformance/message_type_fixtures.h" +#include "conformance/testee.h" + +namespace google { +namespace protobuf { +namespace conformance { +namespace { + +using ::testing::ValuesIn; + +using JsonHelloWorldTest = MessageTypeConformanceTest; + +TEST_P(JsonHelloWorldTest, HelloWorld) { + constexpr absl::string_view kInput = + R"json({"optionalString":"Hello, World!"})json"; + constexpr absl::string_view kExpected = + R"pb(optional_string: 'Hello, World!')pb"; + EXPECT_THAT( + Testee("HelloWorld").ParseJson(message(), kInput).SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT(Testee("HelloWorld").ParseJson(message(), kInput).SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +// Unknown members are ignored whatever kind of JSON value they hold. +TEST_P(JsonHelloWorldTest, IgnoreUnknownJsonNumber) { + EXPECT_THAT(Testee("IgnoreUnknownJsonNumber") + .ParseJson(message(), R"json({"unknown": 1})json", + {/*ignore_unknown_fields=*/true}) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto("")))); +} + +TEST_P(JsonHelloWorldTest, IgnoreUnknownJsonString) { + EXPECT_THAT(Testee("IgnoreUnknownJsonString") + .ParseJson(message(), R"json({"unknown": "a"})json", + {/*ignore_unknown_fields=*/true}) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto("")))); +} + +TEST_P(JsonHelloWorldTest, IgnoreUnknownJsonTrue) { + EXPECT_THAT(Testee("IgnoreUnknownJsonTrue") + .ParseJson(message(), R"json({"unknown": true})json", + {/*ignore_unknown_fields=*/true}) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto("")))); +} + +TEST_P(JsonHelloWorldTest, IgnoreUnknownJsonFalse) { + EXPECT_THAT(Testee("IgnoreUnknownJsonFalse") + .ParseJson(message(), R"json({"unknown": false})json", + {/*ignore_unknown_fields=*/true}) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto("")))); +} + +TEST_P(JsonHelloWorldTest, IgnoreUnknownJsonNull) { + EXPECT_THAT(Testee("IgnoreUnknownJsonNull") + .ParseJson(message(), R"json({"unknown": null})json", + {/*ignore_unknown_fields=*/true}) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto("")))); +} + +TEST_P(JsonHelloWorldTest, IgnoreUnknownJsonObject) { + EXPECT_THAT(Testee("IgnoreUnknownJsonObject") + .ParseJson(message(), R"json({"unknown": {"a": 1}})json", + {/*ignore_unknown_fields=*/true}) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto("")))); +} + +TEST_P(JsonHelloWorldTest, RejectTopLevelNull) { + EXPECT_THAT( + Testee("RejectTopLevelNull").ParseJson(message(), "null").ParseOnly(), + Yields(IsParseError())); +} + +INSTANTIATE_TEST_SUITE_P(All, JsonHelloWorldTest, + ValuesIn(AllTestMessageTypes()), MessageTypeParamName); + +} // namespace +} // namespace conformance +} // namespace protobuf +} // namespace google
diff --git a/conformance/json_integer_test.cc b/conformance/json_integer_test.cc new file mode 100644 index 0000000..151a0ba --- /dev/null +++ b/conformance/json_integer_test.cc
@@ -0,0 +1,613 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2026 Google LLC. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +// JSON conformance tests for integer fields: the value ranges of the 32- and +// 64-bit types, integers as JSON strings and as float literals, rejected +// out-of-range, non-integer, boolean, non-numeric and malformed values, and +// the 64-bit types being serialized as strings. This replaces the "Integer +// fields" block of the legacy BinaryAndJsonConformanceSuiteImpl<M>:: +// RunJsonTestsForNonRepeatedTypes(); the test names and the requests sent to +// the testee are identical to the legacy ones. +// +// Like the legacy RunValidJsonTest(), each valid input is sent twice: once to +// be serialized as binary ("<name>.ProtobufOutput") and once as JSON +// ("<name>.JsonOutput"). ExpectParseFailureForJson() asked for JSON output +// under a name without an output suffix, and the legacy "validators" +// (RunValidJsonTestWithValidator()) looked at the serialized JSON text itself. + +#include <gmock/gmock.h> +#include <gtest/gtest.h> +#include "absl/strings/string_view.h" +#include "conformance/binary_test_util.h" +#include "conformance/json_test_util.h" +#include "conformance/matchers.h" +#include "conformance/message_type_fixtures.h" +#include "conformance/testee.h" + +namespace google { +namespace protobuf { +namespace conformance { +namespace { + +using ::testing::ValuesIn; + +using JsonIntegerTest = MessageTypeConformanceTest; + +// --------------------------------------------------------------------------- +// Value ranges. 64-bit values are quoted in JSON. +// --------------------------------------------------------------------------- + +TEST_P(JsonIntegerTest, Int32FieldMaxValue) { + constexpr absl::string_view kInput = + R"json({"optionalInt32": 2147483647})json"; + constexpr absl::string_view kExpected = R"pb(optional_int32: 2147483647)pb"; + EXPECT_THAT(Testee("Int32FieldMaxValue") + .ParseJson(message(), kInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT( + Testee("Int32FieldMaxValue").ParseJson(message(), kInput).SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +TEST_P(JsonIntegerTest, Int32FieldMinValue) { + constexpr absl::string_view kInput = + R"json({"optionalInt32": -2147483648})json"; + constexpr absl::string_view kExpected = R"pb(optional_int32: -2147483648)pb"; + EXPECT_THAT(Testee("Int32FieldMinValue") + .ParseJson(message(), kInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT( + Testee("Int32FieldMinValue").ParseJson(message(), kInput).SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +TEST_P(JsonIntegerTest, Uint32FieldMaxValue) { + constexpr absl::string_view kInput = + R"json({"optionalUint32": 4294967295})json"; + constexpr absl::string_view kExpected = R"pb(optional_uint32: 4294967295)pb"; + EXPECT_THAT(Testee("Uint32FieldMaxValue") + .ParseJson(message(), kInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT(Testee("Uint32FieldMaxValue") + .ParseJson(message(), kInput) + .SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +TEST_P(JsonIntegerTest, Int64FieldMaxValue) { + constexpr absl::string_view kInput = + R"json({"optionalInt64": "9223372036854775807"})json"; + constexpr absl::string_view kExpected = + R"pb(optional_int64: 9223372036854775807)pb"; + EXPECT_THAT(Testee("Int64FieldMaxValue") + .ParseJson(message(), kInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT( + Testee("Int64FieldMaxValue").ParseJson(message(), kInput).SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +TEST_P(JsonIntegerTest, Int64FieldMinValue) { + constexpr absl::string_view kInput = + R"json({"optionalInt64": "-9223372036854775808"})json"; + constexpr absl::string_view kExpected = + R"pb(optional_int64: -9223372036854775808)pb"; + EXPECT_THAT(Testee("Int64FieldMinValue") + .ParseJson(message(), kInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT( + Testee("Int64FieldMinValue").ParseJson(message(), kInput).SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +TEST_P(JsonIntegerTest, Uint64FieldMaxValue) { + constexpr absl::string_view kInput = + R"json({"optionalUint64": "18446744073709551615"})json"; + constexpr absl::string_view kExpected = + R"pb(optional_uint64: 18446744073709551615)pb"; + EXPECT_THAT(Testee("Uint64FieldMaxValue") + .ParseJson(message(), kInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT(Testee("Uint64FieldMaxValue") + .ParseJson(message(), kInput) + .SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +// While not the largest Int64, this is the largest Int64 which can be exactly +// represented within an IEEE-754 64-bit float, which is the expected level of +// interoperability guarantee. Larger values may work in some implementations, +// but should not be relied upon. +TEST_P(JsonIntegerTest, Int64FieldMaxValueNotQuoted) { + constexpr absl::string_view kInput = + R"json({"optionalInt64": 9223372036854774784})json"; + constexpr absl::string_view kExpected = + R"pb(optional_int64: 9223372036854774784)pb"; + EXPECT_THAT(Testee("Int64FieldMaxValueNotQuoted") + .ParseJson(message(), kInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT(Testee("Int64FieldMaxValueNotQuoted") + .ParseJson(message(), kInput) + .SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +TEST_P(JsonIntegerTest, Int64FieldMinValueNotQuoted) { + constexpr absl::string_view kInput = + R"json({"optionalInt64": -9223372036854775808})json"; + constexpr absl::string_view kExpected = + R"pb(optional_int64: -9223372036854775808)pb"; + EXPECT_THAT(Testee("Int64FieldMinValueNotQuoted") + .ParseJson(message(), kInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT(Testee("Int64FieldMinValueNotQuoted") + .ParseJson(message(), kInput) + .SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +// Largest interoperable Uint64; see the comment above for +// Int64FieldMaxValueNotQuoted. +TEST_P(JsonIntegerTest, Uint64FieldMaxValueNotQuoted) { + constexpr absl::string_view kInput = + R"json({"optionalUint64": 18446744073709549568})json"; + constexpr absl::string_view kExpected = + R"pb(optional_uint64: 18446744073709549568)pb"; + EXPECT_THAT(Testee("Uint64FieldMaxValueNotQuoted") + .ParseJson(message(), kInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT(Testee("Uint64FieldMaxValueNotQuoted") + .ParseJson(message(), kInput) + .SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +// Values can be represented as JSON strings. +TEST_P(JsonIntegerTest, Int32FieldStringValue) { + constexpr absl::string_view kInput = + R"json({"optionalInt32": "2147483647"})json"; + constexpr absl::string_view kExpected = R"pb(optional_int32: 2147483647)pb"; + EXPECT_THAT(Testee("Int32FieldStringValue") + .ParseJson(message(), kInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT(Testee("Int32FieldStringValue") + .ParseJson(message(), kInput) + .SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +TEST_P(JsonIntegerTest, Int32FieldStringValueEscaped) { + constexpr absl::string_view kInput = + R"json({"optionalInt32": "2\u003147483647"})json"; + constexpr absl::string_view kExpected = R"pb(optional_int32: 2147483647)pb"; + EXPECT_THAT(Testee("Int32FieldStringValueEscaped") + .ParseJson(message(), kInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT(Testee("Int32FieldStringValueEscaped") + .ParseJson(message(), kInput) + .SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +TEST_P(JsonIntegerTest, Int32FieldStringValueZero) { + constexpr absl::string_view kInput = R"json({"optionalInt32": "0"})json"; + constexpr absl::string_view kExpected = R"pb(optional_int32: 0)pb"; + EXPECT_THAT(Testee("Int32FieldStringValueZero") + .ParseJson(message(), kInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT(Testee("Int32FieldStringValueZero") + .ParseJson(message(), kInput) + .SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +TEST_P(JsonIntegerTest, Int32FieldQuotedExponentialValue) { + constexpr absl::string_view kInput = R"json({"optionalInt32": "1e5"})json"; + constexpr absl::string_view kExpected = R"pb(optional_int32: 100000)pb"; + EXPECT_THAT(Testee("Int32FieldQuotedExponentialValue") + .ParseJson(message(), kInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT(Testee("Int32FieldQuotedExponentialValue") + .ParseJson(message(), kInput) + .SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +// --------------------------------------------------------------------------- +// Parsers reject out-of-bound integer values. +// --------------------------------------------------------------------------- + +TEST_P(JsonIntegerTest, Int32FieldTooLarge) { + EXPECT_THAT( + Testee("Int32FieldTooLarge") + .ParseJson(message(), R"json({"optionalInt32": 2147483648})json") + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(JsonIntegerTest, Int32FieldTooSmall) { + EXPECT_THAT( + Testee("Int32FieldTooSmall") + .ParseJson(message(), R"json({"optionalInt32": -2147483649})json") + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(JsonIntegerTest, Uint32FieldTooLarge) { + EXPECT_THAT( + Testee("Uint32FieldTooLarge") + .ParseJson(message(), R"json({"optionalUint32": 4294967296})json") + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(JsonIntegerTest, Int64FieldTooLarge) { + EXPECT_THAT( + Testee("Int64FieldTooLarge") + .ParseJson(message(), + R"json({"optionalInt64": "9223372036854775808"})json") + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(JsonIntegerTest, Int64FieldTooSmall) { + EXPECT_THAT( + Testee("Int64FieldTooSmall") + .ParseJson(message(), + R"json({"optionalInt64": "-9223372036854775809"})json") + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(JsonIntegerTest, Uint64FieldTooLarge) { + EXPECT_THAT( + Testee("Uint64FieldTooLarge") + .ParseJson(message(), + R"json({"optionalUint64": "18446744073709551616"})json") + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(JsonIntegerTest, Uint64QuotedExponentFieldTooLarge) { + EXPECT_THAT( + Testee("Uint64QuotedExponentFieldTooLarge") + .ParseJson(message(), R"json({"optionalUint64": "1e536870000"})json") + .ParseOnly(), + Yields(IsParseError())); +} + +// --------------------------------------------------------------------------- +// Parsers reject non-integer numeric values. +// --------------------------------------------------------------------------- + +TEST_P(JsonIntegerTest, Int32FieldNotInteger) { + EXPECT_THAT(Testee("Int32FieldNotInteger") + .ParseJson(message(), R"json({"optionalInt32": 0.5})json") + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(JsonIntegerTest, Uint32FieldNotInteger) { + EXPECT_THAT(Testee("Uint32FieldNotInteger") + .ParseJson(message(), R"json({"optionalUint32": 0.5})json") + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(JsonIntegerTest, Int64FieldNotInteger) { + EXPECT_THAT(Testee("Int64FieldNotInteger") + .ParseJson(message(), R"json({"optionalInt64": "0.5"})json") + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(JsonIntegerTest, Uint64FieldNotInteger) { + EXPECT_THAT(Testee("Uint64FieldNotInteger") + .ParseJson(message(), R"json({"optionalUint64": "0.5"})json") + .ParseOnly(), + Yields(IsParseError())); +} + +// --------------------------------------------------------------------------- +// Parsers reject boolean values for integer fields. +// --------------------------------------------------------------------------- + +TEST_P(JsonIntegerTest, Int32FieldTrueValue) { + EXPECT_THAT(Testee("Int32FieldTrueValue") + .ParseJson(message(), R"json({"optionalInt32": true})json") + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(JsonIntegerTest, Int32FieldFalseValue) { + EXPECT_THAT(Testee("Int32FieldFalseValue") + .ParseJson(message(), R"json({"optionalInt32": false})json") + .ParseOnly(), + Yields(IsParseError())); +} + +// --------------------------------------------------------------------------- +// Parsers reject non-numeric string values. +// --------------------------------------------------------------------------- + +TEST_P(JsonIntegerTest, Int32FieldStringValuePartiallyNumeric) { + EXPECT_THAT(Testee("Int32FieldStringValuePartiallyNumeric") + .ParseJson(message(), R"json({"optionalInt32": "12abc"})json") + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(JsonIntegerTest, Int32FieldStringValuePartiallyNumericSpace) { + EXPECT_THAT(Testee("Int32FieldStringValuePartiallyNumericSpace") + .ParseJson(message(), R"json({"optionalInt32": "12 34"})json") + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(JsonIntegerTest, Int32FieldStringValuePartiallyNumericComma) { + EXPECT_THAT(Testee("Int32FieldStringValuePartiallyNumericComma") + .ParseJson(message(), R"json({"optionalInt32": "12,34"})json") + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(JsonIntegerTest, Int32FieldStringValuePartiallyNumericUnicode) { + // Not a raw string literal: the payload holds the UTF-8 bytes of U+8C37 + // U+6B4C between the digits. + EXPECT_THAT(Testee("Int32FieldStringValuePartiallyNumericUnicode") + .ParseJson(message(), + "{\"optionalInt32\": \"12\xE8\xB0\xB7\xE6\xAD\x8C" + "34\"}") + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(JsonIntegerTest, Int32FieldStringValueNonNumeric) { + EXPECT_THAT(Testee("Int32FieldStringValueNonNumeric") + .ParseJson(message(), R"json({"optionalInt32": "abc"})json") + .ParseOnly(), + Yields(IsParseError())); +} + +// Only ASCII digits are accepted in quoted integers. Other Unicode +// decimal digits, such as Arabic-Indic or fullwidth digits, are rejected. +// Not raw string literals: the payloads hold the UTF-8 bytes of U+0661 +// U+0662 U+0663 and of U+FF11 U+FF12 U+FF13. +TEST_P(JsonIntegerTest, Int32FieldStringValueArabicIndicDigits) { + constexpr absl::string_view kInput = + "{\"optionalInt32\": \"\xD9\xA1\xD9\xA2\xD9\xA3\"}"; + EXPECT_THAT(Testee("Int32FieldStringValueArabicIndicDigits") + .ParseJson(message(), kInput) + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(JsonIntegerTest, Int64FieldStringValueFullwidthDigits) { + constexpr absl::string_view kInput = + "{\"optionalInt64\": \"\xEF\xBC\x91\xEF\xBC\x92\xEF\xBC\x93\"}"; + EXPECT_THAT(Testee("Int64FieldStringValueFullwidthDigits") + .ParseJson(message(), kInput) + .ParseOnly(), + Yields(IsParseError())); +} + +// --------------------------------------------------------------------------- +// Parsers reject empty string values. +// --------------------------------------------------------------------------- + +TEST_P(JsonIntegerTest, Int32FieldEmptyString) { + EXPECT_THAT(Testee("Int32FieldEmptyString") + .ParseJson(message(), R"json({"optionalInt32": ""})json") + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(JsonIntegerTest, Uint32FieldEmptyString) { + EXPECT_THAT(Testee("Uint32FieldEmptyString") + .ParseJson(message(), R"json({"optionalUint32": ""})json") + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(JsonIntegerTest, Int64FieldEmptyString) { + EXPECT_THAT(Testee("Int64FieldEmptyString") + .ParseJson(message(), R"json({"optionalInt64": ""})json") + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(JsonIntegerTest, Uint64FieldEmptyString) { + EXPECT_THAT(Testee("Uint64FieldEmptyString") + .ParseJson(message(), R"json({"optionalUint64": ""})json") + .ParseOnly(), + Yields(IsParseError())); +} + +// --------------------------------------------------------------------------- +// Integers represented as float values are accepted. +// --------------------------------------------------------------------------- + +TEST_P(JsonIntegerTest, Int32FieldFloatTrailingZero) { + constexpr absl::string_view kInput = + R"json({"optionalInt32": 100000.000})json"; + constexpr absl::string_view kExpected = R"pb(optional_int32: 100000)pb"; + EXPECT_THAT(Testee("Int32FieldFloatTrailingZero") + .ParseJson(message(), kInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT(Testee("Int32FieldFloatTrailingZero") + .ParseJson(message(), kInput) + .SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +TEST_P(JsonIntegerTest, Int32FieldExponentialFormat) { + constexpr absl::string_view kInput = R"json({"optionalInt32": 1e5})json"; + constexpr absl::string_view kExpected = R"pb(optional_int32: 100000)pb"; + EXPECT_THAT(Testee("Int32FieldExponentialFormat") + .ParseJson(message(), kInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT(Testee("Int32FieldExponentialFormat") + .ParseJson(message(), kInput) + .SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +TEST_P(JsonIntegerTest, Int32FieldMaxFloatValue) { + constexpr absl::string_view kInput = + R"json({"optionalInt32": 2.147483647e9})json"; + constexpr absl::string_view kExpected = R"pb(optional_int32: 2147483647)pb"; + EXPECT_THAT(Testee("Int32FieldMaxFloatValue") + .ParseJson(message(), kInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT(Testee("Int32FieldMaxFloatValue") + .ParseJson(message(), kInput) + .SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +TEST_P(JsonIntegerTest, Int32FieldMinFloatValue) { + constexpr absl::string_view kInput = + R"json({"optionalInt32": -2.147483648e9})json"; + constexpr absl::string_view kExpected = R"pb(optional_int32: -2147483648)pb"; + EXPECT_THAT(Testee("Int32FieldMinFloatValue") + .ParseJson(message(), kInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT(Testee("Int32FieldMinFloatValue") + .ParseJson(message(), kInput) + .SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +TEST_P(JsonIntegerTest, Uint32FieldMaxFloatValue) { + constexpr absl::string_view kInput = + R"json({"optionalUint32": 4.294967295e9})json"; + constexpr absl::string_view kExpected = R"pb(optional_uint32: 4294967295)pb"; + EXPECT_THAT(Testee("Uint32FieldMaxFloatValue") + .ParseJson(message(), kInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT(Testee("Uint32FieldMaxFloatValue") + .ParseJson(message(), kInput) + .SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +// --------------------------------------------------------------------------- +// Parsers reject non-numeric and malformed numeric values. +// --------------------------------------------------------------------------- + +TEST_P(JsonIntegerTest, Int32FieldNotNumber) { + EXPECT_THAT(Testee("Int32FieldNotNumber") + .ParseJson(message(), R"json({"optionalInt32": "3x3"})json") + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(JsonIntegerTest, Uint32FieldNotNumber) { + EXPECT_THAT(Testee("Uint32FieldNotNumber") + .ParseJson(message(), R"json({"optionalUint32": "3x3"})json") + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(JsonIntegerTest, Int64FieldNotNumber) { + EXPECT_THAT(Testee("Int64FieldNotNumber") + .ParseJson(message(), R"json({"optionalInt64": "3x3"})json") + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(JsonIntegerTest, Uint64FieldNotNumber) { + EXPECT_THAT(Testee("Uint64FieldNotNumber") + .ParseJson(message(), R"json({"optionalUint64": "3x3"})json") + .ParseOnly(), + Yields(IsParseError())); +} + +// JSON does not allow "+" on numeric values. +TEST_P(JsonIntegerTest, Int32FieldPlusSign) { + EXPECT_THAT(Testee("Int32FieldPlusSign") + .ParseJson(message(), R"json({"optionalInt32": +1})json") + .ParseOnly(), + Yields(IsParseError())); +} + +// JSON doesn't allow leading 0s. +TEST_P(JsonIntegerTest, Int32FieldLeadingZero) { + EXPECT_THAT(Testee("Int32FieldLeadingZero") + .ParseJson(message(), R"json({"optionalInt32": 01})json") + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(JsonIntegerTest, Int32FieldNegativeWithLeadingZero) { + EXPECT_THAT(Testee("Int32FieldNegativeWithLeadingZero") + .ParseJson(message(), R"json({"optionalInt32": -01})json") + .ParseOnly(), + Yields(IsParseError())); +} + +// String values must follow the same syntax rule. Specifically leading or +// trailing spaces are not allowed. +TEST_P(JsonIntegerTest, Int32FieldLeadingSpace) { + EXPECT_THAT(Testee("Int32FieldLeadingSpace") + .ParseJson(message(), R"json({"optionalInt32": " 1"})json") + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(JsonIntegerTest, Int32FieldTrailingSpace) { + EXPECT_THAT(Testee("Int32FieldTrailingSpace") + .ParseJson(message(), R"json({"optionalInt32": "1 "})json") + .ParseOnly(), + Yields(IsParseError())); +} + +// --------------------------------------------------------------------------- +// 64-bit values are serialized as strings. +// --------------------------------------------------------------------------- + +TEST_P(JsonIntegerTest, Int64FieldBeString) { + EXPECT_THAT(Testee(kP1, "Int64FieldBeString") + .ParseJson(message(), R"json({"optionalInt64": 1})json") + .SerializeJson(), + Yields(JsonPayload( + HasJsonMemberThat("optionalInt64", IsJsonString("1"))))); +} + +TEST_P(JsonIntegerTest, Uint64FieldBeString) { + EXPECT_THAT(Testee(kP1, "Uint64FieldBeString") + .ParseJson(message(), R"json({"optionalUint64": 1})json") + .SerializeJson(), + Yields(JsonPayload( + HasJsonMemberThat("optionalUint64", IsJsonString("1"))))); +} + +INSTANTIATE_TEST_SUITE_P(All, JsonIntegerTest, ValuesIn(AllTestMessageTypes()), + MessageTypeParamName); + +} // namespace +} // namespace conformance +} // namespace protobuf +} // namespace google
diff --git a/conformance/json_map_test.cc b/conformance/json_map_test.cc new file mode 100644 index 0000000..f92eb75 --- /dev/null +++ b/conformance/json_map_test.cc
@@ -0,0 +1,229 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2026 Google LLC. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +// JSON conformance tests for map fields: keys are always JSON strings (so +// unquoted integer and bool keys are rejected and escapes in keys are +// honored) and message values are nested objects. This replaces the "Map +// fields" block of the legacy +// BinaryAndJsonConformanceSuiteImpl<M>::RunJsonTestsForNonRepeatedTypes(), +// for every test message type; the test names and the requests sent to the +// testee are identical to the legacy ones. +// +// Like the legacy RunValidJsonTest(), the valid input is sent twice: once to +// be serialized as binary ("<name>.ProtobufOutput") and once as JSON +// ("<name>.JsonOutput"). The legacy ExpectParseFailureForJson() asked for +// JSON output under a name without an output suffix. + +#include <gmock/gmock.h> +#include <gtest/gtest.h> +#include "absl/strings/string_view.h" +#include "conformance/binary_test_util.h" +#include "conformance/matchers.h" +#include "conformance/message_type_fixtures.h" +#include "conformance/testee.h" + +namespace google { +namespace protobuf { +namespace conformance { +namespace { + +using ::testing::ValuesIn; + +using JsonMapTest = MessageTypeConformanceTest; + +// Map keys are always JSON strings, even for integer and bool key types. +TEST_P(JsonMapTest, Int32MapField) { + constexpr absl::string_view kInput = + R"json({"mapInt32Int32": {"1": 2, "3": 4}})json"; + constexpr absl::string_view kExpected = R"pb( + map_int32_int32: { key: 1 value: 2 } + map_int32_int32: { key: 3 value: 4 } + )pb"; + EXPECT_THAT( + Testee("Int32MapField").ParseJson(message(), kInput).SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT( + Testee("Int32MapField").ParseJson(message(), kInput).SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +TEST_P(JsonMapTest, Int32MapFieldKeyNotQuoted) { + EXPECT_THAT( + Testee(kP1, "Int32MapFieldKeyNotQuoted") + .ParseJson(message(), R"json({"mapInt32Int32": {1: 2, 3: 4}})json") + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(JsonMapTest, Uint32MapField) { + constexpr absl::string_view kInput = + R"json({"mapUint32Uint32": {"1": 2, "3": 4}})json"; + constexpr absl::string_view kExpected = R"pb( + map_uint32_uint32: { key: 1 value: 2 } + map_uint32_uint32: { key: 3 value: 4 } + )pb"; + EXPECT_THAT( + Testee("Uint32MapField").ParseJson(message(), kInput).SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT( + Testee("Uint32MapField").ParseJson(message(), kInput).SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +TEST_P(JsonMapTest, Uint32MapFieldKeyNotQuoted) { + EXPECT_THAT( + Testee(kP1, "Uint32MapFieldKeyNotQuoted") + .ParseJson(message(), R"json({"mapUint32Uint32": {1: 2, 3: 4}})json") + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(JsonMapTest, Int64MapField) { + constexpr absl::string_view kInput = + R"json({"mapInt64Int64": {"1": 2, "3": 4}})json"; + constexpr absl::string_view kExpected = R"pb( + map_int64_int64: { key: 1 value: 2 } + map_int64_int64: { key: 3 value: 4 } + )pb"; + EXPECT_THAT( + Testee("Int64MapField").ParseJson(message(), kInput).SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT( + Testee("Int64MapField").ParseJson(message(), kInput).SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +TEST_P(JsonMapTest, Int64MapFieldKeyNotQuoted) { + EXPECT_THAT( + Testee(kP1, "Int64MapFieldKeyNotQuoted") + .ParseJson(message(), R"json({"mapInt64Int64": {1: 2, 3: 4}})json") + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(JsonMapTest, Uint64MapField) { + constexpr absl::string_view kInput = + R"json({"mapUint64Uint64": {"1": 2, "3": 4}})json"; + constexpr absl::string_view kExpected = R"pb( + map_uint64_uint64: { key: 1 value: 2 } + map_uint64_uint64: { key: 3 value: 4 } + )pb"; + EXPECT_THAT( + Testee("Uint64MapField").ParseJson(message(), kInput).SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT( + Testee("Uint64MapField").ParseJson(message(), kInput).SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +TEST_P(JsonMapTest, Uint64MapFieldKeyNotQuoted) { + EXPECT_THAT( + Testee(kP1, "Uint64MapFieldKeyNotQuoted") + .ParseJson(message(), R"json({"mapUint64Uint64": {1: 2, 3: 4}})json") + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(JsonMapTest, BoolMapField) { + constexpr absl::string_view kInput = + R"json({"mapBoolBool": {"true": true, "false": false}})json"; + constexpr absl::string_view kExpected = R"pb( + map_bool_bool: { key: true value: true } + map_bool_bool: { key: false value: false } + )pb"; + EXPECT_THAT( + Testee("BoolMapField").ParseJson(message(), kInput).SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT( + Testee("BoolMapField").ParseJson(message(), kInput).SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +TEST_P(JsonMapTest, BoolMapFieldKeyNotQuoted) { + EXPECT_THAT( + Testee(kP1, "BoolMapFieldKeyNotQuoted") + .ParseJson(message(), + R"json({"mapBoolBool": {true: true, false: false}})json") + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(JsonMapTest, MessageMapField) { + constexpr absl::string_view kInput = R"json({ + "mapStringNestedMessage": { + "hello": {"a": 1234}, + "world": {"a": 5678} + } + })json"; + constexpr absl::string_view kExpected = R"pb( + map_string_nested_message: { + key: "hello" + value: { a: 1234 } + } + map_string_nested_message: { + key: "world" + value: { a: 5678 } + } + )pb"; + EXPECT_THAT( + Testee("MessageMapField").ParseJson(message(), kInput).SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT( + Testee("MessageMapField").ParseJson(message(), kInput).SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +// Since Map keys are represented as JSON strings, escaping should be allowed. +TEST_P(JsonMapTest, Int32MapEscapedKey) { + constexpr absl::string_view kInput = + R"json({"mapInt32Int32": {"\u0031": 2}})json"; + constexpr absl::string_view kExpected = + R"pb(map_int32_int32: { key: 1 value: 2 })pb"; + EXPECT_THAT(Testee("Int32MapEscapedKey") + .ParseJson(message(), kInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT( + Testee("Int32MapEscapedKey").ParseJson(message(), kInput).SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +TEST_P(JsonMapTest, Int64MapEscapedKey) { + constexpr absl::string_view kInput = + R"json({"mapInt64Int64": {"\u0031": 2}})json"; + constexpr absl::string_view kExpected = + R"pb(map_int64_int64: { key: 1 value: 2 })pb"; + EXPECT_THAT(Testee("Int64MapEscapedKey") + .ParseJson(message(), kInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT( + Testee("Int64MapEscapedKey").ParseJson(message(), kInput).SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +TEST_P(JsonMapTest, BoolMapEscapedKey) { + constexpr absl::string_view kInput = + R"json({"mapBoolBool": {"tr\u0075e": true}})json"; + constexpr absl::string_view kExpected = + R"pb(map_bool_bool: { key: true value: true })pb"; + EXPECT_THAT(Testee("BoolMapEscapedKey") + .ParseJson(message(), kInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT( + Testee("BoolMapEscapedKey").ParseJson(message(), kInput).SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +INSTANTIATE_TEST_SUITE_P(All, JsonMapTest, ValuesIn(AllTestMessageTypes()), + MessageTypeParamName); + +} // namespace +} // namespace conformance +} // namespace protobuf +} // namespace google
diff --git a/conformance/json_message_test.cc b/conformance/json_message_test.cc new file mode 100644 index 0000000..92907ed --- /dev/null +++ b/conformance/json_message_test.cc
@@ -0,0 +1,55 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2026 Google LLC. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +// JSON conformance tests for (singular) message fields: a nested message +// given as a JSON object. This replaces the "Message fields" block of the +// legacy +// BinaryAndJsonConformanceSuiteImpl<M>::RunJsonTestsForNonRepeatedTypes(); the +// test name and the requests sent to the testee are identical to the legacy +// ones. +// +// Like the legacy RunValidJsonTest(), the valid input is sent twice: once to +// be serialized as binary ("<name>.ProtobufOutput") and once as JSON +// ("<name>.JsonOutput"). + +#include <gmock/gmock.h> +#include <gtest/gtest.h> +#include "absl/strings/string_view.h" +#include "conformance/binary_test_util.h" +#include "conformance/matchers.h" +#include "conformance/message_type_fixtures.h" +#include "conformance/testee.h" + +namespace google { +namespace protobuf { +namespace conformance { +namespace { + +using ::testing::ValuesIn; + +using JsonMessageTest = MessageTypeConformanceTest; + +TEST_P(JsonMessageTest, MessageField) { + constexpr absl::string_view kInput = + R"json({"optionalNestedMessage": {"a": 1234}})json"; + constexpr absl::string_view kExpected = + R"pb(optional_nested_message: { a: 1234 })pb"; + EXPECT_THAT( + Testee("MessageField").ParseJson(message(), kInput).SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT( + Testee("MessageField").ParseJson(message(), kInput).SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +INSTANTIATE_TEST_SUITE_P(All, JsonMessageTest, ValuesIn(AllTestMessageTypes()), + MessageTypeParamName); + +} // namespace +} // namespace conformance +} // namespace protobuf +} // namespace google
diff --git a/conformance/json_null_test.cc b/conformance/json_null_test.cc new file mode 100644 index 0000000..403dfe0 --- /dev/null +++ b/conformance/json_null_test.cc
@@ -0,0 +1,134 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2026 Google LLC. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +// JSON conformance tests for `null`: it is accepted (and means "unset") as +// the value of a field of any type, but not as an element of a repeated +// field or as a map key or value. This replaces the legacy +// BinaryAndJsonConformanceSuiteImpl<M>::RunJsonTestsForNullTypes(), for +// every test message type; the test names and the requests sent to the +// testee are identical to the legacy ones. +// +// Like the legacy RunValidJsonTest(), the valid input is sent twice: once to +// be serialized as binary ("<name>.ProtobufOutput") and once as JSON +// ("<name>.JsonOutput"). The legacy ExpectParseFailureForJson() asked for +// JSON output under a name without an output suffix. + +#include <gmock/gmock.h> +#include <gtest/gtest.h> +#include "absl/strings/string_view.h" +#include "conformance/binary_test_util.h" +#include "conformance/matchers.h" +#include "conformance/message_type_fixtures.h" +#include "conformance/testee.h" + +namespace google { +namespace protobuf { +namespace conformance { +namespace { + +using ::testing::ValuesIn; + +class JsonNullTest : public MessageTypeConformanceTest { + private: + TestPriority DefaultPriority() const override { return kP1; } +}; + +// "null" is accepted for all field types, and leaves the field unset. +TEST_P(JsonNullTest, AllFieldAcceptNull) { + constexpr absl::string_view kInput = R"json({ + "optionalInt32": null, + "optionalInt64": null, + "optionalUint32": null, + "optionalUint64": null, + "optionalSint32": null, + "optionalSint64": null, + "optionalFixed32": null, + "optionalFixed64": null, + "optionalSfixed32": null, + "optionalSfixed64": null, + "optionalFloat": null, + "optionalDouble": null, + "optionalBool": null, + "optionalString": null, + "optionalBytes": null, + "optionalNestedEnum": null, + "optionalNestedMessage": null, + "repeatedInt32": null, + "repeatedInt64": null, + "repeatedUint32": null, + "repeatedUint64": null, + "repeatedSint32": null, + "repeatedSint64": null, + "repeatedFixed32": null, + "repeatedFixed64": null, + "repeatedSfixed32": null, + "repeatedSfixed64": null, + "repeatedFloat": null, + "repeatedDouble": null, + "repeatedBool": null, + "repeatedString": null, + "repeatedBytes": null, + "repeatedNestedEnum": null, + "repeatedNestedMessage": null, + "mapInt32Int32": null, + "mapBoolBool": null, + "mapStringNestedMessage": null + })json"; + EXPECT_THAT(Testee(kP0, "AllFieldAcceptNull") + .ParseJson(message(), kInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto("")))); + EXPECT_THAT(Testee(kP0, "AllFieldAcceptNull") + .ParseJson(message(), kInput) + .SerializeJson(), + Yields(WhenParsed(EqualsTextProto("")))); +} + +// Repeated field elements cannot be null. +TEST_P(JsonNullTest, RepeatedFieldPrimitiveElementIsNull) { + EXPECT_THAT( + Testee("RepeatedFieldPrimitiveElementIsNull") + .ParseJson(message(), R"json({"repeatedInt32": [1, null, 2]})json") + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(JsonNullTest, RepeatedFieldMessageElementIsNull) { + EXPECT_THAT( + Testee("RepeatedFieldMessageElementIsNull") + .ParseJson( + message(), + R"json({"repeatedNestedMessage": [{"a":1}, null, {"a":2}]})json") + .ParseOnly(), + Yields(IsParseError())); +} + +// Map field keys cannot be null. +TEST_P(JsonNullTest, MapFieldKeyIsNull) { + EXPECT_THAT( + Testee("MapFieldKeyIsNull") + .ParseJson(message(), R"json({"mapInt32Int32": {null: 1}})json") + .ParseOnly(), + Yields(IsParseError())); +} + +// Map field values cannot be null. +TEST_P(JsonNullTest, MapFieldValueIsNull) { + EXPECT_THAT( + Testee("MapFieldValueIsNull") + .ParseJson(message(), R"json({"mapInt32Int32": {"0": null}})json") + .ParseOnly(), + Yields(IsParseError())); +} + +INSTANTIATE_TEST_SUITE_P(All, JsonNullTest, ValuesIn(AllTestMessageTypes()), + MessageTypeParamName); + +} // namespace +} // namespace conformance +} // namespace protobuf +} // namespace google
diff --git a/conformance/json_oneof_test.cc b/conformance/json_oneof_test.cc new file mode 100644 index 0000000..540cf12 --- /dev/null +++ b/conformance/json_oneof_test.cc
@@ -0,0 +1,201 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2026 Google LLC. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +// JSON conformance tests for oneof fields: setting more than one member of a +// oneof in the same object, `null` members, and explicitly zero/empty +// members (which must still mark the oneof as set). This replaces the +// "Oneof fields" block of the legacy +// BinaryAndJsonConformanceSuiteImpl<M>::RunJsonTestsForNonRepeatedTypes(), +// for every test message type; the test names and the requests sent to the +// testee are identical to the legacy ones. +// +// Like the legacy RunValidJsonTest(), the valid input is sent twice: once to +// be serialized as binary ("<name>.ProtobufOutput") and once as JSON +// ("<name>.JsonOutput"). The legacy RunValidJsonTestOrParseFailure() asked +// for binary output under a name without an output suffix. + +#include <gmock/gmock.h> +#include <gtest/gtest.h> +#include "absl/strings/string_view.h" +#include "conformance/binary_test_util.h" +#include "conformance/conformance.pb.h" +#include "conformance/matchers.h" +#include "conformance/message_type_fixtures.h" +#include "conformance/testee.h" + +namespace google { +namespace protobuf { +namespace conformance { +namespace { + +using ::testing::AnyOf; +using ::testing::ValuesIn; + +class JsonOneofTest : public MessageTypeConformanceTest { + private: + TestPriority DefaultPriority() const override { return kP1; } +}; + +// Setting two members of the same oneof may either be rejected or resolved +// in favor of the last one. +TEST_P(JsonOneofTest, OneofFieldDuplicate) { + EXPECT_THAT( + Testee(kP0, "OneofFieldDuplicate") + .ParseJson(message(), + R"json({"oneofUint32": 1, "oneofString": "test"})json") + .ParseOnly({/*output_format=*/::conformance::PROTOBUF}), + Yields(AnyOf(IsParseError(), WhenParsed(EqualsTextProto( + R"pb(oneof_string: "test")pb"))))); +} + +TEST_P(JsonOneofTest, OneofFieldDuplicate2) { + EXPECT_THAT( + Testee(kP0, "OneofFieldDuplicate2") + .ParseJson(message(), + R"json({"oneofString": "test", "oneofUint32": 1})json") + .ParseOnly({/*output_format=*/::conformance::PROTOBUF}), + Yields(AnyOf(IsParseError(), + WhenParsed(EqualsTextProto(R"pb(oneof_uint32: 1)pb"))))); +} + +// A null oneof member does not count as setting the oneof, so a second, +// non-null member is not a duplicate. +TEST_P(JsonOneofTest, OneofFieldNullFirst) { + constexpr absl::string_view kInput = + R"json({"oneofUint32": null, "oneofString": "test"})json"; + constexpr absl::string_view kExpected = R"pb(oneof_string: "test")pb"; + EXPECT_THAT(Testee(kP0, "OneofFieldNullFirst") + .ParseJson(message(), kInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT(Testee(kP0, "OneofFieldNullFirst") + .ParseJson(message(), kInput) + .SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +TEST_P(JsonOneofTest, OneofFieldNullSecond) { + constexpr absl::string_view kInput = + R"json({"oneofString": "test", "oneofUint32": null})json"; + constexpr absl::string_view kExpected = R"pb(oneof_string: "test")pb"; + EXPECT_THAT(Testee(kP0, "OneofFieldNullSecond") + .ParseJson(message(), kInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT(Testee(kP0, "OneofFieldNullSecond") + .ParseJson(message(), kInput) + .SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +// A oneof member explicitly set to its default value is still set. +TEST_P(JsonOneofTest, OneofZeroUint32) { + constexpr absl::string_view kInput = R"json({"oneofUint32": 0})json"; + constexpr absl::string_view kExpected = R"pb(oneof_uint32: 0)pb"; + EXPECT_THAT( + Testee("OneofZeroUint32").ParseJson(message(), kInput).SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT( + Testee("OneofZeroUint32").ParseJson(message(), kInput).SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +TEST_P(JsonOneofTest, OneofZeroMessage) { + constexpr absl::string_view kInput = R"json({"oneofNestedMessage": {}})json"; + constexpr absl::string_view kExpected = R"pb(oneof_nested_message: {})pb"; + EXPECT_THAT( + Testee("OneofZeroMessage").ParseJson(message(), kInput).SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT( + Testee("OneofZeroMessage").ParseJson(message(), kInput).SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +TEST_P(JsonOneofTest, OneofZeroString) { + constexpr absl::string_view kInput = R"json({"oneofString": ""})json"; + constexpr absl::string_view kExpected = R"pb(oneof_string: "")pb"; + EXPECT_THAT( + Testee("OneofZeroString").ParseJson(message(), kInput).SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT( + Testee("OneofZeroString").ParseJson(message(), kInput).SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +TEST_P(JsonOneofTest, OneofZeroBytes) { + constexpr absl::string_view kInput = R"json({"oneofBytes": ""})json"; + constexpr absl::string_view kExpected = R"pb(oneof_bytes: "")pb"; + EXPECT_THAT( + Testee("OneofZeroBytes").ParseJson(message(), kInput).SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT( + Testee("OneofZeroBytes").ParseJson(message(), kInput).SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +TEST_P(JsonOneofTest, OneofZeroBool) { + constexpr absl::string_view kInput = R"json({"oneofBool": false})json"; + constexpr absl::string_view kExpected = R"pb(oneof_bool: false)pb"; + EXPECT_THAT( + Testee("OneofZeroBool").ParseJson(message(), kInput).SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT( + Testee("OneofZeroBool").ParseJson(message(), kInput).SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +TEST_P(JsonOneofTest, OneofZeroUint64) { + constexpr absl::string_view kInput = R"json({"oneofUint64": 0})json"; + constexpr absl::string_view kExpected = R"pb(oneof_uint64: 0)pb"; + EXPECT_THAT( + Testee("OneofZeroUint64").ParseJson(message(), kInput).SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT( + Testee("OneofZeroUint64").ParseJson(message(), kInput).SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +TEST_P(JsonOneofTest, OneofZeroFloat) { + constexpr absl::string_view kInput = R"json({"oneofFloat": 0.0})json"; + constexpr absl::string_view kExpected = R"pb(oneof_float: 0)pb"; + EXPECT_THAT( + Testee("OneofZeroFloat").ParseJson(message(), kInput).SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT( + Testee("OneofZeroFloat").ParseJson(message(), kInput).SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +TEST_P(JsonOneofTest, OneofZeroDouble) { + constexpr absl::string_view kInput = R"json({"oneofDouble": 0.0})json"; + constexpr absl::string_view kExpected = R"pb(oneof_double: 0)pb"; + EXPECT_THAT( + Testee("OneofZeroDouble").ParseJson(message(), kInput).SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT( + Testee("OneofZeroDouble").ParseJson(message(), kInput).SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +TEST_P(JsonOneofTest, OneofZeroEnum) { + constexpr absl::string_view kInput = R"json({"oneofEnum":"FOO"})json"; + constexpr absl::string_view kExpected = R"pb(oneof_enum: FOO)pb"; + EXPECT_THAT( + Testee("OneofZeroEnum").ParseJson(message(), kInput).SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT( + Testee("OneofZeroEnum").ParseJson(message(), kInput).SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +INSTANTIATE_TEST_SUITE_P(All, JsonOneofTest, ValuesIn(AllTestMessageTypes()), + MessageTypeParamName); + +} // namespace +} // namespace conformance +} // namespace protobuf +} // namespace google
diff --git a/conformance/json_repeated_test.cc b/conformance/json_repeated_test.cc new file mode 100644 index 0000000..bc94712 --- /dev/null +++ b/conformance/json_repeated_test.cc
@@ -0,0 +1,269 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2026 Google LLC. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +// JSON conformance tests for repeated fields: JSON arrays of primitives, +// enums, strings, bytes and messages round-trip, array elements of the wrong +// type are rejected, a single value where an array was expected is rejected, +// and so is a trailing comma in an array. This replaces the legacy +// BinaryAndJsonConformanceSuiteImpl<M>::RunJsonTestsForRepeatedTypes(); the +// test names and the requests sent to the testee are identical to the legacy +// ones. +// +// Like the legacy RunValidJsonTest(), each valid input is sent twice: once to +// be serialized as binary ("<name>.ProtobufOutput") and once as JSON +// ("<name>.JsonOutput"). ExpectParseFailureForJson() asked for JSON output +// under a name without an output suffix. + +#include <gmock/gmock.h> +#include <gtest/gtest.h> +#include "absl/strings/string_view.h" +#include "conformance/binary_test_util.h" +#include "conformance/matchers.h" +#include "conformance/message_type_fixtures.h" +#include "conformance/testee.h" + +namespace google { +namespace protobuf { +namespace conformance { +namespace { + +using ::testing::ValuesIn; + +using JsonRepeatedTest = MessageTypeConformanceTest; + +// --------------------------------------------------------------------------- +// Repeated fields of each kind round-trip. +// --------------------------------------------------------------------------- + +TEST_P(JsonRepeatedTest, PrimitiveRepeatedField) { + constexpr absl::string_view kInput = + R"json({"repeatedInt32": [1, 2, 3, 4]})json"; + constexpr absl::string_view kExpected = + R"pb(repeated_int32: [ 1, 2, 3, 4 ])pb"; + EXPECT_THAT(Testee("PrimitiveRepeatedField") + .ParseJson(message(), kInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT(Testee("PrimitiveRepeatedField") + .ParseJson(message(), kInput) + .SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +TEST_P(JsonRepeatedTest, EnumRepeatedField) { + constexpr absl::string_view kInput = + R"json({"repeatedNestedEnum": ["FOO", "BAR", "BAZ"]})json"; + constexpr absl::string_view kExpected = + R"pb(repeated_nested_enum: [ FOO, BAR, BAZ ])pb"; + EXPECT_THAT(Testee("EnumRepeatedField") + .ParseJson(message(), kInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT( + Testee("EnumRepeatedField").ParseJson(message(), kInput).SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +TEST_P(JsonRepeatedTest, StringRepeatedField) { + constexpr absl::string_view kInput = + R"json({"repeatedString": ["Hello", "world"]})json"; + constexpr absl::string_view kExpected = + R"pb(repeated_string: [ "Hello", "world" ])pb"; + EXPECT_THAT(Testee("StringRepeatedField") + .ParseJson(message(), kInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT(Testee("StringRepeatedField") + .ParseJson(message(), kInput) + .SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +TEST_P(JsonRepeatedTest, BytesRepeatedField) { + constexpr absl::string_view kInput = + R"json({"repeatedBytes": ["AAEC", "AQI="]})json"; + constexpr absl::string_view kExpected = + R"pb(repeated_bytes: [ "\x00\x01\x02", "\x01\x02" ])pb"; + EXPECT_THAT(Testee("BytesRepeatedField") + .ParseJson(message(), kInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT( + Testee("BytesRepeatedField").ParseJson(message(), kInput).SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +TEST_P(JsonRepeatedTest, MessageRepeatedField) { + constexpr absl::string_view kInput = + R"json({"repeatedNestedMessage": [{"a": 1234}, {"a": 5678}]})json"; + constexpr absl::string_view kExpected = R"pb( + repeated_nested_message: { a: 1234 } + repeated_nested_message: { a: 5678 } + )pb"; + EXPECT_THAT(Testee("MessageRepeatedField") + .ParseJson(message(), kInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT(Testee("MessageRepeatedField") + .ParseJson(message(), kInput) + .SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +// --------------------------------------------------------------------------- +// Repeated field elements of the wrong type are rejected. +// --------------------------------------------------------------------------- + +TEST_P(JsonRepeatedTest, + RepeatedFieldWrongElementTypeExpectingIntegersGotBool) { + EXPECT_THAT(Testee("RepeatedFieldWrongElementTypeExpectingIntegersGotBool") + .ParseJson(message(), + R"json({"repeatedInt32": [1, false, 3, 4]})json") + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(JsonRepeatedTest, + RepeatedFieldWrongElementTypeExpectingIntegersGotString) { + EXPECT_THAT(Testee("RepeatedFieldWrongElementTypeExpectingIntegersGotString") + .ParseJson(message(), + R"json({"repeatedInt32": [1, 2, "name", 4]})json") + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(JsonRepeatedTest, + RepeatedFieldWrongElementTypeExpectingIntegersGotMessage) { + EXPECT_THAT( + Testee("RepeatedFieldWrongElementTypeExpectingIntegersGotMessage") + .ParseJson(message(), + R"json({"repeatedInt32": [1, 2, 3, {"a": 4}]})json") + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(JsonRepeatedTest, RepeatedFieldWrongElementTypeExpectingStringsGotInt) { + EXPECT_THAT( + Testee("RepeatedFieldWrongElementTypeExpectingStringsGotInt") + .ParseJson(message(), + R"json({"repeatedString": ["1", 2, "3", "4"]})json") + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(JsonRepeatedTest, RepeatedFieldWrongElementTypeExpectingStringsGotBool) { + EXPECT_THAT( + Testee("RepeatedFieldWrongElementTypeExpectingStringsGotBool") + .ParseJson(message(), + R"json({"repeatedString": ["1", "2", false, "4"]})json") + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(JsonRepeatedTest, + RepeatedFieldWrongElementTypeExpectingStringsGotMessage) { + EXPECT_THAT( + Testee("RepeatedFieldWrongElementTypeExpectingStringsGotMessage") + .ParseJson(message(), + R"json({"repeatedString": ["1", 2, "3", {"a": 4}]})json") + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(JsonRepeatedTest, RepeatedFieldWrongElementTypeExpectingMessagesGotInt) { + EXPECT_THAT( + Testee("RepeatedFieldWrongElementTypeExpectingMessagesGotInt") + .ParseJson(message(), + R"json({"repeatedNestedMessage": [{"a": 1}, 2]})json") + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(JsonRepeatedTest, + RepeatedFieldWrongElementTypeExpectingMessagesGotBool) { + EXPECT_THAT( + Testee("RepeatedFieldWrongElementTypeExpectingMessagesGotBool") + .ParseJson(message(), + R"json({"repeatedNestedMessage": [{"a": 1}, false]})json") + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(JsonRepeatedTest, + RepeatedFieldWrongElementTypeExpectingMessagesGotString) { + EXPECT_THAT( + Testee("RepeatedFieldWrongElementTypeExpectingMessagesGotString") + .ParseJson(message(), + R"json({"repeatedNestedMessage": [{"a": 1}, "2"]})json") + .ParseOnly(), + Yields(IsParseError())); +} + +// --------------------------------------------------------------------------- +// A single value where a repeated field was expected is rejected, even if it +// is of the right type. +// --------------------------------------------------------------------------- + +TEST_P(JsonRepeatedTest, SingleValueForRepeatedFieldInt32) { + EXPECT_THAT(Testee("SingleValueForRepeatedFieldInt32") + .ParseJson(message(), R"json({"repeatedInt32": 1})json") + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(JsonRepeatedTest, SingleValueForRepeatedFieldMessage) { + EXPECT_THAT(Testee("SingleValueForRepeatedFieldMessage") + .ParseJson(message(), + R"json({"repeatedNestedMessage": {"a": 1}})json") + .ParseOnly(), + Yields(IsParseError())); +} + +// --------------------------------------------------------------------------- +// A trailing comma in a repeated field is rejected. +// --------------------------------------------------------------------------- + +TEST_P(JsonRepeatedTest, RepeatedFieldTrailingComma) { + EXPECT_THAT( + Testee(kP1, "RepeatedFieldTrailingComma") + .ParseJson(message(), R"json({"repeatedInt32": [1, 2, 3, 4,]})json") + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(JsonRepeatedTest, RepeatedFieldTrailingCommaWithSpace) { + EXPECT_THAT( + Testee(kP1, "RepeatedFieldTrailingCommaWithSpace") + .ParseJson(message(), R"json({"repeatedInt32": [1, 2, 3, 4 ,]})json") + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(JsonRepeatedTest, RepeatedFieldTrailingCommaWithSpaceCommaSpace) { + EXPECT_THAT( + Testee(kP1, "RepeatedFieldTrailingCommaWithSpaceCommaSpace") + .ParseJson(message(), R"json({"repeatedInt32": [1, 2, 3, 4 , ]})json") + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(JsonRepeatedTest, RepeatedFieldTrailingCommaWithNewlines) { + EXPECT_THAT( + Testee(kP1, "RepeatedFieldTrailingCommaWithNewlines") + .ParseJson(message(), + "{\"repeatedInt32\": [\n 1,\n 2,\n 3,\n 4,\n]}") + .ParseOnly(), + Yields(IsParseError())); +} + +INSTANTIATE_TEST_SUITE_P(All, JsonRepeatedTest, ValuesIn(AllTestMessageTypes()), + MessageTypeParamName); + +} // namespace +} // namespace conformance +} // namespace protobuf +} // namespace google
diff --git a/conformance/json_string_test.cc b/conformance/json_string_test.cc new file mode 100644 index 0000000..a9bde6f --- /dev/null +++ b/conformance/json_string_test.cc
@@ -0,0 +1,258 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2026 Google LLC. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +// JSON conformance tests for string and bytes fields: Unicode text, JSON +// escapes (including \u escapes, surrogate pairs and embedded NUL), rejected +// malformed escapes, unpaired surrogates, non-string values and single-quoted +// strings, and base64 / base64url bytes. This replaces the "String fields" +// and "Bytes fields" blocks (and the single-quote tests at the end) of the +// legacy +// BinaryAndJsonConformanceSuiteImpl<M>::RunJsonTestsForNonRepeatedTypes(); the +// test names and the requests sent to the testee are identical to the legacy +// ones. +// +// Like the legacy RunValidJsonTest(), each valid input is sent twice: once to +// be serialized as binary ("<name>.ProtobufOutput") and once as JSON +// ("<name>.JsonOutput"). ExpectParseFailureForJson() asked for JSON output +// under a name without an output suffix. + +#include <gmock/gmock.h> +#include <gtest/gtest.h> +#include "absl/strings/string_view.h" +#include "conformance/binary_test_util.h" +#include "conformance/matchers.h" +#include "conformance/message_type_fixtures.h" +#include "conformance/testee.h" + +namespace google { +namespace protobuf { +namespace conformance { +namespace { + +using ::testing::ValuesIn; + +// --------------------------------------------------------------------------- +// String fields. +// --------------------------------------------------------------------------- + +using JsonStringTest = MessageTypeConformanceTest; + +TEST_P(JsonStringTest, StringField) { + constexpr absl::string_view kInput = + R"json({"optionalString": "Hello world!"})json"; + constexpr absl::string_view kExpected = + R"pb(optional_string: "Hello world!")pb"; + EXPECT_THAT( + Testee("StringField").ParseJson(message(), kInput).SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT( + Testee("StringField").ParseJson(message(), kInput).SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +// Non-ASCII characters in the JSON text itself, as opposed to the \uXXXX +// escapes tested below. Note that kInput is deliberately not a raw string +// literal: the compiler resolves the \x escapes, so the payload holds the +// UTF-8 bytes of U+8C37 U+6B4C ("Google" in Chinese). In the raw string +// literals below, the \u escapes reach the testee's JSON parser verbatim. +TEST_P(JsonStringTest, StringFieldUnicode) { + constexpr absl::string_view kInput = + "{\"optionalString\": \"\xE8\xB0\xB7\xE6\xAD\x8C\"}"; + constexpr absl::string_view kExpected = + R"pb(optional_string: "\xE8\xB0\xB7\xE6\xAD\x8C")pb"; + EXPECT_THAT(Testee("StringFieldUnicode") + .ParseJson(message(), kInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT( + Testee("StringFieldUnicode").ParseJson(message(), kInput).SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +TEST_P(JsonStringTest, StringFieldEscape) { + constexpr absl::string_view kInput = + R"json({"optionalString": "\"\\\/\b\f\n\r\t"})json"; + constexpr absl::string_view kExpected = + R"pb(optional_string: "\"\\/\b\f\n\r\t")pb"; + EXPECT_THAT(Testee("StringFieldEscape") + .ParseJson(message(), kInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT( + Testee("StringFieldEscape").ParseJson(message(), kInput).SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +TEST_P(JsonStringTest, StringFieldUnicodeEscape) { + constexpr absl::string_view kInput = + R"json({"optionalString": "\u8C37\u6B4C"})json"; + constexpr absl::string_view kExpected = + R"pb(optional_string: "\xE8\xB0\xB7\xE6\xAD\x8C")pb"; + EXPECT_THAT(Testee("StringFieldUnicodeEscape") + .ParseJson(message(), kInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT(Testee("StringFieldUnicodeEscape") + .ParseJson(message(), kInput) + .SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +// U+1F601 (grinning face with smiling eyes). +TEST_P(JsonStringTest, StringFieldSurrogatePair) { + constexpr absl::string_view kInput = + R"json({"optionalString": "\uD83D\uDE01"})json"; + constexpr absl::string_view kExpected = + R"pb(optional_string: "\xF0\x9F\x98\x81")pb"; + EXPECT_THAT(Testee("StringFieldSurrogatePair") + .ParseJson(message(), kInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT(Testee("StringFieldSurrogatePair") + .ParseJson(message(), kInput) + .SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +TEST_P(JsonStringTest, StringFieldEmbeddedNull) { + constexpr absl::string_view kInput = + R"json({"optionalString": "Hello\u0000world!"})json"; + constexpr absl::string_view kExpected = + R"pb(optional_string: "Hello\000world!")pb"; + EXPECT_THAT(Testee("StringFieldEmbeddedNull") + .ParseJson(message(), kInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT(Testee("StringFieldEmbeddedNull") + .ParseJson(message(), kInput) + .SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +// Unicode escapes must start with "\u" (lowercase u). +TEST_P(JsonStringTest, StringFieldUppercaseEscapeLetter) { + EXPECT_THAT( + Testee(kP1, "StringFieldUppercaseEscapeLetter") + .ParseJson(message(), "{\"optionalString\": \"\\U8C37\\U6b4C\"}") + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(JsonStringTest, StringFieldInvalidEscape) { + EXPECT_THAT( + Testee(kP1, "StringFieldInvalidEscape") + .ParseJson(message(), "{\"optionalString\": \"\\uXXXX\\u6B4C\"}") + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(JsonStringTest, StringFieldUnterminatedEscape) { + EXPECT_THAT( + Testee(kP1, "StringFieldUnterminatedEscape") + .ParseJson(message(), R"json({"optionalString": "\u8C3"})json") + .ParseOnly(), + Yields(IsParseError())); +} + +// The unpaired-surrogate inputs below are ordinary (not raw) string literals: +// MSVC rejects a lone surrogate written as \uXXXX even inside a raw string +// literal (error C3850), so the backslash is escaped to keep the \u escape +// out of the compiler's hands and deliver it to the testee's JSON parser. +TEST_P(JsonStringTest, StringFieldUnpairedHighSurrogate) { + EXPECT_THAT(Testee(kP1, "StringFieldUnpairedHighSurrogate") + .ParseJson(message(), "{\"optionalString\": \"\\uD800\"}") + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(JsonStringTest, StringFieldUnpairedLowSurrogate) { + EXPECT_THAT(Testee(kP1, "StringFieldUnpairedLowSurrogate") + .ParseJson(message(), "{\"optionalString\": \"\\uDC00\"}") + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(JsonStringTest, StringFieldSurrogateInWrongOrder) { + EXPECT_THAT( + Testee(kP1, "StringFieldSurrogateInWrongOrder") + .ParseJson(message(), "{\"optionalString\": \"\\uDE01\\uD83D\"}") + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(JsonStringTest, StringFieldNotAString) { + EXPECT_THAT(Testee("StringFieldNotAString") + .ParseJson(message(), R"json({"optionalString": 12345})json") + .ParseOnly(), + Yields(IsParseError())); +} + +// http://www.rfc-editor.org/rfc/rfc7159.txt says strings have to use double +// quotes. +TEST_P(JsonStringTest, StringFieldSingleQuoteKey) { + EXPECT_THAT( + Testee(kP1, "StringFieldSingleQuoteKey") + .ParseJson(message(), R"json({'optionalString': "Hello world!"})json") + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(JsonStringTest, StringFieldSingleQuoteValue) { + EXPECT_THAT( + Testee(kP1, "StringFieldSingleQuoteValue") + .ParseJson(message(), R"json({"optionalString": 'Hello world!'})json") + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(JsonStringTest, StringFieldSingleQuoteBoth) { + EXPECT_THAT( + Testee(kP1, "StringFieldSingleQuoteBoth") + .ParseJson(message(), R"json({'optionalString': 'Hello world!'})json") + .ParseOnly(), + Yields(IsParseError())); +} + +INSTANTIATE_TEST_SUITE_P(All, JsonStringTest, ValuesIn(AllTestMessageTypes()), + MessageTypeParamName); + +// --------------------------------------------------------------------------- +// Bytes fields. +// --------------------------------------------------------------------------- + +using JsonBytesTest = MessageTypeConformanceTest; + +TEST_P(JsonBytesTest, BytesField) { + constexpr absl::string_view kInput = R"json({"optionalBytes": "AQI="})json"; + constexpr absl::string_view kExpected = R"pb(optional_bytes: "\x01\x02")pb"; + EXPECT_THAT( + Testee("BytesField").ParseJson(message(), kInput).SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT(Testee("BytesField").ParseJson(message(), kInput).SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +TEST_P(JsonBytesTest, BytesFieldBase64Url) { + constexpr absl::string_view kInput = R"json({"optionalBytes": "-_"})json"; + constexpr absl::string_view kExpected = R"pb(optional_bytes: "\xfb")pb"; + EXPECT_THAT(Testee(kP1, "BytesFieldBase64Url") + .ParseJson(message(), kInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT(Testee(kP1, "BytesFieldBase64Url") + .ParseJson(message(), kInput) + .SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +INSTANTIATE_TEST_SUITE_P(All, JsonBytesTest, ValuesIn(AllTestMessageTypes()), + MessageTypeParamName); + +} // namespace +} // namespace conformance +} // namespace protobuf +} // namespace google
diff --git a/conformance/json_struct_test.cc b/conformance/json_struct_test.cc new file mode 100644 index 0000000..6d2fd03 --- /dev/null +++ b/conformance/json_struct_test.cc
@@ -0,0 +1,176 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2026 Google LLC. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +// JSON conformance tests for google.protobuf.Struct fields: a Struct holding +// every kind of Value, an empty ListValue member, and Structs nested 25 deep +// (must be accepted) and 200 deep (must be rejected). This replaces the +// legacy BinaryAndJsonConformanceSuiteImpl<M>::RunJsonTestsForStruct(), which +// ran for the proto3-style message types only; the test names and the +// requests sent to the testee are identical to the legacy ones. +// +// Like the legacy RunValidJsonTest(), each valid input is sent twice: once to +// be serialized as binary ("<name>.ProtobufOutput") and once as JSON +// ("<name>.JsonOutput"). ExpectParseFailureForJson() asked for JSON output +// under a name without an output suffix. + +#include <string> + +#include <gmock/gmock.h> +#include <gtest/gtest.h> +#include "absl/strings/str_cat.h" +#include "absl/strings/string_view.h" +#include "conformance/binary_test_util.h" +#include "conformance/json_test_util.h" +#include "conformance/matchers.h" +#include "conformance/message_type_fixtures.h" +#include "conformance/testee.h" + +namespace google { +namespace protobuf { +namespace conformance { +namespace { + +using ::testing::ValuesIn; + +using JsonStructTest = MessageTypeConformanceTest; + +TEST_P(JsonStructTest, Struct) { + constexpr absl::string_view kInput = R"json({ + "optionalStruct": { + "nullValue": null, + "intValue": 1234, + "boolValue": true, + "doubleValue": 1234.5678, + "stringValue": "Hello world!", + "listValue": [1234, "5678"], + "objectValue": { + "value": 0 + } + } + })json"; + constexpr absl::string_view kExpected = R"pb( + optional_struct: { + fields: { + key: "nullValue" + value: { null_value: NULL_VALUE } + } + fields: { + key: "intValue" + value: { number_value: 1234 } + } + fields: { + key: "boolValue" + value: { bool_value: true } + } + fields: { + key: "doubleValue" + value: { number_value: 1234.5678 } + } + fields: { + key: "stringValue" + value: { string_value: "Hello world!" } + } + fields: { + key: "listValue" + value: { + list_value: { + values: { number_value: 1234 } + values: { string_value: "5678" } + } + } + } + fields: { + key: "objectValue" + value: { + struct_value: { + fields: { + key: "value" + value: { number_value: 0 } + } + } + } + } + } + )pb"; + EXPECT_THAT(Testee("Struct").ParseJson(message(), kInput).SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT(Testee("Struct").ParseJson(message(), kInput).SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +TEST_P(JsonStructTest, StructWithEmptyListValue) { + constexpr absl::string_view kInput = R"json({ + "optionalStruct": { + "listValue": [] + } + })json"; + constexpr absl::string_view kExpected = R"pb( + optional_struct: { + fields: { + key: "listValue" + value: { list_value: {} } + } + } + )pb"; + EXPECT_THAT(Testee("StructWithEmptyListValue") + .ParseJson(message(), kInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT(Testee("StructWithEmptyListValue") + .ParseJson(message(), kInput) + .SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +TEST_P(JsonStructTest, StructDeepNesting25) { + const std::string input = + absl::StrCat(R"({"optionalStruct": {)", Repeat(R"("n": {)", 25), + R"("value": 1)", std::string(25, '}'), "}}"); + const std::string expected = + absl::StrCat("optional_struct: {\n", + Repeat(" fields: {\n" + " key: \"n\"\n" + " value: {\n" + " struct_value: {\n", + 25), + " fields: {\n" + " key: \"value\"\n" + " value: { number_value: 1 }\n" + " }\n", + Repeat(" }\n" + " }\n" + " }\n", + 25), + "}\n"); + EXPECT_THAT(Testee(kP1, "StructDeepNesting25") + .ParseJson(message(), input) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(expected)))); + EXPECT_THAT(Testee(kP1, "StructDeepNesting25") + .ParseJson(message(), input) + .SerializeJson(), + Yields(WhenParsed(EqualsTextProto(expected)))); +} + +TEST_P(JsonStructTest, StructDeepNesting200) { + const std::string input = + absl::StrCat(R"({"optionalStruct": {)", Repeat(R"("n": {)", 200), + R"("value": 1)", std::string(200, '}'), "}}"); + EXPECT_THAT(Testee(kP1, "StructDeepNesting200") + .ParseJson(message(), input) + .ParseOnly(), + Yields(IsParseError())); +} + +INSTANTIATE_TEST_SUITE_P(All, JsonStructTest, + ValuesIn(Proto3TestMessageTypes()), + MessageTypeParamName); + +} // namespace +} // namespace conformance +} // namespace protobuf +} // namespace google
diff --git a/conformance/json_test_util.h b/conformance/json_test_util.h new file mode 100644 index 0000000..fc18fa9 --- /dev/null +++ b/conformance/json_test_util.h
@@ -0,0 +1,133 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2026 Google LLC. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +#ifndef GOOGLE_PROTOBUF_CONFORMANCE_JSON_TEST_UTIL_H__ +#define GOOGLE_PROTOBUF_CONFORMANCE_JSON_TEST_UTIL_H__ + +#include <string> + +#include <gmock/gmock.h> +#include "absl/strings/str_cat.h" +#include "absl/strings/string_view.h" +#include "json/value.h" + +// Helpers shared by the gtest-based JSON conformance tests (json_*_test.cc). +// +// Like binary_test_util.h, this file only holds matchers and pure values. +// Every EXPECT_THAT lives in the tests themselves, so that a failure points +// at the test and not at a helper; the tests use the framework directly: +// +// EXPECT_THAT( +// Testee("HelloWorld") +// .ParseJson(message(), R"json({"optionalString": "Hi"})json") +// .SerializeBinary(), +// Yields(WhenParsed(EqualsTextProto(R"pb(optional_string: 'Hi')pb")))); +// +// The message type sets the tests are parameterized over are in +// binary_test_util.h: AllTestMessageTypes(), Proto3TestMessageTypes() and +// Proto2TestMessageTypes(). + +namespace google { +namespace protobuf { +namespace conformance { + +// Returns `s` repeated `count` times, for building the deeply nested JSON +// inputs of the recursion-limit tests. +inline std::string Repeat(absl::string_view s, int count) { + std::string result; + for (int i = 0; i < count; ++i) { + absl::StrAppend(&result, s); + } + return result; +} + +// Matches a JSON object that has a member named `name`. Meant for use inside +// JsonPayload(): +// +// Yields(JsonPayload(AllOf(HasJsonMember("x"), HasJsonMember("y")))) +// Yields(JsonPayload(Not(HasJsonMember("x")))) +// +// A value that isn't a JSON object does not match. The failure message is +// "Expect: JSON payload has member "x", but got: ...". +MATCHER_P(HasJsonMember, name, + absl::StrCat(negation ? "doesn't have" : "has", " member \"", name, + "\"")) { + if (!arg.isObject()) { + *result_listener << "which is not a JSON object"; + return false; + } + return arg.isMember(name); +} + +// Matches a JSON object that has a member named `name` whose value matches +// `member_matcher`, a matcher on a Json::Value. For example, this checks +// that an int64 field is serialized as a string: +// +// Yields(JsonPayload(HasJsonMemberThat("optionalInt64", IsJsonString("1")))) +// +// A value that isn't a JSON object does not match. +MATCHER_P2(HasJsonMemberThat, name, member_matcher, + absl::StrCat(negation ? "doesn't have" : "has", " member \"", name, + "\" that ", + ::testing::DescribeMatcher<const Json::Value&>( + member_matcher))) { + if (!arg.isObject()) { + *result_listener << "which is not a JSON object"; + return false; + } + return arg.isMember(name) && ::testing::ExplainMatchResult( + member_matcher, arg[name], result_listener); +} + +// Matches a JSON string equal to `expected`. A number or any other JSON kind +// doesn't match, whatever its textual form. +// +// Yields(JsonPayload(HasJsonMemberThat("optionalInt64", IsJsonString("1")))) +MATCHER_P(IsJsonString, expected, + absl::StrCat(negation ? "isn't" : "is", " the JSON string \"", + expected, "\"")) { + return arg.isString() && arg.asString() == expected; +} + +// Matches a JSON number that jsoncpp parsed as a signed integer +// (Json::intValue) equal to `expected`. A string "123" or a real 123.0 +// doesn't match. +// +// Yields(JsonPayload(HasJsonMemberThat("optionalInt32", IsJsonInt(123)))) +MATCHER_P(IsJsonInt, expected, + absl::StrCat(negation ? "isn't" : "is", " the JSON integer ", + expected)) { + return arg.type() == Json::intValue && arg.asInt() == expected; +} + +// Matches the JSON value `null`. +// +// Yields(JsonPayload(HasJsonMemberThat("oneofNullValue", IsJsonNull()))) +MATCHER(IsJsonNull, absl::StrCat(negation ? "isn't" : "is", " JSON null")) { + return arg.isNull(); +} + +// Matches a JSON object with no members, that is `{}`. This is the +// serialization of a message none of whose fields is emitted: +// +// Yields(JsonPayload(IsEmptyJsonObject())) +// +// Unlike Json::Value::empty(), neither `null` nor `[]` matches. +MATCHER(IsEmptyJsonObject, + absl::StrCat(negation ? "isn't" : "is", " an empty JSON object")) { + if (!arg.isObject()) { + *result_listener << "which is not a JSON object"; + return false; + } + return arg.empty(); +} + +} // namespace conformance +} // namespace protobuf +} // namespace google + +#endif // GOOGLE_PROTOBUF_CONFORMANCE_JSON_TEST_UTIL_H__
diff --git a/conformance/json_test_util_test.cc b/conformance/json_test_util_test.cc new file mode 100644 index 0000000..24ccb50 --- /dev/null +++ b/conformance/json_test_util_test.cc
@@ -0,0 +1,163 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2026 Google LLC. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +#include "conformance/json_test_util.h" + +#include <gmock/gmock.h> +#include <gtest/gtest.h> +#include "json/config.h" +#include "json/value.h" + +namespace google { +namespace protobuf { +namespace conformance { +namespace { + +using ::testing::DescribeMatcher; +using ::testing::ExplainMatchResult; +using ::testing::Not; +using ::testing::StringMatchResultListener; + +TEST(HasJsonMemberTest, Matches) { + Json::Value value; + value["foo"] = 1; + EXPECT_THAT(value, HasJsonMember("foo")); + EXPECT_THAT(value, Not(HasJsonMember("bar"))); + // Only objects have members. + EXPECT_THAT(Json::Value(Json::nullValue), Not(HasJsonMember("foo"))); + EXPECT_THAT(Json::Value(Json::arrayValue), Not(HasJsonMember("foo"))); +} + +TEST(HasJsonMemberTest, ExplainsNonObject) { + StringMatchResultListener listener; + EXPECT_FALSE(ExplainMatchResult(HasJsonMember("foo"), + Json::Value(Json::arrayValue), &listener)); + EXPECT_EQ(listener.str(), "which is not a JSON object"); + + // Objects are matched by membership alone, with no further explanation. + Json::Value value; + value["foo"] = 1; + StringMatchResultListener object_listener; + EXPECT_TRUE( + ExplainMatchResult(HasJsonMember("foo"), value, &object_listener)); + EXPECT_EQ(object_listener.str(), ""); +} + +TEST(HasJsonMemberTest, Description) { + EXPECT_EQ(DescribeMatcher<const Json::Value&>(HasJsonMember("foo")), + R"(has member "foo")"); + EXPECT_EQ(DescribeMatcher<const Json::Value&>(HasJsonMember("foo"), + /*negation=*/true), + R"(doesn't have member "foo")"); +} + +TEST(HasJsonMemberThatTest, Matches) { + Json::Value value; + value["str"] = "1"; + value["num"] = 1; + EXPECT_THAT(value, HasJsonMemberThat("str", IsJsonString("1"))); + EXPECT_THAT(value, Not(HasJsonMemberThat("str", IsJsonString("2")))); + EXPECT_THAT(value, Not(HasJsonMemberThat("num", IsJsonString("1")))); + EXPECT_THAT(value, Not(HasJsonMemberThat("missing", IsJsonString("1")))); + // Only objects have members. + EXPECT_THAT(Json::Value(Json::nullValue), + Not(HasJsonMemberThat("str", IsJsonString("1")))); + EXPECT_THAT(Json::Value(Json::arrayValue), + Not(HasJsonMemberThat("str", IsJsonString("1")))); +} + +TEST(HasJsonMemberThatTest, Description) { + EXPECT_EQ(DescribeMatcher<const Json::Value&>( + HasJsonMemberThat("foo", IsJsonString("1"))), + R"(has member "foo" that is the JSON string "1")"); + EXPECT_EQ(DescribeMatcher<const Json::Value&>( + HasJsonMemberThat("foo", IsJsonString("1")), + /*negation=*/true), + R"(doesn't have member "foo" that is the JSON string "1")"); +} + +TEST(IsJsonStringTest, Matches) { + EXPECT_THAT(Json::Value("1"), IsJsonString("1")); + EXPECT_THAT(Json::Value("2"), Not(IsJsonString("1"))); + EXPECT_THAT(Json::Value(1), Not(IsJsonString("1"))); + EXPECT_THAT(Json::Value(Json::nullValue), Not(IsJsonString("1"))); +} + +TEST(IsJsonStringTest, Description) { + EXPECT_EQ(DescribeMatcher<const Json::Value&>(IsJsonString("1")), + R"(is the JSON string "1")"); + EXPECT_EQ(DescribeMatcher<const Json::Value&>(IsJsonString("1"), + /*negation=*/true), + R"(isn't the JSON string "1")"); +} + +TEST(IsJsonIntTest, Matches) { + EXPECT_THAT(Json::Value(123), IsJsonInt(123)); + EXPECT_THAT(Json::Value(-1), IsJsonInt(-1)); + EXPECT_THAT(Json::Value(124), Not(IsJsonInt(123))); + // Only signed integers (Json::intValue) match, like the legacy validators. + EXPECT_THAT(Json::Value("123"), Not(IsJsonInt(123))); + EXPECT_THAT(Json::Value(123.0), Not(IsJsonInt(123))); + EXPECT_THAT(Json::Value(Json::UInt(123)), Not(IsJsonInt(123))); + EXPECT_THAT(Json::Value(Json::nullValue), Not(IsJsonInt(123))); +} + +TEST(IsJsonIntTest, Description) { + EXPECT_EQ(DescribeMatcher<const Json::Value&>(IsJsonInt(123)), + "is the JSON integer 123"); + EXPECT_EQ(DescribeMatcher<const Json::Value&>(IsJsonInt(123), + /*negation=*/true), + "isn't the JSON integer 123"); +} + +TEST(IsJsonNullTest, Matches) { + EXPECT_THAT(Json::Value(Json::nullValue), IsJsonNull()); + EXPECT_THAT(Json::Value(), IsJsonNull()); + EXPECT_THAT(Json::Value(0), Not(IsJsonNull())); + EXPECT_THAT(Json::Value(""), Not(IsJsonNull())); + EXPECT_THAT(Json::Value("null"), Not(IsJsonNull())); + EXPECT_THAT(Json::Value(Json::objectValue), Not(IsJsonNull())); +} + +TEST(IsJsonNullTest, Description) { + EXPECT_EQ(DescribeMatcher<const Json::Value&>(IsJsonNull()), "is JSON null"); + EXPECT_EQ( + DescribeMatcher<const Json::Value&>(IsJsonNull(), /*negation=*/true), + "isn't JSON null"); +} + +TEST(IsEmptyJsonObjectTest, Matches) { + EXPECT_THAT(Json::Value(Json::objectValue), IsEmptyJsonObject()); + Json::Value value; + value["foo"] = 1; + EXPECT_THAT(value, Not(IsEmptyJsonObject())); + // Only objects match, even though Json::Value::empty() is also true for + // null and for an empty array. + EXPECT_THAT(Json::Value(Json::nullValue), Not(IsEmptyJsonObject())); + EXPECT_THAT(Json::Value(Json::arrayValue), Not(IsEmptyJsonObject())); + EXPECT_THAT(Json::Value(""), Not(IsEmptyJsonObject())); +} + +TEST(IsEmptyJsonObjectTest, ExplainsNonObject) { + StringMatchResultListener listener; + EXPECT_FALSE(ExplainMatchResult(IsEmptyJsonObject(), + Json::Value(Json::arrayValue), &listener)); + EXPECT_EQ(listener.str(), "which is not a JSON object"); +} + +TEST(IsEmptyJsonObjectTest, Description) { + EXPECT_EQ(DescribeMatcher<const Json::Value&>(IsEmptyJsonObject()), + "is an empty JSON object"); + EXPECT_EQ(DescribeMatcher<const Json::Value&>(IsEmptyJsonObject(), + /*negation=*/true), + "isn't an empty JSON object"); +} + +} // namespace +} // namespace conformance +} // namespace protobuf +} // namespace google
diff --git a/conformance/json_timestamp_test.cc b/conformance/json_timestamp_test.cc new file mode 100644 index 0000000..23e1fd6 --- /dev/null +++ b/conformance/json_timestamp_test.cc
@@ -0,0 +1,690 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2026 Google LLC. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +// JSON conformance tests for google.protobuf.Timestamp: its RFC 3339 string +// form, its range, UTC offsets, the rejection of out-of-range, malformed or +// invalid date-time components in either direction, and the number of +// fractional digits a serializer emits. This replaces the Timestamp part of +// the legacy +// BinaryAndJsonConformanceSuiteImpl<M>::RunJsonTestsForWrapperTypes(), which +// ran for the proto3-style message types only; the test names and the requests +// sent to the testee are identical to the legacy ones, except that the +// binary-input tests below are BINARY_TEST requests (see below). +// +// Like the legacy RunValidJsonTest(), each valid input is sent twice: once to +// be serialized as binary ("<name>.ProtobufOutput") and once as JSON +// ("<name>.JsonOutput"). ExpectParseFailureForJson() asked for JSON output +// under a name without an output suffix; ExpectSerializeFailureForJson() sent +// a binary message (built here from its wire format) to be serialized as +// JSON ("ProtobufInput.<name>.JsonOutput"), categorized as JSON_TEST; here +// ParseBinary() categorizes them as BINARY_TEST, which no testee tells apart +// from JSON_TEST on a binary payload (b/563657722). And the +// legacy "validators" (RunValidJsonTestWithValidator()) looked at the +// serialized JSON text itself. + +#include <cstdint> + +#include <gmock/gmock.h> +#include <gtest/gtest.h> +#include "absl/strings/string_view.h" +#include "conformance/binary_test_util.h" +#include "conformance/binary_wireformat.h" +#include "conformance/json_test_util.h" +#include "conformance/matchers.h" +#include "conformance/message_type_fixtures.h" +#include "conformance/testee.h" + +namespace google { +namespace protobuf { +namespace conformance { +namespace { + +using ::testing::ValuesIn; + +using JsonTimestampTest = MessageTypeConformanceTest; + +// optional_timestamp is field 302 of the test message types; Timestamp's +// seconds is field 1 and nanos field 2. +constexpr uint32_t kOptionalTimestamp = 302; +constexpr uint32_t kSeconds = 1; +constexpr uint32_t kNanos = 2; + +TEST_P(JsonTimestampTest, TimestampMinValue) { + constexpr absl::string_view kInput = + R"json({"optionalTimestamp": "0001-01-01T00:00:00Z"})json"; + constexpr absl::string_view kExpected = + R"pb(optional_timestamp: { seconds: -62135596800 })pb"; + EXPECT_THAT(Testee("TimestampMinValue") + .ParseJson(message(), kInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT( + Testee("TimestampMinValue").ParseJson(message(), kInput).SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +TEST_P(JsonTimestampTest, TimestampMaxValue) { + constexpr absl::string_view kInput = + R"json({"optionalTimestamp": "9999-12-31T23:59:59.999999999Z"})json"; + constexpr absl::string_view kExpected = + R"pb(optional_timestamp: { seconds: 253402300799 nanos: 999999999 })pb"; + EXPECT_THAT(Testee("TimestampMaxValue") + .ParseJson(message(), kInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT( + Testee("TimestampMaxValue").ParseJson(message(), kInput).SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +TEST_P(JsonTimestampTest, TimestampRepeatedValue) { + constexpr absl::string_view kInput = R"json({ + "repeatedTimestamp": [ + "0001-01-01T00:00:00Z", + "9999-12-31T23:59:59.999999999Z" + ] + })json"; + constexpr absl::string_view kExpected = R"pb( + repeated_timestamp: { seconds: -62135596800 } + repeated_timestamp: { seconds: 253402300799 nanos: 999999999 } + )pb"; + EXPECT_THAT(Testee("TimestampRepeatedValue") + .ParseJson(message(), kInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT(Testee("TimestampRepeatedValue") + .ParseJson(message(), kInput) + .SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +TEST_P(JsonTimestampTest, TimestampEpochValue) { + constexpr absl::string_view kInput = + R"json({"optionalTimestamp": "1970-01-01T00:00:00.000Z"})json"; + constexpr absl::string_view kExpected = + R"pb(optional_timestamp: { seconds: 0 })pb"; + EXPECT_THAT(Testee("TimestampEpochValue") + .ParseJson(message(), kInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT(Testee("TimestampEpochValue") + .ParseJson(message(), kInput) + .SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +TEST_P(JsonTimestampTest, TimestampNanoAfterEpochValue) { + constexpr absl::string_view kInput = + R"json({"optionalTimestamp": "1970-01-01T00:00:00.000000001Z"})json"; + constexpr absl::string_view kExpected = + R"pb(optional_timestamp: { seconds: 0 nanos: 1 })pb"; + EXPECT_THAT(Testee("TimestampNanoAfterEpochlValue") + .ParseJson(message(), kInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT(Testee("TimestampNanoAfterEpochlValue") + .ParseJson(message(), kInput) + .SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +TEST_P(JsonTimestampTest, TimestampNanoBeforeEpochValue) { + constexpr absl::string_view kInput = + R"json({"optionalTimestamp": "1969-12-31T23:59:59.999999999Z"})json"; + constexpr absl::string_view kExpected = + R"pb(optional_timestamp: { seconds: -1 nanos: 999999999 })pb"; + EXPECT_THAT(Testee("TimestampNanoBeforeEpochValue") + .ParseJson(message(), kInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT(Testee("TimestampNanoBeforeEpochValue") + .ParseJson(message(), kInput) + .SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +TEST_P(JsonTimestampTest, TimestampLittleAfterEpochValue) { + constexpr absl::string_view kInput = + R"json({"optionalTimestamp": "1970-01-01T00:00:01.000000001Z"})json"; + constexpr absl::string_view kExpected = + R"pb(optional_timestamp: { seconds: 1 nanos: 1 })pb"; + EXPECT_THAT(Testee("TimestampLittleAfterEpochlValue") + .ParseJson(message(), kInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT(Testee("TimestampLittleAfterEpochlValue") + .ParseJson(message(), kInput) + .SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +TEST_P(JsonTimestampTest, TimestampLittleBeforeEpochValue) { + constexpr absl::string_view kInput = + R"json({"optionalTimestamp": "1969-12-31T23:59:58.999999999Z"})json"; + constexpr absl::string_view kExpected = + R"pb(optional_timestamp: { seconds: -2 nanos: 999999999 })pb"; + EXPECT_THAT(Testee("TimestampLittleBeforeEpochValue") + .ParseJson(message(), kInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT(Testee("TimestampLittleBeforeEpochValue") + .ParseJson(message(), kInput) + .SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +TEST_P(JsonTimestampTest, TimestampTenAndHalfSecondsAfterEpochValue) { + constexpr absl::string_view kInput = + R"json({"optionalTimestamp": "1970-01-01T00:00:10.500Z"})json"; + constexpr absl::string_view kExpected = + R"pb(optional_timestamp: { seconds: 10 nanos: 500000000 })pb"; + EXPECT_THAT(Testee("TimestampTenAndHalfSecondsAfterEpochValue") + .ParseJson(message(), kInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT(Testee("TimestampTenAndHalfSecondsAfterEpochValue") + .ParseJson(message(), kInput) + .SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +TEST_P(JsonTimestampTest, TimestampTenAndHalfSecondsBeforeEpochValue) { + constexpr absl::string_view kInput = + R"json({"optionalTimestamp": "1969-12-31T23:59:49.500Z"})json"; + constexpr absl::string_view kExpected = + R"pb(optional_timestamp: { seconds: -11 nanos: 500000000 })pb"; + EXPECT_THAT(Testee("TimestampTenAndHalfSecondsBeforeEpochValue") + .ParseJson(message(), kInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT(Testee("TimestampTenAndHalfSecondsBeforeEpochValue") + .ParseJson(message(), kInput) + .SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +TEST_P(JsonTimestampTest, TimestampLeap) { + constexpr absl::string_view kInput = + R"json({"optionalTimestamp": "1993-02-10T00:00:00.000Z"})json"; + constexpr absl::string_view kExpected = + R"pb(optional_timestamp: { seconds: 729302400 })pb"; + EXPECT_THAT( + Testee("TimestampLeap").ParseJson(message(), kInput).SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT( + Testee("TimestampLeap").ParseJson(message(), kInput).SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +TEST_P(JsonTimestampTest, TimestampWithPositiveOffset) { + constexpr absl::string_view kInput = + R"json({"optionalTimestamp": "1970-01-01T08:00:01+08:00"})json"; + constexpr absl::string_view kExpected = + R"pb(optional_timestamp: { seconds: 1 })pb"; + EXPECT_THAT(Testee("TimestampWithPositiveOffset") + .ParseJson(message(), kInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT(Testee("TimestampWithPositiveOffset") + .ParseJson(message(), kInput) + .SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +TEST_P(JsonTimestampTest, TimestampWithNegativeOffset) { + constexpr absl::string_view kInput = + R"json({"optionalTimestamp": "1969-12-31T16:00:01-08:00"})json"; + constexpr absl::string_view kExpected = + R"pb(optional_timestamp: { seconds: 1 })pb"; + EXPECT_THAT(Testee("TimestampWithNegativeOffset") + .ParseJson(message(), kInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT(Testee("TimestampWithNegativeOffset") + .ParseJson(message(), kInput) + .SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +TEST_P(JsonTimestampTest, TimestampNull) { + constexpr absl::string_view kInput = R"json({"optionalTimestamp": null})json"; + constexpr absl::string_view kExpected = ""; + EXPECT_THAT( + Testee("TimestampNull").ParseJson(message(), kInput).SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT( + Testee("TimestampNull").ParseJson(message(), kInput).SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +// Out-of-range and malformed JSON values are rejected. +TEST_P(JsonTimestampTest, TimestampJsonInputTooSmall) { + EXPECT_THAT( + Testee("TimestampJsonInputTooSmall") + .ParseJson(message(), + R"json({"optionalTimestamp": "0000-01-01T00:00:00Z"})json") + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(JsonTimestampTest, TimestampJsonInputTooLarge) { + EXPECT_THAT( + Testee("TimestampJsonInputTooLarge") + .ParseJson( + message(), + R"json({"optionalTimestamp": "10000-01-01T00:00:00Z"})json") + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(JsonTimestampTest, TimestampJsonInputMissingZ) { + EXPECT_THAT( + Testee("TimestampJsonInputMissingZ") + .ParseJson(message(), + R"json({"optionalTimestamp": "0001-01-01T00:00:00"})json") + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(JsonTimestampTest, TimestampJsonInputMissingT) { + EXPECT_THAT( + Testee("TimestampJsonInputMissingT") + .ParseJson(message(), + R"json({"optionalTimestamp": "0001-01-01 00:00:00Z"})json") + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(JsonTimestampTest, TimestampJsonInputLowercaseZ) { + EXPECT_THAT( + Testee("TimestampJsonInputLowercaseZ") + .ParseJson(message(), + R"json({"optionalTimestamp": "0001-01-01T00:00:00z"})json") + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(JsonTimestampTest, TimestampJsonInputLowercaseT) { + EXPECT_THAT( + Testee("TimestampJsonInputLowercaseT") + .ParseJson(message(), + R"json({"optionalTimestamp": "0001-01-01t00:00:00Z"})json") + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(JsonTimestampTest, TimestampWithMissingColonInOffset) { + EXPECT_THAT( + Testee("TimestampWithMissingColonInOffset") + .ParseJson( + message(), + R"json({"optionalTimestamp": "1970-01-01T08:00:01+0800"})json") + .ParseOnly(), + Yields(IsParseError())); +} + +// Out-of-range or inconsistent messages can't be serialized. The inputs +// are the legacy text-format messages on the wire (zero fields omitted). +TEST_P(JsonTimestampTest, TimestampProtoInputTooSmall) { + EXPECT_THAT( + Testee("TimestampProtoInputTooSmall") + .ParseBinary(message(), + LengthPrefixedField(kOptionalTimestamp, + VarintField(kSeconds, -62135596801))) + .SerializeJson(), + Yields(IsSerializeError())); +} + +TEST_P(JsonTimestampTest, TimestampProtoInputTooLarge) { + EXPECT_THAT( + Testee("TimestampProtoInputTooLarge") + .ParseBinary(message(), + LengthPrefixedField(kOptionalTimestamp, + VarintField(kSeconds, 253402300800))) + .SerializeJson(), + Yields(IsSerializeError())); +} + +TEST_P(JsonTimestampTest, TimestampProtoNegativeNanos) { + EXPECT_THAT(Testee("TimestampProtoNegativeNanos") + .ParseBinary(message(), LengthPrefixedField( + kOptionalTimestamp, + Wire(VarintField(kSeconds, 5000), + VarintField(kNanos, -1)))) + .SerializeJson(), + Yields(IsSerializeError())); +} + +TEST_P(JsonTimestampTest, TimestampProtoNanoTooLarge) { + EXPECT_THAT( + Testee("TimestampProtoNanoTooLarge") + .ParseBinary(message(), LengthPrefixedField( + kOptionalTimestamp, + Wire(VarintField(kSeconds, 5000), + VarintField(kNanos, 1000000000)))) + .SerializeJson(), + Yields(IsSerializeError())); +} + +// Serializers normalize to UTC and emit 0, 3, 6 or 9 fractional digits, +// whichever is the fewest that represent the value exactly. +TEST_P(JsonTimestampTest, TimestampZeroNormalized) { + EXPECT_THAT( + Testee(kP1, "TimestampZeroNormalized") + .ParseJson( + message(), + R"json({"optionalTimestamp": "1969-12-31T16:00:00-08:00"})json") + .SerializeJson(), + Yields(JsonPayload(HasJsonMemberThat( + "optionalTimestamp", IsJsonString("1970-01-01T00:00:00Z"))))); +} + +TEST_P(JsonTimestampTest, TimestampHasZeroFractionalDigit) { + constexpr absl::string_view kInput = + R"json({"optionalTimestamp": "1970-01-01T00:00:00.000000000Z"})json"; + EXPECT_THAT(Testee(kP1, "TimestampHasZeroFractionalDigit") + .ParseJson(message(), kInput) + .SerializeJson(), + Yields(JsonPayload(HasJsonMemberThat( + "optionalTimestamp", IsJsonString("1970-01-01T00:00:00Z"))))); +} + +TEST_P(JsonTimestampTest, TimestampHas3FractionalDigits) { + constexpr absl::string_view kInput = + R"json({"optionalTimestamp": "1970-01-01T00:00:00.010000000Z"})json"; + EXPECT_THAT( + Testee(kP1, "TimestampHas3FractionalDigits") + .ParseJson(message(), kInput) + .SerializeJson(), + Yields(JsonPayload(HasJsonMemberThat( + "optionalTimestamp", IsJsonString("1970-01-01T00:00:00.010Z"))))); +} + +TEST_P(JsonTimestampTest, TimestampHas6FractionalDigits) { + constexpr absl::string_view kInput = + R"json({"optionalTimestamp": "1970-01-01T00:00:00.000010000Z"})json"; + EXPECT_THAT( + Testee(kP1, "TimestampHas6FractionalDigits") + .ParseJson(message(), kInput) + .SerializeJson(), + Yields(JsonPayload(HasJsonMemberThat( + "optionalTimestamp", IsJsonString("1970-01-01T00:00:00.000010Z"))))); +} + +TEST_P(JsonTimestampTest, TimestampHas9FractionalDigits) { + constexpr absl::string_view kInput = + R"json({"optionalTimestamp": "1970-01-01T00:00:00.000000010Z"})json"; + EXPECT_THAT(Testee(kP1, "TimestampHas9FractionalDigits") + .ParseJson(message(), kInput) + .SerializeJson(), + Yields(JsonPayload(HasJsonMemberThat( + "optionalTimestamp", + IsJsonString("1970-01-01T00:00:00.000000010Z"))))); +} + +// Out of bounds / invalid components should JSON parse fail. +TEST_P(JsonTimestampTest, TimestampJsonInputMonthTooLarge) { + EXPECT_THAT( + Testee("TimestampJsonInputMonthTooLarge") + .ParseJson(message(), + R"json({"optionalTimestamp": "1970-13-01T00:00:00Z"})json") + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(JsonTimestampTest, TimestampJsonInputMonthZero) { + EXPECT_THAT( + Testee("TimestampJsonInputMonthZero") + .ParseJson(message(), + R"json({"optionalTimestamp": "1970-00-01T00:00:00Z"})json") + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(JsonTimestampTest, TimestampJsonInputDayTooLarge) { + EXPECT_THAT( + Testee("TimestampJsonInputDayTooLarge") + .ParseJson(message(), + R"json({"optionalTimestamp": "1970-01-32T00:00:00Z"})json") + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(JsonTimestampTest, TimestampJsonInputDayZero) { + EXPECT_THAT( + Testee("TimestampJsonInputDayZero") + .ParseJson(message(), + R"json({"optionalTimestamp": "1970-01-00T00:00:00Z"})json") + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(JsonTimestampTest, TimestampJsonInputHourTooLarge) { + EXPECT_THAT( + Testee("TimestampJsonInputHourTooLarge") + .ParseJson(message(), + R"json({"optionalTimestamp": "1970-01-01T24:00:00Z"})json") + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(JsonTimestampTest, TimestampJsonInputHourTooLarge25) { + EXPECT_THAT( + Testee("TimestampJsonInputHourTooLarge25") + .ParseJson(message(), + R"json({"optionalTimestamp": "1970-01-01T25:00:00Z"})json") + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(JsonTimestampTest, TimestampJsonInputMinuteTooLarge) { + EXPECT_THAT( + Testee("TimestampJsonInputMinuteTooLarge") + .ParseJson(message(), + R"json({"optionalTimestamp": "1970-01-01T00:60:00Z"})json") + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(JsonTimestampTest, TimestampJsonInputSecondTooLarge) { + EXPECT_THAT( + Testee("TimestampJsonInputSecondTooLarge") + .ParseJson(message(), + R"json({"optionalTimestamp": "1970-01-01T00:00:60Z"})json") + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(JsonTimestampTest, TimestampJsonInputInvalidOffsetHour) { + EXPECT_THAT( + Testee("TimestampJsonInputInvalidOffsetHour") + .ParseJson( + message(), + R"json({"optionalTimestamp": "1970-01-01T00:00:00+24:00"})json") + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(JsonTimestampTest, TimestampJsonInputInvalidOffsetMinute) { + EXPECT_THAT( + Testee("TimestampJsonInputInvalidOffsetMinute") + .ParseJson( + message(), + R"json({"optionalTimestamp": "1970-01-01T00:00:00+00:60"})json") + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(JsonTimestampTest, TimestampJsonInputOffsetBoundaryUnderflow) { + EXPECT_THAT( + Testee("TimestampJsonInputOffsetBoundaryUnderflow") + .ParseJson( + message(), + R"json({"optionalTimestamp": "0001-01-01T00:00:00+00:01"})json") + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(JsonTimestampTest, TimestampJsonInputOffsetBoundaryOverflow) { + EXPECT_THAT( + Testee("TimestampJsonInputOffsetBoundaryOverflow") + .ParseJson( + message(), + R"json({"optionalTimestamp": "9999-12-31T23:59:59-00:01"})json") + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(JsonTimestampTest, TimestampJsonInputInvalidNanos) { + constexpr absl::string_view kInput = + R"json({"optionalTimestamp": "1970-01-01T00:00:00.1234567890Z"})json"; + EXPECT_THAT(Testee("TimestampJsonInputInvalidNanos") + .ParseJson(message(), kInput) + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(JsonTimestampTest, TimestampJsonInputInvalidCharsInNanos) { + EXPECT_THAT( + Testee("TimestampJsonInputInvalidCharsInNanos") + .ParseJson( + message(), + R"json({"optionalTimestamp": "1970-01-01T00:00:00.123aZ"})json") + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(JsonTimestampTest, TimestampJsonInputYearTooShort) { + EXPECT_THAT( + Testee("TimestampJsonInputYearTooShort") + .ParseJson(message(), + R"json({"optionalTimestamp": "999-01-01T00:00:00Z"})json") + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(JsonTimestampTest, TimestampJsonInputYearTooLong) { + EXPECT_THAT( + Testee("TimestampJsonInputYearTooLong") + .ParseJson( + message(), + R"json({"optionalTimestamp": "00001-01-01T00:00:00Z"})json") + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(JsonTimestampTest, TimestampJsonInputMonthTooShort) { + EXPECT_THAT( + Testee("TimestampJsonInputMonthTooShort") + .ParseJson(message(), + R"json({"optionalTimestamp": "1970-1-01T00:00:00Z"})json") + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(JsonTimestampTest, TimestampJsonInputDayTooShort) { + EXPECT_THAT( + Testee("TimestampJsonInputDayTooShort") + .ParseJson(message(), + R"json({"optionalTimestamp": "1970-01-1T00:00:00Z"})json") + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(JsonTimestampTest, TimestampJsonInputNonLeapFeb29) { + EXPECT_THAT( + Testee("TimestampJsonInputNonLeapFeb29") + .ParseJson(message(), + R"json({"optionalTimestamp": "2001-02-29T00:00:00Z"})json") + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(JsonTimestampTest, TimestampJsonInputLeapFeb29) { + constexpr absl::string_view kInput = + R"json({"optionalTimestamp": "2000-02-29T00:00:00Z"})json"; + constexpr absl::string_view kExpected = + R"pb(optional_timestamp: { seconds: 951782400 })pb"; + EXPECT_THAT(Testee("TimestampJsonInputLeapFeb29") + .ParseJson(message(), kInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT(Testee("TimestampJsonInputLeapFeb29") + .ParseJson(message(), kInput) + .SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +// Honoring time zones correctly. +TEST_P(JsonTimestampTest, TimestampWithOffsetShiftsDay) { + constexpr absl::string_view kInput = + R"json({"optionalTimestamp": "1970-01-02T01:00:00+02:00"})json"; + constexpr absl::string_view kExpected = + R"pb(optional_timestamp: { seconds: 82800 })pb"; + EXPECT_THAT(Testee("TimestampWithOffsetShiftsDay") + .ParseJson(message(), kInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT(Testee("TimestampWithOffsetShiftsDay") + .ParseJson(message(), kInput) + .SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +TEST_P(JsonTimestampTest, TimestampWithOffsetBoundaryInBoundsMin) { + constexpr absl::string_view kInput = + R"json({"optionalTimestamp": "0001-01-01T00:00:00-00:01"})json"; + constexpr absl::string_view kExpected = + R"pb(optional_timestamp: { seconds: -62135596740 })pb"; + EXPECT_THAT(Testee("TimestampWithOffsetBoundaryInBoundsMin") + .ParseJson(message(), kInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT(Testee("TimestampWithOffsetBoundaryInBoundsMin") + .ParseJson(message(), kInput) + .SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +TEST_P(JsonTimestampTest, TimestampWithOffsetBoundaryInBoundsMax) { + constexpr absl::string_view kInput = + R"json({"optionalTimestamp": "9999-12-31T23:59:59+00:01"})json"; + constexpr absl::string_view kExpected = + R"pb(optional_timestamp: { seconds: 253402300739 })pb"; + EXPECT_THAT(Testee("TimestampWithOffsetBoundaryInBoundsMax") + .ParseJson(message(), kInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT(Testee("TimestampWithOffsetBoundaryInBoundsMax") + .ParseJson(message(), kInput) + .SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +TEST_P(JsonTimestampTest, TimestampWithComplexOffset) { + constexpr absl::string_view kInput = + R"json({"optionalTimestamp": "1970-01-01T00:00:00-11:30"})json"; + constexpr absl::string_view kExpected = + R"pb(optional_timestamp: { seconds: 41400 })pb"; + EXPECT_THAT(Testee("TimestampWithComplexOffset") + .ParseJson(message(), kInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT(Testee("TimestampWithComplexOffset") + .ParseJson(message(), kInput) + .SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +INSTANTIATE_TEST_SUITE_P(All, JsonTimestampTest, + ValuesIn(Proto3TestMessageTypes()), + MessageTypeParamName); + +} // namespace +} // namespace conformance +} // namespace protobuf +} // namespace google
diff --git a/conformance/json_unknown_enum_test.cc b/conformance/json_unknown_enum_test.cc new file mode 100644 index 0000000..b02b0c4 --- /dev/null +++ b/conformance/json_unknown_enum_test.cc
@@ -0,0 +1,146 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2026 Google LLC. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +// JSON conformance tests for unknown enum values given as string labels, in +// a singular field, a repeated field and a map value: a parser must reject +// them unless it is asked to ignore unknown fields, in which case it drops +// the unknown value (and, in a list or map mixing known and unknown labels, +// keeps the known ones). This replaces the legacy +// BinaryAndJsonConformanceSuiteImpl<M>:: +// RunJsonTestsForUnknownEnumStringValues(), +// which ran for every test message type; the test names and the requests sent +// to the testee are identical to the legacy ones. +// +// Like the legacy RunValidJsonIgnoreUnknownTest(), the ignore-unknown tests +// only have the binary-output leg ("<name>.ProtobufOutput", in the +// JSON_IGNORE_UNKNOWN_PARSING_TEST category), and like +// ExpectParseFailureForJson() the rejections ask for JSON output under a name +// without an output suffix. The inputs are the legacy raw string literals +// verbatim, including their snake_case member names and whitespace. + +#include <gmock/gmock.h> +#include <gtest/gtest.h> +#include "conformance/binary_test_util.h" +#include "conformance/matchers.h" +#include "conformance/message_type_fixtures.h" +#include "conformance/testee.h" + +namespace google { +namespace protobuf { +namespace conformance { +namespace { + +using ::testing::ValuesIn; + +class JsonUnknownEnumTest : public MessageTypeConformanceTest { + private: + TestPriority DefaultPriority() const override { return kP1; } +}; + +// Unknown enum string values are a parse failure when not ignoring unknown +// fields. +TEST_P(JsonUnknownEnumTest, RejectUnknownEnumStringValueInOptionalField) { + EXPECT_THAT(Testee("RejectUnknownEnumStringValueInOptionalField") + .ParseJson(message(), R"json({ + "optional_nested_enum": "UNKNOWN_ENUM_VALUE" + })json") + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(JsonUnknownEnumTest, RejectUnknownEnumStringValueInRepeatedField) { + EXPECT_THAT(Testee("RejectUnknownEnumStringValueInRepeatedField") + .ParseJson(message(), R"json({ + "repeated_nested_enum": ["UNKNOWN_ENUM_VALUE"] + })json") + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(JsonUnknownEnumTest, RejectUnknownEnumStringValueInMapValue) { + EXPECT_THAT(Testee("RejectUnknownEnumStringValueInMapValue") + .ParseJson(message(), R"json({ + "map_string_nested_enum": {"key": "UNKNOWN_ENUM_VALUE"} + })json") + .ParseOnly(), + Yields(IsParseError())); +} + +// ... and are ignored when ignoring unknown fields. +TEST_P(JsonUnknownEnumTest, IgnoreUnknownEnumStringValueInOptionalField) { + EXPECT_THAT(Testee("IgnoreUnknownEnumStringValueInOptionalField") + .ParseJson(message(), R"json({ + "optional_nested_enum": "UNKNOWN_ENUM_VALUE" + })json", + {/*ignore_unknown_fields=*/true}) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto("")))); +} + +TEST_P(JsonUnknownEnumTest, IgnoreUnknownEnumStringValueInRepeatedField) { + EXPECT_THAT(Testee("IgnoreUnknownEnumStringValueInRepeatedField") + .ParseJson(message(), R"json({ + "repeated_nested_enum": ["UNKNOWN_ENUM_VALUE"] + })json", + {/*ignore_unknown_fields=*/true}) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto("")))); +} + +TEST_P(JsonUnknownEnumTest, IgnoreUnknownEnumStringValueInMapValue) { + EXPECT_THAT(Testee("IgnoreUnknownEnumStringValueInMapValue") + .ParseJson(message(), R"json({ + "map_string_nested_enum": {"key": "UNKNOWN_ENUM_VALUE"} + })json", + {/*ignore_unknown_fields=*/true}) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto("")))); +} + +// Like IgnoreUnknownEnumStringValueInRepeatedField, but highlights the +// potentially unexpected behavior in an array with mixed known and unknown +// enum string values. +TEST_P(JsonUnknownEnumTest, IgnoreUnknownEnumStringValueInRepeatedPart) { + EXPECT_THAT(Testee("IgnoreUnknownEnumStringValueInRepeatedPart") + .ParseJson(message(), R"json({ + "repeated_nested_enum": [ + "FOO", + "UNKNOWN_ENUM_VALUE", + "FOO" + ]})json", + {/*ignore_unknown_fields=*/true}) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(R"pb( + repeated_nested_enum: FOO + repeated_nested_enum: FOO + )pb")))); +} + +// Like IgnoreUnknownEnumStringValueInMapValue, with a mixture of known and +// unknown enum string values in the map. +TEST_P(JsonUnknownEnumTest, IgnoreUnknownEnumStringValueInMapPart) { + EXPECT_THAT(Testee("IgnoreUnknownEnumStringValueInMapPart") + .ParseJson(message(), R"json({ + "map_string_nested_enum": { + "key1": "FOO", + "key2": "UNKNOWN_ENUM_VALUE" + }})json", + {/*ignore_unknown_fields=*/true}) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(R"pb(map_string_nested_enum: { + key: "key1" + value: FOO + })pb")))); +} + +INSTANTIATE_TEST_SUITE_P(All, JsonUnknownEnumTest, + ValuesIn(AllTestMessageTypes()), MessageTypeParamName); + +} // namespace +} // namespace conformance +} // namespace protobuf +} // namespace google
diff --git a/conformance/json_value_test.cc b/conformance/json_value_test.cc new file mode 100644 index 0000000..95f49e9 --- /dev/null +++ b/conformance/json_value_test.cc
@@ -0,0 +1,336 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2026 Google LLC. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +// JSON conformance tests for google.protobuf.Value, ListValue and NullValue +// fields: every JSON kind a Value accepts, repeated Values and ListValues, how +// a NullValue is serialized inside and outside a oneof, the non-finite numbers +// a Value cannot express, and lists and Structs nested 25 deep (must be +// accepted) and 200 deep (must be rejected). This replaces the legacy +// BinaryAndJsonConformanceSuiteImpl<M>::RunJsonTestsForValue(), which ran for +// the proto3-style message types only; the test names and the requests sent to +// the testee are identical to the legacy ones, except that the binary-input +// tests below are BINARY_TEST requests (see below). +// +// Like the legacy RunValidJsonTest(), each valid input is sent twice: once to +// be serialized as binary ("<name>.ProtobufOutput") and once as JSON +// ("<name>.JsonOutput"). ExpectParseFailureForJson() asked for JSON output +// under a name without an output suffix; RunValidJsonTestWithValidator() +// asked for JSON output and inspected the JSON text. +// ExpectSerializeFailureForJson() sent the serialized text-format message as +// binary input, categorized as a JSON test, under +// "ProtobufInput.<name>.JsonOutput"; the same bytes are built here with the +// wire-format builders, and ParseBinary() categorizes them as BINARY_TEST, +// which no testee tells apart from JSON_TEST on a binary payload +// (b/563657722). + +#include <cstdint> +#include <limits> +#include <string> + +#include <gmock/gmock.h> +#include <gtest/gtest.h> +#include "absl/strings/str_cat.h" +#include "absl/strings/string_view.h" +#include "conformance/binary_test_util.h" +#include "conformance/binary_wireformat.h" +#include "conformance/json_test_util.h" +#include "conformance/matchers.h" +#include "conformance/message_type_fixtures.h" +#include "conformance/testee.h" + +namespace google { +namespace protobuf { +namespace conformance { +namespace { + +using ::testing::ValuesIn; + +using JsonValueTest = MessageTypeConformanceTest; + +// Field numbers of the binary inputs of the serialize-failure tests: the +// test message's optional_value and google.protobuf.Value.number_value. +constexpr uint32_t kOptionalValue = 306; +constexpr uint32_t kValueNumberValue = 2; + +TEST_P(JsonValueTest, ValueAcceptInteger) { + constexpr absl::string_view kInput = R"json({"optionalValue": 1})json"; + constexpr absl::string_view kExpected = + R"pb(optional_value: { number_value: 1 })pb"; + EXPECT_THAT(Testee("ValueAcceptInteger") + .ParseJson(message(), kInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT( + Testee("ValueAcceptInteger").ParseJson(message(), kInput).SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +TEST_P(JsonValueTest, ValueAcceptFloat) { + constexpr absl::string_view kInput = R"json({"optionalValue": 1.5})json"; + constexpr absl::string_view kExpected = + R"pb(optional_value: { number_value: 1.5 })pb"; + EXPECT_THAT( + Testee("ValueAcceptFloat").ParseJson(message(), kInput).SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT( + Testee("ValueAcceptFloat").ParseJson(message(), kInput).SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +TEST_P(JsonValueTest, ValueAcceptBool) { + constexpr absl::string_view kInput = R"json({"optionalValue": false})json"; + constexpr absl::string_view kExpected = + R"pb(optional_value: { bool_value: false })pb"; + EXPECT_THAT( + Testee("ValueAcceptBool").ParseJson(message(), kInput).SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT( + Testee("ValueAcceptBool").ParseJson(message(), kInput).SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +TEST_P(JsonValueTest, ValueAcceptNull) { + constexpr absl::string_view kInput = R"json({"optionalValue": null})json"; + constexpr absl::string_view kExpected = + R"pb(optional_value: { null_value: NULL_VALUE })pb"; + EXPECT_THAT( + Testee("ValueAcceptNull").ParseJson(message(), kInput).SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT( + Testee("ValueAcceptNull").ParseJson(message(), kInput).SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +TEST_P(JsonValueTest, ValueAcceptString) { + constexpr absl::string_view kInput = R"json({"optionalValue": "hello"})json"; + constexpr absl::string_view kExpected = + R"pb(optional_value: { string_value: "hello" })pb"; + EXPECT_THAT(Testee("ValueAcceptString") + .ParseJson(message(), kInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT( + Testee("ValueAcceptString").ParseJson(message(), kInput).SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +TEST_P(JsonValueTest, ValueAcceptList) { + constexpr absl::string_view kInput = + R"json({"optionalValue": [0, "hello"]})json"; + constexpr absl::string_view kExpected = R"pb( + optional_value: { + list_value: { + values: { number_value: 0 } + values: { string_value: "hello" } + } + } + )pb"; + EXPECT_THAT( + Testee("ValueAcceptList").ParseJson(message(), kInput).SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT( + Testee("ValueAcceptList").ParseJson(message(), kInput).SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +TEST_P(JsonValueTest, ValueAcceptObject) { + constexpr absl::string_view kInput = + R"json({"optionalValue": {"value": 1}})json"; + constexpr absl::string_view kExpected = R"pb( + optional_value: { + struct_value: { + fields: { + key: "value" + value: { number_value: 1 } + } + } + } + )pb"; + EXPECT_THAT(Testee("ValueAcceptObject") + .ParseJson(message(), kInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT( + Testee("ValueAcceptObject").ParseJson(message(), kInput).SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +TEST_P(JsonValueTest, RepeatedValue) { + constexpr absl::string_view kInput = R"json({ + "repeatedValue": [["a"]] + })json"; + constexpr absl::string_view kExpected = R"pb( + repeated_value: + [ { + list_value: { + values: + [ { string_value: "a" }] + } + }] + )pb"; + EXPECT_THAT( + Testee("RepeatedValue").ParseJson(message(), kInput).SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT( + Testee("RepeatedValue").ParseJson(message(), kInput).SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +TEST_P(JsonValueTest, RepeatedListValue) { + constexpr absl::string_view kInput = R"json({ + "repeatedListValue": [["a"]] + })json"; + constexpr absl::string_view kExpected = R"pb( + repeated_list_value: + [ { + values: + [ { string_value: "a" }] + }] + )pb"; + EXPECT_THAT(Testee("RepeatedListValue") + .ParseJson(message(), kInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT( + Testee("RepeatedListValue").ParseJson(message(), kInput).SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +// A NullValue set in a oneof is emitted as `null`, whether it was given as +// the enum name or as `null`. +TEST_P(JsonValueTest, NullValueInOtherOneofOldFormat) { + EXPECT_THAT( + Testee(kP1, "NullValueInOtherOneofOldFormat") + .ParseJson(message(), R"json({"oneofNullValue": "NULL_VALUE"})json") + .SerializeJson(), + Yields(JsonPayload(HasJsonMemberThat("oneofNullValue", IsJsonNull())))); +} + +TEST_P(JsonValueTest, NullValueInOtherOneofNewFormat) { + EXPECT_THAT( + Testee(kP1, "NullValueInOtherOneofNewFormat") + .ParseJson(message(), R"json({"oneofNullValue": null})json") + .SerializeJson(), + Yields(JsonPayload(HasJsonMemberThat("oneofNullValue", IsJsonNull())))); +} + +// A NullValue field outside a oneof has no presence, so it is omitted. +TEST_P(JsonValueTest, NullValueInNormalMessage) { + EXPECT_THAT( + Testee(kP1, "NullValueInNormalMessage") + .ParseJson(message(), R"json({"optionalNullValue": null})json") + .SerializeJson(), + Yields(JsonPayload(IsEmptyJsonObject()))); +} + +// The binary form of the legacy input `optional_value: { number_value: nan}`. +TEST_P(JsonValueTest, ValueRejectNanNumberValue) { + EXPECT_THAT( + Testee(kP1, "ValueRejectNanNumberValue") + .ParseBinary( + message(), + LengthPrefixedField( + kOptionalValue, + DoubleField(kValueNumberValue, + std::numeric_limits<double>::quiet_NaN()))) + .SerializeJson(), + Yields(IsSerializeError())); +} + +// The binary form of the legacy input `optional_value: { number_value: inf}`. +TEST_P(JsonValueTest, ValueRejectInfNumberValue) { + EXPECT_THAT(Testee(kP1, "ValueRejectInfNumberValue") + .ParseBinary( + message(), + LengthPrefixedField( + kOptionalValue, + DoubleField(kValueNumberValue, + std::numeric_limits<double>::infinity()))) + .SerializeJson(), + Yields(IsSerializeError())); +} + +TEST_P(JsonValueTest, ListValueDeepNesting25) { + const std::string input = + absl::StrCat(R"({"optionalValue": )", std::string(25, '['), "1", + std::string(25, ']'), "}"); + const std::string expected = absl::StrCat("optional_value: {\n", + Repeat(" list_value: {\n" + " values: {\n", + 25), + " number_value: 1\n", + Repeat(" }\n" + " }\n", + 25), + "}\n"); + EXPECT_THAT(Testee(kP1, "ListValueDeepNesting25") + .ParseJson(message(), input) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(expected)))); + EXPECT_THAT(Testee(kP1, "ListValueDeepNesting25") + .ParseJson(message(), input) + .SerializeJson(), + Yields(WhenParsed(EqualsTextProto(expected)))); +} + +TEST_P(JsonValueTest, ListValueDeepNesting200) { + const std::string input = + absl::StrCat(R"({"optionalValue": )", std::string(200, '['), "1", + std::string(200, ']'), "}"); + EXPECT_THAT(Testee(kP1, "ListValueDeepNesting200") + .ParseJson(message(), input) + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(JsonValueTest, ValueDeepNesting25) { + const std::string input = + absl::StrCat(R"({"optionalValue": {)", Repeat(R"("n": {)", 25), + R"("value": 1)", std::string(25, '}'), "}}"); + const std::string expected = + absl::StrCat("optional_value: {\n", " struct_value: {\n", + Repeat(" fields: {\n" + " key: \"n\"\n" + " value: {\n" + " struct_value: {\n", + 25), + " fields: {\n" + " key: \"value\"\n" + " value: { number_value: 1 }\n" + " }\n", + Repeat(" }\n" + " }\n" + " }\n", + 25), + " }\n", "}\n"); + EXPECT_THAT(Testee(kP1, "ValueDeepNesting25") + .ParseJson(message(), input) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(expected)))); + EXPECT_THAT(Testee(kP1, "ValueDeepNesting25") + .ParseJson(message(), input) + .SerializeJson(), + Yields(WhenParsed(EqualsTextProto(expected)))); +} + +TEST_P(JsonValueTest, ValueDeepNesting200) { + const std::string input = + absl::StrCat(R"({"optionalValue": {)", Repeat(R"("n": {)", 200), + R"("value": 1)", std::string(200, '}'), "}}"); + EXPECT_THAT(Testee(kP1, "ValueDeepNesting200") + .ParseJson(message(), input) + .ParseOnly(), + Yields(IsParseError())); +} + +INSTANTIATE_TEST_SUITE_P(All, JsonValueTest, ValuesIn(Proto3TestMessageTypes()), + MessageTypeParamName); + +} // namespace +} // namespace conformance +} // namespace protobuf +} // namespace google
diff --git a/conformance/json_wrapper_test.cc b/conformance/json_wrapper_test.cc new file mode 100644 index 0000000..f86c729 --- /dev/null +++ b/conformance/json_wrapper_test.cc
@@ -0,0 +1,398 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2026 Google LLC. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +// JSON conformance tests for the well-known wrapper types +// (google.protobuf.BoolValue, Int32Value, ...): a wrapper is represented by +// its value alone, is present even when that value is the default, and is +// unset by `null`. This replaces the wrapper type part of the legacy +// BinaryAndJsonConformanceSuiteImpl<M>::RunJsonTestsForWrapperTypes(), which +// ran for the proto3-style message types only; the test names and the +// requests sent to the testee are identical to the legacy ones. +// +// Like the legacy RunValidJsonTest(), each input is sent twice: once to be +// serialized as binary ("<name>.ProtobufOutput") and once as JSON +// ("<name>.JsonOutput"). + +#include <gmock/gmock.h> +#include <gtest/gtest.h> +#include "absl/strings/string_view.h" +#include "conformance/binary_test_util.h" +#include "conformance/matchers.h" +#include "conformance/message_type_fixtures.h" +#include "conformance/testee.h" + +namespace google { +namespace protobuf { +namespace conformance { +namespace { + +using ::testing::ValuesIn; + +using JsonWrapperTest = MessageTypeConformanceTest; + +// Singular wrappers holding their default value are present (a wrapper +// message is set even when its value is the default). +TEST_P(JsonWrapperTest, OptionalBoolWrapper) { + constexpr absl::string_view kInput = + R"json({"optionalBoolWrapper": false})json"; + constexpr absl::string_view kExpected = + R"pb(optional_bool_wrapper: { value: false })pb"; + EXPECT_THAT(Testee("OptionalBoolWrapper") + .ParseJson(message(), kInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT(Testee("OptionalBoolWrapper") + .ParseJson(message(), kInput) + .SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +TEST_P(JsonWrapperTest, OptionalInt32Wrapper) { + constexpr absl::string_view kInput = R"json({"optionalInt32Wrapper": 0})json"; + constexpr absl::string_view kExpected = + R"pb(optional_int32_wrapper: { value: 0 })pb"; + EXPECT_THAT(Testee("OptionalInt32Wrapper") + .ParseJson(message(), kInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT(Testee("OptionalInt32Wrapper") + .ParseJson(message(), kInput) + .SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +TEST_P(JsonWrapperTest, OptionalUint32Wrapper) { + constexpr absl::string_view kInput = + R"json({"optionalUint32Wrapper": 0})json"; + constexpr absl::string_view kExpected = + R"pb(optional_uint32_wrapper: { value: 0 })pb"; + EXPECT_THAT(Testee("OptionalUint32Wrapper") + .ParseJson(message(), kInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT(Testee("OptionalUint32Wrapper") + .ParseJson(message(), kInput) + .SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +TEST_P(JsonWrapperTest, OptionalInt64Wrapper) { + constexpr absl::string_view kInput = R"json({"optionalInt64Wrapper": 0})json"; + constexpr absl::string_view kExpected = + R"pb(optional_int64_wrapper: { value: 0 })pb"; + EXPECT_THAT(Testee("OptionalInt64Wrapper") + .ParseJson(message(), kInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT(Testee("OptionalInt64Wrapper") + .ParseJson(message(), kInput) + .SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +TEST_P(JsonWrapperTest, OptionalUint64Wrapper) { + constexpr absl::string_view kInput = + R"json({"optionalUint64Wrapper": 0})json"; + constexpr absl::string_view kExpected = + R"pb(optional_uint64_wrapper: { value: 0 })pb"; + EXPECT_THAT(Testee("OptionalUint64Wrapper") + .ParseJson(message(), kInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT(Testee("OptionalUint64Wrapper") + .ParseJson(message(), kInput) + .SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +TEST_P(JsonWrapperTest, OptionalFloatWrapper) { + constexpr absl::string_view kInput = R"json({"optionalFloatWrapper": 0})json"; + constexpr absl::string_view kExpected = + R"pb(optional_float_wrapper: { value: 0 })pb"; + EXPECT_THAT(Testee("OptionalFloatWrapper") + .ParseJson(message(), kInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT(Testee("OptionalFloatWrapper") + .ParseJson(message(), kInput) + .SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +TEST_P(JsonWrapperTest, OptionalDoubleWrapper) { + constexpr absl::string_view kInput = + R"json({"optionalDoubleWrapper": 0})json"; + constexpr absl::string_view kExpected = + R"pb(optional_double_wrapper: { value: 0 })pb"; + EXPECT_THAT(Testee("OptionalDoubleWrapper") + .ParseJson(message(), kInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT(Testee("OptionalDoubleWrapper") + .ParseJson(message(), kInput) + .SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +TEST_P(JsonWrapperTest, OptionalStringWrapper) { + constexpr absl::string_view kInput = + R"json({"optionalStringWrapper": ""})json"; + constexpr absl::string_view kExpected = + R"pb(optional_string_wrapper: { value: "" })pb"; + EXPECT_THAT(Testee("OptionalStringWrapper") + .ParseJson(message(), kInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT(Testee("OptionalStringWrapper") + .ParseJson(message(), kInput) + .SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +TEST_P(JsonWrapperTest, OptionalBytesWrapper) { + constexpr absl::string_view kInput = + R"json({"optionalBytesWrapper": ""})json"; + constexpr absl::string_view kExpected = + R"pb(optional_bytes_wrapper: { value: "" })pb"; + EXPECT_THAT(Testee("OptionalBytesWrapper") + .ParseJson(message(), kInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT(Testee("OptionalBytesWrapper") + .ParseJson(message(), kInput) + .SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +TEST_P(JsonWrapperTest, OptionalWrapperTypesWithNonDefaultValue) { + constexpr absl::string_view kInput = R"json({ + "optionalBoolWrapper": true, + "optionalInt32Wrapper": 1, + "optionalUint32Wrapper": 1, + "optionalInt64Wrapper": "1", + "optionalUint64Wrapper": "1", + "optionalFloatWrapper": 1, + "optionalDoubleWrapper": 1, + "optionalStringWrapper": "1", + "optionalBytesWrapper": "AQI=" + })json"; + constexpr absl::string_view kExpected = R"pb( + optional_bool_wrapper: { value: true } + optional_int32_wrapper: { value: 1 } + optional_uint32_wrapper: { value: 1 } + optional_int64_wrapper: { value: 1 } + optional_uint64_wrapper: { value: 1 } + optional_float_wrapper: { value: 1 } + optional_double_wrapper: { value: 1 } + optional_string_wrapper: { value: "1" } + optional_bytes_wrapper: { value: "\x01\x02" } + )pb"; + EXPECT_THAT(Testee("OptionalWrapperTypesWithNonDefaultValue") + .ParseJson(message(), kInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT(Testee("OptionalWrapperTypesWithNonDefaultValue") + .ParseJson(message(), kInput) + .SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +// Repeated wrappers. +TEST_P(JsonWrapperTest, RepeatedBoolWrapper) { + constexpr absl::string_view kInput = + R"json({"repeatedBoolWrapper": [true, false]})json"; + constexpr absl::string_view kExpected = R"pb( + repeated_bool_wrapper: { value: true } + repeated_bool_wrapper: { value: false } + )pb"; + EXPECT_THAT(Testee("RepeatedBoolWrapper") + .ParseJson(message(), kInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT(Testee("RepeatedBoolWrapper") + .ParseJson(message(), kInput) + .SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +TEST_P(JsonWrapperTest, RepeatedInt32Wrapper) { + constexpr absl::string_view kInput = + R"json({"repeatedInt32Wrapper": [0, 1]})json"; + constexpr absl::string_view kExpected = R"pb( + repeated_int32_wrapper: { value: 0 } + repeated_int32_wrapper: { value: 1 } + )pb"; + EXPECT_THAT(Testee("RepeatedInt32Wrapper") + .ParseJson(message(), kInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT(Testee("RepeatedInt32Wrapper") + .ParseJson(message(), kInput) + .SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +TEST_P(JsonWrapperTest, RepeatedUint32Wrapper) { + constexpr absl::string_view kInput = + R"json({"repeatedUint32Wrapper": [0, 1]})json"; + constexpr absl::string_view kExpected = R"pb( + repeated_uint32_wrapper: { value: 0 } + repeated_uint32_wrapper: { value: 1 } + )pb"; + EXPECT_THAT(Testee("RepeatedUint32Wrapper") + .ParseJson(message(), kInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT(Testee("RepeatedUint32Wrapper") + .ParseJson(message(), kInput) + .SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +TEST_P(JsonWrapperTest, RepeatedInt64Wrapper) { + constexpr absl::string_view kInput = + R"json({"repeatedInt64Wrapper": [0, 1]})json"; + constexpr absl::string_view kExpected = R"pb( + repeated_int64_wrapper: { value: 0 } + repeated_int64_wrapper: { value: 1 } + )pb"; + EXPECT_THAT(Testee("RepeatedInt64Wrapper") + .ParseJson(message(), kInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT(Testee("RepeatedInt64Wrapper") + .ParseJson(message(), kInput) + .SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +TEST_P(JsonWrapperTest, RepeatedUint64Wrapper) { + constexpr absl::string_view kInput = + R"json({"repeatedUint64Wrapper": [0, 1]})json"; + constexpr absl::string_view kExpected = R"pb( + repeated_uint64_wrapper: { value: 0 } + repeated_uint64_wrapper: { value: 1 } + )pb"; + EXPECT_THAT(Testee("RepeatedUint64Wrapper") + .ParseJson(message(), kInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT(Testee("RepeatedUint64Wrapper") + .ParseJson(message(), kInput) + .SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +TEST_P(JsonWrapperTest, RepeatedFloatWrapper) { + constexpr absl::string_view kInput = + R"json({"repeatedFloatWrapper": [0, 1]})json"; + constexpr absl::string_view kExpected = R"pb( + repeated_float_wrapper: { value: 0 } + repeated_float_wrapper: { value: 1 } + )pb"; + EXPECT_THAT(Testee("RepeatedFloatWrapper") + .ParseJson(message(), kInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT(Testee("RepeatedFloatWrapper") + .ParseJson(message(), kInput) + .SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +TEST_P(JsonWrapperTest, RepeatedDoubleWrapper) { + constexpr absl::string_view kInput = + R"json({"repeatedDoubleWrapper": [0, 1]})json"; + constexpr absl::string_view kExpected = R"pb( + repeated_double_wrapper: { value: 0 } + repeated_double_wrapper: { value: 1 } + )pb"; + EXPECT_THAT(Testee("RepeatedDoubleWrapper") + .ParseJson(message(), kInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT(Testee("RepeatedDoubleWrapper") + .ParseJson(message(), kInput) + .SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +TEST_P(JsonWrapperTest, RepeatedStringWrapper) { + constexpr absl::string_view kInput = + R"json({"repeatedStringWrapper": ["", "AQI="]})json"; + constexpr absl::string_view kExpected = R"pb( + repeated_string_wrapper: { value: "" } + repeated_string_wrapper: { value: "AQI=" } + )pb"; + EXPECT_THAT(Testee("RepeatedStringWrapper") + .ParseJson(message(), kInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT(Testee("RepeatedStringWrapper") + .ParseJson(message(), kInput) + .SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +TEST_P(JsonWrapperTest, RepeatedBytesWrapper) { + constexpr absl::string_view kInput = + R"json({"repeatedBytesWrapper": ["", "AQI="]})json"; + constexpr absl::string_view kExpected = R"pb( + repeated_bytes_wrapper: { value: "" } + repeated_bytes_wrapper: { value: "\x01\x02" } + )pb"; + EXPECT_THAT(Testee("RepeatedBytesWrapper") + .ParseJson(message(), kInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT(Testee("RepeatedBytesWrapper") + .ParseJson(message(), kInput) + .SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +// `null` unsets a wrapper (unlike a Value, for which it is a null value). +TEST_P(JsonWrapperTest, WrapperTypesWithNullValue) { + constexpr absl::string_view kInput = R"json({ + "optionalBoolWrapper": null, + "optionalInt32Wrapper": null, + "optionalUint32Wrapper": null, + "optionalInt64Wrapper": null, + "optionalUint64Wrapper": null, + "optionalFloatWrapper": null, + "optionalDoubleWrapper": null, + "optionalStringWrapper": null, + "optionalBytesWrapper": null, + "repeatedBoolWrapper": null, + "repeatedInt32Wrapper": null, + "repeatedUint32Wrapper": null, + "repeatedInt64Wrapper": null, + "repeatedUint64Wrapper": null, + "repeatedFloatWrapper": null, + "repeatedDoubleWrapper": null, + "repeatedStringWrapper": null, + "repeatedBytesWrapper": null + })json"; + constexpr absl::string_view kExpected = ""; + EXPECT_THAT(Testee("WrapperTypesWithNullValue") + .ParseJson(message(), kInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); + EXPECT_THAT(Testee("WrapperTypesWithNullValue") + .ParseJson(message(), kInput) + .SerializeJson(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +INSTANTIATE_TEST_SUITE_P(All, JsonWrapperTest, + ValuesIn(Proto3TestMessageTypes()), + MessageTypeParamName); + +} // namespace +} // namespace conformance +} // namespace protobuf +} // namespace google
diff --git a/conformance/matchers.cc b/conformance/matchers.cc index 7ea51a4..cdc516d 100644 --- a/conformance/matchers.cc +++ b/conformance/matchers.cc
@@ -16,27 +16,32 @@ #include <gmock/gmock.h> #include <gtest/gtest.h> #include "absl/base/nullability.h" -#include "absl/base/thread_annotations.h" #include "absl/log/absl_check.h" #include "absl/log/absl_log.h" #include "absl/memory/memory.h" #include "absl/status/status.h" +#include "absl/status/statusor.h" #include "absl/strings/ascii.h" #include "absl/strings/escaping.h" #include "absl/strings/str_cat.h" #include "absl/strings/str_format.h" #include "absl/strings/string_view.h" -#include "absl/synchronization/mutex.h" #include "absl/types/optional.h" +#include "json/reader.h" +#include "json/value.h" #include "conformance/binary_wireformat.h" #include "conformance/conformance.pb.h" +#include "conformance/failure_list.h" +#include "conformance/filtering_test_runner.h" #include "conformance/global_test_environment.h" -#include "conformance/test_manager.h" +#include "conformance/result_record.h" #include "conformance/testee.h" #include "google/protobuf/descriptor.h" +#include "google/protobuf/json/json.h" #include "google/protobuf/message.h" #include "google/protobuf/port.h" #include "google/protobuf/text_format.h" +#include "google/protobuf/unknown_field_set.h" #include "google/protobuf/util/field_comparator.h" #include "google/protobuf/util/message_differencer.h" @@ -47,9 +52,15 @@ using ::conformance::ConformanceResponse; using ::conformance::WireFormat; -using ::google::protobuf::conformance::internal::GetGlobalTestManager; -using ::google::protobuf::conformance::internal::TestManager; +using ::google::protobuf::conformance::internal::CurrentGtestResult; +using ::google::protobuf::conformance::internal::FailureList; +using ::google::protobuf::conformance::internal::GetGlobalFailureList; +using ::google::protobuf::conformance::internal::ParseResultRecordMessage; +using ::google::protobuf::conformance::internal::RecordedResult; +using ::google::protobuf::conformance::internal::ResultRecord; +using ::google::protobuf::conformance::internal::ResultRecordMessage; using ::google::protobuf::conformance::internal::TestResult; +using ::google::protobuf::conformance::internal::YieldedResult; // Implements EqualsTextProto() and EqualsBinaryProto(): matches a message // equivalent to the one obtained by decoding `expected` (in `format`) as the @@ -127,8 +138,9 @@ failure_message_(failure_message), runtime_error_failure_message_(runtime_error_failure_message) {} - bool MatchAndExplain(const TestResult& result, + bool MatchAndExplain(const YieldedResult& yielded, testing::MatchResultListener* listener) const { + const TestResult& result = yielded.result(); ConformanceResponse::ResultCase actual = result.response().result_case(); if (actual == expected_result_) { return true; @@ -299,20 +311,32 @@ } return message; } - case ::conformance::JSON: + case ::conformance::JSON: { + // Decoded with the default json::ParseOptions, which is effectively what + // the legacy runner did. + // TODO: Set allow_legacy_nonconformant_behavior = false + // once the failures that surfaces have been triaged across languages. + absl::Status status = json::JsonStringToMessage( + result.response().json_payload(), message.get()); + if (!status.ok()) { + ABSL_LOG(INFO) << "JSON output of " << result.name() + << " is unparseable: " << status; + failure_message = UnparseableMessage(result.format()); + return nullptr; + } + return message; + } case ::conformance::JSPB: case ::conformance::UNSPECIFIED: default: - // TODO: b/410122158 - Support JSON once the JSON suite is migrated. - failure_message = absl::StrCat( - "WhenParsed is not supported for ", WireFormat_Name(result.format()), - " output; use RawPayload() to match the raw JSON text until JSON " - "matching is migrated to gtest (b/410122158)."); - return nullptr; + // PayloadMatcher only calls ParsePayload() once the response carries + // the payload the test asked for, and tests can only ask for PROTOBUF, + // TEXT_FORMAT or JSON output. + google::protobuf::internal::Unreachable(); } } -// Formats binary data the same way the legacy runner does in failure messages. +// Formats binary data the same way the legacy runner did in failure messages. std::string ToOctString(absl::string_view binary_string) { std::string oct_string; for (char ch : binary_string) { @@ -331,7 +355,7 @@ virtual ~PayloadMatcher() = default; - bool MatchAndExplain(const TestResult& result, + bool MatchAndExplain(const YieldedResult& yielded, testing::MatchResultListener* listener) const; void DescribeTo(std::ostream* os) const { DescribeInnerTo(os, /*negation=*/false); @@ -357,7 +381,9 @@ }; bool PayloadMatcher::MatchAndExplain( - const TestResult& result, testing::MatchResultListener* listener) const { + const YieldedResult& yielded, + testing::MatchResultListener* listener) const { + const TestResult& result = yielded.result(); const ConformanceResponse& response = result.response(); switch (response.result_case()) { case ConformanceResponse::RESULT_NOT_SET: @@ -461,8 +487,8 @@ bool RawPayloadMatcher::MatchPayload( const TestResult& result, testing::MatchResultListener* listener) const { - // Like the legacy runner's `require_same_wire_format`, binary output must at - // least be parseable as the test's message type. + // Like the legacy runner's byte-exact mode, binary output must at least be + // parseable as the test's message type. if (result.format() == ::conformance::PROTOBUF && !NewMessage(result.type()) ->ParseFromString(result.response().protobuf_payload())) { @@ -481,51 +507,160 @@ return false; } -// Implements Yields(). This is the single matcher that talks to the global -// TestManager; see matchers.h for the policy it applies. +// Implements JsonPayload(). +class JsonPayloadMatcher : public PayloadMatcher { + public: + explicit JsonPayloadMatcher(testing::Matcher<const Json::Value&> matcher) + : matcher_(std::move(matcher)) {} + + private: + bool MatchPayload(const TestResult& result, + testing::MatchResultListener* listener) const override; + void DescribeInnerTo(std::ostream* os, bool negation) const override; + + testing::Matcher<const Json::Value&> matcher_; +}; + +bool JsonPayloadMatcher::MatchPayload( + const TestResult& result, testing::MatchResultListener* listener) const { + // PayloadMatcher only checked that the payload is in the requested format; + // a test that asked for anything but JSON can't be matched this way. This + // is an authoring bug, so it is deliberately checked only after the base + // class has reported the "real" conformance failures (errors, skips, wrong + // payload kind) that also apply to correctly written tests. + if (result.format() != ::conformance::JSON) { + *listener << "JsonPayload() needs JSON output, but the test asked for " + << WireFormat_Name(result.format()) << " output."; + return false; + } + + // Parsed with jsoncpp's defaults, like the legacy runner's validators. + const std::string& payload = result.response().json_payload(); + Json::CharReaderBuilder builder; + const std::unique_ptr<Json::CharReader> reader(builder.newCharReader()); + Json::Value value; + std::string error; + if (!reader->parse(payload.data(), payload.data() + payload.size(), &value, + &error)) { + *listener << "JSON payload cannot be parsed as valid JSON: " << error; + return false; + } + + testing::StringMatchResultListener inner_listener; + if (matcher_.MatchAndExplain(value, &inner_listener)) { + *listener << inner_listener.str(); + return true; + } + if (inner_listener.str().empty()) { + // JSON is readable, so it is quoted and escaped rather than shown in octal. + *listener << "Expect: JSON payload " + << testing::DescribeMatcher<const Json::Value&>(matcher_) + << ", but got: \"" << absl::CEscape(payload) << "\""; + } else { + *listener << inner_listener.str(); + } + return false; +} + +void JsonPayloadMatcher::DescribeInnerTo(std::ostream* os, + bool negation) const { + *os << "JSON payload " + << testing::DescribeMatcher<const Json::Value&>(matcher_, negation); +} + +// What an earlier check of the conformance test `name` recorded in the gtest +// result Yields() records to, if anything (see ResultRecordMessage()), as +// ResultRecord::ToString(). +absl::optional<std::string> RecordedValue(absl::string_view name) { + const testing::TestResult& gtest_result = CurrentGtestResult(); + for (int i = 0; i < gtest_result.total_part_count(); ++i) { + const testing::TestPartResult& part = gtest_result.GetTestPartResult(i); + if (part.type() != testing::TestPartResult::kSuccess) continue; + absl::StatusOr<RecordedResult> recorded = + ParseResultRecordMessage(part.message()); + if (recorded.ok() && recorded->test_name == name) { + return recorded->record.ToString(); + } + } + return absl::nullopt; +} + +// The status to record for a result that is a failure: kCrash for a testee +// that raised an error or timed out, kFail otherwise (including a response +// with no result at all). +ResultRecord::Status FailureStatus(const ConformanceResponse& response) { + switch (response.result_case()) { + case ConformanceResponse::kRuntimeError: + case ConformanceResponse::kTimeoutError: + return ResultRecord::Status::kCrash; + default: + return ResultRecord::Status::kFail; + } +} + +// Logs what became of a result the first time it is checked, as the legacy +// runner did: a testee skip at INFO, and a failure that doesn't fail the test +// at INFO if it is listed (an expected failure) or as a WARNING if it isn't (a +// tolerated one). +void LogOutcome(const TestResult& result, const ResultRecord& record, + const absl::Status& verdict) { + absl::string_view message = + absl::StripTrailingAsciiWhitespace(record.message); + switch (record.status) { + case ResultRecord::Status::kSkip: + ABSL_LOG(INFO) << "Skipping test " << result.name() << ": " << message; + break; + case ResultRecord::Status::kFail: + case ResultRecord::Status::kCrash: + if (!verdict.ok()) break; + if (GetGlobalFailureList().MatchingEntry(result.name()).has_value()) { + ABSL_LOG(INFO) << "Ignoring expected failure for test " << result.name() + << ": " << message; + } else { + ABSL_LOG(WARNING) << "WARNING, test=" << result.name() << ": " + << message; + } + break; + default: + break; + } +} + +// Implements Yields(). This is the single matcher that reads the global +// failure list; see matchers.h for the policy it applies. // -// The TestManager must only hear about each test once. It remembers the -// names it was told (TestManager::WasReported()), and a result it already -// knows is not evaluated again. gtest evaluates a failing matcher a second -// time, through the same matcher object, to explain the failure. The matcher -// keeps its last failed verdict for that rerun. Anything else that checks a -// reported result is a test bug and fails. -// -// Matchers are nominally immutable. The cache only affects how a failure -// that has already been decided is explained, never a verdict, and copies of -// a Matcher share one impl, so the matcher behaves the same through every -// copy. +// The matcher changes nothing in the process but the gtest result it runs +// in: what it learns about a result goes into a gtest success whose message +// carries a ResultRecord (see ResultRecordMessage()), which the test +// environment tallies as gtest reports it. The success also tells the +// matcher whether the test was checked before. gtest evaluates a failing +// matcher a second time to explain the failure; that evaluation finds its own +// record and only repeats the verdict. A check that reaches a different +// outcome than the recorded one is a test bug and fails. class YieldsMatcherImpl : public testing::MatcherInterface<const TestResult&> { public: - explicit YieldsMatcherImpl(testing::Matcher<const TestResult&> inner) + explicit YieldsMatcherImpl(testing::Matcher<const YieldedResult&> inner) : inner_(std::move(inner)) {} bool MatchAndExplain(const TestResult& result, testing::MatchResultListener* listener) const override { - { - absl::MutexLock lock(&mutex_); - if (last_failure_.has_value() && last_failure_->result == &result && - last_failure_->name == result.name()) { - *listener << last_failure_->explanation; - return false; - } - } - if (GetGlobalTestManager().WasReported(result.name())) { + ResultRecord record; + absl::Status verdict = Evaluate(result, record); + const std::string value = record.ToString(); + absl::optional<std::string> recorded = RecordedValue(result.name()); + if (!recorded.has_value()) { + SUCCEED() << ResultRecordMessage(result.name(), record); + LogOutcome(result, record, verdict); + } else if (*recorded != value) { *listener << "TestResult for " << result.name() - << " was already checked; each result may be checked exactly " - "once"; + << " was already checked, with a different outcome: the first " + "check recorded \"" + << *recorded << "\", this one would record \"" << value + << "\"; each result may be checked once"; return false; } - absl::Status status = Evaluate(result); - *listener << status.message(); - absl::MutexLock lock(&mutex_); - if (status.ok()) { - last_failure_.reset(); - } else { - last_failure_ = LastFailure{&result, std::string(result.name()), - std::string(status.message())}; - } - return status.ok(); + *listener << verdict.message(); + return verdict.ok(); } void DescribeTo(std::ostream* os) const override { @@ -541,60 +676,135 @@ } private: - // Reports `result` to the global TestManager and returns the gtest verdict: - // OK if the test passes, otherwise an error whose message explains why not. - absl::Status Evaluate(const TestResult& result) const { - TestManager& manager = GetGlobalTestManager(); + // Decides `result`: fills in `record` with what to record about it and + // returns the gtest verdict, OK if the test passes, otherwise an error whose + // message explains why not. Reads the global failure list and changes + // nothing. + absl::Status Evaluate(const TestResult& result, ResultRecord& record) const { + const FailureList& failure_list = GetGlobalFailureList(); const std::string name(result.name()); const ConformanceResponse& response = result.response(); + record.priority = result.priority(); - // A skip is decided here; the inner matcher never sees it. The manager - // marks a listed entry as seen and returns an error for it, so this is the - // only place that reports a listed test the testee skipped. + // A test the runner filtered out with --test never ran: it is neither a + // testee skip nor, if listed, a "listed but skipped" failure. The record + // only says that the test exists, so that its failure list entry isn't + // reported as matching no test. + if (response.result_case() == ConformanceResponse::kSkipped && + response.skipped() == FilteringTestRunner::kNotSelectedSkipReason) { + record.status = ResultRecord::Status::kNotSelected; + return absl::OkStatus(); + } + + // A skip is decided here; the inner matcher never sees it. A listed test + // the testee skipped fails (see FailureList::VerdictOnSkip()). if (response.result_case() == ConformanceResponse::kSkipped) { - absl::Status status = manager.ReportSkip(name, response.skipped()); - ABSL_LOG(INFO) << "Skipping test " << name << ": " << response.skipped(); - return status; + record.status = ResultRecord::Status::kSkip; + record.message = response.skipped(); + return failure_list.VerdictOnSkip(name, response.skipped()); } testing::StringMatchResultListener inner_listener; - bool matched = inner_.MatchAndExplain(result, &inner_listener); + bool matched = + inner_.MatchAndExplain(YieldedResult(result), &inner_listener); std::string message = inner_listener.str(); if (matched) { - return manager.ReportSuccess(name); + record.status = ResultRecord::Status::kPass; + return failure_list.VerdictOnSuccess(name); } if (message.empty()) { message = absl::StrCat( "which doesn't match (", - testing::DescribeMatcher<const TestResult&>(inner_), ")"); + testing::DescribeMatcher<const YieldedResult&>(inner_), ")"); } + record.status = FailureStatus(response); + record.message = message; - // The manager decides whether the failure is expected (listed), tolerated - // (above the enforcement level) or unexpected, and logs the first two. - absl::Status status = - manager.ReportFailure(name, result.priority(), message); - if (status.ok()) { - return status; + // The failure list decides whether the failure is expected (listed), + // tolerated (above the enforcement level) or unexpected. + absl::Status verdict = + failure_list.VerdictOnFailure(name, result.priority(), message); + if (verdict.ok()) { + return verdict; } - return absl::Status(status.code(), - absl::StrCat(message, "\n", status.message())); + return absl::Status(verdict.code(), + absl::StrCat(message, "\n", verdict.message())); } - // The failure this matcher explained last, identified by the result's - // address and name, so that gtest's rerun gets the same explanation. A - // destroyed result's address may be reused, but not its name: Testee::Run() - // rejects duplicate test names. - struct LastFailure { - const TestResult* result; - std::string name; - std::string explanation; - }; + testing::Matcher<const YieldedResult&> inner_; +}; - testing::Matcher<const TestResult&> inner_; - mutable absl::Mutex mutex_; - mutable absl::optional<LastFailure> last_failure_ ABSL_GUARDED_BY(mutex_); +// Formats `fields` for descriptions, e.g. `[666: "abc", 666: 123]`. +std::string DescribeUnknownFields(const UnknownFieldSet& fields) { + std::string out = "["; + for (int i = 0; i < fields.field_count(); ++i) { + const UnknownField& field = fields.field(i); + if (i > 0) absl::StrAppend(&out, ", "); + absl::StrAppend(&out, field.number(), ": "); + switch (field.type()) { + case UnknownField::TYPE_VARINT: + absl::StrAppend(&out, field.varint()); + break; + case UnknownField::TYPE_FIXED32: + absl::StrAppend(&out, "fixed32(", field.fixed32(), ")"); + break; + case UnknownField::TYPE_FIXED64: + absl::StrAppend(&out, "fixed64(", field.fixed64(), ")"); + break; + case UnknownField::TYPE_LENGTH_DELIMITED: + absl::StrAppend(&out, "\"", absl::CEscape(field.length_delimited()), + "\""); + break; + case UnknownField::TYPE_GROUP: + absl::StrAppend(&out, DescribeUnknownFields(field.group())); + break; + } + } + return absl::StrCat(out, "]"); +} + +// Implements HasUnknownFieldsInOrder(): matches a message whose unknown field +// set holds exactly the fields of `expected`, in the same order. +// +// UnknownFieldSet serializes its fields in order with a canonical encoding for +// each, so two sets hold equal fields in the same order exactly when their +// serializations are equal. Comparing those also sidesteps copying the +// expected set, which UnknownFieldSet doesn't support. +class UnknownFieldsInOrderMatcher { + public: + using is_gtest_matcher = void; + + explicit UnknownFieldsInOrderMatcher(const UnknownFieldSet& expected) + : description_(DescribeUnknownFields(expected)) { + ABSL_CHECK(expected.SerializeToString(&expected_)) + << "Failed to serialize the expected unknown fields " << description_; + } + + bool MatchAndExplain(const Message& actual, + testing::MatchResultListener* listener) const { + std::string serialized; + if (actual.GetReflection()->GetUnknownFields(actual).SerializeToString( + &serialized) && + serialized == expected_) { + return true; + } + *listener << "Unknown field mismatch"; + return false; + } + void DescribeTo(std::ostream* os) const { + *os << "has exactly the unknown fields " << description_ + << ", in that order"; + } + void DescribeNegationTo(std::ostream* os) const { + *os << "doesn't have exactly the unknown fields " << description_ + << " in that order"; + } + + private: + std::string description_; // `expected`, formatted for descriptions + std::string expected_; // `expected`'s serialization }; } // namespace @@ -617,20 +827,36 @@ } } -testing::Matcher<const TestResult&> MakeWhenParsedMatcher( +testing::Matcher<const YieldedResult&> MakeWhenParsedMatcher( testing::Matcher<const Message&> m, const Descriptor* absl_nullable type_override) { return WhenParsedMatcher(std::move(m), type_override); } +testing::Matcher<const YieldedResult&> MakeJsonPayloadMatcher( + testing::Matcher<const Json::Value&> m) { + return JsonPayloadMatcher(std::move(m)); +} + testing::Matcher<const TestResult&> MakeYieldsMatcher( - testing::Matcher<const TestResult&> inner) { + testing::Matcher<const YieldedResult&> inner) { return testing::MakeMatcher(new YieldsMatcherImpl(std::move(inner))); } +const testing::TestResult& CurrentGtestResult() { + const testing::UnitTest& unit_test = *testing::UnitTest::GetInstance(); + if (const testing::TestInfo* test_info = unit_test.current_test_info()) { + return *test_info->result(); + } + if (const testing::TestSuite* suite = unit_test.current_test_suite()) { + return suite->ad_hoc_test_result(); + } + return unit_test.ad_hoc_test_result(); +} + } // namespace internal -testing::Matcher<const internal::TestResult&> RawPayload(Wire bytes) { +testing::Matcher<const internal::YieldedResult&> RawPayload(Wire bytes) { return RawPayloadMatcher(std::move(bytes).str()); } @@ -644,14 +870,19 @@ std::move(bytes).str()); } -testing::Matcher<const internal::TestResult&> IsParseError() { +testing::Matcher<const Message&> HasUnknownFieldsInOrder( + const UnknownFieldSet& expected) { + return UnknownFieldsInOrderMatcher(expected); +} + +testing::Matcher<const internal::YieldedResult&> IsParseError() { return FailureMatcher( "parse error", ConformanceResponse::kParseError, "Should have failed to parse, but didn't.", "Should have failed to parse, but raised an error instead."); } -testing::Matcher<const internal::TestResult&> IsSerializeError() { +testing::Matcher<const internal::YieldedResult&> IsSerializeError() { return FailureMatcher( "serialize error", ConformanceResponse::kSerializeError, "Should have failed to serialize, but didn't.",
diff --git a/conformance/matchers.h b/conformance/matchers.h index 080d4bf..52218b1 100644 --- a/conformance/matchers.h +++ b/conformance/matchers.h
@@ -16,28 +16,38 @@ // .SerializeBinary(), // Yields(WhenParsed(EqualsTextProto(R"pb(optional_int32: 1)pb")))); // -// The leaf matchers (WhenParsed, RawPayload, IsParseError, ...) only look at -// the TestResult, so they compose with other gMock matchers: -// Yields(AnyOf(IsParseError(), WhenParsed(m))) works as expected. -// EqualsTextProto() and EqualsBinaryProto() match a `const Message&` and are -// meant to be used inside WhenParsed(). +// The leaf matchers (WhenParsed, RawPayload, JsonPayload, IsParseError, ...) +// only look at the result, so they compose with other gMock matchers: +// Yields(AnyOf(IsParseError(), WhenParsed(m))) works as expected. They match +// the YieldedResult that Yields() hands them rather than the TestResult, so a +// leaf matcher applied to a TestResult without Yields() does not compile. +// EqualsTextProto(), EqualsBinaryProto() and HasUnknownFieldsInOrder() match +// a `const Message&` and are meant to be used inside WhenParsed(). // -// Yields() is the one matcher that talks to the global TestManager. It -// records the outcome of the test and turns the failure list into the gtest -// verdict: +// Yields() is the one matcher that reads the global failure list (see +// GetGlobalFailureList() in global_test_environment.h). It turns the failure +// list into the gtest verdict: // // - A failure that is in the failure list passes. A listed test that // succeeds fails. // - A failure of a test above the enforcement level is tolerated unless // the test is listed. kP0 failures always count (see TestPriority). -// - A test the testee skipped passes unless it is listed. The TestManager -// counts it in ListedSkips(). +// - A test the testee skipped passes unless it is listed. The test +// environment counts it as a listed skip. +// - A test the runner filtered out with --test is not counted at all. +// +// Yields() changes nothing in the process. It records the outcome of the +// test as a gtest success in the running test, whose message carries an +// internal::ResultRecord (see ResultRecordMessage() in result_record.h). The +// test environment picks the records up as gtest reports them, tallies them in +// its ResultLedger and reports each test's outcome as a property of the gtest +// test (see test_environment.h). // // The failure messages of the leaf matchers end up in failure lists, so each // matcher documents its message and keeps it stable. Messages that come from // an inner matcher use gMock's wording, which can change between versions. // Failure list entries for those should only use a prefix of the message (see -// TestManager::ReportFailure()). +// ResultLedger::VerdictOnFailure()). // // This file also defines PrintTo() for TestResult, which gtest uses to print a // result when a matcher on it fails. @@ -52,16 +62,38 @@ #include <gtest/gtest.h> #include "absl/base/nullability.h" #include "absl/strings/string_view.h" +#include "json/value.h" #include "conformance/binary_wireformat.h" #include "conformance/testee.h" #include "google/protobuf/descriptor.h" #include "google/protobuf/message.h" +#include "google/protobuf/unknown_field_set.h" namespace google { namespace protobuf { namespace conformance { namespace internal { +// The view of a TestResult that Yields() hands to its inner matcher. The +// leaf matchers match this rather than the TestResult itself, which is what +// makes a forgotten Yields() a compile error: +// +// EXPECT_THAT(Testee().ParseBinary(type, input).SerializeBinary(), +// IsParseError()); // error: TestResult isn't a YieldedResult +// +// It is a view: the result must outlive it. Only Yields() and the tests of +// the leaf matchers construct one. +class YieldedResult { + public: + explicit YieldedResult(const TestResult& result) : result_(&result) {} + YieldedResult(TestResult&&) = delete; + + const TestResult& result() const { return *result_; } + + private: + const TestResult* absl_nonnull result_; +}; + // Prints a TestResult in gtest failure output. The output has the test's // priority and name and a short form of the response. Long payloads are // truncated. Binary payloads are also decoded as the test's message type. @@ -73,19 +105,31 @@ // Implements WhenParsed() and WhenParsedAs() below. The payload is // decoded as `type_override` if it is non-null, and as the test's message type // otherwise. -testing::Matcher<const TestResult&> MakeWhenParsedMatcher( +testing::Matcher<const YieldedResult&> MakeWhenParsedMatcher( testing::Matcher<const Message&> m, const Descriptor* absl_nullable type_override = nullptr); +// Likewise for JsonPayload(m). +testing::Matcher<const YieldedResult&> MakeJsonPayloadMatcher( + testing::Matcher<const Json::Value&> m); + // Implements Yields(): see the function below for the semantics. testing::Matcher<const TestResult&> MakeYieldsMatcher( - testing::Matcher<const TestResult&> inner); + testing::Matcher<const YieldedResult&> inner); + +// The gtest result that a test part result reported now goes to: the running +// test's, else (in SetUpTestSuite() or TearDownTestSuite()) the current test +// suite's ad hoc result, else the whole run's. Yields() records what it +// learns about each conformance test as a success there, and finds there what +// an earlier check of the same test recorded. +const testing::TestResult& CurrentGtestResult(); } // namespace internal // Matches a result whose payload, decoded as the test's message type, matches -// `m`. The payload is decoded from binary or text output. `m` is any matcher -// on `const Message&`, usually EqualsTextProto() or EqualsBinaryProto(). +// `m`. The payload is decoded from whichever output format the test asked +// for. `m` is any matcher on `const Message&`, usually EqualsTextProto() or +// EqualsBinaryProto(). // // EXPECT_THAT( // Testee().ParseBinary(type, input).SerializeBinary(), @@ -93,10 +137,8 @@ // // A payload that can't be decoded fails with "<format> output we received // from test was unparseable." Otherwise the failure message comes from `m`. -// JSON output can't be decoded yet (b/410122158) and fails with a message -// saying so. Use RawPayload() for it. template <typename M> -testing::Matcher<const internal::TestResult&> WhenParsed(M m) { +testing::Matcher<const internal::YieldedResult&> WhenParsed(M m) { return internal::MakeWhenParsedMatcher( testing::SafeMatcherCast<const Message&>(std::move(m))); } @@ -112,7 +154,7 @@ // // Failure messages are the same as WhenParsed()'s. template <typename T, typename M> -testing::Matcher<const internal::TestResult&> WhenParsedAs(M m) { +testing::Matcher<const internal::YieldedResult&> WhenParsedAs(M m) { return internal::MakeWhenParsedMatcher( testing::SafeMatcherCast<const Message&>(std::move(m)), T::descriptor()); } @@ -129,7 +171,25 @@ // result, an error, a skipped test and the wrong output format. A PROTOBUF // payload must also be parseable as the test's message type. Otherwise it // fails with "Protobuf output we received from test was unparseable." -testing::Matcher<const internal::TestResult&> RawPayload(Wire bytes); +testing::Matcher<const internal::YieldedResult&> RawPayload(Wire bytes); + +// Matches a result whose JSON payload, parsed with jsoncpp, matches `m`. `m` +// is any matcher on `const Json::Value&`. The test must have asked for JSON +// output. +// +// EXPECT_THAT(Testee().ParseJson(type, input).SerializeJson(), +// Yields(JsonPayload(HasJsonMember("optionalInt32")))); +// +// Use this instead of WhenParsed() when the JSON text itself matters, for +// example to check that an int64 field is serialized as a string. +// +// A payload that isn't valid JSON fails with "JSON payload cannot be parsed +// as valid JSON: <error>". Otherwise the failure message comes from `m`. +template <typename M> +testing::Matcher<const internal::YieldedResult&> JsonPayload(M m) { + return internal::MakeJsonPayloadMatcher( + testing::SafeMatcherCast<const Json::Value&>(std::move(m))); +} // Matches a message equivalent to `text`, parsed as the actual message's type. // Messages are compared with MessageDifferencer, with NaN equal to NaN. @@ -147,7 +207,18 @@ // Yields(WhenParsed(EqualsBinaryProto(VarintField(1, 1))))); testing::Matcher<const Message&> EqualsBinaryProto(Wire bytes); -// TODO: b/410122158 - Add JSON matchers once JSON support is migrated. +// Matches a message whose unknown field set holds exactly the fields of +// `expected`, in the same order. Known fields are ignored. Meant for use +// inside WhenParsed() or WhenParsedAs(): +// +// UnknownFieldSet expected; +// expected.AddVarint(1, 1); +// EXPECT_THAT(Testee().ParseBinary(type, input).SerializeBinary(), +// Yields(WhenParsed(HasUnknownFieldsInOrder(expected)))); +// +// The failure message is "Unknown field mismatch". +testing::Matcher<const Message&> HasUnknownFieldsInOrder( + const UnknownFieldSet& expected); // Matches a response that reports a parse error. // @@ -157,7 +228,7 @@ // Any other response fails with "Should have failed to parse, but didn't." A // runtime error fails with "Should have failed to parse, but raised an error // instead." -testing::Matcher<const internal::TestResult&> IsParseError(); +testing::Matcher<const internal::YieldedResult&> IsParseError(); // Matches a response that reports a serialize error. // @@ -166,28 +237,33 @@ // // Any other response fails like it does for IsParseError(), with the // corresponding "Should have failed to serialize, ..." messages. -testing::Matcher<const internal::TestResult&> IsSerializeError(); +testing::Matcher<const internal::YieldedResult&> IsSerializeError(); // Wraps the matcher of every conformance test's EXPECT_THAT. // // EXPECT_THAT(Testee().ParseBinary(type, input).SerializeBinary(), // Yields(WhenParsed(EqualsBinaryProto(input)))); // -// Yields() evaluates `inner` against the TestResult, reports the outcome to -// the global TestManager and applies the failure list and priority rules -// described at the top of this file. Its verdict is not simply `inner`'s: an -// expected failure passes and an unexpected success fails. Do not wrap -// Yields() in Not() or other combinators. Compose `inner` instead. +// Yields() evaluates `inner` against the result, wrapped in an +// internal::YieldedResult (which is what the leaf matchers match), records +// the outcome in the running gtest test and applies the failure list and +// priority rules described at the top of this file. Its verdict is not +// simply `inner`'s: an expected failure passes and an unexpected success +// fails. Do not wrap Yields() in Not() or other combinators. Compose `inner` +// instead. // -// Each test may be checked exactly once. The first evaluation reports the -// result to the TestManager, which remembers the test's name. gtest -// evaluates a failing matcher a second time to explain the failure; Yields() -// replays its own failed verdict for that. Any other check of a result the -// TestManager has already heard about fails with "already checked". +// Each result is meant to be checked once. gtest evaluates a failing matcher +// a second time to explain the failure, so a second check is allowed as long +// as it reaches the same outcome, which it then only repeats. A check that +// reaches a different outcome than the one recorded is a test bug and fails +// with "already checked". A gtest test that runs a conformance test but +// never checks its result fails too (see ConformanceEnvironment in +// test_environment.h). template <typename M> testing::Matcher<const internal::TestResult&> Yields(M inner) { return internal::MakeYieldsMatcher( - testing::SafeMatcherCast<const internal::TestResult&>(std::move(inner))); + testing::SafeMatcherCast<const internal::YieldedResult&>( + std::move(inner))); } } // namespace conformance
diff --git a/conformance/matchers_test.cc b/conformance/matchers_test.cc index a8e3e19..83c2d7e 100644 --- a/conformance/matchers_test.cc +++ b/conformance/matchers_test.cc
@@ -12,10 +12,11 @@ #include <iterator> #include <ostream> #include <string> +#include <type_traits> #include <utility> +#include <vector> #include <gmock/gmock.h> -#include <gtest/gtest-spi.h> #include <gtest/gtest.h> #include "absl/base/log_severity.h" #include "absl/log/absl_check.h" @@ -26,16 +27,22 @@ #include "absl/strings/str_cat.h" #include "absl/strings/string_view.h" #include "absl/types/optional.h" +#include "json/value.h" #include "conformance/binary_wireformat.h" #include "conformance/conformance.pb.h" +#include "conformance/failure_list.h" #include "conformance/global_test_environment.h" +#include "conformance/json_test_util.h" +#include "conformance/matchers_testing.h" #include "conformance/mock_test_runner.h" -#include "conformance/test_manager.h" +#include "conformance/result_ledger.h" +#include "conformance/result_record.h" #include "conformance/testee.h" #include "google/protobuf/descriptor.h" #include "google/protobuf/message.h" #include "google/protobuf/test_messages_proto2.pb.h" #include "google/protobuf/text_format.h" +#include "google/protobuf/unknown_field_set.h" namespace google { namespace protobuf { @@ -46,13 +53,13 @@ // Backing state for the mocked-out global environment below. In the real // conformance binary this would be populated from CLI flags and the failure // list on disk, but we don't want to drag that in for a unit test. -TestManager* global_test_manager = nullptr; +const FailureList* global_failure_list = nullptr; } // namespace -TestManager& GetGlobalTestManager() { - ABSL_CHECK(global_test_manager != nullptr); - return *global_test_manager; +const FailureList& GetGlobalFailureList() { + ABSL_CHECK(global_failure_list != nullptr); + return *global_failure_list; } } // namespace internal @@ -63,8 +70,10 @@ using ::conformance::ConformanceResponse; using ::conformance::WireFormat; using ::google::protobuf::conformance::TestPriority; -using ::google::protobuf::conformance::internal::TestManager; +using ::google::protobuf::conformance::internal::ResultLedger; +using ::google::protobuf::conformance::internal::ResultRecord; using ::google::protobuf::conformance::internal::TestResult; +using ::google::protobuf::conformance::internal::YieldedResult; using ::protobuf_test_messages::proto2::TestAllTypesProto2; using ::protobuf_test_messages::proto2::UnknownToTestAllTypes; using ::testing::_; @@ -82,6 +91,8 @@ using ::testing::Pair; using ::testing::PrintToString; using ::testing::Return; +using ::testing::SizeIs; +using ::testing::StartsWith; using ::testing::TestParamInfo; using ::testing::Truly; using ::testing::Value; @@ -93,9 +104,9 @@ // `response`. The matchers never look at the input payload itself, only at // its format, so the input is empty. // -// The leaf matchers never touch the TestManager; the tests make sure of it by -// not installing one at all while they run. Only YieldsTest below installs -// one. +// The leaf matchers never touch the failure list; the tests make sure of it +// by not installing one at all while they run. Only YieldsTest below +// installs one. TestResult CreateResult(absl::string_view test_name, WireFormat input_format, WireFormat output_format, ConformanceResponse response) { @@ -149,14 +160,14 @@ return response; } -template <class M> -std::string Describe(const M& m) { - return testing::DescribeMatcher<const TestResult&>(m, /*negation=*/false); +template <class T> +std::string Describe(const testing::Matcher<T>& m) { + return testing::DescribeMatcher<T>(m, /*negation=*/false); } -template <class M> -std::string DescribeNegation(const M& m) { - return testing::DescribeMatcher<const TestResult&>(m, /*negation=*/true); +template <class T> +std::string DescribeNegation(const testing::Matcher<T>& m) { + return testing::DescribeMatcher<T>(m, /*negation=*/true); } // Runs `m` on `arg` once. The returned AssertionResult succeeds iff `m` @@ -170,6 +181,13 @@ << listener.str(); } +// The leaf matchers match a YieldedResult; this overload wraps `result` in +// one, so that their tests can pass the TestResult. +testing::AssertionResult Explain( + const testing::Matcher<const YieldedResult&>& m, const TestResult& result) { + return Explain(m, YieldedResult(result)); +} + // Matches an Explain() result whose matcher accepted the value and whose // explanation matches `explanation` (a string or a matcher on std::string). MATCHER_P(Accepts, explanation, @@ -282,6 +300,15 @@ } // --------------------------------------------------------------------------- +// YieldedResult +// --------------------------------------------------------------------------- + +// What makes a forgotten Yields() a compile error: the leaf matchers match a +// YieldedResult, and a TestResult does not convert to one. +static_assert(!std::is_convertible_v<const TestResult&, YieldedResult>); +static_assert(!std::is_constructible_v<YieldedResult, TestResult>); + +// --------------------------------------------------------------------------- // Descriptions // --------------------------------------------------------------------------- @@ -328,6 +355,36 @@ "doesn't equal binary proto \"\\010\\t\""); } +TEST(MatcherDescriptionTest, HasUnknownFieldsInOrder) { + UnknownFieldSet expected; + expected.AddLengthDelimited(666, "abc"); + expected.AddVarint(666, 123); + EXPECT_EQ(testing::DescribeMatcher<const Message&>( + HasUnknownFieldsInOrder(expected)), + "has exactly the unknown fields [666: \"abc\", 666: 123], in that " + "order"); + EXPECT_EQ(testing::DescribeMatcher<const Message&>( + HasUnknownFieldsInOrder(expected), /*negation=*/true), + "doesn't have exactly the unknown fields [666: \"abc\", 666: 123] " + "in that order"); +} + +TEST(MatcherDescriptionTest, HasUnknownFieldsInOrderCoversEveryFieldKind) { + UnknownFieldSet expected; + expected.AddVarint(1, 2); + expected.AddFixed32(3, 4); + expected.AddFixed64(5, 6); + expected.AddLengthDelimited(7, "\001\n"); + expected.AddGroup(8)->AddVarint(9, 10); + EXPECT_EQ(testing::DescribeMatcher<const Message&>( + HasUnknownFieldsInOrder(expected)), + "has exactly the unknown fields [1: 2, 3: fixed32(4), 5: " + "fixed64(6), 7: \"\\001\\n\", 8: [9: 10]], in that order"); + EXPECT_EQ(testing::DescribeMatcher<const Message&>( + HasUnknownFieldsInOrder(UnknownFieldSet())), + "has exactly the unknown fields [], in that order"); +} + TEST(MatcherDescriptionTest, FailureMatchers) { EXPECT_EQ(Describe(IsParseError()), "is a parse error"); EXPECT_EQ(DescribeNegation(IsParseError()), "is not a parse error"); @@ -484,16 +541,47 @@ Rejects("Protobuf output we received from test was unparseable.")); } -TEST(WhenParsedTest, JsonOutputIsNotSupported) { - TestResult result = - CreateResult("foo", ::conformance::JSON, - ResponseFromText(R"pb(json_payload: "{}")pb")); +TEST(WhenParsedTest, JsonOutput) { + TestResult result = CreateResult( + "foo", ::conformance::JSON, + ResponseFromText(R"pb(json_payload: "{\"optionalInt32\": 9}")pb")); EXPECT_THAT( - Explain(WhenParsed(_), result), - Rejects("WhenParsed is not supported for JSON output; use " - "RawPayload() to match the raw JSON text until JSON matching is " - "migrated to gtest (b/410122158).")); + Explain(WhenParsed(EqualsTextProto(R"pb(optional_int32: 9)pb")), result), + Accepts(IsEmpty())); + EXPECT_TRUE(Value(YieldedResult(result), + WhenParsed(EqualsBinaryProto(VarintField(1, 9))))); + EXPECT_THAT( + Explain(WhenParsed(EqualsTextProto(R"pb(optional_int32: 1)pb")), result), + Rejects("Output was not equivalent to reference message: " + "modified: optional_int32: 1 -> 9\n")); +} + +TEST(WhenParsedTest, UnparseableJsonPayload) { + TestResult result = CreateResult( + "foo", ::conformance::JSON, + ResponseFromText(R"pb(json_payload: "{\"nonsense\": 1}")pb")); + + EXPECT_THAT(Explain(WhenParsed(_), result), + Rejects("JSON output we received from test was unparseable.")); + EXPECT_THAT( + Explain(WhenParsed(_), + CreateResult("bar", ::conformance::JSON, + ResponseFromText(R"pb(json_payload: "{")pb"))), + Rejects("JSON output we received from test was unparseable.")); +} + +TEST(WhenParsedTest, JsonOutputIsDecodedWithLegacyLeniency) { + // The legacy runner decoded JSON output with the default ParseOptions, + // which tolerate some non-conformant output; so does WhenParsed() until + // b/563658359 tightens it. Single-quoted strings are one such case; this + // test documents the current behaviour rather than endorsing it. + TestResult result = CreateResult( + "foo", ::conformance::JSON, + ResponseFromText(R"pb(json_payload: "{'optionalInt32': 9}")pb")); + + EXPECT_TRUE(Value(YieldedResult(result), + WhenParsed(EqualsTextProto(R"pb(optional_int32: 9)pb")))); } TEST(WhenParsedTest, TextOutput) { @@ -501,8 +589,8 @@ "foo", ::conformance::TEXT_FORMAT, ResponseFromText(R"pb(text_payload: "optional_int32: 9")pb")); - EXPECT_TRUE( - Value(result, WhenParsed(EqualsTextProto(R"pb(optional_int32: 9)pb")))); + EXPECT_TRUE(Value(YieldedResult(result), + WhenParsed(EqualsTextProto(R"pb(optional_int32: 9)pb")))); } TEST(WhenParsedTest, TextOutputFieldNumbersAccepted) { @@ -512,8 +600,8 @@ CreateResult("foo", ::conformance::TEXT_FORMAT, ResponseFromText(R"pb(text_payload: "1: 9")pb")); - EXPECT_TRUE( - Value(result, WhenParsed(EqualsTextProto(R"pb(optional_int32: 9)pb")))); + EXPECT_TRUE(Value(YieldedResult(result), + WhenParsed(EqualsTextProto(R"pb(optional_int32: 9)pb")))); } TEST(WhenParsedTest, EqualsTextProtoMismatch) { @@ -534,7 +622,7 @@ CreateResult("foo", ::conformance::PROTOBUF, ProtobufPayload(Wire(message.SerializeAsString()))); - EXPECT_TRUE(Value(result, WhenParsed(EqualsTextProto(R"pb( + EXPECT_TRUE(Value(YieldedResult(result), WhenParsed(EqualsTextProto(R"pb( optional_float: nan optional_double: nan )pb")))); @@ -554,8 +642,8 @@ ProtobufPayload(VarintField(1, 2))); // Equivalent, but not byte-identical (over-long varint encoding). - EXPECT_TRUE( - Value(result, WhenParsed(EqualsBinaryProto(LongVarintField(1, 2, 1))))); + EXPECT_TRUE(Value(YieldedResult(result), + WhenParsed(EqualsBinaryProto(LongVarintField(1, 2, 1))))); } TEST(WhenParsedTest, EqualsBinaryProtoWithTextOutput) { @@ -563,7 +651,8 @@ "foo", ::conformance::TEXT_FORMAT, ResponseFromText(R"pb(text_payload: "optional_int32: 9")pb")); - EXPECT_TRUE(Value(result, WhenParsed(EqualsBinaryProto(VarintField(1, 9))))); + EXPECT_TRUE(Value(YieldedResult(result), + WhenParsed(EqualsBinaryProto(VarintField(1, 9))))); } TEST(WhenParsedTest, EqualsBinaryProtoMismatch) { @@ -588,9 +677,9 @@ TestResult result = CreateResult("foo", ::conformance::PROTOBUF, ProtobufPayload(VarintField(1, 2))); - EXPECT_TRUE(Value(result, WhenParsed(_))); + EXPECT_TRUE(Value(YieldedResult(result), WhenParsed(_))); EXPECT_TRUE(Value( - result, + YieldedResult(result), WhenParsed(AllOf(EqualsTextProto(R"pb(optional_int32: 2)pb"), Not(EqualsTextProto(R"pb(optional_int32: 3)pb")))))); } @@ -612,11 +701,12 @@ TestResult result = CreateResult("foo", ::conformance::PROTOBUF, ProtobufPayload(VarintField(1, 2))); - EXPECT_TRUE(Value(result, Not(WhenParsed(EqualsTextProto(""))))); + YieldedResult yielded(result); + EXPECT_TRUE(Value(yielded, Not(WhenParsed(EqualsTextProto(""))))); EXPECT_TRUE(Value( - result, AnyOf(IsParseError(), - WhenParsed(EqualsTextProto(R"pb(optional_int32: 2)pb"))))); - EXPECT_TRUE(Value(result, AllOf(Not(IsParseError()), WhenParsed(_)))); + yielded, AnyOf(IsParseError(), + WhenParsed(EqualsTextProto(R"pb(optional_int32: 2)pb"))))); + EXPECT_TRUE(Value(yielded, AllOf(Not(IsParseError()), WhenParsed(_)))); } // --------------------------------------------------------------------------- @@ -719,7 +809,7 @@ TestResult result = CreateResult("foo", ::conformance::PROTOBUF, ProtobufPayload(VarintField(1, 2))); - EXPECT_TRUE(Value(result, RawPayload(VarintField(1, 2)))); + EXPECT_TRUE(Value(YieldedResult(result), RawPayload(VarintField(1, 2)))); EXPECT_THAT(Explain(RawPayload(VarintField(1, 2)), result), Accepts(IsEmpty())); } @@ -736,8 +826,8 @@ } TEST(RawPayloadTest, UnparseableProtobufOutputFailsEvenForTheExactBytes) { - // Like the legacy runner's require_same_wire_format, binary output must - // decode as the test's message type. + // Like the legacy runner's byte-exact mode, binary output must decode as + // the test's message type. TestResult result = CreateResult("foo", ::conformance::PROTOBUF, ProtobufPayload(Wire("\001"))); @@ -751,12 +841,13 @@ "foo", ::conformance::TEXT_FORMAT, ResponseFromText(R"pb(text_payload: "optional_int32: 9")pb")); - EXPECT_TRUE(Value(result, RawPayload(Wire("optional_int32: 9")))); + EXPECT_TRUE( + Value(YieldedResult(result), RawPayload(Wire("optional_int32: 9")))); // Text output isn't required to be parseable. - EXPECT_TRUE(Value( + TestResult nonsense = CreateResult("bar", ::conformance::TEXT_FORMAT, - ResponseFromText(R"pb(text_payload: "nonsense: 1")pb")), - RawPayload(Wire("nonsense: 1")))); + ResponseFromText(R"pb(text_payload: "nonsense: 1")pb")); + EXPECT_TRUE(Value(YieldedResult(nonsense), RawPayload(Wire("nonsense: 1")))); } TEST(RawPayloadTest, JsonOutput) { @@ -765,7 +856,7 @@ CreateResult("foo", ::conformance::JSON, ResponseFromText(R"pb(json_payload: "{}")pb")); - EXPECT_TRUE(Value(result, RawPayload(Wire("{}")))); + EXPECT_TRUE(Value(YieldedResult(result), RawPayload(Wire("{}")))); } // --------------------------------------------------------------------------- @@ -832,6 +923,172 @@ } // --------------------------------------------------------------------------- +// HasUnknownFieldsInOrder() +// --------------------------------------------------------------------------- + +// Field 666 is defined by none of the test messages, so it always stays +// unknown. + +TEST(HasUnknownFieldsInOrderTest, SameFieldsInSameOrder) { + UnknownFieldSet expected; + expected.AddLengthDelimited(666, "abc"); + expected.AddVarint(666, 123); + TestAllTypesProto2 message; + message.mutable_unknown_fields()->AddLengthDelimited(666, "abc"); + message.mutable_unknown_fields()->AddVarint(666, 123); + + EXPECT_THAT(Explain(HasUnknownFieldsInOrder(expected), message), + Accepts(IsEmpty())); +} + +TEST(HasUnknownFieldsInOrderTest, DifferentOrder) { + UnknownFieldSet expected; + expected.AddLengthDelimited(666, "abc"); + expected.AddVarint(666, 123); + TestAllTypesProto2 message; + message.mutable_unknown_fields()->AddVarint(666, 123); + message.mutable_unknown_fields()->AddLengthDelimited(666, "abc"); + + auto matcher = HasUnknownFieldsInOrder(expected); + EXPECT_THAT(Explain(matcher, message), Rejects("Unknown field mismatch")); + EXPECT_THAT(message, Not(matcher)); +} + +TEST(HasUnknownFieldsInOrderTest, MissingField) { + UnknownFieldSet expected; + expected.AddLengthDelimited(666, "abc"); + expected.AddVarint(666, 123); + TestAllTypesProto2 message; + message.mutable_unknown_fields()->AddLengthDelimited(666, "abc"); + + EXPECT_THAT(Explain(HasUnknownFieldsInOrder(expected), message), + Rejects("Unknown field mismatch")); +} + +TEST(HasUnknownFieldsInOrderTest, ExtraField) { + UnknownFieldSet expected; + expected.AddLengthDelimited(666, "abc"); + TestAllTypesProto2 message; + message.mutable_unknown_fields()->AddLengthDelimited(666, "abc"); + message.mutable_unknown_fields()->AddVarint(666, 123); + + EXPECT_THAT(Explain(HasUnknownFieldsInOrder(expected), message), + Rejects("Unknown field mismatch")); +} + +TEST(HasUnknownFieldsInOrderTest, DifferentValue) { + UnknownFieldSet expected; + expected.AddVarint(666, 123); + TestAllTypesProto2 message; + message.mutable_unknown_fields()->AddVarint(666, 124); + + EXPECT_THAT(Explain(HasUnknownFieldsInOrder(expected), message), + Rejects("Unknown field mismatch")); +} + +TEST(HasUnknownFieldsInOrderTest, DifferentFieldNumber) { + UnknownFieldSet expected; + expected.AddVarint(666, 123); + TestAllTypesProto2 message; + message.mutable_unknown_fields()->AddVarint(667, 123); + + EXPECT_THAT(Explain(HasUnknownFieldsInOrder(expected), message), + Rejects("Unknown field mismatch")); +} + +TEST(HasUnknownFieldsInOrderTest, DifferentWireType) { + UnknownFieldSet expected; + expected.AddVarint(666, 1); + TestAllTypesProto2 message; + message.mutable_unknown_fields()->AddFixed32(666, 1); + + EXPECT_THAT(Explain(HasUnknownFieldsInOrder(expected), message), + Rejects("Unknown field mismatch")); +} + +TEST(HasUnknownFieldsInOrderTest, GroupsAreComparedByContent) { + UnknownFieldSet expected; + expected.AddGroup(666)->AddVarint(1, 2); + TestAllTypesProto2 same; + same.mutable_unknown_fields()->AddGroup(666)->AddVarint(1, 2); + TestAllTypesProto2 different; + different.mutable_unknown_fields()->AddGroup(666)->AddVarint(1, 3); + + EXPECT_THAT(Explain(HasUnknownFieldsInOrder(expected), same), + Accepts(IsEmpty())); + EXPECT_THAT(Explain(HasUnknownFieldsInOrder(expected), different), + Rejects("Unknown field mismatch")); +} + +TEST(HasUnknownFieldsInOrderTest, EmptyExpectedMatchesOnlyNoUnknownFields) { + TestAllTypesProto2 without; + TestAllTypesProto2 with; + with.mutable_unknown_fields()->AddVarint(666, 1); + + EXPECT_THAT(Explain(HasUnknownFieldsInOrder(UnknownFieldSet()), without), + Accepts(IsEmpty())); + EXPECT_THAT(Explain(HasUnknownFieldsInOrder(UnknownFieldSet()), with), + Rejects("Unknown field mismatch")); +} + +TEST(HasUnknownFieldsInOrderTest, IgnoresKnownFields) { + UnknownFieldSet expected; + expected.AddVarint(666, 123); + TestAllTypesProto2 message; + message.set_optional_int32(9); + message.set_optional_string("known"); + message.mutable_unknown_fields()->AddVarint(666, 123); + + EXPECT_THAT(Explain(HasUnknownFieldsInOrder(expected), message), + Accepts(IsEmpty())); +} + +// The end-to-end case: the mismatch text is what ends up in failure lists. +TEST(WhenParsedTest, HasUnknownFieldsInOrderKeepsTheLegacyMessage) { + UnknownFieldSet expected; + expected.AddLengthDelimited(666, "abc"); + expected.AddVarint(666, 123); + TestResult in_order = + CreateResult("foo", ::conformance::PROTOBUF, + ProtobufPayload(Wire(LengthPrefixedField(666, "abc"), + VarintField(666, 123)))); + TestResult reordered = + CreateResult("foo", ::conformance::PROTOBUF, + ProtobufPayload(Wire(VarintField(666, 123), + LengthPrefixedField(666, "abc")))); + + EXPECT_THAT(Explain(WhenParsed(HasUnknownFieldsInOrder(expected)), in_order), + Accepts(IsEmpty())); + EXPECT_THAT(Explain(WhenParsed(HasUnknownFieldsInOrder(expected)), reordered), + Rejects("Unknown field mismatch")); +} + +TEST(WhenParsedAsTest, HasUnknownFieldsInOrderSeesTheGivenTypesUnknowns) { + // Field 1001 is unknown to the test's type (TestAllTypesProto2) but is + // optional_int32 of the shadow type UnknownToTestAllTypes; 666 is unknown + // to both. + TestResult result = CreateResult( + "foo", ::conformance::PROTOBUF, + ProtobufPayload(Wire(VarintField(1001, 7), VarintField(666, 1)))); + UnknownFieldSet both; + both.AddVarint(1001, 7); + both.AddVarint(666, 1); + UnknownFieldSet only_666; + only_666.AddVarint(666, 1); + + EXPECT_THAT(Explain(WhenParsed(HasUnknownFieldsInOrder(both)), result), + Accepts(IsEmpty())); + EXPECT_THAT(Explain(WhenParsedAs<UnknownToTestAllTypes>( + HasUnknownFieldsInOrder(only_666)), + result), + Accepts(IsEmpty())); + EXPECT_THAT(Explain(WhenParsedAs<UnknownToTestAllTypes>( + HasUnknownFieldsInOrder(both)), + result), + Rejects("Unknown field mismatch")); +} + +// --------------------------------------------------------------------------- // Failure matchers // --------------------------------------------------------------------------- @@ -841,7 +1098,7 @@ ResponseFromText(R"pb(parse_error: "failed to parse")pb")); EXPECT_THAT(Explain(IsParseError(), result), Accepts(IsEmpty())); - EXPECT_FALSE(Value(result, IsSerializeError())); + EXPECT_FALSE(Value(YieldedResult(result), IsSerializeError())); } TEST(FailureMatcherTest, ParseErrorWithPayload) { @@ -895,7 +1152,7 @@ ResponseFromText(R"pb(serialize_error: "failed")pb")); EXPECT_THAT(Explain(IsSerializeError(), result), Accepts(IsEmpty())); - EXPECT_FALSE(Value(result, IsParseError())); + EXPECT_FALSE(Value(YieldedResult(result), IsParseError())); } TEST(FailureMatcherTest, SerializeErrorWithPayload) { @@ -921,16 +1178,18 @@ CreateResult("foo", ::conformance::PROTOBUF, ResponseFromText(R"pb(parse_error: "failed to parse")pb")); - EXPECT_TRUE(Value(result, Not(IsSerializeError()))); - EXPECT_TRUE(Value(result, AnyOf(IsSerializeError(), IsParseError()))); - EXPECT_FALSE(Value(result, AllOf(IsSerializeError(), IsParseError()))); + EXPECT_TRUE(Value(YieldedResult(result), Not(IsSerializeError()))); + EXPECT_TRUE( + Value(YieldedResult(result), AnyOf(IsSerializeError(), IsParseError()))); + EXPECT_FALSE( + Value(YieldedResult(result), AllOf(IsSerializeError(), IsParseError()))); } // --------------------------------------------------------------------------- // Yields() // --------------------------------------------------------------------------- -// A snapshot of the TestManager counters, for concise assertions. +// A snapshot of the ResultLedger counters, for concise assertions. struct Counts { int expected_successes = 0; int expected_failures = 0; @@ -960,16 +1219,16 @@ } }; -Counts GetCounts(const TestManager& manager) { +Counts GetCounts(const ResultLedger& ledger) { return Counts{ - /*expected_successes=*/manager.expected_successes(), - /*expected_failures=*/manager.expected_failures(), - /*unexpected_successes=*/manager.unexpected_successes(), - /*unexpected_failures=*/manager.unexpected_failures(), - /*skipped=*/manager.skipped(), - /*listed_skips=*/manager.listed_skips(), + /*expected_successes=*/ledger.expected_successes(), + /*expected_failures=*/ledger.expected_failures(), + /*unexpected_successes=*/ledger.unexpected_successes(), + /*unexpected_failures=*/ledger.unexpected_failures(), + /*skipped=*/ledger.skipped(), + /*listed_skips=*/ledger.listed_skips(), /*tolerated_failures=*/ - manager.tolerated_failures(), + ledger.tolerated_failures(), }; } @@ -985,6 +1244,7 @@ constexpr absl::string_view kSerializeError = R"pb(serialize_error: "can't serialize")pb"; constexpr absl::string_view kRuntimeError = R"pb(runtime_error: "crashed")pb"; +constexpr absl::string_view kTimeoutError = R"pb(timeout_error: "too slow")pb"; constexpr absl::string_view kSkipped = R"pb(skipped: "not supported")pb"; // The legacy failure messages the canned responses lead to. @@ -1015,10 +1275,10 @@ class YieldsTest : public testing::Test { protected: - YieldsTest() { internal::global_test_manager = &test_manager_; } + YieldsTest() { internal::global_failure_list = &ledger_.failure_list(); } ~YieldsTest() override { - test_manager_.Finalize().IgnoreError(); - internal::global_test_manager = nullptr; + ledger_.Finalize().IgnoreError(); + internal::global_failure_list = nullptr; } // Makes the testee answer every test with `response`. @@ -1052,13 +1312,27 @@ // failure list file would. void AddToFailureList(absl::string_view test_name, absl::string_view message) { - ASSERT_THAT(test_manager_.LoadFailureList(WriteTempFile( + ASSERT_THAT(ledger_.LoadFailureList(WriteTempFile( absl::StrCat("failure_list_", ++failure_lists_, ".txt"), absl::StrCat(test_name, " # ", message, "\n"))), IsOk()); } - TestManager test_manager_; + // Replays the outcomes Yields() recorded on the running gtest test into + // ledger_, as the test environment's listener does as they arrive, + // and returns the counters. Tests the ledger has heard about are skipped, + // so this can be called repeatedly. + Counts ReplayedCounts() { + for (const auto& [name, value] : + internal::RecordedResults(internal::CurrentGtestResult())) { + if (!ledger_.WasReported(name)) { + ledger_.Report(name, *ResultRecord::Parse(value)); + } + } + return GetCounts(ledger_); + } + + ResultLedger ledger_; NiceMock<MockTestRunner> runner_; internal::Testee testee_{&runner_}; int failure_lists_ = 0; @@ -1070,8 +1344,20 @@ EXPECT_THAT(Explain(matcher, Run(kPayload2)), Accepts(IsEmpty())); EXPECT_THAT(Run(kPayload2, TestPriority::kP0, "other"), matcher); - EXPECT_EQ(GetCounts(test_manager_), (Counts{/*expected_successes=*/2})); - EXPECT_THAT(test_manager_.Finalize(), IsOk()); + EXPECT_EQ(ReplayedCounts(), (Counts{/*expected_successes=*/2})); + EXPECT_THAT(ledger_.Finalize(), IsOk()); +} + +TEST_F(YieldsTest, InnerMatcherSeesTheResultAsAYieldedResult) { + const TestResult* seen = nullptr; + auto matcher = Yields(Truly([&](const YieldedResult& yielded) { + seen = &yielded.result(); + return true; + })); + TestResult result = Run(kPayload2); + + EXPECT_THAT(Explain(matcher, result), Accepts(IsEmpty())); + EXPECT_EQ(seen, &result); } TEST_F(YieldsTest, UnlistedFailureFails) { @@ -1082,14 +1368,14 @@ EXPECT_THAT(Explain(matcher, result), Rejects(absl::StrCat(kMismatch1, "\nUnexpected failure for test: ", kP0Foo))); - EXPECT_NONFATAL_FAILURE(EXPECT_THAT(result, matcher), - "modified: optional_int32: 1 -> 2"); - EXPECT_EQ(GetCounts(test_manager_), (Counts{/*expected_successes=*/0, - /*expected_failures=*/0, - /*unexpected_successes=*/0, - /*unexpected_failures=*/1})); + EXPECT_YIELDS_FAILURE(EXPECT_THAT(result, matcher), + "modified: optional_int32: 1 -> 2"); + EXPECT_EQ(ReplayedCounts(), (Counts{/*expected_successes=*/0, + /*expected_failures=*/0, + /*unexpected_successes=*/0, + /*unexpected_failures=*/1})); // The recorded message is the legacy one, as the failure list would get it. - EXPECT_THAT(test_manager_.UnexpectedFailures(), + EXPECT_THAT(ledger_.UnexpectedFailures(), ElementsAre(FieldsAre(kP0Foo, "Output was not equivalent to reference " "message: modified: optional_int32: 1 -> 2", @@ -1098,47 +1384,27 @@ TEST_F(YieldsTest, UnlistedFailureIsCountedOnceDespiteGtestRetrying) { // gtest evaluates a matcher a second time to explain a failed assertion. - EXPECT_NONFATAL_FAILURE(EXPECT_THAT(Run(kPayload2), Yields(IsParseError())), - "Should have failed to parse, but didn't."); - EXPECT_EQ(GetCounts(test_manager_), (Counts{/*expected_successes=*/0, - /*expected_failures=*/0, - /*unexpected_successes=*/0, - /*unexpected_failures=*/1})); - EXPECT_THAT(test_manager_.UnexpectedFailures(), + EXPECT_YIELDS_FAILURE(EXPECT_THAT(Run(kPayload2), Yields(IsParseError())), + "Should have failed to parse, but didn't."); + EXPECT_EQ(ReplayedCounts(), (Counts{/*expected_successes=*/0, + /*expected_failures=*/0, + /*unexpected_successes=*/0, + /*unexpected_failures=*/1})); + EXPECT_THAT(ledger_.UnexpectedFailures(), ElementsAre(FieldsAre(kP0Foo, kNotAParseError, absl::nullopt))); } -TEST_F(YieldsTest, StaleVerdictIsNeverReplayedForAnotherResult) { - // A long-lived matcher that checked a result which has since been destroyed - // must not replay that verdict for a different result, even one that reuses - // the same address: the replay is keyed to the result's name as well. - auto first_matcher = Yields(IsParseError()); - { - TestResult first = Run(kPayload2, TestPriority::kP0, "first"); - EXPECT_NONFATAL_FAILURE(EXPECT_THAT(first, first_matcher), - "Should have failed to parse, but didn't."); - } - TestResult second = Run(kParseError, TestPriority::kP0, "second"); - EXPECT_THAT(second, Yields(IsParseError())); - - EXPECT_THAT(Explain(first_matcher, second), - Rejects(HasSubstr("was already checked"))); - EXPECT_EQ(GetCounts(test_manager_), - (Counts{/*expected_successes=*/1, /*expected_failures=*/0, - /*unexpected_successes=*/0, /*unexpected_failures=*/1})); -} - TEST_F(YieldsTest, PayloadBytesMismatchRecordsTheLegacyOctalMessage) { - EXPECT_NONFATAL_FAILURE( + EXPECT_YIELDS_FAILURE( EXPECT_THAT(Run(kPayload2), Yields(RawPayload(VarintField(1, 1)))), "Output was not equivalent to reference message: " "Expect: \\010\\001, but got: \\010\\002"); - EXPECT_EQ(GetCounts(test_manager_), (Counts{/*expected_successes=*/0, - /*expected_failures=*/0, - /*unexpected_successes=*/0, - /*unexpected_failures=*/1})); + EXPECT_EQ(ReplayedCounts(), (Counts{/*expected_successes=*/0, + /*expected_failures=*/0, + /*unexpected_successes=*/0, + /*unexpected_failures=*/1})); EXPECT_THAT( - test_manager_.UnexpectedFailures(), + ledger_.UnexpectedFailures(), ElementsAre(FieldsAre(kP0Foo, "Output was not equivalent to reference message: " "Expect: \\010\\001, but got: \\010\\002", @@ -1153,23 +1419,23 @@ Yields(WhenParsed(EqualsTextProto(R"pb(optional_int32: 2)pb")))); EXPECT_THAT(RunText(kText2, TestPriority::kP0, "raw"), Yields(RawPayload(Wire("optional_int32: 2")))); - EXPECT_EQ(GetCounts(test_manager_), (Counts{/*expected_successes=*/2})); + EXPECT_EQ(ReplayedCounts(), (Counts{/*expected_successes=*/2})); - EXPECT_NONFATAL_FAILURE( + EXPECT_YIELDS_FAILURE( EXPECT_THAT( RunText(kText2, TestPriority::kP0, "mismatch"), Yields(WhenParsed(EqualsTextProto(R"pb(optional_int32: 1)pb")))), "modified: optional_int32: 1 -> 2"); - EXPECT_NONFATAL_FAILURE( + EXPECT_YIELDS_FAILURE( EXPECT_THAT(RunText(R"pb(text_payload: "nonsense: 1")pb", TestPriority::kP0, "unparseable"), Yields(WhenParsed(_))), "TEXT_FORMAT output we received from test was unparseable."); - EXPECT_EQ(GetCounts(test_manager_), + EXPECT_EQ(ReplayedCounts(), (Counts{/*expected_successes=*/2, /*expected_failures=*/0, /*unexpected_successes=*/0, /*unexpected_failures=*/2})); EXPECT_THAT( - test_manager_.UnexpectedFailures(), + ledger_.UnexpectedFailures(), ElementsAre( FieldsAre("Required.Proto2.ProtobufInput.mismatch.TextFormatOutput", "Output was not equivalent to reference message: " @@ -1199,10 +1465,10 @@ TestResult result = Run(kPayload2); EXPECT_THAT(Explain(matcher, result), Accepts(IsEmpty())); - EXPECT_EQ(GetCounts(test_manager_), (Counts{/*expected_successes=*/0, - /*expected_failures=*/1})); - EXPECT_THAT(test_manager_.UnexpectedFailures(), IsEmpty()); - EXPECT_THAT(test_manager_.Finalize(), IsOk()); + EXPECT_EQ(ReplayedCounts(), (Counts{/*expected_successes=*/0, + /*expected_failures=*/1})); + EXPECT_THAT(ledger_.UnexpectedFailures(), IsEmpty()); + EXPECT_THAT(ledger_.Finalize(), IsOk()); } TEST_F(YieldsTest, ListedFailureWithMessagePrefixPasses) { @@ -1210,9 +1476,9 @@ EXPECT_THAT(Run(kPayload2), Yields(WhenParsed(EqualsTextProto(R"pb(optional_int32: 1)pb")))); - EXPECT_EQ(GetCounts(test_manager_), (Counts{/*expected_successes=*/0, - /*expected_failures=*/1})); - EXPECT_THAT(test_manager_.Finalize(), IsOk()); + EXPECT_EQ(ReplayedCounts(), (Counts{/*expected_successes=*/0, + /*expected_failures=*/1})); + EXPECT_THAT(ledger_.Finalize(), IsOk()); } TEST_F(YieldsTest, ListedFailureViaWildcardPasses) { @@ -1220,9 +1486,9 @@ kNotAParseError); EXPECT_THAT(Run(kPayload2), Yields(IsParseError())); - EXPECT_EQ(GetCounts(test_manager_), (Counts{/*expected_successes=*/0, - /*expected_failures=*/1})); - EXPECT_THAT(test_manager_.Finalize(), IsOk()); + EXPECT_EQ(ReplayedCounts(), (Counts{/*expected_successes=*/0, + /*expected_failures=*/1})); + EXPECT_THAT(ledger_.Finalize(), IsOk()); } TEST_F(YieldsTest, ListedFailureWithDifferentMessageFails) { @@ -1235,13 +1501,13 @@ Rejects(absl::StrCat( kNotAParseError, "\nUnexpected failure message for test: ", kP0Foo, " expected: Some other message actual: ", kNotAParseError))); - EXPECT_NONFATAL_FAILURE(EXPECT_THAT(result, matcher), - "Unexpected failure message for test"); - EXPECT_EQ(GetCounts(test_manager_), (Counts{/*expected_successes=*/0, - /*expected_failures=*/0, - /*unexpected_successes=*/0, - /*unexpected_failures=*/1})); - EXPECT_THAT(test_manager_.UnexpectedFailures(), + EXPECT_YIELDS_FAILURE(EXPECT_THAT(result, matcher), + "Unexpected failure message for test"); + EXPECT_EQ(ReplayedCounts(), (Counts{/*expected_successes=*/0, + /*expected_failures=*/0, + /*unexpected_successes=*/0, + /*unexpected_failures=*/1})); + EXPECT_THAT(ledger_.UnexpectedFailures(), ElementsAre(FieldsAre(kP0Foo, kNotAParseError, absl::nullopt))); } @@ -1257,31 +1523,31 @@ Rejects(absl::StrCat("test ", kP0Foo, " (matched to ", kP0Foo, ") is in the failure list, but test succeeded. " "Remove its match from the failure list."))); - EXPECT_NONFATAL_FAILURE(EXPECT_THAT(result, matcher), - "is in the failure list, but test succeeded"); - EXPECT_EQ(GetCounts(test_manager_), (Counts{/*expected_successes=*/0, - /*expected_failures=*/0, - /*unexpected_successes=*/1})); + EXPECT_YIELDS_FAILURE(EXPECT_THAT(result, matcher), + "is in the failure list, but test succeeded"); + EXPECT_EQ(ReplayedCounts(), (Counts{/*expected_successes=*/0, + /*expected_failures=*/0, + /*unexpected_successes=*/1})); EXPECT_THAT( - test_manager_.UnexpectedSuccesses(), + ledger_.UnexpectedSuccesses(), ElementsAre(FieldsAre(kP0Foo, kNotAParseError, Optional(kP0Foo)))); } TEST_F(YieldsTest, ListedSuccessViaWildcardNamesTheWildcard) { AddToFailureList("Required.*.ProtobufInput.foo.ProtobufOutput", ""); - EXPECT_NONFATAL_FAILURE( + EXPECT_YIELDS_FAILURE( EXPECT_THAT(Run(kParseError), Yields(IsParseError())), "test Required.Proto2.ProtobufInput.foo.ProtobufOutput (matched to " "Required.*.ProtobufInput.foo.ProtobufOutput) is in the failure list, " "but test succeeded. Remove its match from the failure list."); - EXPECT_EQ(GetCounts(test_manager_), (Counts{/*expected_successes=*/0, - /*expected_failures=*/0, - /*unexpected_successes=*/1})); + EXPECT_EQ(ReplayedCounts(), (Counts{/*expected_successes=*/0, + /*expected_failures=*/0, + /*unexpected_successes=*/1})); } TEST_F(YieldsTest, P1FailureIsToleratedWhenNotEnforced) { - test_manager_.set_enforcement_level(kP0); + ledger_.set_enforcement_level(kP0); absl::ScopedMockLog log; EXPECT_CALL(log, Log).Times(AnyNumber()); EXPECT_CALL( @@ -1296,60 +1562,60 @@ EXPECT_THAT(Explain(matcher, result), Accepts(IsEmpty())); // It's neither a skip nor a failure. - EXPECT_EQ(GetCounts(test_manager_), + EXPECT_EQ(ReplayedCounts(), (Counts{/*expected_successes=*/0, /*expected_failures=*/0, /*unexpected_successes=*/0, /*unexpected_failures=*/0, /*skipped=*/0, /*listed_skips=*/0, /*tolerated_failures=*/1})); - EXPECT_THAT(test_manager_.UnexpectedFailures(), IsEmpty()); - EXPECT_THAT(test_manager_.Finalize(), IsOk()); + EXPECT_THAT(ledger_.UnexpectedFailures(), IsEmpty()); + EXPECT_THAT(ledger_.Finalize(), IsOk()); } TEST_F(YieldsTest, P1FailureFailsWhenEnforced) { - test_manager_.set_enforcement_level(kP1); + ledger_.set_enforcement_level(kP1); - EXPECT_NONFATAL_FAILURE( + EXPECT_YIELDS_FAILURE( EXPECT_THAT(Run(kPayload2, TestPriority::kP1), Yields(IsParseError())), "Should have failed to parse, but didn't."); - EXPECT_EQ(GetCounts(test_manager_), (Counts{/*expected_successes=*/0, - /*expected_failures=*/0, - /*unexpected_successes=*/0, - /*unexpected_failures=*/1})); - EXPECT_THAT(test_manager_.UnexpectedFailures(), + EXPECT_EQ(ReplayedCounts(), (Counts{/*expected_successes=*/0, + /*expected_failures=*/0, + /*unexpected_successes=*/0, + /*unexpected_failures=*/1})); + EXPECT_THAT(ledger_.UnexpectedFailures(), ElementsAre(FieldsAre(kP1Foo, kNotAParseError, absl::nullopt))); } TEST_F(YieldsTest, ListedP1FailureIsTrackedEvenWhenNotEnforced) { // A kP1 test that's already in the failure list keeps being tracked // there, so that the list can't go stale unnoticed. - test_manager_.set_enforcement_level(kP0); + ledger_.set_enforcement_level(kP0); AddToFailureList(kP1Foo, kNotAParseError); EXPECT_THAT(Run(kPayload2, TestPriority::kP1), Yields(IsParseError())); - EXPECT_EQ(GetCounts(test_manager_), (Counts{/*expected_successes=*/0, - /*expected_failures=*/1})); - EXPECT_THAT(test_manager_.Finalize(), IsOk()); + EXPECT_EQ(ReplayedCounts(), (Counts{/*expected_successes=*/0, + /*expected_failures=*/1})); + EXPECT_THAT(ledger_.Finalize(), IsOk()); } TEST_F(YieldsTest, ListedP1FailureWithDifferentMessageFailsEvenWhenNotEnforced) { - test_manager_.set_enforcement_level(kP0); + ledger_.set_enforcement_level(kP0); AddToFailureList(kP1Foo, "Some other message"); - EXPECT_NONFATAL_FAILURE( + EXPECT_YIELDS_FAILURE( EXPECT_THAT(Run(kPayload2, TestPriority::kP1), Yields(IsParseError())), "Unexpected failure message for test"); - EXPECT_EQ(GetCounts(test_manager_), (Counts{/*expected_successes=*/0, - /*expected_failures=*/0, - /*unexpected_successes=*/0, - /*unexpected_failures=*/1})); + EXPECT_EQ(ReplayedCounts(), (Counts{/*expected_successes=*/0, + /*expected_failures=*/0, + /*unexpected_successes=*/0, + /*unexpected_failures=*/1})); } TEST_F(YieldsTest, P1SuccessPasses) { - test_manager_.set_enforcement_level(kP0); + ledger_.set_enforcement_level(kP0); EXPECT_THAT(Run(kParseError, TestPriority::kP1), Yields(IsParseError())); - EXPECT_EQ(GetCounts(test_manager_), (Counts{/*expected_successes=*/1})); + EXPECT_EQ(ReplayedCounts(), (Counts{/*expected_successes=*/1})); } TEST_F(YieldsTest, UnlistedSkipPasses) { @@ -1363,7 +1629,7 @@ // The inner matcher never gets to see a skipped result. bool inner_called = false; - auto matcher = Yields(Truly([&](const TestResult&) { + auto matcher = Yields(Truly([&](const YieldedResult&) { inner_called = true; return false; })); @@ -1371,20 +1637,50 @@ EXPECT_THAT(Explain(matcher, result), Accepts(IsEmpty())); EXPECT_FALSE(inner_called); - EXPECT_EQ(GetCounts(test_manager_), (Counts{/*expected_successes=*/0, - /*expected_failures=*/0, - /*unexpected_successes=*/0, - /*unexpected_failures=*/0, - /*skipped=*/1})); - EXPECT_THAT(test_manager_.Finalize(), IsOk()); + EXPECT_EQ(ReplayedCounts(), (Counts{/*expected_successes=*/0, + /*expected_failures=*/0, + /*unexpected_successes=*/0, + /*unexpected_failures=*/0, + /*skipped=*/1})); + EXPECT_THAT(ledger_.Finalize(), IsOk()); +} + +TEST_F(YieldsTest, NotSelectedByRunnerPassesSilently) { + // A filtering runner answers tests that weren't selected with this exact + // skip reason; that isn't a testee skip and must not be logged or counted. + AddToFailureList(kP0Foo, ""); + absl::ScopedMockLog log; + EXPECT_CALL(log, Log(_, _, HasSubstr(kP0Foo))).Times(0); + log.StartCapturingLogs(); + + bool inner_called = false; + auto matcher = Yields(Truly([&](const YieldedResult&) { + inner_called = true; + return false; + })); + const std::string response = R"pb(skipped: "Not selected by --test.")pb"; + TestResult result = Run(response); + + EXPECT_THAT(Explain(matcher, result), Accepts(IsEmpty())); + EXPECT_FALSE(inner_called); + EXPECT_EQ(ReplayedCounts(), Counts{}); + // Even a listed test is simply unseen, not "listed but skipped". Its entry + // does count as matched, though: the test exists, it just didn't run. + EXPECT_THAT(ledger_.Finalize().message(), + HasSubstr(absl::StrCat("were not seen: ", kP0Foo))); + EXPECT_THAT(ledger_.UnmatchedExpectedFailures(), IsEmpty()); + + // An unlisted one likewise. + EXPECT_THAT(Run(response, TestPriority::kP0, "other"), matcher); + EXPECT_EQ(ReplayedCounts(), Counts{}); } TEST_F(YieldsTest, ListedSkipFailsOnce) { // Deliberately stricter than the legacy runner, which passed this case: a // listed test the testee skips fails, but only here. The entry counts as - // seen (a skip says nothing about whether it is still needed), so Finalize() - // doesn't report it a second time and --fix keeps it. - ASSERT_THAT(test_manager_.LoadFailureList(WriteTempFile( + // seen, so Finalize() doesn't report it a second time, and --fix drops it + // like that of a test that passed. + ASSERT_THAT(ledger_.LoadFailureList(WriteTempFile( "failure_list.txt", absl::StrCat(kP0Foo, " # abc\n"))), IsOk()); auto matcher = Yields(IsParseError()); @@ -1397,30 +1693,34 @@ "test ", kP0Foo, " (matched to ", kP0Foo, ") is in the failure list but was skipped by the testee: " "not supported. Remove its match from the failure list."))); - EXPECT_NONFATAL_FAILURE(EXPECT_THAT(result, matcher), - "is in the failure list but was skipped"); - // The manager records it as a listed skip, not as an unexpected failure. - EXPECT_EQ(GetCounts(test_manager_), + EXPECT_YIELDS_FAILURE(EXPECT_THAT(result, matcher), + "is in the failure list but was skipped"); + // The ledger records it as a listed skip, not as an unexpected failure. + EXPECT_EQ(ReplayedCounts(), (Counts{/*expected_successes=*/0, /*expected_failures=*/0, /*unexpected_successes=*/0, /*unexpected_failures=*/0, /*skipped=*/1, /*listed_skips=*/1})); - EXPECT_THAT(test_manager_.ListedSkips(), ElementsAre(Pair(kP0Foo, kP0Foo))); - EXPECT_THAT(test_manager_.UnexpectedFailures(), IsEmpty()); - EXPECT_THAT(test_manager_.Finalize(), IsOk()); + EXPECT_THAT(ledger_.ListedSkips(), ElementsAre(Pair(kP0Foo, kP0Foo))); + EXPECT_THAT(ledger_.UnexpectedFailures(), IsEmpty()); + EXPECT_THAT(ledger_.Finalize(), IsOk()); const std::string fixed = WriteTempFile("fixed.txt", ""); - ASSERT_THAT(test_manager_.SaveFailureList(fixed), IsOk()); - EXPECT_EQ(ReadFile(fixed), absl::StrCat(kP0Foo, " # abc\n")); + ASSERT_THAT(ledger_.SaveFailureList(fixed), IsOk()); + EXPECT_EQ(ReadFile(fixed), ""); } TEST_F(YieldsTest, RuntimeErrorKeepsTheInnerMatchersFailureMessage) { // The legacy runner reports a runtime error where a parse error was expected // with a dedicated message, which the failure lists rely on. - EXPECT_NONFATAL_FAILURE( + EXPECT_YIELDS_FAILURE( EXPECT_THAT(Run(kRuntimeError), Yields(IsParseError())), "Should have failed to parse, but raised an error instead."); + EXPECT_EQ(ReplayedCounts(), (Counts{/*expected_successes=*/0, + /*expected_failures=*/0, + /*unexpected_successes=*/0, + /*unexpected_failures=*/1})); EXPECT_THAT( - test_manager_.UnexpectedFailures(), + ledger_.UnexpectedFailures(), ElementsAre(FieldsAre( kP0Foo, "Should have failed to parse, but raised an error instead.", absl::nullopt))); @@ -1431,9 +1731,9 @@ "Should have failed to parse, but raised an error instead."); EXPECT_THAT(Run(kRuntimeError), Yields(IsParseError())); - EXPECT_EQ(GetCounts(test_manager_), (Counts{/*expected_successes=*/0, - /*expected_failures=*/1})); - EXPECT_THAT(test_manager_.Finalize(), IsOk()); + EXPECT_EQ(ReplayedCounts(), (Counts{/*expected_successes=*/0, + /*expected_failures=*/1})); + EXPECT_THAT(ledger_.Finalize(), IsOk()); } TEST_F(YieldsTest, AnyOfMatchesEitherLeg) { @@ -1442,7 +1742,7 @@ EXPECT_THAT(Run(kParseError, TestPriority::kP0, "first"), matcher); EXPECT_THAT(Run(kPayload2, TestPriority::kP0, "second"), matcher); - EXPECT_EQ(GetCounts(test_manager_), (Counts{/*expected_successes=*/2})); + EXPECT_EQ(ReplayedCounts(), (Counts{/*expected_successes=*/2})); } TEST_F(YieldsTest, AnyOfFailsWhenNoLegMatches) { @@ -1456,17 +1756,17 @@ HasSubstr("Should have failed to parse, but didn't."), HasSubstr("Failed to parse input or produce output."), HasSubstr(absl::StrCat("Unexpected failure for test: ", kP0Foo))))); - EXPECT_NONFATAL_FAILURE(EXPECT_THAT(result, matcher), "Unexpected failure"); - EXPECT_EQ(GetCounts(test_manager_), (Counts{/*expected_successes=*/0, - /*expected_failures=*/0, - /*unexpected_successes=*/0, - /*unexpected_failures=*/1})); + EXPECT_YIELDS_FAILURE(EXPECT_THAT(result, matcher), "Unexpected failure"); + EXPECT_EQ(ReplayedCounts(), (Counts{/*expected_successes=*/0, + /*expected_failures=*/0, + /*unexpected_successes=*/0, + /*unexpected_failures=*/1})); } TEST_F(YieldsTest, NotComposes) { EXPECT_THAT(Run(kPayload2, TestPriority::kP0, "first"), Yields(Not(IsParseError()))); - EXPECT_EQ(GetCounts(test_manager_), (Counts{/*expected_successes=*/1})); + EXPECT_EQ(ReplayedCounts(), (Counts{/*expected_successes=*/1})); auto matcher = Yields(Not(IsParseError())); TestResult result = Run(kParseError, TestPriority::kP0, "second"); @@ -1476,83 +1776,435 @@ "which doesn't match (is not a parse error)", "\nUnexpected failure for test: ", "Required.Proto2.ProtobufInput.second.ProtobufOutput"))); - EXPECT_NONFATAL_FAILURE(EXPECT_THAT(result, matcher), - "which doesn't match (is not a parse error)"); - EXPECT_EQ(GetCounts(test_manager_), + EXPECT_YIELDS_FAILURE(EXPECT_THAT(result, matcher), + "which doesn't match (is not a parse error)"); + EXPECT_EQ(ReplayedCounts(), (Counts{/*expected_successes=*/1, /*expected_failures=*/0, /*unexpected_successes=*/0, /*unexpected_failures=*/1})); } -TEST_F(YieldsTest, CheckingAPassedResultAgainFails) { - // Whatever the second matcher is, including the very same object. +TEST_F(YieldsTest, CheckingAPassedResultAgainWithTheSameOutcomePasses) { + // Whatever the second matcher is, as long as it reaches the same outcome; + // the test is recorded and counted once. auto matcher = Yields(IsParseError()); TestResult result = Run(kParseError); EXPECT_THAT(result, matcher); - EXPECT_NONFATAL_FAILURE( - EXPECT_THAT(result, matcher), - "TestResult for Required.Proto2.ProtobufInput.foo.ProtobufOutput was " - "already checked; each result may be checked exactly once"); - EXPECT_NONFATAL_FAILURE(EXPECT_THAT(result, Yields(IsParseError())), - "was already checked"); - EXPECT_NONFATAL_FAILURE(EXPECT_THAT(result, Yields(Not(IsParseError()))), - "was already checked"); - EXPECT_EQ(GetCounts(test_manager_), (Counts{/*expected_successes=*/1})); + EXPECT_THAT(result, matcher); + EXPECT_THAT(result, Yields(IsParseError())); + EXPECT_THAT(result, Yields(Not(WhenParsed(_)))); + EXPECT_EQ(ReplayedCounts(), (Counts{/*expected_successes=*/1})); } -TEST_F(YieldsTest, CheckingAFailedResultAgainReplaysTheFailure) { +TEST_F(YieldsTest, CheckingAResultAgainWithADifferentOutcomeFails) { + TestResult result = Run(kParseError); + EXPECT_THAT(result, Yields(IsParseError())); + + // The first outcome stays recorded; the ledger never hears of the second. + EXPECT_YIELDS_FAILURE( + EXPECT_THAT(result, Yields(Not(IsParseError()))), + "TestResult for Required.Proto2.ProtobufInput.foo.ProtobufOutput was " + "already checked, with a different outcome: the first check recorded " + "\"P0 PASS\", this one would record \"P0 FAIL: which doesn't match (is " + "not a parse error)\"; each result may be checked once"); + EXPECT_EQ(ReplayedCounts(), (Counts{/*expected_successes=*/1})); + EXPECT_THAT(ledger_.UnexpectedFailures(), IsEmpty()); +} + +TEST_F(YieldsTest, CheckingAFailedResultAgainRepeatsTheFailure) { // gtest re-evaluates a failing matcher, through the same matcher object, to - // explain the failure. The matcher replays its own failed verdict, so the - // manager hears about the test only once. Any other matcher finds the - // result already checked. + // explain the failure. That evaluation reaches the same outcome, so it + // only repeats the verdict, and the ledger hears about the test once. So + // does any other matcher that reaches the same outcome; one that doesn't + // finds the result already checked. auto matcher = Yields(IsParseError()); TestResult result = Run(kPayload2); - EXPECT_NONFATAL_FAILURE(EXPECT_THAT(result, matcher), - "Should have failed to parse, but didn't."); + EXPECT_YIELDS_FAILURE(EXPECT_THAT(result, matcher), + "Should have failed to parse, but didn't."); EXPECT_THAT(Explain(matcher, result), Rejects(absl::StrCat(kNotAParseError, "\nUnexpected failure for test: ", kP0Foo))); EXPECT_THAT(Explain(Yields(IsParseError()), result), - Rejects(HasSubstr("was already checked"))); + Rejects(absl::StrCat(kNotAParseError, + "\nUnexpected failure for test: ", kP0Foo))); EXPECT_THAT(Explain(Yields(WhenParsed(_)), result), - Rejects(HasSubstr("was already checked"))); - EXPECT_EQ(GetCounts(test_manager_), (Counts{/*expected_successes=*/0, - /*expected_failures=*/0, - /*unexpected_successes=*/0, - /*unexpected_failures=*/1})); - EXPECT_THAT(test_manager_.UnexpectedFailures(), + Rejects(HasSubstr("was already checked, with a different " + "outcome: the first check recorded \"P0 FAIL: " + "Should have failed to parse, but didn't.\", " + "this one would record \"P0 PASS\""))); + EXPECT_EQ(ReplayedCounts(), (Counts{/*expected_successes=*/0, + /*expected_failures=*/0, + /*unexpected_successes=*/0, + /*unexpected_failures=*/1})); + EXPECT_THAT(ledger_.UnexpectedFailures(), ElementsAre(FieldsAre(kP0Foo, kNotAParseError, absl::nullopt))); } TEST_F(YieldsTest, ACopyOfACheckedResultIsAlreadyChecked) { TestResult original = Run(kPayload2); - EXPECT_NONFATAL_FAILURE(EXPECT_THAT(original, Yields(IsParseError())), - "Should have failed to parse, but didn't."); + EXPECT_YIELDS_FAILURE(EXPECT_THAT(original, Yields(IsParseError())), + "Should have failed to parse, but didn't."); - // The manager knows the test by name, so a copy of the result is checked - // too and the manager hears nothing more. + // The record is keyed by the test's name, so a copy of the result counts as + // checked too: the same outcome is repeated, a different one is refused, + // and the ledger hears nothing more. TestResult copy = original; EXPECT_THAT(Explain(Yields(IsParseError()), copy), + Rejects(HasSubstr("Unexpected failure for test"))); + EXPECT_THAT(Explain(Yields(WhenParsed(_)), copy), Rejects(HasSubstr("was already checked"))); - EXPECT_EQ(GetCounts(test_manager_), (Counts{/*expected_successes=*/0, - /*expected_failures=*/0, - /*unexpected_successes=*/0, - /*unexpected_failures=*/1})); + EXPECT_EQ(ReplayedCounts(), (Counts{/*expected_successes=*/0, + /*expected_failures=*/0, + /*unexpected_successes=*/0, + /*unexpected_failures=*/1})); } TEST_F(YieldsTest, SameMatcherCanCheckSeveralResults) { auto matcher = Yields(IsParseError()); EXPECT_THAT(Run(kParseError, TestPriority::kP0, "first"), matcher); EXPECT_THAT(Run(kParseError, TestPriority::kP0, "second"), matcher); - EXPECT_NONFATAL_FAILURE( + EXPECT_YIELDS_FAILURE( EXPECT_THAT(Run(kPayload2, TestPriority::kP0, "third"), matcher), "Should have failed to parse, but didn't."); - EXPECT_EQ(GetCounts(test_manager_), + EXPECT_EQ(ReplayedCounts(), (Counts{/*expected_successes=*/2, /*expected_failures=*/0, /*unexpected_successes=*/0, /*unexpected_failures=*/1})); } +// --------------------------------------------------------------------------- +// The results Yields() records +// --------------------------------------------------------------------------- + +// The outcomes recorded so far on the running gtest test, in order, as +// (test name, ResultRecord::ToString()) pairs. +std::vector<std::pair<std::string, std::string>> RecordedResults() { + return internal::RecordedResults(internal::CurrentGtestResult()); +} + +TEST_F(YieldsTest, RecordsAPass) { + EXPECT_THAT(Run(kPayload2), + Yields(WhenParsed(EqualsTextProto(R"pb(optional_int32: 2)pb")))); + EXPECT_THAT(RecordedResults(), ElementsAre(Pair(kP0Foo, "P0 PASS"))); +} + +TEST_F(YieldsTest, RecordsAFailWithTheLegacyMessage) { + TestResult result = Run(kPayload2); + EXPECT_YIELDS_FAILURE( + EXPECT_THAT(result, Yields(WhenParsed(EqualsTextProto(R"pb( + optional_int32: 1 + )pb")))), + "modified: optional_int32: 1 -> 2"); + EXPECT_THAT( + RecordedResults(), + ElementsAre(Pair(kP0Foo, + "P0 FAIL: Output was not equivalent to reference " + "message: modified: optional_int32: 1 -> 2"))); +} + +TEST_F(YieldsTest, RecordsAnExpectedFailureAsAFail) { + // The record says what the testee did, not whether it was expected. + AddToFailureList(kP0Foo, kNotAParseError); + EXPECT_THAT(Run(kPayload2), Yields(IsParseError())); + EXPECT_THAT( + RecordedResults(), + ElementsAre(Pair(kP0Foo, absl::StrCat("P0 FAIL: ", kNotAParseError)))); +} + +TEST_F(YieldsTest, RecordsAToleratedP1FailureAsAFail) { + ledger_.set_enforcement_level(kP0); + EXPECT_THAT(Run(kPayload2, TestPriority::kP1), Yields(IsParseError())); + EXPECT_THAT( + RecordedResults(), + ElementsAre(Pair(kP1Foo, absl::StrCat("P1 FAIL: ", kNotAParseError)))); +} + +TEST_F(YieldsTest, RecordsARuntimeErrorAsACrash) { + EXPECT_YIELDS_FAILURE(EXPECT_THAT(Run(kRuntimeError), Yields(WhenParsed(_))), + "Failed to parse input or produce output."); + EXPECT_THAT(RecordedResults(), + ElementsAre(Pair(kP0Foo, + "P0 CRASH: Failed to parse input or produce " + "output."))); +} + +TEST_F(YieldsTest, RecordsATimeoutAsACrash) { + EXPECT_YIELDS_FAILURE(EXPECT_THAT(Run(kTimeoutError), Yields(WhenParsed(_))), + "Failed to parse input or produce output."); + EXPECT_THAT(RecordedResults(), + ElementsAre(Pair(kP0Foo, + "P0 CRASH: Failed to parse input or produce " + "output."))); +} + +TEST_F(YieldsTest, RecordsAnEmptyResponseAsAFail) { + EXPECT_YIELDS_FAILURE(EXPECT_THAT(Run(""), Yields(WhenParsed(_))), + "Response didn't have any field in the Response."); + EXPECT_THAT(RecordedResults(), + ElementsAre(Pair(kP0Foo, + "P0 FAIL: Response didn't have any field in " + "the Response."))); +} + +TEST_F(YieldsTest, RecordsASkipWithTheReason) { + EXPECT_THAT(Run(kSkipped), Yields(_)); + EXPECT_THAT(RecordedResults(), + ElementsAre(Pair(kP0Foo, "P0 SKIP: not supported"))); +} + +TEST_F(YieldsTest, RecordsAListedSkipAsASkip) { + AddToFailureList(kP0Foo, ""); + EXPECT_YIELDS_FAILURE(EXPECT_THAT(Run(kSkipped), Yields(_)), + "is in the failure list but was skipped"); + EXPECT_THAT(RecordedResults(), + ElementsAre(Pair(kP0Foo, "P0 SKIP: not supported"))); +} + +TEST_F(YieldsTest, RecordsATestTheRunnerDidNotSelectAsNotSelected) { + const std::string response = R"pb(skipped: "Not selected by --test.")pb"; + EXPECT_THAT(Run(response), Yields(_)); + EXPECT_THAT(RecordedResults(), ElementsAre(Pair(kP0Foo, "P0 NOT_SELECTED"))); +} + +TEST_F(YieldsTest, RecordsOnceDespiteGtestRetrying) { + // gtest evaluates a failing matcher a second time to explain the failure, + // and a later check that reaches the same outcome repeats it. None of that + // records a second time. + auto matcher = Yields(IsParseError()); + TestResult result = Run(kPayload2); + EXPECT_YIELDS_FAILURE(EXPECT_THAT(result, matcher), + "Should have failed to parse, but didn't."); + EXPECT_YIELDS_FAILURE(EXPECT_THAT(result, matcher), + "Should have failed to parse, but didn't."); + EXPECT_YIELDS_FAILURE(EXPECT_THAT(result, Yields(IsParseError())), + "Should have failed to parse, but didn't."); + EXPECT_THAT( + RecordedResults(), + ElementsAre(Pair(kP0Foo, absl::StrCat("P0 FAIL: ", kNotAParseError)))); +} + +TEST_F(YieldsTest, RecordsEveryResult) { + auto matcher = + Yields(WhenParsed(EqualsTextProto(R"pb(optional_int32: 2)pb"))); + EXPECT_THAT(Run(kPayload2, TestPriority::kP0, "first"), matcher); + EXPECT_THAT(Run(kSkipped, TestPriority::kP1, "second"), matcher); + EXPECT_THAT( + RecordedResults(), + ElementsAre( + Pair("Required.Proto2.ProtobufInput.first.ProtobufOutput", "P0 PASS"), + Pair("Recommended.Proto2.ProtobufInput.second.ProtobufOutput", + "P1 SKIP: not supported"))); +} + +// --------------------------------------------------------------------------- +// JsonPayload() +// --------------------------------------------------------------------------- + +// A JSON-input, JSON-output result carrying the given JSON payload. +TestResult JsonResult(absl::string_view json) { + ConformanceResponse response; + response.set_json_payload(json); + return CreateResult("foo", /*input_format=*/::conformance::JSON, + /*output_format=*/::conformance::JSON, + std::move(response)); +} + +constexpr absl::string_view kJsonInt32 = R"json({"optionalInt32": 9})json"; + +TEST(JsonPayloadTest, Description) { + EXPECT_EQ(Describe(JsonPayload(HasJsonMember("optionalInt32"))), + R"(JSON payload has member "optionalInt32")"); + EXPECT_EQ(DescribeNegation(JsonPayload(HasJsonMember("optionalInt32"))), + R"(JSON payload doesn't have member "optionalInt32")"); + EXPECT_EQ(Describe(JsonPayload(_)), "JSON payload is anything"); +} + +TEST(JsonPayloadTest, Matches) { + TestResult result = JsonResult(kJsonInt32); + + EXPECT_THAT(Explain(JsonPayload(HasJsonMember("optionalInt32")), result), + Accepts(IsEmpty())); + YieldedResult yielded(result); + EXPECT_TRUE(Value(yielded, JsonPayload(_))); + EXPECT_TRUE(Value(yielded, JsonPayload(Truly([](const Json::Value& value) { + return value["optionalInt32"].asInt() == 9; + })))); + EXPECT_TRUE(Value(yielded, JsonPayload(SizeIs(1)))); +} + +TEST(JsonPayloadTest, InnerMatcherWithoutExplanation) { + // The description and the actual JSON text, quoted and escaped like + // RawPayload() does, are reported instead. + TestResult result = JsonResult(kJsonInt32); + + EXPECT_THAT(Explain(JsonPayload(HasJsonMember("optionalInt64")), result), + Rejects("Expect: JSON payload has member \"optionalInt64\", " + "but got: \"{\\\"optionalInt32\\\": 9}\"")); +} + +TEST(JsonPayloadTest, InnerMatcherExplanationIsUsed) { + TestResult result = JsonResult(kJsonInt32); + + EXPECT_THAT(Explain(JsonPayload(SizeIs(2)), result), + Rejects("whose size 1 doesn't match")); + // Truly() explains itself, tersely; this is what a lambda validator ported + // from the legacy suite records in the failure list. Only the gist is + // pinned, since the exact wording belongs to gMock. + EXPECT_THAT(Explain(JsonPayload(Truly([](const Json::Value& value) { + return value.isNull(); + })), + result), + Rejects(HasSubstr("didn't satisfy"))); +} + +TEST(JsonPayloadTest, InvalidJson) { + // The legacy validator's message, followed by jsoncpp's own diagnostic. + // The payload has to be valid JSON before any inner matcher gets a say. + TestResult result = JsonResult("{"); + + EXPECT_THAT( + Explain(JsonPayload(_), result), + Rejects(StartsWith("JSON payload cannot be parsed as valid JSON: "))); + EXPECT_THAT( + Explain(JsonPayload(_), JsonResult("")), + Rejects(StartsWith("JSON payload cannot be parsed as valid JSON: "))); + EXPECT_THAT( + Explain(JsonPayload(_), JsonResult("nonsense")), + Rejects(StartsWith("JSON payload cannot be parsed as valid JSON: "))); +} + +TEST(JsonPayloadTest, ParsesAnyJsonValueNotJustObjects) { + // Scalars and arrays are valid JSON documents to jsoncpp's reader (like the + // legacy validator); whether they are acceptable is up to the inner + // matcher. + TestResult null_result = JsonResult("null"); + EXPECT_TRUE(Value(YieldedResult(null_result), + JsonPayload(Truly([](const Json::Value& value) { + return value.isNull(); + })))); + TestResult array_result = JsonResult("[1, 2]"); + EXPECT_TRUE(Value(YieldedResult(array_result), JsonPayload(SizeIs(2)))); +} + +TEST(JsonPayloadTest, EmptyResponse) { + TestResult result = + CreateResult("foo", ::conformance::JSON, ConformanceResponse()); + + EXPECT_THAT(Explain(JsonPayload(_), result), + Rejects("Response didn't have any field in the Response.")); +} + +TEST_P(PayloadMatcherErrorResponseTest, IsAFailureForJsonPayload) { + TestResult result = CreateResult("foo", ::conformance::JSON, + ResponseFromText(GetParam().response)); + + EXPECT_THAT(Explain(JsonPayload(_), result), + Rejects("Failed to parse input or produce output.")); +} + +TEST(JsonPayloadTest, Skipped) { + TestResult result = + CreateResult("foo", ::conformance::JSON, + ResponseFromText(R"pb(skipped: "skipped message")pb")); + + EXPECT_THAT(Explain(JsonPayload(_), result), + Rejects("the testee skipped the test: skipped message")); +} + +TEST(JsonPayloadTest, WrongOutputFormat) { + // Same handling as the other payload matchers. + TestResult result = CreateResult("foo", ::conformance::JSON, + ProtobufPayload(VarintField(1, 9))); + + EXPECT_THAT( + Explain(JsonPayload(_), result), + Rejects("Test was asked for JSON output but provided PROTOBUF instead.")); + EXPECT_THAT( + Explain(JsonPayload(_), + CreateResult("bar", ::conformance::JSON, + ResponseFromText(R"pb(text_payload: "")pb"))), + Rejects( + "Test was asked for JSON output but provided TEXT_FORMAT instead.")); +} + +TEST(JsonPayloadTest, RequiresATestThatAskedForJsonOutput) { + // A test that asked for (and got) some other format can't be matched by + // JsonPayload() at all; that is a bug in the test. + TestResult result = CreateResult("foo", ::conformance::PROTOBUF, + ProtobufPayload(VarintField(1, 9))); + + EXPECT_THAT(Explain(JsonPayload(_), result), + Rejects("JsonPayload() needs JSON output, but the test asked " + "for PROTOBUF output.")); +} + +TEST(JsonPayloadTest, ComposesWithGMock) { + TestResult result = JsonResult(kJsonInt32); + + YieldedResult yielded(result); + EXPECT_TRUE(Value(yielded, Not(JsonPayload(HasJsonMember("optionalInt64"))))); + EXPECT_TRUE(Value( + yielded, + AnyOf(IsParseError(), JsonPayload(HasJsonMember("optionalInt32"))))); + EXPECT_TRUE(Value( + yielded, AllOf(JsonPayload(HasJsonMember("optionalInt32")), + WhenParsed(EqualsTextProto(R"pb(optional_int32: 9)pb"))))); +} + +TEST_F(YieldsTest, JsonOutput) { + // JSON-input, JSON-output tests through the whole stack: the legacy + // messages are what gets recorded for the failure list. + auto run_json = [&](absl::string_view response, absl::string_view name) { + RespondWith(response); + return testee_.CreateTest(name, TestPriority::kP0) + .ParseJson(TestAllTypesProto2::descriptor(), "{}") + .SerializeJson(); + }; + constexpr absl::string_view kJsonResponse = + R"pb(json_payload: "{\"optionalInt32\": 9}")pb"; + // Kept out of the EXPECT_YIELDS_FAILURE() arguments below: MSVC's + // traditional preprocessor mis-scans raw string literals containing \" or + // an odd number of quotes when they appear inside macro arguments. + constexpr absl::string_view kUnparseableJsonResponse = + R"pb(json_payload: "{\"nonsense\": 1}")pb"; + constexpr absl::string_view kInvalidJsonResponse = R"pb(json_payload: "{")pb"; + + EXPECT_THAT(run_json(kJsonResponse, "parsed"), + Yields(WhenParsed(EqualsTextProto(R"pb(optional_int32: 9)pb")))); + EXPECT_THAT(run_json(kJsonResponse, "validated"), + Yields(JsonPayload(HasJsonMember("optionalInt32")))); + EXPECT_EQ(ReplayedCounts(), (Counts{/*expected_successes=*/2})); + + EXPECT_YIELDS_FAILURE( + EXPECT_THAT(run_json(kUnparseableJsonResponse, "unparseable"), + Yields(WhenParsed(_))), + "JSON output we received from test was unparseable."); + EXPECT_YIELDS_FAILURE(EXPECT_THAT(run_json(kInvalidJsonResponse, "invalid"), + Yields(JsonPayload(_))), + "JSON payload cannot be parsed as valid JSON: "); + EXPECT_YIELDS_FAILURE( + EXPECT_THAT(run_json(kJsonResponse, "mismatch"), + Yields(JsonPayload(HasJsonMember("optionalInt64")))), + "Expect: JSON payload has member \"optionalInt64\", but got: " + "\"{\\\"optionalInt32\\\": 9}\""); + EXPECT_EQ(ReplayedCounts(), + (Counts{/*expected_successes=*/2, /*expected_failures=*/0, + /*unexpected_successes=*/0, /*unexpected_failures=*/3})); + EXPECT_THAT( + ledger_.UnexpectedFailures(), + ElementsAre( + FieldsAre("Required.Proto2.JsonInput.invalid.JsonOutput", + StartsWith("JSON payload cannot be parsed as valid JSON: "), + absl::nullopt), + FieldsAre("Required.Proto2.JsonInput.mismatch.JsonOutput", + "Expect: JSON payload has member \"optionalInt64\", but " + "got: \"{\\\"optionalInt32\\\": 9}\"", + absl::nullopt), + FieldsAre("Required.Proto2.JsonInput.unparseable.JsonOutput", + "JSON output we received from test was unparseable.", + absl::nullopt))); +} + } // namespace } // namespace conformance } // namespace protobuf
diff --git a/conformance/matchers_testing.cc b/conformance/matchers_testing.cc new file mode 100644 index 0000000..19f22f4 --- /dev/null +++ b/conformance/matchers_testing.cc
@@ -0,0 +1,89 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2026 Google LLC. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +#include "conformance/matchers_testing.h" + +#include <string> +#include <utility> +#include <vector> + +#include <gtest/gtest.h> +#include "absl/container/flat_hash_set.h" +#include "absl/status/statusor.h" +#include "absl/strings/match.h" +#include "absl/strings/string_view.h" +#include "absl/types/optional.h" +#include "conformance/result_record.h" + +namespace google { +namespace protobuf { +namespace conformance { +namespace internal { +namespace { + +// The record in `part`, if it is a gtest success that carries one. +absl::optional<RecordedResult> RecordIn(const testing::TestPartResult& part) { + if (part.type() != testing::TestPartResult::kSuccess) return absl::nullopt; + absl::StatusOr<RecordedResult> recorded = + ParseResultRecordMessage(part.message()); + if (!recorded.ok()) return absl::nullopt; + return *std::move(recorded); +} + +} // namespace + +std::vector<std::pair<std::string, std::string>> RecordedResults( + const testing::TestResult& result) { + std::vector<std::pair<std::string, std::string>> results; + for (int i = 0; i < result.total_part_count(); ++i) { + if (absl::optional<RecordedResult> recorded = + RecordIn(result.GetTestPartResult(i)); + recorded.has_value()) { + results.emplace_back(recorded->test_name, recorded->record.ToString()); + } + } + return results; +} + +void ExpectYieldsFailure(const testing::TestPartResultArray& results, + absl::string_view substr) { + int failures = 0; + // gtest evaluates a failing matcher twice, and the second evaluation can't + // see what the first one recorded while both are intercepted; record each + // outcome once. + absl::flat_hash_set<std::string> recorded; + for (int i = 0; i < results.size(); ++i) { + const testing::TestPartResult& part = results.GetTestPartResult(i); + if (part.type() == testing::TestPartResult::kSuccess) { + if (absl::optional<RecordedResult> record = RecordIn(part); + record.has_value() && + recorded + .insert(ResultRecordMessage(record->test_name, record->record)) + .second) { + SUCCEED() << ResultRecordMessage(record->test_name, record->record); + } + continue; + } + ++failures; + if (part.type() == testing::TestPartResult::kNonFatalFailure && + absl::StrContains(part.message(), substr)) { + continue; + } + ADD_FAILURE() << "Expected: 1 non-fatal failure containing \"" << substr + << "\"\n Actual:\n" + << part; + } + if (failures != 1) { + ADD_FAILURE() << "Expected: 1 non-fatal failure containing \"" << substr + << "\"\n Actual: " << failures << " failures"; + } +} + +} // namespace internal +} // namespace conformance +} // namespace protobuf +} // namespace google
diff --git a/conformance/matchers_testing.h b/conformance/matchers_testing.h new file mode 100644 index 0000000..6a0da0b --- /dev/null +++ b/conformance/matchers_testing.h
@@ -0,0 +1,64 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2026 Google LLC. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +// Helpers for unit tests of code that checks conformance results with +// Yields() (see matchers.h). + +#ifndef GOOGLE_PROTOBUF_CONFORMANCE_MATCHERS_TESTING_H__ +#define GOOGLE_PROTOBUF_CONFORMANCE_MATCHERS_TESTING_H__ + +#include <string> +#include <utility> +#include <vector> + +#include <gtest/gtest-spi.h> +#include <gtest/gtest.h> +#include "absl/strings/string_view.h" + +namespace google { +namespace protobuf { +namespace conformance { +namespace internal { + +// The outcomes Yields() recorded in `result` (see ResultRecordMessage() in +// result_record.h), in order, as (conformance test name, +// ResultRecord::ToString()) pairs. +std::vector<std::pair<std::string, std::string>> RecordedResults( + const testing::TestResult& result); + +// Implements EXPECT_YIELDS_FAILURE(): `results` is what the statement +// reported. +void ExpectYieldsFailure(const testing::TestPartResultArray& results, + absl::string_view substr); + +} // namespace internal +} // namespace conformance +} // namespace protobuf +} // namespace google + +// Like EXPECT_NONFATAL_FAILURE(statement, substr), for a statement that checks +// a conformance result with Yields(): `statement` must report exactly one +// non-fatal failure, whose message contains `substr`. EXPECT_NONFATAL_FAILURE +// would also intercept the gtest success Yields() records the outcome with +// (see ResultRecordMessage() in result_record.h) and complain about it. This +// records each outcome `statement` recorded on the running gtest test once it +// is done, once, so that the test environment still hears about the result. +#define EXPECT_YIELDS_FAILURE(statement, substr) \ + do { \ + ::testing::TestPartResultArray gtest_failures; \ + { \ + ::testing::ScopedFakeTestPartResultReporter gtest_reporter( \ + ::testing::ScopedFakeTestPartResultReporter:: \ + INTERCEPT_ONLY_CURRENT_THREAD, \ + >est_failures); \ + statement; \ + } \ + ::google::protobuf::conformance::internal::ExpectYieldsFailure(gtest_failures, \ + substr); \ + } while (false) + +#endif // GOOGLE_PROTOBUF_CONFORMANCE_MATCHERS_TESTING_H__
diff --git a/conformance/message_type_fixtures.cc b/conformance/message_type_fixtures.cc new file mode 100644 index 0000000..93498c9 --- /dev/null +++ b/conformance/message_type_fixtures.cc
@@ -0,0 +1,27 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2026 Google LLC. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +#include "conformance/message_type_fixtures.h" + +#include <string> + +#include <gtest/gtest.h> +#include "conformance/binary_test_util.h" +#include "google/protobuf/descriptor.h" + +namespace google { +namespace protobuf { +namespace conformance { + +std::string MessageTypeParamName( + const testing::TestParamInfo<const Descriptor*>& info) { + return ParamName(info.param); +} + +} // namespace conformance +} // namespace protobuf +} // namespace google
diff --git a/conformance/message_type_fixtures.h b/conformance/message_type_fixtures.h new file mode 100644 index 0000000..1f634be --- /dev/null +++ b/conformance/message_type_fixtures.h
@@ -0,0 +1,103 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2026 Google LLC. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +#ifndef GOOGLE_PROTOBUF_CONFORMANCE_MESSAGE_TYPE_FIXTURES_H__ +#define GOOGLE_PROTOBUF_CONFORMANCE_MESSAGE_TYPE_FIXTURES_H__ + +#include <string> +#include <tuple> + +#include <gtest/gtest.h> +#include "absl/strings/str_join.h" +#include "conformance/binary_test_util.h" +#include "conformance/test_fixture.h" +#include "conformance/test_protos/test_messages_edition2023.pb.h" +#include "google/protobuf/descriptor.h" + +// Fixtures for conformance tests that know which message type they exercise, +// and the INSTANTIATE_TEST_SUITE_P name generators that go with them. The +// fixtures report the message type through ConformanceTest::MessageUnderTest(). +// The base SetUp() then skips the test when --maximum_edition doesn't cover +// the message's edition, so test bodies never have to gate themselves. +// +// Suites parameterized over more than the message type, with a std::tuple +// whose first element is the `const Descriptor*`, derive from ConformanceTest +// and override MessageUnderTest() themselves (see +// binary_premature_eof_test.cc). + +namespace google { +namespace protobuf { +namespace conformance { + +// The fixture for conformance tests parameterized over the test message type +// only. Give each group its own suite name with a type alias. Instantiate it +// over AllTestMessageTypes() or Proto3TestMessageTypes() with +// MessageTypeParamName(): +// +// using FooTest = MessageTypeConformanceTest; +// +// TEST_P(FooTest, Bar) { +// EXPECT_THAT(Testee("Bar").ParseBinary(message(), ...).ParseOnly(), +// Yields(IsParseError())); +// } +// +// INSTANTIATE_TEST_SUITE_P(All, FooTest, ValuesIn(AllTestMessageTypes()), +// MessageTypeParamName); +class MessageTypeConformanceTest + : public ConformanceTest, + public testing::WithParamInterface<const Descriptor*> { + protected: + const Descriptor* message() const { return GetParam(); } + + private: + const Descriptor* MessageUnderTest() const override { return GetParam(); } +}; + +// The fixture for conformance tests that only exist for +// TestAllTypesEdition2023, such as extensions and delimited fields. Like +// MessageTypeConformanceTest, alias it per group: +// +// using DelimitedExtensionTest = Edition2023ConformanceTest; +class Edition2023ConformanceTest : public ConformanceTest { + protected: + static const Descriptor* message() { + return protobuf_test_messages::editions::TestAllTypesEdition2023:: + descriptor(); + } + + private: + const Descriptor* MessageUnderTest() const override { return message(); } +}; + +// The INSTANTIATE_TEST_SUITE_P name generator for suites parameterized over +// the message type only: ParamName() of the descriptor, e.g. "EditionsProto2". +std::string MessageTypeParamName( + const testing::TestParamInfo<const Descriptor*>& info); + +// The INSTANTIATE_TEST_SUITE_P name generator for suites parameterized over a +// std::tuple: ParamName() of every element, joined with "_", e.g. +// "Proto3_INT32" for a (const Descriptor*, FieldDescriptor::Type) tuple. Every +// element type needs a ParamName() overload (see binary_test_util.h). +// +// INSTANTIATE_TEST_SUITE_P(All, FooTest, +// Combine(ValuesIn(AllTestMessageTypes()), +// ValuesIn(kTypes)), +// TupleParamName<FooTest::ParamType>); +template <typename Tuple> +std::string TupleParamName(const testing::TestParamInfo<Tuple>& info) { + return std::apply( + [](const auto&... params) { + return absl::StrJoin({ParamName(params)...}, "_"); + }, + info.param); +} + +} // namespace conformance +} // namespace protobuf +} // namespace google + +#endif // GOOGLE_PROTOBUF_CONFORMANCE_MESSAGE_TYPE_FIXTURES_H__
diff --git a/conformance/message_type_fixtures_test.cc b/conformance/message_type_fixtures_test.cc new file mode 100644 index 0000000..b612888 --- /dev/null +++ b/conformance/message_type_fixtures_test.cc
@@ -0,0 +1,147 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2026 Google LLC. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +#include "conformance/message_type_fixtures.h" + +#include <cstddef> +#include <tuple> + +#include <gmock/gmock.h> +#include <gtest/gtest.h> +#include "absl/strings/match.h" +#include "absl/strings/string_view.h" +#include "absl/types/optional.h" +#include "conformance/binary_test_util.h" +#include "conformance/mock_test_runner.h" +#include "conformance/test_environment.h" +#include "conformance/test_environment_testing.h" +#include "google/protobuf/descriptor.h" +#include "editions/golden/test_messages_proto2_editions.pb.h" +#include "google/protobuf/test_messages_proto2.pb.h" +#include "google/protobuf/test_messages_proto3.pb.h" + +namespace google { +namespace protobuf { +namespace conformance { +namespace { + +using ::protobuf_test_messages::proto2::TestAllTypesProto2; +using ::protobuf_test_messages::proto3::TestAllTypesProto3; +using TestAllTypesProto2Editions = + ::protobuf_test_messages::editions::proto2::TestAllTypesProto2; +using ::testing::HasSubstr; +using ::testing::Not; +using ::testing::StrictMock; +using ::testing::TestParamInfo; +using ::testing::ValuesIn; + +// The fixtures under test consult the process-global ConformanceEnvironment in +// SetUp(), so this binary installs one for the duration of the run. It keeps +// the default maximum_edition (EDITION_PROTO3), under which every editions +// message is unsupported and must be skipped. No conformance test is ever run +// against the testee, so any call on the strict mock fails. +class GlobalEnvironment : public testing::Environment { + public: + void SetUp() override { + environment_.emplace( + internal::ConformanceEnvironmentOptions{/*runner=*/&runner_}); + } + void TearDown() override { environment_.reset(); } + + private: + StrictMock<MockTestRunner> runner_; + absl::optional<internal::ScopedGlobalConformanceEnvironment> environment_; +}; + +testing::Environment* const kGlobalEnvironment = + testing::AddGlobalTestEnvironment(new GlobalEnvironment); + +TEST(MessageTypeParamNameTest, IsParamNameOfTheDescriptor) { + size_t index = 0; + for (const Descriptor* message : AllTestMessageTypes()) { + EXPECT_EQ(MessageTypeParamName( + TestParamInfo<const Descriptor*>(message, index++)), + ParamName(message)); + } + EXPECT_EQ(MessageTypeParamName(TestParamInfo<const Descriptor*>( + TestAllTypesProto2Editions::descriptor(), 0)), + "EditionsProto2"); +} + +TEST(TupleParamNameTest, JoinsElementNamesWithUnderscore) { + using Param = std::tuple<const Descriptor*, FieldDescriptor::Type>; + EXPECT_EQ( + TupleParamName(TestParamInfo<Param>( + {TestAllTypesProto3::descriptor(), FieldDescriptor::TYPE_INT32}, 0)), + "Proto3_INT32"); + EXPECT_EQ(TupleParamName( + TestParamInfo<Param>({TestAllTypesProto2Editions::descriptor(), + FieldDescriptor::TYPE_MESSAGE}, + 1)), + "EditionsProto2_MESSAGE"); +} + +TEST(TupleParamNameTest, HandlesIntsAndSingleElementTuples) { + using Param = std::tuple<const Descriptor*, int, int, int>; + EXPECT_EQ(TupleParamName(TestParamInfo<Param>( + {TestAllTypesProto2::descriptor(), 6, 0, 3}, 0)), + "Proto2_6_0_3"); + EXPECT_EQ(TupleParamName(TestParamInfo<std::tuple<const Descriptor*>>( + {TestAllTypesProto3::descriptor()}, 0)), + "Proto3"); +} + +// Expects that every test of the current suite that ran was skipped iff +// `skipped_if(test name)` holds. For use from TearDownTestSuite(). +template <typename Predicate> +void ExpectSkippedIff(Predicate skipped_if) { + const testing::TestSuite& suite = + *testing::UnitTest::GetInstance()->current_test_suite(); + for (int i = 0; i < suite.total_test_count(); ++i) { + const testing::TestInfo& test = *suite.GetTestInfo(i); + if (!test.should_run()) continue; // Filtered out or sharded away. + EXPECT_EQ(test.result()->Skipped(), skipped_if(test.name())) << test.name(); + } +} + +// Instantiated over all four message types: SetUp() must skip the two editions +// instances (via MessageUnderTest()) before their bodies run, and let the +// proto2/proto3 ones through. +class MessageTypeSkipTest : public MessageTypeConformanceTest { + protected: + static void TearDownTestSuite() { + ExpectSkippedIff([](absl::string_view name) { + return absl::StrContains(name, "Editions"); + }); + } +}; + +TEST_P(MessageTypeSkipTest, BodyOnlyRunsForSupportedMessages) { + EXPECT_THAT(ParamName(message()), Not(HasSubstr("Editions"))); +} + +INSTANTIATE_TEST_SUITE_P(All, MessageTypeSkipTest, + ValuesIn(AllTestMessageTypes()), MessageTypeParamName); + +// TestAllTypesEdition2023 is never supported under EDITION_PROTO3, so SetUp() +// must skip every test of an Edition2023ConformanceTest suite. +class Edition2023SkipTest : public Edition2023ConformanceTest { + protected: + static void TearDownTestSuite() { + ExpectSkippedIff([](absl::string_view) { return true; }); + } +}; + +TEST_F(Edition2023SkipTest, BodyNeverRuns) { + FAIL() << "SetUp() must skip this test: " << message()->full_name() + << " isn't supported under --maximum_edition=PROTO3."; +} + +} // namespace +} // namespace conformance +} // namespace protobuf +} // namespace google
diff --git a/conformance/recursion_limit_payloads.cc b/conformance/recursion_limit_payloads.cc new file mode 100644 index 0000000..91bf0b6 --- /dev/null +++ b/conformance/recursion_limit_payloads.cc
@@ -0,0 +1,120 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2026 Google LLC. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +#include "conformance/recursion_limit_payloads.h" + +#include <cstddef> +#include <string> +#include <utility> +#include <vector> + +#include "absl/log/absl_check.h" +#include "absl/strings/str_cat.h" +#include "conformance/binary_wireformat.h" + +namespace google { +namespace protobuf { +namespace conformance { +namespace { + +// One nesting level of such a payload, seen from outside: the bytes before the +// level below it (the level's own fields, then the tag of the field that holds +// the next level; its length prefix is added by Nested()) and the bytes after +// it (e.g. the end tag of a group). The innermost level holds no next level. +struct Level { + Wire before; + Wire after; +}; + +// The serialization of `levels[0]`, which holds the serialization of +// `levels[1]` as a length-prefixed submessage, and so on down to +// `levels.back()`. Two passes and no recursion: the sizes inside out, the +// bytes outside in. +Wire Nested(const std::vector<Level>& levels) { + std::vector<size_t> sizes(levels.size()); + for (size_t i = levels.size(); i-- > 0;) { + sizes[i] = levels[i].before.size() + levels[i].after.size(); + if (i + 1 < levels.size()) { + sizes[i] += Varint(sizes[i + 1]).size() + sizes[i + 1]; + } + } + std::string bytes; + bytes.reserve(sizes.empty() ? 0 : sizes[0]); + for (size_t i = 0; i < levels.size(); ++i) { + absl::StrAppend(&bytes, levels[i].before.data()); + if (i + 1 < levels.size()) { + absl::StrAppend(&bytes, Varint(sizes[i + 1]).data()); + } + } + for (size_t i = levels.size(); i-- > 0;) { + absl::StrAppend(&bytes, levels[i].after.data()); + } + return Wire(std::move(bytes)); +} + +} // namespace + +Wire DeepMapPayload(int depth) { + ABSL_DCHECK_GE(depth, 1); + std::vector<Level> levels; + levels.push_back({Tag(301, WireType::kLengthPrefixed)}); + for (int i = 0; i < depth; ++i) { + // The map entry: C++ writes the key even when it is the default. + levels.push_back( + {Wire(VarintField(1, 0), Tag(2, WireType::kLengthPrefixed))}); + // The nested message. + Wire fields = VarintField(1, 123); + if (i + 1 < depth) { + fields = Wire(fields, Tag(301, WireType::kLengthPrefixed)); + } + levels.push_back({fields}); + } + return Nested(levels); +} + +Wire DeepMapStringKeyPayload(int depth) { + ABSL_DCHECK_GE(depth, 1); + std::vector<Level> levels; + levels.push_back({Tag(71, WireType::kLengthPrefixed)}); + for (int i = 0; i < depth; ++i) { + // The map entry: C++ writes the key even when it is empty. + levels.push_back( + {Wire(LengthPrefixedField(1, ""), Tag(2, WireType::kLengthPrefixed))}); + // The NestedMessage. + levels.push_back({Tag(2, WireType::kLengthPrefixed)}); + // The nested TestAllTypesEdition2023. + Wire fields = VarintField(1, 123); + if (i + 1 < depth) { + fields = Wire(fields, Tag(71, WireType::kLengthPrefixed)); + } + levels.push_back({fields}); + } + return Nested(levels); +} + +// A message set item is a group: start tag (1), type_id (2), message (3), +// end tag. +Wire DeepMessageSetPayload(int depth) { + ABSL_DCHECK_GE(depth, 0); + std::vector<Level> levels; + levels.push_back({Tag(500, WireType::kLengthPrefixed)}); + for (int i = 0; i <= depth; ++i) { + // The MessageSetCorrect and its item. + levels.push_back( + {Wire(Tag(1, WireType::kStartGroup), VarintField(2, 4135312), + Tag(3, WireType::kLengthPrefixed)), + Tag(1, WireType::kEndGroup)}); + // The MessageSetCorrectExtension2. + levels.push_back( + {i < depth ? Tag(10, WireType::kLengthPrefixed) : VarintField(9, 123)}); + } + return Nested(levels); +} + +} // namespace conformance +} // namespace protobuf +} // namespace google
diff --git a/conformance/recursion_limit_payloads.h b/conformance/recursion_limit_payloads.h new file mode 100644 index 0000000..fe78242 --- /dev/null +++ b/conformance/recursion_limit_payloads.h
@@ -0,0 +1,47 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2026 Google LLC. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +// The deeply nested payloads of the recursion-limit performance tests +// (binary_recursion_limit_test.cc). They are built directly on the wire +// rather than serialized from a message object of that depth. ByteSizeLong(), +// the serializer and the destructor recurse once per nesting level, which +// overflows the stack under the sanitizers' larger frames (b/563731788). The +// bytes are identical to what the message objects serialize, which +// recursion_limit_payloads_test.cc checks at a depth the stack can take. + +#ifndef GOOGLE_PROTOBUF_CONFORMANCE_RECURSION_LIMIT_PAYLOADS_H__ +#define GOOGLE_PROTOBUF_CONFORMANCE_RECURSION_LIMIT_PAYLOADS_H__ + +#include "conformance/binary_wireformat.h" + +namespace google { +namespace protobuf { +namespace conformance { + +// TestAllTypesEdition2023 whose map_recursive (301) entry {key 0, value} +// holds a TestAllTypesEdition2023 with optional_int32 (1) = 123 and, `depth` +// times in all, its own such entry. `depth` must be at least 1. +Wire DeepMapPayload(int depth); + +// TestAllTypesEdition2023 whose map_string_nested_message (71) entry {key "", +// value} holds a NestedMessage whose corecursive (2) is a +// TestAllTypesEdition2023 with optional_int32 (1) = 123 and, `depth` times in +// all, its own such entry. `depth` must be at least 1. +Wire DeepMapStringKeyPayload(int depth); + +// TestAllTypesProto2 whose message_set_correct (500) holds a MessageSetCorrect +// with one MessageSetCorrectExtension2 item (type id 4135312). The item's +// sub_msg (10) is again such a MessageSetCorrect, `depth` times. The +// innermost extension holds i (9) = 123 instead. With `depth` 0 it is the +// only one. +Wire DeepMessageSetPayload(int depth); + +} // namespace conformance +} // namespace protobuf +} // namespace google + +#endif // GOOGLE_PROTOBUF_CONFORMANCE_RECURSION_LIMIT_PAYLOADS_H__
diff --git a/conformance/recursion_limit_payloads_test.cc b/conformance/recursion_limit_payloads_test.cc new file mode 100644 index 0000000..9f22282 --- /dev/null +++ b/conformance/recursion_limit_payloads_test.cc
@@ -0,0 +1,84 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2026 Google LLC. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +#include "conformance/recursion_limit_payloads.h" + +#include <gtest/gtest.h> +#include "conformance/binary_wireformat.h" +#include "conformance/test_protos/test_messages_edition2023.pb.h" +#include "google/protobuf/test_messages_proto2.pb.h" + +namespace google { +namespace protobuf { +namespace conformance { +namespace { + +using ::protobuf_test_messages::editions::TestAllTypesEdition2023; +using ::protobuf_test_messages::proto2::TestAllTypesProto2; + +// The same payloads built from message objects the way the legacy suite built +// them, at depths the stack can take: the shallowest, and one deep enough for +// the length prefixes to need two bytes. +class RecursionLimitPayloadsTest : public ::testing::TestWithParam<int> {}; + +TEST_P(RecursionLimitPayloadsTest, DeepMapMatchesTheMessageObject) { + const int depth = GetParam(); + TestAllTypesEdition2023 message; + TestAllTypesEdition2023* sub = &message; + for (int i = 0; i < depth; i++) { + sub = &(*sub->mutable_map_recursive())[0]; + sub->set_optional_int32(123); + } + EXPECT_EQ(DeepMapPayload(depth), Wire(message.SerializeAsString())); +} + +TEST_P(RecursionLimitPayloadsTest, DeepMapStringKeyMatchesTheMessageObject) { + const int depth = GetParam(); + TestAllTypesEdition2023 message; + TestAllTypesEdition2023* sub = &message; + for (int i = 0; i < depth; i++) { + sub = (*sub->mutable_map_string_nested_message())[""].mutable_corecursive(); + sub->set_optional_int32(123); + } + EXPECT_EQ(DeepMapStringKeyPayload(depth), Wire(message.SerializeAsString())); +} + +TEST_P(RecursionLimitPayloadsTest, DeepMessageSetMatchesTheMessageObject) { + const int depth = GetParam(); + TestAllTypesProto2 message; + TestAllTypesProto2::MessageSetCorrect* sub = + message.mutable_message_set_correct(); + for (int i = 0; i < depth; i++) { + sub = + sub->MutableExtension(TestAllTypesProto2::MessageSetCorrectExtension2:: + message_set_extension) + ->mutable_sub_msg(); + } + sub->MutableExtension( + TestAllTypesProto2::MessageSetCorrectExtension2::message_set_extension) + ->set_i(123); + EXPECT_EQ(DeepMessageSetPayload(depth), Wire(message.SerializeAsString())); +} + +INSTANTIATE_TEST_SUITE_P(Depths, RecursionLimitPayloadsTest, + ::testing::Values(1, 3, 50)); + +// A message set of depth 0 is a single extension holding i. (A plain TEST, so +// it can't share the parameterized fixture's suite name.) +TEST(RecursionLimitPayloadsDepthZeroTest, MessageSet) { + TestAllTypesProto2 message; + message.mutable_message_set_correct() + ->MutableExtension(TestAllTypesProto2::MessageSetCorrectExtension2:: + message_set_extension) + ->set_i(123); + EXPECT_EQ(DeepMessageSetPayload(0), Wire(message.SerializeAsString())); +} + +} // namespace +} // namespace conformance +} // namespace protobuf +} // namespace google
diff --git a/conformance/result_ledger.cc b/conformance/result_ledger.cc new file mode 100644 index 0000000..c25e341 --- /dev/null +++ b/conformance/result_ledger.cc
@@ -0,0 +1,515 @@ +#include "conformance/result_ledger.h" + +#include <algorithm> +#include <cstddef> +#include <fstream> +#include <ios> +#include <string> +#include <utility> +#include <vector> + +#include "absl/algorithm/container.h" +#include "absl/container/btree_map.h" +#include "absl/container/flat_hash_set.h" +#include "absl/log/absl_check.h" +#include "absl/log/absl_log.h" +#include "absl/status/status.h" +#include "absl/strings/ascii.h" +#include "absl/strings/match.h" +#include "absl/strings/str_cat.h" +#include "absl/strings/str_join.h" +#include "absl/strings/string_view.h" +#include "absl/types/optional.h" +#include "conformance/failure_list.h" +#include "conformance/result_record.h" +#include "conformance/testee.h" + +namespace google { +namespace protobuf { +namespace conformance { +namespace internal { +namespace { + +bool InsertUniqueTest(absl::flat_hash_set<std::string>& set, + absl::string_view value) { + if (!set.emplace(value).second) { + return false; + } + return true; +} + +void IncrementIfUnique(bool unique, int& counter) { + if (unique) { + ++counter; + } +} + +// Formats an entry line for SaveFailureList(): the test name padded to +// `alignment`, then " # " and the message, or the name alone if the line has +// no message. A comment or blank line is only stripped. +std::string ReformatLine(size_t alignment, absl::string_view line) { + size_t comment_pos = line.find('#'); + absl::string_view test_name = + absl::StripAsciiWhitespace(line.substr(0, comment_pos)); + if (test_name.empty()) { + return std::string(absl::StripAsciiWhitespace(line)); + } + absl::string_view message; + if (comment_pos != absl::string_view::npos) { + message = absl::StripAsciiWhitespace(line.substr(comment_pos + 1)); + } + if (message.empty()) { + return std::string(test_name); + } + ABSL_CHECK_GE(alignment, test_name.length()); + std::string whitespace(alignment - test_name.length(), ' '); + return absl::StrCat(test_name, whitespace, " # ", message); +} + +// The failure list line for `test_name` failing with `message`, which may be +// empty; ReformatLine() aligns it. +std::string EntryLine(absl::string_view test_name, absl::string_view message) { + if (message.empty()) return std::string(test_name); + return absl::StrCat(test_name, " # ", message); +} + +// The test name (or wildcard) of a failure list line, i.e. what precedes its +// comment, with all whitespace removed: the key entries are loaded and looked +// up by, so that LoadFailureList() and AppendSavedLines() agree on it. +std::string EntryKeyOf(absl::string_view line) { + std::string key(line.substr(0, line.find('#'))); + key.erase(std::remove_if(key.begin(), key.end(), absl::ascii_isspace), + key.end()); + return key; +} + +// The test name of a failure list line as written (an entry as listed, see +// EntryKeyOf() for the key it is looked up by), or empty for a comment or +// blank line. +absl::string_view EntryOf(absl::string_view line) { + return absl::StripAsciiWhitespace(line.substr(0, line.find('#'))); +} + +// The copybara markers that delimit a region the open source export drops. +// They are spelled in two pieces so that the export's scrubber, which +// processes every file, doesn't take these literals for markers. +constexpr absl::string_view kStripBeginMarker = + "copybara:" + "strip_begin"; +constexpr absl::string_view kStripEndMarker = + "copybara:" + "strip_end"; + +// Whether `line` is a comment whose text starts with `marker`, e.g. a +// `# <kStripBeginMarker>` line (copybara allows text after the marker). +bool IsCommentStartingWith(absl::string_view line, absl::string_view marker) { + line = absl::StripAsciiWhitespace(line); + if (!absl::ConsumePrefix(&line, "#")) return false; + return absl::StartsWith(absl::StripLeadingAsciiWhitespace(line), marker); +} + +bool IsStripBegin(absl::string_view line) { + return IsCommentStartingWith(line, kStripBeginMarker); +} + +// Also matches the `strip_end_and_replace_begin` marker, which closes the +// block as well. +bool IsStripEnd(absl::string_view line) { + return IsCommentStartingWith(line, kStripEndMarker); +} + +// A line SaveFailureList() writes, with the alignment scope it belongs to: 0 +// for the lines outside copybara strip blocks, k > 0 for the lines of the k-th +// block (its marker lines included). +struct SavedLine { + std::string text; + size_t scope; +}; + +} // namespace + +ResultLedger::~ResultLedger() { + if (!finalized_) { + ABSL_LOG(FATAL) + << "ResultLedger::Finalize() was not called before destruction."; + } +} + +absl::Status ResultLedger::LoadFailureList(absl::string_view filename) { + std::ifstream infile(std::string{filename}); + + if (!infile.is_open()) { + return absl::InternalError( + absl::StrCat("Couldn't open failure list file: ", filename)); + } + + for (std::string line; std::getline(infile, line);) { + failure_list_lines_.push_back(line); + + std::string test_name = EntryKeyOf(line); + + if (test_name.empty()) { // Skip empty lines and comments. + continue; + } + + // Find our failure message if it exists. Will be set to an empty string + // if no message is found. Empty failure messages also pass our tests. + size_t comment_pos = line.find('#'); + std::string message; + if (comment_pos != std::string::npos) { + message = line.substr(comment_pos + 1); // +1 to skip the delimiter + // If we had only whitespace after the delimiter, we will have an empty + // failure message and the test will still pass. + message = std::string(absl::StripAsciiWhitespace(message)); + } + if (absl::Status status = AddExpectedFailure(test_name, message); + !status.ok()) { + return status; + } + } + + return absl::OkStatus(); +} + +absl::Status ResultLedger::AddExpectedFailure( + absl::string_view test_name, absl::string_view failure_message) { + if (absl::Status status = failure_list_.Add(test_name, failure_message); + !status.ok()) { + return status; + } + ABSL_CHECK(unseen_expected_failures_.emplace(test_name).second); + ABSL_CHECK(unmatched_expected_failures_.emplace(test_name).second); + return absl::OkStatus(); +} + +absl::Status ResultLedger::SaveFailureList(absl::string_view filename) const { + std::ofstream outfile(std::string{filename}, std::ios::binary); + if (!outfile.is_open()) { + return absl::InternalError(absl::StrCat( + "Couldn't open failure list file for writing: ", filename)); + } + + // The loaded lines with unseen entries dropped and changed entries rewritten + // (see AppendSavedLines()), each tagged with its alignment scope; the new + // failures are merged in below. A copybara strip block runs from a + // strip_begin line to the next strip_end line; a strip_begin with no + // strip_end after it, a strip_end outside a block and a strip_begin inside + // one are ordinary comments, as they are to LoadFailureList(). + std::vector<SavedLine> lines; + size_t blocks = 0; + size_t scope = 0; + for (size_t i = 0; i < failure_list_lines_.size(); ++i) { + const std::string& line = failure_list_lines_[i]; + if (scope == 0 && IsStripBegin(line) && + std::any_of( + failure_list_lines_.begin() + static_cast<std::ptrdiff_t>(i), + failure_list_lines_.end(), IsStripEnd)) { + scope = ++blocks; + } + std::vector<std::string> saved; + AppendSavedLines(line, saved); + for (std::string& text : saved) { + lines.push_back({std::move(text), scope}); + } + if (scope != 0 && IsStripEnd(line)) { + scope = 0; + } + } + + // Calculate the alignment of each scope: the longest name among its lines + // and, outside the blocks, the new failures. + std::vector<size_t> alignment(blocks + 1, 0); + for (const SavedLine& line : lines) { + alignment[line.scope] = + std::max(alignment[line.scope], EntryOf(line.text).length()); + } + for (const auto& failure : new_failures_) { + alignment[0] = std::max(alignment[0], failure.first.length()); + } + + // Output the lines, inserting the new failures by name among the lines + // outside the blocks. + auto to_add = new_failures_.begin(); + for (const SavedLine& line : lines) { + if (line.scope == 0) { + absl::string_view test_name = EntryOf(line.text); + while (to_add != new_failures_.end() && test_name > to_add->first) { + outfile << ReformatLine(alignment[0], + EntryLine(to_add->first, to_add->second)) + << "\n"; + ++to_add; + } + } + outfile << ReformatLine(alignment[line.scope], line.text) << "\n"; + } + + // Add any remaining new failures. + while (to_add != new_failures_.end()) { + outfile << ReformatLine(alignment[0], + EntryLine(to_add->first, to_add->second)) + << "\n"; + ++to_add; + } + + outfile.close(); + if (!outfile.good()) { + return absl::InternalError( + absl::StrCat("Failed to write failure list file: ", filename)); + } + return absl::OkStatus(); +} + +void ResultLedger::AppendSavedLines(absl::string_view line, + std::vector<std::string>& lines) const { + const std::string entry = EntryKeyOf(line); + if (entry.empty()) { + // A comment or blank line. + lines.emplace_back(line); + return; + } + + auto it = entry_matches_.find(entry); + if (it == entry_matches_.end()) { + // No test matched the entry with a verdict: it is unseen and goes. + return; + } + const absl::btree_map<std::string, MatchOutcome>& matches = it->second; + + // Kept verbatim unless a matched test succeeded, was skipped or failed with + // a different message. + const bool changed = absl::c_any_of(matches, [](const auto& match) { + return match.second.kind != MatchOutcome::kExpectedFailure; + }); + if (!changed) { + lines.emplace_back(line); + return; + } + + // Kept as the entry (a wildcard included) with the new message if every + // matched test failed with that same message. + const MatchOutcome& first = matches.begin()->second; + const bool same_new_message = + first.kind == MatchOutcome::kOtherFailure && + absl::c_all_of(matches, [&](const auto& match) { + return match.second.kind == MatchOutcome::kOtherFailure && + match.second.message == first.message; + }); + if (same_new_message) { + lines.push_back(EntryLine(entry, first.message)); + return; + } + + // Otherwise the matched set diverged: one line per matched test that still + // fails (which for an exact entry that succeeded or was skipped is none). + const std::string expected_message = *failure_list_.ExpectedMessage(entry); + for (const auto& [test_name, outcome] : matches) { + switch (outcome.kind) { + case MatchOutcome::kSuccess: + case MatchOutcome::kSkip: + break; + case MatchOutcome::kOtherFailure: + lines.push_back(EntryLine(test_name, outcome.message)); + break; + case MatchOutcome::kExpectedFailure: + lines.push_back(EntryLine(test_name, expected_message)); + break; + } + } +} + +absl::Status ResultLedger::ReportSuccess(absl::string_view test_name) { + bool unique = InsertUniqueTest(seen_tests_, test_name); + absl::optional<std::string> failure_match = MarkMatched(test_name); + + if (failure_match.has_value()) { + // This was expected to fail, but it succeeded. + IncrementIfUnique(unique, number_of_matches_[*failure_match]); + IncrementIfUnique(unique, unexpected_successes_); + if (unique) { + unexpected_success_matches_[test_name] = *failure_match; + } + unseen_expected_failures_.erase(*failure_match); + entry_matches_[*failure_match][test_name] = { + /*kind=*/MatchOutcome::kSuccess}; + } else { + // This wasn't expected to fail. + IncrementIfUnique(unique, expected_successes_); + } + return failure_list_.VerdictOnSuccess(test_name); +} + +absl::Status ResultLedger::ReportFailure(absl::string_view test_name, + TestPriority priority, + absl::string_view failure_message) { + bool unique = InsertUniqueTest(seen_tests_, test_name); + absl::optional<std::string> failure_match = MarkMatched(test_name); + + std::string formatted_failure_message = FormatFailureMessage(failure_message); + // Counts the failure as unexpected and records it for UnexpectedFailures(). + auto record_unexpected_failure = [&] { + IncrementIfUnique(unique, unexpected_failures_); + if (unique) { + unexpected_failure_messages_[test_name] = formatted_failure_message; + } + }; + + if (!failure_match.has_value()) { + if (priority > failure_list_.enforcement_level()) { + // Tolerated: neither a failure nor a skip, and not written to the + // failure list; only counted. + IncrementIfUnique(unique, tolerated_failures_); + } else { + // This was not expected to fail. + record_unexpected_failure(); + new_failures_[test_name] = formatted_failure_message; + } + return failure_list_.VerdictOnFailure(test_name, priority, failure_message); + } + + // The entry is seen either way: Finalize() is about entries no test + // reached, and a message mismatch is already reported by the verdict, by + // UnexpectedFailures() and by SaveFailureList(). + unseen_expected_failures_.erase(*failure_match); + if (!absl::StartsWith(formatted_failure_message, + *failure_list_.ExpectedMessage(*failure_match))) { + record_unexpected_failure(); + entry_matches_[*failure_match][test_name] = { + /*kind=*/MatchOutcome::kOtherFailure, + /*message=*/formatted_failure_message}; + } else { + entry_matches_[*failure_match][test_name] = { + /*kind=*/MatchOutcome::kExpectedFailure}; + IncrementIfUnique(unique, number_of_matches_[*failure_match]); + IncrementIfUnique(unique, expected_failures_); + } + return failure_list_.VerdictOnFailure(test_name, priority, failure_message); +} + +absl::Status ResultLedger::ReportSkip(absl::string_view test_name, + absl::string_view skip_reason) { + bool unique = InsertUniqueTest(seen_tests_, test_name); + IncrementIfUnique(unique, skipped_); + absl::optional<std::string> failure_match = MarkMatched(test_name); + if (!failure_match.has_value()) { + return absl::OkStatus(); + } + + // A skipped test is a listed test that didn't fail, so its match is not an + // unseen entry for Finalize() but is dropped by SaveFailureList(), like a + // success (the verdict asks for the same by hand). It doesn't count toward + // the entry's wildcard expansion cap (number_of_matches_) either, unlike in + // the legacy runner, which counted every test a wildcard matched: the cap + // is about real outcomes hidden by a wildcard, and a skip isn't one. + unseen_expected_failures_.erase(*failure_match); + entry_matches_[*failure_match][test_name] = {/*kind=*/MatchOutcome::kSkip}; + IncrementIfUnique(unique, listed_skips_); + if (unique) { + listed_skip_matches_[test_name] = *failure_match; + } + return failure_list_.VerdictOnSkip(test_name, skip_reason); +} + +void ResultLedger::ReportNotSelected(absl::string_view test_name) { + seen_tests_.emplace(test_name); + MarkMatched(test_name); +} + +void ResultLedger::Report(absl::string_view test_name, + const ResultRecord& record) { + switch (record.status) { + case ResultRecord::Status::kPass: + ReportSuccess(test_name).IgnoreError(); + break; + case ResultRecord::Status::kFail: + case ResultRecord::Status::kCrash: + ReportFailure(test_name, record.priority, record.message).IgnoreError(); + break; + case ResultRecord::Status::kSkip: + ReportSkip(test_name, record.message).IgnoreError(); + break; + case ResultRecord::Status::kNotSelected: + ReportNotSelected(test_name); + break; + } +} + +absl::optional<std::string> ResultLedger::MarkMatched( + absl::string_view test_name) { + absl::optional<std::string> failure_match = + failure_list_.MatchingEntry(test_name); + if (failure_match.has_value()) { + unmatched_expected_failures_.erase(*failure_match); + } + return failure_match; +} + +std::vector<std::string> ResultLedger::OverexpandedWildcards() const { + std::vector<std::string> entries; + for (const auto& [entry, matches] : number_of_matches_) { + if (matches > kMaximumWildcardExpansions) entries.push_back(entry); + } + absl::c_sort(entries); + return entries; +} + +std::vector<std::string> ResultLedger::UnseenExpectedFailures() const { + std::vector<std::string> unseen(unseen_expected_failures_.begin(), + unseen_expected_failures_.end()); + absl::c_sort(unseen); + return unseen; +} + +std::vector<std::string> ResultLedger::UnmatchedExpectedFailures() const { + std::vector<std::string> unmatched(unmatched_expected_failures_.begin(), + unmatched_expected_failures_.end()); + absl::c_sort(unmatched); + return unmatched; +} + +std::vector<UnexpectedResult> ResultLedger::UnexpectedFailures() const { + std::vector<UnexpectedResult> failures; + failures.reserve(unexpected_failure_messages_.size()); + // The map is ordered by test name. + for (const auto& [test_name, failure_message] : + unexpected_failure_messages_) { + failures.push_back( + {/*test_name=*/test_name, /*failure_message=*/failure_message}); + } + return failures; +} + +std::vector<UnexpectedResult> ResultLedger::UnexpectedSuccesses() const { + std::vector<UnexpectedResult> successes; + successes.reserve(unexpected_success_matches_.size()); + // The map is ordered by test name. + for (const auto& [test_name, matched_entry] : unexpected_success_matches_) { + successes.push_back( + {/*test_name=*/test_name, + /*failure_message=*/*failure_list_.ExpectedMessage(matched_entry), + /*matched_entry=*/matched_entry}); + } + return successes; +} + +std::vector<std::pair<std::string, std::string>> ResultLedger::ListedSkips() + const { + // The map is ordered by test name. + return std::vector<std::pair<std::string, std::string>>( + listed_skip_matches_.begin(), listed_skip_matches_.end()); +} + +absl::Status ResultLedger::Finalize() { + finalized_ = true; + std::vector<std::string> unseen = UnseenExpectedFailures(); + if (!unseen.empty()) { + return absl::FailedPreconditionError( + absl::StrCat("The following expected failures were not seen: ", + absl::StrJoin(unseen, ", "))); + } + return absl::OkStatus(); +} + +} // namespace internal +} // namespace conformance +} // namespace protobuf +} // namespace google
diff --git a/conformance/result_ledger.h b/conformance/result_ledger.h new file mode 100644 index 0000000..45cecc5 --- /dev/null +++ b/conformance/result_ledger.h
@@ -0,0 +1,306 @@ +#ifndef GOOGLE_PROTOBUF_CONFORMANCE_RESULT_LEDGER_H__ +#define GOOGLE_PROTOBUF_CONFORMANCE_RESULT_LEDGER_H__ + +#include <string> +#include <utility> +#include <vector> + +#include "absl/container/btree_map.h" +#include "absl/container/flat_hash_map.h" +#include "absl/container/flat_hash_set.h" +#include "absl/status/status.h" +#include "absl/strings/string_view.h" +#include "absl/types/optional.h" +#include "conformance/failure_list.h" +#include "conformance/result_record.h" +#include "conformance/testee.h" + +namespace google { +namespace protobuf { +namespace conformance { +namespace internal { + +// A test whose result contradicted the failure list. See +// ResultLedger::UnexpectedFailures() and ResultLedger::UnexpectedSuccesses(). +// TODO: Remove with conformance_test_runner. +struct UnexpectedResult { + // The full name of the test, as reported to the ResultLedger. + std::string test_name; + // For an unexpected failure, the test's failure message as + // ResultLedger::SaveFailureList() would write it. For an unexpected success, + // the message of the failure list entry that matched the test. + std::string failure_message; + // For an unexpected success, the failure list entry that matched + // `test_name`, possibly a wildcard. Unset for an unexpected failure, which + // matched no entry or matched one with a different message. + absl::optional<std::string> matched_entry; +}; + +// How many tests one wildcard entry may cover with a verdict, that is an +// expected failure or an unexpected success. See OverexpandedWildcards(). +inline constexpr int kMaximumWildcardExpansions = 20; + +// The ledger of a conformance run: tallies the results of its tests against +// its failure list. It knows nothing of gtest; the test environment's +// ResultListener (result_listener.h) feeds it. The failure list is loaded +// once (see LoadFailureList()) into a FailureList, which decides each result: +// Yields() (matchers.h) asks it for the verdict and records the outcome, and +// the listener reports the outcome here (see Report()) and reads the counters +// back as test properties. The environment owns the ledger and, under --fix, +// writes the new failure list from it (see SaveFailureList()). +class ResultLedger { + public: + ResultLedger() = default; + ~ResultLedger(); + + // The lowest priority whose failures fail the suite (see TestPriority in + // testee.h). ReportFailure() tolerates a failing test of a lower priority + // unless it is in the failure list. Defaults to kLowestPriority, which + // enforces every priority. + void set_enforcement_level(TestPriority level) { + failure_list_.set_enforcement_level(level); + } + + // Loads a failure list from disk and adds its entries to the ones loaded so + // far. Each line that isn't blank or a comment names one expected failure, + // optionally followed by `#` and the expected failure message. Returns an + // error if the file can't be opened. Also returns an error if an entry is + // already present, including one from an earlier file, or contains an + // invalid wildcard. + absl::Status LoadFailureList(absl::string_view filename); + + // The failure list as loaded so far, which decides each result. Yields() + // reads the global environment's through GetGlobalFailureList(). + const FailureList& failure_list() const { return failure_list_; } + + // Saves an updated failure list to disk based on the reported results. + // Comment and blank lines stay where they are. Entries are updated as + // follows: + // + // - An entry that no test matched with a verdict is dropped. These are + // the entries Finalize() reports as unseen. + // - An entry whose matched tests all still fail with its message is kept + // verbatim, wildcards included. + // - An entry whose matched tests all failed with the same different + // message is kept with that message instead, wildcards included. + // - Any other entry is rewritten in place, one line per matched test in + // name order. A test that succeeded or was skipped (see ReportSkip()) + // is left out. A test that failed with a different message gets that + // message. A test that still fails with the entry's message keeps it. + // An exact entry therefore goes when its test succeeded or was skipped. + // A wildcard entry is expanded into the tests it matched, so that the + // tests that still fail stay listed. + // - Tests that failed but matched no entry are inserted in name order + // with their messages. + // + // Entry lines are aligned to the longest name. + // + // A copybara strip block (`# copybara` `:strip_begin` ... `:strip_end`) is + // the region the open source export drops. It is kept as one contiguous + // region and aligned on its own. Its entries are handled as above and stay + // inside it. No new failure is ever inserted into it. A new failure goes + // in front of the first entry outside any block that sorts after it, or at + // the end of the file. One that sorts between the last entry before a + // block and the first entry after it lands right after the block's + // strip_end. Unbalanced markers are ordinary comments, as they are to + // LoadFailureList(). + // + // Returns an error if `filename` can't be opened for writing or the write + // fails. + absl::Status SaveFailureList(absl::string_view filename) const; + + // Records a successful test run and returns + // FailureList::VerdictOnSuccess(). + absl::Status ReportSuccess(absl::string_view test_name); + + // Records a failed test run along with the failure message and returns + // FailureList::VerdictOnFailure(). A tolerated failure is only counted + // (see tolerated_failures()). An unexpected one is also kept for + // UnexpectedFailures() and SaveFailureList(). + absl::Status ReportFailure(absl::string_view test_name, TestPriority priority, + absl::string_view failure_message); + + // Records a test that the testee skipped for `skip_reason` and returns + // FailureList::VerdictOnSkip(). If the test matched a failure list entry, + // the entry counts as seen and matched, so Finalize() doesn't report it as + // unseen, and SaveFailureList() drops the match like a success. A listed + // test that is skipped is also recorded for ListedSkips(). + absl::Status ReportSkip(absl::string_view test_name, + absl::string_view skip_reason); + + // Records a test the runner didn't run because it wasn't selected (see + // ResultRecord::Status::kNotSelected). + // The legacy runner matched a test name against the failure list before + // checking whether the test was selected. Like it, this only marks the + // entry the name matches, if any, as matched for UnmatchedExpectedFailures(). + // The test is not counted by any statistic and its entry stays unseen. + // TODO: Remove with conformance_test_runner. + void ReportNotSelected(absl::string_view test_name); + + // Records `record`, the outcome Yields() recorded for `test_name`, through + // the Report*() method for its status. The verdict was applied when the + // result was checked, so it is dropped here. + void Report(absl::string_view test_name, const ResultRecord& record); + + // Whether any of the Report*() methods has been called for `test_name`. + // The ResultListener checks this before it reports a recorded outcome (see + // Report()), so that a test is counted once. + bool WasReported(absl::string_view test_name) const { + return seen_tests_.contains(test_name); + } + + // The sorted failure list entries that more than kMaximumWildcardExpansions + // reported tests matched with a verdict, that is as an expected failure or + // an unexpected success. Skips don't count. Such a wildcard hides too + // much; the test environment fails the run over it. + std::vector<std::string> OverexpandedWildcards() const; + + // Runs sanity checks over the failure list to make sure everything we + // expected to run was reported. Returns an error naming the sorted expected + // failure entries that have not been reported as a failure, an unexpected + // success or a skip. Must be called before destruction. + absl::Status Finalize(); + + // Returns the sorted expected failure entries that no reported test name has + // matched so far, regardless of the outcome. Names are reported through + // ReportSuccess(), ReportFailure(), ReportSkip() or ReportNotSelected(). A + // wildcard entry counts as matched once it has matched any test name. + // Unlike the unseen entries Finalize() reports, an entry whose test was not + // selected is not returned. It is a real test, only not run here. + // TODO: Remove with conformance_test_runner. + std::vector<std::string> UnmatchedExpectedFailures() const; + + // Returns the tests reported as unexpected failures so far, sorted by test + // name. These are the tests unexpected_failures() counts. Each result's + // `failure_message` is the message as SaveFailureList() would write it, and + // its `matched_entry` is unset. This includes tests that were expected to + // fail, but with a different message. SaveFailureList() rewrites their + // entry, so adding these lines to the failure list would duplicate it. + // TODO: Remove with conformance_test_runner. + std::vector<UnexpectedResult> UnexpectedFailures() const; + + // Returns the tests reported as unexpected successes so far, sorted by test + // name. These are the tests unexpected_successes() counts. Each result's + // `matched_entry` is the failure list entry the test matched, and + // `failure_message` is that entry's message. + // TODO: Remove with conformance_test_runner. + std::vector<UnexpectedResult> UnexpectedSuccesses() const; + + // Returns the tests the testee skipped although they are in the failure + // list, sorted by test name. These are the tests listed_skips() counts. + // Each pair is the test name and the failure list entry it matched. Such a + // test fails the gtest run: the matchers in matchers.h fail it with the + // error ReportSkip() returns. It is not an unexpected failure or success. + // SaveFailureList() under --fix drops its match like a passing test's, that + // is the whole entry, or the test's own line when a wildcard entry is + // expanded. + // TODO: Remove with conformance_test_runner. + std::vector<std::pair<std::string, std::string>> ListedSkips() const; + + // The number of tests that were reported skipped. This includes the listed + // skips. + int skipped() const { return skipped_; } + + // The number of skipped tests that are in the failure list. See + // ListedSkips(). + int listed_skips() const { return listed_skips_; } + + // The number of tests that failed but were tolerated because their priority + // is above the enforcement level. + int tolerated_failures() const { return tolerated_failures_; } + + // The number of tests that were in the failure list and failed with the + // expected message. + int expected_failures() const { return expected_failures_; } + + // The number of tests that failed but were not in the failure list, or + // failed with a different message than the one listed. + int unexpected_failures() const { return unexpected_failures_; } + + // The number of tests that were not in the failure list and succeeded. + int expected_successes() const { return expected_successes_; } + + // The number of tests that were in the failure list but succeeded. + int unexpected_successes() const { return unexpected_successes_; } + + private: + // Adds one failure list entry, as LoadFailureList() does for each line. + // Returns an error if the test name is already present or contains an + // invalid wildcard. + absl::Status AddExpectedFailure(absl::string_view test_name, + absl::string_view failure_message); + + // The sorted expected failure entries Finalize() complains about: those not + // reported as a failure, an unexpected success or a skip so far. + std::vector<std::string> UnseenExpectedFailures() const; + + // Marks the failure list entry `test_name` matches, if any, as matched (see + // UnmatchedExpectedFailures()) and returns it. + absl::optional<std::string> MarkMatched(absl::string_view test_name); + + // The verdict on a test that matched a failure list entry, as far as + // SaveFailureList() is concerned. + struct MatchOutcome { + enum Kind { + kExpectedFailure, // Failed with the entry's message. + kOtherFailure, // Failed with a different message (see `message`). + kSuccess, // Succeeded (see ReportSuccess()). + kSkip, // Was skipped by the testee (see ReportSkip()). + }; + Kind kind; + // The formatted failure message, for kOtherFailure. + std::string message; + }; + + // Appends to `lines` what SaveFailureList() writes for `line`, a line of the + // failure list as loaded. That is the line itself, nothing, or the entry's + // rewritten lines, possibly expanded. See SaveFailureList(). + void AppendSavedLines(absl::string_view line, + std::vector<std::string>& lines) const; + + FailureList failure_list_; + + absl::flat_hash_set<std::string> unseen_expected_failures_; + // Entries never matched by name by any Report*() call. + absl::flat_hash_set<std::string> unmatched_expected_failures_; + // The tests that matched each entry with a verdict, by test name, and how + // they came out. Entries are as listed, wildcards included. + // ReportNotSelected() is not a verdict and doesn't record anything here. + absl::flat_hash_map<std::string, absl::btree_map<std::string, MatchOutcome>> + entry_matches_; + // How many tests matched each entry as an expected failure or an unexpected + // success. See OverexpandedWildcards(). + absl::flat_hash_map<std::string, int> number_of_matches_; + + // Every test name reported so far (see WasReported()), so that a test + // reported more than once is only counted once. + absl::flat_hash_set<std::string> seen_tests_; + + // The tests counted by unexpected_failures_, mapped to their formatted + // failure message, and the tests counted by unexpected_successes_, mapped to + // the failure list entry they matched. + absl::btree_map<std::string, std::string> unexpected_failure_messages_; + absl::btree_map<std::string, std::string> unexpected_success_matches_; + // The tests counted by listed_skips_, mapped to the entry they matched. + absl::btree_map<std::string, std::string> listed_skip_matches_; + + std::vector<std::string> failure_list_lines_; + // The tests that failed but matched no entry, with their formatted messages. + absl::btree_map<std::string, std::string> new_failures_; + + int skipped_ = 0; + int listed_skips_ = 0; + int tolerated_failures_ = 0; + int expected_failures_ = 0; + int unexpected_failures_ = 0; + int expected_successes_ = 0; + int unexpected_successes_ = 0; + bool finalized_ = false; +}; + +} // namespace internal +} // namespace conformance +} // namespace protobuf +} // namespace google + +#endif // GOOGLE_PROTOBUF_CONFORMANCE_RESULT_LEDGER_H__
diff --git a/conformance/result_ledger_test.cc b/conformance/result_ledger_test.cc new file mode 100644 index 0000000..09b5bbe --- /dev/null +++ b/conformance/result_ledger_test.cc
@@ -0,0 +1,1916 @@ +#include "conformance/result_ledger.h" + +#include <initializer_list> +#include <string> + +#include "google/protobuf/testing/file.h" +#include "google/protobuf/testing/file.h" +#include "google/protobuf/testing/file.h" +#include <gmock/gmock.h> +#include "google/protobuf/testing/googletest.h" +#include <gtest/gtest.h> +#include "absl/log/absl_check.h" +#include "absl/status/status.h" +#include "absl/status/status_matchers.h" +#include "absl/strings/str_cat.h" +#include "absl/strings/string_view.h" +#include "absl/strings/substitute.h" +#include "absl/types/optional.h" +#include "conformance/failure_list.h" +#include "conformance/result_record.h" +#include "conformance/testee.h" + +namespace google { +namespace protobuf { +namespace conformance { +namespace internal { +namespace { + +using ::absl_testing::IsOk; +using ::absl_testing::StatusIs; +using ::testing::AllOf; +using ::testing::ElementsAre; +using ::testing::Eq; +using ::testing::FieldsAre; +using ::testing::HasSubstr; +using ::testing::IsEmpty; +using ::testing::Not; +using ::testing::Optional; +using ::testing::Pair; +using ::testing::SizeIs; + +class ResultLedgerTest : public ::testing::Test { + protected: + ResultLedgerTest() + : tmp_dir_(absl::StrCat(TestTempDir(), "/result_ledger_test/")), + failure_list_path_(absl::StrCat( + tmp_dir_, + testing::UnitTest::GetInstance()->current_test_info()->name())) { + if (!File::Exists(tmp_dir_)) { + ABSL_CHECK_OK( + File::RecursivelyCreateDir(tmp_dir_, 0777)); + } + } + + struct Failure { + absl::string_view name; + absl::string_view message; + + Failure( // NOLINT(google-explicit-constructor) + absl::string_view name) + : name(name), message("") {} + Failure(absl::string_view name, absl::string_view message) + : name(name), message(message) {} + }; + + void CreateFailureList( + std::initializer_list<const Failure> expected_failures) { + std::string content; + for (auto failure : expected_failures) { + absl::StrAppend(&content, failure.name, " # ", failure.message, "\n"); + } + ABSL_CHECK_OK( + File::SetContents(failure_list_path_, content, true)); + } + + void CreateFailureList(absl::string_view content) { + ABSL_CHECK_OK( + File::SetContents(failure_list_path_, content, true)); + } + + absl::string_view failure_list() const { return failure_list_path_; } + + ~ResultLedgerTest() override { + File::DeleteRecursively(failure_list_path_, NULL, NULL); + } + + private: + std::string tmp_dir_; + std::string failure_list_path_; + std::string output_path_; +}; + +TEST_F(ResultLedgerTest, RequiresFinalize) { + EXPECT_DEATH({ ResultLedger ledger; }, "Finalize"); +} + +TEST_F(ResultLedgerTest, ReportSuccess) { + CreateFailureList({}); + ResultLedger ledger; + ASSERT_THAT(ledger.LoadFailureList(failure_list()), IsOk()); + + EXPECT_THAT(ledger.ReportSuccess("foo"), IsOk()); + + EXPECT_THAT(ledger.Finalize(), IsOk()); + EXPECT_EQ(ledger.expected_failures(), 0); + EXPECT_EQ(ledger.unexpected_failures(), 0); + EXPECT_EQ(ledger.expected_successes(), 1); + EXPECT_EQ(ledger.unexpected_successes(), 0); + EXPECT_EQ(ledger.skipped(), 0); +} + +TEST_F(ResultLedgerTest, ReportSkipped) { + CreateFailureList({}); + ResultLedger ledger; + ASSERT_THAT(ledger.LoadFailureList(failure_list()), IsOk()); + + EXPECT_THAT(ledger.ReportSkip("foo", "reason"), IsOk()); + + EXPECT_THAT(ledger.Finalize(), IsOk()); + EXPECT_EQ(ledger.expected_failures(), 0); + EXPECT_EQ(ledger.unexpected_failures(), 0); + EXPECT_EQ(ledger.expected_successes(), 0); + EXPECT_EQ(ledger.unexpected_successes(), 0); + EXPECT_EQ(ledger.skipped(), 1); + EXPECT_EQ(ledger.listed_skips(), 0); + EXPECT_THAT(ledger.ListedSkips(), IsEmpty()); +} + +TEST_F(ResultLedgerTest, ReportSkipMarksListedEntrySeenAndMatched) { + CreateFailureList({{"foo", "abc"}, {"bar.*", "abc"}, {"baz", "abc"}}); + ResultLedger ledger; + ASSERT_THAT(ledger.LoadFailureList(failure_list()), IsOk()); + + // A listed skip is reported like an unexpected success, naming the entry. + EXPECT_THAT(ledger.ReportSkip("foo", "not supported"), + StatusIs(absl::StatusCode::kFailedPrecondition, + "test foo (matched to foo) is in the failure list but " + "was skipped by the testee: not supported. Remove its " + "match from the failure list.")); + EXPECT_THAT(ledger.ReportSkip("bar.x", "no"), + StatusIs(absl::StatusCode::kFailedPrecondition, + "test bar.x (matched to bar.*) is in the failure list " + "but was skipped by the testee: no. Remove its match " + "from the failure list.")); + // Reporting a test twice returns the error again but counts it once. + EXPECT_THAT(ledger.ReportSkip("bar.x", "no"), Not(IsOk())); + + // A skip is only counted as a skip, but the entries it matched are neither + // unmatched nor unseen: dropping the match is left to SaveFailureList(). + EXPECT_THAT(ledger.UnmatchedExpectedFailures(), ElementsAre("baz")); + EXPECT_THAT(ledger.Finalize(), StatusIs(absl::StatusCode::kFailedPrecondition, + HasSubstr("were not seen: baz"))); + EXPECT_EQ(ledger.skipped(), 2); + EXPECT_EQ(ledger.listed_skips(), 2); + EXPECT_THAT(ledger.ListedSkips(), + ElementsAre(Pair("bar.x", "bar.*"), Pair("foo", "foo"))); + EXPECT_EQ(ledger.expected_failures(), 0); + EXPECT_EQ(ledger.unexpected_failures(), 0); + EXPECT_EQ(ledger.expected_successes(), 0); + EXPECT_EQ(ledger.unexpected_successes(), 0); + EXPECT_THAT(ledger.UnexpectedFailures(), IsEmpty()); + EXPECT_THAT(ledger.UnexpectedSuccesses(), IsEmpty()); +} + +TEST_F(ResultLedgerTest, ReportNotSelected) { + CreateFailureList({{"foo"}, {"bar.*"}, {"baz"}}); + ResultLedger ledger; + ASSERT_THAT(ledger.LoadFailureList(failure_list()), IsOk()); + + ledger.ReportNotSelected("foo"); + ledger.ReportNotSelected("bar.x"); + ledger.ReportNotSelected("unlisted"); + + // Only the matched entries stop being unmatched; nothing else changes. + EXPECT_THAT(ledger.UnmatchedExpectedFailures(), ElementsAre("baz")); + EXPECT_THAT(ledger.Finalize(), + StatusIs(absl::StatusCode::kFailedPrecondition, + "The following expected failures were not seen: bar.*, " + "baz, foo")); + EXPECT_EQ(ledger.expected_failures(), 0); + EXPECT_EQ(ledger.unexpected_failures(), 0); + EXPECT_EQ(ledger.expected_successes(), 0); + EXPECT_EQ(ledger.unexpected_successes(), 0); + EXPECT_EQ(ledger.skipped(), 0); + EXPECT_EQ(ledger.tolerated_failures(), 0); +} + +TEST_F(ResultLedgerTest, WasReported) { + CreateFailureList({}); + ResultLedger ledger; + ASSERT_THAT(ledger.LoadFailureList(failure_list()), IsOk()); + EXPECT_FALSE(ledger.WasReported("success")); + + EXPECT_THAT(ledger.ReportSuccess("success"), IsOk()); + EXPECT_THAT(ledger.ReportFailure("failure", kP0, "abc"), Not(IsOk())); + EXPECT_THAT(ledger.ReportSkip("skip", "reason"), IsOk()); + ledger.ReportNotSelected("not_selected"); + + EXPECT_TRUE(ledger.WasReported("success")); + EXPECT_TRUE(ledger.WasReported("failure")); + EXPECT_TRUE(ledger.WasReported("skip")); + EXPECT_TRUE(ledger.WasReported("not_selected")); + EXPECT_FALSE(ledger.WasReported("other")); + EXPECT_THAT(ledger.Finalize(), IsOk()); +} + +TEST_F(ResultLedgerTest, FailureListVerdictsRecordNothing) { + CreateFailureList({{"foo", "abc"}, {"bar.*", "abc"}, {"baz", "abc"}}); + ResultLedger ledger; + ASSERT_THAT(ledger.LoadFailureList(failure_list()), IsOk()); + ledger.set_enforcement_level(kP0); + + const FailureList& failure_list = ledger.failure_list(); + EXPECT_THAT(failure_list.VerdictOnSuccess("unlisted"), IsOk()); + EXPECT_THAT(failure_list.VerdictOnSuccess("foo"), + StatusIs(absl::StatusCode::kFailedPrecondition, + "test foo (matched to foo) is in the failure list, but " + "test succeeded. Remove its match from the failure " + "list.")); + EXPECT_THAT(failure_list.VerdictOnFailure("foo", kP0, "abc"), IsOk()); + EXPECT_THAT(failure_list.VerdictOnFailure("foo", kP0, "xyz"), + StatusIs(absl::StatusCode::kFailedPrecondition, + "Unexpected failure message for test: foo expected: " + "abc actual: xyz")); + EXPECT_THAT(failure_list.VerdictOnFailure("bar.x", kP1, "abc"), IsOk()); + EXPECT_THAT(failure_list.VerdictOnFailure("unlisted", kP0, "xyz"), + StatusIs(absl::StatusCode::kFailedPrecondition, + "Unexpected failure for test: unlisted")); + EXPECT_THAT(failure_list.VerdictOnFailure("unlisted", kP1, "xyz"), IsOk()); + EXPECT_THAT(failure_list.VerdictOnSkip("unlisted", "reason"), IsOk()); + EXPECT_THAT(failure_list.VerdictOnSkip("bar.x", "reason"), + StatusIs(absl::StatusCode::kFailedPrecondition, + "test bar.x (matched to bar.*) is in the failure list " + "but was skipped by the testee: reason. Remove its " + "match from the failure list.")); + + // Nothing was reported. + EXPECT_FALSE(ledger.WasReported("foo")); + EXPECT_FALSE(ledger.WasReported("unlisted")); + EXPECT_THAT(ledger.UnmatchedExpectedFailures(), + ElementsAre("bar.*", "baz", "foo")); + EXPECT_THAT(ledger.Finalize(), + StatusIs(absl::StatusCode::kFailedPrecondition, + HasSubstr("were not seen: bar.*, baz, foo"))); + EXPECT_EQ(ledger.expected_failures(), 0); + EXPECT_EQ(ledger.unexpected_failures(), 0); + EXPECT_EQ(ledger.expected_successes(), 0); + EXPECT_EQ(ledger.unexpected_successes(), 0); + EXPECT_EQ(ledger.skipped(), 0); + EXPECT_EQ(ledger.tolerated_failures(), 0); +} + +TEST_F(ResultLedgerTest, FailureListLookupsRecordNothing) { + CreateFailureList({{"foo", "abc"}, {"bar.*", "abc"}}); + ResultLedger ledger; + ASSERT_THAT(ledger.LoadFailureList(failure_list()), IsOk()); + + const FailureList& failure_list = ledger.failure_list(); + EXPECT_THAT(failure_list.MatchingEntry("foo"), Optional(std::string("foo"))); + EXPECT_THAT(failure_list.MatchingEntry("bar.x"), + Optional(std::string("bar.*"))); + EXPECT_EQ(failure_list.MatchingEntry("foo.x"), absl::nullopt); + EXPECT_EQ(failure_list.MatchingEntry("unlisted"), absl::nullopt); + EXPECT_THAT(failure_list.ExpectedMessage("foo"), + Optional(std::string("abc"))); + EXPECT_THAT(failure_list.ExpectedMessage("bar.*"), + Optional(std::string("abc"))); + // Entries are looked up as listed, not matched. + EXPECT_EQ(failure_list.ExpectedMessage("bar.x"), absl::nullopt); + + // Looking up a name doesn't match its entry. + EXPECT_THAT(ledger.UnmatchedExpectedFailures(), ElementsAre("bar.*", "foo")); + ledger.Finalize().IgnoreError(); +} + +TEST_F(ResultLedgerTest, ReportRecordDispatchesOnTheStatus) { + CreateFailureList({{"fail", "abc"}, + {"crash", "abc"}, + {"skip", "abc"}, + {"not_selected", "abc"}}); + ResultLedger ledger; + ASSERT_THAT(ledger.LoadFailureList(failure_list()), IsOk()); + + ledger.Report("pass", {kP0, ResultRecord::Status::kPass, ""}); + ledger.Report("fail", {kP0, ResultRecord::Status::kFail, "abc"}); + ledger.Report("crash", {kP0, ResultRecord::Status::kCrash, "xyz"}); + ledger.Report("skip", {kP0, ResultRecord::Status::kSkip, "reason"}); + ledger.Report("not_selected", {kP0, ResultRecord::Status::kNotSelected, ""}); + // The verdict is dropped: a tolerated failure is recorded like any other. + ledger.set_enforcement_level(kP0); + ledger.Report("tolerated", {kP1, ResultRecord::Status::kFail, "xyz"}); + + EXPECT_TRUE(ledger.WasReported("pass")); + EXPECT_TRUE(ledger.WasReported("not_selected")); + EXPECT_EQ(ledger.expected_successes(), 1); + EXPECT_EQ(ledger.expected_failures(), 1); + EXPECT_THAT(ledger.UnexpectedFailures(), + ElementsAre(FieldsAre("crash", "xyz", absl::nullopt))); + EXPECT_EQ(ledger.unexpected_failures(), 1); + EXPECT_THAT(ledger.ListedSkips(), ElementsAre(Pair("skip", "skip"))); + EXPECT_EQ(ledger.skipped(), 1); + EXPECT_EQ(ledger.tolerated_failures(), 1); + EXPECT_EQ(ledger.unexpected_successes(), 0); + // A test the runner didn't select leaves its entry unseen. + EXPECT_THAT(ledger.Finalize(), StatusIs(absl::StatusCode::kFailedPrecondition, + HasSubstr("not_selected"))); +} + +TEST_F(ResultLedgerTest, ReportExpectedFailure) { + CreateFailureList({{"foo", "abc"}}); + ResultLedger ledger; + ASSERT_THAT(ledger.LoadFailureList(failure_list()), IsOk()); + + EXPECT_THAT(ledger.ReportFailure("foo", kP0, "abc"), IsOk()); + EXPECT_THAT(ledger.Finalize(), IsOk()); + + EXPECT_EQ(ledger.expected_failures(), 1); + EXPECT_EQ(ledger.unexpected_failures(), 0); + EXPECT_EQ(ledger.expected_successes(), 0); + EXPECT_EQ(ledger.unexpected_successes(), 0); + EXPECT_EQ(ledger.skipped(), 0); +} + +TEST_F(ResultLedgerTest, ReportDuplicates) { + CreateFailureList({{"foo", "abc"}}); + ResultLedger ledger; + ASSERT_THAT(ledger.LoadFailureList(failure_list()), IsOk()); + + ASSERT_THAT(ledger.ReportSuccess("bar"), IsOk()); + ASSERT_THAT(ledger.ReportSuccess("bar"), IsOk()); + ASSERT_THAT(ledger.ReportSkip("baz", "reason"), IsOk()); + ASSERT_THAT(ledger.ReportSkip("baz", "reason"), IsOk()); + ASSERT_THAT(ledger.ReportFailure("foo", kP0, "abc"), IsOk()); + ASSERT_THAT(ledger.ReportFailure("foo", kP0, "abc"), IsOk()); + ASSERT_THAT(ledger.Finalize(), IsOk()); + + EXPECT_EQ(ledger.expected_failures(), 1); + EXPECT_EQ(ledger.unexpected_failures(), 0); + EXPECT_EQ(ledger.expected_successes(), 1); + EXPECT_EQ(ledger.unexpected_successes(), 0); + EXPECT_EQ(ledger.skipped(), 1); +} + +TEST_F(ResultLedgerTest, ReportExpectedFailureWildcard) { + CreateFailureList({{"foo.*.bar", "abc"}}); + ResultLedger ledger; + ASSERT_THAT(ledger.LoadFailureList(failure_list()), IsOk()); + + EXPECT_THAT(ledger.ReportFailure("foo.baz.bar", kP0, "abc"), IsOk()); + EXPECT_THAT(ledger.Finalize(), IsOk()); + + EXPECT_EQ(ledger.expected_failures(), 1); + EXPECT_EQ(ledger.unexpected_failures(), 0); + EXPECT_EQ(ledger.expected_successes(), 0); + EXPECT_EQ(ledger.unexpected_successes(), 0); + EXPECT_EQ(ledger.skipped(), 0); +} + +TEST_F(ResultLedgerTest, ReportUnseenFailure) { + CreateFailureList({{"foo.bar"}}); + ResultLedger ledger; + ASSERT_THAT(ledger.LoadFailureList(failure_list()), IsOk()); + + EXPECT_THAT(ledger.Finalize(), StatusIs(absl::StatusCode::kFailedPrecondition, + AllOf(HasSubstr("were not seen"), + HasSubstr("foo.bar")))); + + EXPECT_EQ(ledger.expected_failures(), 0); + EXPECT_EQ(ledger.unexpected_failures(), 0); + EXPECT_EQ(ledger.expected_successes(), 0); + EXPECT_EQ(ledger.unexpected_successes(), 0); + EXPECT_EQ(ledger.skipped(), 0); +} + +TEST_F(ResultLedgerTest, ReportUnseenFailureUnrelatedSkip) { + CreateFailureList({{"foo.bar"}}); + ResultLedger ledger; + ASSERT_THAT(ledger.LoadFailureList(failure_list()), IsOk()); + + // "foo" doesn't match the entry "foo.bar", so the entry stays unseen. + ASSERT_THAT(ledger.ReportSkip("foo", "reason"), IsOk()); + + EXPECT_THAT(ledger.Finalize(), StatusIs(absl::StatusCode::kFailedPrecondition, + AllOf(HasSubstr("were not seen"), + HasSubstr("foo.bar")))); + + EXPECT_EQ(ledger.expected_failures(), 0); + EXPECT_EQ(ledger.unexpected_failures(), 0); + EXPECT_EQ(ledger.expected_successes(), 0); + EXPECT_EQ(ledger.unexpected_successes(), 0); + EXPECT_EQ(ledger.skipped(), 1); +} + +TEST_F(ResultLedgerTest, ReportUnexpectedFailure) { + CreateFailureList({}); + ResultLedger ledger; + ASSERT_THAT(ledger.LoadFailureList(failure_list()), IsOk()); + + ASSERT_THAT(ledger.ReportFailure("foo_failing", kP0, ""), + StatusIs(absl::StatusCode::kFailedPrecondition, + AllOf(HasSubstr("Unexpected failure"), + HasSubstr("foo_failing")))); + + EXPECT_THAT(ledger.Finalize(), IsOk()); + + EXPECT_EQ(ledger.expected_failures(), 0); + EXPECT_EQ(ledger.unexpected_failures(), 1); + EXPECT_EQ(ledger.expected_successes(), 0); + EXPECT_EQ(ledger.unexpected_successes(), 0); + EXPECT_EQ(ledger.skipped(), 0); +} + +TEST_F(ResultLedgerTest, ReportUnexpectedFailureMismatchedName) { + CreateFailureList({{"foo"}}); + ResultLedger ledger; + ASSERT_THAT(ledger.LoadFailureList(failure_list()), IsOk()); + + ASSERT_THAT(ledger.ReportFailure("foo_failing", kP0, ""), + StatusIs(absl::StatusCode::kFailedPrecondition, + AllOf(HasSubstr("Unexpected failure"), + HasSubstr("foo_failing")))); + ASSERT_THAT(ledger.ReportFailure("foo", kP0, ""), IsOk()); + + EXPECT_THAT(ledger.Finalize(), IsOk()); + EXPECT_EQ(ledger.expected_failures(), 1); + EXPECT_EQ(ledger.unexpected_failures(), 1); + EXPECT_EQ(ledger.expected_successes(), 0); + EXPECT_EQ(ledger.unexpected_successes(), 0); + EXPECT_EQ(ledger.skipped(), 0); +} + +TEST_F(ResultLedgerTest, ReportUnexpectedFailureMismatchedMessage) { + CreateFailureList({{"foo.*.bar", "message"}}); + ResultLedger ledger; + ASSERT_THAT(ledger.LoadFailureList(failure_list()), IsOk()); + + ASSERT_THAT(ledger.ReportFailure("foo.a.bar", kP0, "abc"), + StatusIs(absl::StatusCode::kFailedPrecondition, + AllOf(HasSubstr("Unexpected failure"), HasSubstr("foo"), + HasSubstr("message"), HasSubstr("abc")))); + EXPECT_THAT(ledger.ReportFailure("foo.b.bar", kP0, "message"), IsOk()); + + EXPECT_THAT(ledger.Finalize(), IsOk()); + EXPECT_EQ(ledger.expected_failures(), 1); + EXPECT_EQ(ledger.unexpected_failures(), 1); + EXPECT_EQ(ledger.expected_successes(), 0); + EXPECT_EQ(ledger.unexpected_successes(), 0); + EXPECT_EQ(ledger.skipped(), 0); +} + +TEST_F(ResultLedgerTest, ReportFailureMatchesExpectedMessagePrefix) { + CreateFailureList({{"foo", "abc"}}); + ResultLedger ledger; + ASSERT_THAT(ledger.LoadFailureList(failure_list()), IsOk()); + + // Like the legacy runner, the actual message only has to start with the + // expected one. + EXPECT_THAT(ledger.ReportFailure("foo", kP0, "abc: more details"), IsOk()); + + EXPECT_THAT(ledger.Finalize(), IsOk()); + EXPECT_EQ(ledger.expected_failures(), 1); + EXPECT_EQ(ledger.unexpected_failures(), 0); +} + +TEST_F(ResultLedgerTest, ReportFailureRejectsExpectedMessageSuffix) { + CreateFailureList({{"foo", "abc: more details"}}); + ResultLedger ledger; + ASSERT_THAT(ledger.LoadFailureList(failure_list()), IsOk()); + + // The expected message is longer than the actual one, so it's not a prefix. + EXPECT_THAT(ledger.ReportFailure("foo", kP0, "abc"), + StatusIs(absl::StatusCode::kFailedPrecondition, + HasSubstr("Unexpected failure message"))); + + // The mismatch is an unexpected failure (which --fix fixes); the entry + // itself was seen, so Finalize() has nothing to add. + EXPECT_THAT(ledger.UnexpectedFailures(), + ElementsAre(FieldsAre("foo", "abc", absl::nullopt))); + EXPECT_THAT(ledger.Finalize(), IsOk()); + EXPECT_EQ(ledger.expected_failures(), 0); + EXPECT_EQ(ledger.unexpected_failures(), 1); +} + +TEST_F(ResultLedgerTest, ReportFailureEmptyExpectedMessageMatchesAnything) { + CreateFailureList({{"foo"}}); + ResultLedger ledger; + ASSERT_THAT(ledger.LoadFailureList(failure_list()), IsOk()); + + EXPECT_THAT(ledger.ReportFailure("foo", kP0, "any message at all"), IsOk()); + + EXPECT_THAT(ledger.Finalize(), IsOk()); + EXPECT_EQ(ledger.expected_failures(), 1); + EXPECT_EQ(ledger.unexpected_failures(), 0); +} + +TEST_F(ResultLedgerTest, ReportFailurePrefixMatchIgnoresNewlinesAndWhitespace) { + CreateFailureList({{"foo", "abc def"}}); + ResultLedger ledger; + ASSERT_THAT(ledger.LoadFailureList(failure_list()), IsOk()); + + EXPECT_THAT(ledger.ReportFailure("foo", kP0, " abc def\nghi\n"), IsOk()); + + EXPECT_THAT(ledger.Finalize(), IsOk()); + EXPECT_EQ(ledger.expected_failures(), 1); +} + +TEST_F(ResultLedgerTest, PrefixMatchedFailureIsNotAnUnexpectedFailure) { + CreateFailureList({{"foo", "abc"}}); + ResultLedger ledger; + ASSERT_THAT(ledger.LoadFailureList(failure_list()), IsOk()); + + ASSERT_THAT(ledger.ReportFailure("foo", kP0, "abc: more details"), IsOk()); + + EXPECT_THAT(ledger.UnexpectedFailures(), IsEmpty()); + EXPECT_EQ(ledger.unexpected_failures(), 0); + EXPECT_THAT(ledger.Finalize(), IsOk()); +} + +TEST_F(ResultLedgerTest, FinalizeReportsUnseenExpectedFailures) { + CreateFailureList({{"ccc"}, {"aaa"}, {"bbb.*.baz"}, {"ddd"}, {"eee"}}); + ResultLedger ledger; + ASSERT_THAT(ledger.LoadFailureList(failure_list()), IsOk()); + + // Failures, unexpected successes and skips all count as "seen". + ASSERT_THAT(ledger.ReportFailure("bbb.x.baz", kP0, ""), IsOk()); + ASSERT_THAT(ledger.ReportSuccess("ccc"), Not(IsOk())); + ASSERT_THAT(ledger.ReportSkip("ddd", "reason"), Not(IsOk())); + // Tests that weren't selected to run do not. + ledger.ReportNotSelected("eee"); + + EXPECT_THAT(ledger.Finalize(), + StatusIs(absl::StatusCode::kFailedPrecondition, + "The following expected failures were not seen: aaa, " + "eee")); +} + +TEST_F(ResultLedgerTest, UnmatchedExpectedFailures) { + CreateFailureList({{"ccc"}, {"aaa"}, {"bbb.*.baz"}, {"ddd"}, {"eee", "msg"}}); + ResultLedger ledger; + ASSERT_THAT(ledger.LoadFailureList(failure_list()), IsOk()); + + EXPECT_THAT(ledger.UnmatchedExpectedFailures(), + ElementsAre("aaa", "bbb.*.baz", "ccc", "ddd", "eee")); + + // Any report whose name matches an entry counts, whatever the outcome. + ASSERT_THAT(ledger.ReportFailure("bbb.x.baz", kP0, ""), IsOk()); + ASSERT_THAT(ledger.ReportSuccess("ccc"), Not(IsOk())); + ASSERT_THAT(ledger.ReportSkip("ddd", "reason"), Not(IsOk())); + ASSERT_THAT(ledger.ReportFailure("eee", kP0, "other message"), Not(IsOk())); + // Reports that match no entry change nothing. + ASSERT_THAT(ledger.ReportSuccess("aaa.child"), IsOk()); + ASSERT_THAT(ledger.ReportSkip("zzz", "reason"), IsOk()); + + EXPECT_THAT(ledger.UnmatchedExpectedFailures(), ElementsAre("aaa")); + // A message mismatch is a verdict on the entry too: seen, not unseen. + EXPECT_THAT(ledger.Finalize(), + StatusIs(absl::StatusCode::kFailedPrecondition, + "The following expected failures were not seen: aaa")); +} + +TEST_F(ResultLedgerTest, UnmatchedExpectedFailuresEmpty) { + ResultLedger ledger; + EXPECT_THAT(ledger.UnmatchedExpectedFailures(), IsEmpty()); + EXPECT_THAT(ledger.Finalize(), IsOk()); +} + +TEST_F(ResultLedgerTest, UnexpectedFailures) { + CreateFailureList({{"foo.*.bar", "expected"}}); + ResultLedger ledger; + ASSERT_THAT(ledger.LoadFailureList(failure_list()), IsOk()); + + EXPECT_THAT(ledger.UnexpectedFailures(), IsEmpty()); + + // Not in the failure list at all, in the list with another message, and the + // messages are formatted as SaveFailureList() would write them. + ASSERT_THAT(ledger.ReportFailure("zzz", kP0, " new\nfailure "), + Not(IsOk())); + ASSERT_THAT(ledger.ReportFailure("foo.a.bar", kP0, "other message"), + Not(IsOk())); + ASSERT_THAT(ledger.ReportFailure("aaa", kP0, std::string(1000, 'x')), + Not(IsOk())); + // Expected failures, successes and skips are not reported. + ASSERT_THAT(ledger.ReportFailure("foo.b.bar", kP0, "expected: more"), IsOk()); + ASSERT_THAT(ledger.ReportSuccess("bbb"), IsOk()); + ASSERT_THAT(ledger.ReportSkip("ccc", "reason"), IsOk()); + + EXPECT_THAT( + ledger.UnexpectedFailures(), + ElementsAre(FieldsAre("aaa", std::string(128, 'x'), absl::nullopt), + FieldsAre("foo.a.bar", "other message", absl::nullopt), + FieldsAre("zzz", "newfailure", absl::nullopt))); + EXPECT_EQ(ledger.unexpected_failures(), 3); + EXPECT_THAT(ledger.Finalize(), IsOk()); +} + +TEST_F(ResultLedgerTest, OverexpandedWildcardKeepsMatching) { + CreateFailureList({{"foo.*.bar"}}); + ResultLedger ledger; + ASSERT_THAT(ledger.LoadFailureList(failure_list()), IsOk()); + + for (int i = 0; i < 100; ++i) { + ASSERT_THAT(ledger.ReportSuccess(absl::StrCat("foo.", i, ".bar")), + Not(IsOk())); + } + // The entry is far over the cap, but the verdicts don't change: the test + // environment fails the run over OverexpandedWildcards() instead. + ASSERT_THAT(ledger.ReportFailure("foo.baz.bar", kP0, "msg"), IsOk()); + + EXPECT_THAT(ledger.OverexpandedWildcards(), ElementsAre("foo.*.bar")); + EXPECT_THAT(ledger.UnexpectedFailures(), IsEmpty()); + EXPECT_EQ(ledger.expected_failures(), 1); + EXPECT_EQ(ledger.unexpected_failures(), 0); + EXPECT_THAT(ledger.UnexpectedSuccesses(), SizeIs(100)); + EXPECT_EQ(ledger.unexpected_successes(), 100); + EXPECT_THAT(ledger.Finalize(), IsOk()); +} + +TEST_F(ResultLedgerTest, UnexpectedFailuresIgnoreDuplicateReports) { + ResultLedger ledger; + + // Only the first report of a test counts. + ASSERT_THAT(ledger.ReportFailure("foo", kP0, "first"), Not(IsOk())); + ASSERT_THAT(ledger.ReportFailure("foo", kP0, "second"), Not(IsOk())); + + EXPECT_THAT(ledger.UnexpectedFailures(), + ElementsAre(FieldsAre("foo", "first", absl::nullopt))); + EXPECT_EQ(ledger.unexpected_failures(), 1); + EXPECT_THAT(ledger.Finalize(), IsOk()); +} + +TEST_F(ResultLedgerTest, UnexpectedSuccesses) { + CreateFailureList({{"foo.*.bar", "wildcard message"}, {"zzz", " zzz msg "}}); + ResultLedger ledger; + ASSERT_THAT(ledger.LoadFailureList(failure_list()), IsOk()); + + EXPECT_THAT(ledger.UnexpectedSuccesses(), IsEmpty()); + + ASSERT_THAT(ledger.ReportSuccess("zzz"), Not(IsOk())); + ASSERT_THAT(ledger.ReportSuccess("foo.b.bar"), Not(IsOk())); + ASSERT_THAT(ledger.ReportSuccess("foo.a.bar"), Not(IsOk())); + ASSERT_THAT(ledger.ReportSuccess("foo.a.bar"), Not(IsOk())); + // Expected successes are not reported. + ASSERT_THAT(ledger.ReportSuccess("aaa"), IsOk()); + + // Sorted by test name; the message is the (normalized) one of the entry the + // test matched. + EXPECT_THAT( + ledger.UnexpectedSuccesses(), + ElementsAre(FieldsAre("foo.a.bar", "wildcard message", + Optional(std::string("foo.*.bar"))), + FieldsAre("foo.b.bar", "wildcard message", + Optional(std::string("foo.*.bar"))), + FieldsAre("zzz", "zzz msg", Optional(std::string("zzz"))))); + EXPECT_EQ(ledger.unexpected_successes(), 3); + EXPECT_THAT(ledger.UnexpectedFailures(), IsEmpty()); + EXPECT_THAT(ledger.Finalize(), IsOk()); +} + +TEST_F(ResultLedgerTest, OverexpandedWildcards) { + CreateFailureList({{"foo.*.bar", "abc"}, {"baz.*", "abc"}}); + ResultLedger ledger; + ASSERT_THAT(ledger.LoadFailureList(failure_list()), IsOk()); + + // Expected failures and unexpected successes count toward the cap, each + // test once however often it is reported. + for (int i = 0; i < kMaximumWildcardExpansions; ++i) { + ASSERT_THAT( + ledger.ReportFailure(absl::StrCat("foo.", i, ".bar"), kP0, "abc"), + IsOk()); + ASSERT_THAT(ledger.ReportSuccess(absl::StrCat("baz.", i)), Not(IsOk())); + } + ASSERT_THAT(ledger.ReportFailure("foo.0.bar", kP0, "abc"), IsOk()); + ASSERT_THAT(ledger.ReportSuccess("baz.0"), Not(IsOk())); + EXPECT_THAT(ledger.OverexpandedWildcards(), IsEmpty()); + + // Skips and failures with another message don't count. + ASSERT_THAT(ledger.ReportSkip("foo.skip.bar", "reason"), Not(IsOk())); + ASSERT_THAT(ledger.ReportFailure("foo.other.bar", kP0, "xyz"), Not(IsOk())); + EXPECT_THAT(ledger.OverexpandedWildcards(), IsEmpty()); + + // One more verdict puts an entry over the cap. The list is sorted. + ASSERT_THAT(ledger.ReportFailure("foo.more.bar", kP0, "abc"), IsOk()); + ASSERT_THAT(ledger.ReportSuccess("baz.more"), Not(IsOk())); + EXPECT_THAT(ledger.OverexpandedWildcards(), + ElementsAre("baz.*", "foo.*.bar")); + EXPECT_THAT(ledger.Finalize(), IsOk()); +} + +TEST_F(ResultLedgerTest, LoadFailureListInvalidFile) { + ResultLedger ledger; + EXPECT_THAT(ledger.LoadFailureList(absl::StrCat(failure_list(), "/invalid")), + StatusIs(absl::StatusCode::kInternal, HasSubstr("/invalid"))); + ledger.Finalize().IgnoreError(); +} + +TEST_F(ResultLedgerTest, LoadFailureListDuplicateFailure) { + CreateFailureList(R"( + foo # abc + foo # zyx +)"); + ResultLedger ledger; + + EXPECT_THAT(ledger.LoadFailureList(failure_list()), + StatusIs(absl::StatusCode::kAlreadyExists, + AllOf(HasSubstr("already exists"), HasSubstr("foo")))); + ledger.Finalize().IgnoreError(); +} + +TEST_F(ResultLedgerTest, LoadFailureListOverlappingFailure) { + CreateFailureList(R"( + foo.*.bar # abc + foo.bar.* # zyx +)"); + ResultLedger ledger; + + EXPECT_THAT(ledger.LoadFailureList(failure_list()), + StatusIs(absl::StatusCode::kAlreadyExists, + AllOf(HasSubstr("foo.bar.*"), HasSubstr("foo.*.bar")))); + ledger.Finalize().IgnoreError(); +} + +TEST_F(ResultLedgerTest, UnexpectedSuccessMessageNamesTheEntry) { + CreateFailureList({{"foo", "abc"}}); + ResultLedger ledger; + ASSERT_THAT(ledger.LoadFailureList(failure_list()), IsOk()); + + // The message is the legacy runner's, including the matched entry. + EXPECT_THAT(ledger.ReportSuccess("foo"), + StatusIs(absl::StatusCode::kFailedPrecondition, + "test foo (matched to foo) is in the failure list, but " + "test succeeded. Remove its match from the failure " + "list.")); + EXPECT_THAT(ledger.Finalize(), IsOk()); + EXPECT_EQ(ledger.unexpected_successes(), 1); +} + +TEST_F(ResultLedgerTest, UnexpectedSuccessMessageNamesTheWildcardEntry) { + CreateFailureList({{"foo.*.bar", "abc"}}); + ResultLedger ledger; + ASSERT_THAT(ledger.LoadFailureList(failure_list()), IsOk()); + + EXPECT_THAT(ledger.ReportSuccess("foo.a.bar"), + StatusIs(absl::StatusCode::kFailedPrecondition, + HasSubstr("test foo.a.bar (matched to foo.*.bar) is in " + "the failure list"))); + EXPECT_THAT(ledger.Finalize(), IsOk()); +} + +TEST_F(ResultLedgerTest, EveryPriorityIsEnforcedByDefault) { + ResultLedger ledger; + EXPECT_THAT(ledger.ReportFailure("p0", kP0, "abc"), Not(IsOk())); + EXPECT_THAT(ledger.ReportFailure("p1", kP1, "abc"), Not(IsOk())); + EXPECT_EQ(ledger.unexpected_failures(), 2); + EXPECT_EQ(ledger.tolerated_failures(), 0); + EXPECT_THAT(ledger.Finalize(), IsOk()); +} + +TEST_F(ResultLedgerTest, EnforcementLevelZeroToleratesP1) { + // Only kP0 is enforced: an unlisted kP1 failure is tolerated. + ResultLedger ledger; + ledger.set_enforcement_level(kP0); + EXPECT_THAT(ledger.ReportFailure("p0", kP0, "abc"), Not(IsOk())); + EXPECT_THAT(ledger.ReportFailure("p1", kP1, "abc"), IsOk()); + EXPECT_EQ(ledger.unexpected_failures(), 1); + EXPECT_EQ(ledger.tolerated_failures(), 1); + EXPECT_THAT(ledger.Finalize(), IsOk()); +} + +TEST_F(ResultLedgerTest, ToleratedFailure) { + CreateFailureList({{"listed", "abc"}}); + ResultLedger ledger; + ledger.set_enforcement_level(kP0); + ASSERT_THAT(ledger.LoadFailureList(failure_list()), IsOk()); + + ASSERT_THAT(ledger.ReportFailure("foo", kP1, "abc"), IsOk()); + ASSERT_THAT(ledger.ReportFailure("bar", kP1, "xyz"), IsOk()); + // Duplicates only count once. + ASSERT_THAT(ledger.ReportFailure("foo", kP1, "abc"), IsOk()); + ASSERT_THAT(ledger.ReportFailure("listed", kP0, "abc"), IsOk()); + + EXPECT_EQ(ledger.tolerated_failures(), 2); + // A tolerated failure is neither a skip nor a failure of any kind, and it + // isn't written to the failure list. + EXPECT_EQ(ledger.skipped(), 0); + EXPECT_EQ(ledger.expected_failures(), 1); + EXPECT_EQ(ledger.unexpected_failures(), 0); + EXPECT_EQ(ledger.expected_successes(), 0); + EXPECT_EQ(ledger.unexpected_successes(), 0); + EXPECT_THAT(ledger.UnexpectedFailures(), IsEmpty()); + EXPECT_THAT(ledger.Finalize(), IsOk()); + + EXPECT_THAT(ledger.SaveFailureList(absl::StrCat(failure_list(), ".new")), + IsOk()); + std::string content; + ASSERT_THAT(File::GetContents(absl::StrCat(failure_list(), ".new"), &content, + true), + IsOk()); + EXPECT_EQ(content, "listed # abc\n"); +} + +TEST_F(ResultLedgerTest, ToleratedFailureIsCountedOnceAcrossKinds) { + ResultLedger ledger; + ledger.set_enforcement_level(kP0); + // The same test name can only be counted under one outcome. + ASSERT_THAT(ledger.ReportSuccess("foo"), IsOk()); + ASSERT_THAT(ledger.ReportFailure("foo", kP1, "abc"), IsOk()); + + EXPECT_EQ(ledger.expected_successes(), 1); + EXPECT_EQ(ledger.tolerated_failures(), 0); + EXPECT_THAT(ledger.Finalize(), IsOk()); +} + +TEST_F(ResultLedgerTest, ListedFailureIsCheckedWhateverItsPriority) { + // A listed test is never tolerated, so that the failure list can't go stale + // unnoticed. + CreateFailureList({{"foo", "abc"}, {"bar", "abc"}}); + ResultLedger ledger; + ledger.set_enforcement_level(kP0); + ASSERT_THAT(ledger.LoadFailureList(failure_list()), IsOk()); + + EXPECT_THAT(ledger.ReportFailure("foo", kP1, "abc"), IsOk()); + EXPECT_THAT(ledger.ReportFailure("bar", kP1, "xyz"), Not(IsOk())); + + EXPECT_EQ(ledger.expected_failures(), 1); + EXPECT_EQ(ledger.unexpected_failures(), 1); + EXPECT_EQ(ledger.tolerated_failures(), 0); + ledger.Finalize().IgnoreError(); +} + +TEST_F(ResultLedgerTest, LoadFailureListInvalidWildcard) { + CreateFailureList({{"foo.b*r", "abc"}}); + ResultLedger ledger; + EXPECT_THAT(ledger.LoadFailureList(failure_list()), + StatusIs(absl::StatusCode::kInvalidArgument)); + ledger.Finalize().IgnoreError(); +} + +TEST_F(ResultLedgerTest, LoadFailureListTruncatesMessage) { + // Like a reported message, so that the two still compare equal. + std::string message(1000, 'b'); + CreateFailureList({{"foo", message}}); + ResultLedger ledger; + ASSERT_THAT(ledger.LoadFailureList(failure_list()), IsOk()); + + EXPECT_THAT(ledger.ReportFailure("foo", kP0, message), IsOk()); + EXPECT_THAT(ledger.Finalize(), IsOk()); + EXPECT_EQ(ledger.expected_failures(), 1); +} + +TEST_F(ResultLedgerTest, LoadSecondFailureList) { + CreateFailureList({{"foo", "abc"}}); + const std::string second = absl::StrCat(failure_list(), ".2"); + ABSL_CHECK_OK(File::SetContents(second, "bar # zyx\n", true)); + const std::string third = absl::StrCat(failure_list(), ".3"); + ABSL_CHECK_OK(File::SetContents(third, "foo # zyx\n", true)); + ResultLedger ledger; + ASSERT_THAT(ledger.LoadFailureList(failure_list()), IsOk()); + ASSERT_THAT(ledger.LoadFailureList(second), IsOk()); + + // Entries must not repeat across files either. + EXPECT_THAT(ledger.LoadFailureList(third), + StatusIs(absl::StatusCode::kAlreadyExists)); + EXPECT_THAT(ledger.ReportFailure("foo", kP0, "abc"), IsOk()); + EXPECT_THAT(ledger.ReportFailure("bar", kP0, "zyx"), IsOk()); + EXPECT_THAT(ledger.Finalize(), IsOk()); + EXPECT_EQ(ledger.expected_failures(), 2); +} + +TEST_F(ResultLedgerTest, ExpectedMessagesAreNormalized) { + CreateFailureList({{"foo", " abc "}, {"bar.*.baz", "xyz"}, {"qux"}}); + ResultLedger ledger; + ASSERT_THAT(ledger.LoadFailureList(failure_list()), IsOk()); + + // Like reported messages (see ReportFailure()), so that SaveFailureList() + // compares like with like. + const FailureList& failure_list = ledger.failure_list(); + EXPECT_THAT(failure_list.ExpectedMessage("foo"), Optional(Eq("abc"))); + EXPECT_THAT(failure_list.ExpectedMessage("bar.*.baz"), Optional(Eq("xyz"))); + EXPECT_THAT(failure_list.ExpectedMessage("qux"), Optional(Eq(""))); + EXPECT_THAT(ledger.Finalize(), Not(IsOk())); +} + +TEST_F(ResultLedgerTest, FailureListMatchesWholeNameComponents) { + CreateFailureList({{"foo", "abc"}, {"bar.*.baz", "abc"}, {"qux", "abc"}}); + ResultLedger ledger; + ASSERT_THAT(ledger.LoadFailureList(failure_list()), IsOk()); + + EXPECT_THAT(ledger.ReportFailure("foo", kP0, "abc"), IsOk()); + EXPECT_THAT(ledger.ReportFailure("qux", kP0, "abc"), IsOk()); + EXPECT_THAT(ledger.ReportFailure("bar.a.baz", kP0, "abc"), IsOk()); + EXPECT_THAT(ledger.ReportFailure("bar.b.baz", kP0, "abc"), IsOk()); + // Neither a prefix nor an extension of an entry matches it, and a wildcard + // stands for exactly one component. + EXPECT_THAT(ledger.ReportFailure("foo.bar", kP0, "abc"), Not(IsOk())); + EXPECT_THAT(ledger.ReportFailure("bar", kP0, "abc"), Not(IsOk())); + EXPECT_THAT(ledger.ReportFailure("bar.a", kP0, "abc"), Not(IsOk())); + EXPECT_THAT(ledger.ReportFailure("bar.a.baz.qux", kP0, "abc"), Not(IsOk())); + EXPECT_THAT(ledger.ReportFailure("other", kP0, "abc"), Not(IsOk())); + + EXPECT_EQ(ledger.expected_failures(), 4); + EXPECT_EQ(ledger.unexpected_failures(), 5); + EXPECT_THAT(ledger.Finalize(), IsOk()); +} + +TEST_F(ResultLedgerTest, SaveFailureListNoop) { + CreateFailureList(R"( +foo # abc +)"); + ResultLedger ledger; + ASSERT_THAT(ledger.LoadFailureList(failure_list()), IsOk()); + ASSERT_THAT(ledger.ReportFailure("foo", kP0, "abc"), IsOk()); + ASSERT_THAT(ledger.Finalize(), IsOk()); + + EXPECT_THAT(ledger.SaveFailureList(absl::StrCat(failure_list(), ".new")), + IsOk()); + + std::string content; + ASSERT_THAT(File::GetContents(absl::StrCat(failure_list(), ".new"), &content, + true), + IsOk()); + EXPECT_EQ(content, R"( +foo # abc +)"); +} + +TEST_F(ResultLedgerTest, SaveFailureListNoopWildcard) { + CreateFailureList(R"( +foo.*.bar # abc +)"); + ResultLedger ledger; + ASSERT_THAT(ledger.LoadFailureList(failure_list()), IsOk()); + ASSERT_THAT(ledger.ReportFailure("foo.a.bar", kP0, "abc"), IsOk()); + ASSERT_THAT(ledger.Finalize(), IsOk()); + + EXPECT_THAT(ledger.SaveFailureList(absl::StrCat(failure_list(), ".new")), + IsOk()); + + std::string content; + ASSERT_THAT(File::GetContents(absl::StrCat(failure_list(), ".new"), &content, + true), + IsOk()); + EXPECT_EQ(content, R"( +foo.*.bar # abc +)"); +} + +TEST_F(ResultLedgerTest, SaveFailureListKeepsEntryWithInternalWhitespace) { + // LoadFailureList() ignores whitespace anywhere in the name, so the entry + // matches "foo.bar"; SaveFailureList() must find it under the same key. + CreateFailureList(R"( +foo .bar # abc +)"); + ResultLedger ledger; + ASSERT_THAT(ledger.LoadFailureList(failure_list()), IsOk()); + ASSERT_THAT(ledger.ReportFailure("foo.bar", kP0, "abc"), IsOk()); + ASSERT_THAT(ledger.Finalize(), IsOk()); + + EXPECT_THAT(ledger.SaveFailureList(absl::StrCat(failure_list(), ".new")), + IsOk()); + + std::string content; + ASSERT_THAT(File::GetContents(absl::StrCat(failure_list(), ".new"), &content, + true), + IsOk()); + EXPECT_EQ(content, R"( +foo .bar # abc +)"); +} + +TEST_F(ResultLedgerTest, SaveFailureListKeepsPrefixMatchedEntryVerbatim) { + CreateFailureList(R"( +foo # abc +)"); + ResultLedger ledger; + ASSERT_THAT(ledger.LoadFailureList(failure_list()), IsOk()); + // The actual message is longer than the entry's, which is only a prefix. + ASSERT_THAT(ledger.ReportFailure("foo", kP0, "abc: more details"), IsOk()); + ASSERT_THAT(ledger.Finalize(), IsOk()); + + EXPECT_THAT(ledger.SaveFailureList(absl::StrCat(failure_list(), ".new")), + IsOk()); + + std::string content; + ASSERT_THAT(File::GetContents(absl::StrCat(failure_list(), ".new"), &content, + true), + IsOk()); + EXPECT_EQ(content, R"( +foo # abc +)"); +} + +TEST_F(ResultLedgerTest, SaveFailureListRemoveNewPassing) { + CreateFailureList(R"( +foo # abc +)"); + ResultLedger ledger; + ASSERT_THAT(ledger.LoadFailureList(failure_list()), IsOk()); + ASSERT_THAT(ledger.ReportSuccess("foo"), Not(IsOk())); + ASSERT_THAT(ledger.Finalize(), IsOk()); + + EXPECT_THAT(ledger.SaveFailureList(absl::StrCat(failure_list(), ".new")), + IsOk()); + + std::string content; + ASSERT_THAT(File::GetContents(absl::StrCat(failure_list(), ".new"), &content, + true), + IsOk()); + EXPECT_EQ(content, R"( +)"); +} + +TEST_F(ResultLedgerTest, SaveFailureListDropsUnseenEntry) { + CreateFailureList(R"( +foo # abc +bar # def +)"); + ResultLedger ledger; + ASSERT_THAT(ledger.LoadFailureList(failure_list()), IsOk()); + ASSERT_THAT(ledger.ReportFailure("bar", kP0, "def"), IsOk()); + ASSERT_THAT(ledger.Finalize(), + StatusIs(absl::StatusCode::kFailedPrecondition, + "The following expected failures were not seen: foo")); + + EXPECT_THAT(ledger.SaveFailureList(absl::StrCat(failure_list(), ".new")), + IsOk()); + + std::string content; + ASSERT_THAT(File::GetContents(absl::StrCat(failure_list(), ".new"), &content, + true), + IsOk()); + EXPECT_EQ(content, R"( +bar # def +)"); +} + +TEST_F(ResultLedgerTest, SaveFailureListExpandsWildcardWhenOneMatchPasses) { + // The wildcard no longer describes the failures: the test that passed must + // go, but the ones that still fail must stay listed, so the wildcard is + // expanded into them, in its place. + CreateFailureList(R"( +# Header kept above the expansion. +foo.*.bar # abc +zzz # zzz +)"); + ResultLedger ledger; + ASSERT_THAT(ledger.LoadFailureList(failure_list()), IsOk()); + ASSERT_THAT(ledger.ReportFailure("foo.c.bar", kP0, "abc"), IsOk()); + ASSERT_THAT(ledger.ReportSuccess("foo.b.bar"), Not(IsOk())); + ASSERT_THAT(ledger.ReportFailure("foo.a.bar", kP0, "abc: details"), IsOk()); + ASSERT_THAT(ledger.ReportFailure("zzz", kP0, "zzz"), IsOk()); + ASSERT_THAT(ledger.Finalize(), IsOk()); + + EXPECT_THAT(ledger.SaveFailureList(absl::StrCat(failure_list(), ".new")), + IsOk()); + + std::string content; + ASSERT_THAT(File::GetContents(absl::StrCat(failure_list(), ".new"), &content, + true), + IsOk()); + EXPECT_EQ(content, R"( +# Header kept above the expansion. +foo.a.bar # abc +foo.c.bar # abc +zzz # zzz +)"); +} + +TEST_F(ResultLedgerTest, SaveFailureListDropsWildcardWhenEveryMatchPasses) { + CreateFailureList(R"( +# Header left behind. +foo.*.bar # abc +)"); + ResultLedger ledger; + ASSERT_THAT(ledger.LoadFailureList(failure_list()), IsOk()); + ASSERT_THAT(ledger.ReportSuccess("foo.a.bar"), Not(IsOk())); + ASSERT_THAT(ledger.ReportSuccess("foo.b.bar"), Not(IsOk())); + ASSERT_THAT(ledger.Finalize(), IsOk()); + + EXPECT_THAT(ledger.SaveFailureList(absl::StrCat(failure_list(), ".new")), + IsOk()); + + std::string content; + ASSERT_THAT(File::GetContents(absl::StrCat(failure_list(), ".new"), &content, + true), + IsOk()); + EXPECT_EQ(content, R"( +# Header left behind. +)"); +} + +TEST_F(ResultLedgerTest, + SaveFailureListExpandsWildcardWhenOneMatchChangesMessage) { + // One match fails with a different message while another still fails with + // the listed one: the wildcard is expanded so that each test gets its own + // message, and the result loads again (it used to contain the wildcard + // twice). + CreateFailureList(R"( +foo.*.bar # abc +)"); + ResultLedger ledger; + ASSERT_THAT(ledger.LoadFailureList(failure_list()), IsOk()); + ASSERT_THAT(ledger.ReportFailure("foo.a.bar", kP0, "xyz"), Not(IsOk())); + ASSERT_THAT(ledger.ReportFailure("foo.b.bar", kP0, "abc"), IsOk()); + ASSERT_THAT(ledger.Finalize(), IsOk()); + + EXPECT_THAT(ledger.SaveFailureList(absl::StrCat(failure_list(), ".new")), + IsOk()); + + std::string content; + ASSERT_THAT(File::GetContents(absl::StrCat(failure_list(), ".new"), &content, + true), + IsOk()); + EXPECT_EQ(content, R"( +foo.a.bar # xyz +foo.b.bar # abc +)"); + ResultLedger reloaded; + EXPECT_THAT(reloaded.LoadFailureList(absl::StrCat(failure_list(), ".new")), + IsOk()); + EXPECT_THAT(reloaded.failure_list().ExpectedMessage("foo.a.bar"), + Optional(std::string("xyz"))); + EXPECT_THAT(reloaded.failure_list().ExpectedMessage("foo.b.bar"), + Optional(std::string("abc"))); + ASSERT_THAT(reloaded.Finalize(), Not(IsOk())); +} + +TEST_F(ResultLedgerTest, + SaveFailureListExpandsWildcardWhenMatchesChangeToDifferentMessages) { + CreateFailureList(R"( +foo.*.bar # abc +)"); + ResultLedger ledger; + ASSERT_THAT(ledger.LoadFailureList(failure_list()), IsOk()); + ASSERT_THAT(ledger.ReportFailure("foo.a.bar", kP0, "xyz"), Not(IsOk())); + ASSERT_THAT(ledger.ReportFailure("foo.b.bar", kP0, "uvw"), Not(IsOk())); + EXPECT_THAT(ledger.UnexpectedFailures(), + ElementsAre(FieldsAre("foo.a.bar", "xyz", absl::nullopt), + FieldsAre("foo.b.bar", "uvw", absl::nullopt))); + ASSERT_THAT(ledger.Finalize(), IsOk()); + + EXPECT_THAT(ledger.SaveFailureList(absl::StrCat(failure_list(), ".new")), + IsOk()); + + std::string content; + ASSERT_THAT(File::GetContents(absl::StrCat(failure_list(), ".new"), &content, + true), + IsOk()); + EXPECT_EQ(content, R"( +foo.a.bar # xyz +foo.b.bar # uvw +)"); +} + +TEST_F(ResultLedgerTest, + SaveFailureListKeepsWildcardWhenEveryMatchChangesToTheSameMessage) { + // The wildcard still describes the failures, only the message moved on, so + // the author's wildcard is kept with the new message. + CreateFailureList(R"( +foo.*.bar # abc +)"); + ResultLedger ledger; + ASSERT_THAT(ledger.LoadFailureList(failure_list()), IsOk()); + ASSERT_THAT(ledger.ReportFailure("foo.a.bar", kP0, "xyz"), Not(IsOk())); + ASSERT_THAT(ledger.ReportFailure("foo.b.bar", kP0, "xyz"), Not(IsOk())); + EXPECT_THAT(ledger.UnexpectedFailures(), + ElementsAre(FieldsAre("foo.a.bar", "xyz", absl::nullopt), + FieldsAre("foo.b.bar", "xyz", absl::nullopt))); + ASSERT_THAT(ledger.Finalize(), IsOk()); + + EXPECT_THAT(ledger.SaveFailureList(absl::StrCat(failure_list(), ".new")), + IsOk()); + + std::string content; + ASSERT_THAT(File::GetContents(absl::StrCat(failure_list(), ".new"), &content, + true), + IsOk()); + EXPECT_EQ(content, R"( +foo.*.bar # xyz +)"); +} + +TEST_F(ResultLedgerTest, SaveFailureListExpandedWildcardDropsSkippedMatch) { + // A skipped test can't be an expected failure, so when the wildcard is + // expanded (here because another match passed) the skipped test gets no + // line, like the one that passed. + CreateFailureList(R"( +foo.*.bar # abc +)"); + ResultLedger ledger; + ASSERT_THAT(ledger.LoadFailureList(failure_list()), IsOk()); + ASSERT_THAT(ledger.ReportSkip("foo.a.bar", "reason"), Not(IsOk())); + ASSERT_THAT(ledger.ReportSuccess("foo.b.bar"), Not(IsOk())); + ASSERT_THAT(ledger.ReportFailure("foo.c.bar", kP0, "abc"), IsOk()); + ASSERT_THAT(ledger.Finalize(), IsOk()); + + EXPECT_THAT(ledger.SaveFailureList(absl::StrCat(failure_list(), ".new")), + IsOk()); + + std::string content; + ASSERT_THAT(File::GetContents(absl::StrCat(failure_list(), ".new"), &content, + true), + IsOk()); + EXPECT_EQ(content, R"( +foo.c.bar # abc +)"); +} + +TEST_F(ResultLedgerTest, SaveFailureListExpandsWildcardWhenOneMatchIsSkipped) { + // A skip alone is reason to expand the wildcard, just like a pass: the tests + // that still fail stay listed, the skipped one goes. + CreateFailureList(R"( +foo.*.bar # abc +)"); + ResultLedger ledger; + ASSERT_THAT(ledger.LoadFailureList(failure_list()), IsOk()); + ASSERT_THAT(ledger.ReportFailure("foo.a.bar", kP0, "abc"), IsOk()); + ASSERT_THAT(ledger.ReportSkip("foo.b.bar", "reason"), Not(IsOk())); + ASSERT_THAT(ledger.ReportFailure("foo.c.bar", kP0, "abc"), IsOk()); + ASSERT_THAT(ledger.Finalize(), IsOk()); + + EXPECT_THAT(ledger.SaveFailureList(absl::StrCat(failure_list(), ".new")), + IsOk()); + + std::string content; + ASSERT_THAT(File::GetContents(absl::StrCat(failure_list(), ".new"), &content, + true), + IsOk()); + EXPECT_EQ(content, R"( +foo.a.bar # abc +foo.c.bar # abc +)"); +} + +TEST_F(ResultLedgerTest, SaveFailureListDropsWildcardWhenEveryMatchIsSkipped) { + CreateFailureList(R"( +# Header left behind. +foo.*.bar # abc +)"); + ResultLedger ledger; + ASSERT_THAT(ledger.LoadFailureList(failure_list()), IsOk()); + ASSERT_THAT(ledger.ReportSkip("foo.a.bar", "reason"), Not(IsOk())); + ASSERT_THAT(ledger.ReportSkip("foo.b.bar", "reason"), Not(IsOk())); + ASSERT_THAT(ledger.Finalize(), IsOk()); + + EXPECT_THAT(ledger.SaveFailureList(absl::StrCat(failure_list(), ".new")), + IsOk()); + + std::string content; + ASSERT_THAT(File::GetContents(absl::StrCat(failure_list(), ".new"), &content, + true), + IsOk()); + EXPECT_EQ(content, R"( +# Header left behind. +)"); +} + +TEST_F(ResultLedgerTest, SaveFailureListKeepsWildcardWithTooManyMatches) { + // Exceeding the wildcard expansion cap is reported separately (see + // OverexpandedWildcards()); the tests do still fail with the listed + // message, so the entry is kept. + CreateFailureList(R"( +foo.*.bar # abc +)"); + ResultLedger ledger; + ASSERT_THAT(ledger.LoadFailureList(failure_list()), IsOk()); + for (int i = 0; i <= kMaximumWildcardExpansions; ++i) { + ASSERT_THAT( + ledger.ReportFailure(absl::StrCat("foo.", i, ".bar"), kP0, "abc"), + IsOk()); + } + ASSERT_THAT(ledger.OverexpandedWildcards(), ElementsAre("foo.*.bar")); + ASSERT_THAT(ledger.Finalize(), IsOk()); + + EXPECT_THAT(ledger.SaveFailureList(absl::StrCat(failure_list(), ".new")), + IsOk()); + + std::string content; + ASSERT_THAT(File::GetContents(absl::StrCat(failure_list(), ".new"), &content, + true), + IsOk()); + EXPECT_EQ(content, R"( +foo.*.bar # abc +)"); +} + +TEST_F(ResultLedgerTest, SaveFailureListExpansionInterleavesNewFailures) { + // New failures are inserted by name among the expanded lines too, and the + // alignment accounts for the expanded names. + CreateFailureList(R"( +# Block one. +f.*.x # abc + +# Block two. +zz # zzz +)"); + ResultLedger ledger; + ASSERT_THAT(ledger.LoadFailureList(failure_list()), IsOk()); + ASSERT_THAT(ledger.ReportFailure("f.aaaa.x", kP0, "abc"), IsOk()); + ASSERT_THAT(ledger.ReportSuccess("f.b.x"), Not(IsOk())); + ASSERT_THAT(ledger.ReportFailure("f.cc.x", kP0, "abc"), IsOk()); + ASSERT_THAT(ledger.ReportFailure("f.bb.y", kP0, "new"), Not(IsOk())); + ASSERT_THAT(ledger.ReportFailure("zz", kP0, "zzz"), IsOk()); + ASSERT_THAT(ledger.Finalize(), IsOk()); + + EXPECT_THAT(ledger.SaveFailureList(absl::StrCat(failure_list(), ".new")), + IsOk()); + + std::string content; + ASSERT_THAT(File::GetContents(absl::StrCat(failure_list(), ".new"), &content, + true), + IsOk()); + EXPECT_EQ(content, R"( +# Block one. +f.aaaa.x # abc +f.bb.y # new +f.cc.x # abc + +# Block two. +zz # zzz +)"); +} + +TEST_F(ResultLedgerTest, SaveFailureListChangedMessageStaysInPlace) { + // An exact entry whose message changed is rewritten where it is, so that it + // stays under its comment block. + CreateFailureList(R"( +# About bbb. +bbb # abc + +# About aaa. +aaa # aaa +)"); + ResultLedger ledger; + ASSERT_THAT(ledger.LoadFailureList(failure_list()), IsOk()); + ASSERT_THAT(ledger.ReportFailure("bbb", kP0, "xyz"), Not(IsOk())); + ASSERT_THAT(ledger.ReportFailure("aaa", kP0, "aaa"), IsOk()); + EXPECT_THAT(ledger.UnexpectedFailures(), + ElementsAre(FieldsAre("bbb", "xyz", absl::nullopt))); + ASSERT_THAT(ledger.Finalize(), IsOk()); + + EXPECT_THAT(ledger.SaveFailureList(absl::StrCat(failure_list(), ".new")), + IsOk()); + + std::string content; + ASSERT_THAT(File::GetContents(absl::StrCat(failure_list(), ".new"), &content, + true), + IsOk()); + EXPECT_EQ(content, R"( +# About bbb. +bbb # xyz + +# About aaa. +aaa # aaa +)"); +} + +TEST_F(ResultLedgerTest, SaveFailureListKeepsMessagelessWildcardVerbatim) { + CreateFailureList(R"( +foo.*.bar +zzz # zzz +)"); + ResultLedger ledger; + ASSERT_THAT(ledger.LoadFailureList(failure_list()), IsOk()); + // An empty expected message matches any failure. + ASSERT_THAT(ledger.ReportFailure("foo.a.bar", kP0, "abc"), IsOk()); + ASSERT_THAT(ledger.ReportFailure("foo.b.bar", kP0, "xyz"), IsOk()); + ASSERT_THAT(ledger.ReportFailure("zzz", kP0, "zzz"), IsOk()); + ASSERT_THAT(ledger.Finalize(), IsOk()); + + EXPECT_THAT(ledger.SaveFailureList(absl::StrCat(failure_list(), ".new")), + IsOk()); + + std::string content; + ASSERT_THAT(File::GetContents(absl::StrCat(failure_list(), ".new"), &content, + true), + IsOk()); + EXPECT_EQ(content, R"( +foo.*.bar +zzz # zzz +)"); +} + +TEST_F(ResultLedgerTest, SaveFailureListExpandsMessagelessWildcard) { + // The expanded lines have no message either: no `#` and no trailing space. + CreateFailureList(R"( +foo.*.bar +)"); + ResultLedger ledger; + ASSERT_THAT(ledger.LoadFailureList(failure_list()), IsOk()); + ASSERT_THAT(ledger.ReportFailure("foo.a.bar", kP0, "abc"), IsOk()); + ASSERT_THAT(ledger.ReportSuccess("foo.b.bar"), Not(IsOk())); + ASSERT_THAT(ledger.ReportFailure("foo.c.bar", kP0, ""), IsOk()); + ASSERT_THAT(ledger.Finalize(), IsOk()); + + EXPECT_THAT(ledger.SaveFailureList(absl::StrCat(failure_list(), ".new")), + IsOk()); + + std::string content; + ASSERT_THAT(File::GetContents(absl::StrCat(failure_list(), ".new"), &content, + true), + IsOk()); + EXPECT_EQ(content, R"( +foo.a.bar +foo.c.bar +)"); +} + +TEST_F(ResultLedgerTest, SaveFailureListDropsSkippedEntry) { + // A skipped test can't be an expected failure, so its entry goes like that + // of a test that passed (whether the skip itself is acceptable is decided, + // and reported, by the caller). + CreateFailureList(R"( +foo # abc +)"); + ResultLedger ledger; + ASSERT_THAT(ledger.LoadFailureList(failure_list()), IsOk()); + ASSERT_THAT(ledger.ReportSkip("foo", "reason"), Not(IsOk())); + ASSERT_THAT(ledger.Finalize(), IsOk()); + + EXPECT_THAT(ledger.SaveFailureList(absl::StrCat(failure_list(), ".new")), + IsOk()); + + std::string content; + ASSERT_THAT(File::GetContents(absl::StrCat(failure_list(), ".new"), &content, + true), + IsOk()); + EXPECT_EQ(content, R"( +)"); +} + +TEST_F(ResultLedgerTest, SaveFailureListUnwritablePath) { + ResultLedger ledger; + ASSERT_THAT(ledger.ReportFailure("foo", kP0, "abc"), Not(IsOk())); + ASSERT_THAT(ledger.Finalize(), IsOk()); + + // A directory can't be opened for writing. + const std::string directory = absl::StrCat(failure_list(), ".dir"); + ASSERT_THAT(File::RecursivelyCreateDir(directory, 0777), + IsOk()); + EXPECT_THAT(ledger.SaveFailureList(directory), + StatusIs(absl::StatusCode::kInternal, HasSubstr(directory))); + // Neither can a file in a directory that doesn't exist. + const std::string missing_directory = + absl::StrCat(failure_list(), ".missing/list.txt"); + EXPECT_THAT( + ledger.SaveFailureList(missing_directory), + StatusIs(absl::StatusCode::kInternal, HasSubstr(missing_directory))); +} + +TEST_F(ResultLedgerTest, SaveFailureListWriteFailure) { + ResultLedger ledger; + ASSERT_THAT(ledger.ReportFailure("foo", kP0, "abc"), Not(IsOk())); + ASSERT_THAT(ledger.Finalize(), IsOk()); + + // /dev/full opens fine but every write to it fails with ENOSPC, which is + // the only way to reach the write-failure path without a filesystem mock. + // It is a Linux device. +#ifdef __linux__ + EXPECT_THAT(ledger.SaveFailureList("/dev/full"), + StatusIs(absl::StatusCode::kInternal, + HasSubstr("Failed to write failure list file"))); +#else + GTEST_SKIP() << "/dev/full is Linux-only"; +#endif +} + +TEST_F(ResultLedgerTest, SaveFailureListChangeFailureMessage) { + CreateFailureList(R"( +foo # abc +)"); + ResultLedger ledger; + ASSERT_THAT(ledger.LoadFailureList(failure_list()), IsOk()); + ASSERT_THAT(ledger.ReportFailure("foo", kP0, "zyx"), Not(IsOk())); + EXPECT_THAT(ledger.UnexpectedFailures(), + ElementsAre(FieldsAre("foo", "zyx", absl::nullopt))); + ASSERT_THAT(ledger.Finalize(), IsOk()); + + EXPECT_THAT(ledger.SaveFailureList(absl::StrCat(failure_list(), ".new")), + IsOk()); + + std::string content; + ASSERT_THAT(File::GetContents(absl::StrCat(failure_list(), ".new"), &content, + true), + IsOk()); + EXPECT_EQ(content, R"( +foo # zyx +)"); +} + +TEST_F(ResultLedgerTest, SaveFailureListAddNewFailure) { + ResultLedger ledger; + ASSERT_THAT(ledger.ReportFailure("foo", kP0, "abc"), Not(IsOk())); + ASSERT_THAT(ledger.Finalize(), IsOk()); + + EXPECT_THAT(ledger.SaveFailureList(absl::StrCat(failure_list(), ".new")), + IsOk()); + + std::string content; + ASSERT_THAT(File::GetContents(absl::StrCat(failure_list(), ".new"), &content, + true), + IsOk()); + EXPECT_EQ(content, R"(foo # abc +)"); +} + +TEST_F(ResultLedgerTest, SaveFailureListAddNormalizedMessage) { + ResultLedger ledger; + std::string message = absl::StrCat("aa\n", std::string(1000, 'b')); + std::string normalized_message = absl::StrCat("aa", std::string(126, 'b')); + ASSERT_THAT(ledger.ReportFailure("foo", kP0, message), Not(IsOk())); + ASSERT_THAT(ledger.Finalize(), IsOk()); + + EXPECT_THAT(ledger.SaveFailureList(absl::StrCat(failure_list(), ".new")), + IsOk()); + + std::string content; + ASSERT_THAT(File::GetContents(absl::StrCat(failure_list(), ".new"), &content, + true), + IsOk()); + EXPECT_EQ(content, absl::StrCat("foo # ", normalized_message, "\n")); +} + +TEST_F(ResultLedgerTest, SaveFailureListInsertAlphabetically) { + CreateFailureList(R"( +# This is a comment. +aaa # aaa +# This is another comment. +bbb # bbb + +ccc # ccc +)"); + ResultLedger ledger; + ASSERT_THAT(ledger.LoadFailureList(failure_list()), IsOk()); + ASSERT_THAT(ledger.ReportFailure("aaa", kP0, "aaa"), IsOk()); + ASSERT_THAT(ledger.ReportFailure("bbb", kP0, "bbb"), IsOk()); + ASSERT_THAT(ledger.ReportFailure("ccc", kP0, "ccc"), IsOk()); + ASSERT_THAT(ledger.ReportFailure("abc", kP0, "abc"), Not(IsOk())); + ASSERT_THAT(ledger.Finalize(), IsOk()); + + EXPECT_THAT(ledger.SaveFailureList(absl::StrCat(failure_list(), ".new")), + IsOk()); + + std::string content; + ASSERT_THAT(File::GetContents(absl::StrCat(failure_list(), ".new"), &content, + true), + IsOk()); + EXPECT_EQ(content, R"( +# This is a comment. +aaa # aaa +# This is another comment. +abc # abc +bbb # bbb + +ccc # ccc +)"); +} + +TEST_F(ResultLedgerTest, SaveFailureListInsertAligned) { + CreateFailureList(R"( +# This is a comment. +aa # aaa +# This is another comment. +bbbb #bbb + +cc # ccc +)"); + ResultLedger ledger; + ASSERT_THAT(ledger.LoadFailureList(failure_list()), IsOk()); + ASSERT_THAT(ledger.ReportFailure("aa", kP0, "aaa"), IsOk()); + ASSERT_THAT(ledger.ReportFailure("bbbb", kP0, " bbb "), IsOk()); + ASSERT_THAT(ledger.ReportFailure("cc", kP0, "ccc"), IsOk()); + ASSERT_THAT(ledger.ReportFailure("abcdef", kP0, "abc"), Not(IsOk())); + ASSERT_THAT(ledger.Finalize(), IsOk()); + + EXPECT_THAT(ledger.SaveFailureList(absl::StrCat(failure_list(), ".new")), + IsOk()); + + std::string content; + ASSERT_THAT(File::GetContents(absl::StrCat(failure_list(), ".new"), &content, + true), + IsOk()); + EXPECT_EQ(content, R"( +# This is a comment. +aa # aaa +# This is another comment. +abcdef # abc +bbbb # bbb + +cc # ccc +)"); +} + +// The copybara marker lines of a strip block (the region the open source +// export drops), spelled in two pieces so that the export's scrubber, which +// processes every file, leaves this test's data alone. +constexpr absl::string_view kStripBegin = + "# copybara:" + "strip_begin"; +constexpr absl::string_view kStripEnd = + "# copybara:" + "strip_end"; + +TEST_F(ResultLedgerTest, SaveFailureListKeepsUnchangedStripBlockVerbatim) { + // The block is aligned on its own, so a list whose block is narrower than + // the rest is a no-op byte for byte. + CreateFailureList(absl::Substitute(R"( +aaa.long.name # aaa +zzz # zzz + +# Google-only failures. +$0 +bbb # bbb +ccc # ccc +$1 +)", + kStripBegin, kStripEnd)); + ResultLedger ledger; + ASSERT_THAT(ledger.LoadFailureList(failure_list()), IsOk()); + ASSERT_THAT(ledger.ReportFailure("aaa.long.name", kP0, "aaa"), IsOk()); + ASSERT_THAT(ledger.ReportFailure("zzz", kP0, "zzz"), IsOk()); + ASSERT_THAT(ledger.ReportFailure("bbb", kP0, "bbb"), IsOk()); + ASSERT_THAT(ledger.ReportFailure("ccc", kP0, "ccc"), IsOk()); + ASSERT_THAT(ledger.Finalize(), IsOk()); + + EXPECT_THAT(ledger.SaveFailureList(absl::StrCat(failure_list(), ".new")), + IsOk()); + + std::string content; + ASSERT_THAT(File::GetContents(absl::StrCat(failure_list(), ".new"), &content, + true), + IsOk()); + EXPECT_EQ(content, absl::Substitute(R"( +aaa.long.name # aaa +zzz # zzz + +# Google-only failures. +$0 +bbb # bbb +ccc # ccc +$1 +)", + kStripBegin, kStripEnd)); +} + +TEST_F(ResultLedgerTest, SaveFailureListRewritesEntryInsideStripBlockInPlace) { + CreateFailureList(absl::Substitute(R"( +aaa.long.name # aaa +$0 +bbb # bbb +ccc # ccc +$1 +)", + kStripBegin, kStripEnd)); + ResultLedger ledger; + ASSERT_THAT(ledger.LoadFailureList(failure_list()), IsOk()); + ASSERT_THAT(ledger.ReportFailure("aaa.long.name", kP0, "aaa"), IsOk()); + ASSERT_THAT(ledger.ReportFailure("bbb", kP0, "new message"), Not(IsOk())); + ASSERT_THAT(ledger.ReportFailure("ccc", kP0, "ccc"), IsOk()); + ASSERT_THAT(ledger.Finalize(), IsOk()); + + EXPECT_THAT(ledger.SaveFailureList(absl::StrCat(failure_list(), ".new")), + IsOk()); + + std::string content; + ASSERT_THAT(File::GetContents(absl::StrCat(failure_list(), ".new"), &content, + true), + IsOk()); + EXPECT_EQ(content, absl::Substitute(R"( +aaa.long.name # aaa +$0 +bbb # new message +ccc # ccc +$1 +)", + kStripBegin, kStripEnd)); +} + +TEST_F(ResultLedgerTest, SaveFailureListExpandsWildcardInsideStripBlock) { + // The expansion takes the wildcard's place and widens the block's column + // only. + CreateFailureList(absl::Substitute(R"( +aaa # aaa +$0 +b.*.b # bbb +ccc # ccc +$1 +)", + kStripBegin, kStripEnd)); + ResultLedger ledger; + ASSERT_THAT(ledger.LoadFailureList(failure_list()), IsOk()); + ASSERT_THAT(ledger.ReportFailure("aaa", kP0, "aaa"), IsOk()); + ASSERT_THAT(ledger.ReportFailure("b.long.b", kP0, "bbb"), IsOk()); + ASSERT_THAT(ledger.ReportSuccess("b.x.b"), Not(IsOk())); + ASSERT_THAT(ledger.ReportFailure("ccc", kP0, "ccc"), IsOk()); + ASSERT_THAT(ledger.Finalize(), IsOk()); + + EXPECT_THAT(ledger.SaveFailureList(absl::StrCat(failure_list(), ".new")), + IsOk()); + + std::string content; + ASSERT_THAT(File::GetContents(absl::StrCat(failure_list(), ".new"), &content, + true), + IsOk()); + EXPECT_EQ(content, absl::Substitute(R"( +aaa # aaa +$0 +b.long.b # bbb +ccc # ccc +$1 +)", + kStripBegin, kStripEnd)); +} + +TEST_F(ResultLedgerTest, SaveFailureListDropsPassingEntryInsideStripBlock) { + CreateFailureList(absl::Substitute(R"( +aaa # aaa +$0 +bbb # bbb +ccc # ccc +$1 +)", + kStripBegin, kStripEnd)); + ResultLedger ledger; + ASSERT_THAT(ledger.LoadFailureList(failure_list()), IsOk()); + ASSERT_THAT(ledger.ReportFailure("aaa", kP0, "aaa"), IsOk()); + ASSERT_THAT(ledger.ReportSuccess("bbb"), Not(IsOk())); + ASSERT_THAT(ledger.ReportFailure("ccc", kP0, "ccc"), IsOk()); + ASSERT_THAT(ledger.Finalize(), IsOk()); + + EXPECT_THAT(ledger.SaveFailureList(absl::StrCat(failure_list(), ".new")), + IsOk()); + + std::string content; + ASSERT_THAT(File::GetContents(absl::StrCat(failure_list(), ".new"), &content, + true), + IsOk()); + EXPECT_EQ(content, absl::Substitute(R"( +aaa # aaa +$0 +ccc # ccc +$1 +)", + kStripBegin, kStripEnd)); +} + +TEST_F(ResultLedgerTest, SaveFailureListInsertsNewFailuresOutsideStripBlock) { + // A new failure goes in front of the first entry outside the block that + // sorts after it: `aab` (which sorts before the block), `bbb` (which would + // sort into it) and `yyy` (which sorts after it) all land after the block, + // before `yyz`; `zzz` sorts last and goes at the end. The new names only + // widen the column outside the block. + CreateFailureList(absl::Substitute(R"( +aaa # aaa +$0 +bba # bba +bbc # bbc +$1 +yyz # yyz +)", + kStripBegin, kStripEnd)); + ResultLedger ledger; + ASSERT_THAT(ledger.LoadFailureList(failure_list()), IsOk()); + ASSERT_THAT(ledger.ReportFailure("aaa", kP0, "aaa"), IsOk()); + ASSERT_THAT(ledger.ReportFailure("bba", kP0, "bba"), IsOk()); + ASSERT_THAT(ledger.ReportFailure("bbc", kP0, "bbc"), IsOk()); + ASSERT_THAT(ledger.ReportFailure("yyz", kP0, "yyz"), IsOk()); + ASSERT_THAT(ledger.ReportFailure("aab", kP0, "new"), Not(IsOk())); + ASSERT_THAT(ledger.ReportFailure("bbb", kP0, "new"), Not(IsOk())); + ASSERT_THAT(ledger.ReportFailure("yyy.long", kP0, "new"), Not(IsOk())); + ASSERT_THAT(ledger.ReportFailure("zzz", kP0, "new"), Not(IsOk())); + ASSERT_THAT(ledger.Finalize(), IsOk()); + + EXPECT_THAT(ledger.SaveFailureList(absl::StrCat(failure_list(), ".new")), + IsOk()); + + std::string content; + ASSERT_THAT(File::GetContents(absl::StrCat(failure_list(), ".new"), &content, + true), + IsOk()); + EXPECT_EQ(content, absl::Substitute(R"( +aaa # aaa +$0 +bba # bba +bbc # bbc +$1 +aab # new +bbb # new +yyy.long # new +yyz # yyz +zzz # new +)", + kStripBegin, kStripEnd)); +} + +TEST_F(ResultLedgerTest, + SaveFailureListAppendsNewFailuresAfterTrailingStripBlock) { + CreateFailureList(absl::Substitute(R"( +aaa # aaa +# Keep this block last. +$0 +bbb # bbb +$1 +)", + kStripBegin, kStripEnd)); + ResultLedger ledger; + ASSERT_THAT(ledger.LoadFailureList(failure_list()), IsOk()); + ASSERT_THAT(ledger.ReportFailure("aaa", kP0, "aaa"), IsOk()); + ASSERT_THAT(ledger.ReportFailure("bbb", kP0, "bbb"), IsOk()); + ASSERT_THAT(ledger.ReportFailure("abc", kP0, "new"), Not(IsOk())); + ASSERT_THAT(ledger.Finalize(), IsOk()); + + EXPECT_THAT(ledger.SaveFailureList(absl::StrCat(failure_list(), ".new")), + IsOk()); + + std::string content; + ASSERT_THAT(File::GetContents(absl::StrCat(failure_list(), ".new"), &content, + true), + IsOk()); + EXPECT_EQ(content, absl::Substitute(R"( +aaa # aaa +# Keep this block last. +$0 +bbb # bbb +$1 +abc # new +)", + kStripBegin, kStripEnd)); +} + +TEST_F(ResultLedgerTest, + SaveFailureListTreatsUnbalancedStripMarkersAsComments) { + // A strip_end with no open block and a strip_begin with no strip_end after + // it don't delimit anything: everything is one region, aligned together, + // and new failures are inserted by name as usual. + CreateFailureList(absl::Substitute(R"( +aaa # aaa +$1 +ccc.long # ccc +$0 +eee # eee +)", + kStripBegin, kStripEnd)); + ResultLedger ledger; + ASSERT_THAT(ledger.LoadFailureList(failure_list()), IsOk()); + ASSERT_THAT(ledger.ReportFailure("aaa", kP0, "aaa"), IsOk()); + ASSERT_THAT(ledger.ReportFailure("ccc.long", kP0, "ccc"), IsOk()); + ASSERT_THAT(ledger.ReportFailure("eee", kP0, "eee"), IsOk()); + ASSERT_THAT(ledger.ReportFailure("bbb", kP0, "new"), Not(IsOk())); + ASSERT_THAT(ledger.ReportFailure("ddd", kP0, "new"), Not(IsOk())); + ASSERT_THAT(ledger.Finalize(), IsOk()); + + EXPECT_THAT(ledger.SaveFailureList(absl::StrCat(failure_list(), ".new")), + IsOk()); + + std::string content; + ASSERT_THAT(File::GetContents(absl::StrCat(failure_list(), ".new"), &content, + true), + IsOk()); + EXPECT_EQ(content, absl::Substitute(R"( +aaa # aaa +$1 +bbb # new +ccc.long # ccc +$0 +ddd # new +eee # eee +)", + kStripBegin, kStripEnd)); +} + +TEST_F(ResultLedgerTest, SaveFailureListIgnoresNestedStripBegin) { + // A second strip_begin inside a block is a plain comment; the block ends at + // the first strip_end, and the second strip_end is a plain comment too. + CreateFailureList(absl::Substitute(R"( +$0 +aaa.long # aaa +$0 +bbb # bbb +$1 +ccc # ccc +$1 +)", + kStripBegin, kStripEnd)); + ResultLedger ledger; + ASSERT_THAT(ledger.LoadFailureList(failure_list()), IsOk()); + ASSERT_THAT(ledger.ReportFailure("aaa.long", kP0, "aaa"), IsOk()); + ASSERT_THAT(ledger.ReportFailure("bbb", kP0, "bbb"), IsOk()); + ASSERT_THAT(ledger.ReportFailure("ccc", kP0, "ccc"), IsOk()); + ASSERT_THAT(ledger.ReportFailure("bba", kP0, "new"), Not(IsOk())); + ASSERT_THAT(ledger.Finalize(), IsOk()); + + EXPECT_THAT(ledger.SaveFailureList(absl::StrCat(failure_list(), ".new")), + IsOk()); + + std::string content; + ASSERT_THAT(File::GetContents(absl::StrCat(failure_list(), ".new"), &content, + true), + IsOk()); + EXPECT_EQ(content, absl::Substitute(R"( +$0 +aaa.long # aaa +$0 +bbb # bbb +$1 +bba # new +ccc # ccc +$1 +)", + kStripBegin, kStripEnd)); +} + +} // namespace +} // namespace internal +} // namespace conformance +} // namespace protobuf +} // namespace google
diff --git a/conformance/result_listener.cc b/conformance/result_listener.cc new file mode 100644 index 0000000..628c8ee --- /dev/null +++ b/conformance/result_listener.cc
@@ -0,0 +1,130 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2026 Google LLC. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +#include "conformance/result_listener.h" + +#include <iterator> +#include <string> +#include <utility> + +#include <gtest/gtest.h> +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "conformance/result_ledger.h" +#include "conformance/result_record.h" + +namespace google { +namespace protobuf { +namespace conformance { +namespace internal { + +Statistics Statistics::From(const ResultLedger& ledger) { + Statistics statistics; + statistics.skipped_tests = ledger.skipped(); + statistics.listed_skips = ledger.listed_skips(); + statistics.tolerated_failures = ledger.tolerated_failures(); + statistics.expected_failures = ledger.expected_failures(); + statistics.unexpected_failures = ledger.unexpected_failures(); + statistics.expected_successes = ledger.expected_successes(); + statistics.unexpected_successes = ledger.unexpected_successes(); + return statistics; +} + +Statistics Statistics::operator-(const Statistics& other) const { + Statistics delta; + delta.skipped_tests = skipped_tests - other.skipped_tests; + delta.listed_skips = listed_skips - other.listed_skips; + delta.tolerated_failures = tolerated_failures - other.tolerated_failures; + delta.expected_failures = expected_failures - other.expected_failures; + delta.unexpected_failures = unexpected_failures - other.unexpected_failures; + delta.expected_successes = expected_successes - other.expected_successes; + delta.unexpected_successes = + unexpected_successes - other.unexpected_successes; + return delta; +} + +Statistics& Statistics::operator+=(const Statistics& other) { + skipped_tests += other.skipped_tests; + listed_skips += other.listed_skips; + tolerated_failures += other.tolerated_failures; + expected_failures += other.expected_failures; + unexpected_failures += other.unexpected_failures; + expected_successes += other.expected_successes; + unexpected_successes += other.unexpected_successes; + return *this; +} + +void Statistics::RecordProperties() const { + testing::Test::RecordProperty("skipped_tests", skipped_tests); + testing::Test::RecordProperty("listed_skips", listed_skips); + testing::Test::RecordProperty("tolerated_failures", tolerated_failures); + testing::Test::RecordProperty("expected_failures", expected_failures); + testing::Test::RecordProperty("unexpected_failures", unexpected_failures); + testing::Test::RecordProperty("expected_successes", expected_successes); + testing::Test::RecordProperty("unexpected_successes", unexpected_successes); +} + +void ResultListener::OnTestSuiteStart(const testing::TestSuite& /*suite*/) { + suite_start_ = Statistics::From(*ledger_); +} + +void ResultListener::OnTestStart(const testing::TestInfo& /*test_info*/) { + // Results checked in SetUpTestSuite() are the suite's, not this test's: + // hold them back until the suite ends. + suite_results_.insert(suite_results_.end(), + std::make_move_iterator(recorded_results_.begin()), + std::make_move_iterator(recorded_results_.end())); + recorded_results_.clear(); + test_start_ = Statistics::From(*ledger_); +} + +void ResultListener::OnTestPartResult(const testing::TestPartResult& part) { + if (part.type() != testing::TestPartResult::kSuccess) return; + absl::StatusOr<RecordedResult> recorded = + ParseResultRecordMessage(part.message()); + if (absl::IsNotFound(recorded.status())) return; // An ordinary success. + if (!recorded.ok()) { + undecodable_results_.emplace_back(recorded.status().message()); + return; + } + // The first outcome recorded for a test is the one that counts. + if (ledger_->WasReported(recorded->test_name)) return; + ledger_->Report(recorded->test_name, recorded->record); + recorded_results_.push_back(*std::move(recorded)); +} + +void ResultListener::OnTestEnd(const testing::TestInfo& /*test_info*/) { + ReportRecordedResults(); + (Statistics::From(*ledger_) - test_start_).RecordProperties(); +} + +void ResultListener::OnTestSuiteEnd(const testing::TestSuite& /*suite*/) { + // What SetUpTestSuite() checked, then what TearDownTestSuite() did. + recorded_results_.insert(recorded_results_.begin(), + std::make_move_iterator(suite_results_.begin()), + std::make_move_iterator(suite_results_.end())); + suite_results_.clear(); + ReportRecordedResults(); + (Statistics::From(*ledger_) - suite_start_).RecordProperties(); +} + +void ResultListener::ReportRecordedResults() { + for (const RecordedResult& recorded : recorded_results_) { + testing::Test::RecordProperty(recorded.test_name, + recorded.record.ToString()); + } + recorded_results_.clear(); + for (const std::string& error : undecodable_results_) { + ADD_FAILURE() << error << "\nThe outcome it recorded is lost."; + } + undecodable_results_.clear(); +} + +} // namespace internal +} // namespace conformance +} // namespace protobuf +} // namespace google
diff --git a/conformance/result_listener.h b/conformance/result_listener.h new file mode 100644 index 0000000..6048b70 --- /dev/null +++ b/conformance/result_listener.h
@@ -0,0 +1,104 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2026 Google LLC. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +#ifndef GOOGLE_PROTOBUF_CONFORMANCE_RESULT_LISTENER_H__ +#define GOOGLE_PROTOBUF_CONFORMANCE_RESULT_LISTENER_H__ + +#include <string> +#include <vector> + +#include <gtest/gtest.h> +#include "absl/base/nullability.h" +#include "conformance/result_ledger.h" +#include "conformance/result_record.h" + +namespace google { +namespace protobuf { +namespace conformance { +namespace internal { + +// A snapshot of the ResultLedger's counters. The ResultListener reports the +// per-test and per-suite deltas, and the environment the run's total, as test +// properties. +struct Statistics { + int skipped_tests = 0; + int listed_skips = 0; + int tolerated_failures = 0; + int expected_failures = 0; + int unexpected_failures = 0; + int expected_successes = 0; + int unexpected_successes = 0; + + static Statistics From(const ResultLedger& ledger); + Statistics operator-(const Statistics& other) const; + Statistics& operator+=(const Statistics& other); + + // Records each statistic as a gtest property of the current test (or suite, + // or run) under the names skipped_tests, listed_skips, + // tolerated_failures, expected_failures, unexpected_failures, + // expected_successes and unexpected_successes. + void RecordProperties() const; +}; + +// How the conformance test environment hears what every conformance test did. +// Yields() (matchers.h) records each outcome as a gtest success in the running +// test (see ResultRecordMessage() in result_record.h). This listener picks +// those out of the test part results as gtest reports them and tallies them in +// the ResultLedger at once. At the end of every gtest test and test suite it +// records the outcomes checked there as properties named after the conformance +// tests (see ReportRecordedResults()) and the statistics the test or suite +// added as its properties. gtest still attributes properties to the test at +// that point (it forgets the current test only after the listeners ran), and +// likewise for the suite. +// +// ConformanceEnvironment (test_environment.h) registers one with gtest. The +// listener doesn't depend on the environment, so unit tests drive it directly. +class ResultListener : public testing::EmptyTestEventListener { + public: + explicit ResultListener(ResultLedger* absl_nonnull ledger) + : ledger_(ledger) {} + + void OnTestSuiteStart(const testing::TestSuite& suite) override; + void OnTestStart(const testing::TestInfo& test_info) override; + // gtest holds its lock while the listeners see a test part result, so this + // must not call back into it; it only tallies. + void OnTestPartResult(const testing::TestPartResult& part) override; + void OnTestEnd(const testing::TestInfo& test_info) override; + void OnTestSuiteEnd(const testing::TestSuite& suite) override; + + // Records the outcome of every conformance test checked since the last call + // as a property of the current gtest test (or suite, or run) named after the + // conformance test, with ResultRecord::ToString() as the value, so that the + // reports can tell what each test did. The outcomes themselves were tallied + // in the ledger as they were checked. Also fails the current test for every + // gtest success that claimed to carry an outcome but couldn't be decoded + // (see ParseResultRecordMessage() in result_record.h), so that no outcome + // goes missing quietly. Runs when a test or a suite ends, and from + // ConformanceEnvironment::TearDown() for anything checked outside a test + // suite. + void ReportRecordedResults(); + + private: + ResultLedger* const absl_nonnull ledger_; + // The counters when the current suite and test started. + Statistics suite_start_; + Statistics test_start_; + // The outcomes checked since ReportRecordedResults() last ran, in order, and + // the gtest successes that claimed to carry an outcome but couldn't be + // decoded. + std::vector<RecordedResult> recorded_results_; + std::vector<std::string> undecodable_results_; + // The results checked in SetUpTestSuite(), held back while the tests run. + std::vector<RecordedResult> suite_results_; +}; + +} // namespace internal +} // namespace conformance +} // namespace protobuf +} // namespace google + +#endif // GOOGLE_PROTOBUF_CONFORMANCE_RESULT_LISTENER_H__
diff --git a/conformance/result_listener_test.cc b/conformance/result_listener_test.cc new file mode 100644 index 0000000..6b2e5c2 --- /dev/null +++ b/conformance/result_listener_test.cc
@@ -0,0 +1,189 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2026 Google LLC. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +#include "conformance/result_listener.h" + +#include <string> +#include <utility> +#include <vector> + +#include <gmock/gmock.h> +#include <gtest/gtest-spi.h> +#include <gtest/gtest.h> +#include "absl/strings/str_cat.h" +#include "absl/strings/string_view.h" +#include "conformance/result_ledger.h" +#include "conformance/result_record.h" +#include "conformance/testee.h" + +namespace google { +namespace protobuf { +namespace conformance { +namespace internal { +namespace { + +using ::testing::AllOf; +using ::testing::Contains; +using ::testing::ElementsAre; +using ::testing::IsEmpty; +using ::testing::Key; +using ::testing::Not; +using ::testing::Pair; + +constexpr absl::string_view kFooTestName = + "Required.Proto3.ProtobufInput.Foo.ProtobufOutput"; +constexpr absl::string_view kSetUpTestName = + "Required.Proto3.ProtobufInput.SetUpTestSuite.ProtobufOutput"; +constexpr absl::string_view kTearDownTestName = + "Required.Proto3.ProtobufInput.TearDownTestSuite.ProtobufOutput"; + +ResultRecord Pass() { return {kP0, ResultRecord::Status::kPass, ""}; } +ResultRecord Fail() { return {kP0, ResultRecord::Status::kFail, "boom"}; } + +// A gtest success with `message`, as SUCCEED() << message reports it to the +// test event listeners. +testing::TestPartResult Success(absl::string_view message) { + return testing::TestPartResult(testing::TestPartResult::kSuccess, __FILE__, + __LINE__, + absl::StrCat("Succeeded\n", message).c_str()); +} + +// The success Yields() reports when it records `record` for `test_name` (see +// ResultRecordMessage() in result_record.h). +testing::TestPartResult RecordedSuccess(absl::string_view test_name, + const ResultRecord& record) { + return Success(ResultRecordMessage(test_name, record)); +} + +// Drives a listener by hand, with the gtest test and suite it hears about +// being this one. Whatever it records lands on the running test, including +// what it records when told that the suite ended. +class ResultListenerTest : public testing::Test { + protected: + ~ResultListenerTest() override { + // ResultLedger insists on being finalized before destruction. + ledger_.Finalize().IgnoreError(); + } + + static const testing::TestSuite& CurrentSuite() { + return *testing::UnitTest::GetInstance()->current_test_suite(); + } + static const testing::TestInfo& CurrentTest() { + return *testing::UnitTest::GetInstance()->current_test_info(); + } + + // The properties of the running test so far, as (key, value) pairs. + static std::vector<std::pair<std::string, std::string>> Properties() { + const testing::TestResult& result = *CurrentTest().result(); + std::vector<std::pair<std::string, std::string>> properties; + for (int i = 0; i < result.test_property_count(); ++i) { + const testing::TestProperty& property = result.GetTestProperty(i); + properties.emplace_back(property.key(), property.value()); + } + return properties; + } + + // The outcomes among Properties(): those whose value is a ResultRecord. + static std::vector<std::pair<std::string, std::string>> Outcomes() { + std::vector<std::pair<std::string, std::string>> outcomes; + for (const auto& property : Properties()) { + if (ResultRecord::Parse(property.second).has_value()) { + outcomes.push_back(property); + } + } + return outcomes; + } + + ResultLedger ledger_; + ResultListener listener_{&ledger_}; +}; + +TEST_F(ResultListenerTest, TalliesAtOnceAndReportsWhenTheTestEnds) { + listener_.OnTestSuiteStart(CurrentSuite()); + listener_.OnTestStart(CurrentTest()); + + listener_.OnTestPartResult(RecordedSuccess(kFooTestName, Pass())); + EXPECT_EQ(ledger_.expected_successes(), 1); + EXPECT_THAT(Properties(), IsEmpty()); + + listener_.OnTestEnd(CurrentTest()); + EXPECT_THAT(Properties(), AllOf(Contains(Pair(kFooTestName, "P0 PASS")), + Contains(Pair("expected_successes", "1")), + Contains(Pair("unexpected_failures", "0")))); +} + +TEST_F(ResultListenerTest, TheFirstOutcomeOfATestCounts) { + listener_.OnTestStart(CurrentTest()); + listener_.OnTestPartResult(RecordedSuccess(kFooTestName, Fail())); + listener_.OnTestPartResult(RecordedSuccess(kFooTestName, Pass())); + EXPECT_EQ(ledger_.unexpected_failures(), 1); + EXPECT_EQ(ledger_.expected_successes(), 0); + + listener_.OnTestEnd(CurrentTest()); + EXPECT_THAT(Outcomes(), ElementsAre(Pair(kFooTestName, "P0 FAIL: boom"))); +} + +TEST_F(ResultListenerTest, IgnoresOtherTestPartResults) { + listener_.OnTestStart(CurrentTest()); + // A success that merely looks like a record, and a failure that carries one. + listener_.OnTestPartResult(Success("P0 FAIL: not a record")); + listener_.OnTestPartResult(testing::TestPartResult( + testing::TestPartResult::kNonFatalFailure, __FILE__, __LINE__, + ResultRecordMessage(kFooTestName, Fail()).c_str())); + listener_.OnTestEnd(CurrentTest()); + EXPECT_EQ(ledger_.unexpected_failures(), 0); + EXPECT_THAT(Outcomes(), IsEmpty()); +} + +TEST_F(ResultListenerTest, ReportsRecordedResultsOnRequest) { + // What ConformanceEnvironment::TearDown() does for the results checked + // outside any test suite. + listener_.OnTestPartResult(RecordedSuccess(kFooTestName, Fail())); + EXPECT_THAT(Properties(), Not(Contains(Key(kFooTestName)))); + listener_.ReportRecordedResults(); + EXPECT_THAT(Properties(), Contains(Pair(kFooTestName, "P0 FAIL: boom"))); +} + +TEST_F(ResultListenerTest, UndecodableRecordFailsTheTest) { + // A success marked as a record that can't be read is a bug somewhere, and + // the outcome it carried is lost: the test fails rather than staying quiet. + listener_.OnTestPartResult( + Success(absl::StrCat(kResultRecordMarker, "\tnot a record"))); + EXPECT_NONFATAL_FAILURE(listener_.ReportRecordedResults(), + "Can't decode the conformance result record"); + // Once. + listener_.ReportRecordedResults(); +} + +TEST_F(ResultListenerTest, HoldsSetUpTestSuiteResultsBackUntilTheSuiteEnds) { + listener_.OnTestSuiteStart(CurrentSuite()); + listener_.OnTestPartResult(RecordedSuccess(kSetUpTestName, Pass())); + + listener_.OnTestStart(CurrentTest()); + listener_.OnTestPartResult(RecordedSuccess(kFooTestName, Fail())); + listener_.OnTestEnd(CurrentTest()); + // The test reports its own outcome and statistics only. + EXPECT_THAT(Outcomes(), ElementsAre(Pair(kFooTestName, "P0 FAIL: boom"))); + EXPECT_THAT(Properties(), AllOf(Contains(Pair("expected_successes", "0")), + Contains(Pair("unexpected_failures", "1")))); + + listener_.OnTestPartResult(RecordedSuccess(kTearDownTestName, Pass())); + listener_.OnTestSuiteEnd(CurrentSuite()); + // The suite reports what SetUpTestSuite() checked, then what + // TearDownTestSuite() did, and the statistics of the whole suite. + EXPECT_THAT(Outcomes(), ElementsAre(Pair(kFooTestName, "P0 FAIL: boom"), + Pair(kSetUpTestName, "P0 PASS"), + Pair(kTearDownTestName, "P0 PASS"))); + EXPECT_THAT(Properties(), AllOf(Contains(Pair("expected_successes", "2")), + Contains(Pair("unexpected_failures", "1")))); +} + +} // namespace +} // namespace internal +} // namespace conformance +} // namespace protobuf +} // namespace google
diff --git a/conformance/result_record.cc b/conformance/result_record.cc new file mode 100644 index 0000000..216b846 --- /dev/null +++ b/conformance/result_record.cc
@@ -0,0 +1,109 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2026 Google LLC. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +#include "conformance/result_record.h" + +#include <iterator> +#include <string> +#include <utility> +#include <vector> + +#include "absl/algorithm/container.h" +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "absl/strings/ascii.h" +#include "absl/strings/escaping.h" +#include "absl/strings/match.h" +#include "absl/strings/str_cat.h" +#include "absl/strings/str_split.h" +#include "absl/strings/string_view.h" +#include "absl/strings/strip.h" +#include "absl/types/optional.h" +#include "conformance/testee.h" + +namespace google { +namespace protobuf { +namespace conformance { +namespace internal { +namespace { + +// The status words of ResultRecord::ToString(), indexed by Status. +constexpr absl::string_view kStatusNames[] = {"PASS", "FAIL", "CRASH", "SKIP", + "NOT_SELECTED"}; + +} // namespace + +std::string ResultRecord::ToString() const { + std::string value = absl::StrCat(PriorityName(priority), " ", + kStatusNames[static_cast<int>(status)]); + absl::string_view stripped = absl::StripTrailingAsciiWhitespace(message); + if (!stripped.empty()) absl::StrAppend(&value, ": ", stripped); + return value; +} + +absl::optional<ResultRecord> ResultRecord::Parse(absl::string_view value) { + ResultRecord record; + bool found = false; + for (TestPriority priority : {kP0, kP1}) { + if (absl::ConsumePrefix(&value, + absl::StrCat(PriorityName(priority), " "))) { + record.priority = priority; + found = true; + break; + } + } + if (!found) return absl::nullopt; + absl::string_view status = value.substr(0, value.find(':')); + const absl::string_view* name = absl::c_find(kStatusNames, status); + if (name == std::end(kStatusNames)) return absl::nullopt; + record.status = static_cast<Status>(name - std::begin(kStatusNames)); + value.remove_prefix(status.size()); + if (!value.empty() && !absl::ConsumePrefix(&value, ": ")) { + return absl::nullopt; + } + record.message = std::string(value); + return record; +} + +std::string ResultRecordMessage(absl::string_view test_name, + const ResultRecord& record) { + return absl::StrCat(kResultRecordMarker, "\t", absl::CEscape(test_name), "\t", + absl::CEscape(record.ToString())); +} + +absl::StatusOr<RecordedResult> ParseResultRecordMessage( + absl::string_view message) { + for (absl::string_view line : absl::StrSplit(message, '\n')) { + if (!absl::StartsWith(line, kResultRecordMarker)) continue; + auto invalid = [line] { + return absl::InvalidArgumentError( + absl::StrCat("Can't decode the conformance result record in a gtest " + "success: ", + line)); + }; + std::vector<absl::string_view> fields = absl::StrSplit(line, '\t'); + if (fields.size() != 3 || fields[0] != kResultRecordMarker) { + return invalid(); + } + RecordedResult result; + std::string value; + if (!absl::CUnescape(fields[1], &result.test_name) || + !absl::CUnescape(fields[2], &value)) { + return invalid(); + } + absl::optional<ResultRecord> record = ResultRecord::Parse(value); + if (!record.has_value()) return invalid(); + result.record = *std::move(record); + return result; + } + return absl::NotFoundError("Not a conformance result record."); +} + +} // namespace internal +} // namespace conformance +} // namespace protobuf +} // namespace google
diff --git a/conformance/result_record.h b/conformance/result_record.h new file mode 100644 index 0000000..50ae98c --- /dev/null +++ b/conformance/result_record.h
@@ -0,0 +1,86 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2026 Google LLC. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +#ifndef GOOGLE_PROTOBUF_CONFORMANCE_RESULT_RECORD_H__ +#define GOOGLE_PROTOBUF_CONFORMANCE_RESULT_RECORD_H__ + +#include <string> + +#include "absl/status/statusor.h" +#include "absl/strings/string_view.h" +#include "absl/types/optional.h" +#include "conformance/testee.h" + +namespace google { +namespace protobuf { +namespace conformance { +namespace internal { + +// The outcome of one conformance test, as Yields() (matchers.h) records it +// when it checks the test's result and as the test environment tallies it +// (see ResultLedger::Report() in result_ledger.h) and reports it, as a gtest +// property named after the test whose value is ToString(), e.g. "P0 PASS" or +// "P1 FAIL: Failed to parse input.". +struct ResultRecord { + enum class Status { + kPass, // The result matched the expectation. + kFail, // It didn't. + kCrash, // The testee hit a runtime error or timed out. + kSkip, // The testee declined to run the test. + kNotSelected, // The runner filtered the test out, so it never ran. + }; + + TestPriority priority; + Status status; + // The failure message or skip reason, if any. + std::string message; + + // "<priority> <status>[: <message>]": the priority as PriorityName() spells + // it, the status as PASS, FAIL, CRASH, SKIP or NOT_SELECTED, and the message + // stripped of trailing whitespace and left out when empty. + std::string ToString() const; + + // The inverse of ToString(). Returns nullopt for a value in any other + // form. + static absl::optional<ResultRecord> Parse(absl::string_view value); +}; + +// How Yields() hands a record to the test environment: as the message of a +// gtest success (SUCCEED()) in the running test. gtest prints nothing for a +// success and reports it to the test event listeners at once, so the +// environment's listener sees the record as soon as the result is checked, +// while the record stays with the gtest result it belongs to. +// +// The first line that starts with kResultRecordMarker carries the record: +// the marker, the test name and ToString(), tab-separated and C-escaped onto +// that one line, so that what gtest appends to the message (SCOPED_TRACE +// output) doesn't get in the way. +inline constexpr absl::string_view kResultRecordMarker = "[conformance result]"; + +std::string ResultRecordMessage(absl::string_view test_name, + const ResultRecord& record); + +// A record as ParseResultRecordMessage() finds it. +struct RecordedResult { + std::string test_name; + ResultRecord record; +}; + +// The inverse of ResultRecordMessage(): the record in `message`, the message +// of a test part result. Returns kNotFound if there is none, that is if the +// part is an ordinary success, and kInvalidArgument if the message claims to +// carry a record that can't be decoded. The test environment fails the test +// over the latter, so that no outcome goes missing quietly. +absl::StatusOr<RecordedResult> ParseResultRecordMessage( + absl::string_view message); + +} // namespace internal +} // namespace conformance +} // namespace protobuf +} // namespace google + +#endif // GOOGLE_PROTOBUF_CONFORMANCE_RESULT_RECORD_H__
diff --git a/conformance/result_record_test.cc b/conformance/result_record_test.cc new file mode 100644 index 0000000..987078d --- /dev/null +++ b/conformance/result_record_test.cc
@@ -0,0 +1,153 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2026 Google LLC. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +#include "conformance/result_record.h" + +#include <string> + +#include <gmock/gmock.h> +#include <gtest/gtest.h> +#include "absl/status/status.h" +#include "absl/status/status_matchers.h" +#include "absl/strings/str_cat.h" +#include "absl/strings/string_view.h" +#include "absl/types/optional.h" +#include "conformance/testee.h" + +namespace google { +namespace protobuf { +namespace conformance { +namespace internal { +namespace { + +using ::absl_testing::IsOkAndHolds; +using ::absl_testing::StatusIs; +using ::testing::FieldsAre; +using ::testing::HasSubstr; +using ::testing::Not; +using ::testing::Optional; +using ::testing::Property; +using ::testing::StartsWith; + +TEST(ResultRecordTest, ToString) { + EXPECT_EQ((ResultRecord{kP0, ResultRecord::Status::kPass, ""}).ToString(), + "P0 PASS"); + EXPECT_EQ((ResultRecord{kP1, ResultRecord::Status::kFail, "boom"}).ToString(), + "P1 FAIL: boom"); + EXPECT_EQ((ResultRecord{kP0, ResultRecord::Status::kCrash, "x"}).ToString(), + "P0 CRASH: x"); + EXPECT_EQ((ResultRecord{kP0, ResultRecord::Status::kSkip, "why"}).ToString(), + "P0 SKIP: why"); + EXPECT_EQ( + (ResultRecord{kP0, ResultRecord::Status::kNotSelected, ""}).ToString(), + "P0 NOT_SELECTED"); +} + +TEST(ResultRecordTest, ToStringStripsTrailingWhitespace) { + EXPECT_EQ( + (ResultRecord{kP0, ResultRecord::Status::kFail, "a: b \n\n"}).ToString(), + "P0 FAIL: a: b"); + EXPECT_EQ((ResultRecord{kP0, ResultRecord::Status::kFail, " \n"}).ToString(), + "P0 FAIL"); +} + +TEST(ResultRecordTest, ParseInvertsToString) { + for (TestPriority priority : {kP0, kP1}) { + for (ResultRecord::Status status : + {ResultRecord::Status::kPass, ResultRecord::Status::kFail, + ResultRecord::Status::kCrash, ResultRecord::Status::kSkip, + ResultRecord::Status::kNotSelected}) { + for (absl::string_view message : {"", "msg", "a: b: c", "multi\nline"}) { + ResultRecord record{priority, status, std::string(message)}; + EXPECT_THAT(ResultRecord::Parse(record.ToString()), + Optional(FieldsAre(priority, status, message))) + << record.ToString(); + } + } + } +} + +TEST(ResultRecordTest, ParseRejectsOtherValues) { + EXPECT_EQ(ResultRecord::Parse(""), absl::nullopt); + EXPECT_EQ(ResultRecord::Parse("garbage"), absl::nullopt); + EXPECT_EQ(ResultRecord::Parse("P0"), absl::nullopt); + EXPECT_EQ(ResultRecord::Parse("P0 "), absl::nullopt); + EXPECT_EQ(ResultRecord::Parse("P2 PASS"), absl::nullopt); + EXPECT_EQ(ResultRecord::Parse("P0 BOGUS"), absl::nullopt); + EXPECT_EQ(ResultRecord::Parse("P0 pass"), absl::nullopt); + EXPECT_EQ(ResultRecord::Parse("P0 PASS extra"), absl::nullopt); + EXPECT_EQ(ResultRecord::Parse("P0 FAIL:"), absl::nullopt); + EXPECT_EQ(ResultRecord::Parse("P0 FAIL:msg"), absl::nullopt); + EXPECT_EQ(ResultRecord::Parse(" P0 PASS"), absl::nullopt); +} + +TEST(ResultRecordMessageTest, IsOneMarkedLine) { + EXPECT_EQ(ResultRecordMessage("Required.Foo", + {kP0, ResultRecord::Status::kPass, ""}), + absl::StrCat(kResultRecordMarker, "\tRequired.Foo\tP0 PASS")); + // Names and messages are escaped onto the one line. + std::string message = ResultRecordMessage( + "odd\tname", {kP1, ResultRecord::Status::kFail, "two\nlines\twith tab"}); + EXPECT_THAT(message, StartsWith(kResultRecordMarker)); + EXPECT_THAT(message, Not(HasSubstr("\n"))); +} + +TEST(ResultRecordMessageTest, ParseInvertsResultRecordMessage) { + for (absl::string_view name : {"Required.Foo", "a b\tc", "multi\nline"}) { + for (absl::string_view message : + {"", "msg", "a: b: c", "tab\tbed", "multi\nline\n", "quote\"d\\"}) { + ResultRecord record{kP1, ResultRecord::Status::kFail, + std::string(message)}; + EXPECT_THAT(ParseResultRecordMessage(ResultRecordMessage(name, record)), + IsOkAndHolds(FieldsAre(name, Property(&ResultRecord::ToString, + record.ToString())))) + << ResultRecordMessage(name, record); + } + } +} + +TEST(ResultRecordMessageTest, ParseFindsTheRecordAmongGtestsOwnLines) { + // SUCCEED() prefixes its message and appends SCOPED_TRACE() context. + ResultRecord record{kP0, ResultRecord::Status::kSkip, "not supported"}; + EXPECT_THAT( + ParseResultRecordMessage(absl::StrCat( + "Succeeded\n", ResultRecordMessage("Required.Foo", record), + "\nGoogle Test trace:\nfoo_test.cc:12: P0 PASS looks like one")), + IsOkAndHolds(FieldsAre( + "Required.Foo", + Property(&ResultRecord::ToString, "P0 SKIP: not supported")))); +} + +TEST(ResultRecordMessageTest, ParseTellsOrdinarySuccessesApart) { + EXPECT_THAT(ParseResultRecordMessage(""), + StatusIs(absl::StatusCode::kNotFound)); + EXPECT_THAT(ParseResultRecordMessage("Succeeded"), + StatusIs(absl::StatusCode::kNotFound)); + EXPECT_THAT(ParseResultRecordMessage("Succeeded\nP0 PASS"), + StatusIs(absl::StatusCode::kNotFound)); + EXPECT_THAT(ParseResultRecordMessage( + absl::StrCat("Succeeded\n ", kResultRecordMarker, "\ta\tb")), + StatusIs(absl::StatusCode::kNotFound)); +} + +TEST(ResultRecordMessageTest, ParseRejectsAMarkedLineItCantDecode) { + for (absl::string_view rest : + {"", "\tnot a record", "\tname\tP0 BOGUS", "\tname\tP0 PASS\textra", + "x\tname\tP0 PASS", "\tbad\\escape\tP0 PASS"}) { + EXPECT_THAT( + ParseResultRecordMessage(absl::StrCat(kResultRecordMarker, rest)), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Can't decode the conformance result record"))) + << rest; + } +} + +} // namespace +} // namespace internal +} // namespace conformance +} // namespace protobuf +} // namespace google
diff --git a/conformance/sample_conformance_test.cc b/conformance/sample_conformance_test.cc new file mode 100644 index 0000000..e38c32a --- /dev/null +++ b/conformance/sample_conformance_test.cc
@@ -0,0 +1,80 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2026 Google LLC. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +// A tiny conformance suite used to exercise the gtest conformance framework end +// to end against the real C++ testee. Some of these tests fail on purpose; see +// the sample_failure_list_*.txt files and test_environment_integration_test.cc. + +#include <gmock/gmock.h> +#include <gtest/gtest.h> +#include "conformance/binary_wireformat.h" +#include "conformance/matchers.h" +#include "conformance/test_fixture.h" +#include "conformance/testee.h" +#include "google/protobuf/test_messages_proto3.pb.h" + +namespace google { +namespace protobuf { +namespace conformance { +namespace { + +using ::protobuf_test_messages::proto3::TestAllTypesProto3; + +using SampleConformanceTest = ConformanceTest; + +TEST_F(SampleConformanceTest, SamplePass) { + EXPECT_THAT( + Testee() + .ParseBinary(TestAllTypesProto3::descriptor(), VarintField(1, 99)) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(R"pb(optional_int32: 99)pb")))); +} + +// Valid input is expected to parse, so this always fails against a conforming +// testee. +TEST_F(SampleConformanceTest, SampleDeliberateFailure) { + EXPECT_THAT( + Testee() + .ParseBinary(TestAllTypesProto3::descriptor(), VarintField(1, 99)) + .SerializeBinary(), + Yields(IsParseError())); +} + +// Same as above, but only P1. +TEST_F(SampleConformanceTest, SampleP1Failure) { + EXPECT_THAT( + Testee(kP1) + .ParseBinary(TestAllTypesProto3::descriptor(), VarintField(1, 99)) + .SerializeBinary(), + Yields(IsParseError())); +} + +// Only runs with gtest's also_run_disabled_tests flag. Its presence must not +// make a normal run count as partial (see IsPartialRun() in +// test_environment.cc). +TEST_F(SampleConformanceTest, DISABLED_SampleDisabled) { + EXPECT_THAT( + Testee() + .ParseBinary(TestAllTypesProto3::descriptor(), VarintField(1, 99)) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(R"pb(optional_int32: 99)pb")))); +} + +// Runs a conformance test but never checks its result with Yields(), which +// fails the test. Only runs when asked for by name (see +// test_environment_integration_test.cc), so that the other runs stay clean. +TEST_F(SampleConformanceTest, DISABLED_SampleUncheckedResult) { + auto unchecked = + Testee("SampleUncheckedResult") + .ParseBinary(TestAllTypesProto3::descriptor(), VarintField(1, 99)) + .SerializeBinary(); +} + +} // namespace +} // namespace conformance +} // namespace protobuf +} // namespace google
diff --git a/conformance/sample_failure_list_empty.txt b/conformance/sample_failure_list_empty.txt new file mode 100644 index 0000000..a4f32fb --- /dev/null +++ b/conformance/sample_failure_list_empty.txt
@@ -0,0 +1,2 @@ +# Intentionally empty: every deliberate failure in sample_conformance_test is +# unexpected with this list.
diff --git a/conformance/sample_failure_list_ok.txt b/conformance/sample_failure_list_ok.txt new file mode 100644 index 0000000..d2429a2 --- /dev/null +++ b/conformance/sample_failure_list_ok.txt
@@ -0,0 +1,3 @@ +# Expected failures of sample_conformance_test. These tests fail on purpose. +Recommended.Proto3.ProtobufInput.SampleP1Failure.ProtobufOutput # Should have failed to parse, but didn't. +Required.Proto3.ProtobufInput.SampleDeliberateFailure.ProtobufOutput # Should have failed to parse, but didn't.
diff --git a/conformance/sample_failure_list_unseen.txt b/conformance/sample_failure_list_unseen.txt new file mode 100644 index 0000000..1ff44a8 --- /dev/null +++ b/conformance/sample_failure_list_unseen.txt
@@ -0,0 +1,4 @@ +# Like sample_failure_list_ok.txt, plus an entry for a test that never runs. +Recommended.Proto3.ProtobufInput.SampleP1Failure.ProtobufOutput # Should have failed to parse, but didn't. +Required.Proto3.ProtobufInput.SampleDeliberateFailure.ProtobufOutput # Should have failed to parse, but didn't. +Required.Proto3.ProtobufInput.SampleNeverRuns.ProtobufOutput # This test doesn't exist.
diff --git a/conformance/test_environment.cc b/conformance/test_environment.cc index 98c69ef..45a5915 100644 --- a/conformance/test_environment.cc +++ b/conformance/test_environment.cc
@@ -7,6 +7,12 @@ #include "conformance/test_environment.h" +#ifdef _WIN32 +#include <direct.h> // for _getcwd +#else +#include <unistd.h> // for getcwd +#endif + #include <algorithm> #include <cstdlib> #include <memory> @@ -24,9 +30,12 @@ #include "absl/strings/str_cat.h" #include "absl/strings/string_view.h" #include "absl/types/optional.h" +#include "absl/types/span.h" +#include "conformance/failure_list.h" #include "conformance/fork_pipe_runner.h" #include "conformance/global_test_environment.h" -#include "conformance/test_manager.h" +#include "conformance/result_ledger.h" +#include "conformance/result_listener.h" #include "conformance/test_runner.h" #include "conformance/testee.h" @@ -36,6 +45,21 @@ namespace internal { namespace { +// The current working directory, or "" if it can't be determined. +std::string CurrentWorkingDirectory() { + // Both glibc/macOS (as an extension) and the MSVC CRT malloc() a buffer of + // the right size when given a null one. +#ifdef _WIN32 + char* cwd = _getcwd(nullptr, 0); +#else + char* cwd = getcwd(nullptr, 0); +#endif + if (cwd == nullptr) return ""; + std::string result = cwd; + std::free(cwd); + return result; +} + // Whether `path` is absolute on this platform. Only paths that start with a // slash are absolute on POSIX; Windows also has drive letters ("C:\\...", // "C:/...") and root-relative backslash paths. @@ -112,54 +136,26 @@ } // namespace -Statistics Statistics::From(const TestManager& manager) { - Statistics statistics; - statistics.skipped_tests = manager.skipped(); - statistics.listed_skips = manager.listed_skips(); - statistics.tolerated_failures = manager.tolerated_failures(); - statistics.expected_failures = manager.expected_failures(); - statistics.unexpected_failures = manager.unexpected_failures(); - statistics.expected_successes = manager.expected_successes(); - statistics.unexpected_successes = manager.unexpected_successes(); - return statistics; -} +// Hooks ReportUncheckedResults() into the end of every gtest test. gtest +// still attributes failures to the test at that point (it forgets the current +// test only after the listeners ran). +class ConformanceEnvironment::UncheckedResultReporter + : public testing::EmptyTestEventListener { + public: + explicit UncheckedResultReporter( + ConformanceEnvironment* absl_nonnull environment) + : environment_(environment) {} -Statistics Statistics::operator-(const Statistics& other) const { - Statistics delta; - delta.skipped_tests = skipped_tests - other.skipped_tests; - delta.listed_skips = listed_skips - other.listed_skips; - delta.tolerated_failures = tolerated_failures - other.tolerated_failures; - delta.expected_failures = expected_failures - other.expected_failures; - delta.unexpected_failures = unexpected_failures - other.unexpected_failures; - delta.expected_successes = expected_successes - other.expected_successes; - delta.unexpected_successes = - unexpected_successes - other.unexpected_successes; - return delta; -} + void OnTestEnd(const testing::TestInfo& /*test_info*/) override { + environment_->ReportUncheckedResults(); + } -Statistics& Statistics::operator+=(const Statistics& other) { - skipped_tests += other.skipped_tests; - listed_skips += other.listed_skips; - tolerated_failures += other.tolerated_failures; - expected_failures += other.expected_failures; - unexpected_failures += other.unexpected_failures; - expected_successes += other.expected_successes; - unexpected_successes += other.unexpected_successes; - return *this; -} + private: + ConformanceEnvironment* const absl_nonnull environment_; +}; -void Statistics::RecordProperties() const { - testing::Test::RecordProperty("skipped_tests", skipped_tests); - testing::Test::RecordProperty("listed_skips", listed_skips); - testing::Test::RecordProperty("tolerated_failures", tolerated_failures); - testing::Test::RecordProperty("expected_failures", expected_failures); - testing::Test::RecordProperty("unexpected_failures", unexpected_failures); - testing::Test::RecordProperty("expected_successes", expected_successes); - testing::Test::RecordProperty("unexpected_successes", unexpected_successes); -} - -TestManager& GetGlobalTestManager() { - return ConformanceEnvironment::Get().test_manager(); +const FailureList& GetGlobalFailureList() { + return ConformanceEnvironment::Get().ledger().failure_list(); } ConformanceEnvironment::ConformanceEnvironment( @@ -167,24 +163,34 @@ : options_(std::move(options)), owned_runner_(TakeOwnedRunner(options_)), testee_(std::make_unique<Testee>( - options_.runner != nullptr ? options_.runner : owned_runner_.get())) { + options_.runner != nullptr ? options_.runner : owned_runner_.get())), + result_listener_(new ResultListener(&ledger_)), + unchecked_result_reporter_(new UncheckedResultReporter(this)) { ABSL_CHECK(global_environment == nullptr) << "A global ConformanceEnvironment is already set."; global_environment = this; - // Anything below proto3 (including the legacy runner's EDITION_UNKNOWN + // Anything below proto3 (including conformance_test_runner's EDITION_UNKNOWN // default) is meaningless, since proto2 and proto3 tests always run. This is // the one place the maximum edition is clamped; see the option's comment. options_.maximum_edition = std::max(options_.maximum_edition, EDITION_PROTO3); - test_manager_.set_enforcement_level(options_.enforcement_level); + ledger_.set_enforcement_level(options_.enforcement_level); + testing::TestEventListeners& listeners = + testing::UnitTest::GetInstance()->listeners(); + listeners.Append(result_listener_); + listeners.Append(unchecked_result_reporter_); } ConformanceEnvironment::~ConformanceEnvironment() { - // TestManager insists on being finalized before destruction. An installed + testing::TestEventListeners& listeners = + testing::UnitTest::GetInstance()->listeners(); + delete listeners.Release(unchecked_result_reporter_); + delete listeners.Release(result_listener_); + // ResultLedger insists on being finalized before destruction. An installed // environment is never destroyed (see Install()), so this only runs for // environments constructed by ScopedGlobalConformanceEnvironment in unit // tests, and those don't necessarily call TearDown(); make sure we don't // crash on the way out. - test_manager_.Finalize().IgnoreError(); + ledger_.Finalize().IgnoreError(); ABSL_CHECK_EQ(global_environment, this); global_environment = nullptr; } @@ -194,12 +200,13 @@ ABSL_CHECK(global_environment == nullptr) << "ConformanceEnvironment::Install() must only be called once."; // gtest deletes the environments it owns at the end of RUN_ALL_TESTS(), but - // callers (e.g. the merged conformance_test_runner) still need the test - // manager afterwards. So although this class is a testing::Environment, it - // isn't registered with gtest itself: gtest only gets a forwarding proxy, - // and the real environment intentionally lives until the process exits. - // Only the environment object leaks, though; TearDown() releases the testee. - // TODO: b/410122039 - register it directly once the legacy suites are gone. + // the merged conformance_test_runner still needs the ledger + // afterwards. So although this class is a testing::Environment, it isn't + // registered with gtest itself: gtest only gets a forwarding proxy, and the + // real environment intentionally lives until the process exits. Only the + // environment object leaks, though; TearDown() releases the testee. + // TODO: register it directly once conformance_test_main.cc no + // longer needs it after RUN_ALL_TESTS() (see the class comment). auto* environment = new ConformanceEnvironment(std::move(options)); testing::AddGlobalTestEnvironment(new ForwardingEnvironment(environment)); return *environment; @@ -209,10 +216,32 @@ ABSL_CHECK(global_environment != nullptr) << "No ConformanceEnvironment has been installed. Conformance test " "binaries must call ConformanceEnvironment::Install() before " - "RUN_ALL_TESTS()."; + "RUN_ALL_TESTS(); the usual way to do that is to depend on the " + "test_environment_main library instead of a generic gtest main."; return *global_environment; } +void ConformanceEnvironment::ReportUncheckedResults() { + if (testee_ == nullptr) { + return; // TearDown() has already run. + } + absl::Span<const std::string> tests_run = testee_->tests_run(); + absl::Span<const std::string> new_tests = + tests_run.subspan(results_reconciled_); + results_reconciled_ = tests_run.size(); + // Don't pile a second failure onto a test that bailed out before it could + // check its results. + if (testing::Test::HasFatalFailure()) { + return; + } + for (const std::string& test_name : new_tests) { + if (!ledger_.WasReported(test_name)) { + ADD_FAILURE() << "TestResult for " << test_name + << " was never checked; wrap the matcher in Yields()"; + } + } +} + Testee& ConformanceEnvironment::testee() { ABSL_CHECK(testee_ != nullptr) << "The testee has already been shut down by " @@ -223,7 +252,7 @@ void ConformanceEnvironment::SetUp() { for (const std::string& failure_list : options_.failure_list_files) { - absl::Status status = test_manager_.LoadFailureList(failure_list); + absl::Status status = ledger_.LoadFailureList(failure_list); ASSERT_TRUE(status.ok()) << "Failed to load failure list " << failure_list << ": " << status; } @@ -231,7 +260,18 @@ } void ConformanceEnvironment::TearDown() { - Statistics::From(test_manager_).RecordProperties(); + // What's left after the last test: results checked outside any test suite + // are only reported here, and results run after the last test (e.g. in a + // TearDownTestSuite()) are only checked here. + result_listener_->ReportRecordedResults(); + ReportUncheckedResults(); + Statistics::From(ledger_).RecordProperties(); + for (const std::string& entry : ledger_.OverexpandedWildcards()) { + ADD_FAILURE() << "The failure list entry " << entry << " matched more than " + << kMaximumWildcardExpansions + << " tests, which hides too much. List the failing tests " + "individually, or narrow the wildcard."; + } const bool partial_run = partial_run_override_.value_or(IsPartialRun()); // The path the failure list was rewritten to, if --fix succeeded. @@ -245,6 +285,7 @@ "run. Rerun with --fix and without the filter."; } else { std::string output_file = options_.fix_output_file; + const char* workspace = std::getenv("BUILD_WORKSPACE_DIRECTORY"); if (output_file.empty()) { if (options_.failure_list_files.empty()) { ADD_FAILURE() << "--fix needs --fix_output_file when no " @@ -254,9 +295,7 @@ "--failure_list is given."; } else if (IsAbsolutePath(options_.failure_list_files[0])) { output_file = options_.failure_list_files[0]; - } else if (const char* workspace = - std::getenv("BUILD_WORKSPACE_DIRECTORY"); - workspace != nullptr) { + } else if (workspace != nullptr) { output_file = absl::StrCat(workspace, "/", options_.failure_list_files[0]); } else { @@ -264,26 +303,55 @@ "$BUILD_WORKSPACE_DIRECTORY (which `bazel run` " "sets) to locate the failure list to rewrite."; } + } else if (!IsAbsolutePath(output_file) && workspace != nullptr) { + // Like a relative --failure_list, a relative --fix_output_file names a + // source file: under `bazel run` the working directory is the + // runfiles tree, so resolve it against the workspace instead. This is + // how conformance_test() (conformance.bzl) lets --fix create a failure + // list that doesn't exist yet. + output_file = absl::StrCat(workspace, "/", output_file); } if (!output_file.empty()) { - absl::Status status = test_manager_.SaveFailureList(output_file); + const bool relative = !IsAbsolutePath(output_file); + absl::Status status = ledger_.SaveFailureList(output_file); if (status.ok()) { - ABSL_LOG(INFO) << "Wrote updated failure list to " << output_file; - fixed_list = output_file; + // Report where the file actually landed: a relative path outside + // `bazel run` is relative to whatever the working directory is. + std::string absolute_path = output_file; + if (relative) { + std::string cwd = CurrentWorkingDirectory(); + if (!cwd.empty()) { + absolute_path = absl::StrCat(cwd, "/", output_file); + } + } + ABSL_LOG(INFO) << "Wrote updated failure list to " << absolute_path; + if (relative && workspace == nullptr && + (std::getenv("TEST_SRCDIR") != nullptr || + std::getenv("TEST_TMPDIR") != nullptr)) { + ABSL_LOG(WARNING) + << "The updated failure list was written inside the test " + "sandbox, not into the source tree, because " + "$BUILD_WORKSPACE_DIRECTORY is unset. To update the " + "checked-in list use `bazel run <target> -- --fix` instead " + "of `bazel test`."; + } + fixed_list = absolute_path; } else { ADD_FAILURE() << "Failed to write updated failure list to " - << output_file << ": " << status; + << output_file << ": " << status + << " (does the directory exist?)"; } } } } - absl::Status status = test_manager_.Finalize(); + absl::Status status = ledger_.Finalize(); if (!status.ok() && set_up_succeeded_ && options_.check_unseen_expected_failures) { if (partial_run) { // Only a subset of the tests ran, so most expected failures were never - // exercised. The legacy runner's --test flag behaves the same way. + // exercised. The conformance_test_runner --test flag behaves the same + // way. ABSL_LOG(INFO) << "Not checking for unseen expected failures because " "only some of the tests ran (e.g. because of " "--gtest_filter or sharding):\n"
diff --git a/conformance/test_environment.h b/conformance/test_environment.h index f66d25a..1c624ec 100644 --- a/conformance/test_environment.h +++ b/conformance/test_environment.h
@@ -8,24 +8,29 @@ // The process-wide plumbing shared by every gtest-based conformance suite. // // ConformanceEnvironment is the process-global state of a conformance test -// binary. It owns the testee connection and the TestManager, records -// statistics, and checks or regenerates the failure list at the end of the -// run. Exactly one is installed per test binary, before RUN_ALL_TESTS(); -// Install() hooks it into gtest. Nothing in this header is meant for the -// conformance suites themselves. +// binary. It owns the testee connection, the ResultLedger and the +// ResultListener that feeds it (see result_ledger.h and result_listener.h), +// records the run's statistics, and checks or regenerates the failure list at +// the end of the run. Exactly one is installed per test binary, normally by +// test_environment_main.cc from command-line flags (see +// test_environment_flags.h). Install() hooks it into gtest. Nothing in this +// header is meant for the conformance suites themselves. // // Everything here is single-threaded. Use it only from gtest's main thread: // test bodies, fixtures and the environment hooks. The global environment is -// a plain pointer and TestManager isn't thread-safe, so tests must not create -// or run conformance Tests from other threads. +// a plain pointer and ResultLedger isn't thread-safe, so tests must not create +// or run conformance Tests from other threads. Yields() only reads the +// failure list; the environment's ResultListener is what tallies the +// outcomes, from gtest's listener hooks. // -// --gtest_repeat is not supported. Each conformance test result is recorded -// in the TestManager exactly once, so a repeated run would see every test as -// already recorded and the statistics and failure-list checks would be wrong. +// --gtest_repeat is not supported. The testee connection rejects a test name +// it has already run, and the ResultLedger counts each test once, so a +// repeated run would crash or report wrong statistics. #ifndef GOOGLE_PROTOBUF_CONFORMANCE_TEST_ENVIRONMENT_H__ #define GOOGLE_PROTOBUF_CONFORMANCE_TEST_ENVIRONMENT_H__ +#include <cstddef> #include <memory> #include <string> #include <vector> @@ -34,7 +39,8 @@ #include <gtest/gtest.h> #include "absl/base/nullability.h" #include "absl/types/optional.h" -#include "conformance/test_manager.h" +#include "conformance/result_ledger.h" +#include "conformance/result_listener.h" #include "conformance/test_runner.h" #include "conformance/testee.h" @@ -43,7 +49,9 @@ namespace conformance { namespace internal { -// Options for a ConformanceEnvironment. +// Options for a ConformanceEnvironment. Normally populated from command-line +// flags by test_environment_main.cc (see OptionsFromFlags() in +// test_environment_flags.h). struct ConformanceEnvironmentOptions { // Exactly one of `runner`, `owned_runner` or `testee_binary` must be set. // @@ -63,7 +71,7 @@ std::vector<std::string> testee_args; // Failure list files to load. All of them are loaded into a single - // TestManager, so entries must not overlap between files. SetUp() fails + // ResultLedger, so entries must not overlap between files. SetUp() fails // fatally if they do. std::vector<std::string> failure_list_files; @@ -86,21 +94,10 @@ // EDITION_PROTO3. That is the one place the clamp lives. Edition maximum_edition = EDITION_PROTO3; - // When true, only performance tests run; when false, only regular tests run. - // Because the two kinds of tests are mutually exclusive within one run, a - // performance run needs its own failure list, like conformance_test_runner's - // *_performance.txt lists: with a shared list, every regular test's entry - // would be "unseen" (and dropped by `fix`). - // - // TODO: b/410126673 - This in-binary mode is transitional. It only mirrors - // conformance_test_runner's --performance flag until the performance tests - // are a suite of their own, which BUILD files enable or disable like any - // other suite. Remove it together with the --performance flag at that - // point. - bool performance = false; - // Rewrites the failure list at the end of the run. Entries that no longer - // fail are dropped. New failures are added with their messages. + // fail are dropped. New failures are added with their messages. The rules, + // wildcard entries and copybara strip blocks included, are those of + // ResultLedger::SaveFailureList(). // // Refused when only a subset of the binary's tests ran, because of // --gtest_filter, sharding or --gtest_fail_fast. A test failure is reported @@ -110,8 +107,9 @@ // Where to write the fixed failure list. Required when `fix` is set and // `failure_list_files` doesn't have exactly one entry. Otherwise defaults - // to that single file under $BUILD_WORKSPACE_DIRECTORY, that is the source - // file under `bazel run`. + // to that single file. Relative paths, here and in `failure_list_files`, + // are resolved under $BUILD_WORKSPACE_DIRECTORY when it is set. Under + // `bazel run` that is the source file. std::string fix_output_file; // Whether TearDown() fails the run if some expected failures were never @@ -120,33 +118,13 @@ // sharding, since a partial run can't see every expected failure. The // conformance_test_runner --test flag behaves the same way. // - // The merged runner sets this to false. It reports the unmatched entries - // itself (see TestManager::UnmatchedExpectedFailures). + // The merged runner (conformance_test_main.cc) sets this to false. It + // reports the unmatched entries itself, per failure list flag and in the + // conformance_test_runner report format (see + // ResultLedger::UnmatchedExpectedFailures). bool check_unseen_expected_failures = true; }; -// A snapshot of the TestManager's counters. Used to report per-test and -// per-suite deltas as test properties. -struct Statistics { - int skipped_tests = 0; - int listed_skips = 0; - int tolerated_failures = 0; - int expected_failures = 0; - int unexpected_failures = 0; - int expected_successes = 0; - int unexpected_successes = 0; - - static Statistics From(const TestManager& manager); - Statistics operator-(const Statistics& other) const; - Statistics& operator+=(const Statistics& other); - - // Records each statistic as a gtest property of the current test (or suite, - // or run) under the names skipped_tests, listed_skips, - // tolerated_failures, expected_failures, unexpected_failures, - // expected_successes and unexpected_successes. - void RecordProperties() const; -}; - // Test helpers, defined in test_environment_testing.h. class ScopedGlobalConformanceEnvironment; class ScopedPartialRunOverride; @@ -160,15 +138,25 @@ // to make sure of that. gtest deletes the environments registered with it at // the end of RUN_ALL_TESTS() (see RunAllTests in gtest.cc). The installed // instance must outlive that, because the transitional merged runner -// (conformance_test_main.cc) reads test_manager() afterwards. Install() -// therefore registers a private proxy with gtest that forwards to SetUp() and -// TearDown(). The environment object itself is intentionally leaked. The -// testee is not: TearDown() releases it. +// (conformance_test_main.cc) reads ledger() afterwards to print the +// results in the conformance_test_runner report format. Install() therefore +// registers a private proxy with gtest that forwards to SetUp() and TearDown(). +// The environment object itself is intentionally leaked. The testee is not: +// TearDown() releases it. // TODO: b/563707827 - once the merged runner has retired (its users moved to // the conformance_test() macro), nothing needs the environment after // RUN_ALL_TESTS(); register it directly with AddGlobalTestEnvironment() and // delete the proxy. // +// Every conformance test a gtest test runs must be checked with Yields() (see +// matchers.h), which records its outcome as a gtest success in the running +// test. The ResultListener (result_listener.h) the environment registers +// with gtest tallies the outcome in the ResultLedger at once, and at the end +// of each gtest test (and test suite) records the outcomes checked in it and +// its statistics as its properties. The environment itself fails the test +// for every result the testee ran that was never checked (see +// ReportUncheckedResults()). +// // Single-threaded. See the file comment. class ConformanceEnvironment : public testing::Environment { public: @@ -181,7 +169,7 @@ // makes it the process-global instance. Must be called exactly once, before // RUN_ALL_TESTS(): // - // ConformanceEnvironment::Install(std::move(options)); + // ConformanceEnvironment::Install(OptionsFromFlags()); // return RUN_ALL_TESTS(); // // The environment itself is never destroyed (see the class comment), but @@ -189,7 +177,7 @@ static ConformanceEnvironment& Install(ConformanceEnvironmentOptions options); // Returns the process-global instance. Check-fails if Install() hasn't been - // called. + // called, which typically means test_environment_main wasn't linked in. static ConformanceEnvironment& Get(); // Loads the failure lists. Any failure here is fatal, so no tests run. @@ -198,10 +186,15 @@ // Finishes the run. gtest runs this after the last test (see Install()). // - // Records the run's statistics as test properties. Rewrites the failure - // list if `fix` was requested and all of the tests ran. Fails if expected - // failures were never seen, unless `check_unseen_expected_failures` is false - // or only a subset of the tests ran (see `fix`). Finally releases + // Reports the results checked outside any test suite (see + // ResultListener::ReportRecordedResults()), fails for every result run after + // the last test that was never checked (see ReportUncheckedResults()) and + // records the run's statistics as test properties. Fails for every failure + // list entry that matched too many tests (see + // ResultLedger::OverexpandedWildcards()). Rewrites the failure list if + // `fix` was requested and all of the tests ran. Fails if expected failures + // were never seen, unless `check_unseen_expected_failures` is false or only + // a subset of the tests ran (see `fix`). Finally releases // the testee. An owned runner is destroyed, which for a ForkPipeRunner // stops the process. For a caller-owned runner only our reference is // dropped, so the caller may destroy the runner right after @@ -212,9 +205,19 @@ // releases the testee. conformance_test_main relies on this. void TearDown() override; + // Fails the current gtest test for every conformance test the testee has + // run since the previous call whose result was never checked with Yields(). + // Otherwise the outcome of such a test would silently bypass the failure + // list. A checked result is one whose outcome the ResultListener has + // tallied in the ledger. A listener the environment registers with gtest + // calls it when a test ends, and TearDown() for anything run after the last + // test. A test that failed fatally is left alone: an ASSERT_* that didn't + // hold returned before the test could check its results. + void ReportUncheckedResults(); + // Where the tests' outcomes are recorded. The transitional merged runner // (conformance_test_main.cc) reads it after RUN_ALL_TESTS(). - TestManager& test_manager() { return test_manager_; } + const ResultLedger& ledger() const { return ledger_; } // The connection to the testee. Check-fails once TearDown() has released // it. @@ -225,6 +228,7 @@ private: friend class ScopedGlobalConformanceEnvironment; friend class ScopedPartialRunOverride; + class UncheckedResultReporter; // Creates an environment and makes it the process-global one. Check-fails if // there already is one. The testee connection is created eagerly, though a @@ -238,7 +242,7 @@ // Null when using the caller's `runner`. Released in TearDown(). std::unique_ptr<ConformanceTestRunner> owned_runner_; - TestManager test_manager_; + ResultLedger ledger_; // Null once TearDown() has released it. std::unique_ptr<Testee> testee_; @@ -248,6 +252,18 @@ absl::optional<bool> partial_run_override_; bool set_up_succeeded_ = false; + // Tallies the outcomes Yields() records in `ledger_` as gtest reports them, + // and reports them and the statistics at the end of every gtest test and + // suite. gtest owns it while it is registered; the destructor takes it + // back. + ResultListener* absl_nonnull result_listener_; + + // Calls ReportUncheckedResults() at the end of every gtest test. gtest owns + // it like result_listener_. + UncheckedResultReporter* absl_nonnull unchecked_result_reporter_; + + // How many of testee().tests_run() ReportUncheckedResults() has looked at. + size_t results_reconciled_ = 0; }; } // namespace internal
diff --git a/conformance/test_environment_flags.cc b/conformance/test_environment_flags.cc new file mode 100644 index 0000000..061a9f9 --- /dev/null +++ b/conformance/test_environment_flags.cc
@@ -0,0 +1,82 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2026 Google LLC. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +// Defines the global --testee_binary, --testee_args, --failure_list, +// --enforcement_level, --maximum_edition, --fix and --fix_output_file flags. +// Being process-global ABSL_FLAGs with short, generic names, they must only be +// linked into binaries with a test_environment_main-style main (see +// test_environment_flags.h), never into conformance_test_runner, whose command +// line parses flags of the same names itself. + +#include "conformance/test_environment_flags.h" + +#include <string> +#include <vector> + +#include "google/protobuf/descriptor.pb.h" +#include "absl/flags/flag.h" +#include "absl/types/span.h" +#include "conformance/test_environment.h" +#include "conformance/testee.h" + +ABSL_FLAG(std::string, testee_binary, "", + "The testee program to run conformance tests against."); +ABSL_FLAG(std::vector<std::string>, testee_args, {}, + "Comma-separated arguments to pass to --testee_binary. Positional " + "arguments are appended to these."); +ABSL_FLAG(std::vector<std::string>, failure_list, {}, + "Comma-separated list of files containing the tests that are " + "expected to fail, one test name per line. Use '#' for comments."); +ABSL_FLAG(google::protobuf::conformance::TestPriority, enforcement_level, + google::protobuf::conformance::kLowestPriority, + "The lowest priority whose failures fail the run, as its number: 0 " + "enforces only P0, 1 enforces P0 and P1. The default enforces every " + "priority. Failures of tests of a lower priority are tolerated " + "(counted, not failed) unless the test is in the failure list, which " + "is checked whatever the level."); +ABSL_FLAG(google::protobuf::Edition, maximum_edition, google::protobuf::EDITION_PROTO3, + "Only run conformance tests up to and including this edition, e.g. " + "EDITION_2023. Defaults to proto2 and proto3 only."); +ABSL_FLAG(bool, fix, false, + "Rewrite the failure list at the end of the run to match the " + "observed results. See --fix_output_file. Refused when only some " + "of the tests run (--gtest_filter, sharding). Use it via `bazel run " + "<target> -- --fix`: under `bazel test` the file is written inside " + "the test sandbox and discarded."); +ABSL_FLAG(std::string, fix_output_file, "", + "Where --fix writes the updated failure list. Required when more " + "than one --failure_list is given; otherwise defaults to the " + "failure list itself. A relative path (this flag's or the failure " + "list's) is resolved under $BUILD_WORKSPACE_DIRECTORY when that is " + "set, so that `bazel run ... -- --fix` creates or updates the file " + "in your workspace."); + +namespace google { +namespace protobuf { +namespace conformance { +namespace internal { + +ConformanceEnvironmentOptions OptionsFromFlags( + absl::Span<char* const> positional_args) { + ConformanceEnvironmentOptions options; + options.testee_binary = absl::GetFlag(FLAGS_testee_binary); + options.testee_args = absl::GetFlag(FLAGS_testee_args); + for (const char* positional_arg : positional_args) { + options.testee_args.push_back(positional_arg); + } + options.failure_list_files = absl::GetFlag(FLAGS_failure_list); + options.enforcement_level = absl::GetFlag(FLAGS_enforcement_level); + options.maximum_edition = absl::GetFlag(FLAGS_maximum_edition); + options.fix = absl::GetFlag(FLAGS_fix); + options.fix_output_file = absl::GetFlag(FLAGS_fix_output_file); + return options; +} + +} // namespace internal +} // namespace conformance +} // namespace protobuf +} // namespace google
diff --git a/conformance/test_environment_flags.h b/conformance/test_environment_flags.h new file mode 100644 index 0000000..6262008 --- /dev/null +++ b/conformance/test_environment_flags.h
@@ -0,0 +1,59 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2026 Google LLC. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +// The command-line flags of a conformance test binary and their translation +// into ConformanceEnvironmentOptions. test_environment_main.cc is the usual +// consumer. An in-process testee needs no main of its own: it defines +// MakeTesteeRunner() (see testee_runner.h). A binary with its own main can +// link this library instead to get the same flags. It then calls +// OptionsFromFlags() and sets `owned_runner` before installing the +// environment. + +#ifndef GOOGLE_PROTOBUF_CONFORMANCE_TEST_ENVIRONMENT_FLAGS_H__ +#define GOOGLE_PROTOBUF_CONFORMANCE_TEST_ENVIRONMENT_FLAGS_H__ + +#include <string> +#include <vector> + +#include "google/protobuf/descriptor.pb.h" +#include "absl/flags/declare.h" +#include "absl/types/span.h" +#include "conformance/test_environment.h" +#include "conformance/testee.h" + +namespace google { +namespace protobuf { +namespace conformance { +namespace internal { + +// Builds the environment options from the flags declared below. +// `positional_args` are the non-flag command-line arguments, excluding +// argv[0]. They are appended to --testee_args. +// +// ConformanceEnvironment::Install(OptionsFromFlags()); +// +// This doesn't insist on --testee_binary. Whether it is required (forked +// testee) or must be absent (in-process testee) is up to the linked +// MakeTesteeRunner() (see testee_runner.h), which test_environment_main.cc +// hands the options to. +ConformanceEnvironmentOptions OptionsFromFlags( + absl::Span<char* const> positional_args = {}); + +} // namespace internal +} // namespace conformance +} // namespace protobuf +} // namespace google + +ABSL_DECLARE_FLAG(std::string, testee_binary); +ABSL_DECLARE_FLAG(std::vector<std::string>, testee_args); +ABSL_DECLARE_FLAG(std::vector<std::string>, failure_list); +ABSL_DECLARE_FLAG(google::protobuf::conformance::TestPriority, enforcement_level); +ABSL_DECLARE_FLAG(google::protobuf::Edition, maximum_edition); +ABSL_DECLARE_FLAG(bool, fix); +ABSL_DECLARE_FLAG(std::string, fix_output_file); + +#endif // GOOGLE_PROTOBUF_CONFORMANCE_TEST_ENVIRONMENT_FLAGS_H__
diff --git a/conformance/test_environment_flags_test.cc b/conformance/test_environment_flags_test.cc new file mode 100644 index 0000000..0d1aea8 --- /dev/null +++ b/conformance/test_environment_flags_test.cc
@@ -0,0 +1,241 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2026 Google LLC. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +#include "conformance/test_environment_flags.h" + +#include <string> +#include <vector> + +#include "google/protobuf/descriptor.pb.h" +#include <gmock/gmock.h> +#include <gtest/gtest.h> +#include "absl/flags/flag.h" +#include "absl/flags/parse.h" +#include "absl/flags/reflection.h" +#include "absl/strings/string_view.h" +#include "conformance/test_environment.h" +#include "conformance/testee.h" + +namespace google { +namespace protobuf { +namespace conformance { +namespace { + +using ::google::protobuf::conformance::internal::ConformanceEnvironmentOptions; +using ::google::protobuf::conformance::internal::OptionsFromFlags; +using ::testing::ElementsAre; +using ::testing::HasSubstr; +using ::testing::IsEmpty; +using ::testing::IsNull; + +// The flag-parsing step of a test_environment_main-style main, on the given +// command line (without argv[0]): parses the absl flags with the same parser +// the main relies on (InitGoogle() / absl::ParseCommandLine()) and returns the +// remaining positional arguments, which the main hands to OptionsFromFlags(). +// Exits the process on a malformed command line, like the main would, so only +// well-formed command lines are tested this way. +// +// The returned pointers point into `args` (argv is made of its strings' data, +// hence the non-const reference), so `args` must outlive them: pass a local. +std::vector<char*> ParseCommandLine(std::vector<std::string>& args) { + std::string argv0 = "conformance_test"; + std::vector<char*> argv = {argv0.data()}; + for (std::string& arg : args) argv.push_back(arg.data()); + std::vector<char*> positional_args; + std::vector<absl::UnrecognizedFlag> unrecognized_flags; + absl::ParseAbseilFlagsOnly(static_cast<int>(argv.size()), argv.data(), + positional_args, unrecognized_flags); + positional_args.erase(positional_args.begin()); // argv[0] + return positional_args; +} + +// --- OptionsFromFlags() ------------------------------------------------------ + +TEST(OptionsFromFlagsTest, Defaults) { + absl::FlagSaver saver; + + ConformanceEnvironmentOptions options = OptionsFromFlags(); + + EXPECT_THAT(options.runner, IsNull()); + EXPECT_THAT(options.owned_runner, IsNull()); + EXPECT_THAT(options.testee_binary, IsEmpty()); + EXPECT_THAT(options.testee_args, IsEmpty()); + EXPECT_THAT(options.failure_list_files, IsEmpty()); + EXPECT_EQ(options.enforcement_level, kLowestPriority); + EXPECT_EQ(options.maximum_edition, EDITION_PROTO3); + EXPECT_FALSE(options.fix); + EXPECT_THAT(options.fix_output_file, IsEmpty()); + EXPECT_TRUE(options.check_unseen_expected_failures); +} + +TEST(OptionsFromFlagsTest, ForwardsEveryFlag) { + absl::FlagSaver saver; + absl::SetFlag(&FLAGS_testee_binary, "testee"); + absl::SetFlag(&FLAGS_testee_args, {"--a", "--b"}); + absl::SetFlag(&FLAGS_failure_list, {"x.txt", "y.txt"}); + absl::SetFlag(&FLAGS_enforcement_level, kP0); + absl::SetFlag(&FLAGS_maximum_edition, EDITION_2023); + absl::SetFlag(&FLAGS_fix, true); + absl::SetFlag(&FLAGS_fix_output_file, "out.txt"); + + ConformanceEnvironmentOptions options = OptionsFromFlags(); + + EXPECT_EQ(options.testee_binary, "testee"); + EXPECT_THAT(options.testee_args, ElementsAre("--a", "--b")); + EXPECT_THAT(options.failure_list_files, ElementsAre("x.txt", "y.txt")); + EXPECT_EQ(options.enforcement_level, kP0); + EXPECT_EQ(options.maximum_edition, EDITION_2023); + EXPECT_TRUE(options.fix); + EXPECT_EQ(options.fix_output_file, "out.txt"); + // Not flags: the main sets the runner, the environment the rest. + EXPECT_THAT(options.runner, IsNull()); + EXPECT_THAT(options.owned_runner, IsNull()); + EXPECT_TRUE(options.check_unseen_expected_failures); +} + +TEST(OptionsFromFlagsTest, AppendsPositionalArgsToTesteeArgs) { + absl::FlagSaver saver; + absl::SetFlag(&FLAGS_testee_args, {"--from_flag"}); + std::vector<std::string> positional = {"--from_command_line", "value"}; + std::vector<char*> positional_args; + for (std::string& arg : positional) positional_args.push_back(arg.data()); + + ConformanceEnvironmentOptions options = OptionsFromFlags(positional_args); + + EXPECT_THAT(options.testee_args, + ElementsAre("--from_flag", "--from_command_line", "value")); +} + +TEST(OptionsFromFlagsTest, PositionalArgsAloneBecomeTesteeArgs) { + absl::FlagSaver saver; + std::vector<std::string> positional = {"--only_positional"}; + std::vector<char*> positional_args; + for (std::string& arg : positional) positional_args.push_back(arg.data()); + + ConformanceEnvironmentOptions options = OptionsFromFlags(positional_args); + + EXPECT_THAT(options.testee_args, ElementsAre("--only_positional")); +} + +TEST(OptionsFromFlagsTest, FixWithoutOutputFileLeavesTheOutputFileEmpty) { + // Defaulting --fix_output_file to the single --failure_list (and resolving + // it under $BUILD_WORKSPACE_DIRECTORY) is the environment's job, not the + // flags'. + absl::FlagSaver saver; + absl::SetFlag(&FLAGS_failure_list, {"failure_lists/cpp_binary.txt"}); + absl::SetFlag(&FLAGS_fix, true); + + ConformanceEnvironmentOptions options = OptionsFromFlags(); + + EXPECT_TRUE(options.fix); + EXPECT_THAT(options.fix_output_file, IsEmpty()); + EXPECT_THAT(options.failure_list_files, + ElementsAre("failure_lists/cpp_binary.txt")); +} + +TEST(OptionsFromFlagsTest, FixOutputFileWithoutFixIsForwardedAsIs) { + absl::FlagSaver saver; + absl::SetFlag(&FLAGS_fix_output_file, "out.txt"); + + ConformanceEnvironmentOptions options = OptionsFromFlags(); + + EXPECT_FALSE(options.fix); + EXPECT_EQ(options.fix_output_file, "out.txt"); +} + +// --- The flags as conformance_test() (conformance.bzl) passes them ----------- + +TEST(CommandLineTest, ParsesTheMacroCommandLine) { + absl::FlagSaver saver; + std::vector<std::string> args = { + "--testee_binary=google/protobuf/conformance/conformance_cpp", + "--failure_list=google/protobuf/conformance/failure_lists/" + "cpp_binary.txt", + "--maximum_edition=EDITION_2023"}; + std::vector<char*> positional_args = ParseCommandLine(args); + + ConformanceEnvironmentOptions options = OptionsFromFlags(positional_args); + + EXPECT_THAT(positional_args, IsEmpty()); + EXPECT_EQ(options.testee_binary, + "google/protobuf/conformance/conformance_cpp"); + EXPECT_THAT(options.failure_list_files, + ElementsAre("google/protobuf/conformance/failure_lists/" + "cpp_binary.txt")); + EXPECT_EQ(options.maximum_edition, EDITION_2023); + EXPECT_EQ(options.enforcement_level, kLowestPriority); + EXPECT_FALSE(options.fix); +} + +TEST(CommandLineTest, ParsesTheFixFlags) { + absl::FlagSaver saver; + std::vector<std::string> args = {"--fix", "--fix_output_file=new.txt", + "--enforcement_level=0"}; + std::vector<char*> positional_args = ParseCommandLine(args); + + ConformanceEnvironmentOptions options = OptionsFromFlags(positional_args); + + EXPECT_TRUE(options.fix); + EXPECT_EQ(options.fix_output_file, "new.txt"); + EXPECT_EQ(options.enforcement_level, kP0); +} + +TEST(CommandLineTest, FailureListIsCommaSeparated) { + absl::FlagSaver saver; + std::vector<std::string> args = {"--failure_list=a.txt,b.txt"}; + std::vector<char*> positional_args = ParseCommandLine(args); + + ConformanceEnvironmentOptions options = OptionsFromFlags(positional_args); + + EXPECT_THAT(options.failure_list_files, ElementsAre("a.txt", "b.txt")); +} + +TEST(CommandLineTest, RepeatedFailureListFlagReplacesThePreviousValue) { + // absl vector flags don't accumulate: the last occurrence wins, so two + // lists must be given as --failure_list=a.txt,b.txt (see the flag's help). + absl::FlagSaver saver; + std::vector<std::string> args = {"--failure_list=a.txt", + "--failure_list=b.txt"}; + std::vector<char*> positional_args = ParseCommandLine(args); + + ConformanceEnvironmentOptions options = OptionsFromFlags(positional_args); + + EXPECT_THAT(options.failure_list_files, ElementsAre("b.txt")); +} + +TEST(CommandLineTest, TesteeArgsAreCommaSeparatedAndPositionalArgsAppended) { + absl::FlagSaver saver; + std::vector<std::string> args = {"--testee_binary=testee", + "--testee_args=--x,--y", "--", "--z", "w"}; + std::vector<char*> positional_args = ParseCommandLine(args); + + ConformanceEnvironmentOptions options = OptionsFromFlags(positional_args); + + EXPECT_THAT(positional_args, + ElementsAre(absl::string_view("--z"), absl::string_view("w"))); + EXPECT_THAT(options.testee_args, ElementsAre("--x", "--y", "--z", "w")); +} + +TEST(EnforcementLevelFlagTest, ParsesThePriorityNumber) { + TestPriority priority = kLowestPriority; + std::string error; + EXPECT_TRUE(AbslParseFlag("0", &priority, &error)); + EXPECT_EQ(priority, kP0); + EXPECT_TRUE(AbslParseFlag("1", &priority, &error)); + EXPECT_EQ(priority, kP1); + EXPECT_EQ(AbslUnparseFlag(kP1), "1"); + + for (absl::string_view bad : {"-1", "2", "P1", ""}) { + EXPECT_FALSE(AbslParseFlag(bad, &priority, &error)) << bad; + EXPECT_THAT(error, HasSubstr("from 0 to 1")) << bad; + } +} + +} // namespace +} // namespace conformance +} // namespace protobuf +} // namespace google
diff --git a/conformance/test_environment_integration_test.cc b/conformance/test_environment_integration_test.cc new file mode 100644 index 0000000..354eb42 --- /dev/null +++ b/conformance/test_environment_integration_test.cc
@@ -0,0 +1,444 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2026 Google LLC. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +// End-to-end checks of test_environment_main and ConformanceEnvironment, +// running the deliberately failing sample_conformance_test against the real +// C++ testee. Only what needs a real test process is checked here: the +// command line handling of the main, what gtest itself contributes to a run +// (partial-run detection, the XML report) and the exit code. Everything else +// is covered in-process by test_environment_test. + +#include <algorithm> +#include <cstddef> +#include <cstdio> +#include <cstdlib> +#include <initializer_list> +#include <string> +#include <vector> + +#ifndef _WIN32 +#include <sys/wait.h> // for WEXITSTATUS +#endif + +#include "tools/cpp/runfiles/runfiles.h" +#include "google/protobuf/testing/file.h" +#include "google/protobuf/testing/file.h" +#include <gmock/gmock.h> +#include "google/protobuf/testing/googletest.h" +#include <gtest/gtest.h> +#include "absl/log/absl_check.h" +#include "absl/strings/str_cat.h" +#include "absl/strings/str_replace.h" +#include "absl/strings/string_view.h" +#include "absl/types/span.h" + +namespace google { +namespace protobuf { +namespace conformance { +namespace { + +using ::testing::AllOf; +using ::testing::HasSubstr; +using ::testing::Not; + +// The names of the sample tests, as they appear in failure lists. +constexpr absl::string_view kPass = + "Required.Proto3.ProtobufInput.SamplePass.ProtobufOutput"; +constexpr absl::string_view kDeliberateFailure = + "Required.Proto3.ProtobufInput.SampleDeliberateFailure.ProtobufOutput"; +constexpr absl::string_view kP1Failure = + "Recommended.Proto3.ProtobufInput.SampleP1Failure.ProtobufOutput"; +constexpr absl::string_view kNeverRuns = + "Required.Proto3.ProtobufInput.SampleNeverRuns.ProtobufOutput"; +constexpr absl::string_view kUncheckedResult = + "Required.Proto3.ProtobufInput.SampleUncheckedResult.ProtobufOutput"; +constexpr absl::string_view kFailureMessage = + "Should have failed to parse, but didn't."; + +// gtest spells its flags --gtest_*, Google's internal build --gunit_*. +constexpr absl::string_view kGtestFlag = "--" GTEST_FLAG_PREFIX_; + +std::string Rlocation(const std::string& path) { + static const bazel::tools::cpp::runfiles::Runfiles* const runfiles = [] { + std::string error; + bazel::tools::cpp::runfiles::Runfiles* runfiles = + bazel::tools::cpp::runfiles::Runfiles::CreateForTest(&error); + ABSL_CHECK(runfiles != nullptr) << error; + return runfiles; + }(); + return runfiles->Rlocation(path); +} + +// The BUILD rule passes the sample test and the testee as $(rlocationpath) +// in these variables. +std::string RunfilesPathFromEnvironment(const char* name) { + const char* path = std::getenv(name); + ABSL_CHECK(path != nullptr) << name << " is not set"; + return path; +} + +const std::string& SampleTest() { + static const std::string* const path = + new std::string(Rlocation(RunfilesPathFromEnvironment("SAMPLE_TEST"))); + return *path; +} + +const std::string& TesteeBinary() { + static const std::string* const path = + new std::string(Rlocation(RunfilesPathFromEnvironment("TESTEE"))); + return *path; +} + +// The path of sample_failure_list_<name>.txt, a data dependency from the +// sample test's package. +std::string FailureListPath(absl::string_view name) { + std::string package = RunfilesPathFromEnvironment("SAMPLE_TEST"); + package.erase(package.rfind('/')); + return Rlocation( + absl::StrCat(package, "/sample_failure_list_", name, ".txt")); +} + +std::string FailureListFlag(absl::string_view name) { + return absl::StrCat("--failure_list=", FailureListPath(name)); +} + +std::string TmpPath(absl::string_view name) { + return absl::StrCat(TestTempDir(), "/", name); +} + +std::string WriteFile(absl::string_view name, absl::string_view content) { + std::string path = TmpPath(name); + ABSL_CHECK_OK(File::SetContents(path, content, true)); + return path; +} + +// `path` is a std::string (not a string_view) for the open source +// File::GetContents() shim. +std::string ReadFile(const std::string& path) { + std::string content; + ABSL_CHECK_OK(File::GetContents(path, &content, true)); + return content; +} + +// Quotes `arg` for the shell that popen() runs commands through. +std::string ShellQuote(absl::string_view arg) { +#ifdef _WIN32 + return absl::StrCat("\"", arg, "\""); +#else + return absl::StrCat("'", absl::StrReplaceAll(arg, {{"'", "'\\''"}}), "'"); +#endif +} + +struct RunResult { + int exit_code; + std::string output; // stdout and stderr +}; + +// Runs `binary` with `args` and returns its exit code and output. +RunResult RunBinary(const std::string& binary, + absl::Span<const std::string> args) { + std::string command = ShellQuote(binary); + for (const std::string& arg : args) { + absl::StrAppend(&command, " ", ShellQuote(arg)); + } + absl::StrAppend(&command, " 2>&1"); +#ifdef _WIN32 + // cmd.exe strips the first and last quote of a command line that starts + // with one, so give it a pair to strip. + FILE* pipe = _popen(absl::StrCat("\"", command, "\"").c_str(), "r"); +#else + FILE* pipe = popen(command.c_str(), "r"); +#endif + ABSL_CHECK(pipe != nullptr) << "Failed to run " << command; + + RunResult result; + char buffer[4096]; + size_t n; + while ((n = fread(buffer, 1, sizeof(buffer), pipe)) > 0) { + result.output.append(buffer, n); + } +#ifdef _WIN32 + result.exit_code = _pclose(pipe); +#else + int status = pclose(pipe); + result.exit_code = WIFEXITED(status) ? WEXITSTATUS(status) : -1; +#endif + return result; +} + +// Runs the sample test against the C++ testee with `args`. +RunResult RunSample(std::vector<std::string> args) { + args.insert(args.begin(), absl::StrCat("--testee_binary=", TesteeBinary())); + return RunBinary(SampleTest(), args); +} + +// Returns the part of the gtest XML `report` from the element opening with +// `open` up to the first of `ends`. gtest writes an element's properties +// before its children, so the right `ends` isolate one element's properties. +std::string XmlScope(const std::string& report, absl::string_view open, + std::initializer_list<absl::string_view> ends) { + size_t begin = report.find(open); + if (begin == std::string::npos) return ""; + size_t end = report.size(); + for (absl::string_view terminator : ends) { + end = std::min(end, report.find(terminator, begin + open.size())); + } + return report.substr(begin, end - begin); +} + +std::string XmlTestCase(const std::string& report, absl::string_view name) { + return XmlScope(report, absl::StrCat("<testcase name=\"", name, "\""), + {"<testcase", "</testsuite>"}); +} + +std::string XmlTestSuite(const std::string& report, absl::string_view name) { + return XmlScope(report, absl::StrCat("<testsuite name=\"", name, "\""), + {"<testcase", "</testsuite>"}); +} + +std::string XmlTestRun(const std::string& report) { + return XmlScope(report, "<testsuites", {"<testsuite "}); +} + +// Sets `name` to `value` in this process's environment, which the binaries +// the tests run inherit. The MSVC CRT has no setenv()/unsetenv(). +void SetEnvironmentVariable(const char* name, const char* value) { +#ifdef _WIN32 + ABSL_CHECK_EQ(_putenv_s(name, value), 0); +#else + ABSL_CHECK_EQ(setenv(name, value, /*overwrite=*/1), 0); +#endif +} + +void UnsetEnvironmentVariable(const char* name) { +#ifdef _WIN32 + ABSL_CHECK_EQ(_putenv_s(name, ""), 0); // An empty value removes it. +#else + ABSL_CHECK_EQ(unsetenv(name), 0); +#endif +} + +class TestEnvironmentIntegrationTest : public testing::Test { + protected: + static void SetUpTestSuite() { + // The test runner configures this binary's gtest through these variables + // (its filter, sharding, XML report, ...). gtest has read them by now, + // and they must not reach the binaries the tests run. + for (const char* name : + {"TESTBRIDGE_TEST_ONLY", "TESTBRIDGE_TEST_RUNNER_FAIL_FAST", + "XML_OUTPUT_FILE", "TEST_PREMATURE_EXIT_FILE", + "TEST_WARNINGS_OUTPUT_FILE", "TEST_TOTAL_SHARDS", "TEST_SHARD_INDEX", + "TEST_SHARD_STATUS_FILE", "GTEST_TOTAL_SHARDS", "GTEST_SHARD_INDEX", + "GTEST_SHARD_STATUS_FILE"}) { + UnsetEnvironmentVariable(name); + } + } +}; + +TEST_F(TestEnvironmentIntegrationTest, PassesWithAMatchingFailureList) { + RunResult run = RunSample({FailureListFlag("ok")}); + EXPECT_EQ(run.exit_code, 0) << run.output; + EXPECT_THAT(run.output, + AllOf(HasSubstr("[ OK ] SampleConformanceTest.SamplePass"), + HasSubstr("[ OK ] " + "SampleConformanceTest.SampleDeliberateFailure"), + HasSubstr("[ OK ] " + "SampleConformanceTest.SampleP1Failure"))); +} + +TEST_F(TestEnvironmentIntegrationTest, FailsWithAnEmptyFailureList) { + RunResult run = RunSample({FailureListFlag("empty")}); + EXPECT_NE(run.exit_code, 0); + EXPECT_THAT(run.output, + AllOf(HasSubstr("[ FAILED ] " + "SampleConformanceTest.SampleDeliberateFailure"), + HasSubstr(kDeliberateFailure), HasSubstr(kFailureMessage))); +} + +TEST_F(TestEnvironmentIntegrationTest, AListedTestThatPassesFailsTheRun) { + // The list also expects the test that passes to fail. + std::string list = WriteFile( + "pass_listed.txt", absl::StrCat(ReadFile(FailureListPath("ok")), kPass, + " # Should have failed to parse\n")); + RunResult run = RunSample({absl::StrCat("--failure_list=", list)}); + EXPECT_NE(run.exit_code, 0); + EXPECT_THAT(run.output, + AllOf(HasSubstr("[ FAILED ] SampleConformanceTest.SamplePass"), + HasSubstr("is in the failure list, but test succeeded"), + HasSubstr("[ OK ] " + "SampleConformanceTest.SampleDeliberateFailure"), + HasSubstr("[ OK ] " + "SampleConformanceTest.SampleP1Failure"))); +} + +TEST_F(TestEnvironmentIntegrationTest, EnforcementLevelToleratesP1Failures) { + // Only the P0 failure is listed. + std::string list = + WriteFile("p0_only.txt", + absl::StrCat(kDeliberateFailure, " # ", kFailureMessage, "\n")); + std::string xml = TmpPath("tolerated.xml"); + RunResult run = + RunSample({absl::StrCat("--failure_list=", list), "--enforcement_level=0", + absl::StrCat(kGtestFlag, "output=xml:", xml)}); + EXPECT_EQ(run.exit_code, 0) << run.output; + EXPECT_THAT(run.output, + HasSubstr("[ OK ] SampleConformanceTest.SampleP1Failure")); + EXPECT_THAT(XmlTestCase(ReadFile(xml), "SampleP1Failure"), + HasSubstr("name=\"tolerated_failures\" value=\"1\"")); + + // Every priority is enforced by default. + run = RunSample({absl::StrCat("--failure_list=", list)}); + EXPECT_NE(run.exit_code, 0); + EXPECT_THAT( + run.output, + AllOf(HasSubstr("[ FAILED ] SampleConformanceTest.SampleP1Failure"), + HasSubstr( + absl::StrCat("Unexpected failure for test: ", kP1Failure)))); +} + +TEST_F(TestEnvironmentIntegrationTest, AResultThatIsNeverCheckedFailsItsTest) { + // The sample test that forgets Yields() is disabled, so that the other runs + // stay clean; this run asks for it by name. + RunResult run = + RunSample({absl::StrCat(kGtestFlag, "also_run_disabled_tests"), + absl::StrCat(kGtestFlag, "filter=*SampleUncheckedResult")}); + EXPECT_NE(run.exit_code, 0); + EXPECT_THAT( + run.output, + AllOf(HasSubstr("[ FAILED ] " + "SampleConformanceTest.DISABLED_SampleUncheckedResult"), + HasSubstr(absl::StrCat( + "TestResult for ", kUncheckedResult, + " was never checked; wrap the matcher in Yields()")))); +} + +TEST_F(TestEnvironmentIntegrationTest, ListsTestsWithoutATestee) { + RunResult run = + RunBinary(SampleTest(), {absl::StrCat(kGtestFlag, "list_tests")}); + EXPECT_EQ(run.exit_code, 0) << run.output; + EXPECT_THAT(run.output, HasSubstr("SampleDeliberateFailure")); +} + +TEST_F(TestEnvironmentIntegrationTest, PassesPositionalArgumentsToTheTestee) { + // The testee ignores them; this checks that flag parsing lets them through. + RunResult run = + RunSample({FailureListFlag("ok"), "--", "--ignored_by_testee"}); + EXPECT_EQ(run.exit_code, 0) << run.output; +} + +// Partial runs are detected from gtest's own test counts and state (see +// IsPartialRun() in test_environment.cc), which only a real run sets up. + +TEST_F(TestEnvironmentIntegrationTest, UnseenExpectedFailuresFailAFullRun) { + // The DISABLED_ sample test doesn't run either; that mustn't make the run + // count as partial. + RunResult run = RunSample({FailureListFlag("unseen")}); + EXPECT_NE(run.exit_code, 0); + EXPECT_THAT( + run.output, + HasSubstr(absl::StrCat("expected failures were not seen: ", kNeverRuns))); +} + +TEST_F(TestEnvironmentIntegrationTest, UnseenExpectedFailuresPassAFilteredRun) { + RunResult run = + RunSample({FailureListFlag("unseen"), + absl::StrCat(kGtestFlag, + "filter=SampleConformanceTest.Sample*Failure")}); + EXPECT_EQ(run.exit_code, 0) << run.output; + EXPECT_THAT(run.output, Not(HasSubstr("were not seen"))); +} + +TEST_F(TestEnvironmentIntegrationTest, UnseenExpectedFailuresPassAShardedRun) { + // gtest takes the shard from the environment: TEST_* in Google's internal + // build, GTEST_* in open source. + for (const char* name : {"TEST_TOTAL_SHARDS", "GTEST_TOTAL_SHARDS"}) { + SetEnvironmentVariable(name, "2"); + } + for (const char* name : {"TEST_SHARD_INDEX", "GTEST_SHARD_INDEX"}) { + SetEnvironmentVariable(name, "0"); + } + RunResult run = RunSample({FailureListFlag("unseen")}); + for (const char* name : {"TEST_TOTAL_SHARDS", "GTEST_TOTAL_SHARDS", + "TEST_SHARD_INDEX", "GTEST_SHARD_INDEX"}) { + UnsetEnvironmentVariable(name); + } + EXPECT_EQ(run.exit_code, 0) << run.output; + EXPECT_THAT(run.output, AllOf(HasSubstr("Note: This is test shard 1 of 2"), + Not(HasSubstr("were not seen")))); +} + +TEST_F(TestEnvironmentIntegrationTest, AnEarlyStopUnderFailFastIsAPartialRun) { + // With a list that only names the never-run test, the deliberate failure + // stops the run before the other tests, and the unseen entry isn't reported + // on top of it. + std::string list = + WriteFile("never_runs_only.txt", absl::StrCat(kNeverRuns, "\n")); + RunResult run = RunSample({absl::StrCat("--failure_list=", list), + absl::StrCat(kGtestFlag, "fail_fast")}); + EXPECT_NE(run.exit_code, 0); + EXPECT_THAT(run.output, + AllOf(HasSubstr("[ FAILED ] " + "SampleConformanceTest.SampleDeliberateFailure"), + Not(HasSubstr("were not seen")))); +} + +TEST_F(TestEnvironmentIntegrationTest, FixRewritesTheFailureListInPlace) { + std::string list = WriteFile("failure_list.txt", ""); + RunResult run = RunSample({absl::StrCat("--failure_list=", list), "--fix"}); + // The failures were unexpected in this run; the rewritten list expects them. + EXPECT_NE(run.exit_code, 0); + EXPECT_THAT(ReadFile(list), + AllOf(HasSubstr(kDeliberateFailure), HasSubstr(kP1Failure), + HasSubstr(absl::StrCat("# ", kFailureMessage)))); + run = RunSample({absl::StrCat("--failure_list=", list)}); + EXPECT_EQ(run.exit_code, 0) << run.output; +} + +TEST_F(TestEnvironmentIntegrationTest, RecordsStatisticsInTheXmlReport) { + std::string xml = TmpPath("report.xml"); + RunResult run = RunSample( + {FailureListFlag("ok"), absl::StrCat(kGtestFlag, "output=xml:", xml)}); + ASSERT_EQ(run.exit_code, 0) << run.output; + std::string report = ReadFile(xml); + // Each level gets its own numbers: the P0 and P1 failures are one expected + // failure each, two on their suite and on the run. + EXPECT_THAT(XmlTestCase(report, "SampleDeliberateFailure"), + HasSubstr("name=\"expected_failures\" value=\"1\"")); + EXPECT_THAT(XmlTestCase(report, "SamplePass"), + HasSubstr("name=\"expected_successes\" value=\"1\"")); + EXPECT_THAT(XmlTestSuite(report, "SampleConformanceTest"), + HasSubstr("name=\"expected_failures\" value=\"2\"")); + EXPECT_THAT(XmlTestRun(report), + AllOf(HasSubstr("name=\"expected_failures\" value=\"2\""), + HasSubstr("name=\"unexpected_failures\" value=\"0\""))); +} + +TEST_F(TestEnvironmentIntegrationTest, RecordsEachOutcomeInTheXmlReport) { + std::string xml = TmpPath("outcomes.xml"); + RunResult run = RunSample( + {FailureListFlag("ok"), absl::StrCat(kGtestFlag, "output=xml:", xml)}); + ASSERT_EQ(run.exit_code, 0) << run.output; + std::string report = ReadFile(xml); + // Each conformance test's outcome is a property, named after it, of the + // gtest test that checked it. gtest writes ' as ' in attributes. + EXPECT_THAT( + XmlTestCase(report, "SamplePass"), + HasSubstr(absl::StrCat("name=\"", kPass, "\" value=\"P0 PASS\""))); + EXPECT_THAT( + XmlTestCase(report, "SampleDeliberateFailure"), + HasSubstr(absl::StrCat("name=\"", kDeliberateFailure, + "\" value=\"P0 FAIL: Should have failed to parse, " + "but didn't.\""))); + EXPECT_THAT( + XmlTestCase(report, "SampleP1Failure"), + HasSubstr(absl::StrCat("name=\"", kP1Failure, "\" value=\"P1 FAIL: "))); +} + +} // namespace +} // namespace conformance +} // namespace protobuf +} // namespace google
diff --git a/conformance/test_environment_main.cc b/conformance/test_environment_main.cc new file mode 100644 index 0000000..00c7b22 --- /dev/null +++ b/conformance/test_environment_main.cc
@@ -0,0 +1,63 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2026 Google LLC. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +// A gtest main for conformance test binaries. It configures a +// ConformanceEnvironment from the command-line flags declared in +// test_environment_flags.h and installs it before running all tests. +// Conformance suites should depend on the test_environment_main library +// instead of a generic gtest main. +// +// The testee is whatever the library defining MakeTesteeRunner() (see +// testee_runner.h) makes of --testee_binary and --testee_args; the binary must +// link exactly one such library, :forked_testee by default. Any positional +// arguments left after flag parsing are appended to --testee_args, so +// `conformance_test --testee_binary=foo -- --bar` passes `--bar` to a forked +// testee. + +#include <utility> +#include <vector> + +#include <gtest/gtest.h> +#include "absl/types/span.h" +#include "conformance/test_environment.h" +#include "conformance/test_environment_flags.h" +#include "conformance/testee_runner.h" + +#include "absl/flags/parse.h" +#include "absl/log/initialize.h" + +// TODO: A custom main only because the stock gtest main parses +// absl flags only when gtest is built with Abseil (GTEST_HAS_ABSL), which +// open-source protobuf doesn't require yet. Once it can be required, install +// the environment from a static initializer and use the stock main instead. +int main(int argc, char* argv[]) { + // In Google's internal build, InitGoogleTest() also runs InitGoogle(), which + // parses absl flags, leaves only positional arguments in argv and + // initializes logging. In open source we do that ourselves, after gtest has + // removed its own flags from argv. + testing::InitGoogleTest(&argc, argv); + std::vector<char*> positional_args(argv, argv + argc); + positional_args = absl::ParseCommandLine(argc, argv); + absl::InitializeLog(); + + // Listing tests doesn't need a testee, and shouldn't require any flags. + if (!GTEST_FLAG_GET(list_tests)) { + google::protobuf::conformance::internal::ConformanceEnvironmentOptions options = + google::protobuf::conformance::internal::OptionsFromFlags( + absl::MakeConstSpan(positional_args).subspan(1)); + // The linked testee decides what the flags mean, and the environment + // takes its runner instead of the flags (it would otherwise spawn + // --testee_binary itself). + options.owned_runner = google::protobuf::conformance::MakeTesteeRunner( + options.testee_binary, options.testee_args); + options.testee_binary.clear(); + options.testee_args.clear(); + google::protobuf::conformance::internal::ConformanceEnvironment::Install( + std::move(options)); + } + return RUN_ALL_TESTS(); +}
diff --git a/conformance/test_environment_test.cc b/conformance/test_environment_test.cc index 91280e2..3325918 100644 --- a/conformance/test_environment_test.cc +++ b/conformance/test_environment_test.cc
@@ -36,9 +36,12 @@ #include "conformance/conformance.pb.h" #include "conformance/global_test_environment.h" #include "conformance/matchers.h" +#include "conformance/matchers_testing.h" #include "conformance/mock_test_runner.h" +#include "conformance/result_ledger.h" +#include "conformance/result_listener.h" +#include "conformance/result_record.h" #include "conformance/test_environment_testing.h" -#include "conformance/test_manager.h" #include "conformance/testee.h" #include "google/protobuf/test_messages_proto3.pb.h" #include "google/protobuf/text_format.h" @@ -51,11 +54,20 @@ using ::absl_testing::IsOk; using ::google::protobuf::conformance::internal::ConformanceEnvironment; using ::google::protobuf::conformance::internal::ConformanceEnvironmentOptions; +using ::google::protobuf::conformance::internal::ResultRecord; +using ::google::protobuf::conformance::internal::ResultRecordMessage; using ::protobuf_test_messages::proto3::TestAllTypesProto3; using ::testing::_; +using ::testing::Contains; +using ::testing::ElementsAre; +using ::testing::FieldsAre; using ::testing::HasSubstr; +using ::testing::IsEmpty; +using ::testing::Key; using ::testing::NiceMock; using ::testing::Not; +using ::testing::Pair; +using ::testing::Property; using ::testing::Return; // The name the testee sees for a kP0 test named "Foo" that parses @@ -64,6 +76,8 @@ "Required.Proto3.ProtobufInput.Foo.ProtobufOutput"; constexpr absl::string_view kBarTestName = "Required.Proto3.ProtobufInput.Bar.ProtobufOutput"; +constexpr absl::string_view kSetUpTestSuiteTestName = + "Required.Proto3.ProtobufInput.SetUpTestSuite.ProtobufOutput"; std::string SerializedResponse(absl::string_view textproto) { ::conformance::ConformanceResponse response; @@ -160,6 +174,47 @@ std::string old_directory_; }; +// The messages of the failures in `failures`. +std::vector<std::string> Messages( + const testing::TestPartResultArray& failures) { + std::vector<std::string> messages; + for (int i = 0; i < failures.size(); ++i) { + messages.push_back(failures.GetTestPartResult(i).message()); + } + return messages; +} + +// The properties of `result`, as (key, value) pairs. +std::vector<std::pair<std::string, std::string>> Properties( + const testing::TestResult& result) { + std::vector<std::pair<std::string, std::string>> properties; + for (int i = 0; i < result.test_property_count(); ++i) { + const testing::TestProperty& property = result.GetTestProperty(i); + properties.emplace_back(property.key(), property.value()); + } + return properties; +} + +// Runs ReportUncheckedResults() on `env` and returns the messages of the +// failures it reports. +std::vector<std::string> UncheckedResultFailures(ConformanceEnvironment& env) { + testing::TestPartResultArray failures; + { + testing::ScopedFakeTestPartResultReporter reporter( + testing::ScopedFakeTestPartResultReporter:: + INTERCEPT_ONLY_CURRENT_THREAD, + &failures); + env.ReportUncheckedResults(); + } + return Messages(failures); +} + +// Creates a kP0 test named `name` against the global testee, as the fixture's +// Testee() would. +internal::Test CreateP0Test(absl::string_view name) { + return ConformanceEnvironment::Get().testee().CreateTest(name, kP0); +} + // Tears down `env` in a full run, expecting it to report unseen expected // failures with a message containing `expected_message`. void TearDownExpectingUnseenFailures(ConformanceEnvironment& env, @@ -233,23 +288,26 @@ TEST_F(ConformanceEnvironmentTest, ScopedEnvironmentIsTheGlobalOne) { internal::ScopedGlobalConformanceEnvironment env({/*runner=*/&fake_runner_}); - EXPECT_EQ(&internal::GetGlobalTestManager(), &env->test_manager()); + EXPECT_EQ(&internal::GetGlobalFailureList(), &env->ledger().failure_list()); } -TEST_F(ConformanceEnvironmentTest, EnforcementLevelIsPassedToTestManager) { +TEST_F(ConformanceEnvironmentTest, EnforcementLevelIsPassedToTheFailureList) { { // Every priority is enforced by default. internal::ScopedGlobalConformanceEnvironment env( {/*runner=*/&fake_runner_}); - EXPECT_THAT(env->test_manager().ReportFailure("P1.x", kP1, "boom"), - Not(IsOk())); + EXPECT_THAT( + env->ledger().failure_list().VerdictOnFailure("P1.x", kP1, "boom"), + Not(IsOk())); } { ConformanceEnvironmentOptions options; options.runner = &fake_runner_; options.enforcement_level = kP0; internal::ScopedGlobalConformanceEnvironment env(std::move(options)); - EXPECT_THAT(env->test_manager().ReportFailure("P1.x", kP1, "boom"), IsOk()); + EXPECT_THAT( + env->ledger().failure_list().VerdictOnFailure("P1.x", kP1, "boom"), + IsOk()); } } @@ -262,9 +320,8 @@ .WillOnce(Return(SerializedResponse(R"pb(protobuf_payload: "\010c")pb"))); EXPECT_THAT(RunP0("Foo"), Yields(WhenParsed(EqualsTextProto(R"pb(optional_int32: 99)pb")))); - - EXPECT_EQ(env->test_manager().expected_successes(), 1); - EXPECT_EQ(env->test_manager().unexpected_failures(), 0); + EXPECT_EQ(env->ledger().expected_successes(), 1); + EXPECT_EQ(env->ledger().unexpected_failures(), 0); env->TearDown(); } @@ -272,13 +329,167 @@ internal::ScopedGlobalConformanceEnvironment env({/*runner=*/&fake_runner_}); env->SetUp(); - EXPECT_NONFATAL_FAILURE(EXPECT_THAT(RunP0("Foo"), Yields(IsParseError())), - "Should have failed to parse, but didn't."); + EXPECT_YIELDS_FAILURE(EXPECT_THAT(RunP0("Foo"), Yields(IsParseError())), + "Should have failed to parse, but didn't."); - EXPECT_EQ(env->test_manager().unexpected_failures(), 1); + EXPECT_EQ(env->ledger().unexpected_failures(), 1); env->TearDown(); } +TEST_F(ConformanceEnvironmentTest, ResultsThatAreNeverCheckedFailTheTest) { + internal::ScopedGlobalConformanceEnvironment env({/*runner=*/&fake_runner_}); + env->SetUp(); + + // A result that is stored but never checked and one matched without + // Yields() both leave the ResultLedger in the dark; only what Yields() + // records reaches it. (Dropping a result or applying a leaf matcher to it + // without Yields() does not compile; see YieldedResult in matchers.h.) + internal::TestResult unchecked = RunP0("Foo"); + EXPECT_THAT(RunP0("Bar"), + Property(&internal::TestResult::name, HasSubstr("Bar"))); + EXPECT_THAT(RunP0("Baz"), + Yields(WhenParsed(EqualsTextProto(R"pb(optional_int32: 99)pb")))); + + EXPECT_THAT( + UncheckedResultFailures(env.environment()), + ElementsAre(HasSubstr(absl::StrCat( + "TestResult for ", kFooTestName, + " was never checked; wrap the matcher in Yields()")), + HasSubstr(absl::StrCat( + "TestResult for ", kBarTestName, + " was never checked; wrap the matcher in Yields()")))); + // Each result is reported once. + EXPECT_THAT(UncheckedResultFailures(env.environment()), IsEmpty()); + env->TearDown(); +} + +TEST_F(ConformanceEnvironmentTest, TearDownReportsResultsThatWereNeverChecked) { + internal::ScopedGlobalConformanceEnvironment env({/*runner=*/&fake_runner_}); + env->SetUp(); + internal::TestResult unchecked = RunP0("Foo"); + EXPECT_NONFATAL_FAILURE( + env->TearDown(), + absl::StrCat("TestResult for ", kFooTestName, + " was never checked; wrap the matcher in Yields()")); +} + +TEST_F(ConformanceEnvironmentTest, ReportUncheckedResultsIsANoOpAfterTearDown) { + internal::ScopedGlobalConformanceEnvironment env({/*runner=*/&fake_runner_}); + env->SetUp(); + env->TearDown(); + EXPECT_THAT(UncheckedResultFailures(env.environment()), IsEmpty()); +} + +TEST_F(ConformanceEnvironmentTest, TalliesRecordedResultsAsTheyArrive) { + internal::ScopedGlobalConformanceEnvironment env({/*runner=*/&fake_runner_}); + env->SetUp(); + + // What Yields() records (see ResultRecordMessage() in result_record.h), by + // hand, next to a success that isn't a record. + SUCCEED() << ResultRecordMessage(kFooTestName, + {kP0, ResultRecord::Status::kFail, "boom"}); + SUCCEED() << "P0 FAIL: not a record"; + EXPECT_THAT(env->ledger().UnexpectedFailures(), + ElementsAre(FieldsAre(kFooTestName, "boom", absl::nullopt))); + EXPECT_EQ(env->ledger().expected_successes(), 0); + + // The first outcome recorded for a test is the one that counts. + SUCCEED() << ResultRecordMessage(kFooTestName, + {kP0, ResultRecord::Status::kPass, ""}); + EXPECT_EQ(env->ledger().unexpected_failures(), 1); + EXPECT_EQ(env->ledger().expected_successes(), 0); + env->TearDown(); +} + +// The environment's ResultListener reports the results Yields() records and +// the statistics when a gtest test or test suite ends (result_listener_test.cc +// drives it by hand), so this only shows through an environment that outlives +// the test body, like the one a test binary installs. This suite keeps one +// alive across its tests, and until the suite itself has ended, and inspects +// what the listener did after each body has returned. The failure the +// environment adds for an unchecked result is a real one, so that is checked +// end to end in test_environment_integration_test.cc instead. +class LongLivedEnvironmentTest : public testing::Test { + protected: + struct SuiteState { + FakeTestRunner runner; + internal::ScopedGlobalConformanceEnvironment env{{/*runner=*/&runner}}; + }; + + // Inspects the suite's properties once the suite has ended, then destroys + // the environment. gtest tells its listeners about a suite ending in + // reverse order of registration, so this runs after the environment's + // listener as long as it is registered first. + class SuiteEndChecker : public testing::EmptyTestEventListener { + public: + explicit SuiteEndChecker(const testing::TestSuite* suite) + : suite_under_test_(suite) {} + + void OnTestSuiteEnd(const testing::TestSuite& suite) override { + if (&suite != suite_under_test_ || suite_ == nullptr) return; + // The suite's own properties hold what SetUpTestSuite() checked and the + // statistics of the whole suite. + std::vector<std::pair<std::string, std::string>> properties = + Properties(suite.ad_hoc_test_result()); + EXPECT_THAT(properties, + Contains(Pair(kSetUpTestSuiteTestName, "P0 PASS"))); + EXPECT_THAT(properties, Contains(Pair("expected_successes", "2"))); + EXPECT_THAT(properties, Contains(Pair("unexpected_failures", "0"))); + delete suite_; + suite_ = nullptr; + } + + private: + const testing::TestSuite* const suite_under_test_; + }; + + static void SetUpTestSuite() { + testing::UnitTest& unit_test = *testing::UnitTest::GetInstance(); + // gtest owns the checker and deletes it when the program ends. + unit_test.listeners().Append( + new SuiteEndChecker(unit_test.current_test_suite())); + suite_ = new SuiteState(); + // A result checked outside any test is the suite's. It is tallied at + // once, before the first test starts, so it is neither counted toward that + // test's statistics nor reported as unchecked when it ends, and it is + // reported as a property of the suite, not of the test. + EXPECT_THAT( + ConformanceEnvironment::Get() + .testee() + .CreateTest("SetUpTestSuite", kP0) + .ParseBinary(TestAllTypesProto3::descriptor(), VarintField(1, 99)) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(R"pb(optional_int32: 99)pb")))); + } + + static void TearDownTestSuite() { + // The test's properties hold what Yields() recorded for Bar and the + // statistics of the test alone. + const testing::TestSuite& suite = + *testing::UnitTest::GetInstance()->current_test_suite(); + ASSERT_EQ(suite.total_test_count(), 1); + std::vector<std::pair<std::string, std::string>> properties = + Properties(*suite.GetTestInfo(0)->result()); + EXPECT_THAT(properties, Contains(Pair(kBarTestName, "P0 PASS"))); + EXPECT_THAT(properties, Contains(Pair("expected_successes", "1"))); + EXPECT_THAT(properties, Contains(Pair("unexpected_failures", "0"))); + EXPECT_THAT(properties, Not(Contains(Key(kSetUpTestSuiteTestName)))); + } + + static SuiteState* suite_; +}; + +LongLivedEnvironmentTest::SuiteState* LongLivedEnvironmentTest::suite_ = + nullptr; + +TEST_F(LongLivedEnvironmentTest, ReportsResultsWhenTheTestEnds) { + EXPECT_THAT( + CreateP0Test("Bar") + .ParseBinary(TestAllTypesProto3::descriptor(), VarintField(1, 99)) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(R"pb(optional_int32: 99)pb")))); +} + TEST_F(ConformanceEnvironmentTest, ExpectedFailureFromFailureListPasses) { std::string failure_list = WriteFile("failures.txt", @@ -291,8 +502,8 @@ EXPECT_THAT(RunP0("Foo"), Yields(IsParseError())); - EXPECT_EQ(env->test_manager().expected_failures(), 1); - EXPECT_EQ(env->test_manager().unexpected_failures(), 0); + EXPECT_EQ(env->ledger().expected_failures(), 1); + EXPECT_EQ(env->ledger().unexpected_failures(), 0); env->TearDown(); } @@ -309,7 +520,7 @@ EXPECT_THAT(RunP0("Foo"), Yields(IsParseError())); EXPECT_THAT(RunP0("Bar"), Yields(IsParseError())); - EXPECT_EQ(env->test_manager().expected_failures(), 2); + EXPECT_EQ(env->ledger().expected_failures(), 2); env->TearDown(); } @@ -348,9 +559,9 @@ EXPECT_THAT(RunP1("Foo"), Yields(IsParseError())); - EXPECT_EQ(env->test_manager().tolerated_failures(), 1); - EXPECT_EQ(env->test_manager().skipped(), 0); - EXPECT_EQ(env->test_manager().unexpected_failures(), 0); + EXPECT_EQ(env->ledger().tolerated_failures(), 1); + EXPECT_EQ(env->ledger().skipped(), 0); + EXPECT_EQ(env->ledger().unexpected_failures(), 0); env->TearDown(); } @@ -369,8 +580,8 @@ EXPECT_THAT(RunP1("Foo"), Yields(IsParseError())); - EXPECT_EQ(env->test_manager().expected_failures(), 1); - EXPECT_EQ(env->test_manager().tolerated_failures(), 0); + EXPECT_EQ(env->ledger().expected_failures(), 1); + EXPECT_EQ(env->ledger().tolerated_failures(), 0); // ...and so it isn't unseen either. internal::ScopedPartialRunOverride full_run(false); env->TearDown(); @@ -415,6 +626,28 @@ "\nRemove them from the failure list, or rerun with --fix")); } +TEST_F(ConformanceEnvironmentTest, TearDownFailsForAnOverexpandedWildcard) { + std::string entry = "Required.Proto3.ProtobufInput.*.ProtobufOutput"; + std::string failure_list = WriteFile( + "failures.txt", absl::StrCat(entry, " # Should have failed to parse\n")); + ConformanceEnvironmentOptions options; + options.runner = &fake_runner_; + options.failure_list_files = {failure_list}; + internal::ScopedGlobalConformanceEnvironment env(std::move(options)); + env->SetUp(); + + // Each test is an expected failure; only their number is the problem. + for (int i = 0; i <= internal::kMaximumWildcardExpansions; ++i) { + EXPECT_THAT(RunP0(absl::StrCat("Test", i)), Yields(IsParseError())); + } + + internal::ScopedPartialRunOverride full_run(false); + EXPECT_NONFATAL_FAILURE( + env->TearDown(), + absl::StrCat("The failure list entry ", entry, " matched more than ", + internal::kMaximumWildcardExpansions, " tests")); +} + TEST_F(ConformanceEnvironmentTest, UnseenExpectedFailuresAreFineInPartialRun) { std::string failure_list = WriteFile("failures.txt", absl::StrCat(kFooTestName, "\n")); @@ -426,8 +659,8 @@ internal::ScopedPartialRunOverride partial_run(true); env->TearDown(); - EXPECT_THAT(env->test_manager().Finalize().message(), - HasSubstr(absl::StrCat("were not seen: ", kFooTestName))); + EXPECT_THAT(env->ledger().UnmatchedExpectedFailures(), + ElementsAre(kFooTestName)); } TEST_F(ConformanceEnvironmentTest, UnseenCheckCanBeDisabled) { @@ -442,8 +675,8 @@ internal::ScopedPartialRunOverride full_run(false); env->TearDown(); - EXPECT_THAT(env->test_manager().Finalize().message(), - HasSubstr(absl::StrCat("were not seen: ", kFooTestName))); + EXPECT_THAT(env->ledger().UnmatchedExpectedFailures(), + ElementsAre(kFooTestName)); } TEST_F(ConformanceEnvironmentTest, FixWritesToTheOutputFile) { @@ -459,8 +692,8 @@ env->SetUp(); // Foo (listed) is never run; Bar (unlisted) fails. - EXPECT_NONFATAL_FAILURE(EXPECT_THAT(RunP0("Bar"), Yields(IsParseError())), - "Should have failed to parse, but didn't."); + EXPECT_YIELDS_FAILURE(EXPECT_THAT(RunP0("Bar"), Yields(IsParseError())), + "Should have failed to parse, but didn't."); // The unseen entry is still reported, but the message points at the // rewritten list instead of suggesting --fix. TearDownExpectingUnseenFailures( @@ -489,8 +722,8 @@ internal::ScopedGlobalConformanceEnvironment env(std::move(options)); env->SetUp(); - EXPECT_NONFATAL_FAILURE(EXPECT_THAT(RunP0("Bar"), Yields(IsParseError())), - "Should have failed to parse, but didn't."); + EXPECT_YIELDS_FAILURE(EXPECT_THAT(RunP0("Bar"), Yields(IsParseError())), + "Should have failed to parse, but didn't."); TearDownExpectingUnseenFailures( env.environment(), absl::StrCat("updated failure list written to ", failure_list)); @@ -520,8 +753,8 @@ internal::ScopedGlobalConformanceEnvironment env(std::move(options)); env->SetUp(); - EXPECT_NONFATAL_FAILURE(EXPECT_THAT(RunP0("Bar"), Yields(IsParseError())), - "Should have failed to parse, but didn't."); + EXPECT_YIELDS_FAILURE(EXPECT_THAT(RunP0("Bar"), Yields(IsParseError())), + "Should have failed to parse, but didn't."); TearDownExpectingUnseenFailures( env.environment(), absl::StrCat("updated failure list written to ", workspace, "/failures.txt")); @@ -553,6 +786,58 @@ EXPECT_EQ(ReadFile(TmpPath("runfiles/failures.txt")), ""); } +TEST_F(ConformanceEnvironmentTest, + FixResolvesRelativeOutputFileUnderBuildWorkspaceDirectory) { + // conformance_test() passes a relative --fix_output_file (and no + // --failure_list) when the failure list doesn't exist yet; --fix must then + // create it in the source tree, not in the runfiles. + std::string runfiles = CreateTmpDir("runfiles"); + std::string workspace = CreateTmpDir("workspace"); + ScopedWorkingDirectory cwd(runfiles); + ScopedEnvironmentVariable build_workspace_directory( + "BUILD_WORKSPACE_DIRECTORY", workspace); + + ConformanceEnvironmentOptions options; + options.runner = &fake_runner_; + options.fix = true; + options.fix_output_file = "failures.txt"; + internal::ScopedGlobalConformanceEnvironment env(std::move(options)); + env->SetUp(); + + EXPECT_YIELDS_FAILURE(EXPECT_THAT(RunP0("Bar"), Yields(IsParseError())), + "Should have failed to parse, but didn't."); + internal::ScopedPartialRunOverride full_run(false); + env->TearDown(); + + EXPECT_THAT(ReadFile(TmpPath("workspace/failures.txt")), + HasSubstr(kBarTestName)); + EXPECT_FALSE( + File::Exists(TmpPath("runfiles/failures.txt"))); +} + +TEST_F(ConformanceEnvironmentTest, + FixWithRelativeOutputFileWritesToTheWorkingDirectoryOutsideBlazeRun) { + std::string runfiles = CreateTmpDir("runfiles"); + ScopedWorkingDirectory cwd(runfiles); + ScopedEnvironmentVariable build_workspace_directory( + "BUILD_WORKSPACE_DIRECTORY", absl::nullopt); + + ConformanceEnvironmentOptions options; + options.runner = &fake_runner_; + options.fix = true; + options.fix_output_file = "failures.txt"; + internal::ScopedGlobalConformanceEnvironment env(std::move(options)); + env->SetUp(); + + EXPECT_YIELDS_FAILURE(EXPECT_THAT(RunP0("Bar"), Yields(IsParseError())), + "Should have failed to parse, but didn't."); + internal::ScopedPartialRunOverride full_run(false); + env->TearDown(); + + EXPECT_THAT(ReadFile(TmpPath("runfiles/failures.txt")), + HasSubstr(kBarTestName)); +} + TEST_F(ConformanceEnvironmentTest, FixWithMultipleListsNeedsOutputFile) { std::string first = WriteFile("first.txt", ""); std::string second = WriteFile("second.txt", ""); @@ -593,8 +878,8 @@ internal::ScopedGlobalConformanceEnvironment env(std::move(options)); env->SetUp(); - EXPECT_NONFATAL_FAILURE(EXPECT_THAT(RunP0("Bar"), Yields(IsParseError())), - "Should have failed to parse, but didn't."); + EXPECT_YIELDS_FAILURE(EXPECT_THAT(RunP0("Bar"), Yields(IsParseError())), + "Should have failed to parse, but didn't."); // Rewriting would drop Foo, whose test simply wasn't selected. The unseen // check itself stays quiet in a partial run, so this is the only failure. @@ -615,8 +900,8 @@ internal::ScopedGlobalConformanceEnvironment env(std::move(options)); env->SetUp(); - EXPECT_NONFATAL_FAILURE(EXPECT_THAT(RunP0("Bar"), Yields(IsParseError())), - "Should have failed to parse, but didn't."); + EXPECT_YIELDS_FAILURE(EXPECT_THAT(RunP0("Bar"), Yields(IsParseError())), + "Should have failed to parse, but didn't."); internal::ScopedPartialRunOverride full_run(false); EXPECT_NONFATAL_FAILURE( @@ -624,18 +909,43 @@ absl::StrCat("Failed to write updated failure list to ", not_a_file)); } +TEST_F(ConformanceEnvironmentTest, FixDropsListedSkips) { + std::string failure_list = + WriteFile("failures.txt", absl::StrCat(kFooTestName, " # listed\n")); + std::string fixed = TmpPath("fixed.txt"); + MockTestRunner mock; + ConformanceEnvironmentOptions options; + options.runner = &mock; + options.failure_list_files = {failure_list}; + options.fix = true; + options.fix_output_file = fixed; + internal::ScopedGlobalConformanceEnvironment env(std::move(options)); + env->SetUp(); + + // Foo (listed) is skipped by the testee, which fails it (see matchers.h); + // the rewritten list no longer has its entry, so the fixed run passes. + EXPECT_CALL(mock, RunTest(kFooTestName, _)) + .WillOnce(Return(SerializedResponse(R"pb(skipped: "not supported")pb"))); + EXPECT_YIELDS_FAILURE(EXPECT_THAT(RunP0("Foo"), Yields(IsParseError())), + "is in the failure list but was skipped by the testee"); + internal::ScopedPartialRunOverride full_run(false); + env->TearDown(); + + EXPECT_THAT(ReadFile(fixed), Not(HasSubstr(kFooTestName))); +} + TEST_F(ConformanceEnvironmentTest, StatisticsSnapshotAndDelta) { internal::ScopedGlobalConformanceEnvironment env({/*runner=*/&fake_runner_}); env->SetUp(); - internal::Statistics before = internal::Statistics::From(env->test_manager()); + internal::Statistics before = internal::Statistics::From(env->ledger()); EXPECT_THAT(RunP0("Foo"), Yields(WhenParsed(EqualsTextProto(R"pb(optional_int32: 99)pb")))); - EXPECT_NONFATAL_FAILURE(EXPECT_THAT(RunP0("Bar"), Yields(IsParseError())), - "Should have failed to parse"); + EXPECT_YIELDS_FAILURE(EXPECT_THAT(RunP0("Bar"), Yields(IsParseError())), + "Should have failed to parse"); internal::Statistics delta = - internal::Statistics::From(env->test_manager()) - before; + internal::Statistics::From(env->ledger()) - before; EXPECT_EQ(delta.expected_successes, 1); EXPECT_EQ(delta.unexpected_failures, 1); EXPECT_EQ(delta.expected_failures, 0); @@ -661,18 +971,17 @@ options.failure_list_files = {failure_list}; internal::ScopedGlobalConformanceEnvironment env(std::move(options)); env->SetUp(); - internal::Statistics before = internal::Statistics::From(env->test_manager()); + internal::Statistics before = internal::Statistics::From(env->ledger()); // A listed test the testee skips fails (see matchers.h) and is both a skip // and a listed skip, but neither an expected nor an unexpected anything. EXPECT_CALL(mock, RunTest(kFooTestName, _)) .WillOnce(Return(SerializedResponse(R"pb(skipped: "not supported")pb"))); - EXPECT_NONFATAL_FAILURE( - EXPECT_THAT(RunP0("Foo"), Yields(IsParseError())), - "is in the failure list but was skipped by the testee"); + EXPECT_YIELDS_FAILURE(EXPECT_THAT(RunP0("Foo"), Yields(IsParseError())), + "is in the failure list but was skipped by the testee"); internal::Statistics delta = - internal::Statistics::From(env->test_manager()) - before; + internal::Statistics::From(env->ledger()) - before; EXPECT_EQ(delta.skipped_tests, 1); EXPECT_EQ(delta.listed_skips, 1); EXPECT_EQ(delta.expected_failures, 0); @@ -731,7 +1040,7 @@ env->TearDown(); EXPECT_TRUE(destroyed); // The statistics survive the testee. - EXPECT_EQ(env->test_manager().expected_successes(), 1); + EXPECT_EQ(env->ledger().expected_successes(), 1); } TEST_F(ConformanceEnvironmentTest, TearDownWithoutSetUpOnlyReleasesTheTestee) { @@ -755,7 +1064,7 @@ EXPECT_TRUE(destroyed); EXPECT_FALSE(File::Exists(fixed)); EXPECT_THAT(ReadFile(failure_list), HasSubstr("never seen")); - EXPECT_EQ(env->test_manager().expected_failures(), 0); + EXPECT_EQ(env->ledger().expected_failures(), 0); } TEST_F(ConformanceEnvironmentTest, OwnedRunnerDiesWithTheEnvironment) { @@ -773,6 +1082,32 @@ EXPECT_DEATH(ConformanceEnvironment::Get(), "Install"); } +TEST(ConformanceEnvironmentDeathTest, + UncheckedResultsAreNotReportedAfterAFatalFailure) { + // An ASSERT_* that doesn't hold returns from the test before it can check + // its results. A real (non-intercepted) fatal failure would fail this test, + // so the scenario runs in a death test child that exits with the number of + // stray failures. + FakeTestRunner runner; + EXPECT_EXIT( + { + internal::ScopedGlobalConformanceEnvironment env({/*runner=*/&runner}); + internal::TestResult unchecked = + CreateP0Test("Foo") + .ParseBinary(TestAllTypesProto3::descriptor(), + VarintField(1, 99)) + .SerializeBinary(); + // What ASSERT_* records before returning. + [] { FAIL() << "fatal failure"; }(); + ABSL_CHECK(testing::Test::HasFatalFailure()); + // std::exit() runs no destructors, so the mock is leaked on purpose. + testing::Mock::AllowLeak(&runner); + std::exit(static_cast<int>( + UncheckedResultFailures(env.environment()).size())); + }, + testing::ExitedWithCode(0), ""); +} + TEST(ConformanceEnvironmentDeathTest, RequiresExactlyOneTestee) { FakeTestRunner runner; EXPECT_DEATH(internal::ScopedGlobalConformanceEnvironment env({}),
diff --git a/conformance/test_fixture.cc b/conformance/test_fixture.cc index 5958da9..8749585 100644 --- a/conformance/test_fixture.cc +++ b/conformance/test_fixture.cc
@@ -11,7 +11,6 @@ #include "google/protobuf/descriptor.pb.h" #include <gtest/gtest.h> -#include "absl/container/flat_hash_map.h" #include "absl/log/absl_check.h" #include "absl/strings/string_view.h" #include "conformance/test_environment.h" @@ -25,32 +24,27 @@ namespace { using ::google::protobuf::conformance::internal::ConformanceEnvironment; -using ::google::protobuf::conformance::internal::Statistics; // Whether tests for `descriptor`'s message type run under `maximum_edition`: // the file's edition must not be newer than it. bool IsSupported(const Descriptor& descriptor, Edition maximum_edition) { Edition edition = FileDescriptorLegacy(descriptor.file()).edition(); if (edition == EDITION_UNSTABLE) { - // `maximum_edition` is the maximum *stable* edition. Like the legacy - // runner, run the unstable tests alongside any editions tests. - // TODO: this runs the (newer-than-anything) unstable tests - // at --maximum_edition=2023 while a stable 2024 message would be skipped; - // gate EDITION_UNSTABLE on its own flag once legacy parity no longer - // matters. + // `maximum_edition` is the maximum *stable* edition; EDITION_UNSTABLE is + // newer than any of them, so it is gated on the editions tier instead: the + // unstable tests run whenever the 2023 tests do, as in the original suites + // and the open-source runner. Every test message the suites use is 2023 + // or older, so nothing is skipped that the unstable tests would leapfrog. + // TODO: once a test message of a newer stable edition + // exists, give EDITION_UNSTABLE its own opt-in (an `unstable_edition` + // option / flag / conformance_test() attribute) so that + // --maximum_edition=2023 keeps skipping the newer stable message while + // still running these. return maximum_edition >= EDITION_2023; } return edition <= maximum_edition; } -// Statistics accumulated by ConformanceTest::TearDown() for each test suite, -// keyed by suite name, until TearDownTestSuite() reports them. -absl::flat_hash_map<std::string, Statistics>& SuiteStatistics() { - static auto* suite_statistics = - new absl::flat_hash_map<std::string, Statistics>(); - return *suite_statistics; -} - std::string CurrentTestName() { const testing::TestInfo* test_info = testing::UnitTest::GetInstance()->current_test_info(); @@ -60,56 +54,18 @@ return test_info->name(); } -std::string CurrentTestSuiteName() { - const testing::TestSuite* test_suite = - testing::UnitTest::GetInstance()->current_test_suite(); - ABSL_CHECK(test_suite != nullptr) - << "ConformanceTest's TearDown hooks must run inside a test suite."; - return test_suite->name(); -} - } // namespace void ConformanceTest::SetUp() { - ConformanceEnvironment& environment = ConformanceEnvironment::Get(); - initial_statistics_ = Statistics::From(environment.test_manager()); - // TODO: Transitional. Goes away with the in-binary - // performance mode, see ConformanceEnvironmentOptions::performance. - if (IsPerformanceTest() && !environment.options().performance) { - GTEST_SKIP() << "Performance tests only run with --performance."; - } - if (!IsPerformanceTest() && environment.options().performance) { - GTEST_SKIP() << "Only performance tests run with --performance."; - } if (const Descriptor* message = MessageUnderTest(); message != nullptr && - !IsSupported(*message, environment.options().maximum_edition)) { + !IsSupported(*message, + ConformanceEnvironment::Get().options().maximum_edition)) { GTEST_SKIP() << "Skipping " << message->full_name() << " because its edition is newer than --maximum_edition."; } } -void ConformanceTest::TearDown() { - Statistics delta = - Statistics::From(ConformanceEnvironment::Get().test_manager()) - - initial_statistics_; - delta.RecordProperties(); - SuiteStatistics()[CurrentTestSuiteName()] += delta; -} - -void ConformanceTest::TearDownTestSuite() { - auto node = SuiteStatistics().extract(CurrentTestSuiteName()); - if (!node) { - // gtest only runs TearDownTestSuite() for suites with at least one test to - // run, and TearDown() runs even for tests skipped in SetUp(), so this only - // happens when a fatal failure in a SetUpTestSuite() skipped every test - // outright (which is already reported), or when a subclass's TearDown() - // doesn't call ConformanceTest::TearDown(). - return; - } - node.mapped().RecordProperties(); -} - internal::Test ConformanceTest::Testee() { return Testee(CurrentTestName()); } internal::Test ConformanceTest::Testee(absl::string_view name) {
diff --git a/conformance/test_fixture.h b/conformance/test_fixture.h index 4a0dcfd..38ab3f7 100644 --- a/conformance/test_fixture.h +++ b/conformance/test_fixture.h
@@ -8,9 +8,8 @@ // The gtest fixture for conformance test suites. // // ConformanceTest is the fixture every conformance test should use. It -// handles edition gating, per-test statistics and, for now, performance-test -// filtering. A suite overrides DefaultPriority() (see TestPriority in -// testee.h) to say how important its tests are. +// handles edition gating. A suite overrides DefaultPriority() (see +// TestPriority in testee.h) to say how important its tests are. // // Its Testee() builds a test against the global testee, at the suite's // priority or at the one it is given. @@ -52,7 +51,6 @@ #include <gtest/gtest.h> #include "absl/base/nullability.h" #include "absl/strings/string_view.h" -#include "conformance/test_environment.h" #include "conformance/testee.h" #include "google/protobuf/descriptor.h" @@ -60,9 +58,8 @@ namespace protobuf { namespace conformance { -// The fixture for all conformance tests. Tests that aren't performance tests -// derive from this directly. Performance tests derive from the transitional -// PerformanceConformanceTest. A suite overrides the defaults below as needed: +// The fixture for all conformance tests. A suite subclasses it and overrides +// the defaults below as needed: // // class FooTest : public ConformanceTest { // private: @@ -74,12 +71,19 @@ // Yields(WhenParsed(EqualsBinaryProto(input)))); // } // -// Subclasses that override SetUp(), TearDown() or TearDownTestSuite() must -// call the base implementation, first thing in SetUp() and last thing in -// TearDown() and TearDownTestSuite(), so that the statistics recorded there -// cover the whole test. The base SetUp() may skip the test. gtest only skips -// the test body on GTEST_SKIP(), so an overriding SetUp() must bail out itself -// afterwards: +// Naming rule: a fixture that belongs to the performance conformance suite +// (conformance_suite(name = "performance") in the BUILD file) must be named +// `*PerformanceTest`, and no fixture in any other suite may be. The +// performance suite's own test binaries need no such rule. +// conformance_test_runner, however, links every suite into one binary and +// selects the performance tests for its --performance flag by that suffix +// (see PerformanceGtestFilter() in conformance_test_runner.h). Nothing checks +// the rule mechanically. A misnamed fixture silently runs with the wrong flag. +// TODO: the rule goes away with conformance_test_runner. +// +// Subclasses that override SetUp() must call the base implementation first +// thing. It may skip the test. gtest only skips the test body on +// GTEST_SKIP(), so an overriding SetUp() must bail out itself afterwards: // // void SetUp() override { // ConformanceTest::SetUp(); @@ -96,25 +100,10 @@ // A single test can say otherwise with Testee(priority). virtual TestPriority DefaultPriority() const { return kP0; } - // Skips the test if it isn't the kind (performance or regular) selected by - // the environment or if MessageUnderTest() isn't supported under - // --maximum_edition, and snapshots statistics for TearDown(). + // Skips the test if MessageUnderTest() isn't supported under + // --maximum_edition. void SetUp() override; - // Records this test's statistics as test properties and accumulates them for - // the suite. - void TearDown() override; - - // Records the suite's accumulated statistics as suite properties. - static void TearDownTestSuite(); - - // Whether this is a performance test. Performance tests only run when the - // environment was configured with `performance = true`, and regular tests - // only run when it wasn't. - // TODO: Transitional. Goes away with the in-binary - // performance mode, see ConformanceEnvironmentOptions::performance. - virtual bool IsPerformanceTest() const { return false; } - // The message type this test exercises, if the fixture knows it. When // non-null, SetUp() skips the test unless the message is supported under // the current --maximum_edition, so that the test can use it @@ -144,17 +133,6 @@ internal::Test Testee(absl::string_view name); internal::Test Testee(TestPriority priority); internal::Test Testee(TestPriority priority, absl::string_view name); - - private: - internal::Statistics initial_statistics_; -}; - -// The fixture for performance conformance tests. -// TODO: Transitional. Goes away with the in-binary -// performance mode, see ConformanceEnvironmentOptions::performance. -class PerformanceConformanceTest : public ConformanceTest { - protected: - bool IsPerformanceTest() const override { return true; } }; } // namespace conformance
diff --git a/conformance/test_fixture_test.cc b/conformance/test_fixture_test.cc index 9442d39..93588b9 100644 --- a/conformance/test_fixture_test.cc +++ b/conformance/test_fixture_test.cc
@@ -105,12 +105,11 @@ Yields(WhenParsed(EqualsTextProto(R"pb(optional_int32: 99)pb")))); } -// A ConformanceTest whose SetUp() and TearDown() leave the global environment -// alone, so that Testee() can be called without one. +// A ConformanceTest whose SetUp() leaves the global environment alone, so +// that Testee() can be called without one. class TesteeDeathTest : public ConformanceTest { protected: void SetUp() override {} - void TearDown() override {} }; TEST_F(TesteeDeathTest, WithoutInstall) { EXPECT_DEATH(Testee(), "Install"); } @@ -209,17 +208,6 @@ FAIL() << "SetUp() should have skipped this test."; } -// TODO: Transitional. Goes away with the in-binary -// performance mode, see ConformanceEnvironmentOptions::performance. -class PerformanceFixtureTest : public FixtureTest { - protected: - bool IsPerformanceTest() const override { return true; } -}; - -TEST_F(PerformanceFixtureTest, IsSkippedWithoutPerformanceMode) { - FAIL() << "SetUp() should have skipped this test."; -} - // Checks, from an overriding SetUp(), that the documented `if (IsSkipped()) // return;` pattern sees the base class's skip. class SkipAwareSetUpTest : public Edition2023MessageTest {
diff --git a/conformance/test_manager.cc b/conformance/test_manager.cc deleted file mode 100644 index cb626c8..0000000 --- a/conformance/test_manager.cc +++ /dev/null
@@ -1,398 +0,0 @@ -#include "conformance/test_manager.h" - -#include <algorithm> -#include <cctype> -#include <cstddef> -#include <fstream> -#include <ios> -#include <string> -#include <utility> -#include <vector> - -#include "absl/algorithm/container.h" -#include "absl/container/flat_hash_set.h" -#include "absl/log/absl_check.h" -#include "absl/log/absl_log.h" -#include "absl/status/status.h" -#include "absl/strings/ascii.h" -#include "absl/strings/match.h" -#include "absl/strings/str_cat.h" -#include "absl/strings/str_join.h" -#include "absl/strings/string_view.h" -#include "absl/types/optional.h" -#include "conformance/testee.h" - -namespace google { -namespace protobuf { -namespace conformance { -namespace internal { -namespace { - -// How many failures one wildcard entry may cover. As in the legacy runner the -// check precedes the count of the current match, so the limit is reached one -// failure later than the number says: the (kMaximumWildcardExpansions + 2)th -// is the first one reported. -constexpr int kMaximumWildcardExpansions = 20; -constexpr int kFailureMessageLengthLimit = 128; - -bool InsertUniqueTest(absl::flat_hash_set<std::string>& set, - absl::string_view value) { - if (!set.emplace(value).second) { - return false; - } - return true; -} - -void IncrementIfUnique(bool unique, int& counter) { - if (unique) { - ++counter; - } -} - -void Normalize(std::string& input) { - input.erase(std::remove(input.begin(), input.end(), '\n'), input.end()); -} - -// Sets up a failure message properly for our failure lists. -std::string FormatFailureMessage(absl::string_view input) { - std::string result(absl::StripAsciiWhitespace(input)); - // Remove newlines - Normalize(result); - // Truncate failure message if needed - if (result.length() > kFailureMessageLengthLimit) { - result = result.substr(0, kFailureMessageLengthLimit); - } - return result; -} - -std::string ReformatLine(size_t alignment, absl::string_view line) { - size_t comment_pos = line.find('#'); - absl::string_view test_name = - absl::StripAsciiWhitespace(line.substr(0, comment_pos)); - if (test_name.empty()) { - return std::string(absl::StripAsciiWhitespace(line)); - } - absl::string_view message = - absl::StripAsciiWhitespace(line.substr(comment_pos + 1)); - ABSL_CHECK_GE(alignment, test_name.length()); - std::string whitespace(alignment - test_name.length(), ' '); - return absl::StrCat(test_name, whitespace, " # ", message); -} - -} // namespace - -TestManager::~TestManager() { - if (!finalized_) { - ABSL_LOG(FATAL) - << "TestManager::Finalize() was not called before destruction."; - } -} - -absl::Status TestManager::LoadFailureList(absl::string_view filename) { - std::ifstream infile(std::string{filename}); - - if (!infile.is_open()) { - return absl::InternalError( - absl::StrCat("Couldn't open failure list file: ", filename)); - } - - for (std::string line; std::getline(infile, line);) { - failure_list_lines_.push_back(line); - - // Remove comments. - std::string test_name = line.substr(0, line.find('#')); - - test_name.erase( - std::remove_if(test_name.begin(), test_name.end(), ::isspace), - test_name.end()); - - if (test_name.empty()) { // Skip empty lines. - continue; - } - - // If we remove whitespace from the beginning of a line, and what we have - // left at first is a '#', then we have a comment. - if (test_name[0] != '#') { - // Find our failure message if it exists. Will be set to an empty string - // if no message is found. Empty failure messages also pass our tests. - size_t comment_pos = line.find('#'); - std::string message; - if (comment_pos != std::string::npos) { - message = line.substr(comment_pos + 1); // +1 to skip the delimiter - // If we had only whitespace after the delimiter, we will have an empty - // failure message and the test will still pass. - message = std::string(absl::StripAsciiWhitespace(message)); - } - if (absl::Status status = AddExpectedFailure(test_name, message); - !status.ok()) { - return status; - } - } - } - - return absl::OkStatus(); -} - -absl::Status TestManager::AddExpectedFailure( - absl::string_view test_name, absl::string_view failure_message) { - if (absl::Status status = expected_failure_list_.Insert(test_name); - !status.ok()) { - return status; - } - // Normalize the message the same way reported failures are normalized so - // that the two compare equal in ReportFailure. - ABSL_CHECK(expected_failure_messages_ - .emplace(test_name, FormatFailureMessage(failure_message)) - .second); - ABSL_CHECK(unseen_expected_failures_.emplace(test_name).second); - ABSL_CHECK(unmatched_expected_failures_.emplace(test_name).second); - return absl::OkStatus(); -} - -absl::Status TestManager::SaveFailureList(absl::string_view filename) const { - std::ofstream outfile(std::string{filename}, std::ios::binary); - if (!outfile.is_open()) { - return absl::InternalError(absl::StrCat( - "Couldn't open failure list file for writing: ", filename)); - } - - // Calculate alignment. - size_t alignment = 0; - for (const std::string& line : failure_list_lines_) { - absl::string_view test_name = absl::StripAsciiWhitespace( - absl::string_view(line).substr(0, line.find('#'))); - alignment = std::max(alignment, test_name.length()); - } - for (const auto& failure : new_failures_) { - alignment = std::max(alignment, failure.first.length()); - } - - // Output existing failure list, stripping out unseen tests and inserting - // new failures. - auto to_add = new_failures_.begin(); - for (const std::string& line : failure_list_lines_) { - absl::string_view test_name = absl::StripAsciiWhitespace( - absl::string_view(line).substr(0, line.find('#'))); - if (unseen_expected_failures_.contains(test_name) || - seen_unexpected_successes_.contains(test_name)) { - continue; - } - while (to_add != new_failures_.end() && test_name > to_add->first) { - outfile << ReformatLine(alignment, absl::StrCat(to_add->first, " # ", - to_add->second)) - << "\n"; - ++to_add; - } - outfile << ReformatLine(alignment, line) << "\n"; - } - - // Add any remaining new failures. - while (to_add != new_failures_.end()) { - outfile << ReformatLine(alignment, - absl::StrCat(to_add->first, " # ", to_add->second)) - << "\n"; - ++to_add; - } - - outfile.close(); - if (!outfile.good()) { - return absl::InternalError( - absl::StrCat("Failed to write failure list file: ", filename)); - } - return absl::OkStatus(); -} - -absl::Status TestManager::ReportSuccess(absl::string_view test_name) { - bool unique = InsertUniqueTest(seen_tests_, test_name); - absl::optional<std::string> failure_match = MarkMatched(test_name); - - if (failure_match.has_value()) { - // This was expected to fail, but it succeeded. - IncrementIfUnique(unique, number_of_matches_[*failure_match]); - IncrementIfUnique(unique, unexpected_successes_); - if (unique) { - unexpected_success_matches_[test_name] = *failure_match; - } - unseen_expected_failures_.erase(*failure_match); - seen_unexpected_successes_.insert(*failure_match); - return absl::FailedPreconditionError(absl::StrCat( - "test ", test_name, " (matched to ", *failure_match, - ") is in the failure list, but test succeeded. Remove its match from " - "the failure list.")); - } - - // This wasn't expected to fail. - IncrementIfUnique(unique, expected_successes_); - return absl::OkStatus(); -} - -absl::Status TestManager::ReportFailure(absl::string_view test_name, - TestPriority priority, - absl::string_view failure_message) { - bool unique = InsertUniqueTest(seen_tests_, test_name); - absl::optional<std::string> failure_match = MarkMatched(test_name); - - std::string formatted_failure_message = FormatFailureMessage(failure_message); - // Counts the failure as unexpected and records it for UnexpectedFailures(). - auto record_unexpected_failure = [&] { - IncrementIfUnique(unique, unexpected_failures_); - if (unique) { - unexpected_failure_messages_[test_name] = formatted_failure_message; - } - }; - - if (!failure_match.has_value()) { - if (priority > enforcement_level_) { - // Tolerated: neither a failure nor a skip, and not written to the - // failure list; only counted. - IncrementIfUnique(unique, tolerated_failures_); - ABSL_LOG(WARNING) << "WARNING, test=" << test_name << ": " - << absl::StripTrailingAsciiWhitespace(failure_message); - return absl::OkStatus(); - } - // This was not expected to fail. - record_unexpected_failure(); - new_failures_[test_name] = formatted_failure_message; - return absl::FailedPreconditionError( - absl::StrCat("Unexpected failure for test: ", test_name)); - } - - // Mirror the legacy runner, which only requires the actual failure message to - // start with the expected one. - const std::string& expected_failure_message = - expected_failure_messages_.at(*failure_match); - if (!absl::StartsWith(formatted_failure_message, expected_failure_message)) { - record_unexpected_failure(); - // TODO: b/563659620 - Keying the replacement by the (possibly wildcard) - // entry duplicates the line if another test later matches the entry with - // the expected message: with `foo.*.bar # abc` listed, - // ReportFailure("foo.a.bar", "xyz") + ReportFailure("foo.b.bar", "abc") - // makes SaveFailureList() keep the original line and add a second - // `foo.*.bar` line, which LoadFailureList() then rejects. - new_failures_[*failure_match] = formatted_failure_message; - return absl::FailedPreconditionError(absl::StrCat( - "Unexpected failure message for test: ", test_name, " expected: ", - expected_failure_message, " actual: ", formatted_failure_message)); - } - - unseen_expected_failures_.erase(*failure_match); - - if (number_of_matches_[*failure_match] > kMaximumWildcardExpansions) { - record_unexpected_failure(); - return absl::FailedPreconditionError( - absl::StrCat("The wildcard ", *failure_match, - " served as matches to too many test " - "names exceeding the max amount of ", - kMaximumWildcardExpansions, " for test: ", test_name)); - } - - IncrementIfUnique(unique, number_of_matches_[*failure_match]); - IncrementIfUnique(unique, expected_failures_); - ABSL_LOG(INFO) << "Ignoring expected failure for test " << test_name << ": " - << absl::StripTrailingAsciiWhitespace(failure_message); - - return absl::OkStatus(); -} - -absl::Status TestManager::ReportSkip(absl::string_view test_name, - absl::string_view skip_reason) { - bool unique = InsertUniqueTest(seen_tests_, test_name); - IncrementIfUnique(unique, skipped_); - absl::optional<std::string> failure_match = MarkMatched(test_name); - if (!failure_match.has_value()) { - return absl::OkStatus(); - } - - // A skip says nothing about whether the entry is still needed, so it is - // neither reported as unseen by Finalize() nor dropped by SaveFailureList(). - // Nor does it count toward the entry's wildcard expansion cap - // (number_of_matches_), unlike in the legacy runner, which counted every - // test a wildcard matched: the cap is about real outcomes hidden by a - // wildcard, and a skip isn't one. - unseen_expected_failures_.erase(*failure_match); - IncrementIfUnique(unique, listed_skips_); - if (unique) { - listed_skip_matches_[test_name] = *failure_match; - } - return absl::FailedPreconditionError(absl::StrCat( - "test ", test_name, " (matched to ", *failure_match, - ") is in the failure list but was skipped by the testee: ", skip_reason, - ". Remove its match from the failure list.")); -} - -void TestManager::ReportNotSelected(absl::string_view test_name) { - seen_tests_.emplace(test_name); - MarkMatched(test_name); -} - -absl::optional<std::string> TestManager::MarkMatched( - absl::string_view test_name) { - absl::optional<std::string> failure_match = - expected_failure_list_.WalkDownMatch(test_name); - if (failure_match.has_value()) { - unmatched_expected_failures_.erase(*failure_match); - } - return failure_match; -} - -std::vector<std::string> TestManager::UnseenExpectedFailures() const { - std::vector<std::string> unseen(unseen_expected_failures_.begin(), - unseen_expected_failures_.end()); - absl::c_sort(unseen); - return unseen; -} - -std::vector<std::string> TestManager::UnmatchedExpectedFailures() const { - std::vector<std::string> unmatched(unmatched_expected_failures_.begin(), - unmatched_expected_failures_.end()); - absl::c_sort(unmatched); - return unmatched; -} - -std::vector<UnexpectedResult> TestManager::UnexpectedFailures() const { - std::vector<UnexpectedResult> failures; - failures.reserve(unexpected_failure_messages_.size()); - // The map is ordered by test name. - for (const auto& [test_name, failure_message] : - unexpected_failure_messages_) { - failures.push_back( - {/*test_name=*/test_name, /*failure_message=*/failure_message}); - } - return failures; -} - -std::vector<UnexpectedResult> TestManager::UnexpectedSuccesses() const { - std::vector<UnexpectedResult> successes; - successes.reserve(unexpected_success_matches_.size()); - // The map is ordered by test name. - for (const auto& [test_name, matched_entry] : unexpected_success_matches_) { - successes.push_back( - {/*test_name=*/test_name, - /*failure_message=*/expected_failure_messages_.at(matched_entry), - /*matched_entry=*/matched_entry}); - } - return successes; -} - -std::vector<std::pair<std::string, std::string>> TestManager::ListedSkips() - const { - // The map is ordered by test name. - return std::vector<std::pair<std::string, std::string>>( - listed_skip_matches_.begin(), listed_skip_matches_.end()); -} - -absl::Status TestManager::Finalize() { - finalized_ = true; - std::vector<std::string> unseen = UnseenExpectedFailures(); - if (!unseen.empty()) { - return absl::FailedPreconditionError( - absl::StrCat("The following expected failures were not seen: ", - absl::StrJoin(unseen, ", "))); - } - return absl::OkStatus(); -} - -} // namespace internal -} // namespace conformance -} // namespace protobuf -} // namespace google
diff --git a/conformance/test_manager.h b/conformance/test_manager.h deleted file mode 100644 index fcf5830..0000000 --- a/conformance/test_manager.h +++ /dev/null
@@ -1,232 +0,0 @@ -#ifndef GOOGLE_PROTOBUF_CONFORMANCE_TEST_MANAGER_H__ -#define GOOGLE_PROTOBUF_CONFORMANCE_TEST_MANAGER_H__ - -#include <string> -#include <utility> -#include <vector> - -#include "absl/container/btree_map.h" -#include "absl/container/flat_hash_map.h" -#include "absl/container/flat_hash_set.h" -#include "absl/status/status.h" -#include "absl/strings/string_view.h" -#include "absl/types/optional.h" -#include "conformance/conformance.pb.h" -#include "conformance/failure_list_trie_node.h" -#include "conformance/testee.h" - -namespace google { -namespace protobuf { -namespace conformance { -namespace internal { - -// A test whose result contradicted the failure list. See -// TestManager::UnexpectedFailures() and TestManager::UnexpectedSuccesses(). -// TODO: b/563707827 - Remove with conformance_test_runner. -struct UnexpectedResult { - // The full name of the test, as reported to the TestManager. - std::string test_name; - // For an unexpected failure, the test's failure message as - // TestManager::SaveFailureList() would write it. For an unexpected success, - // the message of the failure list entry that matched the test. - std::string failure_message; - // For an unexpected success, the failure list entry that matched - // `test_name`, possibly a wildcard. Unset for an unexpected failure, which - // matched no entry or matched one with a different message. - absl::optional<std::string> matched_entry; -}; - -// Tracks the expected failures and the actual results of a test suite. The -// conformance matchers (see matchers.h) report the outcome of every test -// here. The test environment reads the results back and reports them as test -// properties. Under --fix the results also populate the new failure list. -class TestManager { - public: - TestManager() : expected_failure_list_("root") {} - ~TestManager(); - - // The lowest priority whose failures fail the suite (see TestPriority in - // testee.h). ReportFailure() tolerates a failing test of a lower priority - // unless it is in the failure list. Defaults to kLowestPriority, which - // enforces every priority. - void set_enforcement_level(TestPriority level) { enforcement_level_ = level; } - - // Loads a failure list from disk and adds its entries to the ones loaded so - // far. Each line that isn't blank or a comment names one expected failure, - // optionally followed by `#` and the expected failure message. Returns an - // error if the file can't be opened. Also returns an error if an entry is - // already present, including one from an earlier file, or contains an - // invalid wildcard. - absl::Status LoadFailureList(absl::string_view filename); - - // Saves an updated failure list to disk based on the reported results. - // Returns an error if `filename` can't be opened for writing or the write - // fails. - absl::Status SaveFailureList(absl::string_view filename) const; - - // Reports a successful test run. This will return an error if the test was - // expected to fail. - absl::Status ReportSuccess(absl::string_view test_name); - - // Reports a failed test run along with the failure message. Returns OK if - // the failure doesn't fail the suite. That is the case when the test is in - // the failure list with a matching message (an expected failure, logged at - // INFO), or when it isn't listed and `priority` is above the enforcement - // level (a tolerated failure, logged as a WARNING and only counted by - // tolerated_failures()). Otherwise the failure is unexpected and the error - // says why. A listed test is checked whatever its priority, so that the - // failure list can't go stale unnoticed. The normalized actual message - // only needs to start with the expected message, so an empty expected - // message matches any failure. - absl::Status ReportFailure(absl::string_view test_name, TestPriority priority, - absl::string_view failure_message); - - // Reports a test that the testee skipped for `skip_reason`. A skip is not - // a verdict on the test's failure list entry, if any. The entry counts as - // seen and matched, so that Finalize() doesn't report it and - // SaveFailureList() keeps it, but the skip is not an expected failure. A - // listed test that is skipped is recorded (see ListedSkips()) and returns an - // error that names the matched entry, like an unexpected success. Whether - // that error fails the test is the caller's policy. The matchers fail such - // a test. - absl::Status ReportSkip(absl::string_view test_name, - absl::string_view skip_reason); - - // Reports a test the runner didn't run because it wasn't selected. - // The original conformance_test_runner matches a test name against the - // failure list before checking whether the test was selected. Like it, - // this only marks the entry the name matches, if any, as matched for - // UnmatchedExpectedFailures(). The test is not counted by any statistic - // and its entry stays unseen. - // TODO: b/563707827 - Remove with conformance_test_runner. - void ReportNotSelected(absl::string_view test_name); - - // Whether any of the Report*() methods has been called for `test_name`. - // Yields() checks this to reject a second check of the same test. - bool WasReported(absl::string_view test_name) const { - return seen_tests_.contains(test_name); - } - - // Runs sanity checks over the failure list to make sure everything we - // expected to run was reported. Returns an error naming the sorted expected - // failure entries that have not been reported as a failure, an unexpected - // success or a skip. Must be called before destruction. - absl::Status Finalize(); - - // Returns the sorted expected failure entries that no reported test name has - // matched so far, regardless of the outcome. Names are reported through - // ReportSuccess(), ReportFailure(), ReportSkip() or ReportNotSelected(). A - // wildcard entry counts as matched once it has matched any test name. - // Unlike the unseen entries Finalize() reports, an entry whose test was not - // selected is not returned. It is a real test, only not run here. - // TODO: b/563707827 - Remove with conformance_test_runner. - std::vector<std::string> UnmatchedExpectedFailures() const; - - // Returns the tests reported as unexpected failures so far, sorted by test - // name. These are the tests unexpected_failures() counts. Each result's - // `failure_message` is the message as SaveFailureList() would write it, and - // its `matched_entry` is unset. This includes tests that were expected to - // fail, but with a different message. SaveFailureList() replaces their - // entry, so adding these lines to the failure list would duplicate it. - // TODO: b/563707827 - Remove with conformance_test_runner. - std::vector<UnexpectedResult> UnexpectedFailures() const; - - // Returns the tests reported as unexpected successes so far, sorted by test - // name. These are the tests unexpected_successes() counts. Each result's - // `matched_entry` is the failure list entry the test matched, and - // `failure_message` is that entry's message. - // TODO: b/563707827 - Remove with conformance_test_runner. - std::vector<UnexpectedResult> UnexpectedSuccesses() const; - - // Returns the tests the testee skipped although they are in the failure - // list, sorted by test name. These are the tests listed_skips() counts. - // Each pair is the test name and the failure list entry it matched. Such a - // test fails the gtest run: the matchers fail it with the error - // ReportSkip() returns. It is not an unexpected failure or success. Its - // entry is kept as is, also by SaveFailureList() under --fix, so removing - // the entry is up to the user. - // TODO: b/563707827 - Remove with conformance_test_runner. - std::vector<std::pair<std::string, std::string>> ListedSkips() const; - - // The number of tests that were reported skipped. This includes the listed - // skips. - int skipped() const { return skipped_; } - - // The number of skipped tests that are in the failure list. See - // ListedSkips(). - int listed_skips() const { return listed_skips_; } - - // The number of tests that failed but were tolerated because their priority - // is above the enforcement level. - int tolerated_failures() const { return tolerated_failures_; } - - // The number of tests that were in the failure list and failed with the - // expected message. - int expected_failures() const { return expected_failures_; } - - // The number of tests that failed but were not in the failure list, or - // failed with a different message than the one listed. - int unexpected_failures() const { return unexpected_failures_; } - - // The number of tests that were not in the failure list and succeeded. - int expected_successes() const { return expected_successes_; } - - // The number of tests that were in the failure list but succeeded. - int unexpected_successes() const { return unexpected_successes_; } - - private: - // Adds one failure list entry, as LoadFailureList() does for each line. - // Returns an error if the test name is already present or contains an - // invalid wildcard. - absl::Status AddExpectedFailure(absl::string_view test_name, - absl::string_view failure_message); - - // The sorted expected failure entries Finalize() complains about: those not - // reported as a failure, an unexpected success or a skip so far. - std::vector<std::string> UnseenExpectedFailures() const; - - // Marks the failure list entry `test_name` matches, if any, as matched (see - // UnmatchedExpectedFailures()) and returns it. - absl::optional<std::string> MarkMatched(absl::string_view test_name); - - FailureListTrieNode expected_failure_list_; - absl::flat_hash_map<std::string, std::string> expected_failure_messages_; - - absl::flat_hash_set<std::string> unseen_expected_failures_; - // Entries never matched by name by any Report*() call. - absl::flat_hash_set<std::string> unmatched_expected_failures_; - absl::flat_hash_set<std::string> seen_unexpected_successes_; - absl::flat_hash_map<std::string, int> number_of_matches_; - - // Every test name reported so far (see WasReported()), so that a test - // reported more than once is only counted once. - absl::flat_hash_set<std::string> seen_tests_; - - // The tests counted by unexpected_failures_, mapped to their formatted - // failure message, and the tests counted by unexpected_successes_, mapped to - // the failure list entry they matched. - absl::btree_map<std::string, std::string> unexpected_failure_messages_; - absl::btree_map<std::string, std::string> unexpected_success_matches_; - // The tests counted by listed_skips_, mapped to the entry they matched. - absl::btree_map<std::string, std::string> listed_skip_matches_; - - std::vector<std::string> failure_list_lines_; - absl::btree_map<std::string, std::string> new_failures_; - - int skipped_ = 0; - int listed_skips_ = 0; - int tolerated_failures_ = 0; - int expected_failures_ = 0; - int unexpected_failures_ = 0; - int expected_successes_ = 0; - int unexpected_successes_ = 0; - bool finalized_ = false; - TestPriority enforcement_level_ = kLowestPriority; -}; - -} // namespace internal -} // namespace conformance -} // namespace protobuf -} // namespace google - -#endif // GOOGLE_PROTOBUF_CONFORMANCE_TEST_MANAGER_H__
diff --git a/conformance/test_manager_test.cc b/conformance/test_manager_test.cc deleted file mode 100644 index 1b33d18..0000000 --- a/conformance/test_manager_test.cc +++ /dev/null
@@ -1,1073 +0,0 @@ -#include "conformance/test_manager.h" - -#include <initializer_list> -#include <string> - -#include "google/protobuf/testing/file.h" -#include "google/protobuf/testing/file.h" -#include "google/protobuf/testing/file.h" -#include <gmock/gmock.h> -#include "google/protobuf/testing/googletest.h" -#include <gtest/gtest.h> -#include "absl/log/absl_check.h" -#include "absl/status/status.h" -#include "absl/status/status_matchers.h" -#include "absl/strings/str_cat.h" -#include "absl/strings/string_view.h" -#include "absl/types/optional.h" -#include "conformance/testee.h" - -namespace google { -namespace protobuf { -namespace conformance { -namespace internal { -namespace { - -using ::absl_testing::IsOk; -using ::absl_testing::StatusIs; -using ::testing::AllOf; -using ::testing::ElementsAre; -using ::testing::FieldsAre; -using ::testing::HasSubstr; -using ::testing::IsEmpty; -using ::testing::Not; -using ::testing::Optional; -using ::testing::Pair; -using ::testing::SizeIs; - -class TestManagerTest : public ::testing::Test { - protected: - TestManagerTest() - : tmp_dir_(absl::StrCat(TestTempDir(), "/test_manager_test/")), - failure_list_path_(absl::StrCat( - tmp_dir_, - testing::UnitTest::GetInstance()->current_test_info()->name())) { - if (!File::Exists(tmp_dir_)) { - ABSL_CHECK_OK( - File::RecursivelyCreateDir(tmp_dir_, 0777)); - } - } - - struct Failure { - absl::string_view name; - absl::string_view message; - - Failure( // NOLINT(google-explicit-constructor) - absl::string_view name) - : name(name), message("") {} - Failure(absl::string_view name, absl::string_view message) - : name(name), message(message) {} - }; - - void CreateFailureList( - std::initializer_list<const Failure> expected_failures) { - std::string content; - for (auto failure : expected_failures) { - absl::StrAppend(&content, failure.name, " # ", failure.message, "\n"); - } - ABSL_CHECK_OK( - File::SetContents(failure_list_path_, content, true)); - } - - void CreateFailureList(absl::string_view content) { - ABSL_CHECK_OK( - File::SetContents(failure_list_path_, content, true)); - } - - absl::string_view failure_list() const { return failure_list_path_; } - - ~TestManagerTest() override { - File::DeleteRecursively(failure_list_path_, NULL, NULL); - } - - private: - std::string tmp_dir_; - std::string failure_list_path_; - std::string output_path_; -}; - -TEST_F(TestManagerTest, RequiresFinalize) { - EXPECT_DEATH({ TestManager manager; }, "Finalize"); -} - -TEST_F(TestManagerTest, ReportSuccess) { - CreateFailureList({}); - TestManager manager; - ASSERT_THAT(manager.LoadFailureList(failure_list()), IsOk()); - - EXPECT_THAT(manager.ReportSuccess("foo"), IsOk()); - - EXPECT_THAT(manager.Finalize(), IsOk()); - EXPECT_EQ(manager.expected_failures(), 0); - EXPECT_EQ(manager.unexpected_failures(), 0); - EXPECT_EQ(manager.expected_successes(), 1); - EXPECT_EQ(manager.unexpected_successes(), 0); - EXPECT_EQ(manager.skipped(), 0); -} - -TEST_F(TestManagerTest, ReportSkipped) { - CreateFailureList({}); - TestManager manager; - ASSERT_THAT(manager.LoadFailureList(failure_list()), IsOk()); - - EXPECT_THAT(manager.ReportSkip("foo", "reason"), IsOk()); - - EXPECT_THAT(manager.Finalize(), IsOk()); - EXPECT_EQ(manager.expected_failures(), 0); - EXPECT_EQ(manager.unexpected_failures(), 0); - EXPECT_EQ(manager.expected_successes(), 0); - EXPECT_EQ(manager.unexpected_successes(), 0); - EXPECT_EQ(manager.skipped(), 1); - EXPECT_EQ(manager.listed_skips(), 0); - EXPECT_THAT(manager.ListedSkips(), IsEmpty()); -} - -TEST_F(TestManagerTest, ReportSkipMarksListedEntrySeenAndMatched) { - CreateFailureList({{"foo", "abc"}, {"bar.*", "abc"}, {"baz", "abc"}}); - TestManager manager; - ASSERT_THAT(manager.LoadFailureList(failure_list()), IsOk()); - - // A listed skip is reported like an unexpected success, naming the entry. - EXPECT_THAT(manager.ReportSkip("foo", "not supported"), - StatusIs(absl::StatusCode::kFailedPrecondition, - "test foo (matched to foo) is in the failure list but " - "was skipped by the testee: not supported. Remove its " - "match from the failure list.")); - EXPECT_THAT(manager.ReportSkip("bar.x", "no"), - StatusIs(absl::StatusCode::kFailedPrecondition, - "test bar.x (matched to bar.*) is in the failure list " - "but was skipped by the testee: no. Remove its match " - "from the failure list.")); - // Reporting a test twice returns the error again but counts it once. - EXPECT_THAT(manager.ReportSkip("bar.x", "no"), Not(IsOk())); - - // A skip is only counted as a skip, but the entries it matched are neither - // unmatched nor unseen: whether the entry is still needed is unknown. - EXPECT_THAT(manager.UnmatchedExpectedFailures(), ElementsAre("baz")); - EXPECT_THAT(manager.Finalize(), - StatusIs(absl::StatusCode::kFailedPrecondition, - HasSubstr("were not seen: baz"))); - EXPECT_EQ(manager.skipped(), 2); - EXPECT_EQ(manager.listed_skips(), 2); - EXPECT_THAT(manager.ListedSkips(), - ElementsAre(Pair("bar.x", "bar.*"), Pair("foo", "foo"))); - EXPECT_EQ(manager.expected_failures(), 0); - EXPECT_EQ(manager.unexpected_failures(), 0); - EXPECT_EQ(manager.expected_successes(), 0); - EXPECT_EQ(manager.unexpected_successes(), 0); - EXPECT_THAT(manager.UnexpectedFailures(), IsEmpty()); - EXPECT_THAT(manager.UnexpectedSuccesses(), IsEmpty()); -} - -TEST_F(TestManagerTest, ReportNotSelected) { - CreateFailureList({{"foo"}, {"bar.*"}, {"baz"}}); - TestManager manager; - ASSERT_THAT(manager.LoadFailureList(failure_list()), IsOk()); - - manager.ReportNotSelected("foo"); - manager.ReportNotSelected("bar.x"); - manager.ReportNotSelected("unlisted"); - - // Only the matched entries stop being unmatched; nothing else changes. - EXPECT_THAT(manager.UnmatchedExpectedFailures(), ElementsAre("baz")); - EXPECT_THAT(manager.Finalize(), - StatusIs(absl::StatusCode::kFailedPrecondition, - "The following expected failures were not seen: bar.*, " - "baz, foo")); - EXPECT_EQ(manager.expected_failures(), 0); - EXPECT_EQ(manager.unexpected_failures(), 0); - EXPECT_EQ(manager.expected_successes(), 0); - EXPECT_EQ(manager.unexpected_successes(), 0); - EXPECT_EQ(manager.skipped(), 0); - EXPECT_EQ(manager.tolerated_failures(), 0); -} - -TEST_F(TestManagerTest, WasReported) { - CreateFailureList({}); - TestManager manager; - ASSERT_THAT(manager.LoadFailureList(failure_list()), IsOk()); - EXPECT_FALSE(manager.WasReported("success")); - - EXPECT_THAT(manager.ReportSuccess("success"), IsOk()); - EXPECT_THAT(manager.ReportFailure("failure", kP0, "abc"), Not(IsOk())); - EXPECT_THAT(manager.ReportSkip("skip", "reason"), IsOk()); - manager.ReportNotSelected("not_selected"); - - EXPECT_TRUE(manager.WasReported("success")); - EXPECT_TRUE(manager.WasReported("failure")); - EXPECT_TRUE(manager.WasReported("skip")); - EXPECT_TRUE(manager.WasReported("not_selected")); - EXPECT_FALSE(manager.WasReported("other")); - EXPECT_THAT(manager.Finalize(), IsOk()); -} - -TEST_F(TestManagerTest, ReportExpectedFailure) { - CreateFailureList({{"foo", "abc"}}); - TestManager manager; - ASSERT_THAT(manager.LoadFailureList(failure_list()), IsOk()); - - EXPECT_THAT(manager.ReportFailure("foo", kP0, "abc"), IsOk()); - EXPECT_THAT(manager.Finalize(), IsOk()); - - EXPECT_EQ(manager.expected_failures(), 1); - EXPECT_EQ(manager.unexpected_failures(), 0); - EXPECT_EQ(manager.expected_successes(), 0); - EXPECT_EQ(manager.unexpected_successes(), 0); - EXPECT_EQ(manager.skipped(), 0); -} - -TEST_F(TestManagerTest, ReportDuplicates) { - CreateFailureList({{"foo", "abc"}}); - TestManager manager; - ASSERT_THAT(manager.LoadFailureList(failure_list()), IsOk()); - - ASSERT_THAT(manager.ReportSuccess("bar"), IsOk()); - ASSERT_THAT(manager.ReportSuccess("bar"), IsOk()); - ASSERT_THAT(manager.ReportSkip("baz", "reason"), IsOk()); - ASSERT_THAT(manager.ReportSkip("baz", "reason"), IsOk()); - ASSERT_THAT(manager.ReportFailure("foo", kP0, "abc"), IsOk()); - ASSERT_THAT(manager.ReportFailure("foo", kP0, "abc"), IsOk()); - ASSERT_THAT(manager.Finalize(), IsOk()); - - EXPECT_EQ(manager.expected_failures(), 1); - EXPECT_EQ(manager.unexpected_failures(), 0); - EXPECT_EQ(manager.expected_successes(), 1); - EXPECT_EQ(manager.unexpected_successes(), 0); - EXPECT_EQ(manager.skipped(), 1); -} - -TEST_F(TestManagerTest, ReportExpectedFailureWildcard) { - CreateFailureList({{"foo.*.bar", "abc"}}); - TestManager manager; - ASSERT_THAT(manager.LoadFailureList(failure_list()), IsOk()); - - EXPECT_THAT(manager.ReportFailure("foo.baz.bar", kP0, "abc"), IsOk()); - EXPECT_THAT(manager.Finalize(), IsOk()); - - EXPECT_EQ(manager.expected_failures(), 1); - EXPECT_EQ(manager.unexpected_failures(), 0); - EXPECT_EQ(manager.expected_successes(), 0); - EXPECT_EQ(manager.unexpected_successes(), 0); - EXPECT_EQ(manager.skipped(), 0); -} - -TEST_F(TestManagerTest, ReportUnseenFailure) { - CreateFailureList({{"foo.bar"}}); - TestManager manager; - ASSERT_THAT(manager.LoadFailureList(failure_list()), IsOk()); - - EXPECT_THAT( - manager.Finalize(), - StatusIs(absl::StatusCode::kFailedPrecondition, - AllOf(HasSubstr("were not seen"), HasSubstr("foo.bar")))); - - EXPECT_EQ(manager.expected_failures(), 0); - EXPECT_EQ(manager.unexpected_failures(), 0); - EXPECT_EQ(manager.expected_successes(), 0); - EXPECT_EQ(manager.unexpected_successes(), 0); - EXPECT_EQ(manager.skipped(), 0); -} - -TEST_F(TestManagerTest, ReportUnseenFailureUnrelatedSkip) { - CreateFailureList({{"foo.bar"}}); - TestManager manager; - ASSERT_THAT(manager.LoadFailureList(failure_list()), IsOk()); - - // "foo" doesn't match the entry "foo.bar", so the entry stays unseen. - ASSERT_THAT(manager.ReportSkip("foo", "reason"), IsOk()); - - EXPECT_THAT( - manager.Finalize(), - StatusIs(absl::StatusCode::kFailedPrecondition, - AllOf(HasSubstr("were not seen"), HasSubstr("foo.bar")))); - - EXPECT_EQ(manager.expected_failures(), 0); - EXPECT_EQ(manager.unexpected_failures(), 0); - EXPECT_EQ(manager.expected_successes(), 0); - EXPECT_EQ(manager.unexpected_successes(), 0); - EXPECT_EQ(manager.skipped(), 1); -} - -TEST_F(TestManagerTest, ReportUnexpectedFailure) { - CreateFailureList({}); - TestManager manager; - ASSERT_THAT(manager.LoadFailureList(failure_list()), IsOk()); - - ASSERT_THAT(manager.ReportFailure("foo_failing", kP0, ""), - StatusIs(absl::StatusCode::kFailedPrecondition, - AllOf(HasSubstr("Unexpected failure"), - HasSubstr("foo_failing")))); - - EXPECT_THAT(manager.Finalize(), IsOk()); - - EXPECT_EQ(manager.expected_failures(), 0); - EXPECT_EQ(manager.unexpected_failures(), 1); - EXPECT_EQ(manager.expected_successes(), 0); - EXPECT_EQ(manager.unexpected_successes(), 0); - EXPECT_EQ(manager.skipped(), 0); -} - -TEST_F(TestManagerTest, ReportUnexpectedFailureMismatchedName) { - CreateFailureList({{"foo"}}); - TestManager manager; - ASSERT_THAT(manager.LoadFailureList(failure_list()), IsOk()); - - ASSERT_THAT(manager.ReportFailure("foo_failing", kP0, ""), - StatusIs(absl::StatusCode::kFailedPrecondition, - AllOf(HasSubstr("Unexpected failure"), - HasSubstr("foo_failing")))); - ASSERT_THAT(manager.ReportFailure("foo", kP0, ""), IsOk()); - - EXPECT_THAT(manager.Finalize(), IsOk()); - EXPECT_EQ(manager.expected_failures(), 1); - EXPECT_EQ(manager.unexpected_failures(), 1); - EXPECT_EQ(manager.expected_successes(), 0); - EXPECT_EQ(manager.unexpected_successes(), 0); - EXPECT_EQ(manager.skipped(), 0); -} - -TEST_F(TestManagerTest, ReportUnexpectedFailureMismatchedMessage) { - CreateFailureList({{"foo.*.bar", "message"}}); - TestManager manager; - ASSERT_THAT(manager.LoadFailureList(failure_list()), IsOk()); - - ASSERT_THAT(manager.ReportFailure("foo.a.bar", kP0, "abc"), - StatusIs(absl::StatusCode::kFailedPrecondition, - AllOf(HasSubstr("Unexpected failure"), HasSubstr("foo"), - HasSubstr("message"), HasSubstr("abc")))); - EXPECT_THAT(manager.ReportFailure("foo.b.bar", kP0, "message"), IsOk()); - - EXPECT_THAT(manager.Finalize(), IsOk()); - EXPECT_EQ(manager.expected_failures(), 1); - EXPECT_EQ(manager.unexpected_failures(), 1); - EXPECT_EQ(manager.expected_successes(), 0); - EXPECT_EQ(manager.unexpected_successes(), 0); - EXPECT_EQ(manager.skipped(), 0); -} - -TEST_F(TestManagerTest, ReportFailureMatchesExpectedMessagePrefix) { - CreateFailureList({{"foo", "abc"}}); - TestManager manager; - ASSERT_THAT(manager.LoadFailureList(failure_list()), IsOk()); - - // Like the legacy runner, the actual message only has to start with the - // expected one. - EXPECT_THAT(manager.ReportFailure("foo", kP0, "abc: more details"), IsOk()); - - EXPECT_THAT(manager.Finalize(), IsOk()); - EXPECT_EQ(manager.expected_failures(), 1); - EXPECT_EQ(manager.unexpected_failures(), 0); -} - -TEST_F(TestManagerTest, ReportFailureRejectsExpectedMessageSuffix) { - CreateFailureList({{"foo", "abc: more details"}}); - TestManager manager; - ASSERT_THAT(manager.LoadFailureList(failure_list()), IsOk()); - - // The expected message is longer than the actual one, so it's not a prefix. - EXPECT_THAT(manager.ReportFailure("foo", kP0, "abc"), - StatusIs(absl::StatusCode::kFailedPrecondition, - HasSubstr("Unexpected failure message"))); - - // Like any other message mismatch, the stale entry is still reported as - // unseen so that --fix replaces it. - EXPECT_THAT(manager.Finalize(), Not(IsOk())); - EXPECT_EQ(manager.expected_failures(), 0); - EXPECT_EQ(manager.unexpected_failures(), 1); -} - -TEST_F(TestManagerTest, ReportFailureEmptyExpectedMessageMatchesAnything) { - CreateFailureList({{"foo"}}); - TestManager manager; - ASSERT_THAT(manager.LoadFailureList(failure_list()), IsOk()); - - EXPECT_THAT(manager.ReportFailure("foo", kP0, "any message at all"), IsOk()); - - EXPECT_THAT(manager.Finalize(), IsOk()); - EXPECT_EQ(manager.expected_failures(), 1); - EXPECT_EQ(manager.unexpected_failures(), 0); -} - -TEST_F(TestManagerTest, ReportFailurePrefixMatchIgnoresNewlinesAndWhitespace) { - CreateFailureList({{"foo", "abc def"}}); - TestManager manager; - ASSERT_THAT(manager.LoadFailureList(failure_list()), IsOk()); - - EXPECT_THAT(manager.ReportFailure("foo", kP0, " abc def\nghi\n"), IsOk()); - - EXPECT_THAT(manager.Finalize(), IsOk()); - EXPECT_EQ(manager.expected_failures(), 1); -} - -TEST_F(TestManagerTest, PrefixMatchedFailureIsNotAnUnexpectedFailure) { - CreateFailureList({{"foo", "abc"}}); - TestManager manager; - ASSERT_THAT(manager.LoadFailureList(failure_list()), IsOk()); - - ASSERT_THAT(manager.ReportFailure("foo", kP0, "abc: more details"), IsOk()); - - EXPECT_THAT(manager.UnexpectedFailures(), IsEmpty()); - EXPECT_EQ(manager.unexpected_failures(), 0); - EXPECT_THAT(manager.Finalize(), IsOk()); -} - -TEST_F(TestManagerTest, FinalizeReportsUnseenExpectedFailures) { - CreateFailureList({{"ccc"}, {"aaa"}, {"bbb.*.baz"}, {"ddd"}, {"eee"}}); - TestManager manager; - ASSERT_THAT(manager.LoadFailureList(failure_list()), IsOk()); - - // Failures, unexpected successes and skips all count as "seen". - ASSERT_THAT(manager.ReportFailure("bbb.x.baz", kP0, ""), IsOk()); - ASSERT_THAT(manager.ReportSuccess("ccc"), Not(IsOk())); - ASSERT_THAT(manager.ReportSkip("ddd", "reason"), Not(IsOk())); - // Tests that weren't selected to run do not. - manager.ReportNotSelected("eee"); - - EXPECT_THAT(manager.Finalize(), - StatusIs(absl::StatusCode::kFailedPrecondition, - "The following expected failures were not seen: aaa, " - "eee")); -} - -TEST_F(TestManagerTest, UnmatchedExpectedFailures) { - CreateFailureList({{"ccc"}, {"aaa"}, {"bbb.*.baz"}, {"ddd"}, {"eee", "msg"}}); - TestManager manager; - ASSERT_THAT(manager.LoadFailureList(failure_list()), IsOk()); - - EXPECT_THAT(manager.UnmatchedExpectedFailures(), - ElementsAre("aaa", "bbb.*.baz", "ccc", "ddd", "eee")); - - // Any report whose name matches an entry counts, whatever the outcome. - ASSERT_THAT(manager.ReportFailure("bbb.x.baz", kP0, ""), IsOk()); - ASSERT_THAT(manager.ReportSuccess("ccc"), Not(IsOk())); - ASSERT_THAT(manager.ReportSkip("ddd", "reason"), Not(IsOk())); - ASSERT_THAT(manager.ReportFailure("eee", kP0, "other message"), Not(IsOk())); - // Reports that match no entry change nothing. - ASSERT_THAT(manager.ReportSuccess("aaa.child"), IsOk()); - ASSERT_THAT(manager.ReportSkip("zzz", "reason"), IsOk()); - - EXPECT_THAT(manager.UnmatchedExpectedFailures(), ElementsAre("aaa")); - // Message mismatches are still unseen, though. - EXPECT_THAT(manager.Finalize(), - StatusIs(absl::StatusCode::kFailedPrecondition, - "The following expected failures were not seen: aaa, " - "eee")); -} - -TEST_F(TestManagerTest, UnmatchedExpectedFailuresEmpty) { - TestManager manager; - EXPECT_THAT(manager.UnmatchedExpectedFailures(), IsEmpty()); - EXPECT_THAT(manager.Finalize(), IsOk()); -} - -TEST_F(TestManagerTest, UnexpectedFailures) { - CreateFailureList({{"foo.*.bar", "expected"}}); - TestManager manager; - ASSERT_THAT(manager.LoadFailureList(failure_list()), IsOk()); - - EXPECT_THAT(manager.UnexpectedFailures(), IsEmpty()); - - // Not in the failure list at all, in the list with another message, and the - // messages are formatted as SaveFailureList() would write them. - ASSERT_THAT(manager.ReportFailure("zzz", kP0, " new\nfailure "), - Not(IsOk())); - ASSERT_THAT(manager.ReportFailure("foo.a.bar", kP0, "other message"), - Not(IsOk())); - ASSERT_THAT(manager.ReportFailure("aaa", kP0, std::string(1000, 'x')), - Not(IsOk())); - // Expected failures, successes and skips are not reported. - ASSERT_THAT(manager.ReportFailure("foo.b.bar", kP0, "expected: more"), - IsOk()); - ASSERT_THAT(manager.ReportSuccess("bbb"), IsOk()); - ASSERT_THAT(manager.ReportSkip("ccc", "reason"), IsOk()); - - EXPECT_THAT( - manager.UnexpectedFailures(), - ElementsAre(FieldsAre("aaa", std::string(128, 'x'), absl::nullopt), - FieldsAre("foo.a.bar", "other message", absl::nullopt), - FieldsAre("zzz", "newfailure", absl::nullopt))); - EXPECT_EQ(manager.unexpected_failures(), 3); - EXPECT_THAT(manager.Finalize(), IsOk()); -} - -TEST_F(TestManagerTest, UnexpectedFailuresIncludeExceededWildcardMatches) { - CreateFailureList({{"foo.*.bar"}}); - TestManager manager; - ASSERT_THAT(manager.LoadFailureList(failure_list()), IsOk()); - - for (int i = 0; i < 100; ++i) { - ASSERT_THAT(manager.ReportSuccess(absl::StrCat("foo.", i, ".bar")), - Not(IsOk())); - } - ASSERT_THAT(manager.ReportFailure("foo.baz.bar", kP0, "msg"), Not(IsOk())); - - EXPECT_THAT(manager.UnexpectedFailures(), - ElementsAre(FieldsAre("foo.baz.bar", "msg", absl::nullopt))); - EXPECT_EQ(manager.unexpected_failures(), 1); - EXPECT_THAT(manager.UnexpectedSuccesses(), SizeIs(100)); - EXPECT_EQ(manager.unexpected_successes(), 100); - EXPECT_THAT(manager.Finalize(), IsOk()); -} - -TEST_F(TestManagerTest, UnexpectedFailuresIgnoreDuplicateReports) { - TestManager manager; - - // Only the first report of a test counts. - ASSERT_THAT(manager.ReportFailure("foo", kP0, "first"), Not(IsOk())); - ASSERT_THAT(manager.ReportFailure("foo", kP0, "second"), Not(IsOk())); - - EXPECT_THAT(manager.UnexpectedFailures(), - ElementsAre(FieldsAre("foo", "first", absl::nullopt))); - EXPECT_EQ(manager.unexpected_failures(), 1); - EXPECT_THAT(manager.Finalize(), IsOk()); -} - -TEST_F(TestManagerTest, UnexpectedSuccesses) { - CreateFailureList({{"foo.*.bar", "wildcard message"}, {"zzz", " zzz msg "}}); - TestManager manager; - ASSERT_THAT(manager.LoadFailureList(failure_list()), IsOk()); - - EXPECT_THAT(manager.UnexpectedSuccesses(), IsEmpty()); - - ASSERT_THAT(manager.ReportSuccess("zzz"), Not(IsOk())); - ASSERT_THAT(manager.ReportSuccess("foo.b.bar"), Not(IsOk())); - ASSERT_THAT(manager.ReportSuccess("foo.a.bar"), Not(IsOk())); - ASSERT_THAT(manager.ReportSuccess("foo.a.bar"), Not(IsOk())); - // Expected successes are not reported. - ASSERT_THAT(manager.ReportSuccess("aaa"), IsOk()); - - // Sorted by test name; the message is the (normalized) one of the entry the - // test matched. - EXPECT_THAT( - manager.UnexpectedSuccesses(), - ElementsAre(FieldsAre("foo.a.bar", "wildcard message", - Optional(std::string("foo.*.bar"))), - FieldsAre("foo.b.bar", "wildcard message", - Optional(std::string("foo.*.bar"))), - FieldsAre("zzz", "zzz msg", Optional(std::string("zzz"))))); - EXPECT_EQ(manager.unexpected_successes(), 3); - EXPECT_THAT(manager.UnexpectedFailures(), IsEmpty()); - EXPECT_THAT(manager.Finalize(), IsOk()); -} - -TEST_F(TestManagerTest, ReportUnexpectedFailureTooManyWildcardMatches) { - CreateFailureList({{"foo.*.bar"}}); - TestManager manager; - ASSERT_THAT(manager.LoadFailureList(failure_list()), IsOk()); - - for (int i = 0; i < 100; ++i) { - EXPECT_THAT(manager.ReportSuccess(absl::StrCat("foo.", i, ".bar")), - StatusIs(absl::StatusCode::kFailedPrecondition)); - } - ASSERT_THAT(manager.ReportFailure("foo.baz.bar", kP0, ""), - StatusIs(absl::StatusCode::kFailedPrecondition, - AllOf(HasSubstr("too many test names"), - HasSubstr("foo.baz.bar")))); - - EXPECT_THAT(manager.Finalize(), IsOk()); - EXPECT_EQ(manager.expected_failures(), 0); - EXPECT_EQ(manager.unexpected_failures(), 1); - EXPECT_EQ(manager.expected_successes(), 0); - EXPECT_EQ(manager.unexpected_successes(), 100); - EXPECT_EQ(manager.skipped(), 0); -} - -TEST_F(TestManagerTest, LoadFailureListInvalidFile) { - TestManager manager; - EXPECT_THAT(manager.LoadFailureList(absl::StrCat(failure_list(), "/invalid")), - StatusIs(absl::StatusCode::kInternal, HasSubstr("/invalid"))); - manager.Finalize().IgnoreError(); -} - -TEST_F(TestManagerTest, LoadFailureListDuplicateFailure) { - CreateFailureList(R"( - foo # abc - foo # zyx -)"); - TestManager manager; - - EXPECT_THAT(manager.LoadFailureList(failure_list()), - StatusIs(absl::StatusCode::kAlreadyExists, - AllOf(HasSubstr("already exists"), HasSubstr("foo")))); - manager.Finalize().IgnoreError(); -} - -TEST_F(TestManagerTest, LoadFailureListOverlappingFailure) { - CreateFailureList(R"( - foo.*.bar # abc - foo.bar.* # zyx -)"); - TestManager manager; - - EXPECT_THAT(manager.LoadFailureList(failure_list()), - StatusIs(absl::StatusCode::kAlreadyExists, - AllOf(HasSubstr("foo.bar.*"), HasSubstr("foo.*.bar")))); - manager.Finalize().IgnoreError(); -} - -TEST_F(TestManagerTest, UnexpectedSuccessMessageNamesTheEntry) { - CreateFailureList({{"foo", "abc"}}); - TestManager manager; - ASSERT_THAT(manager.LoadFailureList(failure_list()), IsOk()); - - // The message is the legacy runner's, including the matched entry. - EXPECT_THAT(manager.ReportSuccess("foo"), - StatusIs(absl::StatusCode::kFailedPrecondition, - "test foo (matched to foo) is in the failure list, but " - "test succeeded. Remove its match from the failure " - "list.")); - EXPECT_THAT(manager.Finalize(), IsOk()); - EXPECT_EQ(manager.unexpected_successes(), 1); -} - -TEST_F(TestManagerTest, UnexpectedSuccessMessageNamesTheWildcardEntry) { - CreateFailureList({{"foo.*.bar", "abc"}}); - TestManager manager; - ASSERT_THAT(manager.LoadFailureList(failure_list()), IsOk()); - - EXPECT_THAT(manager.ReportSuccess("foo.a.bar"), - StatusIs(absl::StatusCode::kFailedPrecondition, - HasSubstr("test foo.a.bar (matched to foo.*.bar) is in " - "the failure list"))); - EXPECT_THAT(manager.Finalize(), IsOk()); -} - -TEST_F(TestManagerTest, EveryPriorityIsEnforcedByDefault) { - TestManager manager; - EXPECT_THAT(manager.ReportFailure("p0", kP0, "abc"), Not(IsOk())); - EXPECT_THAT(manager.ReportFailure("p1", kP1, "abc"), Not(IsOk())); - EXPECT_EQ(manager.unexpected_failures(), 2); - EXPECT_EQ(manager.tolerated_failures(), 0); - EXPECT_THAT(manager.Finalize(), IsOk()); -} - -TEST_F(TestManagerTest, EnforcementLevelZeroToleratesP1) { - // Only kP0 is enforced: an unlisted kP1 failure is tolerated. - TestManager manager; - manager.set_enforcement_level(kP0); - EXPECT_THAT(manager.ReportFailure("p0", kP0, "abc"), Not(IsOk())); - EXPECT_THAT(manager.ReportFailure("p1", kP1, "abc"), IsOk()); - EXPECT_EQ(manager.unexpected_failures(), 1); - EXPECT_EQ(manager.tolerated_failures(), 1); - EXPECT_THAT(manager.Finalize(), IsOk()); -} - -TEST_F(TestManagerTest, ToleratedFailure) { - CreateFailureList({{"listed", "abc"}}); - TestManager manager; - manager.set_enforcement_level(kP0); - ASSERT_THAT(manager.LoadFailureList(failure_list()), IsOk()); - - ASSERT_THAT(manager.ReportFailure("foo", kP1, "abc"), IsOk()); - ASSERT_THAT(manager.ReportFailure("bar", kP1, "xyz"), IsOk()); - // Duplicates only count once. - ASSERT_THAT(manager.ReportFailure("foo", kP1, "abc"), IsOk()); - ASSERT_THAT(manager.ReportFailure("listed", kP0, "abc"), IsOk()); - - EXPECT_EQ(manager.tolerated_failures(), 2); - // A tolerated failure is neither a skip nor a failure of any kind, and it - // isn't written to the failure list. - EXPECT_EQ(manager.skipped(), 0); - EXPECT_EQ(manager.expected_failures(), 1); - EXPECT_EQ(manager.unexpected_failures(), 0); - EXPECT_EQ(manager.expected_successes(), 0); - EXPECT_EQ(manager.unexpected_successes(), 0); - EXPECT_THAT(manager.UnexpectedFailures(), IsEmpty()); - EXPECT_THAT(manager.Finalize(), IsOk()); - - EXPECT_THAT(manager.SaveFailureList(absl::StrCat(failure_list(), ".new")), - IsOk()); - std::string content; - ASSERT_THAT(File::GetContents(absl::StrCat(failure_list(), ".new"), &content, - true), - IsOk()); - EXPECT_EQ(content, "listed # abc\n"); -} - -TEST_F(TestManagerTest, ToleratedFailureIsCountedOnceAcrossKinds) { - TestManager manager; - manager.set_enforcement_level(kP0); - // The same test name can only be counted under one outcome. - ASSERT_THAT(manager.ReportSuccess("foo"), IsOk()); - ASSERT_THAT(manager.ReportFailure("foo", kP1, "abc"), IsOk()); - - EXPECT_EQ(manager.expected_successes(), 1); - EXPECT_EQ(manager.tolerated_failures(), 0); - EXPECT_THAT(manager.Finalize(), IsOk()); -} - -TEST_F(TestManagerTest, ListedFailureIsCheckedWhateverItsPriority) { - // A listed test is never tolerated, so that the failure list can't go stale - // unnoticed. - CreateFailureList({{"foo", "abc"}, {"bar", "abc"}}); - TestManager manager; - manager.set_enforcement_level(kP0); - ASSERT_THAT(manager.LoadFailureList(failure_list()), IsOk()); - - EXPECT_THAT(manager.ReportFailure("foo", kP1, "abc"), IsOk()); - EXPECT_THAT(manager.ReportFailure("bar", kP1, "xyz"), Not(IsOk())); - - EXPECT_EQ(manager.expected_failures(), 1); - EXPECT_EQ(manager.unexpected_failures(), 1); - EXPECT_EQ(manager.tolerated_failures(), 0); - manager.Finalize().IgnoreError(); -} - -TEST_F(TestManagerTest, LoadFailureListInvalidWildcard) { - CreateFailureList({{"foo.b*r", "abc"}}); - TestManager manager; - EXPECT_THAT(manager.LoadFailureList(failure_list()), - StatusIs(absl::StatusCode::kInvalidArgument)); - manager.Finalize().IgnoreError(); -} - -TEST_F(TestManagerTest, LoadFailureListTruncatesMessage) { - // Like a reported message, so that the two still compare equal. - std::string message(1000, 'b'); - CreateFailureList({{"foo", message}}); - TestManager manager; - ASSERT_THAT(manager.LoadFailureList(failure_list()), IsOk()); - - EXPECT_THAT(manager.ReportFailure("foo", kP0, message), IsOk()); - EXPECT_THAT(manager.Finalize(), IsOk()); - EXPECT_EQ(manager.expected_failures(), 1); -} - -TEST_F(TestManagerTest, LoadSecondFailureList) { - CreateFailureList({{"foo", "abc"}}); - const std::string second = absl::StrCat(failure_list(), ".2"); - ABSL_CHECK_OK(File::SetContents(second, "bar # zyx\n", true)); - const std::string third = absl::StrCat(failure_list(), ".3"); - ABSL_CHECK_OK(File::SetContents(third, "foo # zyx\n", true)); - TestManager manager; - ASSERT_THAT(manager.LoadFailureList(failure_list()), IsOk()); - ASSERT_THAT(manager.LoadFailureList(second), IsOk()); - - // Entries must not repeat across files either. - EXPECT_THAT(manager.LoadFailureList(third), - StatusIs(absl::StatusCode::kAlreadyExists)); - EXPECT_THAT(manager.ReportFailure("foo", kP0, "abc"), IsOk()); - EXPECT_THAT(manager.ReportFailure("bar", kP0, "zyx"), IsOk()); - EXPECT_THAT(manager.Finalize(), IsOk()); - EXPECT_EQ(manager.expected_failures(), 2); -} - -TEST_F(TestManagerTest, FailureListMatchesWholeNameComponents) { - CreateFailureList({{"foo", "abc"}, {"bar.*.baz", "abc"}, {"qux", "abc"}}); - TestManager manager; - ASSERT_THAT(manager.LoadFailureList(failure_list()), IsOk()); - - EXPECT_THAT(manager.ReportFailure("foo", kP0, "abc"), IsOk()); - EXPECT_THAT(manager.ReportFailure("qux", kP0, "abc"), IsOk()); - EXPECT_THAT(manager.ReportFailure("bar.a.baz", kP0, "abc"), IsOk()); - EXPECT_THAT(manager.ReportFailure("bar.b.baz", kP0, "abc"), IsOk()); - // Neither a prefix nor an extension of an entry matches it, and a wildcard - // stands for exactly one component. - EXPECT_THAT(manager.ReportFailure("foo.bar", kP0, "abc"), Not(IsOk())); - EXPECT_THAT(manager.ReportFailure("bar", kP0, "abc"), Not(IsOk())); - EXPECT_THAT(manager.ReportFailure("bar.a", kP0, "abc"), Not(IsOk())); - EXPECT_THAT(manager.ReportFailure("bar.a.baz.qux", kP0, "abc"), Not(IsOk())); - EXPECT_THAT(manager.ReportFailure("other", kP0, "abc"), Not(IsOk())); - - EXPECT_EQ(manager.expected_failures(), 4); - EXPECT_EQ(manager.unexpected_failures(), 5); - EXPECT_THAT(manager.Finalize(), IsOk()); -} - -TEST_F(TestManagerTest, SaveFailureListNoop) { - CreateFailureList(R"( -foo # abc -)"); - TestManager manager; - ASSERT_THAT(manager.LoadFailureList(failure_list()), IsOk()); - ASSERT_THAT(manager.ReportFailure("foo", kP0, "abc"), IsOk()); - ASSERT_THAT(manager.Finalize(), IsOk()); - - EXPECT_THAT(manager.SaveFailureList(absl::StrCat(failure_list(), ".new")), - IsOk()); - - std::string content; - ASSERT_THAT(File::GetContents(absl::StrCat(failure_list(), ".new"), &content, - true), - IsOk()); - EXPECT_EQ(content, R"( -foo # abc -)"); -} - -TEST_F(TestManagerTest, SaveFailureListNoopWildcard) { - CreateFailureList(R"( -foo.*.bar # abc -)"); - TestManager manager; - ASSERT_THAT(manager.LoadFailureList(failure_list()), IsOk()); - ASSERT_THAT(manager.ReportFailure("foo.a.bar", kP0, "abc"), IsOk()); - ASSERT_THAT(manager.Finalize(), IsOk()); - - EXPECT_THAT(manager.SaveFailureList(absl::StrCat(failure_list(), ".new")), - IsOk()); - - std::string content; - ASSERT_THAT(File::GetContents(absl::StrCat(failure_list(), ".new"), &content, - true), - IsOk()); - EXPECT_EQ(content, R"( -foo.*.bar # abc -)"); -} - -TEST_F(TestManagerTest, SaveFailureListKeepsPrefixMatchedEntryVerbatim) { - CreateFailureList(R"( -foo # abc -)"); - TestManager manager; - ASSERT_THAT(manager.LoadFailureList(failure_list()), IsOk()); - // The actual message is longer than the entry's, which is only a prefix. - ASSERT_THAT(manager.ReportFailure("foo", kP0, "abc: more details"), IsOk()); - ASSERT_THAT(manager.Finalize(), IsOk()); - - EXPECT_THAT(manager.SaveFailureList(absl::StrCat(failure_list(), ".new")), - IsOk()); - - std::string content; - ASSERT_THAT(File::GetContents(absl::StrCat(failure_list(), ".new"), &content, - true), - IsOk()); - EXPECT_EQ(content, R"( -foo # abc -)"); -} - -TEST_F(TestManagerTest, SaveFailureListRemoveNewPassing) { - CreateFailureList(R"( -foo # abc -)"); - TestManager manager; - ASSERT_THAT(manager.LoadFailureList(failure_list()), IsOk()); - ASSERT_THAT(manager.ReportSuccess("foo"), Not(IsOk())); - ASSERT_THAT(manager.Finalize(), IsOk()); - - EXPECT_THAT(manager.SaveFailureList(absl::StrCat(failure_list(), ".new")), - IsOk()); - - std::string content; - ASSERT_THAT(File::GetContents(absl::StrCat(failure_list(), ".new"), &content, - true), - IsOk()); - EXPECT_EQ(content, R"( -)"); -} - -TEST_F(TestManagerTest, SaveFailureListRemovePartialPassingWildcard) { - // This is a weird case where the wildcard no longer fully matches the - // failure. Users would have to updated the failure list twice to get the - // failure list to be correct. - CreateFailureList(R"( -foo.*.bar # abc -)"); - TestManager manager; - ASSERT_THAT(manager.LoadFailureList(failure_list()), IsOk()); - ASSERT_THAT(manager.ReportSuccess("foo.a.bar"), Not(IsOk())); - ASSERT_THAT(manager.ReportFailure("foo.b.bar", kP0, "abc"), IsOk()); - ASSERT_THAT(manager.Finalize(), IsOk()); - - EXPECT_THAT(manager.SaveFailureList(absl::StrCat(failure_list(), ".new")), - IsOk()); - - std::string content; - ASSERT_THAT(File::GetContents(absl::StrCat(failure_list(), ".new"), &content, - true), - IsOk()); - EXPECT_EQ(content, R"( -)"); -} - -TEST_F(TestManagerTest, SaveFailureListKeepsSkippedEntry) { - // A skipped test says nothing about whether its entry is still needed, so - // the entry is kept verbatim (whether the skip itself is acceptable is - // decided, and reported, by the caller). - CreateFailureList(R"( -foo # abc -)"); - TestManager manager; - ASSERT_THAT(manager.LoadFailureList(failure_list()), IsOk()); - ASSERT_THAT(manager.ReportSkip("foo", "reason"), Not(IsOk())); - ASSERT_THAT(manager.Finalize(), IsOk()); - - EXPECT_THAT(manager.SaveFailureList(absl::StrCat(failure_list(), ".new")), - IsOk()); - - std::string content; - ASSERT_THAT(File::GetContents(absl::StrCat(failure_list(), ".new"), &content, - true), - IsOk()); - EXPECT_EQ(content, R"( -foo # abc -)"); -} - -TEST_F(TestManagerTest, SaveFailureListUnwritablePath) { - TestManager manager; - ASSERT_THAT(manager.ReportFailure("foo", kP0, "abc"), Not(IsOk())); - ASSERT_THAT(manager.Finalize(), IsOk()); - - // A directory can't be opened for writing. - const std::string directory = absl::StrCat(failure_list(), ".dir"); - ASSERT_THAT(File::RecursivelyCreateDir(directory, 0777), - IsOk()); - EXPECT_THAT(manager.SaveFailureList(directory), - StatusIs(absl::StatusCode::kInternal, HasSubstr(directory))); - // Neither can a file in a directory that doesn't exist. - const std::string missing_directory = - absl::StrCat(failure_list(), ".missing/list.txt"); - EXPECT_THAT( - manager.SaveFailureList(missing_directory), - StatusIs(absl::StatusCode::kInternal, HasSubstr(missing_directory))); -} - -TEST_F(TestManagerTest, SaveFailureListWriteFailure) { - TestManager manager; - ASSERT_THAT(manager.ReportFailure("foo", kP0, "abc"), Not(IsOk())); - ASSERT_THAT(manager.Finalize(), IsOk()); - - // /dev/full opens fine but every write to it fails with ENOSPC, which is - // the only way to reach the write-failure path without a filesystem mock. - // It is a Linux device. -#ifdef __linux__ - EXPECT_THAT(manager.SaveFailureList("/dev/full"), - StatusIs(absl::StatusCode::kInternal, - HasSubstr("Failed to write failure list file"))); -#else - GTEST_SKIP() << "/dev/full is Linux-only"; -#endif -} - -TEST_F(TestManagerTest, SaveFailureListChangeFailureMessage) { - CreateFailureList(R"( -foo # abc -)"); - TestManager manager; - ASSERT_THAT(manager.LoadFailureList(failure_list()), IsOk()); - ASSERT_THAT(manager.ReportFailure("foo", kP0, "zyx"), Not(IsOk())); - ASSERT_THAT(manager.Finalize(), Not(IsOk())); - - EXPECT_THAT(manager.SaveFailureList(absl::StrCat(failure_list(), ".new")), - IsOk()); - - std::string content; - ASSERT_THAT(File::GetContents(absl::StrCat(failure_list(), ".new"), &content, - true), - IsOk()); - EXPECT_EQ(content, R"( -foo # zyx -)"); -} - -TEST_F(TestManagerTest, SaveFailureListAddNewFailure) { - TestManager manager; - ASSERT_THAT(manager.ReportFailure("foo", kP0, "abc"), Not(IsOk())); - ASSERT_THAT(manager.Finalize(), IsOk()); - - EXPECT_THAT(manager.SaveFailureList(absl::StrCat(failure_list(), ".new")), - IsOk()); - - std::string content; - ASSERT_THAT(File::GetContents(absl::StrCat(failure_list(), ".new"), &content, - true), - IsOk()); - EXPECT_EQ(content, R"(foo # abc -)"); -} - -TEST_F(TestManagerTest, SaveFailureListAddNormalizedMessage) { - TestManager manager; - std::string message = absl::StrCat("aa\n", std::string(1000, 'b')); - std::string normalized_message = absl::StrCat("aa", std::string(126, 'b')); - ASSERT_THAT(manager.ReportFailure("foo", kP0, message), Not(IsOk())); - ASSERT_THAT(manager.Finalize(), IsOk()); - - EXPECT_THAT(manager.SaveFailureList(absl::StrCat(failure_list(), ".new")), - IsOk()); - - std::string content; - ASSERT_THAT(File::GetContents(absl::StrCat(failure_list(), ".new"), &content, - true), - IsOk()); - EXPECT_EQ(content, absl::StrCat("foo # ", normalized_message, "\n")); -} - -TEST_F(TestManagerTest, SaveFailureListInsertAlphabetically) { - CreateFailureList(R"( -# This is a comment. -aaa # aaa -# This is another comment. -bbb # bbb - -ccc # ccc -)"); - TestManager manager; - ASSERT_THAT(manager.LoadFailureList(failure_list()), IsOk()); - ASSERT_THAT(manager.ReportFailure("aaa", kP0, "aaa"), IsOk()); - ASSERT_THAT(manager.ReportFailure("bbb", kP0, "bbb"), IsOk()); - ASSERT_THAT(manager.ReportFailure("ccc", kP0, "ccc"), IsOk()); - ASSERT_THAT(manager.ReportFailure("abc", kP0, "abc"), Not(IsOk())); - ASSERT_THAT(manager.Finalize(), IsOk()); - - EXPECT_THAT(manager.SaveFailureList(absl::StrCat(failure_list(), ".new")), - IsOk()); - - std::string content; - ASSERT_THAT(File::GetContents(absl::StrCat(failure_list(), ".new"), &content, - true), - IsOk()); - EXPECT_EQ(content, R"( -# This is a comment. -aaa # aaa -# This is another comment. -abc # abc -bbb # bbb - -ccc # ccc -)"); -} - -TEST_F(TestManagerTest, SaveFailureListInsertAligned) { - CreateFailureList(R"( -# This is a comment. -aa # aaa -# This is another comment. -bbbb #bbb - -cc # ccc -)"); - TestManager manager; - ASSERT_THAT(manager.LoadFailureList(failure_list()), IsOk()); - ASSERT_THAT(manager.ReportFailure("aa", kP0, "aaa"), IsOk()); - ASSERT_THAT(manager.ReportFailure("bbbb", kP0, " bbb "), IsOk()); - ASSERT_THAT(manager.ReportFailure("cc", kP0, "ccc"), IsOk()); - ASSERT_THAT(manager.ReportFailure("abcdef", kP0, "abc"), Not(IsOk())); - ASSERT_THAT(manager.Finalize(), IsOk()); - - EXPECT_THAT(manager.SaveFailureList(absl::StrCat(failure_list(), ".new")), - IsOk()); - - std::string content; - ASSERT_THAT(File::GetContents(absl::StrCat(failure_list(), ".new"), &content, - true), - IsOk()); - EXPECT_EQ(content, R"( -# This is a comment. -aa # aaa -# This is another comment. -abcdef # abc -bbbb # bbb - -cc # ccc -)"); -} - -} // namespace -} // namespace internal -} // namespace conformance -} // namespace protobuf -} // namespace google
diff --git a/conformance/testee.cc b/conformance/testee.cc index 9ec8524..4a6bc6c 100644 --- a/conformance/testee.cc +++ b/conformance/testee.cc
@@ -1,3 +1,10 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2025 Google LLC. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + #include "conformance/testee.h" #include <string> @@ -5,6 +12,7 @@ #include "absl/log/absl_check.h" #include "absl/log/absl_log.h" +#include "absl/strings/numbers.h" #include "absl/strings/str_cat.h" #include "absl/strings/string_view.h" #include "conformance/binary_wireformat.h" @@ -36,16 +44,21 @@ return ::conformance::UNSPECIFIED; } +// Builds the full test name according to `name_style` (see NameStyle): +// "<Level>.<Edition>.<InputFormat>Input.<name>", followed by +// ".<OutputFormat>Output" unless the style omits it. std::string GetTestName(absl::string_view test_name, TestPriority priority, const ::conformance::ConformanceRequest& request, - const Descriptor& message) { - absl::string_view level = PriorityLevelName(priority); - std::string syntax_identifier = GetEditionIdentifier(message); - - return absl::StrCat( - level, ".", syntax_identifier, ".", - GetFormatIdentifier(GetInputFormat(request)), "Input.", test_name, ".", - GetFormatIdentifier(request.requested_output_format()), "Output"); + const Descriptor& message, NameStyle name_style) { + std::string full_name = absl::StrCat( + PriorityLevelName(priority), ".", GetEditionIdentifier(message), ".", + GetFormatIdentifier(GetInputFormat(request)), "Input.", test_name); + if (name_style == NameStyle::kWithOutputFormat) { + absl::StrAppend(&full_name, ".", + GetFormatIdentifier(request.requested_output_format()), + "Output"); + } + return full_name; } } // namespace @@ -54,6 +67,7 @@ absl::string_view test_name, const ConformanceRequest& request) { ABSL_CHECK(test_names_ran_.emplace(test_name).second) << "Duplicated test name: " << test_name; + tests_run_.emplace_back(test_name); std::string serialized_request; // TODO: Remove this suppression. @@ -101,7 +115,7 @@ } TestResult InMemoryMessage::SerializeBinary() && { - return SerializeImpl(::conformance::PROTOBUF); + return Finish(::conformance::PROTOBUF, NameStyle::kWithOutputFormat); } TestResult InMemoryMessage::SerializeText(TextSerializationOptions options) && { @@ -109,23 +123,45 @@ request_.set_print_unknown_fields(true); } - return SerializeImpl(::conformance::TEXT_FORMAT); + return Finish(::conformance::TEXT_FORMAT, NameStyle::kWithOutputFormat); } TestResult InMemoryMessage::SerializeJson() && { - return SerializeImpl(::conformance::JSON); + return Finish(::conformance::JSON, NameStyle::kWithOutputFormat); } -TestResult InMemoryMessage::SerializeImpl( - const ::conformance::WireFormat format) { - request_.set_requested_output_format(format); +TestResult InMemoryMessage::ParseOnly(ParseOnlyOptions options) && { + // We don't expect output, but if the testee erroneously accepts the input we + // let it send its response in the input's format (unless the test says + // otherwise). We must not leave it unspecified. This is exactly what the + // legacy runner did. + return Finish(options.output_format.value_or(GetInputFormat(request_)), + NameStyle::kWithoutOutputFormat); +} - std::string full_name = GetTestName(name_, priority_, request_, *type_); +InMemoryMessage&& InMemoryMessage::OverrideTestCategory( + ::conformance::TestCategory category) && { + ABSL_DCHECK_EQ(category, ::conformance::TEXT_FORMAT_TEST) + << "OverrideTestCategory() only relabels the text-format suite's " + "binary-input tests as TEXT_FORMAT_TEST"; + ABSL_DCHECK(request_.has_protobuf_payload()) + << "OverrideTestCategory() is for PROTOBUF-input tests"; + request_.set_test_category(category); + return std::move(*this); +} + +TestResult InMemoryMessage::Finish( + const ::conformance::WireFormat output_format, const NameStyle name_style) { + request_.set_requested_output_format(output_format); + + std::string full_name = + GetTestName(name_, priority_, request_, *type_, name_style); ::conformance::ConformanceResponse response = testee_->Run(full_name, request_); - return TestResult(full_name, priority_, type_, format, std::move(response)); + return TestResult(full_name, priority_, type_, output_format, + std::move(response)); } } // namespace internal @@ -140,6 +176,23 @@ return "Unknown"; } +bool AbslParseFlag(absl::string_view text, TestPriority* priority, + std::string* error) { + int number; + if (!absl::SimpleAtoi(text, &number) || number < 0 || + number > static_cast<int>(kLowestPriority)) { + *error = absl::StrCat("expected a priority number from 0 to ", + static_cast<int>(kLowestPriority)); + return false; + } + *priority = static_cast<TestPriority>(number); + return true; +} + +std::string AbslUnparseFlag(TestPriority priority) { + return absl::StrCat(static_cast<int>(priority)); +} + absl::string_view PriorityLevelName(TestPriority priority) { // TODO: b/564550230 - return PriorityName() once the tests are renamed. return priority == TestPriority::kP1 ? "Recommended" : "Required";
diff --git a/conformance/testee.h b/conformance/testee.h index 2757d36..3a3e924 100644 --- a/conformance/testee.h +++ b/conformance/testee.h
@@ -1,11 +1,21 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2025 Google LLC. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + #ifndef GOOGLE_PROTOBUF_CONFORMANCE_TESTEE_H__ #define GOOGLE_PROTOBUF_CONFORMANCE_TESTEE_H__ #include <string> #include <utility> +#include <vector> #include "absl/container/flat_hash_set.h" #include "absl/strings/string_view.h" +#include "absl/types/optional.h" +#include "absl/types/span.h" #include "conformance/binary_wireformat.h" #include "conformance/conformance.pb.h" #include "conformance/test_runner.h" @@ -25,7 +35,7 @@ // .SerializeBinary(), // Yields(WhenParsed(EqualsBinaryProto(input)))); -// TODO Possible future APIs to expand conformance coverage: +// TODO: Possible future APIs to expand conformance coverage: // - Add ClearUnknownFields() to InMemoryMessage // - Add MergeFrom() method to InMemoryMessage to merge raw binary // - Remove && qualifiers on Parse* and add InMemoryMessage::Merge that merges @@ -56,22 +66,34 @@ inline constexpr TestPriority kP0 = TestPriority::kP0; inline constexpr TestPriority kP1 = TestPriority::kP1; -// The lowest priority there is. An enforcement level (see TestManager in -// test_manager.h) is the lowest priority whose unlisted failures fail the run; +// The lowest priority there is. An enforcement level (see ResultLedger in +// result_ledger.h) is the lowest priority whose unlisted failures fail the run; // kLowestPriority, the default, enforces every priority. inline constexpr TestPriority kLowestPriority = kP1; // The name of a priority: "P0" or "P1". absl::string_view PriorityName(TestPriority priority); +// Flag support for TestPriority, as the priority's number: "0" is kP0, "1" is +// kP1. +bool AbslParseFlag(absl::string_view text, TestPriority* priority, + std::string* error); +std::string AbslUnparseFlag(TestPriority priority); + // The level a priority is named with in test names, until the rename (see // TestPriority): "Required" for kP0, "Recommended" for kP1. absl::string_view PriorityLevelName(TestPriority priority); namespace internal { -// The final result of a conformance test, to be processed by a matcher. -class TestResult { +// The final result of a conformance test: the testee's response and what the +// test asked of it. Hand it to exactly one EXPECT_THAT(..., Yields(...)), +// which records the outcome against the failure list (see matchers.h). +// Dropping a result does not compile, and neither does a leaf matcher applied +// to one without Yields() (see YieldedResult in matchers.h). The test +// environment fails a gtest test that runs a conformance test but never +// checks its result in any other way (see Testee::tests_run()). +class [[nodiscard]] TestResult { public: // The name of the test that was run, useful for failure matching and // reporting. @@ -117,6 +139,21 @@ bool print_unknown_fields = false; }; +// Options for InMemoryMessage::ParseOnly(). +struct ParseOnlyOptions { + // The output format to request from the testee. Defaults to the input's + // format. + absl::optional<::conformance::WireFormat> output_format; +}; + +// How a test's full name is derived from the name it was created with: +// kWithOutputFormat <S>.<E>.<Input>Input.<name>.<Output>Output +// kWithoutOutputFormat <S>.<E>.<Input>Input.<name> (ParseOnly()) +enum class NameStyle { + kWithOutputFormat, + kWithoutOutputFormat, +}; + // This class represents a message held in memory by the testee that can be // manipulated in various ways. class InMemoryMessage { @@ -129,6 +166,51 @@ TestResult SerializeText(TextSerializationOptions options = {}) &&; TestResult SerializeJson() &&; + // Finishes a test whose outcome depends only on parsing, for example one + // that expects a parse error: + // + // EXPECT_THAT(Testee().ParseBinary(type, input).ParseOnly(), + // Yields(IsParseError())); + // + // There is no parse-only request, so the testee is still asked to + // serialize, by default in the input's format. The test name carries no + // output-format suffix, for example "Required.Proto3.ProtobufInput.<name>". + // + // `options.output_format` requests a different output format without + // changing the name. It exists for the JSON tests that need PROTOBUF + // output from JSON input while still being named + // "<S>.<E>.JsonInput.<name>", for example in + // RunValidJsonTestOrParseFailure. See ParseOnlyOptions. + // + // Like the Serialize*() methods, this consumes the message and must remain + // the terminal call of a test. Once further operations on an + // InMemoryMessage exist, such as Merge or reflection accessors (see the + // TODO at the top of this file), they have to come before it, not after. + TestResult ParseOnly(ParseOnlyOptions options = {}) &&; + + // Relabels a PROTOBUF-input request as TEXT_FORMAT_TEST. That is the only + // category accepted. Anything else is a DCHECK failure. + // + // Testee().ParseBinary(type, input) + // .OverrideTestCategory(::conformance::TEXT_FORMAT_TEST) + // .SerializeText() + // + // ParseBinary() categorizes its request as BINARY_TEST, but the text-format + // suite's binary-input tests (text_unknown_field_test.cc) have to stay + // TEXT_FORMAT_TEST. No testee tells BINARY_TEST and JSON_TEST apart on a + // binary payload. The OSS Ruby testee, however, skips every + // TEXT_FORMAT_TEST request and cannot produce text-format output. + // Relabeling those tests would turn its skips into failures that no failure + // list covers. The JSON suite's binary-input tests are plain BINARY_TEST + // requests. + // + // Returns *this as an rvalue. Must be chained in the same expression. + // A reference bound to the result would outlive the temporary it refers to. + // TODO: drop once the Ruby testee skips text-format tests by + // payload and requested output format rather than by category. + InMemoryMessage&& OverrideTestCategory( + ::conformance::TestCategory category) &&; + private: InMemoryMessage(class Testee* testee, absl::string_view name, TestPriority priority, const Descriptor* type, @@ -140,7 +222,11 @@ request_(std::move(request)) {} friend class Test; - TestResult SerializeImpl(::conformance::WireFormat format); + // Finishes the test: requests `output_format` from the testee and runs it + // under the name built according to `name_style`. All public terminal + // methods funnel into this. + TestResult Finish(::conformance::WireFormat output_format, + NameStyle name_style); class Testee* testee_; std::string name_; @@ -187,6 +273,13 @@ return Test(this, name, priority); } + // The full names of the tests run so far, in the order they ran. Each of + // them must be checked with Yields() (see matchers.h). + // ConformanceEnvironment (test_environment.h) compares this with the tests + // the ResultLedger has heard about and fails a gtest test that ran a + // conformance test but never checked its result. + absl::Span<const std::string> tests_run() const { return tests_run_; } + private: ::conformance::ConformanceResponse Run( absl::string_view test_name, @@ -195,6 +288,8 @@ ConformanceTestRunner* runner_; + // The tests run so far, in order, and as a set for Run()'s duplicate check. + std::vector<std::string> tests_run_; absl::flat_hash_set<std::string> test_names_ran_; };
diff --git a/conformance/testee_runner.h b/conformance/testee_runner.h new file mode 100644 index 0000000..1717025 --- /dev/null +++ b/conformance/testee_runner.h
@@ -0,0 +1,52 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2026 Google LLC. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +// The link-time seam that decides which testee a conformance test binary +// (one with test_environment_main.cc's main) runs against. +// +// Exactly one library linked into the binary defines MakeTesteeRunner(): +// +// * :forked_testee, which conformance_test() (conformance.bzl) links by +// default, spawns --testee_binary with --testee_args and talks to it over +// pipes (ForkPipeRunner). +// * An in-process testee library answers the requests in this process. That +// makes the testee debuggable from the test binary and needs no fork or +// pipe. conformance_test(in_process = True) links it in place of +// :forked_testee. + +#ifndef GOOGLE_PROTOBUF_CONFORMANCE_TESTEE_RUNNER_H__ +#define GOOGLE_PROTOBUF_CONFORMANCE_TESTEE_RUNNER_H__ + +#include <memory> +#include <string> + +#include "absl/strings/string_view.h" +#include "absl/types/span.h" +#include "conformance/test_runner.h" + +namespace google { +namespace protobuf { +namespace conformance { + +// Returns the runner the conformance suites of this binary talk to, given the +// values of --testee_binary and --testee_args. The latter includes any +// positional arguments. Never returns null. Check-fails, with a message +// naming the flag, on a command line the linked testee can't make sense of. +// A forked testee needs `testee_binary`. An in-process testee rejects a +// non-empty `testee_binary` or `testee_args` rather than silently running the +// linked testee instead of the requested one. +// +// Called once, before the tests run (see test_environment_main.cc), and the +// result is destroyed by ConformanceEnvironment::TearDown(). +std::unique_ptr<ConformanceTestRunner> MakeTesteeRunner( + absl::string_view testee_binary, absl::Span<const std::string> testee_args); + +} // namespace conformance +} // namespace protobuf +} // namespace google + +#endif // GOOGLE_PROTOBUF_CONFORMANCE_TESTEE_RUNNER_H__
diff --git a/conformance/testee_test.cc b/conformance/testee_test.cc index 124bf35..a1dd4b9 100644 --- a/conformance/testee_test.cc +++ b/conformance/testee_test.cc
@@ -22,7 +22,13 @@ namespace { using ::protobuf_test_messages::proto2::TestAllTypesProto2; +using ::testing::_; +using ::testing::AnyNumber; +using ::testing::DoAll; +using ::testing::ElementsAre; +using ::testing::IsEmpty; using ::testing::Return; +using ::testing::SaveArg; MATCHER_P(RequestEquals, expected_textproto, "") { ::conformance::ConformanceRequest request, expected; @@ -65,7 +71,7 @@ EXPECT_EQ(result.name(), "Required.Proto2.ProtobufInput.foo.ProtobufOutput"); EXPECT_EQ(result.priority(), TestPriority::kP0); EXPECT_EQ(result.type(), TestAllTypesProto2::descriptor()); - EXPECT_THAT(result.format(), ::conformance::PROTOBUF); + EXPECT_EQ(result.format(), ::conformance::PROTOBUF); EXPECT_THAT(result.response(), EqualsProto(R"pb(runtime_error: "error")pb")); } @@ -91,7 +97,7 @@ "Recommended.Proto2.TextFormatInput.foo.TextFormatOutput"); EXPECT_EQ(result.priority(), TestPriority::kP1); EXPECT_EQ(result.type(), TestAllTypesProto2::descriptor()); - EXPECT_THAT(result.format(), ::conformance::TEXT_FORMAT); + EXPECT_EQ(result.format(), ::conformance::TEXT_FORMAT); EXPECT_THAT(result.response(), EqualsProto(R"pb(runtime_error: "error")pb")); } @@ -119,7 +125,7 @@ "Required.Proto2.ProtobufInput.foo.TextFormatOutput"); EXPECT_EQ(result.priority(), TestPriority::kP0); EXPECT_EQ(result.type(), TestAllTypesProto2::descriptor()); - EXPECT_THAT(result.format(), ::conformance::TEXT_FORMAT); + EXPECT_EQ(result.format(), ::conformance::TEXT_FORMAT); EXPECT_THAT(result.response(), EqualsProto(R"pb(runtime_error: "error")pb")); } @@ -143,7 +149,7 @@ EXPECT_EQ(result.name(), "Required.Proto2.JsonInput.foo.JsonOutput"); EXPECT_EQ(result.priority(), TestPriority::kP0); EXPECT_EQ(result.type(), TestAllTypesProto2::descriptor()); - EXPECT_THAT(result.format(), ::conformance::JSON); + EXPECT_EQ(result.format(), ::conformance::JSON); EXPECT_THAT(result.response(), EqualsProto(R"pb(runtime_error: "error")pb")); } @@ -169,7 +175,7 @@ EXPECT_EQ(result.name(), "Required.Proto2.JsonInput.foo.ProtobufOutput"); EXPECT_EQ(result.priority(), TestPriority::kP0); EXPECT_EQ(result.type(), TestAllTypesProto2::descriptor()); - EXPECT_THAT(result.format(), ::conformance::PROTOBUF); + EXPECT_EQ(result.format(), ::conformance::PROTOBUF); EXPECT_THAT(result.response(), EqualsProto(R"pb(runtime_error: "error")pb")); } @@ -195,7 +201,7 @@ EXPECT_EQ(result.name(), "Required.Proto2.ProtobufInput.foo.ProtobufOutput"); EXPECT_EQ(result.priority(), TestPriority::kP0); EXPECT_EQ(result.type(), TestAllTypesProto2::descriptor()); - EXPECT_THAT(result.format(), ::conformance::PROTOBUF); + EXPECT_EQ(result.format(), ::conformance::PROTOBUF); EXPECT_THAT( result.response(), EqualsProto( @@ -222,12 +228,339 @@ .SerializeBinary(); EXPECT_DEATH( - testee.CreateTest("foo", TestPriority::kP0) + (void)testee.CreateTest("foo", TestPriority::kP0) .ParseBinary(TestAllTypesProto2::descriptor(), Wire("wire")) .SerializeBinary(), "Duplicated test name: Required.Proto2.ProtobufInput.foo.ProtobufOutput"); } +TEST(TesteeTest, RecordsTheTestsRun) { + MockTestRunner mock; + Testee testee(&mock); + EXPECT_CALL(mock, RunTest).WillRepeatedly(Return(std::string("\004"))); + EXPECT_THAT(testee.tests_run(), IsEmpty()); + + (void)testee.CreateTest("foo", TestPriority::kP0) + .ParseBinary(TestAllTypesProto2::descriptor(), Wire("wire")) + .SerializeBinary(); + (void)testee.CreateTest("bar", TestPriority::kP1) + .ParseBinary(TestAllTypesProto2::descriptor(), Wire("wire")) + .SerializeText(); + + EXPECT_THAT( + testee.tests_run(), + ElementsAre("Required.Proto2.ProtobufInput.foo.ProtobufOutput", + "Recommended.Proto2.ProtobufInput.bar.TextFormatOutput")); +} + +TEST(TesteeTest, ParseOnlyBinary) { + MockTestRunner mock; + Testee testee(&mock); + EXPECT_CALL( + mock, + RunTest("Required.Proto2.ProtobufInput.foo", RequestEquals(R"pb( + protobuf_payload: "wire" + requested_output_format: PROTOBUF + message_type: "protobuf_test_messages.proto2.TestAllTypesProto2" + test_category: BINARY_TEST + )pb"))) + .WillOnce(RespondWith(R"pb(parse_error: "error")pb")); + + TestResult result = + testee.CreateTest("foo", TestPriority::kP0) + .ParseBinary(TestAllTypesProto2::descriptor(), Wire("wire")) + .ParseOnly(); + + EXPECT_EQ(result.name(), "Required.Proto2.ProtobufInput.foo"); + EXPECT_EQ(result.priority(), TestPriority::kP0); + EXPECT_EQ(result.type(), TestAllTypesProto2::descriptor()); + EXPECT_EQ(result.format(), ::conformance::PROTOBUF); + EXPECT_THAT(result.response(), EqualsProto(R"pb(parse_error: "error")pb")); +} + +// The request of a ParseOnly() test must be byte-identical to the one +// SerializeBinary() sends for the same input; only the test name differs. +TEST(TesteeTest, ParseOnlyBinarySendsSameRequestAsSerializeBinary) { + MockTestRunner mock; + Testee testee(&mock); + std::string serialized_request, parse_only_request; + EXPECT_CALL(mock, + RunTest("Required.Proto2.ProtobufInput.foo.ProtobufOutput", _)) + .WillOnce(DoAll(SaveArg<1>(&serialized_request), + RespondWith(R"pb(parse_error: "error")pb"))); + EXPECT_CALL(mock, RunTest("Required.Proto2.ProtobufInput.foo", _)) + .WillOnce(DoAll(SaveArg<1>(&parse_only_request), + RespondWith(R"pb(parse_error: "error")pb"))); + + TestResult serialized = + testee.CreateTest("foo", TestPriority::kP0) + .ParseBinary(TestAllTypesProto2::descriptor(), Wire("wire")) + .SerializeBinary(); + TestResult parse_only = + testee.CreateTest("foo", TestPriority::kP0) + .ParseBinary(TestAllTypesProto2::descriptor(), Wire("wire")) + .ParseOnly(); + + EXPECT_EQ(serialized.name(), + "Required.Proto2.ProtobufInput.foo.ProtobufOutput"); + EXPECT_EQ(parse_only.name(), "Required.Proto2.ProtobufInput.foo"); + EXPECT_EQ(parse_only_request, serialized_request); +} + +TEST(TesteeTest, ParseOnlyText) { + MockTestRunner mock; + Testee testee(&mock); + EXPECT_CALL( + mock, + RunTest("Recommended.Proto2.TextFormatInput.foo", RequestEquals(R"pb( + text_payload: "text" + requested_output_format: TEXT_FORMAT + message_type: "protobuf_test_messages.proto2.TestAllTypesProto2" + test_category: TEXT_FORMAT_TEST + )pb"))) + .WillOnce(RespondWith(R"pb(parse_error: "error")pb")); + + TestResult result = testee.CreateTest("foo", TestPriority::kP1) + .ParseText(TestAllTypesProto2::descriptor(), "text") + .ParseOnly(); + + EXPECT_EQ(result.name(), "Recommended.Proto2.TextFormatInput.foo"); + EXPECT_EQ(result.priority(), TestPriority::kP1); + EXPECT_EQ(result.format(), ::conformance::TEXT_FORMAT); + EXPECT_THAT(result.response(), EqualsProto(R"pb(parse_error: "error")pb")); +} + +TEST(TesteeTest, ParseOnlyJson) { + MockTestRunner mock; + Testee testee(&mock); + EXPECT_CALL( + mock, + RunTest("Required.Proto2.JsonInput.foo", RequestEquals(R"pb( + json_payload: "json" + requested_output_format: JSON + message_type: "protobuf_test_messages.proto2.TestAllTypesProto2" + test_category: JSON_TEST + )pb"))) + .WillOnce(RespondWith(R"pb(parse_error: "error")pb")); + + TestResult result = testee.CreateTest("foo", TestPriority::kP0) + .ParseJson(TestAllTypesProto2::descriptor(), "json") + .ParseOnly(); + + EXPECT_EQ(result.name(), "Required.Proto2.JsonInput.foo"); + EXPECT_EQ(result.format(), ::conformance::JSON); + EXPECT_THAT(result.response(), EqualsProto(R"pb(parse_error: "error")pb")); +} + +TEST(TesteeDeathTest, ParseOnlyDuplicateTestName) { + MockTestRunner mock; + Testee testee(&mock); + EXPECT_CALL(mock, RunTest("Required.Proto2.ProtobufInput.foo", _)) + .WillRepeatedly(RespondWith(R"pb(parse_error: "error")pb")); + + TestResult result = + testee.CreateTest("foo", TestPriority::kP0) + .ParseBinary(TestAllTypesProto2::descriptor(), Wire("wire")) + .ParseOnly(); + + EXPECT_DEATH((void)testee.CreateTest("foo", TestPriority::kP0) + .ParseBinary(TestAllTypesProto2::descriptor(), Wire("wire")) + .ParseOnly(), + "Duplicated test name: Required.Proto2.ProtobufInput.foo"); +} + +TEST(TesteeTest, OverrideTestCategory) { + MockTestRunner mock; + Testee testee(&mock); + // The category changes, the name (derived from the input format) doesn't. + EXPECT_CALL( + mock, + RunTest("Required.Proto2.ProtobufInput.foo.TextFormatOutput", + RequestEquals(R"pb( + protobuf_payload: "wire" + requested_output_format: TEXT_FORMAT + message_type: "protobuf_test_messages.proto2.TestAllTypesProto2" + test_category: TEXT_FORMAT_TEST + )pb"))) + .WillOnce(RespondWith(R"pb(text_payload: "")pb")); + + TestResult result = + testee.CreateTest("foo", TestPriority::kP0) + .ParseBinary(TestAllTypesProto2::descriptor(), Wire("wire")) + .OverrideTestCategory(::conformance::TEXT_FORMAT_TEST) + .SerializeText(); + + EXPECT_EQ(result.name(), + "Required.Proto2.ProtobufInput.foo.TextFormatOutput"); +} + +TEST(TesteeTest, OverrideTestCategoryParseOnly) { + MockTestRunner mock; + Testee testee(&mock); + EXPECT_CALL( + mock, + RunTest("Required.Proto2.ProtobufInput.foo", RequestEquals(R"pb( + protobuf_payload: "wire" + requested_output_format: PROTOBUF + message_type: "protobuf_test_messages.proto2.TestAllTypesProto2" + test_category: TEXT_FORMAT_TEST + )pb"))) + .WillOnce(RespondWith(R"pb(parse_error: "error")pb")); + + TestResult result = + testee.CreateTest("foo", TestPriority::kP0) + .ParseBinary(TestAllTypesProto2::descriptor(), Wire("wire")) + .OverrideTestCategory(::conformance::TEXT_FORMAT_TEST) + .ParseOnly(); + + EXPECT_EQ(result.name(), "Required.Proto2.ProtobufInput.foo"); +} + +TEST(TesteeDeathTest, OverrideTestCategoryAcceptsOnlyTextFormatTest) { + MockTestRunner mock; + Testee testee(&mock); + // Without the DCHECK (opt builds) the request goes through as relabeled. + EXPECT_CALL(mock, RunTest) + .Times(AnyNumber()) + .WillRepeatedly(RespondWith(R"pb(parse_error: "error")pb")); + EXPECT_DEBUG_DEATH( + (void)testee.CreateTest("foo", TestPriority::kP0) + .ParseBinary(TestAllTypesProto2::descriptor(), Wire("wire")) + .OverrideTestCategory(::conformance::JSON_TEST) + .ParseOnly(), + "only relabels the text-format suite's binary-input tests"); +} + +TEST(TesteeDeathTest, OverrideTestCategoryNeedsBinaryInput) { + MockTestRunner mock; + Testee testee(&mock); + // Without the DCHECK (opt builds) the request goes through as relabeled. + EXPECT_CALL(mock, RunTest) + .Times(AnyNumber()) + .WillRepeatedly(RespondWith(R"pb(parse_error: "error")pb")); + EXPECT_DEBUG_DEATH((void)testee.CreateTest("foo", TestPriority::kP0) + .ParseJson(TestAllTypesProto2::descriptor(), "{}") + .OverrideTestCategory(::conformance::TEXT_FORMAT_TEST) + .ParseOnly(), + "is for PROTOBUF-input tests"); +} + +// --------------------------------------------------------------------------- +// JSON test names and ParseOnly() output override +// --------------------------------------------------------------------------- + +TEST(TesteeTest, SerializeJsonNamesTheTestWithInputAndOutputFormat) { + MockTestRunner mock; + Testee testee(&mock); + EXPECT_CALL(mock, RunTest("Required.Proto2.JsonInput.foo.JsonOutput", _)) + .WillOnce(RespondWith(R"pb(json_payload: "{}")pb")); + + TestResult result = testee.CreateTest("foo", TestPriority::kP0) + .ParseJson(TestAllTypesProto2::descriptor(), "json") + .SerializeJson(); + + EXPECT_EQ(result.name(), "Required.Proto2.JsonInput.foo.JsonOutput"); + EXPECT_EQ(result.format(), ::conformance::JSON); +} + +TEST(TesteeTest, SerializeJsonFromBinaryInputNamesTheInputFormat) { + MockTestRunner mock; + Testee testee(&mock); + EXPECT_CALL( + mock, + RunTest("Recommended.Proto2.ProtobufInput.foo.JsonOutput", + RequestEquals(R"pb( + protobuf_payload: "wire" + requested_output_format: JSON + message_type: "protobuf_test_messages.proto2.TestAllTypesProto2" + test_category: BINARY_TEST + )pb"))) + .WillOnce(RespondWith(R"pb(serialize_error: "error")pb")); + + TestResult result = + testee.CreateTest("foo", TestPriority::kP1) + .ParseBinary(TestAllTypesProto2::descriptor(), Wire("wire")) + .SerializeJson(); + + EXPECT_EQ(result.name(), "Recommended.Proto2.ProtobufInput.foo.JsonOutput"); + EXPECT_EQ(result.format(), ::conformance::JSON); + EXPECT_THAT(result.response(), + EqualsProto(R"pb(serialize_error: "error")pb")); +} + +TEST(TesteeTest, ParseOnlyJsonWithProtobufOutput) { + // The legacy RunValidJsonTestOrParseFailure asked for PROTOBUF output but + // named the test like a parse-failure test. + MockTestRunner mock; + Testee testee(&mock); + EXPECT_CALL( + mock, + RunTest("Required.Proto2.JsonInput.foo", RequestEquals(R"pb( + json_payload: "json" + requested_output_format: PROTOBUF + message_type: "protobuf_test_messages.proto2.TestAllTypesProto2" + test_category: JSON_TEST + )pb"))) + .WillOnce(RespondWith(R"pb(parse_error: "error")pb")); + + TestResult result = + testee.CreateTest("foo", TestPriority::kP0) + .ParseJson(TestAllTypesProto2::descriptor(), "json") + .ParseOnly({/*output_format=*/::conformance::PROTOBUF}); + + EXPECT_EQ(result.name(), "Required.Proto2.JsonInput.foo"); + EXPECT_EQ(result.format(), ::conformance::PROTOBUF); + EXPECT_THAT(result.response(), EqualsProto(R"pb(parse_error: "error")pb")); +} + +// With the output format overridden, the request is byte-identical to the one +// SerializeBinary() sends for the same input; only the name differs. +TEST(TesteeTest, ParseOnlyWithProtobufOutputSendsSameRequestAsSerializeBinary) { + MockTestRunner mock; + Testee testee(&mock); + std::string serialized_request, parse_only_request; + EXPECT_CALL(mock, RunTest("Required.Proto2.JsonInput.foo.ProtobufOutput", _)) + .WillOnce(DoAll(SaveArg<1>(&serialized_request), + RespondWith(R"pb(parse_error: "error")pb"))); + EXPECT_CALL(mock, RunTest("Required.Proto2.JsonInput.foo", _)) + .WillOnce(DoAll(SaveArg<1>(&parse_only_request), + RespondWith(R"pb(parse_error: "error")pb"))); + + TestResult serialized = + testee.CreateTest("foo", TestPriority::kP0) + .ParseJson(TestAllTypesProto2::descriptor(), "json") + .SerializeBinary(); + TestResult parse_only = + testee.CreateTest("foo", TestPriority::kP0) + .ParseJson(TestAllTypesProto2::descriptor(), "json") + .ParseOnly({/*output_format=*/::conformance::PROTOBUF}); + + EXPECT_EQ(serialized.name(), "Required.Proto2.JsonInput.foo.ProtobufOutput"); + EXPECT_EQ(parse_only.name(), "Required.Proto2.JsonInput.foo"); + EXPECT_EQ(parse_only_request, serialized_request); +} + +TEST(TesteeTest, ParseOnlyWithExplicitInputFormatIsTheDefault) { + MockTestRunner mock; + Testee testee(&mock); + EXPECT_CALL( + mock, + RunTest("Required.Proto2.JsonInput.foo", RequestEquals(R"pb( + json_payload: "json" + requested_output_format: JSON + message_type: "protobuf_test_messages.proto2.TestAllTypesProto2" + test_category: JSON_TEST + )pb"))) + .WillOnce(RespondWith(R"pb(parse_error: "error")pb")); + + TestResult result = testee.CreateTest("foo", TestPriority::kP0) + .ParseJson(TestAllTypesProto2::descriptor(), "json") + .ParseOnly({/*output_format=*/::conformance::JSON}); + + EXPECT_EQ(result.name(), "Required.Proto2.JsonInput.foo"); + EXPECT_EQ(result.format(), ::conformance::JSON); +} + TEST(TestPriorityTest, PriorityName) { EXPECT_EQ(PriorityName(kP0), "P0"); EXPECT_EQ(PriorityName(kP1), "P1");
diff --git a/conformance/text_any_test.cc b/conformance/text_any_test.cc new file mode 100644 index 0000000..fd96dd5 --- /dev/null +++ b/conformance/text_any_test.cc
@@ -0,0 +1,199 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2026 Google LLC. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +// Text-format conformance tests for google.protobuf.Any fields: the +// `[type_url] { ... }` expansion, raw type_url/value pairs, and the type URL +// prefixes and separators a parser must accept or reject. This replaces the +// legacy TextFormatConformanceTestSuiteImpl<M>::RunAnyTests(), which ran for +// the proto3-style message types only; the test names and the requests sent to +// the testee are identical to the legacy ones. + +#include <gmock/gmock.h> +#include <gtest/gtest.h> +#include "absl/strings/string_view.h" +#include "conformance/binary_test_util.h" +#include "conformance/matchers.h" +#include "conformance/message_type_fixtures.h" +#include "conformance/testee.h" + +namespace google { +namespace protobuf { +namespace conformance { +namespace { + +using ::testing::ValuesIn; + +// The inputs below are the legacy raw string literals verbatim: their +// surrounding whitespace and line breaks are part of the request bytes. The +// packed message is always protobuf_test_messages.proto3.TestAllTypesProto3, +// whichever message type the test runs for, as in the legacy suite. + +using TextAnyTest = MessageTypeConformanceTest; + +TEST_P(TextAnyTest, AnyField) { + constexpr absl::string_view kInput = R"( + optional_any: { + [type.googleapis.com/protobuf_test_messages.proto3.TestAllTypesProto3] + { optional_int32: 12345 + } + } + )"; + EXPECT_THAT(Testee("AnyField").ParseText(message(), kInput).SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kInput)))); + EXPECT_THAT(Testee("AnyField").ParseText(message(), kInput).SerializeText(), + Yields(WhenParsed(EqualsTextProto(kInput)))); +} + +TEST_P(TextAnyTest, WithRawBytes) { + constexpr absl::string_view kInput = R"( + optional_any: { + type_url: + "type.googleapis.com/protobuf_test_messages.proto3.TestAllTypesProto3" value: + "\b\271`" + } + )"; + EXPECT_THAT(Testee("AnyFieldWithRawBytes") + .ParseText(message(), kInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kInput)))); + EXPECT_THAT(Testee("AnyFieldWithRawBytes") + .ParseText(message(), kInput) + .SerializeText(), + Yields(WhenParsed(EqualsTextProto(kInput)))); +} + +TEST_P(TextAnyTest, WithInvalidType) { + constexpr absl::string_view kInput = R"( + optional_any: { + [type.googleapis.com/unknown] { + optional_int32: 12345 + } + } + )"; + EXPECT_THAT(Testee("AnyFieldWithInvalidType") + .ParseText(message(), kInput) + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(TextAnyTest, WithCustomTypeUrlPrefix) { + constexpr absl::string_view kInput = R"( + optional_any: { + [non.default.domain/protobuf_test_messages.proto3.TestAllTypesProto3] + { + optional_int32: 12345 + } + } + )"; + EXPECT_THAT(Testee("AnyFieldWithCustomTypeUrlPrefix") + .ParseText(message(), kInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kInput)))); + EXPECT_THAT(Testee("AnyFieldWithCustomTypeUrlPrefix") + .ParseText(message(), kInput) + .SerializeText(), + Yields(WhenParsed(EqualsTextProto(kInput)))); +} + +TEST_P(TextAnyTest, WithSpecialCharactersInTypeUrlPrefix) { + constexpr absl::string_view kInput = R"( + optional_any: { + [non.default.domain/sub-path_0/~!$&()*+,;=/protobuf_test_messages.proto3.TestAllTypesProto3] + { + optional_int32: 12345 + } + } + )"; + EXPECT_THAT(Testee("AnyFieldWithSpecialCharactersInTypeUrlPrefix") + .ParseText(message(), kInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kInput)))); + EXPECT_THAT(Testee("AnyFieldWithSpecialCharactersInTypeUrlPrefix") + .ParseText(message(), kInput) + .SerializeText(), + Yields(WhenParsed(EqualsTextProto(kInput)))); +} + +TEST_P(TextAnyTest, WithValidUrlPercentEscapeInTypeUrlPrefix) { + constexpr absl::string_view kInput = R"( + optional_any: { + [non.default.domain/%2F/protobuf_test_messages.proto3.TestAllTypesProto3] + { + optional_int32: 12345 + } + } + )"; + EXPECT_THAT(Testee("AnyFieldWithValidUrlPercentEscapeInTypeUrlPrefix") + .ParseText(message(), kInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kInput)))); + EXPECT_THAT(Testee("AnyFieldWithValidUrlPercentEscapeInTypeUrlPrefix") + .ParseText(message(), kInput) + .SerializeText(), + Yields(WhenParsed(EqualsTextProto(kInput)))); +} + +TEST_P(TextAnyTest, WithInvalidUrlPercentEscapeInTypeUrlPrefix) { + constexpr absl::string_view kInput = R"( + optional_any: { + [non.default.domain/%ZZ/protobuf_test_messages.proto3.TestAllTypesProto3] + { + optional_int32: 12345 + } + } + )"; + EXPECT_THAT(Testee("AnyFieldWithInvalidUrlPercentEscapeInTypeUrlPrefix") + .ParseText(message(), kInput) + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(TextAnyTest, WithWhitespaceInTypeUrl) { + constexpr absl::string_view kInput = + "optional_any: {\n" + " [ ty pe.go\nogleap\tis.com/\n" + " proto buf_te\nst_messages.proto3.Test\tAllTypesProto3 ]\n" + " {\n" + " optional_int32: 12345\n" + " }\n" + "}"; + EXPECT_THAT(Testee("AnyFieldWithWhitespaceInTypeUrl") + .ParseText(message(), kInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kInput)))); + EXPECT_THAT(Testee("AnyFieldWithWhitespaceInTypeUrl") + .ParseText(message(), kInput) + .SerializeText(), + Yields(WhenParsed(EqualsTextProto(kInput)))); +} + +TEST_P(TextAnyTest, WithCommentsInTypeUrl) { + constexpr absl::string_view kInput = + "optional_any: {\n" + " [type.google # comment \napis.com/" + "protobuf_test_messages.proto3.Test # comment \nAllTypesProto3]" + " {\n" + " optional_int32: 12345\n" + " }\n" + "}"; + EXPECT_THAT(Testee("AnyFieldWithCommentsInTypeUrl") + .ParseText(message(), kInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kInput)))); + EXPECT_THAT(Testee("AnyFieldWithCommentsInTypeUrl") + .ParseText(message(), kInput) + .SerializeText(), + Yields(WhenParsed(EqualsTextProto(kInput)))); +} + +INSTANTIATE_TEST_SUITE_P(All, TextAnyTest, ValuesIn(Proto3TestMessageTypes()), + MessageTypeParamName); + +} // namespace +} // namespace conformance +} // namespace protobuf +} // namespace google
diff --git a/conformance/text_enum_test.cc b/conformance/text_enum_test.cc new file mode 100644 index 0000000..c0cf932 --- /dev/null +++ b/conformance/text_enum_test.cc
@@ -0,0 +1,94 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2026 Google LLC. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +// Text-format conformance tests for enum fields given by number. An open +// enum (proto3-style message types) keeps any number, a closed enum +// (proto2-style message types) rejects numbers that aren't one of its values. +// This replaces the legacy TextFormatConformanceTestSuiteImpl<M>:: +// RunOpenEnumTests() and RunClosedEnumTests(); the test names and the +// requests sent to the testee are identical to the legacy ones. + +#include <gmock/gmock.h> +#include <gtest/gtest.h> +#include "absl/strings/string_view.h" +#include "conformance/binary_test_util.h" +#include "conformance/matchers.h" +#include "conformance/message_type_fixtures.h" +#include "conformance/testee.h" + +namespace google { +namespace protobuf { +namespace conformance { +namespace { + +using ::testing::ValuesIn; + +// TODO: Write the plain-string inputs below as R"pb(...)pb" raw +// strings once the migration's request goldens are gone. The goldens pin the +// exact request bytes, and clang-format would reformat the whitespace of these +// inputs inside a pb raw string. +constexpr absl::string_view kKnownNumberInput = + "\n optional_nested_enum: 1\n "; +constexpr absl::string_view kUnknownNumberInput = + "\n optional_nested_enum: 42\n "; + +using TextOpenEnumTest = MessageTypeConformanceTest; + +TEST_P(TextOpenEnumTest, ByNumber) { + EXPECT_THAT(Testee("ClosedEnumFieldByNumber") + .ParseText(message(), kKnownNumberInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kKnownNumberInput)))); + EXPECT_THAT(Testee("ClosedEnumFieldByNumber") + .ParseText(message(), kKnownNumberInput) + .SerializeText(), + Yields(WhenParsed(EqualsTextProto(kKnownNumberInput)))); +} + +TEST_P(TextOpenEnumTest, WithUnknownNumber) { + EXPECT_THAT(Testee("ClosedEnumFieldWithUnknownNumber") + .ParseText(message(), kUnknownNumberInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kUnknownNumberInput)))); + EXPECT_THAT(Testee("ClosedEnumFieldWithUnknownNumber") + .ParseText(message(), kUnknownNumberInput) + .SerializeText(), + Yields(WhenParsed(EqualsTextProto(kUnknownNumberInput)))); +} + +INSTANTIATE_TEST_SUITE_P(All, TextOpenEnumTest, + ValuesIn(Proto3TestMessageTypes()), + MessageTypeParamName); + +using TextClosedEnumTest = MessageTypeConformanceTest; + +TEST_P(TextClosedEnumTest, ByNumber) { + EXPECT_THAT(Testee("ClosedEnumFieldByNumber") + .ParseText(message(), kKnownNumberInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kKnownNumberInput)))); + EXPECT_THAT(Testee("ClosedEnumFieldByNumber") + .ParseText(message(), kKnownNumberInput) + .SerializeText(), + Yields(WhenParsed(EqualsTextProto(kKnownNumberInput)))); +} + +TEST_P(TextClosedEnumTest, WithUnknownNumber) { + EXPECT_THAT(Testee("ClosedEnumFieldWithUnknownNumber") + .ParseText(message(), kUnknownNumberInput) + .ParseOnly(), + Yields(IsParseError())); +} + +INSTANTIATE_TEST_SUITE_P(All, TextClosedEnumTest, + ValuesIn(Proto2TestMessageTypes()), + MessageTypeParamName); + +} // namespace +} // namespace conformance +} // namespace protobuf +} // namespace google
diff --git a/conformance/text_float_test.cc b/conformance/text_float_test.cc new file mode 100644 index 0000000..6058415 --- /dev/null +++ b/conformance/text_float_test.cc
@@ -0,0 +1,537 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2026 Google LLC. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +// Text-format conformance tests for float and double literals: the decimal +// forms with and without the "f"/"F" type suffix, the case-insensitive special +// values, the rejected hex and octal forms, and the literals that overflow or +// underflow the field's range. This replaces the floating point section of +// the legacy TextFormatConformanceTestSuiteImpl<M>::RunAllTests(), which ran +// for the proto3-style message types only; the test names and the requests +// sent to the testee are identical to the legacy ones. + +#include <ostream> +#include <string> +#include <tuple> + +#include <gmock/gmock.h> +#include <gtest/gtest.h> +#include "absl/strings/str_cat.h" +#include "absl/strings/string_view.h" +#include "conformance/binary_test_util.h" +#include "conformance/matchers.h" +#include "conformance/message_type_fixtures.h" +#include "conformance/test_fixture.h" +#include "conformance/testee.h" +#include "google/protobuf/descriptor.h" + +namespace google { +namespace protobuf { +namespace conformance { +namespace { + +using ::testing::Combine; +using ::testing::Values; +using ::testing::ValuesIn; + +// The type suffix a float literal is tested with. The legacy suite ran every +// literal test three times: bare, and with the "f" and "F" suffixes, the +// latter two with "_f" / "_F" appended to the test name. +struct FloatSuffix { + absl::string_view suffix; + // The component of the gtest parameter name (ParamName() requires a + // non-empty one, so the bare case can't just use `suffix`). + absl::string_view param_name; +}; + +constexpr FloatSuffix kFloatSuffixes[] = { + {"", "NoSuffix"}, + {"f", "f"}, + {"F", "F"}, +}; + +std::string ParamName(const FloatSuffix& suffix) { + return std::string(suffix.param_name); +} + +// So a failing test prints the suffix by name instead of as raw bytes. +void PrintTo(const FloatSuffix& suffix, std::ostream* os) { + *os << suffix.param_name; +} + +// Parameterized over (test message type, float suffix). Reporting the +// message type through MessageUnderTest() makes the base SetUp() skip the +// editions instance when --maximum_edition doesn't cover it. TestName() and +// Input() only build values; every expectation is in the test bodies. +class TextFloatLiteralTest : public ConformanceTest, + public testing::WithParamInterface< + std::tuple<const Descriptor*, FloatSuffix>> { + protected: + const Descriptor* message() const { return std::get<0>(GetParam()); } + absl::string_view suffix() const { return std::get<1>(GetParam()).suffix; } + + // The legacy test name: `base` as is for the bare literal, otherwise + // "<base>_<suffix>", e.g. "FloatFieldMaxValue_f". + std::string TestName(absl::string_view base) const { + return suffix().empty() ? std::string(base) + : absl::StrCat(base, "_", suffix()); + } + + // The text-format input setting optional_float to `literal` with the + // suffix appended, e.g. "optional_float: 3.192837f". + std::string Input(absl::string_view literal) const { + return absl::StrCat("optional_float: ", literal, suffix()); + } + + private: + const Descriptor* MessageUnderTest() const override { return message(); } +}; + +TEST_P(TextFloatLiteralTest, Positive) { + const std::string input = Input("3.192837"); + EXPECT_THAT(Testee(TestName("FloatField")) + .ParseText(message(), input) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(input)))); + EXPECT_THAT(Testee(TestName("FloatField")) + .ParseText(message(), input) + .SerializeText(), + Yields(WhenParsed(EqualsTextProto(input)))); +} + +// The field has implicit presence, so zero means unset. +TEST_P(TextFloatLiteralTest, Zero) { + EXPECT_THAT(Testee(TestName("FloatFieldZero")) + .ParseText(message(), Input("0")) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto("")))); + EXPECT_THAT(Testee(TestName("FloatFieldZero")) + .ParseText(message(), Input("0")) + .SerializeText(), + Yields(WhenParsed(EqualsTextProto("")))); +} + +TEST_P(TextFloatLiteralTest, Negative) { + const std::string input = Input("-3.192837"); + EXPECT_THAT(Testee(TestName("FloatFieldNegative")) + .ParseText(message(), input) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(input)))); + EXPECT_THAT(Testee(TestName("FloatFieldNegative")) + .ParseText(message(), input) + .SerializeText(), + Yields(WhenParsed(EqualsTextProto(input)))); +} + +TEST_P(TextFloatLiteralTest, VeryPreciseNumber) { + const std::string input = Input("3.123456789123456789"); + EXPECT_THAT(Testee(TestName("FloatFieldWithVeryPreciseNumber")) + .ParseText(message(), input) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(input)))); + EXPECT_THAT(Testee(TestName("FloatFieldWithVeryPreciseNumber")) + .ParseText(message(), input) + .SerializeText(), + Yields(WhenParsed(EqualsTextProto(input)))); +} + +TEST_P(TextFloatLiteralTest, MaxValue) { + const std::string input = Input("3.4028235e+38"); + EXPECT_THAT(Testee(TestName("FloatFieldMaxValue")) + .ParseText(message(), input) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(input)))); + EXPECT_THAT(Testee(TestName("FloatFieldMaxValue")) + .ParseText(message(), input) + .SerializeText(), + Yields(WhenParsed(EqualsTextProto(input)))); +} + +TEST_P(TextFloatLiteralTest, MinValue) { + const std::string input = Input("1.17549e-38"); + EXPECT_THAT(Testee(TestName("FloatFieldMinValue")) + .ParseText(message(), input) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(input)))); + EXPECT_THAT(Testee(TestName("FloatFieldMinValue")) + .ParseText(message(), input) + .SerializeText(), + Yields(WhenParsed(EqualsTextProto(input)))); +} + +TEST_P(TextFloatLiteralTest, WithInt32Max) { + const std::string input = Input("4294967296"); + EXPECT_THAT(Testee(TestName("FloatFieldWithInt32Max")) + .ParseText(message(), input) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(input)))); + EXPECT_THAT(Testee(TestName("FloatFieldWithInt32Max")) + .ParseText(message(), input) + .SerializeText(), + Yields(WhenParsed(EqualsTextProto(input)))); +} + +TEST_P(TextFloatLiteralTest, LargerThanInt64) { + const std::string input = Input("9223372036854775808"); + EXPECT_THAT(Testee(TestName("FloatFieldLargerThanInt64")) + .ParseText(message(), input) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(input)))); + EXPECT_THAT(Testee(TestName("FloatFieldLargerThanInt64")) + .ParseText(message(), input) + .SerializeText(), + Yields(WhenParsed(EqualsTextProto(input)))); +} + +TEST_P(TextFloatLiteralTest, TooLarge) { + const std::string input = Input("3.4028235e+39"); + EXPECT_THAT(Testee(TestName("FloatFieldTooLarge")) + .ParseText(message(), input) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(input)))); + EXPECT_THAT(Testee(TestName("FloatFieldTooLarge")) + .ParseText(message(), input) + .SerializeText(), + Yields(WhenParsed(EqualsTextProto(input)))); +} + +TEST_P(TextFloatLiteralTest, TooSmall) { + const std::string input = Input("1.17549e-39"); + EXPECT_THAT(Testee(TestName("FloatFieldTooSmall")) + .ParseText(message(), input) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(input)))); + EXPECT_THAT(Testee(TestName("FloatFieldTooSmall")) + .ParseText(message(), input) + .SerializeText(), + Yields(WhenParsed(EqualsTextProto(input)))); +} + +TEST_P(TextFloatLiteralTest, LargerThanUint64) { + const std::string input = Input("18446744073709551616"); + EXPECT_THAT(Testee(TestName("FloatFieldLargerThanUint64")) + .ParseText(message(), input) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(input)))); + EXPECT_THAT(Testee(TestName("FloatFieldLargerThanUint64")) + .ParseText(message(), input) + .SerializeText(), + Yields(WhenParsed(EqualsTextProto(input)))); +} + +// https://protobuf.dev/reference/protobuf/textformat-spec/#literals says +// "-0" is a valid float literal. -0 should be considered not the same as 0 +// when considering implicit presence, and so should round trip. +TEST_P(TextFloatLiteralTest, NegativeZero) { + const std::string input = Input("-0"); + EXPECT_THAT(Testee(TestName("FloatFieldNegativeZero")) + .ParseText(message(), input) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(input)))); + EXPECT_THAT(Testee(TestName("FloatFieldNegativeZero")) + .ParseText(message(), input) + .SerializeText(), + Yields(WhenParsed(EqualsTextProto(input)))); +} + +// https://protobuf.dev/reference/protobuf/textformat-spec/#literals says +// ".123", "-.123", ".123e2" are a valid float literal. +TEST_P(TextFloatLiteralTest, NoLeadingZero) { + const std::string input = Input(".123"); + EXPECT_THAT(Testee(TestName("FloatFieldNoLeadingZero")) + .ParseText(message(), input) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(input)))); + EXPECT_THAT(Testee(TestName("FloatFieldNoLeadingZero")) + .ParseText(message(), input) + .SerializeText(), + Yields(WhenParsed(EqualsTextProto(input)))); +} + +TEST_P(TextFloatLiteralTest, NegativeNoLeadingZero) { + const std::string input = Input("-.123"); + EXPECT_THAT(Testee(TestName("FloatFieldNegativeNoLeadingZero")) + .ParseText(message(), input) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(input)))); + EXPECT_THAT(Testee(TestName("FloatFieldNegativeNoLeadingZero")) + .ParseText(message(), input) + .SerializeText(), + Yields(WhenParsed(EqualsTextProto(input)))); +} + +TEST_P(TextFloatLiteralTest, NoLeadingZeroWithExponent) { + const std::string input = Input(".123e2"); + EXPECT_THAT(Testee(TestName("FloatFieldNoLeadingZeroWithExponent")) + .ParseText(message(), input) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(input)))); + EXPECT_THAT(Testee(TestName("FloatFieldNoLeadingZeroWithExponent")) + .ParseText(message(), input) + .SerializeText(), + Yields(WhenParsed(EqualsTextProto(input)))); +} + +INSTANTIATE_TEST_SUITE_P(All, TextFloatLiteralTest, + Combine(ValuesIn(Proto3TestMessageTypes()), + ValuesIn(kFloatSuffixes)), + TupleParamName<TextFloatLiteralTest::ParamType>); + +// https://protobuf.dev/reference/protobuf/textformat-spec/#value say case +// doesn't matter for special values, test a few. +// +// Parameterized over (test message type, spelling of the special value); the +// two suites below only differ in the spellings they are instantiated with. +class TextFloatSpecialValueTest + : public ConformanceTest, + public testing::WithParamInterface< + std::tuple<const Descriptor*, absl::string_view>> { + protected: + const Descriptor* message() const { return std::get<0>(GetParam()); } + absl::string_view spelling() const { return std::get<1>(GetParam()); } + + private: + const Descriptor* MessageUnderTest() const override { return message(); } +}; + +using TextFloatNanTest = TextFloatSpecialValueTest; + +TEST_P(TextFloatNanTest, Spelling) { + const std::string input = absl::StrCat("optional_float: ", spelling()); + EXPECT_THAT(Testee(absl::StrCat("FloatFieldValue_", spelling())) + .ParseText(message(), input) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(input)))); + EXPECT_THAT(Testee(absl::StrCat("FloatFieldValue_", spelling())) + .ParseText(message(), input) + .SerializeText(), + Yields(WhenParsed(EqualsTextProto(input)))); +} + +INSTANTIATE_TEST_SUITE_P(All, TextFloatNanTest, + Combine(ValuesIn(Proto3TestMessageTypes()), + Values("nan", "NaN", "nAn")), + TupleParamName<TextFloatNanTest::ParamType>); + +using TextFloatInfinityTest = TextFloatSpecialValueTest; + +TEST_P(TextFloatInfinityTest, Positive) { + const std::string input = absl::StrCat("optional_float: ", spelling()); + EXPECT_THAT(Testee(absl::StrCat("FloatFieldValue_Pos", spelling())) + .ParseText(message(), input) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(input)))); + EXPECT_THAT(Testee(absl::StrCat("FloatFieldValue_Pos", spelling())) + .ParseText(message(), input) + .SerializeText(), + Yields(WhenParsed(EqualsTextProto(input)))); +} + +TEST_P(TextFloatInfinityTest, Negative) { + const std::string input = absl::StrCat("optional_float: -", spelling()); + EXPECT_THAT(Testee(absl::StrCat("FloatFieldValue_Neg", spelling())) + .ParseText(message(), input) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(input)))); + EXPECT_THAT(Testee(absl::StrCat("FloatFieldValue_Neg", spelling())) + .ParseText(message(), input) + .SerializeText(), + Yields(WhenParsed(EqualsTextProto(input)))); +} + +INSTANTIATE_TEST_SUITE_P(All, TextFloatInfinityTest, + Combine(ValuesIn(Proto3TestMessageTypes()), + Values("inf", "infinity", "INF", "INFINITY", + "iNF", "inFINITY")), + TupleParamName<TextFloatInfinityTest::ParamType>); + +// The remaining float tests, and their double counterparts, run once per +// message type. +using TextFloatTest = MessageTypeConformanceTest; + +// https://protobuf.dev/reference/protobuf/textformat-spec/#numeric and +// https://protobuf.dev/reference/protobuf/textformat-spec/#value says +// hex or octal float literals are invalid. +TEST_P(TextFloatTest, NoHex) { + EXPECT_THAT(Testee("FloatFieldNoHex") + .ParseText(message(), R"pb(optional_float: 0x1)pb") + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(TextFloatTest, NoNegativeHex) { + EXPECT_THAT(Testee("FloatFieldNoNegativeHex") + .ParseText(message(), R"pb(optional_float: -0x1)pb") + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(TextFloatTest, NoOctal) { + EXPECT_THAT(Testee("FloatFieldNoOctal") + .ParseText(message(), R"pb(optional_float: 012)pb") + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(TextFloatTest, NoNegativeOctal) { + EXPECT_THAT(Testee("FloatFieldNoNegativeOctal") + .ParseText(message(), R"pb(optional_float: -012)pb") + .ParseOnly(), + Yields(IsParseError())); +} + +// https://protobuf.dev/reference/protobuf/textformat-spec/#value says +// overflows are mapped to infinity/-infinity. +TEST_P(TextFloatTest, OverflowInfinity) { + EXPECT_THAT( + Testee("FloatFieldOverflowInfinity") + .ParseText(message(), R"pb(optional_float: 1e50)pb") + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(R"pb(optional_float: inf)pb")))); + EXPECT_THAT( + Testee("FloatFieldOverflowInfinity") + .ParseText(message(), R"pb(optional_float: 1e50)pb") + .SerializeText(), + Yields(WhenParsed(EqualsTextProto(R"pb(optional_float: inf)pb")))); +} + +TEST_P(TextFloatTest, OverflowNegativeInfinity) { + EXPECT_THAT( + Testee("FloatFieldOverflowNegativeInfinity") + .ParseText(message(), R"pb(optional_float: -1e50)pb") + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(R"pb(optional_float: -inf)pb")))); + EXPECT_THAT( + Testee("FloatFieldOverflowNegativeInfinity") + .ParseText(message(), R"pb(optional_float: -1e50)pb") + .SerializeText(), + Yields(WhenParsed(EqualsTextProto(R"pb(optional_float: -inf)pb")))); +} + +// Exponent is one more than uint64 max. +TEST_P(TextFloatTest, OverflowInfinityHugeExponent) { + EXPECT_THAT( + Testee("FloatFieldOverflowInfinityHugeExponent") + .ParseText(message(), R"pb(optional_float: 1e18446744073709551616)pb") + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(R"pb(optional_float: inf)pb")))); + EXPECT_THAT( + Testee("FloatFieldOverflowInfinityHugeExponent") + .ParseText(message(), R"pb(optional_float: 1e18446744073709551616)pb") + .SerializeText(), + Yields(WhenParsed(EqualsTextProto(R"pb(optional_float: inf)pb")))); +} + +TEST_P(TextFloatTest, LargeNegativeExponentParsesAsZero) { + EXPECT_THAT(Testee("FloatFieldLargeNegativeExponentParsesAsZero") + .ParseText(message(), R"pb(optional_float: 1e-50)pb") + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto("")))); + EXPECT_THAT(Testee("FloatFieldLargeNegativeExponentParsesAsZero") + .ParseText(message(), R"pb(optional_float: 1e-50)pb") + .SerializeText(), + Yields(WhenParsed(EqualsTextProto("")))); +} + +TEST_P(TextFloatTest, NegativeLargeNegativeExponentParsesAsNegativeZero) { + EXPECT_THAT(Testee("NegFloatFieldLargeNegativeExponentParsesAsNegZero") + .ParseText(message(), R"pb(optional_float: -1e-50)pb") + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(R"pb(optional_float: -0)pb")))); + EXPECT_THAT(Testee("NegFloatFieldLargeNegativeExponentParsesAsNegZero") + .ParseText(message(), R"pb(optional_float: -1e-50)pb") + .SerializeText(), + Yields(WhenParsed(EqualsTextProto(R"pb(optional_float: -0)pb")))); +} + +INSTANTIATE_TEST_SUITE_P(All, TextFloatTest, ValuesIn(Proto3TestMessageTypes()), + MessageTypeParamName); + +using TextDoubleTest = MessageTypeConformanceTest; + +// https://protobuf.dev/reference/protobuf/textformat-spec/#value says +// overflows are mapped to infinity/-infinity. +TEST_P(TextDoubleTest, OverflowInfinity) { + EXPECT_THAT( + Testee("DoubleFieldOverflowInfinity") + .ParseText(message(), R"pb(optional_double: 1e9999)pb") + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(R"pb(optional_double: inf)pb")))); + EXPECT_THAT( + Testee("DoubleFieldOverflowInfinity") + .ParseText(message(), R"pb(optional_double: 1e9999)pb") + .SerializeText(), + Yields(WhenParsed(EqualsTextProto(R"pb(optional_double: inf)pb")))); +} + +TEST_P(TextDoubleTest, OverflowNegativeInfinity) { + EXPECT_THAT( + Testee("DoubleFieldOverflowNegativeInfinity") + .ParseText(message(), R"pb(optional_double: -1e9999)pb") + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(R"pb(optional_double: -inf)pb")))); + EXPECT_THAT( + Testee("DoubleFieldOverflowNegativeInfinity") + .ParseText(message(), R"pb(optional_double: -1e9999)pb") + .SerializeText(), + Yields(WhenParsed(EqualsTextProto(R"pb(optional_double: -inf)pb")))); +} + +// TODO: Write the plain-string inputs below as R"pb(...)pb" raw +// strings once the migration's request goldens are gone. The goldens pin the +// exact request bytes, and clang-format would reformat the whitespace of these +// inputs inside a pb raw string. + +// Exponent is one more than uint64 max. +TEST_P(TextDoubleTest, OverflowInfinityHugeExponent) { + EXPECT_THAT( + Testee("DoubleFieldOverflowInfinityHugeExponent") + .ParseText(message(), "optional_double: 1e18446744073709551616") + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(R"pb(optional_double: inf)pb")))); + EXPECT_THAT( + Testee("DoubleFieldOverflowInfinityHugeExponent") + .ParseText(message(), "optional_double: 1e18446744073709551616") + .SerializeText(), + Yields(WhenParsed(EqualsTextProto(R"pb(optional_double: inf)pb")))); +} + +TEST_P(TextDoubleTest, LargeNegativeExponentParsesAsZero) { + EXPECT_THAT( + Testee("DoubleFieldLargeNegativeExponentParsesAsZero") + .ParseText(message(), "optional_double: 1e-18446744073709551616") + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto("")))); + EXPECT_THAT( + Testee("DoubleFieldLargeNegativeExponentParsesAsZero") + .ParseText(message(), "optional_double: 1e-18446744073709551616") + .SerializeText(), + Yields(WhenParsed(EqualsTextProto("")))); +} + +TEST_P(TextDoubleTest, NegativeLargeNegativeExponentParsesAsNegativeZero) { + EXPECT_THAT( + Testee("NegDoubleFieldLargeNegativeExponentParsesAsNegZero") + .ParseText(message(), + R"pb(optional_double: -1e-18446744073709551616)pb") + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(R"pb(optional_double: -0)pb")))); + EXPECT_THAT( + Testee("NegDoubleFieldLargeNegativeExponentParsesAsNegZero") + .ParseText(message(), + R"pb(optional_double: -1e-18446744073709551616)pb") + .SerializeText(), + Yields(WhenParsed(EqualsTextProto(R"pb(optional_double: -0)pb")))); +} + +INSTANTIATE_TEST_SUITE_P(All, TextDoubleTest, + ValuesIn(Proto3TestMessageTypes()), + MessageTypeParamName); + +} // namespace +} // namespace conformance +} // namespace protobuf +} // namespace google
diff --git a/conformance/text_format_conformance_suite.cc b/conformance/text_format_conformance_suite.cc deleted file mode 100644 index 44e1728..0000000 --- a/conformance/text_format_conformance_suite.cc +++ /dev/null
@@ -1,991 +0,0 @@ -// Protocol Buffers - Google's data interchange format -// Copyright 2008 Google Inc. All rights reserved. -// -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file or at -// https://developers.google.com/open-source/licenses/bsd - -#include "conformance/text_format_conformance_suite.h" - -#include <cstddef> -#include <string> -#include <utility> -#include <vector> - -#include "absl/log/absl_log.h" -#include "absl/log/die_if_null.h" -#include "absl/strings/str_cat.h" -#include "absl/strings/str_format.h" -#include "conformance/conformance_test.h" -#include "conformance/test_protos/test_messages_edition2023.pb.h" -#include "conformance/test_protos/test_messages_edition_unstable.pb.h" -#include "editions/golden/test_messages_proto2_editions.pb.h" -#include "editions/golden/test_messages_proto3_editions.pb.h" -#include "google/protobuf/test_messages_proto2.pb.h" -#include "google/protobuf/test_messages_proto3.pb.h" -#include "google/protobuf/text_format.h" - -using conformance::ConformanceRequest; -using conformance::ConformanceResponse; -using conformance::TestStatus; -using conformance::WireFormat; -using protobuf_test_messages::editions::TestAllTypesEdition2023; -using protobuf_test_messages::proto2::TestAllTypesProto2; -using protobuf_test_messages::proto2::UnknownToTestAllTypes; -using protobuf_test_messages::proto3::TestAllTypesProto3; -using TestAllTypesProto2Editions = - protobuf_test_messages::editions::proto2::TestAllTypesProto2; -using TestAllTypesProto3Editions = - protobuf_test_messages::editions::proto3::TestAllTypesProto3; - -namespace google { -namespace protobuf { -namespace conformance { - -// The number of repetitions to use for performance tests. -// Corresponds approx to 500KB wireformat bytes. -static const size_t kPerformanceRepeatCount = 50000; - -TextFormatConformanceTestSuite::TextFormatConformanceTestSuite() { - SetFailureListFlagName("--text_format_failure_list"); -} - -bool TextFormatConformanceTestSuite::ParseTextFormatResponse( - const ConformanceResponse& response, - const ConformanceRequestSetting& setting, Message* test_message) { - TextFormat::Parser parser; - const ConformanceRequest& request = setting.GetRequest(); - if (request.print_unknown_fields()) { - parser.AllowFieldNumber(true); - } - if (!parser.ParseFromString(response.text_payload(), test_message)) { - ABSL_LOG(ERROR) << "INTERNAL ERROR: internal text->protobuf transcode " - << "yielded unparseable proto. Text payload: " - << response.text_payload(); - return false; - } - - return true; -} - -bool TextFormatConformanceTestSuite::ParseResponse( - const ConformanceResponse& response, - const ConformanceRequestSetting& setting, Message* test_message) { - const ConformanceRequest& request = setting.GetRequest(); - WireFormat requested_output = request.requested_output_format(); - const std::string& test_name = setting.GetTestName(); - ConformanceLevel level = setting.GetLevel(); - - TestStatus test; - test.set_name(test_name); - switch (response.result_case()) { - case ConformanceResponse::kProtobufPayload: { - if (requested_output != ::conformance::PROTOBUF) { - test.set_failure_message(absl::StrCat( - "Test was asked for ", WireFormatToString(requested_output), - " output but provided PROTOBUF instead.")); - ReportFailure(test, level, request, response); - return false; - } - - if (!test_message->ParseFromString(response.protobuf_payload())) { - test.set_failure_message( - "Protobuf output we received from test was unparseable."); - ReportFailure(test, level, request, response); - return false; - } - - break; - } - - case ConformanceResponse::kTextPayload: { - if (requested_output != ::conformance::TEXT_FORMAT) { - test.set_failure_message(absl::StrCat( - "Test was asked for ", WireFormatToString(requested_output), - " output but provided TEXT_FORMAT instead.")); - ReportFailure(test, level, request, response); - return false; - } - - if (!ParseTextFormatResponse(response, setting, test_message)) { - test.set_failure_message( - "TEXT_FORMAT output we received from test was unparseable."); - ReportFailure(test, level, request, response); - return false; - } - - break; - } - - default: - ABSL_LOG(FATAL) << test_name - << ": unknown payload type: " << response.result_case(); - } - - return true; -} - -void TextFormatConformanceTestSuite::RunSuiteImpl() { - TextFormatConformanceTestSuiteImpl<TestAllTypesProto2>(this); - TextFormatConformanceTestSuiteImpl<TestAllTypesProto3>(this); - if (maximum_edition_ >= Edition::EDITION_2023) { - TextFormatConformanceTestSuiteImpl<TestAllTypesProto2Editions>(this); - TextFormatConformanceTestSuiteImpl<TestAllTypesProto3Editions>(this); - TextFormatConformanceTestSuiteImpl<TestAllTypesEdition2023>(this); - } -} - -template <typename MessageType> -TextFormatConformanceTestSuiteImpl<MessageType>:: - TextFormatConformanceTestSuiteImpl(TextFormatConformanceTestSuite* suite) - : suite_(*ABSL_DIE_IF_NULL(suite)) { - // Flag control performance tests to keep them internal and opt-in only - if (suite_.performance_) { - if (MessageType::GetDescriptor()->name() == "TestAllTypesEdition2023") { - // There are no editions-sensitive performance tests. - return; - } - if (MessageType::GetDescriptor()->name() == "TestAllTypesEditionUnstable") { - // There are no editions-sensitive performance tests. - return; - } - RunTextFormatPerformanceTests(); - } else { - if (MessageType::GetDescriptor()->name() == "TestAllTypesProto2") { - RunGroupTests(); - RunClosedEnumTests(); - } - if (MessageType::GetDescriptor()->name() == "TestAllTypesEdition2023") { - RunDelimitedTests(); - } - if (MessageType::GetDescriptor()->name() == "TestAllTypesProto3") { - RunAnyTests(); - RunOpenEnumTests(); - // TODO Run these over proto2 also. - RunAllTests(); - } - } -} - -template <typename MessageType> -void TextFormatConformanceTestSuiteImpl<MessageType>::ExpectParseFailure( - const std::string& test_name, ConformanceLevel level, - const std::string& input) { - MessageType prototype; - // We don't expect output, but if the program erroneously accepts the protobuf - // we let it send its response as this. We must not leave it unspecified. - ConformanceRequestSetting setting( - level, ::conformance::TEXT_FORMAT, ::conformance::TEXT_FORMAT, - ::conformance::TEXT_FORMAT_TEST, prototype, test_name, input); - const ConformanceRequest& request = setting.GetRequest(); - ConformanceResponse response; - std::string effective_test_name = absl::StrCat( - setting.ConformanceLevelToString(level), ".", - setting.GetSyntaxIdentifier(), ".TextFormatInput.", test_name); - - if (!suite_.RunTest(effective_test_name, request, &response)) { - return; - } - - TestStatus test; - test.set_name(effective_test_name); - if (response.result_case() == ConformanceResponse::kParseError) { - suite_.ReportSuccess(test); - } else if (response.result_case() == ConformanceResponse::kSkipped) { - suite_.ReportSkip(test, request, response); - } else if (response.result_case() == ConformanceResponse::kRuntimeError) { - test.set_failure_message( - "Should have failed to parse, but raised an error instead."); - suite_.ReportFailure(test, level, request, response); - } else { - test.set_failure_message("Should have failed to parse, but didn't."); - suite_.ReportFailure(test, level, request, response); - } -} - -template <typename MessageType> -void TextFormatConformanceTestSuiteImpl<MessageType>::RunValidTextFormatTest( - const std::string& test_name, ConformanceLevel level, - const std::string& input_text) { - MessageType prototype; - RunValidTextFormatTestWithMessage(test_name, level, input_text, prototype); -} - -template <typename MessageType> -void TextFormatConformanceTestSuiteImpl<MessageType>:: - RunValidTextFormatTestWithMessage(const std::string& test_name, - ConformanceLevel level, - const std::string& input_text, - const Message& message) { - ConformanceRequestSetting setting1( - level, ::conformance::TEXT_FORMAT, ::conformance::PROTOBUF, - ::conformance::TEXT_FORMAT_TEST, message, test_name, input_text); - suite_.RunValidInputTest(setting1, input_text); - ConformanceRequestSetting setting2( - level, ::conformance::TEXT_FORMAT, ::conformance::TEXT_FORMAT, - ::conformance::TEXT_FORMAT_TEST, message, test_name, input_text); - suite_.RunValidInputTest(setting2, input_text); -} - -template <typename MessageType> -void TextFormatConformanceTestSuiteImpl<MessageType>:: - RunValidTextFormatTestWithExpected(const std::string& test_name, - ConformanceLevel level, - const std::string& input_text, - const std::string& expected_text) { - MessageType prototype; - ConformanceRequestSetting setting1( - level, ::conformance::TEXT_FORMAT, ::conformance::PROTOBUF, - ::conformance::TEXT_FORMAT_TEST, prototype, test_name, input_text); - suite_.RunValidInputTest(setting1, expected_text); - ConformanceRequestSetting setting2( - level, ::conformance::TEXT_FORMAT, ::conformance::TEXT_FORMAT, - ::conformance::TEXT_FORMAT_TEST, prototype, test_name, input_text); - suite_.RunValidInputTest(setting2, expected_text); -} - -template <typename MessageType> -void TextFormatConformanceTestSuiteImpl< - MessageType>::RunValidUnknownTextFormatTest(const std::string& test_name, - const Message& message) { - std::string serialized_input; - // TODO: Remove this suppression. - (void)message.SerializeToString(&serialized_input); - MessageType prototype; - ConformanceRequestSetting setting1( - RECOMMENDED, ::conformance::PROTOBUF, ::conformance::TEXT_FORMAT, - ::conformance::TEXT_FORMAT_TEST, prototype, - absl::StrCat(test_name, "_Drop"), serialized_input); - setting1.SetPrototypeMessageForCompare(message); - suite_.RunValidBinaryInputTest(setting1, ""); - - ConformanceRequestSetting setting2( - RECOMMENDED, ::conformance::PROTOBUF, ::conformance::TEXT_FORMAT, - ::conformance::TEXT_FORMAT_TEST, prototype, - absl::StrCat(test_name, "_Print"), serialized_input); - setting2.SetPrototypeMessageForCompare(message); - setting2.SetPrintUnknownFields(true); - suite_.RunValidBinaryInputTest(setting2, serialized_input); -} - -template <typename MessageType> -void TextFormatConformanceTestSuiteImpl<MessageType>::RunDelimitedTests() { - RunValidTextFormatTest("GroupFieldNoColon", REQUIRED, - "GroupLikeType { group_int32: 1 }"); - RunValidTextFormatTest("GroupFieldWithColon", REQUIRED, - "GroupLikeType: { group_int32: 1 }"); - RunValidTextFormatTest("GroupFieldEmpty", REQUIRED, "GroupLikeType {}"); - RunValidTextFormatTest( - "GroupFieldExtension", REQUIRED, - "[protobuf_test_messages.editions.groupliketype] { c: 1 }"); - RunValidTextFormatTest( - "DelimitedFieldExtension", REQUIRED, - "[protobuf_test_messages.editions.delimited_ext] { c: 1 }"); - - - // Test that lower-cased group name (i.e. implicit field name) are accepted. - RunValidTextFormatTest("DelimitedFieldLowercased", REQUIRED, - "groupliketype { group_int32: 1 }"); - RunValidTextFormatTest("DelimitedFieldLowercasedDifferent", REQUIRED, - "delimited_field { group_int32: 1 }"); - - // Extensions always used the field name, and should never accept the message - // name. - ExpectParseFailure( - "DelimitedFieldExtensionMessageName", REQUIRED, - "[protobuf_test_messages.editions.GroupLikeType] { group_int32: 1 }"); - - // Extension names can contain whitespace and comments. - RunValidTextFormatTest( - "ExtensionNameWithWhitespace", REQUIRED, - "[protobuf _test_messages.edit\tions.exten\nsion_int32]: 1"); - RunValidTextFormatTest( - "ExtensionNameWithComment", REQUIRED, - "[protobuf_test_messages.edit # comment \nions.extension_int32]: 1"); -} - -template <typename MessageType> -void TextFormatConformanceTestSuiteImpl<MessageType>::RunGroupTests() { - RunValidTextFormatTest("GroupFieldNoColon", REQUIRED, - "Data { group_int32: 1 }"); - RunValidTextFormatTest("GroupFieldWithColon", REQUIRED, - "Data: { group_int32: 1 }"); - RunValidTextFormatTest("GroupFieldEmpty", REQUIRED, "Data {}"); - RunValidTextFormatTest("GroupFieldMultiWord", REQUIRED, - "MultiWordGroupField { group_int32: 1 }"); - - // Test that lower-cased group name (i.e. implicit field name) is accepted - RunValidTextFormatTest("GroupFieldLowercased", REQUIRED, - "data { group_int32: 1 }"); - RunValidTextFormatTest("GroupFieldLowercasedMultiWord", REQUIRED, - "multiwordgroupfield { group_int32: 1 }"); - - // Test extensions of group type - RunValidTextFormatTest("GroupFieldExtension", REQUIRED, - absl::StrFormat("[%s] { group_int32: 1 }", - MessageType::GetDescriptor() - ->file() - ->FindExtensionByName("groupfield") - ->PrintableNameForExtension())); - ExpectParseFailure("GroupFieldExtensionGroupName", REQUIRED, - absl::StrFormat("[%s] { group_int32: 1 }", - MessageType::GetDescriptor() - ->file() - ->FindMessageTypeByName("GroupField") - ->full_name())); -} - -template <typename MessageType> -void TextFormatConformanceTestSuiteImpl<MessageType>::RunAllTests() { - RunValidTextFormatTest("HelloWorld", REQUIRED, - "optional_string: 'Hello, World!'"); - // Integer fields. - RunValidTextFormatTest("Int32FieldMaxValue", REQUIRED, - "optional_int32: 2147483647"); - RunValidTextFormatTest("Int32FieldMinValue", REQUIRED, - "optional_int32: -2147483648"); - RunValidTextFormatTest("Uint32FieldMaxValue", REQUIRED, - "optional_uint32: 4294967295"); - RunValidTextFormatTest("Int64FieldMaxValue", REQUIRED, - "optional_int64: 9223372036854775807"); - RunValidTextFormatTest("Int64FieldMinValue", REQUIRED, - "optional_int64: -9223372036854775808"); - RunValidTextFormatTest("Uint64FieldMaxValue", REQUIRED, - "optional_uint64: 18446744073709551615"); - // Integer fields - Hex - RunValidTextFormatTestWithExpected("Int32FieldMaxValueHex", REQUIRED, - "optional_int32: 0x7FFFFFFF", - "optional_int32: 2147483647"); - RunValidTextFormatTestWithExpected("Int32FieldMinValueHex", REQUIRED, - "optional_int32: -0x80000000", - "optional_int32: -2147483648"); - RunValidTextFormatTestWithExpected("Uint32FieldMaxValueHex", REQUIRED, - "optional_uint32: 0xFFFFFFFF", - "optional_uint32: 4294967295"); - RunValidTextFormatTestWithExpected("Int64FieldMaxValueHex", REQUIRED, - "optional_int64: 0x7FFFFFFFFFFFFFFF", - "optional_int64: 9223372036854775807"); - RunValidTextFormatTestWithExpected("Int64FieldMinValueHex", REQUIRED, - "optional_int64: -0x8000000000000000", - "optional_int64: -9223372036854775808"); - RunValidTextFormatTestWithExpected("Uint64FieldMaxValueHex", REQUIRED, - "optional_uint64: 0xFFFFFFFFFFFFFFFF", - "optional_uint64: 18446744073709551615"); - // Integer fields - Octal - RunValidTextFormatTestWithExpected("Int32FieldMaxValueOctal", REQUIRED, - "optional_int32: 017777777777", - "optional_int32: 2147483647"); - RunValidTextFormatTestWithExpected("Int32FieldMinValueOctal", REQUIRED, - "optional_int32: -020000000000", - "optional_int32: -2147483648"); - RunValidTextFormatTestWithExpected("Uint32FieldMaxValueOctal", REQUIRED, - "optional_uint32: 037777777777", - "optional_uint32: 4294967295"); - RunValidTextFormatTestWithExpected("Int64FieldMaxValueOctal", REQUIRED, - "optional_int64: 0777777777777777777777", - "optional_int64: 9223372036854775807"); - RunValidTextFormatTestWithExpected("Int64FieldMinValueOctal", REQUIRED, - "optional_int64: -01000000000000000000000", - "optional_int64: -9223372036854775808"); - RunValidTextFormatTestWithExpected("Uint64FieldMaxValueOctal", REQUIRED, - "optional_uint64: 01777777777777777777777", - "optional_uint64: 18446744073709551615"); - - // Parsers reject out-of-bound integer values. - ExpectParseFailure("Int32FieldTooLarge", REQUIRED, - "optional_int32: 2147483648"); - ExpectParseFailure("Int32FieldTooSmall", REQUIRED, - "optional_int32: -2147483649"); - ExpectParseFailure("Uint32FieldTooLarge", REQUIRED, - "optional_uint32: 4294967296"); - ExpectParseFailure("Int64FieldTooLarge", REQUIRED, - "optional_int64: 9223372036854775808"); - ExpectParseFailure("Int64FieldTooSmall", REQUIRED, - "optional_int64: -9223372036854775809"); - ExpectParseFailure("Uint64FieldTooLarge", REQUIRED, - "optional_uint64: 18446744073709551616"); - // Parsers reject out-of-bound integer values - Hex - ExpectParseFailure("Int32FieldTooLargeHex", REQUIRED, - "optional_int32: 0x80000000"); - ExpectParseFailure("Int32FieldTooSmallHex", REQUIRED, - "optional_int32: -0x80000001"); - ExpectParseFailure("Uint32FieldTooLargeHex", REQUIRED, - "optional_uint32: 0x100000000"); - ExpectParseFailure("Int64FieldTooLargeHex", REQUIRED, - "optional_int64: 0x8000000000000000"); - ExpectParseFailure("Int64FieldTooSmallHex", REQUIRED, - "optional_int64: -0x8000000000000001"); - ExpectParseFailure("Uint64FieldTooLargeHex", REQUIRED, - "optional_uint64: 0x10000000000000000"); - // Parsers reject out-of-bound integer values - Octal - ExpectParseFailure("Int32FieldTooLargeOctal", REQUIRED, - "optional_int32: 020000000000"); - ExpectParseFailure("Int32FieldTooSmallOctal", REQUIRED, - "optional_int32: -020000000001"); - ExpectParseFailure("Uint32FieldTooLargeOctal", REQUIRED, - "optional_uint32: 040000000000"); - ExpectParseFailure("Int64FieldTooLargeOctal", REQUIRED, - "optional_int64: 01000000000000000000000"); - ExpectParseFailure("Int64FieldTooSmallOctal", REQUIRED, - "optional_int64: -01000000000000000000001"); - ExpectParseFailure("Uint64FieldTooLargeOctal", REQUIRED, - "optional_uint64: 02000000000000000000000"); - - // Floating point fields - for (const auto& suffix : std::vector<std::string>{"", "f", "F"}) { - const std::string name_suffix = - suffix.empty() ? "" : absl::StrCat("_", suffix); - - RunValidTextFormatTest(absl::StrCat("FloatField", name_suffix), REQUIRED, - absl::StrCat("optional_float: 3.192837", suffix)); - RunValidTextFormatTestWithExpected( - absl::StrCat("FloatFieldZero", name_suffix), REQUIRED, - absl::StrCat("optional_float: 0", suffix), - "" /* implicit presence, so zero means unset*/); - RunValidTextFormatTest(absl::StrCat("FloatFieldNegative", name_suffix), - REQUIRED, - absl::StrCat("optional_float: -3.192837", suffix)); - RunValidTextFormatTest( - absl::StrCat("FloatFieldWithVeryPreciseNumber", name_suffix), REQUIRED, - absl::StrCat("optional_float: 3.123456789123456789", suffix)); - RunValidTextFormatTest( - absl::StrCat("FloatFieldMaxValue", name_suffix), REQUIRED, - absl::StrCat("optional_float: 3.4028235e+38", suffix)); - RunValidTextFormatTest(absl::StrCat("FloatFieldMinValue", name_suffix), - REQUIRED, - absl::StrCat("optional_float: 1.17549e-38", suffix)); - RunValidTextFormatTest(absl::StrCat("FloatFieldWithInt32Max", name_suffix), - REQUIRED, - absl::StrCat("optional_float: 4294967296", suffix)); - RunValidTextFormatTest( - absl::StrCat("FloatFieldLargerThanInt64", name_suffix), REQUIRED, - absl::StrCat("optional_float: 9223372036854775808", suffix)); - RunValidTextFormatTest( - absl::StrCat("FloatFieldTooLarge", name_suffix), REQUIRED, - absl::StrCat("optional_float: 3.4028235e+39", suffix)); - RunValidTextFormatTest(absl::StrCat("FloatFieldTooSmall", name_suffix), - REQUIRED, - absl::StrCat("optional_float: 1.17549e-39", suffix)); - RunValidTextFormatTest( - absl::StrCat("FloatFieldLargerThanUint64", name_suffix), REQUIRED, - absl::StrCat("optional_float: 18446744073709551616", suffix)); - // https://protobuf.dev/reference/protobuf/textformat-spec/#literals says - // "-0" is a valid float literal. -0 should be considered not the same as 0 - // when considering implicit presence, and so should round trip. - RunValidTextFormatTest(absl::StrCat("FloatFieldNegativeZero", name_suffix), - REQUIRED, - absl::StrCat("optional_float: -0", suffix)); - // https://protobuf.dev/reference/protobuf/textformat-spec/#literals says - // ".123", "-.123", ".123e2" are a valid float literal. - RunValidTextFormatTest(absl::StrCat("FloatFieldNoLeadingZero", name_suffix), - REQUIRED, - absl::StrCat("optional_float: .123", suffix)); - RunValidTextFormatTest( - absl::StrCat("FloatFieldNegativeNoLeadingZero", name_suffix), REQUIRED, - absl::StrCat("optional_float: -.123", suffix)); - RunValidTextFormatTest( - absl::StrCat("FloatFieldNoLeadingZeroWithExponent", name_suffix), - REQUIRED, absl::StrCat("optional_float: .123e2", suffix)); - } - // https://protobuf.dev/reference/protobuf/textformat-spec/#value say case - // doesn't matter for special values, test a few - for (const auto& value : std::vector<std::string>{"nan", "NaN", "nAn"}) { - RunValidTextFormatTest(absl::StrCat("FloatFieldValue_", value), REQUIRED, - absl::StrCat("optional_float: ", value)); - } - for (const auto& value : std::vector<std::string>{ - "inf", "infinity", "INF", "INFINITY", "iNF", "inFINITY"}) { - RunValidTextFormatTest(absl::StrCat("FloatFieldValue_Pos", value), REQUIRED, - absl::StrCat("optional_float: ", value)); - RunValidTextFormatTest(absl::StrCat("FloatFieldValue_Neg", value), REQUIRED, - absl::StrCat("optional_float: -", value)); - } - // https://protobuf.dev/reference/protobuf/textformat-spec/#numeric and - // https://protobuf.dev/reference/protobuf/textformat-spec/#value says - // hex or octal float literals are invalid. - ExpectParseFailure("FloatFieldNoHex", REQUIRED, "optional_float: 0x1"); - ExpectParseFailure("FloatFieldNoNegativeHex", REQUIRED, - "optional_float: -0x1"); - ExpectParseFailure("FloatFieldNoOctal", REQUIRED, "optional_float: 012"); - ExpectParseFailure("FloatFieldNoNegativeOctal", REQUIRED, - "optional_float: -012"); - // https://protobuf.dev/reference/protobuf/textformat-spec/#value says - // overflows are mapped to infinity/-infinity. - RunValidTextFormatTestWithExpected("FloatFieldOverflowInfinity", REQUIRED, - "optional_float: 1e50", - "optional_float: inf"); - RunValidTextFormatTestWithExpected("FloatFieldOverflowNegativeInfinity", - REQUIRED, "optional_float: -1e50", - "optional_float: -inf"); - RunValidTextFormatTestWithExpected("DoubleFieldOverflowInfinity", REQUIRED, - "optional_double: 1e9999", - "optional_double: inf"); - RunValidTextFormatTestWithExpected("DoubleFieldOverflowNegativeInfinity", - REQUIRED, "optional_double: -1e9999", - "optional_double: -inf"); - // Exponent is one more than uint64 max. - RunValidTextFormatTestWithExpected( - "FloatFieldOverflowInfinityHugeExponent", REQUIRED, - "optional_float: 1e18446744073709551616", "optional_float: inf"); - RunValidTextFormatTestWithExpected( - "DoubleFieldOverflowInfinityHugeExponent", REQUIRED, - "optional_double: 1e18446744073709551616", "optional_double: inf"); - RunValidTextFormatTestWithExpected( - "DoubleFieldLargeNegativeExponentParsesAsZero", REQUIRED, - "optional_double: 1e-18446744073709551616", ""); - RunValidTextFormatTestWithExpected( - "NegDoubleFieldLargeNegativeExponentParsesAsNegZero", REQUIRED, - "optional_double: -1e-18446744073709551616", "optional_double: -0"); - - RunValidTextFormatTestWithExpected( - "FloatFieldLargeNegativeExponentParsesAsZero", REQUIRED, - "optional_float: 1e-50", ""); - RunValidTextFormatTestWithExpected( - "NegFloatFieldLargeNegativeExponentParsesAsNegZero", REQUIRED, - "optional_float: -1e-50", "optional_float: -0"); - - // String literals x {Strings, Bytes} - for (const auto& field_type : std::vector<std::string>{"String", "Bytes"}) { - const std::string field_name = - field_type == "String" ? "optional_string" : "optional_bytes"; - RunValidTextFormatTest( - absl::StrCat("StringLiteralConcat", field_type), REQUIRED, - absl::StrCat(field_name, ": 'first' \"second\"\n'third'")); - RunValidTextFormatTest( - absl::StrCat("StringLiteralBasicEscapes", field_type), REQUIRED, - absl::StrCat(field_name, ": '\\a\\b\\f\\n\\r\\t\\v\\?\\\\\\'\\\"'")); - RunValidTextFormatTest( - absl::StrCat("StringLiteralOctalEscapes", field_type), REQUIRED, - absl::StrCat(field_name, ": '\\341\\210\\264'")); - RunValidTextFormatTest(absl::StrCat("StringLiteralHexEscapes", field_type), - REQUIRED, - absl::StrCat(field_name, ": '\\xe1\\x88\\xb4'")); - RunValidTextFormatTest( - absl::StrCat("StringLiteralShortUnicodeEscape", field_type), - RECOMMENDED, absl::StrCat(field_name, ": '\\u1234'")); - RunValidTextFormatTest( - absl::StrCat("StringLiteralLongUnicodeEscapes", field_type), - RECOMMENDED, absl::StrCat(field_name, ": '\\U00001234\\U00010437'")); - // String literals don't include line feeds. - ExpectParseFailure(absl::StrCat("StringLiteralIncludesLF", field_type), - REQUIRED, - absl::StrCat(field_name, ": 'first line\nsecond line'")); - // Unicode escapes don't include code points that lie beyond the planes - // (> 0x10ffff). - ExpectParseFailure( - absl::StrCat("StringLiteralLongUnicodeEscapeTooLarge", field_type), - REQUIRED, absl::StrCat(field_name, ": '\\U00110000'")); - // Unicode escapes don't include surrogates. - ExpectParseFailure( - absl::StrCat("StringLiteralShortUnicodeEscapeSurrogatePair", - field_type), - RECOMMENDED, absl::StrCat(field_name, ": '\\ud801\\udc37'")); - ExpectParseFailure( - absl::StrCat("StringLiteralShortUnicodeEscapeSurrogateFirstOnly", - field_type), - RECOMMENDED, absl::StrCat(field_name, ": '\\ud800'")); - ExpectParseFailure( - absl::StrCat("StringLiteralShortUnicodeEscapeSurrogateSecondOnly", - field_type), - RECOMMENDED, absl::StrCat(field_name, ": '\\udc00'")); - ExpectParseFailure( - absl::StrCat("StringLiteralLongUnicodeEscapeSurrogateFirstOnly", - field_type), - RECOMMENDED, absl::StrCat(field_name, ": '\\U0000d800'")); - ExpectParseFailure( - absl::StrCat("StringLiteralLongUnicodeEscapeSurrogateSecondOnly", - field_type), - RECOMMENDED, absl::StrCat(field_name, ": '\\U0000dc00'")); - ExpectParseFailure( - absl::StrCat("StringLiteralLongUnicodeEscapeSurrogatePair", field_type), - RECOMMENDED, absl::StrCat(field_name, ": '\\U0000d801\\U00000dc37'")); - ExpectParseFailure( - absl::StrCat("StringLiteralUnicodeEscapeSurrogatePairLongShort", - field_type), - RECOMMENDED, absl::StrCat(field_name, ": '\\U0000d801\\udc37'")); - ExpectParseFailure( - absl::StrCat("StringLiteralUnicodeEscapeSurrogatePairShortLong", - field_type), - RECOMMENDED, absl::StrCat(field_name, ": '\\ud801\\U0000dc37'")); - - // The following method depend on the type of field, as strings have extra - // validation. - const auto test_method = - field_type == "String" - ? &TextFormatConformanceTestSuiteImpl::ExpectParseFailure - : &TextFormatConformanceTestSuiteImpl::RunValidTextFormatTest; - - // String fields reject invalid UTF-8 byte sequences; bytes fields don't. - (this->*test_method)(absl::StrCat(field_type, "FieldBadUTF8Octal"), - REQUIRED, absl::StrCat(field_name, ": '\\300'")); - (this->*test_method)(absl::StrCat(field_type, "FieldBadUTF8Hex"), REQUIRED, - absl::StrCat(field_name, ": '\\xc0'")); - } - - // Separators - for (const auto& test_case : std::vector<std::pair<std::string, std::string>>{ - {"string", "\"abc\""}, - {"bytes", "\"abc\""}, - {"int32", "123"}, - {"bool", "true"}, - {"double", "1.23"}, - {"fixed32", "0x123"}, - }) { - // Optional Field Separators - for (const auto& field_type : - std::vector<std::string>{"Single", "Repeated"}) { - std::string field_name, field_value; - if (field_type == "Single") { - field_name = absl::StrCat("optional_", test_case.first); - field_value = test_case.second; - } else { - field_name = absl::StrCat("repeated_", test_case.first); - field_value = absl::StrCat("[", test_case.second, "]"); - } - - RunValidTextFormatTest(absl::StrCat("FieldSeparatorCommaTopLevel", - field_type, "_", test_case.first), - REQUIRED, - absl::StrCat(field_name, ": ", field_value, ",")); - RunValidTextFormatTest(absl::StrCat("FieldSeparatorSemiTopLevelSingle", - field_type, "_", test_case.first), - REQUIRED, - absl::StrCat(field_name, ": ", field_value, ";")); - - ExpectParseFailure( - absl::StrCat("FieldSeparatorCommaTopLevelDuplicatesFails", field_type, - "_", test_case.first), - REQUIRED, absl::StrCat(field_name, ": ", field_value, ",,")); - ExpectParseFailure( - absl::StrCat("FieldSeparatorSemiTopLevelDuplicateFails", field_type, - "_", test_case.first), - REQUIRED, absl::StrCat(field_name, ": ", field_value, ";;")); - } - - // Required List Separators - RunValidTextFormatTest( - absl::StrCat("ListSeparator_", test_case.first), REQUIRED, - absl::StrCat("repeated_", test_case.first, ": [", test_case.second, ",", - test_case.second, "]")); - ExpectParseFailure( - absl::StrCat("ListSeparatorSemiFails_", test_case.first), REQUIRED, - absl::StrCat("repeated_", test_case.first, ": [", test_case.second, ";", - test_case.second, "]")); - // For string and bytes, if we skip the separator, the parser will treat - // the two values as a single value. - if (test_case.first == "string" || test_case.first == "bytes") { - RunValidTextFormatTest( - absl::StrCat("ListSeparatorMissingIsOneValue_", test_case.first), - REQUIRED, - absl::StrCat("repeated_", test_case.first, ": [", test_case.second, - " ", test_case.second, "]")); - } else { - ExpectParseFailure( - absl::StrCat("ListSeparatorMissingFails_", test_case.first), REQUIRED, - absl::StrCat("repeated_", test_case.first, ": [", test_case.second, - " ", test_case.second, "]")); - } - ExpectParseFailure( - absl::StrCat("ListSeparatorDuplicateFails_", test_case.first), REQUIRED, - absl::StrCat("repeated_", test_case.first, ": [", test_case.second, - ",,", test_case.second, "]")); - ExpectParseFailure( - absl::StrCat("ListSeparatorSingleTrailingFails_", test_case.first), - REQUIRED, - absl::StrCat("repeated_", test_case.first, ": [", test_case.second, - ",]")); - ExpectParseFailure( - absl::StrCat("ListSeparatorTwoValuesTrailingFails_", test_case.first), - REQUIRED, - absl::StrCat("repeated_", test_case.first, ": [", test_case.second, ",", - test_case.second, ",]")); - } - // The test message don't really have all types nested, so just check one - // data type for the nested field separator support - RunValidTextFormatTest("FieldSeparatorCommaNested", REQUIRED, - "optional_nested_message: { a: 123, }"); - RunValidTextFormatTest("FieldSeparatorSemiNested", REQUIRED, - "optional_nested_message: { a: 123; }"); - ExpectParseFailure("FieldSeparatorCommaNestedDuplicates", REQUIRED, - "optional_nested_message: { a: 123,, }"); - ExpectParseFailure("FieldSeparatorSemiNestedDuplicates", REQUIRED, - "optional_nested_message: { a: 123;; }"); - - // Unknown Fields - UnknownToTestAllTypes message; - // Unable to print unknown Fixed32/Fixed64 fields as if they are known. - // Fixed32/Fixed64 fields are not added in the tests. - message.set_optional_int32(123); - message.set_optional_string("hello"); - message.set_optional_bool(true); - RunValidUnknownTextFormatTest("ScalarUnknownFields", message); - - message.Clear(); - message.mutable_nested_message()->set_c(111); - RunValidUnknownTextFormatTest("MessageUnknownFields", message); - - message.Clear(); - message.mutable_optionalgroup()->set_a(321); - RunValidUnknownTextFormatTest("GroupUnknownFields", message); - - message.add_repeated_int32(1); - message.add_repeated_int32(2); - message.add_repeated_int32(3); - RunValidUnknownTextFormatTest("RepeatedUnknownFields", message); - - // Reserved field names - for (const auto& test_case : std::vector<std::pair<std::string, std::string>>{ - {"Boolean", "true"}, - {"Integer", "-123"}, - {"Float", "0.123"}, - {"Enum", "ENUM_VALUE"}, - {"String", "\"hello\""}, - {"Message", "{ a: 123 }"}, - {"MessageAngleBrackets", "< a: 123 >"}, - {"RepeatedInteger", "[-123, 456]"}, - {"RepeatedFloat", "[0.123, 1e-10] "}, - {"RepeatedString", R"pb(["hello", "world"])pb"}, - }) { - RunValidTextFormatTest(absl::StrCat("ReservedFieldName.", test_case.first), - REQUIRED, - absl::StrCat("reserved_field: ", test_case.second)); - // TODO: Add a test for reserved field numbers on 999999. - } - - // Map fields - MessageType prototype; - (*prototype.mutable_map_string_string())["c"] = "value"; - (*prototype.mutable_map_string_string())["b"] = "value"; - (*prototype.mutable_map_string_string())["a"] = "value"; - RunValidTextFormatTestWithMessage("AlphabeticallySortedMapStringKeys", - REQUIRED, - R"( - map_string_string { - key: "a" - value: "value" - } - map_string_string { - key: "b" - value: "value" - } - map_string_string { - key: "c" - value: "value" - } - )", - prototype); - - prototype.Clear(); - (*prototype.mutable_map_int32_int32())[3] = 0; - (*prototype.mutable_map_int32_int32())[2] = 0; - (*prototype.mutable_map_int32_int32())[1] = 0; - RunValidTextFormatTestWithMessage("AlphabeticallySortedMapIntKeys", REQUIRED, - R"( - map_int32_int32 { - key: 1 - value: 0 - } - map_int32_int32 { - key: 2 - value: 0 - } - map_int32_int32 { - key: 3 - value: 0 - } - )", - prototype); - - prototype.Clear(); - (*prototype.mutable_map_bool_bool())[true] = false; - (*prototype.mutable_map_bool_bool())[false] = false; - RunValidTextFormatTestWithMessage("AlphabeticallySortedMapBoolKeys", REQUIRED, - R"( - map_bool_bool { - key: false - value: false - } - map_bool_bool { - key: true - value: false - } - )", - prototype); - - prototype.Clear(); - ConformanceRequestSetting setting_map( - REQUIRED, ::conformance::TEXT_FORMAT, ::conformance::PROTOBUF, - ::conformance::TEXT_FORMAT_TEST, prototype, "DuplicateMapKey", R"( - map_string_nested_message { - key: "duplicate" - value: { a: 123 } - } - map_string_nested_message { - key: "duplicate" - value: { corecursive: {} } - } - )"); - // The last-specified value will be retained in a parsed map - suite_.RunValidInputTest(setting_map, R"( - map_string_nested_message { - key: "duplicate" - value: { corecursive: {} } - } - )"); -} - -template <typename MessageType> -void TextFormatConformanceTestSuiteImpl<MessageType>::RunAnyTests() { - // Any fields - RunValidTextFormatTest("AnyField", REQUIRED, - R"( - optional_any: { - [type.googleapis.com/protobuf_test_messages.proto3.TestAllTypesProto3] - { optional_int32: 12345 - } - } - )"); - RunValidTextFormatTest("AnyFieldWithRawBytes", REQUIRED, - R"( - optional_any: { - type_url: - "type.googleapis.com/protobuf_test_messages.proto3.TestAllTypesProto3" value: - "\b\271`" - } - )"); - ExpectParseFailure("AnyFieldWithInvalidType", REQUIRED, - R"( - optional_any: { - [type.googleapis.com/unknown] { - optional_int32: 12345 - } - } - )"); - RunValidTextFormatTest("AnyFieldWithCustomTypeUrlPrefix", REQUIRED, - R"( - optional_any: { - [non.default.domain/protobuf_test_messages.proto3.TestAllTypesProto3] - { - optional_int32: 12345 - } - } - )"); - RunValidTextFormatTest("AnyFieldWithSpecialCharactersInTypeUrlPrefix", - REQUIRED, - R"( - optional_any: { - [non.default.domain/sub-path_0/~!$&()*+,;=/protobuf_test_messages.proto3.TestAllTypesProto3] - { - optional_int32: 12345 - } - } - )"); - RunValidTextFormatTest("AnyFieldWithValidUrlPercentEscapeInTypeUrlPrefix", - REQUIRED, - R"( - optional_any: { - [non.default.domain/%2F/protobuf_test_messages.proto3.TestAllTypesProto3] - { - optional_int32: 12345 - } - } - )"); - ExpectParseFailure("AnyFieldWithInvalidUrlPercentEscapeInTypeUrlPrefix", - REQUIRED, - R"( - optional_any: { - [non.default.domain/%ZZ/protobuf_test_messages.proto3.TestAllTypesProto3] - { - optional_int32: 12345 - } - } - )"); - RunValidTextFormatTest( - "AnyFieldWithWhitespaceInTypeUrl", REQUIRED, - "optional_any: {\n" - " [ ty pe.go\nogleap\tis.com/\n" - " proto buf_te\nst_messages.proto3.Test\tAllTypesProto3 ]\n" - " {\n" - " optional_int32: 12345\n" - " }\n" - "}"); - RunValidTextFormatTest( - "AnyFieldWithCommentsInTypeUrl", REQUIRED, - "optional_any: {\n" - " [type.google # comment \napis.com/" - "protobuf_test_messages.proto3.Test # comment \nAllTypesProto3]" - " {\n" - " optional_int32: 12345\n" - " }\n" - "}"); -} - -template <typename MessageType> -void TextFormatConformanceTestSuiteImpl< - MessageType>::RunTextFormatPerformanceTests() { - TestTextFormatPerformanceMergeMessageWithRepeatedField("Bool", - "repeated_bool: true"); - TestTextFormatPerformanceMergeMessageWithRepeatedField( - "Double", "repeated_double: 123"); - TestTextFormatPerformanceMergeMessageWithRepeatedField( - "Int32", "repeated_uint32: 123"); - TestTextFormatPerformanceMergeMessageWithRepeatedField( - "Int64", "repeated_uint64: 123"); - TestTextFormatPerformanceMergeMessageWithRepeatedField( - "String", R"(repeated_string: "foo")"); - TestTextFormatPerformanceMergeMessageWithRepeatedField( - "Bytes", R"(repeated_bytes: "foo")"); -} - -// This is currently considered valid input by some languages but not others -template <typename MessageType> -void TextFormatConformanceTestSuiteImpl<MessageType>:: - TestTextFormatPerformanceMergeMessageWithRepeatedField( - const std::string& test_type_name, const std::string& message_field) { - std::string recursive_message = - absl::StrCat("recursive_message { ", message_field, " }"); - - std::string input; - for (size_t i = 0; i < kPerformanceRepeatCount; i++) { - absl::StrAppend(&input, recursive_message); - } - - std::string expected = "recursive_message { "; - for (size_t i = 0; i < kPerformanceRepeatCount; i++) { - absl::StrAppend(&expected, message_field, " "); - } - absl::StrAppend(&expected, "}"); - - RunValidTextFormatTestWithExpected( - absl::StrCat("TestTextFormatPerformanceMergeMessageWithRepeatedField", - test_type_name), - RECOMMENDED, input, expected); -} - -template <typename MessageType> -void TextFormatConformanceTestSuiteImpl<MessageType>::RunOpenEnumTests() { - RunValidTextFormatTest("ClosedEnumFieldByNumber", REQUIRED, - R"( - optional_nested_enum: 1 - )"); - RunValidTextFormatTest("ClosedEnumFieldWithUnknownNumber", REQUIRED, - R"( - optional_nested_enum: 42 - )"); -} - -template <typename MessageType> -void TextFormatConformanceTestSuiteImpl<MessageType>::RunClosedEnumTests() { - RunValidTextFormatTest("ClosedEnumFieldByNumber", REQUIRED, - R"( - optional_nested_enum: 1 - )"); - ExpectParseFailure("ClosedEnumFieldWithUnknownNumber", REQUIRED, - R"( - optional_nested_enum: 42 - )"); -} - -} // namespace conformance -} // namespace protobuf -} // namespace google
diff --git a/conformance/text_format_conformance_suite.h b/conformance/text_format_conformance_suite.h deleted file mode 100644 index dd5cffc..0000000 --- a/conformance/text_format_conformance_suite.h +++ /dev/null
@@ -1,84 +0,0 @@ -// Protocol Buffers - Google's data interchange format -// Copyright 2008 Google Inc. All rights reserved. -// -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file or at -// https://developers.google.com/open-source/licenses/bsd - -#ifndef GOOGLE_PROTOBUF_CONFORMANCE_TEXT_FORMAT_CONFORMANCE_SUITE_H__ -#define GOOGLE_PROTOBUF_CONFORMANCE_TEXT_FORMAT_CONFORMANCE_SUITE_H__ - -#include <string> - -#include "conformance/conformance_test.h" -#include "google/protobuf/message.h" - -namespace google { -namespace protobuf { -namespace conformance { - -class TextFormatConformanceTestSuite : public ConformanceTestSuite { - public: - TextFormatConformanceTestSuite(); - - private: - void RunSuiteImpl() override; - - bool ParseTextFormatResponse( - const ::conformance::ConformanceResponse& response, - const ConformanceRequestSetting& setting, Message* test_message); - bool ParseResponse(const ::conformance::ConformanceResponse& response, - const ConformanceRequestSetting& setting, - Message* test_message) override; - - template <typename MessageType> - friend class TextFormatConformanceTestSuiteImpl; -}; - -template <typename MessageType> -class TextFormatConformanceTestSuiteImpl { - public: - explicit TextFormatConformanceTestSuiteImpl( - TextFormatConformanceTestSuite* suite); - - private: - using ConformanceRequestSetting = - TextFormatConformanceTestSuite::ConformanceRequestSetting; - using ConformanceLevel = TextFormatConformanceTestSuite::ConformanceLevel; - constexpr static ConformanceLevel RECOMMENDED = ConformanceLevel::RECOMMENDED; - constexpr static ConformanceLevel REQUIRED = ConformanceLevel::REQUIRED; - - void RunAllTests(); - - void RunDelimitedTests(); - void RunGroupTests(); - void RunAnyTests(); - void RunOpenEnumTests(); - void RunClosedEnumTests(); - - void RunTextFormatPerformanceTests(); - void RunValidTextFormatTest(const std::string& test_name, - ConformanceLevel level, const std::string& input); - void RunValidTextFormatTestWithExpected(const std::string& test_name, - ConformanceLevel level, - const std::string& input_text, - const std::string& expected_text); - void RunValidUnknownTextFormatTest(const std::string& test_name, - const Message& message); - void RunValidTextFormatTestWithMessage(const std::string& test_name, - ConformanceLevel level, - const std::string& input_text, - const Message& message); - void ExpectParseFailure(const std::string& test_name, ConformanceLevel level, - const std::string& input); - void TestTextFormatPerformanceMergeMessageWithRepeatedField( - const std::string& test_type_name, const std::string& message_field); - - TextFormatConformanceTestSuite& suite_; -}; - -} // namespace conformance -} // namespace protobuf -} // namespace google - -#endif // GOOGLE_PROTOBUF_CONFORMANCE_TEXT_FORMAT_CONFORMANCE_SUITE_H__
diff --git a/conformance/text_format_failure_list_java.txt b/conformance/text_format_failure_list_java.txt index 80dcee2..485591f 100644 --- a/conformance/text_format_failure_list_java.txt +++ b/conformance/text_format_failure_list_java.txt
@@ -1,7 +1,7 @@ -Recommended.*.ProtobufInput.GroupUnknownFields_Drop.TextFormatOutput # TEXT_FORMAT output we received from test was unparseable. -Recommended.*.ProtobufInput.MessageUnknownFields_Drop.TextFormatOutput # TEXT_FORMAT output we received from test was unparseable. -Recommended.*.ProtobufInput.RepeatedUnknownFields_Drop.TextFormatOutput # TEXT_FORMAT output we received from test was unparseable. -Recommended.*.ProtobufInput.ScalarUnknownFields_Drop.TextFormatOutput # TEXT_FORMAT output we received from test was unparseable. +Recommended.*.ProtobufInput.GroupUnknownFields_Drop.TextFormatOutput # Output was not equivalent to reference message: added: optionalgroup: { a: 321 } +Recommended.*.ProtobufInput.MessageUnknownFields_Drop.TextFormatOutput # Output was not equivalent to reference message: added: nested_message: { c: 111 } +Recommended.*.ProtobufInput.RepeatedUnknownFields_Drop.TextFormatOutput # Output was not equivalent to reference message: added: optionalgroup: { a: 321 }added: repeated_int32[0]: 1added: repeated_int32 +Recommended.*.ProtobufInput.ScalarUnknownFields_Drop.TextFormatOutput # Output was not equivalent to reference message: added: optional_int32: 123added: optional_string: "hello"added: optional_bool: t Required.*.TextFormatInput.AnyField.ProtobufOutput # Failed to parse input or produce output. Required.*.TextFormatInput.AnyField.TextFormatOutput # Failed to parse input or produce output. Required.*.TextFormatInput.FloatFieldNoNegativeOctal # Should have failed to parse, but didn't.
diff --git a/conformance/text_group_test.cc b/conformance/text_group_test.cc new file mode 100644 index 0000000..dea3cae --- /dev/null +++ b/conformance/text_group_test.cc
@@ -0,0 +1,330 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2026 Google LLC. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +// Text-format conformance tests for group fields (proto2-style message types) +// and for their editions successor, delimited-encoded message fields +// (TestAllTypesEdition2023). This replaces the legacy +// TextFormatConformanceTestSuiteImpl<M>::RunGroupTests() and +// RunDelimitedTests(); the test names and the requests sent to the testee are +// identical to the legacy ones. + +#include <string> + +#include <gmock/gmock.h> +#include <gtest/gtest.h> +#include "absl/strings/str_cat.h" +#include "conformance/binary_test_util.h" +#include "conformance/matchers.h" +#include "conformance/message_type_fixtures.h" +#include "conformance/testee.h" +#include "google/protobuf/descriptor.h" + +namespace google { +namespace protobuf { +namespace conformance { +namespace { + +using ::testing::ValuesIn; + +// Group fields. In the test message types, "Data" is a group field with an +// int32 field group_int32, "MultiWordGroupField" is a group with the same +// field, and "groupfield" is an extension of group type GroupField. +using TextGroupTest = MessageTypeConformanceTest; + +TEST_P(TextGroupTest, NoColon) { + EXPECT_THAT( + Testee("GroupFieldNoColon") + .ParseText(message(), R"pb(Data { group_int32: 1 })pb") + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(R"pb(Data { group_int32: 1 })pb")))); + EXPECT_THAT( + Testee("GroupFieldNoColon") + .ParseText(message(), R"pb(Data { group_int32: 1 })pb") + .SerializeText(), + Yields(WhenParsed(EqualsTextProto(R"pb(Data { group_int32: 1 })pb")))); +} + +TEST_P(TextGroupTest, WithColon) { + EXPECT_THAT( + Testee("GroupFieldWithColon") + .ParseText(message(), R"pb(Data: { group_int32: 1 })pb") + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(R"pb(Data: { group_int32: 1 })pb")))); + EXPECT_THAT( + Testee("GroupFieldWithColon") + .ParseText(message(), R"pb(Data: { group_int32: 1 })pb") + .SerializeText(), + Yields(WhenParsed(EqualsTextProto(R"pb(Data: { group_int32: 1 })pb")))); +} + +TEST_P(TextGroupTest, Empty) { + EXPECT_THAT(Testee("GroupFieldEmpty") + .ParseText(message(), R"pb(Data {})pb") + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(R"pb(Data {})pb")))); + EXPECT_THAT(Testee("GroupFieldEmpty") + .ParseText(message(), R"pb(Data {})pb") + .SerializeText(), + Yields(WhenParsed(EqualsTextProto(R"pb(Data {})pb")))); +} + +TEST_P(TextGroupTest, MultiWord) { + EXPECT_THAT( + Testee("GroupFieldMultiWord") + .ParseText(message(), R"pb(MultiWordGroupField { group_int32: 1 })pb") + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(R"pb( + MultiWordGroupField { group_int32: 1 } + )pb")))); + EXPECT_THAT( + Testee("GroupFieldMultiWord") + .ParseText(message(), R"pb(MultiWordGroupField { group_int32: 1 })pb") + .SerializeText(), + Yields(WhenParsed(EqualsTextProto(R"pb( + MultiWordGroupField { group_int32: 1 } + )pb")))); +} + +// The lower-cased group name (i.e. the implicit field name) is accepted. +TEST_P(TextGroupTest, Lowercased) { + EXPECT_THAT( + Testee("GroupFieldLowercased") + .ParseText(message(), R"pb(data { group_int32: 1 })pb") + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(R"pb(data { group_int32: 1 })pb")))); + EXPECT_THAT( + Testee("GroupFieldLowercased") + .ParseText(message(), R"pb(data { group_int32: 1 })pb") + .SerializeText(), + Yields(WhenParsed(EqualsTextProto(R"pb(data { group_int32: 1 })pb")))); +} + +TEST_P(TextGroupTest, LowercasedMultiWord) { + EXPECT_THAT( + Testee("GroupFieldLowercasedMultiWord") + .ParseText(message(), R"pb(multiwordgroupfield { group_int32: 1 })pb") + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(R"pb( + multiwordgroupfield { group_int32: 1 } + )pb")))); + EXPECT_THAT( + Testee("GroupFieldLowercasedMultiWord") + .ParseText(message(), R"pb(multiwordgroupfield { group_int32: 1 })pb") + .SerializeText(), + Yields(WhenParsed(EqualsTextProto(R"pb( + multiwordgroupfield { group_int32: 1 } + )pb")))); +} + +// An extension of group type is named by the extension field. +TEST_P(TextGroupTest, Extension) { + const FieldDescriptor* extension = + message()->file()->FindExtensionByName("groupfield"); + ASSERT_NE(extension, nullptr); + const std::string input = absl::StrCat( + "[", extension->PrintableNameForExtension(), "] { group_int32: 1 }"); + EXPECT_THAT(Testee("GroupFieldExtension") + .ParseText(message(), input) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(input)))); + EXPECT_THAT( + Testee("GroupFieldExtension").ParseText(message(), input).SerializeText(), + Yields(WhenParsed(EqualsTextProto(input)))); +} + +// ... and never by the name of its group message type. +TEST_P(TextGroupTest, ExtensionGroupName) { + const Descriptor* group = + message()->file()->FindMessageTypeByName("GroupField"); + ASSERT_NE(group, nullptr); + EXPECT_THAT(Testee("GroupFieldExtensionGroupName") + .ParseText(message(), absl::StrCat("[", group->full_name(), + "] { group_int32: 1 }")) + .ParseOnly(), + Yields(IsParseError())); +} + +INSTANTIATE_TEST_SUITE_P(All, TextGroupTest, ValuesIn(Proto2TestMessageTypes()), + MessageTypeParamName); + +// Delimited-encoded message fields of TestAllTypesEdition2023 (message()): +// groupliketype and delimited_field are fields of the nested message type +// GroupLikeType (whose group-like name may stand in for the field name), and +// the groupliketype and delimited_ext extensions are of the top-level +// GroupLikeType. The fixture skips itself when --maximum_edition doesn't +// cover editions. +using TextDelimitedTest = Edition2023ConformanceTest; + +TEST_F(TextDelimitedTest, GroupFieldNoColon) { + EXPECT_THAT( + Testee("GroupFieldNoColon") + .ParseText(message(), R"pb(GroupLikeType { group_int32: 1 })pb") + .SerializeBinary(), + Yields(WhenParsed( + EqualsTextProto(R"pb(GroupLikeType { group_int32: 1 })pb")))); + EXPECT_THAT( + Testee("GroupFieldNoColon") + .ParseText(message(), R"pb(GroupLikeType { group_int32: 1 })pb") + .SerializeText(), + Yields(WhenParsed( + EqualsTextProto(R"pb(GroupLikeType { group_int32: 1 })pb")))); +} + +TEST_F(TextDelimitedTest, GroupFieldWithColon) { + EXPECT_THAT( + Testee("GroupFieldWithColon") + .ParseText(message(), R"pb(GroupLikeType: { group_int32: 1 })pb") + .SerializeBinary(), + Yields(WhenParsed( + EqualsTextProto(R"pb(GroupLikeType: { group_int32: 1 })pb")))); + EXPECT_THAT( + Testee("GroupFieldWithColon") + .ParseText(message(), R"pb(GroupLikeType: { group_int32: 1 })pb") + .SerializeText(), + Yields(WhenParsed( + EqualsTextProto(R"pb(GroupLikeType: { group_int32: 1 })pb")))); +} + +TEST_F(TextDelimitedTest, GroupFieldEmpty) { + EXPECT_THAT(Testee("GroupFieldEmpty") + .ParseText(message(), R"pb(GroupLikeType {})pb") + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(R"pb(GroupLikeType {})pb")))); + EXPECT_THAT(Testee("GroupFieldEmpty") + .ParseText(message(), R"pb(GroupLikeType {})pb") + .SerializeText(), + Yields(WhenParsed(EqualsTextProto(R"pb(GroupLikeType {})pb")))); +} + +TEST_F(TextDelimitedTest, GroupFieldExtension) { + EXPECT_THAT( + Testee("GroupFieldExtension") + .ParseText( + message(), + R"pb([protobuf_test_messages.editions.groupliketype] { c: 1 })pb") + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto( + R"pb([protobuf_test_messages.editions.groupliketype] { c: 1 })pb")))); + EXPECT_THAT( + Testee("GroupFieldExtension") + .ParseText( + message(), + R"pb([protobuf_test_messages.editions.groupliketype] { c: 1 })pb") + .SerializeText(), + Yields(WhenParsed(EqualsTextProto( + R"pb([protobuf_test_messages.editions.groupliketype] { c: 1 })pb")))); +} + +TEST_F(TextDelimitedTest, DelimitedFieldExtension) { + EXPECT_THAT( + Testee("DelimitedFieldExtension") + .ParseText( + message(), + R"pb([protobuf_test_messages.editions.delimited_ext] { c: 1 })pb") + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto( + R"pb([protobuf_test_messages.editions.delimited_ext] { c: 1 })pb")))); + EXPECT_THAT( + Testee("DelimitedFieldExtension") + .ParseText( + message(), + R"pb([protobuf_test_messages.editions.delimited_ext] { c: 1 })pb") + .SerializeText(), + Yields(WhenParsed(EqualsTextProto( + R"pb([protobuf_test_messages.editions.delimited_ext] { c: 1 })pb")))); +} + + +// The lower-cased group name (i.e. the implicit field name) is accepted. +TEST_F(TextDelimitedTest, DelimitedFieldLowercased) { + EXPECT_THAT( + Testee("DelimitedFieldLowercased") + .ParseText(message(), R"pb(groupliketype { group_int32: 1 })pb") + .SerializeBinary(), + Yields(WhenParsed( + EqualsTextProto(R"pb(groupliketype { group_int32: 1 })pb")))); + EXPECT_THAT( + Testee("DelimitedFieldLowercased") + .ParseText(message(), R"pb(groupliketype { group_int32: 1 })pb") + .SerializeText(), + Yields(WhenParsed( + EqualsTextProto(R"pb(groupliketype { group_int32: 1 })pb")))); +} + +TEST_F(TextDelimitedTest, DelimitedFieldLowercasedDifferent) { + EXPECT_THAT( + Testee("DelimitedFieldLowercasedDifferent") + .ParseText(message(), R"pb(delimited_field { group_int32: 1 })pb") + .SerializeBinary(), + Yields(WhenParsed( + EqualsTextProto(R"pb(delimited_field { group_int32: 1 })pb")))); + EXPECT_THAT( + Testee("DelimitedFieldLowercasedDifferent") + .ParseText(message(), R"pb(delimited_field { group_int32: 1 })pb") + .SerializeText(), + Yields(WhenParsed( + EqualsTextProto(R"pb(delimited_field { group_int32: 1 })pb")))); +} + +// TODO: Write the plain-string inputs below as R"pb(...)pb" raw +// strings once the migration's request goldens are gone. The goldens pin the +// exact request bytes, and clang-format would reformat the whitespace of these +// inputs inside a pb raw string. + +// Extensions always use the field name, and never accept the message name. +TEST_F(TextDelimitedTest, DelimitedFieldExtensionMessageName) { + EXPECT_THAT(Testee("DelimitedFieldExtensionMessageName") + .ParseText(message(), + "[protobuf_test_messages.editions.GroupLikeType] " + "{ group_int32: 1 }") + .ParseOnly(), + Yields(IsParseError())); +} + +// Extension names can contain whitespace and comments. +TEST_F(TextDelimitedTest, ExtensionNameWithWhitespace) { + EXPECT_THAT( + Testee("ExtensionNameWithWhitespace") + .ParseText( + message(), + "[protobuf _test_messages.edit\tions.exten\nsion_int32]: 1") + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto( + "[protobuf _test_messages.edit\tions.exten\nsion_int32]: 1")))); + EXPECT_THAT( + Testee("ExtensionNameWithWhitespace") + .ParseText( + message(), + "[protobuf _test_messages.edit\tions.exten\nsion_int32]: 1") + .SerializeText(), + Yields(WhenParsed(EqualsTextProto( + "[protobuf _test_messages.edit\tions.exten\nsion_int32]: 1")))); +} + +TEST_F(TextDelimitedTest, ExtensionNameWithComment) { + EXPECT_THAT(Testee("ExtensionNameWithComment") + .ParseText(message(), + "[protobuf_test_messages.edit # comment " + "\nions.extension_int32]: 1") + .SerializeBinary(), + Yields(WhenParsed( + EqualsTextProto("[protobuf_test_messages.edit # comment " + "\nions.extension_int32]: 1")))); + EXPECT_THAT(Testee("ExtensionNameWithComment") + .ParseText(message(), + "[protobuf_test_messages.edit # comment " + "\nions.extension_int32]: 1") + .SerializeText(), + Yields(WhenParsed( + EqualsTextProto("[protobuf_test_messages.edit # comment " + "\nions.extension_int32]: 1")))); +} + +} // namespace +} // namespace conformance +} // namespace protobuf +} // namespace google
diff --git a/conformance/text_hello_world_test.cc b/conformance/text_hello_world_test.cc new file mode 100644 index 0000000..d812355 --- /dev/null +++ b/conformance/text_hello_world_test.cc
@@ -0,0 +1,58 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2026 Google LLC. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +// The smoke test of the text-format conformance suite: a single string field +// round-trips through the testee. This replaces the legacy +// TextFormatConformanceTestSuiteImpl<M>::RunAllTests()'s "HelloWorld" test, +// which ran for the proto3-style message types only; the test names and the +// requests sent to the testee are identical to the legacy ones. +// +// Like the legacy RunValidTextFormatTest(), every valid-input test here and in +// the other text_*_test.cc files has two legs, in the legacy order: the input +// is parsed and serialized as binary ("<name>.ProtobufOutput"), then parsed +// and serialized as text format ("<name>.TextFormatOutput"), and each result +// must be equivalent to the expected message. + +#include <gmock/gmock.h> +#include <gtest/gtest.h> +#include "conformance/binary_test_util.h" +#include "conformance/matchers.h" +#include "conformance/message_type_fixtures.h" +#include "conformance/testee.h" + +namespace google { +namespace protobuf { +namespace conformance { +namespace { + +using ::testing::ValuesIn; + +using TextHelloWorldTest = MessageTypeConformanceTest; + +TEST_P(TextHelloWorldTest, HelloWorld) { + EXPECT_THAT( + Testee("HelloWorld") + .ParseText(message(), R"pb(optional_string: 'Hello, World!')pb") + .SerializeBinary(), + Yields(WhenParsed( + EqualsTextProto(R"pb(optional_string: 'Hello, World!')pb")))); + EXPECT_THAT( + Testee("HelloWorld") + .ParseText(message(), R"pb(optional_string: 'Hello, World!')pb") + .SerializeText(), + Yields(WhenParsed( + EqualsTextProto(R"pb(optional_string: 'Hello, World!')pb")))); +} + +INSTANTIATE_TEST_SUITE_P(All, TextHelloWorldTest, + ValuesIn(Proto3TestMessageTypes()), + MessageTypeParamName); + +} // namespace +} // namespace conformance +} // namespace protobuf +} // namespace google
diff --git a/conformance/text_integer_test.cc b/conformance/text_integer_test.cc new file mode 100644 index 0000000..140f583 --- /dev/null +++ b/conformance/text_integer_test.cc
@@ -0,0 +1,431 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2026 Google LLC. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +// Text-format conformance tests for integer fields: the extreme values of +// each integer type in decimal, hex and octal, and rejection of values just +// past them. This replaces the integer blocks of the legacy +// TextFormatConformanceTestSuiteImpl<M>::RunAllTests(); the test names and +// the requests sent to the testee are identical to the legacy ones. +// +// The legacy suite ran RunAllTests() only for TestAllTypesProto3, which its +// RunSuiteImpl() instantiated in its proto3 and editions flavours, so these +// run over Proto3TestMessageTypes() (like HelloWorld). + +#include <gmock/gmock.h> +#include <gtest/gtest.h> +#include "conformance/binary_test_util.h" +#include "conformance/matchers.h" +#include "conformance/message_type_fixtures.h" +#include "conformance/testee.h" + +namespace google { +namespace protobuf { +namespace conformance { +namespace { + +using ::testing::ValuesIn; + +using TextIntegerTest = MessageTypeConformanceTest; + +// Extreme values in decimal. +TEST_P(TextIntegerTest, Int32FieldMaxValue) { + EXPECT_THAT( + Testee("Int32FieldMaxValue") + .ParseText(message(), R"pb(optional_int32: 2147483647)pb") + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(R"pb(optional_int32: 2147483647)pb")))); + EXPECT_THAT( + Testee("Int32FieldMaxValue") + .ParseText(message(), R"pb(optional_int32: 2147483647)pb") + .SerializeText(), + Yields(WhenParsed(EqualsTextProto(R"pb(optional_int32: 2147483647)pb")))); +} + +TEST_P(TextIntegerTest, Int32FieldMinValue) { + EXPECT_THAT(Testee("Int32FieldMinValue") + .ParseText(message(), R"pb(optional_int32: -2147483648)pb") + .SerializeBinary(), + Yields(WhenParsed( + EqualsTextProto(R"pb(optional_int32: -2147483648)pb")))); + EXPECT_THAT(Testee("Int32FieldMinValue") + .ParseText(message(), R"pb(optional_int32: -2147483648)pb") + .SerializeText(), + Yields(WhenParsed( + EqualsTextProto(R"pb(optional_int32: -2147483648)pb")))); +} + +TEST_P(TextIntegerTest, Uint32FieldMaxValue) { + EXPECT_THAT(Testee("Uint32FieldMaxValue") + .ParseText(message(), R"pb(optional_uint32: 4294967295)pb") + .SerializeBinary(), + Yields(WhenParsed( + EqualsTextProto(R"pb(optional_uint32: 4294967295)pb")))); + EXPECT_THAT(Testee("Uint32FieldMaxValue") + .ParseText(message(), R"pb(optional_uint32: 4294967295)pb") + .SerializeText(), + Yields(WhenParsed( + EqualsTextProto(R"pb(optional_uint32: 4294967295)pb")))); +} + +TEST_P(TextIntegerTest, Int64FieldMaxValue) { + EXPECT_THAT( + Testee("Int64FieldMaxValue") + .ParseText(message(), R"pb(optional_int64: 9223372036854775807)pb") + .SerializeBinary(), + Yields(WhenParsed( + EqualsTextProto(R"pb(optional_int64: 9223372036854775807)pb")))); + EXPECT_THAT( + Testee("Int64FieldMaxValue") + .ParseText(message(), R"pb(optional_int64: 9223372036854775807)pb") + .SerializeText(), + Yields(WhenParsed( + EqualsTextProto(R"pb(optional_int64: 9223372036854775807)pb")))); +} + +// TODO: Write the plain-string inputs below as R"pb(...)pb" raw +// strings once the migration's request goldens are gone. The goldens pin the +// exact request bytes, and clang-format would reformat the whitespace of these +// inputs inside a pb raw string. +TEST_P(TextIntegerTest, Int64FieldMinValue) { + EXPECT_THAT(Testee("Int64FieldMinValue") + .ParseText(message(), "optional_int64: -9223372036854775808") + .SerializeBinary(), + Yields(WhenParsed( + EqualsTextProto("optional_int64: -9223372036854775808")))); + EXPECT_THAT(Testee("Int64FieldMinValue") + .ParseText(message(), "optional_int64: -9223372036854775808") + .SerializeText(), + Yields(WhenParsed( + EqualsTextProto("optional_int64: -9223372036854775808")))); +} + +TEST_P(TextIntegerTest, Uint64FieldMaxValue) { + EXPECT_THAT(Testee("Uint64FieldMaxValue") + .ParseText(message(), "optional_uint64: 18446744073709551615") + .SerializeBinary(), + Yields(WhenParsed( + EqualsTextProto("optional_uint64: 18446744073709551615")))); + EXPECT_THAT(Testee("Uint64FieldMaxValue") + .ParseText(message(), "optional_uint64: 18446744073709551615") + .SerializeText(), + Yields(WhenParsed( + EqualsTextProto("optional_uint64: 18446744073709551615")))); +} + +// Extreme values in hex; the responses are compared to the decimal form. +TEST_P(TextIntegerTest, Int32FieldMaxValueHex) { + EXPECT_THAT( + Testee("Int32FieldMaxValueHex") + .ParseText(message(), R"pb(optional_int32: 0x7FFFFFFF)pb") + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(R"pb(optional_int32: 2147483647)pb")))); + EXPECT_THAT( + Testee("Int32FieldMaxValueHex") + .ParseText(message(), R"pb(optional_int32: 0x7FFFFFFF)pb") + .SerializeText(), + Yields(WhenParsed(EqualsTextProto(R"pb(optional_int32: 2147483647)pb")))); +} + +TEST_P(TextIntegerTest, Int32FieldMinValueHex) { + EXPECT_THAT(Testee("Int32FieldMinValueHex") + .ParseText(message(), R"pb(optional_int32: -0x80000000)pb") + .SerializeBinary(), + Yields(WhenParsed( + EqualsTextProto(R"pb(optional_int32: -2147483648)pb")))); + EXPECT_THAT(Testee("Int32FieldMinValueHex") + .ParseText(message(), R"pb(optional_int32: -0x80000000)pb") + .SerializeText(), + Yields(WhenParsed( + EqualsTextProto(R"pb(optional_int32: -2147483648)pb")))); +} + +TEST_P(TextIntegerTest, Uint32FieldMaxValueHex) { + EXPECT_THAT(Testee("Uint32FieldMaxValueHex") + .ParseText(message(), R"pb(optional_uint32: 0xFFFFFFFF)pb") + .SerializeBinary(), + Yields(WhenParsed( + EqualsTextProto(R"pb(optional_uint32: 4294967295)pb")))); + EXPECT_THAT(Testee("Uint32FieldMaxValueHex") + .ParseText(message(), R"pb(optional_uint32: 0xFFFFFFFF)pb") + .SerializeText(), + Yields(WhenParsed( + EqualsTextProto(R"pb(optional_uint32: 4294967295)pb")))); +} + +TEST_P(TextIntegerTest, Int64FieldMaxValueHex) { + EXPECT_THAT( + Testee("Int64FieldMaxValueHex") + .ParseText(message(), R"pb(optional_int64: 0x7FFFFFFFFFFFFFFF)pb") + .SerializeBinary(), + Yields(WhenParsed( + EqualsTextProto(R"pb(optional_int64: 9223372036854775807)pb")))); + EXPECT_THAT( + Testee("Int64FieldMaxValueHex") + .ParseText(message(), R"pb(optional_int64: 0x7FFFFFFFFFFFFFFF)pb") + .SerializeText(), + Yields(WhenParsed( + EqualsTextProto(R"pb(optional_int64: 9223372036854775807)pb")))); +} + +TEST_P(TextIntegerTest, Int64FieldMinValueHex) { + EXPECT_THAT( + Testee("Int64FieldMinValueHex") + .ParseText(message(), R"pb(optional_int64: -0x8000000000000000)pb") + .SerializeBinary(), + Yields( + WhenParsed(EqualsTextProto("optional_int64: -9223372036854775808")))); + EXPECT_THAT( + Testee("Int64FieldMinValueHex") + .ParseText(message(), R"pb(optional_int64: -0x8000000000000000)pb") + .SerializeText(), + Yields( + WhenParsed(EqualsTextProto("optional_int64: -9223372036854775808")))); +} + +TEST_P(TextIntegerTest, Uint64FieldMaxValueHex) { + EXPECT_THAT( + Testee("Uint64FieldMaxValueHex") + .ParseText(message(), R"pb(optional_uint64: 0xFFFFFFFFFFFFFFFF)pb") + .SerializeBinary(), + Yields(WhenParsed( + EqualsTextProto("optional_uint64: 18446744073709551615")))); + EXPECT_THAT( + Testee("Uint64FieldMaxValueHex") + .ParseText(message(), R"pb(optional_uint64: 0xFFFFFFFFFFFFFFFF)pb") + .SerializeText(), + Yields(WhenParsed( + EqualsTextProto("optional_uint64: 18446744073709551615")))); +} + +// Extreme values in octal. +TEST_P(TextIntegerTest, Int32FieldMaxValueOctal) { + EXPECT_THAT( + Testee("Int32FieldMaxValueOctal") + .ParseText(message(), R"pb(optional_int32: 017777777777)pb") + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(R"pb(optional_int32: 2147483647)pb")))); + EXPECT_THAT( + Testee("Int32FieldMaxValueOctal") + .ParseText(message(), R"pb(optional_int32: 017777777777)pb") + .SerializeText(), + Yields(WhenParsed(EqualsTextProto(R"pb(optional_int32: 2147483647)pb")))); +} + +TEST_P(TextIntegerTest, Int32FieldMinValueOctal) { + EXPECT_THAT(Testee("Int32FieldMinValueOctal") + .ParseText(message(), R"pb(optional_int32: -020000000000)pb") + .SerializeBinary(), + Yields(WhenParsed( + EqualsTextProto(R"pb(optional_int32: -2147483648)pb")))); + EXPECT_THAT(Testee("Int32FieldMinValueOctal") + .ParseText(message(), R"pb(optional_int32: -020000000000)pb") + .SerializeText(), + Yields(WhenParsed( + EqualsTextProto(R"pb(optional_int32: -2147483648)pb")))); +} + +TEST_P(TextIntegerTest, Uint32FieldMaxValueOctal) { + EXPECT_THAT(Testee("Uint32FieldMaxValueOctal") + .ParseText(message(), R"pb(optional_uint32: 037777777777)pb") + .SerializeBinary(), + Yields(WhenParsed( + EqualsTextProto(R"pb(optional_uint32: 4294967295)pb")))); + EXPECT_THAT(Testee("Uint32FieldMaxValueOctal") + .ParseText(message(), R"pb(optional_uint32: 037777777777)pb") + .SerializeText(), + Yields(WhenParsed( + EqualsTextProto(R"pb(optional_uint32: 4294967295)pb")))); +} + +TEST_P(TextIntegerTest, Int64FieldMaxValueOctal) { + EXPECT_THAT( + Testee("Int64FieldMaxValueOctal") + .ParseText(message(), R"pb(optional_int64: 0777777777777777777777)pb") + .SerializeBinary(), + Yields(WhenParsed( + EqualsTextProto(R"pb(optional_int64: 9223372036854775807)pb")))); + EXPECT_THAT( + Testee("Int64FieldMaxValueOctal") + .ParseText(message(), R"pb(optional_int64: 0777777777777777777777)pb") + .SerializeText(), + Yields(WhenParsed( + EqualsTextProto(R"pb(optional_int64: 9223372036854775807)pb")))); +} + +TEST_P(TextIntegerTest, Int64FieldMinValueOctal) { + EXPECT_THAT( + Testee("Int64FieldMinValueOctal") + .ParseText(message(), "optional_int64: -01000000000000000000000") + .SerializeBinary(), + Yields( + WhenParsed(EqualsTextProto("optional_int64: -9223372036854775808")))); + EXPECT_THAT( + Testee("Int64FieldMinValueOctal") + .ParseText(message(), "optional_int64: -01000000000000000000000") + .SerializeText(), + Yields( + WhenParsed(EqualsTextProto("optional_int64: -9223372036854775808")))); +} + +TEST_P(TextIntegerTest, Uint64FieldMaxValueOctal) { + EXPECT_THAT( + Testee("Uint64FieldMaxValueOctal") + .ParseText(message(), "optional_uint64: 01777777777777777777777") + .SerializeBinary(), + Yields(WhenParsed( + EqualsTextProto("optional_uint64: 18446744073709551615")))); + EXPECT_THAT( + Testee("Uint64FieldMaxValueOctal") + .ParseText(message(), "optional_uint64: 01777777777777777777777") + .SerializeText(), + Yields(WhenParsed( + EqualsTextProto("optional_uint64: 18446744073709551615")))); +} + +// Parsers reject out-of-bound integer values. +TEST_P(TextIntegerTest, Int32FieldTooLarge) { + EXPECT_THAT(Testee("Int32FieldTooLarge") + .ParseText(message(), R"pb(optional_int32: 2147483648)pb") + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(TextIntegerTest, Int32FieldTooSmall) { + EXPECT_THAT(Testee("Int32FieldTooSmall") + .ParseText(message(), R"pb(optional_int32: -2147483649)pb") + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(TextIntegerTest, Uint32FieldTooLarge) { + EXPECT_THAT(Testee("Uint32FieldTooLarge") + .ParseText(message(), R"pb(optional_uint32: 4294967296)pb") + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(TextIntegerTest, Int64FieldTooLarge) { + EXPECT_THAT( + Testee("Int64FieldTooLarge") + .ParseText(message(), R"pb(optional_int64: 9223372036854775808)pb") + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(TextIntegerTest, Int64FieldTooSmall) { + EXPECT_THAT(Testee("Int64FieldTooSmall") + .ParseText(message(), "optional_int64: -9223372036854775809") + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(TextIntegerTest, Uint64FieldTooLarge) { + EXPECT_THAT(Testee("Uint64FieldTooLarge") + .ParseText(message(), "optional_uint64: 18446744073709551616") + .ParseOnly(), + Yields(IsParseError())); +} + +// ... in hex +TEST_P(TextIntegerTest, Int32FieldTooLargeHex) { + EXPECT_THAT(Testee("Int32FieldTooLargeHex") + .ParseText(message(), R"pb(optional_int32: 0x80000000)pb") + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(TextIntegerTest, Int32FieldTooSmallHex) { + EXPECT_THAT(Testee("Int32FieldTooSmallHex") + .ParseText(message(), R"pb(optional_int32: -0x80000001)pb") + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(TextIntegerTest, Uint32FieldTooLargeHex) { + EXPECT_THAT(Testee("Uint32FieldTooLargeHex") + .ParseText(message(), R"pb(optional_uint32: 0x100000000)pb") + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(TextIntegerTest, Int64FieldTooLargeHex) { + EXPECT_THAT( + Testee("Int64FieldTooLargeHex") + .ParseText(message(), R"pb(optional_int64: 0x8000000000000000)pb") + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(TextIntegerTest, Int64FieldTooSmallHex) { + EXPECT_THAT( + Testee("Int64FieldTooSmallHex") + .ParseText(message(), R"pb(optional_int64: -0x8000000000000001)pb") + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(TextIntegerTest, Uint64FieldTooLargeHex) { + EXPECT_THAT(Testee("Uint64FieldTooLargeHex") + .ParseText(message(), "optional_uint64: 0x10000000000000000") + .ParseOnly(), + Yields(IsParseError())); +} + +// ... and in octal. +TEST_P(TextIntegerTest, Int32FieldTooLargeOctal) { + EXPECT_THAT(Testee("Int32FieldTooLargeOctal") + .ParseText(message(), R"pb(optional_int32: 020000000000)pb") + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(TextIntegerTest, Int32FieldTooSmallOctal) { + EXPECT_THAT(Testee("Int32FieldTooSmallOctal") + .ParseText(message(), R"pb(optional_int32: -020000000001)pb") + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(TextIntegerTest, Uint32FieldTooLargeOctal) { + EXPECT_THAT(Testee("Uint32FieldTooLargeOctal") + .ParseText(message(), R"pb(optional_uint32: 040000000000)pb") + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(TextIntegerTest, Int64FieldTooLargeOctal) { + EXPECT_THAT( + Testee("Int64FieldTooLargeOctal") + .ParseText(message(), "optional_int64: 01000000000000000000000") + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(TextIntegerTest, Int64FieldTooSmallOctal) { + EXPECT_THAT( + Testee("Int64FieldTooSmallOctal") + .ParseText(message(), "optional_int64: -01000000000000000000001") + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(TextIntegerTest, Uint64FieldTooLargeOctal) { + EXPECT_THAT( + Testee("Uint64FieldTooLargeOctal") + .ParseText(message(), "optional_uint64: 02000000000000000000000") + .ParseOnly(), + Yields(IsParseError())); +} + +INSTANTIATE_TEST_SUITE_P(All, TextIntegerTest, + ValuesIn(Proto3TestMessageTypes()), + MessageTypeParamName); + +} // namespace +} // namespace conformance +} // namespace protobuf +} // namespace google
diff --git a/conformance/text_map_test.cc b/conformance/text_map_test.cc new file mode 100644 index 0000000..b86950f --- /dev/null +++ b/conformance/text_map_test.cc
@@ -0,0 +1,141 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2026 Google LLC. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +// Text-format conformance tests for map fields: entries given in key order +// round-trip, and a duplicated key keeps the last value. This replaces the +// legacy map block of TextFormatConformanceTestSuiteImpl<M>::RunAllTests(), +// which ran for the proto3-style message types only; the test names and the +// requests sent to the testee are identical to the legacy ones. +// +// The legacy tests passed a prototype pre-filled with the map entries in +// reverse order to RunValidTextFormatTestWithMessage(); that had no effect +// (the legacy ConformanceRequestSetting only took the prototype's descriptor +// and compared against prototype.New()), so nothing replaces it here. + +#include <gmock/gmock.h> +#include <gtest/gtest.h> +#include "absl/strings/string_view.h" +#include "conformance/binary_test_util.h" +#include "conformance/matchers.h" +#include "conformance/message_type_fixtures.h" +#include "conformance/testee.h" + +namespace google { +namespace protobuf { +namespace conformance { +namespace { + +using ::testing::ValuesIn; + +using TextMapTest = MessageTypeConformanceTest; + +// TODO: Write the plain-string inputs below as R"pb(...)pb" raw +// strings once the migration's request goldens are gone. The goldens pin the +// exact request bytes, and clang-format would reformat the whitespace of these +// inputs inside a pb raw string. +TEST_P(TextMapTest, AlphabeticallySortedMapStringKeys) { + constexpr absl::string_view kInput = R"( + map_string_string { + key: "a" + value: "value" + } + map_string_string { + key: "b" + value: "value" + } + map_string_string { + key: "c" + value: "value" + } + )"; + EXPECT_THAT(Testee("AlphabeticallySortedMapStringKeys") + .ParseText(message(), kInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kInput)))); + EXPECT_THAT(Testee("AlphabeticallySortedMapStringKeys") + .ParseText(message(), kInput) + .SerializeText(), + Yields(WhenParsed(EqualsTextProto(kInput)))); +} + +TEST_P(TextMapTest, AlphabeticallySortedMapIntKeys) { + constexpr absl::string_view kInput = R"( + map_int32_int32 { + key: 1 + value: 0 + } + map_int32_int32 { + key: 2 + value: 0 + } + map_int32_int32 { + key: 3 + value: 0 + } + )"; + EXPECT_THAT(Testee("AlphabeticallySortedMapIntKeys") + .ParseText(message(), kInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kInput)))); + EXPECT_THAT(Testee("AlphabeticallySortedMapIntKeys") + .ParseText(message(), kInput) + .SerializeText(), + Yields(WhenParsed(EqualsTextProto(kInput)))); +} + +TEST_P(TextMapTest, AlphabeticallySortedMapBoolKeys) { + constexpr absl::string_view kInput = R"( + map_bool_bool { + key: false + value: false + } + map_bool_bool { + key: true + value: false + } + )"; + EXPECT_THAT(Testee("AlphabeticallySortedMapBoolKeys") + .ParseText(message(), kInput) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kInput)))); + EXPECT_THAT(Testee("AlphabeticallySortedMapBoolKeys") + .ParseText(message(), kInput) + .SerializeText(), + Yields(WhenParsed(EqualsTextProto(kInput)))); +} + +// The last-specified value will be retained in a parsed map. Like the legacy +// test, this only checks the binary output. +TEST_P(TextMapTest, DuplicateMapKey) { + constexpr absl::string_view kInput = R"( + map_string_nested_message { + key: "duplicate" + value: { a: 123 } + } + map_string_nested_message { + key: "duplicate" + value: { corecursive: {} } + } + )"; + constexpr absl::string_view kExpected = R"pb( + map_string_nested_message { + key: "duplicate" + value: { corecursive: {} } + } + )pb"; + EXPECT_THAT( + Testee("DuplicateMapKey").ParseText(message(), kInput).SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(kExpected)))); +} + +INSTANTIATE_TEST_SUITE_P(All, TextMapTest, ValuesIn(Proto3TestMessageTypes()), + MessageTypeParamName); + +} // namespace +} // namespace conformance +} // namespace protobuf +} // namespace google
diff --git a/conformance/text_performance_test.cc b/conformance/text_performance_test.cc new file mode 100644 index 0000000..704cbd9 --- /dev/null +++ b/conformance/text_performance_test.cc
@@ -0,0 +1,151 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2026 Google LLC. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +// Text-format performance conformance tests: tens of thousands of occurrences +// of recursive_message, each holding one repeated field entry, that have to be +// merged into a single recursive_message. Part of the performance suite +// (see conformance.bzl). This replaces the legacy +// TextFormatConformanceTestSuiteImpl<M>::RunTextFormatPerformanceTests() and +// its helper, which ran for the four TestAllTypes{Proto2,Proto3} message types +// (proto and editions flavours); the test names and the requests sent to the +// testee are identical to the legacy ones. + +#include <string> + +#include <gmock/gmock.h> +#include <gtest/gtest.h> +#include "absl/strings/str_cat.h" +#include "absl/strings/string_view.h" +#include "conformance/binary_test_util.h" +#include "conformance/matchers.h" +#include "conformance/message_type_fixtures.h" +#include "conformance/testee.h" + +namespace google { +namespace protobuf { +namespace conformance { +namespace { + +using ::testing::ValuesIn; + +// The number of repetitions to use for performance tests. +constexpr int kPerformanceRepeatCount = 50000; + +// The input of the legacy +// TestTextFormatPerformanceMergeMessageWithRepeatedField() test for `field`, +// one entry of a repeated field of recursive_message: "recursive_message { +// <field> }" repeated kPerformanceRepeatCount times. +std::string RepeatedRecursiveMessageInput(absl::string_view field) { + const std::string recursive_message = + absl::StrCat("recursive_message { ", field, " }"); + std::string input; + for (int i = 0; i < kPerformanceRepeatCount; ++i) { + absl::StrAppend(&input, recursive_message); + } + return input; +} + +// The expected result of parsing RepeatedRecursiveMessageInput(field): a +// single recursive_message holding all kPerformanceRepeatCount entries. +std::string MergedRecursiveMessage(absl::string_view field) { + std::string expected = "recursive_message { "; + for (int i = 0; i < kPerformanceRepeatCount; ++i) { + absl::StrAppend(&expected, field, " "); + } + absl::StrAppend(&expected, "}"); + return expected; +} + +// Parameterized over the test message type only. +class TextPerformanceTest : public MessageTypeConformanceTest { + private: + TestPriority DefaultPriority() const override { return kP1; } +}; + +TEST_P(TextPerformanceTest, MergeMessageWithRepeatedFieldBool) { + constexpr absl::string_view kName = + "TestTextFormatPerformanceMergeMessageWithRepeatedFieldBool"; + const std::string input = + RepeatedRecursiveMessageInput("repeated_bool: true"); + const std::string expected = MergedRecursiveMessage("repeated_bool: true"); + EXPECT_THAT(Testee(kName).ParseText(message(), input).SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(expected)))); + EXPECT_THAT(Testee(kName).ParseText(message(), input).SerializeText(), + Yields(WhenParsed(EqualsTextProto(expected)))); +} + +TEST_P(TextPerformanceTest, MergeMessageWithRepeatedFieldDouble) { + constexpr absl::string_view kName = + "TestTextFormatPerformanceMergeMessageWithRepeatedFieldDouble"; + const std::string input = + RepeatedRecursiveMessageInput("repeated_double: 123"); + const std::string expected = MergedRecursiveMessage("repeated_double: 123"); + EXPECT_THAT(Testee(kName).ParseText(message(), input).SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(expected)))); + EXPECT_THAT(Testee(kName).ParseText(message(), input).SerializeText(), + Yields(WhenParsed(EqualsTextProto(expected)))); +} + +// The legacy Int32/Int64 tests actually exercise repeated_uint32 and +// repeated_uint64; the names are kept so failure-list entries keep matching. +TEST_P(TextPerformanceTest, MergeMessageWithRepeatedFieldInt32) { + constexpr absl::string_view kName = + "TestTextFormatPerformanceMergeMessageWithRepeatedFieldInt32"; + const std::string input = + RepeatedRecursiveMessageInput("repeated_uint32: 123"); + const std::string expected = MergedRecursiveMessage("repeated_uint32: 123"); + EXPECT_THAT(Testee(kName).ParseText(message(), input).SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(expected)))); + EXPECT_THAT(Testee(kName).ParseText(message(), input).SerializeText(), + Yields(WhenParsed(EqualsTextProto(expected)))); +} + +TEST_P(TextPerformanceTest, MergeMessageWithRepeatedFieldInt64) { + constexpr absl::string_view kName = + "TestTextFormatPerformanceMergeMessageWithRepeatedFieldInt64"; + const std::string input = + RepeatedRecursiveMessageInput("repeated_uint64: 123"); + const std::string expected = MergedRecursiveMessage("repeated_uint64: 123"); + EXPECT_THAT(Testee(kName).ParseText(message(), input).SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(expected)))); + EXPECT_THAT(Testee(kName).ParseText(message(), input).SerializeText(), + Yields(WhenParsed(EqualsTextProto(expected)))); +} + +TEST_P(TextPerformanceTest, MergeMessageWithRepeatedFieldString) { + constexpr absl::string_view kName = + "TestTextFormatPerformanceMergeMessageWithRepeatedFieldString"; + const std::string input = + RepeatedRecursiveMessageInput(R"(repeated_string: "foo")"); + const std::string expected = + MergedRecursiveMessage(R"(repeated_string: "foo")"); + EXPECT_THAT(Testee(kName).ParseText(message(), input).SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(expected)))); + EXPECT_THAT(Testee(kName).ParseText(message(), input).SerializeText(), + Yields(WhenParsed(EqualsTextProto(expected)))); +} + +TEST_P(TextPerformanceTest, MergeMessageWithRepeatedFieldBytes) { + constexpr absl::string_view kName = + "TestTextFormatPerformanceMergeMessageWithRepeatedFieldBytes"; + const std::string input = + RepeatedRecursiveMessageInput(R"(repeated_bytes: "foo")"); + const std::string expected = + MergedRecursiveMessage(R"(repeated_bytes: "foo")"); + EXPECT_THAT(Testee(kName).ParseText(message(), input).SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(expected)))); + EXPECT_THAT(Testee(kName).ParseText(message(), input).SerializeText(), + Yields(WhenParsed(EqualsTextProto(expected)))); +} + +INSTANTIATE_TEST_SUITE_P(All, TextPerformanceTest, + ValuesIn(AllTestMessageTypes()), MessageTypeParamName); + +} // namespace +} // namespace conformance +} // namespace protobuf +} // namespace google
diff --git a/conformance/text_reserved_field_test.cc b/conformance/text_reserved_field_test.cc new file mode 100644 index 0000000..dac88a2 --- /dev/null +++ b/conformance/text_reserved_field_test.cc
@@ -0,0 +1,176 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2026 Google LLC. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +// Text-format conformance tests for reserved field names: a field name that +// the message reserves (reserved_field in the test message types) is skipped +// by the parser, whatever the shape of its value. This replaces the legacy +// reserved-field block of TextFormatConformanceTestSuiteImpl<M>::RunAllTests(), +// which ran for the proto3-style message types only; the test names and the +// requests sent to the testee are identical to the legacy ones. + +#include <gmock/gmock.h> +#include <gtest/gtest.h> +#include "conformance/binary_test_util.h" +#include "conformance/matchers.h" +#include "conformance/message_type_fixtures.h" +#include "conformance/testee.h" + +namespace google { +namespace protobuf { +namespace conformance { +namespace { + +using ::testing::ValuesIn; + +// The expected message of every test is the input itself, i.e. an empty +// message once the reserved field has been skipped. +using TextReservedFieldTest = MessageTypeConformanceTest; + +TEST_P(TextReservedFieldTest, Boolean) { + EXPECT_THAT( + Testee("ReservedFieldName.Boolean") + .ParseText(message(), R"pb(reserved_field: true)pb") + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(R"pb(reserved_field: true)pb")))); + EXPECT_THAT( + Testee("ReservedFieldName.Boolean") + .ParseText(message(), R"pb(reserved_field: true)pb") + .SerializeText(), + Yields(WhenParsed(EqualsTextProto(R"pb(reserved_field: true)pb")))); +} + +TEST_P(TextReservedFieldTest, Integer) { + EXPECT_THAT( + Testee("ReservedFieldName.Integer") + .ParseText(message(), R"pb(reserved_field: -123)pb") + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(R"pb(reserved_field: -123)pb")))); + EXPECT_THAT( + Testee("ReservedFieldName.Integer") + .ParseText(message(), R"pb(reserved_field: -123)pb") + .SerializeText(), + Yields(WhenParsed(EqualsTextProto(R"pb(reserved_field: -123)pb")))); +} + +TEST_P(TextReservedFieldTest, Float) { + EXPECT_THAT( + Testee("ReservedFieldName.Float") + .ParseText(message(), R"pb(reserved_field: 0.123)pb") + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(R"pb(reserved_field: 0.123)pb")))); + EXPECT_THAT( + Testee("ReservedFieldName.Float") + .ParseText(message(), R"pb(reserved_field: 0.123)pb") + .SerializeText(), + Yields(WhenParsed(EqualsTextProto(R"pb(reserved_field: 0.123)pb")))); +} + +TEST_P(TextReservedFieldTest, Enum) { + EXPECT_THAT( + Testee("ReservedFieldName.Enum") + .ParseText(message(), R"pb(reserved_field: ENUM_VALUE)pb") + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(R"pb(reserved_field: ENUM_VALUE)pb")))); + EXPECT_THAT( + Testee("ReservedFieldName.Enum") + .ParseText(message(), R"pb(reserved_field: ENUM_VALUE)pb") + .SerializeText(), + Yields(WhenParsed(EqualsTextProto(R"pb(reserved_field: ENUM_VALUE)pb")))); +} + +TEST_P(TextReservedFieldTest, String) { + EXPECT_THAT( + Testee("ReservedFieldName.String") + .ParseText(message(), R"pb(reserved_field: "hello")pb") + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(R"pb(reserved_field: "hello")pb")))); + EXPECT_THAT( + Testee("ReservedFieldName.String") + .ParseText(message(), R"pb(reserved_field: "hello")pb") + .SerializeText(), + Yields(WhenParsed(EqualsTextProto(R"pb(reserved_field: "hello")pb")))); +} + +TEST_P(TextReservedFieldTest, Message) { + EXPECT_THAT( + Testee("ReservedFieldName.Message") + .ParseText(message(), R"pb(reserved_field: { a: 123 })pb") + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(R"pb(reserved_field: { a: 123 })pb")))); + EXPECT_THAT( + Testee("ReservedFieldName.Message") + .ParseText(message(), R"pb(reserved_field: { a: 123 })pb") + .SerializeText(), + Yields(WhenParsed(EqualsTextProto(R"pb(reserved_field: { a: 123 })pb")))); +} + +TEST_P(TextReservedFieldTest, MessageAngleBrackets) { + EXPECT_THAT( + Testee("ReservedFieldName.MessageAngleBrackets") + .ParseText(message(), R"pb(reserved_field: < a: 123 >)pb") + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(R"pb(reserved_field: < a: 123 >)pb")))); + EXPECT_THAT( + Testee("ReservedFieldName.MessageAngleBrackets") + .ParseText(message(), R"pb(reserved_field: < a: 123 >)pb") + .SerializeText(), + Yields(WhenParsed(EqualsTextProto(R"pb(reserved_field: < a: 123 >)pb")))); +} + +// TODO: Write the plain-string inputs below as R"pb(...)pb" raw +// strings once the migration's request goldens are gone. The goldens pin the +// exact request bytes, and clang-format would reformat the whitespace of these +// inputs inside a pb raw string. +TEST_P(TextReservedFieldTest, RepeatedInteger) { + EXPECT_THAT( + Testee("ReservedFieldName.RepeatedInteger") + .ParseText(message(), "reserved_field: [-123, 456]") + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto("reserved_field: [-123, 456]")))); + EXPECT_THAT( + Testee("ReservedFieldName.RepeatedInteger") + .ParseText(message(), "reserved_field: [-123, 456]") + .SerializeText(), + Yields(WhenParsed(EqualsTextProto("reserved_field: [-123, 456]")))); +} + +// The trailing space of the legacy input is part of the request bytes and is +// kept. +TEST_P(TextReservedFieldTest, RepeatedFloat) { + EXPECT_THAT( + Testee("ReservedFieldName.RepeatedFloat") + .ParseText(message(), "reserved_field: [0.123, 1e-10] ") + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto("reserved_field: [0.123, 1e-10] ")))); + EXPECT_THAT( + Testee("ReservedFieldName.RepeatedFloat") + .ParseText(message(), "reserved_field: [0.123, 1e-10] ") + .SerializeText(), + Yields(WhenParsed(EqualsTextProto("reserved_field: [0.123, 1e-10] ")))); +} + +TEST_P(TextReservedFieldTest, RepeatedString) { + EXPECT_THAT(Testee("ReservedFieldName.RepeatedString") + .ParseText(message(), R"(reserved_field: ["hello", "world"])") + .SerializeBinary(), + Yields(WhenParsed( + EqualsTextProto(R"(reserved_field: ["hello", "world"])")))); + EXPECT_THAT(Testee("ReservedFieldName.RepeatedString") + .ParseText(message(), R"(reserved_field: ["hello", "world"])") + .SerializeText(), + Yields(WhenParsed( + EqualsTextProto(R"(reserved_field: ["hello", "world"])")))); +} + +INSTANTIATE_TEST_SUITE_P(All, TextReservedFieldTest, + ValuesIn(Proto3TestMessageTypes()), + MessageTypeParamName); + +} // namespace +} // namespace conformance +} // namespace protobuf +} // namespace google
diff --git a/conformance/text_string_test.cc b/conformance/text_string_test.cc new file mode 100644 index 0000000..d0b8400 --- /dev/null +++ b/conformance/text_string_test.cc
@@ -0,0 +1,293 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2026 Google LLC. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +// Text-format conformance tests for string literals, run for a string and for +// a bytes field: concatenation, the escape sequences, the rejected line feeds +// and out-of-range or surrogate unicode escapes, and the invalid UTF-8 byte +// sequences that only string fields reject. This replaces the +// "String literals x {Strings, Bytes}" section of the legacy +// TextFormatConformanceTestSuiteImpl<M>::RunAllTests(), which ran for the +// proto3-style message types only; the test names and the requests sent to +// the testee are identical to the legacy ones. + +#include <ostream> +#include <string> +#include <tuple> + +#include <gmock/gmock.h> +#include <gtest/gtest.h> +#include "absl/strings/str_cat.h" +#include "absl/strings/string_view.h" +#include "conformance/binary_test_util.h" +#include "conformance/matchers.h" +#include "conformance/message_type_fixtures.h" +#include "conformance/test_fixture.h" +#include "conformance/testee.h" +#include "google/protobuf/descriptor.h" + +namespace google { +namespace protobuf { +namespace conformance { +namespace { + +using ::testing::Combine; +using ::testing::ValuesIn; + +// The field a string literal is tested with. The legacy suite ran every +// literal test for the string field and for the bytes field, with "String" / +// "Bytes" appended to the test name. +struct StringField { + absl::string_view type_name; // "String" or "Bytes", as in the test names. + absl::string_view field_name; // The name of the field of that type. +}; + +constexpr StringField kStringFields[] = { + {"String", "optional_string"}, + {"Bytes", "optional_bytes"}, +}; + +std::string ParamName(const StringField& field) { + return std::string(field.type_name); +} + +// So a failing test prints the field by name instead of as raw bytes. +void PrintTo(const StringField& field, std::ostream* os) { + *os << field.field_name; +} + +// Parameterized over (test message type, string field). Reporting the +// message type through MessageUnderTest() makes the base SetUp() skip the +// editions instance when --maximum_edition doesn't cover it. TestName() and +// Input() only build values; every expectation is in the test bodies. +class TextStringLiteralTest : public ConformanceTest, + public testing::WithParamInterface< + std::tuple<const Descriptor*, StringField>> { + protected: + const Descriptor* message() const { return std::get<0>(GetParam()); } + const StringField& field() const { return std::get<1>(GetParam()); } + + // The legacy test name: "StringLiteral<base><String|Bytes>", e.g. + // "StringLiteralConcatBytes". + std::string TestName(absl::string_view base) const { + return absl::StrCat("StringLiteral", base, field().type_name); + } + + // The text-format input setting the field to `literal`, e.g. + // "optional_bytes: 'first' \"second\"". + std::string Input(absl::string_view literal) const { + return absl::StrCat(field().field_name, ": ", literal); + } + + private: + TestPriority DefaultPriority() const override { return kP1; } + const Descriptor* MessageUnderTest() const override { return message(); } +}; + +TEST_P(TextStringLiteralTest, Concat) { + const std::string input = Input("'first' \"second\"\n'third'"); + EXPECT_THAT(Testee(kP0, TestName("Concat")) + .ParseText(message(), input) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(input)))); + EXPECT_THAT(Testee(kP0, TestName("Concat")) + .ParseText(message(), input) + .SerializeText(), + Yields(WhenParsed(EqualsTextProto(input)))); +} + +TEST_P(TextStringLiteralTest, BasicEscapes) { + const std::string input = Input(R"('\a\b\f\n\r\t\v\?\\\'\"')"); + EXPECT_THAT(Testee(kP0, TestName("BasicEscapes")) + .ParseText(message(), input) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(input)))); + EXPECT_THAT(Testee(kP0, TestName("BasicEscapes")) + .ParseText(message(), input) + .SerializeText(), + Yields(WhenParsed(EqualsTextProto(input)))); +} + +TEST_P(TextStringLiteralTest, OctalEscapes) { + const std::string input = Input("'\\341\\210\\264'"); + EXPECT_THAT(Testee(kP0, TestName("OctalEscapes")) + .ParseText(message(), input) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(input)))); + EXPECT_THAT(Testee(kP0, TestName("OctalEscapes")) + .ParseText(message(), input) + .SerializeText(), + Yields(WhenParsed(EqualsTextProto(input)))); +} + +TEST_P(TextStringLiteralTest, HexEscapes) { + const std::string input = Input("'\\xe1\\x88\\xb4'"); + EXPECT_THAT(Testee(kP0, TestName("HexEscapes")) + .ParseText(message(), input) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(input)))); + EXPECT_THAT(Testee(kP0, TestName("HexEscapes")) + .ParseText(message(), input) + .SerializeText(), + Yields(WhenParsed(EqualsTextProto(input)))); +} + +TEST_P(TextStringLiteralTest, ShortUnicodeEscape) { + const std::string input = Input("'\\u1234'"); + EXPECT_THAT(Testee(TestName("ShortUnicodeEscape")) + .ParseText(message(), input) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(input)))); + EXPECT_THAT(Testee(TestName("ShortUnicodeEscape")) + .ParseText(message(), input) + .SerializeText(), + Yields(WhenParsed(EqualsTextProto(input)))); +} + +TEST_P(TextStringLiteralTest, LongUnicodeEscapes) { + const std::string input = Input("'\\U00001234\\U00010437'"); + EXPECT_THAT(Testee(TestName("LongUnicodeEscapes")) + .ParseText(message(), input) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(input)))); + EXPECT_THAT(Testee(TestName("LongUnicodeEscapes")) + .ParseText(message(), input) + .SerializeText(), + Yields(WhenParsed(EqualsTextProto(input)))); +} + +// String literals don't include line feeds. +TEST_P(TextStringLiteralTest, IncludesLF) { + EXPECT_THAT(Testee(kP0, TestName("IncludesLF")) + .ParseText(message(), Input("'first line\nsecond line'")) + .ParseOnly(), + Yields(IsParseError())); +} + +// Unicode escapes don't include code points that lie beyond the planes +// (> 0x10ffff). +TEST_P(TextStringLiteralTest, LongUnicodeEscapeTooLarge) { + EXPECT_THAT(Testee(kP0, TestName("LongUnicodeEscapeTooLarge")) + .ParseText(message(), Input("'\\U00110000'")) + .ParseOnly(), + Yields(IsParseError())); +} + +// Unicode escapes don't include surrogates. +TEST_P(TextStringLiteralTest, ShortUnicodeEscapeSurrogatePair) { + EXPECT_THAT(Testee(TestName("ShortUnicodeEscapeSurrogatePair")) + .ParseText(message(), Input("'\\ud801\\udc37'")) + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(TextStringLiteralTest, ShortUnicodeEscapeSurrogateFirstOnly) { + EXPECT_THAT(Testee(TestName("ShortUnicodeEscapeSurrogateFirstOnly")) + .ParseText(message(), Input("'\\ud800'")) + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(TextStringLiteralTest, ShortUnicodeEscapeSurrogateSecondOnly) { + EXPECT_THAT(Testee(TestName("ShortUnicodeEscapeSurrogateSecondOnly")) + .ParseText(message(), Input("'\\udc00'")) + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(TextStringLiteralTest, LongUnicodeEscapeSurrogateFirstOnly) { + EXPECT_THAT(Testee(TestName("LongUnicodeEscapeSurrogateFirstOnly")) + .ParseText(message(), Input("'\\U0000d800'")) + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(TextStringLiteralTest, LongUnicodeEscapeSurrogateSecondOnly) { + EXPECT_THAT(Testee(TestName("LongUnicodeEscapeSurrogateSecondOnly")) + .ParseText(message(), Input("'\\U0000dc00'")) + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(TextStringLiteralTest, LongUnicodeEscapeSurrogatePair) { + EXPECT_THAT(Testee(TestName("LongUnicodeEscapeSurrogatePair")) + .ParseText(message(), Input("'\\U0000d801\\U00000dc37'")) + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(TextStringLiteralTest, UnicodeEscapeSurrogatePairLongShort) { + EXPECT_THAT(Testee(TestName("UnicodeEscapeSurrogatePairLongShort")) + .ParseText(message(), Input("'\\U0000d801\\udc37'")) + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(TextStringLiteralTest, UnicodeEscapeSurrogatePairShortLong) { + EXPECT_THAT(Testee(TestName("UnicodeEscapeSurrogatePairShortLong")) + .ParseText(message(), Input("'\\ud801\\U0000dc37'")) + .ParseOnly(), + Yields(IsParseError())); +} + +INSTANTIATE_TEST_SUITE_P(All, TextStringLiteralTest, + Combine(ValuesIn(Proto3TestMessageTypes()), + ValuesIn(kStringFields)), + TupleParamName<TextStringLiteralTest::ParamType>); + +// String fields reject invalid UTF-8 byte sequences; bytes fields don't. +using TextStringFieldTest = MessageTypeConformanceTest; + +TEST_P(TextStringFieldTest, BadUtf8Octal) { + EXPECT_THAT(Testee("StringFieldBadUTF8Octal") + .ParseText(message(), "optional_string: '\\300'") + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(TextStringFieldTest, BadUtf8Hex) { + EXPECT_THAT(Testee("StringFieldBadUTF8Hex") + .ParseText(message(), "optional_string: '\\xc0'") + .ParseOnly(), + Yields(IsParseError())); +} + +INSTANTIATE_TEST_SUITE_P(All, TextStringFieldTest, + ValuesIn(Proto3TestMessageTypes()), + MessageTypeParamName); + +using TextBytesFieldTest = MessageTypeConformanceTest; + +TEST_P(TextBytesFieldTest, BadUtf8Octal) { + EXPECT_THAT(Testee("BytesFieldBadUTF8Octal") + .ParseText(message(), "optional_bytes: '\\300'") + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto("optional_bytes: '\\300'")))); + EXPECT_THAT(Testee("BytesFieldBadUTF8Octal") + .ParseText(message(), "optional_bytes: '\\300'") + .SerializeText(), + Yields(WhenParsed(EqualsTextProto("optional_bytes: '\\300'")))); +} + +TEST_P(TextBytesFieldTest, BadUtf8Hex) { + EXPECT_THAT(Testee("BytesFieldBadUTF8Hex") + .ParseText(message(), "optional_bytes: '\\xc0'") + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto("optional_bytes: '\\xc0'")))); + EXPECT_THAT(Testee("BytesFieldBadUTF8Hex") + .ParseText(message(), "optional_bytes: '\\xc0'") + .SerializeText(), + Yields(WhenParsed(EqualsTextProto("optional_bytes: '\\xc0'")))); +} + +INSTANTIATE_TEST_SUITE_P(All, TextBytesFieldTest, + ValuesIn(Proto3TestMessageTypes()), + MessageTypeParamName); + +} // namespace +} // namespace conformance +} // namespace protobuf +} // namespace google
diff --git a/conformance/text_syntax_test.cc b/conformance/text_syntax_test.cc new file mode 100644 index 0000000..c12ff2b --- /dev/null +++ b/conformance/text_syntax_test.cc
@@ -0,0 +1,303 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2026 Google LLC. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +// Text-format conformance tests for the separators of the syntax: the +// optional "," / ";" after a field, and the "," between the values of a list. +// This replaces the "Separators" block of the legacy +// TextFormatConformanceTestSuiteImpl<M>::RunAllTests(), which ran for the +// proto3-style message types only; the test names and the requests sent to +// the testee are identical to the legacy ones. + +#include <ostream> +#include <string> +#include <tuple> + +#include <gmock/gmock.h> +#include <gtest/gtest.h> +#include "absl/strings/str_cat.h" +#include "absl/strings/string_view.h" +#include "conformance/binary_test_util.h" +#include "conformance/matchers.h" +#include "conformance/message_type_fixtures.h" +#include "conformance/test_fixture.h" +#include "conformance/testee.h" +#include "google/protobuf/descriptor.h" + +namespace google { +namespace protobuf { +namespace conformance { +namespace { + +using ::testing::Combine; +using ::testing::Values; +using ::testing::ValuesIn; + +// A field type the separator tests run over: the legacy suite ran every +// separator test once per entry, with "_<type>" appended to the test name and +// `value` as the literal of the optional_<type> / repeated_<type> field. +struct SeparatorCase { + absl::string_view type; + absl::string_view value; + // Whether two adjacent values of the type are one value to the parser + // (string literals concatenate) rather than a missing list separator. + bool adjacent_values_concatenate; +}; + +constexpr SeparatorCase kSeparatorCases[] = { + {"string", "\"abc\"", true}, {"bytes", "\"abc\"", true}, + {"int32", "123", false}, {"bool", "true", false}, + {"double", "1.23", false}, {"fixed32", "0x123", false}, +}; + +std::string ParamName(const SeparatorCase& test_case) { + // Qualified: the overloads in this namespace hide the outer ones. + return conformance::ParamName(test_case.type); +} + +// For gtest's failure messages (the parameter is otherwise printed as bytes). +void PrintTo(const SeparatorCase& test_case, std::ostream* os) { + *os << test_case.type; +} + +// Whether the field separator tests use the optional_<type> field with a +// bare value ("Single") or the repeated_<type> field with a one-element list +// ("Repeated"); the legacy test names carry the same two words. +enum class FieldCardinality { kSingle, kRepeated }; + +std::string ParamName(FieldCardinality cardinality) { + return cardinality == FieldCardinality::kSingle ? "Single" : "Repeated"; +} + +void PrintTo(FieldCardinality cardinality, std::ostream* os) { + *os << ParamName(cardinality); +} + +// The separators after a top-level field, parameterized over (test message +// type, field type, single/repeated). Reporting the message type through +// MessageUnderTest() makes the base SetUp() skip the editions instance when +// --maximum_edition doesn't cover it. TestName() and Input() only build +// values; every expectation is in the test bodies. +class TextFieldSeparatorTest + : public ConformanceTest, + public testing::WithParamInterface< + std::tuple<const Descriptor*, SeparatorCase, FieldCardinality>> { + protected: + const Descriptor* message() const { return std::get<0>(GetParam()); } + const SeparatorCase& test_case() const { return std::get<1>(GetParam()); } + FieldCardinality cardinality() const { return std::get<2>(GetParam()); } + + // The legacy test name, e.g. "FieldSeparatorCommaTopLevelSingle_int32". + std::string TestName(absl::string_view base) const { + return absl::StrCat(base, ParamName(cardinality()), "_", test_case().type); + } + + // The field set to its value, followed by `separator`, e.g. + // "optional_int32: 123," or "repeated_int32: [123],". + std::string Input(absl::string_view separator) const { + const bool single = cardinality() == FieldCardinality::kSingle; + return absl::StrCat(single ? "optional_" : "repeated_", test_case().type, + ": ", single ? "" : "[", test_case().value, + single ? "" : "]", separator); + } + + private: + const Descriptor* MessageUnderTest() const override { return message(); } +}; + +TEST_P(TextFieldSeparatorTest, Comma) { + const std::string input = Input(","); + EXPECT_THAT(Testee(TestName("FieldSeparatorCommaTopLevel")) + .ParseText(message(), input) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(input)))); + EXPECT_THAT(Testee(TestName("FieldSeparatorCommaTopLevel")) + .ParseText(message(), input) + .SerializeText(), + Yields(WhenParsed(EqualsTextProto(input)))); +} + +// Note: the legacy name has "Single" in it even for the repeated variant. +TEST_P(TextFieldSeparatorTest, Semi) { + const std::string input = Input(";"); + EXPECT_THAT(Testee(TestName("FieldSeparatorSemiTopLevelSingle")) + .ParseText(message(), input) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(input)))); + EXPECT_THAT(Testee(TestName("FieldSeparatorSemiTopLevelSingle")) + .ParseText(message(), input) + .SerializeText(), + Yields(WhenParsed(EqualsTextProto(input)))); +} + +TEST_P(TextFieldSeparatorTest, CommaDuplicatesFails) { + EXPECT_THAT(Testee(TestName("FieldSeparatorCommaTopLevelDuplicatesFails")) + .ParseText(message(), Input(",,")) + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(TextFieldSeparatorTest, SemiDuplicatesFails) { + EXPECT_THAT(Testee(TestName("FieldSeparatorSemiTopLevelDuplicateFails")) + .ParseText(message(), Input(";;")) + .ParseOnly(), + Yields(IsParseError())); +} + +INSTANTIATE_TEST_SUITE_P( + All, TextFieldSeparatorTest, + Combine(ValuesIn(Proto3TestMessageTypes()), ValuesIn(kSeparatorCases), + Values(FieldCardinality::kSingle, FieldCardinality::kRepeated)), + TupleParamName<TextFieldSeparatorTest::ParamType>); + +// The required "," between the values of a list, parameterized over (test +// message type, field type). +class TextListSeparatorTest + : public ConformanceTest, + public testing::WithParamInterface< + std::tuple<const Descriptor*, SeparatorCase>> { + protected: + const Descriptor* message() const { return std::get<0>(GetParam()); } + const SeparatorCase& test_case() const { return std::get<1>(GetParam()); } + + // The legacy test name, e.g. "ListSeparator_int32". + std::string TestName(absl::string_view base) const { + return absl::StrCat(base, "_", test_case().type); + } + + // The repeated field set to a list whose body is `list_body`, e.g. + // "repeated_int32: [123,123]". + std::string Input(absl::string_view list_body) const { + return absl::StrCat("repeated_", test_case().type, ": [", list_body, "]"); + } + + absl::string_view value() const { return test_case().value; } + + private: + const Descriptor* MessageUnderTest() const override { return message(); } +}; + +TEST_P(TextListSeparatorTest, Comma) { + const std::string input = Input(absl::StrCat(value(), ",", value())); + EXPECT_THAT(Testee(TestName("ListSeparator")) + .ParseText(message(), input) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(input)))); + EXPECT_THAT(Testee(TestName("ListSeparator")) + .ParseText(message(), input) + .SerializeText(), + Yields(WhenParsed(EqualsTextProto(input)))); +} + +TEST_P(TextListSeparatorTest, SemiFails) { + EXPECT_THAT( + Testee(TestName("ListSeparatorSemiFails")) + .ParseText(message(), Input(absl::StrCat(value(), ";", value()))) + .ParseOnly(), + Yields(IsParseError())); +} + +// For string and bytes, if we skip the separator, the parser will treat the +// two values as a single value. +TEST_P(TextListSeparatorTest, Missing) { + const std::string input = Input(absl::StrCat(value(), " ", value())); + if (test_case().adjacent_values_concatenate) { + EXPECT_THAT(Testee(TestName("ListSeparatorMissingIsOneValue")) + .ParseText(message(), input) + .SerializeBinary(), + Yields(WhenParsed(EqualsTextProto(input)))); + EXPECT_THAT(Testee(TestName("ListSeparatorMissingIsOneValue")) + .ParseText(message(), input) + .SerializeText(), + Yields(WhenParsed(EqualsTextProto(input)))); + } else { + EXPECT_THAT(Testee(TestName("ListSeparatorMissingFails")) + .ParseText(message(), input) + .ParseOnly(), + Yields(IsParseError())); + } +} + +TEST_P(TextListSeparatorTest, DuplicateFails) { + EXPECT_THAT( + Testee(TestName("ListSeparatorDuplicateFails")) + .ParseText(message(), Input(absl::StrCat(value(), ",,", value()))) + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(TextListSeparatorTest, SingleTrailingFails) { + EXPECT_THAT(Testee(TestName("ListSeparatorSingleTrailingFails")) + .ParseText(message(), Input(absl::StrCat(value(), ","))) + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(TextListSeparatorTest, TwoValuesTrailingFails) { + EXPECT_THAT( + Testee(TestName("ListSeparatorTwoValuesTrailingFails")) + .ParseText(message(), Input(absl::StrCat(value(), ",", value(), ","))) + .ParseOnly(), + Yields(IsParseError())); +} + +INSTANTIATE_TEST_SUITE_P(All, TextListSeparatorTest, + Combine(ValuesIn(Proto3TestMessageTypes()), + ValuesIn(kSeparatorCases)), + TupleParamName<TextListSeparatorTest::ParamType>); + +// The test messages don't have every type nested, so just check one data +// type for the nested field separator support. +using TextNestedSeparatorTest = MessageTypeConformanceTest; + +TEST_P(TextNestedSeparatorTest, Comma) { + EXPECT_THAT(Testee("FieldSeparatorCommaNested") + .ParseText(message(), "optional_nested_message: { a: 123, }") + .SerializeBinary(), + Yields(WhenParsed( + EqualsTextProto("optional_nested_message: { a: 123, }")))); + EXPECT_THAT(Testee("FieldSeparatorCommaNested") + .ParseText(message(), "optional_nested_message: { a: 123, }") + .SerializeText(), + Yields(WhenParsed( + EqualsTextProto("optional_nested_message: { a: 123, }")))); +} + +TEST_P(TextNestedSeparatorTest, Semi) { + EXPECT_THAT(Testee("FieldSeparatorSemiNested") + .ParseText(message(), "optional_nested_message: { a: 123; }") + .SerializeBinary(), + Yields(WhenParsed( + EqualsTextProto("optional_nested_message: { a: 123; }")))); + EXPECT_THAT(Testee("FieldSeparatorSemiNested") + .ParseText(message(), "optional_nested_message: { a: 123; }") + .SerializeText(), + Yields(WhenParsed( + EqualsTextProto("optional_nested_message: { a: 123; }")))); +} + +TEST_P(TextNestedSeparatorTest, CommaDuplicates) { + EXPECT_THAT(Testee("FieldSeparatorCommaNestedDuplicates") + .ParseText(message(), "optional_nested_message: { a: 123,, }") + .ParseOnly(), + Yields(IsParseError())); +} + +TEST_P(TextNestedSeparatorTest, SemiDuplicates) { + EXPECT_THAT(Testee("FieldSeparatorSemiNestedDuplicates") + .ParseText(message(), "optional_nested_message: { a: 123;; }") + .ParseOnly(), + Yields(IsParseError())); +} + +INSTANTIATE_TEST_SUITE_P(All, TextNestedSeparatorTest, + ValuesIn(Proto3TestMessageTypes()), + MessageTypeParamName); + +} // namespace +} // namespace conformance +} // namespace protobuf +} // namespace google
diff --git a/conformance/text_unknown_field_test.cc b/conformance/text_unknown_field_test.cc new file mode 100644 index 0000000..6437287 --- /dev/null +++ b/conformance/text_unknown_field_test.cc
@@ -0,0 +1,132 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2026 Google LLC. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +// Text-format conformance tests for unknown fields: a binary payload whose +// fields are all unknown to the test message type is serialized as text +// format, once dropping the unknown fields ("<name>_Drop") and once printing +// them by field number ("<name>_Print"). This replaces the "Unknown Fields" +// block of the legacy TextFormatConformanceTestSuiteImpl<M>::RunAllTests() and +// its RunValidUnknownTextFormatTest() helper, which ran for the proto3-style +// message types only; the test names and the requests sent to the testee are +// identical to the legacy ones. +// +// The payloads are serializations of UnknownToTestAllTypes, whose fields have +// numbers no TestAllTypes message defines, and the testee's text output is +// decoded as that type too (see WhenParsedAs() in matchers.h): unknown +// fields printed by number become its known fields again, so the _Print leg +// expects the whole payload back and the _Drop leg an empty message. Like the +// legacy tests these are RECOMMENDED and categorized as TEXT_FORMAT_TEST +// although their input is binary. + +#include <gmock/gmock.h> +#include <gtest/gtest.h> +#include "conformance/binary_test_util.h" +#include "conformance/binary_wireformat.h" +#include "conformance/conformance.pb.h" +#include "conformance/matchers.h" +#include "conformance/message_type_fixtures.h" +#include "conformance/testee.h" +#include "google/protobuf/test_messages_proto2.pb.h" + +namespace google { +namespace protobuf { +namespace conformance { +namespace { + +using ::protobuf_test_messages::proto2::UnknownToTestAllTypes; +using ::testing::ValuesIn; + +class TextUnknownFieldTest : public MessageTypeConformanceTest { + private: + TestPriority DefaultPriority() const override { return kP1; } +}; + +// Unable to print unknown Fixed32/Fixed64 fields as if they are known. +// Fixed32/Fixed64 fields are not added in the tests. +TEST_P(TextUnknownFieldTest, ScalarUnknownFields) { + UnknownToTestAllTypes unknown; + unknown.set_optional_int32(123); + unknown.set_optional_string("hello"); + unknown.set_optional_bool(true); + const Wire bytes(unknown.SerializeAsString()); + EXPECT_THAT(Testee("ScalarUnknownFields_Drop") + .ParseBinary(message(), bytes) + .OverrideTestCategory(::conformance::TEXT_FORMAT_TEST) + .SerializeText(), + Yields(WhenParsedAs<UnknownToTestAllTypes>(EqualsTextProto("")))); + EXPECT_THAT( + Testee("ScalarUnknownFields_Print") + .ParseBinary(message(), bytes) + .OverrideTestCategory(::conformance::TEXT_FORMAT_TEST) + .SerializeText({/*print_unknown_fields=*/true}), + Yields(WhenParsedAs<UnknownToTestAllTypes>(EqualsBinaryProto(bytes)))); +} + +TEST_P(TextUnknownFieldTest, MessageUnknownFields) { + UnknownToTestAllTypes unknown; + unknown.mutable_nested_message()->set_c(111); + const Wire bytes(unknown.SerializeAsString()); + EXPECT_THAT(Testee("MessageUnknownFields_Drop") + .ParseBinary(message(), bytes) + .OverrideTestCategory(::conformance::TEXT_FORMAT_TEST) + .SerializeText(), + Yields(WhenParsedAs<UnknownToTestAllTypes>(EqualsTextProto("")))); + EXPECT_THAT( + Testee("MessageUnknownFields_Print") + .ParseBinary(message(), bytes) + .OverrideTestCategory(::conformance::TEXT_FORMAT_TEST) + .SerializeText({/*print_unknown_fields=*/true}), + Yields(WhenParsedAs<UnknownToTestAllTypes>(EqualsBinaryProto(bytes)))); +} + +TEST_P(TextUnknownFieldTest, GroupUnknownFields) { + UnknownToTestAllTypes unknown; + unknown.mutable_optionalgroup()->set_a(321); + const Wire bytes(unknown.SerializeAsString()); + EXPECT_THAT(Testee("GroupUnknownFields_Drop") + .ParseBinary(message(), bytes) + .OverrideTestCategory(::conformance::TEXT_FORMAT_TEST) + .SerializeText(), + Yields(WhenParsedAs<UnknownToTestAllTypes>(EqualsTextProto("")))); + EXPECT_THAT( + Testee("GroupUnknownFields_Print") + .ParseBinary(message(), bytes) + .OverrideTestCategory(::conformance::TEXT_FORMAT_TEST) + .SerializeText({/*print_unknown_fields=*/true}), + Yields(WhenParsedAs<UnknownToTestAllTypes>(EqualsBinaryProto(bytes)))); +} + +// Note: the legacy test built this payload on top of the group one without +// clearing it, so the group is part of this payload too. +TEST_P(TextUnknownFieldTest, RepeatedUnknownFields) { + UnknownToTestAllTypes unknown; + unknown.mutable_optionalgroup()->set_a(321); + unknown.add_repeated_int32(1); + unknown.add_repeated_int32(2); + unknown.add_repeated_int32(3); + const Wire bytes(unknown.SerializeAsString()); + EXPECT_THAT(Testee("RepeatedUnknownFields_Drop") + .ParseBinary(message(), bytes) + .OverrideTestCategory(::conformance::TEXT_FORMAT_TEST) + .SerializeText(), + Yields(WhenParsedAs<UnknownToTestAllTypes>(EqualsTextProto("")))); + EXPECT_THAT( + Testee("RepeatedUnknownFields_Print") + .ParseBinary(message(), bytes) + .OverrideTestCategory(::conformance::TEXT_FORMAT_TEST) + .SerializeText({/*print_unknown_fields=*/true}), + Yields(WhenParsedAs<UnknownToTestAllTypes>(EqualsBinaryProto(bytes)))); +} + +INSTANTIATE_TEST_SUITE_P(All, TextUnknownFieldTest, + ValuesIn(Proto3TestMessageTypes()), + MessageTypeParamName); + +} // namespace +} // namespace conformance +} // namespace protobuf +} // namespace google
diff --git a/upb/conformance/conformance_upb_failures.txt b/upb/conformance/conformance_upb_failures.txt index d1fa6e5..9d13500 100644 --- a/upb/conformance/conformance_upb_failures.txt +++ b/upb/conformance/conformance_upb_failures.txt
@@ -1,6 +1,6 @@ -Recommended.*.JsonInput.FieldNameDuplicate # Should have failed to parse or matched expected output but did not. -Recommended.*.JsonInput.FieldNameDuplicateDifferentCasing1 # Should have failed to parse or matched expected output but did not. -Recommended.*.JsonInput.FieldNameDuplicateDifferentCasing2 # Should have failed to parse or matched expected output but did not. +Recommended.*.JsonInput.FieldNameDuplicate # Should have failed to parse, but didn't., and Output was not equivalent to reference message: added: optional_nested_message.a: +Recommended.*.JsonInput.FieldNameDuplicateDifferentCasing1 # Should have failed to parse, but didn't., and Output was not equivalent to reference message: added: optional_nested_message.a: +Recommended.*.JsonInput.FieldNameDuplicateDifferentCasing2 # Should have failed to parse, but didn't., and Output was not equivalent to reference message: added: optional_nested_message.a: Required.*.JsonInput.Int32FieldQuotedExponentialValue.* # Failed to parse input or produce output. Required.*.JsonInput.AnyWithNoType.* # Failed to parse input or produce output. # TODO: Uncomment once conformance tests can express failures that are not expected to be fixed.