command tests: plan and init (#28616)

* command/init: add test for reconfigure

* command/plan: adding tests

* command/apply: tests

* command: show and refresh tests
This commit is contained in:
Kristin Laemmert 2021-05-05 14:13:20 -04:00 committed by GitHub
parent 1e3a60c7ac
commit 5f30efe857
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 455 additions and 22 deletions

View File

@ -28,6 +28,7 @@ import (
"github.com/hashicorp/terraform/states" "github.com/hashicorp/terraform/states"
"github.com/hashicorp/terraform/states/statemgr" "github.com/hashicorp/terraform/states/statemgr"
"github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/terraform"
"github.com/hashicorp/terraform/tfdiags"
tfversion "github.com/hashicorp/terraform/version" tfversion "github.com/hashicorp/terraform/version"
) )
@ -1023,6 +1024,56 @@ func TestApply_refresh(t *testing.T) {
} }
} }
func TestApply_refreshFalse(t *testing.T) {
// Create a temporary working directory that is empty
td := tempDir(t)
testCopyDir(t, testFixturePath("apply"), td)
defer os.RemoveAll(td)
defer testChdir(t, td)()
originalState := 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{
AttrsJSON: []byte(`{"ami":"bar"}`),
Status: states.ObjectReady,
},
addrs.AbsProviderConfig{
Provider: addrs.NewDefaultProvider("test"),
Module: addrs.RootModule,
},
)
})
statePath := testStateFile(t, originalState)
p := applyFixtureProvider()
view, done := testView(t)
c := &ApplyCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(p),
View: view,
},
}
args := []string{
"-state", statePath,
"-auto-approve",
"-refresh=false",
}
code := c.Run(args)
output := done(t)
if code != 0 {
t.Fatalf("bad: %d\n\n%s", code, output.Stderr())
}
if p.ReadResourceCalled {
t.Fatal("should not call ReadResource when refresh=false")
}
}
func TestApply_shutdown(t *testing.T) { func TestApply_shutdown(t *testing.T) {
// Create a temporary working directory that is empty // Create a temporary working directory that is empty
td := tempDir(t) td := tempDir(t)
@ -2102,6 +2153,84 @@ func TestApply_jsonGoldenReference(t *testing.T) {
} }
} }
func TestApply_warnings(t *testing.T) {
// Create a temporary working directory that is empty
td := tempDir(t)
testCopyDir(t, testFixturePath("apply"), td)
defer os.RemoveAll(td)
defer testChdir(t, td)()
p := testProvider()
p.GetProviderSchemaResponse = applyFixtureSchema()
p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse {
return providers.PlanResourceChangeResponse{
PlannedState: req.ProposedNewState,
Diagnostics: tfdiags.Diagnostics{
tfdiags.SimpleWarning("warning 1"),
tfdiags.SimpleWarning("warning 2"),
},
}
}
p.ApplyResourceChangeFn = func(req providers.ApplyResourceChangeRequest) providers.ApplyResourceChangeResponse {
return providers.ApplyResourceChangeResponse{
NewState: cty.UnknownAsNull(req.PlannedState),
}
}
t.Run("full warnings", func(t *testing.T) {
view, done := testView(t)
c := &ApplyCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(p),
View: view,
},
}
args := []string{"-auto-approve"}
code := c.Run(args)
output := done(t)
if code != 0 {
t.Fatalf("bad: %d\n\n%s", code, output.Stderr())
}
wantWarnings := []string{
"warning 1",
"warning 2",
}
for _, want := range wantWarnings {
if !strings.Contains(output.Stdout(), want) {
t.Errorf("missing warning %s", want)
}
}
})
t.Run("compact warnings", func(t *testing.T) {
view, done := testView(t)
c := &ApplyCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(p),
View: view,
},
}
code := c.Run([]string{"-auto-approve", "-compact-warnings"})
output := done(t)
if code != 0 {
t.Fatalf("bad: %d\n\n%s", code, output.Stderr())
}
// the output should contain 2 warnings and a message about -compact-warnings
wantWarnings := []string{
"warning 1",
"warning 2",
"To see the full warning notes, run Terraform without -compact-warnings.",
}
for _, want := range wantWarnings {
if !strings.Contains(output.Stdout(), want) {
t.Errorf("missing warning %s", want)
}
}
})
}
// applyFixtureSchema returns a schema suitable for processing the // applyFixtureSchema returns a schema suitable for processing the
// configuration in testdata/apply . This schema should be // configuration in testdata/apply . This schema should be
// assigned to a mock provider named "test". // assigned to a mock provider named "test".

