Allow referencing output from test run in local variables block (tofu test) (#1254)

Signed-off-by: siddharthasonker95 <158144589+siddharthasonker95@users.noreply.github.com>
This commit is contained in:
Siddhartha Sonker 2024-02-19 15:48:56 +05:30 committed by GitHub
parent e21a14fb0c
commit accfe1c412
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 128 additions and 17 deletions

View File

@ -18,6 +18,7 @@ ENHANCEMENTS:
* Added "cidrcontains" function. ([$366](https://github.com/opentofu/opentofu/issues/366))
* Allow test run blocks to reference previous run block's module outputs ([#1129](https://github.com/opentofu/opentofu/pull/1129))
* Support the XDG Base Directory Specification ([#1200](https://github.com/opentofu/opentofu/pull/1200))
* Allow referencing the output from a test run in the local variables block of another run (tofu test). ([#1254](https://github.com/opentofu/opentofu/pull/1254))
BUG FIXES:
* `tofu test` resources cleanup at the end of tests changed to use simple reverse run block order. ([#1043](https://github.com/opentofu/opentofu/pull/1043))

View File

@ -662,7 +662,7 @@ func (runner *TestFileRunner) destroy(config *configs.Config, state *states.Stat
var diags tfdiags.Diagnostics
variables, variableDiags := buildInputVariablesForTest(run, file, config, runner.Suite.GlobalVariables)
variables, variableDiags := buildInputVariablesForTest(run, file, config, runner.Suite.GlobalVariables, runner.States)
diags = diags.Append(variableDiags)
if diags.HasErrors() {
@ -724,7 +724,7 @@ func (runner *TestFileRunner) plan(config *configs.Config, state *states.State,
references, referenceDiags := run.GetReferences()
diags = diags.Append(referenceDiags)
variables, variableDiags := buildInputVariablesForTest(run, file, config, runner.Suite.GlobalVariables)
variables, variableDiags := buildInputVariablesForTest(run, file, config, runner.Suite.GlobalVariables, runner.States)
diags = diags.Append(variableDiags)
if diags.HasErrors() {
@ -988,15 +988,17 @@ func (runner *TestFileRunner) Cleanup(file *moduletest.File) {
// Crucially, it differs from prepareInputVariablesForAssertions in that it only
// includes variables that are reference by the config and not everything that
// is defined within the test run block and test file.
func buildInputVariablesForTest(run *moduletest.Run, file *moduletest.File, config *configs.Config, globals map[string]backend.UnparsedVariableValue) (tofu.InputValues, tfdiags.Diagnostics) {
func buildInputVariablesForTest(run *moduletest.Run, file *moduletest.File, config *configs.Config, globals map[string]backend.UnparsedVariableValue, states map[string]*TestFileState) (tofu.InputValues, tfdiags.Diagnostics) {
variables := make(map[string]backend.UnparsedVariableValue)
evalCtx := getEvalContextFromStates(states)
for name := range config.Module.Variables {
if run != nil {
if expr, exists := run.Config.Variables[name]; exists {
// Local variables take precedence.
variables[name] = unparsedVariableValueExpression{
variables[name] = testVariableValueExpression{
expr: expr,
sourceType: tofu.ValueFromConfig,
ctx: evalCtx,
}
continue
}
@ -1028,6 +1030,34 @@ func buildInputVariablesForTest(run *moduletest.Run, file *moduletest.File, conf
return backend.ParseVariableValues(variables, config.Module.Variables)
}
// getEvalContextFromStates constructs an hcl.EvalContext based on the provided map
// of TestFileState instances. It extracts the relevant information from the
// states to create a context suitable for HCL evaluation, including the output
// values of modules.
//
// Parameters:
// - states: A map of TestFileState instances containing the state information.
//
// Returns:
// - *hcl.EvalContext: The constructed HCL evaluation context.
func getEvalContextFromStates(states map[string]*TestFileState) *hcl.EvalContext {
runCtx := make(map[string]cty.Value)
for _, state := range states {
if state.Run == nil {
continue
}
outputs := make(map[string]cty.Value)
mod := state.State.Modules[""] // Empty string is what is used by the module in the test runner
for outName, out := range mod.OutputValues {
outputs[outName] = out.Value
}
runCtx[state.Run.Name] = cty.ObjectVal(outputs)
}
ctx := &hcl.EvalContext{Variables: map[string]cty.Value{"run": cty.ObjectVal(runCtx)}}
return ctx
}
type testVariableValueExpression struct {
expr hcl.Expression
sourceType tofu.ValueSourceType
@ -1065,19 +1095,7 @@ func (v testVariableValueExpression) ParseVariableValue(mode configs.VariablePar
// available are also defined in the config. It returns a function that resets
// the config which must be called so the config can be reused going forward.
func (runner *TestFileRunner) prepareInputVariablesForAssertions(config *configs.Config, run *moduletest.Run, file *moduletest.File, globals map[string]backend.UnparsedVariableValue) (tofu.InputValues, func(), tfdiags.Diagnostics) {
runCtx := make(map[string]cty.Value)
for _, state := range runner.States {
if state.Run == nil {
continue
}
outputs := make(map[string]cty.Value)
mod := state.State.Modules[""] // Empty string is what is used by the module in the test runner
for outName, out := range mod.OutputValues {
outputs[outName] = out.Value
}
runCtx[state.Run.Name] = cty.ObjectVal(outputs)
}
ctx := &hcl.EvalContext{Variables: map[string]cty.Value{"run": cty.ObjectVal(runCtx)}}
ctx := getEvalContextFromStates(runner.States)
variables := make(map[string]backend.UnparsedVariableValue)

View File

@ -1118,3 +1118,73 @@ Condition expression could not be evaluated at this time.
})
}
}
func TestTest_LocalVariables(t *testing.T) {
td := t.TempDir()
testCopyDir(t, testFixturePath(path.Join("test", "pass_with_local_variable")), td)
defer testChdir(t, td)()
provider := testing_command.NewProvider(nil)
providerSource, close := newMockProviderSource(t, map[string][]string{
"test": {"1.0.0"},
})
defer close()
streams, done := terminal.StreamsForTesting(t)
view := views.NewView(streams)
ui := new(cli.MockUi)
meta := Meta{
testingOverrides: metaOverridesForProvider(provider.Provider),
Ui: ui,
View: view,
Streams: streams,
ProviderSource: providerSource,
}
init := &InitCommand{
Meta: meta,
}
if code := init.Run(nil); code != 0 {
t.Fatalf("expected status code 0 but got %d: %s", code, ui.ErrorWriter)
}
c := &TestCommand{
Meta: meta,
}
code := c.Run([]string{"-verbose", "-no-color"})
output := done(t)
if code != 0 {
t.Errorf("expected status code 0 but got %d", code)
}
expected := `tests/test.tftest.hcl... pass
run "first"... pass
Outputs:
foo = "bar"
run "second"... pass
No changes. Your infrastructure matches the configuration.
OpenTofu has compared your real infrastructure against your configuration and
found no differences, so no changes are needed.
Success! 2 passed, 0 failed.
`
actual := output.All()
if diff := cmp.Diff(actual, expected); len(diff) > 0 {
t.Errorf("output didn't match expected:\nexpected:\n%s\nactual:\n%s\ndiff:\n%s", expected, actual, diff)
}
if provider.ResourceCount() > 0 {
t.Errorf("should have deleted all resources on completion but left %v", provider.ResourceString())
}
}

View File

@ -0,0 +1,4 @@
output "foo" {
description = "Output"
value = "bar"
}

View File

@ -0,0 +1,15 @@
run "first" {
command = apply
}
run "second" {
command = plan
module {
source = "./tests/testmodule"
}
variables {
foo = run.first.foo
}
}

View File

@ -0,0 +1,3 @@
variable "foo" {
type = string
}