From 39233ed5be83b65ef40ec2a275e85a01ab7fa284 Mon Sep 17 00:00:00 2001 From: Eugene Blikh Date: Sat, 16 May 2026 09:43:22 +0300 Subject: [PATCH] text: wire pb.text into conformance runner TEXT_FORMAT output MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The runner short-circuited every TEXT_FORMAT request to `skipped`, even though pb.text.encode has been encode-capable since M7. Plug it into cmd/conformance/core.lua so protobuf/JSON input → text output exercises the existing encoder end-to-end. Text-format input remains deferred (pb.text is encode-only). Text-format suite: 0 ✓ / 430 skipped / 4 expected fails → 2 ✓ / 426 skipped / 6 expected fails (Scalar/Message *_Print added — unknown-field rendering still missing). --- PLAN.md | 19 ++++--- cmd/conformance/core.lua | 18 +++++-- test/conformance/known_failures_text.txt | 25 +++++++-- test/conformance_test.lua | 65 ++++++++++++++++++++++-- 4 files changed, 108 insertions(+), 19 deletions(-) diff --git a/PLAN.md b/PLAN.md index d0e9796208ae16d31bde627e1a4890291fb0a276..09cbc6c00dd8d30f031ff64c49c6670bc8605168 100644 --- a/PLAN.md +++ b/PLAN.md @@ -144,13 +144,18 @@ fiber and bridges client ↔ handler via `fiber.channel`. All four flavors Watchlists at `test/conformance/known_failures.txt` (main suite) and `test/conformance/known_failures_text.txt` (text-format suite). Current baseline (2026-05-16): - - Binary+JSON suite: 1389 ✓ / 1313 skipped / 79 expected fails - - Text-format suite: 0 ✓ / 430 skipped / 4 expected fails - JSON output now runs end-to-end through the harness. The remaining - expected fails are canonical-form edge cases (Duration/Timestamp - formatting, double precision, NaN handling, JSON-input rejection - rules). The `PB_CONFORMANCE_SKIP_JSON=1` env var still short- - circuits JSON output if a new encoder bug crashes jsoncpp. + - Binary+JSON suite: 1478 ✓ / 1313 skipped / 15 expected fails + - Text-format suite: 2 ✓ / 426 skipped / 6 expected fails + JSON output runs end-to-end; remaining expected fails are + Recommended-only edge cases (FieldMask round-trip, + duplicate-field-name rejection, null-in-collection rejection, + unknown-enum-name rejection, NullValue oneof validator). + Text-format output is wired through `pb.text.encode`; the six + expected fails are all unknown-field cases (Group/Repeated parse + rejection, *_Print not rendering captured unknowns). Text-format + *input* parsing is still deferred. The `PB_CONFORMANCE_SKIP_JSON=1` + env var still short-circuits JSON output if a new encoder bug + crashes jsoncpp. CI wire-up pending — the image build is the long pole (~10–15 min on a clean cache). - [x] Cross-impl interop: 18-fixture corpus in `test/interop/fixtures/` diff --git a/cmd/conformance/core.lua b/cmd/conformance/core.lua index f41be45fcb82132494f7177b21f148eb36066a2d..ff267c4aadbba3968358588097ce9d7f6ba7002b 100644 --- a/cmd/conformance/core.lua +++ b/cmd/conformance/core.lua @@ -57,8 +57,11 @@ local function dispatch(req) return {parse_error = 'json decode failed: ' .. tostring(decoded)} end msg = decoded - elseif req.jspb_payload ~= nil or req.text_payload ~= nil then - return {skipped = 'jspb/text input not supported'} + elseif req.jspb_payload ~= nil then + return {skipped = 'jspb input not supported'} + elseif req.text_payload ~= nil then + -- pb.text is encode-only; text-format input parsing is deferred. + return {skipped = 'text-format input not supported'} else return {runtime_error = 'no payload set in ConformanceRequest'} end @@ -92,8 +95,15 @@ local function dispatch(req) tostring(jbytes)} end return {json_payload = jbytes} - elseif out_fmt == JSPB or out_fmt == TEXT_FORMAT then - return {skipped = 'jspb/text output not supported'} + elseif out_fmt == TEXT_FORMAT then + local ok, tbytes = pcall(pb.text.encode, desc, msg) + if not ok then + return {serialize_error = 'text encode failed: ' .. + tostring(tbytes)} + end + return {text_payload = tbytes} + elseif out_fmt == JSPB then + return {skipped = 'jspb output not supported'} else return {runtime_error = 'unknown requested_output_format: ' .. tostring(out_fmt)} diff --git a/test/conformance/known_failures_text.txt b/test/conformance/known_failures_text.txt index fb491d8a296fde4a0c52cb7d2cd2a09ac90a0eae..b1c89e3148828b4823151c9b423848efb5187196 100644 --- a/test/conformance/known_failures_text.txt +++ b/test/conformance/known_failures_text.txt @@ -1,11 +1,26 @@ # conformance_test_runner --text_format_failure_list # -# Text-format conformance is intentionally deferred — pb.text only does -# encoding from a Lua table, not text-protobuf input parsing nor protobuf- -# input→text-output, so the harness's text-format suite has nothing to -# exercise yet. These tests fail because we can't produce text-format -# output from the protobuf payloads they ship. +# Text-format OUTPUT is now wired through cmd/conformance/core.lua to +# pb.text.encode (protobuf/JSON input → text output). Input parsing is +# still deferred — pb.text only encodes — so any test whose payload is +# text_payload still returns `skipped`. +# +# Remaining expected failures all stem from unknown-field handling: +# +# * Group/Repeated *_Drop: our decoder rejects payloads carrying +# wire types 3/4 (proto2 groups) and length-mismatched repeated +# unknown bytes, so we never reach the encoder. Expected output is +# an empty text body, which we'd produce trivially if the decoder +# accepted the input. Fixing requires skip_field to tolerate groups. +# +# * *_Print: with `print_unknown_fields: true` the harness expects the +# text output to include the original unknown fields. We capture +# unknown bytes during decode but pb.text doesn't render them, so +# output comes back empty. Fixing requires the text encoder to walk +# the captured unknown set. Recommended.Proto3.ProtobufInput.GroupUnknownFields_Drop.TextFormatOutput Recommended.Proto3.ProtobufInput.GroupUnknownFields_Print.TextFormatOutput +Recommended.Proto3.ProtobufInput.MessageUnknownFields_Print.TextFormatOutput Recommended.Proto3.ProtobufInput.RepeatedUnknownFields_Drop.TextFormatOutput Recommended.Proto3.ProtobufInput.RepeatedUnknownFields_Print.TextFormatOutput +Recommended.Proto3.ProtobufInput.ScalarUnknownFields_Print.TextFormatOutput diff --git a/test/conformance_test.lua b/test/conformance_test.lua index 1968906723c5c69eb9bf52d8fb243fc420008be2..60e1031259ed56f5cd64f52a58a8e9deb16db463 100644 --- a/test/conformance_test.lua +++ b/test/conformance_test.lua @@ -128,14 +128,73 @@ core_g.test_unsupported_message_type_skipped = function() 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}) +core_g.test_pb_to_text = function() + -- protobuf input + TEXT_FORMAT output runs pb.text.encode on the + -- decoded Lua table. Result should look like `protoc --decode` output. + local input = proto3.TestAllTypesProto3_encode({ + optional_int32 = 42, + optional_string = 'hello', + repeated_int32 = {1, 2, 3}, + }) 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') + t.assert_not(resp.parse_error, resp.parse_error) + t.assert_not(resp.serialize_error, resp.serialize_error) + t.assert_not(resp.skipped, resp.skipped) + t.assert_str_contains(resp.text_payload, 'optional_int32: 42') + t.assert_str_contains(resp.text_payload, 'optional_string: "hello"') + -- Repeated fields emit once per element, snake_case, mainline form. + t.assert_str_contains(resp.text_payload, 'repeated_int32: 1') + t.assert_str_contains(resp.text_payload, 'repeated_int32: 2') + t.assert_str_contains(resp.text_payload, 'repeated_int32: 3') +end + +core_g.test_json_to_text = function() + local resp = decode_resp(core.handle_request(encode_req({ + json_payload = [[{"optionalInt32": 7, "optionalString": "abc"}]], + requested_output_format = TEXT, + message_type = PROTO3_NAME, + }))) + t.assert_not(resp.parse_error, resp.parse_error) + t.assert_not(resp.serialize_error, resp.serialize_error) + t.assert_str_contains(resp.text_payload, 'optional_int32: 7') + t.assert_str_contains(resp.text_payload, 'optional_string: "abc"') +end + +core_g.test_empty_message_to_text = function() + -- Empty proto3 message has no fields to print; text encoder produces + -- the empty string. The result field is still text_payload (empty + -- string), not skipped/runtime_error. + local resp = decode_resp(core.handle_request(encode_req({ + protobuf_payload = '', + requested_output_format = TEXT, + message_type = PROTO3_NAME, + }))) + t.assert_not(resp.parse_error, resp.parse_error) + t.assert_not(resp.serialize_error, resp.serialize_error) + t.assert_equals(resp.text_payload, '') +end + +core_g.test_text_input_skipped = function() + -- Text-format input parsing is still deferred (pb.text is encode-only). + local resp = decode_resp(core.handle_request(encode_req({ + text_payload = 'optional_int32: 1\n', + requested_output_format = PROTOBUF, + message_type = PROTO3_NAME, + }))) + t.assert_str_contains(resp.skipped or '', 'text-format input') +end + +core_g.test_jspb_output_skipped = function() + local resp = decode_resp(core.handle_request(encode_req({ + protobuf_payload = '', + requested_output_format = conformance.WireFormat.JSPB, + message_type = PROTO3_NAME, + }))) + t.assert_str_contains(resp.skipped or '', 'jspb') end core_g.test_empty_payload_decodes_as_empty_message = function()