View File

@ -485,6 +485,54 @@ func TestInit_backendConfigFilePowershellConfusion(t *testing.T) {
} }
} }
func TestInit_backendReconfigure(t *testing.T) {
// Create a temporary working directory that is empty
td := tempDir(t)
testCopyDir(t, testFixturePath("init-backend"), td)
defer os.RemoveAll(td)
defer testChdir(t, td)()
providerSource, close := newMockProviderSource(t, map[string][]string{
"hashicorp/test": {"1.2.3"},
})
defer close()
ui := new(cli.MockUi)
view, _ := testView(t)
c := &InitCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(testProvider()),
ProviderSource: providerSource,
Ui: ui,
View: view,
},
}
// create some state, so the backend has something to migrate.
f, err := os.Create("foo") // this is the path" in the backend config
if err != nil {
t.Fatalf("err: %s", err)
}
err = writeStateForTesting(testState(), f)
f.Close()
if err != nil {
t.Fatalf("err: %s", err)
}
args := []string{}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
}
// now run init again, changing the path.
// The -reconfigure flag prevents init from migrating
// Without -reconfigure, the test fails since the backend asks for input on migrating state
args = []string{"-reconfigure", "-backend-config", "path=changed"}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
}
}
func TestInit_backendConfigFileChange(t *testing.T) { func TestInit_backendConfigFileChange(t *testing.T) {
// Create a temporary working directory that is empty // Create a temporary working directory that is empty
td := tempDir(t) td := tempDir(t)

View File

@ -2,6 +2,8 @@ package command
import ( import (
"bytes" "bytes"
"context"
"fmt"
"io/ioutil" "io/ioutil"
"os" "os"
"path" "path"
@ -21,6 +23,7 @@ import (
"github.com/hashicorp/terraform/providers" "github.com/hashicorp/terraform/providers"
"github.com/hashicorp/terraform/states" "github.com/hashicorp/terraform/states"
"github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/terraform"
"github.com/hashicorp/terraform/tfdiags"
) )
func TestPlan(t *testing.T) { func TestPlan(t *testing.T) {
@ -808,21 +811,37 @@ func TestPlan_detailedExitcode(t *testing.T) {
defer os.RemoveAll(td) defer os.RemoveAll(td)
defer testChdir(t, td)() defer testChdir(t, td)()
p := planFixtureProvider() t.Run("return 1", func(t *testing.T) {
view, done := testView(t) view, done := testView(t)
c := &PlanCommand{ c := &PlanCommand{
Meta: Meta{ Meta: Meta{
testingOverrides: metaOverridesForProvider(p), // Running plan without setting testingOverrides is similar to plan without init
View: view, View: view,
}, },
} }
code := c.Run([]string{"-detailed-exitcode"})
output := done(t)
if code != 1 {
t.Fatalf("bad: %d\n\n%s", code, output.Stderr())
}
})
args := []string{"-detailed-exitcode"} t.Run("return 2", func(t *testing.T) {
code := c.Run(args) p := planFixtureProvider()
output := done(t) view, done := testView(t)
if code != 2 { c := &PlanCommand{
t.Fatalf("bad: %d\n\n%s", code, output.Stderr()) Meta: Meta{
} testingOverrides: metaOverridesForProvider(p),
View: view,
},
}
code := c.Run([]string{"-detailed-exitcode"})
output := done(t)
if code != 2 {
t.Fatalf("bad: %d\n\n%s", code, output.Stderr())
}
})
} }
func TestPlan_detailedExitcode_emptyDiff(t *testing.T) { func TestPlan_detailedExitcode_emptyDiff(t *testing.T) {
@ -1102,7 +1121,155 @@ func TestPlan_replace(t *testing.T) {
if got, want := stdout, "test_instance.a will be replaced, as requested"; !strings.Contains(got, want) { if got, want := stdout, "test_instance.a will be replaced, as requested"; !strings.Contains(got, want) {
t.Errorf("missing replace explanation\ngot output:\n%s\n\nwant substring: %s", got, want) t.Errorf("missing replace explanation\ngot output:\n%s\n\nwant substring: %s", got, want)
} }
}
// Verify that the parallelism flag allows no more than the desired number of
// concurrent calls to PlanResourceChange.
func TestPlan_parallelism(t *testing.T) {
// Create a temporary working directory that is empty
td := tempDir(t)
testCopyDir(t, testFixturePath("parallelism"), td)
defer os.RemoveAll(td)
defer testChdir(t, td)()
par := 4
// started is a semaphore that we use to ensure that we never have more
// than "par" plan operations happening concurrently
started := make(chan struct{}, par)
// beginCtx is used as a starting gate to hold back PlanResourceChange
// calls until we reach the desired concurrency. The cancel func "begin" is
// called once we reach the desired concurrency, allowing all apply calls
// to proceed in unison.
beginCtx, begin := context.WithCancel(context.Background())
// Since our mock provider has its own mutex preventing concurrent calls
// to ApplyResourceChange, we need to use a number of separate providers
// here. They will all have the same mock implementation function assigned
// but crucially they will each have their own mutex.
providerFactories := map[addrs.Provider]providers.Factory{}
for i := 0; i < 10; i++ {
name := fmt.Sprintf("test%d", i)
provider := &terraform.MockProvider{}
provider.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{
ResourceTypes: map[string]providers.Schema{
name + "_instance": {Block: &configschema.Block{}},
},
}
provider.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse {
// If we ever have more than our intended parallelism number of
// plan operations running concurrently, the semaphore will fail.
select {
case started <- struct{}{}:
defer func() {
<-started
}()
default:
t.Fatal("too many concurrent apply operations")
}
// If we never reach our intended parallelism, the context will
// never be canceled and the test will time out.
if len(started) >= par {
begin()
}
<-beginCtx.Done()
// do some "work"
// Not required for correctness, but makes it easier to spot a
// failure when there is more overlap.
time.Sleep(10 * time.Millisecond)
return providers.PlanResourceChangeResponse{
PlannedState: req.ProposedNewState,
}
}
providerFactories[addrs.NewDefaultProvider(name)] = providers.FactoryFixed(provider)
}
testingOverrides := &testingOverrides{
Providers: providerFactories,
}
view, done := testView(t)
c := &PlanCommand{
Meta: Meta{
testingOverrides: testingOverrides,
View: view,
},
}
args := []string{
fmt.Sprintf("-parallelism=%d", par),
}
res := c.Run(args)
output := done(t)
if res != 0 {
t.Fatal(output.Stdout())
}
}
func TestPlan_warnings(t *testing.T) {
td := tempDir(t)
testCopyDir(t, testFixturePath("plan"), td)
defer os.RemoveAll(td)
defer testChdir(t, td)()
t.Run("full warnings", func(t *testing.T) {
p := planWarningsFixtureProvider()
view, done := testView(t)
c := &PlanCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(p),
View: view,
},
}
code := c.Run([]string{})
output := done(t)
if code != 0 {
t.Fatalf("bad: %d\n\n%s", code, output.Stderr())
}
// the output should contain 3 warnings (returned by planWarningsFixtureProvider())
wantWarnings := []string{
"warning 1",
"warning 2",
"warning 3",
}
for _, want := range wantWarnings {
if !strings.Contains(output.Stdout(), want) {
t.Errorf("missing warning %s", want)
}
}
})
t.Run("compact warnings", func(t *testing.T) {
p := planWarningsFixtureProvider()
view, done := testView(t)
c := &PlanCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(p),
View: view,
},
}
code := c.Run([]string{"-compact-warnings"})
output := done(t)
if code != 0 {
t.Fatalf("bad: %d\n\n%s", code, output.Stderr())
}
// the output should contain 3 warnings (returned by planWarningsFixtureProvider())
// and the message that plan was run with -compact-warnings
wantWarnings := []string{
"warning 1",
"warning 2",
"warning 3",
"To see the full warning notes, run Terraform without -compact-warnings.",
}
for _, want := range wantWarnings {
if !strings.Contains(output.Stdout(), want) {
t.Errorf("missing warning %s", want)
}
}
})
} }
// planFixtureSchema returns a schema suitable for processing the // planFixtureSchema returns a schema suitable for processing the
@ -1182,6 +1349,25 @@ func planVarsFixtureProvider() *terraform.MockProvider {
return p return p
} }
// planFixtureProvider returns a mock provider that is configured for basic
// operation with the configuration in testdata/plan. This mock has
// GetSchemaResponse and PlanResourceChangeFn populated, returning 3 warnings.
func planWarningsFixtureProvider() *terraform.MockProvider {
p := testProvider()
p.GetProviderSchemaResponse = planFixtureSchema()
p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse {
return providers.PlanResourceChangeResponse{
Diagnostics: tfdiags.Diagnostics{
tfdiags.SimpleWarning("warning 1"),
tfdiags.SimpleWarning("warning 2"),
tfdiags.SimpleWarning("warning 3"),
},
PlannedState: req.ProposedNewState,
}
}
return p
}
const planVarFile = ` const planVarFile = `
foo = "bar" foo = "bar"
` `

