OSDN Git Service

LayoutLib: import of the GB layoutlib.
[android-x86/frameworks-base.git] / tools / layoutlib / create / tests / com / android / tools / layoutlib / create / DelegateClassAdapterTest.java
1 /*
2  * Copyright (C) 2010 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16
17 package com.android.tools.layoutlib.create;
18
19
20 import static org.junit.Assert.assertEquals;
21 import static org.junit.Assert.assertFalse;
22 import static org.junit.Assert.assertNotNull;
23 import static org.junit.Assert.assertSame;
24 import static org.junit.Assert.assertTrue;
25 import static org.junit.Assert.fail;
26
27 import com.android.tools.layoutlib.create.dataclass.ClassWithNative;
28 import com.android.tools.layoutlib.create.dataclass.OuterClass;
29 import com.android.tools.layoutlib.create.dataclass.OuterClass.InnerClass;
30
31 import org.junit.Before;
32 import org.junit.Test;
33 import org.objectweb.asm.ClassReader;
34 import org.objectweb.asm.ClassVisitor;
35 import org.objectweb.asm.ClassWriter;
36
37 import java.io.IOException;
38 import java.io.PrintWriter;
39 import java.io.StringWriter;
40 import java.lang.annotation.Annotation;
41 import java.lang.reflect.Constructor;
42 import java.lang.reflect.InvocationTargetException;
43 import java.lang.reflect.Method;
44 import java.lang.reflect.Modifier;
45 import java.util.HashMap;
46 import java.util.HashSet;
47 import java.util.Map;
48 import java.util.Map.Entry;
49 import java.util.Set;
50
51 public class DelegateClassAdapterTest {
52
53     private MockLog mLog;
54
55     private static final String NATIVE_CLASS_NAME = ClassWithNative.class.getCanonicalName();
56     private static final String OUTER_CLASS_NAME = OuterClass.class.getCanonicalName();
57     private static final String INNER_CLASS_NAME = OuterClass.class.getCanonicalName() + "$" +
58                                                    InnerClass.class.getSimpleName();
59
60     @Before
61     public void setUp() throws Exception {
62         mLog = new MockLog();
63         mLog.setVerbose(true); // capture debug error too
64     }
65
66     /**
67      * Tests that a class not being modified still works.
68      */
69     @SuppressWarnings("unchecked")
70     @Test
71     public void testNoOp() throws Throwable {
72         // create an instance of the class that will be modified
73         // (load the class in a distinct class loader so that we can trash its definition later)
74         ClassLoader cl1 = new ClassLoader(this.getClass().getClassLoader()) { };
75         Class<ClassWithNative> clazz1 = (Class<ClassWithNative>) cl1.loadClass(NATIVE_CLASS_NAME);
76         ClassWithNative instance1 = clazz1.newInstance();
77         assertEquals(42, instance1.add(20, 22));
78         try {
79             instance1.callNativeInstance(10, 3.1415, new Object[0] );
80             fail("Test should have failed to invoke callTheNativeMethod [1]");
81         } catch (UnsatisfiedLinkError e) {
82             // This is expected to fail since the native method is not implemented.
83         }
84
85         // Now process it but tell the delegate to not modify any method
86         ClassWriter cw = new ClassWriter(0 /*flags*/);
87
88         HashSet<String> delegateMethods = new HashSet<String>();
89         String internalClassName = NATIVE_CLASS_NAME.replace('.', '/');
90         DelegateClassAdapter cv = new DelegateClassAdapter(
91                 mLog, cw, internalClassName, delegateMethods);
92
93         ClassReader cr = new ClassReader(NATIVE_CLASS_NAME);
94         cr.accept(cv, 0 /* flags */);
95
96         // Load the generated class in a different class loader and try it again
97
98         ClassLoader2 cl2 = null;
99         try {
100             cl2 = new ClassLoader2() {
101                 @Override
102                 public void testModifiedInstance() throws Exception {
103                     Class<?> clazz2 = loadClass(NATIVE_CLASS_NAME);
104                     Object i2 = clazz2.newInstance();
105                     assertNotNull(i2);
106                     assertEquals(42, callAdd(i2, 20, 22));
107
108                     try {
109                         callCallNativeInstance(i2, 10, 3.1415, new Object[0]);
110                         fail("Test should have failed to invoke callTheNativeMethod [2]");
111                     } catch (InvocationTargetException e) {
112                         // This is expected to fail since the native method has NOT been
113                         // overridden here.
114                         assertEquals(UnsatisfiedLinkError.class, e.getCause().getClass());
115                     }
116
117                     // Check that the native method does NOT have the new annotation
118                     Method[] m = clazz2.getDeclaredMethods();
119                     assertEquals("native_instance", m[2].getName());
120                     assertTrue(Modifier.isNative(m[2].getModifiers()));
121                     Annotation[] a = m[2].getAnnotations();
122                     assertEquals(0, a.length);
123                 }
124             };
125             cl2.add(NATIVE_CLASS_NAME, cw);
126             cl2.testModifiedInstance();
127         } catch (Throwable t) {
128             throw dumpGeneratedClass(t, cl2);
129         }
130     }
131
132     /**
133      * {@link DelegateMethodAdapter} does not support overriding constructors yet,
134      * so this should fail with an {@link UnsupportedOperationException}.
135      *
136      * Although not tested here, the message of the exception should contain the
137      * constructor signature.
138      */
139     @Test(expected=UnsupportedOperationException.class)
140     public void testConstructorsNotSupported() throws IOException {
141         ClassWriter cw = new ClassWriter(0 /*flags*/);
142
143         String internalClassName = NATIVE_CLASS_NAME.replace('.', '/');
144
145         HashSet<String> delegateMethods = new HashSet<String>();
146         delegateMethods.add("<init>");
147         DelegateClassAdapter cv = new DelegateClassAdapter(
148                 mLog, cw, internalClassName, delegateMethods);
149
150         ClassReader cr = new ClassReader(NATIVE_CLASS_NAME);
151         cr.accept(cv, 0 /* flags */);
152     }
153
154     @Test
155     public void testDelegateNative() throws Throwable {
156         ClassWriter cw = new ClassWriter(0 /*flags*/);
157         String internalClassName = NATIVE_CLASS_NAME.replace('.', '/');
158
159         HashSet<String> delegateMethods = new HashSet<String>();
160         delegateMethods.add(DelegateClassAdapter.ALL_NATIVES);
161         DelegateClassAdapter cv = new DelegateClassAdapter(
162                 mLog, cw, internalClassName, delegateMethods);
163
164         ClassReader cr = new ClassReader(NATIVE_CLASS_NAME);
165         cr.accept(cv, 0 /* flags */);
166
167         // Load the generated class in a different class loader and try it
168         ClassLoader2 cl2 = null;
169         try {
170             cl2 = new ClassLoader2() {
171                 @Override
172                 public void testModifiedInstance() throws Exception {
173                     Class<?> clazz2 = loadClass(NATIVE_CLASS_NAME);
174                     Object i2 = clazz2.newInstance();
175                     assertNotNull(i2);
176
177                     // Use reflection to access inner methods
178                     assertEquals(42, callAdd(i2, 20, 22));
179
180                      Object[] objResult = new Object[] { null };
181                      int result = callCallNativeInstance(i2, 10, 3.1415, objResult);
182                      assertEquals((int)(10 + 3.1415), result);
183                      assertSame(i2, objResult[0]);
184
185                      // Check that the native method now has the new annotation and is not native
186                      Method[] m = clazz2.getDeclaredMethods();
187                      assertEquals("native_instance", m[2].getName());
188                      assertFalse(Modifier.isNative(m[2].getModifiers()));
189                      Annotation[] a = m[2].getAnnotations();
190                      assertEquals("LayoutlibDelegate", a[0].annotationType().getSimpleName());
191                 }
192             };
193             cl2.add(NATIVE_CLASS_NAME, cw);
194             cl2.testModifiedInstance();
195         } catch (Throwable t) {
196             throw dumpGeneratedClass(t, cl2);
197         }
198     }
199
200     @Test
201     public void testDelegateInner() throws Throwable {
202         // We'll delegate the "get" method of both the inner and outer class.
203         HashSet<String> delegateMethods = new HashSet<String>();
204         delegateMethods.add("get");
205
206         // Generate the delegate for the outer class.
207         ClassWriter cwOuter = new ClassWriter(0 /*flags*/);
208         String outerClassName = OUTER_CLASS_NAME.replace('.', '/');
209         DelegateClassAdapter cvOuter = new DelegateClassAdapter(
210                 mLog, cwOuter, outerClassName, delegateMethods);
211         ClassReader cr = new ClassReader(OUTER_CLASS_NAME);
212         cr.accept(cvOuter, 0 /* flags */);
213
214         // Generate the delegate for the inner class.
215         ClassWriter cwInner = new ClassWriter(0 /*flags*/);
216         String innerClassName = INNER_CLASS_NAME.replace('.', '/');
217         DelegateClassAdapter cvInner = new DelegateClassAdapter(
218                 mLog, cwInner, innerClassName, delegateMethods);
219         cr = new ClassReader(INNER_CLASS_NAME);
220         cr.accept(cvInner, 0 /* flags */);
221
222         // Load the generated classes in a different class loader and try them
223         ClassLoader2 cl2 = null;
224         try {
225             cl2 = new ClassLoader2() {
226                 @Override
227                 public void testModifiedInstance() throws Exception {
228
229                     // Check the outer class
230                     Class<?> outerClazz2 = loadClass(OUTER_CLASS_NAME);
231                     Object o2 = outerClazz2.newInstance();
232                     assertNotNull(o2);
233
234                     // The original Outer.get returns 1+10+20,
235                     // but the delegate makes it return 4+10+20
236                     assertEquals(4+10+20, callGet(o2, 10, 20));
237
238                     // Check the inner class. Since it's not a static inner class, we need
239                     // to use the hidden constructor that takes the outer class as first parameter.
240                     Class<?> innerClazz2 = loadClass(INNER_CLASS_NAME);
241                     Constructor<?> innerCons = innerClazz2.getConstructor(
242                                                                 new Class<?>[] { outerClazz2 });
243                     Object i2 = innerCons.newInstance(new Object[] { o2 });
244                     assertNotNull(i2);
245
246                     // The original Inner.get returns 3+10+20,
247                     // but the delegate makes it return 6+10+20
248                     assertEquals(6+10+20, callGet(i2, 10, 20));
249                 }
250             };
251             cl2.add(OUTER_CLASS_NAME, cwOuter.toByteArray());
252             cl2.add(INNER_CLASS_NAME, cwInner.toByteArray());
253             cl2.testModifiedInstance();
254         } catch (Throwable t) {
255             throw dumpGeneratedClass(t, cl2);
256         }
257     }
258
259     //-------
260
261     /**
262      * A class loader than can define and instantiate our modified classes.
263      * <p/>
264      * The trick here is that this class loader will test our <em>modified</em> version
265      * of the classes, the one with the delegate calls.
266      * <p/>
267      * Trying to do so in the original class loader generates all sort of link issues because
268      * there are 2 different definitions of the same class name. This class loader will
269      * define and load the class when requested by name and provide helpers to access the
270      * instance methods via reflection.
271      */
272     private abstract class ClassLoader2 extends ClassLoader {
273
274         private final Map<String, byte[]> mClassDefs = new HashMap<String, byte[]>();
275
276         public ClassLoader2() {
277             super(null);
278         }
279
280         public ClassLoader2 add(String className, byte[] definition) {
281             mClassDefs.put(className, definition);
282             return this;
283         }
284
285         public ClassLoader2 add(String className, ClassWriter rewrittenClass) {
286             mClassDefs.put(className, rewrittenClass.toByteArray());
287             return this;
288         }
289
290         private Set<Entry<String, byte[]>> getByteCode() {
291             return mClassDefs.entrySet();
292         }
293
294         @SuppressWarnings("unused")
295         @Override
296         protected Class<?> findClass(String name) throws ClassNotFoundException {
297             try {
298                 return super.findClass(name);
299             } catch (ClassNotFoundException e) {
300
301                 byte[] def = mClassDefs.get(name);
302                 if (def != null) {
303                     // Load the modified ClassWithNative from its bytes representation.
304                     return defineClass(name, def, 0, def.length);
305                 }
306
307                 try {
308                     // Load everything else from the original definition into the new class loader.
309                     ClassReader cr = new ClassReader(name);
310                     ClassWriter cw = new ClassWriter(0);
311                     cr.accept(cw, 0);
312                     byte[] bytes = cw.toByteArray();
313                     return defineClass(name, bytes, 0, bytes.length);
314
315                 } catch (IOException ioe) {
316                     throw new RuntimeException(ioe);
317                 }
318             }
319         }
320
321         /**
322          * Accesses {@link OuterClass#get()} or {@link InnerClass#get() }via reflection.
323          */
324         public int callGet(Object instance, int a, long b) throws Exception {
325             Method m = instance.getClass().getMethod("get",
326                     new Class<?>[] { int.class, long.class } );
327
328             Object result = m.invoke(instance, new Object[] { a, b });
329             return ((Integer) result).intValue();
330         }
331
332         /**
333          * Accesses {@link ClassWithNative#add(int, int)} via reflection.
334          */
335         public int callAdd(Object instance, int a, int b) throws Exception {
336             Method m = instance.getClass().getMethod("add",
337                     new Class<?>[] { int.class, int.class });
338
339             Object result = m.invoke(instance, new Object[] { a, b });
340             return ((Integer) result).intValue();
341         }
342
343         /**
344          * Accesses {@link ClassWithNative#callNativeInstance(int, double, Object[])}
345          * via reflection.
346          */
347         public int callCallNativeInstance(Object instance, int a, double d, Object[] o)
348                 throws Exception {
349             Method m = instance.getClass().getMethod("callNativeInstance",
350                     new Class<?>[] { int.class, double.class, Object[].class });
351
352             Object result = m.invoke(instance, new Object[] { a, d, o });
353             return ((Integer) result).intValue();
354         }
355
356         public abstract void testModifiedInstance() throws Exception;
357     }
358
359     /**
360      * For debugging, it's useful to dump the content of the generated classes
361      * along with the exception that was generated.
362      *
363      * However to make it work you need to pull in the org.objectweb.asm.util.TraceClassVisitor
364      * class and associated utilities which are found in the ASM source jar. Since we don't
365      * want that dependency in the source code, we only put it manually for development and
366      * access the TraceClassVisitor via reflection if present.
367      *
368      * @param t The exception thrown by {@link ClassLoader2#testModifiedInstance()}
369      * @param cl2 The {@link ClassLoader2} instance with the generated bytecode.
370      * @return Either original {@code t} or a new wrapper {@link Throwable}
371      */
372     private Throwable dumpGeneratedClass(Throwable t, ClassLoader2 cl2) {
373         try {
374             // For debugging, dump the bytecode of the class in case of unexpected error
375             // if we can find the TraceClassVisitor class.
376             Class<?> tcvClass = Class.forName("org.objectweb.asm.util.TraceClassVisitor");
377
378             StringBuilder sb = new StringBuilder();
379             sb.append('\n').append(t.getClass().getCanonicalName());
380             if (t.getMessage() != null) {
381                 sb.append(": ").append(t.getMessage());
382               }
383
384             for (Entry<String, byte[]> entry : cl2.getByteCode()) {
385                 String className = entry.getKey();
386                 byte[] bytes = entry.getValue();
387
388                 StringWriter sw = new StringWriter();
389                 PrintWriter pw = new PrintWriter(sw);
390                 // next 2 lines do: TraceClassVisitor tcv = new TraceClassVisitor(pw);
391                 Constructor<?> cons = tcvClass.getConstructor(new Class<?>[] { pw.getClass() });
392                 Object tcv = cons.newInstance(new Object[] { pw });
393                 ClassReader cr2 = new ClassReader(bytes);
394                 cr2.accept((ClassVisitor) tcv, 0 /* flags */);
395
396                 sb.append("\nBytecode dump: <").append(className).append(">:\n")
397                   .append(sw.toString());
398             }
399
400             // Re-throw exception with new message
401             RuntimeException ex = new RuntimeException(sb.toString(), t);
402             return ex;
403         } catch (Throwable ignore) {
404             // In case of problem, just throw the original exception as-is.
405             return t;
406         }
407     }
408
409 }