-- Test for bd-mz6 / ra6 3c: C-side scalar decode.
--
-- Only runs when PB_ENABLE_C=1 is set in the environment AND the C
-- runtime module is loadable. Otherwise the group is skipped, mirroring
-- the gate on c_runtime_encode_test.lua.
--
-- Acceptance per bd-mz6:
-- Person decode round-trip works for the bytes produced by 3b; the
-- decoded table is shape-identical to pure-Lua decode for the same
-- input bytes (both codegen modes).
local t = require('luatest')
local ffi = require('ffi')
local pb = require('pb')
local c_runtime = pb.c_runtime
local function skip_if_no_c()
if c_runtime == nil then
t.skip('PB_ENABLE_C not set or pb.c_runtime not available')
end
end
-- mode=full is the byte-equal reference for both encode and decode.
local full_hello
for _, mode in ipairs({'full', 'runtime'}) do
local g = t.group('c_runtime_decode.' .. mode)
local hello
g.before_all(function()
skip_if_no_c()
hello = require(mode .. '.hello.hello_pb')
full_hello = require('full.hello.hello_pb')
end)
g.before_each(skip_if_no_c)
-- ---------- Acceptance per bd-mz6 ----------
function g.test_acceptance_person_round_trip_from_3b_bytes()
local msg = {
name = 'x',
age = 42,
balance = -7,
user_id = 0xDEADBEEFCAFEBABEULL,
weight_kg = 3.14,
}
local plan = c_runtime.compile_plan(hello.Person_descriptor)
local bytes = c_runtime.encode(plan, msg)
local c_decoded = c_runtime.decode(plan, bytes)
local lua_decoded = full_hello.Person_decode(bytes)
t.assert_equals(c_decoded, lua_decoded,
'C decode matches mode=full pure-Lua decode shape-for-shape')
end
-- ---------- Per-kind coverage ----------
function g.test_empty_input_produces_empty_table()
local plan = c_runtime.compile_plan(hello.Address_descriptor)
t.assert_equals(c_runtime.decode(plan, ''), {})
end
function g.test_address_strings_and_int32()
local plan = c_runtime.compile_plan(hello.Address_descriptor)
local msg = {street = 'Main', city = 'Springfield', zip = 12345}
local bytes = full_hello.Address_encode(msg)
t.assert_equals(c_runtime.decode(plan, bytes),
full_hello.Address_decode(bytes))
end
function g.test_proto3_optional_empty_string_present()
-- Address.apartment is proto3-optional; presence beats default.
-- 3b emits the tag for apartment='' (optional bypasses suppression),
-- so decode must surface the empty string in the result table.
local plan = c_runtime.compile_plan(hello.Address_descriptor)
local bytes = full_hello.Address_encode({apartment = ''})
local lua_decoded = full_hello.Address_decode(bytes)
local c_decoded = c_runtime.decode(plan, bytes)
t.assert_equals(c_decoded, lua_decoded)
t.assert_equals(c_decoded.apartment, '')
end
function g.test_double_negative_zero_decodes()
local plan = c_runtime.compile_plan(hello.Person_descriptor)
-- LuaJIT constant-folds the literal -0.0 to +0.0, so build the
-- sign-bit-set zero at runtime via a cdata round-trip. Bytes
-- encoded from this value must round-trip to a value whose 1/x
-- is -inf — the only way to distinguish -0.0 from +0.0 in Lua.
local neg_zero = ffi.new('double[1]', 0)[0] * -1
local bytes = full_hello.Person_encode({weight_kg = neg_zero})
t.assert(#bytes > 0, '-0.0 should not be zero-suppressed')
local c_decoded = c_runtime.decode(plan, bytes)
t.assert_equals(1 / c_decoded.weight_kg, -math.huge)
end
function g.test_enum_as_number_value()
local plan = c_runtime.compile_plan(hello.Person_descriptor)
local bytes = full_hello.Person_encode({status = 2}) -- ERROR
local c_decoded = c_runtime.decode(plan, bytes)
local lua_decoded = full_hello.Person_decode(bytes)
t.assert_equals(c_decoded, lua_decoded)
t.assert_equals(c_decoded.status, 2)
end
function g.test_fixed64_returns_uint64_cdata()
local plan = c_runtime.compile_plan(hello.Person_descriptor)
local msg = {user_id = ffi.new('uint64_t', 0x123456789ABCDEF0)}
local bytes = full_hello.Person_encode(msg)
local c_decoded = c_runtime.decode(plan, bytes)
t.assert_equals(type(c_decoded.user_id), 'cdata')
t.assert_equals(c_decoded.user_id, msg.user_id)
end
function g.test_sint32_negative_round_trip()
local plan = c_runtime.compile_plan(hello.Person_descriptor)
local bytes = full_hello.Person_encode({balance = -1})
t.assert_equals(c_runtime.decode(plan, bytes),
full_hello.Person_decode(bytes))
bytes = full_hello.Person_encode({balance = 0x7fffffff})
t.assert_equals(c_runtime.decode(plan, bytes),
full_hello.Person_decode(bytes))
end
function g.test_bytes_field_preserves_high_bytes()
local plan = c_runtime.compile_plan(hello.Person_descriptor)
local bytes = full_hello.Person_encode({avatar = '\x00\x01\xff\xfe'})
local c_decoded = c_runtime.decode(plan, bytes)
t.assert_equals(c_decoded.avatar, '\x00\x01\xff\xfe')
end
function g.test_skips_repeated_and_message_tags()
-- 3c scope: repeated/map/message tags are skipped over by wire
-- type rather than decoded (3d/3e/3i extend this). The result
-- table contains only the singular scalar fields, matching what
-- a 3b-encoded input would have produced anyway.
local plan = c_runtime.compile_plan(hello.Person_descriptor)
-- Build bytes via the full Lua codec, then decode through C.
local rich = {
name = 'x',
emails = {'a@b'}, -- repeated string
address = {street = 'Main'}, -- message
lucky_numbers = {1, 2, 3}, -- repeated packed
ages_by_nickname = {alice = 30}, -- map
}
local bytes = full_hello.Person_encode(rich)
local c_decoded = c_runtime.decode(plan, bytes)
t.assert_equals(c_decoded, {name = 'x'})
end
function g.test_skips_unknown_field_tags()
-- Append a synthetic unknown-field tag (id=999, wire varint=0)
-- to a valid encoding. The C decoder should skip it, not error.
local good = full_hello.Person_encode({name = 'x'})
-- field 999, wire 0 => tag varint = (999<<3) | 0 = 7992 => 0xF8 0x3E
local unknown_tag = '\xf8\x3e\x05' -- tag + varint value 5
local plan = c_runtime.compile_plan(hello.Person_descriptor)
local c_decoded = c_runtime.decode(plan, good .. unknown_tag)
t.assert_equals(c_decoded, {name = 'x'})
end
function g.test_wkt_override_rejected()
-- has_override plans don't expose field arrays; mirror 3b's gate.
local plan = c_runtime.compile_plan(pb.wkt.Timestamp_descriptor)
t.assert_error_msg_contains('override', function()
c_runtime.decode(plan, '')
end)
end
function g.test_truncated_input_errors()
local plan = c_runtime.compile_plan(hello.Person_descriptor)
-- Tag for field 1 (string) followed by length-prefix without the
-- string bytes that the length claims.
t.assert_error_msg_contains('truncated', function()
c_runtime.decode(plan, '\x0a\x05ab')
end)
end
end