testing framework: introduce test command optional flags (#33504)

* testing framework: introduce test command optional flags

* address consistency checks
This commit is contained in:
Liam Cervante 2023-07-19 10:07:46 +02:00 committed by GitHub
parent 2cc81cfec6
commit 6882dd9530
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
49 changed files with 1287 additions and 193 deletions

View File

@ -364,7 +364,7 @@ func (s failingState) WriteState(state *states.State) error {
func testOperationApply(t *testing.T, configDir string) (*backend.Operation, func(), func(*testing.T) *terminal.TestOutput) {
t.Helper()
_, configLoader, configCleanup := initwd.MustLoadConfigForTests(t, configDir)
_, configLoader, configCleanup := initwd.MustLoadConfigForTests(t, configDir, "tests")
streams, done := terminal.StreamsForTesting(t)
view := views.NewOperation(arguments.ViewHuman, false, views.NewView(streams))

View File

@ -32,7 +32,7 @@ func TestLocalRun(t *testing.T) {
configDir := "./testdata/empty"
b := TestLocal(t)
_, configLoader, configCleanup := initwd.MustLoadConfigForTests(t, configDir)
_, configLoader, configCleanup := initwd.MustLoadConfigForTests(t, configDir, "tests")
defer configCleanup()
streams, _ := terminal.StreamsForTesting(t)
@ -63,7 +63,7 @@ func TestLocalRun_error(t *testing.T) {
// should then cause LocalRun to return with the state unlocked.
b.Backend = backendWithStateStorageThatFailsRefresh{}
_, configLoader, configCleanup := initwd.MustLoadConfigForTests(t, configDir)
_, configLoader, configCleanup := initwd.MustLoadConfigForTests(t, configDir, "tests")
defer configCleanup()
streams, _ := terminal.StreamsForTesting(t)
@ -90,7 +90,7 @@ func TestLocalRun_stalePlan(t *testing.T) {
configDir := "./testdata/apply"
b := TestLocal(t)
_, configLoader, configCleanup := initwd.MustLoadConfigForTests(t, configDir)
_, configLoader, configCleanup := initwd.MustLoadConfigForTests(t, configDir, "tests")
defer configCleanup()
// Write an empty state file with serial 3

View File

@ -10,6 +10,8 @@ import (
"strings"
"testing"
"github.com/zclconf/go-cty/cty"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/backend"
"github.com/hashicorp/terraform/internal/command/arguments"
@ -24,7 +26,6 @@ import (
"github.com/hashicorp/terraform/internal/states"
"github.com/hashicorp/terraform/internal/terminal"
"github.com/hashicorp/terraform/internal/terraform"
"github.com/zclconf/go-cty/cty"
)
func TestLocal_planBasic(t *testing.T) {
@ -716,7 +717,7 @@ func TestLocal_planOutPathNoChange(t *testing.T) {
func testOperationPlan(t *testing.T, configDir string) (*backend.Operation, func(), func(*testing.T) *terminal.TestOutput) {
t.Helper()
_, configLoader, configCleanup := initwd.MustLoadConfigForTests(t, configDir)
_, configLoader, configCleanup := initwd.MustLoadConfigForTests(t, configDir, "tests")
streams, done := terminal.StreamsForTesting(t)
view := views.NewOperation(arguments.ViewHuman, false, views.NewView(streams))

View File

@ -267,7 +267,7 @@ func TestLocal_refreshEmptyState(t *testing.T) {
func testOperationRefresh(t *testing.T, configDir string) (*backend.Operation, func(), func(*testing.T) *terminal.TestOutput) {
t.Helper()
_, configLoader, configCleanup := initwd.MustLoadConfigForTests(t, configDir)
_, configLoader, configCleanup := initwd.MustLoadConfigForTests(t, configDir, "tests")
streams, done := terminal.StreamsForTesting(t)
view := views.NewOperation(arguments.ViewHuman, false, views.NewView(streams))

View File

@ -15,6 +15,8 @@ import (
"github.com/google/go-cmp/cmp"
tfe "github.com/hashicorp/go-tfe"
version "github.com/hashicorp/go-version"
"github.com/mitchellh/cli"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/backend"
"github.com/hashicorp/terraform/internal/cloud"
@ -29,7 +31,6 @@ import (
"github.com/hashicorp/terraform/internal/terminal"
"github.com/hashicorp/terraform/internal/terraform"
tfversion "github.com/hashicorp/terraform/version"
"github.com/mitchellh/cli"
)
func testOperationApply(t *testing.T, configDir string) (*backend.Operation, func(), func(*testing.T) *terminal.TestOutput) {
@ -41,7 +42,7 @@ func testOperationApply(t *testing.T, configDir string) (*backend.Operation, fun
func testOperationApplyWithTimeout(t *testing.T, configDir string, timeout time.Duration) (*backend.Operation, func(), func(*testing.T) *terminal.TestOutput) {
t.Helper()
_, configLoader, configCleanup := initwd.MustLoadConfigForTests(t, configDir)
_, configLoader, configCleanup := initwd.MustLoadConfigForTests(t, configDir, "tests")
streams, done := terminal.StreamsForTesting(t)
view := views.NewView(streams)

View File

@ -12,6 +12,8 @@ import (
"github.com/hashicorp/terraform/internal/tfdiags"
tfe "github.com/hashicorp/go-tfe"
"github.com/zclconf/go-cty/cty"
"github.com/hashicorp/terraform/internal/backend"
"github.com/hashicorp/terraform/internal/command/arguments"
"github.com/hashicorp/terraform/internal/command/clistate"
@ -20,7 +22,6 @@ import (
"github.com/hashicorp/terraform/internal/initwd"
"github.com/hashicorp/terraform/internal/states/statemgr"
"github.com/hashicorp/terraform/internal/terminal"
"github.com/zclconf/go-cty/cty"
)
func TestRemoteStoredVariableValue(t *testing.T) {
@ -186,7 +187,7 @@ func TestRemoteContextWithVars(t *testing.T) {
b, bCleanup := testBackendDefault(t)
defer bCleanup()
_, configLoader, configCleanup := initwd.MustLoadConfigForTests(t, configDir)
_, configLoader, configCleanup := initwd.MustLoadConfigForTests(t, configDir, "tests")
defer configCleanup()
workspaceID, err := b.getRemoteWorkspaceID(context.Background(), backend.DefaultStateName)
@ -409,7 +410,7 @@ func TestRemoteVariablesDoNotOverride(t *testing.T) {
b, bCleanup := testBackendDefault(t)
defer bCleanup()
_, configLoader, configCleanup := initwd.MustLoadConfigForTests(t, configDir)
_, configLoader, configCleanup := initwd.MustLoadConfigForTests(t, configDir, "tests")
defer configCleanup()
workspaceID, err := b.getRemoteWorkspaceID(context.Background(), backend.DefaultStateName)

View File

@ -14,6 +14,8 @@ import (
"github.com/google/go-cmp/cmp"
tfe "github.com/hashicorp/go-tfe"
"github.com/mitchellh/cli"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/backend"
"github.com/hashicorp/terraform/internal/cloud"
@ -27,7 +29,6 @@ import (
"github.com/hashicorp/terraform/internal/states/statemgr"
"github.com/hashicorp/terraform/internal/terminal"
"github.com/hashicorp/terraform/internal/terraform"
"github.com/mitchellh/cli"
)
func testOperationPlan(t *testing.T, configDir string) (*backend.Operation, func(), func(*testing.T) *terminal.TestOutput) {
@ -39,7 +40,7 @@ func testOperationPlan(t *testing.T, configDir string) (*backend.Operation, func
func testOperationPlanWithTimeout(t *testing.T, configDir string, timeout time.Duration) (*backend.Operation, func(), func(*testing.T) *terminal.TestOutput) {
t.Helper()
_, configLoader, configCleanup := initwd.MustLoadConfigForTests(t, configDir)
_, configLoader, configCleanup := initwd.MustLoadConfigForTests(t, configDir, "tests")
streams, done := terminal.StreamsForTesting(t)
view := views.NewView(streams)

View File

@ -8,6 +8,7 @@ import (
"testing"
"github.com/google/go-cmp/cmp"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/configs/configload"
"github.com/hashicorp/terraform/internal/initwd"
@ -18,7 +19,7 @@ func TestChecksHappyPath(t *testing.T) {
loader, close := configload.NewLoaderForTests(t)
defer close()
inst := initwd.NewModuleInstaller(loader.ModulesDir(), loader, nil)
_, instDiags := inst.InstallModules(context.Background(), fixtureDir, true, initwd.ModuleInstallHooksImpl{})
_, instDiags := inst.InstallModules(context.Background(), fixtureDir, "tests", true, initwd.ModuleInstallHooksImpl{})
if instDiags.HasErrors() {
t.Fatal(instDiags.Err())
}

View File

@ -18,6 +18,8 @@ import (
tfe "github.com/hashicorp/go-tfe"
mocks "github.com/hashicorp/go-tfe/mocks"
version "github.com/hashicorp/go-version"
"github.com/mitchellh/cli"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/backend"
"github.com/hashicorp/terraform/internal/command/arguments"
@ -32,7 +34,6 @@ import (
"github.com/hashicorp/terraform/internal/terminal"
"github.com/hashicorp/terraform/internal/terraform"
tfversion "github.com/hashicorp/terraform/version"
"github.com/mitchellh/cli"
)
func testOperationApply(t *testing.T, configDir string) (*backend.Operation, func(), func(*testing.T) *terminal.TestOutput) {
@ -44,7 +45,7 @@ func testOperationApply(t *testing.T, configDir string) (*backend.Operation, fun
func testOperationApplyWithTimeout(t *testing.T, configDir string, timeout time.Duration) (*backend.Operation, func(), func(*testing.T) *terminal.TestOutput) {
t.Helper()
_, configLoader, configCleanup := initwd.MustLoadConfigForTests(t, configDir)
_, configLoader, configCleanup := initwd.MustLoadConfigForTests(t, configDir, "tests")
streams, done := terminal.StreamsForTesting(t)
view := views.NewView(streams)

View File

@ -9,6 +9,8 @@ import (
"testing"
tfe "github.com/hashicorp/go-tfe"
"github.com/zclconf/go-cty/cty"
"github.com/hashicorp/terraform/internal/backend"
"github.com/hashicorp/terraform/internal/command/arguments"
"github.com/hashicorp/terraform/internal/command/clistate"
@ -19,7 +21,6 @@ import (
"github.com/hashicorp/terraform/internal/terminal"
"github.com/hashicorp/terraform/internal/terraform"
"github.com/hashicorp/terraform/internal/tfdiags"
"github.com/zclconf/go-cty/cty"
)
func TestRemoteStoredVariableValue(t *testing.T) {
@ -185,7 +186,7 @@ func TestRemoteContextWithVars(t *testing.T) {
b, bCleanup := testBackendWithName(t)
defer bCleanup()
_, configLoader, configCleanup := initwd.MustLoadConfigForTests(t, configDir)
_, configLoader, configCleanup := initwd.MustLoadConfigForTests(t, configDir, "tests")
defer configCleanup()
workspaceID, err := b.getRemoteWorkspaceID(context.Background(), testBackendSingleWorkspaceName)
@ -408,7 +409,7 @@ func TestRemoteVariablesDoNotOverride(t *testing.T) {
b, bCleanup := testBackendWithName(t)
defer bCleanup()
_, configLoader, configCleanup := initwd.MustLoadConfigForTests(t, configDir)
_, configLoader, configCleanup := initwd.MustLoadConfigForTests(t, configDir, "tests")
defer configCleanup()
workspaceID, err := b.getRemoteWorkspaceID(context.Background(), testBackendSingleWorkspaceName)

View File

@ -16,6 +16,8 @@ import (
"github.com/google/go-cmp/cmp"
tfe "github.com/hashicorp/go-tfe"
"github.com/mitchellh/cli"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/backend"
"github.com/hashicorp/terraform/internal/command/arguments"
@ -29,7 +31,6 @@ import (
"github.com/hashicorp/terraform/internal/states/statemgr"
"github.com/hashicorp/terraform/internal/terminal"
"github.com/hashicorp/terraform/internal/terraform"
"github.com/mitchellh/cli"
)
func testOperationPlan(t *testing.T, configDir string) (*backend.Operation, func(), func(*testing.T) *terminal.TestOutput) {
@ -41,7 +42,7 @@ func testOperationPlan(t *testing.T, configDir string) (*backend.Operation, func
func testOperationPlanWithTimeout(t *testing.T, configDir string, timeout time.Duration) (*backend.Operation, func(), func(*testing.T) *terminal.TestOutput) {
t.Helper()
_, configLoader, configCleanup := initwd.MustLoadConfigForTests(t, configDir)
_, configLoader, configCleanup := initwd.MustLoadConfigForTests(t, configDir, "tests")
streams, done := terminal.StreamsForTesting(t)
view := views.NewView(streams)

View File

@ -9,6 +9,8 @@ import (
"testing"
"time"
"github.com/mitchellh/cli"
"github.com/hashicorp/terraform/internal/backend"
"github.com/hashicorp/terraform/internal/command/arguments"
"github.com/hashicorp/terraform/internal/command/clistate"
@ -17,7 +19,6 @@ import (
"github.com/hashicorp/terraform/internal/plans"
"github.com/hashicorp/terraform/internal/states/statemgr"
"github.com/hashicorp/terraform/internal/terminal"
"github.com/mitchellh/cli"
)
func testOperationRefresh(t *testing.T, configDir string) (*backend.Operation, func(), func(*testing.T) *terminal.TestOutput) {
@ -29,7 +30,7 @@ func testOperationRefresh(t *testing.T, configDir string) (*backend.Operation, f
func testOperationRefreshWithTimeout(t *testing.T, configDir string, timeout time.Duration) (*backend.Operation, func(), func(*testing.T) *terminal.TestOutput) {
t.Helper()
_, configLoader, configCleanup := initwd.MustLoadConfigForTests(t, configDir)
_, configLoader, configCleanup := initwd.MustLoadConfigForTests(t, configDir, "tests")
streams, done := terminal.StreamsForTesting(t)
view := views.NewView(streams)

View File

@ -0,0 +1,58 @@
package arguments
import "github.com/hashicorp/terraform/internal/tfdiags"
// Test represents the command-line arguments for the test command.
type Test struct {
// Filter contains a list of test files to execute. If empty, all test files
// will be executed.
Filter []string
// TestDirectory allows the user to override the directory that the test
// command will use to discover test files, defaults to "tests". Regardless
// of the value here, test files within the configuration directory will
// always be discovered.
TestDirectory string
// ViewType specifies which output format to use: human or JSON.
ViewType ViewType
// You can specify common variables for all tests from the command line.
Vars *Vars
// Verbose tells the test command to print out the plan either in
// human-readable format or JSON for each run step depending on the
// ViewType.
Verbose bool
}
func ParseTest(args []string) (*Test, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
test := Test{
Vars: new(Vars),
}
var jsonOutput bool
cmdFlags := extendedFlagSet("test", nil, nil, test.Vars)
cmdFlags.Var((*flagStringSlice)(&test.Filter), "filter", "filter")
cmdFlags.StringVar(&test.TestDirectory, "test-directory", "tests", "test-directory")
cmdFlags.BoolVar(&jsonOutput, "json", false, "json")
cmdFlags.BoolVar(&test.Verbose, "verbose", false, "verbose")
if err := cmdFlags.Parse(args); err != nil {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Failed to parse command-line flags",
err.Error()))
}
switch {
case jsonOutput:
test.ViewType = ViewJSON
default:
test.ViewType = ViewHuman
}
return &test, diags
}

View File

@ -0,0 +1,154 @@
package arguments
import (
"reflect"
"testing"
"github.com/davecgh/go-spew/spew"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/hashicorp/terraform/internal/tfdiags"
)
func TestParseTest_Vars(t *testing.T) {
tcs := map[string]struct {
args []string
want []FlagNameValue
}{
"no var flags by default": {
args: nil,
want: nil,
},
"one var": {
args: []string{"-var", "foo=bar"},
want: []FlagNameValue{
{Name: "-var", Value: "foo=bar"},
},
},
"one var-file": {
args: []string{"-var-file", "cool.tfvars"},
want: []FlagNameValue{
{Name: "-var-file", Value: "cool.tfvars"},
},
},
"ordering preserved": {
args: []string{
"-var", "foo=bar",
"-var-file", "cool.tfvars",
"-var", "boop=beep",
},
want: []FlagNameValue{
{Name: "-var", Value: "foo=bar"},
{Name: "-var-file", Value: "cool.tfvars"},
{Name: "-var", Value: "boop=beep"},
},
},
}
for name, tc := range tcs {
t.Run(name, func(t *testing.T) {
got, diags := ParseTest(tc.args)
if len(diags) > 0 {
t.Fatalf("unexpected diags: %v", diags)
}
if vars := got.Vars.All(); !cmp.Equal(vars, tc.want) {
t.Fatalf("unexpected result\n%s", cmp.Diff(vars, tc.want))
}
if got, want := got.Vars.Empty(), len(tc.want) == 0; got != want {
t.Fatalf("expected Empty() to return %t, but was %t", want, got)
}
})
}
}
func TestParseTest(t *testing.T) {
tcs := map[string]struct {
args []string
want *Test
wantDiags tfdiags.Diagnostics
}{
"defaults": {
args: nil,
want: &Test{
Filter: nil,
TestDirectory: "tests",
ViewType: ViewHuman,
Vars: &Vars{},
},
wantDiags: nil,
},
"with-filters": {
args: []string{"-filter=one.tftest", "-filter=two.tftest"},
want: &Test{
Filter: []string{"one.tftest", "two.tftest"},
TestDirectory: "tests",
ViewType: ViewHuman,
Vars: &Vars{},
},
wantDiags: nil,
},
"json": {
args: []string{"-json"},
want: &Test{
Filter: nil,
TestDirectory: "tests",
ViewType: ViewJSON,
Vars: &Vars{},
},
wantDiags: nil,
},
"test-directory": {
args: []string{"-test-directory=other"},
want: &Test{
Filter: nil,
TestDirectory: "other",
ViewType: ViewHuman,
Vars: &Vars{},
},
wantDiags: nil,
},
"verbose": {
args: []string{"-verbose"},
want: &Test{
Filter: nil,
TestDirectory: "tests",
ViewType: ViewHuman,
Verbose: true,
Vars: &Vars{},
},
},
"unknown flag": {
args: []string{"-boop"},
want: &Test{
Filter: nil,
TestDirectory: "tests",
ViewType: ViewHuman,
Vars: &Vars{},
},
wantDiags: tfdiags.Diagnostics{
tfdiags.Sourceless(
tfdiags.Error,
"Failed to parse command-line flags",
"flag provided but not defined: -boop",
),
},
},
}
cmpOpts := cmpopts.IgnoreUnexported(Operation{}, Vars{}, State{})
for name, tc := range tcs {
t.Run(name, func(t *testing.T) {
got, diags := ParseTest(tc.args)
if diff := cmp.Diff(tc.want, got, cmpOpts); len(diff) > 0 {
t.Errorf("diff:\n%s", diff)
}
if !reflect.DeepEqual(diags, tc.wantDiags) {
t.Errorf("wrong result\ngot: %s\nwant: %s", spew.Sdump(diags), spew.Sdump(tc.wantDiags))
}
})
}
}

View File

@ -26,6 +26,8 @@ import (
svchost "github.com/hashicorp/terraform-svchost"
"github.com/hashicorp/terraform-svchost/disco"
"github.com/zclconf/go-cty/cty"
"github.com/hashicorp/terraform/internal/addrs"
backendInit "github.com/hashicorp/terraform/internal/backend/init"
backendLocal "github.com/hashicorp/terraform/internal/backend/local"
@ -50,7 +52,6 @@ import (
"github.com/hashicorp/terraform/internal/terminal"
"github.com/hashicorp/terraform/internal/terraform"
"github.com/hashicorp/terraform/version"
"github.com/zclconf/go-cty/cty"
)
// These are the directories for our test data and fixtures.
@ -158,7 +159,7 @@ func testModuleWithSnapshot(t *testing.T, name string) (*configs.Config, *config
// sources only this ultimately just records all of the module paths
// in a JSON file so that we can load them below.
inst := initwd.NewModuleInstaller(loader.ModulesDir(), loader, registry.NewClient(nil, nil))
_, instDiags := inst.InstallModules(context.Background(), dir, true, initwd.ModuleInstallHooksImpl{})
_, instDiags := inst.InstallModules(context.Background(), dir, "tests", true, initwd.ModuleInstallHooksImpl{})
if instDiags.HasErrors() {
t.Fatal(instDiags.Err())
}

View File

@ -19,10 +19,12 @@ type GetCommand struct {
func (c *GetCommand) Run(args []string) int {
var update bool
var testsDirectory string
args = c.Meta.process(args)
cmdFlags := c.Meta.defaultFlagSet("get")
cmdFlags.BoolVar(&update, "update", false, "update")
cmdFlags.StringVar(&testsDirectory, "test-directory", "tests", "test-directory")
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
if err := cmdFlags.Parse(args); err != nil {
c.Ui.Error(fmt.Sprintf("Error parsing command-line flags: %s\n", err.Error()))
@ -41,7 +43,7 @@ func (c *GetCommand) Run(args []string) int {
path = c.normalizePath(path)
abort, diags := getModules(ctx, &c.Meta, path, update)
abort, diags := getModules(ctx, &c.Meta, path, testsDirectory, update)
c.showDiagnostics(diags)
if abort || diags.HasErrors() {
return 1
@ -68,10 +70,12 @@ Usage: terraform [global options] get [options]
Options:
-update Check already-downloaded modules for available updates
and install the newest versions available.
-update Check already-downloaded modules for available updates
and install the newest versions available.
-no-color Disable text coloring in the output.
-no-color Disable text coloring in the output.
-test-directory=path Set the Terraform test directory, defaults to "tests".
`
return strings.TrimSpace(helpText)
@ -81,10 +85,10 @@ func (c *GetCommand) Synopsis() string {
return "Install or upgrade remote Terraform modules"
}
func getModules(ctx context.Context, m *Meta, path string, upgrade bool) (abort bool, diags tfdiags.Diagnostics) {
func getModules(ctx context.Context, m *Meta, path string, testsDir string, upgrade bool) (abort bool, diags tfdiags.Diagnostics) {
hooks := uiModuleInstallHooks{
Ui: m.Ui,
ShowLocalPaths: true,
}
return m.installModules(ctx, path, upgrade, hooks)
return m.installModules(ctx, path, testsDir, upgrade, hooks)
}

View File

@ -41,7 +41,7 @@ type InitCommand struct {
}
func (c *InitCommand) Run(args []string) int {
var flagFromModule, flagLockfile string
var flagFromModule, flagLockfile, testsDirectory string
var flagBackend, flagCloud, flagGet, flagUpgrade bool
var flagPluginPath FlagStringSlice
flagConfigExtra := newRawFlags("-backend-config")
@ -62,6 +62,7 @@ func (c *InitCommand) Run(args []string) int {
cmdFlags.Var(&flagPluginPath, "plugin-dir", "plugin directory")
cmdFlags.StringVar(&flagLockfile, "lockfile", "", "Set a dependency lockfile mode")
cmdFlags.BoolVar(&c.Meta.ignoreRemoteVersion, "ignore-remote-version", false, "continue even if remote and local Terraform versions are incompatible")
cmdFlags.StringVar(&testsDirectory, "test-directory", "tests", "test-directory")
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
if err := cmdFlags.Parse(args); err != nil {
return 1
@ -172,7 +173,7 @@ func (c *InitCommand) Run(args []string) int {
}
// Load just the root module to begin backend and module initialization
rootModEarly, earlyConfDiags := c.loadSingleModuleWithTests(path, "tests")
rootModEarly, earlyConfDiags := c.loadSingleModuleWithTests(path, testsDirectory)
// There may be parsing errors in config loading but these will be shown later _after_
// checking for core version requirement errors. Not meeting the version requirement should
@ -233,7 +234,7 @@ func (c *InitCommand) Run(args []string) int {
}
if flagGet {
modsOutput, modsAbort, modsDiags := c.getModules(ctx, path, rootModEarly, flagUpgrade)
modsOutput, modsAbort, modsDiags := c.getModules(ctx, path, testsDirectory, rootModEarly, flagUpgrade)
diags = diags.Append(modsDiags)
if modsAbort || modsDiags.HasErrors() {
c.showDiagnostics(diags)
@ -246,7 +247,7 @@ func (c *InitCommand) Run(args []string) int {
// With all of the modules (hopefully) installed, we can now try to load the
// whole configuration tree.
config, confDiags := c.loadConfigWithTests(path, "tests")
config, confDiags := c.loadConfigWithTests(path, testsDirectory)
// configDiags will be handled after the version constraint check, since an
// incorrect version of terraform may be producing errors for configuration
// constructs added in later versions.
@ -342,7 +343,7 @@ func (c *InitCommand) Run(args []string) int {
return 0
}
func (c *InitCommand) getModules(ctx context.Context, path string, earlyRoot *configs.Module, upgrade bool) (output bool, abort bool, diags tfdiags.Diagnostics) {
func (c *InitCommand) getModules(ctx context.Context, path, testsDir string, earlyRoot *configs.Module, upgrade bool) (output bool, abort bool, diags tfdiags.Diagnostics) {
testModules := false // We can also have modules buried in test files.
for _, file := range earlyRoot.Tests {
for _, run := range file.Runs {
@ -373,7 +374,7 @@ func (c *InitCommand) getModules(ctx context.Context, path string, earlyRoot *co
ShowLocalPaths: true,
}
installAbort, installDiags := c.installModules(ctx, path, upgrade, hooks)
installAbort, installDiags := c.installModules(ctx, path, testsDir, upgrade, hooks)
diags = diags.Append(installDiags)
// At this point, installModules may have generated error diags or been
@ -1175,6 +1176,8 @@ Options:
See the documentation on configuring Terraform with
Terraform Cloud for more information.
-test-directory=path Set the Terraform test directory, defaults to "tests".
`
return strings.TrimSpace(helpText)
}

View File

@ -3,17 +3,17 @@
package jsonplan
// module is the representation of a module in state. This can be the root
// Module is the representation of a module in state. This can be the root
// module or a child module.
type module struct {
type Module struct {
// Resources are sorted in a user-friendly order that is undefined at this
// time, but consistent.
Resources []resource `json:"resources,omitempty"`
Resources []Resource `json:"resources,omitempty"`
// Address is the absolute module address, omitted for the root module
Address string `json:"address,omitempty"`
// Each module object can optionally have its own nested "child_modules",
// recursively describing the full module tree.
ChildModules []module `json:"child_modules,omitempty"`
ChildModules []Module `json:"child_modules,omitempty"`
}

View File

@ -48,11 +48,11 @@ const (
// Plan is the top-level representation of the json format of a plan. It includes
// the complete config and current state.
type plan struct {
type Plan struct {
FormatVersion string `json:"format_version,omitempty"`
TerraformVersion string `json:"terraform_version,omitempty"`
Variables variables `json:"variables,omitempty"`
PlannedValues stateValues `json:"planned_values,omitempty"`
Variables Variables `json:"variables,omitempty"`
PlannedValues StateValues `json:"planned_values,omitempty"`
// ResourceDrift and ResourceChanges are sorted in a user-friendly order
// that is undefined at this time, but consistent.
ResourceDrift []ResourceChange `json:"resource_drift,omitempty"`
@ -66,8 +66,8 @@ type plan struct {
Errored bool `json:"errored"`
}
func newPlan() *plan {
return &plan{
func newPlan() *Plan {
return &Plan{
FormatVersion: FormatVersion,
}
}
@ -150,17 +150,17 @@ type Importing struct {
ID string `json:"id,omitempty"`
}
type output struct {
type Output struct {
Sensitive bool `json:"sensitive"`
Type json.RawMessage `json:"type,omitempty"`
Value json.RawMessage `json:"value,omitempty"`
}
// variables is the JSON representation of the variables provided to the current
// Variables is the JSON representation of the variables provided to the current
// plan.
type variables map[string]*variable
type Variables map[string]*Variable
type variable struct {
type Variable struct {
Value json.RawMessage `json:"value,omitempty"`
}
@ -212,13 +212,14 @@ func MarshalForRenderer(
return output.OutputChanges, output.ResourceChanges, output.ResourceDrift, output.RelevantAttributes, nil
}
// Marshal returns the json encoding of a terraform plan.
func Marshal(
// MarshalForLog returns the original JSON compatible plan, ready for a logging
// package to marshal further.
func MarshalForLog(
config *configs.Config,
p *plans.Plan,
sf *statefile.File,
schemas *terraform.Schemas,
) ([]byte, error) {
) (*Plan, error) {
output := newPlan()
output.TerraformVersion = version.String()
output.Timestamp = p.Timestamp.Format(time.RFC3339)
@ -293,12 +294,26 @@ func Marshal(
return nil, fmt.Errorf("error marshaling config: %s", err)
}
ret, err := json.Marshal(output)
return ret, err
return output, nil
}
func (p *plan) marshalPlanVariables(vars map[string]plans.DynamicValue, decls map[string]*configs.Variable) error {
p.Variables = make(variables, len(vars))
// Marshal returns the json encoding of a terraform plan.
func Marshal(
config *configs.Config,
p *plans.Plan,
sf *statefile.File,
schemas *terraform.Schemas,
) ([]byte, error) {
output, err := MarshalForLog(config, p, sf, schemas)
if err != nil {
return nil, err
}
return json.Marshal(output)
}
func (p *Plan) marshalPlanVariables(vars map[string]plans.DynamicValue, decls map[string]*configs.Variable) error {
p.Variables = make(Variables, len(vars))
for k, v := range vars {
val, err := v.Decode(cty.DynamicPseudoType)
@ -309,7 +324,7 @@ func (p *plan) marshalPlanVariables(vars map[string]plans.DynamicValue, decls ma
if err != nil {
return err
}
p.Variables[k] = &variable{
p.Variables[k] = &Variable{
Value: valJSON,
}
}
@ -338,7 +353,7 @@ func (p *plan) marshalPlanVariables(vars map[string]plans.DynamicValue, decls ma
if err != nil {
return err
}
p.Variables[name] = &variable{
p.Variables[name] = &Variable{
Value: valJSON,
}
}
@ -639,7 +654,7 @@ func MarshalOutputChanges(changes *plans.Changes) (map[string]Change, error) {
return outputChanges, nil
}
func (p *plan) marshalPlannedValues(changes *plans.Changes, schemas *terraform.Schemas) error {
func (p *Plan) marshalPlannedValues(changes *plans.Changes, schemas *terraform.Schemas) error {
// marshal the planned changes into a module
plan, err := marshalPlannedValues(changes, schemas)
if err != nil {
@ -657,7 +672,7 @@ func (p *plan) marshalPlannedValues(changes *plans.Changes, schemas *terraform.S
return nil
}
func (p *plan) marshalRelevantAttrs(plan *plans.Plan) error {
func (p *Plan) marshalRelevantAttrs(plan *plans.Plan) error {
for _, ra := range plan.RelevantAttributes {
addr := ra.Resource.String()
path, err := encodePath(ra.Attr)

View File

@ -10,7 +10,7 @@ import (
)
// Resource is the representation of a resource in the json plan
type resource struct {
type Resource struct {
// Address is the absolute resource address
Address string `json:"address,omitempty"`
@ -37,7 +37,7 @@ type resource struct {
// resource, whose structure depends on the resource type schema. Any
// unknown values are omitted or set to null, making them indistinguishable
// from absent values.
AttributeValues attributeValues `json:"values,omitempty"`
AttributeValues AttributeValues `json:"values,omitempty"`
// SensitiveValues is similar to AttributeValues, but with all sensitive
// values replaced with true, and all non-sensitive leaf values omitted.

View File

@ -19,22 +19,22 @@ import (
"github.com/hashicorp/terraform/internal/terraform"
)
// stateValues is the common representation of resolved values for both the
// StateValues is the common representation of resolved values for both the
// prior state (which is always complete) and the planned new state.
type stateValues struct {
Outputs map[string]output `json:"outputs,omitempty"`
RootModule module `json:"root_module,omitempty"`
type StateValues struct {
Outputs map[string]Output `json:"outputs,omitempty"`
RootModule Module `json:"root_module,omitempty"`
}
// attributeValues is the JSON representation of the attribute values of the
// AttributeValues is the JSON representation of the attribute values of the
// resource, whose structure depends on the resource type schema.
type attributeValues map[string]interface{}
type AttributeValues map[string]interface{}
func marshalAttributeValues(value cty.Value, schema *configschema.Block) attributeValues {
func marshalAttributeValues(value cty.Value, schema *configschema.Block) AttributeValues {
if value == cty.NilVal || value.IsNull() {
return nil
}
ret := make(attributeValues)
ret := make(AttributeValues)
it := value.ElementIterator()
for it.Next() {
@ -47,13 +47,13 @@ func marshalAttributeValues(value cty.Value, schema *configschema.Block) attribu
// marshalPlannedOutputs takes a list of changes and returns a map of output
// values
func marshalPlannedOutputs(changes *plans.Changes) (map[string]output, error) {
func marshalPlannedOutputs(changes *plans.Changes) (map[string]Output, error) {
if changes.Outputs == nil {
// No changes - we're done here!
return nil, nil
}
ret := make(map[string]output)
ret := make(map[string]Output)
for _, oc := range changes.Outputs {
if oc.ChangeSrc.Action == plans.Delete {
@ -82,7 +82,7 @@ func marshalPlannedOutputs(changes *plans.Changes) (map[string]output, error) {
}
}
ret[oc.Addr.OutputValue.Name] = output{
ret[oc.Addr.OutputValue.Name] = Output{
Value: json.RawMessage(after),
Type: json.RawMessage(afterType),
Sensitive: oc.Sensitive,
@ -93,8 +93,8 @@ func marshalPlannedOutputs(changes *plans.Changes) (map[string]output, error) {
}
func marshalPlannedValues(changes *plans.Changes, schemas *terraform.Schemas) (module, error) {
var ret module
func marshalPlannedValues(changes *plans.Changes, schemas *terraform.Schemas) (Module, error) {
var ret Module
// build two maps:
// module name -> [resource addresses]
@ -166,8 +166,8 @@ func marshalPlannedValues(changes *plans.Changes, schemas *terraform.Schemas) (m
}
// marshalPlanResources
func marshalPlanResources(changes *plans.Changes, ris []addrs.AbsResourceInstance, schemas *terraform.Schemas) ([]resource, error) {
var ret []resource
func marshalPlanResources(changes *plans.Changes, ris []addrs.AbsResourceInstance, schemas *terraform.Schemas) ([]Resource, error) {
var ret []Resource
for _, ri := range ris {
r := changes.ResourceInstance(ri)
@ -175,7 +175,7 @@ func marshalPlanResources(changes *plans.Changes, ris []addrs.AbsResourceInstanc
continue
}
resource := resource{
resource := Resource{
Address: r.Addr.String(),
Type: r.Addr.Resource.Resource.Type,
Name: r.Addr.Resource.Resource.Name,
@ -252,14 +252,14 @@ func marshalPlanModules(
childModules []addrs.ModuleInstance,
moduleMap map[string][]addrs.ModuleInstance,
moduleResourceMap map[string][]addrs.AbsResourceInstance,
) ([]module, error) {
) ([]Module, error) {
var ret []module
var ret []Module
for _, child := range childModules {
moduleResources := moduleResourceMap[child.String()]
// cm for child module, naming things is hard.
var cm module
var cm Module
// don't populate the address for the root module
if child.String() != "" {
cm.Address = child.String()

View File

@ -8,19 +8,20 @@ import (
"reflect"
"testing"
"github.com/zclconf/go-cty/cty"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/configs/configschema"
"github.com/hashicorp/terraform/internal/plans"
"github.com/hashicorp/terraform/internal/providers"
"github.com/hashicorp/terraform/internal/terraform"
"github.com/zclconf/go-cty/cty"
)
func TestMarshalAttributeValues(t *testing.T) {
tests := []struct {
Attr cty.Value
Schema *configschema.Block
Want attributeValues
Want AttributeValues
}{
{
cty.NilVal,
@ -58,7 +59,7 @@ func TestMarshalAttributeValues(t *testing.T) {
},
},
},
attributeValues{"foo": json.RawMessage(`"bar"`)},
AttributeValues{"foo": json.RawMessage(`"bar"`)},
},
{
cty.ObjectVal(map[string]cty.Value{
@ -72,7 +73,7 @@ func TestMarshalAttributeValues(t *testing.T) {
},
},
},
attributeValues{"foo": json.RawMessage(`null`)},
AttributeValues{"foo": json.RawMessage(`null`)},
},
{
cty.ObjectVal(map[string]cty.Value{
@ -96,7 +97,7 @@ func TestMarshalAttributeValues(t *testing.T) {
},
},
},
attributeValues{
AttributeValues{
"bar": json.RawMessage(`{"hello":"world"}`),
"baz": json.RawMessage(`["goodnight","moon"]`),
},
@ -117,7 +118,7 @@ func TestMarshalPlannedOutputs(t *testing.T) {
tests := []struct {
Changes *plans.Changes
Want map[string]output
Want map[string]Output
Err bool
}{
{
@ -138,7 +139,7 @@ func TestMarshalPlannedOutputs(t *testing.T) {
},
},
},
map[string]output{
map[string]Output{
"bar": {
Sensitive: false,
Type: json.RawMessage(`"string"`),
@ -159,7 +160,7 @@ func TestMarshalPlannedOutputs(t *testing.T) {
},
},
},
map[string]output{},
map[string]Output{},
false,
},
}
@ -187,7 +188,7 @@ func TestMarshalPlanResources(t *testing.T) {
Action plans.Action
Before cty.Value
After cty.Value
Want []resource
Want []Resource
Err bool
}{
"create with unknowns": {
@ -197,7 +198,7 @@ func TestMarshalPlanResources(t *testing.T) {
"woozles": cty.UnknownVal(cty.String),
"foozles": cty.UnknownVal(cty.String),
}),
Want: []resource{{
Want: []Resource{{
Address: "test_thing.example",
Mode: "managed",
Type: "test_thing",
@ -205,7 +206,7 @@ func TestMarshalPlanResources(t *testing.T) {
Index: addrs.InstanceKey(nil),
ProviderName: "registry.terraform.io/hashicorp/test",
SchemaVersion: 1,
AttributeValues: attributeValues{},
AttributeValues: AttributeValues{},
SensitiveValues: json.RawMessage("{}"),
}},
Err: false,
@ -240,7 +241,7 @@ func TestMarshalPlanResources(t *testing.T) {
"woozles": cty.StringVal("baz"),
"foozles": cty.StringVal("bat"),
}),
Want: []resource{{
Want: []Resource{{
Address: "test_thing.example",
Mode: "managed",
Type: "test_thing",
@ -248,7 +249,7 @@ func TestMarshalPlanResources(t *testing.T) {
Index: addrs.InstanceKey(nil),
ProviderName: "registry.terraform.io/hashicorp/test",
SchemaVersion: 1,
AttributeValues: attributeValues{
AttributeValues: AttributeValues{
"woozles": json.RawMessage(`"baz"`),
"foozles": json.RawMessage(`"bat"`),
},

View File

@ -29,18 +29,18 @@ const (
DataResourceMode = "data"
)
// state is the top-level representation of the json format of a terraform
// State is the top-level representation of the json format of a terraform
// state.
type state struct {
type State struct {
FormatVersion string `json:"format_version,omitempty"`
TerraformVersion string `json:"terraform_version,omitempty"`
Values *stateValues `json:"values,omitempty"`
Values *StateValues `json:"values,omitempty"`
Checks json.RawMessage `json:"checks,omitempty"`
}
// stateValues is the common representation of resolved values for both the prior
// StateValues is the common representation of resolved values for both the prior
// state (which is always complete) and the planned new state.
type stateValues struct {
type StateValues struct {
Outputs map[string]Output `json:"outputs,omitempty"`
RootModule Module `json:"root_module,omitempty"`
}
@ -135,8 +135,8 @@ func marshalAttributeValues(value cty.Value) AttributeValues {
}
// newState() returns a minimally-initialized state
func newState() *state {
return &state{
func newState() *State {
return &State{
FormatVersion: FormatVersion,
}
}
@ -162,13 +162,13 @@ func MarshalForRenderer(sf *statefile.File, schemas *terraform.Schemas) (Module,
return root, outputs, err
}
// Marshal returns the json encoding of a terraform state.
func Marshal(sf *statefile.File, schemas *terraform.Schemas) ([]byte, error) {
// MarshalForLog returns the origin JSON compatible state, read for a logging
// package to marshal further.
func MarshalForLog(sf *statefile.File, schemas *terraform.Schemas) (*State, error) {
output := newState()
if sf == nil || sf.State.Empty() {
ret, err := json.Marshal(output)
return ret, err
return output, nil
}
if sf.TerraformVersion != nil {
@ -186,12 +186,22 @@ func Marshal(sf *statefile.File, schemas *terraform.Schemas) ([]byte, error) {
output.Checks = jsonchecks.MarshalCheckStates(sf.State.CheckResults)
}
return output, nil
}
// Marshal returns the json encoding of a terraform state.
func Marshal(sf *statefile.File, schemas *terraform.Schemas) ([]byte, error) {
output, err := MarshalForLog(sf, schemas)
if err != nil {
return nil, err
}
ret, err := json.Marshal(output)
return ret, err
}
func (jsonstate *state) marshalStateValues(s *states.State, schemas *terraform.Schemas) error {
var sv stateValues
func (jsonstate *State) marshalStateValues(s *states.State, schemas *terraform.Schemas) error {
var sv StateValues
var err error
// only marshal the root module outputs

View File

@ -183,7 +183,7 @@ func (m *Meta) loadHCLFile(filename string) (hcl.Body, tfdiags.Diagnostics) {
// can then be relayed to the end-user. The uiModuleInstallHooks type in
// this package has a reasonable implementation for displaying notifications
// via a provided cli.Ui.
func (m *Meta) installModules(ctx context.Context, rootDir string, upgrade bool, hooks initwd.ModuleInstallHooks) (abort bool, diags tfdiags.Diagnostics) {
func (m *Meta) installModules(ctx context.Context, rootDir, testsDir string, upgrade bool, hooks initwd.ModuleInstallHooks) (abort bool, diags tfdiags.Diagnostics) {
ctx, span := tracer.Start(ctx, "install modules")
defer span.End()
@ -203,7 +203,7 @@ func (m *Meta) installModules(ctx context.Context, rootDir string, upgrade bool,
inst := initwd.NewModuleInstaller(m.modulesDir(), loader, m.registryClient())
_, moreDiags := inst.InstallModules(ctx, rootDir, upgrade, hooks)
_, moreDiags := inst.InstallModules(ctx, rootDir, testsDir, upgrade, hooks)
diags = diags.Append(moreDiags)
if ctx.Err() == context.Canceled {

View File

@ -30,8 +30,11 @@ func (c *ProvidersCommand) Synopsis() string {
}
func (c *ProvidersCommand) Run(args []string) int {
var testsDirectory string
args = c.Meta.process(args)
cmdFlags := c.Meta.defaultFlagSet("providers")
cmdFlags.StringVar(&testsDirectory, "test-directory", "tests", "test-directory")
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
if err := cmdFlags.Parse(args); err != nil {
c.Ui.Error(fmt.Sprintf("Error parsing command-line flags: %s\n", err.Error()))
@ -70,7 +73,7 @@ func (c *ProvidersCommand) Run(args []string) int {
return 1
}
config, configDiags := c.loadConfigWithTests(configPath, "tests")
config, configDiags := c.loadConfigWithTests(configPath, testsDirectory)
diags = diags.Append(configDiags)
if configDiags.HasErrors() {
c.showDiagnostics(diags)
@ -172,7 +175,7 @@ func (c *ProvidersCommand) populateTreeNode(tree treeprint.Tree, node *configs.M
}
const providersCommandHelp = `
Usage: terraform [global options] providers [DIR]
Usage: terraform [global options] providers [options] [DIR]
Prints out a tree of modules in the referenced configuration annotated with
their provider requirements.
@ -180,4 +183,8 @@ Usage: terraform [global options] providers [DIR]
This provides an overview of all of the provider requirements across all
referenced modules, as an aid to understanding why particular provider
plugins are needed and why particular versions are selected.
Options:
-test-directory=path Set the Terraform test directory, defaults to "tests".
`

View File

@ -3,11 +3,10 @@ package command
import (
"context"
"fmt"
"path"
"sort"
"strings"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/backend"
"github.com/hashicorp/terraform/internal/command/arguments"
@ -43,7 +42,26 @@ Usage: terraform [global options] test [options]
Options:
TODO: implement optional arguments.
-filter=testfile If specified, Terraform will only execute the test files
specified by this flag. You can use this option multiple
times to execute more than one test file.
-json If specified, machine readable output will be printed in
JSON format
-test-directory=path Set the Terraform test directory, defaults to "tests".
-var 'foo=bar' Set a value for one of the input variables in the root
module of the configuration. Use this option more than
once to set more than one variable.
-var-file=filename Load variable values from the given file, in addition
to the default files terraform.tfvars and *.auto.tfvars.
Use this option more than once to include more than one
variables file.
-verbose Print the plan or state for each test run block as it
executes.
`
return strings.TrimSpace(helpText)
}
@ -55,26 +73,69 @@ func (c *TestCommand) Synopsis() string {
func (c *TestCommand) Run(rawArgs []string) int {
var diags tfdiags.Diagnostics
common, _ := arguments.ParseView(rawArgs)
common, rawArgs := arguments.ParseView(rawArgs)
c.View.Configure(common)
view := views.NewTest(arguments.ViewHuman, c.View)
args, diags := arguments.ParseTest(rawArgs)
if diags.HasErrors() {
c.View.Diagnostics(diags)
c.View.HelpPrompt("test")
return 1
}
config, configDiags := c.loadConfigWithTests(".", "tests")
view := views.NewTest(args.ViewType, c.View)
config, configDiags := c.loadConfigWithTests(".", args.TestDirectory)
diags = diags.Append(configDiags)
if configDiags.HasErrors() {
view.Diagnostics(nil, nil, diags)
return 1
}
var fileDiags tfdiags.Diagnostics
suite := moduletest.Suite{
Files: func() map[string]*moduletest.File {
files := make(map[string]*moduletest.File)
if len(args.Filter) > 0 {
for _, name := range args.Filter {
file, ok := config.Module.Tests[name]
if !ok {
// If the filter is invalid, we'll simply skip this
// entry and print a warning. But we could still execute
// any other tests within the filter.
fileDiags.Append(tfdiags.Sourceless(
tfdiags.Warning,
"Unknown test file",
fmt.Sprintf("The specified test file, %s, could not be found.", name)))
continue
}
var runs []*moduletest.Run
for ix, run := range file.Runs {
runs = append(runs, &moduletest.Run{
Config: run,
Index: ix,
Name: run.Name,
})
}
files[name] = &moduletest.File{
Config: file,
Name: name,
Runs: runs,
}
}
return files
}
// Otherwise, we'll just do all the tests in the directory!
for name, file := range config.Module.Tests {
var runs []*moduletest.Run
for _, run := range file.Runs {
for ix, run := range file.Runs {
runs = append(runs, &moduletest.Run{
Config: run,
Index: ix,
Name: run.Name,
})
}
@ -88,6 +149,30 @@ func (c *TestCommand) Run(rawArgs []string) int {
}(),
}
diags = diags.Append(fileDiags)
if fileDiags.HasErrors() {
view.Diagnostics(nil, nil, diags)
return 1
}
// Users can also specify variables via the command line, so we'll parse
// all that here.
var items []rawFlag
for _, variable := range args.Vars.All() {
items = append(items, rawFlag{
Name: variable.Name,
Value: variable.Value,
})
}
c.variableArgs = rawFlags{items: &items}
variables, variableDiags := c.collectVariableValues()
diags = diags.Append(variableDiags)
if variableDiags.HasErrors() {
view.Diagnostics(nil, nil, diags)
return 1
}
runningCtx, done := context.WithCancel(context.Background())
stopCtx, stop := context.WithCancel(runningCtx)
cancelCtx, cancel := context.WithCancel(context.Background())
@ -106,6 +191,8 @@ func (c *TestCommand) Run(rawArgs []string) int {
// default to these values.
Cancelled: false,
Stopped: false,
Verbose: args.Verbose,
}
view.Abstract(&suite)
@ -116,7 +203,7 @@ func (c *TestCommand) Run(rawArgs []string) int {
defer stop()
defer cancel()
runner.Start()
runner.Start(variables)
}()
// Wait for the operation to complete, or for an interrupt to occur.
@ -187,9 +274,12 @@ type TestRunner struct {
// respond to external calls from the test command.
StoppedCtx context.Context
CancelledCtx context.Context
// Verbose tells the runner to print out plan files during each test run.
Verbose bool
}
func (runner *TestRunner) Start() {
func (runner *TestRunner) Start(globals map[string]backend.UnparsedVariableValue) {
var files []string
for name := range runner.Suite.Files {
files = append(files, name)
@ -203,16 +293,16 @@ func (runner *TestRunner) Start() {
}
file := runner.Suite.Files[name]
runner.ExecuteTestFile(file)
runner.ExecuteTestFile(file, globals)
runner.Suite.Status = runner.Suite.Status.Merge(file.Status)
}
}
func (runner *TestRunner) ExecuteTestFile(file *moduletest.File) {
func (runner *TestRunner) ExecuteTestFile(file *moduletest.File, globals map[string]backend.UnparsedVariableValue) {
mgr := new(TestStateManager)
mgr.runner = runner
mgr.State = states.NewState()
defer mgr.cleanupStates(file)
defer mgr.cleanupStates(file, globals)
file.Status = file.Status.Merge(moduletest.Pass)
for _, run := range file.Runs {
@ -241,13 +331,13 @@ func (runner *TestRunner) ExecuteTestFile(file *moduletest.File) {
if run.Config.ConfigUnderTest != nil {
// Then we want to execute a different module under a kind of
// sandbox.
state := runner.ExecuteTestRun(run, file, states.NewState(), run.Config.ConfigUnderTest)
state := runner.ExecuteTestRun(run, file, states.NewState(), run.Config.ConfigUnderTest, globals)
mgr.States = append(mgr.States, &TestModuleState{
State: state,
Run: run,
})
} else {
mgr.State = runner.ExecuteTestRun(run, file, mgr.State, runner.Config)
mgr.State = runner.ExecuteTestRun(run, file, mgr.State, runner.Config, globals)
}
file.Status = file.Status.Merge(run.Status)
}
@ -258,7 +348,7 @@ func (runner *TestRunner) ExecuteTestFile(file *moduletest.File) {
}
}
func (runner *TestRunner) ExecuteTestRun(run *moduletest.Run, file *moduletest.File, state *states.State, config *configs.Config) *states.State {
func (runner *TestRunner) ExecuteTestRun(run *moduletest.Run, file *moduletest.File, state *states.State, config *configs.Config, globals map[string]backend.UnparsedVariableValue) *states.State {
if runner.Cancelled {
// Don't do anything, just give up and return immediately.
// The surrounding functions should stop this even being called, but in
@ -299,7 +389,7 @@ func (runner *TestRunner) ExecuteTestRun(run *moduletest.Run, file *moduletest.F
ForceReplace: replaces,
SkipRefresh: !run.Config.Options.Refresh,
ExternalReferences: references,
}, run.Config.Command)
}, run.Config.Command, globals)
diags = run.ValidateExpectedFailures(diags)
run.Diagnostics = run.Diagnostics.Append(diags)
@ -321,7 +411,34 @@ func (runner *TestRunner) ExecuteTestRun(run *moduletest.Run, file *moduletest.F
return state
}
variables, diags := buildInputVariablesForAssertions(run, file, config)
// If the user wants to render the plans as part of the test output, we
// track that here.
if runner.Verbose {
schemas, diags := ctx.Schemas(config, state)
// If we're going to fail to render the plan, let's not fail the overall
// test. It can still have succeeded. So we'll add the diagnostics, but
// still report the test status as a success.
if diags.HasErrors() {
// This is very unlikely.
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Warning,
"Failed to print verbose output",
fmt.Sprintf("Terraform failed to print the verbose output for %s, other diagnostics will contain more details as to why.", path.Join(file.Name, run.Name))))
} else {
run.Verbose = &moduletest.Verbose{
Plan: plan,
State: state,
Config: config,
Providers: schemas.Providers,
Provisioners: schemas.Provisioners,
}
}
run.Diagnostics = run.Diagnostics.Append(diags)
}
variables, diags := buildInputVariablesForAssertions(run, file, config, globals)
run.Diagnostics = run.Diagnostics.Append(diags)
if diags.HasErrors() {
run.Status = moduletest.Error
@ -341,7 +458,7 @@ func (runner *TestRunner) ExecuteTestRun(run *moduletest.Run, file *moduletest.F
//
// The command argument decides whether it executes only a plan or also applies
// the plan it creates during the planning.
func (runner *TestRunner) execute(run *moduletest.Run, file *moduletest.File, config *configs.Config, state *states.State, opts *terraform.PlanOpts, command configs.TestCommand) (*terraform.Context, *plans.Plan, *states.State, tfdiags.Diagnostics) {
func (runner *TestRunner) execute(run *moduletest.Run, file *moduletest.File, config *configs.Config, state *states.State, opts *terraform.PlanOpts, command configs.TestCommand, globals map[string]backend.UnparsedVariableValue) (*terraform.Context, *plans.Plan, *states.State, tfdiags.Diagnostics) {
if opts.Mode == plans.DestroyMode && state.Empty() {
// Nothing to do!
return nil, nil, state, nil
@ -370,7 +487,7 @@ func (runner *TestRunner) execute(run *moduletest.Run, file *moduletest.File, co
// Second, gather any variables and give them to the plan options.
variables, variableDiags := buildInputVariablesForTest(run, file, config)
variables, variableDiags := buildInputVariablesForTest(run, file, config, globals)
diags = diags.Append(variableDiags)
if variableDiags.HasErrors() {
return nil, nil, state, diags
@ -556,7 +673,7 @@ type TestModuleState struct {
Run *moduletest.Run
}
func (manager *TestStateManager) cleanupStates(file *moduletest.File) {
func (manager *TestStateManager) cleanupStates(file *moduletest.File, globals map[string]backend.UnparsedVariableValue) {
if manager.runner.Cancelled {
// We are still going to print out the resources that we have left
@ -576,7 +693,7 @@ func (manager *TestStateManager) cleanupStates(file *moduletest.File) {
// First, we'll clean up the main state.
_, _, state, diags := manager.runner.execute(nil, file, manager.runner.Config, manager.State, &terraform.PlanOpts{
Mode: plans.DestroyMode,
}, configs.ApplyTestCommand)
}, configs.ApplyTestCommand, globals)
manager.runner.View.DestroySummary(diags, nil, file, state)
// Then we'll clean up the additional states for custom modules in reverse
@ -593,7 +710,7 @@ func (manager *TestStateManager) cleanupStates(file *moduletest.File) {
_, _, state, diags := manager.runner.execute(module.Run, file, module.Run.Config.ConfigUnderTest, module.State, &terraform.PlanOpts{
Mode: plans.DestroyMode,
}, configs.ApplyTestCommand)
}, configs.ApplyTestCommand, globals)
manager.runner.View.DestroySummary(diags, module.Run, file, state)
}
}
@ -606,37 +723,44 @@ func (manager *TestStateManager) cleanupStates(file *moduletest.File) {
// Crucially, it differs from buildInputVariablesForAssertions in that it only
// includes variables that are reference by the config and not everything that
// is defined within the test run block and test file.
func buildInputVariablesForTest(run *moduletest.Run, file *moduletest.File, config *configs.Config) (terraform.InputValues, tfdiags.Diagnostics) {
variables := make(map[string]hcl.Expression)
func buildInputVariablesForTest(run *moduletest.Run, file *moduletest.File, config *configs.Config, globals map[string]backend.UnparsedVariableValue) (terraform.InputValues, tfdiags.Diagnostics) {
variables := make(map[string]backend.UnparsedVariableValue)
for name := range config.Module.Variables {
if run != nil {
if expr, exists := run.Config.Variables[name]; exists {
// Local variables take precedence.
variables[name] = expr
variables[name] = unparsedVariableValueExpression{
expr: expr,
sourceType: terraform.ValueFromConfig,
}
continue
}
}
if file != nil {
if expr, exists := file.Config.Variables[name]; exists {
// If it's not set locally, it maybe set globally.
variables[name] = expr
// If it's not set locally, it maybe set for the entire file.
variables[name] = unparsedVariableValueExpression{
expr: expr,
sourceType: terraform.ValueFromConfig,
}
continue
}
}
if globals != nil {
// If it's not set locally or at the file level, maybe it was
// defined globally.
if variable, exists := globals[name]; exists {
variables[name] = variable
}
}
// If it's not set at all that might be okay if the variable is optional
// so we'll just not add anything to the map.
}
unparsed := make(map[string]backend.UnparsedVariableValue)
for key, value := range variables {
unparsed[key] = unparsedVariableValueExpression{
expr: value,
sourceType: terraform.ValueFromConfig,
}
}
return backend.ParseVariableValues(unparsed, config.Module.Variables)
return backend.ParseVariableValues(variables, config.Module.Variables)
}
// buildInputVariablesForAssertions creates a terraform.InputValues mapping that
@ -651,32 +775,41 @@ func buildInputVariablesForTest(run *moduletest.Run, file *moduletest.File, conf
// defined within the config. We might want to remove these warnings in the
// future, since it is actually okay for test files to have variables defined
// outside the configuration.
func buildInputVariablesForAssertions(run *moduletest.Run, file *moduletest.File, config *configs.Config) (terraform.InputValues, tfdiags.Diagnostics) {
merged := make(map[string]hcl.Expression)
func buildInputVariablesForAssertions(run *moduletest.Run, file *moduletest.File, config *configs.Config, globals map[string]backend.UnparsedVariableValue) (terraform.InputValues, tfdiags.Diagnostics) {
variables := make(map[string]backend.UnparsedVariableValue)
if run != nil {
for name, expr := range run.Config.Variables {
merged[name] = expr
variables[name] = unparsedVariableValueExpression{
expr: expr,
sourceType: terraform.ValueFromConfig,
}
}
}
if file != nil {
for name, expr := range file.Config.Variables {
if _, exists := merged[name]; exists {
if _, exists := variables[name]; exists {
// Then this variable was defined at the run level and we want
// that value to take precedence.
continue
}
merged[name] = expr
variables[name] = unparsedVariableValueExpression{
expr: expr,
sourceType: terraform.ValueFromConfig,
}
}
}
unparsed := make(map[string]backend.UnparsedVariableValue)
for key, value := range merged {
unparsed[key] = unparsedVariableValueExpression{
expr: value,
sourceType: terraform.ValueFromConfig,
for name, variable := range globals {
if _, exists := variables[name]; exists {
// Then this value was already defined at either the run level
// or the file level, and we want those values to take
// precedence.
continue
}
variables[name] = variable
}
return backend.ParseVariableValues(unparsed, config.Module.Variables)
return backend.ParseVariableValues(variables, config.Module.Variables)
}

View File

@ -5,6 +5,7 @@ import (
"strings"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/mitchellh/cli"
"github.com/zclconf/go-cty/cty"
@ -17,6 +18,7 @@ import (
func TestTest(t *testing.T) {
tcs := map[string]struct {
override string
args []string
expected string
code int
@ -30,6 +32,11 @@ func TestTest(t *testing.T) {
expected: "1 passed, 0 failed.",
code: 0,
},
"simple_pass_nested_alternate": {
args: []string{"-test-directory", "other"},
expected: "1 passed, 0 failed.",
code: 0,
},
"pass_with_locals": {
expected: "1 passed, 0 failed.",
code: 0,
@ -62,6 +69,26 @@ func TestTest(t *testing.T) {
expected: "1 passed, 0 failed.",
code: 0,
},
"multiple_files": {
expected: "2 passed, 0 failed",
code: 0,
},
"multiple_files_with_filter": {
override: "multiple_files",
args: []string{"-filter=one.tftest"},
expected: "1 passed, 0 failed",
code: 0,
},
"variables": {
expected: "2 passed, 0 failed",
code: 0,
},
"variables_overridden": {
override: "variables",
args: []string{"-var=input=foo"},
expected: "1 passed, 1 failed",
code: 1,
},
"simple_fail": {
expected: "0 passed, 1 failed.",
code: 1,
@ -89,8 +116,13 @@ func TestTest(t *testing.T) {
t.Skip()
}
file := name
if len(tc.override) > 0 {
file = tc.override
}
td := t.TempDir()
testCopyDir(t, testFixturePath(path.Join("test", name)), td)
testCopyDir(t, testFixturePath(path.Join("test", file)), td)
defer testChdir(t, td)()
provider := testing_command.NewProvider(nil)
@ -326,3 +358,62 @@ func TestTest_ModuleDependencies(t *testing.T) {
}
}
}
func TestTest_Verbose(t *testing.T) {
td := t.TempDir()
testCopyDir(t, testFixturePath(path.Join("test", "plan_then_apply")), td)
defer testChdir(t, td)()
provider := testing_command.NewProvider(nil)
view, done := testView(t)
c := &TestCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(provider.Provider),
View: view,
},
}
code := c.Run([]string{"-verbose", "-no-color"})
output := done(t)
if code != 0 {
t.Errorf("expected status code 0 but got %d", code)
}
expected := `main.tftest... pass
run "validate_test_resource"... pass
Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
+ create
Terraform will perform the following actions:
# test_resource.foo will be created
+ resource "test_resource" "foo" {
+ id = "constant_value"
+ value = "bar"
}
Plan: 1 to add, 0 to change, 0 to destroy.
run "validate_test_resource"... pass
# test_resource.foo:
resource "test_resource" "foo" {
id = "constant_value"
value = "bar"
}
Success! 2 passed, 0 failed.
`
actual := output.All()
if diff := cmp.Diff(actual, expected); len(diff) > 0 {
t.Errorf("output didn't match expected:\nexpected:\n%s\nactual:\n%s\ndiff:\n%s", expected, actual, diff)
}
if provider.ResourceCount() > 0 {
t.Errorf("should have deleted all resources on completion but left %v", provider.ResourceString())
}
}

View File

@ -0,0 +1,3 @@
resource "test_resource" "foo" {
value = "bar"
}

View File

@ -0,0 +1,6 @@
run "validate_test_resource" {
assert {
condition = test_resource.foo.value == "bar"
error_message = "invalid value"
}
}

View File

@ -0,0 +1,6 @@
run "validate_test_resource" {
assert {
condition = test_resource.foo.value == "bar"
error_message = "invalid value"
}
}

View File

@ -1,3 +1,4 @@
resource "test_resource" "foo" {
id = "constant_value"
value = "bar"
}

View File

@ -0,0 +1,3 @@
resource "test_resource" "foo" {
value = "bar"
}

View File

@ -0,0 +1,6 @@
run "validate_test_resource" {
assert {
condition = test_resource.foo.value == "bar"
error_message = "invalid value"
}
}

View File

@ -0,0 +1,8 @@
variable "input" {
type = string
default = "bar"
}
resource "test_resource" "foo" {
value = var.input
}

View File

@ -0,0 +1,6 @@
run "validate_test_resource" {
assert {
condition = test_resource.foo.value == "bar"
error_message = "invalid value"
}
}

View File

@ -0,0 +1,10 @@
run "validate_test_resource" {
variables {
input = "bar"
}
assert {
condition = test_resource.foo.value == "bar"
error_message = "invalid value"
}
}

View File

@ -33,6 +33,8 @@ const (
MessageTestAbstract MessageType = "test_abstract"
MessageTestFile MessageType = "test_file"
MessageTestRun MessageType = "test_run"
MessageTestPlan MessageType = "test_plan"
MessageTestState MessageType = "test_state"
MessageTestSummary MessageType = "test_summary"
MessageTestCleanup MessageType = "test_cleanup"
)

View File

@ -377,7 +377,7 @@ func TestJSONView_Outputs(t *testing.T) {
// against a slice of structs representing the desired log messages. It
// verifies that the output of JSONView is in JSON log format, one message per
// line.
func testJSONViewOutputEqualsFull(t *testing.T, output string, want []map[string]interface{}) {
func testJSONViewOutputEqualsFull(t *testing.T, output string, want []map[string]interface{}, options ...cmp.Option) {
t.Helper()
// Remove final trailing newline
@ -415,7 +415,7 @@ func testJSONViewOutputEqualsFull(t *testing.T, output string, want []map[string
}
}
if !cmp.Equal(wantStruct, gotStruct) {
if !cmp.Equal(wantStruct, gotStruct, options...) {
t.Errorf("unexpected output on line %d:\n%s", i, cmp.Diff(wantStruct, gotStruct))
}
}
@ -423,7 +423,7 @@ func testJSONViewOutputEqualsFull(t *testing.T, output string, want []map[string
// testJSONViewOutputEquals skips the first line of output, since it ought to
// be a version message that we don't care about for most of our tests.
func testJSONViewOutputEquals(t *testing.T, output string, want []map[string]interface{}) {
func testJSONViewOutputEquals(t *testing.T, output string, want []map[string]interface{}, options ...cmp.Option) {
t.Helper()
// Remove up to the first newline
@ -431,5 +431,5 @@ func testJSONViewOutputEquals(t *testing.T, output string, want []map[string]int
if index >= 0 {
output = output[index+1:]
}
testJSONViewOutputEqualsFull(t, output, want)
testJSONViewOutputEqualsFull(t, output, want, options...)
}

View File

@ -120,7 +120,7 @@ func TestShowJSON(t *testing.T) {
},
}
config, _, configCleanup := initwd.MustLoadConfigForTests(t, "./testdata/show")
config, _, configCleanup := initwd.MustLoadConfigForTests(t, "./testdata/show", "tests")
defer configCleanup()
for name, testCase := range testCases {

View File

@ -7,9 +7,16 @@ import (
"github.com/mitchellh/colorstring"
"github.com/hashicorp/terraform/internal/command/arguments"
"github.com/hashicorp/terraform/internal/command/jsonformat"
"github.com/hashicorp/terraform/internal/command/jsonplan"
"github.com/hashicorp/terraform/internal/command/jsonprovider"
"github.com/hashicorp/terraform/internal/command/jsonstate"
"github.com/hashicorp/terraform/internal/command/views/json"
"github.com/hashicorp/terraform/internal/configs"
"github.com/hashicorp/terraform/internal/moduletest"
"github.com/hashicorp/terraform/internal/states"
"github.com/hashicorp/terraform/internal/states/statefile"
"github.com/hashicorp/terraform/internal/terraform"
"github.com/hashicorp/terraform/internal/tfdiags"
)
@ -116,6 +123,72 @@ func (t *TestHuman) File(file *moduletest.File) {
func (t *TestHuman) Run(run *moduletest.Run, file *moduletest.File) {
t.view.streams.Printf(" run %q... %s\n", run.Name, colorizeTestStatus(run.Status, t.view.colorize))
if run.Verbose != nil {
// We're going to be more verbose about what we print, here's the plan
// or the state depending on the type of run we did.
schemas := &terraform.Schemas{
Providers: run.Verbose.Providers,
Provisioners: run.Verbose.Provisioners,
}
renderer := jsonformat.Renderer{
Streams: t.view.streams,
Colorize: t.view.colorize,
RunningInAutomation: t.view.runningInAutomation,
}
if run.Config.Command == configs.ApplyTestCommand {
// Then we'll print the state.
root, outputs, err := jsonstate.MarshalForRenderer(statefile.New(run.Verbose.State, file.Name, uint64(run.Index)), schemas)
if err != nil {
run.Diagnostics = run.Diagnostics.Append(tfdiags.Sourceless(
tfdiags.Warning,
"Failed to render test state",
fmt.Sprintf("Terraform could not marshal the state for display: %v", err)))
} else {
state := jsonformat.State{
StateFormatVersion: jsonstate.FormatVersion,
ProviderFormatVersion: jsonprovider.FormatVersion,
RootModule: root,
RootModuleOutputs: outputs,
ProviderSchemas: jsonprovider.MarshalForRenderer(schemas),
}
renderer.RenderHumanState(state)
}
} else {
// We'll print the plan.
outputs, changed, drift, attrs, err := jsonplan.MarshalForRenderer(run.Verbose.Plan, schemas)
if err != nil {
run.Diagnostics = run.Diagnostics.Append(tfdiags.Sourceless(
tfdiags.Warning,
"Failed to render test plan",
fmt.Sprintf("Terraform could not marshal the plan for display: %v", err)))
} else {
plan := jsonformat.Plan{
PlanFormatVersion: jsonplan.FormatVersion,
ProviderFormatVersion: jsonprovider.FormatVersion,
OutputChanges: outputs,
ResourceChanges: changed,
ResourceDrift: drift,
ProviderSchemas: jsonprovider.MarshalForRenderer(schemas),
RelevantAttributes: attrs,
}
var opts []jsonformat.PlanRendererOpt
if !run.Verbose.Plan.CanApply() {
opts = append(opts, jsonformat.CanNotApply)
}
if run.Verbose.Plan.Errored {
opts = append(opts, jsonformat.Errored)
}
renderer.RenderHumanPlan(plan, run.Verbose.Plan.UIMode, opts...)
}
}
}
// Finally we'll print out a summary of the diagnostics from the run.
t.Diagnostics(run, file, run.Diagnostics)
}
@ -257,6 +330,46 @@ func (t *TestJSON) Run(run *moduletest.Run, file *moduletest.File) {
"@testfile", file.Name,
"@testrun", run.Name)
if run.Verbose != nil {
schemas := &terraform.Schemas{
Providers: run.Verbose.Providers,
Provisioners: run.Verbose.Provisioners,
}
if run.Config.Command == configs.ApplyTestCommand {
state, err := jsonstate.MarshalForLog(statefile.New(run.Verbose.State, file.Name, uint64(run.Index)), schemas)
if err != nil {
run.Diagnostics = run.Diagnostics.Append(tfdiags.Sourceless(
tfdiags.Warning,
"Failed to render test state",
fmt.Sprintf("Terraform could not marshal the state for display: %v", err)))
} else {
t.view.log.Info(
"-verbose flag enabled, printing state",
"type", json.MessageTestState,
json.MessageTestState, state,
"@testfile", file.Name,
"@testrun", run.Name)
}
} else {
plan, err := jsonplan.MarshalForLog(run.Verbose.Config, run.Verbose.Plan, nil, schemas)
if err != nil {
run.Diagnostics = run.Diagnostics.Append(tfdiags.Sourceless(
tfdiags.Warning,
"Failed to render test plan",
fmt.Sprintf("Terraform could not marshal the plan for display: %v", err)))
} else {
t.view.log.Info(
"-verbose flag enabled, printing plan",
"type", json.MessageTestPlan,
json.MessageTestPlan, plan,
"@testfile", file.Name,
"@testrun", run.Name)
}
}
}
t.Diagnostics(run, file, run.Diagnostics)
}

View File

@ -1,13 +1,19 @@
package views
import (
"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/command/arguments"
"github.com/hashicorp/terraform/internal/configs"
"github.com/hashicorp/terraform/internal/configs/configschema"
"github.com/hashicorp/terraform/internal/moduletest"
"github.com/hashicorp/terraform/internal/plans"
"github.com/hashicorp/terraform/internal/providers"
"github.com/hashicorp/terraform/internal/states"
"github.com/hashicorp/terraform/internal/terminal"
"github.com/hashicorp/terraform/internal/tfdiags"
@ -524,6 +530,162 @@ other details
Error: an error occurred
something bad happened during this test
`,
},
"verbose_plan": {
Run: &moduletest.Run{
Name: "run_block",
Status: moduletest.Pass,
Config: &configs.TestRun{
Command: configs.PlanTestCommand,
},
Verbose: &moduletest.Verbose{
Plan: &plans.Plan{
Changes: &plans.Changes{
Resources: []*plans.ResourceInstanceChangeSrc{
{
Addr: addrs.AbsResourceInstance{
Module: addrs.RootModuleInstance,
Resource: addrs.ResourceInstance{
Resource: addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "test_resource",
Name: "creating",
},
},
},
PrevRunAddr: addrs.AbsResourceInstance{
Module: addrs.RootModuleInstance,
Resource: addrs.ResourceInstance{
Resource: addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "test_resource",
Name: "creating",
},
},
},
ProviderAddr: addrs.AbsProviderConfig{
Module: addrs.RootModule,
Provider: addrs.Provider{
Hostname: addrs.DefaultProviderRegistryHost,
Namespace: "hashicorp",
Type: "test",
},
},
ChangeSrc: plans.ChangeSrc{
Action: plans.Create,
After: dynamicValue(
t,
cty.ObjectVal(map[string]cty.Value{
"value": cty.StringVal("Hello, world!"),
}),
cty.Object(map[string]cty.Type{
"value": cty.String,
})),
},
},
},
},
},
State: states.NewState(), // empty state
Config: &configs.Config{},
Providers: map[addrs.Provider]providers.ProviderSchema{
addrs.Provider{
Hostname: addrs.DefaultProviderRegistryHost,
Namespace: "hashicorp",
Type: "test",
}: {
ResourceTypes: map[string]providers.Schema{
"test_resource": {
Block: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"value": {
Type: cty.String,
},
},
},
},
},
},
},
},
},
StdOut: ` run "run_block"... pass
Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
+ create
Terraform will perform the following actions:
# test_resource.creating will be created
+ resource "test_resource" "creating" {
+ value = "Hello, world!"
}
Plan: 1 to add, 0 to change, 0 to destroy.
`,
},
"verbose_apply": {
Run: &moduletest.Run{
Name: "run_block",
Status: moduletest.Pass,
Config: &configs.TestRun{
Command: configs.ApplyTestCommand,
},
Verbose: &moduletest.Verbose{
Plan: &plans.Plan{}, // empty plan
State: states.BuildState(func(state *states.SyncState) {
state.SetResourceInstanceCurrent(
addrs.AbsResourceInstance{
Module: addrs.RootModuleInstance,
Resource: addrs.ResourceInstance{
Resource: addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "test_resource",
Name: "creating",
},
},
},
&states.ResourceInstanceObjectSrc{
AttrsJSON: []byte(`{"value":"foobar"}`),
},
addrs.AbsProviderConfig{
Module: addrs.RootModule,
Provider: addrs.Provider{
Hostname: addrs.DefaultProviderRegistryHost,
Namespace: "hashicorp",
Type: "test",
},
})
}),
Config: &configs.Config{},
Providers: map[addrs.Provider]providers.ProviderSchema{
addrs.Provider{
Hostname: addrs.DefaultProviderRegistryHost,
Namespace: "hashicorp",
Type: "test",
}: {
ResourceTypes: map[string]providers.Schema{
"test_resource": {
Block: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"value": {
Type: cty.String,
},
},
},
},
},
},
},
},
},
StdOut: ` run "run_block"... pass
# test_resource.creating:
resource "test_resource" "creating" {
value = "foobar"
}
`,
},
}
@ -2033,6 +2195,257 @@ func TestTestJSON_Run(t *testing.T) {
},
},
},
"verbose_plan": {
run: &moduletest.Run{
Name: "run_block",
Status: moduletest.Pass,
Config: &configs.TestRun{
Command: configs.PlanTestCommand,
},
Verbose: &moduletest.Verbose{
Plan: &plans.Plan{
Changes: &plans.Changes{
Resources: []*plans.ResourceInstanceChangeSrc{
{
Addr: addrs.AbsResourceInstance{
Module: addrs.RootModuleInstance,
Resource: addrs.ResourceInstance{
Resource: addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "test_resource",
Name: "creating",
},
},
},
PrevRunAddr: addrs.AbsResourceInstance{
Module: addrs.RootModuleInstance,
Resource: addrs.ResourceInstance{
Resource: addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "test_resource",
Name: "creating",
},
},
},
ProviderAddr: addrs.AbsProviderConfig{
Module: addrs.RootModule,
Provider: addrs.Provider{
Hostname: addrs.DefaultProviderRegistryHost,
Namespace: "hashicorp",
Type: "test",
},
},
ChangeSrc: plans.ChangeSrc{
Action: plans.Create,
After: dynamicValue(
t,
cty.ObjectVal(map[string]cty.Value{
"value": cty.StringVal("foobar"),
}),
cty.Object(map[string]cty.Type{
"value": cty.String,
})),
},
},
},
},
},
State: states.NewState(), // empty state
Config: &configs.Config{
Module: &configs.Module{
ProviderRequirements: &configs.RequiredProviders{},
},
},
Providers: map[addrs.Provider]providers.ProviderSchema{
addrs.Provider{
Hostname: addrs.DefaultProviderRegistryHost,
Namespace: "hashicorp",
Type: "test",
}: {
ResourceTypes: map[string]providers.Schema{
"test_resource": {
Block: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"value": {
Type: cty.String,
},
},
},
},
},
},
},
},
},
want: []map[string]interface{}{
{
"@level": "info",
"@message": " \"run_block\"... pass",
"@module": "terraform.ui",
"@testfile": "main.tftest",
"@testrun": "run_block",
"test_run": map[string]interface{}{
"path": "main.tftest",
"run": "run_block",
"status": "pass",
},
"type": "test_run",
},
{
"@level": "info",
"@message": "-verbose flag enabled, printing plan",
"@module": "terraform.ui",
"@testfile": "main.tftest",
"@testrun": "run_block",
"test_plan": map[string]interface{}{
"configuration": map[string]interface{}{
"root_module": map[string]interface{}{},
},
"errored": false,
"planned_values": map[string]interface{}{
"root_module": map[string]interface{}{
"resources": []interface{}{
map[string]interface{}{
"address": "test_resource.creating",
"mode": "managed",
"name": "creating",
"provider_name": "registry.terraform.io/hashicorp/test",
"schema_version": 0.0,
"sensitive_values": map[string]interface{}{},
"type": "test_resource",
"values": map[string]interface{}{
"value": "foobar",
},
},
},
},
},
"resource_changes": []interface{}{
map[string]interface{}{
"address": "test_resource.creating",
"change": map[string]interface{}{
"actions": []interface{}{"create"},
"after": map[string]interface{}{
"value": "foobar",
},
"after_sensitive": map[string]interface{}{},
"after_unknown": map[string]interface{}{},
"before": nil,
"before_sensitive": false,
},
"mode": "managed",
"name": "creating",
"provider_name": "registry.terraform.io/hashicorp/test",
"type": "test_resource",
},
},
},
"type": "test_plan",
},
},
},
"verbose_apply": {
run: &moduletest.Run{
Name: "run_block",
Status: moduletest.Pass,
Config: &configs.TestRun{
Command: configs.ApplyTestCommand,
},
Verbose: &moduletest.Verbose{
Plan: &plans.Plan{}, // empty plan
State: states.BuildState(func(state *states.SyncState) {
state.SetResourceInstanceCurrent(
addrs.AbsResourceInstance{
Module: addrs.RootModuleInstance,
Resource: addrs.ResourceInstance{
Resource: addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "test_resource",
Name: "creating",
},
},
},
&states.ResourceInstanceObjectSrc{
AttrsJSON: []byte(`{"value":"foobar"}`),
},
addrs.AbsProviderConfig{
Module: addrs.RootModule,
Provider: addrs.Provider{
Hostname: addrs.DefaultProviderRegistryHost,
Namespace: "hashicorp",
Type: "test",
},
})
}),
Config: &configs.Config{
Module: &configs.Module{},
},
Providers: map[addrs.Provider]providers.ProviderSchema{
addrs.Provider{
Hostname: addrs.DefaultProviderRegistryHost,
Namespace: "hashicorp",
Type: "test",
}: {
ResourceTypes: map[string]providers.Schema{
"test_resource": {
Block: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"value": {
Type: cty.String,
},
},
},
},
},
},
},
},
},
want: []map[string]interface{}{
{
"@level": "info",
"@message": " \"run_block\"... pass",
"@module": "terraform.ui",
"@testfile": "main.tftest",
"@testrun": "run_block",
"test_run": map[string]interface{}{
"path": "main.tftest",
"run": "run_block",
"status": "pass",
},
"type": "test_run",
},
{
"@level": "info",
"@message": "-verbose flag enabled, printing state",
"@module": "terraform.ui",
"@testfile": "main.tftest",
"@testrun": "run_block",
"test_state": map[string]interface{}{
"values": map[string]interface{}{
"root_module": map[string]interface{}{
"resources": []interface{}{
map[string]interface{}{
"address": "test_resource.creating",
"mode": "managed",
"name": "creating",
"provider_name": "registry.terraform.io/hashicorp/test",
"schema_version": 0.0,
"sensitive_values": map[string]interface{}{},
"type": "test_resource",
"values": map[string]interface{}{
"value": "foobar",
},
},
},
},
},
},
"type": "test_state",
},
},
},
}
for name, tc := range tcs {
t.Run(name, func(t *testing.T) {
@ -2042,7 +2455,17 @@ func TestTestJSON_Run(t *testing.T) {
file := &moduletest.File{Name: "main.tftest"}
view.Run(tc.run, file)
testJSONViewOutputEquals(t, done(t).All(), tc.want)
testJSONViewOutputEquals(t, done(t).All(), tc.want, cmp.FilterPath(func(path cmp.Path) bool {
return strings.Contains(path.Last().String(), "version") || strings.Contains(path.Last().String(), "timestamp")
}, cmp.Ignore()))
})
}
}
func dynamicValue(t *testing.T, value cty.Value, typ cty.Type) plans.DynamicValue {
d, err := plans.NewDynamicValue(value, typ)
if err != nil {
t.Fatalf("failed to create dynamic value: %s", err)
}
return d
}

View File

@ -85,11 +85,11 @@ func NewModuleInstaller(modsDir string, loader *configload.Loader, reg *registry
// If successful (the returned diagnostics contains no errors) then the
// first return value is the early configuration tree that was constructed by
// the installation process.
func (i *ModuleInstaller) InstallModules(ctx context.Context, rootDir string, upgrade bool, hooks ModuleInstallHooks) (*configs.Config, tfdiags.Diagnostics) {
func (i *ModuleInstaller) InstallModules(ctx context.Context, rootDir, testsDir string, upgrade bool, hooks ModuleInstallHooks) (*configs.Config, tfdiags.Diagnostics) {
log.Printf("[TRACE] ModuleInstaller: installing child modules for %s into %s", rootDir, i.modsDir)
var diags tfdiags.Diagnostics
rootMod, mDiags := i.loader.Parser().LoadConfigDirWithTests(rootDir, "tests")
rootMod, mDiags := i.loader.Parser().LoadConfigDirWithTests(rootDir, testsDir)
if rootMod == nil {
// We drop the diagnostics here because we only want to report module
// loading errors after checking the core version constraints, which we

View File

@ -46,7 +46,7 @@ func TestModuleInstaller(t *testing.T) {
loader, close := configload.NewLoaderForTests(t)
defer close()
inst := NewModuleInstaller(modulesDir, loader, nil)
_, diags := inst.InstallModules(context.Background(), ".", false, hooks)
_, diags := inst.InstallModules(context.Background(), ".", "tests", false, hooks)
assertNoDiagnostics(t, diags)
wantCalls := []testInstallHookCall{
@ -110,7 +110,7 @@ func TestModuleInstaller_error(t *testing.T) {
loader, close := configload.NewLoaderForTests(t)
defer close()
inst := NewModuleInstaller(modulesDir, loader, nil)
_, diags := inst.InstallModules(context.Background(), ".", false, hooks)
_, diags := inst.InstallModules(context.Background(), ".", "tests", false, hooks)
if !diags.HasErrors() {
t.Fatal("expected error")
@ -131,7 +131,7 @@ func TestModuleInstaller_emptyModuleName(t *testing.T) {
loader, close := configload.NewLoaderForTests(t)
defer close()
inst := NewModuleInstaller(modulesDir, loader, nil)
_, diags := inst.InstallModules(context.Background(), ".", false, hooks)
_, diags := inst.InstallModules(context.Background(), ".", "tests", false, hooks)
if !diags.HasErrors() {
t.Fatal("expected error")
@ -169,7 +169,7 @@ func TestModuleInstaller_packageEscapeError(t *testing.T) {
loader, close := configload.NewLoaderForTests(t)
defer close()
inst := NewModuleInstaller(modulesDir, loader, nil)
_, diags := inst.InstallModules(context.Background(), ".", false, hooks)
_, diags := inst.InstallModules(context.Background(), ".", "tests", false, hooks)
if !diags.HasErrors() {
t.Fatal("expected error")
@ -207,7 +207,7 @@ func TestModuleInstaller_explicitPackageBoundary(t *testing.T) {
loader, close := configload.NewLoaderForTests(t)
defer close()
inst := NewModuleInstaller(modulesDir, loader, nil)
_, diags := inst.InstallModules(context.Background(), ".", false, hooks)
_, diags := inst.InstallModules(context.Background(), ".", "tests", false, hooks)
if diags.HasErrors() {
t.Fatalf("unexpected errors\n%s", diags.Err().Error())
@ -230,7 +230,7 @@ func TestModuleInstaller_ExactMatchPrerelease(t *testing.T) {
loader, close := configload.NewLoaderForTests(t)
defer close()
inst := NewModuleInstaller(modulesDir, loader, registry.NewClient(nil, nil))
cfg, diags := inst.InstallModules(context.Background(), ".", false, hooks)
cfg, diags := inst.InstallModules(context.Background(), ".", "tests", false, hooks)
if diags.HasErrors() {
t.Fatalf("found unexpected errors: %s", diags.Err())
@ -257,7 +257,7 @@ func TestModuleInstaller_PartialMatchPrerelease(t *testing.T) {
loader, close := configload.NewLoaderForTests(t)
defer close()
inst := NewModuleInstaller(modulesDir, loader, registry.NewClient(nil, nil))
cfg, diags := inst.InstallModules(context.Background(), ".", false, hooks)
cfg, diags := inst.InstallModules(context.Background(), ".", "tests", false, hooks)
if diags.HasErrors() {
t.Fatalf("found unexpected errors: %s", diags.Err())
@ -280,7 +280,7 @@ func TestModuleInstaller_invalid_version_constraint_error(t *testing.T) {
loader, close := configload.NewLoaderForTests(t)
defer close()
inst := NewModuleInstaller(modulesDir, loader, nil)
_, diags := inst.InstallModules(context.Background(), ".", false, hooks)
_, diags := inst.InstallModules(context.Background(), ".", "tests", false, hooks)
if !diags.HasErrors() {
t.Fatal("expected error")
@ -306,7 +306,7 @@ func TestModuleInstaller_invalidVersionConstraintGetter(t *testing.T) {
loader, close := configload.NewLoaderForTests(t)
defer close()
inst := NewModuleInstaller(modulesDir, loader, nil)
_, diags := inst.InstallModules(context.Background(), ".", false, hooks)
_, diags := inst.InstallModules(context.Background(), ".", "tests", false, hooks)
if !diags.HasErrors() {
t.Fatal("expected error")
@ -332,7 +332,7 @@ func TestModuleInstaller_invalidVersionConstraintLocal(t *testing.T) {
loader, close := configload.NewLoaderForTests(t)
defer close()
inst := NewModuleInstaller(modulesDir, loader, nil)
_, diags := inst.InstallModules(context.Background(), ".", false, hooks)
_, diags := inst.InstallModules(context.Background(), ".", "tests", false, hooks)
if !diags.HasErrors() {
t.Fatal("expected error")
@ -358,7 +358,7 @@ func TestModuleInstaller_symlink(t *testing.T) {
loader, close := configload.NewLoaderForTests(t)
defer close()
inst := NewModuleInstaller(modulesDir, loader, nil)
_, diags := inst.InstallModules(context.Background(), ".", false, hooks)
_, diags := inst.InstallModules(context.Background(), ".", "tests", false, hooks)
assertNoDiagnostics(t, diags)
wantCalls := []testInstallHookCall{
@ -434,7 +434,7 @@ func TestLoaderInstallModules_registry(t *testing.T) {
loader, close := configload.NewLoaderForTests(t)
defer close()
inst := NewModuleInstaller(modulesDir, loader, registry.NewClient(nil, nil))
_, diags := inst.InstallModules(context.Background(), dir, false, hooks)
_, diags := inst.InstallModules(context.Background(), dir, "tests", false, hooks)
assertNoDiagnostics(t, diags)
v := version.Must(version.NewVersion("0.0.1"))
@ -597,7 +597,7 @@ func TestLoaderInstallModules_goGetter(t *testing.T) {
loader, close := configload.NewLoaderForTests(t)
defer close()
inst := NewModuleInstaller(modulesDir, loader, registry.NewClient(nil, nil))
_, diags := inst.InstallModules(context.Background(), dir, false, hooks)
_, diags := inst.InstallModules(context.Background(), dir, "tests", false, hooks)
assertNoDiagnostics(t, diags)
wantCalls := []testInstallHookCall{
@ -715,7 +715,7 @@ func TestModuleInstaller_fromTests(t *testing.T) {
loader, close := configload.NewLoaderForTests(t)
defer close()
inst := NewModuleInstaller(modulesDir, loader, nil)
_, diags := inst.InstallModules(context.Background(), ".", false, hooks)
_, diags := inst.InstallModules(context.Background(), ".", "tests", false, hooks)
assertNoDiagnostics(t, diags)
wantCalls := []testInstallHookCall{
@ -772,7 +772,7 @@ func TestLoadInstallModules_registryFromTest(t *testing.T) {
loader, close := configload.NewLoaderForTests(t)
defer close()
inst := NewModuleInstaller(modulesDir, loader, registry.NewClient(nil, nil))
_, diags := inst.InstallModules(context.Background(), dir, false, hooks)
_, diags := inst.InstallModules(context.Background(), dir, "tests", false, hooks)
assertNoDiagnostics(t, diags)
v := version.Must(version.NewVersion("0.0.1"))

View File

@ -31,7 +31,7 @@ import (
// As with NewLoaderForTests, a cleanup function is returned which must be
// called before the test completes in order to remove the temporary
// modules directory.
func LoadConfigForTests(t *testing.T, rootDir string) (*configs.Config, *configload.Loader, func(), tfdiags.Diagnostics) {
func LoadConfigForTests(t *testing.T, rootDir string, testsDir string) (*configs.Config, *configload.Loader, func(), tfdiags.Diagnostics) {
t.Helper()
var diags tfdiags.Diagnostics
@ -39,7 +39,7 @@ func LoadConfigForTests(t *testing.T, rootDir string) (*configs.Config, *configl
loader, cleanup := configload.NewLoaderForTests(t)
inst := NewModuleInstaller(loader.ModulesDir(), loader, registry.NewClient(nil, nil))
_, moreDiags := inst.InstallModules(context.Background(), rootDir, true, ModuleInstallHooksImpl{})
_, moreDiags := inst.InstallModules(context.Background(), rootDir, testsDir, true, ModuleInstallHooksImpl{})
diags = diags.Append(moreDiags)
if diags.HasErrors() {
cleanup()
@ -65,10 +65,10 @@ func LoadConfigForTests(t *testing.T, rootDir string) (*configs.Config, *configl
// This is useful for concisely writing tests that don't expect errors at
// all. For tests that expect errors and need to assert against them, use
// LoadConfigForTests instead.
func MustLoadConfigForTests(t *testing.T, rootDir string) (*configs.Config, *configload.Loader, func()) {
func MustLoadConfigForTests(t *testing.T, rootDir, testsDir string) (*configs.Config, *configload.Loader, func()) {
t.Helper()
config, loader, cleanup, diags := LoadConfigForTests(t, rootDir)
config, loader, cleanup, diags := LoadConfigForTests(t, rootDir, testsDir)
if diags.HasErrors() {
cleanup()
t.Fatal(diags.Err())

View File

@ -8,13 +8,14 @@ import (
"path/filepath"
"testing"
"github.com/zclconf/go-cty/cty"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/configs/configload"
"github.com/hashicorp/terraform/internal/configs/configschema"
"github.com/hashicorp/terraform/internal/initwd"
"github.com/hashicorp/terraform/internal/providers"
"github.com/hashicorp/terraform/internal/registry"
"github.com/zclconf/go-cty/cty"
)
func testAnalyzer(t *testing.T, fixtureName string) *Analyzer {
@ -24,7 +25,7 @@ func testAnalyzer(t *testing.T, fixtureName string) *Analyzer {
defer cleanup()
inst := initwd.NewModuleInstaller(loader.ModulesDir(), loader, registry.NewClient(nil, nil))
_, instDiags := inst.InstallModules(context.Background(), configDir, true, initwd.ModuleInstallHooksImpl{})
_, instDiags := inst.InstallModules(context.Background(), configDir, "tests", true, initwd.ModuleInstallHooksImpl{})
if instDiags.HasErrors() {
t.Fatalf("unexpected module installation errors: %s", instDiags.Err().Error())
}

View File

@ -7,18 +7,38 @@ import (
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/configs"
"github.com/hashicorp/terraform/internal/configs/configschema"
"github.com/hashicorp/terraform/internal/plans"
"github.com/hashicorp/terraform/internal/providers"
"github.com/hashicorp/terraform/internal/states"
"github.com/hashicorp/terraform/internal/tfdiags"
)
type Run struct {
Config *configs.TestRun
Verbose *Verbose
Name string
Index int
Status Status
Diagnostics tfdiags.Diagnostics
}
// Verbose is a utility struct that holds all the information required for a run
// to render the results verbosely.
//
// At the moment, this basically means printing out the plan. To do that we need
// all the information within this struct.
type Verbose struct {
Plan *plans.Plan
State *states.State
Config *configs.Config
Providers map[addrs.Provider]providers.ProviderSchema
Provisioners map[string]*configschema.Block
}
func (run *Run) GetTargets() ([]addrs.Targetable, tfdiags.Diagnostics) {
var diagnostics tfdiags.Diagnostics
var targets []addrs.Targetable

View File

@ -538,7 +538,7 @@ func loadRefactoringFixture(t *testing.T, dir string) (*configs.Config, instance
defer cleanup()
inst := initwd.NewModuleInstaller(loader.ModulesDir(), loader, registry.NewClient(nil, nil))
_, instDiags := inst.InstallModules(context.Background(), dir, true, initwd.ModuleInstallHooksImpl{})
_, instDiags := inst.InstallModules(context.Background(), dir, "tests", true, initwd.ModuleInstallHooksImpl{})
if instDiags.HasErrors() {
t.Fatal(instDiags.Err())
}

View File

@ -274,7 +274,7 @@ func testSession(t *testing.T, test testSessionTest) {
},
}
config, _, cleanup, configDiags := initwd.LoadConfigForTests(t, "testdata/config-fixture")
config, _, cleanup, configDiags := initwd.LoadConfigForTests(t, "testdata/config-fixture", "tests")
defer cleanup()
if configDiags.HasErrors() {
t.Fatalf("unexpected problems loading config: %s", configDiags.Err())

View File

@ -67,7 +67,7 @@ func testModuleWithSnapshot(t *testing.T, name string) (*configs.Config, *config
// sources only this ultimately just records all of the module paths
// in a JSON file so that we can load them below.
inst := initwd.NewModuleInstaller(loader.ModulesDir(), loader, registry.NewClient(nil, nil))
_, instDiags := inst.InstallModules(context.Background(), dir, true, initwd.ModuleInstallHooksImpl{})
_, instDiags := inst.InstallModules(context.Background(), dir, "tests", true, initwd.ModuleInstallHooksImpl{})
if instDiags.HasErrors() {
t.Fatal(instDiags.Err())
}
@ -124,7 +124,7 @@ func testModuleInline(t *testing.T, sources map[string]string) *configs.Config {
// sources only this ultimately just records all of the module paths
// in a JSON file so that we can load them below.
inst := initwd.NewModuleInstaller(loader.ModulesDir(), loader, registry.NewClient(nil, nil))
_, instDiags := inst.InstallModules(context.Background(), cfgPath, true, initwd.ModuleInstallHooksImpl{})
_, instDiags := inst.InstallModules(context.Background(), cfgPath, "tests", true, initwd.ModuleInstallHooksImpl{})
if instDiags.HasErrors() {
t.Fatal(instDiags.Err())
}