stackeval: Tests for StackCall.Instances

pull/34738/head
Martin Atkins 3 years ago
parent 48d2d15a40
commit 75602fa9eb

@ -46,6 +46,10 @@ func newStackCallInstance(call *StackCall, key addrs.InstanceKey, repetition ins
}
}
func (c *StackCallInstance) RepetitionData() instances.RepetitionData {
return c.repetition
}
// CallerStack returns the stack instance that contains the call that this
// is an instance of.
func (c *StackCallInstance) CallerStack(ctx context.Context) *Stack {

@ -0,0 +1,226 @@
package stackeval
import (
"context"
"testing"
"github.com/davecgh/go-spew/spew"
"github.com/google/go-cmp/cmp"
"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"
"github.com/zclconf/go-cty-debug/ctydebug"
"github.com/zclconf/go-cty/cty"
)
func TestStackCallCheckInstances(t *testing.T) {
getStackCall := func(ctx context.Context, main *Main) *StackCall {
mainStack := main.MainStack(ctx)
call := mainStack.EmbeddedStackCall(ctx, stackaddrs.StackCall{Name: "child"})
if call == nil {
t.Fatal("stack.child does not exist, but it should exist")
}
return call
}
subtestInPromisingTask(t, "single instance", func(ctx context.Context, t *testing.T) {
cfg := testStackConfig(t, "stack_call", "single_instance")
main := testEvaluator(t, testEvaluatorOpts{
Config: cfg,
TestOnlyGlobals: map[string]cty.Value{
"child_stack_inputs": cty.EmptyObjectVal,
},
})
call := getStackCall(ctx, main)
forEachVal, diags := call.CheckForEachValue(ctx, InspectPhase)
assertNoDiags(t, diags)
if forEachVal != cty.NilVal {
t.Fatalf("unexpected for_each value\ngot: %#v\nwant: cty.NilVal", forEachVal)
}
insts, diags := call.CheckInstances(ctx, InspectPhase)
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, "stack_call", "for_each")
subtestInPromisingTask(t, "no instances", func(ctx context.Context, t *testing.T) {
main := testEvaluator(t, testEvaluatorOpts{
Config: cfg,
TestOnlyGlobals: map[string]cty.Value{
"child_stack_for_each": cty.MapValEmpty(cty.EmptyObject),
},
})
call := getStackCall(ctx, main)
forEachVal, diags := call.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, diags := call.CheckInstances(ctx, InspectPhase)
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{
"child_stack_for_each": wantForEachVal,
},
})
call := getStackCall(ctx, main)
gotForEachVal, diags := call.CheckForEachValue(ctx, InspectPhase)
assertNoDiags(t, diags)
if !wantForEachVal.RawEquals(gotForEachVal) {
t.Fatalf("unexpected for_each value\ngot: %#v\nwant: %#v", gotForEachVal, wantForEachVal)
}
insts, diags := call.CheckInstances(ctx, InspectPhase)
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{
"child_stack_for_each": cty.NullVal(cty.Map(cty.EmptyObject)),
},
})
call := getStackCall(ctx, main)
gotVal, diags := call.CheckForEachValue(ctx, InspectPhase)
assertMatchingDiag(t, diags, func(diag tfdiags.Diagnostic) bool {
return diag.Severity() == tfdiags.Error && diag.Description().Detail == "The for_each value must not be null."
})
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{
"child_stack_for_each": cty.StringVal("nope"),
},
})
call := getStackCall(ctx, main)
gotVal, diags := call.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 embedded stack.")
})
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 to represent that we don't know enough to predict
// how many instances there are. This is a different result than
// when we know there are zero instances, which would be a non-nil
// empty map.
gotInsts, diags := call.CheckInstances(ctx, InspectPhase)
assertNoDiags(t, diags)
if gotInsts != nil {
t.Errorf("wrong instances; want nil\n%#v", gotInsts)
}
})
subtestInPromisingTask(t, "unknown", func(ctx context.Context, t *testing.T) {
main := testEvaluator(t, testEvaluatorOpts{
Config: cfg,
TestOnlyGlobals: map[string]cty.Value{
"child_stack_for_each": cty.UnknownVal(cty.Map(cty.EmptyObject)),
},
})
// For now it's invalid to use an unknown value in for_each.
// Later we're expecting to make this succeed but announce that
// planning everything beneath this call must be deferred to a
// future plan after everything else has been applied first.
call := getStackCall(ctx, main)
gotVal, diags := call.CheckForEachValue(ctx, InspectPhase)
assertMatchingDiag(t, diags, func(diag tfdiags.Diagnostic) bool {
return (diag.Severity() == tfdiags.Error &&
diag.Description().Detail == "The for_each value must not be derived from values that will be determined only during the apply phase.")
})
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 invalid, CheckInstances should
// return nil to represent that we don't know enough to predict
// how many instances there are. This is a different result than
// when we know there are zero instances, which would be a non-nil
// empty map.
gotInsts, diags := call.CheckInstances(ctx, InspectPhase)
assertNoDiags(t, diags)
if gotInsts != nil {
t.Errorf("wrong instances; want nil\n%#v", gotInsts)
}
})
})
}

