From 7770117f6a566f317017be8e1ac7cce4eb57152a Mon Sep 17 00:00:00 2001 From: Ian Wahbe Date: Wed, 9 Oct 2024 17:58:50 +0200 Subject: [PATCH] [PF] Property based Configure tests --- pf/tests/internal/cross-tests/configure.go | 18 +- pf/tests/internal/cross-tests/util.go | 71 ++- pf/tests/provider_configure_test.go | 19 + .../util/property/pf/schema/provider/attr.go | 92 ++++ .../property/pf/schema/provider/schema.go | 446 ++++++++++++++++++ .../property/pf/value/provider/primitive.go | 51 ++ .../util/property/pf/value/provider/util.go | 160 +++++++ .../util/property/pf/value/provider/value.go | 402 ++++++++++++++++ 8 files changed, 1239 insertions(+), 20 deletions(-) create mode 100644 pf/tests/util/property/pf/schema/provider/attr.go create mode 100644 pf/tests/util/property/pf/schema/provider/schema.go create mode 100644 pf/tests/util/property/pf/value/provider/primitive.go create mode 100644 pf/tests/util/property/pf/value/provider/util.go create mode 100644 pf/tests/util/property/pf/value/provider/value.go diff --git a/pf/tests/internal/cross-tests/configure.go b/pf/tests/internal/cross-tests/configure.go index a7ad4bae0..a73af358b 100644 --- a/pf/tests/internal/cross-tests/configure.go +++ b/pf/tests/internal/cross-tests/configure.go @@ -81,7 +81,7 @@ func MakeConfigure(schema schema.Schema, tfConfig map[string]cty.Value, puConfig // +--------------------+ +---------------------+ // // Configure should be safe to run in parallel. -func Configure(t *testing.T, schema schema.Schema, tfConfig map[string]cty.Value, puConfig resource.PropertyMap) { +func Configure(t TestingT, schema schema.Schema, tfConfig map[string]cty.Value, puConfig resource.PropertyMap) { skipUnlessLinux(t) // By default, logs only show when they are on a failed test. By logging to @@ -103,8 +103,8 @@ func Configure(t *testing.T, schema schema.Schema, tfConfig map[string]cty.Value } var tfOutput, puOutput tfsdk.Config - t.Run("tf", func(t *testing.T) { - defer propageteSkip(topLevelT, t) + + withAugment(t, func(t augmentedT) { // --- Run Terraform Provider --- var hcl bytes.Buffer err := crosstests.WritePF(&hcl).Provider(schema, providerName, tfConfig) require.NoError(t, err) @@ -125,8 +125,7 @@ resource "` + providerName + `_res" "res" {} require.NoError(t, err) }) - t.Run("bridged", func(t *testing.T) { - defer propageteSkip(topLevelT, t) + withAugment(t, func(t augmentedT) { // --- Run Pulumi Provider --- dir := t.TempDir() pulumiYaml := map[string]any{ @@ -187,11 +186,6 @@ resource "` + providerName + `_res" "res" {} contract.Ignore(test.Up(t)) // Assert that the update succeeded, but not the result. }) - skipCompare := t.Failed() || t.Skipped() - t.Run("compare", func(t *testing.T) { - if skipCompare { - t.Skipf("skipping since earlier steps did not complete") - } - assert.Equal(t, tfOutput, puOutput) - }) + // --- Compare results ----------------------------- + assert.Equal(t, tfOutput, puOutput) } diff --git a/pf/tests/internal/cross-tests/util.go b/pf/tests/internal/cross-tests/util.go index b782ab371..790eb3df1 100644 --- a/pf/tests/internal/cross-tests/util.go +++ b/pf/tests/internal/cross-tests/util.go @@ -19,20 +19,26 @@ import ( "os" "runtime" "strings" - "testing" + "time" "github.com/pulumi/pulumi/sdk/v3/go/common/diag" "github.com/pulumi/pulumi/sdk/v3/go/common/resource" "github.com/stretchr/testify/require" ) -func propageteSkip(parent, child *testing.T) { - if child.Skipped() { - parent.Skipf("skipping due to skipped child test") - } +type TestingT interface { + Skip(args ...any) + Failed() bool + Errorf(format string, args ...any) + Name() string + Log(...any) + Logf(string, ...any) + Fail() + FailNow() + Helper() } -type testLogSink struct{ t *testing.T } +type testLogSink struct{ t TestingT } func (s testLogSink) Log(_ context.Context, sev diag.Severity, urn resource.URN, msg string) error { return s.log("LOG", sev, urn, msg) @@ -50,7 +56,7 @@ func (s testLogSink) log(kind string, sev diag.Severity, urn resource.URN, msg s return nil } -func convertResourceValue(t *testing.T, properties resource.PropertyMap) map[string]any { +func convertResourceValue(t TestingT, properties resource.PropertyMap) map[string]any { var convertValue func(resource.PropertyValue) (any, bool) convertValue = func(v resource.PropertyValue) (any, bool) { if v.IsComputed() { @@ -81,8 +87,57 @@ func convertResourceValue(t *testing.T, properties resource.PropertyMap) map[str return properties.MapRepl(nil, convertValue) } -func skipUnlessLinux(t *testing.T) { +func skipUnlessLinux(t TestingT) { if ci, ok := os.LookupEnv("CI"); ok && ci == "true" && !strings.Contains(strings.ToLower(runtime.GOOS), "linux") { t.Skip("Skipping on non-Linux platforms as our CI does not yet install Terraform CLI required for these tests") } } + +type augmentedT interface { + TestingT + Cleanup(func()) + Deadline() (time.Time, bool) + TempDir() string +} + +func withAugment(t TestingT, f func(t augmentedT)) { + c := withAugmentedT{TestingT: t} + defer c.all() + f(&c) +} + +type withAugmentedT struct { + TestingT + tasks []func() +} + +// TempDir returns a temporary directory for the test to use. +// The directory is automatically removed when the test and +// all its subtests complete. +// Each subsequent call to t.TempDir returns a unique directory; +// if the directory creation fails, TempDir terminates the test by calling Fatal. +func (t *withAugmentedT) TempDir() string { + name := t.Name() + name = strings.ReplaceAll(name, "#", "") + name = strings.ReplaceAll(name, string(os.PathSeparator), "") + dir, err := os.MkdirTemp("", name) + require.NoError(t, err) + return dir +} + +func (t *withAugmentedT) Cleanup(f func()) { + t.tasks = append(t.tasks, f) +} + +func (t *withAugmentedT) Deadline() (time.Time, bool) { + return time.Time{}, false +} + +func (t *withAugmentedT) all() { + for i := len(t.tasks) - 1; i >= 0; i-- { + v := t.tasks[i] + if v != nil { + v() + } + } +} diff --git a/pf/tests/provider_configure_test.go b/pf/tests/provider_configure_test.go index b8b9305e1..c8167c7dc 100644 --- a/pf/tests/provider_configure_test.go +++ b/pf/tests/provider_configure_test.go @@ -15,6 +15,7 @@ package tfbridgetests import ( + "context" "testing" "github.com/hashicorp/terraform-plugin-framework/diag" @@ -22,14 +23,19 @@ import ( "github.com/pulumi/providertest/replay" "github.com/pulumi/pulumi-terraform-bridge/pf/tests/internal/cross-tests" "github.com/pulumi/pulumi-terraform-bridge/pf/tests/internal/testprovider" + propProviderSchema "github.com/pulumi/pulumi-terraform-bridge/pf/tests/util/property/pf/schema/provider" + propProviderValue "github.com/pulumi/pulumi-terraform-bridge/pf/tests/util/property/pf/value/provider" tfpf "github.com/pulumi/pulumi-terraform-bridge/pf/tfbridge" "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfbridge" "github.com/pulumi/pulumi/sdk/v3/go/common/resource" "github.com/stretchr/testify/require" "github.com/zclconf/go-cty/cty" + "pgregory.net/rapid" ) func TestConfigure(t *testing.T) { + t.Parallel() + t.Run("string", crosstests.MakeConfigure( schema.Schema{Attributes: map[string]schema.Attribute{ "k": schema.StringAttribute{Optional: true}, @@ -69,6 +75,19 @@ func TestConfigureInvalidTypes(t *testing.T) { )) } +func TestConfigureProperties(t *testing.T) { + t.Parallel() + + rapid.Check(t, func(t *rapid.T) { + ctx := context.Background() + schema := propProviderSchema.Schema(ctx).Draw(t, "schema") + value := propProviderValue.WithValue(schema, + propProviderValue.NoEmptyObjects(), + ).Draw(t, "value") + crosstests.Configure(t, schema, value.Tf.AsValueMap(), value.Pu) + }) +} + // Test interaction of Configure and Create. // // The resource TestConfigRes will read stringConfigProp information the provider receives via Configure. diff --git a/pf/tests/util/property/pf/schema/provider/attr.go b/pf/tests/util/property/pf/schema/provider/attr.go new file mode 100644 index 000000000..69bdc3464 --- /dev/null +++ b/pf/tests/util/property/pf/schema/provider/attr.go @@ -0,0 +1,92 @@ +// Copyright 2016-2024, Pulumi Corporation. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package provider + +import ( + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/types" + "pgregory.net/rapid" +) + +func attrType(depth int) *rapid.Generator[attr.Type] { + if depth <= 1 { + return attrPrimitiveType() + } + + types := []*rapid.Generator[attr.Type]{ + attrPrimitiveType(), + rapid.Map(attrListType(depth-1), castToAttrType), + rapid.Map(attrMapType(depth-1), castToAttrType), + rapid.Map(attrSetType(depth-1), castToAttrType), + rapid.Map(attrObjectType(depth-1), castToAttrType), + } + + if false { // TODO: Enable testing tuples + types = append(types, rapid.Map(attrTupleType(depth-1), castToAttrType)) + } + + return rapid.OneOf(types...) +} + +func attrPrimitiveType() *rapid.Generator[attr.Type] { + return rapid.OneOf( + rapid.Map(rapid.Just(types.BoolType), castToAttrType), + rapid.Map(rapid.Just(types.NumberType), castToAttrType), + rapid.Map(rapid.Just(types.Float64Type), castToAttrType), + rapid.Map(rapid.Just(types.StringType), castToAttrType), + rapid.Map(rapid.Just(types.Int64Type), castToAttrType), + ) +} + +func attrListType(depth int) *rapid.Generator[types.ListType] { + return rapid.Map(attrType(depth-1), func(t attr.Type) types.ListType { + return types.ListType{ElemType: t} + }) +} + +func attrMapType(depth int) *rapid.Generator[types.MapType] { + return rapid.Map(attrType(depth-1), func(t attr.Type) types.MapType { + return types.MapType{ElemType: t} + }) +} + +func attrSetType(depth int) *rapid.Generator[types.SetType] { + return rapid.Map(attrType(depth-1), func(t attr.Type) types.SetType { + return types.SetType{ElemType: t} + }) +} + +func attrTupleType(depth int) *rapid.Generator[types.TupleType] { + return rapid.Custom(func(t *rapid.T) types.TupleType { + return types.TupleType{ + // 1 is assumed to be a minimum. + // 4 is chosen as an arbitrary maximum. + ElemTypes: rapid.SliceOfN(attrType(depth-1), 1, 4).Draw(t, "ElemTypes"), + } + }) +} + +func attrObjectType(depth int) *rapid.Generator[types.ObjectType] { + return rapid.Custom(func(t *rapid.T) types.ObjectType { + return types.ObjectType{ + AttrTypes: rapid.MapOf( + rapid.StringMatching(tfIdentifierRegexp), + attrType(depth-1), + ).Draw(t, "AttrTypes"), + } + }) +} + +func castToAttrType[T attr.Type](v T) attr.Type { return v } diff --git a/pf/tests/util/property/pf/schema/provider/schema.go b/pf/tests/util/property/pf/schema/provider/schema.go new file mode 100644 index 000000000..ba0d4c882 --- /dev/null +++ b/pf/tests/util/property/pf/schema/provider/schema.go @@ -0,0 +1,446 @@ +// Copyright 2016-2024, Pulumi Corporation. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package pf provides [rapid] based property testing to Hashicorp's Plugin Framework for +// TF SDKs. +package provider + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/provider/schema" + "pgregory.net/rapid" +) + +const ( + tfIdentifierRegexp = "[a-z_][a-z_0-9]*" + defaultDepth = 3 + maxObjectSize = 4 +) + +func Schema(ctx context.Context) *rapid.Generator[schema.Schema] { + return rapid.Custom(func(t *rapid.T) schema.Schema { + // We need to generate a set of attributes and blocks, all with non-overlapping names. + names := attrAndBlockNames().Draw(t, "names") + s := schema.Schema{ + Attributes: attributes(names.attr, defaultDepth).Draw(t, "attributes"), + Blocks: blocks(names.block, defaultDepth).Draw(t, "blocks"), + Description: rapid.String().Draw(t, "description"), + MarkdownDescription: rapid.String().Draw(t, "markdown description"), + DeprecationMessage: rapid.String().Draw(t, "deprecation message"), + } + + if diags := s.ValidateImplementation(ctx); diags.HasError() { + t.Fatalf("Invalid schema generated: %#v", diags) + } + + return s + }) +} + +func attrAndBlockNames() *rapid.Generator[struct{ attr, block []string }] { + return rapid.Custom(func(t *rapid.T) struct{ attr, block []string } { + attrAndBlockNames := rapid.SliceOfNDistinct( + rapid.StringMatching(tfIdentifierRegexp), + -1, + maxObjectSize, + rapid.ID, + ).Draw(t, "names") + split := rapid.IntRange(0, len(attrAndBlockNames)).Draw(t, "attr block split") + attrNames, blockNames := attrAndBlockNames[:split], attrAndBlockNames[split:] + return struct{ attr, block []string }{attrNames, blockNames} + }) +} + +func attributes(names []string, depth int) *rapid.Generator[map[string]schema.Attribute] { + if len(names) == 0 { + return rapid.Just[map[string]schema.Attribute](nil) + } + return rapid.Custom(func(t *rapid.T) map[string]schema.Attribute { + m := make(map[string]schema.Attribute, len(names)) + for _, v := range names { + m[v] = attribute(depth).Draw(t, v) + } + return m + }) +} + +func attribute(depth int) *rapid.Generator[schema.Attribute] { + return rapid.Custom(func(t *rapid.T) schema.Attribute { + if depth <= 1 { + return rapid.OneOf(primitiveAttributes()...).Draw(t, "attr") + } + return rapid.OneOf( + append(primitiveAttributes(), complexAttributes(depth)...)..., + ).Draw(t, "attr") + }) +} + +func primitiveAttributes() []*rapid.Generator[schema.Attribute] { + return []*rapid.Generator[schema.Attribute]{ + rapid.Map(stringAttr(), castToAttribute), + rapid.Map(boolAttr(), castToAttribute), + rapid.Map(float64Attr(), castToAttribute), + rapid.Map(int64Attr(), castToAttribute), + rapid.Map(numberAttr(), castToAttribute), + } +} + +func castToAttribute[T schema.Attribute](v T) schema.Attribute { return v } + +func boolAttr() *rapid.Generator[schema.BoolAttribute] { + return rapid.Custom(func(t *rapid.T) schema.BoolAttribute { + + required := rapid.Bool().Draw(t, "required") + optional := !required + return schema.BoolAttribute{ + Required: required, + Optional: optional, + Sensitive: rapid.Bool().Draw(t, "sensitive"), + Description: rapid.String().Draw(t, "description"), + MarkdownDescription: rapid.String().Draw(t, "markdown description"), + DeprecationMessage: rapid.String().Draw(t, "deprecation message"), + CustomType: nil, // We are omitting custom types for now. + Validators: nil, // We are omitting validators here + } + }) +} + +func float64Attr() *rapid.Generator[schema.Float64Attribute] { + return rapid.Custom(func(t *rapid.T) schema.Float64Attribute { + + required := rapid.Bool().Draw(t, "required") + optional := !required + return schema.Float64Attribute{ + Required: required, + Optional: optional, + Sensitive: rapid.Bool().Draw(t, "sensitive"), + Description: rapid.String().Draw(t, "description"), + MarkdownDescription: rapid.String().Draw(t, "markdown description"), + DeprecationMessage: rapid.String().Draw(t, "deprecation message"), + CustomType: nil, // We are omitting custom types for now. + Validators: nil, // We are omitting validators here + } + }) +} + +func int64Attr() *rapid.Generator[schema.Int64Attribute] { + return rapid.Custom(func(t *rapid.T) schema.Int64Attribute { + + required := rapid.Bool().Draw(t, "required") + optional := !required + return schema.Int64Attribute{ + Required: required, + Optional: optional, + Sensitive: rapid.Bool().Draw(t, "sensitive"), + Description: rapid.String().Draw(t, "description"), + MarkdownDescription: rapid.String().Draw(t, "markdown description"), + DeprecationMessage: rapid.String().Draw(t, "deprecation message"), + CustomType: nil, // We are omitting custom types for now. + Validators: nil, // We are omitting validators here + } + }) +} + +func numberAttr() *rapid.Generator[schema.NumberAttribute] { + return rapid.Custom(func(t *rapid.T) schema.NumberAttribute { + + required := rapid.Bool().Draw(t, "required") + optional := !required + return schema.NumberAttribute{ + Required: required, + Optional: optional, + Sensitive: rapid.Bool().Draw(t, "sensitive"), + Description: rapid.String().Draw(t, "description"), + MarkdownDescription: rapid.String().Draw(t, "markdown description"), + DeprecationMessage: rapid.String().Draw(t, "deprecation message"), + CustomType: nil, // We are omitting custom types for now. + Validators: nil, // We are omitting validators here + } + }) +} + +func stringAttr() *rapid.Generator[schema.StringAttribute] { + return rapid.Custom(func(t *rapid.T) schema.StringAttribute { + + required := rapid.Bool().Draw(t, "required") + optional := !required + return schema.StringAttribute{ + Required: required, + Optional: optional, + Sensitive: rapid.Bool().Draw(t, "sensitive"), + Description: rapid.String().Draw(t, "description"), + MarkdownDescription: rapid.String().Draw(t, "markdown description"), + DeprecationMessage: rapid.String().Draw(t, "deprecation message"), + CustomType: nil, // We are omitting custom types for now. + Validators: nil, // We are omitting validators here + } + }) +} + +func complexAttributes(depth int) []*rapid.Generator[schema.Attribute] { + return []*rapid.Generator[schema.Attribute]{ + rapid.Map(listAttr(depth), castToAttribute), + rapid.Map(listNestedAttr(depth), castToAttribute), + rapid.Map(mapAttr(depth), castToAttribute), + rapid.Map(mapNestedAttr(depth), castToAttribute), + rapid.Map(setAttr(depth), castToAttribute), + rapid.Map(setNestedAttr(depth), castToAttribute), + rapid.Map(objectAttr(depth), castToAttribute), + rapid.Map(singleNestedAttr(depth), castToAttribute), + } +} + +func listAttr(depth int) *rapid.Generator[schema.ListAttribute] { + return rapid.Custom(func(t *rapid.T) schema.ListAttribute { + + required := rapid.Bool().Draw(t, "required") + optional := !required + return schema.ListAttribute{ + ElementType: attrType(depth-1).Draw(t, "ElementType"), + Required: required, + Optional: optional, + Sensitive: rapid.Bool().Draw(t, "sensitive"), + Description: rapid.String().Draw(t, "description"), + MarkdownDescription: rapid.String().Draw(t, "markdown description"), + DeprecationMessage: rapid.String().Draw(t, "deprecation message"), + CustomType: nil, // We are omitting custom types for now. + Validators: nil, // We are omitting validators here + } + }) +} + +func listNestedAttr(depth int) *rapid.Generator[schema.ListNestedAttribute] { + return rapid.Custom(func(t *rapid.T) schema.ListNestedAttribute { + + required := rapid.Bool().Draw(t, "required") + optional := !required + return schema.ListNestedAttribute{ + NestedObject: nestedAttributeObject(depth-1).Draw(t, "NestedObject"), + Required: required, + Optional: optional, + Sensitive: rapid.Bool().Draw(t, "sensitive"), + Description: rapid.String().Draw(t, "description"), + MarkdownDescription: rapid.String().Draw(t, "markdown description"), + DeprecationMessage: rapid.String().Draw(t, "deprecation message"), + CustomType: nil, // We are omitting custom types for now. + Validators: nil, // We are omitting validators here + } + }) +} + +func mapAttr(depth int) *rapid.Generator[schema.MapAttribute] { + return rapid.Custom(func(t *rapid.T) schema.MapAttribute { + + required := rapid.Bool().Draw(t, "required") + optional := !required + return schema.MapAttribute{ + ElementType: attrType(depth-1).Draw(t, "ElementType"), + Required: required, + Optional: optional, + Sensitive: rapid.Bool().Draw(t, "sensitive"), + Description: rapid.String().Draw(t, "description"), + MarkdownDescription: rapid.String().Draw(t, "markdown description"), + DeprecationMessage: rapid.String().Draw(t, "deprecation message"), + CustomType: nil, // We are omitting custom types for now. + Validators: nil, // We are omitting validators here + } + }) +} + +func mapNestedAttr(depth int) *rapid.Generator[schema.MapNestedAttribute] { + return rapid.Custom(func(t *rapid.T) schema.MapNestedAttribute { + + required := rapid.Bool().Draw(t, "required") + optional := !required + return schema.MapNestedAttribute{ + NestedObject: nestedAttributeObject(depth-1).Draw(t, "NestedObject"), + Required: required, + Optional: optional, + Sensitive: rapid.Bool().Draw(t, "sensitive"), + Description: rapid.String().Draw(t, "description"), + MarkdownDescription: rapid.String().Draw(t, "markdown description"), + DeprecationMessage: rapid.String().Draw(t, "deprecation message"), + CustomType: nil, // We are omitting custom types for now. + Validators: nil, // We are omitting validators here + } + }) +} + +func setAttr(depth int) *rapid.Generator[schema.SetAttribute] { + return rapid.Custom(func(t *rapid.T) schema.SetAttribute { + + required := rapid.Bool().Draw(t, "required") + optional := !required + return schema.SetAttribute{ + ElementType: attrType(depth-1).Draw(t, "ElementType"), + Required: required, + Optional: optional, + Sensitive: rapid.Bool().Draw(t, "sensitive"), + Description: rapid.String().Draw(t, "description"), + MarkdownDescription: rapid.String().Draw(t, "markdown description"), + DeprecationMessage: rapid.String().Draw(t, "deprecation message"), + CustomType: nil, // We are omitting custom types for now. + Validators: nil, // We are omitting validators here + } + }) +} + +func setNestedAttr(depth int) *rapid.Generator[schema.SetNestedAttribute] { + return rapid.Custom(func(t *rapid.T) schema.SetNestedAttribute { + + required := rapid.Bool().Draw(t, "required") + optional := !required + return schema.SetNestedAttribute{ + NestedObject: nestedAttributeObject(depth-1).Draw(t, "NestedObject"), + Required: required, + Optional: optional, + Sensitive: rapid.Bool().Draw(t, "sensitive"), + Description: rapid.String().Draw(t, "description"), + MarkdownDescription: rapid.String().Draw(t, "markdown description"), + DeprecationMessage: rapid.String().Draw(t, "deprecation message"), + CustomType: nil, // We are omitting custom types for now. + Validators: nil, // We are omitting validators here + } + }) +} + +func objectAttr(depth int) *rapid.Generator[schema.ObjectAttribute] { + return rapid.Custom(func(t *rapid.T) schema.ObjectAttribute { + + required := rapid.Bool().Draw(t, "required") + optional := !required + return schema.ObjectAttribute{ + AttributeTypes: attrObjectType(depth-1).Draw(t, "AttributeTypes").AttrTypes, + Required: required, + Optional: optional, + Sensitive: rapid.Bool().Draw(t, "sensitive"), + Description: rapid.String().Draw(t, "description"), + MarkdownDescription: rapid.String().Draw(t, "markdown description"), + DeprecationMessage: rapid.String().Draw(t, "deprecation message"), + CustomType: nil, // We are omitting custom types for now. + Validators: nil, // We are omitting validators here + } + }) +} + +func singleNestedAttr(depth int) *rapid.Generator[schema.SingleNestedAttribute] { + return rapid.Custom(func(t *rapid.T) schema.SingleNestedAttribute { + + required := rapid.Bool().Draw(t, "required") + optional := !required + return schema.SingleNestedAttribute{ + Attributes: nestedAttributeObject(depth-1).Draw(t, "Attributes").Attributes, + CustomType: nil, // We are omitting custom types for now. + Required: required, + Optional: optional, + Sensitive: rapid.Bool().Draw(t, "sensitive"), + Description: rapid.String().Draw(t, "description"), + MarkdownDescription: rapid.String().Draw(t, "markdown description"), + DeprecationMessage: rapid.String().Draw(t, "deprecation message"), + Validators: nil, // We are omitting validators here + } + }) +} + +func nestedAttributeObject(depth int) *rapid.Generator[schema.NestedAttributeObject] { + return rapid.Custom(func(t *rapid.T) schema.NestedAttributeObject { + + return schema.NestedAttributeObject{ + Attributes: rapid.MapOfN( + rapid.StringMatching(tfIdentifierRegexp), + attribute(depth-1), + -1, maxObjectSize, + ).Draw(t, "Attributes"), + CustomType: nil, // We are omitting custom types for now. + Validators: nil, // We are omitting validators here + } + }) +} + +func blocks(names []string, depth int) *rapid.Generator[map[string]schema.Block] { + if len(names) == 0 || depth < 1 { + return rapid.Just[map[string]schema.Block](nil) + } + return rapid.Custom(func(t *rapid.T) map[string]schema.Block { + m := make(map[string]schema.Block, len(names)) + for _, k := range names { + m[k] = block(depth).Draw(t, k) + } + return m + }) +} + +func castToBlock[T schema.Block](v T) schema.Block { return v } + +func block(depth int) *rapid.Generator[schema.Block] { + return rapid.OneOf( + rapid.Map(listNestedBlock(depth), castToBlock), + rapid.Map(setNestedBlock(depth), castToBlock), + rapid.Map(singleNestedBlock(depth), castToBlock), + ) +} + +func listNestedBlock(depth int) *rapid.Generator[schema.ListNestedBlock] { + return rapid.Custom(func(t *rapid.T) schema.ListNestedBlock { + return schema.ListNestedBlock{ + NestedObject: nestedBlockObject(depth-1).Draw(t, "NestedObject"), + Description: rapid.String().Draw(t, "description"), + MarkdownDescription: rapid.String().Draw(t, "markdown description"), + DeprecationMessage: rapid.String().Draw(t, "deprecation message"), + CustomType: nil, // We are omitting custom types for now. + Validators: nil, // We are omitting validators here + } + }) +} + +func setNestedBlock(depth int) *rapid.Generator[schema.SetNestedBlock] { + return rapid.Custom(func(t *rapid.T) schema.SetNestedBlock { + return schema.SetNestedBlock{ + NestedObject: nestedBlockObject(depth-1).Draw(t, "NestedObject"), + Description: rapid.String().Draw(t, "description"), + MarkdownDescription: rapid.String().Draw(t, "markdown description"), + DeprecationMessage: rapid.String().Draw(t, "deprecation message"), + CustomType: nil, // We are omitting custom types for now. + Validators: nil, // We are omitting validators here + } + }) +} + +func singleNestedBlock(depth int) *rapid.Generator[schema.SingleNestedBlock] { + return rapid.Custom(func(t *rapid.T) schema.SingleNestedBlock { + names := attrAndBlockNames().Draw(t, "names") + return schema.SingleNestedBlock{ + Attributes: attributes(names.attr, depth-1).Draw(t, "Attributes"), + Blocks: blocks(names.block, depth-1).Draw(t, "Blocks"), + Description: rapid.String().Draw(t, "description"), + MarkdownDescription: rapid.String().Draw(t, "markdown description"), + DeprecationMessage: rapid.String().Draw(t, "deprecation message"), + CustomType: nil, // We are omitting custom types for now. + Validators: nil, // We are omitting validators here + } + }) +} + +func nestedBlockObject(depth int) *rapid.Generator[schema.NestedBlockObject] { + return rapid.Custom(func(t *rapid.T) schema.NestedBlockObject { + names := attrAndBlockNames().Draw(t, "names") + return schema.NestedBlockObject{ + Attributes: attributes(names.attr, depth-1).Draw(t, "Attributes"), + Blocks: blocks(names.block, depth-1).Draw(t, "Blocks"), + CustomType: nil, // We are omitting custom types for now. + Validators: nil, // We are omitting validators here + } + }) +} diff --git a/pf/tests/util/property/pf/value/provider/primitive.go b/pf/tests/util/property/pf/value/provider/primitive.go new file mode 100644 index 000000000..c4c3a15cb --- /dev/null +++ b/pf/tests/util/property/pf/value/provider/primitive.go @@ -0,0 +1,51 @@ +// Copyright 2016-2024, Pulumi Corporation. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package provider + +import ( + "github.com/zclconf/go-cty/cty" + "pgregory.net/rapid" + + "github.com/pulumi/pulumi/sdk/v3/go/common/resource" +) + +func boolVal(t *rapid.T) value { + f := rapid.Bool().Draw(t, "v") + return value{ + hasValue: true, + Tf: cty.BoolVal(f), + Pu: resource.NewProperty(f), + } +} + +func stringVal(t *rapid.T) value { + s := rapid.String().Draw(t, "v") + + return value{ + hasValue: true, + Tf: cty.StringVal(s), + Pu: resource.NewProperty(s), + } + +} + +func numberVal(t *rapid.T) value { + f := rapid.Float64().Draw(t, "v") + return value{ + hasValue: true, + Tf: cty.NumberFloatVal(f), + Pu: resource.NewProperty(f), + } +} diff --git a/pf/tests/util/property/pf/value/provider/util.go b/pf/tests/util/property/pf/value/provider/util.go new file mode 100644 index 000000000..7c20f98ac --- /dev/null +++ b/pf/tests/util/property/pf/value/provider/util.go @@ -0,0 +1,160 @@ +// Copyright 2016-2024, Pulumi Corporation. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package provider + +import ( + "fmt" + + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/zclconf/go-cty/cty" + + shim "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfshim" + "github.com/pulumi/pulumi/sdk/v3/go/common/resource" +) + +func shimType(t tftypes.Type) shim.ValueType { + switch { + case t.Is(tftypes.Set{}): + return shim.TypeSet + case t.Is(tftypes.Map{}): + return shim.TypeMap + case t.Is(tftypes.List{}): + return shim.TypeList + default: + // This is only used when interacting with + // [tfbridge.TerraformToPulumiNameV2], so other scalar types don't matter + // here. + return shim.TypeBool + } +} + +func makeConvertMap(elem cty.Type) func(map[string]value) value { + return func(m map[string]value) value { return convertMap(m, elem) } +} + +func convertMap(m map[string]value, elem cty.Type) value { + tfMap, puMap := make(map[string]cty.Value, len(m)), make(resource.PropertyMap, len(m)) + for k, v := range m { + tfMap[k] = v.Tf + if v.hasValue { + puMap[resource.PropertyKey(k)] = v.Pu + } + } + + ctyMap := cty.MapValEmpty(elem) + if len(tfMap) > 0 { + ctyMap = cty.MapVal(tfMap) + } + + return value{ + hasValue: true, + Tf: ctyMap, + Pu: resource.NewProperty(puMap), + } +} + +func convertObject(m map[string]value, names map[string]resource.PropertyKey) value { + tfMap, puMap := make(map[string]cty.Value, len(m)), make(resource.PropertyMap, len(m)) + for k, v := range m { + tfMap[k] = v.Tf + if v.hasValue { + puMap[names[k]] = v.Pu + } + } + return value{ + hasValue: true, + Tf: cty.ObjectVal(tfMap), + Pu: resource.NewProperty(puMap), + } +} + +func makeConvertSet(elem cty.Type) func([]value) value { + return func(a []value) value { return convertSet(a, elem) } +} + +func convertSet(a []value, elem cty.Type) value { + tfArr, puArr := make([]cty.Value, len(a)), make([]resource.PropertyValue, len(a)) + for i, v := range a { + tfArr[i] = v.Tf + puArr[i] = v.Pu + } + + ctyList := cty.SetValEmpty(elem) + if len(tfArr) > 0 { + ctyList = cty.SetVal(tfArr) + } + + return value{ + hasValue: true, + Tf: ctyList, + Pu: resource.NewProperty(puArr), + } +} + +func makeConvertList(elem cty.Type) func([]value) value { + return func(a []value) value { return convertList(a, elem) } +} + +func convertList(a []value, elem cty.Type) value { + tfArr, puArr := make([]cty.Value, len(a)), make([]resource.PropertyValue, len(a)) + for i, v := range a { + tfArr[i] = v.Tf + puArr[i] = v.Pu + } + + ctyList := cty.ListValEmpty(elem) + if len(tfArr) > 0 { + ctyList = cty.ListVal(tfArr) + } + + return value{ + hasValue: true, + Tf: ctyList, + Pu: resource.NewProperty(puArr), + } +} + +func ctyType(typ tftypes.Type) cty.Type { + switch { + case typ.Is(tftypes.Bool): + return cty.Bool + case typ.Is(tftypes.Number): + return cty.Number + case typ.Is(tftypes.String): + return cty.String + case typ.Is(tftypes.Map{}): + return cty.Map(ctyType(typ.(tftypes.Map).ElementType)) + case typ.Is(tftypes.List{}): + return cty.List(ctyType(typ.(tftypes.List).ElementType)) + case typ.Is(tftypes.Set{}): + return cty.Set(ctyType(typ.(tftypes.Set).ElementType)) + case typ.Is(tftypes.Object{}): + o := typ.(tftypes.Object) + attrs := make(map[string]cty.Type, len(o.AttributeTypes)) + optionals := make([]string, 0, len(o.OptionalAttributes)) + + for k, v := range o.AttributeTypes { + attrs[k] = ctyType(v) + } + for k := range o.OptionalAttributes { + optionals = append(optionals, k) + } + + return cty.ObjectWithOptionalAttrs(attrs, optionals) + default: + panic(fmt.Sprintf("Unknown tftypes.Type: %v", typ)) + } + +} diff --git a/pf/tests/util/property/pf/value/provider/value.go b/pf/tests/util/property/pf/value/provider/value.go new file mode 100644 index 000000000..a846bee36 --- /dev/null +++ b/pf/tests/util/property/pf/value/provider/value.go @@ -0,0 +1,402 @@ +// Copyright 2016-2024, Pulumi Corporation. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package provider + +import ( + "context" + "fmt" + "hash/maphash" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/provider/schema" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/zclconf/go-cty/cty" + "pgregory.net/rapid" + + "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfbridge" + shimschema "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfshim/schema" + + "github.com/pulumi/pulumi/sdk/v3/go/common/resource" + "github.com/pulumi/pulumi/sdk/v3/go/common/util/contract" +) + +const maxIterableSize = 4 + +// Value represents a single conceptual value, represented in both a [cty.Value] and in a +// [resource.PropertyValue]. +type Value struct { + Tf cty.Value + Pu resource.PropertyMap +} + +type value struct { + Tf cty.Value + Pu resource.PropertyValue + hasValue bool +} + +type generator struct { + // isInput means that the generator is generating input values, not output values. + isInput bool + + // Forbid generating empty objects unless necessary. + // + // TODO: Obey noEmptyObjects in value generation. + noEmptyObjects bool +} + +func WithValue(s schema.Schema, options ...Option) *rapid.Generator[Value] { + ctx := context.Background() + g := generator{isInput: true} + + for _, o := range options { + o(&g) + } + + return rapid.Map( + g.nestedBlockObject(ctx, schema.NestedBlockObject{ + Attributes: s.Attributes, + Blocks: s.Blocks, + }), + func(v value) Value { + var pu resource.PropertyMap + if !v.Pu.IsNull() { + pu = v.Pu.ObjectValue() + } + return Value{Tf: v.Tf, Pu: pu} + }, + ) +} + +type Option func(*generator) + +func NoEmptyObjects() Option { return func(o *generator) { o.noEmptyObjects = true } } + +func (g generator) withAttr(ctx context.Context, attr schema.Attribute) *rapid.Generator[value] { + contract.Assertf(g.isInput, "only input values are implemented") + + // computed & !optional => the provider will set the value + if attr.IsComputed() && !attr.IsOptional() { + return rapid.Just(value{hasValue: false}) + } + + makeOptional := func(v *rapid.Generator[value]) *rapid.Generator[value] { + if attr.IsRequired() { + return v + } + + return rapid.Custom(func(t *rapid.T) value { + // optional => we may set the value + v := v.Draw(t, "v") + if attr.IsOptional() && rapid.Bool().Draw(t, "drop optional") { + return value{hasValue: false, Tf: cty.NullVal(v.Tf.Type())} + } + + return v + }) + } + + getValue := func() *rapid.Generator[value] { + switch attr := attr.(type) { + + // Primitive attributes + + case schema.StringAttribute: + return rapid.Custom(stringVal) + case schema.NumberAttribute: + return rapid.Custom(numberVal) + case schema.BoolAttribute: + return rapid.Custom(boolVal) + + case schema.Int32Attribute: + return rapid.Custom(func(t *rapid.T) value { + i := rapid.Int32().Draw(t, "v") + return value{ + hasValue: true, + Tf: cty.NumberIntVal(int64(i)), + Pu: resource.NewProperty(float64(i)), + } + }) + case schema.Int64Attribute: + return rapid.Custom(func(t *rapid.T) value { + i := rapid.Int64().Draw(t, "v") + return value{ + hasValue: true, + Tf: cty.NumberIntVal(i), + Pu: resource.NewProperty(float64(i)), + } + }) + case schema.Float32Attribute: + return rapid.Custom(func(t *rapid.T) value { + f := rapid.Float32().Draw(t, "v") + return value{ + hasValue: true, + Tf: cty.NumberFloatVal(float64(f)), + Pu: resource.NewProperty(float64(f)), + } + }) + case schema.Float64Attribute: + return rapid.Custom(func(t *rapid.T) value { + f := rapid.Float64().Draw(t, "v") + return value{ + hasValue: true, + Tf: cty.NumberFloatVal(f), + Pu: resource.NewProperty(f), + } + }) + + // Complex attributes + + case schema.MapAttribute: + elemType := attr.ElementType.TerraformType(ctx) + return rapid.Map( + rapid.MapOf(rapid.String(), baseAttr(elemType)), + makeConvertMap(ctyType(elemType)), + ) + + case schema.ListAttribute: + elemType := attr.ElementType.TerraformType(ctx) + return rapid.Map( + rapid.SliceOfN(baseAttr(elemType), 0, maxIterableSize), + makeConvertList(ctyType(elemType)), + ) + case schema.SetAttribute: + elemType := attr.ElementType.TerraformType(ctx) + return rapid.Map( + rapid.SliceOfNDistinct(baseAttr(elemType), 0, maxIterableSize, valueID), + makeConvertSet(ctyType(elemType)), + ) + + case schema.ObjectAttribute: + m := make(map[string]value, len(attr.AttributeTypes)) + if len(attr.AttributeTypes) == 0 { + // TODO: Explore sending empty maps here + return rapid.Just(value{Tf: cty.EmptyObjectVal}) + } + names := translateAttrNames(ctx, attr.AttributeTypes) + return rapid.Custom(func(t *rapid.T) value { + for k, a := range attr.AttributeTypes { + m[k] = baseAttr(a.TerraformType(ctx)).Draw(t, k) + } + return convertObject(m, names) + }) + + // Nested attributes + case schema.SingleNestedAttribute: + m := make(map[string]value, len(attr.Attributes)) + if len(attr.Attributes) == 0 { + return rapid.Just(value{Tf: cty.EmptyObjectVal}) + } + names := translateNames(ctx, attr.Attributes, nil) + return rapid.Custom(func(t *rapid.T) value { + for k, a := range attr.Attributes { + m[k] = g.withAttr(ctx, a).Draw(t, k) + } + return convertObject(m, names) + }) + case schema.MapNestedAttribute: + return rapid.Map( + rapid.MapOfN(rapid.String(), g.nestedObject(ctx, attr.NestedObject), 0, maxIterableSize), + makeConvertMap(ctyType(attr.NestedObject.Type().TerraformType(ctx))), + ) + case schema.ListNestedAttribute: + return rapid.Map( + rapid.SliceOfN(g.nestedObject(ctx, attr.NestedObject), 0, maxIterableSize), + makeConvertList(ctyType(attr.NestedObject.Type().TerraformType(ctx))), + ) + case schema.SetNestedAttribute: + return rapid.Map( + rapid.SliceOfNDistinct(g.nestedObject(ctx, attr.NestedObject), 0, maxIterableSize, valueID), + makeConvertSet(ctyType(attr.NestedObject.Type().TerraformType(ctx))), + ) + default: + panic(fmt.Sprintf("Unknown schema.Attribute type %T", attr)) + } + } + + return makeOptional(getValue()) +} + +func baseAttr(typ tftypes.Type) *rapid.Generator[value] { + switch { + case typ.Is(tftypes.Object{}): + o := typ.(tftypes.Object) + if len(o.AttributeTypes) == 0 { + return rapid.Just(value{Tf: cty.EmptyObjectVal}) + } + + names := translareTftypeNames(o.AttributeTypes) + + return rapid.Custom(func(t *rapid.T) value { + m := make(map[string]value, len(o.AttributeTypes)) + for k, a := range o.AttributeTypes { + ctyType := ctyType(a) + if _, isOptional := o.OptionalAttributes[k]; isOptional && + rapid.Bool().Draw(t, "ignore") { + m[k] = value{Tf: cty.NullVal(ctyType)} + } else { + t.Log(a) + m[k] = baseAttr(a).Draw(t, k) + } + } + return convertObject(m, names) + }) + case typ.Is(tftypes.Bool): + return rapid.Custom(boolVal) + case typ.Is(tftypes.Number): + return rapid.Custom(numberVal) + case typ.Is(tftypes.String): + return rapid.Custom(stringVal) + case typ.Is(tftypes.Map{}): + elemType := typ.(tftypes.Map).ElementType + return rapid.Map( + rapid.MapOfN(rapid.String(), baseAttr(elemType), 0, maxIterableSize), + makeConvertMap(ctyType(elemType)), + ) + case typ.Is(tftypes.List{}): + elemType := typ.(tftypes.List).ElementType + return rapid.Map( + rapid.SliceOfN(baseAttr(elemType), 0, maxIterableSize), + makeConvertList(ctyType(elemType)), + ) + case typ.Is(tftypes.Set{}): + elemType := typ.(tftypes.Set).ElementType + return rapid.Map( + rapid.SliceOfNDistinct(baseAttr(elemType), 0, maxIterableSize, valueID), + makeConvertSet(ctyType(elemType)), + ) + default: + panic(fmt.Sprintf("Unknown tftypes.Type: %v", typ)) + } +} + +func (g generator) nestedObject(ctx context.Context, obj schema.NestedAttributeObject) *rapid.Generator[value] { + if len(obj.Attributes) == 0 { + m := make(map[string]value, len(obj.Attributes)) + return rapid.Just(convertObject(m, nil)) + } + + names := translateNames(ctx, obj.Attributes, nil) + return rapid.Custom(func(t *rapid.T) value { + m := make(map[string]value, len(obj.Attributes)) + for k, a := range obj.Attributes { + m[k] = g.withAttr(ctx, a).Draw(t, k) + } + return convertObject(m, names) + }) +} + +func (g generator) withBlock(ctx context.Context, block schema.Block) *rapid.Generator[value] { + contract.Assertf(g.isInput, "only input values are implemented") + + switch block := block.(type) { + case schema.ListNestedBlock: + return rapid.Map( + rapid.SliceOfN(g.nestedBlockObject(ctx, block.NestedObject), -1, maxIterableSize), + makeConvertList(ctyType(block.NestedObject.Type().TerraformType(ctx))), + ) + case schema.SetNestedBlock: + return rapid.Map( + rapid.SliceOfNDistinct(g.nestedBlockObject(ctx, block.NestedObject), -1, maxIterableSize, valueID), + makeConvertSet(ctyType(block.NestedObject.Type().TerraformType(ctx))), + ) + case schema.SingleNestedBlock: + return g.nestedBlockObject(ctx, schema.NestedBlockObject{ + Attributes: block.Attributes, + Blocks: block.Blocks, + }) + default: + panic(fmt.Sprintf("Unknown schema.Block type: %T", block)) + } +} + +func (g generator) nestedBlockObject(ctx context.Context, obj schema.NestedBlockObject) *rapid.Generator[value] { + + // All rapid.Custom generators *must* consume entropy, so we special case empty + // schemas. + if len(obj.Attributes)+len(obj.Blocks) == 0 { + return rapid.Just(value{ + Tf: cty.EmptyObjectVal, + Pu: resource.NewNullProperty(), + }) + } + + names := translateNames(ctx, obj.Attributes, obj.Blocks) + + return rapid.Custom(func(t *rapid.T) value { + m := map[string]value{} + for k, s := range obj.Attributes { + m[k] = g.withAttr(ctx, s).Draw(t, k) + } + + for k, b := range obj.Blocks { + m[k] = g.withBlock(ctx, b).Draw(t, k) + } + + return convertObject(m, names) + }) +} + +func valueID(v value) uint64 { + h := maphash.Hash{} + _, err := h.WriteString(v.Tf.GoString()) + contract.AssertNoErrorf(err, "maphash.Hash.WriteString does not error") + _, err = h.WriteString(v.Pu.String()) + contract.AssertNoErrorf(err, "maphash.Hash.WriteString does not error") + return h.Sum64() +} + +func translareTftypeNames(attrs map[string]tftypes.Type) map[string]resource.PropertyKey { + sch := make(shimschema.SchemaMap, len(attrs)) + for k, v := range attrs { + sch[k] = (&shimschema.Schema{ + Type: shimType(v), + }).Shim() + } + + names := make(map[string]resource.PropertyKey, len(sch)) + for k := range sch { + names[k] = resource.PropertyKey(tfbridge.TerraformToPulumiNameV2(k, sch, nil)) + } + return names +} + +func translateAttrNames(ctx context.Context, attrs map[string]attr.Type) map[string]resource.PropertyKey { + m := make(map[string]tftypes.Type, len(attrs)) + for k, v := range attrs { + m[k] = v.TerraformType(ctx) + } + return translareTftypeNames(m) +} + +func translateNames(ctx context.Context, attrs map[string]schema.Attribute, blocks map[string]schema.Block) map[string]resource.PropertyKey { + sch := make(shimschema.SchemaMap, len(attrs)+len(blocks)) + for k, v := range attrs { + sch[k] = (&shimschema.Schema{ + Type: shimType(v.GetType().TerraformType(ctx)), + }).Shim() + } + for k, v := range blocks { + sch[k] = (&shimschema.Schema{ + Type: shimType(v.Type().TerraformType(ctx)), + }).Shim() + } + + names := make(map[string]resource.PropertyKey, len(sch)) + for k := range sch { + names[k] = resource.PropertyKey(tfbridge.TerraformToPulumiNameV2(k, sch, nil)) + } + return names +}