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