blob: 0c0491197a66a5ec87b7b24cc48c59fea09f22dc [file] [log] [blame]
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package stackeval
import (
"context"
"strings"
"testing"
"github.com/davecgh/go-spew/spew"
"github.com/google/go-cmp/cmp"
"github.com/zclconf/go-cty-debug/ctydebug"
"github.com/zclconf/go-cty/cty"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/instances"
"github.com/hashicorp/terraform/internal/stacks/stackaddrs"
"github.com/hashicorp/terraform/internal/tfdiags"
)
// TestComponentInstances is a test of the [Component.CheckInstances] function.
//
// In particular, note that it's _not_ a test of the [ComponentInstance] type
// as a whole, although [Component.CheckInstances] does return a collection of
// those so there is _some_ coverage of that in here.
func TestComponentCheckInstances(t *testing.T) {
getComponent := func(ctx context.Context, main *Main) *Component {
mainStack := main.MainStack()
component := mainStack.Component(stackaddrs.Component{Name: "foo"})
if component == nil {
t.Fatal("component.foo does not exist, but it should exist")
}
return component
}
subtestInPromisingTask(t, "single instance", func(ctx context.Context, t *testing.T) {
cfg := testStackConfig(t, "component", "single_instance")
main := testEvaluator(t, testEvaluatorOpts{
Config: cfg,
TestOnlyGlobals: map[string]cty.Value{
"component_inputs": cty.EmptyObjectVal,
},
})
component := getComponent(ctx, main)
forEachVal, diags := component.CheckForEachValue(ctx, InspectPhase)
assertNoDiags(t, diags)
if forEachVal != cty.NilVal {
t.Fatalf("unexpected for_each value\ngot: %#v\nwant: cty.NilVal", forEachVal)
}
insts, unknown, diags := component.CheckInstances(ctx, InspectPhase)
assertNoDiags(t, diags)
assertFalse(t, unknown)
if got, want := len(insts), 1; got != want {
t.Fatalf("wrong number of instances %d; want %d\n%#v", got, want, insts)
}
inst, ok := insts[addrs.NoKey]
if !ok {
t.Fatalf("missing expected addrs.NoKey instance\n%s", spew.Sdump(insts))
}
if diff := cmp.Diff(instances.RepetitionData{}, inst.RepetitionData(), ctydebug.CmpOptions); diff != "" {
t.Errorf("wrong repetition data\n%s", diff)
}
})
t.Run("for_each", func(t *testing.T) {
cfg := testStackConfig(t, "component", "for_each")
subtestInPromisingTask(t, "no instances", func(ctx context.Context, t *testing.T) {
main := testEvaluator(t, testEvaluatorOpts{
Config: cfg,
TestOnlyGlobals: map[string]cty.Value{
"component_instances": cty.MapValEmpty(cty.EmptyObject),
},
})
component := getComponent(ctx, main)
forEachVal, diags := component.CheckForEachValue(ctx, InspectPhase)
assertNoDiags(t, diags)
if got, want := forEachVal, cty.MapValEmpty(cty.EmptyObject); !want.RawEquals(got) {
t.Fatalf("unexpected for_each value\ngot: %#v\nwant: %#v", got, want)
}
insts, unknown, diags := component.CheckInstances(ctx, InspectPhase)
assertNoDiags(t, diags)
assertFalse(t, unknown)
if got, want := len(insts), 0; got != want {
t.Fatalf("wrong number of instances %d; want %d\n%#v", got, want, insts)
}
// For this particular function we take the unusual approach of
// distinguishing between a nil map and a non-nil empty map so
// we can distinguish between "definitely no instances" (this case)
// and "we don't know how many instances there are" (tested in other
// subtests of this test, below.)
if insts == nil {
t.Error("CheckInstances result is nil; should be non-nil empty map")
}
})
subtestInPromisingTask(t, "two instances", func(ctx context.Context, t *testing.T) {
wantForEachVal := cty.MapVal(map[string]cty.Value{
"a": cty.ObjectVal(map[string]cty.Value{
"test_string": cty.StringVal("in a"),
}),
"b": cty.ObjectVal(map[string]cty.Value{
"test_string": cty.StringVal("in b"),
}),
})
main := testEvaluator(t, testEvaluatorOpts{
Config: cfg,
TestOnlyGlobals: map[string]cty.Value{
"component_instances": wantForEachVal,
},
})
component := getComponent(ctx, main)
gotForEachVal, diags := component.CheckForEachValue(ctx, InspectPhase)
assertNoDiags(t, diags)
if !wantForEachVal.RawEquals(gotForEachVal) {
t.Fatalf("unexpected for_each value\ngot: %#v\nwant: %#v", gotForEachVal, wantForEachVal)
}
insts, unknown, diags := component.CheckInstances(ctx, InspectPhase)
assertNoDiags(t, diags)
assertFalse(t, unknown)
if got, want := len(insts), 2; got != want {
t.Fatalf("wrong number of instances %d; want %d\n%#v", got, want, insts)
}
t.Run("instance a", func(t *testing.T) {
inst, ok := insts[addrs.StringKey("a")]
if !ok {
t.Fatalf("missing expected addrs.StringKey(\"a\") instance\n%s", spew.Sdump(insts))
}
wantRepData := instances.RepetitionData{
EachKey: cty.StringVal("a"),
EachValue: cty.ObjectVal(map[string]cty.Value{
"test_string": cty.StringVal("in a"),
}),
}
if diff := cmp.Diff(wantRepData, inst.RepetitionData(), ctydebug.CmpOptions); diff != "" {
t.Errorf("wrong repetition data\n%s", diff)
}
})
t.Run("instance b", func(t *testing.T) {
inst, ok := insts[addrs.StringKey("b")]
if !ok {
t.Fatalf("missing expected addrs.StringKey(\"b\") instance\n%s", spew.Sdump(insts))
}
wantRepData := instances.RepetitionData{
EachKey: cty.StringVal("b"),
EachValue: cty.ObjectVal(map[string]cty.Value{
"test_string": cty.StringVal("in b"),
}),
}
if diff := cmp.Diff(wantRepData, inst.RepetitionData(), ctydebug.CmpOptions); diff != "" {
t.Errorf("wrong repetition data\n%s", diff)
}
})
})
subtestInPromisingTask(t, "null", func(ctx context.Context, t *testing.T) {
main := testEvaluator(t, testEvaluatorOpts{
Config: cfg,
TestOnlyGlobals: map[string]cty.Value{
"component_instances": cty.NullVal(cty.Map(cty.EmptyObject)),
},
})
component := getComponent(ctx, main)
gotVal, diags := component.CheckForEachValue(ctx, InspectPhase)
assertMatchingDiag(t, diags, func(diag tfdiags.Diagnostic) bool {
return diag.Severity() == tfdiags.Error && strings.Contains(diag.Description().Detail, "The for_each expression produced a null value")
})
wantVal := cty.DynamicVal // placeholder for invalid result
if !wantVal.RawEquals(gotVal) {
t.Errorf("wrong result\ngot: %#v\nwant: %#v", gotVal, wantVal)
}
})
subtestInPromisingTask(t, "string", func(ctx context.Context, t *testing.T) {
main := testEvaluator(t, testEvaluatorOpts{
Config: cfg,
TestOnlyGlobals: map[string]cty.Value{
"component_instances": cty.StringVal("nope"),
},
})
component := getComponent(ctx, main)
gotVal, diags := component.CheckForEachValue(ctx, InspectPhase)
assertMatchingDiag(t, diags, func(diag tfdiags.Diagnostic) bool {
return (diag.Severity() == tfdiags.Error &&
diag.Description().Detail == "The for_each expression must produce either a map of any type or a set of strings. The keys of the map or the set elements will serve as unique identifiers for multiple instances of this component.")
})
wantVal := cty.DynamicVal // placeholder for invalid result
if !wantVal.RawEquals(gotVal) {
t.Errorf("wrong result\ngot: %#v\nwant: %#v", gotVal, wantVal)
}
// When the for_each expression is invalid, CheckInstances should
// return nil and diagnostics.
gotInsts, unknown, diags := component.CheckInstances(ctx, InspectPhase)
assertFalse(t, unknown)
if gotInsts != nil {
t.Fatalf("unexpected instances\ngot: %#v\nwant: nil", gotInsts)
}
assertMatchingDiag(t, diags, func(diag tfdiags.Diagnostic) bool {
return (diag.Severity() == tfdiags.Error &&
diag.Description().Detail == "The for_each expression must produce either a map of any type or a set of strings. The keys of the map or the set elements will serve as unique identifiers for multiple instances of this component.")
})
})
subtestInPromisingTask(t, "unknown", func(ctx context.Context, t *testing.T) {
main := testEvaluator(t, testEvaluatorOpts{
Config: cfg,
TestOnlyGlobals: map[string]cty.Value{
"component_instances": cty.UnknownVal(cty.Map(cty.EmptyObject)),
},
})
component := getComponent(ctx, main)
gotVal, diags := component.CheckForEachValue(ctx, InspectPhase)
assertNoDiags(t, diags)
wantVal := cty.UnknownVal(cty.Map(cty.EmptyObject))
if !wantVal.RawEquals(gotVal) {
t.Errorf("wrong result\ngot: %#v\nwant: %#v", gotVal, wantVal)
}
// When the for_each expression is unknown, CheckInstances should
// return a single instance with dynamic values in the repetition data.
gotInsts, unknown, diags := component.CheckInstances(ctx, InspectPhase)
assertNoDiags(t, diags)
assertTrue(t, unknown)
if got, want := len(gotInsts), 0; got != want {
t.Fatalf("wrong number of instances %d; want %d\n%#v", got, want, gotInsts)
}
})
})
}
func TestComponentResultValue(t *testing.T) {
getComponent := func(ctx context.Context, t *testing.T, main *Main) *Component {
mainStack := main.MainStack()
component := mainStack.Component(stackaddrs.Component{Name: "foo"})
if component == nil {
t.Fatal("component.foo does not exist, but it should exist")
}
return component
}
subtestInPromisingTask(t, "single instance", func(ctx context.Context, t *testing.T) {
cfg := testStackConfig(t, "component", "single_instance")
main := testEvaluator(t, testEvaluatorOpts{
Config: cfg,
TestOnlyGlobals: map[string]cty.Value{
"child_stack_inputs": cty.ObjectVal(map[string]cty.Value{
"test": cty.StringVal("hello"),
}),
},
})
component := getComponent(ctx, t, main)
got := component.ResultValue(ctx, InspectPhase)
want := cty.ObjectVal(map[string]cty.Value{
// FIXME: This currently returns an unknown value because we
// aren't tracking component output values in prior state.
// Once we fix that, we should see an output value called "test"
// here.
"test": cty.DynamicVal,
})
if diff := cmp.Diff(want, got, ctydebug.CmpOptions); diff != "" {
t.Fatalf("wrong result\n%s", diff)
}
})
t.Run("for_each", func(t *testing.T) {
cfg := testStackConfig(t, "component", "for_each")
subtestInPromisingTask(t, "no instances", func(ctx context.Context, t *testing.T) {
main := testEvaluator(t, testEvaluatorOpts{
Config: cfg,
TestOnlyGlobals: map[string]cty.Value{
"component_instances": cty.MapValEmpty(cty.EmptyObject),
},
})
component := getComponent(ctx, t, main)
got := component.ResultValue(ctx, InspectPhase)
want := cty.EmptyObjectVal
if diff := cmp.Diff(want, got, ctydebug.CmpOptions); diff != "" {
t.Fatalf("wrong result\n%s", diff)
}
})
subtestInPromisingTask(t, "two instances", func(ctx context.Context, t *testing.T) {
forEachVal := cty.MapVal(map[string]cty.Value{
"a": cty.ObjectVal(map[string]cty.Value{
"test": cty.StringVal("in a"),
}),
"b": cty.ObjectVal(map[string]cty.Value{
"test": cty.StringVal("in b"),
}),
})
main := testEvaluator(t, testEvaluatorOpts{
Config: cfg,
TestOnlyGlobals: map[string]cty.Value{
"component_instances": forEachVal,
},
})
component := getComponent(ctx, t, main)
got := component.ResultValue(ctx, InspectPhase)
want := cty.ObjectVal(map[string]cty.Value{
"a": cty.ObjectVal(map[string]cty.Value{
// FIXME: This currently returns an unknown value because we
// aren't tracking component output values in prior state.
// Once we fix that, we should see an output value called "test"
// here.
"test": cty.DynamicVal,
}),
"b": cty.ObjectVal(map[string]cty.Value{
// FIXME: This currently returns an unknown value because we
// aren't tracking component output values in prior state.
// Once we fix that, we should see an output value called "test"
// here.
"test": cty.DynamicVal,
}),
})
// FIXME: the cmp transformer ctydebug.CmpOptions seems to find
// this particular pair of values troubling, causing it to get
// into an infinite recursion. For now we'll just use RawEquals,
// at the expense of a less helpful failure message. This seems
// to be a bug in upstream ctydebug.
if !want.RawEquals(got) {
t.Fatalf("wrong result\ngot: %#v\nwant: %#v", got, want)
}
})
subtestInPromisingTask(t, "null", func(ctx context.Context, t *testing.T) {
main := testEvaluator(t, testEvaluatorOpts{
Config: cfg,
TestOnlyGlobals: map[string]cty.Value{
"component_instances": cty.NullVal(cty.Map(cty.EmptyObject)),
},
})
component := getComponent(ctx, t, main)
got := component.ResultValue(ctx, InspectPhase)
// When the for_each expression is null, the result value should
// be a cty.NilVal.
want := cty.NilVal
// FIXME: the cmp transformer ctydebug.CmpOptions seems to find
// this particular pair of values troubling, causing it to get
// into an infinite recursion. For now we'll just use RawEquals,
// at the expense of a less helpful failure message. This seems
// to be a bug in upstream ctydebug.
if !want.RawEquals(got) {
t.Fatalf("wrong result\ngot: %#v\nwant: %#v", got, want)
}
})
subtestInPromisingTask(t, "string", func(ctx context.Context, t *testing.T) {
main := testEvaluator(t, testEvaluatorOpts{
Config: cfg,
TestOnlyGlobals: map[string]cty.Value{
"component_instances": cty.StringVal("nope"),
},
})
component := getComponent(ctx, t, main)
got := component.ResultValue(ctx, InspectPhase)
// When the for_each expression is null, the result value should
// be a cty.NilVal.
want := cty.NilVal
// FIXME: the cmp transformer ctydebug.CmpOptions seems to find
// this particular pair of values troubling, causing it to get
// into an infinite recursion. For now we'll just use RawEquals,
// at the expense of a less helpful failure message. This seems
// to be a bug in upstream ctydebug.
if !want.RawEquals(got) {
t.Fatalf("wrong result\ngot: %#v\nwant: %#v", got, want)
}
})
subtestInPromisingTask(t, "unknown", func(ctx context.Context, t *testing.T) {
main := testEvaluator(t, testEvaluatorOpts{
Config: cfg,
TestOnlyGlobals: map[string]cty.Value{
"component_instances": cty.UnknownVal(cty.Map(cty.EmptyObject)),
},
})
component := getComponent(ctx, t, main)
got := component.ResultValue(ctx, InspectPhase)
// When the for_each expression is unknown, the result value
// is a dynamic instance.
want := cty.DynamicVal
if diff := cmp.Diff(want, got, ctydebug.CmpOptions); diff != "" {
t.Fatalf("wrong result\n%s", diff)
}
})
})
}