Skip to content

Commit

Permalink
Merge pull request #6262 from hashicorp/f-import-single
Browse files Browse the repository at this point in the history
ID-only tests for all resources
  • Loading branch information
mitchellh committed Apr 20, 2016
2 parents 29daf8c + f2c4f8e commit e6a3726
Show file tree
Hide file tree
Showing 12 changed files with 2,367 additions and 7 deletions.
4 changes: 4 additions & 0 deletions Godeps/Godeps.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

116 changes: 116 additions & 0 deletions helper/resource/testing.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -145,15 +148,56 @@ func Test(t TestT, c TestCase) {
var state *terraform.State

// Go through each step and run it
var idRefreshCheck *terraform.ResourceState
errored := false
for i, step := range c.Steps {
var err error
log.Printf("[WARN] Test: Executing step %d", i)
state, err = testStep(opts, state, step)
if err != nil {
errored = true
t.Error(fmt.Sprintf(
"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 {
log.Printf(
"[WARN] Test: Running ID-only refresh check on %s",
idRefreshCheck.Primary.ID)
if err := testIDOnlyRefresh(opts, idRefreshCheck); err != nil {
t.Error(fmt.Sprintf(
"ID-Only refresh test failure: %s", err))
break
}
}
}
}

// If we never checked an id-only refresh, it is a failure.
if !errored && len(c.Steps) > 0 && idRefreshCheck == nil {
t.Error("ID-only refresh check never ran.")
}

// If we have a state, then run the destroy
Expand Down Expand Up @@ -195,6 +239,78 @@ func UnitTest(t TestT, c TestCase) {
Test(t, c)
}

func testIDOnlyRefresh(opts terraform.ContextOpts, r *terraform.ResourceState) error {
// TODO: We guard by this right now so master doesn't explode. We
// need to remove this eventually to make this part of the normal tests.
if os.Getenv("TF_ACC_IDONLY") == "" {
return nil
}

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.
actual := state.RootModule().Resources[name].Primary.Attributes
expected := r.Primary.Attributes
if !reflect.DeepEqual(actual, expected) {
// Determine only the different attributes
for k, v := range expected {
if av, ok := actual[k]; ok && v == av {
delete(expected, k)
delete(actual, k)
}
}

spewConf := spew.NewDefaultConfig()
spewConf.SortKeys = true
return fmt.Errorf(
"Attributes not equivalent. Difference is shown below. Top is actual, bottom is expected."+
"\n\n%s\n\n%s",
spewConf.Sdump(actual), spewConf.Sdump(expected))
}

return nil
}

func testStep(
opts terraform.ContextOpts,
state *terraform.State,
Expand Down
137 changes: 130 additions & 7 deletions helper/resource/testing_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ import (
func init() {
testTesting = true

// TODO: Remove when we remove the guard on id checks
if err := os.Setenv("TF_ACC_IDONLY", "1"); err != nil {
panic(err)
}

if err := os.Setenv(TestEnvVar, "1"); err != nil {
panic(err)
}
Expand All @@ -21,17 +26,23 @@ func TestTest(t *testing.T) {
mp := testProvider()
mp.DiffReturn = nil

mp.ApplyReturn = &terraform.InstanceState{
ID: "foo",
mp.ApplyFn = func(
info *terraform.InstanceInfo,
state *terraform.InstanceState,
diff *terraform.InstanceDiff) (*terraform.InstanceState, error) {
if !diff.Destroy {
return &terraform.InstanceState{
ID: "foo",
}, nil
}

return nil, nil
}

var refreshCount int32
mp.RefreshFn = func(*terraform.InstanceInfo, *terraform.InstanceState) (*terraform.InstanceState, error) {
atomic.AddInt32(&refreshCount, 1)
if atomic.LoadInt32(&refreshCount) == 1 {
return &terraform.InstanceState{ID: "foo"}, nil
} else {
return nil, nil
}
return &terraform.InstanceState{ID: "foo"}, nil
}

checkDestroy := false
Expand Down Expand Up @@ -83,6 +94,118 @@ 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.ApplyFn = func(
info *terraform.InstanceInfo,
state *terraform.InstanceState,
diff *terraform.InstanceDiff) (*terraform.InstanceState, error) {
if !diff.Destroy {
return &terraform.InstanceState{
ID: "foo",
}, nil
}

return nil, nil
}

var refreshCount int32
mp.RefreshFn = func(*terraform.InstanceInfo, *terraform.InstanceState) (*terraform.InstanceState, error) {
atomic.AddInt32(&refreshCount, 1)
return &terraform.InstanceState{ID: "foo"}, 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_idRefreshFail(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.ApplyFn = func(
info *terraform.InstanceInfo,
state *terraform.InstanceState,
diff *terraform.InstanceDiff) (*terraform.InstanceState, error) {
if !diff.Destroy {
return &terraform.InstanceState{
ID: "foo",
}, nil
}

return nil, nil
}

var refreshCount int32
mp.RefreshFn = func(*terraform.InstanceInfo, *terraform.InstanceState) (*terraform.InstanceState, error) {
atomic.AddInt32(&refreshCount, 1)
if atomic.LoadInt32(&refreshCount) == expectedRefresh-1 {
return &terraform.InstanceState{
ID: "foo",
Attributes: map[string]string{"foo": "bar"},
}, nil
} else 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.Fatal("test didn't fail")
}
t.Logf("failure reason: %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 {
Expand Down
13 changes: 13 additions & 0 deletions vendor/github.com/davecgh/go-spew/LICENSE

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit e6a3726

Please sign in to comment.