-- Proto2 round-trip + required-field validation, parametrized over both
-- codegen modes. The fixture is generated from test/proto/proto2_basic.proto
-- by `just gen-proto2-tests`.
local t = require('luatest')
local ffi = require('ffi')
local function hex(s)
local out = {}
for i = 1, #s do out[i] = string.format('%02x', s:byte(i)) end
return table.concat(out)
end
local MODES = {'full', 'runtime'}
for _, mode in ipairs(MODES) do
local g = t.group('proto2_basic.' .. mode)
local pb = require(mode .. '.proto2_basic.proto2_basic_pb')
-- ----- Defaults: explicit-optional fields with [default = X] -----
g.test_defaults_descriptor_carries_default_value = function()
local d = pb.Defaults_descriptor.field_by_name
t.assert_equals(d.i.default_value, 17)
t.assert_equals(d.s.default_value, 'hello')
t.assert_equals(d.b.default_value, true)
t.assert_equals(d.f.default_value, 3.5)
t.assert_equals(d.d.default_value, 1.5)
t.assert(d.i64.default_value == ffi.cast('int64_t', 1234567890123),
'int64 default cdata equality')
t.assert(d.u64.default_value == ffi.cast('uint64_t', 17),
'uint64 default cdata equality')
t.assert_equals(d.by.default_value, '\x00\xff')
t.assert_equals(d.color.default_value, 'GREEN')
end
g.test_empty_message_round_trips_to_empty_bytes = function()
-- All proto2 fields are presence-tracked. An empty Lua table has no
-- fields set, so encode produces zero bytes (no defaults serialized).
local enc = pb.Defaults_encode({})
t.assert_equals(enc, '', 'no defaults on the wire')
t.assert_equals(pb.Defaults_decode(enc), {})
end
g.test_set_value_round_trip = function()
local val = {i = 42, s = 'world', b = false, f = -1.5}
local dec = pb.Defaults_decode(pb.Defaults_encode(val))
t.assert_equals(dec.i, 42)
t.assert_equals(dec.s, 'world')
t.assert_equals(dec.b, false)
t.assert_equals(dec.f, -1.5)
-- d/i64/u64/by/color stay absent.
t.assert_equals(dec.d, nil)
t.assert_equals(dec.color, nil)
end
g.test_set_to_proto_default_still_serializes = function()
-- proto2 presence means setting a field to its declared default
-- still emits it on the wire (no proto3-style elision).
local enc = pb.Defaults_encode({i = 17})
t.assert_not_equals(enc, '', 'presence-tracked field at default must serialize')
t.assert_equals(pb.Defaults_decode(enc).i, 17)
end
-- ----- Cardinality: required vs optional vs repeated -----
g.test_required_missing_errors = function()
local ok, err = pcall(pb.Cardinality_encode, {})
t.assert_equals(ok, false)
t.assert_str_contains(err, 'required field missing on encode')
t.assert_str_contains(err, 'proto2_basic.Cardinality.r')
end
g.test_required_zero_emitted = function()
-- Required field at proto2-default zero must still be on the wire.
local enc = pb.Cardinality_encode({r = 0})
t.assert_equals(hex(enc), '0800',
'required int32=0: tag 1/VARINT + varint 0')
end
g.test_required_set = function()
local enc = pb.Cardinality_encode({r = 7})
local dec = pb.Cardinality_decode(enc)
t.assert_equals(dec.r, 7)
end
g.test_repeated_unpacked_by_default = function()
-- Proto2 default for repeated scalars is NOT packed.
local enc = pb.Cardinality_encode({r = 0, packed_default = {1, 2, 3}})
-- field 3, wire VARINT (0x18) repeated three times.
t.assert_equals(hex(enc), '0800' .. '180118021803')
end
g.test_repeated_explicit_packed = function()
local enc = pb.Cardinality_encode({r = 0, explicitly_packed = {1, 2, 3}})
-- field 4, wire LEN (0x22), len=3, varints 1,2,3.
t.assert_equals(hex(enc), '0800' .. '2203' .. '010203')
end
g.test_repeated_explicit_unpacked = function()
local enc = pb.Cardinality_encode({r = 0, explicitly_unpacked = {1, 2, 3}})
-- field 5, wire VARINT (0x28) repeated three times.
t.assert_equals(hex(enc), '0800' .. '280128022803')
end
g.test_repeated_decode_accepts_both_packed_and_unpacked = function()
-- A wire stream with packed_default encoded as packed (legal — proto
-- consumers must accept either form) decodes the same way as unpacked.
local packed = '\x08\x00\x1a\x03\x01\x02\x03' -- field 3 with LEN
local unpacked = '\x08\x00\x18\x01\x18\x02\x18\x03'
local a = pb.Cardinality_decode(packed)
local b = pb.Cardinality_decode(unpacked)
t.assert_equals(a.packed_default, {1, 2, 3})
t.assert_equals(b.packed_default, {1, 2, 3})
end
-- ----- Nested required message -----
g.test_nested_required_message_missing_errors = function()
local ok, err = pcall(pb.Nested_encode, {})
t.assert_equals(ok, false)
t.assert_str_contains(err, 'required field missing on encode')
t.assert_str_contains(err, 'proto2_basic.Nested.inner')
end
g.test_nested_required_inner_required = function()
-- Inner message also has its own required field (x). When Inner
-- itself is required on the outer message, missing it errors on the
-- inner encode call.
local ok, err = pcall(pb.Nested_encode, {inner = {}})
t.assert_equals(ok, false)
t.assert_str_contains(err, 'proto2_basic.Nested.Inner.x')
end
g.test_nested_required_filled_round_trips = function()
local val = {inner = {x = 5}, inner_opt = {x = 9}}
local dec = pb.Nested_decode(pb.Nested_encode(val))
t.assert_equals(dec.inner.x, 5)
t.assert_equals(dec.inner_opt.x, 9)
end
-- ----- Enum default surfaced in descriptor -----
g.test_enum_default_descriptor = function()
local f = pb.Defaults_descriptor.field_by_name.color
t.assert_equals(f.default_value, 'GREEN')
-- The enum descriptor itself round-trips the symbolic name.
t.assert_equals(pb.Defaults_Color_descriptor.by_name['GREEN'], 1)
end
end
-- Parity: full and runtime modes must produce byte-identical output for
-- the same input. Equivalent to the existing parity.full_vs_runtime group.
do
local g = t.group('proto2_basic.parity')
local full = require('full.proto2_basic.proto2_basic_pb')
local runtime = require('runtime.proto2_basic.proto2_basic_pb')
local function check_parity(msg, encode_full, encode_runtime, value)
local bf, br = encode_full(value), encode_runtime(value)
t.assert_equals(hex(bf), hex(br),
msg .. ': full and runtime modes must produce identical bytes')
end
g.test_defaults_round_trip_parity = function()
check_parity('Defaults set values', full.Defaults_encode,
runtime.Defaults_encode,
{i = 42, s = 'world', b = true, f = 0.5, d = -1.5})
end
g.test_cardinality_required_parity = function()
check_parity('Cardinality.r=0', full.Cardinality_encode,
runtime.Cardinality_encode, {r = 0})
check_parity('Cardinality full', full.Cardinality_encode,
runtime.Cardinality_encode, {
r = 1,
o = 2,
packed_default = {3, 4, 5},
explicitly_packed = {6, 7, 8},
explicitly_unpacked = {9, 10},
})
end
g.test_nested_parity = function()
check_parity('Nested', full.Nested_encode, runtime.Nested_encode,
{inner = {x = 5}, inner_opt = {x = 9}})
end
end