-- 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