From 5f30efe8579c90372c73171f9c2f88cb64860680 Mon Sep 17 00:00:00 2001 From: Kristin Laemmert Date: Wed, 5 May 2021 14:13:20 -0400 Subject: [PATCH] command tests: plan and init (#28616) * command/init: add test for reconfigure * command/plan: adding tests * command/apply: tests * command: show and refresh tests --- command/apply_test.go | 129 ++++++++++++++++++++++++ command/init_test.go | 48 +++++++++ command/plan_test.go | 214 +++++++++++++++++++++++++++++++++++++--- command/refresh_test.go | 73 ++++++++++++++ command/show_test.go | 13 +-- 5 files changed, 455 insertions(+), 22 deletions(-) diff --git a/command/apply_test.go b/command/apply_test.go index f69aae5dc..ab6d2c6a9 100644 --- a/command/apply_test.go +++ b/command/apply_test.go @@ -28,6 +28,7 @@ import ( "github.com/hashicorp/terraform/states" "github.com/hashicorp/terraform/states/statemgr" "github.com/hashicorp/terraform/terraform" + "github.com/hashicorp/terraform/tfdiags" 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) { // Create a temporary working directory that is empty 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 // configuration in testdata/apply . This schema should be // assigned to a mock provider named "test". diff --git a/command/init_test.go b/command/init_test.go index c8aabaa90..99fdbe964 100644 --- a/command/init_test.go +++ b/command/init_test.go @@ -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) { // Create a temporary working directory that is empty td := tempDir(t) diff --git a/command/plan_test.go b/command/plan_test.go index 2a865a9ca..7d4611b1a 100644 --- a/command/plan_test.go +++ b/command/plan_test.go @@ -2,6 +2,8 @@ package command import ( "bytes" + "context" + "fmt" "io/ioutil" "os" "path" @@ -21,6 +23,7 @@ import ( "github.com/hashicorp/terraform/providers" "github.com/hashicorp/terraform/states" "github.com/hashicorp/terraform/terraform" + "github.com/hashicorp/terraform/tfdiags" ) func TestPlan(t *testing.T) { @@ -808,21 +811,37 @@ func TestPlan_detailedExitcode(t *testing.T) { defer os.RemoveAll(td) defer testChdir(t, td)() - p := planFixtureProvider() - view, done := testView(t) - c := &PlanCommand{ - Meta: Meta{ - testingOverrides: metaOverridesForProvider(p), - View: view, - }, - } + t.Run("return 1", func(t *testing.T) { + view, done := testView(t) + c := &PlanCommand{ + Meta: Meta{ + // Running plan without setting testingOverrides is similar to plan without init + 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"} - code := c.Run(args) - output := done(t) - if code != 2 { - t.Fatalf("bad: %d\n\n%s", code, output.Stderr()) - } + t.Run("return 2", func(t *testing.T) { + p := planFixtureProvider() + view, done := testView(t) + c := &PlanCommand{ + 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) { @@ -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) { 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 @@ -1182,6 +1349,25 @@ func planVarsFixtureProvider() *terraform.MockProvider { 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 = ` foo = "bar" ` diff --git a/command/refresh_test.go b/command/refresh_test.go index 407b177d4..e593e494f 100644 --- a/command/refresh_test.go +++ b/command/refresh_test.go @@ -22,6 +22,7 @@ import ( "github.com/hashicorp/terraform/states" "github.com/hashicorp/terraform/states/statefile" "github.com/hashicorp/terraform/states/statemgr" + "github.com/hashicorp/terraform/tfdiags" ) 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 // assigned to a mock provider named "test". func refreshFixtureSchema() *providers.GetProviderSchemaResponse { diff --git a/command/show_test.go b/command/show_test.go index f1f0fa0a8..a520e4f8e 100644 --- a/command/show_test.go +++ b/command/show_test.go @@ -41,11 +41,11 @@ func TestShow(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 - statePath := testStateFile(t, testState()) - stateDir := filepath.Dir(statePath) - defer os.RemoveAll(stateDir) - defer testChdir(t, stateDir)() + testStateFileDefault(t, testState()) ui := new(cli.MockUi) view, _ := testView(t) @@ -57,10 +57,7 @@ func TestShow_noArgs(t *testing.T) { }, } - // the statefile created by testStateFile is named state.tfstate - // so one arg is required - args := []string{"state.tfstate"} - if code := c.Run(args); code != 0 { + if code := c.Run([]string{}); code != 0 { t.Fatalf("bad: \n%s", ui.OutputWriter.String()) }