-
Notifications
You must be signed in to change notification settings - Fork 209
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
Implement an AllowAllUnexported Option #40
Comments
Policy 1 would cause failing tests to unexpectedly pass because Policy 2 (the camp I fall under), goes by the philosophy that the test writer should know explicitly what they are comparing, and provide an explicit whitelist (via Policy 3, goes by the philosophy that comparing on unexported fields generally works (even if unexported fields have absolutely no compatibility guarantees). In the event where it would have failed (e.g., For the time being, I took the stance of policy 2, but am sympathetic to policy 3. However, I want to be driven by more data that policy 2 is causing more problems than it is solving. The use case you are suggesting seems to be one where you want to provide a blanket way to compare all types, which was precisely the type of problem we were trying to avoid with this package. I heavily believe users must know what they are comparing and should take at least some minimal steps to ensure that their comparisons are forward compatible. That being said, if you want to experiment with an func DeepAllowUnexported(vs ...interface{}) cmp.Option {
m := make(map[reflect.Type]struct{})
for _, v := range vs {
structTypes(reflect.ValueOf(v), m)
}
var typs []interface{}
for t := range m {
typs = append(typs, reflect.New(t).Elem().Interface())
}
return cmp.AllowUnexported(typs...)
}
func structTypes(v reflect.Value, m map[reflect.Type]struct{}) {
if !v.IsValid() {
return
}
switch v.Kind() {
case reflect.Ptr:
if !v.IsNil() {
structTypes(v.Elem(), m)
}
case reflect.Interface:
if !v.IsNil() {
structTypes(v.Elem(), m)
}
case reflect.Slice, reflect.Array:
for i := 0; i < v.Len(); i++ {
structTypes(v.Index(i), m)
}
case reflect.Map:
for _, k := range v.MapKeys() {
structTypes(v.MapIndex(k), m)
}
case reflect.Struct:
m[v.Type()] = struct{}{}
for i := 0; i < v.NumField(); i++ {
structTypes(v.Field(i), m)
}
}
} Thus, to perform the equivalent of cmp.Equal(x, y, DeepAllowUnexported(x, y), cmp.Options(opts)) |
@dsnet thanks! Furthermore, I understand and agree with the concerns that users should know exactly what they are testing. With that being said though there are already many tests (any tests using the |
github.com/google/go-cmp does not compare unexported fields by default and there is no straightforward way to enable this. There is a discussion on google/go-cmp#40 and a suggested workaround but that still feels clumsy. During my time at eBay we developed a comparator which handles the usual test cases fine and which is a small change after the test refactoring from gopcua#128.
github.com/google/go-cmp does not compare unexported fields by default and there is no straightforward way to enable this. There is a discussion on google/go-cmp#40 and a suggested workaround but that still feels clumsy. During my time at eBay we developed a comparator which handles the usual test cases fine and which is a small change after the test refactoring from gopcua#128.
Any chance of DeepAllowUnexported being added to cmpopts? |
With a big note on its usage. This can sometimes be useful. Fixes: google#40
As a counter-proposal to For example,
but not struct types types found in:
Note: although not advertised, However, I don't want to act on my proposal yet. The |
How about all unexported fields of structs in the current package (from where cmp was called) are allowed by default. This seems really natural to me. It would ensure they do not depend on private fields from external packages but still allow them to compare private fields in the current package, where they most often would expect to do so. |
I can see the benefit of doing this and we considered this approach during the development of
|
Why does Regarding the comparison of unexpected fields, I think the present approach is too restrictive. Any time its bad idea to compare a struct directly, library authors will usually provide a Given Given the verbosity and surprise with the behaviour of this library in regards to unexported fields and in my opinion, relatively little benefit, I'd say its better to allow unexported fields to be comparable by default. |
Reflection only allows you to interact with unexported fields through the reflect API, but does not allow you to obtain an In regards to the rest of your comment, it may be time to re-evaluate the policy on unexported fields now that more usage of |
I analyzed a large corpus of Go code without tens of thousands of imports of Of all the usages That said there is a minority that wants the ability to compare all unexported regardless of the risks it presents. As such, I'm thinking about adding support for #40 (comment), which would also allow the user to explicitly say that they want to compare on all unexported fields. |
It's not clear to me how the data proves the current policy is correct. There aren't many situations where it actually protects you from doing something you don't want to do. The data makes this clear as 98% of the time, the types were in control of the user so The other 2% of the time, I don't think its fair to the usage was incorrect. It could very well have been correct if the type implemented Furthermore, the tradeoff with the current policy is to favour library authors at the expense of users but there are usually more library users than authors. Thus this seems to be the wrong tradeoff. |
Had the number of improper usages of Also, there were twice as many custom
The discussion here should not involve the
Yes, the current policy favors library authors. More users than authors does not imply that user convenience is the right policy. The fact that there are more users of a library than the authors of the library suggests that the library author carry more weight than the individual users. |
I don't follow this logic. This just proves it's easy to use, not that its the right policy. After all, its just adding an option.
Interesting. Could you show me an example of when someone needed a custom Comparer? The only time I ran into using a custom comparer was for comparing
That is true but that wasn't the purpose of me saying that. We should ignore incorrect usage results in the analysis involving types with a
Why does the library author carry more weight when the library has more users? All it shows is the library author has a popular library, and if anything, a lot of code is being written using it and thus the boilerplate should be minimal. Also how exactly did you conduct your analysis? What is a type a user owned versus one that isn't? Interesting related discussion at: golang/go#30450 |
An improvement to a widely used library benefits most (if not all) users, while more users of a library does not bring direct benefit back to the library (or other users). Allowing users to depend on unexported fields by default prevents library authors from being able to make changes to the internal representation of their packages. I am the owner of several widely used packages and I see this problem happening over and over again. An example might be something like this:
The current policy requires all users to pay a small up-front cost either by:
By having the user be explicit about what they are comparing is to their benefit. It greatly reduces the chances of being broken by upstream changes and more clearly codifies what the test was even testing for. It is far better to have breakage resistance today than to break later on and try to fix an issue long after you have forgotten about the code. In social sciences, this phenomenon is known as the "Tragedy of the Commons", which describes "a situation in a shared-resource system where individual users acting independently according to their own self-interest behave contrary to the common good of all users". Analogue to our situation:
Allowing unexported fields by default is convenient for users (and towards their individual self-interest), but it is unhelpful for the overall ecosystem since it allows for the creation of brittle tests that break on changes in dependencies that otherwise should not matter to the user. This slows down or prevents library improvements and is contrary to the good of all users. I hope this explanation convinces you, but I also recognize that the choice of policy is also subjective. This is a fundamentally a social sciences problem where there is no "right approach".
Unfortunately I can't release the data, but type "ownership" was a defined by a heuristic that compared the import paths for the type and where it was called from. For open-source code, if the type is declared in the same repo as the where the test, then the test owner owns the type. Within Google (which uses a massive mono-repo), ownership was fuzzily determined as long as the two paths shared a sufficiently long path prefix. |
I really appreciate the thorough response.
I don't maintain widely used libraries but I do read a lot of GitHub and frequently contribute. I personally have never seen an issue with private field comparisons being broken after an update. It'd help if you could link me to such an issue. But regardless, I'll defer to your experience. If you have seen it happen often, then yes the current policy does make sense. |
At least two issues come to mind:
There are many more minor issues filed, but finding them is like finding needles in a haystack. |
Those issues are more regarding misuse of |
The discussion here is regarding whether The presence of the We could add an The question is what to do when a type lacks an |
I disagree. It's a much bigger problem with
But they wouldn't have been doing so if they were using cmp which makes the example less convincing.
Likewise here. Wouldn't happen with
In my experience, there are very few comparisons where there is no |
Under that same argument, the current policy is reasonable since anyone can easily adjust
But Also, I don't think we should add an
If that's you're perspective, then why bother arguing for Unfortunately, a cursory glance (both in Google and the open-source repos) of types defined in Go reveals that most types actually do not have an I alluded to this earlier, but "just adding an |
That's where we disagree, its not easy. Its verbose and messy and the general solution you've provided here works well (we use it at @cdr) but it's fairly verbose as well even if it can be wrapped in a function.
What's wrong with that argument? If a type has private fields but there is an expectation that it will be compared, which there is for all the types we've discussed, an Equal method should be provided and thus
I apologize, I forgot to clarify that the other condition is the comparison is illegal. Most of the times when a type lacks an equal method, has private fields and is being compared, the comparison is legal. However, after all this time has passed, I'm not strongly in favour of allowing private comparisons by default. Most of the time I'm comparing private fields are within the same package where I think it'd be a good compromise for |
It seems that #116 will satisfy your use case. While I'm still opposed to an I'm still not satisfied with the API of #116 and hopefully will get around to thinking about it more soon. |
It's similar but what I'm arguing for is that by default, if the type being compared is in the same package as where the comparison is taking place, it should be ok to allow unexpected fields to be compared. |
I understand the sentiment, but I don't know how to implement that:
|
Is there any implementation of Go that does not support it? It'd make debugging hell.
We can use
Fair point, I had not considered this. However, I think it'd be ok if the policy was that if there is any function in the list of callers that shares the same package as the type being compared, then comparing unexpected fields is ok. |
Also for when the Frame is not known or Function is empty, we can just fallback to the current behaviour. |
Yes (and it does make debugging hell). It's not so much that the implementation doesn't support it, but rather that it's implementation is buggy or incomplete. Accurate stack frame information is (unfortunately) more often considered a nice-to-have rather than a necessity of proper program functioning.
That field is only intended for human debugging. It doesn't have a formally specified grammar that can be always parsed in an unambiguous way. For example, is
I'm not fond of any approach that has different behavior depending on where it's run from or what tool is used to build it. |
Makes sense. We can just fall back to the current behaviour though since most users will use a Go implementation with accurate frame information.
We don't need to accurately parse the package from that field. Just need to check whether it matches the package in the caller i.e foo/bar/pkg.v1.Foo could refer to a caller in package foo/bar/pkg.v1 or in foo/bar/pkg but not both. If we compare against the package path of the type and find a match for all path segments and a prefix for the last segment, then that should work.
I agree in general but in this case, I'd argue it's an intuitive extension of Go's package public/private model. For most users, it'll just work transparently and as expected. |
Even the package prefix function in #116 would suffer from the same faults but I think it'd be worse as you'd have to manually update the package path when renaming/moving. |
I sent out #176 which should address everything requested here. It simply adds an An Exporter(func(reflect.Type) bool { return true }) An exporter that is dependent on the current call stack can also be implemented in terms of this since it's just a function. |
Seems good to me. |
Add an Exporter option that accepts a function to determine which struct types to permit access to unexported fields. Treat this as a first-class option and implement AllowUnexported in terms of the new Exporter option. The new Exporter option: * Better matches the existing style of top-level options both by name (e.g., Comparer, Transformer, and Reporter) and by API style (all accept a function). * Is more flexible as it enables users to functionally implement AllowAllUnexported by simply doing: Exporter(func(reflect.Type) bool { return true }) Fixes #40
Is there an equivalent trick to get "IgnoreAllUnexported" semantics? |
Hi, I was wondering if you would consider adding a new option which would apply the
AllowUnexported
Option to all types and would not require a slice of input like the currentAllowUnexported
function does?My use case is that I have a lot of tests which use the
github.com/stretchr/testify
package, which ultimately usesreflect.DeepEqual
for comparing two objects, that break in Go 1.9 because they try to compare structs which containtime.Time
fields. Admittedly, I could define anEqual
method for all such structs but that doesn't seem to address the root of the problem to me, which is that I would likereflect.DeepEqual
functionality (which compares all fields, both private and public) with the caveat that I would like to check if the types being compared have anEqual
method for testing equality. Furthermore, while for structs I could pass the structs being compared toAllowUnexported
, I would like the same behavior for composite types as well (e.g. maps and slices) which is not supported byAllowUnexported
currently since it specifically checks that the types of the objects passed to it are structs.I would be more than happy to put up a PR for such an
Option
, a first look at the code leads me to believe that we could define an additional field onstate
indicating that we want to always check private fields. Then, when set to true, we would always extract a struct's fields intryExporting
. With that being said, I got the impression that you might be opposed to such anOption
given the warnings forAllowUnexported
already and so I thought it better to open an issue first where we could discuss.Thanks!
The text was updated successfully, but these errors were encountered: