-- Self-test for the conformance runner. -- -- The Google conformance suite is an external binary -- (`conformance_test_runner`) we can't reasonably bundle here, so this -- test stands in for it: drives our runner with crafted -- `ConformanceRequest` cases and asserts well-formed -- `ConformanceResponse` output. -- -- Two layers: -- 1. `core` group — calls `cmd.conformance.core.handle_request` directly. -- Covers every dispatch arm without paying subprocess cost. -- 2. `subprocess` group — actually pipes framed bytes through -- `tarantool cmd/conformance-runner.lua`. Covers the length-prefixed -- framing and the read-until-EOF loop. local t = require('luatest') local fio = require('fio') local core = require('cmd.conformance.core') local conformance = require('full.conformance.conformance_pb') local proto3 = require('full.protobuf_test_messages.proto3.test_messages_proto3_pb') local PROTOBUF = conformance.WireFormat.PROTOBUF local JSON = conformance.WireFormat.JSON local TEXT = conformance.WireFormat.TEXT_FORMAT local PROTO3_NAME = 'protobuf_test_messages.proto3.TestAllTypesProto3' local function encode_req(t_) return conformance.ConformanceRequest_encode(t_) end local function decode_resp(bytes) return conformance.ConformanceResponse_decode(bytes) end -- --------------------------------------------------------------------------- -- 1. Direct dispatch (no subprocess) -- --------------------------------------------------------------------------- local core_g = t.group('conformance.core') core_g.test_failureset_preflight = function() -- The conformance runner asks for a FailureSet up front. Empty payload, -- empty FailureSet response is the canonical answer. local req = encode_req({ protobuf_payload = '', requested_output_format = PROTOBUF, message_type = 'conformance.FailureSet', }) local resp = decode_resp(core.handle_request(req)) t.assert_equals(resp.protobuf_payload, '') t.assert_equals(resp.skipped, nil) t.assert_equals(resp.parse_error, nil) t.assert_equals(resp.runtime_error, nil) end core_g.test_pb_to_pb_roundtrip = function() -- Encode a TestAllTypesProto3 ourselves, ask the runner to round-trip -- it through pb->pb, assert byte-identical output. Since our encoder -- is deterministic for non-map fields, the output bytes must match -- input bytes exactly. local input = proto3.TestAllTypesProto3_encode({ optional_int32 = 42, optional_string = 'hello', repeated_int32 = {1, 2, 3, 4}, }) local resp = decode_resp(core.handle_request(encode_req({ protobuf_payload = input, requested_output_format = PROTOBUF, message_type = PROTO3_NAME, }))) t.assert_equals(resp.protobuf_payload, input) end core_g.test_pb_to_json = function() local input = proto3.TestAllTypesProto3_encode({ optional_int32 = 7, optional_string = 'world', }) local resp = decode_resp(core.handle_request(encode_req({ protobuf_payload = input, requested_output_format = JSON, message_type = PROTO3_NAME, }))) t.assert_str_contains(resp.json_payload, '"optionalInt32":7') t.assert_str_contains(resp.json_payload, '"optionalString":"world"') end core_g.test_json_to_pb = function() local resp = decode_resp(core.handle_request(encode_req({ json_payload = [[{"optionalInt32": 9, "optionalString": "abc"}]], requested_output_format = PROTOBUF, message_type = PROTO3_NAME, }))) t.assert_not(resp.parse_error, resp.parse_error) t.assert_not(resp.runtime_error, resp.runtime_error) local decoded = proto3.TestAllTypesProto3_decode(resp.protobuf_payload) t.assert_equals(decoded.optional_int32, 9) t.assert_equals(decoded.optional_string, 'abc') end core_g.test_parse_error_on_malformed_protobuf = function() -- A truncated varint should produce a parse_error, not a crash. local resp = decode_resp(core.handle_request(encode_req({ protobuf_payload = '\x08', -- tag(1, VARINT), no value requested_output_format = PROTOBUF, message_type = PROTO3_NAME, }))) t.assert_not_equals(resp.parse_error, nil) end core_g.test_parse_error_on_malformed_json = function() local resp = decode_resp(core.handle_request(encode_req({ json_payload = '{not valid json', requested_output_format = PROTOBUF, message_type = PROTO3_NAME, }))) t.assert_not_equals(resp.parse_error, nil) end core_g.test_unsupported_message_type_skipped = function() -- proto2 / editions test message types are intentionally unsupported. local resp = decode_resp(core.handle_request(encode_req({ protobuf_payload = '', requested_output_format = PROTOBUF, message_type = 'protobuf_test_messages.proto2.TestAllTypesProto2', }))) t.assert_str_contains(resp.skipped or '', 'unsupported message type') end core_g.test_text_format_skipped = function() local input = proto3.TestAllTypesProto3_encode({optional_int32 = 1}) local resp = decode_resp(core.handle_request(encode_req({ protobuf_payload = input, requested_output_format = TEXT, message_type = PROTO3_NAME, }))) t.assert_str_contains(resp.skipped or '', 'jspb/text') end core_g.test_empty_payload_decodes_as_empty_message = function() -- proto3 says empty bytes is a valid empty message. local resp = decode_resp(core.handle_request(encode_req({ protobuf_payload = '', requested_output_format = PROTOBUF, message_type = PROTO3_NAME, }))) t.assert_equals(resp.protobuf_payload, '') end core_g.test_wkt_timestamp_field_roundtrip = function() -- Exercise the WKT path: TestAllTypesProto3.optional_timestamp. -- We rely on our WKT Timestamp encoder/decoder. local input = proto3.TestAllTypesProto3_encode({ optional_timestamp = require('datetime').new({timestamp = 1700000000}), }) local resp = decode_resp(core.handle_request(encode_req({ protobuf_payload = input, requested_output_format = PROTOBUF, message_type = PROTO3_NAME, }))) t.assert_equals(resp.protobuf_payload, input) end -- --------------------------------------------------------------------------- -- Regression tests for fixes referenced in test/conformance/TRIAGE.md -- Each test pins one code path against a conformance-shaped request so a -- future change that re-opens the bug is caught locally without needing the -- Docker conformance runner. Tests are grouped by fix; multiple tests per -- fix exist because each fix touches several distinct code paths (per -- scalar type, per shape — singular/repeated/map). -- --------------------------------------------------------------------------- -- Shared helpers for the regression group. local function pb_roundtrip(input) return decode_resp(core.handle_request(encode_req({ protobuf_payload = input, requested_output_format = PROTOBUF, message_type = PROTO3_NAME, }))) end local function json_to_pb(json_str) return decode_resp(core.handle_request(encode_req({ json_payload = json_str, requested_output_format = PROTOBUF, message_type = PROTO3_NAME, }))) end -- ========================================================================= -- Fix 1: varint truncation to 32 bits (commit 0c13fd7) — int32/uint32/ -- sint32/enum decode must drop bits above bit 31 and sign-extend signed -- variants. Distinct code paths: typed scalar decoders (wire.lua) plus -- enum-via-decode_varint sites (codec.lua + inline.go + lazy.lua). -- ========================================================================= core_g.test_int32_truncates_high_bits_to_zero = function() -- 1<<33: low 32 bits all zero → decode to 0, encode to "" (default elision) local resp = pb_roundtrip('\x08\x80\x80\x80\x80\x10') t.assert_not(resp.parse_error, resp.parse_error) t.assert_equals(resp.protobuf_payload, '') end core_g.test_int32_truncates_with_sign_extension = function() -- (1<<33)-1: low 32 bits 0xFFFFFFFF → sign-extend to -1 → 10-byte varint local resp = pb_roundtrip('\x08\xff\xff\xff\xff\x1f') t.assert_not(resp.parse_error, resp.parse_error) -- -1 as int32 is encoded as uint64 0xFFFFFFFFFFFFFFFF = 10-byte varint t.assert_equals(resp.protobuf_payload, '\x08\xff\xff\xff\xff\xff\xff\xff\xff\xff\x01') end core_g.test_uint32_truncates_to_low_32_bits = function() -- field 3 = optional_uint32. Input (1<<33)-1 → low 32 = UINT32_MAX → 5-byte local resp = pb_roundtrip('\x18\xff\xff\xff\xff\x1f') t.assert_not(resp.parse_error, resp.parse_error) t.assert_equals(resp.protobuf_payload, '\x18\xff\xff\xff\xff\x0f') end core_g.test_sint32_zigzag_truncates_to_32_bits = function() -- field 5 = optional_sint32. zz64(INT32_MAX+2) = 4294967298 → low 32 = 2 -- → zigzag_decode(2) = 1 → re-encoded as zz32(1) = varint(2) -- zz64(4294967298) varint: 0x82 0x80 0x80 0x80 0x10 local resp = pb_roundtrip('\x28\x82\x80\x80\x80\x10') t.assert_not(resp.parse_error, resp.parse_error) t.assert_equals(resp.protobuf_payload, '\x28\x02') -- field 5, varint(2) end core_g.test_enum_singular_truncates_to_int32 = function() -- field 21 = optional_nested_enum. Input INT64_MAX varint → low 32 bits -- = 0xFFFFFFFF → -1 → 10-byte uint64 form on re-encode. -- Tag: (21<<3)|0 = 0xA8 0x01. Varint(INT64_MAX) is 9 bytes 0xFF×8 0x7F. local resp = pb_roundtrip( '\xa8\x01' .. '\xff\xff\xff\xff\xff\xff\xff\xff\x7f') t.assert_not(resp.parse_error, resp.parse_error) t.assert_equals(resp.protobuf_payload, '\xa8\x01' .. '\xff\xff\xff\xff\xff\xff\xff\xff\xff\x01') end core_g.test_packed_repeated_int32_truncates_each_element = function() -- field 31 = repeated_int32 (packed). Input contains one over-range value -- (1<<33) — must decode-then-encode as 0; since 0 is in a packed list it -- is still emitted (lists never elide). -- Tag: (31<<3)|2 = 0xFA 0x01. Length-prefixed payload of varint(1<<33). local payload = '\x80\x80\x80\x80\x10' local input = '\xfa\x01' .. string.char(#payload) .. payload local resp = pb_roundtrip(input) t.assert_not(resp.parse_error, resp.parse_error) local decoded = proto3.TestAllTypesProto3_decode(resp.protobuf_payload) t.assert_equals(decoded.repeated_int32, {0}) end core_g.test_repeated_scalar_selects_last_after_truncation = function() -- proto3 last-one-wins on singular scalars: send int32 twice, both -- over-range, only the second value survives, and it must be truncated. -- Input: tag(1,VARINT) + varint(1<<33), tag(1,VARINT) + varint((1<<33)-1) local input = '\x08\x80\x80\x80\x80\x10' .. '\x08\xff\xff\xff\xff\x1f' local resp = pb_roundtrip(input) t.assert_not(resp.parse_error, resp.parse_error) -- Last value (1<<33)-1 truncates to -1 → 10-byte varint t.assert_equals(resp.protobuf_payload, '\x08\xff\xff\xff\xff\xff\xff\xff\xff\xff\x01') end -- ========================================================================= -- Fix 2: reject illegal wire types 6/7 in decode_tag (commit 7442ea2). -- The check must fire whether the tag references a known field (typed -- reader path) or an unknown one (skip_field path). -- ========================================================================= core_g.test_wire_type_6_known_field_rejected = function() -- tag = (1<<3)|6 = 0x0e against optional_int32 t.assert_not_equals(pb_roundtrip('\x0e\x01').parse_error, nil) end core_g.test_wire_type_7_known_field_rejected = function() -- tag = (1<<3)|7 = 0x0f against optional_int32 t.assert_not_equals(pb_roundtrip('\x0f\x01').parse_error, nil) end core_g.test_wire_type_6_unknown_field_rejected = function() -- tag = (1000<<3)|6, varint encoded. 1000 fits in 2 bytes. -- (1000<<3)|6 = 8006 → varint 0xc6 0xbe 0x00. Plus one payload byte. -- Actually 8006 = 0x1F46 → varint two-byte: 0xc6 0x3e t.assert_not_equals(pb_roundtrip('\xc6\x3e\x01').parse_error, nil) end core_g.test_wire_type_6_after_valid_field_rejected = function() -- A valid field followed by an illegal one — confirms the check fires -- mid-stream, not only on the first tag. t.assert_not_equals(pb_roundtrip('\x08\x05' .. '\x0e\x01').parse_error, nil) end core_g.test_valid_wire_types_still_accepted = function() -- Positive control: wt=0 (VARINT) for known field, wt=2 (LEN) for -- string, wt=5 (I32) for float — none should error. local input = '\x08\x05' -- tag(1,VARINT) int32=5 .. '\x72\x03foo' -- tag(14,LEN) string="foo" .. '\x5d\x00\x00\x00\x00' -- tag(11,I32) float=0 local resp = pb_roundtrip(input) t.assert_not(resp.parse_error, resp.parse_error) end -- ========================================================================= -- Fix 3: WKT registry self-registers (commit 50ed9b6) so json_to_any can -- resolve @type for any well-known type without manual pb.register calls. -- ========================================================================= core_g.test_pb_lookup_returns_wkt_descriptors = function() local pb = require('pb') for _, name in ipairs({ 'google.protobuf.Timestamp', 'google.protobuf.Duration', 'google.protobuf.FieldMask', 'google.protobuf.Empty', 'google.protobuf.Any', 'google.protobuf.Struct', 'google.protobuf.Value', 'google.protobuf.ListValue', 'google.protobuf.Int32Value', 'google.protobuf.StringValue', 'google.protobuf.BoolValue', }) do t.assert_not_equals(pb.lookup(name), nil, 'pb.lookup must resolve ' .. name) t.assert_not_equals(pb.lookup('type.googleapis.com/' .. name), nil, 'pb.lookup must resolve fully-qualified URL for ' .. name) end end core_g.test_any_with_timestamp_resolves = function() local resp = json_to_pb([[{"optionalAny": { "@type": "type.googleapis.com/google.protobuf.Timestamp", "value": "1970-01-01T00:00:01Z" }}]]) t.assert_not(resp.parse_error, resp.parse_error) t.assert_not_equals(resp.protobuf_payload, '') end core_g.test_any_with_duration_resolves = function() local resp = json_to_pb([[{"optionalAny": { "@type": "type.googleapis.com/google.protobuf.Duration", "value": "1.5s" }}]]) t.assert_not(resp.parse_error, resp.parse_error) t.assert_not_equals(resp.protobuf_payload, '') end core_g.test_any_with_int32_wrapper_resolves = function() local resp = json_to_pb([[{"optionalAny": { "@type": "type.googleapis.com/google.protobuf.Int32Value", "value": 42 }}]]) t.assert_not(resp.parse_error, resp.parse_error) t.assert_not_equals(resp.protobuf_payload, '') end core_g.test_any_with_struct_resolves = function() local resp = json_to_pb([[{"optionalAny": { "@type": "type.googleapis.com/google.protobuf.Struct", "value": {"key": "val"} }}]]) t.assert_not(resp.parse_error, resp.parse_error) t.assert_not_equals(resp.protobuf_payload, '') end core_g.test_any_with_user_type_resolves = function() -- core.lua registers TestAllTypesProto3 in the WKT registry so Any -- can also embed user types — pin that behavior. local resp = json_to_pb(string.format([[{"optionalAny": { "@type": "type.googleapis.com/%s", "optionalInt32": 7 }}]], PROTO3_NAME)) t.assert_not(resp.parse_error, resp.parse_error) t.assert_not_equals(resp.protobuf_payload, '') end -- ========================================================================= -- Fix 4: skip_field bounds checks (commit 1712192). Truncated unknown -- fields of every wire type must raise a parse_error rather than the -- outer `while pos <= len` exiting silently. -- ========================================================================= core_g.test_skip_field_rejects_truncated_i64 = function() -- field 105 (unknown), I64, 3 of 8 bytes t.assert_not_equals(pb_roundtrip('\xc9\x06\x00\x00\x00').parse_error, nil) end core_g.test_skip_field_rejects_truncated_i32 = function() -- field 105 (unknown), I32, 2 of 4 bytes t.assert_not_equals(pb_roundtrip('\xcd\x06\x00\x00').parse_error, nil) end core_g.test_skip_field_rejects_truncated_len_fast_path = function() -- field 105 (unknown), LEN; declared length 5, only 2 bytes follow. -- Length byte 5 < 0x80 → fast-path bounds check exercised. t.assert_not_equals(pb_roundtrip('\xca\x06\x05ab').parse_error, nil) end core_g.test_skip_field_rejects_truncated_len_multibyte = function() -- field 105 (unknown), LEN; declared length 200 (multi-byte varint), -- only 1 byte follows. Slow-path bounds check exercised. t.assert_not_equals(pb_roundtrip('\xca\x06\xc8\x01a').parse_error, nil) end core_g.test_skip_field_accepts_complete_unknown = function() -- Positive control: an unknown field whose payload IS fully present -- must round-trip cleanly (and our parser preserves unknowns). local input = '\xc9\x06' .. '\x01\x02\x03\x04\x05\x06\x07\x08' local resp = pb_roundtrip(input) t.assert_not(resp.parse_error, resp.parse_error) t.assert_equals(resp.protobuf_payload, input) end -- ========================================================================= -- Fix 5: drop unrecognized enum string names in JSON input (commit -- a2f1209). decode_enum returns nil; callers must skip nil values in -- repeated and map shapes, and elide the field for singular. -- ========================================================================= core_g.test_unknown_enum_string_elided_in_singular = function() local resp = json_to_pb( [[{"optionalNestedEnum": "DEFINITELY_NOT_A_VALUE"}]]) t.assert_not(resp.parse_error, resp.parse_error) t.assert_not(resp.serialize_error, resp.serialize_error) -- Field unset → encoded as empty (default elision). t.assert_equals(resp.protobuf_payload, '') end core_g.test_unknown_enum_string_dropped_from_repeated = function() local resp = json_to_pb( [[{"repeatedNestedEnum": ["FOO", "DEFINITELY_NOT_A_VALUE", "BAR"]}]]) t.assert_not(resp.parse_error, resp.parse_error) t.assert_not(resp.serialize_error, resp.serialize_error) local decoded = proto3.TestAllTypesProto3_decode(resp.protobuf_payload) t.assert_equals(decoded.repeated_nested_enum, {0, 1}) end core_g.test_unknown_enum_string_dropped_from_map_value = function() -- map_string_nested_enum: drop the entry whose enum name is unknown, -- keep the entry whose name resolves. local resp = json_to_pb([[{"mapStringNestedEnum": { "good": "BAR", "bad": "DEFINITELY_NOT_A_VALUE" }}]]) t.assert_not(resp.parse_error, resp.parse_error) t.assert_not(resp.serialize_error, resp.serialize_error) local decoded = proto3.TestAllTypesProto3_decode(resp.protobuf_payload) t.assert_equals(decoded.map_string_nested_enum, {good = 1}) end core_g.test_unknown_enum_numeric_preserved = function() -- proto3 spec: unknown *integer* enum values pass through unchanged. -- Only string names that don't resolve get dropped. local resp = json_to_pb([[{"optionalNestedEnum": 999}]]) t.assert_not(resp.parse_error, resp.parse_error) local decoded = proto3.TestAllTypesProto3_decode(resp.protobuf_payload) t.assert_equals(decoded.optional_nested_enum, 999) end core_g.test_unknown_enum_numeric_string_preserved = function() -- Numeric strings like "999" should also be preserved as the integer. local resp = json_to_pb([[{"optionalNestedEnum": "999"}]]) t.assert_not(resp.parse_error, resp.parse_error) local decoded = proto3.TestAllTypesProto3_decode(resp.protobuf_payload) t.assert_equals(decoded.optional_nested_enum, 999) end -- ========================================================================= -- Fix 6: oneof + repeated-message merge (commit TBD). Two occurrences of a -- singular message field — including a oneof branch — must merge per -- proto3 spec: scalars last-wins, repeated fields concatenate, nested -- sub-messages merge recursively. Sibling clearing still enforces oneof -- exclusivity. -- ========================================================================= local function dup_msg(tag_bytes, sub1, sub2) return tag_bytes .. string.char(#sub1) .. sub1 .. tag_bytes .. string.char(#sub2) .. sub2 end core_g.test_singular_message_merge_scalar_last_wins = function() -- optional_nested_message (id 18, tag 0x92 0x01) appears twice; -- scalar `a` must take last-wins. local sub1 = proto3.TestAllTypesProto3_NestedMessage_encode({a = 1234}) local sub2 = proto3.TestAllTypesProto3_NestedMessage_encode({a = 4321}) local resp = pb_roundtrip(dup_msg('\x92\x01', sub1, sub2)) t.assert_not(resp.parse_error, resp.parse_error) local decoded = proto3.TestAllTypesProto3_decode(resp.protobuf_payload) t.assert_equals(decoded.optional_nested_message.a, 4321) end core_g.test_oneof_message_merge_not_replace = function() -- oneof_nested_message (id 112, tag 0x82 0x07) appears twice. Pre-fix -- the second occurrence replaced the first wholesale, losing fields -- unique to submsg1. Post-fix, scalar last-wins applies inside the -- merged sub-message. local sub1 = proto3.TestAllTypesProto3_NestedMessage_encode({a = 1234}) local sub2 = proto3.TestAllTypesProto3_NestedMessage_encode({a = 4321}) local resp = pb_roundtrip(dup_msg('\x82\x07', sub1, sub2)) t.assert_not(resp.parse_error, resp.parse_error) local decoded = proto3.TestAllTypesProto3_decode(resp.protobuf_payload) t.assert_equals(decoded.oneof_nested_message.a, 4321) end core_g.test_message_merge_recurses_into_submessage = function() -- NestedMessage.corecursive is itself a TestAllTypesProto3. When two -- outer NestedMessage occurrences both have corecursive set with -- DIFFERENT scalar fields, the inner messages must merge recursively -- (not be replaced). local inner1 = proto3.TestAllTypesProto3_decode( proto3.TestAllTypesProto3_encode({optional_int32 = 7})) local inner2 = proto3.TestAllTypesProto3_decode( proto3.TestAllTypesProto3_encode({optional_int64 = 42LL})) local sub1 = proto3.TestAllTypesProto3_NestedMessage_encode( {corecursive = inner1}) local sub2 = proto3.TestAllTypesProto3_NestedMessage_encode( {corecursive = inner2}) local resp = pb_roundtrip(dup_msg('\x92\x01', sub1, sub2)) t.assert_not(resp.parse_error, resp.parse_error) local decoded = proto3.TestAllTypesProto3_decode(resp.protobuf_payload) local cc = decoded.optional_nested_message.corecursive t.assert_equals(cc.optional_int32, 7, 'inner field from submsg1 must survive recursive merge') t.assert_equals(tonumber(cc.optional_int64), 42, 'inner field from submsg2 must survive recursive merge') end core_g.test_message_merge_concatenates_repeated_in_submessage = function() -- NestedMessage.corecursive has repeated_int32 = field 31. Two outer -- occurrences with corecursive set must concat the inner repeated. local inner1 = proto3.TestAllTypesProto3_decode( proto3.TestAllTypesProto3_encode({repeated_int32 = {1, 2, 3}})) local inner2 = proto3.TestAllTypesProto3_decode( proto3.TestAllTypesProto3_encode({repeated_int32 = {4, 5}})) local sub1 = proto3.TestAllTypesProto3_NestedMessage_encode( {corecursive = inner1}) local sub2 = proto3.TestAllTypesProto3_NestedMessage_encode( {corecursive = inner2}) local resp = pb_roundtrip(dup_msg('\x92\x01', sub1, sub2)) t.assert_not(resp.parse_error, resp.parse_error) local decoded = proto3.TestAllTypesProto3_decode(resp.protobuf_payload) t.assert_equals(decoded.optional_nested_message.corecursive.repeated_int32, {1, 2, 3, 4, 5}) end -- ========================================================================= -- Fix 7: tag validation in decode_tag. Field number 0 is illegal; field -- numbers > 2^29-1 are illegal; tag varints must be minimally encoded. -- The uint64-cdata bit ops are required so very high field numbers don't -- alias back into the valid range after a 32-bit truncation. -- ========================================================================= core_g.test_field_number_zero_rejected = function() -- IllegalZeroFieldNum: tag 0x00 = field 0, wt 0. One trailing byte. t.assert_not_equals(pb_roundtrip('\x00\x00').parse_error, nil) end core_g.test_field_number_far_too_high_rejected = function() -- 7-byte tag varint with bits in the highest byte that would put the -- field number > 2^29. tonumber+bit.rshift truncation would have -- mis-recovered fn=10 — the uint64 bit ops catch it. local input = '\xd2\x80\x80\x80\x80\x80\x0f\xd2\t' t.assert_not_equals(pb_roundtrip(input).parse_error, nil) end core_g.test_field_number_slightly_too_high_rejected = function() -- 5-byte tag with fn = 2^31+1, still > 2^29-1 → reject. The 32-bit -- truncation aliased this to fn=1 pre-fix. local input = '\x88\x80\x80\x80\x40\xd2\t' t.assert_not_equals(pb_roundtrip(input).parse_error, nil) end core_g.test_overlong_tag_varint_rejected = function() -- Tag 1, wt 0 encoded as 5 bytes (canonical is 1). Trailing byte 0 -- with byte_count > 1 triggers the overlong check. local input = '\x88\x80\x80\x80\x00\x01' t.assert_not_equals(pb_roundtrip(input).parse_error, nil) end -- ========================================================================= -- Fix 8: UTF-8 validation on proto3 string fields. Singular, repeated, -- oneof, map key, and map value all share decode_string in wire.lua, so -- pinning a few distinct shapes is enough. -- ========================================================================= core_g.test_invalid_utf8_singular_string_rejected = function() -- field 14 = optional_string. tag(14, LEN) = 0x72. One-byte payload -- 0xff is an isolated start of a 5-byte UTF-8 sequence (illegal). t.assert_not_equals(pb_roundtrip('\x72\x01\xff').parse_error, nil) end core_g.test_invalid_utf8_repeated_string_rejected = function() -- field 44 = repeated_string. Encode three entries, middle invalid. -- tag(44, LEN) = (44<<3)|2 = 354 → varint 0xe2 0x02. local tag = '\xe2\x02' local input = tag .. '\x02ok' .. tag .. '\x01\xff' .. tag .. '\x02ok' t.assert_not_equals(pb_roundtrip(input).parse_error, nil) end core_g.test_invalid_utf8_oneof_string_rejected = function() -- oneof_string id 113, tag (113<<3)|2 = 906 → varint 0x8a 0x07. t.assert_not_equals(pb_roundtrip('\x8a\x07\x01\xff').parse_error, nil) end core_g.test_invalid_utf8_lone_surrogate_rejected = function() -- 0xED 0xA0 0x80 is U+D800 (a UTF-16 surrogate), invalid as a code -- point. Pin the surrogate-range branch of is_valid_utf8. t.assert_not_equals(pb_roundtrip('\x72\x03\xed\xa0\x80').parse_error, nil) end core_g.test_invalid_utf8_above_max_codepoint_rejected = function() -- 0xF4 0x90 0x80 0x80 is U+110000, one past Unicode's max. Pin the -- > U+10FFFF branch. local input = '\x72\x04\xf4\x90\x80\x80' t.assert_not_equals(pb_roundtrip(input).parse_error, nil) end core_g.test_valid_utf8_singular_string_round_trips = function() -- Positive control: a multi-byte UTF-8 string survives unchanged. local s = '\xe2\x9c\x85' -- ✅ U+2705 local input = '\x72' .. string.char(#s) .. s local resp = pb_roundtrip(input) t.assert_not(resp.parse_error, resp.parse_error) t.assert_equals(resp.protobuf_payload, input) end core_g.test_bytes_field_accepts_arbitrary_bytes = function() -- bytes (not string) must NOT validate UTF-8. field 15 = optional_bytes. -- tag(15, LEN) = 0x7a. local resp = pb_roundtrip('\x7a\x03\xff\xfe\xfd') t.assert_not(resp.parse_error, resp.parse_error) end -- ========================================================================= -- Fix 9: JSON null on a field is "use default" (drop the field), except -- for google.protobuf.Value where null is itself a Value (NullValue). -- Compounded by Tarantool's box.NULL aliasing to nil under __eq, so the -- decode-side `if dv ~= nil` checks must use rawequal, and the encode- -- side message field check must accept cdata box.NULL. -- ========================================================================= core_g.test_null_scalar_fields_treated_as_default = function() -- Every primitive type set to null in JSON should produce an empty -- protobuf payload (default elision on encode). local resp = json_to_pb([[{ "optionalInt32": null, "optionalInt64": null, "optionalUint32": null, "optionalBool": null, "optionalString": null }]]) t.assert_not(resp.parse_error, resp.parse_error) t.assert_equals(resp.protobuf_payload, '') end core_g.test_null_repeated_field_treated_as_empty = function() -- A repeated field set to null must not crash on #jv (it isn't an -- array). The field stays empty. local resp = json_to_pb([[{"repeatedInt32": null}]]) t.assert_not(resp.parse_error, resp.parse_error) t.assert_equals(resp.protobuf_payload, '') end core_g.test_null_map_field_treated_as_empty = function() -- A map field set to null must not crash on pairs(jv). Stays empty. local resp = json_to_pb([[{"mapStringString": null}]]) t.assert_not(resp.parse_error, resp.parse_error) t.assert_equals(resp.protobuf_payload, '') end core_g.test_null_wrapper_treated_as_absent = function() -- proto3 JSON: null in a *Value wrapper field means the wrapper is -- absent, NOT a wrapper containing 0/empty/false. Empty payload. local resp = json_to_pb([[{"optionalInt32Wrapper": null}]]) t.assert_not(resp.parse_error, resp.parse_error) t.assert_equals(resp.protobuf_payload, '') end core_g.test_null_value_field_emits_null_value_member = function() -- google.protobuf.Value is the exception: JSON null IS a value -- (NullValue.NULL_VALUE). The field must emit tag(value, LEN) + the -- inner Value's bytes ('\x08\x00' = field 1 VARINT 0). -- field 306 = optional_value. tag = (306<<3)|2 = 2450 → varint 0x92 0x13. local resp = json_to_pb([[{"optionalValue": null}]]) t.assert_not(resp.parse_error, resp.parse_error) t.assert_equals(resp.protobuf_payload, '\x92\x13\x02\x08\x00') end -- ========================================================================= -- Fix 10: lower-camelCase mapping for JSON field names. The proto3 spec -- collapses runs of underscores (`__`) and drops trailing underscores; a -- leading underscore causes capitalization of the next letter so the -- generated JSON name has no leading underscore. -- ========================================================================= core_g.test_json_decode_camel_case_drops_double_underscore = function() -- field 404 = field__name4_ → canonical JSON name "fieldName4". -- tag = (404<<3)|0 = 3232 → varint 0xa0 0x19. Plus value varint. local resp = json_to_pb([[{"fieldName4": 4}]]) t.assert_not(resp.parse_error, resp.parse_error) t.assert_equals(resp.protobuf_payload, '\xa0\x19\x04') end core_g.test_json_decode_camel_case_leading_underscore_capitalizes = function() -- field 403 = _field_name3 → canonical "FieldName3". -- tag = (403<<3)|0 = 3224 → varint 0x98 0x19. local resp = json_to_pb([[{"FieldName3": 3}]]) t.assert_not(resp.parse_error, resp.parse_error) t.assert_equals(resp.protobuf_payload, '\x98\x19\x03') end core_g.test_json_decode_camel_case_trailing_underscore_drops = function() -- field 417 = field_name17__ → canonical "fieldName17". -- tag = (417<<3)|0 = 3336 → varint 0x88 0x1a. local resp = json_to_pb([[{"fieldName17": 17}]]) t.assert_not(resp.parse_error, resp.parse_error) t.assert_equals(resp.protobuf_payload, '\x88\x1a\x11') end -- ========================================================================= -- Fix 11: google.protobuf.NullValue descriptor exposed so enum fields -- whose type is NullValue (e.g. oneof_null_value, or Value.null_value) -- can be decoded from JSON without indexing a nil descriptor. -- ========================================================================= core_g.test_oneof_null_value_decodes_from_json = function() -- oneof_null_value is the singleton enum NullValue (NULL_VALUE=0). With -- the descriptor in place, decode_enum can resolve "NULL_VALUE" → 0. -- Field 120, tag (120<<3)|0 = 960 → varint 0xc0 0x07. Value 0 elides. -- Empty payload is the expected output for a 0-valued enum in a oneof -- — except this is a oneof branch with explicit presence, so it WILL -- emit. Either way, we want no parse_error. local resp = json_to_pb([[{"oneofNullValue": "NULL_VALUE"}]]) t.assert_not(resp.parse_error, resp.parse_error) end core_g.test_oneof_merge_still_clears_sibling_branches = function() -- The post-fix merge code must still clear oneof siblings: setting -- oneof_uint32 first, then merging two oneof_nested_message entries, -- must leave oneof_uint32 cleared in the result. local sub1 = proto3.TestAllTypesProto3_NestedMessage_encode({a = 1}) local sub2 = proto3.TestAllTypesProto3_NestedMessage_encode({a = 2}) local input = '\xf8\x06\x09' -- tag(111, VARINT) oneof_uint32 = 9 .. dup_msg('\x82\x07', sub1, sub2) local resp = pb_roundtrip(input) t.assert_not(resp.parse_error, resp.parse_error) local decoded = proto3.TestAllTypesProto3_decode(resp.protobuf_payload) t.assert_equals(decoded.oneof_uint32, nil, 'oneof sibling must be cleared after the message branch is set') t.assert_equals(decoded.oneof_nested_message.a, 2) end -- --------------------------------------------------------------------------- -- 2. Subprocess: stdin/stdout framing -- --------------------------------------------------------------------------- -- -- These tests verify only the framing wrapper in cmd/conformance-runner.lua; -- the dispatch logic is fully covered by the `core` group above. local sub_g = t.group('conformance.subprocess') local REPO_ROOT = fio.abspath(fio.pathjoin( fio.dirname(debug.getinfo(1, 'S').source:sub(2)), '..')) local RUNNER = fio.pathjoin(REPO_ROOT, 'cmd', 'conformance-runner.lua') local function le32(n) return string.char( n % 256, math.floor(n / 256) % 256, math.floor(n / 65536) % 256, math.floor(n / 16777216) % 256) end local function read_le32(s, off) local b1, b2, b3, b4 = s:byte(off, off + 3) return b1 + b2 * 256 + b3 * 65536 + b4 * 16777216 end -- Run the runner with `input_bytes` piped to stdin, return the raw stdout. local function run_runner(input_bytes) local in_path = os.tmpname() local out_path = os.tmpname() local err_path = os.tmpname() local fin = assert(io.open(in_path, 'wb')) fin:write(input_bytes); fin:close() local lua_path = os.getenv('LUA_PATH') or '' local cmd = string.format( 'cd %q && LUA_PATH=%q tarantool cmd/conformance-runner.lua < %q > %q 2> %q', REPO_ROOT, lua_path, in_path, out_path, err_path) local rc = os.execute(cmd) local fout = assert(io.open(out_path, 'rb')) local out = fout:read('*a'); fout:close() local ferr = io.open(err_path, 'r') local err = ferr and ferr:read('*a') or '' if ferr then ferr:close() end os.remove(in_path); os.remove(out_path); os.remove(err_path) return rc, out, err end -- Parse a stream of length-prefixed responses out of `bytes`. local function parse_framed(bytes) local out, off = {}, 1 while off + 4 <= #bytes + 1 do local n = read_le32(bytes, off) off = off + 4 if off + n - 1 > #bytes then break end out[#out + 1] = bytes:sub(off, off + n - 1) off = off + n end return out, off end sub_g.test_single_request_round_trip = function() local req = encode_req({ protobuf_payload = '', requested_output_format = PROTOBUF, message_type = 'conformance.FailureSet', }) local framed = le32(#req) .. req local rc, out, err = run_runner(framed) t.assert_equals(rc, 0, 'runner exited non-zero, stderr=' .. err) local resps = parse_framed(out) t.assert_equals(#resps, 1, 'expected 1 framed response, stderr=' .. err) local resp = decode_resp(resps[1]) t.assert_equals(resp.protobuf_payload, '') end sub_g.test_multiple_requests_in_one_session = function() -- The conformance runner sends many requests over a single pipe; the -- script must loop until EOF rather than handle one and exit. local req1 = encode_req({ protobuf_payload = '', requested_output_format = PROTOBUF, message_type = 'conformance.FailureSet', }) local payload = proto3.TestAllTypesProto3_encode({optional_int32 = 17}) local req2 = encode_req({ protobuf_payload = payload, requested_output_format = PROTOBUF, message_type = PROTO3_NAME, }) local req3 = encode_req({ json_payload = [[{"optionalInt32": 99}]], requested_output_format = JSON, message_type = PROTO3_NAME, }) local framed = le32(#req1) .. req1 .. le32(#req2) .. req2 .. le32(#req3) .. req3 local rc, out, err = run_runner(framed) t.assert_equals(rc, 0, 'runner exited non-zero, stderr=' .. err) local resps = parse_framed(out) t.assert_equals(#resps, 3, 'expected 3 framed responses, stderr=' .. err) local r1 = decode_resp(resps[1]) local r2 = decode_resp(resps[2]) local r3 = decode_resp(resps[3]) t.assert_equals(r1.protobuf_payload, '') t.assert_equals(r2.protobuf_payload, payload) t.assert_str_contains(r3.json_payload, '"optionalInt32":99') end sub_g.test_empty_stdin_clean_exit = function() local rc, out, err = run_runner('') t.assert_equals(rc, 0, 'runner exited non-zero, stderr=' .. err) t.assert_equals(out, '', 'unexpected output on empty stdin') end