-
Notifications
You must be signed in to change notification settings - Fork 75
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
not taking all null paths #123
Comments
The reason why the exception is not reached, is that EqualsVerifier does not check the entire state space (see also the FAQ). Depending on whether id is null, your equals method behaves very differently. Of course EqualsVerifier does some basic checks for null, but it doesn't check everything with regards to null, just like it doesn't check everything with regards to 1, 1337 or 123456789. There are a lot of possible long values. That said, null is a special enough value that it might warrant a few extra checks. I'll look into that. I think at least the EqualsVerifier check for TestB should have failed. Also, if you're trying to do the "canEqual" thing as described in the article bij Odersky, Venners and Spoon, you should notice that it should be "that.newCanEqual(this)", and not "this.newCanEqual(that)", though it doesn't lead to different results in the EqualsVerifier check. I didn't quite understand your final paragraph about non-final and different jars. |
so I have a git repository for my AbstractEntityBase which is implementing Identified. Different Repositories use it. In another repository I don't have to suppress
yeah, I agree, I wasn't really expecting more. |
Hi, I would be very surprised if that were true. Bytecode doesn't just change if it's in another jar, and EqualsVerifier certainly doesn't check what jar a class came from. Maybe there's a stale jar in a cache somewhere though? In principle, Warning.NONFINAL_FIELDS is always needed if fields aren't final. The only exception is, when the field isn't actually referenced in the equals method. That applies to TestB's name field. So perhaps TestB's superclass doesn't get loaded correctly? |
maybe? but that'd be just as weird, and I don't think that hibernate and such would work in that case. although you can see in Identified I don't reference it directly, I'm only referencing |
OK, I'm looking forward to that. I've spent some time trying to understand what's happening in your equals method above, and I've managed to find a way to make EqualsVerifier fail on TestB. I'll include that in the next release. I'll look at finding a way to do the same for TestA, and if I do I'll include that too. I'll let you know when it's released. I have to say though, this might be one of the more complicated equals methods I've seen so far, and I've seen a few. The cyclomatic complexity of that thing must be pretty high, it took me quite a while to figure out how it works. BTW -- if you remove the else before |
I wasn't able to find a way to make the TestA test fail, because technically, it isn't wrong (as long as newCanEqual returns true). The only way to make it incorrect, is from a subclass such as TestB, that takes an extension point of the equals method and implements it incorrectly. (Which is why I recommend against suppressing STRICT_INHERITANCE.) Hoewever, I just released EqualsVerifier 1.7.4, which contains the change I mentioned before that makes the TestB test fail. |
I updated Identified too
|
Hi, Well, I still would've liked it if it could have flagged TestA, because while technically correct, it's not a good equals method. I can't reproduce any issue with the code you just posted. I've tried it both with 1.7.3 and 1.7.4, and the test passes just fine. It would help a lot if you could create a zip with a complete maven project (or another build tool) that reproduces the issue. |
well in theory the problem is is that it passes, I would think it should fail with "non final fields" and not including fields in hashcode. I did notice after sharing this that I'm still not presenting the full problem which is it's not catching that id is not marked final in my code (it is here). I can tar up the 4 repos of my code? though, not sure where you'd like me to put said archive? or just give you access on bitbucket... Though the hashcode thing is fun, as soon as I include name in hashcode it'll fail with superclass not matching, which is probably correct, but not what that article you suggest demonstrates... so why is it bad this time? |
Hi, Thanks for the update. I misunderstood, and didn't realise that you expected it to fail when it didn't, instead of the other way around. I've spent some time looking at it, and I finally understand what's going on. You ask why EV succeeds when the fields are non-final, without suppressing Why it doesn't fail on This obviously begs the question: why does EqualsVerifier think that Once you fix this, you'll start running into issues relating to inheritance and subclasses, which is where my main issue with this implementation of Each of these are hard things that are difficult to get right on their own, let alone when you mix them together. These behaviours interact in subtle ways, which in this case result in a seriously flawed implementation of So, why do I think that this implementation is so flawed? First of all, I'm not a big fan of the 'versioned entity' thing. It's trying to merge two very different equivalence relations into one, which may better be kept separate, for instance using a Guava Equivalence. But that's just my personal opinion: feel free to disagree. The main issue, however, is that the way Also, there's the unintended consequence of the So, my advice would be to re-think what you want out of your equals method. Do you really need the interface on top? Does it really make it easier to extend your system correctly? Because in my opinion, it has actually made things more complicated than they need to be. Also: do you really need to add state in subclasses, or is an equality relation based on Also, while I'm at it, I can't help but notice the redundant null check in Having said all that, I'll try to come up with a few checks to add to EqualsVerifier to reduce the likelyhood that classes like this one fall through the cracks. I already have a couple of ideas for that, which I will probably add in the next version. I hope this helps. Cheers, |
obviously because I'm dumb... and I've done something I tell people not to do, "don't over complicate it". The first is definitely some artifact of a generated equals that might be a little over-complicated that I merged into the method signature. The second I suspect is an artifact of some other check I had, where if id is equivalent return true, else do something else (not wanting to return on false). I'll be fixing these for sure and then combing through all the things. I think I should still send you this other archive, it may "pass" for some of the reasons you've outlined, but it encounters the non final field issue (where it passes) on id, which is not final in that example. I haven't been able to come up with a trivial example of this. |
So, all this, and the problem you care most about still hasn't come up? ;) |
well.. the problem I care about most is rather relative, it may be the same problem
Here's the tarball, yes I own the code I just haven't licensed it yet. Sorry about the complexity I know it's not an ideal test case... I just haven't been able to find the ideal test case. The test that should fail is in mmp-domain You'll note that although id is a non final field in AbstractEntityBase, running Equalsverifier against Station which extends AbstractEntityBase, does not need me to suppress non final fields. Also name equals is a lot less dumb here since it shouldn't be null, though theoretically contains an NPE problem. I might change it to use Objects.equal. This might be because, as you noted, As a final note, that "name" implementation, was only written up for that test, the Station one is the real one; though probably just as bad ;). |
Your Station class is a JPA @entity. EqualsVerifier disables the mutability check for JPA Entities because Entities don't work if they're immutable. That's why it passes without the All my other issues still stand, though: the In the mean time, I'm going to keep trying to find a way to make EqualsVerifier flag this, because I consider it a bug that it doesn't. |
huh, I'd never considered that it was the annotations. Why doesn't it do that for the Abstract then? Shouldn't it also have detected @MappedSuperClass? |
You're right. I'll fix that in the next release, as well. |
Prefix thought on my accidental bug report without reading your source (I don't know how easy this would be). in order to make EqualsVerifier accident proof... you'd have to change its API. I am fixing the I'm a bit surprised at the Scala article as it doesn't really talk about the "Traits" issue of using Composition over Inheritance, it is preferred for re-usability. Java of course has only recently gained this option, in fact it's one of the reasons I even considered migrating to Java. I wouldn't flag the method incorrect just because of usage of Composition. Honestly I only have an Abstract because JPA doesn't recognize Composition, otherwise I would annotate the interface to get the reusable bits. If everything needed to make the comparison is available in the Trait, why shouldn't it be used? Obviously Java could't make this change for legacy reasons. Though of course, for me, it's worth asking (rhetorical) what the correct behavior of a Set of Identified is, if I use a Long instead of a UUID. That would currently result in the wrong behavior. To be fair I'm not sure that 2 AbstractEntityBase should be equals either... that's a side effect of desire for DRYness, as mentioned above that class wouldn't exist if I could help it, as it isn't actually a thing in the domain. |
partially to help me understand the no bugs in this example, I think.
Thanks for the improvements, criticisms and great tool |
Hi Caleb, Your "prefix thought" is interesting indeed. I hadn't thought of solving it that way; I'll think about it! Your points example is interesting, but I'll have to investigate further to be sure that it's actually correct. You've forgotten to test the ColoredPointImpl with EqualsVerifier, but that too seems to pass (after adding a few null checks and some prefab values). However, we've seen EqualsVerifier be wrong before! So I'm still not convinced. I'll get back to you on that. I hope you'll agree that, if it's indeed needed to override both equals and canEqual like they do in the article, having the implementation also in an interface is redundant, and removing it would simplify things. Of course, all that I've said so far, was assuming that overriding both equals and canEqual is necessary. Just as an aside, the reason why the Station class from your tarball is still incorrect, even though it passes the EqualsVerifier test, is that it's not symmetric. That can easily be shown with this test:
|
OK, I found a counter-example:
The test fails, because the implementation not symmetric. Is this a contrived example? Maybe, but I could have made the code look more like a "real" equals method and have it fail in the same way. What you're trying to do, until you get it 100% right, isn't much different! These problems aren't always obvious. However, the original implementation guards against this:
This test passes, because And that's the real lesson I've learned from creating EqualsVerifier: incorrectness often comes from outside. Your As I said in my earlier post, Odersky et al.'s As an encore, I will now break
See? It's easy! :) Also, keep in mind that equals methods are often used in tight inner loops. Your |
I surrender! and let you go figure out how to make equals verifier defeat my stupidity ;) |
Well, I wouldn't call it stupidity. I'd call it misapplied ingenuity ;). I'll let you know when I release a new version with fixes for some of the things we've been discussing. |
Thanks. I did try one thing that might fix that, but I've really temporarily given up. |
Well, given the academic backgrounds of both Odersky and Spoon (2 of the co-authors of the article), I'd be surprised if they hadn't constructed a mathematical proof to show that their solution is correct, and resistant to malice and stupidity. There's another guy who invented a similar (though slightly less elegant) mechanism back in 2002, who also mentions mathematical proofs. So, I'm not saying it's impossible, but ... know what you're getting yourself into ;). |
A heads-up: I just released version 1.7.5, which fixes a number of the things we've discussed. See also the changelog. I'm closing the issue now, but feel free to re-open it, or add a new one, if you find anything else! |
awesome, thanks! |
note: this time I am calling verify, because I have caused failures.
and not sure why they both need non-final fields in this example but in a non example where the "TestA" is in a different jar from "TestB". Observed trying to use newCanEqual to verify subclass equality on name.
The text was updated successfully, but these errors were encountered: