Skip to content

Commit 3732bff

Browse files
author
Liam Cervante
authored
[Testing Framework] Adds TestContext for evaluating test assertions (#33326)
* Add test structure to views package for rendering test output * Add test file HCL configuration and parser functionality * Adds a TestContext structure for evaluating assertions against the state and plan
1 parent d49e991 commit 3732bff

13 files changed

+909
-34
lines changed

internal/plans/plan.go

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,12 @@ import (
77
"sort"
88
"time"
99

10+
"github.com/zclconf/go-cty/cty"
11+
1012
"github.com/hashicorp/terraform/internal/addrs"
1113
"github.com/hashicorp/terraform/internal/configs/configschema"
1214
"github.com/hashicorp/terraform/internal/lang/globalref"
1315
"github.com/hashicorp/terraform/internal/states"
14-
"github.com/zclconf/go-cty/cty"
1516
)
1617

1718
// Plan is the top-level type representing a planned set of changes.
@@ -88,6 +89,29 @@ type Plan struct {
8889
PrevRunState *states.State
8990
PriorState *states.State
9091

92+
// PlannedState is the temporary planned state that was created during the
93+
// graph walk that generated this plan.
94+
//
95+
// This is required by the testing framework when evaluating run blocks
96+
// executing in plan mode. The graph updates the state with certain values
97+
// that are difficult to retrieve later, such as local values that reference
98+
// updated resources. It is easier to build the testing scope with access
99+
// to same temporary state the plan used/built.
100+
//
101+
// This is never recorded outside of Terraform. It is not written into the
102+
// binary plan file, and it is not written into the JSON structured outputs.
103+
// The testing framework never writes the plans out but holds everything in
104+
// memory as it executes, so there is no need to add any kind of
105+
// serialization for this field. This does mean that you shouldn't rely on
106+
// this field existing unless you have just generated the plan.
107+
PlannedState *states.State
108+
109+
// ExternalReferences are references that are being made to resources within
110+
// the plan from external sources. As with PlannedState this is used by the
111+
// terraform testing framework, and so isn't written into any external
112+
// representation of the plan.
113+
ExternalReferences []*addrs.Reference
114+
91115
// Timestamp is the record of truth for when the plan happened.
92116
Timestamp time.Time
93117
}

internal/terraform/context_apply.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,13 @@ import (
77
"fmt"
88
"log"
99

10+
"github.com/zclconf/go-cty/cty"
11+
1012
"github.com/hashicorp/terraform/internal/addrs"
1113
"github.com/hashicorp/terraform/internal/configs"
1214
"github.com/hashicorp/terraform/internal/plans"
1315
"github.com/hashicorp/terraform/internal/states"
1416
"github.com/hashicorp/terraform/internal/tfdiags"
15-
"github.com/zclconf/go-cty/cty"
1617
)
1718

1819
// Apply performs the actions described by the given Plan object and returns
@@ -176,6 +177,7 @@ func (c *Context) applyGraph(plan *plans.Plan, config *configs.Config, validate
176177
Targets: plan.TargetAddrs,
177178
ForceReplace: plan.ForceReplaceAddrs,
178179
Operation: operation,
180+
ExternalReferences: plan.ExternalReferences,
179181
}).Build(addrs.RootModuleInstance)
180182
diags = diags.Append(moreDiags)
181183
if moreDiags.HasErrors() {

internal/terraform/context_apply2_test.go

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2162,3 +2162,85 @@ import {
21622162
t.Errorf("expected addr to be %s, but was %s", wantAddr, addr)
21632163
}
21642164
}
2165+
2166+
func TestContext2Apply_noExternalReferences(t *testing.T) {
2167+
m := testModuleInline(t, map[string]string{
2168+
"main.tf": `
2169+
resource "test_object" "a" {
2170+
test_string = "foo"
2171+
}
2172+
2173+
locals {
2174+
local_value = test_object.a.test_string
2175+
}
2176+
`,
2177+
})
2178+
2179+
p := simpleMockProvider()
2180+
ctx := testContext2(t, &ContextOpts{
2181+
Providers: map[addrs.Provider]providers.Factory{
2182+
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
2183+
},
2184+
})
2185+
2186+
plan, diags := ctx.Plan(m, states.NewState(), nil)
2187+
if diags.HasErrors() {
2188+
t.Errorf("expected no errors, but got %s", diags)
2189+
}
2190+
2191+
state, diags := ctx.Apply(plan, m)
2192+
if diags.HasErrors() {
2193+
t.Errorf("expected no errors, but got %s", diags)
2194+
}
2195+
2196+
// We didn't specify any external references, so the unreferenced local
2197+
// value should have been tidied up and never made it into the state.
2198+
module := state.RootModule()
2199+
if len(module.LocalValues) > 0 {
2200+
t.Errorf("expected no local values in the state but found %d", len(module.LocalValues))
2201+
}
2202+
}
2203+
2204+
func TestContext2Apply_withExternalReferences(t *testing.T) {
2205+
m := testModuleInline(t, map[string]string{
2206+
"main.tf": `
2207+
resource "test_object" "a" {
2208+
test_string = "foo"
2209+
}
2210+
2211+
locals {
2212+
local_value = test_object.a.test_string
2213+
}
2214+
`,
2215+
})
2216+
2217+
p := simpleMockProvider()
2218+
ctx := testContext2(t, &ContextOpts{
2219+
Providers: map[addrs.Provider]providers.Factory{
2220+
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
2221+
},
2222+
})
2223+
2224+
plan, diags := ctx.Plan(m, states.NewState(), &PlanOpts{
2225+
Mode: plans.NormalMode,
2226+
ExternalReferences: []*addrs.Reference{
2227+
mustReference("local.local_value"),
2228+
},
2229+
})
2230+
if diags.HasErrors() {
2231+
t.Errorf("expected no errors, but got %s", diags)
2232+
}
2233+
2234+
state, diags := ctx.Apply(plan, m)
2235+
if diags.HasErrors() {
2236+
t.Errorf("expected no errors, but got %s", diags)
2237+
}
2238+
2239+
// We did specify the local value in the external references, so it should
2240+
// have been preserved even though it is not referenced by anything directly
2241+
// in the config.
2242+
module := state.RootModule()
2243+
if module.LocalValues["local_value"].AsString() != "foo" {
2244+
t.Errorf("expected local value to be \"foo\" but was \"%s\"", module.LocalValues["local_value"].AsString())
2245+
}
2246+
}

