mirror of
https://github.com/opentofu/opentofu.git
synced 2024-12-23 07:33:32 -06:00
6522f73249
This continues our ongoing effort to get a coherent chain of context.Context all the way from "package main" to all of our calls to external components. Context.Eval doesn't yet do anything with its new context, but we'll plumb this deeper in future. All of the _test.go file updates here are purely mechanical additions of the extra argument. No test is materially modified by this change, which is intentional to get some assurance that isn't a breaking change. Signed-off-by: Martin Atkins <mart@degeneration.co.uk>
454 lines
10 KiB
Go
454 lines
10 KiB
Go
// Copyright (c) The OpenTofu Authors
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
// Copyright (c) 2023 HashiCorp, Inc.
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
package repl
|
|
|
|
import (
|
|
"context"
|
|
"flag"
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/google/go-cmp/cmp"
|
|
"github.com/zclconf/go-cty/cty"
|
|
|
|
"github.com/opentofu/opentofu/internal/addrs"
|
|
"github.com/opentofu/opentofu/internal/configs/configschema"
|
|
"github.com/opentofu/opentofu/internal/initwd"
|
|
"github.com/opentofu/opentofu/internal/providers"
|
|
"github.com/opentofu/opentofu/internal/states"
|
|
"github.com/opentofu/opentofu/internal/tofu"
|
|
|
|
_ "github.com/opentofu/opentofu/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,
|
|
},
|
|
addrs.NoKey,
|
|
)
|
|
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,
|
|
},
|
|
addrs.NoKey,
|
|
)
|
|
})
|
|
|
|
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 := &tofu.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", "tests")
|
|
defer cleanup()
|
|
if configDiags.HasErrors() {
|
|
t.Fatalf("unexpected problems loading config: %s", configDiags.Err())
|
|
}
|
|
|
|
// Build the TF context
|
|
ctx, diags := tofu.NewContext(&tofu.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(context.Background(), config, state, addrs.RootModuleInstance, &tofu.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
|
|
}{
|
|
// Primitives
|
|
{
|
|
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))
|
|
}
|
|
}
|
|
}
|