diff --git a/go.mod b/go.mod index bc942e484..91211c0bf 100644 --- a/go.mod +++ b/go.mod @@ -26,7 +26,6 @@ require ( require ( github.com/agext/levenshtein v1.2.2 // indirect github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect - github.com/davecgh/go-spew v1.1.1 // indirect github.com/fatih/color v1.13.0 // indirect github.com/golang/protobuf v1.5.2 // indirect github.com/hashicorp/errwrap v1.0.0 // indirect diff --git a/helper/resource/copy.go b/helper/resource/copy.go new file mode 100644 index 000000000..1f6e63600 --- /dev/null +++ b/helper/resource/copy.go @@ -0,0 +1,69 @@ +package resource + +import ( + "io" + "os" + "path" +) + +// CopyFile copies a single file from src to dest. +func CopyFile(src, dest string) error { + var srcFileInfo os.FileInfo + + srcFile, err := os.Open(src) + if err != nil { + return err + } + defer srcFile.Close() + + destFile, err := os.Create(dest) + if err != nil { + return err + } + defer destFile.Close() + + if _, err = io.Copy(destFile, srcFile); err != nil { + return err + } + + if srcFileInfo, err = os.Stat(src); err != nil { + return err + } + + return os.Chmod(dest, srcFileInfo.Mode()) +} + +// CopyDir recursively copies directories and files +// from src to dest. +func CopyDir(src string, dest string) error { + srcInfo, err := os.Stat(src) + if err != nil { + return err + } + + if err = os.MkdirAll(dest, srcInfo.Mode()); err != nil { + return err + } + + dirEntries, err := os.ReadDir(src) + if err != nil { + return err + } + + for _, dirEntry := range dirEntries { + srcFilepath := path.Join(src, dirEntry.Name()) + destFilepath := path.Join(dest, dirEntry.Name()) + + if dirEntry.IsDir() { + if err = CopyDir(srcFilepath, destFilepath); err != nil { + return err + } + } else { + if err = CopyFile(srcFilepath, destFilepath); err != nil { + return err + } + } + } + + return nil +} diff --git a/helper/resource/copy_test.go b/helper/resource/copy_test.go new file mode 100644 index 000000000..0aeca454a --- /dev/null +++ b/helper/resource/copy_test.go @@ -0,0 +1,110 @@ +package resource + +import ( + "os" + "path/filepath" + "testing" +) + +func TestCopyFile(t *testing.T) { + srcDir, err := os.MkdirTemp("", "") + if err != nil { + t.Fatalf("cannot create src dir: %s", err) + } + defer os.RemoveAll(srcDir) + + _, err = os.Create(filepath.Join(srcDir, "src.txt")) + if err != nil { + t.Fatalf("cannot create src file: %s", err) + } + + destDir, err := os.MkdirTemp("", "") + if err != nil { + t.Fatalf("cannot create dest dir: %s", err) + } + defer os.RemoveAll(destDir) + + err = CopyFile(filepath.Join(srcDir, "src.txt"), filepath.Join(destDir, "src.txt")) + if err != nil { + t.Fatalf("cannot copy src file: %s", err) + } + + srcDirEntries, err := os.ReadDir(srcDir) + if err != nil { + t.Fatalf("cannot read src dir: %s", srcDir) + } + + destDirEntries, err := os.ReadDir(srcDir) + if err != nil { + t.Fatalf("cannot read dest dir: %s", srcDir) + } + + if !Equal(t, srcDirEntries, destDirEntries) { + t.Fatalf("dir entries differ: %v, %v", srcDirEntries, destDirEntries) + } +} + +func TestCopyDir(t *testing.T) { + srcDir, err := os.MkdirTemp("", "") + if err != nil { + t.Fatalf("cannot create src dir: %s", err) + } + defer os.RemoveAll(srcDir) + + _, err = os.Create(filepath.Join(srcDir, "src.txt")) + if err != nil { + t.Fatalf("cannot create src file: %s", err) + } + + err = CopyDir(srcDir, srcDir+"_1") + if err != nil { + t.Fatalf("cannot copy dir: %s", err) + } + defer os.RemoveAll(srcDir + "_1") + + srcDirEntries, err := os.ReadDir(srcDir) + if err != nil { + t.Fatalf("cannot read src dir: %s", srcDir) + } + + destDirEntries, err := os.ReadDir(srcDir) + if err != nil { + t.Fatalf("cannot read dest dir: %s", srcDir) + } + + if !Equal(t, srcDirEntries, destDirEntries) { + t.Fatalf("dir entries differ: %v, %v", srcDirEntries, destDirEntries) + } +} + +func Equal(t *testing.T, a, b []os.DirEntry) bool { + if len(a) != len(b) { + return false + } + + for i, v := range a { + if v.Type() != b[i].Type() { + return false + } + + if v.Name() != b[i].Name() { + return false + } + + aInfo, err := v.Info() + if err != nil { + t.Fatalf("cannot get file info: %s", err) + } + + bInfo, err := b[i].Info() + if err != nil { + t.Fatalf("cannot get file info: %s", err) + } + + if aInfo.Mode() != bInfo.Mode() { + return false + } + } + + return true +} diff --git a/helper/resource/testing_new.go b/helper/resource/testing_new.go index e8c901880..dd4e4154f 100644 --- a/helper/resource/testing_new.go +++ b/helper/resource/testing_new.go @@ -5,16 +5,16 @@ import ( "fmt" "os" "reflect" + "strconv" "strings" "github.com/google/go-cmp/cmp" tfjson "github.com/hashicorp/terraform-json" "github.com/mitchellh/go-testing-interface" - "github.com/hashicorp/terraform-plugin-testing/terraform" - "github.com/hashicorp/terraform-plugin-testing/internal/logging" "github.com/hashicorp/terraform-plugin-testing/internal/plugintest" + "github.com/hashicorp/terraform-plugin-testing/terraform" ) func runPostTestDestroy(ctx context.Context, t testing.T, c TestCase, wd *plugintest.WorkingDir, providers *providerFactories, statePreDestroy *terraform.State) error { @@ -85,11 +85,6 @@ func runNewTest(ctx context.Context, t testing.T, c TestCase, helper *plugintest } } - if v := os.Getenv(plugintest.EnvTfAccPersistWorkingDir); v != "" { - t.Log(fmt.Sprintf("Working directory and files have been persisted in: %s", wd.GetHelper().WorkingDirectory())) - return - } - wd.Close() }() @@ -122,9 +117,14 @@ func runNewTest(ctx context.Context, t testing.T, c TestCase, helper *plugintest // use this to track last step successfully applied // acts as default for import tests var appliedCfg string + var stepNumber int for stepIndex, step := range c.Steps { - stepNumber := stepIndex + 1 // 1-based indexing for humans + if stepNumber > 0 { + copyWorkingDir(ctx, t, stepNumber, wd) + } + + stepNumber = stepIndex + 1 // 1-based indexing for humans ctx = logging.TestStepNumberContext(ctx, stepNumber) logging.HelperResourceDebug(ctx, "Starting TestStep") @@ -331,6 +331,10 @@ func runNewTest(ctx context.Context, t testing.T, c TestCase, helper *plugintest t.Fatalf("Step %d/%d, unsupported test mode", stepNumber, len(c.Steps)) } + + if stepNumber > 0 { + copyWorkingDir(ctx, t, stepNumber, wd) + } } func getState(ctx context.Context, t testing.T, wd *plugintest.WorkingDir) (*terraform.State, error) { @@ -445,3 +449,27 @@ func testIDRefresh(ctx context.Context, t testing.T, c TestCase, wd *plugintest. return nil } + +func copyWorkingDir(ctx context.Context, t testing.T, stepNumber int, wd *plugintest.WorkingDir) { + if v := os.Getenv(plugintest.EnvTfAccPersistWorkingDir); v != "" { + dest := wd.GetHelper().WorkingDirectory() + "_" + strconv.Itoa(stepNumber) + + err := os.Setenv(plugintest.EnvTfAccPersistWorkingDirPath, wd.GetHelper().WorkingDirectory()) + if err != nil { + t.Log(fmt.Sprintf("could not set %s env var: %s", plugintest.EnvTfAccPersistWorkingDirPath, err)) + } + + err = CopyDir(wd.GetHelper().WorkingDirectory(), dest) + if err != nil { + logging.HelperResourceError(ctx, + "Unexpected error copying working directory files", + map[string]interface{}{logging.KeyError: err}, + ) + t.Fatalf("TestStep %d/%d error copying working directory files: %s", stepNumber, err) + } + + t.Log(fmt.Sprintf("Working directory and files have been copied to: %s", dest)) + + return + } +} diff --git a/helper/resource/teststep_providers_test.go b/helper/resource/teststep_providers_test.go index cf226a656..bb3b214ca 100644 --- a/helper/resource/teststep_providers_test.go +++ b/helper/resource/teststep_providers_test.go @@ -3,6 +3,8 @@ package resource import ( "context" "fmt" + "os" + "strconv" "strings" "testing" "time" @@ -16,6 +18,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/customdiff" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-testing/internal/plugintest" "github.com/hashicorp/terraform-plugin-testing/terraform" ) @@ -1868,6 +1871,70 @@ func TestTest_TestStep_ProviderFactories_Refresh_Inline(t *testing.T) { }) } +func TestTest_TestStep_ProviderFactories_CopyWorkingDir_EachTestStep(t *testing.T) { + t.Parallel() + + err := os.Setenv(plugintest.EnvTfAccPersistWorkingDir, "1") + if err != nil { + t.Fatalf("cannot set %s env var: %s", plugintest.EnvTfAccPersistWorkingDir, err) + } + + testSteps := []TestStep{ + { + Config: `resource "random_password" "test" { }`, + }, + { + Config: `resource "random_password" "test" { }`, + }, + } + + Test(t, TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "random": func() (*schema.Provider, error) { //nolint:unparam // required signature + return &schema.Provider{ + ResourcesMap: map[string]*schema.Resource{ + "random_password": { + CreateContext: func(ctx context.Context, d *schema.ResourceData, i interface{}) diag.Diagnostics { + d.SetId("id") + return nil + }, + DeleteContext: func(_ context.Context, _ *schema.ResourceData, _ interface{}) diag.Diagnostics { + return nil + }, + ReadContext: func(_ context.Context, d *schema.ResourceData, _ interface{}) diag.Diagnostics { + return nil + }, + Schema: map[string]*schema.Schema{ + "id": { + Computed: true, + Type: schema.TypeString, + }, + }, + }, + }, + }, nil + }, + }, + Steps: testSteps, + }) + + workingDirPath := os.Getenv(plugintest.EnvTfAccPersistWorkingDirPath) + + for k := range testSteps { + dir := workingDirPath + "_" + strconv.Itoa(k+1) + + _, err := os.ReadDir(dir) + if err != nil { + t.Fatalf("cannot read dir: %s", dir) + } + } + + err = os.Unsetenv(plugintest.EnvTfAccPersistWorkingDir) + if err != nil { + t.Fatalf("cannot unset %s env var: %s", plugintest.EnvTfAccPersistWorkingDir, err) + } +} + func TestTest_TestStep_ProviderFactories_RefreshWithPlanModifier_Inline(t *testing.T) { t.Parallel() diff --git a/internal/plugintest/environment_variables.go b/internal/plugintest/environment_variables.go index 86d9c0cb3..cd2c8e893 100644 --- a/internal/plugintest/environment_variables.go +++ b/internal/plugintest/environment_variables.go @@ -113,4 +113,8 @@ const ( // test. Can be set to any value to persist the working directory and // its contents, however "1" is conventional. EnvTfAccPersistWorkingDir = "TF_ACC_PERSIST_WORKING_DIR" + + // EnvTfAccPersistWorkingDirPath environment variable is populated + // with the working directory path for the current test step. + EnvTfAccPersistWorkingDirPath = "TF_ACC_PERSIST_WORKING_DIR_PATH" ) diff --git a/internal/plugintest/helper.go b/internal/plugintest/helper.go index b864073e2..d1ef86643 100644 --- a/internal/plugintest/helper.go +++ b/internal/plugintest/helper.go @@ -96,10 +96,6 @@ func (h *Helper) Close() error { } } - if v := os.Getenv(EnvTfAccPersistWorkingDir); v != "" { - return nil - } - return os.RemoveAll(h.baseDir) }