repl: Make tests compile and execute without panics

There are still some errors left, because our expression evaluator now
does more validation than before and so we'll need to (in a subsequent
commit) actually use a fixture configuration for these tests so that the
validations will allow the expressions to be validated.
This commit is contained in:
Martin Atkins 2018-09-28 17:15:35 -07:00
parent 85aa8769db
commit 5ff35c1a9a
2 changed files with 93 additions and 56 deletions

View File

@ -4,44 +4,44 @@ import (
"strings"
"testing"
"github.com/hashicorp/terraform/config/module"
"github.com/hashicorp/terraform/addrs"
"github.com/hashicorp/terraform/configs"
"github.com/hashicorp/terraform/providers"
"github.com/hashicorp/terraform/states"
"github.com/hashicorp/terraform/terraform"
)
func TestSession_basicState(t *testing.T) {
state := &terraform.State{
Modules: []*terraform.ModuleState{
&terraform.ModuleState{
Path: []string{"root"},
Resources: map[string]*terraform.ResourceState{
"test_instance.foo": &terraform.ResourceState{
Type: "test_instance",
Primary: &terraform.InstanceState{
ID: "bar",
Attributes: map[string]string{
"id": "bar",
},
},
},
},
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"}`),
},
&terraform.ModuleState{
Path: []string{"root", "module"},
Resources: map[string]*terraform.ResourceState{
"test_instance.foo": &terraform.ResourceState{
Type: "test_instance",
Primary: &terraform.InstanceState{
ID: "bar",
Attributes: map[string]string{
"id": "bar",
},
},
},
},
addrs.ProviderConfig{
Type: "aws",
}.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: "aws",
}.Absolute(addrs.RootModuleInstance),
)
})
t.Run("basic", func(t *testing.T) {
testSession(t, testSessionTest{
@ -112,9 +112,8 @@ func TestSession_stateless(t *testing.T) {
testSession(t, testSessionTest{
Inputs: []testSessionInput{
{
Input: "exit",
Error: true,
ErrorContains: ErrSessionExit.Error(),
Input: "exit",
Exit: true,
},
},
})
@ -159,7 +158,7 @@ func TestSession_stateless(t *testing.T) {
{
Input: "test_instance.bar.id",
Error: true,
ErrorContains: "'test_instance.bar' not found",
ErrorContains: `resource "test_instance" "bar" has not been declared`,
},
},
})
@ -167,34 +166,49 @@ func TestSession_stateless(t *testing.T) {
}
func testSession(t *testing.T, test testSessionTest) {
p := &terraform.MockProvider{}
p.GetSchemaReturn = &terraform.ProviderSchema{}
// Build the TF context
ctx, err := terraform.NewContext(&terraform.ContextOpts{
ctx, diags := terraform.NewContext(&terraform.ContextOpts{
State: test.State,
Module: module.NewEmptyTree(),
ProviderResolver: providers.ResolverFixed(map[string]providers.Factory{
"aws": providers.FactoryFixed(p),
}),
Config: configs.NewEmptyConfig(),
})
if err != nil {
t.Fatalf("err: %s", err)
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())
}
// Build the session
s := &Session{
Interpolater: ctx.Interpolater(),
Scope: scope,
}
// Test the inputs. We purposely don't use subtests here because
// the inputs don't recognize subtests, but a sequence of stateful
// the inputs don't represent subtests, but a sequence of stateful
// operations.
for _, input := range test.Inputs {
result, err := s.Handle(input.Input)
if (err != nil) != input.Error {
t.Fatalf("%q: err: %s", input.Input, err)
result, exit, diags := s.Handle(input.Input)
if exit != input.Exit {
t.Fatalf("incorrect 'exit' result %t; want %t", exit, input.Exit)
}
if err != nil {
if (diags.HasErrors()) != input.Error {
t.Fatalf("%q: unexpected errors: %s", input.Input, diags.Err())
}
if diags.HasErrors() {
if input.ErrorContains != "" {
if !strings.Contains(err.Error(), input.ErrorContains) {
if !strings.Contains(diags.Err().Error(), input.ErrorContains) {
t.Fatalf(
"%q: err should contain: %q\n\n%s",
input.Input, input.ErrorContains, err)
"%q: diagnostics should contain: %q\n\n%s",
input.Input, input.ErrorContains, diags.Err(),
)
}
}
@ -216,8 +230,8 @@ func testSession(t *testing.T, test testSessionTest) {
}
type testSessionTest struct {
State *terraform.State // State to use
Module string // Module name in test-fixtures to load
State *states.State // State to use
Module string // Module name in test-fixtures to load
// Inputs are the list of test inputs that are run in order.
// Each input can test the output of each step.
@ -230,5 +244,6 @@ type testSessionInput struct {
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
ErrorContains string
}

View File

@ -329,9 +329,17 @@ func (d *evaluationStateData) GetModuleInstanceOutput(addr addrs.ModuleCallOutpu
// name is declared at all.
moduleConfig := d.Evaluator.Config.DescendentForInstance(moduleAddr)
if moduleConfig == nil {
// should never happen, since we can't be evaluating in a module
// that wasn't mentioned in configuration.
panic(fmt.Sprintf("output value read from %s, which has no configuration", moduleAddr))
// this doesn't happen in normal circumstances due to our validation
// pass, but it can turn up in some unusual situations, like in the
// "terraform console" repl where arbitrary expressions can be
// evaluated.
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: `Reference to undeclared module`,
Detail: fmt.Sprintf(`The configuration contains no %s.`, moduleAddr),
Subject: rng.ToHCL().Ptr(),
})
return cty.DynamicVal, diags
}
config := moduleConfig.Module.Outputs[addr.Name]
@ -348,7 +356,7 @@ func (d *evaluationStateData) GetModuleInstanceOutput(addr addrs.ModuleCallOutpu
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: `Reference to undeclared output value`,
Detail: fmt.Sprintf(`An output value with the name %q has not been declared in %s.%s`, addr.Name, moduleAddr, suggestion),
Detail: fmt.Sprintf(`An output value with the name %q has not been declared in %s.%s`, addr.Name, moduleDisplayAddr(moduleAddr), suggestion),
Subject: rng.ToHCL().Ptr(),
})
return cty.DynamicVal, diags
@ -448,7 +456,7 @@ func (d *evaluationStateData) GetResourceInstance(addr addrs.ResourceInstance, r
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: `Reference to undeclared resource`,
Detail: fmt.Sprintf(`A resource %q %q has not been declared in %s`, addr.Resource.Type, addr.Resource.Name, moduleAddr),
Detail: fmt.Sprintf(`A resource %q %q has not been declared in %s`, addr.Resource.Type, addr.Resource.Name, moduleDisplayAddr(moduleAddr)),
Subject: rng.ToHCL().Ptr(),
})
return cty.DynamicVal, diags
@ -872,3 +880,17 @@ func nameSuggestion(given string, suggestions []string) string {
}
return ""
}
// 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
// root module instance is given, or a string representation of the module
// address otherwise.
func moduleDisplayAddr(addr addrs.ModuleInstance) string {
switch {
case addr.IsRoot():
return "the root module"
default:
return addr.String()
}
}