opentofu/terraform/context_test.go
Martin Atkins 1c0b715999 core: return explicit caption if tests fail to construct context
The previous error was very generic, making it hard to quickly tell from
the test output that the error was during context initialization.
2017-06-09 14:03:59 -07:00

439 lines
7.7 KiB
Go

package terraform
import (
"fmt"
"strings"
"testing"
"time"
"github.com/hashicorp/go-version"
"github.com/hashicorp/terraform/flatmap"
)
func TestNewContextRequiredVersion(t *testing.T) {
cases := []struct {
Name string
Module string
Version string
Value string
Err bool
}{
{
"no requirement",
"",
"0.1.0",
"",
false,
},
{
"doesn't match",
"",
"0.1.0",
"> 0.6.0",
true,
},
{
"matches",
"",
"0.7.0",
"> 0.6.0",
false,
},
{
"module matches",
"context-required-version-module",
"0.5.0",
"",
false,
},
{
"module doesn't match",
"context-required-version-module",
"0.4.0",
"",
true,
},
}
for i, tc := range cases {
t.Run(fmt.Sprintf("%d-%s", i, tc.Name), func(t *testing.T) {
// Reset the version for the tests
old := SemVersion
SemVersion = version.Must(version.NewVersion(tc.Version))
defer func() { SemVersion = old }()
name := "context-required-version"
if tc.Module != "" {
name = tc.Module
}
mod := testModule(t, name)
if tc.Value != "" {
mod.Config().Terraform.RequiredVersion = tc.Value
}
_, err := NewContext(&ContextOpts{
Module: mod,
})
if (err != nil) != tc.Err {
t.Fatalf("err: %s", err)
}
if err != nil {
return
}
})
}
}
func TestNewContextState(t *testing.T) {
cases := map[string]struct {
Input *ContextOpts
Err bool
}{
"empty TFVersion": {
&ContextOpts{
State: &State{},
},
false,
},
"past TFVersion": {
&ContextOpts{
State: &State{TFVersion: "0.1.2"},
},
false,
},
"equal TFVersion": {
&ContextOpts{
State: &State{TFVersion: Version},
},
false,
},
"future TFVersion": {
&ContextOpts{
State: &State{TFVersion: "99.99.99"},
},
true,
},
"future TFVersion, allowed": {
&ContextOpts{
State: &State{TFVersion: "99.99.99"},
StateFutureAllowed: true,
},
false,
},
}
for k, tc := range cases {
ctx, err := NewContext(tc.Input)
if (err != nil) != tc.Err {
t.Fatalf("%s: err: %s", k, err)
}
if err != nil {
continue
}
// Version should always be set to our current
if ctx.state.TFVersion != Version {
t.Fatalf("%s: state not set to current version", k)
}
}
}
func testContext2(t *testing.T, opts *ContextOpts) *Context {
// Enable the shadow graph
opts.Shadow = true
ctx, err := NewContext(opts)
if err != nil {
t.Fatalf("failed to create test context\n\n%s\n", err)
}
return ctx
}
func testDataApplyFn(
info *InstanceInfo,
d *InstanceDiff) (*InstanceState, error) {
return testApplyFn(info, new(InstanceState), d)
}
func testDataDiffFn(
info *InstanceInfo,
c *ResourceConfig) (*InstanceDiff, error) {
return testDiffFn(info, new(InstanceState), c)
}
func testApplyFn(
info *InstanceInfo,
s *InstanceState,
d *InstanceDiff) (*InstanceState, error) {
if d.Destroy {
return nil, nil
}
id := "foo"
if idAttr, ok := d.Attributes["id"]; ok && !idAttr.NewComputed {
id = idAttr.New
}
result := &InstanceState{
ID: id,
Attributes: make(map[string]string),
}
// Copy all the prior attributes
for k, v := range s.Attributes {
result.Attributes[k] = v
}
if d != nil {
result = result.MergeDiff(d)
}
return result, nil
}
func testDiffFn(
info *InstanceInfo,
s *InstanceState,
c *ResourceConfig) (*InstanceDiff, error) {
diff := new(InstanceDiff)
diff.Attributes = make(map[string]*ResourceAttrDiff)
if s != nil {
diff.DestroyTainted = s.Tainted
}
for k, v := range c.Raw {
// Ignore __-prefixed keys since they're used for magic
if k[0] == '_' && k[1] == '_' {
continue
}
if k == "nil" {
return nil, nil
}
// This key is used for other purposes
if k == "compute_value" {
continue
}
if k == "compute" {
attrDiff := &ResourceAttrDiff{
Old: "",
New: "",
NewComputed: true,
}
if cv, ok := c.Config["compute_value"]; ok {
if cv.(string) == "1" {
attrDiff.NewComputed = false
attrDiff.New = fmt.Sprintf("computed_%s", v.(string))
}
}
diff.Attributes[v.(string)] = attrDiff
continue
}
// If this key is not computed, then look it up in the
// cleaned config.
found := false
for _, ck := range c.ComputedKeys {
if ck == k {
found = true
break
}
}
if !found {
v = c.Config[k]
}
for k, attrDiff := range testFlatAttrDiffs(k, v) {
if k == "require_new" {
attrDiff.RequiresNew = true
}
if _, ok := c.Raw["__"+k+"_requires_new"]; ok {
attrDiff.RequiresNew = true
}
if attr, ok := s.Attributes[k]; ok {
attrDiff.Old = attr
}
diff.Attributes[k] = attrDiff
}
}
for _, k := range c.ComputedKeys {
diff.Attributes[k] = &ResourceAttrDiff{
Old: "",
NewComputed: true,
}
}
// If we recreate this resource because it's tainted, we keep all attrs
if !diff.RequiresNew() {
for k, v := range diff.Attributes {
if v.NewComputed {
continue
}
old, ok := s.Attributes[k]
if !ok {
continue
}
if old == v.New {
delete(diff.Attributes, k)
}
}
}
if !diff.Empty() {
diff.Attributes["type"] = &ResourceAttrDiff{
Old: "",
New: info.Type,
}
}
return diff, nil
}
// generate ResourceAttrDiffs for nested data structures in tests
func testFlatAttrDiffs(k string, i interface{}) map[string]*ResourceAttrDiff {
diffs := make(map[string]*ResourceAttrDiff)
// check for strings and empty containers first
switch t := i.(type) {
case string:
diffs[k] = &ResourceAttrDiff{New: t}
return diffs
case map[string]interface{}:
if len(t) == 0 {
diffs[k] = &ResourceAttrDiff{New: ""}
return diffs
}
case []interface{}:
if len(t) == 0 {
diffs[k] = &ResourceAttrDiff{New: ""}
return diffs
}
}
flat := flatmap.Flatten(map[string]interface{}{k: i})
for k, v := range flat {
attrDiff := &ResourceAttrDiff{
Old: "",
New: v,
}
diffs[k] = attrDiff
}
return diffs
}
func testProvider(prefix string) *MockResourceProvider {
p := new(MockResourceProvider)
p.RefreshFn = func(info *InstanceInfo, s *InstanceState) (*InstanceState, error) {
return s, nil
}
p.ResourcesReturn = []ResourceType{
ResourceType{
Name: fmt.Sprintf("%s_instance", prefix),
},
}
return p
}
func testProvisioner() *MockResourceProvisioner {
p := new(MockResourceProvisioner)
return p
}
func checkStateString(t *testing.T, state *State, expected string) {
actual := strings.TrimSpace(state.String())
expected = strings.TrimSpace(expected)
if actual != expected {
t.Fatalf("state does not match! actual:\n%s\n\nexpected:\n%s", actual, expected)
}
}
func resourceState(resourceType, resourceID string) *ResourceState {
return &ResourceState{
Type: resourceType,
Primary: &InstanceState{
ID: resourceID,
},
}
}
// Test helper that gives a function 3 seconds to finish, assumes deadlock and
// fails test if it does not.
func testCheckDeadlock(t *testing.T, f func()) {
timeout := make(chan bool, 1)
done := make(chan bool, 1)
go func() {
time.Sleep(3 * time.Second)
timeout <- true
}()
go func(f func(), done chan bool) {
defer func() { done <- true }()
f()
}(f, done)
select {
case <-timeout:
t.Fatalf("timed out! probably deadlock")
case <-done:
// ok
}
}
const testContextGraph = `
root: root
aws_instance.bar
aws_instance.bar -> provider.aws
aws_instance.foo
aws_instance.foo -> provider.aws
provider.aws
root
root -> aws_instance.bar
root -> aws_instance.foo
`
const testContextRefreshModuleStr = `
aws_instance.web: (tainted)
ID = bar
module.child:
aws_instance.web:
ID = new
`
const testContextRefreshOutputStr = `
aws_instance.web:
ID = foo
foo = bar
Outputs:
foo = bar
`
const testContextRefreshOutputPartialStr = `
<no state>
`
const testContextRefreshTaintedStr = `
aws_instance.web: (tainted)
ID = foo
`