protobuf/ruby/tests/basic.rb
Joshua Haberman 1a74ba4cb4
Fix for wrappers with a zero value (#7195)
* Add failing tests for issues with wrapped values where the value is the default

* Add test for wrapped values without a value set

* Bugfix for wrapper types with default values.

The previous optimizations for wrapper types had a bug that prevented
wrappers from registering as "present" if the "value" field was not
present on the wire.

In practice the "value" field will not be serialized when it is zero,
according to proto3 semantics, but due to the optimization this
prevented it from creating a new object to represent the presence of the
field.

The fix is to ensure that if the wrapper message is present on the wire,
we always initialize its value to zero.

Co-authored-by: Dan Quan <dan@quan.io>
2020-02-11 08:20:00 -08:00

526 lines
18 KiB
Ruby

#!/usr/bin/ruby
# basic_test_pb.rb is in the same directory as this test.
$LOAD_PATH.unshift(File.expand_path(File.dirname(__FILE__)))
require 'basic_test_pb'
require 'common_tests'
require 'google/protobuf'
require 'json'
require 'test/unit'
# ------------- generated code --------------
module BasicTest
pool = Google::Protobuf::DescriptorPool.new
pool.build do
add_message "BadFieldNames" do
optional :dup, :int32, 1
optional :class, :int32, 2
end
end
BadFieldNames = pool.lookup("BadFieldNames").msgclass
# ------------ test cases ---------------
class MessageContainerTest < Test::Unit::TestCase
# Required by CommonTests module to resolve proto3 proto classes used in tests.
def proto_module
::BasicTest
end
include CommonTests
def test_has_field
m = TestMessage.new
assert !m.has_optional_msg?
m.optional_msg = TestMessage2.new
assert m.has_optional_msg?
assert TestMessage.descriptor.lookup('optional_msg').has?(m)
m = OneofMessage.new
assert !m.has_my_oneof?
m.a = "foo"
assert m.has_my_oneof?
assert_raise NoMethodError do
m.has_a?
end
assert_raise ArgumentError do
OneofMessage.descriptor.lookup('a').has?(m)
end
m = TestMessage.new
assert_raise NoMethodError do
m.has_optional_int32?
end
assert_raise ArgumentError do
TestMessage.descriptor.lookup('optional_int32').has?(m)
end
assert_raise NoMethodError do
m.has_optional_string?
end
assert_raise ArgumentError do
TestMessage.descriptor.lookup('optional_string').has?(m)
end
assert_raise NoMethodError do
m.has_optional_bool?
end
assert_raise ArgumentError do
TestMessage.descriptor.lookup('optional_bool').has?(m)
end
assert_raise NoMethodError do
m.has_repeated_msg?
end
assert_raise ArgumentError do
TestMessage.descriptor.lookup('repeated_msg').has?(m)
end
end
def test_set_clear_defaults
m = TestMessage.new
m.optional_int32 = -42
assert_equal -42, m.optional_int32
m.clear_optional_int32
assert_equal 0, m.optional_int32
m.optional_int32 = 50
assert_equal 50, m.optional_int32
TestMessage.descriptor.lookup('optional_int32').clear(m)
assert_equal 0, m.optional_int32
m.optional_string = "foo bar"
assert_equal "foo bar", m.optional_string
m.clear_optional_string
assert_equal "", m.optional_string
m.optional_string = "foo"
assert_equal "foo", m.optional_string
TestMessage.descriptor.lookup('optional_string').clear(m)
assert_equal "", m.optional_string
m.optional_msg = TestMessage2.new(:foo => 42)
assert_equal TestMessage2.new(:foo => 42), m.optional_msg
assert m.has_optional_msg?
m.clear_optional_msg
assert_equal nil, m.optional_msg
assert !m.has_optional_msg?
m.optional_msg = TestMessage2.new(:foo => 42)
assert_equal TestMessage2.new(:foo => 42), m.optional_msg
TestMessage.descriptor.lookup('optional_msg').clear(m)
assert_equal nil, m.optional_msg
m.repeated_int32.push(1)
assert_equal [1], m.repeated_int32
m.clear_repeated_int32
assert_equal [], m.repeated_int32
m.repeated_int32.push(1)
assert_equal [1], m.repeated_int32
TestMessage.descriptor.lookup('repeated_int32').clear(m)
assert_equal [], m.repeated_int32
m = OneofMessage.new
m.a = "foo"
assert_equal "foo", m.a
assert m.has_my_oneof?
m.clear_a
assert !m.has_my_oneof?
m.a = "foobar"
assert m.has_my_oneof?
m.clear_my_oneof
assert !m.has_my_oneof?
m.a = "bar"
assert_equal "bar", m.a
assert m.has_my_oneof?
OneofMessage.descriptor.lookup('a').clear(m)
assert !m.has_my_oneof?
end
def test_initialization_map_errors
e = assert_raise ArgumentError do
TestMessage.new(:hello => "world")
end
assert_match(/hello/, e.message)
e = assert_raise ArgumentError do
MapMessage.new(:map_string_int32 => "hello")
end
assert_equal e.message, "Expected Hash object as initializer value for map field 'map_string_int32' (given String)."
e = assert_raise ArgumentError do
TestMessage.new(:repeated_uint32 => "hello")
end
assert_equal e.message, "Expected array as initializer value for repeated field 'repeated_uint32' (given String)."
end
def test_map_field
m = MapMessage.new
assert m.map_string_int32 == {}
assert m.map_string_msg == {}
m = MapMessage.new(
:map_string_int32 => {"a" => 1, "b" => 2},
:map_string_msg => {"a" => TestMessage2.new(:foo => 1),
"b" => TestMessage2.new(:foo => 2)},
:map_string_enum => {"a" => :A, "b" => :B})
assert m.map_string_int32.keys.sort == ["a", "b"]
assert m.map_string_int32["a"] == 1
assert m.map_string_msg["b"].foo == 2
assert m.map_string_enum["a"] == :A
m.map_string_int32["c"] = 3
assert m.map_string_int32["c"] == 3
m.map_string_msg["c"] = TestMessage2.new(:foo => 3)
assert m.map_string_msg["c"] == TestMessage2.new(:foo => 3)
m.map_string_msg.delete("b")
m.map_string_msg.delete("c")
assert m.map_string_msg == { "a" => TestMessage2.new(:foo => 1) }
assert_raise Google::Protobuf::TypeError do
m.map_string_msg["e"] = TestMessage.new # wrong value type
end
# ensure nothing was added by the above
assert m.map_string_msg == { "a" => TestMessage2.new(:foo => 1) }
m.map_string_int32 = Google::Protobuf::Map.new(:string, :int32)
assert_raise Google::Protobuf::TypeError do
m.map_string_int32 = Google::Protobuf::Map.new(:string, :int64)
end
assert_raise Google::Protobuf::TypeError do
m.map_string_int32 = {}
end
assert_raise TypeError do
m = MapMessage.new(:map_string_int32 => { 1 => "I am not a number" })
end
end
def test_map_field_with_symbol
m = MapMessage.new
assert m.map_string_int32 == {}
assert m.map_string_msg == {}
m = MapMessage.new(
:map_string_int32 => {a: 1, "b" => 2},
:map_string_msg => {a: TestMessage2.new(:foo => 1),
b: TestMessage2.new(:foo => 10)})
assert_equal 1, m.map_string_int32[:a]
assert_equal 2, m.map_string_int32[:b]
assert_equal 10, m.map_string_msg[:b].foo
end
def test_map_inspect
m = MapMessage.new(
:map_string_int32 => {"a" => 1, "b" => 2},
:map_string_msg => {"a" => TestMessage2.new(:foo => 1),
"b" => TestMessage2.new(:foo => 2)},
:map_string_enum => {"a" => :A, "b" => :B})
expected = "<BasicTest::MapMessage: map_string_int32: {\"b\"=>2, \"a\"=>1}, map_string_msg: {\"b\"=><BasicTest::TestMessage2: foo: 2>, \"a\"=><BasicTest::TestMessage2: foo: 1>}, map_string_enum: {\"b\"=>:B, \"a\"=>:A}>"
assert_equal expected, m.inspect
end
def test_map_corruption
# This pattern led to a crash in a previous version of upb/protobuf.
m = MapMessage.new(map_string_int32: { "aaa" => 1 })
m.map_string_int32['podid'] = 2
m.map_string_int32['aaa'] = 3
end
def test_map_wrappers
run_asserts = ->(m) {
assert_equal 2.0, m.map_double[0].value
assert_equal 4.0, m.map_float[0].value
assert_equal 3, m.map_int32[0].value
assert_equal 4, m.map_int64[0].value
assert_equal 5, m.map_uint32[0].value
assert_equal 6, m.map_uint64[0].value
assert_equal true, m.map_bool[0].value
assert_equal 'str', m.map_string[0].value
assert_equal 'fun', m.map_bytes[0].value
}
m = proto_module::Wrapper.new(
map_double: {0 => Google::Protobuf::DoubleValue.new(value: 2.0)},
map_float: {0 => Google::Protobuf::FloatValue.new(value: 4.0)},
map_int32: {0 => Google::Protobuf::Int32Value.new(value: 3)},
map_int64: {0 => Google::Protobuf::Int64Value.new(value: 4)},
map_uint32: {0 => Google::Protobuf::UInt32Value.new(value: 5)},
map_uint64: {0 => Google::Protobuf::UInt64Value.new(value: 6)},
map_bool: {0 => Google::Protobuf::BoolValue.new(value: true)},
map_string: {0 => Google::Protobuf::StringValue.new(value: 'str')},
map_bytes: {0 => Google::Protobuf::BytesValue.new(value: 'fun')},
)
run_asserts.call(m)
serialized = proto_module::Wrapper::encode(m)
m2 = proto_module::Wrapper::decode(serialized)
run_asserts.call(m2)
# Test the case where we are serializing directly from the parsed form
# (before anything lazy is materialized).
m3 = proto_module::Wrapper::decode(serialized)
serialized2 = proto_module::Wrapper::encode(m3)
m4 = proto_module::Wrapper::decode(serialized2)
run_asserts.call(m4)
# Test that the lazy form compares equal to the expanded form.
m5 = proto_module::Wrapper::decode(serialized2)
assert_equal m5, m
end
def test_map_wrappers_with_default_values
run_asserts = ->(m) {
assert_equal 0.0, m.map_double[0].value
assert_equal 0.0, m.map_float[0].value
assert_equal 0, m.map_int32[0].value
assert_equal 0, m.map_int64[0].value
assert_equal 0, m.map_uint32[0].value
assert_equal 0, m.map_uint64[0].value
assert_equal false, m.map_bool[0].value
assert_equal '', m.map_string[0].value
assert_equal '', m.map_bytes[0].value
}
m = proto_module::Wrapper.new(
map_double: {0 => Google::Protobuf::DoubleValue.new(value: 0.0)},
map_float: {0 => Google::Protobuf::FloatValue.new(value: 0.0)},
map_int32: {0 => Google::Protobuf::Int32Value.new(value: 0)},
map_int64: {0 => Google::Protobuf::Int64Value.new(value: 0)},
map_uint32: {0 => Google::Protobuf::UInt32Value.new(value: 0)},
map_uint64: {0 => Google::Protobuf::UInt64Value.new(value: 0)},
map_bool: {0 => Google::Protobuf::BoolValue.new(value: false)},
map_string: {0 => Google::Protobuf::StringValue.new(value: '')},
map_bytes: {0 => Google::Protobuf::BytesValue.new(value: '')},
)
run_asserts.call(m)
serialized = proto_module::Wrapper::encode(m)
m2 = proto_module::Wrapper::decode(serialized)
run_asserts.call(m2)
# Test the case where we are serializing directly from the parsed form
# (before anything lazy is materialized).
m3 = proto_module::Wrapper::decode(serialized)
serialized2 = proto_module::Wrapper::encode(m3)
m4 = proto_module::Wrapper::decode(serialized2)
run_asserts.call(m4)
# Test that the lazy form compares equal to the expanded form.
m5 = proto_module::Wrapper::decode(serialized2)
assert_equal m5, m
end
def test_map_wrappers_with_no_value
run_asserts = ->(m) {
assert_equal 0.0, m.map_double[0].value
assert_equal 0.0, m.map_float[0].value
assert_equal 0, m.map_int32[0].value
assert_equal 0, m.map_int64[0].value
assert_equal 0, m.map_uint32[0].value
assert_equal 0, m.map_uint64[0].value
assert_equal false, m.map_bool[0].value
assert_equal '', m.map_string[0].value
assert_equal '', m.map_bytes[0].value
}
m = proto_module::Wrapper.new(
map_double: {0 => Google::Protobuf::DoubleValue.new()},
map_float: {0 => Google::Protobuf::FloatValue.new()},
map_int32: {0 => Google::Protobuf::Int32Value.new()},
map_int64: {0 => Google::Protobuf::Int64Value.new()},
map_uint32: {0 => Google::Protobuf::UInt32Value.new()},
map_uint64: {0 => Google::Protobuf::UInt64Value.new()},
map_bool: {0 => Google::Protobuf::BoolValue.new()},
map_string: {0 => Google::Protobuf::StringValue.new()},
map_bytes: {0 => Google::Protobuf::BytesValue.new()},
)
run_asserts.call(m)
serialized = proto_module::Wrapper::encode(m)
m2 = proto_module::Wrapper::decode(serialized)
run_asserts.call(m2)
# Test the case where we are serializing directly from the parsed form
# (before anything lazy is materialized).
m3 = proto_module::Wrapper::decode(serialized)
serialized2 = proto_module::Wrapper::encode(m3)
m4 = proto_module::Wrapper::decode(serialized2)
run_asserts.call(m4)
end
def test_concurrent_decoding
o = Outer.new
o.items[0] = Inner.new
raw = Outer.encode(o)
thds = 2.times.map do
Thread.new do
100000.times do
assert_equal o, Outer.decode(raw)
end
end
end
thds.map(&:join)
end
def test_map_encode_decode
m = MapMessage.new(
:map_string_int32 => {"a" => 1, "b" => 2},
:map_string_msg => {"a" => TestMessage2.new(:foo => 1),
"b" => TestMessage2.new(:foo => 2)},
:map_string_enum => {"a" => :A, "b" => :B})
m2 = MapMessage.decode(MapMessage.encode(m))
assert m == m2
m3 = MapMessageWireEquiv.decode(MapMessage.encode(m))
assert m3.map_string_int32.length == 2
kv = {}
m3.map_string_int32.map { |msg| kv[msg.key] = msg.value }
assert kv == {"a" => 1, "b" => 2}
kv = {}
m3.map_string_msg.map { |msg| kv[msg.key] = msg.value }
assert kv == {"a" => TestMessage2.new(:foo => 1),
"b" => TestMessage2.new(:foo => 2)}
end
def test_protobuf_decode_json_ignore_unknown_fields
m = TestMessage.decode_json({
optional_string: "foo",
not_in_message: "some_value"
}.to_json, { ignore_unknown_fields: true })
assert_equal m.optional_string, "foo"
e = assert_raise Google::Protobuf::ParseError do
TestMessage.decode_json({ not_in_message: "some_value" }.to_json)
end
assert_match(/No such field: not_in_message/, e.message)
end
#def test_json_quoted_string
# m = TestMessage.decode_json(%q(
# "optionalInt64": "1",,
# }))
# puts(m)
# assert_equal 1, m.optional_int32
#end
def test_to_h
m = TestMessage.new(:optional_bool => true, :optional_double => -10.100001, :optional_string => 'foo', :repeated_string => ['bar1', 'bar2'], :repeated_msg => [TestMessage2.new(:foo => 100)])
expected_result = {
:optional_bool=>true,
:optional_bytes=>"",
:optional_double=>-10.100001,
:optional_enum=>:Default,
:optional_float=>0.0,
:optional_int32=>0,
:optional_int64=>0,
:optional_msg=>nil,
:optional_string=>"foo",
:optional_uint32=>0,
:optional_uint64=>0,
:repeated_bool=>[],
:repeated_bytes=>[],
:repeated_double=>[],
:repeated_enum=>[],
:repeated_float=>[],
:repeated_int32=>[],
:repeated_int64=>[],
:repeated_msg=>[{:foo => 100}],
:repeated_string=>["bar1", "bar2"],
:repeated_uint32=>[],
:repeated_uint64=>[]
}
assert_equal expected_result, m.to_h
m = MapMessage.new(
:map_string_int32 => {"a" => 1, "b" => 2},
:map_string_msg => {"a" => TestMessage2.new(:foo => 1),
"b" => TestMessage2.new(:foo => 2)},
:map_string_enum => {"a" => :A, "b" => :B})
expected_result = {
:map_string_int32 => {"a" => 1, "b" => 2},
:map_string_msg => {"a" => {:foo => 1}, "b" => {:foo => 2}},
:map_string_enum => {"a" => :A, "b" => :B}
}
assert_equal expected_result, m.to_h
end
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: {}}
assert_equal JSON.parse(MapMessage.encode_json(m), :symbolize_names => true), expected
json = MapMessage.encode_json(m, :preserve_proto_fieldnames => true)
assert_equal JSON.parse(json, :symbolize_names => true), expected_preserve
m2 = MapMessage.decode_json(MapMessage.encode_json(m))
assert_equal m, m2
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})
expected = {mapStringInt32: {}, mapStringMsg: {a: {foo: 0}}, mapStringEnum: {}}
actual = MapMessage.encode_json(m, :emit_defaults => true)
assert_equal JSON.parse(actual, :symbolize_names => true), expected
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)
end
def test_file_descriptor
file_descriptor = TestMessage.descriptor.file_descriptor
assert nil != file_descriptor
assert_equal "tests/basic_test.proto", file_descriptor.name
assert_equal :proto3, file_descriptor.syntax
file_descriptor = TestEnum.descriptor.file_descriptor
assert nil != file_descriptor
assert_equal "tests/basic_test.proto", file_descriptor.name
assert_equal :proto3, file_descriptor.syntax
end
# Ruby 2.5 changed to raise FrozenError instead of RuntimeError
FrozenErrorType = Gem::Version.new(RUBY_VERSION) < Gem::Version.new('2.5') ? RuntimeError : FrozenError
def test_map_freeze
m = proto_module::MapMessage.new
m.map_string_int32['a'] = 5
m.map_string_msg['b'] = proto_module::TestMessage2.new
m.map_string_int32.freeze
m.map_string_msg.freeze
assert m.map_string_int32.frozen?
assert m.map_string_msg.frozen?
assert_raise(FrozenErrorType) { m.map_string_int32['foo'] = 1 }
assert_raise(FrozenErrorType) { m.map_string_msg['bar'] = proto_module::TestMessage2.new }
assert_raise(FrozenErrorType) { m.map_string_int32.delete('a') }
assert_raise(FrozenErrorType) { m.map_string_int32.clear }
end
end
end