diff --git a/cmd/release.go b/cmd/release.go index c2337e3..0563e4b 100644 --- a/cmd/release.go +++ b/cmd/release.go @@ -6,6 +6,7 @@ import ( "io" "log/slog" "os" + "regexp" "strings" "time" @@ -156,6 +157,10 @@ func newReleasePlanCmd() *cobra.Command { packageFieldsRaw string publishFieldsRaw string logFile string + + publishIncludeFoundTransitive bool + publishExcludeRegex string + publishExcludeFile string ) cmd := &cobra.Command{ Use: "plan", @@ -204,6 +209,26 @@ func newReleasePlanCmd() *cobra.Command { } slog.Info("release plan: exclude policy loaded", "patterns", len(excludes)) + var publishExcludes []*regexp.Regexp + if publishExcludeFile != "" { + fileExcludes, loadErr := release.LoadExcludePatterns(publishExcludeFile) + if loadErr != nil { + return loadErr + } + publishExcludes = append(publishExcludes, fileExcludes...) + } + if publishExcludeRegex != "" { + re, compileErr := regexp.Compile(publishExcludeRegex) + if compileErr != nil { + return fmt.Errorf("invalid --publish-exclude-regex pattern: %w", compileErr) + } + publishExcludes = append(publishExcludes, re) + } + slog.Info("release plan: publish exclude policy loaded", + "publishIncludeFoundTransitive", publishIncludeFoundTransitive, + "publishExcludePatterns", len(publishExcludes), + ) + packageFields := splitCSVFields(packageFieldsRaw, []string{"location", "type", "path", "dependency"}) publishFields := splitCSVFields(publishFieldsRaw, []string{"location", "type", "path", "dependency"}) logEnabled, logPath := manifestLogPath(cmd, logFile) @@ -283,24 +308,26 @@ func newReleasePlanCmd() *cobra.Command { } planResult, buildErr := release.BuildPlan(context.Background(), release.PlanOptions{ - Assets: fullAssets, - ConnectorSourceAssets: fullAssets, - Targets: opts.Targets, - FilterMissingTransitive: filterMissingTrans, - TargetResolutionOptions: targetResolutionOpts, - Validations: validationsByTarget, - OutputRoot: outputRoot, - PackageFileBaseName: "full_build.package", - PlanExt: planExt, - WriteAssets: writeAssets, - PackageFields: packageFields, - PublishFields: publishFields, - IncludeConnectors: opts.IncludeConnectors, - IncludeConnections: opts.IncludeConnections, - InfoEnabled: infoEnabled, - RenderPackageTotals: makeTypeCountRenderer(logWriter), - RenderPublishTotals: makeTypeCountRenderer(logWriter), - RenderConnectorTotals: makeTypeCountRenderer(logWriter), + Assets: fullAssets, + ConnectorSourceAssets: fullAssets, + Targets: opts.Targets, + FilterMissingTransitive: filterMissingTrans, + TargetResolutionOptions: targetResolutionOpts, + Validations: validationsByTarget, + OutputRoot: outputRoot, + PackageFileBaseName: "full_build.package", + PlanExt: planExt, + WriteAssets: writeAssets, + PackageFields: packageFields, + PublishFields: publishFields, + IncludeConnectors: opts.IncludeConnectors, + IncludeConnections: opts.IncludeConnections, + PublishIncludeFoundTransitive: publishIncludeFoundTransitive, + PublishExcludePatterns: publishExcludes, + InfoEnabled: infoEnabled, + RenderPackageTotals: makeTypeCountRenderer(logWriter), + RenderPublishTotals: makeTypeCountRenderer(logWriter), + RenderConnectorTotals: makeTypeCountRenderer(logWriter), OnTargetFilesGenerated: func(env, packageFile, publishFile string, publishAssetCount int) { slog.Info("release plan: full mode files generated", "environment", env, @@ -369,24 +396,26 @@ func newReleasePlanCmd() *cobra.Command { connectorDependencyAssets := release.ApplyPolicies(assets, true, true, excludes) planResult, buildErr := release.BuildPlan(context.Background(), release.PlanOptions{ - Assets: allFiltered, - ConnectorSourceAssets: connectorDependencyAssets, - Targets: opts.Targets, - FilterMissingTransitive: filterMissingTrans, - TargetResolutionOptions: targetResolutionOpts, - Validations: validationsByTarget, - OutputRoot: outputRoot, - PackageFileBaseName: "tag_build.package", - PlanExt: planExt, - WriteAssets: writeAssets, - PackageFields: packageFields, - PublishFields: publishFields, - IncludeConnectors: opts.IncludeConnectors, - IncludeConnections: opts.IncludeConnections, - InfoEnabled: infoEnabled, - RenderPackageTotals: makeTypeCountRenderer(logWriter), - RenderPublishTotals: makeTypeCountRenderer(logWriter), - RenderConnectorTotals: makeTypeCountRenderer(logWriter), + Assets: allFiltered, + ConnectorSourceAssets: connectorDependencyAssets, + Targets: opts.Targets, + FilterMissingTransitive: filterMissingTrans, + TargetResolutionOptions: targetResolutionOpts, + Validations: validationsByTarget, + OutputRoot: outputRoot, + PackageFileBaseName: "tag_build.package", + PlanExt: planExt, + WriteAssets: writeAssets, + PackageFields: packageFields, + PublishFields: publishFields, + IncludeConnectors: opts.IncludeConnectors, + IncludeConnections: opts.IncludeConnections, + PublishIncludeFoundTransitive: publishIncludeFoundTransitive, + PublishExcludePatterns: publishExcludes, + InfoEnabled: infoEnabled, + RenderPackageTotals: makeTypeCountRenderer(logWriter), + RenderPublishTotals: makeTypeCountRenderer(logWriter), + RenderConnectorTotals: makeTypeCountRenderer(logWriter), OnTargetProcessing: func(env string) { slog.Info("release plan: processing target", "environment", env) }, @@ -429,6 +458,12 @@ func newReleasePlanCmd() *cobra.Command { cmd.Flags().StringVar(&planOutput, "output", "csv", "plan file output format: csv|json|yaml") cmd.Flags().StringVar(&packageFieldsRaw, "package-fields", "location,type,path,dependency", "fields for generated package files") cmd.Flags().StringVar(&publishFieldsRaw, "publish-fields", "location,type,path,dependency", "fields for generated publish files") + cmd.Flags().BoolVar(&publishIncludeFoundTransitive, "publish-include-found-transitive", false, + "include transitive dependencies already found in the target in the generated publish file (package file is unaffected; independent of --include-found-transitive)") + cmd.Flags().StringVar(&publishExcludeRegex, "publish-exclude-regex", "", + "regex matched against an asset's location to exclude it from the generated publish file only") + cmd.Flags().StringVar(&publishExcludeFile, "publish-exclude-file", "", + "path to regex patterns file (one per line, # comments) matched against location to exclude assets from the generated publish file only; same format as the release manifest's excludeFile") bindManifestLogFlag(cmd, &logFile) return cmd } diff --git a/completions/iics.bash b/completions/iics.bash index 6a63eeb..7e56f20 100644 --- a/completions/iics.bash +++ b/completions/iics.bash @@ -5127,10 +5127,20 @@ _iics_release_plan() two_word_flags+=("--package-fields") local_nonpersistent_flags+=("--package-fields") local_nonpersistent_flags+=("--package-fields=") + flags+=("--publish-exclude-file=") + two_word_flags+=("--publish-exclude-file") + local_nonpersistent_flags+=("--publish-exclude-file") + local_nonpersistent_flags+=("--publish-exclude-file=") + flags+=("--publish-exclude-regex=") + two_word_flags+=("--publish-exclude-regex") + local_nonpersistent_flags+=("--publish-exclude-regex") + local_nonpersistent_flags+=("--publish-exclude-regex=") flags+=("--publish-fields=") two_word_flags+=("--publish-fields") local_nonpersistent_flags+=("--publish-fields") local_nonpersistent_flags+=("--publish-fields=") + flags+=("--publish-include-found-transitive") + local_nonpersistent_flags+=("--publish-include-found-transitive") flags+=("--target-profile-map=") two_word_flags+=("--target-profile-map") local_nonpersistent_flags+=("--target-profile-map") diff --git a/docs/ChangeRequests/pending/CR-0039-release-plan-publish-transitive.md b/docs/ChangeRequests/pending/CR-0039-release-plan-publish-transitive.md new file mode 100644 index 0000000..3d41898 --- /dev/null +++ b/docs/ChangeRequests/pending/CR-0039-release-plan-publish-transitive.md @@ -0,0 +1,57 @@ +# CR-0039: `release plan` publish-only transitive inclusion and location excludes + +## CR Type + +- [x] Enhancement to existing command + +## Problem + +`release plan` generates a `package` file and a `publish` file per target, +both derived from the same asset list. `--include-found-transitive` controls +transitive-dependency inclusion for both files identically: by default, +transitive dependencies already found in the target are dropped from both; +explicit and missing-transitive assets are always kept. + +There is no way to give the publish file a different transitive policy than +the package file, and no way to exclude specific assets from the publish file +only. Operators running `release plan` manually sometimes want the publish +file to include found-transitive assets (e.g. to force a republish) while +leaving the package file's deployment scope untouched, and to exclude a few +specific assets from publish by location pattern. + +## Proposed Solution + +Add three CLI-only flags to `release plan` (no manifest schema change - this +is for manual, ad hoc runs, not automated pipeline manifests): + +- `--publish-include-found-transitive` (bool, default `false`): include + transitive dependencies already found in the target in the generated + publish file. Package file is unaffected; independent of + `--include-found-transitive`. +- `--publish-exclude-regex `: regex matched against an asset's + `location`, excluding matches from the publish file only. +- `--publish-exclude-file `: path to a regex-patterns file (one per + line, `#` comments), same format as the existing `release.LoadExcludePatterns` + used by the manifest's `excludeFile`. Combined (OR) with + `--publish-exclude-regex` when both are given. + +Excludes always apply to the publish file regardless of whether +`--publish-include-found-transitive` is set. Default behavior (no new flags) +is byte-identical to today - this is strictly additive and non-breaking. + +`iics publish` (the execution command) is untouched; it already has +`--from-file`/stdin to control exactly what gets published. + +## Implementation + +- `internal/release/plan.go` - new `ExcludeAssetsByLocation(assets, patterns)` + helper (`ApplyPolicies` refactored to reuse it, no behavior change). +- `internal/release/build.go` - `PlanOptions.PublishIncludeFoundTransitive`, + `PlanOptions.PublishExcludePatterns`; `BuildPlan` computes publish assets + from the unfiltered per-plan asset list when the flag is set, then applies + the exclude patterns. +- `cmd/release.go` - three new flags on `newReleasePlanCmd`, wired into + `PlanOptions` at both the full-deployment and selective/tag-mode call + sites. +- Tests: `internal/release/plan_test.go`, `internal/release/build_test.go`. +- Docs: `docs/documentation/release.md`. diff --git a/docs/documentation/release.md b/docs/documentation/release.md index b5513da..68e7294 100644 --- a/docs/documentation/release.md +++ b/docs/documentation/release.md @@ -168,6 +168,27 @@ Two checks are performed against the resolved target client: to be deployed); assets that **already exist** are **excluded** (assumed stable). - Use `--include-found-transitive` to disable this filtering. +#### Publish-only transitive inclusion and excludes (CLI-only, ad hoc) + +These flags affect the generated **publish file only**; the package file's deployment scope +and transitive policy are always governed by `--include-found-transitive` as described above. +They are independent of each other and of `--include-found-transitive`. There is no manifest +schema field for these; they exist for manual, ad hoc `release plan` runs, not automated +pipeline manifests. + +- `--publish-include-found-transitive` (bool, default `false`): include transitive dependencies + already found in the target in the generated publish file, even when they are excluded from + the package file. Useful for forcing a republish of assets that already exist in the target. +- `--publish-exclude-regex `: a regex matched against an asset's `location`; matches + are excluded from the publish file only. +- `--publish-exclude-file `: path to a regex-patterns file (one pattern per line, `#` + comments), same format as the manifest's `excludeFile`, matched against `location` to exclude + assets from the publish file only. Combined with `--publish-exclude-regex` using OR semantics + when both are given. + +Excludes always apply to the publish file regardless of whether +`--publish-include-found-transitive` is set. Default behavior (no new flags) is unchanged. + #### Per-asset validation (always runs) - Checks every asset and annotates the output with `Status` and `Warning` values, written as a @@ -191,6 +212,9 @@ Two checks are performed against the resolved target client: | `--valid-targets` | string | | Comma-separated allowlist for valid targets (overrides `IICS_VALID_DEPLOY_TARGETS`) | | `--target-profile-map` | string | | Comma-separated mapping `TARGET=profile` used for target org credential resolution (overrides `IICS_TARGET_PROFILE_MAP`) | | `--include-found-transitive` | bool | `false` | Keep all resolved transitive dependencies, even when they already exist in the target environment | +| `--publish-include-found-transitive` | bool | `false` | Include transitive dependencies already found in the target in the generated publish file only (package file unaffected; independent of `--include-found-transitive`) | +| `--publish-exclude-regex` | string | | Regex matched against an asset's `location` to exclude it from the generated publish file only | +| `--publish-exclude-file` | string | | Path to regex patterns file (same format as `excludeFile`) to exclude assets from the generated publish file only | | `--output` | string | `csv` | Plan file output format: `csv`, `json`, `yaml` | | `--package-fields` | string | `location,type,path,dependency` | Fields for package files; `STATUS ()` is auto-added per environment file | | `--publish-fields` | string | `location,type,path,dependency` | Fields for publish files | diff --git a/internal/release/build.go b/internal/release/build.go index 5d41fc5..6e7328d 100644 --- a/internal/release/build.go +++ b/internal/release/build.go @@ -6,6 +6,7 @@ import ( "log/slog" "os" "path/filepath" + "regexp" "strings" ) @@ -57,6 +58,16 @@ type PlanOptions struct { IncludeConnectors bool IncludeConnections bool + // PublishIncludeFoundTransitive, when true, computes the publish file's + // assets from the unfiltered per-plan Assets (explicit + all transitive, + // found and missing) instead of the missing-transitive-filtered envAssets + // used for the package file. Package file output is unaffected. + PublishIncludeFoundTransitive bool + // PublishExcludePatterns, when non-empty, drops assets whose Location + // matches any pattern from the publish file only (package file output is + // unaffected). + PublishExcludePatterns []*regexp.Regexp + InfoEnabled bool RenderPackageTotals TypeCountRenderer RenderPublishTotals TypeCountRenderer @@ -131,7 +142,14 @@ func BuildPlan(ctx context.Context, opts PlanOptions) (PlanResult, error) { envPackageAssets = annotated } envPackageFields := EnsureCurrentTargetStatusField(opts.PackageFields, env) - publishAssets := PublishAssets(envAssets) + publishSource := envAssets + if opts.PublishIncludeFoundTransitive { + publishSource = opts.Assets // unfiltered per-plan input: explicit + all transitive (found and missing) + } + publishAssets := PublishAssets(publishSource) + if len(opts.PublishExcludePatterns) > 0 { + publishAssets = ExcludeAssetsByLocation(publishAssets, opts.PublishExcludePatterns) + } if opts.InfoEnabled { if opts.RenderPackageTotals != nil { diff --git a/internal/release/build_test.go b/internal/release/build_test.go index a21a948..f7d892b 100644 --- a/internal/release/build_test.go +++ b/internal/release/build_test.go @@ -7,6 +7,7 @@ import ( "net/http/httptest" "os" "path/filepath" + "regexp" "strings" "testing" @@ -289,6 +290,167 @@ func TestBuildPlanReusesPrecomputedValidations(t *testing.T) { } } +func TestBuildPlanPublishIncludeFoundTransitive(t *testing.T) { + t.Setenv("HOME", t.TempDir()) + + assets := []Asset{ + {Location: "Explore/A.PROCESS", Path: "Explore/A", Type: "PROCESS", Dependency: "explicit"}, + {Location: "Explore/Missing.GUIDE", Path: "Explore/Missing", Type: "GUIDE", Dependency: "transitive"}, + {Location: "Explore/Existing.TASKFLOW", Path: "Explore/Existing", Type: "TASKFLOW", Dependency: "transitive"}, + } + validations := map[string]map[string]AssetValidation{ + "QA": { + "Explore/A.PROCESS": {Status: "missing"}, + "Explore/Missing.GUIDE": {Status: "missing"}, + "Explore/Existing.TASKFLOW": {Status: "found"}, + }, + } + + runPlan := func(includeFoundTransitive bool) PlanResult { + outputRoot := t.TempDir() + result, err := BuildPlan(context.Background(), PlanOptions{ + Assets: assets, + ConnectorSourceAssets: assets, + Targets: []string{"QA"}, + FilterMissingTransitive: true, + Validations: validations, + OutputRoot: outputRoot, + PackageFileBaseName: "full_build.package", + PlanExt: "csv", + WriteAssets: WriteAssetsCSV, + PackageFields: []string{"location", "type", "path", "dependency"}, + PublishFields: []string{"location", "type", "path", "dependency"}, + PublishIncludeFoundTransitive: includeFoundTransitive, + CompletedLabel: "full mode", + }) + if err != nil { + t.Fatalf("BuildPlan() error = %v", err) + } + return result + } + + withoutFlag := runPlan(false) + packageAssets := withoutFlag.AssetsByTarget["QA"] + if len(packageAssets) != 2 { + t.Fatalf("AssetsByTarget[QA] len = %d, want 2: %#v", len(packageAssets), packageAssets) + } + publishAssets := withoutFlag.PublishByTarget["QA"] + for _, a := range publishAssets { + if a.Location == "Explore/Existing.TASKFLOW" { + t.Fatalf("Existing.TASKFLOW should not be in publish output when PublishIncludeFoundTransitive is false: %#v", publishAssets) + } + } + + withFlag := runPlan(true) + packageAssetsWithFlag := withFlag.AssetsByTarget["QA"] + if len(packageAssetsWithFlag) != len(packageAssets) { + t.Fatalf("AssetsByTarget[QA] changed when PublishIncludeFoundTransitive set: got %d, want %d", len(packageAssetsWithFlag), len(packageAssets)) + } + publishAssetsWithFlag := withFlag.PublishByTarget["QA"] + found := false + for _, a := range publishAssetsWithFlag { + if a.Location == "Explore/Existing.TASKFLOW" { + found = true + } + } + if !found { + t.Fatalf("Existing.TASKFLOW should be in publish output when PublishIncludeFoundTransitive is true: %#v", publishAssetsWithFlag) + } +} + +func TestBuildPlanPublishExcludePatterns(t *testing.T) { + t.Setenv("HOME", t.TempDir()) + + assets := []Asset{ + {Location: "Explore/A.PROCESS", Path: "Explore/A", Type: "PROCESS", Dependency: "explicit"}, + {Location: "Explore/Secret.GUIDE", Path: "Explore/Secret", Type: "GUIDE", Dependency: "explicit"}, + } + validations := map[string]map[string]AssetValidation{ + "QA": { + "Explore/A.PROCESS": {Status: "missing"}, + "Explore/Secret.GUIDE": {Status: "missing"}, + }, + } + excludePattern := regexp.MustCompile(`Secret`) + + outputRoot := t.TempDir() + result, err := BuildPlan(context.Background(), PlanOptions{ + Assets: assets, + ConnectorSourceAssets: assets, + Targets: []string{"QA"}, + FilterMissingTransitive: true, + Validations: validations, + OutputRoot: outputRoot, + PackageFileBaseName: "full_build.package", + PlanExt: "csv", + WriteAssets: WriteAssetsCSV, + PackageFields: []string{"location", "type", "path", "dependency"}, + PublishFields: []string{"location", "type", "path", "dependency"}, + PublishExcludePatterns: []*regexp.Regexp{excludePattern}, + CompletedLabel: "full mode", + }) + if err != nil { + t.Fatalf("BuildPlan() error = %v", err) + } + + packageAssets := result.AssetsByTarget["QA"] + if len(packageAssets) != 2 { + t.Fatalf("AssetsByTarget[QA] len = %d, want 2 (package unaffected by publish excludes): %#v", len(packageAssets), packageAssets) + } + + publishAssets := result.PublishByTarget["QA"] + if len(publishAssets) != 1 { + t.Fatalf("PublishByTarget[QA] len = %d, want 1: %#v", len(publishAssets), publishAssets) + } + if publishAssets[0].Location != "Explore/A.PROCESS" { + t.Fatalf("PublishByTarget[QA][0].Location = %q, want Explore/A.PROCESS", publishAssets[0].Location) + } +} + +func TestBuildPlanPublishExcludeAppliesWithIncludeFoundTransitive(t *testing.T) { + t.Setenv("HOME", t.TempDir()) + + assets := []Asset{ + {Location: "Explore/A.PROCESS", Path: "Explore/A", Type: "PROCESS", Dependency: "explicit"}, + {Location: "Explore/Existing.TASKFLOW", Path: "Explore/Existing", Type: "TASKFLOW", Dependency: "transitive"}, + } + validations := map[string]map[string]AssetValidation{ + "QA": { + "Explore/A.PROCESS": {Status: "missing"}, + "Explore/Existing.TASKFLOW": {Status: "found"}, + }, + } + excludePattern := regexp.MustCompile(`Existing`) + + outputRoot := t.TempDir() + result, err := BuildPlan(context.Background(), PlanOptions{ + Assets: assets, + ConnectorSourceAssets: assets, + Targets: []string{"QA"}, + FilterMissingTransitive: true, + Validations: validations, + OutputRoot: outputRoot, + PackageFileBaseName: "full_build.package", + PlanExt: "csv", + WriteAssets: WriteAssetsCSV, + PackageFields: []string{"location", "type", "path", "dependency"}, + PublishFields: []string{"location", "type", "path", "dependency"}, + PublishIncludeFoundTransitive: true, + PublishExcludePatterns: []*regexp.Regexp{excludePattern}, + CompletedLabel: "full mode", + }) + if err != nil { + t.Fatalf("BuildPlan() error = %v", err) + } + + publishAssets := result.PublishByTarget["QA"] + for _, a := range publishAssets { + if a.Location == "Explore/Existing.TASKFLOW" { + t.Fatalf("Existing.TASKFLOW should still be excluded from publish despite PublishIncludeFoundTransitive: %#v", publishAssets) + } + } +} + func TestBuildPlanPropagatesTargetResolutionError(t *testing.T) { t.Setenv("HOME", t.TempDir()) // No IICS_USER_*/IICS_PWD_* env vars and no config profile for "UNKNOWNENV": diff --git a/internal/release/plan.go b/internal/release/plan.go index 707d993..f2bac52 100644 --- a/internal/release/plan.go +++ b/internal/release/plan.go @@ -186,18 +186,9 @@ func LoadExcludePatterns(filePath string) ([]*regexp.Regexp, error) { } func ApplyPolicies(assets []Asset, includeConnectors, includeConnections bool, excludePatterns []*regexp.Regexp) []Asset { + assets = ExcludeAssetsByLocation(assets, excludePatterns) filtered := make([]Asset, 0, len(assets)) for _, a := range assets { - excluded := false - for _, re := range excludePatterns { - if re.MatchString(a.Location) { - excluded = true - break - } - } - if excluded { - continue - } if !includeConnectors && isConnectorType(a.Type) { continue } else if !includeConnections && isConnectionType(a.Type) { @@ -208,6 +199,27 @@ func ApplyPolicies(assets []Asset, includeConnectors, includeConnections bool, e return filtered } +// ExcludeAssetsByLocation drops assets whose Location matches any of patterns. +func ExcludeAssetsByLocation(assets []Asset, patterns []*regexp.Regexp) []Asset { + if len(patterns) == 0 { + return assets + } + out := make([]Asset, 0, len(assets)) + for _, a := range assets { + excluded := false + for _, re := range patterns { + if re.MatchString(a.Location) { + excluded = true + break + } + } + if !excluded { + out = append(out, a) + } + } + return out +} + func ConnectorAssets(assets []Asset) []Asset { out := make([]Asset, 0) for _, a := range assets { diff --git a/internal/release/plan_test.go b/internal/release/plan_test.go index abd0717..990c6cd 100644 --- a/internal/release/plan_test.go +++ b/internal/release/plan_test.go @@ -7,6 +7,7 @@ import ( "os" "path/filepath" "reflect" + "regexp" "strings" "testing" @@ -72,6 +73,30 @@ func TestApplyPoliciesIncludeFlags(t *testing.T) { } } +func TestExcludeAssetsByLocation(t *testing.T) { + assets := []Asset{ + {Location: "Explore/A.PROCESS", Type: "PROCESS"}, + {Location: "Explore/Secret.GUIDE", Type: "GUIDE"}, + {Location: "Explore/SYS/B.TASKFLOW", Type: "TASKFLOW"}, + } + + patterns := []*regexp.Regexp{ + regexp.MustCompile(`Secret`), + regexp.MustCompile(`^Explore/SYS/`), + } + got := ExcludeAssetsByLocation(assets, patterns) + if len(got) != 1 || got[0].Location != "Explore/A.PROCESS" { + t.Fatalf("unexpected filtered assets: %#v", got) + } + + if got := ExcludeAssetsByLocation(assets, nil); len(got) != len(assets) { + t.Fatalf("nil patterns should return input unchanged: got %#v", got) + } + if got := ExcludeAssetsByLocation(assets, []*regexp.Regexp{}); len(got) != len(assets) { + t.Fatalf("empty patterns should return input unchanged: got %#v", got) + } +} + func TestShouldWriteConnectorPackage(t *testing.T) { tests := []struct { name string