1/*
2 * Copyright (C) 2008 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
18package com.android.tools.layoutlib.create;
19
20
21import static org.junit.Assert.assertArrayEquals;
22import static org.junit.Assert.assertFalse;
23import static org.junit.Assert.assertTrue;
24
25import org.junit.After;
26import org.junit.Before;
27import org.junit.Test;
28import org.objectweb.asm.ClassReader;
29import org.objectweb.asm.ClassVisitor;
30import org.objectweb.asm.FieldVisitor;
31import org.objectweb.asm.MethodVisitor;
32import org.objectweb.asm.Opcodes;
33import org.objectweb.asm.Type;
34
35import java.io.File;
36import java.io.IOException;
37import java.io.InputStream;
38import java.net.URL;
39import java.util.ArrayList;
40import java.util.Collections;
41import java.util.Enumeration;
42import java.util.HashSet;
43import java.util.Map;
44import java.util.Set;
45import java.util.TreeMap;
46import java.util.zip.ZipEntry;
47import java.util.zip.ZipFile;
48
49/**
50 * Unit tests for some methods of {@link AsmGenerator}.
51 */
52public class AsmGeneratorTest {
53
54    private MockLog mLog;
55    private ArrayList<String> mOsJarPath;
56    private String mOsDestJar;
57    private File mTempFile;
58
59    // ASM internal name for the the class in java package that should be refactored.
60    private static final String JAVA_CLASS_NAME = "java/lang/JavaClass";
61
62    @Before
63    public void setUp() throws Exception {
64        mLog = new MockLog();
65        URL url = this.getClass().getClassLoader().getResource("data/mock_android.jar");
66
67        mOsJarPath = new ArrayList<String>();
68        mOsJarPath.add(url.getFile());
69
70        mTempFile = File.createTempFile("mock", ".jar");
71        mOsDestJar = mTempFile.getAbsolutePath();
72        mTempFile.deleteOnExit();
73    }
74
75    @After
76    public void tearDown() throws Exception {
77        if (mTempFile != null) {
78            mTempFile.delete();
79            mTempFile = null;
80        }
81    }
82
83    @Test
84    public void testClassRenaming() throws IOException, LogAbortException {
85
86        ICreateInfo ci = new ICreateInfo() {
87            @Override
88            public Class<?>[] getInjectedClasses() {
89                // classes to inject in the final JAR
90                return new Class<?>[0];
91            }
92
93            @Override
94            public String[] getDelegateMethods() {
95                return new String[0];
96            }
97
98            @Override
99            public String[] getDelegateClassNatives() {
100                return new String[0];
101            }
102
103            @Override
104            public String[] getOverriddenMethods() {
105                // methods to force override
106                return new String[0];
107            }
108
109            @Override
110            public String[] getRenamedClasses() {
111                // classes to rename (so that we can replace them)
112                return new String[] {
113                        "mock_android.view.View", "mock_android.view._Original_View",
114                        "not.an.actual.ClassName", "anoter.fake.NewClassName",
115                };
116            }
117
118            @Override
119            public String[] getJavaPkgClasses() {
120              return new String[0];
121            }
122
123            @Override
124            public Set<String> getExcludedClasses() {
125                return null;
126            }
127
128            @Override
129            public String[] getDeleteReturns() {
130                 // methods deleted from their return type.
131                return new String[0];
132            }
133        };
134
135        AsmGenerator agen = new AsmGenerator(mLog, mOsDestJar, ci);
136
137        AsmAnalyzer aa = new AsmAnalyzer(mLog, mOsJarPath, agen,
138                null,                 // derived from
139                new String[] {        // include classes
140                    "**"
141                },
142                new HashSet<String>(0) /* excluded classes */,
143                new String[]{} /* include files */);
144        aa.analyze();
145        agen.generate();
146
147        Set<String> notRenamed = agen.getClassesNotRenamed();
148        assertArrayEquals(new String[] { "not/an/actual/ClassName" }, notRenamed.toArray());
149
150    }
151
152    @Test
153    public void testClassRefactoring() throws IOException, LogAbortException {
154        ICreateInfo ci = new ICreateInfo() {
155            @Override
156            public Class<?>[] getInjectedClasses() {
157                // classes to inject in the final JAR
158                return new Class<?>[] {
159                        com.android.tools.layoutlib.create.dataclass.JavaClass.class
160                };
161            }
162
163            @Override
164            public String[] getDelegateMethods() {
165                return new String[0];
166            }
167
168            @Override
169            public String[] getDelegateClassNatives() {
170                return new String[0];
171            }
172
173            @Override
174            public String[] getOverriddenMethods() {
175                // methods to force override
176                return new String[0];
177            }
178
179            @Override
180            public String[] getRenamedClasses() {
181                // classes to rename (so that we can replace them)
182                return new String[0];
183            }
184
185            @Override
186            public String[] getJavaPkgClasses() {
187             // classes to refactor (so that we can replace them)
188                return new String[] {
189                        "java.lang.JavaClass", "com.android.tools.layoutlib.create.dataclass.JavaClass",
190                };
191            }
192
193            @Override
194            public Set<String> getExcludedClasses() {
195                return Collections.singleton("java.lang.JavaClass");
196            }
197
198            @Override
199            public String[] getDeleteReturns() {
200                 // methods deleted from their return type.
201                return new String[0];
202            }
203        };
204
205        AsmGenerator agen = new AsmGenerator(mLog, mOsDestJar, ci);
206
207        AsmAnalyzer aa = new AsmAnalyzer(mLog, mOsJarPath, agen,
208                null,                 // derived from
209                new String[] {        // include classes
210                    "**"
211                },
212                new HashSet<String>(1),
213                new String[] {        /* include files */
214                    "mock_android/data/data*"
215                });
216        aa.analyze();
217        agen.generate();
218        Map<String, ClassReader> output = new TreeMap<String, ClassReader>();
219        Map<String, InputStream> filesFound = new TreeMap<String, InputStream>();
220        parseZip(mOsDestJar, output, filesFound);
221        boolean injectedClassFound = false;
222        for (ClassReader cr: output.values()) {
223            TestClassVisitor cv = new TestClassVisitor();
224            cr.accept(cv, 0);
225            injectedClassFound |= cv.mInjectedClassFound;
226        }
227        assertTrue(injectedClassFound);
228        assertArrayEquals(new String[] {"mock_android/data/dataFile"},
229                filesFound.keySet().toArray());
230    }
231
232    @Test
233    public void testClassExclusion() throws IOException, LogAbortException {
234        ICreateInfo ci = new ICreateInfo() {
235            @Override
236            public Class<?>[] getInjectedClasses() {
237                return new Class<?>[0];
238            }
239
240            @Override
241            public String[] getDelegateMethods() {
242                return new String[0];
243            }
244
245            @Override
246            public String[] getDelegateClassNatives() {
247                return new String[0];
248            }
249
250            @Override
251            public String[] getOverriddenMethods() {
252                // methods to force override
253                return new String[0];
254            }
255
256            @Override
257            public String[] getRenamedClasses() {
258                // classes to rename (so that we can replace them)
259                return new String[0];
260            }
261
262            @Override
263            public String[] getJavaPkgClasses() {
264                // classes to refactor (so that we can replace them)
265                return new String[0];
266            }
267
268            @Override
269            public Set<String> getExcludedClasses() {
270                Set<String> set = new HashSet<String>(2);
271                set.add("mock_android.dummy.InnerTest");
272                set.add("java.lang.JavaClass");
273                return set;
274            }
275
276            @Override
277            public String[] getDeleteReturns() {
278                // methods deleted from their return type.
279                return new String[0];
280            }
281        };
282
283        AsmGenerator agen = new AsmGenerator(mLog, mOsDestJar, ci);
284        Set<String> excludedClasses = ci.getExcludedClasses();
285        AsmAnalyzer aa = new AsmAnalyzer(mLog, mOsJarPath, agen,
286                null,                 // derived from
287                new String[] {        // include classes
288                        "**"
289                },
290                excludedClasses,
291                new String[] {        /* include files */
292                        "mock_android/data/data*"
293                });
294        aa.analyze();
295        agen.generate();
296        Map<String, ClassReader> output = new TreeMap<String, ClassReader>();
297        Map<String, InputStream> filesFound = new TreeMap<String, InputStream>();
298        parseZip(mOsDestJar, output, filesFound);
299        for (String s : output.keySet()) {
300            assertFalse(excludedClasses.contains(s));
301        }
302        assertArrayEquals(new String[] {"mock_android/data/dataFile"},
303                filesFound.keySet().toArray());
304    }
305
306    private void parseZip(String jarPath,
307            Map<String, ClassReader> classes,
308            Map<String, InputStream> filesFound) throws IOException {
309
310            ZipFile zip = new ZipFile(jarPath);
311            Enumeration<? extends ZipEntry> entries = zip.entries();
312            ZipEntry entry;
313            while (entries.hasMoreElements()) {
314                entry = entries.nextElement();
315                if (entry.getName().endsWith(".class")) {
316                    ClassReader cr = new ClassReader(zip.getInputStream(entry));
317                    String className = classReaderToClassName(cr);
318                    classes.put(className, cr);
319                } else {
320                    filesFound.put(entry.getName(), zip.getInputStream(entry));
321                }
322            }
323
324    }
325
326    private String classReaderToClassName(ClassReader classReader) {
327        if (classReader == null) {
328            return null;
329        } else {
330            return classReader.getClassName().replace('/', '.');
331        }
332    }
333
334    private class TestClassVisitor extends ClassVisitor {
335
336        boolean mInjectedClassFound = false;
337
338        TestClassVisitor() {
339            super(Opcodes.ASM4);
340        }
341
342        @Override
343        public void visit(int version, int access, String name, String signature,
344                String superName, String[] interfaces) {
345            assertTrue(!getBase(name).equals(JAVA_CLASS_NAME));
346            if (name.equals("com/android/tools/layoutlib/create/dataclass/JavaClass")) {
347                mInjectedClassFound = true;
348            }
349            super.visit(version, access, name, signature, superName, interfaces);
350        }
351
352        @Override
353        public FieldVisitor visitField(int access, String name, String desc,
354                String signature, Object value) {
355            assertTrue(testType(Type.getType(desc)));
356            return super.visitField(access, name, desc, signature, value);
357        }
358
359        @SuppressWarnings("hiding")
360        @Override
361        public MethodVisitor visitMethod(int access, String name, String desc,
362                String signature, String[] exceptions) {
363            MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions);
364            return new MethodVisitor(Opcodes.ASM4, mv) {
365
366                @Override
367                public void visitFieldInsn(int opcode, String owner, String name,
368                        String desc) {
369                    assertTrue(!getBase(owner).equals(JAVA_CLASS_NAME));
370                    assertTrue(testType(Type.getType(desc)));
371                    super.visitFieldInsn(opcode, owner, name, desc);
372                }
373
374                @Override
375                public void visitLdcInsn(Object cst) {
376                    if (cst instanceof Type) {
377                        assertTrue(testType((Type)cst));
378                    }
379                    super.visitLdcInsn(cst);
380                }
381
382                @Override
383                public void visitTypeInsn(int opcode, String type) {
384                    assertTrue(!getBase(type).equals(JAVA_CLASS_NAME));
385                    super.visitTypeInsn(opcode, type);
386                }
387
388                @Override
389                public void visitMethodInsn(int opcode, String owner, String name,
390                        String desc) {
391                    assertTrue(!getBase(owner).equals(JAVA_CLASS_NAME));
392                    assertTrue(testType(Type.getType(desc)));
393                    super.visitMethodInsn(opcode, owner, name, desc);
394                }
395
396            };
397        }
398
399        private boolean testType(Type type) {
400            int sort = type.getSort();
401            if (sort == Type.OBJECT) {
402                assertTrue(!getBase(type.getInternalName()).equals(JAVA_CLASS_NAME));
403            } else if (sort == Type.ARRAY) {
404                assertTrue(!getBase(type.getElementType().getInternalName())
405                        .equals(JAVA_CLASS_NAME));
406            } else if (sort == Type.METHOD) {
407                boolean r = true;
408                for (Type t : type.getArgumentTypes()) {
409                    r &= testType(t);
410                }
411                return r & testType(type.getReturnType());
412            }
413            return true;
414        }
415
416        private String getBase(String className) {
417            if (className == null) {
418                return null;
419            }
420            int pos = className.indexOf('$');
421            if (pos > 0) {
422                return className.substring(0, pos);
423            }
424            return className;
425        }
426    }
427}
428