Skip to content
This repository was archived by the owner on Jun 27, 2023. It is now read-only.

Add archive mode #633

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,23 @@ the version of mockgen used to generate your mocks.

## Running mockgen

`mockgen` has two modes of operation: source and reflect.
`mockgen` has three modes of operation: archive, source and reflect.

### Archive mode

Archive mode generates mock interfaces from a package archive
file (.a). It is enabled by using the -archive flag, the import
path is also needed as a non-flag argument. No other flags are
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can archive mode optionally take a list of interfaces, just like reflect mode? Mocking extra interfaces may introduce extra dependencies to the mock code.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could be, but it also means this mode will derive from the source mode. We will need to manually match the specified interfaces to the resolved types. IMHO, the generated code will not introduce dependencies that are not already transitively depended on (in theory), so it should be just fine.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Saying we have:

package foo

type SFoo struct {
}

type IBar Interface {
  Method1() string
}

type IBaz interface {
  Method2() SFoo
}

When we only mock IBar, then the mock won't depend on package foo, so it can be compiled into a different package foomock and imported by the tests in package foo. However, when we mock both IBar and IBaz, if the mock is still compiled into foomock, then the mock would have to import package foo because a method in IBaz returns foo.SFoo. Now the tests in foo cannot import foomock, because of circular dependencies.

Copy link
Author

@dr-dime dr-dime Oct 24, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, in this case, I'll just put the generated mocks in foo, say mock_foo_test.go.

I'm never a big fan of exporting mocks for unit tests in external packages. The obvious reason is that you are actually testing against an imaginary counterpart, a passing test doesn't reflect the actual usage of the package being mocked.

If you need to provide something for external packages to test against, create a fake sharing the same logic.

This also follows https://github.com/golang/go/wiki/CodeReviewComments#interfaces.

required.

Example:

```bash
# Build the package to a archive.
go build -o pkg.a database/sql/driver

mockgen -archive=pkg.a database/sql/driver
```

### Source mode

Expand Down Expand Up @@ -71,6 +87,8 @@ The `mockgen` command is used to generate source code for a mock
class given a Go source file containing interfaces to be mocked.
It supports the following flags:

- `-archive`: A package archive file containing interfaces to be mocked.

- `-source`: A file containing interfaces to be mocked.

- `-destination`: A file to which to write the resulting source code. If you
Expand Down
55 changes: 55 additions & 0 deletions mockgen/archive.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package main

import (
"fmt"
"go/token"
"go/types"
"log"
"os"

"github.com/golang/mock/mockgen/model"

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove empty line

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this follows google's Golang style guide for grouping imports, i.e. std, local then third-party.

"golang.org/x/tools/go/gcexportdata"
)

func archiveMode(importpath, archive string) (*model.Package, error) {
f, err := os.Open(archive)
if err != nil {
return nil, err
}
defer f.Close()
r, err := gcexportdata.NewReader(f)
if err != nil {
return nil, fmt.Errorf("read export data %q: %v", archive, err)
}

fset := token.NewFileSet()
imports := make(map[string]*types.Package)
tp, err := gcexportdata.Read(r, fset, imports, importpath)
if err != nil {
return nil, err
}

pkg := &model.Package{
Name: tp.Name(),
PkgPath: tp.Path(),
}
for _, name := range tp.Scope().Names() {
m := tp.Scope().Lookup(name)
tn, ok := m.(*types.TypeName)
if !ok {
continue
}
ti, ok := tn.Type().Underlying().(*types.Interface)
if !ok {
continue
}
it, err := model.InterfaceFromGoTypesType(ti)
if err != nil {
log.Fatal(err)
}
it.Name = m.Name()
pkg.Interfaces = append(pkg.Interfaces, it)
}
return pkg, nil
}
18 changes: 17 additions & 1 deletion mockgen/mockgen.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ var (
)

