From fe7e5309288d644a8c5dcf8e0ee4f1e2c1f30379 Mon Sep 17 00:00:00 2001 From: Anton Tayanovskyy Date: Wed, 27 Mar 2024 13:39:05 -0400 Subject: [PATCH 01/45] Start writing a Rapid generator for type-value pairs --- pkg/tests/cross-tests/rapid_tv_gen.go | 102 ++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 pkg/tests/cross-tests/rapid_tv_gen.go diff --git a/pkg/tests/cross-tests/rapid_tv_gen.go b/pkg/tests/cross-tests/rapid_tv_gen.go new file mode 100644 index 000000000..b3347fa83 --- /dev/null +++ b/pkg/tests/cross-tests/rapid_tv_gen.go @@ -0,0 +1,102 @@ +package crosstests + +import ( + "fmt" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "pgregory.net/rapid" +) + +// Combines a TF value representing resource inputs with its schema. The value has to conform to the schema. The name +// "tv" stands for a typed value. +type tv struct { + schema schema.Schema + typ tftypes.Type + value tftypes.Value +} + +type schemaT func(schema.Schema) schema.Schema + +type tvGen struct{} + +func (tvg *tvGen) GenTV(maxDepth int) *rapid.Generator[tv] { + opts := []*rapid.Generator[tv]{ + tvg.GenString(), + } + if maxDepth > 1 { + opts = append(opts, tvg.GenObject(maxDepth-1)) + } + return rapid.OneOf(opts...) +} + +func (tvg *tvGen) GenObject(maxDepth int) *rapid.Generator[tv] { + return rapid.Custom[tv](func(t *rapid.T) tv { + fieldSchemas := map[string]*schema.Schema{} + objectFields := map[string]tftypes.Value{} + fieldTypes := map[string]tftypes.Type{} + nFields := rapid.IntRange(0, 3).Draw(t, "nFields") + for i := 0; i < nFields; i++ { + fieldName := fmt.Sprintf("f%d", i) + fieldTV := tvg.GenTV(maxDepth-1).Draw(t, fieldName) + fieldSchemas[fieldName] = &fieldTV.schema + objectFields[fieldName] = fieldTV.value + fieldTypes[fieldName] = fieldTV.typ + } + objSchema := schema.Schema{ + Type: schema.TypeMap, + Elem: &schema.Resource{ + Schema: fieldSchemas, + }, + } + st := tvg.GenSchemaTransform().Draw(t, "schemaTransform") + objType := tftypes.Object{AttributeTypes: fieldTypes} + objValue := tftypes.NewValue(objType, objectFields) + return tv{st(objSchema), objType, objValue} + }) +} + +func (tvg *tvGen) GenString() *rapid.Generator[tv] { + s := schema.Schema{ + Type: schema.TypeString, + } + values := []tftypes.Value{ + tftypes.NewValue(tftypes.String, nil), + tftypes.NewValue(tftypes.String, tftypes.UnknownValue), + tftypes.NewValue(tftypes.String, ""), + tftypes.NewValue(tftypes.String, "text"), + } + return rapid.Custom[tv](func(t *rapid.T) tv { + st := tvg.GenSchemaTransform().Draw(t, "schemaTransform") + value := rapid.SampledFrom(values).Draw(t, "sampleValue") + return tv{st(s), tftypes.String, value} + }) +} + +func (*tvGen) GenSchemaTransform() *rapid.Generator[schemaT] { + return rapid.Custom[schemaT](func(t *rapid.T) schemaT { + k := rapid.SampledFrom([]string{"o", "r", "c", "co"}).Draw(t, "optionalKind") + secret := rapid.Bool().Draw(t, "secret") + forceNew := rapid.Bool().Draw(t, "forceNew") + + return func(s schema.Schema) schema.Schema { + switch k { + case "o": + s.Optional = true + case "r": + s.Required = true + case "c": + s.Computed = true + case "co": + s.Computed = true + s.Optional = true + } + if forceNew { + s.ForceNew = true + } + if secret { + s.Sensitive = true + } + return s + } + }) +} From af033dabeb2b7166a962023d9cb8afdabb7eb381 Mon Sep 17 00:00:00 2001 From: Anton Tayanovskyy Date: Wed, 27 Mar 2024 14:49:09 -0400 Subject: [PATCH 02/45] Toward rapid tests over diff --- pkg/tests/cross-tests/cross_test.go | 27 ++++++++++----- pkg/tests/cross-tests/rapid_test.go | 47 +++++++++++++++++++++++++++ pkg/tests/cross-tests/rapid_tv_gen.go | 25 +++++++++----- 3 files changed, 81 insertions(+), 18 deletions(-) create mode 100644 pkg/tests/cross-tests/rapid_test.go diff --git a/pkg/tests/cross-tests/cross_test.go b/pkg/tests/cross-tests/cross_test.go index 55f2ceea5..184b861ac 100644 --- a/pkg/tests/cross-tests/cross_test.go +++ b/pkg/tests/cross-tests/cross_test.go @@ -69,7 +69,15 @@ const ( providerVer = "0.0.1" ) -func runDiffCheck(t *testing.T, tc diffTestCase) { +type T interface { + Logf(string, ...any) + TempDir() string + require.TestingT + assert.TestingT + pulumitest.T +} + +func runDiffCheck(t T, tc diffTestCase) { // ctx := context.Background() tfwd := t.TempDir() @@ -119,7 +127,7 @@ func runDiffCheck(t *testing.T, tc diffTestCase) { verifyBasicDiffAgreement(t, p2, x.Summary) } -func tfWriteJSON(t *testing.T, cwd string, rconfig any) { +func tfWriteJSON(t T, cwd string, rconfig any) { config := map[string]any{ "resource": map[string]any{ rtype: map[string]any{ @@ -142,7 +150,7 @@ func (*tfPlan) OpType() *apitype.OpType { return nil } -func runTFPlan(t *testing.T, cwd string, reattachConfig *plugin.ReattachConfig) tfPlan { +func runTFPlan(t T, cwd string, reattachConfig *plugin.ReattachConfig) tfPlan { planFile := filepath.Join(cwd, "test.tfplan") env := []string{formatReattachEnvVar(providerName, reattachConfig)} execCmd(t, cwd, env, "terraform", "plan", "-refresh=false", "-out", planFile) @@ -154,7 +162,7 @@ func runTFPlan(t *testing.T, cwd string, reattachConfig *plugin.ReattachConfig) return tp } -func runTFApply(t *testing.T, cwd string, reattachConfig *plugin.ReattachConfig, p tfPlan) { +func runTFApply(t T, cwd string, reattachConfig *plugin.ReattachConfig, p tfPlan) { execCmd(t, cwd, []string{formatReattachEnvVar(providerName, reattachConfig)}, "terraform", "apply", "-auto-approve", "-refresh=false", p.PlanFile) } @@ -167,7 +175,7 @@ func toTFProvider(tc diffTestCase) *schema.Provider { } } -func startTFProvider(t *testing.T, tc diffTestCase) *plugin.ReattachConfig { +func startTFProvider(t T, tc diffTestCase) *plugin.ReattachConfig { tc.Resource.CustomizeDiff = func( ctx context.Context, rd *schema.ResourceDiff, i interface{}, ) error { @@ -215,7 +223,8 @@ func startTFProvider(t *testing.T, tc diffTestCase) *plugin.ReattachConfig { serveOpts := []tf5server.ServeOpt{ tf5server.WithDebug(ctx, reattachConfigCh, closeCh), - tf5server.WithLoggingSink(t), + // TODO - can this not assume testing.T + // tf5server.WithLoggingSink(t), } go func() { @@ -642,7 +651,7 @@ func startPulumiProvider( return &handle, nil } -func pulumiWriteYaml(t *testing.T, tc diffTestCase, puwd string, tfConfig any) { +func pulumiWriteYaml(t T, tc diffTestCase, puwd string, tfConfig any) { schema := sdkv2.NewResource(tc.Resource).Schema() pConfig, err := convertConfigToPulumi(schema, nil, tfConfig) require.NoErrorf(t, err, "convertConfigToPulumi failed") @@ -666,7 +675,7 @@ func pulumiWriteYaml(t *testing.T, tc diffTestCase, puwd string, tfConfig any) { require.NoErrorf(t, err, "writing Pulumi.yaml") } -func execCmd(t *testing.T, wdir string, environ []string, program string, args ...string) *exec.Cmd { +func execCmd(t T, wdir string, environ []string, program string, args ...string) *exec.Cmd { t.Logf("%s %s", program, strings.Join(args, " ")) cmd := exec.Command(program, args...) var stdout, stderr bytes.Buffer @@ -738,7 +747,7 @@ func parseChangesFromTFPlan(plan tfPlan) string { return actions[0] } -func verifyBasicDiffAgreement(t *testing.T, plan tfPlan, us auto.UpdateSummary) { +func verifyBasicDiffAgreement(t T, plan tfPlan, us auto.UpdateSummary) { t.Logf("UpdateSummary.ResourceChanges: %#v", us.ResourceChanges) tfAction := parseChangesFromTFPlan(plan) switch tfAction { diff --git a/pkg/tests/cross-tests/rapid_test.go b/pkg/tests/cross-tests/rapid_test.go new file mode 100644 index 000000000..3c3a356a5 --- /dev/null +++ b/pkg/tests/cross-tests/rapid_test.go @@ -0,0 +1,47 @@ +package crosstests + +import ( + "testing" + "time" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "pgregory.net/rapid" +) + +func TestDiffConvergence(outerT *testing.T) { + tvg := &tvGen{} + + rapid.Check(outerT, func(t *rapid.T) { + tv := tvg.GenObject(3).Draw(t, "tv") + + c1 := tv.valueGen.Draw(t, "config1") + c2 := tv.valueGen.Draw(t, "config2") + + tc := diffTestCase{ + Resource: &schema.Resource{ + Schema: tv.schema.Elem.(*schema.Resource).Schema, + }, + Config1: c1, + Config2: c2, + } + + runDiffCheck(&rapidTWithCleanup{t, outerT}, tc) + }) +} + +type rapidTWithCleanup struct { + *rapid.T + outerT *testing.T +} + +func (rtc *rapidTWithCleanup) TempDir() string { + return rtc.outerT.TempDir() +} + +func (*rapidTWithCleanup) Deadline() (time.Time, bool) { + return time.Time{}, false +} + +func (*rapidTWithCleanup) Cleanup(work func()) { + panic("unexpected cleanup scheduled") +} diff --git a/pkg/tests/cross-tests/rapid_tv_gen.go b/pkg/tests/cross-tests/rapid_tv_gen.go index b3347fa83..6e333a079 100644 --- a/pkg/tests/cross-tests/rapid_tv_gen.go +++ b/pkg/tests/cross-tests/rapid_tv_gen.go @@ -10,9 +10,9 @@ import ( // Combines a TF value representing resource inputs with its schema. The value has to conform to the schema. The name // "tv" stands for a typed value. type tv struct { - schema schema.Schema - typ tftypes.Type - value tftypes.Value + schema schema.Schema + typ tftypes.Type + valueGen *rapid.Generator[tftypes.Value] } type schemaT func(schema.Schema) schema.Schema @@ -32,14 +32,14 @@ func (tvg *tvGen) GenTV(maxDepth int) *rapid.Generator[tv] { func (tvg *tvGen) GenObject(maxDepth int) *rapid.Generator[tv] { return rapid.Custom[tv](func(t *rapid.T) tv { fieldSchemas := map[string]*schema.Schema{} - objectFields := map[string]tftypes.Value{} fieldTypes := map[string]tftypes.Type{} + fieldGenerators := map[string]*rapid.Generator[tftypes.Value]{} nFields := rapid.IntRange(0, 3).Draw(t, "nFields") for i := 0; i < nFields; i++ { fieldName := fmt.Sprintf("f%d", i) fieldTV := tvg.GenTV(maxDepth-1).Draw(t, fieldName) fieldSchemas[fieldName] = &fieldTV.schema - objectFields[fieldName] = fieldTV.value + fieldGenerators[fieldName] = fieldTV.valueGen fieldTypes[fieldName] = fieldTV.typ } objSchema := schema.Schema{ @@ -50,8 +50,15 @@ func (tvg *tvGen) GenObject(maxDepth int) *rapid.Generator[tv] { } st := tvg.GenSchemaTransform().Draw(t, "schemaTransform") objType := tftypes.Object{AttributeTypes: fieldTypes} - objValue := tftypes.NewValue(objType, objectFields) - return tv{st(objSchema), objType, objValue} + objGen := rapid.Custom[tftypes.Value](func(t *rapid.T) tftypes.Value { + fields := map[string]tftypes.Value{} + for f, fg := range fieldGenerators { + fv := fg.Draw(t, f) + fields[f] = fv + } + return tftypes.NewValue(objType, fields) + }) + return tv{st(objSchema), objType, objGen} }) } @@ -65,10 +72,10 @@ func (tvg *tvGen) GenString() *rapid.Generator[tv] { tftypes.NewValue(tftypes.String, ""), tftypes.NewValue(tftypes.String, "text"), } + valueGen := rapid.SampledFrom(values) return rapid.Custom[tv](func(t *rapid.T) tv { st := tvg.GenSchemaTransform().Draw(t, "schemaTransform") - value := rapid.SampledFrom(values).Draw(t, "sampleValue") - return tv{st(s), tftypes.String, value} + return tv{st(s), tftypes.String, valueGen} }) } From e96246b9d21351f330a9d42b7be3883a4e699508 Mon Sep 17 00:00:00 2001 From: Anton Tayanovskyy Date: Wed, 27 Mar 2024 15:21:41 -0400 Subject: [PATCH 03/45] Fixes --- pkg/tests/cross-tests/rapid_test.go | 3 +++ pkg/tests/cross-tests/rapid_tv_gen.go | 39 ++++++++++++++++++++------- 2 files changed, 32 insertions(+), 10 deletions(-) diff --git a/pkg/tests/cross-tests/rapid_test.go b/pkg/tests/cross-tests/rapid_test.go index 3c3a356a5..2539ddcdb 100644 --- a/pkg/tests/cross-tests/rapid_test.go +++ b/pkg/tests/cross-tests/rapid_test.go @@ -1,6 +1,8 @@ package crosstests import ( + "io" + "log" "testing" "time" @@ -9,6 +11,7 @@ import ( ) func TestDiffConvergence(outerT *testing.T) { + log.SetOutput(io.Discard) tvg := &tvGen{} rapid.Check(outerT, func(t *rapid.T) { diff --git a/pkg/tests/cross-tests/rapid_tv_gen.go b/pkg/tests/cross-tests/rapid_tv_gen.go index 6e333a079..707cde142 100644 --- a/pkg/tests/cross-tests/rapid_tv_gen.go +++ b/pkg/tests/cross-tests/rapid_tv_gen.go @@ -50,14 +50,20 @@ func (tvg *tvGen) GenObject(maxDepth int) *rapid.Generator[tv] { } st := tvg.GenSchemaTransform().Draw(t, "schemaTransform") objType := tftypes.Object{AttributeTypes: fieldTypes} - objGen := rapid.Custom[tftypes.Value](func(t *rapid.T) tftypes.Value { - fields := map[string]tftypes.Value{} - for f, fg := range fieldGenerators { - fv := fg.Draw(t, f) - fields[f] = fv - } - return tftypes.NewValue(objType, fields) - }) + var objGen *rapid.Generator[tftypes.Value] + if len(fieldGenerators) > 0 { + objGen = rapid.Custom[tftypes.Value](func(t *rapid.T) tftypes.Value { + fields := map[string]tftypes.Value{} + for f, fg := range fieldGenerators { + fv := fg.Draw(t, f) + fields[f] = fv + } + return tftypes.NewValue(objType, fields) + }) + } else { + objGen = rapid.Just(tftypes.NewValue(objType, map[string]tftypes.Value{})) + } + objGen = tvg.withNullAndUnknown(objType, objGen) return tv{st(objSchema), objType, objGen} }) } @@ -66,8 +72,8 @@ func (tvg *tvGen) GenString() *rapid.Generator[tv] { s := schema.Schema{ Type: schema.TypeString, } + nilValue := tftypes.NewValue(tftypes.String, nil) values := []tftypes.Value{ - tftypes.NewValue(tftypes.String, nil), tftypes.NewValue(tftypes.String, tftypes.UnknownValue), tftypes.NewValue(tftypes.String, ""), tftypes.NewValue(tftypes.String, "text"), @@ -75,10 +81,23 @@ func (tvg *tvGen) GenString() *rapid.Generator[tv] { valueGen := rapid.SampledFrom(values) return rapid.Custom[tv](func(t *rapid.T) tv { st := tvg.GenSchemaTransform().Draw(t, "schemaTransform") - return tv{st(s), tftypes.String, valueGen} + s := st(s) + if s.Required { + return tv{s, tftypes.String, valueGen} + } + return tv{s, tftypes.String, rapid.OneOf(valueGen, rapid.Just(nilValue))} }) } +func (*tvGen) withNullAndUnknown( + t tftypes.Type, + v *rapid.Generator[tftypes.Value], +) *rapid.Generator[tftypes.Value] { + nullV := tftypes.NewValue(t, nil) + unknownV := tftypes.NewValue(t, tftypes.UnknownValue) + return rapid.OneOf(v, rapid.Just(nullV), rapid.Just(unknownV)) +} + func (*tvGen) GenSchemaTransform() *rapid.Generator[schemaT] { return rapid.Custom[schemaT](func(t *rapid.T) schemaT { k := rapid.SampledFrom([]string{"o", "r", "c", "co"}).Draw(t, "optionalKind") From 998a846e5ffb08ff2ef1c04fd331898297060394 Mon Sep 17 00:00:00 2001 From: Anton Tayanovskyy Date: Wed, 27 Mar 2024 15:53:53 -0400 Subject: [PATCH 04/45] Turn off pesky logging --- pkg/tests/cross-tests/cross_test.go | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/pkg/tests/cross-tests/cross_test.go b/pkg/tests/cross-tests/cross_test.go index 184b861ac..3b20c542f 100644 --- a/pkg/tests/cross-tests/cross_test.go +++ b/pkg/tests/cross-tests/cross_test.go @@ -7,6 +7,7 @@ import ( "fmt" "hash/crc32" "io" + "log" "os" "os/exec" "path/filepath" @@ -15,6 +16,7 @@ import ( "strings" "testing" + "github.com/hashicorp/go-hclog" "github.com/hashicorp/go-plugin" "github.com/hashicorp/terraform-plugin-go/tfprotov5" "github.com/hashicorp/terraform-plugin-go/tfprotov5/tf5server" @@ -176,6 +178,10 @@ func toTFProvider(tc diffTestCase) *schema.Provider { } func startTFProvider(t T, tc diffTestCase) *plugin.ReattachConfig { + os.Setenv("TF_LOG_PROVIDER", "off") + os.Setenv("TF_LOG_SDK", "off") + os.Setenv("TF_LOG_SDK_PROTO", "off") + tc.Resource.CustomizeDiff = func( ctx context.Context, rd *schema.ResourceDiff, i interface{}, ) error { @@ -222,9 +228,11 @@ func startTFProvider(t T, tc diffTestCase) *plugin.ReattachConfig { closeCh := make(chan struct{}) serveOpts := []tf5server.ServeOpt{ + tf5server.WithGoPluginLogger(hclog.FromStandardLogger(log.New(io.Discard, "", 0), hclog.DefaultOptions)), tf5server.WithDebug(ctx, reattachConfigCh, closeCh), + tf5server.WithoutLogStderrOverride(), // TODO - can this not assume testing.T - // tf5server.WithLoggingSink(t), + //tf5server.WithLoggingSink(t), } go func() { From 0433b44dc3c4fd1ff9d33209566d160906bf8ce0 Mon Sep 17 00:00:00 2001 From: Anton Tayanovskyy Date: Wed, 27 Mar 2024 16:27:53 -0400 Subject: [PATCH 05/45] Implement cleanup --- pkg/tests/cross-tests/rapid_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/tests/cross-tests/rapid_test.go b/pkg/tests/cross-tests/rapid_test.go index 2539ddcdb..199ca49c2 100644 --- a/pkg/tests/cross-tests/rapid_test.go +++ b/pkg/tests/cross-tests/rapid_test.go @@ -45,6 +45,6 @@ func (*rapidTWithCleanup) Deadline() (time.Time, bool) { return time.Time{}, false } -func (*rapidTWithCleanup) Cleanup(work func()) { - panic("unexpected cleanup scheduled") +func (rtc *rapidTWithCleanup) Cleanup(work func()) { + rtc.outerT.Cleanup(work) } From f17b14f822a91eb30ffe68db12de1e5191b9347d Mon Sep 17 00:00:00 2001 From: Anton Tayanovskyy Date: Wed, 27 Mar 2024 19:20:06 -0400 Subject: [PATCH 06/45] Facility to write actual HCL surface syntax --- pkg/tests/cross-tests/tfwrite.go | 38 ++++++++++ pkg/tests/cross-tests/tfwrite_test.go | 102 ++++++++++++++++++++++++++ 2 files changed, 140 insertions(+) create mode 100644 pkg/tests/cross-tests/tfwrite.go create mode 100644 pkg/tests/cross-tests/tfwrite_test.go diff --git a/pkg/tests/cross-tests/tfwrite.go b/pkg/tests/cross-tests/tfwrite.go new file mode 100644 index 000000000..cf589881c --- /dev/null +++ b/pkg/tests/cross-tests/tfwrite.go @@ -0,0 +1,38 @@ +package crosstests + +import ( + "fmt" + "io" + + "github.com/hashicorp/hcl/v2/hclwrite" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/zclconf/go-cty/cty" +) + +// Writes a resource delaration. Note that unknowns are not yet supported in cty.Value, it will error out if found. +func WriteHCL(out io.Writer, sch map[string]*schema.Schema, resourceType, resourceName string, config cty.Value) error { + if !config.IsWhollyKnown() { + return fmt.Errorf("WriteHCL cannot yet write unknowns") + } + f := hclwrite.NewEmptyFile() + block := f.Body().AppendNewBlock("resource", []string{resourceType, resourceName}) + writeBlock(block.Body(), sch, config.AsValueMap()) + _, err := f.WriteTo(out) + return err +} + +func writeBlock(body *hclwrite.Body, schemas map[string]*schema.Schema, values map[string]cty.Value) { + for key, sch := range schemas { + value, ok := values[key] + if !ok { + continue + } + switch elem := sch.Elem.(type) { + case *schema.Resource: // TODO sch.ConfigMode + newBlock := body.AppendNewBlock(key, nil) + writeBlock(newBlock.Body(), elem.Schema, value.AsValueMap()) + default: + body.SetAttributeValue(key, value) + } + } +} diff --git a/pkg/tests/cross-tests/tfwrite_test.go b/pkg/tests/cross-tests/tfwrite_test.go new file mode 100644 index 000000000..40077d1c0 --- /dev/null +++ b/pkg/tests/cross-tests/tfwrite_test.go @@ -0,0 +1,102 @@ +package crosstests + +import ( + "bytes" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hexops/autogold/v2" + "github.com/stretchr/testify/require" + "github.com/zclconf/go-cty/cty" +) + +func TestWriteHCL(t *testing.T) { + type testCase struct { + name string + value cty.Value + schema map[string]*schema.Schema + expect autogold.Value + } + + testCases := []testCase{ + { + "simple", + cty.ObjectVal(map[string]cty.Value{"x": cty.StringVal("OK")}), + map[string]*schema.Schema{"x": { + Type: schema.TypeString, + Optional: true, + }}, + autogold.Expect(` +resource "res" "ex" { + x = "OK" +} +`), + }, + { + "simple-null", + cty.ObjectVal(map[string]cty.Value{"x": cty.NullVal(cty.String)}), + map[string]*schema.Schema{"x": { + Type: schema.TypeString, + Optional: true, + }}, + autogold.Expect(` +resource "res" "ex" { + x = null +} +`), + }, + { + "simple-missing", + cty.ObjectVal(map[string]cty.Value{}), + map[string]*schema.Schema{"x": { + Type: schema.TypeString, + Optional: true, + }}, + autogold.Expect(` +resource "res" "ex" { +} +`), + }, + { + "with-block", + cty.ObjectVal(map[string]cty.Value{ + "x": cty.StringVal("OK"), + "y": cty.ObjectVal(map[string]cty.Value{ + "foo": cty.NumberIntVal(42), + }), + }), + map[string]*schema.Schema{ + "x": { + Type: schema.TypeString, + Optional: true, + }, + "y": { + Type: schema.TypeMap, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "foo": {Type: schema.TypeInt, Required: true}, + }, + }, + }, + }, + autogold.Expect(` +resource "res" "ex" { + x = "OK" + y { + foo = 42 + } +} +`), + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + var out bytes.Buffer + err := WriteHCL(&out, tc.schema, "res", "ex", tc.value) + require.NoError(t, err) + tc.expect.Equal(t, "\n"+out.String()) + }) + } +} From 302098b8b2d9c041bb4afa63d7024dc3964f3bc6 Mon Sep 17 00:00:00 2001 From: Anton Tayanovskyy Date: Wed, 27 Mar 2024 19:54:50 -0400 Subject: [PATCH 07/45] Adapt tftypes.Value to cty.Value --- pkg/tests/cross-tests/adapt.go | 118 +++++++++++++++++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 pkg/tests/cross-tests/adapt.go diff --git a/pkg/tests/cross-tests/adapt.go b/pkg/tests/cross-tests/adapt.go new file mode 100644 index 000000000..7e4d87350 --- /dev/null +++ b/pkg/tests/cross-tests/adapt.go @@ -0,0 +1,118 @@ +package crosstests + +import ( + "math/big" + + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/pulumi/pulumi/sdk/v3/go/common/util/contract" + "github.com/zclconf/go-cty/cty" +) + +type typeAdapter struct { + typ tftypes.Type +} + +func (ta *typeAdapter) ToCty() cty.Type { + t := ta.typ + switch { + case t.Is(tftypes.String): + return cty.String + case t.Is(tftypes.Number): + return cty.Number + case t.Is(tftypes.Bool): + return cty.Bool + case t.Is(tftypes.List{}): + return cty.List(FromType(t.(tftypes.List).ElementType).ToCty()) + case t.Is(tftypes.Set{}): + return cty.Set(FromType(t.(tftypes.Set).ElementType).ToCty()) + case t.Is(tftypes.Map{}): + return cty.Map(FromType(t.(tftypes.Map).ElementType).ToCty()) + case t.Is(tftypes.Object{}): + fields := map[string]cty.Type{} + for k, v := range t.(tftypes.Object).AttributeTypes { + fields[k] = FromType(v).ToCty() + } + return cty.Object(fields) + default: + contract.Failf("unexpected type %v", t) + return cty.NilType + } +} + +func FromType(t tftypes.Type) *typeAdapter { + return &typeAdapter{t} +} + +type valueAdapter struct { + value *tftypes.Value +} + +func (va *valueAdapter) ToCty() cty.Value { + v := va.value + t := v.Type() + switch { + case v.IsNull(): + return cty.NullVal(FromType(t).ToCty()) + case !v.IsKnown(): + return cty.UnknownVal(FromType(t).ToCty()) + case t.Is(tftypes.String): + var s string + err := v.As(&s) + contract.AssertNoErrorf(err, "unexpected error converting string") + return cty.StringVal(s) + case t.Is(tftypes.Number): + var n *big.Float + err := v.As(&n) + contract.AssertNoErrorf(err, "unexpected error converting number") + return cty.NumberVal(n) + case t.Is(tftypes.Bool): + var b bool + err := v.As(&b) + contract.AssertNoErrorf(err, "unexpected error converting bool") + return cty.BoolVal(b) + case t.Is(tftypes.List{}): + var vals []*tftypes.Value + err := v.As(&vals) + contract.AssertNoErrorf(err, "unexpected error converting list") + var outVals []cty.Value + for i, el := range vals { + outVals[i] = FromValue(el).ToCty() + } + return cty.ListVal(outVals) + case t.Is(tftypes.Set{}): + var vals []*tftypes.Value + err := v.As(&vals) + contract.AssertNoErrorf(err, "unexpected error converting set") + var outVals []cty.Value + for i, el := range vals { + outVals[i] = FromValue(el).ToCty() + } + return cty.SetVal(outVals) + case t.Is(tftypes.Map{}): + var vals map[string]*tftypes.Value + err := v.As(&vals) + contract.AssertNoErrorf(err, "unexpected error converting map") + outVals := make(map[string]cty.Value, len(vals)) + for k, el := range vals { + outVals[k] = FromValue(el).ToCty() + } + return cty.MapVal(outVals) + case t.Is(tftypes.Object{}): + var vals map[string]*tftypes.Value + err := v.As(&vals) + contract.AssertNoErrorf(err, "unexpected error converting object") + outVals := make(map[string]cty.Value, len(vals)) + for k, el := range vals { + outVals[k] = FromValue(el).ToCty() + } + return cty.ObjectVal(outVals) + + default: + contract.Failf("unexpected type %v", t) + return cty.NilVal + } +} + +func FromValue(v *tftypes.Value) *valueAdapter { + return &valueAdapter{v} +} From f7dd19704f49b7fe9498d7ce20cea926f9134bd9 Mon Sep 17 00:00:00 2001 From: Anton Tayanovskyy Date: Wed, 27 Mar 2024 21:24:15 -0400 Subject: [PATCH 08/45] Quick builders for tftypes.Value --- pkg/tests/cross-tests/quick.go | 86 +++++++++++++++++++++++++++++ pkg/tests/cross-tests/quick_test.go | 14 +++++ 2 files changed, 100 insertions(+) create mode 100644 pkg/tests/cross-tests/quick.go create mode 100644 pkg/tests/cross-tests/quick_test.go diff --git a/pkg/tests/cross-tests/quick.go b/pkg/tests/cross-tests/quick.go new file mode 100644 index 000000000..8e75b9e9f --- /dev/null +++ b/pkg/tests/cross-tests/quick.go @@ -0,0 +1,86 @@ +package crosstests + +import ( + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/pulumi/pulumi/sdk/v3/go/common/util/contract" +) + +type quickBuilder struct { +} + +func qb() *quickBuilder { + return &quickBuilder{} +} + +func (q *quickBuilder) objT() quickType { + return quickType{inner: tftypes.Object{}} +} + +func (q *quickBuilder) strT() quickType { + return quickType{inner: tftypes.String} +} + +func (q *quickBuilder) str(x string) quickValue { + return func(tftypes.Type) tftypes.Value { + return tftypes.NewValue(tftypes.String, x) + } +} + +func (q *quickBuilder) unk() quickValue { + return func(t tftypes.Type) tftypes.Value { + return tftypes.NewValue(t, tftypes.UnknownValue) + } +} + +func (q *quickBuilder) obj() quickValue { + return func(t tftypes.Type) tftypes.Value { + contract.Assertf(t.Is(tftypes.Object{}), "expected object type, got %s", t) + m := map[string]tftypes.Value{} + for k, v := range t.(tftypes.Object).AttributeTypes { + m[k] = q.null().build(quickType{v}) + } + return tftypes.NewValue(t, m) + } +} + +func (q *quickBuilder) null() quickValue { + return func(t tftypes.Type) tftypes.Value { + return tftypes.NewValue(t, nil) + } +} + +type quickType struct { + inner tftypes.Type +} + +func (qt quickType) fld(name string, t quickType) quickType { + contract.Assertf(qt.inner.Is(tftypes.Object{}), "expected object type, got %s", qt.inner) + copy := map[string]tftypes.Type{} + for k, v := range qt.inner.(tftypes.Object).AttributeTypes { + copy[k] = v + } + copy[name] = t.inner + return quickType{tftypes.Object{ + AttributeTypes: copy, + }} +} + +type quickValue func(tftypes.Type) tftypes.Value + +func (qv quickValue) build(t quickType) tftypes.Value { + return qv(t.inner) +} + +func (qv quickValue) fld(name string, value quickValue) quickValue { + return func(t tftypes.Type) tftypes.Value { + contract.Assertf(t.Is(tftypes.Object{}), "expected object type, got %s", t) + attrTy := t.(tftypes.Object).AttributeTypes[name] + contract.Assertf(attrTy != nil, "cannot find attribute type for %q", name) + old := qv(t) + dst := map[string]tftypes.Value{} + err := old.As(&dst) + contract.Assertf(err == nil, "expected object value, got %s", old) + dst[name] = value(attrTy) + return tftypes.NewValue(t, dst) + } +} diff --git a/pkg/tests/cross-tests/quick_test.go b/pkg/tests/cross-tests/quick_test.go new file mode 100644 index 000000000..3fa900652 --- /dev/null +++ b/pkg/tests/cross-tests/quick_test.go @@ -0,0 +1,14 @@ +package crosstests + +import ( + "testing" + + "github.com/hexops/autogold/v2" +) + +func TestQuick(t *testing.T) { + q := qb() + ty := q.objT().fld("f0", q.strT()).fld("f1", q.objT()) + value := q.obj().fld("f0", q.str("OK")).fld("f1", q.obj()).build(ty) + autogold.Expect(`tftypes.Object["f0":tftypes.String, "f1":tftypes.Object[]]<"f0":tftypes.String<"OK">, "f1":tftypes.Object[]<>>`).Equal(t, value.String()) +} From 121076e4a198493d946e8a09920dbcd013cb78dd Mon Sep 17 00:00:00 2001 From: Anton Tayanovskyy Date: Wed, 27 Mar 2024 22:59:30 -0400 Subject: [PATCH 09/45] Fixes to block writing --- pkg/tests/cross-tests/tfwrite.go | 24 +++++++++++++++++++++--- pkg/tests/cross-tests/tfwrite_test.go | 2 +- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/pkg/tests/cross-tests/tfwrite.go b/pkg/tests/cross-tests/tfwrite.go index cf589881c..a5a9c1c81 100644 --- a/pkg/tests/cross-tests/tfwrite.go +++ b/pkg/tests/cross-tests/tfwrite.go @@ -3,9 +3,11 @@ package crosstests import ( "fmt" "io" + "sort" "github.com/hashicorp/hcl/v2/hclwrite" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/pulumi/pulumi/sdk/v3/go/common/util/contract" "github.com/zclconf/go-cty/cty" ) @@ -22,15 +24,31 @@ func WriteHCL(out io.Writer, sch map[string]*schema.Schema, resourceType, resour } func writeBlock(body *hclwrite.Body, schemas map[string]*schema.Schema, values map[string]cty.Value) { - for key, sch := range schemas { + keys := make([]string, 0, len(schemas)) + for key := range schemas { + keys = append(keys, key) + } + sort.Strings(keys) + for _, key := range keys { + sch := schemas[key] value, ok := values[key] if !ok { continue } switch elem := sch.Elem.(type) { case *schema.Resource: // TODO sch.ConfigMode - newBlock := body.AppendNewBlock(key, nil) - writeBlock(newBlock.Body(), elem.Schema, value.AsValueMap()) + if sch.Type == schema.TypeMap { + body.SetAttributeValue(key, value) + } else if sch.Type == schema.TypeSet { + for _, v := range value.AsValueSet().Values() { + newBlock := body.AppendNewBlock(key, nil) + writeBlock(newBlock.Body(), elem.Schema, v.AsValueMap()) + } + } else if sch.Type == schema.TypeList { + body.SetAttributeValue(key, value) + } else { + contract.Failf("unexpected schema type %v", sch.Type) + } default: body.SetAttributeValue(key, value) } diff --git a/pkg/tests/cross-tests/tfwrite_test.go b/pkg/tests/cross-tests/tfwrite_test.go index 40077d1c0..d4240c8a9 100644 --- a/pkg/tests/cross-tests/tfwrite_test.go +++ b/pkg/tests/cross-tests/tfwrite_test.go @@ -82,7 +82,7 @@ resource "res" "ex" { autogold.Expect(` resource "res" "ex" { x = "OK" - y { + y = { foo = 42 } } From f32a7809bc3e47e80cb2e1d1773e896b917d58da Mon Sep 17 00:00:00 2001 From: Anton Tayanovskyy Date: Wed, 27 Mar 2024 22:59:50 -0400 Subject: [PATCH 10/45] Separate TF driver --- pkg/tests/cross-tests/tf_driver.go | 195 +++++++++++++++++++++++++++++ 1 file changed, 195 insertions(+) create mode 100644 pkg/tests/cross-tests/tf_driver.go diff --git a/pkg/tests/cross-tests/tf_driver.go b/pkg/tests/cross-tests/tf_driver.go new file mode 100644 index 000000000..927268630 --- /dev/null +++ b/pkg/tests/cross-tests/tf_driver.go @@ -0,0 +1,195 @@ +package crosstests + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "log" + "os" + "path/filepath" + + "github.com/hashicorp/go-hclog" + "github.com/hashicorp/go-plugin" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-go/tfprotov5/tf5server" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/convert" + "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfshim/sdk-v2" + "github.com/pulumi/pulumi/sdk/v3/go/common/util/contract" + "github.com/stretchr/testify/require" +) + +type tfDriver struct { + cwd string + providerName string + reattachConfig *plugin.ReattachConfig + res *schema.Resource +} + +type tfPlan struct { + PlanFile string + RawPlan any +} + +func newTfDriver(t T, dir, providerName, resName string, res *schema.Resource) *tfDriver { + // Did not find a less intrusive way to disable annoying logging: + os.Setenv("TF_LOG_PROVIDER", "off") + os.Setenv("TF_LOG_SDK", "off") + os.Setenv("TF_LOG_SDK_PROTO", "off") + + // res.CustomizeDiff = func( + // ctx context.Context, rd *schema.ResourceDiff, i interface{}, + // ) error { + // return nil + // } + + if res.DeleteContext == nil { + res.DeleteContext = func( + ctx context.Context, rd *schema.ResourceData, i interface{}, + ) diag.Diagnostics { + return diag.Diagnostics{} + } + } + + if res.CreateContext == nil { + res.CreateContext = func( + ctx context.Context, rd *schema.ResourceData, i interface{}, + ) diag.Diagnostics { + rd.SetId("newid") + return diag.Diagnostics{} + } + } + + res.UpdateContext = func( + ctx context.Context, rd *schema.ResourceData, i interface{}, + ) diag.Diagnostics { + return diag.Diagnostics{} + } + + p := &schema.Provider{ + ResourcesMap: map[string]*schema.Resource{ + resName: res, + }, + } + + serverFactory := func() tfprotov5.ProviderServer { + return p.GRPCProvider() + } + + ctx := context.Background() + + reattachConfigCh := make(chan *plugin.ReattachConfig) + closeCh := make(chan struct{}) + + serveOpts := []tf5server.ServeOpt{ + tf5server.WithGoPluginLogger(hclog.FromStandardLogger(log.New(io.Discard, "", 0), hclog.DefaultOptions)), + tf5server.WithDebug(ctx, reattachConfigCh, closeCh), + tf5server.WithoutLogStderrOverride(), + } + + go func() { + err := tf5server.Serve(providerName, serverFactory, serveOpts...) + require.NoError(t, err) + }() + + reattachConfig := <-reattachConfigCh + return &tfDriver{ + providerName: providerName, + cwd: dir, + reattachConfig: reattachConfig, + res: res, + } +} + +func (d *tfDriver) coalesce(t T, x any) *tftypes.Value { + if x == nil { + return nil + } + objectType := convert.InferObjectType(sdkv2.NewSchemaMap(d.res.Schema), nil) + t.Logf("infer object type: %v", objectType) + v := FromType(objectType).NewValue(x) + return &v +} + +func (d *tfDriver) writePlanApply( + t T, + resourceSchema map[string]*schema.Schema, + resourceType, resourceName string, + rawConfig any, +) *tfPlan { + config := d.coalesce(t, rawConfig) + if config != nil { + d.write(t, resourceSchema, resourceType, resourceName, *config) + } + plan := d.plan(t) + d.apply(t, plan) + return plan +} + +func (d *tfDriver) write( + t T, + resourceSchema map[string]*schema.Schema, + resourceType, resourceName string, + config tftypes.Value, +) { + var buf bytes.Buffer + err := WriteHCL(&buf, resourceSchema, resourceType, resourceName, FromValue(config).ToCty()) + t.Logf("HCL: \n%s\n", buf.String()) + bytes := buf.Bytes() + err = os.WriteFile(filepath.Join(d.cwd, "test.tf"), bytes, 0600) + require.NoErrorf(t, err, "writing test.tf") +} + +func (d *tfDriver) plan(t T) *tfPlan { + planFile := filepath.Join(d.cwd, "test.tfplan") + env := []string{d.formatReattachEnvVar()} + execCmd(t, d.cwd, env, "terraform", "plan", "-refresh=false", "-out", planFile) + cmd := execCmd(t, d.cwd, env, "terraform", "show", "-json", planFile) + tp := tfPlan{PlanFile: planFile} + err := json.Unmarshal(cmd.Stdout.(*bytes.Buffer).Bytes(), &tp.RawPlan) + require.NoErrorf(t, err, "failed to unmarshal terraform plan") + return &tp +} + +func (d *tfDriver) apply(t T, plan *tfPlan) { + execCmd(t, d.cwd, []string{d.formatReattachEnvVar()}, + "terraform", "apply", "-auto-approve", "-refresh=false", plan.PlanFile) +} + +func (d *tfDriver) formatReattachEnvVar() string { + name := d.providerName + pluginReattachConfig := d.reattachConfig + + type reattachConfigAddr struct { + Network string + String string + } + + type reattachConfig struct { + Protocol string + ProtocolVersion int + Pid int + Test bool + Addr reattachConfigAddr + } + + reattachBytes, err := json.Marshal(map[string]reattachConfig{ + name: { + Protocol: string(pluginReattachConfig.Protocol), + ProtocolVersion: pluginReattachConfig.ProtocolVersion, + Pid: pluginReattachConfig.Pid, + Test: pluginReattachConfig.Test, + Addr: reattachConfigAddr{ + Network: pluginReattachConfig.Addr.Network(), + String: pluginReattachConfig.Addr.String(), + }, + }, + }) + + contract.AssertNoErrorf(err, "failed to build TF_REATTACH_PROVIDERS string") + return fmt.Sprintf("TF_REATTACH_PROVIDERS=%s", string(reattachBytes)) +} From bb3452a6cb3df0b657eee1b61afbd161d8d8af96 Mon Sep 17 00:00:00 2001 From: Anton Tayanovskyy Date: Wed, 27 Mar 2024 23:00:15 -0400 Subject: [PATCH 11/45] Remove quick.go --- pkg/tests/cross-tests/quick.go | 86 ----------------------------- pkg/tests/cross-tests/quick_test.go | 14 ----- 2 files changed, 100 deletions(-) delete mode 100644 pkg/tests/cross-tests/quick.go delete mode 100644 pkg/tests/cross-tests/quick_test.go diff --git a/pkg/tests/cross-tests/quick.go b/pkg/tests/cross-tests/quick.go deleted file mode 100644 index 8e75b9e9f..000000000 --- a/pkg/tests/cross-tests/quick.go +++ /dev/null @@ -1,86 +0,0 @@ -package crosstests - -import ( - "github.com/hashicorp/terraform-plugin-go/tftypes" - "github.com/pulumi/pulumi/sdk/v3/go/common/util/contract" -) - -type quickBuilder struct { -} - -func qb() *quickBuilder { - return &quickBuilder{} -} - -func (q *quickBuilder) objT() quickType { - return quickType{inner: tftypes.Object{}} -} - -func (q *quickBuilder) strT() quickType { - return quickType{inner: tftypes.String} -} - -func (q *quickBuilder) str(x string) quickValue { - return func(tftypes.Type) tftypes.Value { - return tftypes.NewValue(tftypes.String, x) - } -} - -func (q *quickBuilder) unk() quickValue { - return func(t tftypes.Type) tftypes.Value { - return tftypes.NewValue(t, tftypes.UnknownValue) - } -} - -func (q *quickBuilder) obj() quickValue { - return func(t tftypes.Type) tftypes.Value { - contract.Assertf(t.Is(tftypes.Object{}), "expected object type, got %s", t) - m := map[string]tftypes.Value{} - for k, v := range t.(tftypes.Object).AttributeTypes { - m[k] = q.null().build(quickType{v}) - } - return tftypes.NewValue(t, m) - } -} - -func (q *quickBuilder) null() quickValue { - return func(t tftypes.Type) tftypes.Value { - return tftypes.NewValue(t, nil) - } -} - -type quickType struct { - inner tftypes.Type -} - -func (qt quickType) fld(name string, t quickType) quickType { - contract.Assertf(qt.inner.Is(tftypes.Object{}), "expected object type, got %s", qt.inner) - copy := map[string]tftypes.Type{} - for k, v := range qt.inner.(tftypes.Object).AttributeTypes { - copy[k] = v - } - copy[name] = t.inner - return quickType{tftypes.Object{ - AttributeTypes: copy, - }} -} - -type quickValue func(tftypes.Type) tftypes.Value - -func (qv quickValue) build(t quickType) tftypes.Value { - return qv(t.inner) -} - -func (qv quickValue) fld(name string, value quickValue) quickValue { - return func(t tftypes.Type) tftypes.Value { - contract.Assertf(t.Is(tftypes.Object{}), "expected object type, got %s", t) - attrTy := t.(tftypes.Object).AttributeTypes[name] - contract.Assertf(attrTy != nil, "cannot find attribute type for %q", name) - old := qv(t) - dst := map[string]tftypes.Value{} - err := old.As(&dst) - contract.Assertf(err == nil, "expected object value, got %s", old) - dst[name] = value(attrTy) - return tftypes.NewValue(t, dst) - } -} diff --git a/pkg/tests/cross-tests/quick_test.go b/pkg/tests/cross-tests/quick_test.go deleted file mode 100644 index 3fa900652..000000000 --- a/pkg/tests/cross-tests/quick_test.go +++ /dev/null @@ -1,14 +0,0 @@ -package crosstests - -import ( - "testing" - - "github.com/hexops/autogold/v2" -) - -func TestQuick(t *testing.T) { - q := qb() - ty := q.objT().fld("f0", q.strT()).fld("f1", q.objT()) - value := q.obj().fld("f0", q.str("OK")).fld("f1", q.obj()).build(ty) - autogold.Expect(`tftypes.Object["f0":tftypes.String, "f1":tftypes.Object[]]<"f0":tftypes.String<"OK">, "f1":tftypes.Object[]<>>`).Equal(t, value.String()) -} From d66b6f0c523894d7cda23e771ddafb755fc74774 Mon Sep 17 00:00:00 2001 From: Anton Tayanovskyy Date: Wed, 27 Mar 2024 23:03:03 -0400 Subject: [PATCH 12/45] More adapters --- pkg/tests/cross-tests/adapt.go | 72 ++++++++++++++++++++++++++++++---- 1 file changed, 64 insertions(+), 8 deletions(-) diff --git a/pkg/tests/cross-tests/adapt.go b/pkg/tests/cross-tests/adapt.go index 7e4d87350..c5d8ebafd 100644 --- a/pkg/tests/cross-tests/adapt.go +++ b/pkg/tests/cross-tests/adapt.go @@ -39,12 +39,68 @@ func (ta *typeAdapter) ToCty() cty.Type { } } +func (ta *typeAdapter) NewValue(value any) tftypes.Value { + t := ta.typ + if value == nil { + return tftypes.NewValue(t, nil) + } + switch t := value.(type) { + case tftypes.Value: + return t + case *tftypes.Value: + return *t + } + switch { + case t.Is(tftypes.List{}): + elT := t.(tftypes.List).ElementType + switch v := value.(type) { + case []any: + values := []tftypes.Value{} + for _, el := range v { + values = append(values, FromType(elT).NewValue(el)) + } + return tftypes.NewValue(t, values) + } + case t.Is(tftypes.Set{}): + elT := t.(tftypes.Set).ElementType + switch v := value.(type) { + case []any: + values := []tftypes.Value{} + for _, el := range v { + values = append(values, FromType(elT).NewValue(el)) + } + return tftypes.NewValue(t, values) + } + case t.Is(tftypes.Map{}): + elT := t.(tftypes.Map).ElementType + switch v := value.(type) { + case map[string]any: + values := map[string]tftypes.Value{} + for k, el := range v { + values[k] = FromType(elT).NewValue(el) + } + return tftypes.NewValue(t, values) + } + case t.Is(tftypes.Object{}): + aT := t.(tftypes.Object).AttributeTypes + switch v := value.(type) { + case map[string]any: + values := map[string]tftypes.Value{} + for k, el := range v { + values[k] = FromType(aT[k]).NewValue(el) + } + return tftypes.NewValue(t, values) + } + } + return tftypes.NewValue(t, value) +} + func FromType(t tftypes.Type) *typeAdapter { return &typeAdapter{t} } type valueAdapter struct { - value *tftypes.Value + value tftypes.Value } func (va *valueAdapter) ToCty() cty.Value { @@ -71,25 +127,25 @@ func (va *valueAdapter) ToCty() cty.Value { contract.AssertNoErrorf(err, "unexpected error converting bool") return cty.BoolVal(b) case t.Is(tftypes.List{}): - var vals []*tftypes.Value + var vals []tftypes.Value err := v.As(&vals) contract.AssertNoErrorf(err, "unexpected error converting list") - var outVals []cty.Value + outVals := make([]cty.Value, len(vals)) for i, el := range vals { outVals[i] = FromValue(el).ToCty() } return cty.ListVal(outVals) case t.Is(tftypes.Set{}): - var vals []*tftypes.Value + var vals []tftypes.Value err := v.As(&vals) contract.AssertNoErrorf(err, "unexpected error converting set") - var outVals []cty.Value + outVals := make([]cty.Value, len(vals)) for i, el := range vals { outVals[i] = FromValue(el).ToCty() } return cty.SetVal(outVals) case t.Is(tftypes.Map{}): - var vals map[string]*tftypes.Value + var vals map[string]tftypes.Value err := v.As(&vals) contract.AssertNoErrorf(err, "unexpected error converting map") outVals := make(map[string]cty.Value, len(vals)) @@ -98,7 +154,7 @@ func (va *valueAdapter) ToCty() cty.Value { } return cty.MapVal(outVals) case t.Is(tftypes.Object{}): - var vals map[string]*tftypes.Value + var vals map[string]tftypes.Value err := v.As(&vals) contract.AssertNoErrorf(err, "unexpected error converting object") outVals := make(map[string]cty.Value, len(vals)) @@ -113,6 +169,6 @@ func (va *valueAdapter) ToCty() cty.Value { } } -func FromValue(v *tftypes.Value) *valueAdapter { +func FromValue(v tftypes.Value) *valueAdapter { return &valueAdapter{v} } From 0161bf78ea31430467ca08272f0e92487c63b9c8 Mon Sep 17 00:00:00 2001 From: Anton Tayanovskyy Date: Wed, 27 Mar 2024 23:03:16 -0400 Subject: [PATCH 13/45] Helper files --- pkg/tests/cross-tests/exec.go | 25 +++++++++++++++++++++++++ pkg/tests/cross-tests/t.go | 15 +++++++++++++++ 2 files changed, 40 insertions(+) create mode 100644 pkg/tests/cross-tests/exec.go create mode 100644 pkg/tests/cross-tests/t.go diff --git a/pkg/tests/cross-tests/exec.go b/pkg/tests/cross-tests/exec.go new file mode 100644 index 000000000..9d1fd9465 --- /dev/null +++ b/pkg/tests/cross-tests/exec.go @@ -0,0 +1,25 @@ +package crosstests + +import ( + "bytes" + "os" + "os/exec" + "strings" + + "github.com/stretchr/testify/require" +) + +func execCmd(t T, wdir string, environ []string, program string, args ...string) *exec.Cmd { + t.Logf("%s %s", program, strings.Join(args, " ")) + cmd := exec.Command(program, args...) + var stdout, stderr bytes.Buffer + cmd.Dir = wdir + cmd.Env = os.Environ() + cmd.Env = append(cmd.Env, environ...) + cmd.Stdout = &stdout + cmd.Stderr = &stderr + err := cmd.Run() + require.NoError(t, err, "error from `%s %s`\n\nStdout:\n%s\n\nStderr:\n%s\n\n", + program, strings.Join(args, " "), stdout.String(), stderr.String()) + return cmd +} diff --git a/pkg/tests/cross-tests/t.go b/pkg/tests/cross-tests/t.go new file mode 100644 index 000000000..412e31a20 --- /dev/null +++ b/pkg/tests/cross-tests/t.go @@ -0,0 +1,15 @@ +package crosstests + +import ( + "github.com/pulumi/providertest/pulumitest" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type T interface { + Logf(string, ...any) + TempDir() string + require.TestingT + assert.TestingT + pulumitest.T +} From 8f4fcd7c1af60dc45da403525909a1dadedaaf52 Mon Sep 17 00:00:00 2001 From: Anton Tayanovskyy Date: Wed, 27 Mar 2024 23:04:12 -0400 Subject: [PATCH 14/45] Clean up cross_test --- pkg/tests/cross-tests/cross_test.go | 280 +++++++--------------------- 1 file changed, 67 insertions(+), 213 deletions(-) diff --git a/pkg/tests/cross-tests/cross_test.go b/pkg/tests/cross-tests/cross_test.go index 3b20c542f..5356b6b9d 100644 --- a/pkg/tests/cross-tests/cross_test.go +++ b/pkg/tests/cross-tests/cross_test.go @@ -7,19 +7,13 @@ import ( "fmt" "hash/crc32" "io" - "log" "os" - "os/exec" "path/filepath" "runtime" "slices" "strings" "testing" - "github.com/hashicorp/go-hclog" - "github.com/hashicorp/go-plugin" - "github.com/hashicorp/terraform-plugin-go/tfprotov5" - "github.com/hashicorp/terraform-plugin-go/tfprotov5/tf5server" "github.com/hashicorp/terraform-plugin-go/tftypes" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" @@ -56,8 +50,13 @@ type diffTestCase struct { // Otherwise assume an Update flow for a resource. // // See https://developer.hashicorp.com/terraform/language/syntax/json + // + // This also accepts tftypes.Value encoded data. Config1, Config2 any + // Optional object type for the resource. + ObjectType *tftypes.Object + // Bypass interacting with the bridged Pulumi provider. SkipPulumi bool } @@ -71,33 +70,12 @@ const ( providerVer = "0.0.1" ) -type T interface { - Logf(string, ...any) - TempDir() string - require.TestingT - assert.TestingT - pulumitest.T -} - func runDiffCheck(t T, tc diffTestCase) { - // ctx := context.Background() tfwd := t.TempDir() - reattachConfig := startTFProvider(t, tc) - - tfWriteJSON(t, tfwd, tc.Config1) - p1 := runTFPlan(t, tfwd, reattachConfig) - runTFApply(t, tfwd, reattachConfig, p1) - - tfWriteJSON(t, tfwd, tc.Config2) - p2 := runTFPlan(t, tfwd, reattachConfig) - runTFApply(t, tfwd, reattachConfig, p2) - - { - planBytes, err := json.MarshalIndent(p2.RawPlan, "", " ") - contract.AssertNoErrorf(err, "failed to marshal terraform plan") - t.Logf("TF plan: %v", string(planBytes)) - } + tfd := newTfDriver(t, tfwd, providerShortName, rtype, tc.Resource) + _ = tfd.writePlanApply(t, tc.Resource.Schema, rtype, "example", tc.Config1) + tfDiffPlan := tfd.writePlanApply(t, tc.Resource.Schema, rtype, "example", tc.Config2) if tc.SkipPulumi { return @@ -126,47 +104,7 @@ func runDiffCheck(t T, tc diffTestCase) { pulumiWriteYaml(t, tc, puwd, tc.Config2) x := pt.Up() - verifyBasicDiffAgreement(t, p2, x.Summary) -} - -func tfWriteJSON(t T, cwd string, rconfig any) { - config := map[string]any{ - "resource": map[string]any{ - rtype: map[string]any{ - "example": rconfig, - }, - }, - } - config1bytes, err := json.MarshalIndent(config, "", " ") - require.NoErrorf(t, err, "serializing test.tf.json") - err = os.WriteFile(filepath.Join(cwd, "test.tf.json"), config1bytes, 0600) - require.NoErrorf(t, err, "writing test.tf.json") -} - -type tfPlan struct { - PlanFile string - RawPlan any -} - -func (*tfPlan) OpType() *apitype.OpType { - return nil -} - -func runTFPlan(t T, cwd string, reattachConfig *plugin.ReattachConfig) tfPlan { - planFile := filepath.Join(cwd, "test.tfplan") - env := []string{formatReattachEnvVar(providerName, reattachConfig)} - execCmd(t, cwd, env, "terraform", "plan", "-refresh=false", "-out", planFile) - - cmd := execCmd(t, cwd, env, "terraform", "show", "-json", planFile) - tp := tfPlan{PlanFile: planFile} - err := json.Unmarshal(cmd.Stdout.(*bytes.Buffer).Bytes(), &tp.RawPlan) - contract.AssertNoErrorf(err, "failed to unmarshal terraform plan") - return tp -} - -func runTFApply(t T, cwd string, reattachConfig *plugin.ReattachConfig, p tfPlan) { - execCmd(t, cwd, []string{formatReattachEnvVar(providerName, reattachConfig)}, - "terraform", "apply", "-auto-approve", "-refresh=false", p.PlanFile) + verifyBasicDiffAgreement(t, *tfDiffPlan, x.Summary) } func toTFProvider(tc diffTestCase) *schema.Provider { @@ -177,122 +115,43 @@ func toTFProvider(tc diffTestCase) *schema.Provider { } } -func startTFProvider(t T, tc diffTestCase) *plugin.ReattachConfig { - os.Setenv("TF_LOG_PROVIDER", "off") - os.Setenv("TF_LOG_SDK", "off") - os.Setenv("TF_LOG_SDK_PROTO", "off") - - tc.Resource.CustomizeDiff = func( - ctx context.Context, rd *schema.ResourceDiff, i interface{}, - ) error { - // fmt.Printf(`\n\n CustomizeDiff: rd.Get("set") ==> %#v\n\n\n`, rd.Get("set")) - // fmt.Println("\n\nGetRawPlan: ", rd.GetRawPlan().GoString()) - // fmt.Println("\n\nGetRawConfig: ", rd.GetRawConfig().GoString()) - // fmt.Println("\n\nGetRawState: ", rd.GetRawState().GoString()) - return nil - } - - if tc.Resource.DeleteContext == nil { - tc.Resource.DeleteContext = func( - ctx context.Context, rd *schema.ResourceData, i interface{}, - ) diag.Diagnostics { - return diag.Diagnostics{} - } - } - - if tc.Resource.CreateContext == nil { - tc.Resource.CreateContext = func( - ctx context.Context, rd *schema.ResourceData, i interface{}, - ) diag.Diagnostics { - rd.SetId("newid") - return diag.Diagnostics{} - } - } - - tc.Resource.UpdateContext = func( - ctx context.Context, rd *schema.ResourceData, i interface{}, - ) diag.Diagnostics { - //fmt.Printf(`\n\n Update: rd.Get("set") ==> %#v\n\n\n`, rd.Get("set")) - return diag.Diagnostics{} - } - - p := toTFProvider(tc) - - serverFactory := func() tfprotov5.ProviderServer { - return p.GRPCProvider() - } - - ctx := context.Background() - - reattachConfigCh := make(chan *plugin.ReattachConfig) - closeCh := make(chan struct{}) - - serveOpts := []tf5server.ServeOpt{ - tf5server.WithGoPluginLogger(hclog.FromStandardLogger(log.New(io.Discard, "", 0), hclog.DefaultOptions)), - tf5server.WithDebug(ctx, reattachConfigCh, closeCh), - tf5server.WithoutLogStderrOverride(), - // TODO - can this not assume testing.T - //tf5server.WithLoggingSink(t), - } - - go func() { - err := tf5server.Serve(providerName, serverFactory, serveOpts...) - require.NoError(t, err) - }() - - reattachConfig := <-reattachConfigCh - return reattachConfig -} - -func formatReattachEnvVar(name string, pluginReattachConfig *plugin.ReattachConfig) string { - type reattachConfigAddr struct { - Network string - String string - } - - type reattachConfig struct { - Protocol string - ProtocolVersion int - Pid int - Test bool - Addr reattachConfigAddr - } - - reattachBytes, err := json.Marshal(map[string]reattachConfig{ - name: { - Protocol: string(pluginReattachConfig.Protocol), - ProtocolVersion: pluginReattachConfig.ProtocolVersion, - Pid: pluginReattachConfig.Pid, - Test: pluginReattachConfig.Test, - Addr: reattachConfigAddr{ - Network: pluginReattachConfig.Addr.Network(), - String: pluginReattachConfig.Addr.String(), +func TestUnchangedBasicObject(t *testing.T) { + t.Skipf("TODO - this does not translate correctly to Pulumi yet") + skipUnlessLinux(t) + cfg := map[string]any{"f0": map[string]any{"x": "ok"}} + runDiffCheck(t, diffTestCase{ + Resource: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "f0": { + Required: true, + Type: schema.TypeMap, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "x": {Optional: true, Type: schema.TypeString}, + }, + }, + }, }, }, + Config1: cfg, + Config2: cfg, }) - - contract.AssertNoErrorf(err, "failed to build TF_REATTACH_PROVIDERS string") - return fmt.Sprintf("TF_REATTACH_PROVIDERS=%s", string(reattachBytes)) } func TestSimpleStringNoChange(t *testing.T) { skipUnlessLinux(t) + config := map[string]any{"name": "A"} runDiffCheck(t, diffTestCase{ Resource: &schema.Resource{ Schema: map[string]*schema.Schema{ "name": { - Type: schema.TypeString, Optional: true, }, }, }, - Config1: map[string]any{ - "name": "A", - }, - Config2: map[string]any{ - "name": "A", - }, + Config1: config, + Config2: config, }) } @@ -329,21 +188,14 @@ func TestSetReordering(t *testing.T) { }, }, }, - CreateContext: func( - ctx context.Context, rd *schema.ResourceData, i interface{}, - ) diag.Diagnostics { - rd.SetId("newid") - require.IsType(t, &schema.Set{}, rd.Get("set")) - return diag.Diagnostics{} - }, } runDiffCheck(t, diffTestCase{ Resource: resource, Config1: map[string]any{ - "set": []string{"A", "B"}, + "set": []any{"A", "B"}, }, Config2: map[string]any{ - "set": []string{"B", "A"}, + "set": []any{"B", "A"}, }, }) } @@ -579,8 +431,8 @@ func TestAws2442(t *testing.T) { }, } - jsonifyParameters := func(parameters []parameter) []map[string]interface{} { - var result []map[string]interface{} + jsonifyParameters := func(parameters []parameter) []interface{} { + var result []interface{} for _, p := range parameters { result = append(result, map[string]interface{}{ "name": p.name, @@ -661,7 +513,7 @@ func startPulumiProvider( func pulumiWriteYaml(t T, tc diffTestCase, puwd string, tfConfig any) { schema := sdkv2.NewResource(tc.Resource).Schema() - pConfig, err := convertConfigToPulumi(schema, nil, tfConfig) + pConfig, err := convertConfigToPulumi(schema, nil, tc.ObjectType, tfConfig) require.NoErrorf(t, err, "convertConfigToPulumi failed") data := map[string]any{ "name": "project", @@ -683,48 +535,50 @@ func pulumiWriteYaml(t T, tc diffTestCase, puwd string, tfConfig any) { require.NoErrorf(t, err, "writing Pulumi.yaml") } -func execCmd(t T, wdir string, environ []string, program string, args ...string) *exec.Cmd { - t.Logf("%s %s", program, strings.Join(args, " ")) - cmd := exec.Command(program, args...) - var stdout, stderr bytes.Buffer - cmd.Dir = wdir - cmd.Env = os.Environ() - cmd.Env = append(cmd.Env, environ...) - cmd.Stdout = &stdout - cmd.Stderr = &stderr - err := cmd.Run() - require.NoError(t, err, "error from `%s %s`\n\nStdout:\n%s\n\nStderr:\n%s\n\n", - program, strings.Join(args, " "), stdout.String(), stderr.String()) - return cmd -} - func convertConfigToPulumi( schemaMap shim.SchemaMap, schemaInfos map[string]*tfbridge.SchemaInfo, + objectType *tftypes.Object, tfConfig any, ) (any, error) { - objectType := convert.InferObjectType(schemaMap, nil) - bytes, err := json.Marshal(tfConfig) - if err != nil { - return nil, err - } - // Knowingly using a deprecated function so we can connect back up to tftypes.Value; if this disappears it - // should not be prohibitively difficult to rewrite or vendor. - // - //nolint:staticcheck - v, err := tftypes.ValueFromJSON(bytes, objectType) - if err != nil { - return nil, err + var v *tftypes.Value + + switch tfConfig := tfConfig.(type) { + case *tftypes.Value: + v = tfConfig + if objectType == nil { + ty := v.Type().(tftypes.Object) + objectType = &ty + } + default: + if objectType == nil { + t := convert.InferObjectType(schemaMap, nil) + objectType = &t + } + bytes, err := json.Marshal(tfConfig) + if err != nil { + return nil, err + } + // Knowingly using a deprecated function so we can connect back up to tftypes.Value; if this disappears it + // should not be prohibitively difficult to rewrite or vendor. + // + //nolint:staticcheck + value, err := tftypes.ValueFromJSON(bytes, *objectType) + if err != nil { + return nil, err + } + v = &value } + decoder, err := convert.NewObjectDecoder(convert.ObjectSchema{ SchemaMap: schemaMap, SchemaInfos: schemaInfos, - Object: &objectType, + Object: objectType, }) if err != nil { return nil, err } - pm, err := convert.DecodePropertyMap(decoder, v) + pm, err := convert.DecodePropertyMap(decoder, *v) if err != nil { return nil, err } @@ -751,7 +605,7 @@ func parseChangesFromTFPlan(plan tfPlan) string { contract.AssertNoErrorf(err, "failed to unmarshal terraform plan") contract.Assertf(len(pp.ResourceChanges) == 1, "expected exactly one resource change") actions := pp.ResourceChanges[0].Change.Actions - contract.Assertf(len(actions) == 1, "expected exactly one action") + contract.Assertf(len(actions) == 1, "expected exactly one action, got %v", strings.Join(actions, ", ")) return actions[0] } From 180f6a336b34e266a937825ae2fd6711add38c23 Mon Sep 17 00:00:00 2001 From: Anton Tayanovskyy Date: Wed, 27 Mar 2024 23:04:31 -0400 Subject: [PATCH 15/45] Generate generators --- pkg/tests/cross-tests/rapid_tv_gen.go | 91 ++++++++++++++++++++------- 1 file changed, 68 insertions(+), 23 deletions(-) diff --git a/pkg/tests/cross-tests/rapid_tv_gen.go b/pkg/tests/cross-tests/rapid_tv_gen.go index 707cde142..88fefeed0 100644 --- a/pkg/tests/cross-tests/rapid_tv_gen.go +++ b/pkg/tests/cross-tests/rapid_tv_gen.go @@ -1,7 +1,9 @@ package crosstests import ( + "bytes" "fmt" + "github.com/hashicorp/terraform-plugin-go/tftypes" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "pgregory.net/rapid" @@ -12,19 +14,21 @@ import ( type tv struct { schema schema.Schema typ tftypes.Type - valueGen *rapid.Generator[tftypes.Value] + valueGen *rapid.Generator[wrappedValue] } type schemaT func(schema.Schema) schema.Schema -type tvGen struct{} +type tvGen struct { + generateUnknowns bool +} func (tvg *tvGen) GenTV(maxDepth int) *rapid.Generator[tv] { opts := []*rapid.Generator[tv]{ tvg.GenString(), } if maxDepth > 1 { - opts = append(opts, tvg.GenObject(maxDepth-1)) + opts = append(opts, tvg.GenObjectValue(maxDepth-1)) } return rapid.OneOf(opts...) } @@ -33,7 +37,7 @@ func (tvg *tvGen) GenObject(maxDepth int) *rapid.Generator[tv] { return rapid.Custom[tv](func(t *rapid.T) tv { fieldSchemas := map[string]*schema.Schema{} fieldTypes := map[string]tftypes.Type{} - fieldGenerators := map[string]*rapid.Generator[tftypes.Value]{} + fieldGenerators := map[string]*rapid.Generator[wrappedValue]{} nFields := rapid.IntRange(0, 3).Draw(t, "nFields") for i := 0; i < nFields; i++ { fieldName := fmt.Sprintf("f%d", i) @@ -56,15 +60,22 @@ func (tvg *tvGen) GenObject(maxDepth int) *rapid.Generator[tv] { fields := map[string]tftypes.Value{} for f, fg := range fieldGenerators { fv := fg.Draw(t, f) - fields[f] = fv + fields[f] = fv.inner } return tftypes.NewValue(objType, fields) }) } else { objGen = rapid.Just(tftypes.NewValue(objType, map[string]tftypes.Value{})) } - objGen = tvg.withNullAndUnknown(objType, objGen) - return tv{st(objSchema), objType, objGen} + return tv{st(objSchema), objType, rapid.Map(objGen, newWrappedValue)} + }) +} + +// Like [GenObject] but can also return top-level nil or unknown. +func (tvg *tvGen) GenObjectValue(maxDepth int) *rapid.Generator[tv] { + return rapid.Map(tvg.GenObject(maxDepth), func(x tv) tv { + x.valueGen = tvg.withNullAndUnknown(x.typ, x.valueGen) + return x }) } @@ -74,35 +85,42 @@ func (tvg *tvGen) GenString() *rapid.Generator[tv] { } nilValue := tftypes.NewValue(tftypes.String, nil) values := []tftypes.Value{ - tftypes.NewValue(tftypes.String, tftypes.UnknownValue), tftypes.NewValue(tftypes.String, ""), tftypes.NewValue(tftypes.String, "text"), } - valueGen := rapid.SampledFrom(values) + if tvg.generateUnknowns { + values = append(values, tftypes.NewValue(tftypes.String, tftypes.UnknownValue)) + } + valueGen := rapid.Map(rapid.SampledFrom(values), newWrappedValue) return rapid.Custom[tv](func(t *rapid.T) tv { st := tvg.GenSchemaTransform().Draw(t, "schemaTransform") s := st(s) if s.Required { return tv{s, tftypes.String, valueGen} } - return tv{s, tftypes.String, rapid.OneOf(valueGen, rapid.Just(nilValue))} + return tv{s, tftypes.String, rapid.OneOf(valueGen, rapid.Just(newWrappedValue(nilValue)))} }) } -func (*tvGen) withNullAndUnknown( +func (tvg *tvGen) withNullAndUnknown( t tftypes.Type, - v *rapid.Generator[tftypes.Value], -) *rapid.Generator[tftypes.Value] { + v *rapid.Generator[wrappedValue], +) *rapid.Generator[wrappedValue] { nullV := tftypes.NewValue(t, nil) - unknownV := tftypes.NewValue(t, tftypes.UnknownValue) - return rapid.OneOf(v, rapid.Just(nullV), rapid.Just(unknownV)) + if tvg.generateUnknowns { + unknownV := tftypes.NewValue(t, tftypes.UnknownValue) + return rapid.OneOf(v, + rapid.Just(newWrappedValue(nullV)), + rapid.Just(newWrappedValue(unknownV))) + } + return rapid.OneOf(v, rapid.Just(newWrappedValue(nullV))) } func (*tvGen) GenSchemaTransform() *rapid.Generator[schemaT] { return rapid.Custom[schemaT](func(t *rapid.T) schemaT { k := rapid.SampledFrom([]string{"o", "r", "c", "co"}).Draw(t, "optionalKind") - secret := rapid.Bool().Draw(t, "secret") - forceNew := rapid.Bool().Draw(t, "forceNew") + // secret := rapid.Bool().Draw(t, "secret") + // forceNew := rapid.Bool().Draw(t, "forceNew") return func(s schema.Schema) schema.Schema { switch k { @@ -116,13 +134,40 @@ func (*tvGen) GenSchemaTransform() *rapid.Generator[schemaT] { s.Computed = true s.Optional = true } - if forceNew { - s.ForceNew = true - } - if secret { - s.Sensitive = true - } + // if forceNew { + // s.ForceNew = true + // } + // if secret { + // s.Sensitive = true + // } return s } }) } + +// Wrapping tftypes.Value to provide a friendlier GoString implementation. Whenever rapid draws a value it logs it, and +// it is really nice to be able to actually read the result. +type wrappedValue struct { + inner tftypes.Value +} + +func newWrappedValue(v tftypes.Value) wrappedValue { + return wrappedValue{v} +} + +func (s wrappedValue) GoString() string { + var buf bytes.Buffer + fmt.Fprintf(&buf, "<<<\n") + tftypes.Walk(s.inner, func(ap *tftypes.AttributePath, v tftypes.Value) (bool, error) { + switch { + case v.Type().Is(tftypes.Object{}) || v.Type().Is(tftypes.Set{}) || + v.Type().Is(tftypes.Map{}) || v.Type().Is(tftypes.List{}): + return true, nil + default: + fmt.Fprintf(&buf, "%s: %s\n", ap.String(), v.String()) + return true, nil + } + }) + fmt.Fprintf(&buf, ">>>\n") + return buf.String() + ":" + fmt.Sprintf("%#v", s.inner) +} From 896c6c5de45979c6b830ae917a24f91074d587b8 Mon Sep 17 00:00:00 2001 From: Anton Tayanovskyy Date: Wed, 27 Mar 2024 23:07:39 -0400 Subject: [PATCH 16/45] WIP on rapid --- pkg/tests/cross-tests/rapid_test.go | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/pkg/tests/cross-tests/rapid_test.go b/pkg/tests/cross-tests/rapid_test.go index 199ca49c2..74a670e9c 100644 --- a/pkg/tests/cross-tests/rapid_test.go +++ b/pkg/tests/cross-tests/rapid_test.go @@ -6,26 +6,32 @@ import ( "testing" "time" + "github.com/hashicorp/terraform-plugin-go/tftypes" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "pgregory.net/rapid" ) func TestDiffConvergence(outerT *testing.T) { + t.Skipf("Work in progress") log.SetOutput(io.Discard) tvg := &tvGen{} rapid.Check(outerT, func(t *rapid.T) { + + outerT.Logf("Iterating..") tv := tvg.GenObject(3).Draw(t, "tv") c1 := tv.valueGen.Draw(t, "config1") c2 := tv.valueGen.Draw(t, "config2") + ty := tv.typ.(tftypes.Object) tc := diffTestCase{ Resource: &schema.Resource{ Schema: tv.schema.Elem.(*schema.Resource).Schema, }, - Config1: c1, - Config2: c2, + Config1: c1.inner, + Config2: c2.inner, + ObjectType: &ty, } runDiffCheck(&rapidTWithCleanup{t, outerT}, tc) From 93ed7494ea8f601ed6add447c512184596708db1 Mon Sep 17 00:00:00 2001 From: Anton Tayanovskyy Date: Tue, 2 Apr 2024 09:12:44 -0400 Subject: [PATCH 17/45] tfwrite support for nested blocks --- pkg/tests/cross-tests/tfwrite.go | 5 +- pkg/tests/cross-tests/tfwrite_test.go | 68 ++++++++++++++++++++++++++- 2 files changed, 71 insertions(+), 2 deletions(-) diff --git a/pkg/tests/cross-tests/tfwrite.go b/pkg/tests/cross-tests/tfwrite.go index a5a9c1c81..0d84ff70a 100644 --- a/pkg/tests/cross-tests/tfwrite.go +++ b/pkg/tests/cross-tests/tfwrite.go @@ -45,7 +45,10 @@ func writeBlock(body *hclwrite.Body, schemas map[string]*schema.Schema, values m writeBlock(newBlock.Body(), elem.Schema, v.AsValueMap()) } } else if sch.Type == schema.TypeList { - body.SetAttributeValue(key, value) + for _, v := range value.AsValueSlice() { + newBlock := body.AppendNewBlock(key, nil) + writeBlock(newBlock.Body(), elem.Schema, v.AsValueMap()) + } } else { contract.Failf("unexpected schema type %v", sch.Type) } diff --git a/pkg/tests/cross-tests/tfwrite_test.go b/pkg/tests/cross-tests/tfwrite_test.go index d4240c8a9..52fc0e78f 100644 --- a/pkg/tests/cross-tests/tfwrite_test.go +++ b/pkg/tests/cross-tests/tfwrite_test.go @@ -58,7 +58,7 @@ resource "res" "ex" { `), }, { - "with-block", + "single-nested-block", cty.ObjectVal(map[string]cty.Value{ "x": cty.StringVal("OK"), "y": cty.ObjectVal(map[string]cty.Value{ @@ -86,6 +86,72 @@ resource "res" "ex" { foo = 42 } } +`), + }, + { + "list-nested-block", + cty.ObjectVal(map[string]cty.Value{ + "blk": cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "foo": cty.NumberIntVal(1), + }), + cty.ObjectVal(map[string]cty.Value{ + "foo": cty.NumberIntVal(2), + }), + }), + }), + map[string]*schema.Schema{ + "blk": { + Type: schema.TypeList, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "foo": {Type: schema.TypeInt, Required: true}, + }, + }, + }, + }, + autogold.Expect(` +resource "res" "ex" { + blk { + foo = 1 + } + blk { + foo = 2 + } +} +`), + }, + { + "set-nested-block", + cty.ObjectVal(map[string]cty.Value{ + "blk": cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "foo": cty.NumberIntVal(1), + }), + cty.ObjectVal(map[string]cty.Value{ + "foo": cty.NumberIntVal(2), + }), + }), + }), + map[string]*schema.Schema{ + "blk": { + Type: schema.TypeSet, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "foo": {Type: schema.TypeInt, Required: true}, + }, + }, + }, + }, + autogold.Expect(` +resource "res" "ex" { + blk { + foo = 1 + } + blk { + foo = 2 + } +} `), }, } From 67a7c503630dd6ce4ec0a868f46521f69874e21e Mon Sep 17 00:00:00 2001 From: Anton Tayanovskyy Date: Tue, 2 Apr 2024 09:48:44 -0400 Subject: [PATCH 18/45] wip --- pkg/tests/cross-tests/cross_test.go | 17 ++++++++++++----- pkg/tests/cross-tests/rapid_test.go | 2 +- pkg/tfshim/sdk-v2/cty.go | 7 +++++-- 3 files changed, 18 insertions(+), 8 deletions(-) diff --git a/pkg/tests/cross-tests/cross_test.go b/pkg/tests/cross-tests/cross_test.go index 5356b6b9d..90551d758 100644 --- a/pkg/tests/cross-tests/cross_test.go +++ b/pkg/tests/cross-tests/cross_test.go @@ -99,12 +99,19 @@ func runDiffCheck(t T, tc diffTestCase) { ), ) - pt.Up() + defer func() { + for _, log := range pt.GrpcLog().Entries { + t.Logf("%v\n req: %s\n res: %s\n", log.Method, log.Request, log.Response) + } + }() - pulumiWriteYaml(t, tc, puwd, tc.Config2) - x := pt.Up() + pt.Up() - verifyBasicDiffAgreement(t, *tfDiffPlan, x.Summary) + if 1+2 == 4 { + pulumiWriteYaml(t, tc, puwd, tc.Config2) + x := pt.Up() + verifyBasicDiffAgreement(t, *tfDiffPlan, x.Summary) + } } func toTFProvider(tc diffTestCase) *schema.Provider { @@ -116,7 +123,6 @@ func toTFProvider(tc diffTestCase) *schema.Provider { } func TestUnchangedBasicObject(t *testing.T) { - t.Skipf("TODO - this does not translate correctly to Pulumi yet") skipUnlessLinux(t) cfg := map[string]any{"f0": map[string]any{"x": "ok"}} runDiffCheck(t, diffTestCase{ @@ -530,6 +536,7 @@ func pulumiWriteYaml(t T, tc diffTestCase, puwd string, tfConfig any) { } b, err := yaml.Marshal(data) require.NoErrorf(t, err, "marshaling Pulumi.yaml") + t.Logf("\n\n%s", b) p := filepath.Join(puwd, "Pulumi.yaml") err = os.WriteFile(p, b, 0600) require.NoErrorf(t, err, "writing Pulumi.yaml") diff --git a/pkg/tests/cross-tests/rapid_test.go b/pkg/tests/cross-tests/rapid_test.go index 74a670e9c..0f5e27d9e 100644 --- a/pkg/tests/cross-tests/rapid_test.go +++ b/pkg/tests/cross-tests/rapid_test.go @@ -12,7 +12,7 @@ import ( ) func TestDiffConvergence(outerT *testing.T) { - t.Skipf("Work in progress") + outerT.Skipf("Work in progress") log.SetOutput(io.Discard) tvg := &tvGen{} diff --git a/pkg/tfshim/sdk-v2/cty.go b/pkg/tfshim/sdk-v2/cty.go index bfc93c922..1cf906521 100644 --- a/pkg/tfshim/sdk-v2/cty.go +++ b/pkg/tfshim/sdk-v2/cty.go @@ -119,7 +119,7 @@ func recoverCtyValue(dT cty.Type, value interface{}) (cty.Value, error) { case dT.IsObjectType(): return recoverCtyValueOfObjectType(dT, value) default: - return cty.NilVal, fmt.Errorf("Cannot reconcile map %v to %v", value, dT) + return cty.NilVal, fmt.Errorf("Cannot reconcile map %v to %v", value, dT.GoString()) } case []interface{}: switch { @@ -130,7 +130,10 @@ func recoverCtyValue(dT cty.Type, value interface{}) (cty.Value, error) { case dT.IsListType(): return recoverCtyValueOfListType(dT, value) default: - return cty.NilVal, fmt.Errorf("Cannot reconcile slice %v to %v", value, dT) + if 1+1 == 2 { + //panic(fmt.Errorf("Cannot reconcile slice %v to %v", value, dT.GoString())) + } + return cty.NilVal, fmt.Errorf("Cannot reconcile slice %v to %v", value, dT.GoString()) } default: v, err := recoverScalarCtyValue(dT, value) From b6ab53435b40dae0afe0a89020835031e1cb7118 Mon Sep 17 00:00:00 2001 From: Anton Tayanovskyy Date: Tue, 2 Apr 2024 11:04:30 -0400 Subject: [PATCH 19/45] Fix object repr --- pkg/tests/cross-tests/cross_test.go | 13 ++++++------- pkg/tfshim/sdk-v2/cty.go | 5 +---- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/pkg/tests/cross-tests/cross_test.go b/pkg/tests/cross-tests/cross_test.go index 90551d758..8e068f64b 100644 --- a/pkg/tests/cross-tests/cross_test.go +++ b/pkg/tests/cross-tests/cross_test.go @@ -107,11 +107,9 @@ func runDiffCheck(t T, tc diffTestCase) { pt.Up() - if 1+2 == 4 { - pulumiWriteYaml(t, tc, puwd, tc.Config2) - x := pt.Up() - verifyBasicDiffAgreement(t, *tfDiffPlan, x.Summary) - } + pulumiWriteYaml(t, tc, puwd, tc.Config2) + x := pt.Up() + verifyBasicDiffAgreement(t, *tfDiffPlan, x.Summary) } func toTFProvider(tc diffTestCase) *schema.Provider { @@ -124,13 +122,14 @@ func toTFProvider(tc diffTestCase) *schema.Provider { func TestUnchangedBasicObject(t *testing.T) { skipUnlessLinux(t) - cfg := map[string]any{"f0": map[string]any{"x": "ok"}} + cfg := map[string]any{"f0": []any{map[string]any{"x": "ok"}}} runDiffCheck(t, diffTestCase{ Resource: &schema.Resource{ Schema: map[string]*schema.Schema{ "f0": { Required: true, - Type: schema.TypeMap, + Type: schema.TypeList, + MaxItems: 1, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "x": {Optional: true, Type: schema.TypeString}, diff --git a/pkg/tfshim/sdk-v2/cty.go b/pkg/tfshim/sdk-v2/cty.go index 1cf906521..76013531d 100644 --- a/pkg/tfshim/sdk-v2/cty.go +++ b/pkg/tfshim/sdk-v2/cty.go @@ -130,10 +130,7 @@ func recoverCtyValue(dT cty.Type, value interface{}) (cty.Value, error) { case dT.IsListType(): return recoverCtyValueOfListType(dT, value) default: - if 1+1 == 2 { - //panic(fmt.Errorf("Cannot reconcile slice %v to %v", value, dT.GoString())) - } - return cty.NilVal, fmt.Errorf("Cannot reconcile slice %v to %v", value, dT.GoString()) + return cty.NilVal, fmt.Errorf("Cannot reconcile slice %v to %v %v %v", value, dT.GoString(), len(value), dT.IsObjectType()) } default: v, err := recoverScalarCtyValue(dT, value) From df6ca0b8a90ec48d1dd77da15cc8b16908b42c3d Mon Sep 17 00:00:00 2001 From: Anton Tayanovskyy Date: Tue, 2 Apr 2024 11:07:58 -0400 Subject: [PATCH 20/45] Remove debug println --- pkg/tests/cross-tests/cross_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/pkg/tests/cross-tests/cross_test.go b/pkg/tests/cross-tests/cross_test.go index 8e068f64b..ecc4349f2 100644 --- a/pkg/tests/cross-tests/cross_test.go +++ b/pkg/tests/cross-tests/cross_test.go @@ -279,7 +279,6 @@ func TestAws2442(t *testing.T) { // Now intentionally reorder parameters away from the canonical order. err := rd.Set("parameter", parameterList[0:3]) require.NoError(t, err) - fmt.Println("CREATE! set to 3") return make(diag.Diagnostics, 0) }, // UpdateContext: func(ctx context.Context, rd *schema.ResourceData, i interface{}) diag.Diagnostics { From 0d255a9da9b822b937e81186ecd4bea1424ddb19 Mon Sep 17 00:00:00 2001 From: Anton Tayanovskyy Date: Tue, 2 Apr 2024 11:22:45 -0400 Subject: [PATCH 21/45] Fixes to pretty-printing --- pkg/tests/cross-tests/pretty.go | 41 +++++++++++++++++++++ pkg/tests/cross-tests/rapid_tv_gen.go | 52 ++++++++------------------- 2 files changed, 55 insertions(+), 38 deletions(-) create mode 100644 pkg/tests/cross-tests/pretty.go diff --git a/pkg/tests/cross-tests/pretty.go b/pkg/tests/cross-tests/pretty.go new file mode 100644 index 000000000..1d0b8e52e --- /dev/null +++ b/pkg/tests/cross-tests/pretty.go @@ -0,0 +1,41 @@ +package crosstests + +import ( + "bytes" + "fmt" + + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +// Large printouts of tftypes.Value are very difficult to read when debugging the tests, especially because of all the +// extraneous type information printed. This wrapper is a work in progress to implement better pretty-printing. +type prettyValueWrapper struct { + inner tftypes.Value +} + +func newPrettyValueWrapper(v tftypes.Value) prettyValueWrapper { + return prettyValueWrapper{v} +} + +func (s prettyValueWrapper) Value() tftypes.Value { + return s.inner +} + +// This is not yet valid Go syntax, but when rapid.Draw is used to pull a value it calls GoString and logs the result, +// which is the primary way to interact with the printout, so the code opts to implement this. +func (s prettyValueWrapper) GoString() string { + var buf bytes.Buffer + fmt.Fprintf(&buf, "<<<\n") + tftypes.Walk(s.inner, func(ap *tftypes.AttributePath, v tftypes.Value) (bool, error) { + switch { + case v.Type().Is(tftypes.Object{}) || v.Type().Is(tftypes.Set{}) || + v.Type().Is(tftypes.Map{}) || v.Type().Is(tftypes.List{}): + return true, nil + default: + fmt.Fprintf(&buf, "%s: %s\n", ap.String(), v.String()) + return true, nil + } + }) + fmt.Fprintf(&buf, ">>>\n") + return buf.String() + ":" + fmt.Sprintf("%#v", s.inner) +} diff --git a/pkg/tests/cross-tests/rapid_tv_gen.go b/pkg/tests/cross-tests/rapid_tv_gen.go index 88fefeed0..2afaacbc8 100644 --- a/pkg/tests/cross-tests/rapid_tv_gen.go +++ b/pkg/tests/cross-tests/rapid_tv_gen.go @@ -1,7 +1,6 @@ package crosstests import ( - "bytes" "fmt" "github.com/hashicorp/terraform-plugin-go/tftypes" @@ -14,7 +13,7 @@ import ( type tv struct { schema schema.Schema typ tftypes.Type - valueGen *rapid.Generator[wrappedValue] + valueGen *rapid.Generator[tftypes.Value] } type schemaT func(schema.Schema) schema.Schema @@ -37,7 +36,7 @@ func (tvg *tvGen) GenObject(maxDepth int) *rapid.Generator[tv] { return rapid.Custom[tv](func(t *rapid.T) tv { fieldSchemas := map[string]*schema.Schema{} fieldTypes := map[string]tftypes.Type{} - fieldGenerators := map[string]*rapid.Generator[wrappedValue]{} + fieldGenerators := map[string]*rapid.Generator[tftypes.Value]{} nFields := rapid.IntRange(0, 3).Draw(t, "nFields") for i := 0; i < nFields; i++ { fieldName := fmt.Sprintf("f%d", i) @@ -59,15 +58,15 @@ func (tvg *tvGen) GenObject(maxDepth int) *rapid.Generator[tv] { objGen = rapid.Custom[tftypes.Value](func(t *rapid.T) tftypes.Value { fields := map[string]tftypes.Value{} for f, fg := range fieldGenerators { - fv := fg.Draw(t, f) - fields[f] = fv.inner + fv := tvg.drawValue(t, fg, f) + fields[f] = fv } return tftypes.NewValue(objType, fields) }) } else { objGen = rapid.Just(tftypes.NewValue(objType, map[string]tftypes.Value{})) } - return tv{st(objSchema), objType, rapid.Map(objGen, newWrappedValue)} + return tv{st(objSchema), objType, objGen} }) } @@ -91,29 +90,29 @@ func (tvg *tvGen) GenString() *rapid.Generator[tv] { if tvg.generateUnknowns { values = append(values, tftypes.NewValue(tftypes.String, tftypes.UnknownValue)) } - valueGen := rapid.Map(rapid.SampledFrom(values), newWrappedValue) + valueGen := rapid.SampledFrom(values) return rapid.Custom[tv](func(t *rapid.T) tv { st := tvg.GenSchemaTransform().Draw(t, "schemaTransform") s := st(s) if s.Required { return tv{s, tftypes.String, valueGen} } - return tv{s, tftypes.String, rapid.OneOf(valueGen, rapid.Just(newWrappedValue(nilValue)))} + return tv{s, tftypes.String, rapid.OneOf(valueGen, rapid.Just(nilValue))} }) } func (tvg *tvGen) withNullAndUnknown( t tftypes.Type, - v *rapid.Generator[wrappedValue], -) *rapid.Generator[wrappedValue] { + v *rapid.Generator[tftypes.Value], +) *rapid.Generator[tftypes.Value] { nullV := tftypes.NewValue(t, nil) if tvg.generateUnknowns { unknownV := tftypes.NewValue(t, tftypes.UnknownValue) return rapid.OneOf(v, - rapid.Just(newWrappedValue(nullV)), - rapid.Just(newWrappedValue(unknownV))) + rapid.Just(nullV), + rapid.Just(unknownV)) } - return rapid.OneOf(v, rapid.Just(newWrappedValue(nullV))) + return rapid.OneOf(v, rapid.Just(nullV)) } func (*tvGen) GenSchemaTransform() *rapid.Generator[schemaT] { @@ -145,29 +144,6 @@ func (*tvGen) GenSchemaTransform() *rapid.Generator[schemaT] { }) } -// Wrapping tftypes.Value to provide a friendlier GoString implementation. Whenever rapid draws a value it logs it, and -// it is really nice to be able to actually read the result. -type wrappedValue struct { - inner tftypes.Value -} - -func newWrappedValue(v tftypes.Value) wrappedValue { - return wrappedValue{v} -} - -func (s wrappedValue) GoString() string { - var buf bytes.Buffer - fmt.Fprintf(&buf, "<<<\n") - tftypes.Walk(s.inner, func(ap *tftypes.AttributePath, v tftypes.Value) (bool, error) { - switch { - case v.Type().Is(tftypes.Object{}) || v.Type().Is(tftypes.Set{}) || - v.Type().Is(tftypes.Map{}) || v.Type().Is(tftypes.List{}): - return true, nil - default: - fmt.Fprintf(&buf, "%s: %s\n", ap.String(), v.String()) - return true, nil - } - }) - fmt.Fprintf(&buf, ">>>\n") - return buf.String() + ":" + fmt.Sprintf("%#v", s.inner) +func (*tvGen) drawValue(t *rapid.T, g *rapid.Generator[tftypes.Value], label string) tftypes.Value { + return rapid.Map(g, newPrettyValueWrapper).Draw(t, label).Value() } From 4e3eef920f3af34ccdacef0ce6931a8304831bf3 Mon Sep 17 00:00:00 2001 From: Anton Tayanovskyy Date: Tue, 2 Apr 2024 14:12:22 -0400 Subject: [PATCH 22/45] Progress --- pkg/tests/cross-tests/cross_test.go | 6 ++ pkg/tests/cross-tests/rapid_test.go | 12 ++- pkg/tests/cross-tests/rapid_tv_gen.go | 102 +++++++++++++++++--------- 3 files changed, 78 insertions(+), 42 deletions(-) diff --git a/pkg/tests/cross-tests/cross_test.go b/pkg/tests/cross-tests/cross_test.go index ecc4349f2..ef3b9cc0b 100644 --- a/pkg/tests/cross-tests/cross_test.go +++ b/pkg/tests/cross-tests/cross_test.go @@ -549,6 +549,12 @@ func convertConfigToPulumi( var v *tftypes.Value switch tfConfig := tfConfig.(type) { + case tftypes.Value: + v = &tfConfig + if objectType == nil { + ty := v.Type().(tftypes.Object) + objectType = &ty + } case *tftypes.Value: v = tfConfig if objectType == nil { diff --git a/pkg/tests/cross-tests/rapid_test.go b/pkg/tests/cross-tests/rapid_test.go index 0f5e27d9e..367a1ad2f 100644 --- a/pkg/tests/cross-tests/rapid_test.go +++ b/pkg/tests/cross-tests/rapid_test.go @@ -6,31 +6,29 @@ import ( "testing" "time" - "github.com/hashicorp/terraform-plugin-go/tftypes" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "pgregory.net/rapid" ) func TestDiffConvergence(outerT *testing.T) { - outerT.Skipf("Work in progress") log.SetOutput(io.Discard) tvg := &tvGen{} rapid.Check(outerT, func(t *rapid.T) { outerT.Logf("Iterating..") - tv := tvg.GenObject(3).Draw(t, "tv") + tv := tvg.GenBlock(3).Draw(t, "tv") c1 := tv.valueGen.Draw(t, "config1") c2 := tv.valueGen.Draw(t, "config2") - ty := tv.typ.(tftypes.Object) + ty := tv.typ tc := diffTestCase{ Resource: &schema.Resource{ - Schema: tv.schema.Elem.(*schema.Resource).Schema, + Schema: tv.schemaMap, }, - Config1: c1.inner, - Config2: c2.inner, + Config1: c1, + Config2: c2, ObjectType: &ty, } diff --git a/pkg/tests/cross-tests/rapid_tv_gen.go b/pkg/tests/cross-tests/rapid_tv_gen.go index 2afaacbc8..1a6f93fc7 100644 --- a/pkg/tests/cross-tests/rapid_tv_gen.go +++ b/pkg/tests/cross-tests/rapid_tv_gen.go @@ -16,6 +16,13 @@ type tv struct { valueGen *rapid.Generator[tftypes.Value] } +// Here "tb" stands for a typed block. Like [tv] but for non-nil/unknown blocks. +type tb struct { + schemaMap map[string]*schema.Schema + typ tftypes.Object + valueGen *rapid.Generator[tftypes.Value] +} + type schemaT func(schema.Schema) schema.Schema type tvGen struct { @@ -27,13 +34,14 @@ func (tvg *tvGen) GenTV(maxDepth int) *rapid.Generator[tv] { tvg.GenString(), } if maxDepth > 1 { - opts = append(opts, tvg.GenObjectValue(maxDepth-1)) + opts = append(opts, tvg.GenSingleNestedBlock(maxDepth-1)) } return rapid.OneOf(opts...) } -func (tvg *tvGen) GenObject(maxDepth int) *rapid.Generator[tv] { - return rapid.Custom[tv](func(t *rapid.T) tv { +// Generate a resource or datasource inputs, which are always blocks in TF. +func (tvg *tvGen) GenBlock(maxDepth int) *rapid.Generator[tb] { + return rapid.Custom[tb](func(t *rapid.T) tb { fieldSchemas := map[string]*schema.Schema{} fieldTypes := map[string]tftypes.Type{} fieldGenerators := map[string]*rapid.Generator[tftypes.Value]{} @@ -45,13 +53,6 @@ func (tvg *tvGen) GenObject(maxDepth int) *rapid.Generator[tv] { fieldGenerators[fieldName] = fieldTV.valueGen fieldTypes[fieldName] = fieldTV.typ } - objSchema := schema.Schema{ - Type: schema.TypeMap, - Elem: &schema.Resource{ - Schema: fieldSchemas, - }, - } - st := tvg.GenSchemaTransform().Draw(t, "schemaTransform") objType := tftypes.Object{AttributeTypes: fieldTypes} var objGen *rapid.Generator[tftypes.Value] if len(fieldGenerators) > 0 { @@ -66,15 +67,43 @@ func (tvg *tvGen) GenObject(maxDepth int) *rapid.Generator[tv] { } else { objGen = rapid.Just(tftypes.NewValue(objType, map[string]tftypes.Value{})) } - return tv{st(objSchema), objType, objGen} + // for k, v := range fieldSchemas { + // fmt.Printf("###### field %q %#v\n\n", k, v) + // } + return tb{fieldSchemas, objType, objGen} }) } -// Like [GenObject] but can also return top-level nil or unknown. -func (tvg *tvGen) GenObjectValue(maxDepth int) *rapid.Generator[tv] { - return rapid.Map(tvg.GenObject(maxDepth), func(x tv) tv { - x.valueGen = tvg.withNullAndUnknown(x.typ, x.valueGen) - return x +// Single-nested blocks represent object types. In schemav2 providers there is no natural encoding for these, so they +// are typically encoded as MaxItems=1 lists with a *Resource Elem. +// +// See https://developer.hashicorp.com/terraform/plugin/framework/handling-data/blocks/single-nested +func (tvg *tvGen) GenSingleNestedBlock(maxDepth int) *rapid.Generator[tv] { + return rapid.Custom[tv](func(t *rapid.T) tv { + st := tvg.GenSchemaTransform().Draw(t, "schemaTransform") + bl := tvg.GenBlock(maxDepth).Draw(t, "block") + listWrapType := tftypes.List{ElementType: bl.typ} + listWrap := func(v tftypes.Value) tftypes.Value { + return tftypes.NewValue(listWrapType, []tftypes.Value{v}) + } + return tv{ + schema: st(schema.Schema{ + Type: schema.TypeList, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: bl.schemaMap, + }, + }), + typ: listWrapType, + // A few open questions here, can these values ever be unknown (likely yes) and how is that + // represented in TF? Also, can these values be null or this is just represented as an empty + // list? Should an empty list be part of the values here? + // + // This should also account for required schemas. + // + // valueGen: tvg.withNullAndUnknown(listWrapType, rapid.Map(bl.valueGen, listWrap)), + valueGen: rapid.Map(bl.valueGen, listWrap), + } }) } @@ -82,7 +111,7 @@ func (tvg *tvGen) GenString() *rapid.Generator[tv] { s := schema.Schema{ Type: schema.TypeString, } - nilValue := tftypes.NewValue(tftypes.String, nil) + //nilValue := tftypes.NewValue(tftypes.String, nil) values := []tftypes.Value{ tftypes.NewValue(tftypes.String, ""), tftypes.NewValue(tftypes.String, "text"), @@ -94,26 +123,26 @@ func (tvg *tvGen) GenString() *rapid.Generator[tv] { return rapid.Custom[tv](func(t *rapid.T) tv { st := tvg.GenSchemaTransform().Draw(t, "schemaTransform") s := st(s) - if s.Required { - return tv{s, tftypes.String, valueGen} - } - return tv{s, tftypes.String, rapid.OneOf(valueGen, rapid.Just(nilValue))} + //if s.Required { + return tv{s, tftypes.String, valueGen} + //} + //return tv{s, tftypes.String, rapid.OneOf(valueGen, rapid.Just(nilValue))} }) } -func (tvg *tvGen) withNullAndUnknown( - t tftypes.Type, - v *rapid.Generator[tftypes.Value], -) *rapid.Generator[tftypes.Value] { - nullV := tftypes.NewValue(t, nil) - if tvg.generateUnknowns { - unknownV := tftypes.NewValue(t, tftypes.UnknownValue) - return rapid.OneOf(v, - rapid.Just(nullV), - rapid.Just(unknownV)) - } - return rapid.OneOf(v, rapid.Just(nullV)) -} +// func (tvg *tvGen) withNullAndUnknown( +// t tftypes.Type, +// v *rapid.Generator[tftypes.Value], +// ) *rapid.Generator[tftypes.Value] { +// nullV := tftypes.NewValue(t, nil) +// if tvg.generateUnknowns { +// unknownV := tftypes.NewValue(t, tftypes.UnknownValue) +// return rapid.OneOf(v, +// rapid.Just(nullV), +// rapid.Just(unknownV)) +// } +// return rapid.OneOf(v, rapid.Just(nullV)) +// } func (*tvGen) GenSchemaTransform() *rapid.Generator[schemaT] { return rapid.Custom[schemaT](func(t *rapid.T) schemaT { @@ -128,7 +157,10 @@ func (*tvGen) GenSchemaTransform() *rapid.Generator[schemaT] { case "r": s.Required = true case "c": - s.Computed = true + s.Optional = true + // TODO this currently triggers Value for unconfigurable attribute + // because the provider + // s.Computed = true case "co": s.Computed = true s.Optional = true From 09041dd912d360bf77269e8d7c4bdb466e2f78a8 Mon Sep 17 00:00:00 2001 From: Anton Tayanovskyy Date: Tue, 2 Apr 2024 14:19:38 -0400 Subject: [PATCH 23/45] Misc --- pkg/tests/cross-tests/cross_test.go | 10 +++++----- pkg/tests/cross-tests/rapid_test.go | 2 ++ pkg/tests/go.mod | 12 +++++++++--- pkg/tests/go.sum | 9 ++++++++- 4 files changed, 24 insertions(+), 9 deletions(-) diff --git a/pkg/tests/cross-tests/cross_test.go b/pkg/tests/cross-tests/cross_test.go index ef3b9cc0b..5f8afd537 100644 --- a/pkg/tests/cross-tests/cross_test.go +++ b/pkg/tests/cross-tests/cross_test.go @@ -99,11 +99,11 @@ func runDiffCheck(t T, tc diffTestCase) { ), ) - defer func() { - for _, log := range pt.GrpcLog().Entries { - t.Logf("%v\n req: %s\n res: %s\n", log.Method, log.Request, log.Response) - } - }() + // defer func() { + // for _, log := range pt.GrpcLog().Entries { + // t.Logf("%v\n req: %s\n res: %s\n", log.Method, log.Request, log.Response) + // } + // }() pt.Up() diff --git a/pkg/tests/cross-tests/rapid_test.go b/pkg/tests/cross-tests/rapid_test.go index 367a1ad2f..b52a79b01 100644 --- a/pkg/tests/cross-tests/rapid_test.go +++ b/pkg/tests/cross-tests/rapid_test.go @@ -11,6 +11,8 @@ import ( ) func TestDiffConvergence(outerT *testing.T) { + outerT.Parallel() + log.SetOutput(io.Discard) tvg := &tvGen{} diff --git a/pkg/tests/go.mod b/pkg/tests/go.mod index 95d2a5100..a5cc178dc 100644 --- a/pkg/tests/go.mod +++ b/pkg/tests/go.mod @@ -9,9 +9,11 @@ replace ( require ( github.com/hashicorp/terraform-plugin-sdk/v2 v2.33.0 + github.com/hexops/autogold/v2 v2.2.1 github.com/pulumi/providertest v0.0.11 github.com/pulumi/pulumi-terraform-bridge/v3 v3.80.0 github.com/stretchr/testify v1.8.4 + pgregory.net/rapid v0.6.1 ) require ( @@ -51,6 +53,8 @@ require ( github.com/hashicorp/hil v0.0.0-20190212132231-97b3a9cdfa93 // indirect github.com/hashicorp/terraform-registry-address v0.2.3 // indirect github.com/hashicorp/terraform-svchost v0.1.1 // indirect + github.com/hexops/gotextdiff v1.0.3 // indirect + github.com/hexops/valast v1.4.4 // indirect github.com/huandu/xstrings v1.3.3 // indirect github.com/iancoleman/strcase v0.2.0 // indirect github.com/imdario/mergo v0.3.15 // indirect @@ -63,6 +67,7 @@ require ( github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect github.com/muesli/reflow v0.3.0 // indirect github.com/muesli/termenv v0.15.2 // indirect + github.com/nightlyone/lockfile v1.0.0 // indirect github.com/nxadm/tail v1.4.11 // indirect github.com/pgavlin/fx v0.1.6 // indirect github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect @@ -84,6 +89,7 @@ require ( google.golang.org/genproto/googleapis/api v0.0.0-20231120223509-83a465c0220f // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f // indirect gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect + mvdan.cc/gofumpt v0.5.0 // indirect ) require ( @@ -144,7 +150,7 @@ require ( github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320 // indirect - github.com/hashicorp/go-hclog v1.5.0 // indirect + github.com/hashicorp/go-hclog v1.5.0 github.com/hashicorp/go-immutable-radix v1.3.1 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/go-plugin v1.6.0 @@ -158,7 +164,7 @@ require ( github.com/hashicorp/go-version v1.6.0 // indirect github.com/hashicorp/golang-lru v0.5.4 // indirect github.com/hashicorp/hcl v1.0.0 // indirect - github.com/hashicorp/hcl/v2 v2.19.1 // indirect + github.com/hashicorp/hcl/v2 v2.19.1 github.com/hashicorp/logutils v1.0.0 // indirect github.com/hashicorp/terraform-plugin-go v0.22.0 github.com/hashicorp/terraform-plugin-log v0.9.0 // indirect @@ -217,7 +223,7 @@ require ( github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xeipuuv/gojsonschema v1.2.0 // indirect - github.com/zclconf/go-cty v1.14.2 // indirect + github.com/zclconf/go-cty v1.14.2 go.opencensus.io v0.24.0 // indirect go.uber.org/atomic v1.9.0 // indirect gocloud.dev v0.36.0 // indirect diff --git a/pkg/tests/go.sum b/pkg/tests/go.sum index f42e954a0..1a4e52da9 100644 --- a/pkg/tests/go.sum +++ b/pkg/tests/go.sum @@ -1739,6 +1739,7 @@ github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5Kwzbycv github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= @@ -1756,8 +1757,9 @@ github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2 github.com/frankban/quicktest v1.10.0/go.mod h1:ui7WezCLWMWxVWr1GETZY3smRy0G4KWq9vcPtJmFl7Y= github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k= github.com/frankban/quicktest v1.13.0/go.mod h1:qLE0fzW0VuyUAJgPU19zByoIr0HtCHN/r/VLSOOIySU= -github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps= +github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY= +github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= @@ -2258,6 +2260,7 @@ github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ= github.com/hetznercloud/hcloud-go v1.33.1/go.mod h1:XX/TQub3ge0yWR2yHWmnDVIrB+MQbda1pHxkUmDlUME= github.com/hetznercloud/hcloud-go v1.35.0/go.mod h1:mepQwR6va27S3UQthaEPGS86jtzSY9xWL1e9dyxXpgA= +github.com/hexops/autogold v0.8.1/go.mod h1:97HLDXyG23akzAoRYJh/2OBs3kd80eHyKPvZw0S5ZBY= github.com/hexops/autogold v1.3.0 h1:IEtGNPxBeBu8RMn8eKWh/Ll9dVNgSnJ7bp/qHgMQ14o= github.com/hexops/autogold v1.3.0/go.mod h1:d4hwi2rid66Sag+BVuHgwakW/EmaFr8vdTSbWDbrDRI= github.com/hexops/autogold/v2 v2.2.1 h1:JPUXuZQGkcQMv7eeDXuNMovjfoRYaa0yVcm+F3voaGY= @@ -2460,6 +2463,7 @@ github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Ky github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= @@ -2820,6 +2824,7 @@ github.com/rogpeppe/go-internal v1.2.2/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= github.com/rs/cors v1.8.2/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= @@ -3766,6 +3771,7 @@ golang.org/x/tools v0.8.0/go.mod h1:JxBZ99ISMI5ViVkT1tr6tdNmXeTrcpVSD3vZ1RsRdN4= golang.org/x/tools v0.9.1/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc= golang.org/x/tools v0.9.3/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc= golang.org/x/tools v0.10.0/go.mod h1:UJwyiVBsOA2uwvK/e5OY3GTpDUJriEd+/YlqAwLPmyM= +golang.org/x/tools v0.12.0/go.mod h1:Sc0INKfu04TlqNoRA1hgpFZbhYXHPr4V5DzpSBTPqQM= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.15.0 h1:zdAyfUGbYmuVokhzVmghFl2ZJh5QhcfebBgmVPFYA+8= golang.org/x/tools v0.15.0/go.mod h1:hpksKq4dtpQWS1uQ61JkdqWM3LscIS6Slf+VVkm+wQk= @@ -4343,6 +4349,7 @@ modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= modernc.org/token v1.0.1/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= modernc.org/z v1.5.1/go.mod h1:eWFB510QWW5Th9YGZT81s+LwvaAs3Q2yr4sP0rmLkv8= +mvdan.cc/gofumpt v0.4.0/go.mod h1:PljLOHDeZqgS8opHRKLzp2It2VBuSdteAgqUfzMTxlQ= mvdan.cc/gofumpt v0.5.0 h1:0EQ+Z56k8tXjj/6TQD25BFNKQXpCvT0rnansIc7Ug5E= mvdan.cc/gofumpt v0.5.0/go.mod h1:HBeVDtMKRZpXyxFciAirzdKklDlGu8aAy1wEbH5Y9js= nhooyr.io/websocket v1.8.6/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0= From dea5f25eff6e74fc5e46ba55fddc5cb3d1576934 Mon Sep 17 00:00:00 2001 From: Anton Tayanovskyy Date: Tue, 2 Apr 2024 14:26:31 -0400 Subject: [PATCH 24/45] Simplify diff --- pkg/tfshim/sdk-v2/cty.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/tfshim/sdk-v2/cty.go b/pkg/tfshim/sdk-v2/cty.go index 76013531d..c0fad35cf 100644 --- a/pkg/tfshim/sdk-v2/cty.go +++ b/pkg/tfshim/sdk-v2/cty.go @@ -130,7 +130,7 @@ func recoverCtyValue(dT cty.Type, value interface{}) (cty.Value, error) { case dT.IsListType(): return recoverCtyValueOfListType(dT, value) default: - return cty.NilVal, fmt.Errorf("Cannot reconcile slice %v to %v %v %v", value, dT.GoString(), len(value), dT.IsObjectType()) + return cty.NilVal, fmt.Errorf("Cannot reconcile slice %v to %v", value, dT.GoString()) } default: v, err := recoverScalarCtyValue(dT, value) From f70e5c7a587b94052b018622c0e698009a6bb66c Mon Sep 17 00:00:00 2001 From: Anton Tayanovskyy Date: Tue, 2 Apr 2024 15:19:20 -0400 Subject: [PATCH 25/45] More attr and block generators --- pkg/tests/cross-tests/rapid_tv_gen.go | 302 ++++++++++++++++++++++---- 1 file changed, 254 insertions(+), 48 deletions(-) diff --git a/pkg/tests/cross-tests/rapid_tv_gen.go b/pkg/tests/cross-tests/rapid_tv_gen.go index 1a6f93fc7..9c388c9dd 100644 --- a/pkg/tests/cross-tests/rapid_tv_gen.go +++ b/pkg/tests/cross-tests/rapid_tv_gen.go @@ -5,6 +5,7 @@ import ( "github.com/hashicorp/terraform-plugin-go/tftypes" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/pulumi/pulumi/sdk/v3/go/common/util/contract" "pgregory.net/rapid" ) @@ -25,21 +26,120 @@ type tb struct { type schemaT func(schema.Schema) schema.Schema +type attrKind int + +const ( + optionalAttr attrKind = iota + 1 + requiredAttr + computedAttr + computedOptionalAttr +) + type tvGen struct { generateUnknowns bool } -func (tvg *tvGen) GenTV(maxDepth int) *rapid.Generator[tv] { +func (tvg *tvGen) GenBlockOrAttr(maxDepth int) *rapid.Generator[tv] { + opts := []*rapid.Generator[tv]{ + tvg.GenAttr(maxDepth), + } + if maxDepth > 1 { + opts = append(opts, + tvg.GenSingleNestedBlock(maxDepth-1), + tvg.GenListNestedBlock(maxDepth-1), + tvg.GenSetNestedBlock(maxDepth-1), + ) + } + return rapid.OneOf(opts...) +} + +func (tvg *tvGen) GenAttr(maxDepth int) *rapid.Generator[tv] { opts := []*rapid.Generator[tv]{ tvg.GenString(), + tvg.GenBool(), + tvg.GenInt(), + tvg.GenFloat(), } if maxDepth > 1 { - opts = append(opts, tvg.GenSingleNestedBlock(maxDepth-1)) + opts = append(opts, + tvg.GenMapAttr(maxDepth-1), + tvg.GenListAttr(maxDepth-1), + tvg.GenSetAttr(maxDepth-1), + ) } return rapid.OneOf(opts...) } -// Generate a resource or datasource inputs, which are always blocks in TF. +func (tvg *tvGen) GenMapAttr(maxDepth int) *rapid.Generator[tv] { + ge := rapid.Custom[tv](func(t *rapid.T) tv { + inner := tvg.GenAttr(maxDepth).Draw(t, "attrGen") + mapWrapType := tftypes.Map{ElementType: inner.typ} + mapWrap := func(vs map[string]tftypes.Value) tftypes.Value { + return tftypes.NewValue(mapWrapType, vs) + } + keyGen := rapid.SampledFrom([]string{"a", "b"}) + vg := rapid.Map(rapid.MapOf(keyGen, inner.valueGen), mapWrap) + return tv{ + schema: schema.Schema{ + Type: schema.TypeMap, + Elem: &inner.schema, + }, + typ: mapWrapType, + valueGen: vg, + } + }) + ge = tvg.WithSchemaTransform(ge) + ge = tvg.WithNullAndUnknown(ge) + return ge +} + +func (tvg *tvGen) GenListAttr(maxDepth int) *rapid.Generator[tv] { + ge := rapid.Custom[tv](func(t *rapid.T) tv { + inner := tvg.GenAttr(maxDepth).Draw(t, "attrGen") + listWrapType := tftypes.List{ElementType: inner.typ} + listWrap := func(vs []tftypes.Value) tftypes.Value { + return tftypes.NewValue(listWrapType, vs) + } + vg := rapid.Map(rapid.SliceOfN(inner.valueGen, 0, 3), listWrap) + return tv{ + schema: schema.Schema{ + // TODO get creative with the hash function + Type: schema.TypeList, + Elem: &inner.schema, + }, + typ: listWrapType, + valueGen: vg, + } + }) + ge = tvg.WithSchemaTransform(ge) + ge = tvg.WithNullAndUnknown(ge) + return ge +} + +func (tvg *tvGen) GenSetAttr(maxDepth int) *rapid.Generator[tv] { + ge := rapid.Custom[tv](func(t *rapid.T) tv { + inner := tvg.GenAttr(maxDepth).Draw(t, "attrGen") + setWrapType := tftypes.Set{ElementType: inner.typ} + setWrap := func(vs []tftypes.Value) tftypes.Value { + return tftypes.NewValue(setWrapType, vs) + } + vg := rapid.Map(rapid.SliceOfN(inner.valueGen, 0, 3), setWrap) + return tv{ + schema: schema.Schema{ + // TODO get creative with the hash function + Type: schema.TypeSet, + Elem: &inner.schema, + }, + typ: setWrapType, + valueGen: vg, + } + }) + ge = tvg.WithSchemaTransform(ge) + ge = tvg.WithNullAndUnknown(ge) + return ge +} + +// TF blocks can be resource or datasource inputs, or nested blocks. func (tvg *tvGen) GenBlock(maxDepth int) *rapid.Generator[tb] { return rapid.Custom[tb](func(t *rapid.T) tb { fieldSchemas := map[string]*schema.Schema{} @@ -48,7 +148,7 @@ func (tvg *tvGen) GenBlock(maxDepth int) *rapid.Generator[tb] { nFields := rapid.IntRange(0, 3).Draw(t, "nFields") for i := 0; i < nFields; i++ { fieldName := fmt.Sprintf("f%d", i) - fieldTV := tvg.GenTV(maxDepth-1).Draw(t, fieldName) + fieldTV := tvg.GenBlockOrAttr(maxDepth-1).Draw(t, fieldName) fieldSchemas[fieldName] = &fieldTV.schema fieldGenerators[fieldName] = fieldTV.valueGen fieldTypes[fieldName] = fieldTV.typ @@ -70,6 +170,9 @@ func (tvg *tvGen) GenBlock(maxDepth int) *rapid.Generator[tb] { // for k, v := range fieldSchemas { // fmt.Printf("###### field %q %#v\n\n", k, v) // } + + err := schema.InternalMap(fieldSchemas).InternalValidate(nil) + contract.AssertNoErrorf(err, "rapid_tv_gen generated an invalid schema: please fix") return tb{fieldSchemas, objType, objGen} }) } @@ -107,70 +210,173 @@ func (tvg *tvGen) GenSingleNestedBlock(maxDepth int) *rapid.Generator[tv] { }) } +func (tvg *tvGen) GenListNestedBlock(maxDepth int) *rapid.Generator[tv] { + ge := rapid.Custom[tv](func(t *rapid.T) tv { + bl := tvg.GenBlock(maxDepth).Draw(t, "block") + listWrapType := tftypes.List{ElementType: bl.typ} + listWrap := func(vs []tftypes.Value) tftypes.Value { + return tftypes.NewValue(listWrapType, vs) + } + vg := rapid.Map(rapid.SliceOfN(bl.valueGen, 0, 3), listWrap) + return tv{ + schema: schema.Schema{ + Type: schema.TypeList, + // TODO: randomly trigger MaxItems: 1 + Elem: &schema.Resource{ + Schema: bl.schemaMap, + }, + }, + typ: listWrapType, + valueGen: vg, + } + }) + ge = tvg.WithSchemaTransform(ge) + ge = tvg.WithNullAndUnknown(ge) + return ge +} + +func (tvg *tvGen) GenSetNestedBlock(maxDepth int) *rapid.Generator[tv] { + ge := rapid.Custom[tv](func(t *rapid.T) tv { + bl := tvg.GenBlock(maxDepth).Draw(t, "block") + setWrapType := tftypes.Set{ElementType: bl.typ} + setWrap := func(vs []tftypes.Value) tftypes.Value { + return tftypes.NewValue(setWrapType, vs) + } + vg := rapid.Map(rapid.SliceOfN(bl.valueGen, 0, 3), setWrap) + return tv{ + schema: schema.Schema{ + Type: schema.TypeSet, + // TODO: randomly trigger MaxItems: 1 + // TODO: get a bit inventive with custom hash functions + Elem: &schema.Resource{ + Schema: bl.schemaMap, + }, + }, + typ: setWrapType, + valueGen: vg, + } + }) + ge = tvg.WithSchemaTransform(ge) + ge = tvg.WithNullAndUnknown(ge) + return ge +} + +func (tvg *tvGen) GenAttrKind() *rapid.Generator[attrKind] { + return rapid.SampledFrom([]attrKind{ + optionalAttr, + requiredAttr, + computedAttr, + computedOptionalAttr, + }) +} + func (tvg *tvGen) GenString() *rapid.Generator[tv] { - s := schema.Schema{ - Type: schema.TypeString, - } - //nilValue := tftypes.NewValue(tftypes.String, nil) - values := []tftypes.Value{ + return tvg.GenScalar(schema.TypeString, []tftypes.Value{ tftypes.NewValue(tftypes.String, ""), tftypes.NewValue(tftypes.String, "text"), + }) +} + +func (tvg *tvGen) GenBool() *rapid.Generator[tv] { + return tvg.GenScalar(schema.TypeBool, []tftypes.Value{ + tftypes.NewValue(tftypes.Bool, false), + tftypes.NewValue(tftypes.Bool, true), + }) +} + +func (tvg *tvGen) GenInt() *rapid.Generator[tv] { + return tvg.GenScalar(schema.TypeInt, []tftypes.Value{ + tftypes.NewValue(tftypes.Number, 0), + tftypes.NewValue(tftypes.Number, -1), + tftypes.NewValue(tftypes.Number, 42), + }) +} + +func (tvg *tvGen) GenFloat() *rapid.Generator[tv] { + return tvg.GenScalar(schema.TypeInt, []tftypes.Value{ + tftypes.NewValue(tftypes.Number, float64(0.0)), + tftypes.NewValue(tftypes.Number, float64(-1.0)), + tftypes.NewValue(tftypes.Number, float64(42.0)), + }) +} + +func (tvg *tvGen) GenScalar(vt schema.ValueType, values []tftypes.Value) *rapid.Generator[tv] { + s := schema.Schema{ + Type: vt, } - if tvg.generateUnknowns { - values = append(values, tftypes.NewValue(tftypes.String, tftypes.UnknownValue)) + g := tv{ + schema: s, + typ: values[0].Type(), + valueGen: rapid.SampledFrom(values), } - valueGen := rapid.SampledFrom(values) + gen := tvg.WithSchemaTransform(rapid.Just(g)) + gen = tvg.WithNullAndUnknown(gen) + return gen +} + +func (tvg *tvGen) WithSchemaTransform(gen *rapid.Generator[tv]) *rapid.Generator[tv] { return rapid.Custom[tv](func(t *rapid.T) tv { + tv0 := gen.Draw(t, "tv") st := tvg.GenSchemaTransform().Draw(t, "schemaTransform") - s := st(s) - //if s.Required { - return tv{s, tftypes.String, valueGen} - //} - //return tv{s, tftypes.String, rapid.OneOf(valueGen, rapid.Just(nilValue))} + return tv{ + schema: st(tv0.schema), + typ: tv0.typ, + valueGen: tv0.valueGen, + } + }) +} + +func (tvg *tvGen) WithNullAndUnknown(gen *rapid.Generator[tv]) *rapid.Generator[tv] { + return rapid.Custom[tv](func(t *rapid.T) tv { + tv0 := gen.Draw(t, "tv") + gen := tv0.valueGen + if tvg.generateUnknowns || tv0.schema.Required { + options := []*rapid.Generator[tftypes.Value]{gen} + if tvg.generateUnknowns { + unkGen := rapid.Just(tftypes.NewValue(tv0.typ, tftypes.UnknownValue)) + options = append(options, unkGen) + } + if !tv0.schema.Required { + nullGen := rapid.Just(tftypes.NewValue(tv0.typ, nil)) + options = append(options, nullGen) + } + gen = rapid.OneOf(options...) + } + return tv{ + schema: tv0.schema, + typ: tv0.typ, + valueGen: gen, + } }) } -// func (tvg *tvGen) withNullAndUnknown( -// t tftypes.Type, -// v *rapid.Generator[tftypes.Value], -// ) *rapid.Generator[tftypes.Value] { -// nullV := tftypes.NewValue(t, nil) -// if tvg.generateUnknowns { -// unknownV := tftypes.NewValue(t, tftypes.UnknownValue) -// return rapid.OneOf(v, -// rapid.Just(nullV), -// rapid.Just(unknownV)) -// } -// return rapid.OneOf(v, rapid.Just(nullV)) -// } - -func (*tvGen) GenSchemaTransform() *rapid.Generator[schemaT] { +func (tvg *tvGen) GenSchemaTransform() *rapid.Generator[schemaT] { return rapid.Custom[schemaT](func(t *rapid.T) schemaT { - k := rapid.SampledFrom([]string{"o", "r", "c", "co"}).Draw(t, "optionalKind") - // secret := rapid.Bool().Draw(t, "secret") - // forceNew := rapid.Bool().Draw(t, "forceNew") + attrKind := tvg.GenAttrKind().Draw(t, "attrKind") + secret := rapid.Bool().Draw(t, "secret") + forceNew := rapid.Bool().Draw(t, "forceNew") return func(s schema.Schema) schema.Schema { - switch k { - case "o": + switch attrKind { + case optionalAttr: s.Optional = true - case "r": + case requiredAttr: s.Required = true - case "c": + case computedAttr: + // TODO this currently triggers errors in the tests because the provider needs to be + // taught to polyfill computed values instead of passing them as inputs. s.Optional = true - // TODO this currently triggers Value for unconfigurable attribute - // because the provider // s.Computed = true - case "co": + case computedOptionalAttr: s.Computed = true s.Optional = true } - // if forceNew { - // s.ForceNew = true - // } - // if secret { - // s.Sensitive = true - // } + if forceNew { + s.ForceNew = true + } + if secret { + s.Sensitive = true + } return s } }) From 1f82d12b73b04bdfcd8c77c0e59e611fb7dfb61f Mon Sep 17 00:00:00 2001 From: Anton Tayanovskyy Date: Tue, 9 Apr 2024 17:49:34 -0400 Subject: [PATCH 26/45] Pretty-printing improved --- pkg/tests/cross-tests/pretty.go | 171 +++++++++++++++++++++++++-- pkg/tests/cross-tests/pretty_test.go | 78 ++++++++++++ pkg/tests/cross-tests/rapid_test.go | 5 +- 3 files changed, 240 insertions(+), 14 deletions(-) create mode 100644 pkg/tests/cross-tests/pretty_test.go diff --git a/pkg/tests/cross-tests/pretty.go b/pkg/tests/cross-tests/pretty.go index 1d0b8e52e..6314c6f8f 100644 --- a/pkg/tests/cross-tests/pretty.go +++ b/pkg/tests/cross-tests/pretty.go @@ -3,8 +3,14 @@ package crosstests import ( "bytes" "fmt" + "io" + "math/big" + "slices" + "sort" + "strings" "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/pulumi/pulumi/sdk/v3/go/common/util/contract" ) // Large printouts of tftypes.Value are very difficult to read when debugging the tests, especially because of all the @@ -21,21 +27,164 @@ func (s prettyValueWrapper) Value() tftypes.Value { return s.inner } -// This is not yet valid Go syntax, but when rapid.Draw is used to pull a value it calls GoString and logs the result, -// which is the primary way to interact with the printout, so the code opts to implement this. +// When using rapid.Draw is used to pull a value it calls GoString and logs the result, which is the primary way to +// interact with the printout, so the code opts to implement this. The printed values can be copied to make tests of +// their own that are not rapid-driven. func (s prettyValueWrapper) GoString() string { + tp := newPrettyPrinterForTypes(s.inner) + var buf bytes.Buffer - fmt.Fprintf(&buf, "<<<\n") - tftypes.Walk(s.inner, func(ap *tftypes.AttributePath, v tftypes.Value) (bool, error) { + fmt.Fprintf(&buf, "\n") + + for _, oT := range tp.DeclaredObjectTypes() { + fmt.Fprintf(&buf, "%s := ", tp.TypeLiteral(oT)) + tp.ObjectTypeDefinition(&buf, oT) + fmt.Fprintf(&buf, "\n") + } + + var walk func(level int, v tftypes.Value) + + walk = func(level int, v tftypes.Value) { + tL := tp.TypeReferenceString(v.Type()) + indent := strings.Repeat("\t", level+1) switch { - case v.Type().Is(tftypes.Object{}) || v.Type().Is(tftypes.Set{}) || - v.Type().Is(tftypes.Map{}) || v.Type().Is(tftypes.List{}): - return true, nil + case v.Type().Is(tftypes.Object{}): + fmt.Fprintf(&buf, `tftypes.NewValue(%s, map[string]tftypes.Value{`, tL) + var elements map[string]tftypes.Value + err := v.As(&elements) + contract.AssertNoErrorf(err, "this cast should always succeed") + keys := []string{} + for k := range elements { + keys = append(keys, k) + } + sort.Strings(keys) + for _, k := range keys { + fmt.Fprintf(&buf, "\n%s\t%q: ", indent, k) + walk(level+1, elements[k]) + fmt.Fprintf(&buf, ",") + } + fmt.Fprintf(&buf, "\n%s})", indent) + case v.Type().Is(tftypes.List{}): + fmt.Fprintf(&buf, `tftypes.NewValue(%s, []tftypes.Value{`, tL) + var els []tftypes.Value + err := v.As(&els) + contract.AssertNoErrorf(err, "this cast should always succeed") + for _, el := range els { + fmt.Fprintf(&buf, "\n\t%s", indent) + walk(level+1, el) + fmt.Fprintf(&buf, ",") + } + fmt.Fprintf(&buf, "\n%s})", indent) + case v.Type().Is(tftypes.Number): + var n big.Float + err := v.As(&n) + contract.AssertNoErrorf(err, "this cast should always succeed") + fmt.Fprintf(&buf, "tftypes.NewValue(tftypes.Number, %v)", n.String()) + case v.Type().Is(tftypes.Bool): + var b bool + err := v.As(&b) + contract.AssertNoErrorf(err, "this cast should always succeed") + fmt.Fprintf(&buf, "tftypes.NewValue(tftypes.Bool, %v)", b) default: - fmt.Fprintf(&buf, "%s: %s\n", ap.String(), v.String()) - return true, nil + panic(fmt.Sprintf("not supported yet: %v", v.Type().String())) + } + } + + walk(0, s.inner) + + return buf.String() +} + +type prettyPrinterForTypes struct { + objectTypes []tftypes.Object +} + +func newPrettyPrinterForTypes(v tftypes.Value) prettyPrinterForTypes { + objectTypes := []tftypes.Object{} + + addObjectType := func(t tftypes.Type) { + oT, ok := t.(tftypes.Object) + if !ok { + return + } + for _, alt := range objectTypes { + if alt.Equal(oT) { + return + } } + objectTypes = append(objectTypes, oT) + } + + _ = tftypes.Walk(v, func(ap *tftypes.AttributePath, v tftypes.Value) (bool, error) { + addObjectType(v.Type()) + return true, nil }) - fmt.Fprintf(&buf, ">>>\n") - return buf.String() + ":" + fmt.Sprintf("%#v", s.inner) + + return prettyPrinterForTypes{objectTypes: objectTypes} +} + +func (pp prettyPrinterForTypes) DeclaredObjectTypes() []tftypes.Object { + copy := slices.Clone(pp.objectTypes) + slices.Reverse(copy) + return copy +} + +func (pp prettyPrinterForTypes) TypeLiteral(t tftypes.Object) string { + for i, alt := range pp.objectTypes { + if alt.Equal(t) { + return fmt.Sprintf("t%d", i) + } + } + contract.Failf("improper use of the type pretty-printer") + return "" +} + +func (pp prettyPrinterForTypes) ObjectTypeDefinition(w io.Writer, ty tftypes.Object) { + fmt.Fprintf(w, "tftypes.Object{AttributeTypes: map[string]tftypes.Type{") + keys := []string{} + for k := range ty.AttributeTypes { + keys = append(keys, k) + } + sort.Strings(keys) + for _, k := range keys { + t := ty.AttributeTypes[k] + fmt.Fprintf(w, "\n\t") + fmt.Fprintf(w, "\t%q: ", k) + pp.TypeReference(w, t) + fmt.Fprintf(w, ",") + } + fmt.Fprintf(w, "\n}}") +} + +func (pp prettyPrinterForTypes) TypeReferenceString(t tftypes.Type) string { + var buf bytes.Buffer + pp.TypeReference(&buf, t) + return buf.String() +} + +func (pp prettyPrinterForTypes) TypeReference(w io.Writer, t tftypes.Type) { + switch { + case t.Is(tftypes.Object{}): + fmt.Fprintf(w, pp.TypeLiteral(t.(tftypes.Object))) + case t.Is(tftypes.List{}): + fmt.Fprintf(w, "tftypes.List{ElementType: ") + pp.TypeReference(w, t.(tftypes.List).ElementType) + fmt.Fprintf(w, "}") + case t.Is(tftypes.Set{}): + fmt.Fprintf(w, "tftypes.Set{ElementType: ") + pp.TypeReference(w, t.(tftypes.Set).ElementType) + fmt.Fprintf(w, "}") + case t.Is(tftypes.Map{}): + fmt.Fprintf(w, "tftypes.Map{ElementType: ") + pp.TypeReference(w, t.(tftypes.Map).ElementType) + fmt.Fprintf(w, "}") + case t.Is(tftypes.String): + fmt.Fprintf(w, "tftypes.String") + case t.Is(tftypes.Number): + fmt.Fprintf(w, "tftypes.Number") + case t.Is(tftypes.Bool): + fmt.Fprintf(w, "tftypes.Bool") + default: + contract.Failf("Not supported yet") + } } diff --git a/pkg/tests/cross-tests/pretty_test.go b/pkg/tests/cross-tests/pretty_test.go new file mode 100644 index 000000000..6893d9ee1 --- /dev/null +++ b/pkg/tests/cross-tests/pretty_test.go @@ -0,0 +1,78 @@ +package crosstests + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hexops/autogold/v2" +) + +func TestPrettyPrint(t *testing.T) { + type testCase struct { + v tftypes.Value + e autogold.Value + } + + testCases := []testCase{ + { + tftypes.NewValue(tftypes.Number, 42), + autogold.Expect("\ntftypes.NewValue(tftypes.Number, 42)"), + }, + { + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "f1": tftypes.Bool, + "f0": tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "n": tftypes.Number, + }, + }, + }, + }, + }, map[string]tftypes.Value{ + "f1": tftypes.NewValue(tftypes.Bool, true), + "f0": tftypes.NewValue(tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "n": tftypes.Number, + }, + }, + }, []tftypes.Value{ + tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "n": tftypes.Number, + }, + }, map[string]tftypes.Value{ + "n": tftypes.NewValue(tftypes.Number, 42), + }), + }), + }), + autogold.Expect(` +t1 := tftypes.Object{AttributeTypes: map[string]tftypes.Type{ + "n": tftypes.Number, +}} +t0 := tftypes.Object{AttributeTypes: map[string]tftypes.Type{ + "f0": tftypes.List{ElementType: t1}, + "f1": tftypes.Bool, +}} +tftypes.NewValue(t0, map[string]tftypes.Value{ + "f0": tftypes.NewValue(tftypes.List{ElementType: t1}, []tftypes.Value{ + tftypes.NewValue(t1, map[string]tftypes.Value{ + "n": tftypes.NewValue(tftypes.Number, 42), + }), + }), + "f1": tftypes.NewValue(tftypes.Bool, true), +})`), + }, + } + + for i, tc := range testCases { + tc := tc + t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { + pw := prettyValueWrapper{tc.v} + tc.e.Equal(t, pw.GoString()) + }) + } +} diff --git a/pkg/tests/cross-tests/rapid_test.go b/pkg/tests/cross-tests/rapid_test.go index b52a79b01..c88712b89 100644 --- a/pkg/tests/cross-tests/rapid_test.go +++ b/pkg/tests/cross-tests/rapid_test.go @@ -17,12 +17,11 @@ func TestDiffConvergence(outerT *testing.T) { tvg := &tvGen{} rapid.Check(outerT, func(t *rapid.T) { - outerT.Logf("Iterating..") tv := tvg.GenBlock(3).Draw(t, "tv") - c1 := tv.valueGen.Draw(t, "config1") - c2 := tv.valueGen.Draw(t, "config2") + c1 := rapid.Map(tv.valueGen, newPrettyValueWrapper).Draw(t, "config1").Value() + c2 := rapid.Map(tv.valueGen, newPrettyValueWrapper).Draw(t, "config2").Value() ty := tv.typ tc := diffTestCase{ From f080620caa96e54ffed5413a14f5d1b5b91eaafc Mon Sep 17 00:00:00 2001 From: Anton Tayanovskyy Date: Wed, 10 Apr 2024 12:15:10 -0400 Subject: [PATCH 27/45] WIP --- pkg/tests/cross-tests/pretty.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/tests/cross-tests/pretty.go b/pkg/tests/cross-tests/pretty.go index 6314c6f8f..2295d4034 100644 --- a/pkg/tests/cross-tests/pretty.go +++ b/pkg/tests/cross-tests/pretty.go @@ -135,7 +135,7 @@ func (pp prettyPrinterForTypes) TypeLiteral(t tftypes.Object) string { return fmt.Sprintf("t%d", i) } } - contract.Failf("improper use of the type pretty-printer") + contract.Failf("improper use of the type pretty-printer: %v", t.String()) return "" } From 86050a4ba96141cbe49c05b0e5b0deae99971565 Mon Sep 17 00:00:00 2001 From: Anton Tayanovskyy Date: Thu, 11 Apr 2024 13:37:55 -0400 Subject: [PATCH 28/45] Fix pretty-printing type traversal --- pkg/tests/cross-tests/pretty.go | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/pkg/tests/cross-tests/pretty.go b/pkg/tests/cross-tests/pretty.go index 2295d4034..25491e641 100644 --- a/pkg/tests/cross-tests/pretty.go +++ b/pkg/tests/cross-tests/pretty.go @@ -102,6 +102,29 @@ type prettyPrinterForTypes struct { func newPrettyPrinterForTypes(v tftypes.Value) prettyPrinterForTypes { objectTypes := []tftypes.Object{} + var visitTypes func(t tftypes.Type, vis func(tftypes.Type)) + visitTypes = func(t tftypes.Type, vis func(tftypes.Type)) { + vis(t) + switch { + case t.Is(tftypes.Object{}): + for _, v := range t.(tftypes.Object).AttributeTypes { + visitTypes(v, vis) + } + case t.Is(tftypes.List{}): + visitTypes(t.(tftypes.List).ElementType, vis) + case t.Is(tftypes.Map{}): + visitTypes(t.(tftypes.Map).ElementType, vis) + case t.Is(tftypes.Set{}): + visitTypes(t.(tftypes.Set).ElementType, vis) + case t.Is(tftypes.Tuple{}): + for _, et := range t.(tftypes.Tuple).ElementTypes { + visitTypes(et, vis) + } + default: + return + } + } + addObjectType := func(t tftypes.Type) { oT, ok := t.(tftypes.Object) if !ok { @@ -116,7 +139,7 @@ func newPrettyPrinterForTypes(v tftypes.Value) prettyPrinterForTypes { } _ = tftypes.Walk(v, func(ap *tftypes.AttributePath, v tftypes.Value) (bool, error) { - addObjectType(v.Type()) + visitTypes(v.Type(), addObjectType) return true, nil }) From 52b564a1dd3ddf844688621cf6f90630d49ae841 Mon Sep 17 00:00:00 2001 From: Anton Tayanovskyy Date: Thu, 11 Apr 2024 13:39:12 -0400 Subject: [PATCH 29/45] Do not use tabs in pp --- pkg/tests/cross-tests/pretty.go | 10 +++++----- pkg/tests/cross-tests/pretty_test.go | 20 ++++++++++---------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/pkg/tests/cross-tests/pretty.go b/pkg/tests/cross-tests/pretty.go index 25491e641..d093199dd 100644 --- a/pkg/tests/cross-tests/pretty.go +++ b/pkg/tests/cross-tests/pretty.go @@ -46,7 +46,7 @@ func (s prettyValueWrapper) GoString() string { walk = func(level int, v tftypes.Value) { tL := tp.TypeReferenceString(v.Type()) - indent := strings.Repeat("\t", level+1) + indent := strings.Repeat(". ", level+1) switch { case v.Type().Is(tftypes.Object{}): fmt.Fprintf(&buf, `tftypes.NewValue(%s, map[string]tftypes.Value{`, tL) @@ -59,7 +59,7 @@ func (s prettyValueWrapper) GoString() string { } sort.Strings(keys) for _, k := range keys { - fmt.Fprintf(&buf, "\n%s\t%q: ", indent, k) + fmt.Fprintf(&buf, "\n%s %q: ", indent, k) walk(level+1, elements[k]) fmt.Fprintf(&buf, ",") } @@ -70,7 +70,7 @@ func (s prettyValueWrapper) GoString() string { err := v.As(&els) contract.AssertNoErrorf(err, "this cast should always succeed") for _, el := range els { - fmt.Fprintf(&buf, "\n\t%s", indent) + fmt.Fprintf(&buf, "\n. %s", indent) walk(level+1, el) fmt.Fprintf(&buf, ",") } @@ -171,8 +171,8 @@ func (pp prettyPrinterForTypes) ObjectTypeDefinition(w io.Writer, ty tftypes.Obj sort.Strings(keys) for _, k := range keys { t := ty.AttributeTypes[k] - fmt.Fprintf(w, "\n\t") - fmt.Fprintf(w, "\t%q: ", k) + fmt.Fprintf(w, "\n ") + fmt.Fprintf(w, " %q: ", k) pp.TypeReference(w, t) fmt.Fprintf(w, ",") } diff --git a/pkg/tests/cross-tests/pretty_test.go b/pkg/tests/cross-tests/pretty_test.go index 6893d9ee1..b0d29338e 100644 --- a/pkg/tests/cross-tests/pretty_test.go +++ b/pkg/tests/cross-tests/pretty_test.go @@ -51,20 +51,20 @@ func TestPrettyPrint(t *testing.T) { }), autogold.Expect(` t1 := tftypes.Object{AttributeTypes: map[string]tftypes.Type{ - "n": tftypes.Number, + "n": tftypes.Number, }} t0 := tftypes.Object{AttributeTypes: map[string]tftypes.Type{ - "f0": tftypes.List{ElementType: t1}, - "f1": tftypes.Bool, + "f0": tftypes.List{ElementType: t1}, + "f1": tftypes.Bool, }} tftypes.NewValue(t0, map[string]tftypes.Value{ - "f0": tftypes.NewValue(tftypes.List{ElementType: t1}, []tftypes.Value{ - tftypes.NewValue(t1, map[string]tftypes.Value{ - "n": tftypes.NewValue(tftypes.Number, 42), - }), - }), - "f1": tftypes.NewValue(tftypes.Bool, true), -})`), +. "f0": tftypes.NewValue(tftypes.List{ElementType: t1}, []tftypes.Value{ +. . . tftypes.NewValue(t1, map[string]tftypes.Value{ +. . . "n": tftypes.NewValue(tftypes.Number, 42), +. . . }), +. . }), +. "f1": tftypes.NewValue(tftypes.Bool, true), +. })`), }, } From 61c2d85c8f8ce0dc6e645daa3a5758b17364d4f6 Mon Sep 17 00:00:00 2001 From: Anton Tayanovskyy Date: Thu, 11 Apr 2024 13:46:09 -0400 Subject: [PATCH 30/45] More pretty-printing fixes --- pkg/tests/cross-tests/pretty.go | 9 ++++++--- pkg/tests/cross-tests/pretty_test.go | 20 ++++++++++---------- 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/pkg/tests/cross-tests/pretty.go b/pkg/tests/cross-tests/pretty.go index d093199dd..2049fb06b 100644 --- a/pkg/tests/cross-tests/pretty.go +++ b/pkg/tests/cross-tests/pretty.go @@ -46,7 +46,7 @@ func (s prettyValueWrapper) GoString() string { walk = func(level int, v tftypes.Value) { tL := tp.TypeReferenceString(v.Type()) - indent := strings.Repeat(". ", level+1) + indent := strings.Repeat(" ", level) switch { case v.Type().Is(tftypes.Object{}): fmt.Fprintf(&buf, `tftypes.NewValue(%s, map[string]tftypes.Value{`, tL) @@ -163,6 +163,10 @@ func (pp prettyPrinterForTypes) TypeLiteral(t tftypes.Object) string { } func (pp prettyPrinterForTypes) ObjectTypeDefinition(w io.Writer, ty tftypes.Object) { + if len(ty.AttributeTypes) == 0 { + fmt.Fprintf(w, "tftypes.Object{}") + return + } fmt.Fprintf(w, "tftypes.Object{AttributeTypes: map[string]tftypes.Type{") keys := []string{} for k := range ty.AttributeTypes { @@ -171,8 +175,7 @@ func (pp prettyPrinterForTypes) ObjectTypeDefinition(w io.Writer, ty tftypes.Obj sort.Strings(keys) for _, k := range keys { t := ty.AttributeTypes[k] - fmt.Fprintf(w, "\n ") - fmt.Fprintf(w, " %q: ", k) + fmt.Fprintf(w, "\n %q: ", k) pp.TypeReference(w, t) fmt.Fprintf(w, ",") } diff --git a/pkg/tests/cross-tests/pretty_test.go b/pkg/tests/cross-tests/pretty_test.go index b0d29338e..62623b9a8 100644 --- a/pkg/tests/cross-tests/pretty_test.go +++ b/pkg/tests/cross-tests/pretty_test.go @@ -51,20 +51,20 @@ func TestPrettyPrint(t *testing.T) { }), autogold.Expect(` t1 := tftypes.Object{AttributeTypes: map[string]tftypes.Type{ - "n": tftypes.Number, + "n": tftypes.Number, }} t0 := tftypes.Object{AttributeTypes: map[string]tftypes.Type{ - "f0": tftypes.List{ElementType: t1}, - "f1": tftypes.Bool, + "f0": tftypes.List{ElementType: t1}, + "f1": tftypes.Bool, }} tftypes.NewValue(t0, map[string]tftypes.Value{ -. "f0": tftypes.NewValue(tftypes.List{ElementType: t1}, []tftypes.Value{ -. . . tftypes.NewValue(t1, map[string]tftypes.Value{ -. . . "n": tftypes.NewValue(tftypes.Number, 42), -. . . }), -. . }), -. "f1": tftypes.NewValue(tftypes.Bool, true), -. })`), + "f0": tftypes.NewValue(tftypes.List{ElementType: t1}, []tftypes.Value{ +. tftypes.NewValue(t1, map[string]tftypes.Value{ + "n": tftypes.NewValue(tftypes.Number, 42), + }), + }), + "f1": tftypes.NewValue(tftypes.Bool, true), +})`), }, } From 424be58867af9322e435d22b93f54d6951398ab6 Mon Sep 17 00:00:00 2001 From: Anton Tayanovskyy Date: Thu, 11 Apr 2024 14:43:14 -0400 Subject: [PATCH 31/45] Fix empty list/set/etc adapters --- pkg/tests/cross-tests/adapt.go | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/pkg/tests/cross-tests/adapt.go b/pkg/tests/cross-tests/adapt.go index c5d8ebafd..8fd913b0c 100644 --- a/pkg/tests/cross-tests/adapt.go +++ b/pkg/tests/cross-tests/adapt.go @@ -130,6 +130,9 @@ func (va *valueAdapter) ToCty() cty.Value { var vals []tftypes.Value err := v.As(&vals) contract.AssertNoErrorf(err, "unexpected error converting list") + if len(vals) == 0 { + return cty.ListValEmpty(FromType(t).ToCty()) + } outVals := make([]cty.Value, len(vals)) for i, el := range vals { outVals[i] = FromValue(el).ToCty() @@ -138,6 +141,9 @@ func (va *valueAdapter) ToCty() cty.Value { case t.Is(tftypes.Set{}): var vals []tftypes.Value err := v.As(&vals) + if len(vals) == 0 { + return cty.SetValEmpty(FromType(t).ToCty()) + } contract.AssertNoErrorf(err, "unexpected error converting set") outVals := make([]cty.Value, len(vals)) for i, el := range vals { @@ -147,6 +153,9 @@ func (va *valueAdapter) ToCty() cty.Value { case t.Is(tftypes.Map{}): var vals map[string]tftypes.Value err := v.As(&vals) + if len(vals) == 0 { + return cty.MapValEmpty(FromType(t).ToCty()) + } contract.AssertNoErrorf(err, "unexpected error converting map") outVals := make(map[string]cty.Value, len(vals)) for k, el := range vals { @@ -156,13 +165,15 @@ func (va *valueAdapter) ToCty() cty.Value { case t.Is(tftypes.Object{}): var vals map[string]tftypes.Value err := v.As(&vals) + if len(vals) == 0 { + return cty.EmptyObjectVal + } contract.AssertNoErrorf(err, "unexpected error converting object") outVals := make(map[string]cty.Value, len(vals)) for k, el := range vals { outVals[k] = FromValue(el).ToCty() } return cty.ObjectVal(outVals) - default: contract.Failf("unexpected type %v", t) return cty.NilVal From b3079d68e647de48bc43649d9932b59a357bb66c Mon Sep 17 00:00:00 2001 From: Anton Tayanovskyy Date: Thu, 11 Apr 2024 15:10:59 -0400 Subject: [PATCH 32/45] Pretty-print schemas the quick way --- pkg/tests/cross-tests/pretty.go | 15 +++++++++++++++ pkg/tests/cross-tests/rapid_test.go | 4 ++++ 2 files changed, 19 insertions(+) diff --git a/pkg/tests/cross-tests/pretty.go b/pkg/tests/cross-tests/pretty.go index 2049fb06b..e16689ef6 100644 --- a/pkg/tests/cross-tests/pretty.go +++ b/pkg/tests/cross-tests/pretty.go @@ -10,9 +10,19 @@ import ( "strings" "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hexops/valast" "github.com/pulumi/pulumi/sdk/v3/go/common/util/contract" ) +type prettySchemaWrapper struct { + sch schema.Schema +} + +func (psw prettySchemaWrapper) GoString() string { + return valast.String(psw.sch) +} + // Large printouts of tftypes.Value are very difficult to read when debugging the tests, especially because of all the // extraneous type information printed. This wrapper is a work in progress to implement better pretty-printing. type prettyValueWrapper struct { @@ -85,6 +95,11 @@ func (s prettyValueWrapper) GoString() string { err := v.As(&b) contract.AssertNoErrorf(err, "this cast should always succeed") fmt.Fprintf(&buf, "tftypes.NewValue(tftypes.Bool, %v)", b) + case v.Type().Is(tftypes.String): + var s string + err := v.As(&s) + contract.AssertNoErrorf(err, "this cast should always succeed") + fmt.Fprintf(&buf, "tftypes.NewValue(tftypes.String, %q)", s) default: panic(fmt.Sprintf("not supported yet: %v", v.Type().String())) } diff --git a/pkg/tests/cross-tests/rapid_test.go b/pkg/tests/cross-tests/rapid_test.go index c88712b89..1e48a25ff 100644 --- a/pkg/tests/cross-tests/rapid_test.go +++ b/pkg/tests/cross-tests/rapid_test.go @@ -20,6 +20,10 @@ func TestDiffConvergence(outerT *testing.T) { outerT.Logf("Iterating..") tv := tvg.GenBlock(3).Draw(t, "tv") + t.Logf("Schema:\n%v\n", (&prettySchemaWrapper{schema.Schema{Elem: &schema.Resource{ + Schema: tv.schemaMap, + }}}).GoString()) + c1 := rapid.Map(tv.valueGen, newPrettyValueWrapper).Draw(t, "config1").Value() c2 := rapid.Map(tv.valueGen, newPrettyValueWrapper).Draw(t, "config2").Value() ty := tv.typ From 7d9af8348b7052df24ef6d2deb4e618abf32b11c Mon Sep 17 00:00:00 2001 From: Anton Tayanovskyy Date: Thu, 11 Apr 2024 15:21:03 -0400 Subject: [PATCH 33/45] Eliminate secrets from the tests --- pkg/tests/cross-tests/cross_test.go | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/pkg/tests/cross-tests/cross_test.go b/pkg/tests/cross-tests/cross_test.go index 5f8afd537..08df31994 100644 --- a/pkg/tests/cross-tests/cross_test.go +++ b/pkg/tests/cross-tests/cross_test.go @@ -19,6 +19,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/pulumi/pulumi/sdk/v3/go/auto" "github.com/pulumi/pulumi/sdk/v3/go/common/apitype" + "github.com/pulumi/pulumi/sdk/v3/go/common/resource" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "google.golang.org/grpc" @@ -33,6 +34,7 @@ import ( shim "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfshim" sdkv2 "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfshim/sdk-v2" shimv2 "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfshim/sdk-v2" + "github.com/pulumi/pulumi-terraform-bridge/v3/unstable/propertyvalue" pulumidiag "github.com/pulumi/pulumi/sdk/v3/go/common/diag" "github.com/pulumi/pulumi/sdk/v3/go/common/diag/colors" "github.com/pulumi/pulumi/sdk/v3/go/common/util/contract" @@ -519,13 +521,21 @@ func pulumiWriteYaml(t T, tc diffTestCase, puwd string, tfConfig any) { schema := sdkv2.NewResource(tc.Resource).Schema() pConfig, err := convertConfigToPulumi(schema, nil, tc.ObjectType, tfConfig) require.NoErrorf(t, err, "convertConfigToPulumi failed") + + // Not testing secrets yet, but schema secrets may be set by convertConfigToPulumi. + pConfig = propertyvalue.RemoveSecrets(resource.NewObjectProperty(pConfig)).ObjectValue() + + // This is a bit of a leap of faith that serializing PropertyMap to YAML in this way will yield valid Pulumi + // YAML. This probably needs refinement. + yamlProperties := pConfig.Mappable() + data := map[string]any{ "name": "project", "runtime": "yaml", "resources": map[string]any{ "example": map[string]any{ "type": fmt.Sprintf("%s:index:%s", providerShortName, rtok), - "properties": pConfig, + "properties": yamlProperties, }, }, "backend": map[string]any{ @@ -545,7 +555,7 @@ func convertConfigToPulumi( schemaInfos map[string]*tfbridge.SchemaInfo, objectType *tftypes.Object, tfConfig any, -) (any, error) { +) (resource.PropertyMap, error) { var v *tftypes.Value switch tfConfig := tfConfig.(type) { @@ -589,11 +599,13 @@ func convertConfigToPulumi( if err != nil { return nil, err } + + // There is not yet a way to opt out of marking schema secrets, so the resulting map might have secrets marked. pm, err := convert.DecodePropertyMap(decoder, *v) if err != nil { return nil, err } - return pm.Mappable(), nil + return pm, nil } // Still discovering the structure of JSON-serialized TF plans. The information required from these is, primarily, is From a7b960a83c4ff90cc2f4cd1a4a6a5ac535cd1412 Mon Sep 17 00:00:00 2001 From: Anton Tayanovskyy Date: Thu, 11 Apr 2024 15:55:31 -0400 Subject: [PATCH 34/45] Link the right providertest version --- pkg/tests/cross-tests/t.go | 2 +- pkg/tests/go.mod | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/tests/cross-tests/t.go b/pkg/tests/cross-tests/t.go index 412e31a20..921cf5a9e 100644 --- a/pkg/tests/cross-tests/t.go +++ b/pkg/tests/cross-tests/t.go @@ -11,5 +11,5 @@ type T interface { TempDir() string require.TestingT assert.TestingT - pulumitest.T + pulumitest.PT } diff --git a/pkg/tests/go.mod b/pkg/tests/go.mod index a5cc178dc..0265f4c9e 100644 --- a/pkg/tests/go.mod +++ b/pkg/tests/go.mod @@ -10,7 +10,7 @@ replace ( require ( github.com/hashicorp/terraform-plugin-sdk/v2 v2.33.0 github.com/hexops/autogold/v2 v2.2.1 - github.com/pulumi/providertest v0.0.11 + github.com/pulumi/providertest v0.0.12 github.com/pulumi/pulumi-terraform-bridge/v3 v3.80.0 github.com/stretchr/testify v1.8.4 pgregory.net/rapid v0.6.1 From 19ee5283443f3b03971f7aa008c7ceef0a5d8f24 Mon Sep 17 00:00:00 2001 From: Anton Tayanovskyy Date: Thu, 11 Apr 2024 15:55:49 -0400 Subject: [PATCH 35/45] Pin empty required list panic --- pkg/tests/cross-tests/cross_test.go | 32 +++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/pkg/tests/cross-tests/cross_test.go b/pkg/tests/cross-tests/cross_test.go index 08df31994..211ed15e0 100644 --- a/pkg/tests/cross-tests/cross_test.go +++ b/pkg/tests/cross-tests/cross_test.go @@ -207,6 +207,38 @@ func TestSetReordering(t *testing.T) { }) } +// If a list-nested block has Required set, it cannot be empty. TF emits an error. Pulumi currently panics. +// +// │ Error: Insufficient f0 blocks +// │ +// │ on test.tf line 1, in resource "crossprovider_testres" "example": +// │ 1: resource "crossprovider_testres" "example" { +func TestEmptyRequiredList(t *testing.T) { + t.Skip("TODO - fix panic and make a negative test here") + skipUnlessLinux(t) + resource := &schema.Resource{ + Schema: map[string]*schema.Schema{ + "f0": { + Type: schema.TypeList, + Required: true, + Elem: &schema.Resource{Schema: map[string]*schema.Schema{ + "f0": { + Type: schema.TypeString, + Required: true, + Sensitive: true, + }, + }}, + }, + }, + } + + runDiffCheck(t, diffTestCase{ + Resource: resource, + Config1: map[string]any{"f0": []any{}}, + Config2: map[string]any{"f0": []any{}}, + }) +} + func TestAws2442(t *testing.T) { skipUnlessLinux(t) hashes := map[int]string{} From 65915a787fdc7f68066212947da95b4aa71182ac Mon Sep 17 00:00:00 2001 From: Anton Tayanovskyy Date: Thu, 11 Apr 2024 15:57:24 -0400 Subject: [PATCH 36/45] Do not test in CI yet --- pkg/tests/cross-tests/rapid_test.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pkg/tests/cross-tests/rapid_test.go b/pkg/tests/cross-tests/rapid_test.go index 1e48a25ff..8629d1ca6 100644 --- a/pkg/tests/cross-tests/rapid_test.go +++ b/pkg/tests/cross-tests/rapid_test.go @@ -3,6 +3,7 @@ package crosstests import ( "io" "log" + "os" "testing" "time" @@ -11,6 +12,10 @@ import ( ) func TestDiffConvergence(outerT *testing.T) { + _, ok := os.Getenv("PULUMI_EXPERIMENTAL") + if !ok { + outerT.Skip("TODO - we do not currently pass all cases; using this as an exploration tool") + } outerT.Parallel() log.SetOutput(io.Discard) From 15521f7ad9df21776254aa0f4192347e704b01bc Mon Sep 17 00:00:00 2001 From: Anton Tayanovskyy Date: Thu, 11 Apr 2024 16:01:00 -0400 Subject: [PATCH 37/45] Lint etc --- pkg/tests/cross-tests/adapt.go | 38 ++++++++++++++--------------- pkg/tests/cross-tests/pretty.go | 2 +- pkg/tests/cross-tests/rapid_test.go | 2 +- pkg/tests/cross-tests/tf_driver.go | 5 ++-- pkg/tests/go.mod | 2 +- pkg/tests/go.sum | 4 +-- 6 files changed, 27 insertions(+), 26 deletions(-) diff --git a/pkg/tests/cross-tests/adapt.go b/pkg/tests/cross-tests/adapt.go index 8fd913b0c..be656fd67 100644 --- a/pkg/tests/cross-tests/adapt.go +++ b/pkg/tests/cross-tests/adapt.go @@ -22,15 +22,15 @@ func (ta *typeAdapter) ToCty() cty.Type { case t.Is(tftypes.Bool): return cty.Bool case t.Is(tftypes.List{}): - return cty.List(FromType(t.(tftypes.List).ElementType).ToCty()) + return cty.List(fromType(t.(tftypes.List).ElementType).ToCty()) case t.Is(tftypes.Set{}): - return cty.Set(FromType(t.(tftypes.Set).ElementType).ToCty()) + return cty.Set(fromType(t.(tftypes.Set).ElementType).ToCty()) case t.Is(tftypes.Map{}): - return cty.Map(FromType(t.(tftypes.Map).ElementType).ToCty()) + return cty.Map(fromType(t.(tftypes.Map).ElementType).ToCty()) case t.Is(tftypes.Object{}): fields := map[string]cty.Type{} for k, v := range t.(tftypes.Object).AttributeTypes { - fields[k] = FromType(v).ToCty() + fields[k] = fromType(v).ToCty() } return cty.Object(fields) default: @@ -57,7 +57,7 @@ func (ta *typeAdapter) NewValue(value any) tftypes.Value { case []any: values := []tftypes.Value{} for _, el := range v { - values = append(values, FromType(elT).NewValue(el)) + values = append(values, fromType(elT).NewValue(el)) } return tftypes.NewValue(t, values) } @@ -67,7 +67,7 @@ func (ta *typeAdapter) NewValue(value any) tftypes.Value { case []any: values := []tftypes.Value{} for _, el := range v { - values = append(values, FromType(elT).NewValue(el)) + values = append(values, fromType(elT).NewValue(el)) } return tftypes.NewValue(t, values) } @@ -77,7 +77,7 @@ func (ta *typeAdapter) NewValue(value any) tftypes.Value { case map[string]any: values := map[string]tftypes.Value{} for k, el := range v { - values[k] = FromType(elT).NewValue(el) + values[k] = fromType(elT).NewValue(el) } return tftypes.NewValue(t, values) } @@ -87,7 +87,7 @@ func (ta *typeAdapter) NewValue(value any) tftypes.Value { case map[string]any: values := map[string]tftypes.Value{} for k, el := range v { - values[k] = FromType(aT[k]).NewValue(el) + values[k] = fromType(aT[k]).NewValue(el) } return tftypes.NewValue(t, values) } @@ -95,7 +95,7 @@ func (ta *typeAdapter) NewValue(value any) tftypes.Value { return tftypes.NewValue(t, value) } -func FromType(t tftypes.Type) *typeAdapter { +func fromType(t tftypes.Type) *typeAdapter { return &typeAdapter{t} } @@ -108,9 +108,9 @@ func (va *valueAdapter) ToCty() cty.Value { t := v.Type() switch { case v.IsNull(): - return cty.NullVal(FromType(t).ToCty()) + return cty.NullVal(fromType(t).ToCty()) case !v.IsKnown(): - return cty.UnknownVal(FromType(t).ToCty()) + return cty.UnknownVal(fromType(t).ToCty()) case t.Is(tftypes.String): var s string err := v.As(&s) @@ -131,35 +131,35 @@ func (va *valueAdapter) ToCty() cty.Value { err := v.As(&vals) contract.AssertNoErrorf(err, "unexpected error converting list") if len(vals) == 0 { - return cty.ListValEmpty(FromType(t).ToCty()) + return cty.ListValEmpty(fromType(t).ToCty()) } outVals := make([]cty.Value, len(vals)) for i, el := range vals { - outVals[i] = FromValue(el).ToCty() + outVals[i] = fromValue(el).ToCty() } return cty.ListVal(outVals) case t.Is(tftypes.Set{}): var vals []tftypes.Value err := v.As(&vals) if len(vals) == 0 { - return cty.SetValEmpty(FromType(t).ToCty()) + return cty.SetValEmpty(fromType(t).ToCty()) } contract.AssertNoErrorf(err, "unexpected error converting set") outVals := make([]cty.Value, len(vals)) for i, el := range vals { - outVals[i] = FromValue(el).ToCty() + outVals[i] = fromValue(el).ToCty() } return cty.SetVal(outVals) case t.Is(tftypes.Map{}): var vals map[string]tftypes.Value err := v.As(&vals) if len(vals) == 0 { - return cty.MapValEmpty(FromType(t).ToCty()) + return cty.MapValEmpty(fromType(t).ToCty()) } contract.AssertNoErrorf(err, "unexpected error converting map") outVals := make(map[string]cty.Value, len(vals)) for k, el := range vals { - outVals[k] = FromValue(el).ToCty() + outVals[k] = fromValue(el).ToCty() } return cty.MapVal(outVals) case t.Is(tftypes.Object{}): @@ -171,7 +171,7 @@ func (va *valueAdapter) ToCty() cty.Value { contract.AssertNoErrorf(err, "unexpected error converting object") outVals := make(map[string]cty.Value, len(vals)) for k, el := range vals { - outVals[k] = FromValue(el).ToCty() + outVals[k] = fromValue(el).ToCty() } return cty.ObjectVal(outVals) default: @@ -180,6 +180,6 @@ func (va *valueAdapter) ToCty() cty.Value { } } -func FromValue(v tftypes.Value) *valueAdapter { +func fromValue(v tftypes.Value) *valueAdapter { return &valueAdapter{v} } diff --git a/pkg/tests/cross-tests/pretty.go b/pkg/tests/cross-tests/pretty.go index e16689ef6..5fc62079d 100644 --- a/pkg/tests/cross-tests/pretty.go +++ b/pkg/tests/cross-tests/pretty.go @@ -206,7 +206,7 @@ func (pp prettyPrinterForTypes) TypeReferenceString(t tftypes.Type) string { func (pp prettyPrinterForTypes) TypeReference(w io.Writer, t tftypes.Type) { switch { case t.Is(tftypes.Object{}): - fmt.Fprintf(w, pp.TypeLiteral(t.(tftypes.Object))) + fmt.Fprintf(w, "%s", pp.TypeLiteral(t.(tftypes.Object))) case t.Is(tftypes.List{}): fmt.Fprintf(w, "tftypes.List{ElementType: ") pp.TypeReference(w, t.(tftypes.List).ElementType) diff --git a/pkg/tests/cross-tests/rapid_test.go b/pkg/tests/cross-tests/rapid_test.go index 8629d1ca6..60a601f01 100644 --- a/pkg/tests/cross-tests/rapid_test.go +++ b/pkg/tests/cross-tests/rapid_test.go @@ -12,7 +12,7 @@ import ( ) func TestDiffConvergence(outerT *testing.T) { - _, ok := os.Getenv("PULUMI_EXPERIMENTAL") + _, ok := os.LookupEnv("PULUMI_EXPERIMENTAL") if !ok { outerT.Skip("TODO - we do not currently pass all cases; using this as an exploration tool") } diff --git a/pkg/tests/cross-tests/tf_driver.go b/pkg/tests/cross-tests/tf_driver.go index 927268630..6e1794fc7 100644 --- a/pkg/tests/cross-tests/tf_driver.go +++ b/pkg/tests/cross-tests/tf_driver.go @@ -111,7 +111,7 @@ func (d *tfDriver) coalesce(t T, x any) *tftypes.Value { } objectType := convert.InferObjectType(sdkv2.NewSchemaMap(d.res.Schema), nil) t.Logf("infer object type: %v", objectType) - v := FromType(objectType).NewValue(x) + v := fromType(objectType).NewValue(x) return &v } @@ -137,7 +137,8 @@ func (d *tfDriver) write( config tftypes.Value, ) { var buf bytes.Buffer - err := WriteHCL(&buf, resourceSchema, resourceType, resourceName, FromValue(config).ToCty()) + err := WriteHCL(&buf, resourceSchema, resourceType, resourceName, fromValue(config).ToCty()) + require.NoError(t, err) t.Logf("HCL: \n%s\n", buf.String()) bytes := buf.Bytes() err = os.WriteFile(filepath.Join(d.cwd, "test.tf"), bytes, 0600) diff --git a/pkg/tests/go.mod b/pkg/tests/go.mod index 0265f4c9e..8c2ed9aaf 100644 --- a/pkg/tests/go.mod +++ b/pkg/tests/go.mod @@ -10,6 +10,7 @@ replace ( require ( github.com/hashicorp/terraform-plugin-sdk/v2 v2.33.0 github.com/hexops/autogold/v2 v2.2.1 + github.com/hexops/valast v1.4.4 github.com/pulumi/providertest v0.0.12 github.com/pulumi/pulumi-terraform-bridge/v3 v3.80.0 github.com/stretchr/testify v1.8.4 @@ -54,7 +55,6 @@ require ( github.com/hashicorp/terraform-registry-address v0.2.3 // indirect github.com/hashicorp/terraform-svchost v0.1.1 // indirect github.com/hexops/gotextdiff v1.0.3 // indirect - github.com/hexops/valast v1.4.4 // indirect github.com/huandu/xstrings v1.3.3 // indirect github.com/iancoleman/strcase v0.2.0 // indirect github.com/imdario/mergo v0.3.15 // indirect diff --git a/pkg/tests/go.sum b/pkg/tests/go.sum index 1a4e52da9..f806de677 100644 --- a/pkg/tests/go.sum +++ b/pkg/tests/go.sum @@ -2792,8 +2792,8 @@ github.com/pulumi/appdash v0.0.0-20231130102222-75f619a67231 h1:vkHw5I/plNdTr435 github.com/pulumi/appdash v0.0.0-20231130102222-75f619a67231/go.mod h1:murToZ2N9hNJzewjHBgfFdXhZKjY3z5cYC1VXk+lbFE= github.com/pulumi/esc v0.6.2 h1:+z+l8cuwIauLSwXQS0uoI3rqB+YG4SzsZYtHfNoXBvw= github.com/pulumi/esc v0.6.2/go.mod h1:jNnYNjzsOgVTjCp0LL24NsCk8ZJxq4IoLQdCT0X7l8k= -github.com/pulumi/providertest v0.0.11 h1:mg8MQ7Cq7+9XlHIkBD+aCqQO4mwAJEISngZgVdnQUe8= -github.com/pulumi/providertest v0.0.11/go.mod h1:HsxjVsytcMIuNj19w1lT2W0QXY0oReXl1+h6eD2JXP8= +github.com/pulumi/providertest v0.0.12 h1:UjcFQHHs4AGJyJqxhvC2q8yVQ7Li+UyCyP95HZcK03U= +github.com/pulumi/providertest v0.0.12/go.mod h1:REAoaN+hGOtdWJGirfWYqcSjCejlbGfzyVTUuemJTuE= github.com/pulumi/pulumi-java/pkg v0.10.0 h1:D1i5MiiNrxYr2uJ1szcj1aQwF9DYv7TTsPmajB9dKSw= github.com/pulumi/pulumi-java/pkg v0.10.0/go.mod h1:xu6UgYtQm+xXOo1/DZNa2CWVPytu+RMkZVTtI7w7ffY= github.com/pulumi/pulumi-yaml v1.6.0 h1:mb/QkebWXTa1fR+P3ZkCCHGXOYC6iTN8X8By9eNz8xM= From 488a977e1bfaac2996661537ff214cff75789b3e Mon Sep 17 00:00:00 2001 From: Anton Tayanovskyy Date: Thu, 11 Apr 2024 16:04:46 -0400 Subject: [PATCH 38/45] Docs for pretty.go --- pkg/tests/cross-tests/pretty.go | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/pkg/tests/cross-tests/pretty.go b/pkg/tests/cross-tests/pretty.go index 5fc62079d..2042c0597 100644 --- a/pkg/tests/cross-tests/pretty.go +++ b/pkg/tests/cross-tests/pretty.go @@ -1,3 +1,18 @@ +// 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. + +// Define pretty-printers to make test output easier to interpret. package crosstests import ( @@ -15,6 +30,8 @@ import ( "github.com/pulumi/pulumi/sdk/v3/go/common/util/contract" ) +// Write out schema values using [valast.String]. This may break down once we start testing callbacks, but works for +// simple schemas and makes it easier to read test printout. type prettySchemaWrapper struct { sch schema.Schema } @@ -37,9 +54,6 @@ func (s prettyValueWrapper) Value() tftypes.Value { return s.inner } -// When using rapid.Draw is used to pull a value it calls GoString and logs the result, which is the primary way to -// interact with the printout, so the code opts to implement this. The printed values can be copied to make tests of -// their own that are not rapid-driven. func (s prettyValueWrapper) GoString() string { tp := newPrettyPrinterForTypes(s.inner) @@ -110,6 +124,7 @@ func (s prettyValueWrapper) GoString() string { return buf.String() } +// Assist [prettyValueWrapper] to write out types nicely. type prettyPrinterForTypes struct { objectTypes []tftypes.Object } From ddef975b2526e226c6c69436314dea28708f075e Mon Sep 17 00:00:00 2001 From: Anton Tayanovskyy Date: Thu, 11 Apr 2024 16:06:36 -0400 Subject: [PATCH 39/45] Document adapt.go --- pkg/tests/cross-tests/adapt.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/pkg/tests/cross-tests/adapt.go b/pkg/tests/cross-tests/adapt.go index be656fd67..713afef27 100644 --- a/pkg/tests/cross-tests/adapt.go +++ b/pkg/tests/cross-tests/adapt.go @@ -1,3 +1,19 @@ +// 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. + +// Adapters for converting morally several morally equivalent typed representations of TF values for integrating with +// all the libraries cross-testing is using. package crosstests import ( From 991182e5d4b0d9dbd15db26768d0277dab5d2f52 Mon Sep 17 00:00:00 2001 From: Anton Tayanovskyy Date: Thu, 11 Apr 2024 16:46:01 -0400 Subject: [PATCH 40/45] Cleanup --- pkg/tests/cross-tests/ci.go | 28 +++ pkg/tests/cross-tests/cross_test.go | 314 ++-------------------------- pkg/tests/cross-tests/diff_check.go | 134 ++++++++++++ pkg/tests/cross-tests/pu_driver.go | 186 ++++++++++++++++ pkg/tests/cross-tests/tf_driver.go | 25 +++ 5 files changed, 387 insertions(+), 300 deletions(-) create mode 100644 pkg/tests/cross-tests/ci.go create mode 100644 pkg/tests/cross-tests/diff_check.go create mode 100644 pkg/tests/cross-tests/pu_driver.go diff --git a/pkg/tests/cross-tests/ci.go b/pkg/tests/cross-tests/ci.go new file mode 100644 index 000000000..2565e8cfa --- /dev/null +++ b/pkg/tests/cross-tests/ci.go @@ -0,0 +1,28 @@ +// 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 + +// Helpers to disable failing CI runs. +package crosstests + +import ( + "os" + "runtime" + "strings" + "testing" +) + +func skipUnlessLinux(t *testing.T) { + 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") + } +} diff --git a/pkg/tests/cross-tests/cross_test.go b/pkg/tests/cross-tests/cross_test.go index 211ed15e0..a7f6bae46 100644 --- a/pkg/tests/cross-tests/cross_test.go +++ b/pkg/tests/cross-tests/cross_test.go @@ -1,127 +1,33 @@ +// 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 + +// Various test cases for comparing bridged provider behavior against the equivalent TF provider. package crosstests import ( "bytes" "context" - "encoding/json" "fmt" "hash/crc32" - "io" - "os" - "path/filepath" - "runtime" "slices" "strings" "testing" - "github.com/hashicorp/terraform-plugin-go/tftypes" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - "github.com/pulumi/pulumi/sdk/v3/go/auto" - "github.com/pulumi/pulumi/sdk/v3/go/common/apitype" - "github.com/pulumi/pulumi/sdk/v3/go/common/resource" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "google.golang.org/grpc" - "gopkg.in/yaml.v3" - - "github.com/pulumi/providertest/providers" - "github.com/pulumi/providertest/pulumitest" - "github.com/pulumi/providertest/pulumitest/opttest" - "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/convert" - "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfbridge" - "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfgen" - shim "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfshim" - sdkv2 "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfshim/sdk-v2" - shimv2 "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfshim/sdk-v2" - "github.com/pulumi/pulumi-terraform-bridge/v3/unstable/propertyvalue" - pulumidiag "github.com/pulumi/pulumi/sdk/v3/go/common/diag" - "github.com/pulumi/pulumi/sdk/v3/go/common/diag/colors" - "github.com/pulumi/pulumi/sdk/v3/go/common/util/contract" - "github.com/pulumi/pulumi/sdk/v3/go/common/util/rpcutil" - pulumirpc "github.com/pulumi/pulumi/sdk/v3/proto/go" ) -type diffTestCase struct { - // Schema for the resource to test diffing on. - Resource *schema.Resource - - // Two resource configurations. The representation assumes JSON Configuration Syntax - // accepted by TF, that is, these values when serialized with JSON should parse as .tf.json - // files. If Config1 is nil, assume a Create flow. If Config2 is nil, assume a Delete flow. - // Otherwise assume an Update flow for a resource. - // - // See https://developer.hashicorp.com/terraform/language/syntax/json - // - // This also accepts tftypes.Value encoded data. - Config1, Config2 any - - // Optional object type for the resource. - ObjectType *tftypes.Object - - // Bypass interacting with the bridged Pulumi provider. - SkipPulumi bool -} - -const ( - providerShortName = "crossprovider" - rtype = "crossprovider_testres" - rtok = "TestRes" - rtoken = providerShortName + ":index:" + rtok - providerName = "registry.terraform.io/hashicorp/" + providerShortName - providerVer = "0.0.1" -) - -func runDiffCheck(t T, tc diffTestCase) { - tfwd := t.TempDir() - - tfd := newTfDriver(t, tfwd, providerShortName, rtype, tc.Resource) - _ = tfd.writePlanApply(t, tc.Resource.Schema, rtype, "example", tc.Config1) - tfDiffPlan := tfd.writePlanApply(t, tc.Resource.Schema, rtype, "example", tc.Config2) - - if tc.SkipPulumi { - return - } - - puwd := t.TempDir() - pulumiWriteYaml(t, tc, puwd, tc.Config1) - - pt := pulumitest.NewPulumiTest(t, puwd, - // Needed while using Nix-built pulumi. - opttest.Env("PULUMI_AUTOMATION_API_SKIP_VERSION_CHECK", "true"), - opttest.TestInPlace(), - opttest.SkipInstall(), - opttest.AttachProvider( - providerShortName, - func(ctx context.Context, pt providers.PulumiTest) (providers.Port, error) { - handle, err := startPulumiProvider(ctx, tc) - require.NoError(t, err) - return providers.Port(handle.Port), nil - }, - ), - ) - - // defer func() { - // for _, log := range pt.GrpcLog().Entries { - // t.Logf("%v\n req: %s\n res: %s\n", log.Method, log.Request, log.Response) - // } - // }() - - pt.Up() - - pulumiWriteYaml(t, tc, puwd, tc.Config2) - x := pt.Up() - verifyBasicDiffAgreement(t, *tfDiffPlan, x.Summary) -} - -func toTFProvider(tc diffTestCase) *schema.Provider { - return &schema.Provider{ - ResourcesMap: map[string]*schema.Resource{ - rtype: tc.Resource, - }, - } -} - func TestUnchangedBasicObject(t *testing.T) { skipUnlessLinux(t) cfg := map[string]any{"f0": []any{map[string]any{"x": "ok"}}} @@ -497,195 +403,3 @@ func TestAws2442(t *testing.T) { Config2: cfg2, }) } - -func toPulumiProvider(tc diffTestCase) tfbridge.ProviderInfo { - return tfbridge.ProviderInfo{ - Name: providerShortName, - - P: shimv2.NewProvider(toTFProvider(tc), shimv2.WithPlanResourceChange( - func(tfResourceType string) bool { return true }, - )), - - Resources: map[string]*tfbridge.ResourceInfo{ - rtype: { - Tok: rtoken, - }, - }, - } -} - -func startPulumiProvider( - ctx context.Context, - tc diffTestCase, -) (*rpcutil.ServeHandle, error) { - info := toPulumiProvider(tc) - - sink := pulumidiag.DefaultSink(io.Discard, io.Discard, pulumidiag.FormatOptions{ - Color: colors.Never, - }) - - schema, err := tfgen.GenerateSchema(info, sink) - if err != nil { - return nil, fmt.Errorf("tfgen.GenerateSchema failed: %w", err) - } - - schemaBytes, err := json.MarshalIndent(schema, "", " ") - if err != nil { - return nil, fmt.Errorf("json.MarshalIndent(schema, ..) failed: %w", err) - } - - prov := tfbridge.NewProvider(ctx, nil, providerShortName, providerVer, info.P, info, schemaBytes) - - handle, err := rpcutil.ServeWithOptions(rpcutil.ServeOptions{ - Init: func(srv *grpc.Server) error { - pulumirpc.RegisterResourceProviderServer(srv, prov) - return nil - }, - }) - if err != nil { - return nil, fmt.Errorf("rpcutil.ServeWithOptions failed: %w", err) - } - - return &handle, nil -} - -func pulumiWriteYaml(t T, tc diffTestCase, puwd string, tfConfig any) { - schema := sdkv2.NewResource(tc.Resource).Schema() - pConfig, err := convertConfigToPulumi(schema, nil, tc.ObjectType, tfConfig) - require.NoErrorf(t, err, "convertConfigToPulumi failed") - - // Not testing secrets yet, but schema secrets may be set by convertConfigToPulumi. - pConfig = propertyvalue.RemoveSecrets(resource.NewObjectProperty(pConfig)).ObjectValue() - - // This is a bit of a leap of faith that serializing PropertyMap to YAML in this way will yield valid Pulumi - // YAML. This probably needs refinement. - yamlProperties := pConfig.Mappable() - - data := map[string]any{ - "name": "project", - "runtime": "yaml", - "resources": map[string]any{ - "example": map[string]any{ - "type": fmt.Sprintf("%s:index:%s", providerShortName, rtok), - "properties": yamlProperties, - }, - }, - "backend": map[string]any{ - "url": "file://./data", - }, - } - b, err := yaml.Marshal(data) - require.NoErrorf(t, err, "marshaling Pulumi.yaml") - t.Logf("\n\n%s", b) - p := filepath.Join(puwd, "Pulumi.yaml") - err = os.WriteFile(p, b, 0600) - require.NoErrorf(t, err, "writing Pulumi.yaml") -} - -func convertConfigToPulumi( - schemaMap shim.SchemaMap, - schemaInfos map[string]*tfbridge.SchemaInfo, - objectType *tftypes.Object, - tfConfig any, -) (resource.PropertyMap, error) { - var v *tftypes.Value - - switch tfConfig := tfConfig.(type) { - case tftypes.Value: - v = &tfConfig - if objectType == nil { - ty := v.Type().(tftypes.Object) - objectType = &ty - } - case *tftypes.Value: - v = tfConfig - if objectType == nil { - ty := v.Type().(tftypes.Object) - objectType = &ty - } - default: - if objectType == nil { - t := convert.InferObjectType(schemaMap, nil) - objectType = &t - } - bytes, err := json.Marshal(tfConfig) - if err != nil { - return nil, err - } - // Knowingly using a deprecated function so we can connect back up to tftypes.Value; if this disappears it - // should not be prohibitively difficult to rewrite or vendor. - // - //nolint:staticcheck - value, err := tftypes.ValueFromJSON(bytes, *objectType) - if err != nil { - return nil, err - } - v = &value - } - - decoder, err := convert.NewObjectDecoder(convert.ObjectSchema{ - SchemaMap: schemaMap, - SchemaInfos: schemaInfos, - Object: objectType, - }) - if err != nil { - return nil, err - } - - // There is not yet a way to opt out of marking schema secrets, so the resulting map might have secrets marked. - pm, err := convert.DecodePropertyMap(decoder, *v) - if err != nil { - return nil, err - } - return pm, nil -} - -// Still discovering the structure of JSON-serialized TF plans. The information required from these is, primarily, is -// whether the resource is staying unchanged, being updated or replaced. Secondarily, would be also great to know -// detailed paths of properties causing the change, though that is more difficult to cross-compare with Pulumi. -// -// For now this is code is similar to `jq .resource_changes[0].change.actions[0] plan.json`. -func parseChangesFromTFPlan(plan tfPlan) string { - type p struct { - ResourceChanges []struct { - Change struct { - Actions []string `json:"actions"` - } `json:"change"` - } `json:"resource_changes"` - } - jb, err := json.Marshal(plan.RawPlan) - contract.AssertNoErrorf(err, "failed to marshal terraform plan") - var pp p - err = json.Unmarshal(jb, &pp) - contract.AssertNoErrorf(err, "failed to unmarshal terraform plan") - contract.Assertf(len(pp.ResourceChanges) == 1, "expected exactly one resource change") - actions := pp.ResourceChanges[0].Change.Actions - contract.Assertf(len(actions) == 1, "expected exactly one action, got %v", strings.Join(actions, ", ")) - return actions[0] -} - -func verifyBasicDiffAgreement(t T, plan tfPlan, us auto.UpdateSummary) { - t.Logf("UpdateSummary.ResourceChanges: %#v", us.ResourceChanges) - tfAction := parseChangesFromTFPlan(plan) - switch tfAction { - case "update": - require.NotNilf(t, us.ResourceChanges, "UpdateSummary.ResourceChanges should not be nil") - rc := *us.ResourceChanges - assert.Equalf(t, 1, rc[string(apitype.OpSame)], "expected one resource to stay the same - the stack") - assert.Equalf(t, 1, rc[string(apitype.Update)], "expected the test resource to get an update plan") - assert.Equalf(t, 2, len(rc), "expected two entries in UpdateSummary.ResourceChanges") - case "no-op": - require.NotNilf(t, us.ResourceChanges, "UpdateSummary.ResourceChanges should not be nil") - rc := *us.ResourceChanges - assert.Equalf(t, 2, rc[string(apitype.OpSame)], "expected the test resource and the stack to stay the same") - assert.Equalf(t, 1, len(rc), "expected one entry in UpdateSummary.ResourceChanges") - default: - panic("TODO: do not understand this TF action yet: " + tfAction) - } -} - -func skipUnlessLinux(t *testing.T) { - 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") - } -} diff --git a/pkg/tests/cross-tests/diff_check.go b/pkg/tests/cross-tests/diff_check.go new file mode 100644 index 000000000..34dab0993 --- /dev/null +++ b/pkg/tests/cross-tests/diff_check.go @@ -0,0 +1,134 @@ +// 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 + +// Compares the effect of transitioning between two randomly sampled resource configurations. +package crosstests + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/pulumi/providertest/providers" + "github.com/pulumi/providertest/pulumitest" + "github.com/pulumi/providertest/pulumitest/opttest" + shimv2 "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfshim/sdk-v2" + "github.com/pulumi/pulumi/sdk/v3/go/auto" + "github.com/pulumi/pulumi/sdk/v3/go/common/apitype" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type diffTestCase struct { + // Schema for the resource to test diffing on. + Resource *schema.Resource + + // Two resource configurations. The representation assumes JSON Configuration Syntax + // accepted by TF, that is, these values when serialized with JSON should parse as .tf.json + // files. If Config1 is nil, assume a Create flow. If Config2 is nil, assume a Delete flow. + // Otherwise assume an Update flow for a resource. + // + // See https://developer.hashicorp.com/terraform/language/syntax/json + // + // This also accepts tftypes.Value encoded data. + Config1, Config2 any + + // Optional object type for the resource. + ObjectType *tftypes.Object + + // Bypass interacting with the bridged Pulumi provider. + SkipPulumi bool +} + +func runDiffCheck(t T, tc diffTestCase) { + var ( + providerShortName = "crossprovider" + rtype = "crossprovider_testres" + rtok = "TestRes" + rtoken = providerShortName + ":index:" + rtok + providerVer = "0.0.1" + ) + + tfwd := t.TempDir() + + tfd := newTfDriver(t, tfwd, providerShortName, rtype, tc.Resource) + _ = tfd.writePlanApply(t, tc.Resource.Schema, rtype, "example", tc.Config1) + tfDiffPlan := tfd.writePlanApply(t, tc.Resource.Schema, rtype, "example", tc.Config2) + + if tc.SkipPulumi { + return + } + + tfp := &schema.Provider{ + ResourcesMap: map[string]*schema.Resource{ + rtype: tc.Resource, + }, + } + + shimProvider := shimv2.NewProvider(tfp, shimv2.WithPlanResourceChange( + func(tfResourceType string) bool { return true }, + )) + + pd := &pulumiDriver{ + name: providerShortName, + version: providerVer, + shimProvider: shimProvider, + pulumiResourceToken: rtoken, + tfResourceName: rtype, + objectType: nil, + } + + puwd := t.TempDir() + pd.writeYAML(t, puwd, tc.Config1) + + pt := pulumitest.NewPulumiTest(t, puwd, + opttest.TestInPlace(), + opttest.SkipInstall(), + opttest.AttachProvider( + providerShortName, + func(ctx context.Context, pt providers.PulumiTest) (providers.Port, error) { + handle, err := pd.startPulumiProvider(ctx) + require.NoError(t, err) + return providers.Port(handle.Port), nil + }, + ), + ) + + pt.Up() + + pd.writeYAML(t, puwd, tc.Config2) + x := pt.Up() + + tfAction := tfd.parseChangesFromTFPlan(*tfDiffPlan) + + tc.verifyBasicDiffAgreement(t, tfAction, x.Summary) +} + +func (tc *diffTestCase) verifyBasicDiffAgreement(t T, tfAction string, us auto.UpdateSummary) { + t.Logf("UpdateSummary.ResourceChanges: %#v", us.ResourceChanges) + switch tfAction { + case "update": + require.NotNilf(t, us.ResourceChanges, "UpdateSummary.ResourceChanges should not be nil") + rc := *us.ResourceChanges + assert.Equalf(t, 1, rc[string(apitype.OpSame)], "expected one resource to stay the same - the stack") + assert.Equalf(t, 1, rc[string(apitype.Update)], "expected the test resource to get an update plan") + assert.Equalf(t, 2, len(rc), "expected two entries in UpdateSummary.ResourceChanges") + case "no-op": + require.NotNilf(t, us.ResourceChanges, "UpdateSummary.ResourceChanges should not be nil") + rc := *us.ResourceChanges + assert.Equalf(t, 2, rc[string(apitype.OpSame)], "expected the test resource and stack to stay the same") + assert.Equalf(t, 1, len(rc), "expected one entry in UpdateSummary.ResourceChanges") + default: + panic("TODO: do not understand this TF action yet: " + tfAction) + } +} diff --git a/pkg/tests/cross-tests/pu_driver.go b/pkg/tests/cross-tests/pu_driver.go new file mode 100644 index 000000000..4284703a9 --- /dev/null +++ b/pkg/tests/cross-tests/pu_driver.go @@ -0,0 +1,186 @@ +// 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 + +// Driver code for running tests against an in-process bridged provider under Pulumi CLI. +package crosstests + +import ( + "context" + "encoding/json" + "fmt" + "io" + "os" + "path/filepath" + + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/convert" + "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfbridge" + "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfgen" + shim "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfshim" + "github.com/pulumi/pulumi-terraform-bridge/v3/unstable/propertyvalue" + pulumidiag "github.com/pulumi/pulumi/sdk/v3/go/common/diag" + "github.com/pulumi/pulumi/sdk/v3/go/common/diag/colors" + "github.com/pulumi/pulumi/sdk/v3/go/common/resource" + "github.com/pulumi/pulumi/sdk/v3/go/common/tokens" + "github.com/pulumi/pulumi/sdk/v3/go/common/util/rpcutil" + pulumirpc "github.com/pulumi/pulumi/sdk/v3/proto/go" + "github.com/stretchr/testify/require" + "google.golang.org/grpc" + "gopkg.in/yaml.v3" +) + +type pulumiDriver struct { + name string + version string + shimProvider shim.Provider + pulumiResourceToken string + tfResourceName string + objectType *tftypes.Object +} + +func (pd *pulumiDriver) providerInfo() tfbridge.ProviderInfo { + return tfbridge.ProviderInfo{ + Name: pd.name, + P: pd.shimProvider, + + Resources: map[string]*tfbridge.ResourceInfo{ + pd.tfResourceName: { + Tok: tokens.Type(pd.pulumiResourceToken), + }, + }, + } +} + +func (pd *pulumiDriver) startPulumiProvider(ctx context.Context) (*rpcutil.ServeHandle, error) { + info := pd.providerInfo() + + sink := pulumidiag.DefaultSink(io.Discard, io.Discard, pulumidiag.FormatOptions{ + Color: colors.Never, + }) + + schema, err := tfgen.GenerateSchema(info, sink) + if err != nil { + return nil, fmt.Errorf("tfgen.GenerateSchema failed: %w", err) + } + + schemaBytes, err := json.MarshalIndent(schema, "", " ") + if err != nil { + return nil, fmt.Errorf("json.MarshalIndent(schema, ..) failed: %w", err) + } + + prov := tfbridge.NewProvider(ctx, nil, pd.name, pd.version, info.P, info, schemaBytes) + + handle, err := rpcutil.ServeWithOptions(rpcutil.ServeOptions{ + Init: func(srv *grpc.Server) error { + pulumirpc.RegisterResourceProviderServer(srv, prov) + return nil + }, + }) + if err != nil { + return nil, fmt.Errorf("rpcutil.ServeWithOptions failed: %w", err) + } + + return &handle, nil +} + +func (pd *pulumiDriver) writeYAML(t T, workdir string, tfConfig any) { + res := pd.shimProvider.ResourcesMap().Get(pd.tfResourceName) + schema := res.Schema() + pConfig, err := pd.convertConfigToPulumi(schema, nil, pd.objectType, tfConfig) + require.NoErrorf(t, err, "convertConfigToPulumi failed") + + // Not testing secrets yet, but schema secrets may be set by convertConfigToPulumi. + pConfig = propertyvalue.RemoveSecrets(resource.NewObjectProperty(pConfig)).ObjectValue() + + // This is a bit of a leap of faith that serializing PropertyMap to YAML in this way will yield valid Pulumi + // YAML. This probably needs refinement. + yamlProperties := pConfig.Mappable() + + data := map[string]any{ + "name": "project", + "runtime": "yaml", + "resources": map[string]any{ + "example": map[string]any{ + "type": pd.pulumiResourceToken, + "properties": yamlProperties, + }, + }, + "backend": map[string]any{ + "url": "file://./data", + }, + } + b, err := yaml.Marshal(data) + require.NoErrorf(t, err, "marshaling Pulumi.yaml") + t.Logf("\n\n%s", b) + p := filepath.Join(workdir, "Pulumi.yaml") + err = os.WriteFile(p, b, 0600) + require.NoErrorf(t, err, "writing Pulumi.yaml") +} + +func (pd *pulumiDriver) convertConfigToPulumi( + schemaMap shim.SchemaMap, + schemaInfos map[string]*tfbridge.SchemaInfo, + objectType *tftypes.Object, + tfConfig any, +) (resource.PropertyMap, error) { + var v *tftypes.Value + + switch tfConfig := tfConfig.(type) { + case tftypes.Value: + v = &tfConfig + if objectType == nil { + ty := v.Type().(tftypes.Object) + objectType = &ty + } + case *tftypes.Value: + v = tfConfig + if objectType == nil { + ty := v.Type().(tftypes.Object) + objectType = &ty + } + default: + if objectType == nil { + t := convert.InferObjectType(schemaMap, nil) + objectType = &t + } + bytes, err := json.Marshal(tfConfig) + if err != nil { + return nil, err + } + // Knowingly using a deprecated function so we can connect back up to tftypes.Value; if this disappears + // it should not be prohibitively difficult to rewrite or vendor. + // + //nolint:staticcheck + value, err := tftypes.ValueFromJSON(bytes, *objectType) + if err != nil { + return nil, err + } + v = &value + } + + decoder, err := convert.NewObjectDecoder(convert.ObjectSchema{ + SchemaMap: schemaMap, + SchemaInfos: schemaInfos, + Object: objectType, + }) + if err != nil { + return nil, err + } + + // There is not yet a way to opt out of marking schema secrets, so the resulting map might have secrets marked. + pm, err := convert.DecodePropertyMap(decoder, *v) + if err != nil { + return nil, err + } + return pm, nil +} diff --git a/pkg/tests/cross-tests/tf_driver.go b/pkg/tests/cross-tests/tf_driver.go index 6e1794fc7..e893db370 100644 --- a/pkg/tests/cross-tests/tf_driver.go +++ b/pkg/tests/cross-tests/tf_driver.go @@ -9,6 +9,7 @@ import ( "log" "os" "path/filepath" + "strings" "github.com/hashicorp/go-hclog" "github.com/hashicorp/go-plugin" @@ -194,3 +195,27 @@ func (d *tfDriver) formatReattachEnvVar() string { contract.AssertNoErrorf(err, "failed to build TF_REATTACH_PROVIDERS string") return fmt.Sprintf("TF_REATTACH_PROVIDERS=%s", string(reattachBytes)) } + +// Still discovering the structure of JSON-serialized TF plans. The information required from these is, primarily, is +// whether the resource is staying unchanged, being updated or replaced. Secondarily, would be also great to know +// detailed paths of properties causing the change, though that is more difficult to cross-compare with Pulumi. +// +// For now this is code is similar to `jq .resource_changes[0].change.actions[0] plan.json`. +func (*tfDriver) parseChangesFromTFPlan(plan tfPlan) string { + type p struct { + ResourceChanges []struct { + Change struct { + Actions []string `json:"actions"` + } `json:"change"` + } `json:"resource_changes"` + } + jb, err := json.Marshal(plan.RawPlan) + contract.AssertNoErrorf(err, "failed to marshal terraform plan") + var pp p + err = json.Unmarshal(jb, &pp) + contract.AssertNoErrorf(err, "failed to unmarshal terraform plan") + contract.Assertf(len(pp.ResourceChanges) == 1, "expected exactly one resource change") + actions := pp.ResourceChanges[0].Change.Actions + contract.Assertf(len(actions) == 1, "expected exactly one action, got %v", strings.Join(actions, ", ")) + return actions[0] +} From 37b22a971b03d80dc6765fa7fe5a40dbc6f554e4 Mon Sep 17 00:00:00 2001 From: Anton Tayanovskyy Date: Thu, 11 Apr 2024 16:50:19 -0400 Subject: [PATCH 41/45] Copyright --- pkg/tests/cross-tests/exec.go | 14 ++++++++++++++ pkg/tests/cross-tests/pretty_test.go | 14 ++++++++++++++ pkg/tests/cross-tests/rapid_test.go | 16 ++++++++++++++++ pkg/tests/cross-tests/rapid_tv_gen.go | 15 +++++++++++++++ pkg/tests/cross-tests/t.go | 15 +++++++++++++++ pkg/tests/cross-tests/tf_driver.go | 15 +++++++++++++++ pkg/tests/cross-tests/tfwrite.go | 15 +++++++++++++++ pkg/tests/cross-tests/tfwrite_test.go | 14 ++++++++++++++ 8 files changed, 118 insertions(+) diff --git a/pkg/tests/cross-tests/exec.go b/pkg/tests/cross-tests/exec.go index 9d1fd9465..15af65b44 100644 --- a/pkg/tests/cross-tests/exec.go +++ b/pkg/tests/cross-tests/exec.go @@ -1,3 +1,17 @@ +// 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 + +// Helpers to execute OS commands. package crosstests import ( diff --git a/pkg/tests/cross-tests/pretty_test.go b/pkg/tests/cross-tests/pretty_test.go index 62623b9a8..5662b643f 100644 --- a/pkg/tests/cross-tests/pretty_test.go +++ b/pkg/tests/cross-tests/pretty_test.go @@ -1,3 +1,17 @@ +// 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 crosstests import ( diff --git a/pkg/tests/cross-tests/rapid_test.go b/pkg/tests/cross-tests/rapid_test.go index 60a601f01..7ca02c457 100644 --- a/pkg/tests/cross-tests/rapid_test.go +++ b/pkg/tests/cross-tests/rapid_test.go @@ -1,3 +1,19 @@ +// 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. + +// Rapid-driven property-based tests. These allow randomized exploration of the schema space and locating +// counter-examples. package crosstests import ( diff --git a/pkg/tests/cross-tests/rapid_tv_gen.go b/pkg/tests/cross-tests/rapid_tv_gen.go index 9c388c9dd..76e75baa6 100644 --- a/pkg/tests/cross-tests/rapid_tv_gen.go +++ b/pkg/tests/cross-tests/rapid_tv_gen.go @@ -1,3 +1,18 @@ +// 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. + +// Rapid generators for schema and value spaces. package crosstests import ( diff --git a/pkg/tests/cross-tests/t.go b/pkg/tests/cross-tests/t.go index 921cf5a9e..92dacbcaf 100644 --- a/pkg/tests/cross-tests/t.go +++ b/pkg/tests/cross-tests/t.go @@ -1,3 +1,18 @@ +// 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. + +// Abstractions to allow tests to work against both *testing.T and rapid.TB. package crosstests import ( diff --git a/pkg/tests/cross-tests/tf_driver.go b/pkg/tests/cross-tests/tf_driver.go index e893db370..ba33b7913 100644 --- a/pkg/tests/cross-tests/tf_driver.go +++ b/pkg/tests/cross-tests/tf_driver.go @@ -1,3 +1,18 @@ +// 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. + +// Helper code to drive Terraform CLI to run tests against an in-process provider. package crosstests import ( diff --git a/pkg/tests/cross-tests/tfwrite.go b/pkg/tests/cross-tests/tfwrite.go index 0d84ff70a..e1e324d01 100644 --- a/pkg/tests/cross-tests/tfwrite.go +++ b/pkg/tests/cross-tests/tfwrite.go @@ -1,3 +1,18 @@ +// 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. + +// Helper code to emit Terraform HCL code to drive the Terraform CLI. package crosstests import ( diff --git a/pkg/tests/cross-tests/tfwrite_test.go b/pkg/tests/cross-tests/tfwrite_test.go index 52fc0e78f..e433c4922 100644 --- a/pkg/tests/cross-tests/tfwrite_test.go +++ b/pkg/tests/cross-tests/tfwrite_test.go @@ -1,3 +1,17 @@ +// 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 crosstests import ( From 525c31a169866cf8ee0f7ce7151786be72102aa9 Mon Sep 17 00:00:00 2001 From: Anton Tayanovskyy Date: Thu, 11 Apr 2024 16:51:16 -0400 Subject: [PATCH 42/45] Grammar --- pkg/tests/cross-tests/adapt.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/tests/cross-tests/adapt.go b/pkg/tests/cross-tests/adapt.go index 713afef27..e7ca74ded 100644 --- a/pkg/tests/cross-tests/adapt.go +++ b/pkg/tests/cross-tests/adapt.go @@ -12,8 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -// Adapters for converting morally several morally equivalent typed representations of TF values for integrating with -// all the libraries cross-testing is using. +// Adapters for converting morally equivalent typed representations of TF values for integrating with all the libraries +// cross-testing is using. package crosstests import ( From 4624b952515f9c0b505c2f1722b7a6a0d7a37348 Mon Sep 17 00:00:00 2001 From: Anton Tayanovskyy Date: Fri, 12 Apr 2024 14:26:34 -0400 Subject: [PATCH 43/45] PR Feedback --- pkg/tests/cross-tests/diff_check.go | 20 ++++++-------------- pkg/tests/cross-tests/pu_driver.go | 2 +- pkg/tests/cross-tests/rapid_tv_gen.go | 6 +----- pkg/tests/cross-tests/tf_driver.go | 6 ------ pkg/tests/cross-tests/tfwrite.go | 3 ++- pkg/tfshim/sdk-v2/cty.go | 4 ++-- 6 files changed, 12 insertions(+), 29 deletions(-) diff --git a/pkg/tests/cross-tests/diff_check.go b/pkg/tests/cross-tests/diff_check.go index 34dab0993..4adfd8ef3 100644 --- a/pkg/tests/cross-tests/diff_check.go +++ b/pkg/tests/cross-tests/diff_check.go @@ -33,21 +33,17 @@ type diffTestCase struct { // Schema for the resource to test diffing on. Resource *schema.Resource - // Two resource configurations. The representation assumes JSON Configuration Syntax - // accepted by TF, that is, these values when serialized with JSON should parse as .tf.json - // files. If Config1 is nil, assume a Create flow. If Config2 is nil, assume a Delete flow. - // Otherwise assume an Update flow for a resource. + // Two resource configurations to simulate an Update from the desired state of Config1 to Config2. // - // See https://developer.hashicorp.com/terraform/language/syntax/json + // Currently they need to be non-nil, but it would make sense to also test Create and Delete flows, especially + // Create, since there is the non-obvious detail that TF still takes Create calls through the diff logic code + // including diff customization and PlanResource change. // - // This also accepts tftypes.Value encoded data. + // Prefer passing [tftypes.Value] representations. Config1, Config2 any - // Optional object type for the resource. + // Optional object type for the resource. If left nil will be inferred from Resource schema. ObjectType *tftypes.Object - - // Bypass interacting with the bridged Pulumi provider. - SkipPulumi bool } func runDiffCheck(t T, tc diffTestCase) { @@ -65,10 +61,6 @@ func runDiffCheck(t T, tc diffTestCase) { _ = tfd.writePlanApply(t, tc.Resource.Schema, rtype, "example", tc.Config1) tfDiffPlan := tfd.writePlanApply(t, tc.Resource.Schema, rtype, "example", tc.Config2) - if tc.SkipPulumi { - return - } - tfp := &schema.Provider{ ResourcesMap: map[string]*schema.Resource{ rtype: tc.Resource, diff --git a/pkg/tests/cross-tests/pu_driver.go b/pkg/tests/cross-tests/pu_driver.go index 4284703a9..23ef4b80d 100644 --- a/pkg/tests/cross-tests/pu_driver.go +++ b/pkg/tests/cross-tests/pu_driver.go @@ -99,7 +99,7 @@ func (pd *pulumiDriver) writeYAML(t T, workdir string, tfConfig any) { pConfig, err := pd.convertConfigToPulumi(schema, nil, pd.objectType, tfConfig) require.NoErrorf(t, err, "convertConfigToPulumi failed") - // Not testing secrets yet, but schema secrets may be set by convertConfigToPulumi. + // TODO[pulumi/pulumi-terraform-bridge#1864]: schema secrets may be set by convertConfigToPulumi. pConfig = propertyvalue.RemoveSecrets(resource.NewObjectProperty(pConfig)).ObjectValue() // This is a bit of a leap of faith that serializing PropertyMap to YAML in this way will yield valid Pulumi diff --git a/pkg/tests/cross-tests/rapid_tv_gen.go b/pkg/tests/cross-tests/rapid_tv_gen.go index 76e75baa6..c85d94e9d 100644 --- a/pkg/tests/cross-tests/rapid_tv_gen.go +++ b/pkg/tests/cross-tests/rapid_tv_gen.go @@ -141,7 +141,7 @@ func (tvg *tvGen) GenSetAttr(maxDepth int) *rapid.Generator[tv] { vg := rapid.Map(rapid.SliceOfN(inner.valueGen, 0, 3), setWrap) return tv{ schema: schema.Schema{ - // TODO get creative with the hash function + // TODO[pulumi/pulumi-terraform-bridge#1862 alternative hash functions Type: schema.TypeSet, Elem: &inner.schema, }, @@ -182,10 +182,6 @@ func (tvg *tvGen) GenBlock(maxDepth int) *rapid.Generator[tb] { } else { objGen = rapid.Just(tftypes.NewValue(objType, map[string]tftypes.Value{})) } - // for k, v := range fieldSchemas { - // fmt.Printf("###### field %q %#v\n\n", k, v) - // } - err := schema.InternalMap(fieldSchemas).InternalValidate(nil) contract.AssertNoErrorf(err, "rapid_tv_gen generated an invalid schema: please fix") return tb{fieldSchemas, objType, objGen} diff --git a/pkg/tests/cross-tests/tf_driver.go b/pkg/tests/cross-tests/tf_driver.go index ba33b7913..1d5c9eb57 100644 --- a/pkg/tests/cross-tests/tf_driver.go +++ b/pkg/tests/cross-tests/tf_driver.go @@ -57,12 +57,6 @@ func newTfDriver(t T, dir, providerName, resName string, res *schema.Resource) * os.Setenv("TF_LOG_SDK", "off") os.Setenv("TF_LOG_SDK_PROTO", "off") - // res.CustomizeDiff = func( - // ctx context.Context, rd *schema.ResourceDiff, i interface{}, - // ) error { - // return nil - // } - if res.DeleteContext == nil { res.DeleteContext = func( ctx context.Context, rd *schema.ResourceData, i interface{}, diff --git a/pkg/tests/cross-tests/tfwrite.go b/pkg/tests/cross-tests/tfwrite.go index e1e324d01..28871402f 100644 --- a/pkg/tests/cross-tests/tfwrite.go +++ b/pkg/tests/cross-tests/tfwrite.go @@ -50,8 +50,9 @@ func writeBlock(body *hclwrite.Body, schemas map[string]*schema.Schema, values m if !ok { continue } + contract.Assertf(sch.ConfigMode != 0, "ConfigMode != nil is not yet supported") switch elem := sch.Elem.(type) { - case *schema.Resource: // TODO sch.ConfigMode + case *schema.Resource: if sch.Type == schema.TypeMap { body.SetAttributeValue(key, value) } else if sch.Type == schema.TypeSet { diff --git a/pkg/tfshim/sdk-v2/cty.go b/pkg/tfshim/sdk-v2/cty.go index c0fad35cf..1338d516a 100644 --- a/pkg/tfshim/sdk-v2/cty.go +++ b/pkg/tfshim/sdk-v2/cty.go @@ -119,7 +119,7 @@ func recoverCtyValue(dT cty.Type, value interface{}) (cty.Value, error) { case dT.IsObjectType(): return recoverCtyValueOfObjectType(dT, value) default: - return cty.NilVal, fmt.Errorf("Cannot reconcile map %v to %v", value, dT.GoString()) + return cty.NilVal, fmt.Errorf("Cannot reconcile map %v to %#v", value, dT) } case []interface{}: switch { @@ -130,7 +130,7 @@ func recoverCtyValue(dT cty.Type, value interface{}) (cty.Value, error) { case dT.IsListType(): return recoverCtyValueOfListType(dT, value) default: - return cty.NilVal, fmt.Errorf("Cannot reconcile slice %v to %v", value, dT.GoString()) + return cty.NilVal, fmt.Errorf("Cannot reconcile slice %v to %#v", value, dT) } default: v, err := recoverScalarCtyValue(dT, value) From 8571b80e96746eb34a1490290451481b58f7eccc Mon Sep 17 00:00:00 2001 From: Anton Tayanovskyy Date: Fri, 12 Apr 2024 14:43:44 -0400 Subject: [PATCH 44/45] More PR feedback --- pkg/tests/cross-tests/rapid_test.go | 2 +- pkg/tests/cross-tests/rapid_tv_gen.go | 75 +++++++++++++++++---------- 2 files changed, 50 insertions(+), 27 deletions(-) diff --git a/pkg/tests/cross-tests/rapid_test.go b/pkg/tests/cross-tests/rapid_test.go index 7ca02c457..fcb729425 100644 --- a/pkg/tests/cross-tests/rapid_test.go +++ b/pkg/tests/cross-tests/rapid_test.go @@ -39,7 +39,7 @@ func TestDiffConvergence(outerT *testing.T) { rapid.Check(outerT, func(t *rapid.T) { outerT.Logf("Iterating..") - tv := tvg.GenBlock(3).Draw(t, "tv") + tv := tvg.GenBlockWithDepth(3).Draw(t, "tv") t.Logf("Schema:\n%v\n", (&prettySchemaWrapper{schema.Schema{Elem: &schema.Resource{ Schema: tv.schemaMap, diff --git a/pkg/tests/cross-tests/rapid_tv_gen.go b/pkg/tests/cross-tests/rapid_tv_gen.go index c85d94e9d..80b6919b7 100644 --- a/pkg/tests/cross-tests/rapid_tv_gen.go +++ b/pkg/tests/cross-tests/rapid_tv_gen.go @@ -44,7 +44,8 @@ type schemaT func(schema.Schema) schema.Schema type attrKind int const ( - optionalAttr attrKind = iota + 1 + invalidAttr attrKind = iota + optionalAttr requiredAttr computedAttr computedOptionalAttr @@ -54,40 +55,62 @@ type tvGen struct { generateUnknowns bool } -func (tvg *tvGen) GenBlockOrAttr(maxDepth int) *rapid.Generator[tv] { +func (tvg *tvGen) GenBlock() *rapid.Generator[tv] { + return rapid.Custom[tv](func(t *rapid.T) tv { + depth := rapid.IntRange(0, 3).Draw(t, "depth") + tv := tvg.GenBlockOrAttrWithDepth(depth).Draw(t, "tv") + return tv + }) +} + +func (tvg *tvGen) GenAttr() *rapid.Generator[tv] { + return rapid.Custom[tv](func(t *rapid.T) tv { + depth := rapid.IntRange(0, 3).Draw(t, "depth") + tv := tvg.GenAttrWithDepth(depth).Draw(t, "tv") + return tv + }) +} + +func (tvg *tvGen) GenBlockOrAttrWithDepth(depth int) *rapid.Generator[tv] { opts := []*rapid.Generator[tv]{ - tvg.GenAttr(maxDepth), + tvg.GenAttrWithDepth(depth), } - if maxDepth > 1 { + if depth > 1 { opts = append(opts, - tvg.GenSingleNestedBlock(maxDepth-1), - tvg.GenListNestedBlock(maxDepth-1), - tvg.GenSetNestedBlock(maxDepth-1), + tvg.GenSingleNestedBlock(depth-1), + tvg.GenListNestedBlock(depth-1), + tvg.GenSetNestedBlock(depth-1), ) } return rapid.OneOf(opts...) } -func (tvg *tvGen) GenAttr(maxDepth int) *rapid.Generator[tv] { +func (tvg *tvGen) GenAttrWithDepth(depth int) *rapid.Generator[tv] { opts := []*rapid.Generator[tv]{ tvg.GenString(), tvg.GenBool(), tvg.GenInt(), tvg.GenFloat(), } - if maxDepth > 1 { + if depth > 1 { opts = append(opts, - tvg.GenMapAttr(maxDepth-1), - tvg.GenListAttr(maxDepth-1), - tvg.GenSetAttr(maxDepth-1), + tvg.GenMapAttr(depth-1), + tvg.GenListAttr(depth-1), + tvg.GenSetAttr(depth-1), ) } return rapid.OneOf(opts...) } -func (tvg *tvGen) GenMapAttr(maxDepth int) *rapid.Generator[tv] { +func (tvg *tvGen) GenMapAttr(depth int) *rapid.Generator[tv] { ge := rapid.Custom[tv](func(t *rapid.T) tv { - inner := tvg.GenAttr(maxDepth).Draw(t, "attrGen") + // Only generating primitive types for the value of a Map. This is due to this limitation: + // https://github.com/hashicorp/terraform-plugin-sdk/issues/62 + // + // Ideally per pulumi/pulumi-terraform-bridge#1873 bridged providers should reject these at build time + // so the runtime tests should not concern themselves with these unsupported combinations. + zeroDepth := 0 + inner := tvg.GenAttrWithDepth(zeroDepth).Draw(t, "attrGen") mapWrapType := tftypes.Map{ElementType: inner.typ} mapWrap := func(vs map[string]tftypes.Value) tftypes.Value { return tftypes.NewValue(mapWrapType, vs) @@ -108,9 +131,9 @@ func (tvg *tvGen) GenMapAttr(maxDepth int) *rapid.Generator[tv] { return ge } -func (tvg *tvGen) GenListAttr(maxDepth int) *rapid.Generator[tv] { +func (tvg *tvGen) GenListAttr(depth int) *rapid.Generator[tv] { ge := rapid.Custom[tv](func(t *rapid.T) tv { - inner := tvg.GenAttr(maxDepth).Draw(t, "attrGen") + inner := tvg.GenAttrWithDepth(depth).Draw(t, "attrGen") listWrapType := tftypes.List{ElementType: inner.typ} listWrap := func(vs []tftypes.Value) tftypes.Value { return tftypes.NewValue(listWrapType, vs) @@ -131,9 +154,9 @@ func (tvg *tvGen) GenListAttr(maxDepth int) *rapid.Generator[tv] { return ge } -func (tvg *tvGen) GenSetAttr(maxDepth int) *rapid.Generator[tv] { +func (tvg *tvGen) GenSetAttr(depth int) *rapid.Generator[tv] { ge := rapid.Custom[tv](func(t *rapid.T) tv { - inner := tvg.GenAttr(maxDepth).Draw(t, "attrGen") + inner := tvg.GenAttrWithDepth(depth).Draw(t, "attrGen") setWrapType := tftypes.Set{ElementType: inner.typ} setWrap := func(vs []tftypes.Value) tftypes.Value { return tftypes.NewValue(setWrapType, vs) @@ -155,7 +178,7 @@ func (tvg *tvGen) GenSetAttr(maxDepth int) *rapid.Generator[tv] { } // TF blocks can be resource or datasource inputs, or nested blocks. -func (tvg *tvGen) GenBlock(maxDepth int) *rapid.Generator[tb] { +func (tvg *tvGen) GenBlockWithDepth(depth int) *rapid.Generator[tb] { return rapid.Custom[tb](func(t *rapid.T) tb { fieldSchemas := map[string]*schema.Schema{} fieldTypes := map[string]tftypes.Type{} @@ -163,7 +186,7 @@ func (tvg *tvGen) GenBlock(maxDepth int) *rapid.Generator[tb] { nFields := rapid.IntRange(0, 3).Draw(t, "nFields") for i := 0; i < nFields; i++ { fieldName := fmt.Sprintf("f%d", i) - fieldTV := tvg.GenBlockOrAttr(maxDepth-1).Draw(t, fieldName) + fieldTV := tvg.GenBlockOrAttrWithDepth(depth-1).Draw(t, fieldName) fieldSchemas[fieldName] = &fieldTV.schema fieldGenerators[fieldName] = fieldTV.valueGen fieldTypes[fieldName] = fieldTV.typ @@ -192,10 +215,10 @@ func (tvg *tvGen) GenBlock(maxDepth int) *rapid.Generator[tb] { // are typically encoded as MaxItems=1 lists with a *Resource Elem. // // See https://developer.hashicorp.com/terraform/plugin/framework/handling-data/blocks/single-nested -func (tvg *tvGen) GenSingleNestedBlock(maxDepth int) *rapid.Generator[tv] { +func (tvg *tvGen) GenSingleNestedBlock(depth int) *rapid.Generator[tv] { return rapid.Custom[tv](func(t *rapid.T) tv { st := tvg.GenSchemaTransform().Draw(t, "schemaTransform") - bl := tvg.GenBlock(maxDepth).Draw(t, "block") + bl := tvg.GenBlockWithDepth(depth).Draw(t, "block") listWrapType := tftypes.List{ElementType: bl.typ} listWrap := func(v tftypes.Value) tftypes.Value { return tftypes.NewValue(listWrapType, []tftypes.Value{v}) @@ -221,9 +244,9 @@ func (tvg *tvGen) GenSingleNestedBlock(maxDepth int) *rapid.Generator[tv] { }) } -func (tvg *tvGen) GenListNestedBlock(maxDepth int) *rapid.Generator[tv] { +func (tvg *tvGen) GenListNestedBlock(depth int) *rapid.Generator[tv] { ge := rapid.Custom[tv](func(t *rapid.T) tv { - bl := tvg.GenBlock(maxDepth).Draw(t, "block") + bl := tvg.GenBlockWithDepth(depth).Draw(t, "block") listWrapType := tftypes.List{ElementType: bl.typ} listWrap := func(vs []tftypes.Value) tftypes.Value { return tftypes.NewValue(listWrapType, vs) @@ -246,9 +269,9 @@ func (tvg *tvGen) GenListNestedBlock(maxDepth int) *rapid.Generator[tv] { return ge } -func (tvg *tvGen) GenSetNestedBlock(maxDepth int) *rapid.Generator[tv] { +func (tvg *tvGen) GenSetNestedBlock(depth int) *rapid.Generator[tv] { ge := rapid.Custom[tv](func(t *rapid.T) tv { - bl := tvg.GenBlock(maxDepth).Draw(t, "block") + bl := tvg.GenBlockWithDepth(depth).Draw(t, "block") setWrapType := tftypes.Set{ElementType: bl.typ} setWrap := func(vs []tftypes.Value) tftypes.Value { return tftypes.NewValue(setWrapType, vs) From 8c9933b601e2260aa8dff49cb91d5582ee4c94d0 Mon Sep 17 00:00:00 2001 From: Anton Tayanovskyy Date: Fri, 12 Apr 2024 14:48:47 -0400 Subject: [PATCH 45/45] Fix panic --- pkg/tests/cross-tests/tfwrite.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/tests/cross-tests/tfwrite.go b/pkg/tests/cross-tests/tfwrite.go index 28871402f..eb9402ae2 100644 --- a/pkg/tests/cross-tests/tfwrite.go +++ b/pkg/tests/cross-tests/tfwrite.go @@ -50,7 +50,7 @@ func writeBlock(body *hclwrite.Body, schemas map[string]*schema.Schema, values m if !ok { continue } - contract.Assertf(sch.ConfigMode != 0, "ConfigMode != nil is not yet supported") + contract.Assertf(sch.ConfigMode == 0, "ConfigMode > 0 is not yet supported: %v", sch.ConfigMode) switch elem := sch.Elem.(type) { case *schema.Resource: if sch.Type == schema.TypeMap {