diff --git a/.changes/unreleased/FEATURES-20230602-101545.yaml b/.changes/unreleased/FEATURES-20230602-101545.yaml new file mode 100644 index 000000000..36bf8344b --- /dev/null +++ b/.changes/unreleased/FEATURES-20230602-101545.yaml @@ -0,0 +1,6 @@ +kind: FEATURES +body: 'tfversion: Introduced new `tfversion` package with interface and built-in Terraform + version check functionality' +time: 2023-06-02T10:15:45.704158-04:00 +custom: + Issue: "128" diff --git a/.changes/unreleased/FEATURES-20230602-124403.yaml b/.changes/unreleased/FEATURES-20230602-124403.yaml new file mode 100644 index 000000000..7d3edea04 --- /dev/null +++ b/.changes/unreleased/FEATURES-20230602-124403.yaml @@ -0,0 +1,6 @@ +kind: FEATURES +body: 'tfversion: Added `SkipAbove` built-in version check, which skips the test if + the Terraform CLI version is above the given maximum.' +time: 2023-06-02T12:44:03.123635-04:00 +custom: + Issue: "128" diff --git a/.changes/unreleased/FEATURES-20230602-124437.yaml b/.changes/unreleased/FEATURES-20230602-124437.yaml new file mode 100644 index 000000000..e9ef4cd95 --- /dev/null +++ b/.changes/unreleased/FEATURES-20230602-124437.yaml @@ -0,0 +1,6 @@ +kind: FEATURES +body: 'tfversion: Added `SkipBelow` built-in version check, which skips the test if + the Terraform CLI version is below the given minimum.' +time: 2023-06-02T12:44:37.228557-04:00 +custom: + Issue: "128" diff --git a/.changes/unreleased/FEATURES-20230602-124453.yaml b/.changes/unreleased/FEATURES-20230602-124453.yaml new file mode 100644 index 000000000..dfb1585e8 --- /dev/null +++ b/.changes/unreleased/FEATURES-20230602-124453.yaml @@ -0,0 +1,7 @@ +kind: FEATURES +body: 'tfversion: Added `SkipBetween` built-in version check, which skips the test + if the Terraform CLI version is between the given minimum (inclusive) and maximum + (exclusive).' +time: 2023-06-02T12:44:53.737283-04:00 +custom: + Issue: "128" diff --git a/.changes/unreleased/FEATURES-20230602-124514.yaml b/.changes/unreleased/FEATURES-20230602-124514.yaml new file mode 100644 index 000000000..d01125254 --- /dev/null +++ b/.changes/unreleased/FEATURES-20230602-124514.yaml @@ -0,0 +1,6 @@ +kind: FEATURES +body: 'tfversion: Added `SkipIf` built-in version check, which skips the test if the + Terraform CLI version matches the given version.' +time: 2023-06-02T12:45:14.812485-04:00 +custom: + Issue: "128" diff --git a/.changes/unreleased/FEATURES-20230602-124532.yaml b/.changes/unreleased/FEATURES-20230602-124532.yaml new file mode 100644 index 000000000..16390acca --- /dev/null +++ b/.changes/unreleased/FEATURES-20230602-124532.yaml @@ -0,0 +1,6 @@ +kind: FEATURES +body: 'tfversion: Added `RequireAbove` built-in version check, which fails the test + if the Terraform CLI version is below the given maximum.' +time: 2023-06-02T12:45:32.983833-04:00 +custom: + Issue: "128" diff --git a/.changes/unreleased/FEATURES-20230602-124653.yaml b/.changes/unreleased/FEATURES-20230602-124653.yaml new file mode 100644 index 000000000..732c86a43 --- /dev/null +++ b/.changes/unreleased/FEATURES-20230602-124653.yaml @@ -0,0 +1,6 @@ +kind: FEATURES +body: 'tfversion: Added `RequireBelow` built-in version check, which fails the test + if the Terraform CLI version is above the given minimum.' +time: 2023-06-02T12:46:53.705136-04:00 +custom: + Issue: "128" diff --git a/.changes/unreleased/FEATURES-20230602-124711.yaml b/.changes/unreleased/FEATURES-20230602-124711.yaml new file mode 100644 index 000000000..7e2ceb4d9 --- /dev/null +++ b/.changes/unreleased/FEATURES-20230602-124711.yaml @@ -0,0 +1,6 @@ +kind: FEATURES +body: 'tfversion: Added `RequireBetween` built-in version check, fails the test if + the Terraform CLI version is outside the given minimum (exclusive) and maximum (inclusive).' +time: 2023-06-02T12:47:11.762061-04:00 +custom: + Issue: "128" diff --git a/.changes/unreleased/FEATURES-20230602-124732.yaml b/.changes/unreleased/FEATURES-20230602-124732.yaml new file mode 100644 index 000000000..f802b01c4 --- /dev/null +++ b/.changes/unreleased/FEATURES-20230602-124732.yaml @@ -0,0 +1,6 @@ +kind: FEATURES +body: 'tfversion: Added `RequireNot` built-in version check, which fails the test + if the Terraform CLI version matches the given version.' +time: 2023-06-02T12:47:32.000508-04:00 +custom: + Issue: "128" diff --git a/.changes/unreleased/FEATURES-20230602-124753.yaml b/.changes/unreleased/FEATURES-20230602-124753.yaml new file mode 100644 index 000000000..2fee8c19e --- /dev/null +++ b/.changes/unreleased/FEATURES-20230602-124753.yaml @@ -0,0 +1,6 @@ +kind: FEATURES +body: 'tfversion: Added `Any` built-in version check, which fails the test if none + of the given sub-checks return a nil error and empty skip message.' +time: 2023-06-02T12:47:53.181503-04:00 +custom: + Issue: "128" diff --git a/.changes/unreleased/FEATURES-20230602-124807.yaml b/.changes/unreleased/FEATURES-20230602-124807.yaml new file mode 100644 index 000000000..6c89987f6 --- /dev/null +++ b/.changes/unreleased/FEATURES-20230602-124807.yaml @@ -0,0 +1,6 @@ +kind: FEATURES +body: 'tfversion: Added `All` built-in version check, which fails or skips the test + if any of the given sub-checks return a non-nil error or non-empty skip message.' +time: 2023-06-02T12:48:07.933978-04:00 +custom: + Issue: "128" diff --git a/.golangci.yml b/.golangci.yml index 560707ab8..bd09eed57 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -22,4 +22,8 @@ linters: - unconvert - unparam - unused - - vet \ No newline at end of file + - vet + +run: + # Prevent false positive timeouts in CI + timeout: 5m \ No newline at end of file diff --git a/helper/resource/testcase_providers_test.go b/helper/resource/testcase_providers_test.go index 9ad10cad5..486ef1b5c 100644 --- a/helper/resource/testcase_providers_test.go +++ b/helper/resource/testcase_providers_test.go @@ -14,6 +14,8 @@ import ( "github.com/hashicorp/terraform-plugin-go/tfprotov6" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + "github.com/hashicorp/terraform-plugin-testing/internal/plugintest" ) func TestTestCaseProviderConfig(t *testing.T) { @@ -332,7 +334,7 @@ func TestTest_TestCase_ExternalProvidersAndProviderFactories_NonHashiCorpNamespa func TestTest_TestCase_ExternalProviders_Error(t *testing.T) { t.Parallel() - testExpectTFatal(t, func() { + plugintest.TestExpectTFatal(t, func() { Test(&mockT{}, TestCase{ ExternalProviders: map[string]ExternalProvider{ "testnonexistent": { @@ -368,7 +370,7 @@ func TestTest_TestCase_ProtoV5ProviderFactories(t *testing.T) { func TestTest_TestCase_ProtoV5ProviderFactories_Error(t *testing.T) { t.Parallel() - testExpectTFatal(t, func() { + plugintest.TestExpectTFatal(t, func() { Test(&mockT{}, TestCase{ ProtoV5ProviderFactories: map[string]func() (tfprotov5.ProviderServer, error){ "test": func() (tfprotov5.ProviderServer, error) { //nolint:unparam // required signature @@ -404,7 +406,7 @@ func TestTest_TestCase_ProtoV6ProviderFactories(t *testing.T) { func TestTest_TestCase_ProtoV6ProviderFactories_Error(t *testing.T) { t.Parallel() - testExpectTFatal(t, func() { + plugintest.TestExpectTFatal(t, func() { Test(&mockT{}, TestCase{ ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ "test": func() (tfprotov6.ProviderServer, error) { //nolint:unparam // required signature @@ -440,7 +442,7 @@ func TestTest_TestCase_ProviderFactories(t *testing.T) { func TestTest_TestCase_ProviderFactories_Error(t *testing.T) { t.Parallel() - testExpectTFatal(t, func() { + plugintest.TestExpectTFatal(t, func() { Test(&mockT{}, TestCase{ ProviderFactories: map[string]func() (*schema.Provider, error){ "test": func() (*schema.Provider, error) { //nolint:unparam // required signature diff --git a/helper/resource/testing.go b/helper/resource/testing.go index 71dbeb921..be7c01c6c 100644 --- a/helper/resource/testing.go +++ b/helper/resource/testing.go @@ -25,6 +25,7 @@ import ( "github.com/hashicorp/terraform-plugin-testing/plancheck" "github.com/hashicorp/terraform-plugin-testing/terraform" + "github.com/hashicorp/terraform-plugin-testing/tfversion" "github.com/hashicorp/terraform-plugin-testing/internal/addrs" "github.com/hashicorp/terraform-plugin-testing/internal/logging" @@ -323,6 +324,12 @@ type TestCase struct { // acceptance tests, such as verifying that keys are setup. PreCheck func() + // TerraformVersionChecks is a list of checks to run against + // the Terraform CLI version which is running the testing. + // Each check is executed in order, respecting the first skip + // or fail response, unless the Any() meta check is also used. + TerraformVersionChecks []tfversion.TerraformVersionCheck + // ProviderFactories can be specified for the providers that are valid. // // This can also be specified at the TestStep level to enable per-step @@ -824,6 +831,17 @@ func Test(t testing.T, c TestCase) { } }(helper) + // Run the TerraformVersionChecks if we have it. + // This is done after creating the helper because a working directory is required + // to retrieve the Terraform version. + if c.TerraformVersionChecks != nil { + logging.HelperResourceDebug(ctx, "Calling TestCase Terraform version checks") + + runTFVersionChecks(ctx, t, helper.TerraformVersion(), c.TerraformVersionChecks) + + logging.HelperResourceDebug(ctx, "Called TestCase Terraform version checks") + } + runNewTest(ctx, t, c, helper) logging.HelperResourceDebug(ctx, "Finished TestCase") diff --git a/helper/resource/testing_test.go b/helper/resource/testing_test.go index 1546bfc3f..221706bfb 100644 --- a/helper/resource/testing_test.go +++ b/helper/resource/testing_test.go @@ -27,45 +27,6 @@ func init() { } } -// testExpectTFatal provides a wrapper for logic which should call -// (*testing.T).Fatal() or (*testing.T).Fatalf(). -// -// Since we do not want the wrapping test to fail when an expected test error -// occurs, it is required that the testLogic passed in uses -// github.com/mitchellh/go-testing-interface.RuntimeT instead of the real -// *testing.T. -// -// If Fatal() or Fatalf() is not called in the logic, the real (*testing.T).Fatal() will -// be called to fail the test. -func testExpectTFatal(t *testing.T, testLogic func()) { - t.Helper() - - var recoverIface interface{} - - func() { - defer func() { - recoverIface = recover() - }() - - testLogic() - }() - - if recoverIface == nil { - t.Fatalf("expected t.Fatal(), got none") - } - - recoverStr, ok := recoverIface.(string) - - if !ok { - t.Fatalf("expected string from recover(), got: %v (%T)", recoverIface, recoverIface) - } - - // this string is hardcoded in github.com/mitchellh/go-testing-interface - if !strings.HasPrefix(recoverStr, "testing.T failed, see logs for output") { - t.Fatalf("expected t.Fatal(), got: %s", recoverStr) - } -} - func TestParallelTest(t *testing.T) { t.Parallel() diff --git a/helper/resource/teststep_providers_test.go b/helper/resource/teststep_providers_test.go index 44960abc0..114395578 100644 --- a/helper/resource/teststep_providers_test.go +++ b/helper/resource/teststep_providers_test.go @@ -1053,7 +1053,7 @@ func TestTest_TestStep_ExternalProviders_DifferentVersions(t *testing.T) { func TestTest_TestStep_ExternalProviders_Error(t *testing.T) { t.Parallel() - testExpectTFatal(t, func() { + plugintest.TestExpectTFatal(t, func() { Test(&mockT{}, TestCase{ Steps: []TestStep{ { @@ -1354,7 +1354,7 @@ func TestTest_TestStep_ProtoV5ProviderFactories(t *testing.T) { func TestTest_TestStep_ProtoV5ProviderFactories_Error(t *testing.T) { t.Parallel() - testExpectTFatal(t, func() { + plugintest.TestExpectTFatal(t, func() { Test(&mockT{}, TestCase{ Steps: []TestStep{ { @@ -1390,7 +1390,7 @@ func TestTest_TestStep_ProtoV6ProviderFactories(t *testing.T) { func TestTest_TestStep_ProtoV6ProviderFactories_Error(t *testing.T) { t.Parallel() - testExpectTFatal(t, func() { + plugintest.TestExpectTFatal(t, func() { Test(&mockT{}, TestCase{ Steps: []TestStep{ { @@ -1426,7 +1426,7 @@ func TestTest_TestStep_ProviderFactories(t *testing.T) { func TestTest_TestStep_ProviderFactories_Error(t *testing.T) { t.Parallel() - testExpectTFatal(t, func() { + plugintest.TestExpectTFatal(t, func() { Test(&mockT{}, TestCase{ Steps: []TestStep{ { diff --git a/helper/resource/tfversion_checks.go b/helper/resource/tfversion_checks.go new file mode 100644 index 000000000..1bec0abd6 --- /dev/null +++ b/helper/resource/tfversion_checks.go @@ -0,0 +1,31 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package resource + +import ( + "context" + + "github.com/hashicorp/go-version" + "github.com/mitchellh/go-testing-interface" + + "github.com/hashicorp/terraform-plugin-testing/tfversion" +) + +func runTFVersionChecks(ctx context.Context, t testing.T, terraformVersion *version.Version, terraformVersionChecks []tfversion.TerraformVersionCheck) { + t.Helper() + + for _, tfVersionCheck := range terraformVersionChecks { + resp := tfversion.CheckTerraformVersionResponse{} + tfVersionCheck.CheckTerraformVersion(ctx, tfversion.CheckTerraformVersionRequest{TerraformVersion: terraformVersion}, &resp) + + if resp.Error != nil { + t.Fatalf(resp.Error.Error()) + } + + if resp.Skip != "" { + t.Skip(resp.Skip) + } + } + +} diff --git a/helper/resource/tfversion_checks_test.go b/helper/resource/tfversion_checks_test.go new file mode 100644 index 000000000..0bc2238f4 --- /dev/null +++ b/helper/resource/tfversion_checks_test.go @@ -0,0 +1,65 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package resource + +import ( + "context" + "testing" + + "github.com/hashicorp/go-version" + + "github.com/hashicorp/terraform-plugin-testing/internal/plugintest" + "github.com/hashicorp/terraform-plugin-testing/tfversion" + + testinginterface "github.com/mitchellh/go-testing-interface" +) + +func TestRunTFVersionChecks(t *testing.T) { + t.Parallel() + + tests := map[string]struct { + versionChecks []tfversion.TerraformVersionCheck + tfVersion *version.Version + expectError bool + }{ + "run-test": { + versionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipIf(version.Must(version.NewVersion("1.1.0"))), + tfversion.RequireAbove(version.Must(version.NewVersion("1.2.0"))), + }, + tfVersion: version.Must(version.NewVersion("1.3.0")), + expectError: false, + }, + "skip-test": { + versionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipIf(version.Must(version.NewVersion("1.1.0"))), + }, + tfVersion: version.Must(version.NewVersion("1.1.0")), + expectError: false, + }, + "fail-test": { + versionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireNot(version.Must(version.NewVersion("1.1.0"))), + }, + tfVersion: version.Must(version.NewVersion("1.1.0")), + expectError: true, + }, + } + + for name, test := range tests { + name, test := name, test + + t.Run(name, func(t *testing.T) { + t.Parallel() + + if test.expectError { + plugintest.TestExpectTFatal(t, func() { + runTFVersionChecks(context.Background(), &testinginterface.RuntimeT{}, test.tfVersion, test.versionChecks) + }) + } else { + runTFVersionChecks(context.Background(), t, test.tfVersion, test.versionChecks) + } + }) + } +} diff --git a/internal/plugintest/helper.go b/internal/plugintest/helper.go index ef14a0596..3c9772cfc 100644 --- a/internal/plugintest/helper.go +++ b/internal/plugintest/helper.go @@ -10,6 +10,7 @@ import ( "os" "strings" + "github.com/hashicorp/go-version" "github.com/hashicorp/terraform-exec/tfexec" "github.com/hashicorp/terraform-plugin-testing/internal/logging" @@ -42,6 +43,7 @@ type Helper struct { // for tests that use fixture files. sourceDir string terraformExec string + terraformVer *version.Version // execTempDir is created during DiscoverConfig to store any downloaded // binaries @@ -78,11 +80,23 @@ func InitHelper(ctx context.Context, config *Config) (*Helper, error) { return nil, fmt.Errorf("failed to create temporary directory for test helper: %s", err) } + tf, err := tfexec.NewTerraform(baseDir, config.TerraformExec) + if err != nil { + return nil, fmt.Errorf("unable to create terraform-exec instance: %w", err) + } + + tfVersion, _, err := tf.Version(ctx, false) + + if err != nil { + return nil, fmt.Errorf("error calling terraform version command: %w", err) + } + return &Helper{ baseDir: baseDir, sourceDir: config.SourceDir, terraformExec: config.TerraformExec, execTempDir: config.execTempDir, + terraformVer: tfVersion, }, nil } @@ -301,3 +315,8 @@ func (h *Helper) WorkingDirectory() string { func (h *Helper) TerraformExecPath() string { return h.terraformExec } + +// TerraformVersion returns the Terraform CLI version being used when running tests. +func (h *Helper) TerraformVersion() *version.Version { + return h.terraformVer +} diff --git a/internal/plugintest/util.go b/internal/plugintest/util.go index 067dc74af..acccb3bcf 100644 --- a/internal/plugintest/util.go +++ b/internal/plugintest/util.go @@ -10,6 +10,7 @@ import ( "path" "path/filepath" "strings" + "testing" ) func symlinkFile(src string, dest string) error { @@ -137,3 +138,42 @@ func CopyDir(src, dest, baseDirName string) error { return nil } + +// TestExpectTFatal provides a wrapper for logic which should call +// (*testing.T).Fatal() or (*testing.T).Fatalf(). +// +// Since we do not want the wrapping test to fail when an expected test error +// occurs, it is required that the testLogic passed in uses +// github.com/mitchellh/go-testing-interface.RuntimeT instead of the real +// *testing.T. +// +// If Fatal() or Fatalf() is not called in the logic, the real (*testing.T).Fatal() will +// be called to fail the test. +func TestExpectTFatal(t *testing.T, testLogic func()) { + t.Helper() + + var recoverIface interface{} + + func() { + defer func() { + recoverIface = recover() + }() + + testLogic() + }() + + if recoverIface == nil { + t.Fatalf("expected t.Fatal(), got none") + } + + recoverStr, ok := recoverIface.(string) + + if !ok { + t.Fatalf("expected string from recover(), got: %v (%T)", recoverIface, recoverIface) + } + + // this string is hardcoded in github.com/mitchellh/go-testing-interface + if !strings.HasPrefix(recoverStr, "testing.T failed, see logs for output") { + t.Fatalf("expected t.Fatal(), got: %s", recoverStr) + } +} diff --git a/tfversion/all.go b/tfversion/all.go new file mode 100644 index 000000000..78e1f1780 --- /dev/null +++ b/tfversion/all.go @@ -0,0 +1,45 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package tfversion + +import ( + "context" +) + +// All will return the first non-nil error or non-empty skip message +// if any of the given checks return a non-nil error or non-empty skip message. +// Otherwise, it will return a nil error and empty skip message (run the test) +// +// Use of All is only necessary when used in conjunction with Any as the +// TerraformVersionChecks field automatically applies a logical AND. +func All(terraformVersionChecks ...TerraformVersionCheck) TerraformVersionCheck { + return allCheck{ + terraformVersionChecks: terraformVersionChecks, + } +} + +// allCheck implements the TerraformVersionCheck interface +type allCheck struct { + terraformVersionChecks []TerraformVersionCheck +} + +// CheckTerraformVersion satisfies the TerraformVersionCheck interface. +func (a allCheck) CheckTerraformVersion(ctx context.Context, req CheckTerraformVersionRequest, resp *CheckTerraformVersionResponse) { + + for _, subCheck := range a.terraformVersionChecks { + checkResp := CheckTerraformVersionResponse{} + + subCheck.CheckTerraformVersion(ctx, CheckTerraformVersionRequest{TerraformVersion: req.TerraformVersion}, &checkResp) + + if checkResp.Error != nil { + resp.Error = checkResp.Error + return + } + + if checkResp.Skip != "" { + resp.Skip = checkResp.Skip + return + } + } +} diff --git a/tfversion/all_test.go b/tfversion/all_test.go new file mode 100644 index 000000000..4751eeb5c --- /dev/null +++ b/tfversion/all_test.go @@ -0,0 +1,103 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package tfversion_test + +import ( + "testing" + + "github.com/hashicorp/go-version" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + testinginterface "github.com/mitchellh/go-testing-interface" + + r "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/internal/plugintest" + "github.com/hashicorp/terraform-plugin-testing/tfversion" +) + +func Test_All_RunTest(t *testing.T) { //nolint:paralleltest + t.Setenv("TF_ACC_TERRAFORM_VERSION", "1.1.0") + + r.UnitTest(t, r.TestCase{ + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": func() (tfprotov6.ProviderServer, error) { //nolint:unparam // required signature + return nil, nil + }, + }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.Any( + tfversion.All( + tfversion.RequireNot(version.Must(version.NewVersion("0.15.0"))), //returns nil + tfversion.SkipIf(version.Must(version.NewVersion("1.2.0"))), //returns nil + tfversion.RequireBelow(version.Must(version.NewVersion("1.2.0"))), //returns nil + ), + ), + }, + Steps: []r.TestStep{ + { + Config: `variable "a" { + nullable = true + default = "hello" + }`, + }, + }, + }) +} + +func Test_All_SkipTest(t *testing.T) { //nolint:paralleltest + t.Setenv("TF_ACC_TERRAFORM_VERSION", "1.0.7") + + r.UnitTest(t, r.TestCase{ + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": func() (tfprotov6.ProviderServer, error) { //nolint:unparam // required signature + return nil, nil + }, + }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.Any( + tfversion.All( + tfversion.RequireNot(version.Must(version.NewVersion("0.15.0"))), //returns nil + tfversion.SkipBelow(version.Must(version.NewVersion("1.2.0"))), //returns skip + tfversion.SkipIf(version.Must(version.NewVersion("1.0.7"))), //returns skip + tfversion.RequireBelow(version.Must(version.NewVersion("1.2.0"))), //returns nil + ), + ), + }, + Steps: []r.TestStep{ + { + Config: `variable "a" { + nullable = true + default = "hello" + }`, + }, + }, + }) +} + +func Test_All_Error(t *testing.T) { //nolint:paralleltest + t.Setenv("TF_ACC_TERRAFORM_VERSION", "1.1.0") + + plugintest.TestExpectTFatal(t, func() { + r.UnitTest(&testinginterface.RuntimeT{}, r.TestCase{ + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": func() (tfprotov6.ProviderServer, error) { //nolint:unparam // required signature + return nil, nil + }, + }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.Any( + tfversion.All( + tfversion.RequireNot(version.Must(version.NewVersion("1.1.0"))), //returns error + tfversion.SkipIf(version.Must(version.NewVersion("1.1.0"))), //returns skip + tfversion.RequireAbove(version.Must(version.NewVersion("1.2.0"))), //returns nil + ), + ), + }, + Steps: []r.TestStep{ + { + Config: `//non-empty config`, + }, + }, + }) + }) +} diff --git a/tfversion/any.go b/tfversion/any.go new file mode 100644 index 000000000..27088e1a5 --- /dev/null +++ b/tfversion/any.go @@ -0,0 +1,59 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package tfversion + +import ( + "context" + "strings" + + "github.com/hashicorp/terraform-plugin-testing/internal/errorshim" +) + +// Any will return a nil error and empty skip message (run the test) +// if any of the given checks return a nil error and empty skip message. +// Otherwise, it will return all errors and fail the test if any of the given +// checks return a non-nil error, or it will return all skip messages +// and skip (pass) the test. +func Any(terraformVersionChecks ...TerraformVersionCheck) TerraformVersionCheck { + return anyCheck{ + terraformVersionChecks: terraformVersionChecks, + } +} + +// anyCheck implements the TerraformVersionCheck interface +type anyCheck struct { + terraformVersionChecks []TerraformVersionCheck +} + +// CheckTerraformVersion satisfies the TerraformVersionCheck interface. +func (a anyCheck) CheckTerraformVersion(ctx context.Context, req CheckTerraformVersionRequest, resp *CheckTerraformVersionResponse) { + var joinedErrors error + strBuilder := strings.Builder{} + + for _, subCheck := range a.terraformVersionChecks { + checkResp := CheckTerraformVersionResponse{} + + subCheck.CheckTerraformVersion(ctx, CheckTerraformVersionRequest{TerraformVersion: req.TerraformVersion}, &checkResp) + + if checkResp.Error == nil && checkResp.Skip == "" { + resp.Error = nil + resp.Skip = "" + return + } + + if checkResp.Error != nil { + // TODO: Once Go 1.20 is the minimum supported version for this module, replace with `errors.Join` function + // - https://github.com/hashicorp/terraform-plugin-testing/issues/99 + joinedErrors = errorshim.Join(joinedErrors, checkResp.Error) + } + + if checkResp.Skip != "" { + strBuilder.WriteString(checkResp.Skip) + strBuilder.WriteString("\n") + } + } + + resp.Error = joinedErrors + resp.Skip = strings.TrimSpace(strBuilder.String()) +} diff --git a/tfversion/any_test.go b/tfversion/any_test.go new file mode 100644 index 000000000..bf5e1f7b1 --- /dev/null +++ b/tfversion/any_test.go @@ -0,0 +1,94 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package tfversion_test + +import ( + "testing" + + "github.com/hashicorp/go-version" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + testinginterface "github.com/mitchellh/go-testing-interface" + + r "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/internal/plugintest" + "github.com/hashicorp/terraform-plugin-testing/tfversion" +) + +func Test_Any_RunTest(t *testing.T) { //nolint:paralleltest + t.Setenv("TF_ACC_TERRAFORM_VERSION", "1.1.0") + + r.UnitTest(t, r.TestCase{ + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": func() (tfprotov6.ProviderServer, error) { //nolint:unparam // required signature + return nil, nil + }, + }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.Any( + tfversion.RequireNot(version.Must(version.NewVersion("1.1.0"))), //returns error + tfversion.RequireBelow(version.Must(version.NewVersion("1.2.0"))), //returns nil + ), + }, + Steps: []r.TestStep{ + { + Config: `variable "a" { + nullable = true + default = "hello" + }`, + }, + }, + }) +} + +func Test_Any_SkipTest(t *testing.T) { //nolint:paralleltest + t.Setenv("TF_ACC_TERRAFORM_VERSION", "1.1.0") + + r.UnitTest(t, r.TestCase{ + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": func() (tfprotov6.ProviderServer, error) { //nolint:unparam // required signature + return nil, nil + }, + }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.Any( + tfversion.SkipIf(version.Must(version.NewVersion("1.1.0"))), //returns skip + tfversion.SkipBelow(version.Must(version.NewVersion("1.2.0"))), //returns skip + ), + }, + Steps: []r.TestStep{ + { + Config: `variable "a" { + nullable = true + default = "hello" + }`, + }, + }, + }) +} + +func Test_Any_Error(t *testing.T) { //nolint:paralleltest + t.Setenv("TF_ACC_TERRAFORM_VERSION", "1.1.0") + + plugintest.TestExpectTFatal(t, func() { + r.UnitTest(&testinginterface.RuntimeT{}, r.TestCase{ + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": func() (tfprotov6.ProviderServer, error) { //nolint:unparam // required signature + return nil, nil + }, + }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.Any( + tfversion.SkipIf(version.Must(version.NewVersion("1.1.0"))), //returns skip + tfversion.RequireNot(version.Must(version.NewVersion("1.1.0"))), //returns error + tfversion.RequireAbove(version.Must(version.NewVersion("1.2.0"))), //returns error + ), + }, + Steps: []r.TestStep{ + { + Config: `//non-empty config`, + }, + }, + }) + }) +} diff --git a/tfversion/doc.go b/tfversion/doc.go new file mode 100644 index 000000000..d73b474d2 --- /dev/null +++ b/tfversion/doc.go @@ -0,0 +1,5 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +// Package tfversion contains the Terraform version check interface, request/response structs, and common version check implementations. +package tfversion diff --git a/tfversion/require_above.go b/tfversion/require_above.go new file mode 100644 index 000000000..4734bcf6e --- /dev/null +++ b/tfversion/require_above.go @@ -0,0 +1,35 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package tfversion + +import ( + "context" + "fmt" + + "github.com/hashicorp/go-version" +) + +// RequireAbove will fail the test if the Terraform CLI +// version is below the given version. For example, if given +// version.Must(version.NewVersion("0.15.0")), then 0.14.x or +// any other prior minor versions will fail the test. +func RequireAbove(minimumVersion *version.Version) TerraformVersionCheck { + return requireAboveCheck{ + minimumVersion: minimumVersion, + } +} + +// requireAboveCheck implements the TerraformVersionCheck interface +type requireAboveCheck struct { + minimumVersion *version.Version +} + +// CheckTerraformVersion satisfies the TerraformVersionCheck interface. +func (r requireAboveCheck) CheckTerraformVersion(ctx context.Context, req CheckTerraformVersionRequest, resp *CheckTerraformVersionResponse) { + + if req.TerraformVersion.LessThan(r.minimumVersion) { + resp.Error = fmt.Errorf("expected Terraform CLI version above %s but detected version is %s", + r.minimumVersion, req.TerraformVersion) + } +} diff --git a/tfversion/require_above_test.go b/tfversion/require_above_test.go new file mode 100644 index 000000000..b78ac8b13 --- /dev/null +++ b/tfversion/require_above_test.go @@ -0,0 +1,63 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package tfversion_test + +import ( + "testing" + + "github.com/hashicorp/go-version" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + + r "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/internal/plugintest" + "github.com/hashicorp/terraform-plugin-testing/tfversion" + + testinginterface "github.com/mitchellh/go-testing-interface" +) + +func Test_RequireAbove(t *testing.T) { //nolint:paralleltest + t.Setenv("TF_ACC_TERRAFORM_VERSION", "1.1.0") + + r.UnitTest(t, r.TestCase{ + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": func() (tfprotov6.ProviderServer, error) { //nolint:unparam // required signature + return nil, nil + }, + }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireAbove(version.Must(version.NewVersion("1.1.0"))), + }, + Steps: []r.TestStep{ + { + //nullable argument only available in TF v1.1.0+ + Config: `variable "a" { + nullable = true + default = "hello" + }`, + }, + }, + }) +} + +func Test_RequireAbove_Error(t *testing.T) { //nolint:paralleltest + t.Setenv("TF_ACC_TERRAFORM_VERSION", "1.0.7") + + plugintest.TestExpectTFatal(t, func() { + r.UnitTest(&testinginterface.RuntimeT{}, r.TestCase{ + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": func() (tfprotov6.ProviderServer, error) { //nolint:unparam // required signature + return nil, nil + }, + }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireAbove(version.Must(version.NewVersion("1.1.0"))), + }, + Steps: []r.TestStep{ + { + Config: `//non-empty config`, + }, + }, + }) + }) +} diff --git a/tfversion/require_below.go b/tfversion/require_below.go new file mode 100644 index 000000000..99efa5346 --- /dev/null +++ b/tfversion/require_below.go @@ -0,0 +1,35 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package tfversion + +import ( + "context" + "fmt" + + "github.com/hashicorp/go-version" +) + +// RequireBelow will fail the test if the Terraform CLI +// version is above the given version. For example, if given +// version.Must(version.NewVersion("0.15.0")), then versions 0.15.x and +// above will fail the test. +func RequireBelow(maximumVersion *version.Version) TerraformVersionCheck { + return requireBelowCheck{ + maximumVersion: maximumVersion, + } +} + +// requireBelowCheck implements the TerraformVersionCheck interface +type requireBelowCheck struct { + maximumVersion *version.Version +} + +// CheckTerraformVersion satisfies the TerraformVersionCheck interface. +func (s requireBelowCheck) CheckTerraformVersion(ctx context.Context, req CheckTerraformVersionRequest, resp *CheckTerraformVersionResponse) { + + if req.TerraformVersion.GreaterThan(s.maximumVersion) { + resp.Error = fmt.Errorf("expected Terraform CLI version below %s but detected version is %s", + s.maximumVersion, req.TerraformVersion) + } +} diff --git a/tfversion/require_below_test.go b/tfversion/require_below_test.go new file mode 100644 index 000000000..9c3e40d8b --- /dev/null +++ b/tfversion/require_below_test.go @@ -0,0 +1,64 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package tfversion_test + +import ( + "testing" + + "github.com/hashicorp/go-version" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + + r "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/internal/plugintest" + "github.com/hashicorp/terraform-plugin-testing/tfversion" + + testinginterface "github.com/mitchellh/go-testing-interface" +) + +func Test_RequireBelow(t *testing.T) { //nolint:paralleltest + t.Setenv("TF_ACC_TERRAFORM_VERSION", "1.2.0") + + r.UnitTest(t, r.TestCase{ + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": func() (tfprotov6.ProviderServer, error) { //nolint:unparam // required signature + return nil, nil + }, + }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireBelow(version.Must(version.NewVersion("1.3.0"))), + }, + Steps: []r.TestStep{ + { + //module_variable_optional_attrs experiment is deprecated in TF v1.3.0 + Config: ` + terraform { + experiments = [module_variable_optional_attrs] + } + `, + }, + }, + }) +} + +func Test_RequireBelow_Error(t *testing.T) { //nolint:paralleltest + t.Setenv("TF_ACC_TERRAFORM_VERSION", "1.4.0") + + plugintest.TestExpectTFatal(t, func() { + r.UnitTest(&testinginterface.RuntimeT{}, r.TestCase{ + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": func() (tfprotov6.ProviderServer, error) { //nolint:unparam // required signature + return nil, nil + }, + }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireBelow(version.Must(version.NewVersion("1.3.0"))), + }, + Steps: []r.TestStep{ + { + Config: `//non-empty config`, + }, + }, + }) + }) +} diff --git a/tfversion/require_between.go b/tfversion/require_between.go new file mode 100644 index 000000000..b99297928 --- /dev/null +++ b/tfversion/require_between.go @@ -0,0 +1,38 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package tfversion + +import ( + "context" + "fmt" + + "github.com/hashicorp/go-version" +) + +// RequireBetween will fail the test if the Terraform CLI +// version is outside the given minimum (exclusive) and maximum (inclusive). +// For example, if given a minimum version of version.Must(version.NewVersion("0.15.0")) +// and a maximum version of version.Must(version.NewVersion("1.0.0")), then 0.15.x or +// any other prior versions and versions greater than 1.0.0 will fail the test. +func RequireBetween(minimumVersion, maximumVersion *version.Version) TerraformVersionCheck { + return requireBetweenCheck{ + minimumVersion: minimumVersion, + maximumVersion: maximumVersion, + } +} + +// requireBetweenCheck implements the TerraformVersionCheck interface +type requireBetweenCheck struct { + minimumVersion *version.Version + maximumVersion *version.Version +} + +// CheckTerraformVersion satisfies the TerraformVersionCheck interface. +func (s requireBetweenCheck) CheckTerraformVersion(ctx context.Context, req CheckTerraformVersionRequest, resp *CheckTerraformVersionResponse) { + + if req.TerraformVersion.LessThan(s.minimumVersion) || req.TerraformVersion.GreaterThanOrEqual(s.maximumVersion) { + resp.Error = fmt.Errorf("expected Terraform CLI version between %s and %s but detected version is %s", + s.minimumVersion, s.maximumVersion, req.TerraformVersion) + } +} diff --git a/tfversion/require_between_test.go b/tfversion/require_between_test.go new file mode 100644 index 000000000..c15e22006 --- /dev/null +++ b/tfversion/require_between_test.go @@ -0,0 +1,98 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package tfversion_test + +import ( + "testing" + + "github.com/hashicorp/go-version" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + + r "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/internal/plugintest" + "github.com/hashicorp/terraform-plugin-testing/tfversion" + + testinginterface "github.com/mitchellh/go-testing-interface" +) + +func Test_RequireBetween(t *testing.T) { //nolint:paralleltest + t.Setenv("TF_ACC_TERRAFORM_VERSION", "1.2.0") + + r.UnitTest(t, r.TestCase{ + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": func() (tfprotov6.ProviderServer, error) { //nolint:unparam // required signature + return nil, nil + }, + }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireBetween(version.Must(version.NewVersion("1.2.0")), version.Must(version.NewVersion("1.3.0"))), + }, + Steps: []r.TestStep{ + { + //module_variable_optional_attrs experiment is deprecated in TF v1.3.0 + //precondition block is only available in TF v1.2.0+ + Config: ` + terraform { + experiments = [module_variable_optional_attrs] + } + + locals { + ex_var = "hello" + } + + output "example" { + value = "output" + precondition { + condition = local.ex_var != "hi" + error_message = "precondition_error" + } + }`, + }, + }, + }) +} + +func Test_RequireBetween_Error_BelowMin(t *testing.T) { //nolint:paralleltest + t.Setenv("TF_ACC_TERRAFORM_VERSION", "1.1.0") + + plugintest.TestExpectTFatal(t, func() { + r.UnitTest(&testinginterface.RuntimeT{}, r.TestCase{ + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": func() (tfprotov6.ProviderServer, error) { //nolint:unparam // required signature + return nil, nil + }, + }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireBetween(version.Must(version.NewVersion("1.2.0")), version.Must(version.NewVersion("1.3.0"))), + }, + Steps: []r.TestStep{ + { + Config: `//non-empty config`, + }, + }, + }) + }) +} + +func Test_RequireBetween_Error_EqToMax(t *testing.T) { //nolint:paralleltest + t.Setenv("TF_ACC_TERRAFORM_VERSION", "1.3.0") + + plugintest.TestExpectTFatal(t, func() { + r.UnitTest(&testinginterface.RuntimeT{}, r.TestCase{ + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": func() (tfprotov6.ProviderServer, error) { //nolint:unparam // required signature + return nil, nil + }, + }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireBetween(version.Must(version.NewVersion("1.2.0")), version.Must(version.NewVersion("1.3.0"))), + }, + Steps: []r.TestStep{ + { + Config: `//non-empty config`, + }, + }, + }) + }) +} diff --git a/tfversion/require_not.go b/tfversion/require_not.go new file mode 100644 index 000000000..18a9b68d1 --- /dev/null +++ b/tfversion/require_not.go @@ -0,0 +1,32 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package tfversion + +import ( + "context" + "fmt" + + "github.com/hashicorp/go-version" +) + +// RequireNot will fail the test if the Terraform CLI +// version matches the given version. +func RequireNot(version *version.Version) TerraformVersionCheck { + return requireNotCheck{ + version: version, + } +} + +// requireNotCheck implements the TerraformVersionCheck interface +type requireNotCheck struct { + version *version.Version +} + +// CheckTerraformVersion satisfies the TerraformVersionCheck interface. +func (s requireNotCheck) CheckTerraformVersion(ctx context.Context, req CheckTerraformVersionRequest, resp *CheckTerraformVersionResponse) { + + if req.TerraformVersion.Equal(s.version) { + resp.Error = fmt.Errorf("unexpected Terraform CLI version: %s", s.version) + } +} diff --git a/tfversion/require_not_test.go b/tfversion/require_not_test.go new file mode 100644 index 000000000..dee5968a6 --- /dev/null +++ b/tfversion/require_not_test.go @@ -0,0 +1,59 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package tfversion_test + +import ( + "testing" + + "github.com/hashicorp/go-version" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + + r "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/internal/plugintest" + "github.com/hashicorp/terraform-plugin-testing/tfversion" + + testinginterface "github.com/mitchellh/go-testing-interface" +) + +func Test_RequireNot(t *testing.T) { //nolint:paralleltest + t.Setenv("TF_ACC_TERRAFORM_VERSION", "1.4.3") + + r.UnitTest(t, r.TestCase{ + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": func() (tfprotov6.ProviderServer, error) { //nolint:unparam // required signature + return nil, nil + }, + }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireNot(version.Must(version.NewVersion("1.1.0"))), + }, + Steps: []r.TestStep{ + { + Config: `//non-empty config`, + }, + }, + }) +} + +func Test_RequireNot_Error(t *testing.T) { //nolint:paralleltest + t.Setenv("TF_ACC_TERRAFORM_VERSION", "1.1.0") + + plugintest.TestExpectTFatal(t, func() { + r.UnitTest(&testinginterface.RuntimeT{}, r.TestCase{ + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": func() (tfprotov6.ProviderServer, error) { //nolint:unparam // required signature + return nil, nil + }, + }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireNot(version.Must(version.NewVersion("1.1.0"))), + }, + Steps: []r.TestStep{ + { + Config: `//non-empty config`, + }, + }, + }) + }) +} diff --git a/tfversion/skip_above.go b/tfversion/skip_above.go new file mode 100644 index 000000000..ffc69b857 --- /dev/null +++ b/tfversion/skip_above.go @@ -0,0 +1,35 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package tfversion + +import ( + "context" + "fmt" + + "github.com/hashicorp/go-version" +) + +// SkipAbove will skip (pass) the test if the Terraform CLI +// version is below the given version. For example, if given +// version.Must(version.NewVersion("0.15.0")), then 0.14.x or +// any other prior minor versions will skip the test. +func SkipAbove(maximumVersion *version.Version) TerraformVersionCheck { + return skipAboveCheck{ + maximumVersion: maximumVersion, + } +} + +// skipAboveCheck implements the TerraformVersionCheck interface +type skipAboveCheck struct { + maximumVersion *version.Version +} + +// CheckTerraformVersion satisfies the TerraformVersionCheck interface. +func (s skipAboveCheck) CheckTerraformVersion(ctx context.Context, req CheckTerraformVersionRequest, resp *CheckTerraformVersionResponse) { + + if req.TerraformVersion.GreaterThan(s.maximumVersion) { + resp.Skip = fmt.Sprintf("Terraform CLI version %s is above maximum version %s: skipping test", + req.TerraformVersion, s.maximumVersion) + } +} diff --git a/tfversion/skip_above_test.go b/tfversion/skip_above_test.go new file mode 100644 index 000000000..4390cc9cf --- /dev/null +++ b/tfversion/skip_above_test.go @@ -0,0 +1,64 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package tfversion_test + +import ( + "testing" + + "github.com/hashicorp/go-version" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + + r "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/tfversion" +) + +func Test_SkipAbove_SkipTest(t *testing.T) { //nolint:paralleltest + t.Setenv("TF_ACC_TERRAFORM_VERSION", "1.3.0") + + r.UnitTest(t, r.TestCase{ + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": func() (tfprotov6.ProviderServer, error) { //nolint:unparam // required signature + return nil, nil + }, + }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipAbove(version.Must(version.NewVersion("1.2.9"))), + }, + Steps: []r.TestStep{ + { + //module_variable_optional_attrs experiment is deprecated in TF v1.3.0 + Config: ` + terraform { + experiments = [module_variable_optional_attrs] + } + `, + }, + }, + }) +} + +func Test_SkipAbove_RunTest(t *testing.T) { //nolint:paralleltest + t.Setenv("TF_ACC_TERRAFORM_VERSION", "1.2.9") + + r.UnitTest(t, r.TestCase{ + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": func() (tfprotov6.ProviderServer, error) { //nolint:unparam // required signature + return nil, nil + }, + }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipAbove(version.Must(version.NewVersion("1.2.9"))), + }, + Steps: []r.TestStep{ + { + //module_variable_optional_attrs experiment is deprecated in TF v1.3.0 + Config: ` + terraform { + experiments = [module_variable_optional_attrs] + } + `, + }, + }, + }) +} diff --git a/tfversion/skip_below.go b/tfversion/skip_below.go new file mode 100644 index 000000000..0b3dffddc --- /dev/null +++ b/tfversion/skip_below.go @@ -0,0 +1,35 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package tfversion + +import ( + "context" + "fmt" + + "github.com/hashicorp/go-version" +) + +// SkipBelow will skip (pass) the test if the Terraform CLI +// version is below the given version. For example, if given +// version.Must(version.NewVersion("0.15.0")), then 0.14.x or +// any other prior minor versions will skip the test. +func SkipBelow(minimumVersion *version.Version) TerraformVersionCheck { + return skipBelowCheck{ + minimumVersion: minimumVersion, + } +} + +// skipBelowCheck implements the TerraformVersionCheck interface +type skipBelowCheck struct { + minimumVersion *version.Version +} + +// CheckTerraformVersion satisfies the TerraformVersionCheck interface. +func (s skipBelowCheck) CheckTerraformVersion(ctx context.Context, req CheckTerraformVersionRequest, resp *CheckTerraformVersionResponse) { + + if req.TerraformVersion.LessThan(s.minimumVersion) { + resp.Skip = fmt.Sprintf("Terraform CLI version %s is below minimum version %s: skipping test", + req.TerraformVersion, s.minimumVersion) + } +} diff --git a/tfversion/skip_below_test.go b/tfversion/skip_below_test.go new file mode 100644 index 000000000..e8c43089e --- /dev/null +++ b/tfversion/skip_below_test.go @@ -0,0 +1,62 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package tfversion_test + +import ( + "testing" + + "github.com/hashicorp/go-version" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + + r "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/tfversion" +) + +func Test_SkipBelow_SkipTest(t *testing.T) { //nolint:paralleltest + t.Setenv("TF_ACC_TERRAFORM_VERSION", "1.0.7") + + r.UnitTest(t, r.TestCase{ + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": func() (tfprotov6.ProviderServer, error) { //nolint:unparam // required signature + return nil, nil + }, + }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(version.Must(version.NewVersion("1.1.0"))), + }, + Steps: []r.TestStep{ + { + //nullable argument only available in TF v1.1.0+ + Config: `variable "a" { + nullable = true + default = "hello" + }`, + }, + }, + }) +} + +func Test_SkipBelow_RunTest(t *testing.T) { //nolint:paralleltest + t.Setenv("TF_ACC_TERRAFORM_VERSION", "1.1.0") + + r.UnitTest(t, r.TestCase{ + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": func() (tfprotov6.ProviderServer, error) { //nolint:unparam // required signature + return nil, nil + }, + }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(version.Must(version.NewVersion("1.1.0"))), + }, + Steps: []r.TestStep{ + { + //nullable argument only available in TF v1.1.0+ + Config: `variable "a" { + nullable = true + default = "hello" + }`, + }, + }, + }) +} diff --git a/tfversion/skip_between.go b/tfversion/skip_between.go new file mode 100644 index 000000000..fb6e94108 --- /dev/null +++ b/tfversion/skip_between.go @@ -0,0 +1,38 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package tfversion + +import ( + "context" + "fmt" + + "github.com/hashicorp/go-version" +) + +// SkipBetween will skip the test if the Terraform CLI +// version is between the given minimum (inclusive) and maximum (exclusive). +// For example, if given a minimum version of version.Must(version.NewVersion("0.15.0")) +// and a maximum version of version.Must(version.NewVersion("0.16.0")), then versions 0.15.x +// will skip the test. +func SkipBetween(minimumVersion, maximumVersion *version.Version) TerraformVersionCheck { + return skipBetweenCheck{ + minimumVersion: minimumVersion, + maximumVersion: maximumVersion, + } +} + +// skipBetweenCheck implements the TerraformVersionCheck interface +type skipBetweenCheck struct { + minimumVersion *version.Version + maximumVersion *version.Version +} + +// CheckTerraformVersion satisfies the TerraformVersionCheck interface. +func (s skipBetweenCheck) CheckTerraformVersion(ctx context.Context, req CheckTerraformVersionRequest, resp *CheckTerraformVersionResponse) { + + if req.TerraformVersion.GreaterThanOrEqual(s.minimumVersion) && req.TerraformVersion.LessThan(s.maximumVersion) { + resp.Skip = fmt.Sprintf("Terraform CLI version %s is between %s and %s: skipping test.", + req.TerraformVersion, s.minimumVersion, s.maximumVersion) + } +} diff --git a/tfversion/skip_between_test.go b/tfversion/skip_between_test.go new file mode 100644 index 000000000..098c03d20 --- /dev/null +++ b/tfversion/skip_between_test.go @@ -0,0 +1,93 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package tfversion_test + +import ( + "testing" + + "github.com/hashicorp/go-version" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + + r "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/tfversion" + + testinginterface "github.com/mitchellh/go-testing-interface" +) + +func Test_SkipBetween_SkipTest(t *testing.T) { //nolint:paralleltest + t.Setenv("TF_ACC_TERRAFORM_VERSION", "1.2.0") + + r.UnitTest(t, r.TestCase{ + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": func() (tfprotov6.ProviderServer, error) { //nolint:unparam // required signature + return nil, nil + }, + }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBetween(version.Must(version.NewVersion("1.2.0")), version.Must(version.NewVersion("1.3.0"))), + }, + Steps: []r.TestStep{ + { + //module_variable_optional_attrs experiment is deprecated in TF v1.3.0 + //precondition block is only available in TF v1.2.0+ + Config: ` + terraform { + experiments = [module_variable_optional_attrs] + } + + locals { + ex_var = "hello" + } + + output "example" { + value = "output" + precondition { + condition = local.ex_var != "hi" + error_message = "precondition_error" + } + }`, + }, + }, + }) +} + +func Test_SkipBetween_RunTest_AboveMax(t *testing.T) { //nolint:paralleltest + t.Setenv("TF_ACC_TERRAFORM_VERSION", "1.3.0") + + r.UnitTest(&testinginterface.RuntimeT{}, r.TestCase{ + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": func() (tfprotov6.ProviderServer, error) { //nolint:unparam // required signature + return nil, nil + }, + }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBetween(version.Must(version.NewVersion("1.2.0")), version.Must(version.NewVersion("1.3.0"))), + }, + Steps: []r.TestStep{ + { + Config: `//non-empty config`, + }, + }, + }) +} + +func Test_SkipBetween_RunTest_EqToMin(t *testing.T) { //nolint:paralleltest + t.Setenv("TF_ACC_TERRAFORM_VERSION", "1.2.0") + + r.UnitTest(&testinginterface.RuntimeT{}, r.TestCase{ + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": func() (tfprotov6.ProviderServer, error) { //nolint:unparam // required signature + return nil, nil + }, + }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBetween(version.Must(version.NewVersion("1.2.0")), version.Must(version.NewVersion("1.3.0"))), + }, + Steps: []r.TestStep{ + { + Config: `//non-empty config`, + }, + }, + }) +} diff --git a/tfversion/skip_if.go b/tfversion/skip_if.go new file mode 100644 index 000000000..6ece5e05d --- /dev/null +++ b/tfversion/skip_if.go @@ -0,0 +1,32 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package tfversion + +import ( + "context" + "fmt" + + "github.com/hashicorp/go-version" +) + +// SkipIf will skip (pass) the test if the Terraform CLI +// version matches the given version. +func SkipIf(version *version.Version) TerraformVersionCheck { + return skipIfCheck{ + version: version, + } +} + +// skipIfCheck implements the TerraformVersionCheck interface +type skipIfCheck struct { + version *version.Version +} + +// CheckTerraformVersion satisfies the TerraformVersionCheck interface. +func (s skipIfCheck) CheckTerraformVersion(ctx context.Context, req CheckTerraformVersionRequest, resp *CheckTerraformVersionResponse) { + + if req.TerraformVersion.Equal(s.version) { + resp.Skip = fmt.Sprintf("Terraform CLI version is %s: skipping test.", s.version) + } +} diff --git a/tfversion/skip_if_test.go b/tfversion/skip_if_test.go new file mode 100644 index 000000000..8e9a8be9e --- /dev/null +++ b/tfversion/skip_if_test.go @@ -0,0 +1,56 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package tfversion_test + +import ( + "testing" + + "github.com/hashicorp/go-version" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + + r "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/tfversion" + + testinginterface "github.com/mitchellh/go-testing-interface" +) + +func Test_SkipIf_SkipTest(t *testing.T) { //nolint:paralleltest + t.Setenv("TF_ACC_TERRAFORM_VERSION", "1.4.3") + + r.UnitTest(t, r.TestCase{ + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": func() (tfprotov6.ProviderServer, error) { //nolint:unparam // required signature + return nil, nil + }, + }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipIf(version.Must(version.NewVersion("1.4.3"))), + }, + Steps: []r.TestStep{ + { + Config: `//non-empty config`, + }, + }, + }) +} + +func Test_SkipIf_RunTest(t *testing.T) { //nolint:paralleltest + t.Setenv("TF_ACC_TERRAFORM_VERSION", "1.1.0") + + r.UnitTest(&testinginterface.RuntimeT{}, r.TestCase{ + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": func() (tfprotov6.ProviderServer, error) { //nolint:unparam // required signature + return nil, nil + }, + }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipIf(version.Must(version.NewVersion("1.2.0"))), + }, + Steps: []r.TestStep{ + { + Config: `//non-empty config`, + }, + }, + }) +} diff --git a/tfversion/version_check.go b/tfversion/version_check.go new file mode 100644 index 000000000..554ec2247 --- /dev/null +++ b/tfversion/version_check.go @@ -0,0 +1,39 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package tfversion + +import ( + "context" + + "github.com/hashicorp/go-version" +) + +// TerraformVersionCheck is the interface for writing check logic against the Terraform CLI version. +// The Terraform CLI version is determined by the binary selected by the TF_ACC_TERRAFORM_PATH environment +// variable value, installed by the TF_ACC_TERRAFORM_VERSION value, or already existing based on the PATH environment +// variable. This logic is executed at the beginning of the TestCase before any TestStep is executed. +// +// This package contains some built-in functionality that implements the interface, otherwise consumers can use this +// interface for implementing their own custom logic. +type TerraformVersionCheck interface { + // CheckTerraformVersion should implement the logic to either pass, error (failing the test), or skip (passing the test). + CheckTerraformVersion(context.Context, CheckTerraformVersionRequest, *CheckTerraformVersionResponse) +} + +// CheckTerraformVersionRequest is the request received for the CheckTerraformVersion method of the +// TerraformVersionCheck interface. The response of that method is CheckTerraformVersionResponse. +type CheckTerraformVersionRequest struct { + // TerraformVersion is the version associated with the selected Terraform CLI binary. + TerraformVersion *version.Version +} + +// CheckTerraformVersionResponse is the response returned for the CheckTerraformVersion method of the +// TerraformVersionCheck interface. The request of that method is CheckTerraformVersionRequest. +type CheckTerraformVersionResponse struct { + // Error will result in failing the test with a given error message. + Error error + + // Skip will result in passing the test with a given skip message. + Skip string +} diff --git a/tfversion/versions.go b/tfversion/versions.go new file mode 100644 index 000000000..6cd04b27f --- /dev/null +++ b/tfversion/versions.go @@ -0,0 +1,32 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package tfversion + +import "github.com/hashicorp/go-version" + +// Common use version variables to simplify provider testing implementations. +// This list is not intended to be exhaustive of all Terraform versions, +// however these should at least include cases where Terraform +// introduced new configuration language features. +var ( + // Version0_12_26 is the first Terraform CLI version supported + // by the testing code. + Version0_12_26 *version.Version = version.Must(version.NewVersion("0.12.26")) + + // Major versions + + Version1_0_0 *version.Version = version.Must(version.NewVersion("1.0.0")) + Version2_0_0 *version.Version = version.Must(version.NewVersion("2.0.0")) + + // Minor versions + + Version0_13_0 *version.Version = version.Must(version.NewVersion("0.13.0")) + Version0_14_0 *version.Version = version.Must(version.NewVersion("0.14.0")) + Version0_15_0 *version.Version = version.Must(version.NewVersion("0.15.0")) + Version1_1_0 *version.Version = version.Must(version.NewVersion("1.1.0")) + Version1_2_0 *version.Version = version.Must(version.NewVersion("1.2.0")) + Version1_3_0 *version.Version = version.Must(version.NewVersion("1.3.0")) + Version1_4_0 *version.Version = version.Must(version.NewVersion("1.4.0")) + Version1_5_0 *version.Version = version.Must(version.NewVersion("1.5.0")) +) diff --git a/website/data/plugin-testing-nav-data.json b/website/data/plugin-testing-nav-data.json index a3699b093..5e6468792 100644 --- a/website/data/plugin-testing-nav-data.json +++ b/website/data/plugin-testing-nav-data.json @@ -17,6 +17,10 @@ "title": "Test Steps", "path": "acceptance-tests/teststep" }, + { + "title": "Terraform Version Checks", + "path": "acceptance-tests/tfversion-checks" + }, { "title": "Plan Checks", "path": "acceptance-tests/plan-checks" diff --git a/website/docs/plugin/testing/acceptance-tests/testcase.mdx b/website/docs/plugin/testing/acceptance-tests/testcase.mdx index 9614e2af3..98aa9586c 100644 --- a/website/docs/plugin/testing/acceptance-tests/testcase.mdx +++ b/website/docs/plugin/testing/acceptance-tests/testcase.mdx @@ -144,6 +144,39 @@ func testAccPreCheck(t *testing.T) { } ``` +### TerraformVersionChecks + +**Type:** [`[]tfversion.TerraformVersionCheck`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/tfversion#TerraformVersionCheck) + +**Default:** `nil` + +**Required:** no + +**TerraformVersionChecks** if non-nil, will be called after any defined PreChecks +but before any test steps are executed. The [Terraform Version Checks](/terraform/plugin/testing/acceptance-tests/tfversion-checks) +are generic checks that check logic against the Terraform CLI version and can +immediately pass or fail a test before any test steps are executed. + +The tfversion package provides built-in checks for common scenarios. + +**Example usage:** + +```go +// File: example/widget_test.go +package example + +func TestAccExampleWidget_basic(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_1_0), // built-in check from tfversion package + }, + // ... + }) +} + +``` + ### Providers **Type:** [`map[string]*schema.Provider`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema#Provider) @@ -168,6 +201,9 @@ package example func TestAccExampleWidget_basic(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_1_0), + }, Providers: testAccProviders, // ... }) @@ -211,6 +247,9 @@ package example func TestAccExampleWidget_basic(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_1_0), + }, Providers: testAccProviders, CheckDestroy: testAccCheckExampleResourceDestroy, // ... @@ -279,6 +318,9 @@ package example func TestAccExampleWidget_basic(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_1_0), + }, Providers: testAccProviders, CheckDestroy: testAccCheckExampleResourceDestroy, Steps: []resource.TestStep{ diff --git a/website/docs/plugin/testing/acceptance-tests/tfversion-checks.mdx b/website/docs/plugin/testing/acceptance-tests/tfversion-checks.mdx new file mode 100644 index 000000000..cb2865b7a --- /dev/null +++ b/website/docs/plugin/testing/acceptance-tests/tfversion-checks.mdx @@ -0,0 +1,259 @@ +--- +page_title: 'Plugin Development - Acceptance Testing: Terraform Version Checks' +description: >- + Terraform Version Checks are generic checks defined at the TestCase level that check logic against the Terraform CLI version. The testing module + provides built-in Version Checks for common use-cases, but custom Version Checks can also be implemented. +--- + +# Terraform Version Checks + +**Terraform Version Checks** are generic checks defined at the TestCase level that check logic against the Terraform CLI version. The checks are executed at the beginning of the TestCase before any TestStep is executed. + +The Terraform CLI version is determined by the binary selected by the [`TF_ACC_TERRAFORM_PATH`](/terraform/plugin/testing/acceptance-tests#environment-variables) environment variable value, installed by the [`TF_ACC_TERRAFORM_VERSION`](/terraform/plugin/testing/acceptance-tests#environment-variables) value, or already existing based on the `PATH` environment variable. + +A **version check** will either return an error and fail the associated test, return a skip message and pass the associated test immediately by skipping, or it will return nothing and allow the associated test to run. + +## Built-in Version Checks and Variables + +The `terraform-plugin-testing` module provides a package [`tfversion`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/tfversion) with built-in version checks for common use-cases. There are three types of version checks: Skip Checks, Require Checks, and Collection Checks. + +## Version Variables + +The built-in checks in the `tfversion` package typically require the use of the [`github.com/hashicorp/go-version`](https://pkg.go.dev/github.com/hashicorp/go-version) module [`version.Version`](https://pkg.go.dev/github.com/hashicorp/go-version#Version) type. To simplify provider testing implementations, the `tfversion` package provides [built-in variables](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/tfversion#pkg-variables) for common use case versions, such as each released minor and major Terraform version. These follow the pattern of `Version{MAJOR}_{MINOR}_{PATCH}` with the major, minor, and patch version numbers, such as `Version1_2_0`. + +### Skip Version Checks + +Skip Version Checks will pass the associated test by skipping and provide a skip message if the detected Terraform CLI version satisfies the specified check criteria. + +| Check | Description | +|---------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------| +| [`tfversion.SkipAbove(maximumVersion)`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/tfversion#SkipAbove) | Skips the test if the Terraform CLI version is above the given maximum. | +| [`tfversion.SkipBelow(minimumVersion)`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/tfversion#SkipBelow) | Skips the test if the Terraform CLI version is below the given minimum. | +| [`tfversion.SkipBetween(minimumVersion, maximumVersion)`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/tfversion#SkipBetween) | Skips the test if the Terraform CLI version is between the given minimum (inclusive) and maximum (exclusive). | +| [`tfversion.SkipIf(version)`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/tfversion#SkipIf) | Skips the test if the Terraform CLI version matches the given version. | + +#### Example using `tfversion.SkipBetween` + +The built-in [`tfversion.SkipBetween`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/tfversion#SkipBetween) version check is useful for skipping all patch versions associated with a minor version. + +In the following example, we have written a test that skips all Terraform CLI patch versions associated with 0.14.0: + +```go +package example_test + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/tfversion" +) + +func Test_Skip_TF14(t *testing.T) { + t.Parallel() + + resource.UnitTest(t, resource.TestCase{ + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": func() (tfprotov6.ProviderServer, error) { + return nil, nil + }, + }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBetween(tfversion.Version0_14_0, tfversion.Version0_15_0), + }, + Steps: []resource.TestStep{ + { + Config: `//example test config`, + }, + }, + }) +} +``` + +### Require Version Checks + +Require Version Checks will raise an error and fail the associated test if the detected Terraform CLI version does not satisfy the specified check requirements. + +| Check | Description | +|---------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------| +| [`tfversion.RequireAbove(minimumVersion)`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/tfversion#RequireAbove) | Fails the test if the Terraform CLI version is below the given maximum. | +| [`tfversion.RequireBelow(maximumVersion)`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/tfversion#RequireBelow) | Fails the test if the Terraform CLI version is above the given minimum. | +| [`tfversion.RequireBetween(minimumVersion, maximumVersion)`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/tfversion#RequireBetween) | Fails the test if the Terraform CLI version is outside the given minimum (exclusive) and maximum (inclusive). | +| [`tfversion.RequireNot(version)`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/tfversion#RequireNot) | Fails the test if the Terraform CLI version matches the given version. | + + +#### Example using `tfversion.RequireAbove` + +The built-in [`tfversion.RequireAbove`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/tfversion#RequireAbove) version check is useful for required tests that may use features only available in newer versions of the Terraform ClI. + +In the following example, the test Terraform configuration uses the `nullable` argument for an input variable, a feature that is only available in Terraform CLI versions `1.3.0` and above. The version check will fail the test with a specific error if the detected version is below `1.3.0`. + +```go +package example_test + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/tfversion" +) + +func Test_Require_TF1_3(t *testing.T) { + t.Parallel() + + resource.UnitTest(t, resource.TestCase{ + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": func() (tfprotov6.ProviderServer, error) { + return nil, nil + }, + }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireAbove(tfversion.Version1_3_0), + }, + Steps: []resource.TestStep{ + { + Config: `variable "a" { + nullable = true + default = "hello" + }`, + }, + }, + }) +} +``` + +### Collection Version Checks + +Collection Version Checks operate on multiple version checks and can be used to create more complex checks. + +[`tfversion.Any(TerraformVersionChecks...)`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/tfversion#Any) will run the associated test by returning a nil error and empty skip message +if any of the given version sub-checks return a nil error and empty skip message. If none of the sub-checks return a nil error and empty skip message, then the check will return all sub-check errors and fail the associated test. +Otherwise, if none of the sub-checks return a non-nil error, the check will pass the associated test by skipping and return all sub-check skip messages. + +[`tfversion.All(TerraformVersionChecks...)`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/tfversion#All) will either fail or skip the associated test if any of the given sub-checks return a non-nil error or non-empty skip message. The check will return the +first non-nil error or non-empty skip message from the given sub-checks in the order that they are given. Otherwise, if all sub-checks return a nil error and empty skip message, then the check will return a nil error and empty skip message and run the associated test. This check should only be +used in conjunction with `tfversion.Any()` as the behavior provided by this check is applied to the `TerraformVersionChecks` field by default. + +#### Example using `tfversion.Any` + +In the following example, the test will only run if either the Terraform CLI version is above `1.2.0` or if it's below `1.0.0` but not version `0.15.0`, otherwise an error will be returned. + +```go +package example_test + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/tfversion" +) + +func Test_Any(t *testing.T) { + t.Parallel() + + resource.UnitTest(t, resource.TestCase{ + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": func() (tfprotov6.ProviderServer, error) { //nolint:unparam // required signature + return nil, nil + }, + }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.Any( + tfversion.All( + tfversion.RequireNot(tfversion.Version0_15_0), + tfversion.RequireBelow(tfversion.Version1_0_0), + ), + tfversion.RequireAbove(tfversion.Version1_2_0), + ), + }, + Steps: []resource.TestStep{ + { + Config: `//example test config`, + }, + }, + }) +} +``` + + +## Custom Version Checks + +The package [`tfversion`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/tfversion) also provides the [`TerraformVersionCheck`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/tfversion#TerraformVersionCheck) interface, which can be implemented for a custom version check. + +The [`tfversion.CheckTerraformVersionRequest`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/tfversion#CheckTerraformVersionRequest) has a `TerraformVersion` field of type [`*version.Version`](https://pkg.go.dev/github.com/hashicorp/go-version#Version) which contains the version of the Terraform CLI binary running the test. + +The [`tfversion.CheckTerraformVersionResponse`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/tfversion#CheckTerraformVersionResponse) has an `Error` field and a `Skip` field. The behavior of the version check depends on which field is populated. Populating the `Error` field will fail the associated test with the given error. +Populating the `Skip` field will pass the associated test by skipping the test with the given skip message. Only one of these fields should be populated. + +Here is an example implementation of a version check returns an error if the detected Terraform CLI version matches the given version: + +```go +package example_test + +import ( + "context" + "fmt" + + "github.com/hashicorp/go-version" +) + +// Ensure implementation satisfies the tfversion.TerraformVersionCheck interface. +var _ tfversion.TerraformVersionCheck = requireNotCheck{} + +// RequireNot will fail the test if the given version matches. +func RequireNot(v *version.Version) tfversion.TerraformVersionCheck { + return requireNotCheck{ + version: v, + } +} + +type requireNotCheck struct { + version *version.Version +} + +func (s requireNotCheck) CheckTerraformVersion(ctx context.Context, req tfversion.CheckTerraformVersionRequest, resp *tfversion.CheckTerraformVersionResponse) { + if req.TerraformVersion.Equal(s.version) { + resp.Error = fmt.Errorf("unexpected Terraform CLI version: %s", s.version) + } +} +``` + +And example usage: + +```go +package example_test + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/tfversion" +) + +func Test_RequireNot(t *testing.T) { + t.Parallel() + + resource.UnitTest(t, resource.TestCase{ + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": func() (tfprotov6.ProviderServer, error) { + return nil, nil + }, + }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireNot(tfversion.Version0_13_0), + }, + Steps: []resource.TestStep{ + { + Config: `//example test config`, + }, + }, + }) +} +```