Status: implemented Implemented in: 2026-05-02 App: reproto
Add automated end-to-end roundtrip tests for edition .proto files.
After specs 0028–0029, reproto fully renders edition files; this spec
closes the testing gap by:
test_roundtrip_edition parametrized test that exercises
the .proto → .pb → reproto → .pb pipeline and asserts descriptor
fidelity.test_roundtrip.py already has _run_roundtrip which:
.proto fixture with protoc --descriptor_set_out..proto from the .pb..proto with protoc..pb byte-identity (after clearing source_code_info)..proto text equality via normalize_proto_batch,
which runs uncomment (tree-sitter comment stripping) followed by
buf format (canonical whitespace and ordering).Step 5 is fully applicable to edition files: buf format supports
editions syntax, and the normalize pipeline eliminates the surface
differences (comment headers, whitespace, option ordering) between
the source fixture and reproto's output. Both .pb and .proto
comparisons should be run for edition roundtrips, exactly as for
proto2/proto3.
EDITION_FIXTURES list and test_roundtrip_edition in
test_roundtrip.py.editions_roundtrip.proto fixture covering all phase 1–4
constructs..pb and .proto
comparisons are run, same as proto2/proto3).--force-proto2-output roundtrip testing of edition sources (the
.pb will differ by syntax and features fields; this is correct
behaviour covered separately).The fixture must follow the rules in
reproto/src/reproto/tests/fixtures/STYLE.md so that the proto-text
comparison passes. That document covers definition order, custom option
name qualification, reserved statement formatting, bytes default escapes,
json_name suppression, and multi-option field formatting.
Two additional rules specific to edition fixtures:
Field-level feature overrides use inline composite form. Reproto
emits int32 x = 1 [features.field_presence = EXPLICIT];, not a
standalone option features.field_presence = EXPLICIT; line inside
the message body. Fixtures must match.
Message- and enum-level feature overrides use standalone option
form. Reproto emits option features.enum_type = CLOSED; as a
standalone statement, not inline. Fixtures must match.
editions_roundtrip.proto fixtureAdd reproto/src/reproto/tests/fixtures/editions_roundtrip.proto.
The file uses edition = "2023" and exercises:
features override (e.g. option features.enum_type = CLOSED;).features override (e.g. option features.field_presence = IMPLICIT;).field_presence = IMPLICIT (no label, proto3-like).field_presence = EXPLICIT (explicit presence).field_presence = LEGACY_REQUIRED.repeated_field_encoding = EXPANDED.message_encoding = DELIMITED (group-style).field_presence = EXPLICIT and a default value.features.enum_type = CLOSED.import weak (allowed in editions).extensions range (allowed in editions).The companion weak_import_proto2_dep.proto is already in the fixtures
directory and can be reused as the weak-import target (it is proto2,
which editions can import).
EDITION_COMPANIONS and EDITION_FIXTURESIn test_roundtrip.py:
EDITION_FIXTURES: list[str] = [
"editions_roundtrip.proto",
]
If the fixture imports a companion (e.g. for weak import), add it to
FIXTURE_COMPANIONS.
test_roundtrip_edition@pytest.mark.parametrize("fixture_name", EDITION_FIXTURES)
def test_roundtrip_edition(fixture_name: str, tmp_path: Path) -> None:
"""End-to-end roundtrip for edition .proto files.
Same two-level check as for proto2/proto3: .pb descriptor
byte-identity and normalized .proto text equality (uncomment
+ buf format).
"""
orig_dir = tmp_path / "orig"
new_dir = tmp_path / "new"
orig_dir.mkdir()
new_dir.mkdir()
_, content = get_fixture_content(fixture_name)
_run_roundtrip(fixture_name, content, orig_dir, new_dir)
_run_roundtrip with compare_proto=False already skips step 5. The
.pb comparison in step 4 uses pb_diff / normalize_proto_batch and
clears source_code_info before comparing — confirmed sufficient by the
manual experiment that preceded this spec.
_run_roundtrip .pb comparison and source_code_infoThe current _run_roundtrip compiles the fixture without
--include_imports, so source_code_info is absent from both the
original and recompiled .pb. Raw byte comparison therefore works
for edition fixtures too, as verified by the manual experiment
preceding this spec.
No changes to _run_roundtrip are needed.
The Edition enum in descriptor.proto defines EDITION_2024 (value
1001) alongside EDITION_2023 (1000). The render_features_block and
allow_* guards in reproto are edition-year-agnostic: they key off
ctx.target_syntax == "editions" without checking the year.
_edition_name maps any Edition enum value to its string name
generically.
edition = "2024". Check with protoc --version and
the protoc changelog. If edition = "2024" is not accepted,
a fixture cannot be compiled and the test cannot run.FeatureSet fields (e.g. json_format, utf8_validation)
or new fields entirely. The _FEATURE_FIELDS list in syntax.py
only covers the six fields present since edition 2023. New fields
would need to be added.build_edition_defaults: the edition defaults table is built from
the descriptor's FeatureSet.edition_defaults annotations. If the
descriptor bundled in the variant does not include 2024 defaults,
feature resolution will fall back to 2023 defaults for edition 2024
files — potentially wrong.When edition 2024 support is available:
EDITION_FIXTURES: list[str] = [
"editions_roundtrip.proto", # 2023
"editions_roundtrip_2024.proto", # 2024 (future)
]
Each fixture is parametrized independently so a missing protoc does not
block the 2023 tests. Use pytest.mark.skipif keyed on a
protoc_supports_edition helper if needed.
| Test | What it covers |
|---|---|
test_roundtrip_edition[editions_roundtrip.proto] | Full .pb + .proto roundtrip for edition 2023 |
T-rt-2 regression: existing test_roundtrip fixtures | No proto2/proto3 regression |
| File | Change |
|---|---|
reproto/src/reproto/tests/test_roundtrip.py | Add EDITION_FIXTURES, test_roundtrip_edition, companion entries |
reproto/src/reproto/tests/fixtures/editions_roundtrip.proto | New fixture |
reproto/src/reproto/tests/fixtures/editions_custom_option_dep.proto | New companion (imported custom option) |
No changes to production code.
All resolved during implementation:
--include_imports for edition fixtures: confirmed. The fixture
compiles without --include_imports; source_code_info is absent
on both sides and raw byte comparison works.
Weak import in the fixture: weak_import_proto2_dep.proto is a
suitable companion. The fixture references WeakDep (short name,
relative to package mockup.editions) in a field to avoid a protoc
unused-import warning.
Custom option in the fixture: both strategies are used. The
fixture defines a local extend google.protobuf.MessageOptions block
(inline) and imports editions_custom_option_dep.proto (companion)
which defines a second MessageOptions extension. The companion is
proto2, imported by the editions fixture, verifying cross-syntax
extension import.