1a74ba4cb4
* 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>
526 lines
18 KiB
Ruby
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
|