Skip to content

Commit

Permalink
Add granular mock generation
Browse files Browse the repository at this point in the history
  • Loading branch information
sonalys committed Jun 7, 2024
1 parent 64b53b9 commit d30b286
Show file tree
Hide file tree
Showing 5 changed files with 85 additions and 13 deletions.
21 changes: 17 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ Fake is a Go type-safe [mocking](https://en.wikipedia.org/wiki/Mock_object) gene

- Type-safe mock generation
- Support for generics
- Granular mock generation
- Mock cache for ultra-fast mock regeneration
- Function call configuration, with Repeatability and Optional calls
- Automatic call assertion
Expand Down Expand Up @@ -40,13 +41,16 @@ Usage:
The flags are:
-input STRING Folder to scan for interfaces, can be invoked multiple times
-output STRING Output folder, it will follow a tree structure repeating the package path
-ignore STRING Folder to ignore, can be invoked multiple times
-input STRING Folder to scan for interfaces, can be invoked multiple times
-output STRING Output folder, it will follow a tree structure repeating the package path
-ignore STRING Folder to ignore, can be invoked multiple times
-interface STRING Usually used with go:generate for granular mock generation for specific interfaces
-mockPackage STRING Used with -interface. Specify the package name of the generated mock
```

## Example
## Examples

A very simple example would be:

Expand All @@ -71,6 +75,15 @@ func (s *StubInterface[T]) Login(userID string) error
...
```

Granular generation with go:gen:

```go
//go:generate fake -input FILENAME.go -interface Reader
type Reader interface {
io.Reader
}
```

---

```go
Expand Down
20 changes: 20 additions & 0 deletions entrypoint/fake/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"flag"
"fmt"
"os"
"path"

"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
Expand All @@ -30,13 +31,32 @@ func (s *StrSlice) Set(value string) error {

func main() {
var input, ignore StrSlice
var interfaceName, pkgName *string
flag.Var(&input, "input", "Folder to scan for .go files recursively")
output := flag.String("output", "mocks", "Folder to output the generated mocks")
flag.Var(&ignore, "ignore", "Specify which folders should be ignored")
interfaceName = flag.String("interface", "", "If you want to generate a single interface on the same folder, specify using this flag")
pkgName = flag.String("mockPackage", "", "Usable with -interface only. Provide if you want a different package from the interface being generated")
flag.Parse()
if len(input) == 0 {
// Defaults to $CWD
input = []string{"."}
}
if interfaceName != nil {
if len(input) == 0 {
log.Error().Msg("-input must be specified when using -interace")
}
if *output != "mocks" {
log.Error().Msgf("-output %s cannot be used when -interface is set", *output)
return
}
mockgen.GenerateInterface(mockgen.GenerateInterfaceConfig{
PackageName: *pkgName,
Filename: input[0],
InterfaceName: *interfaceName,
OutputFolder: path.Dir(input[0]),
})
return
}
mockgen.Run(input, *output, ignore)
}
28 changes: 27 additions & 1 deletion run.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,36 @@
package fake

import (
"fmt"
"path"
"strings"

"github.com/rs/zerolog/log"
"github.com/sonalys/fake/internal/caching"
"github.com/sonalys/fake/internal/files"
)

type GenerateInterfaceConfig struct {
PackageName string
Filename string
InterfaceName string
OutputFolder string
}

func GenerateInterface(c GenerateInterfaceConfig) {
gen := NewGenerator(c.PackageName)
b := gen.GenerateFile(c.Filename, c.OutputFolder, c.InterfaceName)
oldFilename := strings.TrimRight(path.Base(c.Filename), path.Ext(c.Filename))
filename := fmt.Sprintf("%s.%s.gen.go", oldFilename, c.InterfaceName)
outputFilename := path.Join(c.OutputFolder, filename)
outputFile, err := files.CreateFileAndFolders(outputFilename)
if err != nil {
log.Fatal().Err(err).Msgf("error opening file %s", outputFilename)
}
outputFile.Write(b)
outputFile.Close()
}

func Run(dirs []string, output string, ignore []string) {
gen := NewGenerator("mocks")
fileHashes, err := caching.GetUncachedFiles(dirs, append(ignore, output), output)
Expand All @@ -21,8 +44,11 @@ func Run(dirs []string, output string, ignore []string) {
if !lockFile.Changed() {
continue
}
if gen.WriteFile(curFilePath, outDir) {
if b := gen.GenerateFile(curFilePath, outDir); len(b) > 0 {
counter++
outputFile := openOutputFile(curFilePath, output)
outputFile.Write(b)
outputFile.Close()
} else {
// Remove empty files from our new lock file.
delete(fileHashes, curFilePath)
Expand Down
1 change: 1 addition & 0 deletions testdata/stub.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
stub "github.com/stretchr/testify/require"
)

//go:generate fake -input stub.go -interface Reader
type Reader interface {
io.Reader
}
Expand Down
28 changes: 20 additions & 8 deletions writer.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,30 +11,42 @@ import (
"github.com/sonalys/fake/internal/files"
)

func (g *Generator) WriteFile(input, output string) bool {
func (g *Generator) GenerateFile(input, output string, interfaceNames ...string) []byte {
parsedFile, err := g.ParseFile(input)
if err != nil {
log.Panic().Msgf("failed to parse file: %s", input)
}
interfaces := parsedFile.ListInterfaces()
if len(interfaces) == 0 {
return false
return nil
}
var selectedInterfaces []*ParsedInterface
if len(interfaceNames) > 0 {
for i := range interfaces {
for j := range interfaceNames {
if interfaces[i].Name == interfaceNames[j] {
selectedInterfaces = append(selectedInterfaces, interfaces[i])
}
}
}
} else {
selectedInterfaces = interfaces
}

header := bytes.NewBuffer(make([]byte, 0, parsedFile.Size))
body := bytes.NewBuffer(make([]byte, 0, parsedFile.Size))
if g.MockPackageName == "" {
g.MockPackageName = parsedFile.PkgName
}
writeHeader(header, g.MockPackageName)
// Iterate through the declarations in the file
for _, i := range interfaces {
for _, i := range selectedInterfaces {
i.write(body)
}
// writeImports comes after interfaces because we only add external dependencies after generating interfaces.
parsedFile.writeImports(header)
header.Write(body.Bytes())
b := formatCode(header.Bytes())
outputFile := openOutputFile(input, output)
defer outputFile.Close()
outputFile.Write(b)
return true
return formatCode(header.Bytes())
}

func writeHeader(w io.Writer, packageName string) {
Expand Down

0 comments on commit d30b286

Please sign in to comment.