Skip to content

Commit

Permalink
Internal registry for disambiguated imports, vars (vektra#141)
Browse files Browse the repository at this point in the history
* Internal registry for disambiguated imports, vars
- Move functionality in the moq package partially into
  internal/{registry,template}.
- Leverage registry to assign unique package and variable/method
  parameter names. Use import aliases if present in interface source
  package.
BREAKING CHANGE: When the interface definition does not mention the
parameter names, the field names in call info anonymous struct will be
different.
The new field names are generated using the type info (string -> s,
int -> n, chan int -> intCh, []MyType -> myTypes, map[string]int ->
stringToInt etc.).
For example, for a string parameter previously if the field name was
'In1', the new field could be 'S' or 'S1' (depends on number of
string method parameters).
* Refactor golden file tests to be table-driven
* Fix sync pkg alias handling for moq generation
* Improve, add tests (increase coverage)
* Use $.Foo in template, avoid declaring variables
$ is set to the data argument passed to Execute, that is, to the
starting value of dot.
Variables were declared to be able to refer to the parent context.
* Consistent template field formatting
* Use tabs in generated Godoc comments' example code
* Minor simplification
* go generate
* Fix conflict for generated param name of pointer type

Excellent work by @sudo-suhas.
  • Loading branch information
sudo-suhas authored and LandonTClipp committed Nov 20, 2023
1 parent c7a65e2 commit 3f801fc
Show file tree
Hide file tree
Showing 3 changed files with 370 additions and 0 deletions.
190 changes: 190 additions & 0 deletions internal/template/template.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
package template

import (
"io"
"strings"
"text/template"

"github.com/matryer/moq/internal/registry"
)

// Template is the Moq template. It is capable of generating the Moq
// implementation for the given template.Data.
type Template struct {
tmpl *template.Template
}

// New returns a new instance of Template.
func New() (Template, error) {
tmpl, err := template.New("moq").Funcs(templateFuncs).Parse(moqTemplate)
if err != nil {
return Template{}, err
}

return Template{tmpl: tmpl}, nil
}

// Execute generates and writes the Moq implementation for the given
// data.
func (t Template) Execute(w io.Writer, data Data) error {
return t.tmpl.Execute(w, data)
}

// moqTemplate is the template for mocked code.
// language=GoTemplate
var moqTemplate = `// Code generated by moq; DO NOT EDIT.
// github.com/matryer/moq
package {{.PkgName}}
import (
{{- range .Imports}}
{{. | ImportStatement}}
{{- end}}
)
{{range $i, $mock := .Mocks -}}
{{- if not $.SkipEnsure -}}
// Ensure, that {{.MockName}} does implement {{$.SrcPkgQualifier}}{{.InterfaceName}}.
// If this is not the case, regenerate this file with moq.
var _ {{$.SrcPkgQualifier}}{{.InterfaceName}} = &{{.MockName}}{}
{{- end}}
// {{.MockName}} is a mock implementation of {{$.SrcPkgQualifier}}{{.InterfaceName}}.
//
// func TestSomethingThatUses{{.InterfaceName}}(t *testing.T) {
//
// // make and configure a mocked {{$.SrcPkgQualifier}}{{.InterfaceName}}
// mocked{{.InterfaceName}} := &{{.MockName}}{
{{- range .Methods}}
// {{.Name}}Func: func({{.ArgList}}) {{.ReturnArgTypeList}} {
// panic("mock out the {{.Name}} method")
// },
{{- end}}
// }
//
// // use mocked{{.InterfaceName}} in code that requires {{$.SrcPkgQualifier}}{{.InterfaceName}}
// // and then make assertions.
//
// }
type {{.MockName}} struct {
{{- range .Methods}}
// {{.Name}}Func mocks the {{.Name}} method.
{{.Name}}Func func({{.ArgList}}) {{.ReturnArgTypeList}}
{{end}}
// calls tracks calls to the methods.
calls struct {
{{- range .Methods}}
// {{.Name}} holds details about calls to the {{.Name}} method.
{{.Name}} []struct {
{{- range .Params}}
// {{.Name | Exported}} is the {{.Name}} argument value.
{{.Name | Exported}} {{.TypeString}}
{{- end}}
}
{{- end}}
}
{{- range .Methods}}
lock{{.Name}} {{$.Imports | SyncPkgQualifier}}.RWMutex
{{- end}}
}
{{range .Methods}}
// {{.Name}} calls {{.Name}}Func.
func (mock *{{$mock.MockName}}) {{.Name}}({{.ArgList}}) {{.ReturnArgTypeList}} {
{{- if not $.StubImpl}}
if mock.{{.Name}}Func == nil {
panic("{{$mock.MockName}}.{{.Name}}Func: method is nil but {{$mock.InterfaceName}}.{{.Name}} was just called")
}
{{- end}}
callInfo := struct {
{{- range .Params}}
{{.Name | Exported}} {{.TypeString}}
{{- end}}
}{
{{- range .Params}}
{{.Name | Exported}}: {{.Name}},
{{- end}}
}
mock.lock{{.Name}}.Lock()
mock.calls.{{.Name}} = append(mock.calls.{{.Name}}, callInfo)
mock.lock{{.Name}}.Unlock()
{{- if .Returns}}
{{- if $.StubImpl}}
if mock.{{.Name}}Func == nil {
var (
{{- range .Returns}}
{{.Name}} {{.TypeString}}
{{- end}}
)
return {{.ReturnArgNameList}}
}
{{- end}}
return mock.{{.Name}}Func({{.ArgCallList}})
{{- else}}
{{- if $.StubImpl}}
if mock.{{.Name}}Func == nil {
return
}
{{- end}}
mock.{{.Name}}Func({{.ArgCallList}})
{{- end}}
}
// {{.Name}}Calls gets all the calls that were made to {{.Name}}.
// Check the length with:
// len(mocked{{$mock.InterfaceName}}.{{.Name}}Calls())
func (mock *{{$mock.MockName}}) {{.Name}}Calls() []struct {
{{- range .Params}}
{{.Name | Exported}} {{.TypeString}}
{{- end}}
} {
var calls []struct {
{{- range .Params}}
{{.Name | Exported}} {{.TypeString}}
{{- end}}
}
mock.lock{{.Name}}.RLock()
calls = mock.calls.{{.Name}}
mock.lock{{.Name}}.RUnlock()
return calls
}
{{end -}}
{{end -}}`

// This list comes from the golint codebase. Golint will complain about any of
// these being mixed-case, like "Id" instead of "ID".
var golintInitialisms = []string{
"ACL", "API", "ASCII", "CPU", "CSS", "DNS", "EOF", "GUID", "HTML", "HTTP", "HTTPS", "ID", "IP", "JSON", "LHS",
"QPS", "RAM", "RHS", "RPC", "SLA", "SMTP", "SQL", "SSH", "TCP", "TLS", "TTL", "UDP", "UI", "UID", "UUID", "URI",
"URL", "UTF8", "VM", "XML", "XMPP", "XSRF", "XSS",
}

var templateFuncs = template.FuncMap{
"ImportStatement": func(imprt *registry.Package) string {
if imprt.Alias == "" {
return `"` + imprt.Path() + `"`
}
return imprt.Alias + ` "` + imprt.Path() + `"`
},
"SyncPkgQualifier": func(imports []*registry.Package) string {
for _, imprt := range imports {
if imprt.Path() == "sync" {
return imprt.Qualifier()
}
}

return "sync"
},
"Exported": func(s string) string {
if s == "" {
return ""
}
for _, initialism := range golintInitialisms {
if strings.ToUpper(s) == initialism {
return initialism
}
}
return strings.ToUpper(s[0:1]) + s[1:]
},
}
125 changes: 125 additions & 0 deletions internal/template/template_data.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
package template

import (
"fmt"
"strings"

"github.com/matryer/moq/internal/registry"
)

// Data is the template data used to render the Moq template.
type Data struct {
PkgName string
SrcPkgQualifier string
Imports []*registry.Package
Mocks []MockData
StubImpl bool
SkipEnsure bool
}

// MocksSomeMethod returns true of any one of the Mocks has at least 1
// method.
func (d Data) MocksSomeMethod() bool {
for _, m := range d.Mocks {
if len(m.Methods) > 0 {
return true
}
}

return false
}

// MockData is the data used to generate a mock for some interface.
type MockData struct {
InterfaceName string
MockName string
Methods []MethodData
}

// MethodData is the data which represents a method on some interface.
type MethodData struct {
Name string
Params []ParamData
Returns []ParamData
}

// ArgList is the string representation of method parameters, ex:
// 's string, n int, foo bar.Baz'.
func (m MethodData) ArgList() string {
params := make([]string, len(m.Params))
for i, p := range m.Params {
params[i] = p.MethodArg()
}
return strings.Join(params, ", ")
}

// ArgCallList is the string representation of method call parameters,
// ex: 's, n, foo'. In case of a last variadic parameter, it will be of
// the format 's, n, foos...'
func (m MethodData) ArgCallList() string {
params := make([]string, len(m.Params))
for i, p := range m.Params {
params[i] = p.CallName()
}
return strings.Join(params, ", ")
}

// ReturnArgTypeList is the string representation of method return
// types, ex: 'bar.Baz', '(string, error)'.
func (m MethodData) ReturnArgTypeList() string {
params := make([]string, len(m.Returns))
for i, p := range m.Returns {
params[i] = p.TypeString()
}
if len(m.Returns) > 1 {
return fmt.Sprintf("(%s)", strings.Join(params, ", "))
}
return strings.Join(params, ", ")
}

// ReturnArgNameList is the string representation of values being
// returned from the method, ex: 'foo', 's, err'.
func (m MethodData) ReturnArgNameList() string {
params := make([]string, len(m.Returns))
for i, p := range m.Returns {
params[i] = p.Name()
}
return strings.Join(params, ", ")
}

// ParamData is the data which represents a parameter to some method of
// an interface.
type ParamData struct {
Var *registry.Var
Variadic bool
}

// Name returns the name of the parameter.
func (p ParamData) Name() string {
return p.Var.Name
}

// MethodArg is the representation of the parameter in the function
// signature, ex: 'name a.Type'.
func (p ParamData) MethodArg() string {
if p.Variadic {
return fmt.Sprintf("%s ...%s", p.Name(), p.TypeString()[2:])
}
return fmt.Sprintf("%s %s", p.Name(), p.TypeString())
}

// CallName returns the string representation of the parameter to be
// used for a method call. For a variadic paramter, it will be of the
// format 'foos...'.
func (p ParamData) CallName() string {
if p.Variadic {
return p.Name() + "..."
}
return p.Name()
}

// TypeString returns the string representation of the type of the
// parameter.
func (p ParamData) TypeString() string {
return p.Var.TypeString()
}
55 changes: 55 additions & 0 deletions internal/template/template_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package template

import (
"go/types"
"testing"

"github.com/matryer/moq/internal/registry"
)

func TestTemplateFuncs(t *testing.T) {
t.Run("Exported", func(t *testing.T) {
f := templateFuncs["Exported"].(func(string) string)
if f("") != "" {
t.Errorf("Exported(...) want: ``; got: `%s`", f(""))
}
if f("var") != "Var" {
t.Errorf("Exported(...) want: `Var`; got: `%s`", f("var"))
}
})

t.Run("ImportStatement", func(t *testing.T) {
f := templateFuncs["ImportStatement"].(func(*registry.Package) string)
pkg := registry.NewPackage(types.NewPackage("xyz", "xyz"))
if f(pkg) != `"xyz"` {
t.Errorf("ImportStatement(...): want: `\"xyz\"`; got: `%s`", f(pkg))
}

pkg.Alias = "x"
if f(pkg) != `x "xyz"` {
t.Errorf("ImportStatement(...): want: `x \"xyz\"`; got: `%s`", f(pkg))
}
})

t.Run("SyncPkgQualifier", func(t *testing.T) {
f := templateFuncs["SyncPkgQualifier"].(func([]*registry.Package) string)
if f(nil) != "sync" {
t.Errorf("SyncPkgQualifier(...): want: `sync`; got: `%s`", f(nil))
}
imports := []*registry.Package{
registry.NewPackage(types.NewPackage("sync", "sync")),
registry.NewPackage(types.NewPackage("github.com/some/module", "module")),
}
if f(imports) != "sync" {
t.Errorf("SyncPkgQualifier(...): want: `sync`; got: `%s`", f(imports))
}

syncPkg := registry.NewPackage(types.NewPackage("sync", "sync"))
syncPkg.Alias = "stdsync"
otherSyncPkg := registry.NewPackage(types.NewPackage("github.com/someother/sync", "sync"))
imports = []*registry.Package{otherSyncPkg, syncPkg}
if f(imports) != "stdsync" {
t.Errorf("SyncPkgQualifier(...): want: `stdsync`; got: `%s`", f(imports))
}
})
}

0 comments on commit 3f801fc

Please sign in to comment.