From 7fa212bba15da2fd725860dcc2e511a89eadba89 Mon Sep 17 00:00:00 2001 From: Jason Lunn Date: Wed, 16 Mar 2022 13:37:46 -0400 Subject: [PATCH 1/2] Fix NPE during encoding and add regression test for issue 9507. (cherry picked from commit 58e320a7323b55f65137222064ed0e72fe423e23) --- .../google/protobuf/jruby/RubyMessage.java | 18 +++++++----- ruby/tests/basic.rb | 28 +++++++++++++++++++ 2 files changed, 39 insertions(+), 7 deletions(-) diff --git a/ruby/src/main/java/com/google/protobuf/jruby/RubyMessage.java b/ruby/src/main/java/com/google/protobuf/jruby/RubyMessage.java index f55eb9b2850d..0d76116097af 100644 --- a/ruby/src/main/java/com/google/protobuf/jruby/RubyMessage.java +++ b/ruby/src/main/java/com/google/protobuf/jruby/RubyMessage.java @@ -62,6 +62,9 @@ import java.util.Map; public class RubyMessage extends RubyObject { + private final String DEFAULT_VALUE = "google.protobuf.FieldDescriptorProto.default_value"; + private final String TYPE = "type"; + public RubyMessage(Ruby runtime, RubyClass klazz, Descriptor descriptor) { super(runtime, klazz); @@ -677,6 +680,8 @@ protected DynamicMessage build(ThreadContext context, int depth, int recursionLi throw context.runtime.newRuntimeError("Recursion limit exceeded during encoding."); } + RubySymbol typeBytesSymbol = RubySymbol.newSymbol(context.runtime, "TYPE_BYTES"); + // Handle the typical case where the fields.keySet contain the fieldDescriptors for (FieldDescriptor fieldDescriptor : fields.keySet()) { IRubyObject value = fields.get(fieldDescriptor); @@ -707,13 +712,12 @@ protected DynamicMessage build(ThreadContext context, int depth, int recursionLi * stringDefaultValue}. */ boolean isDefaultStringForBytes = false; - FieldDescriptor enumFieldDescriptorForType = - this.builder.getDescriptorForType().findFieldByName("type"); - String type = enumFieldDescriptorForType == null ? - null : fields.get(enumFieldDescriptorForType).toString(); - if (type != null && type.equals("TYPE_BYTES") && - fieldDescriptor.getFullName().equals("google.protobuf.FieldDescriptorProto.default_value")) { - isDefaultStringForBytes = true; + if (DEFAULT_VALUE.equals(fieldDescriptor.getFullName())) { + FieldDescriptor enumFieldDescriptorForType = + this.builder.getDescriptorForType().findFieldByName(TYPE); + if (typeBytesSymbol.equals(fields.get(enumFieldDescriptorForType))) { + isDefaultStringForBytes = true; + } } builder.setField(fieldDescriptor, convert(context, fieldDescriptor, value, depth, recursionLimit, isDefaultStringForBytes)); } diff --git a/ruby/tests/basic.rb b/ruby/tests/basic.rb index cdcd2950857a..4ff1c155834d 100755 --- a/ruby/tests/basic.rb +++ b/ruby/tests/basic.rb @@ -79,6 +79,34 @@ def test_issue_9440 assert_equal 8, msg.id end + def test_issue_9507 + pool = Google::Protobuf::DescriptorPool.new + pool.build do + add_message "NpeMessage" do + optional :type, :enum, 1, "TestEnum" + optional :other, :string, 2 + end + add_enum "TestEnum" do + value :Something, 0 + end + end + + msgclass = pool.lookup("NpeMessage").msgclass + + m = msgclass.new( + other: "foo" # must be set, but can be blank + ) + + begin + encoded = msgclass.encode(m) + rescue java.lang.NullPointerException => e + flunk "NPE rescued" + end + decoded = msgclass.decode(encoded) + decoded.inspect + decoded.to_proto + end + def test_has_field m = TestSingularFields.new assert !m.has_singular_msg? From 31a6482b8597099547769323878f27fef498e3d3 Mon Sep 17 00:00:00 2001 From: Jason Lunn Date: Mon, 28 Mar 2022 18:43:53 -0400 Subject: [PATCH 2/2] Implement `respond_to?` in RubyMessage (#9677) All synthetic methods implemented by `method_missing` are now supported by `respond_to?`. Fixes issue #9202. * Fix null pointer exceptions exposed by new regression tests. * Fix clear_ on oneofs so that it is safe to call repeatedly and so that respond_to? does not depend on whether the oneof is currently cleared. * Code cleanup: reenable more tests on JRuby. * Align JRuby behavior with CRuby by throwing a RuntimeError when attempting to assign to a oneof. (cherry picked from commit 8e7f93669612a06cafc39625f61258b9d6b77a3c) --- .../google/protobuf/jruby/RubyDescriptor.java | 2 + .../google/protobuf/jruby/RubyMessage.java | 99 ++++++++- ruby/tests/basic.rb | 65 ++++-- ruby/tests/basic_proto2.rb | 17 +- ruby/tests/common_tests.rb | 192 +++++++++++++++++- 5 files changed, 344 insertions(+), 31 deletions(-) diff --git a/ruby/src/main/java/com/google/protobuf/jruby/RubyDescriptor.java b/ruby/src/main/java/com/google/protobuf/jruby/RubyDescriptor.java index a59596a5b6b4..bfe7ec325540 100644 --- a/ruby/src/main/java/com/google/protobuf/jruby/RubyDescriptor.java +++ b/ruby/src/main/java/com/google/protobuf/jruby/RubyDescriptor.java @@ -209,6 +209,8 @@ public IRubyObject allocate(Ruby runtime, RubyClass klazz) { klass.include(new IRubyObject[] {messageExts}); klass.instance_variable_set(runtime.newString(Utils.DESCRIPTOR_INSTANCE_VAR), this); klass.defineAnnotatedMethods(RubyMessage.class); + // Workaround for https://github.com/jruby/jruby/issues/7154 + klass.searchMethod("respond_to?").setIsBuiltin(false); return klass; } diff --git a/ruby/src/main/java/com/google/protobuf/jruby/RubyMessage.java b/ruby/src/main/java/com/google/protobuf/jruby/RubyMessage.java index 0d76116097af..2167f15dc473 100644 --- a/ruby/src/main/java/com/google/protobuf/jruby/RubyMessage.java +++ b/ruby/src/main/java/com/google/protobuf/jruby/RubyMessage.java @@ -260,6 +260,86 @@ public IRubyObject eq(ThreadContext context, IRubyObject other) { return runtime.getTrue(); } + /* + * call-seq: + * Message.respond_to?(method_name, search_private_and_protected) => boolean + * + * Parallels method_missing, returning true when this object implements a method with the given + * method_name. + */ + @JRubyMethod(name="respond_to?", required = 1, optional = 1) + public IRubyObject respondTo(ThreadContext context, IRubyObject [] args) { + String methodName = args[0].asJavaString(); + if (descriptor.findFieldByName(methodName) != null) { + return context.runtime.getTrue(); + } + RubyDescriptor rubyDescriptor = (RubyDescriptor) getDescriptor(context, metaClass); + IRubyObject oneofDescriptor = rubyDescriptor.lookupOneof(context, args[0]); + if (!oneofDescriptor.isNil()) { + return context.runtime.getTrue(); + } + if (methodName.startsWith(CLEAR_PREFIX)) { + String strippedMethodName = methodName.substring(6); + oneofDescriptor = rubyDescriptor.lookupOneof(context, context.runtime.newSymbol(strippedMethodName)); + if (!oneofDescriptor.isNil()) { + return context.runtime.getTrue(); + } + + if (descriptor.findFieldByName(strippedMethodName) != null) { + return context.runtime.getTrue(); + } + } + if (methodName.startsWith(HAS_PREFIX) && methodName.endsWith(QUESTION_MARK)) { + String strippedMethodName = methodName.substring(4, methodName.length() - 1); + FieldDescriptor fieldDescriptor = descriptor.findFieldByName(strippedMethodName); + if (fieldDescriptor != null && + (!proto3 || fieldDescriptor.getContainingOneof() == null || fieldDescriptor + .getContainingOneof().isSynthetic()) && + fieldDescriptor.hasPresence()) { + return context.runtime.getTrue(); + } + oneofDescriptor = rubyDescriptor.lookupOneof(context, RubyString.newString(context.runtime, strippedMethodName)); + if (!oneofDescriptor.isNil()) { + return context.runtime.getTrue(); + } + } + if (methodName.endsWith(AS_VALUE_SUFFIX)) { + FieldDescriptor fieldDescriptor = descriptor.findFieldByName( + methodName.substring(0, methodName.length() - 9)); + if (fieldDescriptor != null && isWrappable(fieldDescriptor)) { + return context.runtime.getTrue(); + } + } + if (methodName.endsWith(CONST_SUFFIX)) { + FieldDescriptor fieldDescriptor = descriptor.findFieldByName( + methodName.substring(0, methodName.length() - 6)); + if (fieldDescriptor != null) { + if (fieldDescriptor.getType() == FieldDescriptor.Type.ENUM) { + return context.runtime.getTrue(); + } + } + } + if (methodName.endsWith(Utils.EQUAL_SIGN)) { + String strippedMethodName = methodName.substring(0, methodName.length() - 1); + FieldDescriptor fieldDescriptor = descriptor.findFieldByName(strippedMethodName); + if (fieldDescriptor != null) { + return context.runtime.getTrue(); + } + if (strippedMethodName.endsWith(AS_VALUE_SUFFIX)) { + strippedMethodName = methodName.substring(0, strippedMethodName.length() - 9); + fieldDescriptor = descriptor.findFieldByName(strippedMethodName); + if (fieldDescriptor != null && isWrappable(fieldDescriptor)) { + return context.runtime.getTrue(); + } + } + } + boolean includePrivate = false; + if (args.length == 2) { + includePrivate = context.runtime.getTrue().equals(args[1]); + } + return metaClass.respondsToMethod(methodName, includePrivate) ? context.runtime.getTrue() : context.runtime.getFalse(); + } + /* * call-seq: * Message.method_missing(*args) @@ -291,10 +371,9 @@ public IRubyObject eq(ThreadContext context, IRubyObject other) { public IRubyObject methodMissing(ThreadContext context, IRubyObject[] args) { Ruby runtime = context.runtime; String methodName = args[0].asJavaString(); + RubyDescriptor rubyDescriptor = (RubyDescriptor) getDescriptor(context, metaClass); if (args.length == 1) { - RubyDescriptor rubyDescriptor = (RubyDescriptor) getDescriptor(context, metaClass); - // If we find a Oneof return it's name (use lookupOneof because it has an index) IRubyObject oneofDescriptor = rubyDescriptor.lookupOneof(context, args[0]); @@ -331,9 +410,12 @@ public IRubyObject methodMissing(ThreadContext context, IRubyObject[] args) { if (methodName.startsWith(CLEAR_PREFIX)) { methodName = methodName.substring(6); oneofDescriptor = rubyDescriptor.lookupOneof(context, runtime.newSymbol(methodName)); - if (!oneofDescriptor.isNil()) { fieldDescriptor = oneofCases.get(((RubyOneofDescriptor) oneofDescriptor).getDescriptor()); + if (fieldDescriptor == null) { + // Clearing an already cleared oneof; return here to avoid NoMethodError. + return context.nil; + } } if (fieldDescriptor == null) { @@ -379,8 +461,7 @@ public IRubyObject methodMissing(ThreadContext context, IRubyObject[] args) { } else if (methodName.endsWith(CONST_SUFFIX)) { methodName = methodName.substring(0, methodName.length() - 6); fieldDescriptor = descriptor.findFieldByName(methodName); - - if (fieldDescriptor.getType() == FieldDescriptor.Type.ENUM) { + if (fieldDescriptor != null && fieldDescriptor.getType() == FieldDescriptor.Type.ENUM) { IRubyObject enumValue = getFieldInternal(context, fieldDescriptor); if (!enumValue.isNil()) { @@ -404,17 +485,21 @@ public IRubyObject methodMissing(ThreadContext context, IRubyObject[] args) { methodName = methodName.substring(0, methodName.length() - 1); // Trim equals sign FieldDescriptor fieldDescriptor = descriptor.findFieldByName(methodName); - if (fieldDescriptor != null) { return setFieldInternal(context, fieldDescriptor, args[1]); } + IRubyObject oneofDescriptor = rubyDescriptor.lookupOneof(context, RubyString.newString(context.runtime, methodName)); + if (!oneofDescriptor.isNil()) { + throw runtime.newRuntimeError("Oneof accessors are read-only."); + } + if (methodName.endsWith(AS_VALUE_SUFFIX)) { methodName = methodName.substring(0, methodName.length() - 9); fieldDescriptor = descriptor.findFieldByName(methodName); - if (fieldDescriptor != null) { + if (fieldDescriptor != null && isWrappable(fieldDescriptor)) { if (args[1].isNil()) { return setFieldInternal(context, fieldDescriptor, args[1]); } diff --git a/ruby/tests/basic.rb b/ruby/tests/basic.rb index 4ff1c155834d..5b781b7a692a 100755 --- a/ruby/tests/basic.rb +++ b/ruby/tests/basic.rb @@ -66,8 +66,11 @@ def test_issue_8311_crash def test_issue_8559_crash msg = TestMessage.new msg.repeated_int32 = ::Google::Protobuf::RepeatedField.new(:int32, [1, 2, 3]) - # TODO: Remove the platform check once https://github.com/jruby/jruby/issues/6818 is released in JRuby 9.3.0.0 - GC.start(full_mark: true, immediate_sweep: true) unless RUBY_PLATFORM == "java" + + # https://github.com/jruby/jruby/issues/6818 was fixed in JRuby 9.3.0.0 + if cruby_or_jruby_9_3_or_higher? + GC.start(full_mark: true, immediate_sweep: true) + end TestMessage.encode(msg) end @@ -99,7 +102,7 @@ def test_issue_9507 begin encoded = msgclass.encode(m) - rescue java.lang.NullPointerException => e + rescue java.lang.NullPointerException flunk "NPE rescued" end decoded = msgclass.decode(encoded) @@ -173,7 +176,7 @@ def test_set_clear_defaults m = TestSingularFields.new m.singular_int32 = -42 - assert_equal -42, m.singular_int32 + assert_equal( -42, m.singular_int32 ) m.clear_singular_int32 assert_equal 0, m.singular_int32 @@ -568,8 +571,6 @@ def test_to_h def test_json_maps - # TODO: Fix JSON in JRuby version. - return if RUBY_PLATFORM == "java" m = MapMessage.new(:map_string_int32 => {"a" => 1}) expected = {mapStringInt32: {a: 1}, mapStringMsg: {}, mapStringEnum: {}} expected_preserve = {map_string_int32: {a: 1}, map_string_msg: {}, map_string_enum: {}} @@ -583,8 +584,6 @@ def test_json_maps end def test_json_maps_emit_defaults_submsg - # TODO: Fix JSON in JRuby version. - return if RUBY_PLATFORM == "java" m = MapMessage.new(:map_string_msg => {"a" => TestMessage2.new(foo: 0)}) expected = {mapStringInt32: {}, mapStringMsg: {a: {foo: 0}}, mapStringEnum: {}} @@ -594,8 +593,6 @@ def test_json_maps_emit_defaults_submsg end def test_json_emit_defaults_submsg - # TODO: Fix JSON in JRuby version. - return if RUBY_PLATFORM == "java" m = TestSingularFields.new(singular_msg: proto_module::TestMessage2.new) expected = { @@ -618,8 +615,6 @@ def test_json_emit_defaults_submsg end def test_respond_to - # This test fails with JRuby 1.7.23, likely because of an old JRuby bug. - return if RUBY_PLATFORM == "java" msg = MapMessage.new assert msg.respond_to?(:map_string_int32) assert !msg.respond_to?(:bacon) @@ -694,5 +689,51 @@ def test_utf8 m2 = proto_module::TestMessage.decode(proto_module::TestMessage.encode(m)) assert_equal m2, m end + + def test_map_fields_respond_to? # regression test for issue 9202 + msg = proto_module::MapMessage.new + assert msg.respond_to?(:map_string_int32=) + msg.map_string_int32 = Google::Protobuf::Map.new(:string, :int32) + assert msg.respond_to?(:map_string_int32) + assert_equal( Google::Protobuf::Map.new(:string, :int32), msg.map_string_int32 ) + assert msg.respond_to?(:clear_map_string_int32) + msg.clear_map_string_int32 + + assert !msg.respond_to?(:has_map_string_int32?) + assert_raise NoMethodError do + msg.has_map_string_int32? + end + assert !msg.respond_to?(:map_string_int32_as_value) + assert_raise NoMethodError do + msg.map_string_int32_as_value + end + assert !msg.respond_to?(:map_string_int32_as_value=) + assert_raise NoMethodError do + msg.map_string_int32_as_value = :boom + end + end + end + + def test_oneof_fields_respond_to? # regression test for issue 9202 + msg = proto_module::OneofMessage.new + # `has_` prefix + "?" suffix actions should only work for oneofs fields. + assert msg.has_my_oneof? + assert msg.respond_to? :has_my_oneof? + assert !msg.respond_to?( :has_a? ) + assert_raise NoMethodError do + msg.has_a? + end + assert !msg.respond_to?( :has_b? ) + assert_raise NoMethodError do + msg.has_b? + end + assert !msg.respond_to?( :has_c? ) + assert_raise NoMethodError do + msg.has_c? + end + assert !msg.respond_to?( :has_d? ) + assert_raise NoMethodError do + msg.has_d? + end end end diff --git a/ruby/tests/basic_proto2.rb b/ruby/tests/basic_proto2.rb index a7114ea6edf5..5391c302ee93 100755 --- a/ruby/tests/basic_proto2.rb +++ b/ruby/tests/basic_proto2.rb @@ -142,7 +142,7 @@ def test_set_clear_defaults m = TestMessageDefaults.new m.optional_int32 = -42 - assert_equal -42, m.optional_int32 + assert_equal( -42, m.optional_int32 ) assert m.has_optional_int32? m.clear_optional_int32 assert_equal 1, m.optional_int32 @@ -255,5 +255,20 @@ def test_file_descriptor assert_equal "tests/basic_test_proto2.proto", file_descriptor.name assert_equal :proto2, file_descriptor.syntax end + + def test_oneof_fields_respond_to? # regression test for issue 9202 + msg = proto_module::OneofMessage.new(a: "foo") + # `has_` prefix + "?" suffix actions should only work for oneofs fields. + assert msg.respond_to? :has_my_oneof? + assert msg.has_my_oneof? + assert msg.respond_to? :has_a? + assert msg.has_a? + assert msg.respond_to? :has_b? + assert !msg.has_b? + assert msg.respond_to? :has_c? + assert !msg.has_c? + assert msg.respond_to? :has_d? + assert !msg.has_d? + end end end diff --git a/ruby/tests/common_tests.rb b/ruby/tests/common_tests.rb index 358846916dba..5918c8a8b136 100644 --- a/ruby/tests/common_tests.rb +++ b/ruby/tests/common_tests.rb @@ -796,7 +796,7 @@ def test_repeated_push m.repeated_string += %w[two three] assert_equal %w[one two three], m.repeated_string - m.repeated_string.push *['four', 'five'] + m.repeated_string.push( *['four', 'five'] ) assert_equal %w[one two three four five], m.repeated_string m.repeated_string.push 'six', 'seven' @@ -1085,8 +1085,6 @@ def test_reflection end def test_json - # TODO: Fix JSON in JRuby version. - return if RUBY_PLATFORM == "java" m = proto_module::TestMessage.new(:optional_int32 => 1234, :optional_int64 => -0x1_0000_0000, :optional_uint32 => 0x8000_0000, @@ -1288,6 +1286,7 @@ def test_wrappers_set_to_default m2 = proto_module::Wrapper.decode(m.to_proto) run_asserts.call(m2) m3 = proto_module::Wrapper.decode_json(m.to_json) + run_asserts.call(m3) end def test_wrapper_getters @@ -1539,8 +1538,6 @@ def test_wrapper_setters_as_value assert_nil m.bytes_as_value } - m = proto_module::Wrapper.new - m2 = proto_module::Wrapper.new( double: Google::Protobuf::DoubleValue.new(value: 2.0), float: Google::Protobuf::FloatValue.new(value: 4.0), @@ -1787,27 +1784,200 @@ def test_eq assert_nil h[m2] end + def cruby_or_jruby_9_3_or_higher? + # https://github.com/jruby/jruby/issues/6818 was fixed in JRuby 9.3.0.0 + match = RUBY_PLATFORM == "java" && + JRUBY_VERSION.match(/^(\d+)\.(\d+)\.\d+\.\d+$/) + match && (match[1].to_i > 9 || (match[1].to_i == 9 && match[2].to_i >= 3)) + end + def test_object_gc m = proto_module::TestMessage.new(optional_msg: proto_module::TestMessage2.new) m.optional_msg - # TODO: Remove the platform check once https://github.com/jruby/jruby/issues/6818 is released in JRuby 9.3.0.0 - GC.start(full_mark: true, immediate_sweep: true) unless RUBY_PLATFORM == "java" + # https://github.com/jruby/jruby/issues/6818 was fixed in JRuby 9.3.0.0 + GC.start(full_mark: true, immediate_sweep: true) if cruby_or_jruby_9_3_or_higher? m.optional_msg.inspect end def test_object_gc_freeze m = proto_module::TestMessage.new m.repeated_float.freeze - # TODO: Remove the platform check once https://github.com/jruby/jruby/issues/6818 is released in JRuby 9.3.0.0 - GC.start(full_mark: true) unless RUBY_PLATFORM == "java" + # https://github.com/jruby/jruby/issues/6818 was fixed in JRuby 9.3.0.0 + GC.start(full_mark: true) if cruby_or_jruby_9_3_or_higher? # Make sure we remember that the object is frozen. # The wrapper object contains this information, so we need to ensure that # the previous GC did not collect it. assert m.repeated_float.frozen? - # TODO: Remove the platform check once https://github.com/jruby/jruby/issues/6818 is released in JRuby 9.3.0.0 - GC.start(full_mark: true, immediate_sweep: true) unless RUBY_PLATFORM == "java" + # https://github.com/jruby/jruby/issues/6818 was fixed in JRuby 9.3.0.0 + GC.start(full_mark: true, immediate_sweep: true) if cruby_or_jruby_9_3_or_higher? assert m.repeated_float.frozen? end + + def test_optional_fields_respond_to? # regression test for issue 9202 + msg = proto_module::TestMessage.new + assert msg.respond_to? :optional_int32= + msg.optional_int32 = 42 + + assert msg.respond_to? :optional_int32 + assert_equal 42, msg.optional_int32 + + assert msg.respond_to? :clear_optional_int32 + msg.clear_optional_int32 + assert_equal 0, msg.optional_int32 + + assert msg.respond_to? :has_optional_int32? + assert !msg.has_optional_int32? + + assert !msg.respond_to?( :optional_int32_as_value= ) + assert_raise NoMethodError do + msg.optional_int32_as_value = 42 + end + + assert !msg.respond_to?( :optional_int32_as_value ) + assert_raise NoMethodError do + msg.optional_int32_as_value + end + + assert msg.respond_to? :optional_enum_const + assert_equal 0, msg.optional_enum_const + + assert !msg.respond_to?( :foo ) + assert_raise NoMethodError do + msg.foo + end + + assert !msg.respond_to?( :foo_const ) + assert_raise NoMethodError do + msg.foo_const + end + + assert !msg.respond_to?( :optional_int32_const ) + assert_raise NoMethodError do + msg.optional_int32_const + end + end + + def test_oneof_fields_respond_to? # regression test for issue 9202 + msg = proto_module::OneofMessage.new + + # names of the elements of a oneof and the oneof itself are valid actions. + assert msg.respond_to? :my_oneof + assert_nil msg.my_oneof + assert msg.respond_to? :a + assert_equal "", msg.a + assert msg.respond_to? :b + assert_equal 0, msg.b + assert msg.respond_to? :c + assert_nil msg.c + assert msg.respond_to? :d + assert_equal :Default, msg.d + + # `clear` prefix actions work on elements of a oneof and the oneof itself. + assert msg.respond_to? :clear_my_oneof + msg.clear_my_oneof + # Repeatedly clearing a oneof used to cause a NoMethodError under JRuby + msg.clear_my_oneof + assert msg.respond_to? :clear_a + msg.clear_a + assert msg.respond_to? :clear_b + msg.clear_b + assert msg.respond_to? :clear_c + msg.clear_c + assert msg.respond_to? :clear_d + msg.clear_d + + # `=` suffix actions should work on elements of a oneof but not the oneof itself. + assert !msg.respond_to?( :my_oneof= ) + error = assert_raise RuntimeError do + msg.my_oneof = nil + end + assert_equal "Oneof accessors are read-only.", error.message + assert msg.respond_to? :a= + msg.a = "foo" + assert msg.respond_to? :b= + msg.b = 42 + assert msg.respond_to? :c= + msg.c = proto_module::TestMessage2.new + assert msg.respond_to? :d= + msg.d = :Default + + # `has_` prefix + "?" suffix actions work for oneofs fields. + assert msg.respond_to? :has_my_oneof? + assert msg.has_my_oneof? + + # `_as_value` suffix actions should only work for wrapped fields. + assert !msg.respond_to?( :my_oneof_as_value ) + assert_raise NoMethodError do + msg.my_oneof_as_value + end + assert !msg.respond_to?( :a_as_value ) + assert_raise NoMethodError do + msg.a_as_value + end + assert !msg.respond_to?( :b_as_value ) + assert_raise NoMethodError do + msg.b_as_value + end + assert !msg.respond_to?( :c_as_value ) + assert_raise NoMethodError do + msg.c_as_value + end + assert !msg.respond_to?( :d_as_value ) + assert_raise NoMethodError do + msg.d_as_value + end + + # `_as_value=` suffix actions should only work for wrapped fields. + assert !msg.respond_to?( :my_oneof_as_value= ) + assert_raise NoMethodError do + msg.my_oneof_as_value = :boom + end + assert !msg.respond_to?( :a_as_value= ) + assert_raise NoMethodError do + msg.a_as_value = "" + end + assert !msg.respond_to?( :b_as_value= ) + assert_raise NoMethodError do + msg.b_as_value = 42 + end + assert !msg.respond_to?( :c_as_value= ) + assert_raise NoMethodError do + msg.c_as_value = proto_module::TestMessage2.new + end + assert !msg.respond_to?( :d_as_value= ) + assert_raise NoMethodError do + msg.d_as_value = :Default + end + + # `_const` suffix actions should only work for enum fields. + assert !msg.respond_to?( :my_oneof_const ) + assert_raise NoMethodError do + msg.my_oneof_const + end + assert !msg.respond_to?( :a_const ) + assert_raise NoMethodError do + msg.a_const + end + assert !msg.respond_to?( :b_const ) + assert_raise NoMethodError do + msg.b_const + end + assert !msg.respond_to?( :c_const ) + assert_raise NoMethodError do + msg.c_const + end + assert msg.respond_to? :d_const + assert_equal 0, msg.d_const + end + + def test_wrapped_fields_respond_to? # regression test for issue 9202 + msg = proto_module::Wrapper.new + assert msg.respond_to?( :double_as_value= ) + msg.double_as_value = 42 + assert msg.respond_to?( :double_as_value ) + assert_equal 42, msg.double_as_value + assert_equal Google::Protobuf::DoubleValue.new(value: 42), msg.double + end end