-- Tests for the lazy / zero-copy decode view (runtime/pb/lazy.lua). -- Parameterized over both codegen modes — the generated _decode_lazy -- shim is identical in both, but the descriptor shape it consumes -- comes from each mode's emitted module. -- -- Field-name arguments to :get / :has / :set / :clear / :which are -- routed through the codegen-emitted M._fields and -- M._oneofs constants tables. See docs/api-modes.md for the -- documented contract. Map keys and repeated indices stay as raw -- values — those aren't field names. local t = require('luatest') local ffi = require('ffi') local pb = require('pb') local MODES = {'full', 'runtime'} for _, mode in ipairs(MODES) do local g = t.group('lazy.' .. mode) local hello = require(mode .. '.hello.hello_pb') -- Local aliases keep the assertions short without losing the -- typo-strict lookup. local AF = hello.Address_fields local PF = hello.Person_fields local RF = hello.Result_fields local EF = hello.Event_fields local RO = hello.Result_oneofs -- ---- Basic scalar access ---- g.test_singular_scalars_decode_on_demand = function() local enc = hello.Address_encode({street = 'Main St', city = 'SF', zip = 42}) local v = hello.Address_decode_lazy(enc) t.assert_equals(v:get(AF.street), 'Main St') t.assert_equals(v:get(AF.city), 'SF') t.assert_equals(v:get(AF.zip), 42) t.assert_equals(v:get(AF.apartment), nil, 'absent optional') end g.test_has_reports_wire_presence = function() local enc = hello.Address_encode({street = 'X'}) local v = hello.Address_decode_lazy(enc) t.assert_equals(v:has(AF.street), true) t.assert_equals(v:has(AF.city), false) t.assert_equals(v:has(AF.zip), false) end g.test_get_caches_repeated_calls = function() local enc = hello.Address_encode({street = 'Main'}) local v = hello.Address_decode_lazy(enc) local a = v:get(AF.street) local b = v:get(AF.street) t.assert_is(a, b, 'string values are interned but cache should hit') end -- ---- Repeated fields ---- g.test_unpacked_repeated_string = function() local enc = hello.Person_encode({emails = {'a@x', 'b@x', 'c@x'}}) local v = hello.Person_decode_lazy(enc) local arr = v:get(PF.emails) t.assert_not_equals(arr, nil) t.assert_equals(arr:len(), 3) t.assert_equals(arr:at(1), 'a@x') t.assert_equals(arr:at(2), 'b@x') t.assert_equals(arr:at(3), 'c@x') local seen = {} for i, s in arr:iter() do seen[i] = s end t.assert_equals(seen, {'a@x', 'b@x', 'c@x'}) end g.test_packed_repeated_int32 = function() local enc = hello.Person_encode({lucky_numbers = {7, 13, 42}}) local v = hello.Person_decode_lazy(enc) local arr = v:get(PF.lucky_numbers) t.assert_equals(arr:len(), 3) t.assert_equals(arr:at(1), 7) t.assert_equals(arr:at(2), 13) t.assert_equals(arr:at(3), 42) end g.test_repeated_message_returns_subviews = function() local enc = hello.Person_encode({ friends = { {name = 'Bob', age = 20}, {name = 'Carol', age = 30}, }, }) local v = hello.Person_decode_lazy(enc) local fr = v:get(PF.friends) t.assert_equals(fr:len(), 2) t.assert_equals(fr:at(1):get(PF.name), 'Bob') t.assert_equals(fr:at(1):get(PF.age), 20) t.assert_equals(fr:at(2):get(PF.name), 'Carol') end g.test_absent_repeated_is_nil = function() local enc = hello.Person_encode({name = 'X'}) local v = hello.Person_decode_lazy(enc) t.assert_equals(v:get(PF.emails), nil) t.assert_equals(v:has(PF.emails), false) end -- ---- Nested singular message ---- g.test_nested_message_subview = function() local enc = hello.Person_encode({ name = 'Alice', address = {street = 'Main', city = 'Springfield'}, }) local v = hello.Person_decode_lazy(enc) local addr = v:get(PF.address) t.assert_equals(addr:get(AF.street), 'Main') t.assert_equals(addr:get(AF.city), 'Springfield') end -- ---- Map fields ---- g.test_map_get_and_has = function() local enc = hello.Person_encode({ages_by_nickname = {alice = 30, bob = 25}}) local v = hello.Person_decode_lazy(enc) local m = v:get(PF.ages_by_nickname) t.assert_equals(m:get('alice'), 30) t.assert_equals(m:get('bob'), 25) t.assert_equals(m:has('alice'), true) t.assert_equals(m:has('zzz'), false) t.assert_equals(m:get('zzz'), nil) end g.test_map_keys_and_iter = function() local enc = hello.Person_encode({ages_by_nickname = {alice = 30}}) local v = hello.Person_decode_lazy(enc) local m = v:get(PF.ages_by_nickname) t.assert_equals(m:keys(), {'alice'}) local seen = {} for k, val in m:iter() do seen[k] = val end t.assert_equals(seen, {alice = 30}) end g.test_map_message_values_are_subviews = function() local enc = hello.Person_encode({ addresses_by_label = {home = {street = 'Main', city = 'SF'}}, }) local v = hello.Person_decode_lazy(enc) local m = v:get(PF.addresses_by_label) local home = m:get('home') t.assert_equals(home:get(AF.street), 'Main') t.assert_equals(home:get(AF.city), 'SF') end -- ---- Oneof ---- g.test_oneof_which_text_branch = function() local enc = hello.Result_encode({id = 1, text = 'ok'}) local v = hello.Result_decode_lazy(enc) t.assert_equals(v:which(RO.outcome), 'text') t.assert_equals(v:get(RF.text), 'ok') -- Inactive branches: not on the wire, so :has is false. t.assert_equals(v:has(RF.code), false) t.assert_equals(v:has(RF.details), false) end g.test_oneof_which_message_branch = function() local enc = hello.Result_encode({details = {street = 'Main'}}) local v = hello.Result_decode_lazy(enc) t.assert_equals(v:which(RO.outcome), 'details') local d = v:get(RF.details) t.assert_equals(d:get(AF.street), 'Main') end g.test_oneof_no_branch_set = function() local enc = hello.Result_encode({id = 1}) local v = hello.Result_decode_lazy(enc) t.assert_equals(v:which(RO.outcome), nil) end -- ---- iter / names ---- g.test_iter_yields_present_fields_in_wire_order = function() local enc = hello.Address_encode({street = 'A', city = 'B', zip = 7}) local v = hello.Address_decode_lazy(enc) local seen = {} for name, val in v:iter() do seen[#seen + 1] = {name, val} end t.assert_equals(seen, {{'street', 'A'}, {'city', 'B'}, {'zip', 7}}) end g.test_names_yields_present_fields_only = function() local enc = hello.Address_encode({street = 'A', zip = 7}) local v = hello.Address_decode_lazy(enc) local names = {} for name in v:names() do names[#names + 1] = name end t.assert_equals(names, {'street', 'zip'}) end g.test_iter_skips_unknown_fields = function() -- Hand-craft bytes with an extra unknown field id. local known = hello.Address_encode({street = 'X'}) local extra = string.char(0x68, 0x05) -- tag (id=13, varint), value=5 local v = hello.Address_decode_lazy(known .. extra) local names = {} for name in v:names() do names[#names + 1] = name end t.assert_equals(names, {'street'}) t.assert_equals(v:get(AF.street), 'X') end -- ---- WKT eager-wrap ---- g.test_wkt_timestamp_eager_wrapped = function() local datetime = require('datetime') local dt = datetime.new({timestamp = 1700000000, nsec = 0}) local enc = hello.Event_encode({title = 'launch', created_at = dt}) local v = hello.Event_decode_lazy(enc) t.assert_equals(v:get(EF.title), 'launch') local ts = v:get(EF.created_at) -- WKT descriptors carry desc.decode; lazy delegates to it, -- producing whatever the eager codec produces — for Timestamp, -- a datetime cdata equal to the original. t.assert_equals(ts, dt) end -- ---- 64-bit cdata correctness ---- g.test_fixed64_uint64_cdata = function() local big = ffi.cast('uint64_t', 0xdeadbeefcafebabeULL) local enc = hello.Person_encode({user_id = big}) local v = hello.Person_decode_lazy(enc) local got = v:get(PF.user_id) t.assert_equals(ffi.cast('uint64_t', got), big) end -- ---- Phase 2: mutation + passthrough re-encode ---- g.test_untouched_view_round_trips_bytes_verbatim = function() local orig = hello.Person_encode({ name = 'Alice', age = 30, emails = {'a@x', 'b@x'}, address = {street = 'Main', city = 'SF', zip = 100}, }) local v = hello.Person_decode_lazy(orig) t.assert_equals(v:encode(), orig, 'untouched lazy view -> identical bytes') end g.test_set_singular_scalar_round_trips_via_eager = function() local orig = hello.Address_encode({street = 'A', city = 'B', zip = 1}) local v = hello.Address_decode_lazy(orig) v:set(AF.city, 'C') local out = v:encode() local eager = hello.Address_decode(out) t.assert_equals(eager.street, 'A') t.assert_equals(eager.city, 'C') t.assert_equals(eager.zip, 1) end g.test_set_repeated_replaces_entire_field = function() local orig = hello.Person_encode({emails = {'a@x', 'b@x'}}) local v = hello.Person_decode_lazy(orig) v:set(PF.emails, {'new@x'}) local eager = hello.Person_decode(v:encode()) t.assert_equals(eager.emails, {'new@x'}) end g.test_set_singular_message_passthrough_for_others = function() local orig = hello.Person_encode({ name = 'Alice', age = 30, emails = {'a@x'}, address = {street = 'Old', city = 'X'}, }) local v = hello.Person_decode_lazy(orig) v:set(PF.address, {street = 'New', city = 'Y'}) local eager = hello.Person_decode(v:encode()) t.assert_equals(eager.name, 'Alice') t.assert_equals(eager.age, 30) t.assert_equals(eager.emails, {'a@x'}) t.assert_equals(eager.address.street, 'New') t.assert_equals(eager.address.city, 'Y') end g.test_unknown_fields_preserved_through_set = function() local known = hello.Address_encode({street = 'X'}) local extra = string.char(0x68, 0x05) -- id=13, varint, value=5 local v = hello.Address_decode_lazy(known .. extra) v:set(AF.zip, 99) local out = v:encode() -- Unknown field bytes should still be present in the output. t.assert(out:find(extra, 1, true) ~= nil, 'unknown bytes preserved') local eager = hello.Address_decode(out) t.assert_equals(eager.street, 'X') t.assert_equals(eager.zip, 99) end g.test_oneof_set_clears_other_branches = function() local orig = hello.Result_encode({id = 1, text = 'hello'}) local v = hello.Result_decode_lazy(orig) v:set(RF.code, 42) v:set(RF.text, nil) -- explicit clear local eager = hello.Result_decode(v:encode()) t.assert_equals(eager.id, 1) t.assert_equals(eager.code, 42) t.assert_equals(eager.text, nil) end g.test_sub_view_mutation_propagates_to_parent_encode = function() local orig = hello.Person_encode({ name = 'Alice', address = {street = 'Old', city = 'X', zip = 1}, }) local v = hello.Person_decode_lazy(orig) local addr = v:get(PF.address) addr:set(AF.street, 'New') local eager = hello.Person_decode(v:encode()) t.assert_equals(eager.name, 'Alice') t.assert_equals(eager.address.street, 'New') t.assert_equals(eager.address.city, 'X') end -- ---- :get matches eager :decode ---- g.test_lazy_get_matches_eager_decode = function() local p = { name = 'Alice', age = 30, emails = {'a@x', 'b@x'}, status = hello.Status.OK, address = {street = 'Main', city = 'SF', zip = 100}, lucky_numbers = {1, 2, 3}, ages_by_nickname = {alice = 30}, } local enc = hello.Person_encode(p) local eager = hello.Person_decode(enc) local v = hello.Person_decode_lazy(enc) t.assert_equals(v:get(PF.name), eager.name) t.assert_equals(v:get(PF.age), eager.age) t.assert_equals(v:get(PF.status), eager.status) t.assert_equals(v:get(PF.emails):tolist(), eager.emails) t.assert_equals(v:get(PF.lucky_numbers):tolist(), eager.lucky_numbers) t.assert_equals(v:get(PF.ages_by_nickname):totable(), eager.ages_by_nickname) local addr = v:get(PF.address) t.assert_equals(addr:get(AF.street), eager.address.street) t.assert_equals(addr:get(AF.zip), eager.address.zip) end -- ---- Field-name constants table contract ---- g.test_field_names_table_errors_on_typo = function() t.assert_error_msg_contains( 'unknown field name: "steet"', function() return hello.Address_fields.steet end) end g.test_field_names_table_is_read_only = function() t.assert_error_msg_contains( 'field_names table is read-only', function() hello.Address_fields.new_key = 'x' end) end g.test_oneof_names_table_errors_on_typo = function() t.assert_error_msg_contains( 'unknown field name: "outcom"', function() return hello.Result_oneofs.outcom end) end end