var (
archive = flag.String("archive", "", "(archive mode) Input Go archive file; enables archive mode.")
source = flag.String("source", "", "(source mode) Input Go source file; enables source mode.")
destination = flag.String("destination", "", "Output file; defaults to stdout.")
mockNames = flag.String("mock_names", "", "Comma-separated interfaceName=mockName pairs of explicit mock names to use. Mock names default to 'Mock'+ interfaceName suffix.")
Expand All @@ -80,6 +81,12 @@ func main() {
var packageName string
if *source != "" {
pkg, err = sourceMode(*source)
} else if *archive != "" {
if flag.NArg() != 1 {
usage()
log.Fatal("Expected exactly one argument")
}
pkg, err = archiveMode(flag.Arg(0), *archive)
} else {
if flag.NArg() != 2 {
usage()
Expand Down Expand Up @@ -139,6 +146,8 @@ func main() {
g := new(generator)
if *source != "" {
g.filename = *source
} else if *archive != "" {
g.filename = *archive
} else {
g.srcPackage = packageName
g.srcInterfaces = flag.Arg(1)
Expand Down Expand Up @@ -201,7 +210,14 @@ func usage() {
flag.PrintDefaults()
}

const usageText = `mockgen has two modes of operation: source and reflect.
const usageText = `mockgen has three modes of operation: archive, source and reflect.

Archive mode generates mock interfaces from a package archive
file (.a). It is enabled by using the -archive flag, the import
path is also needed as a non-flag argument. No other flags are
required.
Example:
mockgen -archive=pkg.a importpath

Source mode generates mock interfaces from a source file.
It is enabled by using the -source flag. Other flags that
Expand Down
164 changes: 164 additions & 0 deletions mockgen/model/model_gotypes.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
package model

import (
"fmt"
"go/types"
)

// InterfaceFromGoTypesType returns a pointer to an interface for the
// given reflection interface type.
func InterfaceFromGoTypesType(it *types.Interface) (*Interface, error) {
intf := &Interface{}

for i := 0; i < it.NumMethods(); i++ {
mt := it.Method(i)
// TODO: need to skip unexported methods? or just raise an error?
m := &Method{
Name: mt.Name(),
}

var err error
m.In, m.Variadic, m.Out, err = funcArgsFromGoTypesType(mt.Type().(*types.Signature))
if err != nil {
return nil, err
}

intf.AddMethod(m)
}

return intf, nil
}

func funcArgsFromGoTypesType(t *types.Signature) (in []*Parameter, variadic *Parameter, out []*Parameter, err error) {
nin := t.Params().Len()
if t.Variadic() {
nin--
}
var p *Parameter
for i := 0; i < nin; i++ {
p, err = parameterFromGoTypesType(t.Params().At(i), false)
if err != nil {
return
}
in = append(in, p)
}
if t.Variadic() {
p, err = parameterFromGoTypesType(t.Params().At(nin), true)
if err != nil {
return
}
variadic = p
}
for i := 0; i < t.Results().Len(); i++ {
p, err = parameterFromGoTypesType(t.Results().At(i), false)
if err != nil {
return
}
out = append(out, p)
}
return
}

func parameterFromGoTypesType(v *types.Var, variadic bool) (*Parameter, error) {
t := v.Type()
if variadic {
t = t.(*types.Slice).Elem()
}
tt, err := typeFromGoTypesType(t)
if err != nil {
return nil, err
}
return &Parameter{Name: v.Name(), Type: tt}, nil
}

func typeFromGoTypesType(t types.Type) (Type, error) {
// Hack workaround for https://golang.org/issue/3853.
// This explicit check should not be necessary.
// if t == byteType {
// return PredeclaredType("byte"), nil
// }

if t, ok := t.(*types.Named); ok {
tn := t.Obj()
if tn.Pkg() == nil {
return PredeclaredType(tn.Name()), nil
}
return &NamedType{
Package: tn.Pkg().Path(),
Type: tn.Name(),
}, nil
}

// only unnamed or predeclared types after here

// Lots of types have element types. Let's do the parsing and error checking for all of them.
var elemType Type
if t, ok := t.(interface{ Elem() types.Type }); ok {
var err error
elemType, err = typeFromGoTypesType(t.Elem())
if err != nil {
return nil, err
}
}

switch t := t.(type) {
case *types.Array:
return &ArrayType{
Len: int(t.Len()),
Type: elemType,
}, nil
case *types.Basic:
return PredeclaredType(t.String()), nil
case *types.Chan:
var dir ChanDir
switch t.Dir() {
case types.RecvOnly:
dir = RecvDir
case types.SendOnly:
dir = SendDir
}
return &ChanType{
Dir: dir,
Type: elemType,
}, nil
case *types.Signature:
in, variadic, out, err := funcArgsFromGoTypesType(t)
if err != nil {
return nil, err
}
return &FuncType{
In: in,
Out: out,
Variadic: variadic,
}, nil
case *types.Interface:
if t.NumMethods() == 0 {
return PredeclaredType("interface{}"), nil
}
case *types.Map:
kt, err := typeFromGoTypesType(t.Key())
if err != nil {
return nil, err
}
return &MapType{
Key: kt,
Value: elemType,
}, nil
case *types.Pointer:
return &PointerType{
Type: elemType,
}, nil
case *types.Slice:
return &ArrayType{
Len: -1,
Type: elemType,
}, nil
case *types.Struct:
if t.NumFields() == 0 {
return PredeclaredType("struct{}"), nil
}
}

// TODO: Struct, UnsafePointer
return nil, fmt.Errorf("can't yet turn %v (%T) into a model.Type", t.String(), t)
}