Skip to content

Commit

Permalink
Fixes #2. Generate mocks for dependent interfaces.
Browse files Browse the repository at this point in the history
  • Loading branch information
sdchang committed May 28, 2017
1 parent 9fe99a6 commit feff431
Show file tree
Hide file tree
Showing 19 changed files with 499 additions and 110 deletions.
6 changes: 4 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@

# Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736
.glide/
vendor/
vendor/*

**/actual/mockhiato_mocks.go
# Mockhiato tests
**/test/**/actual/**/mockhiato_mocks.go
!**/test/**/vendor/
2 changes: 2 additions & 0 deletions cmd/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,6 @@ func init() {
"Configures path of the project to generate mocks for. Default is current working directory.")
generateCmd.Flags().StringP("MockFileName", "n", "mockhiato_mocks.go",
"Configures the file name of generated mocks.")
generateCmd.Flags().StringP("DependentMocksPath", "d", "mocks",
"Configures where mocks for dependent interfaces (referenced but not defined by the project) will be created.")
}
23 changes: 19 additions & 4 deletions lib/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@ import (

// Config configures Mockhiato behavior.
type Config struct {
Verbose bool
ProjectPath string
MockFileName string
Verbose bool
ProjectPath string
MockFileName string
DependentMocksPath string
}

// Formatter formats mock files.
Expand All @@ -32,13 +33,17 @@ type Project struct {
PackagePath string
// VendorPath is the project's vedor path, which should be PackagePath/vendor
VendorPath string
// DependentMocksPath is where mocks for dependent interfaces (referenced but not defined by the project) will be created.
DependentMocksPath string

// Program is the loaded project
Program *loader.Program
// Packages is a list of packages with interfaces that can be mocked.
Packages []*Package
// DependentPackage contains dependent interfaces (referenced but not defined by the project) that can be mocked.
DependentPackage *GeneratedPackage

//GenPaths contains a list of generated file paths
// GenAbsPaths contains a list of generated file paths
GenAbsPaths []string
}

Expand All @@ -57,6 +62,16 @@ type Package struct {
Interfaces []*Interface
}

// GeneratedPackage contains metadata for a package that should be generated.
type GeneratedPackage struct {
// ContextName is the name of the package that should be generated.
ContextName string
// ContextPath is the path of the package that should be generated.
ContextPath string
// Interfaces contains interface definitions found in the package.
Interfaces []*Interface
}

// Interface contains metadata for an interface definition. Formatters rely on this to generate mocks.
type Interface struct {
TObject types.Object
Expand Down
19 changes: 19 additions & 0 deletions lib/generate/integration_test/mocks/mockhiato_mocks.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package mocks

// Auto-generated by Mockhiato
import (
mock "github.com/stretchr/testify/mock"
)

// errorMock implements mocks.error
type errorMock struct{ mock.Mock }

// Error implements (mocks.error).Error
func (r *errorMock) Error() string {
ret := r.Called()
var ret0 string
if a := ret.Get(0); a != nil {
ret0 = a.(string)
}
return ret0
}
88 changes: 66 additions & 22 deletions lib/generate/oracle.go
Original file line number Diff line number Diff line change
@@ -1,33 +1,35 @@
package generate

import (
"go/ast"
"go/parser"
"go/types"
"path/filepath"
"sort"
"strings"

"golang.org/x/tools/go/loader"

"github.com/littledot/mockhiato/lib"
"github.com/littledot/mockhiato/lib/plugin/github.com/stretchr/testify"
log "github.com/sirupsen/logrus"
"golang.org/x/tools/go/loader"
)

// Oracle parses Go projects, looking for interfaces to mock.
type Oracle struct {
formatter lib.Formatter

config lib.Config
// allMockedInterfaces indexes interfaces that will be mocked
allMockedInterfaces map[types.Object]*lib.Interface
}

// NewOracle creates a new oracle.
func NewOracle(config lib.Config) *Oracle {
oracle := &Oracle{
formatter: testify.NewTestifyFormatter(config),
config: config,
return &Oracle{
testify.NewTestifyFormatter(config),
config,
map[types.Object]*lib.Interface{},
}
return oracle
}

// ScanProject walks the project directory, indexing valid Go source code
Expand All @@ -45,6 +47,7 @@ func (r *Oracle) ScanProject(project *lib.Project) {
project.GoSrcAbsPath = projectPath[0 : srcPos+len(src)-1]
project.PackagePath = projectPath[srcPos+len(src):]
project.VendorPath = filepath.Join(project.PackagePath, "vendor")
project.DependentMocksPath = filepath.Join(project.PackagePath, r.config.DependentMocksPath)

logScanProjectResults(project)
}
Expand All @@ -61,6 +64,7 @@ func (r *Oracle) TypeCheckProject(project *lib.Project) {
panic(err)
}

// Find interfaces defined by project
for _, allPackage := range program.AllPackages {
packagePath := allPackage.Pkg.Path()
if !strings.HasPrefix(packagePath, project.PackagePath) { // External dependency? Skip
Expand All @@ -69,7 +73,7 @@ func (r *Oracle) TypeCheckProject(project *lib.Project) {
if strings.HasPrefix(packagePath, project.VendorPath) { // Vendor dependency? Skip
continue
}
interfaces := getDefinedInterfaces(&allPackage.Info)
interfaces := getInterfaces(allPackage.Info.Defs)
num := len(interfaces)
if num == 0 { // 0 interfaces defined? Skip
log.Debugf("Ignore package %s because it has 0 interfaces", packagePath)
Expand All @@ -82,9 +86,30 @@ func (r *Oracle) TypeCheckProject(project *lib.Project) {
pack.Interfaces = interfaces

project.Packages = append(project.Packages, pack)

for _, iface := range interfaces {
r.allMockedInterfaces[iface.TObject] = iface
}
log.Debugf("Found package %s with %d interfaces", packagePath, num)
}

// Find interfaces used by project
genPackage := &lib.GeneratedPackage{}
genPackage.ContextPath = project.DependentMocksPath
genPackage.ContextName = filepath.Base(genPackage.ContextPath)
genPackage.Interfaces = []*lib.Interface{}
project.DependentPackage = genPackage

for _, pack := range project.Packages { // Only need to inspect packages that needs to be mocked
interfaces := getInterfaces(pack.PackageInfo.Uses)
for _, iface := range interfaces {
if _, exists := r.allMockedInterfaces[iface.TObject]; !exists { // Interface not mocked before? Mock it
genPackage.Interfaces = append(genPackage.Interfaces, iface)
r.allMockedInterfaces[iface.TObject] = iface
}
}
}

logTypeCheckProjectResults(project)
}

Expand All @@ -95,32 +120,39 @@ func (r *Oracle) GenerateMocks(project *lib.Project) {
logGenerateMocksResults(project)
}

// getDefinedInterfaces returns defined interfaces in the package.
func getDefinedInterfaces(info *types.Info) []*lib.Interface {
// getInterfaces returns interfaces in the package.
func getInterfaces(objs map[*ast.Ident]types.Object) []*lib.Interface {
interfaces := []*lib.Interface{}
for _, def := range info.Defs {
if def == nil {
continue
}
if _, ok := def.(*types.TypeName); !ok {
continue
}
if !types.IsInterface(def.Type()) {
continue
}

for _, def := range filterInterfaces(objs) {
interfaceDef := def.Type().Underlying().(*types.Interface).Complete()
iface := &lib.Interface{
TObject: def,
TInterface: interfaceDef,
}
interfaces = append(interfaces, iface)
}

sort.Sort(byInterfaceName(interfaces))
return interfaces
}

// filterInterfaces returns interfaces.
func filterInterfaces(objs map[*ast.Ident]types.Object) map[*ast.Ident]types.Object {
interfaces := map[*ast.Ident]types.Object{}
for ident, obj := range objs {
if obj == nil {
continue
}
if _, ok := obj.(*types.TypeName); !ok {
continue
}
if !types.IsInterface(obj.Type()) {
continue
}
interfaces[ident] = obj
}
return interfaces
}

type byInterfaceName []*lib.Interface

func (r byInterfaceName) Len() int { return len(r) }
Expand All @@ -133,18 +165,30 @@ func logScanProjectResults(project *lib.Project) {
log.Infof("GOPATH is %s", project.GoAbsPath)
log.Infof("Project package is %s", project.PackagePath)
log.Infof("Project vendor path is %s", project.VendorPath)
log.Infof("Dependent mocks path is %s", project.DependentMocksPath)
}

func logTypeCheckProjectResults(project *lib.Project) {
log.Infof("Type check complete")

// Report stats for interfaces defined by project
for _, pack := range project.Packages {
ifaces := []string{}
for _, iface := range pack.Interfaces {
ifaces = append(ifaces, iface.TObject.Name())
}
log.Infof("Type checker found %d interface(s) in package %s: %s",
log.Infof("Type checker found %d interface(s) defined in package %s: %s",
len(ifaces), pack.Context.Path(), strings.Join(ifaces, ", "))
}

// Report stats for interfaces used by project
dep := project.DependentPackage
ifaces := []string{}
for _, iface := range dep.Interfaces {
ifaces = append(ifaces, iface.TObject.Name())
}
log.Infof("Type checker found %d interface(s) used by project: %s",
len(dep.Interfaces), strings.Join(ifaces, ", "))
}

func logGenerateMocksResults(project *lib.Project) {
Expand Down
1 change: 1 addition & 0 deletions lib/generate/oracle_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ func TestGenerate(t *testing.T) {
config := lib.Config{}
config.ProjectPath = actualPath
config.MockFileName = "mockhiato_mocks.go"
config.DependentMocksPath = "mocks"

generate.Run(config)

Expand Down
20 changes: 20 additions & 0 deletions lib/generate/test/dependent_interface/actual/demo.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package example

import (
"os"

"gitgud.com/bigdot"
)

// Target is an interface that should be mocked.
type Target interface {
// Should generate mocks for dependent interfaces in /mocks
GoInterface(fi os.FileInfo) (err error)
VendorInterface(bigdot.VendorDep)

// Should not identify B as dependent interface and generate mocks in /mocks
DefineAndUse(B)
}

// B B
type B interface{}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 20 additions & 0 deletions lib/generate/test/dependent_interface/expect/demo.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package example

import (
"os"

"gitgud.com/bigdot"
)

// Target is an interface that should be mocked.
type Target interface {
// Should generate mocks for dependent interfaces in /mocks
GoInterface(fi os.FileInfo) (err error)
VendorInterface(bigdot.VendorDep)

// Should not identify B as dependent interface and generate mocks in /mocks
DefineAndUse(B)
}

// B B
type B interface{}
36 changes: 36 additions & 0 deletions lib/generate/test/dependent_interface/expect/mockhiato_mocks.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package example

// Auto-generated by Mockhiato
import (
bigdot "gitgud.com/bigdot"
mock "github.com/stretchr/testify/mock"
os "os"
)

// BMock implements example.B
type BMock struct{ mock.Mock }

// TargetMock implements example.Target
type TargetMock struct{ mock.Mock }

// DefineAndUse implements (example.Target).DefineAndUse
func (r *TargetMock) DefineAndUse(p0 B) {
r.Called(p0)
return
}

// GoInterface implements (example.Target).GoInterface
func (r *TargetMock) GoInterface(p0 os.FileInfo) error {
ret := r.Called(p0)
var ret0 error
if a := ret.Get(0); a != nil {
ret0 = a.(error)
}
return ret0
}

// VendorInterface implements (example.Target).VendorInterface
func (r *TargetMock) VendorInterface(p0 bigdot.VendorDep) {
r.Called(p0)
return
}
Loading

0 comments on commit feff431

Please sign in to comment.