-
Notifications
You must be signed in to change notification settings - Fork 220
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
Read module dependencies from debug.BuildInfo instead of go.mod #199
Conversation
From the image above, the sentry module that's mark as version 0.0.0 is replace module. I'm not sure how do we display this kind of module in sentry. The idea that I think will be add remark after version such as v0.0.0 (replaced by ). |
@wingyplus thanks for your contribution! Could you please update |
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.
Thanks for contributing ❤️ !
Good start, I have a feel suggestions below.
I noticed this integration went in and never got proper tests. If that's something you'd like to contribute too, would be very welcome. Specially now, testing it got much simpler since we don't depend on parsing external files.
The basic test cases would be around the kinds of output debug.ReadBuildInfo()
can return: no replacements, replacements with mod+version, absolute path, relative path.
integrations.go
Outdated
if fileExists("vendor/modules.txt") { | ||
return getModulesFromVendorTxt() | ||
} | ||
|
||
if fileExists("vendor/vendor.json") { |
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 is AFAICT used by the old govendor
tool.
There is no reason for us to keep supporting that, govendor
being only one among other once popular tools from the past (like dep
, godep
, glide
, ...).
Go Modules are the present and the future, we can concentrate on that use case. Prior to Modules, there are so many ways to track dependencies and versions that we can't reasonably support everything. Users can still roll their own integration to capture this info if needed and not using Modules.
Would you like to have the pleasure to delete this portion of the code too?
In doing so, you'll likely realize there is no need for 3 functions: extractModules
, getModules
and getModulesFromBuildInfo
-- we can move everything into one.
integrations.go
Outdated
func getModulesFromBuildInfo() (map[string]string, error) { | ||
info, ok := debug.ReadBuildInfo() | ||
if !ok { | ||
return nil, fmt.Errorf("cannot dependencies from debug.BuildInfo") |
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.
As we can see from #197, returning an error here will only confuse users.
The case when ok = False
is when the main package is built with GO111MODULE=off
, so there's nothing the integration can do anyway.
We can log a message to help with debugging "why package/module information is not appearing in Sentry?" but the message should not be about a "failure", but rather say this integration is disabled when build is not using Go Modules.
We can use a similar message as documented in https://golang.org/pkg/runtime/debug/#ReadBuildInfo:
"The Modules integration is not available in binaries built without module support."
integrations.go
Outdated
modules := make(map[string]string) | ||
for _, dep := range info.Deps { | ||
modules[dep.Path] = dep.Version |
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.
Needs to include info.Main
too 😉
As you well noted in a comment, we also need to handle replacements. The Sentry server only takes a map[string]string
for package/module name => version, no concept of replacements. While we could look into adding replacements server-side and in the web interface, I think we can well start with just reporting replacements within the current server constraints.
There are replacements pointing to a module+version, and replacements pointing to absolute local path and relative local path, see https://github.com/golang/go/wiki/Modules#when-should-i-use-the-replace-directive.
We could use a syntax similar to how go list -m all
prints replacements:
module/name v0.0.0 => replacement
map[string]string{"module/name": "v0.0.0 => replacement"}
What do you think?
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 sound good to me for doing this.
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.
Fixed.
@rhcarvalho Thanks for your review. Main issue was adressed.
Integration tests would a bit hard because debug.ReadBuildInfo didn't inject modules in test mode (see golang/go#33976). Mocking var readBuildInfo = debug.ReadBuildInfo // this way we can inject a function that returns a modules list.
func extractModules(){
info, err := readBuildInfo()
// ....
} Do you have any suggestion? |
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.
Mocking
debug.ReadBuildInfo
might be a bit easier. The illustrate that I think is:var readBuildInfo = debug.ReadBuildInfo // this way we can inject a function that returns a modules list. func extractModules(){ info, err := readBuildInfo() // .... }Do you have any suggestion?
What you wrote works. There is one drawback that is global state, and tests could not run in parallel if we ever wanted to.
I do have a suggestion for an alternative design. Here's a bit of a skeleton:
func extractModules(*debug.BuildInfo) map[string]string {
// ...
}
// ...
info, ok := debug.ReadBuildInfo()
// ...
_ = extractModules(info)
func Test...(t *testing.T) {
tests := []...
// ...
for _, tt := range tests {
tt := tt
// ...
t.Run( //...
got := extractModules(tt.info)
if diff := cmp.Diff(tt.want, got); diff != "" {
// ...
}
})
}
}
With this we can have several test cases that each define the input as a *debug.BuildInfo
and the corresponding expected map[string]string
. This way we are back to simple input-output comparisons and no need for any mocks or other supporting code.
What do you think?
integrations.go
Outdated
for _, dep := range info.Deps { | ||
ver := dep.Version | ||
if dep.Replace != nil { | ||
ver += fmt.Sprintf(" => %s %s", dep.Replace.Path, dep.Version) |
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.
When the replacement is coming from a local path, there is no version (empty 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.
This is my mistake. It should be dep.Replace.Version
not dep.Version
.
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.
@rhcarvalho Fixed
@rhcarvalho That's great idea! Let's try. |
The helper assertEqual takes (got, want).
When a test fails, cmp.Diff produces output which makes it easier to spot the difference, what went wrong.
Because now we're printing a fixed string, without string formatting.
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.
@wingyplus awesome PR. Thank you!! 🥳
Going to release this into 0.6.0
.
info: &debug.BuildInfo{ | ||
Main: debug.Module{ | ||
Path: "my/module", | ||
Version: "(devel)", |
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 just realized that at the moment the Version
of the main module (in fact, the module that contains package main
) is always "(devel)"
. That is unfortunate, but at least the module name will be in Sentry. Proper version can be added using the Releases feature.
if diff := cmp.Diff(tt.want, got); diff != "" { | ||
t.Errorf("modules info mismatch (-want +got):\n%s", diff) | ||
} |
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 took the liberty to change this to use cmp.Diff
to make it easier to spot problems when tests fail.
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.
@rhcarvalho I think we can turn assertEquals
to uses cmp.Diff
to display the diff when it's not equal. What do you think?
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 don't see the point, as it just limits the usability. Personally I prefer calling cmp.Diff
directly, it is more clear and flexible.
assertEquals
always calls t.Fatal*
, even when t.Error*
would be more appropriate. Moving cmp.Diff
into assertEquals
would also limit control of comparison options.
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.
Ah, I see. Thanks for your point.
@rhcarvalho thanks!! 😃 |
This changes getting modules from
runtime/debug
instead of getting it fromgo.mod
orvendor/modules.txt
.Fixes #184