-- Protobuf text-format parser tests.
--
-- Decoder is descriptor-driven, so output is mode-independent; we still
-- run every assertion under both `full` and `runtime` generated modules
-- to confirm parity with the encoder/descriptor codegen.
local t = require('luatest')
local ffi = require('ffi')
local pb = require('pb')
local MODES = {'full', 'runtime'}
for _, mode in ipairs(MODES) do
local g = t.group('text_decode.' .. mode)
local hello = require(mode .. '.hello.hello_pb')
-- ---- scalars -------------------------------------------------------
g.test_basic_scalar = function()
local m = pb.text.decode(hello.Address_descriptor,
'street: "Pushkina 1"\ncity: "Moscow"\nzip: 123456\n')
t.assert_equals(m, {street = 'Pushkina 1', city = 'Moscow', zip = 123456})
end
g.test_empty = function()
t.assert_equals(pb.text.decode(hello.Address_descriptor, ''), {})
end
g.test_int64_lossless = function()
local m = pb.text.decode(hello.Person_descriptor,
'user_id: 18369917520866213889\n')
t.assert_equals(m.user_id, ffi.cast('uint64_t', 18369917520866213889ULL))
end
g.test_int_radixes = function()
local m = pb.text.decode(hello.Address_descriptor,
'zip: 0xff\n')
t.assert_equals(m.zip, 255)
m = pb.text.decode(hello.Address_descriptor, 'zip: 010\n')
t.assert_equals(m.zip, 8)
end
g.test_int_negative = function()
local m = pb.text.decode(hello.Address_descriptor, 'zip: -7\n')
t.assert_equals(m.zip, -7)
end
g.test_float_format = function()
local m = pb.text.decode(hello.Person_descriptor, 'weight_kg: 72.5\n')
t.assert_equals(m.weight_kg, 72.5)
end
g.test_float_specials = function()
local m = pb.text.decode(hello.Person_descriptor, 'weight_kg: inf\n')
t.assert_equals(m.weight_kg, math.huge)
m = pb.text.decode(hello.Person_descriptor, 'weight_kg: -INFINITY\n')
t.assert_equals(m.weight_kg, -math.huge)
m = pb.text.decode(hello.Person_descriptor, 'weight_kg: NaN\n')
t.assert_not_equals(m.weight_kg, m.weight_kg) -- NaN ~= NaN
end
g.test_float_trailing_f = function()
local m = pb.text.decode(hello.Person_descriptor, 'weight_kg: 1.5f\n')
t.assert_equals(m.weight_kg, 1.5)
end
g.test_string_escapes = function()
local m = pb.text.decode(hello.Person_descriptor,
'name: "a\\"b\\\\c\\nd\\te"\n')
t.assert_equals(m.name, 'a"b\\c\nd\te')
end
g.test_octal_and_hex_escapes = function()
local m = pb.text.decode(hello.Person_descriptor,
'avatar: "\\000\\001\\xff"\n')
t.assert_equals(m.avatar, '\x00\x01\xff')
end
g.test_unicode_escape = function()
-- ሴ is BMP, \U00010437 is supplementary plane.
local m = pb.text.decode(hello.Person_descriptor,
'name: "\\u00e9\\U00010437"\n')
-- U+00E9 -> 0xC3 0xA9; U+10437 -> 0xF0 0x90 0x90 0xB7.
t.assert_equals(m.name, '\xc3\xa9\xf0\x90\x90\xb7')
end
g.test_adjacent_string_concat = function()
local m = pb.text.decode(hello.Person_descriptor,
'name: "ab" "cd" "ef"\n')
t.assert_equals(m.name, 'abcdef')
end
g.test_comment_skipped = function()
local m = pb.text.decode(hello.Address_descriptor,
'# leading\nstreet: "S" # trailing\n')
t.assert_equals(m.street, 'S')
end
g.test_separator_semi_and_comma = function()
local m = pb.text.decode(hello.Address_descriptor,
'street: "S",\nzip: 1;\n')
t.assert_equals(m, {street = 'S', zip = 1})
end
g.test_double_semicolon_errors = function()
-- Mainline TextFormat rejects `;;` between fields (treats the
-- empty entry as a duplicate). Pins FieldSeparatorSemi*.
local ok, e = pcall(pb.text.decode, hello.Address_descriptor,
'street: "S";;\n')
t.assert_not(ok)
t.assert_str_contains(e, 'field-entry start')
end
-- ---- enums ---------------------------------------------------------
g.test_enum_by_name = function()
local m = pb.text.decode(hello.Person_descriptor, 'status: ERROR\n')
t.assert_equals(m.status, hello.Status.ERROR)
end
g.test_enum_by_number = function()
local m = pb.text.decode(hello.Person_descriptor, 'status: 2\n')
t.assert_equals(m.status, 2)
end
g.test_enum_unknown_name_errors = function()
local ok, err = pcall(pb.text.decode, hello.Person_descriptor,
'status: NOPE\n')
t.assert_not(ok)
t.assert_str_contains(err, 'unknown enum')
end
-- ---- repeated ------------------------------------------------------
g.test_repeated_long_form = function()
local m = pb.text.decode(hello.Person_descriptor,
'emails: "a"\nemails: "b"\nemails: "c"\n')
t.assert_equals(m.emails, {'a', 'b', 'c'})
end
g.test_repeated_short_form = function()
local m = pb.text.decode(hello.Person_descriptor,
'lucky_numbers: [1, 2, 3]\n')
t.assert_equals(m.lucky_numbers, {1, 2, 3})
end
g.test_repeated_short_empty = function()
local m = pb.text.decode(hello.Person_descriptor, 'lucky_numbers: []\n')
t.assert_equals(m.lucky_numbers, {})
end
g.test_repeated_short_separate_lists_append = function()
local m = pb.text.decode(hello.Person_descriptor,
'lucky_numbers: [1] lucky_numbers: [2, 3]\n')
t.assert_equals(m.lucky_numbers, {1, 2, 3})
end
-- ---- aggregates ----------------------------------------------------
g.test_nested_message_curly = function()
local m = pb.text.decode(hello.Person_descriptor,
'address { street: "S" zip: 1 }\n')
t.assert_equals(m.address, {street = 'S', zip = 1})
end
g.test_nested_message_angle = function()
local m = pb.text.decode(hello.Person_descriptor,
'address < street: "S" zip: 1 >\n')
t.assert_equals(m.address, {street = 'S', zip = 1})
end
g.test_nested_message_with_colon = function()
local m = pb.text.decode(hello.Person_descriptor,
'address: { street: "S" }\n')
t.assert_equals(m.address.street, 'S')
end
-- ---- maps ----------------------------------------------------------
g.test_map_entry_single = function()
local m = pb.text.decode(hello.Person_descriptor,
'ages_by_nickname { key: "alice" value: 30 }\n')
t.assert_equals(m.ages_by_nickname, {alice = 30})
end
g.test_map_entry_multi = function()
local m = pb.text.decode(hello.Person_descriptor,
'ages_by_nickname { key: "a" value: 1 }\n' ..
'ages_by_nickname { key: "b" value: 2 }\n')
t.assert_equals(m.ages_by_nickname, {a = 1, b = 2})
end
g.test_map_message_value = function()
local m = pb.text.decode(hello.Person_descriptor,
'addresses_by_label { key: "home" value { street: "S" zip: 1 } }\n')
t.assert_equals(m.addresses_by_label.home, {street = 'S', zip = 1})
end
-- ---- oneof ---------------------------------------------------------
g.test_oneof_last_wins = function()
-- Setting two `outcome` members in sequence keeps only the last;
-- `id` is outside the oneof so it survives untouched.
local m = pb.text.decode(hello.Result_descriptor,
'id: 7\ntext: "first"\ncode: 99\n')
t.assert_equals(m.id, 7)
t.assert_equals(m.code, 99)
t.assert_equals(m.text, nil)
end
-- ---- error surface -------------------------------------------------
g.test_unknown_field_errors_by_default = function()
local ok, err = pcall(pb.text.decode, hello.Address_descriptor,
'street: "S"\nbogus: 1\n')
t.assert_not(ok)
t.assert_str_contains(err, 'unknown field')
end
g.test_unknown_field_dropped_when_allowed = function()
local m = pb.text.decode(hello.Address_descriptor,
'street: "S"\nbogus: 1\n', {allow_unknown_fields = true})
t.assert_equals(m, {street = 'S'})
end
g.test_unknown_numeric_id_dropped = function()
local m = pb.text.decode(hello.Address_descriptor,
'street: "S"\n9999: 42\n')
t.assert_equals(m, {street = 'S'})
end
-- ---- round-trip with the encoder ----------------------------------
g.test_round_trip_encode_decode = function()
local src = {
name = 'Alice',
user_id = ffi.cast('uint64_t', 42),
emails = {'a@x', 'b@x'},
address = {street = 'Main', city = 'NYC', zip = 10001},
status = hello.Status.ACTIVE,
ages_by_nickname = {al = 30},
}
local text = pb.text.encode(hello.Person_descriptor, src)
local decoded = pb.text.decode(hello.Person_descriptor, text)
-- Compare via re-encode for cdata equality stability.
t.assert_equals(pb.encode(hello.Person_descriptor, decoded),
pb.encode(hello.Person_descriptor, src))
end
end