Skip to content

Commit

Permalink
feat: generate simple Create test cases
Browse files Browse the repository at this point in the history
  • Loading branch information
ericwenn committed Aug 8, 2021
1 parent b59755e commit 77b4e3a
Show file tree
Hide file tree
Showing 15 changed files with 795 additions and 1,670 deletions.
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ module github.com/einride/protoc-gen-go-aiptest
go 1.15

require (
github.com/stoewer/go-strcase v1.2.0
go.einride.tech/aip v0.44.0
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d // indirect
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069 // indirect
google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67
google.golang.org/grpc v1.39.1
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0
google.golang.org/protobuf v1.27.1
gotest.tools/v3 v3.0.3
)
7 changes: 7 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
Expand Down Expand Up @@ -43,11 +44,15 @@ github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/stoewer/go-strcase v1.2.0 h1:Z2iHWqGXH00XYgqDmNgQbIBxf3wrNq0F3feEy0ainaU=
github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
go.einride.tech/aip v0.44.0 h1:/3TMSgnMlYTIk8a5BifwsbJevfWkqOOSdQKAZgty9YU=
Expand Down Expand Up @@ -145,8 +150,10 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ=
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.3 h1:fvjTMHxHEw/mxHbtzPi3JCcKXQRAnQTBRo6YCJSVHKI=
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0=
gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8=
Expand Down
27 changes: 19 additions & 8 deletions internal/plugin/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ func Generate(plugin *protogen.Plugin) error {
if len(resources) == 0 {
continue
}
messages, err := findResourceMessages(protoRegistry, resources)
messages, err := findResourceMessages(plugin, resources)
if err != nil {
return err
}
Expand Down Expand Up @@ -105,16 +105,27 @@ func findServiceResources(
}

func findResourceMessages(
registry *protoregistry.Files,
plugin *protogen.Plugin,
resources []*aipreflect.ResourceDescriptor,
) ([]protoreflect.MessageDescriptor, error) {
msgs := make([]protoreflect.MessageDescriptor, 0, len(resources))
) ([]*protogen.Message, error) {
allMessages := allPluginMessages(plugin)
msgs := make([]*protogen.Message, 0, len(resources))
for _, resource := range resources {
msg, err := registry.FindDescriptorByName(resource.Message)
if err != nil {
return nil, fmt.Errorf("find descriptor for resource '%s': %w", resource.Type.Type(), err)
msg, ok := allMessages[resource.Message]
if !ok {
return nil, fmt.Errorf("found no message descriptor for resource '%s'", resource.Type.Type())
}
msgs = append(msgs, msg.(protoreflect.MessageDescriptor))
msgs = append(msgs, msg)
}
return msgs, nil
}

func allPluginMessages(plugin *protogen.Plugin) map[protoreflect.FullName]*protogen.Message {
msgs := make(map[protoreflect.FullName]*protogen.Message)
for _, file := range plugin.Files {
for _, message := range file.Messages {
msgs[message.Desc.FullName()] = message
}
}
return msgs
}
34 changes: 34 additions & 0 deletions internal/plugin/helper.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package plugin

import (
"github.com/stoewer/go-strcase"
"go.einride.tech/aip/reflect/aipreflect"
"google.golang.org/protobuf/compiler/protogen"
"google.golang.org/protobuf/reflect/protoreflect"
)

func hasParent(resource *aipreflect.ResourceDescriptor) bool {
if len(resource.Names) == 0 {
return false
}
return len(resource.Names[0].Ancestors) > 0
}

func findMethod(service *protogen.Service, methodName protoreflect.Name) (*protogen.Method, bool) {
for _, method := range service.Methods {
if method.Desc.Name() == methodName {
return method, true
}
}
return nil, false
}

func hasUserSettableID(resource *aipreflect.ResourceDescriptor, method protoreflect.MethodDescriptor) bool {
idField := strcase.LowerCamelCase(resource.Singular.UpperCamelCase()) + "_id"
return hasField(method.Input(), protoreflect.Name(idField))
}

func hasField(message protoreflect.MessageDescriptor, field protoreflect.Name) bool {
f := message.Fields().ByName(field)
return f != nil
}
36 changes: 36 additions & 0 deletions internal/plugin/method.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package plugin

import (
"go.einride.tech/aip/reflect/aipreflect"
"google.golang.org/protobuf/compiler/protogen"
)

type methodCreate struct {
resource *aipreflect.ResourceDescriptor
method *protogen.Method

parent string
message string
userSettableID string
}

func (m *methodCreate) Generate(f *protogen.GeneratedFile, response string, err string, assign string) {
f.P(response, ", ", err, " ", assign, " fx.service.", m.method.GoName, "(fx.ctx, &", m.method.Input.GoIdent, "{")
if hasParent(m.resource) {
f.P("Parent: ", m.parent, ",")
}

switch {
case m.message != "":
f.P(m.resource.Singular.UpperCamelCase(), ": ", m.message, ",")
case !hasParent(m.resource):
f.P(m.resource.Singular.UpperCamelCase(), ": fx.Create(),")
default:
f.P(m.resource.Singular.UpperCamelCase(), ": fx.Create(", m.parent, "),")
}

if hasUserSettableID(m.resource, m.method.Desc) && m.userSettableID != "" {
f.P(m.resource.Singular.UpperCamelCase(), "Id: ", m.userSettableID, ",")
}
f.P("})")
}
131 changes: 125 additions & 6 deletions internal/plugin/resource.go
Original file line number Diff line number Diff line change
@@ -1,34 +1,153 @@
package plugin

import (
"strconv"

"go.einride.tech/aip/reflect/aipreflect"
"google.golang.org/protobuf/compiler/protogen"
"google.golang.org/protobuf/reflect/protoreflect"
)

type resourceGenerator struct {
service *protogen.Service
resource *aipreflect.ResourceDescriptor
message protoreflect.MessageDescriptor
message *protogen.Message
}

func (r *resourceGenerator) Generate(f *protogen.GeneratedFile) error {
r.generateFixture(f)
r.generateTestMethod(f)
testCases := r.collectTestCases()
r.generateTestMethod(f, testCases)
r.generateTestCases(f, testCases)
r.generateParentMethods(f)
r.generateSkip(f)
return nil
}

func (r *resourceGenerator) generateFixture(f *protogen.GeneratedFile) {
context := f.QualifiedGoIdent(protogen.GoIdent{
GoName: "Context",
GoImportPath: "context",
})
service := f.QualifiedGoIdent(protogen.GoIdent{
GoName: r.service.GoName + "Server",
GoImportPath: r.service.Methods[0].Input.GoIdent.GoImportPath,
})

f.P("type ", r.resource.Type.Type(), " struct {")
f.P("ctx ", context)
f.P("service ", service)
f.P("currParent int")
f.P()

if hasParent(r.resource) {
f.P("// The parents to use when creating resources.")
f.P("// At least one parent needs to be set. Depending on methods available on the resource,")
f.P("// more may be required. If insufficient number of parents are")
f.P("// provided the test will fail.")
f.P("Parents []string")
}
_, hasCreate := r.resource.Methods[aipreflect.MethodTypeCreate]
if hasCreate {
f.P("// Create should return a resource which is valid to create, ie.")
f.P("// all required fields set.")
if hasParent(r.resource) {
f.P("Create func(parent string) *", r.message.GoIdent)
} else {
f.P("Create func() *", r.message.GoIdent)
}
}

f.P("// Patterns of tests to skip.")
f.P("// For example if a service has a Get method:")
f.P("// Skip: [\"Get\"] will skip all tests for Get.")
f.P("// Skip: [\"Get/persisted\"] will only skip the subtest called \"persisted\" of Get.")
f.P("Skip []string")
f.P("}")
f.P()
}

func (r *resourceGenerator) generateTestMethod(f *protogen.GeneratedFile) {
testing := f.QualifiedGoIdent(protogen.GoIdent{
func (r *resourceGenerator) generateTestMethod(f *protogen.GeneratedFile, testCases []testCase) {
testingT := f.QualifiedGoIdent(protogen.GoIdent{
GoName: "T",
GoImportPath: "testing",
})

f.P("func (fx *", r.resource.Type.Type(), ") test(t *", testing, ") {")
f.P("func (fx *", r.resource.Type.Type(), ") test(t *", testingT, ") {")
for _, tc := range testCases {
if !tc.enabled {
continue
}
f.P("t.Run(", strconv.Quote(tc.Name()), ", fx.", tc.FuncName(), ")")
}
f.P("}")
f.P()
}

func (r *resourceGenerator) generateTestCases(f *protogen.GeneratedFile, testCases []testCase) {
testingT := f.QualifiedGoIdent(protogen.GoIdent{
GoName: "T",
GoImportPath: "testing",
})
for _, tc := range testCases {
if !tc.enabled {
continue
}
f.P("func (fx *", r.resource.Type.Type(), ")", tc.FuncName(), "(t *", testingT, ") {")
tc.fn(f)
f.P("}")
f.P()
}
}

func (r *resourceGenerator) generateSkip(f *protogen.GeneratedFile) {
testingT := f.QualifiedGoIdent(protogen.GoIdent{
GoName: "T",
GoImportPath: "testing",
})
stringsContains := f.QualifiedGoIdent(protogen.GoIdent{
GoName: "Contains",
GoImportPath: "strings",
})
f.P("func (fx *", r.resource.Type.Type(), ") maybeSkip(t *", testingT, ") {")
f.P("for _, skip := range fx.Skip {")
f.P("if ", stringsContains, "(t.Name(), skip) {")
f.P("t.Skip(\"skipped because of .Skip\")")
f.P("}")
f.P("}")
f.P("}")
f.P()
}

func (r *resourceGenerator) generateParentMethods(f *protogen.GeneratedFile) {
if !hasParent(r.resource) {
return
}
testingT := f.QualifiedGoIdent(protogen.GoIdent{
GoName: "T",
GoImportPath: "testing",
})
f.P("func (fx *", r.resource.Type.Type(), ") nextParent(t *", testingT, ", pristine bool) string {")
f.P("if pristine {")
f.P("fx.currParent++")
f.P("}")
f.P("if fx.currParent >= len(fx.Parents) {")
f.P("t.Fatal(\"need at least\", fx.currParent + 1, \"parents\")")
f.P("}")
f.P("return fx.Parents[fx.currParent]")
f.P("}")
f.P()
f.P("func (fx *", r.resource.Type.Type(), ") peekNextParent(t *", testingT, ") string {")
f.P("next := fx.currParent + 1")
f.P("if next >= len(fx.Parents) {")
f.P("t.Fatal(\"need at least\", next +1, \"parents\")")
f.P("}")
f.P("return fx.Parents[next]")
f.P("}")
f.P()
}

func (r *resourceGenerator) collectTestCases() []testCase {
return []testCase{
r.createTestCase(),
}
}
6 changes: 4 additions & 2 deletions internal/plugin/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,12 @@ package plugin
import (
"go.einride.tech/aip/reflect/aipreflect"
"google.golang.org/protobuf/compiler/protogen"
"google.golang.org/protobuf/reflect/protoreflect"
)

type serviceGenerator struct {
service *protogen.Service
resources []*aipreflect.ResourceDescriptor
messages []protoreflect.MessageDescriptor
messages []*protogen.Message
}

func (s *serviceGenerator) Generate(f *protogen.GeneratedFile) error {
Expand All @@ -18,6 +17,7 @@ func (s *serviceGenerator) Generate(f *protogen.GeneratedFile) error {
for i, resource := range s.resources {
message := s.messages[i]
generator := resourceGenerator{
service: s.service,
resource: resource,
message: message,
}
Expand Down Expand Up @@ -62,6 +62,8 @@ func (s *serviceGenerator) generateTestMethods(f *protogen.GeneratedFile) {
for _, resource := range s.resources {
resourceFx := resource.Type.Type()
f.P("func (fx *", serviceFx, ") Test", resourceFx, "(t *", testing, ", options ", resourceFx, ") {")
f.P("options.ctx = fx.Context")
f.P("options.service = fx.Service")
f.P("options.test(t)")
f.P("}")
f.P()
Expand Down
29 changes: 29 additions & 0 deletions internal/plugin/testcase.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package plugin

import "google.golang.org/protobuf/compiler/protogen"

type testCase struct {
enabled bool
name string
fn func(file *protogen.GeneratedFile)
}

func disabledTestCase() testCase {
return testCase{}
}

func newTestCase(name string, fn func(f *protogen.GeneratedFile)) testCase {
return testCase{
enabled: true,
name: name,
fn: fn,
}
}

func (t testCase) Name() string {
return t.name
}

func (t testCase) FuncName() string {
return "test" + t.name
}
Loading

0 comments on commit 77b4e3a

Please sign in to comment.