Files
opentf/internal/tofu/node_root_variable_test.go
Martin Atkins 84147b50e9 tofu: GraphNodeExecutable interface takes context.Context
In order to generate OpenTelemetry traces of the main work that OpenTofu
Core does we'll need to be able to propagate the active trace context into
the main "Execute" method of each graph node, since that's where we
typically make requests to providers, and other such work that could take
a noticeable amount of time.

Changing these frequently-used interfaces is always a noisy diff, so this
commit intentionally focuses only on changing the signature of that
interface and its one caller, and then dealing with all of the fallout of
that on existing unit test code.

For any use of Execute that was affected by this change we'll also switch
to our newer naming scheme of using "evalCtx" as the name of the
tofu.EvalContext variable, in preparation for using "ctx" idiomatically to
refer to context.Context. However, the implementations currently don't yet
name their context.Context argument because the method bodies don't yet
make use of it. We'll name each of those arguments to "ctx" individually
as we gradually add tracing support to each graph node type.

Signed-off-by: Martin Atkins <mart@degeneration.co.uk>
2025-05-02 08:46:47 -07:00

216 lines
6.8 KiB
Go

// Copyright (c) The OpenTofu Authors
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2023 HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package tofu
import (
"testing"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hcltest"
"github.com/zclconf/go-cty/cty"
"github.com/opentofu/opentofu/internal/addrs"
"github.com/opentofu/opentofu/internal/checks"
"github.com/opentofu/opentofu/internal/configs"
"github.com/opentofu/opentofu/internal/lang"
)
func TestNodeRootVariableExecute(t *testing.T) {
t.Run("type conversion", func(t *testing.T) {
evalCtx := new(MockEvalContext)
n := &NodeRootVariable{
Addr: addrs.InputVariable{Name: "foo"},
Config: &configs.Variable{
Name: "foo",
Type: cty.String,
ConstraintType: cty.String,
},
RawValue: &InputValue{
Value: cty.True,
SourceType: ValueFromUnknown,
},
}
evalCtx.ChecksState = checks.NewState(&configs.Config{
Module: &configs.Module{
Variables: map[string]*configs.Variable{
"foo": n.Config,
},
},
})
diags := n.Execute(t.Context(), evalCtx, walkApply)
if diags.HasErrors() {
t.Fatalf("unexpected error: %s", diags.Err())
}
if !evalCtx.SetRootModuleArgumentCalled {
t.Fatalf("ctx.SetRootModuleArgument wasn't called")
}
if got, want := evalCtx.SetRootModuleArgumentAddr.String(), "var.foo"; got != want {
t.Errorf("wrong address for ctx.SetRootModuleArgument\ngot: %s\nwant: %s", got, want)
}
if got, want := evalCtx.SetRootModuleArgumentValue, cty.StringVal("true"); !want.RawEquals(got) {
// NOTE: The given value was cty.Bool but the type constraint was
// cty.String, so it was NodeRootVariable's responsibility to convert
// as part of preparing the "final value".
t.Errorf("wrong value for ctx.SetRootModuleArgument\ngot: %#v\nwant: %#v", got, want)
}
})
t.Run("validation", func(t *testing.T) {
evalCtx := new(MockEvalContext)
// The variable validation function gets called with OpenTofu's
// built-in functions available, so we need a minimal scope just for
// it to get the functions from.
evalCtx.EvaluationScopeScope = &lang.Scope{}
// We need to reimplement a _little_ bit of EvalContextBuiltin logic
// here to get a similar effect with EvalContextMock just to get the
// value to flow through here in a realistic way that'll make this test
// useful.
var finalVal cty.Value
evalCtx.SetRootModuleArgumentFunc = func(addr addrs.InputVariable, v cty.Value) {
if addr.Name == "foo" {
t.Logf("set %s to %#v", addr.String(), v)
finalVal = v
}
}
evalCtx.GetVariableValueFunc = func(addr addrs.AbsInputVariableInstance) cty.Value {
if addr.String() != "var.foo" {
return cty.NilVal
}
t.Logf("reading final val for %s (%#v)", addr.String(), finalVal)
return finalVal
}
n := &NodeRootVariable{
Addr: addrs.InputVariable{Name: "foo"},
Config: &configs.Variable{
Name: "foo",
Type: cty.Number,
ConstraintType: cty.Number,
Validations: []*configs.CheckRule{
{
Condition: fakeHCLExpressionFunc(func(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
// This returns true only if the given variable value
// is exactly cty.Number, which allows us to verify
// that we were given the value _after_ type
// conversion.
// This had previously not been handled correctly,
// as reported in:
// https://github.com/hashicorp/terraform/issues/29899
vars := ctx.Variables["var"]
if vars == cty.NilVal || !vars.Type().IsObjectType() || !vars.Type().HasAttribute("foo") {
t.Logf("var.foo isn't available")
return cty.False, nil
}
val := vars.GetAttr("foo")
if val == cty.NilVal || val.Type() != cty.Number {
t.Logf("var.foo is %#v; want a number", val)
return cty.False, nil
}
return cty.True, nil
}),
ErrorMessage: hcltest.MockExprLiteral(cty.StringVal("Must be a number.")),
},
},
},
RawValue: &InputValue{
// Note: This is a string, but the variable's type constraint
// is number so it should be converted before use.
Value: cty.StringVal("5"),
SourceType: ValueFromUnknown,
},
}
ref := &nodeVariableReference{
Addr: n.Addr,
Config: n.Config,
}
evalCtx.ChecksState = checks.NewState(&configs.Config{
Module: &configs.Module{
Variables: map[string]*configs.Variable{
"foo": n.Config,
},
},
})
diags := n.Execute(t.Context(), evalCtx, walkApply)
if diags.HasErrors() {
t.Fatalf("unexpected error: %s", diags.Err())
}
g, err := ref.DynamicExpand(evalCtx)
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
for _, v := range g.Vertices() {
if ev, ok := v.(GraphNodeExecutable); ok {
diags = ev.Execute(t.Context(), evalCtx, walkApply)
if diags.HasErrors() {
t.Fatalf("unexpected error: %s", diags.Err())
}
}
}
if !evalCtx.SetRootModuleArgumentCalled {
t.Fatalf("ctx.SetRootModuleArgument wasn't called")
}
if got, want := evalCtx.SetRootModuleArgumentAddr.String(), "var.foo"; got != want {
t.Errorf("wrong address for ctx.SetRootModuleArgument\ngot: %s\nwant: %s", got, want)
}
if got, want := evalCtx.SetRootModuleArgumentValue, cty.NumberIntVal(5); !want.RawEquals(got) {
// NOTE: The given value was cty.Bool but the type constraint was
// cty.String, so it was NodeRootVariable's responsibility to convert
// as part of preparing the "final value".
t.Errorf("wrong value for ctx.SetRootModuleArgument\ngot: %#v\nwant: %#v", got, want)
}
if status := evalCtx.Checks().ObjectCheckStatus(n.Addr.Absolute(addrs.RootModuleInstance)); status != checks.StatusPass {
t.Errorf("expected checks to pass but go %s instead", status)
}
})
}
// fakeHCLExpressionFunc is a fake implementation of hcl.Expression that just
// directly produces a value with direct Go code.
//
// An expression of this type has no references and so it cannot access any
// variables from the EvalContext unless something else arranges for them
// to be guaranteed available. For example, custom variable validations just
// unconditionally have access to the variable they are validating regardless
// of references.
type fakeHCLExpressionFunc func(*hcl.EvalContext) (cty.Value, hcl.Diagnostics)
var _ hcl.Expression = fakeHCLExpressionFunc(nil)
func (f fakeHCLExpressionFunc) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
return f(ctx)
}
func (f fakeHCLExpressionFunc) Variables() []hcl.Traversal {
return nil
}
func (f fakeHCLExpressionFunc) Functions() []hcl.Traversal {
return nil
}
func (f fakeHCLExpressionFunc) Range() hcl.Range {
return hcl.Range{
Filename: "fake",
Start: hcl.InitialPos,
End: hcl.InitialPos,
}
}
func (f fakeHCLExpressionFunc) StartRange() hcl.Range {
return f.Range()
}