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