Skip to content

Commit 03a1eb7

Browse files
Yuval ShavitDavidSeptimus
Yuval Shavit
authored andcommitted
add test to check input/output types
See godoc for the specific validations.
1 parent 0cddfb3 commit 03a1eb7

File tree

4 files changed

+300
-18
lines changed

4 files changed

+300
-18
lines changed

go.mod

+4-2
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ require (
2525
github.com/stretchr/testify v1.8.1
2626
go.uber.org/atomic v1.9.0
2727
go.uber.org/zap v1.19.1
28+
golang.org/x/tools v0.7.0
2829
gopkg.in/yaml.v3 v3.0.1
2930
helm.sh/helm/v3 v3.11.1
3031
k8s.io/api v0.26.0
@@ -40,6 +41,7 @@ require (
4041
github.com/mattn/go-sqlite3 v2.0.3+incompatible // indirect
4142
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect
4243
github.com/rivo/uniseg v0.4.4 // indirect
44+
golang.org/x/mod v0.9.0 // indirect
4345
golang.org/x/oauth2 v0.4.0 // indirect
4446
)
4547

@@ -136,11 +138,11 @@ require (
136138
go.starlark.net v0.0.0-20230112144946-fae38c8a6d89 // indirect
137139
go.uber.org/multierr v1.7.0 // indirect
138140
golang.org/x/crypto v0.5.0 // indirect
139-
golang.org/x/net v0.7.0 // indirect
141+
golang.org/x/net v0.8.0 // indirect
140142
golang.org/x/sync v0.1.0 // indirect
141143
golang.org/x/sys v0.6.0 // indirect
142144
golang.org/x/term v0.6.0 // indirect
143-
golang.org/x/text v0.7.0 // indirect
145+
golang.org/x/text v0.8.0 // indirect
144146
golang.org/x/time v0.3.0 // indirect
145147
google.golang.org/appengine v1.6.7 // indirect
146148
google.golang.org/genproto v0.0.0-20230113154510-dbe35b8444a5 // indirect

go.sum

+8-4
Original file line numberDiff line numberDiff line change
@@ -817,6 +817,8 @@ golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
817817
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
818818
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
819819
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
820+
golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs=
821+
golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
820822
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
821823
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
822824
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -865,8 +867,8 @@ golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT
865867
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
866868
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
867869
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
868-
golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
869-
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
870+
golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
871+
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
870872
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
871873
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
872874
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -991,8 +993,8 @@ golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
991993
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
992994
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
993995
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
994-
golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
995-
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
996+
golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
997+
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
996998
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
997999
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
9981000
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@@ -1061,6 +1063,8 @@ golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
10611063
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
10621064
golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
10631065
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
1066+
golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4=
1067+
golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s=
10641068
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
10651069
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
10661070
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

pkg/infra/iac2/inputs_outputs_test.go

+263
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
1+
package iac2
2+
3+
import (
4+
"fmt"
5+
"github.com/klothoplatform/klotho/pkg/core"
6+
"github.com/klothoplatform/klotho/pkg/infra/kubernetes"
7+
"github.com/klothoplatform/klotho/pkg/provider/aws/resources"
8+
"github.com/klothoplatform/klotho/pkg/provider/aws/resources/cloudwatch"
9+
"github.com/klothoplatform/klotho/pkg/provider/aws/resources/ecr"
10+
"github.com/klothoplatform/klotho/pkg/provider/aws/resources/iam"
11+
"github.com/klothoplatform/klotho/pkg/provider/aws/resources/lambda"
12+
"github.com/klothoplatform/klotho/pkg/provider/aws/resources/s3"
13+
"github.com/klothoplatform/klotho/pkg/provider/aws/resources/vpc"
14+
assert "github.com/stretchr/testify/assert"
15+
"go/types"
16+
"golang.org/x/tools/go/packages"
17+
"reflect"
18+
"strings"
19+
"testing"
20+
)
21+
22+
type (
23+
Methods struct {
24+
// signatures is the set of all methods declared on a type. Each signature follows the general format:
25+
//
26+
// <name> func(<args>) <return type>
27+
//
28+
// The args do not include the receiver type. For example:
29+
//
30+
// KlothoConstructRef func() []github.com/klothoplatform/klotho/pkg/core.AnnotationKey
31+
signatures map[string]struct{}
32+
isInterface bool
33+
}
34+
35+
TypeRef struct {
36+
pkg string
37+
name string
38+
}
39+
)
40+
41+
// TestKnownTemplates performs several tests to make sure that our Go structs match up with the factory.ts templates.
42+
//
43+
// For each known type, it checks:
44+
// - That there's a template for that struct
45+
// - That the template has a valid "output" type
46+
// - That for each input defined in the template's Args: (a) there is a corresponding field in the Go struct, and (b)
47+
// that the Go struct's field type matches with the Arg field type.
48+
//
49+
// To do the field-matching, we look at the Go struct's field type, and compute from it the expected TypeScript type.
50+
// For primitives (int, bool, string, etc) we just convert it to the corresponding TypeScript primitive. For structs,
51+
// we look up that struct's template and expect whatever the output of that template is. See the [iac2] package docs
52+
// for more ("Why a template for a template?").
53+
//
54+
// We don't check the template's return expression, because we assume that if it has a valid output type, it'll also
55+
// have a valid return expression. (Otherwise, our separate tsc checks will fail.)
56+
//
57+
// With all that done, we also check that we've validated all the structs in pkg/provider/aws/. To do this,
58+
// we use the reflective [packages.Load] to find all the types within that package, and then filter down to those types
59+
// that conform to core.Resource. Then we simply check that each one of those is in the list of types we checked.
60+
func TestKnownTemplates(t *testing.T) {
61+
var allResources = []core.Resource{
62+
&resources.Region{},
63+
&vpc.Vpc{},
64+
&vpc.VpcEndpoint{},
65+
&kubernetes.KlothoHelmChart{},
66+
&lambda.LambdaFunction{},
67+
&ecr.EcrImage{},
68+
&cloudwatch.LogGroup{},
69+
&vpc.ElasticIp{},
70+
&vpc.NatGateway{},
71+
&vpc.Subnet{},
72+
&vpc.InternetGateway{},
73+
&iam.IamRole{},
74+
&ecr.EcrRepository{},
75+
&s3.S3Bucket{},
76+
&resources.AccountId{},
77+
}
78+
79+
tp := standardTemplatesProvider()
80+
testedTypes := make(map[TypeRef]struct{})
81+
for _, res := range allResources {
82+
resType := reflect.TypeOf(res)
83+
for resType.Kind() == reflect.Pointer {
84+
resType = resType.Elem()
85+
}
86+
testedTypes[TypeRef{pkg: resType.PkgPath(), name: resType.Name()}] = struct{}{}
87+
t.Run(resType.String(), func(t *testing.T) {
88+
var tmpl ResourceCreationTemplate
89+
90+
tmplFound := t.Run("template exists", func(t *testing.T) {
91+
assert := assert.New(t)
92+
found, err := tp.getTemplate(res)
93+
if !assert.NoError(err) {
94+
return
95+
}
96+
tmpl = found
97+
})
98+
if !tmplFound {
99+
return
100+
}
101+
t.Run("output", func(t *testing.T) {
102+
assert := assert.New(t)
103+
assert.NotEmpty(tmpl.OutputType)
104+
})
105+
106+
t.Run("inputs", func(t *testing.T) {
107+
for inputName, inputTsType := range tmpl.InputTypes {
108+
if inputName == "dependsOn" {
109+
continue
110+
}
111+
t.Run(inputName, func(t *testing.T) {
112+
assert := assert.New(t)
113+
114+
field, fieldFound := resType.FieldByName(inputName)
115+
if !assert.Truef(fieldFound, `missing field`, field.Name) {
116+
return
117+
}
118+
assert.Truef(field.IsExported(), `field is not exported`, field.Name)
119+
120+
expectedType := &strings.Builder{}
121+
if err := buildExpectedTsType(expectedType, tp, field.Type); !assert.NoError(err) {
122+
return
123+
}
124+
assert.NotEmpty(expectedType, `couldn't determine expected type'`)
125+
assert.Equal(expectedType.String(), inputTsType, `field type`)
126+
})
127+
}
128+
})
129+
})
130+
}
131+
t.Run("all types tested", func(t *testing.T) {
132+
assert := assert.New(t)
133+
134+
// Find the methods for core.Resource
135+
var t2 reflect.Type = reflect.TypeOf((*core.Resource)(nil)).Elem()
136+
coreResourceRef := TypeRef{
137+
pkg: t2.PkgPath(),
138+
name: t2.Name(),
139+
}
140+
coreTypes, err := getTypesInPackage(coreResourceRef.pkg)
141+
if !assert.NoError(err) {
142+
return
143+
}
144+
coreResourceType := coreTypes[coreResourceRef]
145+
if !assert.NotEmptyf(coreResourceType, `couldn't find %v!`, coreResourceRef) {
146+
return
147+
}
148+
149+
// Find all structs that implement core.Resource
150+
resourcesTypes, err := getTypesInPackage("github.com/klothoplatform/klotho/pkg/provider/aws/...")
151+
if !assert.NoError(err) {
152+
return
153+
}
154+
for ref, methods := range resourcesTypes {
155+
// Ignore all interfaces, and all structs/typedefs that don't implement core.Resource
156+
if methods.isInterface || !methods.containsAllMethodsIn(coreResourceType) {
157+
continue
158+
}
159+
assert.Contains(testedTypes, ref)
160+
}
161+
})
162+
}
163+
164+
// buildExpectedTsType converts a Go type to an expected TypeScript type. For example, a map[string]int would translate
165+
// to Record<string, number>.
166+
func buildExpectedTsType(out *strings.Builder, tp *templatesProvider, t reflect.Type) error {
167+
// env vars are a special case
168+
if t.PkgPath() == `github.com/klothoplatform/klotho/pkg/provider/aws/resources` && t.Name() == `EnvironmentVariables` {
169+
out.WriteString(`Record<string, pulumi.Output<string>>`)
170+
return nil
171+
}
172+
173+
// ok, general cases now
174+
switch t.Kind() {
175+
case reflect.Bool:
176+
out.WriteString(`boolean`)
177+
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
178+
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64,
179+
reflect.Float32, reflect.Float64:
180+
out.WriteString(`number`)
181+
case reflect.String:
182+
out.WriteString(`string`)
183+
case reflect.Struct:
184+
res, err := tp.getTemplateForType(t.Name())
185+
if err != nil {
186+
return err
187+
}
188+
out.WriteString(res.OutputType)
189+
case reflect.Array, reflect.Slice:
190+
out.WriteString("[]")
191+
buildExpectedTsType(out, tp, t.Elem())
192+
case reflect.Map:
193+
out.WriteString("Record<")
194+
buildExpectedTsType(out, tp, t.Key())
195+
out.WriteString(", ")
196+
buildExpectedTsType(out, tp, t.Elem())
197+
out.WriteRune('>')
198+
case reflect.Pointer, reflect.Interface:
199+
// Interface happens when the value is inside a map, slice, or array. Basically, the reflected type is
200+
// interface{},instead of being the actual type. So, we basically pull the item out of the collection, and then
201+
// reflect on it directly.
202+
buildExpectedTsType(out, tp, t.Elem())
203+
}
204+
return nil
205+
}
206+
207+
// getTypesInPackage finds all types within a package (which may be "..."-ed).
208+
func getTypesInPackage(packageName string) (map[TypeRef]Methods, error) {
209+
config := &packages.Config{Mode: packages.LoadSyntax}
210+
pkgs, err := packages.Load(config, packageName)
211+
if err != nil {
212+
return nil, err
213+
}
214+
result := make(map[TypeRef]Methods)
215+
for _, pkg := range pkgs {
216+
for _, obj := range pkg.TypesInfo.Defs {
217+
if obj == nil {
218+
continue
219+
}
220+
if _, ok := obj.(*types.TypeName); !ok {
221+
continue
222+
}
223+
typ, ok := obj.Type().(*types.Named)
224+
if !ok {
225+
continue
226+
}
227+
key := TypeRef{
228+
pkg: pkg.PkgPath,
229+
name: obj.Name(),
230+
}
231+
result[key] = getMethods(typ)
232+
}
233+
}
234+
return result, nil
235+
}
236+
237+
func getMethods(t *types.Named) Methods {
238+
type hasMethods interface {
239+
NumMethods() int
240+
Method(int) *types.Func
241+
}
242+
result := Methods{}
243+
var tMethods hasMethods = t
244+
if underlyingInterface, ok := t.Underlying().(*types.Interface); ok {
245+
tMethods = underlyingInterface
246+
result.isInterface = true
247+
}
248+
result.signatures = make(map[string]struct{}, tMethods.NumMethods())
249+
for i := 0; i < tMethods.NumMethods(); i++ {
250+
method := tMethods.Method(i)
251+
result.signatures[fmt.Sprintf(`%s %s`, method.Name(), method.Type().String())] = struct{}{}
252+
}
253+
return result
254+
}
255+
256+
func (m Methods) containsAllMethodsIn(other Methods) bool {
257+
for sig, _ := range other.signatures {
258+
if _, exists := m.signatures[sig]; !exists {
259+
return false
260+
}
261+
}
262+
return true
263+
}

0 commit comments

Comments
 (0)