mirror of
https://github.com/zap-proto/go.git
synced 2026-10-30 15:05:00 +00:00
The parser moved to idl and the C++ backend was still holding the old one, so the two lines of work could not both be had. They meet here: emitcpp.go imports idl like emit.go does, and structSize moves to idl beside the Struct whose width it reports — a stated `@N` or where the last field ends is a property of the declaration, not of whoever is printing it. The grammar tests move with the grammar. Emission tests stay with the emitter. Every golden, Go and C++, is unchanged: the merge is a move, not a change of what the generator prints.
237 lines
7.4 KiB
Go
237 lines
7.4 KiB
Go
// SPDX-License-Identifier: BSD-3-Clause-Eco
|
|
// Copyright (C) 2025, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"github.com/zap-proto/go/idl"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// TestGoldenCPP emits per-struct C++ for every testdata schema and diffs each
|
|
// result against its fixture, exactly as TestGolden does for Go. Run with
|
|
// -update to regenerate.
|
|
func TestGoldenCPP(t *testing.T) {
|
|
matches, err := filepath.Glob("testdata/*.zap")
|
|
if err != nil {
|
|
t.Fatalf("glob: %v", err)
|
|
}
|
|
if len(matches) == 0 {
|
|
t.Fatal("no testdata/*.zap files")
|
|
}
|
|
for _, in := range matches {
|
|
t.Run(filepath.Base(in), func(t *testing.T) {
|
|
file := parseFile(t, in)
|
|
emitted, err := EmitCPP(file)
|
|
if err != nil {
|
|
t.Fatalf("emit %s: %v", in, err)
|
|
}
|
|
var names []string
|
|
for n := range emitted {
|
|
names = append(names, n)
|
|
}
|
|
sort.Strings(names)
|
|
for _, name := range names {
|
|
golden := filepath.Join("testdata", name+".golden")
|
|
if *update {
|
|
if err := os.WriteFile(golden, emitted[name], 0o644); err != nil {
|
|
t.Fatalf("write golden %s: %v", golden, err)
|
|
}
|
|
t.Logf("updated %s", golden)
|
|
continue
|
|
}
|
|
want, err := os.ReadFile(golden)
|
|
if err != nil {
|
|
t.Fatalf("read golden %s: %v (run with -update)", golden, err)
|
|
}
|
|
if string(want) != string(emitted[name]) {
|
|
t.Errorf("%s does not match golden", name)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestBackendsShareOneModel is the property that makes two backends safe: they
|
|
// read the same AST, so a field's offset and a method's ordinal appear in the
|
|
// C++ output exactly where the Go output has them. If the two ever drifted,
|
|
// this is where it would show — no schema needs to be written twice.
|
|
func TestBackendsShareOneModel(t *testing.T) {
|
|
file := parseFile(t, "testdata/basetx.zap")
|
|
goSrc, _, err := emitSingleString(EmitSingle, file)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cppSrc, _, err := emitSingleString(EmitCPPSingle, file)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// gofmt aligns the Go const block, so compare on collapsed whitespace.
|
|
goSrc, cppSrc = collapse(goSrc), collapse(cppSrc)
|
|
for _, f := range file.Structs[0].Fields {
|
|
goWant := fmt.Sprintf("baseTx%sOff = %d", f.Name, f.Offset)
|
|
cppWant := fmt.Sprintf("kBaseTx%sOff = %d", f.Name, f.Offset)
|
|
if !strings.Contains(goSrc, goWant) {
|
|
t.Errorf("Go output missing %q", goWant)
|
|
}
|
|
if !strings.Contains(cppSrc, cppWant) {
|
|
t.Errorf("C++ output missing %q", cppWant)
|
|
}
|
|
}
|
|
|
|
echo := parseFile(t, "testdata/echo.zap")
|
|
goSrc, _, err = emitSingleString(EmitSingle, echo)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cppSrc, _, err = emitSingleString(EmitCPPSingle, echo)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
goSrc, cppSrc = collapse(goSrc), collapse(cppSrc)
|
|
for _, m := range echo.Interfaces[0].Methods {
|
|
name := ordinalName(echo.Interfaces[0], m)
|
|
if !strings.Contains(goSrc, fmt.Sprintf("%s uint32 = %d", name, m.Ordinal)) {
|
|
t.Errorf("Go output missing ordinal %s = %d", name, m.Ordinal)
|
|
}
|
|
if !strings.Contains(cppSrc, fmt.Sprintf("k%s = %d", name, m.Ordinal)) {
|
|
t.Errorf("C++ output missing ordinal k%s = %d", name, m.Ordinal)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestCPPWritesTheSameWireVersionAsGo pins the header both generated builders
|
|
// write. The two runtimes default differently, so both backends name the
|
|
// version, and the one they name is 2 — the header every vector in the Lux
|
|
// chain corpus opens with.
|
|
func TestCPPWritesTheSameWireVersionAsGo(t *testing.T) {
|
|
file := parseFile(t, "testdata/basetx.zap")
|
|
src, _, err := emitSingleString(EmitCPPSingle, file)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(src, "zap::Builder b(256, zap::kVersion2)") {
|
|
t.Error("generated C++ builder does not write the chains' wire version")
|
|
}
|
|
}
|
|
|
|
// TestCPPFixedFieldIsAlwaysItsWidth guards the divergence the corpus caught:
|
|
// Go's [N]byte accessor answers N zero bytes for a buffer too short to hold
|
|
// the field, so the C++ span accessor has to answer N zero bytes too.
|
|
func TestCPPFixedFieldIsAlwaysItsWidth(t *testing.T) {
|
|
file := parseFile(t, "testdata/basetx.zap")
|
|
src, _, err := emitSingleString(EmitCPPSingle, file)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(src, "std::array<std::uint8_t, 32> zero{}") ||
|
|
!strings.Contains(src, "b.size() == 32 ? b : std::span<const std::uint8_t>(zero)") {
|
|
t.Error("generated C++ bytes_fixed accessor is not total in shape")
|
|
}
|
|
}
|
|
|
|
// TestCPPNestedStructOrderIsFree checks that a struct may name one declared
|
|
// after it: the classes are forward-declared and the accessors that return one
|
|
// are defined after every class body, under their qualified name so a field
|
|
// may carry the name of its own type.
|
|
func TestCPPNestedStructOrderIsFree(t *testing.T) {
|
|
src := []byte(`package p
|
|
struct A {
|
|
Child B @0
|
|
}
|
|
struct B {
|
|
N u32 @0
|
|
}`)
|
|
file, err := idl.Parse("order.zap", src)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
out, _, err := emitSingleString(EmitCPPSingle, file)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(out, "class A;\nclass B;") {
|
|
t.Error("classes are not forward-declared")
|
|
}
|
|
want := "inline ::p::B A::Child() const { return ::p::B(o_.object(kAChildOff)); }"
|
|
if !strings.Contains(out, want) {
|
|
t.Error("nested accessor is not defined out of line, qualified")
|
|
}
|
|
if strings.Index(out, "class B {") > strings.Index(out, want) {
|
|
t.Error("nested accessor is defined before the class it returns")
|
|
}
|
|
}
|
|
|
|
// TestUnknownLangIsRefused keeps the backend table honest: an unknown -lang is
|
|
// an error, not a silent fallback to Go.
|
|
func TestUnknownLangIsRefused(t *testing.T) {
|
|
if _, _, err := backend("rust"); err == nil {
|
|
t.Error("backend(\"rust\") should fail until a Rust emitter exists")
|
|
}
|
|
for _, lang := range []string{"go", "cpp"} {
|
|
if _, _, err := backend(lang); err != nil {
|
|
t.Errorf("backend(%q): %v", lang, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// collapse squeezes runs of spaces and tabs to one space, so an assertion
|
|
// about what a line says does not also assert how gofmt aligned it.
|
|
func collapse(s string) string { return strings.Join(strings.Fields(s), " ") }
|
|
|
|
func parseFile(t *testing.T, path string) *idl.File {
|
|
t.Helper()
|
|
src, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatalf("read %s: %v", path, err)
|
|
}
|
|
file, err := idl.Parse(path, src)
|
|
if err != nil {
|
|
t.Fatalf("parse %s: %v", path, err)
|
|
}
|
|
return file
|
|
}
|
|
|
|
func emitSingleString(emit func(*idl.File) (string, []byte, error), f *idl.File) (string, string, error) {
|
|
name, body, err := emit(f)
|
|
return string(body), name, err
|
|
}
|
|
|
|
// TestWhitespaceEquivalenceCPP is TestWhitespaceEquivalence's other half: the
|
|
// brace fixture and its indentation-only twin generate byte-identical C++ too.
|
|
// The desugar is shared, so this is what it means for it to be shared.
|
|
func TestWhitespaceEquivalenceCPP(t *testing.T) {
|
|
brace := emitAllCPP(t, "testdata/packed.zap", "schema.zap")
|
|
ws := emitAllCPP(t, "testdata/ws/packed_ws.zap", "schema.zap")
|
|
if len(brace) != len(ws) {
|
|
t.Fatalf("file-count mismatch: brace=%d ws=%d", len(brace), len(ws))
|
|
}
|
|
for name, want := range brace {
|
|
got, ok := ws[name]
|
|
if !ok {
|
|
t.Errorf("ws missing generated file %q", name)
|
|
continue
|
|
}
|
|
if string(got) != string(want) {
|
|
t.Errorf("%s: ws C++ output != brace C++ output", name)
|
|
}
|
|
}
|
|
}
|
|
|
|
func emitAllCPP(t *testing.T, path, srcName string) map[string][]byte {
|
|
t.Helper()
|
|
file := parseFile(t, path)
|
|
file.Source = srcName
|
|
out, err := EmitCPP(file)
|
|
if err != nil {
|
|
t.Fatalf("emit cpp %s: %v", path, err)
|
|
}
|
|
return out
|
|
}
|