View File

@ -22,6 +22,7 @@ import (
"github.com/hashicorp/terraform/states" "github.com/hashicorp/terraform/states"
"github.com/hashicorp/terraform/states/statefile" "github.com/hashicorp/terraform/states/statefile"
"github.com/hashicorp/terraform/states/statemgr" "github.com/hashicorp/terraform/states/statemgr"
"github.com/hashicorp/terraform/tfdiags"
) )
var equateEmpty = cmpopts.EquateEmpty() var equateEmpty = cmpopts.EquateEmpty()
@ -859,6 +860,78 @@ func TestRefresh_targetFlagsDiags(t *testing.T) {
} }
} }
func TestRefresh_warnings(t *testing.T) {
// Create a temporary working directory that is empty
td := tempDir(t)
testCopyDir(t, testFixturePath("apply"), td)
defer os.RemoveAll(td)
defer testChdir(t, td)()
p := testProvider()
p.GetProviderSchemaResponse = refreshFixtureSchema()
p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse {
return providers.PlanResourceChangeResponse{
PlannedState: req.ProposedNewState,
Diagnostics: tfdiags.Diagnostics{
tfdiags.SimpleWarning("warning 1"),
tfdiags.SimpleWarning("warning 2"),
},
}
}
t.Run("full warnings", func(t *testing.T) {
view, done := testView(t)
c := &RefreshCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(p),
View: view,
},
}
code := c.Run([]string{})
output := done(t)
if code != 0 {
t.Fatalf("bad: %d\n\n%s", code, output.Stderr())
}
wantWarnings := []string{
"warning 1",
"warning 2",
}
for _, want := range wantWarnings {
if !strings.Contains(output.Stdout(), want) {
t.Errorf("missing warning %s", want)
}
}
})
t.Run("compact warnings", func(t *testing.T) {
view, done := testView(t)
c := &RefreshCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(p),
View: view,
},
}
code := c.Run([]string{"-compact-warnings"})
output := done(t)
if code != 0 {
t.Fatalf("bad: %d\n\n%s", code, output.Stderr())
}
// the output should contain 2 warnings and a message about -compact-warnings
wantWarnings := []string{
"warning 1",
"warning 2",
"To see the full warning notes, run Terraform without -compact-warnings.",
}
for _, want := range wantWarnings {
if !strings.Contains(output.Stdout(), want) {
t.Errorf("missing warning %s", want)
}
}
})
}
// configuration in testdata/refresh . This schema should be // configuration in testdata/refresh . This schema should be
// assigned to a mock provider named "test". // assigned to a mock provider named "test".
func refreshFixtureSchema() *providers.GetProviderSchemaResponse { func refreshFixtureSchema() *providers.GetProviderSchemaResponse {

View File

@ -41,11 +41,11 @@ func TestShow(t *testing.T) {
} }
func TestShow_noArgs(t *testing.T) { func TestShow_noArgs(t *testing.T) {
// Get a temp cwd
tmp, cwd := testCwd(t)
defer testFixCwd(t, tmp, cwd)
// Create the default state // Create the default state
statePath := testStateFile(t, testState()) testStateFileDefault(t, testState())
stateDir := filepath.Dir(statePath)
defer os.RemoveAll(stateDir)
defer testChdir(t, stateDir)()
ui := new(cli.MockUi) ui := new(cli.MockUi)
view, _ := testView(t) view, _ := testView(t)
@ -57,10 +57,7 @@ func TestShow_noArgs(t *testing.T) {
}, },
} }
// the statefile created by testStateFile is named state.tfstate if code := c.Run([]string{}); code != 0 {
// so one arg is required
args := []string{"state.tfstate"}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: \n%s", ui.OutputWriter.String()) t.Fatalf("bad: \n%s", ui.OutputWriter.String())
} }