terraform/repl/session_test.go

296 lines
7.0 KiB
Go
Raw Normal View History

2016-11-14 07:04:21 +01:00
package repl
import (
"flag"
"io/ioutil"
"log"
"os"
2016-11-14 07:04:21 +01:00
"strings"
"testing"
"github.com/zclconf/go-cty/cty"
"github.com/hashicorp/terraform/addrs"
"github.com/hashicorp/terraform/configs/configload"
"github.com/hashicorp/terraform/configs/configschema"
"github.com/hashicorp/terraform/helper/logging"
"github.com/hashicorp/terraform/providers"
"github.com/hashicorp/terraform/states"
2016-11-14 07:04:21 +01:00
"github.com/hashicorp/terraform/terraform"
)
func TestMain(m *testing.M) {
flag.Parse()
if testing.Verbose() {
// if we're verbose, use the logging requested by TF_LOG
logging.SetOutput()
} else {
// otherwise silence all logs
log.SetOutput(ioutil.Discard)
}
os.Exit(m.Run())
}
2016-11-14 07:04:21 +01:00
func TestSession_basicState(t *testing.T) {
state := states.BuildState(func(s *states.SyncState) {
s.SetResourceInstanceCurrent(
addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "test_instance",
Name: "foo",
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"bar"}`),
2016-11-14 07:04:21 +01:00
},
addrs.ProviderConfig{
Type: "test",
}.Absolute(addrs.RootModuleInstance),
)
s.SetResourceInstanceCurrent(
addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "test_instance",
Name: "foo",
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance.Child("module", addrs.NoKey)),
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"bar"}`),
},
addrs.ProviderConfig{
Type: "test",
}.Absolute(addrs.RootModuleInstance),
)
})
2016-11-14 07:04:21 +01:00
t.Run("basic", func(t *testing.T) {
testSession(t, testSessionTest{
State: state,
Inputs: []testSessionInput{
{
Input: "test_instance.foo.id",
Output: "bar",
},
},
})
})
t.Run("missing resource", func(t *testing.T) {
testSession(t, testSessionTest{
State: state,
Inputs: []testSessionInput{
{
Input: "test_instance.bar.id",
Error: true,
core: Static-validate resource references against schemas In the initial move to HCL2 we started relying only on full expression evaluation to catch attribute errors, but that's not sufficient for resource attributes in practice because during validation we can't know yet whether a resource reference evaluates to a single object or to a list of objects (if count is set). To address this, here we reinstate some static validation of resource references by analyzing directly the reference objects, disregarding any instance index if present, and produce errors if the remaining subsequent traversal steps do not correspond to items within the resource type schema. This also allows us to produce some more specialized error messages for certain situations. In particular, we can recognize a reference like aws_instance.foo.count, which in 0.11 and prior was a weird special case for determining the count value of a resource block, and offer a helpful error showing the new length(aws_instance.foo) usage pattern. This eventually delegates to the static traversal validation logic that was added to the configschema package in a previous commit, which also includes some specialized error messages that distinguish between attributes and block types in the schema so that the errors relate more directly to constructs the user can see in the configuration. In future we could potentially move more of the checks from the dynamic schema construction step to the static validation step, but resources are the reference type that most needs this immediately due to the ambiguity caused by the instance indexing syntax. We can safely refactor other reference types to be statically validated in later releases. This is verified by two pre-existing context validate tests which we temporarily disabled during earlier work (now re-enabled) and also by a new validate test aimed specifically at the special case for the "count" attribute.
2018-11-21 02:25:05 +01:00
ErrorContains: `A managed resource "test_instance" "bar" has not been declared`,
2016-11-14 07:04:21 +01:00
},
},
})
})
t.Run("missing module", func(t *testing.T) {
testSession(t, testSessionTest{
State: state,
Inputs: []testSessionInput{
{
Input: "module.child",
Error: true,
ErrorContains: `No module call named "child" is declared in the root module.`,
},
},
})
})
t.Run("missing module referencing just one output", func(t *testing.T) {
testSession(t, testSessionTest{
State: state,
Inputs: []testSessionInput{
{
Input: "module.child.foo",
Error: true,
ErrorContains: `No module call named "child" is declared in the root module.`,
},
},
})
})
t.Run("missing module output", func(t *testing.T) {
testSession(t, testSessionTest{
State: state,
Inputs: []testSessionInput{
{
Input: "module.module.foo",
Error: true,
ErrorContains: `An output value with the name "foo" has not been declared in module.module`,
},
},
})
})
2016-11-14 07:04:21 +01:00
}
func TestSession_stateless(t *testing.T) {
t.Run("exit", func(t *testing.T) {
testSession(t, testSessionTest{
Inputs: []testSessionInput{
{
Input: "exit",
Exit: true,
2016-11-14 07:04:21 +01:00
},
},
})
})
t.Run("help", func(t *testing.T) {
testSession(t, testSessionTest{
Inputs: []testSessionInput{
{
Input: "help",
OutputContains: "allows you to",
},
},
})
})
t.Run("help with spaces", func(t *testing.T) {
testSession(t, testSessionTest{
Inputs: []testSessionInput{
{
Input: "help ",
OutputContains: "allows you to",
},
},
})
})
t.Run("basic math", func(t *testing.T) {
testSession(t, testSessionTest{
Inputs: []testSessionInput{
{
Input: "1 + 5",
Output: "6",
},
},
})
})
t.Run("missing resource", func(t *testing.T) {
testSession(t, testSessionTest{
Inputs: []testSessionInput{
{
Input: "test_instance.bar.id",
Error: true,
ErrorContains: `resource "test_instance" "bar" has not been declared`,
2016-11-14 07:04:21 +01:00
},
},
})
})
}
func testSession(t *testing.T, test testSessionTest) {
core: Static-validate resource references against schemas In the initial move to HCL2 we started relying only on full expression evaluation to catch attribute errors, but that's not sufficient for resource attributes in practice because during validation we can't know yet whether a resource reference evaluates to a single object or to a list of objects (if count is set). To address this, here we reinstate some static validation of resource references by analyzing directly the reference objects, disregarding any instance index if present, and produce errors if the remaining subsequent traversal steps do not correspond to items within the resource type schema. This also allows us to produce some more specialized error messages for certain situations. In particular, we can recognize a reference like aws_instance.foo.count, which in 0.11 and prior was a weird special case for determining the count value of a resource block, and offer a helpful error showing the new length(aws_instance.foo) usage pattern. This eventually delegates to the static traversal validation logic that was added to the configschema package in a previous commit, which also includes some specialized error messages that distinguish between attributes and block types in the schema so that the errors relate more directly to constructs the user can see in the configuration. In future we could potentially move more of the checks from the dynamic schema construction step to the static validation step, but resources are the reference type that most needs this immediately due to the ambiguity caused by the instance indexing syntax. We can safely refactor other reference types to be statically validated in later releases. This is verified by two pre-existing context validate tests which we temporarily disabled during earlier work (now re-enabled) and also by a new validate test aimed specifically at the special case for the "count" attribute.
2018-11-21 02:25:05 +01:00
t.Helper()
p := &terraform.MockProvider{}
p.GetSchemaReturn = &terraform.ProviderSchema{
ResourceTypes: map[string]*configschema.Block{
"test_instance": {
Attributes: map[string]*configschema.Attribute{
"id": {Type: cty.String, Computed: true},
},
},
},
}
loader, cleanup := configload.NewLoaderForTests(t)
defer cleanup()
configDiags := loader.InstallModules("testdata/config-fixture", false, nil)
if configDiags.HasErrors() {
t.Fatalf("unexpected problems initializing test config: %s", configDiags.Error())
}
config, configDiags := loader.LoadConfig("testdata/config-fixture")
if configDiags.HasErrors() {
t.Fatalf("unexpected problems loading config: %s", configDiags.Error())
}
2016-11-14 07:04:21 +01:00
// Build the TF context
ctx, diags := terraform.NewContext(&terraform.ContextOpts{
State: test.State,
ProviderResolver: providers.ResolverFixed(map[string]providers.Factory{
"test": providers.FactoryFixed(p),
}),
Config: config,
2016-11-14 07:04:21 +01:00
})
if diags.HasErrors() {
t.Fatalf("failed to create context: %s", diags.Err())
}
scope, diags := ctx.Eval(addrs.RootModuleInstance)
if diags.HasErrors() {
t.Fatalf("failed to create scope: %s", diags.Err())
2016-11-14 07:04:21 +01:00
}
// Build the session
s := &Session{
Scope: scope,
2016-11-14 07:04:21 +01:00
}
// Test the inputs. We purposely don't use subtests here because
// the inputs don't represent subtests, but a sequence of stateful
2016-11-14 07:04:21 +01:00
// operations.
for _, input := range test.Inputs {
result, exit, diags := s.Handle(input.Input)
if exit != input.Exit {
t.Fatalf("incorrect 'exit' result %t; want %t", exit, input.Exit)
}
if (diags.HasErrors()) != input.Error {
t.Fatalf("%q: unexpected errors: %s", input.Input, diags.Err())
2016-11-14 07:04:21 +01:00
}
if diags.HasErrors() {
2016-11-14 07:04:21 +01:00
if input.ErrorContains != "" {
if !strings.Contains(diags.Err().Error(), input.ErrorContains) {
2016-11-14 07:04:21 +01:00
t.Fatalf(
"%q: diagnostics should contain: %q\n\n%s",
input.Input, input.ErrorContains, diags.Err(),
)
2016-11-14 07:04:21 +01:00
}
}
continue
}
if input.Output != "" && result != input.Output {
t.Fatalf(
"%q: expected:\n\n%s\n\ngot:\n\n%s",
input.Input, input.Output, result)
}
if input.OutputContains != "" && !strings.Contains(result, input.OutputContains) {
t.Fatalf(
"%q: expected contains:\n\n%s\n\ngot:\n\n%s",
input.Input, input.OutputContains, result)
}
}
}
type testSessionTest struct {
State *states.State // State to use
Module string // Module name in test-fixtures to load
2016-11-14 07:04:21 +01:00
// Inputs are the list of test inputs that are run in order.
// Each input can test the output of each step.
Inputs []testSessionInput
}
// testSessionInput is a single input to test for a session.
type testSessionInput struct {
Input string // Input string
Output string // Exact output string to check
OutputContains string
Error bool // Error is true if error is expected
Exit bool // Exit is true if exiting is expected
2016-11-14 07:04:21 +01:00
ErrorContains string
}