stacks: introduce shared functions for common tests (#35718)

pull/35723/head
Liam Cervante 2 years ago committed by GitHub
parent 9ea9905b43
commit 0ae6bc34c4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -6,11 +6,11 @@ package stackruntime
import (
"context"
"path"
"sort"
"strconv"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/hashicorp/hcl/v2"
"github.com/zclconf/go-cty/cty"
"github.com/hashicorp/terraform/internal/addrs"
@ -35,21 +35,12 @@ func TestApplyDestroy(t *testing.T) {
t.Fatal(err)
}
type cycle struct {
mode plans.Mode
inputs map[string]cty.Value
wantPlannedChanges []stackplan.PlannedChange
wantPlannedDiags []expectedDiagnostic
wantAppliedChanges []stackstate.AppliedChange
wantAppliedDiags []expectedDiagnostic
}
tcs := map[string]struct {
path string
description string
state *stackstate.State
store *stacks_testing_provider.ResourceStore
cycles []cycle
cycles []TestCycle
}{
"missing-resource": {
path: path.Join("with-single-input", "valid"),
@ -70,10 +61,10 @@ func TestApplyDestroy(t *testing.T) {
Status: states.ObjectReady,
})).
Build(),
cycles: []cycle{
cycles: []TestCycle{
{
mode: plans.DestroyMode,
inputs: map[string]cty.Value{
planMode: plans.DestroyMode,
planInputs: map[string]cty.Value{
"input": cty.StringVal("hello"),
},
wantAppliedChanges: []stackstate.AppliedChange{
@ -119,10 +110,10 @@ func TestApplyDestroy(t *testing.T) {
Status: states.ObjectReady,
})).
Build(),
cycles: []cycle{
cycles: []TestCycle{
{
mode: plans.DestroyMode,
inputs: map[string]cty.Value{
planMode: plans.DestroyMode,
planInputs: map[string]cty.Value{
"id": cty.StringVal("foo"),
"resource": cty.StringVal("bar"),
},
@ -183,10 +174,10 @@ func TestApplyDestroy(t *testing.T) {
Status: states.ObjectReady,
})).
Build(),
cycles: []cycle{
cycles: []TestCycle{
{
mode: plans.NormalMode,
inputs: map[string]cty.Value{
planMode: plans.NormalMode,
planInputs: map[string]cty.Value{
"id": cty.StringVal("foo"),
"resource": cty.StringVal("bar"),
},
@ -239,8 +230,8 @@ func TestApplyDestroy(t *testing.T) {
},
},
{
mode: plans.DestroyMode,
inputs: map[string]cty.Value{
planMode: plans.DestroyMode,
planInputs: map[string]cty.Value{
"id": cty.StringVal("foo"),
"resource": cty.StringVal("bar"),
},
@ -273,9 +264,9 @@ func TestApplyDestroy(t *testing.T) {
"dependent-resources": {
path: "dependent-component",
description: "test the order of operations during create and destroy",
cycles: []cycle{
cycles: []TestCycle{
{
mode: plans.NormalMode,
planMode: plans.NormalMode,
wantAppliedChanges: []stackstate.AppliedChange{
&stackstate.AppliedChangeComponentInstance{
ComponentAddr: mustAbsComponent("component.self"),
@ -329,7 +320,7 @@ func TestApplyDestroy(t *testing.T) {
},
},
{
mode: plans.DestroyMode,
planMode: plans.DestroyMode,
wantAppliedChanges: []stackstate.AppliedChange{
&stackstate.AppliedChangeComponentInstance{
ComponentAddr: mustAbsComponent("component.self"),
@ -391,9 +382,9 @@ func TestApplyDestroy(t *testing.T) {
"fail_apply": cty.True,
})).
Build(),
cycles: []cycle{
cycles: []TestCycle{
{
mode: plans.DestroyMode,
planMode: plans.DestroyMode,
wantAppliedChanges: []stackstate.AppliedChange{
&stackstate.AppliedChangeComponentInstance{
ComponentAddr: mustAbsComponent("component.self"),
@ -422,18 +413,22 @@ func TestApplyDestroy(t *testing.T) {
Schema: stacks_testing_provider.FailedResourceSchema,
},
},
wantAppliedDiags: []expectedDiagnostic{
expectDiagnostic(tfdiags.Error, "failedResource error", "failed during apply"),
},
wantAppliedDiags: initDiags(func(diags tfdiags.Diagnostics) tfdiags.Diagnostics {
return diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "failedResource error",
Detail: "failed during apply",
})
}),
},
},
},
"destroy-after-failed-apply": {
path: path.Join("with-single-input", "failed-child"),
description: "tests destroying when state is only partially applied",
cycles: []cycle{
cycles: []TestCycle{
{
mode: plans.NormalMode,
planMode: plans.NormalMode,
wantAppliedChanges: []stackstate.AppliedChange{
&stackstate.AppliedChangeComponentInstance{
ComponentAddr: mustAbsComponent("component.child"),
@ -475,12 +470,16 @@ func TestApplyDestroy(t *testing.T) {
Schema: stacks_testing_provider.TestingResourceSchema,
},
},
wantAppliedDiags: []expectedDiagnostic{
expectDiagnostic(tfdiags.Error, "failedResource error", "failed during apply"),
},
wantAppliedDiags: initDiags(func(diags tfdiags.Diagnostics) tfdiags.Diagnostics {
return diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "failedResource error",
Detail: "failed during apply",
})
}),
},
{
mode: plans.DestroyMode,
planMode: plans.DestroyMode,
wantAppliedChanges: []stackstate.AppliedChange{
&stackstate.AppliedChangeComponentInstance{
ComponentAddr: mustAbsComponent("component.child"),
@ -515,9 +514,9 @@ func TestApplyDestroy(t *testing.T) {
"destroy-after-deferred-apply": {
path: "deferred-dependent",
description: "tests what happens when a destroy plan is applied after components have been deferred",
cycles: []cycle{
cycles: []TestCycle{
{
mode: plans.NormalMode,
planMode: plans.NormalMode,
wantAppliedChanges: []stackstate.AppliedChange{
&stackstate.AppliedChangeComponentInstance{
ComponentAddr: mustAbsComponent("component.deferred"),
@ -555,7 +554,7 @@ func TestApplyDestroy(t *testing.T) {
},
},
{
mode: plans.DestroyMode,
planMode: plans.DestroyMode,
wantAppliedChanges: []stackstate.AppliedChange{
&stackstate.AppliedChangeComponentInstance{
ComponentAddr: mustAbsComponent("component.deferred"),
@ -626,9 +625,9 @@ func TestApplyDestroy(t *testing.T) {
"deferred": cty.True,
})).
Build(),
cycles: []cycle{
cycles: []TestCycle{
{
mode: plans.DestroyMode,
planMode: plans.DestroyMode,
wantPlannedChanges: []stackplan.PlannedChange{
&stackplan.PlannedChangeApplyable{
Applyable: true,
@ -762,13 +761,13 @@ func TestApplyDestroy(t *testing.T) {
"destroy-with-input-dependency": {
path: path.Join("with-single-input-and-output", "input-dependency"),
description: "tests destroy operations with input dependencies",
cycles: []cycle{
cycles: []TestCycle{
{
// Just create everything normally, and don't validate it.
mode: plans.NormalMode,
planMode: plans.NormalMode,
},
{
mode: plans.DestroyMode,
planMode: plans.DestroyMode,
wantAppliedChanges: []stackstate.AppliedChange{
&stackstate.AppliedChangeComponentInstance{
ComponentAddr: mustAbsComponent("component.child"),
@ -805,13 +804,13 @@ func TestApplyDestroy(t *testing.T) {
"destroy-with-provider-dependency": {
path: path.Join("with-single-input-and-output", "provider-dependency"),
description: "tests destroy operations with provider dependencies",
cycles: []cycle{
cycles: []TestCycle{
{
// Just create everything normally, and don't validate it.
mode: plans.NormalMode,
planMode: plans.NormalMode,
},
{
mode: plans.DestroyMode,
planMode: plans.DestroyMode,
wantAppliedChanges: []stackstate.AppliedChange{
&stackstate.AppliedChangeComponentInstance{
ComponentAddr: mustAbsComponent("component.child"),
@ -848,13 +847,13 @@ func TestApplyDestroy(t *testing.T) {
"destroy-with-for-each-dependency": {
path: path.Join("with-single-input-and-output", "for-each-dependency"),
description: "tests destroy operations with for-each dependencies",
cycles: []cycle{
cycles: []TestCycle{
{
// Just create everything normally, and don't validate it.
mode: plans.NormalMode,
planMode: plans.NormalMode,
},
{
mode: plans.DestroyMode,
planMode: plans.DestroyMode,
wantAppliedChanges: []stackstate.AppliedChange{
&stackstate.AppliedChangeComponentInstance{
ComponentAddr: mustAbsComponent("component.child"),
@ -891,9 +890,7 @@ func TestApplyDestroy(t *testing.T) {
}
for name, tc := range tcs {
t.Run(name, func(t *testing.T) {
ctx := context.Background()
cfg := loadMainBundleConfigForTest(t, tc.path)
lock := depsfile.NewLocks()
lock.SetProvider(
@ -908,123 +905,28 @@ func TestApplyDestroy(t *testing.T) {
store = stacks_testing_provider.NewResourceStore()
}
state := tc.state
for ix, cycle := range tc.cycles {
planReq := PlanRequest{
PlanMode: cycle.mode,
Config: cfg,
ProviderFactories: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) {
return stacks_testing_provider.NewProviderWithData(t, store), nil
},
testContext := TestContext{
timestamp: &fakePlanTimestamp,
config: loadMainBundleConfigForTest(t, tc.path),
providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) {
return stacks_testing_provider.NewProviderWithData(t, store), nil
},
DependencyLocks: *lock,
ForcePlanTimestamp: &fakePlanTimestamp,
InputValues: func() map[stackaddrs.InputVariable]ExternalInputValue {
inputs := make(map[stackaddrs.InputVariable]ExternalInputValue, len(cycle.inputs))
for k, v := range cycle.inputs {
inputs[stackaddrs.InputVariable{Name: k}] = ExternalInputValue{Value: v}
}
return inputs
}(),
PrevState: state,
}
planChangesCh := make(chan stackplan.PlannedChange)
planDiagsCh := make(chan tfdiags.Diagnostic)
planResp := PlanResponse{
PlannedChanges: planChangesCh,
Diagnostics: planDiagsCh,
}
go Plan(ctx, &planReq, &planResp)
planChanges, planDiags := collectPlanOutput(planChangesCh, planDiagsCh)
expectDiagnosticsForTest(t, planDiags, cycle.wantPlannedDiags...)
},
dependencyLocks: *lock,
}
if cycle.wantPlannedChanges != nil {
// nil indicates skip this check, empty slice indicates no changes expected.
sort.SliceStable(planChanges, func(i, j int) bool {
return plannedChangeSortKey(planChanges[i]) < plannedChangeSortKey(planChanges[j])
state := tc.state
for ix, cycle := range tc.cycles {
t.Run(strconv.FormatInt(int64(ix), 10), func(t *testing.T) {
var plan *stackplan.Plan
t.Run("plan", func(t *testing.T) {
plan = testContext.Plan(t, ctx, state, cycle)
})
if diff := cmp.Diff(cycle.wantPlannedChanges, planChanges, changesCmpOpts); diff != "" {
t.Fatalf("wrong planned changes %d\n%s", ix, diff)
}
}
planLoader := stackplan.NewLoader()
for _, change := range planChanges {
proto, err := change.PlannedChangeProto()
if err != nil {
t.Fatal(err)
}
for _, rawMsg := range proto.Raw {
err = planLoader.AddRaw(rawMsg)
if err != nil {
t.Fatal(err)
}
}
}
plan, err := planLoader.Plan()
if err != nil {
t.Fatal(err)
}
applyReq := ApplyRequest{
Config: cfg,
Plan: plan,
ProviderFactories: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) {
return stacks_testing_provider.NewProviderWithData(t, store), nil
},
},
DependencyLocks: *lock,
}
applyChangesCh := make(chan stackstate.AppliedChange)
applyDiagsCh := make(chan tfdiags.Diagnostic)
applyResp := ApplyResponse{
AppliedChanges: applyChangesCh,
Diagnostics: applyDiagsCh,
}
go Apply(ctx, &applyReq, &applyResp)
applyChanges, applyDiags := collectApplyOutput(applyChangesCh, applyDiagsCh)
expectDiagnosticsForTest(t, applyDiags, cycle.wantAppliedDiags...)
if cycle.wantAppliedChanges != nil {
// nil indicates skip this check, empty slice indicates no changes expected.
sort.SliceStable(applyChanges, func(i, j int) bool {
return appliedChangeSortKey(applyChanges[i]) < appliedChangeSortKey(applyChanges[j])
t.Run("apply", func(t *testing.T) {
state = testContext.Apply(t, ctx, plan, cycle)
})
if diff := cmp.Diff(cycle.wantAppliedChanges, applyChanges, changesCmpOpts); diff != "" {
t.Fatalf("wrong applied changes %d\n%s", ix, diff)
}
}
stateLoader := stackstate.NewLoader()
for _, change := range applyChanges {
proto, err := change.AppliedChangeProto()
if err != nil {
t.Fatal(err)
}
for _, rawMsg := range proto.Raw {
if rawMsg.Value == nil {
// This is a removal notice, so we don't need to add it to the
// state.
continue
}
err = stateLoader.AddRaw(rawMsg.Key, rawMsg.Value)
if err != nil {
t.Fatal(err)
}
}
}
state = stateLoader.State()
})
}
})

@ -4,11 +4,14 @@
package stackruntime
import (
"context"
"crypto/sha256"
"encoding/json"
"fmt"
"sort"
"strings"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/hashicorp/go-slug/sourceaddrs"
@ -17,7 +20,9 @@ import (
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/configs/configschema"
"github.com/hashicorp/terraform/internal/depsfile"
"github.com/hashicorp/terraform/internal/plans"
"github.com/hashicorp/terraform/internal/providers"
"github.com/hashicorp/terraform/internal/stacks/stackaddrs"
"github.com/hashicorp/terraform/internal/stacks/stackconfig"
"github.com/hashicorp/terraform/internal/stacks/stackplan"
@ -28,6 +33,200 @@ import (
// This file has helper functions used by other tests. It doesn't contain any
// test cases of its own.
// TestContext contains all the information shared across multiple operations
// in a single test.
type TestContext struct {
// timestamp is the timestamp that should be applied for this test.
timestamp *time.Time
// config is the config to use for this test.
config *stackconfig.Config
// providers are the providers that should be available within this test.
providers map[addrs.Provider]providers.Factory
// dependencyLocks is the locks file that should be used for this test.
dependencyLocks depsfile.Locks
}
// TestCycle defines a single plan / apply cycle that should be performed within
// a test.
type TestCycle struct {
// Validate options
wantValidateDiags tfdiags.Diagnostics
// Plan options
planMode plans.Mode
planInputs map[string]cty.Value
wantPlannedChanges []stackplan.PlannedChange
wantPlannedDiags tfdiags.Diagnostics
// Apply options
applyInputs map[string]cty.Value
wantAppliedChanges []stackstate.AppliedChange
wantAppliedDiags tfdiags.Diagnostics
}
func (tc TestContext) Validate(t *testing.T, ctx context.Context, cycle TestCycle) {
t.Helper()
gotDiags := Validate(ctx, &ValidateRequest{
Config: tc.config,
ProviderFactories: tc.providers,
DependencyLocks: tc.dependencyLocks,
ExperimentsAllowed: true,
})
validateDiags(t, cycle.wantValidateDiags, gotDiags)
}
func (tc TestContext) Plan(t *testing.T, ctx context.Context, state *stackstate.State, cycle TestCycle) *stackplan.Plan {
t.Helper()
request := PlanRequest{
PlanMode: cycle.planMode,
Config: tc.config,
PrevState: state,
InputValues: func() map[stackaddrs.InputVariable]ExternalInputValue {
inputs := make(map[stackaddrs.InputVariable]ExternalInputValue, len(cycle.planInputs))
for k, v := range cycle.planInputs {
inputs[stackaddrs.InputVariable{Name: k}] = ExternalInputValue{Value: v}
}
return inputs
}(),
ProviderFactories: tc.providers,
DependencyLocks: tc.dependencyLocks,
ForcePlanTimestamp: tc.timestamp,
ExperimentsAllowed: true,
}
changesCh := make(chan stackplan.PlannedChange)
diagsCh := make(chan tfdiags.Diagnostic)
response := PlanResponse{
PlannedChanges: changesCh,
Diagnostics: diagsCh,
}
go Plan(ctx, &request, &response)
changes, diags := collectPlanOutput(changesCh, diagsCh)
validateDiags(t, cycle.wantPlannedDiags, diags)
if cycle.wantPlannedChanges != nil {
// if this is nil (as opposed to empty) then we don't validate the
// returned changes.
sort.SliceStable(changes, func(i, j int) bool {
return plannedChangeSortKey(changes[i]) < plannedChangeSortKey(changes[j])
})
if diff := cmp.Diff(cycle.wantPlannedChanges, changes, changesCmpOpts); len(diff) > 0 {
t.Errorf("wrong planned changes\n%s", diff)
}
}
planLoader := stackplan.NewLoader()
for _, change := range changes {
proto, err := change.PlannedChangeProto()
if err != nil {
t.Fatal(err)
}
for _, rawMsg := range proto.Raw {
err = planLoader.AddRaw(rawMsg)
if err != nil {
t.Fatal(err)
}
}
}
plan, err := planLoader.Plan()
if err != nil {
t.Fatal(err)
}
return plan
}
func (tc TestContext) Apply(t *testing.T, ctx context.Context, plan *stackplan.Plan, cycle TestCycle) *stackstate.State {
t.Helper()
request := ApplyRequest{
Config: tc.config,
Plan: plan,
InputValues: func() map[stackaddrs.InputVariable]ExternalInputValue {
inputs := make(map[stackaddrs.InputVariable]ExternalInputValue, len(cycle.applyInputs))
for k, v := range cycle.applyInputs {
inputs[stackaddrs.InputVariable{Name: k}] = ExternalInputValue{Value: v}
}
return inputs
}(),
ProviderFactories: tc.providers,
ExperimentsAllowed: true,
DependencyLocks: tc.dependencyLocks,
}
changesCh := make(chan stackstate.AppliedChange)
diagsCh := make(chan tfdiags.Diagnostic)
response := ApplyResponse{
AppliedChanges: changesCh,
Diagnostics: diagsCh,
}
go Apply(ctx, &request, &response)
changes, diags := collectApplyOutput(changesCh, diagsCh)
validateDiags(t, cycle.wantAppliedDiags, diags)
if cycle.wantAppliedChanges != nil {
// nil indicates skip this check, empty slice indicates no changes expected.
sort.SliceStable(changes, func(i, j int) bool {
return appliedChangeSortKey(changes[i]) < appliedChangeSortKey(changes[j])
})
if diff := cmp.Diff(cycle.wantAppliedChanges, changes, changesCmpOpts); diff != "" {
t.Errorf("wrong applied changes\n%s", diff)
}
}
stateLoader := stackstate.NewLoader()
for _, change := range changes {
proto, err := change.AppliedChangeProto()
if err != nil {
t.Fatal(err)
}
for _, rawMsg := range proto.Raw {
if rawMsg.Value == nil {
// This is a removal notice, so we don't need to add it to the
// state.
continue
}
err = stateLoader.AddRaw(rawMsg.Key, rawMsg.Value)
if err != nil {
t.Fatal(err)
}
}
}
return stateLoader.State()
}
func initDiags(cb func(diags tfdiags.Diagnostics) tfdiags.Diagnostics) tfdiags.Diagnostics {
var diags tfdiags.Diagnostics
return cb(diags)
}
func validateDiags(t *testing.T, wantDiags, gotDiags tfdiags.Diagnostics) {
t.Helper()
sort.SliceStable(gotDiags, diagnosticSortFunc(gotDiags))
sort.SliceStable(wantDiags, diagnosticSortFunc(wantDiags))
gotDiags = gotDiags.ForRPC()
wantDiags = wantDiags.ForRPC()
if diff := cmp.Diff(wantDiags, gotDiags); len(diff) > 0 {
t.Errorf("wrong diagnostics\n%s", diff)
}
}
// loadConfigForTest is a test helper that tries to open bundleRoot as a
// source bundle, and then if successful tries to load the given source address
// from it as a stack configuration. If any part of the operation fails then

@ -58,17 +58,8 @@ func TestPlan_valid(t *testing.T) {
// We've added this test before the implementation was ready.
t.SkipNow()
}
ctx := context.Background()
cfg := loadMainBundleConfigForTest(t, name)
fakePlanTimestamp, err := time.Parse(time.RFC3339, "1991-08-25T20:57:08Z")
if err != nil {
t.Fatal(err)
}
changesCh := make(chan stackplan.PlannedChange, 8)
diagsCh := make(chan tfdiags.Diagnostic, 2)
lock := depsfile.NewLocks()
lock.SetProvider(
addrs.NewDefaultProvider("testing"),
@ -82,9 +73,15 @@ func TestPlan_valid(t *testing.T) {
providerreqs.MustParseVersionConstraints("=0.0.0"),
providerreqs.PreferredHashes([]providerreqs.Hash{}),
)
req := PlanRequest{
Config: cfg,
ProviderFactories: map[addrs.Provider]providers.Factory{
fakePlanTimestamp, err := time.Parse(time.RFC3339, "1991-08-25T20:57:08Z")
if err != nil {
t.Fatal(err)
}
testContext := TestContext{
config: loadMainBundleConfigForTest(t, name),
providers: map[addrs.Provider]providers.Factory{
// We support both hashicorp/testing and
// terraform.io/builtin/testing as providers. This lets us
// test the provider aliasing feature. Both providers
@ -101,40 +98,16 @@ func TestPlan_valid(t *testing.T) {
return stacks_testing_provider.NewProvider(t), nil
},
},
DependencyLocks: *lock,
InputValues: func() map[stackaddrs.InputVariable]ExternalInputValue {
inputs := map[stackaddrs.InputVariable]ExternalInputValue{}
for k, v := range tc.planInputVars {
inputs[stackaddrs.InputVariable{Name: k}] = ExternalInputValue{
Value: v,
}
}
return inputs
}(),
ForcePlanTimestamp: &fakePlanTimestamp,
}
resp := PlanResponse{
PlannedChanges: changesCh,
Diagnostics: diagsCh,
dependencyLocks: *lock,
timestamp: &fakePlanTimestamp,
}
go Plan(ctx, &req, &resp)
_, diags := collectPlanOutput(changesCh, diagsCh)
// We don't care about the planned changes here, just the
// diagnostics.
// The following will fail the test if there are any error
// diagnostics.
reportDiagnosticsForTest(t, diags)
// We also want to fail if there are just warnings, since the
// configurations here are supposed to be totally problem-free.
if len(diags) != 0 {
// reportDiagnosticsForTest already showed the diagnostics in
// the log
t.FailNow()
cycle := TestCycle{
planInputs: tc.planInputVars,
wantPlannedChanges: nil, // don't care about the planned changes in this test.
wantPlannedDiags: nil, // should return no diagnostics.
}
testContext.Plan(t, ctx, nil, cycle)
})
}
}
@ -158,17 +131,8 @@ func TestPlan_invalid(t *testing.T) {
// We've added this test before the implementation was ready.
t.SkipNow()
}
ctx := context.Background()
cfg := loadMainBundleConfigForTest(t, name)
fakePlanTimestamp, err := time.Parse(time.RFC3339, "1991-08-25T20:57:08Z")
if err != nil {
t.Fatal(err)
}
changesCh := make(chan stackplan.PlannedChange, 8)
diagsCh := make(chan tfdiags.Diagnostic, 2)
lock := depsfile.NewLocks()
lock.SetProvider(
addrs.NewDefaultProvider("testing"),
@ -176,9 +140,15 @@ func TestPlan_invalid(t *testing.T) {
providerreqs.MustParseVersionConstraints("=0.0.0"),
providerreqs.PreferredHashes([]providerreqs.Hash{}),
)
req := PlanRequest{
Config: cfg,
ProviderFactories: map[addrs.Provider]providers.Factory{
fakePlanTimestamp, err := time.Parse(time.RFC3339, "1991-08-25T20:57:08Z")
if err != nil {
t.Fatal(err)
}
testContext := TestContext{
config: loadMainBundleConfigForTest(t, name),
providers: map[addrs.Provider]providers.Factory{
// We support both hashicorp/testing and
// terraform.io/builtin/testing as providers. This lets us
// test the provider aliasing feature. Both providers
@ -190,31 +160,16 @@ func TestPlan_invalid(t *testing.T) {
return stacks_testing_provider.NewProvider(t), nil
},
},
DependencyLocks: *lock,
InputValues: func() map[stackaddrs.InputVariable]ExternalInputValue {
inputs := map[stackaddrs.InputVariable]ExternalInputValue{}
for k, v := range tc.planInputVars {
inputs[stackaddrs.InputVariable{Name: k}] = ExternalInputValue{
Value: v,
}
}
return inputs
}(),
ForcePlanTimestamp: &fakePlanTimestamp,
dependencyLocks: *lock,
timestamp: &fakePlanTimestamp,
}
resp := PlanResponse{
PlannedChanges: changesCh,
Diagnostics: diagsCh,
}
go Plan(ctx, &req, &resp)
_, gotDiags := collectPlanOutput(changesCh, diagsCh)
wantDiags := tc.diags()
sort.SliceStable(gotDiags, diagnosticSortFunc(gotDiags))
if diff := cmp.Diff(wantDiags.ForRPC(), gotDiags.ForRPC()); diff != "" {
t.Errorf("wrong diagnostics\n%s", diff)
cycle := TestCycle{
planInputs: tc.planInputVars,
wantPlannedChanges: nil, // don't care about the planned changes in this test.
wantPlannedDiags: tc.diags(),
}
testContext.Plan(t, ctx, nil, cycle)
})
}
}

@ -6,11 +6,8 @@ package stackruntime
import (
"context"
"path/filepath"
"sort"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/hashicorp/hcl/v2"
"github.com/zclconf/go-cty/cty"
@ -338,9 +335,8 @@ func TestValidate_valid(t *testing.T) {
// We've added this test before the implementation was ready.
t.SkipNow()
}
ctx := context.Background()
cfg := loadMainBundleConfigForTest(t, name)
lock := depsfile.NewLocks()
lock.SetProvider(
addrs.NewDefaultProvider("testing"),
@ -355,9 +351,9 @@ func TestValidate_valid(t *testing.T) {
providerreqs.PreferredHashes([]providerreqs.Hash{}),
)
diags := Validate(ctx, &ValidateRequest{
Config: cfg,
ProviderFactories: map[addrs.Provider]providers.Factory{
testContext := TestContext{
config: loadMainBundleConfigForTest(t, name),
providers: map[addrs.Provider]providers.Factory{
// We support both hashicorp/testing and
// terraform.io/builtin/testing as providers. This lets us
// test the provider aliasing feature. Both providers
@ -374,20 +370,11 @@ func TestValidate_valid(t *testing.T) {
return stacks_testing_provider.NewProvider(t), nil
},
},
DependencyLocks: *lock,
})
// The following will fail the test if there are any error
// diagnostics.
reportDiagnosticsForTest(t, diags)
// We also want to fail if there are just warnings, since the
// configurations here are supposed to be totally problem-free.
if len(diags) != 0 {
// reportDiagnosticsForTest already showed the diagnostics in
// the log
t.FailNow()
dependencyLocks: *lock,
}
cycle := TestCycle{} // empty, as we expect no diagnostics
testContext.Validate(t, ctx, cycle)
})
}
}
@ -399,9 +386,7 @@ func TestValidate_invalid(t *testing.T) {
// We've added this test before the implementation was ready.
t.SkipNow()
}
ctx := context.Background()
cfg := loadMainBundleConfigForTest(t, name)
lock := depsfile.NewLocks()
lock.SetProvider(
@ -410,10 +395,16 @@ func TestValidate_invalid(t *testing.T) {
providerreqs.MustParseVersionConstraints("=0.0.0"),
providerreqs.PreferredHashes([]providerreqs.Hash{}),
)
lock.SetProvider(
addrs.NewDefaultProvider("other"),
providerreqs.MustParseVersion("0.0.0"),
providerreqs.MustParseVersionConstraints("=0.0.0"),
providerreqs.PreferredHashes([]providerreqs.Hash{}),
)
gotDiags := Validate(ctx, &ValidateRequest{
Config: cfg,
ProviderFactories: map[addrs.Provider]providers.Factory{
testContext := TestContext{
config: loadMainBundleConfigForTest(t, name),
providers: map[addrs.Provider]providers.Factory{
// We support both hashicorp/testing and
// terraform.io/builtin/testing as providers. This lets us
// test the provider aliasing feature. Both providers
@ -424,128 +415,81 @@ func TestValidate_invalid(t *testing.T) {
addrs.NewBuiltInProvider("testing"): func() (providers.Interface, error) {
return stacks_testing_provider.NewProvider(t), nil
},
// We also support an "other" provider out of the box to
// test the provider aliasing feature.
addrs.NewDefaultProvider("other"): func() (providers.Interface, error) {
return stacks_testing_provider.NewProvider(t), nil
},
},
DependencyLocks: *lock,
}).ForRPC()
// Let's make the returned diagnostics stable so that we can
// compare them easily.
sort.SliceStable(gotDiags, diagnosticSortFunc(gotDiags))
wantDiags := tc.diags().ForRPC()
if diff := cmp.Diff(wantDiags, gotDiags); diff != "" {
t.Errorf("wrong diagnostics\n%s", diff)
dependencyLocks: *lock,
}
testContext.Validate(t, ctx, TestCycle{
wantValidateDiags: tc.diags(),
})
})
}
}
func TestValidate_embeddedStackSelfRef(t *testing.T) {
ctx := context.Background()
// One possible failure mode for this test is to deadlock itself if
// our deadlock detection is incorrect, so we'll try to make it bail
// if it runs too long.
ctx, cancel := context.WithTimeout(ctx, time.Second*5)
defer cancel()
ctx, span := tracer.Start(ctx, "TestValidate_embeddedStackSelfRef")
defer span.End()
cfg := loadMainBundleConfigForTest(t, "validate-embedded-stack-selfref")
gotDiags := Validate(ctx, &ValidateRequest{
Config: cfg,
})
// We'll normalize the diagnostics to be of consistent underlying type
// using ForRPC, so that we can easily diff them; we don't actually care
// about which underlying implementation is in use.
gotDiags = gotDiags.ForRPC()
var wantDiags tfdiags.Diagnostics
wantDiags = wantDiags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Self-dependent items in configuration",
`The following items in your configuration form a circular dependency chain through their references:
func TestValidate(t *testing.T) {
tcs := map[string]struct {
path string
providers map[addrs.Provider]providers.Factory
locks *depsfile.Locks
wantDiags tfdiags.Diagnostics
}{
"embedded-stack-selfref": {
path: "validate-embedded-stack-selfref",
wantDiags: initDiags(func(diags tfdiags.Diagnostics) tfdiags.Diagnostics {
return diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Self-dependent items in configuration",
`The following items in your configuration form a circular dependency chain through their references:
- stack.a collected outputs
- stack.a.output.a value
- stack.a inputs
Terraform uses references to decide a suitable order for performing operations, so configuration items may not refer to their own results either directly or indirectly.`,
))
wantDiags = wantDiags.ForRPC()
if diff := cmp.Diff(wantDiags, gotDiags); diff != "" {
t.Errorf("wrong diagnostics\n%s", diff)
}
}
func TestValidate_missing_provider_from_lockfile(t *testing.T) {
ctx := context.Background()
cfg := loadMainBundleConfigForTest(t, filepath.Join("with-single-input", "input-from-component"))
lock := depsfile.NewLocks()
diags := Validate(ctx, &ValidateRequest{
Config: cfg,
ProviderFactories: map[addrs.Provider]providers.Factory{
// We support both hashicorp/testing and
// terraform.io/builtin/testing as providers. This lets us
// test the provider aliasing feature. Both providers
// support the same set of resources and data sources.
addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) {
return stacks_testing_provider.NewProvider(t), nil
},
addrs.NewBuiltInProvider("testing"): func() (providers.Interface, error) {
return stacks_testing_provider.NewProvider(t), nil
))
}),
},
"missing-provider-from-lockfile": {
path: filepath.Join("with-single-input", "input-from-component"),
providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) {
return stacks_testing_provider.NewProvider(t), nil
},
},
locks: depsfile.NewLocks(), // deliberately empty
wantDiags: initDiags(func(diags tfdiags.Diagnostics) tfdiags.Diagnostics {
return diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Provider missing from lockfile",
Detail: "Provider \"registry.terraform.io/hashicorp/testing\" is not in the lockfile. This provider must be in the lockfile to be used in the configuration. Please run `tfstacks providers lock` to update the lockfile and run this operation again with an updated configuration.",
Subject: &hcl.Range{
Filename: "git::https://example.com/test.git//with-single-input/input-from-component/input-from-component.tfstack.hcl",
Start: hcl.Pos{Line: 8, Column: 1, Byte: 98},
End: hcl.Pos{Line: 8, Column: 29, Byte: 126},
},
})
}),
},
DependencyLocks: *lock,
})
if len(diags) != 1 {
t.Fatalf("expected exactly one diagnostic, got %d", len(diags))
}
diag := diags[0]
if diag.Severity() != tfdiags.Error {
t.Fatalf("expected error diagnostic, got %s", diag.Severity())
}
if diag.Description().Summary != "Provider missing from lockfile" {
t.Fatalf("expected diagnostic summary 'Provider missing from lockfile', got %q", diag.Description().Summary)
}
if diag.Description().Detail != "Provider \"registry.terraform.io/hashicorp/testing\" is not in the lockfile. This provider must be in the lockfile to be used in the configuration. Please run `tfstacks providers lock` to update the lockfile and run this operation again with an updated configuration." {
t.Fatalf("expected diagnostic detail to be a specific message, got %q", diag.Description().Detail)
}
}
func TestValidate_impliedProviderTypes(t *testing.T) {
tcs := []struct {
directory string
providers map[addrs.Provider]providers.Factory
wantDiags func() tfdiags.Diagnostics
}{
{
directory: "with-hashicorp-provider",
"implied-provider-type-with-hashicorp-provider": {
path: filepath.Join("legacy-module", "with-hashicorp-provider"),
providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) {
return stacks_testing_provider.NewProvider(t), nil
},
},
},
{
directory: "with-non-hashicorp-provider",
"implied-provider-type-with-non-hashicorp-provider": {
path: filepath.Join("legacy-module", "with-non-hashicorp-provider"),
providers: map[addrs.Provider]providers.Factory{
addrs.NewProvider(addrs.DefaultProviderRegistryHost, "other", "testing"): func() (providers.Interface, error) {
return stacks_testing_provider.NewProvider(t), nil
},
},
wantDiags: func() tfdiags.Diagnostics {
var diags tfdiags.Diagnostics
diags = diags.Append(&hcl.Diagnostic{
wantDiags: initDiags(func(diags tfdiags.Diagnostics) tfdiags.Diagnostics {
return diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid provider configuration",
Detail: "The provider configuration slot \"testing\" requires a configuration for provider \"registry.terraform.io/hashicorp/testing\", not for provider \"registry.terraform.io/other/testing\"." +
@ -556,40 +500,37 @@ func TestValidate_impliedProviderTypes(t *testing.T) {
End: hcl.Pos{Line: 21, Column: 39, Byte: 471},
},
})
return diags
},
}),
},
}
for _, tc := range tcs {
t.Run(tc.directory, func(t *testing.T) {
for name, tc := range tcs {
t.Run(name, func(t *testing.T) {
ctx := context.Background()
lock := depsfile.NewLocks()
for addr := range tc.providers {
lock.SetProvider(
addr,
providerreqs.MustParseVersion("0.0.0"),
providerreqs.MustParseVersionConstraints("=0.0.0"),
providerreqs.PreferredHashes([]providerreqs.Hash{}),
)
ctx, span := tracer.Start(ctx, name)
defer span.End()
locks := tc.locks
if locks == nil {
locks = depsfile.NewLocks()
for addr := range tc.providers {
locks.SetProvider(
addr,
providerreqs.MustParseVersion("0.0.0"),
providerreqs.MustParseVersionConstraints("=0.0.0"),
providerreqs.PreferredHashes([]providerreqs.Hash{}),
)
}
}
cfg := loadMainBundleConfigForTest(t, filepath.Join("legacy-module", tc.directory))
gotDiags := Validate(ctx, &ValidateRequest{
Config: cfg,
ProviderFactories: tc.providers,
DependencyLocks: *lock,
}).ForRPC()
wantDiags := tfdiags.Diagnostics{}.ForRPC()
if tc.wantDiags != nil {
wantDiags = tc.wantDiags().ForRPC()
}
if diff := cmp.Diff(wantDiags, gotDiags); diff != "" {
t.Errorf("wrong diagnostics\n%s", diff)
testContext := TestContext{
config: loadMainBundleConfigForTest(t, tc.path),
providers: tc.providers,
dependencyLocks: *locks,
}
testContext.Validate(t, ctx, TestCycle{
wantValidateDiags: tc.wantDiags,
})
})
}
}

Loading…
Cancel
Save