-
Notifications
You must be signed in to change notification settings - Fork 0
Support go:mock annotations to mock all interfaces in one call #6
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
Conversation
| if outputFile == "" { | ||
| outputPath = strings.TrimSuffix(inputPath, ".go") + "_mock.go" | ||
| } else { | ||
| outputPath = filepath.Join(filepath.Dir(inputPath), outputFile) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This actually does support relative paths, e.g. ../example_mock.go, but since the mock will be generated into a different directory than the source interface, its package name and imports may be broken. This is an issue with the existing functionality too though, and fixing it in general could be very complicated. Furthermore, generating mocks into different directories from their interfaces doesn't seem all that useful anyway.
| for _, t := range *visited { | ||
| if t == typ { | ||
| return true | ||
| } | ||
| func validateType(typ types.Type, visited map[types.Type]bool) bool { | ||
| if visited[typ] { | ||
| return true | ||
| } | ||
| *visited = append(*visited, typ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My linter pointed out this loop could be a slices.Contains call, which made me realize it could be a map lookup.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Or a collections.Set 🤷
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That was my first thought, but mock is GPL 3.0, and nicheinc/collections is currently private. 🤫
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🤯
| // Find the interface by name | ||
| ifaceObj := pkg.Types.Scope().Lookup(ifaceName) | ||
| if ifaceObj == nil { | ||
| return Interface{}, errors.Errorf("interface not found in package: %s", ifaceName) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I didn't know there used to be a third-party (?) errors package before errors and fmt.Errorf were invented!
| //go:embed template.tmpl | ||
| var tmpl string |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I renamed template.go to template.tmpl and embedded it in this tmpl variable, which makes the template file a little neater (no giant raw string literal) and allows some IDEs to provide template validation.
| // Load package info. | ||
| pkgs, packageErr := packages.Load(&packages.Config{Mode: packages.LoadSyntax}, config.dir) | ||
| if packageErr != nil { | ||
| log.Fatalf(`Error loading package information: %s`, packageErr) | ||
| } | ||
| ifaceName := args[0] | ||
|
|
||
| // Parse the package and get info about the interface | ||
| iface, err := iface.GetInterface(*dir, ifaceName) | ||
| if err != nil { | ||
| log.Fatalf("Error getting interface information: %s", err) | ||
| if len(pkgs) < 1 { | ||
| log.Fatalf(`No packages found in %s`, config.dir) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I pulled this out of GetInterface since it's now the common first step of both single-interface and multi-interface mode.
|
😮 |
examples/directive/myInterface.go
Outdated
| // TODO: chan types | ||
| // TODO: map types |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are these still todos?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess not? 1e410f8
@nathanjcochran Do you remember if map and chan types as params/returns didn't use to generate correctly, or why these TODOs are here? Seven years later they seem to work no problem, but maybe I don't understand what those TODOs mean. 🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like I added those TODOs while I was in the process of creating that test interface and working through some errors I was encountering at the time: nathanjcochran@3332ebb.
So I think it was just a reminder to myself to add those types to test interface, which I apparently never got around to doing. So I guess it was actually a reminder to you 😅.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it was right around the time I discovered this Go bug: golang/go#25262 (hence the removal of this and this), and I probably got sidetracked filing that issue.
| ast.Inspect(fileNode, func(node ast.Node) bool { | ||
| // Only consider declarations with godoc comments. | ||
| decl, isGenDecl := node.(*ast.GenDecl) | ||
| if !isGenDecl || decl.Doc == nil { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it worth putting an explicit check for a node = nil here? I think you get saved by is isGenDecl being false in that scenario, but my understanding is nil is the base-case parameter
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Certainly doesn't hurt: b37e654
|
@bclarkx2 suggested a step beyond one mock file per input file: one mock file per package. That was already possible on this branch by setting each Active Niche employees can see the results in https://github.com/nicheinc/lead/pull/926. 😄 |
Description
This PR enables
mockto generate more than one mock in a single invocation and to generate more than one mock into the same file. Running without the formerly-required positional argument will search the input directory (-d) for any interface annotated with a//go:mockdirective in its godoc comments. (I chosego:mockin analogy togo:generate, but I'm open to other ideas for how to mark interfaces for mocking, if anyone has qualms about a made-upgodirective!) By default, an interface defined inexample.gowill be mocked intoexample_mock.go, or the output filename can be specified as part of the directive, e.g.//go:mock example_mock.go.This new execution mode has three benefits over the single-interface/
go:generateapproach:mockonly has to parse and analyze packages once, not once per mocked interface. This can provide massive performance gains in very large repos - see my benchmarks below.go:generate,go:mockdirectives are appreciably DRYer, especially in the common case where the mock file's name corresponds to the interface file's name. Compare:mockto know how to locate and update one mock without interfering with existing mocks in the same file.Backward Compatibility
I don't think the new mode of execution has any major drawbacks vs. the old one. It does lose the ability to generate one mock at a time, but
mockin that way as far as I know (the prescribed way is to rungo generate ./...),The only other use case for the single-interface mode I can think of is wanting to generate a mock without marking up the source code with
go:mock(orgo:generate). That's also not something we've found useful at Niche so far since generally the person/code defining the mock is also the one defining the interface. (For that matter,mock's import generation logic relies on mocks being generated into the same package as the mocked interface.)Regardless, I decided to put some effort into ensuring the old mode continues to work as-is. If we didn't care about backward compatibility, I would drop the
-ooption and probably turn the-doption into a positional argument so thatmockcould be invoked asmock ./...ormock -w ./..., which would be more in line with similar tools likegofmt,gofumpt, andmodernize. However, I think it's worth keeping old scripts working as-is.Testing Considerations
I've split the
exampledirectory into two packages:examples/generateandexamples/directive. Thegeneratepackage contains the old examples, usinggo:generatedirectives. Thedirectivepackage is just copied from the existing examples, except that it usesgo:mockdirectives instead ofgo:generate. It also has one extra test file,customOutputFile.gofor testing out custom output filenames and generation of two mocks into the same file.I've also tested this out in https://github.com/nicheinc/lead/pull/926. Since that repo is private to
nicheinc, I'll summarize the most interesting results here as well. Usinggo tool mockinstead ofgo run github.com/nicheinc/mockresulted in a 2.8x speedup by itself. Usinggo tool mock -w -d ./...resulted in an additional speedup of 18x, for a total speedup of 51x!Versioning
Minor. Changes are backward-compatible.