From d30b2862ea5527dd17d7924c3c7b3ee8fb11a411 Mon Sep 17 00:00:00 2001 From: Alysson Ribeiro <15274059+sonalys@users.noreply.github.com> Date: Fri, 7 Jun 2024 21:58:53 +0200 Subject: [PATCH] Add granular mock generation --- README.md | 21 +++++++++++++++++---- entrypoint/fake/main.go | 20 ++++++++++++++++++++ run.go | 28 +++++++++++++++++++++++++++- testdata/stub.go | 1 + writer.go | 28 ++++++++++++++++++++-------- 5 files changed, 85 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index a03087e..28b90e9 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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: @@ -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 diff --git a/entrypoint/fake/main.go b/entrypoint/fake/main.go index c1ec234..027c57a 100644 --- a/entrypoint/fake/main.go +++ b/entrypoint/fake/main.go @@ -4,6 +4,7 @@ import ( "flag" "fmt" "os" + "path" "github.com/rs/zerolog" "github.com/rs/zerolog/log" @@ -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) } diff --git a/run.go b/run.go index f964916..cfc55b5 100644 --- a/run.go +++ b/run.go @@ -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) @@ -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) diff --git a/testdata/stub.go b/testdata/stub.go index 1459bc2..01f68a6 100644 --- a/testdata/stub.go +++ b/testdata/stub.go @@ -9,6 +9,7 @@ import ( stub "github.com/stretchr/testify/require" ) +//go:generate fake -input stub.go -interface Reader type Reader interface { io.Reader } diff --git a/writer.go b/writer.go index e9e30a2..0165125 100644 --- a/writer.go +++ b/writer.go @@ -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) {