From 16a34fe47ffc4149abb4ae03dfedeea19df1f92e Mon Sep 17 00:00:00 2001 From: Samsondeen <40821565+dsa0x@users.noreply.github.com> Date: Wed, 5 Feb 2025 09:48:05 +0100 Subject: [PATCH] Accept CLI option for the number of parallel ops in a test run's plan/apply (#36323) --- .../ENHANCEMENTS-20250123-101838.yaml | 5 + go.mod | 6 +- go.sum | 16 ++-- .../backend/remote-state/kubernetes/go.mod | 2 +- .../backend/remote-state/kubernetes/go.sum | 4 +- internal/cloud/test.go | 5 + internal/cloud/test_test.go | 75 +++++++++++++++ internal/cloud/tfe_client_mock.go | 34 ++++++- internal/command/arguments/test.go | 9 ++ internal/command/arguments/test_test.go | 91 ++++++++++++------- internal/command/test.go | 35 ++++--- internal/command/test_test.go | 8 ++ website/docs/cli/commands/test.mdx | 2 + 13 files changed, 229 insertions(+), 63 deletions(-) create mode 100644 .changes/unreleased/ENHANCEMENTS-20250123-101838.yaml diff --git a/.changes/unreleased/ENHANCEMENTS-20250123-101838.yaml b/.changes/unreleased/ENHANCEMENTS-20250123-101838.yaml new file mode 100644 index 0000000000..f60563616d --- /dev/null +++ b/.changes/unreleased/ENHANCEMENTS-20250123-101838.yaml @@ -0,0 +1,5 @@ +kind: ENHANCEMENTS +body: Terraform Test command now accepts a -parallelism=n option, which sets the number of parallel operations in a test run's plan/apply operation. +time: 2025-01-23T10:18:38.979866+01:00 +custom: + Issue: "34237" diff --git a/go.mod b/go.mod index db39a54f5c..005a998b6e 100644 --- a/go.mod +++ b/go.mod @@ -28,12 +28,12 @@ require ( github.com/hashicorp/go-plugin v1.6.0 github.com/hashicorp/go-retryablehttp v0.7.7 github.com/hashicorp/go-slug v0.16.3 - github.com/hashicorp/go-tfe v1.70.0 + github.com/hashicorp/go-tfe v1.74.1 github.com/hashicorp/go-uuid v1.0.3 github.com/hashicorp/go-version v1.7.0 github.com/hashicorp/hcl v1.0.0 github.com/hashicorp/hcl/v2 v2.23.1-0.20250203194505-ba0759438da2 - github.com/hashicorp/jsonapi v1.3.1 + github.com/hashicorp/jsonapi v1.3.2 github.com/hashicorp/terraform-registry-address v0.2.3 github.com/hashicorp/terraform-svchost v0.1.1 github.com/hashicorp/terraform/internal/backend/remote-state/azure v0.0.0-00010101000000-000000000000 @@ -256,7 +256,7 @@ require ( go.opentelemetry.io/proto/otlp v1.0.0 // indirect golang.org/x/exp/typeparams v0.0.0-20231108232855-2478ac86f678 // indirect golang.org/x/sync v0.10.0 // indirect - golang.org/x/time v0.7.0 // indirect + golang.org/x/time v0.9.0 // indirect golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect google.golang.org/api v0.126.0 // indirect google.golang.org/appengine v1.6.8 // indirect diff --git a/go.sum b/go.sum index 9cdc192534..b01ca1eb77 100644 --- a/go.sum +++ b/go.sum @@ -1123,8 +1123,8 @@ github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerX github.com/hashicorp/go-sockaddr v1.0.2 h1:ztczhD1jLxIRjVejw8gFomI1BQZOe2WoVOu0SyteCQc= github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A= github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= -github.com/hashicorp/go-tfe v1.70.0 h1:R5a9Z+jdVz6eRWtSLsl1nw+5Qe/swunZcJgeKK5NQtQ= -github.com/hashicorp/go-tfe v1.70.0/go.mod h1:2rOcdTxXwbWm0W7dCKjC3Ec8KQ+HhW165GiurXNshc4= +github.com/hashicorp/go-tfe v1.74.1 h1:I/8fOwSYox17IZV7SULIQH0ZRPNL2g/biW6hHWnOTVY= +github.com/hashicorp/go-tfe v1.74.1/go.mod h1:kGHWMZ3HHjitgqON8nBZ4kPVJ3cLbzM4JMgmNVMs9aQ= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= @@ -1141,8 +1141,8 @@ github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/hcl/v2 v2.23.1-0.20250203194505-ba0759438da2 h1:JP8y98OtHTujECs4s/HxlKc5yql/RlC99Dt1Iz4R+lM= github.com/hashicorp/hcl/v2 v2.23.1-0.20250203194505-ba0759438da2/go.mod h1:k+HgkLpoWu9OS81sy4j1XKDXaWm/rLysG33v5ibdDnc= -github.com/hashicorp/jsonapi v1.3.1 h1:GtPvnmcWgYwCuDGvYT5VZBHcUyFdq9lSyCzDjn1DdPo= -github.com/hashicorp/jsonapi v1.3.1/go.mod h1:kWfdn49yCjQvbpnvY1dxxAuAFzISwrrMDQOcu6NsFoM= +github.com/hashicorp/jsonapi v1.3.2 h1:gP3fX2ZT7qXi+PbwieptzkspIohO2kCSiBUvUTBAbMs= +github.com/hashicorp/jsonapi v1.3.2/go.mod h1:kWfdn49yCjQvbpnvY1dxxAuAFzISwrrMDQOcu6NsFoM= github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= github.com/hashicorp/mdns v1.0.4/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc= @@ -1460,8 +1460,8 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.563/go.mod h1:7sCQWVkxcsR38nffDW057DRGk8mUjK1Ing/EFOK8s8Y= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.588 h1:DYtBXB7sVc3EOW5horg8j55cLZynhsLYhHrvQ/jXKKM= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.588/go.mod h1:7sCQWVkxcsR38nffDW057DRGk8mUjK1Ing/EFOK8s8Y= @@ -1917,8 +1917,8 @@ golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxb golang.org/x/time v0.0.0-20220922220347-f3bd1da661af/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.1.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= -golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= +golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/internal/backend/remote-state/kubernetes/go.mod b/internal/backend/remote-state/kubernetes/go.mod index c82c77d67c..10449c5fb7 100644 --- a/internal/backend/remote-state/kubernetes/go.mod +++ b/internal/backend/remote-state/kubernetes/go.mod @@ -64,7 +64,7 @@ require ( golang.org/x/sys v0.29.0 // indirect golang.org/x/term v0.28.0 // indirect golang.org/x/text v0.21.0 // indirect - golang.org/x/time v0.7.0 // indirect + golang.org/x/time v0.9.0 // indirect golang.org/x/tools v0.25.0 // indirect google.golang.org/protobuf v1.34.2 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect diff --git a/internal/backend/remote-state/kubernetes/go.sum b/internal/backend/remote-state/kubernetes/go.sum index 7da99cb5c9..c6af694f1c 100644 --- a/internal/backend/remote-state/kubernetes/go.sum +++ b/internal/backend/remote-state/kubernetes/go.sum @@ -492,8 +492,8 @@ golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= -golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= +golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= diff --git a/internal/cloud/test.go b/internal/cloud/test.go index e81d07494f..3f97be0f8d 100644 --- a/internal/cloud/test.go +++ b/internal/cloud/test.go @@ -82,6 +82,10 @@ type TestSuiteRunner struct { // Verbose tells the runner to print out plan files during each test run. Verbose bool + // OperationParallelism is the limit Terraform places on total parallel operations + // during the plan or apply command within a single test run. + OperationParallelism int + // Filters restricts which test files will be executed. Filters []string @@ -204,6 +208,7 @@ func (runner *TestSuiteRunner) Test() (moduletest.Status, tfdiags.Diagnostics) { Filters: runner.Filters, TestDirectory: tfe.String(runner.TestingDirectory), Verbose: tfe.Bool(runner.Verbose), + Parallelism: tfe.Int(runner.OperationParallelism), Variables: func() []*tfe.RunVariable { runVariables := make([]*tfe.RunVariable, 0, len(variables)) for name, value := range variables { diff --git a/internal/cloud/test_test.go b/internal/cloud/test_test.go index 2b1dbee164..e33cec14df 100644 --- a/internal/cloud/test_test.go +++ b/internal/cloud/test_test.go @@ -103,6 +103,81 @@ Success! 2 passed, 0 failed. } } +func TestTest_Parallelism(t *testing.T) { + + streams, _ := terminal.StreamsForTesting(t) + view := views.NewTest(arguments.ViewHuman, views.NewView(streams)) + + colorize := mockColorize() + colorize.Disable = true + + mock := NewMockClient() + client := &tfe.Client{ + ConfigurationVersions: mock.ConfigurationVersions, + Organizations: mock.Organizations, + RegistryModules: mock.RegistryModules, + TestRuns: mock.TestRuns, + } + + if _, err := client.Organizations.Create(context.Background(), tfe.OrganizationCreateOptions{ + Name: tfe.String("organisation"), + }); err != nil { + t.Fatalf("failed to create organisation: %v", err) + } + + if _, err := client.RegistryModules.Create(context.Background(), "organisation", tfe.RegistryModuleCreateOptions{ + Name: tfe.String("name"), + Provider: tfe.String("provider"), + RegistryName: "app.terraform.io", + Namespace: "organisation", + }); err != nil { + t.Fatalf("failed to create registry module: %v", err) + } + + runner := TestSuiteRunner{ + // Configuration data. + ConfigDirectory: "testdata/test", + TestingDirectory: "tests", + Config: nil, // We don't need this for this test. + Source: "app.terraform.io/organisation/name/provider", + + // Cancellation controls, we won't be doing any cancellations in this + // test. + Stopped: false, + Cancelled: false, + StoppedCtx: context.Background(), + CancelledCtx: context.Background(), + + // Test Options, empty for this test. + GlobalVariables: nil, + Verbose: false, + OperationParallelism: 4, + Filters: nil, + + // Outputs + Renderer: &jsonformat.Renderer{ + Streams: streams, + Colorize: colorize, + RunningInAutomation: false, + }, + View: view, + Streams: streams, + + // Networking + Services: nil, // Don't need this when the client is overridden. + clientOverride: client, + } + + _, diags := runner.Test() + if len(diags) > 0 { + t.Errorf("found diags and expected none: %s", diags.ErrWithWarnings()) + } + + if mock.TestRuns.parallelism != 4 { + t.Errorf("expected parallelism to be 4 but was %d", mock.TestRuns.parallelism) + } +} + func TestTest_JSON(t *testing.T) { streams, done := terminal.StreamsForTesting(t) diff --git a/internal/cloud/tfe_client_mock.go b/internal/cloud/tfe_client_mock.go index 53bfc374cf..5a5ccaf33b 100644 --- a/internal/cloud/tfe_client_mock.go +++ b/internal/cloud/tfe_client_mock.go @@ -1673,9 +1673,10 @@ type MockTestRuns struct { client *MockClient // TestRuns and modules keep track of our tfe.TestRun objects. - TestRuns map[string]*tfe.TestRun - modules map[string][]*tfe.TestRun - logs map[string]string + TestRuns map[string]*tfe.TestRun + modules map[string][]*tfe.TestRun + logs map[string]string + parallelism int // delayedCancel allows a mock test run to cancel an operation instead of // completing an operation. It's used @@ -1767,6 +1768,9 @@ func (m *MockTestRuns) Create(ctx context.Context, options tfe.TestRunCreateOpti "test.log", ) m.modules[tr.RegistryModule.ID] = append(m.modules[tr.RegistryModule.ID], tr) + if options.Parallelism != nil { + m.parallelism = *options.Parallelism + } return tr, nil } @@ -2170,6 +2174,30 @@ func (m *MockWorkspaces) UpdateByID(ctx context.Context, workspaceID string, opt return w, nil } +func (m *MockWorkspaces) ListEffectiveTagBindings(ctx context.Context, workspaceID string) ([]*tfe.EffectiveTagBinding, error) { + w, ok := m.workspaceIDs[workspaceID] + if !ok { + return nil, tfe.ErrResourceNotFound + } + var effectiveTagBindings []*tfe.EffectiveTagBinding + for _, tb := range w.TagBindings { + effectiveTagBindings = append(effectiveTagBindings, &tfe.EffectiveTagBinding{ + Key: tb.Key, + Value: tb.Value, + }) + } + return effectiveTagBindings, nil +} + +func (m *MockWorkspaces) DeleteAllTagBindings(ctx context.Context, workspaceID string) error { + w, ok := m.workspaceIDs[workspaceID] + if !ok { + return tfe.ErrResourceNotFound + } + w.TagBindings = nil + return nil +} + func updateMockWorkspaceAttributes(w *tfe.Workspace, options tfe.WorkspaceUpdateOptions) error { // for TestCloud_setUnavailableTerraformVersion if w.Name == "unavailable-terraform-version" && options.TerraformVersion != nil { diff --git a/internal/command/arguments/test.go b/internal/command/arguments/test.go index 848ac81915..bd5d3ec213 100644 --- a/internal/command/arguments/test.go +++ b/internal/command/arguments/test.go @@ -18,6 +18,10 @@ type Test struct { // will be executed. Filter []string + // OperationParallelism is the limit Terraform places on total parallel operations + // during the plan or apply command within a single test run. + OperationParallelism int + // 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 @@ -55,6 +59,7 @@ func ParseTest(args []string) (*Test, tfdiags.Diagnostics) { cmdFlags.BoolVar(&jsonOutput, "json", false, "json") cmdFlags.StringVar(&test.JUnitXMLFile, "junit-xml", "", "junit-xml") cmdFlags.BoolVar(&test.Verbose, "verbose", false, "verbose") + cmdFlags.IntVar(&test.OperationParallelism, "parallelism", DefaultParallelism, "parallelism") // TODO: Finalise the name of this flag. cmdFlags.StringVar(&test.CloudRunSource, "cloud-run", "", "cloud-run") @@ -73,6 +78,10 @@ func ParseTest(args []string) (*Test, tfdiags.Diagnostics) { "The -junit-xml option is currently not compatible with remote test execution via the -cloud-run flag. If you are interested in JUnit XML output for remotely-executed tests please open an issue in GitHub.")) } + if test.OperationParallelism < 1 { + test.OperationParallelism = DefaultParallelism + } + switch { case jsonOutput: test.ViewType = ViewJSON diff --git a/internal/command/arguments/test_test.go b/internal/command/arguments/test_test.go index be02f149c7..018c961721 100644 --- a/internal/command/arguments/test_test.go +++ b/internal/command/arguments/test_test.go @@ -72,60 +72,88 @@ func TestParseTest(t *testing.T) { "defaults": { args: nil, want: &Test{ - Filter: nil, - TestDirectory: "tests", - ViewType: ViewHuman, - Vars: &Vars{}, + Filter: nil, + TestDirectory: "tests", + ViewType: ViewHuman, + Vars: &Vars{}, + OperationParallelism: 10, }, wantDiags: nil, }, "with-filters": { args: []string{"-filter=one.tftest.hcl", "-filter=two.tftest.hcl"}, want: &Test{ - Filter: []string{"one.tftest.hcl", "two.tftest.hcl"}, - TestDirectory: "tests", - ViewType: ViewHuman, - Vars: &Vars{}, + Filter: []string{"one.tftest.hcl", "two.tftest.hcl"}, + TestDirectory: "tests", + ViewType: ViewHuman, + Vars: &Vars{}, + OperationParallelism: 10, }, wantDiags: nil, }, "json": { args: []string{"-json"}, want: &Test{ - Filter: nil, - TestDirectory: "tests", - ViewType: ViewJSON, - Vars: &Vars{}, + Filter: nil, + TestDirectory: "tests", + ViewType: ViewJSON, + Vars: &Vars{}, + OperationParallelism: 10, }, wantDiags: nil, }, "test-directory": { args: []string{"-test-directory=other"}, want: &Test{ - Filter: nil, - TestDirectory: "other", - ViewType: ViewHuman, - Vars: &Vars{}, + Filter: nil, + TestDirectory: "other", + ViewType: ViewHuman, + Vars: &Vars{}, + OperationParallelism: 10, }, wantDiags: nil, }, "verbose": { args: []string{"-verbose"}, want: &Test{ - Filter: nil, - TestDirectory: "tests", - ViewType: ViewHuman, - Verbose: true, - Vars: &Vars{}, + Filter: nil, + TestDirectory: "tests", + ViewType: ViewHuman, + Verbose: true, + Vars: &Vars{}, + OperationParallelism: 10, }, }, + "with-parallelism-set": { + args: []string{"-parallelism=5"}, + want: &Test{ + Filter: nil, + TestDirectory: "tests", + ViewType: ViewHuman, + Vars: &Vars{}, + OperationParallelism: 5, + }, + wantDiags: nil, + }, + "with-parallelism-0": { + args: []string{"-parallelism=0"}, + want: &Test{ + Filter: nil, + TestDirectory: "tests", + ViewType: ViewHuman, + Vars: &Vars{}, + OperationParallelism: 10, + }, + wantDiags: nil, + }, "unknown flag": { args: []string{"-boop"}, want: &Test{ - Filter: nil, - TestDirectory: "tests", - ViewType: ViewHuman, - Vars: &Vars{}, + Filter: nil, + TestDirectory: "tests", + ViewType: ViewHuman, + Vars: &Vars{}, + OperationParallelism: 10, }, wantDiags: tfdiags.Diagnostics{ tfdiags.Sourceless( @@ -138,12 +166,13 @@ func TestParseTest(t *testing.T) { "incompatible flags: -junit-xml and -cloud-run": { args: []string{"-junit-xml=./output.xml", "-cloud-run=foobar"}, want: &Test{ - CloudRunSource: "foobar", - JUnitXMLFile: "./output.xml", - Filter: nil, - TestDirectory: "tests", - ViewType: ViewHuman, - Vars: &Vars{}, + CloudRunSource: "foobar", + JUnitXMLFile: "./output.xml", + Filter: nil, + TestDirectory: "tests", + ViewType: ViewHuman, + Vars: &Vars{}, + OperationParallelism: 10, }, wantDiags: tfdiags.Diagnostics{ tfdiags.Sourceless( diff --git a/internal/command/test.go b/internal/command/test.go index 38ccf46f50..8b236fe779 100644 --- a/internal/command/test.go +++ b/internal/command/test.go @@ -59,6 +59,9 @@ Options: -no-color If specified, output won't contain any color. + -parallelism=n Limit the number of concurrent operations within the + plan/apply operation of a test run. Defaults to 10. + -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 @@ -99,6 +102,7 @@ func (c *TestCommand) Run(rawArgs []string) int { c.View.HelpPrompt("test") return 1 } + c.Meta.parallelism = args.OperationParallelism view := views.NewTest(args.ViewType, c.View) @@ -183,21 +187,22 @@ func (c *TestCommand) Run(rawArgs []string) int { } runner = &cloud.TestSuiteRunner{ - ConfigDirectory: ".", // Always loading from the current directory. - TestingDirectory: args.TestDirectory, - Config: config, - Services: c.Services, - Source: args.CloudRunSource, - GlobalVariables: variables, - Stopped: false, - Cancelled: false, - StoppedCtx: stopCtx, - CancelledCtx: cancelCtx, - Verbose: args.Verbose, - Filters: args.Filter, - Renderer: renderer, - View: view, - Streams: c.Streams, + ConfigDirectory: ".", // Always loading from the current directory. + TestingDirectory: args.TestDirectory, + Config: config, + Services: c.Services, + Source: args.CloudRunSource, + GlobalVariables: variables, + Stopped: false, + Cancelled: false, + StoppedCtx: stopCtx, + CancelledCtx: cancelCtx, + Verbose: args.Verbose, + OperationParallelism: args.OperationParallelism, + Filters: args.Filter, + Renderer: renderer, + View: view, + Streams: c.Streams, } } else { localRunner := &local.TestSuiteRunner{ diff --git a/internal/command/test_test.go b/internal/command/test_test.go index 0a232c6a06..216276c132 100644 --- a/internal/command/test_test.go +++ b/internal/command/test_test.go @@ -35,6 +35,7 @@ func TestTest_Runs(t *testing.T) { code int initCode int skip bool + desc string }{ "simple_pass": { expectedOut: []string{"1 passed, 0 failed."}, @@ -54,6 +55,13 @@ func TestTest_Runs(t *testing.T) { expectedOut: []string{"1 passed, 0 failed."}, code: 0, }, + "simple_pass_cmd_parallel": { + override: "simple_pass", + args: []string{"-parallelism", "1"}, + expectedOut: []string{"1 passed, 0 failed."}, + code: 0, + desc: "simple_pass with parallelism set to 1", + }, "simple_pass_very_nested_alternate": { override: "simple_pass_very_nested", args: []string{"-test-directory", "./tests/subdir"}, diff --git a/website/docs/cli/commands/test.mdx b/website/docs/cli/commands/test.mdx index 555a3add7e..1e1e2bed7a 100644 --- a/website/docs/cli/commands/test.mdx +++ b/website/docs/cli/commands/test.mdx @@ -38,6 +38,8 @@ The following options apply to the Terraform `terraform test` command: * `-verbose` - Prints out the plan or state for each `run` block within a test file, based on the `command` attribute of each `run` block. +* `-parallelism=` - Specifies the number of plan/apply operations to execute in parallel within a single test run. The default is 10. + ## State Management Each Terraform test file will maintain all Terraform state it requires within memory as it executes, starting empty. This state is entirely separate from any existing state for the configuration under test, so you can safely execute Terraform test commands without affecting any live infrastructure.