From fdb3d4f7b714cee44350f394b25526f2eec431d4 Mon Sep 17 00:00:00 2001 From: Rudolf Thomas Date: Fri, 11 Mar 2022 17:58:13 +0100 Subject: [PATCH 1/3] Support testing of configurations in JSON syntax. This implements the proposal described at https://github.com/hashicorp/terraform-plugin-sdk/pull/722#issuecomment-941379029 --- helper/resource/testing.go | 3 + internal/plugintest/working_dir.go | 36 +++++++--- internal/plugintest/working_dir_json_test.go | 69 ++++++++++++++++++++ 3 files changed, 98 insertions(+), 10 deletions(-) create mode 100644 internal/plugintest/working_dir_json_test.go diff --git a/helper/resource/testing.go b/helper/resource/testing.go index f5b4cfeee0a..9bb40938ce5 100644 --- a/helper/resource/testing.go +++ b/helper/resource/testing.go @@ -445,6 +445,9 @@ type TestStep struct { // Config a string of the configuration to give to Terraform. If this // is set, then the TestCase will execute this step with the same logic // as a `terraform apply`. + // + // JSON Configuration Syntax can be used and is assumed whenever Config + // contains valid JSON. Config string // Check is called after the Config is applied. Use this step to diff --git a/internal/plugintest/working_dir.go b/internal/plugintest/working_dir.go index 8b555f462c9..0bd5aa63a46 100644 --- a/internal/plugintest/working_dir.go +++ b/internal/plugintest/working_dir.go @@ -3,6 +3,7 @@ package plugintest import ( "bytes" "context" + "encoding/json" "errors" "fmt" "io/ioutil" @@ -15,8 +16,9 @@ import ( ) const ( - ConfigFileName = "terraform_plugin_test.tf" - PlanFileName = "tfplan" + ConfigFileName = "terraform_plugin_test.tf" + ConfigFileNameJSON = ConfigFileName + ".json" + PlanFileName = "tfplan" ) // WorkingDir represents a distinct working directory that can be used for @@ -29,6 +31,10 @@ type WorkingDir struct { // baseDir is the root of the working directory tree baseDir string + // configFilename is the full filename where the latest configuration + // was stored; empty until SetConfig is called. + configFilename string + // baseArgs is arguments that should be appended to all commands baseArgs []string @@ -84,11 +90,20 @@ func (wd *WorkingDir) GetHelper() *Helper { // Destroy to establish the configuration. Any previously-set configuration is // discarded and any saved plan is cleared. func (wd *WorkingDir) SetConfig(ctx context.Context, cfg string) error { - configFilename := filepath.Join(wd.baseDir, ConfigFileName) - err := ioutil.WriteFile(configFilename, []byte(cfg), 0700) + outFilename := filepath.Join(wd.baseDir, ConfigFileName) + rmFilename := filepath.Join(wd.baseDir, ConfigFileNameJSON) + bCfg := []byte(cfg) + if json.Valid(bCfg) { + outFilename, rmFilename = rmFilename, outFilename + } + if err := os.Remove(rmFilename); err != nil && !os.IsNotExist(err) { + return fmt.Errorf("unable to remove %q: %w", rmFilename, err) + } + err := ioutil.WriteFile(outFilename, bCfg, 0700) if err != nil { return err } + wd.configFilename = outFilename var mismatch *tfexec.ErrVersionMismatch err = wd.tf.SetDisablePluginTLS(true) @@ -157,11 +172,16 @@ func (wd *WorkingDir) ClearPlan(ctx context.Context) error { return nil } +var errWorkingDirSetConfigNotCalled = fmt.Errorf("must call SetConfig before Init") + // Init runs "terraform init" for the given working directory, forcing Terraform // to use the current version of the plugin under test. func (wd *WorkingDir) Init(ctx context.Context) error { - if _, err := os.Stat(wd.configFilename()); err != nil { - return fmt.Errorf("must call SetConfig before Init") + if wd.configFilename == "" { + return errWorkingDirSetConfigNotCalled + } + if _, err := os.Stat(wd.configFilename); err != nil { + return errWorkingDirSetConfigNotCalled } logging.HelperResourceTrace(ctx, "Calling Terraform CLI init command") @@ -173,10 +193,6 @@ func (wd *WorkingDir) Init(ctx context.Context) error { return err } -func (wd *WorkingDir) configFilename() string { - return filepath.Join(wd.baseDir, ConfigFileName) -} - func (wd *WorkingDir) planFilename() string { return filepath.Join(wd.baseDir, PlanFileName) } diff --git a/internal/plugintest/working_dir_json_test.go b/internal/plugintest/working_dir_json_test.go new file mode 100644 index 00000000000..e15e64530f4 --- /dev/null +++ b/internal/plugintest/working_dir_json_test.go @@ -0,0 +1,69 @@ +package plugintest_test + +import ( + "context" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +// TestJSONConfig verifies that TestStep.Config can contain JSON. +// This test also proves that when changing the HCL and JSON formats back and +// forth, the framework deletes the previous configuration file. +func TestJSONConfig(t *testing.T) { + providerFactories := map[string]func() (*schema.Provider, error){ + "tst": func() (*schema.Provider, error) { return tstProvider(), nil }, + } + resource.Test(t, resource.TestCase{ + IsUnitTest: true, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{{ + Config: `{"resource":{"tst_t":{"r1":{"s":"x1"}}}}`, + Check: resource.TestCheckResourceAttr("tst_t.r1", "s", "x1"), + }, { + Config: `resource "tst_t" "r1" { s = "x2" }`, + Check: resource.TestCheckResourceAttr("tst_t.r1", "s", "x2"), + }, { + Config: `{"resource":{"tst_t":{"r1":{"s":"x3"}}}}`, + Check: resource.TestCheckResourceAttr("tst_t.r1", "s", "x3"), + }}, + }) +} + +func tstProvider() *schema.Provider { + return &schema.Provider{ + ResourcesMap: map[string]*schema.Resource{ + "tst_t": &schema.Resource{ + CreateContext: resourceTstTCreate, + ReadContext: resourceTstTRead, + UpdateContext: resourceTstTCreate, // Update is the same as Create + DeleteContext: resourceTstTDelete, + Schema: map[string]*schema.Schema{ + "s": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + }, + }, + }, + } +} + +func resourceTstTCreate(ctx context.Context, d *schema.ResourceData, _ interface{}) diag.Diagnostics { + d.SetId(d.Get("s").(string)) + return nil +} + +func resourceTstTRead(ctx context.Context, d *schema.ResourceData, _ interface{}) diag.Diagnostics { + if err := d.Set("s", d.Id()); err != nil { + return diag.FromErr(err) + } + return nil +} + +func resourceTstTDelete(ctx context.Context, d *schema.ResourceData, _ interface{}) diag.Diagnostics { + d.SetId("") + return nil +} From dadff8634c35388abb3c68149dcc5ce42e793b87 Mon Sep 17 00:00:00 2001 From: Brian Flad Date: Fri, 11 Mar 2022 16:16:07 -0500 Subject: [PATCH 2/3] Apply suggestions from code review --- internal/plugintest/working_dir_json_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/plugintest/working_dir_json_test.go b/internal/plugintest/working_dir_json_test.go index e15e64530f4..1845fe1a6b1 100644 --- a/internal/plugintest/working_dir_json_test.go +++ b/internal/plugintest/working_dir_json_test.go @@ -14,7 +14,7 @@ import ( // forth, the framework deletes the previous configuration file. func TestJSONConfig(t *testing.T) { providerFactories := map[string]func() (*schema.Provider, error){ - "tst": func() (*schema.Provider, error) { return tstProvider(), nil }, + "tst": func() (*schema.Provider, error) { return tstProvider(), nil }, //nolint:unparam // required signature } resource.Test(t, resource.TestCase{ IsUnitTest: true, @@ -35,7 +35,7 @@ func TestJSONConfig(t *testing.T) { func tstProvider() *schema.Provider { return &schema.Provider{ ResourcesMap: map[string]*schema.Resource{ - "tst_t": &schema.Resource{ + "tst_t": { CreateContext: resourceTstTCreate, ReadContext: resourceTstTRead, UpdateContext: resourceTstTCreate, // Update is the same as Create From feac89ecc04202fe90b3e3e07bdb945d50c541da Mon Sep 17 00:00:00 2001 From: Brian Flad Date: Fri, 11 Mar 2022 16:20:39 -0500 Subject: [PATCH 3/3] Update internal/plugintest/working_dir_json_test.go --- internal/plugintest/working_dir_json_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/plugintest/working_dir_json_test.go b/internal/plugintest/working_dir_json_test.go index 1845fe1a6b1..ac4ac7e5031 100644 --- a/internal/plugintest/working_dir_json_test.go +++ b/internal/plugintest/working_dir_json_test.go @@ -41,7 +41,7 @@ func tstProvider() *schema.Provider { UpdateContext: resourceTstTCreate, // Update is the same as Create DeleteContext: resourceTstTDelete, Schema: map[string]*schema.Schema{ - "s": &schema.Schema{ + "s": { Type: schema.TypeString, Required: true, },