Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support multiple package sources for a single file target (config file only) #33

Merged
merged 4 commits into from
Jun 9, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

## [Unreleased]

Nothing changed yet.
### Added

- Added support for `sources` in config file. [#33](https://github.com/derision-test/go-mockgen/pull/33)

## [v1.3.1] - 2022-06-06

Expand Down
16 changes: 13 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,23 @@ A configuration file is also supported. If no command line arguments are supplie
```yaml
force: true
mocks:
- path: github.com/cache/user/pkg
- filename: foo/bar/mock_cache_test.go
path: github.com/usr/pkg/cache
interfaces:
- Cache
filename: foo/bar/mock_cache_test.go
- filename: foo/baz/mocks_test.go
# Supports multiple package sources in a single file
sources:
- path: github.com/usr/pkg/timer
interfaces:
- Timer
- path: github.com/usr/pkg/stopwatch
interfaces:
- LapTimer
- Stopwatch
```

The top level of the configuration file may also set the keys `exclude`, `prefix`, `constructor-prefix`, `goimports`, `file-prefix`, `force`, `disable-formatting`, and `for-tests`. Top-level excludes will also be applied to each mock generator entry. The values for interface and constructor prefixes, goimports, and file content prefixes will apply to each mock generator entry if a value is not set. The remaining boolean values will be true for each mock generator entry if set at the top level (regardless of the setting of each entry).
The top level of the configuration file may also set the keys `exclude`, `prefix`, `constructor-prefix`, `goimports`, `file-prefix`, `force`, `disable-formatting`, and `for-tests`. Top-level excludes will also be applied to each mock generator entry. The values for interface and constructor prefixes, goimports, generated packag names, and file content prefixes will apply to each mock generator entry source(s) if a value is not set. The remaining boolean values will be true for each mock generator entry if set at the top level (regardless of the setting of each entry).

## Testing with Mocks

Expand Down
160 changes: 105 additions & 55 deletions cmd/go-mockgen/args.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,27 +56,31 @@ func parseOptions() ([]*generation.Options, error) {

func parseFlags() (*generation.Options, error) {
opts := &generation.Options{
ImportPaths: []string{},
Interfaces: []string{},
PackageOptions: []generation.PackageOptions{
{
ImportPaths: []string{},
Interfaces: []string{},
},
},
}

app := kingpin.New(consts.Name, consts.Description).Version(consts.Version)
app.UsageWriter(os.Stdout)

app.Arg("path", "The import paths used to search for eligible interfaces").Required().StringsVar(&opts.ImportPaths)
app.Flag("package", "The name of the generated package. It will be inferred from the output options by default.").Short('p').StringVar(&opts.PkgName)
app.Flag("interfaces", "A list of target interfaces to generate defined in the given the import paths.").Short('i').StringsVar(&opts.Interfaces)
app.Flag("exclude", "A list of interfaces to exclude from generation. Mocks for all other exported interfaces defined in the given import paths are generated.").Short('e').StringsVar(&opts.Exclude)
app.Flag("dirname", "The target output directory. Each mock will be written to a unique file.").Short('d').StringVar(&opts.OutputDir)
app.Flag("filename", "The target output file. All mocks are written to this file.").Short('o').StringVar(&opts.OutputFilename)
app.Flag("import-path", "The import path of the generated package. It will be inferred from the target directory by default.").StringVar(&opts.PkgName)
app.Flag("prefix", "A prefix used in the name of each mock struct. Should be TitleCase by convention.").StringVar(&opts.Prefix)
app.Flag("constructor-prefix", "A prefix used in the name of each mock constructor function (after the initial `New`/`NewStrict` prefixes). Should be TitleCase by convention.").StringVar(&opts.ConstructorPrefix)
app.Flag("force", "Do not abort if a write to disk would overwrite an existing file.").Short('f').BoolVar(&opts.Force)
app.Flag("disable-formatting", "Do not run goimports over the rendered files.").BoolVar(&opts.DisableFormatting)
app.Flag("goimports", "Path to the goimports binary.").Default("goimports").StringVar(&opts.GoImportsBinary)
app.Flag("for-test", "Append _test suffix to generated package names and file names.").Default("false").BoolVar(&opts.ForTest)
app.Flag("file-prefix", "Content that is written at the top of each generated file.").StringVar(&opts.FilePrefix)
app.Arg("path", "The import paths used to search for eligible interfaces").Required().StringsVar(&opts.PackageOptions[0].ImportPaths)
app.Flag("package", "The name of the generated package. It will be inferred from the output options by default.").Short('p').StringVar(&opts.ContentOptions.PkgName)
app.Flag("interfaces", "A list of target interfaces to generate defined in the given the import paths.").Short('i').StringsVar(&opts.PackageOptions[0].Interfaces)
app.Flag("exclude", "A list of interfaces to exclude from generation. Mocks for all other exported interfaces defined in the given import paths are generated.").Short('e').StringsVar(&opts.PackageOptions[0].Exclude)
app.Flag("dirname", "The target output directory. Each mock will be written to a unique file.").Short('d').StringVar(&opts.OutputOptions.OutputDir)
app.Flag("filename", "The target output file. All mocks are written to this file.").Short('o').StringVar(&opts.OutputOptions.OutputFilename)
app.Flag("import-path", "The import path of the generated package. It will be inferred from the target directory by default.").StringVar(&opts.ContentOptions.PkgName)
app.Flag("prefix", "A prefix used in the name of each mock struct. Should be TitleCase by convention.").StringVar(&opts.ContentOptions.Prefix)
app.Flag("constructor-prefix", "A prefix used in the name of each mock constructor function (after the initial `New`/`NewStrict` prefixes). Should be TitleCase by convention.").StringVar(&opts.ContentOptions.ConstructorPrefix)
app.Flag("force", "Do not abort if a write to disk would overwrite an existing file.").Short('f').BoolVar(&opts.OutputOptions.Force)
app.Flag("disable-formatting", "Do not run goimports over the rendered files.").BoolVar(&opts.OutputOptions.DisableFormatting)
app.Flag("goimports", "Path to the goimports binary.").Default("goimports").StringVar(&opts.OutputOptions.GoImportsBinary)
app.Flag("for-test", "Append _test suffix to generated package names and file names.").Default("false").BoolVar(&opts.OutputOptions.ForTest)
app.Flag("file-prefix", "Content that is written at the top of each generated file.").StringVar(&opts.ContentOptions.FilePrefix)

if _, err := app.Parse(os.Args[1:]); err != nil {
return nil, err
Expand All @@ -103,8 +107,15 @@ func parseManifest() ([]*generation.Options, error) {
FilePrefix string `yaml:"file-prefix"`

Mocks []struct {
Path string `yaml:"path"`
Paths []string `yaml:"paths"`
Path string `yaml:"path"`
Paths []string `yaml:"paths"`
Sources []struct {
Path string `yaml:"path"`
Paths []string `yaml:"paths"`
Interfaces []string `yaml:"interfaces"`
Exclude []string `yaml:"exclude"`
Prefix string `yaml:"prefix"`
} `yaml:"sources"`
Package string `yaml:"package"`
Interfaces []string `yaml:"interfaces"`
Exclude []string `yaml:"exclude"`
Expand Down Expand Up @@ -154,30 +165,63 @@ func parseManifest() ([]*generation.Options, error) {
opts.ForTest = true
}

// Validation
// Canonicalization
paths := opts.Paths
if opts.Path != "" {
paths = append(paths, opts.Path)
}

// Defaults
if opts.Goimports == "" {
opts.Goimports = "goimports"
}

var packageOptions []generation.PackageOptions
if len(opts.Sources) > 0 {
if len(opts.Paths) > 0 {
return nil, fmt.Errorf("sources and path/paths are mutually exclusive")
}

for _, source := range opts.Sources {
// Canonicalization
paths := source.Paths
if source.Path != "" {
paths = append(paths, source.Path)
}

packageOptions = append(packageOptions, generation.PackageOptions{
ImportPaths: paths,
Interfaces: source.Interfaces,
Exclude: source.Exclude,
Prefix: source.Prefix,
})
}
} else {
packageOptions = append(packageOptions, generation.PackageOptions{
ImportPaths: paths,
Interfaces: opts.Interfaces,
Exclude: opts.Exclude,
Prefix: opts.Prefix,
})
}

allOptions = append(allOptions, &generation.Options{
ImportPaths: paths,
PkgName: opts.Package,
Interfaces: opts.Interfaces,
Exclude: opts.Exclude,
OutputDir: opts.Dirname,
OutputFilename: opts.Filename,
OutputImportPath: opts.ImportPath,
Prefix: opts.Prefix,
ConstructorPrefix: opts.ConstructorPrefix,
Force: opts.Force,
DisableFormatting: opts.DisableFormatting,
GoImportsBinary: opts.Goimports,
ForTest: opts.ForTest,
FilePrefix: opts.FilePrefix,
PackageOptions: packageOptions,
OutputOptions: generation.OutputOptions{
OutputDir: opts.Dirname,
OutputFilename: opts.Filename,
Force: opts.Force,
DisableFormatting: opts.DisableFormatting,
GoImportsBinary: opts.Goimports,
ForTest: opts.ForTest,
},
ContentOptions: generation.ContentOptions{
PkgName: opts.Package,
OutputImportPath: opts.ImportPath,
Prefix: opts.Prefix,
ConstructorPrefix: opts.ConstructorPrefix,
FilePrefix: opts.FilePrefix,
},
})
}

Expand All @@ -190,28 +234,28 @@ func validateOutputPaths(opts *generation.Options) (bool, error) {
return true, fmt.Errorf("failed to get current directory")
}

if opts.OutputFilename == "" && opts.OutputDir == "" {
opts.OutputDir = wd
if opts.OutputOptions.OutputFilename == "" && opts.OutputOptions.OutputDir == "" {
opts.OutputOptions.OutputDir = wd
}

if opts.OutputFilename != "" && opts.OutputDir != "" {
if opts.OutputOptions.OutputFilename != "" && opts.OutputOptions.OutputDir != "" {
return false, fmt.Errorf("dirname and filename are mutually exclusive")
}

if opts.OutputFilename != "" {
opts.OutputDir = path.Dir(opts.OutputFilename)
opts.OutputFilename = path.Base(opts.OutputFilename)
if opts.OutputOptions.OutputFilename != "" {
opts.OutputOptions.OutputDir = path.Dir(opts.OutputOptions.OutputFilename)
opts.OutputOptions.OutputFilename = path.Base(opts.OutputOptions.OutputFilename)
}

if err := paths.EnsureDirExists(opts.OutputDir); err != nil {
if err := paths.EnsureDirExists(opts.OutputOptions.OutputDir); err != nil {
return true, fmt.Errorf(
"failed to make output directory %s: %s",
opts.OutputDir,
opts.OutputOptions.OutputDir,
err.Error(),
)
}

if opts.OutputDir, err = cleanPath(opts.OutputDir); err != nil {
if opts.OutputOptions.OutputDir, err = cleanPath(opts.OutputOptions.OutputDir); err != nil {
return true, err
}

Expand All @@ -221,33 +265,39 @@ func validateOutputPaths(opts *generation.Options) (bool, error) {
var goIdentifierPattern = regexp.MustCompile("^[A-Za-z]([A-Za-z0-9_]*[A-Za-z])?$")

func validateOptions(opts *generation.Options) (bool, error) {
if len(opts.Interfaces) != 0 && len(opts.Exclude) != 0 {
return false, fmt.Errorf("interface lists and exclude lists are mutually exclusive")
for _, packageOpts := range opts.PackageOptions {
if len(packageOpts.Interfaces) != 0 && len(packageOpts.Exclude) != 0 {
return false, fmt.Errorf("interface lists and exclude lists are mutually exclusive")
}

if packageOpts.Prefix != "" && !goIdentifierPattern.Match([]byte(packageOpts.Prefix)) {
return false, fmt.Errorf("prefix `%s` is illegal", packageOpts.Prefix)
}
}

if opts.OutputImportPath == "" {
path, ok := paths.InferImportPath(opts.OutputDir)
if opts.ContentOptions.OutputImportPath == "" {
path, ok := paths.InferImportPath(opts.OutputOptions.OutputDir)
if !ok {
return false, fmt.Errorf("could not infer output import path")
}

opts.OutputImportPath = path
opts.ContentOptions.OutputImportPath = path
}

if opts.PkgName == "" {
opts.PkgName = opts.OutputImportPath[strings.LastIndex(opts.OutputImportPath, string(os.PathSeparator))+1:]
if opts.ContentOptions.PkgName == "" {
opts.ContentOptions.PkgName = opts.ContentOptions.OutputImportPath[strings.LastIndex(opts.ContentOptions.OutputImportPath, string(os.PathSeparator))+1:]
}

if !goIdentifierPattern.Match([]byte(opts.PkgName)) {
return false, fmt.Errorf("package name `%s` is illegal", opts.PkgName)
if !goIdentifierPattern.Match([]byte(opts.ContentOptions.PkgName)) {
return false, fmt.Errorf("package name `%s` is illegal", opts.ContentOptions.PkgName)
}

if opts.Prefix != "" && !goIdentifierPattern.Match([]byte(opts.Prefix)) {
return false, fmt.Errorf("prefix `%s` is illegal", opts.Prefix)
if opts.ContentOptions.Prefix != "" && !goIdentifierPattern.Match([]byte(opts.ContentOptions.Prefix)) {
return false, fmt.Errorf("prefix `%s` is illegal", opts.ContentOptions.Prefix)
}

if opts.ConstructorPrefix != "" && !goIdentifierPattern.Match([]byte(opts.ConstructorPrefix)) {
return false, fmt.Errorf("constructor-`prefix `%s` is illegal", opts.ConstructorPrefix)
if opts.ContentOptions.ConstructorPrefix != "" && !goIdentifierPattern.Match([]byte(opts.ContentOptions.ConstructorPrefix)) {
return false, fmt.Errorf("constructor-`prefix `%s` is illegal", opts.ContentOptions.ConstructorPrefix)
}

return false, nil
Expand Down
21 changes: 16 additions & 5 deletions cmd/go-mockgen/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,16 +45,25 @@ func mainErr() error {

var importPaths []string
for _, opts := range allOptions {
importPaths = append(importPaths, opts.ImportPaths...)
for _, packageOpts := range opts.PackageOptions {
importPaths = append(importPaths, packageOpts.ImportPaths...)
}
}

log.Printf("loading data for %d packages\n", len(importPaths))

pkgs, err := packages.Load(&packages.Config{Mode: packages.NeedName | packages.NeedImports | packages.NeedSyntax | packages.NeedTypes}, importPaths...)
if err != nil {
return fmt.Errorf("could not load packages %s (%s)", strings.Join(importPaths, ","), err.Error())
}

for _, opts := range allOptions {
ifaces, err := types.Extract(pkgs, opts.ImportPaths, opts.Interfaces, opts.Exclude)
typePackageOpts := make([]types.PackageOptions, 0, len(opts.PackageOptions))
for _, packageOpts := range opts.PackageOptions {
typePackageOpts = append(typePackageOpts, types.PackageOptions(packageOpts))
}

ifaces, err := types.Extract(pkgs, typePackageOpts)
if err != nil {
return err
}
Expand All @@ -64,9 +73,11 @@ func mainErr() error {
nameMap[strings.ToLower(t.Name)] = struct{}{}
}

for _, name := range opts.Interfaces {
if _, ok := nameMap[strings.ToLower(name)]; !ok {
return fmt.Errorf("type '%s' not found in supplied import paths", name)
for _, packageOpts := range opts.PackageOptions {
for _, name := range packageOpts.Interfaces {
if _, ok := nameMap[strings.ToLower(name)]; !ok {
return fmt.Errorf("type '%s' not found in supplied import paths", name)
}
}
}

Expand Down
Loading