replace testDiffFn and testApplyFn

Replace the old mock provider test functions with modern equivalents.
There were a lot of inconsistencies in how they were used, so we needed
to update a lot of tests to match the correct behavior.
This commit is contained in:
James Bardin 2020-10-08 08:56:03 -04:00
parent ca7b5bc28b
commit 0a6853a3f8
9 changed files with 590 additions and 1177 deletions

File diff suppressed because it is too large Load Diff

View File

@ -30,7 +30,14 @@ func TestContext2Input_provider(t *testing.T) {
},
},
ResourceTypes: map[string]*configschema.Block{
"aws_instance": {},
"aws_instance": {
Attributes: map[string]*configschema.Attribute{
"id": {
Type: cty.String,
Computed: true,
},
},
},
},
}
@ -98,7 +105,14 @@ func TestContext2Input_providerMulti(t *testing.T) {
},
},
ResourceTypes: map[string]*configschema.Block{
"aws_instance": {},
"aws_instance": {
Attributes: map[string]*configschema.Attribute{
"id": {
Type: cty.String,
Computed: true,
},
},
},
},
}
@ -159,25 +173,6 @@ func TestContext2Input_providerOnce(t *testing.T) {
},
})
//count := 0
/*p.InputFn = func(i UIInput, c *ResourceConfig) (*ResourceConfig, error) {
count++
_, set := c.Config["from_input"]
if count == 1 {
if set {
return nil, errors.New("from_input should not be set")
}
c.Config["from_input"] = "x"
}
if count > 1 && !set {
return nil, errors.New("from_input should be set")
}
return c, nil
}*/
if diags := ctx.Input(InputModeStd); diags.HasErrors() {
t.Fatalf("input errors: %s", diags.Err())
}
@ -202,7 +197,14 @@ func TestContext2Input_providerId(t *testing.T) {
},
},
ResourceTypes: map[string]*configschema.Block{
"aws_instance": {},
"aws_instance": {
Attributes: map[string]*configschema.Attribute{
"id": {
Type: cty.String,
Computed: true,
},
},
},
},
}

File diff suppressed because it is too large Load Diff

View File

@ -726,6 +726,7 @@ func TestContext2Refresh_output(t *testing.T) {
"foo": {
Type: cty.String,
Optional: true,
Computed: true,
},
},
},

View File

