-- 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_message_fields()
-- 3b scope: repeated/map/message silently skipped (3d/3e will
-- replace this). Singular fields still encode.
local plan = c_runtime.compile_plan(hello.Person_descriptor)
local msg = {
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 expected = full_hello.Person_encode({name = 'x'})
t.assert_equals(c_runtime.encode(plan, msg), expected)
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