mirror of
https://github.com/hashicorp/terraform.git
synced 2026-04-15 22:10:37 -04:00
191 lines
6.4 KiB
Go
191 lines
6.4 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
package stackruntime
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/hashicorp/go-slug/sourceaddrs"
|
|
"github.com/hashicorp/go-slug/sourcebundle"
|
|
"github.com/zclconf/go-cty/cty"
|
|
|
|
"github.com/hashicorp/terraform/internal/configs/configschema"
|
|
"github.com/hashicorp/terraform/internal/plans"
|
|
"github.com/hashicorp/terraform/internal/stacks/stackconfig"
|
|
"github.com/hashicorp/terraform/internal/stacks/stackplan"
|
|
"github.com/hashicorp/terraform/internal/stacks/stackstate"
|
|
"github.com/hashicorp/terraform/internal/tfdiags"
|
|
)
|
|
|
|
// This file has helper functions used by other tests. It doesn't contain any
|
|
// test cases of its own.
|
|
|
|
// 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
|
|
// it halts execution of the test and doesn't return.
|
|
func loadConfigForTest(t *testing.T, bundleRoot string, configSourceAddr string) *stackconfig.Config {
|
|
t.Helper()
|
|
sources, err := sourcebundle.OpenDir(bundleRoot)
|
|
if err != nil {
|
|
t.Fatalf("cannot load source bundle: %s", err)
|
|
}
|
|
|
|
// We force using remote source addresses here because that avoids
|
|
// us having to deal with the extra version constraints argument
|
|
// that registry sources require. Exactly what source address type
|
|
// we use isn't relevant for tests in this package, since it's
|
|
// the sourcebundle package's responsibility to make sure its
|
|
// abstraction works for all of the source types.
|
|
sourceAddr, err := sourceaddrs.ParseRemoteSource(configSourceAddr)
|
|
if err != nil {
|
|
t.Fatalf("invalid config source address: %s", err)
|
|
}
|
|
|
|
cfg, diags := stackconfig.LoadConfigDir(sourceAddr, sources)
|
|
reportDiagnosticsForTest(t, diags)
|
|
return cfg
|
|
}
|
|
|
|
func mainBundleSourceAddrStr(dirName string) string {
|
|
return "git::https://example.com/test.git//" + dirName
|
|
}
|
|
|
|
// loadMainBundleConfigForTest is a convenience wrapper around
|
|
// loadConfigForTest that knows the location and package address of our
|
|
// "main" source bundle, in ./testdata/mainbundle, so that we can use that
|
|
// conveniently without duplicating its location and synthetic package address
|
|
// in every single test function.
|
|
//
|
|
// dirName should begin with the name of a subdirectory that's present in
|
|
// ./testdata/mainbundle/test . It can optionally refer to subdirectories
|
|
// thereof, using forward slashes as the path separator just as we'd do
|
|
// in the subdirectory portion of a remote source address (which is exactly
|
|
// what we're using this as.)
|
|
func loadMainBundleConfigForTest(t *testing.T, dirName string) *stackconfig.Config {
|
|
t.Helper()
|
|
fullSourceAddr := mainBundleSourceAddrStr(dirName)
|
|
return loadConfigForTest(t, "./testdata/mainbundle", fullSourceAddr)
|
|
}
|
|
|
|
// reportDiagnosticsForTest creates a test log entry for every diagnostic in
|
|
// the given diags, and halts the test if any of them are error diagnostics.
|
|
func reportDiagnosticsForTest(t *testing.T, diags tfdiags.Diagnostics) {
|
|
t.Helper()
|
|
for _, diag := range diags {
|
|
var b strings.Builder
|
|
desc := diag.Description()
|
|
locs := diag.Source()
|
|
|
|
switch sev := diag.Severity(); sev {
|
|
case tfdiags.Error:
|
|
b.WriteString("Error: ")
|
|
case tfdiags.Warning:
|
|
b.WriteString("Warning: ")
|
|
default:
|
|
t.Errorf("unsupported diagnostic type %s", sev)
|
|
}
|
|
b.WriteString(desc.Summary)
|
|
if desc.Address != "" {
|
|
b.WriteString("\nwith ")
|
|
b.WriteString(desc.Summary)
|
|
}
|
|
if locs.Subject != nil {
|
|
b.WriteString("\nat ")
|
|
b.WriteString(locs.Subject.StartString())
|
|
}
|
|
if desc.Detail != "" {
|
|
b.WriteString("\n\n")
|
|
b.WriteString(desc.Detail)
|
|
}
|
|
t.Log(b.String())
|
|
}
|
|
if diags.HasErrors() {
|
|
t.FailNow()
|
|
}
|
|
}
|
|
|
|
// appliedChangeSortKey returns a string that can be used to sort applied
|
|
// changes in a predictable order for testing purposes. This is used to
|
|
// ensure that we can compare applied changes in a consistent way across
|
|
// different test runs.
|
|
func appliedChangeSortKey(change stackstate.AppliedChange) string {
|
|
switch change := change.(type) {
|
|
case *stackstate.AppliedChangeResourceInstanceObject:
|
|
return change.ResourceInstanceObjectAddr.String()
|
|
case *stackstate.AppliedChangeComponentInstance:
|
|
return change.ComponentInstanceAddr.String()
|
|
case *stackstate.AppliedChangeDiscardKeys:
|
|
// There should only be a single discard keys in a plan, so we can just
|
|
// return a static string here.
|
|
return "discard"
|
|
default:
|
|
// This is only going to happen during tests, so we can panic here.
|
|
panic(fmt.Errorf("unrecognized applied change type: %T", change))
|
|
}
|
|
}
|
|
|
|
// plannedChangeSortKey returns a string that can be used to sort planned
|
|
// changes in a predictable order for testing purposes. This is used to
|
|
// ensure that we can compare planned changes in a consistent way across
|
|
// different test runs.
|
|
func plannedChangeSortKey(change stackplan.PlannedChange) string {
|
|
switch change := change.(type) {
|
|
case *stackplan.PlannedChangeRootInputValue:
|
|
return change.Addr.String()
|
|
case *stackplan.PlannedChangeComponentInstance:
|
|
return change.Addr.String()
|
|
case *stackplan.PlannedChangeResourceInstancePlanned:
|
|
return change.ResourceInstanceObjectAddr.String()
|
|
case *stackplan.PlannedChangeOutputValue:
|
|
return change.Addr.String()
|
|
case *stackplan.PlannedChangeHeader:
|
|
// There should only be a single header in a plan, so we can just return
|
|
// a static string here.
|
|
return "header"
|
|
case *stackplan.PlannedChangeApplyable:
|
|
// There should only be a single applyable marker in a plan, so we can
|
|
// just return a static string here.
|
|
return "applyable"
|
|
default:
|
|
// This is only going to happen during tests, so we can panic here.
|
|
panic(fmt.Errorf("unrecognized planned change type: %T", change))
|
|
}
|
|
}
|
|
|
|
func mustPlanDynamicValue(v cty.Value) plans.DynamicValue {
|
|
ret, err := plans.NewDynamicValue(v, v.Type())
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return ret
|
|
}
|
|
|
|
func mustPlanDynamicValueDynamicType(v cty.Value) plans.DynamicValue {
|
|
ret, err := plans.NewDynamicValue(v, cty.DynamicPseudoType)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return ret
|
|
}
|
|
|
|
func mustPlanDynamicValueSchema(v cty.Value, block *configschema.Block) plans.DynamicValue {
|
|
ty := block.ImpliedType()
|
|
ret, err := plans.NewDynamicValue(v, ty)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return ret
|
|
}
|
|
|
|
func mustMarshalJSONAttrs(attrs map[string]interface{}) []byte {
|
|
jsonAttrs, err := json.Marshal(attrs)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return jsonAttrs
|
|
}
|