472 lines
12 KiB
Go
472 lines
12 KiB
Go
|
package main
|
||
|
|
||
|
import (
|
||
|
"bytes"
|
||
|
"fmt"
|
||
|
"go/parser"
|
||
|
"go/token"
|
||
|
"io/ioutil"
|
||
|
"path/filepath"
|
||
|
"sort"
|
||
|
"strconv"
|
||
|
"strings"
|
||
|
"testing"
|
||
|
|
||
|
"zombiezen.com/go/capnproto2"
|
||
|
"zombiezen.com/go/capnproto2/encoding/text"
|
||
|
"zombiezen.com/go/capnproto2/internal/schema"
|
||
|
)
|
||
|
|
||
|
func readTestFile(name string) ([]byte, error) {
|
||
|
path := filepath.Join("testdata", name)
|
||
|
return ioutil.ReadFile(path)
|
||
|
}
|
||
|
|
||
|
func mustReadTestFile(t *testing.T, name string) []byte {
|
||
|
data, err := readTestFile(name)
|
||
|
if err != nil {
|
||
|
t.Fatal(err)
|
||
|
}
|
||
|
return data
|
||
|
}
|
||
|
|
||
|
func mustReadGeneratorRequest(t *testing.T, name string) schema.CodeGeneratorRequest {
|
||
|
data := mustReadTestFile(t, name)
|
||
|
msg, err := capnp.Unmarshal(data)
|
||
|
if err != nil {
|
||
|
t.Fatalf("Unmarshaling %s: %v", name, err)
|
||
|
}
|
||
|
req, err := schema.ReadRootCodeGeneratorRequest(msg)
|
||
|
if err != nil {
|
||
|
t.Fatalf("Reading code generator request %s: %v", name, err)
|
||
|
}
|
||
|
return req
|
||
|
}
|
||
|
|
||
|
func TestBuildNodeMap(t *testing.T) {
|
||
|
tests := []struct {
|
||
|
name string
|
||
|
fileID uint64
|
||
|
fileNodes []uint64
|
||
|
}{
|
||
|
{
|
||
|
name: "go.capnp.out",
|
||
|
fileID: 0xd12a1c51fedd6c88,
|
||
|
fileNodes: []uint64{
|
||
|
0xbea97f1023792be0,
|
||
|
0xe130b601260e44b5,
|
||
|
0xc58ad6bd519f935e,
|
||
|
0xa574b41924caefc7,
|
||
|
0xc8768679ec52e012,
|
||
|
0xfa10659ae02f2093,
|
||
|
0xc2b96012172f8df1,
|
||
|
},
|
||
|
},
|
||
|
{
|
||
|
name: "group.capnp.out",
|
||
|
fileID: 0x83c2b5818e83ab19,
|
||
|
fileNodes: []uint64{
|
||
|
0xd119fd352d8ea888, // the struct
|
||
|
0x822357857e5925d4, // the group
|
||
|
},
|
||
|
},
|
||
|
}
|
||
|
for _, test := range tests {
|
||
|
data, err := readTestFile(test.name)
|
||
|
if err != nil {
|
||
|
t.Errorf("readTestFile(%q): %v", test.name, err)
|
||
|
continue
|
||
|
}
|
||
|
msg, err := capnp.Unmarshal(data)
|
||
|
if err != nil {
|
||
|
t.Errorf("Unmarshaling %s: %v", test.name, err)
|
||
|
continue
|
||
|
}
|
||
|
req, err := schema.ReadRootCodeGeneratorRequest(msg)
|
||
|
if err != nil {
|
||
|
t.Errorf("Reading code generator request %s: %v", test.name, err)
|
||
|
continue
|
||
|
}
|
||
|
nodes, err := buildNodeMap(req)
|
||
|
if err != nil {
|
||
|
t.Errorf("%s: buildNodeMap: %v", test.name, err)
|
||
|
}
|
||
|
f := nodes[test.fileID]
|
||
|
if f == nil {
|
||
|
t.Errorf("%s: node map is missing file node @%#x", test.name, test.fileID)
|
||
|
continue
|
||
|
}
|
||
|
if f.Id() != test.fileID {
|
||
|
t.Errorf("%s: node map has ID @%#x for lookup of @%#x", test.name, f.Id(), test.fileID)
|
||
|
}
|
||
|
|
||
|
// Test node.nodes collection
|
||
|
for _, id := range test.fileNodes {
|
||
|
found := false
|
||
|
for _, fn := range f.nodes {
|
||
|
if fn.Id() == id {
|
||
|
found = true
|
||
|
break
|
||
|
}
|
||
|
}
|
||
|
if !found {
|
||
|
t.Errorf("%s: missing @%#x from file nodes", test.name, id)
|
||
|
}
|
||
|
}
|
||
|
// Test map lookup
|
||
|
for _, k := range test.fileNodes {
|
||
|
n := nodes[k]
|
||
|
if n == nil {
|
||
|
t.Errorf("%s: missing @%#x from node map", test.name, k)
|
||
|
}
|
||
|
if n.Id() != k {
|
||
|
t.Errorf("%s: node map has ID @%#x for lookup of @%#x", test.name, n.Id(), k)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func TestRemoteScope(t *testing.T) {
|
||
|
type scopeTest struct {
|
||
|
name string
|
||
|
constID uint64
|
||
|
initImports []importSpec
|
||
|
|
||
|
remoteName string
|
||
|
remoteNew string
|
||
|
imports []importSpec
|
||
|
}
|
||
|
tests := []scopeTest{
|
||
|
{
|
||
|
name: "same-file struct",
|
||
|
constID: 0x84efedc75e99768d, // scopes.fooVar
|
||
|
remoteName: "Foo",
|
||
|
remoteNew: "NewFoo",
|
||
|
},
|
||
|
{
|
||
|
name: "different file struct",
|
||
|
constID: 0x836faf1834d91729, // scopes.otherFooVar
|
||
|
remoteName: "otherscopes.Foo",
|
||
|
remoteNew: "otherscopes.NewFoo",
|
||
|
imports: []importSpec{
|
||
|
{name: "otherscopes", path: "zombiezen.com/go/capnproto2/capnpc-go/testdata/otherscopes"},
|
||
|
},
|
||
|
},
|
||
|
{
|
||
|
name: "same-file struct list",
|
||
|
constID: 0xcda2680ec5c921e0, // scopes.fooListVar
|
||
|
remoteName: "Foo_List",
|
||
|
remoteNew: "NewFoo_List",
|
||
|
},
|
||
|
{
|
||
|
name: "different file struct list",
|
||
|
constID: 0x83e7e1b3cd1be338, // scopes.otherFooListVar
|
||
|
remoteName: "otherscopes.Foo_List",
|
||
|
remoteNew: "otherscopes.NewFoo_List",
|
||
|
imports: []importSpec{
|
||
|
{name: "otherscopes", path: "zombiezen.com/go/capnproto2/capnpc-go/testdata/otherscopes"},
|
||
|
},
|
||
|
},
|
||
|
{
|
||
|
name: "built-in Int32 list",
|
||
|
constID: 0xacf3d9917d0bb0f0, // scopes.intList
|
||
|
remoteName: "capnp.Int32List",
|
||
|
remoteNew: "capnp.NewInt32List",
|
||
|
imports: []importSpec{
|
||
|
{name: "capnp", path: "zombiezen.com/go/capnproto2"},
|
||
|
},
|
||
|
},
|
||
|
}
|
||
|
req := mustReadGeneratorRequest(t, "scopes.capnp.out")
|
||
|
nodes, err := buildNodeMap(req)
|
||
|
if err != nil {
|
||
|
t.Fatal("buildNodeMap:", err)
|
||
|
}
|
||
|
collect := func(test scopeTest) (g *generator, typ schema.Type, from *node, ok bool) {
|
||
|
g = newGenerator(0xd68755941d99d05e, nodes, genoptions{})
|
||
|
v := nodes[test.constID]
|
||
|
if v == nil {
|
||
|
t.Errorf("Can't find const @%#x for %s test", test.constID, test.name)
|
||
|
return nil, schema.Type{}, nil, false
|
||
|
}
|
||
|
if v.Which() != schema.Node_Which_const {
|
||
|
t.Errorf("Type of node @%#x in %s test is a %v node; want const. Check the test.", test.constID, test.name, v.Which())
|
||
|
return nil, schema.Type{}, nil, false
|
||
|
}
|
||
|
constType, _ := v.Const().Type()
|
||
|
for _, i := range test.initImports {
|
||
|
g.imports.add(i)
|
||
|
}
|
||
|
return g, constType, v, true
|
||
|
}
|
||
|
for _, test := range tests {
|
||
|
g, typ, from, ok := collect(test)
|
||
|
if !ok {
|
||
|
continue
|
||
|
}
|
||
|
rn, err := g.RemoteTypeName(typ, from)
|
||
|
if err != nil {
|
||
|
t.Errorf("%s: g.RemoteTypeName(nodes[%#x].Const().Type(), nodes[%#x]) error: %v", test.name, test.constID, test.constID, err)
|
||
|
continue
|
||
|
}
|
||
|
if rn != test.remoteName {
|
||
|
t.Errorf("%s: g.RemoteTypeName(nodes[%#x].Const().Type(), nodes[%#x]) = %q; want %q", test.name, test.constID, test.constID, rn, test.remoteName)
|
||
|
continue
|
||
|
}
|
||
|
if !hasExactImports(test.imports, g.imports) {
|
||
|
t.Errorf("%s: g.RemoteTypeName(nodes[%#x].Const().Type(), nodes[%#x]); g.imports = %s; want %s", test.name, test.constID, test.constID, formatImportSpecs(g.imports.usedImports()), formatImportSpecs(test.imports))
|
||
|
continue
|
||
|
}
|
||
|
}
|
||
|
for _, test := range tests {
|
||
|
g, typ, from, ok := collect(test)
|
||
|
if !ok {
|
||
|
continue
|
||
|
}
|
||
|
rn, err := g.RemoteTypeNew(typ, from)
|
||
|
if err != nil {
|
||
|
t.Errorf("%s: g.RemoteTypeNew(nodes[%#x].Const().Type(), nodes[%#x]) error: %v", test.name, test.constID, test.constID, err)
|
||
|
continue
|
||
|
}
|
||
|
if rn != test.remoteNew {
|
||
|
t.Errorf("%s: g.RemoteTypeNew(nodes[%#x].Const().Type(), nodes[%#x]) = %q; want %q", test.name, test.constID, test.constID, rn, test.remoteNew)
|
||
|
continue
|
||
|
}
|
||
|
if !hasExactImports(test.imports, g.imports) {
|
||
|
t.Errorf("%s: g.RemoteTypeNew(nodes[%#x].Const().Type(), nodes[%#x]); g.imports = %s; want %s", test.name, test.constID, test.constID, formatImportSpecs(g.imports.usedImports()), formatImportSpecs(test.imports))
|
||
|
continue
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func hasExactImports(specs []importSpec, imp imports) bool {
|
||
|
used := imp.usedImports()
|
||
|
if len(used) != len(specs) {
|
||
|
return false
|
||
|
}
|
||
|
outer:
|
||
|
for i := range specs {
|
||
|
for j := range used {
|
||
|
if specs[i] == used[j] {
|
||
|
continue outer
|
||
|
}
|
||
|
}
|
||
|
return false
|
||
|
}
|
||
|
return true
|
||
|
}
|
||
|
|
||
|
func formatImportSpecs(specs []importSpec) string {
|
||
|
var buf bytes.Buffer
|
||
|
for i, s := range specs {
|
||
|
if i > 0 {
|
||
|
buf.WriteString("; ")
|
||
|
}
|
||
|
buf.WriteString(s.String())
|
||
|
}
|
||
|
return buf.String()
|
||
|
}
|
||
|
|
||
|
func TestDefineConstNodes(t *testing.T) {
|
||
|
req := mustReadGeneratorRequest(t, "const.capnp.out")
|
||
|
nodes, err := buildNodeMap(req)
|
||
|
if err != nil {
|
||
|
t.Fatal("buildNodeMap:", err)
|
||
|
}
|
||
|
g := newGenerator(0xc260cb50ae622e10, nodes, genoptions{})
|
||
|
getCalls := traceGenerator(g)
|
||
|
err = g.defineConstNodes(nodes[0xc260cb50ae622e10].nodes)
|
||
|
if err != nil {
|
||
|
t.Fatal("defineConstNodes:", err)
|
||
|
}
|
||
|
calls := getCalls()
|
||
|
if len(calls) != 1 {
|
||
|
t.Fatalf("defineConstNodes called %d templates; want 1", len(calls))
|
||
|
}
|
||
|
p, ok := calls[0].params.(constantsParams)
|
||
|
if calls[0].name != "constants" || !ok {
|
||
|
t.Fatalf("defineConstNodes rendered %v; want render of constants template", calls[0])
|
||
|
}
|
||
|
if !containsExactlyIDs(p.Consts, 0xda96e2255811b258) {
|
||
|
t.Errorf("defineConstNodes rendered Consts %s", nodeListString(p.Consts))
|
||
|
}
|
||
|
if !containsExactlyIDs(p.Vars, 0xe0a385c7be1fea4d) {
|
||
|
t.Errorf("defineConstNodes rendered Vars %s", nodeListString(p.Vars))
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func TestDefineFile(t *testing.T) {
|
||
|
// Sanity check to make sure codegen produces parseable Go.
|
||
|
|
||
|
const iterations = 3
|
||
|
|
||
|
defaultOptions := genoptions{
|
||
|
promises: true,
|
||
|
schemas: true,
|
||
|
structStrings: true,
|
||
|
}
|
||
|
tests := []struct {
|
||
|
fileID uint64
|
||
|
fname string
|
||
|
opts genoptions
|
||
|
}{
|
||
|
{0x832bcc6686a26d56, "aircraft.capnp.out", defaultOptions},
|
||
|
{0x832bcc6686a26d56, "aircraft.capnp.out", genoptions{
|
||
|
promises: false,
|
||
|
schemas: false,
|
||
|
structStrings: false,
|
||
|
}},
|
||
|
{0x832bcc6686a26d56, "aircraft.capnp.out", genoptions{
|
||
|
promises: true,
|
||
|
schemas: false,
|
||
|
structStrings: false,
|
||
|
}},
|
||
|
{0x832bcc6686a26d56, "aircraft.capnp.out", genoptions{
|
||
|
promises: false,
|
||
|
schemas: true,
|
||
|
structStrings: false,
|
||
|
}},
|
||
|
{0x832bcc6686a26d56, "aircraft.capnp.out", genoptions{
|
||
|
promises: true,
|
||
|
schemas: true,
|
||
|
structStrings: false,
|
||
|
}},
|
||
|
{0x832bcc6686a26d56, "aircraft.capnp.out", genoptions{
|
||
|
promises: false,
|
||
|
schemas: true,
|
||
|
structStrings: true,
|
||
|
}},
|
||
|
{0x83c2b5818e83ab19, "group.capnp.out", defaultOptions},
|
||
|
{0xb312981b2552a250, "rpc.capnp.out", defaultOptions},
|
||
|
{0xd68755941d99d05e, "scopes.capnp.out", defaultOptions},
|
||
|
{0xecd50d792c3d9992, "util.capnp.out", defaultOptions},
|
||
|
}
|
||
|
for _, test := range tests {
|
||
|
data, err := readTestFile(test.fname)
|
||
|
if err != nil {
|
||
|
t.Errorf("reading %s: %v", test.fname, err)
|
||
|
continue
|
||
|
}
|
||
|
msg, err := capnp.Unmarshal(data)
|
||
|
if err != nil {
|
||
|
t.Errorf("Unmarshaling %s: %v", test.fname, err)
|
||
|
continue
|
||
|
}
|
||
|
req, err := schema.ReadRootCodeGeneratorRequest(msg)
|
||
|
if err != nil {
|
||
|
t.Errorf("Reading code generator request %s: %v", test.fname, err)
|
||
|
continue
|
||
|
}
|
||
|
nodes, err := buildNodeMap(req)
|
||
|
if err != nil {
|
||
|
t.Errorf("buildNodeMap %s: %v", test.fname, err)
|
||
|
continue
|
||
|
}
|
||
|
g := newGenerator(test.fileID, nodes, test.opts)
|
||
|
if err := g.defineFile(); err != nil {
|
||
|
t.Errorf("defineFile %s %+v: %v", test.fname, test.opts, err)
|
||
|
continue
|
||
|
}
|
||
|
src := g.generate()
|
||
|
if _, err := parser.ParseFile(token.NewFileSet(), test.fname+".go", src, 0); err != nil {
|
||
|
// TODO(light): log src
|
||
|
t.Errorf("generate %s %+v failed to parse: %v", test.fname, test.opts, err)
|
||
|
}
|
||
|
|
||
|
// Generation should be deterministic between runs.
|
||
|
for i := 0; i < iterations-1; i++ {
|
||
|
g := newGenerator(test.fileID, nodes, test.opts)
|
||
|
if err := g.defineFile(); err != nil {
|
||
|
t.Errorf("defineFile %s %+v [iteration %d]: %v", test.fname, test.opts, i+2, err)
|
||
|
continue
|
||
|
}
|
||
|
src2 := g.generate()
|
||
|
if !bytes.Equal(src, src2) {
|
||
|
t.Errorf("defineFile %s %+v [iteration %d] did not match iteration 1: non-deterministic", test.fname, test.opts, i+2)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func TestSchemaVarLiteral(t *testing.T) {
|
||
|
tests := []string{
|
||
|
"",
|
||
|
"foo",
|
||
|
"deadbeefdeadbeef",
|
||
|
"deadbeefdeadbeefdeadbeef",
|
||
|
"\x00\x00",
|
||
|
"\xff\xff",
|
||
|
"\n",
|
||
|
" ~\"\\",
|
||
|
"\xff\xff\x27\xa1\xe3\xf1",
|
||
|
}
|
||
|
for _, test := range tests {
|
||
|
got := schemaVarParams{schema: []byte(test)}.SchemaLiteral()
|
||
|
u, err := strconv.Unquote(strings.Replace(got, "\" +\n\t\"", "", -1))
|
||
|
if err != nil {
|
||
|
t.Errorf("schema literal of %q does not parse: %v\n\tproduced: %s", test, err, got)
|
||
|
} else if u != test {
|
||
|
t.Errorf("schema literal of %q != %s", test, got)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
type traceRenderer struct {
|
||
|
renderer
|
||
|
calls []renderCall
|
||
|
}
|
||
|
|
||
|
func traceGenerator(g *generator) (getCalls func() []renderCall) {
|
||
|
tr := &traceRenderer{renderer: g.r}
|
||
|
g.r = tr
|
||
|
return func() []renderCall { return tr.calls }
|
||
|
}
|
||
|
|
||
|
func (tr *traceRenderer) Render(name string, params interface{}) error {
|
||
|
tr.calls = append(tr.calls, renderCall{name, params})
|
||
|
return tr.renderer.Render(name, params)
|
||
|
}
|
||
|
|
||
|
type renderCall struct {
|
||
|
name string
|
||
|
params interface{}
|
||
|
}
|
||
|
|
||
|
func (rc renderCall) String() string {
|
||
|
return fmt.Sprintf("{%q %#v}", rc.name, rc.params)
|
||
|
}
|
||
|
|
||
|
func containsExactlyIDs(nodes []*node, ids ...uint64) bool {
|
||
|
if len(nodes) != len(ids) {
|
||
|
return false
|
||
|
}
|
||
|
sorted := make([]uint64, len(ids))
|
||
|
copy(sorted, ids)
|
||
|
sort.Sort(uint64Slice(sorted))
|
||
|
actual := make([]uint64, len(nodes))
|
||
|
for i := range nodes {
|
||
|
actual[i] = nodes[i].Id()
|
||
|
}
|
||
|
sort.Sort(uint64Slice(actual))
|
||
|
for i := range sorted {
|
||
|
if actual[i] != sorted[i] {
|
||
|
return false
|
||
|
}
|
||
|
}
|
||
|
return true
|
||
|
}
|
||
|
|
||
|
func nodeListString(n []*node) string {
|
||
|
b := new(bytes.Buffer)
|
||
|
e := text.NewEncoder(b)
|
||
|
b.WriteByte('[')
|
||
|
for i, nn := range n {
|
||
|
if i > 0 {
|
||
|
b.WriteByte(' ')
|
||
|
}
|
||
|
e.Encode(0xe682ab4cf923a417, nn.Struct)
|
||
|
}
|
||
|
b.WriteByte(']')
|
||
|
return b.String()
|
||
|
}
|