Skip to content

Commit

Permalink
add replace-type parameter
Browse files Browse the repository at this point in the history
  • Loading branch information
RangelReale committed Feb 21, 2023
1 parent 8641a5b commit 3589265
Show file tree
Hide file tree
Showing 8 changed files with 287 additions and 15 deletions.
109 changes: 96 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ Table of Contents
+ [Notes](#notes)
- [Expecter Interfaces](#expecter-interfaces)
- [Mock constructors](#mock-constructors)
- [Replace Types](#replace-types)
- [Extended Flag Descriptions](#extended-flag-descriptions)
- [Mocking interfaces in `main`](#mocking-interfaces-in--main-)
- [Semantic Versioning](#semantic-versioning)
Expand Down Expand Up @@ -391,24 +392,106 @@ The constructor sets up common functionalities automatically
- The `AssertExpectations` method is registered to be called at the end of the tests via `t.Cleanup()` method.
- The testing.TB interface is registered on the `mock.Mock` so that tests don't panic when a call on the mock is unexpected.

Replace Types
-------------

The `replace-type` parameter allows adding a list of type replacements to be made in package and/or type names.
This can help overcome some parsing problems like type aliases that the Go parser doesn't provide enough information.

```shell
mockery --replace-type github.com/vektra/mockery/v2/baz/internal/foo.InternalBaz=baz:github.com/vektra/mockery/v2/baz.Baz
```

This parameter can be specified multiple times.

This will replace any imported named `"github.com/vektra/mockery/v2/baz/internal/foo"`
with `baz "github.com/vektra/mockery/v2/baz"`. The alias is defined with `:` before
the package name. Also, the `InternalBaz` type that comes from this package will be renamed to `baz.Baz`.

This next example fixes a common problem of type aliases that point to an internal package.

`cloud.google.com/go/pubsub.Message` is a type alias defined like this:

```go
import (
ipubsub "cloud.google.com/go/internal/pubsub"
)

type Message = ipubsub.Message
```

The Go parser that mockery uses doesn't provide a way to detect this alias and sends the application the package and
type name of the type in the internal package, which will not work.

We can use "replace-type" with only the package part to replace any import of `cloud.google.com/go/internal/pubsub` to
`cloud.google.com/go/pubsub`. We don't need to change the alias or type name in this case, because they are `pubsub`
and `Message` in both cases.

```shell
mockery --replace-type cloud.google.com/go/internal/pubsub=cloud.google.com/go/pubsub
```

Original source:

```go
import (
"cloud.google.com/go/pubsub"
)

type Handler struct {
HandleMessage(m pubsub.Message) error
}
```

Mock generated without this parameter:

```go
import (
mock "github.com/stretchr/testify/mock"

pubsub "cloud.google.com/go/internal/pubsub"
)

func (_m *Handler) HandleMessage(m pubsub.Message) error {
// ...
return nil
}
```

Mock generated with this parameter.

```go
import (
mock "github.com/stretchr/testify/mock"

pubsub "cloud.google.com/go/pubsub"
)

func (_m *Handler) HandleMessage(m pubsub.Message) error {
// ...
return nil
}
```

Extended Flag Descriptions
--------------------------

The following descriptions provide additional elaboration on a few common parameters.

| flag name | description |
|---|---|
| `--name` | The `--name` option takes either the name or matching regular expression of the interface to generate mock(s) for. |
| `--all` | It's common for a big package to have a lot of interfaces, so mockery provides `--all`. This option will tell mockery to scan all files under the directory named by `--dir` ("." by default) and generates mocks for any interfaces it finds. This option implies `--recursive=true`. |
| `--recursive` | Use the `--recursive` option to search subdirectories for the interface(s). This option is only compatible with `--name`. The `--all` option implies `--recursive=true`. |
| `--output` | mockery always generates files with the package `mocks` to keep things clean and simple. You can control which mocks directory is used by using `--output`, which defaults to `./mocks`. |
|`--outpkg`| Use `--outpkg` to specify the package name of the generated mocks.|
| `--inpackage` and `--keeptree` | For some complex repositories, there could be multiple interfaces with the same name but in different packages. In that case, `--inpackage` allows generating the mocked interfaces directly in the package that it mocks. In the case you don't want to generate the mocks into the package but want to keep a similar structure, use the option `--keeptree`. |
| `--filename` | Use the `--filename` and `--structname` to override the default generated file and struct name. These options are only compatible with non-regular expressions in `--name`, where only one mock is generated. |
| `--case` | mockery generates files using the casing of the original interface name. This can be modified by specifying `--case underscore` to format the generated file name using underscore casing. |
| `--print` | Use `mockery --print` to have the resulting code printed out instead of written to disk. |
| `--exported` | Use `mockery --exported` to generate public mocks for private interfaces. |
| `--with-expecter` | Use `mockery --with-expecter` to generate `EXPECT()` methods for your mocks. This is the preferred way to setup your mocks. |
| flag name | description |
|-------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `--name` | The `--name` option takes either the name or matching regular expression of the interface to generate mock(s) for. |
| `--all` | It's common for a big package to have a lot of interfaces, so mockery provides `--all`. This option will tell mockery to scan all files under the directory named by `--dir` ("." by default) and generates mocks for any interfaces it finds. This option implies `--recursive=true`. |
| `--recursive` | Use the `--recursive` option to search subdirectories for the interface(s). This option is only compatible with `--name`. The `--all` option implies `--recursive=true`. |
| `--output` | mockery always generates files with the package `mocks` to keep things clean and simple. You can control which mocks directory is used by using `--output`, which defaults to `./mocks`. |
| `--outpkg` | Use `--outpkg` to specify the package name of the generated mocks. |
| `--inpackage` and `--keeptree` | For some complex repositories, there could be multiple interfaces with the same name but in different packages. In that case, `--inpackage` allows generating the mocked interfaces directly in the package that it mocks. In the case you don't want to generate the mocks into the package but want to keep a similar structure, use the option `--keeptree`. |
| `--filename` | Use the `--filename` and `--structname` to override the default generated file and struct name. These options are only compatible with non-regular expressions in `--name`, where only one mock is generated. |
| `--case` | mockery generates files using the casing of the original interface name. This can be modified by specifying `--case underscore` to format the generated file name using underscore casing. |
| `--print` | Use `mockery --print` to have the resulting code printed out instead of written to disk. |
| `--exported` | Use `mockery --exported` to generate public mocks for private interfaces. |
| `--with-expecter` | Use `mockery --with-expecter` to generate `EXPECT()` methods for your mocks. This is the preferred way to setup your mocks. |
| `--replace-type source=destination` | Replaces aliases, packages and/or types during generation. |

Mocking interfaces in `main`
----------------------------
Expand Down
1 change: 1 addition & 0 deletions cmd/mockery.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ func NewRootCmd() *cobra.Command {
pFlags.Bool("unroll-variadic", true, "For functions with variadic arguments, do not unroll the arguments into the underlying testify call. Instead, pass variadic slice as-is.")
pFlags.Bool("exported", false, "Generates public mocks for private interfaces.")
pFlags.Bool("with-expecter", false, "Generate expecter utility around mock's On, Run and Return methods with explicit types. This option is NOT compatible with -unroll-variadic=false")
pFlags.StringArray("replace-type", nil, "Replace types")

viper.BindPFlags(pFlags)

Expand Down
2 changes: 2 additions & 0 deletions cmd/mockery_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ func TestConfigEnvFlags(t *testing.T) {
UnrollVariadic: false,
Exported: true,
WithExpecter: true,
ReplaceType: []string{},
}

env(t, "CONFIG", expected.Config)
Expand Down Expand Up @@ -77,6 +78,7 @@ func TestConfigEnvFlags(t *testing.T) {
env(t, "UNROLL_VARIADIC", fmt.Sprint(expected.UnrollVariadic))
env(t, "EXPORTED", fmt.Sprint(expected.Exported))
env(t, "WITH_EXPECTER", fmt.Sprint(expected.WithExpecter))
env(t, "REPLACE_TYPE", strings.Join(expected.ReplaceType, ","))

initConfig(nil, nil)

Expand Down
3 changes: 2 additions & 1 deletion pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,5 +51,6 @@ type Config struct {
TestOnly bool
UnrollVariadic bool `mapstructure:"unroll-variadic"`
Version bool
WithExpecter bool `mapstructure:"with-expecter"`
WithExpecter bool `mapstructure:"with-expecter"`
ReplaceType []string `mapstructure:"replace-type"`
}
12 changes: 12 additions & 0 deletions pkg/fixtures/example_project/baz/foo.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package baz

import (
ifoo "github.com/vektra/mockery/v2/pkg/fixtures/example_project/baz/internal/foo"
)

type Baz = ifoo.InternalBaz

type Foo interface {
DoFoo() string
GetBaz() (*Baz, error)
}
6 changes: 6 additions & 0 deletions pkg/fixtures/example_project/baz/internal/foo/foo.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package foo

type InternalBaz struct {
One string
Two int
}
60 changes: 59 additions & 1 deletion pkg/generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,14 +101,48 @@ func (g *Generator) getPackageScopedType(ctx context.Context, o *types.TypeName)
if o.Pkg() == nil || o.Pkg().Name() == "main" || (!g.KeepTree && g.InPackage && o.Pkg() == g.iface.Pkg) {
return o.Name()
}
return g.addPackageImport(ctx, o.Pkg()) + "." + o.Name()
pkg := g.addPackageImport(ctx, o.Pkg())
name := o.Name()
g.checkReplaceType(ctx, func(from replaceType, to replaceType) bool {
if o.Pkg().Path() == from.pkg && name == from.typ {
name = to.typ
return false
}
return true
})
return pkg + "." + name
}

func (g *Generator) addPackageImport(ctx context.Context, pkg *types.Package) string {
return g.addPackageImportWithName(ctx, pkg.Path(), pkg.Name())
}

func (g *Generator) checkReplaceType(ctx context.Context, f func(from replaceType, to replaceType) bool) {
for _, replace := range g.ReplaceType {
r := strings.SplitN(replace, "=", 2)
if len(r) == 2 {
if !f(parseReplaceType(r[0]), parseReplaceType(r[1])) {
break
}
} else {
log := zerolog.Ctx(ctx)
log.Error().Msgf("invalid replace type value: %s", replace)
}
}
}

func (g *Generator) addPackageImportWithName(ctx context.Context, path, name string) string {
g.checkReplaceType(ctx, func(from replaceType, to replaceType) bool {
if path == from.pkg {
path = to.pkg
if to.alias != "" {
name = to.alias
}
return false
}
return true
})

if existingName, pathExists := g.packagePathToName[path]; pathExists {
return existingName
}
Expand Down Expand Up @@ -882,3 +916,27 @@ func resolveCollision(names map[string]struct{}, variable string) string {

return ret
}

type replaceType struct {
alias string
pkg string
typ string
}

func parseReplaceType(t string) replaceType {
ret := replaceType{}
r := strings.SplitN(t, ":", 2)
if len(r) > 1 {
ret.alias = r[0]
t = r[1]
}
lastDot := strings.LastIndex(t, ".")
lastSlash := strings.LastIndex(t, "/")
if lastDot == -1 || (lastSlash > -1 && lastDot < lastSlash) {
ret.pkg = t
} else {
ret.pkg = t[:lastDot]
ret.typ = t[lastDot+1:]
}
return ret
}
109 changes: 109 additions & 0 deletions pkg/generator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2329,6 +2329,90 @@ import mock "github.com/stretchr/testify/mock"
s.checkPrologueGeneration(generator, expected)
}

func (s *GeneratorSuite) TestInternalPackagePrologue() {
expected := `package mocks
import baz "github.com/vektra/mockery/v2/pkg/fixtures/example_project/baz"
import mock "github.com/stretchr/testify/mock"
`
generator := NewGenerator(
s.ctx,
config.Config{InPackage: false, LogLevel: "debug", ReplaceType: []string{
"github.com/vektra/mockery/v2/pkg/fixtures/example_project/baz/internal/foo.InternalBaz=baz:github.com/vektra/mockery/v2/pkg/fixtures/example_project/baz.Baz",
}},
s.getInterfaceFromFile("example_project/baz/foo.go", "Foo"),
pkg,
)

s.checkPrologueGeneration(generator, expected)
}

func (s *GeneratorSuite) TestInternalPackage() {
expected := `// Foo is an autogenerated mock type for the Foo type
type Foo struct {
mock.Mock
}
// DoFoo provides a mock function with given fields:
func (_m *Foo) DoFoo() string {
ret := _m.Called()
var r0 string
if rf, ok := ret.Get(0).(func() string); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(string)
}
return r0
}
// GetBaz provides a mock function with given fields:
func (_m *Foo) GetBaz() (*baz.Baz, error) {
ret := _m.Called()
var r0 *baz.Baz
if rf, ok := ret.Get(0).(func() *baz.Baz); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*baz.Baz)
}
}
var r1 error
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
type mockConstructorTestingTNewFoo interface {
mock.TestingT
Cleanup(func())
}
// NewFoo creates a new instance of Foo. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
func NewFoo(t mockConstructorTestingTNewFoo) *Foo {
mock := &Foo{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}
`
cfg := config.Config{InPackage: false, LogLevel: "debug", ReplaceType: []string{
"github.com/vektra/mockery/v2/pkg/fixtures/example_project/baz/internal/foo.InternalBaz=baz:github.com/vektra/mockery/v2/pkg/fixtures/example_project/baz.Baz",
}}

s.checkGenerationWithConfig("example_project/baz/foo.go", "Foo", cfg, expected)
}

func (s *GeneratorSuite) TestGenericGenerator() {
expected := `// RequesterGenerics is an autogenerated mock type for the RequesterGenerics type
type RequesterGenerics[TAny interface{}, TComparable comparable, TSigned constraints.Signed, TIntf test.GetInt, TExternalIntf io.Writer, TGenIntf test.GetGeneric[TSigned], TInlineType interface{ ~int | ~uint }, TInlineTypeGeneric interface {
Expand Down Expand Up @@ -2691,3 +2775,28 @@ func TestGeneratorSuite(t *testing.T) {
generatorSuite := new(GeneratorSuite)
suite.Run(t, generatorSuite)
}

func TestParseReplaceType(t *testing.T) {
tests := []struct {
value string
expected replaceType
}{
{
value: "github.com/vektra/mockery/v2/pkg/fixtures/example_project/baz/internal/foo.InternalBaz",
expected: replaceType{alias: "", pkg: "github.com/vektra/mockery/v2/pkg/fixtures/example_project/baz/internal/foo", typ: "InternalBaz"},
},
{
value: "baz:github.com/vektra/mockery/v2/pkg/fixtures/example_project/baz.Baz",
expected: replaceType{alias: "baz", pkg: "github.com/vektra/mockery/v2/pkg/fixtures/example_project/baz", typ: "Baz"},
},
{
value: "github.com/vektra/mockery/v2/pkg/fixtures/example_project/baz",
expected: replaceType{alias: "", pkg: "github.com/vektra/mockery/v2/pkg/fixtures/example_project/baz", typ: ""},
},
}

for _, test := range tests {
actual := parseReplaceType(test.value)
assert.Equal(t, test.expected, actual)
}
}

0 comments on commit 3589265

Please sign in to comment.