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

ID-only tests for all resources #6262

Merged
merged 9 commits into from
Apr 20, 2016
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