-- Test for bd-y1n / ra6 3b: C-side scalar encode. -- -- Only runs when PB_ENABLE_C=1 is set in the environment AND the C -- runtime module is loadable. Otherwise the group is skipped, which -- keeps `just test` green on hosts without the C module built. -- -- Acceptance per bd-y1n: -- Person encode for {name='x', age=42, balance=-7, user_id=..., -- weight_kg=3.14} byte-equal to mode=full pure-Lua output. 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 -- The mode=full reference is the byte-equality target; the test runs -- against both codegen modes' descriptors (the C plan is mode-agnostic). local full_hello for _, mode in ipairs({'full', 'runtime'}) do local g = t.group('c_runtime_encode.' .. 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-y1n ---------- function g.test_acceptance_person_scalar_subset() 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 c_bytes = c_runtime.encode(plan, msg) local lua_bytes = full_hello.Person_encode(msg) t.assert_equals(c_bytes, lua_bytes, 'C encode matches mode=full pure-Lua encode byte-for-byte') end -- ---------- Per-kind coverage ---------- function g.test_empty_message_produces_empty_string() local plan = c_runtime.compile_plan(hello.Address_descriptor) t.assert_equals(c_runtime.encode(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} t.assert_equals(c_runtime.encode(plan, msg), full_hello.Address_encode(msg)) end function g.test_proto3_optional_emits_empty_string() -- Address.apartment is proto3-optional; presence beats default. local plan = c_runtime.compile_plan(hello.Address_descriptor) local msg = {apartment = ''} t.assert_equals(c_runtime.encode(plan, msg), full_hello.Address_encode(msg)) end function g.test_double_negative_zero_emits() local plan = c_runtime.compile_plan(hello.Person_descriptor) -- +0.0 -> skip; -0.0 -> emit (sign bit preserved by wire spec). local m_pos = {weight_kg = 0.0} local m_neg = {weight_kg = -0.0} t.assert_equals(c_runtime.encode(plan, m_pos), full_hello.Person_encode(m_pos)) t.assert_equals(c_runtime.encode(plan, m_neg), full_hello.Person_encode(m_neg)) end function g.test_enum_as_number() local plan = c_runtime.compile_plan(hello.Person_descriptor) local msg = {status = 2} -- ERROR t.assert_equals(c_runtime.encode(plan, msg), full_hello.Person_encode(msg)) end function g.test_enum_as_string_lookup() local plan = c_runtime.compile_plan(hello.Person_descriptor) local msg = {status = 'OK'} t.assert_equals(c_runtime.encode(plan, msg), full_hello.Person_encode(msg)) end function g.test_enum_zero_value_suppressed() local plan = c_runtime.compile_plan(hello.Person_descriptor) local msg = {status = 'UNKNOWN'} -- = 0; default-suppressed t.assert_equals(c_runtime.encode(plan, msg), '') end function g.test_enum_unknown_string_errors() local plan = c_runtime.compile_plan(hello.Person_descriptor) t.assert_error_msg_contains("unknown enum value 'NOPE'", function() c_runtime.encode(plan, {status = 'NOPE'}) end) end function g.test_fixed64_cdata_round_trip() local plan = c_runtime.compile_plan(hello.Person_descriptor) local msg = {user_id = ffi.new('uint64_t', 0x123456789ABCDEF0)} t.assert_equals(c_runtime.encode(plan, msg), full_hello.Person_encode(msg)) end function g.test_sint32_negative_and_zero() local plan = c_runtime.compile_plan(hello.Person_descriptor) t.assert_equals(c_runtime.encode(plan, {balance = 0}), '') t.assert_equals( c_runtime.encode(plan, {balance = -1}), full_hello.Person_encode({balance = -1})) t.assert_equals( c_runtime.encode(plan, {balance = 0x7fffffff}), full_hello.Person_encode({balance = 0x7fffffff})) end function g.test_int32_zero_suppressed() local plan = c_runtime.compile_plan(hello.Person_descriptor) t.assert_equals(c_runtime.encode(plan, {age = 0}), '') end function g.test_bytes_field() -- Person.avatar is bytes @8 local plan = c_runtime.compile_plan(hello.Person_descriptor) local msg = {avatar = '\x00\x01\xff\xfe'} t.assert_equals(c_runtime.encode(plan, msg), full_hello.Person_encode(msg)) end function g.test_skips_repeated_and_map_fields() -- 3d scope: singular message now encoded. Repeated and map -- are still silently skipped — 3e/3h will replace this. local plan = c_runtime.compile_plan(hello.Person_descriptor) local msg = { name = 'x', emails = {'a@b'}, -- repeated string (skip) address = {street = 'Main'}, -- message (encode) lucky_numbers = {1, 2, 3}, -- repeated packed (skip) ages_by_nickname = {alice = 30}, -- map (skip) } local expected = full_hello.Person_encode( {name = 'x', address = {street = 'Main'}}) t.assert_equals(c_runtime.encode(plan, msg), expected) end -- ---------- Sub-message encode (bd-hwe / ra6 3d) ---------- function g.test_singular_submessage_round_trip() -- Person.address (one-level singular sub-message) round-trip. local plan = c_runtime.compile_plan(hello.Person_descriptor) local msg = { name = 'x', address = {street = 'Main', city = 'Springfield', zip = 100}, } t.assert_equals(c_runtime.encode(plan, msg), full_hello.Person_encode(msg)) end function g.test_empty_submessage_emits_tag_with_zero_length() -- Singular sub-message with an empty table — proto3 presence -- semantics require emitting tag + length(0). The Lua codec -- does this; the C codec must match. local plan = c_runtime.compile_plan(hello.Person_descriptor) local msg = {name = 'x', address = {}} t.assert_equals(c_runtime.encode(plan, msg), full_hello.Person_encode(msg)) end function g.test_submessage_with_proto3_optional_field() -- Address.apartment is proto3-optional. Encode through the -- sub-message must surface the empty string just like the -- top-level Address codec does. local plan = c_runtime.compile_plan(hello.Person_descriptor) local msg = {address = {apartment = ''}} t.assert_equals(c_runtime.encode(plan, msg), full_hello.Person_encode(msg)) end function g.test_submessage_non_table_value_errors() local plan = c_runtime.compile_plan(hello.Person_descriptor) t.assert_error_msg_contains('table', function() c_runtime.encode(plan, {address = 'not a table'}) end) end function g.test_nested_5_levels() -- 5-level chain of singular sub-messages. Exercises the -- recursion depth and the cycle-breaking c_plan stash. local cn = require(mode .. '.c_nested.c_nested_pb') local full_cn = require('full.c_nested.c_nested_pb') local msg = { v = 1, next = {v = 2, next = {v = 3, next = {v = 4, next = {v = 5}}}}, } local plan = c_runtime.compile_plan(cn.L1_descriptor) t.assert_equals(c_runtime.encode(plan, msg), full_cn.L1_encode(msg)) end function g.test_large_submessage_triggers_parent_grow() -- Parent buffer starts at 4KB stack-backed; force a parent -- regrow path during the post-recursion ebuf_reserve by -- packing a >4KB string into the sub-message. local plan = c_runtime.compile_plan(hello.Person_descriptor) local msg = { name = 'x', address = {street = string.rep('s', 8192)}, } t.assert_equals(c_runtime.encode(plan, msg), full_hello.Person_encode(msg)) end function g.test_long_string_grows_buffer() -- Stack buffer is 4KB; force the heap-promotion path with a -- string that pushes past it. local plan = c_runtime.compile_plan(hello.Person_descriptor) local msg = {name = string.rep('a', 8192)} t.assert_equals(c_runtime.encode(plan, msg), full_hello.Person_encode(msg)) end function g.test_wkt_override_rejected() -- has_override plans skip field-walk; 3b does not handle them. local plan = c_runtime.compile_plan(pb.wkt.Timestamp_descriptor) t.assert_error_msg_contains('override', function() c_runtime.encode(plan, {seconds = 1}) end) end end