From a08dfecc57eefcbdff0127b4615ac797ea008d1a Mon Sep 17 00:00:00 2001 From: kt Date: Wed, 7 Oct 2026 10:47:51 -0700 Subject: [PATCH 1/3] changelog and readme --- CHANGELOG.md | 5 + README.md | 19 +++ cli/pr-ast.go | 37 ++---- cli/pr.go | 38 ++---- integration/integration_azurerm_test.go | 123 ++++++++++++++++++ .../batch/actions/batch_pool_resize/action.go | 19 +++ .../actions/batch_pool_resize/action_test.go | 25 ++++ .../internal/services/batch/parse/pool.go | 24 ++++ .../internal/services/batch/registration.go | 37 ++++++ .../resources/batch_account/data_source.go | 22 ++++ .../batch_account/data_source_test.go | 25 ++++ .../batch/resources/batch_account/r_schema.go | 24 ++++ .../batch/resources/batch_account/r_update.go | 20 +++ .../batch/resources/batch_account/resource.go | 26 ++++ .../resources/batch_account/resource_test.go | 36 +++++ .../batch/resources/batch_pool/r_read.go | 22 ++++ .../batch/resources/batch_pool/r_schema.go | 20 +++ .../batch/resources/batch_pool/resource.go | 16 +++ .../resources/batch_pool/resource_test.go | 25 ++++ .../services/batch/validate/account_name.go | 22 ++++ .../services/batch/validate/pool_id.go | 23 ++++ .../batch/2024-07-01/pool/id_pool.go | 23 ++++ lib/provider/file-types.go | 5 +- lib/provider/file.go | 24 ++++ lib/provider/resource_folder.go | 42 ++++++ lib/provider/resource_folder_test.go | 76 +++++++++++ 26 files changed, 722 insertions(+), 56 deletions(-) create mode 100644 integration/testdata/azurerm/internal/services/batch/actions/batch_pool_resize/action.go create mode 100644 integration/testdata/azurerm/internal/services/batch/actions/batch_pool_resize/action_test.go create mode 100644 integration/testdata/azurerm/internal/services/batch/parse/pool.go create mode 100644 integration/testdata/azurerm/internal/services/batch/registration.go create mode 100644 integration/testdata/azurerm/internal/services/batch/resources/batch_account/data_source.go create mode 100644 integration/testdata/azurerm/internal/services/batch/resources/batch_account/data_source_test.go create mode 100644 integration/testdata/azurerm/internal/services/batch/resources/batch_account/r_schema.go create mode 100644 integration/testdata/azurerm/internal/services/batch/resources/batch_account/r_update.go create mode 100644 integration/testdata/azurerm/internal/services/batch/resources/batch_account/resource.go create mode 100644 integration/testdata/azurerm/internal/services/batch/resources/batch_account/resource_test.go create mode 100644 integration/testdata/azurerm/internal/services/batch/resources/batch_pool/r_read.go create mode 100644 integration/testdata/azurerm/internal/services/batch/resources/batch_pool/r_schema.go create mode 100644 integration/testdata/azurerm/internal/services/batch/resources/batch_pool/resource.go create mode 100644 integration/testdata/azurerm/internal/services/batch/resources/batch_pool/resource_test.go create mode 100644 integration/testdata/azurerm/internal/services/batch/validate/account_name.go create mode 100644 integration/testdata/azurerm/internal/services/batch/validate/pool_id.go create mode 100644 integration/testdata/azurerm/vendor/github.com/hashicorp/go-azure-sdk/resource-manager/batch/2024-07-01/pool/id_pool.go create mode 100644 lib/provider/resource_folder.go create mode 100644 lib/provider/resource_folder_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index d9c5e45..36aaaca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## Unreleased + +- support the azurerm resource-per-folder layout (`internal/services/batch/resources/batch_account/{resource,r_create,data_source}.go`, and `actions/` alongside `resources/`): a change to any file in a resource folder now discovers the tests in that folder, and import tracing from helper packages follows into resource folders +- API discovery lists directories at the PR's merge commit rather than the default branch, so tests are found in directories the PR adds or moves files into + ## v1.3.5 (2026-10-20) - build with Go 1.26.9 for its fixes to the standard library's HTTP and TLS code, mostly ways a misbehaving connection could stall or crash the program (CVE-2026-78667, CVE-2026-78669, CVE-2026-97031 and others); govulncheck finds tctest reaches several of them, and the binary now scans clean ([#152](https://github.com/katbyte/tctest/pull/152)) diff --git a/README.md b/README.md index 8013ae7..b57ef2d 100755 --- a/README.md +++ b/README.md @@ -337,6 +337,25 @@ When no test regex is provided, `tctest` automatically discovers tests by: Files in `/client/`, `/parse/`, `/validate/` subdirectories and `registration.go`/`resourceids.go` are automatically skipped. Deleted files are also excluded. +### Resource Folders + +The azurerm resource-per-folder layout gives each resource (or action) its own package under `resources/` (or `actions/`) in the service, and drops the resource name from the file names: + +``` +internal/services/batch/resources/batch_account/ + resource.go r_create.go r_schema.go data_source.go list.go + resource_test.go resource_identity_gen_test.go data_source_test.go list_test.go +internal/services/managedredis/actions/managed_redis_flush_databases/ + action.go action_test.go +``` + +Any file directly inside `/resources//` or `/actions//` belongs to that folder's resource. Since the file names no longer say which resource they belong to, the folder does: + +- a change to `data_source.go`, `list.go`, `action.go` or `ephemeral.go` discovers that file's own tests (`data_source_test.go`, ...) +- a change to any other file in the folder (`resource.go`, `r_*.go`, shared models) discovers every test file in the folder + +Helper packages such as `parse/` and `validate/` are unaffected, and both layouts can be mixed within a service. The service (and so the build triggered) is still the first directory: `batch`. + ### Test Output Discovered tests are grouped by service and displayed with padded service names for alignment: diff --git a/cli/pr-ast.go b/cli/pr-ast.go index 3c0525d..f01ed66 100644 --- a/cli/pr-ast.go +++ b/cli/pr-ast.go @@ -176,26 +176,7 @@ func (dc *AstDiscoveryContext) findLocalTestFiles(relativeDir string, resourcePr continue } - matched := false - for _, prefix := range resourcePrefixes { - if !strings.HasPrefix(pf.BaseName, prefix) { - continue - } - remainder := pf.BaseName[len(prefix):] - - for _, re := range dc.Config.AccTestFileSuffixRegexes { - if re.MatchString(remainder) { - matched = true - break - } - } - - if matched { - break - } - } - - if matched { + if pf.IsTestFor(resourcePrefixes, dc.Config.AccTestFileSuffixRegexes) { testFiles = append(testFiles, pf) } } @@ -329,7 +310,7 @@ func (dc *AstDiscoveryContext) traceImportsToResourceFiles(helperFiles []provide // if we have no symbol info, fall back to package-level matching if len(symbols) == 0 { - if dc.Config.FileRegEx.MatchString(relPath) { + if dc.isResourceFile(relPath) { dir := filepath.ToSlash(filepath.Dir(relPath)) result[dir] = append(result[dir], relPath) clog.Log.Debugf(" traced: %s imports %s (depth %d, package-level)", relPath, pkgPath, depth+1) @@ -369,7 +350,7 @@ func (dc *AstDiscoveryContext) traceImportsToResourceFiles(helperFiles []provide } // this file uses a changed symbol - if dc.Config.FileRegEx.MatchString(relPath) { + if dc.isResourceFile(relPath) { // it's a resource file — add to results dir := filepath.ToSlash(filepath.Dir(relPath)) result[dir] = append(result[dir], relPath) @@ -397,6 +378,12 @@ func (dc *AstDiscoveryContext) traceImportsToResourceFiles(helperFiles []provide return result } +// isResourceFile returns true if the repo-relative path is a file whose sibling tests should run when it is affected: +// it matches the fileregex (resource at the service root), or it lives in a resource folder. +func (dc *AstDiscoveryContext) isResourceFile(relPath string) bool { + return dc.Config.FileRegEx.MatchString(relPath) || provider.InResourceFolder(relPath) +} + func (dc *AstDiscoveryContext) SortedTestFiles() []*provider.File { files := make([]*provider.File, 0, len(dc.TestFiles)) for _, pf := range dc.TestFiles { @@ -512,7 +499,7 @@ func (dc *AstDiscoveryContext) TraceHelperFiles(helperFiles []provider.File) { if entries, err := os.ReadDir(filepath.Join(dc.RepoPath, dir)); err == nil { for _, entry := range entries { if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".go") { - if dc.Config.FileRegEx.MatchString(filepath.ToSlash(filepath.Join(dir, entry.Name()))) { + if dc.isResourceFile(filepath.ToSlash(filepath.Join(dir, entry.Name()))) { isSamePkg = true break } @@ -560,7 +547,7 @@ func (dc *AstDiscoveryContext) TraceHelperFiles(helperFiles []provider.File) { continue } relPath := filepath.ToSlash(filepath.Join(dir, entry.Name())) - if !dc.Config.FileRegEx.MatchString(relPath) { + if !dc.isResourceFile(relPath) { continue } @@ -742,7 +729,7 @@ func (dc *AstDiscoveryContext) TraceVendorFiles(vendorFiles []provider.File) { } relPath = filepath.ToSlash(relPath) - if !dc.Config.FileRegEx.MatchString(relPath) { + if !dc.isResourceFile(relPath) { return nil } diff --git a/cli/pr.go b/cli/pr.go index e6140be..3ad991c 100755 --- a/cli/pr.go +++ b/cli/pr.go @@ -94,7 +94,7 @@ func (ghr GithubRepo) PrTestsFromAPI(pri int, cfg DiscoveryConfig) (map[string][ } clog.Log.Tracef("listing files...") - filesFiltered, err := ghr.GetPullRequestTestFiles(pri, cfg) + filesFiltered, err := ghr.GetPullRequestTestFiles(pri, pr.GetMergeCommitSHA(), cfg) if err != nil { return nil, fmt.Errorf("failed to get PR files for %s/%s/pull/%d: %w", ghr.Owner, ghr.Name, pri, err) } @@ -192,7 +192,9 @@ func (f *FlagData) CheckPrCanBuild(number int) error { // GetPullRequestTestFiles fetches all changed files in a PR and determines the related test files. // It classifies files based on the DiscoveryConfig and lists contents of directories containing changed resources to find related tests. -func (ghr GithubRepo) GetPullRequestTestFiles(pri int, cfg DiscoveryConfig) ([]provider.File, error) { +// Directories are listed at ref (the PR's merge commit, the same commit file contents are downloaded from) so directories +// the PR adds or moves files into are seen; an empty ref lists the default branch. +func (ghr GithubRepo) GetPullRequestTestFiles(pri int, ref string, cfg DiscoveryConfig) ([]provider.File, error) { // track resource files that need sibling test file discovery // key: directory path, value: list of resource prefixes (e.g. "foo") resourcePrefixesByPackage := map[string][]string{} @@ -280,13 +282,12 @@ func (ghr GithubRepo) GetPullRequestTestFiles(pri int, cfg DiscoveryConfig) ([]p client, ctx := ghr.NewClient() for dir, prefixes := range resourcePrefixesByPackage { clog.Log.Debugf(" listing directory %s for related test files...", dir) - _, dirContents, _, err := client.Repositories.GetContents(ctx, ghr.Owner, ghr.Name, dir, nil) + _, dirContents, _, err := client.Repositories.GetContents(ctx, ghr.Owner, ghr.Name, dir, &github.RepositoryContentGetOptions{Ref: ref}) if err != nil { - // a directory new in this PR won't exist on the default branch; anything else - // would silently drop derived sibling test files + // a missing directory is not fatal; anything else would silently drop derived sibling test files var ghErr *github.ErrorResponse if errors.As(err, &ghErr) && ghErr.Response != nil && ghErr.Response.StatusCode == http.StatusNotFound { - clog.Log.Debugf(" directory %s not found (new in this PR?), skipping sibling test discovery", dir) + clog.Log.Debugf(" directory %s not found, skipping sibling test discovery", dir) continue } return nil, fmt.Errorf("failed to list directory %s for related test files: %w", dir, err) @@ -294,30 +295,7 @@ func (ghr GithubRepo) GetPullRequestTestFiles(pri int, cfg DiscoveryConfig) ([]p for _, entry := range dirContents { pf := provider.NewFile(path.Join(dir, entry.GetName())) - if pf.Type != provider.FileTypeTest { - continue - } - - shouldInclude := false - for _, resource := range prefixes { - if !strings.HasPrefix(pf.BaseName, resource) { - continue - } - - remainder := pf.BaseName[len(resource):] - for _, testSuffix := range cfg.AccTestFileSuffixRegexes { - if testSuffix.MatchString(remainder) { - shouldInclude = true - break - } - } - - if shouldInclude { - break - } - } - - if !shouldInclude { + if pf.Type != provider.FileTypeTest || !pf.IsTestFor(prefixes, cfg.AccTestFileSuffixRegexes) { continue } diff --git a/integration/integration_azurerm_test.go b/integration/integration_azurerm_test.go index f3b4e35..2898eb4 100644 --- a/integration/integration_azurerm_test.go +++ b/integration/integration_azurerm_test.go @@ -46,6 +46,21 @@ var azurermPRs = []prDef{ {1021, "open", "dns bug fix", []changedFile{ {"internal/services/dns/dns_a_record_resource.go", "modified", ""}, }}, + {1030, "open", "resource folder: part of the resource changed", []changedFile{ + {"internal/services/batch/resources/batch_account/r_update.go", "modified", ""}, + }}, + {1031, "open", "resource folder: data source changed", []changedFile{ + {"internal/services/batch/resources/batch_account/data_source.go", "modified", ""}, + }}, + {1032, "open", "resource folder: helper package beside the folders changed", []changedFile{ + {"internal/services/batch/validate/account_name.go", "modified", ""}, + }}, + {1033, "open", "action folder changed", []changedFile{ + {"internal/services/batch/actions/batch_pool_resize/action.go", "modified", ""}, + }}, + {1034, "open", "resource folder: test file changed", []changedFile{ + {"internal/services/batch/resources/batch_account/resource_test.go", "modified", "@@ -26,3 +26,3 @@"}, + }}, } // azurermASTPRs are served for the AST-mode tests; the merge refs for these @@ -90,6 +105,27 @@ var azurermASTPRs = []prDef{ {2012, "open", "conflicted pr", []changedFile{ {"internal/services/postgres/postgresql_flexible_server_resource_test.go", "modified", ""}, }}, + {2013, "open", "resource folder: part of the resource changed", []changedFile{ + {"internal/services/batch/resources/batch_account/r_update.go", "modified", ""}, + }}, + {2014, "open", "resource folder: data source changed", []changedFile{ + {"internal/services/batch/resources/batch_account/data_source.go", "modified", ""}, + }}, + {2015, "open", "resource folder: helper package beside the folders changed", []changedFile{ + {"internal/services/batch/validate/account_name.go", "modified", ""}, + }}, + {2016, "open", "action folder changed", []changedFile{ + {"internal/services/batch/actions/batch_pool_resize/action.go", "modified", ""}, + }}, + {2017, "open", "resource folder: test file changed", []changedFile{ + {"internal/services/batch/resources/batch_account/resource_test.go", "modified", "@@ -26,3 +26,3 @@"}, + }}, + {2018, "open", "resource folder: two-level helper chain changed", []changedFile{ + {"internal/services/batch/parse/pool.go", "modified", ""}, + }}, + {2019, "open", "resource folder: vendored dependency changed", []changedFile{ + {"vendor/github.com/hashicorp/go-azure-sdk/resource-manager/batch/2024-07-01/pool/id_pool.go", "modified", ""}, + }}, } // conflictedPRs marks the PR numbers the mock serves with mergeable=false @@ -233,11 +269,50 @@ func TestAPIDiscoveryAzureRM(t *testing.T) { name: "--service all triggers every service directly", args: []string{"pr", "1001", "--service", "all", "--all"}, want: []trigger{ + {"TF_E2E_BATCH", "refs/pull/1001/merge", "TestAcc"}, {"TF_E2E_COSMOS", "refs/pull/1001/merge", "TestAcc"}, {"TF_E2E_DNS", "refs/pull/1001/merge", "TestAcc"}, {"TF_E2E_POSTGRES", "refs/pull/1001/merge", "TestAcc"}, }, }, + { + // resource-per-folder layout: files drop the resource name (batch/resources/batch_account/r_update.go), so the + // folder rather than a filename prefix ties them to their tests — and the pool folder next door is left alone + name: "resource folder file runs every test in its folder", + args: []string{"pr", "1030"}, + want: []trigger{{"TF_E2E_BATCH", "refs/pull/1030/merge", "(TestAccBatchAccount|TestAccBatchAccountDataSource)"}}, + }, + { + name: "resource folder data source runs only the data source tests", + args: []string{"pr", "1031"}, + want: []trigger{{"TF_E2E_BATCH", "refs/pull/1031/merge", "(TestAccBatchAccountDataSource)"}}, + }, + { + name: "helper package beside resource folders is not mistaken for one", + args: []string{"pr", "1032"}, + want: nil, + wantExit: 1, + }, + { + name: "action folder runs the action tests", + args: []string{"pr", "1033"}, + want: []trigger{{"TF_E2E_BATCH", "refs/pull/1033/merge", "(TestAccBatchPoolResizeAction)"}}, + }, + { + name: "resource folder test file runs its tests", + args: []string{"pr", "1034"}, + want: []trigger{{"TF_E2E_BATCH", "refs/pull/1034/merge", "(TestAccBatchAccount)"}}, + }, + { + name: "individual flag narrows resource folder test file to the modified test functions", + args: []string{"pr", "1034", "-i"}, + want: []trigger{{"TF_E2E_BATCH", "refs/pull/1034/merge", "(TestAccBatchAccount_update)"}}, + }, + { + name: "individual flag lists every test in the resource folder by full name", + args: []string{"pr", "1030", "-i"}, + want: []trigger{{"TF_E2E_BATCH", "refs/pull/1030/merge", "(TestAccBatchAccountDataSource_basic|TestAccBatchAccount_basic|TestAccBatchAccount_update)"}}, + }, } for _, tt := range cases { @@ -374,6 +449,54 @@ func TestASTDiscoveryAzureRM(t *testing.T) { want: nil, wantExit: 1, }, + { + name: "resource folder file runs every test in its folder", + args: []string{"pr", "2013"}, + want: []trigger{{"TF_E2E_BATCH", "refs/pull/2013/merge", "(TestAccBatchAccount|TestAccBatchAccountDataSource)"}}, + }, + { + name: "resource folder data source runs only the data source tests", + args: []string{"pr", "2014"}, + want: []trigger{{"TF_E2E_BATCH", "refs/pull/2014/merge", "(TestAccBatchAccountDataSource)"}}, + }, + { + // validate.AccountName is only used by batch_account/r_schema.go, so the pool folder is left alone + name: "cross-package helper traces into the resource folder using it", + args: []string{"pr", "2015"}, + want: []trigger{{"TF_E2E_BATCH", "refs/pull/2015/merge", "(TestAccBatchAccount|TestAccBatchAccountDataSource)"}}, + }, + { + name: "action folder runs the action tests", + args: []string{"pr", "2016"}, + want: []trigger{{"TF_E2E_BATCH", "refs/pull/2016/merge", "(TestAccBatchPoolResizeAction)"}}, + }, + { + name: "resource folder test file runs its tests", + args: []string{"pr", "2017"}, + want: []trigger{{"TF_E2E_BATCH", "refs/pull/2017/merge", "(TestAccBatchAccount)"}}, + }, + { + name: "individual flag narrows resource folder test file to the modified test functions", + args: []string{"pr", "2017", "-i"}, + want: []trigger{{"TF_E2E_BATCH", "refs/pull/2017/merge", "(TestAccBatchAccount_update)"}}, + }, + { + name: "individual flag lists every test in the resource folder by full name", + args: []string{"pr", "2013", "-i"}, + want: []trigger{{"TF_E2E_BATCH", "refs/pull/2013/merge", "(TestAccBatchAccountDataSource_basic|TestAccBatchAccount_basic|TestAccBatchAccount_update)"}}, + }, + { + // parse.PoolID is used by validate.PoolID, which only batch_pool/r_schema.go uses; the account folder + // imports the same validate package for a different function — it must NOT be selected + name: "two-level helper chain traces into the resource folder using it", + args: []string{"pr", "2018"}, + want: []trigger{{"TF_E2E_BATCH", "refs/pull/2018/merge", "(TestAccBatchPool)"}}, + }, + { + name: "vendored dependency change traces into the resource folder importing it", + args: []string{"pr", "2019"}, + want: []trigger{{"TF_E2E_BATCH", "refs/pull/2019/merge", "(TestAccBatchPool)"}}, + }, } for _, tt := range cases { diff --git a/integration/testdata/azurerm/internal/services/batch/actions/batch_pool_resize/action.go b/integration/testdata/azurerm/internal/services/batch/actions/batch_pool_resize/action.go new file mode 100644 index 0000000..53ae654 --- /dev/null +++ b/integration/testdata/azurerm/internal/services/batch/actions/batch_pool_resize/action.go @@ -0,0 +1,19 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package batch_pool_resize + +import ( + "github.com/hashicorp/terraform-plugin-framework/action" + "github.com/hashicorp/terraform-provider-azurerm/internal/sdk" +) + +type BatchPoolResizeAction struct { + sdk.ActionMetadata +} + +var _ sdk.Action = &BatchPoolResizeAction{} + +func Action() action.Action { + return &BatchPoolResizeAction{} +} diff --git a/integration/testdata/azurerm/internal/services/batch/actions/batch_pool_resize/action_test.go b/integration/testdata/azurerm/internal/services/batch/actions/batch_pool_resize/action_test.go new file mode 100644 index 0000000..9ce58b8 --- /dev/null +++ b/integration/testdata/azurerm/internal/services/batch/actions/batch_pool_resize/action_test.go @@ -0,0 +1,25 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package batch_pool_resize_test + +import ( + "testing" + + "github.com/hashicorp/terraform-provider-azurerm/internal/acceptance" +) + +type BatchPoolResizeAction struct{} + +func TestAccBatchPoolResizeAction_basic(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_batch_pool_resize", "test") + a := BatchPoolResizeAction{} + + data.ResourceTest(t, a, []acceptance.TestStep{ + {Config: a.basic(data)}, + }) +} + +func (BatchPoolResizeAction) basic(data acceptance.TestData) string { + return "" +} diff --git a/integration/testdata/azurerm/internal/services/batch/parse/pool.go b/integration/testdata/azurerm/internal/services/batch/parse/pool.go new file mode 100644 index 0000000..4cb48d8 --- /dev/null +++ b/integration/testdata/azurerm/internal/services/batch/parse/pool.go @@ -0,0 +1,24 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package parse + +import ( + "fmt" + "strings" +) + +type PoolId struct { + AccountName string + Name string +} + +// PoolID parses "/" into a PoolId struct +func PoolID(input string) (*PoolId, error) { + account, name, found := strings.Cut(input, "/") + if !found { + return nil, fmt.Errorf("parsing %q as a Pool ID", input) + } + + return &PoolId{AccountName: account, Name: name}, nil +} diff --git a/integration/testdata/azurerm/internal/services/batch/registration.go b/integration/testdata/azurerm/internal/services/batch/registration.go new file mode 100644 index 0000000..28bc304 --- /dev/null +++ b/integration/testdata/azurerm/internal/services/batch/registration.go @@ -0,0 +1,37 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package batch + +import ( + "github.com/hashicorp/terraform-plugin-framework/action" + "github.com/hashicorp/terraform-provider-azurerm/internal/sdk" + "github.com/hashicorp/terraform-provider-azurerm/internal/services/batch/actions/batch_pool_resize" + "github.com/hashicorp/terraform-provider-azurerm/internal/services/batch/resources/batch_account" + "github.com/hashicorp/terraform-provider-azurerm/internal/services/batch/resources/batch_pool" +) + +type Registration struct{} + +func (r Registration) Name() string { + return "Batch" +} + +func (r Registration) Resources() []sdk.Resource { + return []sdk.Resource{ + batch_account.BatchAccountResource{}, + batch_pool.BatchPoolResource{}, + } +} + +func (r Registration) DataSources() []sdk.DataSource { + return []sdk.DataSource{ + batch_account.BatchAccountDataSource{}, + } +} + +func (r Registration) Actions() []func() action.Action { + return []func() action.Action{ + batch_pool_resize.Action, + } +} diff --git a/integration/testdata/azurerm/internal/services/batch/resources/batch_account/data_source.go b/integration/testdata/azurerm/internal/services/batch/resources/batch_account/data_source.go new file mode 100644 index 0000000..e7221ef --- /dev/null +++ b/integration/testdata/azurerm/internal/services/batch/resources/batch_account/data_source.go @@ -0,0 +1,22 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package batch_account + +import ( + "time" + + "github.com/hashicorp/terraform-provider-azurerm/internal/sdk" +) + +type BatchAccountDataSource struct{} + +var _ sdk.DataSource = BatchAccountDataSource{} + +func (r BatchAccountDataSource) ResourceType() string { + return "azurerm_batch_account" +} + +func (r BatchAccountDataSource) Read() sdk.ResourceFunc { + return sdk.ResourceFunc{Timeout: 5 * time.Minute} +} diff --git a/integration/testdata/azurerm/internal/services/batch/resources/batch_account/data_source_test.go b/integration/testdata/azurerm/internal/services/batch/resources/batch_account/data_source_test.go new file mode 100644 index 0000000..efa3258 --- /dev/null +++ b/integration/testdata/azurerm/internal/services/batch/resources/batch_account/data_source_test.go @@ -0,0 +1,25 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package batch_account_test + +import ( + "testing" + + "github.com/hashicorp/terraform-provider-azurerm/internal/acceptance" +) + +type BatchAccountDataSource struct{} + +func TestAccBatchAccountDataSource_basic(t *testing.T) { + data := acceptance.BuildTestData(t, "data.azurerm_batch_account", "test") + r := BatchAccountDataSource{} + + data.DataSourceTest(t, []acceptance.TestStep{ + {Config: r.basic(data)}, + }) +} + +func (BatchAccountDataSource) basic(data acceptance.TestData) string { + return "" +} diff --git a/integration/testdata/azurerm/internal/services/batch/resources/batch_account/r_schema.go b/integration/testdata/azurerm/internal/services/batch/resources/batch_account/r_schema.go new file mode 100644 index 0000000..19c4022 --- /dev/null +++ b/integration/testdata/azurerm/internal/services/batch/resources/batch_account/r_schema.go @@ -0,0 +1,24 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package batch_account + +import ( + "github.com/hashicorp/terraform-provider-azurerm/internal/services/batch/validate" + "github.com/hashicorp/terraform-provider-azurerm/internal/tf/pluginsdk" +) + +type BatchAccountModel struct { + Name string `tfschema:"name"` +} + +func (r BatchAccountResource) Arguments() map[string]*pluginsdk.Schema { + return map[string]*pluginsdk.Schema{ + "name": { + Type: pluginsdk.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validate.AccountName, + }, + } +} diff --git a/integration/testdata/azurerm/internal/services/batch/resources/batch_account/r_update.go b/integration/testdata/azurerm/internal/services/batch/resources/batch_account/r_update.go new file mode 100644 index 0000000..6caf834 --- /dev/null +++ b/integration/testdata/azurerm/internal/services/batch/resources/batch_account/r_update.go @@ -0,0 +1,20 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package batch_account + +import ( + "context" + "time" + + "github.com/hashicorp/terraform-provider-azurerm/internal/sdk" +) + +func (r BatchAccountResource) Update() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Timeout: 30 * time.Minute, + Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error { + return nil + }, + } +} diff --git a/integration/testdata/azurerm/internal/services/batch/resources/batch_account/resource.go b/integration/testdata/azurerm/internal/services/batch/resources/batch_account/resource.go new file mode 100644 index 0000000..4212984 --- /dev/null +++ b/integration/testdata/azurerm/internal/services/batch/resources/batch_account/resource.go @@ -0,0 +1,26 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package batch_account + +import ( + "time" + + "github.com/hashicorp/terraform-provider-azurerm/internal/sdk" +) + +type BatchAccountResource struct{} + +var _ sdk.ResourceWithUpdate = BatchAccountResource{} + +func (r BatchAccountResource) ResourceType() string { + return "azurerm_batch_account" +} + +func (r BatchAccountResource) ModelObject() interface{} { + return &BatchAccountModel{} +} + +func (r BatchAccountResource) Read() sdk.ResourceFunc { + return sdk.ResourceFunc{Timeout: 5 * time.Minute} +} diff --git a/integration/testdata/azurerm/internal/services/batch/resources/batch_account/resource_test.go b/integration/testdata/azurerm/internal/services/batch/resources/batch_account/resource_test.go new file mode 100644 index 0000000..ad3bf99 --- /dev/null +++ b/integration/testdata/azurerm/internal/services/batch/resources/batch_account/resource_test.go @@ -0,0 +1,36 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package batch_account_test + +import ( + "testing" + + "github.com/hashicorp/terraform-provider-azurerm/internal/acceptance" +) + +type BatchAccountResource struct{} + +func TestAccBatchAccount_basic(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_batch_account", "test") + r := BatchAccountResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + {Config: r.basic(data)}, + data.ImportStep(), + }) +} + +func TestAccBatchAccount_update(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_batch_account", "test") + r := BatchAccountResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + {Config: r.basic(data)}, + data.ImportStep(), + }) +} + +func (BatchAccountResource) basic(data acceptance.TestData) string { + return "" +} diff --git a/integration/testdata/azurerm/internal/services/batch/resources/batch_pool/r_read.go b/integration/testdata/azurerm/internal/services/batch/resources/batch_pool/r_read.go new file mode 100644 index 0000000..45e6691 --- /dev/null +++ b/integration/testdata/azurerm/internal/services/batch/resources/batch_pool/r_read.go @@ -0,0 +1,22 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package batch_pool + +import ( + "context" + "time" + + "github.com/hashicorp/go-azure-sdk/resource-manager/batch/2024-07-01/pool" + "github.com/hashicorp/terraform-provider-azurerm/internal/sdk" +) + +func (r BatchPoolResource) Read() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Timeout: 5 * time.Minute, + Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error { + _, err := pool.ParsePoolID(metadata.ResourceData.Id()) + return err + }, + } +} diff --git a/integration/testdata/azurerm/internal/services/batch/resources/batch_pool/r_schema.go b/integration/testdata/azurerm/internal/services/batch/resources/batch_pool/r_schema.go new file mode 100644 index 0000000..6753b7c --- /dev/null +++ b/integration/testdata/azurerm/internal/services/batch/resources/batch_pool/r_schema.go @@ -0,0 +1,20 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package batch_pool + +import ( + "github.com/hashicorp/terraform-provider-azurerm/internal/services/batch/validate" + "github.com/hashicorp/terraform-provider-azurerm/internal/tf/pluginsdk" +) + +func (r BatchPoolResource) Arguments() map[string]*pluginsdk.Schema { + return map[string]*pluginsdk.Schema{ + "pool_id": { + Type: pluginsdk.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validate.PoolID, + }, + } +} diff --git a/integration/testdata/azurerm/internal/services/batch/resources/batch_pool/resource.go b/integration/testdata/azurerm/internal/services/batch/resources/batch_pool/resource.go new file mode 100644 index 0000000..d2312d4 --- /dev/null +++ b/integration/testdata/azurerm/internal/services/batch/resources/batch_pool/resource.go @@ -0,0 +1,16 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package batch_pool + +import ( + "github.com/hashicorp/terraform-provider-azurerm/internal/sdk" +) + +type BatchPoolResource struct{} + +var _ sdk.Resource = BatchPoolResource{} + +func (r BatchPoolResource) ResourceType() string { + return "azurerm_batch_pool" +} diff --git a/integration/testdata/azurerm/internal/services/batch/resources/batch_pool/resource_test.go b/integration/testdata/azurerm/internal/services/batch/resources/batch_pool/resource_test.go new file mode 100644 index 0000000..9a396dd --- /dev/null +++ b/integration/testdata/azurerm/internal/services/batch/resources/batch_pool/resource_test.go @@ -0,0 +1,25 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package batch_pool_test + +import ( + "testing" + + "github.com/hashicorp/terraform-provider-azurerm/internal/acceptance" +) + +type BatchPoolResource struct{} + +func TestAccBatchPool_basic(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_batch_pool", "test") + r := BatchPoolResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + {Config: r.basic(data)}, + }) +} + +func (BatchPoolResource) basic(data acceptance.TestData) string { + return "" +} diff --git a/integration/testdata/azurerm/internal/services/batch/validate/account_name.go b/integration/testdata/azurerm/internal/services/batch/validate/account_name.go new file mode 100644 index 0000000..2aacfbb --- /dev/null +++ b/integration/testdata/azurerm/internal/services/batch/validate/account_name.go @@ -0,0 +1,22 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package validate + +import ( + "fmt" + "regexp" +) + +func AccountName(i interface{}, k string) (warnings []string, errors []error) { + v, ok := i.(string) + if !ok { + errors = append(errors, fmt.Errorf("expected type of %q to be string", k)) + return + } + + if !regexp.MustCompile("^[a-z0-9]{3,24}$").MatchString(v) { + errors = append(errors, fmt.Errorf("%q must be 3 to 24 lowercase letters and numbers", k)) + } + return +} diff --git a/integration/testdata/azurerm/internal/services/batch/validate/pool_id.go b/integration/testdata/azurerm/internal/services/batch/validate/pool_id.go new file mode 100644 index 0000000..5ebaf1d --- /dev/null +++ b/integration/testdata/azurerm/internal/services/batch/validate/pool_id.go @@ -0,0 +1,23 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package validate + +import ( + "fmt" + + "github.com/hashicorp/terraform-provider-azurerm/internal/services/batch/parse" +) + +func PoolID(i interface{}, k string) (warnings []string, errors []error) { + v, ok := i.(string) + if !ok { + errors = append(errors, fmt.Errorf("expected type of %q to be string", k)) + return + } + + if _, err := parse.PoolID(v); err != nil { + errors = append(errors, err) + } + return +} diff --git a/integration/testdata/azurerm/vendor/github.com/hashicorp/go-azure-sdk/resource-manager/batch/2024-07-01/pool/id_pool.go b/integration/testdata/azurerm/vendor/github.com/hashicorp/go-azure-sdk/resource-manager/batch/2024-07-01/pool/id_pool.go new file mode 100644 index 0000000..6efc23c --- /dev/null +++ b/integration/testdata/azurerm/vendor/github.com/hashicorp/go-azure-sdk/resource-manager/batch/2024-07-01/pool/id_pool.go @@ -0,0 +1,23 @@ +// Copyright (c) HashiCorp Inc. All rights reserved. +// Licensed under the MIT License. See NOTICE.txt in the project root for license information. + +package pool + +import ( + "fmt" + "strings" +) + +type PoolId struct { + BatchAccountName string + PoolName string +} + +func ParsePoolID(input string) (*PoolId, error) { + account, name, found := strings.Cut(input, "/pools/") + if !found { + return nil, fmt.Errorf("parsing %q as a Pool ID", input) + } + + return &PoolId{BatchAccountName: account, PoolName: name}, nil +} diff --git a/lib/provider/file-types.go b/lib/provider/file-types.go index c1c7d9f..c15051d 100644 --- a/lib/provider/file-types.go +++ b/lib/provider/file-types.go @@ -10,7 +10,7 @@ type FileType int const ( FileTypeOther FileType = iota // outside service directories - FileTypeResource // matches fileregex (resource or data source at service root) + FileTypeResource // matches fileregex (resource or data source at service root), or lives in a resource folder FileTypeHelper // in service dir but doesn't match fileregex (e.g. parse/, validate/, client/) FileTypeTest // _test.go with TestAcc functions FileTypeUnitTest // _test.go without TestAcc functions @@ -49,6 +49,9 @@ func DefaultClassifier(f *File) FileType { } if f.InServicePackage() { + if InResourceFolder(f.RelPath) { + return FileTypeResource + } if strings.HasSuffix(f.Name, "_resource.go") || strings.HasSuffix(f.Name, "_data_source.go") { return FileTypeResource } diff --git a/lib/provider/file.go b/lib/provider/file.go index 30ed174..4bba348 100644 --- a/lib/provider/file.go +++ b/lib/provider/file.go @@ -115,10 +115,34 @@ func (f *File) InServicePackage() bool { // ResourcePrefix returns the prefix used for test file discovery. // For "batch_account_resource.go" → "batch_account". // For "batch_account_data_source.go" → "batch_account_data_source". +// Files in a resource folder are matched by folder rather than name, see resourceFolderPrefix. func (f *File) ResourcePrefix() string { + if InResourceFolder(f.RelPath) { + return f.resourceFolderPrefix() + } return strings.TrimSuffix(f.BaseName, "_resource") } +// IsTestFor returns true if this file's name is one of the resource prefixes followed by an acceptance test file +// suffix, e.g. "batch_account" + "_resource_test". An empty prefix matches any test file (see ResourcePrefix). +func (f *File) IsTestFor(prefixes []string, suffixes []*regexp.Regexp) bool { + for _, prefix := range prefixes { + if prefix == "" { + return true + } + if !strings.HasPrefix(f.BaseName, prefix) { + continue + } + remainder := f.BaseName[len(prefix):] + for _, re := range suffixes { + if re.MatchString(remainder) { + return true + } + } + } + return false +} + // IsMigrationFile returns true if this file lives inside a known state migration subdirectory // of a service package (e.g. internal/services/storage/migration/storage_queue_v1_to_v2.go). func (f *File) IsMigrationFile() bool { diff --git a/lib/provider/resource_folder.go b/lib/provider/resource_folder.go new file mode 100644 index 0000000..b0b593e --- /dev/null +++ b/lib/provider/resource_folder.go @@ -0,0 +1,42 @@ +package provider + +import ( + "slices" + "strings" +) + +// ResourceFolderGroups are the directories below a service that hold one folder per resource in the resource-per-folder +// layout. Every file directly inside one of those folders belongs to that resource, whatever it is called: +// +// internal/services/batch/resources/batch_account/{resource.go, r_create.go, r_schema.go, data_source.go, list.go, resource_test.go} +// internal/services/managedredis/actions/managed_redis_flush_databases/{action.go, action_test.go} +var ResourceFolderGroups = []string{"resources", "actions"} + +// resourceFolderOwnTests are the files in a resource folder that have tests of their own (data_source.go → +// data_source_test.go), so a change to one only needs those. A change to any other file in the folder (resource.go, r_*.go, +// shared models) runs every test in the folder. +var resourceFolderOwnTests = []string{"data_source.go", "list.go", "action.go", "ephemeral.go"} + +// InResourceFolder returns true if relPath sits directly inside a resource folder: +// internal/services////, where group is one of ResourceFolderGroups. +func InResourceFolder(relPath string) bool { + for _, prefix := range ServiceDirPrefixes { + _, rest, found := strings.Cut("/"+relPath, "/"+prefix+"/") + if !found { + continue + } + parts := strings.Split(rest, "/") // /// + return len(parts) == 4 && slices.Contains(ResourceFolderGroups, parts[1]) + } + return false +} + +// resourceFolderPrefix is ResourcePrefix for a file in a resource folder. The folder already scopes the resource, so only +// the files with tests of their own keep a prefix ("data_source.go" → "data_source"); everything else returns "", which +// IsTestFor treats as every test file in the folder. +func (f *File) resourceFolderPrefix() string { + if slices.Contains(resourceFolderOwnTests, f.Name) { + return f.BaseName + } + return "" +} diff --git a/lib/provider/resource_folder_test.go b/lib/provider/resource_folder_test.go new file mode 100644 index 0000000..dffd690 --- /dev/null +++ b/lib/provider/resource_folder_test.go @@ -0,0 +1,76 @@ +package provider + +import ( + "regexp" + "slices" + "testing" +) + +func TestInResourceFolder(t *testing.T) { + t.Parallel() + + for path, want := range map[string]bool{ + "internal/services/batch/resources/batch_account/resource.go": true, + "internal/services/batch/resources/batch_account/r_create.go": true, + "internal/services/batch/resources/batch_account/resource_test.go": true, + "internal/services/managedredis/actions/managed_redis_flush_databases/action.go": true, + "internal/service/ec2/resources/vpc/resource.go": true, + + "internal/services/batch/batch_account_resource.go": false, // flat layout + "internal/services/batch/validate/account_name.go": false, // helper package + "internal/services/logic/parse/action.go": false, // helper package that happens to hold an action.go + "internal/services/batch/account/resource.go": false, // one level down, not in a group + "internal/services/batch/resources/helpers.go": false, // in the group but not in a folder + "internal/services/batch/resources/batch_account/migration/v0_to_v1.go": false, // below the folder + "internal/acceptance/resources/thing/resource.go": false, // not in a service + } { + if got := InResourceFolder(path); got != want { + t.Errorf("InResourceFolder(%s) = %v, want %v", path, got, want) + } + } +} + +func TestResourceFolderTests(t *testing.T) { + t.Parallel() + + suffixes := []*regexp.Regexp{ + regexp.MustCompile(`^_resource.*_test$`), + regexp.MustCompile(`^_test$`), + regexp.MustCompile(`^_list_test$`), + regexp.MustCompile(`^_identity_gen_test$`), + regexp.MustCompile(`^_data_source_test$`), + } + const folder = "internal/services/batch/resources/batch_account/" + folderTests := []string{"data_source_test", "list_test", "resource_identity_gen_test", "resource_test"} + + for _, tc := range []struct { + changed string + prefix string + tests []string + }{ + {changed: "resource.go", prefix: "", tests: folderTests}, + {changed: "r_update.go", prefix: "", tests: folderTests}, + {changed: "batch_account.go", prefix: "", tests: folderTests}, + {changed: "data_source.go", prefix: "data_source", tests: []string{"data_source_test"}}, + {changed: "list.go", prefix: "list", tests: []string{"list_test"}}, + } { + f := NewFile(folder + tc.changed) + if f.Type != FileTypeResource { + t.Errorf("%s: type = %s, want [RESOURCE]", tc.changed, f.TypeLabel()) + } + if got := f.ResourcePrefix(); got != tc.prefix { + t.Errorf("%s: ResourcePrefix() = %q, want %q", tc.changed, got, tc.prefix) + } + + var got []string + for _, name := range folderTests { + test := NewFile(folder + name + ".go") + if test.IsTestFor([]string{f.ResourcePrefix()}, suffixes) { + got = append(got, name) + } + } + if !slices.Equal(got, tc.tests) { + t.Errorf("%s: matched tests %v, want %v", tc.changed, got, tc.tests) + } + } +} From 8876215c11322bf1e17e0a57afd4b5ff91b9f9bd Mon Sep 17 00:00:00 2001 From: kt Date: Wed, 21 Oct 2026 11:43:53 -0700 Subject: [PATCH 2/3] resource folders: run the folder's tests when its shared test setup or migration changes A change to only a resource folder's test_helpers_test.go found no tests: it holds the configs the folder's tests build on but no tests of its own. 53 azurerm folders have one. It now counts as part of the resource, so a change runs every test in the folder. In API mode, a change to a resource folder's migration/ package found no tests either: the test prefix was guessed from the migration's file name (feature_resource_v0_to_v1.go to feature_resource), which no file in the folder starts with. Inside a resource folder it is now every test in that folder. AST mode already found them by following resource.go's import. --- CHANGELOG.md | 2 +- README.md | 1 + integration/integration_azurerm_test.go | 35 +++++++++++++++++++ integration/integration_test.go | 2 +- .../migration/account_resource_v0_to_v1.go | 6 ++++ .../batch/resources/batch_account/resource.go | 5 +++ .../batch_account/test_helpers_test.go | 13 +++++++ lib/provider/file-types.go | 4 +++ lib/provider/file.go | 6 ++++ lib/provider/resource_folder.go | 23 ++++++++++-- lib/provider/resource_folder_test.go | 24 +++++++++++++ 11 files changed, 116 insertions(+), 5 deletions(-) create mode 100644 integration/testdata/azurerm/internal/services/batch/resources/batch_account/migration/account_resource_v0_to_v1.go create mode 100644 integration/testdata/azurerm/internal/services/batch/resources/batch_account/test_helpers_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 36aaaca..867132d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ ## Unreleased -- support the azurerm resource-per-folder layout (`internal/services/batch/resources/batch_account/{resource,r_create,data_source}.go`, and `actions/` alongside `resources/`): a change to any file in a resource folder now discovers the tests in that folder, and import tracing from helper packages follows into resource folders +- support the azurerm resource-per-folder layout (`internal/services/batch/resources/batch_account/{resource,r_create,data_source}.go`, and `actions/` alongside `resources/`): a change to any file in a resource folder, its shared `test_helpers_test.go` or its `migration/` package now discovers the tests in that folder, and import tracing from helper packages follows into resource folders - API discovery lists directories at the PR's merge commit rather than the default branch, so tests are found in directories the PR adds or moves files into ## v1.3.5 (2026-10-20) diff --git a/README.md b/README.md index b57ef2d..959070b 100755 --- a/README.md +++ b/README.md @@ -353,6 +353,7 @@ Any file directly inside `/resources//` or `/actions/< - a change to `data_source.go`, `list.go`, `action.go` or `ephemeral.go` discovers that file's own tests (`data_source_test.go`, ...) - a change to any other file in the folder (`resource.go`, `r_*.go`, shared models) discovers every test file in the folder +- so does a change to the folder's `test_helpers_test.go`, which holds no tests of its own, only the configs the folder's tests share, or to its `migration/` package Helper packages such as `parse/` and `validate/` are unaffected, and both layouts can be mixed within a service. The service (and so the build triggered) is still the first directory: `batch`. diff --git a/integration/integration_azurerm_test.go b/integration/integration_azurerm_test.go index 2898eb4..9ad5aa2 100644 --- a/integration/integration_azurerm_test.go +++ b/integration/integration_azurerm_test.go @@ -61,6 +61,12 @@ var azurermPRs = []prDef{ {1034, "open", "resource folder: test file changed", []changedFile{ {"internal/services/batch/resources/batch_account/resource_test.go", "modified", "@@ -26,3 +26,3 @@"}, }}, + {1035, "open", "resource folder: shared test setup changed", []changedFile{ + {"internal/services/batch/resources/batch_account/test_helpers_test.go", "modified", ""}, + }}, + {1036, "open", "resource folder: state migration changed", []changedFile{ + {"internal/services/batch/resources/batch_account/migration/account_resource_v0_to_v1.go", "modified", ""}, + }}, } // azurermASTPRs are served for the AST-mode tests; the merge refs for these @@ -126,6 +132,12 @@ var azurermASTPRs = []prDef{ {2019, "open", "resource folder: vendored dependency changed", []changedFile{ {"vendor/github.com/hashicorp/go-azure-sdk/resource-manager/batch/2024-07-01/pool/id_pool.go", "modified", ""}, }}, + {2020, "open", "resource folder: shared test setup changed", []changedFile{ + {"internal/services/batch/resources/batch_account/test_helpers_test.go", "modified", ""}, + }}, + {2021, "open", "resource folder: state migration changed", []changedFile{ + {"internal/services/batch/resources/batch_account/migration/account_resource_v0_to_v1.go", "modified", ""}, + }}, } // conflictedPRs marks the PR numbers the mock serves with mergeable=false @@ -313,6 +325,18 @@ func TestAPIDiscoveryAzureRM(t *testing.T) { args: []string{"pr", "1030", "-i"}, want: []trigger{{"TF_E2E_BATCH", "refs/pull/1030/merge", "(TestAccBatchAccountDataSource_basic|TestAccBatchAccount_basic|TestAccBatchAccount_update)"}}, }, + { + // test_helpers_test.go has no tests of its own, only the config the folder's tests build on + name: "resource folder shared test setup runs every test in its folder", + args: []string{"pr", "1035"}, + want: []trigger{{"TF_E2E_BATCH", "refs/pull/1035/merge", "(TestAccBatchAccount|TestAccBatchAccountDataSource)"}}, + }, + { + // the migration's file name (account_resource_v0_to_v1.go) does not match the folder's test file names, the folder does + name: "resource folder state migration runs every test in its folder", + args: []string{"pr", "1036"}, + want: []trigger{{"TF_E2E_BATCH", "refs/pull/1036/merge", "(TestAccBatchAccount|TestAccBatchAccountDataSource)"}}, + }, } for _, tt := range cases { @@ -497,6 +521,17 @@ func TestASTDiscoveryAzureRM(t *testing.T) { args: []string{"pr", "2019"}, want: []trigger{{"TF_E2E_BATCH", "refs/pull/2019/merge", "(TestAccBatchPool)"}}, }, + { + name: "resource folder shared test setup runs every test in its folder", + args: []string{"pr", "2020"}, + want: []trigger{{"TF_E2E_BATCH", "refs/pull/2020/merge", "(TestAccBatchAccount|TestAccBatchAccountDataSource)"}}, + }, + { + // traced through resource.go, which imports the migration package + name: "resource folder state migration runs every test in its folder", + args: []string{"pr", "2021"}, + want: []trigger{{"TF_E2E_BATCH", "refs/pull/2021/merge", "(TestAccBatchAccount|TestAccBatchAccountDataSource)"}}, + }, } for _, tt := range cases { diff --git a/integration/integration_test.go b/integration/integration_test.go index a1a6c25..af089b0 100644 --- a/integration/integration_test.go +++ b/integration/integration_test.go @@ -64,7 +64,7 @@ func TestMain(m *testing.M) { // build the git upstreams used by the AST-mode tests azurermUpstream = filepath.Join(tmp, "azurerm-upstream") - if err := buildGitUpstream("testdata/azurerm", azurermUpstream, 2000, 2020); err != nil { + if err := buildGitUpstream("testdata/azurerm", azurermUpstream, 2000, 2025); err != nil { fmt.Fprintf(os.Stderr, "integration test setup: git fixture: %v\n", err) return 1 } diff --git a/integration/testdata/azurerm/internal/services/batch/resources/batch_account/migration/account_resource_v0_to_v1.go b/integration/testdata/azurerm/internal/services/batch/resources/batch_account/migration/account_resource_v0_to_v1.go new file mode 100644 index 0000000..cfceeff --- /dev/null +++ b/integration/testdata/azurerm/internal/services/batch/resources/batch_account/migration/account_resource_v0_to_v1.go @@ -0,0 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package migration + +type AccountV0ToV1 struct{} diff --git a/integration/testdata/azurerm/internal/services/batch/resources/batch_account/resource.go b/integration/testdata/azurerm/internal/services/batch/resources/batch_account/resource.go index 4212984..0b6f431 100644 --- a/integration/testdata/azurerm/internal/services/batch/resources/batch_account/resource.go +++ b/integration/testdata/azurerm/internal/services/batch/resources/batch_account/resource.go @@ -7,6 +7,7 @@ import ( "time" "github.com/hashicorp/terraform-provider-azurerm/internal/sdk" + "github.com/hashicorp/terraform-provider-azurerm/internal/services/batch/resources/batch_account/migration" ) type BatchAccountResource struct{} @@ -24,3 +25,7 @@ func (r BatchAccountResource) ModelObject() interface{} { func (r BatchAccountResource) Read() sdk.ResourceFunc { return sdk.ResourceFunc{Timeout: 5 * time.Minute} } + +func (r BatchAccountResource) StateUpgraders() []interface{} { + return []interface{}{migration.AccountV0ToV1{}} +} diff --git a/integration/testdata/azurerm/internal/services/batch/resources/batch_account/test_helpers_test.go b/integration/testdata/azurerm/internal/services/batch/resources/batch_account/test_helpers_test.go new file mode 100644 index 0000000..ccbac68 --- /dev/null +++ b/integration/testdata/azurerm/internal/services/batch/resources/batch_account/test_helpers_test.go @@ -0,0 +1,13 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package batch_account_test + +import ( + "github.com/hashicorp/terraform-provider-azurerm/internal/acceptance" +) + +// template is config the folder's tests build on; a file like this holds no tests of its own +func (BatchAccountResource) template(data acceptance.TestData) string { + return "" +} diff --git a/lib/provider/file-types.go b/lib/provider/file-types.go index c15051d..baf0268 100644 --- a/lib/provider/file-types.go +++ b/lib/provider/file-types.go @@ -34,6 +34,10 @@ func DefaultClassifier(f *File) FileType { } if strings.HasSuffix(f.RelPath, "_test.go") { + if f.isResourceFolderSharedTest() { + return FileTypeResource // no tests of its own, only setup for the folder's tests, so a change runs all of them + } + // if we have local path/content, do an in-depth detection for acceptance vs unit test if content, err := f.GetContent(); err == nil { if strings.Contains(string(content), "func TestAcc") { diff --git a/lib/provider/file.go b/lib/provider/file.go index 4bba348..b4412e3 100644 --- a/lib/provider/file.go +++ b/lib/provider/file.go @@ -5,6 +5,7 @@ package provider import ( "fmt" "os" + "path" "path/filepath" "regexp" "slices" @@ -152,7 +153,12 @@ func (f *File) IsMigrationFile() bool { // MigrationResourcePrefix extracts the resource name from a migration filename by stripping // the version-transition suffix (e.g. "_v0_to_v1"). // For "storage_queue_v1_to_v2" -> "storage_queue". +// A migration package inside a resource folder returns "", every test in that folder, as the folder already names the resource. func (f *File) MigrationResourcePrefix() string { + if isResourceFolder(path.Dir(path.Dir(f.RelPath))) { + return "" + } + return regexp.MustCompile(`_v\d+_to_v\d+$`).ReplaceAllString(f.BaseName, "") } diff --git a/lib/provider/resource_folder.go b/lib/provider/resource_folder.go index b0b593e..577f78a 100644 --- a/lib/provider/resource_folder.go +++ b/lib/provider/resource_folder.go @@ -1,6 +1,7 @@ package provider import ( + "path" "slices" "strings" ) @@ -17,20 +18,36 @@ var ResourceFolderGroups = []string{"resources", "actions"} // shared models) runs every test in the folder. var resourceFolderOwnTests = []string{"data_source.go", "list.go", "action.go", "ephemeral.go"} +// resourceFolderSharedTests are the test files in a resource folder that hold no tests, only setup the folder's tests share +// (test_helpers_test.go holds the configs they build on). They count as part of the resource, so a change to one runs every +// test in the folder. +var resourceFolderSharedTests = []string{"test_helpers_test.go"} + // InResourceFolder returns true if relPath sits directly inside a resource folder: // internal/services////, where group is one of ResourceFolderGroups. func InResourceFolder(relPath string) bool { + return isResourceFolder(path.Dir(relPath)) +} + +// isResourceFolder returns true if dir is a resource folder: internal/services///. +func isResourceFolder(dir string) bool { for _, prefix := range ServiceDirPrefixes { - _, rest, found := strings.Cut("/"+relPath, "/"+prefix+"/") + _, rest, found := strings.Cut("/"+dir, "/"+prefix+"/") if !found { continue } - parts := strings.Split(rest, "/") // /// - return len(parts) == 4 && slices.Contains(ResourceFolderGroups, parts[1]) + parts := strings.Split(rest, "/") // // + return len(parts) == 3 && slices.Contains(ResourceFolderGroups, parts[1]) } return false } +// isResourceFolderSharedTest returns true if f is a test file holding setup its resource folder's tests share, see +// resourceFolderSharedTests. +func (f *File) isResourceFolderSharedTest() bool { + return slices.Contains(resourceFolderSharedTests, f.Name) && InResourceFolder(f.RelPath) +} + // resourceFolderPrefix is ResourcePrefix for a file in a resource folder. The folder already scopes the resource, so only // the files with tests of their own keep a prefix ("data_source.go" → "data_source"); everything else returns "", which // IsTestFor treats as every test file in the folder. diff --git a/lib/provider/resource_folder_test.go b/lib/provider/resource_folder_test.go index dffd690..d274f55 100644 --- a/lib/provider/resource_folder_test.go +++ b/lib/provider/resource_folder_test.go @@ -53,6 +53,7 @@ func TestResourceFolderTests(t *testing.T) { {changed: "batch_account.go", prefix: "", tests: folderTests}, {changed: "data_source.go", prefix: "data_source", tests: []string{"data_source_test"}}, {changed: "list.go", prefix: "list", tests: []string{"list_test"}}, + {changed: "test_helpers_test.go", prefix: "", tests: folderTests}, // setup the folder's tests share } { f := NewFile(folder + tc.changed) if f.Type != FileTypeResource { @@ -74,3 +75,26 @@ func TestResourceFolderTests(t *testing.T) { } } } + +func TestResourceFolderSharedTestsOutsideAFolder(t *testing.T) { + t.Parallel() + + // only a resource folder's test_helpers_test.go is setup for the folder; anywhere else it is an ordinary test file + if f := NewFile("internal/services/batch/test_helpers_test.go"); f.Type != FileTypeTest { + t.Errorf("type = %s, want [TEST]", f.TypeLabel()) + } +} + +func TestMigrationResourcePrefix(t *testing.T) { + t.Parallel() + + for path, want := range map[string]string{ + "internal/services/storage/migration/storage_queue_v1_to_v2.go": "storage_queue", + "internal/services/appconfiguration/resources/app_configuration_feature/migration/feature_resource_v0_to_v1.go": "", // the folder names the resource + } { + f := NewFile(path) + if got := f.MigrationResourcePrefix(); got != want { + t.Errorf("MigrationResourcePrefix(%s) = %q, want %q", path, got, want) + } + } +} From e4236d16cb3b07953f27a102d46a83e768e46060 Mon Sep 17 00:00:00 2001 From: kt Date: Mon, 26 Oct 2026 20:19:23 -0700 Subject: [PATCH 3/3] resource folders: a package nested in a folder (migration/) is always a helper, so its folder's tests run in both modes azurerm's fileregex matches everything under resources/, and its newest refactor PRs name migration files like notification_hub_resource.go. Both turned a folder's migration/ files into resource files with no tests beside them, so a migration-only change triggered nothing in API or AST mode. A file in a package nested inside a resource folder is now a helper whatever its name or the regex says, the AST resource check asks the classifier instead of re-doing the regex, and the azurerm integration suite runs with azurerm's real regex and gains a migration file named like a resource. --- README.md | 2 + cli/flags.go | 4 +- cli/pr-ast.go | 19 +++++---- integration/integration_azurerm_test.go | 42 +++++++++++++------ .../migration/account_resource.go | 7 ++++ .../batch/resources/batch_account/resource.go | 2 +- lib/provider/file-types.go | 5 ++- lib/provider/file.go | 3 +- lib/provider/resource_folder.go | 12 ++++++ lib/provider/resource_folder_test.go | 35 ++++++++++++++++ 10 files changed, 104 insertions(+), 27 deletions(-) create mode 100644 integration/testdata/azurerm/internal/services/batch/resources/batch_account/migration/account_resource.go diff --git a/README.md b/README.md index 959070b..7bd2a45 100755 --- a/README.md +++ b/README.md @@ -357,6 +357,8 @@ Any file directly inside `/resources//` or `/actions/< Helper packages such as `parse/` and `validate/` are unaffected, and both layouts can be mixed within a service. The service (and so the build triggered) is still the first directory: `batch`. +Inside a resource folder the layout decides, not `--fileregex` or the file name: azurerm's regex matches every file under `resources/`, and a migration file may be called `notification_hub_resource.go`; neither turns a `migration/` file into a resource file whose own directory is searched for tests. + ### Test Output Discovered tests are grouped by service and displayed with padded service names for alignment: diff --git a/cli/flags.go b/cli/flags.go index fcb41ff..15fed82 100755 --- a/cli/flags.go +++ b/cli/flags.go @@ -333,7 +333,9 @@ func GetFlags() *FlagData { // Set the global File classifier using the configured regex provider.Classifier = func(file *provider.File) provider.FileType { t := provider.DefaultClassifier(file) - if t == provider.FileTypeHelper && f.DiscoveryConfig.FileRegEx != nil && f.DiscoveryConfig.FileRegEx.MatchString(file.RelPath) { + // the regex never promotes a package nested inside a resource folder (its migration/): those stay helpers so the folder's + // tests are reached through them, and azurerm's regex is loose enough to match everything under resources/ + if t == provider.FileTypeHelper && f.DiscoveryConfig.FileRegEx != nil && f.DiscoveryConfig.FileRegEx.MatchString(file.RelPath) && !provider.BelowResourceFolder(file.RelPath) { return provider.FileTypeResource } return t diff --git a/cli/pr-ast.go b/cli/pr-ast.go index f01ed66..b47257b 100644 --- a/cli/pr-ast.go +++ b/cli/pr-ast.go @@ -310,7 +310,7 @@ func (dc *AstDiscoveryContext) traceImportsToResourceFiles(helperFiles []provide // if we have no symbol info, fall back to package-level matching if len(symbols) == 0 { - if dc.isResourceFile(relPath) { + if isResourceFile(relPath) { dir := filepath.ToSlash(filepath.Dir(relPath)) result[dir] = append(result[dir], relPath) clog.Log.Debugf(" traced: %s imports %s (depth %d, package-level)", relPath, pkgPath, depth+1) @@ -350,7 +350,7 @@ func (dc *AstDiscoveryContext) traceImportsToResourceFiles(helperFiles []provide } // this file uses a changed symbol - if dc.isResourceFile(relPath) { + if isResourceFile(relPath) { // it's a resource file — add to results dir := filepath.ToSlash(filepath.Dir(relPath)) result[dir] = append(result[dir], relPath) @@ -378,10 +378,11 @@ func (dc *AstDiscoveryContext) traceImportsToResourceFiles(helperFiles []provide return result } -// isResourceFile returns true if the repo-relative path is a file whose sibling tests should run when it is affected: -// it matches the fileregex (resource at the service root), or it lives in a resource folder. -func (dc *AstDiscoveryContext) isResourceFile(relPath string) bool { - return dc.Config.FileRegEx.MatchString(relPath) || provider.InResourceFolder(relPath) +// isResourceFile returns true if the repo-relative path is a file whose sibling tests should run when it is affected: one the +// classifier calls a resource, as it matches the fileregex at the service root or lives in a resource folder. A package nested +// inside a folder (migration/) is a helper, so tracing carries on through it to the folder. +func isResourceFile(relPath string) bool { + return provider.NewFile(relPath).Type == provider.FileTypeResource } func (dc *AstDiscoveryContext) SortedTestFiles() []*provider.File { @@ -499,7 +500,7 @@ func (dc *AstDiscoveryContext) TraceHelperFiles(helperFiles []provider.File) { if entries, err := os.ReadDir(filepath.Join(dc.RepoPath, dir)); err == nil { for _, entry := range entries { if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".go") { - if dc.isResourceFile(filepath.ToSlash(filepath.Join(dir, entry.Name()))) { + if isResourceFile(filepath.ToSlash(filepath.Join(dir, entry.Name()))) { isSamePkg = true break } @@ -547,7 +548,7 @@ func (dc *AstDiscoveryContext) TraceHelperFiles(helperFiles []provider.File) { continue } relPath := filepath.ToSlash(filepath.Join(dir, entry.Name())) - if !dc.isResourceFile(relPath) { + if !isResourceFile(relPath) { continue } @@ -729,7 +730,7 @@ func (dc *AstDiscoveryContext) TraceVendorFiles(vendorFiles []provider.File) { } relPath = filepath.ToSlash(relPath) - if !dc.isResourceFile(relPath) { + if !isResourceFile(relPath) { return nil } diff --git a/integration/integration_azurerm_test.go b/integration/integration_azurerm_test.go index 9ad5aa2..b7f7f8a 100644 --- a/integration/integration_azurerm_test.go +++ b/integration/integration_azurerm_test.go @@ -2,7 +2,6 @@ package integration import ( "encoding/json" - "maps" "regexp" "strings" "testing" @@ -67,6 +66,9 @@ var azurermPRs = []prDef{ {1036, "open", "resource folder: state migration changed", []changedFile{ {"internal/services/batch/resources/batch_account/migration/account_resource_v0_to_v1.go", "modified", ""}, }}, + {1037, "open", "resource folder: state migration named like a resource changed", []changedFile{ + {"internal/services/batch/resources/batch_account/migration/account_resource.go", "modified", ""}, + }}, } // azurermASTPRs are served for the AST-mode tests; the merge refs for these @@ -138,6 +140,9 @@ var azurermASTPRs = []prDef{ {2021, "open", "resource folder: state migration changed", []changedFile{ {"internal/services/batch/resources/batch_account/migration/account_resource_v0_to_v1.go", "modified", ""}, }}, + {2022, "open", "resource folder: state migration named like a resource changed", []changedFile{ + {"internal/services/batch/resources/batch_account/migration/account_resource.go", "modified", ""}, + }}, } // conflictedPRs marks the PR numbers the mock serves with mergeable=false @@ -151,6 +156,9 @@ func azurermEnv(gh *mockGitHub, tc *mockTeamCity) map[string]string { "TCTEST_GITHUB_RAW_URL": gh.rawURL(), // azurerm-style per-service build configurations "TCTEST_BUILD_TYPE_ID_ADD_SERVICE_SUFFIX": "true", + // azurerm's own file regex (teamcity-run-tests-on-comment.yaml): it also matches everything under resources/, so the + // resource folder cases run against the regex they will meet in production + "TCTEST_FILEREGEX": `internal/services/[a-z]*/[_a-zA-Z]*(resource|data_source)`, } } @@ -332,11 +340,18 @@ func TestAPIDiscoveryAzureRM(t *testing.T) { want: []trigger{{"TF_E2E_BATCH", "refs/pull/1035/merge", "(TestAccBatchAccount|TestAccBatchAccountDataSource)"}}, }, { - // the migration's file name (account_resource_v0_to_v1.go) does not match the folder's test file names, the folder does + // the migration's file name (account_resource_v0_to_v1.go) does not match the folder's test file names, the folder does; + // azurerm's regex matches the file too, which must not turn it into a resource file with no tests beside it name: "resource folder state migration runs every test in its folder", args: []string{"pr", "1036"}, want: []trigger{{"TF_E2E_BATCH", "refs/pull/1036/merge", "(TestAccBatchAccount|TestAccBatchAccountDataSource)"}}, }, + { + // azurerm names some of these account_resource.go, which the _resource.go rule would take for a resource file + name: "resource folder state migration named like a resource runs every test in its folder", + args: []string{"pr", "1037"}, + want: []trigger{{"TF_E2E_BATCH", "refs/pull/1037/merge", "(TestAccBatchAccount|TestAccBatchAccountDataSource)"}}, + }, } for _, tt := range cases { @@ -361,22 +376,17 @@ func TestAPIDiscoveryAzureRM(t *testing.T) { func TestASTDiscoveryAzureRM(t *testing.T) { t.Parallel() - // matches only resource/data-source files directly in a service dir, so the - // shared dns ipv6_address.go helper stays a helper (the default regex would claim it) - const narrowFileRegex = `^internal/services/[a-z]+/[a-z0-9_]+(resource|data_source)\.go$` - cases := []struct { name string args []string - extra map[string]string want []trigger wantExit int }{ { - name: "same-package helper traces to resources using its symbols", - args: []string{"pr", "2001"}, - extra: map[string]string{"TCTEST_FILEREGEX": narrowFileRegex}, - want: []trigger{{"TF_E2E_DNS", "refs/pull/2001/merge", "(TestAccDataSourceDnsAAAARecord|TestAccDnsAAAARecord)"}}, + // the shared dns ipv6_address.go helper stays a helper under azurerm's regex (the default regex would claim it) + name: "same-package helper traces to resources using its symbols", + args: []string{"pr", "2001"}, + want: []trigger{{"TF_E2E_DNS", "refs/pull/2001/merge", "(TestAccDataSourceDnsAAAARecord|TestAccDnsAAAARecord)"}}, }, { name: "cross-package helper traces through import symbol usage", @@ -527,11 +537,18 @@ func TestASTDiscoveryAzureRM(t *testing.T) { want: []trigger{{"TF_E2E_BATCH", "refs/pull/2020/merge", "(TestAccBatchAccount|TestAccBatchAccountDataSource)"}}, }, { - // traced through resource.go, which imports the migration package + // traced through resource.go, which imports the migration package; azurerm's regex matches the migration file too, which + // must not make it a resource file whose own directory is searched for tests instead name: "resource folder state migration runs every test in its folder", args: []string{"pr", "2021"}, want: []trigger{{"TF_E2E_BATCH", "refs/pull/2021/merge", "(TestAccBatchAccount|TestAccBatchAccountDataSource)"}}, }, + { + // azurerm names some of these account_resource.go, which the _resource.go rule would take for a resource file + name: "resource folder state migration named like a resource runs every test in its folder", + args: []string{"pr", "2022"}, + want: []trigger{{"TF_E2E_BATCH", "refs/pull/2022/merge", "(TestAccBatchAccount|TestAccBatchAccountDataSource)"}}, + }, } for _, tt := range cases { @@ -542,7 +559,6 @@ func TestASTDiscoveryAzureRM(t *testing.T) { tc := newMockTeamCity(t) env := azurermEnv(gh, tc) env["TCTEST_LOCAL_REPO_PATH"] = cloneUpstream(t, azurermUpstream) - maps.Copy(env, tt.extra) res := runTCTest(t, env, tt.args...) if res.exitCode != tt.wantExit { diff --git a/integration/testdata/azurerm/internal/services/batch/resources/batch_account/migration/account_resource.go b/integration/testdata/azurerm/internal/services/batch/resources/batch_account/migration/account_resource.go new file mode 100644 index 0000000..e18aff4 --- /dev/null +++ b/integration/testdata/azurerm/internal/services/batch/resources/batch_account/migration/account_resource.go @@ -0,0 +1,7 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package migration + +// named like a resource file, as azurerm's are (notification_hub/migration/notification_hub_resource.go) +type AccountV1ToV2 struct{} diff --git a/integration/testdata/azurerm/internal/services/batch/resources/batch_account/resource.go b/integration/testdata/azurerm/internal/services/batch/resources/batch_account/resource.go index 0b6f431..4890b30 100644 --- a/integration/testdata/azurerm/internal/services/batch/resources/batch_account/resource.go +++ b/integration/testdata/azurerm/internal/services/batch/resources/batch_account/resource.go @@ -27,5 +27,5 @@ func (r BatchAccountResource) Read() sdk.ResourceFunc { } func (r BatchAccountResource) StateUpgraders() []interface{} { - return []interface{}{migration.AccountV0ToV1{}} + return []interface{}{migration.AccountV0ToV1{}, migration.AccountV1ToV2{}} } diff --git a/lib/provider/file-types.go b/lib/provider/file-types.go index baf0268..53b600c 100644 --- a/lib/provider/file-types.go +++ b/lib/provider/file-types.go @@ -11,7 +11,7 @@ type FileType int const ( FileTypeOther FileType = iota // outside service directories FileTypeResource // matches fileregex (resource or data source at service root), or lives in a resource folder - FileTypeHelper // in service dir but doesn't match fileregex (e.g. parse/, validate/, client/) + FileTypeHelper // in service dir but doesn't match fileregex (e.g. parse/, validate/, client/), or nested in a resource folder (migration/) FileTypeTest // _test.go with TestAcc functions FileTypeUnitTest // _test.go without TestAcc functions FileTypeVendor // under vendor/ @@ -56,6 +56,9 @@ func DefaultClassifier(f *File) FileType { if InResourceFolder(f.RelPath) { return FileTypeResource } + if BelowResourceFolder(f.RelPath) { + return FileTypeHelper // a package nested in a resource folder (migration/) is a helper of that resource whatever its files are called + } if strings.HasSuffix(f.Name, "_resource.go") || strings.HasSuffix(f.Name, "_data_source.go") { return FileTypeResource } diff --git a/lib/provider/file.go b/lib/provider/file.go index b4412e3..de615a2 100644 --- a/lib/provider/file.go +++ b/lib/provider/file.go @@ -5,7 +5,6 @@ package provider import ( "fmt" "os" - "path" "path/filepath" "regexp" "slices" @@ -155,7 +154,7 @@ func (f *File) IsMigrationFile() bool { // For "storage_queue_v1_to_v2" -> "storage_queue". // A migration package inside a resource folder returns "", every test in that folder, as the folder already names the resource. func (f *File) MigrationResourcePrefix() string { - if isResourceFolder(path.Dir(path.Dir(f.RelPath))) { + if BelowResourceFolder(f.RelPath) { return "" } diff --git a/lib/provider/resource_folder.go b/lib/provider/resource_folder.go index 577f78a..372d727 100644 --- a/lib/provider/resource_folder.go +++ b/lib/provider/resource_folder.go @@ -29,6 +29,18 @@ func InResourceFolder(relPath string) bool { return isResourceFolder(path.Dir(relPath)) } +// BelowResourceFolder returns true if relPath sits in a package nested inside a resource folder, like its migration/ package: +// internal/services/////. The file belongs to that resource, but its tests sit up in +// the folder, not beside it, so it is never a resource file itself whatever the file regex says. +func BelowResourceFolder(relPath string) bool { + for dir := path.Dir(path.Dir(relPath)); dir != "." && dir != "/"; dir = path.Dir(dir) { + if isResourceFolder(dir) { + return true + } + } + return false +} + // isResourceFolder returns true if dir is a resource folder: internal/services///. func isResourceFolder(dir string) bool { for _, prefix := range ServiceDirPrefixes { diff --git a/lib/provider/resource_folder_test.go b/lib/provider/resource_folder_test.go index d274f55..3cb9923 100644 --- a/lib/provider/resource_folder_test.go +++ b/lib/provider/resource_folder_test.go @@ -30,6 +30,40 @@ func TestInResourceFolder(t *testing.T) { } } +func TestBelowResourceFolder(t *testing.T) { + t.Parallel() + + for path, want := range map[string]bool{ + "internal/services/batch/resources/batch_account/migration/account_resource_v0_to_v1.go": true, + "internal/services/managedredis/actions/managed_redis_flush_databases/helpers/flush.go": true, + + "internal/services/batch/resources/batch_account/resource.go": false, // in the folder, not below it + "internal/services/storage/migration/storage_queue_v1_to_v2.go": false, // flat layout + "internal/services/batch/resources/helpers.go": false, // in the group but not in a folder + "internal/services/batch/validate/account_name.go": false, // helper package beside the folders + } { + if got := BelowResourceFolder(path); got != want { + t.Errorf("BelowResourceFolder(%s) = %v, want %v", path, got, want) + } + } +} + +func TestBelowResourceFolderIsAHelper(t *testing.T) { + t.Parallel() + + // a package nested in a resource folder is a helper of that resource whatever its files are called, so the folder's tests + // are found through it rather than beside it + for _, path := range []string{ + "internal/services/batch/resources/batch_account/migration/account_resource_v0_to_v1.go", + "internal/services/notificationhub/resources/notification_hub/migration/notification_hub_resource.go", // the _resource.go rule would claim it + "internal/services/appservice/resources/windows_web_app/migration/windows_web_app.go", + } { + if f := NewFile(path); f.Type != FileTypeHelper { + t.Errorf("%s: type = %s, want [HELPER]", path, f.TypeLabel()) + } + } +} + func TestResourceFolderTests(t *testing.T) { t.Parallel() @@ -91,6 +125,7 @@ func TestMigrationResourcePrefix(t *testing.T) { for path, want := range map[string]string{ "internal/services/storage/migration/storage_queue_v1_to_v2.go": "storage_queue", "internal/services/appconfiguration/resources/app_configuration_feature/migration/feature_resource_v0_to_v1.go": "", // the folder names the resource + "internal/services/notificationhub/resources/notification_hub/migration/notification_hub_resource.go": "", } { f := NewFile(path) if got := f.MigrationResourcePrefix(); got != want {