internal/terraform/context_plan.go

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,11 @@ type PlanOpts struct {
7474
// fully-functional new object.
7575
ForceReplace []addrs.AbsResourceInstance
7676

77+
// ExternalReferences allows the external caller to pass in references to
78+
// nodes that should not be pruned even if they are not referenced within
79+
// the actual graph.
80+
ExternalReferences []*addrs.Reference
81+
7782
// ImportTargets is a list of target resources to import. These resources
7883
// will be added to the plan graph.
7984
ImportTargets []*ImportTarget
@@ -644,13 +649,15 @@ func (c *Context) planWalk(config *configs.Config, prevRunState *states.State, o
644649
diags = diags.Append(driftDiags)
645650

646651
plan := &plans.Plan{
647-
UIMode: opts.Mode,
648-
Changes: changes,
649-
DriftedResources: driftedResources,
650-
PrevRunState: prevRunState,
651-
PriorState: priorState,
652-
Checks: states.NewCheckResults(walker.Checks),
653-
Timestamp: timestamp,
652+
UIMode: opts.Mode,
653+
Changes: changes,
654+
DriftedResources: driftedResources,
655+
PrevRunState: prevRunState,
656+
PriorState: priorState,
657+
PlannedState: walker.State.Close(),
658+
ExternalReferences: opts.ExternalReferences,
659+
Checks: states.NewCheckResults(walker.Checks),
660+
Timestamp: timestamp,
654661

655662
// Other fields get populated by Context.Plan after we return
656663
}
@@ -670,6 +677,7 @@ func (c *Context) planGraph(config *configs.Config, prevRunState *states.State,
670677
skipRefresh: opts.SkipRefresh,
671678
preDestroyRefresh: opts.PreDestroyRefresh,
672679
Operation: walkPlan,
680+
ExternalReferences: opts.ExternalReferences,
673681
ImportTargets: opts.ImportTargets,
674682
GenerateConfigPath: opts.GenerateConfigPath,
675683
}).Build(addrs.RootModuleInstance)
@@ -684,6 +692,7 @@ func (c *Context) planGraph(config *configs.Config, prevRunState *states.State,
684692
skipRefresh: opts.SkipRefresh,
685693
skipPlanChanges: true, // this activates "refresh only" mode.
686694
Operation: walkPlan,
695+
ExternalReferences: opts.ExternalReferences,
687696
}).Build(addrs.RootModuleInstance)
688697
return graph, walkPlan, diags
689698
case plans.DestroyMode:

