Move external dependency mockimpl to monorepo (#15863)

#15560

Probably best to review commit by commit.
First commit adds the mockimpl files, second commit amends README.md and
third commit fixes golangci-lint issues.

- [X] Manual QA for all new/changed functionality

Tested by adding a dummy method to service.go and running `make
generate-mock`.

---------

Co-authored-by: Victor Lyuboslavsky <victor.lyuboslavsky@gmail.com>
This commit is contained in:
Lucas Manuel Rodriguez
2024-01-10 11:46:24 -03:00
committed by GitHub
co-authored by Victor Lyuboslavsky
parent 4627a92447
commit eeb9931f40
8 changed files with 738 additions and 10 deletions
-1
View File
@@ -173,7 +173,6 @@ generate-dev: .prefix
NODE_ENV=development yarn run webpack --progress --watch
generate-mock: .prefix
go install github.com/fleetdm/mockimpl@ecbb3041eabfc9e046a3f2e414e32c28254b75b2
go generate github.com/fleetdm/fleet/v4/server/mock github.com/fleetdm/fleet/v4/server/mock/mockresult github.com/fleetdm/fleet/v4/server/service/mock
generate-doc: .prefix
+5 -5
View File
@@ -6,11 +6,11 @@ import (
"github.com/fleetdm/fleet/v4/server/fleet"
)
//go:generate mockimpl -o datastore_mock.go "s *DataStore" "fleet.Datastore"
//go:generate mockimpl -o datastore_installers.go "s *InstallerStore" "fleet.InstallerStore"
//go:generate mockimpl -o nanomdm/storage.go "s *Storage" "github.com/micromdm/nanomdm/storage.AllStorage"
//go:generate mockimpl -o nanodep/storage.go "s *Storage" "github.com/micromdm/nanodep/storage.AllStorage"
//go:generate mockimpl -o scep/depot.go "d *Depot" "depot.Depot"
//go:generate go run ./mockimpl/impl.go -o datastore_mock.go "s *DataStore" "fleet.Datastore"
//go:generate go run ./mockimpl/impl.go -o datastore_installers.go "s *InstallerStore" "fleet.InstallerStore"
//go:generate go run ./mockimpl/impl.go -o nanomdm/storage.go "s *Storage" "github.com/micromdm/nanomdm/storage.AllStorage"
//go:generate go run ./mockimpl/impl.go -o nanodep/storage.go "s *Storage" "github.com/micromdm/nanodep/storage.AllStorage"
//go:generate go run ./mockimpl/impl.go -o scep/depot.go "d *Depot" "depot.Depot"
var _ fleet.Datastore = (*Store)(nil)
+21
View File
@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2014 Josh Bleecher Snyder
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
+66
View File
@@ -0,0 +1,66 @@
# mockimpl
> The contents of this directory were copied (on December 2023) from https://github.com/fleetdm/mockimpl which was forked from https://github.com/groob/mockimpl.
`mockimpl` generates mock method stubs for implementing an interface.
mockimpl is based on [impl](https://github.com/josharian/impl)
```bash
go get -u github.com/groob/mockimpl
```
Sample usage:
```bash
$ impl 'f *File' io.ReadWriteCloser
// Automatically generated by mockimpl. DO NOT EDIT!
package mock
import "io"
var _ io.ReadWriteCloser = (*File)(nil)
type ReadFunc func(p []byte) (n int, err error)
type WriteFunc func(p []byte) (n int, err error)
type CloseFunc func() error
type File struct {
ReadFunc ReadFunc
ReadFuncInvoked bool
WriteFunc WriteFunc
WriteFuncInvoked bool
CloseFunc CloseFunc
CloseFuncInvoked bool
}
func (f *File) Read(p []byte) (n int, err error) {
f.ReadFuncInvoked = true
return f.ReadFunc(p)
}
func (f *File) Write(p []byte) (n int, err error) {
f.WriteFuncInvoked = true
return f.WriteFunc(p)
}
func (f *File) Close() error {
f.CloseFuncInvoked = true
return f.CloseFunc()
}
# You can also provide a full name by specifying the package path.
# This helps in cases where the interface can't be guessed
# just from the package name and interface name.
$ impl 's *Source' golang.org/x/oauth2.TokenSource
func (s *Source) Token() (*oauth2.Token, error) {
panic("not implemented")
}
```
You can use `impl` from Vim with [vim-go-impl](https://github.com/rhysd/vim-go-impl)
+424
View File
@@ -0,0 +1,424 @@
// impl generates method stubs for implementing an interface.
package main
import (
"bytes"
"flag"
"fmt"
"go/ast"
"go/build"
"go/format"
"go/parser"
"go/printer"
"go/token"
"log"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
"text/template"
"golang.org/x/tools/imports"
)
const usage = `impl [o output.go] <recv> <iface>
impl generates method stubs for recv to implement iface.
Examples:
impl 'f *File' io.Reader
impl Murmur hash.Hash
Don't forget the single quotes around the receiver type
to prevent shell globbing.
`
// findInterface returns the import path and identifier of an interface.
// For example, given "http.ResponseWriter", findInterface returns
// "net/http", "ResponseWriter".
// If a fully qualified interface is given, such as "net/http.ResponseWriter",
// it simply parses the input.
func findInterface(iface string) (path string, id string, err error) {
if len(strings.Fields(iface)) != 1 {
return "", "", fmt.Errorf("couldn't parse interface: %s", iface)
}
if slash := strings.LastIndex(iface, "/"); slash > -1 {
// package path provided
dot := strings.LastIndex(iface, ".")
// make sure iface does not end with "/" (e.g. reject net/http/)
if slash+1 == len(iface) {
return "", "", fmt.Errorf("interface name cannot end with a '/' character: %s", iface)
}
// make sure iface does not end with "." (e.g. reject net/http.)
if dot+1 == len(iface) {
return "", "", fmt.Errorf("interface name cannot end with a '.' character: %s", iface)
}
// make sure iface has exactly one "." after "/" (e.g. reject net/http/httputil)
if strings.Count(iface[slash:], ".") != 1 {
return "", "", fmt.Errorf("invalid interface name: %s", iface)
}
return iface[:dot], iface[dot+1:], nil
}
src := []byte("package hack\n" + "var i " + iface)
// If we couldn't determine the import path, goimports will
// auto fix the import path.
imp, err := imports.Process(".", src, nil)
if err != nil {
return "", "", fmt.Errorf("couldn't parse interface: %s", iface)
}
// imp should now contain an appropriate import.
// Parse out the import and the identifier.
fset := token.NewFileSet()
f, err := parser.ParseFile(fset, "", imp, 0)
if err != nil {
panic(err)
}
if len(f.Imports) == 0 {
return "", "", fmt.Errorf("unrecognized interface: %s", iface)
}
raw := f.Imports[0].Path.Value // "io"
path, err = strconv.Unquote(raw) // io
if err != nil {
panic(err)
}
decl := f.Decls[1].(*ast.GenDecl) // var i io.Reader
spec := decl.Specs[0].(*ast.ValueSpec) // i io.Reader
sel := spec.Type.(*ast.SelectorExpr) // io.Reader
id = sel.Sel.Name // Reader
return path, id, nil
}
// Pkg is a parsed build.Package.
type Pkg struct {
*build.Package
*token.FileSet
}
// typeSpec locates the *ast.TypeSpec for type id in the import path.
func typeSpec(path string, id string) (Pkg, *ast.TypeSpec, error) {
pkg, err := build.Import(path, "", 0)
if err != nil {
return Pkg{}, nil, fmt.Errorf("couldn't find package %s: %v", path, err)
}
fset := token.NewFileSet() // share one fset across the whole package
for _, file := range pkg.GoFiles {
f, err := parser.ParseFile(fset, filepath.Join(pkg.Dir, file), nil, 0)
if err != nil {
continue
}
for _, decl := range f.Decls {
decl, ok := decl.(*ast.GenDecl)
if !ok || decl.Tok != token.TYPE {
continue
}
for _, spec := range decl.Specs {
spec := spec.(*ast.TypeSpec)
if spec.Name.Name != id {
continue
}
return Pkg{Package: pkg, FileSet: fset}, spec, nil
}
}
}
return Pkg{}, nil, fmt.Errorf("type %s not found in %s", id, path)
}
// gofmt pretty-prints e.
func (p Pkg) gofmt(e ast.Expr) string {
var buf bytes.Buffer
printer.Fprint(&buf, p.FileSet, e)
return buf.String()
}
// fullType returns the fully qualified type of e.
// Examples, assuming package net/http:
//
// fullType(int) => "int"
// fullType(Handler) => "http.Handler"
// fullType(io.Reader) => "io.Reader"
// fullType(*Request) => "*http.Request"
func (p Pkg) fullType(e ast.Expr) string {
ast.Inspect(e, func(n ast.Node) bool {
switch n := n.(type) {
case *ast.Ident:
// Using typeSpec instead of IsExported here would be
// more accurate, but it'd be crazy expensive, and if
// the type isn't exported, there's no point trying
// to implement it anyway.
if n.IsExported() {
n.Name = p.Package.Name + "." + n.Name
}
case *ast.SelectorExpr:
return false
}
return true
})
return p.gofmt(e)
}
func (p Pkg) params(field *ast.Field, defaultName string) []Param {
var params []Param
typ := p.fullType(field.Type)
for _, name := range field.Names {
params = append(params, Param{Name: name.Name, Type: typ})
}
// Handle anonymous params
if len(params) == 0 {
params = []Param{{Type: typ, Name: defaultName}}
}
return params
}
// Method represents a method signature.
type Method struct {
RecvShort string
Recv string
Func
}
type Struc struct {
IName string
Func
}
// Func represents a function signature.
type Func struct {
Name string
Params []Param
Res []Param
}
// Param represents a parameter in a function or method signature.
type Param struct {
Name string
Type string
}
// CalledArgument will correctly generate call to a function with a
// variadic parameter
func (p *Param) CalledArgument() string {
variadic, _ := regexp.MatchString("^[.]{3}", p.Type)
if variadic {
return p.Name + "..."
}
return p.Name
}
func (p Pkg) funcsig(f *ast.Field) Func {
fn := Func{Name: f.Names[0].Name}
typ := f.Type.(*ast.FuncType)
if typ.Params != nil {
for pos, field := range typ.Params.List {
defaultName := fmt.Sprintf("p%d", pos)
fn.Params = append(fn.Params, p.params(field, defaultName)...)
}
}
if typ.Results != nil {
for _, field := range typ.Results.List {
fn.Res = append(fn.Res, p.params(field, "")...)
}
}
return fn
}
// funcs returns the set of methods required to implement iface.
// It is called funcs rather than methods because the
// function descriptions are functions; there is no receiver.
func funcs(iface string) ([]Func, error) {
// Locate the interface.
path, id, err := findInterface(iface)
if err != nil {
return nil, err
}
// Parse the package and find the interface declaration.
p, spec, err := typeSpec(path, id)
if err != nil {
return nil, fmt.Errorf("interface %s not found: %s", iface, err)
}
idecl, ok := spec.Type.(*ast.InterfaceType)
if !ok {
return nil, fmt.Errorf("not an interface: %s", iface)
}
if idecl.Methods == nil {
return nil, fmt.Errorf("empty interface: %s", iface)
}
var fns []Func
for _, fndecl := range idecl.Methods.List {
if len(fndecl.Names) == 0 {
// Embedded interface: recurse
embedded, err := funcs(p.fullType(fndecl.Type))
if err != nil {
return nil, err
}
fns = append(fns, embedded...)
continue
}
fn := p.funcsig(fndecl)
fns = append(fns, fn)
}
return fns, nil
}
const stub = "func ({{.Recv}}) {{.Name}}" +
"({{range .Params}}{{.Name}} {{.Type}}, {{end}})" +
"({{range .Res}}{{.Name}} {{.Type}}, {{end}})" +
"{\n" + "{{.RecvShort}}.mu.Lock()" + "\n" +
"{{.RecvShort}}.{{.Name}}FuncInvoked = true" + "\n" +
"{{.RecvShort}}.mu.Unlock()" + "\n" +
"return {{.RecvShort}}.{{.Name}}Func({{range .Params}}{{.CalledArgument}}, {{end}})" +
"\n" + "}\n\n"
var tmpl = template.Must(template.New("test").Parse(stub))
// genStubs prints nicely formatted method stubs
// for fns using receiver expression recv.
// If recv is not a valid receiver expression,
// genStubs will panic.
func genStubs(recv string, fns []Func) []byte {
var buf bytes.Buffer
for _, fn := range fns {
meth := Method{Recv: recv, RecvShort: shortRecv(recv), Func: fn}
tmpl.Execute(&buf, meth) //nolint:errcheck
}
pretty, err := format.Source(buf.Bytes())
if err != nil {
panic(err)
}
return pretty
}
func shortRecv(recv string) string {
s := strings.SplitN(recv, "*", 2)[0]
return s
}
const packageStr = "// Automatically generated by mockimpl. DO NOT EDIT!" +
"\n\n" + "package mock" + "\n\n"
const str = "{{.Name}}Func {{.Name}}Func" +
"\n" + "{{.Name}}FuncInvoked bool" +
"\n\n"
const funcTypeStr = "type {{.Name}}Func func" +
"({{range .Params}}{{.Name}} {{.Type}}, {{end}})" +
"({{range .Res}}{{.Name}} {{.Type}}, {{end}})" +
"\n\n"
var (
tmplStr = template.Must(template.New("testtwo").Parse(str))
funcTypetmplStr = template.Must(template.New("funcTypetmpl").Parse(funcTypeStr))
)
func genStr(name string, fns []Func) []byte {
var buf bytes.Buffer
for _, fn := range fns {
meth := Struc{IName: name, Func: fn}
funcTypetmplStr.Execute(&buf, meth) //nolint:errcheck
}
buf.WriteString("type ")
buf.WriteString(name)
buf.WriteString(" struct {\n")
for _, fn := range fns {
meth := Struc{IName: name, Func: fn}
tmplStr.Execute(&buf, meth) //nolint:errcheck
}
buf.WriteString("\n")
buf.WriteString("mu sync.Mutex")
buf.WriteString("\n")
buf.WriteString("}")
pretty, err := format.Source(buf.Bytes())
if err != nil {
panic(err)
}
return pretty
// return buf.Bytes()
}
// validReceiver reports whether recv is a valid receiver expression.
func validReceiver(recv string) bool {
if recv == "" {
// The parse will parse empty receivers, but we don't want to accept them,
// since it won't generate a usable code snippet.
return false
}
fset := token.NewFileSet()
_, err := parser.ParseFile(fset, "", "package hack\nfunc ("+recv+") Foo()", 0)
return err == nil
}
func main() {
flOut := flag.String("o", "", "output file")
flag.Parse()
args := flag.Args()
if len(args) != 2 {
fmt.Fprint(os.Stderr, usage)
os.Exit(2)
}
recv, iface := args[0], args[1]
if !validReceiver(recv) {
fatal(fmt.Sprintf("invalid receiver: %q", recv))
}
fns, err := funcs(iface)
if err != nil {
fatal(err)
}
src := genStubs(recv, fns)
recName := strings.SplitN(recv, " ", 2)
name := strings.TrimPrefix(recName[1], "*")
src2 := genStr(name, fns)
path, ifaceID, err := findInterface(iface)
if err != nil {
fatal(err)
}
var buf bytes.Buffer
fmt.Fprint(&buf, packageStr)
fmt.Fprintf(&buf, "import \"%s\"\n\n", path)
fmt.Fprintf(&buf, "var _ %s.%s = (*%s)(nil)\n\n", filepath.Base(path), ifaceID, name)
fmt.Fprint(&buf, string(src2))
buf.WriteString("\n")
fmt.Fprint(&buf, string(src))
pretty, err := format.Source(buf.Bytes())
if err != nil {
panic(err)
}
imp, err := imports.Process("", pretty, nil)
if err != nil {
panic(err)
}
switch *flOut {
case "":
fmt.Println(string(imp))
default:
f, err := os.Create(*flOut)
if err != nil {
log.Fatal(err)
}
defer f.Close()
_, err = f.Write(imp)
if err != nil {
log.Fatal(err)
}
}
}
func fatal(msg interface{}) {
fmt.Fprintln(os.Stderr, msg)
os.Exit(1)
}
+218
View File
@@ -0,0 +1,218 @@
package main
import (
"reflect"
"testing"
)
type errBool bool
func (b errBool) String() string {
if b {
return "an error"
}
return "no error"
}
func TestFindInterface(t *testing.T) {
cases := []struct {
iface string
path string
id string
wantErr bool
}{
{iface: "net.Conn", path: "net", id: "Conn"},
{iface: "http.ResponseWriter", path: "net/http", id: "ResponseWriter"},
{iface: "net.Tennis", wantErr: true},
{iface: "a + b", wantErr: true},
{iface: "a/b/c/", wantErr: true},
{iface: "a/b/c/pkg", wantErr: true},
{iface: "a/b/c/pkg.", wantErr: true},
{iface: "a/b/c/pkg.Typ", path: "a/b/c/pkg", id: "Typ"},
{iface: "a/b/c/pkg.Typ.Foo", wantErr: true},
}
for _, tt := range cases {
path, id, err := findInterface(tt.iface)
gotErr := err != nil
if tt.wantErr != gotErr {
t.Errorf("findInterface(%q).err=%v want %s", tt.iface, err, errBool(tt.wantErr))
continue
}
if tt.path != path {
t.Errorf("findInterface(%q).path=%q want %q", tt.iface, path, tt.path)
}
if tt.id != id {
t.Errorf("findInterface(%q).id=%q want %q", tt.iface, id, tt.id)
}
}
}
func TestTypeSpec(t *testing.T) {
// For now, just test whether we can find the interface.
cases := []struct {
path string
id string
wantErr bool
}{
{path: "net", id: "Conn"},
{path: "net", id: "Con", wantErr: true},
}
for _, tt := range cases {
p, spec, err := typeSpec(tt.path, tt.id)
gotErr := err != nil
if tt.wantErr != gotErr {
t.Errorf("typeSpec(%q, %q).err=%v want %s", tt.path, tt.id, err, errBool(tt.wantErr))
continue
}
if err == nil {
if reflect.DeepEqual(p, Pkg{}) {
t.Errorf("typeSpec(%q, %q).pkg=Pkg{} want non-nil", tt.path, tt.id)
}
if spec == nil {
t.Errorf("typeSpec(%q, %q).spec=nil want non-nil", tt.path, tt.id)
}
}
}
}
func TestFuncs(t *testing.T) {
cases := []struct {
iface string
want []Func
wantErr bool
}{
{
iface: "io.ReadWriter",
want: []Func{
{
Name: "Read",
Params: []Param{{Name: "p", Type: "[]byte"}},
Res: []Param{
{Name: "n", Type: "int"},
{Name: "err", Type: "error"},
},
},
{
Name: "Write",
Params: []Param{{Name: "p", Type: "[]byte"}},
Res: []Param{
{Name: "n", Type: "int"},
{Name: "err", Type: "error"},
},
},
},
},
{
iface: "http.ResponseWriter",
want: []Func{
{
Name: "Header",
Res: []Param{{Type: "http.Header"}},
},
{
Name: "Write",
Params: []Param{{Type: "[]byte", Name: "p0"}},
Res: []Param{{Type: "int"}, {Type: "error"}},
},
{
Name: "WriteHeader",
Params: []Param{{Type: "int", Name: "statusCode"}},
},
},
},
{
iface: "http.Handler",
want: []Func{
{
Name: "ServeHTTP",
Params: []Param{
{Name: "p0", Type: "http.ResponseWriter"},
{Name: "p1", Type: "*http.Request"},
},
},
},
},
{
iface: "ast.Node",
want: []Func{
{
Name: "Pos",
Res: []Param{{Type: "token.Pos"}},
},
{
Name: "End",
Res: []Param{{Type: "token.Pos"}},
},
},
},
{
iface: "cipher.AEAD",
want: []Func{
{
Name: "NonceSize",
Res: []Param{{Type: "int"}},
},
{
Name: "Overhead",
Res: []Param{{Type: "int"}},
},
{
Name: "Seal",
Params: []Param{
{Name: "dst", Type: "[]byte"},
{Name: "nonce", Type: "[]byte"},
{Name: "plaintext", Type: "[]byte"},
{Name: "additionalData", Type: "[]byte"},
},
Res: []Param{{Type: "[]byte"}},
},
{
Name: "Open",
Params: []Param{
{Name: "dst", Type: "[]byte"},
{Name: "nonce", Type: "[]byte"},
{Name: "ciphertext", Type: "[]byte"},
{Name: "additionalData", Type: "[]byte"},
},
Res: []Param{{Type: "[]byte"}, {Type: "error"}},
},
},
},
{iface: "net.Tennis", wantErr: true},
}
for _, tt := range cases {
fns, err := funcs(tt.iface)
gotErr := err != nil
if tt.wantErr != gotErr {
t.Errorf("funcs(%q).err=%v want %s", tt.iface, err, errBool(tt.wantErr))
continue
}
if !reflect.DeepEqual(fns, tt.want) {
t.Errorf("funcs(%q).fns=\n%v\nwant\n%v\n", tt.iface, fns, tt.want)
}
}
}
func TestValidReceiver(t *testing.T) {
cases := []struct {
recv string
want bool
}{
{recv: "f", want: true},
{recv: "F", want: true},
{recv: "f F", want: true},
{recv: "f *F", want: true},
{recv: "", want: false},
{recv: "a+b", want: false},
}
for _, tt := range cases {
got := validReceiver(tt.recv)
if got != tt.want {
t.Errorf("validReceiver(%q)=%t want %t", tt.recv, got, tt.want)
}
}
}
+1 -1
View File
@@ -1,3 +1,3 @@
package mock
//go:generate mockimpl -o datastore_query_results.go "s *QueryResultStore" "fleet.QueryResultStore"
//go:generate go run ../mockimpl/impl.go -o datastore_query_results.go "s *QueryResultStore" "fleet.QueryResultStore"
+3 -3
View File
@@ -1,5 +1,5 @@
package mock
//go:generate mockimpl -o service_osquery.go "s *TLSService" "fleet.OsqueryService"
//go:generate mockimpl -o service_pusher_factory.go "s *APNSPushProviderFactory" "github.com/micromdm/nanomdm/push.PushProviderFactory"
//go:generate mockimpl -o service_push_provider.go "s *APNSPushProvider" "github.com/micromdm/nanomdm/push.PushProvider"
//go:generate go run ../../mock/mockimpl/impl.go -o service_osquery.go "s *TLSService" "fleet.OsqueryService"
//go:generate go run ../../mock/mockimpl/impl.go -o service_pusher_factory.go "s *APNSPushProviderFactory" "github.com/micromdm/nanomdm/push.PushProviderFactory"
//go:generate go run ../../mock/mockimpl/impl.go -o service_push_provider.go "s *APNSPushProvider" "github.com/micromdm/nanomdm/push.PushProvider"