Skip to content

Commit 40918e0

Browse files
rustyconoverclaude
andcommitted
Test the C++ records generator
Determinism, checked-in drift (VGI_EXTENSION_ROOT selects the extension checkout), an Arrow-free header, member order/nullability against each ARROW_SCHEMA, known shapes, enum wire order, and the schema-parity pairs the extension's C++ test checks. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1 parent 8d8aae6 commit 40918e0

2 files changed

Lines changed: 139 additions & 15 deletions

File tree

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
# Copyright 2025, 2026 Query Farm LLC - https://query.farm
2+
3+
"""Drift + behavior tests for `vgi.codegen.cpp_records`.
4+
5+
The deep correctness check — that every generated record encodes with exactly its
6+
`vgi_protocol_schemas.hpp` factory's schema, and round-trips — lives on the C++
7+
side, in the extension's ``test/cpp/test_protocol_records.cpp``: only a C++
8+
compiler can run the generated codecs. From here we check the model the
9+
generator builds and that the checked-in files are current.
10+
11+
The checked-in comparison is skipped when the sibling `vgi` checkout is not
12+
present; ``VGI_EXTENSION_ROOT`` points it at another checkout (e.g. a worktree).
13+
"""
14+
15+
from __future__ import annotations
16+
17+
import io
18+
import os
19+
from pathlib import Path
20+
21+
import pytest
22+
23+
from vgi.codegen import cpp_records
24+
from vgi.codegen.cpp_records import Presence
25+
26+
_REGEN_HINT = (
27+
"To regenerate, run:\n"
28+
" uv run --project ~/Development/vgi-python python scripts/regen_generated.py\n"
29+
"\n"
30+
"Do not redirect a generator into its destination with `>`: the shell\n"
31+
"truncates the file before the generator runs, so any failure destroys\n"
32+
"the checked-in artifact. The script renders to memory first."
33+
)
34+
35+
36+
def _extension_root() -> Path:
37+
override = os.environ.get("VGI_EXTENSION_ROOT")
38+
if override:
39+
return Path(override)
40+
return Path(__file__).resolve().parents[2] / "vgi"
41+
42+
43+
def _emitted(which: str) -> str:
44+
buf = io.StringIO()
45+
cpp_records.emit(buf, which)
46+
return buf.getvalue()
47+
48+
49+
@pytest.mark.parametrize("which", ["hpp", "cpp"])
50+
def test_generator_is_deterministic(which: str) -> None:
51+
"""Calling emit() twice produces byte-identical output."""
52+
assert _emitted(which) == _emitted(which), f"cpp_records {which} is non-deterministic"
53+
54+
55+
@pytest.mark.parametrize(
56+
("which", "relative"),
57+
[("hpp", "src/generated/vgi_protocol_records.hpp"), ("cpp", "src/generated/vgi_protocol_records.cpp")],
58+
)
59+
def test_checked_in_files_are_current(which: str, relative: str) -> None:
60+
"""The extension's checked-in generated files match the generator."""
61+
path = _extension_root() / relative
62+
if not path.exists():
63+
pytest.skip(f"{path} not present (set VGI_EXTENSION_ROOT)")
64+
assert path.read_text() == _emitted(which), f"{path} is stale.\n{_REGEN_HINT}"
65+
66+
67+
def test_header_is_arrow_free() -> None:
68+
"""The records header only forward-declares Arrow, so it is cheap to include anywhere."""
69+
header = _emitted("hpp")
70+
assert "#include <arrow" not in header
71+
assert "class RecordBatch;" in header
72+
73+
74+
def test_members_follow_arrow_schema_order_and_nullability() -> None:
75+
"""Member order is column order; nullable columns are optional or defaulted, others required."""
76+
for record in cpp_records.build_model().records.values():
77+
assert [m.wire_name for m in record.members] == record.schema.names, record.name
78+
for member, field in zip(record.members, record.schema, strict=True):
79+
if field.nullable:
80+
assert member.presence in (Presence.OPTIONAL, Presence.DEFAULTED), (record.name, member.wire_name)
81+
else:
82+
assert member.presence is Presence.REQUIRED, (record.name, member.wire_name)
83+
if member.presence is Presence.OPTIONAL:
84+
assert member.declared_type.startswith("std::optional<"), (record.name, member.wire_name)
85+
86+
87+
def test_known_shapes() -> None:
88+
"""Spot-check the mapping on records the extension decodes and encodes."""
89+
model = cpp_records.build_model()
90+
types = {r.name: {m.wire_name: m.declared_type for m in r.members} for r in model.records.values()}
91+
attach = types["CatalogAttachResult"]
92+
assert attach["attach_opaque_data"] == "Bytes"
93+
assert attach["comment"] == "std::optional<std::string>"
94+
assert attach["tags"] == "std::map<std::string, std::string>"
95+
assert attach["settings"] == "std::vector<Bytes>"
96+
init = types["InitRequest"]
97+
assert init["phase"] == "std::optional<VgiInitPhase>"
98+
assert init["pushdown_filters"] == "std::optional<LargeBytes>"
99+
assert init["join_keys"] == "std::optional<std::vector<LargeBytes>>"
100+
assert types["CatalogInfo"]["releases"] == "std::vector<CatalogRelease>"
101+
assert types["CatalogRelease"]["released_at"] == "std::optional<TimestampMicros>"
102+
assert types["FunctionInfo"]["function_type"] == "FunctionType"
103+
# A wire name that is a C++ keyword gets a trailing underscore.
104+
assert "namespace_" in {m.ident for m in model.records["FilterFunctionCapability"].members}
105+
assert "max_" in {m.ident for m in model.records["TableFunctionCardinalityResult"].members}
106+
# Results are named after their method, matching their schema factory.
107+
assert "SecretLookupResult" in model.records
108+
109+
110+
def test_enum_wire_values_are_member_names_in_order() -> None:
111+
"""Enumerator order is the wire dictionary order; the wire value is the member name."""
112+
from vgi.protocol import TableInOutFunctionInitPhase
113+
114+
phase = cpp_records.build_model().enums["VgiInitPhase"]
115+
assert [m.wire_name for m in phase.members] == [m.name for m in TableInOutFunctionInitPhase]
116+
assert [m.ident for m in phase.members] == ["Input", "Finalize", "TableBuffering", "TableBufferingFinalize"]
117+
118+
119+
def test_every_factory_backed_record_gets_a_parity_pair() -> None:
120+
"""Records with a vgi_protocol_schemas factory are listed for the C++ schema-parity test."""
121+
source = _emitted("cpp")
122+
factories = cpp_records._factory_names()
123+
for name in cpp_records.build_model().records:
124+
if name in factories:
125+
assert f'{{"{name}", {name}RecordSchema(), {name}Schema()}}' in source, name