internal/terraform/context_plan2_test.go

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import (
1313

1414
"github.com/davecgh/go-spew/spew"
1515
"github.com/google/go-cmp/cmp"
16+
"github.com/zclconf/go-cty/cty"
17+
1618
"github.com/hashicorp/terraform/internal/addrs"
1719
"github.com/hashicorp/terraform/internal/checks"
1820
"github.com/hashicorp/terraform/internal/configs/configschema"
@@ -21,7 +23,6 @@ import (
2123
"github.com/hashicorp/terraform/internal/providers"
2224
"github.com/hashicorp/terraform/internal/states"
2325
"github.com/hashicorp/terraform/internal/tfdiags"
24-
"github.com/zclconf/go-cty/cty"
2526
)
2627

2728
func TestContext2Plan_removedDuringRefresh(t *testing.T) {
@@ -4888,3 +4889,54 @@ import {
48884889
t.Errorf("got:\n%s\nwant:\n%s\ndiff:\n%s", got, want, diff)
48894890
}
48904891
}
4892+
4893+
func TestContext2Plan_plannedState(t *testing.T) {
4894+
addr := mustResourceInstanceAddr("test_object.a")
4895+
m := testModuleInline(t, map[string]string{
4896+
"main.tf": `
4897+
resource "test_object" "a" {
4898+
test_string = "foo"
4899+
}
4900+
4901+
locals {
4902+
local_value = test_object.a.test_string
4903+
}
4904+
`,
4905+
})
4906+
4907+
p := simpleMockProvider()
4908+
ctx := testContext2(t, &ContextOpts{
4909+
Providers: map[addrs.Provider]providers.Factory{
4910+
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
4911+
},
4912+
})
4913+
4914+
state := states.NewState()
4915+
plan, diags := ctx.Plan(m, state, nil)
4916+
if diags.HasErrors() {
4917+
t.Errorf("expected no errors, but got %s", diags)
4918+
}
4919+
4920+
module := state.RootModule()
4921+
4922+
// So, the original state shouldn't have been updated at all.
4923+
if len(module.LocalValues) > 0 {
4924+
t.Errorf("expected no local values in the state but found %d", len(module.LocalValues))
4925+
}
4926+
4927+
if len(module.Resources) > 0 {
4928+
t.Errorf("expected no resources in the state but found %d", len(module.LocalValues))
4929+
}
4930+
4931+
// But, this makes it hard for the testing framework to valid things about
4932+
// the returned plan. So, the plan contains the planned state:
4933+
module = plan.PlannedState.RootModule()
4934+
4935+
if module.LocalValues["local_value"].AsString() != "foo" {
4936+
t.Errorf("expected local value to be \"foo\" but was \"%s\"", module.LocalValues["local_value"].AsString())
4937+
}
4938+
4939+
if module.ResourceInstance(addr.Resource).Current.Status != states.ObjectPlanned {
4940+
t.Errorf("expected resource to be in planned state")
4941+
}
4942+
}

internal/terraform/evaluate.go

Lines changed: 4 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,13 @@ import (
1111
"sync"
1212
"time"
1313

14-
"github.com/agext/levenshtein"
1514
"github.com/hashicorp/hcl/v2"
1615
"github.com/zclconf/go-cty/cty"
1716

1817
"github.com/hashicorp/terraform/internal/addrs"
1918
"github.com/hashicorp/terraform/internal/configs"
2019
"github.com/hashicorp/terraform/internal/configs/configschema"
20+
"github.com/hashicorp/terraform/internal/didyoumean"
2121
"github.com/hashicorp/terraform/internal/instances"
2222
"github.com/hashicorp/terraform/internal/lang"
2323
"github.com/hashicorp/terraform/internal/lang/marks"
@@ -229,7 +229,7 @@ func (d *evaluationStateData) GetInputVariable(addr addrs.InputVariable, rng tfd
229229
for k := range moduleConfig.Module.Variables {
230230
suggestions = append(suggestions, k)
231231
}
232-
suggestion := nameSuggestion(addr.Name, suggestions)
232+
suggestion := didyoumean.NameSuggestion(addr.Name, suggestions)
233233
if suggestion != "" {
234234
suggestion = fmt.Sprintf(" Did you mean %q?", suggestion)
235235
} else {
@@ -325,7 +325,7 @@ func (d *evaluationStateData) GetLocalValue(addr addrs.LocalValue, rng tfdiags.S
325325
for k := range moduleConfig.Module.Locals {
326326
suggestions = append(suggestions, k)
327327
}
328-
suggestion := nameSuggestion(addr.Name, suggestions)
328+
suggestion := didyoumean.NameSuggestion(addr.Name, suggestions)
329329
if suggestion != "" {
330330
suggestion = fmt.Sprintf(" Did you mean %q?", suggestion)
331331
}
@@ -624,7 +624,7 @@ func (d *evaluationStateData) GetPathAttr(addr addrs.PathAttr, rng tfdiags.Sourc
624624
return cty.StringVal(filepath.ToSlash(sourceDir)), diags
625625

626626
default:
627-
suggestion := nameSuggestion(addr.Name, []string{"cwd", "module", "root"})
627+
suggestion := didyoumean.NameSuggestion(addr.Name, []string{"cwd", "module", "root"})
628628
if suggestion != "" {
629629
suggestion = fmt.Sprintf(" Did you mean %q?", suggestion)
630630
}
@@ -940,25 +940,6 @@ func (d *evaluationStateData) GetTerraformAttr(addr addrs.TerraformAttr, rng tfd
940940
}
941941
}
942942

943-
// nameSuggestion tries to find a name from the given slice of suggested names
944-
// that is close to the given name and returns it if found. If no suggestion
945-
// is close enough, returns the empty string.
946-
//
947-
// The suggestions are tried in order, so earlier suggestions take precedence
948-
// if the given string is similar to two or more suggestions.
949-
//
950-
// This function is intended to be used with a relatively-small number of
951-
// suggestions. It's not optimized for hundreds or thousands of them.
952-
func nameSuggestion(given string, suggestions []string) string {
953-
for _, suggestion := range suggestions {
954-
dist := levenshtein.Distance(given, suggestion, nil)
955-
if dist < 3 { // threshold determined experimentally
956-
return suggestion
957-
}
958-
}
959-
return ""
960-
}
961-
962943
// moduleDisplayAddr returns a string describing the given module instance
963944
// address that is appropriate for returning to users in situations where the
964945
// root module is possible. Specifically, it returns "the root module" if the

internal/terraform/graph_builder_apply.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,11 @@ type ApplyGraphBuilder struct {
5252

5353
// Plan Operation this graph will be used for.
5454
Operation walkOperation
55+
56+
// ExternalReferences allows the external caller to pass in references to
57+
// nodes that should not be pruned even if they are not referenced within
58+
// the actual graph.
59+
ExternalReferences []*addrs.Reference
5560
}
5661

5762
// See GraphBuilder
@@ -144,6 +149,11 @@ func (b *ApplyGraphBuilder) Steps() []GraphTransformer {
144149
// objects that can belong to modules.
145150
&ModuleExpansionTransformer{Config: b.Config},
146151

152+
// Plug in any external references.
153+
&ExternalReferenceTransformer{
154+
ExternalReferences: b.ExternalReferences,
155+
},
156+
147157
// Connect references so ordering is correct
148158
&ReferenceTransformer{},
149159
&AttachDependenciesTransformer{},

internal/terraform/graph_builder_plan.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,11 @@ type PlanGraphBuilder struct {
7373
// Plan Operation this graph will be used for.
7474
Operation walkOperation
7575

76+
// ExternalReferences allows the external caller to pass in references to
77+
// nodes that should not be pruned even if they are not referenced within
78+
// the actual graph.
79+
ExternalReferences []*addrs.Reference
80+
7681
// ImportTargets are the list of resources to import.
7782
ImportTargets []*ImportTarget
7883

@@ -193,6 +198,11 @@ func (b *PlanGraphBuilder) Steps() []GraphTransformer {
193198
// objects that can belong to modules.
194199
&ModuleExpansionTransformer{Concrete: b.ConcreteModule, Config: b.Config},
195200

201+
// Plug in any external references.
202+
&ExternalReferenceTransformer{
203+
ExternalReferences: b.ExternalReferences,
204+
},
205+
196206
&ReferenceTransformer{},
197207

198208
&AttachDependenciesTransformer{},

0 commit comments

Comments
 (0)