@ -5,16 +5,13 @@ import (
"bytes"
"fmt"
"io/ioutil"
"log"
"os"
"path/filepath"
"sort"
"strconv"
"strings"
"testing"
"time"
"github.com/davecgh/go-spew/spew"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/hashicorp/go-version"
@ -22,7 +19,6 @@ import (
"github.com/hashicorp/terraform/configs/configload"
"github.com/hashicorp/terraform/configs/configschema"
"github.com/hashicorp/terraform/configs/hcl2shim"
"github.com/hashicorp/terraform/flatmap"
"github.com/hashicorp/terraform/plans"
"github.com/hashicorp/terraform/plans/planfile"
"github.com/hashicorp/terraform/providers"
@ -132,300 +128,98 @@ func testContext2(t *testing.T, opts *ContextOpts) *Context {
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
func testApplyFn(req providers.ApplyResourceChangeRequest) (resp providers.ApplyResourceChangeResponse) {
resp.NewState = req.PlannedState
if req.PlannedState.IsNull() {
resp.NewState = cty.NullVal(req.PriorState.Type())
return
}
// find the OLD id, which is probably in the ID field for now, but eventually
// ID should only be in one place.
id := s.ID
if id == "" {
id = s.Attributes["id"]
}
if idAttr, ok := d.Attributes["id"]; ok && !idAttr.NewComputed {
id = idAttr.New
planned := req.PlannedState.AsValueMap()
if planned == nil {
planned = map[string]cty.Value{}
}
if id == "" || id == hcl2shim.UnknownVariableValue {
id = "foo"
id, ok := planned["id"]
if !ok || id.IsNull() || !id.IsKnown() {
planned["id"] = cty.StringVal("foo")
}
result := &InstanceState{
ID: id,
Attributes: make(map[string]string),
// our default schema has a computed "type" attr
if ty, ok := planned["type"]; ok && !ty.IsNull() {
planned["type"] = cty.StringVal(req.TypeName)
}
// Copy all the prior attributes
for k, v := range s.Attributes {
result.Attributes[k] = v
if cmp, ok := planned["compute"]; ok && !cmp.IsNull() {
computed := cmp.AsString()
if val, ok := planned[computed]; ok && !val.IsKnown() {
planned[computed] = cty.StringVal("computed_value")
}
}
if d != nil {
result = result.MergeDiff(d)
for k, v := range planned {
if k == "unknown" {
// "unknown" should cause an error
continue
}
// The id attribute always matches ID for the sake of this mock
// implementation, since it's following the pre-0.12 assumptions where
// these two were treated as synonyms.
result.Attributes["id"] = result.ID
return result, nil
}
func testDiffFn(
info *InstanceInfo,
s *InstanceState,
c *ResourceConfig) (*InstanceDiff, error) {
diff := new(InstanceDiff)
diff.Attributes = make(map[string]*ResourceAttrDiff)
defer func() {
log.Printf("[TRACE] testDiffFn: generated diff is:\n%s", spew.Sdump(diff))
}()
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] == '_' {
// ...though we do still need to include them in the diff, to
// simulate normal provider behaviors.
old := s.Attributes[k]
var new string
switch tv := v.(type) {
case string:
new = tv
if !v.IsKnown() {
switch k {
case "type":
planned[k] = cty.StringVal(req.TypeName)
default:
new = fmt.Sprintf("%#v", v)
}
if new == hcl2shim.UnknownVariableValue {
diff.Attributes[k] = &ResourceAttrDiff{
Old: old,
New: "",
NewComputed: true,
}
} else {
diff.Attributes[k] = &ResourceAttrDiff{
Old: old,
New: new,
}
}
continue
}
if k == "nil" {
return nil, nil
}
// This key is used for other purposes
if k == "compute_value" {
if old, ok := s.Attributes["compute_value"]; !ok || old != v.(string) {
diff.Attributes["compute_value"] = &ResourceAttrDiff{
Old: old,
New: v.(string),
}
}
continue
}
if k == "compute" {
// The "compute" value itself must be included in the diff if it
// has changed since prior.
if old, ok := s.Attributes["compute"]; !ok || old != v.(string) {
diff.Attributes["compute"] = &ResourceAttrDiff{
Old: old,
New: v.(string),
}
}
if v == hcl2shim.UnknownVariableValue || v == "unknown" {
// compute wasn't set in the config, so don't use these
// computed values from the schema.
delete(c.Raw, k)
delete(c.Raw, "compute_value")
// we need to remove this from the list of ComputedKeys too,
// since it would get re-added to the diff further down
newComputed := make([]string, 0, len(c.ComputedKeys))
for _, ck := range c.ComputedKeys {
if ck == "compute" || ck == "compute_value" {
continue
}
newComputed = append(newComputed, ck)
}
c.ComputedKeys = newComputed
if v == "unknown" {
diff.Attributes["unknown"] = &ResourceAttrDiff{
Old: "",
New: "",
NewComputed: true,
}
c.ComputedKeys = append(c.ComputedKeys, "unknown")
}
continue
}
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) {
// we need to ignore 'id' for now, since it's always inferred to be
// computed.
if k == "id" {
continue
}
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 {
if k == "id" {
continue
}
old := ""
if s != nil {
old = s.Attributes[k]
}
diff.Attributes[k] = &ResourceAttrDiff{
Old: 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)
planned[k] = cty.NullVal(v.Type())
}
}
}
if !diff.Empty() {
diff.Attributes["type"] = &ResourceAttrDiff{
Old: "",
New: info.Type,
}
if s != nil && s.Attributes != nil {
diff.Attributes["type"].Old = s.Attributes["type"]
}
}
return diff, nil
resp.NewState = cty.ObjectVal(planned)
return
}
// 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
func testDiffFn(req providers.PlanResourceChangeRequest) (resp providers.PlanResourceChangeResponse) {
var planned map[string]cty.Value
if !req.ProposedNewState.IsNull() {
planned = req.ProposedNewState.AsValueMap()
}
case []interface{}:
if len(t) == 0 {
diffs[k] = &ResourceAttrDiff{New: ""}
return diffs
if planned == nil {
planned = map[string]cty.Value{}
}
// id is always computed for the tests
if id, ok := planned["id"]; ok && id.IsNull() {
planned["id"] = cty.UnknownVal(cty.String)
}
// the old tests have require_new replace on every plan
if _, ok := planned["require_new"]; ok {
resp.RequiresReplace = append(resp.RequiresReplace, cty.Path{cty.GetAttrStep{Name: "require_new"}})
}
for k := range planned {
requiresNewKey := "__" + k + "_requires_new"
_, ok := planned[requiresNewKey]
if ok {
resp.RequiresReplace = append(resp.RequiresReplace, cty.Path{cty.GetAttrStep{Name: requiresNewKey}})
}
}
flat := flatmap.Flatten(map[string]interface{}{k: i})
for k, v := range flat {
attrDiff := &ResourceAttrDiff{
Old: "",
New: v,
if v, ok := planned["compute"]; ok && !v.IsNull() {
k := v.AsString()
unknown := cty.UnknownVal(cty.String)
if strings.HasSuffix(k, ".#") {
k = k[:len(k)-2]
unknown = cty.UnknownVal(cty.List(cty.String))
}
diffs[k] = attrDiff
planned[k] = unknown
}
// The legacy flatmap-based diff producing done by helper/schema would
// additionally insert a k+".%" key here recording the length of the map,
// which is for some reason not also done by flatmap.Flatten. To make our
// mock shims helper/schema-compatible, we'll just fake that up here.
switch t := i.(type) {
case map[string]interface{}:
attrDiff := &ResourceAttrDiff{
Old: "",
New: strconv.Itoa(len(t)),
}
diffs[k+".%"] = attrDiff
if t, ok := planned["type"]; ok && t.IsNull() {
planned["type"] = cty.UnknownVal(cty.String)
}
return diffs
resp.PlannedState = cty.ObjectVal(planned)
return
}
func testProvider(prefix string) *MockProvider {

View File

@ -2,7 +2,6 @@ package terraform
import (
"encoding/json"
"fmt"
"sync"
"github.com/zclconf/go-cty/cty"
@ -89,14 +88,12 @@ type MockProvider struct {
CloseCalled bool
CloseError error
// Legacy callbacks: if these are set, we will shim incoming calls for
// new-style methods to these old-fashioned terraform.ResourceProvider
// mock callbacks, for the benefit of older tests that were written against
// the old mock API.
ValidateFn func(c *ResourceConfig) (ws []string, es []error)
ConfigureFn func(c *ResourceConfig) error
DiffFn func(info *InstanceInfo, s *InstanceState, c *ResourceConfig) (*InstanceDiff, error)
ApplyFn func(info *InstanceInfo, s *InstanceState, d *InstanceDiff) (*InstanceState, error)
//ValidateFn func(providers.ValidateResourceTypeConfigRequest) providers.ValidateResourceTypeConfigResponse
//ConfigureFn func(providers.ConfigureRequest) providers.ConfigureResponse
DiffFn func(providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse
ApplyFn func(providers.ApplyResourceChangeRequest) providers.ApplyResourceChangeResponse
}
func (p *MockProvider) GetSchema() providers.GetSchemaResponse {
@ -297,47 +294,7 @@ func (p *MockProvider) PlanResourceChange(r providers.PlanResourceChangeRequest)
p.PlanResourceChangeRequest = r
if p.DiffFn != nil {
ps := p.getSchema()
if ps.ResourceTypes == nil || ps.ResourceTypes[r.TypeName].Block == nil {
return providers.PlanResourceChangeResponse{
Diagnostics: tfdiags.Diagnostics(nil).Append(fmt.Printf("mock provider has no schema for resource type %s", r.TypeName)),
}
}
schema := ps.ResourceTypes[r.TypeName].Block
info := &InstanceInfo{
Type: r.TypeName,
}
priorState := NewInstanceStateShimmedFromValue(r.PriorState, 0)
cfg := NewResourceConfigShimmed(r.ProposedNewState, schema)
legacyDiff, err := p.DiffFn(info, priorState, cfg)
var res providers.PlanResourceChangeResponse
res.PlannedState = r.ProposedNewState
if err != nil {
res.Diagnostics = res.Diagnostics.Append(err)
}
if legacyDiff != nil {
newVal, err := legacyDiff.ApplyToValue(r.PriorState, schema)
if err != nil {
res.Diagnostics = res.Diagnostics.Append(err)
}
res.PlannedState = newVal
var requiresNew []string
for attr, d := range legacyDiff.Attributes {
if d.RequiresNew {
requiresNew = append(requiresNew, attr)
}
}
requiresReplace, err := hcl2shim.RequiresReplace(requiresNew, schema.ImpliedType())
if err != nil {
res.Diagnostics = res.Diagnostics.Append(err)
}
res.RequiresReplace = requiresReplace
}
return res
return p.DiffFn(r)
}
if p.PlanResourceChangeFn != nil {
return p.PlanResourceChangeFn(r)
@ -353,90 +310,7 @@ func (p *MockProvider) ApplyResourceChange(r providers.ApplyResourceChangeReques
p.Unlock()
if p.ApplyFn != nil {
// ApplyFn is a special callback fashioned after our old provider
// interface, which expected to be given an actual diff rather than
// separate old/new values to apply. Therefore we need to approximate
// a diff here well enough that _most_ of our legacy ApplyFns in old
// tests still see the behavior they are expecting. New tests should
// not use this, and should instead use ApplyResourceChangeFn directly.
providerSchema := p.getSchema()
schema, ok := providerSchema.ResourceTypes[r.TypeName]
if !ok {
return providers.ApplyResourceChangeResponse{
Diagnostics: tfdiags.Diagnostics(nil).Append(fmt.Errorf("no mocked schema available for resource type %s", r.TypeName)),
}
}
info := &InstanceInfo{
Type: r.TypeName,
}
priorVal := r.PriorState
plannedVal := r.PlannedState
priorMap := hcl2shim.FlatmapValueFromHCL2(priorVal)
plannedMap := hcl2shim.FlatmapValueFromHCL2(plannedVal)
s := NewInstanceStateShimmedFromValue(priorVal, 0)
d := &InstanceDiff{
Attributes: make(map[string]*ResourceAttrDiff),
}
if plannedMap == nil { // destroying, then
d.Destroy = true
// Destroy diffs don't have any attribute diffs
} else {
if priorMap == nil { // creating, then
// We'll just make an empty prior map to make things easier below.
priorMap = make(map[string]string)
}
for k, new := range plannedMap {
old := priorMap[k]
newComputed := false
if new == hcl2shim.UnknownVariableValue {
new = ""
newComputed = true
}
d.Attributes[k] = &ResourceAttrDiff{
Old: old,
New: new,
NewComputed: newComputed,
Type: DiffAttrInput, // not generally used in tests, so just hard-coded
}
}
// Also need any attributes that were removed in "planned"
for k, old := range priorMap {
if _, ok := plannedMap[k]; ok {
continue
}
d.Attributes[k] = &ResourceAttrDiff{
Old: old,
NewRemoved: true,
Type: DiffAttrInput,
}
}
}
newState, err := p.ApplyFn(info, s, d)
resp := providers.ApplyResourceChangeResponse{}
if err != nil {
resp.Diagnostics = resp.Diagnostics.Append(err)
}
if newState != nil {
var newVal cty.Value
if newState != nil {
var err error
newVal, err = newState.AttrsAsObjectValue(schema.Block.ImpliedType())
if err != nil {
resp.Diagnostics = resp.Diagnostics.Append(err)
}
} else {
// If apply returned a nil new state then that's the old way to
// indicate that the object was destroyed. Our new interface calls
// for that to be signalled as a null value.
newVal = cty.NullVal(schema.Block.ImpliedType())
}
resp.NewState = newVal
}
return resp
return p.ApplyFn(r)
}
if p.ApplyResourceChangeFn != nil {
return p.ApplyResourceChangeFn(r)

View File

@ -414,12 +414,15 @@ aws_instance.bar:
aws_instance.foo.0:
ID = foo
provider = provider["registry.terraform.io/hashicorp/aws"]
type = aws_instance
aws_instance.foo.1:
ID = foo
provider = provider["registry.terraform.io/hashicorp/aws"]
type = aws_instance
aws_instance.foo.2:
ID = foo
provider = provider["registry.terraform.io/hashicorp/aws"]
type = aws_instance
`
const testTerraformApplyProviderAliasStr = `
@ -439,9 +442,11 @@ const testTerraformApplyProviderAliasConfigStr = `
another_instance.bar:
ID = foo
provider = provider["registry.terraform.io/hashicorp/another"].two
type = another_instance
another_instance.foo:
ID = foo
provider = provider["registry.terraform.io/hashicorp/another"]
type = another_instance
`
const testTerraformApplyEmptyModuleStr = `
@ -487,6 +492,7 @@ const testTerraformApplyCancelStr = `
aws_instance.foo:
ID = foo
provider = provider["registry.terraform.io/hashicorp/aws"]
type = aws_instance
value = 2
`
@ -590,9 +596,11 @@ aws_instance.bar:
aws_instance.foo.0:
ID = foo
provider = provider["registry.terraform.io/hashicorp/aws"]
type = aws_instance
aws_instance.foo.1:
ID = foo
provider = provider["registry.terraform.io/hashicorp/aws"]
type = aws_instance
`
const testTerraformApplyForEachVariableStr = `
aws_instance.foo["b15c6d616d6143248c575900dff57325eb1de498"]:
@ -613,18 +621,22 @@ aws_instance.foo["e30a7edcc42a846684f2a4eea5f3cd261d33c46d"]:
aws_instance.one["a"]:
ID = foo
provider = provider["registry.terraform.io/hashicorp/aws"]
type = aws_instance
aws_instance.one["b"]:
ID = foo
provider = provider["registry.terraform.io/hashicorp/aws"]
type = aws_instance
aws_instance.two["a"]:
ID = foo
provider = provider["registry.terraform.io/hashicorp/aws"]
type = aws_instance
Dependencies:
aws_instance.one
aws_instance.two["b"]:
ID = foo
provider = provider["registry.terraform.io/hashicorp/aws"]
type = aws_instance
Dependencies:
aws_instance.one`
@ -632,9 +644,11 @@ const testTerraformApplyMinimalStr = `
aws_instance.bar:
ID = foo
provider = provider["registry.terraform.io/hashicorp/aws"]
type = aws_instance
aws_instance.foo:
ID = foo
provider = provider["registry.terraform.io/hashicorp/aws"]
type = aws_instance
`
const testTerraformApplyModuleStr = `
@ -688,9 +702,11 @@ module.child:
aws_instance.foo:
ID = foo
provider = provider["registry.terraform.io/hashicorp/aws"]
type = aws_instance
test_instance.foo:
ID = foo
provider = provider["registry.terraform.io/hashicorp/test"]
type = test_instance
`
const testTerraformApplyModuleProviderAliasStr = `
@ -699,6 +715,7 @@ module.child:
aws_instance.foo:
ID = foo
provider = module.child.provider["registry.terraform.io/hashicorp/aws"].eu
type = aws_instance
`
const testTerraformApplyModuleVarRefExistingStr = `
@ -706,6 +723,7 @@ aws_instance.foo:
ID = foo
provider = provider["registry.terraform.io/hashicorp/aws"]
foo = bar
type = aws_instance
module.child:
aws_instance.foo:
@ -733,6 +751,7 @@ const testTerraformApplyProvisionerStr = `
aws_instance.bar:
ID = foo
provider = provider["registry.terraform.io/hashicorp/aws"]
type = aws_instance
Dependencies:
aws_instance.foo
@ -752,12 +771,14 @@ module.child:
aws_instance.bar:
ID = foo
provider = provider["registry.terraform.io/hashicorp/aws"]
type = aws_instance
`
const testTerraformApplyProvisionerFailStr = `
aws_instance.bar: (tainted)
ID = foo
provider = provider["registry.terraform.io/hashicorp/aws"]
type = aws_instance
aws_instance.foo:
ID = foo
provider = provider["registry.terraform.io/hashicorp/aws"]
@ -769,6 +790,7 @@ const testTerraformApplyProvisionerFailCreateStr = `
aws_instance.bar: (tainted)
ID = foo
provider = provider["registry.terraform.io/hashicorp/aws"]
type = aws_instance
`
const testTerraformApplyProvisionerFailCreateNoIdStr = `
@ -850,14 +872,16 @@ const testTerraformApplyDestroyStr = `
const testTerraformApplyErrorStr = `
aws_instance.bar: (tainted)
ID = bar
ID =
provider = provider["registry.terraform.io/hashicorp/aws"]
foo = 2
Dependencies:
aws_instance.foo
aws_instance.foo:
ID = foo
provider = provider["registry.terraform.io/hashicorp/aws"]
type = aws_instance
value = 2
`
@ -866,6 +890,7 @@ aws_instance.bar:
ID = bar
provider = provider["registry.terraform.io/hashicorp/aws"]
require_new = abc
type = aws_instance
`
const testTerraformApplyErrorDestroyCreateBeforeDestroyStr = `
@ -881,12 +906,14 @@ const testTerraformApplyErrorPartialStr = `
aws_instance.bar:
ID = bar
provider = provider["registry.terraform.io/hashicorp/aws"]
type = aws_instance
Dependencies:
aws_instance.foo
aws_instance.foo:
ID = foo
provider = provider["registry.terraform.io/hashicorp/aws"]
type = aws_instance
value = 2
`
@ -1107,7 +1134,6 @@ const testTerraformApplyUnknownAttrStr = `
aws_instance.foo: (tainted)
ID = foo
provider = provider["registry.terraform.io/hashicorp/aws"]
compute = unknown
num = 2
type = aws_instance
`

View File

@ -1,7 +1,11 @@
variable "require_new" {
type = string
}
resource "aws_instance" "web" {
// require_new is a special attribute recognized by testDiffFn that forces
// a new resource on every apply
require_new = "yes"
require_new = var.require_new
lifecycle {
create_before_destroy = true
}

View File

@ -1,4 +1,3 @@
resource "aws_instance" "foo" {
num = "2"
compute = "unknown"
}