From 5d48467aa7d5882ae48a74379aaa41546af450a0 Mon Sep 17 00:00:00 2001 From: Kristian Bolino Date: Mon, 9 Oct 2023 16:56:31 -0400 Subject: [PATCH 1/3] Add exclude-regex to config Addresses https://github.com/vektra/mockery/issues/718 --- docs/configuration.md | 3 +- docs/features.md | 15 ++++++- pkg/config/config.go | 27 ++++++++++-- pkg/config/config_test.go | 91 +++++++++++++++++++++++++++++++++++++-- 4 files changed, 127 insertions(+), 9 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index cf391311..473b9b02 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -67,9 +67,10 @@ Parameter Descriptions | `disable-version-string` | :fontawesome-solid-x: | `#!yaml false` | Disable the version string in the generated mock files. | | `dry-run` | :fontawesome-solid-x: | `#!yaml false` | Print the actions that would be taken, but don't perform the actions. | | `exclude` | :fontawesome-solid-x: | `#!yaml []` | Specify subpackages to exclude when using `#!yaml recursive: True` | +| `exclude-regex` | :fontawesome-solid-x: | `#!yaml ""` | When set along with `include-regex`, then interfaces which match `include-regex` but also match `exclude-regex` will not be generated. If `all` is set, or if `include-regex` is not set, then `exclude-regex` has no effect. | | `filename` | :fontawesome-solid-check: | `#!yaml "mock_{{.InterfaceName}}.go"` | The name of the file the mock will reside in. | | `include-auto-generated` | :fontawesome-solid-x: | `#!yaml true` | Set to `#!yaml false` if you need mockery to skip auto-generated files during its recursive package discovery. When set to `#!yaml true`, mockery includes auto-generated files when determining if a particular directory is an importable package. | -| `include-regex` | :fontawesome-solid-x: | `#!yaml ""` | When set, only interface names that match the expression will be generated. This setting is ignored if `all: True` is specified in the configuration | +| `include-regex` | :fontawesome-solid-x: | `#!yaml ""` | When set, only interface names that match the expression will be generated. This setting is ignored if `all: True` is specified in the configuration. To further refine the interfaces generated, use `exclude-regex`. | | `inpackage` | :fontawesome-solid-x: | `#!yaml false` | When generating mocks alongside the original interfaces, you must specify `inpackage: True` to inform mockery that the mock is being placed in the same package as the original interface. | | `mockname` | :fontawesome-solid-check: | `#!yaml "Mock{{.InterfaceName}}"` | The name of the generated mock. | | `outpkg` | :fontawesome-solid-check: | `#!yaml "{{.PackageName}}"` | Use `outpkg` to specify the package name of the generated mocks. | diff --git a/docs/features.md b/docs/features.md index 748fc44c..979b03b6 100644 --- a/docs/features.md +++ b/docs/features.md @@ -193,8 +193,21 @@ packages: include-regex: ".*Client" ``` +To further refine matched interfaces, you can also use `exclude-regex`. If an interface matches both `include-regex` and `exclude-regex` then it will not be generated. For example, to generate all interfaces except those ending in `Func`: + +```yaml +packages: + github.com/user/project: + config: + recursive: true + include-regex: ".*" + exclude-regex: ".*Func" +``` + +You can only use `exclude-regex` with `include-regex`. If set by itself, `exclude-regex` has no effect. + ??? note "all: true" - Using `all: true` will override `include-regex` and issue a warning. + Using `all: true` will override `include-regex` (and `exclude-regex`) and issue a warning. Mock Constructors ----------------- diff --git a/pkg/config/config.go b/pkg/config/config.go index 86439929..2b4f205b 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -41,6 +41,7 @@ type Config struct { DisableConfigSearch bool `mapstructure:"disable-config-search"` DisableVersionString bool `mapstructure:"disable-version-string"` DryRun bool `mapstructure:"dry-run"` + ExcludeRegex string `mapstructure:"exclude-regex"` Exported bool `mapstructure:"exported"` FileName string `mapstructure:"filename"` IncludeAutoGenerated bool `mapstructure:"include-auto-generated"` @@ -271,19 +272,37 @@ func (c *Config) ShouldGenerateInterface(ctx context.Context, packageName, inter } _, interfaceExists := interfacesSection[interfaceName] - var matchedByRegex bool + matchedByRegex := false if pkgConfig.IncludeRegex != "" { if pkgConfig.All { log := zerolog.Ctx(ctx) - log.Warn().Msg("interface config has both `all` and `include-regex` set. `include-regex` will be ignored") + log.Warn().Msg("interface config has both `all` and `include-regex` set: `include-regex` will be ignored") } else { matchedByRegex, err = regexp.MatchString(pkgConfig.IncludeRegex, interfaceName) if err != nil { - return false, err + return false, fmt.Errorf("evaluating `include-regex`: %w", err) } } } - return pkgConfig.All || interfaceExists || matchedByRegex, nil + excludedByRegex := false + if pkgConfig.ExcludeRegex != "" { + if pkgConfig.All { + log := zerolog.Ctx(ctx) + log.Warn().Msg("interface config has both `all` and `exclude-regex` set: `exclude-regex` will be ignored") + } else if pkgConfig.IncludeRegex == "" { + log := zerolog.Ctx(ctx) + log.Warn().Msg("interface config has `exclude-regex` set but not `include-regex`: `exclude-regex` will be ignored") + } else { + excludedByRegex, err = regexp.MatchString(pkgConfig.ExcludeRegex, interfaceName) + if err != nil { + return false, fmt.Errorf("evaluating `exclude-regex`: %w", err) + } + if excludedByRegex { + matchedByRegex = false + } + } + } + return pkgConfig.All || interfaceExists || (matchedByRegex && !excludedByRegex), nil } func (c *Config) getInterfacesSection(ctx context.Context, packageName string) (map[string]any, error) { diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index 2e151e07..b5758052 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -609,7 +609,7 @@ func TestConfig_ShouldGenerateInterface(t *testing.T) { want: true, }, { - name: "should generate using included-regex", + name: "should generate using include-regex", c: &Config{ Packages: map[string]interface{}{ "some_package": map[string]interface{}{ @@ -622,7 +622,7 @@ func TestConfig_ShouldGenerateInterface(t *testing.T) { want: true, }, { - name: "should generate when using all and included-regex doesn't match", + name: "should generate when using all and include-regex doesn't match", c: &Config{ Packages: map[string]interface{}{ "some_package": map[string]interface{}{ @@ -636,7 +636,7 @@ func TestConfig_ShouldGenerateInterface(t *testing.T) { want: true, }, { - name: "should not generate when included-regex doesn't match", + name: "should not generate when include-regex doesn't match", c: &Config{ Packages: map[string]interface{}{ "some_package": map[string]interface{}{ @@ -648,6 +648,91 @@ func TestConfig_ShouldGenerateInterface(t *testing.T) { }, want: false, }, + { + name: "should not generate when include-regex and exclude-regex both match", + c: &Config{ + Packages: map[string]interface{}{ + "some_package": map[string]interface{}{ + "config": map[string]interface{}{ + "include-regex": ".*Interface", + "exclude-regex": "Some.*", + }, + }, + }, + }, + want: false, + }, + { + name: "should generate when include-regex matches but not exclude-regex", + c: &Config{ + Packages: map[string]interface{}{ + "some_package": map[string]interface{}{ + "config": map[string]interface{}{ + "include-regex": ".*Interface", + "exclude-regex": "Foo.*", + }, + }, + }, + }, + want: true, + }, + { + name: "should not generate when neither include-regex nor exclude-regex match", + c: &Config{ + Packages: map[string]interface{}{ + "some_package": map[string]interface{}{ + "config": map[string]interface{}{ + "include-regex": ".*XInterface", + "exclude-regex": "Foo.*", + }, + }, + }, + }, + want: false, + }, + { + name: "should not generate when exclude-regex doesn't match but include-regex isn't set", + c: &Config{ + Packages: map[string]interface{}{ + "some_package": map[string]interface{}{ + "config": map[string]interface{}{ + "exclude-regex": "Foo.*", + }, + }, + }, + }, + want: false, + }, + { + name: "should generate when using all and exclude-regex matches", + c: &Config{ + Packages: map[string]interface{}{ + "some_package": map[string]interface{}{ + "config": map[string]interface{}{ + "all": true, + "exclude-regex": ".*Interface", + }, + }, + }, + }, + want: true, + }, + { + name: "should generate when interface is selected and exclude-regex matches", + c: &Config{ + Packages: map[string]interface{}{ + "some_package": map[string]interface{}{ + "interfaces": map[string]interface{}{ + "SomeInterface": struct{}{}, + }, + "config": map[string]interface{}{ + "exclude-regex": ".*Interface", + }, + }, + }, + }, + want: true, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { From e8ebf521445ed27e5113c99cc4e2d5bcd85d93fa Mon Sep 17 00:00:00 2001 From: Kristian Bolino Date: Fri, 13 Oct 2023 10:36:22 -0400 Subject: [PATCH 2/3] Refactor Config.ShouldGenerateInterface PR https://github.com/vektra/mockery/pull/720 --- pkg/config/config.go | 66 ++++++++++++++++++++++++-------------------- 1 file changed, 36 insertions(+), 30 deletions(-) diff --git a/pkg/config/config.go b/pkg/config/config.go index 2b4f205b..dc46aad6 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -263,46 +263,52 @@ func (c *Config) ExcludePath(path string) bool { func (c *Config) ShouldGenerateInterface(ctx context.Context, packageName, interfaceName string) (bool, error) { pkgConfig, err := c.GetPackageConfig(ctx, packageName) if err != nil { - return false, err + return false, fmt.Errorf("getting package config: %w", err) + } + + log := zerolog.Ctx(ctx) + if pkgConfig.All { + if pkgConfig.IncludeRegex != "" { + log.Warn().Msg("interface config has both `all` and `include-regex` set: `include-regex` will be ignored") + } + if pkgConfig.ExcludeRegex != "" { + log.Warn().Msg("interface config has both `all` and `exclude-regex` set: `exclude-regex` will be ignored") + } + return true, nil } interfacesSection, err := c.getInterfacesSection(ctx, packageName) if err != nil { - return false, err + return false, fmt.Errorf("getting interfaces section: %w", err) } _, interfaceExists := interfacesSection[interfaceName] - - matchedByRegex := false - if pkgConfig.IncludeRegex != "" { - if pkgConfig.All { - log := zerolog.Ctx(ctx) - log.Warn().Msg("interface config has both `all` and `include-regex` set: `include-regex` will be ignored") - } else { - matchedByRegex, err = regexp.MatchString(pkgConfig.IncludeRegex, interfaceName) - if err != nil { - return false, fmt.Errorf("evaluating `include-regex`: %w", err) - } - } + if interfaceExists { + return true, nil } - excludedByRegex := false - if pkgConfig.ExcludeRegex != "" { - if pkgConfig.All { - log := zerolog.Ctx(ctx) - log.Warn().Msg("interface config has both `all` and `exclude-regex` set: `exclude-regex` will be ignored") - } else if pkgConfig.IncludeRegex == "" { - log := zerolog.Ctx(ctx) + + includeRegex := pkgConfig.IncludeRegex + excludeRegex := pkgConfig.ExcludeRegex + if includeRegex == "" { + if excludeRegex != "" { log.Warn().Msg("interface config has `exclude-regex` set but not `include-regex`: `exclude-regex` will be ignored") - } else { - excludedByRegex, err = regexp.MatchString(pkgConfig.ExcludeRegex, interfaceName) - if err != nil { - return false, fmt.Errorf("evaluating `exclude-regex`: %w", err) - } - if excludedByRegex { - matchedByRegex = false - } } + return false, nil + } + includedByRegex, err := regexp.MatchString(includeRegex, interfaceName) + if err != nil { + return false, fmt.Errorf("evaluating `include-regex`: %w", err) + } + if !includedByRegex { + return false, nil + } + if excludeRegex == "" { + return true, nil + } + excludedByRegex, err := regexp.MatchString(excludeRegex, interfaceName) + if err != nil { + return false, fmt.Errorf("evaluating `exclude-regex`: %w", err) } - return pkgConfig.All || interfaceExists || (matchedByRegex && !excludedByRegex), nil + return !excludedByRegex, nil } func (c *Config) getInterfacesSection(ctx context.Context, packageName string) (map[string]any, error) { From 25befa273a013f6eabf78666786f1234bf4c6f0a Mon Sep 17 00:00:00 2001 From: Kristian Bolino Date: Fri, 13 Oct 2023 11:07:33 -0400 Subject: [PATCH 3/3] Cover error conditions in tests PR https://github.com/vektra/mockery/pull/720 --- pkg/config/config.go | 6 ++- pkg/config/config_test.go | 80 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+), 1 deletion(-) diff --git a/pkg/config/config.go b/pkg/config/config.go index dc46aad6..3747da20 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -320,7 +320,11 @@ func (c *Config) getInterfacesSection(ctx context.Context, packageName string) ( if !exists { return make(map[string]any), nil } - return interfaceSection.(map[string]any), nil + mapConfig, ok := interfaceSection.(map[string]any) + if !ok { + return nil, fmt.Errorf("interfaces section has type %T, expected map[string]any", interfaceSection) + } + return mapConfig, nil } func (c *Config) GetInterfaceConfig(ctx context.Context, packageName string, interfaceName string) ([]*Config, error) { diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index b5758052..51550bf3 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -572,6 +572,17 @@ func TestConfig_ShouldGenerateInterface(t *testing.T) { want: false, wantErr: true, }, + { + name: "invalid interfaces section returns error", + c: &Config{ + Packages: map[string]interface{}{ + "some_package": map[string]interface{}{ + "interfaces": true, + }, + }, + }, + wantErr: true, + }, { name: "should generate all interfaces", c: &Config{ @@ -733,6 +744,75 @@ func TestConfig_ShouldGenerateInterface(t *testing.T) { }, want: true, }, + { + name: "invalid include-regex is ignored if all is set", + c: &Config{ + Packages: map[string]interface{}{ + "some_package": map[string]interface{}{ + "config": map[string]interface{}{ + "all": true, + "include-regex": "[", + }, + }, + }, + }, + want: true, + }, + { + name: "invalid include-regex results in error", + c: &Config{ + Packages: map[string]interface{}{ + "some_package": map[string]interface{}{ + "config": map[string]interface{}{ + "include-regex": "[", + }, + }, + }, + }, + wantErr: true, + }, + { + name: "invalid exclude-regex is ignored if all is set", + c: &Config{ + Packages: map[string]interface{}{ + "some_package": map[string]interface{}{ + "config": map[string]interface{}{ + "all": true, + "include-regex": ".*", + "exclude-regex": "[", + }, + }, + }, + }, + want: true, + }, + { + name: "invalid exclude-regex is ignored if include-regex is not set", + c: &Config{ + Packages: map[string]interface{}{ + "some_package": map[string]interface{}{ + "config": map[string]interface{}{ + "exclude-regex": "[", + }, + }, + }, + }, + want: false, + }, + { + name: "invalid exclude-regex results in error", + c: &Config{ + Packages: map[string]interface{}{ + "some_package": map[string]interface{}{ + "config": map[string]interface{}{ + "include-regex": ".*", + "exclude-regex": "[", + }, + }, + }, + }, + wantErr: true, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) {