mirror of https://github.com/hashicorp/terraform
Two bugs were making this not work quite right: - The Stack type wasn't treating its child stack objects as singletons, instead creating a new one on each request and therefore preventing child objects from being treated as singletons too. - StackCallInstance.CheckInputVariableValues wasn't actually using the value from its expression evaluation, and was instead just always returning an empty object. This commit also includes the tests that helped find these bugs.pull/34738/head
parent
9a943af56d
commit
992f6d4115
@ -0,0 +1,113 @@
|
||||
package stackeval
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/terraform/internal/addrs"
|
||||
"github.com/hashicorp/terraform/internal/promising"
|
||||
"github.com/hashicorp/terraform/internal/stacks/stackaddrs"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
)
|
||||
|
||||
func TestInputVariableValue(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
cfg := testStackConfig(t, "input_variable", "basics")
|
||||
|
||||
childStackAddr := stackaddrs.RootStackInstance.Child("child", addrs.NoKey)
|
||||
|
||||
tests := map[string]struct {
|
||||
NameVal cty.Value
|
||||
WantRootVal cty.Value
|
||||
WantChildVal cty.Value
|
||||
|
||||
WantRootErr bool
|
||||
}{
|
||||
"known string": {
|
||||
NameVal: cty.StringVal("jackson"),
|
||||
WantRootVal: cty.StringVal("jackson"),
|
||||
WantChildVal: cty.StringVal("child of jackson"),
|
||||
},
|
||||
"unknown string": {
|
||||
NameVal: cty.UnknownVal(cty.String),
|
||||
WantRootVal: cty.UnknownVal(cty.String),
|
||||
WantChildVal: cty.UnknownVal(cty.String).Refine().
|
||||
NotNull().
|
||||
StringPrefix("child of ").
|
||||
NewValue(),
|
||||
},
|
||||
"unknown of unknown type": {
|
||||
NameVal: cty.DynamicVal,
|
||||
WantRootVal: cty.UnknownVal(cty.String),
|
||||
WantChildVal: cty.UnknownVal(cty.String).Refine().
|
||||
NotNull().
|
||||
StringPrefix("child of ").
|
||||
NewValue(),
|
||||
},
|
||||
"bool": {
|
||||
// This one is testing that the given value gets converted to
|
||||
// the declared type constraint, which is string in this case.
|
||||
NameVal: cty.True,
|
||||
WantRootVal: cty.StringVal("true"),
|
||||
WantChildVal: cty.StringVal("child of true"),
|
||||
},
|
||||
"object": {
|
||||
// This one is testing that the given value gets converted to
|
||||
// the declared type constraint, which is string in this case.
|
||||
NameVal: cty.EmptyObjectVal,
|
||||
WantRootErr: true, // Type mismatch error
|
||||
},
|
||||
}
|
||||
|
||||
for name, test := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
main := testEvaluator(t, testEvaluatorOpts{
|
||||
Config: cfg,
|
||||
InputVariableValues: map[string]cty.Value{
|
||||
"name": test.NameVal,
|
||||
},
|
||||
})
|
||||
|
||||
t.Run("root", func(t *testing.T) {
|
||||
promising.MainTask(ctx, func(ctx context.Context) (struct{}, error) {
|
||||
mainStack := main.MainStack(ctx)
|
||||
rootVar := mainStack.InputVariable(ctx, stackaddrs.InputVariable{Name: "name"})
|
||||
got, diags := rootVar.CheckValue(ctx, InspectPhase)
|
||||
|
||||
if test.WantRootErr {
|
||||
if !diags.HasErrors() {
|
||||
t.Errorf("succeeded; want error\ngot: %#v", got)
|
||||
}
|
||||
return struct{}{}, nil
|
||||
}
|
||||
|
||||
if diags.HasErrors() {
|
||||
t.Errorf("unexpected errors\n%s", diags.Err().Error())
|
||||
}
|
||||
want := test.WantRootVal
|
||||
if !want.RawEquals(got) {
|
||||
t.Errorf("wrong value\ngot: %#v\nwant: %#v", got, want)
|
||||
}
|
||||
return struct{}{}, nil
|
||||
})
|
||||
})
|
||||
if !test.WantRootErr {
|
||||
t.Run("child", func(t *testing.T) {
|
||||
promising.MainTask(ctx, func(ctx context.Context) (struct{}, error) {
|
||||
childStack := main.Stack(ctx, childStackAddr, InspectPhase)
|
||||
rootVar := childStack.InputVariable(ctx, stackaddrs.InputVariable{Name: "name"})
|
||||
got, diags := rootVar.CheckValue(ctx, InspectPhase)
|
||||
if diags.HasErrors() {
|
||||
t.Errorf("unexpected errors\n%s", diags.Err().Error())
|
||||
}
|
||||
want := test.WantChildVal
|
||||
if !want.RawEquals(got) {
|
||||
t.Errorf("wrong value\ngot: %#v\nwant: %#v", got, want)
|
||||
}
|
||||
return struct{}{}, nil
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,4 @@
|
||||
|
||||
variable "name" {
|
||||
type = string
|
||||
}
|
||||
@ -0,0 +1,12 @@
|
||||
|
||||
variable "name" {
|
||||
type = string
|
||||
}
|
||||
|
||||
stack "child" {
|
||||
source = "./child"
|
||||
|
||||
inputs = {
|
||||
name = "child of ${var.name}"
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
{
|
||||
"terraform_source_bundle": 1,
|
||||
"packages": [
|
||||
{
|
||||
"source": "https://testing.invalid/input_variable.tar.gz",
|
||||
"local": "input_variable"
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -0,0 +1,109 @@
|
||||
package stackeval
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/go-slug/sourceaddrs"
|
||||
"github.com/hashicorp/go-slug/sourcebundle"
|
||||
"github.com/hashicorp/terraform/internal/stacks/stackaddrs"
|
||||
"github.com/hashicorp/terraform/internal/stacks/stackconfig"
|
||||
"github.com/hashicorp/terraform/internal/stacks/stackstate"
|
||||
"github.com/hashicorp/terraform/internal/tfdiags"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
)
|
||||
|
||||
// This file contains some general test utilities that many of our other
|
||||
// _test.go files rely on. It doesn't actually contain any tests itself.
|
||||
|
||||
// testStackConfig loads a stack configuration from the source bundle in this
|
||||
// package's testdata directory.
|
||||
//
|
||||
// "collection" is the name of one of the synthetic source packages that's
|
||||
// declared in the source bundle, and "subPath" is the path within that
|
||||
// package.
|
||||
func testStackConfig(t *testing.T, collection string, subPath string) *stackconfig.Config {
|
||||
t.Helper()
|
||||
|
||||
// Our collection of test configurations is laid out like a source
|
||||
// bundle that was installed from some source addresses that don't
|
||||
// really exist, and so we'll construct a suitable fake source
|
||||
// address following that scheme.
|
||||
fakeSrcStr := fmt.Sprintf("https://testing.invalid/%s.tar.gz//%s", collection, subPath)
|
||||
fakeSrc, err := sourceaddrs.ParseRemoteSource(fakeSrcStr)
|
||||
if err != nil {
|
||||
t.Fatalf("artificial source address string %q is invalid: %s", fakeSrcStr, err)
|
||||
}
|
||||
|
||||
sources, err := sourcebundle.OpenDir("testdata/sourcebundle")
|
||||
if err != nil {
|
||||
t.Fatalf("cannot open source bundle: %s", err)
|
||||
}
|
||||
|
||||
ret, diags := stackconfig.LoadConfigDir(fakeSrc, sources)
|
||||
if diags.HasErrors() {
|
||||
t.Fatalf("configuration is invalid\n%s", diags.Err().Error())
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
// testEvaluator constructs a [Main] that's configured for [InspectPhase] using
|
||||
// the given configuration, state, and other options.
|
||||
//
|
||||
// This evaluator is suitable for tests that focus only on evaluation logic
|
||||
// within this package, but will not be suitable for all situations. Some
|
||||
// tests should instantiate [Main] directly, particularly if they intend to
|
||||
// exercise phase-specific functionality like planning or applying component
|
||||
// instances.
|
||||
func testEvaluator(t *testing.T, opts testEvaluatorOpts) *Main {
|
||||
t.Helper()
|
||||
if opts.Config == nil {
|
||||
t.Fatal("Config field must not be nil")
|
||||
}
|
||||
if opts.State == nil {
|
||||
opts.State = stackstate.NewState()
|
||||
}
|
||||
|
||||
inputVals := make(map[stackaddrs.InputVariable]ExternalInputValue, len(opts.InputVariableValues))
|
||||
for name, val := range opts.InputVariableValues {
|
||||
inputVals[stackaddrs.InputVariable{Name: name}] = ExternalInputValue{
|
||||
Value: val,
|
||||
DefRange: tfdiags.SourceRange{
|
||||
Filename: "<test-input>",
|
||||
Start: tfdiags.SourcePos{
|
||||
Line: 1,
|
||||
Column: 1,
|
||||
Byte: 0,
|
||||
},
|
||||
End: tfdiags.SourcePos{
|
||||
Line: 1,
|
||||
Column: 1,
|
||||
Byte: 0,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return NewForInspecting(opts.Config, opts.State, InspectOpts{
|
||||
InputVariableValues: inputVals,
|
||||
ProviderFactories: opts.ProviderFactories,
|
||||
})
|
||||
}
|
||||
|
||||
type testEvaluatorOpts struct {
|
||||
// Config is required.
|
||||
Config *stackconfig.Config
|
||||
|
||||
// State is optional; testEvaluator will use an empty state if this is nil.
|
||||
State *stackstate.State
|
||||
|
||||
// InputVariableValues is optional and if set will provide the values
|
||||
// for the root stack input variables. Any variable not defined here
|
||||
// will evaluate to an unknown value of the configured type.
|
||||
InputVariableValues map[string]cty.Value
|
||||
|
||||
// ProviderFactories is optional and if set provides factory functions
|
||||
// for provider types that the test can use. If not set then any attempt
|
||||
// to use provider configurations will lead to some sort of error.
|
||||
ProviderFactories ProviderFactories
|
||||
}
|
||||
Loading…
Reference in new issue