Introduce 'run' keyword for referencing outputs from earlier run blocks (#33683)

* introduce 'run' keyword for referencing outputs from earlier run blocks

* fix code consistency
compliance/license-update
Liam Cervante 3 years ago committed by GitHub
parent 6a04e988d6
commit 9742f22c4e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -111,6 +111,14 @@ func ParseRefFromTestingScope(traversal hcl.Traversal) (*Reference, tfdiags.Diag
Remaining: remain,
}
diags = checkDiags
case "run":
name, rng, remain, runDiags := parseSingleAttrRef(traversal)
reference = &Reference{
Subject: Run{Name: name},
SourceRange: tfdiags.SourceRangeFromHCL(rng),
Remaining: remain,
}
diags = runDiags
}
if reference != nil {

@ -67,6 +67,51 @@ func TestParseRefInTestingScope(t *testing.T) {
nil,
`The "check" object does not support this operation.`,
},
{
`run.zero`,
&Reference{
Subject: Run{
Name: "zero",
},
SourceRange: tfdiags.SourceRange{
Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0},
End: tfdiags.SourcePos{Line: 1, Column: 9, Byte: 8},
},
},
``,
},
{
`run.zero.value`,
&Reference{
Subject: Run{
Name: "zero",
},
SourceRange: tfdiags.SourceRange{
Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0},
End: tfdiags.SourcePos{Line: 1, Column: 9, Byte: 8},
},
Remaining: hcl.Traversal{
hcl.TraverseAttr{
Name: "value",
SrcRange: hcl.Range{
Start: hcl.Pos{Line: 1, Column: 9, Byte: 8},
End: hcl.Pos{Line: 1, Column: 15, Byte: 14},
},
},
},
},
``,
},
{
`run`,
nil,
`The "run" object cannot be accessed directly. Instead, access one of its attributes.`,
},
{
`run["foo"]`,
nil,
`The "run" object does not support this operation.`,
},
// Sanity check at least one of the others works to verify it does
// fall through to the core function.
@ -827,7 +872,7 @@ func TestParseRef(t *testing.T) {
`A reference to a resource type must be followed by at least one attribute access, specifying the resource name.`,
},
// Should interpret checks and outputs as resource types.
// Should interpret checks, outputs, and runs as resource types.
{
`output.value`,
&Reference{
@ -858,6 +903,21 @@ func TestParseRef(t *testing.T) {
},
``,
},
{
`run.zero`,
&Reference{
Subject: Resource{
Mode: ManagedResourceMode,
Type: "run",
Name: "zero",
},
SourceRange: tfdiags.SourceRange{
Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0},
End: tfdiags.SourcePos{Line: 1, Column: 9, Byte: 8},
},
},
``,
},
}
for _, test := range tests {

@ -0,0 +1,27 @@
package addrs
import "fmt"
// Run is the address of a run block within a testing file.
//
// Run blocks are only accessible from within the same testing file, and they
// do not support any meta-arguments like "count" or "for_each". So this address
// uniquely describes a run block from within a single testing file.
type Run struct {
referenceable
Name string
}
func (r Run) String() string {
return fmt.Sprintf("run.%s", r.Name)
}
func (r Run) Equal(run Run) bool {
return r.Name == run.Name
}
func (r Run) UniqueKey() UniqueKey {
return r // A Run is its own UniqueKey
}
func (r Run) uniqueKeySigil() {}

@ -12,6 +12,7 @@ import (
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
hcljson "github.com/hashicorp/hcl/v2/json"
"github.com/hashicorp/terraform/internal/backend"
"github.com/hashicorp/terraform/internal/configs"
"github.com/hashicorp/terraform/internal/terraform"
@ -266,3 +267,22 @@ func (v unparsedVariableValueString) ParseVariableValue(mode configs.VariablePar
SourceType: v.sourceType,
}, diags
}
type unparsedTestVariableValue struct {
expr hcl.Expression
ctx *hcl.EvalContext
}
func (v unparsedTestVariableValue) ParseVariableValue(mode configs.VariableParsingMode) (*terraform.InputValue, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
val, hclDiags := v.expr.Value(v.ctx) // nil because no function calls or variable references are allowed here
diags = diags.Append(hclDiags)
rng := tfdiags.SourceRangeFromHCL(v.expr.Range())
return &terraform.InputValue{
Value: val,
SourceType: terraform.ValueFromConfig, // Test variables always come from config.
SourceRange: rng,
}, diags
}

@ -18,6 +18,7 @@ import (
"github.com/hashicorp/terraform/internal/command/arguments"
"github.com/hashicorp/terraform/internal/command/views"
"github.com/hashicorp/terraform/internal/configs"
"github.com/hashicorp/terraform/internal/lang/marks"
"github.com/hashicorp/terraform/internal/logging"
"github.com/hashicorp/terraform/internal/moduletest"
"github.com/hashicorp/terraform/internal/plans"
@ -251,7 +252,7 @@ func (c *TestCommand) Run(rawArgs []string) int {
defer stop()
defer cancel()
runner.Start(variables)
runner.Start()
}()
// Wait for the operation to complete, or for an interrupt to occur.
@ -299,8 +300,10 @@ func (c *TestCommand) Run(rawArgs []string) int {
return 0
}
// test runner
// TestSuiteRunner executes an entire set of Terraform test files.
//
// It contains all shared information needed by all the test files, like the
// main configuration and the global variable values.
type TestSuiteRunner struct {
command *TestCommand
@ -332,7 +335,7 @@ type TestSuiteRunner struct {
Verbose bool
}
func (runner *TestSuiteRunner) Start(globals map[string]backend.UnparsedVariableValue) {
func (runner *TestSuiteRunner) Start() {
var files []string
for name := range runner.Suite.Files {
files = append(files, name)
@ -349,12 +352,13 @@ func (runner *TestSuiteRunner) Start(globals map[string]backend.UnparsedVariable
fileRunner := &TestFileRunner{
Suite: runner,
States: map[string]*TestFileState{
RelevantStates: map[string]*TestFileState{
MainStateIdentifier: {
Run: nil,
State: states.NewState(),
},
},
PriorStates: make(map[string]*terraform.TestContext),
}
fileRunner.ExecuteTestFile(file)
@ -364,11 +368,29 @@ func (runner *TestSuiteRunner) Start(globals map[string]backend.UnparsedVariable
}
type TestFileRunner struct {
// Suite contains all the helpful metadata about the test that we need
// during the execution of a file.
Suite *TestSuiteRunner
States map[string]*TestFileState
// RelevantStates is a mapping of module keys to it's last applied state
// file.
//
// This is used to clean up the infrastructure created during the test after
// the test has finished.
RelevantStates map[string]*TestFileState
// PriorStates is mapping from run block names to the TestContexts that were
// created when that run block executed.
//
// This is used to allow run blocks to refer back to the output values of
// previous run blocks. It is passed into the Evaluate functions that
// validate the test assertions, and used when calculating values for
// variables within run blocks.
PriorStates map[string]*terraform.TestContext
}
// TestFileState is a helper struct that just maps a run block to the state that
// was produced by the execution of that run block.
type TestFileState struct {
Run *moduletest.Run
State *states.State
@ -424,22 +446,22 @@ func (runner *TestFileRunner) ExecuteTestFile(file *moduletest.File) {
continue // Abort!
}
if _, exists := runner.States[key]; !exists {
runner.States[key] = &TestFileState{
if _, exists := runner.RelevantStates[key]; !exists {
runner.RelevantStates[key] = &TestFileState{
Run: nil,
State: states.NewState(),
}
}
}
state, updatedState := runner.ExecuteTestRun(run, file, runner.States[key].State, config)
state, updatedState := runner.ExecuteTestRun(run, file, runner.RelevantStates[key].State, config)
if updatedState {
// Only update the most recent run and state if the state was
// actually updated by this change. We want to use the run that
// most recently updated the tracked state as the cleanup
// configuration.
runner.States[key].State = state
runner.States[key].Run = run
runner.RelevantStates[key].State = state
runner.RelevantStates[key].Run = run
}
file.Status = file.Status.Merge(run.Status)
@ -499,7 +521,7 @@ func (runner *TestFileRunner) ExecuteTestRun(run *moduletest.Run, file *modulete
return state, false
}
variables, resetVariables, variableDiags := prepareInputVariablesForAssertions(config, run, file, runner.Suite.GlobalVariables)
variables, resetVariables, variableDiags := runner.prepareInputVariablesForAssertions(config, run, file)
defer resetVariables()
run.Diagnostics = run.Diagnostics.Append(variableDiags)
@ -533,7 +555,19 @@ func (runner *TestFileRunner) ExecuteTestRun(run *moduletest.Run, file *modulete
run.Diagnostics = run.Diagnostics.Append(diags)
}
planCtx.TestContext(config, plan.PlannedState, plan, variables).Evaluate(run)
// First, make the test context we can use to validate the assertions
// of the
ctx := planCtx.TestContext(run, config, plan.PlannedState, plan, variables)
// Second, evaluate the run block directly. We also pass in all the
// previous contexts so this run block can refer to outputs from
// previous run blocks.
ctx.Evaluate(runner.PriorStates)
// Now we've successfully validated this run block, lets add it into
// our prior states so future run blocks can access it.
runner.PriorStates[run.Name] = ctx
return state, false
}
@ -572,7 +606,7 @@ func (runner *TestFileRunner) ExecuteTestRun(run *moduletest.Run, file *modulete
return updated, true
}
variables, resetVariables, variableDiags := prepareInputVariablesForAssertions(config, run, file, runner.Suite.GlobalVariables)
variables, resetVariables, variableDiags := runner.prepareInputVariablesForAssertions(config, run, file)
defer resetVariables()
run.Diagnostics = run.Diagnostics.Append(variableDiags)
@ -606,7 +640,19 @@ func (runner *TestFileRunner) ExecuteTestRun(run *moduletest.Run, file *modulete
run.Diagnostics = run.Diagnostics.Append(diags)
}
applyCtx.TestContext(config, updated, plan, variables).Evaluate(run)
// First, make the test context we can use to validate the assertions
// of the
ctx := applyCtx.TestContext(run, config, updated, plan, variables)
// Second, evaluate the run block directly. We also pass in all the
// previous contexts so this run block can refer to outputs from
// previous run blocks.
ctx.Evaluate(runner.PriorStates)
// Now we've successfully validated this run block, lets add it into
// our prior states so future run blocks can access it.
runner.PriorStates[run.Name] = ctx
return updated, true
}
@ -655,7 +701,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 := runner.buildInputVariablesForTest(run, file, config)
diags = diags.Append(variableDiags)
if diags.HasErrors() {
@ -717,7 +763,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 := runner.buildInputVariablesForTest(run, file, config)
diags = diags.Append(variableDiags)
if diags.HasErrors() {
@ -844,8 +890,8 @@ func (runner *TestFileRunner) wait(ctx *terraform.Context, runningCtx context.Co
log.Printf("[DEBUG] TestFileRunner: test execution cancelled during %s", identifier)
states := make(map[*moduletest.Run]*states.State)
states[nil] = runner.States[MainStateIdentifier].State
for key, module := range runner.States {
states[nil] = runner.RelevantStates[MainStateIdentifier].State
for key, module := range runner.RelevantStates {
if key == MainStateIdentifier {
continue
}
@ -902,7 +948,7 @@ func (runner *TestFileRunner) Cleanup(file *moduletest.File) {
}
// First, we'll clean up the main state.
main := runner.States[MainStateIdentifier]
main := runner.RelevantStates[MainStateIdentifier]
var diags tfdiags.Diagnostics
updated := main.State
@ -931,7 +977,7 @@ func (runner *TestFileRunner) Cleanup(file *moduletest.File) {
}
var states []*TestFileState
for key, state := range runner.States {
for key, state := range runner.RelevantStates {
if key == MainStateIdentifier {
// We processed the main state above.
continue
@ -995,23 +1041,21 @@ func (runner *TestFileRunner) Cleanup(file *moduletest.File) {
}
}
// helper functions
// buildInputVariablesForTest creates a terraform.InputValues mapping for
// variable values that are relevant to the config being tested.
//
// 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) (terraform.InputValues, tfdiags.Diagnostics) {
func (runner *TestFileRunner) buildInputVariablesForTest(run *moduletest.Run, file *moduletest.File, config *configs.Config) (terraform.InputValues, tfdiags.Diagnostics) {
variables := make(map[string]backend.UnparsedVariableValue)
for name := range config.Module.Variables {
if run != nil {
if expr, exists := run.Config.Variables[name]; exists {
// Local variables take precedence.
variables[name] = unparsedVariableValueExpression{
expr: expr,
sourceType: terraform.ValueFromConfig,
variables[name] = unparsedTestVariableValue{
expr: expr,
ctx: runner.EvalCtx(),
}
continue
}
@ -1028,10 +1072,10 @@ func buildInputVariablesForTest(run *moduletest.Run, file *moduletest.File, conf
}
}
if globals != nil {
if runner.Suite.GlobalVariables != nil {
// If it's not set locally or at the file level, maybe it was
// defined globally.
if variable, exists := globals[name]; exists {
if variable, exists := runner.Suite.GlobalVariables[name]; exists {
variables[name] = variable
}
}
@ -1055,14 +1099,14 @@ func buildInputVariablesForTest(run *moduletest.Run, file *moduletest.File, conf
// In addition, it modifies the provided config so that any variables that are
// 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 prepareInputVariablesForAssertions(config *configs.Config, run *moduletest.Run, file *moduletest.File, globals map[string]backend.UnparsedVariableValue) (terraform.InputValues, func(), tfdiags.Diagnostics) {
func (runner *TestFileRunner) prepareInputVariablesForAssertions(config *configs.Config, run *moduletest.Run, file *moduletest.File) (terraform.InputValues, func(), tfdiags.Diagnostics) {
variables := make(map[string]backend.UnparsedVariableValue)
if run != nil {
for name, expr := range run.Config.Variables {
variables[name] = unparsedVariableValueExpression{
expr: expr,
sourceType: terraform.ValueFromConfig,
variables[name] = unparsedTestVariableValue{
expr: expr,
ctx: runner.EvalCtx(),
}
}
}
@ -1081,7 +1125,7 @@ func prepareInputVariablesForAssertions(config *configs.Config, run *moduletest.
}
}
for name, variable := range globals {
for name, variable := range runner.Suite.GlobalVariables {
if _, exists := variables[name]; exists {
// Then this value was already defined at either the run level
// or the file level, and we want those values to take
@ -1157,3 +1201,38 @@ func prepareInputVariablesForAssertions(config *configs.Config, run *moduletest.
config.Module.Variables = currentVars
}, diags
}
// EvalCtx returns an hcl.EvalContext that allows the variables blocks within
// run blocks to evaluate references to the outputs from other run blocks.
func (runner *TestFileRunner) EvalCtx() *hcl.EvalContext {
return &hcl.EvalContext{
Variables: func() map[string]cty.Value {
blocks := make(map[string]cty.Value)
for run, ctx := range runner.PriorStates {
outputs := make(map[string]cty.Value)
for _, output := range ctx.Config.Module.Outputs {
value := ctx.State.OutputValue(addrs.AbsOutputValue{
Module: addrs.RootModuleInstance,
OutputValue: addrs.OutputValue{
Name: output.Name,
},
})
if value.Sensitive {
outputs[output.Name] = value.Value.Mark(marks.Sensitive)
continue
}
outputs[output.Name] = value.Value
}
blocks[run] = cty.ObjectVal(outputs)
}
return map[string]cty.Value{
"run": cty.ObjectVal(blocks),
}
}(),
}
}

@ -132,6 +132,14 @@ func TestTest(t *testing.T) {
expected: "1 passed, 0 failed.",
code: 0,
},
"shared_state": {
expected: "2 passed, 0 failed.",
code: 0,
},
"shared_state_object": {
expected: "2 passed, 0 failed.",
code: 0,
},
}
for name, tc := range tcs {
t.Run(name, func(t *testing.T) {
@ -149,13 +157,33 @@ func TestTest(t *testing.T) {
defer testChdir(t, td)()
provider := testing_command.NewProvider(nil)
view, done := testView(t)
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{
testingOverrides: metaOverridesForProvider(provider.Provider),
View: view,
},
Meta: meta,
}
code := c.Run(tc.args)

@ -0,0 +1,12 @@
variable "input" {
type = string
}
resource "test_resource" "foo" {
value = var.input
}
output "value" {
value = test_resource.foo.value
}

@ -0,0 +1,37 @@
variables {
foo = "foo"
}
run "setup" {
module {
source = "./setup"
}
variables {
input = "foo"
}
assert {
condition = output.value == var.foo
error_message = "bad"
}
}
run "test" {
variables {
input = run.setup.value
}
assert {
condition = output.value == var.foo
error_message = "double bad"
}
assert {
condition = run.setup.value == var.foo
error_message = "triple bad"
}
}

@ -0,0 +1,12 @@
variable "input" {
type = string
}
resource "test_resource" "foo" {
value = var.input
}
output "value" {
value = test_resource.foo.value
}

@ -0,0 +1,12 @@
variable "input" {
type = string
}
resource "test_resource" "foo" {
value = var.input
}
output "value" {
value = test_resource.foo.value
}

@ -0,0 +1,37 @@
variables {
foo = "foo"
}
run "setup" {
module {
source = "./setup"
}
variables {
input = "foo"
}
assert {
condition = output.value == var.foo
error_message = "bad"
}
}
run "test" {
variables {
input = run.setup.value
}
assert {
condition = output.value == var.foo
error_message = "double bad"
}
assert {
condition = run.setup == { value : "foo" }
error_message = "triple bad"
}
}

@ -0,0 +1,12 @@
variable "input" {
type = string
}
resource "test_resource" "foo" {
value = var.input
}
output "value" {
value = test_resource.foo.value
}

@ -36,4 +36,5 @@ type Data interface {
GetInputVariable(addrs.InputVariable, tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics)
GetOutput(addrs.OutputValue, tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics)
GetCheckBlock(addrs.Check, tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics)
GetRunBlock(addrs.Run, tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics)
}

@ -21,6 +21,7 @@ type dataForTests struct {
TerraformAttrs map[string]cty.Value
InputVariables map[string]cty.Value
CheckBlocks map[string]cty.Value
RunBlocks map[string]cty.Value
}
var _ Data = &dataForTests{}
@ -74,3 +75,7 @@ func (d *dataForTests) GetOutput(addr addrs.OutputValue, rng tfdiags.SourceRange
func (d *dataForTests) GetCheckBlock(addr addrs.Check, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) {
return d.CheckBlocks[addr.Name], nil
}
func (d *dataForTests) GetRunBlock(addr addrs.Run, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) {
return d.RunBlocks[addr.Name], nil
}

@ -288,6 +288,7 @@ func (s *Scope) evalContext(refs []*addrs.Reference, selfAddr addrs.Referenceabl
countAttrs := map[string]cty.Value{}
forEachAttrs := map[string]cty.Value{}
checkBlocks := map[string]cty.Value{}
runBlocks := map[string]cty.Value{}
var self cty.Value
for _, ref := range refs {
@ -416,7 +417,12 @@ func (s *Scope) evalContext(refs []*addrs.Reference, selfAddr addrs.Referenceabl
case addrs.Check:
val, valDiags := normalizeRefValue(s.Data.GetCheckBlock(subj, rng))
diags = diags.Append(valDiags)
outputValues[subj.Name] = val
checkBlocks[subj.Name] = val
case addrs.Run:
val, valDiags := normalizeRefValue(s.Data.GetRunBlock(subj, rng))
diags = diags.Append(valDiags)
runBlocks[subj.Name] = val
default:
// Should never happen
@ -443,8 +449,9 @@ func (s *Scope) evalContext(refs []*addrs.Reference, selfAddr addrs.Referenceabl
vals["count"] = cty.ObjectVal(countAttrs)
vals["each"] = cty.ObjectVal(forEachAttrs)
// Checks and outputs are conditionally included in the available scope, so
// we'll only write out their values if we actually have something for them.
// Checks, outputs, and run blocks are conditionally included in the
// available scope, so we'll only write out their values if we actually have
// something for them.
if len(checkBlocks) > 0 {
vals["check"] = cty.ObjectVal(checkBlocks)
}
@ -453,6 +460,10 @@ func (s *Scope) evalContext(refs []*addrs.Reference, selfAddr addrs.Referenceabl
vals["output"] = cty.ObjectVal(outputValues)
}
if len(runBlocks) > 0 {
vals["run"] = cty.ObjectVal(runBlocks)
}
if self != cty.NilVal {
vals["self"] = self
}

@ -6,6 +6,7 @@ package lang
import (
"bytes"
"encoding/json"
"fmt"
"testing"
"github.com/hashicorp/terraform/internal/addrs"
@ -73,51 +74,70 @@ func TestScopeEvalContext(t *testing.T) {
InputVariables: map[string]cty.Value{
"baz": cty.StringVal("boop"),
},
OutputValues: map[string]cty.Value{
"rootoutput0": cty.StringVal("rootbar0"),
"rootoutput1": cty.StringVal("rootbar1"),
},
CheckBlocks: map[string]cty.Value{
"check0": cty.ObjectVal(map[string]cty.Value{
"status": cty.StringVal("pass"),
}),
"check1": cty.ObjectVal(map[string]cty.Value{
"status": cty.StringVal("fail"),
}),
},
RunBlocks: map[string]cty.Value{
"zero": cty.ObjectVal(map[string]cty.Value{
"run0output0": cty.StringVal("run0bar0"),
"run0output1": cty.StringVal("run0bar1"),
}),
},
}
tests := []struct {
Expr string
Want map[string]cty.Value
Expr string
Want map[string]cty.Value
TestingOnly bool
}{
{
`12`,
map[string]cty.Value{},
Expr: `12`,
Want: map[string]cty.Value{},
},
{
`count.index`,
map[string]cty.Value{
Expr: `count.index`,
Want: map[string]cty.Value{
"count": cty.ObjectVal(map[string]cty.Value{
"index": cty.NumberIntVal(0),
}),
},
},
{
`each.key`,
map[string]cty.Value{
Expr: `each.key`,
Want: map[string]cty.Value{
"each": cty.ObjectVal(map[string]cty.Value{
"key": cty.StringVal("a"),
}),
},
},
{
`each.value`,
map[string]cty.Value{
Expr: `each.value`,
Want: map[string]cty.Value{
"each": cty.ObjectVal(map[string]cty.Value{
"value": cty.NumberIntVal(1),
}),
},
},
{
`local.foo`,
map[string]cty.Value{
Expr: `local.foo`,
Want: map[string]cty.Value{
"local": cty.ObjectVal(map[string]cty.Value{
"foo": cty.StringVal("bar"),
}),
},
},
{
`null_resource.foo`,
map[string]cty.Value{
Expr: `null_resource.foo`,
Want: map[string]cty.Value{
"null_resource": cty.ObjectVal(map[string]cty.Value{
"foo": cty.ObjectVal(map[string]cty.Value{
"attr": cty.StringVal("bar"),
@ -133,8 +153,8 @@ func TestScopeEvalContext(t *testing.T) {
},
},
{
`null_resource.foo.attr`,
map[string]cty.Value{
Expr: `null_resource.foo.attr`,
Want: map[string]cty.Value{
"null_resource": cty.ObjectVal(map[string]cty.Value{
"foo": cty.ObjectVal(map[string]cty.Value{
"attr": cty.StringVal("bar"),
@ -150,8 +170,8 @@ func TestScopeEvalContext(t *testing.T) {
},
},
{
`null_resource.multi`,
map[string]cty.Value{
Expr: `null_resource.multi`,
Want: map[string]cty.Value{
"null_resource": cty.ObjectVal(map[string]cty.Value{
"multi": cty.TupleVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
@ -178,8 +198,8 @@ func TestScopeEvalContext(t *testing.T) {
},
{
// at this level, all instance references return the entire resource
`null_resource.multi[1]`,
map[string]cty.Value{
Expr: `null_resource.multi[1]`,
Want: map[string]cty.Value{
"null_resource": cty.ObjectVal(map[string]cty.Value{
"multi": cty.TupleVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
@ -206,8 +226,8 @@ func TestScopeEvalContext(t *testing.T) {
},
{
// at this level, all instance references return the entire resource
`null_resource.each["each1"]`,
map[string]cty.Value{
Expr: `null_resource.each["each1"]`,
Want: map[string]cty.Value{
"null_resource": cty.ObjectVal(map[string]cty.Value{
"each": cty.ObjectVal(map[string]cty.Value{
"each0": cty.ObjectVal(map[string]cty.Value{
@ -234,8 +254,8 @@ func TestScopeEvalContext(t *testing.T) {
},
{
// at this level, all instance references return the entire resource
`null_resource.each["each1"].attr`,
map[string]cty.Value{
Expr: `null_resource.each["each1"].attr`,
Want: map[string]cty.Value{
"null_resource": cty.ObjectVal(map[string]cty.Value{
"each": cty.ObjectVal(map[string]cty.Value{
"each0": cty.ObjectVal(map[string]cty.Value{
@ -261,8 +281,8 @@ func TestScopeEvalContext(t *testing.T) {
},
},
{
`foo(null_resource.multi, null_resource.multi[1])`,
map[string]cty.Value{
Expr: `foo(null_resource.multi, null_resource.multi[1])`,
Want: map[string]cty.Value{
"null_resource": cty.ObjectVal(map[string]cty.Value{
"multi": cty.TupleVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
@ -288,8 +308,8 @@ func TestScopeEvalContext(t *testing.T) {
},
},
{
`data.null_data_source.foo`,
map[string]cty.Value{
Expr: `data.null_data_source.foo`,
Want: map[string]cty.Value{
"data": cty.ObjectVal(map[string]cty.Value{
"null_data_source": cty.ObjectVal(map[string]cty.Value{
"foo": cty.ObjectVal(map[string]cty.Value{
@ -300,8 +320,8 @@ func TestScopeEvalContext(t *testing.T) {
},
},
{
`module.foo`,
map[string]cty.Value{
Expr: `module.foo`,
Want: map[string]cty.Value{
"module": cty.ObjectVal(map[string]cty.Value{
"foo": cty.ObjectVal(map[string]cty.Value{
"output0": cty.StringVal("bar0"),
@ -312,8 +332,8 @@ func TestScopeEvalContext(t *testing.T) {
},
// any module reference returns the entire module
{
`module.foo.output1`,
map[string]cty.Value{
Expr: `module.foo.output1`,
Want: map[string]cty.Value{
"module": cty.ObjectVal(map[string]cty.Value{
"foo": cty.ObjectVal(map[string]cty.Value{
"output0": cty.StringVal("bar0"),
@ -323,98 +343,158 @@ func TestScopeEvalContext(t *testing.T) {
},
},
{
`path.module`,
map[string]cty.Value{
Expr: `path.module`,
Want: map[string]cty.Value{
"path": cty.ObjectVal(map[string]cty.Value{
"module": cty.StringVal("foo/bar"),
}),
},
},
{
`self.baz`,
map[string]cty.Value{
Expr: `self.baz`,
Want: map[string]cty.Value{
"self": cty.ObjectVal(map[string]cty.Value{
"attr": cty.StringVal("multi1"),
}),
},
},
{
`terraform.workspace`,
map[string]cty.Value{
Expr: `terraform.workspace`,
Want: map[string]cty.Value{
"terraform": cty.ObjectVal(map[string]cty.Value{
"workspace": cty.StringVal("default"),
}),
},
},
{
`var.baz`,
map[string]cty.Value{
Expr: `var.baz`,
Want: map[string]cty.Value{
"var": cty.ObjectVal(map[string]cty.Value{
"baz": cty.StringVal("boop"),
}),
},
},
{
Expr: "run.zero",
Want: map[string]cty.Value{
"run": cty.ObjectVal(map[string]cty.Value{
"zero": cty.ObjectVal(map[string]cty.Value{
"run0output0": cty.StringVal("run0bar0"),
"run0output1": cty.StringVal("run0bar1"),
}),
}),
},
TestingOnly: true,
},
{
Expr: "run.zero.run0output0",
Want: map[string]cty.Value{
"run": cty.ObjectVal(map[string]cty.Value{
"zero": cty.ObjectVal(map[string]cty.Value{
"run0output0": cty.StringVal("run0bar0"),
"run0output1": cty.StringVal("run0bar1"),
}),
}),
},
TestingOnly: true,
},
{
Expr: "output.rootoutput0",
Want: map[string]cty.Value{
"output": cty.ObjectVal(map[string]cty.Value{
"rootoutput0": cty.StringVal("rootbar0"),
}),
},
TestingOnly: true,
},
{
Expr: "check.check0",
Want: map[string]cty.Value{
"check": cty.ObjectVal(map[string]cty.Value{
"check0": cty.ObjectVal(map[string]cty.Value{
"status": cty.StringVal("pass"),
}),
}),
},
TestingOnly: true,
},
}
for _, test := range tests {
t.Run(test.Expr, func(t *testing.T) {
expr, parseDiags := hclsyntax.ParseExpression([]byte(test.Expr), "", hcl.Pos{Line: 1, Column: 1})
if len(parseDiags) != 0 {
t.Errorf("unexpected diagnostics during parse")
for _, diag := range parseDiags {
t.Errorf("- %s", diag)
}
return
}
refs, refsDiags := ReferencesInExpr(addrs.ParseRef, expr)
if refsDiags.HasErrors() {
t.Fatal(refsDiags.Err())
exec := func(t *testing.T, parseRef ParseRef, test struct {
Expr string
Want map[string]cty.Value
TestingOnly bool
}) {
expr, parseDiags := hclsyntax.ParseExpression([]byte(test.Expr), "", hcl.Pos{Line: 1, Column: 1})
if len(parseDiags) != 0 {
t.Errorf("unexpected diagnostics during parse")
for _, diag := range parseDiags {
t.Errorf("- %s", diag)
}
scope := &Scope{
Data: data,
ParseRef: addrs.ParseRef,
// "self" will just be an arbitrary one of the several resource
// instances we have in our test dataset.
SelfAddr: addrs.ResourceInstance{
Resource: addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "null_resource",
Name: "multi",
},
Key: addrs.IntKey(1),
return
}
refs, refsDiags := ReferencesInExpr(parseRef, expr)
if refsDiags.HasErrors() {
t.Fatal(refsDiags.Err())
}
scope := &Scope{
Data: data,
ParseRef: parseRef,
// "self" will just be an arbitrary one of the several resource
// instances we have in our test dataset.
SelfAddr: addrs.ResourceInstance{
Resource: addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "null_resource",
Name: "multi",
},
Key: addrs.IntKey(1),
},
}
ctx, ctxDiags := scope.EvalContext(refs)
if ctxDiags.HasErrors() {
t.Fatal(ctxDiags.Err())
}
// For easier test assertions we'll just remove any top-level
// empty objects from our variables map.
for k, v := range ctx.Variables {
if v.RawEquals(cty.EmptyObjectVal) {
delete(ctx.Variables, k)
}
ctx, ctxDiags := scope.EvalContext(refs)
if ctxDiags.HasErrors() {
t.Fatal(ctxDiags.Err())
}
// For easier test assertions we'll just remove any top-level
// empty objects from our variables map.
for k, v := range ctx.Variables {
if v.RawEquals(cty.EmptyObjectVal) {
delete(ctx.Variables, k)
}
}
}
gotVal := cty.ObjectVal(ctx.Variables)
wantVal := cty.ObjectVal(test.Want)
if !gotVal.RawEquals(wantVal) {
// We'll JSON-ize our values here just so it's easier to
// read them in the assertion output.
gotJSON := formattedJSONValue(gotVal)
wantJSON := formattedJSONValue(wantVal)
t.Errorf(
"wrong result\nexpr: %s\ngot: %s\nwant: %s",
test.Expr, gotJSON, wantJSON,
)
}
}
gotVal := cty.ObjectVal(ctx.Variables)
wantVal := cty.ObjectVal(test.Want)
for _, test := range tests {
if !gotVal.RawEquals(wantVal) {
// We'll JSON-ize our values here just so it's easier to
// read them in the assertion output.
gotJSON := formattedJSONValue(gotVal)
wantJSON := formattedJSONValue(wantVal)
if !test.TestingOnly {
t.Run(test.Expr, func(t *testing.T) {
exec(t, addrs.ParseRef, test)
})
}
t.Errorf(
"wrong result\nexpr: %s\ngot: %s\nwant: %s",
test.Expr, gotJSON, wantJSON,
)
}
t.Run(fmt.Sprintf("%s-testing", test.Expr), func(t *testing.T) {
exec(t, addrs.ParseRefFromTestingScope, test)
})
}
}

@ -65,6 +65,13 @@ type Evaluator struct {
// ensures they can be safely accessed and modified concurrently.
Changes *plans.ChangesSync
// AlternateStates allows callers to reference states from outside this
// evaluator.
//
// The main use case here is for the testing framework to call into other
// run blocks.
AlternateStates map[string]*evaluationStateData
PlanTimestamp time.Time
}
@ -1005,6 +1012,33 @@ func (d *evaluationStateData) GetCheckBlock(addr addrs.Check, rng tfdiags.Source
return cty.NilVal, diags
}
func (d *evaluationStateData) GetRunBlock(run addrs.Run, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
data, exists := d.Evaluator.AlternateStates[run.Name]
if !exists {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Reference to unavailable run block",
Detail: fmt.Sprintf("The current test file either contains no %s, or hasn't executed it yet.", run),
Subject: rng.ToHCL().Ptr(),
})
return cty.DynamicVal, diags
}
outputs := make(map[string]cty.Value)
for _, outputCfg := range data.Evaluator.Config.Module.Outputs {
output, outputDiags := data.GetOutput(outputCfg.Addr(), rng)
diags = diags.Append(outputDiags)
if outputDiags.HasErrors() {
continue
}
outputs[outputCfg.Name] = output
}
return cty.ObjectVal(outputs), diags
}
// moduleDisplayAddr returns a string describing the given module instance
// address that is appropriate for returning to users in situations where the
// root module is possible. Specifically, it returns "the root module" if the

@ -634,3 +634,95 @@ func evaluatorForModule(stateSync *states.SyncState, changesSync *plans.ChangesS
Changes: changesSync,
}
}
func TestGetRunBlocks(t *testing.T) {
evaluator := &Evaluator{
AlternateStates: map[string]*evaluationStateData{
"zero": {
Evaluator: &Evaluator{
State: states.BuildState(func(state *states.SyncState) {
state.SetOutputValue(addrs.AbsOutputValue{
Module: addrs.RootModuleInstance,
OutputValue: addrs.OutputValue{
Name: "value",
},
}, cty.StringVal("Hello, world!"), false)
}).SyncWrapper(),
Config: &configs.Config{
Module: &configs.Module{
Outputs: map[string]*configs.Output{
"value": {
Name: "value",
},
},
},
},
},
},
"one": {
Evaluator: &Evaluator{
State: states.BuildState(func(state *states.SyncState) {
state.SetOutputValue(addrs.AbsOutputValue{
Module: addrs.RootModuleInstance,
OutputValue: addrs.OutputValue{
Name: "value",
},
}, cty.StringVal("Hello, universe!"), false)
}).SyncWrapper(),
Config: &configs.Config{
Module: &configs.Module{
Outputs: map[string]*configs.Output{
"value": {
Name: "value",
},
},
},
},
},
},
},
}
data := &evaluationStateData{
Evaluator: evaluator,
ModulePath: nil,
InstanceKeyData: EvalDataForNoInstanceKey,
}
scope := evaluator.Scope(data, nil, nil)
got, diags := scope.Data.GetRunBlock(addrs.Run{Name: "zero"}, tfdiags.SourceRange{})
want := cty.ObjectVal(map[string]cty.Value{
"value": cty.StringVal("Hello, world!"),
})
if diags.HasErrors() {
t.Fatalf("unexpected diagnostics: %s", diags.Err())
}
if !got.RawEquals(want) {
t.Errorf("\ngot: %#v\nwant: %#v", got, want)
}
got, diags = scope.Data.GetRunBlock(addrs.Run{Name: "one"}, tfdiags.SourceRange{})
want = cty.ObjectVal(map[string]cty.Value{
"value": cty.StringVal("Hello, universe!"),
})
if diags.HasErrors() {
t.Fatalf("unexpected diagnostics: %s", diags.Err())
}
if !got.RawEquals(want) {
t.Errorf("\ngot: %#v\nwant: %#v", got, want)
}
_, diags = scope.Data.GetRunBlock(addrs.Run{Name: "two"}, tfdiags.SourceRange{})
if !diags.HasErrors() {
t.Fatalf("expected some diags but got none")
}
if diags[0].Description().Summary != "Reference to unavailable run block" {
t.Errorf("retrieved unexpected diagnostic: %s", diags[0].Description().Summary)
}
}

@ -111,6 +111,10 @@ func (d *evaluationStateData) staticValidateReference(ref *addrs.Reference, self
}
return d.staticValidateModuleCallReference(modCfg, addr.Call.Call, remain, ref.SourceRange)
// We can also validate any run blocks that are referenced actually exist.
case addrs.Run:
return d.staticValidateRunBlockReference(addr, ref.Remaining, ref.SourceRange)
default:
// Anything else we'll just permit through without any static validation
// and let it be caught during dynamic evaluation, in evaluate.go .
@ -315,6 +319,33 @@ func (d *evaluationStateData) staticValidateModuleCallReference(modCfg *configs.
return diags
}
func (d *evaluationStateData) staticValidateRunBlockReference(addr addrs.Run, remain hcl.Traversal, rng tfdiags.SourceRange) tfdiags.Diagnostics {
var diags tfdiags.Diagnostics
_, exists := d.Evaluator.AlternateStates[addr.Name]
if !exists {
var suggestions []string
for name := range d.Evaluator.AlternateStates {
suggestions = append(suggestions, name)
}
sort.Strings(suggestions)
suggestion := didyoumean.NameSuggestion(addr.Name, suggestions)
if suggestion != "" {
suggestion = fmt.Sprintf(" Did you mean %q?", suggestion)
}
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: `Reference to unavailable run block`,
Detail: fmt.Sprintf(`The run block named %q is not available, either it does not exist or has not yet been executed.%s`, addr.Name, suggestion),
Subject: rng.ToHCL().Ptr(),
})
return diags
}
return diags
}
// moduleConfigDisplayAddr returns a string describing the given module
// address that is appropriate for returning to users in situations where the
// root module is possible. Specifically, it returns "the root module" if the

@ -18,9 +18,10 @@ import (
func TestStaticValidateReferences(t *testing.T) {
tests := []struct {
Ref string
Src addrs.Referenceable
WantErr string
Ref string
Src addrs.Referenceable
ParseRef lang.ParseRef
WantErr string
}{
{
Ref: "aws_instance.no_count",
@ -79,6 +80,15 @@ For example, to correlate with indices of a referring resource, use:
WantErr: ``,
Src: addrs.Check{Name: "foo"},
},
{
Ref: "run.zero",
WantErr: `Reference to undeclared resource: A managed resource "run" "zero" has not been declared in the root module.`,
},
{
Ref: "run.zero",
ParseRef: addrs.ParseRefFromTestingScope,
WantErr: `Reference to unavailable run block: The run block named "zero" is not available, either it does not exist or has not yet been executed.`,
},
}
cfg := testModule(t, "static-validate-refs")
@ -122,7 +132,12 @@ For example, to correlate with indices of a referring resource, use:
t.Fatal(hclDiags.Error())
}
refs, diags := lang.References(addrs.ParseRef, []hcl.Traversal{traversal})
parseRef := addrs.ParseRef
if test.ParseRef != nil {
parseRef = test.ParseRef
}
refs, diags := lang.References(parseRef, []hcl.Traversal{traversal})
if diags.HasErrors() {
t.Fatal(diags.Err())
}

@ -25,6 +25,7 @@ import (
type TestContext struct {
*Context
Run *moduletest.Run
Config *configs.Config
State *states.State
Plan *plans.Plan
@ -33,9 +34,10 @@ type TestContext struct {
// TestContext creates a TestContext structure that can evaluate test assertions
// against the provided state and plan.
func (c *Context) TestContext(config *configs.Config, state *states.State, plan *plans.Plan, variables InputValues) *TestContext {
func (c *Context) TestContext(run *moduletest.Run, config *configs.Config, state *states.State, plan *plans.Plan, variables InputValues) *TestContext {
return &TestContext{
Context: c,
Run: run,
Config: config,
State: state,
Plan: plan,
@ -43,29 +45,27 @@ func (c *Context) TestContext(config *configs.Config, state *states.State, plan
}
}
// Evaluate processes the assertions inside the provided configs.TestRun against
// the embedded state.
func (ctx *TestContext) Evaluate(run *moduletest.Run) {
defer ctx.acquireRun("evaluate")()
switch run.Config.Command {
func (ctx *TestContext) evaluationStateData(alternateStates map[string]*evaluationStateData) *evaluationStateData {
var operation walkOperation
switch ctx.Run.Config.Command {
case configs.PlanTestCommand:
ctx.evaluate(ctx.State.SyncWrapper(), ctx.Plan.Changes.SyncWrapper(), run, walkPlan)
operation = walkPlan
case configs.ApplyTestCommand:
ctx.evaluate(ctx.State.SyncWrapper(), ctx.Plan.Changes.SyncWrapper(), run, walkApply)
operation = walkApply
default:
panic(fmt.Errorf("unrecognized TestCommand: %q", run.Config.Command))
panic(fmt.Errorf("unrecognized TestCommand: %q", ctx.Run.Config.Command))
}
}
func (ctx *TestContext) evaluate(state *states.SyncState, changes *plans.ChangesSync, run *moduletest.Run, operation walkOperation) {
data := &evaluationStateData{
return &evaluationStateData{
Evaluator: &Evaluator{
Operation: operation,
Meta: ctx.meta,
Config: ctx.Config,
Plugins: ctx.plugins,
State: state,
Changes: changes,
Operation: operation,
Meta: ctx.meta,
Config: ctx.Config,
Plugins: ctx.plugins,
State: ctx.State.SyncWrapper(),
Changes: ctx.Plan.Changes.SyncWrapper(),
AlternateStates: alternateStates,
VariableValues: func() map[string]map[string]cty.Value {
variables := map[string]map[string]cty.Value{
addrs.RootModule.String(): make(map[string]cty.Value),
@ -82,16 +82,28 @@ func (ctx *TestContext) evaluate(state *states.SyncState, changes *plans.Changes
InstanceKeyData: EvalDataForNoInstanceKey,
Operation: operation,
}
}
// Evaluate processes the assertions inside the provided configs.TestRun against
// the embedded state.
func (ctx *TestContext) Evaluate(priorContexts map[string]*TestContext) {
alternateStates := make(map[string]*evaluationStateData)
for name, priorContext := range priorContexts {
alternateStates[name] = priorContext.evaluationStateData(nil)
}
data := ctx.evaluationStateData(alternateStates)
scope := &lang.Scope{
Data: data,
BaseDir: ".",
PureOnly: operation != walkApply,
PureOnly: data.Operation != walkApply,
PlanTimestamp: ctx.Plan.Timestamp,
}
// We're going to assume the run has passed, and then if anything fails this
// value will be updated.
run := ctx.Run
run.Status = run.Status.Merge(moduletest.Pass)
// Now validate all the assertions within this run block.

@ -9,6 +9,7 @@ import (
ctymsgpack "github.com/zclconf/go-cty/cty/msgpack"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/configs"
"github.com/hashicorp/terraform/internal/configs/configschema"
"github.com/hashicorp/terraform/internal/moduletest"
"github.com/hashicorp/terraform/internal/plans"
@ -19,11 +20,12 @@ import (
func TestTestContext_Evaluate(t *testing.T) {
tcs := map[string]struct {
configs map[string]string
state *states.State
plan *plans.Plan
variables InputValues
provider *MockProvider
configs map[string]string
state *states.State
plan *plans.Plan
variables InputValues
provider *MockProvider
priorStates map[string]func(config *configs.Config) *TestContext
expectedDiags []tfdiags.Description
expectedStatus moduletest.Status
@ -532,6 +534,94 @@ run "test_case" {
},
},
},
"with_prior_state": {
configs: map[string]string{
"main.tf": `
resource "test_resource" "a" {
value = "Hello, world!"
}
`,
"main.tftest.hcl": `
run "setup" {}
run "test_case" {
assert {
condition = test_resource.a.value == run.setup.value
error_message = "invalid value"
}
}
`,
},
plan: &plans.Plan{
Changes: plans.NewChanges(),
},
state: states.BuildState(func(state *states.SyncState) {
state.SetResourceInstanceCurrent(
addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "test_resource",
Name: "a",
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: encodeCtyValue(t, cty.ObjectVal(map[string]cty.Value{
"value": cty.StringVal("Hello, world!"),
})),
},
addrs.AbsProviderConfig{
Module: addrs.RootModule,
Provider: addrs.NewDefaultProvider("test"),
})
}),
priorStates: map[string]func(config *configs.Config) *TestContext{
"setup": func(config *configs.Config) *TestContext {
return &TestContext{
Context: &Context{},
Run: &moduletest.Run{
Config: config.Module.Tests["main.tftest.hcl"].Runs[0],
Name: "setup",
},
Config: &configs.Config{
Module: &configs.Module{
Outputs: map[string]*configs.Output{
"value": {
Name: "value",
},
},
},
},
Plan: &plans.Plan{
Changes: plans.NewChanges(),
},
State: states.BuildState(func(state *states.SyncState) {
state.SetOutputValue(addrs.AbsOutputValue{
Module: addrs.RootModuleInstance,
OutputValue: addrs.OutputValue{
Name: "value",
},
}, cty.StringVal("Hello, world!"), false)
}),
}
},
},
provider: &MockProvider{
GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{
ResourceTypes: map[string]providers.Schema{
"test_resource": {
Block: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"value": {
Type: cty.String,
Required: true,
},
},
},
},
},
},
},
expectedStatus: moduletest.Pass,
},
}
for name, tc := range tcs {
t.Run(name, func(t *testing.T) {
@ -542,13 +632,19 @@ run "test_case" {
},
})
priorStates := make(map[string]*TestContext)
for run, ps := range tc.priorStates {
priorStates[run] = ps(config)
}
file := config.Module.Tests["main.tftest.hcl"]
run := moduletest.Run{
Config: config.Module.Tests["main.tftest.hcl"].Runs[0],
Name: "test_case",
Config: file.Runs[len(file.Runs)-1], // We always simulate the last run block.
Name: "test_case", // and it should be named test_case
}
tctx := ctx.TestContext(config, tc.state, tc.plan, tc.variables)
tctx.Evaluate(&run)
tctx := ctx.TestContext(&run, config, tc.state, tc.plan, tc.variables)
tctx.Evaluate(priorStates)
if expected, actual := tc.expectedStatus, run.Status; expected != actual {
t.Errorf("expected status \"%s\" but got \"%s\"", expected, actual)

Loading…
Cancel
Save