Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding RefreshState test step #1070

Merged
merged 14 commits into from Oct 12, 2022
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions .changelog/1070.txt
@@ -0,0 +1,3 @@
```release-note:enhancement
helper/resource: Add RefreshState to allow testing of refresh in isolation
bendbennett marked this conversation as resolved.
Show resolved Hide resolved
```
8 changes: 8 additions & 0 deletions helper/resource/testing.go
Expand Up @@ -570,6 +570,14 @@ type TestStep struct {
// at the end of the test step that is verifying import behavior.
ImportStatePersist bool

//---------------------------------------------------------------
// RefreshState testing
//---------------------------------------------------------------

// RefreshState, if true, will test the functionality of `terraform
// refresh` by refreshing the state.
RefreshState bool
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should consider what website documentation we can add around this. It'd also be good to denote here and in the website that its current intention is refresh, (potentially) plan, and optionally running checks.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This field should also document that it cannot be the first TestStep and any other potential restrictions or contradictory definitions (such as ImportState + RefreshState for example)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've updated the documentation. In terms of the website docs, where do you think they should be added? The information around ImportState is in /plugin/sdkv2/resources. As we're just discussing testing here should be add a new section or add/amend an area of the website docs that already exist?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think https://www.terraform.io/plugin/sdkv2/testing/acceptance-tests/teststep is okay enough for now. Ideally that page would probably be broken up by test mode, but adding information in the current information architecture should be okay. 👍

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have added a couple of lines. Let me know if you think this needs to be expanded.


// ProviderFactories can be specified for the providers that are valid for
// this TestStep. When providers are specified at the TestStep level, all
// TestStep within a TestCase must declare providers.
Expand Down
39 changes: 39 additions & 0 deletions helper/resource/testing_new.go
Expand Up @@ -241,6 +241,45 @@ func runNewTest(ctx context.Context, t testing.T, c TestCase, helper *plugintest
continue
}

if step.RefreshState {
logging.HelperResourceTrace(ctx, "TestStep is RefreshState mode")

err := testStepNewRefreshState(ctx, t, wd, step, appliedCfg, providers)
if step.ExpectError != nil {
logging.HelperResourceDebug(ctx, "Checking TestStep ExpectError")
if err == nil {
logging.HelperResourceError(ctx,
"Error running refresh: expected an error but got none",
)
t.Fatalf("Step %d/%d error running refresh: expected an error but got none", stepNumber, len(c.Steps))
}
if !step.ExpectError.MatchString(err.Error()) {
logging.HelperResourceError(ctx,
fmt.Sprintf("Error running refresh: expected an error with pattern (%s)", step.ExpectError.String()),
map[string]interface{}{logging.KeyError: err},
)
t.Fatalf("Step %d/%d error running refresh, expected an error with pattern (%s), no match on: %s", stepNumber, len(c.Steps), step.ExpectError.String(), err)
}
} else {
if err != nil && c.ErrorCheck != nil {
logging.HelperResourceDebug(ctx, "Calling TestCase ErrorCheck")
err = c.ErrorCheck(err)
logging.HelperResourceDebug(ctx, "Called TestCase ErrorCheck")
}
if err != nil {
logging.HelperResourceError(ctx,
"Error running refresh",
map[string]interface{}{logging.KeyError: err},
)
t.Fatalf("Step %d/%d error running refresh: %s", stepNumber, len(c.Steps), err)
}
}

logging.HelperResourceDebug(ctx, "Finished TestStep")

continue
}

if step.Config != "" {
logging.HelperResourceTrace(ctx, "TestStep is Config mode")

Expand Down
86 changes: 86 additions & 0 deletions helper/resource/testing_new_refresh_state.go
@@ -0,0 +1,86 @@
package resource

import (
"context"

"github.com/davecgh/go-spew/spew"
"github.com/mitchellh/go-testing-interface"

"github.com/hashicorp/terraform-plugin-sdk/v2/internal/logging"
"github.com/hashicorp/terraform-plugin-sdk/v2/internal/plugintest"
"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
)

func testStepNewRefreshState(ctx context.Context, t testing.T, wd *plugintest.WorkingDir, step TestStep, cfg string, providers *providerFactories) error {
t.Helper()

spewConf := spew.NewDefaultConfig()
spewConf.SortKeys = true

var err error
err = runProviderCommand(ctx, t, func() error {
bendbennett marked this conversation as resolved.
Show resolved Hide resolved
_, err = getState(ctx, t, wd)
if err != nil {
return err
}
return nil
}, wd, providers)
if err != nil {
t.Fatalf("Error getting state: %s", err)
}

if step.Config == "" {
Copy link
Member

@bflad bflad Oct 5, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this TestStep mode worry about a configuration? What happens if the configuration changes during a refresh step from a prior step? Here are the potential issues I can see here with a new/updated configuration:

  • Changes which providers are defined. If the existing state has a resource with a provider which is no longer presented properly, then an error could be raised.
  • Has an invalid provider configuration, which raises a new error.
  • Has an invalid resource configuration, which raises a new error.
  • Has an updated resource configuration, which wouldn't be reflected during the refresh and causes checks to potentially have unexpected results compared to the configuration.

The prior TestStep with a configuration would've already caught the first few and I think rather than introduce the potential for these classes of issues, we can avoid this by not resetting the working directory and its configuration. The validation that a refresh step is not the first step can be implemented in the testcase_validate.go and/or teststep_validate.go files, which runs prior to executing any of the test steps.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have refactored accordingly.

logging.HelperResourceTrace(ctx, "Using prior TestStep Config for refresh")

step.Config = cfg
if step.Config == "" {
t.Fatal("Cannot refresh state with no specified config")
}
}

err = wd.SetConfig(ctx, step.Config)
if err != nil {
t.Fatalf("Error setting test config: %s", err)
}

logging.HelperResourceDebug(ctx, "Running Terraform CLI init and refresh")

err = runProviderCommand(ctx, t, func() error {
return wd.Init(ctx)
}, wd, providers)
if err != nil {
t.Fatalf("Error running init: %s", err)
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to the above, I'm not sure we should intentionally run init again, given the potential issues it could cause.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have refactored.


err = runProviderCommand(ctx, t, func() error {
return wd.Refresh(ctx)
}, wd, providers)
if err != nil {
return err
}

var refreshState *terraform.State
err = runProviderCommand(ctx, t, func() error {
refreshState, err = getState(ctx, t, wd)
if err != nil {
return err
}
return nil
}, wd, providers)
if err != nil {
t.Fatalf("Error getting state: %s", err)
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should refresh TestStep run plan after the refresh to check for unexpected plan differences? If so, it'll need that Terraform command added and check against ExpectNonEmptyPlan if those differences are expected.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have added a plan following the check and the check against ExpectNonEmptyPlan.

// Go through the refreshed state and verify
if step.Check != nil {
logging.HelperResourceDebug(ctx, "Calling TestStep Check for RefreshState")

if err := step.Check(refreshState); err != nil {
t.Fatal(err)
}

logging.HelperResourceDebug(ctx, "Called TestStep Check for RefreshState")
}

return nil
}
61 changes: 61 additions & 0 deletions helper/resource/teststep_providers_test.go
Expand Up @@ -1574,6 +1574,67 @@ func TestTest_TestStep_ProviderFactories_Import_External_WithoutPersistNonMatch(
})
}

func TestTest_TestStep_ProviderFactories_Refresh_Inline(t *testing.T) {
t.Parallel()

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")
err := d.Set("min_special", 10)
if err != nil {
panic(err)
}
return nil
},
DeleteContext: func(_ context.Context, _ *schema.ResourceData, _ interface{}) diag.Diagnostics {
return nil
},
ReadContext: func(_ context.Context, d *schema.ResourceData, _ interface{}) diag.Diagnostics {
err := d.Set("min_special", 2)
if err != nil {
panic(err)
}
return nil
},
Schema: map[string]*schema.Schema{
"min_special": {
Computed: true,
Type: schema.TypeInt,
},

"id": {
Computed: true,
Type: schema.TypeString,
},
},
},
},
}, nil
},
},
Steps: []TestStep{
{
Config: `resource "random_password" "test" { }`,
Check: TestCheckResourceAttr("random_password.test", "min_special", "10"),
},
{
Config: `resource "random_password" "test" { }`,
RefreshState: true,
Check: TestCheckResourceAttr("random_password.test", "min_special", "2"),
},
{
Config: `resource "random_password" "test" { }`,
Check: TestCheckResourceAttr("random_password.test", "min_special", "2"),
},
},
})
}

func composeImportStateCheck(fs ...ImportStateCheckFunc) ImportStateCheckFunc {
return func(s []*terraform.InstanceState) error {
for i, f := range fs {
Expand Down