opentofu/internal/command/views/plan_test.go
James Bardin 09dde8d5ed improve the contributing attributes filter
The initial rough implementation contained a bug where it would
incorrectly return a NilVal in some cases.

Improve the heuristics here to insert null values more precisely when
parent objects change to or from null. We also check for dynamic types
changing, in which case the entire object must be taken when we can't
match the individual attribute values.
2022-03-30 10:04:48 -04:00

394 lines
11 KiB
Go

package views
import (
"testing"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/command/arguments"
"github.com/hashicorp/terraform/internal/configs/configschema"
"github.com/hashicorp/terraform/internal/lang/globalref"
"github.com/hashicorp/terraform/internal/plans"
"github.com/hashicorp/terraform/internal/providers"
"github.com/hashicorp/terraform/internal/terminal"
"github.com/hashicorp/terraform/internal/terraform"
"github.com/zclconf/go-cty/cty"
)
// Ensure that the correct view type and in-automation settings propagate to the
// Operation view.
func TestPlanHuman_operation(t *testing.T) {
streams, done := terminal.StreamsForTesting(t)
defer done(t)
v := NewPlan(arguments.ViewHuman, NewView(streams).SetRunningInAutomation(true)).Operation()
if hv, ok := v.(*OperationHuman); !ok {
t.Fatalf("unexpected return type %t", v)
} else if hv.inAutomation != true {
t.Fatalf("unexpected inAutomation value on Operation view")
}
}
// Verify that Hooks includes a UI hook
func TestPlanHuman_hooks(t *testing.T) {
streams, done := terminal.StreamsForTesting(t)
defer done(t)
v := NewPlan(arguments.ViewHuman, NewView(streams).SetRunningInAutomation((true)))
hooks := v.Hooks()
var uiHook *UiHook
for _, hook := range hooks {
if ch, ok := hook.(*UiHook); ok {
uiHook = ch
}
}
if uiHook == nil {
t.Fatalf("expected Hooks to include a UiHook: %#v", hooks)
}
}
// Helper functions to build a trivial test plan, to exercise the plan
// renderer.
func testPlan(t *testing.T) *plans.Plan {
t.Helper()
plannedVal := cty.ObjectVal(map[string]cty.Value{
"id": cty.UnknownVal(cty.String),
"foo": cty.StringVal("bar"),
})
priorValRaw, err := plans.NewDynamicValue(cty.NullVal(plannedVal.Type()), plannedVal.Type())
if err != nil {
t.Fatal(err)
}
plannedValRaw, err := plans.NewDynamicValue(plannedVal, plannedVal.Type())
if err != nil {
t.Fatal(err)
}
changes := plans.NewChanges()
addr := addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "test_resource",
Name: "foo",
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance)
changes.SyncWrapper().AppendResourceInstanceChange(&plans.ResourceInstanceChangeSrc{
Addr: addr,
PrevRunAddr: addr,
ProviderAddr: addrs.AbsProviderConfig{
Provider: addrs.NewDefaultProvider("test"),
Module: addrs.RootModule,
},
ChangeSrc: plans.ChangeSrc{
Action: plans.Create,
Before: priorValRaw,
After: plannedValRaw,
},
})
return &plans.Plan{
Changes: changes,
}
}
func testSchemas() *terraform.Schemas {
provider := testProvider()
return &terraform.Schemas{
Providers: map[addrs.Provider]*terraform.ProviderSchema{
addrs.NewDefaultProvider("test"): provider.ProviderSchema(),
},
}
}
func testProvider() *terraform.MockProvider {
p := new(terraform.MockProvider)
p.ReadResourceFn = func(req providers.ReadResourceRequest) providers.ReadResourceResponse {
return providers.ReadResourceResponse{NewState: req.PriorState}
}
p.GetProviderSchemaResponse = testProviderSchema()
return p
}
func testProviderSchema() *providers.GetProviderSchemaResponse {
return &providers.GetProviderSchemaResponse{
Provider: providers.Schema{
Block: &configschema.Block{},
},
ResourceTypes: map[string]providers.Schema{
"test_resource": {
Block: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"id": {Type: cty.String, Computed: true},
"foo": {Type: cty.String, Optional: true},
},
},
},
},
}
}
func TestFilterRefreshChange(t *testing.T) {
tests := map[string]struct {
paths []cty.Path
before, after, expected cty.Value
}{
"attr was null": {
// nested attr was null
paths: []cty.Path{
cty.GetAttrPath("attr").GetAttr("attr_null_before").GetAttr("b"),
},
before: cty.ObjectVal(map[string]cty.Value{
"attr": cty.ObjectVal(map[string]cty.Value{
"attr_null_before": cty.ObjectVal(map[string]cty.Value{
"a": cty.StringVal("old"),
"b": cty.NullVal(cty.String),
}),
}),
}),
after: cty.ObjectVal(map[string]cty.Value{
"attr": cty.ObjectVal(map[string]cty.Value{
"attr_null_before": cty.ObjectVal(map[string]cty.Value{
"a": cty.StringVal("new"),
"b": cty.StringVal("new"),
}),
}),
}),
expected: cty.ObjectVal(map[string]cty.Value{
"attr": cty.ObjectVal(map[string]cty.Value{
"attr_null_before": cty.ObjectVal(map[string]cty.Value{
// we old picked the change in b
"a": cty.StringVal("old"),
"b": cty.StringVal("new"),
}),
}),
}),
},
"object was null": {
// nested object attrs were null
paths: []cty.Path{
cty.GetAttrPath("attr").GetAttr("obj_null_before").GetAttr("b"),
},
before: cty.ObjectVal(map[string]cty.Value{
"attr": cty.ObjectVal(map[string]cty.Value{
"obj_null_before": cty.NullVal(cty.Object(map[string]cty.Type{
"a": cty.String,
"b": cty.String,
})),
"other": cty.ObjectVal(map[string]cty.Value{
"o": cty.StringVal("old"),
}),
}),
}),
after: cty.ObjectVal(map[string]cty.Value{
"attr": cty.ObjectVal(map[string]cty.Value{
"obj_null_before": cty.ObjectVal(map[string]cty.Value{
"a": cty.StringVal("new"),
"b": cty.StringVal("new"),
}),
"other": cty.ObjectVal(map[string]cty.Value{
"o": cty.StringVal("new"),
}),
}),
}),
expected: cty.ObjectVal(map[string]cty.Value{
"attr": cty.ObjectVal(map[string]cty.Value{
"obj_null_before": cty.ObjectVal(map[string]cty.Value{
// optimally "a" would be null, but we need to take the
// entire object since it was null before.
"a": cty.StringVal("new"),
"b": cty.StringVal("new"),
}),
"other": cty.ObjectVal(map[string]cty.Value{
"o": cty.StringVal("old"),
}),
}),
}),
},
"object becomes null": {
// nested object attr becoming null
paths: []cty.Path{
cty.GetAttrPath("attr").GetAttr("obj_null_after").GetAttr("a"),
},
before: cty.ObjectVal(map[string]cty.Value{
"attr": cty.ObjectVal(map[string]cty.Value{
"obj_null_after": cty.ObjectVal(map[string]cty.Value{
"a": cty.StringVal("old"),
"b": cty.StringVal("old"),
}),
"other": cty.ObjectVal(map[string]cty.Value{
"o": cty.StringVal("old"),
}),
}),
}),
after: cty.ObjectVal(map[string]cty.Value{
"attr": cty.ObjectVal(map[string]cty.Value{
"obj_null_after": cty.NullVal(cty.Object(map[string]cty.Type{
"a": cty.String,
"b": cty.String,
})),
"other": cty.ObjectVal(map[string]cty.Value{
"o": cty.StringVal("new"),
}),
}),
}),
expected: cty.ObjectVal(map[string]cty.Value{
"attr": cty.ObjectVal(map[string]cty.Value{
"obj_null_after": cty.ObjectVal(map[string]cty.Value{
"a": cty.NullVal(cty.String),
"b": cty.StringVal("old"),
}),
"other": cty.ObjectVal(map[string]cty.Value{
"o": cty.StringVal("old"),
}),
}),
}),
},
"dynamic adding values": {
// dynamic gaining values
paths: []cty.Path{
cty.GetAttrPath("attr").GetAttr("after").GetAttr("a"),
},
before: cty.ObjectVal(map[string]cty.Value{
"attr": cty.DynamicVal,
}),
after: cty.ObjectVal(map[string]cty.Value{
"attr": cty.ObjectVal(map[string]cty.Value{
// the entire attr object is taken here because there is
// nothing to compare within the before value
"after": cty.ObjectVal(map[string]cty.Value{
"a": cty.StringVal("new"),
"b": cty.StringVal("new"),
}),
"other": cty.ObjectVal(map[string]cty.Value{
"o": cty.StringVal("new"),
}),
}),
}),
expected: cty.ObjectVal(map[string]cty.Value{
"attr": cty.ObjectVal(map[string]cty.Value{
"after": cty.ObjectVal(map[string]cty.Value{
"a": cty.StringVal("new"),
"b": cty.StringVal("new"),
}),
// "other" is picked up here too this time, because we need
// to take the entire dynamic "attr" value
"other": cty.ObjectVal(map[string]cty.Value{
"o": cty.StringVal("new"),
}),
}),
}),
},
"whole object becomes null": {
// whole object becomes null
paths: []cty.Path{
cty.GetAttrPath("attr").GetAttr("after").GetAttr("a"),
},
before: cty.ObjectVal(map[string]cty.Value{
"attr": cty.ObjectVal(map[string]cty.Value{
"after": cty.ObjectVal(map[string]cty.Value{
"a": cty.StringVal("old"),
"b": cty.StringVal("old"),
}),
}),
}),
after: cty.NullVal(cty.Object(map[string]cty.Type{
"attr": cty.DynamicPseudoType,
})),
// since we have a dynamic type we have to take the entire object
// because the paths may not apply between versions.
expected: cty.NullVal(cty.Object(map[string]cty.Type{
"attr": cty.DynamicPseudoType,
})),
},
"whole object was null": {
// whole object was null
paths: []cty.Path{
cty.GetAttrPath("attr").GetAttr("after").GetAttr("a"),
},
before: cty.NullVal(cty.Object(map[string]cty.Type{
"attr": cty.DynamicPseudoType,
})),
after: cty.ObjectVal(map[string]cty.Value{
"attr": cty.ObjectVal(map[string]cty.Value{
"after": cty.ObjectVal(map[string]cty.Value{
"a": cty.StringVal("new"),
"b": cty.StringVal("new"),
}),
}),
}),
expected: cty.ObjectVal(map[string]cty.Value{
"attr": cty.ObjectVal(map[string]cty.Value{
"after": cty.ObjectVal(map[string]cty.Value{
"a": cty.StringVal("new"),
"b": cty.StringVal("new"),
}),
}),
}),
},
"restructured dynamic": {
// dynamic value changing structure significantly
paths: []cty.Path{
cty.GetAttrPath("attr").GetAttr("list").IndexInt(1).GetAttr("a"),
},
before: cty.ObjectVal(map[string]cty.Value{
"attr": cty.ObjectVal(map[string]cty.Value{
"list": cty.ListVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"a": cty.StringVal("old"),
}),
}),
}),
}),
after: cty.ObjectVal(map[string]cty.Value{
"attr": cty.ObjectVal(map[string]cty.Value{
"after": cty.ObjectVal(map[string]cty.Value{
"a": cty.StringVal("new"),
"b": cty.StringVal("new"),
}),
}),
}),
// the path does not apply at all to the new object, so we must
// take all the changes
expected: cty.ObjectVal(map[string]cty.Value{
"attr": cty.ObjectVal(map[string]cty.Value{
"after": cty.ObjectVal(map[string]cty.Value{
"a": cty.StringVal("new"),
"b": cty.StringVal("new"),
}),
}),
}),
},
}
for k, tc := range tests {
t.Run(k, func(t *testing.T) {
addr, diags := addrs.ParseAbsResourceInstanceStr("test_resource.a")
if diags != nil {
t.Fatal(diags.ErrWithWarnings())
}
change := &plans.ResourceInstanceChange{
Addr: addr,
Change: plans.Change{
Before: tc.before,
After: tc.after,
Action: plans.Update,
},
}
var contributing []globalref.ResourceAttr
for _, p := range tc.paths {
contributing = append(contributing, globalref.ResourceAttr{
Resource: addr,
Attr: p,
})
}
res := filterRefreshChange(change, contributing)
if !res.After.RawEquals(tc.expected) {
t.Errorf("\nexpected: %#v\ngot: %#v\n", tc.expected, res.After)
}
})
}
}