‎vgi/codegen/cpp_records.py‎

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -378,8 +378,8 @@ def _render_header(model: _Model, namespace: list[str]) -> str:
378378
for e in model.enums.values():
379379
out.write("\n".join(_doc(e.doc, "")) + ("\n" if e.doc else ""))
380380
out.write(f"enum class {e.name} : int16_t {{\n")
381-
for m in e.members:
382-
out.write(f"\t{m.ident},\n")
381+
for em in e.members:
382+
out.write(f"\t{em.ident},\n")
383383
out.write("};\n")
384384
out.write(f"//! The wire name of a {e.name} value.\n")
385385
out.write(f"const char *ToWire({e.name} value);\n")
@@ -402,9 +402,7 @@ def _render_header(model: _Model, namespace: list[str]) -> str:
402402
out.write(f"\t{m.declared_type} {m.ident}{init};\n")
403403
out.write("};\n")
404404
out.write(f"bool operator==(const {r.name} &a, const {r.name} &b);\n")
405-
out.write(
406-
f"inline bool operator!=(const {r.name} &a, const {r.name} &b) {{\n\treturn !(a == b);\n}}\n"
407-
)
405+
out.write(f"inline bool operator!=(const {r.name} &a, const {r.name} &b) {{\n\treturn !(a == b);\n}}\n")
408406
out.write(f"//! A one-row batch with {r.name}'s exact protocol schema.\n")
409407
out.write(f"std::shared_ptr<arrow::RecordBatch> ToBatch(const {r.name} &value);\n")
410408
out.write(
@@ -470,7 +468,7 @@ def _render_source(model: _Model, namespace: list[str]) -> str:
470468
out.write(f"static const char *const k{e.name}Wire[] = {{{wires}}};\n")
471469
out.write(f"static constexpr int16_t k{e.name}Count = {len(e.members)};\n\n")
472470
out.write(f"const char *ToWire({e.name} value) {{\n")
473-
out.write(f"\tauto i = static_cast<int16_t>(value);\n")
471+
out.write("\tauto i = static_cast<int16_t>(value);\n")
474472
out.write(f'\treturn i >= 0 && i < k{e.name}Count ? k{e.name}Wire[i] : "";\n}}\n\n')
475473
out.write(f"std::optional<{e.name}> {e.name}FromWire(const std::string &wire) {{\n")
476474
out.write(f"\tfor (int16_t i = 0; i < k{e.name}Count; i++) {{\n")
@@ -500,8 +498,11 @@ def _render_source(model: _Model, namespace: list[str]) -> str:
500498
for r in model.records.values():
501499
fields = ",\n\t ".join(_emit_field(f, origin=f"{r.name}.{f.name}") for f in r.schema)
502500
out.write(f"static const std::shared_ptr<arrow::Schema> &{r.name}RecordSchema() {{\n")
503-
out.write(f"\tstatic const auto schema = arrow::schema({{\n\t {fields},\n\t}});\n" if r.members else
504-
"\tstatic const auto schema = arrow::schema(arrow::FieldVector{});\n")
501+
out.write(
502+
f"\tstatic const auto schema = arrow::schema({{\n\t {fields},\n\t}});\n"
503+
if r.members
504+
else "\tstatic const auto schema = arrow::schema(arrow::FieldVector{});\n"
505+
)
505506
out.write("\treturn schema;\n}\n\n")
506507

507508
out.write(f"bool operator==(const {r.name} &a, const {r.name} &b) {{\n")
@@ -531,15 +532,17 @@ def _render_source(model: _Model, namespace: list[str]) -> str:
531532

532533
out.write(f"std::shared_ptr<arrow::RecordBatch> ToBatch(const {r.name} &value) {{\n")
533534
out.write(f"\tconst auto &schema = {r.name}RecordSchema();\n")
534-
out.write(f"\treturn wire::BatchFromStruct(schema, BuildColumn(wire::StructTypeOf(schema), {{&value}}));\n}}\n\n")
535+
out.write("\treturn wire::BatchFromStruct(schema, BuildColumn(wire::StructTypeOf(schema), {&value}));\n}\n\n")
535536
out.write(
536537
f"void FromBatch(const std::shared_ptr<arrow::RecordBatch> &batch, int64_t row, {r.name} &out,\n"
537538
f" const std::string &worker_path) {{\n"
538539
)
539540
out.write(f'\twire::RowReader r(batch, row, "{r.name}", worker_path);\n\tRead{r.name}(r, out);\n}}\n\n')
540541

541542
factories = _factory_names()
542-
out.write("std::vector<std::pair<std::string, std::shared_ptr<arrow::RecordBatch>>> DefaultRecordBatchesForTesting() {\n")
543+
out.write(
544+
"std::vector<std::pair<std::string, std::shared_ptr<arrow::RecordBatch>>> DefaultRecordBatchesForTesting() {\n"
545+
)
543546
out.write("\treturn {\n")
544547
for r in model.records.values():
545548
out.write(f'\t {{"{r.name}", ToBatch({r.name} {{}})}},\n')
@@ -556,13 +559,10 @@ def _render_source(model: _Model, namespace: list[str]) -> str:
556559
def _factory_names() -> set[str]:
557560
"""Stems of the schema factories vgi_protocol_schemas.hpp emits (see cpp_schemas.emit)."""
558561
from vgi.codegen._common import EXTRA_RESPONSE_TYPES, REQUEST_TYPES, collect_schemas
559-
560562
from vgi.secret_protocol import VgiSecretProtocol
561563

562564
names = {s.name for s in collect_schemas(extra_response_types=(*EXTRA_RESPONSE_TYPES, *REQUEST_TYPES))}
563-
secret = collect_schemas(
564-
VgiSecretProtocol, info_types=(), extra_response_types=(), check_info_subclasses=False
565-
)
565+
secret = collect_schemas(VgiSecretProtocol, info_types=(), extra_response_types=(), check_info_subclasses=False)
566566
return names | {s.name for s in secret}
567567

568568

@@ -604,4 +604,3 @@ def main() -> None:
604604

605605
if __name__ == "__main__":
606606
main()
607-

0 commit comments

Comments
 (0)