diff --git a/CHANGELOG.md b/CHANGELOG.md index bbf690818e..c4833db724 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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)) diff --git a/internal/command/test.go b/internal/command/test.go index 49f59e2edf..1123ae21ab 100644 --- a/internal/command/test.go +++ b/internal/command/test.go @@ -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) diff --git a/internal/command/test_test.go b/internal/command/test_test.go index a9234217c7..4a329e48c7 100644 --- a/internal/command/test_test.go +++ b/internal/command/test_test.go @@ -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()) + } +} diff --git a/internal/command/testdata/test/pass_with_local_variable/main.tf b/internal/command/testdata/test/pass_with_local_variable/main.tf new file mode 100644 index 0000000000..548fe68384 --- /dev/null +++ b/internal/command/testdata/test/pass_with_local_variable/main.tf @@ -0,0 +1,4 @@ +output "foo" { + description = "Output" + value = "bar" +} \ No newline at end of file diff --git a/internal/command/testdata/test/pass_with_local_variable/tests/test.tftest.hcl b/internal/command/testdata/test/pass_with_local_variable/tests/test.tftest.hcl new file mode 100644 index 0000000000..4ccdf20225 --- /dev/null +++ b/internal/command/testdata/test/pass_with_local_variable/tests/test.tftest.hcl @@ -0,0 +1,15 @@ +run "first" { + command = apply +} + +run "second" { + command = plan + + module { + source = "./tests/testmodule" + } + + variables { + foo = run.first.foo + } +} \ No newline at end of file diff --git a/internal/command/testdata/test/pass_with_local_variable/tests/testmodule/main.tf b/internal/command/testdata/test/pass_with_local_variable/tests/testmodule/main.tf new file mode 100644 index 0000000000..7fe3000783 --- /dev/null +++ b/internal/command/testdata/test/pass_with_local_variable/tests/testmodule/main.tf @@ -0,0 +1,3 @@ +variable "foo" { + type = string +}