diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4afdc2aa..b787b64c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -471,9 +471,11 @@ jobs: docker rm "$CID" chmod +x /usr/local/bin/conformance_test_runner /usr/local/bin/protoc - # Both builds use the same target dir so build-script artifacts - # (buffa-codegen, buffa-build, syn, etc.) are compiled only once. - # The std binary is copied out before the no_std build overwrites it. + # All three builds use the same target dir so build-script artifacts + # (buffa-codegen, buffa-build, syn, etc.) are compiled only once. The std + # and table binaries are copied out before the next build overwrites + # them; the no_std build stays last because its run uses the artifact in + # place. - name: Determine host target id: host run: echo "triple=$(rustc -vV | grep '^host:' | cut -d' ' -f2)" >> "$GITHUB_OUTPUT" @@ -486,6 +488,14 @@ jobs: - name: Save std binary run: cp conformance/target/${{ steps.host.outputs.triple }}/release/conformance /tmp/buffa-conformance-std + - name: Build conformance binary (table) + run: >- + cargo build --release --manifest-path conformance/Cargo.toml + --features table --target ${{ steps.host.outputs.triple }} + + - name: Save table binary + run: cp conformance/target/${{ steps.host.outputs.triple }}/release/conformance /tmp/buffa-conformance-table + - name: Build conformance binary (no_std) run: >- cargo build --release --manifest-path conformance/Cargo.toml @@ -549,3 +559,10 @@ jobs: --failure_list conformance/known_failures_view_vtable.txt --maximum_edition 2024 /tmp/buffa-conformance-std + + - name: Run conformance tests (via-table) + run: >- + conformance_test_runner + --failure_list conformance/known_failures_table.txt + --maximum_edition 2024 + /tmp/buffa-conformance-table diff --git a/.gitignore b/.gitignore index e3ba196b..088f70d0 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ # Conformance proto files are fetched from the tools image. conformance/protos/ conformance/target-nostd/ +conformance/target-table/ # Cargo build artefacts target/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5b3d8594..b8105d7b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -82,22 +82,22 @@ needed — the runner just has to be built from protobuf source once: ```bash task conformance-tools-local # one-time: cmake-builds the runner into .local/bin, # populates conformance/protos/ (~10-20 min) -task conformance-local # seven runs, same failure lists as the Docker path +task conformance-local # eight runs, same failure lists as the Docker path ``` Requires cmake, a C++ toolchain, and protoc v30+ on PATH or `$PROTOC` (`task install-protoc`). Set `CONFORMANCE_OUT=` to tee per-run logs. -**Understanding the output**: The conformance runner executes seven runs -(std, no_std, via-view, via-lazy, view-json, via-reflect, via-vtable), each +**Understanding the output**: The conformance runner executes eight runs +(std, no_std, via-view, via-lazy, view-json, via-reflect, via-vtable, via-table), each producing two suites: -1. Binary + JSON suite — expects thousands of successes (~5500 std, ~5500 no_std). The via-view and via-lazy runs only handle binary→binary (~2800); the view-json, via-reflect, and via-vtable runs handle binary→JSON (and via-reflect also binary→binary). -2. Text format suite — 883 successes for std and no_std (the full suite); via-view, via-lazy, view-json, via-reflect, and via-vtable show `0 successes, 883 skipped` (those modes have no `TextFormat` path). +1. Binary + JSON suite — expects thousands of successes (~5500 std and no_std; 5,549 table). The via-view and via-lazy runs only handle binary→binary (~2800); the view-json, via-reflect, and via-vtable runs handle binary→JSON (and via-reflect also binary→binary). +2. Text format suite — 883 successes for std, no_std and table (the full suite); via-view, via-lazy, view-json, via-reflect, and via-vtable show `0 successes, 883 skipped` (those modes have no `TextFormat` path). -So a healthy run shows **14 `CONFORMANCE SUITE PASSED` lines**. +So a healthy run shows **16 `CONFORMANCE SUITE PASSED` lines**. -The Dockerfile builds **two binaries**: one with default features (std) and one with `--no-default-features` (no_std). The std binary is reused for the view/reflect runs by setting an env var: +The Dockerfile builds **three binaries**: one with default features (std), one with `--no-default-features` (no_std) and one with `--features table` (table). The std binary is reused for the view/reflect runs by setting an env var: - **via-view** (`BUFFA_VIA_VIEW=1`) — binary input through `decode_view → to_owned_message → encode`, verifying owned/view decoder parity. - **via-lazy** (`BUFFA_VIA_LAZY=1`) — binary input through `decode_lazy → to_owned_message → encode` on the lazy view family (`lazy_views(true)`), verifying the lazy decoder (record arms, fragment merge, budget capture) against the corpus. @@ -105,13 +105,15 @@ The Dockerfile builds **two binaries**: one with default features (std) and one - **via-reflect** (`BUFFA_VIA_REFLECT=1`) — binary/JSON I/O through `DynamicMessage`'s descriptor-driven codec and reflective serde, verifying the runtime reflection codec independently of any generated type. - **via-vtable** (`BUFFA_VIA_VTABLE=1`) — binary→JSON: decode the view, walk its vtable `ReflectMessage` surface to rebuild a `DynamicMessage`, then serialize to JSON. Verifies the generated `impl ReflectMessage for FooView`. It reuses `DynamicMessage`'s JSON serializer (which passes the corpus cleanly under via-reflect), so any failure isolates a bug in the vtable `get`/`has`/`for_each_set` surface. Requires the conformance crate's `reflect` feature, so it is absent from the no_std binary. -**Expected failures** are listed in `conformance/known_failures.txt` (std binary+JSON), `conformance/known_failures_nostd.txt` (no_std binary+JSON), `conformance/known_failures_view.txt` (via-view), `conformance/known_failures_lazy.txt` (via-lazy), `conformance/known_failures_view_json.txt` (view-json), `conformance/known_failures_reflect.txt` (via-reflect), `conformance/known_failures_view_vtable.txt` (via-vtable), and `conformance/known_failures_text.txt` (text format — shared between std and no_std; currently empty). The text list is passed via `--text_format_failure_list` since the runner validates each suite's list independently. When a previously-failing test starts passing, remove it from the relevant file; when a new test is expected to fail, add it. +**via-table** is a separate binary, built with `--features table` (no env var), that runs the whole std run — binary, JSON and text — against test messages generated with `CodecStrategy::Table`. It exercises the table codec's decode, size and encode paths, oneofs, maps and the bridge to well-known-type children on the proto3, editions and nested proto2 messages. The messages the table cannot handle run through the unrolled codec instead: messages with extension ranges under JSON, `MessageSet` messages, group types and the messages that hold them. That includes the top-level `TestAllTypesProto2`, which via-table therefore does not exercise on the table. `buffa-build` prints a `cargo:warning` naming the messages that fell back, and a compile-time check in `conformance/src/main.rs` fails the build if the messages that must use the table stop doing so. The `table` feature needs Rust 1.77; `conformance/Cargo.toml` says why. + +**Expected failures** are listed in `conformance/known_failures.txt` (std binary+JSON), `conformance/known_failures_nostd.txt` (no_std binary+JSON), `conformance/known_failures_view.txt` (via-view), `conformance/known_failures_lazy.txt` (via-lazy), `conformance/known_failures_view_json.txt` (view-json), `conformance/known_failures_reflect.txt` (via-reflect), `conformance/known_failures_view_vtable.txt` (via-vtable), `conformance/known_failures_table.txt` (via-table; currently empty), and `conformance/known_failures_text.txt` (text format — shared between std, no_std and table; currently empty). The text list is passed via `--text_format_failure_list` since the runner validates each suite's list independently. When a previously-failing test starts passing, remove it from the relevant file; when a new test is expected to fail, add it. **Capturing output**: To save per-run logs for analysis, mount a directory and set `CONFORMANCE_OUT`: ```bash docker run --rm -v /tmp/conf:/out -e CONFORMANCE_OUT=/out buffa-conformance -# logs: /tmp/conf/conformance-{std,nostd,view,lazy,view-json,reflect,vtable}.log +# logs: /tmp/conf/conformance-{std,nostd,view,lazy,view-json,reflect,vtable,table}.log ``` **Upgrading the protobuf version**: bump `TOOLS_IMAGE` in `Taskfile.yml` and `PROTOC_VERSION` in `.github/workflows/ci.yml`, then: diff --git a/Taskfile.yml b/Taskfile.yml index 3e0ac5ac..d5a59b4a 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -77,9 +77,9 @@ tasks: conformance-local: desc: >- - Run the full six-run conformance suite without Docker, using the + Run the full eight-run conformance suite without Docker, using the runner from conformance-tools-local and host cargo builds of the - std/no_std conformance binaries. Requires protoc v30+ on PATH or + std/table/no_std conformance binaries. Requires protoc v30+ on PATH or $PROTOC. cmds: - scripts/run-conformance-local.sh diff --git a/conformance/Cargo.toml b/conformance/Cargo.toml index e35a6368..61dc4702 100644 --- a/conformance/Cargo.toml +++ b/conformance/Cargo.toml @@ -31,6 +31,10 @@ default = ["buffa-std"] # whose generated code therefore omits the reflect impls. buffa-std = ["buffa/std", "buffa-types/std", "reflect"] reflect = ["buffa-types/reflect"] +# Generates the test messages with `CodecStrategy::Table`, for the via-table +# run. Needs Rust 1.77 (`offset_of!`); buffa-build refuses older compilers, so +# the default build stays at the 1.75 MSRV. +table = [] [dependencies] buffa = { path = "../buffa", default-features = false, features = ["json", "text"] } diff --git a/conformance/Dockerfile b/conformance/Dockerfile index 7e21daf8..7e45f3bd 100644 --- a/conformance/Dockerfile +++ b/conformance/Dockerfile @@ -35,9 +35,18 @@ COPY buffa-types/ buffa-types/ COPY protoc-gen-buffa/ protoc-gen-buffa/ COPY conformance/ conformance/ -RUN cargo build --release --manifest-path conformance/Cargo.toml -RUN cargo build --release --manifest-path conformance/Cargo.toml \ - --no-default-features --target-dir conformance/target-nostd +# All three builds share one target dir, so the dependencies and build scripts +# that do not depend on the features (buffa-codegen, buffa-build, syn, ...) are +# compiled once. Each binary is copied out before the next build overwrites it. +RUN mkdir /out \ + && cargo build --release --manifest-path conformance/Cargo.toml \ + && cp conformance/target/release/conformance /out/buffa-conformance \ + && cargo build --release --manifest-path conformance/Cargo.toml \ + --features table \ + && cp conformance/target/release/conformance /out/buffa-conformance-table \ + && cargo build --release --manifest-path conformance/Cargo.toml \ + --no-default-features \ + && cp conformance/target/release/conformance /out/buffa-conformance-nostd # ── Stage 3: runtime image ──────────────────────────────────────────────── FROM ubuntu:24.04 @@ -46,12 +55,12 @@ RUN apt-get update && apt-get install -y libstdc++6 libgcc-s1 \ && rm -rf /var/lib/apt/lists/* COPY --from=tools /conformance_test_runner /usr/local/bin/conformance_test_runner -COPY --from=rust-builder \ - /workspace/conformance/target/release/conformance /usr/local/bin/buffa-conformance -COPY --from=rust-builder \ - /workspace/conformance/target-nostd/release/conformance /usr/local/bin/buffa-conformance-nostd +COPY --from=rust-builder /out/buffa-conformance /usr/local/bin/buffa-conformance +COPY --from=rust-builder /out/buffa-conformance-nostd /usr/local/bin/buffa-conformance-nostd +COPY --from=rust-builder /out/buffa-conformance-table /usr/local/bin/buffa-conformance-table COPY conformance/known_failures.txt /known_failures.txt COPY conformance/known_failures_nostd.txt /known_failures_nostd.txt +COPY conformance/known_failures_table.txt /known_failures_table.txt COPY conformance/known_failures_view.txt /known_failures_view.txt COPY conformance/known_failures_lazy.txt /known_failures_lazy.txt COPY conformance/known_failures_view_json.txt /known_failures_view_json.txt diff --git a/conformance/build.rs b/conformance/build.rs index fdb66123..c8f37528 100644 --- a/conformance/build.rs +++ b/conformance/build.rs @@ -27,7 +27,7 @@ fn main() { // so the no_std binary — built `--no-default-features` — omits it. // TestAllTypesProto3 with serde + textproto enabled. - buffa_build::Config::new() + config() .files(&["protos/google/protobuf/test_messages_proto3.proto"]) .includes(&["protos/"]) .generate_json(true) @@ -39,7 +39,7 @@ fn main() { .expect("buffa_build failed for test_messages_proto3.proto"); // TestAllTypesProto2 with serde + textproto enabled for proto2 conformance. - buffa_build::Config::new() + config() .files(&["protos/google/protobuf/test_messages_proto2.proto"]) .includes(&["protos/"]) .generate_json(true) @@ -54,7 +54,7 @@ fn main() { // Editions test messages: proto3 behavior via editions. let editions_proto3 = protos_dir.join("editions/golden/test_messages_proto3_editions.proto"); if editions_proto3.exists() { - buffa_build::Config::new() + config() .files(&[&editions_proto3]) .includes(&[&protos_dir]) .generate_json(true) @@ -66,7 +66,7 @@ fn main() { .expect("buffa_build failed for test_messages_proto3_editions.proto"); // Editions test messages: proto2 behavior via editions. - buffa_build::Config::new() + config() .files(&["protos/editions/golden/test_messages_proto2_editions.proto"]) .includes(&["protos/"]) .generate_json(true) @@ -82,7 +82,7 @@ fn main() { // JSON enabled for the extension registry (the text `[pkg.ext]` bracket // syntax resolves through the same registry structs); text enabled for // the RunDelimitedTests suite. - buffa_build::Config::new() + config() .files(&["protos/conformance/test_protos/test_messages_edition2023.proto"]) .includes(&["protos/"]) .generate_json(true) @@ -103,6 +103,18 @@ fn main() { emit_reflect_fds(&manifest_dir); } +/// The generator configuration shared by every schema. With the crate's +/// `table` feature it selects [`buffa_build::CodecStrategy::Table`], so that +/// the via-table run drives the table codec through the corpus. +fn config() -> buffa_build::Config { + let config = buffa_build::Config::new(); + if std::env::var_os("CARGO_FEATURE_TABLE").is_some() { + config.codec_strategy(buffa_build::CodecStrategy::Table) + } else { + config + } +} + /// Write `OUT_DIR/conformance_protos.fds` containing the conformance test /// message types and their transitive imports. fn emit_reflect_fds(manifest_dir: &std::path::Path) { diff --git a/conformance/known_failures_table.txt b/conformance/known_failures_table.txt new file mode 100644 index 00000000..4a5c7264 --- /dev/null +++ b/conformance/known_failures_table.txt @@ -0,0 +1,9 @@ +# Known conformance test failures for the table codec (CodecStrategy::Table). +# +# Tests listed here are expected to fail in the via-table run and will not +# cause it to report a failure. Each line is a test name (matched by the +# runner with --failure_list). Remove entries as failures are fixed. +# +# The messages the table cannot handle run through the unrolled codec in this +# run, including the top-level TestAllTypesProto2, so its tests do not exercise +# the table. diff --git a/conformance/known_failures_text.txt b/conformance/known_failures_text.txt index cc6b28c4..35a6f91d 100644 --- a/conformance/known_failures_text.txt +++ b/conformance/known_failures_text.txt @@ -4,4 +4,5 @@ # The text-format suite runs separately from the binary+JSON suite and # validates this list independently. Remove entries as failures are fixed. # -# std and no_std share this file — textproto has no std-only paths. +# All text-format runs (std, no_std, table) share this file — textproto has +# no std-only paths. diff --git a/conformance/run-conformance.sh b/conformance/run-conformance.sh index d9bceeb6..29724504 100755 --- a/conformance/run-conformance.sh +++ b/conformance/run-conformance.sh @@ -1,5 +1,5 @@ #!/bin/bash -set -e +set -eo pipefail # The conformance_test_runner always runs two suites per invocation: # 1. Binary + JSON (expect thousands of successes) @@ -87,3 +87,11 @@ BUFFA_VIA_VTABLE=1 run_suite vtable \ --failure_list /known_failures_view_vtable.txt \ --maximum_edition 2024 \ /usr/local/bin/buffa-conformance + +# Via-table mode: the std run against a binary generated with CodecStrategy::Table. +run_suite table \ + conformance_test_runner \ + --failure_list /known_failures_table.txt \ + --text_format_failure_list /known_failures_text.txt \ + --maximum_edition 2024 \ + /usr/local/bin/buffa-conformance-table diff --git a/conformance/src/main.rs b/conformance/src/main.rs index b031098e..e80a2044 100644 --- a/conformance/src/main.rs +++ b/conformance/src/main.rs @@ -129,6 +129,30 @@ fn setup_type_registry() { set_type_registry(reg); } +// ── Table codec ────────────────────────────────────────────────────────── +// +// Built with the `table` feature, the test messages are generated with +// `CodecStrategy::Table` and this binary is the via-table run. A message that +// the strategy leaves unrolled has no `__BUFFA_TABLE_*` static, so naming the +// ones below fails the build if a fallback rule ever moves them back to the +// unrolled codec and the run would silently stop testing the table. +// `TestAllTypesProto2` is not among them: its extension ranges and group +// fields keep it unrolled, so proto2 reaches the table through its nested and +// sibling messages. + +#[cfg(all(not(no_protos), feature = "table"))] +const _: () = { + fn is_table(_: &'static buffa::table::Table) {} + fn pin() { + is_table(&proto3::__BUFFA_TABLE_TestAllTypesProto3); + is_table(&proto3::test_all_types_proto3::__BUFFA_TABLE_NestedMessage); + is_table(&proto2::__BUFFA_TABLE_TestLargeOneof); + is_table(&proto2::test_all_types_proto2::__BUFFA_TABLE_NestedMessage); + #[cfg(has_editions_protos)] + is_table(&editions_proto3::__BUFFA_TABLE_TestAllTypesProto3); + } +}; + // ── Via-view mode ──────────────────────────────────────────────────────── // // When `BUFFA_VIA_VIEW=1`, binary input is routed through diff --git a/scripts/run-conformance-local.sh b/scripts/run-conformance-local.sh index 869342c6..f5b59ecc 100755 --- a/scripts/run-conformance-local.sh +++ b/scripts/run-conformance-local.sh @@ -2,9 +2,9 @@ # Run the full protobuf conformance suite without Docker. # # Native equivalent of conformance/run-conformance.sh + conformance/Dockerfile: -# builds the std and no_std conformance binaries with the host cargo, then -# drives them through conformance_test_runner (built by -# scripts/build-conformance-tools.sh) for the same seven runs as the Docker +# builds the std, table and no_std conformance binaries with the host cargo, +# then drives them through conformance_test_runner (built by +# scripts/build-conformance-tools.sh) for the same eight runs as the Docker # image. The runner talks to the testee over stdin/stdout pipes, so no # container plumbing is needed. # @@ -21,6 +21,10 @@ ROOT="$(cd "$(dirname "$0")/.." && pwd)" RUNNER="${ROOT}/.local/bin/conformance_test_runner" CONF="${ROOT}/conformance" +# rust-toolchain.toml selects the toolchain from the working directory, so the +# builds below run from the repository root whatever the caller's directory is. +cd "${ROOT}" + if [ ! -x "${RUNNER}" ]; then echo "conformance_test_runner not found at ${RUNNER}." echo "Run: task conformance-tools-local" @@ -31,12 +35,15 @@ if [ ! -f "${CONF}/protos/conformance.proto" ]; then exit 1 fi -echo "=== Building conformance binaries (std + no_std) ===" +echo "=== Building conformance binaries (std + table + no_std) ===" cargo build --release --manifest-path "${CONF}/Cargo.toml" +cargo build --release --manifest-path "${CONF}/Cargo.toml" \ + --features table --target-dir "${CONF}/target-table" cargo build --release --manifest-path "${CONF}/Cargo.toml" \ --no-default-features --target-dir "${CONF}/target-nostd" STD_BIN="${CONF}/target/release/conformance" +TABLE_BIN="${CONF}/target-table/release/conformance" NOSTD_BIN="${CONF}/target-nostd/release/conformance" run_suite() { @@ -96,4 +103,11 @@ BUFFA_VIA_VTABLE=1 run_suite vtable \ --maximum_edition 2024 \ "${STD_BIN}" -echo "All seven conformance runs completed." +run_suite table \ + "${RUNNER}" \ + --failure_list "${CONF}/known_failures_table.txt" \ + --text_format_failure_list "${CONF}/known_failures_text.txt" \ + --maximum_edition 2024 \ + "${TABLE_BIN}" + +echo "All eight conformance runs completed."