@ -0,0 +1,3 @@
# This stack configuration is intentionally left empty; we use it as the
# configuration for a child stack in some of our StackCall and StackCallInstance
# tests where the content of the child stack is irrelevant to the test.

@ -0,0 +1,14 @@
# Set the test-only global "child_stack_for_each" to a map conforming
# to the following type constraint:
#
# map(object({
# test_string = optional(string)
# test_map = optional(map(string))
# }))
stack "child" {
source = "../with_variables_and_outputs"
for_each = _test_only_global.child_stack_for_each
inputs = each.value
}

@ -0,0 +1,13 @@
# Set the test-only global "child_stack_inputs" to an object conforming
# to the following type constraint:
#
# object({
# test_string = optional(string)
# test_map = optional(map(string))
# })
stack "child" {
source = "../with_variables_and_outputs"
inputs = _test_only_global.child_stack_inputs
}

@ -0,0 +1,29 @@
# This is intended for use as a child stack configuration for situations
# where we're testing the propagation of input variables into the child
# stack.
#
# It contains some input variable declarations that we can assign values to
# for testing purposes. Both are optional to give flexibility for reusing
# this across multiple test cases. If you need something more sophisticated
# for your test, prefer to write a new configuration rather than growing this
# one any further.
variable "test_string" {
type = string
default = null
}
variable "test_map" {
type = map(string)
default = null
}
output "test_string" {
type = string
value = var.test_string
}
output "test_map" {
type = string
value = var.test_map
}

@ -8,6 +8,10 @@
{
"source": "https://testing.invalid/output_value.tar.gz",
"local": "output_value"
},
{
"source": "https://testing.invalid/stack_call.tar.gz",
"local": "stack_call"
}
]
}

@ -1,11 +1,13 @@
package stackeval
import (
"context"
"fmt"
"testing"
"github.com/hashicorp/go-slug/sourceaddrs"
"github.com/hashicorp/go-slug/sourcebundle"
"github.com/hashicorp/terraform/internal/promising"
"github.com/hashicorp/terraform/internal/stacks/stackaddrs"
"github.com/hashicorp/terraform/internal/stacks/stackconfig"
"github.com/hashicorp/terraform/internal/stacks/stackstate"
@ -151,6 +153,56 @@ type testEvaluatorOpts struct {
// methods that are intended only as internal API, since it's a public API
// from the perspective of a test caller even though it's not public to
// other callers.
func (m *Main) SetTestOnlyGlobals(vals map[string]cty.Value) {
func (m *Main) SetTestOnlyGlobals(t *testing.T, vals map[string]cty.Value) {
m.testOnlyGlobals = vals
}
func assertNoDiags(t *testing.T, diags tfdiags.Diagnostics) {
t.Helper()
if len(diags) != 0 {
t.Fatalf("unexpected diagnostics\n%s", diags.Err())
}
}
func assertMatchingDiag(t *testing.T, diags tfdiags.Diagnostics, check func(diag tfdiags.Diagnostic) bool) {
t.Helper()
for _, diag := range diags {
if check(diag) {
return
}
}
t.Fatalf("none of the diagnostics is the one we are expecting\n%s", diags.Err())
}
// inPromisingTask is a helper for conveniently running some code in the context
// of a [promising.MainTask], with automatic promise error checking. This
// makes it valid to call functions that expect to run only as part of a
// promising task, which is true of essentially every method in this package
// that takes a [context.Context] as its first argument.
//
// Specifically, if the function encounters any direct promise-related failures,
// such as failure to resolve a promise before returning, this function will
// halt the test with an error message.
func inPromisingTask(t *testing.T, f func(ctx context.Context, t *testing.T)) {
t.Helper()
_, err := promising.MainTask(context.Background(), func(ctx context.Context) (struct{}, error) {
t.Helper()
f(ctx, t)
return struct{}{}, nil
})
if err != nil {
// We could get here if the test produces any self-references or
// if it creates any promises that are left unresolved once it exits.
t.Fatalf("promise resolution failure: %s", err)
}
}
// subtestInPromisingTask compiles [testing.T.Run] with [inPromisingTask] as
// a convenience wrapper for running an entire subtest as a [promising.MainTask].
func subtestInPromisingTask(t *testing.T, name string, f func(ctx context.Context, t *testing.T)) {
t.Helper()
t.Run(name, func(t *testing.T) {
t.Helper()
inPromisingTask(t, f)
})
}

Loading…
Cancel
Save