mirror of
https://github.com/opentofu/opentofu.git
synced 2024-12-30 10:47:14 -06:00
843c50e8ce
This commit introduces a capsule type, `TypeType`, which is used to extricate type information from the console-only `type` function. In combination with the `TypeType` mark, this allows us to restrict the use of this function to top-level display of a value's type. Any other use of `type()` will result in an error diagnostic.
446 lines
10 KiB
Go
446 lines
10 KiB
Go
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))
|
|
}
|
|
}
|
|
}
|