Skip to content

Commit

Permalink
Move cross test utilities to be shared between sdkv2 and pf (#2576)
Browse files Browse the repository at this point in the history
This is a refactor which moves some of the SDKv2 cross-test utilities to
the crosstestimpl colder so that they can be reused in the PF
cross-tests.

related to #2297
  • Loading branch information
VenelinMartinov authored Nov 4, 2024
1 parent 58f0b33 commit ae9cd89
Show file tree
Hide file tree
Showing 7 changed files with 151 additions and 127 deletions.
83 changes: 5 additions & 78 deletions pkg/internal/tests/cross-tests/diff_check.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,21 +12,15 @@
// See the License for the specific language governing permissions and

// Compares the effect of transitioning between two randomly sampled resource configurations.
//
//nolint:lll
package crosstests

import (
"encoding/json"
"fmt"
"os"
"path/filepath"

"github.com/hashicorp/terraform-plugin-go/tftypes"
"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/stretchr/testify/assert"
"github.com/stretchr/testify/require"

crosstestsimpl "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/internal/tests/cross-tests/impl"
Expand All @@ -51,17 +45,7 @@ type diffTestCase struct {
DeleteBeforeReplace bool
}

type pulumiDiffResp struct {
DetailedDiff map[string]interface{} `json:"detailedDiff"`
DeleteBeforeReplace bool `json:"deleteBeforeReplace"`
}

type diffResult struct {
TFDiff tfChange
PulumiDiff pulumiDiffResp
}

func runDiffCheck(t T, tc diffTestCase) diffResult {
func runDiffCheck(t T, tc diffTestCase) crosstestsimpl.DiffResult {
t.Helper()
tfwd := t.TempDir()

Expand Down Expand Up @@ -97,76 +81,19 @@ func runDiffCheck(t T, tc diffTestCase) diffResult {
require.NoErrorf(t, err, "writing Pulumi.yaml")
x := pt.Up(t)

changes := tfd.parseChangesFromTFPlan(*tfDiffPlan)
changes := tfd.driver.ParseChangesFromTFPlan(tfDiffPlan)

diffResponse := pulumiDiffResp{}
diffResponse := crosstestsimpl.PulumiDiffResp{}
for _, entry := range pt.GrpcLog(t).Entries {
if entry.Method == "/pulumirpc.ResourceProvider/Diff" {
err := json.Unmarshal(entry.Response, &diffResponse)
require.NoError(t, err)
}
}
tc.verifyBasicDiffAgreement(t, changes.Actions, x.Summary, diffResponse)
crosstestsimpl.VerifyBasicDiffAgreement(t, changes.Actions, x.Summary, diffResponse)

return diffResult{
return crosstestsimpl.DiffResult{
TFDiff: changes,
PulumiDiff: diffResponse,
}
}

func (tc *diffTestCase) verifyBasicDiffAgreement(t T, tfActions []string, us auto.UpdateSummary, diffResponse pulumiDiffResp) {
t.Helper()
t.Logf("UpdateSummary.ResourceChanges: %#v", us.ResourceChanges)
// Action list from https://github.com/opentofu/opentofu/blob/main/internal/plans/action.go#L11
if len(tfActions) == 0 {
require.FailNow(t, "No TF actions found")
}
if len(tfActions) == 1 {
switch tfActions[0] {
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")
case "create":
require.NotNilf(t, us.ResourceChanges, "UpdateSummary.ResourceChanges should not be nil")
rc := *us.ResourceChanges
assert.Equalf(t, 1, rc[string(apitype.OpSame)], "expected the stack to stay the same")
assert.Equalf(t, 1, rc[string(apitype.OpCreate)], "expected the test resource to get a create plan")
case "read":
require.FailNow(t, "Unexpected TF action: read")
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 "delete":
require.NotNilf(t, us.ResourceChanges, "UpdateSummary.ResourceChanges should not be nil")
rc := *us.ResourceChanges
assert.Equalf(t, 1, rc[string(apitype.OpSame)], "expected the stack to stay the same")
assert.Equalf(t, 1, rc[string(apitype.OpDelete)], "expected the test resource to get a delete plan")
default:
panic("TODO: do not understand this TF action yet: " + tfActions[0])
}
} else if len(tfActions) == 2 {
if tfActions[0] == "create" && tfActions[1] == "delete" {
require.NotNilf(t, us.ResourceChanges, "UpdateSummary.ResourceChanges should not be nil")
rc := *us.ResourceChanges
assert.Equalf(t, 1, rc[string(apitype.OpSame)], "expected the stack to stay the same")
assert.Equalf(t, 1, rc[string(apitype.OpReplace)], "expected the test resource to get a replace plan")
assert.Equalf(t, false, diffResponse.DeleteBeforeReplace, "expected deleteBeforeReplace to be true")
} else if tfActions[0] == "delete" && tfActions[1] == "create" {
require.NotNilf(t, us.ResourceChanges, "UpdateSummary.ResourceChanges should not be nil")
rc := *us.ResourceChanges
t.Logf("UpdateSummary.ResourceChanges: %#v", rc)
assert.Equalf(t, 1, rc[string(apitype.OpSame)], "expected the stack to stay the same")
assert.Equalf(t, 1, rc[string(apitype.OpReplace)], "expected the test resource to get a replace plan")
assert.Equalf(t, true, diffResponse.DeleteBeforeReplace, "expected deleteBeforeReplace to be true")
} else {
panic("TODO: do not understand this TF action yet: " + fmt.Sprint(tfActions))
}
} else {
panic("TODO: do not understand this TF action yet: " + fmt.Sprint(tfActions))
}
}
79 changes: 79 additions & 0 deletions pkg/internal/tests/cross-tests/impl/diff.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package crosstestsimpl

import (
"fmt"

"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"

"github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tests/tfcheck"
)

type PulumiDiffResp struct {
DetailedDiff map[string]interface{} `json:"detailedDiff"`
DeleteBeforeReplace bool `json:"deleteBeforeReplace"`
}

type DiffResult struct {
TFDiff tfcheck.TFChange
PulumiDiff PulumiDiffResp
}

func VerifyBasicDiffAgreement(t T, tfActions []string, us auto.UpdateSummary, diffResponse PulumiDiffResp) {
t.Helper()
t.Logf("UpdateSummary.ResourceChanges: %#v", us.ResourceChanges)
// Action list from https://github.com/opentofu/opentofu/blob/main/internal/plans/action.go#L11
if len(tfActions) == 0 {
require.FailNow(t, "No TF actions found")
}
if len(tfActions) == 1 {
switch tfActions[0] {
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")
case "create":
require.NotNilf(t, us.ResourceChanges, "UpdateSummary.ResourceChanges should not be nil")
rc := *us.ResourceChanges
assert.Equalf(t, 1, rc[string(apitype.OpSame)], "expected the stack to stay the same")
assert.Equalf(t, 1, rc[string(apitype.OpCreate)], "expected the test resource to get a create plan")
case "read":
require.FailNow(t, "Unexpected TF action: read")
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 "delete":
require.NotNilf(t, us.ResourceChanges, "UpdateSummary.ResourceChanges should not be nil")
rc := *us.ResourceChanges
assert.Equalf(t, 1, rc[string(apitype.OpSame)], "expected the stack to stay the same")
assert.Equalf(t, 1, rc[string(apitype.OpDelete)], "expected the test resource to get a delete plan")
default:
panic("TODO: do not understand this TF action yet: " + tfActions[0])
}
} else if len(tfActions) == 2 {
if tfActions[0] == "create" && tfActions[1] == "delete" {
require.NotNilf(t, us.ResourceChanges, "UpdateSummary.ResourceChanges should not be nil")
rc := *us.ResourceChanges
assert.Equalf(t, 1, rc[string(apitype.OpSame)], "expected the stack to stay the same")
assert.Equalf(t, 1, rc[string(apitype.OpReplace)], "expected the test resource to get a replace plan")
assert.Equalf(t, false, diffResponse.DeleteBeforeReplace, "expected deleteBeforeReplace to be true")
} else if tfActions[0] == "delete" && tfActions[1] == "create" {
require.NotNilf(t, us.ResourceChanges, "UpdateSummary.ResourceChanges should not be nil")
rc := *us.ResourceChanges
t.Logf("UpdateSummary.ResourceChanges: %#v", rc)
assert.Equalf(t, 1, rc[string(apitype.OpSame)], "expected the stack to stay the same")
assert.Equalf(t, 1, rc[string(apitype.OpReplace)], "expected the test resource to get a replace plan")
assert.Equalf(t, true, diffResponse.DeleteBeforeReplace, "expected deleteBeforeReplace to be true")
} else {
panic("TODO: do not understand this TF action yet: " + fmt.Sprint(tfActions))
}
} else {
panic("TODO: do not understand this TF action yet: " + fmt.Sprint(tfActions))
}
}
32 changes: 2 additions & 30 deletions pkg/internal/tests/cross-tests/tf_driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,9 @@ package crosstests

import (
"bytes"
"encoding/json"

"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"
"github.com/stretchr/testify/require"
"github.com/zclconf/go-cty/cty"

Expand All @@ -31,7 +29,7 @@ import (
)

type TfResDriver struct {
driver *tfcheck.TfDriver
driver *tfcheck.TFDriver
res *schema.Resource
}

Expand Down Expand Up @@ -84,7 +82,7 @@ func (d *TfResDriver) writePlanApply(
resourceType, resourceName string,
config cty.Value,
lifecycle lifecycleArgs,
) *tfcheck.TfPlan {
) *tfcheck.TFPlan {
if !config.IsNull() {
d.write(t, resourceSchema, resourceType, resourceName, config, lifecycle)
} else {
Expand Down Expand Up @@ -123,32 +121,6 @@ func (d *TfResDriver) write(
d.driver.Write(t, buf.String())
}

type tfChange struct {
Actions []string `json:"actions"`
Before map[string]any `json:"before"`
After map[string]any `json:"after"`
}

// 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 (*TfResDriver) parseChangesFromTFPlan(plan tfcheck.TfPlan) tfChange {
type p struct {
ResourceChanges []struct {
Change tfChange `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")
return pp.ResourceChanges[0].Change
}

func providerHCLProgram(t T, typ string, provider *schema.Provider, config cty.Value) string {
var out bytes.Buffer
w := WriteSDKv2(&out)
Expand Down
6 changes: 3 additions & 3 deletions pkg/pf/tests/internal/cross-tests/configure.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import (
"github.com/pulumi/providertest/providers"
"github.com/pulumi/providertest/pulumitest"
"github.com/pulumi/providertest/pulumitest/opttest"
"github.com/pulumi/pulumi-terraform-bridge/v3/pkg/internal/tests/cross-tests"
crosstests "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/internal/tests/cross-tests"
crosstestsimpl "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/internal/tests/cross-tests/impl"
pb "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/pf/tests/internal/providerbuilder"
"github.com/pulumi/pulumi-terraform-bridge/v3/pkg/pf/tfbridge"
Expand Down Expand Up @@ -110,7 +110,7 @@ func Configure(t *testing.T, schema schema.Schema, tfConfig map[string]cty.Value

var tfOutput, puOutput tfsdk.Config
t.Run("tf", func(t *testing.T) {
defer propageteSkip(topLevelT, t)
defer propagateSkip(topLevelT, t)
var hcl bytes.Buffer
err := crosstests.WritePF(&hcl).Provider(schema, providerName, tfConfig)
require.NoError(t, err)
Expand All @@ -132,7 +132,7 @@ resource "` + providerName + `_res" "res" {}
})

t.Run("bridged", func(t *testing.T) {
defer propageteSkip(topLevelT, t)
defer propagateSkip(topLevelT, t)
dir := t.TempDir()

var puConfig resource.PropertyMap
Expand Down
22 changes: 21 additions & 1 deletion pkg/pf/tests/internal/cross-tests/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,16 @@ import (
"strings"
"testing"

"github.com/pulumi/pulumi-terraform-bridge/v3/pkg/pf/tests/internal/providerbuilder"
"github.com/pulumi/pulumi-terraform-bridge/v3/pkg/pf/tfbridge"
tfbridge0 "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfbridge"
"github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfbridge/info"
"github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfbridge/tokens"
"github.com/pulumi/pulumi/sdk/v3/go/common/diag"
"github.com/pulumi/pulumi/sdk/v3/go/common/resource"
)

func propageteSkip(parent, child *testing.T) {
func propagateSkip(parent, child *testing.T) {
if child.Skipped() {
parent.Skipf("skipping due to skipped child test")
}
Expand Down Expand Up @@ -54,3 +59,18 @@ func skipUnlessLinux(t *testing.T) {
t.Skip("Skipping on non-Linux platforms as our CI does not yet install Terraform CLI required for these tests")
}
}

func bridgedProvider(prov *providerbuilder.Provider) info.Provider {
shimProvider := tfbridge.ShimProvider(prov)

provider := tfbridge0.ProviderInfo{
P: shimProvider,
Name: prov.TypeName,
Version: prov.Version,
MetadataInfo: &tfbridge0.MetadataInfo{},
}

provider.MustComputeTokens(tokens.SingleModule(prov.TypeName, "index", tokens.MakeStandard(prov.TypeName)))

return provider
}
2 changes: 1 addition & 1 deletion pkg/tests/tfcheck/exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import (
"github.com/stretchr/testify/require"
)

func (d *TfDriver) execTf(t pulcheck.T, args ...string) ([]byte, error) {
func (d *TFDriver) execTf(t pulcheck.T, args ...string) ([]byte, error) {
cmd, err := execCmd(t, d.cwd, []string{d.formatReattachEnvVar()}, getTFCommand(), args...)
if stderr := cmd.Stderr.(*bytes.Buffer).String(); len(stderr) > 0 {
t.Logf("%q stderr:\n%s\n", cmd.String(), stderr)
Expand Down
Loading

0 comments on commit ae9cd89

Please sign in to comment.