-- 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_map_fields()
-- 3e scope: singular message + repeated (packed and unpacked)
-- now encode. Map fields remain silently skipped — 3h replaces
-- the gate.
local plan = c_runtime.compile_plan(hello.Person_descriptor)
local msg = {
name = 'x',
emails = {'a@b'}, -- repeated string (encode)
address = {street = 'Main'}, -- message (encode)
lucky_numbers = {1, 2, 3}, -- repeated packed (encode)
ages_by_nickname = {alice = 30}, -- map (skip)
}
local expected = full_hello.Person_encode({
name = 'x',
emails = {'a@b'},
address = {street = 'Main'},
lucky_numbers = {1, 2, 3},
})
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
-- ---------- Repeated + packed (bd-jc9 / ra6 3e) ----------
-- Acceptance per bd-jc9: Person.lucky_numbers (packed int32) round-
-- trips byte-equal, and a fixture mixing packed + unpacked at 10/
-- 100/1000 elements matches mode=full byte-for-byte.
function g.test_acceptance_lucky_numbers_packed_int32()
local plan = c_runtime.compile_plan(hello.Person_descriptor)
local msg = {lucky_numbers = {1, 2, 3, 4, 5, -1, 0x7fffffff}}
t.assert_equals(c_runtime.encode(plan, msg),
full_hello.Person_encode(msg))
end
function g.test_repeated_empty_array_omits_field()
local plan = c_runtime.compile_plan(hello.Person_descriptor)
-- Empty repeated must be wire-equivalent to absent.
t.assert_equals(c_runtime.encode(plan, {lucky_numbers = {}}), '')
t.assert_equals(c_runtime.encode(plan, {emails = {}}), '')
end
function g.test_repeated_emits_zero_elements_no_suppression()
-- Unlike singular scalars, repeated elements are NOT zero-
-- suppressed — every element reaches the wire.
local plan = c_runtime.compile_plan(hello.Person_descriptor)
local msg = {lucky_numbers = {0, 0, 0}}
local bytes = c_runtime.encode(plan, msg)
t.assert_equals(bytes, full_hello.Person_encode(msg))
t.assert(#bytes > 0, 'zero elements still emit')
end
function g.test_repeated_string_per_element_tag()
local plan = c_runtime.compile_plan(hello.Person_descriptor)
local msg = {emails = {'a@b', 'c@d', '', 'long' .. string.rep('x', 200)}}
t.assert_equals(c_runtime.encode(plan, msg),
full_hello.Person_encode(msg))
end
function g.test_repeated_message_self_reference()
-- Person.friends is `repeated Person` — a self-referencing
-- sub-message exercising recursive plan dispatch via the
-- c_plan stash for cycle-breaking.
local plan = c_runtime.compile_plan(hello.Person_descriptor)
local msg = {
name = 'root',
friends = {
{name = 'alice', age = 30},
{name = 'bob', friends = {{name = 'carol'}}},
{}, -- empty friend → tag + len(0) per proto3 presence
},
}
t.assert_equals(c_runtime.encode(plan, msg),
full_hello.Person_encode(msg))
end
function g.test_repeated_non_table_value_errors()
local plan = c_runtime.compile_plan(hello.Person_descriptor)
t.assert_error_msg_contains('repeated', function()
c_runtime.encode(plan, {lucky_numbers = 'not an array'})
end)
end
-- ---------- c_repeated fixture: every dispatch branch ----------
local function counts() return {10, 100, 1000} end
function g.test_fixture_packed_int32_at_counts()
local cr = require(mode .. '.c_repeated.c_repeated_pb')
local full_cr = require('full.c_repeated.c_repeated_pb')
local plan = c_runtime.compile_plan(cr.Holder_descriptor)
for _, n in ipairs(counts()) do
local arr = {}
for i = 1, n do arr[i] = i end
local msg = {packed_int32 = arr}
t.assert_equals(c_runtime.encode(plan, msg),
full_cr.Holder_encode(msg),
('packed_int32 n=%d'):format(n))
end
end
function g.test_fixture_packed_all_numeric_kinds()
local cr = require(mode .. '.c_repeated.c_repeated_pb')
local full_cr = require('full.c_repeated.c_repeated_pb')
local plan = c_runtime.compile_plan(cr.Holder_descriptor)
local msg = {
packed_int32 = {1, -1, 0x7fffffff},
packed_int64 = {ffi.new('int64_t', 1), ffi.new('int64_t', -1)},
packed_sint32 = {-3, 0, 3},
packed_uint32 = {7, 8, 9},
packed_fixed32 = {100, 200},
packed_fixed64 = {ffi.new('uint64_t', 0x1234567890ABCDEFULL)},
packed_double = {1.5, -2.25, 0},
packed_float = {0.5, -0.25},
packed_bool = {true, false, true, true, false},
}
t.assert_equals(c_runtime.encode(plan, msg),
full_cr.Holder_encode(msg))
end
function g.test_fixture_unpacked_scalars_at_counts()
local cr = require(mode .. '.c_repeated.c_repeated_pb')
local full_cr = require('full.c_repeated.c_repeated_pb')
local plan = c_runtime.compile_plan(cr.Holder_descriptor)
for _, n in ipairs(counts()) do
local i32, sx, fx = {}, {}, {}
for i = 1, n do
i32[i] = i
sx[i] = -i
fx[i] = ffi.new('uint64_t', i)
end
local msg = {
unpacked_int32 = i32,
unpacked_sint32 = sx,
unpacked_fixed64 = fx,
}
t.assert_equals(c_runtime.encode(plan, msg),
full_cr.Holder_encode(msg),
('unpacked scalars n=%d'):format(n))
end
end
function g.test_fixture_repeated_strings_and_bytes()
local cr = require(mode .. '.c_repeated.c_repeated_pb')
local full_cr = require('full.c_repeated.c_repeated_pb')
local plan = c_runtime.compile_plan(cr.Holder_descriptor)
for _, n in ipairs(counts()) do
local s, b = {}, {}
for i = 1, n do
s[i] = 'str' .. tostring(i)
b[i] = string.char(i % 256) .. '\0\xff'
end
local msg = {strings = s, blobs = b}
t.assert_equals(c_runtime.encode(plan, msg),
full_cr.Holder_encode(msg),
('strings/blobs n=%d'):format(n))
end
end
function g.test_fixture_repeated_messages_at_counts()
local cr = require(mode .. '.c_repeated.c_repeated_pb')
local full_cr = require('full.c_repeated.c_repeated_pb')
local plan = c_runtime.compile_plan(cr.Holder_descriptor)
for _, n in ipairs(counts()) do
local arr = {}
for i = 1, n do arr[i] = {v = i, s = 'name' .. i} end
local msg = {messages = arr}
t.assert_equals(c_runtime.encode(plan, msg),
full_cr.Holder_encode(msg),
('messages n=%d'):format(n))
end
end
function g.test_fixture_mixed_packed_and_unpacked()
-- All branches simultaneously: packed + unpacked + string/bytes
-- + message, on the same message instance.
local cr = require(mode .. '.c_repeated.c_repeated_pb')
local full_cr = require('full.c_repeated.c_repeated_pb')
local plan = c_runtime.compile_plan(cr.Holder_descriptor)
local p, u, s, m = {}, {}, {}, {}
for i = 1, 100 do
p[i] = i
u[i] = -i
s[i] = 'k' .. i
m[i] = {v = i, s = 's' .. i}
end
local msg = {
packed_int32 = p,
unpacked_int32 = u,
strings = s,
messages = m,
}
t.assert_equals(c_runtime.encode(plan, msg),
full_cr.Holder_encode(msg))
end
end