-
-
Notifications
You must be signed in to change notification settings - Fork 2.6k
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
[#434] Enable mocking generic methods #33
Conversation
bump: Please review changes |
Interesting, I'll need to check a bit more your changes when I will have some free time to code, and see if anything is broken. Also did you tried on your build farm to see if it's compiling well? Also could you add more description in the PR body, like why you did and how it solves the problem, it'll avoid a roundtrip on the following issues 434 and 212. However I wouldn't yet remove the bug tests, as they should now pass, right ? And most importantly thanks for the investigation and patch. |
I also removed report_why_this_exception_happen test in ParentClassNotPublicVeryWeirdBugTest. I believe that this test is added in order to inform developer when he (accidentally) fix this bug. It was great pleasure for me to work on this issue as I learnt a lot of. I would like to contribute to this project even more. Could you tell me which issues are the most important? (I know that there are priorities but maybe some of them are special) |
Hey @alberskib finally I got some time to take a look at this very interesting tweak. Could you detail this part ?
From my experience I only saw For example look at this bridge method for the case described in issue 212 (parent class not public)
The second code shows the bytecode for when the method is a specialized version of the generic parent. You can spot the
|
Also is it possible to add a dedicated test for the |
Also I still have a concerns about method equivalence (real vs bridge) regarding every feature of mockito. Typically rewriting the assertions of your test pass too. But I still fear something will go wrong in some special cases where bridge method are involved. I may need to recheck how these internals were done :)
|
Anyway this looks like very good work / research ! |
Unfortunately my fears came true, Mockito takes do not identify properly bridge methods, it especially appear when stubbing and verifying while the production code uses the upper class, with erasure. package org.mockitousage.bugs;
import static org.fest.assertions.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import java.util.Observable;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.runners.MockitoJUnitRunner;
@RunWith(MockitoJUnitRunner.class)
public class BridgeMethodAndPolymorphimTest {
@Test
public void should_detect_erasure_bridge_method_when_verifying___when_production_code_invokes_interface() {
// given
SpecializedChild mock = mock(SpecializedChild.class);
// when
new ProductionCodeInvokingInterface().invoke(mock, "A");
// then
verify(mock).wontFail("A");
}
@Test
public void should_detect_erasure_bridge_method_when_stubbing___when_production_code_invokes_interface() {
// given
SpecializedChild mock = mock(SpecializedChild.class);
Observable value_to_be_returned = new Observable();
given(mock.wontFail("A")).willReturn(value_to_be_returned);
// when
Observable actual_returned_value = new ProductionCodeInvokingInterface().invoke(mock, "A");
// then
assertThat(actual_returned_value).isSameAs(value_to_be_returned);
}
@Test
public void should_detect_erasure_bridge_method_when_verifying___when_production_code_invokes_child() {
// given
SpecializedChild mock = mock(SpecializedChild.class);
// when
new ProductionCodeInvokingSpecializedChild().invoke(mock, "A");
// then
verify(mock).wontFail("A");
}
@Test
public void should_detect_erasure_bridge_method_when_stubbing___when_production_code_invokes_child() {
// given
SpecializedChild mock = mock(SpecializedChild.class);
Observable value_to_be_returned = new Observable();
given(mock.wontFail("A")).willReturn(value_to_be_returned);
// when
Observable actual_returned_value = new ProductionCodeInvokingSpecializedChild().invoke(mock, "A");
// then
assertThat(actual_returned_value).isSameAs(value_to_be_returned);
}
static class ProductionCodeInvokingInterface {
public <A, B extends CharSequence> A invoke(Interface<A, B> theInterface, B b) {
return theInterface.wontFail(b);
}
}
static class ProductionCodeInvokingSpecializedChild {
public Observable invoke(SpecializedChild theChild, String b) {
return theChild.wontFail(b);
}
}
static interface Interface<A, B extends CharSequence> {
A wontFail(B b);
}
static class SpecializedChild implements Interface<Observable, String> {
@Override
public Observable wontFail(String s) {
throw new IllegalStateException("and yet it fails");
}
// will generate bridge method with signature like :
// Object wontFail(CharSequence b);
}
} By the way note that erasure, do not always erase to Object
|
Nice catch! I will take a look into this tomorrow. I know what is the problem but I need to find solution. |
The problem is with Invocation matcher. This class actually does not take into account bridge methods when comparing method invocations(before this change it was not necessary). If you know some other places where method equality is checked it will be great if you tell me. Thanks to it I will be able to handle all potential problems that this change could introduce. |
Yes good diving, this is the I've made a simple adjustment in the code, the whole test suite pass in Mockito now, but I'd like to test this change in the real world. And see if we can cover more stuff regarding bridge methods (i'm thinking if there's a case where overloading is meddling with generics overriding). Here's my change. public boolean hasSameMethod(Invocation candidate) {
//not using method.equals() for 1 good reason:
//sometimes java generates forwarding methods when generics are in play see JavaGenericsForwardingMethodsTest
Method m1 = invocation.getMethod();
Method m2 = candidate.getMethod();
boolean isM2Bridge = m2.isBridge();
if (m1.getName() != null && m1.getName().equals(m2.getName())) {
/* Avoid unnecessary cloning */
Class[] params1 = m1.getParameterTypes();
Class[] params2 = m2.getParameterTypes();
if (params1.length == params2.length) {
for (int i = 0; i < params1.length; i++) {
if (hasNonMatchingArgTypes(isM2Bridge, params1[i], params2[i]))
return false;
}
return true;
}
}
return false;
}
private boolean hasNonMatchingArgTypes(boolean isM2Bridge, Class argType1, Class argType2) {
return isM2Bridge ? !argType2.isAssignableFrom(argType1) : argType1 != argType2;
} |
Unfortunately I think that it does not solve problem. Imagine next situtation(please take a look into code not the result of test): @RunWith(MockitoJUnitRunner.class)
public class BridgeMethodAndPolymorphimTest {
@Test
public void should_detect_erasure_bridge_method_when_verifying___when_production_code_invokes_interface() {
// given
SpecializedChild mock = mock(SpecializedChild.class);
// when
new ProductionCodeInvokingInterface().invoke(mock, new Observable());
// then
verify((Interface<Observable, String>)mock).wontFail(Matchers.<String>any());
}
static class ProductionCodeInvokingInterface {
public <A, B> A invoke(InterfaceForInterface<A,B> theInterface, B b) {
return theInterface.wontFail(b);
}
}
static class ProductionCodeInvokingSpecializedChild {
public Observable invoke(SpecializedChild theChild, String b) {
return theChild.wontFail(b);
}
}
static interface InterfaceForInterface<A, B>{
A wontFail(B b);
}
static interface Interface<A, B extends CharSequence>{
A wontFail(B b);
}
static class SpecializedChild implements Interface<Observable, String>, InterfaceForInterface<String, Observable> {
@Override
public Observable wontFail(String s) {
throw new IllegalStateException("and yet it fails");
}
@Override public String wontFail(Observable observable) {
return null;
}
// will generate bridge method with signature like :
// Object wontFail(CharSequence b);
}
} I can imagine situation where InvocationMatcher must check whether methods are equals based on next parameters:
with two real methods (not bridge)
and Invocation Matcher must decide whether: |
I agree with you! The proposed patch was just an exploration of how to fix the issue. Indeed this is one of the case I had in mind that could potentially cause problems for this issue. // bridge methods
wontFail(Object)
wontFail(CharSequence)
// real methods (overload)
wontFail(Observable)
wontFail(String) This case may be the real killer of this PR, as we cannot regress on this one. But let's create tests for each cases, and advance one step at a time to see on far it's possible to go. Also I don't think it's a good idea to reprocess the bytecode (CGLIB already do that and it's slow), as the whole class file has to be read to just look at bridge methods (even if cached). If we had our own bytecode engine that would be a different story. Your proposed test case could make sense if the test is rewritten this way : @Test
public void should_detect_erasure_bridge_method_when_verifying___when_production_code_invokes_interface() {
// given
Interface<Observable, String> mock = mock(SpecializedChild.class);
// when
new ProductionCodeInvokingInterface().invoke(mock, new Observable());
// then
verify(mock).wontFail(Matchers.<String>any());
} |
I think that the best idea to solve this issue is to write some method getRealMethod that will find bridge delegation. Thanks to it code will looks like this: public boolean hasSameMethod(Invocation candidate) {
//not using method.equals() for 1 good reason:
//sometimes java generates forwarding methods when generics are in play see JavaGenericsForwardingMethodsTest
Method m1 = getRealMehod(invocation.getMethod());
Method m2 = getRealMehod(candidate.getMethod());
if (m1.getName() != null && m1.getName().equals(m2.getName())) {
/* Avoid unnecessary cloning */
Class[] params1 = m1.getParameterTypes();
Class[] params2 = m2.getParameterTypes();
if (params1.length == params2.length) {
for (int i = 0; i < params1.length; i++) {
if (params1[i] != params2[i])
return false;
}
return true;
}
}
return false;
}
private Method getRealMehod(Method m){
if (!m.isBridge()){
return m
}
// some code
} |
It looks like that finding method to which bridge is delegating is impossible by matching argument types. Please take a look at next example: ....
static class AClass{ }
static class BClass extends AClass{ }
static class CClass extends BClass { }
static class DClass extends CClass{ }
static class EClass extends DClass{ }
static interface IntA<A extends BClass>{
int test (A a);
}
static interface IntB<B extends CClass>{
int test (B b);
}
static class SpecializedAB implements IntA<DClass>, IntB<EClass> {
@Override
public int test(DClass s) {
return 1;
}
@Override public int test(EClass s) {
return 1;
}
}
.... Will result in
Generally it is really difficult problem. Let's imagine that we find great way to determine method to which bridge is delegating invocation(not necessarily by parsing bytecode - alternative way is to analyze call stack during proxy creation). In this situation we comeback to the entry point of this problem as such analyze will lead to the same situation( |
Yeah that's what I feared. Still I don't want to close it yet. So if progress is done there... We could still modify CGLIB.... in order to provide a bridge method callback, or something else so we know what method the bridge method is calling. But I'm quite un confortable patching CGLIB for several reasons (maintenance mostly). |
Awesome research. Do you guys want to keep this PR open for now? |
Guys, I'm going to close this PR to manage WIP in Mockito. I hope that's ok. Feel free to continue discussing it here or on the mailing list. I'd love to fix this problem. |
By the way I had managed to get in touch with some people behind CGLIB, and while they seem busy with other stuff they put CGLIB on github. The repo might change of owner though (like a CGLIB org). Anyway that's good news. So we already discussed that CGLIB must be extended to provide at invocation time the method that the bridge will call. One warning I will issue is that bridge method's bytecode may vary depending on the compiler (IBM J9, Eclipse, different version, etc), however bridge methods are simple enough to deal with that. And I won't mind keeping this PR open. Maybe we could assign a Research milestone. |
Update dependencies
This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [org.mockito:mockito-junit-jupiter](https://github.com/mockito/mockito) | test | minor | `5.11.0` -> `5.12.0` | | [org.geotools:gt-geojson-core](https://github.com/geotools/geotools) | compile | minor | `31.0` -> `31.1` | | [org.geotools:gt-epsg-extension](https://github.com/geotools/geotools) | compile | minor | `31.0` -> `31.1` | | [org.geotools:gt-referencing](https://github.com/geotools/geotools) | compile | minor | `31.0` -> `31.1` | | [org.geotools:gt-epsg-hsql](https://github.com/geotools/geotools) | compile | minor | `31.0` -> `31.1` | | [org.springframework.boot:spring-boot-dependencies](https://spring.io/projects/spring-boot) ([source](https://github.com/spring-projects/spring-boot)) | parent | minor | `3.2.5` -> `3.3.0` | --- ### Release Notes <details> <summary>mockito/mockito (org.mockito:mockito-junit-jupiter)</summary> ### [`v5.12.0`](https://github.com/mockito/mockito/releases/tag/v5.12.0) [Compare Source](mockito/mockito@v5.11.0...v5.12.0) <sup><sup>*Changelog generated by [Shipkit Changelog Gradle Plugin](https://github.com/shipkit/shipkit-changelog)*</sup></sup> ##### 5.12.0 - 2024-05-11 - [25 commit(s)](mockito/mockito@v5.11.0...v5.12.0) by Piotr Przybylak, Stefano Cordio, Tim van der Lippe, dependabot\[bot], jonghoonpark - Bump com.gradle.enterprise from 3.17.2 to 3.17.3 [(#​3341)](mockito/mockito#3341) - Bump org.jetbrains.kotlin:kotlin-stdlib from 1.9.23 to 1.9.24 [(#​3339)](mockito/mockito#3339) - Bump versions.bytebuddy from 1.14.14 to 1.14.15 [(#​3338)](mockito/mockito#3338) - Bump org.shipkit:shipkit-auto-version from 2.0.6 to 2.0.7 [(#​3337)](mockito/mockito#3337) - Bump org.jetbrains.kotlin:kotlin-gradle-plugin from 1.9.23 to 1.9.24 [(#​3336)](mockito/mockito#3336) - Fixes [#​3331](mockito/mockito#3331) : Fix `AdditionalMatchers.and()` and `AdditionalMatchers.or()` not to swap the order of matchers [(#​3335)](mockito/mockito#3335) - AdditionalMatchers.and() and or() swap matcher order [(#​3331)](mockito/mockito#3331) - Bump gradle/wrapper-validation-action from 3.3.1 to 3.3.2 [(#​3327)](mockito/mockito#3327) - Bump versions.bytebuddy from 1.14.13 to 1.14.14 [(#​3324)](mockito/mockito#3324) - Bump org.shipkit:shipkit-auto-version from 2.0.5 to 2.0.6 [(#​3322)](mockito/mockito#3322) - Bump gradle/wrapper-validation-action from 3.3.0 to 3.3.1 [(#​3320)](mockito/mockito#3320) - Bump com.gradle.enterprise from 3.17 to 3.17.2 [(#​3318)](mockito/mockito#33...
This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [org.mockito:mockito-junit-jupiter](https://github.com/mockito/mockito) | test | minor | `5.11.0` -> `5.12.0` | | [org.geotools:gt-geojson-core](https://github.com/geotools/geotools) | compile | minor | `31.0` -> `31.1` | | [org.geotools:gt-epsg-extension](https://github.com/geotools/geotools) | compile | minor | `31.0` -> `31.1` | | [org.geotools:gt-referencing](https://github.com/geotools/geotools) | compile | minor | `31.0` -> `31.1` | | [org.geotools:gt-epsg-hsql](https://github.com/geotools/geotools) | compile | minor | `31.0` -> `31.1` | | [org.springframework.boot:spring-boot-dependencies](https://spring.io/projects/spring-boot) ([source](https://github.com/spring-projects/spring-boot)) | parent | minor | `3.2.5` -> `3.3.0` | --- ### Release Notes <details> <summary>mockito/mockito (org.mockito:mockito-junit-jupiter)</summary> ### [`v5.12.0`](https://github.com/mockito/mockito/releases/tag/v5.12.0) [Compare Source](mockito/mockito@v5.11.0...v5.12.0) <sup><sup>*Changelog generated by [Shipkit Changelog Gradle Plugin](https://github.com/shipkit/shipkit-changelog)*</sup></sup> ##### 5.12.0 - 2024-05-11 - [25 commit(s)](mockito/mockito@v5.11.0...v5.12.0) by Piotr Przybylak, Stefano Cordio, Tim van der Lippe, dependabot\[bot], jonghoonpark - Bump com.gradle.enterprise from 3.17.2 to 3.17.3 [(#​3341)](mockito/mockito#3341) - Bump org.jetbrains.kotlin:kotlin-stdlib from 1.9.23 to 1.9.24 [(#​3339)](mockito/mockito#3339) - Bump versions.bytebuddy from 1.14.14 to 1.14.15 [(#​3338)](mockito/mockito#3338) - Bump org.shipkit:shipkit-auto-version from 2.0.6 to 2.0.7 [(#​3337)](mockito/mockito#3337) - Bump org.jetbrains.kotlin:kotlin-gradle-plugin from 1.9.23 to 1.9.24 [(#​3336)](mockito/mockito#3336) - Fixes [#​3331](mockito/mockito#3331) : Fix `AdditionalMatchers.and()` and `AdditionalMatchers.or()` not to swap the order of matchers [(#​3335)](mockito/mockito#3335) - AdditionalMatchers.and() and or() swap matcher order [(#​3331)](mockito/mockito#3331) - Bump gradle/wrapper-validation-action from 3.3.1 to 3.3.2 [(#​3327)](mockito/mockito#3327) - Bump versions.bytebuddy from 1.14.13 to 1.14.14 [(#​3324)](mockito/mockito#3324) - Bump org.shipkit:shipkit-auto-version from 2.0.5 to 2.0.6 [(#​3322)](mockito/mockito#3322) - Bump gradle/wrapper-validation-action from 3.3.0 to 3.3.1 [(#​3320)](mockito/mockito#3320) - Bump com.gradle.enterprise from 3.17 to 3.17.2 [(#​3318)](mockito/mockito#33...
Now all methods invocations are intercepted by cglib proxy (previously bridge methods are skipped). Thanks to it some specific method invocations that use bridge methods to invoke proper implementations can also be mocked.
Why?
Usually bridge methods delegates invocations by INVOKE_VIRTUAL operation (bytecode inside implementation of bridge method) but sometimes it uses INVOKE_STATIC(some specific situtatuion). As a result INVOKE_VIRTUAL was handled by cglib proxy but INVOKE_STATIC not. It leads to the situtatuion that method invocation by using bridge method with INVOKE_STATIC operation cannot be mocked.
Provided change ensures that all methods invocations are handled by cglib proxy.
In order to properly identifiy compareTo invocations this change looks also for erasured version of compareTo.
One test is removed as it does not make sense. This tests check whether mock invoke some "real" method by invocations of bridge one.
Regarding issue 212: