View Javadoc
1   /*-
2    * #%L
3    * io.earcam.instrumental.reflect
4    * %%
5    * Copyright (C) 2018 earcam
6    * %%
7    * SPDX-License-Identifier: (BSD-3-Clause OR EPL-1.0 OR Apache-2.0 OR MIT)
8    * 
9    * You <b>must</b> choose to accept, in full - any individual or combination of 
10   * the following licenses:
11   * <ul>
12   * 	<li><a href="https://opensource.org/licenses/BSD-3-Clause">BSD-3-Clause</a></li>
13   * 	<li><a href="https://www.eclipse.org/legal/epl-v10.html">EPL-1.0</a></li>
14   * 	<li><a href="https://www.apache.org/licenses/LICENSE-2.0">Apache-2.0</a></li>
15   * 	<li><a href="https://opensource.org/licenses/MIT">MIT</a></li>
16   * </ul>
17   * #L%
18   */
19  package io.earcam.instrumental.reflect;
20  
21  import static io.earcam.instrumental.reflect.Matchers.aStream;
22  import static io.earcam.instrumental.reflect.Matchers.isAbsent;
23  import static io.earcam.instrumental.reflect.Matchers.isPresent;
24  import static java.util.Arrays.stream;
25  import static java.util.stream.Collectors.joining;
26  import static java.util.stream.Collectors.toList;
27  import static org.hamcrest.MatcherAssert.assertThat;
28  import static org.hamcrest.Matchers.allOf;
29  import static org.hamcrest.Matchers.arrayContaining;
30  import static org.hamcrest.Matchers.containsInAnyOrder;
31  import static org.hamcrest.Matchers.equalTo;
32  import static org.hamcrest.Matchers.hasItem;
33  import static org.hamcrest.Matchers.hasProperty;
34  import static org.hamcrest.Matchers.is;
35  import static org.hamcrest.Matchers.not;
36  import static org.junit.jupiter.api.Assertions.assertFalse;
37  import static org.junit.jupiter.api.Assertions.assertTrue;
38  
39  import java.io.Serializable;
40  import java.lang.invoke.MethodHandle;
41  import java.lang.reflect.Method;
42  import java.util.Arrays;
43  import java.util.List;
44  import java.util.Optional;
45  
46  import org.junit.jupiter.api.BeforeAll;
47  import org.junit.jupiter.api.Test;
48  
49  /**
50   * TODO Note: when refactoring this to produce test suites with meaningful
51   * hierarchies and names ;-) you must honour the synthetic accessor (i.e. must
52   * have a failing test when isSynthetic() is not filtered)
53   *
54   * Coverage instrumentation adds methods etc to classes, there cannot assert
55   * on explicit size (e.g. number of methods returned)
56   *
57   */
58  public class MethodsTest {
59  
60  	@Override
61  	protected void finalize() throws Throwable
62  	{
63  		super.finalize();
64  	}
65  
66  	private int onlyHereToInduceBridge = 42;
67  
68  	public class InnerToInduceBridge {
69  		public int nabOuterField()
70  		{
71  			return onlyHereToInduceBridge;
72  		}
73  
74  	}
75  
76  	public static class OuterClassToInduceBridge {
77  
78  		private int numb = 1;
79  
80  		public class Inner {
81  			private int num = 5;
82  
83  
84  			public int nabOuterField()
85  			{
86  				return numb;
87  			}
88  		}
89  
90  
91  		public int nabInnerField()
92  		{
93  			return new Inner().num;
94  		}
95  	}
96  
97  
98  	@SuppressWarnings("unchecked")
99  	@Test
100 	public void getMethodReturnsSynthetic()
101 	{
102 		Optional<Method> method = Methods.allMethodsOf(OuterClassToInduceBridge.Inner.class)
103 				.filter(Methods.IS_SYNTHETIC)
104 				.filter(m -> m.getReturnType().equals(int.class)) // This is needed as coverage instrumentation adds
105 																	 // synthetic method
106 				.findAny();
107 
108 		assertThat(method, isPresent());
109 		assertThat(method.get().getReturnType(), is(int.class));
110 		assertThat(method.get().getParameterTypes(), is(arrayContaining(OuterClassToInduceBridge.Inner.class)));
111 	}
112 
113 	public static class Super {
114 		public void foo()
115 		{}
116 
117 
118 		protected void bar()
119 		{}
120 	}
121 
122 	public static class Sub extends Super {
123 		@Override
124 		public void foo()
125 		{}
126 	}
127 
128 
129 	@Test
130 	public void getMethodReturnsOverriddenFromSubclass()
131 	{
132 		Method method = Methods.getMethod(Sub.class, "foo").get();
133 		assertThat(method.getDeclaringClass(), is(Sub.class));
134 	}
135 
136 
137 	@Test
138 	public void getMethodReturnsInherited()
139 	{
140 		Method method = Methods.getMethod(Sub.class, "bar").get();
141 		assertThat(method.getDeclaringClass(), is(Super.class));
142 	}
143 
144 
145 	@Test
146 	public void getMethodReturnsEmptyOptional()
147 	{
148 		assertThat(Methods.getMethod(Sub.class, "humbug").isPresent(), is(false));
149 	}
150 
151 	// ------
152 
153 	@SuppressWarnings("unused")
154 	private class BaseClass {
155 
156 		protected void boo(String goose)
157 		{}
158 	}
159 
160 	@SuppressWarnings("unused")
161 	private class SubClass extends BaseClass {
162 		@Override
163 		protected void boo(String goose)
164 		{}
165 
166 
167 		public String cheese()
168 		{
169 			return "red leicester";
170 		}
171 	}
172 
173 	@SuppressWarnings("unused")
174 	private class SubOfSubClass extends SubClass {
175 
176 		public String boo(String... geese)
177 		{
178 			return "Boo, boo I say to you geese...\n" +
179 					stream(geese).map(g -> "Boo to " + g).collect(joining("\n"));
180 		}
181 	}
182 
183 	private static Method BASE_BOO_METHOD;
184 	private static Method OVERRIDDING_BOO_METHOD;
185 
186 
187 	@BeforeAll
188 	public static void setUp() throws NoSuchMethodException, SecurityException
189 	{
190 		BASE_BOO_METHOD = BaseClass.class.getDeclaredMethod("boo", String.class);
191 		OVERRIDDING_BOO_METHOD = SubClass.class.getDeclaredMethod("boo", String.class);
192 	}
193 
194 
195 	@Test
196 	public void doesNotIncludeMethodsFromJavaLangObject()
197 	{
198 		assertThat(
199 				Methods.methodsOf(BaseClass.class)
200 						.filter(m -> m.getDeclaringClass() == Object.class)
201 						.findAny(),
202 				isAbsent());
203 
204 		assertThat(Methods.methodsOf(BaseClass.class), aStream(not(hasItem(hasProperty("declaringClass", equalTo(Object.class))))));
205 	}
206 
207 
208 	@Test
209 	public void testFindsSingleDeclaredProtectedMethod()
210 	{
211 		assertThat(Methods.methodsOf(BaseClass.class), aStream(hasItem(BASE_BOO_METHOD)));
212 	}
213 
214 
215 	@Test
216 	public void testFindsCorrectlyOverriddenMethod()
217 	{
218 		assertThat(Methods.methodsOf(SubClass.class).collect(toList()), allOf(
219 				hasItem(OVERRIDDING_BOO_METHOD),
220 				not(hasItem(BASE_BOO_METHOD))));
221 	}
222 
223 
224 	@Test
225 	public void testFindsCorrectlyOverriddenAndInheritedMethod()
226 	{
227 		assertThat(Methods.methodsOf(SubOfSubClass.class).collect(toList()), hasItem(OVERRIDDING_BOO_METHOD));
228 	}
229 
230 
231 	@Test
232 	public void testFindsAllMethodsDeclaredOrInherited()
233 	{
234 		assertThat(Methods.methodsOf(SubOfSubClass.class).collect(toList()), hasItem(OVERRIDDING_BOO_METHOD));
235 	}
236 
237 
238 	@Test
239 	public void givenExistingMethodThenOptionalIsPresent()
240 	{
241 		Optional<Method> declaredMethod = Methods.getMethod(BaseClass.class, "boo", String.class);
242 		assertTrue(declaredMethod.isPresent());
243 		assertThat(declaredMethod.get().getName(), is("boo"));
244 	}
245 
246 
247 	@Test
248 	public void givenNonExistentMethodThenOptionalIsAbsent()
249 	{
250 		assertFalse(Methods.getMethod(BaseClass.class, "nonexistent", String.class).isPresent());
251 	}
252 
253 	interface A<T extends Serializable> {
254 		void doSome(T thing);
255 	}
256 
257 	class B implements A<String> {
258 		@Override
259 		public void doSome(String thing)
260 		{}
261 	}
262 
263 
264 	@Test
265 	public void whenGenericInterfacePresentThenOnlyListConcrete()
266 	{
267 		assertThat(Methods.methodsOf(B.class)
268 				.filter(m -> "doSome".equals(m.getName()))
269 				.filter(m -> String.class.equals(m.getParameterTypes()[0]))
270 				.findAny(), isPresent());
271 	}
272 
273 	public static class W<N extends Number> {
274 		public void receive(N number)
275 		{}
276 	}
277 
278 	class X extends W<Integer> {
279 		int x;
280 
281 
282 		@Override
283 		public void receive(Integer number)
284 		{
285 			x = number;
286 		}
287 	}
288 
289 	public static class Y extends W<Float> {
290 		float y;
291 
292 
293 		@Override
294 		public void receive(Float number)
295 		{
296 			y = number;
297 		}
298 
299 
300 		public void randomUnrelatedMethod(String s1)
301 		{}
302 	}
303 
304 	public static class Z extends Y {
305 		float z;
306 
307 
308 		@Override
309 		public void receive(Float number)
310 		{
311 			z = number;
312 		}
313 
314 
315 		public void randomUnrelatedMethod(String s1, String s2)
316 		{}
317 	}
318 
319 
320 	@Test
321 	public void whenGenericSuperClassPresentForNestedInnerClassThenOnlyListConcrete()
322 	{
323 		assertThat(Methods.methodsOf(X.class)
324 				.filter(m -> "receive".equals(m.getName()))
325 				.count(), is(equalTo(1L)));
326 	}
327 
328 
329 	@Test
330 	public void whenGenericSuperClassPresentForStaticInnerClassThenOnlyListConcrete()
331 	{
332 		assertThat(Methods.methodsOf(Y.class)
333 				.filter(m -> "receive".equals(m.getName()))
334 				.count(), is(equalTo(1L)));
335 	}
336 
337 
338 	@Test
339 	public void testWhenPrivateFieldOnInnerClassIsReferencedThenSyntheticMethodShouldNotBeRetrieved()
340 	{
341 		@SuppressWarnings("unused")
342 		class IntegerEventSubscriber {
343 			private Integer event;
344 
345 
346 			public void on(Integer event)
347 			{
348 				this.event = event;
349 			}
350 		}
351 		IntegerEventSubscriber subscriber = new IntegerEventSubscriber();
352 
353 		subscriber.event = 101;  // This triggers the creation of an synthetic method, in current jvm named "access$0"
354 
355 		assertThat("Direct field access of an inner class' private field trigger the creation of a synthetic method",
356 				Arrays.stream(subscriber.getClass().getDeclaredMethods()).filter(Method::isSynthetic).findAny().isPresent(), is(true));
357 
358 		assertThat(Methods.methodsOf(IntegerEventSubscriber.class).count(), is(1L));
359 
360 	}
361 
362 
363 	@Test
364 	public void whenBlah()
365 	{
366 		assertThat(Methods.methodsOf(Z.class)
367 				.filter(m -> "receive".equals(m.getName()))
368 				.count(), is(equalTo(1L)));
369 	}
370 
371 
372 	@Test
373 	public void whenGenericInterfacePresentForClassThenOnlyListConcrete()
374 	{
375 		assertThat(Methods.methodsOf(String.CASE_INSENSITIVE_ORDER.getClass())
376 				.filter(m -> "compare".equals(m.getName()))
377 				.count(), is(equalTo(1L)));
378 	}
379 
380 	public abstract class TypeCapture<T> {
381 		public abstract void withType(T type);
382 	}
383 
384 
385 	// TODO with TypeLiteral information we can deduce the concrete type (in this case "N" is "Integer"),
386 	// need to look at class def to see upper bound "N extends..."
387 	@Test
388 	public void testGenericType()
389 	{
390 		TypeCapture<W<Integer>> tc = new TypeCapture<W<Integer>>() {
391 			@Override
392 			public void withType(W<Integer> type)
393 			{}
394 		};
395 		assertThat(Methods.methodsOf(tc.getClass())
396 				.filter(m -> "withType".equals(m.getName()))
397 				.count(), is(equalTo(1L)));
398 	}
399 
400 	public abstract class SubTypeCapture<T extends Number> extends TypeCapture<W<T>> {
401 		public abstract void withType(W<T> type);
402 
403 
404 		public abstract void withType(W<Float> type, Object otherParameter);
405 	}
406 
407 
408 	@Test
409 	public void testGenericType2()
410 	{
411 		Method overridden = Methods.getMethod(SubTypeCapture.class, "withType", W.class).get();
412 		Method overloaded = Methods.getMethod(SubTypeCapture.class, "withType", W.class, Object.class).get();
413 
414 		List<Method> methods = Methods.methodsOf(SubTypeCapture.class)
415 				.filter(m -> "withType".equals(m.getName()))
416 				.collect(toList());
417 
418 		assertThat(methods, containsInAnyOrder(overridden, overloaded));
419 	}
420 
421 	interface Foo {
422 		default void defaultMethod(String arg)
423 		{}
424 	}
425 
426 	class Bar implements Foo {
427 		@Override
428 		public void defaultMethod(String arg)
429 		{}
430 	}
431 
432 	class Bar2 implements Foo {}
433 
434 	class Bar3 extends Bar2 {}
435 
436 
437 	@Test
438 	public void methodsOfDoesNotReturnOverridenDefault()
439 	{
440 		Method method = Methods.methodsOf(Bar.class)
441 				.filter(m -> m.getName().equals("defaultMethod"))
442 				.findAny().get();
443 
444 		assertThat(method.getDeclaringClass(), is(Bar.class));
445 	}
446 
447 
448 	@Test
449 	public void methodsOfReturnsDefault()
450 	{
451 		Method method = Methods.methodsOf(Bar2.class)
452 				.filter(m -> m.getName().equals("defaultMethod"))
453 				.findAny().get();
454 
455 		assertThat(method.getDeclaringClass(), is(Foo.class));
456 	}
457 
458 
459 	@Test
460 	public void methodsOfReturnsInheritedDefault()
461 	{
462 		Method method = Methods.methodsOf(Bar3.class)
463 				.filter(m -> m.getName().equals("defaultMethod"))
464 				.findAny().get();
465 
466 		assertThat(method.getDeclaringClass(), is(Foo.class));
467 	}
468 
469 	public static interface WithDefault {
470 		default String hello(String name)
471 		{
472 			return "Hello " + name;
473 		}
474 	}
475 
476 	public static class ImplementingWithDefault implements WithDefault {
477 
478 	}
479 
480 
481 	@Test
482 	public void handleForDefaultMethod() throws Throwable
483 	{
484 		Method method = Methods.getMethod(ImplementingWithDefault.class, "hello", String.class).get();
485 
486 		ImplementingWithDefault target = new ImplementingWithDefault();
487 		String arg = "name";
488 
489 		MethodHandle handle = Methods.handleFor(method);
490 
491 		assertThat(handle.bindTo(target).invoke(arg), is(equalTo(target.hello(arg))));
492 	}
493 
494 
495 	// EARCAM_SNIPPET_BEGIN: normal-handle
496 	public String taDa()
497 	{
498 		return "TA-DA";
499 	}
500 
501 
502 	@Test
503 	public void handleForNormalMethod() throws Throwable
504 	{
505 		Method method = Methods.getMethod(MethodsTest.class, "taDa")
506 				.orElseThrow(NullPointerException::new);
507 
508 		MethodHandle handle = Methods.handleFor(method).bindTo(this);
509 
510 		assertThat(handle.invoke(), is(equalTo(taDa())));
511 	}
512 	// EARCAM_SNIPPET_END: normal-handle
513 
514 
515 	public static String baddaBing()
516 	{
517 		return "Badda Bing";
518 	}
519 
520 
521 	@Test
522 	public void handleForStaticMethod() throws Throwable
523 	{
524 		Method method = Methods.getMethod(MethodsTest.class, "baddaBing").get();
525 		MethodHandle handle = Methods.handleFor(method);
526 
527 		assertThat(handle.invoke(), is(equalTo(baddaBing())));
528 	}
529 
530 
531 	@Override
532 	public int hashCode()
533 	{
534 		return super.hashCode();
535 	}
536 
537 
538 	@Test
539 	public void handleForOverridenMethod() throws Throwable
540 	{
541 		Method method = Methods.getMethod(MethodsTest.class, "hashCode").get();
542 		MethodHandle handle = Methods.handleFor(method).bindTo(this);
543 
544 		assertThat(handle.invoke(), is(equalTo(hashCode())));
545 	}
546 
547 
548 	@Test
549 	public void handleForInheritedMethod() throws Throwable
550 	{
551 		Method method = Methods.getMethod(MethodsTest.class, "equals", Object.class).get();
552 		MethodHandle handle = Methods.handleFor(method).bindTo(this);
553 
554 		assertThat(handle.invokeWithArguments(this), is(true));
555 		assertThat(handle.invokeWithArguments(new Object[] { null }), is(false));
556 	}
557 }