| package repl |
| |
| import ( |
| "flag" |
| "os" |
| "strings" |
| "testing" |
| |
| "github.com/google/go-cmp/cmp" |
| "github.com/zclconf/go-cty/cty" |
| |
| "github.com/hashicorp/terraform/internal/addrs" |
| "github.com/hashicorp/terraform/internal/configs/configschema" |
| "github.com/hashicorp/terraform/internal/initwd" |
| "github.com/hashicorp/terraform/internal/providers" |
| "github.com/hashicorp/terraform/internal/states" |
| "github.com/hashicorp/terraform/internal/terraform" |
| |
| _ "github.com/hashicorp/terraform/internal/logging" |
| ) |
| |
| func TestMain(m *testing.M) { |
| flag.Parse() |
| os.Exit(m.Run()) |
| } |
| |
| 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"}`), |
| }, |
| addrs.AbsProviderConfig{ |
| Provider: addrs.NewDefaultProvider("test"), |
| Module: addrs.RootModule, |
| }, |
| ) |
| 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.AbsProviderConfig{ |
| Provider: addrs.NewDefaultProvider("test"), |
| Module: addrs.RootModule, |
| }, |
| ) |
| }) |
| |
| 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, |
| ErrorContains: `A managed resource "test_instance" "bar" has not been declared`, |
| }, |
| }, |
| }) |
| }) |
| |
| 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: `Unsupported attribute: This object does not have an attribute named "foo"`, |
| }, |
| }, |
| }) |
| }) |
| |
| t.Run("type function", func(t *testing.T) { |
| testSession(t, testSessionTest{ |
| State: state, |
| Inputs: []testSessionInput{ |
| { |
| Input: "type(test_instance.foo)", |
| Output: `object({ |
| id: string, |
| })`, |
| }, |
| }, |
| }) |
| }) |
| } |
| |
| func TestSession_stateless(t *testing.T) { |
| t.Run("exit", func(t *testing.T) { |
| testSession(t, testSessionTest{ |
| Inputs: []testSessionInput{ |
| { |
| Input: "exit", |
| Exit: true, |
| }, |
| }, |
| }) |
| }) |
| |
| 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`, |
| }, |
| }, |
| }) |
| }) |
| |
| t.Run("type function", func(t *testing.T) { |
| testSession(t, testSessionTest{ |
| Inputs: []testSessionInput{ |
| { |
| Input: `type("foo")`, |
| Output: "string", |
| }, |
| }, |
| }) |
| }) |
| |
| t.Run("type type is type", func(t *testing.T) { |
| testSession(t, testSessionTest{ |
| Inputs: []testSessionInput{ |
| { |
| Input: `type(type("foo"))`, |
| Output: "type", |
| }, |
| }, |
| }) |
| }) |
| |
| t.Run("interpolating type with strings is not possible", func(t *testing.T) { |
| testSession(t, testSessionTest{ |
| Inputs: []testSessionInput{ |
| { |
| Input: `"quin${type([])}"`, |
| Error: true, |
| ErrorContains: "Invalid template interpolation value", |
| }, |
| }, |
| }) |
| }) |
| |
| t.Run("type function cannot be used in expressions", func(t *testing.T) { |
| testSession(t, testSessionTest{ |
| Inputs: []testSessionInput{ |
| { |
| Input: `[for i in [1, "two", true]: type(i)]`, |
| Output: "", |
| Error: true, |
| ErrorContains: "Invalid use of type function", |
| }, |
| }, |
| }) |
| }) |
| |
| t.Run("type equality checks are not permitted", func(t *testing.T) { |
| testSession(t, testSessionTest{ |
| Inputs: []testSessionInput{ |
| { |
| Input: `type("foo") == type("bar")`, |
| Output: "", |
| Error: true, |
| ErrorContains: "Invalid use of type function", |
| }, |
| }, |
| }) |
| }) |
| } |
| |
| func testSession(t *testing.T, test testSessionTest) { |
| t.Helper() |
| |
| p := &terraform.MockProvider{} |
| p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ |
| ResourceTypes: map[string]providers.Schema{ |
| "test_instance": { |
| Block: &configschema.Block{ |
| Attributes: map[string]*configschema.Attribute{ |
| "id": {Type: cty.String, Computed: true}, |
| }, |
| }, |
| }, |
| }, |
| } |
| |
| config, _, cleanup, configDiags := initwd.LoadConfigForTests(t, "testdata/config-fixture") |
| defer cleanup() |
| if configDiags.HasErrors() { |
| t.Fatalf("unexpected problems loading config: %s", configDiags.Err()) |
| } |
| |
| // Build the TF context |
| ctx, diags := terraform.NewContext(&terraform.ContextOpts{ |
| Providers: map[addrs.Provider]providers.Factory{ |
| addrs.NewDefaultProvider("test"): providers.FactoryFixed(p), |
| }, |
| }) |
| if diags.HasErrors() { |
| t.Fatalf("failed to create context: %s", diags.Err()) |
| } |
| |
| state := test.State |
| if state == nil { |
| state = states.NewState() |
| } |
| scope, diags := ctx.Eval(config, state, addrs.RootModuleInstance, &terraform.EvalOpts{}) |
| if diags.HasErrors() { |
| t.Fatalf("failed to create scope: %s", diags.Err()) |
| } |
| |
| // Ensure that any console-only functions are available |
| scope.ConsoleMode = true |
| |
| // Build the session |
| s := &Session{ |
| Scope: scope, |
| } |
| |
| // Test the inputs. We purposely don't use subtests here because |
| // the inputs don't represent subtests, but a sequence of stateful |
| // 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()) |
| } |
| if diags.HasErrors() { |
| if input.ErrorContains != "" { |
| if !strings.Contains(diags.Err().Error(), input.ErrorContains) { |
| t.Fatalf( |
| "%q: diagnostics should contain: %q\n\n%s", |
| input.Input, input.ErrorContains, diags.Err(), |
| ) |
| } |
| } |
| |
| 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 testdata to load |
| |
| // 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 |
| ErrorContains string |
| } |
| |
| func TestTypeString(t *testing.T) { |
| tests := []struct { |
| Input cty.Value |
| Want string |
| }{ |
| // Primititves |
| { |
| cty.StringVal("a"), |
| "string", |
| }, |
| { |
| cty.NumberIntVal(42), |
| "number", |
| }, |
| { |
| cty.BoolVal(true), |
| "bool", |
| }, |
| // Collections |
| { |
| cty.EmptyObjectVal, |
| `object({})`, |
| }, |
| { |
| cty.EmptyTupleVal, |
| `tuple([])`, |
| }, |
| { |
| cty.ListValEmpty(cty.String), |
| `list(string)`, |
| }, |
| { |
| cty.MapValEmpty(cty.String), |
| `map(string)`, |
| }, |
| { |
| cty.SetValEmpty(cty.String), |
| `set(string)`, |
| }, |
| { |
| cty.ListVal([]cty.Value{cty.StringVal("a")}), |
| `list(string)`, |
| }, |
| { |
| cty.ListVal([]cty.Value{cty.ListVal([]cty.Value{cty.NumberIntVal(42)})}), |
| `list(list(number))`, |
| }, |
| { |
| cty.ListVal([]cty.Value{cty.MapValEmpty(cty.String)}), |
| `list(map(string))`, |
| }, |
| { |
| cty.ListVal([]cty.Value{cty.ObjectVal(map[string]cty.Value{ |
| "foo": cty.StringVal("bar"), |
| })}), |
| "list(\n object({\n foo: string,\n }),\n)", |
| }, |
| // Unknowns and Nulls |
| { |
| cty.UnknownVal(cty.String), |
| "string", |
| }, |
| { |
| cty.NullVal(cty.Object(map[string]cty.Type{ |
| "foo": cty.String, |
| })), |
| "object({\n foo: string,\n})", |
| }, |
| { // irrelevant marks do nothing |
| cty.ListVal([]cty.Value{cty.ObjectVal(map[string]cty.Value{ |
| "foo": cty.StringVal("bar").Mark("ignore me"), |
| })}), |
| "list(\n object({\n foo: string,\n }),\n)", |
| }, |
| } |
| for _, test := range tests { |
| got := typeString(test.Input.Type()) |
| if got != test.Want { |
| t.Errorf("wrong result:\n%s", cmp.Diff(got, test.Want)) |
| } |
| } |
| } |