diff --git a/helper/resource/testing.go b/helper/resource/testing.go index 659cf26412..3a3b331d78 100644 --- a/helper/resource/testing.go +++ b/helper/resource/testing.go @@ -7,11 +7,14 @@ import ( "log" "os" "path/filepath" + "reflect" "regexp" "strings" "testing" + "github.com/davecgh/go-spew/spew" "github.com/hashicorp/go-getter" + "github.com/hashicorp/terraform/config" "github.com/hashicorp/terraform/config/module" "github.com/hashicorp/terraform/helper/logging" "github.com/hashicorp/terraform/terraform" @@ -145,6 +148,7 @@ func Test(t TestT, c TestCase) { var state *terraform.State // Go through each step and run it + var idRefreshCheck *terraform.ResourceState for i, step := range c.Steps { var err error log.Printf("[WARN] Test: Executing step %d", i) @@ -154,6 +158,36 @@ func Test(t TestT, c TestCase) { "Step %d error: %s", i, err)) break } + + // If we've never checked an id-only refresh and our state isn't + // empty, find the first resource and test it. + if idRefreshCheck == nil && !state.Empty() { + // Find the first non-nil resource in the state + for _, m := range state.Modules { + if len(m.Resources) > 0 { + for _, v := range m.Resources { + if v != nil && v.Primary != nil { + idRefreshCheck = v + break + } + } + } + } + + // If we have an instance to check for refreshes, do it + // immediately. We do it in the middle of another test + // because it shouldn't affect the overall state (refresh + // is read-only semantically) and we want to fail early if + // this fails. If refresh isn't read-only, then this will have + // caught a different bug. + if idRefreshCheck != nil { + if err := testIDOnlyRefresh(opts, idRefreshCheck); err != nil { + t.Error(fmt.Sprintf( + "ID-Only refresh test failure: %s", err)) + break + } + } + } } // If we have a state, then run the destroy @@ -195,6 +229,65 @@ func UnitTest(t TestT, c TestCase) { Test(t, c) } +func testIDOnlyRefresh(opts terraform.ContextOpts, r *terraform.ResourceState) error { + name := fmt.Sprintf("%s.foo", r.Type) + + // Build the state. The state is just the resource with an ID. There + // are no attributes. We only set what is needed to perform a refresh. + state := terraform.NewState() + state.RootModule().Resources[name] = &terraform.ResourceState{ + Type: r.Type, + Primary: &terraform.InstanceState{ + ID: r.Primary.ID, + }, + } + + // Empty module + mod := module.NewTree("root", &config.Config{}) + if err := mod.Load(nil, module.GetModeGet); err != nil { + return fmt.Errorf("Error loading modules: %s", err) + } + + // Initialize the context + opts.Module = mod + opts.State = state + ctx := terraform.NewContext(&opts) + if ws, es := ctx.Validate(); len(ws) > 0 || len(es) > 0 { + if len(es) > 0 { + estrs := make([]string, len(es)) + for i, e := range es { + estrs[i] = e.Error() + } + return fmt.Errorf( + "Configuration is invalid.\n\nWarnings: %#v\n\nErrors: %#v", + ws, estrs) + } + + log.Printf("[WARN] Config warnings: %#v", ws) + } + + // Refresh! + state, err := ctx.Refresh() + if err != nil { + return fmt.Errorf("Error refreshing: %s", err) + } + + // Verify attribute equivalence. + println(state.String()) + actual := state.RootModule().Resources[name].Primary.Attributes + expected := r.Primary.Attributes + if !reflect.DeepEqual(actual, expected) { + // TODO: determine attribute difference + + return fmt.Errorf( + "Attributes not equivalent. Top is what we received, bottom is expected."+ + "\n\n%s\n\n%s", + spew.Sdump(actual), spew.Sdump(expected)) + } + + return nil +} + func testStep( opts terraform.ContextOpts, state *terraform.State, diff --git a/helper/resource/testing_test.go b/helper/resource/testing_test.go index 31e8ab69d7..e57e1f54eb 100644 --- a/helper/resource/testing_test.go +++ b/helper/resource/testing_test.go @@ -83,6 +83,51 @@ func TestTest(t *testing.T) { } } +func TestTest_idRefresh(t *testing.T) { + // Refresh count should be 3: + // 1.) initial Ref/Plan/Apply + // 2.) post Ref/Plan/Apply for plan-check + // 3.) id refresh check + var expectedRefresh int32 = 3 + + mp := testProvider() + mp.DiffReturn = nil + + mp.ApplyReturn = &terraform.InstanceState{ + ID: "foo", + } + var refreshCount int32 + mp.RefreshFn = func(*terraform.InstanceInfo, *terraform.InstanceState) (*terraform.InstanceState, error) { + atomic.AddInt32(&refreshCount, 1) + if atomic.LoadInt32(&refreshCount) < expectedRefresh { + return &terraform.InstanceState{ID: "foo"}, nil + } else { + return nil, nil + } + } + + mt := new(mockT) + Test(mt, TestCase{ + Providers: map[string]terraform.ResourceProvider{ + "test": mp, + }, + Steps: []TestStep{ + TestStep{ + Config: testConfigStr, + }, + }, + }) + + if mt.failed() { + t.Fatalf("test failed: %s", mt.failMessage()) + } + + // See declaration of expectedRefresh for why that number + if refreshCount != expectedRefresh { + t.Fatalf("bad refresh count: %d", refreshCount) + } +} + func TestTest_empty(t *testing.T) { destroyCalled := false checkDestroyFn := func(*terraform.State) error {