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" "strings"
"testing" "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" "github.com/hashicorp/terraform/terraform"
) )
func TestSession_basicState(t *testing.T) { func TestSession_basicState(t *testing.T) {
state := &terraform.State{ state := states.BuildState(func (s *states.SyncState) {
Modules: []*terraform.ModuleState{ s.SetResourceInstanceCurrent(
&terraform.ModuleState{ addrs.Resource{
Path: []string{"root"}, Mode: addrs.ManagedResourceMode,
Resources: map[string]*terraform.ResourceState{ Type: "test_instance",
"test_instance.foo": &terraform.ResourceState{ Name: "foo",
Type: "test_instance", }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
Primary: &terraform.InstanceState{ &states.ResourceInstanceObjectSrc{
ID: "bar", Status: states.ObjectReady,
Attributes: map[string]string{ AttrsJSON: []byte(`{"id":"bar"}`),
"id": "bar",
},
},
},
},
}, },
addrs.ProviderConfig{
&terraform.ModuleState{ Type: "aws",
Path: []string{"root", "module"}, }.Absolute(addrs.RootModuleInstance),
Resources: map[string]*terraform.ResourceState{ )
"test_instance.foo": &terraform.ResourceState{ s.SetResourceInstanceCurrent(
Type: "test_instance", addrs.Resource{
Primary: &terraform.InstanceState{ Mode: addrs.ManagedResourceMode,
ID: "bar", Type: "test_instance",
Attributes: map[string]string{ Name: "foo",
"id": "bar", }.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) { t.Run("basic", func(t *testing.T) {
testSession(t, testSessionTest{ testSession(t, testSessionTest{
@ -112,9 +112,8 @@ func TestSession_stateless(t *testing.T) {
testSession(t, testSessionTest{ testSession(t, testSessionTest{
Inputs: []testSessionInput{ Inputs: []testSessionInput{
{ {
Input: "exit", Input: "exit",
Error: true, Exit: true,
ErrorContains: ErrSessionExit.Error(),
}, },
}, },
}) })
@ -159,7 +158,7 @@ func TestSession_stateless(t *testing.T) {
{ {
Input: "test_instance.bar.id", Input: "test_instance.bar.id",
Error: true, 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) { func testSession(t *testing.T, test testSessionTest) {
p := &terraform.MockProvider{}
p.GetSchemaReturn = &terraform.ProviderSchema{}
// Build the TF context // Build the TF context
ctx, err := terraform.NewContext(&terraform.ContextOpts{ ctx, diags := terraform.NewContext(&terraform.ContextOpts{
State: test.State, State: test.State,
Module: module.NewEmptyTree(), ProviderResolver: providers.ResolverFixed(map[string]providers.Factory{
"aws": providers.FactoryFixed(p),
}),
Config: configs.NewEmptyConfig(),
}) })
if err != nil { if diags.HasErrors() {
t.Fatalf("err: %s", err) 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 // Build the session
s := &Session{ s := &Session{
Interpolater: ctx.Interpolater(), Scope: scope,
} }
// Test the inputs. We purposely don't use subtests here because // 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. // operations.
for _, input := range test.Inputs { for _, input := range test.Inputs {
result, err := s.Handle(input.Input) result, exit, diags := s.Handle(input.Input)
if (err != nil) != input.Error { if exit != input.Exit {
t.Fatalf("%q: err: %s", input.Input, err) 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 input.ErrorContains != "" {
if !strings.Contains(err.Error(), input.ErrorContains) { if !strings.Contains(diags.Err().Error(), input.ErrorContains) {
t.Fatalf( t.Fatalf(
"%q: err should contain: %q\n\n%s", "%q: diagnostics should contain: %q\n\n%s",
input.Input, input.ErrorContains, err) input.Input, input.ErrorContains, diags.Err(),
)
} }
} }
@ -216,8 +230,8 @@ func testSession(t *testing.T, test testSessionTest) {
} }
type testSessionTest struct { type testSessionTest struct {
State *terraform.State // State to use State *states.State // State to use
Module string // Module name in test-fixtures to load Module string // Module name in test-fixtures to load
// Inputs are the list of test inputs that are run in order. // Inputs are the list of test inputs that are run in order.
// Each input can test the output of each step. // Each input can test the output of each step.
@ -230,5 +244,6 @@ type testSessionInput struct {
Output string // Exact output string to check Output string // Exact output string to check
OutputContains string OutputContains string
Error bool // Error is true if error is expected Error bool // Error is true if error is expected
Exit bool // Exit is true if exiting is expected
ErrorContains string ErrorContains string
} }

View File

@ -329,9 +329,17 @@ func (d *evaluationStateData) GetModuleInstanceOutput(addr addrs.ModuleCallOutpu
// name is declared at all. // name is declared at all.
moduleConfig := d.Evaluator.Config.DescendentForInstance(moduleAddr) moduleConfig := d.Evaluator.Config.DescendentForInstance(moduleAddr)
if moduleConfig == nil { if moduleConfig == nil {
// should never happen, since we can't be evaluating in a module // this doesn't happen in normal circumstances due to our validation
// that wasn't mentioned in configuration. // pass, but it can turn up in some unusual situations, like in the
panic(fmt.Sprintf("output value read from %s, which has no configuration", moduleAddr)) // "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] config := moduleConfig.Module.Outputs[addr.Name]
@ -348,7 +356,7 @@ func (d *evaluationStateData) GetModuleInstanceOutput(addr addrs.ModuleCallOutpu
diags = diags.Append(&hcl.Diagnostic{ diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError, Severity: hcl.DiagError,
Summary: `Reference to undeclared output value`, 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(), Subject: rng.ToHCL().Ptr(),
}) })
return cty.DynamicVal, diags return cty.DynamicVal, diags
@ -448,7 +456,7 @@ func (d *evaluationStateData) GetResourceInstance(addr addrs.ResourceInstance, r
diags = diags.Append(&hcl.Diagnostic{ diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError, Severity: hcl.DiagError,
Summary: `Reference to undeclared resource`, 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(), Subject: rng.ToHCL().Ptr(),
}) })
return cty.DynamicVal, diags return cty.DynamicVal, diags
@ -872,3 +880,17 @@ func nameSuggestion(given string, suggestions []string) string {
} }
return "" 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()
}
}